几乎所有的APP应用包括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()用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置以及元素宽高。


宽高,位置信息都有了就可以开始绘制了:

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);


同理如果不是高亮而是遮挡只要把清除区域换成绘制一个半透明黑色区域就可以了。
要注意每次画新区域时要清除上次绘制的内容所以每次都得初始化一次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"
                        />: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',
        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 ✗ 默认空值

页面中直接引用:

参数 功能 类型 是否必填
container 组件容器元素 element
trigger 用于触发组件打开的元素 element
theme 设置组件主题颜色 string ✗ 默认值 #3986FF
license 协议内容 html字符串 ✗ 默认值为谷歌的隐私条款协议
proxy 代理地址,如果页面中存在跨域资源可以设置这个值 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());

maikuraki
9 声望0 粉丝