JavaScript模块化历程


现代前端,模块化已成必备,不只有多种东西类的库,还被写进标准傍边,不论巨细公司,代码都遵循模块化思想在开发,它现已成为独立于技能的一种经历和才能。

本文咱们就来聊聊这个论题。

需求

网页刚出现的时分,只是很简略的文档,样式简略,极少的交互,极少的规划元素,一个页面不会依靠许多文件,逻辑代码少。

但随着Web技能的开展,网页变得越来越丰厚,于用户来说是绝对{ * 8 m ( T B g的福音,但对开发者来说,问题逐渐凸显。

  • 代码多,命名抵触概率大
  • 代码冗余,e ] ? A V n g恳求过多拖慢速度
  • 文件间依k Q D 3 P e 1靠增多,易出现引证c D 5 Y S u [过错,导致代码运转出错
  • 难复用,重写?copy一份?
  • 修改或者改版时,要去成百上千行代码里找,难维护

综上所述,不论是从开发仍是体会角度,都需求处理计划。

模块

无数实践证明,小{ Q c的、安排良好的代码远比庞大的代码更易了解和维护。因此,优化程序的结构和安排办法,把它们分红小的、l o O d耦合相对松散的片段,不失为更明智的做法,这些片段就称为模块。

还好,JavaScript有一种原生的安排办法——函数。

函数

函数用来做什么?封装具备必定功用的代码,它里边能够包裹一切类型的东西,且有自己独立的作用域,然后K e p 8 P D _ S在需求的当地调用即可。比方:

function f1(){
//0 7 L ^ 6 T C 7..H ; F O.
}
fun$ X S ? /cI z 1 0 2 R W ytion f2(){
//...
}
function f3() {
f1();
f2();
}

这样做功用明了B F { L . n r N,完成了代码的清晰安排和别离,但是它们散落在整个文件傍边,或许污染全7 e r y局命名空间,且维护本钱较高,持续探究。

目标

目标能够有属性,而属性既能够是数据,也能够是办法,这就能够很好地满足需求,并且M . 6 ( 9 3 ! Y 4目标的属J c R a G V性经过目标来拜访,相当于设定了一个命名空间。

let myModule = {
name: '张三',
getName() {
console.logy * s (this.name);
}
}

这么一来,在必定程度上处理了命名的问题,但是它的属性仍然是露出的,能够被外部~ x D /更改。比方:

myModuleb ~ L u : u B.name = '李四';
myModule.getName() // ‘李四’

仍是不行理想。

匿名` c .闭包

这种办法在项目中有不少运用,是利用了闭包的特性——私有数据和共享办法,关于这一点,咱们在闭包中有提及。

代码如下:

//module.js
(function, q ? 5(window) {
let nF 3 M P V R d x .ame = 'idea'
//操作数据的函数
function getName() {
c? k $ 8 sonsole.log(`${Z U _ W h 7 Tname}`)
}
//露出办法
window.myMr ` ~ # 4  zodule = { getName }
})(window)

这个时分,能够经过my1 0 3 y N # N ]Module.getName()来获取name,但无法经过myModule.name拜访name

myModule.getName() // "idea"
my? s d r L G b + BModule.name  // undefined

看起来很不错,一起处理了两个问题,但假如这个模块需求依靠另一个模块呢?

也有办法,别忘了,匿名函数也是函数,能够传参~

//module.js
(fW } a 4 5 U nunction(window,$) {
let name = 'idea'
//操作数据的函数
function getName()a % I h * q B f {
consov * C & 0 b hle.log(`${name}`);
$('body@ I G } r b').css(: t j ! 8'color', 'red');
}
//露出办法
window.myModule = { getName }
})B ? G q N(window,jQuery)

当然,这儿的jQuery是另一个当地界说好的模块,经过这种办法引进,就能够在d n t ! bmyModu` Q + t a Q ~le内部运用,并且这便是现代模块完成的思想来历,只是办法不同。

说了上面那么多,模块的完成也有了一些改动和优化o d t ] D D c a,但还有一个东西是没有变的,文件的安排办法,像上面的代码,在页面中会. Q是这样:

<script type="text & ? ? 3 ] s k 9/javascript" src="https://juejin.im/post/5ea56d2451882573 R r 0 J883be5ae/jquery-1.10.1.js"></script>y v r u
<script type="text/javasct 6 Bript" src="https://juejin.im/post/5ea56d2451882573883be5ae/modul?  Z Ke.js"></script>
<script type="text/javascript">
myMoa k n A e U = 9dule.getName()
</script>

咱们处理了命名抵触,数据维护,和引进依靠,但没有处理依靠与文件加载次序的强相关性,~ C d r T p Z z :别的还存在恳求过多的问题。

所以– @ $ ~ 3 v # +,仍然需求更好的计划来处理这些问题。

模块化标准

上面都是开发者运用语言本身的特性+ 1 c % A 4 [不断摸索出来的成果o k 1 ,,各有用武之地,但又都存在不足。

接下来咱们介绍几个运用较广的、更理想的模块化标准( j [ E ~ } q

CommonJS

CommonJS标准中,每个文件便是一个模块,有自己的作用域,模块的变量、函数、类,都是q U f ~私有的,外部不可见。

// num.js
var a = 5;
var b = 3;
var add = f) 8 | Yunction (a,b) {
return a + b;
};

既然数据受维护,想用怎样办?——主动露出。n 6 T Z

module.exports.a = a;
module.exports.Z j P a | z Kb = b;
module.exports.add = add;

module.exports便是露出的办法。

另一个文j ~ K k f L (件中需求这样引证:

//index.js
var num = require('./num.js');  // ./代表相对路径
conson # dle.log(num.a); // 5
console.log(num.b); /A I : m 4 t E/ 5
console.log(K 1  C s 3num.add(a,b)); // 8

require指令担任读入并执行一个JavaScript文件z v m,并回来该模块的export0 O m G r ! b : =s目标,假如S a _ ~ | Y ! ? x没有找到会报错。

CommonJS的特点可总! w ^ q V ( R结如下:

  • 一切代码运转在模块内部,不污染全局。
  • 模块能够屡次加载,但只会在首次加载时运转一次,结果被缓存,再次加载直接读* u X ~ 4 8 P 4取缓存,想让模块再次运转,有必要铲除缓存。
  • 模块的加载次序,按照代码出现的次序。

关于Comy O , } , monJS,多数人或许知道怎样用,却不知道原理是什么,它看起来跟一般的js文件相同,差异在哪?

简略分析一下:

首先,像许多Web东西相同,它依托于Node,Node内部供给一个Module构造函数,一切模块都是Module的实例。

function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...

每个模块内部,都有一O @ { 5 o c R k个module目标,该目标有以下几个常见属性:

  • module.id 识别符,通常是带有绝对路径的模块文件@ , q : A
  • module.parent 回来一个目标,表明调用该模块的模块。
  • module.children 回来一个数组,表明该模块要用到的其他模块。
  • module.exports 表明模块对外输出的Z $ y值。

这么看,应该知道它跟一般js仍是有差异,只是这种差异是无感知的,对开发非常友好了– # D ` 5 A T

CommonJS很好地处理了之前提出的一切问题,但还有一点瑕疵,它是同步的,在Node服务端运用傍边,模块一般存在本地,加载较快,同步问题不大,在浏览器中就不合适了,所以还b S s – ^ ( W }需求异步模块化计划。

AMD

AMD(异步模块界说)是为浏览器环境! C 4 T 0规划的,它界说了一套异步加载标准来处理同步的问题。

语法如Q ! ( 1 4 G J i 8下:

define(id, dependencies, factz  o w V  D Eory)o m S f P / Y %;
  • id 是模块的名字,字符串,可选。
  • dependencies 是依靠模块列表,数组,可选。
  • factory 包裹了模块的具体完成,是“函数”或者“目标”,假如是函数,回来值便是模块的输出接口或者值。

看个示例:

有模块依靠

//  界说
define('myModule', ['jquery'], function($) {
// $ 是 jquery 模块的输] } 7
$('body').d g ; Nt, 4 D j Q 3 M Zext$ C Z C 2 + 4 q('hello world');
});
// 运用
require(['myModule'], function(m8 2 , + ? [ |yModule) {});

模块输出

define(['jquery'],a z q J = Y function($) {
var HelloWor; F 9 A 2ld = function(selectorL l E){
$(selector).text('hello world');
}U V 2 a I ,;
// HelloWoN c Jrld 是该模块输出的对外接口
return HelloWorld;
});

RequireJS

RequireJS是一个恪守AMD标准的东西库,用于客户端的模块管理。它经过defi? ? v x K N R W One办法,将代码界说为模块;经过requif x 8 & t B C h 6rV – d ce办法,完成代码的模块加载,运用时需求下载和导入项目。

//无依靠模块
// msg.js
define(f- | W , R 3 { G gunction() {
let msg = 'www.baidu.com'
function getMsg() {
return msg.toUpperCase()
}
return { getMsg } // 露出模块
})
//有依靠模块
// showMsg.js
define(['msg'], function(getMsg) {
let name = 'idea'
function showMsg() {
alert(msg.getMsg() + name)
}
return { showMsg } // 露出模块
})

运用的时分只需求像下面这样:

<!-- 引进reI S # : N K $quire.js并指定js主文件的进口 -->
<script data-main="js/main" src="js/libs/require.js"></script&gs } v , S # ?t;

CMD

CMD标准可说是站在巨人的肩膀上,它整合了CommonJS和AMD标准的特点,专门用于浏览器模块异步加载。

直接看代码:

//无依靠
define(function(require, exports, module)I ) f{
exports.xxx =I D R T 2 : A value
module.exports = value
}k I _)

exports 参数是5 ? X L ] | module.exports 目标的一个引证。只经过 exports参数来供给接口有时无法满足开发者的一切需求。 比方当模块的接口是某个类的实例时,需求经过 module.exports来完成。

//有依靠
define(a # : J 1 O ) ) mfunction(r! K } 1 N $equire, exports, module){
//同步引进
var mod- N Iule = require('./module')
//异步引进
require.async('./module', function (module) {
})
//条件引进
if (status) {
var x = requie('./x');
}
//露出模块
exports.xxx = value
})

相比之下,CMD标准推重代码责任更单一,且没有全局require,看起来,头部较轻,所需模块就近引进。

SeaJS

CMD标准的集大成R R 8 $者是SeaJS,笔者之前地点的组便是运用SeaJS作为模块加载器。它的运用同Requ& i Y P g 6 FireJS相似,需q E f 1 5 l G 2 8求下载和引进页面。

<script type="text/javascript" src="https://, Q d x N O v . 4juejin.im/post/5ea56d2451882573883be5ae/js/libs/sea.js"></script>
<script type="; x r { , @ , Stext/javascript">
seajs.use('./js/modules/main')
</G [ 0 O + W @ gscript>

看完以上几个计划t n 8,是时分介绍原生完成了。

ES6模块

ES6之前没有专门的模块机制,为供5 O _ I S x V & D给这些才能,ES6引进两个关键字 W ; E # Q a a s

  • export——设定模块的对外接口。
  • import——将其他模块的功用导入。

它规划思想是尽量静态化,使得编译时就能确认模块的依靠联系以及输入和输出的变量。

/** math.js **/
var basic = 0;
var add = function (a, b) {
return a + b;
};
export { basic, add };
/** 引证模块 **/
import { basic, add } from './math';
function test(ele) {
ele.t~ c l D p b / -extContent = add(66 + basa # ] Y A - ic);
}

导入和导D 4 { v l l ! f ?出都既能够是独自的,也能够是集合方式,此处只列集合方式。

除此之外,它还供给了一些不相同的东西:

  • as
//D z q name.js
let myName = "i1 X }dea";
export { myName as exportName }  // 别号导出
impoo + 1rt { exportName as name }V B | x 3 u ` from "~ S e . 3 { $ |./name.js";  // 别号导入
console.log(name);  //  idea

它供给了更改模块导出或者导入接口称号的才能,但称号之间有必要是一一对应的联系。

  • export default
// xxx= u m M.js
var a = "My name is idea!";
export default a; // 只能有一个
// 能够运用任意变量接^ ` | } 9
import b from "./xxx.js";

这种称为默许导; n t /出,便是什么都不管,默许便是它,跟构f b I ? k K +建东西傍边的默许使命相似,直接跑便– O L ( , i H是。

不过它看起来便利,却或许引起一些潜在的问题,这些问题超s _ 3 @ 5 C出本文评论范围,信任大家能够以最好的办法去运用它们~

以上两种办法都非刚需,看各自的编码喜好和需求运用。

不论语法,从方式上看,ES6的模块跟ComH & 2 qmonJS很像,那它俩有什么差异?

  • CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引证。即CommonJS中的值的改动,对引证了它的模块没有影响l ^ y P u r x N了,ES6模块则仍然会有影响。

  • CommonJS模块是运转时加载,加载的是一个目标,. : P运转时才会生成;ESf ` T M a ~ @ V6模块是编译时输出接口,在代码的静态解析阶段就会生成。

  • 运转机制不同,ES6模块是动态引证,不会缓存值,模块变量绑定其地点的h P K L f i t 9模块。

不论怎样,模块化现已被参加标准,且看起来更简洁和直观,许多当地都能看到它的4 _ a =运用,其一是不需求再运用第三方东西,其二,即便浏览器没来得及支撑,咱们也有相似Babel的东西来辅助运用。条件允许的话,仍是能够愉快地用起来K g r U~

总结

内容现已很长,许多东西仍不是很具体,但信任你能够对模块化的开展有[ b R g V %个大概的了解,也能感遭到它的魅力d H $ T ! S $

按理说,对于任何一种东西或者工作办法,咱_ _ K ;们不必知道它是怎样来的,只需知道当下哪种最好用以及怎样用,就能够了,但对它们发生的背景和开展历史有所了解能让咱们愈加有的放矢,在没人替咱们做选择,或者遇到问题时,能有自己的判断和处理办法。

就聊到这儿了,不保证完全正确,欢迎沟通,下篇见!~

参阅:

前端模块化详解
AMD标准
《JavaScript忍者秘籍》等

博客链接:JavaScript模块化进程

找工作中,b5 T e V aase深圳,求勾搭~

发表评论

提供最优质的资源集合

立即查看 了解详情