實現Google帶截圖功能的web反饋外掛

mai-kuraki發表於2018-05-18

幾乎所有的APP應用包括Web應用都需要一個意見反饋,這樣才能瞭解使用者對產品的意見和建議,以便於不斷提升完善自己的產品。目前的反饋元件一般有兩種,一種是開啟一個反饋頁面填寫表單,另一種則是通過彈窗來完成,相比較而言第二種更加方便,而且更加容易元件化。
國內比較典型的有像知乎,百度這樣型別的反饋元件

實現Google帶截圖功能的web反饋外掛
國外則有谷歌的
實現Google帶截圖功能的web反饋外掛
由於本人比較喜歡谷歌的material design,而且谷歌的反饋元件功能也比較齊全,仿照谷歌的元件寫一個自己的通用元件。 下面是PC和手機上的效果圖:
pc
mobile
demo演示
專案github地址

1.開發

首先根據谷歌的反饋外掛分析需要實現哪些功能,這個元件在很多谷歌頁面中都會出現,谷歌搜尋頁出現的位置是底部。 根據實際操作能知道這個元件至少要包含這些功能:

  1. 擷取當前螢幕;
  2. 能編輯頁面的高亮或者遮擋區域;
  3. 可以適應pc於手機;

知道功能後就能一步步開始實現它了。谷歌是通過iframe來實現這個元件的,比較複雜 谷歌的feedback工具載入檔案:load.js和截圖檔案screenshot.min.js有興趣的同學可以看一下。
這裡使用自己的思路,使用現成模組來簡單實現這個功能。

1.擷取螢幕

需要獲取當前螢幕內容,第一時間反應是使用canvas了,先把dom元素畫到canvas如何再生成圖片,幸好有一個牛掰的模組叫做html2canvas,它可以將指定的DOM元素繪製到canvas上。

安裝html2canvas

npm i html2canvas
複製程式碼

html2canvas 1.0.0版本中可以使用作者提供的html2canvas-proxy模組來實現跨越資源代理。具體配置可以參考文件。順便提一下,之前0.5.0版本中,html2canvas提供的代理方便不太好使,解決的方法是自己啟動一個處理服務,在頁面中遇到跨域圖片資源,使用服務將圖片轉成base64格式然後回填到圖片src屬性上,這樣來實現跨越圖片截圖。

如果頁面上不僅僅有圖片還有視訊該怎麼辦呢,怎麼擷取視訊圖片呢?html2canvas是不支援擷取video標籤內容的,但是html2canvas截圖時可以渲染元素的背景圖片。那麼如果可以獲取視訊當前播放的幀,把這一幀作為video標籤的background,html2canvas就能讀取到了。
如何讀取video的幀,這個canvas的drawImage()方法就能做到(注意不能獲取跨域的視訊資源)。

let video = videoItem[0];
if(!video.style.backgroundImage) {
    let w = $(video).width();
    let h = $(video).height();
    $(video).after('<canvas width="'+ w +'" height="'+ h +'"></canvas>');
    let canvas = $(video).next('canvas').css({display: 'none'});
    let ctx = canvas.get(0).getContext('2d');
    ctx.drawImage(video, 0, 0, w, h);
    try {
        video.style.backgroundImage = "url("+ canvas.get(0).toDataURL('image/png') +")";
    }catch (e) {
        console.log(e)
    }finally {
        canvas.remove();
    }
}
複製程式碼

做完這些準備就可以開始時截圖了

html2canvas(document.body, {
    proxy: this.props.proxy || '',
    width: window.innerWidth,
    height: window.innerHeight,
    x: document.documentElement.scrollLeft || document.body.scrollLeft,
    y: document.documentElement.scrollTop || document.body.scrollTop,
}).then((canvas) => {
    let src = canvas.toDataURL('image/png');
    ...
}).catch((e) => {
    
});
複製程式碼

注意:html2canvas v1.0.0使用promise,v0.5.0採用的是回撥函式。

2.高亮區域

核心部分截圖搞定了,接下來就是高亮區域了。高亮分為兩部分:

  1. 滑鼠放在頁面上識別當前滑鼠是在哪個DOM元素上然後將這個DOM元素高亮,給使用者提供一個快捷選區方式。
  2. 使用者自己用滑鼠選區一個高亮區域。

第二點很容易實現,只有滑鼠按下時記錄點選點位置,然後隨著滑鼠移動計算與初始點位置的差值就能得到一個區域了,那麼怎麼識別滑鼠是放在哪個DOM元素上呢?有一個Web API可以輕鬆實現這個功能那就是elementsFromPoint

elementsFromPoint() 方法可以獲取到當前視口內指定座標處,由裡到外排列的所有元素。
複製程式碼

使用方法很簡單,只要給x,y座標就行了。

var elements = document.elementsFromPoint(x, y);
複製程式碼

這個方法返回的是一個包含當前滑鼠所在位置的DOM元素陣列。元素在陣列中的位置與元素的z軸位置和元素包含關係有關,z軸越大位置越靠前,子元素比父元素靠前。

<div>
    <p>
        <img/>
    </p>
</div>
複製程式碼

如果是這個結構那麼當滑鼠在img標籤上document.elementsFromPoint(x, y)返回的值是這樣的

[img, p, div]
複製程式碼

得到元素後那麼後續的操作就簡單了,在一個半透明的黑色區域上摳出透明部分,顯然css是實現不了的,那麼就上canvas吧。
首先要畫一個半透明的遮罩:

let canvas = this.refs.canvas;
if (!this.ctx) {
    this.ctx = canvas.getContext('2d');
}
let docWidth = document.body.clientWidth,
    docHeight = document.body.clientHeight;
if(docHeight < window.innerHeight) {
    docHeight = window.innerHeight;
}
canvas.width = docWidth;
canvas.height = docHeight;
canvas.style.width = docWidth;
canvas.style.height = docHeight;
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
this.ctx.fillRect(0, 0, docWidth, docHeight);
複製程式碼

準備完遮罩就可以開始扣圖了。通過elementsFromPoint的到元素後使用getBoundingClientRect()方法得到元素的位置資訊。 getBoundingClientRect()用於獲得頁面中某個元素的左,上,右和下分別相對瀏覽器視窗的位置以及元素寬高。

實現Google帶截圖功能的web反饋外掛
寬高,位置資訊都有了就可以開始繪製了:

this.ctx.lineWidth = '5';
this.ctx.strokeStyle = '#FEEA4E';
this.ctx.rect(x, y, width, height);
this.ctx.stroke();
this.ctx.clearRect(x, y, width, height);
複製程式碼

實現Google帶截圖功能的web反饋外掛
同理如果不是高亮而是遮擋只要把清除區域換成繪製一個半透明黑色區域就可以了。 要注意每次畫新區域時要清除上次繪製的內容所以每次都得初始化一次canvas內容

3.適配手機

由於手機頁面中的反饋介面與PC差距太大所以不能採用同一套模板,通過一個state來區分該渲染那種型別。在元件willMount的時候判斷裝置型別

let device = 'pc';
let ua = navigator.userAgent;
let ipad = ua.match(/(iPad).*OS\s([\d_]+)/),
    isIphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/),
    isAndroid = ua.match(/(Android)\s+([\d.]+)/),
    isMobile = isIphone || isAndroid;
if (isMobile) {
    device = 'mobile';
    this.setState({
        device: device,
    });
}
複製程式碼

得到裝置後根據裝置進行渲染

{
    this.state.device == 'pc'?
    <PCComponent/>
    :
    <MobileComponnet>
}
複製程式碼

需要注意的是有些手機瀏覽器在開啟輸入法後會導致頁面視窗變化所以需要監聽視窗變化去做適配調整

2.使用

1.安裝:

使用npm

npm install react-googlefeedback --save-dev
複製程式碼

2.使用

react中:

import React from 'react';
import ReactDOM from 'react-dom';
import Feedback from 'react-googlefeedback';
import 'react-googlefeedback/dist/style.css';

const license = `如出於法律原因需要請求更改內容,請前往
                <a href="" >法律幫助</a>
                頁面。系統可能已將部分
                <a href="">帳號和系統資訊</a>
                傳送給Google。我們將根據自己的
                <a href="">隱私權政策</a>和<a href="">服務條款</a>
                使用您提供的資訊幫助解決技術問題和改進我們的服務。`;

class Page extends React.Component {
    constructor() {
        super();
        this.state = {
            open: false,
        }
    }
    open() {
        this.setState({
            open: true,
        })
    }
    cancel() {
        this.setState({
            open: false,
        })
    }
    send(data) {
        console.log(data)
    }
    render() {
        return (
            <div>
                <button onClick={this.open.bind(this)}>feedback</button>
                {
                    this.state.open?
                        <Feedback
                            theme="#3986FF"
                            cancel={this.cancel.bind(this)}
                            send={this.send.bind(this)}
                            license={license}
                            proxy="http://127.0.0.1:5000"
                            title="傳送反饋"
                            placeholder="請說明您的問題或分享您的想法"
                            requiredTip="必須新增說明"
                            editTip="點選編輯高亮或隱藏資訊"
                            loadingTip="正在載入螢幕截圖..."
                            checkboxLabel="包含截圖"
                            cancelLabel="取消"
                            confirmLabel="傳送"
                        />:null
                }
            </div>
        )
    }
}
ReactDOM.render(<Page/>, document.getElementById('main'));
複製程式碼

在頁面中引入js檔案使用:

<body>
    <div id="feedback"></div>
    <button id="btn"></button>
<body>
<script src="react-googlefeedback/dist/googlefeedback.js"></script>
<script>
    new Feedback({
        container: document.getElementById('feedback'),
        trigger: document.getElementById('btn'),
        theme: '#3986FF',
        proxy: 'http://127.0.0.1:5000',
        title: '傳送反饋',
        placeholder: '請說明您的問題或分享您的想法',
        requiredTip: '必須新增說明',
        editTip: '點選編輯高亮或隱藏資訊',
        loadingTip: '正在載入螢幕截圖...',
        checkboxLabel: '包含截圖',
        cancelLabel: '取消',
        confirmLabel: '傳送',
        license: `如出於法律原因需要請求更改內容,請前往
                <a href="" >法律幫助</a>
                頁面。系統可能已將部分
                <a href="">帳號和系統資訊</a>
                傳送給Google。我們將根據自己的
                <a href="">隱私權政策</a>和<a href="">服務條款</a>
                使用您提供的資訊幫助解決技術問題和改進我們的服務。`,
        send: function (data) {
            console.log(data)
        }
    });
</script>
複製程式碼

3.引數說明

react 元件:

引數 功能 型別 是否必填
theme 設定元件主題顏色 string ✗ 預設值 #3986FF
cancel 取消按鈕處理函式 function
send 傳送按鈕處理函式,會傳回收集的資料 function
license 協議內容 html字串 ✗ 預設值為谷歌的隱私條款協議
proxy 代理地址,如果頁面中存在跨域資源可以設定這個值 string ✗ 預設空值
title 彈出標題文字 string
placeholder 文字輸入提示 string
requiredTip 文字必填提示 string
editTip 提示編輯文子 string
loadingTip 載入圖片提示 string
checkboxLabel 勾選框文字 string
cancelLabel 取消按鈕文字 string
confirmLabel 確認按鈕文字 string

頁面中直接引用option引數:

引數 功能 型別 是否必填
container 元件容器元素 element
trigger 用於觸發元件開啟的元素 element
theme 設定元件主題顏色 string ✗ 預設值 #3986FF
license 協議內容 html字串 ✗ 預設值為谷歌的隱私條款協議
proxy 代理地址,如果頁面中存在跨域資源可以設定這個值 string ✗ 預設空值
title 彈出標題文字 string
placeholder 文字輸入提示 string
requiredTip 文字必填提示 string
editTip 提示編輯文子 string
loadingTip 載入圖片提示 string
checkboxLabel 勾選框文字 string
cancelLabel 取消按鈕文字 string
confirmLabel 確認按鈕文字 string
send 傳送按鈕處理函式,會傳回收集的資料 function

4.跨域代理

需要啟動一個服務用於代理。
首先安裝html2canvas-proxy

npm install html2canvas-proxy --save
複製程式碼

在node中使用代理

var proxy = require('html2canvas-proxy');
var express = require('express');
var app = express();
app.use('/', proxy());
複製程式碼

相關文章