十四、服务端技术选型-磨刀不误砍柴工
1、导学
* 服务端技术选型
- 【基层程序员】:对技术有感情,热衷于追求新技术,有技术选型有偏见,会为“什么是最好的语言”而
争吵。【架构师】:冷静、平和的看待所有技术,不管新、旧,只要场景合适即可采纳,把技术当做实
现业务功能的手段,而不会被技术所束缚。另,还要考虑整体团队的学习和使用成本。
* 将收获什么
- nodejs做服务端常用的技术
- 如何以架构师思维做技术选型
- 扩展技术广度
* 主要内容
- nodejs框架选型:Koa2
- 数据库:Mysql Mongodb Redis
- 登录校验:JWT
- 单元测试和接口测试:Jest
- 线上服务:PM2 + nginx
* 关键词
- 上述的各个技术名词,不再赘述
- 技术广度
* 学习方法
- 抛开对框架的成见,要让它们平等的为你服务。只选择最合适的,不要秀技术
- 没有银弹,不要纠结,执行起来,Just do it!
* 注意事项
- 课程不会带着写代码,但你不要眼高手低,要亲自实践
- 要考虑各种成本:学习成本、开发成本、运维成本
- 要考虑稳定性,考虑社区大环境
- 我们还是前端技术人员,不要和纯后端人员或者DBA比拼
- 不要觉得讲解基础知识是在浪费时间。真正浪费时间的是:你因节省这几十分钟而踩坑或者走弯
2、nodejs框架选型
* 选择Koa2框架
- 所有常见的nodejs框架中,Koa2是最简单、最小的
* 主要产出
- 了解各个nodejs框架,扩充知识广度
* 主要内容
- Koa2和Express
- Egg.js
- Nest.js
* 注意事项
- 架构师不纠结于框架。主要看业务需求,研发成本和效率,稳定性。
- Koa2并不一定是你实际工作中的最佳答案,根据自己的情况来选择。
3、介绍koa2和express
* Koa2和Express
- express是非常优秀的框架,npm下载量也远在koa2之上。
- 不过,koa2比express更加简单,而且社区也已经完善,也经历了很多年的验证。我个人更加喜欢
小而美的东西。所以选择koa2。
- 当然,选择express也是非常正确的。这俩都可以。不要纠结于框架。
~ koa2:https://koa.bootcss.com/
~ express:https://expressjs.com/zh-cn/
* 用脚手架创建项目
- koa2:https://www.npmjs.com/package/koa-generator
- express:https://expressjs.com/zh-cn/starter/generator.html
* 用koa2创建项目
- 创建repo:biz-editor-server(为何设置私有private?)
- 本地,用脚手架生成项目
- 调整项目目录,代码都放在src目录下
- 发布到github
* 小结
- 选择koa2并创建项目
- express和koa2用法非常相似,也可以选择
4、egg.js
* egg是现成的框架,也是基于koa2封装的。
* egg拿来讲课,直接使用,会隐藏很多细节。拿就不如我们自己拿koa2,从头封装一个egg。
* egg文档https://eggjs.org/zh-cn/,看完你会发现:它的目录结构、功能模块拆分,和我们最后的代码,
是一样的。
* 可以参考课程已经开发完成的开源项目biz-editor-server,做一个目录的对比。
5、nest.js
* nest.js也是一个框架,默认基于express封装的。
* 使用ts语法的,大量使用装饰器,学习成本会比较高。国内的普及程度没有express和koa广泛,从百度
搜索结果数量能看出来,其实Google也是。
* 所以,相对小众,又比较难入门的框架,一定要慎重选择。架构师思维,千万不要秀技术,不要个性
* nest.js文档https://docs.nestjs.cn/,去了解一下它的设计思想。
########
# 常用命令
nest g module <module-name>
nest g controller <module-name>
nesr g service <module-name>
########
* 路由的完整示例https://docs.nestjs.cn/7/controllers?id=%e5%ae%8c%e6%95%b4%e7%a4%ba%e4%be%8b。
* PS:关于装饰器,不会再讲了,可以去看下《Javascript设计模式》课程https://coding.imooc.com/class/255.html。
6、数据库
* 引言
- Mysql Mongodb Redis都有使用,体现技术广度。注意,真的有使用场景,并不是为了使用而使用。
* 主要产出
- Mysql和Sequelize
- Mongodb和Mongoose
- Redis
* 主要内容
- 介绍Mysql和Sequelize
- 介绍Mongodb和Mongoose
- 介绍Mysql和Mongodb的区别
- 介绍Redis
* 注意事项
- 前端程序员对数据库了解可能不多,如果遇到不懂的一定要自己去补课(相关的实战课)
- 将演示代码,但不会从0带着写,但切忌眼高手低
- 毕竟我们还是前端技术人员,别要求自己有DBA的技术
7、复习数据结构设计
* 回顾之前的数据结构设计和数据流转关系,温故知新。
* 全面考虑一下需要存储的数据,以及需要的存储能力。
- JSON数据存储作品内容
- 作品的其他信息,用户的信息-表格形式存储
- 一些缓存-大型网站,复杂业务,肯定会用到缓存
* 以上需求,就需要使用:Mongodb Mysql Redis。可见是真的有使用场景,并不是为了使用而使用。
8、Mysql和Sequelize
* 引言
- Mysql是Web应用中最常见的关系型数据库。
* 安装
- Mysql Server
- GUI客户端Workbench(或者navicat)
- 课程不讲。但我会整理一个文档,供大家参考。
* 基本使用
- 从入门到基本使用,包括SQL语句,课程不讲,不了解的同学可以去学习实战课。
- 新建一个数据库imooc_lego_course,使用mysql2测试数据库连接。并新建路由/api/db-check,
用于展示结果。直接查看代码演示。
* Sequelize
- 最常用的数据库ORM框架。它让开发者不用写繁琐的SQL语句,通过API即可操作数据。
~ 数据库连接
~ 数据模型
~ 模型和数据表的同步,注意bin/www启动服务时的改动
- 直接看代码演示。
- 代码中没有演示增删改查的API,可以先去看看sequelize文档。
* 小结
- Mysql
- Mysql2
- Sequelize(重点)
* 【附】相关实战课
- 学习一门大型、复杂、非0基础的课程,学习过程中,暂停下来去补充其他知识,是很正常的事情。
~ 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
~ 《Node.js-Koa2从零模拟新浪微博》:https://coding.imooc.com/class/388.html
9、Mongodb
* 引言
- Mongodb是Web应用中最常见的NoSQL数据库。
* 安装
- 服务端
- 客户端compass
- 课程不讲。但我会整理一个文档,供大家参考。
- 新建一个数据库imooc_lego_course,work collection。
* 基本使用
- 概念和使用,课程不再讲。
~ NoSQL
~ db数据库
~ collection集合
~ document文档
* Mongoose
- 基本概念
~ Schema
~ Model
~ Document
- 直接看代码演示
~ 数据库连接
~ 定义Schema Model-在此回顾一下数据结构设计
~ 修改路由/api/db-check
- 代码中没有演示增删改查的API,可以先去看看Mongoose文档。
* 【附】相关实战课
- 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
10、date和时区
* 引言
- Mysql和Mongodb查看数据时,看着时间都不对。其实这和时区有关。
* new Date()
- 控制台运行nodejs,不要用浏览器控制台。
- new Date()直接打印,会显示世界标准时间,和北京时间差8个时区,即8h。同理,new Date(
'2020-11-30 14:15:10')直接打印,结果是2020-11-30T06:15:10.000Z,差着8h。
- 想要获取当前时区的时间,只需要toString()就可以了。
- 如果想要格式,可使用date-fns的format,简单易用。
* mongodb和mysql
- 和js Date一样,mongodb mysql存储的时间,直接显示也是时间标准时间,和北京时间差8h。
例如显示"createdAt": "2020-11-30T06:19:14.000Z"。
- 同理,如果想要显示为当前时区的时间,也只需要toString()即可。所以,可以直接把这个时间返回
给前端,让前端自己去显示当前时区的时间。
########
const d = new Date('2020-11-30T06:19:14.000Z')
d.toString() // 获取使用 date-fns format
########
- 如果在查询数据库时,要进行时间比较,那可以按统一标准:把时间都换成世界标准时间,进行比较。
如要查询mongodb里createdAt在2020-11-20: 0:00:00到2020-11-30 23:59:59的数据
########
const d1 = new Date('2020-11-20: 0:00:00') // 转换为世界标准时间
const d2 = new Date('2020-11-30 23:59:59') // 转换为世界标准时间
// 对d1和d2,使用mongoose进行比较即可
########
* 总结
- 世界上有那么多时区,那么多显示规则。而计算机存储时间的形式都是统一的,也必须统一,否则多乱。
- 所以,只有toString()才分时区,而Date数据不分时区,无论在js中还是在数据库中。
- 另外,Docker虚拟机里,默认没有各个时区,需要自己在Dockerfile里配置。课程后面会讲到,
在此先看一眼。
########
# Dockerfile
FROM node:14
WORKDIR /app
COPY . /app
# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
CMD npm i && npm run prd-dev && npx pm2 log
########
11、Mysql和Mongodb的区别
* Mysql vs Mongodb
- redis是内存数据库,做缓存。这一点很容易理解,不用过多解释。
- 但Mysql和Mongodb有何区别,分别适用于什么场景呢?光一个“关系型”和“NoSQL”,不足以理解。
* 区别
- Mysql是关系型数据库,Mongodb是文件数据库。
- 一个用于存储表格形式,格式规整的数据。一个用于存储文件,格式零散的数据。
* 场景
- 作品信息,用户信息,适合存储在Mysql中
- 作品的内容JSON数据,适合存储在Mongodb中
* 疑问
- 为何不把work信息全部存储到Mongodb?因为work要和user做连表查询。
- 为什么入门课程只讲mongodb?
12、介绍Redis
* Redis
- 大型网站,复杂业务,肯定会用到缓存。
* 多进程内存模型
- 多核CPU擅长处理多进程任务,所有web server也都是多进程的,无论PM2、nginx还是其他。
但进程之间有内存隔离(操作系统的基本功能),所以需要第三方缓存服务。
* 安装
- redis-server
- redis-cli
- 课程不讲。但我会整理一个文档,供大家参考。
* 基本使用
- 安装npm redis插件https://www.npmjs.com/package/redis,参考代码。
- 数据库连接
- 修改路由/api/db-check
* 【附】相关实战课
- 《Node开发Web Server博客》:https://coding.imooc.com/class/320.html
13、登录校验
* 引言
- 选择JWT,放弃Session
* 主要产出
- 了解Web常用的登录鉴权方式
* 主要内容
- cookie和Session
- JWT
- SSO和OAuth2
* 注意事项
- 技术选型要考虑成本:服务租赁成本,维护成本
- 要亲自实践,才能真正理解
* 关于短信验证码登录:
- 用户体验好,无需注册,无需记住密码。
- 但是,它要花钱。而且还要防止攻击,恶意刷接口的。
14、cookie和Session
* cookie做登录校验的过程
- 前端输入用户名密码,传给后端
- 后端验证成功,返回信息时set-cookie
- 接下来所有接口访问,都自动带上cookie(浏览器的默认行为,http协议的规定)
* 为何会有Session?
- cookie只存储userId,不去暴露用户信息
- 用户信息存储在session中
* Session优点
- 原理简单,易于学习
- 用户信息存储在服务端,可以快速封禁某个登录的用户
~ 有这方强需求的人,一定选择Session
* Session的缺点
- 占用服务端内存,有硬件成本
- 多进程、多服务器时,不好同步
~ 一般使用第三方redis存储,成本高
- 跨域传递cookie,需要特殊配置
15、JWT
* JWT === JSON Web Token
* JWT的过程
- 前端输入用户名密码,传给后端
- 后端验证成功,返回一段token字符串
~ 将用户信息加密之后得到的
- 前端获取token之后,存储下来
- 以后访问接口,都在header中带上这段token
* JWT的优点
- 不占用服务器内存
- 多进程、多服务器,不受影响
- 不受跨域限制
* JWT的缺点
- 无法快速封禁登录的用户
* JWT的Session的重要区别
- JWT用户信息存储在客户端
- Session用户信息存储在服务端
* 为何选择JWT?
- 没有快速封禁登录用户的需求(或者极少有这种情况)
- JWT成本低,维护简单
- 需要考虑跨域的扩展性(虽然目前还没有这个需求)
* 代码演示
- 安装npm插件koa-jwt jsonwebtoken
- 封装jwt中间件,并使用
- 封装loginCheck中间件
- 相关的配置项、构造函数等
16、SSO和OAuth2
* 引言
- SSO单点登录
- OAuth2第三方鉴权的常用方式
* 使用cookie实现
- 简单的,如果业务系统都在同一主域名下,比如wenku.baidu.com tieba.baidu.com,就好办了。
可以直接把cookie domain设置为主域名baidu.com,百度也就是这么干的。
* SSO
- 复杂一点的,滴滴这么潮的公司,同时拥有didichuxing.com xiaojukeji.com didiglobal.com
等域名,种cookie是完全绕不开的。
* OAuth2验证
- 上述SSO是oauth的实际案例,其他常见的还有微信登录、github登录等。即,
当设计到第三方用户登录校验时,都会使用OAuth2.0标准。流程参考RFC 6749
- 了解一下大概得流程即可,细节不用太详细看,用到了再说。
17、单元测试和接口测试
* 引言
- 安全感,是人的一种本能需求。男/女朋友需要,你需要,老板也需要。
- 架构师要保证软件质量,得讲求方式方法:单元测试和接口测试,就是重要手段。
* 主要产出
- 单元测试和接口测试
- 单元测试的正确套路
* 主要内容
- Jest和Mocha
- 单元测试为何难以落实
- supertest接口测试
* 注释事项
- 从此开始意识到单元测试的重要性,代码就一定要有测试
* 测试驱动开发TDD
- Test-Driven Development。是一种不同于传统软件开发流程的新型的开发方法。它要求在
编写某个功能的代码之前先编写测试代码,然后只编写使用测试通过的功能代码,通过测试来推动
整个开发的进行。这有助于编写简介可用和高质量的代码,并加速开发过程。
- 我们到没必要这么做,但要知道这个术语。
18、Jest和Mocha
* 引言
- 这两个都是非常优秀的单元测试工具,Jest的npm下载量多一些,不过两者都是一个数量级的。
选择哪个都可以。
* 入门
- 对单元测试未了解的同学,可以通过文档学习
~ https://jestjs.io/docs/zh-Hans/getting-started
~ https://mochajs.cn/#getting-started
* 代码演示
- 先把流程跑起来,测试代码等开发时再写。
~ 安装jest
~ 配置package.json
~ __test__/demo.test.js
* 课外分享
- wangEditor开源项目的单元测试开发指南,最佳实践。
19、单元测试为何难以落实
* 引言
- 单元测试,往往说起来都点头称赞,但做起来都犯难
* 使用方式不合理
- 真正原因:混淆了单元测试和集成测试,导致单元测试代码中有太多Mock!!!如,需要服务器启动
才能执行的代码,就不是单元测试了。
~ 单元测试,是针对一个单元,即单一的功能
~ 单元测试,是针对一段逻辑,有if...else for逻辑的,平铺直叙的代码不用测试
- 如果单元没有被拆分,则拆分出来,再做单元测试。正好,这也是非常好的代码重构。
- 摘抄一段《聊聊架构》里的代码
########
// 改造之前的。不容易进行单元测试,因为有request,需要启动http服务,单元测试就需要Mock
public OrderDTO getUserOrder(HttpRequest request) {
String userId = request.getParameter("userId");
String orderId = request.getParameter("orderId");
UserDTO user = userManager.getUser(userId);
OrderDTO order = orderManager.getOrder(orderId);
if (order != null && order.getUserId != null && order.getUserId.equals(userId)) {
order.setUser(user);
return order;
}
return null;
}
// ---------------- 我是分割线 ----------------
// 平铺直叙的代码不需要单元测试
public OrderDTO getUserOrder(HttpRequest request) {
String userId = request.getParameter("userId");
String orderId = request.getParameter("orderId");
UserDTO user = userManager.getUser(userId);
OrderDTO order = orderManager.getOrder(orderId);
return checkUser(order, user, userId);
}
// 逻辑单元,单独抽离出来,测试输入输出即可,不用Mock
public OrderDTO checkUser(OrderDTO order, UserDTO user, String userId) {
if (order != null && order.getUserId != null && order.getUserId.equals(userId)) {
order.setUser(user);
return order;
}
return null;
}
########
- 另外,测试覆盖率,应该看抽离出来的单元模块,而非所有代码的。因为有些代码根本就不需要、
或者不适合单元测试。
* 另一个现状
- 除了以上代码和技术方面的原因,阻碍单元测试开展的还有另外的原因:研发流程不规范。
~ 研发团队和测试团队分离
~ 如果项目延期,测试团队就抱怨提测延期了,测试时间不够
~ 研发团队也想多一事不如少一事,反正有人做测试,也不用我们写单元测试了
- 上述情况,有工作经验的人相应很熟悉。这种情况,没有太好的解决办法,只能规范研发流程,
规范各个流程的产出。这也是架构师的职责之一。
- 所以,很多现代公司也在做组织架构的转型,特别是现代互联网公司。
~ 以不信任软件工程师为基础。传统软件公司、大公司多采用。特点是:文档规范详细,具有
较大规模的测试团队,研发流程正规且冗长,职责分工明确,程序员就照着文档写代码即可,
写完了就交付测试。
~ 以信任软件工程师为基础。现代互联网中下公司多采用,推荐程序员去理解业务,去承担测试。
这样文档简单,执行效率高。但是要求团队必须敏捷管理,高效沟通,以及要求程序员要了解
业务。
* 小结
- 判断哪些代码需要单元测试,并抽离
- 规范研发流程的步骤和产出
20、supertest接口测试
* 引言
- 有接口测试的保护,让所有接口稳如老狗。
* 本地测试
- jest + supertest
########
// 文档中的一个实例
describe('GET /user', function () {
it('responds with json', function (done) {
request(app)
.get('/user')
.auth('username', 'password')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done)
})
})
########
* 远程测试
- jest + axios,待系统发布到测试机,再使用。
* 代码演示
- 安装supertest axios
- package.json中增加test:remote,不过目前还用不到
- 接口测试代码目录__test__/apis/
* 和单元测试的关系
- 接口测试和单元测试,代码可能都放在__test__下,但两者概念要区分开。
21、pm2和nginx
* 引言
- 线上服务关键:稳定 + 高效
* 主要产出
- pm2和nginx的配置
- pm2和nginx日志拆分
* 主要内容
- pm2
- nginx
* 注意事项
- pm2和nginx都不急于实践,项目上线时会详细讲解
- 但这些内容还是要知道,知道但不熟悉 vs 不知道--这俩是完全不同的
22、pm2
* 特点
- 进程守护 - 稳定
- 多进程 - 高效
- 日志记录 - 问题可追溯
* 基本使用
- 课程不再从0讲解
~ pm2 start xxx.js
~ pm2 restart <id/name>
~ pm2 reload
~ pm2 list
~ pm2 logs <id/name>
~ pm2 stop <id/name>
~ pm2 delete <id/name>
~ pm2 monit
* 配置
########
const os = require('os')
const cpuCoreLength = os.cpus().length // CPU几核
module.exports = {
apps: {
name: 'your-server-name',
script: 'bin/www',
// watch: true, // 无特殊情况,不用实时监听文件,否则可能会导致很多restart
ignore_watch: ['node_modules', '__test__', 'logs'],
// instances: cpuCoreLength, // 线上环境,多进程
instances: 1, // 测试环境,一个进程即可
error_file: './logs/err.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z', // Z表示使用当前时区的时间格式
combine_logs: true, // 多个实例,合并日志
max_memory_restart: '300M', // 内存占用超过300M,则重启
}
}
########
* 代码修改
- 安装:npm i pm2 -g
- pm2配置,在bin/目录下
- 修改package.json scripts(临时修改NODE_ENV=dev然后运行)
* 日志拆分
- 使用pm2-logrotate
- 安装pm2 install pm2-logrotate -g,运行pm2 list即可看到pm2-logrotate的进程
########
# 默认配置如下
$ pm2 set pm2-logrotate:max_size 10M # 日志文件最大10M
$ pm2 set pm2-logrotate:retain 30 # 保留30个文件,多了就自动删掉
$ pm2 set pm2-logrotate:compress false # gzip压缩文件
$ pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
$ pm2 set pm2-logrotate:workerInterval 30 # 单位s,日志检查的时间间隔
$ pm2 set pm2-logrotate:rotateInterval 0 0 * * * # 定时规则
$ pm2 set pm2-logrotate:rotateModule true # 分割pm2模块的日志
# 可修改配置 pm2 set pm2-logrotate:<key> <value>
########
- 可以修改一下rotateInterval试一试。不过,别忘了改回去。
23、nginx
* 引言
- nginx一直是web server的必备神器,以稳定和高性能著称
~ 静态服务
~ 反向代理
~ 负载均衡(课程暂时用不到)
~ access log
* 安装
- 课程不讲。
* 常用命令
- nginx
- nginx -s reload
- nginx -s stop
- nginx -t
- nginx -c xxx.conf
* 配置
- 参考www.imooc-lego.com服务器的nginx配置
########
user root;
worker_processes auto; # 多进程
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
# include /etc/nginx/conf.d/*.conf;
# www.imooc-lego.com
server {
listen 80;
server_name imooc-lego-editor;
charset utf-8;
index index.html;
root /home/work/lego-team/editor;
location / {
try_files $uri $uri/ /index.html; # 支持前端h5 history路由
}
}
# admin.imooc-lego.com
server {
listen 8000;
server_name imooc-lego-admin;
charset utf-8;
location / {
index index.html;
root /home/work/lego-team/admin-fe/dist;
}
location /api/ {
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
########
* 日志拆分
- 系统上线时,还会讲解和演示。
* 使用logrotate
- 阿里云服务器,按照之前配置nginx的方法,已经自带了,无需自己配置。
- 可查看/etc/logrotate.d/nginx文件。
- 这些不急于学习,等项目上线时再说。
* 使用crontab
- crontab即linux的定时任务。可使用它来定时拆分nginx日志,需要手写代码。
- 第一,创建脚本nginxLogRotate.sh
########
#!/bin/bash
base_path='/xxx/xxx/nginx' # 日志目录
log_path=$(date -d yesterday +"%Y%m")
day=$(date -d yesterday +"%Y%m%d")
mkdir -p $base_path/$log_path
mv $base_path/access.log $base_path/$log_path/access_$day.log
mv $base_path/error.log $base_path/$log_path/error_$day.log
########
- 第二,执行crontab -e编辑定时任务,添加这一行
########
0 0 * * * sh /xxx/xxx/nginxLogRotate.sh
########
- 第三,查看已有的定时任务crontab -l
24、开发环境
* 引言
- 高效开发,全面武装
* 效率工具
- eslint prettier
- pre-commit
- commit规范
* 功能
- validator
- cors
十五、服务端CI-CD:github自动化
1、导学
* CI/CD
- 持续集成Continuous Integration(CI)和持续交付Continuous Delivery(CD)
- 要让开发人员更加关注于业务代码的开发,那么架构师就得保证研发流程。
- 合理全面的CI/CD,自动化研发流程,提高研发效率,增加系统稳定性。
* 标题
- 服务端CI/CD流程:让github自动化为我们服务
* 将收获什么
- 使用Github actions进行CI/CD
- 学会Docker在nodejs项目中的应用
- 搭建测试环境
* 主要内容
- 使用Github actions
- 使用Docker
- 使用Docker-compose
- 自动发布到测试机
* 关键词
- CI/CD
- Github actions
- Docker Docker-compose
- 新技能,有点难度
* 学习方法
- 一定要亲自实践起来
- 买一个便宜、低配的云服务器(系统centos 7.x)
* 注意事项
- Linux系统使用和基础命令,不会从0开始讲
2、Github actions
* 引言
- 网上的CI/CD有很多,例如travis,任选一个即可。
- Github actions高效稳定,功能强大,易学易用。
* 主要产出
- 使用Github actions自动化构建和测试
* 主要内容
- 认识Github actions
- 使用Github actions做构建和测试
* 注意事项
- 接口测试,依赖于测试机搭建,后面会讲解
3、认识Github actions
* 引言
- Github 2019年秋天发布的CI/CD工具,功能强大且稳定。
- PS:Github被微软收购之后,越来越强大了,正在由一个git托管服务,变为一个研发项目解决方案。
* 基本认识
- 介绍:https://github.com/imooc-lego/biz-editor-server-open/actions/new
- 中文文档:https://docs.github.com/cn/free-pro-team@latest/actions/learn-github-actions
- 代码在项目的.github/workflows目录下,.yml格式文件。
* 应用场景
- 参考:https://github.com/imooc-lego/biz-editor-server-open/tree/master/.github/workflows
- master分支,自动化测试
- dev分支,自动部署到测试机
- v*.*.*格式的tag,自动上线(支持回滚)
* 代码演示
- 触发条件on
~ push
~ branches
~ paths
- 任务jobs
- 步骤steps,可自定义,也可使用第三方
########
# 直接使用uses第三方
- uses: actions/checkout@v2
# 使用name和uses第三方
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 14
# 使用name和run
- name: lint and test
run: |
npm i
npm run lint
npm run test
# 直接使用run
- run: npm i
- run: npm run lint
- run: npm run test
########
* 小结
- 认识Github action
- 使用场景
- yml语法格式
4、Github actions自动测试
* 新建workflow
- 新建test.yml
########
name: test
on:
push:
branches:
- master
paths:
- '.github/workflows/**'
- '__test__/**'
- 'src/**'
jobs:
test:
runs-on: ubuntu-lastest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 14
- name: lint and test
run: |
npm i
npm run lint
npm run test:remote
########
* 关于测试流程
- pre-commit时执行本地接口测试npm run test:local
- master push时执行远程接口测试npm run test:remote
- 到本节为止,测试机尚未部署,所以暂时使用一个现成的测试机182.92.168.192:8081。
5、Docker
* 引言
- 基于Docker,我们可以把开发、测试环境,一键部署到任何一台机器上。只要该机器安装了Docker。
- 有了Docker就有了一切。
* 主要产出
- 使用Docker构建nodejs项目
* 主要内容
- 认识Docker
- Dockerfile
* 注意事项
- 专业的运维工程师对Docker还有更全面的应用:弹性扩展、微服务等
6、认识Docker
* 介绍
- Docker就是一种虚拟机技术,比传统虚拟机(如vmware、virtualbox)要更加简单、轻量。
~ 启动快
~ 资源占用少
~ 体积小
* 安装
- 不再演示,直接看文档。安装完记得镜像加速,搜索“docker镜像加速”即可。
- 安装完,运行docker version可查看版本。
* 基本概念
- 回忆之前使用vmware、virtualbox时,步骤如下
~ 下载一个centos.iso文件
~ 使用vmware安装一个系统A
~ 使用vmware安装一个系统B
~ ...
- 此处的centos.iso文件就是一个image镜像,安装出来的系统就是一个一个的container容器。
- docker的所有image都可以在https://hub.docker.com/搜索并下载,还可以自定义image上传到这个仓库。
7、常用命令
* 引言
- 如果没有安装,可以在play with docker体验一下
* image镜像
- 下载镜像docker pull <image-name>:<tag>
- 查看所有镜像docker images
- 删除镜像docker rmi <image-id>
- 上传镜像docker push <username>/<repository>:<tag>,要先注册hub.docker.com
- PS:如果docker images出现REPOSITORY是<none>的情况,可以运行docker image prune删除。
* container
- 启动容器:docker run -p xxxx:xxxx -v=hostPath:containerPath -d --name <container-name> <image-name>
~ -p:端口映射
~ -v:数据卷,文件映射
~ -d:后台运行
~ --name:定义容器名称
- 查看所有容器docker ps,加-a显示隐藏的容器
- 停止容器docker stop <container-id>
- 删除容器docker rm <container-id>,加-f是强制删除
- 查看容器信息,如IP地址docker inspect <container-id>
- 查看容器日志docker logs <container-id>
- 进入容器控制台docker exec -it <container-id> /bin/sh
* 功能演示
- 以nginx为例。
########
docker run -p 81:80 -d --name nginx1 nginx
docker ps
# 访问 localhost:81,并查看log
docker exec -it <container-id> /bin/sh
cd /usr/share/nginx/html
echo hello docker world index.html
exit
# 重新访问localhost:81,强制刷新
docker stop <container-id>
docker rm <container-id>
########
- 单独演示一下-v数据卷。掌握好这个概念,后面Docker-compose会继续用到。
########
# 1.新建/Users/wfp/html/index.html,内容自定义即可
# 2.运行
docker run -p 81:80 -v=/Users/wfp/html:/usr/share/nginx/html -d --name nginx1 nginx
# 3.访问 重新访问localhost:81,看是否你创建的页面?
########
* 小结
- docker概念
- docker命令,run时的各个参数
8、Dockerfile
* 引言
- 一个简单的配置文件,描述如何构建一个新的image镜像。
- 注意:必须是Dockerfile这个文件名,必须在项目的根目录。
* 语法
########
FROM node:14
WORKDIR /app
COPY . /app
# 构建镜像时,一般用于做一些系统配置,安装必备的软件。可有多个RUN
RUN xxx
RUN xxx
RUN xxx
# 启动容器时,只能有一个CMD
CMD xxx
# 环境变量
ENV K1=v1
ENV K2=v2
########
* 构建
########
docker build -t <name> . # 最后的`.`指Dockerfile在当前目录下。
docker images
########
* 代码演示
- .dockerignore文件
- Dockerfile文件
- 本地安装pm2:npm i pm2 --save-dev,或者Dockerfile中全局安装pm2
- 由于Docker容器中没有数据库环境,服务启动不会成功。
- 可以临时修改一下,再测试:
~ 去掉数据库连接
~ 去掉bin/www数据同步
~ Dockerfile中改为npm run dev
- 测试过程如下:
########
docker build -t editor-server . # 构建image
docker images
docker run -p 8081:3000 -d --name server1 editor-server # 创建容器,注意端口映射
docker ps
docker logs <container-id> # 需等待构建完成
# 访问localhost:8081,查看docker logs
docker stop <container-id>
docker rm <container-id>
docker rmi <image-id>
########
- 测试完记得git checkout .还原回所有代码。
* 小结
- Dockerfile语法
- 构建镜像
9、Docker-compose
* 引言
- 基于Docker和Docker-compose
- 通过一个配置文件,就可以让你的系统一键启动所有的运行环境:nodejs mysql mongodb redis
* 主要产出
- 通过Docker-compose搭建开发环境
* 主要内容
- 介绍Docker-compose
- Docker-compose配置Redis
- Docker-compose配置Mysql
- Docker-compose配置Mongodb
* 注意事项
- 请预先docker pull用到的镜像,否则Docker-compose初次启动会很慢
10、介绍Docker-compose
* 引言
- 软件设计和开发,有单一职责原则。Docker也一样,每个容器都只负责一个服务。
- 如果开发环境需要多个服务(nodejs mysql mongodb redis),就需要启动多个Docker容器。
- 要连同这多个Docker容器,就需要Docker-compose。
* 安装
- 课程不讲,一行命令即可安装。完成之后运行docker-compose --version可以看到当前版本。
* 配置文件
- 新建docker-compose.yml文件。先以redis为例,演示多个容器如何关联。
########
version: '3'
services:
editor-server: # service name
build:
context: . # 当前目录
dockerfile: Dockerfile # 基于Dockerfile构建
image: editor-server # 依赖于当前Dockerfile创建出来的镜像
container_name: editor-server
ports:
- 8081:3000 # 宿主机通过8081访问
editor-redis: # service name,重要!
image: redis # 引用官网redis镜像
container_name: editor-redis
ports:
# 宿主机,可以用127.0.0.1:6378即可连接容器中的数据库`redis-cli -h 127.0.0.1 -p 6378`
# 但是,其他docker容器不能,因为此时127.0.0.1是docker容器本身,而不是宿主机
- 6378:6379
environment:
- TZ=Asia/Shanghai # 设置时区
########
* 命令
- 构建容器docker-compose build <service-name>
- 启动所有服务器docker-compose up -d,后台启动
- 停止所有服务docker-compose down
- 查看服务docker-compose ps
* 修改代码
- 本地安装pm2 npm i pm2 --save-dev,或者Dockerfile中全局安装pm2
- 新建docker-compose.yml
- 增加config/envs/prd-dev.js
* 演示
- 需要临时改代码,再测试
~ 去掉mysql mongodb连接的代码,只保留redis的代码
~ 去掉bin/www数据同步
- 再测试
########
docker-compose build editor-server # 配置文件的service name
docker-compose up -d
docker-compose ps
# 访问localhost:8081,查看docker logs
docker-compose down
########
* 小结
- Docker-compose的作用
- 配置文件docker-compose.yml
- Docker-compose常用命令
- 容器之间的连接,重要!
~ host: 'editor-redis'
~ 端口依然是6379
11、连接Mysql和Mongodb
* 引言
- 成功连接redis之后,要继续连接mysql和mongodb
* 区别
- redis无数据库,而mysql和mongodb需要创建数据库
- redis是缓存,无需数据持久化,而mysql和mongodb需要
* 代码修改
- 修改docker-compose.yml,代码如下
- 增加mysql/init/init.sql,初始化mysql
- 修改config/envs/prd-dev.js,增加mysql和mongodb的配置
- 修改.gitignore文件,增加一行.docker-volumes/,重要!
########
editor-mysql:
image: mysql # 引用官网mysql镜像
container_name: editor-mysql
restart: always # 出错则重启
privileged: true # 高权限,执行下面的mysql/init
command: --default-authentication-plugin=mysql_native_password # 远程访问
ports:
- 3305:3306 # 宿主机可以用127.0.0.1:3305即可连接容器中的数据库,和redis一样
volumes:
- .docker-volumes/mysql/log:/var/log/mysql # 记录日志
- .docker-volumes/mysql/data:/var/lib/mysql # 数据持久化
- ./mysql/init:/docker-entrypoint-initdb.d/ # 初始化sql
environment:
- MYSQL_DATABASE=imooc_lego_course # 初始化容器时创建数据库
- MYSQL_ROOT_PASSWORD=Mysql_2019
- TZ=Asia/Shanghai # 设置时区
editor-mongo:
image: mongo # 引用官网mongo镜像
container_name: editor-mongo
restart: always
volumes:
- '.docker-volumes/mongo/data:/data/d' # 数据持久化
environment:
- MONGO_INITDB_DATABASE=imooc_lego_course
- TZ=Asia/Shanghai # 设置时区
ports:
- '27016:27017' # 宿主机可以用127.0.0.1:27016即可连接容器中的数据库
########
* 演示
########
docker-compose build editor-server # 配置文件的service name
docker-compose up -d
docker-compose ps
# 访问localhost:8081,查看docker logs
docker-compose down
########
* 小结
- volumes和数据持久化
- environment和创建数据库,设置密码、时区等
12、自动发布到测试机
* 引言
- github actions监听git提交,并执行自定义命令
- docker一键部署开发环境
- 两者集合,即可自动发布到测试机
* 主要产出
- dev分支push时,自动部署到测试机
* 主要内容
- 配置测试机
- 自动发布到测试机
- 远程接口测试
* 注意事项
- 去买一个便宜的服务器,操作系统选择CentOS 7.x
- 思考:线上环境,是否也是这样部署的?
13、配置测试机
* 购买
- 阿里云较贵,可以考虑腾讯云、华为云等。
- 学习用,只是要一个服务器而已,不用太考虑稳定性和其他扩展能力。
* 创建work账号
- 出于安全考虑,日常不会用root账号登录,权限太高了。
- 用root登录,创建work账号。
########
adduser work
passwd work
########
- 添加work的sudo权限。
########
whereis sudoers # 找到文件位置 /etc/sudoers
chmod u+w /etc/sudoers # 修改权限,u表示所有者,w表示写权限,+表示添加
vim /etc/sudoers # 编辑该文件
# 找到 `root ALL=(ALL) ALL`
# 再加一行 `work ALL=(ALL) ALL`
chmod u-w /etc/sudoers
########
- 然后使用work登录机器。输入su,再输入root账号的密码,即可拥有超级权限。
* 登录信任
- 使用work登录机器,创建~/.ssh/authorized_keys文件。
########
# 修改文件夹权限
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
########
- 将本机的id_rsa.pub内容粘贴进来。
- 退出重新用work登录,将不用再输入密码。
* 安装必备软件
- 以下都需要su权限。
* git
########
yum -y install git
git --version
########
* docker
- 安装docker:https://docs.docker.com/engine/install/centos/
- docker镜像加速
- 安装docker-compose:https://docs.docker.com/compose/install/
########
docker version
docker-compose --version
########
* 开发端口
- 开发需要的端口,否则外网无法访问该端口。
~ B端FE-80
~ B端server-8081
~ C端-8082
~ 统计服务,收集日志-8083
~ 统计服务OpenAPI-8080
~ admin FE-8085
~ admin server-8084
- PS:线上环境,不会开放这么多端口,而是用nginx反向代理。
* 小结
- 购买
- work账号
- 必备软件:git docker
- 开放端口,重要!
14、发布到测试机
* 引言
- 回顾github actions
* 梳理思路
- 使用github actions监听dev分支push
- 登录测试机,获取最新dev分支代码
- 重新构建镜像docker-compose build editor-server
- 重启所有容器docker-compose up -d
* 代码演示
- 新建deploy-dev.yml内容如下:
########
name: deploy for dev
on:
push:
branches:
- 'dev' # 只针对dev分支
paths:
- '.github/workflows/*'
# - '__test__/**' # dev不需要立即测试
- 'src/**'
- 'Dockerfile'
- 'docker-compose.yml'
- 'bin/*'
jobs:
deploy-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set ssh key # 临时设置ssh key
run: |
mkdir -p ~/.ssh/
echo "${{secrets.WFP_ID_RSA}}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan "182.92.xxx.xxx" >> ~/.ssh/known_hosts
- name: deploy # 部署
run: |
ssh work@182.92.xxx.xxx "
# 【注意】用work账号登录,手动创建/home/work/imooc-lego目录
# 然后git clone https://username:password@github.com/imooc-lego/biz-editor-server.git -b dev(私有仓库,使用github用户名和密码)
# 记得删除origin,否则会暴露github密码
cd /home/work/imooc-lego/biz-editor-server;
git remote add origin https://wangfupeng1988:${{secrets.WFP_PASSWORD}}@github.com/imooc-lego/biz-editor-server.git;
git checkout dev;
git pull origin dev; # 重新下载最新代码
git remote remove origin; # 删除origin,否则会暴露github密码
# 启动docker
docker-compose build editor-server; # 和docker-compose.yml service名字一直
docker-compose up -d
"
- name: delete ssh key # 删除ssh key
run: rm -rf ~/.ssh/id_rsa
########
* 功能演示
- 教学演示的代码库,和实际项目的代码库,不一样。所以,不能公用一个测试机。
- 可以用实际项目的代码,演示这个过程。
* 远程接口测试
- 待测试机部署完成,即可进行远程接口测试npm run test:remote,即test.yml的内容。
- 注意,一定要等待测试机部署完成,否则测试不通过。
* 小结
- 思路
- 代码实现
- 【特别提醒】如果github actions任务出错,要第一时间去看日志!!!