前置知识: JavaScript

模块动态导入与代码分割

2 minAdvanced2026/6/14

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);
});