【实战】动态表单之表单组件的插件式加载方案

共 9462字,需浏览 19分钟

 ·

2021-03-06 09:25

前言

关于动态化表单方案前面我们已经有过一次分享,没看过的同学可以看下之前的文章 ZooTeam 拍了拍你,来看看如何设计动态化表单。文章中提到随着业务差异化增多,我们采用了动态表单解决重复开发及逻辑堆叠的问题。随着动态化表单系统运行过程中业务方接入的越来越多,自定义组件插件式加载的需求开始出现并慢慢变得强烈。
我们希望添加新的自定义组件之后可以不需要重新发布项目,只需要单独发布自定义组件,然后在系统中注册该自定义组件,就能在配置表单页面的时候直接使用了。那么这就引出一个需求,表单组件的插件式加载并应用的能力。

组件插件式加载方案的现状

关于异步加载,各平台上一搜索,大多数出来的都是一些 Webpack 代码分拆相关的内容。而关于组件插件式加载的内容寥寥无几。让我们具体梳理一下。

一、Webpack 懒加载

Webpack 懒加载,也就是 Webpack 的拆包按需加载功能,其主要使用 import 方法进行静态资源的异步加载,具体使用方法为,代码中采用如下方式引入需要被拆包的文件:
import('./moduleA').then((moduleA) => {
  moduleA.add(1,2); // 3
})
这么 Webpack 在打包时会将 moduleA 单独拆分出来作为一个 JS 文件,项目在执行到这段代码的时候才动态加载这部分 JS 资源。但是如果直接使用 import 方法加载远程资源,Webpack 打包过程会直接报错。不满足需求。
import('http://static.cai-inc.com/moduleA.js').then((moduleA) => {
   // ERROR,打包过程会出现报错
  moduleA.add(1,2);
})
报错信息:

二、现有浏览器支持的 Dynamic Import

对于这种方法,其浏览器兼容性问题难以满足要求,IE 浏览器完全不支持并且有同域名的限制。使用方法同 Webpack 懒加载一样:
import('http://static.cai-inc.com/moduleA.js').then((moduleA) => {
  moduleA.add(1,2); // 3
})

三、require.js AMD 规范

使用 require.js 去加载一个符合 AMD 规范的 JS 文件。具体使用方法如下:
// 需要被动态加载的 moduleA.js
define('moduleA', [], function ({
  var add = function (x, y{
    return x + y;
  };
  return {
    add: add
  };
});
// 加载和使用
require.config({
  paths: {
    "moduleA""lib/moduleA"
  }
});
require(['moduleA'], function (moduleA){
    // 代码
    moduleA.add(1,2); // 使用被动态引入的插件的方法
});
在这个方法中,moduleA 是动态插件,要使用动态插件则需要配置好插件的路径,然后使用 require 进行引用。这需要我们引用 require.js 到现有项目中,在项目的 HTML 中定义一个 Script 标签并设置 data-main="scripts/main" 作为入口文件。但是我们的 React 项目也有一个入口,这会导致出现两个入口。两者用法并不能很好的并存。

需求拆解

那么现在来分析一下实现组件插件式加载的关键问题

一、加载资源

  • 因为插件单独发布之后要放在 CDN 上,所以加载静态资源的方案需要满足没有跨域限制的条件。

二、插件模块打包

  • 插件模块最好能使用现有模块标准例如 CMD、AMD 模块标准,这样我们就可以使用更多的社区开源方案,降低方案的风险性。同时降低团队成员学习使用成本。
  • 插件需要能够被注入依赖,例如项目中已经包含有 Lodash 或者 AntD 组件库的包,这时候插件模块中使用 Lodash 或者 AntD 组件库的话我们当然希望能够直接引用项目中已有的,而不是插件模块中重新引入一个。

需求分析

一、静态资源加载

对于运行中加载静态资源,现有解决方案中不论是哪一种,都是利用动态插入 Script 或者 Link 标签来实现的。而且这种方案不会有域名限制问题。具体实现大体如下:
export default function (url{
  return new Promise(function (resolve, reject{
    const el = document.createElement('script'); // 创建 script 元素
    el.src = url; // url 赋值
    el.async = false// 保持时序
    const loadCallback = function (// 加载成功回调
      el.removeEventListener('load', loadCallback);
      resolve(result);
    };
    const errorCallback = function (evt// 加载失败回调
      el.removeEventListener('error', errorCallback);
      var error = evt.error || new Error("Load javascript failed. src=" + url);
      reject(error);
    };
    el.addEventListener('load', loadCallback);
    el.addEventListener('error', errorCallback);
    document.body.appendChild(el); // 节点插入
  });
}

二、为加载模块注入依赖

关于这一点我们可以看下遵循 AMD 规范的 require.js 是怎么做的。代码:
// require.js
const modules = {};
const define = function(moduleName, depends, callback){
  modules[moduleName] = { // 将模块存起来,等待后续调用
    depends,
    callback,
  };
}
// moduleA.js
define('moduleA', [], ()=>{
  // code
})
因为通过插入 Script 的方式引入 JS 资源,JS 会被立刻执行,所以在 require.js 中加载进来的 JS 模块都是被 define 方法包裹着的,真正需要执行的代码是在回调函数中等待后续调用。当 moduleA.js 被加载成功之后,立即调用 define 方法,这里执行的内容则是把项目的模块储存起来等待调用。依赖的注入则是回调中将依赖作为参数注入。其实不论是基于哪一种规范,动态加载静态资源的策略都大致一样。模块中使用一个函数 A 将目标代码包起来。将该函数 A 作为一个函数 D 的参数。当模块被加载时,浏览器中已经定义好的 D 函数中就可以获取到含有目标代码块的函数 A 了。接下来想在哪里调用就在哪里调用。想注入什么变量就注入什么变量了。
  • 备注
    • 这里是对 AMD 进行了粗略的原理解释,具体实现还有很多细节,想要了解的话,可以在网上找到很多源码解析,这里就不再细讲。
    • Webpack 打包之后的代码的模块管理方式是 Webpack 自己实现的一套类似 CommonJS 规范的东西。去看看打包生成的代码就可以发现里面都是一些 webpack_modules__,webpack_require,webpack_exports 这样的关键词,和 CommonJS 规范的 modules,require,exports 相对应。

三、模块打包标准

由于我们团队使用的是 Webpack 的打包体系,因此想要保持技术栈统一,则要先从 Webpack 的打包入手。让我们将 Webpack 的模块化打包都试一下看看能得出什么。
Webpack library 打包方式有 5 种。
  • 变量:作为一个全局变量,通过 script 标签来访问(libraryTarget:'var')。
  • this:通过 this 对象访问(libraryTarget:'this')。
  • window:通过 window 对象访问,在浏览器中(libraryTarget:'window')。
  • UMD:在 AMD 或 CommonJS 的 require 之后可访问(libraryTarget:'umd')。
  • AMD:基于 AMD 规范的打包方式(libraryTarget:'amd')。
可以排除前三个,我们并不想将模块挂到 window 或者全局变量下。所以我们需要尝试的只有后面两个。
需要被打包的代码块:
export default {
  test()=>{
    console.log('测试模块打包!');
  }
};
AMD 规范打包后:
define(["lodash"], (__WEBPACK_EXTERNAL_MODULE__92__) => (() => {
  // code ...
  // return funciton
})());
UMD 规范打包后:
(function webpackUniversalModuleDefinition(root, factory{
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory(require("lodash")); // cmd
    else if(typeof define === 'function' && define.amd)
        define(["lodash"], factory); // amd
    else { // 
        var a = typeof exports === 'object' ? factory(require("lodash")) : factory(root["_"]);
        for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
    }
})(self, function(__WEBPACK_EXTERNAL_MODULE__92__{
    // code
});
可以看出来,AMD 规范打包后,代码执行了一个 define 方法。依赖注入是通过回调方法的参数进行注入的。那么我们是不是可以在加载 JS 文件之前先在 window 下挂一个 define 方法,等文件加载完执行 define 方法的时候,我们就可以在 define 方法中做我们想做的事情了。同理 UMD 打包规范也可以通过类似的操作达到我们的目的。所以这两种方案都可以。考虑到后期动态表单页面转本地代码的需求,希望插件还能被 npm 安装使用。这里采用了 UMD 规范。

方案选取

一、加载资源的方案

  • 采用动态插入 Script 方式实现 JS 资源加载。

二、模块打包方案

  • UMD 规范的打包方式。
最终实现代码参考:
// importScript.js
export default function (url, _{
  const defineTemp = window.define; // 将 window 下的 define 方法暂存起来。
  let result; // 结果
  window.define = (depends, func) => { // 自定义 define 方法,
    result = func(_); // 包依赖注入 
  }
  window.define.amd = true// 伪装成 amd 的 define。
  return new Promise(function (resolve, reject{
    const el = document.createElement('script'); // 创建 script 元素
    el.src = url;
    el.async = false// 保持时序
    const loadCallback = function (// 加载完成之后处理
      el.removeEventListener('load', loadCallback);
      window.define = defineTemp;
      resolve(result);
    };
    const errorCallback = function (evt// 加载失败之后处理
      el.removeEventListener('error', errorCallback);
      window.define = defineTemp;
      var error = evt.error || new Error("Load javascript failed. src=" + url);
      reject(error);
    };
    el.addEventListener('load', loadCallback); // 绑定事件
    el.addEventListener('error', errorCallback); // 绑定事件
    document.body.appendChild(el); // 插入元素
  });
}
调用方式
import importScript from './importScript.js';
import _ from 'lodash';
importScript('http://static.cai-inc.com/app.bundle.js', _).then((mod)=>{
   // code mod.xxx
})

三、与自定义表单结合

组件插件式引入的方式解决了,但是又引入了一个新的问题,一个表单页面如果有 10 个自定义组件的话,是不是就得动态加载 10 个静态资源呢,如果每个组件都有一个 JS,一个 CSS。那就是 20 个。这是不具备可行性的。
所以就有了组件合并的需求。
在配置表单页面的时候当用户发布该页面的时候,服务端建一个临时项目,将该页面的所有涉及到的自定义组件安装到该项目上,并 export 出去。编译打包,生成符合 UMD 规范的文件模块。然后再按照以上方式进行引入。这样就解决了多文件合并的问题。

总结

最后方案其实很简单,只是对 UMD 规范打包的一种灵活应用。基于 UMD 规范打包出一个组件代码,通过动态插入 Script 标签的方式引入该组件的 JS 代码。在引入之前定义一个 window.define 方法。在该组件的 JS 代码下载成功之后,就会调用到我们定义的 window.define 方法。这样我们就能对插件模块进行依赖注入并将它储存起来备用了。

欢迎关注「前端杂货铺」,一个有温度且致力于前端分享的杂货铺

关注回复「加群」,可加入杂货铺一起交流学习成长


浏览 69
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报