The Vue3 component library, Element-Plus, is a new TypeScript + Composition API refactoring project. The official list of major updates follows. This article will read the source code of Element-Plus and analyze the source code as a whole and in detail from the following aspects. It is recommended to clone the component code before reading this article.
- Develop with TypeScript
- Use the Vue 3.0 Composition API to reduce coupling and simplify logic
- Refactoring mount components using the new Vue 3.0 Teleport feature
- Switch from Vue 2.0 global API to Vue 3.0 instance API
- Internationalization processing
- Official document website package
- Component libraries and style packaging
- Maintain and manage projects using Lerna
Typescript related
Element-plus introduces typescript. In addition to configuring the corresponding ESLint validation rules, plugins, and tsconfig.json, it uses rollup plugins when packaging the es-Module component library.
- @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 [{
/ /... Omit the preceding part
plugins: [
terser(),
nodeResolve(),
vue({
target: 'browser'.css: false.exposeFilename: false,
}),
typescript({
tsconfigOverride: {
'include': [
'packages/**/*'.'typings/vue-shim.d.ts',].'exclude': [
'node_modules'.'packages/**/__tests__/*',],},}),],}]Copy the code
Rollup /plugin-node-resolve Package dependent NPM packages
Rollup-plugin-terser compression code
Rollup-plugin-vue packages the Vue file, and the CSS styles are handed over to gulp, which will be mentioned later.
Rollup-plugin-typescript is used to compile typescript. Node-modules and test files are excluded from configuration, and component implementations are included. Typings /vue-shim.d.ts files are also included.
The typings/vue-shim.d.ts type declaration files used in the plugin (files ending in.d.ts are automatically parsed) define global type declarations that can be used directly in TS or Vue files. Extension templates are also used to give type hints for importing variables of 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'
Copy the code
In addition to the d.ts file, element- Plus uses vue3’s propType for the props type declaration. In the example of Alert below, the props type that uses PropType executes constructors that conform to our custom rules, and then does typescript type validation. The other type declarations in props use interface.
import { PropType } from 'vue'
export default defineComponent({
name: 'ElAlert'.props: {
type: {
type: String as PropType<'success' | 'info' | 'error' | 'warning'>,
default: 'info',}}})Copy the code
See the official documentation for more vue3 typescript support
Composition API
The Vue 3.0 Composition API is used to reduce coupling and simplify logic. An intuitive and concise example is provided in VUE-3-Playground via a shopping cart demo implementation.
For usage of common Composition apis, check out this well-summarized article for a quick look at Vue3’s latest 15 common apis
In addition to using the new Composition API to rewrite components, several reusable hooks files are extracted from the Packages /hooks directory in Element-Plus
In the case of use-attrs used by autocomplete, Input, etc., the main thing is to inherit the attributes and events of the binding, similar to $attrs and $Listener functions, but with some filtering, some attributes and events that do not need to inherit are removed.
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
})
Copy the code
Vue3 still has mixins, which can be used to override logic on a component-specific or global basis. Hooks are also introduced to fix some of the problems with mixins:
- The source of the attributes exposed in the render context is unclear. For example, when reading a component’s template with multiple mixins, it can be difficult to determine from which mixin a particular property was injected.
- Namespace conflict. Mixins can have conflicting property and method names
The benefit of Hooks is
- The attributes exposed to the template have an explicit origin because they are values returned from Hook functions.
- The value returned by Hook functions can be named arbitrarily, so namespace conflicts do not occur.
The use of the Teleport
Elemental-plus uses vue3’s new Teleport feature for several mounted class components, which allows us to move its wrapped elements to the nodes we specify.
Teleport provides a clean way to control which parent node in the DOM renders HTML without having to resort to global state or split it into two components. — Vue official documentation
If you look at the official website, you’ll see that Dialog, Drawer, and Tooltip and Popover that use Popper all add an append-to-body attribute. We use Dialog as an example: appendToBody is false, Teleport is disabled, DOM is still rendered in the current position, and when true, the contents of the Dialog are placed under the body.
<template>
<teleport to="body" :disabled=! "" appendToBody">
<transition
name="dialog-fade"
@after-enter="afterEnter"
@after-leave="afterLeave"
>.</transition>
</teleport>
</tamplate>
Copy the code
In the original element – in the UI, the Tooltip and Popover is directly on the body, the original is through the vue – in popper. Js to use the document body. Under the appendChild to add elements to the body, Element-plus uses Teleport to implement the logic.
Global API – Instance API
When we have installed the component library, the use method executes the install method to mount the component globally. Prototype binds global variables to global methods. Vue.use binds global custom directives vue. prototype binds global variables to global methods
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
// The Vue.component method binds global components
components.forEach(component= > {
Vue.component(component.name, component);
});
// vue. use binds global custom directives
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
// vue. prototype binds global variables and methods
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;
};
Copy the code
However, in VUE 3.0, any API that globally changes the behavior of a VUE will now be moved to the app instance created by createApp, and the corresponding API has been changed accordingly.
In Element-Plus with Vue 3.0, the global API was rewritten as the instance 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 // Set the default size and z-index attributes globally
// Globally register all components except plugins
components.forEach(component= > {
app.component(component.name, component)
})
plugins.forEach(plugin= > {
app.use(plugin as any)
})
}
Copy the code
The $global method added to the message class component was moved to the index.ts component in element-Plus, and several message notification types were placed in plugins. Using app.use calls the install method in the corresponding component index.ts with the following code:
(Message as any).install = (app: App): void= > {
app.config.globalProperties.$message = Message
}
Copy the code
internationalization
Packages /locale/index.ts throws two methods, t and use. T controls the translation and substitution of the Chinese text in vue files. Use changes the global language
// packages/locale/index.ts
export constt = (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 ' '
}
Copy the code
It will introduce the t method in locale into the VUE file
import { t } from '@element-plus/locale'
Copy the code
Then you can use a multi-language key in the template, for example: label=”t(‘ el.datepicker.nextmonth ‘)”. The t method will help you find the corresponding value in the corresponding language file. Take a look at the use method, which is thrown to set the global language category and also modify the language configuration of day.js. Element – Plus introduces day.js instead of moment.js for time formatting and time zone information.
export const use = (l): void= > {
lang = l || lang
if (lang.name) {
dayjs.locale(lang.name)
}
}
Copy the code
After introducing element-Plus to our business component, we will use this use method to set the language category, as you can see in the official documentation
Website package
Website, the documentation site, provides examples of how each control works. Website/entry. In js
import ElementPlus from 'element-plus'
Copy the code
By introducing packages/element-plus/index.ts files, you can use individual packages components in MD, and the component logic changes can take effect immediately.
As with Element-UI, Elder-Plus’s website dev uses webpack for service and packaging, uses vue-loader to handle vue files, and babel-loader to handle JS/TS files. Style files and font ICONS respectively use the corresponding CSS-loader, URL-loader and so on.
Related configuration in the website/webpack. Config. Js
and
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: This notation needs to be adjusted
query: {
limit: 10000.name: path.posix.join('static'.'[name].[hash:7].[ext]'),}},]Copy the code
Component libraries and style packaging
There is a long list of elemental-Plus package commands. Yarn Build :lib and yarn Build :lib-full are full packages in UMD format using Webpack. The rest use rollup and gulp, respectively.
"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"
Copy the code
Package component bundles using rollup
In addition to using WebPack to package components, Element-Plus provides another way to package Es-Modules, which are shipped to NPM in both WebPack-packed and Rollup es-Module bundles. Rollup logic in the build/rollup. Config. Bundle. The js file entry for the export of all components/packages/element – plus/index. Ts, The es-Module specification is finally packaged into lib/index.esm.js. Because Typescript plug-ins are used for packaging, the resulting files include the full index.esm.js file as well as a separate lib file for each component.
// build/rollup.config.bundle.js
export default[{input: path.resolve(__dirname, '.. /packages/element-plus/index.ts'),
output: {
format: 'es'.// The package format is ES, commonJS (CJS), umD, etc
file: 'lib/index.esm.js',},external(id) {
return /^vue/.test(id)
|| deps.some(k= > new RegExp(A '^' + k).test(id))
},
}
]
Copy the code
Use gulp to package style files and font ICONS
As with element-UI, style files and font ICONS are packaged using packages/ Theme-chalk /gulpfile.js, which packages each SCSS file into a separate CSS containing common Base styles, as well as style files for each component.
// 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'))}Copy the code
After copying and deleting files from NPM Script, the packaged style and font icon files will eventually be placed in the lib/ Theme-chalk directory.
cp-cli packages/theme-chalk/lib lib/theme-chalk && rimraf packages/theme-chalk/lib
Copy the code
summary
We see a component library that uses three packaging tools: rollup, Webpack, and gulp. Vue, React, and other open source libraries started using rollup, which made building faster. However, application class engineering still mainly uses WebPack, since WebPack can handle other non-javascript resources with plug-ins and various Loaders. The gulp configuration is more concise than Webpack and does not need to introduce URL-loader, CSS-loader, etc. My understanding is that Webpack is a complete solution, full of functions but troublesome configuration. Rollup and gulp are lightweight and customizable for use where packaging needs are relatively simple.
The introduction of lerna
One overall change, Element-Plus uses Lerna for package management, which is responsible for element-Plus versioning and component versioning. It is also possible to distribute each component individually as an NPM package (although element-Plus currently only has full packages on NPM, and skin packages and multilingual files for individual components are now placed in a folder instead of each component). Each component has such a package.json file
{
"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"}}Copy the code
We then use workspaces to match packages directories. Dependencies are placed in node-modules under the root directory instead of under each component, so that the same dependencies can be reused and the directory structure is clearer.
// package.json
"workspaces": [
"packages/*"
]
Copy the code
The Elemental-Plus script also provides a shell script for generating base files when developing new components. Using NPM Run Gen, you can generate a base component folder under Packages.
"gen": "bash ./scripts/gc.sh".Copy the code
The last
At present, there are a lot of commits for Element-Plus every day, and some functions are still being improved. We can learn about component design and new features of Vue3 by reading the source code of the component library, and can also contribute to the official Pull Request.
Recommended reading
- Rollup + TS Library Development Guide (1) – Construction
- Quick access to Vue3’s latest 15 commonly used apis
- vue-3-playground
- Vue3 official documentation – typescript support
- What’s so great about Vue3? (Detailed comparison to React Hook)