项目初始化
.gitignore
cnpm i eslint -D
-
eslint --init
得到.eslintrc.js
.eslintrc.js
module.exports = {
'env': {
'browser': true,
'commonjs': true,
'es6': true
},
'extends': 'eslint:recommended',
'globals': {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly'
},
'parserOptions': {
'ecmaVersion': 2018
},
'rules': {
'indent': [
'error',
'tab'
],
'linebreak-style': [
'error',
'windows'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'never'
]
}
}
- 创建
eslintignore
.eslintignore
build/
node_modules
- 全局安装
supervisor
实现文件热更新
初步实现
- 创建
src/app.js
app.js
const http = require('http')
const conf = require('./config/defaultConfig')
const chalk = require('chalk')
const path = require('path')
const route = require('./helper/route.js')
const server = http.createServer((req, res) => {
const filePath = path.join(conf.root, req.url)
route(req, res, filePath)
})
server.listen(conf.port, conf.hostname, () => {
const addr = `http://${conf.hostname}:${conf.port}`
console.info(`Server running at ${chalk.green(addr)}`)
})
- 创建
src/config/defaultConfig.js
defaultConfig.js
module.exports = {
root: process.cwd(), // 当前路径
hostname: '127.0.0.1',
port: 3000
}
- 创建
src/hepler/route.js
route.js
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.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8'
})
fs.createReadStream(filePath).pipe(res)
} else if (stats.isDirectory()) { // 如果是文件夹, 返回文件列表
const files = await readdir(filePath)
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8'
})
res.end(files.join(','))
}
} catch (err) {
// 如果不存在
console.error(err)
res.write(404, {
'Content-Type': 'text/plain; charset=utf-8'
})
res.end(`${filePath} is not directory or file\n ${err.toString()}`)
}
}
- 热启动服务
supervisor app.js
效果图
结合handlebars进一步
- 创建
src/template/dir.tpl
dir.tpl
<!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;
}
</style>
</head>
<body>
{{#each files}}
<a href="{{../dir}}/{{this}}">{{this}}</a>
{{/each}}
</body>
</html>
- 安装
cnpm i handlebars -S
router.js
const fs = require('fs')
const path = require('path')
const promisify = require('util').promisify
const stat = promisify(fs.stat)
const readdir = promisify(fs.readdir)
const config = require('../config/defaultConfig')
const Handlebars = require('handlebars')
const tplPath = path.join(__dirname, '../template/dir.tpl')
const source = fs.readFileSync(tplPath)
const template = Handlebars.compile(source.toString())
module.exports = async function (req, res, filePath) {
try {
const stats = await stat(filePath)
// 如果是文件, 返回内容
if (stats.isFile()) {
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8'
})
fs.createReadStream(filePath).pipe(res)
} else if (stats.isDirectory()) { // 如果是文件夹, 返回文件列表
const files = await readdir(filePath)
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8'
})
const dir = path.relative(config.root, filePath)
const data = {
title: path.basename(filePath),
dir: dir ? `/${dir}` : '',
files
}
res.end(template(data))
}
} catch (err) {
// 如果不存在
console.error(err)
res.write(404, {
'Content-Type': 'text/plain; charset=utf-8'
})
res.end(`${filePath} is not directory or file\n ${err.toString()}`)
}
}
效果图
- 优化
Content-Typy
- 新建文件
src/helper/mime.js
const path = require('path')
const mimeTypes = {
'css': 'text/css',
'gif': 'image/gif',
'html': 'text/html',
'ico': 'image/x-icon',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'pdf': 'application/pdf',
'png': 'image/png',
'svg': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
'tiff': 'image/tiff',
'txt': 'text/plain',
'wav': 'audio/x-wav',
'wma': 'audio/x-ms-wma',
'wmv': 'video/x-ms-wmv',
'xml': 'text/xml'
}
module.exports = (filePath) => {
// 获取后缀名
let ext = path.extname(filePath)
.split('.')
.pop()
.toLowerCase()
if (!ext) {
ext = filePath
}
return mimeTypes[ext] || mimeTypes['txt']
}
- 应用
router.js
// 处理contentType
const mime = require('./mime')
...
const data = {
title: path.basename(filePath),
dir: dir ? `/${dir}` : '',
files: files.map(file => {
return {
file,
icon: mime(file)
}
})
}
dir.tpl
<body>
{{#each files}}
<a href="{{../dir}}/{{file}}">【{{icon}}】{{file}}</a>
{{/each}}
</body>
压缩文件
config/defaultConfig.js
module.exports = {
root: global.process.cwd(), // 当前路径
hostname: '127.0.0.1',
port: 3000,
compress: /\.(html|js|css|md)/
}
- 创建文件
helper/compress.js
const {createGzip, createDeflate} = require('zlib')
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())
}
}
route.js
// 压缩文件
const compress = require('./compress')
...
// fs.createReadStream(filePath).pipe(res)
let rs = fs.createReadStream(filePath)
if (filePath.match(config.compress)) {
rs = compress(rs, req, res)
}
rs.pipe(res)
-
range范围
range: bytes=[start]-[end]
Accept-Ranges: bytes
Content-Range: bytes start-end/total
创建文件
src/helper/range.js
range.js
module.exports = (totalSize, req, res) => {
const range = req.headers['range']
if (!range) {
return {
code: 200
}
}
const sizes = range.match(/bytes=(\d*)-(\d*)/) // [匹配到的内容, 第一个分组, 第二个分组]
const end = sizes[2] ? parseInt(sizes[2]) : totalSize - 1
const start = sizes[1] ? parseInt(sizes[1]) : totalSize - end
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: start,
end: end
}
}
- 应用
route.js
// range
const range = require('./range')
...
// 如果是文件, 返回内容
if (stats.isFile()) {
const contentType = mime(filePath)
res.writeHead(200, {
'Content-Type': `${contentType}; charset=utf-8`
})
// fs.createReadStream(filePath).pipe(res)
// let rs = fs.createReadStream(filePath)
let rs
const {code, start, end} = range(stats.size, req, res)
if (code === 200) {
rs = fs.createReadStream(filePath)
} else {
rs = fs.createReadStream(filePath, {start, end})
}
if (filePath.match(config.compress)) {
rs = compress(rs, req, res)
}
rs.pipe(res)
}
- 直接用浏览器访问指定range有些困难, 使用
curl
查看效果 - 使用Linux命令行工具输入
CURL -I http://127.0.0.1:3000/LICENSE
- 指定rang
curl -r 1-10 -i http://127.0.0.1:3000/LICENSE
缓存
Created with Raphaël 2.2.0用户请求本地缓存client失效server未改变304本地缓存协商缓存 返回响应请求资源yesnoyesnoyesno
- Expires, Cache-Control
- If-Modified-Since / Last-Modified
- If-None-Match / ETag
- 创建文件
src/helper/cache.js
cache.js
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.toUTCString()}`); // mtime 需要转成字符串,否则在 windows 环境下会报错
}
}
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;
}
if (etag && etag !== res.getHeader('ETag')) {
return false;
}
return true;
};
- 应用
route.js
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});
}
if (filePath.match(config.compress)) {
rs = compress(rs, req, res);
}
rs.pipe(res);
}
CLI
- 借助命令行工具Yargs
- 安装
cnpm i yargs -S
- 创建文件
src/index.js
index.js
const yargs = require('yargs')
const Server = require('./app')
const argv = yargs
.usage('anywhere [options]')
.option('p', {
alias: 'port',
describe: '端口号',
default: 9527
})
.option('h', {
alias: 'hostname',
describe: 'host',
default: '127.0.0.1'
})
.option('d', {
alias: 'root',
describe: 'root path',
default: global.process.cwd()
})
.version()
.alias('v', 'version')
.help()
.argv
const server = new Server(argv)
server.start()
app.js
const http = require('http')
const conf = require('./config/defaultConfig')
const chalk = require('chalk')
const path = require('path')
const route = require('./helper/route.js')
class Server {
constructor(config) {
this.conf = Object.assign({}, conf, config)
}
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 started at ${chalk.green(addr)}`)
})
}
}
module.exports = Server
route.js
// const config = require('../config/defaultConfig')
快捷打开
- 创建
bin/server
server
#! /usr/bin/env node
require('../src/index')
package.json
"main": "src/app.js",
"bin": {
"xiaozhongserver": "bin/server"
},
- 用linux执行这个文件是没有权限的
- 加上执行权限
chmod +x bin/xiaozhong-server
- 赋予权限后显示
-rwxr-xr-x 1 16416 197609 47 6月 29 17:27 bin/xiaozhong-server*
bin/xiaozhong-server -9898
显示效果
- 发布npm包
npm login
npm publish
安装
npm install xiaozhong-server
使用方法
xiaozhongserver # 把当前文件夹作为静态资源服务器根目录
xiaozhongserver -p 8080 # 设置端口号为 8080
xiaozhongserver -h localhost # 设置 host 为 localhost
xiaozhongserver -d /usr # 设置根目录为 /usr