1 前置准备
2 规范
2.1 代码规范
2.1.1 eslint
eslint乃老生常谈,配置上也较为简单
pnpm i eslint --save-dev
pnpm init @eslint/config
基于上边的步骤,我们生成了基础配置;
由于我的示例项目使用Next.js框架构建,需要在extends 中额外配置"next"。 同时个人建议配置react-hooks 插件
2.1.2 prettier
prettier是格式化工具,我个人使用上更偏爱使用prettier 做代码格式化,如果你在上一步选择了eslint格式化大可忽略
pnpm i prettier --save-dev
我们需要在根目录配置.prettierrc ; 这是我配置的规范,以下指令可以快捷生成
echo \{\"semi\": true,\"tabWidth\": 2,\"trailingComma\": \"es5\",\"singleQuote\": false,\"arrowParens\": \"always\"\} > .prettierrc
同时建议更新eslint 的配置,增加prettier 解决冲突
pnpm i eslint-config-prettier --save-dev
2.1.3 stylelint
pnpm install --save-dev stylelint stylelint-config-standard
stylelint可以帮助我们检查以及格式化样式文件
{
"extends": ["stylelint-config-standard"],
"rules": {
"indentation": 4,
"no-descending-specificity": null
}
}
由于项目启用了scss,需要额外配置
pnpm i -D postcss postcss-scss
2.2 git规范
git规范对于团队开发是非常有利的,在版本出现问题时可以清晰的定位;
2.2.0 husky的配置
做git规范,前置需要配置一下husky,后续的内容都是基于husky
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执行,缺失这里即便配置好后边的命令也不会生效
同时补充一点:husky install 命令必须在.git 同目录下运行,如果你的package.json 和.git 不在同一目录,这是官方的解决方案:
补一手官网链接「https://typicode.github.io/husky」
2.2.1 pre-commit
在代码commit前运行,通过钩子函数,可以判断提交的代码是否符合规范,我们可以在这里做强制格式化
pre-commit可以配合上边制定的eslint与prettier规则运行,我这里的期望是,对于git暂存区的内容做自动规范,所以这里需要用到lint-staged:
pnpm i lint-staged --save-dev
npx husky add .husky/pre-commit "npx lint-staged"
同时在根目录下创建.lintstagedrc ,这是我的配置:
{
"*.{js,jsx,ts,tsx}": ["npx prettier --write", "npx eslint --fix"],
"*.{css,less,scss}": ["npx prettier --write", "npx stylelint --fix"],
"*.{json,md}": ["npx prettier --write"]
}
这样一来,在我们commit之前,代码会自动对暂存区指定文件进行格式化
2.2.2 commit-msg
在pre-commit之后运行,会检查commit的内容,做commit规范
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-conventional 是Anglar的提交规范
同时在根目录新建.commitlintrc.js
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行为之前做本地编包、测试
npx husky add .husky/pre-push "npm run build && npm test"
3 单元测试「可选」
单元测试中最出名的当属Jest 我这里使用的则是Jest和ReactTestingLibrary
3.1 Jest && ReactTestingLibrary
3.1.1 初始化与安装
项目中使用了ts,需要为Jest额外准备babel和typescript环境包
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
接着生成基本配置文件进行初始化
npx ts-jest config:init // ts版本
npx jest --init // js版本
npm set-script test "npx jest"
配置jest.config.js 文件:
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 框架,这样写就行:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
}
module.exports = createJestConfig(customJestConfig)
接着在根目录创建jest.setup.js ,内容可以暂时为空
3.1.2 编写第一个React测试用例 with 「ReactTestingLibrary」
安装依赖包
pnpm i -D @testing-library/jest-dom @testing-library/react
在jest.setup.js 写入全局配置
import '@testing-library/jest-dom';
写第一个测试用例:
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()
})
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;
测试
同时在此补上官网链接
- 「https://jestjs.io/docs/getting-started 」
- 「https://testing-library.com/docs/react-testing-library/intro 」
建议有问题还是啃文档吧 再补上一些有用的教程 - 「https://juejin.cn/post/7039108357554176037 」
4 持续集成/持续部署CI/CD
目前已知CI/CD一般要用到Docker/k8s Jenkins,通过git action在git更新的时候向服务器做更新操作
这真做起来就是抢运维饭碗了啊喂…
嗯…图方便,并且由于前端这边只有静态界面,我这里没有使用服务器。而是通过腾讯静态托管(类似CDN)完成一键部署测试环境。
注意这样是有缺陷的,包括但不限于缺少回滚机制、在本地编包的风险
可能更多人的诉求是当代码合并到某个分支后,机器能自动帮我执行完打包和部署这两个步骤,如果是这样后边不用看了哈…周末要结束我要歇歇了有机会额外出,不是一篇文章能搞定的
4.1 前置准备
先留一个官网链接「https://console.cloud.tencent.com/tcb/hosting 」 正常注册一个云开发环境就行,可以选择「按量付费」再买资源包,一般来讲日花费不到1元。
注册完毕后可以拿到云开发的环境ID,记下来
接着我们需要开通「新建云开发环境」-「静态页面托管」
同时全局安装腾讯云提供的cli,并登陆
npm i @cloudbase/cli -g --force
tcb login
登陆后做一下开发环境验证:
tcb hosting detail -e {{你的环境ID}}
确认已上线
4.2 自定义部署脚本
为了便于使用,我们写一个自定义脚本
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,
};
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()}`));
});
};
const [command, ...argvs] = process.argv.splice(2);
switch (command) {
case "cz":
require(`./scripts/commitizen`)(...argvs);
break;
default:
require(`./scripts/${command}`)(...argvs);
break;
}
这样我们就可以通过脚本命令一键部署,记得部署之前要确认是否在本地编包哦~
npm run pub
附件
附件1 cli目录结构
附件2 commit助手自定义
#! /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 };
});
};
总结
写这篇文章一是汇总部分近期学习和了解到的知识,二是希望能完备一下自己的文章库
~~ 🙅 不可能是防止自己有一天忘了
|