Skip to content
On this page

全局组件设计


全局组件设计

要实现全局组件的能力,那么最大的一个难题就是需要将组件和模板解耦,组件将不再依赖于具体的模板而可以应用于任何模板。这句话听起来有点绕口,也可以简单的设想一下:开发模板的同学和开发全局组件的同学不是同一人,二者之间没有任何交集,模板可以让用户自制组件,从而达到定制渲染某个区域的目的。

要实现这样的能力,我们接下来会基于 Vue 模板详细介绍如何实现运行时组件。(React 类似)

本地构建与运行时编译

我们在使用 vue 项目的时候,基本上都是通过 vue cli 一把梭子干到底,然后以 vue 单文件的方式编码,webpack 编译打包的形式发布。这种方式就是本地构建。本地构建由于我们常常采用 .vue 单文件的方式编写。所以我们需要一个 vue-loader 来对单文件组件进行编译的动作,比如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug

但是 vue 还有另外一种使用方式,就是 cdn 的模式,其实官网一开始的 Demo 就是使用这种方式:

html
<html lang="en">
<head>
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
  <div id="app">
  	{{message}}
  </div>
  <script type="text/javascript">
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    })
  </script>
</body>
</html>

相比本地构建编译,由于在线运行,我们是无法对项目进行相关的 编译时优化 。比如 Vue 项目我们可以做以下几点编译时优化策略:

  • 提升静态树
  • 跳过静态绑定
  • Skipping Children Array Normalization

更多编译时优化可以参考 尤大 在`2017 年的分享 前端工程中的编译时优化

vue 运行时编译的好处也是有的,就是非常灵活,随写随用。比较符合 vue 渐进式的思想。而我们全局组件则是需要解耦模板,所以我们可以采用二者结合的方式,在本地构建,在模板页面运行。

更多关于 vue 的运行时编译和本地编译时的知识可以参考这里:术语

构建目标

有了上面的一些知识,我们知道 Vue 组件可以独立于编译器单独运行,而 vue cli 官网上也是提到了可以针对单独组件做构建目标的区分:

构建一个库会输出:

  • dist/myLib.common.js:一个给打包器用的 CommonJS 包 (不幸的是,webpack 目前还并没有支持 ES modules 输出格式的包)
  • dist/myLib.umd.js:一个直接给浏览器或 AMD loader 使用的 UMD 包
  • dist/myLib.umd.min.js:压缩后的 UMD 构建版本
  • dist/myLib.css:提取出来的 CSS 文件 (可以通过在 vue.config.js 中设置 css: { extract: false } 强制内联)

结合上文一个动态运行 Vue 的环境,一个可以将组件转化成库的 vue cli 所以我们可以得出结论:我们可以将组件打包好的库代码动态注入到页面,再交给页面运行时渲染。

撸代码

第一步,我们需要再利用 vue-cli 来生成一个组件仓库,比如我们现在需要一个 banner 组件,我们需要调整 vue cli 生成的代码库,让其支持组件的开发方式:

1. 创建项目

在指定目录中使用命令创建一个默认的项目,或者根据自己需要自己选择。

shell
vue create coco-global-banner

2. 调整目录

我们需要一个目录存放组件,一个目录存放示例,按照以下方式对目录进行改造。

bash
.
...
|-- examples      // 原 src 目录,改成 examples 用作示例展示
|-- packages      // 新增 packages 用于编写存放组件
...
. 

我们通过上一步的目录改造后,会遇到两个问题。

  1. src 目录更名为 examples,导致项目无法运行
  2. 新增 packages 目录,该目录未加入 webpack 编译

所以我们还需要对 vue.config.js 进行调整:

javascript
const path = require('path');

module.exports = {
  // 修改 src 为 examples
  pages: {
    index: {
      entry: 'examples/main.js',
      template: 'public/index.html',
      filename: 'index.html'
    }
  },
  // 扩展 webpack 配置,使 packages 加入编译
  chainWebpack: config => {
    config.module
      .rule('eslint')
      .exclude.add(path.resolve('dist'))
      .end()
      .exclude.add(path.resolve('examples/docs'))
      .end();
    config.module
      .rule('js')
      .include
      .add('/packages')
      .end()
      .use('babel')
      .loader('babel-loader')
      .tap(options => {
        // 修改它的选项...
        return options
      })
  }
}

3. 编写 banner 组件

我们需要再 packages 下新建一个 coco-global-banner 目录来存放组件,coco-global-banner 目录下新建 index.js 文件来对当前组件进行 export:

javascript
import Component from './index.vue';
import config from './package.json';

// 为组件提供 install 安装方法,供按需引入
Component.install = function (Vue) {
  Vue.component(`${config.name}.${config.version}`, Component);
};

// 默认导出组件
export {
  Component,
};

这样我们就可以在模板组件中通过 components 属性对组件进行注册了。接下来开始编写组件:

html
<template>
  <a :href="obj.link">
    <img
      :src="obj.src"
      width="100%"
      alt="图片"
    />
  </a>
</template>

<script>
export default {
  name: 'banner',
  props: {
    obj: {
      type: Object,
      default: () => {}
    }
  }
}
</script>

然后按照我们之前篇幅的介绍,需要对组件的可编辑信息进行 schema 化,所以我们需要再建一个 package.json 文件来描述组件的信息和名称以及版本:

json
{
  "name": "coco-global-banner",
  "description": "banner组件",
  "version": "0.0.1",
  "snapshot": "https://cdn.img/banner.png",
  "schema": {
    "type": "object",
    "properties": {
      "src": {
        "title": "图片地址",
        "type": "string",
        "format": "image"
      },
      "link": {
        "title": "跳转链接",
        "type": "string",
        "format": "url"
      }
    },
    "required": [
      "src"
    ]
  }
}

4. 编译组件

到此为止我们一个完整的组件库已经开发完成了,接下来就是需要对组件进行编译构建的步骤,生成库代码: 以下我们在 scripts 中修改一条命令 npm run build

--target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。 --dest : 输出目录,默认 dist。这里我们改成 lib [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。

json
{
	// ...
  "lib": "vue-cli-service build --target lib --name coco-global-banner --dest dist packages/index.js"
}

到这里似乎就ok了,请注意,战斗依然没有结束,我们再思考一个问题,如果我们 banner 库不止一个组件,那应该怎么构建呢?看了一下 vue cli 的文档,并没有提供多组件构建的解法。所以笔者想了一个投机的办法,就是循环编译:

javascript
// build.js
const shell = require('shelljs');
const path = require('path');
let scripts = []

shell.ls('packages').forEach(file => {
  /**
   * 由于 vue-cli-service 在 build 过程中会先删除导出的父级目录,
   * 因此需要先执行主入口文件的打包命令
   */
  if (file === 'index.js') {
    scripts.unshift({
      type: 'index',
      script: 'vue-cli-service build --target lib --name index --dest dist packages/index.js',
    })
  } else {
    const config = require(path.resolve(`packages/${file}/package.json`));
    scripts.push({
      type: 'package',
      filename: file,
      script: `vue-cli-service build --target lib --name ${file}.${config.version} --dest dist/${file} packages/${file}/index.js`,
    })
  }
})
scripts.forEach(config => {
  shell.exec(config.script);
});

这样我们就可以通过遍历 packages 目录下所有组件,然后挨个编译,达到曲线救国的目的。

总结

本章节我们介绍了如何设计一个全局组件,但是我们目前为止也只提到了怎么写组件,并没有说清楚全局组件怎么被注册到模板中。接下来一章节我们接着聊。