政采云技术团队.png

伟豪.png

> 这是第 161 篇不掺水的原创,想获取更多原创好文,请搜索公众号【政采云前端团队】关注我们吧~

前言

在我们日常开发中一定会遇到"所见即所得"的需求,如导出查询表格中的内容为Excel表格——《前端导出Excel,让后端刮目相看》open in new window、通过后台网页配置实现配置预览页与实际页面展示的统一——《从零开发一款可视化大屏制作平台》open in new window

今天我们也来实现一个"所见即所得"的需求:将用户所见网页提取为图片。

2C0714D1-BBEB-4810-8C64-F66963489F9D.png

方案1:最短步骤实现结果

第一个想到的方案就是通过浏览器自带的网页另存为图片去实现。

68630441-5C95-4594-AFB1-CDE4C6F1B348.png

但这种方法显然是不可行的。第一需要提示用户操作进行繁琐的操作,第二无法达到局部提取为图片的效果。

方案2:达成初步可行方案

通过调研发现,可以使用 html2canvasopen in new window 将网页先转换为 canvas数据。再将其转换为图片的方法,最终实现我们想要的功能。

20221011134318.jpg

引入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))
        });
    },

如此我们便实现了初步的功能。

1.gif

当然,我们也可以设置一个预览图片来预览我们将要导出的图片。

HTML

<img :src="previewPic" alt="预览图片">

JS

this.previewPic = URL.createObjectURL(that.convertBase64ToBlob(imgData));

展示效果

A9A4712D-B30C-452E-A610-9F5ACF492162.png

将方案进行拓展并升级

需求止步于此,但秉承着"将事情做的更好"的我们岂能止步于此。

实现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)
    }

展示效果

2.gif

如此我们便实现了导出 HTML 为 Word。

实现HTML导出为PDF

目前市面上 HTML 导出 PDF 的实现方式有多种,如jsPDFopen in new windowiTextopen in new windowwkhtmltopdfopen in new window等。在不同情况下我们应该使用不同的解决方案:

方案优点缺点分页图片表格链接中文特殊字符
jsPDF1、整个过程在客户端执行(不需要服务器参与),调用简单1、生成的pdf为图片形式,且内容失真支持支持支持不支持支持支持
iText1、功能基本可以实现,比较灵活 2、生成pdf质量较高1、对html标签严格,少一个结束标签就会报错;2、后端实现复杂,服务器需要安装字体;3、图片渲染比较复杂支持支持支持支持支持支持
wkhtmltopdf1、调用方式简单;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');
      })
    }

展示效果

3.gif

如此我们便实现了导出 HTML 为 PDF。

将功能封装为组件

实现一次HTML导出图片需要写的代码太多,很多参数也需要按需定制。是否能够将其封装成组件呢?

我们可以通过Vue的插槽open in new window将我们导出的内容进行插入

完整组件

<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

参数类型说明
idString唯一ID,盒子内容将作为导出内容
filenameString导出的文件名称(不带文件类型后缀)
outTypeString导出文件类型
isFileBoolean是否导出为文件,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.为什么外网图片展示不出?

7BFD5507-2620-4EEB-A4AF-144F63441CD8.png

A: 设置 html2canvas 方法中 useCORS 为 true 即可。

AC53AB9C-170A-4CF8-8F1B-2C7724EDC3F6.png

最后要说的

项目地址: https://github.com/FireSmallPanda/vuexDemo.gitopen in new window

HTML导出为图片组件地址:HTML导出为图片组件open in new window

根据需求目前只封装了HTML导出为图片,相信大家可以依葫芦画瓢将导出Word和PDF也一并封装为组件。

参考文章

《前端实现将页面保存成图片功能》open in new window

《base64字串转Blob文件流,Blob文件流再下载到本地》open in new window

《render-html-to-pdf》open in new window

《HTML页面导出为PDF》open in new window

外网图片来源——《风光摄影精选》open in new window

推荐阅读

探索组件在线预览和调试open in new window

规范升级 NPM 包open in new window

你想知道的前后端协作规范都在这了open in new window

带你了解 Tree Shakingopen in new window

厉害!这篇正则表达式竟写的如此详尽open in new window

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/open in new window (小报官网首页有微信交流群)

  • 商品选择 sku 插件

开源地址 https://github.com/zcy-inc/skuPathFinder-back/open in new window

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com