这篇文章参考http-server从零实现一个自己的http-server命令行工具

准备工作

跟控制台的交互,这里用到的是 commanderchalk

对于http服务中文件类型的判断这里用到mime,内容模板使用ejs

  • 文件格式类型 mime
  • 内容模板 ejs
在本地开发npm模块的时候,可以使用npm link命令,将npm模块链接到对应的运行项目中,方便地对模块进行调试和测试。

新建文件夹http-server,在文件根目录下执行命令npm init -y,生成package.json文件,给package.json新增bin命令:

// package.json
{
  "name": "http-server",
  "version": "1.0.0",
  "license": "MIT",
  "bin": {
    "my-http-server": "./bin/www.js"
  }
}

在控制台执行npm link可以将全局的my-http-server命令指向这个目录了,当然名称可以根据自己的需要更换,路径./bin/www.js是执行这个命令的文件。
接着就可以到想启动的http服务的文件目录下,打开控制台执行my-http-server启动我们的命令了,不过在这之前还要先创建www.js文件,我们执行的命令,实际是执行node www.js

创建:

$ mkdir bin
$ touch mkdir/www.js

为了让文件能够以nodejs的环境执行,www.js文件开头需要输入声明:

#! /usr/bin/env node

配置项

开始之前,还需要新建配置项,在http-server文件夹下新建config.js输入配置内容:

let config = {
  port: 3000, // 端口号
  host: "127.0.0.1", // 启动路径
  dir: process.cwd() // 命令行执行目录
}
module.exports = config;

通过commander可以让用户方便的跟控制台交互,修改bin/www.js文件下的内容如下:

#! /usr/bin/env node

let config = require("../config");
const commander = require("commander");
const {version} = require("../package.json");

commander
  .version(version)
  .option("-p --port <n>", "set http-server port") // 端口号
  .option("-0 --host <n>", "set http-server host") // 主机路径
  .option("-d --dir <n>", "set http-server directory") // 服务启动路径
  .on("--help",()=>{
      console.log("Example");
      console.log(" $ my-http-server --port --host");
  })
  .parse(process.argv)

// 拿到用户跟控制台交互后的命令,合并到配置文件中
 config = {...config,...commander};

保存文件,在控制台下输入my-webpack-server -h就可以看到对应的交互信息了。

createServer

http-server文件夹下新建server.js,创建Server类,封装httpcreateServer方法:

  • server.js
const http = require("http")

module.exports = class Server {
    constructor(config){
        this.dir = config.dir
        this.port = config.port
        this.host = config.host
    }
    handleRequest(req,res){}
    start(){
        const server = http.createServer(this.handleRequest.bind(this))
        server.listen(this.port, this.host, () => {
            // 成功后回调函数
        })
    }
}

为了能在启动服务后,在控制台输出多色彩的字符串信息,我们可以利用chalk这个依赖包,我们添加下面代码到server.js

const chalk = require("chalk")
...
server.listen(this.port, this.host, () => {
    console.log(chalk.yellow(`Starring up http-server,\r\nserving ${this.dir} \r\n Available on \r\n`))
    console.log(chalk.green(`http://${this.host}:${this.port}`))
})
...

handleRequest 处理请求

接下来开始封装请求方法,handleRequest函数主要做下面的工作:

  • 获取requestsurl路径
  • 判断路径是文件还是文件夹 fs.stat
  • 若路径不存在对应文件文件夹,则返回404
  • 文件则判断文件mime类型,响应文件信息
  • 文件夹判断文件夹里面是否有index.html文件,有则渲染index.html,否则利用ejs模板渲染文件列表
const url = require("url")
const ejs = require("ejs")
const mime = require("mime")
const fs = require("mz/fs")

// 模板文件
const template = fs.readFileSync(
    path.resolve(__dirname, "./template.ejs"),
    "utf-8"
)

...
constructor(){...}
async handleRequest(req, res) {
    this.req = req
    this.res = res
    // 通过url解析路径名
    const { pathname } = url.parse(req.url, true)
    // 拿到文件绝对路径,decodeURIComponent 防止中文符号无法识别
    const absPath = path.join(this.dir, decodeURIComponent(pathname))
    try {
        // 判断是文件还是文件夹
        const statObj = await fs.stat(absPath)
        if (statObj.isDirectory()) {
            let indexPath = absPath+"/index.html";
            // 如果文件夹里面有index.html 则进入
            fs.access(indexPath)
            .then(this.sendFile.bind(this, indexPath))
            .catch(async () => {
                // 模板文件过滤掉.DS_Store,并加上href链接
                let files = await fs.readdir(absPath)
                files = files
                .filter((f) => f !== ".DS_Store")
                .map((file) => ({ file, href: path.join(pathname, file) }))
                    let str = ejs.render(template, {
                    name: path.basename(pathname) || "文件列表",
                    files,
                });
                res.setHeader("Content-Type", "text/html;charset=utf-8")
                res.end(str)
            })
        } else {
            // 文件直接返回
            this.sendFile(absPath)
        }
    } catch (error) {
        this.sendError(error)
    }
}
sendError(error) {
    this.res.statusCode = 404;
    // 返回错误信息,生产环境需要屏蔽
    this.res.end(error.toString())
}
sendFile(filePath) {
    // 判断文件mime类型
    this.res.setHeader(
        "Content-Type",
        mime.getType(filePath) + ";charset=utf-8"
    )
    // 创建文件可读流并返回
    fs.createReadStream(filePath).pipe(this.res)
}
start(){}

模板文件

模板文件渲染文件夹的文件列表

// template.ejs
<ul>
    <% files.forEach(function(item){ %>
    <li><a href="<%=item.href %>"><%=item.file%></a></li>
    <% }) %>
</ul>

引入

最后www.js文件引入Server类,初始化并渲染:

const Server = require("../server")
let server = new Server(config);
server.start()

找个想启动http服务的文件夹打开控制台,输入my-http-server就可以启动服务了。

发布npm包

http-server文件夹下打开控制台,输入下面指令:

  • 如果你是第一次发包,使用npm adduser
  • 如果不是第一次发,使用npm login

输入账号密码和注册邮箱

username:yourname
password:
Email:(this is public)email@domain.com 

确保是在package.json同级目录,然后执行npm publish发布:

$ npm publish

chenwl
117 声望5 粉丝

平坦的路面上曲折前行


« 上一篇
nodejs篇-Stream