这是第 161 篇不掺水的原创,想获取更多原创好文,请搜索公众号【政采云前端团队】关注我们吧~
前言
在我们日常开发中一定会遇到"所见即所得"
的需求,如导出查询表格中的内容为Excel表格——《前端导出Excel,让后端刮目相看》、通过后台网页配置实现配置预览页与实际页面展示的统一——《从零开发一款可视化大屏制作平台》。
今天我们也来实现一个"所见即所得"的需求:将用户所见网页提取为图片。
方案1:最短步骤实现结果
第一个想到的方案就是通过浏览器自带的网页另存为图片去实现。
但这种方法显然是不可行的。第一需要提示用户操作进行繁琐的操作,第二无法达到局部提取为图片的效果。
方案2:达成初步可行方案
通过调研发现,可以使用 html2canvas 将网页先转换为 canvas
数据。再将其转换为图片的方法,最终实现我们想要的功能。
引入html2canvas
cnpm install --save html2canvas
HTML
<div class="box">
<!-- 将可保存为图片的内容 通过一个 标签 框起来 -->
<div id="screenshot-box">
<!-- 内容可以随意 -->
<p>名称:<a-input v-model="formState.name" /></p>
<p>年龄:<a-input v-model="formState.age" /></p>
<p>
班级:
<a-radio-group v-model="formState.class">
<a-radio value="1">班级1</a-radio>
<a-radio value="2">班级2</a-radio>
</a-radio-group>
</p>
</div>
<a-button @click="onSaveCanvas">保存为图片</a-button>
</div>
JS
// 点击保存为Canvas
onSaveCanvas(){
// 这里的类名要与点击事件里的一样
const canvas = document.querySelector('#screenshot-box');
let that = this;
html2canvas(canvas,{scale:2,logging:false,useCORS:true}).then(function(canvas) {
const type = 'png';
let imgData = canvas.toDataURL(type);
// 图片格式处理
let _fixType = function(type) {
type = type.toLowerCase().replace(/jpg/i, 'jpeg');
let r = type.match(/png|jpeg|bmp|gif/)[0];
return 'image/' + r;
};
imgData = imgData.replace(_fixType(type),'image/octet-stream');
let filename = "htmlImg" + '.' + type;
// 保存为文件
// 以bolb文件下载
that.downFileToLocal(filename,that.convertBase64ToBlob(imgData))
});
},
如此我们便实现了初步的功能。
当然,我们也可以设置一个预览图片来预览我们将要导出的图片。
HTML
<img :src="previewPic" alt="预览图片">
JS
this.previewPic = URL.createObjectURL(that.convertBase64ToBlob(imgData));
展示效果
将方案进行拓展并升级
需求止步于此,但秉承着"将事情做的更好"的我们岂能止步于此。
实现HTML导出为Word
我们需要通过 html-docx 来实现导出为Word(导出Word目前只支持原生HTML + CSS)。
引入html-docx
cnpm install --save html-docx-js
HTML
<div id="export-word">
<table border >
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
<tr>
<td>贾维斯</td>
<td>2</td>
</tr>
</table>
</div>
<a-button @click="onWordExport">导出为word</a-button>
JS
onWordExport(){
var contentHtml = document.getElementById("export-word").innerHTML;
const cssHTML = `table {
width: 200px;
border: 1px solid #ccc;
color:red;
}`
var content = `<!DOCTYPE html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
${cssHTML}
</style>
</head>
<body>
${contentHtml}
</body>
</html>`
var converted = htmlDocx.asBlob(content,{orientation:"landscape"});
this.downFileToLocal('word文件名.docx',converted)
}
展示效果
如此我们便实现了导出 HTML 为 Word。
实现HTML导出为PDF
目前市面上 HTML 导出 PDF 的实现方式有多种,如jsPDF、iText、wkhtmltopdf等。在不同情况下我们应该使用不同的解决方案:
方案 | 优点 | 缺点 | 分页 | 图片 | 表格 | 链接 | 中文 | 特殊字符 |
---|---|---|---|---|---|---|---|---|
jsPDF | 1、整个过程在客户端执行(不需要服务器参与),调用简单 | 1、生成的pdf为图片形式,且内容失真 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
iText | 1、功能基本可以实现,比较灵活 2、生成pdf质量较高 | 1、对html标签严格,少一个结束标签就会报错;2、后端实现复杂,服务器需要安装字体;3、图片渲染比较复杂 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
wkhtmltopdf | 1、调用方式简单;2、生成pdf质量较高 | 1、服务器需要安装wkhtmltopdf环境;2、根据网址生成pdf,对于有权限控制的页面需要在拦截器进行处理 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
今天我们使用在客户端执行(不需要服务器参与)的方式——jsPDF。
导入jsPDF
npm install --save jspdf
HTML
<a-button @click="onPDFExport">导出为PDF</a-button>
JS
// 导出为PDF
onPDFExport(){
const canvas = document.querySelector('#screenshot-box');
html2canvas(canvas).then(function(canvas) {
let contentWidth = canvas.width;
let contentHeight = canvas.height;
//一页pdf显示html页面生成的canvas高度;
let pageHeight = contentWidth / 592.28 * 841.89;
//未生成pdf的html页面高度
let leftHeight = contentHeight;
//页面偏移
let position = 0;
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
let imgWidth = 595.28;
let imgHeight = 592.28/contentWidth * contentHeight;
let pageData = canvas.toDataURL('image/jpeg', 1.0);
let pdf = new jsPDF('', 'pt', 'a4');
//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
//当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight );
} else {
while(leftHeight > 0) {
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= 841.89;
//避免添加空白页
if(leftHeight > 0) {
pdf.addPage();
}
}
}
pdf.save('content.pdf');
})
}
展示效果
如此我们便实现了导出 HTML 为 PDF。
将功能封装为组件
实现一次HTML导出图片需要写的代码太多,很多参数也需要按需定制。是否能够将其封装成组件呢?
我们可以通过Vue的插槽将我们导出的内容进行插入
完整组件
<template>
<div class="box">
<!-- 将可保存为图片的内容 通过一个 标签 框起来 -->
<div :id="id">
<!-- 需要导出的内容 -->
<slot></slot>
</div>
</div>
</template>
<script>
//引入html2canvas
import html2canvas from 'html2canvas';
export default {
name: "Html2Image",
props: {
id: { // id
type: String,
default: 'export-box',
},
filename: { // 导出文件名称
type: String,
default: 'htmlPic',
},
outType:{ // 导出类型
type: String,
default: 'png',
validator(value) {
return ['png', 'jpg', 'bmp','jpeg','gif'].includes(value)
}
},
isFile: { // 是否为导出文件
type: Boolean,
default: false,
},
},
methods: {
//点击保存为Canvas
onSaveCanvas(){
// 这里的类名要与点击事件里的一样
const canvas = document.querySelector(`#${this.id}`);
let that = this;
html2canvas(canvas,{scale:2,logging:false,useCORS:true}).then(function(canvas) {
const type = that.outType;
let imgData = canvas.toDataURL(type);
// 图片格式处理
let _fixType = function(type) {
type = type.toLowerCase().replace(/jpg/i, 'jpeg');
let r = type.match(/png|jpeg|bmp|gif/)[0];
return 'image/' + r;
};
imgData = imgData.replace(_fixType(type),'image/octet-stream');
if(!that.isFile){
const retn = URL.createObjectURL(that.convertBase64ToBlob(imgData))
that.$emit('onExport',retn)
return retn
}
// 以bolb文件下载
that.downFileToLocal(that.filename + '.' + type,that.convertBase64ToBlob(imgData))
});
},
// base64转化为Blob对象
convertBase64ToBlob(imageEditorBase64) {
let base64Arr = imageEditorBase64.split(",");
let imgtype = "";
let base64String = "";
if (base64Arr.length > 1) {
//如果是图片base64,去掉头信息
base64String = base64Arr[1];
imgtype = base64Arr[0].substring(
base64Arr[0].indexOf(":") + 1,
base64Arr[0].indexOf(";")
);
}
// 将base64解码
let bytes = atob(base64String);
//let bytes = base64;
let bytesCode = new ArrayBuffer(bytes.length);
// 转换为类型化数组
let byteArray = new Uint8Array(bytesCode);
// 将base64转换为ascii码
for (let i = 0; i < bytes.length; i++) {
byteArray[i] = bytes.charCodeAt(i);
}
// 生成Blob对象(文件对象)
return new Blob([bytesCode], { type: imgtype });
},
// 下载Blob流文件
downFileToLocal(fileName, blob) {
// 创建用于下载文件的a标签
const d = document.createElement("a");
// 设置下载内容
d.href = URL.createObjectURL(blob);
// 设置下载文件的名字
d.download = fileName;
// 界面上隐藏该按钮
d.style.display = "none";
// 放到页面上
document.body.appendChild(d);
// 点击下载文件
d.click();
// 从页面移除掉
document.body.removeChild(d);
// 释放 URL.createObjectURL() 创建的 URL 对象
window.URL.revokeObjectURL(d.href);
},
},
};
</script>
Attributes
参数 | 类型 | 说明 |
---|---|---|
id | String | 唯一ID,盒子内容将作为导出内容 |
filename | String | 导出的文件名称(不带文件类型后缀) |
outType | String | 导出文件类型 |
isFile | Boolean | 是否导出为文件,true将下载文件,false返回bolb路径 |
Methods
方法 | 名称 | 说明 |
---|---|---|
onSaveCanvas | 执行导出或保存方法 | 若isFile为true则保存文件,若isFile为false则返回bolb路径 |
Events
方法 | 名称 | 说明 |
---|---|---|
onExport | 导出方法 | 当触发导出/下载时会触发该方法输出 Bolb路径 类型String |
使用组件
HTML
<Html2Image ref="html2Image" @onExport="exportPic" >
<div class="export-content" style="border: 1px solid #F0F;width:200px">
<div>1</div>
<div>2</div>
<div>3</div>
</div>
</Html2Image>
<a-button @click="onExportImgByComponent">组件导出为图片</a-button>
JS
// 引入组件
import Html2Image from '@/components/Html2Image/Html2Image.vue'
// 使用组件
components: {
Html2Image
},
// methods
onExportImgByComponent(){
this.previewPicComponent =this.$refs.html2Image.onSaveCanvas()
},
exportPic(baseUrl){
// 赋值导出图片的blob路径
this.previewPicComponent = baseUrl;
},
Q&A
Q.为什么外网图片展示不出?
A: 设置 html2canvas 方法中 useCORS 为 true 即可。
最后要说的
项目地址: github.com/FireSmallPa…
HTML导出为图片组件地址:HTML导出为图片组件
根据需求目前只封装了HTML导出为图片,相信大家可以依葫芦画瓢将导出Word和PDF也一并封装为组件。
参考文章
《base64字串转Blob文件流,Blob文件流再下载到本地》
推荐阅读
开源作品
- 政采云前端小报
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
- 商品选择 sku 插件
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com