Open Rhan2020 opened 3 years ago
在开发中经常会遇到需要将配置数据转换成代码的情况,如果只有几个配置的话还好, ( ̄▽ ̄)~* 我们直接 ctrl + c、ctrl + v 操作就好了。(ಥ_ಥ) 然而,产品大佬通常只会甩一个几百行数据的谷歌表格给到前端,让前端自行录入奖励配置、图片配置等映射关系。 o(´^`)o作为一枚有追求(能动脑就不动手)的切图仔,肯定不能一行行录入或者复制到记事本改数据格式的,费时耗力不说还容易出错。若针对每个谷歌表格都写一个读取脚本,显然也是不可取的(每次都要改代码也不行),所以这时候就需要一个高效的开发工具可以满足:
( ̄▽ ̄)~*
ctrl + c
ctrl + v
(ಥ_ಥ)
o(´^`)o
Json
Array
综上,打算撕一个浏览器插件工具来提效我们的研发,让开发者把时间花在更有意义的事情上。
作为还没入门过浏览器插件开发的萌新,于是就去扫盲了下 chrome 插件开发的基础知识:
chrome-plugin-demo ├── background.js // 后台执行脚本 ├── images │ └── 128.png // 插件图标 ├── manifest.json // 清单文件,用于配置插件相关信息,声明脚本事件、用途,声明资源信息等 ├── popup.html // 点击插件图标后的弹窗页面 └── popup.js // 弹窗页面执行的 js
background
content_scripts
popup
manifest.json
加载已解压的插件
Jcanno/vue-chrome-extension
browser_action
webpack hot reload
chrome.tabs.query
chrome.tabs.reload
首先,我们先在 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,且在页面加载完成后进行执行:
https://docs.google.com/
"content_scripts": [ { "matches": [ "https://docs.google.com/*" // 映射到谷歌文档的域名才会加载js ], "css": [ "css/content.css" // css 路径 ], "js": [ "js/content.js" // 注入的 js 文件路径 ], "run_at": "document_end" // js 文件加载的时机 } ],
由于弹窗页面其实只提供了一个入口,用来触发我们注入到页面的代码,所以这里只写了一个按钮来控制页面侧边栏的显示隐藏:
<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 脚本进行通信:
chrome.tabs.sendMessage
methods: { handleClick() { chrome.tabs.query( { active: true, currentWindow: true }, tabs => { let message = { info: "open-panel" }; chrome.tabs.sendMessage(tabs[0].id, message, () => { }); } ); } },
写完之后长这样:
首先我们写 UI 界面,让它position:fixed;固定在谷歌表格的最右边、最顶层,并给个关闭按钮: 然后在这个侧边栏渲染到 dom 的时候使用 chrome.runtime.onMessage 添加事件监听,用来接受 popup 弹窗的指令:
position:fixed;
chrome.runtime.onMessage
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 插件来进行数据格式化处理:
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 脚本来进行打包:
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吗?
php
背景
在开发中经常会遇到需要将配置数据转换成代码的情况,如果只有几个配置的话还好,
( ̄▽ ̄)~*
我们直接ctrl + c
、ctrl + v
操作就好了。(ಥ_ಥ)
然而,产品大佬通常只会甩一个几百行数据的谷歌表格给到前端,让前端自行录入奖励配置、图片配置等映射关系。o(´^`)o
作为一枚有追求(能动脑就不动手)的切图仔,肯定不能一行行录入或者复制到记事本改数据格式的,费时耗力不说还容易出错。若针对每个谷歌表格都写一个读取脚本,显然也是不可取的(每次都要改代码也不行),所以这时候就需要一个高效的开发工具可以满足:ctrl + c
,就得到我们需要的数据格式Json
或Array
。综上,打算撕一个浏览器插件工具来提效我们的研发,让开发者把时间花在更有意义的事情上。
准备工作
作为还没入门过浏览器插件开发的萌新,于是就去扫盲了下 chrome 插件开发的基础知识:
什么是 chrome 插件?
chrome 插件能做什么?
chrome 插件由什么组成?
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
配置下插件相关的信息:background
是运行在插件后台的脚本,整个浏览器插件的生命周期都会存在,且提供了丰富的 chrome API 供调用,这里配置一下我们的background
脚本文件(虽然没用到):然后我们再配置下
content_scripts
,content_scripts
是页面执行脚本,属于页面的一部分,只是浏览器在打开页面的时候自动帮你执行而已,跟页面共用一个 dom,使得我们可以操作页面元素,在第三方网页执行我们自己的 js 代码。我们还可以自定义 js 加载的条件,这里我们配成谷歌表格的域名https://docs.google.com/
,说明只有匹配到这个域名的时候才会执行我们的 js,且在页面加载完成后进行执行:2、功能开发
开发 popup 弹窗
由于弹窗页面其实只提供了一个入口,用来触发我们注入到页面的代码,所以这里只写了一个按钮来控制页面侧边栏的显示隐藏:
再声明下点击回调,使用
chrome.tabs.sendMessage
来与当前选中页面的content_scripts
脚本进行通信:写完之后长这样:
开发
content_scripts
首先我们写 UI 界面,让它
position:fixed;
固定在谷歌表格的最右边、最顶层,并给个关闭按钮: 然后在这个侧边栏渲染到 dom 的时候使用chrome.runtime.onMessage
添加事件监听,用来接受popup
弹窗的指令:监听整个谷歌文档页面的 copy 事件:
待触发
ctrl+c
回调后我们就可以从用户的剪切板拿到选中的单元格元素,并给到封装好的sheetToCode
插件来进行数据格式化处理:封装
sheetToCode
插件来应对多种情况的数据格式处理:我们从用户剪切板拿到表格数据后,可以通过遍历每个单元格生成带详细信息的数组列表:
生成普通 json 格式的数据:
因为在生成的数据格式中,还有键值对的映射形式,所以我们还需要处理合并单元格的情况:
适配一下生成 php 代码格式的数据:
最后来看一下我们最终生成的数据格式: php 代码格式:
打包
我们可以使用
crx
来进行插件的打包,首次使用浏览器打包的话会生成 .pem 密钥,这个对我们之后发布到插件商城、更新插件版本都要用到,所以需要妥善备份。这里我们可以直接运行crx
脚本来进行打包:本来打算发布到谷歌商城的,但发现需要绑定信用卡支付,再缴纳个 $5 来进行开发者注册。觉得太麻烦就算了,反正解压后的代码也同样可以加载并使用,此工具也更新了使用文档到公司内部 wiki 供大伙使用。
总结
到此我们的浏览器插件算是开发完成了,本人也是从上午还是 0 基础开始的浏览器插件开发,通过一顿 google 的学习、参考多方入门文章、官方文档,到下午插件撸出来可以正常使用才正式完工。若文中有理解/描述不当处欢迎及时指正,共同交流学习。 同时也印证我们身为前端开发者的学习能力、资源检索能力、总结输出能力也需要在实践中不断培养和锻炼,这些都会在今后的职业生涯中不断累积个人的影响力,提升自己的核心竞争力。