预渲染的原理:在webpack打包结束并生成文件后(after-emit hook),启动一个server模拟网站的运行,用puppeteer(google官方的headless chrome浏览器)访问指定的页面route,得到相应的html结构,并将结果输出到指定目录,过程类似于爬虫。
这里直接记录尝试过程中的坑:
webpack.prod.conf.js中配置插件:
1 2 | const PrerenderSPAPlugin = require('prerender-spa-plugin'); const Renderer = PrerenderSPAPlugin.PuppeteerRenderer; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | new PrerenderSPAPlugin({ // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。 // staticDir: path.join(config.build.assetsRoot), staticDir: path.join(__dirname, '../dist'), // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1,param必须实体化,动态的不行。 routes: ['/','/home','/introduction'], // 这个很重要,如果没有配置这段,也不会进行预编译 renderer: new Renderer({ inject: { foo: 'bar' }, renderAfterDocumentEvent: 'render-event', // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。 args: ['--no-sandbox', '--disable-setuid-sandbox'] }) }) |
上面的预渲染路由配置要注意/ 和/home是同一个路径,就会出现预渲染两次,因此应该去掉一个。
1 2 3 4 5 6 7 | renderer: new Renderer({ inject: { foo: 'bar' }, renderAfterDocumentEvent: 'render-event', // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。 args: ['--no-sandbox', '--disable-setuid-sandbox'] }) |
这部分经测试可以去掉。
prerender-spa-plugin插件安装的时候已经安装puppeteer,所以不用重复安装。
npm run build完成以后,根目录会出现
配置了路由的页面会出现文件夹,文件夹内是index.html。
配合metainfo插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | declare module "vue/types/options" { interface ComponentOptions<V extends Vue> { metaInfo?: any; } } @Component({ metaInfo() { return { title: "这是公司的首页", meta: [ { name: "keywords", content: "关键字1" }, { name: "description", content: "这是一段网页的描述1111" } ] }; }, } |
注意申明组件ComponentOptions属性,因为原生没有metainfo对象。
预渲染事件触发配置:
main.ts中
1 2 3 4 5 6 7 8 | new Vue({ router, store, render: h => h(App), mounted () { document.dispatchEvent(new Event('render-event')) } }).$mount('#app') |
在实现过程中遇到了很多坑
0.路由模式必须是history,nginx服务器上需要配置
1 2 3 4 5 6 | location / { add_header Cache-Control no-store; root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } |
也就是不带#号的路由。
1.根目录index.html中如果设置了SEO,标题描述内容三元素,在预渲染后不会替换根页面的属性,也就是会出现两次这样的属性。
如果页面中存在2个meta信息,第一个有效,第二是无效的。但是如果删除掉index.html中设置的meta,就会出现预渲染以外的页面,没有title和meta信息,也不合理。
解决方案: 增加一个模板入口文件index2.html,那么会有两个模板文件index.html和index2.html,预编译indexPath指定使用index2.html,在index2.html中不设置meta和title。正常渲染使用index.html,在index.html中正常设置meta和title。
这样预渲染的页面就会有1个meta,预渲染以外的页面也可以正常有meta。
同样这个也需要nginx设置,主页面请求index2.html,其他页面请求index.html.
2.生成的静态页面,只有a标签,才能跳进预渲染页面,router-link或者$router.push跳转会被js代理渲染
当然也有解决方案,在nginx中配置:
1 2 3 4 5 6 7 8 | location / { try_files $uri $uri/ @router; #需要指向下面的@router否则会出现vue的路由在nginx中刷新出现404 } #对应上面的@router,主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件 #因此需要rewrite到index.html中,然后交给路由在处理请求资源 location @router { rewrite ^.*$ /index.html last; } |
3.puppeteer在docker环境部署环境缺少,需要另外安装环境库。
centos:
1 2 3 4 5 6 7 | RUN apt-get update && \ apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \ rm -rf /var/lib/apt/lists/* |
unbantu:
1 | yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc |
我基于第一个安装上传了具备puppeteer运行环境的基础node镜像
1 | FROM dxlani/sino_front8_prerender |
ok,线上部署是可以完成了,我遇到了新的问题。
4.第三方插件无法在index.html中加载出来,CDS资源无法加载
原因:
以百度商桥的插件为例,加载时间超时或者非常的久,久到页面无法刷出,预渲染的页面,会等到所有js加载完以后才能刷新出来
而SPA模型页面不会受影响。
百度商桥这个客服系统竟然是个轮询,一个轮询要20s,这样我们预渲染首页就得等20S的js加载时间,目前解决方案首页不预渲染- -。
而百度商桥插件作为一个js方法单独引入首页组件内,在生命周期中创建和销毁。
在uitl文件夹下创建BaiduBridge.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | export default { hm: function () { if (T) { clearTimeout(T); } //因为使用了vue + prerender + html-webpack-plugin 打包 //这里打包使用了预渲染静态 html ,渲染时会把百度的 js 也镶嵌到 html 代码里面 //所以要把静态 html 里的 baidu.com 有关商桥和离线宝的的js都先删除 //然后设置延迟加载百度统计,他会自动把商桥代码也加载进html let script = document.getElementsByTagName('SCRIPT') for (let x in script) { if (script[x].src && script[x].src.indexOf('baidu.com') !== -1) { console.log(script[x].src); script[x].remove(); } } let T = setTimeout(function () { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?7b594a84f0596cb19ae60b882567a2f2"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); }, 1000); }, } |
在home组件中引入百度商桥
1 2 3 4 5 6 7 8 9 10 | import hm from "@/common/util/BaiduBridge"; mounted() { // 百度商桥 hm.hm(); } destroyed() { // 百度商桥 var child = document.getElementById("newBridge"); child.parentNode.removeChild(child); } |
5.打包完后会报 vue项目报错webpackJsonp is not defined
解决方案就是修改webpack打包js的顺序
找到build→webpack.prod.conf.js→找到HtmlWebpackPlugin插件,添加如下配置即可
1 | chunks: ['manifest', 'vendor', 'app'] |
6.忘记说了,调转路由必须是80端口,除非nginx里面配置调转- -。不配置的情况下,假如页面是通过8080端口打开的,那调转到预渲染页面会默认变成80端口的,也可以说成是端口丢失了吧。
ps:有时候看起来简单易实现的东西,坑却更多。