Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

定制你的专属Vue组件库 #145

Open
yanyue404 opened this issue Jun 10, 2020 · 0 comments
Open

定制你的专属Vue组件库 #145

yanyue404 opened this issue Jun 10, 2020 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Jun 10, 2020

原文链接: 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。

@yanyue404 yanyue404 changed the title 初始化前端 utils 工具库 定制你的专属Vue组件库 Mar 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant