blog
blog copied to clipboard
定制你的专属Vue组件库
原文链接: 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 代码。
附录
- 文章 demo 地址:github.com/Best921/zui…
- npm 包地址:www.npmjs.com/package/zui…
- Vuese 使用指南:vuese.org/
注:vuese 定制的文档不是很方便写演示 demo,目前项目改用 vuepress。