JS 模块化

111 阅读6分钟

JS模块化发展背景与历史变迁

无模块化 --> 语法侧优化IIFE --> 框架层面CommonJS --> 异步AMD --> 按需加载CMD
--> 工程化、组件化

背景

JS本身简单的页面设计:页面动画 + 表单提交并无模块化 or 命名空间的概念

JS的模块化需求日益增长

幼年期:无模块化时期

  1. 开始需要再页面中增加一些不同的JS文件:动画、表单、格式化

  2. 多种js文件为了可读性和可维护性被分在不同的文件中

  3. 不同的文件又被同一个模版引用

    <script scr='jquery.js'></script><script scr='main.js'></script><script scr='dep1.js'></script>
    // ....
    

    此方式是被认可的:

    文件分离是模块化的第一步

    问题出现:

    • 污染全局作用域 > 不能写重名函数等造成冲突 => 不利于大型项目的开发以及多人团队的共建

成长期:模块化的雏形 - IIFE(语法侧的优化)

本质是:作用域的把控

🌰:

// 定义一个全局变量
let conut = 0;
// 代码块1
const increase = () => ++count;
// 代码块2
const reset = () => {
  count = 0;
}
increase();
reset();
// 下一次调用时,总会被reset

利用函数块级作用域

(() => {
  let count = 0;
  // ...
})

仅定义了一个函数,如果立即执行

(() => {
  let count = 0;
  //...
})();

初步实现了最最简单的模块

利用此思想尝试去定义一个最简单的模块

面试题:利用函数作用域控制一个简单的模块

// 封装:
const iifeModule = (() => {
  let count = 0;
  return {
    increase: () => ++count;
    reset: () => {
      count = 0;
    }
  }
})();
// 使用:
iifeModule.increase();
iifeModule.reset();
  • 对外暴露了方法
  • 变量没有污染全局作用域

追问:有额外依赖时,如何优化IIFE相关代码? ==> 参数调配

优化1: 依赖其他模块的IIFE

const iifeModule = ((dependencyModule1, dependencyModule2) => {
  let cont = 0;
  return {
    increase: () => ++count;
    reset: () => {
      count = 0;
    }
  }
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();

源于jquery的解决办法

面试:了解早期jquery的依赖处理以及模块加载方案嘛?/ 了解IIFE是如何解决多方依赖的问题

答:IIFE + 传参调配

实际上,jquery等框架其实应用了revealing的写法:思想上为强调API的使用方式和接口,不会暴露内部内容 revealing 模块模式:只返回一个对象,其属性是私有数据和成员的引用

const iifeModule = ((dependencyModule1, dependencyModule2) => {
  // 内部内容不会被第三方知道
  let cont = 0;
  
  const increase = () => ++count;
  const reset = () => {
      count = 0;
  }
  
  // 只暴露接口:
  return {
    increase, reset
  }
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();

成熟期:

CJS - CommonJS

Node.js制定的一套方案,特征:

  • 通过module + exports 对对外暴露接口
  • 通过require来调用其他模块

模块组织方式:三大块:引入、处理、暴露

main.js文件

// 引入部分
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');

// 处理部分
let cont = 0;
 
const increase = () => ++count;
const reset = () => {
  count = 0;
}
// 做一些跟引入依赖相关事宜...

// 暴露接口部分
exports.increase = increase;
exports.reset = reset;

module.exports = {
  increase, reset;
}

模块使用方式:

const { increase, rest } = require('./main.js')

increase();
reset();

可能被问到的问题:

实际执行处理:

应用场景:公司减少第三方依赖,自己写框架

(function (thisValue, exports, require, module) {
  const dependencyModule1 = require('./dependencyModule1');
	const dependencyModule2 = require('./dependencyModule2');
  // 业务逻辑...
}).call(thisValue, exports, require, module);
  • 优点:CommonJS率先在服务端实现了,从框架层面解决依赖、全局变量污染问题
  • 缺点:CommonJS主要针对了服务端的解决方案 --> 同步引入加载,对于异步拉取依赖的处理不是那么友好

追问:为什么没有?

同步加载是没有问题的,比较快,硬盘端读写

优化异步:import require require.ensure

新的问题 -- 异步依赖

AMD规范

通过异步加载 + 允许制定回调函数

经典实现框架:require.js

新增定义方式:提供了两种方式:define,require

// 通过define来定义一个模块,利用require 进行加载
/*
	define
	pramas: 模块名,依赖模块,工厂方法
*/
define(id, [depends], callback);
require([module], callback);

模块定义方式:依赖1、依赖2加载完成之后再去走cb业务逻辑,即便是异步

define('amdModule', ['dependencyModule1', 'dependencyModule2', (dependencyModule1, dependencyModule2) => {
  // 业务逻辑
  let cont = 0;
 
  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }
  
  return {
    increase, reset
  }
})

引入模块:引入的部分也是正常处理

require(['amdModule'], amdModule => {
  require.increase();
})

面试题2: 如果在AMDmodule中想兼容已有代码,怎么办?

答:IIFE可以,也可以:

define('amdModule', [], require => {
  // 引入部分
  const dependencyModule1 = require(./dependencyModule1);
  const dependencyModule2 = require(./dependencyModule2);

  // 处理部分
  let cont = 0;

  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }
  // 做一些跟引入依赖相关事宜...

  return {
    increase, reset
  }
})

面试题3: AMD中使用revealing?

// define(id, [], (require, export, module))
define((require, export, module) => {
   // 引入部分
  const dependencyModule1 = require(./dependencyModule1);
  const dependencyModule2 = require(./dependencyModule2);

  // 处理部分
  let cont = 0;

  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }
  // 做一些跟引入依赖相关事宜...

  export.increase = increase();
  export.reset = reset();

  module.exports = {
    increase, reset;
  }
})

define(id, [], require => {
  const otherModule = require('amdMnodule');
  otherModule.increase();
  otherModule.reset();
})

面试题4:兼容AMD&CJS / 如何判断是AMD还是CJS

判断条件中 typeof module === "object" && module.exports 判断是否是模块化的东西;二者区别在于CJS没有define方法

UMD的出现

// AMD define
(define('amdModule', ['dependencyModule1', 'dependencyModule2', (dependencyModule1, dependencyModule2) => {
  // 业务逻辑
  let cont = 0;
 
  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }
  
  return {
    increase, reset
  }
}))(
	// 目标是一次性区分CJS和AMD,一个异步函数,一个直接引入
  typeof module === "object"
  	&& module.exports
  	&& typeof define !== "function" // 只有AMD用define去定义异步模块
  		? // 是CommonJS,直接执行工程函数
  			factory => module.exports = factory(require, exports, moudle)
  		: // 是AMD
  			define
) // 立即执行,完整的操作
  • 优点:适合在浏览器中加载异步模块,可以并行加载多个

  • 缺点:会有引入成本,不能按需加载

CMD 规范

按需加载:代码A执行的时候再加载依赖A

主要应用的框架:sea.js

define('module', [], (require, exports, module) => {
  // 不在初始化的时候加载依赖,而是使用的时候加载
  let $ = require('jquery');
  // jquery 相关逻辑
  
  let dependencyModule1 = requre('./dependencyModule1');
  // dependencyModule1相关逻辑
})
  • 优点:按需加载,依赖就近
  • 缺点:依赖于打包,同时加载逻辑存在于每个模块中,会扩大模块的体积

面试题5: AMD & CMD区别

答:依赖就近,按需加载

ES6模块化

走向新时代

新增定义:

引入关键字 -- import

到处关键字 -- export

模块引入、导出和定义的地方:

// 引入区域
import dependencyModule1 from './dependencyModule1.js'
import dependencyModule2 from './dependencyModule2.js'

// 实现代码逻辑
let count = 0;
const increase = () => ++count;
const reset = () => {
  count = 0;
}

// 导出区域
// 方式1:
export const incrase = () => ++count;
export const reset = () => {
  count = 0;
}
// 方式2:
export default { increase, reset }

模版引入的地方:

<script type='module' scr='esModule.js'></script>

node中:

import { increase, reset } from './esModule.mjs';
increase();
reset();

import esModule from './esModule.mjs'
esModule.incrase();
esModule.reset();

面试题6: import如何动态模块

本质上是在考察export promise

在new promise 中的回调去做

ES11原生解决方案:

import('./esModule.js').then(dynamicEsModule => {
  dynamicEsModule.increase();
})
  • 优点(重要性):通过一种最统一的形态去整合了JS的模块化
  • 缺点(局限性):本质上还是做了一个运行时的依赖分析

解决模块化的新思路 - 前端工程化

背景

根本问题 - 运行时进行依赖分析

导致结果:前端的模块化处理方案依赖于运行时分析

解决方案:线下执行 - 打包/预编译的方式

grunt gulp webpack

假设:有个文件:

<!doctype html>
  <script src="main.js"></script>
	<script>
    // 给构建工具一个标示位,可以替换的东西
    require.config(__FRAME_CONFIC___)
  </script>
	<script>
    require(['a', 'e'], ()=> {
    // 业务处理
  })
	</script>
</html>

解析处理:

define('a', () => {
  let b = require('b');
  let c = require('c');
  
  export.run = () {
    // run
  }
})
工程化实现

Step1: 扫描依赖关系表:

{
  a: ['b', 'c'],
  b: ['d'], // 假设依赖d
  e: [] // 没依赖或者全局依赖
 }

Step2:重新生成依赖数据模版:

<!doctype html>
  <script src="main.js"></script>
	<script>
    // 构建工具生成数据
    require.config({
    "deps": {
      a: ['b', 'c'],
      b: ['d'], // 假设依赖d
      e: [] // 没依赖或者全局依赖
    }
  })
  </script>
	<script>
    require(['a', 'e'], ()=> {
    // 业务处理
  })
	</script>
</html>

Step3:执行工具,采用模块化方案解决模块化处理依赖

define('a', ['b','c'], () => {
  // 执行代码
  export.run = function () {}
})

优点:

  1. 构建时生成配置,运行时执行
  2. 最终转化成执行处理依赖
  3. 可以拓展

预编译不再存在异步

完全体:webpack为核心的工程化 + MVVM框架组件化 + 设计模式