背景

自es6以前,JavaScript是天生模块化缺失的,即缺少类似后端语言的class,

作用域也只以函数作为区分。这与早期js的语言定位有关,

作为一个只需要在网页中嵌入几十上百行代码来实现一些基本的交互效果的脚本语言,

确实用不着严格的组织代码规范。但是随着时代的发展,js承担的任务越来越重,

从原先的script引入几十行代码即可的状态变成现在多人协作文件众多的地步,

管理和组织代码的难度越来越大,模块化的需求也越来越迫切。

在此背景下,众多的模块化加载器便应运而生。

模块化规范和实现

前文提到在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;
}));

综上所诉,各个不同的规范都有各自的优点,具体使用需要是各自项目情况而定。没有好不好只有适用与否。

模块加载器实现原理浅析

以上各种模块加载器关于模块化的实现都有各自的特点,并且是比较成型完善的体系。本文也无意去重新实现一个大而全的模块加载器。

本着学习的态度,简单对其中的部分原理进行部分探究。

所有的模块加载器的实现都要有以下步骤:

  • 动态创建脚本节点

    模块化归根到底还是要在浏览器中加载脚本。
  • 解析依赖模块路径

    模块化的初衷就是解决各个文件的依赖问题,所以各个模块之间的依赖分析必不可少。

    该部分需要控制脚本加载队列,对递归依赖进行分析,按顺序进行加载
  • 模块缓存

    将每个模块的内容根据特定规则保存起来,为调用做准备。

    对于没有声明name的模块要匹配的话就需要根据currentScript获取文件名。然后进行缓存.
基础方法声明

既然是有个加载器,当然是会指定一些规则来让使用者遵循。否则也实现不了相应的方法。不同的框架的实现方式也是不同的,不过速途同归。

作为一个模块加载器(简单归简单),基本的接口如下:

  • 定义模块(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来实现了。

动态创建脚本

归根到底前端模块化的实质还是通过script标签来引入相应文件(基于服务端的模块加载器非此类实现,例如webpack等)。

所以必不可少的需要进行创建和引入。主要用到的createElement方法来创建以及appendChild插入dom中。

/**
* @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=路径的原则

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

当然实际情况中的匹配是很复杂的,简单实现就不考虑那么多。

对于匿名模块的存在,是可以通过document.currentScript获取当前路径手动给其增加标识的。

脚本路径无外乎一下几种情况:

  • 相对路径:

    此种路径只需要获取当前跟路径拼接处理即可。(为了简化处理,此处入口文件在项目根目录下)
  • 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接口存在缓存中。依赖模块解析完成之后进行执行。

至此,关于模块化的探究就基本结束了。说来原理大家都知道。无非就是解析一下模块路径,然后动态创建脚本,控制下加载就可以了。实现以下还是有很多收获的

参考文章

js模块化加载器实现的更多相关文章

  1. js模块化/js模块加载器/js模块打包器

    之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7 ...

  2. 再唠叨JS模块化加载之CommonJS、AMD、CMD、ES6

    Javascript模块化编程,已经成为一个迫切的需求.理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块. Javascript社区做了很多努力,在现有的运行环境中,实现” ...

  3. 实现简单的 JS 模块加载器

    实现简单的 JS 模块加载器 1. 背景介绍 按需加载是前端性能优化的一个重要手段,按需加载的本质是从远程服务器加载一段JS代码(这里主要讨论JS,CSS或者其他资源大同小异),该JS代码就是一个模块 ...

  4. JS模块加载器加载原理是怎么样的?

    路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的 ...

  5. 实现一个JavaScript模块化加载器

    对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题.尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中 ...

  6. 关于前端JS模块加载器实现的一些细节

    最近工作需要,实现一个特定环境的模块加载方案,实现过程中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘. 本文不探讨为什么实现模块化,以及模块化相关的规范,直接考虑一些技术 ...

  7. JS的加载和执行

    从JS的加载和执行谈性能优化 ---高性能JS读后感(第一章) 从脚本的"霸道"说起,随着浏览器的进步,js越来越听话了,所以,我们先说说以前的浏览器是怎么加载js的,以及js如何 ...

  8. sea.js模块加载工具

    seajs的使用 seajs是一个jS模块加载器,由淘宝前端架构师玉伯开发,它可以解决命名空间污染,文件依赖的问题.可以在一个js文件中引入另外一个js.require('a.js') 1.安装 np ...

  9. js前端模块化之加载器原理解析(一)

    先来说一下前端模块化的价值:引用模块此处有详细的介绍,可以自行前往观看. 一.总结如下优点: (1)解决命名冲突(2)烦琐的文件依赖(3)模块的版本管理(4)提高可维护性(5)前端性能优化(6)跨环境 ...

随机推荐

  1. POJ3728 LCA RMQ DP

    题意简述:给定一个N个节点的树,1<=N<=50000 每个节点都有一个权值,代表商品在这个节点的价格.商人从某个节点a移动到节点b,且只能购买并出售一次商品,问最多可以产生多大的利润. ...

  2. 安装Ubuntu时的硬盘分区

    根目录 大小:60G~100G(用来安装程序) 新分区的类型:主分区 新分区的位置:空间起始位置 用于:EXT4日志文件系统 挂载点:"/" 大小:4G 新分区的类型:逻辑分区 新 ...

  3. 小学生之Hibernate插入数据修改数据使用数据库默认值的实现

    最近在写一个案例,定时任务对数据库进行更新操作,废话不多说,上代码: @Component("taskJob") public class TaskJob extends Hibe ...

  4. iOS核心笔记—源代码管理工具-GIT

    源代码管理工具-GIT 一. git 概述 1. git 简介? 什么是git? > git是一款开源的分布式版本控制工具 > 在世界上所有的分布式版本控制工具中,git是最快.最简单.最 ...

  5. TCP/IP 协议族的简介

    TCP/IP重要的特性就是分层.TCP/IP 按照层次分为四层:应用层.传输层.网络层.数据链路层.分层的好处就是当某些地方需要改变的时候,只需要将改变的层替换掉即可,而不用去把整体做替换.各层之间的 ...

  6. sql的一点总结<一>

    sql总结 1.常见的数据库对象有哪些?表(table) 视图(view) 序列(sequence) 索引(index) 同义词(synonym)存储过程(procedure) 存储函数(functi ...

  7. UINavigationController实现全屏滑动返回功能

    说明: UINavigationController默认在push出的控制器中都有边沿滑动返回功能,但是只能从屏幕左边滑才能返回,若从屏幕中间画并没有效果.下面实现全屏滑动功能. 探究: 系统默认能够 ...

  8. ZeroMQ初探

    概述 ZeroMQ(也称为 ØMQ,0MQ 或 zmq)是一个可嵌入的网络通讯库(对 Socket 进行了封装). 它提供了携带跨越多种传输协议(如:进程内,进程间,TCP 和多播)的原子消息的 so ...

  9. Python 最大公约数的欧几里得算法及Stein算法

    greatest common divisor(最大公约数) 1.欧几里得算法 欧几里德算法又称辗转相除法,用于计算两个正整数a,b的最大公约数. 其计算原理依赖于下面的定理: 两个整数的最大公约数等 ...

  10. JavaScript处理json格式数据

    JSON即JavaScript对象标记,是一种轻量级的数据交换格式,非常适用于服务器与JavaScript的交互.JSON是基于纯文本的数据格式. JSON是JavaScript的原生格式,可以使用J ...