blog
blog copied to clipboard
浏览器插件开发之-sheetToCode
背景
在开发中经常会遇到需要将配置数据转换成代码的情况,如果只有几个配置的话还好, ( ̄▽ ̄)~*
我们直接 ctrl + c
、ctrl + v
操作就好了。(ಥ_ಥ)
然而,产品大佬通常只会甩一个几百行数据的谷歌表格给到前端,让前端自行录入奖励配置、图片配置等映射关系。
o(´^`)o
作为一枚有追求(能动脑就不动手)的切图仔,肯定不能一行行录入或者复制到记事本改数据格式的,费时耗力不说还容易出错。若针对每个谷歌表格都写一个读取脚本,显然也是不可取的(每次都要改代码也不行),所以这时候就需要一个高效的开发工具可以满足:
- 选中谷歌表格的数据内容,按下神秘按键
ctrl + c
,就得到我们需要的数据格式Json
或Array
。 - 能跨浏览器页面使用。
综上,打算撕一个浏览器插件工具来提效我们的研发,让开发者把时间花在更有意义的事情上。
准备工作
作为还没入门过浏览器插件开发的萌新,于是就去扫盲了下 chrome 插件开发的基础知识:
什么是 chrome 插件?
- chrome 插件其实跟普通的 web 应用一样,是由 html + css + js 经过 zip 打包组成的,在这过程中 chrome 提供了丰富的浏览器级别的 api 供开发者调用,提供一个跨页面脚本执行环境,来满足开发者的定制化需求。chrome 插件可以出现在地址栏、工具栏中。
chrome 插件能做什么?
- 书签控制
- 下载控制
- 窗口控制
- 标签控制
- 网络请求控制
- 各类事件监听
- 自定义原生菜单
- 完善的通信机制
- 等等
chrome 插件由什么组成?
- 如下是一个简单的浏览器插件目录及其对应的说明:
chrome-plugin-demo
├── background.js // 后台执行脚本
├── images
│ └── 128.png // 插件图标
├── manifest.json // 清单文件,用于配置插件相关信息,声明脚本事件、用途,声明资源信息等
├── popup.html // 点击插件图标后的弹窗页面
└── popup.js // 弹窗页面执行的 js
-
background
、content_scripts
、popup
之间的关系:
chrome 插件开发流程是什么样的?
- 普通的开发流程是:
- 按照官方开发文档创建对应文件目录
- 在
manifest.json
中声明插件信息、各资源、脚本用途等 - 然后再开发对应的脚本功能
- 编码保存后使用浏览器扩展中
加载已解压的插件
功能来进行预览和查看。
- 工程化开发流程:
- 相较于原始的开发流程,本次参考了
Jcanno/vue-chrome-extension
搭建好的开发模板。好处是我们可以通过开发 vue 页面的形式来写 UI 跟交互,模板会给我们打包成对应的manifest.json
、background
、content_scripts
、browser_action
等。 - 而且还提供了
webpack hot reload
的方案,原理是通过chrome.tabs.query
去查找当前的活动标签页,再使用chrome.tabs.reload
进行刷新页面,不用每次保存完再去按 F5 刷新页面跟插件了。
- 相较于原始的开发流程,本次参考了
着手开发
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_scripts
, content_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, () => {
});
}
);
}
},
写完之后长这样:
开发 content_scripts
首先我们写 UI 界面,让它position:fixed;
固定在谷歌表格的最右边、最顶层,并给个关闭按钮:
然后在这个侧边栏渲染到 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]`;
}
最后来看一下我们最终生成的数据格式:
php 代码格式:
打包
我们可以使用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 的学习、参考多方入门文章、官方文档,到下午插件撸出来可以正常使用才正式完工。若文中有理解/描述不当处欢迎及时指正,共同交流学习。 同时也印证我们身为前端开发者的学习能力、资源检索能力、总结输出能力也需要在实践中不断培养和锻炼,这些都会在今后的职业生涯中不断累积个人的影响力,提升自己的核心竞争力。
请问选用转换为php
格式的原因是项目中用的是php
吗?