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版本方案
能够说几种方法各有利弊,实际采用哪一种方法还要结合具体场景和生产打包机的环境情况,最佳实践可能永远是下一种。
(原创文章,转载须注明做者及来源)