前端模块化最主要的就是解决作用于和上下文的问题
幼年期:无模块化
- 开始需要在页面中加载不同的JS:动画、组件、格式化
- 多种js文件会被分在不同的文件中
- 不同的文件又被同一个模板所引用
<script src="jquery.js"></script>
<script src="main.js"></script>
<script src="dep1.js"></script>
认可: 文件分离拆分是最基础的模块化(第一步)
script 标签的参数 - async & defer
总结: 普通 - 解析到立即阻塞,立刻下载执行当前script async - 解析到标签开始异步下载,解析完之后开始执行 defer - 解析到标签开始异步下载,下载完成后开始执行并且阻塞渲染,执行完成之后继续渲染
问题出现:
- 污染全局作用域 => 不利于大型项目的开发以及多人团队的共建
- 兼容性 > IE9
- 问题可以被引导到 => 1. 浏览器渲染原理 2. 同步异步原理 3. 模块化加载原理
成长期:模块化前夜 - IIFE(语法侧的优化)
作用域的把控
利用函数的块级作用域 - 隔离区
(() => {
let count = 0;
// ……
})();
初步实现了一个最最最最最最简单的模块 尝试定义一个最简单的模块
const iifeModule = (() => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
console.log(count);
increase();
})();
- 追问:独立模块本身的额外依赖,如何优化
优化1: 依赖其他模块的传参型
const iifeModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
console.log(count);
increase();
})(dependencyModule1, dependencyModule2);
** 面试1:了解jquery或者其他很多开源框架的模块加载方案
const iifeModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
const increate = () => ++count;
const reset = () => {
count = 0;
}
console.log(count);
increate();
return {
increate, reset
}
})(dependencyModule1, dependencyModule2);
iifeModule.increate();
iifeModule.increate();
=> 揭示模式 revealing => 上层无需了解底层实现,仅关注抽象 => 框架
成熟期:
CJS - Commonjs
node.js指定 特征:
- 通过module + exports 去对外暴露接口
- 通过require去引入外部模块
- 同步加载模块
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。
模块输出:模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象
加载模块:加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象
/** 定义模块 math.js **/
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
2
/** 引用自定义模块: 参数包含路径,可省略.js **/
var math = require('./math');
math.add(2, 5);
优点:CommonJs率先在服务端实现了,从框架层面解决了依赖、全局变量污染的问题
缺点:针对了服务端的解决方案。异步拉取依赖处理不是很完美(解释:commonJS用同步的方式加载模块。在
服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载,所以在浏览器端一般就不使用commonJS了。)
面试: 一些开源项目为何要把全局、指针以及框架本身引用作为参数
(function(window, $, undefined) {
const _show = function() {
$("#app").val("hi zhaowa");
}
window.webShow = _show;
})(window, jQuery);
上面问题问的是阻断思路
传入window - 1. 全局作用域转化成局部作用域,提升执行效率 2. 编译时优化
(function(c){})(window) // window会被优化成c 执行完可以销毁 立即清理回收
jquery - 1. 独立定制复写和挂载 2.防止全局串扰 undefined - 防止重写
AMD规范
通过异步加载 + 允许制定回调函数 经典实现框架:require.js
AMD是 RequireJS 对模块定义的规范。AMD 推崇依赖前置。它是依赖前置(依赖必须一开始就写好)会先尽早地执行(依赖)模块 。用 require.config()指定引用路径等,用define()定义模块,用require()加载模块。
依赖前置,预执行(异步加载:依赖先执行),没有延迟,所以用户体验好
// define来定义模块
define(id, [depends], callback);
// require进行加载
require([module], callback);
模块定义地方
define('amdModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
// 业务逻辑……
})
引入的地方
require(['amdModule'], amdModule => {
amdModule.increase();
})
面试题: 如果在AMDModule中想兼容已有代码,怎么办?
// 依赖不填写,通过require引入原有代码
define('amdModule', [], require => {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
// 业务逻辑……
})
面试题:手写兼容CJS&AMD
// 判断关键step1. object还是function step2. exports? step3. define
(define('amdModule'), [], (require, export, module) => {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
export.increase = increase();
})(
// 目标:一次性区分CJS还是AMD
typeof module === "object"
&& module.exports
&& typeof define !== "function"
? // 是CJS
factory => module.exports = factory(require, exports, module)
: // AMD
define
)
- 优点:适合在浏览器中加载异步模块的方案
- 缺点:引入成本
CMD: 中国提出的框架
按需加载 主要应用框架: sea.js
CMD 是 SeaJS 在对模块定义的规范。CMD推崇依赖就近。
依赖就近,懒(延迟)执行,所以性能好
define(function (require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
- 优点: 按需加载,依赖就近
- 缺点:依赖打包,加载逻辑存在于每个模块中,扩大了模块体积,同时功能上依赖编译
AMD与CMD比较:
这也是很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行的原因
AMD:用户体验好,依赖前置,预执行(异步加载:依赖先执行),没有延迟,依赖模块提前执行了;
CMD:性能好,依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)
ES6 Module
走向新时代 新增定义: 引入:import 导出:export
export import webpack需要配置
ES6 Module主要由两个命令构成:export和import。export命令:用于规定模块的对外接口,import命令:用于输入其他模块提供的功能。
使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add }; //暴露给外部的变量
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
- 优点(重要性):通过一种最终统一各端的形态,整合了js模块化的通用方案
- 局限性:本质上还是运行时的依赖分析
ES6 模块与 CommonJS 模块比较:
CommonJs 模块输出的是一个值的拷贝,ES6模块输出的是值得引用 CommonJS 是单个值导出, ES6可以导出多个CommonJS是动态语法可以写在判断里 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
require和import的区别
就是CommonJS和ES6module的区别
- 导入
require导出exports/module.exports是CommonJS的标准,通常适用范围如Node.js import/export是ES6的标准,通常适用范围如Reactrequire是赋值过程并且是运行时才执行,也就是同步加载require可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。import是解构过程并且是编译时执行,理解为异步加载import会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。- commonjs输出的,是一个值的拷贝,而es6输出的是值的引用
- commonjs是运行时加载,es6是编译时输出接口;
解决模块化的新思路 - 前端工程化
遗留
根本问题 - 运行时进行依赖分析
解决方案 - 线下执行
追问:可否简述,实现一个编译时依赖处理的思路
<!doctype html>
<script src="main.js"></script>
<script>
// 给构建工具一个标识位
require.config(__FRAME_CONFIG__);
</script>
<script>
require(['a', 'e'], () => {
// 业务逻辑
})
</script>
</html>
define('a', () => {
let b = require('b')
let c = require('c')
})
工程化实现
step1: 扫描依赖关系表
{
a:['b', 'c'],
b: ['d'],
e: []
}
step2: 根据依赖关系重制模板
<!doctype html>
<script src="main.js"></script>
<script>
// 构建工具生成数据
require.config({
"deps": {
a:['b', 'c'],
b: ['d'],
e: []
}
});
</script>
<script>
require(['a', 'e'], () => {
// 业务逻辑
})
</script>
</html>
define('a', () => {
let b = require('b')
let c = require('c')
})
step3: 执行工具,采用模块化解决方案处理
define('a', ['b', 'c'], () => {
export.run = () => {}
})
优点:
- 构建时生成配置,运行时去运行
- 最终转化成可执行的依赖处理
- 可以拓展