案例-开发 Web Server 博客
L134 - Node.js 从零开发 Web Server 博客项目 前端晋升全栈工程师必备open in new window
流程图
目标
开发一个博客系统,具有博客的基本功能。
需求
需求指导开发。
技术方案
- 数据如何存储。
- 如何与前端对接,接口设计。
存储用户数据
接口设计
开发接口
暂时不连接数据库,先模拟数据,写出接口
初始化路由:根据之前技术方案的设计,做出路由
返回假数据:将路由和数据处理分离,以符合设计原则
处理路由
API 和路由
API
- 前端和后端,不同端(子系统)对接的一个术语
- 有 url(路由),有输入,有输出
路由
- 是 API 的一部分
- 后端系统内部的一个定义
登录
- 登录校验&登录信息存储
- Cookie 和 Session
- Session 写入 Redis
- 开发登录功能,和前端联调
Cookie
客户端操作 Cookie
Server 端操作 Cookie: Set-Cookie
Server 端查看 Cookie:
req.headers.cookie
Server 端修改 Cookie:
res.setHeader('Set-Cookie', 'key=val; path=/; httpOnly')
限制客户端修改 Cookie:
httpOnly
Redis
- web server 中常用的缓存数据库,数据存放在内存中
- 相比 mysql,访问速度快
- 成本更高,可存储的数据量更小
- 解决多进程内存不共享问题,所有进程都访问一个 redis 服务
适合存放session
,因为访问频繁,对性能要求极高。可以不考虑断点丢失数据的问题,登录信息丢失之后可以再去登录。数据量不会太大。
Nodejs 链接 Redis
安装
redis
使用
const redisClient = redis.createClient(6379, '127.0.0.1');
redisClient.on('error', (err) => {
console.error(err);
});
// 测试
redisClient.set('name', 'licong', redis.print);
redisClient.get('name', (err, val) => {
if (err) {
console.error(err);
return;
}
console.log(val);
// 退出
redisClient.quit();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开发登录功能,和前端联调
用到 nginx 反向代理
- 登录功能依赖 cookie,必须用浏览器来联调
- cookie 跨域不共享,前端和 server 端必须同域
- 需要用到 nignx 做代理
Nginx
修改配置:
配置文件路径: nginx-1.20.1 > conf > nginx.conf
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://localhost:8084;
}
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
处理 http 请求
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
server.listen(8000);
2
3
4
5
6
7
GET
请求
处理const server = http.createServer((req, res) => {
const url = req.url;
req.query = querystring.parse(url.split('?')[1]);
res.end(JSON.stringify(req.query));
});
2
3
4
5
POST
请求
处理const server = http.createServer((req, res) => {
if (req.method === 'POST') {
// 设置返回格式
res.setHeader('Content-Type', 'application/json');
let postData = '';
req.on('data', (chunk) => {
postData += chunk.toString();
});
req.on('end', () => {
res.end(postData);
});
}
});
2
3
4
5
6
7
8
9
10
11
12
13
搭建开发环境
- 使用
nodemon
监测文件变化,自动重启 node - 使用
cross-env
设置环境变量,兼容 mac、linux、windows
连接数据库
安装mysql
:
yarn add mysql
- 账户: root
- 密码: 123456
操作数据库
- 建库
- 建表
- 表操作
users 表:
column | datatype | PK | NN | UQ | B | UN | ZF | AI | Default |
---|---|---|---|---|---|---|---|---|---|
id | int | Y | Y | ||||||
username | varchar(20) | Y | |||||||
password | varchar(20) | Y | |||||||
realname | varchar(10) | Y |
blogs 表:
column | datatype | PK | NN | UQ | B | UN | ZF | AI | Default |
---|---|---|---|---|---|---|---|---|---|
id | int | Y | Y | ||||||
title | varchar(50) | Y | |||||||
content | longtext | Y | |||||||
createtime | bigint(20) | Y | |||||||
author | varchar(20) | Y |
区分环境配置
process.env.NODE_ENV
- NODE_ENV=dev
- NODE_ENV=production
PM2
官网:https://pm2.keymetrics.io/docs/usage/quick-start/open in new window
- 进程守护,系统崩溃自动重启
- 启动多进程,充分利用 CPU 和内存
- 自带日志记录功能
常用命令
- 运行
pm2 start app.js
- 查看
pm2 list
- 重启
pm2 restart [0]
- 停止
pm2 stop [0]
- 信息
pm2 info [0]
- 日志
pm2 log
配置文件
- 设置进程数量
- 修改日志文件地址
日志
- 访问日志
- 自定义日志
- nodejs 文件操作 nodejs stream
- 日志功能开发和使用
- 日志文件拆分,日志内容分析
nodejs 文件操作
写入文件:
const fs = require('fs');
const path = require('path');
const fileName = path.resolve(__dirname, 'test.txt');
const content = '\n这是新写入的内容';
const opt = {
flag: 'a', // a: 追加写入,w: 覆盖写入
};
fs.writeFile(fileName, content, opt, (err) => {
if (err) {
console.log(err);
}
});
2
3
4
5
6
7
8
9
10
11
12
读取文件:
const fs = require('fs');
const path = require('path');
const fileName = path.resolve(__dirname, 'test.txt');
fs.readFile(fileName, (err, data) => {
if (err) {
console.log(err);
return;
}
// data 是二进制类型,需要转为字符串
console.log(data.toString());
});
2
3
4
5
6
7
8
9
10
11
I/O 操作的性能瓶颈
I/O 是 input/output 的意思,就是输入输出操作。
- I/O 包括
网络I/O
和文件I/O
- 相比于 CPU 计算和内存读写,比较慢
- 要在有限的硬件资源下,提高 I/O 的操作效率
- 使用 stream
stream
管道
解决网络和文件的读写效率问题
- 拷贝文件
- 网络返回
写日志
const fullFileName = path.join(__dirname, '../', '../', 'logs', fileName);
const writeStream = fs.createWriteStream(fullFileName, {
flags: 'a',
});
writeStream.write(log + '\n');
2
3
4
5
拆分日志
- 按时间拆分
- 实现方式:
linux
的crontab
命令,即定时任务
安全
- sql 注入
- xss 攻击
- 用户密码加密(md5)
express 框架
- 脚手架工具
- 中间件机制
- 开发接口,连接数据库,实现登录,日志记录
- 中间件原理
中间件
每一个在app.use
中注册的函数都是中间件。
- next()
app.use
如果第一个参数是函数,会直接执行。运行next()
会执行下一个符合要求的app.use
中注册的函数,包括满足条件的父路由里面的函数也会执行;不执行next()
下面的就不会执行。
满足条件:
- 请求方式
- 路由地址
路由条件:
app.use('/api')
,get 和 post 都能匹配app.get('/api')
,只匹配 getapp.post('/api')
,只匹配 post
中间件原理
Koa2 框架
async/await 要点
await
后面可以追加promise
对象,获取resolve
的值await
必须包裹在async
函数中async
函数执行返回的也是一个promise
对象- try-catch 可以截获
promise
中reject
的值
洋葱圈模型
一个request
从进入到response
的过程,一层一层进入,再一层一层出来。
- 处理过程
next()
:第一层,开始 -> 第二层,开始 -> 第三层,开始 -> 第三层,结束 -> 第二层,结束 -> 第一层,结束
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
console.log('第一层,开始');
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
console.log('第一层,结束');
});
// x-response-time
app.use(async (ctx, next) => {
console.log('第二层,开始');
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log('第二层,结束');
});
// response
app.use(async (ctx) => {
console.log('第二层,开始');
ctx.body = 'Hello World';
console.log('第二层,结束');
});
app.listen(3000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33