注:以下方式部分也适用于 React
开发时,经常会有以下问题和需求:
以上痛点,我也经常遇到,虽然使用按需引入,可仍然没什么效果,下面根据我最近的经验,整理一下解决办法。
这个算是最常见的处理方式了,需要修改 router 路由配置文件。
正常写 router 路由配置文件时,写法如下:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router);
// 直接引入所有页面文件
import Index from '@/views/Index'
import Home from '@/views/Home'
import User from '@/views/User'
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'index',
component: Index,
},
{
path: '/home',
name: 'home',
component: Home,
},
{
path: '/user',
name: 'user',
component: User,
},
]
});
export default router;
修改为一下方式,这样一来,除了 Index 首页的其他页面,都会单独打包为独立的 js 资源文件,只有在用户即将进入页面时,才会当场加载那个页面的 js 资源文件,加载完成后才会跳页进入。
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router);
// 只引入首页资源文件(也可以和其他页面一样使用下面的方式)
import Index from '@/views/Index'
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'index',
component: Index,
},
{
path: '/home',
name: 'home',
component: import('@/views/Home'), // 异步引入
},
{
path: '/user',
name: 'user',
component: import('@/views/User'), // 异步引入
},
]
});
export default router;
最常见的就是 element-ui,提供按需引入的功能,也就是在需要使用该插件的某个 vue 组件中,解构引入用注册:
<template>
<div>
<span>其他页面 dom</span>
<Button type="primary">按钮</Button>
</div>
</template>
<script>
// 解构引入,使用哪个引入那个
import { Button } from 'element-ui'
export default {
components: {
Button,
},
}
</script>
这种方式比较直接,就是直接在 index.html 中,引入我们需要的依赖的资源文件。
当然是需要判断操作的,当我们是本地启动服务开发时,不用关系文件体积大小,可以仍然照常开发。
当是 build 打包时,才需要进行以下配置,下面的配置,也包含判断操作的逻辑。
所以,我们需要做的有三件事:
判断当前操作,是打包还是启动开发服务。
打包时,index.html 使用资源路径,来引入我们项目需要的依赖,资源对象自动绑定到 window 对象上。
打包时,需要配置 webpack 的文件复制功能,把 index.html 通过路径引入的资源文件,复制到对应目录。
打包时,配置 webpack 的 externals,让项目中使用这些 index.html 通过路径引入的资源时,从 window 中查找并返回给项目,而不是再从 node_modules 中取。
判断当前操作,package.json 文件中:
{
"scripts": {
// 给 serve 和 build,分别使用 npm config 功能传入变量,告诉后面当前正在执行的操作
"serve": "vue-cli-service serve --dev",
"build": "vue-cli-service build --build",
}
}
配置使用资源路径来引入依赖,index.html 文件中:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>首页</title>
<!--
这里注意判断一下环境,这里我使用 VUE_APP_IS_BUILD 字段判断,这个字段后面有配置
只有打包时,才用这种资源路径的方式引入插件 js 文件
如果是起服务的开发模式,则仍然使用默认方式,方便开发时调试
-->
<% if (process.env.VUE_APP_IS_BUILD === 'true') { %>
<script src="./vue.min.js"></script>
<script src="./element-ui.js"></script>
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but this Page doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
配置webpack,vue.config.js 文件中:
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin') // 引入复制插件
function resolve(dir) {
return path.join(__dirname, dir)
}
// 得出是否是 build 打包模式
process.env.VUE_APP_IS_BUILD = process.env.npm_config_build === 'true' ? 'true' : 'false'
// 配置 externals
const externals = {}
if (process.env.VUE_APP_IS_BUILD === 'true') {
// 是 build 打包构建,那么 index.html 是手动引入的 vue 和 element-ui 的资源文件,那么就需要配置一下
// 让组件中,引入 vue 时,自动给它 window.Vue,引入 element-ui 时,给它 window.ELEMENT
// 如果没有配置,或者配置的资源不存在,才会再从 node_modules 中取
externals['vue'] = 'Vue'
externals['element-ui'] = 'ELEMENT'
}
// 配置插件
const plugins = []
if (process.env.VUE_APP_IS_BUILD === 'true') {
// 是 build 打包构建,那么 index.html 是手动引入的 vue 和 element-ui 的资源文件,那么就需要配置一下
// 需要把这两个文件,复制到对应的目录,让 index.html 能成功找到并引入文件
let copy = new CopyWebpackPlugin([
{ from: resolve('node_modules/vue/dist/vue.min.js'), to: resolve('dist/vue.min.js') },
{ from: resolve('node_modules/element-ui/lib/index.js'), to: resolve('dist/element-ui.js')},
])
plugins.push(copy)
}
module.exports = {
configureWebpack: {
externals,
plugins,
},
}
注意上面的例子中,打包时的 vue 也是使用资源路径的引入方式,这是因为,由于我们是使用资源路径的方式引用 element-ui,而 element-ui 要求必须在它之前先引入 vue,否则会报错,所以,我们不得不把 vue 也使用这种方式引入了。
顾名思义,这种方式需要使用 vue-router 的 addRoute 的方式。
使用场景,一般是用于权限系统,即当用户输入 url 即将进入项目时,此时进行拦截,开始获取接口得到当前用户能访问的页面列表,添加这些路由后再放行。
如果用户有权访问,那么当前打开的项目,就有这个页面,可以进入;如果没有权限,那当前根本也就没有这个路由,自动走路由错误的处理。
router 的配置文件:
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/views/Index'
import NotFound from '@/views/NotFound'
Vue.use(Router)
const router = new Router({
isAdded: false, // 用于标记是否已经 addRoute 添加过路由了
routes: [
{
path: '/',
name: 'index',
component: Index,
},
{
path: '/404',
name: '404',
component: NotFound,
},
// 先不添加 * 无路由的重定向
// 因为遇到没有的路由时,需要先去调接口加载路由们
]
})
router.beforeEach(async (to, from, next) => {
// 已经二次添加过路由 或 访问 404 页面
if (router.options.isAdded || to.path === '/404') {
return next()
}
try {
// 调接口获取用户路由列表
let list = await getRouterList()
// 遍历组装新路由
let routes = [] // 组装好的新路由们
for (let routeItem of list) {
routes.push({
path: routeItem.path,
name: routeItem.name,
component: loadPage(routeItem.filePath),
})
}
// 此时添加 空路由 的重定向
routes.push({
path: '*',
redirect: '/404',
})
// addRoute 添加新路由
routes.forEach(route => router.addRoute(route))
// 标记改为添加路由成功
router.options.isAdded = true
// 最终去跳转
next(to)
} catch(err) {
// 出错
next('/404')
throw err
}
})
// 获取路由接口,路由路径、名称、路由对应的页面文件相对路径,都由接口返回
function getRouterList() {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
path: '/home',
name: 'home',
filePath: 'Home',
},
{
path: '/abc',
name: 'abc',
filePath: 'Abc',
},
])
}, 1000);
})
}
// 异步加载页面文件资源
function loadPage(file) {
// 异步引入,当需要打开页面时,才引入对应页面的静态资源
return () => import('@/views/modules/' + file + '.vue')
}
export default router
注意里面的 loadPage 方法中的 import() 方法,需要讲一下。
前面虽然也使用这个方法,但都是直接引入指定的确定文件,而这里我们需要按规则引入某些目录下的文件。
上面方法中,使它的方式如下:
import('@/views/modules/' + file + '.vue')
当 webpack 编译到此处,发现 import 中是 路径 + 变量 的模式,会自动把他们转为正则,然后按照正则匹配目录下文件,把所有符合这个路径的文件全部编译打包为单文件 js,当我们使用时,真的去引用规则中某个文件时,就可以引入了。
注意这里不能直接以 import(file)
的形式使用,因为这么写的话,路径完全是个 file 变量,按照规则,webpack 需要把整个硬盘中的文件都打包,它认为这是个错误的行为,会直接报错。
下面上来自上面 router 的配置文件 的内容的摘抄,下面我使用不同的语法来使用 import(),它也会有几种不同的执行效果。
1. 默认的推荐写法,添加路由后,用户打开哪个页面,才会加载某个页面的 js 资源文件。
// ...
for (let routeItem of list) {
routes.push({
path: routeItem.path,
name: routeItem.name,
component: loadPage(routeItem.filePath),
})
}
// ...
// 异步加载页面文件资源
function loadPage(file) {
// 异步引入,当需要打开页面时,才引入对应页面的静态资源
return () => import('@/views/modules/' + file + '.vue')
}
2. 同步引入,在添加路由时,直接把新页面的 js 资源们全部顺序引入。
// ...
for (let routeItem of list) {
routes.push({
path: routeItem.path,
name: routeItem.name,
component: await loadPage(routeItem.filePath),
})
}
// ...
// 同步加载页面文件资源
async function loadPage(file) {
// 执行此行,立即同步引入对应页面的所有静态资源
return (await import('@/views/modules/' + file + '.vue')).default
}
3. 还有一种极不推荐的写法,直接在编译时,就把所有匹配到的文件 js 资源,打包进 app.js 中
// ...
for (let routeItem of list) {
routes.push({
path: routeItem.path,
name: routeItem.name,
component: loadPage(routeItem.filePath),
})
}
// ...
// 异步加载页面文件资源
function loadPage(file) {
// 编译时,把下匹配到的所有页面资源,直接打包至 app.js 中,相当于就差配置一个路由了
return require('@/views/modules/' + file + '.vue').default
}
其实早该用这个方法了,只是这个配置实在不太容易理解,我也不敢确保我写的是正确的。
下面是一种比较暴力的配置方法,使用 webpack 自带插件 optimization:
官方教程:https://webpack.docschina.org/plugins/split-chunks-plugin/#optimizationsplitchunks
下面是用法,主要是 minSize 和 maxSize 两个字段。
module.exports = {
configureWebpack: config => {
config.optimization.splitChunks = {
chunks: 'all',
minSize: 1024 * 100, // 生成 chunk 的最小体积(以 bytes 字节为单位)
maxSize: 1024 * 800, // 尝试将大于 maxSize 个 bytes 字节的 chunk 分割成较小的部分,这些较小的部分在体积上至少为 minSize
automaticNameDelimiter:'-',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `chunk.${packageName.replace('@', '')}`;
},
priority: 10,
},
}
}
},
}