最近工作需要,实现一个特定环境的模块加载方案,实现过程中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘。

本文不探讨为什么实现模块化,以及模块化相关的规范,直接考虑一些技术实现原理。

1.简单实现模块化

一开始我想如果我的代码只有一个文件,那几行不就实现了吗

main.js

var modules = {}
var define = function(id,factory){
moudles[id] = factory
}
var require = function(id){
return modules[id]
}
define("moduleA",{text:"I am text"})
var moduleA = require("moduleA");
console.log(moduleA)

main.html

<script src="main.js"></script>

2.拆多个文件

后来业务需求的增长,我的一个代码文件逐渐膨胀到了接近2w多行。这个时候每次改动文件找函数找半天啊,俺的编辑器也时不时的开始崩溃了,传到服务端的时候,也要等好久好久了。。。

于是我把文件拆成了3个:

1.module.js

var modules = {}
var define = function(id,factory){
moudles[id] = factory
}
var require = function(id){
return modules[id]
}

2.moduleA.js

define("moduleA",{text:"I am text"})

3.main.js

var A = require("moduleA")
console.log(A)

于是我在html中得这么写了

<script src="module.js"><script>
<script src="moduleA.js"><script>
<script src="main.js"><script>

后来了解到,我可以用构件工具gulpconcatwatch模块,可以监听文件改动,自动生成大文件,以便在开发的时候可以按模块拆成多个文件,运行的时候却是在一个文件。详细可以了解相关资料。

3.按模块加载文件

上面提到用构件工具来实现打包成一个文件,这样做有个缺点,代码如果有错误,报错的行数无法与相应文件模块的行数相对应,debug困难。

这个时候貌似只有不依赖于构件工具,我们在代码中实现加载其他模块。 貌似也挺简单的。

我们得知道,script标签是可以用JS动态创建和加载的

var loadScript = function(src){
var script = document.createElement("script")
script.src = src
document.head.appendChild(script)
}

于是我们可以在main.js中这样去加载

loadScript("module.js")
loadScript("moduleA.js")

这样就可以在页面中只引入一个主文件,然后在主文件中引入其他模块文件了。

多了解一些我们会知道 loadScript 这样的代码加载方法,是并行加载非顺序执行的,有可能moduleA的代码执行的时候module还没有执行,这是就会报错 variable define is not defined 了。

4.控制文件加载顺序

script在加载过程中会有一些状态,支持设立回调函数比如 onloadonreadysteadychange 这样我们可以在当一个模块加载完成后加载另一个模块来控制文件加载顺序。

我们常用的jsonp技术便也大概是这样一个原理。

var loadScript = function(src,callback){
var script = document.createElement("script")
script.src = src
script.onload = callback
document.head.appendChild(script)
}
loadScript("module.js",function(){
loadScript("moduleA.js",function(){
var A = require("moduleA")
console.log(A)
})
})

这样的坏处便是,代码中要写层层回调,模块的加载顺序需要写代码的人自己来管理。

5.XHR加载代码

script标签可以设置src加载远程代码,还可以直接把代码写在标签内。

<script>
define("A","i am A")
</script>

于是我们可以通过XHR对象,加载远程代码文本,然后动态的插入进去,比如innerHTML 甚至,XHR有同步的加载方法,来让我们串行的加载代码,避免写重重回调。当然,同步的XHR请求性能很低

XHR有个硬伤就是受浏览器同源策略影响,不能方便的跨域。

6.实现高级API

有了上面的一些基础,我们就可以来封装一些高级的API了。

一般来说,我们只需要这样一个define(id,deps,factory),实现了模块的定义和加载就基本够用了。

define("moduleC",["moduleA","moduleB"],function(moduleA,moduleB){
console.log(moduleA,moduleB)
})

这样的define做了这么一些事情

  • 将id 和 factory关联
  • 用loadscript的方案,去递归的加载deps,保证该模块被依赖时,模块本身依赖的模块都加载完毕。
  • 收集完毕后按照deps顺序将相关模块通过apply传递给factory

7.自动收集依赖

我们觉得每次去写一堆依赖,然后还要保证deps顺序和factory的变量顺序一致,一一对应着实有些蛋疼,这时候我们会想要把deps去掉,改成在factory里面写依赖。

moduleC.js

define("moduleC",function(require){
var moduleA = require("moduleA")
var moduleB = require("moduleB")
})

这时候需要用到JS的一个神奇的特性,function的toString方法可以拿到函数的源代码。 这样我们可以通过一些手段分析出 require 了哪些模块。可以看这里 https://github.com/seajs/seajs/issues/478

当然为了能够分析出require了哪些模块,我们要对require做一些约定,就是希望require有一些特定的标志,以便于我们能够通过代码文本静态的分析出require项。

比如说 不能够这样,详细见 https://github.com/seajs/seajs/issues/259

var req = require
req("moduleA")

然后呢,也不能用通用的压缩工具压缩,因为压缩工具会把require变量压缩。

8.定义匿名模块

有时候我们觉得文件名已经能够代表模块名字了,我们连定义模块名字都不想要了。

moduleC.js

define(function(){
var moduleA = require("moduleA")
var moduleB = require("moduleB")
return {
A : moduleA,
B : moduleB
};
})

当初看到这样的api用法时都震惊了,因为之前实现define的时候都会把id和factory相关联,这没ID怎么办?后来冷静下来,觉得ID一定是有的,只是有办法不通过函数参数传递。

果然,有一个document有一个对象叫做currentScript,可以获得当前正在执行的script的对象,于是moduleC.js在执行的时候,define是可以通过document.currentScript拿到src为moduleC.js的script对象的,进而可以提取出ID。

这里关于浏览器兼容性有一些细节:

  • document.currentScript只有现代浏览器才支持。
  • IE6-10会有一些黑魔法,利用浏览器单线程执行的特性,获取页面上所有的script标签,判断其readystate为interactive时,该script便是document.currentScript
  • 利用Error.stack,得到文件调用栈,来分析得到currentScript

写匿名模块方便是方便,但是会带来一些麻烦。

比如,不能直接打包成一个文件了,因为依赖于模块的文件名,这个很好理解了。

define(function(){
return 100
})
define(function(){
return 200
})

9.加载文本资源

甚是怀念在孢子工作时的那套代码结构与模块化方案,开发不需要依赖构建工具,模板直接写html文件,不用包装amd。 等等。。,模板直接写html文件是怎么做到的,尝试去看源码,基本看不懂,孢子源码太难读了。后来抓包才知道,原来是后端配合,在特定的目录名称下返回的html文件会自动包上define,黑魔法。。

当然也有其他方法,一般情况下就是用XHR,加载相应地文本,然后用eval设定执行上下文环境为global,来包装define。

10.参考:

  • http://requirejs.org/docs/why.html
  • https://github.com/seajs/seajs/issues/259
  • http://www.cnblogs.com/rubylouvre/archive/2013/01/23/2872618.html

关于前端JS模块加载器实现的一些细节的更多相关文章

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

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

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

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

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

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

  4. sea.js模块加载工具

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

  5. js 简易模块加载器 示例分析

    前端模块化 关注前端技术发展的各位亲们,肯定对模块化开发这个名词不陌生.随着前端工程越来越复杂,代码越来越多,模块化成了必不可免的趋势. 各种标准 由于javascript本身并没有制定相关标准(当然 ...

  6. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  7. 构建服务端的AMD/CMD模块加载器

    本文原文地址:http://trock.lofter.com/post/117023_1208040 . 引言:  在前端开发领域,相信大家对AMD/CMD规范一定不会陌生,尤其对requireJS. ...

  8. JavaScript AMD 模块加载器原理与实现

    关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者 ...

  9. js模块化加载器实现

    背景 自es6以前,JavaScript是天生模块化缺失的,即缺少类似后端语言的class, 作用域也只以函数作为区分.这与早期js的语言定位有关, 作为一个只需要在网页中嵌入几十上百行代码来实现一些 ...

随机推荐

  1. vistual studio 2012 安装失败,提示Microsoft Vistual Studio 2012 Devenv找不到元素,等错误信息

    在安装vistual studio 2012过程中,出现安装失败,提示Microsoft Vistual Studio 2012 Devenv找不到元素,等错误信息 解决方法是更新相应的server补 ...

  2. Linux经常使用命令(一) - ls

    ls命令是linux下最经常使用的命令.ls命令就是list的缩写, 缺省下ls用来打印出当前文件夹的清单, 假设ls指定其它文件夹, 那么就会显示指定文件夹里的文件及文件夹清单. 通过ls 命令不仅 ...

  3. 【视频】零基础学Android开发:蓝牙聊天室APP(二)

    零基础学Android开发:蓝牙聊天室APP第二讲 2.1 课程内容应用场景 2.2 Android UI设计 2.3 组件布局:LinearLayout和RelativeLayout 2.4 Tex ...

  4. 《java系统性能优化》--2.高速缓存

    上一节.简介了怎样发现性能瓶颈.从这节開始.我会和大家分享我在项目中做的一些性能调优工作.这个系列没有什么顺序可言,认为什么重要.就说说什么. 这节.我们聊缓存. 最開始接触缓存这个词,是学习硬件知识 ...

  5. Codeforces 527C Glass Carving(Set)

    意甲冠军  片w*h玻璃  其n斯普利特倍  各事业部为垂直或水平  每个分割窗格区域的最大输出 用两个set存储每次分割的位置   就能够比較方便的把每次分割产生和消失的长宽存下来  每次分割后剩下 ...

  6. Asp.net vNext 学习1

    Asp.net vNext 学习之路(一) 概述 asp.net vNext 也叫 asp.net 5.0,意思是微软推出的下一个版本的asp.net.可以说是微软对asp.net的一个比较重大的重新 ...

  7. UC编程:环境变量的查询与修改

    每个程序中都维护一个指向环境变量的指针char **environ; 子进程会从父进程继承环境变量.子进程环境变量的修改不一定会影响父进程 无关的多个进程之间修改环境变量不会互相影响 打印环境变量 [ ...

  8. LeetCode之Max Points on a Line Total

    1.问题描述 Given n points on a 2D plane, find the maximum number of points that lie on the same straight ...

  9. ReSharper 8.1支持Visual Studio 2013的特色——超强滚动条

    自ReSharper 8.1发布以来,便支持Visual Studio 2013.其中peek功能是它的亮点,滚动条则是它的特色. 接下来小编将展示ReSharper在Visual Studio 20 ...

  10. des和Rijndael加密

    ------------IV的作用: 为了保证数据的安全,.NET基类库中提供的私钥算法类使用称作密码块链(CBC,Cipher Block Chaining)的链模式,算法使用一个密钥和一个初始化向 ...