场景
技术者写文章,基本少不了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看就是这样的:
看下内容:
Popup
头条Markdown编辑器
常规内容,长这样:
就一个输入框和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源码