这是第 154 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:浅谈低代码平台远程组件加载方案
前言
低代码开发平台(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>
方案一:放在全局对象上
步骤
-
打包:组件代码打包为 umd 格式,打包时配置 webpack externals, 使打包产物不包含公共的依赖;
-
上传:打包的组件 js 上传到 cdn;
-
加载:在需要使用组件时,插入一个 script ,在这个 script 中将组件放在一个全局对象上;
-
注册:在 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/lodash@4.17.21.min.js"></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');
缺点
- 组件的依赖共享,需要依赖提前先放到全局,html 模板需要较频繁改动;
- 全局对象上要挂载的内容越来越多,影响加载性能,没有做到真正的按需加载;
- 依赖版本难以管理。如 A 组件依赖了 loadsh 1.0, 而 B 组件依赖了 lodash 2.0,但是全局对象上的 lodash,同时挂载两个版本就必然会有冲突,因此版本必须一致;且后续如果某个组件要升级某个依赖的版本,也势必会影响所以其他组件。
方案二:amd
amd 格式也是一种模块化方案,这里我们选择知名度比较高的 require.js 作为 amd 模块加载器。
步骤
- 打包:组件代码打包为 umd 或 amd 格式,打包时配置 webpack externals,使打包产物不包含公共的依赖;
- 上传:打包的组件 js 上传到 cdn;
- 加载&注册:在需要使用组件时,用 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: 'echarts@5.1.1',
vueDemo: 'vue-demo',
vue: 'vue@3.2.37',
moment: 'https://cdn/moment@2.29.1.min',
},
shim: {
'ant-design-vue': ['css!https://cdn/ant-design-vue@2.1.6.min.css'],
},
});
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
)也需要编译为 amd 格式,然后上传到 cdn 上,开发流程改变,需要定制化的开发平台项目的发布机制。 - 有些第三方库没有提供 amd 或 umd 格式,需要开发者自己开发工具去转换(此过程中可能有很多坑要踩);
优点
- 相比于方案一,组件的依赖可以有版本差异且互相不影响。
- 组件和组件的依赖都可以按需加载,真正做到按需加载。
- 有现成的加载 css 文件的机制;
方案三:ESModule
步骤
- 打包:组件代码打包为 esm 格式,打包时配置webpack externals, 使打包产物不包含公共的依赖;
- 上传:打包的组件 js 上传到 cdn;
- 加载&注册:在需要使用组件时,用 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/vue@3.2.37/dist/vue.esm-browser.js',
'lodash': 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.js'
}
};
使用上述配置打包后产物,中会把 'vue'
替换为 externals 中的 cdn 地址
// 输入
import Vue from 'vue';
// 输出结果
import Vue from 'https://cdn.jsdelivr.net/npm/vue@3.2.37/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/vue@3.2.37/dist/vue.esm-browser.js'
}
}
})
缺点
- 兼容性问题:很多 Webpack 已经支持很好的功能还没有得到主流浏览器的支持
- 对很多第三方依赖的转化处理不完善,缺失完善的解决机制。要将第三方依赖的加载全部交给浏览器本身来接管,那么首先开发工具要做的就是将第三方依赖全部转换为 ESModule 的模块,而现在 npm 上的绝大部分包都是只支持 CommonJS 版本的,因此这里的转换过程通常需要由开发者自己来接管,而这其中有很多底层的问题并没有得到好的解决。同时,在 ESModule 规范推进的过程中,有许多如
exports.default
、exports.__esModule
等利用语法来兼容 ESModule 和 CommonJS 的废案往往也都被 babel 实现,而且被许多开发者使用并且发布到了 npm 上,这就导致了现在 npm 上的许多包中有大量的废弃兼容性代码,而这些代码往往会对开发工具的转化造成阻碍。
优点
- 真正的按需加载
- 代码上更加优雅
关于 webpack 模块联邦
基于笔者对模块联邦的了解,笔者认为 Webpack 的模块联邦,目前更加适合微前端的场景,但是不太适用于低代码平台的场景。但是笔者对 webpack 模块联邦了解不够深入,判断不一定准确,欢迎有不同意见的小伙伴在评论区讨论。
结论
对比上面三个方案,方案一实现起来最简单,但是没有真正实现按需加载,随着项目规模和需要满足的业务场景的扩大,组件的公共依赖会越来越多。方案二 、方案三 都能实现真正的按需加载,其中 require.js 虽然听上去已经是上个世纪的东西了,但是兼容性和坑相对比较少。说到 ESModule, 虽然有兼容性和上面提到的一些格式转化的问题,但随着近些年 Vite 、Snowpack 的发展,在未来 ESModule 一定是大势所趋,目前笔者也正在将负责的我司内部大屏低代码平台改造为 ESModule 方式加载。
参考
推荐阅读
开源作品
- 政采云前端小报
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
- 商品选择 sku 插件
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com