回顾 net 模块

  1. net 模块需要:

    • 手动管理 socket
    • 手动组装消息格式
  2. 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 模块的缺点

  1. 根据不同的请求路径、请求方法,做不同的事情,处理起来比较麻烦;

  2. 读取请求体和写入响应体是通过流的方式,比较麻烦;

案例

客户端代理解决跨域

  1. 服务器之间是不存在跨域的,可以使用 NodeJS 创建一个客户端代理,由它代替浏览器客户端直接向服务端发送请求;

  2. 浏览器客户端可以将发送给服务端的请求发送给客户端代理,由客户端代理转为发送,解决跨域问题;

  1. 服务端:这里用 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('外部服务端启动了'))
    
  2. 客户端代理:

    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('本地服务端启动了'))
    
  3. 客户端:

图片防盗链

  1. 服务端

    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);
    
  2. 客户端

    <!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>
    

静态资源服务器

  1. 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;
    
  2. 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
    
  3. 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 压缩 
    
  4. 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>
    
  5. 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"
      }
    }
    
  6. 在工作区间根目录下执行 npm link,将 package.json bin 下的 ws-http-serverwhs 命令映射到全局;

  7. 如果不指定服务的端口号默认为 3000 ,服务提供 浏览项目目录下 的所有文件;

  8. 效果展示:

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 回顾 net 模块
  2. 2. 创建客户端
  3. 3. 创建服务端
  4. 4. http 模块的缺点
  5. 5. 案例
    1. 5.1. 客户端代理解决跨域
    2. 5.2. 图片防盗链
    3. 5.3. 静态资源服务器