远程组件实践
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
前言
从服务端远程下载一个 JS 文件并注册成组件。
一、什么是远程组件
这里是指在生产环境中,从服务端远程下载一个 JS 文件并注册成组件,使其在生产环境中能够使用。
二、背景
1. 项目背景
我们的项目是个低代码平台,它内置了一些常用组件,可供用户使用。但内置组件不能够完全满足用户的需求,我们希望能够提供一个入口,用户自己上传自定义组件。这样可以极大的增加项目的可拓展性。
低代码平台
需求流程
这也是远程组件的一个典型场景。
2. 技术背景
项目使用的技术栈为 vue2。我们限定自定义组件开发的技术栈也是 vue2。
三、技术实现
1. 流程步骤
几个关键步骤
用户按照 UMD 模块规范开发组件
注册组件
获取到组件模块
渲染组件
响应用户的操作
2. 什么是 UMD 模块规范呢?
所谓 UMD (Universal Module Definition),就是一种 javascript 通用模块定义规范,让你的模块能在 javascript 所有运行环境中发挥作用。
简言之就是能兼容主流 javascript 模块的规范,如 CommonJS, AMD, CMD 等。
下面是规范的代码,以及对应的说明:
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// 这是 commonjs 模块规范,nodejs 环境
var depModule = require('./umd-module-depended')
module.exports = factory(depModule);
} else if (typeof define === 'function' && define.amd) {
// 这是 AMD 模块规范,如 require.js
define(['depModule'], factory)
} else if (typeof define === 'function' && define.cmd) {
// 这是 CMD 模块规范,如sea.js
define(function(require, exports, module) {
var depModule = require('depModule')
module.exports = factory(depModule)
})
} else {
// 没有模块环境,直接挂载在全局对象上
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
// depModule 是依赖模块
return {
name: '我自己是一个umd模块'
}
}))
如果在 html 中直接使用 script 标签引用 umd 格式的 js 文件。就会走到第四个条件分支,即 直接挂载在全局对象上 。这个全局对象指的就是 window。
在我们的项目中,是直接使用 script 标签引用的。但没有走第四个条件分支。之后会说明原因及做法。
3. 如何打包 UMD 规范的组件文件
以 Vue CLI 为例。当运行 vue-cli-service build
时,可以通过 --target
选项指定构建目标为 库 。
上图中 库 的名字为 myLib 。[entry] 为需要构建的入口文件。构建一个库会输出一些文件,需要我们关注的是下面两个:
dist/myLib.umd.js
:一个直接给浏览器或 AMD loader 使用的 UMD 包dist/myLib.umd.min.js
:压缩后的 UMD 构建版本
可见,使用 Vue CLI 打包 UMD 规范文件是十分方便的。
在我们的项目中,打包命令为:
vue-cli-service build --target lib --name Demo ./index.js
我们的 库 名是 Demo 。
./index.js
文件的内容为:
import Demo from './packages/demo/index.vue'
export default {
version: '1.0.0',
Demo
}
./packages/demo/index.vue
文件的内容为:
<template>
<div :style="`width: ${config.width}px;`"></div>
</template>
<script>
export default {
name: "demo",
title: "demo演示组件",
props: {
config: {
type: Object,
}
},
watch: {
config: {
handler: function (_, newConfig) {
this.width = newConfig.width
},
deep: true
}
},
mounted() {
this.width = this.config.width
},
getDefaultConfig() {
return {
defaultProperties: [
{
title: "边长",
name: "width",
type: "SingleInput",
value: 200
}
]
}
}
}
</script>
4. 组件的注册
当使用 Vue CLI 的库模式打包时,我们暴露出的 Demo 是个 *.vue
文件。这里是注册的关键。Vue CLI 将会把这个组件自动包裹并注册为 Web Components 组件,无需在 main.js 里自行注册。需要注意的是,这个包依赖了在页面上全局可用的 Vue。
在 Vue CLI 的库模式中,Vue 是外置的。这意味着包中不会有 Vue。输出代码里会使用一个全局的 Vue 对象。主项目无论使用什么输出格式,都需要将自己系统内的 Vue 对象暴露到 window 上。
所以,在项目中我们需要将 Vue 暴露到 window 上。需要在 main.js 文件添加代码:
window.Vue = Vue
当这个脚本被引入网页时,你的组件就可以以普通 DOM 元素的方式被使用了。
<script src="demo.umd.js"></script>
<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<demo></demo>
5. 获取组件模块
那么,想要使用自定义组件的话,必须要知道 Vue CLI 打包后自动注册的标签名。
事实上,标签名就是 ./packages/demo/index.vue
文件的 name 值,即 demo 。
在 3. 如何打包 UMD 规范的组件文件 中,我们的打包入口是 ./index.js 。它暴露出了 './packages/demo/index.vue'
。那么拿到 ./index.js
就拿到了标签名。
当你使用一个 .js 文件作为入口时,它可能会包含具名导出,所以库会暴露为一个模块。也就是说你的库必须在 UMD 构建中通过
window.yourLib.default
访问。
也就是我们可以通过 window.Demo.default
拿到 ./index.js
。
又有问题了,这里我们又必须知道它的库名 Demo 才行。
怎么办呢?
我们回到上文中的 UMD 模块规范的代码观察。commonjs 模块规范使用了 module.exports
,它是可以将模块直接暴露出来的,而不是挂载在 window 上。
那我们就模拟下 node 环境,这样不需要知道 库 名,就能拿到模块。
// 模拟 node 环境
window.module = {}
window.exports = {}
// 模拟 node 环境获取模块
const module = window.module.exports
这样就不必像官网那样具名访问模块了。
// 官网获取挂载的模块
const module = window.Demo.default
6. 渲染组件
拿到了组件模块,下一步就是将它渲染出来。项目里我们使用 动态组件 + 异步组件 + 渲染函数 的组合来完成。下面分别回顾一下这几个知识点,然后将他们相结合。
6.1 动态组件
主要用于将已知的组件进行切换。
不适用未知的组件。典型场景是在不同组件之间进行动态切换,比如在一个多标签的界面里:
<template>
<component v-bind:is="currentTabComponent"></component>
</template>
<script>
import Home from '../components/Home'
import Posts from '../components/Posts'
import Archive from '../components/Archive'
export default {
components: {
Home,
Posts,
Archive
},
data () {
return {
currentTabComponent
}
}
}
</script>
6.2 异步组件
vue2 官网是这样描述的:
Vue 2.3.0+ 新增如下书写方式:
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
我们在项目中使用的是新增的这种方式。
6.3 动态组件 + 异步组件
下面是项目中将两种组件结合的代码:
<template>
<component v-bind:is="componentFile" :model="model"></component>
</template>
<script>
export default defineComponent({
name: 'AsyncComponent',
props: {
model: {
type: Object,
default: () => {}
}
},
setup() {
const AsyncComponent = () => ({
component: import('./anonymous.vue'),
delay: 200,
timeout: 3000
})
return {
componentFile: AsyncComponent,
}
},
})
</script>
model 是 umd 方式获取到的组件模块,里面包括:组件的标签、组件的可配置数据等。
componentFile 是需要异步加载的组件。
6.4 渲染函数
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
项目中的 anonymous.vue
文件就非使用 渲染函数 不可。毕竟,我们都不知道标签的名字是什么。
它的代码如下:
export default {
name: 'Anonymous',
props: {
model: {
type: Object,
default: () => {}
}
},
render(h) {
const tagName = this.model.tagName
const param = {
"props": {
config: this.model.config
}
}
return h(tagName, param, [])
}
}
以上就是渲染远程组件的具体步骤。下面简单梳理一下远程组件的数据是如何响应的。
7. 远程组件数据的响应
想要数据获得响应,需要给组件开发者和接入者约定好规范。
7.1 组件开发者规范
在用 Vue CLI 打包的入口文件 ./index.js
中暴露的 Demo (./packages/demo/index.vue
)组件中,我们将组件需要响应的属性以及默认值以 getDefaultConfig()
的形式导出。
getDefaultConfig() {
return {
defaultProperties: [
{
title: "边长",
name: "width",
type: "SingleInput",
value: 200
}
]
}
}
同时,我们还需要监听这个传进来的属性值,以便在图表上做出相应的变化。
props: {
config: {
type: Object,
}
}
watch: {
config: {
handler: function (_, newConfig) {
this.width = newConfig.width
},
deep: true
}
}
7.2 组件接入者规范
在 5. 获取组件模块 的时候,我们可以通过 module.getDefaultConfig()
获取到需要响应的属性以及默认值,通过 name 获取到标签名。
在 6.4 渲染函数 步骤中,将属性的默认值、标签名、props (也就是 config) 传给渲染函数。就可以完成数据的响应 了。
render(h) {
// 这里获得了标签名
const tagName = this.model.tagName
const param = {
// 这里传入属性
"props": {
config: this.model.config
}
}
return h(tagName, param, [])
}
四、待改进的地方
js、css 不隔离,没有沙箱能力
限定技术栈(项目限定 vue2 ), 对开发者不友好
如果组件标签相同,会被覆盖
五、对未来优化方向的调研
方案一:微前端
微前端借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用。
主流框架有:single-spa 和 qiankun
主要应用场景
1、跨技术栈重构项目时。
2、跨团队或跨部门协作开发项目时。
微前端拆分的颗粒度为应用。
【第2695期】基于微前端qiankun的多页签缓存方案实践
结合项目的场景,这个方案不是很吻合。既不能很好的解决问题,也没有发挥微前端的真正能力。
方案二:微组件
这里我们把一些基于 Web Components 的轻量级的微前端框架,称为微组件。
框架有:micro-app、magic-microservices 等。
这种解决方案更适合当前的场景。它可以解决 js、css 不隔离的问题,并且不再限定组件开发者的技术栈。
对于组件数据的响应与通讯,则需要进一步的调研和实践。
这里有一个微组件实践可供参考。
方案三:引入 IDE
以上两个方案可以解决前两个问题,但如果要解决三个问题的话就需要引入 IDE 。
这个可能是终极方案,可以极大优化开发者体验,对应的成本也是最高的。这里就不做赘述了。
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波