仿 Element-ui 搭建Vue2组件库 &  Vuepress 编写组件库文档

仿 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 创建一个新项目。

项目目录

  1. 新增 src 平级的 packages 目录
  2. 复制 element-ui 中 button 组件(element-ui 包文件位置 packages/);以及配套的样式,在 packages 中新增 theme 文件夹用于存放样式;
  3. 保持样式层级不变,只复制有用到的样式(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 对其做按需加载;

  1. 根目录新建 components.json 文件,作为 打包的入口配置。
// 后续每次的新增都需要再次配置。
{
  "button": "./packages/button/index.js",
  "index": "./packages/index.js" // 主文件
}
  1. 修改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");
  },
};
  1. 然后直接执行的 npm run build。此时会发现打包出来的结果只有一个index.css文件,是因为现在已经修改成多入口了,样式需要在每个文件中再做单独引入。

解决方案:

  • 每个组件在导入一次文件
<!-- packages/button/src/button.vue -->
<style lang="scss" src="../../theme/button.scss"></style>
  • css 文件单独打包;参考 element-ui 使用 gulp 再分开打包

Description

当打包后的 lib/theme目录下出现了多个 css 文件后,就可以发布到 npm 了,可以在后面需要用到该组件库的项目中实现按需加载。

babel-plugin-component 按需引入

  1. 在新项目中安装插件,并且下载 babel-plugin-component。
npm install niko-ui babel-plugin-component -S
  1. 参考 element-ui 中的使用方式。注意:我这边打包的样式目录是 theme 不是 theme-chalk 需要修改一下。libraryName 则是库名 例如:niko-ui Description
  2. 新项目的 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 编写组件库文档

  1. 安装 VuePress 依赖。
npm install vuepress -D
  1. 根目录下创建 docs 文件夹并在内部创建 README.md 文件。
  2. package.json 添加运行脚本以及打包脚本。
"scripts": {
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
}
  1. 运行 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>

插件

我这边项目里使用了两个插件:

具体可以参照项目内的代码,这里直接放一下 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,然后部署到线上就可以了。

© 2025 Niko Xie