Skip to content
导航栏

JavaScript模块化

1. 什么是模块化

  • 初始的JavaScript是没有模块化的, 因此不同文件声明同一个变量会导致变量被覆盖

1.1 require细节

  • require是同步加载模块, 会阻塞后面代码的执行, 直到模块加载完毕
  • 查找规则
    • 情况1:X是一个Node核心模块
      • 比如path、http: 直接返回核心模块,并且停止查找
    • 情况2: X以"./"或者"../"开头
        1. 如果有后缀名,按照后缀名的格式查找对应的文件
        1. 如果没有后缀名,会按照如下顺序
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
        1. 没有找到对应的文件,将X作为一个目录,查找目录下的index文件
        • 查找index.js文件
        • 查找index.json文件
        • 查找index.node文件
    • 情况3: 直接是一个X(没有路径),并且X不是一个核心模块
      • 从绝对路径查找

2. CommonJS

2.1 什么是commonJs

  • commonJS是一种模块化规范, 用于服务器端的模块化, 由于服务器端的模块文件都存放在本地磁盘, 所以加载速度很快, 不需要考虑异步加载的问题, 所以commonJS规范就是同步加载模块
  • commonJS规范规定, 每个模块内部, module变量代表当前模块, 这个变量是一个对象, 它的exports属性是对外的接口, 加载某个模块, 实际上是加载该模块的module.exports属性
  • 一个模块想要对外暴露变量(函数也是变量), 可以用module.exports = variable; 一个模块要引用其他模块暴露的变量, 用var ref = require('module_name'); 引入的变量就是那个模块的module.exports属性
  • commonJS模块的特点如下:
    • 所有代码都运行在模块作用域, 不会污染全局作用域
    • 模块可以多次加载, 但是只会在第一次加载时运行一次, 然后运行结果就被缓存了, 以后再加载就直接读取缓存结果, 要想让模块再次运行, 必须清除缓存
    • 模块加载的顺序, 按照其在代码中出现的顺序
js
// a.js
var a = 1;

function add() {
  a++;
}

function get() {
  console.log(a);
}

module.exports = {
  add: add,
  get: get
}

// b.js

var a = require('./a.js');

a.add();
a.get();

// c.js

var a = require('./a.js');

a.get();
  • 上述代码中, a.js是一个模块, 里面有一个变量a, 还有两个方法add和get, add方法是对变量a进行自增, get方法是打印变量a的值, 然后通过module.exports将这三个变量暴露出去
  • b.js和c.js都是模块, 通过require方法引入a.js模块, 然后就可以使用a.js模块中暴露出来的变量和方法了

2.2 CommonJS的缺点

  • 加载模块是同步
    • 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行
    • 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
  • 不能直接在浏览器中运行
    • 浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行
    • 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作
  • 浏览器中,我们通常不使用CommonJS规范
    • 在webpack中使用CommonJS是另外一回事
    • 它会将我们的代码转成浏览器可以直接执行的代码;

3. AMD(Asynchronous Module Definition)

  • 异步加载模块
  • AMD规范是commonJS规范的一个补充, 由于commonJS规范是同步加载模块, 所以在浏览器端使用时会有性能问题, 因此AMD规范就是为了解决这个问题而出现的
  • AMD规范规定, 模块必须按照依赖顺序加载, 也就是说, 模块加载器会在加载某个模块之前, 先加载这个模块的所有依赖, 并且依赖加载完后, 就会执行模块的回调函数
  • 依赖第三方库: https://github.com/requirejs/requirejs
js
// index.html
<script src="./lib/require.js" data-main="./src/main.js"></script>


// main.js
require.config({
  baseUrl: '',
  paths: {
    foo: "./src/foo",
    bar: "./src/bar"
  }
})

require(["foo", "bar"], function(foo) {
  console.log("main:", foo)
})


// foo.js
define(function() {
  const name = "hzy"
  const age = 18
  function sum(num1, num2) {
    return num1 + num2
  }

  return {
    name,
    age,
    sum
  }
})

// bar.js
define(["foo"], function(foo) {
  console.log("bar:", foo)
})

4. CMD规范

  • CMD规范和AMD规范很像, 都是为了解决commonJS规范在浏览器端的性能问题, 但是CMD规范和AMD规范的区别在于, AMD规范是提前执行, 而CMD规范是延迟执行, 只有在用到某个模块时, 才会去加载该模块
  • 依赖第三方库: SeaJS
js
// index.html

<script src="./lib/sea.js"></script>
<script>
  seajs.use("./src/main.js")
</script>


// main.js
define(function(require, exports, module) {
  const foo = require("./foo")
  console.log("main:", foo)
})

// foo.js
define(function(require, exports, module) {
  const name = "hzy"
  const age = 18
  function sum(num1, num2) {
    return num1 + num2
  }

  // exports.name = name
  // exports.age = age

  module.exports = {
    name,
    age,
    sum
  }
});

5. ES Module

  • 使用webpack打包, esModule中可以导入commonjs模块
js
// bar.js
const name = "bar"
const age = 100

// es module导出
export {
  name,
  age
}


// foo.js
const name = "foo"
const age = 18

// commonjs导出
module.exports = {
  name,
  age
}


// index.js
// es module导入
// import { name, age } from './foo'

// console.log(name, age)

// commonjs导入 - 使用webpack打包, esmodule中可以导入commonjs模块
const bar = require("./bar")
console.log(bar.name, bar.age)

6. 详解ES Module

  • 文章: https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/

  • ES Module的解析流程

  • 总共三个阶段

    • 阶段一: 构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
    • 阶段二: 实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向 对应的内存地址。
    • 阶段三: 运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;
  • 阶段一: 构建阶段

  • 阶段二三: 实例化阶段和运行阶段