Egg + Vue SSR 组件异步加载
1. JavaScript File Code Spliting 代码分离
Webpack打包是把所有js代码打成一个js文件,我们可以通过 CommonsChunkPlugin
分离出公共组件,但这远远不够。 实际业务开发时,一些主要页面内容往往比较多, 而且会引入第三方组件。其中有些内容的展示不再首屏或者监控脚本等对用户不是那么重要的脚本我们可以通过 require.ensure
代码分离延迟加载。在 Webpack在构建时,解析到require.ensure
时,会单独针对引入的js资源单独构建出chunk文件,这样就能从主js文件里面分离出来。 然后页面加载完后, 通过script标签的方式动态插入到文档中。
require.ensure 使用方式, 第三个参数是指定生产的chunk文件名,不设置时是用数字编号代理。相同require.ensure只会生产一个chunk文件。
require.ensure(['swiper'], ()=> { const Swiper = require('swiper'); ...... }, 'swiper');
2. Vue Component Code Spliting 代码分离
- 我们在用 Vue 开发业务时,往往是把某个功能封装成 Vue 组件,Vue 动态组件加载 相比 纯js 加载更有实际意义。
- 异步加载 Vue 组件(.vue) 已在 Vue 2.5+ 版本支持,包括路由异步加载和非路由异步加载。在具体实现时,我们可以通过
import(filepath)
加载组件。 import()
方案已经列入ECMAScript提案,虽然在提案阶段,但 Webpack 已经支持了该特性。import() 返回的 Promise,通过注释 webpackChunkName 指定生成的 chunk 名称。 Webpack 构建时会独立的 chunkjs 文件,然后在客户端动态插入组件,chunk 机制与 require.ensure 一样。有了动态加载的方案,可以减少服务端渲染 jsbundle 文件的大小,页面 Vue 组件模块也可以按需加载。
Vue.component('async-swiper', (resolve) => { // 通过注释webpackChunkName 指定生成的chunk名称 import(/* webpackChunkName: "asyncSwiper" */ './AsyncSwiper.vue')});<div id="app"><p>Vue dynamic component load</p><async-swiper></async-swiper> </div>
3. Egg + Vue SSR Vue Component Code Spliting
在 Egg + Vue SSR 项目使用 import 异步加载技术时,在服务端渲染时,发现构建的异步asyncSwiper.js 文件找不到,然后根据错误定位发现是 vue-server-renderer 插件里面查找异步组件文件找不到。 我们知道,Node 端查找文件都是通过 require 引入文件的,查找路径也是一层一层想上查找 node_modules 目录下面的文件。但 Webpack 构建生成的不会有node_modules 目录呀,所以导致报错。虽然 vue-server-renderer 的 rendererOptions 提供了 basedir 配置,但也是查找指定的 node_modules 下的文件。问题就卡在这里。既然是查找 node_modules 目录, 而 构建时根本就不存在改目录,那就自己创建 node_modules,然后把 异步文件拷贝到这个 node_modules 不就行了。经过测试,确实可以。目前在 easywebpack-vue ^3.5.1 版本内置支持了。
vue-server-renderer 插件查找文件用到了 resolve 插件, resolve 插件虽然提供了扩展参数,但目前 vue-server-renderer 插件没有暴露出来,后面看看官方是否可以提供扩展参数支持。如果你有更好的方式或者使用方式不对,可以留言告知一下。
3.1 easywebpack-vue SSR 异步组件构建支持
在 Webpack 构建时,通过自定义 Webpack 插件 VueSSRDynamicChunkPlugin 解决上面的问题,具体逻辑如下:
'use strict';const path = require('path');const fs = require('fs');const mkdirp = require('mkdirp');class VueSSRDynamicChunkPlugin { constructor(opts) { this.opts = Object.assign({ }, { chunk: true }, opts); } apply(compiler) { compiler.plugin('emit', (compilation, callback) => { const buildPath = compilation.options.output.path; compilation.chunks.forEach(chunk => { // 找到所有的chunk文件,然后拷贝一份到 编译目录的 node_modules 下面 if (this.opts.chunk && chunk.name === null) { chunk.files.forEach(filename => { const filepath = path.join(buildPath, 'node_modules', filename); const source = compilation.assets[filename].source(); mkdirp.sync(path.dirname(filepath)); fs.writeFileSync(filepath, source, 'utf8'); }); } }); callback(); }); }}module.exports = VueSSRDynamicChunkPlugin;
这样vue-server-renderer
在查找异步渲染查找 chunk
文件逻辑。这里直接把 chunk
文件构建到 app/view/node_modules
下面, 这样异步渲染才能找到该文件。
3.2 项目添加 egg-view-vue-ssr 插件参数配置
如果要使用异步渲染功能,还需要配置 egg-view-vue-ssr 的 renderOptions 的 basedir 属性, 告诉 vue-server-renderer 去 app/view/node_modules 查找异步组件。这里查找目录设置成 app/view 是因为 服务端构建的文件是放在 app/view 目录,这个目录也是 Egg 项目的 View 规范目录。
const path = require('path');const fs = require('fs');module.exports = app => { const exports = {}; exports.vuessr = { renderOptions: { // 告诉 vue-server-renderer 去 app/view 查找异步 chunk 文件 basedir: path.join(app.baseDir, 'app/view') } }; return exports;};
3.3 动态加载举例
<template> <layout> <div> <div class="first">动态动态渲染</div> <div class="second"> <!-- <component :is="name"></component> --> <async></async> </div> </div> </layout></template><style lang="scss">@import "./dynamic.scss";</style><script type="text/babel"> export default { name: 'dynamic', data () { return { show: true } }, components: { async : () => import('./component/async.vue') } }</script>