http-server包
前端开发人员不论是在开发还是测试中都会用到http服务器,方便快捷的使用更加有助于我们编写和调试代码,而http-server则是一个十分好用的包,它几乎不用配置,可以使用任何一个目录生成一个http服务器。
简单说一说http-server的使用。
首先全局安装它:
> npm install http-server -g
复制代码
安装成功后就可以使用命令行来启动一个http服务,进入你想启动的目录,使用命令:
> http-server
复制代码
打开浏览器访问http://localhost:8080/即可看到已经使用当前目录启动了http server。
然而这里并不是讨论http-server的使用,我们可以仿照http-server包手写一个功能近似的静态服务,以更深入的了解和学习。
功能说明
一个静态服务器都能做什么?
- 首先应支持使用计算机上任何一个目录生成http服务;
- 可以在浏览器中浏览目录结构
- 可以访问服务器上的文件
- 支持命令行,可让用户自己指定访问的端口
以上就是http服务器应该有的大致功能,我们使用node.js手写代码来实现它。
准备工作
自己手写一个http服务要有基本的文件读取,输入输出等工作要做,这些大部分功能node.js提供的库都可以实现,但是还需要有些第三方包的支持,下面简单说一下。
mime包
mime包是一个可以根据文件来分析出http内容类型的包,http的内容类型不难理解,当访问一个html页面时,浏览器会根据向应头的Content-Type
类型来呈现内容。
使用npm install mime
安装后即可使用:
const mime = require('mime');
var contentType = mime.getType('/Users/yuet/workspace/static-server/public/node.jpeg');//image/jpeg
复制代码
可以看到,传入正确的文件路径即可得到对应的内容类型,这样可以省去我们自己写代码判断生成Content-Type
。
chalk包
chalk包顾名思义,它译为粉笔,有了它就可以方便的在控制台输出内容时使用不同的颜色,同样安装它npm install chalk
即可使用:
const chalk = require('chalk');
console.log(chalk.red('hello node'));
复制代码
以上使用chalk.red()
,将输出内容变为红色显示,chalk有很多颜色方法支持,使用它可以突出重点内容,方便用户查看。
debug包
debug包可以在node.js环境中帮助我们调试。
举例:
var a = require('debug')('worker:a')
, b = require('debug')('worker:b');
a('doing lots of uninteresting work');
b('doing some work');
复制代码
调用debug包会返回一个方法,它可以指定操作系统中某环境变量名,以上在操作系统中可以设置两个环境变量work:a
和work:b
,当前若是为work:a的环境,则只会输出a方法中的内容,这有利于我们区别开发和生产环境。
默认配置
启动一个http服务器,端口、host主机、以及目录地址这三个内容必不可少,当然用户在使用时可以更改,但是需要有一个默认的配置:
const path = require('path');
let config = {
host: '127.0.0.1',
port: 8080,
dir: path.join(__dirname, '../public')
};
复制代码
指定本机,默认端口号为8080,缺省目录就为当前项目中的/public目录。将此配置对象导出供外面使用。
代码基本结构
可以封装一个类,将静态文件操作、服务器的启停写在里面:
const config = require('./config');
class Server {
constructor() {
this.config = config;
}
//封装读取文件等操作
handleRequest(req, res) {}
/** * 启动服务器 */
start() {}
/** * 发送文件 */
sendFile(req, res, filePath, stat) {}
/** * 发送错误消息 */
sendError(req, res, e) {}
}
new Server().start();
复制代码
以上代码就是大致的结构,其中将默认的配置对象读入并挂到类的this上,这样可以方便的调用配置信息。
核心代码
静态服务器是基于http的,因此要引入http模块来启动一个http服务,并监听端口:
start() {
let server = http.createServer(this.handleRequest);
let {host, port} = this.config;
server.listen({
host,
port
}, () => {
debug(`服务器已经启动在 http://${host}:${chalk.greenBright(port)}/ 上...`);
});
}
复制代码
当启动了服务,用户在浏览器中输入了一个地址,此地址可能是一个目录,也有可能是一个确切的文件,所以要分别处理。
let {pathname} = url.parse(req.url, true);
let resource = path.join(this.config.dir, pathname);
let s = await stat(resource);
if (s.isDirectory()) {
//是目录
} else {
//是文件
}
复制代码
以上代码,首先使用url的模块parse方法将浏览器地址解析出来得到绝对路径,并使用fs模块的stat方法判断是否是一个目录,这里使用了async-await操作简化了异步代码。
当是目录的时候,我们的工作就是把目录下的所有文件都读取出来并展示给用户:
let files = await readdir(resource);//目录下的所有文件
files = files.map((i) => {
return {
fileName: i,
path: path.join(pathname, i)
}
});
let html = ejs.render(temp, {files});
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(html);
复制代码
其中fs.readdir()
方法可以将当前目录下的所有文件全部返回,它返回一个Array类型,使用map映射将文件地址拼接到对象中的目的则是为了ejs模版渲染。这样在模版中循环输出,即可将当前目录下的所有文件展示在浏览器中。
ejs模版:
<ul>
<%files.forEach((i)=>{%>
<li><a href="<%=i.path%>"><%=i.fileName%></a></li>
<%})%>
</ul>
复制代码
若用户访问的是个确切的文件,那么使用流的方式输出即可:
let contentType = mime.getType(filePath)
? mime.getType(filePath)
: 'text/plain';
res.setHeader('Content-Type', contentType + ';charset=utf8');
fs.createReadStream(filePat).pipe(res);
复制代码
可以看到使用mime模块得到真正的内容类型,使用流pipe到响应对象中即可。
使用命令行运行
到此为止代码已基本实现,但是运行程序时依然使用node命令来运行,这显然不符合要求,比如http-server包,在全局安装之后是使用http-server
命令来执行。那么怎么也达到这样的效果呢?
配置命令行工具
在package.json文件中,使用bin配置节来配置自己的命令:
package.json:
"bin": {
"my-http-server": "bin/www"
},
复制代码
这个配置节的意义是当执行my-http-server命令,运行bin/www文件。
接下来就是在项目目录中创建bin/www文件:
#! /usr/bin/env node
console.log('hello');
复制代码
其中井号惊叹号!/usr/bin/env node
是要说明去环境变量中找到node所在的目录,使用node来执行下面的脚本。
这时,打开控制台使用npm link命令把我们的命令链接到全局
这时就可以正确使用我们自己编写的命令了:
> my-http-server
复制代码
执行结果:
hello
复制代码
使用命令行传递用户参数
配置好命令行工具后,下面的任务就是处理命令行参数,以达到用户自己指定端口的目的。
yargs包
处理命令行参数,完全可以自己写代码截取,但是第三方yargs已经做好了这部分工作,我们只需安装使用就好,使用npm安装:npm install yargs
使用方法:
#! /usr/bin/env node
const yargs=require('yargs').argv;
console.log(yargs);
复制代码
此时执行命令行,可以使用参数:
my-http-server --port 3000
复制代码
运行结果:
_: [],
help: false,
version: false,
port: 3000,
'$0': '/usr/local/bin/my-http-server' }
复制代码
可以看到,使用yargs包,可以将命令行参数转为对象,我们可以很方便的拿到port端口号进行操作。
配置命令并传参覆盖默认参数:
#! /usr/bin/env node
const yargs=require('yargs');
const Server=require('../src/app');
let commandParameter=yargs.option('port',{
alias:'p',
default:3000,
type:Number,
description:'端口号'
}).option('host',{
default:'127.0.0.1',
type:String,
description:'IP地址,默认为127.0.0.1'
}).option('dir',{
default:process.cwd(),
type:String,
description:'静态文件地址,默认为当前目录'
}).usage('my-http-server [options]').argv;
new Server(commandParameter).start();
复制代码
这样,在www文件中接收到的参数传入new Server(),覆盖掉默认参数即可:
constructor(args) {
this.config = {
...config,
...args //命令行参数覆盖默认参数
};
}
复制代码