从零开始,构建自己的UI组件库,并且发布到npm
# 前言
前端项目随着业务扩张,就会自然的抽象出可以被复用的业务组件,这也是一种业务沉淀,虽然我们用了很多别人的库, 但是大多时候我们还是会有一些自己的自定义修改,每个人都会逐渐的开发产出自己的组件,但是这些组件必须加以规范跟流程化, 否则容易造成组件复用率不高、拓展性不强、耦合度过高等问题。 所以对于团队来说还是有必要需要自己的组件库。也可以学习一下,对于库模式的打包的要求,进一步学习了webpack。
# 参考资料
# 环境准备
编写组件我们首先考虑的问题就是
- 脚手架搭建
- 如何规划目录结构
- 如何编写文档跟示例
- 编写组件
- 发布到npm上
# 脚手架的搭建
常用的打包工具有webpack,Rollup ,gulp。 因为我们开发的是组件库因为方便快速的原因,这里选择webpack。 Rollup适合打包纯JS项目。gulp适合打包CSS。同时如果自己配置webpack的话会比较复杂一点(ps:有利于学习webpack) 本次示例采用vue-cli做脚手架。
# 如何规划目录结构
跟我们一般做项目不同所有的代码都在src文件夹里面,自己做一个组件库的话,起码要分开两个文件夹一个用来保存组件代码的 文件夹package,一个用来展示示例跟文档的目录examples。 所以我们在用vue-cli安装好项目之后
# 全局安装 vue-cli
$ npm install --global vue-cli
# 创建一个基于 webpack 模板的新项目
$ vue init webpack my-project
# 安装依赖,走你
$ cd my-project
$ npm install
$ npm run dev
2
3
4
5
6
7
8
将项目目录结构改成如下所示
.
...
|-- examples // 原 src 目录,改成 examples 用作示例展示
|-- packages // 新增 packages 用于编写存放组件
...
.
2
3
4
5
6
因为项目结构发生了变化导致项目不能启动所以我们得重新配置入口
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
}
}
2
3
4
5
6
7
8
9
10
packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。
chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 如何编写文档跟示例
思考一下如何编写文档跟示例,对于文档的编写,自然是markdown最合适不过了,那么怎么让我们在vue下可以去写 markdown 文档呢? 这里我们使用一个叫vue-markdown-loader的插件。这个插件可以讲markdown文档转换成vue文件, 首先在 example/docs 目录下新建 test.md。然后在vue.config配置
config.module.rule('md')
.test(/\.md$/)
.use('vue-loader')
.loader('vue-loader')
.end()
.use('vue-markdown-loader')
2
3
4
5
6
7
同时创建一个新的路由,指向我们的md文件:
{
path: '/test',
name: 'test',
component: r => require.ensure([], () => r(require('../docs/test.md')))
}
2
3
4
5
6
现在我们能在http://localhost:8080/#/test 看到我们写的markdown了。 现在我们需要解决下一步的问题,除了需要用markdown写文档之外。我们最好是能在 边编写文档的时候能够显示示例。如下图所示 所以我们的需求变成了不仅仅是写完markdown能转换成vue,还能在markdown里面写vue代码 那我们需要怎么做呢?vue-mark-down 功能肯定不止这些!于是我们继续阅读它的文档,发现其实他就是封装了 markdown-it,支持 options 选项。这样我们就可以为我们的markdown定义独特的标识符,这里我用 demo 标识需要显示代码块的地方,所以我需要配置options 选项 :
config.module.rule('md')
.test(/\.md$/)
.use('vue-loader')
.loader('vue-loader')
.end()
.use('vue-markdown-loader')
.loader('vue-markdown-loader/lib/markdown-compiler')
.options({
preset: 'default',
breaks: true,
raw: true,
typographer: true,
preprocess: function (MarkdownIt, source) {
MarkdownIt.renderer.rules.table_open = function () {
return '<table class="table">'
}
MarkdownIt.renderer.rules.fence = utils.wrapCustomClass(MarkdownIt.renderer.rules.fence)
// ```html `` 给这种样式加个class hljs
// 但是markdown-it 有个bug fence整合attr的时候直接加载class数组上而不是class的值上
// markdown-it\lib\renderer.js 71行 这么修改可以修复bug
// tmpAttrs[i] += ' ' + options.langPrefix + langName; --> tmpAttrs[i][1] += ' ' + options.langPrefix + langName;
// const fence = MarkdownIt.renderer.rules.fence
// MarkdownIt.renderer.rules.fence = function(...args){
// args[0][args[1]].attrJoin('class', 'hljs')
// var a = fence(...args)
// return a
// }
// ```code`` 给这种样式加个class codeInline
const codeInline = MarkdownIt.renderer.rules.codeInline
MarkdownIt.renderer.rules.codeInline = function (...args) {
args[0][args[1]].attrJoin('class', 'codeInline')
return codeInline(...args)
}
return source
},
use: [
[MarkdownItContainer, 'demo', {
validate: params => params.trim().match(/^demo\s*(.*)$/),
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
// const html = tokens[idx + 1].content
const html = utils.convertHtml(striptags(tokens[idx + 1].content, 'script'))
// 移除描述,防止被添加到代码块
tokens[idx + 2].children = []
return `<demo-block>
<div slot="highlight">`
}
return '</div></demo-block>\n'
}
}]
]
});
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
简单介绍一下上面配置的任务就是能识别markdown里面的demo标签然后将他直接转换成html代码并且放置在 我们自己写的demo-block组件里面。
# 编写组件
接来下我们考虑怎么编写组件
- 在 packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 button/
- 在 button/ 目录下创建 src/ 目录存储组件源码
- 在 /button 目录下创建 index.js 文件对外提供对组件的引用。
修改 /packages/button/index.js文件,对外提供引用。
// 导入组件,组件必须声明 name
import HcButton from './src'
// 为组件提供 install 安装方法,供按需引入
HcButton.install = function (Vue) {
Vue.component(HcButton.name, HcButton)
}
// 导出组件
export default HcButton
2
3
4
5
6
7
8
9
10
在button/src/index.js 编写源码
修改 /packages/index.js 文件,对整个组件库进行导出。
// 导入button组件
import HcButton from "./button";
import HcUpload from "./upload";
import HcCheckBox from "./checkbox";
import AutoScroll from "./autoscroll";
import HcQuillEditor from "./quill-editor";
// 组件列表
const components = [HcButton, HcUpload, HcCheckBox, AutoScroll, HcQuillEditor];
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,那么所有的组件都会被注册
const install = function(Vue) {
// 判断是否安装
if (install.installed) return;
// 遍历注册全局组件
components.map((component) => Vue.component(component.name, component));
};
// 判断是否是直接引入文件
if (typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}
export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
HcButton,
HcUpload,
HcCheckBox,
AutoScroll,
HcQuillEditor,
};
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
# 在示例中导入组件库
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import demoBlock from './components/demo-block.vue'
// 导入组件库
import hcui from '../packages'
// 注册组件库
Vue.use(hcui)
Vue.component('demo-block', demoBlock)
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 发布到 npm,方便直接在项目中引用
1、package.js 中新增一条编译为库的命令 在库模式中,Vue是外置的,这意味着即使在代码中引入了 Vue,打包后的文件也是不包含Vue的。 以下我们在 scripts 中新增一条命令
- --target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
- --dest : 输出目录,默认 dist。这里我们改成 lib
- [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。
"script": {
// ...
"lib": "vue-cli-service build --target lib --name vcolorpicker --dest lib packages/index.js"
}
2
3
4
执行编译库命令
$ npm run lib
- 配置 package.json 文件中发布到 npm 的字段
- name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
- version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
- description: 描述。
- main: 入口文件,该字段需指向我们最终编译后的包文件。
- keyword:关键字,以空格分离希望用户最终搜索的词。
- author:作者
- private:是否私有,需要修改为 false 才能发布到 npm
- license: 开源协议 以下为参考设置
{
"name": "vcolorpicker",
"version": "0.1.5",
"description": "基于 Vue 的颜色选择器",
"main": "lib/vcolorpicker.umd.min.js",
"keyword": "vcolorpicker colorpicker color-picker",
"private": false
}
2
3
4
5
6
7
8
- 添加 .npmignore 文件,设置忽略发布文件 我们发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。
和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。
# 忽略目录
examples/
packages/
public/
# 忽略指定文件
vue.config.js
babel.config.js
*.map
2
3
4
5
6
7
8
9
- 登录到 npm
首先需要到 npm 上注册一个账号,注册过程略。
如果配置了淘宝镜像,先设置回npm镜像:
$ npm config set registry http://registry.npmjs.org
然后在终端执行登录命令,输入用户名、密码、邮箱即可登录。
$ npm login
5、发布到 npm
执行发布命令,发布组件到 npm
$ npm publish
- 发布成功 发布成功后稍等几分钟,即可在 npm 官网搜索到。