前端实现录屏、跨页面录屏、截屏功能
录屏实现方案:rrweb.js
官方文档:https://github.com/rrweb-io/rrweb
截屏实现方案:html2canvas.js
官方文档:http://html2canvas.hertzen.com/
rrweb录屏实现的原理是记录dom的变化,播放的时候重新执行dom操作,所以不支持生成mp4,只可以在web端重放
html2canvas截图实现的原理是生成一个canvas画布,画布的内容是插件帮我们实现好的,我们需要做的是将canvas画布保存成base64图片,保存到本地或者上传
服务端例子:express
需要引入的文件:
rrweb:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css"/> <script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>
html2canvas(从官网下载到本地的文件)
<script src="/javascripts/html2canvas.js"></script>
下面用一个例子演示一下跨页面录屏和截屏的效果
前端完整代码如下:
页面1:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" /> </head> <body> <!-- 不支持 IE11 以下的浏览器 --> <script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script> <script src="/javascripts/html2canvas.js"></script> <button class="btn">开始录制</button> <button class="btn2">结束并播放</button> <button class="btn3" id="capture">截图</button> <button class="btn4">下一步</button> <button class="btn5">清空服务器数据</button> <br> <br> <div>this is page1</div> <div> <input type="text" placeholder="请输入信息"> </div> <script> window.onload = function(){ let el = document.getElementsByClassName("btn")[0]; let el2 = document.getElementsByClassName("btn2")[0]; let el3 = document.getElementById("capture") let el4 = document.getElementsByClassName("btn4")[0]; let el5 = document.getElementsByClassName("btn5")[0]; let events = [] let r el.onclick = function(){ alert("开始录制") r = rrweb.record({ emit(event) { // 将 event 存入 events 数组中 events.push(event); console.log(events) }, }); } el2.onclick = function(){ alert("播放") r() new rrwebPlayer({ target: document.body, // 可以自定义 DOM 元素 data: { events, }, }); } el3.onclick = function(){ html2canvas(document.querySelector("body")).then(canvas => { // document.body.appendChild(canvas) let pic = canvas.toDataURL("image/png"); downloadFileByBase64(pic,'金融测试下载') // window.location.href = pic; }); } el4.onclick = function(){ // console.log(JSON.stringify(events)) // console.log(events.length) uploadEvents(1,events) return false } el5.onclick = function(){ // console.log(JSON.stringify(events)) // console.log(events.length) ajax({ url:'/clearEvents', type:'POST', dataType:'json', data:{}, // data:{name:1}, success:function(response,xml){ //请求成功后执行的代码 alert(response.msg) // console.log(events) window.open("/page2.html") }, error:function(status){ alert(response.msg) } }); return false } } // 分批上传 function uploadEvents(m,events){ console.log(JSON.stringify(events).length) ajax({ url:'/saveEvents', type:'POST', dataType:'json', data:{events:JSON.stringify(events),page:1}, // data:{name:1}, success:function(response,xml){ //请求成功后执行的代码 response = JSON.parse(response); // console.log(events) window.open("/page2.html") }, error:function(status){ alert(response.msg) } }); } // base64生成图片方法 function dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); } function downloadFile(url,name='What\'s the fuvk'){ var a = document.createElement("a") a.setAttribute("href",url) a.setAttribute("download",name) a.setAttribute("target","_blank") let clickEvent = document.createEvent("MouseEvents"); clickEvent.initEvent("click", true, true); a.dispatchEvent(clickEvent); } function downloadFileByBase64(base64,name){ var myBlob = dataURLtoBlob(base64) var myUrl = URL.createObjectURL(myBlob) downloadFile(myUrl,name) } // 原生ajax方法 function ajax(options){ var options=options||{}; options.type=(options.type||'GET').toUpperCase(); options.dataType=options.dataType||'json'; var params=formatParams(options.data); //创建-第一步 var xhr; //非IE6 if(window.XMLHttpRequest){ xhr=new XMLHttpRequest(); }else{ //ie6及其以下版本浏览器 xhr=ActiveXObject('Microsoft.XMLHTTP'); } //接收-第三步 xhr.onreadystatechange=function(){ if(xhr.readyState==4){ var status=xhr.status; if(status>=200&&status<300){ options.success&&options.success(xhr.responseText,xhr.responseXML); }else{ options.error&&options.error(status); } } } //连接和发送-第二步 if(options.type=='GET'){ xhr.open('GET',options.url+'?'+params,true); xhr.send(null); }else if(options.type=='POST'){ xhr.open('POST',options.url,true); //设置表单提交时的内容类型 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(params); } } //格式化参数 function formatParams(data){ var arr=[]; for(var name in data){ arr.push(encodeURIComponent(name)+'='+encodeURIComponent(data[name])); } // arr.push(('v='+Math.random()).replace('.','')); return arr.join('&'); } // save 函数用于将 events 发送至后端存入,并重置 events 数组 // function save() { // const body = JSON.stringify({ events }); // events = []; // fetch('http://YOUR_BACKEND_API', { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // }, // body, // }); // 每 10 秒调用一次 save 方法,避免请求过多 // setInterval(save, 10 * 1000); </script> </body> </html>
页面2:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" /> </head> <body> <!-- 不支持 IE11 以下的浏览器 --> <script src="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script> <script src="/javascripts/html2canvas.js"></script> <button class="btn">开始录制2</button> <button class="btn2">结束并播放1和2</button> <button class="btn3" id="capture">截图2</button> <br> <br> <div>this is page2</div> <div> <input type="text" placeholder="请输入信息"> </div> <script> window.onload = function(){ let el = document.getElementsByClassName("btn")[0]; let el2 = document.getElementsByClassName("btn2")[0]; let el3 = document.getElementById("capture") console.log(el) let events = [] let r el.onclick = function(){ alert("开始录制") r = rrweb.record({ emit(event) { // 将 event 存入 events 数组中 events.push(event); console.log(events) }, }); } el2.onclick = function(){ alert("播放") if(r){ r() } ajax({ url:'/saveEvents', type:'POST', dataType:'json', data:{events:JSON.stringify(events),finished:false}, // data:{name:1}, success:function(response,xml){ //请求成功后执行的代码 // console.log(events) ajax({ url:'/getEvents', type:'POST', dataType:'json', data:{}, success:function(response,xml){ //请求成功后执行的代码 response = JSON.parse(response); events = response; console.log(response) new rrwebPlayer({ target: document.body, // 可以自定义 DOM 元素 data: { events, }, }); }, error:function(status){ alert(response.msg) } }); }, error:function(status){ alert(response.msg) } }); } el3.onclick = function(){ html2canvas(document.querySelector("body")).then(canvas => { // document.body.appendChild(canvas) let pic = canvas.toDataURL("image/png"); downloadFileByBase64(pic,'金融测试下载') // window.location.href = pic; }); } } function dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); } function downloadFile(url,name='What\'s the fuvk'){ var a = document.createElement("a") a.setAttribute("href",url) a.setAttribute("download",name) a.setAttribute("target","_blank") let clickEvent = document.createEvent("MouseEvents"); clickEvent.initEvent("click", true, true); a.dispatchEvent(clickEvent); } function downloadFileByBase64(base64,name){ var myBlob = dataURLtoBlob(base64) var myUrl = URL.createObjectURL(myBlob) downloadFile(myUrl,name) } // 原生ajax方法 function ajax(options){ var options=options||{}; options.type=(options.type||'GET').toUpperCase(); options.dataType=options.dataType||'json'; var params=formatParams(options.data); //创建-第一步 var xhr; //非IE6 if(window.XMLHttpRequest){ xhr=new XMLHttpRequest(); }else{ //ie6及其以下版本浏览器 xhr=ActiveXObject('Microsoft.XMLHTTP'); } //接收-第三步 xhr.onreadystatechange=function(){ if(xhr.readyState==4){ var status=xhr.status; if(status>=200&&status<300){ options.success&&options.success(xhr.responseText,xhr.responseXML); }else{ options.error&&options.error(status); } } } //连接和发送-第二步 if(options.type=='GET'){ xhr.open('GET',options.url+'?'+params,true); xhr.send(null); }else if(options.type=='POST'){ xhr.open('POST',options.url,true); //设置表单提交时的内容类型 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(params); } } //格式化参数 function formatParams(data){ var arr=[]; for(var name in data){ arr.push(encodeURIComponent(name)+'='+encodeURIComponent(data[name])); } // arr.push(('v='+Math.random()).replace('.','')); return arr.join('&'); } // save 函数用于将 events 发送至后端存入,并重置 events 数组 // function save() { // const body = JSON.stringify({ events }); // events = []; // fetch('http://YOUR_BACKEND_API', { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // }, // body, // }); // 每 10 秒调用一次 save 方法,避免请求过多 // setInterval(save, 10 * 1000); </script> </body> </html>
服务端代码:
/* * @Author: your name * @Date: 2020-07-18 19:09:47 * @LastEditTime: 2020-07-19 12:24:16 * @LastEditors: Please set LastEditors * @Description: In User Settings Edit * @FilePath: /record-2/app.js */ var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var fs = require('fs'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var bodyParser = require('body-parser'); var app = express(); app.use(bodyParser.urlencoded({ extended:true })); let events = []; // view engine setup app.use(express.static(path.join(__dirname, 'public'))); // 服务端页面 app.get('/', function(req, res, next) { // res.writeHead(200,{'Content-Type':'text/html'}) fs.readFile('./html/index.html','utf-8',function(err,data){ if(err){ throw err ; } res.send(data); }); }); app.get('/page2.html', function(req, res, next) { // res.writeHead(200,{'Content-Type':'text/html'}) fs.readFile('./html/page2.html','utf-8',function(err,data){ if(err){ throw err ; } res.send(data); }); }); app.post('/clearEvents',function(req, res, next) { // res.writeHead(200,{'Content-Type':'text/html'}) console.log(req.body) events = []; res.send({msg:"操作成功"}) // if(req.query.events.length>0){ // res.send({state:1,msg:"上传成功"}) // }else{ // res.send({state:0,msg:"参数错误,未获取到录屏信息"}) // } }); // 存 app.post('/saveEvents',function(req, res, next) { // res.writeHead(200,{'Content-Type':'text/html'}) console.log(req.body) res.send({state:1,msg:"上传成功"}) // res.send(req.body.events) if(req.body.page==1){ events = [] } events = events.concat(JSON.parse(req.body.events)) fs.writeFile("./data.txt",JSON.stringify(req.body.events),'utf-8',function(){ }) // if(req.query.events.length>0){ // res.send({state:1,msg:"上传成功"}) // }else{ // res.send({state:0,msg:"参数错误,未获取到录屏信息"}) // } }); // 取 app.post('/getEvents',function(req, res, next) { res.send(events); // fs.readFile('./data.txt','utf-8',function(err,data){ // if(err){ // throw err ; // } // console.log(JSON.parse(data)) // res.send(data); // }); // if(req.query.events.length>0){ // res.send({state:1,msg:"上传成功"}) // }else{ // res.send({state:0,msg:"参数错误,未获取到录屏信息"}) // } }); module.exports = app;
上面的例子如果只想在前端看效果的话,只运行第一个页面的前端代码也能查看。