自制小工具(1):谷歌插件导出Excel表格

1,372 阅读6分钟

本系列是本人在OPPO工作一年半中,为了解决开发或者运营的困扰,自制的一些小工具合集,特地以文章的形式记录下来,一方面作为自己的知识储备,另一方面期望能够给遇到相同问题的同学一些启发。

运营苦恼

埋点数据平台中有上百条字段,但运营只想选取其中的十几条字段,然后做成 excel 报表。可令人头痛的是,这个系统又是其他团队用 PHP 开发的,埋点数据系统的后台不支持 过滤出指定字段,并且取数据的时候需要手动复制到本地的 Excel 表格,整理一个活动的数据有时候会消耗运营半天时间。

从问题中我们分析到:查找指定数据制作Excel表格 是运营耗时的重要原因,如何提升呢?

  • 过滤出指定数据表格,提高查找效率。
  • 支持以方便的形式将表格导出为 Excel,减少人工的复制粘贴操作。

知道了要做的方向,接下来就应该设计如何解决问题了。

编写脚本

我们的第一版方案很简单,写一段JS脚本,通过操纵dom的方式,模拟人工选择点击按钮,选中输入的指定字段。

let targetStr = 'angular vue react';

let ul = dropdownMenu;
let children = ul.children;
let targetArr = targetStr.split(" "); // 生成目标数组

for (let i = 0; i < children.length; i++) {
    let li = children[i].children[0];
    let label = li.innerHTML;
    let input = li.children[0];
    
    for (let j = 0; j < targetArr.length; j++) {
        if (label.indexOf(targetArr[j]) > -1) {
          input.click();
        }
    } 
}

这里只演示大致含义:生成需要选中的字段数组,遍历所有所有的单选框,遍历目标字段数组匹配完成之后,触发click事件,完成选中所有按钮的效果,下面是生成excel表格的逻辑。


function tableToExcel(fileName, excelContent) {
    var uri = "data:application/vnd.ms-excel;base64,";
    var fileName = fileName || "excelexport";
    var template =
      '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name="renderer" content="webkit"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body><table>{table}</table></body></html>';
    var ctx = { worksheet: "Worksheet", table: excelContent };
    var a = document.createElement("a");
    a.hreflang = "zh";
    a.type = "application/vnd.ms-excel";
    let blob = new Blob([format(template, ctx)]);
    a.href = URL.createObjectURL(blob);
    a.target = "_blank";
    a.download = fileName + ".xls";
    a.tableBorder = 1;

    document.body.appendChild(a);
    a.click();
	
    // 点击下载完成后,删除创建的a标签
    setTimeout(function () {
      document.body.removeChild(a);
    }, 200);
}

document.getElementById('targetTable').addEventListener("dblclick", function (e) {
  tableToExcel("表格", e.target.innerHTML);
});

这里的逻辑大致是:给指定的表格,绑定一个双击事件,双击下载 Excel 表格。其中不难发现,其实触发下载的逻辑很简单:

  • 拼接一段特定的字符串,生成一个 Blod 对象
  • 创建一个 a 标签,href 参数为一个 Blod 对象生成的 url 。

最后,我们把这两段代码粘贴到谷歌的 console 控制台中,回车就可以运行了,这样第一个版本就完成了。

咱们思维发散一下,能否实现一个函数,输入特定格式的数组,然后生成一个excel表格呢?其实无非就是重新组装一个 table 标签罢了。

优化体验

其实不难发现,作为一个小工具,给技术人员使用很方便,但对于运营等非技术人员,会产生一些问题。

  • 流程繁琐。需要打开控制台,然后然后跑代码。
  • 需要手动修改代码。过滤字符串的流程需要修改代码里面的 targetStr 字符串。
  • 重复操作。每次过滤,都需要跑一遍代码。
  • 学习成本较高。每个运营都要学习一遍在控制台内运行代码的方法

其实针对上述的操作流程,很简单,我们用脚本在页面内插入一个 input 输入框。通过这样的方式,运营只需要跑一次代码就行了。

但是,有没有方法避免运营同学跑代码,界面化操作呢?很简单,编写一个谷歌插件!

谷歌插件

谷歌插件可以给当前的网页插入一段JS脚本,同时也可以在顶部增加一个弹窗,这个弹窗可以用一个新的 html 实现。

  • 通过弹窗实现一个 input 让运营输入需要选中的字段。
  • 在网页内插入一段脚本,给所有的表格添加上双击下载事件,并且接收弹窗输入值完成过滤逻辑。

谷歌插件实现效果

因为涉及到部分业务代码,在此只提供部分逻辑,仅供大家参考,改编自项目 chrome-plugin-demo,关于怎样编写谷歌教学很全面。

// manifest.json
{
  "manifest_version": 2,
  "name": "埋点数据平台插件",
  "version": "1.0",
  "description": "此插件仅用于埋点数据平台",
  "author": "ting",
  "icons": {
    "48": "icon.png",
    "128": "icon.png"
  },
  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"   // 指定弹窗页面名称。
  },
  "content_scripts": [
    {
      "matches": ["*://baidu.com/*"],   // 限制只能在百度域名下运行此插件
      "js": ["content-script.js"]	// 设定插入
    }
  ]
  "permissions": ["tabs", "storage"],  // 申请权限:tabs和storage(用于弹窗和注入JS沟通)
}

上面主要是配置我们的弹窗和注入JS的文件,这个文件会告诉谷歌浏览器解析逻辑的路径。

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" />
	<title>popup</title>
	<style>
		p {
			margin: 0;
			padding: 10px 0;
		}
	</style>
</head>
<body style="width:500px;min-height:100px;">
	<textarea name="" id="textarea" cols="60" rows="10"></textarea>
	<p>*需要过滤的字段需要用<b>空格</b>分开,示例:pv-login pv-channel</p>
	<button id="filter-button">过滤</button>
	<script type="text/javascript" src="popup.js"></script>
</body>
</html>

弹窗主要有三个元素:一个taxtarea 输入框,一个确认按钮,还有一段提示文案。

// popup.js
// 将textarea中的文案传递给 content-script.js(注入窗口的JS文件)
function sendMessage(value) {
  chrome.tabs.query(
    {
      active: true,
      currentWindow: true,
    },
    (tabs) => {
      let message = {
        data: value,
      };
      chrome.tabs.sendMessage(tabs[0].id, message);
    }
  );
}

// 关闭弹窗时会销毁数据,所有使用 storage 缓存当前输入的内容。
chrome.storage.local.get(["ltExportExcelVal"], function (result) {
  document.getElementById("textarea").value = result.ltExportExcelVal || "";
});

// 点击过滤按钮
document.getElementById("filter-button").addEventListener("click", function () {
  let value = document.getElementById("textarea").value;
  
  sendMessage(value);
  
  // 缓存上一次确认输入的内容
  chrome.storage.local.set({ ltExportExcelVal: value }, function () {
    console.log("Value is set to " + value);
  });
});

在 popup.js 中主要做了两件事:将输入信息传递出去和缓存上次输入的内容。


// 因为 content-script.js 插入后,有可能table表格还没有完成加载。
// 所以需要等页面加载完毕,然后给每个表格添加上一个事件。
(function (win) {
    "use strict";
    var listeners = [];
    var doc = win.document;
    var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
    var observer;

    function check() {
        for (var i = 0; i < listeners.length; i++) {
          var listener = listeners[i];
          var elements = doc.querySelectorAll(listener.selector);
          for (var j = 0; j < elements.length; j++) {
            var element = elements[j];
            if (!element.ready) {
              element.ready = true;
              listener.fn.call(element, element);
            }
          }
        }
    }

    function ready(selector, fn) {
        listeners.push({
          selector: selector,
          fn: fn,
        });
        if (!observer) {
          observer = new MutationObserver(check);
          observer.observe(doc.documentElement, {
            childList: true,
            subtree: true,
          });
        }
        check();
    }
  	win.ready = ready;
})(this);


// 给所有table表格添加双击事件
function tableToExcel(fileName, excelContent) { //... }

// 给所有 table 添加一个双击事件。
ready("table", function (element) {
    element.addEventListener("dblclick", function () {
      tableToExcel("表格", element.innerHTML);
    });

    document.getElementById('targetTable').addEventListener
});

// 获取弹窗中传递过来的值,过滤出指定字段
chrome.runtime.onMessage.addListener((request) => {
    let targetStr = request.data;
    // ...模拟用户点击过滤出指定table字段。
});

通过这个谷歌插件,我获得了一大堆运营给我的零食。

一些思考和总结

其实这个小工具的开发原理和逻辑都和简单,但它能够帮助运营或者帮助其他同学减轻很多无效的工作量。其实作为前端开发,我们的技能不应该仅仅在用在业务上面,如果能帮助到周围的同事和家人解决问题,也能够实现自身的价值。

另外,第一次了解到原来 html 可以直接导出 Excel 文件,xlsx 文件原理竟然是 xml 格式文件,还可以直接解压,同理 html 也可以导出 html, txt 等格式的文件,利用这个原理我们可以实现更多有趣且实用的功能!