前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2022年3月最新Eslint + Prettier + Husky + Stylelint + Jest + CI/CD 超详细前端单元测试&规范工程化工作流

2022年3月最新Eslint + Prettier + Husky + Stylelint + Jest + CI/CD 超详细前端单元测试&规范工程化工作流

作者头像
源心锁
发布2022-08-12 11:43:04
1.8K0
发布2022-08-12 11:43:04
举报
文章被收录于专栏:前端魔法指南前端魔法指南
2022年3月最新Eslint + Prettier + Husky + Stylelint + Jest + CI/CD 超详细前端单元测试&规范工程化工作流
2022年3月最新Eslint + Prettier + Husky + Stylelint + Jest + CI/CD 超详细前端单元测试&规范工程化工作流

1 前置准备

  • 一个正常运行的前端项目
  • 一个准备好的git仓库

2 规范

2.1 代码规范

2.1.1 eslint

eslint乃老生常谈,配置上也较为简单

代码语言:javascript
复制
pnpm i eslint --save-dev
pnpm init @eslint/config
image.png
image.png

基于上边的步骤,我们生成了基础配置;

image.png
image.png

由于我的示例项目使用Next.js框架构建,需要在extends中额外配置"next"。 同时个人建议配置react-hooks插件

2.1.2 prettier

prettier是格式化工具,我个人使用上更偏爱使用prettier做代码格式化,如果你在上一步选择了eslint格式化大可忽略

代码语言:javascript
复制
pnpm i prettier --save-dev

我们需要在根目录配置.prettierrc; 这是我配置的规范,以下指令可以快捷生成

代码语言:javascript
复制
echo \{\"semi\": true,\"tabWidth\": 2,\"trailingComma\": \"es5\",\"singleQuote\": false,\"arrowParens\": \"always\"\} > .prettierrc
image.png
image.png

同时建议更新eslint的配置,增加prettier解决冲突

代码语言:javascript
复制
pnpm i eslint-config-prettier --save-dev
image.png
image.png

2.1.3 stylelint

代码语言:javascript
复制
pnpm install --save-dev stylelint stylelint-config-standard

stylelint可以帮助我们检查以及格式化样式文件

代码语言:javascript
复制
{
  "extends": ["stylelint-config-standard"],
  "rules": {
    "indentation": 4,
    "no-descending-specificity": null
  }
}

由于项目启用了scss,需要额外配置

代码语言:javascript
复制
pnpm i -D postcss postcss-scss
image.png
image.png

2.2 git规范

git规范对于团队开发是非常有利的,在版本出现问题时可以清晰的定位;

2.2.0 husky的配置

做git规范,前置需要配置一下husky,后续的内容都是基于husky

代码语言:javascript
复制
pnpm i husky --save-dev
npm set-script postinstall "npx husky install"
npx husky install

这里有两个地方是可能存在问题的:

npm set-script postinstall "npx husky install": >> 为package.json文件添加postinstall的脚本,该钩子会在npm运行install命令之后运行

npx husky install: >> 该命令的意义是初始化husky,将 git hooks 钩子交由,husky执行,缺失这里即便配置好后边的命令也不会生效

image.png
image.png

同时补充一点:husky install命令必须在.git同目录下运行,如果你的package.json.git不在同一目录,这是官方的解决方案:

image.png
image.png

补一手官网链接「typicode.github.io/husky」

2.2.1 pre-commit

在代码commit前运行,通过钩子函数,可以判断提交的代码是否符合规范,我们可以在这里做强制格式化

pre-commit可以配合上边制定的eslint与prettier规则运行,我这里的期望是,对于git暂存区的内容做自动规范,所以这里需要用到lint-staged:

代码语言:javascript
复制
pnpm i lint-staged --save-dev
npx husky add .husky/pre-commit "npx lint-staged"

同时在根目录下创建.lintstagedrc,这是我的配置:

代码语言:javascript
复制
{
  "*.{js,jsx,ts,tsx}": ["npx prettier --write", "npx eslint --fix"],
  "*.{css,less,scss}": ["npx prettier --write", "npx stylelint --fix"],
  "*.{json,md}": ["npx prettier --write"]
}

image.png
image.png

这样一来,在我们commit之前,代码会自动对暂存区指定文件进行格式化

2.2.2 commit-msg

在pre-commit之后运行,会检查commit的内容,做commit规范

代码语言:javascript
复制
pnpm i commitlint --save-dev
pnpm i @commitlint/config-conventional --save-dev
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
  • @commitlint/config-conventionalAnglar的提交规范
image.png
image.png

同时在根目录新建.commitlintrc.js

代码语言:javascript
复制
module.exports = {extends: ["@commitlint/config-conventional"]};

2.2.3 commit助手

commit助手可以帮助我们遵循commit-msg

commit助手这里推荐

  • commitizen
  • cz-conventional-changelog
  • commitlint-config-cz
  • cz-customizable 这些包,但是具体的使用可以自行探索,我这里是自己写的,在后边可以看到。

2.2.4 pre-push

pre-push可以在代码push之前运行一些脚本,目前的实践就是在push行为之前做本地编包、测试

代码语言:javascript
复制
npx husky add .husky/pre-push "npm run build && npm test"

3 单元测试「可选」

单元测试中最出名的当属Jest 我这里使用的则是JestReactTestingLibrary

3.1 Jest && ReactTestingLibrary

3.1.1 初始化与安装

项目中使用了ts,需要为Jest额外准备babel和typescript环境包

代码语言:javascript
复制
pnpm i jest -D
pnpm i -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript 
pnpm i -D @babel/preset-react react-test-renderer @types/react-test-renderer identity-obj-proxy
pnpm i ts-jest @types/jest -D

接着生成基本配置文件进行初始化

代码语言:javascript
复制
npx ts-jest config:init // ts版本
npx jest --init // js版本

npm set-script test "npx jest"

配置jest.config.js文件:

代码语言:javascript
复制
module.exports = {
  collectCoverageFrom: [
    "**/*.{js,jsx,ts,tsx}",
    "!**/*.d.ts",
    "!**/node_modules/**",
  ],
  moduleNameMapper: {
    "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
    "^.+\\.(css|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",
    "^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$": `<rootDir>/__mocks__/fileMock.js`,
    "^@/components/(.*)$": "<rootDir>/components/$1",
  },
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/.next/"],
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],
  },
  transformIgnorePatterns: [
    "/node_modules/",
    "^.+\\.module\\.(css|sass|scss)$",
  ],
};

当然如果使用Next框架,这样写就行:

代码语言:javascript
复制
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})
const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
}

module.exports = createJestConfig(customJestConfig)

接着在根目录创建jest.setup.js,内容可以暂时为空

编写第一个React测试用例 with 「ReactTestingLibrary」

安装依赖包

代码语言:javascript
复制
pnpm i -D @testing-library/jest-dom @testing-library/react 

jest.setup.js写入全局配置

代码语言:javascript
复制
import '@testing-library/jest-dom';

写第一个测试用例:

代码语言:javascript
复制
// home.test.tsx
import Home from "../pages/index";
import React from 'react'
import { render, screen } from '@testing-library/react'

it('renders homepage HelloWorld', () => {
  render(<Home/>)
  const helloworld = screen.getByRole('region', {
    name: /helloworld/i,
  })
  expect(helloworld).toBeInTheDocument()
})

代码语言:javascript
复制

// index.tsx
import type { NextPage } from "next";
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.scss";

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create 1 Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <section className={styles.title} aria-label="helloworld">HelloWorld</section>
        <span className={styles.logo}>
          <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
        </span>
      </main>
    </div>
  );
};

export default Home;

测试

image.png
image.png

同时在此补上官网链接

4 持续集成/持续部署CI/CD

目前已知CI/CD一般要用到Docker/k8s Jenkins,通过git action在git更新的时候向服务器做更新操作

这真做起来就是抢运维饭碗了啊喂...

嗯...图方便,并且由于前端这边只有静态界面,我这里没有使用服务器。而是通过腾讯静态托管(类似CDN)完成一键部署测试环境。

注意这样是有缺陷的,包括但不限于缺少回滚机制、在本地编包的风险

可能更多人的诉求是当代码合并到某个分支后,机器能自动帮我执行完打包和部署这两个步骤,如果是这样后边不用看了哈...周末要结束我要歇歇了有机会额外出,不是一篇文章能搞定的

4.1 前置准备

先留一个官网链接「console.cloud.tencent.com/tcb/hosting 」 正常注册一个云开发环境就行,可以选择「按量付费」再买资源包,一般来讲日花费不到1元。

注册完毕后可以拿到云开发的环境ID,记下来

image.png
image.png

接着我们需要开通「新建云开发环境」-「静态页面托管」

同时全局安装腾讯云提供的cli,并登陆

代码语言:javascript
复制
npm i @cloudbase/cli -g --force
tcb login

登陆后做一下开发环境验证:

代码语言:javascript
复制
tcb hosting detail -e {{你的环境ID}}
image.png
image.png

确认已上线

4.2 自定义部署脚本

为了便于使用,我们写一个自定义脚本

  • utils.js
代码语言:javascript
复制
const { blue } = require("chalk");
const { exec } = require("child_process");
const sys = (command, ...rest) =>
  new Promise((resolve, reject) => {
    exec(command, (err, stdout) => {
      if (err) {
        reject(err);
        return;
      }
      resolve([stdout, ...rest]);
    });
  });
  
module.exports = {
  blue,
  sys,
};
  • publish.js
代码语言:javascript
复制
const { sys, blue } = require("../resources/utils");
const inquirer = require("inquirer");
const ora = require("ora");

const publishCli = (envID) => [
  `tcb hosting deploy ./out ./livestea -e ${envID}`,
];

module.exports = async () => {
  const spinner = ora("代码发布中ing...");
  inquirer
    .prompt([
      {
        type: "confirm",
        name: "build",
        message: "是否先进行静态遍包(默认否)",
        default: false,
      },
      {
        type: "list",
        name: "value",
        choices: [
          {
            name: "测试环境",
            value: {
              envID: "xxx",
              url: "xxx",
            },
          },
          new inquirer.Separator("---无授权请不要发布正式环境---"),
          {
            name: "正式环境",
            value: {
              envID: "xxx",
              url: "xxx",
            },
          },
        ],
        message: "选择发布环境:",
      },
      {
        type: "confirm",
        name: "confirm",
        message: "确认发布?",
      },
    ])
    
    .then((answers) => {
      if (!answers.build) {
        return answers;
      }
      return sys("npm run export").then(() => answers);
    })
    .then((answers) => {
      const { confirm, value } = answers;
      if (!confirm) {
        return;
      }
      const { envID, url } = value;
      const [command] = publishCli(envID);
      console.log(command);
      spinner.start();
      return sys(command, url);
    })
    .then(([status, url]) => {
      spinner.stop();
      console.log(status);
      spinner.text = "代码发布成功";
      spinner.succeed();
      return url;
    })
    .then((url) => {
      console.log(blue(`${url}?time=${Date.now()}`));
    });
};
  • main.js
代码语言:javascript
复制
const [command, ...argvs] = process.argv.splice(2);

switch (command) {
  case "cz":
    require(`./scripts/commitizen`)(...argvs);
    break;
  default:
    require(`./scripts/${command}`)(...argvs);
    break;
}

image.png
image.png

这样我们就可以通过脚本命令一键部署,记得部署之前要确认是否在本地编包哦~

代码语言:javascript
复制
npm run pub
image.png
image.png
image.png
image.png

附件

附件1 cli目录结构

image.png
image.png

附件2 commit助手自定义

  • ora用来加载loading效果
image.png
image.png
  • inquirer用来做命令行交互
image.png
image.png
  • chalk用来给打印信息上色
image.png
image.png
代码语言:javascript
复制
#! /usr/bin/env node
const inquirer = require("inquirer");
const ora = require("ora");
const precommit = require("./precommit");
const { yellow } = require("chalk");
const { errorCodeFunc, errorCode, getError } = require("../resources/error");
const { sys } = require("../resources/utils");
const { CUSTOM_ERR_ERROR, CUSTOM_ERR_INFO, CUSTOM_ERR_IGNORED } = errorCode;

const commitizen = {
  types: [
    { value: "feat", name: "feat:     新功能" },
    { value: "fix", name: "fix:      修复" },
    { value: "docs", name: "docs:     文档变更" },
    { value: "style", name: "style:    代码格式(不影响代码运行的变动)" },
    {
      value: "refactor",
      name: "refactor: 重构(既不是增加feature,也不是修复bug)",
    },
    { value: "perf", name: "perf:     性能优化" },
    { value: "test", name: "test:     增加测试" },
    { value: "chore", name: "chore:    构建过程或辅助工具的变动" },
    { value: "revert", name: "revert:   回退" },
    { value: "build", name: "build:    打包" },
    { value: "ci", name: "ci:       持续集成修改" },
  ],
  messages: {
    type: "请选择提交类型:",
    scope: "请输入修改范围(可选):",
    subject: "请简要描述提交(必填):",
    body: "请输入详细描述(可选):",
    footer: "请输入要关闭的issue(可选):",
    confirmCommit: "确认使用以上信息提交?",
  },
};
const { types, messages } = commitizen;

module.exports = async () => {
  let commit = null;
  const spinner = ora("代码提交中ing...");

  precommit()
    .then((e) => {
      if (!e.code) {
        throw { code: CUSTOM_ERR_IGNORED };
      }
      return inquirer.prompt([
        {
          type: "list",
          name: "type",
          message: messages.type,
          choices: types,
          loop: false,
        },
        {
          type: "input",
          name: "subject",
          message: messages.subject,
        },
        {
          type: "input",
          name: "scope",
          message: messages.scope,
        },
        {
          type: "body",
          name: "body",
          message: messages.body,
        },
        {
          type: "footer",
          name: "footer",
          message: messages.footer,
        },
      ]);
    })
    .then((answers) => {
      const { subject } = answers;
      if (!subject) {
        throw {
          code: CUSTOM_ERR_ERROR,
          msg: "commit信息中必须包含基本的【描述提交】",
        };
      }
      return answers;
    })
    .then(({ type, scope, subject, body, footer }) => {
      const _header = `${type}${scope ? `(${scope})` : ""}: ${subject};`;

      const _body = `${body ? "\n" + body : body}`;

      const _footer = `${footer ? "\n" + footer : footer}`;
      return `${_header}${_body}${_footer}`.replaceAll("`", "\\`");
    })
    .then((str) => {
      console.log(yellow("------------------------"));
      console.log(str.replaceAll("\\`", "`"));
      commit = str;
      console.log(yellow("------------------------"));
      return inquirer.prompt([
        {
          type: "confirm",
          name: "confirm",
          message: messages.confirmCommit,
        },
      ]);
    })
    .then(({ confirm }) => {
      if (!confirm) {
        throw {
          code: CUSTOM_ERR_INFO,
          msg: "取消提交",
        };
      }
      return;
    })
    .then(() => {
      const command = `git commit -m "${commit}"`;
      console.log(`\n${command}\n`);
      spinner.start();
      return sys(command);
    })
    .then(([res]) => {
      spinner.stop();
      console.log(res);
      spinner.text = "代码提交成功";
      spinner.succeed();
    })
    .catch((e) => {
      spinner.stop();
      errorCodeFunc(e.code ?? getError(e).code, e);
      spinner.text = "代码提交失败";
      spinner.start();
      spinner.fail();
      return { code: 0, errMsg: e };
    });
};

总结

写这篇文章一是汇总部分近期学习和了解到的知识,二是希望能完备一下自己的文章库

~~ 🙅 不可能是防止自己有一天忘了

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-03-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 前置准备
  • 2 规范
    • 2.1 代码规范
      • 2.1.1 eslint
      • 2.1.2 prettier
      • 2.1.3 stylelint
    • 2.2 git规范
      • 2.2.0 husky的配置
      • 2.2.1 pre-commit
      • 2.2.2 commit-msg
      • 2.2.3 commit助手
      • 2.2.4 pre-push
  • 3 单元测试「可选」
    • 3.1 Jest && ReactTestingLibrary
      • 3.1.1 初始化与安装
      • 编写第一个React测试用例 with 「ReactTestingLibrary」
  • 4 持续集成/持续部署CI/CD
    • 4.1 前置准备
      • 4.2 自定义部署脚本
      • 附件
        • 附件1 cli目录结构
          • 附件2 commit助手自定义
          • 总结
          相关产品与服务
          云开发 CloudBase
          云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档