Appearance
全局组件设计
全局组件设计
要实现全局组件的能力,那么最大的一个难题就是需要将组件和模板解耦,组件将不再依赖于具体的模板而可以应用于任何模板。这句话听起来有点绕口,也可以简单的设想一下:开发模板的同学和开发全局组件的同学不是同一人,二者之间没有任何交集,模板可以让用户自制组件,从而达到定制渲染某个区域的目的。
要实现这样的能力,我们接下来会基于 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 用于编写存放组件
...
.
我们通过上一步的目录改造后,会遇到两个问题。
- src 目录更名为 examples,导致项目无法运行
- 新增 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
目录下所有组件,然后挨个编译,达到曲线救国的目的。
总结
本章节我们介绍了如何设计一个全局组件,但是我们目前为止也只提到了怎么写组件,并没有说清楚全局组件怎么被注册到模板中。接下来一章节我们接着聊。