共计 12229 个字符,预计需要花费 31 分钟才能阅读完成。
本文首发于 vivo 互联网技术 微信公众号
链接:https://mp.weixin.qq.com/s/15sedEuUVTsgyUm1lswrKA
作者:Morrain
一、前言
上一篇《前端科普系列(2):Node.js 换个角度看世界》,咱们聊了 Node.js 相干的货色,Node.js 能在诞生后火到如此一塌糊涂,离不开它成熟的模块化实现,Node.js 的模块化是在 CommonJS 标准的根底上实现的。那 CommonJS 又是什么呢?
先来看下,它在维基百科上的定义:
CommonJS 是一个我的项目,其指标是为 JavaScript 在网页浏览器之外创立模块约定。创立这个我的项目的次要起因是过后不足广泛可承受模式的 JavaScript 脚本模块单元,模块在与运行 JavaScript 脚本的惯例网页浏览器所提供的不同的环境下能够重复使用。
咱们晓得,很长一段时间 JavaScript 语言是没有模块化的概念的,直到 Node.js 的诞生, 把 JavaScript 语言带到服务端后,面对文件系统、网络、操作系统等等简单的业务场景,模块化就变得不可或缺 。于是 Node.js 和 CommonJS 标准就井水不犯河水、相映成辉,独特走入开发者的眼帘。
由此可见,CommonJS 最后是服务于服务端的,所以我说 CommonJS 不是前端,但它的载体是前端语言 JavaScript,为前面前端模块化的流行产生了深远的影响,奠定了牢固的根底。CommonJS:不是前端却反动了前端!
二、为什么须要模块化
1、没有模块化时,前端是什么样子
在之前的《Web:一路前行一路忘川》中,咱们提到过 JavaScript 诞生之初只是作为一个脚本语言来应用,做一些简略的表单校验等等。所以代码量很少,最开始都是间接写到 <script> 标签里,如下所示:
// index.html
<script>
var name = 'morrain'
var age = 18
</script>
随着业务进一步简单,Ajax 诞生当前,前端能做的事件越来越多,代码量飞速增长,开发者们开始把 JavaScript 写到独立的 js 文件中,与 html 文件解耦。像上面这样:
// index.html
<script src="./mine.js"></script>
// mine.js
var name = 'morrain'
var age = 18
再起初,更多的开发者参加进来,更多的 js 文件被引入进来:
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
var name = 'morrain'
var age = 18
// a.js
var name = 'lilei'
var age = 15
// b.js
var name = 'hanmeimei'
var age = 13
不难发现,问题曾经来了!JavaScript 在 ES6 之前是没有模块零碎,也没有关闭作用域的概念的,所以下面三个 js 文件里申明的变量都会存在于全局作用域中。不同的开发者保护不同的 js 文件,很难保障不和其它 js 文件抵触。全局变量净化开始成为开发者的噩梦。
2、模块化的原型
为了解决全局变量净化的问题,开发者开始应用命名空间的办法,既然命名会抵触,那就加上命名空间呗,如下所示:
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
app.mine = {}
app.mine.name = 'morrain'
app.mine.age = 18
// a.js
app.moduleA = {}
app.moduleA.name = 'lilei'
app.moduleA.age = 15
// b.js
app.moduleB = {}
app.moduleB.name = 'hanmeimei'
app.moduleB.age = 13
此时,曾经开始有模摸糊糊的模块化的概念,只不过是用命名空间实现的。这样在肯定水平上是解决了命名抵触的问题,b.js 模块的开发者,能够很不便的通过 app.moduleA.name 来取到模块 A 中的名字,然而也能够通过 app.moduleA.name = ‘rename’ 来任意改掉模块 A 中的名字,而这件事件,模块 A 却毫不知情!这显然是不被容许的。
聪慧的开发者又开始利用 JavaScript 语言的函数作用域,应用闭包的个性来解决下面的这一问题。
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
app.mine = (function(){
var name = 'morrain'
var age = 18
return {getName: function(){return name}
}
})()
// a.js
app.moduleA = (function(){
var name = 'lilei'
var age = 15
return {getName: function(){return name}
}
})()
// b.js
app.moduleB = (function(){
var name = 'hanmeimei'
var age = 13
return {getName: function(){return name}
}
})()
当初 b.js 模块能够通过
app.moduleA.getName() 来取到模块 A 的名字,然而各个模块的名字都保留在各自的函数外部,没有方法被其它模块更改。这样的设计,曾经有了模块化的影子,每个模块外部保护公有的货色,凋谢接口给其它模块应用,但仍然不够优雅,不够完满。譬如上例中,模块 B 能够取到模块 A 的货色,但模块 A 却取不到模块 B 的,因为下面这三个模块加载有先后顺序,相互依赖。当一个前端利用业务规模足够大后,这种依赖关系又变得异样难以保护。
综上所述,前端须要模块化,并且模块化不光要解决全局变量净化、数据保护的问题,还要很好的解决模块之间依赖关系的保护。
三、CommonJS 标准简介
既然 JavaScript 须要模块化来解决下面的问题,那就须要制订模块化的标准,CommonJS 就是解决下面问题的模块化标准,标准就是标准,没有为什么,就和编程语言的语法一样。咱们一起来看看。
1、CommonJS 概述
Node.js 利用由模块组成, 每个文件就是一个模块,有本人的作用域。在一个文件外面定义的变量、函数、类,都是公有的,对其余文件不可见。
// a.js
var name = 'morrain'
var age = 18
下面代码中,a.js 是 Node.js 利用中的一个模块,外面申明的变量 name 和 age 是 a.js 公有的,其它文件都拜访不到。
CommonJS 标准还规定,每个模块外部有两个变量能够应用,require 和 module。
require 用来加载某个模块
module 代表以后模块,是一个对象,保留了以后模块的信息。exports 是 module 上的一个属性,保留了以后模块要导出的接口或者变量,应用 require 加载的某个模块获取到的值就是那个模块应用 exports 导出的值
// a.js
var name = 'morrain'
var age = 18
module.exports.name = name
module.exports.getAge = function(){return age}
//b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge())// 18
2、CommonJS 之 exports
为了不便,Node.js 在实现 CommonJS 标准时,为每个模块提供一个 exports 的公有变量,指向 module.exports。你能够了解为 Node.js 在每个模块开始的中央,增加了如下这行代码。
var exports = module.exports
于是下面的代码也能够这样写:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){return age}
有一点要尤其留神,exports 是模块内的公有局部变量,它只是指向了 module.exports,所以间接对 exports 赋值是有效的,这样只是让 exports 不再指向 module.exports 了而已。
如下所示:
// a.js
var name = 'morrain'
var age = 18
exports = name
如果一个模块的对外接口,就是一个繁多的值,能够应用 module.exports 导出
// a.js
var name = 'morrain'
var age = 18
module.exports = name
3、CommonJS 之 require
require 命令的基本功能是,读入并执行一个 js 文件,而后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
第一次加载某个模块时,Node.js 会缓存该模块。当前再加载该模块,就间接从缓存取出该模块的 module.exports 属性返回了。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){return age}
// b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
a.name = 'rename'
var b = require('a.js')
console.log(b.name) // 'rename'
如上所示,第二次 require 模块 A 时,并没有从新加载并执行模块 A。而是间接返回了第一次 require 时的后果,也就是模块 A 的 module.exports。
还一点须要留神,CommonJS 模块的加载机制是,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块外部的变动就影响不到这个值。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){age = a}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
四、CommonJS 实现
理解 CommonJS 的标准后,不难发现咱们在写合乎 CommonJS 标准的模块时,无外乎就是应用了 require、exports、module 三个货色,而后一个 js 文件就是一个模块。如下所示:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function () {return age}
// b.js
var a = require('a.js')
console.log('a.name=', a.name)
console.log('a.age=', a.getAge())
var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {return age}
// index.js
var b = require('b.js')
console.log('b.name=',b.name)
如果咱们向一个立刻执行函数提供 require、exports、module 三个参数,模块代码放在这个立刻执行函数外面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:
(function(module, exports, require) {
// b.js
var a = require("a.js")
console.log('a.name=', a.name)
console.log('a.age=', a.getAge())
var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {return age}
})(module, module.exports, require)
晓得这个原理后,就很容易把合乎 CommonJS 模块标准的我的项目代码,转化为浏览器反对的代码。很多工具都是这么实现的,从入口模块开始,把所有依赖的模块都放到各自的函数中,把所有模块打包成一个能在浏览器中运行的 js 文件。譬如 Browserify、webpack 等等。
咱们以 webpack 为例,看看如何实现对 CommonJS 标准的反对。咱们应用 webpack 构建时,把各个模块的文件内容依照如下格局打包到一个 js 文件中,因为它是一个立刻执行的匿名函数,所以能够在浏览器间接运行。
// bundle.js
(function (modules) {// 模块治理的实现})({'a.js': function (module, exports, require) {// a.js 文件内容},
'b.js': function (module, exports, require) {// b.js 文件内容},
'index.js': function (module, exports, require) {// index.js 文件内容}
})
接下来,咱们须要依照 CommonJS 的标准,去实现模块治理的内容。首先咱们晓得,CommonJS 标准有阐明,加载过的模块会被缓存,所以须要一个对象来缓存曾经加载过的模块,而后须要一个 require 函数来加载模块,在加载时要生成一个 module,并且 module 上 要有一个 exports 属性,用来接管模块导出的内容。
// bundle.js
(function (modules) {
// 模块治理的实现
var installedModules = {}
/**
* 加载模块的业务逻辑实现
* @param {String} moduleName 要加载的模块名
*/
var require = function (moduleName) {
// 如果曾经加载过,就间接返回
if (installedModules[moduleName]) return installedModules[moduleName].exports
// 如果没有加载,就生成一个 module,并放到 installedModules
var module = installedModules[moduleName] = {
moduleName: moduleName,
exports: {}}
// 执行要加载的模块
modules[moduleName].call(modules.exports, module, module.exports, require)
return module.exports
}
return require('index.js')
})({'a.js': function (module, exports, require) {// a.js 文件内容},
'b.js': function (module, exports, require) {// b.js 文件内容},
'index.js': function (module, exports, require) {// index.js 文件内容}
})
能够看到,CommonJS 外围的标准,下面的实现中都满足了。非常简单,没想像的那么难。
五、其它前端模块化的计划
咱们对 CommonJS 的标准曾经十分相熟了,require 命令的基本功能是,读入并执行一个 js 文件,而后返回该模块的 exports 对象,这在服务端是可行的,因为服务端加载并执行一个文件的工夫生产是能够疏忽的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且胜利拿到了模块导出的值。
这种标准天生就不适用于浏览器,因为它是同步的。可想而知,浏览器端每加载一个文件,要发网络申请去取,如果网速慢,就十分耗时,浏览器就要始终等 require 返回,就会始终卡在那里,阻塞前面代码的执行,从而阻塞页面渲染,使得页面呈现假死状态。
为了解决这个问题,前面倒退起来了泛滥的前端模块化标准,包含 CommonJS 大抵有如下几种:
1、AMD (Asynchronous Module Definition)
在聊 AMD 之前,先相熟一下 RequireJS。
官网是这么介绍它的:
“RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.”
翻译过去大抵就是:
RequireJS 是一个 js 文件和模块加载器。它非常适合在浏览器中应用,但它也能够用在其余 js 环境, 就像 Rhino 和 Node。应用 RequireJS 加载模块化脚本能进步代码的加载速度和品质。
它解决了 CommonJS 标准不能用于浏览器端的问题,而 AMD 就是 RequireJS 在推广过程中对模块定义的规范化产出。
来看看 AMD 标准的实现:
<script src="require.js"></script>
<script src="a.js"></script>
首先要在 html 文件中引入 require.js 工具库,就是这个库提供了定义模块、加载模块等性能。它提供了一个全局的 define 函数用来定义模块。所以在引入 require.js 文件后,再引入的其它文件,都能够应用 define 来定义模块。
define(id?, dependencies?, factory)
id:可选参数,用来定义模块的标识,如果没有提供该参数,就应用 js 文件名(去掉拓展名)对于一个 js 文件只定义了一个模块时,这个参数是能够省略的。dependencies:可选参数,是一个数组,示意以后模块的依赖,如果没有依赖能够不传 factory:工厂办法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次,返回值便是模块要导出的值。如果是对象,此对象应该为模块的输入值。
所以模块 A 能够这么定义:
// a.js
define(function(){
var name = 'morrain'
var age = 18
return {
name,
getAge: () => age}
})
// b.js
define(['a.js'], function(a){
var name = 'lilei'
var age = 15
console.log(a.name) // 'morrain'
console.log(a.getAge()) // 18
return {
name,
getAge: () => age}
})
它采纳异步形式加载模块,模块的加载不影响它前面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载实现之后,这个回调函数才会运行。
RequireJS 的根本思维是,通过 define 办法,将代码定义为模块。当这个模块被 require 时,它开始加载它依赖的模块,当所有依赖的模块加载实现后,开始执行回调函数,返回值是该模块导出的值。AMD 是 “Asynchronous Module Definition” 的缩写,意思就是 ” 异步模块定义 ”。
2、CMD (Common Module Definition)
和 AMD 相似,CMD 是 Sea.js 在推广过程中对模块定义的规范化产出。Sea.js 是阿里的玉伯写的。它的诞生在 RequireJS 之后,玉伯感觉 AMD 标准是异步的,模块的组织模式不够天然和直观。于是他在谋求能像 CommonJS 那样的书写模式。于是就有了 CMD。
Sea.js 官网这么介绍 Sea.js:
“Sea.js 谋求简略、天然的代码书写和组织形式,具备以下外围个性:”
“ 简略敌对的模块定义标准:Sea.js 遵循 CMD 标准,能够像 Node.js 个别书写模块代码。天然直观的代码组织形式:依赖的主动加载、配置的简洁清晰,能够让咱们更多地享受编码的乐趣。”
来看看 CMD 标准的实现:
<script src="sea.js"></script>
<script src="a.js"></script>
首先要在 html 文件中引入 sea.js 工具库,就是这个库提供了定义模块、加载模块等性能。它提供了一个全局的 define 函数用来定义模块。所以在引入 sea.js 文件后,再引入的其它文件,都能够应用 define 来定义模块。
// 所有模块都通过 define 来定义
define(function(require, exports, module) {
// 通过 require 引入依赖
var a = require('xxx')
var b = require('yyy')
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
})
// a.js
define(function(require, exports, module){
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = () => age})
// b.js
define(function(require, exports, module){
var name = 'lilei'
var age = 15
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge()) //18
exports.name = name
exports.getAge = () => age})
Sea.js 能够像 CommonsJS 那样同步的模式书写模块代码的秘诀在于:当 b.js 模块被 require 时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到 require 这个关键字,提取所有的依赖项,而后加载,等到依赖的所有模块加载实现后,执行回调函数,此时再执行到 require(‘a.js’) 这行代码时,a.js 曾经加载好在内存中了
3、ES6 Module
后面提到的 CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点: 都在代码运行后能力确定导出的内容 ,CommonJS 实现中能够看到。
还有一点须要留神,AMD 和 CMD 是社区的开发者们制订的模块加载计划,并不是语言层面的规范。 从 ES6 开始,在语言规范的层面上,实现了模块化性能,而且实现得相当简略,齐全能够取代 CommonJS 和 CMD、AMD 标准,成为浏览器和服务器通用的模块解决方案。
事实也是如些,早在 2013 年 5 月,Node.js 的包管理器 NPM 的作者 Isaac Z. Schlueter 说过 CommonJS 曾经过期,Node.js 的内核开发者曾经决定废除该标准。起因次要有两个,一个是因为 Node.js 自身也不是齐全采纳 CommonJS 的标准,譬如在 CommonJS 之 exports 中的提到 exports 属性就是 Node.js 本人加的,Node.js 过后是决定不再追随 CommonJS 的倒退而倒退了。二来就是 Node.js 也在逐渐用 ES6 Module 代替 CommonJS。
2017.9.12 Node.js 公布的 8.5.0 版本开始反对 ES6 Module。只不过是处于试验阶段。须要增加 –experimental-modules 参数。
2019.11.21 Node.js 公布的 13.2.0 版本中勾销了 –experimental-modules 参数,也就是说从 v13.2 版本开始,Node.js 曾经默认关上了 ES6 Module 的反对。
(1)ES6 Module 语法
任何模块化,都必须思考的两个问题就是导入依赖和导出接口。ES6 Module 也是如此,模块性能次要由两个命令形成:export 和 import。export 命令用于导出模块的对外接口,import 命令用于导入其余模块导出的内容。
具体语法解说请参考阮一峰老师的教程,示例如下:
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {return age}
// 等价于
const name = 'morrain'
const age = 18
function getAge (){return age}
export {
name,
getAge
}
应用 export 命令定义了模块的对外接口当前,其余 JavaScript 文件就能够通过 import 命令加载这个模块。
// b.js
import {name as aName, getAge} from 'a.js'
export const name = 'lilei'
console.log(aName) // 'morrain'
const age = getAge()
console.log(age) // 18
// 等价于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
除了指定加载某个输入值,还能够应用整体加载,即用星号(*)指定一个对象,所有输入值都加载在这个对象下面。
从下面的例子能够看到,应用 import 命令的时候,用户须要晓得所要导入的变量名,这有时候比拟麻烦,于是 ES6 Module 规定了一种不便的用法,应用 export default 命令,为模块指定默认输入。
// a.js
const name = 'morrain'
const age = 18
function getAge () {return age}
export default {
name,
getAge
}
// b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
显然,一个模块只能有一个默认输入,因而 export default 命令只能应用一次。同时能够看到,这时 import 命令前面,不须要再应用大括号了。
除了根底的语法外,还有 as 的用法、export 和 import 复合写法、export * from ‘a’、import() 动静加载 等内容,能够自行学习。
后面提到的 Node.js 曾经默认反对 ES6 Module,浏览器也曾经全面反对 ES6 Module。至于 Node.js 和 浏览器 如何应用 ES6 Module,能够自行学习。
(2)ES6 Module 和 CommonJS 的区别
CommonJS 只能在运行时确定导出的接口,理论导出的就是一个对象。而 ES6 Module 的设计思维是尽量的动态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量,也就是所谓的 ” 编译时加载 ”。
正因为如此,import 命令具备晋升成果,会晋升到整个模块的头部,首先执行。上面的代码是非法的,因为 import 的执行早于 getAge 的调用。
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {return age}
// b.js
const age = getAge()
console.log(age) // 18
import {getAge} from 'a.js'
也正因为 ES6 Module 是编译时加载,所以不能应用表达式和变量,因为这些是只有在运行时能力失去后果的语法结构。如下所示:
// 报错
import {'n' + 'ame'} from 'a.js'
// 报错
let module = 'a.js'
import {name} from module
后面在 CommonJS 之 require 有提到,require 的是被导出的值的拷贝。也就是说,一旦导出一个值,模块外部的变动就影响不到这个值。一起来看看,ES Module 是什么样的。
先回顾一下之前的例子:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){age = a}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
应用 ES6 Module 来实现这个例子:
// a.js
var name = 'morrain'
var age = 18
const setAge = a => age = a
export {
name,
age,
setAge
}
// b.js
import * as a from 'a.js'
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 19
ES6 Module 是 ES6 中对模块的标准,ES6 是 ECMAScript 6.0 的简称,是 JavaScript 语言的下一代规范,曾经在 2015 年 6 月正式公布了。咱们在第一节的《Web:一路前行一路忘川》中提过,ES6 从制订到公布历经了十几年,引入了很多的新个性以及新的机制,对于开发者而言,学习老本还是蛮大的。
下一篇,聊聊 ES6+ 和 Babel,敬请期待……
六、参考文献
- CommonJS 标准
- ES Module 的语法
- ES Module 的加载实现
- 前端模块化开发解决方案详解
- webpack 模块化原理 -commonjs
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:Labs2020 分割。