乐趣区

深入nodejs-搭建静态服务器(实现命令行)

静态服务器
使用 node 搭建一个可在任何目录下通过命令启动的一个简单 http 静态服务器
完整代码链接安装:npm install yg-server - g 启动:yg-server
可通过以上命令安装,启动,来看一下最终的效果
TODO

创建一个静态服务器
通过 yargs 来创建命令行工具
处理缓存
处理压缩

初始化

创建目录:mkdir static-server

进入到该目录:cd static-server

初始化项目:npm init

构建文件夹目录结构:

初始化静态服务器

首先在 src 目录下创建一个 app.js
引入所有需要的包,非 node 自带的需要 npm 安装一下

初始化构造函数,options 参数由命令行传入,后续会讲到

this.host 主机名
this.port 端口号
this.rootPath 根目录
this.cors 是否开启跨域
this.openbrowser 是否自动打开浏览器

const http = require(‘http’); // http 模块
const url = require(‘url’); // 解析路径
const path = require(‘path’); // path 模块
const fs = require(‘fs’); // 文件处理模块
const mime = require(‘mime’); // 解析文件类型
const crypto = require(‘crypto’); // 加密模块
const zlib = require(‘zlib’); // 压缩
const openbrowser = require(‘open’); // 自动启动浏览器
const handlebars = require(‘handlebars’); // 模版
const templates = require(‘./templates’); // 用来渲染的模版文件

class StaticServer {
constructor(options) {
this.host = options.host;
this.port = options.port;
this.rootPath = process.cwd();
this.cors = options.cors;
this.openbrowser = options.openbrowser;
}
}
处理错误响应
在写具体业务前,先封装几个处理响应的函数,分别是错误的响应处理,没有找到资源的响应处理,在后面会调用这么几个函数来做响应

处理错误
返回状态码 500
返回错误信息

responseError(req, res, err) {
res.writeHead(500);
res.end(`there is something wrong in th server! please try later!`);
}

处理资源未找到的响应
返回状态码 404
返回一个 404html

responseNotFound(req, res) {
// 这里是用 handlerbar 处理了一个模版并返回,这个模版只是单纯的一个写着 404html
const html = handlebars.compile(templates.notFound)();
res.writeHead(404, {
‘Content-Type’: ‘text/html’
});
res.end(html);
}
处理缓存
在前面的一篇文章里我介绍过 node 处理缓存的几种方式,这里为了方便我只使用的协商缓存,通过 ETag 来做验证
cacheHandler(req, res, filepath) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filepath);
const md5 = crypto.createHash(‘md5’);
const ifNoneMatch = req.headers[‘if-none-match’];
readStream.on(‘data’, data => {
md5.update(data);
});

readStream.on(‘end’, () => {
let etag = md5.digest(‘hex’);
if (ifNoneMatch === etag) {
resolve(true);
}
resolve(etag);
});

readStream.on(‘error’, err => {
reject(err);
});
});
}
处理压缩

通过请求头 accept-encoding 来判断浏览器支持的压缩方式
设置压缩响应头,并创建对文件的压缩方式

compressHandler(req, res) {
const acceptEncoding = req.headers[‘accept-encoding’];
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 false;
}
}
启动静态服务器

添加一个启动服务器的方法
所有请求都交给 this.requestHandler 这个函数来处理
监听端口号

start() {
const server = http.createSercer((req, res) => this.requestHandler(req, res));
server.listen(this.port, () => {
if (this.openbrowser) {
openbrowser(`http://${this.host}:${this.port}`);
}
console.log(`server started in http://${this.host}:${this.port}`);
});
}
请求处理

通过 url 模块解析请求路径,获取请求资源名
获取请求的文件路径

通过 fs 模块判断文件是否存在,这里分三种情况

请求路径是一个文件夹,则调用 responseDirectory 处理
请求路径是一个文件,则调用 responseFile 处理
如果请求的文件不存在,则调用 responseNotFound 处理

requestHandler(req, res) {
// 通过 url 模块解析请求路径,获取请求文件
const {pathname} = url.parse(req.url);
// 获取请求的文件路径
const filepath = path.join(this.rootPath, pathname);

// 判断文件是否存在
fs.stat(filepath, (err, stat) => {
if (!err) {
if (stat.isDirectory()) {
this.responseDirectory(req, res, filepath, pathname);
} else {
this.responseFile(req, res, filepath, stat);
}
} else {
this.responseNotFound(req, res);
}
});
}

处理请求的文件

每次返回文件前,先调用前面我们写的 cacheHandler 模块来处理缓存
如果有缓存则返回 304
如果不存在缓存,则设置文件类型,etag,跨域响应头
调用 compressHandler 对返回的文件进行压缩处理
返回资源

responseFile(req, res, filepath, stat) {
this.cacheHandler(req, res, filepath).then(
data => {
if (data === true) {
res.writeHead(304);
res.end();
} else {
res.setHeader(‘Content-Type’, mime.getType(filepath) + ‘;charset=utf-8’);
res.setHeader(‘Etag’, data);

this.cors && res.setHeader(‘Access-Control-Allow-Origin’, ‘*’);

const compress = this.compressHandler(req, res);

if (compress) {
fs.createReadStream(filepath)
.pipe(compress)
.pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}
}
},
error => {
this.responseError(req, res, error);
}
);
}
处理请求的文件夹

如果客户端请求的是一个文件夹,则返回的应该是该目录下的所有资源列表,而非一个具体的文件
通过 fs.readdir 可以获取到该文件夹下面所有的文件或文件夹
通过 map 来获取一个数组对象,是为了把该目录下的所有资源通过模版去渲染返回给客户端

responseDirectory(req, res, filepath, pathname) {
fs.readdir(filepath, (err, files) => {
if (!err) {
const fileList = files.map(file => {
const isDirectory = fs.statSync(filepath + ‘/’ + file).isDirectory();
return {
filename: file,
url: path.join(pathname, file),
isDirectory
};
});
const html = handlebars.compile(templates.fileList)({title: pathname, fileList});
res.setHeader(‘Content-Type’, ‘text/html’);
res.end(html);
}
});
app.js 完整代码
const http = require(‘http’);
const url = require(‘url’);
const path = require(‘path’);
const fs = require(‘fs’);
const mime = require(‘mime’);
const crypto = require(‘crypto’);
const zlib = require(‘zlib’);
const openbrowser = require(‘open’);
const handlebars = require(‘handlebars’);
const templates = require(‘./templates’);

class StaticServer {
constructor(options) {
this.host = options.host;
this.port = options.port;
this.rootPath = process.cwd();
this.cors = options.cors;
this.openbrowser = options.openbrowser;
}

/**
* handler request
* @param {*} req
* @param {*} res
*/
requestHandler(req, res) {
const {pathname} = url.parse(req.url);
const filepath = path.join(this.rootPath, pathname);

// To check if a file exists
fs.stat(filepath, (err, stat) => {
if (!err) {
if (stat.isDirectory()) {
this.responseDirectory(req, res, filepath, pathname);
} else {
this.responseFile(req, res, filepath, stat);
}
} else {
this.responseNotFound(req, res);
}
});
}

/**
* Reads the contents of a directory , response files list to client
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
responseDirectory(req, res, filepath, pathname) {
fs.readdir(filepath, (err, files) => {
if (!err) {
const fileList = files.map(file => {
const isDirectory = fs.statSync(filepath + ‘/’ + file).isDirectory();
return {
filename: file,
url: path.join(pathname, file),
isDirectory
};
});
const html = handlebars.compile(templates.fileList)({title: pathname, fileList});
res.setHeader(‘Content-Type’, ‘text/html’);
res.end(html);
}
});
}

/**
* response resource
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
async responseFile(req, res, filepath, stat) {
this.cacheHandler(req, res, filepath).then(
data => {
if (data === true) {
res.writeHead(304);
res.end();
} else {
res.setHeader(‘Content-Type’, mime.getType(filepath) + ‘;charset=utf-8’);
res.setHeader(‘Etag’, data);

this.cors && res.setHeader(‘Access-Control-Allow-Origin’, ‘*’);

const compress = this.compressHandler(req, res);

if (compress) {
fs.createReadStream(filepath)
.pipe(compress)
.pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}
}
},
error => {
this.responseError(req, res, error);
}
);
}

/**
* not found request file
* @param {*} req
* @param {*} res
*/
responseNotFound(req, res) {
const html = handlebars.compile(templates.notFound)();
res.writeHead(404, {
‘Content-Type’: ‘text/html’
});
res.end(html);
}

/**
* server error
* @param {*} req
* @param {*} res
* @param {*} err
*/
responseError(req, res, err) {
res.writeHead(500);
res.end(`there is something wrong in th server! please try later!`);
}

/**
* To check if a file have cache
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
cacheHandler(req, res, filepath) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filepath);
const md5 = crypto.createHash(‘md5’);
const ifNoneMatch = req.headers[‘if-none-match’];
readStream.on(‘data’, data => {
md5.update(data);
});

readStream.on(‘end’, () => {
let etag = md5.digest(‘hex’);
if (ifNoneMatch === etag) {
resolve(true);
}
resolve(etag);
});

readStream.on(‘error’, err => {
reject(err);
});
});
}

/**
* compress file
* @param {*} req
* @param {*} res
*/
compressHandler(req, res) {
const acceptEncoding = req.headers[‘accept-encoding’];
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 false;
}
}

/**
* server start
*/
start() {
const server = http.createServer((req, res) => this.requestHandler(req, res));
server.listen(this.port, () => {
if (this.openbrowser) {
openbrowser(`http://${this.host}:${this.port}`);
}
console.log(`server started in http://${this.host}:${this.port}`);
});
}
}

module.exports = StaticServer;
创建命令行工具

首先在 bin 目录下创建一个 config.js
导出一些默认的配置

module.exports = {
host: ‘localhost’,
port: 3000,
cors: true,
openbrowser: true,
index: ‘index.html’,
charset: ‘utf8’
};

然后创建一个 static-server.js
这里设置的是一些可执行的命令
并实例化了我们最初在 app.js 里写的 server 类,将 options 作为参数传入
最后调用 server.start() 来启动我们的服务器
注意 #! /usr/bin/env node 这一行不能省略哦

#! /usr/bin/env node

const yargs = require(‘yargs’);
const path = require(‘path’);
const config = require(‘./config’);
const StaticServer = require(‘../src/app’);
const pkg = require(path.join(__dirname, ‘..’, ‘package.json’));

const options = yargs
.version(pkg.name + ‘@’ + pkg.version)
.usage(‘yg-server [options]’)
.option(‘p’, { alias: ‘port’, describe: ‘ 设置服务器端口号 ’, type: ‘number’, default: config.port})
.option(‘o’, { alias: ‘openbrowser’, describe: ‘ 是否打开浏览器 ’, type: ‘boolean’, default: config.openbrowser})
.option(‘n’, { alias: ‘host’, describe: ‘ 设置主机名 ’, type: ‘string’, default: config.host})
.option(‘c’, { alias: ‘cors’, describe: ‘ 是否允许跨域 ’, type: ‘string’, default: config.cors})
.option(‘v’, { alias: ‘version’, type: ‘string’})
.example(‘yg-server -p 8000 -o localhost’, ‘ 在根目录开启监听 8000 端口的静态服务器 ’)
.help(‘h’).argv;

const server = new StaticServer(options);

server.start();
入口文件
最后回到根目录下的 index.js,将我们的模块导出,这样可以在根目录下通过 node index 来调试
module.exports = require(‘./bin/static-server’);
配置命令
配置命令非常简单,进入到 package.json 文件里加入一句话
“bin”: {
“yg-server”: “bin/static-server.js”
},

yg-server 是启动该服务器的命令,可以自己定义
然后执行 npm link 生成一个符号链接文件
这样你就可以通过命令来执行自己的服务器了
或者将包托管到 npm 上,然后全局安装,在任何目录下你都可以通过你设置的命令来开启一个静态服务器,在我们平时总会需要这样一个静态服务器

总结
写到这里基本上就写完了,另外还有几个模版文件,是用来在客户端展示的,可以看我的 github,我就不贴了,只是一些 html 而已,你也可以自己设置,这个博客写多了是在是太卡了,字都打不动了。另外有哪里写的不好的地方或看不懂的地方可以给我留言。如果你觉得还有点用,给我 github 这个上点个 star 我会很感激你的哈
个人公众号欢迎关注

退出移动版