从零搭建属于你自己的前端规范+自动化部署
今天想要分享的是关于前端项目规范以及部署上线的整体流程,主要包含:
- 🌈 eslint、prettier
- 📦 husky
- 🛡 git-cz
- ⚙️ 基于 nginx 部署的docker
- 🌍 在 pull request 时触发 CI/CD
- 🎨 基于 vitest 的功能测试(还在完善中…)
重点讲解的是后续的自动化部署,前面的项目规范简单的走一遍这个流程
这个是 demo 的仓库:github.com/Jiaynn/auto…
接下来咱们就直奔主题吧
初始化
首先,我们肯定得初始化一个项目,这里我是基于 Vite 搭建的关于 React+TypeScript 的项目
pnpm create vite@latest
这样一个基本的架子就搭好了
代码规范
1.EditorConfig
.editorconfig
是跨编辑器维护一致编码风格的配置文件,有的编辑器会默认集成读取该配置文件的功能。
这就解决了在团队协作时,不同的开发人员使用了不同的编辑器造成的风格不统一的问题
注意在 vscode 需要安装扩展 EditorConfig For vs Code
安装完此扩展后,在 vscode 中使用快捷键 ctrl+shift+p
打开命令台,输入 Generate .editorcofig
即可快速生成 .editorconfig
文件,如果命令没生效,直接手动创建.editorconfig
文件
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space //缩进风格,可选配置有 `tab` 和 `space`
indent_size = 2 //缩进大小,可设定为 `1-8` 的数字,比如设定为 `2` ,那就是缩进 `2` 个空格。
end_of_line = lf //换行符,可选配置有 `lf` ,`cr` ,`crlf`
charset = utf-8 //编码格式,通常都是选 `utf-8`
trim_trailing_whitespace = false //去除多余的空格
insert_final_newline = false //在尾部插入一行
扩展装完,配置配完,编辑器就会去首先读取这个配置文件,对缩进风格、缩进大小在换行时直接按照配置的来,在你 ctrl+s
保存时,就会按照里面的规则进行代码格式化。
2.Prettier
如果说 EditorConfig
帮你统一编辑器风格,那 Prettier
就是帮你统一项目风格的。
下面是我常用的配置,在根目录下查创建.prettierrc
{
"arrowParens": "always", //箭头函数的参数无论有几个,都要括号包裹
"bracketSameLine": false,
"bracketSpacing": true, //在对象中的括号之间打印空格`{x: 1}` 格式化为 `{ x: 1 }`
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false, //jsx 语法是否单引号
"printWidth": 80, //单行代码最长字符长度,超过之后会自动格式化换行。
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true, //分号是否添加
"singleAttributePerLine": false,
"singleQuote": true, //是否单引号
"tabWidth": 2,
"trailingComma": "none", //对象的最后一个属性末尾是否添加 `,`
"useTabs": true,
"vueIndentScriptAndStyle": false,
"endOfLine": "lf" //与 `.editorconfig` 保持一致设置。
}
.editorconfig
配置文件中某些配置项是会和 Prettier
重合的,例如 指定缩进大小 两者都可以配置。那么两者有什么区别呢?
EditorConfig
的配置项都是一些不涉及具体语法的,比如 缩进大小、文移除多余空格等。
而 Prettier
是一个格式化工具,要根据具体语法格式化,对于不同的语法用单引号还是双引号,加不加分号,哪里换行等,当然,肯定也有缩进大小。
3.Eslint
eslint是一个老生常谈的话题了,这里不过多展示,只给大家看一下我常用的eslint配置,在根目录下创建.eslintrc
;
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
"react-hooks",
"simple-import-sort",
"import",
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{},
{
"usePrettierrc": false
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-var-requires": 0,
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"react/prop-types": "off",
"simple-import-sort/exports": "error",
"simple-import-sort/imports": [
"error",
{
"groups": [
[
"^\\u0000"
],
[
"^react$",
"^@?\\w"
],
[
"^[^.]"
],
[
// ../whatever/
"^\\.\\./(?=.*/)",
// ../
"^\\.\\./",
// ./whatever/
"^\\./(?=.*/)",
// Anything that starts with a dot
"^\\.",
// .html are not side effect imports
"^.+\\.html$"
],
[
"^(./|../).*.s?css$"
]
]
}
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
在这里需要注意的是eslint
和prettier
同时配置后,可能会产生冲突。我们需要更新一下eslint的配置,解决冲突
pnpm add eslint-config-prettier --save-dev
Git 规范
git 规范在团队协作时,也是一个非常重要的点,我们通过 git 规范,在版本出现问题时可以清晰的定位
git-cz
首先,我使用了 git-cz
这个库来进行规范化提交
pnpm add --save-dev git-cz
安装完成后,在 package.json 配置提交命令
随后在需要提交时,执行
pnpm start
husky + Git hooks 配置提交校验
在前面的配置中,我们已经实现使用 git cz
调出规范选项,进行规范的 message 的编辑;
但是如果我们忘记使用 git cz
, 直接使用了 git commit -m "commit message"
, message 信息依然会被提交上去,项目中会出现不规范的提交 message
因此我们需要 husky + commit-msg + commitlint 校验我们的提交信息是否规范。
安装配置 commitlint
- 安装依赖
pnpm add --save-dev @commitlint/config-conventional @commitlint/cli
- 创建
commitlint.config.js
文件
module.exports = {
extends: ["@commitlint/config-conventional"],
// 定义规则类型
rules: {
// type 类型定义,表示 git 提交的 type 必须在以下类型范围内
"type-enum": [
2,
"always",
[
"feat", // 新功能
"fix", // 修复
"docs", // 文档变更
"style", // 代码格式(不影响代码运行的变动)
"refactor", // 重构(既不是增加feature),也不是修复bug
"pref", // 性能优化
"test", // 增加测试
"chore", // 构建过程或辅助工具的变动
"revert", // 回退
"build", // 打包
],
],
// subject 大小写不做校验
"subject-case": [0],
},
};
安装配置husky
- 安装依赖
pnpm add husky --save-dev
- 启动 hooks, 生成 .husky 文件夹
npx husky install
- 在 package.json 中生成 prepare 指令
- 添加 commitlint 的 hook 到 husky 中,
commit-msg
时进行校验
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
添加完成后:
- 此时,不符合规范的 commit 将不会被允许提交
pre-commit 检验当前代码是否有 ESLint 错误
刚才我们配置了在提交时如果提交的信息不符合规范,是不允许被提交的但其实在日常开发中,我们希望可以在提交代码时帮我们进行 Eslint
检查
- 添加 commit 时的 hook,
pre-commit
时运行 npx lint-staged
npx husky add .husky/pre-commit "npx lint-staged"
这个 npx lint-staged
是什么呢?
lint-staged
可以让你当前的代码检查只检查本次修改更新的代码,并在出现错误的时候,自动修复并推送
修改 package.json
配置,在提交时帮我们 prettier 和 eslint
如上配置,每次在你本地 commit 之前,校验你所提的内容是否符合你本地配置的 eslint 规则
- 符合规则,提交成功
- 不符合规则,他会自动执行
eslint --cache --fix
尝试帮你自动修复,修复成功,则会自动帮你把修复好的代码提交;修复失败,提示你错误,让你修复好才可以提交代码;
Vitest测试
这个时候,你就可以编写测试代码了,然后在后续 CI/CD 时可以进行单元测试,由于这方面自己还不太熟练,这里就不过多展开了,有兴趣的可以试试
持续集成/持续部署 CI/CD
最终的效果是基于Github Actions在合并到主分支时,执行打包操作,以及部署到docker,最后在自己的服务器上实时更新
如果你想实现这样的效果就接着往下看吧
配置github Actions
配置CI Workflow
在项目根目录里的.github/workflows
文件夹上新建ci.yml
,代码如下所示:
name: CI
# Event设置为main分支的pull request事件,
# 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧
on:
pull_request:
branches: master
jobs:
# 只需要定义一个job并命名为CI
CI:
runs-on: ubuntu-latest
steps:
# 拉取项目代码
# 此处 actions/checkout 操作是从仓库拉取代码到Runner里的操作
- name: Checkout repository
uses: actions/checkout@v2
# 给当前环境下载node
# actions/setup-node@v3 操作来安装指定版本的 Node.js,此处指定安装的版本为v16
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "16.x"
# 检查缓存
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
# 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存
- name: Cache
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读
id: cache-dependencies
uses: actions/cache@v3
with:
# 缓存文件目录的路径
path: |
**/node_modules
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
# 安装依赖
- name: Installing Dependencies
# 如果缓存标志位没命中,则执行该step。否则就跳过该step
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: yarn install
# 运行代码扫描
- name: Running Lint
# 通过前面章节定义的命令行执行代码扫描
run: yarn lint
- name: build project
run: yarn build
这时,我们在合并到主分支时会执行这个 yml 文件,然后就会出现下面的样子,说明你编写的流水线正在执行了
现在我们只是在每次合并的时候进行了打包,但我们没有进行持续的部署。
我想要的效果是我只要合并在了主分支上,我的服务器上的内容能够自动更新。
我采取了通过 docker 构建镜像,发布到 Docker Hub 上,最后我通过远程登录服务器,执行部署脚本,从而实时更新。
Docker
首先我们先安装一下 docker
brew cask install docker
查看版本
docker -v
拉取 Nginx 镜像
首先打开你的 Docker ,默认会启动。
控制台拉取 Nginx 镜像:
docker pull nginx
1.在根目录下创建nginx.conf
user nginx;
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;
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /etc/nginx/html;
index index.html index.htm;
}
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
- 在根目录下创建
Dockerfile
# 设置基础镜像
FROM daocloud.io/library/nginx:1.9.1
# 定义作者
MAINTAINER jiaynn
# 添加时区环境变量,亚洲,上海
ENV TimeZone=Asia/Shanghai
# 将dist文件中的内容复制到 /etc/nginx/html/ 这个目录下面
COPY dist/ /etc/nginx/html/
# 将配置文件中的内容复制到 /etc/nginx 这个目录下面(增加自己的代理及一些配置)
RUN rm -rf /etc/nginx/nginx.conf
# 用本地的nginx配置文件覆盖镜像的Nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
- 配置 CD 流水线 直接把它补充到
.github/workflows
文件夹上的ci.yml
中
⚠️注意,在编写CD Workflow前,我们要准备以下东西:
- 内置
nginx
的服务器一台:用于部署制品 - 服务器的账号和密码
- Docker Hub 的账号和密码
- Docker Hub 的远程仓库
把步骤 2 和步骤 3 及其他关于机器的信息都放在对应仓库的Secret里,对应其中的账号密码,你可以提供密钥对,通过 ssh 免密登录进行部署,这里我为了方便,直接使用账号密码登录
具体流程:
与此同时,我还配置了一些变量在上面
详细的解释写在了注释里面
- name: 打包镜像, 上传 Docker Hub
run: |
# 登录docker, secrets.DOCKER_USERNMAE 就是我们在github上配置的docker的账户和密码
docker login -u ${{secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
# 打包镜像 -t参数给镜像命名
# .是基于当前目录的 Dockerfile 来构建镜像
docker build --platform linux/amd64 -t ${{ vars.USER_NAME }}/${{ vars.IMAGE_NAME }}:latest .
# 推送到我们的 docker 镜像仓库
docker push ${{ secrets.DOCKER_REPOSITORY }}
- name: 登录服务器, 执行脚本
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.REMOTE_HOST }}
username: root
password: ${{ secrets.REMOTE_PASSWORD }}
# 执行脚本
script: |
# 部署脚本 后面的vars是传递给脚本的参数
deploy.sh ${{ vars.USER_NAME }} ${{ vars.IMAGE_NAME }} ${{ vars.PORT }} ${{ vars.CONTAINS_PORT }}
deploy.sh
需要将这个文件写在你的服务器上,因为是你的流水线登录到你的服务器上后执行的脚本 远程登录服务器,创建 script 文件夹,以及创建 deploy.sh 文件
# 这里的$1、$2对应上面传递过来的参数
user_name=$1
image_name=$2
PORT=$3
CONTAINS_PORT=$4
# 如果传入的参数有一个为空,我们就提示他输入参数,然后退出
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" == ""]; then
echo "请输入参数"
exit
fi
# 删除容器,就是删除旧的容器
# docker ps -a 获取所有的容器
# | grep ${image_name} 得到这个容器 awk '${print $1}' 根据空格分割,输出第一项
containerId=`docker ps -a | grep ${image_name} | awk '{print $1}'`
if [ "$containerId" != "" ] ; then
# 停止运行
docker stop $containerId
# 删除容器
docker rm $containerId
echo "Delete Container Success"
fi
# 删除镜像
# 获取所有的镜像,得到我们自己构建的镜像的id
imageId=`docker images | grep ${user_name}/${image_name} | awk '{print $3}'`
if [ "$imageId" != "" ] ; then
# 删除镜像
docker rmi -f $imageId
echo "Delete Image Success"
fi
# 登录docker
docker login -u lijiayan -p 你在docker hub上获取的密钥
# 拉取docker上新的镜像
docker pull ${user_name}/${image_name}:latest
# 运行最新的镜像
# -d 设置容器在后台运行
# -p 表示端口映射,把本机的 92 端口映射到 container 的 80 端口(这样外网就能通过本机的 92 端口访问了
# 如果服务器重启后,我们需要重新启动docker
# 执行 systemctl restart docker 重新启动docker
# 但docker启动了,里面的容器没有启动,所以我们添加--restart=always ,docker启动后,容器也可以启动
# dokcer ps -a 查看所有的容器
docker run -d -p $3:$4 --name $image_name --restart=always ${user_name}/${image_name}:latest
echo "Start Container Successs"
echo "$image_name"
这是正在跑我们的流水线 这样最新的部署就成功啦