Vue Cli3 从 0 开始搭建组件库

一时造轮子一时爽,一直造轮子一直爽。

写在前面

为什么要自己搭建组件库?市面上已经存在了那么多优秀的组件库,为什么要重复造轮子?其实也没什么特别的理由,真要说一个的话,就是市面上的再好也不如自己的适合,每个团队或多或少都会需要自己的组件库,项目的风格,交互等并一定是市面上能满足的,而且搭建组件库是一个长期的过程,在这个过程中,我们会去思考怎么编写组件,也会遇到各式各样的问题,这对我们来说其实是一个不错的学习和成长的过程。

废话少说,先上源码地址

项目搭建

创建项目

用 Vue Cli3 创建一个项目,具体怎么创建,这边就不赘述了,官网上有很详细的描述。

修改目录结构

前面也说了, 市面上有很多优秀的组件库,我们不会完全照搬照抄,但是借鉴一波也无可厚非。

  • 首先,修改 src 目录为examples,用来放我们的实例代码及文。
  • 然后,新增一个目录 packages,用来存放我们的组件源码。
    完整的目录结构如下图:

目录结构

修改配置文件

目录结构调整后,毫无疑问,配置文件也需要调整,新建 vue.config.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
const path = require('path');

const resolve = (dir) => {
return path.join(__dirname, dir);
};

module.exports = {
publicPath: './',
productionSourceMap: false,
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
configureWebpack: {
resolve: {
alias: {
'@': resolve('examples')
}
}
},
chainWebpack: config => {
config.module
.rule('md')
.test(/\.md/)
.use('vue-loader')
.loader('vue-loader')
.end()
.use('./build/md-loader/index.js')
.loader('./build/md-loader/index.js');
}
};

这里主要修改了项目的入口,以及 md 文件的解析,这个在后面文档实现时会用到。

添加组件

壳子搭好了,剩下的当然是写组件了,我们先简单的来一个 Button 组件吧。
首先新建一个目录,取名 button,然后在该目录下新建 src 目录和 index.js 文件

编写组件

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
<template>
<button class="ndc-button" :class="['ndc-button-' + theme]" :disabled="disabled" @click="onClick">
<i v-if="icon" class="ndc-button-icon" :class="icon ? icon : ''"></i>
<slot></slot>
</button>
</template>

<script>
export default {
name: 'NdcButton',
props: {
disabled: {
type: Boolean,
default: false
},
icon: {
type: String,
default: ''
},
theme: {
default: 'default',
validator(value) {
return ['default', 'primary', 'active'].includes(value);
}
}
},
methods: {
onClick(event) {
if (!this.disabled) {
this.$emit('click', event);
}
}
}
};
</script>

暴露组件

index.js 主要用来暴露组件,定义组件的 install 方法,后续按需引入的时候用得到。

1
2
3
4
5
6
7
import NdcButton from './src/button.vue';

NdcButton.install = (Vue) => {
Vue.component(NdcButton.name, NdcButton);
};

export default NdcButton;

tips: 组件中一定要定义 name

暴露组件库

上面描述的仅仅是单个组件的编写,而我们的目标是整个组件库,仅仅暴露组件还不够,我们在 packages 目录下新建一个 index.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
import Button from './button';

const version = '0.2.6';
const components = [
Button
];

const install = Vue => {
components.forEach(Component => {
Vue.use(Component);
});
};

if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}

export {
install,
version,
Button
};

export default {
install,
version
};

每添加一个组件,我们就需要在该文件中暴露,很显然,这么重复的劳动不符合我们的追求,所以,这里我们可以自定义任务来处理。
在 build 目录下,我们新增一个 build-entry.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
const fs = require('fs-extra');
const path = require('path');
const uppercamelize = require('uppercamelcase');

const Components = require('./get-components')();
const packageJson = require('../package.json');

const version = process.env.VERSION || packageJson.version;
const tips = `/* eslint-disable */
// This file is auto gererated by build/build-entry.js`;

function buildPackagesEntry() {
const uninstallComponents = ['Message'];

const importList = Components.map(
name => `import ${uppercamelize(name)} from './${name}'`
);

const exportList = Components.map(name => `${uppercamelize(name)}`);

const installList = exportList.filter(
name => !~uninstallComponents.indexOf(uppercamelize(name))
);

const content = `${tips}
${importList.join('\n')}
const version = '${version}'
const components = [
${installList.join(',\n ')}
]
const install = Vue => {
components.forEach(Component => {
Vue.use(Component)
})

Vue.prototype.$message = Message
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export {
install,
version,
${exportList.join(',\n ')}
}
export default {
install,
version
}
`;

fs.writeFileSync(path.join(__dirname, '../packages/index.js'), content);
}

buildPackagesEntry();

然后在 package.json 中新增一条"build:entry": "node build/build-entry.js "命令即可。

编译

编写完组件后,当然是要编译成库,然后发布使用。这里,Vue Cli3 已经给我们提供了一个现成的库模式编译,具体命令如下:

1
"lib": "vue-cli-service build --target lib --name ndc-custom-ui --dest lib packages/index.js"

不清楚的可以看一下官网上构建目标的描述。

到这里,我们算是搭建了基础的组件库框架,但这还远远不够,毕竟不能按需加载的组件库是没有灵魂的,下面,我们继续研究如何配置组件库的按需加载。

按需加载

要说按需加载,用过 elementUI 的应该都知道他们自己开发的插件babel-plugin-component,既然有现成的插件,我们不用白不用。
用之前,我们需要了解这个插件的原理是啥,从插件的文档中就可以看出,该插件主要就是:

import { Button } from 'components' 会被解析成同时引入components/lib/button/index.js和引入components/lib/button/style.css,这就是这个插件为我们带来的按需引入的功能。

打包单个组件

既然知道了原理,那后面的事情就好办了,上面我们打包后的 lib 目录下只有 ndc-custom-ui.common.js, ndc-custom-ui.umd.js, ndc-custom-ui.umd.min.js 等文件,而要实现按需加载,必须将单个组件单独打包进来。
在 build 目录下新建 webpack.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

const Components = require('./get-components')();
const entry = {};
Components.forEach(c => {
entry[c] = `./packages/${c}/index.js`;
});

const webpackConfig = {
mode: 'production',
entry: entry,
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: '[name].js',
chunkFilename: '[id].js',
libraryTarget: 'umd'
},
resolve: {
extensions: ['.js', '.vue', '.json']
},
performance: {
hints: false
},
stats: 'none',
optimization: {
minimize: false
},
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.vue$/,
loader: 'vue-loader'
}]
},
plugins: [
new ProgressBarPlugin(),
new VueLoaderPlugin()
]
};

module.exports = webpackConfig;

package.json 中新增命令webpack --config ./build/webpack.component.js

样式

我们在 packages 目录下新建一个 theme-chalk 文件夹来放组件样式,里面样式文件与组件一一对应,至于为啥这么做,当然是为了借鉴 elementUI 啦。

发布

到此,组件库算是真的搭建起来了,接下来就是发布到 npm 上了。

首先,修改一下我们的 package.json 文件,新增以下信息:

1
2
3
4
5
6
7
8
9
10
"name": "ndc-custom-ui",
"version": "1.0.0",
"description": "A Component Library for Vue.js.",
"private": false,
"main": "lib/ndc-custom-ui.umd.js",
"files": [
"lib",
"packages"
],
"license": "MIT"

然后,npm login登录 npm 账号,再执行 npm publish发布即可。

文档

组件库搭建完成了,最后,我们的文档肯定是少不了的。
其实文档系统也没啥好说的,把握住一点就是 markdown 文件的解析。我们整个文件系统都是建立在 markdown 文件的解析基础上实现的,具体实现看 build 目录下的 md-loader。这里我基本上照搬了 elementui,再根据自己的理解略微的做了一些修改,然后实现文档发布的脚本。

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
#!/usr/bin/env node
const execSync = require('child_process').execSync;
const VERSION = require('../package.json').version;
const fs = require('fs');
const GIT_COMMIT = execSync('git rev-parse --short HEAD').toString().replace(/\n/, '');

const ghpages = require('gh-pages');
execSync('npm run build:docs');

// 修改文档的域名为 ndc-ui.feminzai.com
fs.writeFile('dist/CNAME', 'ndc-ui.feminzai.com', 'utf8', (error) => {
if (error) {
console.log(error);
return false;
}
console.log('CNAME 写入成功');
});

ghpages.publish('dist', {
user: {
name: 'minteliuwm',
email: 'minteliu.l@gmail.com'
},
repo: 'https://github.com/minteliuwm/ndc-ui.git',
message: `[deploy] ${GIT_COMMIT} - [release] ${VERSION}`
});

package.json 中新增命令"publish:docs": "node build/publish-docs.js"即可。

总结

至此,我们的整个组件库算是搭建完成了,站在巨人的肩膀上让我们节省了很多事情,但同样,我们也需要去理解其中的每一步,以及每个配置文件的作用,思考多了,就会沉淀下自己的一些想法,实践多了,就会有自己的一些东西,仅此而已。

查看评论