Skip to content

JS模块化方案的发展

把一个复杂庞大的 JS 进行模块化分割,封闭内部的实现,对外暴露接口。

Intro

模块化发展: 1、NameSpace 模式,每个模块都导出一个对象 (不安全) 2、IIFE 模式 + window 暴露,基于函数作用域闭包私有模块,需要手动维护依赖关系(自己用IIFE声明依赖,自己控制script标签的引用顺序) 3、自动构建依赖图,模块化模式(在浏览器下要运行)

  • CommonJS 的 broswerify
  • AMD 的 RequireJS

模块化有利于: 1、功能解耦、分离,支持复用,支持按需加载,提高可维护性 2、避免命名空间污染


模块化面临的问题: 1、模块依赖关系 2、如果生产版本也是细粒度模块化的,那么请求过多,页面加载慢

CommonJS

intro

  • 每个文件都可以是一个模块
  • 模块是在运行时才同步加载的,在 Node 环境没问题
  • 在浏览器环境,必须要提前编译打包处理。

🌟 模块导入导出

模块的导入导出

js
//导出
module.exports = xxx
exports.xxx = ...

//导入
const xx = require('xxx')

结合上一节我们对打包产物的分析,可以知道,模块导出的本质是一个对象 module.exports,而且这个 module 是被缓存起来了的。

所以我们可以说,module.exports 是一个对象,require 的结果是这个对象的引用

let fs = require('fs') 就相当于是定义一个 fs 变量,其值为module.exports对象的引用

所以,会发生下面的情况,改变属性会影响原模块,重新赋值变量则只是操作这个变量,对原模块无影响。

js
// tool.js
module.exports = { val: 1 };

// a.js
const tool = require('./tool');
tool.val = 100;      // 改变对象属性
tool = { val: 200 }; // 改变局部变量指向,不影响导出

// b.js
const tool = require('./tool');
console.log(tool.val); // 100

相关的生态

年代工具主要作用背景
2011Browserify让浏览器能使用 Node.js 的 require()解决浏览器没有模块化的问题
2014Webpack不仅打包 JS,还能打包 CSS、图片等所有资源现代前端工程化爆发期(SPA、React、Vue)

Browserify 的原理:

  • 分析入口文件
  • 静态分析,找出 require 语句,递归分析依赖,构建依赖图
  • 把每个文件包装为一个函数,存在一个对象里
  • 然后就是类似上一节我们分析的 webpack 这一套,modules,require,export等等。

Webpack 借鉴了 Browserify 的思想(依赖图 + 模块封装 + 自定义 require),
但功能更强、扩展性更好。

都说 require 是在运行时同步加载依赖,那么 browserify,webpack 是怎么做的呢?

答: Browserify(以及后来的 Webpack、Rollup 等)之所以能“在打包时”分析出依赖关系,靠的正是 静态分析 + 抽象语法树(AST)。只要你的 require() 参数是字符串常量,它就能在编译时解析出依赖。

AMD

intro

  • Asynchronous Module Definition 异步模块定义规范
  • 专门用于浏览器端,模块的加载是异步的。
  • 显示声明的依赖注入

模块定义、暴露和引入

特点:显示声明的依赖注入

1、定义没有依赖的模块并暴露

js
define(function(){
	// ...
	return exposeModule
})

2、定义有依赖的模块并暴露

js
define(['module1', 'module2'], function(m1,m2){
	// ... 依赖 m1, m2 的逻辑
	return exposeModule
})

3、引入模块并使用

js
requirejs(['moduleA', 'moduleB'], function(mA,mB){
	// ... 依赖 m1, m2 的函数逻辑
})

生态 RequireJS

统一采用上面的语法,去写模块和引入模块。

比起 IIFE 方案,模块依赖关系由 RequireJS 去维护,用户只用做配置。

结合 requirejs.config 去配置模块名字和文件的映射。

引入require.js ,设置 data-main 为项目入口文件

但是并非所有第三方库都支持 AMD,有的需要额外配置,比如下面的 angular.js

CMD

Intro

  • Common Module Definition
  • 相对小众,是早期阿里搞的
  • 专门用于浏览器端,模块的加载是异步的。
  • 模块使用时,才加载执行
  • Look Like AMD + CommonJS

模块定义、暴露和引入

CMD 在语法规范上有点像 CommonJS 和 AMD 的结合

1、定义没有依赖的模块并暴露

js
define(function(require, exports, module){
	// ...
	exports.xxx = xxx
	// 或
	module.exports = xxxxx
})

2、定义有依赖的模块并暴露

js
define(function(require, exports, module){
	// 同步引入
	var moduleA = require('./moduleA')
	// 异步引入
	require.async('./moduleB',function(mb){
		// ....
	})

	// ...
	exports.xxx = xxx
	// 或
	module.exports = xxxxx
})

3、引入模块并使用

js
define(function(require){
	// ... 依赖 m1, m2 的函数逻辑
	var mA = require('./moduleA')
})

生态 Sea.JS

.......

ESM

Intro

  • 现代浏览器已经支持 ESM, <script type="module"></script>
  • 兼容不支持ESM 的老浏览,需要进行打包构建处理
  • 还有在生产模式下,要把细粒度的模块整合成 bundle,也需要打包构建处理

模块导入导出

js
// 分别导出
export function foo(){
	//...
}
// 统一导出
export {
	a,
	b
}
// 默认导出
export default xxx
js
import {a} from './foo.js' 

import mod from './bar.js'

生态和方案

Babel + Webpack

1、Babel 负责代码编译,降级 ES6+ --> ES5

  • babel-cli:babel 的 commond line interface
  • babel-preset-es2015: 专门用于降级到 es5 的

.babelrc 文件:babel run control

json
{
	"presets" :["es2015"] // 告诉 babel 要转 es6
}

babel 会把 import / export 降级为 CommonJS语法

2、Webpack 实现浏览器内部的 require

  • __webpack_modules__
  • __webpack_module_cache__
  • __webpack_require__
  • 当然,还有庞大的 loader、plugin 体系