Chrome扩展插件开发

1. 简介

Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,Chrome浏览器扩展开发算是相当简单的,基本只要掌握HTML+CSS+Javascript,即可快速开发一个属于你的Chrome插件!它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包.

2. 学习Chrome插件开发有什么意义?

增强浏览器功能,轻松实现属于自己的“定制版”浏览器,等等。
Chrome插件提供了很多实用API供我们使用,包括但不限于:

  • 书签控制;
  • 下载控制;
  • 窗口控制;
  • 标签控制;
  • 网络请求控制,
  • 各类事件监听;
  • 自定义原生菜单;
  • 完善的通信机制;
    等等;
4. 开发与调试

Chrome插件没有严格的项目结构要求,只要保证本目录有一个manifest.json即可,普通的web开发工具即可。
从右上角菜单->更多工具->扩展程序可以进入 插件管理页面,也可以直接在地址栏输入 chrome://extensions 访问。

5. 核心介绍

manifest.json是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。

下面给出的是一些常见的配置项,均有中文注释,完整的配置文档请戳 这里

{
 //必选
 /*
   指定您的应用包要求的清单文件格式的版本。从 Chrome 18 开始,开发人员应该指定 2
 */
 "manifest_version": 2,
 "name":"我的应用名称",
 "version":"我的应用版本",

 //推荐
 /*
   清单文件-默认语言 指定_locales中的子目录,包含该应用默认字符串。
   对于含有 _locales 目录的应用来说这一属性是必需的,
   在没有 _locales 目录的应用中该属性不能存在
 */
 "default_locale":"en", 

 /*
   这个描述在安装应用之后可以看见
 */
 "description":"关于应用的描述", 

 /*一个或多个代表应用、应用或主题背景的图标*/
 "icons":{
   "16":"icon16.png",
   "48":"icon48.png"
 },

 /*
  选择某一个(或者无)
  browser_action(浏览器按钮)
  page_action(页面按钮)
 */

 // 如果有 browser_action, 即在 chrome toolbar 的右边添加了一个 icon
 "browser_action": {
   "default_icon": "advicedog.jpg",
   "default_title": "Google Mail",      // tooltip, 光标停留在 icon 上时显示
   "default_popup": "popup.html"  // 如果有 popup 的页面, 则用户点击图标就会渲染此 HTML 页面
 },


 // 如果并不是对每个网站页面都需要使用插件, 可以使用 page_action(页面按钮) 而不是 browser_action(浏览器按钮)
 // browser_action 应用更加广泛
 // 如果 page_action 并不应用在当前页面, 会显示灰色

 "page_action":{
   "default_icon": {                    // 可选
     "19": "images/icon19.png",           // 可选
     "38": "images/icon38.png"            // 可选
   },
   "default_title": "Google Mail",      // 可选,在工具提示中显示
   "default_popup": "popup.html"        // 可选
 },

 //可选
 "author":"开发者",
 "automation":"",


 /*
 后台网页
 1.应用通常需要有一个长时间运行的脚本来管理一些任务或状态,而后台网页就是为这一目的而设立。
 通常情况下,后台页面不需要任何 HTML 标记,这种情况下后台页面可以单独使用 JavaScript文件实现。
 后台页面将由应用系统生成,包含 scripts 属性中列出的每一个文件。

 2.page:如果您需要在您的后台页面中指定 HTML,您可以改用 page 属性:

 3.persistent:应用和应用通常需要长时间运行的脚本来管理某些任务或状态,这就是事件页面的作用。
 事件页面只在需要时加载,当事件页面不活动时就会卸载,以便释放内存和其他系统资源。
 如何得到事件页面 就是设置一个"persistent"键,如果没有设置,你将得到一个普通的后台页面。
 */
 "background":{
   "scripts":["background.js"],
   "page": "background.html",
   "persistent":false
 },


 /*
   内容脚本:其实就是向你想要的网页中插入一个脚本代码,执行你想要做的事情
   内容脚本是在网页的上下文中运行的 JavaScript 文件,
   它们可以通过标准的文档对象模型(DOM)来获取浏览器访问的网页详情,或者作出更改。
   
   1.run_at 可选。
   控制 js 中的 JavaScript 文件何时插入,
   可以为 "document_start"、
   "document_end" 或 "document_idle",默认为 "document_idle"。 


   1.1如果是 "document_start",这些文件将在 css 中指定的文件之后,但是在所有其他 DOM 构造或脚本运行之前插入。 

   1.2.如果是 "document_end",文件将在 DOM 完成之后立即插入,但是在加载子资源(如图像与框架)之前插入。 

   1.3.如果是 "document_idle",浏览器将在 "document_end" 和刚发生 window.onload 事件这两个时刻之间选择合适的时候插入,
   具体的插入时间取决于文档的复杂程度以及加载文档所花的时间,并且浏览器会尽可能地为加快页面加载速度而优化。 
 
   2.all_frames 可选。
   控制内容脚本运行在匹配页面的所有框架中还是仅在顶层框架中。 默认为 false,意味着仅在顶层框架中运行
   
   content_scripts还有一些其他不是很常用的属性
 */

 "content_scripts": [{
   "matches": ["https://*.pingan.com.cn/*"], //匹配的地址网页
   "exclude_matches":[],
   "js": ["jquery.js","ideacome.js"], //插入的js
   "css": ["mystyles.css"], //css改变样式
   "run_at":"document_idle",
   "all_frames": true //该匹配下面的所有窗口
 },{
   "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
   "js": ["js/show-image-content-size.js"] //可以针对不同的规则插入不同的内容
 }],

// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
"web_accessible_resources": [
   "images/*.png",
   "style/double-rainbow.css",
   "script/double-rainbow.js",
   "script/main.js",
   "templates/*"
 ],

/**
  如果不是通过 chrome web store 自动更新插件

   我们希望扩展能自动升级,理由和让chrome自动升级一样:修改程序bug和安全漏洞 ,增加新功能,提升性能,改善体验。
   一个扩展的manifest文件里面必须指定一个"update_url"来执行升级检测。

   扩展可以托管在Chrome Web Store,也可以发布到极速浏览器应用开放平台上。
   如果托管在Chrome Web Store则update_url应该是:http://clients2.google.com/service/update2/crx    
 **/
 "update_url": "https://clients2.google.com/service/update2/crx",

// 插件主页,这个很重要,不要浪费了这个免费广告位
"homepage_url": "https://www.baidu.com",

/*  
   扩展或app将使用的一组权限。每个权限是一列已知字符串列表中的一个,
   如geolocatioin或者一个匹配模式,来指定可以访问的一个或者多个主机。
   权限可以帮助限定危险,如果你的扩展或者app被攻击。
   一些权限在安装之前,会告知用户
 */
 "permissions":[
   "tabs", //Required if the extension uses the chrome.tabs or chrome.windows module.
   "bookmarks", //使用chrome.bookmarks模块来创建、组织和管理书签
   "http://www.blogger.com/",    
   "http://*.google.com/",    
   "unlimitedStorage", //提供了一个无限的HTML5配额来存储客户端数据,如数据库和本地存储文件。没有这个权限,扩展仅限于5 MB的本地存储
   "history" //历史记录的使用权限  chrome.history 
   "notifications",//提示
   "cookies",//Required if the extension uses the chrome.cookies module.
 ],

/**开发时为扩展指定的唯一标识值。
注意:通常您并不需要直接使用这个值,而是在您的代码中使用相对路径或者chrome.extension.getURL()得到的绝对路径。
这个值并不是开发时显式指定的,而是Chrome在安装.crx时辅助生成的。(开发时可以通过上传扩展或者手工打包生成crx文件)。 安装完crx,在Chrome的用户数据目录下的Default/Extensions/<extensionId>/<versionString>/manifest.json文件中,您可以看到这个扩展的key。**/

key:'',

"commands": {
     // commands API 用来添加快捷键
     // 需要在 background page 上添加监听器绑定 handler
   "toggle-feature-foo": {
     "suggested_key": {
       "default": "Ctrl+Shift+Y",
       "mac": "Command+Shift+Y"
     },
     "description": "Toggle feature foo",
     "global": true
       // 当 chrome 没有 focus 时也可以生效的快捷键
       // 仅限 Ctrl+Shift+[0..9]
   },
   "_execute_browser_action": {
     "suggested_key": {
       "windows": "Ctrl+Shift+Y",
       "mac": "Command+Shift+Y",
       "chromeos": "Ctrl+Shift+U",
       "linux": "Ctrl+Shift+J"
     }
   },
   "_execute_page_action": {
     "suggested_key": {
       "default": "Ctrl+Shift+E",
       "windows": "Alt+Shift+P",
       "mac": "Alt+Shift+P"
     }
   },
   ...
 },
 "content_capabilities": ...,
 "optional_permissions": ["tabs"], // 其他需要的 permission, 在使用 chrome.permissions API 时用到, 并非安装插件时需要

 "short_name": "Short Name", // 插件名字简写

"storage": {
   "managed_schema": "schema.json"
 }, //  使用 storage.managed api 的话, 需要一个 schema 文件指定存储字段类型等, 类似定义数据库表的 column

......
//还有很多其他的配置
}
5.1 content-scripts

所谓content-scripts,其实就是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还可以包括css的),借助content-scripts我们可以实现通过配置的方式轻松向指定页面注入JS和CSS,最常见的比如:广告屏蔽、页面CSS定制,等等。

content-scripts和原始页面共享DOM,但是不共享JS,如要访问页面JS(例如某个JS变量),只能通过injected js来实现。content-scripts不能访问绝大部分chrome.xxx.api,除了下面这4种:

  • chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
  • chrome.i18n
  • chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
  • chrome.storage

这些API绝大部分时候都够用了,非要调用其它API的话,你还可以通过通信来实现让background来帮你调用。

5.2 background

后台y页面,这是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。

background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS。

经过测试,其实不止是background,所有的直接通过chrome-extension://id/xx.html这种方式打开的网页都可以无限制跨域。

配置中,background可以通过page指定一张网页,也可以通过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页:

{
    // 会一直常驻的后台JS或后台页面
    "background":
    {
        // 2种指定方式,如果指定JS,那么会自动生成一个背景页
        "page": "background.html"
        //"scripts": ["js/background.js"]
    },
}

#######5.3 event-pages
这里顺带介绍一下event-pages,它是一个什么东西呢?鉴于background生命周期太长,长时间挂载后台可能会影响性能,所以Google又弄一个event-pages,在配置文件上,它与background的唯一区别就是多了一个persistent参数:

{
    "background":
    {
        "scripts": ["event-page.js"],
        "persistent": false
    },
}

它的生命周期是:在被需要时加载,在空闲时被关闭,什么叫被需要时呢?比如第一次安装、插件更新、有content-script向它发送消息,等等。

5.4 popup

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。


image.png

popup可以包含任意你想要的HTML内容,并且会自适应大小。可以通过default_popup字段来指定popup页面,也可以调用setPopup()方法。

需要特别注意的是,由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码千万不要写在popup里面。

在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。

5.5 homepage_url

开发者或者插件主页设置


image.png
5.6 injected-script

指的是通过DOM操作的方式向页面注入的一种JS。为什么需要通过这种方式注入JS呢?

这是因为content-script有一个很大的“缺陷”,也就是无法访问页面中的JS,虽然它可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-script中的代码(包括直接写onclick和addEventListener2种方式都不行),但是,“在页面上添加一个按钮并调用插件的扩展API”是一个很常见的需求,那该怎么办呢?其实这就是本小节要讲的。

在content-script中通过DOM方式向页面注入inject-script代码示例:

// 向页面注入JS
function injectCustomJs(jsPath)
{
    jsPath = jsPath || 'js/inject.js';
    var temp = document.createElement('script');
    temp.setAttribute('type', 'text/javascript');
    // 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
    temp.src = chrome.extension.getURL(jsPath);
    temp.onload = function()
    {
        // 放在页面不好看,执行完后移除掉
        this.parentNode.removeChild(this);
    };
    document.head.appendChild(temp);
}

manifest.json
{
    // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
    "web_accessible_resources": ["js/inject.js"],
}

6. Chrome插件的7种展示形式

6.1 browserAction(浏览器右上角)

通过配置browser_action可以在浏览器的右上角增加一个图标,一个browser_action可以拥有一个图标,一个tooltip(即划过显示title),一个badge(图标上面的文字,有字数限制)和一个popup。


tooltip,badge,popup
6.2 pageAction(地址栏右侧)

pageAction和普通的browserAction一样也是放在浏览器右上角,也可以说地址栏的右侧更为准确。只不过没有点亮时是灰色的,点亮了才是彩色的,灰色时无论左键还是右键单击都是弹出选项:

// manifest.json
{
  "name": "测试",
  "description": "........",
  "version": "1.0",
  "permissions": [
    "declarativeContent"
  ],
  "background":{
    "scripts":["background.js"]
  },  
  "page_action": {
    "default_icon": "icon.png",
    "default_title": "我是pageAction",
    "default_popup": "popup.html"
  },
  "manifest_version": 2
}


// background.js
chrome.runtime.onInstalled.addListener(function(){
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
        chrome.declarativeContent.onPageChanged.addRules([
            {
                conditions: [
                    // 只有打开百度才显示pageAction
                    new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
                ],
                actions: [new chrome.declarativeContent.ShowPageAction()]
            }
        ]);
    });
});
6.3 右键菜单(api地址

通过开发Chrome插件可以自定义浏览器的右键菜单,主要是通过chrome.contextMenusAPI实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等

//manifest.json
  "permissions": [
    "declarativeContent",
    "contextMenus",
    "tabs"
  ],
  "background":{
    "scripts":["background.js"]
  },  

//background.js
//可以右键看看
chrome.contextMenus.create({
    title: "测试右键菜单",
    onclick: function(){alert('您点击了右键菜单!');}
});

//选择某些文字才出现这个右键菜单
chrome.contextMenus.create({
    title: '使用度娘搜索:%s', // %s表示选中的文字
    contexts: ['selection'], // 只有当选中文字时才会出现此右键菜单
    onclick: function(params)
    {
        // 注意不能使用location.href,因为location是属于background的window对象
        chrome.tabs.create({url: 'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(params.selectionText)});
    }
});
6.3.2 右键菜单一些基本的api
hrome.contextMenus.create({
    type: 'normal', // 类型,可选:["normal", "checkbox", "radio", "separator"],默认 normal
    title: '菜单的名字', // 显示的文字,除非为“separator”类型否则此参数必需,如果类型为“selection”,可以使用%s显示选定的文本
    contexts: ['page'], // 上下文环境,可选:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],默认page
    onclick: function(){}, // 单击时触发的方法
    parentId: 1, // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
    documentUrlPatterns: 'https://*.baidu.com/*' // 只在某些页面显示此右键菜单
});
// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);
6.4. override(覆盖特定页面)

使用override页可以将Chrome默认的一些特定页面替换掉,改为使用扩展提供的页面。

//一个扩展只能替代一个页面;
"chrome_url_overrides":
  {
      "newtab": "newtab.html"
       "history": "history.html",
      "bookmarks": "bookmarks.html"
  }
6.5 option(选项页)

为了让用户自定义您的应用的行为,您可能会提供一个选项页面。

所谓选项(options)页,就是插件的设置页面,有2个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面:


image.png
//manifest.json

//Chrome40以前的插件配置页写法
"options_page":"xx.html",

//Chrome40以后的插件配置页写法,只是样式不一样了,以弹出框的形式显示
"options_ui":
  {
      "page": "options.html",
      // 添加一些默认的样式,推荐使用
      "chrome_style": true
 },
//为了兼容,建议2种都写,如果都写了,Chrome40以后会默认读取新版的方式;
Chrome40以后的插件配置页写法

页面内容看你自己发挥了。这边有一个示例可以看看

6.6 omnibox

omnibox是向用户提供搜索建议的一种方式。具体看api

6.7 桌面通知

Chrome提供了一个chrome.notificationsAPI以便插件推送桌面通知,暂未找到chrome.notifications和HTML5自带的Notification的显著区别及优势。

在后台JS中,无论是使用chrome.notifications还是Notification都不需要申请权限(HTML5方式需要申请权限),直接使用即可。

"permissions": [
    "notifications"
  ],
chrome.contextMenus.create({
    title: "测试右键菜单",
    onclick: function(){
      chrome.notifications.create(null, {
          type: 'basic',
          iconUrl: 'icon.png',
          title: '这是标题',
          message: '您刚才点击了自定义右键菜单!'
      });
    }
});
image.png
实战1:(platform项目险企报价)

为了实现险企账号的管理,使得出单人员在各个险企报价时,不需要自己记录管理险企的登录地址,以后手动输入账号密码。
或者在报价页面插入一些已知的报价填写信息。

文件目录

项目文件目录
  • manifest.json 清单文件
    具体配置:
image.png
  • popup.html :根据上面的描述我们知道,这是点击浏览器按钮会弹出显示。
  • icon.png :browser_action里面配置的icon显示图标
  • ideacome.js: 除了本脚本之外,其他的脚本都是依赖。
    在这个脚本里面我们可以访问其插入的页面的dom元素,cookie,localStorage,发送ajax请求等等操作。来完成我们想要的效果。

完成上面的工作,我们如何使自己的代码转换成一个可在ChromeL浏览器里面安装扩展程序呢?

接下来我们需要对我们编写的文件进行打包。应用打包为已签名的 ZIP 文件,文件扩展名为“crx”,如:myextension.crx。

当您为应用打包时,应用将获得唯一的密钥对,应用的标识符基于公钥的散列,私有密钥用来为每一个版本的应用签名,必须严格保护,不能由公众访问。注意千万不要将您的私有密钥包含在应用中!

打包步骤:

  • 进入以下URL,打开应用管理页面:
    chrome://extensions
  • 确保右上角的开发者模式复选框已选中。
  • 单击打包应用按钮,出现一个对话框。
  • 在应用根目录字段中,指定应用所在文件夹的路径,例如,C:\myext。(忽略其他字段,您第一次为一个应用打包时不需要指定私有密钥文件。)
  • 单击打包应用。打包程序将创建两个文件:一个 .crx 文件,是实际的可安装的应用;另一个是 .pem 文件,包含私有密钥。

安装图片:

确保已经勾选开发者模式
打开扩展页面
将.crx文件拖入扩展页面
点击添加扩展程序即可

不要丢失私有密钥!确保 .pem 文件保密,并存放在安全的地方。如果您今后需要做如下事情,您需要这一文件:

  • 更新应用: 更新应用与安装步骤一样。不同的是此时密钥已经存在。

学习参考文件

  1. https://chajian.baidu.com/developer/extensions/api_index.html

  2. http://open.chrome.360.cn/extension_dev/samples.html#a1f7cf79dd555b04fa8d603247a040e644996293

  3. https://developer.chrome.com/extensions/runtime

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • chrome扩展开发入门教程 最近在开发chrome插件,看到一篇非常适合入门的教程,特记录一下 注:转载 本文首...
    谢大见阅读 6,419评论 1 25
  • Chrome扩展开发 标签(空格分隔): Chrome扩展 1、写在前面 Chrome插件是一个用Web技术开发...
    记忆的时间差阅读 6,027评论 0 15
  • 架构 总括:Manifest:程序清单Background:插件运行环境/主程序Pop up:弹出页面Conten...
    程序员小逗逼阅读 10,342评论 2 18
  • 这似乎是个忧桑的故事,因为它从未开始,所以我找不到理由让它善终,暗恋终究还是施动者最伤神啊!我的青春平凡平淡,没有...
    璐璐璐锅阅读 250评论 4 1
  • 《费马大定理》你有伟大的目标,有强大的目的性,但是你没有兴趣,你将一事无成。
    妙语天成阅读 213评论 0 0