您好,登錄后才能下訂單哦!
本文小編為大家詳細(xì)介紹“vue組件庫(kù)如何開(kāi)發(fā)使用”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“vue組件庫(kù)如何開(kāi)發(fā)使用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來(lái)學(xué)習(xí)新知識(shí)吧。
考慮到組件庫(kù)整體需要有多邊資源支持,比如組件源碼,庫(kù)文檔站點(diǎn),color-gen等類(lèi)庫(kù)工具,代碼規(guī)范配置,vite插件,腳手架,storybook等等,需要分出很多packages,package之間存在彼此聯(lián)系,因此考慮使用monorepo的管理方式,同時(shí)使用yarn作為包管理工具,lerna作為包發(fā)布工具。
在monorepo之前,根目錄就是一個(gè)workspace,我們直接通過(guò)yarn add/remove/run等就可以對(duì)包進(jìn)行管理。但在monorepo項(xiàng)目中,根目錄下存在多個(gè)子包,yarn 命令無(wú)法直接操作子包,比如根目錄下無(wú)法通過(guò)yarn run dev啟動(dòng)子包package-a中的dev命令,這時(shí)我們就需要開(kāi)啟yarn的workspaces功能,每個(gè)子包對(duì)應(yīng)一個(gè)workspace,之后我們就可以通過(guò)yarn workspace package-a run dev
啟動(dòng)package-a中的dev命令了。
你可能會(huì)想,我們直接cd到package-a下運(yùn)行就可以了,不錯(cuò),但yarn workspaces的用武之地并不只此,像auto link,依賴(lài)提升,單.lock等才是它在monorepo中的價(jià)值所在。
我們?cè)诟夸沺ackge.json中啟用yarn workspaces:
{
"private": true,
"workspaces": [
"packages/*"
]
}
packages目錄下的每個(gè)直接子目錄作為一個(gè)workspace。由于我們的根項(xiàng)目是不需要發(fā)布出去的,因此設(shè)置private為true。
不得不說(shuō),yarn workspaces已經(jīng)具備了lerna部分功能,之所以使用它,是想借用它的發(fā)布工作流以彌補(bǔ)workspaces在monorepo下在這方面的不足。下面我們開(kāi)始將lerna集成到項(xiàng)目中。
首先我們先安裝一下lerna:
# W指workspace-root,即在項(xiàng)目根目錄下安裝,下同
yarn add lerna -D -W
# 由于經(jīng)常使用lerna命令也推薦全局安裝
yarn global add lerna
or
npm i lerna -g
執(zhí)行lerna init
初始化項(xiàng)目,成功之后會(huì)幫我們創(chuàng)建了一個(gè)lerna.json
文件
lerna init
// lerna.json
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "0.0.0"
}
$schema
指向的lerna-schema.json描述了如何配置lerna.json,配置此字段后,鼠標(biāo)懸浮在屬性上會(huì)有對(duì)應(yīng)的描述。注意,以上的路徑值需要你在項(xiàng)目根目錄下安裝lerna。
useWorkspaces
定義了在lerna bootstrap
期間是否結(jié)合yarn workspace。
由于lerna默認(rèn)的工作模式是固定模式,即發(fā)布時(shí)每個(gè)包的版本號(hào)一致。這里我們修改為independent
獨(dú)立模式,同時(shí)將npm客戶(hù)端設(shè)置為yarn
。如果你喜歡pnpm
,just do it!
// lerna.json
{
"version": "independent",
"npmClient": "yarn"
}
至此yarn workspaces
搭配lerna
的monorepo項(xiàng)目就配置好了,非常簡(jiǎn)單!
By the way!由于項(xiàng)目會(huì)使用commitlint
對(duì)提交信息進(jìn)行校驗(yàn)是否符合Argular規(guī)范,而lerna version
默認(rèn)為我們commit的信息是"Publish",因此我們需要進(jìn)行一些額外的配置。
// lerna.json
{
"command": {
"version": {
"message": "chore(release): publish",
"conventionalCommits": true
}
}
}
可以看到,我們使用符合Argular團(tuán)隊(duì)提交規(guī)范的"chore(release): publish"
代替默認(rèn)的"Publish"。
conventionalCommits
表示當(dāng)我們運(yùn)行lerna version
,實(shí)際上會(huì)運(yùn)行lerna version --conventional-commits
幫助我們生成CHANGELOG.md。
在lerna剛發(fā)布的時(shí)候,那時(shí)的包管理工具還沒(méi)有可用的workspaces
解決方案,因此lerna自身實(shí)現(xiàn)了一套解決方案。時(shí)至今日,現(xiàn)代的包管理工具幾乎都內(nèi)置了workspaces
功能,這使得lerna和yarn有許多功能重疊,比如執(zhí)行包pkg-a的dev命令lerna run dev --stream --scope=pkg-a
,我們完全可以使用yarn workspace pkg-a run dev
代替。lerna bootstrap --hoist將安裝包提升到根目錄,而在yarn workspaces中直接運(yùn)行yarn就可以了。
Anyway, 使用yarn
作為軟件包管理工具,lerna
作為軟件包發(fā)布工具,是在monorepo
管理方式下一個(gè)不錯(cuò)的實(shí)踐!
很無(wú)奈,我知道大部分人都不喜歡Lint,但對(duì)我而言,這是必須的。
packages目錄下創(chuàng)建名為@argo-design/eslint-config(非文件夾名)的package
cd argo-eslint-config
yarn add eslint
npx eslint --init
注意這里沒(méi)有-D或者--save-dev。選擇如下:
安裝完成后手動(dòng)將devDependencies
下的依賴(lài)拷貝到dependencies
中。或者你手動(dòng)安裝這一系列依賴(lài)。
// argo-eslint-config/package.json
{
scripts: {
"lint:script": "npx eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./"
}
}
運(yùn)行yarn lint:script
,將會(huì)自動(dòng)修復(fù)代碼規(guī)范錯(cuò)誤警告(如果可以的話(huà))。
安裝VSCode Eslint插件并進(jìn)行如下配置,此時(shí)在你保存代碼時(shí),也會(huì)自動(dòng)修復(fù)代碼規(guī)范錯(cuò)誤警告。
// settings.json
{
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
在argo-eslint-config
中新建包入口文件index.js,并將.eslintrc.js的內(nèi)容拷貝到index.js中
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: ['plugin:vue/vue3-essential', 'standard-with-typescript'],
overrides: [],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['vue'],
rules: {}
}
確保package.json配置main
指向我們剛剛創(chuàng)建的index.js。
// argo-eslint-config/package.json
{
"main": "index.js"
}
根目錄package.json新增如下配置
// argo-eslint-config/package.json
{
"devDependencies": {
"@argo-design/eslint-config": "^1.0.0"
},
"eslintConfig": {
"root": true,
"extends": [
"@argo-design"
]
}
}
最后運(yùn)行yarn重新安裝依賴(lài)。
注意包命名與extends書(shū)寫(xiě)規(guī)則;root表示根配置,對(duì)eslint配置文件冒泡查找到此為止。
接下來(lái)我們引入formatter工具prettier
。首先我們需要關(guān)閉eslint規(guī)則中那些與prettier沖突或者不必要的規(guī)則,最后由prettier
代為實(shí)現(xiàn)這些規(guī)則。前者我們通過(guò)eslint-config-prettier
實(shí)現(xiàn),后者借助插件eslint-plugin-prettier
實(shí)現(xiàn)。比如沖突規(guī)則尾逗號(hào),eslint-config-prettier
幫我們屏蔽了與之沖突的eslint規(guī)則:
{
"comma-dangle": "off",
"no-comma-dangle": "off",
"@typescript-eslint/comma-dangle": "off",
"vue/comma-dangle": "off",
}
通過(guò)配置eslint規(guī)則"prettier/prettier": "error"
讓錯(cuò)誤暴露出來(lái),這些錯(cuò)誤交給eslint-plugin-prettier
收拾。
prettier配置我們也新建一個(gè)package@argo-design/prettier-config
。
cd argo-prettier-config
yarn add prettier
yarn add eslint-config-prettier eslint-plugin-prettier
// argo-prettier-config/index.js
module.exports = {
printWidth: 80, //一行的字符數(shù),如果超過(guò)會(huì)進(jìn)行換行,默認(rèn)為80
semi: false, // 行尾是否使用分號(hào),默認(rèn)為true
trailingComma: 'none', // 是否使用尾逗號(hào)
bracketSpacing: true // 對(duì)象大括號(hào)直接是否有空格
};
完整配置參考官網(wǎng) prettier配置
回到argo-eslint-config/index.js,只需新增如下一條配置即可
module.exports = {
"extends": ["plugin:prettier/recommended"]
};
plugin:prettier/recommended
指的eslint-plugin-prettier
package下的recommended.js。該擴(kuò)展已經(jīng)幫我們配置好了
{
"extends": ["eslint-config-prettier"],
"plugins": ["eslint-plugin-prettier"],
"rules": {
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off"
}
}
根目錄package.json新增如下配置
{
"devDependencies": {
"@argo-design/prettier-config": "^1.0.0"
},
"prettier": "@argo-design/prettier-config"
}
運(yùn)行yarn重新安裝依賴(lài)。
// settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
stylelint配置我們也新建一個(gè)package@argo-design/stylelint-config
。
cd argo-stylelint-config
yarn add stylelint stylelint-prettier stylelint-config-prettier stylelint-order stylelint-config-rational-order postcss-html postcss-less
# 單獨(dú)postcss8
yarn add postcss@^8.0.0
對(duì)于結(jié)合prettier
這里不在贅述。
stylelint-order
允許我們自定義樣式屬性名稱(chēng)順序。而stylelint-config-rational-order
為我們提供了一套合理的開(kāi)箱即用的順序。
值得注意的是,stylelint14版本不在默認(rèn)支持less,sass等預(yù)處理語(yǔ)言。并且stylelint14依賴(lài)postcss8版本,可能需要單獨(dú)安裝,否則vscode 的stylellint擴(kuò)展可能提示報(bào)錯(cuò)TypeError: this.getPosition is not a function at LessParser.inlineComment....
// argo-stylelint-config/index.js
module.exports = {
plugins: [
"stylelint-prettier",
],
extends: [
// "stylelint-config-standard",
"stylelint-config-standard-vue",
"stylelint-config-rational-order",
"stylelint-prettier/recommended"
],
rules: {
"length-zero-no-unit": true, // 值為0不需要單位
"plugin/rational-order": [
true,
{
"border-in-box-model": true, // Border理應(yīng)作為盒子模型的一部分 默認(rèn)false
"empty-line-between-groups": false // 組之間添加空行 默認(rèn)false
}
]
},
overrides: [
{
files: ["*.html", "**/*.html"],
customSyntax: "postcss-html"
},
{
files: ["**/*.{less,css}"],
customSyntax: "postcss-less"
}
]
};
根目錄package.json新增如下配置
{
"devDependencies": {
"@argo-design/stylelint-config": "^1.0.0"
},
"stylelint": {
"extends": [
"@argo-design/stylelint-config"
]
}
}
運(yùn)行yarn重新安裝依賴(lài)。
VSCode安裝Stylelint擴(kuò)展并添加配置
// settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"stylelint.validate": ["css", "less", "vue", "html"],
"css.validate": false,
"less.validate": false
}
修改settings.json之后如不能及時(shí)生效,可以重啟一下vscode。如果你喜歡,可以將eslint,prettier,stylelint配置安裝到全局并集成到編輯器。
為防止一些非法的commit
或push
,我們借助git hooks
工具在對(duì)代碼提交前進(jìn)行 ESLint 與 Stylelint的校驗(yàn),如果校驗(yàn)通過(guò),則成功commit,否則取消commit。
# 在根目錄安裝husky
yarn add husky -D -W
npm pkg set scripts.prepare="husky install"
npm run prepare
# 添加pre-commit鉤子,在提交前運(yùn)行代碼lint
npx husky add .husky/pre-commit "yarn lint"
至此,當(dāng)我們執(zhí)行git commit -m "xxx"
時(shí)就會(huì)先執(zhí)行l(wèi)int校驗(yàn)我們的代碼,如果lint通過(guò),成功commit,否則終止commit。具體的lint命令請(qǐng)自行添加。
現(xiàn)在,當(dāng)我們git commit時(shí),會(huì)對(duì)整個(gè)工作區(qū)的代碼進(jìn)行l(wèi)int。當(dāng)工作區(qū)文件過(guò)多,lint的速度就會(huì)變慢,進(jìn)而影響開(kāi)發(fā)體驗(yàn)。實(shí)際上我們只需要對(duì)暫存區(qū)中的文件進(jìn)行l(wèi)int即可。下面我們引入·lint-staged
解決我們的問(wèn)題。
在根目錄安裝lint-staged
yarn add lint-staged -D -W
在根目錄package.json
中添加如下的配置:
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{less,css}": [
"stylelint --fix",
"prettier --write"
],
"**/*.vue": [
"eslint --fix",
"stylelint --fix",
"prettier --write"
]
}
}
在monorepo中,lint-staged
運(yùn)行時(shí),將始終向上查找并應(yīng)用最接近暫存文件的配置,因此我們可以在根目錄下的package.json中配置lint-staged。值得注意的是,每個(gè)glob匹配的數(shù)組中的命令是從左至右依次運(yùn)行,和webpack的loder應(yīng)用機(jī)制不同!
最后,我們?cè)?husky文件夾中找到pre-commit
,并將yarn lint
修改為npx --no-install lint-staged
。
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged
至此,當(dāng)我們執(zhí)行git commit -m "xxx"
時(shí),lint-staged
會(huì)如期運(yùn)行幫我們校驗(yàn)staged(暫存區(qū))中的代碼,避免了對(duì)工作區(qū)的全量檢查。
除了代碼規(guī)范檢查之后,Git 提交信息的規(guī)范也是不容忽視的一個(gè)環(huán)節(jié),規(guī)范精準(zhǔn)的 commit 信息能夠方便自己和他人追蹤項(xiàng)目和把控進(jìn)度。這里,我們使用大名鼎鼎的Angular團(tuán)隊(duì)提交規(guī)范
。
commit message 由 Header
、Body
、Footer
組成。其中Herder時(shí)必需的,Body和Footer可選。
Header 部分包括三個(gè)字段 type
、scope
和 subject
。
<type>(<scope>): <subject>
其中type 用于說(shuō)明 commit 的提交類(lèi)型(必須是以下幾種之一)。
值 | 描述 |
---|---|
feat | Feature) 新增一個(gè)功能 |
fix | Bug修復(fù) |
docs | Documentation) 文檔相關(guān) |
style | 代碼格式(不影響功能,例如空格、分號(hào)等格式修正),并非css樣式更改 |
refactor | 代碼重構(gòu) |
perf | Performent) 性能優(yōu)化 |
test | 測(cè)試相關(guān) |
build | 構(gòu)建相關(guān)(例如 scopes: webpack、gulp、npm 等) |
ci | 更改持續(xù)集成軟件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 |
chore | 變更構(gòu)建流程或輔助工具,日常事務(wù) |
revert | git revert |
scope 用于指定本次 commit 影響的范圍。
subject 是本次 commit 的簡(jiǎn)潔描述,通常遵循以下幾個(gè)規(guī)范:
用動(dòng)詞開(kāi)頭,第一人稱(chēng)現(xiàn)在時(shí)表述,例如:change 代替 changed 或 changes
第一個(gè)字母小寫(xiě)
結(jié)尾不加句號(hào).
body 是對(duì)本次 commit 的詳細(xì)描述,可以分成多行。跟 subject 類(lèi)似。
如果本次提交的代碼是突破性的變更或關(guān)閉Issue,則 Footer 必需,否則可以省略。
我們可以借助工具幫我們生成規(guī)范的message。
yarn add commitizen -D -W
安裝適配器
yarn add cz-conventional-changelog -D -W
這行命令做了兩件事:
安裝cz-conventional-changelog
到開(kāi)發(fā)依賴(lài)
在根目錄下的package.json中增加了:
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
添加npm scriptscm
"scripts": {
"cm": "cz"
},
至此,執(zhí)行yarn cm
,就能看到交互界面了!跟著交互一步步操作就能自動(dòng)生成規(guī)范的message了。
首先在根目錄安裝依賴(lài):
yarn add commitlint @commitlint/cli @commitlint/config-conventional -D -W
接著新建.commitlintrc.js
:
module.exports = {
extends: ["@commitlint/config-conventional"]
};
最后向husky中添加commit-msg
鉤子,終端執(zhí)行:
npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS"
執(zhí)行成功之后就會(huì)在.husky文件夾中看到commit-msg文件了:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint -e
至此,當(dāng)你提交代碼時(shí),如果pre-commit
鉤子運(yùn)行成功,緊接著在commit-msg
鉤子中,commitlint會(huì)如期運(yùn)行對(duì)我們提交的message進(jìn)行校驗(yàn)。
關(guān)于lint工具的集成到此就告一段落了,在實(shí)際開(kāi)發(fā)中,我們還會(huì)對(duì)lint配置進(jìn)行一些小改動(dòng),比如ignore,相關(guān)rules等等。這些和具體項(xiàng)目有關(guān),我們不會(huì)變更package里的配置。
千萬(wàn)別投機(jī)取巧拷貝別人的配置文件!復(fù)制一時(shí)爽,代碼火葬場(chǎng)。
巧婦難為無(wú)米之炊。組件庫(kù)通常依賴(lài)很多圖標(biāo),因此我們先開(kāi)發(fā)一個(gè)支持按需引入的圖標(biāo)庫(kù)。
假設(shè)我們現(xiàn)在已經(jīng)拿到了一些漂亮的svg圖標(biāo),我們要做的就是將每一個(gè)圖標(biāo)轉(zhuǎn)化生成.vue組件與一個(gè)組件入口index.ts文件。然后再生成匯總所有組件的入口文件。比如我們現(xiàn)在有foo.svg與bar.svg兩個(gè)圖標(biāo),最終生成的文件及結(jié)構(gòu)如下:
相應(yīng)的內(nèi)容如下:
// bar.ts
import _Bar from "./bar.vue";
const Bar = Object.assign(_Bar, {
install: (app) => {
app.component(_Bar.name, _Bar);
}
});
export default Bar;
// foo.ts
import _Foo from "./foo.vue";
const Foo = Object.assign(_Foo, {
install: (app) => {
app.component(_Foo.name, _Foo);
}
});
export default Foo;
// argoIcon.ts
import Foo from "./foo";
import Bar from "./bar";
const icons = [Foo, Bar];
const install = (app) => {
for (const key of Object.keys(icons)) {
app.use(icons[key]);
}
};
const ArgoIcon = {
...icons,
install
};
export default ArgoIcon;
// index.ts
export { default } from "./argoIcon";
export { default as Foo } from "./foo";
export { default as Bar } from "./bar";
之所以這么設(shè)計(jì)是由圖標(biāo)庫(kù)最終如何使用決定的,除此之外argoIcon.ts
也將會(huì)是打包umd
的入口文件。
// 全量引入import ArgoIcon from "圖標(biāo)庫(kù)";
app.use(ArgoIcon);
// 按需引入import { Foo } from "圖標(biāo)庫(kù)";
app.use(Foo);
圖標(biāo)庫(kù)的整個(gè)構(gòu)建流程大概分為以下3步:
整個(gè)流程很簡(jiǎn)單,我們通過(guò)glob匹配到.svg拿到所有svg的路徑,對(duì)于每一個(gè)路徑,我們讀取svg的原始文本信息交由第三方庫(kù)svgo處理,期間包括刪除無(wú)用代碼,壓縮,自定義屬性等,其中最重要的是為svg標(biāo)簽注入我們想要的自定義屬性,就像這樣:
<svg
:class="cls"
:style="innerStyle"
:stroke-linecap="strokeLinecap"
:stroke-linejoin="strokeLinejoin"
:stroke-width="strokeWidth">
<path d="..."></path>
</svg>
之后這段svgHtml
會(huì)傳送給我們預(yù)先準(zhǔn)備好的摸板字符串:
const template = `
<template>
${svgHtml}
</template>
<script setup>
defineProps({
"stroke-linecap": String;
// ...
})
// 省略邏輯代碼...
</script>
`
為摸板字符串填充數(shù)據(jù)后,通過(guò)fs模塊的writeFile生成我們想要的.vue文件。
在打包構(gòu)建方案上直接選擇vite為我們提供的lib模式即可,開(kāi)箱即用,插件擴(kuò)展(后面會(huì)講到),基于rollup,能幫助我們打包生成ESM,這是按需引入的基礎(chǔ)。當(dāng)然,commonjs
與umd
也是少不了的。整個(gè)過(guò)程我們通過(guò)Vite 的JavaScript API
實(shí)現(xiàn):
import { build } from "vite";
import fs from "fs-extra";
const CWD = process.cwd();
const ES_DIR = resolve(CWD, "es");
const LIB_DIR = resolve(CWD, "lib");
interface compileOptions {
umd: boolean;
target: "component" | "icon";
}
async function compileComponent({
umd = false,
target = "component"
}: compileOptions): Promise<void> {
await fs.emptyDir(ES_DIR);
await fs.emptyDir(LIB_DIR);
const config = getModuleConfig(target);
await build(config);
if (umd) {
await fs.emptyDir(DIST_DIR);
const umdConfig = getUmdConfig(target);
await build(umdConfig);
}
}
import { InlineConfig } from "vite";
import glob from "glob";
const langFiles = glob.sync("components/locale/lang/*.ts");
export default function getModuleConfig(type: "component" | "icon"): InlineConfig {
const entry = "components/index.ts";
const input = type === "component" ? [entry, ...langFiles] : entry;
return {
mode: "production",
build: {
emptyOutDir: true,
minify: false,
brotliSize: false,
rollupOptions: {
input,
output: [
{
format: "es", // 打包模式
dir: "es", // 產(chǎn)物存放路徑
entryFileNames: "[name].js", // 入口模塊的產(chǎn)物文件名
preserveModules: true, // 保留模塊結(jié)構(gòu),否則所有模塊都將打包在一個(gè)bundle文件中
/*
* 保留模塊的根路徑,該值會(huì)在打包后的output.dir中被移除
* 我們的入口是components/index.ts,打包后文件結(jié)構(gòu)為:es/components/index.js
* preserveModulesRoot設(shè)為"components",打包后就是:es/index.js
*/
preserveModulesRoot: "components"
},
{
format: "commonjs",
dir: "lib",
entryFileNames: "[name].js",
preserveModules: true,
preserveModulesRoot: "components",
exports: "named" // 導(dǎo)出模式
}
]
},
// 開(kāi)啟lib模式
lib: {
entry,
formats: ["es", "cjs"]
}
},
plugins: [
// 自定義external忽略node_modules
external(),
// 打包聲明文件
dts({
outputDir: "es",
entryRoot: C_DIR
})
]
};
};
export default function getUmdConfig(type: "component" | "icon"): InlineConfig {
const entry =
type === "component"
? "components/argo-components.ts"
: "components/argo-icons.ts";
const entryFileName = type === "component" ? "argo" : "argo-icon";
const name = type === "component" ? "Argo" : "ArgoIcon";
return {
mode: "production",
build: {
target: "modules", // 支持原生 ES 模塊的瀏覽器
outDir: "dist", // 打包產(chǎn)物存放路徑
emptyOutDir: true, // 如果outDir在根目錄下,則清空outDir
sourcemap: true, // 生成sourcemap
minify: false, // 是否壓縮
brotliSize: false, // 禁用 brotli 壓縮大小報(bào)告。
rollupOptions: { // rollup打包選項(xiàng)
external: "vue", // 匹配到的模塊不會(huì)被打包到bundle
output: [
{
format: "umd", // umd格式
entryFileNames: `${entryFileName}.js`, // 即bundle名
globals: {
/*
* format為umd/iife時(shí),標(biāo)記外部依賴(lài)vue,打包后以Vue取代
* 未定義時(shí)打包結(jié)果如下
* var ArgoIcon = function(vue2) {}(vue);
* rollup自動(dòng)猜測(cè)是vue,但實(shí)際是Vue.這會(huì)導(dǎo)致報(bào)錯(cuò)
* 定義后
* var ArgoIcon = function(vue) {}(Vue);
*/
vue: "Vue"
}
},
{
format: "umd",
entryFileNames: `${entryFileName}.min.js`,
globals: {
vue: "Vue"
},
plugins: [terser()] // terser壓縮
},
]
},
// 開(kāi)啟lib模式
lib: {
entry, // 打包入口
name // 全局變量名
}
},
plugins: [vue(), vueJsx()]
};
};
export const CWD = process.cwd();
export const C_DIR = resolve(CWD, "components");
可以看到,我們通過(guò)type區(qū)分組件庫(kù)和圖標(biāo)庫(kù)打包。實(shí)際上打包圖標(biāo)庫(kù)和組件庫(kù)都是差不多的,組件庫(kù)需要額外打包國(guó)際化相關(guān)的語(yǔ)言包文件。圖標(biāo)樣式內(nèi)置在組件之中,因此也不需要額外打包。
我們直接通過(guò)第三方庫(kù) vite-plugin-dts 打包圖標(biāo)庫(kù)的聲明文件。
import dts from "vite-plugin-dts";
plugins: [
dts({
outputDir: "es",
entryRoot: C_DIR
})
]
關(guān)于打包原理可參考插件作者的這片文章。
lequ7.com/guan-yu-qia…
我們都知道實(shí)現(xiàn)tree-shaking的一種方式是基于ESM的靜態(tài)性,即在編譯的時(shí)候就能摸清依賴(lài)之間的關(guān)系,對(duì)于"孤兒"會(huì)殘忍的移除。但是對(duì)于import "icon.css"
這種沒(méi)導(dǎo)入導(dǎo)出的模塊,打包工具并不知道它是否具有副作用,索性移除,這樣就導(dǎo)致頁(yè)面缺少樣式了。sideEffects就是npm與構(gòu)建工具聯(lián)合推出的一個(gè)字段,旨在幫助構(gòu)建工具更好的為npm包進(jìn)行tree-shaking。
使用上,sideEffects設(shè)置為false表示所有模塊都沒(méi)有副作用,也可以設(shè)置數(shù)組,每一項(xiàng)可以是具體的模塊名或Glob匹配。因此,實(shí)現(xiàn)圖標(biāo)庫(kù)的按需引入,只需要在argo-icons項(xiàng)目下的package.json里添加以下配置即可:
{
"sideEffects": false,
}
這將告訴構(gòu)建工具,圖標(biāo)庫(kù)沒(méi)有任何副作用,一切沒(méi)有被引入的代碼或模塊都將被移除。前提是你使用的是ESM。
Last but important!當(dāng)圖標(biāo)庫(kù)在被作為npm包導(dǎo)入時(shí),我們需要在package.json為其配置相應(yīng)的入口文件。
{
"main": "lib/index.js", // 以esm形式被引入時(shí)的入口
"module": "es/index.js", // 以commonjs形式被引入時(shí)的入口
"types": "es/index.d.ts" // 指定聲明文件
}
顧名思義,storybook就是一本"書(shū)",講了很多個(gè)"故事"。在這里,"書(shū)"就是argo-icons,我為它講了3個(gè)故事:
基本使用
按需引入
使用iconfont.cn項(xiàng)目
新建@argo-design/ui-storybook
package,并在該目錄下運(yùn)行:
npx storybook init -t vue3 -b webpack5
-t (即--type): 指定項(xiàng)目類(lèi)型,storybook會(huì)根據(jù)項(xiàng)目依賴(lài)及配置文件等推算項(xiàng)目類(lèi)型,但顯然我們僅僅是通過(guò)npm init新創(chuàng)建的項(xiàng)目,storybook無(wú)法自動(dòng)判斷項(xiàng)目類(lèi)型,故需要指定type為vue3,然后storybook會(huì)幫我們初始化storybook vue3 app。
-b (--builder): 指定構(gòu)建工具,默認(rèn)是webpack4,另外支持webpack5, vite。這里指定webpack5,否則后續(xù)會(huì)有類(lèi)似報(bào)錯(cuò):cannot read property of undefine(reading 'get')...因?yàn)閟torybook默認(rèn)以webpack4構(gòu)建,但是@storybook/vue3
依賴(lài)webpack5,會(huì)沖突導(dǎo)致報(bào)錯(cuò)。這里是天坑??!
storybook默認(rèn)使用yarn安裝,如需指定npm請(qǐng)使用--use-npm。
這行命令主要幫我們做以下事情:
注入必要的依賴(lài)到packages.json(如若沒(méi)有指定-s,將幫我們自動(dòng)安裝依賴(lài))。
注入啟動(dòng),打包項(xiàng)目的腳本。
添加Storybook配置,詳見(jiàn).storybook目錄。
添加Story范例文件以幫助我們上手,詳見(jiàn)stories目錄。
其中1,2步具體代碼如下:
{
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"devDependencies": {
"@storybook/vue3": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"vue-loader": "^16.8.3",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@babel/core": "^7.19.6",
"babel-loader": "^8.2.5"
}
}
接下來(lái)把目光放到.storybook下的main.js與preview.js
preview.js可以具名導(dǎo)出parameters,decorators,argTypes,用于全局配置UI(stories,界面,控件等)的渲染行為。比如默認(rèn)配置中的controls.matchers:
export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
}
}
};
它定義了如果屬性值是以background或color結(jié)尾,那么將為其啟用color控件,我們可以選擇或輸入顏色值,date同理。
除此之外你可以在這里引入全局樣式,注冊(cè)組件等等。更多詳情見(jiàn)官網(wǎng) Configure story rendering
最后來(lái)看看最重要的項(xiàng)目配置文件。
module.exports = {
stories: [
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
framework: "@storybook/vue3",
core: {
builder: "@storybook/builder-webpack5"
},
}
stories, 即查找stroy文件的Glob。
addons, 配置需要的擴(kuò)展。慶幸的是,當(dāng)前一些重要的擴(kuò)展都已經(jīng)集成到@storybook/addon-essentials。
framework和core即是我們初識(shí)化傳遞的-t vue3 -b webpack5
。
更多詳情見(jiàn)官網(wǎng) Configure your Storybook project
由于項(xiàng)目使用到less因此我們需要配置一下less,安裝less以及相關(guān)loader。來(lái)到.storybook/main.js
module.exports = {
webpackFinal: (config) => {
config.module.rules.push({
test: /.less$/,
use: [
{
loader: "style-loader"
},
{
loader: "css-loader"
},
{
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true
}
}
}
]
});
return config;
},
}
storybook默認(rèn)支持解析jsx/tsx,但你如果需要使用jsx書(shū)寫(xiě)vue3的stories,仍需要安裝相關(guān)插件。
在argo-ui-storybook下安裝 @vue/babel-plugin-jsx
yarn add @vue/babel-plugin-jsx -D
新建.babelrc
{
"plugins": ["@vue/babel-plugin-jsx"]
}
關(guān)于如何書(shū)寫(xiě)story,篇幅受限,請(qǐng)自行查閱范例文件或官網(wǎng)。
配置完后終端執(zhí)行yarn storybook
即可啟動(dòng)我們的項(xiàng)目,辛苦的成果也將躍然紙上。
對(duì)于UI,在我們的組件庫(kù)逐漸豐富之后,將會(huì)自建一個(gè)獨(dú)具組件庫(kù)風(fēng)格的文檔站點(diǎn),拭目以待。
在Vue2時(shí)代,組件跨層級(jí)通信方式可謂“百花齊放”,provide/inject就是其中一種。時(shí)至今日,在composition,es6,ts加持下,provide/inject可以更加大展身手。
在創(chuàng)建組件實(shí)例時(shí),會(huì)在自身掛載一個(gè)provides對(duì)象,默認(rèn)指向父實(shí)例的provides。
const instance = {
provides: parent ? parent.provides : Object.create(appContext.provides)
}
appContext.provides即createApp創(chuàng)建的app的provides屬性,默認(rèn)是null
在自身需要為子組件供數(shù)據(jù)時(shí),即調(diào)用provide()時(shí),會(huì)創(chuàng)建一個(gè)新對(duì)象,該對(duì)象的原型指向父實(shí)例的provides,同時(shí)將provide提供的選項(xiàng)添加到新對(duì)象上,這個(gè)新對(duì)象就是實(shí)例新的provides值。代碼簡(jiǎn)化就是
function provide(key, value) {
const parentProvides = currentInstance.parent && currentInstance.parent.provides;
const newObj = Object.create(parentProvides);
currentInstance.provides = newObj;
newObj[key] = value;
}
而inject的實(shí)現(xiàn)原理則時(shí)通過(guò)key去查找祖先provides對(duì)應(yīng)的值:
function inject(key, defaultValue) {
const instance = currentInstance;
const provides = instance.parent == null
? instance.vnode.appContent && instance.vnode.appContent.provides
: instance.parent.provides;
if(provides && key in provides) {
return provides[key]
}
}
你可能會(huì)疑惑,為什么這里是直接去查父組件,而不是先查自身實(shí)例的provides呢?前面不是說(shuō)實(shí)例的provides默認(rèn)指向父實(shí)例的provides么。但是請(qǐng)注意,是“默認(rèn)”。如果當(dāng)前實(shí)例執(zhí)行了provide()是不是把instance.provides“污染”了呢?這時(shí)再執(zhí)行inject(key),如果provide(key)的key與你inject的key一致,就從當(dāng)前實(shí)例provides取key對(duì)應(yīng)的值了,而不是取父實(shí)例的provides!
最后,我畫(huà)了2張圖幫助大家理解
篇幅有限,本文不會(huì)對(duì)組件的具體實(shí)現(xiàn)講解哦,簡(jiǎn)單介紹下文件
__demo__組件使用事例
constants.ts定義的常量
context.ts上下文相關(guān)
interface.ts組件接口
TEMPLATE.md用于生成README.md的模版
button/style下存放組件樣式
style下存放全局樣式
關(guān)于打包組件的esm
與commonjs
模塊在之前打包圖標(biāo)庫(kù)章節(jié)已經(jīng)做了介紹,這里不再贅述。
相對(duì)于圖標(biāo)庫(kù),組件庫(kù)的打包需要額外打包樣式文件,大概流程如下:
生成總?cè)肟赾omponents/index.less并編譯成css。
編譯組件less。
生成dist下的argo.css與argo.min.css。
構(gòu)建組件style/index.ts。
import path from "path";
import { outputFileSync } from "fs-extra";
import glob from "glob";
export const CWD = process.cwd();
export const C_DIR = path.resolve(CWD, "components");
export const lessgen = async () => {
let lessContent = `@import "./style/index.less";\n`; // 全局樣式文件
const lessFiles = glob.sync("**/style/index.less", {
cwd: C_DIR,
ignore: ["style/index.less"]
});
lessFiles.forEach((value) => {
lessContent += `@import "./${value}";\n`;
});
outputFileSync(path.resolve(C_DIR, "index.less"), lessContent);
log.success("genless", "generate index.less success!");
};
代碼很簡(jiǎn)單,值得一提就是為什么不將lessContent初始化為空,glob中將ignore移除,這不是更簡(jiǎn)潔嗎。這是因?yàn)閟tyle/index.less作為全局樣式,我希望它在引用的最頂部。最終將會(huì)在components目錄下生成index.less
內(nèi)容如下:
@import "./style/index.less";
@import "./button/style/index.less";
/* other less of components */
import path from "path";
import { readFile, copySync } from "fs-extra"
import { render } from "less";
export const ES_DIR = path.resolve(CWD, "es");
export const LIB_DIR = path.resolve(CWD, "lib");
const less2css = (lessPath: string): string => {
const source = await readFile(lessPath, "utf-8");
const { css } = await render(source, { filename: lessPath });
return css;
}
const files = glob.sync("**/*.{less,js}", {
cwd: C_DIR
});
for (const filename of files) {
const lessPath = path.resolve(C_DIR, `${filename}`);
// less文件拷貝到es和lib相對(duì)應(yīng)目錄下
copySync(lessPath, path.resolve(ES_DIR, `${filename}`));
copySync(lessPath, path.resolve(LIB_DIR, `${filename}`));
// 組件樣式/總?cè)肟谖募?全局樣式的入口文件編譯成css
if (/index.less$/.test(filename)) {
const cssFilename = filename.replace(".less", ".css");
const ES_DEST = path.resolve(ES_DIR, `${cssFilename}`);
const LIB_DEST = path.resolve(LIB_DIR, `${cssFilename}`);
const css = await less2css(lessPath);
writeFileSync(ES_DEST, css, "utf-8");
writeFileSync(LIB_DEST, css, "utf-8");
}
}
import path from "path";
import CleanCSS, { Output } from "clean-css";
import { ensureDirSync } from "fs-extra";
export const DIST_DIR = path.resolve(CWD, "dist");
console.log("start build components/index.less to dist/argo(.min).css");
const indexCssPath = path.resolve(ES_DIR, "index.css");
const css = readFileSync(indexCssPath, "utf8");
const minContent: Output = new CleanCSS().minify(css);
ensureDirSync(DIST_DIR);
writeFileSync(path.resolve("dist/argo.css"), css);
writeFileSync(path.resolve("dist/argo.min.css"), minContent.styles);
log.success(`build components/index.less to dist/argo(.min).css`);
其中最重要的就是使用clean-css
壓縮css。
如果你使用過(guò)babel-plugin-import
,那一定熟悉這項(xiàng)配置:
["import", { "libraryName": "antd", "style": true }]: import js and css modularly (LESS/Sass source files)
["import", { "libraryName": "antd", "style": "css" }]: import js and css modularly (css built files)
通過(guò)指定style: true,babel-plugin-import
可以幫助我們自動(dòng)引入組件的less文件,如果你擔(dān)心less文件定義的變量會(huì)被覆蓋或沖突,可以指定'css',即可引入組件的css文件樣式。
這一步就是要接入這點(diǎn)。但目前不是很必要,且涉及到vite插件
開(kāi)發(fā),暫可略過(guò),后面會(huì)講。
來(lái)看看最終實(shí)現(xiàn)的樣子。
其中button/style/index.js
內(nèi)容也就是導(dǎo)入less:
import "../../style/index.less";
import "./index.less";
button/style/css.js
內(nèi)容也就是導(dǎo)入css:
import "../../style/index.css";
import "./index.css";
最后你可能會(huì)好奇,諸如上面提及的compileComponent
,compileStyle
等函數(shù)是如何被調(diào)度使用的,這其實(shí)都?xì)w功于腳手架@argo-design/scripts
。當(dāng)它作為依賴(lài)被安裝到項(xiàng)目中時(shí),會(huì)為我們提供諸多命令如argo-scripts genicon
,argo-scripts compileComponent
等,這些函數(shù)都在執(zhí)行命令時(shí)被調(diào)用。
"sideEffects": [
"dist/*",
"es/**/style/*",
"lib/**/style/*",
"*.less"
]
// locale.ts
import { ref, reactive, computed, inject } from "vue";
import { isString } from "../_utils/is";
import zhCN from "./lang/zh-cn";
export interface ArgoLang {
locale: string;
button: {
defaultText: string;
}
}
type ArgoI18nMessages = Record<string, ArgoLang>;
// 默認(rèn)使用中文
const LOCALE = ref("zh-CN");
const I18N_MESSAGES = reactive<ArgoI18nMessages>({
"zh-CN": zhCN
});
// 添加語(yǔ)言包
export const addI18nMessages = (
messages: ArgoI18nMessages,
options?: {
overwrite?: boolean;
}
) => {
for (const key of Object.keys(messages)) {
if (!I18N_MESSAGES[key] || options?.overwrite) {
I18N_MESSAGES[key] = messages[key];
}
}
};
// 切換語(yǔ)言包
export const useLocale = (locale: string) => {
if (!I18N_MESSAGES[locale]) {
console.warn(`use ${locale} failed! Please add ${locale} first`);
return;
}
LOCALE.value = locale;
};
// 獲取當(dāng)前語(yǔ)言
export const getLocale = () => {
return LOCALE.value;
};
export const useI18n = () => {
const i18nMessage = computed<ArgoLang>(() => I18N_MESSAGES[LOCALE.value]);
const locale = computed(() => i18nMessage.value.locale);
const transform = (key: string): string => {
const keyArray = key.split(".");
let temp: any = i18nMessage.value;
for (const keyItem of keyArray) {
if (!temp[keyItem]) {
return key;
}
temp = temp[keyItem];
}
return temp;
};
return {
locale,
t: transform
};
};
添加需要支持的語(yǔ)言包,這里默認(rèn)支持中文和英文。
// lang/zh-CN.ts
const lang: ArgoLang = {
locale: "zh-CN",
button: {
defaultText: "按鈕"
},
}
// lang/en-US.ts
const lang: ArgoLang = {
locale: "en-US",
button: {
defaultText: "Button",
},
}
button組件中接入
<template>
<button>
<slot> {{ t("button.defaultText") }} </slot>
</button>
</template>
<script>
import { defineComponent } from "vue";
import { useI18n } from "../locale";
export default defineComponent({
name: "Button",
setup(props, { emit }) {
const { t } = useI18n();
return {
t
};
}
});
</script>
Button的國(guó)際化僅做演示,實(shí)際上國(guó)際化在日期日歷等組件中才有用武之地。
argo-ui-storybook/stories中添加locale.stories.ts
import { computed } from "vue";
import { Meta, StoryFn } from "@storybook/vue3";
import {
Button,
addI18nMessages,
useLocale,
getLocale
} from "@argo-design/argo-ui/components/index"; // 源文件形式引入方便開(kāi)發(fā)時(shí)調(diào)試
import enUS from "@argo-design/argo-ui/components/locale/lang/en-us";
interface Args {}
export default {
title: "Component/locale",
argTypes: {}
} as Meta<Args>;
const BasicTemplate: StoryFn<Args> = (args) => {
return {
components: { Button },
setup() {
addI18nMessages({ "en-US": enUS });
const currentLang = computed(() => getLocale());
const changeLang = () => {
const lang = getLocale();
if (lang === "en-US") {
useLocale("zh-CN");
} else {
useLocale("en-US");
}
};
return { args, changeLang, currentLang };
},
template: `
<h2>內(nèi)部切換語(yǔ)言,當(dāng)前語(yǔ)言: {{currentLang}}</h2>
<p>僅在未提供ConfigProvider時(shí)生效</p>
<Button type="primary" @click="changeLang">點(diǎn)擊切換語(yǔ)言</Button>
<Button long style="marginTop: 20px;"></Button>
`
};
};
export const Basic = BasicTemplate.bind({});
Basic.storyName = "基本使用";
Basic.args = {};
.preview.js
中全局引入組件庫(kù)樣式
import "@argo-design/argo-ui/components/index.less";
終端啟動(dòng)項(xiàng)目就可以看到效果了。
通常組件庫(kù)都會(huì)提供config-provider組件來(lái)使用國(guó)際化,就像下面這樣
<template>
<a-config-provider :locale="enUS">
<a-button />
</a-config-provider>
</template>
下面我們來(lái)實(shí)現(xiàn)一下config-provider
組件:
<template>
<slot />
</template>
<script>
import type { PropType } from "vue";
import {
defineComponent,
provide,
reactive,
toRefs,
} from "vue";
import { configProviderInjectionKey } from "./context";
export default defineComponent({
name: "ConfigProvider",
props: {
locale: {
type: Object as PropType<ArgoLang>
},
},
setup(props, { slots }) {
const { locale } = toRefs(props);
const config = reactive({
locale,
});
provide(configProviderInjectionKey, config);
}
});
</script>
export interface ConfigProvider {
locale?: ArgoLang;
}
export const configProviderInjectionKey: InjectionKey<ConfigProvider> =
Symbol("ArgoConfigProvider");
修改locale/index.ts中計(jì)算屬性i18nMessage
的獲取邏輯
import { configProviderInjectionKey } from "../config-provider/context";
export const useI18n = () => {
const configProvider = inject(configProviderInjectionKey, undefined);
const i18nMessage = computed<ArgoLang>(
() => configProvider?.locale ?? I18N_MESSAGES[LOCALE.value]
);
const locale = computed(() => i18nMessage.value.locale);
const transform = (key: string): string => {
const keyArray = key.split(".");
let temp: any = i18nMessage.value;
for (const keyItem of keyArray) {
if (!temp[keyItem]) {
return key;
}
temp = temp[keyItem];
}
return temp;
};
return {
locale,
t: transform
};
};
編寫(xiě)stories驗(yàn)證一下:
const ProviderTemplate: StoryFn<Args> = (args) => {
return {
components: { Button, ConfigProvider },
render() {
return (
<ConfigProvider {...args}>
<Button long={true} />
</ConfigProvider>
);
}
};
};
export const Provider = ProviderTemplate.bind({});
Provider.storyName = "在config-provider中使用";
Provider.args = {
// 在這里把enUS傳給ConfigProvider的locale
locale: enUS
};
以上stories使用到了jsx,請(qǐng)確保安裝并配置了@vue/babel-plugin-jsx
可以看到,Button默認(rèn)是英文的,表單控件也接收到enUS語(yǔ)言包了,符合預(yù)期。
值得注意的是,上面提到的按需引入只是引入了組件js邏輯代碼,但對(duì)于樣式依然沒(méi)有引入。
下面我們通過(guò)開(kāi)發(fā)vite插件vite-plugin-auto-import-style,讓組件庫(kù)可以自動(dòng)引入組件樣式。
現(xiàn)在我們書(shū)寫(xiě)的代碼如下,現(xiàn)在我們已經(jīng)知道了,這樣僅僅是加載了組件而已。
import { createApp } from "vue";
import App from "./App.vue";
import { Button, Empty, ConfigProvider } from "@argo-design/argo-ui";
import { Anchor } from "@argo-design/argo-ui";
createApp(App)
.use(Button)
.use(Empty)
.use(ConfigProvider)
.use(Anchor)
.mount("#root");
添加插件之前:
添加插件之后:
import { defineConfig } from "vite";
import argoAutoInjectStyle from 'vite-plugin-argo-auto-inject-style';
export default defineConfig({
plugins: [
argoAutoInjectStyle({
libs: [
{
libraryName: "@argo-design/argo-ui",
resolveStyle: (name) => {
return `@argo-design/argo-ui/es/${name}/style/index.js`;
}
}
]
})
]
})
實(shí)踐之前瀏覽一遍官網(wǎng)插件介紹是個(gè)不錯(cuò)的選擇。插件API
vite插件是一個(gè)對(duì)象,通常由name
和一系列鉤子函數(shù)
組成:
{
name: "vite-plugin-vue-auto-inject-style",
configResolved(config) {}
}
在vite.config.ts
被解析完成后觸發(fā)。常用于擴(kuò)展配置??梢灾苯釉赾onfig上定義或返回一個(gè)對(duì)象,該對(duì)象會(huì)嘗試與配置文件vite.config.ts
中導(dǎo)出的配置對(duì)象深度合并。
在解析完所有配置時(shí)觸發(fā)。形參config
表示最終確定的配置對(duì)象。通常將該配置保存起來(lái)在有需要時(shí)提供給其它鉤子使用。
開(kāi)發(fā)階段每個(gè)傳入模塊請(qǐng)求時(shí)被調(diào)用,常用于解析模塊路徑。返回string或?qū)ο髮⒔K止后續(xù)插件的resolveId鉤子執(zhí)行。
resolveId之后調(diào)用,可自定義模塊加載內(nèi)容
load之后調(diào)用,可自定義修改模塊內(nèi)容。這是一個(gè)串行鉤子,即多個(gè)插件實(shí)現(xiàn)了這個(gè)鉤子,下個(gè)插件的transform需要等待上個(gè)插件的transform鉤子執(zhí)行完畢。上個(gè)transform返回的內(nèi)容將傳給下個(gè)transform鉤子。
為了讓插件完成自動(dòng)引入組件樣式,我們需要完成如下工作:
過(guò)濾出我們想要的文件。
對(duì)文件內(nèi)容進(jìn)行AST解析,將符合條件的import語(yǔ)句提取出來(lái)。
然后解析出具體import的組件。
最后根據(jù)組件查找到樣式文件路徑,生成導(dǎo)入樣式的語(yǔ)句字符串追加到import語(yǔ)句后面即可。
其中過(guò)濾我們使用rollup提供的工具函數(shù)createFilter;
AST解析借助es-module-lexer
,非常出名,千萬(wàn)級(jí)周下載量。
import type { Plugin } from "vite";
import { createFilter } from "@rollup/pluginutils";
import { ExportSpecifier, ImportSpecifier, init, parse } from "es-module-lexer";
import MagicString from "magic-string";
import * as changeCase from "change-case";
import { Lib, VitePluginOptions } from "./types";
const asRE = /\s+as\s+\w+,?/g;
// 插件本質(zhì)是一個(gè)對(duì)象,但為了接受在配置時(shí)傳遞的參數(shù),我們通常在一個(gè)函數(shù)中將其返回。
// 插件默認(rèn)開(kāi)發(fā)和構(gòu)建階段都會(huì)應(yīng)用
export default function(options: VitePluginOptions): Plugin {
const {
libs,
include = ["**/*.vue", "**/*.ts", "**/*.tsx"],
exclude = "node_modules/**"
} = options;
const filter = createFilter(include, exclude);
return {
name: "vite:argo-auto-inject-style",
async transform(code: string, id: string) {
if (!filter(id) || !code || !needTransform(code, libs)) {
return null;
}
await init;
let imports: readonly ImportSpecifier[] = [];
imports = parse(code)[0];
if (!imports.length) {
return null;
}
let s: MagicString | undefined;
const str = () => s || (s = new MagicString(code));
for (let index = 0; index < imports.length; index++) {
// ss import語(yǔ)句開(kāi)始索引
// se import語(yǔ)句介結(jié)束索引
const { n: moduleName, se, ss } = imports[index];
if (!moduleName) continue;
const lib = getLib(moduleName, libs);
if (!lib) continue;
// 整條import語(yǔ)句
const importStr = code.slice(ss, se);
// 拿到每條import語(yǔ)句導(dǎo)入的組件集合
const importItems = getImportItems(importStr);
let endIndex = se + 1;
for (const item of importItems) {
const componentName = item.n;
const paramName = changeCase.paramCase(componentName);
const cssImportStr = `\nimport "${lib.resolveStyle(paramName)}";`;
str().appendRight(endIndex, cssImportStr);
}
}
return {
code: str().toString()
};
}
};
}
export type { Lib, VitePluginOptions };
function getLib(libraryName: string, libs: Lib[]) {
return libs.find((item) => item.libraryName === libraryName);
}
function getImportItems(importStr: string) {
if (!importStr) {
return [];
}
const matchItem = importStr.match(/{(.+?)}/gs);
const formItem = importStr.match(/from.+/gs);
if (!matchItem) return [];
const exportStr = `export ${matchItem[0].replace(asRE, ",")} ${formItem}`;
let importItems: readonly ExportSpecifier[] = [];
try {
importItems = parse(exportStr)[1];
} catch (error) {
console.log(error);
}
return importItems;
}
function needTransform(code: string, libs: Lib[]) {
return libs.some(({ libraryName }) => {
return new RegExp(`('${libraryName}')|("${libraryName}")`).test(code);
});
}
export interface Lib {
libraryName: string;
resolveStyle: (name: string) => string;
}
export type RegOptions =
| string
| RegExp
| Array<string | RegExp>
| null
| undefined;
export interface VitePluginOptions {
include?: RegOptions;
exclude?: RegOptions;
libs: Lib[];
}
在我們的less樣式中,會(huì)定義一系列如下的顏色梯度變量,其值由color-palette函數(shù)完成:
@blue-6: #3491fa;
@blue-1: color-palette(@blue-6, 1);
@blue-2: color-palette(@blue-6, 2);
@blue-3: color-palette(@blue-6, 3);
@blue-4: color-palette(@blue-6, 4);
@blue-5: color-palette(@blue-6, 5);
@blue-7: color-palette(@blue-6, 7);
@blue-8: color-palette(@blue-6, 8);
@blue-9: color-palette(@blue-6, 9);
@blue-10: color-palette(@blue-6, 10);
基于此,我們?cè)傺莼鼍唧w場(chǎng)景下的顏色梯度變量:
@primary-1: @blue-1;
@primary-2: @blue-2;
@primary-3: @blue-3;
// 以此類(lèi)推...
@success-1: @green-1;
@success-2: @green-2;
@success-3: @green-3;
// 以此類(lèi)推...
/* @warn @danger @info等等 */
有了具體場(chǎng)景下的顏色梯度變量,我們就可以設(shè)計(jì)變量供給組件消費(fèi)了:
@color-primary-1: @primary-1;
@color-primary-2: @primary-2;
@color-primary-3: @primary-3;
/* ... */
.argo-btn.arco-btn-primary {
color: #fff;
background-color: @color-primary-1;
}
在使用組件庫(kù)的項(xiàng)目中我們通過(guò) Less 的 ·modifyVars
功能修改變量值:
// webpack.config.js
module.exports = {
rules: [{
test: /.less$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader',
}, {
loader: 'less-loader',
options: {
lessOptions: {
modifyVars: {
'primary-6': '#f85959',
},
javascriptEnabled: true,
},
},
}],
}],
}
// vite.config.js
export default {
css: {
preprocessorOptions: {
less: {
modifyVars: {
'primary-6': '#f85959',
},
javascriptEnabled: true,
}
}
},
}
首先,顏色梯度變量需要增加暗黑風(fēng)格。也是基于@blue-6
計(jì)算,只不過(guò)這里換成了dark-color-palette
函數(shù):
@dark-blue-1: dark-color-palette(@blue-6, 1);
@dark-blue-2: dark-color-palette(@blue-6, 2);
@dark-blue-3: dark-color-palette(@blue-6, 3);
@dark-blue-4: dark-color-palette(@blue-6, 4);
@dark-blue-5: dark-color-palette(@blue-6, 5);
@dark-blue-6: dark-color-palette(@blue-6, 6);
@dark-blue-7: dark-color-palette(@blue-6, 7);
@dark-blue-8: dark-color-palette(@blue-6, 8);
@dark-blue-9: dark-color-palette(@blue-6, 9);
@dark-blue-10: dark-color-palette(@blue-6, 10);
然后,在相應(yīng)節(jié)點(diǎn)下掛載css變量
body {
--color-bg: #fff;
--color-text: #000;
--primary-6: @primary-6;
}
body[argo-theme="dark"] {
--color-bg: #000;
--color-text: #fff;
--primary-6: @dark-primary-6;
}
緊接著,組件消費(fèi)的less變量更改為css變量:
.argo-btn.argo-btn-primary {
color: #fff;
background-color: var(--primary-6);
}
此外,我們還設(shè)置了--color-bg,--color-text等用于設(shè)置body色調(diào):
body {
color: var(--color-bg);
background-color: var(--color-text);
}
最后,在消費(fèi)組件庫(kù)的項(xiàng)目中,通過(guò)編輯body的argo-theme屬性即可切換亮暗模式:
// 設(shè)置為暗黑模式
document.body.setAttribute('argo-theme', 'dark')
// 恢復(fù)亮色模式
document.body.removeAttribute('argo-theme');
前面介紹的是在項(xiàng)目打包時(shí)通過(guò)less配置修改less變量值達(dá)到換膚效果,有了css變量,我們可以實(shí)現(xiàn)在線(xiàn)動(dòng)態(tài)換膚。默認(rèn)的,打包過(guò)后樣式如下:
body {
--primary-6: '#3491fa'
}
.argo-btn {
color: #fff;
background-color: var(--primary-6);
}
在用戶(hù)選擇相應(yīng)顏色后,我們只需要更改css變量--primary-6的值即可:
// 可計(jì)算selectedColor的10個(gè)顏色梯度值列表,并逐一替換
document.body.style.setProperty('--primary-6', colorPalette(selectedColor, 6));
// ....
還記得每個(gè)組件目錄下的TEMPLATE.md文件嗎?
## zh-CN
```yaml
meta:
type: 組件
category: 通用
title: 按鈕 Button
description: 按鈕是一種命令組件,可發(fā)起一個(gè)即時(shí)操作。
```
---
## en-US
```yaml
meta:
type: Component
category: Common
title: Button
description: Button is a command component that can initiate an instant operation.
```
---
@import ./__demo__/basic.md
@import ./__demo__/disabled.md
## API
%%API(button.vue)%%
## TS
%%TS(interface.ts)%%
它是如何一步步被渲染出我們想要的界面呢?
TEMPLATE.md將被解析并生成中英文版READE.md(組件使用文檔),之后在vue-router中被加載使用。
這時(shí)當(dāng)我們?cè)L問(wèn)路由/button,vite服務(wù)器將接管并調(diào)用一系列插件解析成瀏覽器識(shí)別的代碼,最后由瀏覽器渲染出我們的文檔界面。
簡(jiǎn)單起見(jiàn),我們忽略國(guó)際化和使用例子部分。
%%API(button.vue)%%
%%INTERFACE(interface.ts)%%
其中button.vue就是我們的組件,interface.ts就是定義組件的一些接口,比如ButtonProps,ButtonType等。
大致流程如下:
讀取TEMPLATE.md,正則匹配出button.vue;
使用vue-doc-api解析vue文件; let componentDocJson = VueDocApi.parse(path.resolve(__dirname, "button.vue"));
componentDocJson轉(zhuǎn)換成md字符串,md字符串替換掉占位符%%API(button.vue)%%,寫(xiě)入README.md;
關(guān)于vue文件與解析出來(lái)的conponentDocJson結(jié)構(gòu)見(jiàn) vue-docgen-api
由于VueDocApi.parse無(wú)法直接解析.ts文件,因此借助ts-morph
解析ts文件并轉(zhuǎn)換成componentDocJson結(jié)構(gòu)的JSON對(duì)象,再將componentDocJson轉(zhuǎn)換成md字符串,替換掉占位符后最終寫(xiě)入README.md;
讀取TEMPLATE.md,正則匹配出interface.ts;
使用ts-morph解析inerface.ts出interfaces;
interfaces轉(zhuǎn)componentDocJson;
componentDocJson轉(zhuǎn)換成md字符串,md字符串替換掉占位符%%API(button.vue)%%,寫(xiě)入README.md;
import { Project } from "ts-morph";
const project = new Project();
project.addSourceFileAtPath(filepath);
const sourceFile = project.getSourceFile(filepath);
const interfaces = sourceFile.getInterfaces();
const componentDocList = [];
interfaces.forEach((interfaceDeclaration) => {
const properties = interfaceDeclaration.getProperties();
const componentDocJson = {
displayName: interfaceDeclaration.getName(),
exportName: interfaceDeclaration.getName(),
props: formatterProps(properties),
tags: {}
};
if (componentDocJson.props.length) {
componentDocList.push(componentDocJson);
}
});
// genMd(componentDocList);
最終生成README.zh-CN.md如下
```yaml
meta:
type: 組件
category: 通用
title: 按鈕 Button
description: 按鈕是一種命令組件,可發(fā)起一個(gè)即時(shí)操作。
```
@import ./__demo__/basic.md
@import ./__demo__/disabled.md
## API
### `<button>` Props
|參數(shù)名|描述|類(lèi)型|默認(rèn)值|
|---|---|---|:---:|
|type|按鈕的類(lèi)型,分為五種:次要按鈕、主要按鈕、虛框按鈕、線(xiàn)性按鈕、文字按鈕。|`'secondary' | 'primary' | 'dashed' | 'outline' | 'text'`|`"secondary"`|
|shape|按鈕的形狀|`'square' | 'round' | 'circle'`|`"square"`|
|status|按鈕的狀態(tài)|`'normal' | 'warning' | 'success' | 'danger'`|`"normal"`|
|size|按鈕的尺寸|`'mini' | 'small' | 'medium' | 'large'`|`"medium"`|
|long|按鈕的寬度是否隨容器自適應(yīng)。|`boolean`|`false`|
|loading|按鈕是否為加載中狀態(tài)|`boolean`|`false`|
|disabled|按鈕是否禁用|`boolean`|`false`|
|html-type|設(shè)置 `button` 的原生 `type` 屬性,可選值參考 [HTML標(biāo)準(zhǔn)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type "_blank")|`'button' | 'submit' | 'reset'`|`"button"`|
|href|設(shè)置跳轉(zhuǎn)鏈接。設(shè)置此屬性時(shí),按鈕渲染為a標(biāo)簽。|`string`|`-`|
### `<button>` Events
|事件名|描述|參數(shù)|
|---|---|---|
|click|點(diǎn)擊按鈕時(shí)觸發(fā)|event: `Event`|
### `<button>` Slots
|插槽名|描述|參數(shù)|
|---|:---:|---|
|icon|圖標(biāo)|-|
### `<button-group>` Props
|參數(shù)名|描述|類(lèi)型|默認(rèn)值|
|---|---|---|:---:|
|disabled|是否禁用|`boolean`|`false`|
## INTERFACE
### ButtonProps
|參數(shù)名|描述|類(lèi)型|默認(rèn)值|
|---|---|---|:---:|
|type|按鈕類(lèi)型|`ButtonTypes`|`-`|
const Button = () => import("@argo-design/argo-ui/components/button/README.zh-CN.md");
const router = createRouter({
{
path: "/button",
component: Button
}
});
export default router;
首先我們來(lái)看下README.md(為方便直接省略.zh-CN)以及其中的demos.md的樣子與它們最終的UI。
可以看到,README就是一系列demo的集合,而每個(gè)demo都會(huì)被渲染成一個(gè)由代碼示例與代碼示例運(yùn)行結(jié)果組成的代碼塊。
yarn create vite
快速搭建一個(gè)package
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import md from "./plugins/vite-plugin-md/index";
export default defineConfig({
server: {
port: 8002,
},
plugins: [md(), vue()],
});
// App.vue
<template>
<ReadMe />
</template>
<script setup>
import ReadMe from "./readme.md";
</script>
// readme.md
@import ./__demo__/basic.md
開(kāi)發(fā)之前我們先看看插件對(duì)README.md
源碼的解析轉(zhuǎn)換流程。
首先我們來(lái)實(shí)現(xiàn)第一步: 源碼轉(zhuǎn)換。即將
@import "./__demo__/basic.md"
轉(zhuǎn)換成
<template>
<basic-demo />
</template>
<script>
import { defineComponent } from "vue";
import BasicDemo from "./__demo__/basic.md";
export default defineComponent({
name: "ArgoMain",
components: { BasicDemo },
});
</script>
轉(zhuǎn)換過(guò)程我們借助第三方markdown解析工具marked
完成,一個(gè)高速,輕量,無(wú)阻塞,多平臺(tái)的markdown解析器。
眾所周知,md2html規(guī)范中,文本默認(rèn)會(huì)被解析渲染成p標(biāo)簽。也就是說(shuō),README.md里的@import ./__demo__/basic.md
會(huì)被解析渲染成<p>@import ./__demo__/basic.md</p>
,這不是我想要的。所以需要對(duì)marked
進(jìn)行一下小小的擴(kuò)展。
// marked.ts
import { marked } from "marked";
import path from "path";
const mdImport = {
name: "mdImport",
level: "block",
tokenizer(src: string) {
const rule = /^@import\s+(.+)(?:\n|$)/;
const match = rule.exec(src);
if (match) {
const filename = match[1].trim();
const basename = path.basename(filename, ".md");
return {
type: "mdImport",
raw: match[0],
filename,
basename,
};
}
return undefined;
},
renderer(token: any) {
return `<demo-${token.basename} />\n`;
},
};
marked.use({
extensions: [mdImport],
});
export default marked;
我們新建了一個(gè)mdImport
的擴(kuò)展,用來(lái)自定義解析我們的md。在tokenizer 中我們定義了解析規(guī)則并返回一系列自定義的tokens,其中raw就是@import "./__demo__/basic.md"
,filename就是./__demo__/basic.md
,basename就是basic
,我們可以通過(guò)marked.lexer(code)
拿到這些tokens。在renderer中我們自定義了渲染的html,通過(guò)marked.parser(tokens)
可以拿到html字符串了。因此,我們開(kāi)始在插件中完成第一步。
// index.ts
import { Plugin } from "vite";
import marked from "./marked";
export default function vueMdPlugin(): Plugin {
return {
name: "vite:argo-vue-docs",
async transform(code: string, id: string) {
if (!id.endsWith(".md")) {
return null;
}
const tokens = marked.lexer(code);
const html = marked.parser(tokens);
const vueCode = transformMain({ html, tokens });
},
};
}
// vue-template.ts
import changeCase from "change-case";
import marked from "./marked";
export const transformMain = ({
html,
tokens,
}: {
html: string;
tokens: any[];
}): string => {
const imports = [];
const components = [];
for (const token of tokens) {
const componentName = changeCase.pascalCase(`demo-${token.basename}`);
imports.push(`import ${componentName} from "${token.filename}";`);
components.push(componentName);
}
return `
<template>
${html}
</template>
<script>
import { defineComponent } from "vue";
${imports.join("\n")};
export default defineComponent({
name: "ArgoMain",
components: { ${components.join(",")} },
});
</script>
`;
};
其中change-case
是一個(gè)名稱(chēng)格式轉(zhuǎn)換的工具,比如basic-demo轉(zhuǎn)BasicDemo等。
transformMain
返回的vueCode就是我們的目標(biāo)vue模版了。但瀏覽器可不認(rèn)識(shí)vue模版語(yǔ)法,所以我們?nèi)砸獙⑵浣唤o官方插件@vitejs/plugin-vue
的transform
鉤子函數(shù)轉(zhuǎn)換一下。
import { getVueId } from "./utils";
export default function vueMdPlugin(): Plugin {
let vuePlugin: Plugin | undefined;
return {
name: "vite:argo-vue-docs",
configResolved(resolvedConfig) {
vuePlugin = resolvedConfig.plugins.find((p) => p.name === "vite:vue");
},
async transform(code: string, id: string) {
if (!id.endsWith(".md")) {
return null;
}
if (!vuePlugin) {
return this.error("Not found plugin [vite:vue]");
}
const tokens = marked.lexer(code);
const html = marked.parser(tokens);
const vueCode = transformMain({ html, tokens });
return await vuePlugin.transform?.call(this, vueCode, getVueId(id));
},
};
}
// utils.ts
export const getVueId = (id: string) => {
return id.replace(".md", ".vue");
};
這里使用getVueId
修改擴(kuò)展名為.vue是因?yàn)?code>vuePlugin.transform會(huì)對(duì)非vue文件進(jìn)行攔截就像我們上面攔截非md文件一樣。
在configResolved
鉤子函數(shù)中,形參resolvedConfig
是vite最終使用的配置對(duì)象。在該鉤子中拿到其它插件并將其提供給其它鉤子使用,是vite插件開(kāi)發(fā)中的一種“慣用伎倆”了。
在經(jīng)過(guò)vuePlugin.transform
及后續(xù)處理過(guò)后,最終vite服務(wù)器對(duì)readme.md響應(yīng)給瀏覽器的內(nèi)容如下
對(duì)于basic.md?import響應(yīng)如下
可以看到,這一坨字符串可沒(méi)有有效的默認(rèn)導(dǎo)出語(yǔ)句。因此對(duì)于解析語(yǔ)句import DemoBasic from "/src/__demo__/basic.md?import";
瀏覽器會(huì)報(bào)錯(cuò)
Uncaught SyntaxError: The requested module '/src/__demo__/basic.md?import' does not provide an export named 'default' (at readme.vue:9:8)
在帶有module屬性的script標(biāo)簽中,每個(gè)import語(yǔ)句都會(huì)向vite服務(wù)器發(fā)起請(qǐng)求進(jìn)而繼續(xù)走到插件的transform鉤子之中。下面我們繼續(xù),對(duì)/src/__demo__/basic.md?import
進(jìn)行攔截處理。
// index.ts
async transform(code: string, id: string) {
if (!id.endsWith(".md")) {
return null;
}
// 新增對(duì)demo文檔的解析分支
if (isDemoMarkdown(id)) {
const tokens = marked.lexer(code);
const vueCode = transformDemo({ tokens, filename: id });
return await vuePlugin.transform?.call(this, vueCode, getVueId(id));
} else {
const tokens = marked.lexer(code);
const html = marked.parser(tokens);
const vueCode = transformMain({ html, tokens });
return await vuePlugin.transform?.call(this, vueCode, getVueId(id));
}
},
// utils.tsexport
const isDemoMarkdown = (id: string) => {
return //__demo__//.test(id);
};
// vue-template.ts
export const transformDemo = ({
tokens,
filename,
}: {
tokens: any[];
filename: string;
}) => {
const data = {
html: "",
};
const vueCodeTokens = tokens.filter(token => {
return token.type === "code" && token.lang === "vue"
});
data.html = marked.parser(vueCodeTokens);
return `
<template>
<hr />
${data.html}
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ArgoDemo",
});
</script>
`;
};
現(xiàn)在已經(jīng)可以在瀏覽器中看到結(jié)果了,水平線(xiàn)和示例代碼。
那如何實(shí)現(xiàn)示例代碼的運(yùn)行結(jié)果呢?其實(shí)在對(duì)tokens遍歷(filter)的時(shí)候,我們是可以拿到vue模版字符串的,我們可以將其緩存起來(lái),同時(shí)手動(dòng)構(gòu)造一個(gè)import請(qǐng)求import Result from "${virtualPath}";
這個(gè)請(qǐng)求用于返回運(yùn)行結(jié)果。
export const transformDemo = ({
tokens,
filename,
}: {
tokens: any[];
filename: string;
}) => {
const data = {
html: "",
};
const virtualPath = `/@virtual${filename}`;
const vueCodeTokens = tokens.filter(token => {
const isValid = token.type === "code" && token.lang === "vue"
// 緩存vue模版代碼
isValid && createDescriptor(virtualPath, token.text);
return isValid;
});
data.html = marked.parser(vueCodeTokens);
return `
<template>
<Result />
<hr />
${data.html}
</template>
<script>
import { defineComponent } from "vue";
import Result from "${virtualPath}";
export default defineComponent({
name: "ArgoDemo",
components: {
Result
}
});
</script>
`;
};
// utils.ts
export const isVirtualModule = (id: string) => {
return //@virtual/.test(id);
};
export default function docPlugin(): Plugin {
let vuePlugin: Plugin | undefined;
return {
name: "vite:plugin-doc",
resolveId(id) {
if (isVirtualModule(id)) {
return id;
}
return null;
},
load(id) {
// 遇到虛擬md模塊,直接返回緩存的內(nèi)容
if (isVirtualModule(id)) {
return getDescriptor(id);
}
return null;
},
async transform(code, id) {
if (!id.endsWith(".md")) {
return null;
}
if (isVirtualModule(id)) {
return await vuePlugin.transform?.call(this, code, getVueId(id));
}
// 省略其它代碼...
}
}
}
// cache.ts
const cache = new Map();
export const createDescriptor = (id: string, content: string) => {
cache.set(id, content);
};
export const getDescriptor = (id: string) => {
return cache.get(id);
};
最后為示例代碼加上樣式。安裝prismjs
yarn add prismjs
// marked.ts
import Prism from "prismjs";
import loadLanguages from "prismjs/components/index.js";
const languages = ["shell", "js", "ts", "jsx", "tsx", "less", "diff"];
loadLanguages(languages);
marked.setOptions({
highlight(
code: string,
lang: string,
callback?: (error: any, code?: string) => void
): string | void {
if (languages.includes(lang)) {
return Prism.highlight(code, Prism.languages[lang], lang);
}
return Prism.highlight(code, Prism.languages.html, "html");
},
});
項(xiàng)目入口引入css
// main.ts
import "prismjs/themes/prism.css";
重啟預(yù)覽,以上就是vite-plugin-vue-docs
的核心部分了。
最后回到上文構(gòu)建組件style/index.ts遺留的問(wèn)題,index.ts的內(nèi)容很簡(jiǎn)單,即引入組件樣式。
import "../../style/index.less"; // 全局樣式
import "./index.less"; // 組件樣式復(fù)制代碼
index.ts在經(jīng)過(guò)vite的lib模式
構(gòu)建后,我們?cè)黾觕ss插件,在generateBundle
鉤子中,我們可以對(duì)最終的bundle
進(jìn)行新增,刪除或修改。通過(guò)調(diào)用插件上下文中emitFile
方法,為我們額外生成用于引入css樣式的css.js。
import type { Plugin } from "vite";
import { OutputChunk } from "rollup";
export default function cssjsPlugin(): Plugin {
return {
name: "vite:cssjs",
async generateBundle(outputOptions, bundle) {
for (const filename of Object.keys(bundle)) {
const chunk = bundle[filename] as OutputChunk;
this.emitFile({
type: "asset",
fileName: filename.replace("index.js", "css.js"),
source: chunk.code.replace(/.less/g, ".css")
});
}
}
};
}
讀到這里,這篇“vue組件庫(kù)如何開(kāi)發(fā)使用”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過(guò)才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。