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失敗。前端
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
換一種思路,先將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_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代理。固然可使用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版本方案
能夠說幾種方法各有利弊,實際採用哪一種方法還要結合具體場景和生產打包機的環境情況,最佳實踐可能永遠是下一種。
(原創文章,轉載須註明做者及來源)