实战node静态文件服务器的示例代码


Posted in Javascript onMarch 08, 2018

本篇文章主要介绍了实战node静态文件服务器的示例,分享给大家,具体如下:

支持功能:

  1. 读取静态文件
  2. 访问目录可以自动寻找下面的index.html文件, 如果没有index.html则列出文件列表
  3. MIME类型支持
  4. 缓存支持/控制
  5. 支持gzip压缩
  6. Range支持,断点续传
  7. 全局命令执行
  8. 子进程运行

1. 创建服务读取静态文件

首先引入http模块,创建一个服务器,并监听配置端口:

const http = require('http');
 
 const server = http.createServer();
 
 // 监听请求
 server.on('request', request.bind(this));
 
 server.listen(config.port, () => {
  console.log(`静态文件服务启动成功, 访问localhost:${config.port}`);
 });

写一个fn专门处理请求, 返回静态文件, url模块获取路径:

const url = require('url');
 const fs = require('fs');
 function request(req, res) {
 const { pathname } = url.parse(req.url); // 访问路径
 
 const filepath = path.join(config.root, pathname); // 文件路径
 
 fs.createReadStream(filepath).pipe(res); // 读取文件,并响应
 }

支持寻找index.html:

if (pathname === '/') {
  const rootPath = path.join(config.root, 'index.html');
  try{
   const indexStat = fs.statSync(rootPath);
   if (indexStat) {
    filepath = rootPath;
   }
  } catch(e) {
   
  }
 }

访问目录时,列出文件目录:

fs.stat(filepath, (err, stats) => {
 if (err) {
  res.end('not found');
  return;
 }
 if (stats.isDirectory()) {
  let files = fs.readdirSync(filepath);
  files = files.map(file => ({
   name: file,
   url: path.join(pathname, file)
  }));
  let html = this.list()({
   title: pathname,
   files
  });
  res.setHeader('Content-Type', 'text/html');
  res.end(html);
 }
 }

html模板:

function list() {
  let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
  return handlebars.compile(tmpl);
 }
<!DOCTYPE html>
 <html lang="en">
 <head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>{{title}}</title>
 </head>
 <body>
 <h1>hope-server静态文件服务器</h1>
 <ul>
  {{#each files}}
  <li>
   <a href={{url}}>{{name}}</a>
  </li>
  {{/each}}
 </ul>
 </body>
 </html>

2.MIME类型支持

利用mime模块得到文件类型,并设置编码:

res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');

3.缓存支持

http协议缓存:

Cache-Control: http1.1内容,告诉客户端如何缓存数据,以及规则

  1. private 客户端可以缓存
  2. public 客户端和代理服务器都可以缓存
  3. max-age=60 缓存内容将在60秒后失效
  4. no-cache 需要使用对比缓存验证数据,强制向源服务器再次验证
  5. no-store 所有内容都不会缓存,强制缓存和对比缓存都不会触发

Expires: http1.0内容,cache-control会覆盖,告诉客户端缓存什么时候过期

ETag: 内容的hash值 下一次客户端请求在请求头里添加if-none-match: etag值

Last-Modified: 最后的修改时间 下一次客户端请求在请求头里添加if-modified-since: Last-Modified值

handleCache(req, res, stats, hash) {
 // 当资源过期时, 客户端发现上一次请求资源,服务器有发送Last-Modified, 则再次请求时带上if-modified-since
 const ifModifiedSince = req.headers['if-modified-since'];
 // 服务器发送了etag,客户端再次请求时用If-None-Match字段来询问是否过期
 const ifNoneMatch = req.headers['if-none-match'];
 // http1.1内容 max-age=30 为强行缓存30秒 30秒内再次请求则用缓存 private 仅客户端缓存,代理服务器不可缓存
 res.setHeader('Cache-Control', 'private,max-age=30');
 // http1.0内容 作用与Cache-Control一致 告诉客户端什么时间,资源过期 优先级低于Cache-Control
 res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString());
 // 设置ETag 根据内容生成的hash
 res.setHeader('ETag', hash);
 // 设置Last-Modified 文件最后修改时间
 const lastModified = stats.ctime.toGMTString();
 res.setHeader('Last-Modified', lastModified);
 
 // 判断ETag是否过期
 if (ifNoneMatch && ifNoneMatch != hash) {
  return false;
 }
 // 判断文件最后修改时间
 if (ifModifiedSince && ifModifiedSince != lastModified) {
  return false;
 }
 // 如果存在且相等,走缓存304
 if (ifNoneMatch || ifModifiedSince) {
  res.writeHead(304);
  res.end();
  return true;
 } else {
  return false;
 }
 }

4.压缩

客户端发送内容,通过请求头里Accept-Encoding: gzip, deflate告诉服务器支持哪些压缩格式,服务器根据支持的压缩格式,压缩内容。如服务器不支持,则不压缩。

getEncoding(req, res) {
  const acceptEncoding = req.headers['accept-encoding'];
  // gzip和deflate压缩
  if (/\bgzip\b/.test(acceptEncoding)) {
   res.setHeader('Content-Encoding', 'gzip');
   return zlib.createGzip();
  } else if (/\bdeflate\b/.test(acceptEncoding)) {
   res.setHeader('Content-Encoding', 'deflate');
   return zlib.createDeflate();
  } else {
   return null;
  }
 }

5.断点续传

服务器通过请求头中的Range: bytes=0-xxx来判断是否是做Range请求,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,表示Partial Content,并设置Content-Range。如果无效,则返回416状态码,表明Request Range Not Satisfiable。如果不包含Range的请求头,则继续通过常规的方式响应。

getStream(req, res, filepath, statObj) {
  let start = 0;
  let end = statObj.size - 1;
  const range = req.headers['range'];
  if (range) {
   res.setHeader('Accept-Range', 'bytes');
   res.statusCode = 206;//返回整个内容的一块
   let result = range.match(/bytes=(\d*)-(\d*)/);
   if (result) {
    start = isNaN(result[1]) ? start : parseInt(result[1]);
    end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
   }
  }
  return fs.createReadStream(filepath, {
   start, end
  });
 }

6.全局命令执行

通过npm link实现

  1. 为npm包目录创建软链接,将其链到{prefix}/lib/node_modules/
  2. 为可执行文件(bin)创建软链接,将其链到{prefix}/bin/{name}

npm link命令通过链接目录和可执行文件,实现npm包命令的全局可执行。

package.json里面配置

{
 bin: {
 "hope-server": "bin/hope"
 }
 }

在项目下面创建bin目录 hope文件, 利用yargs配置命令行传参数

// 告诉电脑用node运行我的文件
 #! /usr/bin/env node
 
 const yargs = require('yargs');
 const init = require('../src/index.js');
 const argv = yargs.option('d', {
 alias: 'root',
 demand: 'false',
 type: 'string',
 default: process.cwd(),
 description: '静态文件根目录'
 }).option('o', {
 alias: 'host',
 demand: 'false',
 default: 'localhost',
 type: 'string',
 description: '配置监听的主机'
 }).option('p', {
 alias: 'port',
 demand: 'false',
 type: 'number',
 default: 8080,
 description: '配置端口号'
 }).option('c', {
 alias: 'child',
 demand: 'false',
 type: 'boolean',
 default: false,
 description: '是否子进程运行'
 })
 .usage('hope-server [options]')
 .example(
 'hope-server -d / -p 9090 -o localhost', '在本机的9090端口上监听客户端的请求'
 ).help('h').argv;
 
 // 启动服务
 init(argv);

7.子进程运行

通过spawn实现

index.js

const { spawn } = require('child_process');
 const Server = require('./hope');
 
 function init(argv) {
  // 如果配置为子进程开启服务
  if (argv.child) {
   //子进程启动服务
   const child = spawn('node', ['hope.js', JSON.stringify(argv)], {
    cwd: __dirname,
    detached: true,
    stdio: 'inherit'
   });
 
   //后台运行
   child.unref();
   //退出主线程,让子线程单独运行
   process.exit(0);
  } else {
   const server = new Server(argv);
   server.start();
  }
 }
 
 module.exports = init;
hope.js
 if (process.argv[2] && process.argv[2].startsWith('{')) {
 const argv = JSON.parse(process.argv[2]);
 const server = new Hope(argv);
 server.start();
 }

8.源码及测试

源码地址: hope-server

npm install hope-server -g

进入任意目录

hope-server

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
ext 列表页面关于多行查询的办法
Mar 25 Javascript
基于JQuery的Pager分页器实现代码
Jul 17 Javascript
javacript使用break内层跳出外层循环分析
Jan 12 Javascript
javascript实现随时变化着的背景颜色
Apr 02 Javascript
轻量级的原生js日历插件calendar.js使用指南
Apr 28 Javascript
Bootstrap入门书籍之(五)导航条、分页导航
Feb 17 Javascript
js html5 css俄罗斯方块游戏再现
Oct 17 Javascript
bootstrap fileinput组件整合Springmvc上传图片到本地磁盘
May 11 Javascript
jQuery事件对象的属性和方法详解
Sep 09 jQuery
微信小程序自定义toast弹窗效果的实现代码
Nov 15 Javascript
原生js实现无缝轮播图
Jan 11 Javascript
vue动态渲染svg、添加点击事件的实现
Mar 13 Javascript
vue.js或js实现中文A-Z排序的方法
Mar 08 #Javascript
vue.js移动数组位置,同时更新视图的方法
Mar 08 #Javascript
在react-router4中进行代码拆分的方法(基于webpack)
Mar 08 #Javascript
JQuery选中select组件被选中的值方法
Mar 08 #jQuery
vue.js中$set与数组更新方法
Mar 08 #Javascript
vue与vue-i18n结合实现后台数据的多语言切换方法
Mar 08 #Javascript
详解使用vue-cli脚手架初始化Vue项目下的项目结构
Mar 08 #Javascript
You might like
PHP编写登录验证码功能 附调用方法
2016/05/19 PHP
php生成静态页面并实现预览功能
2019/06/27 PHP
thinkphp5实现微信扫码支付
2019/12/23 PHP
如何用javascript控制上传文件的大小
2006/10/26 Javascript
ExtJs3.0中Store添加 baseParams 的Bug
2010/03/10 Javascript
jquery parent和parents的区别分析
2013/10/02 Javascript
javascript中slice(),splice(),split(),substring(),substr()使用方法
2015/03/13 Javascript
9个让JavaScript调试更简单的Console命令
2016/11/14 Javascript
浅析vue数据绑定
2017/01/17 Javascript
详解本地Node.js服务器作为api服务器的解决办法
2017/02/28 Javascript
详解Webstorm 新建.vue文件支持高亮vue语法和es6语法
2017/10/26 Javascript
javascript实现小型区块链功能
2019/04/03 Javascript
Vue中常用rules校验规则(实例代码)
2019/11/14 Javascript
关于vue3.0中的this.$router.replace({ path: '/'})刷新无效果问题
2020/01/16 Javascript
JS面向对象编程基础篇(三) 继承操作实例详解
2020/03/03 Javascript
用VsCode编辑TypeScript的实现方法
2020/05/07 Javascript
解决在Vue中使用axios POST请求变成OPTIONS的问题
2020/08/14 Javascript
Vue+penlayers实现多边形绘制及展示
2020/12/24 Vue.js
[47:52]DOTA2-DPC中国联赛正赛 iG vs LBZS BO3 第二场 3月4日
2021/03/11 DOTA
Python中tell()方法的使用详解
2015/05/24 Python
Python的多维空数组赋值方法
2018/04/13 Python
Python实现随机生成手机号及正则验证手机号的方法
2018/04/25 Python
django 实现电子支付功能的示例代码
2018/07/25 Python
Django实现表单验证
2018/09/08 Python
Appium+python自动化之连接模拟器并启动淘宝APP(超详解)
2019/06/17 Python
python实现Oracle查询分组的方法示例
2020/04/30 Python
Python基于数列实现购物车程序过程详解
2020/06/09 Python
使用Keras加载含有自定义层或函数的模型操作
2020/06/10 Python
python和node.js生成当前时间戳的示例
2020/09/29 Python
python字符串拼接+和join的区别详解
2020/12/03 Python
澳大利亚香水在线:Price Rite Mart
2017/12/28 全球购物
Linux管理员面试题 Linux admin interview questions
2014/11/01 面试题
广告设计专业自荐信范文
2013/11/14 职场文书
21岁生日感言
2014/02/27 职场文书
校本课程教学计划
2015/01/19 职场文书
创业项目(超低成本创业项目)
2019/08/16 职场文书