js模块化历史

123 阅读5分钟

js模板化

课程目标

  • js模块历史
  • 不同模块化原理和实现
  • 相关面试题

知识点

  • 无模块化
  • IIFE
  • CommonJs
  • AMD & CMD & UMD
  • ES6
  • 现代型模块化?
  • 模块化跟工程化?
  • 模块化跟组件化?
  • 模块化与设计模式?

无模块化

JS诞生之初面向简单页面开发,没有模块的概念。

  1. 开始需要在页面中增加一些不同的js:动画、表格、格式化。
  2. js文件的顺序不能乱放,被依赖的文件放在前面,比如jquery.js要放在最前面。各个js文件的依赖关系不明显。
<script src='jquery.js'></script>
<script src='main.js'></script>
  • 问题出现:污染全局作用域 => 不利于大型项目的开发以及多个团队的共建,有可能出现变量及方法重命名覆盖的问题。接着往下看。

IIFE

  • (Imdiately Invoked Function Expression),立即执行的函数表达式 作用域的把控。举个栗子:
let count = 0;
const increase = () => ++count;
const reset = () => {
    count = 0;
}
increase();
reset();

两个方法调用同一个count属性,reset()会把increase()相加的值清空,可以利用函数块极作用域来保证调用increase()方法时,count=1,不会被其他方法覆盖。

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

下面来初步实现一个模块

const iifeModule=(() => {
    let count = 0;
    return {
        inrease: () => ++ count,
        reset: () => {
            count = 0;
        }
    }
})();
// 上面只是一个函数表达式 调用inrease和reset方法
iifeModule.inrease();
iifeModule.reset();

面试题一

  • 如果依赖其他模块如何优化IIFE代码? 优化:依赖其他模块的IIFE,比如dependencyModule1,dependencyModule2就是其他模块
const iifeModule=((dependencyModule1,dependencyModule2) => {
    let count = 0;
    return {
        inrease: () => ++ count,
        reset: () => {
            count = 0;
        }
    }
})(dependencyModule1,dependencyModule2);

iifeModule.inrease();
iifeModule.reset();

面试题二

  • 了解早期jquery的依赖处理以及模块加载方案吗? | 了解传统IIFE如何解决多方依赖的问题? 答:IIFE加传参调配。就是上面代码所示。实际上jquery等框架其实应用了revealing(揭示模式)的写法。

CommonJS

  • CommonJS标志了JavaScript模块化编程正式诞生,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限。CommonJS属于服务端模块。node.js的模块系统,是参照CommonJS规范实现的。 特征:
  • 通过module + exports去对外暴露接口
  • 通过require来调用其他模块 模块组织方式
//  main.js文件
// 引入部分
 const dependencyModule1 = require('./dependencyModule1');
 const dependencyModule2 = require('./dependencyModule2');
//  处理部分
 let count =0;
 const increase = () => ++count;
 const reset = () => {
     count = 0;
 }
//  暴露接口部分
 exports.increase =  increase;
 exports.reset = reset;

//  moudule.exports = {
//      increase,reset
//  }

// main.js模块文件使用
const {increase,reset} = require('./main.js');
increase();
reset();

面试题

  • 用立即执行函数实现上面代码,thisValue代表当前值
(function(thisValue,exports,require,module){
    const dependencyModule1 = require('./dependencyModule1');
    const dependencyModule2 = require('./dependencyModule2');
    
    // 逻辑代码...
}.call(thisValue,exports,require,module));

CommonJS优点:

  • 率先在服务端实现了,从框架层面解决依赖、全局变量污染的问题。 CommonJS缺点:
  • 主要针对了服务端的解决方案。服务端的文件都是保存在本地硬盘或云盘上,可以同步加载完成,等待时间就是硬盘的读取时间,但对于浏览器来说,模块放在服务器端,等待时间取决于网速的快慢。对于异步拉取依赖的处理整合不是那么友好

AMD

  • AMD(Asynchronous Module Definition)异步模块定义
  • 经典实现框架是:require.js
  • 通过异步加载 + 允许制定回调函数
  • 通过define来定义一个模块,然后require
  • 语法:
 define(id?,dependencies?, factory)
 require([module],callback)
  • id:模块名,是个字符串,指的是定义中模块中的名字,参数是可选的,如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字
  • dependencies:定义中模块所依赖模块的数组,参数是可选的,定义了三种特殊的依赖关键字,如果“require”、“exports”、“module”出现在依赖列表中,参数应该按照CommonJS模块规范自由变量去解析
  • factory:为模块初始化要执行的函数或对象,如果为函数,应该只被执行一次,如果是对象,此对象应该为模块的输出值。 栗子:
// 模块定义方式
define('amdModule',['dependencyModule1','dependencyModule2'],(dependencyModule1,dependencyModule2) => {
    // 逻辑代码
    const increate = () => console.log
    return {
        increate
    }
})
// 引入模块 
require(['amdModule'],amdMoudle => {
    amdMoudle.increase();
})
  • AMD优点:适合在浏览器中加载异步模块,可以并行加载多个模块
  • AMD缺点:会有引入成本,不能按需加载

面试题一

  • 如果在AMDmodule中想兼容已有代码,怎么办? define的回调函数已经提供require去兼容已有代码
define('amdModule', [], require => {
    //  处理部分
    let count =0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
    }
    return {
        increase,
        reset
    }
})

面试题二

  • AMD中使用revealing
define('amdModule', [], (require, exports, module) => {
    //  处理部分
    let count =0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
    }
    exports.increase = increase();
    exports.reset = reset();
})
define('module1', [], require => {
    // 使用方法
    const ontherModule = require('amdModule');
    ontherModule.increase();
    ontherModule.reset();
})

面试题三

  • 兼容AMD&CommonJS(UMD) | 如何判断是 CommonJS?
(define('amdModule', [], (require, exports, module) => {
    //  处理部分
    let count =0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
    }
    exports.increase = increase();
    exports.reset = reset();
}))(
    // 区分是CommonJS or AMD
    typeof module === 'object' && module.exports && typeof define !=='function' ?
    // 是CommonJS
    factory => module.exports = factory(require, exprots, moudle)
    :
    // 是AMD
    define('moduleName',[],require => {})
)

CMD

  • 按需加载,
  • 主要应用的框架:sea.js
  • CMD优点:按需加载,依赖就近
  • CMD缺点:依赖于打包,加载逻辑存在于每个模块中,扩大模块体积 栗子:
define('admModule', (require, exports, module) => {
    // 引入部分
    const dependencyModule1 = require('./dependencyModule1');
    //dependencyModule1相关逻辑...
    const dependencyModule2 = require('./dependencyModule2');
    //dependencyModule2相关逻辑...
})

面试题

  • AMD&CMD区别? 答:依赖就近,按需加载,AMD里的require分全局和局部,CMD里没有全局的require,对于依赖的模块AMD是提前执行,CMD是延迟执行,,不过RequireJS从2.0开始,也改成可以延迟执行。CMD推崇依赖就近,AMD推崇依赖前置。

UMD(Universal Module Definition)

  • 通用的模块化系统,兼容AMD和CommonJS,保证模块可以被AMD和CommonJS调用。

ES6

  • 引入关键字:import
  • 卖出关键字:export
//esModule.js
 //  处理部分
 let count =0;
 export const increase = () => ++count;
 export const reset = () => {
     count = 0;
 }
  
 export default { 
    increase,
     reset
 }
 // html中js引入方式
<script type="moudle" src="esModule.js"/>

// node中使用 mjs是node里定义模块的文件后缀
import { increase, reset} from './esModule.mjs'
increate();
reset();
// 两个方式都可以
import esModule from './esModule.mjs'
esModule.increate();
esModule.reset();
  • ES6优点:通过一种最统一的形态整合了js的模块化
  • ES6缺点:局限性,本质上还是运行时的依赖分析

面试题

  • import解决动态模块
// ES11原生解决方案
import('./esModule.js').then(dynamicEsModule => {
    dynamicEsModule.increate();
}) 
  • 解决模块化的新思路--前端工程化
  • 前端的模块化处理方案依赖于运行时分析
  • 解决方案:线下执行:grunt gulp webpack