弹吉他的Coder_Q

V1

2022/09/24阅读:39主题:默认主题

用 node + ts + jest + ci 定时每日给女朋友发送一条消息吧 🥰

一、🚩 前言

好久不见了,各位小伙伴们,最近因为一直在沉淀技术没有怎么输出文章了,今天这篇文章也算是一个偶然的机会下诞生的,哈哈 😂。

🤓 主要原因来源于 Sunshine_Lin三心老总群里的一个小伙伴@我写一个抖音里面一个使用 python 实现的微信公众号定时发送消息给女朋友的脚本。

当时看到的时候心理只想到 “这能难到吉他哥??”。直接答应了一波,而且刚刚好最近女朋友有在开始计划学习,我也可以顺便用程序员的能力给她来一个小提醒

🌚 如果想直接知道技术内幕的可以跳到第三章开始。

项目地址: https://github.com/PlayGuitar-CoderQ/wechat-notices-girlfriend/tree/developer

1.1、🥊 可以学到什么?

  1. 如何使用 coding 设计和迭代自己的项目
  2. 如何一份代码推送多仓库
  3. node 使用 ts 的遇到的坑
  4. 如何在 ts 下用 jest 写测试用例
  5. 如何配置 github action 通过 ci 自动部署 node 项目
  6. linux 服务器下如何设置环境变量
  7. node 生态的一种定时任务方案

二、项目前的准备

⚠️ 后面我就按照我如何开发这个项目来叙述每个步骤的细节,大家有兴趣也可以参考一下,希望大家也能学到东西哦。🥰

2.1、 在 coding 创建项目规划迭代

第一次看到的小伙伴可能不知道 coding 是什么东西,我先来介绍一下吧。

coding 其实是腾讯云旗下的一款 devops 的研发平台

⚠️ 这里如果不懂什么是 devops 的小伙伴我这里附上一个来自知乎小龙飞大佬的比较高赞的解释。什么是DevOps?

注册登录到 coding 后我们可以创建一个团队,创建完成之后我们可以来到“项目”页创建一个项目。

来到项目选择模板我的建议是选择全功能 DevOps 功能😆 因为当前这个模板拥有平台给予项目的全部功能,就不用自己再去配置了,即使你用不到。

  • 项目的相关信息大家可以自己按照自己的项目填写哦 🫡

创建完成之后我们进入到项目里来到项目协同页面,创建我们当前项目的迭代。

那么创建迭代是什么意思呢?其实就是规划我们每个版本的下需要完成的事项了。而版本命名比较常见的就是 v1.x.x 这种格式的命名。

但是以上的命名不是一个固定的规范,因为在开源项目里面我也见过使用元素周期表来命名版本的 🧪。

那么我这个项目的第一个版本我是命名为 v1.0.0 并且当我思考当前项目的时候其实我转变了一下想法。其实不一定只拿来做定时发送微信公众号,其实可以设计成一个能做定时任务的脚本,它能执行不同的定时逻辑。

事项里面总共有三个事项类型。对于敏捷开发里用户故事不懂是什么意思的同学我找了两本来自知乎的文章大家想补充知识的可以点击进入看看,分别是三里屯的柯南的知乎视频浅谈敏捷开发中的用户故事CORNERSTONE什么叫敏捷开发?

所以我第一个版本粗略的只规划了以下需求,并且定制好了任务的优先级。

(是不是突然有种从公司回来又在家上班的感觉了哈哈 😹)

2.1、在 coding 项目里面建立代码仓库

⚠️ 如果是当前创建的项目同学们是想开源的话其实也不必在 coding 里面创建代码仓库。因为我喜欢在 github 和 coding 各保存一份代码。

点击左侧导航页代码仓库,点击创建代码仓库来创建我们当前项目的 repo

此时就拿到我们的仓库地址 git@e.coding.net:xxxxxxx.git

如果有同学不知道创建项目的细节的话其实就是和平时在 githubgitee 创建项目的细节是一样的。

⚠️ 这里使用 ssh 拉取推送项目,因为 ssh 协议是比 https 要快的哦,所以同学们可以直接配置一波 github sshcoding ssh。方法其实都和平常配置 ssh 一样的。这里因为比较简单,这里就不贴任何教程了。嫌麻烦的同学也可以直接使用 https

那么其实我这里的步骤暂时就到这里了,这里也可以顺便更新一下项目的外的公告栏。

其实 coding 还有持续集成和持续部署、制品管理这里我暂时没用到,因为我主要依赖了后面 github 的集成能力 action。有探索精神的同学们可以自学挖掘里面的能力。

当然了我后面的文章也会围绕 coding 的功能去完成其他的项目,当然也会用到其他的功能,可以关注一下哦。

2.2 去 github 创建项目

github 其实创建项目和刚刚在 coding 创建的一样即可。

一样拿到我们仓库的地址 git@github.com:xxxxx.git

2.3 在本地创建一个 node 项目

⚠️ 这里我推荐使用的包管理工具为 pnpm。计算机磁盘利用高效,包安装速度快都是它的优点。具体细节大家可以看看这篇文章字节前端的关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn?

 # 在本地项目文件夹创建一个空文件
 # mkdir 你的项目名称,打开终端输入该命令
 pnpm init
 
 #
 安装一个 typescript
 # pnpm install typescript

执行完之后我们当前项目会有一个 package.json 文件,此时可以在里面填上我们项目的相关信息比如从上到下分别是项目的名称、项目描述、项目的搜索关键词、开源协议。

既然我们使用了 ts 来写 node 的项目那么我们可以安装一个 ts-node 的库用于直接使用 nodets 的文件。

⚠️ 这里给比较新手的小伙伴解释一下 ts-node,因为 node 是基于 v8 引擎来解析 js 代码的。如果直接用 node 运行 ts 文件的代码它是不认识,会报错的。所以基于这个库我们可以直接跑起 ts 代码。

那么我们先在文件夹里面定义一个 index.ts 的文件里面随便写一些逻辑,在 package.jsonscript里我们可以写一个脚本命令去运行它。

# package.json

script: {
  "dev": "ts-node ./index.ts"
}

启动这个脚本 pnpm dev 那么就可以看到我们的逻辑被正常运行了。

但是会有一个问题总不能每次改了代码之后要跑一次 dev 吧?那会十分的麻烦。此时我们就要在 install 一个库 nodemon

⚠️ nodemon 这个库的作用就是用于在修改代码的时候自动帮你重新跑起你的项目代码就不用我们每次都要自己手动执行代码了。

但是我的项目本地并不是采用这个方法的,而我是采用另一个库 ts-node-dev 来启动,这个库的好处就是他既有 ts-node 的能力又有 nodemon的能力,并且它显著提高了运行速度,免去了 ts-node每次实例编译的步骤。

# package.json

script: {
  "dev": tsnd -r tsconfig-paths/register lib/index.ts --files
}

2.4、提交代码到两个仓库

在当前创建的文件夹里面我们执行一下 git init 创建一个隐藏的 .git文件

# 本地仓库与远程勾起关系,origin 是可以根据自己喜欢命名
# 仓库地址这里可以选择刚刚 coding 的也可以选择 github 的,其实就是选择你自己的任意一个仓库就好
git remote add origin 你的仓库地址

#
 第二个仓库添加
git remote set-url -add origin 第二个仓库地址

勾起关系之后,我们可以执行 git remote -v 查看是否有仓库地址存在。

⚠️ 这个时候我们可以在项目里面创建一个 .gitignore 文件,这个文件的作用主要是限制提交的代码内容不包括 xxx。这里的 xxx就是你在文件里面写下的文件,例如我们通常不会提交 node_module 文件到仓库里面所以我们可以写下 node_module

此时我们可以执行 git add . 把代码提到暂存区,然后编写我们的 commit 内容。

⚠️ commit 的规范这里我推荐大家可以参考一下 angular commit 规范,而我项目里采用的就是这个规范,只不过在前面多加了一些特别标识的 emoji

然后我们就可以提交代码 git push origin master。执行完之后,一般仓库就会有我们提交的代码了

⚠️ 可能网络不好,或者没有挂 🪜 的同学有几率会遇到 github 超时的问题,最好的解决办法还是挂梯子,或者改 host

那么我们项目前期的准备工作大概就到这里了。

三、wechat-notices-girlfriend 项目的技术内幕

⚠️ 那么这章节就要开始介绍一下这个项目的技术细节了。

3.1、 项目的结构以及为何这样设计

如图,是当前项目的整体结构

一个项目的结构肯定不是与生俱来的,肯定都是要围绕着目的进行。

因为首当其冲我们要实现的最终目的是要能定时发送消息,所以这里可能就涉及到了请求 第三方api项目的配置信息请求的封装任务核心逻辑,这四个部分就是我们最基础的能力。

不过根据上文说到我一开始的构思,压根不想把逻辑写的太死而是想把任务的注册弄的灵活一点,因为在我们生活场景里肯定总会出现有定时帮忙做某件事情的需求,这样后期我想拓展任务的时候也会方便。

所以这里我的设计是在 core 文件夹里面的 base.ts里面封装了一个 TaskBase的抽象类用于作为所有定时任务的基类,而该抽象类的定时方法由自己直接实现。

而定义的 start方法其实就是由集成它的子类也就是需求不同的定时任务来实现,然后抽象类实现的定时任务就直接调用当前的抽象 start 方法。那么这样每个子类都将拥有自己的定时执行能力。

那么 timed 自然就是它的定时时间了。

然后我们就要开始实现我的微信公众号发送的能力了。在core/task 文件夹里面我新建了一个 wechat的文件夹,代表当前文件是和微信公众号相关的核心逻辑。

wechat/index.ts 文件里面的就是我们实现基类的子类了,而实现的start方法里面正式运行我们的发生消息模板的逻辑,但是对于我本人的代码风格,这里肯定不能把全部发送逻辑写死在这里,因为这个明显和命名职责不符合。所以我对发送消息模板的逻辑又抽了一层出来。

sendTemplate方法就是我们最终的调用发送消息的核心方法了。

红色框框从上到下,分别是调用 第三方api情话 🤪 、获取微信公众号模板 token调用发送消息的接口。那么此时整段逻辑就达到我们的目的了。

然后我们把刚刚实现的 core/task/wechat/index.ts 文件里的 WebChat类 引入到外面的 server.ts文件里,进行注册。

⚠️ 这里的 ServerBase 其实也很好理解就是把每个注册进来的任务全部基于 run 方法跑起来。

然后来到 index.ts 也就是我们的项目入口调用一下 .run 方法那么任务就开始执行了

⚠️ 是不是开到这里觉得怎么很多细节没讲到 🥹,别急这里只是大概给大家讲解了一下大致的流程雏形。下面就是开始讲一些项目里的细节部分已经遇到的坑。

3.2、项目中的细节

1. 定时任务基于什么实现的?

答:其实定时任务是基于一个叫 node-schedule的库实现的,该库的好处就是它可以使用 cron 语法去配置你要执行的时间段。

cron 语法不懂的小伙伴,我为大家准备了一个教程简洁明了看懂cron表达式,以及它的文档node-schedule

也就是 scheduleTask.ts 文件的实现。

2. @config/gloabl 是怎么配置别名的?

答:在 tsconfig.json 里面有一个配置选项是 paths 里面就是配置它的一些项目别名。

要记得配置 baseUrl

3. 为什么在有 axios 的情况下还要再加一个 node-fetch 做请求

答:因为 http文件里面虽然是对请求做了封装。但是这里的封装主要目的是针对不同任务有自己不同规则的请求方法为目的的,所以它们一般都拥有自己的baseUrl

但是比如说一些单个第三方的接口请求我就不是很想在当前 http 的里面去注册这些单个接口的请求实例。所以对于单个接口直接采取 fetch 的方案直接请求。

而我选择 node-fetch的库也是因为它比较轻量而且拥有 webfetch 相同的 api

4. global.ts 里面的配置信息从何而来?

答:因为当前项目是要开源到外部。所以 global.ts 里面的配置文件都是一些个人的私密信息,并不适合明文存储。

所以这里我们借助 nodeprocess.envapi来获取到系统的环境变量。

那么其实我这些具体的数据都是存在我本地电脑的 ~/.profile 文件里的。而这个文件是 mac 系统的环境变量之一。

和我同样是 mac 的同学们写完配置后要记得 source ~/.profile 执行一下哦

win 的同学要去看看 win 下的环境变量是如何设置的了,这里因为我的 win 是游戏主机暂时没有开发环境我就先不做实验了哈哈,偷懒一下 😂。

而微信公众号的那些信息其实是在当前链接

测试微信公众号 (链接地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login)

这里使用微信公众号测试是因为它不需要太多的信息审核和提交并且拥有公众号 api 的能力,每个人都能轻松注册使用。而公众号的对等配置就在下图。

3.3、编写测试用例

⚠️ 测试用例我认为其实是一个开源项目比较必备的东西,因为测试用例不仅提高了项目的质量,也让后期维护或者重构代码更加方便和安全,也有让学习当前项目的开发者有例子可以参考,能更快入手。

那么这里我选用的自然是前端测试框架比较扛把子的项目 jest 一个拥有很丰富的断言场景的库。

不过我们既然是在 ts 环境下那么我们自然也要安装一下 ts-jest

但是 jest 里面要使用 ts 也要费点劲,在建立的 jest.config.ts用以下配置

⚠️ 这里还遇到了一个坑,因为运行测试用例的时候 里面有些逻辑是有基于 tsconfig 配置的别名进行引入的,而 jest 是不认识的。所以在 jest.config.ts 里面才会多一行这样的配置

就是告诉 jest 里面 对应的别名文件的映射是什么地址。

这里我们来到 test/wechat.test.ts 文件里面。我在这里其实就写了两个测试用例,第一个是定时任务的测试用例,这个的用例主要是测试定时时间到达指定时间之后是否有运行到当前传入的函数。

⚠️ 这里稍微有一个槽点,因为定时任务首先是异步的,而且需要等待时间执行,所以这里我想出了一个比较笨的方法,因为定时任务的能力最小单位是秒。

⏰ 所以我这里设置的时间是每分钟的第 10s 执行一次任务,所以每次我们启动用例之后,最小用时可能就是第10s 就开始运行的。此时我们传入的方法可能就被直接执行了。但是最大用时可能是第 11s 开始执行的此时我们要再等大概 70 s 左右才到下一个第 10s 执行,所以为了兼容最大用时的情况我直接让当前用例执行最大时间为80s。

那么我们如何知道方法是否被执行的断言呢,这里就依赖了 jestfn 方法。该 api 暴露的方法只要被调用了我们的 toBeCalled 断言就会判断该方法是成功调用的。

而第二个测试用例,其实就是获取微信公众号 token 是否成功返回 token,并且传入 token 调用发送的模板的接口是否成功返回的 errcode 是为0也就是成功的。

疑问1❓: 那么里面的 test,expect 方法是怎么来的?

答:test 和 expect是本身 test.ts 文件自带的,在测试框架运行的时候会执行这些方法。

疑问2❓:怎么执行这些用例?

答:可以在 package.json 里的 script 里写下 test:watch: jest --watch。这个的意思就是使用 jest观察文件模式执行测试用例,代码一旦发生变动就重新跑当前项目里的 test.xx 的用例,有 TDD 编程的味道了。

3.4、 如何配置 githuh action 和部署项目

首先我们需要在当前项目建立一个 .github/worklfows/test-ci.yml的文件。xxx.yml 随便命名。拥有这个文件夹的项目只要上传到 github 那么 github actions 就能感知到一个 action 配置。

来看看 .yml 文件里面的配置吧。

  • name: 当前 action 的名称

  • on: ci 开始

  • push > branches: -test如果是基于推送方式导致 test 分支有更新那么将触发当前的 ci

  • jobsci 工作开始

  • build > runs-on: ubuntu-latest: 构建的服务系统采取乌班图 (其实 github 不止选择乌班图还有其他系统可供选择)

  • steps > uses: actions/checkout@v3: 开启依赖检查的 action

⚠️ 这里是 github 提供的外部 Action,在 github 有十分丰富的 Action 可供使用。

  • steps > uses: github/codeql-action/init@v2: 开启代码检查的 action

⚠️ github/codeql-action 开头的都是当前 action 的配置,详情可到当前 action 的仓库地址查看配置方法。

  • steps > uses:cross-the-world/scp-pipeline@master: 用于链接服务器上传代码的 Action

⚠️ 这里有个小细节,这里怎么知道我们的服务器 ip 和 密码呢?其实在 github 项目的 settings 里面有一个这样的配置。

⚠️ 它为 action 里面提供了一个 secrets 的上下文用于获取这些隐私配置。而 local: ./*,就是复制当前项目的下的全部文件。remote: /home/task 粘贴到服务器的 home/task 文件夹里。

  • steps > uses:appleboy/ssh-action@master: 使用 ssh 链接服务器执行命令行操作。

⚠️ 在介绍这条 Action 之前我们首先要对当前项目先手动部署起来。在我们的服务器 安装 pm2 用于启动守护 node 项目进程。

⚠️ 然后我们在服务器里也配置好在本地配置的那些环境变量(我当前服务器是centos7)所以可以打开 /etc/profile的文件把配置写上去之后,source 一下即可。如何测试呢?在终端输入 echo $xxx 出现了你配置的 value 即可。

⚠️ 然后进入到项目之后我们执行一下 pnpm build 构建出当前的项目的 dist包,这个 dist包的输出主要是靠 tsconfig.json里面 include内的文件进行编译,然后根据 outDir进行输出。

⚠️ 此时我们可以在服务器里面安装 pnpm 管理工具,安装好之后。拿到当前的 dist 包我们把里面的代码上传到刚刚的部署路径下的home/task里面直接在当前路径下启动 pnpm prod。此时就会执行项目里的脚本代码。然后我们打开 pm2 list 就能看到项目被执行了。

那么回到我们刚刚最后一步其实也就是链接服务器之后执行我们为写下的 script 命令,这些命令其实就我们刚刚手动执行部署用到的命令。

然后我们每次提交代码到测试分支那么 github action 就会自动工作部署项目到指定服务器的 pm2 里面了。

四、后续版本的优化

  1. 当前为了完成第一版的需求所以在 ts 编译方面是直接使用了 tsc 进行编译的,但是这个不是一个好的方法。后面可以研究使用 swc 等方法去构建 ts。

  2. 当前任务注册需要侵入原 server 文件,后期会打算把注册抽离到表层。不再侵入到那么多的核心代码文件里面,减少产生 bug 的风险。

  3. v2.0.0版本 可以单独把定时任务发布成一个 npm 包改造当前项目类似于插件化架构。

  4. 增加邮箱通知功能

  5. 增加 git 钩子提交的时候运行测试用例

  6. 限制包管理工具的使用,只能使用为 pnpm

五、结尾

🤓 很开心能看到这里的同学,这次第一次铆足了劲写了足足 6k 多字,对于文中一些不同看法的观点和觉得说错的地方欢迎在评论区留言一下,可以学习一下。

我是爱好学习的吉他哥我们下一篇见咯 😘

image.png
image.png

分类:

前端

标签:

Node.js

作者介绍

弹吉他的Coder_Q
V1

前端开发工程师