JS模块化(0403)

165 阅读8分钟

20220411110809.jpg

前端模块化最主要的就是解决作用于和上下文的问题

幼年期:无模块化

  1. 开始需要在页面中加载不同的JS:动画、组件、格式化
  2. 多种js文件会被分在不同的文件中
  3. 不同的文件又被同一个模板所引用
  <script src="jquery.js"></script>
  <script src="main.js"></script>
  <script src="dep1.js"></script>

认可: 文件分离拆分是最基础的模块化(第一步)

script 标签的参数 - async & defer

22222.png

总结: 普通 - 解析到立即阻塞,立刻下载执行当前script async - 解析到标签开始异步下载,解析完之后开始执行 defer - 解析到标签开始异步下载,下载完成后开始执行并且阻塞渲染,执行完成之后继续渲染

问题出现:

  • 污染全局作用域 => 不利于大型项目的开发以及多人团队的共建
  1. 兼容性 > IE9
  2. 问题可以被引导到 => 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的区别

  1. 导入require 导出 exports/module.exportsCommonJS 的标准,通常适用范围如 Node.js
  2. import/exportES6 的标准,通常适用范围如 React
  3. require赋值过程并且是运行时才执行,也就是同步加载
  4. require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  5. import解构过程并且是编译时执行,理解为异步加载
  6. import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。
  7. commonjs输出的,是一个值的拷贝,而es6输出的是值的引用
  8. 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 = () => {}
  })

优点:

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

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