使用Chrome插件来补充一些写作网站没有Markdown的坑
场景
技术者写文章,基本少不了Markdown了,但是很多自媒体平台(大而全那种),往往都是坑爹的富文本编辑器(还很多是魔改UEditor,人家官方三年没更新了喂)。
类似这种:
这是很麻烦的一件事,尤其是那些没有代码块的编辑器,没错,说的就是你,头条!这种坑爹玩意儿,就得让程序员手动粘贴代码过来,然后遇到排版不友好的,呵呵,对,说的还是你,头条!
于是吧,我就想着,奶奶个熊,没有我就自己写个插件来搞吧。
事实上,我自己的网站上有自己依赖marked做的一套编辑器,还挺好用,但是由于图床问题,还是得每次把富文本粘贴到头条后,删除图片,重新上传,没办法,穷是本命。
咳咳,最后做出来了,但是发现,没卵用……喵的,Markdown有代码块,人家富文本还是不支持啊……总之写出来分享下方案与思路。
框架
manifest.json 配置
{ "name": "今日头条协作辅助工具", "version": "1.0.0", "description": "今日头条网页版协作缺失工具的补充。", "permissions": [ "activeTab", "declarativeContent" ], "content_scripts": [ { "matches": [ "https://mp.toutiao.com/*" ], "js": [ "js/util.js", "libs/turndown.js", "js/content/index.js" ], "css": [], "run_at": "document_start" } ], "browser_action": { "default_popup": "popup.html", "default_title": "这里可以补充头条网页版本的不足哦。", "default_icon": { "16": "img/logo_16.png", "32": "img/logo_32.png", "48": "img/logo_48.png", "128": "img/logo_128.png" } }, "homepage_url": "https://www.kvker.com/", "icons": { "16": "img/logo_16.png", "32": "img/logo_32.png", "48": "img/logo_48.png", "128": "img/logo_128.png" }, "manifest_version": 2 }
这里主要是看下content_scripts,这个说是scripts,你也可以看到,是可以塞一些css进去的,不过这里就看js。
util.js主要提供一个编辑时候使用的函数,作用是避免每次编辑触发input都转义Markdown2HTML,也就是debounce消抖了。
核心如下(附带throttle节流):
let doLastTimeout let doLastOperates = [] let timeout = 500 let kvkerUtil = { /** * 异步执行的多个操作,只执行最后一个操作,比如输入内容检索 * @param {function} operate 传入的操作 * @param {number} idx (可选)执行特性索引号的操作,一般不会用到 */ doAsyncLast(operate, time = 500, idx) { if (typeof operate !== 'function') { throw '执行doLast函数报错:需要传入函数!' } clearTimeout(doLastTimeout) doLastTimeout = setTimeout(() => { let lastOperate = doLastOperates[doLastOperates.length - 1] lastOperate() doLastOperates = [] clearTimeout(doLastTimeout) doLastTimeout = null }, time) doLastOperates.push(operate) }, /** * 某瞬间同步执行的多个操作,只执行最后一个操作,比如同时多个网络请求返回然后提示消息 * @param {function} operate 传入的操作 * @param {number} idx (可选)执行特性索引号的操作,一般不会用到 */ doSyncLast(operate, time = 500, idx) { if (typeof operate !== 'function') { throw '执行doLast函数报错:需要传入函数!' } if (!doLastTimeout) { doLastTimeout = setTimeout(() => { let lastOperate = doLastOperates[doLastOperates.length - 1] lastOperate() doLastOperates = [] clearTimeout(doLastTimeout) doLastTimeout = null }, time) } doLastOperates.push(operate) }, }
然后是turndown.js,这个是marked.js的反向。marked是把Markdown2HTML,那么turndown就是把HTML2Markdown了。这种东西当然是轮子了,安全好用(npm)。
至于content/index.js,就是核心页面插入的js(不是注入inject,这俩有差,这里不细说),就是document有了就运行的函数,一般都是document_start。这个等下结合插件的js说。
这个文件最后就是看popup.html,这个文件名随意区,作用是点击插件显示的那个小窗户,拿FeHelper看就是这样的:
看下内容:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Popup</title> <style> * { box-sizing: border-box !important; } body { margin: 0; padding: 4px; } #textarea { resize: none; outline: none; padding: 8px; } </style> </head> <body> <h1>头条Markdown编辑器</h1> <textarea id="textarea" cols="80" rows="30"></textarea> <script src="libs/marked.js"></script> <script src="js/popup.js"></script> </body> </html>
常规内容,长这样:
就一个输入框和header,没了,监听这个输入框变化。
然后引入js,marked.js就不用说了,popup.js就是这个页面核心js了,下面细说。
到这里,功能页面与资源齐全了(不算icon什么的)。
逻辑
- 插件的页面输入内容要同步到网页的输入框里面,而且由于网页的输入框是富文本,所以得是Markdown2HTML化之后的HTML字符;
- 网页启动时候,由于content/index.js加载早于富文本生成,所以想办法获取到富文本的标签;
- 网页启动时候,如果有草稿,得把草稿内容HTML2Markdown给插件输入框;
- 基于3,得提示用户在传HTML2Markdown之前,打开popup页面(插件页面),不然传给鬼了(插件页面打开关闭都是重新运行页面)。
一共上面4个核心问题处理,这个简易版插件就完成了(虽然没什么卵用)。
问题1
popup.js
let editor = document.querySelector('#textarea') // 监听输入,并传给content/index.js,并接收回调备用 editor.addEventListener('input', e => { sendMessageToContentScript({ cmd: 'test', value: marked(e.target.value) }, function(response) { console.log('来自content的回复:' + response) }) }) // 发送消息给content/index.js function sendMessageToContentScript(message, callback) { chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, message, function(response) { if(callback) callback(response) }) }) } // 监听页面生成的草稿…… chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { editor.value = request.value sendResponse('我是popup,我已收到你的消息:' + JSON.stringify(request)) })
具体都是chrome插件的api,主要看逻辑即可。
问题2,3,4
content/index.js
let sourceEditor // 每秒一次检查是否加载好编辑器 let interval = setInterval(() => { if(sourceEditor) { // 这里使用alert提示并且阻断运行,给用户时间打开插件……我是不是很机智 alert('插件装载完毕,请打开插件,再关闭弹窗') clearInterval(interval) // 发送草稿给popup sendInitialContent({cmd: 'initialData', value: new TurndownService().turndown(sourceEditor.innerHTML)}) } else { sourceEditor = document.querySelector('.ql-editor') } }, 1000) function sendInitialContent(message) { chrome.runtime.sendMessage(message, function(response) { console.log('收到来自后台的回复:' + response) }) } // 监听popup来的消息 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if(request.cmd === 'test') { console.log(request.value) kvkerUtil.doAsyncLast(() => sourceEditor.innerHTML = request.value) } sendResponse('我收到了你的消息!') })
没错,灵魂是哪个alert,YES!
效果
bug是有的,因为我也没去优化,反正也没用。而且头条这富文本标签挺奇葩的,得去魔改下marked.js才行。
主要是分享下逻辑,以及熟悉下chrome的api。
有兴趣的,可以扒拉源码研究下,没准哪个平台你有兴趣可以做一个完整版的~