[Node]创建静态资源服务器

项目初始化

在这里插入图片描述

.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
 ${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
 ${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(/(gzip|deflate)/)) {
		return rs
	} else if (acceptEncoding.match(/gzip/)) {
		res.setHeader('Content-Encoding', 'gzip')
		return rs.pipe(createGzip())
	} else if (acceptEncoding.match(/deflate/)) {
		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

完结

原文地址:https://www.cnblogs.com/izhaong/p/12154271.html