基于 Vue3 的组件库 element-plus 正式发布,element-plus 是一个使用 TypeScript + Composition API
重构的全新项目。官方列出了下面几项主要更新,本文会阅读 element-plus
的源码,从以下几个方面在整体和细节上来分析重构之后的源码,建议阅读本文前先 clone 组件代码。
- 使用 TypeScript 开发
- 使用 Vue 3.0 Composition API 降低耦合,简化逻辑
- 使用 Vue 3.0 Teleport 新特性重构挂载类组件
- Vue 2.0 全局 API 切换为 Vue 3.0 实例API
- 国际化处理
- 官方文档网站打包
- 组件库和样式打包
- 使用 Lerna 维护和管理项目
Typescript 相关
element-plus 引入了 typescript, 除了配置对应的 eslint
校验规则、插件,定义 tsconfig.json
之外,打包 es-module
格式组件库的时候的时候使用到了一些 rollup
插件。
- @rollup/plugin-node-resolve
- rollup-plugin-terser
- rollup-plugin-typescript2
- rollup-plugin-vue
// build/rollup.config.bundle.js
import { nodeResolve } from '@rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2'
const vue = require('rollup-plugin-vue')
export default [{
// ... 省略前面部分内容
plugins: [
terser(),
nodeResolve(),
vue({
target: 'browser',
css: false,
exposeFilename: false,
}),
typescript({
tsconfigOverride: {
'include': [
'packages/**/*',
'typings/vue-shim.d.ts',
],
'exclude': [
'node_modules',
'packages/**/__tests__/*',
],
},
}),
],
}]
@rollup/plugin-node-resolve
打包依赖的 npm 包
rollup-plugin-terser
压缩代码
rollup-plugin-vue 打包 vue 文件, css 样式交给了后续会提到的 gulp
来处理。
rollup-plugin-typescript2
是用了编译 typescript
的, 配置中排除了 node-modules 和测试相关文件, include 除了包含组件实现,还包含了 typings/vue-shim.d.ts
文件。
插件中使用到的 typings/vue-shim.d.ts
类型声明文件( 以 .d.ts
结尾的文件会被自动解析 ),定义了一些全局的类型声明,可以直接在 ts 或者 vue 文件中使用这些类型约束变量。还使用扩展模板对 import XX from XX.vue
的引入变量给出类型提示。
// typings/vue-shim.d.ts
declare module '*.vue' {
import { defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent>
export default component
}
declare type Nullable<T> = T | null;
declare type CustomizedHTMLElement<T> = HTMLElement & T
declare type Indexable<T> = {
[key: string]: T
}
declare type Hash<T> = Indexable<T>
declare type TimeoutHandle = ReturnType<typeof global.setTimeout>
declare type ComponentSize = 'large' | 'medium' | 'small' | 'mini'
除了 d.ts
文件之外,element-plus 中对于 props 的类型声明使用了 vue3 的 propType
。以 下面的 Alert 为例, 使用了 PropType 的 props 类型会执行符合我们自定义的规则的构造函数,然后结合 typescript 做类型校验。其他非 props 中的类型声明则是使用了 interface
。
import { PropType } from 'vue'
export default defineComponent({
name: 'ElAlert',
props: {
type: {
type: String as PropType<'success' | 'info' | 'error' | 'warning'>,
default: 'info',
}
}
})
更多 vue3 的 typescript 支持可以查看官方文档
Composition API
官方说明使用了 Vue 3.0 Composition API 降低耦合,简化逻辑。Composition API 的使用和 hooks 的复用 vue-3-playground 中通过一个购物车 demo 的实现提供了一个直观和简洁的示例。
关于常用的 Composition API 的用法,可以查看这篇总结得比较好的文章,快速使用Vue3最新的15个常用API
除了使用新的 Composition API 来改写组件之外,element-plus 中 packages/hooks
目录下抽取了几个可复用的 hooks 文件
以 autocomplete, input 等控件使用到的 use-attrs
为例, 主要做的事情是继承绑定的属性和事件,类似于 $attrs
和 $listener
功能,但是做了一些筛选,去掉了一些不需要继承的属性和事件绑定。
watchEffect(() => {
const res = entries(instance.attrs)
.reduce((acm, [key, val]) => {
if (
!allExcludeKeys.includes(key) &&
!(excludeListeners && LISTENER_PREFIX.test(key))
) {
acm[key] = val
}
return acm
}, {})
attrs.value = res
})
Vue3 中仍然保留了 mixin,我们可以在特定组件或者是全局使用 mixin 来复用逻辑,同时也引入了 hooks 来改善 mixin 存在的一些问题:
Hooks 带来的好处是
Teleport 的使用
element-plus 对几个挂载类组件使用了 vue3 的新特性 Teleport
,这个新特性可以帮我们把其包裹的元素移动到我们指定的节点下。
查看官网我们会发现 Dialog,Drawer,以及使用了 Popper 的 Tooltip 和 Popover 都新增了一个 append-to-body
属性。我们以 Dialog 为例:
appendToBody 为 false, Teleport 会被 disabled, DOM 还是在当前位置渲染,当为 true 时, dialog 中的内容放到了 body 下面。
<template>
<teleport to="body" :disabled="!appendToBody">
<transition
name="dialog-fade"
@after-enter="afterEnter"
@after-leave="afterLeave"
>
...
</transition>
</teleport>
</tamplate>
在原来的 element-ui 中,Tooltip 和 Popover 也是直接放在了 body 中,原来是通过 vue-popper.js 来使用 document.body.appendChild
来添加元素到 body 下的,element-plus 使用 Teleport 来实现相关逻辑。
全局 API - 实例 API
当我们安装好组件库,use 方法会执行 install 方法去全局挂载组件。
我们先来看一下 Vue 2.x element-ui 中全局 API 的写法:
Vue.component 方法绑定全局组件
Vue.use 绑定全局自定义指令
Vue.prototype 绑定全局变量和全局方法
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
// Vue.component 方法绑定全局组件
components.forEach(component => {
Vue.component(component.name, component);
});
// Vue.use 绑定全局自定义指令
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
// Vue.prototype 绑定全局变量和全局方法
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
但是在 vue 3.0 中,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,也就是 createApp 产生的 app 上了, 对应的 API 也做了相应的变化。
再来看使用 Vue 3.0 的 element-plus 中,全局 API 改写成了实例 API。
import type { App } from 'vue'
const plugins = [
ElInfiniteScroll,
ElLoading,
ElMessage,
ElMessageBox,
ElNotification,
]
const install = (app: App, opt: InstallOptions): void => {
const option = Object.assign(defaultInstallOpt, opt)
use(option.locale)
app.config.globalProperties.$ELEMENT = option // 全局设置默认的size属性和z-index属性
// 全局注册所有除了plugins之外的组件
components.forEach(component => {
app.component(component.name, component)
})
plugins.forEach(plugin => {
app.use(plugin as any)
})
}
除此之外写法上有一些不同的是,消息类组件添加 $ 全局方法在 element-plus 中被移动到了 index.ts
里面,
几个消息通知类型的组件都放到了 plugins,使用 app.use
会调用对应组件 index.ts 中的 install 方法,代码如下:
(Message as any).install = (app: App): void => {
app.config.globalProperties.$message = Message
}
国际化
packages 下有一个 locale 文件夹,控制语言切换
packages/locale/index.ts
中抛出了 2 个方法,方法 t
和方法 use
, t
控制 vue
文件中文本的翻译替换,use
方法修改全局语言
// packages/locale/index.ts
export const t = (path:string, option?): string => {
let value
const array = path.split('.')
let current = lang
for (let i = 0, j = array.length; i < j; i++) {
const property = array[i]
value = current[property]
if (i === j - 1) return template(value, option)
if (!value) return ''
current = value
}
return ''
}
会在 vue 文件中引入 locale 中的 t 方法
import { t } from '@element-plus/locale'
然后就可以在 template 使用多语言 key 值了,例如:label="t('el.datepicker.nextMonth')"
,t
方法会帮你找到对应的语言文件中的对应值。
再来看看 use
方法,抛出的 use
方法可以设置全局语言种类,也修改 day.js
的语言配置。 element-plus 中引入了 day.js
替换原来的 moment.js
来做时间的格式化和时区信息等的处理。
export const use = (l): void => {
lang = l || lang
if (lang.name) {
dayjs.locale(lang.name)
}
}
我们的业务组件引入 element-plus 之后,会使用这个 use 方法来设置语言种类,可参照官方文档
Website 打包
website, 也就是文档网站,提供各个控件的使用示例。website/entry.js
中的
import ElementPlus from 'element-plus'
其实应该是引入了 packages/element-plus/index.ts
文件,然后就可以在 md 中使用 packages 中的各个组件了,组件逻辑修改也可以立即生效。
和 element-ui 一致,element-plus 的 website dev 起服务和打包都是用的 webpack,使用到了 vue-loader
来处理 vue
文件,使用 babel-loader
处理 js/ts
文件,样式文件和字体图标分别使用了对应的 css-loader
, url-loader
等。
相关配置在 website/webpack.config.js
中
文档展示主要的 md 文件,使用了 website/md-loader/index.js
自己实现的 md-loader
, 分别从 md 中提取出 <template>
和 <script>
内容 ,将 md 中的组件示例转化成了 vue 的字符串,然后再通过 vue-loader
来处理。
rules: [
{
test: /\.vue$/,
use: 'vue-loader',
},
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false,
},
},
},
{
loader: path.resolve(__dirname, './md-loader/index.js'),
},
],
},
{
test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/,
loader: 'url-loader',
// todo: 这种写法有待调整
query: {
limit: 10000,
name: path.posix.join('static', '[name].[hash:7].[ext]'),
},
},
]
组件库和样式打包
element-plus 的打包命令有这么一长串,其中 yarn build:lib
和 yarn build:lib-full
是用到了 webpack 打 umd 格式的全量包。其余的则是分别使用到了 rollup 和 gulp。
"build": "yarn bootstrap && yarn clean:lib && yarn build:esm-bundle && yarn build:lib && yarn build:lib-full && yarn build:esm && yarn build:utils && yarn build:locale && yarn build:locale-umd && yarn build:theme"
使用 rollup 打包组件 bundle
除了使用 webpack 来打包组件之外,element-plus 还提供了另外一种 es-module
的打包方式,最后发布到 npm 的既有 webpack 打包的成果物,也有 rollup
打包的 es-module bundle。
rollup
相关的逻辑在 build/rollup.config.bundle.js
文件中
入口为 export 所有组件的 /packages/element-plus/index.ts
, 采用 es-module 规范最终打包到 lib/index.esm.js
中。由于打包时使用了 Typescript 插件,最后生成的文件除了全量的 index.esm.js
,还有每个组件单独的 lib
文件。
// build/rollup.config.bundle.js
export default [
{
input: path.resolve(__dirname, '../packages/element-plus/index.ts'),
output: {
format: 'es', // 打包格式为 es,可选cjs(commonJS) ,umd 等
file: 'lib/index.esm.js',
},
external(id) {
return /^vue/.test(id)
|| deps.some(k => new RegExp('^' + k).test(id))
},
}
]
使用 gulp 打包样式文件和字体图标
和 element-ui 一样,样式文件和字体图标的打包使用的是 packages/theme-chalk/gulpfile.js
, 把每个 scss 文件打包成单独的 css, 其中包含了通用的 base 样式,还有每个组件的样式文件。
// packages/theme-chalk/gulpfile.js
function compile() {
return src('./src/*.scss')
.pipe(sass.sync())
.pipe(autoprefixer({ cascade: false }))
.pipe(cssmin())
.pipe(rename(function (path) {
if(!noElPrefixFile.test(path.basename)) {
path.basename = `el-${path.basename}`
}
}))
.pipe(dest('./lib'))
}
function copyfont() {
return src('./src/fonts/**')
.pipe(cssmin())
.pipe(dest('./lib/fonts'))
}
再通过 npm script
中一些文件拷贝和删除操作,打包之后的样式和字体图标文件最终会放到 lib/theme-chalk
目录下。
cp-cli packages/theme-chalk/lib lib/theme-chalk && rimraf packages/theme-chalk/lib
小结
我们看到一个组件库使用了 3 种打包工具:rollup
,webpack
,gulp
。
Vue
,React
等开源库开始采用 rollup,构建会更快,然后应用类工程还是主要使用 webpack,因为 webpack 能用插件和各种 loader
处理其他非 javascript 类型的资源。而皮肤包和字体文件采用 gulp 可能是 gulp 的配置比 webpack 更简洁,不需要引入 url-loader
,css-loader
等。我的理解是,webpack 是全套方案,功能齐全但是配置麻烦。 rollup 和 gulp 适用于打包需求比较单一的情况去使用,更轻便和定制化。
引入 lerna
整体上的一点改动,element-plus 采用了 lerna 进行包管理,lerna 可以负责 element-plus 版本和组件版本管理,还可以将每个组件单独发布成 npm 包(不过 element-plus 目前 npm 上只有全量包, 单个组件的皮肤包和多语言文件现在也是放在了一个文件夹下而不是每个组件当中)。
每个组件都有这样一个 package.json
文件
{
"name": "@element-plus/message",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.3"
}
}
然后使用了 workspaces
匹配 packages
目录,依赖会统一放在根目录下的 node-modules
,而不是每个组件下都有,这样相同的依赖可以复用,目录结构也更加清晰。
// package.json
"workspaces": [
"packages/*"
]
element-plus 的 script 中还提供了一个 shell 脚本用于开发新组件的时候生成基础文件, 使用 npm run gen
可以在 packages 下生成一个基础的组件文件夹。
"gen": "bash ./scripts/gc.sh",
最后
element-plus 现在每天的 commit 比较多,有些功能还在不断的完善中,我们可以通过阅读组件库的源码来学习组件设计和 Vue3 的新特性,也可以给官方提 Pull Request 来参与贡献。
推荐阅读
- Rollup + TS 库(library)开发指南(1)—— 工程搭建
- 快速使用Vue3最新的15个常用API
- vue-3-playground
- vue3 官方文档 - typescript 支持
- Vue3 究竟好在哪里?(和 React Hook 的详细对比)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!