案例-开发 Web Server 博客

L134 - Node.js 从零开发 Web Server 博客项目 前端晋升全栈工程师必备open in new window

流程图

image

目标

开发一个博客系统,具有博客的基本功能。

需求

需求指导开发。

技术方案

  • 数据如何存储。
  • 如何与前端对接,接口设计。

存储用户数据

接口设计

开发接口

  • 暂时不连接数据库,先模拟数据,写出接口

  • 初始化路由:根据之前技术方案的设计,做出路由

  • 返回假数据:将路由和数据处理分离,以符合设计原则

处理路由

API 和路由

API

  • 前端和后端,不同端(子系统)对接的一个术语
  • 有 url(路由),有输入,有输出

路由

  • 是 API 的一部分
  • 后端系统内部的一个定义

登录

  • 登录校验&登录信息存储
  • Cookie 和 Session
  • Session 写入 Redis
  • 开发登录功能,和前端联调
  • 客户端操作 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();
});
1
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;
	}
}
1
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);
1
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));
});
1
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);
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

搭建开发环境

  • 使用nodemon监测文件变化,自动重启 node
  • 使用cross-env设置环境变量,兼容 mac、linux、windows

连接数据库

安装mysql

yarn add mysql
1
  • 账户: root
  • 密码: 123456

操作数据库

  1. 建库
  2. 建表
  3. 表操作

users 表:

columndatatypePKNNUQBUNZFAIDefault
idintYY
usernamevarchar(20)Y
passwordvarchar(20)Y
realnamevarchar(10)Y

blogs 表:

columndatatypePKNNUQBUNZFAIDefault
idintYY
titlevarchar(50)Y
contentlongtextY
createtimebigint(20)Y
authorvarchar(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);
  }
});
1
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());
});
1
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');
1
2
3
4
5

拆分日志

  • 按时间拆分
  • 实现方式:linuxcrontab命令,即定时任务

安全

  • sql 注入
  • xss 攻击
  • 用户密码加密(md5)

express 框架

  • 脚手架工具
  • 中间件机制
  • 开发接口,连接数据库,实现登录,日志记录
  • 中间件原理

中间件

每一个在app.use中注册的函数都是中间件。

  • next()

app.use如果第一个参数是函数,会直接执行。运行next()会执行下一个符合要求的app.use中注册的函数,包括满足条件的父路由里面的函数也会执行;不执行next()下面的就不会执行。

满足条件:

  • 请求方式
  • 路由地址

路由条件:

  • app.use('/api'),get 和 post 都能匹配
  • app.get('/api'),只匹配 get
  • app.post('/api'),只匹配 post

中间件原理

Koa2 框架

async/await 要点

  1. await后面可以追加promise对象,获取resolve的值
  2. await必须包裹在async函数中
  3. async函数执行返回的也是一个promise对象
  4. try-catch 可以截获promisereject的值

洋葱圈模型

一个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);
1
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
Last Updated: 2023/8/2 10:45:34
Contributors: licong96