如何搭一个静态服务
- 新建一个文件夹
- 初始化
npm init -y - 所用到的模块
http-server起服务mime chalk debug ejs所需模块http fs util path自带模块yargs
目录结构
启动一个服务需要 src/congfig文件
- 运行的条件 指定主机名
- 指定启动的端口号
- 自定运行的目录
通过config配置 app起服务 tepl编译 bin和package.json链接命令行
配置
config.js
//将配置挂载在我们的实例上,方便后期手动指定,直接get就可以
let path = require('path')
let config = {
hostname : '127.0.0.1', //主机
port:3000, //端口号
dir:path.join(__dirname, 'public')
}
module.exports = config;
tmpl
<!--ejs模版,当是文件夹的时候会用到-->
<body>
<%dirs.map(item=>{%>
<li><a href="<%=item.pathname%>"><%=item.filename%></a></li>
<%})%>
</body>
配置全局管理 (全局映射关系)
package.json
//添加bin配置
"bin": {
"zdl": "bin/www.js"
},
执行npm link
bin/www.js 用此文件作为入口,帮我们引用app.js
#! /usr/bin/env node
//yargs用法 在命令行输入`zdl --help`测试是否生效
// 第一执行了命令后 会执行 bin/www.js
let yargs = require('yargs')
let argv = yargs.option('port', {
alias: 'p',
default: 3000, //默认
demand: false, //是否必填
description: 'this is port'
}).option('hostname', {
alias: 'host',
default: 'localhost',
type: String, //类型
demand: false,
description: 'this is hostname'
}).option('dir', {
alias: 'd',
default: process.cwd(),
type: String,
demand: false,
description: 'this is cwd'
}).usage('zdl [options]').argv; //用法提示
//用此文件作为入口,帮我们引用app.js
let Server = require('../src/app');
new Server(argv).start(); // 开启服务
//相当于在app.js执行
//let server = new Server(argv);
//server.start();
app.js
架子
// 实现一个静态服务
let http = require('http');
let fs = require('fs');
let url = require('url');
let util = require('util');
let path = require('path');
let mime = require('mime');
let ejs = require('ejs'); // 渲染模板
let chalk = require('chalk'); // 粉笔
let debug = require('debug')('*');//所有的都输出
// 第二个参数可以指定环境变量在为什么值时才打印
// window set DEBUG=XXX export DEBUG=XXXX
class Server {
constructor(args) { //config在调用此文件时传参args
constructor(args) {
this.config = {...config,...args},
this.template = template;//渲染模版
}
async handleRequest(req, res) { // 这里的this都是实例
let { pathname } = url.parse(req.url, true);
let p = path.join(this.config.dir, pathname);
try{
let statObj = await stat(p);
if (statObj.isDirectory()) {//若是文件夹直接渲染数据
let dirs = await readdir(p);
dirs = dirs.map(dir => {
return {
filename: dir,
pathname: path.join(pathname, dir)
}
});
let str = ejs.render(this.template, { dirs, title: 'ejs' });//template渲染模板,数据,渲染结果是字符串
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(str);
} else {// 文件 发送文件
this.sendFile(req, res, p, statObj);
}
} catch (e) {// 文件不存在的情况
this.sendError(req, res, e);
}
}
//创建服务
start() {
let server = http.createServer(this.handleRequest.bind(this));//this.handleRequest,this是回调函数的this,也可以在该函数return 箭头函数
let { hostname, port } = this.config;
debug(`http://${hostname}:${chalk.green(port)} start`)
server.listen(port, hostname);
}
//错误处理
sendError(){
// 解析字符串打印对象
//debug(util.inspect(e).toString());
res.statusCode = 404;
res.end(`Not Found`);
}
//发送文件
sendFile(){
...
}
}
module.exports = Server;
添加功能
添加的三个功能都是http的应用,想更具体请参考node~http缓存
class Server {
// 添加缓存功能呢
cache(req, res, p, stat) {
}
//添加压缩功能
gzip(req, res, p, stat) {
}
//添加范围请求功能
range(req, res, p, stat) {
}
//如果是文件
sendFile(req, res, p, stat) {
}
}
如果是目录 就将目录中的内容展现出来. 如果是文件就将文件展示出来
cache
// 添加缓存功能呢
cache(req, res, p, stat) { //Catch-Control Expries if-modified-since if-none-match
let since = req.headers['if-modified-since'];
let match = req.headers['if-none-match'];
let ssince = stat.ctime.toUTCString();
let smatch = stat.ctime.getTime() + stat.size;
res.setHeader('Last-Modified', ssince);
res.setHeader('ETag', smatch);
res.setHeader('Cache-Control', 'max-age=6');
if (since != ssince) {
debug(since, ssince);
return false;
}
if (match != smatch) {
debug(match, smatch);
return false
}
return true;
}
//如果是文件
sendFile(req, res, p, stat) {
if (this.cache(req, res, p, stat)) {// 检测是否有缓存
res.statusCode = 304;
res.end();
return;
}
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
}
gzip
//添加压缩功能
gzip(req, res, p, stat) {
let header = req.headers['accept-encoding'];
if (header) {
if (header.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return zlib.createGzip();
} else if (header.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate')
return zlib.createDeflate();
}
} else {
return false;
}
}
//如果是文件
sendFile(req, res, p, stat) {
...
let compress = this.gzip(req, res, p, stat);
if (compress) { // 返回的是一个压缩流
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p).pipe(compress).pipe(res);
} else {
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p).pipe(res);
}
}
range
//添加范围请求功能
range(req, res, p, stat) {
let range = req.headers['range'];
if(range){
let [, start, end] = range.match(/(\d*)-(\d*)/) || [];
start = start ? parseInt(start) : 0;
end = end ? parseInt(end) : stat.size;
res.statusCode = 206;
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', end - start + 1);
res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
return { start, end };
}else{
return {start:0,end:stat.size}
}
}
//如果是文件
sendFile(req, res, p, stat) {
let { start, end } = this.range(req, res, p, stat);
if (compress) { // 返回的是一个压缩流
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(compress).pipe(res);
} else {
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
}
}
小结
以下是整个app.js的内容
// 实现一个静态服务
// 如果是目录 就将目录中的内容展现出来
// 如果是文件就将文件展示出来
let http = require('http');
let fs = require('fs');
let url = require('url');
let zlib = require('zlib');
let util = require('util');
let path = require('path');
let mime = require('mime');
let ejs = require('ejs'); // 渲染模板
let chalk = require('chalk'); // 粉笔
// 第二个参数可以指定环境变量在为什么值时才打印
// window set DEBUG=XXX export DEBUG=XXXX
let debug = require('debug')('*');
// 运行的条件 指定主机名
// 指定启动的端口号
// 自定运行的目录
let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);
let config = require('./config');
let template = fs.readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');
class Server {
constructor(args) {
this.config = { ...config, ...args };// 将配置挂载在我们的实例上
this.template = template;
}
async handleRequest(req, res) { // 这里的this都是实例
let { pathname } = url.parse(req.url, true);
let p = path.join(this.config.dir, pathname);
// 1.根据路径 如果是文件夹 显示文件夹里的内容
// 2.如果是文件 显示文件的内容
try { // 如果没错误说明文件存在
let statObj = await stat(p);
if (statObj.isDirectory()) {
// 现在需要一个当前目录下的解析出的对象或者数组
let dirs = await readdir(p);
dirs = dirs.map(dir => { // dirs就是要渲染的数据
return {
filename: dir,
pathname: path.join(pathname, dir)
}
});
let str = ejs.render(this.template, { dirs, title: 'ejs' });
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(str);
} else {
// 文件 发送文件
this.sendFile(req, res, p, statObj);
}
} catch (e) {
// 文件不存在的情况
this.sendError(req, res, e);
}
}
// 实现其他功能
// 实现范围请求
// 实现缓存
// 服务器 Cache-Control Expires
// Last-Modified ETag:ctime+size
// 客户端
// if-modified-since if-none-match
cache(req, res, p, stat) {
// 实现缓存
let since = req.headers['if-modified-since'];
let match = req.headers['if-none-match'];
let ssince = stat.ctime.toUTCString();
let smatch = stat.ctime.getTime() + stat.size;
res.setHeader('Last-Modified', ssince);
res.setHeader('ETag', smatch);
res.setHeader('Cache-Control', 'max-age=6');
if (since != ssince) {
debug(since, ssince);
return false;
}
if (match != smatch) {
debug(match, smatch);
return false
}
return true;
}
// 实现服务端压缩
gzip(req, res, p, stat) {
let header = req.headers['accept-encoding'];
if (header) {
if (header.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return zlib.createGzip();
} else if (header.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate')
return zlib.createDeflate();
}
} else {
return false;
}
}
range(req, res, p, stat) {
let range = req.headers['range'];
if(range){
let [, start, end] = range.match(/(\d*)-(\d*)/) || [];
start = start ? parseInt(start) : 0;
end = end ? parseInt(end) : stat.size;
res.statusCode = 206;
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', end - start + 1);
res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
return { start, end };
}else{
return {start:0,end:stat.size}
}
}
sendFile(req, res, p, stat) {
if (this.cache(req, res, p, stat)) {// 检测是否有缓存
res.statusCode = 304;
res.end();
return
};
let compress = this.gzip(req, res, p, stat);
let { start, end } = this.range(req, res, p, stat); //范围请求,返回开始位置和结束位置
if (compress) { // 返回的是一个压缩流
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(compress).pipe(res);
} else {
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
}
}
sendError(req, res, e) {
// 解析字符串打印对象
//debug(util.inspect(e).toString());
res.statusCode = 404;
res.end(`Not Found`);
}
start() {
let server = http.createServer(this.handleRequest.bind(this));
let { hostname, port } = this.config;
debug(`http://${hostname}:${chalk.green(port)} start`)
server.listen(port, hostname);
}
}
// 开启一个服务
module.exports = Server
现在你只需要在cmd里面输入zdl就可以启动一个静态服务了
发布到npm
在用的时候我们可以直接 npm install [name] 然后zdl运行