欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Node 搭建一个静态资源服务器的实现

程序员文章站 2022-03-07 08:07:58
使用 node 的内置模块,创建一个可以访问目录的静态资源服务器,支持fs文件读取,资源压缩与缓存等。 一、创建 http server 服务器 node 的 http...

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

一、创建 http server 服务器

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

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

// server/config.js
module.exports = {
 root: process.cwd(),
 host: '127.0.0.1',
 port: '8877'
}

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

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

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')

const server = http.createserver((request, response) => {
 let filepath = path.join(config.root, request.url)
 response.statuscode = 200
 response.setheader('content-type', 'text/html')
 response.write(`<html><body><h1>hello world! </h1><p>${filepath}</p></body></html>`)
 response.end()
})

server.listen(config.port, config.host, () => {
 const addr = `http://${config.host}:${config.port}`
 console.info(`server started at ${addr}`)
})

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

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

Node 搭建一个静态资源服务器的实现

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

$ npm install supervisor -d
$ supervisor server/http.js

二、使用 fs 读取资源文件

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

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

// server/route.js
const fs = require('fs')

module.exports = function (request, response, filepath){
 fs.stat(filepath, (err, stats) => {
  if (err) {
   response.statuscode = 404
   response.setheader('content-type', 'text/plain')
   response.end(`${filepath} is not a file`)
   return;
  }
  if (stats.isfile()) {
   response.statuscode = 200
   response.setheader('content-type', 'text/plain')
   fs.createreadstream(filepath).pipe(response)
  } 
  else if (stats.isdirectory()) {
   fs.readdir(filepath, (err, files) => {
    response.statuscode = 200
    response.setheader('content-type', 'text/plain')
    response.end(files.join(','))
   })
  }
 })
}

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

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

// server/http.js
const http = require('http')
const path = require('path')

const config = require('./config')
const route = require('./route')

const server = http.createserver((request, response) => {
 let filepath = path.join(config.root, request.url)
 route(request, response, filepath)
})

server.listen(config.port, config.host, () => {
 const addr = `http://${config.host}:${config.port}`
 console.info(`server started at ${addr}`)
})

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

Node 搭建一个静态资源服务器的实现

如果是文件则直接输出:

Node 搭建一个静态资源服务器的实现

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

三、util.promisify 优化 fs 异步

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

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

// server/route.js
const fs = require('fs')
const util = require('util')

const stat = util.promisify(fs.stat)
const readdir = util.promisify(fs.readdir)

module.exports = async function (request, response, filepath) {
 try {
  const stats = await stat(filepath)
  if (stats.isfile()) {
   response.statuscode = 200
   response.setheader('content-type', 'text/plain')
   fs.createreadstream(filepath).pipe(response)
  }
  else if (stats.isdirectory()) {
   const files = await readdir(filepath)
   response.statuscode = 200
   response.setheader('content-type', 'text/plain')
   response.end(files.join(','))
  }
 } catch (err) {
  console.error(err)
  response.statuscode = 404
  response.setheader('content-type', 'text/plain')
  response.end(`${filepath} is not a file`)
 }
}

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

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

四、添加模版引擎

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

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

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

npm i ejs

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

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>node server</title>
</head>
<body>
<% files.foreach(function(name){ %>
 <a href="../<%= dir %>/<%= name %>" rel="external nofollow" > <%= name %></a><br>
<% }) %>
</body>
</html>

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

// server/route.js

const fs = require('fs')
const util = require('util')
const path = require('path')
const ejs = require('ejs')
const config = require('./config')
// 异步优化
const stat = util.promisify(fs.stat)
const readdir = util.promisify(fs.readdir)
// 引入模版
const tplpath = path.join(__dirname,'../src/template/index.ejs')
const sourse = fs.readfilesync(tplpath) // 读出来的是buffer

module.exports = async function (request, response, filepath) {
 try {
  const stats = await stat(filepath)
  if (stats.isfile()) {
   response.statuscode = 200
   ···
  }
  else if (stats.isdirectory()) {
   const files = await readdir(filepath)
   response.statuscode = 200
   response.setheader('content-type', 'text/html')
   // response.end(files.join(','))

   const dir = path.relative(config.root, filepath) // 相对于根目录
   const data = {
    files,
    dir: dir ? `${dir}` : '' // path.relative可能返回空字符串()
   }

   const template = ejs.render(sourse.tostring(),data)
   response.end(template)
  }
 } catch (err) {
  response.statuscode = 404
  ···
 }
}

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

Node 搭建一个静态资源服务器的实现

五、匹配文件 mime 类型

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

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

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

// server/mime.js
const path = require('path')
const mimetypes = {
 'js': 'application/x-javascript',
 'html': 'text/html',
 'css': 'text/css',
 'txt': "text/plain"
}

module.exports = (filepath) => {
 let ext = path.extname(filepath)
  .split('.').pop().tolowercase() // 取扩展名

 if (!ext) { // 如果没有扩展名,例如是文件
  ext = filepath
 }
 return mimetypes[ext] || mimetypes['txt']
}

匹配到文件的 mime 类型,再使用 response.setheader('content-type', 'xxx') 设置响应头:

// server/route.js
const mime = require('./mime')
···
  if (stats.isfile()) {
   const mimetype = mime(filepath)
   response.statuscode = 200
   response.setheader('content-type', mimetype)
   fs.createreadstream(filepath).pipe(response)
  }

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

Node 搭建一个静态资源服务器的实现

六、文件传输压缩

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

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

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

// server/compress.js
const zlib = require('zlib')

module.exports = (readstream, request, response) => {
 const acceptencoding = request.headers['accept-encoding']
 
 if (!acceptencoding || !acceptencoding.match(/\b(gzip|deflate)\b/)) {
  return readstream
 }
 else if (acceptencoding.match(/\bgzip\b/)) {
  response.setheader("content-encoding", 'gzip')
  return readstream.pipe(zlib.creategzip())
 }
 else if (acceptencoding.match(/\bdeflate\b/)) {
  response.setheader("content-encoding", 'deflate')
  return readstream.pipe(zlib.createdeflate())
 }
}

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

// server/route.js
const compress = require('./compress')
···
 if (stats.isfile()) {
   const mimetype = mime(filepath)
   response.statuscode = 200
   response.setheader('content-type', mimetype)
   
   // fs.createreadstream(filepath).pipe(response)
+   let readstream = fs.createreadstream(filepath)
+   if(filepath.match(config.compress)) { // 正则匹配:/\.(html|js|css|md)/
    readstream = compress(readstream,request, response)
   }
   readstream.pipe(response)
  }

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

Node 搭建一个静态资源服务器的实现

七、资源缓存

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

强缓存在 request header 中的字段是 expires 和 cache-control;如果在有效期内则直接加载缓存资源,状态码直接是显示 200。

协商缓存在 request header 中的字段是:

  • if-modified-since(对应值为上次 respond header 中的 last-modified)
  • if-none—match(对应值为上次 respond header 中的 etag)

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

首先配置默认的 cache 字段:

// server/config.js
module.exports = {
 root: process.cwd(),
 host: '127.0.0.1',
 port: '8877',
 compress: /\.(html|js|css|md)/,
 cache: {
  maxage: 2,
  expires: true,
  cachecontrol: true,
  lastmodified: true,
  etag: true
 }
}

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

const config = require('./config')
function refreshres (stats, response) {
 const {maxage, expires, cachecontrol, lastmodified, etag} = config.cache;

 if (expires) {
  response.setheader('expires', (new date(date.now() + maxage * 1000)).toutcstring());
 }
 if (cachecontrol) {
  response.setheader('cache-control', `public, max-age=${maxage}`);
 }
 if (lastmodified) {
  response.setheader('last-modified', stats.mtime.toutcstring());
 }
 if (etag) {
  response.setheader('etag', `${stats.size}-${stats.mtime.toutcstring()}`); // mtime 需要转成字符串,否则在 windows 环境下会报错
 }
}

module.exports = function isfresh (stats, request, response) {
 refreshres(stats, response);

 const lastmodified = request.headers['if-modified-since'];
 const etag = request.headers['if-none-match'];

 if (!lastmodified && !etag) {
  return false;
 }
 if (lastmodified && lastmodified !== response.getheader('last-modified')) {
  return false;
 }
 if (etag && etag !== response.getheader('etag')) {
  return false;
 }
 return true;
};

最后修改 route.js 中的

// server/route.js
+ const iscache = require('./cache')

  if (stats.isfile()) {
   const mimetype = mime(filepath)
   response.setheader('content-type', mimetype)

+   if (iscache(stats, request, response)) {
    response.statuscode = 304;
    response.end();
    return;
   }
   
   response.statuscode = 200
   // fs.createreadstream(filepath).pipe(response)
   let readstream = fs.createreadstream(filepath)
   if(filepath.match(config.compress)) {
    readstream = compress(readstream,request, response)
   }
   readstream.pipe(response)
  }

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

Node 搭建一个静态资源服务器的实现

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

Node 搭建一个静态资源服务器的实现

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