最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 动态表单之表单组件的插件式加载方案

    正文概述 掘金(政采云前端团队)   2021-02-03   388

    动态表单之表单组件的插件式加载方案

    前言

    关于动态化表单方案前面我们已经有过一次分享,没看过的同学可以看下之前的文章 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 方法。这样我们就能对插件模块进行依赖注入并将它储存起来备用了。

    推荐阅读

    结合React源码,五分钟带你掌握优先队列

    【Flutter 技能篇】你不得不会的状态管理 Provider

    招贤纳士

    政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

    如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

    动态表单之表单组件的插件式加载方案


    起源地下载网 » 动态表单之表单组件的插件式加载方案

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元