一、前端模块化规范
前端需要模块化,并且模块化不光要处理全局变量污染、数据保护的问题,还要很好的解决模块之间依赖关系的维护。
既然JavaScript 需要模块化来解决上面的问题,那就需要制定模块化的规范,CommonJS 就是解决上面问题的模块化规范之一
参考链接—— 为什么需要CommonJS
1. CommonJS
为什么要学习CommonJS? 因为node.js最开始是参照CommonJS这个模块化规范做起来的,所以要了解CommonJS他到底为了实现模块化制定了什么规范。
1.1 require
require 用来加载某个模块,module 代表当前模块,是一个对象,保存了当前模块的信息。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值。
require 命令的基本功能是,读入并执行一个 js 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。
第一次加载某个模块时,Node.js 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 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
1.2 CommonJS实现
了解 CommonJS 的规范后,不难发现我们在写符合 CommonJS 规范的模块时,无外乎就是使用了 require 、 exports 、 module 三个东西,然后一个 js 文件就是一个模块。
如果我们向一个 立即执行函数 提供 require 、 exports 、 module 三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在 module.exports 中,这样就实现了模块的加载。如下所示:
JS立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行,这种模式本质上就是函数表达式(命名的或者匿名的),在创建后立即执行。
注意: 1. 函数体后面要有小括号 2. 函数体必须是函数表达式而不能是函数声明
(function(name){
})(name) //其实就是将外面括号中的name传给function来使用
(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) //将外面括号中的module,module.exports, require作为实参传入函数
许多打包工具都是通过把所依赖的模块放到各自的函数中,将所有模块打包成一个能在浏览器中运行的js文件,以实现将符合CommonJS模块规范的项目代码,转换为浏览器支持的代码,比如webpack。
以下代码展示了webpack是如何在打包模块时实现对CommonJS规范的支持。
- bundle.js是一个立即执行的匿名函数,所以能在浏览器中直接运行。函数体内包含了模块管理的具体内容
- 匿名函数后面包含了多个js文件的小括号,将这些文件内容传入匿名函数中
// 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: {}
}
// 执行要加载的模块
// call函数第一个参数表示实际要执行的对象,后面为传入modules[moduleName]中的参数
// 可以理解为 'a.js'.call(installedModules['a.js'] ,module, module.exports, require)
modules[moduleName].call(module.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 文件内容
}
})
2、其他前端模块化
由于CommJS的 require 命令是执行完一个文件后返回他的exports对象,这在服务器端是可行的,因为服务端加载并执行一个文件的时间消费是可以忽略的,模块的加载是运行时同步加载的,require 命令执行完后,文件就执行完了,并且成功拿到了模块导出的值。
但这不适合浏览器端,因为它是同步的。可想而知,浏览器端每加载一个文件,要发网络请求去取,如果网速慢,就非常耗时,浏览器就要一直等 require 返回,就会一直卡在那里,阻塞后面代码的执行,从而阻塞页面渲染,使得页面出现假死状态。
为了解决以上的问题,出现了众多前端模块化规范,如下图所示
2.1 AMD和CMD
AMD和CMD都是用在浏览器端的模块规范
- AMD使用
define以及define中的参数来进行模块化,采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。
//AMD示例
// 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
}
})
- CMD采用同步的形式书写模块代码,首先要在 html 文件中引入
sea.js工具库,就是这个库提供了定义模块、加载模块等功能。它提供了一个全局的define函数用来定义模块。当 b.js 模块被require时,b.js 加载后,Sea.js 会扫描 b.js 的代码,找到require这个关键字,提取所有的依赖项,然后加载,等到依赖的所有模块加载完成后,执行回调函数,此时再执行到require('a.js')这行代码时,a.js 已经加载好在内存中了
// 所有模块都通过 define 来定义
define(function(require, exports, module) {
// 通过 require 引入依赖
var a = require('xxx')
var b = require('yyy')
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
})
// CMD示例
// 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
})
2.2 ES6 Module
ES6 Module 是 ES6 中对模块的规范,ES6 是 ECMAScript 6.0 的简称,是 JavaScript 语言的下一代标准,在 2015 年 6 月正式发布,Node.js已经开始支持ES6的各种用法。
CommonJS 是服务于服务端的,而 AMD、CMD 是服务于浏览器端的,但它们都有一个共同点:都在代码运行后才能确定导出的内容, 而 ES6 Module 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及导入和导出的变量,也就是所谓的 编译时加载。
正因为如此,import 命令具有提升效果,会提升到整个模块的头部,首先执行。也正因为 ES6 Module 是编译时加载, 所以不能使用表达式和变量,因为这些是只有在运行时才能得到结果的语法结构。
ES6与CommonJS不同,传输的不是拷贝的值,因此以下代码会有不同的效果
// 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
二、Node.js
参考内容——《深入浅出Node.js》朴灵