logo

聊聊 JS 模块化

Authors
  • avatar
    Name
    White Play
    Twitter

面试官:谈谈对 JS 模块化的理解

视频版

我做面试官的时候,让候选人谈谈对 JS 模块化的理解是必问的一道题。因为我觉得这道题太妙了,可以问出候选人的技术深度,比如:

  1. 他写没写过业务代码以外的东西?比如包、SDK
  2. 它对工程化、项目构建的理解?
  3. 微前端?tree-shaking?...

说句题外话,这道题很像另一道题:谈谈 Vue 的生命周期。

很多候选人都会背 Vue 的生命周期,但是问到 createdmounted 的区别,就很能看出技术深度了。

普遍获得的答案是,created 早于 mountedmounted 可以获取到 DOM 节点,而 created 不能。

但是更深层次的答案是,mounted 在服务端渲染时不会被调用。一个问题看出候选人是否了解服务端渲染,以及是否了解 Vue 的运行机制。

扯远了回到正题。

CJS 和 ESM

众所周知,JavaScript 有两种模块。一种是 ES6 引入的模块系统,叫 ES Module,简称 ESM; 另一种是 CommonJS 模块,简称 CJS,广泛用于 Node.js。

业务代码大家肯定都已经在使用 ESM 了,但不能否认 CJS 也广泛存在项目中。尤其是工程化领域里,在编写一些项目配置文件时 CJS 还是主流。所以这两种模块化方案还是都要懂的。它们不只是导入导出的语法存在差异,还关系到运行机制的不同。如果不理解内部机制可能会导致意外的运行结果。

// a.js

let a = 1

function increase() {
  a++
}

module.exports = {
  a,
  increase,
}
const p = require('./a')

p.increase()
p.increase()

console.log(p)

查看打印结果我们会发现居然反直觉的是 1。

相同逻辑的代码如果使用 ESM 就是 3,这可能更符合预期(因为增加了两次)。

这是因为 CommonJS 本质上是把模块内脚本运行一遍后,把结果缓存起来拷贝一份给当前上下文去用。

而 ESM 把模块作为只读引用,不会缓存,类似符号链接。

UMD

如果你做过包、SDK 的开发,你就会知道有的时候你的代码可能在浏览器环境里工作(全局变量),也有可能在服务器环境里工作(CommonJS)。这意味着它既要支持浏览器的加载方式又得支持 CommonJS 的方式。

所以社区里还有一种兼容各种工作环境的实现就是 UMD,一般用来配置打包器,让打包器打包出兼容各种环境的代码。

image-20250606104014118

工作原理就是通过 ifelse 来判断当前环境,比如存在对象 exports,那它就要怀疑自己是在 CommonJS 中了,就把库加到 exports 对象里去,以兼容 CommonJS 的模块化实现。

如果你用过 qiankun 微前端框架,那你肯定也接触过 UMD,qiankun 要求子应用打包成 UMD 格式,这样可以满足它在浏览器端动态导入,以及导入后找到暴露出的生命周期函数。

聊到模块化,就会很自然聊到 tree-shaking,这又是一个庞大话题,这里就不展开了。

所以我觉得面试的时候问候选人对模块化的理解,这个问题还是比较有深入探讨的价值的,面试官也可以借此对候选人有一个更全面的了解。