Ajax 技术的出现,让我们的 Web 应用能够在不刷新的状态下显示不同页面的内容,这就是单页应用。在一个单页应用中,往往只有一个 html 文件,然后根据访问的 url 来匹配对应的路由脚本,动态地渲染页面内容。单页应用在优化了用户体验的同时,也给我们带来了许多问题,例如 SEO 不友好、首屏可见时间过长等。服务端渲染(SSR)和预渲染(Prerender)技术正是为解决这些问题而生的。
SEO(Search Engine Optimization):汉译为搜索引擎优化。利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。
客户端渲染:用户访问 url,请求 html 文件,前端根据路由动态渲染页面内容。关键链路较长,有一定的白屏时间;
服务端渲染:用户访问 url,服务端根据访问路径请求所需数据,拼接成 html 字符串,返回给前端。前端接收到 html 时已有部分内容;
预渲染:构建阶段生成匹配预渲染路径的 html 文件(注意:每个需要预渲染的路由都有一个对应的 html)。构建出来的 html 文件已有部分内容。
个性化内容:对于路由是 /my-profile 的页面来说,预渲染就失效了。因为页面内容依据看它的人而显得不同;
经常变化的内容:如果你预渲染一个游戏排行榜,这个排行榜会随着新的玩家记录而更新,预渲染会让你的页面显示不正确直到脚本加载完成并替换成新的数据。这是一个不好的用户体验;
成千上万的路由:不建议预渲染非常多的路由,因为这会严重拖慢你的构建进程。
如果只是改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时(build time)简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
prerender-spa-plugin 是一个 webpack 插件用于在单页应用中预渲染静态 html 内容。因此,该插件限定了你的单页应用必须使用 webpack 构建,且它是框架无关的,无论你是使用 React 或 Vue 甚至不使用框架,都能用来进行预渲染。
那么 prerender-spa-plugin 是如何做到将运行时的 html 打包到文件中的呢?原理很简单,就是在 webpack 构建阶段的最后,在本地启动一个 phantomjs,访问配置了预渲染的路由,再将 phantomjs 中渲染的页面输出到 html 文件中,并建立路由对应的目录。
查看 prerender-spa-plugin 源码 prerender-spa-plugin/lib/phantom-page-render.js。
// 打开页面
page.open(url, function (status) {
...
// 没有设置捕获钩子时,在脚本执行完捕获
if (
!options.captureAfterDocumentEvent &&
!options.captureAfterElementExists &&
!options.captureAfterTime
) {
// 拼接 html
var html = page.evaluate(function () {
var doctype = new window.XMLSerializer().serializeToString(document.doctype)
var outerHTML = document.documentElement.outerHTML
return doctype + outerHTML
})
returnResult(html) // 捕获输出
}
...
})
npm install prerender-spa-plugin --save
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
publicPath:'/',
configureWebpack: config => {
config.entry = ['babel-polyfill', './src/main.js']
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
new PrerenderSPAPlugin({
// 生成文件的路径,也可以与webpakc打包的一致。
// 下面这句话非常重要!!!
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname, '../el-table-selection/dist'),
indexPath: path.join(__dirname, '../el-table-selection/dist', 'index.html'),
// 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
routes: ['/jsbarcode', '/home', '/tableselection', '/example'],
// renderer: new PrerenderSPAPlugin.PuppeteerRenderer({//这样写renderAfterTime生效了
// renderAfterTime: 5000
// }),
// renderAfterDocumentEvent: 'render-event'
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
inject: {
foo: 'bar'
},
headless: false,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
renderAfterDocumentEvent: 'render-event'
})
})
)
}
},
}
此项目的路径:
new Vue({
router,
render: h => h(App),
mounted () {
document.dispatchEvent(new Event('render-event'))
}
}).$mount('#app')
export default new Router({
mode: 'history',
routes
})
设置mode
预渲染的单页应用路由可以使用 history 模式或者 hash 模式。如果设置mode: ‘hash’,就要改一下预渲染的路由文件路径。
// 格式: /dist下对应的文件夹/#/页面路由
routes: ['/jsbarcode/#/home', '/home/#/home','/tableselection/#/home'],
运行npm run build,看一下生成的 dist 的目录里是不是有每个路由名称对应的文件夹。然后找个 目录里 的 index.html 用IDE打开,看文件内容里是否有该文件应该有的内容。有的话,就设置成功了。
本项目运行结果:
可以看到已经打包成功了,接下来就是运行打包后的项目看看效果。
http-server是一个简单的node服务器,可以用它来运行打包后的项目。
npm i -g http-server
http-server
页面加载过程:打包后的html中的静态内容其实只起到一个占位图的效果,之后解析到javascript的时候再触发vue的渲染,生成可交互的页面并覆盖之前的静态页面。这种实现可以解决首屏显示时间过长的问题。
Vue 预渲染实现方案