本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
本文作者为 360 奇舞团前端开发工程师
一、什么是远程组件
这里是指在生产环境中,从服务端远程下载一个 JS 文件并注册成组件,使其在生产环境中能够使用。
二、背景
1. 项目背景
我们的项目是个低代码平台,它内置了一些常用组件,可供用户使用。但内置组件不能够完全满足用户的需求,我们希望能够提供一个入口,用户自己上传自定义组件。这样可以极大的增加项目的可拓展性。
低代码平台
需求流程
这也是远程组件的一个典型场景。
2. 技术背景
项目使用的技术栈为 vue2。我们限定自定义组件开发的技术栈也是 vue2。
三、技术实现
1. 流程步骤
几个关键步骤
用户按照 UMD 模块规范开发组件
注册组件
获取到组件模块
渲染组件
响应用户的操作
2. 什么是 UMD 模块规范呢?
所谓 UMD[1] (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.default6. 渲染组件
拿到了组件模块,下一步就是将它渲染出来。项目里我们使用 动态组件 + 异步组件 + 渲染函数 的组合来完成。下面分别回顾一下这几个知识点,然后将他们相结合。
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 ), 对开发者不友好
如果组件标签相同,会被覆盖
五、对未来优化方向的调研
方案一:微前端
微前端 [2] 借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用。
主流框架有:single-spa 和 qiankun
主要应用场景
1、跨技术栈重构项目时。
2、跨团队或跨部门协作开发项目时。
微前端拆分的颗粒度为应用。
结合项目的场景,这个方案不是很吻合。既不能很好的解决问题,也没有发挥微前端的真正能力。
方案二:微组件
这里我们把一些基于 Web Components 的轻量级的微前端框架,称为微组件。
框架有:micro-app、magic-microservices 等。
这种解决方案更适合当前的场景。它可以解决 js、css 不隔离的问题,并且不再限定组件开发者的技术栈。
对于组件数据的响应与通讯,则需要进一步的调研和实践。
这里有一个微组件实践 [3] 可供参考。
方案三:引入 IDE
以上两个方案可以解决前两个问题,但如果要解决三个问题的话就需要引入 IDE 。
这个可能是终极方案,可以极大优化开发者体验,对应的成本也是最高的。这里就不做赘述了。
谢谢您的阅读。
参考资料
[1]
可能是最详细的 UMD 模块入门指南: _https://juejin.cn/post/6844903927104667662_
[2]
引用自 MicroApp 对于微前端的说明: _https://zeroing.jd.com/docs.html#/_
[3]
微组件实践: _https://juejin.cn/post/7086790887111393293_
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。