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


Posted in Javascript onMay 20, 2019

使用 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 静态资源服务器。希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
获取Javscript执行函数名称的方法
Dec 22 Javascript
用javascript将数据库中的TEXT类型数据动态赋值到TEXTAREA中
Apr 20 Javascript
Js 获取HTML DOM节点元素的方法小结
Apr 24 Javascript
JQuery加载图片自适应固定大小的DIV
Sep 12 Javascript
location.href用法总结(最主要的)
Dec 27 Javascript
js关于命名空间的函数实例
Feb 05 Javascript
js实现可得到不同颜色值的颜色选择器实例
Feb 28 Javascript
jQuery插件pagination实现分页特效
Apr 12 Javascript
基于Jquery easyui 选中特定的tab
Nov 17 Javascript
Bootstrap输入框组件简单实现代码
Mar 06 Javascript
无限循环轮播图之运动框架(原生JS实现)
Oct 01 Javascript
js+html获取系统当前时间
Nov 10 Javascript
vue+element实现打印页面功能
May 20 #Javascript
vue+element实现表单校验功能
May 20 #Javascript
javascript的this关键字详解
May 20 #Javascript
小程序实现新用户判断并跳转激活的方法
May 20 #Javascript
node实现socket链接与GPRS进行通信的方法
May 20 #Javascript
JS求1到任意数之间的所有质数的方法详解
May 20 #Javascript
react 中父组件与子组件双向绑定问题
May 20 #Javascript
You might like
让php处理图片变得简单 基于gb库的图片处理类附实例代码下载
2011/05/17 PHP
PHP利用APC模块实现大文件上传进度条的方法
2015/10/29 PHP
Zend Framework生成验证码并实现验证码验证功能(附demo源码下载)
2016/03/22 PHP
php实现按天数、星期、月份查询的搜索框
2016/05/02 PHP
laravel 中如何使用ajax和vue总结
2017/08/16 PHP
Smarty缓存机制实例详解【三种缓存方式】
2019/07/20 PHP
重定向实现代码
2006/11/20 Javascript
Prototype使用指南之form.js
2007/01/10 Javascript
JS 建立对象的方法
2007/04/21 Javascript
jquery ajax 同步异步的执行示例代码
2010/06/23 Javascript
jsonp原理及使用
2013/10/28 Javascript
javascript使用prototype完成单继承
2014/12/24 Javascript
JS实现选择TextArea内文本的方法
2015/08/03 Javascript
学习JavaScript设计模式之代理模式
2016/01/12 Javascript
Vue2.0权限树组件实现代码
2017/08/29 Javascript
解决mui框架中switch开关通过js控制开或者关状态时小圆点不动的问题
2019/09/03 Javascript
JavaScript中的执行环境和作用域链
2020/09/04 Javascript
[47:12]TFT vs Secret Supermajor小组赛C组 BO3 第三场 6.3
2018/06/04 DOTA
[45:14]Optic vs VP 2018国际邀请赛淘汰赛BO3 第二场 8.24
2018/08/25 DOTA
Python 解决相对路径问题:&quot;No such file or directory&quot;
2020/06/05 Python
python如何将图片转换素描画
2020/09/08 Python
英国网络托管和域名领导者:Web Hosting UK
2017/10/15 全球购物
工程管理造价应届生求职信
2013/11/13 职场文书
董事长岗位职责
2013/11/30 职场文书
市政施工员自我鉴定
2014/01/15 职场文书
教师通用专业自荐书范文
2014/02/11 职场文书
医学专业应届生的自我评价
2014/02/28 职场文书
党的群众路线教育实践活动公开承诺书
2014/03/28 职场文书
入股协议书范本
2014/04/14 职场文书
槐乡的孩子教学反思
2014/04/27 职场文书
咖啡店创业计划书
2014/08/15 职场文书
2014年重阳节老干部座谈会上的讲话稿
2014/09/25 职场文书
中学生社区服务活动报告
2015/02/05 职场文书
《月光曲》教学反思
2016/02/16 职场文书
施工安全责任协议书
2016/03/23 职场文书
Django实现WebSocket在线聊天室功能(channels库)
2021/09/25 Python