静态服务器
使用node搭建一个可在任何目录下通过命令启动的一个简单http静态服务器
完整代码链接
安装:npm install yg-server -g
启动:yg-server
可通过以上命令安装,启动,来看一下最终的效果
TODO
创建一个静态服务器
通过yargs来创建命令行工具
处理缓存
处理压缩
初始化
创建目录:mkdir static-server
进入到该目录:cd static-server
初始化项目:npm init
构建文件夹目录结构:

初始化静态服务器
- 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;
- }
- }
处理错误响应
在写具体业务前,先封装几个处理响应的函数,分别是错误的响应处理,没有找到资源的响应处理,在后面会调用这么几个函数来做响应
- 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);
- });
- });
- }
处理压缩
- 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;
- }
- }
启动静态服务器
- 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"
- },
总结
写到这里基本上就写完了,另外还有几个模版文件,是用来在客户端展示的,可以看我的github,我就不贴了,只是一些html而已,你也可以自己设置,这个博客写多了是在是太卡了,字都打不动了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。