首页
技术分享
实用工具 发布文章 新浪微博 Github

在我们的 vue 项目中(特别是后台系统),总会出现一些需要多业务线共同开发同一个项目的场景,如果各业务团队向框架中提供一些私有的展示组件,但是这些组件并不能和框架一起打包,因为框架不能因为某个私有模块的频繁变更而重复构建发布。在这种场景下我们需要一个加载远程异步代码的组件来完成将这些组件加载到框架中。

vue-cli 作为 Vue 官方推荐的项目构建脚手架,它提供了开发过程中常用的,热重载,构建,调试,单元测试,代码检测等功能。我们本次的异步远端组件将基于 vue-cli 开发。

需求分析

  1. 如何加载远端的代码?
  2. 如何注册加载后的代码到框架中。
  3. 父组件如何和远端引入的组件通信。
  4. 远端代码如何复用框架中已引入的库。
  5. 避免因远端代码被类似 v-for 多次调用导致的不必要请求。

加载远端代码

远端代码应该存储在一个可访问的 URL 上,这样我们通过 Axios 类似的 HTTP client 请求这个链接拿到源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Axios from 'axios';

export default {
name: 'SyncComponent',
props: {
// 父组件提供请求地址
url: {
type: String,
default: ''
}
},
data() {
return {
resData: ''
};
},
async mounted() {
if (!this.url) return;
const res = await Axios.get(this.url); // 我们在组件挂载完成时,请求远端代码并存储结果。
this.resData = res.data;
}
};

以上是基础代码 为了方便 一下例子中 我将省略重复的代码部分。

注册代码到框架中

这部分有些繁琐,涉及到多个问题:

  1. 浏览器并不支持 .vue 模板 或 ES.next 语法,模块需要编译后才可以使用。

处理这部分比较简单,我们自己定义一个webpack配置文件来打包这些模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 在 build 目录下新建 webpack.sync-components.prod.conf.js 文件

const webpack = require('webpack');
const path = require('path');
const utils = require('./utils');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

function resolve(dir) {
return path.join(__dirname, '..', dir)
}

module.exports = {
// 此处引入要打包的组件
entry: {
componentA: resolve('/src/views/component-a.vue')
},
// 输出到静态目录下
output: {
path: resolve('/static/'),
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: false, // ****** vue-loader v13 更新 默认值为 true v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
loaders: utils.cssLoaders({
sourceMap: true,
extract: false // css 不做提取
}),
transformToRequire: {
video: 'src',
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
// 压缩JS
new webpack.optimize.UglifyJsPlugin({
compress: false,
sourceMap: true
}),
// 压缩CSS 注意不做提取
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
})
]
};

至此我们的模块已经被编译成框架可以识别的文件。

  1. 如何将字符串转换成js对象。

new Function

1
2
3
4
5
6
7

async mounted() {
if (!this.url) return;
const res = await Axios.get(this.url);
let Fn = Function;
this.mode = new Fn(`return ${res.data}`)();
}
  1. 转换后的js对象并不能被vue识别。

有两种可能会导致这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue-loader v13 esModule 更新 默认值为 true, v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: false
... 以下省略千军万码
}
}

// UglifyJs 需要取消变量名替换配置,此配置并不会极大影响压缩率
new webpack.optimize.UglifyJsPlugin({
compress: false,
sourceMap: true
})

至此 远程组件就被引入到框架中了。

父组件如何和远端引入的组件通信

这里有一个问题,从 view组件远程异步加载组件 再到 实际业务组件 通信一共三层,中间层 远程异步组件 作为公共组件不可被修改,需要 view组件 直接向 实际业务组件 通信。vuex 和 eventBus 方案都过于繁琐,这里我们采用 $attrs 和 $listeners(vue v2.4+), 来实现 “fallthrough”(vue组件跨层级通信)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 修改 sync-component.vue 组件
// 新增 v-bind="$attrs" v-on="$listeners"
<component
:is="mode"
v-bind="$attrs"
v-on="$listeners">
</component>

// inheritAttrs: true
export default {
name: 'SyncComponent',
props: {
// 父组件提供请求地址
url: {
type: String,
default: ''
}
},
inheritAttrs: true
... 以下省略千军万码
}

远端代码如何复用框架中已引入的库

我们不希望看到远端组件和框架中存在较大库或插件的重复的引入,这部分内容尚处在实践阶段,主要思路是把公共库挂载到Vue原型链上实现组件公共复用 Vue.prototype.$xxx

1
2
3
4
// 全局添加 axios 对象
import axios from 'axios';

Vue.prototype.$http = axios;

引入的远程组件可以访问到框架中的公共包了,这时候还需要配置 webpack 使远程组件打包时不要包含公共包的代码。

1
2
3
4
5
6
// webpack.sync-components.prod.conf.js 添加
externals: {
vue: 'vue',
'element-ui': 'element-ui',
axios: 'axios'
}

避免因远端代码被类似 v-for 多次调用导致的不必要请求。

这部分我们直接用一个全局变量做字典,存储 以 请求地址:数据 为子项的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async mounted() {
if (!this.url) return;
// Cache 缓存 根据 url 参数
if (!window.SyncComponentCache) {
window.SyncComponentCache = {};
}
let res;
if (!window.SyncComponentCache[this.url]) {
window.SyncComponentCache[this.url] = Axios.get(this.url);
res = await window.SyncComponentCache[this.url];
} else {
res = await window.SyncComponentCache[this.url];
}
let Fn = Function;
this.mode = new Fn(`return ${res.data}`)();
console.log(this.mode);
}

至此,异步远程组件就可以加载并和框架进行通信了。

本文中的源码请访问 github 获取,组件已经发布到 NPM 上,可以直接安装。

拓展阅读:
如何在 npm 上发布你的 vue 组件