CICD
快来分享你的内容吧~
- 2024-12-20·后端开发
- 2023-07-17在之前的内容中,我们分阶段的学习了 CICD 的核心原理以及概念。当然虽然概念非常重要,但是学会如何落地也是非常重要的一件事。因此我们结合我们之前学习的内容,做一个实战训练。 创建应用 因为是实战,我们找个真实的项目来操作一下,一个简单的博客应用就非常好。这里以 hexo 为例。 npm install hexo-cli -g # 全局安装hexo的命令行程序 hexo init blog # 初查看全文鱼友0412:赞!👍早三个月看到这篇文章就好了,回看都是坑。其实还可以缓存,mysql/jdk安装/初始化脚本结果其实都可以缓存,提高工作流的速度。2010分享
- 2023-06-07
记录一次失败的定时导出语雀笔记到 github 仓库经历 (1)
github 有一个功能叫 Actions,可以用来指定定时任务。 再通过语雀的批量导出功能,就可以实现自动定时同步语雀笔记到github仓库。 # 第一步:导出文档 可惜,官方的 API 需要充值较贵的会员。因此,这里使用第三方开源的工具进行导出。这个工具叫 ytools 地址: [https://github.com/vannvan/yuque-tools/](https://github.com/vannvan/yuque-tools/) 。 **由于是第三方,建议各位谨慎使用。** 通过阅读 ytools 的文档,得知我们可以通过以下命令,进行全部文档的导出。注: 需要 NodeJS 环境。 1. 安装仓库 ```shell # 全局安装 ytool npm i yuque-tools -g ``` 2. 进行导出全部仓库 ```shell # secrets.YUQUE_USERNAME 语雀的用户名 一般是手机号 # secrets.YUQUE_PASSWORD 语雀的密码 # 不清楚是否为 Bug all 那里写两个才有效 ytool pull ${{ secrets.YUQUE_USERNAME }} ${{ secrets.YUQUE_PASSWORD }} all all lb ``` 在本地进行测试,一切正常。  # 第二步:编排 Action 现在开始编排 Github Actions. 按照这个列表依次操作即可 1. 新建一个仓库,用于保存笔记。 2. 进入仓库的 Settings 页面 + 打开你的 GitHub 仓库页面。 + 在顶部菜单中,点击 Settings(设置)。 3. 找到 Secrets and Variables + 在左侧菜单中,点击 Secrets and Variables。 + 选择 Actions。这将显示你当前仓库中所有的 GitHub Secrets。 4. 添加新的 Secret + 点击右上角的 New repository secret 按钮。 + 依次添加 `YUQUE_USERNAME` `YUQUE_PASSWORD`  5. 在本地项目根目录初始化 Git 仓库 ```shell git init git remote add origin https://github.com/your-username/your-repo.git ``` 6. 创建 `.github/workflows/` 目录并在其中编写 Action YAML 文件。这里我让 GPT 生成的,供参考: ```yaml name: Sync Notes to Private Repository on: schedule: - cron: '0 0 * * *' # 每天午夜 12 点运行 workflow_dispatch: # 允许手动触发 jobs: sync-notes: runs-on: ubuntu-latest steps: # Step 1: Checkout the repository - name: Checkout repository uses: actions/checkout@v3 # Step 2: Set up Node.js environment - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '16' # Step 3: Install ytool globally - name: Install ytool run: | npm i yuque-tools -g # Step 4: Pull notes from Yuque - name: Pull notes from Yuque run: | ytool pull ${{ secrets.YUQUE_USERNAME }} ${{ secrets.YUQUE_PASSWORD }} all all lb # Step 5: Move the notes to the root folder (i.e., from docs to root) - name: Move notes to root folder run: | mv docs/* ./ # 将 docs 文件夹中的内容移动到仓库根目录 # Step 6: Commit and push the pulled notes to the private repository - name: Commit and push changes run: | git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" git add . git commit -m "Sync notes from Yuque" git push env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` 7. 提交这次的更改,并推送到远程仓库。这样就大功告成了。 ```yaml git add . git commit -m "Add GitHub Actions workflow" git branch -M main git push -u origin main ``` # 第三步:使用我们写的 Action 同步笔记 打开仓库的 Actions 选项就可以发现我们创建的 Action  根据 GPT 写的 YAML 文件,这个 Action 既可以当天晚上同步,也可以手动点击这个页面的 run workflow 进行同步。 触发条件后,就会自动执行 YAML 文件中的命令。 # 失败了 由于网络因素 这个无法在 Github Action 的环境进行成功导出  接下来 我还会继续研究 希望可以成功吧 🤣
docker搭建gitlab-runner
## 使用docker容器技术部署gitlab runner 要使用`docker-compose`文件部署GitLab Runner,你可以按照以下步骤操作: ```shell sudo mkdir -p /opt/store/gitlab-runner ``` 1. **创建`docker-compose.yml`文件**: 创建一个名为`docker-compose.yml`的文件,并添加以下内容: ```yaml version: '3.8' services: gitlab-runner: image: registry.cn-hangzhou.aliyuncs.com/misaka-open/gitlab-runner:alpine3.18 container_name: "gitlab-runner" restart: always volumes: - '/opt/store/gitlab-runner:/etc/gitlab-runner' - '/var/run/docker.sock:/var/run/docker.sock' # 这个挂载是将宿主机上的docker socket挂载到了容器内,这样容器内执行的docker命令会被宿主机docker daemon最终执行 ``` 在这个文件中,我们定义了一个名为`gitlab-runner`的服务,使用的是`gitlab/gitlab-runner:latest`镜像,并设置了卷挂载,以便GitLab Runner可以访问宿主机的Docker socket,从而能够执行Docker命令。 2. **启动服务**: 完成`docker-compose.yml`文件的编写后,使用以下命令来启动服务: ```shell docker-compose up -d ``` <img src="https://pic.code-nav.cn/post_picture/1768277211082678273/V3hFz1bHxPq7Z5U0.webp" alt="image-20241220210654105.png" width="100%" /> 这个命令将会启动GitLab Runner服务,并且以后台模式运行。你可以使用`docker ps`命令来验证服务是否已经成功启动。 3. **注册GitLab Runner**: 启动GitLab Runner容器后,你需要注册Runner。进入容器内部,执行注册命令: ```shell docker exec -it gitlab-runner gitlab-ci-multi-runner register ``` 按照提示输入GitLab实例的URL、注册token、Runner描述、标签等信息。对于执行器(executor),选择`docker`。 ```shell Enter the GitLab instance URL (for example, https://gitlab.com/): http://www.codefather.cn/ Enter the registration token: GR1348941yeBTxcSbQyCRywtspD-b Enter a description for the runner: [f20fce6332f4]: yupao-backend-runner Enter tags for the runner (comma-separated): build Enter optional maintenance note for the runner: Enter an executor: parallels, docker+machine, kubernetes, docker, docker-windows, docker-autoscaler, instance, custom, shell, ssh, virtualbox: docker Enter the default Docker image (for example, ruby:2.7): registry.cn-hangzhou.aliyuncs.com/acs/maven #这是配置了国内的中央仓库地址下载依赖比较快 ``` <img src="https://pic.code-nav.cn/post_picture/1768277211082678273/5YOqHKammChKiINO.webp" alt="image-20241220211421165.png" width="100%" /> <img src="https://pic.code-nav.cn/post_picture/1768277211082678273/AFnfu6IjubjACBc4.webp" alt="image-20241220211738334.png" width="100%" /> 以上步骤可以帮助你使用`docker-compose`文件部署GitLab Runner。确保你已经安装了Docker和Docker Compose,并且你的系统满足GitLab Runner的运行要求。
【CICD】基于 github 的完整流程
<html> <head></head> <body> <div class="content ql-editor"> <p>在之前的内容中,我们分阶段的学习了 CICD 的核心原理以及概念。当然虽然概念非常重要,但是学会如何落地也是非常重要的一件事。因此我们结合我们之前学习的内容,做一个实战训练。</p> <p></p> <h2><strong>创建应用</strong></h2> <p></p> <p>因为是实战,我们找个真实的项目来操作一下,一个简单的博客应用就非常好。这里以 <code>hexo</code> 为例。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install hexo-cli -g # 全局安装hexo的命令行程序 hexo init blog # 初始化项目 cd blog # 切换到项目目录 yarn install # 安装项目依赖 yarn server # 启动hexo服务 </div> </div> <p></p> <p>如果顺利的话,我们可以成功看到如下页面: 这就是一个非常简单的应用。现在我们做一些事情让他能够自动化起来。</p> <p></p> <h2><strong>创建仓库</strong></h2> <p></p> <p>因为我们准备使用的是程序员最广泛使用的 <code>github</code> 作为我们流程管理平台,我们需要在 <code>github</code> 创建一个仓库,创建仓库非常简单,只需要简单的点点点就行。 点击 <code>Create repository</code> 就能成功创建一个空仓库了,成功后应当会显示如下: 现在我们其实可以直接把我们的项目上传到 github 了,但是在此之前我们还需要做一些事情让我们的自动化能够跑起来。</p> <p></p> <h2><strong>开始准备持续集成</strong></h2> <p></p> <p>让我们首先来做一下 CI, 在博客场景下,大部分的代码静态检查是不需要的,因此我们使用 <code>prettier</code> 的 <code>markdown</code> 语法美化来模拟一下持续集成场景。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> yarn add -D prettier # 使用yarn安装prettier echo {} > .prettierrc.json # 创建一个空对象的配置文件 </div> </div> <p></p> <p>此时当我们通过<code>yarn</code>去执行<code>prettier</code>的命令行工具时,会出现错误: 说明默认生成的<code>markdown</code>文件有一些规则问题,我们可以通过<code>prettier</code>去修复它。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> yarn prettier --write ./source </div> </div> <p></p> <p>此时我们再运行<code>prettier</code>检查则显示通过,如下图: 为了让<code>github action</code>能够识别这段检查代码,我们需要在<code>.github/workflows</code>目录下创建一个<code>yml</code>文件。 文件内容如下:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> # .github/workflows/cicd.yml name: CICD on: [push] # run <span class="ql-token hljs-built_in">this</span> action in every push jobs: check: runs-on: ubuntu-latest steps: - name: Checkout Source Code uses: actions/checkout<span class="ql-token hljs-meta">@v3</span> - name: Install and Run Check Script run: | yarn install yarn prettier --check ./source </div> </div> <p></p> <p>这个文件的作用就是每次提交代码时都会执行<code>check</code>任务。<code>check</code> 任务会在<code>ubuntu</code>系统下依次执行代码检出(checkout), 安装依赖以及执行检查脚本。 此时,当我们把代码提交的时候,如果通过的话则会在 github 的commit 记录上出现一个绿色的对号。 现在让我们提交一下项目,在我们的博客应用的根目录执行以下命令:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> git init # 初始化git仓库 git add . # 将当前所有文件提交到暂存区 git commit -m <span class="ql-token hljs-string">"init blog"</span> # 提交改动 git remote add origin git<span class="ql-token hljs-meta">@github</span>.com:<your username>/hexo-cicd-test.git # 添加远程地址,并起名为origin git push -u origin main # 提交本地改动到远程 </div> </div> <p></p> <p>如果顺利的话最后会有以下提示信息: 此时我们回到我们创建的项目页,可以看到在我们的提交记录旁边有一个褐色的小圆点,这表示我们的<code>action</code>正在执行中。 如果我们的 <code>action</code> 执行成功,这个小圆点就会变成绿色的对号,否则则会变成红色的叉号。 成功的表现如下: 持续集成的意义在于确保每次的改动对于其他的协作者来说都是健康的,而绿色的对号表示通过了所有我们预先设下的考验。对于一个多人协作项目来说,这意义重大——这代表了其他人有足够的信心能够基于先前的工作继续迭代项目。没有人愿意为自己改动以外的错误负责,而确保每次的提交都能通过持续集成测试则是一个现代工程师所必须具备的基本素养。 持续集成检查可以做的有非常多,比如单元测试、UI测试、lint规则、类型验证、覆盖率测试、文件大小测试等等等等你所有能够想到的一切静态的、动态的代码检测。 当然,千里之行始于足下,我们目前只是做的最简单的一步。这很简单,但很重要</p> <p></p> <h2><strong>接入持续交付</strong></h2> <p></p> <p>正如前文所说,一名合格的研发所需要做的事情是非常多的。当我们代码写完以后还不算完,我们必须将代码部署到环境中,让其他的相关者能够看到成果才算阶段工作的结束。而持续交付(或者说持续部署)则是可以帮助我们自动化完成这一步工作。 <code>github</code> 为所有的开发者提供了免费的<code>github page</code>服务, <code>github page</code>可以免费的托管静态的网页页面。而<code>hexo</code>正好是一个静态的博客系统,因此我们可以选择直接把我们的博客托管在<code>github page</code>服务中。 首先我们先看下编译方式:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> yarn build </div> </div> <p></p> <p> 执行命令后会在 <code>public/</code> 目录下生成静态文件,直接打开<code>public/index.html</code>可以看到我们的博客,我们只需要把他提交的仓库的<code>gh-pages</code>分支即可。 当然,既然是CD(持续交付), 那么怎么能让我们自己动手呢?我们可以通过<code>action</code>来帮我们完成这一步。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> # .github/workflows/cicd.yml name: CICD on: [push] # run <span class="ql-token hljs-built_in">this</span> action in every push permissions: contents: write jobs: check-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Source Code uses: actions/checkout<span class="ql-token hljs-meta">@v3</span> - name: Install and Run Check Script run: | yarn install yarn prettier --check ./source - name: Build run: yarn build - name: Deploy uses: JamesIves/github-pages-deploy-action<span class="ql-token hljs-meta">@v4</span> with: folder: <span class="ql-token hljs-keyword">public</span> </div> </div> <p></p> <p>我们这里用了一个别人做好的现成的action, 可以直接帮我们把某个目录下的文件推送到当前仓库的<code>gh-pages</code>分支。 注意需要给上内容写的权限,这样<code>action</code>才能把内容写入仓库中。 <code>action</code>成功执行完毕以后就可以看到我们的<code>gh-pages</code>分支出现了编译好的内容。 然后跳转到设置页面,配置<code>github page</code>的信息来源与分支,保存即可 这时我们就可以在项目的右下角看到环境中增加了一个 <code>github-pages</code> 的环境, 点击过去即可跳转到 <code>github</code> 帮我们部署的页面。 效果如下: 这时候我们可以看到我们的样式还是有一点小问题的,这是因为我们<code>hexo</code>默认配置的静态资源的资源目录地址是根路径<code>/</code>开始的,如<code>/css/index.css</code>这样的地址。而在这个场景中,我们的资源是从我们的仓库名<code>/hexo-cicd-test</code> 开始的。 我们修改一下配置,在 <code>_config.yml</code> 中增加如下内容:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> root: /hexo-cicd-test </div> </div> <p></p> <p> 添加完毕后通过git提交到远程,我们的自动化工具会自动帮助我们完成部署的功能。当我们 <code>action</code> 运行完毕后刷新页面就能看到最新的成果了。 </p> <p></p> <h2><strong>总结</strong></h2> <p></p> <p>在本节课,我们用一个简单的博客系统真实而简单的学习了综合使用CICD工具并将其用于实战中,在每次提交都会有自动化流程帮我们做重复性工作,增加了生产力与研发效率。 在本课程中,我们主要学习了为什么我们需要掌握<code>DevOps</code>的思想,如何用<code>CICD</code>作为<code>DevOps</code>落地的工具,学习了<code>CICD</code>的内核与底层设计,并且通过全球最大的开源社区<code>github</code>提供的<code>CICD</code>工具帮助我们实战演示了一下<code>CICD</code>的使用方式与集成手段。相信通过这几章的学习,读者能够掌握不同的流水线工具的使用,因为不管这些工具是怎么包装的,其底层的内核都是不变的,正所谓一法通万法通不外乎如是。 最后感谢各位的陪伴,本课程的内容就此结束了,感谢各位读者的支持与学习。</p> <p>#项目# #技术# #嘉宾分享#</p> </div> </body> </html>
【CICD】基于Docker Image的持续部署
<html> <head></head> <body> <div class="content ql-editor"> <p><br></p> <h2><strong>持续部署的基本原理</strong></h2> <p><br></p> <p>在上一章我们学习了如何构建一个 Docker 镜像,Docker 镜像的特性决定了我们不论是怎么样的环境都可以得到一致的结果。确定了产物的一致性。</p> <p><br></p> <p>而除了产物的一致性以外,为了实现持续部署,我们还需要有一套分发机制。</p> <p><br></p> <p>在现代的 <code>devops</code> 实践中,我们经常会谈论到 <code>google</code> 推行的 <code>kubernetes</code> (简称 k8s),我们只需要告诉 <code>kubernetes</code> 集群的 <code>manager</code> 节点,告知我们想要的镜像/实例数等指标,<code>kubernetes</code> 会自动帮我们实现诸如滚动升级、一键扩容等比较常见的也是曾经会让很多维护者困扰的集群管理问题。<code>kubernetes</code> 作为编排工具组合上 <code>docker</code> 的容器一致性能力就可以帮助我们实现我们常说的持续部署。</p> <p></p> <p><br></p> <p>但是,对于个人学习者来说,kubernetes 过于复杂,概因其设计之初就是面向上百上千个节点的统一管理设计的,因此对于需求简单的用户来说反而是一种负担,我们可能并不需要类似滚动升级,也不需要面对这么多节点,对于个人客户或者初创企业来说往往单节点已经能满足一定的需求。作为工具,既然带来了负担,那么我们就选择不用。那么有什么轻量级的替代品么?其实底层的原理非常简单。</p> <p><br></p> <p>我们把这个流程简化一下,换成单机持续部署:</p> <p><br></p> <p></p> <p><br></p> <p><br></p> <p>其中<code>Proxy</code> 就是起到一个接收器的工作,而<code>Signal</code> 则是部署信号的发布者。当<code>Proxy</code> 接收到信号时,运行脚本对本地正在运行的容器进行一个镜像更新的操作。</p> <p><br></p> <p><code>Signal</code> 可以有很多选择,最简单的我们可以是连接到远程终端,那么我们的信号就是一个<code>shell</code> 命令,如果我们想要实现每次提交都部署最新代码,那么我们的信号就可以是<code>github</code> /<code>gitlab</code> 的<code>webhook</code> 。如果我们想要实现每天凌晨自动部署一个新的版本,那么我们就可以设定为一个定时任务。不论是来自外部还是来自内部,我们需要一个触发器来触发部署的操作。</p> <p><br></p> <p><code>Proxy</code> 阶段则是信号的消费者,在这个角色可以是任何方式,比如老牌的 CICD 软件<code>Jenkins</code> 就可以接受来自外部的 http 请求,来执行一段 shell 脚本。当然功能并不复杂,我们甚至可以自己写一个简单的 http 应用来接受来自外部的调用,触发命令来执行镜像的升级。</p> <p><br></p> <p>最简单的也是最原始的方式就是通过<code>docker</code> 手动进行镜像更新、停止容器、删除容器、创建新容器来实现更新。使用<code>shell</code> 命令来描述的话大概会是如下的操作:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> docker pull anyImage </div> <div class="ql-code-block"> docker stop anyContainer </div> <div class="ql-code-block"> docker <span class="ql-token hljs-built_in">rm</span> anyContainer </div> <div class="ql-code-block"> docker run -d --name anyContainer anyImage </div> </div> <p><br></p> <p>不过因为我们在实际操作容器的时候需要更加复杂的如端口映射/目录挂载/修改环境变量等,如果简单的配置修改需要我们手动去维护命令参数的话也未免过于繁琐。因此我们可以考虑使用<code>docker</code> 自带的<code>compose</code> 管理工具。</p> <p><br></p> <p><br></p> <h2><strong>用 Docker Compose 管理镜像</strong></h2> <p><br></p> <p><code>docker compose</code> 是<code>docker</code> 内置的一个配置化管理工具,通过<code>docker compose</code> 可以实现更加规范且有序的容器管理。</p> <p><br></p> <p>一个简单的<code>docker compose</code> 文件可能会表现为以下形式:</p> <p><br></p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> version: "3.3" </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> services: </div> <div class="ql-code-block"> nginx: </div> <div class="ql-code-block"> image: nginx:latest </div> <div class="ql-code-block"> ports: </div> <div class="ql-code-block"> - 8080:80 </div> </div> <p><br></p> <p>一个<code>docker compose</code> 文件的配置并不复杂,定义<code>docker compose</code> 文件格式的版本号(version)、容器名(nginx)、镜像名(nginx:latest)即可,然后如果有网络服务可以再声明一下要映射的端口,如上所示的文件中:</p> <p><br></p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ports: </div> <div class="ql-code-block"> - 8080:80 </div> </div> <p><br></p> <p>表示把容器的80端口映射为宿主机的8080端口,这样,我们就可以通过访问 <a href="http://localhost:8080/" target="_blank" style="color: inherit;">http://localhost:8080</a> 访问镜像中提供的网络服务了。</p> <p><br></p> <p>使用<code>docker compose</code> 也非常简单,只需要记住两个命令即可:</p> <p><br></p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> docker compose up -d <span class="ql-token hljs-comment"># -d 表示在后台持续执行</span> </div> <div class="ql-code-block"> docker compose down <span class="ql-token hljs-comment"># 停止并删除容器</span> </div> </div> <p><br></p> <p><em style="font-family: inherit; font-size: inherit;font-variant-ligatures;font-variant-caps;font-weight;">以上命令需要在配置文件所在目录执行</em></p> <p><br></p> <p><br></p> <h2><strong>通过 Docker compose 来完成最后一块拼图</strong></h2> <p><br></p> <p>当我们了解了 <code>Docker compose</code> 的基本操作以后,我们就能自己做一个简单的部署系统了。一个简单的部署系统无非就是接收到指令,然后执行命令,最后返回结果。</p> <p><br></p> <p>而一个最简单的持续部署就是 <code>Signal(发送端)</code> 、 <code>Proxy(接收端)</code> 、<code>Worker(执行端)</code> 组成,我们通过 <code>github webhook</code> 作为发信端,一个简单的http服务作为接收端,最后通过http服务执行命令来实现部署操作。</p> <p><br></p> <p>其中<code>Proxy</code> 和 <code>Worker</code> 是可以放在同一个服务器上的(当然也可以拓展到多个服务器,不过目前我们先学习比较简单的)。</p> <p><br></p> <p>这时,我们已经了解了这套体系的基本逻辑和设计,那么我们来开始实施一下:</p> <p><br></p> <p><em>需要再次强调的是,本文所示例的方式更多的是为了开拓读者的思路,期望大家学习的时候主要以原理为准,其底层原理放之四海而皆准,而无需拘泥于某个具体的系统</em></p> <p><br></p> <h3><strong>Signal</strong></h3> <p><br></p> <p>只需要在github的设置项中简单的创建一个 webhook 即可,当我们的代码有任何推送信息的时候就会发送一个回调到远程</p> <p><br></p> <p></p> <p><br></p> <p>当然等到我们需求开始复杂了以后可以逐步细化,比如通过github action对改动的分支/文件路径/tag做过滤和判断等。</p> <p><br></p> <h3><strong>Proxy</strong></h3> <p><br></p> <p>接收端我们可以用一些现成的CICD系统,当然因为实际上功能并不复杂我们自己写一下也很简单。为了大家能够了解细节这里手动实现一个简单的http服务,来帮助大家理解内部原理。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> express <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"express"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> execa <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"execa"</span>; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> app = <span class="ql-token hljs-title">express</span>(); <span class="ql-token hljs-comment">// 创建express 实例作为http服务</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> port = <span class="ql-token hljs-number">3000</span>; <span class="ql-token hljs-comment">// 监听端口3000</span> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> app.<span class="ql-token hljs-title">get</span>(<span class="ql-token hljs-string">"/deploy"</span>, <span class="ql-token hljs-keyword">async</span> (req, res) => { </div> <div class="ql-code-block"> res.<span class="ql-token hljs-title">send</span>(<span class="ql-token hljs-string">"Start Deploying!"</span>); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">try</span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">await</span> <span class="ql-token hljs-title">execa</span>(<span class="ql-token hljs-string">"docker"</span>, [<span class="ql-token hljs-string">"compose"</span>, <span class="ql-token hljs-string">"pull"</span>], { </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">stdout</span>: <span class="ql-token hljs-string">"inherit"</span>, </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">stderr</span>: <span class="ql-token hljs-string">"inherit"</span>, </div> <div class="ql-code-block"> }); <span class="ql-token hljs-comment">// 从当前目录下的 docker compose 配置文件中拉取远程的最新镜像</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">await</span> <span class="ql-token hljs-title">execa</span>(<span class="ql-token hljs-string">"docker"</span>, [<span class="ql-token hljs-string">"compose"</span>, <span class="ql-token hljs-string">"up"</span>, <span class="ql-token hljs-string">"-d"</span>], { </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">stdout</span>: <span class="ql-token hljs-string">"inherit"</span>, </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">stderr</span>: <span class="ql-token hljs-string">"inherit"</span>, </div> <div class="ql-code-block"> }); <span class="ql-token hljs-comment">// 启动/更新容器镜像</span> </div> <div class="ql-code-block"> } <span class="ql-token hljs-keyword">catch</span> (e) { </div> <div class="ql-code-block"> <span class="ql-token hljs-variable">console</span>.<span class="ql-token hljs-title">error</span>(e); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> app.<span class="ql-token hljs-title">listen</span>(port, <span class="ql-token hljs-function">() =></span> <span class="ql-token hljs-variable">console</span>.<span class="ql-token hljs-title">log</span>(<span class="ql-token hljs-string">`Deploy app listening on port ${port}!`</span>)); </div> </div> <p><br></p> <p><br></p> <p>逻辑非常简单,启动一个http服务,监听来自网络的请求。当请求<code>/deploy</code> 路由的时候在当前目录执行<code>docker compose</code> 脚本命令,更新镜像,完成后用新的镜像启动服务。以<code>Proxy</code> 的角度来看,就是简单的向<code>Worker</code> 发送一个指令,起到一个转发命令的作用。具体的工作将会由<code>Worker</code> 来完成</p> <p><br></p> <h3><strong>Worker</strong></h3> <p><br></p> <p><code>docker compose</code> 在这套流程中是作为<code>worker</code> 存在的,<code>worker</code> 顾名思义就是干活的角色。<code>docker compose</code> 接收到了来自上级的指令后就负责执行拉取最新的镜像与使用新的镜像重建容器的任务。当命令执行完毕后,我们的新的镜像也就被成功部署了。</p> <p><br></p> <h2><strong>总结</strong></h2> <p><br></p> <p>相信聪明的读者已经可以看出来了,当管理的实例多了,<code>worker</code> 的角色就可以自由的切换成 k8s 或者其他的角色,而上述的任何角色都可以被替换成任意的其他应用,但是其本身的职责是完全不会发生变化的,无非就是 <strong>发送端->接收端->执行端</strong> 这样的流程。只要掌握了其核心的架构,不论工具怎么发展迭代,其内核都是完全不变的。期望我们能够学会的是事物的本质而不是工具的使用。</p> <p><br></p> <p>通过本章内容,我们学会了如何使用 Docker Image 完成一个简单的部署系统。在下面的章节我们会深入学习如何将我们的编码工作与CICD相结合起来,真正的能够从中收益。</p> <p><br></p> <p>小思考: 学会把复杂的、具象化的东西抽象为统一的概念是学习任何事物都应当掌握的能力。除了编程,你还能举出哪些场景是可以把事情抽象的呢?(举个例子: 上学、报班、买课程、买书都可以抽象成用金钱换取知识的过程)</p> <p><br></p> </div> </body> </html>
【CICD】从构建一个镜像开始
<html> <head></head> <body> <div class="content ql-editor"> <p>在学习如何通过 Docker 发布/部署 应用之前,我们先要学习一下如何创建一个 Docker 镜像。</p> <p><br></p> <p>首先,我们要告知 Docker 我们的应用是如何运作起来的,这一步我们需要创建一个 Dockerfile 文件来描述我们需要做的事情。</p> <p><br></p> <blockquote> <div class="blockquote-item"> 因为本课程更多的教大家学习的是通用技术而不是具体某个项目,因此如果大家需要一步步按照教程去走,可以直接使用自己的项目,或者随便找一个现成的初始化项目来学习 </div> </blockquote> <p><br></p> <p>不过,在创建之前,我们先要想一下我们的一个应用是如何运作起来的。我们以一个简单的 node 应用为例,运行一个 node 程序我们需要以下几个要素:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>安装了 nodejs 的运行环境</li> <li data-list="bullet"><span class="ql-ui"></span>node 程序使用 <code>npm install</code> 等命令安装了依赖</li> <li data-list="bullet"><span class="ql-ui"></span>通过命令调用 node 执行对应的程序,如: <code>node index.js</code> , 或者集合到 <code>npm scripts</code> 中,比如:<code>npm run start</code> 。</li> </ol> <p><br></p> <p>因此,我们将其转换成 shell 命令则为如下所示命令:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install </div> <div class="ql-code-block"> npm start </div> </div> <p><br></p> <p>为了让 Docker 知道如何操作,我们需要转换成 Dockerfile 的语法, 在项目的根目录创建一个 Dockerfile 文件:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment"># Dockerfile</span> </div> <div class="ql-code-block"> RUN npm install </div> <div class="ql-code-block"> CMD npm start </div> </div> <p><br></p> <p>其中 <code>RUN</code> 指令则是执行 shell 命令,而 <code>CMD</code> 表示我们要最后一直执行的命令,这行命令的输出会被不断监听并记录到日志中。 从运行时机的角度来解释,RUN是构建镜像的时候会执行的(编译时),<code>CMD</code> 是运行镜像的时候会执行的(运行时)。 当然,仅仅这些是完全不够的,上一节我们说了,docker 是一个完整的、隔离的容器。因此我们为了让我们的代码能够运行,我们可以直接从别人已经做好的某一个现成的环境开始,比如纯系统的<code>Ubuntu</code> , 再比如说<code>nodejs</code> 官方已经做好的镜像开始。 使用方式也非常简单,只需要在 Dockerfile 的最前面加上一行声明即可,如下:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> FROM node:lts </div> </div> <p><br></p> <p><code>FROM</code> 表示我们的 docker 声明文件从某个镜像开始,如果这个镜像不存在的话再构建镜像时会自动从远程下载该镜像。 其中 node 表示镜像的名字,这里是node官网提供的镜像,lts是镜像的 tag,表示使用的node版本是长期支持的版本(Long Term Support)。在本节我们假设我们的项目是从node环境下开始的,如果需要py/go/rust或者其他的语言环境的话也可以直接从相应的镜像开始即可。 准备好了镜像以后,我们则需要把我们的代码丢到 docker 的镜像中,一个最简单的方式就是把当前所在目录的所有文件都复制到镜像中。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> COPY . . </div> </div> <p><br></p> <p>COPY表示复制,前一个.表示当前所在的目录,后一个.表示复制目标路径,即镜像的工作空间。我们也可以自己定义一下工作目录以明确项目的目标目录:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> WORKDIR /app </div> </div> <p><br></p> <p>那么经过一些简单的环境初始化的步骤以后,我们的 <code>Dockerfile</code> 文件大概会长成这个样子:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> FROM node:lts # 从nodejs官方镜像开始 </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> WORKDIR /app # 设定工作目录为根目录下的app文件夹COPY . . # 将构建时的所有内容都复制到工作目录(app文件夹)下RUN npm install # 安装应用依赖CMD npm start # 启动应用 </div> </div> <p><br></p> <p>接下来我们只需要在<code>Dockerfile</code> 所在的目录执行 <code>docker build .</code> 即可编译镜像,其中 <code>.</code> 表示当前目录。<code>docker</code> 会根据 <code>Dockerfile</code> 所描述的步骤一步步去执行镜像的构建操作,当镜像构建完毕后我们会拿到一串唯一的 hash 表示镜像的名称,我们可以直接用或者给他起个名字都行。总之,我们得到了我们的第一个镜像,我们可以通过<code>docker run <image_id></code> 来运行这个镜像。</p> <p><br></p> <h2><strong>总结</strong></h2> <p><br></p> <p>在本节我们简单的学习了如何改造现有的项目,将其构建成一个 Docker 镜像,并运行它。下一节我们来学习一下如何发布镜像、拉取/更新镜像,以及将其运行在服务器上。</p> <p><br></p> <p>小思考: 如果把部署东西当成制作食物,那么镜像就是食物的模具,容器就是食物本身。那么类似的,食物的原材料是什么呢?</p> <p><br></p> <blockquote> <div class="blockquote-item"> 关于Dockerfile的一些进阶的介绍可以看我们星球另一位大佬的文章: </div> <div class="blockquote-item"> 5 个提升你 Dockerfile 水平的技巧 https://articles.zsxq.com/id_9u1a1gbtim2z.html </div> </blockquote> <p><br></p> </div> </body> </html>
【CICD】部署方式
<html> <head></head> <body> <div class="content ql-editor"> <p>在前面的学习中,我们了解了,CI 流程有什么用,以及是如何运作起来的。从本节开始,我们将会开始学习 CD 流程。</p> <p><br></p> <blockquote> <div class="blockquote-item"> 持续部署(英语:Continuous deployment,缩写为 CD),是一种软件工程方法,意指在软件开发流程中,以自动化方式,频繁而且持续性的,将软件部署到生产环境(production environment)中,使软件产品能够快速的发展。 </div> </blockquote> <p><br></p> <p>当然,在学习持续部署(CD)前,我们要先了解一下在此之前我们是如何做的。</p> <p><br></p> <h2><strong>刀耕火种的手工作坊时代:SSH</strong></h2> <p><br></p> <p>在软件开发中,有一个非常需要关注的方向就是如何去把我们开发好的产品部署到服务器上,即如何将我们的产品摆上货架。</p> <p><br></p> <p>回顾历史所能探索到的、比较有体系的部署方式就是通过 <code>ssh</code> 了,<code>ssh</code> 是 <code>linux</code> 内置的一个网络协议,我们可以通过 <code>ssh</code> 来实现远程登录以及文件传输。因此,通过 <code>ssh</code> 进行发布就是一种比较常见的选择了。</p> <p><br></p> <p>我们可以通过 <code>ssh</code> 的 <code>sftp</code> 协议将本地编译好的应用(比如 <code>java</code> 中的 <code>war</code> 包)传输到服务器上,或者通过 <code>ssh</code> 远程登录到服务器上然后通过版本控制工具下载源码打包编译,然后在服务器上起一个持久化的应用,比如<code>apache</code> /<code>tomcat</code> 之类注册到系统的服务,或者通过 <code>screen</code> / <code>tmux</code> 之类的终端工具起一个永不挂断的终端应用。更有甚者也可以通过<code>nohup</code> 命令启动,<code>tail -f</code> 查看日志, <code>kill</code> 中断应用。</p> <p><br></p> <p>当然程序员永远不可能满足于此,程序员们可以编写一些自动化脚本帮我们去做这些事情。比如编写一个 py 脚本,建立一个<code>ssh</code> 连接,自动化的在远程服务器上执行一些命令,实现一键部署,这在当时是很多程序员的首选方案。</p> <p><br></p> <p>然而,总的来看,将一个后端服务在服务器上启动的方式有很多种,但是这些方式也还是逃不开粗糙的本质,我们透过花里胡哨的操作与自动化脚本,究其本质还是脱离不了 ssh 的限制。其内核还是一个一对一的操作,即一个<code>client</code> 终端对应一个<code>server</code> 服务端,这就决定了不论怎么样,这种部署方式对于大规模去应用是不可行的。不管做到何种地步都逃脱不了其手工作坊的本质,更何况还会有各种各样运行环境带来的边界问题困扰着当时的程序员。</p> <p><br></p> <p>那么有没有可能有更加优雅方式,实现从手工作坊完成到工业革命的蜕变呢?</p> <p><br></p> <p><code>Docker</code> 对此举起了革命的旗帜。</p> <p><br></p> <h2><strong>工业革命的制品工坊:Docker Image</strong></h2> <p><br></p> <p><code>docker image</code> 的引入对业界产生了一些颠覆性的变革,首先一点就在于环境的隔离性。为什么这么说呢?以 Linux 的软件生态举例,在 Linux 的包管理中有很多软件都是以源码发布的,不仅仅是因为开源的问题,而是因为 Linux 的生态多样性导致很难预先编译出一个或者多个二进制文件可以同时兼容不同的 Linux 环境,因此与其提供二进制文件不如直接提供源码,让使用者在自己的环境下编译出一个自己环境能够解析的二进制文件。</p> <p><br></p> <p>而类似的问题也存在部署的时候,试想一下,一个应用在本地是正常的,到了线上就出现各种各样奇怪的问题,那是一件多么令人崩溃的一件事情。而不仅于此,如果在服务器 A 是正常的,然后觉得没有问题了,欢天喜地的将文件部署到服务器 B 上,结果出现了问题,那才是真的令人崩溃。</p> <p><br></p> <p>与此类似的,源码编译依赖各种各样版本的编译器,运行时依赖各种各样版本的动态链接库。而这些往往都是在一个环境/系统中唯一的。在实际出现问题之前,这是一个完全混沌的系统,因为你在出现实际问题之前往往很难观察或者说处理不同版本环境的兼容问题——而当实际出现问题以后,往往已经产生了损失。</p> <p><br></p> <p>因此,可以看到,一个完全隔离的运行环境是多么重要,特别的,<code>docker</code> 的环境是一个系统级别的环境,比起应用级别的环境更加底层也更加彻底。系统级别的环境隔离意味着在所有的环境都拥有完全一样的上下文,一台服务器的成功意味着所有服务器的成功。所以我们可以看到,环境隔离是多么重要,而<code>docker</code> 的成功也很大程度上来源于此。</p> <p><br></p> <p></p> <p><br></p> <p><code>docker image</code> 意味着所有的操作都是可复制的。而工业制品与手工业的区别也在于此,这就是为什么我会将 <code>docker</code> 的出现称为工业革命。后续,软件工程师们围绕着<code>docker image</code> 的强大隔离性和原子性,出现了诸如<strong>自动扩容</strong>、<strong>滚动升级</strong>、<strong>高可用</strong>、<strong>分布式</strong>、以及我们本书主要讲的<strong>CICD</strong>等一系列提升软件质量与服务稳定的优秀实践。</p> <p><br></p> <h2><strong>总结</strong></h2> <p><br></p> <p>在本节,我们了解了 <code>docker</code> 的重要性以及能力,下一节我们会学习如何通过 <code>docker</code> 构建一个自己应用的镜像,以及在后续学习如何将这个镜像部署到服务器上。</p> <p><br></p> <p>小思考: 你平时是如何部署应用的呢?</p> <p><br></p> </div> </body> </html>
【CICD】引入单元测试
<html> <head></head> <body> <div class="content ql-editor"> <p>在上一节,我们主要讲了如何通过 <code>Github Action</code> 来做一些简单的 lint 校验和代码格式化检查,这一节我们来学习一下如何写编写单元测试以及如何把单元测试作为我们 <code>CI</code> 的一部分来确保我们的整体的代码质量。</p> <p><br></p> <h2><strong>什么是单元测试</strong></h2> <p><br></p> <p>在学习如何写单元测试之前,我们首先要了解一下什么是单元测试。</p> <p><br></p> <blockquote> <div class="blockquote-item"> 在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试 ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 通常来说,程式设计师每修改一次程式就会进行最少一次单元测试,在编写程式的过程中前后很可能要进行多次单元测试,以证实程式达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是必须的,但也不坏,这牵涉到专案管理的政策决定。 每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用 stubs、mock 或 fake 等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。它的实施方式可以是非常手动的(透过纸笔),或者是做成构建自动化的一部分。 </div> </blockquote> <p><br></p> <p>理想情况是开发同学每次编辑/创建一个功能,都应该加上相应的测试用例, 但是现实的情况会更加复杂一点特别特别的,特别是对于前端环境来说,因为前端的同学面临着你需要去频繁的去更新项目的界面UI,而这些内容是很难被以代码的形式测试的,就算能够以代码的方式去测试到,但是他的维护成本也会非常高,而相对的他的收益又是相对模糊的,导致很多商业的项目在自动化测试用例上的投入并不会很高, 因为对于大部分的商业项目来说,自动化测试用例的投入与维护的成本对比模糊的产出比不上招聘一个QA同学来专门负责测试会收益更加清晰。相对的,良好的开源项目会更加依赖于单元测试而不是人工测试。因为需求变化少,人力成本高,以及缺少定期的回归测试。这两方面的差异导致的不同的项目对于自动化用例的看法也会有一些偏差。</p> <p><br></p> <p>当然,收益的模糊意味着我们不需要去编写任何的单元测试用例了吗?答案是否定的。 对于前端能做到的UI测试,我们现在学的投入会非常高产出的话会比较模糊,但是对于纯函数编写测试用例来说,则是一种投入低以及产出稳定的选择。所以我一般都会推荐我的团队成员去编写一些纯函数,特别是功能复杂的纯函数的单元测试,如果一些复杂的操作被耦合在一些上下文中,那么我也会推荐看能不能把他抽象成一个独立的函数,这样也可以做好代码的抽象工作,方便后续的同学持续性的维护代码。</p> <p><br></p> <p>同时,在编写纯函数的测试用例上践行TDD(Test-Driven Development, 测试驱动开发, 指先编写测试用例和单元测试,然后编码实现其功能。)也会变得可行,因为纯函数的输入输出都是明确的,没有任何副作用。</p> <p><br></p> <p>在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。</li> <li data-list="bullet"><span class="ql-ui"></span>该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。</li> </ol> <p><br></p> <p><br></p> <h2><strong>设计单元测试</strong></h2> <p><br></p> <p>我们举一个简单的例子,我们有一个函数 <code>sum</code> ,它接受两个参数: <code>a</code> 和 <code>b</code> ,然后输出 <code>a+b</code> 的结果。那么这个 <code>sum</code> 函数是一个纯函数么?是的,因为它的输出永远取决于它的输入,而与它当前执行时所处的环境是无关的。</p> <p><br></p> <p>那么现在我们要开始为这个sum函数编写测试用例了。</p> <p><br></p> <p>首先我们设计一下正常用法,比如 1 + 2 = 3。</p> <p><br></p> <p>然后,输入有没有可能是负数或者0?比如-1 + 0 = -1.</p> <p><br></p> <p>那么除了复数有没有可能输入字符串?比如 "1" + 2 = 3, "一" + 2 = 2 ?</p> <p><br></p> <p>既然可能输入字符串了,有没有可能输出对象?输入undefined?输入null? 比如 {"foo": "bar"} + 2 = 2, null + 2 = 2, null + undefined = 0?</p> <p><br></p> <p>我想我已经想到了大部分的正常情况与非正常情况,那么借此我们可以得到一个真值表:</p> <p><br></p> <p></p> <p><br></p> <p>好的,那么我们设计完了真值表以后我们就要开始写代码了,在这里我们会去使用一个目前业界主流的单元测试库 <code>jest</code> (<a href="https://jestjs.io/" target="_blank" style="color: inherit;">https://jestjs.io/</a>),当然不管是什么样的单元测试库,其内核本质上就是去拿不同的输入去执行固定的指令,最后判断结果是否符合预期,所以不需要拘泥于选用什么形式,而是要看到其内核本质。</p> <p><br></p> <p><br></p> <h2><strong>编写单元测试</strong></h2> <p><br></p> <p>好的,那么在我们定义好一个单元测试需要做什么事之后,那么我们就需要开始编写单元测试了,当然在此之前,我们还要对jest做一些简单的配置:</p> <p><br></p> <h3><strong>安装Jest</strong></h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install --save-dev jest </div> </div> <p><br></p> <h3><strong>初始化Jest</strong></h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npx jest --init </div> </div> <p><br></p> <p>根据交互式程序一步步创建即可创建一个最基本的配置文件。运行也非常简单,只需要在命令行中输入<code>npx jest</code> 即可,当然为了方便一般会建议加入到<code>package.json</code> 中(按照最新版的<code>jest --init</code> 会询问你是否要加入,如果选否可以后续手动加入)。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"scripts":</span> <span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"test":</span> <span class="ql-token hljs-string">"jest"</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-punctuation">}</span> </div> <div class="ql-code-block"><span class="ql-token hljs-punctuation">}</span> </div> </div> <p><br></p> <p>执行 <code>npm run test</code> 自动查找匹配规则的单元测试文件。</p> <p><br></p> <p><br></p> <h3><strong>编写单元测试</strong></h3> <p><br></p> <p>官方文档上有个简单的示例,创建<code>sum.test.js</code> 文件, 内容如下:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> sum = <span class="ql-token hljs-built_in">require</span>(<span class="ql-token hljs-string">'./sum'</span>); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-title">test</span>(<span class="ql-token hljs-string">'adds 1 + 2 to equal 3'</span>, <span class="ql-token hljs-function">() =></span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-title">expect</span>(<span class="ql-token hljs-title">sum</span>(<span class="ql-token hljs-number">1</span>, <span class="ql-token hljs-number">2</span>)).<span class="ql-token hljs-title">toBe</span>(<span class="ql-token hljs-number">3</span>); </div> <div class="ql-code-block"> }); </div> </div> <p><br></p> <p>这是一个很简单的测试用例,即简单的输入固定参数然后检查输出的返回值,在本例中定义了测试名称,然后执行 <code>sum</code> 函数,输入参数<code>(1, 2)</code> 然后通过<code>toBe</code> 指令来验证结果是否为<code>3</code> 。</p> <p><br></p> <p>我们可以基于此来做一些简单的改造,使其能够适配我们所能想到的用例并且更加方便的拓展更多的测试用例。这里我们要用到<code>jest</code> 的<code>test.each</code> 方法。</p> <p><br></p> <p>根据上面的真值表,我们可以得到以下的测试用例:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> sum = <span class="ql-token hljs-built_in">require</span>(<span class="ql-token hljs-string">'./sum'</span>); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-title">describe</span>(<span class="ql-token hljs-string">'test `sum`'</span>, <span class="ql-token hljs-function">() =></span> { </div> <div class="ql-code-block"> test.<span class="ql-token hljs-title">each</span>([ </div> <div class="ql-code-block"> [<span class="ql-token hljs-number">1</span>, <span class="ql-token hljs-number">2</span>, <span class="ql-token hljs-number">3</span>], </div> <div class="ql-code-block"> [-<span class="ql-token hljs-number">1</span>, <span class="ql-token hljs-number">0</span>, -<span class="ql-token hljs-number">1</span>], </div> <div class="ql-code-block"> [<span class="ql-token hljs-string">"1"</span>, <span class="ql-token hljs-number">2</span>, <span class="ql-token hljs-number">3</span>], </div> <div class="ql-code-block"> [<span class="ql-token hljs-string">"一"</span>, <span class="ql-token hljs-number">2</span>, <span class="ql-token hljs-number">2</span>], </div> <div class="ql-code-block"> [{<span class="ql-token hljs-string">"foo"</span>: <span class="ql-token hljs-string">"bar"</span>}, <span class="ql-token hljs-number">2</span>, <span class="ql-token hljs-number">2</span>], </div> <div class="ql-code-block"> [<span class="ql-token hljs-literal">null</span>, <span class="ql-token hljs-number">2</span>, <span class="ql-token hljs-number">2</span>], </div> <div class="ql-code-block"> [<span class="ql-token hljs-literal">null</span>, <span class="ql-token hljs-literal">undefined</span>, <span class="ql-token hljs-number">0</span>], </div> <div class="ql-code-block"> ])(<span class="ql-token hljs-string">'sum(%p, %p) = %d'</span>, <span class="ql-token hljs-function">(a, b, output) =></span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-title">expect</span>(<span class="ql-token hljs-title">sum</span>(a, b)).<span class="ql-token hljs-title">toBe</span>(output); </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"> }); </div> </div> <p><br></p> <p>我们把通用的逻辑抽象出来,然后把单元测试用例单独存放,通过 <code>test.each</code> 的数组形式来将测试用例一个传入到测试脚本中,用 <code>(a, b, output)</code> 分别接受第一、第二、第三个参数,然后通过回调函数的形式将参数传入给测试脚本,最终实现用例和脚本隔离。</p> <p><br></p> <p>虽然到此为止,我们的 <code>sum</code> 函数一行代码也没有写,但是我们已经能够通过单元测试了解这个函数是干什么的了,以及他对于一些边界情况的返回是如何处理的。这也是单元测试的另一个好处,我们无需了解一个函数内部是怎么实现的,只需要关心这个函数的输入输出即可。</p> <p><br></p> <p>虽然 <code>sum</code> 函数内部逻辑比较简单,为了确保大家能够简单的重复本节内容的结果,我这边也简单的写一下。当然建议大家可以不看示例代码自己手动尝试一下先写单元测试再写函数内容,通过单元测试的不断反馈不断修正代码逻辑。 <em>sum.js:</em></p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-variable">module</span>.<span class="ql-token hljs-property">exports</span> = <span class="ql-token hljs-keyword">function</span> <span class="ql-token hljs-title">sum</span>(<span class="ql-token hljs-params">a, b</span>) { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> (<span class="ql-token hljs-title">Number</span>(a) || <span class="ql-token hljs-number">0</span>) + (<span class="ql-token hljs-title">Number</span>(b) || <span class="ql-token hljs-number">0</span>) </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p></p> <p><br></p> <p><br></p> <h2><strong>集成到CI流程中</strong></h2> <p><br></p> <p>好了,我们在前面的内容中学习了什么是单元测试以及如何编写单元测试,现在我们要将单元测试集成到我们的CI流程中,确保每次迭代都会自动进行单元测试,来保证整体迭代质量。</p> <p><br></p> <p>实现方式也非常简单,当然,我们可以把单元测试的流程加载<code>git hook</code> 中,但是随着项目的不断扩大,执行单元测试的时间也会变得非常漫长,有的历史比较久远的项目可以执行几个小时甚至几天。因此我是非常不建议把单元测试放在git hooks阶段中的,因为单元测试和代码静态检查有本质的区别 —— 单元测试需要上下文,即整个项目的运行环境,而静态检查只需要单个文件即可,因此静态检查可以做到只扫描修改的文件,而单元测试则是每次都是全量执行。这就是为什么我会推荐大家把代码静态检查放在本地(提前发现问题)而把单元测试放在远程(并行操作)。</p> <p><br></p> <p>所以, 为了整体的研发效率考量,我个人比较推荐把单元测试的流程放在远程的CI pipeline中。还是以<code>github action</code> 为例,只需要在上一节的<code>.github/workflows/ci.yaml</code> 中增加一行配置即可:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> - name: Check Code Format </div> <div class="ql-code-block"> run: prettier --check ./ </div> <div class="ql-code-block"> - name: Run Unit Test </div> <div class="ql-code-block"> run: npm run test # 检查单元测试是否通过 </div> <div class="ql-code-block"> - name: Check Simple Build </div> <div class="ql-code-block"> run: npm run build </div> </div> <p><br></p> <p>是的,因为我们之前的准备工作做的非常充分,因此只需要很简单的一步就能将单元测试流程集成到远程。只需要提交代码就能看见我们的单元测试会在每次提交都会自动执行了。</p> <p><br></p> <h2><strong>总结</strong></h2> <p><br></p> <p>在本节,我们学习了什么是单元测试、如何设计的单元测试以及如何用代码编写测试,最后将单元测试集成到CI流程中,来确保项目整体质量。在下一节,我们会学习如何实现自动发布(CD),让我们每次提交代码自动部署项目。</p> <p><br></p> <p>小思考: 你写的代码中有哪些是纯函数呢?这些纯函数如果写成单元测试可以有多少情况呢?</p> </div> </body> </html>
【CICD】远程仓库CI流程
<html> <head></head> <body> <div class="content ql-editor"> <p>在上一节我们学习了如何构建本地的 CI 流程来确保整体项目的基本质量(也就是下限),但是因为本地都是不可靠的。我们需要一个最终"裁判"来确定项目的质量,因此我们需要学习如何使用远程仓库的流水线来构建 CI 流程。</p> <p><br></p> <p>虽然 git 是一个分布式的协作方式,这里的分布式是指每个终端都可以独立运行。但是对于团队协作来说一定会有一个远程的中心点,比如 github/gitlab 等应用来作为中间交换节点,以统一项目的协作。因此,我们就可以将这些远程端作为“裁判”或者说“最终决策者”来帮我们做中立的判断。</p> <p><br></p> <p>而 <code>github</code> / <code>gitlab</code> 这些比较主流的 git 应用也是这么思考的,因此他们会配备现成的流水线来帮我们做这些事情。这些流水线的本质就是一个简单的容器空间,在里面我们可以检出代码、运行指令等等,然后流水线通过运行指令的返回值来判断这条流水线是不是通过。</p> <p><br></p> <p>当然了,除了内置的流水线以外还有一些第三方的流水线产品,比如 <code>CircleCI</code> 、<code>Travis</code> 等老牌的产品也是非常受欢迎的。不过我们还是以开源社区主流的 <code>github</code> 为例,不论其形态如何变化,需要了解的是他们的内核都是一样的,学会了一个就等于学会了所有。</p> <p><br></p> <p><br></p> <h2><strong>简单示例</strong></h2> <p><br></p> <p><code>github</code> 内置的流水线产品名为 <code>github action</code> ,只需要在 <code>github</code> 的仓库中创建一个 <code>.github/workflows/<any>.yaml</code> 文件即可, any 可以是任何你喜欢的名称。一个最简单的配置文件可能是这样的(来自官方文档的示例):</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> name: GitHub Actions Demo </div> <div class="ql-code-block"> run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 </div> <div class="ql-code-block"> on: [push] </div> <div class="ql-code-block"> jobs: </div> <div class="ql-code-block"> Explore-GitHub-Actions: </div> <div class="ql-code-block"> runs-on: ubuntu-latest </div> <div class="ql-code-block"> steps: </div> <div class="ql-code-block"> - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." </div> <div class="ql-code-block"> - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" </div> <div class="ql-code-block"> - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." </div> <div class="ql-code-block"> - name: Check out repository code </div> <div class="ql-code-block"> uses: actions/checkout@v3 </div> <div class="ql-code-block"> - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." </div> <div class="ql-code-block"> - run: echo "🖥️ The workflow is now ready to test your code on the runner." </div> <div class="ql-code-block"> - name: List files in the repository </div> <div class="ql-code-block"> run: | </div> <div class="ql-code-block"> ls ${{ github.workspace }} </div> <div class="ql-code-block"> - run: echo "🍏 This job's status is ${{ job.status }}." </div> </div> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><code>name</code> 定义了这个 <code>action</code> 的名字</li> <li data-list="bullet"><span class="ql-ui"></span><code>run-name</code> 定义了本次显示的名称,使用了内置变量语法 <code>${{ github.actor }}</code> 从 <code>github action</code> 的内置上下文中获取执行者的名称</li> <li data-list="bullet"><span class="ql-ui"></span><code>on</code> 是 <code>action</code> 的触发时机,<code>push</code> 表示在 commit 推送的时候会触发这个 <code>action</code> 。</li> <li data-list="bullet"><span class="ql-ui"></span><code>jobs</code> 表示要执行的任务,可以执行多个任务。</li> <li data-list="bullet"><span class="ql-ui"></span><code>runs-on</code> 表示要在什么环境下执行,示例是在 <code>ubuntu</code> 系统最新版环境下执行。</li> <li data-list="bullet"><span class="ql-ui"></span><code>steps</code> 表示要运行的步骤,<code>run</code> 表示直接执行 <code>shell</code> 命令,<code>uses</code> 表示使用其他人现成写好的 actions, 如 <code>actions/checkout@v3</code> 就是官方提供的 <code>action</code> ,用于把源代码检出到当前环境目录下。</li> </ol> <p><br></p> <p>最简单的来说,我们只需要编写配置告知 <code>github action</code> 我们要执行的<strong>时机</strong>、<strong>环境</strong>、<strong>命令</strong>。那么每次触发时机的时候 <code>github action</code> 就会自动第一个独立的容器帮我们运行我们指定的命令,来实现做一些检查工作。</p> <p><br></p> <h2><strong>将 CI 流程搬到线上</strong></h2> <p><br></p> <p>那么通过以上的例子,我们可以很快了解到这些流水线的产品的内核逻辑无非就是<strong>面向过程的指令排列</strong>。既然清楚了这一点我们就可以将我们上一节讲的,在本地跑的检查命令丢到线上去执行。</p> <p><br></p> <p>首先我们回忆一下我们在本地要执行的命令:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> eslint --fix </div> <div class="ql-code-block"> prettier --write </div> </div> <p><br></p> <p>如果翻译成 Github Action 的描述就是</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> jobs: </div> <div class="ql-code-block"> CI: </div> <div class="ql-code-block"> runs-on: ubuntu-latest </div> <div class="ql-code-block"> steps: </div> <div class="ql-code-block"> - run: eslint </div> <div class="ql-code-block"> - run: prettier --check ./ </div> </div> <p><br></p> <p>注意当我们在持续集成阶段,我们只需要去判断是否正确,而不是去帮助用户去修改,以保证检查的纯粹性。当然,因为不需要修改,指令则需要进行一些修改。不同的工具可能会有不同指令形式,你可以通过查阅官方文档或者在终端输入相关的<code>--help</code> 命令来查询。几乎所有的命令行程序都会拥有一个 <code>help</code> 指令,如可以运行 <code>prettier --help</code> 来打印出<code>prettier</code> 的所有操作。</p> <p><br></p> <p>我们将这个工作命名为 <code>CI</code> ,顺便通过 <code>runs-on</code> 指定一下执行环境镜像,对于 <code>node</code> 程序来说在除了 <code>window</code> 以外的任何镜像上都是差不多的,但是考虑到 <code>Linux</code> 的开放性,大部分的流水线应用都会对 <code>Linux</code> 环境有很好的支持,因此建议选择主流的几个 <code>Linux</code> 发行版即可。</p> <p><br></p> <p>然后我们还需要补充一些信息,比如给这条流水线命个名字,定义一下触发时机。另外在之前因为是对源代码进行操作,因此还需要拉一下代码。</p> <p><br></p> <p>完整的 action 配置文件应该是这样的:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> # .github/workflows/ci.yaml </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> name: CI </div> <div class="ql-code-block"> on: [push] </div> <div class="ql-code-block"> jobs: </div> <div class="ql-code-block"> CI: </div> <div class="ql-code-block"> runs-on: ubuntu-latest </div> <div class="ql-code-block"> steps: </div> <div class="ql-code-block"> - name: Check out repository code </div> <div class="ql-code-block"> uses: actions/checkout@v3 </div> <div class="ql-code-block"> - name: Install Packages </div> <div class="ql-code-block"> run: npm install </div> <div class="ql-code-block"> - name: Check Lint </div> <div class="ql-code-block"> run: npx eslint </div> <div class="ql-code-block"> - name: Check Code Format </div> <div class="ql-code-block"> run: npx prettier --check ./ </div> <div class="ql-code-block"> - name: Check Simple Build # 对项目直接编译做一次简单的打包操作,防止出现项目格式没有问题但是打包出现问题 </div> <div class="ql-code-block"> run: npm run build </div> </div> <p><br></p> <p>将这个配置文件添加到项目,然后推送代码到远程 Github 平台,就可以在 Action 标签中看到自动化检查运行的记录,在每次 commit 的记录中也可以看到表示通过的绿色对钩以及表示失败的红色叉号,如果是一个 pull request 请求,也会自动出现在检查清单中,帮助审阅者快速判断基本可用性。</p> <p><br></p> <p></p> <p><br></p> <p></p> <p><br></p> <p></p> <p><br></p> <blockquote> <div class="blockquote-item"> 以上为 facebook/react 项目的 ci 检查效果 </div> </blockquote> <p><br></p> <h2><strong>总结</strong></h2> <p><br></p> <p>本节我们学习了如何使用 <code>github action</code> 来帮助我们构建一个简单的远程流水线。通过一个中心节点(Github)来帮助我们做一个唯一的裁判,判断一次提交是否能够达到代码质量的下限。</p> <p><br></p> <p>希望大家能够了解到,不管是什么平台,其内核本质是完全一样的,无非就是顺序执行预设好的命令,然后根据输出来判断是正确的还是错误的,以达到帮助开发者的目的。</p> <p><br></p> <p>下一节我们会学习如何来编写单元测试,通过单元测试来保证代码逻辑正确性。</p> <p><br></p> <p>小思考: 你还见过哪些具有完备CICD流程的项目呢?</p> <p><br></p> </div> </body> </html>
【CICD】从代码规范工具开始
<html> <head></head> <body> <div class="content ql-editor"> <p>不积跬步无以至千里,不积小流无以成江海。我们从最简单也是最能影响我们实际工作效率的地方开始,而说到这个,能够第一时间想到的就是能够一直默默为我们付出的,也是团队协作中必备的工具: 代码规范工具了。</p> <p><br></p> <p>代码规范工具,一般意义上可以分为两种: 代码格式化工具与代码静态检查工具两种。</p> <p><br></p> <h2>代码格式化工具</h2> <p><br></p> <p>代码格式化工具,字面意思就是格式化代码的工具,每个人的编码习惯都有所不同,有的喜欢空格缩进有的Tab缩进,甚至空格缩进还能细分两个空格和四个空格。而对于像是 JavaScript 这样不严格的语言来说,行尾加不加分号也会划分成不同的流派。争论各个流派(编码风格)的好坏是没有意义的,而口头的约束也是难以达成统一,因此,代码格式化工具应运而生。而一般的代码编辑器(editor)都自带有代码格式化能力,有的甚至会绑定了保存自动进行格式化。</p> <p><br></p> <p>那么就是说我们不需要去关心代码格式化相关的问题了么?并不是的,特别是团队协作,或者开源协作上,大家用的代码编辑器(editor)可能不尽相同。有的同学喜欢用vscode,有的同学喜欢用webstorm,习惯传统的同学也会选择vim或者emacs。ide之间尚有偏好设定,那么谁来做一个决策到底用哪种风格呢?因此需要一个第三方的、环境无关、编辑器无关的"裁判"来做最终决策。</p> <p><br></p> <h3>Prettier</h3> <p><br></p> <p>目前前端业界比较流行的,经过多次淘汰最终获胜的解决方案是 Prettier(<a href="https://prettier.io/" target="_blank" style="color: inherit;">https://prettier.io/</a>), Prettier的使用也非常简单</p> <p><br></p> <h3>安装</h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install -D prettier <span class="ql-token hljs-comment"># 安装prettier到devDependencies</span> </div> </div> <p><br></p> <h3>配置</h3> <p><br></p> <p>在项目的根目录下创建一个 <code>.prettierrc.json</code> 文件,然后写入:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{}</span> </div> </div> <p><br></p> <p>是的,你甚至不需要写入任何额外的内容,因为prettier自带的会为你选择一套默认的也是流行的配置方案。</p> <p><br></p> <p>接下来我们可以进行一次简单的格式化了,当然在此之前如果我们需要排除一部分的文件不需要格式化的话我们可以自行建一个 <code>.prettierignore</code> 文件用于忽略格式化。prettier会自动继承 <code>.gitignore</code> 和 <code>.eslintignore</code> 且语法是保持一致的。因此大部分情况下你是不需要再额外的再为prettier进行额外的忽略的。</p> <p><br></p> <p>接下来可以使用命令行来帮助你使用prettier了</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npx prettier --write . </div> </div> <p><br></p> <p>当然,prettier提供了很多已经简化过了的配置,完整的配置信息可以看官方文档: <a href="https://prettier.io/docs/en/options.html" target="_blank" style="color: inherit;">https://prettier.io/docs/en/options.html</a></p> <p><br></p> <p>这边给出一个配置文件用于参考:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"printWidth":</span> <span class="ql-token hljs-number">80,</span> <span class="ql-token hljs-comment">// 推荐列宽80个字符</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"tabWidth":</span> <span class="ql-token hljs-number">2,</span> <span class="ql-token hljs-comment">// 缩进宽度2字符</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"useTabs":</span> <span class="ql-token hljs-keyword">false,</span> <span class="ql-token hljs-comment">// 不使用tab缩进</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"semi":</span> <span class="ql-token hljs-keyword">true,</span> <span class="ql-token hljs-comment">// 每行句末带分号</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"singleQuote":</span> <span class="ql-token hljs-keyword">true,</span> <span class="ql-token hljs-comment">// 对字符串使用单引号而不是双引号</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"trailingComma":</span> <span class="ql-token hljs-string">"es5",</span> <span class="ql-token hljs-comment">// 在换行数组/对象结束带分号,方便diff</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"bracketSpacing":</span> <span class="ql-token hljs-keyword">true,</span> <span class="ql-token hljs-comment">// 对象字面量之间增加空格</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"arrowParens":</span> <span class="ql-token hljs-string">"always",</span> <span class="ql-token hljs-comment">// 箭头函数总是带括号: 偏好(x) => x 而不是 x => x</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"bracketSameLine":</span> <span class="ql-token hljs-keyword">false</span> <span class="ql-token hljs-comment">// 换行的xml结尾的>符号放于下一行而不是同行</span> </div> <div class="ql-code-block"><span class="ql-token hljs-punctuation">}</span> </div> </div> <p><br></p> <h3>在线演示</h3> <p><br></p> <p>通过在线演示来快速验证也是一种比较好的方式,在某些场合下我们使用命令行工具会显得过于臃肿和繁琐,prettier提供了在线平台帮助我们进行快速预览: <a href="https://prettier.io/playground/" target="_blank" style="color: inherit;">https://prettier.io/playground/</a></p> <p><br></p> <h3>集成到editor中</h3> <p><br></p> <p>prettier已经支持了大部分常见的代码编辑器如:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>vscode(通过插件https://github.com/prettier/prettier-vscode)</li> <li data-list="bullet"><span class="ql-ui"></span>webstorm(内置)</li> <li data-list="bullet"><span class="ql-ui"></span>vim(<a href="https://github.com/prettier/vim-prettier" target="_blank" style="color: inherit;">https://github.com/prettier/vim-prettier</a>)</li> <li data-list="bullet"><span class="ql-ui"></span>...</li> </ol> <p><br></p> <p>简单配置后我们可以实现保存自动格式化,且因prettier本身的优化其执行效率还是很高的。</p> <p><br></p> <h3>editorconfig</h3> <p><br></p> <p>前面介绍了现在比较流行的代码格式化工具prettier,那么就不得不提到在另一个方向做出努力的工具 editorconfig 了。</p> <p><br></p> <p>相比他的同行在各种语言,各种规范上做出的努力,editorconfig做的事情更加纯粹也更加简单。他仅仅是统一了不同编辑器在配置上的区别,比如缩进是tab还是空格,行末到底是lf还是crlf,文件结尾是否保留空行之类非常"简单"的事情。</p> <p><br></p> <p>可以注意到的是,这些规则是语言无关、系统无关的,它仅仅是统一了不同编辑器在不同系统下的行为。简单而实用。</p> <p><br></p> <h2>代码静态检查工具</h2> <p><br></p> <p>代码静态检查工具,其实也包含了格式化工具,主要目的是对源代码进行静态分析,然后修复或者发现一些通过一些规则能够发现的问题。当我们涉及到更多团队协作的细节以后,该工具的收益才会慢慢显示出来。如果你的团队还在对一些细节写法产生一些分歧,那么最好的做法就是使用代码静态检查工具对代码进行处理。很多细节的分歧其实是两者皆可的,不论选用哪种方案都是无关紧要的。而其真正的问题是代码中可能会同时出现两种方案,而静态检查就确保了整个仓库都会使用统一的方案进行,从而避免不必要的规范差异。</p> <p><br></p> <p>纵观华夏上下五千年,秦始皇最伟大的地方就在于"书同文,车同轨",规范的统一是重中之重。</p> <p><br></p> <h3>eslint</h3> <p><br></p> <p>eslint就是目前web前端非常流行的一个代码静态检查工具,他包含了大部分你能想到和你不能想到的js代码规则,涉及到整个代码体系的方方面面。同时,插件化的设计让他能够引入来自更多其他开发者的规则,真正做到让开发者专注于自己的代码中。</p> <p><br></p> <p>想要快速集成也非常简单,eslint官方提供了一个快速集成的方式,只需要在你的项目根目录下执行以下命令即可:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm init @eslint/config </div> </div> <p><br></p> <p>接下来有一段交互式的安装引导,按照引导程序一步步进行下去即可</p> <p><br></p> <p></p> <p><br></p> <p>执行命令 <code>eslint ./src/*/.{ts,tsx}</code> 即可开始第一次的代码静态检查,默认会添加一些官方推荐的规则配置。</p> <p><br></p> <p>为了快捷使用也可以添加到<code>npm scripts</code> 中:</p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">//...</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"scripts":</span> <span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"lint":</span> <span class="ql-token hljs-string">"eslint ./src/**/*.{ts,tsx}",</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-punctuation">},</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">//...</span> </div> <div class="ql-code-block"><span class="ql-token hljs-punctuation">}</span> </div> </div> <p>这样我们就可以通过执行 <code>npm run lint</code> 来快速运行代码静态检查,一般来说我们会简单的称呼这个过程为<code>lint</code> 。这个操作在其他语言也是有的,如python的 <code>pylint</code> , go的 <code>golint</code> 等。</p> <p><br></p> <p>如果是一个维护比较久的项目的话可能会在第一次引入<code>eslint</code> 的时候会产生大量的报错。没有关系,这是一个正常的现象。耐心的将他们一个个修复,或者临时在规则中将其改为<code>warning</code> 留在后期修复都是可以的,具体要如何决策需要看具体情况来分析。</p> <p><br></p> <p>修改规则的方式很简单。只需要在生成的<code>.eslintrc</code> 文件的<code>rules</code>字段添加相关规则的变更即可。</p> <p><br></p> <p>首先找到报错的规则:</p> <p><br></p> <p></p> <p><br></p> <p>如图所示规则为最右侧灰字部分,这条规则是<code>react/prop-types</code> .</p> <p><br></p> <p>在<code>react</code> 的推荐用法是推荐使用 <code>prop-types</code> 库对输入参数进行校验的,但是如果使用的是 <code>Typescript</code> 对类型做静态校验的话那么这一步是可选的。</p> <p><br></p> <p>那么在这里我们修改 <code>.eslintrc</code> 文件将其改为 <code>off</code></p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">// ...</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"rules":</span> <span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"react/prop-types":</span> <span class="ql-token hljs-string">"off"</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-punctuation">},</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">// ...</span> </div> <div class="ql-code-block"><span class="ql-token hljs-punctuation">}</span> </div> </div> <p><br></p> <p>如果仅仅是想要临时绕过不想处理也可以使用 <code>warn</code> 来标记,这样<code>eslint</code> 还是为抛出警告但是不会输出错误。这在我们接下来的CI集成中非常重要。</p> <p><br></p> <h3>集成到editor中</h3> <p><br></p> <p>我们还可以使用编辑器的相关插件来在编辑器中直接引入<code>eslint</code> 插件对代码进行检查。</p> <p><br></p> <p>这里直接以<code>vscode</code> 为例,只需要在插件面板中搜索 <code>eslint</code> 即可安装。</p> <p><br></p> <p></p> <p><br></p> <h2>总结</h2> <p><br></p> <p>在本节内容,我们学会了如何使用代码格式化与代码静态检查工具,特别的了解了一下目前比较流行的<code>prettier</code> 和<code>eslint</code> 基本使用。</p> <p><br></p> <p>接下来我们就要将其集成到我们的工作流中,让其成为项目代码质量保证的一部分。</p> <p><br></p> <p>小思考: 在平时的协作中有遇到什么代码规范上的问题?又是如何解决的呢?</p> </div> </body> </html>
【CICD】Why-What-How
<html> <head></head> <body> <div class="content ql-editor"> <p>上一章我们讲了现代化的软件生命周期管理方法论: DevOps。当然,既然是方法论,那么只是一种指导思想。在软件工程中,我们作为程序员更多的需要讲的是落地。而自动化就是实现DevOps的重要手段,其命名则为CICD。</p> <p><br></p> <p>CICD,全称 <strong>Continuous Integration; Continuous Deployment</strong>,中文译名为持续集成与持续分发。这个过程连接了我们从代码编写完毕到交付测试环节的整个链路。如果能够将CICD做好的话,研发同学则完全无需管理这一步,而只需要专注于业务实现即可。</p> <p><br></p> <p>如果作为读者的你在面临着开发完毕以后还要花费大量时间用于部署,或者碍于不明确的团队规范导致整体效率低下,那么我建议你可以好好学习一下本课程,学好CICD能够显著提升整体研发效率。如果你没有遇见或者尚未遇见类似的问题,那么也同样也建议你了解一下CICD。学会CICD也是学会一种解决问题的思路。</p> <p><br></p> <p>想象一下,如果提交了一段代码,软件会进行基本的测试用于检查一些低级问题,并发布到相关的平台并通知相关的测试同学,同时输出一份报告告知开发者此时修改对现有的代码造成了什么样的影响,而这一切都是自动化完成的,这是多么美妙的一件事啊。</p> <p><br></p> <p></p> <p><br></p> <p>好了,那么既然我们确定了我们为什么要学习CICD,以及CICD能够做什么。那么,我们该如何设计一个CICD机制呢?</p> <p><br></p> <p>我们先不讨论应该用什么样的技术,也不先讨论应该去如何实现,以及有什么技术难点 —— 一开始就过于关注细节容易让我们沉入思维误区。我们从整体的、宏观的视角上来看待这个问题:即我们的实际需求。</p> <p><br></p> <p>那么我们的<strong>实际需求</strong>是什么呢?这个需求也是DevOps核心目标,即让开发者专注于业务而不是流程。那么为了实现这个目的,我们想一想,在我们写完业务代码以后我们需要做些什么?</p> <p><br></p> <p>研发自测,发布,交付给测试同学进行测试,再根据测试同学的反馈进行迭代,团队内部进行代码code review,最终代码上线让用户可以看到结果。</p> <p><br></p> <p>可能不同的团队实现的细节有所不同,但是大方向应当是一致的。那么我们就需要CICD帮我们简化这些步骤,实现通过工具、规则等固定的方式来帮我们处理掉一些可能是重复性的工作。</p> <p><br></p> <p>那我们更近一步设计一下我们想要的CICD工具链应该能帮我们做到什么:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>在研发自测阶段可以直接帮我们处理掉代码改动导致联动修改的其他部分——大部分研发同学只会关心改动到的部分,但不得不说程序是复杂的,一处修改可能被涉及到方方面面。所以我们需要编写单元测试用例来帮我们检查其他代码的正确性。以及每次都自动帮我们进行测试。</li> <li data-list="bullet"><span class="ql-ui"></span>发布时候尽量减少流程,越是复杂的系统发布的链路越是漫长。我所遇见过最复杂的发布系统发布一次版本可能需要半小时到一小时才能发布完毕。如果相关工具链可以完善,那么每次我都能省下半小时的时间,更何况并不会只发布一次。</li> <li data-list="bullet"><span class="ql-ui"></span>因为我们需要在发布完毕以后告知测试同学进行测试,那么带来的问题就是作为研发人员的我们必须关注发布的进度,然后在发布完毕后手动告知测试同学进行下一步。那么有没有可能工具可以自动帮我们完成这一步呢?甚至联动相关的项目管理应用可以帮我们直接推进到下一步,作为开发人员只需要关注可能出现的编译/发布错误就行了,而不需要关心代码提交后的结果 —— 开发人员的工作本就应当只需要关心代码的实现。</li> <li data-list="bullet"><span class="ql-ui"></span>在团队代码的code review中,除了业务逻辑以外,最常见到的就是各种格式/拼写/语法等问题。如果我们用代码格式工具帮我们处理掉这些问题,那么在code review中我们就可以更加关注我们应当关注的问题。</li> <li data-list="bullet"><span class="ql-ui"></span>最后,一切都准备就绪,就准备上线了。等等!你是不是打算加班到深夜然后等到夜深人静用户量最少的时候对系统进行升级?大部分的程序员都是这么想的。但是如果说我们可以通过自动化工具自动帮我们发版呢?设定定时任务,上下游自动发版。如果出现发布问题,代码自动回滚。如果出现逻辑问题,也只需要点一个按钮直接回滚。不要认为这是一件不可能的事情,因为我认识的一家海外的TOP互联网公司,他们的团队就是这么做的。</li> </ol> <p><br></p> <p>以上,我已经把CICD所能做的事情,整体的宏观架构已经展现给你看了。如果感到心动,向往这样自动化的生活,并开始隐隐对现在落后的手动作业开始感到厌恶的话,那么我们赶紧进入下一章节,我会带大家学会如何做到这样的流程。</p> <p><br></p> </div> </body> </html>
