前言

npm 生态中的 http-server 模块,是一个简单的、零配置的 HTTP 服务,它非常强大,同时非常简单,可以方便的帮助我们开启本地服务器,以及局域网共享,可以用来做调试、开发、学习时的环境配置,我们本节就模拟 http-server 实现一个启动本地服务的命令行工具。

http-server 使用

http-server 服务器通过命令行启动,使用时需要安装,命令如下:

$ npm install http-server -g

启动本地服务器时在根目录下执行下面命令即可:

$ http-server [path] [option]

path 默认情况下是 ./public,否则是 ./,启动后可以通过 http://localhost:8080 来访问服务器,options 为其他参数, npm 官方文档 https://www.npmjs.com/package/http-server 有详细说明。

功能:当通过浏览器访问 http://localhost:8080 以后,会将我们服务器根目录的目录结构显示在浏览器页面上,当点击文件夹时,可以继续显示内部的文件和文件夹,当点击文件时会直接通过服务器访问文件,并将文件内容显示在浏览器页面上。

实现命令行工具依赖的模块

chalk 模块

chalk 模块是用来控制命令行输出的文字颜色的第三方模块,使用前需要安装,安装命令如下:

$ npm install chalk

chalk 模块的用法如下,模块支持的颜色和更多的 API 可以在 npm 官方文档 https://www.npmjs.com/package/chalk 中查看。

/* 文件位置:~static/tests/staticchalk-test.js */
const chalk = require('chalk');

// 在命令行打印绿色和红色的 hello
console.log(chalk.green('hello'));
console.log(chalk.red('hello'));

在命令行窗口输入 node chalk-test.js 查看命令行打印 hello 的颜色。

debug 模块

debug 模块可以匹配当前环境变量 DEBUG 的值并输出相关信息,作用在于命令行工具可以根据不同情况输出的信息进行调试,是第三方模块,使用前需安装,命令如下:

$ npm install debug

debug 的简单使用如下,如果想了解更详细的 API 可以在 npm 官方文档 https://www.npmjs.com/package/debug 中查看。

/* 文件位置:~static/tests/debug-test1.js —— 用法 1 */
const debug = require('debug')('hello');

debug('hi panda');

当我们在命令行中执行 node debug-test1.js 时发现命令窗口什么也没有打印,那是因为当前根目录的环境变量 DEBUG 的值必须和我们设置的 hello 相匹配才会打印相关信息。

设置环境变量,Windows 系统通过 set DEBUG=hello 设置,Mac 系统通过 export DEBUG=hello 设置,设置环境变量后再次执行 node debug-test.js,我们会发现命令行打印出了下面内容。

hello hi panda +0ms

其中 hello 为我们设置 DEBUG 环境变量的值,hi pandadebug 调试方法打印的信息,+0ms 为距离上次执行的间隔时间。

/* 文件位置:~static/tests/debug-test2.js —— 用法 2 */
const debugA = require('debug')('hello:a');
const debugB = require('debug')('hello:b');

debugA('hi panda');
debugB('hello panda');

上面的代码目的是可以让我们不同的 debug 方法可以匹配不同的环境变量,所以需要重新将环境变量的值设置为 hello:*,这样再次执行 node debug-test2.js 发现命令窗口打印了如下内容。

hello:a hi panda +0ms
hello:b hello panda +0ms

使用 debug 的好处就是可以在开发的时候打印一些调试用的信息,在开发完成后因为匹配不到环境变量,这些信息就会被隐藏。

commander 模块

commander 是一个开发命令行工具的解决方案(的作者是 Node 大神 tj),提供了用户命令行输入和参数解析的强大功能,commander 是第三方模块,使用时需要安装,命令如下:

$ npm install commander

基本用法如下:

/* 文件位置:~static/tests/commander-test1.js */
const commander = require('commander');

// 解析 Node 进程执行时的参数
commander.version('1.0.0').parse(process.argv);

上面文件中 version 方法代表当前执行文件模块的版本,parse 是解析当前命令行进程参数的方法,process.argv 是参数集合(数组),第一个参数为执行的 Node 环境程序执行文件的绝对路径,第二个参数是 node 命令执行文件的绝对路径,后面为通过命令行传入的参数,如 --host--port 等。

在命令行执行 node commander-test.js --help 时默认会在命令行输出如下信息:

Usage: [options]
Options:
    -V, --version  output the version number
    -h, --help     output usage information

当然在我们的命令行工具中,参数不只 --version--help 两个,我们希望更多的参数更多的功能,并且可定制的描述信息,使用案例如下:

/* 文件位置:~static/tests/commander-test2.js */
const commander = require('commander');

// 解析 Node 进程执行时的参数
commander
  .version('1.0.0')
  .usage('[options]')
  .option('-p, --port <n>', 'server port')
  .option('-o, --host <n>', 'server host')
  .option('-d, --dir <n>', 'server dir')
  .parse(process.argv);

console.log(commander.port); // 3000
console.log(commander.host); // localhost
console.log(commander.dir); // public

在执行命令 node commander-test2.js --help 后会在命令窗口输出如下信息:

Usage: yourname-http-server [options]
ptions:
    -V, --version   output the version number
    -p, --port <n>  server port
    -o, --host <n>  server host
    -d, --dir  <n>  server dir
    -h, --help      output usage information

usage 方法可以让我们详细的定制参数的类型和描述,option 方法可以让我们添加执行 --help 指令时打印的命令以及对应的描述信息。

执行下面命令:

$ node commander-test2.js --port 3000 --host localhost --dir public

执行命令后我们发现其实给我们的参数挂在了 commander 对象上,方便我们取值。

在我们使用别人的命令行工具时会发现在上面输出信息的时候经常会在下面输出 How to use 的列表,更详细的描述了每条命令的作用及用法。

/* 文件位置:~static/tests/commander-test3.js */
const commander = require('commander');

// 必须写到 parse 方法的前面
commander.on('--help', function () {
  console.log('\r\n  How to use:')
  console.log('    yourname-http-server --port <val>');
  console.log('    yourname-http-server --host <val>');
  console.log('    yourname-http-server --dir <val>');
});

// 解析 Node 进程执行时的参数
commander
  .version('1.0.0')
  .usage('[options]')
  .option('-p, --port <n>', 'server port')
  .option('-o, --host <n>', 'server host')
  .option('-d, --dir <n>', 'server dir')
  .parse(process.argv);

再次执行命令 node commander-test2.js --help 后会在命令窗口输出如下信息:

Usage: yourname-http-server [options]
Options:
    -V, --version  output the version number
    -p, --port <n>  server port
    -o, --host <n>  server host
    -d, --dir <n>   server dir
    -h, --help     output usage information
How to use:
    yourname-http-server --port <val>
    yourname-http-server --host <val>
    yourname-http-server --dir <val>

以上是 commander 模块的基本用法,如想了解更详细的 API 和使用案例可以到 npm 官方文档查看,地址如下 https://www.npmjs.com/package/commander

实现静态服务的功能

文件目录

  
    static
      |- bin
      | |- yourname-http-server.js
      |- public
      | |- css
      | | |- style.css
      | |- index.html
      | |- 1.txt
      |- tests
      | |- chalk-test.js
      | |- commander-test1.js
      | |- commander-test2.js
      | |- commander-test3.js
      | |- debug-test1.js
      | |- debug-test2.js
      |- config.js
      |- index.html
      |- index.js
      |- package-lock.json
      |- package.json
  

配置文件

在启动静态服务的时候,我们希望可以通过命令行传参的形式来定义当前启动服务的主机名端口号,以及默认检索的文件根目录,所以需要配置文件来实现灵活传参。

/* 文件位置:~static/config.js */
module.exports = {
  port: 3000,
  host: 'localhost',
  dir: process.cwd()
}

在上面的配置中,默认端口号为 3000,默认主机名为 localhost,我们设置默认检索文件的根目录为通过命令行启动服务器的目录,而 process.cwd() 的值就是我们启动命令行执行命令的目录的绝对路径。

创建服务器 Server 类

因为我们的命令行工具启动本地服务可能是在系统的任意位置,或者指定启动服务访问的域,提高可配置性,并且要更方便给服务器扩展更多的方法处理不同的逻辑,所以需要创建一个 Server 类。

/* 文件位置:~static/index.js —— Server 类的创建 */
// 引入依赖
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('mz/fs');
const mime = require('mime');
const zlib = require('zlib');
const chalk = require('chalk');
const ejs = require('ejs');
const debug = require('debug')('http:a');

// 引入配置文件
const config = require('./config');

// 读取模板文件
const templateStr = fs.readFileSync(path.join(__dirname, 'index.html'),'utf8');

class Server {
  constructor() {
    this.config = config; // 配置
    this.template = templateStr; // 模板
  }
}

我们在上面代码中引入了 config.js 配置文件,读取了用于启动服务后展示页面 index.html 的内容,并都挂在了 Server 类的实例上,目的是方便内部的方法使用以及达到不轻易操作全局变量的目的。

启动服务器的 start 方法

后面为了方便代码的拆分,我们将原型上的方法统一使用 Server.prototype.xxx 的方式来书写,实际上是写在 Server 类里面的。

/* 文件位置:~static/index.js —— start 方法 */
Server.prototype.start = function () {
  // 创建服务
  const server = http.createServer(this.handleRequest.bind(this));

  // 从配置中解构端口号和主机名
  const { port, host } = this.config;

  // 启动服务
  server.listen(port, host, () => {
    debug('server start http://' + host + ':' + chalk.green(port));
  });
}

start 方法中创建了服务,在启动服务时只需要创建 Server 的实例并调用 start 方法,由于服务的回调中会处理很多请求响应的逻辑,会导致 start 方法的臃肿,所以将服务的回调函数抽取成 Server 类的一个实例方法 handleRequest,需要注意的是 handleRequest 内部的 this 指向需要我们修正。

在启动服务时我们根据配置可以灵活的设置服务的地址,当设置 host 后,服务将只能通过 host 的值作为主机名的地址访问静态服务器,启动服务的提示我们通过匹配环境变量 DEBUGdebug 方法来打印,并将端口号设置成绿色。

服务回调 handleRequest 方法

在实现 handleRequest 之前我们应该了解要实现的功能:

  • http-server 中,如果访问的服务地址路径后面指定具体要访问的文件,并且当前启动服务根目录按照访问路径可以查找到文件,将文件内容读取后响应给客户端;
  • 如果没指定文件,应该检索当前启动服务根目录或默认设置的目录结构,并将文件的结构通过模板渲染成超链接后将页面响应给客户端;
  • 再次点击页面的上的链接,如果是文件,直接读取并响应文件内容,如果是文件夹,则继续检索内部结构通过模板渲染成页面。
/* 文件位置:~static/index.js —— handleRequest 方法 */
Server.prototype.handleRequest = async function (req, res) {
  // 获取访问的路径,默认为 /
  this.pathname = url.parse(req.url, true).pathname;

  // 将访问的路径名转换成绝对路径,取到的 dir 就是绝对路径
  this.realPath = path.join(this.config.dir, this.pathname);

  debug(realPath); // 打印当前访问的绝对路径,用于调试

  try {
    // 获取 statObj 对象,如果 await 同步使用 try...catch 捕获非法路径
    const statObj = await fs.stat(this.realPath);

    if (statObj.isFile()) {
      // 如果是文件,直接返回文件内容
      this.sendFile(req, res, statObj);
    } else {
      // 如果是文件夹则检索文件夹通过模板渲染后返回页面
      this.sendDirDetails(req, res, statObj);
    }
  } catch (e) {
    // 如果路径非法,发送错误响应
    this.sendError(req, res, e);
  }
}

handleRequest 由于内部需要使用异步操作获取 statObj 对象,所以我们使用了 async 函数,为了函数内部可以使用 await 避免异步回调嵌套,由于 await 会等待到异步执行完毕后继续向下执行,我们可以使用 try...catch... 捕获非法的访问路径,并做出错误响应。

如果路径合法,我们需要检测访问路径对应的是文件还是文件夹,如果是文件则执行响应内容的逻辑,是文件夹执行检索文件夹渲染内部文件列表返回页面的逻辑。

所以我们将错误处理逻辑、响应文件内容逻辑和返回文件夹详情页面的逻辑分别抽离成 Server 类的三个实例方法 sendErrorsendFilesendDirDetails,使得 handleRequest 方法逻辑清晰且不那么臃肿。

错误响应 sendError 方法

在服务器处理不同的请求和响应时可能需要处理不同的错误,这些错误的不同就是捕获错误对象的不同,所以我们的 sendError 方法为了更方便的或取请求参数、处理响应以及更好的复用,将参数设置为请求对象、响应对象和错误对象。

/* 文件位置:~static/index.js —— sendError 方法 */
Server.prototype.sendError = function (req, res, err) {
  // 打印错误对象,方便调试
  console.log(chalk.red(err));

  // 设置错误状态码并响应 Not Found
  res.statusCode = 404;
  res.end('Not Found');
}

渲染目录 sendDirDetails 方法

在渲染文件夹详情之前我们首先要做的就是异步读取文件目录,所以我们同样使用 async 函数来实现,Node.js 中有很多渲染页面的模板,我们本次使用 ejs,语法简单,比较常用,ejs 为第三方模块,使用前需安装,更详细的用法可参照 npm 官方文档 https://www.npmjs.com/package/ejs

$ npm install ejs

sendDirDetails 的参数为请求对象、响应对象和 statObj

/* 文件位置:~static/index.js —— sendDirDetails 方法 */
Server.prototype.sendDirDetails = async function (req, res, statObj) {
  // 读取当前文件夹
  let dirs = await fs.readdir(this.realPath);

  // 构造模板需要的数据
  dirs = dirs.map(dir => ({
    name: dir,
    path: path.join(this.pathname, dir)
  }));

  // 渲染模板
  const pageStr = ejs.render(this.template, { dirs });

  // 响应客户端
  res.setHeader('Content-Type', 'text/html;charset=utf8');
  res.end(pageStr);
}

还记得 Server 类的实例属性 template 存储的就是我们的模板(字符串),里面写的就是 ejs 的语法,我们使用 ejs 模块渲染的 render 方法可以将模板中的 JS 执行,并用传给该方法的参数的值替换掉模板中的变量,返回新的字符串,我们直接将字符串响应给客户端即可。

注意:在构建模板数据的时候 path 为超链接标签要跳转的路径,如果直接使用 dir 的值,多级访问还是会在根目录去查找,所以路径非法会返回 Not Found,我们需要在每次访问的时候都将上一次访问的路径与当前访问的文件夹或文件名进行拼接,保证路径的正确性。

ejs 模板 index.html

上面已经知道了该怎样使用 ejs 对模板进行渲染,也对模板构造了数据,接下来就是使用 ejs 的语法编写我们的模板内容。

<!-- 文件位置:~static/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>Server</title>
</head>
<body>
  <% dirs.forEach(function (item) { %>
    <li>
      <a href="<%= item.path%>"><%= item.name %></a>
    </li>
  <% }) %>
</body>
</html>

模板中 JS 逻辑使用 <% %> 包裹,使用 <%= %> 输出变量。

返回文件内容 sendFile 方法

由于都是根据路径查找或操作文件目录并做出响应,sendFile 方法与 sendDirDetails 方法的参数相同,分别为 reqresstatObj

/* 文件位置:~static/index.js —— sendFile 方法 */
Server.prototype.sendFile = function (req, res, statObj) {
  // 设置和处理缓存
  if (this.cache(req, res, statObj)) {
    res.statusCode = 304;
    return res.end();
  }

  // 创建可读流
  const rs = fs.createReadStream(this.realPath);

  // 响应文件类型
  res.setHeader('Content-Type', mime.getType(this.realPath) + ';charset=utf8');

  // 压缩
  const zip = this.compress(req, res, statObj);
  if (zip) return rs.pipe(zip).pipe(res);

  // 处理范围请求
  if (this.range(req, res, statObj)) return;

  // 响应文件内容
  rs.pipe(res);
}

其实上面的方法在根目录执行 node index.js 启动服务后,通过我们默认配置的地址访问服务器,表面上就已经实现了 http-server 的功能,但是我们为了服务器的性能和功能更强大,又在这基础上实现了缓存策略、服务器压缩和处理范围请求的逻辑。

推荐阅读:

我们将上面的三个功能分别抽离成了 Server 类的三个原型方法,cachecompressrange,并且这三个方法的参数都为 reqresstatObj

缓存策略 cache 方法

我们本次的缓存兼容 HTTP 1.0HTTP 1.1 版本,并且同时使用强制缓存和协商缓存共同存在的策略。

/* 文件位置:~static/index.js —— cache 方法 */
Server.prototype.cache = function (req, res, statObj) {
  // 创建协商缓存标识
  const etag = statObj.ctime.toGMTString() + statObj.size;
  const lastModified = statObj.ctime.toGMTString();

  // 设置强制缓存
  res.setHeader('Cache-Control', 'max-age=30');
  res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toUTCString());

  // 设置协商缓存
  res.setHeader('Etag', etag);
  res.setHeader('Last-Modified', lastModified);

  // 获取协商缓存请求头
  const {
    'if-none-match': ifNodeMatch,
    'if-modified-since': ifModifiedSince
  } = req.headers;

  if (etag !== ifNodeMatch && lastModified !== ifModifiedSince) {
    return false;
  } else {
    return true;
  }
}

我们使用的缓存策略为同时设置强制缓存和协商缓存,当强制缓存有效期内再次请求不会访问服务器,待强制缓存过期再次请求执行协商缓存策略,带标识访问服务器进行确认,确认的同时重新设置强制缓存和协商缓存的响应头信息,如果协商缓存任然生效,则直接返回 304 状态码,如果协商缓存失效则读取文件内容返回浏览器。

服务器压缩 compress 方法

为了减少文件数据在传输过程中消耗的流量和时间,我们在浏览器支持解压的情况下使用服务器压缩功能,浏览器会在请求时默认发送请求头 Accept-Encoding 通知我们的服务器当前支持的压缩格式,我们要做的就是按照压缩格式的优先级进行匹配,按照最高优先级的压缩格式进行压缩,将压缩后的数据返回,并通过响应头 Content-Encoding 通知浏览器当前的压缩格式(压缩流的本质为转化流)。

/* 文件位置:~static/index.js —— compress 方法 */
Server.prototype.compress = function (req, res, statObj) {
  // 获取浏览器支持的压缩格式
  const encoding = req.headers['accept-encoding'];

  // 支持 gzip 使用 gzip 压缩,支持 deflate 使用 deflate 压缩
  if (encoding && encoding.match(/\bgzip\b/)) {
    res.setHeader('Content-Encoding', 'gzip');
    return zlib.createGzip();
  } else if (encoding && encoding.match(/\bdeflate\b/)) {
    res.setHeader('Content-Encoding', 'deflate');
    return zlib.createDeflate();
  } else {
    return false; // 不支持压缩返回 false
  }
}

当浏览器支持压缩时,compress 方法返回的为优先级最高压缩格式的压缩流,不支持返回 false,存在压缩流,则将数据压缩并响应浏览器,与不压缩响应不同的是,需要使用压缩流将可读流转化为可写流写入响应 res 中,所以可读流执行了两次 pipe 方法。

处理范围请求 range 方法

range 方法处理的场景为客户端发送请求只想获取文件的某个范围的数据,此时通过 range 方法读取文件范围对应的内容响应给客户端,通过响应头 Accept-Ranges 通知浏览器当前响应范围请求,通过响应头 Content-Range 通知客户端响应的范围以及文件的总字节数。

/* 文件位置:~static/index.js —— range 方法 */
Server.prototype.range = function (req, res, statObj) {
  // 获取 range 请求头
  const range = req.headers['range'];

  if (range) {
    // 获取范围请求的开始和结束位置
    let [, start, end] = range.match(/(\d*)-(\d*)/);

    // 处理请求头中范围参数不传的问题
    start = start ? ParseInt(start) : 0;
    end = end ? ParseInt(end) : statObj.size - 1;

    // 设置范围请求响应
    res.statusCode = 206;
    res.setHeader('Accept-Ranges', 'bytes');
    res.setHeader('Content-Range', 'bytes ' + start + '-' + end + '/' + statObj.size);
    fs.createReadStream(this.realPath, { start, end }).pipe(res);

    return true;
  } else {
    return false;
  }
}

range 方法默认返回值为布尔值,当不是范围请求时返回值为 false,则直接向下执行 sendFile 中的代码,正常读取文件全部内容并响应给浏览器,如果是范围请求则会处理范围请求后在直接结束后返回 true,会在 sendFile 中直接 return,不再向下执行。

将静态服务器关联到命令行

命令行启动服务器

http-server 实际上是通过命令行启动、并传参的,我们需要将我们的程序与命令行关联,关联命令行只需以下几个步骤。

首先,在根目录 package.json 文件中加入 bin 字段,值为对象,对象内属性为命令名称,值为对应执行文件的路径。

// 文件位置:~static/package.json
{
  "name": "yourname-http-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "chalk": "^2.4.1",
    "commander": "^2.17.1",
    "debug": "^3.1.0",
    "ejs": "^2.6.1",
    "mime": "^2.3.1",
    "mz": "^2.7.0"
  },
  "bin": {
    "yourname-http-server": "bin/yourname-http-server.js"
  },
  "devDependencies": {},
  "author": "",
  "license": "ISC"
}

其次,在 yourname-http-server.js 文件中首行加入注释 #! /usr/bin/env node,在命令行执行命令时,默认会以 Node 执行 yourname-http-server.js 文件。

最后,想要使用我们的命令启动 yourname-http-server.js 文件,则需要将这条命令连接到全局(与 -g 安装效果相同),在当前根目录下执行以下命令。

$ npm link

当在命令行执行 yourname-http-server 时,Node 会默认执行 yourname-http-server.js 文件。

命令行的参数传递

我们现在知道在命令行执行命令后用 Node 启动的文件为 yourname-http-server.js,在启动文件时我们应该启动我们的服务器,并结合 commander 模块的参数解析,则需要用命令行传递的参数替换掉 config.js 中的默认参数。

/* 文件位置:~static/bin/yourname-http-server.js —— 命令行执行文件 */
const commander = require('commander');
const Server = require('../index');

// 增加 How to use
commander.on('--help', function () {
  console.log('\r\n  How to use: \r\n')
  console.log('    yourname-http-server --port <val>');
  console.log('    yourname-http-server --host <val>');
  console.log('    yourname-http-server --dir <val>');
});

// 解析 Node 进程执行时的参数
commander
  .version('1.0.0')
  .usage('[options]')
  .option('-p, --port <n>', 'server port')
  .option('-o, --host <n>', 'server host')
  .option('-d, --dir <n>', 'server dir')
  .parse(process.argv);

// 创建 Server 实例传入命令行解析的参数
const server = new Server(commander);

// 启动服务器
server.start();

我们之前把 config.js 的配置直接挂在了 Server 实例的 config 属性上,创建服务使用的参数也是直接从该属性上获取的,因此我们要用 commander 对象对应的参数覆盖实例上 config 的参数,所以在创建 Server 实例时传入了 commander 对象,下面稍微修改 Server 类的部分代码。

/* 文件位置:~static/index.js —— Server 类 */
class Server {
  constructor(options) {
    // 通过解构赋值将 options 的参数覆盖 config 的参数
    this.config = { ...config, ...options }; // 配置
    this.template = templateStr; // 模板
  }
}

执行下面命令,并通过浏览器访问 http://127.0.0.1:4000 来测试服务器功能。

$ yourname-http-server --port 4000 --host 127.0.0.1

在启动服务时自动打开浏览器

由于 JS 是单线程的,在命令行输入命令启动服务的同时不能去做其他的事,此时要靠多进程来帮助我们打开浏览器,在 JS 中开启一个子进程来打开浏览器。

/* 文件位置:~static/bin/yourname-http-server.js —— 命令行执行文件 */
const commander = require('commander');
const Server = require('../index');

// 增加 How to use
commander.on('--help', function () {
  console.log('\r\n  How to use: \r\n')
  console.log('    yourname-http-server --port <val>');
  console.log('    yourname-http-server --host <val>');
  console.log('    yourname-http-server --dir <val>');
});

// 解析 Node 进程执行时的参数
commander
  .version('1.0.0')
  .usage('[options]')
  .option('-p, --port <n>', 'server port')
  .option('-o, --host <n>', 'server host')
  .option('-d, --dir <n>', 'server dir')
  .parse(process.argv);

// 创建 Server 实例传入命令行解析的参数
const server = new Server(commander);

// 启动服务器
server.start();

// ********** 以下为新增代码 **********
const { exec } = require('child_process');

// 判断系统执行不同的命令打开浏览器
const systemOrder = process.platform === 'win32' ? 'start' : 'open';
exec(systemOrder + 'http://' + commander.localhost + ':' + commander.port);
// ********** 以上为新增代码 **********

发布命令行工具到 npm

在发布我们自己实现的 npm 模块之前需要先做一件事,就是解除当前模块与全局环境的 link,我们可以通过两种方式,第一种方式是直接到系统存储命令文件的文件夹删除模块对应命令的 yourname-http-server.cmdWindows)文件,第二种方式是在模块根目录启动命令行并输入如下命令。

$ npm unlink

输入下面命令进行登录:

$ npm login

登录成功后执行下面命令进行发布:

$ npm publish

发布成功后再次使用自己的模块需要通过 npm 下载并全局安装,命令如下:

$ npm install yourname-http-server -g

任意文件夹内打开命令行,并执行命令启动服务验证。

在发布模块之前如果使用 nrm 切换过其他的源,必须切换回 npm,再进行登录和发布操作。

总结

其实我们实现的静态服务器核心还在于处理请求和响应的逻辑上,只是不再手动输入 node 命令启动,而是借助一些第三方模块关联到了命令行并通过命令启动,开发其他类型的命令行工具也需要借助这些第三方模块,静态服务器只是其中之一,其实类似这种命令行工具在开发的角度来讲属于 “造轮子” 系列,可以独立开发命令行工具是一个成为前端架构的必备技能,希望通过本篇文章可以了解命令行工具的开发流程,在未来 “造轮子” 的道路上提供帮助。