基于 Laravel + Vue 构建 API 驱动的前后端分离应用系列(四十一) —— 通过 Laravel Mix + Vue Router 路由懒加载实现单页面应用 JS 文件按组件分割

前言

随着单页面应用体量越来越大,将所有 JavaScript 代码都打包到一个文件,这个 JavaScript 文件也会随之越来越大,这样会造成的一个后果是在浏览器打开应用首页,会出现 JavaScript 文件加载时间过长而导致页面出现一段时间的空白现象,用户体验很不好。

好在 Vue Router 也考虑到这个问题,为我们提供了路由懒加载功能,通过路由懒加载,我们可以将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件及组件对应 JavaScript 代码,这样用户访问的时候页面加载速度就能大幅提升,这篇增补教程就来为大家演示如何在 Roast 应用中实现前端路由懒加载。

如果你还不了解 Vue Router 的路由懒加载功能,建议花点时间去看下,Vue Router 的路由懒加载基于 Vue 的异步组件和 Webpack 的代码分割功能实现,看完官方文档了解实现原理后,可以看下 Vue-Router + Webpack 路由懒加载实现 快速了解实现思路。

在我们的 Roast 应用中,我们是通过框架自带的 Laravel Mix 进行前端资源的编译打包的,Laravel Mix 其实就是对 Webpack 做了一层封装,所以我们可以结合 Vue 异步组件 + Laravel Mix 很轻松的实现 Vue Router 的路由懒加载。

配置 Laravel Mix

由于在实现 Vue 异步组件的时候我们会用到 import() 函数并返回 Promise,而 Laravel Mix 底层使用的是 Babel 进行 JS 编译,所以需要安装 Babel 的 syntax-dynamic-import 插件,Laravel Mix 底层使用的 babel-core 版本是 6.*,对应版本的插件安装命令如下:

npm install --save-dev babel-plugin-syntax-dynamic-import

注:该插件官方文档提供的安装命令安装的插件版本是 7.*,无法适配 Laravel Mix 使用的 Babel 版本,会导致 npm run dev 运行失败。

然后我们需要在项目根目录下创建 .babelrc 用来指定编译时使用这个插件:

{
    "plugins": ["syntax-dynamic-import"]
}

最后,编辑 webpack.mix.js 中的 webpackConfig 配置如下:

 mix.js('resources/assets/js/app.js', 'public/js')
    .webpackConfig({
        output: {
            chunkFilename: 'js/[name].js'
        },
        module: {
            rules: [
                {
                    test: /\.jsx?$/,
                    exclude: /node_modules(?!\/foundation-sites)|bower_components/,
                    use: [
                        {
                            loader: 'babel-loader',
                            options: Config.babel()
                        }
                    ]
                }
            ]
        },
        resolve: {
            alias: {
                '@': path.resolve('resources/assets/sass')
            }
        }
    })
    .sass('resources/assets/sass/app.scss', 'public/css');

我们在 webpackConfig 中新增了一个 output 配置项用于指定编译后 JS 文件存放位置,由于会有多个 JS 文件,所以我们通过 [name] 动态指定对应文件名。

编写 Vue Router 路由懒加载实现代码

做好上述准备工作后,接下来我们将 resources/assets/js/routes.js 中的路由之前引入的组件都改成异步组件:

// 基于 Vue 异步组件 + Webpack 实现路由懒加载
function loadView(dir, view) {
    // 注释不要去掉,对应上面 webpack 编译后的文件名
    return () => import(/* webpackChunkName: "[request]" */ './' + dir + '/' + view + '.vue');
}

// 前端路由定义
export default new VueRouter({
    routes: [
        {
            path: '/',
            redirect: {name: 'cafes'},
            name: 'layout',
            component: loadView('layouts', 'Layout'),
            children: [
                {
                    path: 'cafes',
                    name: 'cafes',
                    component: loadView('pages', 'Home'),
                    children: [
                        {
                            path: 'new',
                            name: 'newcafe',
                            component: loadView('pages', 'NewCafe'),
                            beforeEnter: requireAuth,
                            meta: {
                                permission: 'user'
                            }
                        },
                        {
                            path: ':id',
                            name: 'cafe',
                            component: loadView('pages', 'Cafe')
                        },
                        {
                            path: 'cities/:id',
                            name: 'city',
                            component: loadView('pages', 'City')
                        }
                    ]
                },
                {
                    path: 'cafes/:id/edit',
                    name: 'editcafe',
                    component: loadView('pages', 'EditCafe'),
                    beforeEnter: requireAuth,
                    meta: {
                        permission: 'user'
                    }
                },
                {
                    path: 'profile',
                    name: 'profile',
                    component: loadView('pages', 'Profile'),
                    beforeEnter: requireAuth,
                    meta: {
                        permission: 'user'
                    }
                },
                {
                    path: '_=_',
                    redirect: '/'
                }
            ]
        },
        {
            path: '/admin',
            name: 'admin',
            component: loadView('layouts', 'Admin'),
            beforeEnter: requireAuth,
            meta: {
                permission: 'owner'
            },
            children: [
                {
                    path: 'actions',
                    name: 'admin-actions',
                    component: loadView('pages/admin', 'Actions'),
                    meta: {
                        permission: 'owner'
                    }
                },
                {
                    path: 'companies',
                    name: 'admin-companies',
                    component: loadView('pages/admin', 'Companies'),
                    meta: {
                        permission: 'owner'
                    }
                },
                {
                    path: 'companies/:id',
                    name: 'admin-company',
                    component: loadView('pages/admin', 'Company'),
                    meta: {
                        permission: 'owner'
                    }
                },
                {
                    path: 'companies/:id/cafe/:cafeID',
                    name: 'admin-cafe',
                    component: loadView('pages/admin', 'Cafe'),
                    meta: {
                        permission: 'owner'
                    }
                },
                {
                    path: 'users',
                    name: 'admin-users',
                    component: loadView('pages/admin', 'Users'),
                    meta: {
                        permission: 'admin'
                    }
                },
                {
                    path: 'users/:id',
                    name: 'admin-user',
                    component: loadView('pages/admin', 'User'),
                    meta: {
                        permission: 'admin'
                    }
                },
                {
                    path: 'brew-methods',
                    name: 'admin-brew-methods',
                    component: loadView('pages/admin', 'BrewMethods'),
                    meta: {
                        permission: 'super-admin'
                    }
                },
                {
                    path: 'brew-methods/:id',
                    name: 'admin-brew-method',
                    component: loadView('pages/admin', 'BrewMethod'),
                    meta: {
                        permission: 'super-admin'
                    }
                },
                {
                    path: 'cities',
                    name: 'admin-cities',
                    component: loadView('pages/admin', 'Cities'),
                    meta: {
                        permission: 'super-admin'
                    }
                },
                {
                    path: 'cities/:id',
                    name: 'admin-city',
                    component: loadView('pages/admin', 'City'),
                    meta: {
                        permission: 'super-admin'
                    }
                },
                {
                    path: '_=_',
                    redirect: '/'
                }
            ]
        }
    ]
});

至于异步组件的实现原理,Vue 官方文档上说的很详细,我就不再赘述了,Webpack 在编译路由文件的时候,会自动将异步组件中的 JavaScript 代码单独提取出来,并且在访问对应路由的时候自动加载对应 JavaScript 文件,非常强大,你完全不用关心如何在不同页面加载不同 JavaScript 文件。

测试路由懒加载

代码编写工作到这里就全部结束了,下面我们在项目根目录下运行 npm run dev 重新编译前端资源,可以看到所有组件中的 JavaScript 代码都已经被分割开了:

最终我们的公共 JS 文件 app.js 的体积相比没有分割之前已经减少了很多:

然后我们访问应用首页 http://roast.test,页面加载成功:

F12 查看 JavaScript 文件的加载情况,可以看到只加载了该页面所需要的 JavaScript 文件,而且加载之后就会缓存起来,下次访问这个页面不会重新加载:

学院君 has written 1176 articles

Laravel学院院长,终身学习者

积分:151457 等级:P12 职业:手艺人 城市:杭州

6 条回复

  1. 感谢学院君提供了这么好的练手项目, 我是前端vue, 后端go 跟着这个项目练了下来, 一直到第7部分, 途中虽然有磕磕绊绊, 不过最后还是达到了我练习go和vue的目的. 后面的我大致看了下, 感觉能学到的也都差不多了, 所以准备到这里就收工啦, 最后再次感谢学院君!

  2. Mr_White_DT Mr_White_DT says:

    只是大致做了一遍就感觉学到了很多,感觉以后还要继续研究一下,谢谢学院君!

登录后才能进行评论,立即登录?