环境 vue2,webpack4

公司的vue项目需要做一下前端优化,主要问题是网页首屏加载缓慢。

作为单页应用的vue,往往是第一次加载时间会稍长,后续由于利用浏览器缓存,再次打开就快了。且单页应用将页面都打包到一个js(一般是app.js)文件中,在路由跳转的时候不会有闪屏,通过js动态渲染页面元素,无需发起额外的请求获取页面。

但是,为什么还要优化呢——

vue 遇见了iframe

此次我收到的项目是作为其他项目的内嵌形式来加载的,没错,就是iframe。这个项目通过js来控制iframe,来达到类似浏览器的Tab标签页管理的效果。每次打开页面就生成一个iframe,去加载指定的url(我现在的vue项目的页面)。这样就发挥不了vue路由跳转的好处了,每次都需要加载app.js,虽然已经有缓存了,下载时间可以省略,但是仅运行这个app.js可能都需要1秒左右的时间,这个时间包括浏览器对js文件的读取,预编译,执行等balabala。

所以,如何减小app.js的文件大小,提升首屏加载速度就显得尤为重要了。

webpack有切片的功能,主要体现在提取公共依赖(css,js等静态文件),和组件懒加载

懒加载在vue中意思为,打开页面或变换路由的时候,由浏览器发起请求获取需要用到的文件,路由每次变化都会请求对应.vue文件打包后的.js文件。简单来说就是“按需加载

vue的懒加载在网上许多地方都有说明,需要使用es6 的import()方法来代替原先的require或者import xx from xx的写法。

import User from '@/view/user.vue'
//或者
const User = require('@/view/user.vue')

new VueRouter({
  routes: [
    { path: '/user', component: User }
  ]
})

//----------------
//变成下面这样的写法
//----------------

new VueRouter({
  routes: [
    { path: '/user', component: () => import('@/view/user.vue') }
  ]
})
// 这样访问/user路由获取app.js后,浏览器还会再请求user.js(webpack chunkFilename)

有的文章还提到babel的syntax-dynamic-import,才能使用import()语法。

首先要知道babel常常用于ES6或更高版本的ES特性转换为es5或更低版本的代码,以应对一些特殊环境和浏览器的需要。

而现在es2020已经支持import()方法了,我并未引入syntax-dynamic-import,也可以正常运行import()函数

现在问题来了,我使用() => import()写法,webpack并未将每个.vue文件单独打包成一个文件。

作为小白经几日研究,发现是代码中原先的的require()造成的

因为我们这个项目比较特殊,有多个路由配置文件举例如下三个同目录文件

/* router/router1.js */
exports default [
    { path: '/r1', component: 'r1' }
]

/* router/router2.js */
exports default [
    { path: '/r2', component: 'r2' }
]

/* router/index.js */
let pathArr = ['router/router1.js','router/router2.js'];
let routersArr = [];
pathArr.forEach( path => { // 多个文件中获取路由数组
    // 坑
    let router = require(`@/router/${path}.js`).default;// 因为文件使用es6的 export default
    routersArr = routersArr.concat(router);
});
routersArr.forEach( item => { // 添加component前后缀
    item.component = require('@/view/${item.component}.vue').default;
});

new VueRouter({
    routes: routesArr
});

这个是通过动态载入的路由,也就是程序运行了才知道require的是什么文件。

经过多方了解和学习后,终于知道了为什么不能分片的原因。

我们都知道webpack配置中需要指定一个entry的入口文件(大多是main.js),那么这个入口文件中大体的结构一般是这样写的

/* main.js */
import Vue from 'vue';
import App from './app.vue';
import routes from './routes/index.js'; //如果文件名为index的话可以省略,如'./routes'

//还可以引入 css框架,i18n,其他组件等,省略
//.....

new Vue({
    routes,
    render: h => h(App)
}).$mount('#app');

webpack进行的是预编译工作,是不执行js文件的。(不信把入口文件的import xxx from xxx 删除,还是能够打包成功)

  1. 当webpack 在main.js文件中发现import x from x语法的时候,如import routes from './routes'。
  2. 进入routes/index.js文件中分析导入语法,当碰到require()语法的时候。
  3. 重点来了,如果在入口或入口依赖的任何文件中,有使用例如require('@/view/' + 变量 + '.js')这么来导入的话,那么,webpack将会匹配'@/view/'路径下面所有的.js文件进行打包成一个文件。因为webpack是不执行js文件的,所以并不知道实际执行的时候require的参数是什么,会忽略变量进行匹配。同理也不建议require()传入其他函数。
  4. routes/index.js等文件的依赖收集完毕后,打包到main.js文件中。
  5. 若导入其他的文件被识别有相同的依赖,则webpack不会重复打包。(具体还未深入了解)

这就是为什么() => import()语法没有被webpack打包成单一的vue文件的原因。

require()将符合条件的文件打包到一个文件中,() => import()会将符合条件的文件单独打包。

而且我发现 require() 语法和import()语法同时存在时,在webpack3和4中 都是优先处理 require()语法的。表现为已经根据上面第3步收集的依赖,import()不会重复收集,所以就没有将.vue文件单独打包成为.js文件了。

解决办法就是不要滥用require(),参数尽量写详细!在webpack根据require()来匹配收集依赖后,import()匹配的依赖没有被收集,那么import()中的依赖就会被单独打包出来。

我的解决办法如下:

/* router/index.js */
let router1 = require('router/router1.js').default;
let router2 = require('router/router2.js').default;

let routersArr = [...router1, ...router2];

routersArr.forEach( item => { // 添加component前后缀
    // 现在使用import就可以了
    item.component = () => import('@/view/${item.component}.vue'); // webpack将会把view/目录下的.vue文件单独打包出来
});

new VueRouter({
    routes: routesArr
});

相信一定有更灵活的解决办法。

有人说这么做了怎么还是没有单独打包文件出来呢。

  1. 我认为还是仔细检查一下入口文件(main.js)的import from依赖,可能其他依赖中也在require()参数中传入了其他参数,导致webpack匹配了变参外的条件。
  2. 确保import()中的依赖,没有先被require()匹配收集。

补充chunkFilename:

关于webpack配置中output的chunkFilename 问题,使用'[name].js'来指定文件名称,输出仍然是id.js的问题

已知两种解决办法

// 第三个参数指定chunkFilename文件名称,不要动态参数----参考资料2,3
require.ensure([], (require) => require(filePath), 'fileName'); 

// webpack magic comment 使用webpack注释指定chunkFilename文件名称----参考资料3,4
// fileName相同的文件会打包到一起
() => import(/*webpackChunkName: "fileName"*/ filePath); 

//若import使用动态参数    ----参考资料3,4
() => import(/*webpackChunkName: "[request]"*/ filePath);

结论:

入口文件或入口文件依赖中,使用require(变量)方法导入,导致与import()的组件冲突

参考资料

  1. es6 import()函数
  2. 代码分割 - 使用 require.ensure
  3. webpack中动态import()打包后的文件名称定义
  4. Webpack Magic Comments(注释指定import名称)

若有错误,敬请指出更正

Logo

前往低代码交流专区

更多推荐