js模块化加载器实现

背景

自es6之前,JavaScript是天生模块化缺失的,即缺乏相似后端语言的class,
做用域也只以函数做为区分。这与早期js的语言定位有关,
做为一个只须要在网页中嵌入几十上百行代码来实现一些基本的交互效果的脚本语言,
确实用不着严格的组织代码规范。可是随着时代的发展,js承担的任务愈来愈重,
从原先的script引入几十行代码便可的状态变成如今多人协做文件众多的地步,
管理和组织代码的难度愈来愈大,模块化的需求也愈来愈迫切。
在此背景下,众多的模块化加载器便应运而生。
html

模块化规范和实现

前文提到在es6模块化出现以前,为了解决模块化的需求,出现了众多的模块化机制例如cmd,amd等。遵循不一样规范有sea.js, require.js等实现。前端

  • AMD:
    Asynchronous Module Definition 异步模块定义。浏览器端模块化开发的规范,
    模块将被异步加载,模块加载不影响后面语句的运行。全部依赖某些模块的语句均放置在回调函数中。
    AMD 是 RequireJS 在推广过程当中对模块定义的规范化的产出。require.js详情参考
//依赖前置,jquery模块先声明
define(['jquery'], function ($) {
/***/
})
  • CommonJS:
    CommonJS是服务器端模块的规范,Node.js采用了这个规范。Node.JS首先采用了js模块化的概念。CommonJS规范参考
//同步加载
var $ = require('jquery');
/****/
module.exports = myFunc;
  • CMD:
    CMD(Common Module Definition) 通用模块定义。该规范是SeaJS推广过程当中发展出来的。
    与AMD区别:
    AMD是依赖关系前置,CMD是按需加载。更多参考
define(function (require, exports, module) {
// 就近依赖
var $ = require('jquery');
/****/
})
  • UMD:
    Universal Module Definition 通用模块规范。
    基于统一规范的目的出现,看起来没那么简约,可是支持amd和commonjs以及全局模块模式。
//作的工做其实就是这么粗暴,判断当前用的什么就以当前规范来定义
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// 全局变量
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// methods
function myFunc(){};
// exposed public method
return myFunc;
}));

综上所诉,各个不一样的规范都有各自的优势,具体使用须要是各自项目状况而定。没有好很差只有适用与否。jquery

模块加载器实现原理浅析

以上各类模块加载器关于模块化的实现都有各自的特色,而且是比较成型完善的体系。本文也无心去从新实现一个大而全的模块加载器。
本着学习的态度,简单对其中的部分原理进行部分探究。
全部的模块加载器的实现都要有如下步骤:webpack

  • 动态建立脚本节点
    模块化归根到底仍是要在浏览器中加载脚本。
  • 解析依赖模块路径
    模块化的初衷就是解决各个文件的依赖问题,因此各个模块之间的依赖分析必不可少。
    该部分须要控制脚本加载队列,对递归依赖进行分析,按顺序进行加载
  • 模块缓存
    将每一个模块的内容根据特定规则保存起来,为调用作准备。
    对于没有声明name的模块要匹配的话就须要根据currentScript获取文件名。而后进行缓存.
基础方法声明

既然是有个加载器,固然是会指定一些规则来让使用者遵循。不然也实现不了相应的方法。不一样的框架的实现方式也是不一样的,不过速途同归。
做为一个模块加载器(简单归简单),基本的接口以下:git

  • 定义模块(define):
    define(deps,func,name)参数以下
    1 deps: 依赖模块,支持数组,或者省略
    2 func: 自身func,即接受依赖模块做为参数的回调
    3 name: 模块对应的name,若是不存在则根据当前路径来命名。
    功能无非是根据不一样的状态将该模块处理后的属性,例如name等。存入模块队列(modules变量)中。
modules[ src ] = {
name : name || src,
src : src,
dps : [],
exports : (typeof fn === "function")&&fn(),
state : "complete"
};
  • 依赖模块接口(require):
    require(deps,func)参数同define。这里就不实现支持commonjs的方式了,即依赖必须前置声明。
//不支持 
var a = require('a');
//支持 
require( ['a'], function(a) {
var a1 = a;
});

这样而来require模块的彻底能够经过define来实现了。es6

动态建立脚本

归根到底前端模块化的实质仍是经过script标签来引入相应文件(基于服务端的模块加载器非此类实现,例如webpack等)。
因此必不可少的须要进行建立和引入。主要用到的createElement方法来建立以及appendChild插入dom中。github

/**
* @param src string
* 此处的src为路径,即define里的字段
* */
var loadScript = function(src) {
/**
* 进一步处理,是否网络路径或者本地路径
* */
var scriptSrc = getUrl(src);
/**
* 接下来实现大同小异,无非是脚本加载变化时的处理函数的作法
* */
var sc = document.createElement("script");
var head = document.getElementsByTagName("head")[0];
sc.src = scriptSrc;
sc.onload = function() {
console.log("script tag is load, the url is : " + src);
};
head.appendChild( sc );
};
解析依赖模块路径

由前面建立脚本可知,须要解析脚本路径来分别区分当前不一样路径。
路径和模块的对应关系遵循id=路径的原则web

 
//此处的a对应的路径即为base+a.js.
require('a', function(){
//abcc
} )

固然实际状况中的匹配是很复杂的,简单实现就不考虑那么多。
对于匿名模块的存在,是能够经过document.currentScript获取当前路径手动给其增长标识的。
脚本路径无外乎一下几种状况:npm

  • 相对路径:
    此种路径只须要获取当前跟路径拼接处理便可。(为了简化处理,此处入口文件在项目根目录下)
  • http网络路径:
    此路径直接不变便可.
  • npm依赖的各类包,此处就先不处理这种了毕竟是简单实现。
var getUrl = function(src) {
var scriptSrc = "";
//判断URL是不是
//相对路径'/'或者'./'开头的,获取当前根路径替换掉其余字符便可。
if( src.indexOf("/") === 0 || src.indexOf("./") === 0 ) {
scriptSrc = require.config.base + src.replace(/(^\/|^\.\/)/,"");
}else if( src.indexOf("http:") === 0 ) {
//直接获取
scriptSrc = src;
}else if( src.match(/^[a-zA-Z1-9]/) ){
//不以路径符开头的直接凭借
scriptSrc = require.config.base + src;
}else if(true) {
alert("src错误!");
};
if (scriptSrc.lastIndexOf(".js") === -1) {
scriptSrc += ".js";
};
return scriptSrc;
};

此处还须要获取当前的根路径,模块化加载一定会有script来加载加载器js。因此能够据此来判断当前路径。
关于兼容性的处理,这里就不在讲述。编程

//去除&?等字符
var repStr = function(str) {
return (str || "").replace(/[\&\?]{1}.+/g,"") || "";
};
if(document.currentScript) return repStr(document.currentScript.src);
模块缓存

脚本加载以后,须要根据模块不一样的状态进行处理。模块主要分如下状态:
1 init:
初始化,即刚进行模块相关属性的处理,未进行模块解析。即将进行模块加载处理
2 loading:
模块解析中,即将完成
3 complete:
模块解析完成,将参数对象,exports接口存在缓存中。依赖模块解析完成以后进行执行。
至此,关于模块化的探究就基本结束了。说来原理你们都知道。无非就是解析一下模块路径,而后动态建立脚本,控制下加载就能够了。实现如下仍是有不少收获的

参考文章
相关文章
相关标签/搜索