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], ()=> {n const Swiper = require(swiper);n ......n }, swiper);n

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) => {n // 通過注釋webpackChunkName 指定生成的chunk名稱n import(/* webpackChunkName: "asyncSwiper" */ ./AsyncSwiper.vue)n});nn<div id="app">n<p>Vue dynamic component load</p><async-swiper></async-swiper> n</div>n

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;nconst path = require(path);nconst fs = require(fs);nconst mkdirp = require(mkdirp);nnclass VueSSRDynamicChunkPlugin {n constructor(opts) {n this.opts = Object.assign({ }, { chunk: true }, opts);n }nn apply(compiler) {n compiler.plugin(emit, (compilation, callback) => {n const buildPath = compilation.options.output.path;n compilation.chunks.forEach(chunk => {n // 找到所有的chunk文件,然後拷貝一份到 編譯目錄的 node_modules 下面n if (this.opts.chunk && chunk.name === null) {n chunk.files.forEach(filename => {n const filepath = path.join(buildPath, node_modules, filename);n const source = compilation.assets[filename].source();n mkdirp.sync(path.dirname(filepath));n fs.writeFileSync(filepath, source, utf8);n });n }n });n callback();n });n }n}nnmodule.exports = VueSSRDynamicChunkPlugin;n

這樣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 規範目錄。

config/config.default.js

const path = require(path);nconst fs = require(fs);nmodule.exports = app => {n const exports = {};nn exports.vuessr = {n renderOptions: {n // 告訴 vue-server-renderer 去 app/view 查找非同步 chunk 文件n basedir: path.join(app.baseDir, app/view)n }n };nn return exports;n};n

3.3 動態載入舉例

<template>n <layout>n <div>n <div class="first">動態動態渲染</div>n <div class="second">n <!-- <component :is="name"></component> -->n <async></async>n </div>n </div>n </layout>n</template>n<style lang="scss">n@import "./dynamic.scss";n</style>n<script type="text/babel">n export default {n name: dynamic,n data () {n return {n show: truen }n },n components: {n async : () => import(./component/async.vue)n }n }n</script>n

app/web/page/dynamic/dynamic.vue


推薦閱讀:

webpack 進階
前端構建系統 Gulp 的使用與常用插件推薦 - 上篇
2016前端探索總結——前端工程與未來
用 husky 和 lint-staged 構建超溜的代碼檢查工作流
2.1 前端工程化概述

TAG:webpack | eggjs | 前端工程化 |