image

模块化的理解

模块化这件事,无论在哪个编程领域都是相当常见的事情,模块化存在的意义就是为了增加可复用性,以尽可能少的代码是实现个性化的需求。

模块化的计划进程

  • 全局 function 模式:将不同的功能封装成不同的全局函数
function myModule1 () {}
function myModule2 () {}
  • namespace模式:简单对象封装
let myModule = {
  msg: 'hello world',
  say() {
    console.log(this.msg)
  }
}
  • IIFE模式:匿名函数自调用
// myModule.js
;(function (window) {
    let msg = 'hello world'
  
  function say () {
    console.log(msg)
  }
  
  window.myModule = { say }
})(window)

// 另一个js
myModule.say()
  • IIFE模式增强:引入依赖(现代模块实现的基石)
;(function (window, $) {
    let msg = 'hello world'
  
  function say () {
    $('body').css('background', 'red')
    console.log(msg)
  }
  
  window.myModule = { say }
})(window, jQuery)

多个 script 很麻烦!

这样子html需要引入多个 script 标签,不仅导致请求数增多 ,还需要我们熟知不同模块的以来顺序,顺序错了就会出错。这些问题可以通过模块化规范来解决。

模块化规范

模块化规范有commonJS、AMD、CMD、ES6模块化

CommonJS

Commonjs是一套用于服务端运行的标准,除了造成最大影响的模块化方案,还包括:IO操作、二进制字符串、进程管理。

后来,nodejs出现,直接采用了 commonJS 的模块化规范,同时还带来 npm(Node Package Manageer)。

在服务端,CommonJS 的模块加载是同步的,因为服务端是从磁盘中或内存中直接读取,所以耗时基本可忽略。浏览器端也想实现这个模块化规范,但这种在模块加载过程中向服务端请求模块代码的过程会很影响体验。

于是乎,业界就诞生了一系列解决方案:AMD、CMD、打包工具(Component、Browserify、Webpack)。

// add.js
const add = (a, b) => a + b
module.exports = add
// index.js
const add = require('./add')
add(1, 5)

AMD规范:RequireJS

RequireJS 是 AMD 规范的代表之作,它定义一个define方法用来定义和加载模块,后期又扩展了一个require方法来加载模块,解决了CommonJS中一个文件只能暴露一个模块的问题。

  • 模块定义
define(id?, dependencies?, factory)

if (typeof define === 'function' && define.amd) {
  define('jquery', [], function {
    return jQuery       
  })
}
  • 模块信息配置
require.config({
  paths: {
    jQuery: 'https://code.jquery.com/jquery-3.4.1.js'
  }
})
  • 依赖模块加载与调用
require('jQuery', function ($) {
    $('#app')
})

CMD规范:sea.js

和 AMD 一样,都是通过 script 标签加载模块。

但相比于 AMD 的异步加载,CMD 更加倾向于懒加载

CMD通过一个define包装函数来完成功能,通过下面代码可以看到,只有require模块或者调用seajs.use()的时候,模块才运行。

define('a', function (require, exports, module) {
    console.log('a load')
  exports.run = function () console.log('a run')
})

define('main', function (require, exports, module) {
    console.log('main load')
  var a = require('a')
  a.run()
})

seajs.use('main')

//main load
// a load
// a run

ES Modules

2015年,ES6推出了官方的模块化规范,相比于CommJS、AMD、CMD,ESM采用了完全静态化的方式进行模块加载。

模块导出

export const function func = () => {}
export const name = ''
// 或
const function func = () => {}
const name = ''
export { func, name }
// 或
const name = ''
export default name

模块导入

import { name, func } form './m.js'
// 或
import * form './m.js'
// 或
import * as mod form './m.js'
// 或
import { name, func as add } form './m.js'
// 或
import name from './m.js'
// 或
import name, {func} from './m.js'

同时导入导入

import { name, getName } from './module_d.js'
export { name, getName }
// 可以简写成
export { name, getName } from './module_d.js'

ESM和CJS的区别

cjs 输出的是值的拷贝,import的值不会跟着模块内的值变化,ESM输出的是值的引用(只读,不能重新赋值),模块内的值变化,import加载的值也会跟着变

// counter.js
var counter = 3
function incCounter() {
  counter++
}
function getCounter() {
  return counter
}
module.exports = {
  counter,
  incCounter,
  getCounter
}

// app.js
const { counter, incCounter, getCounter } = require('./counter')

// cjs 输出的是值的拷贝,counter是基本类型
// 所以调用 incCounter 修改的是模块内的 counter
// 当前模块的 counter 读取的还是模块缓存的值,不会改变
// 但可以通过函数来获取模块内的 counter
console.log({ counter }, getCounter())
incCounter()
console.log({ counter }, getCounter())
// counter.mjs
var counter = 3
function incCounter() {
  counter++
}
function getCounter() {
  return counter
}

export { counter, incCounter, getCounter }
export default counter;

import defaultCounter, { counter, incCounter, getCounter } from './counter.mjs'

// ESM 输出的是值的引用(只读,不能重新赋值)
// 模块所在的位置读取变量的时候
// 会去读取变量所在模块的值
// 所以原始值变了,import 加载的值也会变
console.log({ counter, defaultCounter }, getCounter())
incCounter()
console.log({ counter, defaultCounter }, getCounter())

CJS 模块是运行时加载,ESM 模块是编译时输出接口

CJS 模块是同步加载,ESM 模块的 import 是异步加载,有一个独立的模块依赖的解析阶段(在词法解析阶段,先构建模块依赖图,如果遇到不存在的模块,会提前报错)

Node.js 的模块加载方法

  • node.js 默认只支持 cjs,可以将文件的后缀名从.js改成.mjs即可支持 ESM。

  • 或者在 package.json 指定 { type: 'module' },这样该项目内的 JS 脚本会被解释成 ES6 模块,否则默认为 cjs

  • main字段可以指定模块加载的入口,{ "main": "./src/index.js" }

  • export 字段优先级高于main字段

    • 可以指定子目录别名:{ "exports": { "submodule": "./src/submodule.js" } }
    • main的别名:{ "exports": { ".": "./src/index.js" } 相当于{ "exports": "./src/index.js" }
    • 条件加载,为ES6模块和cjs模块制定不同入口{ exports: { "require": './src/index.cjs', "default": './src/index.js' } }

cjs加载ES6模块

require()不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await命令,导致无法被同步加载。

(async () => {
  await import('./my-app.mjs');
})();

ES6模块加载cjs模块

只能整体加载,不能只加载单一的输出项。

// 正确
import packageMain from 'commonjs-package';

// 报错
import { method } from 'commonjs-package';

循环加载

  • cjs会缓存加载的模块,后面再require,不会再执行模块,而是返回缓存的结果,除非手动清除系统缓存
  • ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

参考:

https://my.oschina.net/u/4088983/blog/4773292

https://es6.ruanyifeng.com/#docs/module-loader#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%8A%A0%E8%BD%BD