nodejs压缩成7z_NodeJS(五):静态资源服务器

1 前言

通过 http 模块,快速创建一个 http server demo,为了在控制台打印出有颜色的信息,可以安装 chalk 库:

npm i chalk

const http = require('http');

const chalk = require('chalk');

const hostname = '127.0.0.1';

const port = 3000;

const server = http.createServer((req, res) => {

res.statusCode = 200;

// res.end('Hello, World!\n');

// res.setHeader('Content-Type', 'text/plain');

res.setHeader('Content-Type', 'text/html;charset=utf-8');

res.write('')

res.write('

')

res.write('

')

res.write('测试')

res.write('')

res.write('')

res.write('')

res.end();

});

server.listen(port, hostname, () => {

console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);

});

在更改 node 代码的时候,需要不断的重启 node 服务,解决方法,可以全局安装 supervisor ,通过 supervisor 监听文件的变化,自动重启:

npm install supervisor -g

Examples:

supervisor myapp.js

supervisor myapp.coffee

supervisor -w scripts -e myext -x myrunner myapp

supervisor -w lib,server.js,config.js server.js

supervisor -- server.js -h host -p port

2 实例

根据访问的 url,如果 url 为文件,则读取文件的内容,如果为文件夹,则展示文件夹中的所有文件。

const http = require('http');

const path = require('path');

const fs = require('fs');

const chalk = require('chalk');

const { hostname, port, root } = require('./config');

const server = http.createServer((req, res) => {

const url = req.url;

const filePath = path.join(root, url);

fs.stat(filePath, (err, status) => {

if (err) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

res.end(`${filePath} 不存在!`);

return;

}

if (status.isFile()) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

fs.createReadStream(filePath).pipe(res);

return;

}

if (status.isDirectory()) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

fs.readdir(filePath, (err, files) => {

if (err) throw err;

res.end(files.join(', '));

return;

})

}

});

});

server.listen(port, hostname, () => {

console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);

});

config / index.js:

module.exports = {

root: process.cwd(), // 查看应用程序当前目录

hostname: '127.0.0.1',

port: 3000,

}

针对上面的代码进行异步优化:

const http = require('http');

const path = require('path');

const fs = require('fs');

const chalk = require('chalk');

const { hostname, port, root } = require('./config');

const fsPromises = fs.promises;

const server = http.createServer((req, res) => {

const url = req.url;

const filePath = path.join(root, url);

stat(filePath, req, res);

});

server.listen(port, hostname, () => {

console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);

});

async function stat(filePath, req, res) {

try {

const status = await fsPromises.stat(filePath);

if (status.isFile()) {

res.statusCode = 200;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

fs.createReadStream(filePath).pipe(res);

return;

}

if (status.isDirectory()) {

res.statusCode = 200;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

const files = await fsPromises.readdir(filePath);

res.end(files.join(', '));

return;

}

} catch (error) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

res.end(`${filePath} 不存在!`);

return;

}

}

进一步扩展功能,目录和文件可以进行点击操作,引入 Handlebars 模板引擎。

安装 handlebars

npm install handlebars

const http = require('http');

const path = require('path');

const Handlebars = require("handlebars");

const fs = require('fs');

const chalk = require('chalk');

const { hostname, port, root } = require('./config');

const fsPromises = fs.promises;

// __dirname test.js 所在的目录 -> C:\E\教程\node\node\src

const templatePath = path.join(__dirname, './template/tpl.html'); // 绝对路径

const source = fs.readFileSync(templatePath);

const template = Handlebars.compile(source.toString());

const server = http.createServer((req, res) => {

const url = req.url;

const filePath = path.join(root, url);

stat(filePath, req, res);

});

server.listen(port, hostname, () => {

console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);

});

async function stat(filePath, req, res) {

try {

const status = await fsPromises.stat(filePath);

if (status.isFile()) {

res.statusCode = 200;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

fs.createReadStream(filePath).pipe(res);

return;

}

if (status.isDirectory()) {

res.statusCode = 200;

res.setHeader('Content-Type', 'text/html;charset=utf-8');

const files = await fsPromises.readdir(filePath); // 所有文件和目录

// Solve the relative path from {from} to {to}. At times we have two absolute paths.

// 当访问 http://127.0.0.1:3000/src 时

// root -> C:\E\教程\node\node\

// filePath -> C:\E\教程\node\node\src

// path.relative(root, filePath) -> src

const dir = path.relative(root, filePath);

const data = {

title: filePath,

dir: dir ? `/${dir}` : "",

files,

}

res.end(template(data));

return;

}

} catch (error) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

res.end(`${filePath} 不存在!`);

return;

}

}

模板:

{{title}}

{{#each files}}

{{this}}

{{/each}}

项目图示

对上面的代码的各种文件请求类型,新增对应的 mimeType 类型

const http = require('http');

const path = require('path');

const Handlebars = require("handlebars");

const fs = require('fs');

const chalk = require('chalk');

const mime = require('./config/mime');

const { hostname, port, root } = require('./config');

const fsPromises = fs.promises;

// __dirname test.js 所在的目录 -> C:\E\教程\node\node\src

const templatePath = path.join(__dirname, './template/tpl.html'); // 绝对路径

const source = fs.readFileSync(templatePath);

const template = Handlebars.compile(source.toString());

const server = http.createServer((req, res) => {

const url = req.url;

const filePath = path.join(root, url);

stat(filePath, req, res);

});

server.listen(port, hostname, () => {

console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);

});

async function stat(filePath, req, res) {

try {

const status = await fsPromises.stat(filePath);

if (status.isFile()) {

const contentType = mime(filePath);

res.statusCode = 200;

res.setHeader('Content-Type', `${contentType};charset=utf-8`);

// res.setHeader('Content-Type', 'text/plain;charset=utf-8');

fs.createReadStream(filePath).pipe(res);

return;

}

if (status.isDirectory()) {

res.statusCode = 200;

res.setHeader('Content-Type', 'text/html;charset=utf-8');

const files = await fsPromises.readdir(filePath); // 所有文件和目录

// Solve the relative path from {from} to {to}. At times we have two absolute paths.

// 当访问 http://127.0.0.1:3000/src 时

// root -> C:\E\教程\node\node\

// filePath -> C:\E\教程\node\node\src

// path.relative(root, filePath) -> src

const dir = path.relative(root, filePath);

const data = {

title: filePath,

dir: dir ? `/${dir}` : "",

files,

}

res.end(template(data));

return;

}

} catch (error) {

res.statusCode = 404;

res.setHeader('Content-Type', 'text/plain;charset=utf-8');

res.end(`${filePath} 不存在!`);

return;

}

}

const path = require('path');

// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types

const mimeType = {

"aac": "audio/aac",

"abw": "application/x-abiword",

"arc": "application/x-freearc",

"avi": "video/x-msvideo",

"azw": "application/vnd.amazon.ebook",

"bin": "application/octet-stream",

"bmp": "image/bmp",

"bz": "application/x-bzip",

"bz2": "application/x-bzip2",

"csh": "application/x-csh",

"css": "text/css",

"csv": "text/csv",

"doc": "application/msword",

"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",

"eot": "application/vnd.ms-fontobject",

"epub": "application/epub+zip",

"gif": "image/gif",

"htm": "text/html",

"html": "text/html",

"ico": "image/vnd.microsoft.icon",

"ics": "text/calendar",

"jar": "application/java-archive",

"jpeg": "image/jpeg",

"jpg": "image/jpeg",

"js": "text/javascript",

"json": "application/json",

"jsonld": "application/ld+json",

"mid": "audio/midi audio/x-midi",

"midi": "audio/midi audio/x-midi",

"mjs": "text/javascript",

"mp3": "audio/mpeg",

"mpeg": "video/mpeg",

"mpkg": "application/vnd.apple.installer+xml",

"odp": "application/vnd.oasis.opendocument.presentation",

"ods": "application/vnd.oasis.opendocument.spreadsheet",

"odt": "application/vnd.oasis.opendocument.text",

"oga": "audio/ogg",

"ogv": "video/ogg",

"ogx": "application/ogg",

"otf": "font/otf",

"png": "image/png",

"pdf": "application/pdf",

"ppt": "application/vnd.ms-powerpoint",

"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",

"rar": "application/x-rar-compressed",

"rtf": "application/rtf",

"sh": "application/x-sh",

"svg": "image/svg+xml",

"swf": "application/x-shockwave-flash",

"tar": "application/x-tar",

"tif": "image/tiff",

"tiff": "image/tiff",

"ttf": "font/ttf",

"txt": "text/plain",

"vsd": "application/vnd.visio",

"wav": "audio/wav",

"weba": "audio/webm",

"webm": "video/webm",

"webp": "image/webp",

"woff": "font/woff",

"woff2": "font/woff2",

"xhtml": "application/xhtml+xml",

"xls": "application/vnd.ms-excel",

"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",

"xml": "application/xml",

"xul": "application/vnd.mozilla.xul+xml",

"zip": "application/zip",

"3gp": "video/3gpp",

"3g2": "video/3gpp2",

"7z": "application/x-7z-compressed",

}

module.exports = (filePath) => {

const ext = path.extname(filePath)

.split(".")

.pop()

.toLowerCase();

return mimeType[ext] || mimeType['txt'];

}

接着对文件进行压缩处理,利用 zlib 进行 gzip 或者 deflate 压缩:

主要代码:

const compress = require('./helper/compress');

if (stats.isFile()) {

const contentType = mime(filePath);

res.setHeader('Content-Type', `${contentType};charset=utf-8`);

res.statusCode = 200;

// fs.createReadStream(filePath).pipe(res);

// 创建可读流

let rs = fs.createReadStream(filePath);

if (filePath.match(config.compress)) {

rs = compress(rs, req, res);

}

rs.pipe(res);

return;

}

compress.js

const zlib = require("zlib");

module.exports = (rs, req, res) => {

// 获取浏览器支持的压缩格式

let encoding = req.headers["accept-encoding"];

// 支持 gzip 使用 gzip 压缩,支持 deflate 使用 deflate 压缩

let compress = "";

let compressType = "";

if (!encoding || !encoding.match(/\b(gzip|bdeflate)\b/)) {

return rs;

} else if (encoding && encoding.match(/\bgzip\b/)) {

compress = zlib.createGzip();

compressType = "gzip";

} else if (encoding && encoding.match(/\bdeflate\b/)) {

compress = zlib.createDeflate();

compressType = "deflate";

}

// 将压缩流返回并设置响应头

res.setHeader("Content-Encoding", compressType);

return rs.pipe(compress);

}

配置文件新增 compress 对特定类型进行压缩

module.exports = {

root: process.cwd(), // 查看应用程序当前目录

hostname: '127.0.0.1',

port: 3000,

compress: /\.(html|js|css)/ // 对特定类型进行压缩

}

接下来对文件进行缓存处理,首先要知道缓存相关的 http 请求字段为:

Expires、Cache-Control

If-Modified-Since / Last-Modified

If-None-Match / ETag

主要代码:

const isFresh = require('./helper/cache');

if (stats.isFile()) {

const contentType = mime(filePath);

res.setHeader('Content-Type', `${contentType};charset=utf-8`);

// 是否可以使用缓存

if (isFresh(stats, req, res)) {

res.statusCode = 304;

res.end();

return;

}

res.statusCode = 200;

// fs.createReadStream(filePath).pipe(res);

// 创建可读流

let rs = fs.createReadStream(filePath);

if (filePath.match(config.compress)) {

rs = compress(rs, req, res);

}

rs.pipe(res);

return;

}

cache.js

const { cache } = require('../config');

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;

}

if (etag && etag !== res.getHeader('ETag')) {

return false;

}

return true;

}

module.exports = {

root: process.cwd(), // 查看应用程序当前目录

hostname: '127.0.0.1',

port: 3000,

compress: /\.(html|js|css)/, // 对特定类型进行压缩

cache: {

maxAge: 10 * 60, // 秒

expires: true,

cacheControl: true,

lastModified: true,

etag: true,

}

}

yargs 是 nodejs 环境下的命令行参数解析工具。

参数

-p 或者 -p 8080 或者 --port=8080 或者 -p=8080

可以通过 process.anyv 读取命令行的参数列表

扩展:npm 版本号的理解:

版本号大概可以理解为 x.y.z,x 表示大版本,可以不兼容上一版本,y 表示有新增的功能,必须兼容同一版本,z 表示 fix 一些 bug 等,常见有如下几种格式:

1.2.* :表示 1.2 这个版本,z 是最新的版本号;

~1.2.0 :跟 1.2.* 同样意思;

2.x :表示 y、z 使用最新的版本号

^2.0.0 :跟 2.x 同样意思;


版权声明:本文为weixin_39612110原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。