Javascript模块全揽

以前写的文章急速Js全栈教程获得了不错的阅读量,霸屏掘金头条3天,点赞过千,阅读近万,甚至还有人在评论区打广告,可见也是一个小小的生态了;)。看来和JS全栈有关的内容,仍是有人很有兴趣的。javascript

此次文章的内容,是JavaScript模块。JavaScript Module 真是很讨厌,可是不得不了解的话题。奇葩在于:css

  1. 它一个很是老的语言,而且使用很是普遍
  2. 但是它不少年来也不支持模块。这得厂家当前是多大的心呢
  3. 再一个但是,它能够直接用现有的语言机制,实现本身的模块,这个就厉害了,由于它释放了社区的力量。事实证实,社区果真不可小看,这个年代,蚂蚁雄兵赛过大象的
  4. 再再一个可是,它的模块还能够有不少型的,这说的是分裂
  5. 这么多型的模块,还搞了各自独立的标准出来,这说的是整合

最近的ES2017,终于在前端也有了媲美后端的模块,可是你们并不许备把它用起来,不少人表示须要继续Webpack玩转ES6模块html

把ES6模块真用的起来,能够不在意Webpack等打包工具带来的加载优化,各类小文件没必要打包这点来讲,我看还得加上HTTP/2的配合就好不少了。这也是文章将要介绍的一个主旨吧。ES6模块的引入,确实有可能对当前主流的打包模式有些影响,参考文章6内有所论述前端

文章天然也很多,可是写做此文的理由仍是存在:java

  1. 我尚未看到一个完整的全览,而且结合HTTP/2的更加没有看到。
  2. 并且,在我看来,即便有了ES6模块,也得了解和学习以前拼出来的各类模块,由于社区内的代码还大量的使用这样的模块,其中的一些设计模式,好比IIFE,也是值得一看的。
  3. 看到JS社区的热情和推进力,相信JS发展的将来是美好的

目录

  1. 最古老的模块加载<script>标签
  2. 此方法的若干问题
  • 全局变量。全局命名污染和命名冲突
  • 依赖管理。都须要HTML管理,而不是分层管理依赖,多文件加载次序很是关键
  • 效率。太多HTTP请求,和并行加载效率低下
  1. 有问题引起的解决方法
  • 命令空间,匿名闭包、依赖引入
  1. 当前主流的模块技术
  • Nodejs的作法,Commonjs方案
  • Nodejs借鉴
  • Require.js实践,AMD和CMD,依赖就近原则
  • 从手写模块,到自动编译,Browerify,Webpack,Rollup
  1. 刚刚落地的模块技术
  • ES6模块,官方发力,对现有技术的影响
  • 弥补ES6问题,HTTP/2
  1. 最佳实践

从脚本加载开始

一切从Javascript的加载开始,自有Javascript依赖,第一个加载模块的方案就是使用HTML标签,也就是<script>标签。也就是说,Javascript自己根本就没有模块和加载的定义,它是利用HTML来完成本该本身作的工做。node

这是初学者遇到的第一个使人困惑的问题。这样的语言,根本就是一个玩具!也许有些尴尬,可是现实就是如此。而且如此的加载方案,在稍微大点的工程中,会遇到几个违反常识的问题:webpack

  1. 全局命名污染。就是说每个被加载的模块都会引入新的全局变量,他们会污染全局空间,并且必须当心命名,避免名字的冲突
  2. 依赖关系单一。此种加载方式,必须按照依赖关系排序,依赖性最大的模块必定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
  3. 加载和执行效率难以细颗粒度的调优。一个个的按依赖次序加载和执行。虽然加载每每是能够并行的,可是执行时串行的。加载的时候,浏览器会中止网页渲染,加载文件越多,网页失去响应的时间就会越长

仍是从一个案例开始。这个案例,不会作任何有意义的工做,也不会作什么功能演示,而只是验证古典的Javascript加载的能力和限定,验证这些问题的存在,进而找到解决问题的方法。git

假设咱们如今有一个主程序,它在index.html内,一个模块dep1,一个模块dep2,依赖关系是index.html依赖dep1,dep1依赖dep2。代码都不复杂,就是直接列表以下:es6

文件index.htmlgithub

// index.html
<script src="dep2.js"></script>
<script src="dep1.js"></script>
<script type="text/javascript">
	console.log(dep1())
</script>
复制代码

文件dep1.js

var v1 = "dep1"
function dep1(){
	return v1+"-"+dep2()
}
复制代码

文件dep2.js

var v2 = "dep2"
function dep2(){
	return v2
}
复制代码

当使用浏览器加载index.html文件时,如我所愿,它会随后加载dep2,dep1,并调用函数dep1,后者调用dep2,而后在控制台输出:

dep1+dep2
复制代码

功能是有效的,依赖关系是是对的,输出也是如指望的。可是它也带来了额外的问题:

  1. 全局变量污染。在本案例中,能够在console内验证,发现变量v1,v2,函数dep1,dep2都是全局变量。可是因为script的加载机制,以及当前采用的Javascript函数和变量的定义不是局部化的,致使了这样的问题。
  2. 依赖关系并不严密。事实上,dep2内的引入变量和函数,只有dep1看获得便可,无需导入到全局变量内。
  3. 加载和执行效率难以细颗粒度的调优。本例内,dep1依赖dep2,它们被并行转入,可是执行必须是串行的,首先执行dep2,而后执行dep1,在此案例中,这样作是合适的,可是有很多代码模块之间并不存在依赖关系,它们彻底可能并发装入并发执行,可是使用script装入是不能如此的,它会按照标签的次序一个个的执行。若是有比较好的指定依赖关系的方法就行了。

讨论到此,我感受我在重复先辈们的话,实际上1960年代,第一届软件工程会议,就提出了模块化编程的概念,而且在以后多年一直努力的批评全局变量和Goto语句了。有时候,你会发现,这样看起来很是不济的语言,却能够在现实的项目中如鱼得水,发展的很是的好。而软件工程思想指导下的一些名流语言却早早夭折。这是另一个有趣的话题了,或许之后有机会谈到。

后端的借鉴

后端Nodejs干净利索的解决了此问题。作法就是对每个装入的模块都注入一个require函数和一个exports对象。其中require函数能够被模块用来引入其余模块,而exports对象则被用来引出当前模块的功能接口。仍是之前文提到的做为案例,作法就是:

文件index.js

// index.js
var d = require('./dep1')
console.log(d.dep1())
复制代码

文件dep1.js

var d = require('./dep2')
var v1 = "dep1"
function dep1(){
	return v1+"-"+d.dep2()
}
exports.dep1 = dep1
复制代码

文件dep2.js

var v2 = "dep2"
function dep2(){
	return v2
}
exports.dep2 = dep2
复制代码

执行命令:

$ node index.js 
dep1-dep2
复制代码

这里有一点变化,就是在nodejs内使用index.js代替了index.html。能够看到:

  1. Nodejs提供了很好的局部化定义变量和函数的能力,若是使用exports声明引出,其余模块看不到本模块的定义。好比v2变量没有声明引出,固然实际上在本案例内原本也没必要引出,那么在dep1内并不会看到v2变量。相似的v1也不会出如今index.js内。
  2. Nodejs提供了更加细粒度的依赖关系。index.js依赖dep1,可是并不依赖于dep2,那么index.js就只要引入dep1,而没必要同时引入dep2。这样的依赖关系,更加符合实际工程代码的需求,而不是一股脑的、不分层次的引入所有须要用到的代码。

在传统的服务器开发的诸多语言中,模块都是最基础也是最必备的,像是JavaScript连个内置模块支持都没有的是不常见的(或者说根本没有?)。使用诸如的require和exports,就在后端干净利索的解决了困恼前端的模块问题,这难免让前端以为应该效仿之。固然,Nodejs加载模块是同步的,这个是不能在前端效仿的。由于后端从磁盘加载代码,速度根本不是问题,而前端加载的都是从网络上进行的, 若是同步的话,加上Javas自己的单线程限定,整个UI就会由于加载代码而被卡死的。对比下二者的速度差别,你就明白了:

硬盘 I/O		
-----------------
HDD:	100 MB/s	
SSD:	600 MB/s	
SATA-III:	6000 Mb/s	
-----------------
网速 I/O
ADSL:	4 Mb/s
4G:	100 Mb/s
Fiber:	100 Mb/s
复制代码

借鉴后的样子,先看看Modules/Async规范

思路倒也简单,只要本身编写一个库,有它来异步加载其余模块,并在加载时注入须要的require和exports便可。这方面的库有几个,好比requirejs,sea.js等。由于咱们只是为了讲清楚概念和思路,所以会那概念上最清晰,和Nodejs最为一致的库来讲明问题,并不会由于那个更加主流而去选择它。从这个标准看,sea.js是说明概念问题的最佳模块装入库。

sea.js 是一个模块加载器,模块加载器须要实现两个基本功能:

  1. 实现模块定义规范
  2. 加载运行符合规范的模块

核心落脚点,就在规范二字上。sea.js要求模块编写必须在真正的代码以外套上一层规定的代码包装,样子看起来是这样的:

define(function(require, exports, module) {
    // 模块代码
});
复制代码

经过传递一个签名为function(require, exports, module)的回调函数给define函数,就能够把须要注入的变量和函数注入到模块代码内。以前的实例代码,在这里写成:

文件index.js

// index.js
define(function(require, exports, module) {
	var d = require('./dep1')
	console.log(d.dep1())
});
复制代码

文件dep1.js

define(function(require, exports, module) {
	var d = require('./dep2')
	var v1 = "dep1"
	function dep1(){
		return v1+"-"+d.dep2()
	}
	exports.dep1 = dep1
});
复制代码

文件dep2.js

define(function(require, exports, module) {
	var v2 = "dep2"
	function dep2(){
		return v2
	}
	exports.dep2 = dep2
});
复制代码

除了加上一层有点看起来莫名其妙的外套代码,其余的模块代码,你该怎么写就怎么写。假若不是那么洁癖,这样的代码确实解决了以前使用script标签加载代码带来的全局变量污染等问题,而且仍是能够异步加载的,那些看起来不错的依赖关系,也如Nodejs同样。以上代码,能够直接把nodejs对应的代码拷贝过来,加上外套便可运行。

咱们不妨加入seajs文件,来看看实际的使用效果:

//index.html
<script type="text/javascript" src="https://cdn.bootcss.com/seajs/3.0.2/sea.js"></script>
<script>
  seajs.use('./index.js');
</script>
复制代码

这里为了偷懒,我使用了seajs的CDN文件。若是有遇到什么问题,你不妨本身下载一个seajs文件,而后改为你的URL便可。

加载此HTML文件,能够在控制台看到输出:

dep1+dep2
复制代码

说明seajs执行效果不错!

  1. seajs经过use方法来加载入口模块,可选接收一个回调函数,当模块加载完成会调用此回调函数,并传入对应的模块做为参数
  2. 来获取到模块后,等待模块(包括模块依赖的模块)加载完成会调用回调函数。
  3. 分析模块的依赖,按依赖关系递归执行document.createElement(‘script’),这些标签的建立会致使浏览器加载对应的脚本

对模块的价值,都是异步加载,浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

能够在控制台输入:

seajs.data.fetchedList
复制代码

查看文件加载清单。

由于不是语言自带,而是社区经过现有的语言特性,硬造出来的一个模块系统,由于看起来代码难免累赘。可是在没有原生模块的状况下,这样作确实是管用的。要知道真正的原生模块,在ES6标准以后才出现,这都是2015年的事儿了。在一些有名的应用如Gmail、Google Map的推进下,Web从简单的展现到App的变化,迫切须要这样相似的模块技术,你们等不了那么久,先弄一个能用的是很重要的。

为何要套这层外壳呢?就是为了解决全局变量污染问题。在JavaScript语言内,惟一提供本地做用域的就是函数和闭包,经过闭包function(require, exports, module),模块加载器给模块注入了必要的函数和变量。看起来在模块以内的任何地方均可以使用require和exports,可是他没都不是全局变量,而是闭包内变量。这些变量都是局部化的,绝对不会污染全局空间。

使用require函数,能够就近指定对其余模块的依赖,函数自己是由sea.js这样的模块加载器提供,它会内部构造依赖关系图谱,并根据依赖关系,设置加入script标签的次序。

更加详细的理解这层外壳,能够阅读seajs源代码,代码量并不大,值得一读。或者看看此问答

固然Seajs也引入了本身的规范,叫作CMD规范。它的前身是Modules/Wrappings规范。SeaJS更多地来自 Modules/2.0 的观点,同时借鉴了 RequireJS 的很多东西,好比将Modules/Wrappings规范里的 module.declare改成define等。 说是规范,却不像是通常的规范那么冗长,可能打印出来也就一两页的纸张而已,这也是JavaScript社区的一个特色吧。Modules/Wrappings

seajs的做者在一篇文章中提到了业界在开发前端模块加载器时的场景:

大概 09 年 - 10 年期间,CommonJS 社区大牛云集。CommonJS 原来叫 ServerJS,推出 Modules/1.0 规范后,在 Node.js 等环境下取得了很不错的实践。09年下半年这帮充满干劲的小伙子们想把 ServerJS 的成功经验进一步推广到浏览器端,因而将社区更名叫 CommonJS,同时激烈争论 Modules 的下一版规范。分歧和冲突由此诞生,逐步造成了三大流派:

  1. Modules/1.x 流派。这个观点以为 1.x 规范已经够用,只要移植到浏览器端就好。要作的是新增 Modules/Transport 规范,即在浏览器上运行前,先经过转换工具将模块转换为符合 Transport 规范的代码。主流表明是服务端的开发人员。如今值得关注的有两个实现:愈来愈火的 component 和走在前沿的 es6 module transpiler。
  2. Modules/Async 流派。这个观点以为浏览器有自身的特征,不该该直接用 Modules/1.x 规范。这个观点下的典型表明是 AMD 规范及其实现 RequireJS。
  3. Modules/2.0 流派。这个观点以为浏览器有自身的特征,不该该直接用 Modules/1.x 规范,但应该尽量与 Modules/1.x 规范保持一致。这个观点下的典型表明是 BravoJS 和 FlyScript 的做者。BravoJS 做者对 CommonJS 的社区的贡献很大,这份 Modules/2.0-draft 规范花了不少心思。FlyScript 的做者提出了 Modules/Wrappings 规范,这规范是 CMD 规范的前身。惋惜的是 BravoJS 太学院派,FlyScript 后来作了自我阉割,将整个网站(flyscript.org)下线了。这个故事有点悲壮,下文细说。

也谈谈require.js

这个模块加载器是更加主流的。之因此不是首先提到它,是由于概念上来讲seajs更加简明。和seajs相比,requirejs是更加主流的框架。它的差别主要是一些零零散散的不一样,好比模块代码的外套是不太同样的:

require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
复制代码

    // some code here });

导出模块变量和函数的方式,也是不一样的。requirejs的引出方式是直接返回:

return {foo:foo}
复制代码

同样的案例,使用requirejs的话,代码是这样的:

index.html文件

<script data-main="index"
 src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js" ></script>
复制代码

index.js文件:

require(['./dep1'], function (d){
	console.log(d.dep1())		
});
复制代码

dep1.js文件:

define(['./dep2'], function (d){
	var v1 = "dep1"
	function dep1(){
		return v1+"-"+d.dep2()
	}
	return {dep1:dep1}
});
复制代码

dep2.js文件:

define(function() {
	var v2 = "dep2"
	function dep2(){
		return v2
	}
	return {dep2:dep2}
});
复制代码

浏览器打开文件index.html,能够看到控制台输出同样的结果。

稍加对比require.js和sea.js。使用Require.js,默认推荐的模块格式是:

define(['a','b'], function(a, b) {
  // do sth
})
复制代码

使用seajs的时候,相似的功能,代码这样写:

define(function(require, exports, module) {
  var a = require('a')
  var b = require('b')
  // do sth
  ...
})
复制代码

Seajs的作法是更加现代的。我须要用的时候,我才去引用它,而不是实现什么都引用好,而后用的时候直接用就好。

Modules/1.x规范

以require.js为表明的Modules/Async流派,尊重了浏览器的特殊性,代价是无论写什么模块,都得本身给本身穿上一层外套,对于有代码洁癖的人来讲,这样的状况是看不下去的。最好是开发人员编写干干净净的模块代码,框架开发者作一个工具,这个工具自动的把这些代码转义成客户端承认的异步代码。即在浏览器上运行前,先经过转换工具将模块转换为符合规范的代码。这就是Modules/1.x 流派的作法。须要注意的是1.x和2.0还有Async流派不能简单的认为版本号大的就更好。却是理解成各自不一样的解决方案为好。

以咱们本身的案例来讲,就是能够直接把nodejs代码那里,使用一个工具作一个转换,便可获得符合前端须要的代码,这些代码是异步加载的、是能够保证模块变量局部化的、是能够由良好的依赖关系定义的。工具browerfy就是作这个的。咱们来试试具体是怎么玩的。

首先安装此工具:

npm install --global browserify
复制代码

到你的nodejs代码内,而后转换此代码,生成一个新的js文件,通常命名为bundle.js:

browserify index.js -o bundle.js
复制代码

而后建立index.html并引入bundle.js:

<script type="text/javascript" src="./bundle.js"></script>
复制代码

使用浏览器打开此HTML文件,能够在控制台看到熟悉的输出,这说明转换是有效的:

dep1+dep2
复制代码

自己nodejs的代码,是不能在浏览器执行的,浏览器内也没有什么require函数,可是转换后就能够执行了。那么,转换的过程,到底玩了什么魔术?

像是browserify这样的工具,就是找到全面被引入的代码,解析它的依赖关系,而且自动的加入咱们在requirejs里面须要的外套代码。尽管bundle.js文件并非为了阅读优化的,可是能够取出其中的代码片断来证明咱们的观点:

{"./dep2":2}],2:[function(require,module,exports){
		var v2 = "dep2"
		function dep2(){
			return v2
		}
		exports.dep2 = dep2
},{}],3:[function(require,module,exports){
		var d = require('./dep1')
		console.log(d.dep1())
},{"./dep1":1}]},{},[3]);
复制代码

咱们能够看到原本的nodejs代码,以及它们对应的外套。仍是比较简单,就不进一步解释了。browserify不但完成了加外套代码的工做,还同时把若干小文件打成一个大的文件,对于当前使用的HTTP主流版本1.1来讲,这样作会让加载效率更高。可是对于HTTP/2.0来讲,它已经支持了多个文件在一个链接内交错传递,所以再作打包的意义就不大了。只是...HTTP/2.0的普及还须要时日。

browerify完成的工做简明而单一。另一个主流的同类工具叫作webpack,不但能够转换js代码,还能够打包css文件、图片文件,而且能够作一些工程化的管理,代价就是webpack学起来也困难的多。实际上像是Vuejs这样的UI开发框架,内部就是使用了webpack作工程化管理和代码转译的。可是在模块化方面,二者是差很少的。就不另外介绍了。

ES6 Module

时间到了May 9, 2018,我看到了阮一峰发布了这样的微博:

今天 Firefox 60发布,默认打开了ES6 模块支持,至此全部浏览器都默认支持ES6模块。前端开发模式可能所以大变。如今的方案是全部模块发到npm,本地写好入口文件,再用webpack打包成一个脚本。可是若是浏览器原生支持,为何还要打包呢?至少简单的应用能够直接加载入口文件,浏览器本身去抓取依赖。 ​​​​
复制代码

这里全部浏览器指的是Edge、Firefox、Chrome、Safari。固然,再一次没有IE。若是想要支持IE或者比较老的版本的话,仍是须要使用打包器来完成代码的转译。另外不少人表示会继续使用Webpack,缘由很简单,Webpack不只仅是完成模块打包工做,还有压缩、混淆等,而且不少框架还须要依赖它。因此迁移并不是一朝一夕之功。而无需考虑老版本浏览器的兼容的代码,是彻底能够大量的使用它了。了不得在把Webpack加起来转换ES Module到加外套的代码就是了。

ES6 Module不是requirejs那样加外套的样子,也不是Nodejs使用require函数的样子,而是另一套有官方提出的模块模式。它使用的是import、export关键字。官方的就是不同,社区是加不了关键字的。一样的案例,使用ES6 Module就是这样的了。

index.html文件:

<script type="module">
		import {dep1 } from './dep1.js'
		console.log(dep1())
</script>
复制代码

dep1.js文件

import {dep2} from './dep2.js'
	var v1 = "dep1"
	export function dep1(){
		return v1+"-"+dep2()
	}
复制代码

dep2.js文件:

var v2 = "dep2"
	export function dep2(){
		return v2
	}
复制代码

ES6 Module要求必须有后台的HTTP服务器,而不能直接在文件系统上完成Demo的测试。所幸使用Nodejs搭建一个服务器也很是简单直接:

npm i http-server -g
http-server
复制代码

在浏览器内访问此HTML文件的URL,能够看到控制台输出:dep1+dep2。这个输出,已是你的老朋友了。

Nodejs在10.9才支持实验版本的ES6 Module,是落后了点,可是对于Nodejs来讲,新的模块技术原本也就并不迫切。

最佳实践建议

综合以上的内容,我认为,在没必要考虑古老的浏览器兼容的状况下,最好的实践是这样的:

  1. 直接使用ES6 Module编写模块代码
  2. 使用Rollup清除没有调用的代码,下降代码的大小
  3. 使用Ugly工具压缩和混淆代码
  4. 使用HTTP/2作网络传递协议

这样的实践,会随着HTTP/2的逐步普及和ES6被更多的开发者采用,而成为更好的选择。

使用ES6 Module的坏处是没法像require那样动态的加载。可是好处是能够精确指明对于一个库,咱们使用的是那些,这就给工具提供了优化的可能,就是说若是我引入了一个库,可是这个库内有些我不会用的,那么这些不会被用到的代码也不会加载到前端了。这个功能对于后端来讲意义不大,可是对于前端来讲,就是很是使人喜欢的功能了。实际上,这样的工具已经有了,比较知名的就是rollup,它属于了一种被称为tree-shaking的技术优化使用代码。

而以往作模块打包,不少的缘由是HTTP/1.1传递大量小文件的时候开销比较大,而打包成单一的问题,就能够更好的利用HTTP/1.1的传输特性。可是HTTP/2.0的一个大的特点就是能够在单一的链接内,并发和交错的传递多个流,由于在一个链接内交错的传递多个文件,就能够再也不有HTTP/1.1的链接开销了。所以,在HTTP/2.0被采纳的网络里面,打包单一文件的价值几乎没有了。直接使用小文件默认状况下就能够获得比较好的优化传输。

按照如今的技术发展的势头,要不了几年,打包器将再也不那么必要,使用原生代码编写模块将会成为主流的。

参考

参考文章很多,其中模块历史和选型以下:

  1. 前端模块化开发那点历史
  2. 梳理的仍是比较清晰
  3. 有点黑客精神的小伙伴,玩的很广谱
  4. 介绍Bower
  5. npm for Beginners: A Guide for Front-end Developers
  6. Es6module 出来了,是否应该从新考虑打包的方案?

将来

这篇文章预计想要编写的YUI方法,YUI Combo方法,想了想仍是算了,由于这样的恐龙代码,已经在平常的代码实践中逐步消失,做为一个曾经比较重要,如今则退居二线的代码库,对它最好的赞许就是让它退休,也没必要给读者增长额外的阅读负担了。毕竟require.js、browerify、webpack都工做的不错,在此基础上发展的Vuejs、React.js也的获得了更多的承认。

本文讲到的模块规范和实践工具,为编写一个广为社区承认的模块起到了最基础的规范做用。可是,JavaScript社区最为使人称道的就是代码库仓库。包括NPM仓库,Bower仓库。在这些仓库内,有模块依赖管理工具,还有工程化工具。这些内容,它们固然是重要的,不在本文的范围内。

做为前端开发者,有人采用Bower管理组件依赖,也有人使用Npm作相似的工做。有不少时候,这样的实践是使人困惑的。还有这里npm and the front end,NPM官方也对npm在前端的使用,提出了[本身的见解][blog.npmjs.org/post/101775…]。

这些未尽的内容,或许在将来的文章中表达之。

相关文章
相关标签/搜索