• 如何构建一个微型的CMD模块化加载器


    前言

    前端模块化是一个老生常谈的话题,模块化的好处是不言而喻,比如易于代码复用、易于维护、易于团队开发d等云云。对于前端模块加载器,以前仅仅止步于会用的阶段,为了加深对前端模块化的理解,大概花了一周的时间来学习、调研并尝试自己实现一个简易版的符合CMD规范的加载器。

    设计

    加载器是按照CMD规范进行设计的,具体的CMD规范就不列出了,详情请见CMD规范

    入口函数 use(ids, callback)

    模块定义函数 define(factory)

    模块加载函数 require(id)

    取得模块接口函数 getModuleExports(module)


    代码实现

    use(ids, callback)

    use为程序启动的入口,主要干两件事:

    1. 加载指定的模块
    2. 待模块加载完成后,调用回调函数
     1 function use(ids, callback) {
     2      if (!Array.isArray(ids)) ids = [ids];
     3      Promise.all(ids.map(function (id) {
     4          return load(myLoader.config.root + id);
     5      })).then(function (list) {
     6          callback.apply(global, list);// 加载完成, 调用回调函数
     7      }, function (error) {
     8          throw error;
     9      });
    10  }
     1 function load(id) {
     2        return new Promise(function (resolve, reject) {
     3            var module = myLoader.modules[id] || Module.create(id); // 取得模块或者新建模块 此时模块正在加载或者已经加载完成
     4            module.on("complete", function () {
     5                var exports = getModuleExports(module);
     6                resolve(exports);// 加载完成-> 通知调用者
     7            })
     8            module.on("error", reject);
     9        })
    10    }

     use会调用load函数,这个函数的作用是根据模块的id,加载模块,并返回一个Promise对象。

    define(factory)

    define的作用主要是用来定义一个模块。按照CMD的规范,定义一个模块的代码类似:

    1 var factory = function(require, exports, module){
    2     // some code
    3 }
    4 define(factory);


    为了方便说明,我给匿名函数取名为factory, factory就是我们模块定义的工厂函数,它只是define函数的一个参数,并不会被直接执行,而是会在需要的时候由专门的函数来调用生成接口。
     

    所以, 一个模块文件被浏览器下载下来后,并不会直接运行我们的模块定义代码,而是会首先执行一个define函数,这个函数会取得模块定义的源代码(通过函数的toString()函数来取得源代码),然后利用正则匹配找到依赖的模块(匹配require("dep.js")这样的字符串),然后加载依赖的模块,最后发射一个自定义事件complete,通知当前模块, 模块已经加载完成,此时,当前模块的就会调用与complete事件绑定的回调函数,完成与这个模块相关的任务,比如resolve与这个模块加载绑定的Promise
    具体实现为:

     1 function define(factory) {
     2   var id = getCurrentScript();
     3   id = id.replace(location.origin, "");
     4   var module = myLoader.modules[id];
     5   module.factory = factory;
     6   var dependences = getDependcencs(factory);
     7   if (dependences) {
     8       Promise.all(dependences.map(function (dep) {
     9           return load(myLoader.config.root + dep);
    10       })).then(function () {
    11           module.fire("complete"); // 依赖加载完成,通知模块。
    12       }, function () {
    13           module.fire("error");
    14       });
    15   } else {
    16       module.fire("complete");//没有依赖,通知模块加载完成
    17   }
    18 }


    require(id)
     

    require函数比较简单,主要作用就是根据模块id获取指定的模块,然后返回这个模块的对外接口。

    1 function require(id) {
    2        var module = myLoader.modules[myLoader.config.root + id];
    3        if (!module) throw "can not load find module by id:" + id;
    4        else {
    5            return getModuleExports(module); // 返回模块的对外接口。
    6        }
    7    }


    模块定义代码直到现在,才会被运行。运行模块定义代码的函数就是
    getModuleExports函数: 

    1 function getModuleExports(module) {
    2     if (!module.exports) {
    3         module.exports = {};
    4         module.factory(require, module.exports, module);
    5     }
    6     return module.exports;
    7 }


    记得刚接触
    sea.js的时候,对接口暴露对象moduleexports的区别不是很清楚,学习完别人的源码并尝试自己实现一遍的时候,它们的区别已经非常明朗了: 

    exports只是module.exports的一个引用,单纯的改变exports的值并不会对module.exports造成任何影响,所以通过

    1 exports = {
    2    foo: function(o){
    3       return o;
    4    }
    5 }


    这样的形式来定义接口是无效的。
     

    测试

    DEMO请见这里, 源码请见这里

    请打开控制台查看结果

    总结

    果然学习技术最好方法之一就是阅读别人的代码。阅读别人的代码是痛苦的,因为代码里充斥这他个人的代码癖好,有时候一个很简单的条件判语句可能用一些hack技巧实现了之后,在不了解的情况下,看的就比较痛苦了,以为另有玄机,傻乎乎的看了半天。不过,到最后搞明白之后,还是有些许成就感的。

    前端模块化加载器,以前是只见树木不见森林,通过这次学习,不能说完全搞清楚了一个模块加载器的所有实现细节,但是对于像模块是怎样实现异步加载的,模块是如何定义的,模块间如何进行依赖分析的这些问题有了一个更深的认识和理解。

    参考资料

  • 相关阅读:
    使用Lua编写Wireshark插件解析KCP UDP包,解析视频RTP包
    开源自己用python封装的一个Windows GUI(UI Automation)自动化工具,支持MFC,Windows Forms,WPF,Metro,Qt
    2019 WebRtc AudioMixer混音流程
    记录一次定位视频通话 音视频卡顿的原因分析过程。
    C++标准库里自带的数值类型和字符串互相转换函数
    C++ raw string literal
    使用multiprocessing解决PyMuPDF不支持多线程加载导致的界面卡死无响应问题,及一个PyQt5实现的简易PDF阅读器例子
    使用ctypes调用系统C API函数需要注意的问题,函数参数中有指针或结构体的情况下最好不要修改argtypes
    使用python uiautomation从钉钉网页版提取公司所有联系人信息
    使用python UIAutomation从QQ2017(v8.9)群界面获取所有群成员详细资料,
  • 原文地址:https://www.cnblogs.com/Natumsol/p/5063797.html
Copyright © 2020-2023  润新知