一、从“全局变量混乱”到“模块时代”的发展历程
1.1 最早的痛点:谁都能改的全局变量
以前写JS代码,就是在HTML里插一堆script标签,所有变量、函数都扔在全局空间里,就像一群人挤在同一个大办公室,每个人的“名字”都得独一无二,不然喊一声“拿数据”,最后一个回应的人说的内容会覆盖之前的,出问题根本找不到根源。比如两个脚本都定义了function getUserName(),后面的会覆盖前面的,要是某个地方调用这个函数,拿到的就是被改坏的内容,这种bug特别难排查,新手甚至老手都踩过坑。
1.2 第一个解决方案:CommonJS(Node.js专属)
后来Node.js出来,为服务端JS解决了模块问题,推出了CommonJS方案——它把每个JS文件当成独立的模块,用require导入、module.exports导出,就像把每个人的办公桌隔成小格子,自己的东西放自己的格子里,不会乱碰别人的。
技术栈:Node.js CommonJS
// moduleUtils.js(导出方:封装工具函数)
// 计算两个数的和,方便其他模块复用
function add(a, b) {
return a + b;
}
// 计算两个数的乘积,作为备用工具
function multiply(a, b) {
return a * b;
}
// 导出要对外提供的函数,CommonJS的核心语法
module.exports = {
add,
multiply
};
// app.js(导入方:使用工具函数)
// 导入moduleUtils里的add和multiply函数,自动绑定成对象
const { add, multiply } = require('./moduleUtils');
// 调用add函数:2加3等于5
console.log(`2 + 3 = ${add(2, 3)}`);
// 调用multiply函数:4乘5等于20
console.log(`4 × 5 = ${multiply(4, 5)}`);
这个方案优点是简单、同步加载,符合服务端文件系统的特性;缺点是同步加载不适合浏览器,因为浏览器加载资源是异步的,同步加载会阻塞页面渲染,直到所有模块都加载完才显示内容,用户体验很差。
1.3 浏览器的妥协:AMD和CMD
为了让JS模块能在浏览器跑,前端社区推出了AMD(比如require.js)和CMD(比如sea.js)方案,都是异步加载模块,解决浏览器阻塞问题。但这两个方案都有各自的问题:AMD语法复杂,CMD有依赖顺序坑,而且属于社区临时方案,不是官方标准,后来慢慢被淘汰了。
1.4 现在的主流:ES Modules(ES6+官方标准)
2015年ES6推出了官方的模块化方案——ES Modules,用import和export语法,是静态的,打包工具能直接分析哪些代码没用到(就是大家常说的“树摇”),删掉后能大幅减小生产环境的代码体积,而且原生支持浏览器,不用额外引入其他库。
技术栈:Browser JavaScript ES Modules
// moduleMath.js(导出方:ES模块的标准语法)
// 直接导出需要对外提供的函数,不用写对象包裹
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// 这个函数没被用到,打包时会被树摇去掉,不会出现在最终代码里
export function unusedFunc() {
console.log("这个函数没人用");
}
// main.js(导入方:按需导入)
// 只导入需要的add和subtract函数,实现静态树摇的前提
import { add, subtract } from './moduleMath.js';
// 计算结果:2加3等于5
console.log(`2 + 3 = ${add(2, 3)}`);
// 计算结果:5减1等于4
console.log(`5 - 1 = ${subtract(5, 1)}`);
这个方案是现在前端的绝对主流,优点是官方标准、浏览器原生支持、适合树摇优化;缺点是旧浏览器(比如IE11)不支持,生产环境如果要兼容,需要用babel转义或者打包工具(比如webpack)自动处理。
二、生产环境下的选型优化策略
2.1 先搞清楚你的应用场景
选模块化方案的第一步是看自己的场景,不能盲目跟风用ES Modules:
- 场景1:纯Node.js服务端项目,选CommonJS,兼容性最好,不用额外配置;
- 场景2:现代前端项目(React/Vue/Angular),首选ES Modules,配合打包工具优化;
- 场景3:需要兼容旧浏览器的老项目,可能需要转成ES5兼容代码,或者用AMD方案;
- 场景4:前后端同构项目(比如Next.js),要注意CommonJS和ES Modules的混用问题,不能同时在一个项目里用两种语法。
2.2 不同方案的优缺点和选型要点
2.2.1 CommonJS
- 应用场景:Node.js服务端、简单脚本工具;
- 优点:语法简单、无依赖、兼容性拉满;
- 缺点:同步加载不适合前端浏览器,不能树摇(因为是动态导入,打包工具没法提前知道哪些代码没用到);
- 注意事项:如果在前端用CommonJS,必须经过打包工具(webpack/rollup)处理,不然浏览器会报错。
2.2.2 ES Modules
- 应用场景:现代前端项目、需要优化性能的项目;
- 优点:静态语法支持树摇、浏览器原生支持、按需加载方便;
- 缺点:旧浏览器不支持、如果混用CommonJS会出问题(比如Node里用import需要设置
type: module); - 注意事项:浏览器里用ES模块的script标签必须加
type="module",而且导入的文件路径必须带.js后缀,不然会报错。
2.3 生产环境的核心优化技巧
2.3.1 树摇:去掉没用的代码
ES Modules是静态导入,打包工具(比如webpack、rollup)能分析模块里的导出函数,把没用到的删掉,这就是树摇。比如上面示例里的unusedFunc,打包后不会出现在最终代码里,能减小体积。要注意树摇只能用ES Modules,CommonJS做不到,因为它的导入是动态的,打包工具没法提前知道哪些代码用到了。
2.3.2 动态import:按需加载减体积
如果项目代码很大,首屏加载慢,可以用动态import语法,把代码拆分成小块,用户需要的时候才加载。最常用的场景是路由懒加载,比如React里的路由组件,用户点到某个页面才加载对应的JS文件。 技术栈:React + ES Modules
// App.js(前端路由配置,实现懒加载)
import { lazy, Suspense } from 'react';
// 动态导入首页组件,用户访问首页时才加载,不会出现在首屏代码里
const Home = lazy(() => import('./Home.js'));
// 动态导入列表页组件,同理
const List = lazy(() => import('./List.js'));
function App() {
return (
<div>
{/* 用Suspense包裹,显示加载状态,提升用户体验 */}
<Suspense fallback={<div>正在加载,请稍候...</div>}>
{/* 根据路由切换对应的组件 */}
<Switch>
<Route path="/" exact component={Home} />
<Route path="/list" component={List} />
</Switch>
</Suspense>
</div>
);
}
这个技巧能大幅减少首屏的代码体积,用户不用一打开页面就加载所有代码,提升加载速度,尤其适合手机端。
2.4 踩坑注意事项
- CommonJS和ES Modules不能混用:比如在Node.js项目里,一个文件用
require,另一个用import,会报错,要么把所有CommonJS转成ES Modules(设置package.json的type: module),要么统一用一种; - 旧浏览器兼容:如果要兼容IE11,必须把ES Modules转成ES5代码,用babel的
@babel/preset-env配置,或者用打包工具自动转; - 路径问题:浏览器里用ES Modules导入文件,路径必须带
.js后缀,比如import './module.js',不能写import './module',不然会404。
三、总结
JS模块化方案从解决全局变量混乱起步,经历了CommonJS、AMD、CMD,最终落地到ES Modules标准,每个阶段都贴合当时的场景需求。生产环境选型的核心是“看场景选方案”:服务端用CommonJS,现代前端用ES Modules,老项目按需兼容。优化的核心技巧是用树摇和动态import,减小代码体积,提升加载速度。不管哪种方案,都要注意避免混用、兼容旧环境等坑,这样才能写出维护性好、性能高的代码。
评论
围绕“JavaScript模块化方案的发展历程及生产环境下的选型优化策略”参与讨论