回顾 net 模块
net 模块需要:
- 手动管理 socket
- 手动组装消息格式
http 模块建立在 net 模块之上的,无需手动做以上操作;
创建客户端
const http = require("http");
const request = http.request(
"http://yuanjin.tech:5005/api/movie",
{ method: "GET" },
resp => {
console.log("服务器响应的状态码", resp.statusCode);
console.log("服务器响应头", resp.headers);
let result = "";
resp.on("data", chunk => {
result += chunk.toString("utf-8");
});
resp.on("end", chunk => {
console.log(JSON.parse(result));
});
}
);
request.end(); // 表示消息体结束,POST 请求需要向请求体写入内容(write、end)
创建服务端
const http = require("http");
const url = require("url");
function handleReq(req) {
console.log("有请求来了!");
const urlobj = url.parse(req.url);
console.log("请求路径", urlobj);
console.log("请求方法", req.method);
console.log("请求头", req.headers);
let body = "";
req.on("data", chunk => {
body += chunk.toString("utf-8");
});
req.on("end", () => {
console.log("请求体", body);
});
}
const server = http.createServer((req, res) => {
handleReq(req);
// 设置响应头
res.setHeader("a", "1");
res.setHeader("b", "2");
res.statusCode = 404;
// 设置响应体
res.write("你好!");
res.end();
});
server.listen(9527);
server.on("listening", () => {
console.log("server listen 9527");
});
http 模块的缺点
根据不同的请求路径、请求方法,做不同的事情,处理起来比较麻烦;
读取请求体和写入响应体是通过流的方式,比较麻烦;
案例
客户端代理解决跨域
服务器之间是不存在跨域的,可以使用 NodeJS 创建一个客户端代理,由它代替浏览器客户端直接向服务端发送请求;
浏览器客户端可以将发送给服务端的请求发送给客户端代理,由客户端代理转为发送,解决跨域问题;
-
服务端:这里用 node 模拟方便演示
const http = require('http') const server = http.createServer((req, res) => { const arr = [] req.on('data', chunk => arr.push(chunk)) req.on('end', () => { console.log(Buffer.concat(arr).toString()) res.end('获取到了客户端的数据') }) }) server.listen(1234, () => console.log('外部服务端启动了'))
-
客户端代理:
const http = require('http') const options = { host: 'localhost', port: 1234, path: '/', method: 'POST' } const server = http.createServer((request, response) => { // 向服务端发送请求,并将响应的数据返回给客户端 const req = http.request(options, res => { const arr = [] res.on('data', chunk => arr.push(chunk)) res.on('end', () => { const ret = Buffer.concat(arr).toString() response.setHeader('content-type', 'text/html;charset=utf-8') response.end(ret) // 将数据发送给客户端 }) }) req.end('你好张三') // 将数据发送给服务端 }) server.listen(1000, () => console.log('本地服务端启动了'))
-
客户端:
图片防盗链
-
服务端
const http = require('http'); const fs = require('fs'); const url = require('url'); const path = require('path'); const mime = require('mime'); const server = http.createServer((req, res) => { const { pathname } = url.parse(req.url, true); const absPath = path.join(__dirname, pathname); fs.stat(absPath, (err, statObj) => { if (err) return res.end('Not Found') if (statObj.isFile()) { // 对 jpg 图片进行防盗链处理 if (/\.jpg/.test(absPath)) { // referer 来源 iframe img,表示这个资源被谁引用过 let referer = req.headers['referer'] || req.headers['referrer']; if (referer) { let host = req.headers.host; referer = url.parse(referer).host; console.log(host); // localhost:3000 console.log(referer); // 127.0.0.1:5500 if (host !== referer) { // 判断客户端请求的 host 和图片 referer 是否一样 // 不一样返回 404 错误图片 fs.createReadStream(path.resolve(__dirname, '404.jpg')).pipe(res); return; } } } res.setHeader('Content-Type', mime.getType(absPath)) fs.createReadStream(absPath).pipe(res); } else { return res.end('Not Found') } }) }).listen(3000);
-
客户端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <!-- 如果网站没有 referer,会导致发送任何资源都不会带 referer --> <!-- <meta name="referrer" content="never"> --> </head> <body> <!-- 如果图片直接打开是不会增加 referer 的 --> <!-- 应该进行校验 如果引用我的人 和我的域不是同一个 应该返回错误图片 --> <img src="http://localhost:3000/1.jpg" /> </body> </html>
静态资源服务器
-
config.js
const config = { // 设置端口号的配置 port: { option: '-p,--port <val>', description: 'set your server port', usage: 'zhu-http-server --port 3000', // 案例 default: 3000 }, // 可以配置目录 directory: { option: '-d,--directory <val>', description: 'set your start directory', usage: 'zhu-http-server --directory D:', default: process.cwd(), }, // 可以配置主机名 host: { option: '-h,--host <val>', description: "set your hostname", usage: 'zhu-http-server --host 127.0.0.1', default: 'localhost' } } module.exports = config;
-
www .js
#! /usr/bin/env node const program = require('commander'); // 命令行工具模块 const { version } = require('../package.json'); const config = require('./config'); const Server = require('../src/server'); program.usage('[args]') program.version(version); Object.values(config).forEach(val => { if (val.option) { program.option(val.option, val.description); } }); program.on('--help', () => { console.log('\r\nExamples:'); Object.values(config).forEach(val => { if (val.usage) { console.log(' ' + val.usage) } }); }) // 解析用户的参数 let parserObj = program.parse(process.argv); let keys = Object.keys(config); // 最终用户拿到的数据 let resultConfig = {} keys.forEach(key=>{ resultConfig[key] = parserObj[key] || config[key].default }); let server = new Server(resultConfig); server.start(); // 开启一个 server
-
server.js
// ------------- core ------------------ const http = require('http'); const fs = require('fs').promises; const { createReadStream, createWriteStream, readFileSync } = require('fs'); const url = require('url'); const path = require('path'); // babel-node (webpack) const crypto = require('crypto'); // ------------------------------------ const ejs = require('ejs'); // 服务端读取目录进行渲染,模板工具模块 const debug = require('debug')('server'); const mime = require('mime'); // 识别文件的类型,自动设置 content-type const chalk = require('chalk'); // 提供颜色 const template = readFileSync(path.resolve(__dirname, 'template.ejs'), 'utf8'); // 根据环境变量来进行打印 process.env.DEBUG class Server { constructor(config) { this.port = config.port; this.directory = config.directory; this.host = config.host; this.template = template; // this.handleRequest = this.handleRequest.bind(this); // 绑定死 this } async handleRequest(req, res) { // 指的就是当前自己 Server 的实例 let { pathname } = url.parse(req.url); // 不考虑传递参数问题 pathname = decodeURIComponent(pathname); // 将中文进行一次转义 // 通过路径找到这个文件返回 let filePath = path.join(this.directory, pathname); try { let statObj = await fs.stat(filePath); if (statObj.isFile()) { this.sendFile(req, res, filePath, statObj) } else { // 文件夹先尝试找 index.html let concatfilePath = path.join(filePath, 'index.html'); // 如果存在 html try { let statObj = await fs.stat(concatfilePath); this.sendFile(req, res, concatfilePath, statObj); } catch (e) { // 列出目录结构 this.showList(req, res, filePath, statObj, pathname); } } } catch (e) { this.sendError(req, res, e); } } async showList(req, res, filePath, statObj, pathname) { // 读取目录包含的信息 let dirs = await fs.readdir(filePath); // 渲染列表 try { // 异步渲染 let parseObj = dirs.map(item => ({ dir: item, href: path.join(pathname, item) // 拼接当前 url 的路径 })); let r = await ejs.render(this.template, { dirs: parseObj }, { async: true }); res.setHeader('Content-Type', 'text/html;charset=utf-8'); res.end(r); } catch (e) { this.sendError(req, res); } } gzip(req, res, filePath, statObj) { if (req.headers['accept-encoding'] && req.headers['accept-encoding'].includes('gzip')) { res.setHeader('Content-Encoding', 'gzip') return require('zlib').createGzip(); // 创建转化流 } else { return false; } } async cache(req, res, filePath, statObj) { // 设置「强制缓存」,新老版本浏览器全部支持,缓存 10s res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()); res.setHeader('Cache-Control', 'max-age=10'); // 设置「协商缓存-资源指纹对比」,准确,资源很大会消耗一定时间读取文件生成 etag let fileContent = await fs.readFile(filePath); let ifNoneMatch = req.headers['if-none-match']; let etag = crypto.createHash('md5').update(fileContent).digest('base64'); res.setHeader('Etag', etag); // 设置「协商缓存-缓存时间对比」,不够准确 let ifModifiedScince = req.headers['if-modified-since']; let ctime = statObj.ctime.toGMTString() res.setHeader('Last-Modified', ctime); // 资源内容修改了,指纹匹配失效,需要重新发起请求 if (ifNoneMatch !== etag) return false; // 资源内容修改了,客户端与服务端的文件修改时间不一致,需要重新发起请求 if (ctime !== ifModifiedScince) return false; return true; } async sendFile(req, res, filePath, statObj) { // 缓存 try { let cache = await this.cache(req, res, filePath, statObj); if (cache) { // 有缓存直接让用户查找缓存即可 res.statusCode = 304; return res.end() } } catch (e) { console.log(e) } // 这里需要掌握 header,来看一下浏览器是否支持 gzip 压缩 // 客户端和服务端定义的一个相互能识别的规则 let gzip = this.gzip(req, res, filePath, statObj); if (gzip) { res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8'); createReadStream(filePath).pipe(gzip).pipe(res); } else { createReadStream(filePath).pipe(res); } } // 专门用来处理错误信息 sendError(req, res, e) { debug(e); res.statusCode = 404; res.end('Not Found'); } start() { // 等价 const server = http.createServer((req,res)=>this.handleRequest(req,res)); const server = http.createServer(this.handleRequest.bind(this)); server.listen(this.port, this.host, () => { console.log(chalk.yellow(`Starting up http-server, serving ./${this.directory.split('\\').pop()}\r\n`)); console.log(chalk.green(`http://${this.host}:${this.port}`)); }) } } module.exports = Server; // gzip 压缩(前端可以通过 webpack 插件进行压缩),如果前端压缩了后端直接返回即可; // 若前端没有进行压缩,后端在返回的时候进行 .gz 压缩
-
template.ejs
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <% dirs.forEach(item=>{ %> <li><a href="<%=item.href%>"> <%=item.dir%> </a></li> <%})%> </body> </html>
-
package.json
{ "name": "ws-http-server", "version": "1.0.1", "description": "", "main": "index.js", "keywords": [ "http-server" ], "author": "", "license": "MIT", "bin": { "ws-http-server": "./bin/www.js", "whs": "./bin/www.js" }, "dependencies": { "chalk": "^4.1.0", "commander": "^5.1.0", "debug": "^4.1.1", "ejs": "^3.1.3", "mime": "^2.4.6" } }
-
在工作区间根目录下执行
npm link
,将 package.json bin 下的 ws-http-server、whs 命令映射到全局; -
如果不指定服务的端口号默认为 3000 ,服务提供 浏览项目目录下 的所有文件;
-
效果展示:
node👉 net 模块
上一篇