项目初始化

.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
  • 指定rangcurl -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

完结

[Node]创建静态资源服务器的更多相关文章

  1. 使用node搭建静态资源服务器

    安装 npm install yumu-static-server -g 使用 shift+鼠标右键  在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...

  2. node 创建静态web服务器(下)(处理异步获取数据的两种方式)

    接上一章. 上一章我们说创建的静态web服务器只能识别html,css,js文件,功能较为单一,且图片格式为text/html,这是不合理的. 本章,我们将解决该问题. 这里,我们先准备好一个json ...

  3. node 创建静态web服务器(上)

    声明:本文仅用来做学习记录. 本文将使用node创建一个简单的静态web服务器. 准备工作: 首先,准备好一个类似图片中这样的页面 第一步: 创建 http 服务: const http = requ ...

  4. 使用Node.js搭建静态资源服务器

    对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...

  5. 原生node写一个静态资源服务器

    myanywhere 用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解. 相关知识 es6及es7语法 http的相关网络知识 响应头 缓存相关 压缩相 ...

  6. node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)

    我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...

  7. 极简 Node.js 入门 - 5.3 静态资源服务器

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  8. 使用 Express 实现一个简单的 SPA 静态资源服务器

    背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...

  9. Nginx——静态资源服务器(一)

    java web的项目中,我们经常将项目部署到Tomcat或者jetty上,可以通过Tomcat或者jetty启动的服务来访问静态资源.但是随着Nginx的普及,用Nginx来作为静态资源服务器,似乎 ...

随机推荐

  1. 【算法•日更•第三十九期】迭代加深搜索:洛谷SP7579 YOKOF - Power Calculus 题解

    废话不多说,直接上题: SP7579 YOKOF - Power Calculus 题意翻译 (略过没有营养的题干) 题目大意: 给出正整数n,若只能使用乘法或除法,输出使x经过运算(自己乘或除自己, ...

  2. 喵的Unity游戏开发之路 - 推球:游戏中的物理

    很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发. 本文不 ...

  3. docker简记

    title: docker学习简记 date: 2019-10-16 15:10:39 tags: docker Docker简记 1:Docker简介 1)出现背景 一款产品从开发到上线,从操作系统 ...

  4. Word Count(C语言)

    1.项目地址 https://github.com/namoyuwen/word-count 2.项目相关要求 2.1 项目描述 Word Count    1. 实现一个简单而完整的软件工具(源程序 ...

  5. Jmeter+Ant+jenkins实现api自动化测试的持续集成

    0基础上手教程 @jmeter的使用 jmeter是一个基于java语言编写的开源测试工具,广泛应用于接口测试,性能测试,自动化测试. 接口自动化的编写教程,将于后续分享. 问题一:为什么用ant,而 ...

  6. 兄弟,别再爬妹子图了整点JS逆向吧--陆金所密码加密破解

    好久没有写爬虫文章了,今晚上得空看了一下陆金所登录密码加密,这个网站js加密代码不难,适合练手,篇幅有限,完整js代码我放在了这里从今天开始种树,不废话,直接开整. 前戏热身 打开陆金所网站,点击到登 ...

  7. 使用VS开发的一个开机自启动启动、可接收指定数据关闭电脑或打开其他程序

    使用VS开发的一个开机自启动启动.可接收指定数据关闭电脑或打开其他程序需要注意的几点 为了能够在其他电脑上运行自己写的程序,需要在VS改一下编译的运行库.(项目->属性->配置属性-> ...

  8. #企业项目实战 .Net Core + Vue/Angular 分库分表日志系统六 | 最终篇-通过AOP自动连接数据库-完成日志业务

    教程预览 01 | 前言 02 | 简单的分库分表设计 03 | 控制反转搭配简单业务 04 | 强化设计方案 05 | 完善业务自动创建数据库 06 | 最终篇-通过AOP自动连接数据库-完成日志业 ...

  9. python练习 - 文本的平均列数+CSV格式清洗与转换

    文本的平均列数 描述 打印输出附件文件的平均列数,计算方法如下:‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬ ...

  10. 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 ...