vue等远程组建调研

一、什么是远程组件

这里是指在生产环境中,从服务端远程下载一个JS文件并注册成组件,使其在生产环境中能够使用。

二、背景

1. 项目背景

我们的项目是个低代码平台,它内置了一些常用组件,可供用户使用。但内置组件不能够完全满足用户的需求,我们希望能够提供一个入口,用户自己上传自定义组件。这样可以极大的增加项目的可拓展性。

111.png
222.jpg

这也是远程组件的一个典型场景。

2. 技术背景

项目使用的技术栈为 vue2。我们限定自定义组件开发的技术栈也是 vue2。

三、技术实现

1. 流程步骤
333.jpg

几个关键步骤

  • 用户按照 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 选项指定构建目标为

4444.png

上图中 的名字为 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 官网是这样描述的:

666.png

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]借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用。

777.png

主流框架有: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270