使用Chrome插件来补充一些写作网站没有Markdown的坑

chzh0 2019-09-01

场景

技术者写文章,基本少不了Markdown了,但是很多自媒体平台(大而全那种),往往都是坑爹的富文本编辑器(还很多是魔改UEditor,人家官方三年没更新了喂)。

类似这种:

使用Chrome插件来补充一些写作网站没有Markdown的坑

这是很麻烦的一件事,尤其是那些没有代码块的编辑器,没错,说的就是你,头条!这种坑爹玩意儿,就得让程序员手动粘贴代码过来,然后遇到排版不友好的,呵呵,对,说的还是你,头条!
于是吧,我就想着,奶奶个熊,没有我就自己写个插件来搞吧。

事实上,我自己的网站上有自己依赖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看就是这样的:

使用Chrome插件来补充一些写作网站没有Markdown的坑

看下内容:

<!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>

常规内容,长这样:

使用Chrome插件来补充一些写作网站没有Markdown的坑

就一个输入框和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!


效果

使用Chrome插件来补充一些写作网站没有Markdown的坑

bug是有的,因为我也没去优化,反正也没用。而且头条这富文本标签挺奇葩的,得去魔改下marked.js才行。

主要是分享下逻辑,以及熟悉下chrome的api。

有兴趣的,可以扒拉源码研究下,没准哪个平台你有兴趣可以做一个完整版的~


资源

头条插件v0.0.1源码

相关推荐

xiongweiwei00 / 0评论 2020-05-26
tenvainvi / 0评论 2020-05-10
Kingcxx / 0评论 2020-03-07
xiongweiwei00 / 0评论 2020-03-04