先讲下服务端渲染和预渲染的区别吧
服务端渲染(SSR)和预渲染(Prerendering)的区别
然后是预渲染的业务场景
新建一个vue的demo,按照官方的提示进行按照依赖,果不其然报错了
其实原因还是很简单,被墙了,根据npm的提示我在根目录新建一个 .npmrc 文件配置了变量 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
但是其实并没有解决问题,配置该变量只是跳过了下载无头浏览器的步骤让你可以顺利安装依赖而已,我们再来看下源码,先进入它上面报错的相关文件,node_modules\puppeteer\install.js 发现是调用了BrowserFetcher 对象的download方法
然后我们看下那个变量为什么可以跳过下载,果然简单粗暴啊
报错信息在64行,那我们接着追溯,找到了node_modules\puppeteer\lib\BrowserFetcher.js
在它的顶部我们发现了一些类似下载链接的东西,我们看到有个地址 storage.googleapis.com
ping 一下看看
丢包率还是挺高的,证明这个地址是真的不靠谱,无头浏览器的包在140m左右,这样的网络环境,肯定不适合下载大的包,原因找到了,怎么解决呢,我百度了下,找到一个镜像npm.taobao.org/mirrors/chr… 进去一看,这不就是我们想要的吗
可是百度了一圈,大家的解决方案都是先跳过无头浏览器的下载,然后手动下载解压到Puppeteer依赖包的根目录下,这样的方式在多人协作开发的情况下还是有些不方便的,尤其是项目组来新人的时候,怎么办? 没办法,自己动手丰衣足食,首先在package.json 的scripts里面添加一个运行项,随便起一个名字,我的是叫 "install-chromium": "node ./download-chromium.js",后面这个是用node执行脚本的意思,接下来新建 download-chromium.js 在根目录 然后开始写我们的下载脚本
download-chromium.js
const os = require('os');
const fs = require('fs');
const path = require('path');
const extract = require('extract-zip');
const util = require('util');
const URL = require('url');
const progressStream = require('progress-stream')
const fetch = require("node-fetch");
const ProgressBar = require('./progress-bar');
// 阻止证书认证
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
// 这里的下载url的地址实际位置进行动态拼接
const DEFAULT_DOWNLOAD_HOST = 'https://npm.taobao.org/mirrors';
const download_path = '/chromium-browser-snapshots'
const version = require('./node_modules/puppeteer/package.json').puppeteer.chromium_revision
const downloadURLs = {
linux: `${DEFAULT_DOWNLOAD_HOST}${download_path}/Linux_x64/${version}/chrome-linux.zip`,
mac: `${DEFAULT_DOWNLOAD_HOST}${download_path}/Mac/${version}/chrome-mac.zip`,
win32: `${DEFAULT_DOWNLOAD_HOST}${download_path}/Win/${version}/chrome-win32.zip`,
win64: `${DEFAULT_DOWNLOAD_HOST}${download_path}/Win_x64/${version}/chrome-win.zip`,
};
// 判断操作系统
const platform = os.platform();
let _platform = ''
if (platform === 'darwin')
_platform = 'mac';
else if (platform === 'linux')
_platform = 'linux';
else if (platform === 'win32')
_platform = os.arch() === 'x64' ? 'win64' : 'win32';
// 这里是一些控制台颜色的配置,不要也可,我只是为了好玩
var styles = {
'bold' :'\x1B[1m%s\x1B[22m',
'italic' :'\x1B[3m%s\x1B[23m',
'underline' :'\x1B[4m%s\x1B[24m',
'inverse' :'\x1B[7m%s\x1B[27m',
'strikethrough' :'\x1B[9m%s\x1B[29m',
'white' :'\x1B[37m%s\x1B[39m',
'grey' :'\x1B[90m%s\x1B[39m',
'black' :'\x1B[30m%s\x1B[39m',
'blue' :'\x1B[34m%s\x1B[39m',
'cyan' :'\x1B[36m%s\x1B[39m',
'green' :'\x1B[32m%s\x1B[39m',
'magenta' :'\x1B[35m%s\x1B[39m',
'red' :'\x1B[31m%s\x1B[39m',
'yellow' :'\x1B[33m%s\x1B[39m',
'whiteBG' :'\x1B[47m%s\x1B[49m',
'greyBG' :'\x1B[49;5;8m%s\x1B[49m',
'blackBG' :'\x1B[40m%s\x1B[49m',
'blueBG' :'\x1B[44m%s\x1B[49m',
'cyanBG' :'\x1B[46m%s\x1B[49m',
'greenBG' :'\x1B[42m%s\x1B[49m',
'magentaBG' :'\x1B[45m%s\x1B[49m',
'redBG' :'\x1B[41m%s\x1B[49m',
'yellowBG' :'\x1B[43m%s\x1B[49m'
};
const pb = new ProgressBar('下载进度', 0);
// 下载函数
function download(u, p) {
//下载 的文件 地址
let fileURL = u;
//下载保存的文件路径
let fileSavePath = path.join(__dirname, path.basename(fileURL));
//缓存文件路径
let tmpFileSavePath = fileSavePath + ".tmp";
const fileStream = fs.createWriteStream(tmpFileSavePath).on('error', function (e) {
console.error('error==>', e)
}).on('ready', function () {
console.log(styles.yellow,"开始下载:", fileURL);
}).on('finish', function () {
//下载完成后重命名文件
fs.renameSync(tmpFileSavePath, fileSavePath);
// node_modules\puppeteer\.local-chromium\win64-686378\chrome-win 在使用淘宝镜像下载成功过一次后知道了无头浏览器的位置,所以我这里进行拼接路径,将文件下载后解压到这个位置并在解压后将下载文件删除
const mkdirPath = path.join(__dirname, `./node_modules/puppeteer/.local-chromium/${_platform}-${version}`)
fs.mkdir(mkdirPath, { recursive: true }, (err) => {
if (err) throw err;
console.log(styles.yellow,"\n chromium 下载成功,开始安装...");
const zipPath = path.join(__dirname, fileName)
extractZip(zipPath, mkdirPath).then(res => {
console.log(styles.green,"chromium 解压成功!开始删除下载文件");
fs.unlinkSync(zipPath)
console.log(styles.green,'删除成功!')
})
});
});
return fetch(u, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
}).then(res => {
//获取请求头中的文件大小数据
let fsize = res.headers.get("content-length");
let str = progressStream({
length: fsize,
time: 100 /* ms */
});
// 下载进度
str.on('progress', function (progressData) {
pb.render({
completed: progressData.percentage,
total: 100,
spend: progressData.speed,
})
});
res.body.pipe(str).pipe(fileStream);
}).catch(err => console.error(err))
}
/**
* @param {string} zipPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function extractZip(zipPath, folderPath) {
return new Promise((fulfill, reject) => extract(zipPath, { dir: folderPath }, err => {
if (err)
reject(err);
else
fulfill();
}));
}
console.log(`当前是${_platform}系统`);
var url = downloadURLs[_platform];
const fileName = url.split("/").reverse()[0]
download(url, fileName)
然后我为了让下载能够看得到进度条,又写了个小玩意
progress-bar.js
// 这里用到一个很实用的 npm 模块,用以在同一行打印文本
var slog = require('single-line-log').stdout;
// 封装的 ProgressBar 工具
function ProgressBar(description, bar_length){
// 两个基本参数(属性)
this.description = description || 'Progress'; // 命令行开头的文字信息
this.length = bar_length || 25; // 进度条的长度(单位:字符),默认设为 25
// 刷新进度条图案、文字的方法
this.render = function (opts){
var percent = (opts.completed / opts.total).toFixed(4); // 计算进度(子任务的 完成数 除以 总数)
var cell_num = Math.floor(percent * this.length); // 计算需要多少个 █ 符号来拼凑图案
// 拼接黑色条
var cell = '';
for (var i=0;i<cell_num;i++) {
cell += '█';
}
// 拼接灰色条
var empty = '';
for (var i=0;i<this.length-cell_num;i++) {
empty += '░';
}
let spend = `${(opts.spend/1024).toFixed(0)} kb\s`
// 拼接最终文本
var cmdText = this.description + ': ' + (100*percent).toFixed(2) + '% ' + cell + empty + ' ' + spend;
// 在单行输出文本
slog(cmdText);
};
}
module.exports = ProgressBar;
跑下我们刚刚写的脚本看看
确认下有没有下载到指定的位置
依赖配置好了,接下来就简单了,在根目录新建vue.config.js,并配置如下
vue.config.js
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
configureWebpack: config => {
const TARGET = process.env.npm_lifecycle_event;
const plugins=[]
const prerenderSPAPlugin = new PrerenderSPAPlugin({
// 生成文件的路径,也可以与webpakc打包的一致。
// 下面这句话非常重要!!!
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname, 'dist'),
// 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
routes: ['/', '/product', '/about'],
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
inject: {
foo: 'bar'
},
headless: false,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
renderAfterDocumentEvent: 'render-event'
})
})
if (process.env.NODE_ENV === 'production') {
plugins.push(prerenderSPAPlugin)
};
return {
plugins,
};
}
}
在src\main.js中添加事件
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
mounted(){
// 这是新加的事件
document.dispatchEvent(new Event('render-event'))
}
}).$mount('#app')
因为我们配置的是只有production 模式下才能生效,那我们build下看看
可以看到build成功后,dist文件夹里面添加了两个目录,是我们刚刚配置好的路由,点击进去分别是一个html静态页面,因为是demo,这两个页面我并没有做,路由也没有配置,所以它是按照App.vue 也就是我们的根组件去渲染的,至此,大功告成。
- 一个webpack预渲染插件,www.npmjs.com/package/pre…↩
- Puppeteer 是 Google Chrome 出品的一个无头浏览器,提供高级 API,通过 DevTools Protocol 来控制 Chrome 或 Chromium。www.npmjs.com/package/pup…↩
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!