JavaScript 模块化发展史
第一阶段
在 JavaScript 语言刚刚诞生,js只用于实现小的效果,js代码通常只有几百行,专业的前端工程师还没有出现,都是后端顺带完成前端工作
大事件
-
1996年,NetScape将JavaScript语言提交给欧洲的一个标准制定阻止ECMA(欧洲计算机制造商协会)
-
1998年,NetScape在与微软浏览器IE的竞争中失利,宣布破产
第二阶段
随着ajax的出现,改变了 JavaScript 在浏览器中扮演的角色,js可以与服务器进行交互,优化了用户体验。
js代码逐渐增加,前端逻辑逐渐复杂。
但是前端规模没有进一步扩大,主要由于以下几个原因
- 浏览器解释执行JS的速度太慢
- 用户端的电脑配置不足
- 更多的代码带来了全局变量污染、依赖关系混乱等问题
大事件 - IE浏览器制霸市场后,几乎不再更新
- ES4.0流产,导致JS语言10年间几乎毫无变化
- 2008年ES5发布,仅解决了一些 JS API 不足的糟糕局面
第三阶段
到了2008年,谷歌的 V8 引擎发布,将JS的执行速度推上了一个新的台阶,甚至可以和后端语言媲美。
摩尔定律持续发酵,个人电脑的配置开始飞跃(摩尔定律指的是当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。)
同时,nodejs诞生,对模块化的需求进一步加深,
经过社区的激烈讨论,最终,形成了一个模块化方案,即鼎鼎大名的CommonJS,该方案,彻底解决了全局变量污染和依赖混乱的问题
该方案一出,立即被nodejs支持,于是,nodejs成为了第一个为JS语言实现模块化的平台,为前端接下来的迅猛发展奠定了实践基础
大事件
- 2008年,V8发布
- IE的市场逐步被 firefox 和 chrome 蚕食,现已无力回天
- 2009年,nodejs发布,并附带commonjs模块化标准
第四阶段
随着后端模块化的诞生,前段模块化也相继出现AMD,CMD,终于在2015年,ES6发布,它提出了官方的模块化解决方案 —— ES6 模块化
前端模块化的出现使得一些框架相继诞生
既然JS也能编写大型应用,那么自然也需要像其他语言那样有解决复杂问题的开发框架
- Angular、React、Vue等前端开发框架出现
- Express、Koa等后端开发框架出现
- 各种后端数据库驱动出现
要开发大型应用,自然少不了各种实用的第三方库的支持
- npm包管理器出现,实用第三方库变得极其方便
- webpack等构建工具出现,专门用于打包和部署
总结
由于ajax的出现,js代码量激增,同时也完成了更多的任务,但是由于代码量的增多,js暴露出来三个问题(1.js执行速度太慢2.用户电脑配置不高3.代码增多导致全局变量污染),v8引擎的出现与摩尔定律的发酵,解决了前两个问题,nodejs和commonJs的出现,也将前端模块化推上日程,于是出现了三种前端模块化解决方案(AMD,CMD,ES6),前端模块化的出现也给框架的诞生提供了基础
模块化规范介绍
由于nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的CommonJS作为模块化规范
在学习CommonJS之前,首先认识两个重要的概念:模块的导出和模块的导入
何为导出
要理解模块的导出,首先要理解模块的含义
什么是模块?
模块就是一个JS文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用
模块有两个核心要素:隐藏和暴露
隐藏的,是自己内部的实现
暴露的,是希望外部使用的接口
任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或api调用来暴露接口
何为导入
当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。
当通过某种语法或api去使用一个模块时,这个过程叫做模块的导入
nodejs对CommonJS的实现
为了实现CommonJS规范,nodejs对模块做出了以下处理
-
为了保证高效的执行,仅加载必要的模块。nodejs只有执行到
require
函数时才会加载并执行模块(这叫做依赖延迟声明) -
为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
(function(){ //模块中的代码 })()
-
为了保证顺利的导出模块内容,nodejs做了以下处理
- 在模块开始执行前,初始化一个值
module.exports = {}
module.exports
即模块的导出值- 为了方便开发者便捷的导出,nodejs在初始化完
module.exports
后,又声明了一个变量exports = module.exports
(function(module){ module.exports = {}; var exports = module.exports; //模块中的代码 return module.exports; })()
- 在模块开始执行前,初始化一个值
-
为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果
commonJs的导出
module.export.xxx = xxx;
module.export.xxx = {
xxx: xxx
}
commonJs的导入
var xxx = require('./xxx.js'); // 路径必须为相对路径
es6的官方模块化标准
es6模块化标准通过script标签来引入文件,使用type="module"来声明此文件作为一个模块来执行
es6模块化特点如下
- 使用依赖依赖预声明的方式导入模块
- 灵活的多种导入导出方式
- 规范的路径表示法:所有路径必须以./或…/开头
es6模块化的基本导入导出
导出
es6的基本导出可以有多个,每个都必须是声明表达式,或具名符号
export const a = 1;
export const b = 2;
// 这样是不可以的↓
const c = 3;
export c;
es6的模块化支持简写
const a = 1;
const b = 2;
const c = 3;
export {a, b, c};
// 导出时支持重命名
export {a as a1, b as b2, c as c3};
导入
import {a, b} from './xxx.js'; // 只需要导入需要的变量,c不需要就可以不导入
// 同样的导入时也支持重命名导入数据
import {a as a1, b as b2} from './xxx.js';
es6模块化的默认导入导出
导出
export default 默认导出的数据
或者
export {默认导出的数据 as default}
由于每个模块仅允许有一个默认导出,因此,每个模块不能出现多个默认导出语句
导入
import xxx from './xxx.js'; // 由于默认导出没有给数据命名,所以不存在重命名
导入导出混合写法
一个模块里可以同时存在基本导入导出与默认导入导出
导出
const a = 1;
const b = 2;
const c = 3;
export {a, b, c};
export default {d: 4}
导入
import d, {a, b, c} from './xxx.js';
// d为默认导出,abc为正常导出
或
import * as data from './xxx.js';
// data中有一个属性叫做default,这个属性值即为默认导出数据
// 想导入所有正常导出,也可以使用*
es6模块化的其他细节
- 尽量导出不可变值
尽量使用const定义导出的数据,确保它在本模块内也是一个常量。
因为,虽然导入后,无法更改导入内容,但是在导入的模块内部却有可能发生更改,这将导致一些无法预料的事情发生
- 可以使用无绑定的导入用于执行一些初始化代码
如果我们只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入:
import "模块路径"
- 可以使用绑定再导出,来重新导出来自另一个模块的内容
有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法轻松完成
export {绑定的标识符} from "模块路径"
例如a.js需要依赖b.js,b.js需要依赖c.js。
b.js需要原封不动的导出c.js里的变量c。
那么,b.js可以这样写
export { c } from './c.js';
依赖延迟声明与依赖预声明的区别
- 依赖延迟声明
- 优点:某些时候可以提高效率(某些情况见下方例子)
- 缺点:无法在一开始确定模块依赖关系(比较模糊)
- 依赖预声明
- 优点:在一开始可以确定模块依赖关系
- 缺点:某些时候效率较低
nodejs执行在本地,文件从本地读取,速度快,所以commonJs使用依赖延迟声明
而浏览器读取文件需要请求,速度较慢,使用依赖预声明
es6模块化标准的导入,应该写在js最上面,即使不写在最上面,浏览器在预编译的时候,也会将导入模块置于顶部
例
if (Math.random() > 0.5) {
// 导入A模块
} else {
// 导入B模块
}
当这种情况出现的时候,依赖延迟声明效率较高