动机
在blogger中写博客的时候经常需要插入代码,通过在HTML编辑模式下为代码段添加<code>...</code>
标签实现,一旦插入代码较多这个过程就显得比较繁琐了;因此考虑开发一个chrome的插件实现这个功能。主要的需求描述如下:选中需要添加代码HTML标签的代码段,右键菜单中显示需要添加代码段的模式,点击后将添加了HTML标签的代码段替换选中的原代码段。
Demo
设计思路
预期实现的功能:为所选的可编辑内容弹出右键菜单,根据相应的标签选择替换所选内容。
为了实现这个功能,首先要对chrome插件的架构有一定的了解。下图给出了Chrome插件的插件的一个基本框架图。(由来源作者总结,感觉很清晰)
值得注意的是:Chrome的插件是有一个独立的运行环境的,可以是popup或者background,均可以是由HTML/CSS/JavaScript编写,用于呈现插件的显示或者功能。而此外Chrome还提供了content script的方式,用于在“匹配的”网页中注入(injection)content script脚本,从而获取或者操作页面上的DOM(Document Object Model)对象。注意:处于安全性的考虑,content script只对页面上的DOM对象有操作的权限,而对页面中的JavaScript脚本或其中出现的变量等均无访问的权限。
Message机制:目前看来,content script与插件之间是相互隔离的,无法交互;而实际上Chrome还提供了一个Message的方法为content script与插件之间提供了通信的方式。例如:可以在content script中发送消息(sendMessage
),而在background script中注册该消息的监听(onMessage.addListener
),如此便可以实现content script通知background script的需求了;反之类似。
插件框架:有了以上对Chrome插件架构的基本了解以后,就可以设计本插件的框架图了,如下图所示。在extension的background页面调用了contextMenus这个API将插件功能添加到右键菜单中,为“selection”类型的内容(DOM对象)创建了该插件的右键菜单,并且在创建时绑定了“onclick
”方法,在其中调用了sendMessage
方法,以此通知content script当前选择了需要替换的内容以及需要添加的标签类型。另一方面,在content script中,注册了onMessage
方法,从而可以监听background中发送的消息。在onMessage
方法中,实现了对“selection”内容的添加标签后替换的功能。
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgq2JAg9e-IH106M-e9MqJ6dxtXKNJX_XXbp-v_fJ_1yLPQwVahuXu66oeP4blorcWYudl1XhVfpKTmKLfJzZHrSlIcYl8DJ_7Jc3wKd8OJYutjGpe-BOZ20CfYGxddAAuQcKhw3VJaFokr/s640/sshot-2.png) |
设计思路:插件框架图 |
实现
根据以上的设计思路,完成代码如下。(完整项目地址:
CodeTag)
Chrome 插件下载地址:
Code Tag
{
"name": "Code Tag",
"description": "This extension helps add html tag for editable selected context",
"version": "0.2",
"permissions": [
"contextMenus"
],
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["addTag.js"]
}
],
"background": {
"scripts": [
"background.js"
]
},
"icons": {
"128": "icon.png"
},
"manifest_version": 2
}
background.js
// parent menu
var parent = chrome.contextMenus.create({
"title": "Code Tag",
"contexts": ["selection"]
});
// sub-menu for block style
chrome.contextMenus.create({
"title": "Block",
"parentId": parent,
"contexts": ["selection"],
"onclick": function (info, tab) {
if (info.editable) {
chrome.tabs.query({
"active": true,
"currentWindow": true
}, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
// codetag message, indicating block style
"codetag": "block"
});
});
}
}
});
// sub-menu for inline style
chrome.contextMenus.create({
"title": "Inline",
"parentId": parent,
"contexts": ["selection"],
"onclick": function (info, tab) {
if (info.editable) {
chrome.tabs.query({
"active": true,
"currentWindow": true
}, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
// codetag message, indicating inline style
"codetag": "inline"
});
});
}
}
});
addTag.js
(content script)
// trancode "<" and ">" in code snippet into html coding
function transCode(text) {
var newStr;
newStr = text.replace(/</g, "<");
newStr = newStr.replace(/>/g, ">");
return newStr;
}
// register listener on message, fired when a sendMessage called in background
// in this function, selection is recognized and replaced with tag added context
// different tags are determined by the message sent from background
chrome.extension.onMessage.addListener(function (message, sender, callback) {
// get selection
var sel = window.getSelection();
var codeWithTag;
if (message.codetag == "block") {
codeWithTag = "<pre><code>" + transCode(sel.toString()) + "</code></pre>";
}
if (message.codetag == "inline") {
codeWithTag = "<code>" + transCode(sel.toString()) + "</code>";
}
var elem = document.activeElement;
var start = elem.selectionStart;
var end = elem.selectionEnd;
elem.value = elem.value.slice(0, start) + codeWithTag + elem.value.substr(end);
// Set cursor after selected text
elem.selectionStart = start + codeWithTag.length;
elem.selectionEnd = elem.selectionStart;
});
目前实现的功能是两个,分别可以添加
<code>...</code>
或者
<pre><code>...</code></pre>
标签,从而实现Inline(嵌入行内)和Block(代码块)模式。(
注:为了实现语法高亮,可以参考之前的文章:
使用 highlight.js 高亮博文中的代码)
遇到的问题
1. getSelection
无法获取预期的内容
起初,因为没有搞清楚Chrome插件的架构,导致踩了不少雷,其中就包括遇到getSelection
方法无法获取预期内容的问题;最初设计插件的时候只考虑了background,而未添加content script,这样一来实际上是无法通过JavaScript代码访问到页面内容的。而getSelection
是属于JavaScript中的方法。准确来说,在background中调用getSelection
也只是在background的页面中进行操作,也就是说如果访问DOM对象,例如document
,指代的是background页面,而我当时以为是当前的网页,这也就造成了getSelection
无法获取预期内容的现象。
2. info.selectionText
将换行符替换为空格
接上一个问题,由于无法获取getSelection
的预期内容,那么后续的操作也就无从进行下去。改变思路找到Chrome的contextMenus
中的info.selectionText
属性,可以获取在菜单创建时所选择的文本。看上去可以解决需求。但是当选中的文本中出现换行符时,就不能如愿了,换行符被替换为空格。这个方案再次失效。其中selectionText
将换行符替换为空格的原因可能在于Chrome在菜单的显示中提供了“%s
”直接转换选中文本的方式,如果保留换行符那么在菜单显示的时候就不方便了。
3.
插件的Debug窗口无法看到content script
最终找到了
Chrome插件(Extensions)开发攻略 这篇文章,对Chrome插件的架构有了重新的认识,意识到可以通过content script实现需求。与此同时也了解了Chrome的Debug工具。但是在调试的过程中又遇到了新的问题。原本是在插件的背景页启动了Debug页面,而在其中的content script部分并未发现所写的content script代码,无从调试。原因在于content script代码是注入到匹配的页面中,而不是插件的背景页,所以要调试content script需要在网页中进行审查(Inspector),启动Debug。
4.
contextMenus
同时满足两个条件
需求中是要对
可编辑(
editable
)的
选中内容(
selection
)进行替换,
contextMenus
可以根据点击发生的对象类型弹出相应的右键菜单,并且可以对不同的对象类型进行“OR”操作,但是并不支持“AND”操作,因此仅对于可编辑的选中内容右键弹出该插件菜单的操作是不能“直接”实现的。简单地变通方式可以如下:在
contextMenus
中只关注“
selection
”类型的对象,并且检查对象的“
info.editable
”属性进行判断,如果为真则触发
sendMessage
方法,否则不进行处理。
参考