背景
学习服务端知识, 入门就是要把文件挂载到服务器上, 我们才能去访问相应的文件本地开发的时候, 我们也会经常把文件放在服务器上去访问, 以便达到在同一个局域网内, 通过同一个服务器地址访问相同的文件, 比如我们会用 xampp, 会用 sulime 的插件 sublime-server 等等本篇文章就通过 node, 手写一个静态资源服务器, 以达到你可以随意定义任何一个文件夹为根目录, 去访问相应的文件, 达到 anywhere is your static-server
主要实现功能
读取静态文件
静态资源缓存
资源压缩
MIME 类型支持
断点续传
发布为可执行命令并可以后台运行, 可以通过 npm install -g 安装
- Useage
- //forhelp
- $ st-server -h
- //start
- $ st-server
- // or with port
- $ st-server -p 8800
- // or with hostname
- $ st-server -0 localhost -p 8888
- // or with folder
- $ st-server -d /
- // full parameters
- $ st-server -d / -p 9900 -o localhost
其中可以配置三个参数,-d 代表你要访问的根目录,-p 代表端口号 (目前暂不支持多次开启用同一个端口号, 需要手动杀死之前的进程),-o 代表 hostname 所有源代码已经上传至 github
源码分析
全部代码基于一个 StaticServer 类进行实现, 在构造函数中首先引入所有的配置, argv 是通过命令行敲入传进来的参数, 然后在获取需要编译的模板, 该模板是简单的显示一个文件夹下所有文件的列表基于 handlebars 实现然后开启服务, 监听请求, 由 this.request() 处理
- class StaticServer{
- constructor(argv){
- this.config = Object.assign({},config,argv);
- this.compileTpl = compileTpl();
- }
- startServer(){
- let server = http.createServer();
- server.on('request',this.request.bind(this));
- server.listen(this.config.port,()=>{
- let serverUrl = `http://${this.config.host}:${this.config.port}`;
debug(` 服务已开启, 地址为 ${chalk.green(serverUrl)}`);
- })
- }
- }
主线就是读取想要搭建静态服务的地址, 如果是文件夹, 则查找该文件夹下是否有 index.html 文件, 有则显示, 没有则列出所有的文件; 如果是文件的话, 则直接显示该文件内容大前提在显示具体的文件之前, 要判断有没有缓存, 有直接获取缓存, 没有的话再请求服务器
- async request(req,res){
- let {pathname} = url.parse(req.url);
- if(pathname == '/favicon.ico'){
- return this.sendError('NOT FOUND',req,res);
- }
- // 获取需要读的文件目录
- let filePath = path.join(this.config.root,pathname);
- let statObj = await fsStat(filePath);
- if(statObj.isDirectory()){// 如果是一个目录的话 列出目录下面的内容
- let files = await readDir(filePath);
- let isHasIndexHtml = false;
- files = files.map(file=>{
- if(file.indexOf('index.html')>-1){
- isHasIndexHtml = true;
- }
- return {
- name:file,
- url:path.join(pathname,file)
- }
- })
- if(isHasIndexHtml){
- let statObjN = await fsStat(filePath+'/index.html');
- return this.sendFile(req,res,filePath+'/index.html',statObjN);
- }
- let resHtml = this.compileTpl({
- title:filePath,
- files
- })
- res.setHeader('Content-Type','text/html');
- res.end(resHtml);
- }else{
- this.sendFile(req,res,filePath,statObj);
- }
- }
- sendFile(req,res,filePath,statObj){
- // 判断是否走缓存
- if (this.getFileFromCache(req, res, statObj)) return; // 如果走缓存, 则直接返回
- res.setHeader('Content-Type',mime.getType(filePath)+';charset=utf-8');
- let encoding = this.getEncoding(req,res);
- // 常见一个可读流
- let rs = this.getPartStream(req,res,filePath,statObj);
- if(encoding){
- rs.pipe(encoding).pipe(res);
- }else{
- rs.pipe(res);
- }
- }
sendFile 方法就是向浏览器输出内容的方法, 主要包括以下几个重要的点:
缓存处理
- getFileFromCache(req,res,statObj){
- let ifModifiedSince = req.headers['if-modified-since'];
- let isNoneMatch = req.headers['if-none-match'];
- res.setHeader('Cache-Control','private,max-age=60');
- res.setHeader('Expires',new Date(Date.now() + 60*1000).toUTCString());
- let etag = crypto.createHash('sha1').update(statObj.ctime.toUTCString() + statObj.size).digest('hex');
- let lastModified = statObj.ctime.toGMTString();
- res.setHeader('ETag', etag);
- res.setHeader('Last-Modified', lastModified);
- if (isNoneMatch && isNoneMatch != etag) {
- return false;
- }
- if (ifModifiedSince && ifModifiedSince != lastModified) {
- return false;
- }
- if (isNoneMatch || ifModifiedSince) {
- res.statusCode = 304;
- res.end('');
- return true;
- } else {
- return false;
- }
- }
这里我们通过 Last-Modified,ETag 实现协商缓存, Cache-Control,Expires 实现强制缓存, 当所有缓存条件成立时才会生效 Last-Modified 原理是通过文件的修改时间, 判断文件是否修改过, ETag 通过文件内容的加密判断是否修改过 Cache-Control,Expire 通过时间进行强缓 2. 对文件进行压缩, 压缩文件以后可以减少体积, 加快传输速度和节约带宽 , 这里支持 gzip 和 deflate 两种方式, 用 node 本身的模块 zlib 进行处理
- getEncoding(req,res){
- let acceptEncoding = req.headers['accept-encoding'];
- if(acceptEncoding.match(/\bgzip\b/)){
- res.setHeader('Content-Encoding','gzip');
- return zlib.createGzip();
- }else if(acceptEncoding.match(/\bdeflate\b/)){
- res.setHeader('Conetnt-Encoding','deflate');
- return zlib.createDeflate();
- }else{
- return null;
- }
- }
通过 range, 进行断点续传的处理
- getPartStream(req, res, filePath, statObj) {
- let start = 0;
- let end = statObj.size - 1;
- let 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
- })
- }
生成命令行工具, 用 npm 安装 yargs 包进行操作, 并在 package.json 中添加 "bin": { "st-Server": "bin/www" }, 指向需要执行命令的文件, 然后在 www 中配置对应的命令, 并且开启子进程进行主代码的操作, 为了解决你开启命令后, 命令行一直处于卡顿的状态开启子进程也是 node 原生模块 child_process 支持的
- #! /usr/bin/env node
- let yargs = require('yargs');
- let 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: 8800,
- description: '请配置端口号'
- })
- .usage('st-server [options]')
- .example(
- 'st-server -d / -p 9900 -o localhost', '在本机的 9900 端口上监听客户端的请求'
- ).help('h').argv;
- let path = require('path');
- let {
- spawn
- } = require('child_process');
- let p1 = spawn('node', ['www.js', JSON.stringify(argv)], {
- cwd: __dirname
- });
- p1.unref();
- process.exit(0);
参考
anywhere
来源: https://juejin.im/post/5a97639ff265da4e791043d8