淺談低程式碼平臺遠端元件載入方案

語言: CN / TW / HK

前言

低程式碼開發平臺(LCDP)是無需編碼(0程式碼)或通過少量程式碼就可以快速生成應用程式的開發平臺。通過視覺化進行應用程式開發的方法,使具有不同經驗水平的開發人員可以通過圖形化的使用者介面,使用拖拽元件和模型驅動的邏輯來建立網頁和移動應用程式。這兩年越來越多的公司和開發人員開始自研低程式碼平臺來達到降本提效的目的。今天和大家分享一下低程式碼平臺開發過程中遇的一個問題和對應的解決思路。

問題

低程式碼平臺之所以不需要寫程式碼是因為平臺提供了很多可配置的元件,讓平臺的使用者可以通過配置的方式生成自己想要的產物。那麼如果想要能配置出更多的效果,就需要保證物料庫足夠豐富。

如果物料元件很多,就需要按需載入元件。現有的開發工具如 webpack 也支援程式碼分割。但是在低程式碼平臺的開發場景中,平臺應用是和元件分離的,需要使用者在選擇某個元件的時候,要載入遠端元件程式碼。

載入方案

元件程式碼

我們以 vue 框架為例,假如當前有一個元件 A,程式碼如下,如何遠端載入這個元件呢?

<template>
    <div class="wp">{{text}}</div>
</template>

<script>
import { defineComponent, ref } from 'vue';
import _ from 'lodash';

export default defineComponent({
  setup(props) {
    console.log(_.get(props, 'a'));
    return {
      onAdd,
      option,
      size,
      text: 'hello world',
    };
  },
});
</script>

<style>
.wp {
    color: pink;
}
</style>

方案一:放在全域性物件上

步驟

  1. 打包:元件程式碼打包為 umd 格式,打包時配置 webpack externals, 使打包產物不包含公共的依賴;

  2. 上傳:打包的元件 js 上傳到 cdn;

  3. 載入:在需要使用元件時,插入一個 script ,在這個 script 中將元件放在一個全域性物件上;

  4. 註冊:在 script 插入完成後,從全域性物件上獲取元件,並進行註冊;

元件打包

首先需要增加一個入口檔案

import Component from './index.vue';

if(!window.share) {
  window.share = {};
}

window.share[Component.name] = Component;

以上面的入口檔案為入口,用 webpack 打包為 umd 格式

// 元件打包 webpack 配置
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  mode: 'production',
  entry: path.resolve(__dirname, './comps/index.js'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
    library: { type: 'umd' }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ],
  externals: {
    vue: 'vue',
    lodash: 'lodash',
  }
};

html 模板

元件公共依賴都需要先加入到模板 html 中

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn/vue.global.js"></script>
    <script src="https://cdn/[email protected]"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

元件載入邏輯

const loadComponent = (name) => new Promise((resolve) => {
  const script = document.createElement('script');
  script.src=`http://xxx/${name}.js`;
  script.onload = script.onreadystatechange = function(){
     resolve();
  };
  document.querySelector('head').appendChild(script);
})

const addComp = async (name) => {
      await loadComponent(name);
      // 註冊元件,其中 app 為 Vue 應用例項物件
      app.component(name, window.share[name]);
}

// 動態註冊元件
addComp('A');

缺點

  1. 元件的依賴共享,需要依賴提前先放到全域性,html 模板需要較頻繁改動;
  2. 全域性物件上要掛載的內容越來越多,影響載入效能,沒有做到真正的按需載入;
  3. 依賴版本難以管理。如 A 元件依賴了 loadsh 1.0, 而 B 元件依賴了 lodash 2.0,但是全域性物件上的 lodash,同時掛載兩個版本就必然會有衝突,因此版本必須一致;且後續如果某個元件要升級某個依賴的版本,也勢必會影響所以其他元件。

方案二:amd

amd 格式也是一種模組化方案,這裡我們選擇知名度比較高的 require.js 作為 amd 模組載入器。

步驟

  1. 打包:元件程式碼打包為 umd 或 amd 格式,打包時配置 webpack externals,使打包產物不包含公共的依賴;
  2. 上傳:打包的元件 js 上傳到 cdn;
  3. 載入&註冊:在需要使用元件時,用 requirejs 獲取元件,並進行註冊。

元件打包

用 amd 格式來做遠端載入時不需要像方案一一樣,增加額外的入口檔案,可以直接將 .vue 檔案作為入口。以下是 webpack 打包配置示例

// 元件打包 webpack 配置
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  mode: 'production',
  entry: path.resolve(__dirname, './comps/index.vue'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
    library: { type: 'umd' }  // 輸出 amd 或者 umd
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ],
  externals: {
    vue: 'vue',
    lodash: 'lodash',
  }
};

html 模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./require.js"></script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

元件載入邏輯

// main.js
requirejs.config({
  baseUrl: 'https://cdn.xxx.com',
  map: {
    '*': {
      css: 'require-css',
    },
  },
  paths: {
    echarts: '[email protected]',
    vueDemo: 'vue-demo',
    vue: '[email protected]',
    moment: 'https://cdn/[email protected]',
  },
  shim: {
    'ant-design-vue': ['css!https://cdn/[email protected]'],
  },
});

requirejs(['vue', 'vue-demo', 'vue-app'], function (vue, vueDemoModule, VueAppModule) {
  const app = Vue.createApp(VueAppModule.default);
  app.component('vue-demo', vueDemoModule.default);
  const vm = app.mount('#app');
});

缺點

vue-app

優點

  1. 相比於方案一,元件的依賴可以有版本差異且互相不影響。
  2. 元件和元件的依賴都可以按需載入,真正做到按需載入。
  3. 有現成的載入 css 檔案的機制;

方案三:ESModule

步驟

  1. 打包:元件程式碼打包為 esm 格式,打包時配置webpack externals, 使打包產物不包含公共的依賴;
  2. 上傳:打包的元件 js 上傳到 cdn;
  3. 載入&註冊:在需要使用元件時,用 esm 的動態引入獲取元件,並進行註冊;

元件打包

這裡需要注意的是,externals 配置項中直接把公共依賴配置為 cdn 地址;

import path from 'path';
import VueLoader from 'vue-loader';

const VueLoaderPlugin = VueLoader.VueLoaderPlugin;

const __dirname = path.resolve();

export default {
  mode: 'development',
  entry: path.resolve(__dirname, './src/vue-demo.vue'),
  output: {
    filename: 'vue-demo.esm.js',
    path: path.resolve(__dirname, 'components'),
    library: { type: 'module' }
  },
  experiments: { outputModule: true },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ],
  externals: {
    vue: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.esm-browser.js',
    'lodash': 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.js'
  }
};

使用上述配置打包後產物,中會把 'vue' 替換為 externals 中的 cdn 地址

// 輸入
import Vue from 'vue';

// 輸出結果
import Vue from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.esm-browser.js';

元件載入邏輯

const list = ref([]);

const addComp = async () => {
  const VueDemo = await import(/* @vite-ignore */`http://cdn/components/vue-demo.esm.js`)
  window.app.component('vue-demo', VueDemo.default);
  list.value.push({ key: new Date().valueOf(), name: 'vue-demo' });
}

vite 配置

需要注意的是要保證本地開發時引入的 vue 也是遠端的,所以需要在 vite 的配置檔案中增加 alias 配置。

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      'vue': 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.esm-browser.js'
    }
  }
})

缺點

  1. 相容性問題:很多 Webpack 已經支援很好的功能還沒有得到主流瀏覽器的支援
  2. 對很多第三方依賴的轉化處理不完善,缺失完善的解決機制。要將第三方依賴的載入全部交給瀏覽器本身來接管,那麼首先開發工具要做的就是將第三方依賴全部轉換為 ESModule 的模組,而現在 npm 上的絕大部分包都是隻支援 CommonJS 版本的,因此這裡的轉換過程通常需要由開發者自己來接管,而這其中有很多底層的問題並沒有得到好的解決。同時,在 ESModule 規範推進的過程中,有許多如 exports.defaultexports.__esModule 等利用語法來相容 ESModule 和 CommonJS 的廢案往往也都被 babel 實現,而且被許多開發者使用並且釋出到了 npm 上,這就導致了現在 npm 上的許多包中有大量的廢棄相容性程式碼,而這些程式碼往往會對開發工具的轉化造成阻礙。

優點

  1. 真正的按需載入
  2. 程式碼上更加優雅

關於 webpack 模組聯邦

基於筆者對模組聯邦的瞭解,筆者認為 Webpack 的模組聯邦,目前更加適合微前端的場景,但是不太適用於低程式碼平臺的場景。但是筆者對 webpack 模組聯邦瞭解不夠深入,判斷不一定準確,歡迎有不同意見的小夥伴在評論區討論。

結論

對比上面三個方案,方案一實現起來最簡單,但是沒有真正實現按需載入,隨著專案規模和需要滿足的業務場景的擴大,元件的公共依賴會越來越多。方案二 、方案三 都能實現真正的按需載入,其中 require.js 雖然聽上去已經是上個世紀的東西了,但是相容性和坑相對比較少。說到 ESModule, 雖然有相容性和上面提到的一些格式轉化的問題,但隨著近些年 Vite 、Snowpack 的發展,在未來 ESModule 一定是大勢所趨,目前筆者也正在將負責的我司內部大屏低程式碼平臺改造為 ESModule 方式載入。

參考

❉ 作者介紹 ❉