仿Element自定义Vue组件库🍊

3,894 阅读8分钟

前言 🍊

市面上目前已有各种各样的UI组件库,他们的强大毋庸置疑。但是有时候我们有必要开发一套属于自己团队的定制化组件库。还有时候原有的组件不能满足我们的各种需求,就需要在原有的组件上进行改造。

所以本文的目的就是让读者能通过此文,小能做一个简单的插件供人使用,大能架构和维护一个组件库不在话下。欢迎指点,有疑问的也可以欢迎下方评论留言。

技术栈🍊

  • vue-cli3的基本创建和打包技巧
  • vue的基本语法
  • npm的发布

完整项目目录结构🍊

vase-ui
├─ .eslintrc.js
├─ .gitignore
├─ .npmignore
├─ babel.config.js
├─ deploy.sh
├─ docs   // vuepress开发目录
│  ├─ .vuepress
│  │  ├─ components   // 在markdown中可以使用的vue组件
│  │  │  ├─ vs-button.vue
│  │  │  └─ vs-home.vue
│  │  ├─ config.js   // vurepess配置修改入口,包括左边sidebar,右上方nav导航菜单等
│  │  └─ dist   // vuepress打包目录
│  │     ├─ 404.html
│  │     ├─ assets
│  │     │  ├─ css
│  │     │  ├─ img
│  │     │  └─ js
│  │     ├─ index.html
│  │     └─ views   // vuepress视图文件,格式是markdown
│  │        ├─ components
│  │        │  └─ basic
│  │        │     └─ index.html
│  │        └─ guide
│  │           ├─ get-started.html
│  │           └─ install.html
│  ├─ README.md
│  └─ views
│     ├─ components
│     │  └─ basic
│     │     └─ README.md
│     └─ guide
│        ├─ get-started.md
│        └─ install.md
├─ package-lock.json
├─ package.json   // 与npm发布相关,记录版本号,包入口文件地址
├─ packages   // 组件库源码目录
│  ├─ button
│  │  ├─ index.js
│  │  └─ src
│  │     └─ button.vue
│  ├─ fonts
│  │  ├─ font.scss
│  │  └─ src
│  │     ├─ element-icons.ttf
│  │     └─ element-icons.woff
│  ├─ index.js   // 组件库源码组件入口文件,执行npm run build的目标文件
│  └─ keep-alive
│     └─ index.js
├─ public   //公共资源入口,如favicon
│  ├─ favicon.ico
│  └─ index.html
├─ README.md
├─ test
│  ├─ App.vue
│  ├─ main.js
│  └─ TestButton.vue
├─ types
│  ├─ button.d.ts
│  ├─ component.d.ts
│  ├─ index.d.ts
│  └─ vase-ui.d.ts
├─ vue.config.js
└─ yarn.lock

项目规划🍊

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

创建项目

项目名起自己觉得有意义的名字就行了

$ vue create vase-ui

注意:由于我们是开发一个第三方依赖库,我们选择 Manually select features

选择那些特性需要安装在项目中

 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 ( ) Router
 ( ) Vuex
 (*) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

复制代码系统默认的包含了基本的 Babel + ESLint 设置的 preset,我们只需要选择CSS配置。移动键盘上下键选择需要的特性,按下键盘空格键即可选中

安装哪一种 CSS 预处理语言

  Sass/SCSS (with dart-sass)
  Sass/SCSS (with node-sass)
  Less
  Stylus

复制代码由于Element UI中的样式采用Sass,所以我们选择第一项即可 为什么不选择第二项呢? 因为dart-sassnode-sass更好下载

选择代码风格

  ESLint with error prevention only
  ESLint + Airbnb config
  ESLint + Standard config
  ESLint + Prettier

一般选 ESLint + Prettier的组合

那种方式进行代码格式检测

 (*) Lint on save
 ( ) Lint and fix on commit

复制代码选择Ctrl+S保存时检测代码格式即可

配置文件生成方式

  In dedicated config files
  In package.json

看个人喜好,我选第一种

是否保存预配置

Save this as a preset for future projects? (y/N)

复制代码看项目需要,我这里选择 N。回车后,系统会自动帮我们把选择的配置集成到模板中,然后生成一个完整的项目。

调整目录

这里我们参考element的目录结构

删除src、assets目录,在根目录中创建一个packages目录用来存放我们要开发的UI组件;在根目录创建一个test目录用于测试我们自己开发的UI组件(再引入了vuepress之后这个文件夹也不再需要了)。

|-- packages      // 将原src目录改为 packages 用于编写存放组件

新增packages目录,该目录未加入webpack编译

注:cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。

注: vue的package文件夹采用了monorepo:monorepo是一种将多个package放在一个repo中的代码管理模式,摒弃了传统的多个package多个repo的模式。关于monorepo可以在 https://juejin.cn/post/6844903961896435720这篇文章中了解

webpack配置修改

packages 是我们新增的一个目录,默认是不被 webpack 处理的,所以需要添加配置对该目录的支持。

chainWebpack 是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。在根目录创建vue.config.js文件,作如下配置:

// 修改 src 为 test
const path = require("path");
module.exports = {
  pages: {
    index: {
      entry: "test/main.js",
      template: "public/index.html",
      filename: "index.html"
    }
  },
  // 扩展 webpack 配置,使 packages 加入编译
  chainWebpack: config => {
    config.module
      .rule("js")
      .include.add(path.resolve(__dirname, "packages"))
      .end()
      .use("babel")
      .loader("babel-loader")
      .tap(options => {
        return options;
      });
  }
};

组件编写🍊

button组件

  1. packages 目录下,所有的单个组件都以文件夹的形式存储,所有这里创建一个目录 button
  2. button 目录下创建 src 目录存储组件源码
  3. button 目录下创建 index.js 文件对外提供对组件的引用。
  4. 创建fonts文件夹作用是存放element的一些基础样式
  • /packages/button/src/button.vue核心组件代码如下(样式代码这里不贴出,可以在代码仓库找到):
<template>
  <button
    class="vs-button"
    :disabled="disabled"
    @click="handleClick"
    :class="[
      type ? `vs-button--${type}` : '',
      buttonSize ? `vs-button--${buttonSize}` : '',
      {
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
        'is-disabled': disabled
      }
    ]"
  >
    <i :class="icon" v-if="icon"></i>
    <!-- 如果没有传入插槽的时候才显示 -->
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script>
export default {
  name: "VsButton",
  props: {
    size: String,
    type: {
      type: String,
      default: "default"
    },
    plain: {
      type: Boolean,
      default: false
    },
    round: {
      type: Boolean,
      default: false
    },
    circle: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    icon: {
      type: String,
      default: ""
    }
  },
  methods: {
    handleClick(e) {
      this.$emit("click", e);
    }
  }
};
</script>
<style lang="scss">
</style>
  • 修改/packages/button/index.js文件,对外提供引用:
import VsButton from "./src/button.vue";
// 为组件提供 install 安装方法,供按需引入
VsButton.install = function(Vue) {
  Vue.component(VsButton.name, VsButton);
};
export default VsButton;

keep-alive组件

介绍:这个组件是对Vue前进刷新后退不刷新,简易页面的堆栈实现

  1. packages 目录下创建 keep-alive
  2. keep-alive 目录下创建 index.js 文件在里头用函数式组件并对外提供对组件的引用。
  • 完整代码如下:
import Vue from "vue";

let cacheKey = "cacheTo";
let $router = { beforeEach: () => {} };
// Vue.observable处理使组件的store具有可插拔性
const state = Vue.observable({
  caches: []
});
const clearCache = () => {
  if (state.caches.length > 0) {
    state.caches = [];
  }
};
const addCache = name => state.caches.push(name);

const beforeEach = () => {
  $router.beforeEach((to, from, next) => {
    // 1. 都不是类列表页
    //     清空缓存
    // 2. 都是类列表页
    //     若`to`不在`from`的配置中,清空缓存,同时要新增`to`缓存
    //     保留`from`的缓存,新增`to`缓存
    // 3. 新路由是类列表页
    //     若`from`不在`to`的配置中,清空缓存,新增`to`缓存
    //     否则,无需处理
    // 4. 旧路由是类列表页
    //     若`to`不在`from`的配置中,清空缓存

    const toName = to.name;
    const toCacheTo = (to.meta || {})[cacheKey];
    const isToPageLikeList = toCacheTo && toCacheTo.length > 0;
    const fromName = from.name;
    const fromCacheTo = (from.meta || {})[cacheKey];
    const isFromPageLikeList = fromCacheTo && fromCacheTo.length > 0;

    if (!isToPageLikeList && !isFromPageLikeList) {
      clearCache();
    } else if (isToPageLikeList && isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache();
      }
      addCache(toName);
    } else if (isToPageLikeList) {
      if (toCacheTo.indexOf(fromName) === -1) {
        clearCache();
        addCache(toName);
      }
    } else if (isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache();
      }
    }
    next();
  });
};
const VsKeepAlive = {
  install(Vue, options = { key: "", router: "" }) {
    const { key = "cacheTo", router } = options;

    if (key) {
      cacheKey = key;
      $router = router;
      beforeEach();
    }

    const component = {
      name: "VsKeepAlive",
      functional: true,
      render(h, { children }) {
        return h("keep-alive", { props: { include: state.caches } }, children);
      }
    };

    Vue.component("VsKeepAlive", component);
  }
};
export default VsKeepAlive;

使用注意事项:

  1. 路由配置不能少了name属性,并且这个name需要和组件name一样
  2. cacheTo优先级小于keepAlive,所以,处理这种需求的页面不要设置keepAlive
  3. 可以设置2个页面之前仅在相互切换时缓存,不过我还没发现可用的场景

导出组件

  • 整合所有的组件,对外导出,即一个完整的组件库 修改 /packages/index.js 文件,对整个组件库进行导出:
import Button from "./button";
import KeepAlive from "./keep-alive";

import "./fonts/font.scss";
// 存储组件列表
const components = [Button];

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function(Vue, options = { key: "", router: {} }) {
  const { key = "cacheTo", router } = options;
  // 遍历注册全局组件
  components.forEach(function(item) {
    if (item.install) {
      Vue.use(item);
    } else if (item.name) {
      Vue.component(item.name, item);
    }
  });
  Vue.use(KeepAlive, { key, router });
};

// 判断是否是直接引入文件
if (typeof window !== "undefined" && window.Vue) {
  install(window.Vue);
}
export { Button, KeepAlive };
export default {
  // 导出的对象必须具有 install,才能被 Vue.use() 方法安装
  version: "0.3.4",
  install
};

注:如果需要使用我们的keep-alive组件时,需要在注册时传入router参数。

这里我们可以看下element是怎么对这一块进行定义的:

npm发布🍊

  • package.json 中的script下新增一条编译为库的命令,然后就可以使用npm run lib命令进行打包啦
"scripts": {
    "lib": "vue-cli-service build --target lib --name vase-ui --dest lib packages/index.js"
  },

注:

  1. --target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
  2. dest : 输出目录,默认 dist。这里我们改成 lib
  3. [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录
  • 执行编译库命令

npm run lib

  • 配置 package.json 文件中发布到 npm 的字段
  1. name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
  2. version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
  3. description: 描述。
  4. main: 入口文件,该字段需指向我们最终编译后的包文件。
  5. keyword:关键字,以空格分离希望用户最终搜索的词。
  6. author:作者
  7. private:是否私有,需要修改为 false 才能发布到 npm
  8. license: 开源协议
  9. 希望打包库的产生文件
  10. browserslist: 指定了项目的目标浏览器的范围
  11. repository: 指定代码所在的位置。这对想要贡献的人很有帮助。如果git repo在GitHub上,那么该npm docs 命令将能够找到你。

以下为我的参考设置

{
 "name": "vase-ui",
  "version": "0.3.4",
  "description": "A Component Library for Vue.js.",
  "private": false,
  "main": "lib/vase-ui.common.js",
  "files": [
    "lib",
    "src",
    "packages",
    "types"
  ],
  "repository": {
    "type": "git",
    "url": "git@github.com:JohnYu588/vase-ui.git"
  },
  "author": "yzx",
  "license": "MIT",
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
 }
  • 添加 .npmignore 文件,设置忽略发布文件
# 忽略目录
test/
packages/
public/
docs/
node_modules/

# 忽略指定文件
vue.config.js
babel.config.js
*.map
.editorconfig.js
  • 发布npm
  1. npm官网注册一个账号
  2. 执行npm login输入账号密码登录
  3. 执行npm publish上传
  4. 发布成功后稍等几分钟,即可在 npm 官网搜索到。以下是刚提交的

注意

  1. 一定要在package.json的scripts中添加main方便其他人下载时找到对应打包的文件
  2. 上传到npm上时,要将package.json中的private属性值改为false
  3. 修改源码后发布到npm时一定要更改项目的版本号

官网制作🍊

使用vue press

  • 在原有项目中使用
# 安装依赖
npm install -D vuepress
# 创建一个 docs 目录
mkdir docs
  • package.json中进行脚本配置
  "scripts": {
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  },
  • 简单配置 在docs/.vuepress下新建文件config.js
module.exports = {
  base: '/vase-ui/',
  title: 'Vase UI',
  description: 'Inspiration from heian vase',
  head: [['link', { rel: 'icon', href: '/favicon.ico' }]],
  themeConfig: {
    nav: [
      { text: 'Home', link: '/' },
      { text: 'Github', link: 'https://github.com/JohnYu588/vase-ui/' },
    ],
    sidebar: [
      {
        title: '开发指南',
        collapsable: true,
        children: ['views/guide/install.md', 'views/guide/get-started.md'],
      },
      {
        title: '组件',
        collapsable: true,
        children: ['views/components/basic/'],
      },
    ],
  },
};
  • 使用vue组件

官网中提到,所有在 .vuepress/components 中找到的 *.vue 文件将会自动地被注册为全局的异步组件,可以在markdown中引用,我们可以在这里编写展示案例 vue文件中的代码高亮我用的是vue-highlightjs: 在/docs/.vuepress/components/下创建按钮vs-botton.vue文件代码如下:

<template>
  <div>
    <h3>基础用法</h3>
    <vs-button>default</vs-button>
    <vs-button type="primary">primary</vs-button>
    <vs-button type="info">info</vs-button>
    <vs-button type="success">success</vs-button>
    <vs-button type="warning">warning</vs-button>
    <vs-button type="danger">danger</vs-button>
    <pre v-highlightjs><code class="vue">{{code1}}</code></pre>
  </div>
</template>

<script>
import btn from '../../../packages/button/src/button';
import Vue from 'vue'
import VueHighlightJS from 'vue-highlightjs';
Vue.use(VueHighlightJS);

export default {data() {
    return {
      code1: `
      <vs-button>default</vs-button>
      <vs-button type="primary">primary</vs-button>
       `
        .replace(/^\s*/gm, '')
        .trim(),
      code2: `
       <s-button disabled type="primary">disabled</s-button>    
       `
        .replace(/^\s*/gm, '')
        .trim(),
      code3: `
        <s-button  icon="home"  type="primary">home</s-button>
        <s-button  icon="phone-fill" type="primary" icon-position="right">call</s-button>
        <s-button  icon="visible"  type="primary">show password</s-button>
       `
        .replace(/^\s*/gm, '')
        .trim(),
      code4: `
       <s-button  loading icon="download"  type="primary">加载中</s-button>
       `
        .replace(/^\s*/gm, '')
        .trim(),
      code5: `
        <s-button-group>
          <s-button icon="left" icon-position="left">prev</s-button>
          <s-button>middle</s-button>
          <s-button icon="right" icon-position="right">next</s-button>
        </s-button-group>
       `
        .replace(/^ {8}/gm, '')
        .trim(),
    };
  },
  components: {
    'vs-button': btn,
  },
};
</script>
// 样式这里不贴出
<style lang="scss" scoped></style>
  • 编写文档

由于所有的页面在生成静态 HTML 时都需要通过 Node.js 服务端渲染,对于SSR 不怎么友好的组件(比如包含了自定义指令),你可以将它们包裹在内置的 ClientOnly 组件中,而且注意因为是ssr,组件内部beforeCreate, created生命周期钩子函数访问不到浏览器 / DOM 的 API,只能在beforeMount和mounted中调用。

/docs/views/components/basic下创建README.md:

---
title: 'Basic 基础'
sidebarDepth: 2
---

## Button 按钮

<ClientOnly>
  <vs-button/>
<font size=5>Attributes</font>
| 参数| 说明 | 类型 | 可选值 | 默认值 |
| :------ | ------ | ------ | ------ | ------ |
| type | 按钮类型 | string |primary, info, success, warning, danger | - |
| disabled | 按钮是否禁用 | boolean |- | false |
| icon | 按钮上图标名称 | string |- | - |
| icon-position | 图标在按钮的左右位置 | string|left, right  | - |
| loading | 显示加载中图标 | boolean |- | false |

</ClientOnly>

注:参考上面在guide目录下创建安装帮助和开始页的文档(具体看我的git)

  • 部署到github

官网上介绍的很清楚,点这里。 在项目根目录下新增deploy.sh,windows下直接命令行运行./deploy.sh即可发布到github pages上。

deploy.sh:

#!/usr/bin/env sh

# 确保脚本抛出遇到的错误
set -e

# 生成静态文件
npm run docs:build

# 进入生成的文件夹
cd docs/.vuepress/dist

# 如果是发布到自定义域名
# echo 'www.example.com' > CNAME

git init
git add -A
git commit -m 'deploy'

# 如果发布到 https://<USERNAME>.github.io
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master

# 如果发布到 https://<USERNAME>.github.io/<REPO>
git push -f git@github.com:JohnYu588/vase-ui.git master:gh-pages

cd -
  • 预览 执行yarn docs:dev命令预览官网:

上传完毕后在https://johnyu588.github.io/vase-ui/上直接可以看到

使用新发布的组件库🍊

npm install vase-ui -S
  • 引用 在项目的main.js中引入有两种方式
  1. 全局注册
import VaseUI from "vase-ui";
Vue.use(VaseUI, { router });
  1. 按需引入
import { Button, KeepAlive } from "vase-ui";
Vue.use(Button)
.use(KeepAlive, { router });
  • 使用
  <vs-button icon="vs-icon-check" circle plain type="primary">测试</vs-button>

关于keep-alive的使用参考Vue前进刷新后退不刷新,简易页面堆栈实现[1]

git地址:https://github.com/JohnYu588/vase-ui

参考🍊

少女风vue组件库制作全攻略~~

从零实现一套属于自己的UI框架-发布到npm

Vue前进刷新后退不刷新,简易页面堆栈实现

参考资料

[1]

Vue前进刷新后退不刷新,简易页面堆栈实现: https://juejin.cn/post/6844904002526642184。