bigo-frontend / blog

👨🏻‍💻👩🏻‍💻 bigo前端技术博客
https://juejin.cn/user/4450420286057022/posts
MIT License
129 stars 9 forks source link

浏览器插件开发之-sheetToCode #78

Open Rhan2020 opened 3 years ago

Rhan2020 commented 3 years ago

背景

在开发中经常会遇到需要将配置数据转换成代码的情况,如果只有几个配置的话还好, ( ̄▽ ̄)~* 我们直接 ctrl + cctrl + v 操作就好了。(ಥ_ಥ) 然而,产品大佬通常只会甩一个几百行数据的谷歌表格给到前端,让前端自行录入奖励配置、图片配置等映射关系。 four.png o(´^`)o作为一枚有追求(能动脑就不动手)的切图仔,肯定不能一行行录入或者复制到记事本改数据格式的,费时耗力不说还容易出错。若针对每个谷歌表格都写一个读取脚本,显然也是不可取的(每次都要改代码也不行),所以这时候就需要一个高效的开发工具可以满足:

综上,打算撕一个浏览器插件工具来提效我们的研发,让开发者把时间花在更有意义的事情上。

准备工作

作为还没入门过浏览器插件开发的萌新,于是就去扫盲了下 chrome 插件开发的基础知识:

什么是 chrome 插件?
chrome 插件能做什么?
chrome 插件由什么组成?
chrome-plugin-demo
├── background.js  // 后台执行脚本
├── images
│   └── 128.png    // 插件图标
├── manifest.json  // 清单文件,用于配置插件相关信息,声明脚本事件、用途,声明资源信息等
├── popup.html     // 点击插件图标后的弹窗页面
└── popup.js       // 弹窗页面执行的 js

着手开发

1、基础配置

首先,我们先在 manifest.json 配置下插件相关的信息:

"manifest_version": 2, // 清单版本
"name": "chrome-sheetToCode", // 应用名称
"description": "transform sheet to code", // 应用描述,会显示在插件管理页面中
"version": "1.0.0", // 插件版本号
"browser_action": { // 浏览器工具栏配置
  "default_title": "chrome-sheetToCode", // 弹窗标题
  "default_icon": "assets/logo.png", // 插件图标
  "default_popup": "popup.html"  // 点击插件图标后,显示的UI弹窗页面
},
"icons": { // 不同尺寸下的图标
  "16": "assets/logo.png",
  "48": "assets/logo.png",
  "128": "assets/logo.png"
}

background 是运行在插件后台的脚本,整个浏览器插件的生命周期都会存在,且提供了丰富的 chrome API 供调用,这里配置一下我们的background脚本文件(虽然没用到):

// 后台执行脚本文件配置
"background": {
  "scripts": ["js/background.js"]
},

然后我们再配置下content_scriptscontent_scripts是页面执行脚本,属于页面的一部分,只是浏览器在打开页面的时候自动帮你执行而已,跟页面共用一个 dom,使得我们可以操作页面元素,在第三方网页执行我们自己的 js 代码。我们还可以自定义 js 加载的条件,这里我们配成谷歌表格的域名 https://docs.google.com/,说明只有匹配到这个域名的时候才会执行我们的 js,且在页面加载完成后进行执行:

"content_scripts": [
  {
    "matches": [
      "https://docs.google.com/*" // 映射到谷歌文档的域名才会加载js
    ],
    "css": [
      "css/content.css"           // css 路径
    ],
    "js": [
      "js/content.js"             // 注入的 js 文件路径
    ],
    "run_at": "document_end"      // js 文件加载的时机
  }
],

2、功能开发

开发 popup 弹窗

由于弹窗页面其实只提供了一个入口,用来触发我们注入到页面的代码,所以这里只写了一个按钮来控制页面侧边栏的显示隐藏:

<template>
  <div>
    <button id="open-btn" @click="handleClick">open panel</button>
  </div>
</template>
#open-btn {
  color: black;
  border-radius: 20px;
  height: 40px;
  width: 100px;
  padding: 10px 20px;
  margin: 20px auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

再声明下点击回调,使用 chrome.tabs.sendMessage 来与当前选中页面的 content_scripts 脚本进行通信:

  methods: {
    handleClick() {
      chrome.tabs.query(
        {
          active: true,
          currentWindow: true
        },
        tabs => {
          let message = {
            info: "open-panel"
          };
          chrome.tabs.sendMessage(tabs[0].id, message, () => {
          });
        }
      );
    }
  },

写完之后长这样: popup.png

开发 content_scripts

首先我们写 UI 界面,让它position:fixed;固定在谷歌表格的最右边、最顶层,并给个关闭按钮: first.png 然后在这个侧边栏渲染到 dom 的时候使用 chrome.runtime.onMessage 添加事件监听,用来接受 popup 弹窗的指令:

mounted() {
  chrome.runtime.onMessage.addListener(request => {
    if (request.info === "open-panel") {
      // 显示侧边栏面板
      this.showPanel = true;
      // 调用页面的 focus 来关闭 popup
      window.focus();
    }
  });
}

监听整个谷歌文档页面的 copy 事件:

created() {
  document.addEventListener("copy", this.copyEvent);
}

待触发ctrl+c回调后我们就可以从用户的剪切板拿到选中的单元格元素,并给到封装好的 sheetToCode 插件来进行数据格式化处理:

copyEvent(event) {
  var clipboardData = event.clipboardData || window.clipboardData;
  if (!clipboardData) {
    return;
  }
  var text = clipboardData.getData("text/html");
  if (!text) {
    return;
  }
  const result = sheetToCode(text);
  // 将处理后的返回结果更新到当前实例
  this.data = result;
}

封装 sheetToCode 插件来应对多种情况的数据格式处理:

export default function htmlTransform(text) {
  const arr = htmlToArr(text);
  if (arr.length === 0) {
    alert(
      "操作可能失败!如果文档表格有背景色,请将删除背景色或者将该文档**剔除格式**拷贝到新文档"
    );
  }
  const dbkeyJson_row = arrToJson_doublekey(arr, "row");
  const dbkeyJson_col = arrToJson_doublekey(arr, "col");
  const json_row = arrToJson(arr, "row");
  const json_col = arrToJson(arr, "col");
  const dbkeyPhpRow = dbkeyJsonToPhpCode(dbkeyJson_row);
  const dbkeyPhpCol = dbkeyJsonToPhpCode(dbkeyJson_col);
  const jsonPhpRow = jsonToPhpCode(json_row);
  const jsonPhpCol = jsonToPhpCode(json_col);
  return {
    dbkeyJson_row,
    dbkeyJson_col,
    json_row,
    json_col,
    dbkeyPhpRow,
    dbkeyPhpCol,
    jsonPhpRow,
    jsonPhpCol,
    xmlObj,
  };
}

我们从用户剪切板拿到表格数据后,可以通过遍历每个单元格生成带详细信息的数组列表:

function htmlToArr(text) {
  let dom = document.createElement(`div`);
  dom.innerHTML = text;
  dom = dom.querySelector("table tbody");
  if (!dom) {
    return [];
  }
  // raw arr
  const cellArr = Array.prototype.map.call(dom.children || [], (it) => {
    return Array.prototype.map.call(it.children || [], (cell) => {
      return {
        row: cell.getAttribute("rowspan") - 0 || 1,
        col: cell.getAttribute("colspan") - 0 || 1,
        val: cell.innerText,
      };
    });
  });

  // map arr
  for (let i = 0; i < cellArr.length; i++) {
    const row = cellArr[i];
    for (let j = 0; j < row.length; j++) {
      const cell = row[j];
      const id = cell.id || `${i}-${j}`;
      if (cell.col > 1) {
        row.splice(j + 1, 0, {
          ...cell,
          id,
          col: cell.col - 1,
        });
      }
      if (cell.row > 1) {
        cellArr[i + 1].splice(j, 0, {
          ...cell,
          id,
          row: cell.row - 1,
        });
      }
      row[j] = {
        id,
        val: cell.val,
      };
    }
  }
  return cellArr;
}

生成普通 json 格式的数据:

// 单键json
function arrToJson(arr, major = "col") {
  if (arr.length < 2 || arr[0].length < 2) {
    return {};
  }
  if (major === "col") {
    return arr.reduce((res, cur) => {
      res[cur[0].val] = cur.slice(1).map((it) => it.val);
      return res;
    }, {});
  }
  if (major === "row") {
    const body = arr.slice(1);
    return arr[0].reduce((res, cur, idx) => {
      res[cur.val] = body.map((it) => it[idx].val);
      return res;
    }, {});
  }
}

因为在生成的数据格式中,还有键值对的映射形式,所以我们还需要处理合并单元格的情况:

// 双键json
// row0,col0不应该有重复键 todo
function arrToJson_doublekey(arr, major = "col") {
  if (arr.length < 2 || arr[0].length < 2) {
    return {};
  }
  if (major === "row") {
    const body = arr.slice(1);
    const obj = arr[0].slice(1).reduce((res, cur, idx) => {
      const colsObj = body.reduce((cols, it) => {
        cols[it[0].val] = it[idx + 1].val;
        return cols;
      }, {});
      res[cur.val] = colsObj;
      return res;
    }, {});
    return obj;
  } else {
    const subKey = arr[0].slice(1);
    const body = arr.slice(1);
    const obj = body.reduce((res, cur) => {
      const rowObj = cur.slice(1).reduce((rows, it, idx) => {
        rows[subKey[idx].val] = it.val;
        return rows;
      }, {});
      res[cur[0].val] = rowObj;
      return res;
    }, {});
    return obj;
  }
}

适配一下生成 php 代码格式的数据:

// 双键json转php
function dbkeyJsonToPhpCode(json) {
  const keys = Object.keys(json);
  const subKeys = Object.keys(json[keys[0]]);
  return keys
    .map((it) => {
      const item = json[it];
      const val = subKeys
        .map(
          (subKey) =>
            `\t"${(subKey || "").replace(/"/g, '\\"')}"=>"${(
              item[subKey] || ""
            ).replace(/"/g, '\\"')}"`
        )
        .join(",\n");
      return `"${(it || "").replace(/"/g, '\\"')}" => [\n${val}\n]`;
    })
    .join(",\n");
}

// 单键json转php
function jsonToPhpCode(json) {
  const keys = Object.keys(json);
  const rows = keys
    .map((it) => {
      const item = json[it];
      return `\t"${(it || "").replace(/"/g, '\\"')}" => [${item
        .map((it) => `"${(it || "").replace(/"/g, '\\"')}"`)
        .join(", ")}]`;
    })
    .join(",\n");
  return `[\n${rows}\n]`;
}

最后来看一下我们最终生成的数据格式: second.png php 代码格式: third.png

打包

我们可以使用crx来进行插件的打包,首次使用浏览器打包的话会生成 .pem 密钥,这个对我们之后发布到插件商城、更新插件版本都要用到,所以需要妥善备份。这里我们可以直接运行 crx 脚本来进行打包:

npm run build:crx
const fs = require("fs");
const path = require("path");
const manifest = require(path.resolve(__dirname, "../chrome/manifest.json"));
const ChromeExtension = require("crx");
const crxName = `${manifest.name}-v${manifest.version}.crx`;
const crx = new ChromeExtension({
  privateKey: fs.readFileSync(path.resolve(__dirname, "../../dist.pem")),
});

crx
  .load(path.resolve(__dirname, "../../dist"))
  .then((crx) => crx.pack())
  .then((crxBuffer) => {
    fs.writeFile(crxName, crxBuffer, (err) =>
      err
        ? console.error(err)
        : console.log(`>>>>>>>  ${crxName}  <<<<<<< 已打包完成`)
    );
  })
  .catch((err) => {
    console.error(err);
  });

本来打算发布到谷歌商城的,但发现需要绑定信用卡支付,再缴纳个 $5 来进行开发者注册。觉得太麻烦就算了,反正解压后的代码也同样可以加载并使用,此工具也更新了使用文档到公司内部 wiki 供大伙使用。

总结

到此我们的浏览器插件算是开发完成了,本人也是从上午还是 0 基础开始的浏览器插件开发,通过一顿 google 的学习、参考多方入门文章、官方文档,到下午插件撸出来可以正常使用才正式完工。若文中有理解/描述不当处欢迎及时指正,共同交流学习。 同时也印证我们身为前端开发者的学习能力、资源检索能力、总结输出能力也需要在实践中不断培养和锻炼,这些都会在今后的职业生涯中不断累积个人的影响力,提升自己的核心竞争力。

fayeah commented 2 years ago

请问选用转换为php格式的原因是项目中用的是php吗?