从零开始打造属于自己的UI库

目前主流的 Vue UI 框架很多, 每个 UI 框架的风格各有各的特点, 国内目前知名度较高的莫过于 Element 和 iView

大概是 16 年的时候,那时候刚开始上手 Vue2.0, 同时也分别使用了 Element 和 iView 两个 UI 框架来实现一些运营系统的前端开发。

从那时候开始就想着,能写出这么完善和优秀的框架得需要多久啊!内心一直很渴望也能够自己写一写 UI 框架,然后自己设计,自己配色。

不求推广到给别人使用!只求能够自己过把瘾!以及在写 UI 框架的过程中能够学习和沉淀多一丁点前端技术。

Vue

The best-ui is not the best !important;

PS: Best UI 大部分学习和借鉴了 Element 和 iView 组件库的源码

项目结构

+– build
+– examples
+– lib
+– packages
+– components
+– theme
+– src
+– mixins
+– utils
+– index.js
+– tests
+– types
+– babel.config.js
+– components.json
+– gulpfile.js
+– package.json

看过 Element 的源码的同学估计一目了然,这里主要参考了 Element UI 的项目结构

简单讲一下这里的项目目录和文件的作用

  • build 主要是和构建相关
  • examples 用于编写一些简单的 demo 代码,开发组件调试所用
  • lib 是打包构建完成后代码的存放目录
  • packages 是每个组件的源码实现, components 表示每个组件独立的目录,theme 存放组件的样式 .scss 文件
  • src 是 UI 框架的入口文件存放位置,以及一些工具函数,mixins,plugins 文件的存放
  • tests 单元测试和集成测试(不好意思,写这篇文章的时候一个组件的单元测试都还没写,我是个辣鸡)
  • types d.ts 的类型声明文件(写这篇文章的时候所有组件的 d.ts 文件还没完成补齐,我是个辣鸡第二遍)
  • babel.config.js babel 的配置声明
  • components.json 用来指定具体组件的源码位置,用于构建多个组件单独的文件
  • gulpfile 用于 gulp 进行样式构建的运行文件

一个前端工程化项目的结构出来了,按照个人的开发习惯,接下来就是要对项目的构建进行配置了,不然光写代码而运行不了,没什么意义。

构建过程配置

Webpack]

对于 Webpack 的配置,本来一开始是使用 vue-cli 来生成对应的构建配置,但是后面发现要去修改适配组件库的构建流程有点鸡肋,所以就干脆自己大致按照 Element 的方式去做一遍配置,每个环节这里都会详细介绍

build 这个构建配置的目录如下:

+– build
+– bin
+– build-component.js
+– build.js
+– watch.js
+– utils
+– config.js
+– webpack.base.js
+– webpack.build.js
+– webpack.component.js
+– webpack.watch.js

bin 目录三个文件代表了三个运行的命令

  • build-component 主要是用于构建多个组件的单文件。
  • build 表示构建整体的组件库,包含多个组件的合并。
  • watch 用于开发构建过程中预览调试

utils 中只有一个 resolve 用来做路径的解析

Webpack 的 Base 配置

webpack.base.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const { VueLoaderPlugin } = require('vue-loader')
const { resolve } = require('./utils/resolve')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')

module.exports = {
resolve: {
extensions: ['.vue', '.js', '.json'],
alias: {
'@': resolve(process.cwd(), 'examples'),
'~': resolve(process.cwd(), 'packages'),
src: resolve(process.cwd(), 'src')
}
},
module: {
rules: [
{
test: /\.js$/,
loader: 'happypack/loader?id=eslint',
enforce: 'pre'
},
{
test: /\.js$/,
loader: 'happypack/loader?id=babel'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loader: {
js: 'happypack/loader?id=babel'
}
}
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
new HappyPack({
id: 'eslint',
loaders: [{
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}],
verbose: false,
threadPool: happyThreadPool
}),
new HappyPack({
id: 'babel',
loaders: [{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}],
verbose: false,
threadPool: happyThreadPool
}),
new ProgressBarPlugin(),
new VueLoaderPlugin()
]
}

使用 happypack 来进行 babel 的转码,配置对应需要的 loader 和 plugins

配置 resolve.alisa 对一些常用的目录文件路径进行缩写配置

这里没有指定 entry 和 output 主要是让其他的环境配置进行指定

Watch 模式

webpack.base.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseConf = require('./webpack.base')

const config = merge(baseConf, {
mode: 'development',
entry: './examples/main.js',
plugins: [
new HtmlWebpackPlugin({
template: './examples/template/index.html'
})
]
})

module.exports = config

bin/watch.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const webpack = require('webpack')
const config = require('../webpack.watch')
const middleware = require('webpack-dev-middleware')

const compiler = webpack(config)
const express = require('express')
const app = express()

app.use(
middleware(compiler, {
})
)

app.listen(3000, () => console.log('Example app listening on port 3000!'))

Watch 模式下使用 webpack-dev-middleware 结合 express 实现一个简单的 dev-server 进行开发预览,对应的入口文件为我们 examples 下的 main.js 文件。

build 模式

webpack.build.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const merge = require('webpack-merge')
const baseConf = require('./webpack.base')
const { externals } = require('./config')
const { resolve } = require('./utils/resolve')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const config = merge(baseConf, {
mode: 'production',
entry: resolve(process.cwd(), 'src', 'index.js'),
output: {
path: resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: 'index.js',
library: 'BEST',
libraryTarget: 'umd'
},
performance: {
hints: false
},
externals,
plugins: [
new CleanWebpackPlugin()
]
})

module.exports = config

指定 mode 为 production,配置对应的 entry 为 src 下的 index.js , 用于进行全局构建,对应的 output 路径为 lib,指定的 libraryTarget 为 umd 模块。

bin/build.js:

1
2
3
4
5
6
7
8
9
10
const webpack = require('webpack')
const config = require('../webpack.build')

const compiler = webpack(config)

compiler.run(function (err, stats) {
if (err) {
return console.error(err)
}
})

直接借助 webapck 的 compiler 执行构建

Build Component 模式

这里说明一下为什么要构建单个的组件文件,因为涉及到 按需加载 的使用,我们需要对单个的组件进行构建,这里可以借助 webpack 的多文件构建模式来执行

webapck.component.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const merge = require('webpack-merge')
const baseConf = require('./webpack.base')
const { externals } = require('./config')
const { resolve } = require('./utils/resolve')

const components = require('../components.json')

const entrys = {}
Object.keys(components).forEach(component => {
entrys[component] = components[component]
})

const config = merge(baseConf, {
mode: 'production',
entry: entrys,
output: {
path: resolve(process.cwd(), 'lib'),
publicPath: '/dist/',
filename: '[name].js',
libraryTarget: 'commonjs2'
},
optimization: {
minimize: false
},
performance: {
hints: false
},
externals
})

module.exports = config

通过读取 component.json 的配置文件来获取构建的组件对应的入口文件路径,对应的 output 路径为 lib

样式构建

Gulp]

Best UI 组件库是用 Sass 来进行样式的编写,我们这里为了和 js 的构建区分开,所以在样式的构建上,单独使用 gulp 对 .scss 的样式文件进行构建

gulpfile.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use strict'

const { series, src, dest } = require('gulp')
const sass = require('gulp-sass')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')
const basepath = './packages/theme'

function compile () {
return src(`${basepath}/src/*.scss`)
.pipe(sass.sync())
.pipe(autoprefixer({
cascade: false
}))
.pipe(cssmin())
.pipe(dest('./lib/theme'))
}

function copyfont () {
return src(`${basepath}/src/fonts/**`)
.pipe(cssmin())
.pipe(dest('./lib/theme/fonts'))
}

exports.build = series(compile, copyfont)

这里沿用了 Element 的 gulpfile。这个 gulp 的构建过程很简单,主要做两件事情:

  • 将 packages/theme/src 下的每个 .scss 和 .css 的文件进行编译,压缩,最后输出到 lib/theme 的目录下
  • 将 packages/theme/src 下的字体文件进行压缩并输出到 lib/theme/fonts 的目录下

package.json 的 script 配置

通过以上完成了对应的构建配置,为了快速执行以上的命令,我们可以在 npm 的 script 中进行配置:

package.json:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"watch": "node ./build/bin/watch.js",
"build:component": "node ./build/bin/build-component.js",
"build": "node ./build/bin/build.js",
"build:all": "npm run build & npm run build:component & npm run build:css",
"build:css": "gulp build --gulpfile ./gulpfile.js",
"lint": "eslint --fix --ext .js,.vue src packages"
}
}

尝试开发一个组件

Vue

以上已经完成了对整个构建过程的配置,当我们把必要的工程化工具都配置好了后,可以尝试上手开发一个组件,我们以开发一个 Button 组件为具体实例

组件交互逻辑开发

在 package 的目录下新建一个 button 的目录,然后新建以下的文件:

button/index.js:

1
2
3
4
5
6
7
import BTButton from './src/button'

BTButton.install = function (Vue) {
Vue.component(BTButton.name, BTButton)
}

export default BTButton

index.js 作为 button 组件的入口文件,这里给 BTButton 定义 install 方法,目的是方便通过 vue 的插件形式进行全局组件注册。

组件的逻辑实现在 button/src/button.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<template>
<button
:disabled="disabled"
type="button"
:class="[
'bt-button',
type ? `bt-button--${type}` : '',
{
'is-circle': circle,
'is-disabled': disabled
}
]"
@click="handleClick">
<i :class="icon" v-if="icon"></i>
<span v-if="$slots.default">
<!-- @slot 默认插槽 -->
<slot></slot>
</span>
</button>
</template>

<script>
/**
* Button 按钮
* @displayName Best Button
*/
export default {

name: 'BtButton',

props: {
/**
* 类型
* `default,primary,info,warn`
*/
type: {
type: String,
default: 'default'
},
/**
* 是否圆形按钮
*/
circle: {
type: Boolean,
default: false
},
/**
* 图标类名
*/
icon: {
type: String,
default: ''
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
}
},

methods: {
/**
* Click 事件
*
* @event click
* @type {object}
*/
handleClick (event) {
this.$emit('click', event)
}
}

}
</script>

button 的组件实现并不复杂,这里用一个很简单的例子来说明添加一个组件开发的过程(牛x的同学可以考虑打造 cli 来快速新建一个组件的模板)

将组件添加到全局入口文件和配置文件

我们将我们开发完的 button 组件注册到 src/index.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Button from '~/button'

const components = [
Button,
]

const install = function (Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component)
})
}

export default {
install,
Button,
}

将开发完成的 button 组件注册到全局的入口文件,提供全量引入的方式。

对于组件的配置文件 componets.json:

1
2
3
4
{
"button": "./packages/button",
"best-ui.common": "./src"
}

配置 webapck 在构建多个组件文件的过程中每个组件文件对应 entry 的路径 和 output 的文件名。

编写 d.ts 类型声明文件

types/button.d.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { BestUIComponent } from './components'

export type ButtonType = 'default' | 'primary' | 'info' | 'warn'

export declare class BTButton extends BestUIComponent {

type?: ButtonType

circle?: boolean

icon?: string

disabled?: boolean
}

types/components.d.ts:

1
2
3
4
5
import Vue from 'vue'

export declare class BestUIComponent extends Vue {
static install (vue: typeof Vue): void
}

编写单元测试

待补充 !-_-!

组件样式开发

Vue

BEM 样式编写规范

BEM代表 “块(block), 元素(element), 修饰符(modifier)”

以 Button 的组件为例子,我们看下怎么理解 BEM 的规范

对于 Best UI, 前缀统一使用 bt 进行命名,所以 button 组件在 Best UI 的样式命名 为 bt-button,这里 bt-button 表示一个 block

当 bt-button 下有其他的关联元素,比如标签,这时候可以命名为 bt-button__label

通常一个 button 有不同的修饰类型,一个 button 可以为 primary 类型,也可以为 info 类型,或者 warning 类型,这种就可以用修饰符来命名 bt-button–primary

我们看下 packages/theme/button.scss 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@charset "UTF-8";
@import "common/var";
@import "mixins/_button";
@import "mixins/mixins";
@import "mixins/utils";

@include b(button) {
display: inline-block;
cursor: pointer;
background: $--button-default-background-color;
border: $--border-base;
border-color: $--button-default-border-color;
color: $--button-default-font-color;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
box-shadow: $--button-box-shadow;
outline: none;
margin: 0;
font-weight: $--button-font-weight;
transition: .1s;

@include utils-user-select(none);

& + & {
margin-left: 10px;
}

@include button-size($--button-padding-vertical, $--button-padding-horizontal, $--button-font-size, $--button-border-radius);

&:hover,
&:focus {
color: $--color-primary;
border-color: $--color-primary-light-7;
background-color: $--color-primary-light-9;
box-shadow: $--button-modifier-box-shadow;
}

&:active {
color: mix($--color-black, $--color-primary, $--button-active-shade-percent);
border-color: mix($--color-black, $--color-primary, $--button-active-shade-percent);
outline: none;
}

@include when(circle) {
border-radius: $--border-radius-circle;
padding: 10px;
font-size: 0;
& > * {
font-size: $--button-font-size;
}
}

@include m(primary) {
@include button-variant($--button-primary-font-color, $--button-primary-background-color, $--button-primary-border-color);
}

@include m(info) {
@include button-variant($--button-info-font-color, $--button-info-background-color, $--button-info-border-color);
}

@include m(warn) {
@include button-variant($--button-warn-font-color, $--button-warn-background-color, $--button-warn-border-color);
}

}

这里咋一看,可能会觉得这 sass 写得也太复杂了吧!笔者一开始看 Element 的时候就有这种感觉,因为这里都是参考了 Element 的规范,我们这里对这里的 sass 写法做一些详细分析。

Sass 公用方法分析

(1) common/var.scss 这个文件主要是对一些颜色,字体大小,边框等等样式变量进行定义,方便主题的统一修改替换

(2) 对于 mixins/utils.scss 这个主要是一些工具的 sass 函数,比如:清除浮动,文字溢出处理,垂直居中等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@mixin utils-user-select($value) {
-moz-user-select: $value;
-webkit-user-select: $value;
-ms-user-select: $value;
}

@mixin utils-clearfix {
$selector: &;

@at-root {
#{$selector}::before,
#{$selector}::after {
display: table;
content: "";
}
#{$selector}::after {
clear: both;
}
}
}

@mixin utils-vertical-center {
$selector: &;

@at-root {
#{$selector}::after {
display: inline-block;
content: "";
height: 100%;
vertical-align: middle;
}
}
}

@mixin utils-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

(3) 我们重点看下 mixins/mixins.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@import "function"; /* 配置 $namespace, $element-separator, $modifier-separator, $state-prefix */
@import "../common/var";

/* BEM */

// block
@mixin b($block) {
$B: $namespace+'-'+$block !global; // $B is the global var

.#{$B} {
@content;
}
}

// element
@mixin e($element) {
$E: $element !global; // $E is the global var
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}

@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}

// modifier
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}

@at-root {
#{$currentSelector} {
@content;
}
}
}

@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}

这里 b 函数,e 函数,m 函数,when 函数分别用于对类名进行封装

比如我们要对 button 命名一个 bt-button,这时候可以用

1
2
3
@include b(button) {
...
}

当我们需要命名 bt-button__label,则写法如下:

1
2
3
4
5
@include b(button) {
@include e(label) {
...
}
}

需要对 bt-button 进行修饰得到 bt-button–primary 的时候:

1
2
3
4
5
@include b(button) {
@include m(primary) {
...
}
}

需要标识状态的类名可以使用 when 函数,比如:是否为圆形按钮,is-circle

1
2
3
4
5
@include b(button) {
@include when(circle) {
...
}
}

通过这种方式高效利用 sass 的强大语法,结合 BEM 的规范让我们书写样式的时候井然有序,简洁明了(前提是已经了解 sass 的语法)。

Vue 组件开发技巧

使用 Vue 框架实现一个组件库,不像我们平时在业务层级可以引入一些库比如 vuex,vue-router。我们需要追求更加简洁,尽量确保不引入多余的库。

跨层级的组件通信

假如我们有 A 组件,然后 A 组件可以通过 slot 插槽嵌入 B 组件,B 组件又可以通过 slot 嵌入 C 组件,这时候 C 组件想向 A 组件进行跨级通信简单通过 props 和 emit 要传递可能会显得比较繁琐。

于是,我们可以自己实现一个 dispatch 和 broadcast 的功能

PS: 这里主要参考 iView 的实现

src/mixins/emitter.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export default {
methods: {
/**
* @desc 向上遍历找寻对应组件名称的父组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
dispatch (componentName, eventName, params) {
let parent = this.$parent || this.$root
let name = parent.$options.name

while (parent && (!name || name !== componentName)) {
parent = parent.$parent

if (parent) {
name = parent.$options.name
}
}
if (parent) {
parent.$emit(eventName, params)
}
},
/**
* @desc 向下便利找寻对应组件名称的子组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
broadcast (componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name

if (name === componentName) {
child.$emit(eventName, params)
} else {
this.broadcast.apply(child, [componentName, eventName, params])
}
})
}
}
}

当我们的 C 组件需要向 A 组件进行通信的时候,我们这时候可以使用 dispatch,在 C 组件中向上寻找 A 组件,通过 name 属性找到 A 组件,然后传递对应的事件名称和参数。

当我们 A 组件需要向 C 组件进行通信,我们可以使用 broadcast 广播向下寻找 C 组件,并通知对应的事件和参数。

其他技巧等待缓慢补充

待补充 !-_-!

发布 Best UI

完成了以上的分析,这时候 Best UI 的大概的结构和实现已经出来了,那我们怎么去发布我们的库呢?

其实很简单,只要我们配置好对应的 package.json 文件就 ok 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
"name": "@douku/best-ui",
"version": "0.1.5",
"private": false,
"main": "lib/best-ui.common.js",
"files": [
"lib",
"src",
"packages",
"types"
],
"typings": "types/index.d.ts",
"scripts": {
"watch": "node ./build/bin/watch.js",
"build:component": "node ./build/bin/build-component.js",
"build": "node ./build/bin/build.js",
"build:all": "npm run build & npm run build:component & npm run build:css",
"build:css": "gulp build --gulpfile ./gulpfile.js",
"lint": "eslint --fix --ext .js,.vue src packages"
},
"repository": {
"type": "git",
"url": "git@github.com:DouKu/best-ui.git"
},
"keywords": [
"best",
"vue",
"components"
],
"bugs": {
"url": "https://github.com/DouKu/best-ui/issues"
},
"license": "MIT",
"unpkg": "lib/index.js",
"style": "lib/theme/index.css",
"dependencies": {
"async-validator": "^3.1.0",
"vue": "^2.6.10"
},
"peerDependencies": {
"vue": "^2.6.10"
},
"devDependencies": {
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"pre-push": "npm run lint"
}
}
}
  • name: 组件库的包名称
  • main: 全局加载的入口文件
  • files: 需要发布的文件列表
  • typings: 类型声明文件位置
  • peerDependencies: 这里简单来说就是如果你安装了 best-ui, 那你最好也安装 vue

使用 Best UI

全局引入

1
2
3
4
5
import Vue from 'vue';
import BestUI from '@douku/best-ui';
import '@douku/best-ui/lib/theme/index.css';

Vue.use(BestUI);

按需加载

.babelrc:

1
2
3
4
5
6
7
8
9
10
11
{
"plugins": [
[
"component",
{
"libraryName": "@douku/best-ui",
"styleLibraryName": "theme"
}
]
]
}

js 使用:

1
2
3
4
5
6
7
8
import { Button } from '@douku/best-ui';

Vue.component(Button.name, Button);

new Vue({
el: '#app',
render: h => h(App)
});

这里简单说一下 babel-plugins-component 实现按需加载的原理, 其实就是:

1
import { Button } from '@douku/best-ui';

转换为:

1
2
const button = require('@douku/best-ui/lib/button.js');
require('@douku/best-ui/lib/theme/button.css');

这样就不难理解为什么能过实现按需加载了!

结尾

今天是 2019 的最后一天了,回看博客,今年自己写的文章比去年多了一些,希望明年能写出更多。

保持热爱前端的态度,不断折腾,追求极致!

源码地址:Best UI

半成品文档地址:Best UI