从零搭建基于 Vue 3.x + ElementPlus 的组件库

vue

前言

由于最近在我司开发中开启了Vue3的重构工作。于是乎,Vue组件库的抽离工作开启,此次算是基于 vue3.x + element-plus 的二次封装,封装常见通用组件。一年前做了 react+antd的组件库 的整合及示例,一年后再看该方案还有诸多缺陷,发现了很多新的东西可以用,在本次的Vue组件库技术选型中调研了更多的方式方法,最终选型不代表是最好的,但一定是现阶段我认为最合适该场景的技术选型,实际上写组件库并不复杂,技术调研和技术选型的整个流程下来,算是对各种方法方法及其优缺点有了一个稍微宽泛的认识,本文目的是记录下本次选型到最终成品的全过程。基础配置不在赘述,文章只大致记录大致的开发框架以及打包和单测,仅供参考。

不想看文章只想看代码的点这里 想先看下交互文档效果的点这里

目的

为什么要做组件库?这个问题也是老生常谈了,PC端后台xx管理系统这种场景下,通用的东西很容易抽象出来,这样就不需要每开一个新项目就在项目里写一套基础组件了,在可直接使用基础组件的基础上开发能省不少时间,本次只对集成度和通用度较高的几个组件进行抽离出几个 npm 包,先实现常见通用功能,后续扩展则可以在不改变原来架构的基础上进行添加功能。这些组件包括通用的布局组件、通用的表格组件、通用的表单组件、通用的文件图片上传组件等。

尤其是表格和表单组件,直接在页面中使用的话会出现很多重复代码,写起来代码冗余多,所以一般项目里就会封装这些基础组件来实现通用,想要实现的目的就是只需要通过不同的配置就能在页面中通用,一般这些其实都大同小异,这也是想要开发这样基础组件库的初衷,不必要在每个项目里都写一套,直接将组建库丢到 npm 管理已经后续升级,这样一来就能慢慢的沉淀出公司自己特有的基础和业务组件库,后续新项目开箱即用。

开始之前

开始之前,除了定好技术栈 Vue 3.x + ElementPlus 外,需要明确组件库的开发原则:简洁、高效、灵活、可扩展。

首先要有可读性好的文档库,有示例可交互;其次能自动化的重复工作绝不手动复制,文档库自动化部署;最后,最好还有组件测试来保证组件的正确性和完成性。

项目结构

├── docs                                     /* 组件库文档 */

│ .vuepress /* vuepress 配置 */

│ ├── clientAppEnhance.ts /* 注册全局组件 */

│ ├── config.ts /* vuepress配置文件 */

│ index.md /* 文档 */

└── packages /* 包 */

│ ├── layout /* 布局组件 */

│ │ ├── src /* vue组件 */

│ │ ├── package.json /* 组件配置文件 */

│ │ ├── typings /* 组件声明文件 */

│ ├── form /* 表单组件 */

│ ├── table /* 表格组件 */

├── templates /* plop 配置clone的模板文件夹 */

├── typings /* 声明文件夹 */

├── .eslintrc.js /* eslint 配置 */

├── .gitignore /* gitignore 配置 */

├── .prettierrc /* prettier 配置 */

├── .stylelintrc /* stylelint 配置 */

├── babel.config.js /* babel 配置 */

├── jest.config.js /* jest 配置 */

├── LICENSE /* license */

├── package.json /* package.json */

├── plopfile.js /* plop 配置 */

├── tsconfig.json /* ts 配置 */

├── rollup.config.js /* rollup 打包配置 */

└── README.md /* 文档说明文件 */

包管理模式

由于是组件库,多个组件包会有共用的依赖,为减少重复代码,因此选用 lerna + yarn workspace 来进行包管理,这也是现如今大多数组件库的选择。

组件打包

组件打包选用 rollup,因为本次的组件是针对几个通用场景来封装组件,打算分开包来进行管理,rollup 打包能打包多种模式的包 esm, cjs, umd 等等,并且esm自带 tree-shaking,打出来的包语义明确,也比较易于调试。

rollup 打包配置文件放到了最外层,对组件的打包进行统一配置

下面的配置有几个关键点:

  1. 多入口,每个组件分开打包,并且分别打包出 umd 格式的 index.js 文件以及 esm 格式的 index.module.js 文件
  2. babel 配置的时候需要手动添加 .ts 和 .vue 的扩展名来正常的编译 ts 和 vue 文件
  3. 每个包下的 package.json 声明 main module 和 typings ,当支持 esm 方式加载的时候回默认加载 index.module.js,否则加载 index.js
  4. 配置的时候将 peerDependencies 添加到 external 配置项中,将peerDependencies的包不打包进去,减小包体积,提高打包效率
  5. esm 支持 tree-shaking,故css不分开打包,这样直接使用 esm 格式就会按需加载,无需借助插件

rollup.config.js

/* eslint-disable @typescript-eslint/no-var-requires */

import fs from 'fs'

import path from 'path'

import json from '@rollup/plugin-json'

import postcss from 'rollup-plugin-postcss'

import vue from '@vitejs/plugin-vue'

import { terser } from 'rollup-plugin-terser'

import { nodeResolve } from '@rollup/plugin-node-resolve'

import typescript from '@rollup/plugin-typescript'

import babel from '@rollup/plugin-babel'

import commonjs from '@rollup/plugin-commonjs'

import { DEFAULT_EXTENSIONS } from '@babel/core'

const isDev = process.env.NODE_ENV !== 'production'

// packages 文件夹路径

const root = path.resolve(__dirname, 'packages')

// 公共插件配置

const getPlugins = () => {

return [

vue(),

typescript({

tsconfig: './tsconfig.json'

}),

nodeResolve({

mainField: ['jsnext:main', 'browser', 'module', 'main'],

browser: true

}),

commonjs(),

json(),

postcss({

plugins: [require('autoprefixer')],

// 把 css 插入到 style 中

inject: true,

// 把 css 放到和js同一目录

// extract: true

// Minimize CSS, boolean or options for cssnano.

minimize: !isDev,

// Enable sourceMap.

sourceMap: isDev,

// This plugin will process files ending with these extensions and the extensions supported by custom loaders.

extensions: ['.sass', '.less', '.scss', '.css']

}),

babel({

exclude: 'node_modules/**',

babelHelpers: 'runtime',

// babel 默认不支持 ts 需要手动添加

extensions: [...DEFAULT_EXTENSIONS, '.ts', '.tsx', '.vue']

}),

// 如果不是开发环境,开启压缩

!isDev && terser({ toplevel: true })

]

}

module.exports = fs

.readdirSync(root)

// 过滤,只保留文件夹

.filter(item => fs.statSync(path.resolve(root, item)).isDirectory())

// 为每一个文件夹创建对应的配置

.map(item => {

const pkg = require(path.resolve(root, item, 'package.json'))

return {

input: path.resolve(root, item, 'src/main.ts'),

output: [

{

name: 'index',

file: path.resolve(root, item, pkg.main),

format: 'umd',

sourcemap: isDev,

globals: {

vue: 'vue',

'element-plus': 'element-plus'

}

},

{

name: 'index.module',

file: path.join(root, item, pkg.module),

format: 'es',

sourcemap: isDev,

globals: {

vue: 'vue',

'element-plus': 'element-plus'

}

}

],

onwarn: function (warning) {

if (warning.code === 'THIS_IS_UNDEFINED' || warning.code === 'CIRCULAR_DEPENDENCY') {

return

}

console.error(`(!) ${warning.message}`)

},

plugins: getPlugins(),

external: Object.keys(require(path.join(root, item, 'package.json'))?.peerDependencies || {})

}

})

组件库文档

sum-ui 组件库文档

本次对比之前的文档库考虑上有所不同,用的是 vuepress ,选它的原因之一是页面简洁灵活,利用插件不仅可以配置组件交互说明,还能配置其他说明引导文档,之前考虑了 vue-styleguidst ,但是其局限性比较强,只能配置组件交互文档并且页面样式没有vuepress 的简洁好看,还调研了 vitepress ,但因为 vitepress 还一直在 WIP 并且把 vuepress 里的 plugins 等多项配置去掉了,如果是纯说明文档用这个完全够,但我们需要有组件交互说明,因而最终还是选择了支持Vue3的 vuepress@next。

vuepress 打包

除了webpack,vuepress@next 还添加了 vite 开发打包的方式,可以在 .vuepress/config.ts 下进行配置

下面的配置有几个关键点:

  1. 读取packges文件夹下的文件夹名,给引用的包添加 alias 别名
  2. 由于组件里支持了jsx语法,所以添加了 @vitejs/plugin-vue-jsx 插件
  3. bundler 的配置(@vuepress/webpack / @vuepress/vite ),如果不设置则默认 webpack, 如果安装了 vuepress-vite 则默认vite打包
  4. 添加 vuepress 插件 vuepress-plugin-demoblock-plus ,该插件参照了 element-plus 的文档渲染实现做了交互组件渲染
  5. 由于使用了 GitHub Actions 自动化部署文档到 GitHub pages, 所以 base 选项的配置需要和github的项目名保持一致,因为加载的静态资源路径是该文件夹下的

.vuepress/config.js

const { readdirSync } = require('fs')

const { join } = require('path')

const chalk = require('chalk')

const headPkgList = []; // 非 @sum-ui/开头的组件

const pkgList = readdirSync(join(__dirname, '../../packages')).filter(

(pkg) => pkg.charAt(0) !== '.' && !headPkgList.includes(pkg),

);

const alias = pkgList.reduce((pre, pkg) => {

pre[`@sum-ui/${pkg}`] = join(__dirname, '../../packages', pkg, 'src/Index.vue');

return {

...pre,

};

}, {});

console.log(`???? alias list \n${chalk.blue(Object.keys(alias).join('\n'))}`);

module.exports = {

title: "sum-ui", // 顶部左侧标题

description: 'Vue3 + ElementPlus 组件库',

base: '/sum-ui/',

bundler: '@vuepress/vite',

bundlerConfig: {

viteOptions: {

plugins: [

vueJsx()

]

}

},

alias,

head: [

// 设置 描述 和 关键词

[

"meta",

{ name: "keywords", content: "Vue3 UI 组件库" },

]

],

themeConfig: {

sidebar: {

// 侧边栏

"/": [

{

text: "介绍",

children: [

{ text: "安装", link: "/guide/install" },

{ text: "快速上手", link: "/guide/start" },

],

},

{

text: "组件",

children: [

{ text: "Layout 布局", link: "/components/layout" },

{ text: "Table 表格", link: "/components/table" }

],

},

],

},

nav: [

// 顶部右侧导航栏

{ text: "介绍", link: "/", activeMatch: "^/$|^/guide/" },

{

text: "组件",

link: "/components/layout.html",

activeMatch: "^/$|^/components/"

}

],

// page meta

editLinkText: '在 GitHub 上编辑此页',

lastUpdatedText: '上次更新',

contributorsText: '贡献者',

},

plugins: ['demoblock-plus'] // vuepress-plugin-demoblock-plus 插件,作用是展示交互文档和代码展开

};

.vuepress/clientAppEnhance.ts

除 config.ts 的配置外,还需要全局注册组件才生效,需要加 clientAppEnhance.ts 来进行配置

import { defineClientAppEnhance } from '@vuepress/client'

import 'element-plus/theme-chalk/src/index.scss' // 全量引入样式文件 scss TODO: 这里如果用element-plus 文档里的方法 vite-plugin-element-plus 插件按需引入的话,dev 正常但是 vuepress build 打包 scss @import 就会报错

import SumTable from '@sum-ui/table'

import SumLayout from '@sum-ui/layout'

export default defineClientAppEnhance(({ app }) => {

app.component('SumTable', SumTable)

app.component('SumLayout', SumLayout)

})

组件开发预览

交互文档库配置完成之后,就能边开发组件库,边看组件最终效果了

yarn docs:dev // vuepress 文档库开发模式

yarn docs:build // vuepress 文档库打包成静态资源文件

打包生成的资源文件可以利用 Github Actions 自动部署到 GitHub Pages 上
sum-ui组件库文档地址

组件测试

组件测试放到每个组件目录下,组件写完可以写该组件的单元测试

vue 的单测用 @vue/test-utils 就可以,另外在组件测试中导入组件的时候,不可直接识别 ts、vue 文件,需要 ts-jestvue-jestbabel-jest 来做转换

配置 jest.config.js

const alias = require('./alias')

module.exports = {

globals: {

// work around: https://github.com/kulshekhar/ts-jest/issues/748#issuecomment-423528659

'ts-jest': {

diagnostics: {

ignoreCodes: [151001]

}

}

},

testEnvironment: 'jsdom',

transform: {

'^.+\\.vue$': 'vue-jest',

'^.+\\.(t|j)sx?$': [

'babel-jest',

{

presets: [

[

'@babel/preset-env',

{

targets: {

node: true

}

}

],

[

'@babel/preset-typescript',

{

isTSX: true,

allExtensions: true

}

]

]

}

]

},

moduleNameMapper: alias, // 声明别名以便于在jest中导入文件加载的时候能够正确加载文件

moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],

// u can change this option to a more specific folder for test single component or util when dev

// for example, ['<rootDir>/packages/input']

roots: ['<rootDir>']

}

vue 支持 tsx

babel 的 preset 配置 isTSX: true, allExtensions: true 两个选项,allExtensions 为 true 支持所有扩展名,主要是为了支持 .vue 文件的解析,isTSX 为 true 支持 jsx 语法的解析

babel.config.js

module.exports = {

// ATTENTION!!

// Preset ordering is reversed, so `@babel/typescript` will called first

// Do not put `@babel/typescript` before `@babel/env`, otherwise will cause a compile error

// See https://github.com/babel/babel/issues/12066

presets: [

'@vue/cli-plugin-babel/preset',

[

'@babel/typescript',

{

isTSX: true,

allExtensions: true

}

]

],

plugins: ['@babel/transform-runtime']

}

主题色相关

由于 element-plus 使用了 css 变量,可以通过改变 css 变量来覆盖主题色

main.js

import { themeVarsKey } from 'element-plus'

const themeVars = {

'--el-color-primary': '#29b6b0'

}

const app = createApp(App)

app.provide(themeVarsKey, themeVars)

其他可能会遇到的问题

Cannot read property 'isCE' of null 报错 https://github.com/vuejs/vue-next/issues/4344

在我本地打包组件后 yarn link 到全局,然后在其他项目里引用的时候遇到这个报错,是由于多个 Vue 包引用问题,发布到 npm 之后从 npm 安装引用正常

以上是 从零搭建基于 Vue 3.x + ElementPlus 的组件库 的全部内容, 来源链接: utcz.com/z/380109.html

回到顶部