场景
技术者写文章,基本少不了 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。
有兴趣的,可以扒拉源码研究下,没准哪个平台你有兴趣可以做一个完整版的~
资源
头条插件 v0.0.1 源码