手写启动一个本地服务器的命令行工具

963 阅读4分钟

本篇主要将 实现一个自己的命令行工具,web缓存,以及web文件压缩融合到一起,如果有部分不是太清楚的地方可以查看文末的链接。

实现本地服务器命令行工具 xl-server

//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('\r\n 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
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;

目录结构的模板文件如下:

//index.html 用来展示文件目录
<!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'},{}]} -->
   <%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(/\bgzip\b/)) {
            res.setHeader('content-encoding','gzip')
            return zlib.createGzip()
        } else if (encoding.match(/\bdeflate\b/)) {
            res.setHeader('content-encoding', 'deflate')
            return zlib.createDeflate();
        } else {
            return false
        }
    } else {
        return false
    }
}

写完之后 我们

npm link 
xl-server   
//然后我们就可以看到启动了一个localhost:3000 并且展示了当前文件目录

源码分享

另外本文的三个部分具体一些的例子可以查看下面的:

以上就是完美实现本地服务器命令行工具,不足之处欢迎各位提出宝贵的意见或建议,也希望能帮助到你从中获得一些知识,谢谢大家的关注!