JavaScript 模块化编程(三):实现一个RequireJS

JavaScript 模块化编程(一):模块的写法
JavaScript 模块化编程(二):规范
JavaScript 模块化编程(三):实现一个RequireJS
JavaScript 模块化编程(四):结合Node源码分析CommonJs规范


虽然RequireJS是一个过时的JS模块化解决方案,但是其对我们了解JS模块化的发展依然很重要。下面将参考RequireJS源码写了一个简单的JavaScript模块加载器。只有300行代码,是源码的六分之一,已经放在我的github上

--------点击查看代码--------


1.RequireJS

先回顾一下RequireJS的两个重要api

定义模块
define(id?, dependencies?, factory)
加载模块
require([module], callback)

require和define函数的关系
(1)require和define函数接收同样的参数,define和require在依赖处理和回调执行上都是一样的
(2)define的回调函数需要有return语句返回模块对象,这样define定义的模块才能被其他模块引用,所以用它来定义模块(建议在一个文件中使用一次);require的回调函数不需要return语句。其实在我看来,require函数可以看做是特殊的define函数,它用来定义一个顶层匿名模块,用来加载和使用模块,这个模块不需要被其他模块加载。

RequireJS中的执行流程
(1)RequireJS首先找到data-main属性,然后根据属性值(通过新建一个script标签)加载并且解析入口文件
(2)先调用 require 函数和define函数,将所有的依赖转化为script 节点插入到dom 中,然后每一个 节点的onload事件中,将该模块作为实体保存起来,并检查所有模块是否加载完成,如果加载完成,递归执行所有回调


2.实现一个简单的RequireJS

首先这里先说明一点,下面我们主要了解实现的整体流程,会粘出一些主要代码,里面可能会包含一些辅助函数,具体可以查看我github上的源码

(1)定义一个全局变量
首先,需要定义几个全局变量,用来保存已经加载的模块,尚未加载的模块,所有模块等全局信息

var  context={
      topModule:"", //顶层模块
      waitings:[], // 尚未加载的模块
      loadeds:[], // 已经加载的模块
      baseUrl:"",
        /**
         * 每一个模块都有下面的几个属性:
         * moduleNmae  模块名称
         * deps 模块依赖
         * factory 模块工厂函数 .
         * args 该模块的依赖模块的返回值
         * returnValue 该模块工厂函数的返回值
         */
      modules:[] // 模块集合
     } ;

(2)加载 require 顶层模块
在require.js 里面都是用 data-main 属性来指定入口文件,所以先寻找 data-main 属性,并插入到 head中 。这里将 data-main 作为的路径作为 baseUrl

/**
 * 查找data-main属性的script标签,
 * 根据属性值(通过新建一个script标签)加载并且解析入口文件
 */
if (isBrowser) {

     var scripts=document.getElementsByTagName('script');
     var head,src,subPath,mainScript;

     eachList(scripts,function(script){
      var dataMain = script.getAttribute('data-main');
            if (dataMain) {
                if (!head) {
                head = script.parentNode;
                }
                if (!context.baseUrl) {
                    src = dataMain.split('/');
                    mainScript = src.pop();
                    subPath = src.length ? src.join('/')  + '/' : './';
                    context.baseUrl = subPath;
                }
                // 创建顶层节点
                var dataMainNode = document.createElement('script');
                dataMainNode.async = true;
                head.appendChild(dataMainNode);
                dataMainNode.src = dataMain+ ".js";
                dataMainNode.onload = function() {
                    // 将顶层模块 从waitings里面除去,并添加到loadeds数组中
                    removeByEle(context.waitings, context.topModule)
                    context.loadeds.push(context.topModule);
                }
                return true;
            }
     });

(3)定义 require 方法
require 方法用来使用模块,也就是定义一个顶层模块,这个模块不需要被其他模块加载

 /**
 * require方法,加载一个模块
 * @param  {[type]}  deps    [依赖数组]
 * @param  {Function} callback [工厂函数]
 * @return {[type]}
 */
 requireJs.require=function(deps,callback){

        if (typeof name !== 'string') {
            callback = deps;
            deps = name;
            name = null;
        }

       if (!isArray(deps)) {
            callback = deps;
            deps = [];
        }

        // 生成随机模块名,方法
        let moduleName = getUnqName();
        context.topModule = moduleName;
        context.waitings.push(moduleName);
        // 生成一个模块配置
        context.modules[moduleName] = {
            moduleName: moduleName,
            deps: deps,
            factory: callback,
            args: [],
            returnValue: ""
        }

        deps.forEach(function(dep) {
            var scriptNode = document.createElement("script");
            scriptNode.setAttribute("data-module-name", dep);
            scriptNode.async = true;
            scriptNode.src = context.baseUrl + dep + ".js";
            document.querySelector("head").appendChild(scriptNode);
            scriptNode.onload = scriptOnload;
            context.waitings.push(dep);
        });

 }

这里需要注意一个函数scriptOnload,在script 节点加载完成后触发。将对应模块从waitings 里面删除,同时往loadeds里面添加该模块,如果发现 waitings为空,那么就开始递归执行工厂函数 。

/**
 *  [每一个脚本插入head中,都会执行这个事件 。这个函数完成两件事:
 *  1. 如果是一个匿名模块加载,那么取得这个匿名模块,并完成模块命名,
 *  2. 当节点加载完毕,判断context.waitings是否为空,如果不为空,返回,如果为空,说明已经全部加载完毕,现在就可以执行所有的工厂函数]
 * @param  {[object]} e [事件对象]
 * @return {[type]}
 */
function scriptOnload(e) {
    e = e || window.event;
    let node = e.target;
    let moduleName = node.getAttribute('data-module-name');
    tempModule.moduleName = moduleName;
    context.modules[moduleName] = tempModule;
    removeByEle(context.waitings, moduleName);
    context.loadeds.push(moduleName);

    if (!context.waitings.length) {
        console.log(context.modules);
        exec(context.topModule);
    }
}

(4)定义 define 方法
其实define函数和上面的require函数做了差不多相同的事,差别在于require自动生成了一个模块名。并且require中设置了context.topModule

/**
 * [define和 require 做的工作几乎相同]
 * @param  {[array]} deps    [依赖数组]
 * @param  {[function]} callback [工厂函数]
 * @return {[type]}
 */
 requireJs.define=function(name,deps,callback){

      if (typeof name !== 'string') {
            callback = deps;
            deps = name;
            name = null;
        }

      if (!isArray(deps)) {
            callback = deps;
            deps = [];
        }

        //生成一个模块配置
        tempModule = {
            deps: deps,
            factory: callback,
            args: [],
            returnValue: ""
        }

        // 递归遍历所有依赖,添加到 `head` 中,并设置 这个节点的一个属性`data-module-name`标识模块名
        deps.forEach(function(dep) {
            var scriptNode = document.createElement("script");
            scriptNode.setAttribute("data-module-name", dep);
            scriptNode.async = true;
            scriptNode.src = context.baseUrl + dep + ".js";
            document.querySelector("head").appendChild(scriptNode);
            scriptNode.onload = scriptOnload;
            context.waitings.push(dep);
        });
 }

(5)执行回调
我们再回到scriptOnload 函数,每个模块加载完成,就会在 waitings 里面去掉,然后检查waitings 数组,如果为空,说明全部加载完,就可以执行 exec函数,在这里函数中,递归执行所有的回调 。

/**
 * 所有模块加载完毕,递归执行工程函数 , 核心方法
 * @param  {[string]} moduleName [模块名]
 * @return {[type]}
 */
function exec(moduleName) {
    let module = context.modules[moduleName];
    let deps = module.deps;
    let args = [];
    if(deps){
        deps.forEach(function(dep) {
            exec(dep);
            args.push(context.modules[dep].returnValue);
        });
        module.args = args;
        module.returnValue = context.modules[moduleName].factory.apply(context.modules[moduleName], args);
    }
}

3.测试
//user.js
define([], function () {
    return {
        checkLogin: function (name,pwd) { 
            return name==="xxx"&&pwd==="yyy"
        }
    }
})

//math.js
define(function () {
    return {
        add: function (a,b){
            return a+b;
        },
        sub:function(a,b){
            return a-b;
        }     
    }
})
//main.js
require(["math","user"], function(math,user) {
  if(user.checkLogin("xxx","yyz")){
    console.log("12+21=" + math.add(12,21));
  }else{
    console.log('please sign in or register first');
  }
})
//test.html
 

结果

JavaScript 模块化编程(三):实现一个RequireJS_第1张图片
result.png

你可能感兴趣的:(JavaScript 模块化编程(三):实现一个RequireJS)