乐趣区

使用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 什么的)。


逻辑

  1. 插件的页面输入内容要同步到网页的输入框里面,而且由于网页的输入框是富文本,所以得是 Markdown2HTML 化之后的 HTML 字符;
  2. 网页启动时候,由于 content/index.js 加载早于富文本生成,所以想办法获取到富文本的标签;
  3. 网页启动时候,如果有草稿,得把草稿内容 HTML2Markdown 给插件输入框;
  4. 基于 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 源码

退出移动版