前言
说起路由懒加载,相信大家很快就知道怎么使用它 实现它,包括在做项目优化的时候,相信大家都会用到路由懒加载,但是问到路由懒加载的原理,可能大家就忽略了 理不清楚了,笔者在阿里巴巴校招面试时就被问到,所以下来后好好总结一下,下面带大家一起去理解路由懒加载的原理。
为什么要进行路由懒加载
因为Vue 是SPA(单页面应用),所以首页第一次加载时会把所有的组件以及组件相关的资源全都加载了,为了解决这个问题,我们需要对Vue实现懒加载(按需加载),可以提高首屏加载速度
那懒加载是如何做到能够加快应用初始加载速度的
懒加载前提: 进行懒加载的子模块(子组件)需要是一个单独的文件
先上结论:
实现原理:
懒加载的本质实际上就是代码分离,把代码分离到不同的 bundle 中,只有当函数被调用的时候,才去按需加载或并行加载对应的组件内容。
而vue中的路由懒加载其实是利用了webpack的异步加载的方法,webpack会把动态加载的页面组件分离成单独的一个chunk.js文件,使用路由懒加载的写法,只会在进入当前这个路由时候才会走 component ,然后在运行import()编译加载相应的组件,通过import()
使得ES6的模块有了动态加载的能力,让url匹配到相应的路径时,会动态加载页面组件,这样首屏的代码量会大幅减少。
可以理解为也是通过Promise的resolve机制,因为Promise函数返回的Promise为resolve组件本身,而我们又可以使用import来导入组件,import会返回一个Promise对象。
当然懒加载也有缺点,就是会额外的增加一个http请求,如果项目非常小的话可以考虑不使用路由懒加载
展开说说:
如何实现代码分离
我们都知道webpack会整合所有的资源,将其打入一个包内。但是要实现懒加载,我们肯定不能让子组件被整合进一个包里头。所以需要解决的问题就是如何将子组件单独打包成一个文件,在我们需要用到他的时候去加载。
首先解决第一个问题,如何将子组件打包成一个单独的文件?
在webpack中提供了一个import方法给我们,注意是方法
,而不是我们经常使用的那个import
正常的引入文件我们会这样做:
import {sum} from './js/count' //同步加载
而这样的写法就是大家所说的同步引入,他是会被打进一个文件内的
使用import方法,就可以做到异步引入,从而使被引入的文件单独被打包
import(/* webpackChunkName: "count" */ './js/count')
这样就会使这个count.js被单独打包成一个文件了(count.chunk.js)被单独打包的文件默认会使用文件的路径作为文件名。
/* webpackChunkName: "count" */这一行注释就是为了修改打包出来的文件名的 count就是文件名。
在webpack配置中我们还需要这样写:
output: {
//所有文件的输出路径
//__dirname: nodejs的变量,代表当前文件的文件夹目录
path: path.resolve(__dirname, "dist"),//绝对路径
//文件名
filename: "bundle.js",
//代码块的文件名
chunkFilename:"[name].chunk.js", 这里的name就是上面的webpackChunkName
clean:true //自动删除上次打包的结果
},
这样我们就实现了一个将文件单独打包的功能。
当然离懒加载还差一点。
虽然上文中我们做到了异步加载,并且也将文件单独打包了,但是如果单纯这样做的话资源还是会被一起加载。
细心的同学可能已经发现了,在vue中我们通常会给需要懒加载的路由这样写:
() => import(/* webpackChunkName: "con" */ './xxx.vue')
没错,这里就是利用了函数只有被调用才会执行的特性。将组件的component属性赋值一个函数,在函数中去异步引入组件。
这样才是一个完整的路由懒加载方案。
这一块详细说就是:
在Webpack中常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。
- 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用来分离代码。
今天我们的核心主要是第三种方式:动态导入
当涉及到动态代码拆分时,Webpack 提供了两个类似的技术:
- 第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import()语法 来实现动态导入
- 第二种,则是 Webpack 的遗留功能,使用 Webpack 特定的 require.ensure (不推荐使用)
我们主要看看 import()语法 的方式:
import()
函数只接受一个参数,就是引用模块的地址,并且使用 promise
式的回调获取加载的模块。在代码中所有被 import()
的模块,都将打成一个单独的模块,放在 chunk
存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
如何使用路由懒加载呢
未使用写法:
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login
}
]
})
路由懒加载写法一:
const Login = ()=> import('@/views/login/login')
......
routes: [
{ path: '/login', component: Login },
]
更简单的写法:
routes: [
{ path: '/', component: ()=>import("@/components/Login") }
]
路由懒加载写法二:
webpack提供的require.ensure()
{
path: '/login',
name: 'Login',
component: resolve => require(["@/components/Login"], resolve)
}
把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中
写法一:
const Login = () => import(/* webpackChunkName: "group-login" */ './Login.vue')
const Reg = () => import(/* webpackChunkName: "group-login" */ './Reg.vue')
// 管理页面
const AlgorithmSet = r => require.ensure([], () => r(require('@/views/algorithm/algorithmSet')), 'Algorithm')
注意:chunk name一定要加引号(单引号双引号都可以)否则webpack不能识别chunk name,会默认按[id]显示,
打出来的包的名字会是 0.js,1.js……
写法二:
{
path: '/home',
name: 'Home',
component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
}, {
path: '/index',
name: 'Index',
component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
},
想必看到这里大家应该明白了路由懒加载的原理了,接下来补充几个知识点:
~import和require的区别
- 浏览器 - ES Modules规范
- Node.js - CommonJs规范
ES6之前,前端模块规范有三种:CommonJS、AMD和CMD
CommonJS用在服务端模块化规范,AMD和CMD用在浏览器模块化规范
ES6之后,出现了ES Modules就可以直接使用import和export来实现模块化
import和require的区别:
-
CommonJS 模块是运行时加载,ES6 module模块是编译时加载;
-
CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,
有一个独立的模块依赖的解析阶段; -
CommonJS 模块输出的是一个值的拷贝,而ES6模块输出的是值的引用;
require表示的是运行时加载,而import表示的是编译时加载(效率更高),由于是编译时加载,所以import命令会提升到整个模块的头部。
- require是运行时调用,所以require理论上可以运用在代码的任何地方
- import是编译时调用,所以必须放在文件开头
~组件懒加载
组件懒加载 使用组件懒加载大大减少当前主包大小:
// import 形式
// import HelloWorld from '@/components/HelloWorld'
// import user from '@/components/user'
// import懒加载形式 异步组件的写法
const HelloWorld = ()=>import("@/components/HelloWorld")
const user = ()=>import("@/components/user")
原来组件中写法
<script>
import One from './one'
export default {
components:{
"One-com":One
},
data () {
return {
msg: 'Welcome'
}
}
}
</script>
ES 提出的import方法
<script>
export default {
components:{
"One-com": ()=>import("./one");
},
data () {
return {
msg: 'Welcome'
}
}
}
</script>
异步方法
<script>
export default {
components: {
"One-com": resolve=>(['./one'],resolve)
},
data () {
return {
msg: 'Welcome'
}
}
}
</script>
~图片懒加载
vue中使用:
npm i -S vue-lazyload
main.js中:
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1,
error: '../static/goods_default.jpg', // 加载失败或者无资源时显示的图片
loading: '../static/weixin.gif', // loading图片,未加载时显示的
attempt: 1
})
使用:
<img v-lazy="getImg" :key="getImg" alt="">
computed: {
getImg() {
return this.goods.img || this.goods.image || this.goods.show.img
}
}
图片懒加载原理
- 一张图片就是一个
<img>
标签,浏览器是否发起请求图片是根据<img>
的src属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给<img>
的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。
思路及实现
方案一:clientHeight、scrollTop 和 offsetTop
首先给图片一个占位资源;接着,通过监听 scroll 事件来判断图片是否到达视口;对 scroll 事件做节流处理,以免频繁触发。
方案二:getBoundingClientRect
方案三:IntersectionObserver
这是浏览器内置的一个 API ,实现了 监听window的scroll事件 、 判断是否在视口中 以及 节流 三大功能。
当然这个 IntersectionObserver 也可以用作其他资源的预加载,功能非常强大。
原理:
首先将页面上的图片的src属性设置为空字符串,而图片的真是路经则设置带data-original属性中,当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入到可视区域,如果图片在可视区域将图片的src属性设置为data-original的值,这样就可以实现延迟加载。
- 加载loading图片
- 判断哪些图片要加载(滚动条以上scrollTop+当前内容clientHeight)
- 替换真图片(将data-src中的值给到src,进行请求图片)
js代码:
// onload是等所有的资源文件加载完毕以后再绑定事件
window.onload = function(){
// 获取图片列表,即img标签列表
var imgs = document.querySelectorAll('img');
// 获取到浏览器顶部的距离
function getTop(e){
return e.offsetTop;
}
// 懒加载实现
function lazyload(imgs){
// 可视区域高度
var h = window.innerHeight;
//滚动区域高度
var s = document.documentElement.scrollTop || document.body.scrollTop;
for(var i=0;i<imgs.length;i++){
//图片距离顶部的距离大于可视区域和滚动区域之和时懒加载
if ((h+s)>getTop(imgs[i])) {
// 真实情况是页面开始有2秒空白,所以使用setTimeout定时2s
(function(i){
setTimeout(function(){
// 不加立即执行函数i会等于9
// 隐形加载图片或其他资源,
//创建一个临时图片,这个图片在内存中不会到页面上去。实现隐形加载
var temp = new Image();
temp.src = imgs[i].getAttribute('data-src');//只会请求一次
// onload判断图片加载完毕,真是图片加载完毕,再赋值给dom节点
temp.onload = function(){
// 获取自定义属性data-src,用真图片替换假图片
imgs[i].src = imgs[i].getAttribute('data-src')
}
},2000)
})(i)
}
}
}
lazyload(imgs);
// 滚屏函数
window.onscroll =function(){
lazyload(imgs);
}
}
完结~