前端基建處理之組件庫優(yōu)化方案
前端基建開發(fā)一直被認為是前端開發(fā)中的 “高階技能”。而內(nèi)部組件庫的開發(fā)則算是基建中比較“容易”入手的一個方向。所以咱們今天就利用這篇文章,來看一看 組件庫開發(fā) 的優(yōu)化方案。
原文:https://juejin.cn/post/7302255044879400998
背景
前段時間入職了新公司后,做一些內(nèi)部前端基建的工作,其中一個工作就是優(yōu)化現(xiàn)有的frontend-common公共組件庫。之前的組件庫一直是以源碼依賴的形式存在,即各個項目通過git submodule的方式將該倉庫引入到各個項目中,作為一個目錄,然后打包的時候?qū)rontend-common的源碼以及項目本身的代碼一起打包到產(chǎn)物中。公共組件的運行依賴于宿主,要求引入frontend-common的項目(宿主)本身要安裝依賴的包,否則無法運行,例如公共組件依賴element這個庫,所以引入公共組件的項目也要求要安裝element才可以運行。
分析
當前這種使用方式以及實際的落地方式上存在一些問題,這里簡單羅列下
- 分支管理不規(guī)范(每個引用frontend-common的子項目都單獨維護了一個分支,沒有合入到主分支,導(dǎo)致各自的差異越來越大)
- 代碼風(fēng)格不統(tǒng)一(不同的開發(fā)的編輯器配置不一樣,導(dǎo)致大家提交上來的代碼五花八門)
- 組件沒有文檔和預(yù)覽(寫公共組件的開發(fā)實現(xiàn)之后就沒有花更多時間在文檔和預(yù)覽上,導(dǎo)致其他開發(fā)要使用組件的時候有上手成本,而且不方便熟悉這些公共組件的功能和使用)
- 沒有提交規(guī)范(因歷史原因,不少提交的commit message上面都是隨便寫,沒有什么規(guī)范,也不方便根據(jù)commit信息判斷改動的內(nèi)容)
- 無法保證改動不影響之前的一些功能(即無法保證能向下兼容,改動需要更多靠人工的方式來驗證功能是否不影響之類的)
- 使用submodule的方式引入的方式不是很優(yōu)雅(個人偏向于用npm包的方式,或者用monorepo的方式)
優(yōu)化思路
根據(jù)上面存在的一些問題我們有針對性的做出一些調(diào)整和策略
- 分支管理規(guī)范,先讓團隊成員把各自的分支合并,如果只是單獨自己項目用的組件,就遷移到自己項目的代碼倉庫中維護,不寫在公共組件中。后續(xù)都從主分支拉新的分支進行開發(fā),本地調(diào)試可以用自己的分支拉取代碼調(diào)試,開發(fā)完之后合并到測試分支,線上環(huán)境和預(yù)發(fā)布環(huán)境必須用指定的分支來拉取公共組件庫的代碼。
- 用eslint + prettier + husky + lint-stage來保證代碼風(fēng)格統(tǒng)一
- 接入storybook,用于做組件預(yù)覽和文檔的功能
- 增加commitlint commitizen等工具,用于命令式生成commit,保證commit信息的規(guī)范
- 增加單元測試,新增一個組件要寫單元測試,后續(xù)修改之后要保證之前的單元測試都運行通過才可以合并代碼
- 因為內(nèi)部基建的原因,暫時還沒有搭建內(nèi)部的npm源,monorepo的方式改動也比較大,暫時不做調(diào)整
改造步驟
倉庫初始化npm
因為原先是作為當成一個組件來使用,所以frontend-common這個代碼倉庫里面是沒有package.json node_module等配置,我們?yōu)榱私尤氲囊?guī)范肯定要增加包來處理這些,所以第一步要初始化npm
npm init
直接按提示輸入即可,這里就不再贅述
增加代碼規(guī)范的包
eslint + prettier + lint-stage + husky + 對應(yīng)的eslint包
根據(jù)自己項目的實際情況增加對應(yīng)的包,比如筆者這個倉庫是用vue2的,就用vue相關(guān)的eslint包
這里筆者列一下自己安裝的包和創(chuàng)建的配置文件
新增包和命令
在package.json中新增對應(yīng)的包和命令、配置
"scripts": {
...
"lint-staged": "lint-staged",
"prepare": " husky install",
},
"lint-staged": {
"*.{js,ts,vue,jsx,tsx}": [
"eslint --cache --fix"
]
},
"devDependencies": {
...
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"husky": "^8.0.0",
"lint-staged": "^13.2.2",
"prettier": "^2.4.1",
}
新增配置文件
- .eslintrc.js (eslint的配置文件)
- .eslintignore(eslint的忽略配置文件)
- .prettier.js (prettier的配置文件)
- commitlint.config.js (commitlint的配置文件)
commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
};
eslintrc.js
// .eslintrc.js
module.exports = {
root: true,
// 指定代碼的運行環(huán)境
env: {
browser: true,
node: true,
es6: true
},
plugins: ['vue', 'prettier'],
extends: [
// 繼承 vue 的標準特性
'plugin:vue/essential',
'eslint:recommended',
// 避免與 prettier 沖突
'plugin:prettier/recommended'
],
parserOptions: {
// 定義ESLint的解析器
parser: '@babel/eslint-parser',
sourceType: 'module'
}
};
處理husky
配置完成之后記得npm i,這時候會添加.husky目錄,給這個目錄添加文件
commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint -e $GIT_PARAMS
pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged
參考目錄如下:
運行
這樣配置完后續(xù)正常commit就會觸發(fā)eslint和commitlint,保證提交的代碼和commit的規(guī)范。如果報錯要修復(fù)完問題才可以正常提交,而且代碼都會進行格式化,保證每個人提交的風(fēng)格都一致。其他的不展開贅述。
接入storybook
初始化storybook
在原先的項目中執(zhí)行命令初始化storybook的相關(guān)配置和依賴
npx -p @storybook/cli sb init --type vue
選擇webpack5和安裝依賴
自動運行storybook
打開瀏覽器,我們可以看到storybook的界面
來走讀一下創(chuàng)建出來的storybook demo文件,我們以Button.stories.js這個文件為例
import MyButton from './Button.vue';
// More on how to set up stories at: https://storybook.js.org/docs/vue/writing-stories/introduction
export default {
title: 'Example/Button',
component: MyButton,
tags: ['autodocs'],
render: (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { MyButton },
template: '<my-button @onClick="onClick" v-bind="$props" />',
}),
argTypes: {
backgroundColor: { control: 'color' },
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
},
},
};
// More on writing stories with args: https://storybook.js.org/docs/vue/writing-stories/args
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};
走讀下default這個配置
-
title: 該故事在 Storybook 應(yīng)用的側(cè)邊欄中的名稱。路徑式的名稱表示故事的層級結(jié)構(gòu)。在這個例子中,"Example" 是一個文件夾,"Button" 是這個文件夾下的一個故事。 -
component: 這是你想要展示的組件,Storybook 將使用它來自動生成文檔頁(如果你啟用了這個功能)。 -
tags: 這是一個標簽數(shù)組,你可以添加任何你喜歡的標簽來幫助你組織和查找你的故事。 -
render: 這是一個函數(shù),返回一個 Vue 組件的配置對象,用于定義如何渲染故事。在這個例子中,所有的 args 和 argTypes 都被傳遞給MyButton組件,你可以在 Storybook 的 UI 中調(diào)整它們的值。 -
argTypes: 這個對象定義了每個 arg 的控件和其他配置。在這個例子中,backgroundColor的控件是一個顏色選擇器,size的控件是一個下拉列表,選項包括 'small'、'medium' 和 'large'。-
control: 用于指定參數(shù)控件的類型,例如:'color'、'select'、'range' 等。 -
options: 用于指定 'select' 類型控件的選項。
-
新建story
新建一個story,用于編寫我們自己的組件的story,如下,這個是我們新創(chuàng)建的stories文件,我們引入自己的vue2組件
先照貓畫虎寫一個配置
import CommonNoFound from "../../../components/commonPage/FcommonNoFound/index.vue";
export default {
title: "components/FcommonNoFound",
component: CommonNoFound,
tags: ["autodocs"],
};
export const NoFound = {};
看下效果
這是因為我們的組件里面用了vue-i18n,用$t然后storybook識別不到,這里我們就需要解決這個vue-i18n的問題
解決vue-i18n
我們需要在.storybook/preview.js中設(shè)置vue-i18n相關(guān)的配置 看下原先的文件
/** @type { import('@storybook/vue').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
我們在這個文件的基礎(chǔ)上增加vue-i18n的配置 要預(yù)先安裝好vue vue-i18n,然后同i18n初始化一致實例化i18n實例然后設(shè)置到storybook中 看下代碼
import Vue from "vue";
import VueI18n from "vue-i18n";
import en from "../lang/en_us";
import zh from "../lang/zh_cn";
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: "en",
messages: {
zh: {
language: "簡體中文",
...zh,
},
en: {
language: "English",
...en,
},
},
});
/** @type { import('@storybook/vue').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
decorators: [
(Story) => ({
components: { Story },
template: '<story v-bind="$props" />',
i18n,
}),
],
};
export default preview;
在這個例子中,decorators 數(shù)組中的函數(shù)接收一個 Story 參數(shù),這個參數(shù)表示當前的故事組件。然后,我們創(chuàng)建了一個模板,這個模板包含一個 story 組件,并且使用 v-bind 來綁定故事的屬性。最后,我們在 components 對象中指定了 Story 組件。
這樣,你的故事組件就會接收到 i18n 實例,并且會正確地被渲染。
解決環(huán)境變量問題
vue代碼里面會有環(huán)境變量,但是在storybook的環(huán)境中這個環(huán)境變量是沒有的,所以我們需要手動設(shè)置這個環(huán)境變量,保證我們的代碼可以正常運行 這時候我們需要一個包,我們安裝dotEnv這個包
npm i dotenv --save-dev
然后我們新建一個.env的文件,在這個文件中我們設(shè)置我們需要的環(huán)境變量,例如我的這個
VUE_APP_WEB_ENV=dev
然后就是在我們的storybook的mainjs或者preview.js配置中引入dotEnv的配置即可
require("dotenv").config();
// 其他配置
解決請求代理問題
我們在常見的vue項目中,本地開發(fā)會經(jīng)常用proxy的配置來解決跨域問題,轉(zhuǎn)發(fā)接口,當我們的組件中依賴了接口的話,這時候我們可以同樣模擬一下這個proxy的過程 我們需要安裝proxy的包
npm install http-proxy-middleware --save-dev
然后我們在.storybook目錄下新建一個middleware.js,然后同我們的webpack配置一樣補充我們需要的proxy,這里補充下筆者接手的這個項目的配置
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function expressMiddleware(router) {
let env = process.env.VUE_APP_WEB_ENV || "dev";
const comonApi = require(`../config/api/${env}.js`);
if (comonApi && Object.keys(comonApi).length) {
Object.keys(comonApi).forEach((e) => {
const apiBase = comonApi[e].apiBase;
const apiRoot = comonApi[e].apiRoot;
if (apiBase && apiRoot) {
router.use(
apiBase,
createProxyMiddleware({
target: apiRoot,
changeOrigin: true,
pathRewrite: {
[`^${apiBase}`]: "",
},
})
);
}
});
}
};
引入組件庫
組件會依賴一些UI庫的組件,比如筆者用到的element ui,在storybook中需要引入這些element的組件,這里我們在.storybook/preview.js中引入element,參考如下
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
解決樣式問題
引入組件會有一些樣式,所以我們也需要處理下引入的css,類似webpack一樣增加對應(yīng)的loader,我們安裝對應(yīng)的loader
npm install --save-dev sass-loader style-loader css-loader
然后在.storybook/main.js文件中補充對應(yīng)的webpack配置
const config = {
webpackFinal: async (config, { configType }) => {
// 處理 SCSS 文件
config.module.rules.push({
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
});
return config;
},
}
接入commitizen
組件庫之前的各種commit都是五花八門,這里為了規(guī)范commit信息,然后方便后面生成changelog,我們這里需要一個命令式的commit提交工具,筆者選擇了用commitizen,先安裝好這個包
npm install --save-dev commitizen cz-conventional-changelog
然后在我們的package.json中增加對應(yīng)的script或者配置
"scripts": {
"commit": "git-cz",
...
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
我們正常運行git add git commit就會觸發(fā)下面這個,然后根據(jù)實際情況填寫內(nèi)容
全部填寫完成之后就會生成對應(yīng)的commit記錄
生成changelog(可忽略)
下面的自動升級版本的命令會自動生成changelog,實際接入中可以不用看這一部分 changelog就是根據(jù)我們的commit生成變更的日志,嘗試效果的話我們需要引入新的包
npm install --save-dev conventional-changelog-cli
在package.json中增加一個生成changelog的腳本,通過這個命令我們可以手動生成changelog
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}
}
自動升級版本
我們需要在準備發(fā)版的時候,更新package.json中的版本號,生成changelog文件,提交更改和創(chuàng)建標簽,這里我們需要用到第三方的工具包,這里用了standard-version
npm install --save-dev standard-version
增加腳本
"scripts": {
"release": "standard-version"
}
當我們準備發(fā)新版本的時候,就跑一下這個命令npm run release,這時候就會幫我們自動增加一個commit做上面說的事情,比如這樣的commit
因為standard-version這個包內(nèi)置了生成changelog的包,所以我們不需要額外引入上面部分提到的conventional-changelog-cli
接入單元測試
單元測試的作用
組件庫會被多個項目引用,每個項目的情況不一樣,可能需要根據(jù)本身項目的需求對組件進行修改或者增加一些改動,原則上改動都是要向下兼容的,每次組件庫更新理論上引用的項目都要跟著更新,驗證下改動是否沒問題,但是考慮到每次都要讓各個項目來驗證這種成本比較高,所以引入單元測試,組件的創(chuàng)建人在寫完組件之后,順便根據(jù)自己的場景補充好單元測試。下一個修改的人如果要修改這個組件,修改完成之后,需要保證原先的單元測試都跑通過才可以,另外需要補充單元測試。
編寫單元測試
我們在編寫好vue組件之后,如果要對當前這個組件編寫單元測試,可以在組件當前的目錄(初定是和組件放在同一個目錄下)創(chuàng)建對應(yīng)的一個 xx.spec.js文件,然后在文件中編寫對應(yīng)的單元測試,可以參考項目中已有的單元測試文件,如下。
import { shallowMount } from "@vue/test-utils";
import CommonNoFound from "./index";
import { i18n, localVue } from "../../../jest.setup";
describe("FcommonNoFound.vue", () => {
it("can find list text", () => {
const wrapper = shallowMount(CommonNoFound, { localVue, i18n });
expect(wrapper.text()).toContain("Lost...");
});
it("can find pageNotFound text", () => {
const wrapper = shallowMount(CommonNoFound, { localVue, i18n });
expect(wrapper.text()).toContain("頁面已飛到太空外");
});
});
運行與調(diào)試單元測試
我們在package.json中增加一個命令,用于運行單元測試
{
"scripts": {
"test": "jest"
}
}
運行單個單測文件,可以單獨驗證單測文件是否運行通過,可以在命令后面補充對應(yīng)的單測文件路徑
npm run test components/commonPage/FcommonNoFound/commonNoFound.spec.js
運行結(jié)果,可以看到哪些通過哪些不通過,如果不通過會有報錯信息,根據(jù)報錯信息調(diào)整單測
全量運行,結(jié)果展示同上
npm run test
單元測試卡點
有了單元測試之后,我們需要在每次提交合并的時候保證所有的單元測試都跑通過,否則就不給合并代碼,相當于對每次合碼都做一次卡點,減少一些改動無法向下兼容,導(dǎo)致引用組件的項目出現(xiàn)問題。
- 可以考慮使用自動化測試在每次PR或者MR的時候做運行所有的單元測試,檢查測試覆蓋率之類的
- 如果無法做自動化測試的話,可以考慮每次PR或者MR的時候要求提交人補充本地運行所有單元測試的結(jié)果,這里就可以通過配置一些MR或者PR提交的模板,要求代碼提交人按這種格式來提交,補充好單元測試的截圖之類的
合并代碼策略
指定分支合并到對應(yīng)的分支,例如合并到release或者master分支,這時候會有預(yù)置的模板,按照模板補充說明然后提交PR進行審核
以下是筆者的搞的一個合碼的模板,要求提交人按這種格式去填寫
組件預(yù)覽部署
在上面的步驟中我們已經(jīng)接入了storybook,可以在本地預(yù)覽,如果我們要單獨把storybook單獨部署一個到一個站點,其他開發(fā)可以直接打開去看
增加構(gòu)建命令
在package.json中增加命令,構(gòu)建出storybook的產(chǎn)物
"scripts": {
"build-dev": "storybook build -o dist",
}
項目部署
配合運維,綁定好分支,然后當指定分支有merge或者Push的時候,觸發(fā)構(gòu)建,這個根據(jù)自己團隊的情況去部署即可。
筆者部署完的大概樣子如下:
總結(jié)
當前這版優(yōu)化對現(xiàn)有的組件庫做了一次大的調(diào)整,本身不涉及具體組件的改動,只是規(guī)范和優(yōu)化整個流程,方便前端開發(fā)接入和使用等,但是還存在不少的優(yōu)化空間,比如以submodule接入的方式,筆者覺得不是很好,還是偏向于用npm包的方式,但是由于內(nèi)部還沒有搞自建的npm源,加上不少項目都已經(jīng)在用submodule的方式了,所以暫時不做這種處理。
