模块化

October 30, 2022

文章导读

「从构建产物洞悉模块化原理」- 不要秃头啊 https://juejin.cn/post/7147365025047379981/

相关问题

  • 模块化的产生是为了解决什么问题?在什么场景下诞生的
  • Web环境中是如何支持模块化的?加载过程时怎么样的
  • CommonJS可以加载ES Module导出的内容吗
  • ES Module可以加载CommonJS 导出的内容吗
  • Webpack内部如何区分一个模块是采用哪种模块化规范
  • 一个模块内可以既使用CommonJS,又使用 ES Module 吗?

模块化发展历史

早期JavaScript开发很容易存在全局污染依赖管理混乱问题。每个加载的js文件都共享变量。

后来可以使用匿名函数自执行的方式,形成独立的块级作用域解决这个问题。这会有新的问题:

  • 代码杂乱无章
  • 没有合适的规范,每个人都可能会任意命名、模块名称可能相同
  • 必须记得每一个模块返回对象命名的问题,才能在其他模块中正确的使用

因此,commonJS规范问世

CommonJS规范

CommonJS具备以下特点:

  • 每一个JS文件都是一个单独的模块
  • 模块中包含CommonJS规范的核心变量:exports、 module.exports、require
  • 使用核心变量,进行模块化开发
//在a.js中导出变量
const name = "不要秃头啊";
const age = "18";
module.exports = { name, age };
//或者:
exports.name = "不要秃头啊";
exports.age = "18";

//在b.js中引用导出的变量
const { name, age } = require("./a.js");
console.log( name , age )

ES Module规范

ES 6开始,JavaScript才有真正意义上模块化规范

ES Module产生的优势有很多,比如:

  • 借助ES Module的静态导入导出优势,实现tree shaking
  • 使用import()懒加载的方式实现代码分割
/**
* 导出
*/
export * from 'module'; //重定向导出 不包括 module内的default
export { name1, name2, ..., nameN } from 'module'; // 重定向命名导出
export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重命名导出
export { name1, name2,, nameN }; // 与之前声明的变量名绑定 命名导出
export { variable1 as name1, variable2 as name2,, nameN }; // 重命名导出
export let name1 = 'name1'; // 声明命名导出 或者 var, const,function, function*, class
export default expression; // 默认导出
export default function () { ... } // 或者 function*, class
export default function name1() { ... } // 或者 function*, class

/**
* 导入
*/
import defaultExport from "module"; // 默认导入
import { a, b, c } from "module"; //解构导入
import defaultExport, { a, b, c as newC } from "module"; //混合导入
import * as name from "module"; //混合导入
var promise = import("module"); //动态导入(异步导入)

CommonJS模块化实现原理

  • 定义对象modules,用来存放模块化的源代码
  • 定义缓存对象cache
  • 定义require函数

    • 判断缓存中是否已加载过
    • 给定义的module变量和cache[moduledId]赋值同一个对象
    • 加载执行模块 给module.exports赋值
    • 导出module.exports
  • 执行入口函数
//模块定义
var modules = {
  "./src/name.js": (module) => {
    module.exports = "不要秃头啊";
  },
};
var cache = {};

//接受模块的路径为参数,返回具体的模块的内容
function require(modulePath) {
  var cachedModule = cache[modulePath]; //获取模块缓存
  if (cachedModule !== undefined) {
    //如果有缓存则不允许模块内容,直接retuen导出的值
    return cachedModule.exports;
  }
  //如果没有缓存,则定义module对象,定义exports属性
  //这里注意!!!module = cache[modulePath] 代表引用的是同一个内存地址
  var module = (cache[modulePath] = {
    exports: {},
  });
  //运行模块内的代码,在模块代码中会给module.exports对象赋值
  modules[modulePath](module, module.exports, require);

  //导入module.exports对象
  return module.exports;
}

(() => {
  let author = require("./src/name.js");
  console.log(author, "author");
})();

ES Module和commonJS可以互相混用吗

  1. ES Module可以直接通过import引入commonJS
  2. commonJS不能通过require引入ES Module,经过babel转

commonJS和esModule的区别

  • commonJS是被加载的时候运行,esModule是编译的时候运行
  • commonJSS输出的是值的浅拷贝, esModule输出值的引用
  • commonJS具有缓存,在第一次加载时,会完整运行整个文件并输出一个对象,浅拷贝在内存中,下次加载文件时,直接从内存中取值
/*************** a.js**********************/
let count = 0
exports.count = count; // 输出值的拷贝
exports.add = ()=>{
    //这里改变count值,并不会将module.exports对象的count属性值改变
    count++;
}

/*************** b.js**********************/
const { count, add } = require('./a.js')
//在支持es6模块的环境下等同于
import { count, add } from './a.js'

console.log(count) //0
add();
console.log(count)//0
/*************** a.js**********************/
export let count = 0;//输出的是值的引用,指向同一块内存
export const add = ()=>{
    count++;//此时引用指向的内存值发生改变
}


/*************** b.js**********************/
import { count, add } from './a.js'

console.log(count) //0
add();
console.log(count)//1

Profile picture

百事可乐

Let it snow, let it snow, let it snow