Javascript 模块的演化历史一定程度上代表了前端的发展史。从早期的 对象字面量
、IIFE
到后来的 commonjs
, AMD
等, 再到如今的 ES Module
。这些模块化方案在互联网技术发展需求下不断革新,演进。
本文从四个阶段来讲述 JS 模块化的发展历程,旨在让大家了解 JS 模块化是如何发展到今天,每个模块方案在当时又解决了什么问题。
Javascript 早期诞生的目的用于客户端验证表单,提高用户体验。站在今天的解决方案角度去回顾,在那个无样式,无交互的,简单的不能再简单的 Web 页面,很难想 JS 的模块化意义在哪里。
如果非要达到一定程度代码复用,对象字面量
完全可以满足 Web 互联网早期的需求。
//Person.js
var Person = {
say: function(words) {
//code
},
run: function() {
//code
}
};
Person.say('say somthing');
Person.run();
历史总会进步,互联网上 Web 页面越来越多样化,好在人们会不断的根据变化的需求调整模块化的方式。
当团队相互合作,去完成某一个项目时,对象字面量
缺点就一览无遗。命名冲突、作用域隔离等问题就不可避免的会发生,只是一个时间早与晚的问题。
javascript 函数,拥有者天然的局部作用域,外界是访问不到函数内部的作用域。自然而然过渡到IIFE
模块化。
(function(global){
var Person = global.Person || {};
var pritiveFn = function(){
//other code
};
var pritiveName = 'Tom';
Person.say = function(words) {
pritiveFn();
console.log( pritiveName + 'say: ' + words);
//other code
}
Person.run = function() {
pritiveFn();
//other code
}
})(window);
Person.say();
Person.run();
这种模式,能任意定义不会被外界访问到局部变量,也不会污染全局作用域,同时还能访问全局中的一些变量。通过传参命名空间,可将模块挂在到全局 Person 命名空间上。
IIEF
的模块化方式,早已***到前端开发的基因。直到今天,在我们的日常开发中,都能见到或用到这种方式。
Web2.0时代的到来,网站应用更加注重用户与服务的双向交互,前端开发也逐渐承担更多的责任。一个网站,可能有成百上千的页面,而且,javascrpt 不局限于客户端。
推崇 commonjs
模块化规范的 Nodejs ,将模块化推向了一个新的高度。
// path/ModuleA.js
var ModuleA = function(){
//code
}
module.exports = ModuleA;
//-------------------------
// path/ModuleB.js
var ModuleB = function(){
//code
}
module.exports = ModuleB;
//------------------------
// path/index.js
var ModuleA = require('./path/ModuleA');
var ModuleB = require('./path/ModuleB');
ModuleA();
ModuleB();
commonjs
规范提供 module.exports
(或者 exports
)接口用于对外暴露模块。require
加载模块。
仔细想想,日常开发中我们理所应当只关心模块的自由导出和加载。而加载速度、依赖顺序、作用域隔离等问题应该交给框架或者其他科学技术来系统解决,让我们无感知。
但,nodejs 毕竟是运行在服务端的 javascript。
nodejs 中每个文件具有独立的作用域,所以每个文件可认为是一个模块。除非你显示的定义在全局 global 对象上,否则其他文件是访问不到该作用域的定义的任何数据。
在 nodejs 中,一个 js 文件拥有访问其他模块(文件)能力,这就很好的解决模块间相互依赖的问题。并且所有文件都是在服务器本地加载,速度极快。
但浏览器客户端的现状是残酷的。看下面例子,如果某个页面依赖Slider
, Dialog
, Tab
模块,而这三个模块又有一些自身的依赖。
<!-- 模块自身的依赖 -->
<script src="./util/Animation.js"></script>
<script src="./util/Mask.js"></script>
<!-- 模块依赖 -->
<script src="./Slider/index.js"></script>
<script src="./Dialog/index.js"></script>
<script src="./Tab/index.js"></script>
<script>
Slider();
Dialog();
Tab();
</script>
上面的例子可以看出:
browserify
,可将commonjs
规范移植到浏览器端,本质上。browserify
是将所有被依赖commonjs
的模块,打包到当前业务代码中。
浏览器中的 js,本身并无加载其他文件(模块)的接口。聪明的人们用动态创建 script 节点实现了动态加载模块。AMD
, 异步模块定义,采用的是异步加载模块方式。依赖模块是异步加载,不会阻塞页面的渲染。
AMD规范中最核心的接口是define
和require
,顾名思义:定义和加载模块。
其中以requirejs
代表,是AMD
规范的实现。
// 定义模块
define(['path/util/Animation'], function(Animation){
// Slider code
return Slider;
});
// 加载执行模块
require(['path/Slider'], function(Slider){
Slider();
})
可以看出,接口的第一个参数,代表模块的依赖路径。模块或业务的代码,放在 callback 中,其中 callback 参数提供暴露出了各依赖模块的接口。
此时,模块规范分成了commonjs
和AMD
两大阵营。天下大势分久必合,需要一种解决方案同时兼容这两种规范。而UMD
规范的诞生就是解决该问题。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 规范
define([], factory);
} else if (typeof exports === 'object') {
// commonjs 规范
module.exports = factory();
} else {
// 挂载到全局
root.globalVar = factory();
}
}(this, function () {
return {};
}));
从上面可以看出 UMD 是通过判断运行环境中是否存在各模块化的接口来实现的。
不管是 commonjs, AMD, UMD,都是毕竟是为了弥补 javascript 模块缺陷而衍生出的民间解决方案。2015年es6的发布,让javascript终于在语言层面上实现了模块化。
// path/ModuleA.js
var ModuleA = function(){
//code
}
exports default ModuleA;
//-------------------------
// path/ModuleB.js
var ModuleB = function(){
//code
}
exports default ModuleB;
//------------------------
// path/index.js
import ModuleA from './path/ModuleA';
import ModuleB from './path/ModuleB';
ModuleA();
ModuleB();
commonjs
已经发展很成熟,也能满足日常需求。初略看,es module “就像是语法糖”,我们为何还要去使用它呢,换句话说,我们是用它能为我们带来哪些收益?
不管是 commonjs
, AMD
,他们的模块架构是 “动态架构”,换句话说,模块依赖是在程序运行时才能确定。而es module
是 “静态架构”,也就是模块依赖在代码编译时就获取到。所以在 commonjs 里能进行 “动态引入” 模块。
if ( Math.random() > 0.5 ) {
require('./ModuleA');
} else {
require('./ModuleB');
}
而在 es module 中是无法进行类似操作的。从这个角度来看,es6 module 灵活性还不如 commonjs。但事物具有两面性。es6 module 其实能为我们带来以下几个收益。
tree shaking
在我们部署项目时,常常需要将各个模块文件打包成单个文件,以便浏览器一次性加载所有模块,减少 reqeust 数量。因为在 HTTP/1 中,浏览器 request 并发数量有限制。不过随之带来的问题是,多个模块打包成单文件,会造成文件 size 过大。
如果我们能在编译期时确定好模块依赖,就可以消除没有用到的模块,以便达到一定程度的优化,来看看下面例子。
// moduleA.js
export function moduleX(){
//some code
}
export function moduleY(){
//some code
}
// index.js
import { moduleX, moduleY } from './moduleA';
moduleX();
通过工具 Rollup, 可将 index.js 打包成如下代码:
'use strict';
function moduleX(){
//some code
}
moduleX();
可以看出,打包的代码只包含 moduleX,最大限度的减少了打包文件 size,这就是所谓的 'tree shaking', 读者可以好好品味下这个词,很传神。
模块变量静态检查
es6 module由于是“静态架构”,在编译时就能确定模块的依赖树以及确保模块一定是被正确的 import/export ,这就为项目质量带来很大的保障。看下面例子:
// module1.mjs
export function moduleX(){
console.log(1);
}
// index.mjs
// 注意:module1.mjs 中并没有 export 出 moduleY
import { moduleX, moduleY } from './module1.mjs';
moduleX();
//注意
let randomNum = Math.random();
if (randomNum) > 0.3 && randomNum < 0.4 ) {
moduleY();
}
如果没有静态检查,在上面代码中的条件判断得出,代码运行期间,执行 moduleY() 函数报错的概率是10%,这种风险在线上环境就是一个非常大的隐患,一旦命中条件判断,你一整年的绩效可能就都没了。
那如果有编译期间静态检查,会是怎样的结果?
运行 node --experimental-modules index.mjs
命令时,控制台会报错:
import { moduleX, moduleY } from './module1.mjs';
^^^^^^^
SyntaxError: The requested module does not provide an export named 'moduleY'
at ModuleJob._instantiate (internal/loader/ModuleJob.js:88:21)
at <anonymous>
这种编译时静态检查对项目的质量把控非常有用。
但 es6 module 有时候也让我很忧伤。因为它很“灵活”,所以给我带来了困扰。
来看看 import 语法:
再来看看 export 语法
额,其实我就想简单的 import/export 而已,“少即使多”。
农业革命是前端史的重大进步,社区各种模块化解决方案以及事实上的标准,从另一方面也推动着 Javascript 从语言层面对模块化进行支持。这为我们架构大型项目,保证项目质量提供了机会。
模块的兼容问题以及重复劳动应该交给工具去做,我们应该留出更多的时间享受”美好生活“。所以,涌现了一大批模块化工具以及周边的模块管理工具。如Browserify、r.js、Webpack、Rollup、 jspm、 npm、yarn等等。
各种工具极大的提高了我们的工作效率,也我们对模块化有了更多的选择。
快乐同时也带来很多的痛,就是因为可选择工具太多,配置太多,让你深陷其中无法自拔。要么忙着写bug, 要么忙着写配置。
科学革命的时代,还未到来。也许到那时候,模块化的使用就像var m = 1;语法一样,它在我们脑海里本应该就是理所当然的存在,而不需借助其他编译、运行等工具来实现。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。