前端模块化

110 阅读6分钟

CommonJS

  1. 模块划分
const fs =      require('fs')      // ①核心模块
const sayName = require('./hello.js')  //② 文件模块
const crypto =  require('crypto-js')   // ③第三方自定义模块
  • ① nodejs 底层的核心模块
  • ② 我们编写的文件模块,比如上述 sayName。
  • ③ 我们通过 npm 下载的第三方自定义模块,比如 crypto-js。
  1. 模块识别
  • 首先像 fs ,http ,path 等标识符,会被作为 nodejs 的核心模块
  • ./ 和 ../ 作为相对路径的文件模块, / 作为绝对路径的文件模块
  • 非路径形式也非核心模块的模块,将作为自定义模块
  1. 模块的处理

优先从缓存中加载

缓存中没有的话,判断模块名有没有带路径,如果模块中有路径,加载文件模块

模块名没有路径,优先加载核心模块,如果不是核心模块,则加载第三方自定义模块

  • 核心模块的优先级仅次于缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载核心模块,加载过程中速度最快。
  • ./ ,../ 和 / 开始的标识符,会被当作文件模块处理,require方法会将路径转换成真实路径,并以真实路径作为索引。
    • require方法:
      1. 作为文件:找到该路径中父文件夹的目录,再查找同名文件 ,如果没找到,且没有后缀的话则尝试添加 .js.json或 .node拓展名查找。
      2. 作为文件夹:如果还是没有找到,会找到同名文件夹的目录,找到后,会找 package.json 下 main 属性指向的文件,如果没有则会继续查找 会找到同名文件夹下的index.js、index.json、index.node文件。如果依旧不存在,则会报错。
  • 自定义模块
    1. 在当前目录下的 node_modules 目录查找。
    2. 如果没有同名文件,则尝试添加 .js.json或 .node拓展名查找。
    3. 如果还是没有,会查找 同名文件夹 ,找到后,会找 package.json 下 main 属性指向的文件。如果没有,则会继续查找 index.js、index.json、index.node文件。
    4. 如果没有,在父级目录的 node_modules 查找,如果没有则沿着路径向上递归,直到根目录下的 node_modules 目录。
  1. 循环引用问题
  • a.js文件
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function(){
    const message = getMes()
    console.log(message)
}
  • b.js文件
const say = require('./a')
const  object = {
   name:'《React进阶实践指南》',
   author:'我不是外星人'
}
console.log('我是 b 文件')
module.exports = function(){
    return object
}
  • 主文件main.js
const a = require('./a')
const b = require('./b')

console.log('node 入口文件')

接下来终端输入 node main.js 运行 main.js,效果如下:

5.jpg

  • ① 首先执行 node main.js ,那么开始执行第一行 require(a.js)
  • ② 那么首先判断 a.js 有没有缓存,因为没有缓存,先加入缓存,然后执行文件 a.js (需要注意 是先加入缓存, 后执行模块内容);
  • ③ a.js 中执行第一行,引用 b.js。
  • ④ 那么判断 b.js 有没有缓存,因为没有缓存,所以加入缓存,然后执行 b.js 文件。
  • ⑤ b.js 执行第一行,再一次循环引用 require(a.js) 此时的 a.js 已经加入缓存,直接读取值。接下来打印 console.log('我是 b 文件'),导出方法。
  • ⑥ b.js 执行完毕,回到 a.js 文件,打印 console.log('我是 a 文件'),导出方法。
  • ⑦ 最后回到 main.js,打印 console.log('node 入口文件') 完成这个流程。

不过这里我们要注意问题:

  • 如上第 ⑤ 的时候,当执行 b.js 模块的时候,因为 a.js 还没有导出 say 方法,所以 b.js 同步上下文中,获取不到 say。

我用一幅流程图描述上述过程:

15.jpg

为了进一步验证上面所说的,我们改造一下 b.js 如下:

const say = require('./a')
const  object = {
   name:'《React进阶实践指南》',
   author:'我不是外星人'
}
console.log('我是 b 文件')
console.log('打印 a 模块' , say)

setTimeout(()=>{
    console.log('异步打印 a 模块' , say)
},0)

module.exports = function(){
    return object
}

打印结果:

6.jpg

  • 第一次打印 say 为空对象。
  • 第二次打印 say 才看到 b.js 导出的方法。

那么如何获取到 say 呢,有两种办法:

  • 一是用动态加载 a.js 的方法。

    延迟加载,用的时候再 require

  • 二个就是如上放在异步中加载。

AMD

  • AMD概念:

    • AMD是异步加载,推崇前置依赖,提前加载;下载完成后就直接执行依赖模块 (依赖模块的执行顺序和书写的顺序不一定一致 )。
    • 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行;
  • AMD规范:

    定义暴露模块:

    //定义没有依赖的模块
    define(function(){
       return 模块
    })
    
    //定义有依赖的模块
    define(['module1', 'module2'], function(m1, m2){
       return 模块
    })
    

    引入使用模块:

    require(['module1', 'module2'], function(m1, m2){
       使用m1/m2
    })
    

CMD

  • CMD规范 -CMD是异步加载,推崇就近依赖,加载完某个依赖模块后并不执行,只是下载而已,遇到 require 语句 的时候 才执行对应的模块 (模块的执行顺序就和书写的顺序一致 )

    定义暴露模块:

    //定义没有依赖的模块
    define(function(require, exports, module){
      exports.xxx = value
      module.exports = value
    })
    
    //定义有依赖的模块
    define(function(require, exports, module){
      //引入依赖模块(同步)
      var module2 = require('./module2')
      //引入依赖模块(异步)
        require.async('./module3', function (m3) {
        })
      //暴露模块
      exports.xxx = value
    })
    

    引入使用模块:

    define(function (require) {
      var m1 = require('./module1')
      var m4 = require('./module4')
      m1.show()
      m4.show()
    })
    

ESM

  • 概念:

    • ESM是异步加载,编译时输出接口,输出的是值的只读引用
    • 遇到模块加载命令 import,就会生 成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载 的那个模块里面去取值。
  • 优势:

    • 借助 Es Module 的静态导入导出的优势,实现了 tree shaking
    • Es Module 还可以 import() 懒加载方式实现代码分割。

    定义暴露模块:

    export default function add() {};
    export default {};
    
    export const name = 'xx';
    export {};
    export function add() {};
    
    // 重命名
    const age = 18;
    export { age as myAge };
    
    • export default xx module 的默认导出。 xx 可以是函数、对象、变量。
    • export xx, xx 可以是变量的声明、对象。

    注意:default 是 export 的语法糖,是导出对象的一个默认属性,相当于 export { default: 'xx' } 或者 export const default = 'xx';

    引入使用模块:

    import module from 'xx'; // module 为 export default 导出的内容;
    import { name } from 'xx'; // name export 导出的内容;
    
    // 混合导出
    import module, { name } from 'xx';
    
    // 全部导出
    import * from 'xx'; // module 为 export + export default 导出的内容;
    
    // 重命名
    import { name as MyName } from 'xx';
    
  • 循环引用问题

    遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。

    a.mjs

    
    import {bar} from './b.mjs';
    export function foo() {
      bar();  
      console.log('执行完毕');
    }
    foo();
    

    b.mjs

    import {foo} from './a.mjs';
    export function bar() {  
      if (Math.random() > 0.5) {
        foo();
      }
    }
    

    main.mjs

    import * as a from './a.mjs';
    
    // 1. moduleA: 导出 foo
    // 2. moduleA:执行 foo();
    // 3. moduleA:执行 bar()
    
    // 4. 开始执行 b模块
    // 5. moduleB:导出 bar
    
    // 6. moduleA:调用 bar()
    // 7. moduleA:根据 Math.random() > 0.5 判断 是否调用foo,如果为 true 调用 foo(), 执行 2,3,4,5,6
    // 8. moduleA:如果 Math.random() > 0.5 为false, 执行  ’console.log('执行完毕');', 并跳出调用栈。