十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
本文小编为大家详细介绍“vue组件库如何开发使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue组件库如何开发使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
为爱民等地区用户提供了全套网页设计制作服务,及爱民网站建设行业解决方案。主营业务为网站制作、成都网站制作、爱民网站设计,以传统方式定制建设网站,并提供域名空间备案等一条龙服务,秉承以专业、用心的态度为用户提供真诚的服务。我们深信只要达到每一位用户的要求,就会得到认可,从而选择与我们长期合作。这样,我们也可以走得更远!
考虑到组件库整体需要有多边资源支持,比如组件源码,库文档站点,color-gen等类库工具,代码规范配置,vite插件,脚手架,storybook等等,需要分出很多packages,package之间存在彼此联系,因此考虑使用monorepo的管理方式,同时使用yarn作为包管理工具,lerna作为包发布工具。
在monorepo之前,根目录就是一个workspace,我们直接通过yarn add/remove/run等就可以对包进行管理。但在monorepo项目中,根目录下存在多个子包,yarn 命令无法直接操作子包,比如根目录下无法通过yarn run dev启动子包package-a中的dev命令,这时我们就需要开启yarn的workspaces功能,每个子包对应一个workspace,之后我们就可以通过yarn workspace package-a run dev
启动package-a中的dev命令了。
你可能会想,我们直接cd到package-a下运行就可以了,不错,但yarn workspaces的用武之地并不只此,像auto link,依赖提升,单.lock等才是它在monorepo中的价值所在。
我们在根目录packge.json中启用yarn workspaces:
{
"private": true,
"workspaces": [
"packages/*"
]
}
packages目录下的每个直接子目录作为一个workspace。由于我们的根项目是不需要发布出去的,因此设置private为true。
不得不说,yarn workspaces已经具备了lerna部分功能,之所以使用它,是想借用它的发布工作流以弥补workspaces在monorepo下在这方面的不足。下面我们开始将lerna集成到项目中。
首先我们先安装一下lerna:
# W指workspace-root,即在项目根目录下安装,下同
yarn add lerna -D -W
# 由于经常使用lerna命令也推荐全局安装
yarn global add lerna
or
npm i lerna -g
执行lerna init
初始化项目,成功之后会帮我们创建了一个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,配置此字段后,鼠标悬浮在属性上会有对应的描述。注意,以上的路径值需要你在项目根目录下安装lerna。
useWorkspaces
定义了在lerna bootstrap
期间是否结合yarn workspace。
由于lerna默认的工作模式是固定模式,即发布时每个包的版本号一致。这里我们修改为independent
独立模式,同时将npm客户端设置为yarn
。如果你喜欢pnpm
,just do it!
// lerna.json
{
"version": "independent",
"npmClient": "yarn"
}
至此yarn workspaces
搭配lerna
的monorepo项目就配置好了,非常简单!
By the way!由于项目会使用commitlint
对提交信息进行校验是否符合Argular规范,而lerna version
默认为我们commit的信息是"Publish",因此我们需要进行一些额外的配置。
// lerna.json
{
"command": {
"version": {
"message": "chore(release): publish",
"conventionalCommits": true
}
}
}
可以看到,我们使用符合Argular团队提交规范的"chore(release): publish"
代替默认的"Publish"。
conventionalCommits
表示当我们运行lerna version
,实际上会运行lerna version --conventional-commits
帮助我们生成CHANGELOG.md。
在lerna刚发布的时候,那时的包管理工具还没有可用的workspaces
解决方案,因此lerna自身实现了一套解决方案。时至今日,现代的包管理工具几乎都内置了workspaces
功能,这使得lerna和yarn有许多功能重叠,比如执行包pkg-a的dev命令lerna run dev --stream --scope=pkg-a
,我们完全可以使用yarn workspace pkg-a run dev
代替。lerna bootstrap --hoist将安装包提升到根目录,而在yarn workspaces中直接运行yarn就可以了。
Anyway, 使用yarn
作为软件包管理工具,lerna
作为软件包发布工具,是在monorepo
管理方式下一个不错的实践!
很无奈,我知道大部分人都不喜欢Lint,但对我而言,这是必须的。
packages目录下创建名为@argo-design/eslint-config(非文件夹名)的package
cd argo-eslint-config
yarn add eslint
npx eslint --init
注意这里没有-D或者--save-dev。选择如下:
安装完成后手动将devDependencies
下的依赖拷贝到dependencies
中。或者你手动安装这一系列依赖。
// argo-eslint-config/package.json
{
scripts: {
"lint:script": "npx eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./"
}
}
运行yarn lint:script
,将会自动修复代码规范错误警告(如果可以的话)。
安装VSCode Eslint插件并进行如下配置,此时在你保存代码时,也会自动修复代码规范错误警告。
// settings.json
{
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
在argo-eslint-config
中新建包入口文件index.js,并将.eslintrc.js的内容拷贝到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
指向我们刚刚创建的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"
]
}
}
最后运行yarn重新安装依赖。
注意包命名与extends书写规则;root表示根配置,对eslint配置文件冒泡查找到此为止。
接下来我们引入formatter工具prettier
。首先我们需要关闭eslint规则中那些与prettier冲突或者不必要的规则,最后由prettier
代为实现这些规则。前者我们通过eslint-config-prettier
实现,后者借助插件eslint-plugin-prettier
实现。比如冲突规则尾逗号,eslint-config-prettier
帮我们屏蔽了与之冲突的eslint规则:
{
"comma-dangle": "off",
"no-comma-dangle": "off",
"@typescript-eslint/comma-dangle": "off",
"vue/comma-dangle": "off",
}
通过配置eslint规则"prettier/prettier": "error"
让错误暴露出来,这些错误交给eslint-plugin-prettier
收拾。
prettier配置我们也新建一个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, //一行的字符数,如果超过会进行换行,默认为80
semi: false, // 行尾是否使用分号,默认为true
trailingComma: 'none', // 是否使用尾逗号
bracketSpacing: true // 对象大括号直接是否有空格
};
完整配置参考官网 prettier配置
回到argo-eslint-config/index.js,只需新增如下一条配置即可
module.exports = {
"extends": ["plugin:prettier/recommended"]
};
plugin:prettier/recommended
指的eslint-plugin-prettier
package下的recommended.js。该扩展已经帮我们配置好了
{
"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"
}
运行yarn重新安装依赖。
// settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
stylelint配置我们也新建一个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
# 单独postcss8
yarn add postcss@^8.0.0
对于结合prettier
这里不在赘述。
stylelint-order
允许我们自定义样式属性名称顺序。而stylelint-config-rational-order
为我们提供了一套合理的开箱即用的顺序。
值得注意的是,stylelint14版本不在默认支持less,sass等预处理语言。并且stylelint14依赖postcss8版本,可能需要单独安装,否则vscode 的stylellint扩展可能提示报错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理应作为盒子模型的一部分 默认false
"empty-line-between-groups": false // 组之间添加空行 默认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"
]
}
}
运行yarn重新安装依赖。
VSCode安装Stylelint扩展并添加配置
// 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之后如不能及时生效,可以重启一下vscode。如果你喜欢,可以将eslint,prettier,stylelint配置安装到全局并集成到编辑器。
为防止一些非法的commit
或push
,我们借助git hooks
工具在对代码提交前进行 ESLint 与 Stylelint的校验,如果校验通过,则成功commit,否则取消commit。
# 在根目录安装husky
yarn add husky -D -W
npm pkg set scripts.prepare="husky install"
npm run prepare
# 添加pre-commit钩子,在提交前运行代码lint
npx husky add .husky/pre-commit "yarn lint"
至此,当我们执行git commit -m "xxx"
时就会先执行lint校验我们的代码,如果lint通过,成功commit,否则终止commit。具体的lint命令请自行添加。
现在,当我们git commit时,会对整个工作区的代码进行lint。当工作区文件过多,lint的速度就会变慢,进而影响开发体验。实际上我们只需要对暂存区中的文件进行lint即可。下面我们引入·lint-staged
解决我们的问题。
在根目录安装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
运行时,将始终向上查找并应用最接近暂存文件的配置,因此我们可以在根目录下的package.json中配置lint-staged。值得注意的是,每个glob匹配的数组中的命令是从左至右依次运行,和webpack的loder应用机制不同!
最后,我们在.husky文件夹中找到pre-commit
,并将yarn lint
修改为npx --no-install lint-staged
。
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged
至此,当我们执行git commit -m "xxx"
时,lint-staged
会如期运行帮我们校验staged(暂存区)中的代码,避免了对工作区的全量检查。
除了代码规范检查之后,Git 提交信息的规范也是不容忽视的一个环节,规范精准的 commit 信息能够方便自己和他人追踪项目和把控进度。这里,我们使用大名鼎鼎的Angular团队提交规范
。
commit message 由 Header
、Body
、Footer
组成。其中Herder时必需的,Body和Footer可选。
Header 部分包括三个字段 type
、scope
和 subject
。
其中type 用于说明 commit 的提交类型(必须是以下几种之一)。
值 | 描述 |
---|---|
feat | Feature) 新增一个功能 |
fix | Bug修复 |
docs | Documentation) 文档相关 |
style | 代码格式(不影响功能,例如空格、分号等格式修正),并非css样式更改 |
refactor | 代码重构 |
perf | Performent) 性能优化 |
test | 测试相关 |
build | 构建相关(例如 scopes: webpack、gulp、npm 等) |
ci | 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 |
chore | 变更构建流程或辅助工具,日常事务 |
revert | git revert |
scope 用于指定本次 commit 影响的范围。
subject 是本次 commit 的简洁描述,通常遵循以下几个规范:
用动词开头,第一人称现在时表述,例如:change 代替 changed 或 changes
第一个字母小写
结尾不加句号.
body 是对本次 commit 的详细描述,可以分成多行。跟 subject 类似。
如果本次提交的代码是突破性的变更或关闭Issue,则 Footer 必需,否则可以省略。
我们可以借助工具帮我们生成规范的message。
yarn add commitizen -D -W
安装适配器
yarn add cz-conventional-changelog -D -W
这行命令做了两件事:
安装cz-conventional-changelog
到开发依赖
在根目录下的package.json中增加了:
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
添加npm scriptscm
"scripts": {
"cm": "cz"
},
至此,执行yarn cm
,就能看到交互界面了!跟着交互一步步操作就能自动生成规范的message了。
首先在根目录安装依赖:
yarn add commitlint @commitlint/cli @commitlint/config-conventional -D -W
接着新建.commitlintrc.js
:
module.exports = {
extends: ["@commitlint/config-conventional"]
};
最后向husky中添加commit-msg
钩子,终端执行:
npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS"
执行成功之后就会在.husky文件夹中看到commit-msg文件了:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint -e
至此,当你提交代码时,如果pre-commit
钩子运行成功,紧接着在commit-msg
钩子中,commitlint会如期运行对我们提交的message进行校验。
关于lint工具的集成到此就告一段落了,在实际开发中,我们还会对lint配置进行一些小改动,比如ignore,相关rules等等。这些和具体项目有关,我们不会变更package里的配置。
千万别投机取巧拷贝别人的配置文件!复制一时爽,代码火葬场。
巧妇难为无米之炊。组件库通常依赖很多图标,因此我们先开发一个支持按需引入的图标库。
假设我们现在已经拿到了一些漂亮的svg图标,我们要做的就是将每一个图标转化生成.vue组件与一个组件入口index.ts文件。然后再生成汇总所有组件的入口文件。比如我们现在有foo.svg与bar.svg两个图标,最终生成的文件及结构如下:
相应的内容如下:
// 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";
之所以这么设计是由图标库最终如何使用决定的,除此之外argoIcon.ts
也将会是打包umd
的入口文件。
// 全量引入import ArgoIcon from "图标库";
app.use(ArgoIcon);
// 按需引入import { Foo } from "图标库";
app.use(Foo);
图标库的整个构建流程大概分为以下3步:
整个流程很简单,我们通过glob匹配到.svg拿到所有svg的路径,对于每一个路径,我们读取svg的原始文本信息交由第三方库svgo处理,期间包括删除无用代码,压缩,自定义属性等,其中最重要的是为svg标签注入我们想要的自定义属性,就像这样:
之后这段svgHtml
会传送给我们预先准备好的摸板字符串:
const template = `
${svgHtml}
`
为摸板字符串填充数据后,通过fs模块的writeFile生成我们想要的.vue文件。
在打包构建方案上直接选择vite为我们提供的lib模式即可,开箱即用,插件扩展(后面会讲到),基于rollup,能帮助我们打包生成ESM,这是按需引入的基础。当然,commonjs
与umd
也是少不了的。整个过程我们通过Vite 的JavaScript API
实现:
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
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", // 产物存放路径
entryFileNames: "[name].js", // 入口模块的产物文件名
preserveModules: true, // 保留模块结构,否则所有模块都将打包在一个bundle文件中
/*
* 保留模块的根路径,该值会在打包后的output.dir中被移除
* 我们的入口是components/index.ts,打包后文件结构为:es/components/index.js
* preserveModulesRoot设为"components",打包后就是:es/index.js
*/
preserveModulesRoot: "components"
},
{
format: "commonjs",
dir: "lib",
entryFileNames: "[name].js",
preserveModules: true,
preserveModulesRoot: "components",
exports: "named" // 导出模式
}
]
},
// 开启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", // 打包产物存放路径
emptyOutDir: true, // 如果outDir在根目录下,则清空outDir
sourcemap: true, // 生成sourcemap
minify: false, // 是否压缩
brotliSize: false, // 禁用 brotli 压缩大小报告。
rollupOptions: { // rollup打包选项
external: "vue", // 匹配到的模块不会被打包到bundle
output: [
{
format: "umd", // umd格式
entryFileNames: `${entryFileName}.js`, // 即bundle名
globals: {
/*
* format为umd/iife时,标记外部依赖vue,打包后以Vue取代
* 未定义时打包结果如下
* var ArgoIcon = function(vue2) {}(vue);
* rollup自动猜测是vue,但实际是Vue.这会导致报错
* 定义后
* var ArgoIcon = function(vue) {}(Vue);
*/
vue: "Vue"
}
},
{
format: "umd",
entryFileNames: `${entryFileName}.min.js`,
globals: {
vue: "Vue"
},
plugins: [terser()] // terser压缩
},
]
},
// 开启lib模式
lib: {
entry, // 打包入口
name // 全局变量名
}
},
plugins: [vue(), vueJsx()]
};
};
export const CWD = process.cwd();
export const C_DIR = resolve(CWD, "components");
可以看到,我们通过type区分组件库和图标库打包。实际上打包图标库和组件库都是差不多的,组件库需要额外打包国际化相关的语言包文件。图标样式内置在组件之中,因此也不需要额外打包。
我们直接通过第三方库 vite-plugin-dts 打包图标库的声明文件。
import dts from "vite-plugin-dts";
plugins: [
dts({
outputDir: "es",
entryRoot: C_DIR
})
]
关于打包原理可参考插件作者的这片文章。
lequ7.com/guan-yu-qia…
我们都知道实现tree-shaking的一种方式是基于ESM的静态性,即在编译的时候就能摸清依赖之间的关系,对于"孤儿"会残忍的移除。但是对于import "icon.css"
这种没导入导出的模块,打包工具并不知道它是否具有副作用,索性移除,这样就导致页面缺少样式了。sideEffects就是npm与构建工具联合推出的一个字段,旨在帮助构建工具更好的为npm包进行tree-shaking。
使用上,sideEffects设置为false表示所有模块都没有副作用,也可以设置数组,每一项可以是具体的模块名或Glob匹配。因此,实现图标库的按需引入,只需要在argo-icons项目下的package.json里添加以下配置即可:
{
"sideEffects": false,
}
这将告诉构建工具,图标库没有任何副作用,一切没有被引入的代码或模块都将被移除。前提是你使用的是ESM。
Last but important!当图标库在被作为npm包导入时,我们需要在package.json为其配置相应的入口文件。
{
"main": "lib/index.js", // 以esm形式被引入时的入口
"module": "es/index.js", // 以commonjs形式被引入时的入口
"types": "es/index.d.ts" // 指定声明文件
}
顾名思义,storybook就是一本"书",讲了很多个"故事"。在这里,"书"就是argo-icons,我为它讲了3个故事:
基本使用
按需引入
使用iconfont.cn项目
新建@argo-design/ui-storybook
package,并在该目录下运行:
npx storybook init -t vue3 -b webpack5
-t (即--type): 指定项目类型,storybook会根据项目依赖及配置文件等推算项目类型,但显然我们仅仅是通过npm init新创建的项目,storybook无法自动判断项目类型,故需要指定type为vue3,然后storybook会帮我们初始化storybook vue3 app。
-b (--builder): 指定构建工具,默认是webpack4,另外支持webpack5, vite。这里指定webpack5,否则后续会有类似报错:cannot read property of undefine(reading 'get')...因为storybook默认以webpack4构建,但是@storybook/vue3
依赖webpack5,会冲突导致报错。这里是天坑!!
storybook默认使用yarn安装,如需指定npm请使用--use-npm。
这行命令主要帮我们做以下事情:
注入必要的依赖到packages.json(如若没有指定-s,将帮我们自动安装依赖)。
注入启动,打包项目的脚本。
添加Storybook配置,详见.storybook目录。
添加Story范例文件以帮助我们上手,详见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"
}
}
接下来把目光放到.storybook下的main.js与preview.js
preview.js可以具名导出parameters,decorators,argTypes,用于全局配置UI(stories,界面,控件等)的渲染行为。比如默认配置中的controls.matchers:
export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
}
}
};
它定义了如果属性值是以background或color结尾,那么将为其启用color控件,我们可以选择或输入颜色值,date同理。
除此之外你可以在这里引入全局样式,注册组件等等。更多详情见官网 Configure story rendering
最后来看看最重要的项目配置文件。
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, 配置需要的扩展。庆幸的是,当前一些重要的扩展都已经集成到@storybook/addon-essentials。
framework和core即是我们初识化传递的-t vue3 -b webpack5
。
更多详情见官网 Configure your Storybook project
由于项目使用到less因此我们需要配置一下less,安装less以及相关loader。来到.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默认支持解析jsx/tsx,但你如果需要使用jsx书写vue3的stories,仍需要安装相关插件。
在argo-ui-storybook下安装 @vue/babel-plugin-jsx
yarn add @vue/babel-plugin-jsx -D
新建.babelrc
{
"plugins": ["@vue/babel-plugin-jsx"]
}
关于如何书写story,篇幅受限,请自行查阅范例文件或官网。
配置完后终端执行yarn storybook
即可启动我们的项目,辛苦的成果也将跃然纸上。
对于UI,在我们的组件库逐渐丰富之后,将会自建一个独具组件库风格的文档站点,拭目以待。
在Vue2时代,组件跨层级通信方式可谓“百花齐放”,provide/inject就是其中一种。时至今日,在composition,es6,ts加持下,provide/inject可以更加大展身手。
在创建组件实例时,会在自身挂载一个provides对象,默认指向父实例的provides。
const instance = {
provides: parent ? parent.provides : Object.create(appContext.provides)
}
appContext.provides即createApp创建的app的provides属性,默认是null
在自身需要为子组件供数据时,即调用provide()时,会创建一个新对象,该对象的原型指向父实例的provides,同时将provide提供的选项添加到新对象上,这个新对象就是实例新的provides值。代码简化就是
function provide(key, value) {
const parentProvides = currentInstance.parent && currentInstance.parent.provides;
const newObj = Object.create(parentProvides);
currentInstance.provides = newObj;
newObj[key] = value;
}
而inject的实现原理则时通过key去查找祖先provides对应的值:
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]
}
}
你可能会疑惑,为什么这里是直接去查父组件,而不是先查自身实例的provides呢?前面不是说实例的provides默认指向父实例的provides么。但是请注意,是“默认”。如果当前实例执行了provide()是不是把instance.provides“污染”了呢?这时再执行inject(key),如果provide(key)的key与你inject的key一致,就从当前实例provides取key对应的值了,而不是取父实例的provides!
最后,我画了2张图帮助大家理解
篇幅有限,本文不会对组件的具体实现讲解哦,简单介绍下文件
__demo__组件使用事例
constants.ts定义的常量
context.ts上下文相关
interface.ts组件接口
TEMPLATE.md用于生成README.md的模版
button/style下存放组件样式
style下存放全局样式
关于打包组件的esm
与commonjs
模块在之前打包图标库章节已经做了介绍,这里不再赘述。
相对于图标库,组件库的打包需要额外打包样式文件,大概流程如下:
生成总入口components/index.less并编译成css。
编译组件less。
生成dist下的argo.css与argo.min.css。
构建组件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!");
};
代码很简单,值得一提就是为什么不将lessContent初始化为空,glob中将ignore移除,这不是更简洁吗。这是因为style/index.less作为全局样式,我希望它在引用的最顶部。最终将会在components目录下生成index.less
内容如下:
@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相对应目录下
copySync(lessPath, path.resolve(ES_DIR, `${filename}`));
copySync(lessPath, path.resolve(LIB_DIR, `${filename}`));
// 组件样式/总入口文件/全局样式的入口文件编译成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。
如果你使用过babel-plugin-import
,那一定熟悉这项配置:
["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)
通过指定style: true,babel-plugin-import
可以帮助我们自动引入组件的less文件,如果你担心less文件定义的变量会被覆盖或冲突,可以指定'css',即可引入组件的css文件样式。
这一步就是要接入这点。但目前不是很必要,且涉及到vite插件
开发,暂可略过,后面会讲。
来看看最终实现的样子。
其中button/style/index.js
内容也就是导入less:
import "../../style/index.less";
import "./index.less";
button/style/css.js
内容也就是导入css:
import "../../style/index.css";
import "./index.css";
最后你可能会好奇,诸如上面提及的compileComponent
,compileStyle
等函数是如何被调度使用的,这其实都归功于脚手架@argo-design/scripts
。当它作为依赖被安装到项目中时,会为我们提供诸多命令如argo-scripts genicon
,argo-scripts compileComponent
等,这些函数都在执行命令时被调用。
"sideEffects": [
"dist/*",
"es/**/style/*",
"lib/**/style/*",
"*.less"
]