前端模块化

187 阅读6分钟

模块化的作用

把项目可以按照功能,模块等进行拆分便于维护和阅读;
防止全局变量的污染;
使用者无需关注模块内部的实现,只需关注使用

初期发展历程

初期何如防止全局变量的污染

使用IIFE立即执行函数,根据块级作用域把变量进行隔离

    (() => {
        let a = 99
        ....
    })()

独立模块本身之外的依赖,如何优化

通过传参的形式把依赖的模块传递进去

    (($) => {
        let a = $('.a')
        ....
    })(jquery)

jquery或vue或react等其他开源框架的模块化加载方案

jquery通过IIFE实现模块化,通过对外暴露对象的形式供外部使用
vue和react是通过实例化的形式实现模块化

   ((window) => {
        return jQuery
    })(window)

为什么有些开源框架需要传入全局对象和自身的引用,最后一个参数定义为undefined?

  • 全局对象:作为参数传递进去,这样在访问这个对象的时候直接从块级作用域中获取,不用去全局作用域中获取,提高了访问效率
  • 自身的引用:防止引用错乱
  • 定义undefined: 防止外部修改

什么是揭示模式(revealing)?

只关注使用不关注内部的具体实现

成熟期发展历程

CJS-CommonJs 同步模块加载

  • 应用:node环境
  • 使用方法:
    引入:
    require('路径')
    导出:
    module.exports = {} 整体导出
    exports // 单独导出
    const a = require('./a.js')
    const b = '12'
    module.exports = { b }
    exports.c = function () {} // 单独导出和整体导出同时使用单独导出无效
  • 特点: 同步模块加载
    依赖延迟加载,哪里需要就在哪里引入,提高效率
    const a = require('./a.js')
    if (a) {
        const b = require('./b.js')
    }
  • 优点 解决了依赖和全局变量污染的问题
  • 缺点 无法加载异步模块
  • 注意事项:
    node中默认开启了缓存,在引用过一次之后再次引用都是直接从缓存中获取
    const a = require('./a.js')
    const aa = require('./a.js')
    a === aa // true
  • 原理:
    通过模块路径找到本地文件进行读取;
    将文件中的代码放入到函数中执行,并且执行的结果module.exports作为require函数的执行结果返回
    (function (module) {
        module.exports = {}
        cosnt exports = module.exports
        ...
        return module.exports
    })(module)
  • 面试题: 下面的输出结果?
 // 导出文件a.js
 exports.a = '123'
 module.exports = { b: 22}
 // b.js 导入
 const u = require('./a.js)
 console.log(u.a) // ?   undefined

AMD-异步模块加载

  • 应用: require.js
  • 使用方法: require是加载模块,defined是定义模块;

加载模块中引入文件

reuqire([依赖的文件],回调函数)

定义的模块中引入异步文件:

defined(模块名, [依赖文件路径], 回调函数)

定义的模块中引入同步文件:

defined('模块名',(require,exports,module) => {
    require(文件路径)
})
  • 特点:

同步和异步模块都可以加载,它并不会阻塞它后面代码的执行,因为它的异步执行是放在回调函数中,等到依赖加载完成之后才会执行这个回调函数;

  • 优点

适合在浏览器环境加载异步模块

  • 缺点

异步模块无法按需加载,必须提前加载所有模块,不管模块中是否使用到都会被提前加载(AMD推崇依赖前置,提前执行),

  • 注意事项:

会有缓存,多个地方引入同一个文件,只会被引入一次

  • 原理:

异步加载 + 回调函数

  • 面试题:

如果在amd模块中想兼容已有代码,怎么处理?

    // 直接同步引入
    define('a', [], require => {
      const b = require('./b.js')
    })

CMD-按需就近加载

  • 应用:sea.js
  • 使用方法:
    define(模块名,(require, exports, module) => {
        // 引入同步
        const a = require('./a.js')
        // 引入异步
        const b = require.async('./b.js')
    })
  • 特点: 按需就近加载
  • 优点: 按需加载,依赖就近
  • 缺点: 依赖打包,引入的依赖需要在模块逻辑中
  • 面试题:
  1. 手写兼容CJS和AMD和CMD的函数
((root, factory) => {
    if (typeof exports === 'function' && typeof module === 'object') { // CJS
        const a = require('a.js')
        module.exports = factory(a)
    } else if (typeof define === 'function' && define.AMD) { // amd
        define('c', ['./a.js'], (a) => {
             return factory(a)
        })
    } else if (typeof define === 'function' && define.CMD) { // cmd
        define('c', (require, exports, module) => {
            const a = require('a.js')
            module.exports = factory(a)
        })
    } else {
        root.UMD = factory(root.a)
    }
})(root, factory)

UMD就是commonjs和AMD的兼容写法,以上代码中去除CMD相关的代码就是UMD;

  1. 说说AMD和CMD的区别?
    AMD依赖前置,提前加载依赖;CMD按需加载依赖,依赖就近

ESM-es6模块化

  • 应用 目前前端工程化中基本使用ESM
  • 使用方法: 引入:
    import + {}或名称 from 路径 或 路径
    必须放在模块中所有代码的前面
    可以使用as进行重命名
    可以直接使用*表示导入所有形成一个对象
    可以直接写路径表示执行这个模块

导出:
export + 声明式语句 或 {} // 基本导出
export default + 变量 // 默认导出
一个模块中只能有一个默认导出,默认导出和基本导出可以同时存在

// as进行重命名
import a as aa from './a.js'
// 导入所有
import * as obj from './b.js'
// 导入执行
import './c.js'
// 导入默认和基本
import a, { b } from './c.js'

// 基本导出
export const a = 1
// 只能导出声明式语句,否则报错
const b = 2
export b // 报错
// 默认导出
export default { c: 3 }
  • 特点: 依赖声明(在所有代码之前声明)
  • 优点: 明确依赖关系,便于阅读和维护
  • 缺点:依赖包需要放在项目中,无法通过链接引入

esm是依赖编译时,在编译时就会加载引入,而不是在运行时,因此无法按条件引入,必须在顶部引入;

ESM和CommonJS的区别
  1. CommonJS输出的是一个值的拷贝,而ESM是一个值的引用,并且CommonJs有缓存,而ESM没有缓存;当CommonJs引入的依赖变化的时候,它之前引入的值是不会跟着变化;而ESM引入的依赖变化的时候,它之前的引入的值会随之变化;
  2. CommonJS是运行时加载,ESM是编译时加载;

总结:
起初通过IIFE立即执行函数的块级作用域解决了全局变量的污染问题,依赖的模块通过传参的形式进行引入,把要提供外部使用的功能进行返回暴露,通过揭示模式,使用者仅仅关注抽象使用,不需要关注模块内部的具体实现,后面又出现了CommonJS,AMD,CMD和ESM,CommonJS是服务器nodejs中的规范,它仅仅可以同步加载模块,而浏览器需要异步加载模块,为了解决这个问题就出现了AMD,AMD既可以同步加载也可以异步加载模块,因为具有回调函数,但是AMD无法进行异步按需加载,后面又出现了CMD,CMD也是可以加载同步和异步模块,并且可以按需加载,它的特点就是按需加载依赖就近,为了规范进行统一就出现了EMS模块化,目前前端工程化中基本都使用EMS模块化