配置文件采用cosmicConfig配置方式,类似prettier配置都可以
.prettierrc.toml
文件,TOML 格式 (须添加.toml
扩展名)package.json
文件添加"prettier"
key- 以 JSON 或 YAML 编写的
.prettierrc
文件。.prettierrc.json
、.prettierrc.yml
、.prettierrc.yaml
或.prettierrc.json5
文件。- 使用
module.exports
导出对象的.prettierrc.js
、.prettierrc.cjs
、prettier.config.js
或prettier.config.cjs
文件。
前端项目添加代码规范(eslint prettier stylelint husky lint-staged commitlint)
模块化
JavaScript 引入模块化的目的
JavaScript 引入模块化主要是为了解决大型应用开发中的代码组织和管理问题。以下是模块化的主要目的和优势:
1. 解决命名冲突(命名空间污染)
问题:传统脚本方式下所有变量都存在于全局作用域,容易导致命名冲突。
模块化解决方案:
// moduleA.js
const privateVar = 'A的内部变量';
export const publicVar = 'A的公开变量';
// moduleB.js
const privateVar = 'B的内部变量'; // 不会与moduleA冲突
2. 代码组织和可维护性
传统问题:大型项目中代码难以组织和维护。
模块化优势:
- 按功能/组件拆分代码
- 清晰的依赖关系
- 更好的项目结构
// 传统方式 - 所有代码在一个文件
// app.js (2000行代码)
// 模块化方式
// components/
// Button.js
// Modal.js
// utils/
// helpers.js
// api.js
3. 依赖管理
问题:传统<script>
标签需要手动管理加载顺序。
模块化解决方案:
// 明确声明依赖
import { fetchData } from './api.js';
import Button from './components/Button.js';
4. 按需加载
优势:可以动态加载模块,提高性能。
// 动态导入
const module = await import('./module.js');
5. 代码复用
优势:模块可以被多个项目或应用共享。
// 共享工具模块
import { formatDate } from '@company/utils';
6. 封装和私有性
优势:模块可以暴露特定接口,保持内部实现私有。
// counter.js
let count = 0; // 私有变量
export function increment() {
count++;
}
export function getCount() {
return count;
}
7. 测试友好
优势:模块可以单独测试。
// 测试单个模块
import { calculate } from './math.js';
test('calculate works', () => {
expect(calculate(2, 3)).toBe(5);
});
JavaScript 模块化发展历程
IIFE 模式:早期解决方案
javascript(function() { // 私有作用域 })();
CommonJS:Node.js 的模块系统
javascriptconst fs = require('fs'); module.exports = {...};
AMD/RequireJS:浏览器异步加载
javascriptdefine(['dep1', 'dep2'], function(dep1, dep2) {...});
ES Modules:现代标准
javascriptimport { func } from './module.js'; export default function() {...};
模块化是现代 JavaScript 开发的基石,使大型复杂应用的开发变得可行和高效。
JavaScript 模块化的缺点
虽然模块化带来了诸多好处,但也存在一些值得注意的缺点和挑战:
1. 性能开销
构建工具依赖
// 简单项目可能不需要模块化构建
// 但引入模块化后通常需要webpack/rollup等工具
import { smallUtil } from './utils'; // 导致必须使用构建工具
运行时性能影响
- 模块解析需要时间
- 模块边界增加了函数调用开销
- 树摇优化(Tree Shaking)不完全时可能包含无用代码
2. 开发复杂性增加
配置复杂度
// 需要配置各种模块解析规则
// webpack.config.js
resolve: {
extensions: ['.js', '.jsx', '.ts'],
alias: {
'@': path.resolve(__dirname, 'src')
}
}
认知负担
- 需要理解多种模块规范(ESM, CommonJS, UMD等)
- 动态导入、循环引用等高级用法增加学习曲线
3. 浏览器兼容性问题
原生ES模块限制
<!-- 需要type="module"且存在跨域限制 -->
<script type="module" src="app.js"></script>
旧浏览器支持
- IE11等旧浏览器不支持原生ES模块
- 需要编译转换增加构建复杂度
4. 工具链依赖
必要构建步骤
# 简单项目变得复杂
npm install webpack babel-loader @babel/core --save-dev
调试困难
- 源码与运行代码不一致
- 需要source map支持
- 错误堆栈信息可能难以追踪
5. 模块边界问题
循环引用陷阱
// a.js
import { b } from './b';
export const a = 'A';
// b.js
import { a } from './a'; // 循环引用!
export const b = 'B';
状态共享挑战
// 跨模块的全局状态管理变得复杂
// 需要额外引入Redux/Vuex等解决方案
6. 包管理问题
依赖膨胀
// package.json
{
"dependencies": {
"lodash": "^4.17.21",
"moment": "^2.29.1",
// ...数十个其他依赖
}
}
版本冲突
- 不同模块可能依赖同一库的不同版本
- 可能导致代码重复或冲突
7. 其他实际问题
冷启动延迟
// 大型应用模块初始化需要时间
import('./huge-module.js'); // 明显的加载延迟
测试复杂性
// 需要额外配置模块模拟
jest.mock('./api-module');
何时应该避免过度模块化?
- 微型项目/简单页面:几个简单脚本可能更高效
- 性能关键代码:模块边界可能带来不必要的开销
- 快速原型开发:构建配置可能拖慢开发速度
- 已有其他封装方式:如Web Components可能已提供足够隔离
总结对比
优点 | 缺点 |
---|---|
代码组织更清晰 | 增加构建复杂度 |
避免命名冲突 | 引入性能开销 |
更好的依赖管理 | 浏览器兼容性问题 |
促进代码复用 | 学习曲线变陡 |
便于团队协作 | 调试难度增加 |
最佳实践建议:根据项目规模权衡,小型项目可以从简单脚本开始,当复杂度增加时再逐步引入模块化。
JavaScript 模块化是否必须依赖打包工具?
模块化本身是一个代码组织概念,不一定必须依赖打包工具,但实际开发中通常会使用打包工具来获得更好的开发体验和性能优化。具体取决于你使用的模块系统和目标环境:
1. 不需要打包工具的情况
浏览器原生ES模块 (ESM)
<!-- 直接使用type="module" -->
<script type="module" src="app.js"></script>
- 优点:无需构建步骤
- 限制:
- 每个文件需要明确扩展名(.js)
- 不支持CommonJS模块
- 需要处理相对/绝对路径问题
- 性能较差(多个小文件请求)
Node.js环境
// 使用CommonJS
const moduleA = require('./moduleA');
module.exports = {...};
// 或Node.js中的ES模块(.mjs文件)
import moduleA from './moduleA.mjs';
- Node.js原生支持CommonJS和ES模块
- 但大型项目仍可能用打包工具优化
2. 通常需要打包工具的情况
开发效率需求
# 典型现代前端工具链
npm install webpack vite rollup --save-dev
- 解决的问题:
- 转换JSX/TypeScript等非标准语法
- 处理CSS/图片等非JS资源
- 热更新(HMR)开发体验
性能优化需求
// webpack.config.js
optimization: {
splitChunks: { chunks: 'all' } // 代码分割
}
- 打包工具提供的优化:
- 代码压缩(tree-shaking)
- 按需加载/代码分割
- 依赖去重
- 生产环境优化
跨模块系统兼容
// 需要处理混合使用的模块
import _ from 'lodash'; // ESM
const fs = require('fs'); // CommonJS
- 打包工具可以统一不同模块规范
3. 现代折中方案
开发时免打包(Vite/Snowpack)
npm create vite@latest
- 利用浏览器原生ESM
- 开发时无需打包,按需编译
- 生产构建时仍会打包
轻量打包工具(esbuild)
npm install esbuild
- 极快的打包速度
- 适合简单项目
4. 是否需要打包工具的判断标准
考虑因素 | 需要打包工具 | 可能免打包 |
---|---|---|
项目规模 | 中大型项目 | 小型项目 |
使用JSX/TS | 需要 | 不需要 |
浏览器兼容性 | 需要支持旧浏览器 | 仅现代浏览器 |
性能要求 | 高(需代码分割等) | 一般 |
使用CSS预处理器 | 需要 | 不使用 |
团队协作 | 需要统一规范 | 个人项目 |
5. 实际建议
- 简单静态网站:可以尝试纯ES模块
- React/Vue项目:推荐使用Vite(开发免打包,生产打包)
- 企业级应用:使用Webpack/Rollup完整打包方案
- Node.js服务:根据复杂度决定是否打包
结论:模块化代码可以不依赖打包工具运行,但现代前端开发中打包工具已成为事实标准,因为它们解决了模块化在实际应用中的诸多工程化问题。
不使用打包工具和ES模块
如果不使用打包工具和原生 ES 模块 (<script type="module">
),以下是几种快速搭建模块化 JavaScript 项目的方法:
方法 1:IIFE + 命名空间模式
原理:使用立即执行函数 (IIFE) 创建闭包作用域,通过全局命名空间组织模块。
项目结构
project/
├── index.html
├── main.js # 入口文件
├── modules/
│ ├── moduleA.js
│ ├── moduleB.js
│ └── utils.js
代码示例
- 模块定义 (
modules/moduleA.js
)
// 通过全局命名空间暴露模块
window.MyApp = window.MyApp || {};
(function() {
const privateVar = '私有变量';
function privateMethod() {
console.log('私有方法');
}
// 暴露公共接口
window.MyApp.moduleA = {
publicMethod: function() {
privateMethod();
return privateVar;
}
};
})();
- 入口文件 (
main.js
)
// 按顺序加载模块
(function() {
// 依赖模块必须手动管理加载顺序
const scriptA = document.createElement('script');
scriptA.src = 'modules/moduleA.js';
document.head.appendChild(scriptA);
scriptA.onload = function() {
// 使用模块
console.log(MyApp.moduleA.publicMethod());
};
})();
- HTML 文件 (
index.html
)
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
</head>
<body></body>
</html>
方法 2:动态脚本加载 + 回调
原理:通过 document.createElement('script')
动态加载脚本,用回调控制执行顺序。
代码示例
// utils/loader.js
function loadModule(src, callback) {
const script = document.createElement('script');
script.src = src;
script.onload = callback;
document.head.appendChild(script);
}
// 按顺序加载模块
loadModule('modules/moduleA.js', function() {
loadModule('modules/moduleB.js', function() {
console.log('所有模块加载完成');
MyApp.moduleA.doSomething();
});
});
方法 3:RequireJS (AMD)
原理:使用 AMD 规范的 RequireJS 实现异步模块加载(虽非原生,但无需打包工具)。
实现步骤
- 引入 RequireJS:
<script src="<https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js>"></script>
- 定义模块 (
modules/moduleC.js
):
// AMD 规范
define('moduleC', ['dependency'], function(dependency) {
return {
sayHello: function() {
console.log('Hello from ModuleC');
}
};
});
- 入口配置 (
main.js
):
requirejs.config({
baseUrl: 'modules',
paths: {
dependency: 'path/to/dependency'
}
});
requirejs(['moduleC'], function(moduleC) {
moduleC.sayHello();
});
方法 4:UMD 模块兼容
原理:编写兼容 CommonJS/AMD/全局变量的模块,直接通过 <script>
标签引入。
模块示例 (modules/umdModule.js
)
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境
define(['dependency'], factory);
} else if (typeof exports === 'object') {
// CommonJS 环境
module.exports = factory(require('dependency'));
} else {
// 全局变量
root.MyModule = factory(root.Dependency);
}
})(this, function(dependency) {
return {
doWork: function() {
console.log('UMD 模块工作');
}
};
});
直接使用
<script src="modules/umdModule.js"></script>
<script>
MyModule.doWork(); // 全局调用
</script>
方法 5:模板引擎拼接
原理:用服务端模板(如 PHP/Python)或构建脚本拼接模块文件。
示例:简单 Shell 脚本合并
# build.sh (Linux/macOS)
cat modules/*.js > bundle.js
<script src="bundle.js"></script>
对比总结
方法 | 优点 | 缺点 |
---|---|---|
IIFE + 命名空间 | 纯原生,无依赖 | 手动管理依赖顺序,易混乱 |
动态脚本加载 | 按需加载 | 回调地狱,调试困难 |
RequireJS (AMD) | 成熟的模块化方案 | 需引入第三方库 |
UMD 模块 | 兼容多种环境 | 模块定义代码冗余 |
模板拼接 | 简单粗暴 | 无依赖分析,可能重复代码 |
推荐选择
- 快速原型开发:IIFE + 命名空间(方法1)
- 需要异步加载:RequireJS(方法3)
- 兼容多环境:UMD 模块(方法4)
- 完全零依赖:动态脚本加载(方法2)
这些方法虽不如现代打包工具高效,但能在不依赖复杂工具链的情况下实现基本模块化。
模块化中重复引用导致的多次打包问题
当一个模块被多个其他模块引用时,如果不进行适当处理,确实可能导致该模块在最终打包产物中出现多次。以下是这个问题的详细分析和解决方案:
问题原因分析
1. 模块系统的工作机制
// utils.js
export function helper() {
console.log('Helper function');
}
// moduleA.js
import { helper } from './utils';
// moduleB.js
import { helper } from './utils';
在上述代码中,如果打包工具配置不当,utils.js
可能会在最终打包结果中出现两次。
2. 导致重复打包的常见场景
- 多入口项目:不同入口文件引用了相同模块
- 动态导入:未正确配置代码分割
- 第三方库:不同版本的同名库被引用
- 配置错误:打包工具未启用去重优化
解决方案
1. 使用打包工具的依赖去重功能
Webpack 配置示例
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 提取公共依赖
minSize: 0, // 即使很小的模块也提取
}
}
};
Rollup 配置示例
// rollup.config.js
export default {
output: {
format: 'esm',
manualChunks(id) {
if (id.includes('node_modules') || id.includes('utils')) {
return 'vendor'; // 将公共模块打包到vendor文件
}
}
}
};
2. 确保单例模式的模块设计
// singleton.js
let instance;
export function getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
3. 检查循环引用问题
// 避免这种结构
// a.js
import { b } from './b';
// b.js
import { a } from './a';
4. 第三方库的特殊处理
对于如lodash这样的库,使用直接导入特定功能:
// 推荐 - 只会打包一次
import debounce from 'lodash/debounce';
// 不推荐 - 可能打包整个lodash
import { debounce } from 'lodash';
检测重复模块的方法
1. 使用打包分析工具
# Webpack
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [new BundleAnalyzerPlugin()]
};
2. 检查打包输出
npx webpack --profile --json > stats.json
最佳实践建议
统一模块引用路径:
javascript// 避免混用不同路径指向同一模块 import util from './utils'; import util from '../src/utils'; // 可能被视为不同模块
合理配置externals:
javascript// webpack.config.js externals: { react: 'React' // 避免重复打包React }
使用monorepo管理共享代码:
shell# 项目结构 packages/ common/ # 共享代码 app1/ # 应用1 app2/ # 应用2
动态导入优化:
javascript// 使用webpack魔法注释 import(/* webpackChunkName: "shared" */ './sharedModule');
通过合理配置打包工具和遵循模块化最佳实践,可以有效地避免模块被多次打包的问题,从而减少最终产物的体积并提高运行效率。