灏天阁

从零搭建属于你自己的前端规范+自动化部署

· Yin灏

从零搭建属于你自己的前端规范+自动化部署

今天想要分享的是关于前端项目规范以及部署上线的整体流程,主要包含:

  • 🌈 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"
    }
  }
}

在这里需要注意的是eslintprettier同时配置后,可能会产生冲突。我们需要更新一下eslint的配置,解决冲突

pnpm add eslint-config-prettier --save-dev

image.png

Git 规范

git 规范在团队协作时,也是一个非常重要的点,我们通过 git 规范,在版本出现问题时可以清晰的定位

git-cz

首先,我使用了 git-cz 这个库来进行规范化提交

pnpm add --save-dev git-cz

安装完成后,在 package.json 配置提交命令

image.png

随后在需要提交时,执行

pnpm start

image.png

husky + Git hooks 配置提交校验

在前面的配置中,我们已经实现使用 git cz 调出规范选项,进行规范的 message 的编辑;

但是如果我们忘记使用 git cz, 直接使用了 git commit -m "commit message", message 信息依然会被提交上去,项目中会出现不规范的提交 message

因此我们需要 husky + commit-msg + commitlint 校验我们的提交信息是否规范。

安装配置 commitlint

  1. 安装依赖
pnpm add --save-dev @commitlint/config-conventional @commitlint/cli
  1. 创建 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

  1. 安装依赖 
pnpm add husky --save-dev
  1. 启动 hooks, 生成 .husky 文件夹 
npx husky install
  1. 在 package.json 中生成 prepare 指令 

image.png

  1. 添加 commitlint 的 hook 到 husky 中,commit-msg 时进行校验

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'

添加完成后:

image.png

  1. 此时,不符合规范的 commit 将不会被允许提交

image.png

pre-commit 检验当前代码是否有 ESLint 错误

刚才我们配置了在提交时如果提交的信息不符合规范,是不允许被提交的但其实在日常开发中,我们希望可以在提交代码时帮我们进行 Eslint 检查

  1. 添加 commit 时的 hook,pre-commit 时运行 npx lint-staged

npx husky add .husky/pre-commit "npx lint-staged"

image.png

这个 npx lint-staged 是什么呢?

lint-staged 可以让你当前的代码检查只检查本次修改更新的代码,并在出现错误的时候,自动修复并推送

修改 package.json 配置,在提交时帮我们 prettier 和 eslint

image.png

如上配置,每次在你本地 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 文件,然后就会出现下面的样子,说明你编写的流水线正在执行了

image.png

现在我们只是在每次合并的时候进行了打包,但我们没有进行持续的部署。

我想要的效果是我只要合并在了主分支上,我的服务器上的内容能够自动更新。

我采取了通过 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;
		}
		
	}
}
  1. 在根目录下创建 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
  1. 配置 CD 流水线 直接把它补充到 .github/workflows 文件夹上的 ci.yml

⚠️注意,在编写CD Workflow前,我们要准备以下东西:

  1. 内置 nginx 的服务器一台:用于部署制品
  2. 服务器的账号和密码
  3. Docker Hub 的账号和密码
  4. Docker Hub 的远程仓库

把步骤 2 和步骤 3 及其他关于机器的信息都放在对应仓库的Secret里,对应其中的账号密码,你可以提供密钥对,通过 ssh 免密登录进行部署,这里我为了方便,直接使用账号密码登录

具体流程:

image.png

image.png

与此同时,我还配置了一些变量在上面

image.png

详细的解释写在了注释里面

- 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 文件

image.png

# 这里的$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"

这是正在跑我们的流水线 image.png 这样最新的部署就成功啦

image.png

- Book Lists -