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 相关文章推荐
JAVASCRIPT HashTable
Jan 22 Javascript
js自动闭合html标签(自动补全html标记)
Oct 04 Javascript
一个级联菜单代码学习及removeClass与addClass的应用
Jan 24 Javascript
浅谈javascript原型链与继承
Jul 13 Javascript
在JavaScript中如何解决用execCommand(
Oct 19 Javascript
特殊日期提示功能的实现方法
Jun 16 Javascript
深入理解Javascript中的作用域链和闭包
Apr 25 Javascript
基于 Bootstrap Datetimepicker 联动
Aug 03 Javascript
angularjs $http调用接口的方式详解
Aug 13 Javascript
vue vue-Router默认hash模式修改为history需要做的修改详解
Sep 13 Javascript
JavaScript canvas绘制渐变颜色的矩形
Feb 18 Javascript
js实现简易ATM功能
Oct 27 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
全国FM电台频率大全 - 16 河南省
2020/03/11 无线电
微信公众平台实现获取用户OpenID的方法
2015/04/15 PHP
php中smarty区域循环的方法
2015/06/11 PHP
PHP的PDO错误与错误处理
2019/01/27 PHP
PHP让网站移动访问更加友好方法
2019/02/14 PHP
做网页的一些技巧
2007/02/01 Javascript
得到文本框选中的文字,动态插入文字的js代码
2007/03/07 Javascript
jQuery 行级解析读取XML文件(附源码)
2009/10/12 Javascript
基于jquery的鼠标拖动效果代码
2012/05/30 Javascript
jquery特效 幻灯片效果示例代码
2013/07/16 Javascript
在Javascript中处理字符串之big()方法的使用
2015/06/08 Javascript
DEDECMS如何为文章添加HOT NEW标志图片
2015/08/14 Javascript
jQuery判断元素是否显示 是否隐藏的简单实现代码
2016/05/19 Javascript
Bootstrap学习笔记之css样式设计(1)
2016/06/07 Javascript
Vue表单验证插件Vue Validator使用方法详解
2017/04/07 Javascript
jQuery实现点击DIV同时点击CheckBox,并为DIV上背景色的实例
2017/12/18 jQuery
vue2 全局变量的设置方法
2018/03/09 Javascript
jQuery实现下拉菜单动态添加数据点击滑出收起其他功能
2018/06/14 jQuery
对angularJs中ng-style动态改变样式的实例讲解
2018/09/30 Javascript
nodejs基础之常用工具模块util用法分析
2018/12/26 NodeJs
小程序和web画三角形实现解析
2019/09/02 Javascript
小程序api实现promise封装过程解析
2019/11/21 Javascript
JS造成内存泄漏的几种情况实例分析
2020/03/02 Javascript
vue 实现动态路由的方法
2020/07/06 Javascript
[47:50]Secret vs VP 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/20 DOTA
对python3 一组数值的归一化处理方法详解
2018/07/11 Python
使用Python获取网段IP个数以及地址清单的方法
2018/11/01 Python
通过PYTHON来实现图像分割详解
2019/06/26 Python
Pandas聚合运算和分组运算的实现示例
2019/10/17 Python
Python turtle画图库&amp;&amp;画姓名实例
2020/01/19 Python
Django全局启用登陆验证login_required的方法
2020/06/02 Python
python设置中文界面实例方法
2020/10/27 Python
Python环境搭建过程从安装到Hello World
2021/02/05 Python
Columbia美国官网:美国著名的户外服装品牌
2016/11/24 全球购物
五年级数学教学反思
2014/02/11 职场文书
2016年教师节特级教师获奖感言
2015/12/09 职场文书