本文学习自:
手写启动一个本地服务器的命令行工具
https://juejin.im/post/5b53f7c5e51d4534b93f27e9
看一下我本地做的效果图
首先
mkdir xl-server
cd xl-server
npm init
修改package.json文件
//package.json
{
"name": "xl-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"bin": {
"xl-server": "bin/xl-server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^2.4.1",
"commander": "^2.16.0",
"debug": "^3.1.0",
}
}
我们知道实现在本地命令行中运行 xl-server 必须有bin目录 上面的bin目录指向了 bin/xl-server.js
// ./bin/xl-server.js
commander.on('--help', () => {
console.log('
how to use:');
console.log(' xl-server --port <val>');
console.log(' xl-server --host <val>');
console.log(' xl-server --dir <val>');
})
commander
.version('1.0.0')
.usage('[option]')
.option('-p,--port <n>','server port')
.parse(process.argv)
let Server = require('../index') //引入index文件导出的类
let server = new Server(commander) //实例
server.start(); //启动
let {exec} = require('child_process');
if(process.platform === 'win32'){ //执行调起浏览器 localhost:port
exec(`start http://localhost:${server.config.port}`);
}else{
exec(`open http://localhost:${server.config.port}`);
}
上面的commander的是一个解析和配置命令行参数的包 具体用法可以去npm官网看看用法commander 我们在index.js里面创建服务
//index.js
// index.js
let http = require('http');
let util = require('util');
let mime = require('mime'); //第三方模块 用来获取内容类型
let chalk = require('chalk'); // 粉笔
//初步最简单的命令行工具
let config = require('./config');
class Server {
constructor(options) {
this.config = {...config, ...options}; //覆盖默认配置例如端口号
}
start() {
let server = http.createServer((req,res) => {
res.end('hello')
});
let { port, host } = this.config;
server.listen(port, host, function () {
console.log(`server start http://${host}:${chalk.green(port)}`)
});
}
}
module.exports = Server;
还缺少一个config.js的默认配置项
module.exports = {
port: 3000,
host:'localhost',
dir:process.cwd() //当前运行目录
}
// 运行的配置
这样一个最简单的 好像什么用都没有的命令行工具就有了 我们在当前的根目录下执行下面的命令就可以看到浏览器打开了 localhost:3000
// npm link
// xl-server
读取本地文件目录或文件内容
//index.js
// 复杂一点的命令行工具 展示目录和文件
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let util = require('util');
let zlib = require('zlib');
let mime = require('mime'); //第三方模块 用来获取内容类型
// let debug = require('debug')('env') //打印输出 会根据环境变量控制输出
let chalk = require('chalk'); // 粉笔
let ejs = require('ejs') //高效的 JavaScript 模板引擎。
let config = require('./config');
let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);
let templateStr = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
class Server {
constructor(options) {
this.config = {...config, ...options};
this.template = templateStr;
}
async handleRequest(req, res) { //这里根据请求的url来读取目录或者文件内容
let { pathname } = url.parse(req.url, true);
let realPath = path.join(this.config.dir, pathname);
try{
let statObj = await stat(realPath)
if(statObj.isFile()) { //文件
this.sendFile(req, res, statObj, realPath)
} else { //文件夹
let dirs = await readdir(realPath);
dirs = dirs.map(dir => ({ name: dir, path: path.join(pathname, dir) }));
let str = ejs.render(this.template, { dirs });
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(str);
}
} catch (e) {
this.sendError(req, res, e);
}
}
sendError(req, res, e) {
console.log(e),
res.end('404')
}
sendFile(req, res, statObj, realPath) {
fs.createReadStream(realPath).pipe(res)
}
start() {
let server = http.createServer(this.handleRequest.bind(this)); //这里用handleRequest来执行
let { port, host } = this.config;
server.listen(port, host, function () {
console.log(`server start http://${host}:${chalk.green(port)}`)
});
}
}
module.exports = Server;
目录结构的模板文件如下:
<!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>Document</title>
</head>
<body>
<!-- 实现渲染列表 {dirs:[{name:'201804',path:'/201804'},{}]} -->
<p>什么什么</p>
<%dirs.forEach(item=>{%>
<li><a href="<%=item.path%>"><%=item.name%></a></li>
<%})%>
</body>
</html>
实现文件目录的缓存和文件压缩
这个操作是在读取文件的过程中实现,所以下面只给出index.js里面的sendFile方法里面增加缓存和压缩
sendFile(req, res, statObj, realPath) {
if (this.cache(req, res, statObj, realPath)) {
res.statusCode = 304;
res.end();
return;
}
res.setHeader('Content-Type', mime.getType(realPath) + ';charset=utf-8');
let zip = this.compress(req, res, statObj, realPath);
if(zip) {
return fs.createReadStream(realPath).pipe(zip).pipe(res)
}
fs.createReadStream(realPath).pipe(res)
}
接下来就是写上面的cache方法和compress方法了
cache(req, res, statObj, realPath) {
res.setHeader('Cache-control','max-age=100') //强制缓存 注意即使是强制缓存也不会缓存主网页
let etag = statObj.ctime.toGMTString() + statObj.size;
let lastModified = statObj.ctime.toGMTString(); //atime创建时间 ctime --- change time 修改时间
res.setHeader('Etag', etag); //Etag -- if-none-match
res.setHeader('Last-Modified', lastModified); //Last-Modified --- if-none-match
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
if (etag != ifNoneMatch) { //两种方式 第一种就行
return false
}
if (lastModified !=ifModifiedSince) { //两种方式 第一种就行,此种只是列出304缓存的另一种方式
return false
}
return true
}
compress(req, res, statObj, realPat) { //实现压缩功能
let encoding = req.headers['accept-encoding'];
if (encoding) {
if (encoding.match(/gzip/)) {
res.setHeader('content-encoding','gzip')
return zlib.createGzip()
} else if (encoding.match(/deflate/)) {
res.setHeader('content-encoding', 'deflate')
return zlib.createDeflate();
} else {
return false
}
} else {
return false
}
}
写完之后 我们
npm link
xl-server
//然后我们就可以看到启动了一个localhost:3000 并且展示了当前文件目录
大家可以看我本地的目录
//index.js中的内容
// 复杂一点的命令行工具 展示目录和文件 完善--> 加上压缩和缓存
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let util = require('util');
let zlib = require('zlib');
let mime = require('mime'); //第三方模块 用来获取内容类型
// let debug = require('debug')('env') //打印输出 会根据环境变量控制输出
let chalk = require('chalk'); // 粉笔
let ejs = require('ejs') //高效的 JavaScript 模板引擎。
let config = require('./config');
let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);
let templateStr = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
class Server {
constructor(options) {
this.config = {...config, ...options};
this.template = templateStr;
}
async handleRequest(req, res) {
let { pathname } = url.parse(req.url, true);
let realPath = path.join(this.config.dir, pathname);
try{
let statObj = await stat(realPath)
if(statObj.isFile()) { //文件
this.sendFile(req, res, statObj, realPath)
} else { //文件夹
let dirs = await readdir(realPath);
dirs = dirs.map(dir => ({ name: dir, path: path.join(pathname, dir) }));
let str = ejs.render(this.template, { dirs });
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(str);
}
} catch (e) {
this.sendError(req, res, e);
}
}
sendError(req, res, e) {
console.log(e); // 将错误打印出来
res.statusCode = 404;
res.end('Not Found');
}
cache(req, res, statObj, realPath) {
res.setHeader('Cache-control','max-age=100') //强制缓存 注意即使是强制缓存也不会缓存主网页
let etag = statObj.ctime.toGMTString() + statObj.size;
let lastModified = statObj.ctime.toGMTString(); //atime创建时间 ctime --- change time 修改时间
res.setHeader('Etag', etag); //Etag -- if-none-match
res.setHeader('Last-Modified', lastModified); //Last-Modified --- if-none-match
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
if (etag != ifNoneMatch) { //两种方式 第一种就行
return false
}
if (lastModified !=ifModifiedSince) { //两种方式 第一种就行,此种只是列出304缓存的另一种方式
return false
}
return true
}
compress(req, res, statObj, realPat) { //实现压缩功能
let encoding = req.headers['accept-encoding'];
if (encoding) {
if (encoding.match(/gzip/)) {
res.setHeader('content-encoding','gzip')
return zlib.createGzip()
} else if (encoding.match(/deflate/)) {
res.setHeader('content-encoding', 'deflate')
return zlib.createDeflate();
} else {
return false
}
} else {
return false
}
}
sendFile(req, res, statObj, realPath) {
if (this.cache(req, res, statObj, realPath)) {
res.statusCode = 304;
res.end();
return;
}
res.setHeader('Content-Type', mime.getType(realPath) + ';charset=utf-8');
let zip = this.compress(req, res, statObj, realPath);
if(zip) {
return fs.createReadStream(realPath).pipe(zip).pipe(res)
}
fs.createReadStream(realPath).pipe(res)
}
start() {
let server = http.createServer(this.handleRequest.bind(this));
let { port, host } = this.config;
server.listen(port, host, function () {
console.log(`server start http://${host}:${chalk.green(port)}`)
});
}
}
module.exports = Server;