Google Chrome 扩展 - CodeProject 模板项
Google Chrome 扩展,提供消息和答案模板。
- Chrome 网上应用店 - 由于新的 Google 政策要求,安装程序从 Chrome 网上应用店提供。
- 下载源代码 V2.0 - 182 KB
V2 重要变更
由于 Google 为保护用户免受有害代码侵害而实施的政策变更,该扩展程序需要进行多项更改。
- 将清单更新到新的 V2 标准。
- 从 Chrome 网上应用店分发扩展程序
- 从 HTML 页面中删除内联脚本块。
我还利用这些更改将 jQuery 升级到新版本。
引言
如果您是 CodeProject 的常客,或者可能是文章版主、导师,甚至花时间在问答区回答问题,您有多少次发现自己发布了相同的内容?您有多少次发现自己前往常见问题列表复制文章常见问题链接?一次、两次、十次?那么,为了节省再次输入,而去查找您以前的帖子并复制它再次使用呢?
这就是这个工具和文章诞生的原因。我想一定有更好的方法,考虑过保存一个文本文件,但放弃了这个想法,然后想等等,我再制作一个 Chrome 扩展程序。我已经做过一次了,所以重复那次的成功应该相当容易。
该工具允许用户创建存储在浏览器上下文中的模板项,并一键将选定的模板项直接插入到您的论坛消息或答案中。您甚至可以只保留小片段,例如链接,并能够快速将其添加到您的消息/答案中。
版本 1.1 修复了一些错误,并引入了第三种项目类型以及编辑器和选择器表单中的过滤功能。
涵盖的主题
在本文中,我们将重点关注以下主要内容:
- Chrome 后台页面和 PageActions
- 从扩展程序上下文在浏览器页面上下文中注入和执行 Chrome 脚本
- 一些用于处理对象克隆、项目识别的 JQuery
Chrome 扩展程序 - 它们是什么?
扩展程序是 Google Chrome 浏览器的附加组件,为了避免重复,请查看我的上一篇 Chrome 扩展程序文章,其中解释了基础知识,并提供了 Chrome 扩展程序和 JQuery 开发者页面的链接。文章可以在这里找到:Chrome 扩展程序 - CodeProject 声誉监视器。如果您以前从未接触过扩展程序,我建议您先阅读此文,因为它涵盖了一些关键的扩展程序创建信息,例如扩展程序结构、清单文件、打包和部署。
注意: Google Chrome 已更改了未由 Chrome 网上应用店提供的打包扩展程序的安装方式。这是为了降低安装“可疑”扩展程序的风险。要使用我网站上的打包扩展程序,请点击链接,然后从“工具”菜单切换到“扩展程序”页面,然后将浏览器底部下载栏中的文件拖到“扩展程序”页面上。您将看到“拖放以安装”消息出现。
这个扩展程序怎么样?
这个扩展程序非常相似,关于 JavaScript 文件、JQuery 引用等方面的想法都一样。这个扩展程序与前一个不同之处在于它的操作方式、触发事件以及与用户页面的交互方式。
首先,让我们看一下这个扩展程序的清单文件
{
"manifest_version": 2,
"name": "CodeProject Template Items",
"short_name": "CPTemplates",
"version": "2.0",
"content_security_policy": "script-src 'self' https://# 'unsafe-eval'; object-src 'self' ",
"description": "Chrome Extension to provide CodeProject template items.",
"icons": {
"48": "images/bob48.png",
"128": "images/bob128.png"
},
"background": {
"scripts": [ "js/background.js" ]
},
"page_action": {
"default_icon": "images/bob.png",
"default_title": "CodeProject: Insert Template Items",
"default_popup": "selector.html"
},
"options_page": "editor.html",
"permissions": [
"tabs",
"http://*.codeproject.com/"
]
}
可以看到常见的项,名称、版本、描述等,但有几个不同之处
background
- 这是在浏览器后台运行的文件,它设置了扩展程序的浏览器集成。在版本 1 中,这是一个带有内联脚本的 html 页面,但对于 V2,需要将其提取到独立的 javascript 文件中。page_action
- 这设置了一个可以在适当情况下由用户触发的操作,其中包含的属性设置了显示的图标、工具提示文本和向用户显示的页面。
您还可以看到根据新规则要求进行的'manifest_versions'
和'content_security_policy'
更改。
后台脚本 - 它在做什么?
当浏览器启动时,扩展程序的后台会被加载,其中的任何脚本都会被执行。请看下面的页面内容
// Check if the address contains the Message Editor URL;
if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
// ... show the page action.
chrome.pageAction.show(tabId);
}
// Check if the address contains the Question URL;
if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
// ... show the page action.
chrome.pageAction.show(tabId);
}
};
// Add a handler to listen for Address Bar changes in the browser, set the callback function
chrome.tabs.onUpdated.addListener(checkForTargetMessageEditorUrl);
当脚本加载时,它会向浏览器添加一个监听器。此监听器等待 Tabs 的 onUpdated()
事件,然后触发回调函数 checkForTargetMessageEditorUrl()
。Chrome 会将对已更新的浏览器标签页的引用传递给回调。检查标签页的 URL,如果它包含适用网页的地址,在本例中是消息编辑器或问题页面,它将在浏览器地址区域内显示清单文件中定义的页面动作图标。如果 URL 不是要查找的 URL,则不显示页面动作图标。地址栏中的页面动作图标如下所示
现在我们有了一个合适的 URL 检查后台代码,它可以向用户显示页面操作。
页面动作
现在,用户可以使用页面操作了,当用户点击图标时,清单文件中定义的弹出窗口会显示给用户,如下图所示
用户现在可以点击她/他想要使用的模板项,然后点击选择以将内容注入浏览器页面。我们稍后将介绍此列表中项目的添加和编辑。
内容是如何注入的?
关于 Chrome、用户页面和扩展程序页面,需要了解的关键一点是它们都彼此隔离。扩展程序和网页之间唯一的共同区域是网页 DOM,扩展程序无法访问页面的脚本或变量,页面也无法访问扩展程序的脚本或变量。唯一的共同元素是网页 DOM,当然,还有托管的 Chrome 浏览器。
让我们看看当用户点击模板项并点击Select
时执行的代码。
//-----------------------------------------
// Button Events - Selector
//-----------------------------------------
function buttonSelect_Click() {
//
//Get the current window and then the current tab of that window
//This should relate to the page on which to do the injection
chrome.windows.getCurrent(function (theWindow) {
chrome.tabs.getSelected(theWindow.id, function (theTab) {
injectToTab(theTab) });
});
}
click
事件有效地询问 Chrome,“当前活动的 Chrome 窗口是哪个?”结果是一个 Window
对象,它被传递给一个回调函数。然后 Chrome 会被询问,“你刚刚告诉我关于的那个窗口的活动标签页是哪个?”Chrome 会将一个 Tab
对象返回给另一个回调函数,然后该函数用于注入内容。
function injectToTab(tab) {
//Build Valid Content for remote execution
var templateItem = parseInt($("#selectTemplates option:selected").val());
var subject = templateStore[templateItem].Subject;
var body = templateStore[templateItem].Body;
// Check if the address contains the Message Editor URL;
if (tab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
//Append the subject
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById('ctl00_MC_Subject').value +=
unescape('" + escape(subject) + "');"
});
//Append the body
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById('ctl00_MC_ContentText_MessageText').value +=
unescape('" + escape(body) + "');"
});
}
// Check if the address contains the Question URL;
if (tab.url.indexOf(".codeproject.com/Questions/") > -1) {
//Append the answer
chrome.tabs.executeScript(tab.id, { code:
"document.getElementById
('ctl00_ctl00_MC_AMC_PostEntryObj_Content_MessageText').value +=
unescape('" + escape(body) + "');"
});
}
}
该函数检查模板列表中选择了哪个项目。然后通过内存中模板数组的索引检索该项目。接着,代码根据标签页的 URL 检查我们正在注入到哪个页面。然后,我们构建一个我们想要在页面上下文中执行的脚本的 string
表示,并在其中添加模板项目的相关属性。在开发此功能时,我发现 string
中的换行符无法正确处理,因此我们必须使用 JavaScript 的 escape
和 unescape
方法才能正确传递数据。
此 string
构建完成后,Chrome 会收到脚本的 string
表示,然后会在网页的上下文中执行该脚本。此脚本执行并向已通过 ID 识别的元素追加模板数据。
添加和存储模板项
有两种方法可以访问用于添加/编辑模板项的页面。第一种方法是从 Chrome 扩展程序页面,然后点击选项;第二种方法是右键单击页面操作图标并选择选项。
选项页面在此例中在清单文件editor.html中定义,该页面被加载。编辑器入口页面基本上是模板列表,如下图所示
然后您可以点击添加,或者选择一个现有模板来编辑/删除/克隆该项。
浏览器 LocalStorage
用于存储内存中模板项的 string
表示。JSON 用于将 JavaScript 对象 stringify
并将其放入本地存储,并且 JSON 也用于将 string
转换回 JavaScript 对象。
当编辑器页面加载时,存储被初始化,检查对象是否已经定义,如果未定义则创建一个新的空存储。如果已经存在,则使用 JSON 将对象 parse
回数组。
初始化存储并将对象 parse
回内存的代码如下所示
function initStorage() {
if (localStorage["TemplateStore"] == undefined) {
localStorage["TemplateStore="] = "{}";
}
else {
templateStore = JSON.parse(localStorage["TemplateStore"]);
}
}
使用 JSON 将数组 stringify
到本地存储的代码如下所示
function saveToLocalStorage() {
//Write out the template store to the local storage
localStorage["TemplateStore"] = JSON.stringify(templateStore);
}
当编辑器加载时,如果存在现有模板项,该数组会使用 JQuery 作为 option
元素加载到页面选择列表中。加载操作分两个阶段进行,第一阶段检查当前页面(由过滤使用),第二阶段根据当前过滤设置将模板加载到 HTML 中,您还可以看到索引号被写入到 option 元素中。
function loadTemplates() {
chrome.windows.getCurrent(function (theWindow) {
chrome.tabs.getSelected(theWindow.id, function (theTab) {
// Check if the address contains the Message URL;
if (theTab.url.indexOf(".codeproject.com/script/Forums/Edit.aspx?") > -1) {
loadTemplatesPart2("Message");
}
else {
// Check if the address contains the Question URL;
if (theTab.url.indexOf(".codeproject.com/Questions/") > -1) {
loadTemplatesPart2("Answer");
}
else {
loadTemplatesPart2("");
}
}
});
});
}
function loadTemplatesPart2(urlType) {
//Clear the list box of any items
$("#selectTemplates").html(""); //Clear the existing selection box
if (templateStore.length > 0) {
//Load Template Items
$("#templateLoading").html("Template store contains: " +
templateStore.length + " item(s).");
//get the filter mode
var filter = $("#selectFilter option:selected").val();
//Get each items Title and add it to the list subject to filters
for (item in templateStore) {
var addit = false;
switch (filter) {
case "All":
//Add all items
addit = true;
break;
case "Auto":
//Always add the snippets regardless of Answer/Message
if (templateStore[item].Type == "Snippet") {
addit = true;
}
else {
//if the item matches the page (message/answer) then addit
addit = (templateStore[item].Type == urlType);
}
break;
default:
//Add if item == filter type
addit = (templateStore[item].Type == filter);
}
if (addit) {
$("#selectTemplates").html($("#selectTemplates").html() +
"<option value="\"" + item.toString() + "\">" +
templateStore[item].Title + "</option>");
}
}
$("#templateLoading").html($("#templateLoading").html() +
" Displaying: " + $("#selectTemplates").prop("length").toString() + " item(s)");
$("#templateListing").show();
}
else {
// No items to Load
$("#templateLoading").html("No items located in local store.");
$("#templateListing").show();
if (!templateSelectMode) {
$("#templateEdit").hide();
$("#buttonEdit").hide();
$("#buttonClone").hide();
$("#buttonDelete").hide();
}
else {
$("#buttonSelect").hide();
}
}
//Add always is shown (in editor form).
if (!templateSelectMode) {
$("#buttonAdd").show();
}
}
添加和编辑模板项
当用户点击添加时,会显示编辑器表单,用户可以在其中输入各种属性的文本。然后创建一个新的模板对象并将其推入数组并存储到本地存储中。
如果用户选择一个现有项,它将被加载到编辑器中,用户可以对其进行更新,然后将其推回数组并写入本地存储。编辑表单只是一个隐藏的 DIV
,它与模板选择器 DIV
交换。这个编辑表单如下所示
以下代码演示了如何识别列表中选择用于编辑的项,从存储数组中取出并用现有详细信息更新表单。
function buttonEdit_Click() {
templateEditMode = true;
templateEditModeItem = parseInt($("#selectTemplates option:selected").val());
//V1.1 Change Switch block replaces If and added Snippet handling
switch (templateStore[templateEditModeItem].Type) {
case "Message":
$("#RadioTypeMessage").prop('checked', true);
break;
case "Answer":
$("#RadioTypeAnswer").prop('checked', true);
break;
case "Snippet":
$("#RadioTypeSnippet").prop('checked', true);
break;
}
$("#TextTitle").val(templateStore[templateEditModeItem].Title);
$("#TextSubject").val(templateStore[templateEditModeItem].Subject);
$("#TextBody").val(templateStore[templateEditModeItem].Body);
$("#templateListing").hide();
$("#templateEdit").show();
}
以下代码是执行的保存操作,用于添加新项目或编辑现有项目。当一个项目被编辑时,会创建一个新项目,旧项目在存储数组中被替换。然后将更新后的数组写入本地存储。
function buttonSave_Click() {
//Create new item
var item = new Template();
//Add item properties
item.Type = $("input[name=RadioType]:checked").val();
item.Title = $("#TextTitle").val().toString();
item.Subject = $("#TextSubject").val().toString();
item.Body = $("#TextBody").val().toString();
if (templateEditMode) {
//Edit Mode
templateStore[templateEditModeItem] = item;
templateEditMode = false;
}
else {
//Add Mode
templateStore.push(item);
}
//Save to local storage
saveToLocalStorage();
//Restart
initEditorView();
}
正如您在上面的代码中看到的,我们通过执行 new Template()
创建一个新模板。Template 实际上是一个返回合适格式对象的函数。执行此操作的函数是
//Template Object Constructor
function Template() {
this.Type = "";
this.Title = "";
this.Subject = "";
this.Body = "";
}
您可能还在编辑器选择器表单上注意到了一个克隆按钮。这允许我们复制一个现有项目。为此,我们使用 JQuery 对现有对象进行深度复制到一个新对象中,首先我们从数组中获取现有项目,将其克隆到一个新对象中,将新对象添加到数组中并将更新写入本地存储。代码如下所示
function buttonClone_Click() {
//Get the current selected item
var original = templateStore[parseInt($("#selectTemplates option:selected").val())];
//Perform a deep copy of the original using JQuery
var copy = $.extend(true, {}, original);
//Push it into the store at the end
templateStore.push(copy);
//save
saveToLocalStorage();
initEditorView();
}
过滤功能
版本 1.1 在编辑器表单和选择器表单上引入了过滤功能。这允许用户在有多种不同类型(消息/答案/片段)的项目时,向下筛选以处理项目。
选择器表单还具有自动过滤器(默认选择)。它的作用是检查用户正在浏览哪个 CodeProject 页面,并相应地过滤列表。如果用户在消息编辑器上,列表会自动过滤消息和片段。如果用户在问题页面上,则列表会自动过滤答案和片段。
通过将模板数组项索引作为选项的值属性存储在 HTML 标记中,并仅将相关项添加到列表中,实现过滤。然后读取该值以识别存储数组中的源索引。
我还需要知道什么?
标题字段是您自己用于识别模板项及其内容的可自由编辑文本,主题字段仅用于论坛消息,正文用于论坛消息、答案和片段。
还有其他吗?接下来是什么?
有关打包、部署和托管等详细信息,请参阅之前的扩展程序链接。
文章顶部的链接是未打包的源代码以及打包分发服务器的链接。如果您使用此功能,Chrome 将自动获取任何未来的更新。
未来
如果您有任何想法,或者发现任何问题,请在下面留言,我会尽力解决。
好了,暂时就到这里,希望您觉得这很有用。
参考链接
历史
- 2014年8月29日 - 更新至V2,以支持新的Google政策要求。
- 2013年10月23日 - 补充了关于在Chrome网上应用商店之外安装的注意事项
- 2011年9月16日 - V1.1,添加了片段类型、过滤功能并修复了添加项错误
- 2011年8月22日 - V1.0,首次发布