Node.js与前端开发实战 | 青训营笔记

81 阅读3分钟

这是我参与「第五届青训营 」笔记创作活动的第7天

一、本堂课重点内容:

Node.js的应用场景
Node.js运行时结构
编写Http Server

二、详细知识点介绍:

Node.js的应用场景

前端工程化

早期使用的外部的库是从浏览器上直接加载,作为开发者不需要服务端的能力,不需要做一些工程化的事情,随着前端工程化的发展,有了Node.js的出现,前端开发者可以利用这门语言来实现这些工具。

1.Bundle: webpack, vite, esbuild, parcel
2.Uglify: uglifyjs
3.Transpile: bablejs, typescript
4.其他语言加入竞争:esbuild, parcel, prisma
5.现状:难以替代

Web服务端应用

使用Node.js开发服务端应用

1.学习曲线平缓,开发效率较高
2.运行效率接近常见的编译语言
3.社区生态丰富及工具链成熟(npm, V8 inspector)
4.与前端结合的场景会有优势(SSR)
5.现状:竞争激烈,Node.js 有自己独特的优势

Electron跨端桌面应用

1.商业应用:vscode,slack,discord,zoom
2.大型公司内的效率工具
3.现状:大部分场景在选型时,都值得考虑

Node.js运行时结构

image-20230131135420886.png

特点

1.异步I/O

setTimeout(() => {
    console.log('B')
})
console.log('A')

例如读取文件,当 Node.js 执行 I/O 操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待

image-20230131154246441.png

2.单线程

function fibonacci(num:number):number {
	if(num === 1 || num === 2) {
        return 1;
    }
    return fibonacci(num-1) + fibonacci(num-2);
}
fibonacci(42)
fibonacci(43)

JS 单线程
实际:JS 线程 + uv 线程池 + V8 任务线程池 + V8 Inspector 线程
优点:不用考虑多线程状态同步问题,也就不需要锁;同时还能比较高效地利用系统资源
缺点:阻塞会产生更多负面影响
解决办法:多进程或多线程

3.跨平台

const net = require('net')
const socket = new net.Socket('/tmp/socket.sock')

1.大部分功能、api 跨平台
2.Node.js 跨平台 + JS 无需编译环境(+Web 跨平台 + 诊断工具跨平台)
= 开发成本低(大部分场景无需担心跨平台问题),整体学习成本低

编写Http Server

安装Node.js

Mac、Linux推荐使用nvm,进行多版本管理
Windows推荐nvm4w或者官方安装包

编写Http Server + Client,收发GET,POST请求

Http Server
const http = require('http');
const server = http.createServer((req, res) => {
    res.end('hello');
});

const port = 3000;

server.listen(port, () => {
    console.log(`server listens on:${port}`);//监听3000端口
})

image-20230131161738950.png

json

const http = require('http');

const server = http.createServer((req, res) => {
    const bufs = [];
    req.on('data', buf => {
        bufs.push(buf);
    });
    req.on('end', () => {
        const buf = Buffer.concat(bufs).toString('utf8');
        let msg='Hello'
        try {
            const ret = JSON.parse(buf);
            msg = ret.msg;
        } catch (err) {  
            //
        }
        const responseJson={
            msg:`receive:${msg}`
        }
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(responseJson));
    });
});

const port = 3000;

server.listen(port, () => {
    console.log(`server listens on:${port}`);//监听3000端口
})

image-20230131164041236.png

Http Client
const http = require('http');
const body = JSON.stringify({
     msg: 'Hello from my own client',
});
const req = http.request('http://127.0.0.1:3000', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
}, (res) => {   
    const bufs = [];
    res.on('data', buf => {
        bufs.push(buf);
    });
    res.on('end', () => {
        const buf = Buffer.concat(bufs);
        const json = JSON.parse(buf);
        console.log('json.msg is:', json.msg);
    });
})
req.end(body);

image-20230131171831454.png

Promisify

用Promise+async await重写以上的例子,因为用了很多的回调函数,不利于维护
技巧:将callback转换成promise,但是只适合只被调用一次的回调函数

function wait(t) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, t);
    });
}
wait(1000).then(() => { console.log('get called');});

重写上面的例子:

const http = require('http');
const server = http.createServer(async (req, res) => { 
    const msg = await new Promise((resolve, reject) => {
        const bufs = [];    
        req.on('data', buf => {
            bufs.push(buf);
        });
        req.on('error', (err) => {
            reject(err);
        })
        req.on('end', () => {
            const buf = Buffer.concat(bufs).toString('utf8');
            let msg = 'hello';
            try {
                const ret = JSON.parse(buf);
                msg = ret.msg;
            } catch (err) {
                // 
            }
            resolve(msg);
        });
    });
    const responseJson = {
        msg: `receive:${msg}`
    }
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify(responseJson));
});

const port = 3000;

server.listen(port, () => {
    console.log(`server listens on:${port}`);//监听3000端口
})

编写静态文件服务器

编写一个服务,服务接收用户发来的HTTP请求,接收处理http请求参数,约定请求的url是期望拿到的文件在静态文件服务器上对应的磁盘上的文件路径,根据文件路径把文件读取出来,再把文件内容返回给该请求

static/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Application</title>
</head>

<body>
    <h1>Hello</h1>
    <script>
        alert('yes')
    </script>
</body>

</html>

static_server.js

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

const folderPath = path.resolve(__dirname, './static');//index.html在static文件夹

const server = http.createServer((req, res) => { 
    // expected http://127.0.0.1:3000/index.html?
    const info = url.parse(req.url);//解析

    // static/index.html
    const filepath = path.resolve(folderPath, './'+info.path);
    console.log('filepath', filepath);

    //stream api
    const filestream = fs.createReadStream(filepath);
    filestream.pipe(res);
});
const port = 3000;
server.listen(port, () => {
    console.log(`listening on:`,port);
})

image-20230131184902696.png

这个例子与高性能、可靠的服务相比,还差什么?
1.CDN:缓存+加速
2.分布式储存,容灾

编写React SSR服务

SSR的特点
1.相比传统HTML模板引擎,可以避免重复编写代码
2.相比SPA(single page applicaton),首屏渲染更快,SEO(搜索引擎优化)友好
3.缺点:通常qps(每秒查询率)较低,前端代码编写时需要考虑服务端渲染情况

安装react相关的包
npm install -g create-react-app
npm init
npm i react react-dom

替换成React:

const http = require('http');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
//return <div>Hello</div>
function App(props) {
    return React.createElement('div', {}, props.children || 'Hello');
}
const server = http.createServer((req, res) => {
    res.end(`
        <!DOCTYPE html>
        <html>
            <head>
                <title>My Application</title>
            </head>
            <body>
                ${ReactDOMServer.renderToString(
                    React.createElement(App, {}, 'my_content'))}
                <script>
                    alert('yes');
                </script>
            </body>
        </html>
    `);
})
const port = 3000;
server.listen(port, () => {
    console.log('listening on: ', port);
})

image-20230131211551184.png

SSR难点
1.需要处理打包代码
2.需要思考前端代码在服务端运行时的逻辑
3.移除对服务端无意义的副作用,或重置环境

Inspector进行调试、诊断

V8 Inspector优点:开箱即用、特性丰富强大、与前端开发一致、跨平台
如何打开
1.node --inspect
例如node --inspect ssr_example.js
2.打开http://localhost:9229/json

场景:
1.查看console.log内容
2.breakpoint
3.高CPU、死循环: cpuprofile
4.高内存占用:heapsnapshot
5.性能分析

部署简介

部署要解决的问题: 1.守护进程:当进程退出时,重新拉起
2.多进程:cluster 便捷地利用多进程
3.记录进程状态,用于诊断

三、引用参考:

欧阳亚东老师的Node.js与前端开发实战课