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('<http>')
res.write('<body>')
res.write('<h1>')
res.write('测试')
res.write('</h1>')
res.write('</body>')
res.write('</http>')
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;
}
}
模板:
<!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>
</head>
<body>
<!-- files 为 传递进来对象的数组 -->
{{#each files}}
<a href="{{../dir}}/{{this}}">{{this}}</a>
{{/each}}
</body>
</html>
项目图示
对上面的代码的各种文件请求类型,新增对应的 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)/ // 对特定类型进行压缩
}
可以参考:https://blog.csdn.net/github_38140984/article/details/83011150
接下来对文件进行缓存处理,首先要知道缓存相关的 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 读取命令行的参数列表
参考:https://www.jianshu.com/p/7851001dd93b
扩展: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 同样意思;
网友评论