javascript模块化 发展历程
javascript模块化的发展,距今已有10个年头左右。
无模块化
在早期,javascript作为一门脚本语言,仅为协助表单校验等界面辅助增强,那时候的前端也比较简单, javascript不需要模块化。
命名空间
后来随着 javascript 需要承担更多的功能,代码量开始上升,为了避免全局命名冲突等问题,提出了使用命名空间的方案,将符合某种规则或者约定的代码,放到同一个命名空间下。 这算是 javascript模块化最早期的雏形。
YAHOO.util.Event.stopPropagation(e)基本的模块化
在这个时期,出现了比较清晰的模块定义,通过闭包来做模块运行空间
// 定义模块
YUI.add('hello', (Y) => {
Y.sayHello = function () {
Y.DOM.set(el, 'innerHTML', 'hello!')
}
}, '1.0.0', { requires: ['dom'] })
// ...
// 使用模块
YUI().use('hello', (Y) => {
Y.sayHello('entry') // <div id="entry">hello!</div>
})CommonJs
CommonJs 其实是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定, 在当年 javascript 的模块化思想还在官方的讨论中, 缺乏普遍可接受形式的javascript脚本模块单元。
CommonJs规范和当时出现的NodeJs相得益彰,共同走入了开发者的实现。
但 CommonJs 其实是面向网页浏览器之外的(如NodeJs,即面向服务端的模块化规范),并不适用于浏览器端。
CommonJs 规范简介
在CommonJs 规范中, 每个文件都是一个模块,有自己的作用域,在文件中定义的变量、函数、类等,都是私有的,对其他文件不可见。
在每个模块中,有两个内部变量可以使用, require 和 module
require用于加载某个模块。module表示当前模块,是一个对象。这个对象中保存了当前模块的信息。exports是module上的一个属性,保存了当前模块要导出的接口或者变量,使用require加载的某个模块获取到的值就是那个模块使用exports导出的值。
let name = 'Mark'
let age = 18
module.exports.name = name
module.exports.getAge = function () {
return age
}let moduleA = require('./a.js')
console.log(moduleA.a) // Mark
// 使用了未导出的变量,获取不到值
console.log(moduleA.age) // undefined
console.log(moduleA.getAge()) // 18在NodeJs环境中,CommonJs的模块由于在服务器环境下,可以从本地进行加载,即 同步加载。
AMD、CMD
注释
在我的印象中, CommonJs规范 和 AMD规范 出现的时间点 相差不远。
AMD 早于 CommonJs。
按我个人理解,CMD 在当年算是从 AMD 衍生出来的一个方案。
注意
CommonJs 和 CMD 是两种方案!不是一样的!
AMD规范
AMD规范,即 异步模块定义(Asynchronous Module Definition)。
AMD 采用 异步加载模块 的方式。
AMD规范仅定义了一个 define 函数,它是一个全局变量:
define(id?, dependencies?, factory);id描述的是当前模块的标识符;dependencies则是当前模块的依赖数组, 它们会在 factory工厂方法被调用前被加载并执行, 并且执行的结果必须以依赖数组定义的顺序,依此顺序作为参数传入 factory工厂方法。factory为模块初始化要执行的函数或者对象。如果函数返回一个值,则该值应该设置为该模块的输出值。
CMD规范
CMD规范,即 公共模块定义(Common Module Definition)
CMD规范 定义了 一个 define 函数,它是一个全局变量:
define(id?, dependencies?, factory);id描述的是当前模块的标识符;dependencies是当前模块的依赖数组, 他们会在 factory 工厂方法被调用前完成加载,但并不立即执行。factory为模块初始化要执行的函数或者对象。如果是一个函数,则函数接受三个参数:
define((require, exports, module) => { })require用于同步加载并执行已经定义好的其他模块;获取模块的输出值,exports是module.exports的别名,用于导出当前模块的输出值;module存储了当前模块的信息。如果是一个对象,则直接作为当前模块的输出值。
两者的差异
AMD规范 和 CMD规范 从规范定义上来看,主要的差异为:
- AMD 的模块在加载后是立即执行的,并且会按照依赖顺序依次传入 factory, 而 CMD的模块在加载后并不立即执行,而是在 factory方法中,通过
require方法调用执行模块获取结果;
规范的实现
- AMD流行的实现库是 require.js;
- CMD流行的实现库是 sea.js;
提示
由于在当下已经越来越少会去选择使用 require.js 以及 sea.js, 这里就不多对这两个库做介绍说明。
NodeJs前端工具链
得益于 NodeJs 的能力,开源社区在模块化方面又再次向前继续迈进。 特别是在推出了 NPM 包管理工具后,前端的工具、模块化出现了井喷式发展。
grunt gulp
既然 CommonJs 不适用于 浏览器端的一个主要原因是同步加载和异步加载之间的问题,那么借助于 grunt 或 gulp 提供的前端工具,在开发时,还是以文件一模块,然后构建时,将模块文件打包在一起,那么由于都是在同一个文件中,则模块之间的加载则可以是同步的。
在这个时期,grunt、gulp 并没有提供直接的模块化打包能力,但是在其基础上,通过插件实现了文件合并,从而能够在开发时,以 某种模块规范进行项目架构和管理,再进行打包构建。
webpack NPM
真正让 前端模块化得到质的飞跃的,是 NPM的推出,内置到了 NodeJS 中。
而 webpack 的出现,这块 真正意义上的 模块打包工具,配合 NPM, 让模块化越来越得以更方便的运用于应用开发中。
webpack 作为一个 模块打包器, 在内部根据 CommonJs规范实现了 模块加载器,使得应用于浏览器端的javascript代码,也能够像 Node端的 javascript代码,拥有类似甚至相同的文件组织结构。
实现了一文件一模块,模块之间通过 require 函数进行 访问。
而 NPM的推出与流行,在前端引入了 package 包的概念,模块以包的形式进行管理, 让越来越多的开发者,能够共享各自开发的模块,开发者可以通过 NPM 安装其他开发者已开发好的模块,然后通过 webpack 实现开发时加载这些模块。
webpack 内部实现了 不同的 模块化规范,包括 匿名函数闭包iife, AMD, CMD,CommonJs等。
webpack 不仅将 javascript 作为模块,而是将一切资源都作为模块进行处理。
其他的模块打包工具
其他包管理工具
说明
与npm 对比,都是社区对于 包管理 的不同理念、不同实践 下所产生的工具。 三者互相发展,并都有各自的特色。
ES Modules
随着 javascript的发展,ECMAScript将模块加载添加到了标准之中,浏览器也开始支持 模块加载。
使用 Javascript 模块依赖于 import 和 export 进行导入和导出。
在 html 导入 javascript模块脚本是,需要在 <script> 标签中添加 type="module" 的属性声明
<script type="module" src="/moduleA.js"></script>import { getAge, name } from './moduleB.js'
console.log(name)
console.log(getAge())export const name = 'Mark'
const age = 18
export function getAge() {
return age
}Deno模块加载
Deno与 Node在模块加载上最大的差别, 就是 放弃了 项目中的node_modules 作为第三方包的存放目录,也抛弃了 类似于 NPM 的中心化管理的 模块管理工具。
Deno 推荐使用的是 去中心化的模块加载管理,支持直接从远程的任意站点加载提供的模块。
如从 官方的 deno.lang,或者从 unpkg.com 加载第三方模块。
说明
这种去中心化模块管理的模块加载方案,相对来说会比较依赖于网络环境,虽然远程的模块首次加载后也会被缓存,但进行生产部署时,往往生产服务器跟公网是隔离的,在这种情况下,就需要自建一个内部服务器作为代理,托管第三方的模块包。
