使用异步组件+Suspense的形式来实现代码分包是前端项目优化的有效&常用手段之一。
何为代码分包?
未进行代码分包的打包文件结构: 如果项目里用了第三方库的话(echarts、elementUI…),理论上还会有一个vendor.xxx.js文件。 index.xxx.js里是我们项目打包后的主文件代码。 进行代码分包的打包文件结构:
通过上述两个图的对比,可以发现,代码分包指的是打包后的项目js文件有多个。 代码分包的直观好处就是减少项目线上环境的首页白屏时间。 首页白屏时间是衡量项目的重要指标之一。首页白屏时间越短,用户体验越好。 代码分包把那些需要异步加载的组件单独形成一个js文件,在使用这些组件时才动态引入。 在首页加载过程中只会引入主js文件,从而实现减少首页白屏时间的目的。
何为异步组件?
所谓异步组件,指的是组件内的数据需要等待服务器响应结果后才能显示在页面上。 在没有收到服务器响应的结果之前,页面不会渲染这个组件。 乍一听,这件事同步组件也可以干呀,同步组件在第一次渲染的时候因为服务器还没有返回数据,因此页面渲染是空的,等前端接收到服务器返回的数据后,可以通过重新渲染把数据填充到页面呀~ 但是有追求的程序员应该发现了:用同步组件干异步组件的事,会导致页面的不必要渲染。
Vue3生成异步组件&进行代码分包
通过defineAsyncComponent加载异步配合import函数模式导入异步组件,在Template模块使用Suspense控制异步组件的显示,完成异步组件的生成后,打包时就会自动实现分包。 我们可以生成一个json文件来模拟服务器端的响应结果:
[
{
"name": "苹果"
},
{
"name": "菠萝"
},
{
"name": "西瓜"
},
{
"name": "葡萄"
}
]
然后编写一个简易版的axios来处理响应结果:
type NameList = {
name: string
}
export const axios = (url:string):Promise<NameList[]> => {
return new Promise((resolve) => {
let xhr: XMLHttpRequest = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 && xhr.status === 200){
setTimeout(() => {
resolve(JSON.parse(xhr.responseText))
}, 2000)
}
}
xhr.send()
})
}
在异步组件中发起请求:
<template>
<div>
<div v-for="(item, index) in list" :key="index">
{{item.name}}
</div>
</div>
</template>
<script lang='ts' setup>
import { axios } from './server'
const list = await axios('./data.json')
</script>
<style lang = "less" scoped>
</style>
在父组件内通过defineAsyncComponent加载异步配合import引入,再使用Suspense控制显示即可
<template>
<div class="content">
<Suspense>
<template #default>
<A></A>
</template>
<template #fallback>
<div>
loading
</div>
</template>
</Suspense>
</div>
</template>
<script lang='ts' setup>
import { reactive, ref, defineAsyncComponent } from 'vue'
const A = defineAsyncComponent(() => import('../../DynamicComponent/A/A.vue'))
</script>
<style lang = "less" scoped>
</style>
页面过了定时2秒后成功加载异步组件:
然后执行npm run build,webpack或vite等打包工具就会帮我们自动分包:
需要注意的是,对于异步组件,在相关被引用的父组件里,应该都以defineAsyncComponent的方式进行引入。不能在某些父组件里以异步的方式进行引入,另一些又以同步的方式(直接使用import)进行引入。否则打包时会出现粘包问题,即没有把异步代码进行分离。
React生成异步组件&进行代码分包
按照React官网的说法:
在你的应用中引入代码分割的最佳方式是通过动态import()语法。
使用之前:
import { add } from './math';
console.log(add(16, 26));
使用之后:
import("./math").then(math => {
console.log(math.add(16, 26));
});
当Webpack解析到该语法时,会自动进行代码分割(代码分包)。 React使用异步组件实现代码分包的代码示例如下: 和Vue重复的data.json和server.ts就不重复写了~直接上子组件和父组件:
import React, {useEffect, useState} from 'react'
import {axios} from "./server";
type NameList = {
name: string
}
const ChildrenComponent = () => {
const [list, setList] = useState<NameList[]>([]);
const getData = async () => {
return await axios("./data.json")
}
useEffect(() => {
getData().then(data => setList(data))
}, []);
return (
<div>
{list.map((item) => (
<p>{item.name}</p>
))}
</div>
)
}
export default ChildrenComponent;
import React,{lazy, Suspense} from 'react'
export const FatherComponent = () => {
const ChildrenComponent = lazy(() => import('./ChildrenComponent'));
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<ChildrenComponent />
</Suspense>
</div>
)
}
在实践React异步组件时,踩得一个坑是:异步子组件的导出形式必须使用default,否则父组件使用import导入时会标红。鼠标移上去,错误提示的意思就是子组件必须用default进行导出~ 后面发现官网针对这点在文章最后也做了解释:
React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件
export const MyComponent = ;
export const MyUnusedComponent = ;
export { MyComponent as default } from "./ManyComponents.js";
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
总结
Vue&React使用异步组件进行代码分包总结如下:
- 相同点
通过上述代码示例,可以发现Vue和React在引入异步组件时,都使用了Suspense这个组件,都采用动态import的方式进行异步组件的导入。 - 不同点
Vue引入动态import采用的是defineAsyncComponent,而React使用的是lazy。
项目优化的手段有很多,除了异步组件,还可以通过路由懒加载的方式来进行代码分包,从而实现减少首页白屏时间的目的,感觉真不错~
|