项目的优化背景?
假如我们开发一个应用,一开始只是简单的功能,随时间的流逝,功能在不断地添加。逐渐的从一个超小应用,变成了一个巨大应用。这时候,你的用户发现,每次访问你的网站打开的速度越来越慢,因为我们的web应用默认不配置的话,它都是构建在一个js文件中。这就是为啥用户访问感觉越来越慢的问题,为了解决这个问题,我们选择进行优化(微前端方向暂时不讨论)
首先思考我们可以从哪些点入手:
- 业务代码
- 第三方包
业务代码
业务代码又可以细分三个领域
- 页面开发
- 路由(按需加载)
- 全局组件
页面开发
假如我们开发了一个a页面,然后这个页面由若干个组件构成,然后我都是使用
import component from 'componentPath/componentName.vue
来引入组件
代码示例
<template>
<div class="page1">
我是{{ page }}
<el-button type="primary" size="default" @click="handleToloadCom">加载组件</el-button>
<asyncComponent v-if="isLoadCom"></asyncComponent>
<el-button @click="handleClick">按钮</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import asyncComponent from './components/asyncComponent.vue'
import axios from 'axios'
const isLoadCom = ref(false)
const router = useRouter()
const page = ref('页面1')
const handleClick = () => {
router.push('/page2')
}
axios.get('./data.json').then(res => {
console.log('res', res)
})
const handleToloadCom = () => {
isLoadCom.value = true
}
</script>
<style lang="scss" scoped>
.page1 {
width: 100px;
height: 100px;
background: yellow;
}
</style>
组件的代码
<template>
<div>
我是按需加载的组件
</div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
这时候,你运行yarn build
会发现,这a页面在打包的过程中,把组件也构建进去了
很明显 这不是我们期望的结果,我们期望的是,只有当我点击按钮,才去加载这个组件,但现在是我加载a页面的时候,就已经加载组件了(因为都在一个js文件里面)。ok,现在我们来换一种组件的引用方式
异步组件(defineAsyncComponent)
const component = defineAsyncComponent(()=>import('componentPath/componentName.vue'))
<template>
<div class="page1">
我是{{ page }}
<el-button type="primary" size="default" @click="handleToloadCom">加载组件</el-button>
<asyncComponent v-if="isLoadCom"></asyncComponent>
<el-button @click="handleClick">按钮</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
import { useRouter } from 'vue-router'
const asyncComponent = defineAsyncComponent(() => import('./components/asyncComponent.vue'))
import axios from 'axios'
const isLoadCom = ref(false)
const router = useRouter()
const page = ref('页面1')
const handleClick = () => {
router.push('/page2')
}
axios.get('./data.json').then(res => {
console.log('res', res)
})
const handleToloadCom = () => {
isLoadCom.value = true
}
</script>
<style lang="scss" scoped>
.page1 {
width: 100px;
height: 100px;
background: yellow;
}
</style>
这时候,再去运行yarn build
通过上图可以发现,我们的a页面和组件,已经被分别打包成了index-hash.js和asyncComoponent-hash.js
这时候我们可以通过
yarn preview
预览我们刚刚的打包结果。
通过这张图可以发现在访问页面的时候,asyncComponent组件并没有加载,只有当我们点击按钮的时候,它才去加载,这样通过异步组件的方式,就达到了在加载a页面时,只加载了当前页面,只有触发操作时,才会去加载对应的组件。(假设我们项目中,有几百个页面,这时候,这种提升就很很明显了。)
路由(按需加载)
有人说,为啥要按需加载,不配置又能怎么样,我们这里用一个vite构建的测试项目,来说明,为什么? 假如我们这个项目有6个页面,采用直接加载页面的方式
import page from '@/views/page/index.vue'
import { createWebHashHistory, createRouter } from 'vue-router'
import page1 from '@/views/page1/index.vue'
import page2 from '@/views/page2/index.vue'
import page3 from '@/views/page3/index.vue'
import page4 from '@/views/page4/index.vue'
import page5 from '@/views/page5/index.vue'
import page6 from '@/views/page6/index.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/page1'
},
{
path: '/page1',
name: 'page1',
component: page1
},
{
path: '/page2',
name: 'page2',
component: page2
},
{
path: '/page3',
name: 'page3',
component: page3
},
{
path: '/page4',
name: 'page4',
component: page4
},
{
path: '/page5',
name: 'page5',
component: page5
},
{
path: '/page6',
name: 'page6',
component: page6
}
]
})
export default router
这时候我们执行yarn build
,会发现,我们的所有页面都构建在一个js文件
这时候我们执行
yarn preview
预览可以发现
我们只是访问了一个页面,但是却把整个项目的资源都加载(因为都是构建在一个js中)。很明显,这不合理。我们想要的应该是访问a页面,你就给我返回a页面的资源,我访问其他页面的时候,再给我返回其他页面的资源。这才是理想情况,而路由按需加载解决的就是这个问题。
下面 我们修改下路由加载的方式
() => import('@/views/page1/index.vue')
import { createWebHashHistory, createRouter } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/page1'
},
{
path: '/page1',
name: 'page1',
component: () => import('@/views/page1/index.vue')
},
{
path: '/page2',
name: 'page2',
component: () => import('@/views/page2/index.vue')
},
{
path: '/page3',
name: 'page3',
component: () => import('@/views/page3/index.vue')
},
{
path: '/page4',
name: 'page4',
component: () => import('@/views/page4/index.vue')
},
{
path: '/page5',
name: 'page5',
component: () => import('@/views/page5/index.vue')
},
{
path: '/page6',
name: 'page6',
component: () => import('@/views/page6/index.vue')
}
]
})
export default router
在执行yarn build
,发现每一个页面都单独构建了一个js文件
我们这时候,再去预览,执行yarn preview
可以发现我们访问a页面的时候,只返回了a页面的资源,其他的并没有加载,这就是路由按需加载的作用。
全局组件
一般来说我们都是使用这种方式,来注册全局组件。
import { App } from 'vue'
import component1 from './component1.vue'
import component2 from './component2.vue'
import component3 from './component3.vue'
import component4 from './component4.vue'
import component5 from './component5.vue'
import component6 from './component6.vue'
const registerComponent = (app: App<Element>) => {
app.component('component1', component1)
app.component('component2', component2)
app.component('component3', component3)
app.component('component4', component4)
app.component('component5', component5)
app.component('component6', component6)
}
export default registerComponent
这时候,我们如果构建项目yarn build
可以发现,这五个组件都被打到一个index-hash.js文件中
那假如我们项目中,有个页面仅仅用到component1,但是却加载了全部组件(因为都在一个js文件)。很明显,这是不合理的。所以我们要修改下代码,使用按需加载。
() => import('./component.vue')
import { App } from 'vue'
const registerComponent = (app: App<Element>) => {
app.component('component1', () => import('./component1.vue'))
app.component('component2', () => import('./component2.vue'))
app.component('component3', () => import('./component3.vue'))
app.component('component4', () => import('./component4.vue'))
app.component('component5', () => import('./component5.vue'))
app.component('component6', () => import('./component6.vue'))
}
export default registerComponent
这时候,我们再来运行yarn build
可以发现,每个组件都单独打了一个包
但是大家有没有发现一个问题,其实我这6个页面,根本都没有地方用到了这些组件,但是他们还是会在打包资源中生成,这是因为在构建的过程中,app.component(componentName,component)
加载了你注册的这个资源,所以构建工具认为你这个资源有用,所以就生成了对应的js。但其实项目里面并不一定需要的。
所以在这里推荐大家,非必要不是使用全局组件注册,因为会影响打包结果(造成很多无用的资源被打包)
第三方包
- 按需加载
- 拆包
按需加载
我们这里用element-plus来做示例说明 不拆分
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import registerComponent from '@/components'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.use(ElementPlus)
registerComponent(app)
app.mount('#app')
执行yarn build
可以发现一个
element-plus
接近1M
下面是配置了按需加载
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const rootPath = path.resolve(__dirname)
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
})
],
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(rootPath, 'src')
}
]
}
})
再次执行yarn build
可以只有100多kb
所以按需加载还是很有用的
拆包
先来看,未拆包打包构建的资源。
可以发现所有的第三方包都在一个js文件中
这时候,我们来进行拆包
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const rootPath = path.resolve(__dirname)
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
})
],
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(rootPath, 'src')
}
]
},
build: {
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
const moduleName = id.split('node_modules/')[1].split('/')[0]
return moduleName
}
}
}
}
}
})
这是拆包后构建的项目资源
可以发现index.js文件已经没了,取而代之的是:pinia、vue-use、vue、vue-router、axios
等js
文件。这样就可以保证,假如你的项目只用到了其中的某一个模块,不是把其他模块也加载进来,避免无用资源的浪费。
总结
- 写业务代码时,注意非页面一开始加载的组件,就使用异步组件,非必要,不用全局组件,实在需要,在注册全局组件的时候,也是用按需加载的方式注册(就是异步组件),路由尽量使用按需加载的方式来配置。
- 使用第三方包,尽量配置按需加载,然后,打包构建的时候,进行拆分更小包。