讓prerender-spa-plugin支持cdn域名的幾種嘗試

react、vue等前端spa框架應用到2c網站的問題之一是較長的白屏時間和不支持seo,prerender是解決這些問題的方案之一。在實踐中我也比較推薦這種方式,其開發成本和維護難度都比server side render(SSR)低不少,性價比突出。css

一般採用的這個webpack插件:prerender-spa-plugin,其實現原理也很簡單:在webpack打包結束並生成文件後(after-emit hook),啓動一個server模擬網站的運行,用puppeteer(google官方的headless chrome瀏覽器)訪問指定的頁面route,獲得相應的html結構,並將結果輸出到指定目錄,過程相似於爬蟲。html

但實際應用到生產項目,仍是會有一些具體問題須要解決。其中最多見的,就是cdn域名問題——網站靜態資源的域名(一般在publicPath或baseUrl處設定)與網站主域不一樣,在webpack打包過程當中,css,js等文件還未上線也即未推送到cdn節點上,致使prerender失敗。前端

方法一 double build

build兩次。第一次將build後的css,js等靜態資源rsync到cdn服務器。第二次再build時就能夠訪問到cdn資源了:vue

{
  "private": true,
  "scripts": {
    "dev": "node build/dev-server.js",
    "prebuild": "node build/prebuild.js && rsync -r dist/static/* example.com:/sites/example/static",
    "build": "npm run prebuild && node build/build.js",
  }
}
複製代碼

這種方式build了兩次,成本高並且顯得dumb。並且實際狀況中爲了保證安全,打包機跟生產機未必可以直通。node

方法二 build後用腳本修改域名

換一種思路,先將webpack的publicPath配置成與prerender同域,即cdn域名指向prerender-spa-plugin啓動的server端口如//127.0.0.1:13010,build後再替換html,js等文件中的域名。react

// vue.config.js
publicPath: '//127.0.0.1:13010/'
複製代碼

修改package.json中的npm scripts:webpack

"scripts": {
  "build": "vue-cli-service build && node replaceCDN.js",
}
複製代碼

replaceCDN.jsnginx

//字符串替換部分,其餘忽略:
  fs.readFile(filename, { flag: 'r+', encoding: 'utf8' }, function (err, data) {
    if (err) {
      console.error(err)
    }
    var replacedContent = data.replace(/\/\/127.0.0.1:13010\//g, cdnPath)
    // write file
    writeFile(filename, replacedContent)
  })

複製代碼

方法三 利用webpack的全局變量和正則替換

__webpack_public_path__是一個webpack暴露的全局變量,能夠在運行時設置publicPath,相關文檔,能夠用於項目運行後動態加載的js/css修改爲cdn域名。git

新增public-path.jsgithub

const isPrerender = window.__PRERENDER_INJECTED__ === 'prerender'
__webpack_public_path__ = isPrerender ? '' : 'http://www.cdn.com'
複製代碼

在main.js的最開始import這個文件

import './public-path'
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})
複製代碼

注意__webpack_public_path__只能修改動態加載的css/js域名,打包時生成在html內的域名還須要修改prerender-spa-plugin的配置,在postProcess回調內進行字符串替換:

// webpack.prod.conf.js或vue.config.js
//...
    new PrerenderSPAPlugin({
      staticDir: config.build.assetsRoot,
      routes: [ '/', '/about', '/contact' ],
      postProcess (renderedRoute) {
        // add CDN
        renderedRoute.html = renderedRoute.html.replace(
          /(<script[^<>]*src=\")((?!http|https)[^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig, `$1${config.build.cdnPath}$2$3` ).replace( /(<link[^<>]*href=\")((?!http|https)[^<>\"]*)(\"[^<>]*>)/ig, `$1${config.build.cdnPath}$2$3` ) return renderedRoute }, renderer: new Renderer({ injectProperty: '__PRERENDER_INJECTED__', inject: 'prerender' }) }) 複製代碼

完整的demo請參考dhgan的github項目。但也有問題,打出來的html引用地址會出現雙斜槓//,如http://127.0.0.1:8083//static/js/app.56447ec298fb275735eb.js

方法四 打包機代理cdn域名

我目前採用的方式是用打包機自己來作cdn代理。固然可使用nginx作這個事兒,但爲了工程化方便,我用的是express結合http-proxy-middleware。 這個方法有個前提條件,打包機自己不能有80端口占用。以vue-cli3作腳手架的項目來介紹一下實現思路:

首先配置/etc/hosts,假設cdn域名爲www.cdn.com:

127.0.0.1 www.cdn.com
複製代碼

配置 vue.config.js:

const isProd = process.env.NODE_ENV === 'production'
module.exports = {
  publicPath: isProd ? '//www.cdn.com/' : '/',
  configureWebpack: {
    plugins: isProd ? [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, 'dist'),
        server: {
          port: 13010
        },
        routes: [
          '/',
          '/about',
          '/contact'
        ]
      })
    ] : []
  }
 }
複製代碼

修改package.json的npm scripts:

"scripts": {
    "start": "npm run serve",
    "serve": "vue-cli-service serve --open",
    "build": "node prerender.js \"vue-cli-service build\""
  },
複製代碼

prerender.js:

var { spawn } = require('child_process')
var express = require('express')
var proxyMiddleware = require('http-proxy-middleware')
var app = express()

function makeProxy (renderPort) {
  var options = {
    target: `http://localhost:${renderPort}`,
    changeOrigin: true
  }
  app.use(proxyMiddleware('/', options))
  app.listen(80)
}

makeProxy(13010)
//爲了保持子進程的顏色輸出
process.env.FORCE_COLOR = true

const [str0, ...rest] = process.argv[2].split(/\s/)
const cmd = spawn(str0, rest, { env: process.env })
cmd.stdout.on('data', function (data) {
  process.stdout.write(data)
})

cmd.stderr.on('data', function (data) {
  process.stderr.write(data)
})

cmd.on('exit', function (code) {
  process.exit(0)
})

複製代碼

上面代碼啓動了一個proxy server來代理cdn的請求,並將@vue/cli3的build過程經過spawn子進程的方式結合進來。

demo參見dunhuang的vue cli3方案

還提供一個cli2.x的版本 dunhuang的vue cli2.x版本方案

總結

能夠說幾種方法各有利弊,實際採用哪一種方法還要結合具體場景和生產打包機的環境情況,最佳實踐可能永遠是下一種。

(原創文章,轉載須註明做者及來源)

相關文章
相關標籤/搜索