{T}

基本用法

动态导入语法

使用 ES2015 的动态导入语法 import()

javascript
const routes = [
  {
    path: '/about',
    component: () => import('@/views/About.vue')
  }
]

原理

import() 返回一个 Promise,Webpack 会将其自动代码分割:

javascript
// 等价于
const About = () => import('@/views/About.vue')

const routes = [
  {
    path: '/about',
    component: About
  }
]

完整示例

javascript
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/user/:id',
    name: 'UserDetail',
    component: () => import('@/views/user/Detail.vue'),
    props: true
  },
  {
    path: '/product',
    name: 'ProductList',
    component: () => import('@/views/product/List.vue'),
    children: [
      {
        path: ':id',
        name: 'ProductDetail',
        component: () => import('@/views/product/Detail.vue')
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

Webpack 分组打包

基本分组

使用 Webpack 的魔法注释 /* webpackChunkName */ 将多个路由打包到同一个 chunk:

javascript
const routes = [
  {
    path: '/user',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/List.vue')
  },
  {
    path: '/user/:id',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/Detail.vue')
  },
  {
    path: '/user/create',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/Create.vue')
  }
]

以上三个组件会打包到 user.[hash].js 文件中。

按功能分组

javascript
// 用户相关路由 → user.js
const routes = [
  {
    path: '/user',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/List.vue')
  },
  {
    path: '/user/:id',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/Detail.vue')
  }
]

// 产品相关路由 → product.js
const routes = [
  {
    path: '/product',
    component: () => import(/* webpackChunkName: "product" */ '@/views/product/List.vue')
  },
  {
    path: '/product/:id',
    component: () => import(/* webpackChunkName: "product" */ '@/views/product/Detail.vue')
  }
]

// 管理后台 → admin.js
const routes = [
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Layout.vue'),
    children: [
      {
        path: 'user',
        component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/User.vue')
      },
      {
        path: 'product',
        component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Product.vue')
      }
    ]
  }
]

分组策略

code
打包结果:
├── app.js           # 主应用代码
├── user.js          # 用户模块
├── product.js       # 产品模块
├── admin.js         # 管理后台
└── vendor.js        # 第三方库

预加载

Prefetch

使用 /* webpackPrefetch: true */ 预加载资源,在浏览器空闲时加载:

javascript
const routes = [
  {
    path: '/about',
    component: () => import(
      /* webpackChunkName: "about" */
      /* webpackPrefetch: true */
      '@/views/About.vue'
    )
  }
]

Prefetch 与懒加载的区别

特性懒加载Prefetch
加载时机路由激活时浏览器空闲时
首屏影响轻微增加网络请求
切换速度需要加载已缓存,瞬间切换
适用场景非核心页面可能访问的页面

预加载策略

javascript
// 首页立即加载(不使用懒加载)
import Home from '@/views/Home.vue'

// 常用页面预加载
const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/about',
    component: () => import(
      /* webpackChunkName: "about" */
      /* webpackPrefetch: true */
      '@/views/About.vue'
    )
  },
  {
    path: '/contact',
    component: () => import(
      /* webpackChunkName: "contact" */
      /* webpackPrefetch: true */
      '@/views/Contact.vue'
    )
  },
  // 低频页面懒加载,不预加载
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue')
  }
]

加载状态处理

加载进度条

使用 NProgress 显示加载进度:

bash
npm install nprogress
npm install @types/nprogress -D
javascript
// router/index.js
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

NProgress.configure({ showSpinner: false })

router.beforeEach((to, from, next) => {
  NProgress.start()
  next()
})

router.afterEach(() => {
  NProgress.done()
})

加载组件

javascript
const routes = [
  {
    path: '/about',
    component: () => ({
      component: import('@/views/About.vue'),
      loading: LoadingComponent,    // 加载时显示的组件
      error: ErrorComponent,        // 加载失败时显示的组件
      delay: 200,                   // 延迟显示 loading 的时间
      timeout: 10000                // 超时时间
    })
  }
]

自定义加载组件

Vue SFC
<!-- components/Loading.vue -->
<template>
  <div class="loading-container">
    <div class="loading-spinner"></div>
    <p>加载中...</p>
  </div>
</template>

<style scoped>
.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 200px;
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 3px solid #f3f3f3;
  border-top: 3px solid #42b983;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>
javascript
// router/index.js
import Loading from '@/components/Loading.vue'
import Error from '@/components/Error.vue'

const routes = [
  {
    path: '/about',
    component: () => ({
      component: import('@/views/About.vue'),
      loading: Loading,
      error: Error,
      delay: 200,
      timeout: 30000
    })
  }
]

实战案例

案例 1:大型项目路由组织

code
views/
├── home/
│   ├── Index.vue
│   └── components/
├── user/
│   ├── List.vue
│   ├── Detail.vue
│   ├── Create.vue
│   └── Edit.vue
├── product/
│   ├── List.vue
│   ├── Detail.vue
│   └── components/
└── admin/
    ├── Layout.vue
    ├── Dashboard.vue
    └── Settings.vue
javascript
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 路由模块化
const routes = [
  // 首页(直接加载)
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/home/Index.vue')
  },
  
  // 用户模块(分组加载)
  {
    path: '/user',
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/List.vue'),
    children: [
      {
        path: ':id',
        component: () => import(/* webpackChunkName: "user" */ '@/views/user/Detail.vue')
      }
    ]
  },
  
  // 产品模块(分组加载)
  {
    path: '/product',
    component: () => import(/* webpackChunkName: "product" */ '@/views/product/List.vue')
  },
  
  // 管理后台(独立分组)
  {
    path: '/admin',
    component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Layout.vue'),
    children: [
      {
        path: '',
        component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Dashboard.vue')
      },
      {
        path: 'settings',
        component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/Settings.vue')
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

案例 2:按需加载第三方库

javascript
// 懒加载 Vue 编辑器组件
const routes = [
  {
    path: '/editor',
    component: () => import(
      /* webpackChunkName: "editor" */
      '@/views/Editor.vue'
    )
  }
]

// Editor.vue
export default {
  components: {
    // 懒加载大型组件
    VueEditor: () => import('vue2-editor').then(m => m.VueEditor)
  }
}

案例 3:权限路由动态加载

javascript
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store'

Vue.use(VueRouter)

// 静态路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/Login.vue'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/views/404.vue'),
    hidden: true
  }
]

// 动态路由(根据权限加载)
export const asyncRoutes = [
  {
    path: '/',
    component: () => import('@/layouts/DefaultLayout.vue'),
    children: [
      {
        path: '',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue')
      }
    ]
  },
  {
    path: '/admin',
    component: () => import('@/layouts/AdminLayout.vue'),
    meta: { roles: ['admin'] },
    children: [
      {
        path: 'user',
        name: 'UserManage',
        component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/User.vue')
      }
    ]
  },
  // 404 必须放在最后
  { path: '*', redirect: '/404', hidden: true }
]

const router = new VueRouter({
  mode: 'history',
  routes: constantRoutes
})

// 动态添加路由
export function addRoutes(routes) {
  routes.forEach(route => {
    router.addRoute(route)
  })
}

export default router

性能优化

1. 合理分组

javascript
// ❌ 不推荐:每个路由单独打包
const routes = [
  { path: '/user', component: () => import('@/views/user/List.vue') },
  { path: '/user/:id', component: () => import('@/views/user/Detail.vue') },
  { path: '/user/create', component: () => import('@/views/user/Create.vue') }
]
// 结果:user.List.[hash].js, user.Detail.[hash].js, user.Create.[hash].js

// ✅ 推荐:相关路由合并打包
const routes = [
  { 
    path: '/user', 
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/List.vue') 
  },
  { 
    path: '/user/:id', 
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/Detail.vue') 
  },
  { 
    path: '/user/create', 
    component: () => import(/* webpackChunkName: "user" */ '@/views/user/Create.vue') 
  }
]
// 结果:user.[hash].js

2. 首屏关键路由

javascript
// ✅ 推荐:首屏关键路由直接加载
import Home from '@/views/Home.vue'

const routes = [
  {
    path: '/',
    component: Home  // 首页直接加载
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue')  // 其他页面懒加载
  }
]

3. 预加载高频页面

javascript
const routes = [
  // 首页
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  // 高频页面预加载
  {
    path: '/product',
    component: () => import(
      /* webpackChunkName: "product" */
      /* webpackPrefetch: true */
      '@/views/product/List.vue'
    )
  },
  // 低频页面懒加载
  {
    path: '/settings',
    component: () => import('@/views/Settings.vue')
  }
]

4. 代码分割策略

javascript
// webpack.config.js 或 vue.config.js
module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          libs: {
            name: 'chunk-libs',
            test: /[\\/]node_modules[\\/]/,
            priority: 10,
            chunks: 'initial'
          },
          elementUI: {
            name: 'chunk-elementUI',
            priority: 20,
            test: /[\\/]node_modules[\\/]_?element-ui(.*)/
          },
          commons: {
            name: 'chunk-commons',
            test: resolve('src/components'),
            minChunks: 3,
            priority: 5,
            reuseExistingChunk: true
          }
        }
      }
    }
  }
}

最佳实践

1. 统一懒加载函数

javascript
// utils/lazy-load.js
export function lazyLoad(view) {
  return () => import(`@/views/${view}.vue`)
}

// 使用
const routes = [
  {
    path: '/about',
    component: lazyLoad('About')
  },
  {
    path: '/user/:id',
    component: lazyLoad('user/Detail')
  }
]

2. 带分组的懒加载

javascript
// utils/lazy-load.js
export function lazyLoad(view, chunkName) {
  return () => import(
    /* webpackChunkName: "${chunkName}" */
    `@/views/${view}.vue`
  )
}

// 使用
const routes = [
  {
    path: '/user',
    component: lazyLoad('user/List', 'user')
  },
  {
    path: '/user/:id',
    component: lazyLoad('user/Detail', 'user')
  }
]

3. 带错误处理的懒加载

javascript
// utils/lazy-load.js
export function lazyLoad(view) {
  return () => import(`@/views/${view}.vue`).catch(() => {
    // 加载失败时返回错误组件
    return import('@/views/Error.vue')
  })
}

4. 开发环境禁用懒加载

javascript
// utils/lazy-load.js
const isProduction = process.env.NODE_ENV === 'production'

export function lazyLoad(view) {
  if (isProduction) {
    return () => import(`@/views/${view}.vue`)
  } else {
    // 开发环境直接加载,提高热更新速度
    return require(`@/views/${view}.vue`).default
  }
}

常见问题

1. 懒加载导致样式闪烁?

确保样式在组件内部使用 scoped,或使用 CSS Modules:

Vue SFC
<style scoped>
/* 组件样式 */
</style>

2. 如何查看打包结果?

使用 Webpack Bundle Analyzer:

bash
npm install webpack-bundle-analyzer -D
javascript
// vue.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin()
    ]
  }
}

3. 如何处理动态 import 的 TypeScript 类型?

typescript
// 声明类型
type Component = () => Promise<typeof import('*.vue').default>

const routes: Array<RouteConfig> = [
  {
    path: '/about',
    component: (): Component => import('@/views/About.vue')
  }
]

4. 懒加载组件如何命名?

使用 webpackChunkName 注释:

javascript
component: () => import(
  /* webpackChunkName: "user" */
  '@/views/user/Detail.vue'
)
// 生成文件: user.[contenthash].js

调试技巧

查看加载的 chunk

javascript
// 在浏览器控制台
console.log(__webpack_modules__)

监控 chunk 加载

javascript
router.beforeEach((to, from, next) => {
  const startTime = Date.now()
  
  router.app.$once('hook:mounted', () => {
    const loadTime = Date.now() - startTime
    console.log(`路由 ${to.path} 加载耗时: ${loadTime}ms`)
  })
  
  next()
})