仿 Element-ui 搭建Vue2组件库 & Vuepress 编写组件库文档
vue 组件库 vuepress 前端
本文示例代码及 npm 地址
https://e.coding.net/huatoujing/tool/niko-ui.git https://www.npmjs.com/package/niko-ui
前提
开始之前先 clone element-ui,并使用 vue-cli 创建一个新项目。
项目目录
- 新增
src平级的packages目录 - 复制 element-ui 中 button 组件(element-ui 包文件位置 packages/);以及配套的样式,在 packages 中新增 theme 文件夹用于存放样式;
- 保持样式层级不变,只复制有用到的样式(element-ui 样式文件位置 pockages/theme-chalk/src/);只复制
button组件的话只需要如下几个文件:- common/var.scss;
- fonts 目录下所有文件;
- mixins 目录下所有文件;
- theme-chalk/button.scss,theme-chalk/icon.scss,theme-chalk/index.scss
4.packages 中新增 index.js 作为所有组件的统一出口;
import Button from "./button";
const components = [Button];
const install = function (Vue) {
// 批量注册
components.forEach((component) => {
Vue.component(component.name, component);
});
};
// 注入 Vue
if (typeof window.Vue !== "undefined" && window.Vue) {
install(window.Vue);
}
export default {
// 导出 install Vue.use() 前提必须实现 install 方法;
install,
// 组件导出
Button,
};
5.去除 src 下 assets components目录,把 src 改名为 examples,因为这里会作为项目的演示目录,修改main.js:
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
import Components from "../packages";
import "../packages/theme/index.scss";
Vue.use(Components);
new Vue({
render: (h) => h(App),
}).$mount("#app");
同时修改vue.config.js:
const path = require("path");
// 库模式打包
module.exports = {
publicPath: "./",
configureWebpack: {
entry: path.join(__dirname, "../examples/main.js"),
},
};
在 examples/App.vue 下引入 button 组件,进行测试:
<n-button type="primary" icon="n-icon-ice-cream-round">
test-btn
</n-button>
这个时候启动服务,可能会有一些报错,比如node-sass版本之类的,注意版本就行。还有这个项目 vue-cli我这边用的是 4,因为webpack5在后面vuepress打包的时候会不兼容。
项目打包
组件库的雏形已经完成,还需要打包供别人使用;打包方式可以自己选择:
库模式打包
vue-cli 构建目标 首先在 package.json 中 scripts 中新增一条命令:
build: lib: "vue-cli-service build --target lib --dest lib --name NikoUI packages/index.js";
记得要在 packages/index.js 中导入 theme/index.scss 文件,否则打出来的包会丢失样式。
执行完以上命令,项目中会出现 lib 文件夹;这个就是我们组件库的源码了;然后就可以发布到 npm 了。
vue.config.js 构建打包
此种打包方式总的来说就是 规定多个入口逐个打包,最后生成不同的文件方便我们使用 babel 对其做按需加载;
- 根目录新建 components.json 文件,作为 打包的入口配置。
// 后续每次的新增都需要再次配置。
{
"button": "./packages/button/index.js",
"index": "./packages/index.js" // 主文件
}
- 修改
vue.config.js
const path = require("path");
const components = require("../components.json");
const resolve = (dir) => path.join(__dirname, "../", dir);
// 获取每个组件的绝对路径
const getComponentEntries = () => {
const entryKeys = Object.keys(components);
entryKeys.forEach((key) => {
components[key] = resolve(components[key]);
});
return components;
};
const componentEntries = getComponentEntries();
module.exports = {
outputDir: resolve("lib"),
publicPath: "./",
configureWebpack: {
entry: componentEntries,
output: {
// 文件名称
filename: "[name].js",
// 构建依赖类型
libraryTarget: "umd",
// 库中被导出的项
libraryExport: "default",
// 引用时的依赖名
library: "vueTestComp",
},
},
css: {
extract: {
filename: "theme/[name].css", //在lib文件夹中建立 theme 文件夹中,生成对应的css文件。
},
},
/**
* 删除splitChunks,因为每个组件是独立打包,不需要抽离每个组件的公共js出来。
* 删除copy,不要复制public文件夹内容到lib文件夹中。
* 删除html,只打包组件,不生成html页面。
* 删除preload以及prefetch,因为不生成html页面,所以这两个也没用。
* 删除hmr,删除热更新。
* 删除自动加上的入口App。
*/
chainWebpack: (config) => {
config.optimization.delete("splitChunks");
config.plugins.delete("copy");
config.plugins.delete("html");
config.plugins.delete("preload");
config.plugins.delete("prefetch");
config.plugins.delete("hmr");
config.entryPoints.delete("app");
},
};
- 然后直接执行的
npm run build。此时会发现打包出来的结果只有一个index.css文件,是因为现在已经修改成多入口了,样式需要在每个文件中再做单独引入。
解决方案:
- 每个组件在导入一次文件
<!-- packages/button/src/button.vue -->
<style lang="scss" src="../../theme/button.scss"></style>
- css 文件单独打包;参考 element-ui 使用 gulp 再分开打包

当打包后的 lib/theme目录下出现了多个 css 文件后,就可以发布到 npm 了,可以在后面需要用到该组件库的项目中实现按需加载。
babel-plugin-component 按需引入
- 在新项目中安装插件,并且下载 babel-plugin-component。
npm install niko-ui babel-plugin-component -S
- 参考 element-ui 中的使用方式。注意:我这边打包的样式目录是 theme 不是 theme-chalk 需要修改一下。libraryName 则是库名 例如:niko-ui

- 新项目的
src/main.js中按需引入组件,在App.vue中直接使用试一下
import { Button as NButton } from "niko-ui";
Vue.use(NButton);
// App.vue
<n-button type="primary">test</n-button>;
如果启动项目后遇到报错,默认去查找了一个 base.css 的文件,这个改下配置就可以:
plugins: [
[
"component",
{
libraryName: "test-comp-base",
// 不去查找 base
styleLibrary: {
name: "theme",
base: false,
},
},
],
];
组件库开发的一个大概流程就是这样,我这里是直接用了库模式打包全量引入的。这边用了Element现成的组件,当然也可以自己开发,或者从其他开源项目找一些自己需要的改一改。后面说一下在这个基础上以vuepress编写文档的步骤。
Vuepress 编写组件库文档
- 安装 VuePress 依赖。
npm install vuepress -D
- 根目录下创建 docs 文件夹并在内部创建 README.md 文件。
- package.json 添加运行脚本以及打包脚本。
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
- 运行
npm run docs:dev vuepress。
基本配置
在文档目录下创建一个 .vuepress 的文件夹,所有 VuePress 相关的文件都将会被放在这里。你的项目结构可能是这样:
├─ docs
│ ├─ README.md
│ └─ .vuepress
└─ package.json
在 .vuepress 中创建 config.js 这是 VuePress 中必备的配置文件。应该导出一个对象,内容如下,这里直接看完整版的:
module.exports = {
title: "NikoUI",
base: "/niko-ui/", // 我这里配了个路由前缀,不需要可以不用
description: "niko-ui 组件库文档",
themeConfig: {
nav: [
// 一级导航
{ text: "主页", link: "/" },
{ text: "组件", link: "/guide/" },
],
sidebar: {
// 侧边栏
"/guide/": [
["", "介绍"], // '' 等价于 /guide/
{
title: "组件",
collapsable: false,
children: [["../guide/Button.md", "Button"]],
},
],
},
},
plugins: ["@vuepress/back-to-top", "demo-container"],
};
导航栏
导航栏 配置主要就两种方式,一级导航和下拉列表导航。
VuePress 遵循 约定大于配置 的原则,我们需要新建一个 guide 目录。
├─ docs
│ ├── .vuepress
│ │ ├── config.js
│ │ └── public // 这个是存放静态资源的,比如首页展示的图片
│ ├── guide
│ │ ├── Button.md
│ │ └── README.md
│ └─ README.md
└─ package.json
/guide/README.md的查找规则为/guide//guide.md的查找规则为/guide.html
README.md 在这里有点像 index.js。
首页
默认主题的首页提供了一套默认的配置,但是需要注意使用的前提。
- 根级的 README.md 中存在。
- home: true 是必须的。
---
home: true
heroImage: /test.png // 这里可以使用外链,也可以是静态资源,图片资源可以放在上面说的 .vuepress/public 中。
heroText: NikoUI
tagline: NikoUI 组件库文档
actionText: 开始使用 →
actionLink: /guide/ // 这里是点击开始使用后的跳转链接。
features:
- title: 基于Vue2.X
details: 基于Vue2.X
- title: 轻量级UI
details: 轻量
- title: 高可用性
details: 良好的API接口设计,统一的使用习惯
footer: MIT Licensed | Copyright © 2022-present Niko
---
### // 这里需要写点东西,哪怕就写个###,如果完全没有,会出现undefined
代码块导入
导入之前需要了解:
docs/.vuepress/components该目录中的 Vue 组件将会被自动注册为全局组件。换句话说就是在这个文件夹中的*.vue是可以在*.md中当做组件被使用的。
components/demo-1.vue 在使用时则是 <demo-1></demo-1>
components/guide/demo1.vue 在使用时则是 <guide-demo1></guide-demo1>
*.md中也是会支持 Vue 的模板语法。原因是:Markdown 文件将首先被编译成 HTML,接着作为一个 Vue 组件传入vue-loader
假设我们此时要完善 Button 组件的文档。首先创建 guide/button/demo1.js 文件夹存放 demo ,接着随意输入一些代码。然后再 Button.md 中进行导入操作 导入的语法为 <<< @/xxx/xx/x
- <<< 特定语法
- @ 的默认值为 process.cwd()
引入外部组件(应用级别配置)
本项目中就是用的这种方式
官方: 由于 VuePress 是一个标准的 Vue 应用,你可以通过创建一个 .vuepress/enhanceApp.js 文件来做一些应用级别的配置,当该文件存在的时候,会被导入到应用内部。enhanceApp.js 应该 export default一个钩子函数,并接受一个包含了一些应用级别属性的对象作为参数。你可以使用这个钩子来安装一些附加的 Vue 插件、注册全局组件,或者增加额外的路由钩子等。
// 引入打包后的样式和文件。
import "../../lib/NikoUI.css";
import NikoUI from "../../lib/NikoUI.umd.min";
export default ({ Vue, options, router, siteData, isServer }) => {
if (!isServer) {
Vue.use(NikoUI);
}
};
然后在 Button.md 中使用组件。
<n-button type="primary">test</n-button>
插件
我这边项目里使用了两个插件:
- @vuepress/plugin-back-to-top 滚动一定长度后,出现返回顶部的箭头。
- vuepress-plugin-demo-container https://calebman.github.io/vuepress-plugin-demo-container/zh/ 用于代码预览,可以避免 组件示例和示例代码本质上一样,却需要写两遍 的问题
具体可以参照项目内的代码,这里直接放一下 Button.md 的部分代码:
# Button
### 基础用法
基础的按钮用法。
::: demo 使用`type`、`plain`、`round`和`circle`属性来定义 Button 的样式。
```html
<template>
<div class="demo-button">
<div>
<n-button>默认按钮</n-button>
<n-button type="primary">主要按钮</n-button>
<n-button type="success">成功按钮</n-button>
<n-button type="info">信息按钮</n-button>
<n-button type="warning">警告按钮</n-button>
<n-button type="danger">危险按钮</n-button>
</div>
<div>
<n-button plain>朴素按钮</n-button>
<n-button type="primary" plain>主要按钮</n-button>
<n-button type="success" plain>成功按钮</n-button>
<n-button type="info" plain>信息按钮</n-button>
<n-button type="warning" plain>警告按钮</n-button>
<n-button type="danger" plain>危险按钮</n-button>
</div>
<div>
<n-button round>圆角按钮</n-button>
<n-button type="primary" round>主要按钮</n-button>
<n-button type="success" round>成功按钮</n-button>
<n-button type="info" round>信息按钮</n-button>
<n-button type="warning" round>警告按钮</n-button>
<n-button type="danger" round>危险按钮</n-button>
</div>
<div>
<n-button icon="n-icon-search" circle></n-button>
<n-button type="primary" icon="n-icon-edit" circle></n-button>
<n-button type="success" icon="n-icon-check" circle></n-button>
<n-button type="info" icon="n-icon-message" circle></n-button>
<n-button type="warning" icon="n-icon-star-off" circle></n-button>
<n-button type="danger" icon="n-icon-delete" circle></n-button>
</div>
</div>
</template>
` ` ` // 这边为了展示代码,```中间加了空格,实际需要去掉
:::
打包
npm run docs:build
如果遇到window is not defined这样的错误,可以将 enhanceApp.js 修改成如下:
// 引入打包后的样式和文件。
import "../../lib/NikoUI.css";
import NikoUI from "../../lib/NikoUI.umd.min";
export default async ({ Vue, options, router, siteData, isServer }) => {
if (!isServer) {
await import("../../lib/NikoUI.umd.min").then((NikoUI) => {
Vue.use(NikoUI);
});
}
};
打包完成后的文件在 /docs/.vuepress/dist,然后部署到线上就可以了。