定制你的专属Vue组件库

590 阅读4分钟
业务后续的需求会复用很多之前开发的组件,于是打算抽成组件库,提升后续开发效率。本文主要讲解如何搭建并发布基于vue的组件库,以及利用Vuese自动生成组件文档。

vue/cli 3.x初始化项目

vue create zui

初始化过程中按默认配置即可。

修改项目结构

修改前:


修改后:


1.为了更具语意化,将src重命名成为examples,同时需要新增vue.config.js文件来配置项目启动入口。

// vue.config.js
module.exports = {
  pages: {
    index: {
      entry: 'examples/main.js',
      template: 'public/index.html',
      filename: 'index.html'
    }
  }
}

2.新增文件夹components,在这里开发我们的组件。

开发一个测试组件

1.在lib下新建自定义组件目录


(1) main.vue 编写组件逻辑

// src/main.vue<template>
  <h1 class="z-demo">Demo</h1>
</template>

<script>
export default {
  name: 'Demo'
}
</script>

(2) demo.scss 组件样式

// demo.scss
.z-demo{
    color: aqua;
}

(3) index.js 导出组件

// index.js
import Demo from './src/main.vue'

// eslint-disable-next-line func-names
Demo.install = function(Vue) {
  Vue.component(Demo.name, Demo)
}

export default Demo

到此即可按需载入组件,下一步是为了实现全局引用功能。

2.在components目录下,配置index.js来导出所有组件,配置index.scss引入所有样式。

// components/index.js
import Demo from './demo';

import { version } from '../../package.json';

const components = {
  Demo
};

const install = function (Vue) {
  if (install.installed) return;
  Object.keys(components).forEach(key => {
    Vue.component(components[key].name, components[key]);
  })
};

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

const API = {
  version,
  install,
  ...components
};

export default API;
// components/css/index.scss
@import './demo.scss';

本地测试

1.引入组件库

// examples/main.js
import Vue from 'vue'
import App from './App.vue'

import '../components/css/index.scss'import Zui from '../components/lib'
Vue.use(Zui)

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

2.使用组件

// examples/App.vue
<template>
  <div id="app">
    <Demo/>
  </div>
</template>

<script>
export default {
  name: 'app',
}
</script>

3.效果


组件库打包

目前为止,采用的是后编译形式,只要把该组件库发布到npm上,就可以直接使用。

但一般的第三方库,都会采用预编译形式,提前打包以提供各种版本的文件。

1.使用gulp打包css文件

// gulpfile.js
const gulp = require("gulp")
const sass = require('gulp-sass')
const minifyCSS = require('gulp-minify-css')
const del = require('del');

gulp.task("sass", async function() {
  await del(['dist/css']);    
  return gulp.src("components/css/**/*.scss")    
    .pipe(sass())
    .pipe(minifyCSS())    
    .pipe(gulp.dest("dist/css"))
})

2.打包js,这里我分别提供了rollup和webpack两种打包方式。

建议采用rollup,因为可以导出es6模块-未来标准。

(1) rollup 方式

// rollup.js
const rollup = require('rollup');
const resolve = require('rollup-plugin-node-resolve');//可以告诉 Rollup 如何查找外部模块
const vue = require('rollup-plugin-vue');
const commonjs = require('rollup-plugin-commonjs');//将 CommonJS 模块转换为 ES6
const json = require('rollup-plugin-json');
const babel = require('rollup-plugin-babel');
const { terser } = require("rollup-plugin-terser");
const fs = require('fs');
const path = require('path');
const glob = require("glob");

async function makeList(dirPath){
  const list = {};
  const files = glob.sync(`${dirPath}/**/index.js`);
  for(let file of files){
    const output = file.split(/[/.]/)[2];
    list[output] = {
      input: file,
      output
    };
  }
  return list;
}

const formatTypeList = [
  { format: 'cjs', min: false, suffix: '.js' },
  { format: 'cjs', min: true, suffix: '.common.min.js' },
  { format: 'umd', min: false, suffix: '.umd.js' },
  { format: 'umd', min: true, suffix: '.umd.min.js' },
  { format: 'es', min: false, suffix: '.js' },
  { format: 'es', min: true, suffix: '.es.min.js' },
]

start('dist/','components/lib');

async function start(outputPath, libPath){
  fsExistsSync(outputPath) && removeDir(outputPath);
  createDir(outputPath);
  const list = await makeList(libPath);
  for({format,min,suffix} of formatTypeList){
    await build(list, format, min, suffix)
  }
}

async function build(list, format, min, suffix){
  console.log(`开始打包成 ${format}${min?'.min':''} 格式`);
  for(moduleName of Object.keys(list)){
    await buildFile(list[moduleName].input, list[moduleName].output, format, min, suffix);
  }
  console.log(`${format}${min?'.min':''} 格式文件打包完成`);
  console.log('=========================================');
}

async function buildFile(input, outputName, format, min, suffix){
  console.log(`start to build file:${outputName}`)
  const bundle = await rollup.rollup({
    input,
    output: {
      file: `dist/${outputName}${suffix}`,
      format,
      name: outputName
    },
    plugins: [ 
      resolve(),
      commonjs(),
      vue(),
      json(),
      babel({
        babelrc: false,// 忽略外部配置文件
        exclude: 'node_modules/**',
        runtimeHelpers: true,
      }),
      min && terser()
    ]
  })
  const { output: outputData } = await bundle.generate({
    format,
    name: outputName
  });
  await write({ output: outputData, fileName: outputName, suffix })
  console.log(`finished building file:${outputName}${suffix}`)
}

async function write({ output, fileName, suffix } = {}) {
  for (const { code } of output) {
    fs.writeFileSync(`dist/${fileName}${suffix}`, code)
  }
}

function removeDir(dir) {
  let files = fs.readdirSync(dir)
  for(var i=0;i<files.length;i++){
    let newPath = path.join(dir,files[i]);
    let stat = fs.statSync(newPath)
    if(stat.isDirectory()){
      //如果是文件夹就递归下去
      removeDir(newPath);
    }else {
     //删除文件
      fs.unlinkSync(newPath);
    }
  }
  fs.rmdirSync(dir)//如果文件夹是空的,就将自己删除掉
}

function createDir(dir) {
  let paths = dir.split('/');
  for(let i =1;i<paths.length;i++){
    let newPath = paths.slice(0,i).join('/');
    try{
    //是否能访问到这个文件,如果能访问到,说明这个文件已经存在,进入循环的下一步。
    //accessSync的第二个参数就是用来判断该文件是否能被读取
      fs.accessSync(newPath,fs.constants.R_OK);
      
    }catch (e){
      fs.mkdirSync(newPath);
    }
  }
}

function fsExistsSync(dir){
  try {
    fs.accessSync(dir, fs.F_OK)
  } catch (e) {
    return false
  }
  return true
}

(2) webpack 方式

// webpack.component.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');// 清理文件夹
const { VueLoaderPlugin } = require('vue-loader')
const glob = require("glob");

const list = {};

async function makeList(dirPath,list){
  const files = glob.sync(`${dirPath}/**/index.js`);
  for(let file of files){
    const output = file.split(/[/.]/)[2];
    list[output] = `./${file}`;
  }
}

makeList('components/lib',list);

module.exports = {
  entry: list,
  mode: 'development',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist2'),
    library: 'zui-pure',
    libraryTarget: 'umd'
  },
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
          }
        ]
      }
    ]
  },
};

3.更新package.json,新增构建命令

{  ...
  "scripts": {
    "serve": "vue-cli-service serve",
    "lint": "vue-cli-service lint",
    "build:js": "node rollup.js",
    "build:css": "npx gulp sass",
    "build": "npm run build:js && npm run build:css"
  },  ...
}

组件库发布

1.更新package.json和README.md

其中 files字段设置要上传到npm上的文件。

// package.json{
  "name": "zui-pure",
  "version": "0.0.1",
  "description": "基于vue的管理端组件库",
  "main": "dist/index.umd.js",
  "keywords": [
    "zui",
    "vue",
    "ui"
  ],
  "author": "zackguo",
  "license": "ISC",  "files": [    "dist",    "components"  ]
}
// README.md# Zui 组件库

> 在 main.js 中引入组件库

```
// 全部引入
import ZUI from "@tencent/zui-pure";
Vue.use(ZUI);

// 按需引入
import { Demo } from "@tencent/zui-pure";
Vue.use(Demo);

```

Copyright (c) 2019-present zackguo

2.注册/登录npm账户

npm adduser

3. 发布tnpm私有包(在npm-package目录下)

npm publish

4.登录npm官网查看


组件库测试

1.全量引入方式

(1) 新建vue工程。

(2) 安装组件库。

tnpm i @tencent/zui-pure

(3) 在main.js引入组件库,在App.vue使用组件。

测试成功


2.按需加载

(1) 安装babel-plugin-component插件,并且在babel.config.js中新增配置。

tnpm i babel-plugin-component
// babel.config.js
module.exports = {
  presets: [
    '@vue/app'
  ],
  plugins: [
    [
      "component",
      {
        "libraryName": "@tencent/zui-pure",        "libDir": "dist",
        "styleLibrary": {          "base": false,          "name": "css"        }
      }
    ]
  ]
}

(2) 在main.js按需加载组件

// src/main.js
import Vue from 'vue'
import App from './App.vue'

// import ZUI from '@tencent/zui-pure'
// Vue.use(ZUI)
import { Demo } from '@tencent/zui-pure'
Vue.use(Demo);

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

测试成功


接入Vuese自动生成文档

1.按照Vuese

tnpm i vuese --save-d

2.在根目录下新增配置文件 .vueserc

{
  "include": [
    "./components/**/*.vue"
  ],
  "title": "zui-doc",
  "genType": "docute",
  "outDir": "./docs"
}

include:指定构建目录。

genType: 指定生成的文档类型,docute 会把vue文件构建出的所有markdown,整合为一个单页应用。

outDir:指定文档输出目录,这里指定为./docs,是为了配和在master分支接入OA Pages。

3.在package.json新增脚本,并启动。

// package.json{
  "name": "zui",
   ...
  "scripts": {
    ...
    "build_doc": "npx vuese gen && npx vuese serve --open"
  },
  ...
}

vuese gen:构建文档。

vuese serve --open:启动文档服务器,打开浏览器查看生成的文档。

npm run build_doc


注:由于demo组件结构过于简单,在生成时被vuese忽略了,于是新增了props。

还有一个问题,发现首页报404:


解决:在docs根目录下添加readme.md


到此,整个vue自定义组件库架子已搭建完毕~

完整的组件库还应该包含单元测试和类型定义,这里就不再赘述了,可以直接参考demo代码。

附录

注:vuese定制的文档不是很方便写演示demo,目前项目改用vuepress。