yanyue404/blog

定制你的专属Vue组件库

Opened this issue · 0 comments

原文链接: https://segmentfault.com/a/1190000038827540
鸣谢: https://github.com/Zack921

业务后续的需求会复用很多之前开发的组件,于是打算抽成组件库,提升后续开发效率。本文主要讲解如何搭建并发布基于 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 中引入组件库

```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。