Vue頁面骨架屏(二)


實現思路

參考原文中在構建時使用 Vue 預渲染骨架屏一節介紹的思路,我將骨架屏也看成路由組件,在構建時使用 Vue 預渲染功能,將骨架屏組件的渲染結果 HTML 片段插入 HTML 頁面模版的掛載點中,將樣式內聯到 head 標簽中。這樣等前端渲染完成時,Vue 將使用客戶端混合,把掛載點中的骨架屏內容替換成真正的頁面內容。

有了以上思路,讓我們看看如何為一個簡單的 Vue 應用添加骨架屏。

具體實現

為此我開發了一個 webpack 插件:vue-skeleton-webpack-plugin。下面將從以下三方面介紹部分實現細節:

  • 使用 Vue 預渲染骨架屏
  • 將骨架屏渲染結果插入 HTML 模版中
  • 開發模式下插入各個骨架屏路由

使用 Vue 預渲染骨架屏

我們使用 Vue 的預渲染功能渲染骨架屏組件,不熟悉的同學可以先閱讀官方文檔中的基本用法一節。

首先需要創建一個僅使用骨架屏組件的入口文件:

// src/entry-skeleton.js

import Skeleton from './Skeleton.vue';
// 創建一個骨架屏 Vue 實例
export default new Vue({
    components: {
        Skeleton
    },
    template: '<skeleton />'
});

接下來創建一個用於服務端渲染的 webpack 配置對象,將剛創建的入口文件指定為 entry 依賴入口:

// webpack.skeleton.conf.js
{
    target: 'node', // 區別默認的 'web'
    entry: resolve('./src/entry-skeleton.js'), // 多頁傳入對象
    output: {
        libraryTarget: 'commonjs2'
    },
    externals: nodeExternals({
        whitelist: /\.css$/
    }),
    plugins: []
}

這里只展示單頁應用的情況,在多頁應用中,指定 entry 為包含各個頁面入口的對象即可。關於多頁中的 webpack 配置對象示例,可參考插件的多頁測試用例或者Lavas MPA 模版

然后我們將這個 webpack 配置對象通過參數傳入骨架屏插件中。

// webpack.dev.conf.js
plugins: [
    new SkeletonWebpackPlugin({ // 我們編寫的插件
        webpackConfig: require('./webpack.skeleton.conf')
    })
]

骨架屏插件運行時會使用 webpack 編譯這個傳入的配置對象,得到骨架屏的 bundle 文件。接下來只需要使用這個 bundle 文件內容創建一個 renderer,調用renderToString()方法就可以得到字符串形式的 HTML 渲染結果了。由於我們不需要將過程中的文件產物保存在硬盤中,使用內存文件系統memory-fs即可。

// vue-skeleton-webpack-plugin/src/ssr.js

const createBundleRenderer = require('vue-server-renderer').createBundleRenderer;
// 從內存文件系統中讀取 bundle 文件
let bundle = mfs.readFileSync(outputPath, 'utf-8');
// 創建 renderer
let renderer = createBundleRenderer(bundle);
// 渲染得到 HTML
renderer.renderToString({}, (err, skeletonHtml) => {});

默認情況下,webpack 模塊引用的樣式內容是內嵌在 JavaScript bundle 中的。官方插件 ExtractTextPlugin可以進行樣式分離。我們也使用這個插件,將骨架屏樣式內容輸出到單獨的 CSS 文件中。 關於插件更多用法,可參考官方文檔或者 Vue 基於 webpack 的模版

// vue-skeleton-webpack-plugin/src/ssr.js

// 加入 ExtractTextPlugin 插件到 webpack 配置對象插件列表中
serverWebpackConfig.plugins.push(new ExtractTextPlugin({
    filename: outputCssBasename // 樣式文件名
}));

至此,我們已經得到了骨架屏的渲染結果 HTML 和樣式內容,接下來需要關心如何將結果注入 HTML 頁面模版中。

注入渲染結果

Vue webpack 模版項目使用了 HTML Webpack Plugin生成 HTML 文件。參考該插件的 事件說明,我們選擇監聽html-webpack-plugin-before-html-processing事件,在事件的回調函數中,插件會傳入當前待處理的 HTML 內容供我們進一步修改。

我們知道骨架屏組件最終的渲染結果包含 HTML 和樣式兩部分,樣式部分可以直接插入 head 標簽內,而 HTML 需要插入掛載點中。插件使用者可以通過參數設置這個掛載點位置,默認將使用<div id="app">

看起來一切都很順利,但是在多頁應用中,情況會變的稍稍復雜。多頁項目中通常會引入多個 HTML Webpack Plugin,例如我們在 Lavas MPA 模版中使用的 Multipage Webpack 插件就是如此,這就會導致html-webpack-plugin-before-html-processing事件被多次觸發。

在多頁應用中,我們傳給骨架屏插件的 webpack 配置對象是包含多個入口的:

// webpack.skeleton.conf.js
entry: {
    page1: resolve('./src/pages/page1/entry-skeleton.js'),
    page2: resolve('./src/pages/page2/entry-skeleton.js')
}

這就意味着每次html-webpack-plugin-before-html-processing事件觸發時,骨架屏插件都需要識別出當前正在處理的入口文件,執行 webpack 編譯當前頁面對應的骨架屏入口文件,渲染對應的骨架屏組件。查找當前處理的入口文件過程如下:

// vue-skeleton-webpack-plugin/src/index.js

// 當前頁面使用的所有 chunks
let usedChunks = htmlPluginData.plugin.options.chunks;
let entryKey;
// chunks 和所有入口文件的交集就是當前待處理的入口文件
if (Array.isArray(usedChunks)) {
    entryKey = Object.keys(skeletonEntries);
    entryKey = entryKey.filter(v => usedChunks.indexOf(v) > -1)[0];
}
// 設置當前的 webpack 配置對象的入口文件和結果輸出文件
webpackConfig.entry = skeletonEntries[entryKey];
webpackConfig.output.filename = `skeleton-${entryKey}.js`;
// 使用配置對象進行服務端渲染
ssr(webpackConfig).then(({skeletonHtml, skeletonCss}) => {
    // 注入骨架屏 HTML 和 CSS 到頁面 HTML 中
});

至此,我們已經完成了骨架屏的渲染和注入工作,接下來有一個開發中的小問題需要關注。

開發模式下插入路由

前面說過,由於 Vue 會使用客戶端混合,骨架屏內容在前端渲染完成后就會被替換,那么如何在開發時方便的查看調試呢?

使用瀏覽器開發工具設置斷點是一個辦法,但如果能在開發模式中向路由文件插入骨架屏組件對應的路由規則,使各個頁面的骨架屏能像其他路由組件一樣被訪問,將使開發調試變得更加方便。向路由文件插入規則代碼的工作將在插件的 loader中完成。如果您對 webpack loader 還不了解,可以參閱官方文檔

我們需要向路由文件插入兩類代碼:引入骨架屏組件的代碼和對應的路由規則對象。關於代碼插入點,引入組件代碼相對簡單,放在文件頂部就行了,而路由規則需要插入路由對象數組中,目前我使用的是簡單的字符串匹配來查找這個數組的起始位置。例如下面的例子中,需要向 loader 傳入routes: [來確定插入路由的位置。

// router.js

import Skeleton from '@/pages/Skeleton.vue'
routes: [
    { // 插入骨架屏路由
        path: '/skeleton',
        name: 'skeleton',
        component: Skeleton
    }
    // ...其余路由規則
]

在多頁應用中,每個頁面對應的骨架屏都需要插入代碼,使用者可以通過占位符設置引入骨架屏組件語句和路由規則的模版。loader 在運行時會使用這些模版,用真實的骨架屏名稱替換掉占位符。在下面的例子中,假設我們有Page1.skeleton.vuePage2.skeleton.vue這兩個骨架屏,開發模式下可以通過/skeleton-page1/skeleton-page2訪問這兩個骨架屏路由。更多參數說明可以參考這里。

// webpack.dev.conf.js

module: {
    rules: [] // 其他規則
        .concat(SkeletonWebpackPlugin.loader({
            resource: resolve('src/router.js'), // 目標路由文件
            options: {
                entry: ['page1', 'page2'],
                importTemplate: 'import [nameCap] from \'@/pages/[name]/[nameCap].skeleton.vue\';',
                routePathTemplate: '/skeleton-[name]'
            }
        }))
} 

demo

multidemo

文章轉載自https://juejin.im/entry/5ab37beb518825557005eecc


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM