1. 简介
前端发展初期或者Javascript
发展初期,我们所要解决的问题只是一个页面上各种内容的排版,表单验证之类的简单交互逻辑处理,这个时候单个js文件就完全能满足我们的开发要求。
随着Web的发展,前端已经从单纯HTML展示发展到前端应用,同时伴随着语言的发展,产生了大量的第三方帮助开发的库(例如:JQuery
),我们所需要的Javascript
脚本逐渐变大,变多,HTML页面中的<script>
逐渐从单个变成了多个,但是在这种从单个到多个的转变中,却遇到了一些问题:
Javascript
文件之间的依赖:因为<script>
脚本的机制,虽然每一个<script>
共享了全局变量,但是却没有办法将后一个<script>
脚本中声明的变量提升到前一个中,如果前一个<script>
中依赖了该变量,则需要调整脚本顺序,但是单纯的从HTML引入脚本的顺序我们又不能明确知道脚本间的依赖关系- 变量的泄漏:因为
<script>
之间是共享全局变量的,但是每个Javascript
脚本在编写初期都是基于单个脚本来编写,不可避免的会出现多个脚本之间使用了同名的全局变量,那么后引入必定会覆盖之前的变量值,从而导致一些难以发现的问题
随着Javascript
文件数量的增加,问题发生的频率就越来越大,工程越来越难以维护,于是机智的工程师们为了解决的这两个问题,提出了模块化解决方案(我始终觉得前端在工程化的过程中是逐步将后端的编码思想引进并不断完善,包括模块化其实就是类似于参考了例如:Java的包或者类引入)
2. 模块化原理
模块化的核心有两个方面:
- 模块的声明和模块的导入,也就是模块化的语法,如何定义一个模块
- 模块的加载器,也就是如何将识别声明和导入的模块,将他们有条件的加载
因为模块的声明和导入是依赖于模块加载器的写法,因此说到底,模块化最主要的也就是如何根据规范编写一个模块加载器。编写模块加载器要解决的就是提到的两个痛点,先看如何解决第二个问题,为了避免泄漏,就是创建一个有限的作用域范围,最佳解决方案就是使用闭包来产生相关的作用域,使用IIFE返回相关函数实在是好的不能再好了
// IIFE函数闭包
(function(){
var a = 1;
return {
f() {
// 一些逻辑处理
console.log(a);
}
}
})()
接下来就是解决依赖关联,也就是我想明确知道自己使用了那些模块,于是如果能将依赖的模块作为参数传递,其实就可以从一定程度上明确我们的依赖关系,于是
// 定义一个IIFE函数
var $ = (function(){
var a = 1;
return {
f() {
// 一些逻辑处理
console.log(a);
}
}
})()
// 创建一个名字和对象的映射关系
var modules = {'$': $}
// 建立依赖关系
function f(depsname, callback) {
let deps = [];
for (let name of depsname) {
deps.push(modules[name]);
}
callback(deps)
}
// 使用依赖关联
f(['$'], ([$]) => { $.f()});
类似上面的处理,我们就可以很明确的知道了依赖了一个名叫$
的模块,而这个模块我们在modules对象里面进行的映射关联,指向了具体的内容
这里的示例只是对原理的简单性阐述,并不是一种最佳实践
3. 模块化方案
3.1 CommonJS
CommonJS
是最先提出的一种模块化解决方案,与其说它是解决方案,不如说它是一个规范,它约定了Javascript
模块化的各种内容,基本上可以是模块化思想的先驱
3.1.1 语法
NodeJS
中的模块化处理方法使用了CommonJS
的核心思想,是CommonJS
规范的实践者
// 模块的声明(导出)
module.exports = {foo}
exports.foo = foo
// 模块的引用(导入)
var foo = require('foo');
有两个地方需要说明:
- 在
NodeJS
中exports === module.exports
,因为默认在每个NodeJS
模块都会在最上层添加var exports = module.exports
的声明require
引用的是对应模块文件的路径,有三种方式:相对路径,绝对路径,文件名,NodeJS
的模块引擎会根据require
中路径的写法,去获取对应的模块
3.1.2 特点
CommonJS
的特点是模块加载是同步的,在模块使用前,需要经过模块加载器将模块语法转换为对应的Javascript
代码,之后才能正常运行,这样带来的问题就是他不能直接在浏览器端进行使用,只能在服务端进行转换后使用
3.2 AMD
AMD
(Asynchronies Module Definition),异步的模块定义,一大波人研究CommonJS
,从CommonJS
分离出来的,自行发展的一个新分支
3.2.1 语法
AMD
的基于CommonJS
规范,衍生出了自己的一套体系,比较出名的模块化脚本加载器就是Require.js
了(之前阿里的Seajs
也很出名的,当然发起SAP
(单页面应用)趋势的Angular.js
也是基于Require.js
来进行模块加载的)以Require.js
为例,说明下基本语法
// 入口配置
<script data-main="app" src="require.js">
// 模块的声明
define(module_id, deps, function(deps){});
define(function(require, export, module));
// 模块的引用
requirejs(deps, function(deps))
3.2.2 特点
AMD
一诞生就是针对浏览器端模块化提出的解决方案,他不需要提前对模块进行转换操作(不过需要配置一个模块的入口文件,注意入口配置中的data-main
属性),直接就可以在浏览器端实现模块的加载,同时模块化过程是异步的,可以做到CommonJS
做不到的模块延迟加载
3.3 ES6模块
随着模块语法的发展,ES6
基于CommonJS
和AMD
,定义了一套模块化的方案,虽然现在浏览器对ES6
模块化方案支持不多(Chrome
似乎已经开始支持<script type="module">
的提案了),但是我们仍然可以通过TypeScript
或Babel
提供的模块Loader,类似CommonJS模块化的处理手段,利用webpack
等工具优先进行模块化转换
3.3.1 语法
ES6
模块的根据不同情况,写法会有一定出入,但是主要就是区分了普通的情况和有default
声明的情况
// ---模块导出---
// 1. 对象,函数,值导出
export { foo }
export function f() {}
export let a = 1
// 2. default导出
export default {}
export { x as default}
// 3. 导出其他模块的非default内容
export * from 'module_name'
export { foo } from 'module_name'
// 4. 导出其他模块的default内容
import A from 'module_name'
export default A
// ---模块导入---
// 1. 导入Default
import A from 'module_name'
// 2. 导出非Default
import { name1 } from 'module_name'
import { name1 as alias } from 'module_name'
// 3. 导入所有非Default内容并创建命名空间
import * as namespace from 'module_name'
// 4. 同时导入Default和非Default内容
import A, {name1} from 'module_name'
import A, * as namespace from 'module_name'
// 5. 单纯进行模块加载
import 'module_name'
3.2.2 特点
ES6
模块化方案有几个需要特别注意的特点:
- 通常理解为一个模块一个文件,而一个文件就是一个模块,每个模块中只能最多有一个
default
的export
- 每个模块只会加载一次
- 每个模块的变量声明默认只在当前模块内有效
- 模块的引入是单例的,即使多次引入,也只会共享一个实例
import
引入的模块是readonly
的
4. 总结
总的来说,随着前端项目的逐渐项目化,使用模块化避免了可能带来的隐藏问题,使得整个项目更便于维护和管理,我们也可以很放心的开发各种扩展库,而不用担心和外部变量冲突的问题,很大程度的促进了前端社区的繁荣
5. 参考
《You Don‘t know Javascript - ES6 & Beyond》
MDN-export
MDN-import
Writing Modular JavaScript With AMD, CommonJS & ES Harmony
CommonJS规范
网友评论