经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Node.js » 查看文章
Node 搭建一个静态资源服务器的实现
来源:jb51  时间:2019/5/21 9:06:10  对本文有异议

使用 Node 的内置模块,创建一个可以访问目录的静态资源服务器,支持fs文件读取,资源压缩与缓存等。

一、创建 HTTP Server 服务器

Node 的 http 模块提供 HTTP 服务器和客户端接口,通过 require('http') 使用。

先创建一个简单的 http server。配置参数如下:

  1. // server/config.js
  2. module.exports = {
  3. root: process.cwd(),
  4. host: '127.0.0.1',
  5. port: '8877'
  6. }

process.cwd()方法返回 Node.js 进程的当前工作目录,和 Linus 命令 pwd 功能一样,

Node 服务器每次收到 HTTP 请求后都会调用 http.createServer() 这个回调函数,每次收一条请求,都会先解析请求头作为新的 request 的一部分,然后用新的 request 和 respond 对象触发回调函数。以下创建一个简单的 http 服务,先默认响应的 status 为 200:

  1. // server/http.js
  2. const http = require('http')
  3. const path = require('path')
  4.  
  5. const config = require('./config')
  6.  
  7. const server = http.createServer((request, response) => {
  8. let filePath = path.join(config.root, request.url)
  9. response.statusCode = 200
  10. response.setHeader('content-type', 'text/html')
  11. response.write(`<html><body><h1>Hello World! </h1><p>${filePath}</p></body></html>`)
  12. response.end()
  13. })
  14.  
  15. server.listen(config.port, config.host, () => {
  16. const addr = `http://${config.host}:${config.port}`
  17. console.info(`server started at ${addr}`)
  18. })

客户端请求静态资源的地址可以通过 request.url 获得,然后使用 path 模块拼接资源的路径。

执行 $ node server/http.js 后访问 http://127.0.0.1 :8877/ 后的任意地址都会显示该路径:

每次修改服务器响应内容,都需要重新启动服务器更新,推荐自动监视更新自动重启的插件supervisor,使用supervisor启动服务器。

  1. $ npm install supervisor -D
  2. $ supervisor server/http.js

二、使用 fs 读取资源文件

我们的目的是搭建一个静态资源服务器,当访问一个到资源文件或目录时,我们希望可以得到它。这时就需要使用 Node 内置的 fs 模块读取静态资源文件,

使用 fs.stat() 读取文件状态信息,通过回调中的状态 stats.isFile() 判断文件还是目录,并使用 fs.readdir() 读取目录中的文件名

  1. // server/route.js
  2. const fs = require('fs')
  3.  
  4. module.exports = function (request, response, filePath){
  5. fs.stat(filePath, (err, stats) => {
  6. if (err) {
  7. response.statusCode = 404
  8. response.setHeader('content-type', 'text/plain')
  9. response.end(`${filePath} is not a file`)
  10. return;
  11. }
  12. if (stats.isFile()) {
  13. response.statusCode = 200
  14. response.setHeader('content-type', 'text/plain')
  15. fs.createReadStream(filePath).pipe(response)
  16. }
  17. else if (stats.isDirectory()) {
  18. fs.readdir(filePath, (err, files) => {
  19. response.statusCode = 200
  20. response.setHeader('content-type', 'text/plain')
  21. response.end(files.join(','))
  22. })
  23. }
  24. })
  25. }

其中 fs.createReadStream() 读取文件流, pipe() 是分段读取文件到内存,优化高并发的情况。

修改之前的 http server ,引入上面新建的 route.js 作为响应函数:

  1. // server/http.js
  2. const http = require('http')
  3. const path = require('path')
  4.  
  5. const config = require('./config')
  6. const route = require('./route')
  7.  
  8. const server = http.createServer((request, response) => {
  9. let filePath = path.join(config.root, request.url)
  10. route(request, response, filePath)
  11. })
  12.  
  13. server.listen(config.port, config.host, () => {
  14. const addr = `http://${config.host}:${config.port}`
  15. console.info(`server started at ${addr}`)
  16. })

再次执行 $ node server/http.js 如果是文件夹则显示目录:

如果是文件则直接输出:

成熟的静态资源服务器 anywhere,深入理解 nodejs 作者写的。

三、util.promisify 优化 fs 异步

我们注意到 fs.stat()fs.readdir() 都有 callback 回调。我们结合 Node 的 util.promisify() 来链式操作,代替地狱回调。

util.promisify 只是返回一个 Promise 实例来方便异步操作,并且可以和 async/await 配合使用,修改 route.js 中 fs 操作相关的代码:

  1. // server/route.js
  2. const fs = require('fs')
  3. const util = require('util')
  4.  
  5. const stat = util.promisify(fs.stat)
  6. const readdir = util.promisify(fs.readdir)
  7.  
  8. module.exports = async function (request, response, filePath) {
  9. try {
  10. const stats = await stat(filePath)
  11. if (stats.isFile()) {
  12. response.statusCode = 200
  13. response.setHeader('content-type', 'text/plain')
  14. fs.createReadStream(filePath).pipe(response)
  15. }
  16. else if (stats.isDirectory()) {
  17. const files = await readdir(filePath)
  18. response.statusCode = 200
  19. response.setHeader('content-type', 'text/plain')
  20. response.end(files.join(','))
  21. }
  22. } catch (err) {
  23. console.error(err)
  24. response.statusCode = 404
  25. response.setHeader('content-type', 'text/plain')
  26. response.end(`${filePath} is not a file`)
  27. }
  28. }

因为 fs.stat()fs.readdir() 都可能返回 error,所以使用 try-catch 捕获。

使用异步时需注意,异步回调需要使用 await 返回异步操作,不加 await 返回的是一个 promise,而且 await 必须在async里面使用。

四、添加模版引擎

从上面的例子是手工输入文件路径,然后返回资源文件。现在优化这个例子,将文件目录变成 html 的 a 链接,点击后返回文件资源。

在第一个例子中使用 response.write() 插入 HTML 标签,这种方式显然是不友好的。这时候就使用模版引擎做到拼接 HTML。

常用的模版引擎有很多,ejs、jade、handlebars,这里的使用ejs:

  1. npm i ejs

新建一个模版 src/template/index.ejs ,和 html 文件很像:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Node Server</title>
  6. </head>
  7. <body>
  8. <% files.forEach(function(name){ %>
  9. <a href="../<%= dir %>/<%= name %>" rel="external nofollow" > <%= name %></a><br>
  10. <% }) %>
  11. </body>
  12. </html>

再次修改 route.js,添加 ejs 模版并 ejs.render() ,在文件目录的代码中传递 files、dir 等参数:

  1. // server/route.js
  2.  
  3. const fs = require('fs')
  4. const util = require('util')
  5. const path = require('path')
  6. const ejs = require('ejs')
  7. const config = require('./config')
  8. // 异步优化
  9. const stat = util.promisify(fs.stat)
  10. const readdir = util.promisify(fs.readdir)
  11. // 引入模版
  12. const tplPath = path.join(__dirname,'../src/template/index.ejs')
  13. const sourse = fs.readFileSync(tplPath) // 读出来的是buffer
  14.  
  15. module.exports = async function (request, response, filePath) {
  16. try {
  17. const stats = await stat(filePath)
  18. if (stats.isFile()) {
  19. response.statusCode = 200
  20. ···
  21. }
  22. else if (stats.isDirectory()) {
  23. const files = await readdir(filePath)
  24. response.statusCode = 200
  25. response.setHeader('content-type', 'text/html')
  26. // response.end(files.join(','))
  27.  
  28. const dir = path.relative(config.root, filePath) // 相对于根目录
  29. const data = {
  30. files,
  31. dir: dir ? `${dir}` : '' // path.relative可能返回空字符串()
  32. }
  33.  
  34. const template = ejs.render(sourse.toString(),data)
  35. response.end(template)
  36. }
  37. } catch (err) {
  38. response.statusCode = 404
  39. ···
  40. }
  41. }

重启动 $ node server/http.js 就可以看到文件目录的链接:

五、匹配文件 MIME 类型

静态资源有图片、css、js、json、html等,

在上面判断 stats.isFile() 后响应头设置的 Content-Type 都为 text/plain,但各种文件有不同的 Mime 类型列表。

我们先根据文件的后缀匹配它的 MIME 类型:

  1. // server/mime.js
  2. const path = require('path')
  3. const mimeTypes = {
  4. 'js': 'application/x-javascript',
  5. 'html': 'text/html',
  6. 'css': 'text/css',
  7. 'txt': "text/plain"
  8. }
  9.  
  10. module.exports = (filePath) => {
  11. let ext = path.extname(filePath)
  12. .split('.').pop().toLowerCase() // 取扩展名
  13.  
  14. if (!ext) { // 如果没有扩展名,例如是文件
  15. ext = filePath
  16. }
  17. return mimeTypes[ext] || mimeTypes['txt']
  18. }

匹配到文件的 MIME 类型,再使用 response.setHeader('Content-Type', 'XXX') 设置响应头:

  1. // server/route.js
  2. const mime = require('./mime')
  3. ···
  4. if (stats.isFile()) {
  5. const mimeType = mime(filePath)
  6. response.statusCode = 200
  7. response.setHeader('Content-Type', mimeType)
  8. fs.createReadStream(filePath).pipe(response)
  9. }

运行 server 服务器访问一个文件,可以看到 Content-Type 修改了:

六、文件传输压缩

注意到 request header 中有 Accept—Encoding:gzip,deflate,告诉服务器客户端所支持的压缩方式,响应时 response header 中使用 content-Encoding 标志文件的压缩方式。

node 内置 zlib 模块支持文件压缩。在前面文件读取使用的是 fs.createReadStream() ,所以压缩是对 ReadStream 文件流。示例 gzip,deflate 方式的压缩:

最常用文件压缩,gzip等,使用,对于文件是用ReadStream文件流进行读取的,所以对ReadStream进行压缩:

  1. // server/compress.js
  2. const zlib = require('zlib')
  3.  
  4. module.exports = (readStream, request, response) => {
  5. const acceptEncoding = request.headers['accept-encoding']
  6. if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
  7. return readStream
  8. }
  9. else if (acceptEncoding.match(/\bgzip\b/)) {
  10. response.setHeader("Content-Encoding", 'gzip')
  11. return readStream.pipe(zlib.createGzip())
  12. }
  13. else if (acceptEncoding.match(/\bdeflate\b/)) {
  14. response.setHeader("Content-Encoding", 'deflate')
  15. return readStream.pipe(zlib.createDeflate())
  16. }
  17. }

修改 route.js 文件读取的代码:

  1. // server/route.js
  2. const compress = require('./compress')
  3. ···
  4. if (stats.isFile()) {
  5. const mimeType = mime(filePath)
  6. response.statusCode = 200
  7. response.setHeader('Content-Type', mimeType)
  8. // fs.createReadStream(filePath).pipe(response)
  9. + let readStream = fs.createReadStream(filePath)
  10. + if(filePath.match(config.compress)) { // 正则匹配:/\.(html|js|css|md)/
  11. readStream = compress(readStream,request, response)
  12. }
  13. readStream.pipe(response)
  14. }

运行 server 可以看到不仅 response header 增加压缩标志,而且 3K 大小的资源压缩到了 1K,效果明显:

七、资源缓存

以上的 Node 服务都是浏览器首次请求或无缓存状态下的,那如果浏览器/客户端请求过资源,一个重要的前端优化点就是缓存资源在客户端。 缓存有强缓存和协商缓存

强缓存在 Request Header 中的字段是 Expires 和 Cache-Control;如果在有效期内则直接加载缓存资源,状态码直接是显示 200。

协商缓存在 Request Header 中的字段是:

  • If-Modified-Since(对应值为上次 Respond Header 中的 Last-Modified)
  • If-None—Match(对应值为上次 Respond Header 中的 Etag)

如果协商成功则返回 304 状态码,更新过期时间并加载浏览器本地资源,否则返回服务器端资源文件。

首先配置默认的 cache 字段:

  1. // server/config.js
  2. module.exports = {
  3. root: process.cwd(),
  4. host: '127.0.0.1',
  5. port: '8877',
  6. compress: /\.(html|js|css|md)/,
  7. cache: {
  8. maxAge: 2,
  9. expires: true,
  10. cacheControl: true,
  11. lastModified: true,
  12. etag: true
  13. }
  14. }

新建 server/cache.js,设置响应头:

  1. const config = require('./config')
  2. function refreshRes (stats, response) {
  3. const {maxAge, expires, cacheControl, lastModified, etag} = config.cache;
  4.  
  5. if (expires) {
  6. response.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
  7. }
  8. if (cacheControl) {
  9. response.setHeader('Cache-Control', `public, max-age=${maxAge}`);
  10. }
  11. if (lastModified) {
  12. response.setHeader('Last-Modified', stats.mtime.toUTCString());
  13. }
  14. if (etag) {
  15. response.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`); // mtime 需要转成字符串,否则在 windows 环境下会报错
  16. }
  17. }
  18.  
  19. module.exports = function isFresh (stats, request, response) {
  20. refreshRes(stats, response);
  21.  
  22. const lastModified = request.headers['if-modified-since'];
  23. const etag = request.headers['if-none-match'];
  24.  
  25. if (!lastModified && !etag) {
  26. return false;
  27. }
  28. if (lastModified && lastModified !== response.getHeader('Last-Modified')) {
  29. return false;
  30. }
  31. if (etag && etag !== response.getHeader('ETag')) {
  32. return false;
  33. }
  34. return true;
  35. };

最后修改 route.js 中的

  1. // server/route.js
  2. + const isCache = require('./cache')
  3.  
  4. if (stats.isFile()) {
  5. const mimeType = mime(filePath)
  6. response.setHeader('Content-Type', mimeType)
  7.  
  8. + if (isCache(stats, request, response)) {
  9. response.statusCode = 304;
  10. response.end();
  11. return;
  12. }
  13. response.statusCode = 200
  14. // fs.createReadStream(filePath).pipe(response)
  15. let readStream = fs.createReadStream(filePath)
  16. if(filePath.match(config.compress)) {
  17. readStream = compress(readStream,request, response)
  18. }
  19. readStream.pipe(response)
  20. }

重启 node server 访问某个文件,在第一次请求成功时 Respond Header 返回缓存时间:

一段时间后再次请求该资源文件,Request Header 发送协商请求字段:

以上就是一个简单的 Node 静态资源服务器。希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号