nodejs 原生服务起一个httpServer

离开express、koa、egg
你还会利用原生node写后端的http服务吗?

定义路由和返回

这里有一个例子,原生node起http服务。
返回了静态页面文件、字符串拼接的html,json对象和优化404。
做个备忘吧!

import { createServer } from "http";
import path from 'path';
import { __dirname } from './utils/index.js'

const httpServer = createServer((req, res) => { // 创建一个http服务
    const { url } = req;
    if (url === '/') { // 返回现有的静态页面
        const file = path.join(__dirname, '../views/index.html');
        const data = readFileSync(file);
        res.end(data);
    } else if (url === '/test') { // 返回手写的html
        res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
        res.write('<h1>你好,这是你人生中创建的第一个服务器</h1>');
        res.end('<h1>响应结束!!!</h1>');  // 响应结束
    } else if (url === '/json') { // 返回json
        res.writeHead(200, 'OK', { 'Content-type': 'application/json' });
        res.end(JSON.stringify({
            msg: '你好啊'
        }));
    } else { // 自定义404
        es.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
        res.end('<h1>你来到了一片荒无人烟之处!</h1>');
    }
});
httpServer.listen(3000, () => { console.log('服务已经启动: http://127.0.0.1:3000'); }); // 启动http服务

因为__dirname 是commonjs规范内置的变量,当package.json的"type":"module"时,便无法找到。
不过有补救的办法

import { fileURLToPath } from 'url'
import path from 'path';
import os from 'os'

const __filenameNew = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filenameNew);

ajax-get

前端页面

const res = await fetch('/get_test?name=张三&age=18').then(res => res.json());
console.log(res);

后端服务

const parsedUrl = url.parse(req.url, true);
const { query, pathname } = parsedUrl;
if (pathname === '/get_test') {
    console.log(query); // { name: '张三', age: '18' }
    res.end("sucess")
}

ajax-post

前端

const res = await fetch('/post_test', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: params,
}).then(res => res.json());
console.log(res);

思考:为什么像后端传json,需要提前序列化一下?
HTTP POST的主体内容只能是字符串或二进制数据。序列化的过程就是将JSON对象转换为字符串的过程。
之所以要在headers放入'content-type': 'application/json' 仅仅是因为让后端知道我们传的字符串是json,方便他们去反序列化。
因为axios底层帮你做了,而fetch自己写,其实也好,方便明白其中原理。

后端内容

let requstBody = '';  // 接收请求体参数(
req.on('data', function (chunk) { requstBody += chunk; });
req.on('end',()=>{
    const requstBodyJson = JSON.parse(requstBody);
    console.log(requstBodyJson); // { name: '张三', age: 18 }
    console.log(requstBodyJson.name); // 张三
})

post请求的参数一般在请求体中。node中post请求体为流的形式存在,需要以事件形式进行接收。
通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中。
不知道为何这样设计,问了gpt,他回答说是为了性能和兼容文件接收以及灵活等等。

form--get

同ajax-get一样,浏览器会自动将参数拼接到api地址后。

form-post

同ajax一样 form表单也支持post和get但是尚delete等等http方法(推测可能是form表单发明的时候 http其他方法还没诞生,等诞生了 大家又都跑去用ajax了)。

<form action="/form_post" method="post">
    <input type="text" name="name" value="李四">
    <input type="text" name="age" value="18">
    <input type="submit" value="提交">
</form>

后端直接接收即可

if (url === '/form_post') {
    let requestBody = '';
    req.on("data", (chunk) => { requestBody += chunk; })
    req.on("end", () => {
        console.log(requestBody); // name赵四&age=18
        console.log(querystring.parse(requestBody)); // { name: '李四', age: '18' }
        res.end("sucess")
    })
}

伪form表单-get

前端form表单有两种形式:真表单和用js的new FormData()

const formData = new FormData();
formData.append('name', '张三');
formData.append('age', 20);

// 开始
const res = await fetch('/ajax_form_get', { method: 'GET', body: formData }).then(res => res.json());
console.log(res);

后端处理模式同真form表单的get一样。

伪form表单-post

前端

// 拼接参数
const formData = new FormData();
formData.append('name', '张三');
formData.append('age', 20);

// 开始
const res = await fetch('/ajax_form_post',{method: 'POST', body: formData}).then(res=>res.json());
console.log(res);

后端

let requstBody = '';  // 接收请求体参数(
req.on('data', function (chunk) { requstBody += chunk; });
req.on('end', () => {
    console.log(requstBody);
})

会打印

------WebKitFormBoundaryOSkYoFTNfeUru4Rg
Content-Disposition: form-data; name="name"

张三
------WebKitFormBoundaryOSkYoFTNfeUru4Rg
Content-Disposition: form-data; name="age"

20
------WebKitFormBoundaryOSkYoFTNfeUru4Rg--

你是不是觉得 数据被node服务加工过了?
不是的,这就是前端传过来的原始数据,只是被浏览器加工过了,在浏览器中就可以看到猫腻

在http协议中使用form提交文件时需要将form标签的method属性设置为post,enctype属性设置为multipart/form-data,并且有至少一个input的type属性为file时,浏览器提交这个form时会在请求头部的Content-Type中自动添加boundary属性。
不知道这个ajax模拟的form-post为何会触发,而且即便我手动将Content-Type设置为application/x-www-form-urlencoded也没用!

这些都是什么呢?
------WebKitFormBoundaryOSkYoFTNfeUru4Rg这个叫做是分隔符,分隔多个文件、表单项。
通常由浏览器自动产生(随机码,为了避免与正文内容重复,会很长很复杂),在请求头Content-Type中能看到boundary。

那如何获取或将其转换成通用模式的数据呢? 这一大段文本也太难操作了吧。

function parseContentDisposition(str) {
    // 将字符串按分隔符分割成数组
    const parts = str.split(';');
    const contentDisposition = {
        type: parts.shift().trim(), // 获取类型
        parameters: {}
    };
    // 解析参数
    parts.forEach(part => {
        const [key, value] = part.split('=');
        const trimmedKey = key.trim();
        const trimmedValue = value.trim().replace(/^"([\s\S]*)"$/, '$1'); // 移除双引号
        contentDisposition.parameters[trimmedKey] = trimmedValue;
    });
    return contentDisposition;
}


// 格式化请求体数据
const fmtBoundary = (req, boundary) => {
    const res = {}
    // 拿到请求头中的分隔符
    const separator = `--${req.headers['content-type'].split('boundary=')[1]}`;
    // 移除尾部分隔符结束符
    boundary = boundary.replace(`${separator}--`, '');
    // 根据分隔符,将请求体分割成数组
    const allContents = boundary.split(separator);
    // 遍历请求体数组 做进一步数据处理
    allContents.forEach(element => {
        if (element) {
            const [othersStr, content] = element.split('\r\n\r\n'); // 根据4个空行,切割出其它信息和核心数据;
            const othersObj = {};
            othersStr.split('\r\n').forEach(item => {
                if (item) {
                    const regex = /^([^:]+):(.*)$/;
                    const [_, key, val] = item.match(regex);
                    if (key === 'Content-Disposition') {
                        othersObj[key] = parseContentDisposition(val);
                        res[othersObj[key].parameters.name] = content.replace('\r\n', '');
                        res['_' + othersObj[key].parameters.name] = othersObj;
                    } else {
                        othersObj[key] = val
                    }
                }
            })
        }
    });
    return res
}

const httpServer = createServer((req, res) => {
    const { url } = req;
    if (url === '/form_post') {
        let requstBody = '';  // 接收请求体参数(
        req.on('data', function (chunk) { requstBody += chunk; });
        req.on('end', () => {
            const formData = fmtBoundary(req, requstBody);
            console.log(formData);
        })
    }
}); // 创建一个http服务
httpServer.listen(3000, () => console.log('服务已经启动: http://127.0.0.1:3000')); // 启动http服务

再次尝试,点击提交表单,后端会打印

可有看到自己写,是可行的 就是太复杂!

伪form表单-post优化

如上所说,自己处理表单post请求,会遭遇请求体分隔符杂乱数据的处理虐心过程。
有没有三方库?---有!

const formData = formidable();
if (url === '/form_post') {
    formData.parse(req,(err, fields, files)=>{
      console.log(fields); // 打印 { name: [ '张三' ], age: [ '20' ] }  
    })
}
posted @ 2024-05-15 17:24  丁少华  阅读(45)  评论(0编辑  收藏  举报