[Node]创建静态资源服务器
项目初始化

.gitignore
cnpm i eslint -Deslint --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: bytesContent-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 缓存
- 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 loginnpm publish
安装
npm install xiaozhong-server
使用方法
xiaozhongserver # 把当前文件夹作为静态资源服务器根目录
xiaozhongserver -p 8080 # 设置端口号为 8080
xiaozhongserver -h localhost # 设置 host 为 localhost
xiaozhongserver -d /usr # 设置根目录为 /usr
完结
[Node]创建静态资源服务器的更多相关文章
- 使用node搭建静态资源服务器
安装 npm install yumu-static-server -g 使用 shift+鼠标右键 在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...
- node 创建静态web服务器(下)(处理异步获取数据的两种方式)
接上一章. 上一章我们说创建的静态web服务器只能识别html,css,js文件,功能较为单一,且图片格式为text/html,这是不合理的. 本章,我们将解决该问题. 这里,我们先准备好一个json ...
- node 创建静态web服务器(上)
声明:本文仅用来做学习记录. 本文将使用node创建一个简单的静态web服务器. 准备工作: 首先,准备好一个类似图片中这样的页面 第一步: 创建 http 服务: const http = requ ...
- 使用Node.js搭建静态资源服务器
对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...
- 原生node写一个静态资源服务器
myanywhere 用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解. 相关知识 es6及es7语法 http的相关网络知识 响应头 缓存相关 压缩相 ...
- node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)
我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...
- 极简 Node.js 入门 - 5.3 静态资源服务器
极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...
- 使用 Express 实现一个简单的 SPA 静态资源服务器
背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...
- Nginx——静态资源服务器(一)
java web的项目中,我们经常将项目部署到Tomcat或者jetty上,可以通过Tomcat或者jetty启动的服务来访问静态资源.但是随着Nginx的普及,用Nginx来作为静态资源服务器,似乎 ...
随机推荐
- 【算法•日更•第三十九期】迭代加深搜索:洛谷SP7579 YOKOF - Power Calculus 题解
废话不多说,直接上题: SP7579 YOKOF - Power Calculus 题意翻译 (略过没有营养的题干) 题目大意: 给出正整数n,若只能使用乘法或除法,输出使x经过运算(自己乘或除自己, ...
- 喵的Unity游戏开发之路 - 推球:游戏中的物理
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发. 本文不 ...
- docker简记
title: docker学习简记 date: 2019-10-16 15:10:39 tags: docker Docker简记 1:Docker简介 1)出现背景 一款产品从开发到上线,从操作系统 ...
- Word Count(C语言)
1.项目地址 https://github.com/namoyuwen/word-count 2.项目相关要求 2.1 项目描述 Word Count 1. 实现一个简单而完整的软件工具(源程序 ...
- Jmeter+Ant+jenkins实现api自动化测试的持续集成
0基础上手教程 @jmeter的使用 jmeter是一个基于java语言编写的开源测试工具,广泛应用于接口测试,性能测试,自动化测试. 接口自动化的编写教程,将于后续分享. 问题一:为什么用ant,而 ...
- 兄弟,别再爬妹子图了整点JS逆向吧--陆金所密码加密破解
好久没有写爬虫文章了,今晚上得空看了一下陆金所登录密码加密,这个网站js加密代码不难,适合练手,篇幅有限,完整js代码我放在了这里从今天开始种树,不废话,直接开整. 前戏热身 打开陆金所网站,点击到登 ...
- 使用VS开发的一个开机自启动启动、可接收指定数据关闭电脑或打开其他程序
使用VS开发的一个开机自启动启动.可接收指定数据关闭电脑或打开其他程序需要注意的几点 为了能够在其他电脑上运行自己写的程序,需要在VS改一下编译的运行库.(项目->属性->配置属性-> ...
- #企业项目实战 .Net Core + Vue/Angular 分库分表日志系统六 | 最终篇-通过AOP自动连接数据库-完成日志业务
教程预览 01 | 前言 02 | 简单的分库分表设计 03 | 控制反转搭配简单业务 04 | 强化设计方案 05 | 完善业务自动创建数据库 06 | 最终篇-通过AOP自动连接数据库-完成日志业 ...
- python练习 - 文本的平均列数+CSV格式清洗与转换
文本的平均列数 描述 打印输出附件文件的平均列数,计算方法如下: ...
- PAT甲级1151(由前序和中序确定LCA)
The lowest common ancestor (LCA) of two nodes U and V in a tree is the deepest node that has both U ...