前端模块化

63 阅读6分钟

模块化是一种处理复杂系统分解成为更好的可管理模块的方式,模块化开发最终的目的是将程序划分成一个个小的结构

  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,定义变量名词时不会影响到其他的结构
  • 可以将自己希望暴露的变量、函数、对象等导出给其结构使用
  • 可以通过某种方式,导入另外结构中的变量、函数、对象等

早期:全局函数模式 简单对象封装 IIFE

模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

CommonJs

CommonJS 是一种模块系统规范,主要用于在服务器端环境(如 Node.js)中管理模块。它提供了模块的定义、加载、导出机制,允许开发者在不同模块之间共享代码。Node.js 中,CommonJS 是默认的模块系统,虽然现在 Node.js 也支持 ECMAScript 模块,但 CommonJS 仍然广泛使用

CommonJS规范的核心变量:exports、module.exports、require,可以使用这些变量来方便的进行模块化开发

  • exportsmodule.exports可以负责对模块中的内容进行导出
  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

exports导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出

// a.js
function add(num1, num2) {
  return num1 + num2;
}
const message = "hello world";
console.log(exports); // {}
exports.add = add;
exports.message = message;


// main.js
// const { add, message } = require("./a"); // 可以拿到文件中导出的exports对象,相当于引用赋值
// console.log(add(10, 30)); // 40
// console.log(message); // hello world

const a = require("./a");
console.log(a.add(10, 30)); // 40
console.log(a.message); // hello world

module.exports导出

// b.js
function add(num1, num2) {
  return num1 + num2;
}
const message = "hello world";

// 方式一
// module.exports.add = add;
// module.exports.message = message;
// console.log(module.exports === exports); // true

// 方式二:开发中常用,module.exports赋值新对象更灵活方便
module.exports = {
  add,
  message,
};

// main.js
const b = require("./b");
console.log(b.add(10, 20)); // 30
console.log(b.message); // hello world

require导入

require(X)

X是⼀个Node核⼼内置模块,⽐如path、http:直接返回核⼼模块,并且停⽌查找

X是以 ./..//(根⽬录)开头的

  • 第⼀步:将X当做⼀个⽂件在对应的⽬录下查找

    1. 直接查找⽂件X
    2. 查找X.js⽂件
    3. 查找X.json⽂件
    4. 查找X.node⽂件
  • 第⼆步:没有找到对应的⽂件,将X作为⼀个⽬录:查找⽬录下⾯的index⽂件

    1. 查找X/index.js⽂件
    2. 查找X/index.json⽂件
    3. 查找X/index.node⽂件
  • 如果没有找到,那么报错:not found

直接是⼀个X(没有路径),并且X不是⼀个核⼼模块:去**node_modules文件夹**中查找

CommonJS暴露的模块到底是什么? CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

CommonJs特点

  • 文件即模块,文件内所有代码都运行在独立的作用域,因此不会污染全局空间。
  • 模块可以被多次引用、加载。在第一次被加载时,会被缓存,之后都从缓存中直接读取结果。
  • 加载某个模块,就是引入该模块的 module.exports 属性。
  • module.exports 属性输出的是值的拷贝,一旦这个值被输出,模块内再发生变化不会影响到输出的值。
  • 模块加载顺序按照代码引入的顺序。
  • CommonJS加载模块是同步的,会阻塞

ES Module

export关键字

export关键字将一个模块中的变量、函数、类等导出

  • 方式一:想导出谁就在语句声明的前面直接加上export关键字

  • 方式二:想导出谁则将需要导出的标识符,放到export后面的 {}

    • 注意:这里的 {} 里面不是ES6的对象字面量的增强写法,{} 也不是表示一个对象export { message: message } 是错误的写法;
  • 方式三:在方式二导出时给标识符起一个别名

// 方式一
export const message1 = "hello world1";
export function add1(num1, num2) {
  return num1 + num2;
}
export class Person1 {
  constructor(name) {
    this.name = name;
  }
}

// 方式二
const message2 = "hello world2";
function add2(num1, num2) {
  return num1 + num2;
}
class Person2 {
  constructor(name) {
    this.name = name;
  }
}
export { message2, add2, Person2 };

// 方式三
const message3 = "hello world3";
function add3(num1, num2) {
  return num1 + num2;
}
class Person3 {
  constructor(name) {
    this.name = name;
  }
}
export { message3, add3 as add0, Person3 as Person0 };

import关键字

import关键字负责从另外一个模块中导入内容

  • 方式一:import { 标识符列表 } from '模块'

    • 注意:这里的 {} 也不是一个对象,里面只是存放导入的标识符列表内容
  • 方式二:通过as关键字在导入时给标识符起别名

  • 方式三:通过 * as 自己名字 将模块功能放到一个模块功能对象上

// 结合export中的代码学习
import {
  message1, // 方式一
  message2,
  message3,
  add0 as add3, // 方式二
  add1,
  add2,
  Person0 as Person3,
  Person1,
  Person2,
} from "./a.js";

import * as a from "./a.js"; // 方式三

console.log(
  message1,
  message2,
  message3,
  add1,
  add2,
  add3,
  Person1,
  Person2,
  Person3,

  a.message1,
  a.message2,
  a.message3,
  a.add1,
  a.add2,
  a.add0,
  a.Person1,
  a.Person2,
  a.Person0
);

export和import结合

/* util/index 通常是不编写逻辑的,在这里统一导入并导出 */

// 方式一
import {
  message1,
  message2,
  message3,
  add0 as add3,
  add1,
  add2,
  Person0 as Person3,
  Person1,
  Person2,
} from "./a.js";
import { getData } from "./b.js";

export {
  message1,
  message2,
  message3,
  add3,
  add1,
  add2,
  Person3,
  Person1,
  Person2,
  getData,
};

// 方式二:结合
export {
  message1,
  message2,
  message3,
  add0 as add3,
  add1,
  add2,
  Person0 as Person3,
  Person1,
  Person2,
} from "./a.js";
export { getData } from "./b.js";

// 方式三:建议当有相应的文档时再这样写
export * from "./a.js";
export * from "./b.js";

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

AMD规范(基本不用)

  • AMD是Asynchronous Module Definition`(异步模块定义)的缩写,⽤的是异步加载模块

CMD规范(基本不用)

  • CMD 是Common Module Definition(通⽤模块定义)的缩写
  • ⽤的也是异步加载模块,但是它将CommonJS的优点吸收了过来,但是⽬前CMD使⽤也⾮常少了
  • CMD也有⾃⼰⽐较优秀的实现⽅案:SeaJS