遠程組件實踐

本文作者爲 360 奇舞團前端開發工程師

一、什麼是遠程組件

這裏是指在生產環境中,從服務端遠程下載一個 JS 文件並註冊成組件,使其在生產環境中能夠使用。

二、背景

1. 項目背景

我們的項目是個低代碼平臺,它內置了一些常用組件,可供用戶使用。但內置組件不能夠完全滿足用戶的需求,我們希望能夠提供一個入口,用戶自己上傳自定義組件。這樣可以極大的增加項目的可拓展性。

低代碼平臺需求流程

這也是遠程組件的一個典型場景。

2. 技術背景

項目使用的技術棧爲 vue2。我們限定自定義組件開發的技術棧也是 vue2。

三、技術實現

1. 流程步驟

幾個關鍵步驟

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] 爲需要構建的入口文件。構建一個庫會輸出一些文件,需要我們關注的是下面兩個:

可見,使用 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>
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, [])
  }

四、待改進的地方

五、對未來優化方向的調研

方案一:微前端

微前端 [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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qTR1XcJE6QH058C-inlZFg