Fork me on GitHub

nodeJS搭建一个静态资源服务器

脚手架搭建

在github上新建一个仓库,然后选择一个node版本的.gitignore文件,然后将项目从github上面拉取下来.

对项目进行node模块初始化

1
npm init

有兴趣可以自行配置一下eslint和editconfig(主要用于代码规范的设置)

模块安装

1
2
3
4
5
npm install chalk #美化终端而已
npm install -g nodemon #主要用于每次服务器的重启
npm install -g supervisor #也可以用这个(和nodemon效果差不多)
npm install handlebars # 用于模板引擎的功能
npm install yargs # 使用这个包来进行cli工具的制作

supervisor可以用来监听文件的状态

简单的http模块的使用

然后新建一个src目录用于存储本次nodeJS服务的全部代码,在src目录下面新建一个config的文件夹(用于保存一些配置文件),里面写一个defaultConfig.js的文件

src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http')
const conf = require('./config/defaultConfig')
const chalk = require('chalk');
const server = http.createServer((req,res)=>{
res.statusCode = 200;
res.setHeader('Content-type','text/html');
res.write('<html><body><h2>chynb</h2></body></html>')
res.end();
});
server.listen(conf.port,conf.hostname,()=>{
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server stared at ${chalk.green(addr)}`);
});

src/config/defaultConfig.js

1
2
3
4
module.exports = {
hostname: '127.0.0.1',
port:'8887'
};

通过上面一些代码我们通过运行nodemon src/app.js就可以在对应的url里面访问到我们想要的地址了.

对文件进行操作

上面我们成功启动了一个简单的静态nodejs服务器,接下来我们对做一些路径的书写
我们进一步对代码进行一个拆分:

  1. 当我们访问某一个文件时(例如localhost:8888/a.js),输出这个文件的目录下面的名称
  2. 当我们访问某一个文件夹的时候(例如localhost:8888/src),直接输出这个目录下面所有的文件名称

src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http')
const conf = require('./config/defaultConfig')
const chalk = require('chalk');
const path = require('path');
const route = require('./helper/route')
const server = http.createServer((req,res)=>{
const filePath = path.join(conf.root,req.url);
route(req,res,filePath) // route函数来执行一个异步的操作,通过async和await函数来降低一层异步的调用
});
server.listen(conf.port,conf.hostname,()=>{
const addr = `http://${conf.hostname}:${conf.port}`;
console.info(`Server stared at ${chalk.green(addr)}`);
});

src/helper/route.js
主要用于封装判断文件和文件夹函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const fs = require('fs') // 用于判断模块是文还是文件夹
const promisify = require('util').promisify;
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir); // 让一些异步操作能够写成同步的形式
module.exports = async function(req,res,filePath){
try {
const stats = await stat(filePath)
if(stats.isFile()){ // 判断是否为文件
res.statusCode = 200;
res.setHeader('Content-Type','text/plain')
fs.createReadStream(filePath).pipe(res)
} else if(stats.isDirectory()) { // 判断是否为文件夹
const files = await readdir(filePath); // 这里一定要加上await的关键字
res.statusCode = 200;
res.setHeader('Content-Type','text/plain');
res.end(files.join(','))
}
} catch(ex) {
console.log(ex)
res.statusCode = 404;
res.setHeader('Content-Type','text/plain');
res.end("some bugs may happen on demo ")
}
}

模板引擎优化

完成了文件和文件夹的基本读取功能之后,我们做一个能够将文件夹显示在网页上面的操作,这里使用模板引擎的方式。(相当于让每个文件夹用类似live-server的形式显示出来),使用一个模板引擎里面带一个a标签.

模板引擎使用的是handleBars

根据官方文档的写法,写一个模板(类似于webpack里面的打包模板)

src/template/dir.tpl.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!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>{{title}}</title>
<style>
body{
margin: 30px;
}
a{
display: block;
font-size: 30px;
}
</style>
</head>
<body>
{{#each files}}
<a href="{{../dir}}/{{this}}">{{this}}</a>
<!-- 文件夹的地方加上文件名 -->
{{/each}}
</body>
</html>

然后修改一波route.js里面的代码(让文件夹能够完整的显示出来,并且能够访问)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
...some code
*/
const tplPath = path.join(__dirname,'../template/dir.tpl.html')
const source = fs.readFileSync(tplPath);
// fs会读出来一个buffer文件流,这里在传入模板引擎去进行编译的时候修改为字符串类型
const template = HandleBars.compile(source.toString())
// ...
else if(stats.isDirectory()) {
const files = await readdir(filePath); // 这里一定要加上await的关键字
res.statusCode = 200;
res.setHeader('Content-Type','text/html');
const dir = path.relative(config.root,filePath)
console.log(chalk.green(dir))
const data = {
titles:path.basename(filePath),
dir: dir? `/${dir}` : '',
files
};
res.end(template(data))
}

这样我们就可以在启动的服务器中访问到我们的文件夹列表了.

文件返回类型判断

一般在http请求中我们需要对文件的返回类型做一个判断,在服务器端(一般要对自己返回的文件类型的返回类型做一个类型判断).

这里需要用到mime类型。

于是新建一个辅助函数
helper/mime.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path')
const mimeTypes = {
//...这里的代码详请点击https://github.com/fireairforce/nodejs-server/tree/master/src/helper/mime.js
}
module.exports = (filePath) =>{
let ext = path.extname(filePath).split('.').pop().toLowerCase();
//读取拓展名,然后根据wd.qaq.js进行分割,取出最后的一部分
if(!ext){
ext = filePath;
}
return mimeTypes[ext] || mimeTypes['txt']
};

然后对route.js中的文件类型的返回值做一个修改
src/helper/route.js

1
2
3
4
5
6
7
8
9
// ...
const mime = require('../helper/mime')
// ...
if(stats.isFile()){
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader('Content-Type',contentType)
fs.createReadStream(filePath).pipe(res)
}

文件类型标注

src/helper/route.js

1
2
3
4
5
6
7
8
- files
+ files:files.map(file=>{
+ return {
+ file,
+ icon: mime(file)
+ }
+ })
};

这个时候模板的地方也要修改一样

1
2
- <a href="{{../dir}}/{{this}}">{{this}}</a>
+ <a href="{{../dir}}/{{file}}">[{{icon}}]{{file}}</a>

文件压缩

我们可以通过在服务端设置返回文件的类型来对一些文件进行压缩.

现在配置里面添加一个正则表达式用于匹配压缩文件:

1
2
3
4
5
6
module.exports = {
root: process.cwd(), // 返回当前的工作目录
hostname: '127.0.0.1',
port:8888,
compress:/\.(html|css|js|md)/ //设置需要压缩的文件的后缀名
};

由于主要是对文件进行操作,所以我们只需要在文件这边进行操作就可以了.
helper/compress.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { createGzip,createDeflate } = require('zlib')
// 通过nodejs里面的一个包来对文件进行压缩
module.exports = (rs, req, res) => {
const acceptEncoding = req.headers['accept-encoding'];
if(!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)){
return rs
} else if(acceptEncoding.match(/\bgzip\b/)){
res.setHeader('Content-Encoding','gzip')
return rs.pipe(createGzip());
} else if(acceptEncoding.match(/\bdeflate\b/)){
res.setHeader('Content-Encoding','deflate');
return rs.pipe(createDeflate());
}
};

然后在src/helper/route.js里面设置一波文件的压缩类型

1
2
3
4
5
6
7
8
9
10
11
12
13
const compress = require('./compress')
// ...
if(stats.isFile()){
const contentType = mime(filePath);
res.statusCode = 200;
res.setHeader('Content-Type',contentType)
let rs = fs.createReadStream(filePath);
if(filePath.match(config.compress)){ // 如果文件符合被压缩的类型
rs = compress(rs,req,res);
}
// fs.createReadStream(filePath).pipe(res)
rs.pipe(res)
}

range请求范围的设定

range(设置文件大小的类型)

  • range:bytes = [start]-[end]
  • Accept-Ranges:bytes
  • Content-Range:bytes start-end/total

src/helper/range.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = (totalSize,req,res) =>{
const range = req.headers['range'] // 在请求头中拿到range
if(!range) {
// 处理不了的情况就直接返回回去
return {code:200};
}
const sizes = range.match(/bytes=(\d*)-(\d*)/);
console.log(size);
const end = sizes[2] || totalSize - 1;
const start = sizes[1] || totalSize - 1;
if(start > end || start<0 || end > totalSize){
return { code :200 }
}
res.setHeader('Accept-Ranges','bytes');
res.setHeader('Content-Range',`bytes ${start}-${end}/${totalSize}`);
res.setHeader('Content-Length',end-start);
return {
code :206,
start:parseInt(start),
end:parseInt(end)
}
}

然后在route中使用,对于不同的文件相应返回的时候通过设定其返回的range值进行切割,从而来优化http的性能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// some codes
const range = require('./range')
//...
const contentType = mime(filePath);
res.setHeader('Content-Type',contentType)
let rs;
const { code , start , end } = range(stats.size,req,res)
if(code === 200) {
res.statusCode = 200;
rs = fs.createReadStream(filePath);
} else {
res.statusCode = 206;
rs = fs.createReadStream(filePath, { start , end })
}
rs = fs.createReadStream(filePath);
if(filePath.match(config.compress)){
rs =compress(rs,req,res);
}
// fs.createReadStream(filePath).pipe(res)
rs.pipe(res)

缓存优化

通过设置本地缓存来优化性能,在响应头里面设置一波cache-control等.

主体还是http那一块的知识,这里不详细介绍,主要就是基于以下的几个http请求头去进行设置.

  • Expires,Cache-Control
  • If-Modified-Since/Last-Modified
  • If-None-Match/Etag

缓存这里就不想过多介绍了,关于性能优化的一个比较多的环节
src/helper/cache.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const { cache } = require('../config/defaultConfig');
function refreshRes(stats,res){
const { maxAge,expires,cacheControl,lastModified,etag } = cache
if(expires){
res.setHeader('Expires',(new Date(Date.now() + maxAge*1000)).toUTCString())
}
if(cacheControl){
res.setHeader('Cache-Control',`public,max-age = ${maxAge}`)
}
if(lastModified){
res.setHeader('Last-Modified',stats.mtime.toUTCString())
}
if(etag) {
res.setHeader('ETag',`${stats.size}-${stats.mtime}`);
}
}
// 用于判断是否需要启用缓存
module.exports = function isFresh(stats,req,res){
refreshRes(stats,res);
const lastModified = req.headers['if-modified-since'];
const etag = req.headers['if-none-match'];
// 如果客户端以上两个信息都没有返回,那就是第一次请求
if(!lastModified && !etag){
return false;
}
if(lastModified&&lastModified!==res.getHeader('Last-Modified')){
return false;
}
// 如果客户端的etag和服务端的etag是不同的.
if(etag&&etag!==res.getHeader('ETag')){
return false;
}
return true;
}

然后在route.js里面使用一波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...some code
const isFresh = require('./cache')
// ...
if(stats.isFile()){
const contentType = mime(filePath);
res.setHeader('Content-Type',contentType)
// 如果缓存还存在
if(isFresh(stats,req,res)){
res.statusCode = 304;
res.end()
return;
}
let rs;
const { code , start , end } = range(stats.size,req,res)
if(code === 200) {
res.statusCode = 200;
rs = fs.createReadStream(filePath);
} else {
res.statusCode = 206;
rs = fs.createReadStream(filePath, { start , end })
}
rs = fs.createReadStream(filePath);
if(filePath.match(config.compress)){
rs =compress(rs,req,res);
}
// fs.createReadStream(filePath).pipe(res)
rs.pipe(res)
}

将包发布成cli工具

接下来我们要做的就是使用npm包将我们的项目写为一个命令行工具然后发布到npm的包上面

首先我们需要设置一个入口文件,这个入口文件就是每次我们去使用我们的工具的时候,调用的入口文件,里面会包含包括工具的版本信息,使用方式等等
我们的cli工具得满足下面的一些要求:

1
2
3
4
5
6
7
8
9
10
download:
npm install -g anydoor
usage:
anydoor # start the server
anydoor -p 8080 # set the port as 8080
anydoor -h localhost #set the port as localhost

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const yargs = require('yargs');
const Server = require('./app');
const argv = yargs.usage('anywhere [options]').option('p',{
alias: 'port',
describe:'端口号',
default: 8888
}).option('h',{
alias:'hostname',
describe:'host',
default:'127.0.0.1'
}).option('d',{
alias:'root',
describe: 'root path',
default:process.cwd()
}).version().alias('v','version').help().argv;
const server = new Server(argv);
server.start();

这个时候由于一些参数并不是和之前一样直接调用defaultConfig.js中的了,所以我们需要重写一个app.js

src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const http = require('http')
const conf = require('./config/defaultConfig')
const chalk = require('chalk');
const path = require('path');
const route = require('./helper/route')
class Server {
constructor(config){
// 配置文件并不是和之前一样使用固定的端口号
// 有可能为用户设置,因而我们需要对其来一波设置
this.conf = Object.assign({},conf,config) // 对配置文件进行merge
}
start() {
const server = http.createServer((req,res)=>{
const filePath = path.join(this.conf.root,req.url);
route(req,res,filePath,this.conf)
});
server.listen(this.conf.port,this.conf.hostname,()=>{
const addr = `http://${this.conf.hostname}:${this.conf.port}`;
console.info(`Server stared at ${chalk.green(addr)}`);
});
}
}
module.exports = Server;

这个时候this.conf就通过参数传递到了route.js之中,然后route.js里面的函数只需要多使用加一个config参数就可以了.

packge.json同时也要做一些修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"name": "nodejs-server",
"version": "1.0.0",
"description": "using for leanring Node JS",
"main": "./src/app.js",
"bin":{
"anydoor":"bin/anydoor"
},
"scripts": {},
"repository": {
"type": "git",
"url": "git+https://github.com/fireairforce/nodejs-server.git"
},
"keywords": [],
"author": "fireairforce",
"license": "MIT",
"bugs": {
"url": "https://github.com/fireairforce/nodejs-server/issues"
},
"homepage": "https://github.com/fireairforce/nodejs-server#readme",
"dependencies": {
"chalk": "^2.4.2",
"handlebars": "^4.1.2",
"yargs": "^13.2.2"
}
}

由于一般使用的是bin的脚本命令,所以写一个:
./bin/anydoor(没有后缀名的文件)

1
2
3
#! /usr/bin/env node
require('../src/index');

这个时候的执行方式为:

1
2
chmod -x bin/anydoor # 给文件加一个执行权限
bin/anydoor -p 9999

这里稍微介绍一下版本号的语义化
x.y.z(例如0.0.1)

  • 一般修复了bug就新增z位
  • 新增了功能就升级一个y位
  • 大版本升级就升级x位(比如说不和之前兼容,API和之前不同了)

一般linux中x为偶数位为稳定版本,奇数位为不稳定版本(并不指代所有)

发布npm包

1
2
npm login #登录你自己的npm账号
npm publish #发布包到npm官网上面去

项目源码

https://github.com/fireairforce/nodejs-server

-------------本文结束感谢您的阅读-------------