从Laravel Mix (4.0.16)起,开始默认支持动态引入了(dynamic imports)。动态引入是一种资源文件分割(code-splitting)技术,可以让我们将js组件、第三方库及其他模块分割到单独的文件里。比如一个项目里,你用到了好些个js组件,有很多的vuejs或react组件,那么你最终打包的app.js或bundle.js文件就会很大,超过1M+是常有的事儿。这个时候,势必就会降低你的网站加载速度,尤其当用户处于一个慢的连接条件下时。

资源文件分割(code-splitting),可以将原本一个大文件,切分成很多小的文件,这样每个就可能只有几十几百K,而不是多少M了,这样就能极大改善加载速度。由于都切分开了,你就可以一开始只加载最想要的那块js文件,只加载页面上用到的,那些暂时用不到的,或者在其他页面才用到的,可以在背后自动去下载,就不影响一开始的页面渲染时间了。

配置动态引入

首先你得升级Laravel Mix到4.0.16以上的版本,才能使用动态引入。

通过.babelrc文件来配置

然后呢,在你的项目根目录创建一个.babelrc文件,当然如果你已经有这个文件了,就在以前基础上更改即可。在这个文件里,将@babel/plugin-syntax-dynamic-import加到“plugins”这个array里面,这样呢就会启用laravel mix里已经带的自动引入插件。

{
  "plugins": [
    "@babel/plugin-syntax-dynamic-import"
  ]
}

通过webpack.mix.js文件来配置

当然了,如果你不想创建上面的.babelrc文件,你对babel及其配置并不熟悉,你也可以在mix配置文件,也即webpack.mix.js里做配置,可以加上下面的代码:

mix.babelConfig({
  plugins: ['@babel/plugin-syntax-dynamic-import'],
});

这样你就不用创建或更改.babelrc文件了

实际使用动态引入

原来我们引入一个js组件,经常是这样的:

// 标准的,或静态的引入
import Component from './components/ExampleComponent.vue';

现在,如果我们想着动态地引入一个组件,也即只有当这个组件在页面用到的时候,才去引入和加载,那么可以这样来写:

// 动态引入
const Component =
        () => import('./components/ExampleComponent.vue');

默认的,Webpack会将这些动态引入的组件切块,切成单独的文件,以0.js, 1.js这样的形式来命名。

Laravel Mix的在命名上,可以使用每个切块的名字,跟上这块内容的hash值,然后跟上.js扩展。如果你想着具体设定每个分块文件的名字,那么在动态引入的时候,可以在组件路径前面,加上下面这样的一段注释

const Component =
        () => import(/* webpackChunkName: "dynamically-imported-component" */ './components/ExampleComponent.vue');

可以看到这里,在vue组件具体路径前面,有一段/* webpackChunkName: "dynamically-imported-component" */ 的注释,这就告诉webpack,我想要这块文件用这么个名字。那么这样,最终产生的文件名就是dynamically-imported-component.js

注意这个时候,这些分块的chunk文件,是直接放到你的public目录下的,这不是我们想要的,我们想让它们起码是在public/js目录下,那么这个可以在你设置分块名字时声明一下路径,比如/* webpackChunkName: "js/dynamic-component" */ ,注意这里我们在具体文件名之前,加了个js/路径,这样它就可以放到public/js目录下了。

那么更进一步地,我们之前说了,可以在每个生成的文件名上加上这块内容的hash值,也即简单说文件名里包含一大堆随机字符,那么这个呢,可以在webpack.mix.js里加上chunk分块文件的输出配置,像下面这样:

mix.webpackConfig({
    output: {
        chunkFilename: 'js/[name].[contenthash].js',
    }
});

可以看到,最关键的是这里文件名里加了[contenthash],这样文件名上就包含有哈希值了,同时留意这里呢也声明了一个js/的路径,这样的话,你在具体动态引入某个文件的时候,就可以不用声明路径了,就可以统一都放到public/js目录下了。

在Vue-Router里使用动态引入

如果你项目里用到了Vue-Router,那么也就可以用动态引入,来将每个页面切分成单独的文件,可以像下面这样做:

const routes = [
  {
    name: 'dashboard',
    path: '/dashboard',
    component:
      () => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard.vue'),
  },
];

[Vue warn]: Failed to resolve async component 问题的解决

可能你会非常兴奋地用上动态引入,可能你开发环境下使用npm run devnpm run watch-poll一切都正常,可是呢,当你到了npm run prod时,或者你npm run dev也会有,你会发现你动态引入的组件,有的正常工作,有的呢却无法加载了,页面上没有任何效果了。尤其是当你dev编译是好的,但prod编译却是不好的,这种时候就最难debug,很难找到问题发生在哪里。

这个时候,如果你用dev编译有问题的组件,往往会看到类似这么个报错:

[Vue warn]: Failed to resolve async component: function () {
  return __webpack_require__.e(/*! import() */ 6).then(__webpack_require__.bind(null, /*! ./components/bad.vue */ "./resources/js/components/bad.vue"));
}
Reason: TypeError: modules[moduleId] is undefined vue.common.dev.js:630

说无法解析某个异步组件,这是为啥呢?经试验,发现这是因为你这个组件里有<style>标签,对,就是vue组件里都有的那个<style>标签,原因是呢,动态引入编译的时候,相应的style-loader没有被加载上,这导致了有<style>标签的vue组件编译失败,很多人发现将<style>标签删掉以后,就正常编译了。

但是,我们用vue组件,怎么可能不用<style>标签呢?尤其是你先前已经有了很多样式时,你不可能轻松都移走吧。那么这个呢,似乎是一个官方的bug,更确切的说是webpack的一个缺陷,Mix的作者曾说在webpack 5发布之前,mix无法完全解决这个问题。

我们可不想听到这种解释,群众的力量向来是无穷的,于是有人发现,通过在app.js引入一个空白的scss文件,可以让mix在编译动态引入的组件前,就强行加载上style-loader,这样就能编译正常了。

具体做法是你随意创建一个空白的scss文件,比如就叫empty.scss,然后呢在app.js里引入它,类似这样

import './empty.scss'

就这样,这样你的动态引入,编译就应该没问题了。

高级用法:脚本预加载

这样已经能够提升你不少页面的加载速度了,但是我们还不满足。现在假设有两个页面,分别是A页面和B页面,A页面是一个很轻量的、没啥组件的页面,而B页面假设用到了我们分离出来的一个my-component.js

那么这个时候,如果一个用户访问了页面A,然后他又点击了页面B,这个时候他就得等待my-component.js这个文件去下载和处理——也即我们加速了页面A的加载,但是却降低了B页面的时间。因为之前不分离js文件的时候,当访问了A时,app.js文件就已经完全下载了,再访问B页面时就不需要请求了,就一般从浏览器缓存直接加载了。

如果怎么样,我们能让浏览器更智能一些就好了,比如说访问了A页面,我们就能预计他会访问B页面,这时浏览器可以在加载完A页面的脚本和资源以后,在合适的时候后台自己预先抓取B页面要用到的脚本,这样当访问B页面时,相关文件已经准备好了。

当然了,这肯定不只是个想法了,其实现在的浏览器都已经支持这样的做法了,使用传统的<link>标签,我们其实就可以声明对某个文件的预加载,相当于在浏览器空闲的时候去背后获取。

<link
    rel="prefetch"
    href="/js/my-component.js"
    as="script"
>

这样了以后,当访问了A页面,速度依然很快,当脚本都加载完了,浏览器就自动去获取my-component.js文件了,当我们再访问到B页面时,也就不需要临时再加载什么文件了,已经都提前获取并放到缓存里了。

结论

这么好的一项功能,赶紧升级一下Mix,然后用到你的项目里吧,相信能提升不少你的网站加载速度。

本文是我们系列课程《Laravel&Vue深度整合实战第二版》的扩展文章,还记得课程里我们用到了Element-Ui,也即饿了么开发的vue ui组件,期间我们只是用了Element-Ui的几个模块,就导致我们的app.js文件瞬间膨胀到1M以上,在实际中,如果你严重依赖很多UI组件,那么你的打包文件好几M也都是很正常的。原来呢,我们得自行在webpack里进行各种设置,要么只是输出我们实际用到的UI模块,要么就自行实现今天说的文件分割效果,这对不怎么熟悉webpack的同学来说,挺恐怖的。那么现在,Mix默认简单地支持了资源文件分割效果,就大大解决了我们这方面的顾虑,让你在很多页面的效果体验上就可以更大胆、更有空间地做些事情了。