从零开始搭建pc端Vue组件库lin-view-ui

前言

虽然目前有很多vue组件库,比如Element-UI,iview等等。每个组件库都有各自的特点,但是每个公司需要的业务组件是不尽相同的,没有哪一个组件库能够非常完美的符合各种需求。比如我现在使用的是Element-UI,我需要使用到Select组件,但是现在有个需求需要进行分页加载,这个时候Element-UI的Select组件就不能适应这种业务场景了。所以我就想着开发一套属于自己的组件库,把平时常用的一些组件封装起来,集成到自己的组件库中,同时也能学会造轮子,提高自己的技术能力。
这里记录一下我从零开始搭建起来的组件库的过程。目前已经有了四十多个组件,后面会一直更新维护的。
github地址:github
演示地址:演示

环境准备

新建项目

首先使用vue-cli3新建一个项目,在新建项目的时候把babeljest这些该选上的都选上。

改造目录结构

项目新建完成之后,我们需要考虑的是改造项目的目录结构。我们需要一个目录存放组件,一个目录存放文档,一个目录进行开发测试。所以我们要对vue-cli3脚手架生成项目结构改造成如下:

  • build --打包组件以及文档的配置
  • docs --存放文档
  • examples --进行开发测试
  • packages --存放组件的
  • src --存放公共资源的
  • tests --单元测试用例

修改package.json

我们需要在package.json添加一些字段

  • description 项目描述
  • main 通过npm安装然后引用的文件入口
  • module ES Module 文件入口,rollup 打包需要的入口文件
  • unpkg npm 上所有的文件都开启 cdn 服务地址
  • license 开源协议
  • keywords 关键字,在npm上搜索包的时候会用上
  • homepage 项目的主页
  • repository 仓库地址
  • files 发布npm包的时候需要进行上传的文件
  • peerDependencies 使用这个包的时候需要预安装依赖
    其次就是dependenciesdevDependencies的使用方式。当你在项目中使用npm进行开发,并且安装了这个依赖库。当你运行
npm install

这个命令的时候,依赖包中dependencies这个字段的依赖包进被安装下来,devDependencies这个字段中的依赖包不会被安装下来。所依这2个字段需要慎重使用不能把所有的依赖包都安装进dependencies这个字段中,否则用户在使用的时候会造成不必要下载和浪费时间。按照我们开发组件库的流程来说,一般如果有多个组件使用到同一个依赖包,比如lodash,这个就需要安装到dependencies中。我们在实现按需加载的时候,把这个依赖包给排除掉,等用户使用的时候在进行打包加载。

{
  "name": "lin-view-ui",
  "version": "1.0.26",
  "description": "vue components library",
  "author": "c10342",
  "private": false,
  "main": "lib/index.js",
  "module": "lib/index.js",
  "unpkg": "lib/index.js",
  "license": "MIT",
  "keywords": [
    "vue",
    "UI",
    "Component"
  ],
  "homepage": "https://github.com/c10342/lin-view-ui",
  "repository": {
    "type": "git",
    "url": "https://github.com/c10342/lin-view-ui"
  },
  "files": [
    "lib",
    "packages",
    "utils",
    "src"
  ],
  "dependencies": {
    "async-validator": "^3.4.0",
    "deepmerge": "^4.2.2",
    "flv.js": "^1.5.0",
    "hls.js": "^0.14.12",
    "lodash": "^4.17.20",
    "resize-observer-polyfill": "^1.5.1"
  },
  "peerDependencies": {
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@babel/runtime-corejs3": "^7.11.2",
    ...
  }
}

添加npm script脚本

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "rimraf lib && npm run build:index && npm run build:components && npm run build:assets",
    "build:assets": "parallel-webpack --config ./build/webpack.assets.js",
    "build:index": "cross-env NODE_ENV=index vue-cli-service build --no-clean",
    "build:components": "cross-env NODE_ENV=components vue-cli-service build --no-clean  && node ./build/write.js",
    "build:docs": "cross-env NODE_ENV=docs vue-cli-service build",
    "test:unit": "vue-cli-service test:unit --watch",
    "test": "vue-cli-service test:unit",
    "upload:docs": "npm run build:docs && node ./build/qiniu.js",
    "prepublishOnly": "npm run build && npm run upload:docs",
    "pub": "node ./build/publish.js"
  }

这里可以看见含有build命令的script脚本都添加了 --no-clean 这个参数,目的就是为了让vue-cli3脚手架在打包的时候不删除原有的目录。这里是因为组件,公共资源这些都是打包到同一个目录中,后面打包的会删除目录在进行打包。同时这里借助了rimraf这个包在打包前进行统一的删除。
由于vue-cli3项目的配置文件只能写在vue.config.js中,所以需要借助cross-env这个包来添加不同的环境变量,在vue.config.js配置文件中读取这个环境变量,然后引用不同的配置来打包组件,文档,公共资源。vue.config.js文件如下:

const devConfig = require("./build/webpack.dev");
const docsConfig = require("./build/webpack.docs");
const indexConfig = require("./build/webpack.index");
const componentsConfig = require("./build/webpack.components");
const env = process.env.NODE_ENV;

let config = devConfig;

if (env === "development") {
  config = devConfig;
} else if (env === "docs") {
  config = docsConfig;
} else if (env === "index") {
  config = indexConfig;
} else if (env === "components") {
  config = componentsConfig;
}

module.exports = config;

通用配置

通用配置主要需要考虑一下四点:
1、添加编译
把packages、examples、docs等目录添加进编译,因为vue-cli3中src外的文件默认是不被webpack处理的。

  chainWebpack: (config) => {
	config.module
    .rule("js")
    .include.add(utils.resolve("packages"))
    .end()
    .include.add(utils.resolve("examples"))
    .end()
    .include.add(utils.resolve("docs"))
    .end()
    .include.add(utils.resolve("src"))
    .end()
    .use("babel")
    .loader("babel-loader")
    .tap((options) => {
      // 修改它的选项...
      return options;
    });
  },

2、删除无关配置

  • 因为我们打包出来的每一个组件都是一个独立的包,所以我们在打包组件的时候并不希望抽离每个组件的公共js,所以需要删除splitChunks这个配置。
  • 删除copy,组件库是不需要复制public文件到打包目录中。
  • 因为只是打包组件,不生成html页面,所以需要删除html。
  • 删除preload以及prefetch,因为删除了html插件,所以这两个也没用。
  • 删除hmr,删除hot-module-reload。
  • 删除自动加上的入口:app
  chainWebpack: (config) => {
	  config.optimization.delete("splitChunks");
	  config.plugins.delete("copy");
	  config.plugins.delete("preload");
	  config.plugins.delete("prefetch");
	  config.plugins.delete("html");
	  config.plugins.delete("hmr");
	  config.entryPoints.delete("app");
  },

3、配置路径别名
配置路径别名是为了方面后面公共资源的引入和按需加载的实现。这里需要做一个约定就是:凡是需要引用到组件或者公共资源的文件,都需要使用路径别名进行引用,否则在后面的按需加载实现会带来一定的麻烦。

configureWebpack:{
  resolve: {
    alias: {
      examples: utils.resolve("examples"),
      packages: utils.resolve("packages"),
      "lin-view-ui": utils.resolve("src/index.js"),
      src: utils.resolve("src"),
    },
  }
}

打包全量包配置

这里说的全量包指的就是组件库中的index.js和style.css。其中index.js包含了所有的组件,公共资源文件。style.css包含了所有组件样式以及字体图标。
这里有四点需要注意:
1、打包的时候需要排除Vue这个库,因为我们的组件库是基于Vue的。当你在使用这个组件库的时候,那么说明你这是一个Vue项目,并且已经有了Vue这个库。所以为了减少打包的体积,我们不需要把Vue这库打包进去。

configureWebpack:{
    externals: {
      vue: {
        root: "Vue",
        commonjs: "vue",
        commonjs2: "vue",
        amd: "vue",
      },
    },
}

2、打包出来的文件模块化规范。首先你要考虑使用的对象。如果你的目标用户只是单纯的面向使用webpack构建项目的用户,那么你可以使用commonjs这个模块化规范。如果你还要考虑其他的使用用户,比如通过script脚本引用。那么你可能需要考虑umd这个模块化规范。因为umd这么模块化规范可以兼容很多模块化规范,比如cmd、amd。

configureWebpack:{
	output: {
      filename: "index.js",
      libraryTarget: "umd",
      libraryExport: "default",
      library: "LinViewUi",
    },
}

3、打包的时候字体图标需要输出到assets/fonts目录下。

  chainWebpack: (config) => {
    config.module
      .rule("fonts")
      .use("url-loader")
      .tap((option) => {
        option.fallback.options.name = "assets/fonts/[name].[hash:8].[ext]";
        return option;
      });
  },

4、打包js和css的时候不需要生成SourceMap文件

{
	productionSourceMap: false,
	css: {
	    sourceMap: false,
	    extract: {
	      filename: "style.css",
	    },
	  },
}

单独打包每个组件,实现按需加载

1、babel-plugin-import
按需加载借助的是babel-plugin-import这个插件。当我们引用如下组件的时候:

import {Button} from 'lin-view-ui'

babel-plugin-import会帮我们转化成如下的引入方式:

import Button from 'lin-view-ui/lib/Button/index'
import 'lin-view-ui/lib/Button/style'

所以我们在打包单个组件的时候需要生成如下目录结构

  • Button
    • index.js
    • style.css
    • style.js
      非常庆幸的是我们可以借助webpack把js和css打包到指定的目录中去
{
  configureWebpack: {
    entry: getComponentEntries("packages"),
    output: {
      filename: "[name]/index.js",
      libraryTarget: "umd",
      libraryExport: "default",
      library: "[name]",
    },
  },
  css: {
    sourceMap: false,
    extract: {
      filename: "[name]/style.css",
    },
  },
}

2、排除公共资源文件和依赖包
我们的公共资源文件都是放在src目录下的,所以公共资源我们只需要定位到src目录下即可。依赖包我们可以通过读取packages.json中的dependencies字段来获取。其中公共资源需要按照约定使用配置的路径别名进行引用,否则很难匹配到你是用了那些公共资源文件。

const packageJson = require("../package.json");
const dependencies = {};
for (const key in packageJson.dependencies) {
  dependencies[key] = {
    root: key,
    commonjs: key,
    commonjs2: key,
    amd: key,
  };
}
const utilsList = fs.readdirSync(utils.resolve("src/utils"));
const mixinsList = fs.readdirSync(utils.resolve("src/mixins"));
const jsList = fs.readdirSync(utils.resolve("src/js"));
const getExternalsList = () => {
  const externals = {
    vue: {
      root: "Vue",
      commonjs: "vue",
      commonjs2: "vue",
      amd: "vue",
    },
    ...dependencies,
    "src/locale/index.js": "lin-view-ui/lib/assets/locale/index.js",
    "src/locale/lang/zh-CN.js": "lin-view-ui/lib/assets/locale/lang/zh-CN.js",
    "src/locale/lang/en-US.js": "lin-view-ui/lib/assets/locale/lang/en-US.js",
    "src/locale/format.js": "lin-view-ui/lib/assets/locale/format.js",
    "src/fonts/iconfont.css": "lin-view-ui/src/fonts/iconfont.css",
    "flv.js/dist/flv.js": "flv.js/dist/flv.js",
  };

  utilsList.forEach(function(file) {
    file = path.basename(file);
    externals[`src/utils/${file}`] = `lin-view-ui/lib/assets/utils/${file}`;
  });
  mixinsList.forEach(function(file) {
    file = path.basename(file);
    externals[`src/mixins/${file}`] = `lin-view-ui/lib/assets/mixins/${file}`;
  });
  jsList.forEach(function(file) {
    file = path.basename(file);
    externals[`src/js/${file}`] = `lin-view-ui/lib/assets/js/${file}`;
  });
  return externals;
};
module.exports = {
  configureWebpack: {
    externals: getExternalsList(),
  },
};

按需加载的实现主要就是两点。一是需要排除公共资源文件和依赖包,否则会增大打包的体积;二是打包出来的目录结构需要符合babel-plugin-import插件的要求,即使你是使用babel-plugin-component插件也是一样的道理

打包公共资源

其实打包公共资源跟打包组件是一样的,只需要获取每个资源文件的入口就可以了,详细配置可以查看打包组件的配置,这里不做讲述了。

演示文档搭建

组件库的演示文档使用的是Markdown。要使用Markdown主要需要考虑的是代码的显示和组件实例的显示。我这里采用的是代码显示跟组件实例显示分离的方式。也就是同一份代码需要写两遍,组件实例代码写一次,组件代码显示写一次。当然大家也可以参考Element-ui的方式,组件代码显示和组件实例不分离。
要使用Markdown,那就需要借助markdown-it-container这个插件来处理Markdown。这里需要做一个约定,就是通过以下
:::demo
```html
```
:::
代码块包含的内容就是需要显示的代码。
同时在全局注册一个名为demo-block的组件,把需要显示的代码插入进该组件的指定位置,通过该组件是用来控制代码的显示和隐藏。

  chainWebpack: (config) => {
  config.module
    .rule("md")
    .test(/\.md/)
    .use("vue-loader")
    .loader("vue-loader")
    .end()
    .use("vue-markdown-loader")
    .loader("vue-markdown-loader/lib/markdown-compiler")
    .options({
      raw: true,
      preprocess: (MarkdownIt, source) => {
        MarkdownIt.renderer.rules.table_open = function() {
          return '<table class="table">';
        };
        MarkdownIt.renderer.rules.fence = utils.wrapCustomClass(
          MarkdownIt.renderer.rules.fence
        );
        // ```code`` 给这种样式加个class code_inline
        const code_inline = MarkdownIt.renderer.rules.code_inline;
        MarkdownIt.renderer.rules.code_inline = function(...args) {
          args[0][args[1]].attrJoin("class", "code_inline");
          return code_inline(...args);
        };
        return source;
      },
      use: [
        [
          MarkdownItContainer,
          "demo",
          {
            validate: (params) => params.trim().match(/^demo\s*(.*)$/),
            render: function(tokens, idx) {
              var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);

              if (tokens[idx].nesting === 1) {
                var desc = tokens[idx + 2].content;
                const html = utils.convertHtml(
                  utils.striptags(tokens[idx + 1].content, "script")
                );
                // 移除描述,防止被添加到代码块
                tokens[idx + 2].children = [];

                return `<demo-block>
                              <div slot="desc">${html}</div>
                              <div slot="highlight">`;
              }
              return "</div></demo-block>\n";
            },
          },
        ],
      ],
    });
  },

到此,Markdown的处理已经完成了。这里需要注意一点就是需要配置文档的入口,因为vue-cli3的默认入口文件时src/main.js,但是已经被我们删除了。

  pages: {
    docs: {
      // page 的入口
      entry: "docs/main.js",
      // 模板来源
      template: "public/index.html",
      // 在 dist/index.html 的输出
      filename: "index.html",
      // 当使用 title 选项时,
      // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: "组件文档",
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk。
      chunks: ["chunk-vendors", "chunk-common", "docs"],
    },
  },

关于文档的部署,我推荐大家使用七牛云,免费的不用钱,又有免费的cdn加速。

index.js文件编写

index.js文件是一个入口文件,所有组件都是通过这个文件引入,然后导出。这里我们使用webpack提供的require.context函数统一引入各个组件,这样就不用了每次新增组件就引入一次组件

const testComps = require.context(
  "../packages",
  true,
  /^\.(\/\w+)\/index\.js$/
);
const reg = /^\.\/(\w+)\/index\.js$/;
const componentObjs = {};
testComps.keys().forEach((key) => {
  const componentEntity = testComps(key).default;
  const result = reg.exec(key)[1];
  componentObjs[result] = componentEntity;
});
const install = (Vue, opts = {}) => {
  Object.keys(componentObjs).forEach((key) => {
    Vue.use(componentObjs[key]);
  });
};
// 判断是否是直接引入文件,如果是,就不用调用 Vue.use()
if (typeof window !== "undefined" && window.Vue) {
  install(window.Vue);
}
export default {
  install,
  ...componentObjs,
};

组件目录结构

这里以Button为例子

  • Button
    • src
      • button.js 引入vue文件和样式文件
      • button.vue
      • style.scss
    • index.js Button组件入口,提供一个install函数

总结

组件库的搭建可以去参考element-ui等著名的开源组件库,如果看不懂也可以去参考别人造轮子做的一些组件库,一般都是参考其他的组件库,然后简化了其中的流程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值