模块动态导入与代码分割
JavaScript模块动态导入import()与代码分割策略详解。
1. 静态导入 vs 动态导入
1.1 静态导入
// 编译时解析,所有模块在启动时加载
import { Router } from './router.js';
import { utils } from './utils.js';
特点:
- 编译时确定依赖关系
- 所有模块在启动时加载
- 支持 Tree Shaking
- 无法条件加载
1.2 动态导入
// 运行时按需加载,返回 Promise
const { Router } = await import('./router.js');
特点:
- 运行时按需加载
- 返回 Promise
- 支持条件加载
- 代码分割的基石
2. import() 语法
2.1 基本用法
// then/catch
import('./module.js')
.then((module) => {
module.doSomething();
})
.catch((error) => {
console.error('模块加载失败:', error);
});
// async/await
try {
const module = await import('./module.js');
module.doSomething();
} catch (error) {
console.error('模块加载失败:', error);
}
2.2 动态路径
// 变量路径(需确保路径可静态分析)
const moduleName = 'user';
const module = await import(`./modules/${moduleName}.js`);
// 完全动态路径(部分打包工具不支持)
const path = getUserInput(); // 安全风险
const module = await import(path);
2.3 解构导入
const { default: Component, helper } = await import('./Component.js');
3. 代码分割策略
3.1 路由级分割
// Vue Router
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
{
path: '/settings',
component: () => import('./views/Settings.vue'),
},
];
// React Router
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
3.2 组件级分割
// 重型组件延迟加载
const HeavyChart = React.lazy(() => import('./components/HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>显示图表</button>
{showChart && (
<Suspense fallback={<div>加载图表...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
3.3 功能级分割
// 编辑器模块按需加载
async function openEditor(file) {
const { Editor } = await import('./editor/Editor.js');
const { highlightPlugin } = await import('./editor/plugins/highlight.js');
const editor = new Editor(file);
editor.use(highlightPlugin);
return editor;
}
3.4 第三方库分割
// Vite/Webpack 自动分割
// lodash 按需导入
import debounce from 'lodash/debounce'; // 仅打包 debounce
// 替代
import { debounce } from 'lodash'; // 可能打包整个 lodash
// 动态导入大型库
async function exportPDF(data) {
const { jsPDF } = await import('jspdf');
const doc = new jsPDF();
// ...
}
4. 打包工具配置
4.1 Vite
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
charts: ['chart.js', 'd3'],
},
},
},
},
});
4.2 Webpack
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
};
4.3 魔法注释
// 命名 chunk
const module = await import(
/* webpackChunkName: "dashboard" */
'./views/Dashboard.js'
);
// 预取
const module = await import(
/* webpackPrefetch: true */
'./views/Dashboard.js'
);
// 预加载
const module = await import(
/* webpackPreload: true */
'./views/Dashboard.js'
);
| 注释 | 说明 |
|---|---|
webpackChunkName | 自定义 chunk 名称 |
webpackPrefetch | 空闲时预取(低优先级) |
webpackPreload | 与父 chunk 并行加载(高优先级) |
5. 预加载策略
5.1 链接预加载
<!-- 预加载关键模块 -->
<link rel="modulepreload" href="/src/router.js" />
<link rel="modulepreload" href="/src/store.js" />
5.2 交互预加载
// 鼠标悬停时预加载
link.addEventListener('mouseenter', () => {
import('./views/Dashboard.js'); // 预加载但不执行
});
// 点击时执行
link.addEventListener('click', async (e) => {
e.preventDefault();
const { Dashboard } = await import('./views/Dashboard.js');
render(Dashboard);
});
5.3 可见性预加载
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 元素进入视口时预加载
import('./components/HeavyWidget.js');
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('[data-preload]').forEach((el) => {
observer.observe(el);
});