摘要:关于我的博客掘金专栏路易斯专栏原文链接扩展开发定制请求响应头域本文共字,阅读需分钟。那么,我会放弃吗反向代理显然不会,既然问题出在上,我去掉就行了。然而无论多少次的学习和模仿,最终的目的还是为了使用,故开发一款定制请求的势在必行。
本文首发于《程序员》杂志2017年第9、10、11期,下面的版本又经过进一步的修订。
关于Github:IHeader
我的博客:louis blog
掘金专栏:路易斯专栏
原文链接:【Chrome扩展开发】定制HTTP请求响应头域
本文共15k字,阅读需15分钟。
导读搜索是程序员的灵魂,为了提升搜索的效率,以便更快的查询信息,我试着同时搜索4个网站,分别是百度、Google、维基、Bing。一个可行的做法就是网页中嵌入4个iframe,通过js拼接前面4个搜索引擎的Search URL并依次在iframe中加载。这个构思丝毫没有问题,简单粗暴。然而就是这么简单的功能,也无法实现。由于Google网站在HTML的response header中添加了X-Frame-Options字段以防止网页被Frame(这项设置常被用来防止Click Cheats),因此我无法将Google Search加入到iframe中来。那么,我会放弃Google吗?
Nginx反向代理Google显然不会,既然问题出在X-Frame-Options上,我去掉就行了。对于请求或响应头域定制,nginx是个不错的选择,其第三方的ngx_headers_more模块就特别擅长这种处理。由于nginx无法动态加载第三方模块,我动态编译了nginx以便加入ngx_headers_more模块。至此,第一步完成,以下是nginx的部分配置。
location / { more_clear_headers "X-Frame-Options"; }
为了让www.google.com正常访问,我需要使用另外一个域名比如louis.google.com。通过nginx,让louis.google.com转发到www.google.com,转发的同时去掉响应头域中的X-Frame-Options字段。于是nginx配置看起来像这样:
server { listen 80; server_name louis.google.com; location / { proxy_pass https://www.google.com/; more_clear_headers "X-Frame-Options"; } }
以上的配置有什么问题吗?且不说http直接转https的问题,即使能转发,实际上由于Google的安全策略限制,我们也访问不了Google首页!
最终我使用了一个Nginx Google代理模块ngx_http_google_filter_module),nginx配置如下:
server { listen 80; server_name louis.google.com; resolver 192.168.1.1; # 需要设置为当前路由的网关 location / { google on; google_robots_allow on; more_clear_headers "X-Frame-Options"; } }
以上,通过实现一个Google网站的反向代理,代理的同时去掉了响应头域中的X-Frame-Options字段。至此,nginx方案完结。
nginx方案有一个明显的缺陷是,配置中resolver对应的网关IP192.168.1.1是随着路由器的改变而改变的,家里和公司就是两个不同的网关(更别说去星巴克了办公了),因此经常需要手动去修改网关然后重启nginx。
IHeader缘起nginx方案的这个缺陷多少有些麻烦,恰好Chrome Extension可以定制headers,为了解决这个问题,我便尝试开发Chrome Extension。(使用Chrome以来,我下载试用过无数的Chrome Extension。每每看到一款优秀的Extension,都要激动好久,总有一种相见恨晚的感觉。Extension以其强大的定制能力,神奇的运行机制征服了无数的开发者,我也不例外。然而无论多少次的学习和模仿,最终的目的还是为了使用,故开发一款定制请求的Extension势在必行。)由于Chrome浏览器与网页的天然联系,使用Chrome Extension的方式去掉响应头域字段,比其它方案要更加简单高效。
要知道,Chrome Extension提供的API中有chrome.webRequest.onHeadersReceived。它能够添加对响应头的监听并同步修改响应头域,去掉X-Frame-Options似乎是小case。
于是新建项目,取名IHeader。目录结构如下:
其中,_locales是国际化配置,目前IHeader支持中文和英文两种语言。
res是资源目录,index.html是extension的首页,options.html是选项页面。
manifest.json是extension的声明配置(总入口),在这里配置extension的名称、版本号、图标、快捷键、资源路径以及权限等。
manifest.json贴出来如下:
{ "name": "IHeader", // 扩展名称 "version": "1.1.0", // 扩展版本号 "icons": { // 上传到chrome webstore需要32px、64px、128px边长的方形图标 "128": "res/images/lightning_green128.png", "32": "res/images/lightning_green.png", "64": "res/images/lightning_green64.png" }, "page_action": { // 扩展的一种类型,说明这是页面级的扩展 "default_title": "IHeader", // 默认名称 "default_icon": "res/images/lightning_default.png", // 默认图标 "default_popup": "res/index.html" // 点击时弹出的页面路径 }, "background": { // 扩展在后台运行的脚本 "persistent": true, // 由于后台脚本需要持续运行,需要设置为true,反之扩展不活动时可能被浏览器关闭 "scripts": ["res/js/message.js", "res/js/background.js"] // 指定运行的脚本,实际上Chrome会启用一个匿名的html去引用这些js脚本。等同于"pages":["background.html"]这种方式(注意这两种互斥,同时设置时,后一种有效) }, "commands": { // 指定快捷键 "toggle_status": { // 快捷命令的名称 "suggested_key": { // 快捷命令的热键 "default": "Alt+H", "windows": "Alt+H", "mac": "Alt+H", "chromeos": "Alt+H", "linux": "Alt+H" }, "description": "Toggle IHeader" // 描述 } }, "content_scripts": [ // 随着每个页面加载的内容脚本,通过它可以访问到页面的DOM { "all_frames": false, // frame中不加载 "matches": ["u003Call_urls>"], // 匹配所有URL "js": ["res/js/message.js", "res/js/content.js"] // 内容脚本的路径 } ], "default_locale": "en", // 默认语言 "description": "__MSG_description__", // 扩展描述 "manifest_version": 2, // Chrome 18及更高版本中,应该指定为2,低于v18版本的Chrome浏览器可以指定为1或不指定 "minimum_chrome_version": "26.0", // 最低支持到v26版本,主要受制于chrome.runtime api "options_page": "res/options.html", // 选项页面的路径 "permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"] // 扩展需要的权限 }Chrome Extension简介
开始开发之前,我们先来刷一波基础知识。
Chrome官方明确规定了插件、扩展和应用的区别:
插件(Plugin)是通过调用 Webkit 内核 NPAPI 来扩展内核功能的一种组件,工作在内核层面,理论上可以用任何一种生成本地二进制程序的语言开发,比如 C/C++、Java 等。插件重点在于接入浏览器底层,拥有更多的权限,可调用系统API,因此插件一般都不能跨系统。比如说最近Adobe宣布放弃的Flash,下载资源的迅雷以及网上付款的网银,它们都提供了Chrome插件,用以在特定场景启用并运行,从而实现丰富的功能。
扩展(Extension)是通过调用 Chrome 提供的 Chrome API 来扩展浏览器功能的一种组件,它完全基于Chrome浏览器,借助HTML,CSS,JS等web技术实现功能,是Chrome提供的一种可开发的扩展技术。比如说今年横空出世的微信小程序,它就是微信提供的一种扩展技术。相对于插件而言,扩展程序拥有有限的权限和API,对底层系统不感知,从而具有良好的跨平台特性。注意插件和扩展都只有在Chrome启动后才会运行。
应用(Application)同样是用于扩充Chrome浏览器功能。它与扩展的区别就在于,它拥有独立运行的用户界面,并且Chrome未启动时也能独立调用,就像一个独立的App一样。
不注意区分的话,我们讲到Chrome插件,往往指的就是以上三者之一。为了避免引起误解,本篇将严格区分概念,避免使用插件这种含糊的说法。
如何安装扩展开发扩展,首先得从安装开始,从Chrome 21起,Chrome浏览器就增加了对扩展安装的限制,默认只允许从 Chrome Web Store (Chrome 网上应用店)安装扩展和应用,这意味着用户一般只能安装Chrome Web Store内的扩展和应用。
如果你拖动一个crx安装文件到Chrome浏览器的任何一个普通网页,将会出现如下提示。
点击继续按钮,则会在浏览器左上角弹出如下警告。
如果你恰好在Github上发现一个不错的Chrome扩展程序,而Chrome Web Store中没有。是不是就没有办法安装呢?当然不是的,Chrome浏览器还有三种其它的方式可以加载扩展程序。
如果是扩展程序源码目录,点击chrome://extensions/页面的加载已解压的扩展程序按钮就可以直接安装。
如果是crx安装文件,直接拖动至chrome://extensions/页面即可安装。安装过程如下所示:
1) 拖放安装
2)点击添加扩展程序
3)添加好的扩展如下所示。
启动Chrome时添加参数--enable-easy-off-store-extension-install ,用以开启简单的扩展安装模式,然后就能像之前一样随意拖动crx文件到浏览器页面进行安装。
说到安装,自然有人会问,安装了某款扩展后,怎么查看该扩展的源码呢?Mac系统的用户请记住这个目录~/Library/Application Support/Google/Chrome/Default/Extensions/(windows的扩展目录暂无)。
扩展打包和更新另外,中间的打包扩展程序按钮用于将本地开发的扩展程序打包成crx包,首次打包还会生成秘钥文件(如IHeader.pem),如下所示。
打包好的扩展程序,可以发送给其他人安装,或发布到Chrome Web Store(开发者注册费用为5$)。
右边的立即更新扩展程序按钮则用于更新扩展。
扩展的基本组成通常一个Chrome扩展包含如下资源或目录:
manifest.json入口配置文件(1个,位于根目录)
js文件(至少1个,位于根目录或子级目录)
32px、64px、128px的方形icon各1个(位于根目录或子级目录)
_locales目录, 用于提供国际化支持(可选,位于根目录)
popup.html 弹出页面(可选,位于根目录或子级目录)
background.html 后台运行的页面,主要用于引入多个后台运行的js(可选,位于根目录或子级目录)
options.html 选项页面,用于扩展的设置(可选,位于根目录或子级目录)
为了方便管理,个人倾向于将HTML、JS、CSS,ICON等资源分类统一到同一个目录。
扩展的分类从使用场景上看,Chrome扩展可分为以下三类:
1)Browser Action,浏览器扩展,可通过manifest.json中的browser_action属性设置,如下所示。
"browser_action": { "default_title": "Qrcode", "default_icon": "images/icon.png", "default_popup": "index.html" // 可选的 },
以上是URL生成二维码的Browser Action扩展,运行如下所示:
该类扩展特点:全局扩展,icon长期占据浏览器右上角工具栏,每个页面均可用。
2)Page Action,页面级扩展,可通过manifest.json中的page_action属性设置,如下所示。
"page_action": { "default_title": "IHeader", "default_icon": "res/images/lightning_default.png", "default_popup": "res/index.html" // 可选的 },
以上是本篇将要讲解的Page Action的扩展——IHeader,它被指定为所有页面可见,其icon状态切换如下所示。
该类扩展特点:不同页面可以拥有不同的状态和不同的icon,icon在指定的页面可见,可见时位于浏览器右上角工具栏。
由上可见,Browser Action与Page Action功能上非常相似,配置上各自的内部属性也完全一致,它们不仅可以配置点击时弹出的页面,同时还可以绑定点击事件,如下所示。
// 以下事件绑定一般在background.js中运行 // Browser Action chrome.browserAction.onClicked.addListener(function(tab) { console.log(tab.id, tab.url); chrome.tabs.executeScript(tab.id, {file: "content.js"}); }); // Page Action chrome.pageAction.onClicked.addListener(function(tab) { console.log(tab.id, tab.url); });
如果非要说两者的差别,开发中能够感受到的就是:前者不需要维护icon状态,后者需要针对每个启用的页面管理不同的icon状态。
3)Omnibox,全能工具条,可通过manifest.json中的omnibox属性设置,如下所示。
"omnibox": { "keyword": "mdn-" //URL地址栏输入关键字"mdn-"+空格后,就会触发Omnibox },
以上是MDN网站快捷查询的Omnibox扩展,运行如下所示:
很明显,你可以对地址栏的各种输入做定制,Chrome的URL地址栏只所以强大,omnibox可谓功不可没。
该类扩展特点:运行在URL地址栏,无弹出界面,用户在输入时,扩展就可以显示建议或者自动完成一些工作。
以上三类决定了扩展如何在浏览器中运行。除此之外,每个扩展程序还可以任意搭载如下页面或脚本。
Background Page,后台页面,可通过manifest.json中的background属性设置,里面再细分script或page,分别表示脚本和页面,如下所示。
"background": { "persistent": true, //默认为false,指定为true时将在后台持续运行 "scripts": ["res/js/background.js"] // 指定后台运行的js // "page": ["res/background.html"] // 指定后台运行的html,html中需引入若干个js,没有用户界面,实际上就相当于引入多个js脚本 },
Background Page在扩展中之所以重要,主要归功于它可以使用所有的Chrome.* API。借助它popup.js 和 content.js 可以随时进行消息通信,并且调用它们原本无法调用的API。
根据persistent值是否为true,Background Page可分为两类:① Persistent Background Pages,② Event Pages。前者持续运行,随时可访问;后者只有在事件触发时才能访问。
该页面特点:运行在浏览器后台,无用户界面,后台页面可用于页面间消息通信以及后台监控,一旦浏览器启动,后台页面就会自动运行。
Content Script,内容脚本,可通过manifest.json中的content_scripts属性设置,如下所示。
"content_scripts": [ { "all_frames": true, // 默认为false,指定为true意味着frame中也加载内容脚本 "matches": ["u003Call_urls>"], // 匹配所有URL,意味着任何页面都会加载 "js": ["res/js/content.js"], // 指定运行的内容脚本 "run_at": "document_end" // 页面加载完成后执行 } ],
除了配置之外,内容脚本还可以通过js的方式动态载入。
// 动态载入js文件 chrome.tabs.executeScript(tabId, {file: "res/js/content.js"}); // 动态载入js语句 chrome.tabs.executeScript(tabId, {code: "alert("Hello Extension!")"});
该脚本特点:每个页面在加载时都会加载内容脚本,加载时机可以指定为document_start、idel或end(分别为页面DOM加载开始时,空闲时及完成后);内容脚本是唯一可以访问页面DOM的脚本,通过它可以操作页面的DOM节点,从而影响视觉呈现;基于安全考虑,内容脚本被设计成与页面其他的JS存在于两个不同的沙盒,因此无法互相访问各自的全局变量。
Option Html,设置页面,可通过manifest.json中的options_page属性设置,如下所示。
"options_page": "res/options.html",
该页面特点:点击扩展程序icon的右键菜单上【选项】按钮进入到设置页面,该页面一般用于扩展的选项设置。
Override Html,替换新建标签页的空白页面,可通过manifest.json中的chrome_url_overrides属性设置,如下所示。
"chrome_url_overrides":{ "newtab": "blank.html" },
该页面特点:常用于替换浏览器默认的空白标签页内容,多见于新开标签页时的壁纸程序,基于它你完全可以打造一个属于自己的空白页。
Devtool Page,开发者页面,可通过manifest.json中的devtools_page属性设置,如下所示。
"devtools_page": "debug.html",
该页面特点:随着控制台打开而启动,可用于将扩展收到的消息输出到当前控制台。
总之,对于Chrome扩展而言,Browser Action、Page Action 或 Omnibox之间是互斥的,其它情况下它并不限制你需要添加哪些页面或脚本,只要你愿意,就可以随意组合。
扩展如何运行调试只要你会写js,就可以开发Chrome扩展程序了。涉及到开发,调试是不可避免的,Chrome扩展的调试也非常简单。我们都知道Chrome浏览器的 chrome://extensions/页面可以查看所有的Chrome扩展,不仅如此,该页面下的加载已解压的扩展程序按钮,便可以直接加载本地开发的扩展程序,如下所示。
注意:需要勾选开发者模式才会出现加载已解压的扩展程序按钮。
成功加载后的扩展跟正常安装的扩展程序,没有什么不同,接下来,我们就可以使用web技术进行调试了。
点击以上的选项或背景页按钮,将分别打开选项页面和背景页。选项页面是一个正常的html页面,按⌃+⌘+J 键打开控制台就可以调试了。背景页没有界面,打开的就是控制台。这两个页面都可以断点debug。
Browser Action 或 Page Action的扩展通常在Chrome浏览器的右上角会出现一个Icon,右键点击该Icon,点击右键菜单的审查弹出内容按钮,将会在打开弹出页面的同时打开它的控制台。这个控制台也可以直接debug。
Chrome Extension APIChrome陆续向开发者开放了大量的API。使用这些API,我们可以监听或代理网络请求,存储数据,管理标签页和Cookie,绑定快捷键、设置右键菜单,添加通知和闹钟,获取CPU、电池、内存、显示器的信息等等(还有很多没有列举出来)。具体请阅读Chrome API官方文档。请注意,使用相应的API,往往需要申请对应的权限,如IHeader申请的权限如下所示。
"permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"]
以上,IHeader依次申请了标签页、请求、请求断点、http网站,https网站,右键菜单,桌面通知的权限。
WebRequest APIChrome Extension API中,能够修改请求的,只有chrome.webRequest了。webRequest能够为请求的不同阶段添加事件监听器,这些事件监听器,可以收集请求的详细信息,甚至修改或取消请求。
事件监听器只在特定阶段触发,它们的触发顺序如下所示。(图片来自MDN)
事件监听器的含义如下所示。
onBeforeRequest,请求发送之前触发(请求的第1个事件,请求尚未创建,此时可以取消或者重定向请求)。
onBeforeSendHeaders,请求头发送之前触发(请求的第2个事件,此时可定制请求头,部分缓存等有关的请求头(Authorization、Cache-Control、Connection、Content-
Length、Host、If-Modified-Since、If-None-Match、If-Range、Partial-Data、Pragma、Proxy-
Authorization、Proxy-Connection和Transfer-Encoding)不出现在请求信息中,可以通过添加同名的key覆盖修改其值,但是不能删除)。
onSendHeaders,请求头发送之前触发(请求的第3个事件,此时只能查看请求信息,可以确认onBeforeSendHeaders事件中都修改了哪些请求头)。
onHeadersReceived,响应头收到之后触发(请求的第4个事件,此时可定制响应头,且只能修改或删除非缓存相关字段或添加字段,由于响应头允许多个同名字段同时存在,因此无法覆盖修改缓存相关的字段)。
onResponseStarted,响应内容开始传输之后触发(请求的第5个事件,此时只能查看响应信息,可以确认onHeadersReceived事件中都修改了哪些响应头)。
onCompleted,响应接受完成后触发(请求的第6个事件,此时只能查看响应信息)。
onBeforeRedirect,onHeadersReceived事件之后,请求重定向之前触发(此时只能查看响应头信息)。
onAuthRequired,onHeadersReceived事件之后,收到401或者407状态码时触发(此时可以取消请求、同步提供凭证或异步提供凭证)。
以上,凡是能够修改请求的事件监听器,都能够指定其extraInfoSpec参数数组中包含"blocking"字符串(意味着能阻塞请求并修改),反之则不行。
另外请注意,Chrome对于请求头和响应头的展示有着明确的规定,即控制台中只展示发送出去或刚接收到的字段。因此编辑后的请求字段,控制台的network栏能够正常展示;而编辑后的响应字段由于不属于刚接收到的字段,所以从控制台上就会看不到编辑的痕迹,如同没修改过一样,实际上编辑仍然有效。
事件监听器含义虽不同,但语法却一致。接下来我们就以onHeadersReceived为例,进行深入分析。
如何绑定header监听还记得我们的目标吗?想要去掉Google网站HTML响应头的X-Frame-Options字段。请看如下代码:
// 监听的回调 var callback = function(details) { var headers = details.responseHeaders; for (var i = 0; i < headers.length; ++i) { // 移除X-Frame-Options字段 if (headers[i].name === "X-Frame-Options") { headers.splice(i, 1); break; } } // 返回修改后的headers列表 return { responseHeaders: headers }; }; // 监听哪些内容 var filter = { urls: [""] }; // 额外的信息规范,可选的 var extraInfoSpec = ["blocking", "responseHeaders"]; /* 监听response headers接收事件*/ chrome.webRequest.onHeadersReceived.addListener(callback, filter, extraInfoSpec);
chrome.webRequest.onHeadersReceived.addListener表示添加一个接收响应头的监听。以上代码中的关键参数或属性,下面逐一讲解。
callback,即事件触发时的回调,该回调默认传入一个参数(details),details就是请求的详情。
filter,Object类型,限制事件回调callback触发的过滤器。filter有四个属性可以指定,分别为①urls(包含指定url的数组)、②types(请求的类型,共8种)、③tabId(标签页id)、④windowId(窗口id)。
extraInfoSpec,数组类型,指的是额外的选项列表。对于headersReceived事件而言,包含"blocking",意味着要求请求同步,基于此才可以修改响应头;包含"responseHeaders"意味着事件回调的默认参数details中将包含responseHeaders字段,该字段指向响应头列表。
既然有了添加监听的方法,自然,还会有移除监听的方法。
chrome.webRequest.onHeadersReceived.removeListener(listener);
除此之外,为了避免重复监听,还可以判断监听是否已经存在。
var bool = chrome.webRequest.onHeadersReceived.hasListener(listener);
为了保证更好的理清以上属性、方法或参数的逻辑关系,请看如下脑图:
扩展状态管理 监听器的状态管理知道了如何绑定监听器,仅仅是第一步。监听器需要在合适的时机绑定,也需要在合适的时机解绑。为了不影响Chrome的访问速度,我们只在需要的标签页创建新的监听器,因此监听器需要依赖filter来区分不同的tabId,考虑到用户可能只需要监听一部分请求类型,types的区分也是不可避免的。又由于一个Tab里不同的时间段可能会加载不同的页面,一个监听器在不同的页面下正常运行也是必须的(因此监听器的filter中不需要指定urls)。
寥寥数语,可能不足以描述出监听器状态管理的原貌,请看下图进一步帮助理解。
以上,一个请求将依次触发上述①②③④⑤五个事件回调,每个事件回调都对应着一个监听器,这些监听器分为两类(从颜色上也可看出端倪)。
②③⑤监听器的主要功能是记录,用于监听页面上每一个Request的请求头和响应头,以及请求响应时间。
①④监听器的主要功能是更新,用于增加、删除或修改指定Request的请求头和响应头。
若Chrome指定的标签页激活了IHeader扩展,②③⑤监听器就会记录当前标签页后续的指定类型的请求信息。若用户在激活了IHeader扩展的标签页更新了Request的请求头或响应头,①或④监听器就会被开启。不用担心监听器开启无限个,我准备了回收机制,单个标签页的所有监听器都会在标签页关闭或IHeader扩展取消激活后释放掉。
首先,为方便管理,先封装下监听器的代码。
/* 独立的监听器 */ var Listener = (function(){ var webRequest = chrome.webRequest; function Listener(type, filter, extraInfoSpec, callback){ this.type = type; // 事件名称 this.filter = filter; // 过滤器 this.extraInfoSpec = extraInfoSpec; // 额外的参数 this.callback = callback; // 事件回调 this.init(); } Listener.prototype.init = function(){ webRequest[this.type].addListener( // 添加一个监听器 this.callback, this.filter, this.extraInfoSpec ); return this; }; Listener.prototype.remove = function(){ webRequest[this.type].removeListener(this.callback); // 移除监听器 return this; }; Listener.prototype.reload = function(){ // 重启监听器(用于选项页面更新请求类型后重启所有已开启的监听器) this.remove().init(); return this; }; return Listener; })();
监听器封装好了,剩下的便是管理,监听器控制器基于标签页的维度统一管理标签页上所有的监听器,代码如下。
/* 监听器控制器 */ var ListenerControler = (function(){ var allListeners = {}; /* 所有的监听器控制器列表 */ function ListenerControler(tabId){ if(allListeners[tabId]){ /* 如有就返回已有的实例 */ return allListeners[tabId]; } if(!(this instanceof ListenerControler)){ /* 强制以构造器方式调用 */ return new ListenerControler(tabId); } /* 初始化变量 */ var _this = this; var filter = getFilter(tabId); // 获取当前监听的filter设置 /* 捕获requestHeaders */ var l1 = new Listener("onSendHeaders", filter, ["requestHeaders"], function(details){ _this.saveMesage("request", details); // 记录请求的头域信息 }); /* 捕获responseHeaders */ var l2 = new Listener("onResponseStarted", filter, ["responseHeaders"], function(details){ _this.saveMesage("response", details); // 记录响应的头域信息 }); /* 捕获 Completed Details */ var l3 = new Listener("onCompleted", filter, ["responseHeaders"], function(details){ _this.saveMesage("complete", details); // 记录请求完成时的时间等信息 }); allListeners[tabId] = this; // 记录当前的标签页控制器 this.tabId = tabId; this.listeners = { // 记录已开启的监听器 "onSendHeaders": l1, "onResponseStarted": l2, "onCompleted": l3 }; this.messages = {}; // 当前标签页的请求信息集合 console.log("tabId=" + tabId + " listener on"); } ListenerControler.has = function(tabId){...} // 判断是否包含指定标签页的控制器 ListenerControler.get = function(tabId){...} // 返回指定标签页的控制器 ListenerControler.getAll = function(){...} // 获取所有的标签页控制器 ListenerControler.remove = function(tabId){...} // 移除指定标签页下的所有监听器 ListenerControler.prototype.remove = function(){...} // 移除当前控制器中的所有监听器 ListenerControler.prototype.saveMesage = function(type, message){...} // 记录请求信息 return ListenerControler; })();
通过监听器控制器的统一调度,标签页中的多个监听器才能高效的工作。
实际上,还有很多工作,上述代码还没有体现出来。比方说用户在激活了IHeader扩展的标签页更新了Request的请求头或响应头,①beforeSendHeaders或④headersReceived监听器又是怎么运作的呢?这部分内容,请结合『如何绑定header监听』节点的内容理解。
Page Action图标状态管理标签页控制器的状态需要由视觉体现出来,因此Page Action图标的管理也是不可避免的。通常,默认的icon可以在manifest.json中指定。
"page_action": { "default_icon": "res/images/lightning_default.png", // 默认图标 },
icon有如下3种状态(后两种状态可以互相切换)。
默认状态,展示默认的icon。
初始状态,展示扩展初始化后的icon。
激活状态,展示扩展激活后的icon。
Chrome提供了chrome.pageAction的API供Page Action使用。目前chrome.pageAction拥有如下方法。
show,在指定的tab下展示Page Action。
hide,在指定的tab下隐藏Page Action。
setTitle,设置Page Action的标题(鼠标移动到该Page Action上时会出现设置好的标题提示)
getTitle,获取Page Action的标题。
setIcon,设置Page Action的图标。
setPopup,设置点击时弹出页面的URL。
getPopup,获取点击时弹出页面的URL。
以上,setTitle、setIcon 和 show方法比较常用。其中,show方法有两种作用,①展示icon,②更新icon,因此一般是先设置好icon的标题和路径,然后调用show展示出来(或更新)。需要注意的是,Page Action在show方法被调用之前,是不会响应点击的,所以需要在初始化工作结束之前调用show方法。千言万语不如上代码,如下。
/* 声明3种icon状态 */ var UNINIT = 0, // 扩展未初始化 INITED = 1, // 扩展已初始化,但未激活 ACTIVE = 2; // 扩展已激活 /* 处理扩展icon状态 */ var PageActionIcon = (function(){ var pageAction = chrome.pageAction, icons = {}, tips = {}; icons[INITED] = "res/images/lightning_green.png"; // 设置不同状态下的icon路径(相对于扩展根目录) icons[ACTIVE] = "res/images/lightning_red.png"; tips[INITED] = Text("iconTips"); // 其它地方有处理,Text被指向chrome.i18n.getMessage,用以读取_locales中指定语言的对应字段的文本信息 tips[ACTIVE] = Text("iconHideTips"); function PageActionIcon(tabId){ // 构造器 this.tabId = tabId; this.status = UNINIT; // 默认为未初始化状态 pageAction.show(tabId); // 展示Page Action } PageActionIcon.prototype.init = function(){...} // 初始化icon PageActionIcon.prototype.active = function(){...} // icon切换为激活状态 PageActionIcon.prototype.hide = function(){...} // 隐藏icon PageActionIcon.prototype.setIcon = function(){ // 设置icon pageAction.setIcon({ // 设置icon的路径 tabId : this.tabId, path : icons[this.status] }); pageAction.setTitle({ // 设置icon的标题 tabId : this.tabId, title : tips[this.status] }); return this; }; PageActionIcon.prototype.restore = function(){// 刷新页面后,icon之前的状态会丢失,需要手动恢复 this.setIcon(); pageAction.show(this.tabId); return this; }; return PageActionIcon; })();
icon管理的准备工作ok了,剩下的就是使用了,如下。
new PageActionIcon(this.tabId).init();标签页的状态管理
对于IHeader扩展程序,一个标签页同时包含了监听器状态和icon状态的变化。因此需要再抽象出一个标签页控制器,对两者进行统一管理,从而供外部调用。代码如下。
/* 处理标签页状态 */ var TabControler = (function(){ var tabs = {}; // 所有的标签页控制器列表 function TabControler(tabId, url){ if(tabs[tabId]){ /* 如有就返回已有的实例 */ return tabs[tabId]; } if(!(this instanceof TabControler)){ /* 强制以构造器方式调用 */ return new TabControler(tabId); } /* 初始化属性 */ tabs[tabId] = this; this.tabId = tabId; this.url = url; this.init(); } TabControler.get = function(tabId){...} // 获取指定的标签页控制器 TabControler.remove = function(tabId){ if(tabs[tabId]){ delete tabs[tabId]; // 移除指定的标签页控制器 ListenerControler.remove(tabId); // 移除指定的监听器控制器 } }; TabControler.prototype.init = function(){...} // 初始化标签页控制器 TabControler.prototype.switchActive = function(){ // 当前标签页状态切换 var icon = this.icon; if(icon){ var status = icon.status; var tabId = this.tabId; switch(status){ case ACTIVE: // 如果是激活状态,则恢复初始状态,移除监听器控制器 icon.init(); ListenerControler.remove(tabId); Message.send(tabId, "ListeningCancel"); // 通知内容脚本从而在控制台输出取消提示(后续将讲到消息通信) break; default: // 如果不是激活状态,则激活之,添加监听器控制器 icon.active(); ListenerControler(tabId); Message.send(tabId, "Listening"); // 并通知内容脚本从而在控制台输出监听提示 } } return this; }; TabControler.prototype.restore = function(){...} // 恢复标签页控制器的状态(针对页面刷新场景) TabControler.prototype.remove = function(){...} // 移除标签页控制器 return TabControler; })();
标签页控制器的抽象,有助于封装扩展的内部运行细节,方便了后续各种场景中对扩展的管理 。
标签页关闭或更新的妥善处理标签页关闭或更新时,为了避免内存泄露和运行稳定,部分数据需要释放或者同步。刚刚封装好的标签页控制器就可以用来做这件事。
首先,Tab关闭时需要释放当前标签页的控制器和监听器对象。
/* 监听tab关闭的事件 */ chrome.tabs.onRemoved.addListener(function(tabId, removeInfo){ TabControler.remove(tabId); // 释放内存,移除标签页控制器和监听器 });
其次,每次Tab在执行跳转或刷新动作时,Page Action的icon都会回到初始状态并且不可点击,此时需要恢复icon之前的状态。
/* 监听tab更新的事件、包含跳转或刷新的动作 */ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo){ if(changeInfo.status === "loading"){ // 页面处于loading时触发 TabControler(tabId).restore(); // 恢复icon状态 } });
以上,页面跳转或刷新时,changeInfo将依次经历两种状态:loading 和complete(部分页面会包含favIconUrl或title信息),如下所示。
随着状态管理的逐渐完善,那么,是时候进行消息通信了(不知道你注意到上述代码中出现的Message对象没有?它就是消息处理的对象)。
消息通信 扩展内部消息通信Chrome扩展内的各页面之间的消息通信,有如下四种方式(以下接口省略chrome前缀)。
类型 | 消息发送 | 消息接收 | 支持版本 |
---|---|---|---|
一次性消息 | extension.sendRequest | extension.onRequest | v33起废弃(早期方案) |
一次性消息 | extension.sendMessage | extension.onMessage | v20+(不建议使用) |
一次性消息 | runtime.sendMessage | runtime.onMessage | v26+(现在主流,推荐使用) |
长期连接 | runtime.connect | runtime.onConnect | v26+ |
目前以上四种方案都可以使用。其中extension.sendRequest发送的消息,只有extension.onRequest才能接收到(已废弃不建议使用,可选读Issue 9965005)。extension.sendMessage 或 runtime.sendMessage 发送的消息,虽然extension.onMessage 和 runtime.onMessage都可以接收,但是runtime api的优先触发。若多个监听同时存在,只有第一个响应才能触发消息的sendResponse回调,其他响应将被忽略,如下所述。
If multiple pages are listening for onMessage events, only the first to call sendResponse() for a particular event will succeed in sending the response. All other responses to that event will be ignored.
我们先看一次性的消息通信,它的基本规律如下所示。
图中出现了一种新的消息通信方式,即chrome.extension.getBackgroundPage,通过它能够获取background.js(后台脚本)的window对象,从而调用window下的任意全局方法。严格来说它不是消息通信,但是它完全能够胜任消息通信的工作,之所以出现在图示中,是因为它才是消息从popup.html到background.js的主流沟通方式。那么你可能会问了,为什么content.js中不具有同样的API呢?
这是因为它们的使用方式不同,各自的权限也不同。popup.html或background.js中chrome.extension对象打印如下:
content.js中chrome.extension对象打印如下:
可以看出,前者包含了全量的属性,后者只保留少量的属性。content.js中并没有chrome.extension.getBackgroundPage方法,因此content.js不能直接调用background.js中的全局方法。
回到消息通信的话题,请看消息发送和监听的简单示例,如下所示:
// 消息流:弹窗页面、选项页面 或 background.js --> content.js // 由于每个tab都可能加载内容脚本,因此需要指定tab chrome.tabs.query( // 查询tab { active: true, currentWindow: true }, // 获取当前窗口激活的标签页,即当前tab function(tabs) { // 获取的列表是包含一个tab对象的数组 chrome.tabs.sendMessage( // 向tab发送消息 tabs[0].id, // 指定tab的id { message: "Hello content.js" }, // 消息内容可以为任意对象 function(response) { // 收到响应后的回调 console.log(response); } ); } ); /* 消息流: * 1. 弹窗页面或选项页面 --> background.js * 2. background.js --> 弹窗页面或选项页面 * 3. content.js --> 弹窗页面、选项页面 或 background.js */ chrome.runtime.sendMessage({ message: "runtime-message" }, function(response) { console.log(response); }); // 可任意选用runtime或extension的onMessage方法监听消息 chrome.runtime.onMessage.addListener( // 添加消息监听 function(request, sender, sendResponse) { // 三个参数分别为①消息内容,②消息发送者,③发送响应的方法 console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension"); if (request.message === "Hello content.js"){ sendResponse({ answer: "goodbye" }); // 发送响应内容 } // return true; // 如需异步调用sendResponse方法,需要显式返回true } );
上述涉及到的API语法如下:
chrome.tabs.query(object queryInfo, function callback),查询符合条件的tab。其中,callback为查询结果的回调,默认传入tabs列表作为参数;queryInfo为标签页的描述信息,包含如下属性。
属性 | 类型 | 支持性 | 描述 |
---|---|---|---|
active | boolean | tab是否激活 | |
audible | boolean | v45+ | tab是否允许声音播放 |
autoDiscardable | boolean | v54+ | tab是否允许被丢弃 |
currentWindow | boolean | v19+ | tab是否在当前窗口中 |
discarded | boolean | v54+ | tab是否处于被丢弃状态 |
highlighted | boolean | tab是否高亮 | |
index | Number | v18+ | tab在窗口中的序号 |
muted | boolean | v45+ | tab是否静音 |
lastFocusedWindow | boolean | v19+ | tab是否位于最后选中的窗口中 |
pinned | boolean | tab是否固定 | |
status | String | tab的状态,可选值为loading或complete | |
title | String | tab中页面的标题(需要申请tabs权限) | |
url | String or Array | tab中页面的链接 | |
windowId | Number | tab所处窗口的id | |
windowType | String | tab所处窗口的类型,值包含normal、popup、panel、appordevtools |
注:丢弃的tab指的是tab内容已经从内存中卸载,但是tab未关闭。
chrome.tabs.sendMessage(integer tabId, any request, object options, function responseCallback),向指定tab下的content.js发送单次消息。其中tabId为标签页的id,request为消息内容,options参数从v41版开始支持,通过它可以指定frameId的值,以便向指定的frame发送消息,responseCallback即收到响应后的回调。
chrome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback),向扩展内或指定的其他扩展发送消息。其中extensionId为其他指定扩展的id,扩展内通信可以忽略该参数,message为消息内容,options参数从v32版开始支持,通过它可以指定includeTlsChannelId(boolean)的值,以便决定TLS通道ID是否会传递到onMessageExternal事件监听回调中,responseCallback即收到响应后的回调。
chrome.runtime.onMessage.addListener(function callback),添加单次消息通信的监听。其中callback类似function(any message, MessageSender sender, function sendResponse) {...}这种函数,message为消息内容,sender即消息发送者,sendResponse用于向消息发送者回复响应,如果需要异步发送响应,请在callback回调中return true(此时将保持消息通道不关闭直到sendResponse方法被调用)。
综上,我们选用chrome.runtime api即可完美的进行消息通信,对于v25,甚至v20以下的版本,请参考以下兼容代码。
var callback = function(message, sender, sendResponse) { // Do something }); var message = { message: "hello" }; // message if (chrome.extension.sendMessage) { // chrome20+ var runtimeOrExtension = chrome.runtime && chrome.runtime.sendMessage ? "runtime" : "extension"; chrome[runtimeOrExtension].onMessage.addListener(callback); // bind event chrome[runtimeOrExtension].sendMessage(message); // send message } else { // chrome19- chrome.extension.onRequest.addListener(callback); // bind event chrome.extension.sendRequest(message); // send message }
想必,一次性的消息通信你已经驾轻就熟了。如果是频繁的通信呢?此时,一次性的消息通信就显得有些复杂。为了满足这种频繁通信的需要,Chrome浏览器专门提供了Chrome.runtime.connect API。基于它,通信的双方就可以建立长期的连接。
长期连接基本规律如下所示:
以上,与上述一次性消息通信一样,长期连接也可以在popup.html、background.js 和 content.js三者中两两之间建立(注意:无论何时主动与content.js建立连接,都需要指定tabId)。如下是popup.html与content.js之间建立长期连接的举例?。
// popup.html 发起长期连接 chrome.tabs.query( {active: true, currentWindow: true}, // 获取当前窗口的激活tab function(tabs) { // 建立连接,如果是与background.js建立连接,应该使用chrome.runtime.connect api var port = chrome.tabs.connect( // 返回Port对象 tabs[0].id, // 指定tabId {name: "call2content.js"} // 连接名称 ); port.postMessage({ greeting: "Hello" }); // 发送消息 port.onMessage.addListener(function(msg) { // 监听消息 if (msg.say == "Hello, who"s there?") { port.postMessage({ say: "Louis" }); } else if (msg.say == "Oh, Louis, how"s it going?") { port.postMessage({ say: "It"s going well, thanks. How about you?" }); } else if (msg.say == "Not good, can you lend me five bucks?") { port.postMessage({ say: "What did you say? Inaudible? The signal was terrible" }); port.disconnect(); // 断开长期连接 } }); } ); // content.js 监听并响应长期连接 chrome.runtime.onConnect.addListener(function(port) { // 监听长期连接,默认传入Port对象 console.assert(port.name == "call2content.js"); // 筛选连接名称 console.group("Long-lived connection is established, sender:" + JSON.stringify(port.sender)); port.onMessage.addListener(function(msg) { var word; if (msg.greeting == "Hello") { word = "Hello, who"s there?"; port.postMessage({ say: word }); } else if (msg.say == "Louis") { word = "Oh, Louis, how"s it going?"; port.postMessage({ say: word }); } else if (msg.say == "It"s going well, thanks. How about you?") { word = "Not good, can you lend me five bucks?"; port.postMessage({ say: word }); } else if (msg.say == "What did you say? Inaudible? The signal was terrible") { word = "Don"t hang up!"; port.postMessage({ say: word }); } console.log(msg); console.log(word); }); port.onDisconnect.addListener(function(port) { // 监听长期连接的断开事件 console.groupEnd(); console.warn(port.name + ": The phone went dead"); }); });
控制台输出如下:
建立长期连接涉及到的API语法如下:
chrome.tabs.connect(integer tabId, object connectInfo),与content.js建立长期连接。tabId为标签页的id,connectInfo为连接的配置信息,可以指定两个属性,分别为name和frameId。name属性指定连接的名称,frameId属性指定tab中唯一的frame去建立连接。
chrome.runtime.connect(string extensionId, object connectInfo),发起长期的连接。其中extensionId为扩展的id,connectInfo为连接的配置信息,目前可以指定两个属性,分别是name和includeTlsChannelId。name属性指定连接的名称,includeTlsChannelId属性从v32版本开始支持,表示TLS通道ID是否会传递到onConnectExternal的监听器中。
chrome.runtime.onConnect.addListener(function callback),监听长期连接的建立。callback为连接建立后的事件回调,该回调默认传入Port对象,通过Port对象可进行页面间的双向通信。Port对象结构如下:
属性 | 类型 | 描述 |
---|---|---|
name | String | 连接的名称 |
disconnect | Function | 立即断开连接(已经断开的连接再次调用没有效果,连接断开后将不会收到新的消息) |
onDisconnect | Object | 断开连接时触发(可添加监听器) |
onMessage | Object | 收到消息时触发(可添加监听器) |
postMessage | Function | 发送消息 |
sender | MessageSender | 连接的发起者(该属性只会出现在连接监听器中,即onConnect 或onConnectExternal中) |
相对于扩展内部的消息通信而言,扩展间的消息通信更加简单。对于一次性消息通信,共涉及到如下两个API:
chrome.runtime.sendMessage,之前讲过,需要特别指定第一个参数extensionId,其它不变。
chrome.runtime.onMessageExternal,监听其它扩展的消息,用法与chrome.runtime.onMessage一致。
对于长期连接消息通信,共涉及到如下两个API:
chrome.runtime.connect,之前讲过,需要特别指定第一个参数extensionId,其它不变。
chrome.runtime.onConnectExternal,监听其它扩展的消息,用法与chrome.runtime.onConnect一致。
发送消息可参考如下代码:
var extensionId = "oknhphbdjjokdjbgnlaikjmfpnhnoend"; // 目标扩展id // 发起一次性消息通信 chrome.runtime.sendMessage(extensionId, { message: "hello" }, function(response) { console.log(response); }); // 发起长期连接消息通信 var port = chrome.runtime.connect(extensionId, {name: "web-page-messages"}); port.postMessage({ greeting: "Hello" }); port.onMessage.addListener(function(msg) { // 通信逻辑见『长期连接消息通信』popup.html示例代码 });
监听消息可参考如下代码:
// 监听一次性消息 chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { console.group("simple request arrived"); console.log(JSON.stringify(request)); console.log(JSON.stringify(sender)); sendResponse("bye"); }); // 监听长期连接 chrome.runtime.onConnect.addListener(function(port) { console.assert(port.name == "web-page-messages"); console.group("Long-lived connection is established, sender:" + JSON.stringify(port.sender)); port.onMessage.addListener(function(msg) { // 通信逻辑见『长期连接消息通信』content.js示例代码 }); port.onDisconnect.addListener(function(port) { console.groupEnd(); console.warn(port.name + ": The phone went dead"); }); });
控制台输出如下:
Web页面与扩展间消息通信除了扩展内部和扩展之间的通信,Web pages 也可以与扩展进行消息通信(单向)。这种通信方式与扩展间的通信非常相似,共需要如下三步便可以通信。
首先,manifest.json指定可接收页面的url规则。
"externally_connectable": { "matches": ["https://developer.chrome.com/*"] }
其次,Web pages 发送信息,比如说在 https://developer.chrome.com/... 页面控制台执行以上『扩展程序间消息通信』小节——消息发送的语句。
最后,扩展监听消息,代码同以上『扩展程序间消息通信』小节——消息监听部分。
至此,扩展程序的消息通信聊得差不多了。基于以上内容,你完全可以自行封装一个message.js,用于简化消息通信。实际上,阅读模式扩展程序就封装了一个message.js,IHeader扩展中的消息通信便基于它。
设置快捷键一般涉及到状态切换的,快捷键能有效提升使用体验。为此我也为IHeader添加了快捷键功能。
为扩展程序设置快捷键,共需要两步。
manifest.json中添加commands声明(可以指定多个命令)。
"commands": { // 命令 "toggle_status": { // 命令名称 "suggested_key": { // 指定默认的和各个平台上绑定的快捷键 "default": "Alt+H", "windows": "Alt+H", "mac": "Alt+H", "chromeos": "Alt+H", "linux": "Alt+H" }, "description": "Toggle IHeader" // 命令的描述 } },
background.js中添加命令的监听。
/* 监听快捷键 */ chrome.commands.onCommand.addListener(function(command) { if (command == "toggle_status") { // 匹配命令名称 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { // 查询当前激活tab var tab = tabs[0]; tab && TabControler(tab.id, tab.url).switchActive(); // 切换tab控制器的状态 }); } });
以上,按下Alt+H键,便可以切换IHeader扩展程序的监听状态了。
设置快捷键时,请注意Mac与Windows、linux等系统的差别,Mac既有Ctrl键又有Command键。另外,若设置的快捷键与Chrome的默认快捷键冲突,那么设置将静默失败,因此请记得绕过以下Chrome快捷键(KeyCue是查看快捷键的应用,请忽略之)。
添加右键菜单除了快捷键外,还可以为扩展程序添加右键菜单,如IHeader的右键菜单。
为扩展程序添加右键菜单,共需要三步。
申请菜单权限,需在manifest.json的permissions属性中添加"contextMenus"权限。
"permissions": ["contextMenus"]
菜单需在background.js中手动创建。
chrome.contextMenus.removeAll(); // 创建之前建议清空菜单 chrome.contextMenus.create({ // 创建右键菜单 title: "切换Header监听模式", // 指定菜单名称 id: "contextMenu-0", // 指定菜单id contexts: ["all"] // 所有地方可见 });
由于chrome.contextMenus.create(object createProperties, function callback)方法默认返回新菜单的id,因此它通过回调(第二个参数callback)来告知是否创建成功,而第一个参数createProperties则为菜单项指定配置信息。
绑定右键菜单的功能。
chrome.contextMenus.onClicked.addListener(function (menu, tab){ // 绑定点击事件 TabControler(tab.id, tab.url).switchActive(); // 切换扩展状态 });安装或更新
Chrome为扩展程序提供了丰富的API,比如说,你可以监听扩展安装或更新事件,进行一些初始化处理或给予友好的提示,如下。
/* 安装提示 */ chrome.runtime.onInstalled.addListener(function(data){ if(data.reason == "install" || data.reason == "update"){ chrome.tabs.query({}, function(tabs){ tabs.forEach(function(tab){ TabControler(tab.id).restore(); // 恢复所有tab的状态 }); }); // 初始化时重启全局监听器 ... // 动态载入Notification js文件 setTimeout(function(){ var partMessage = data.reason == "install" ? "安装成功" : "更新成功"; chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { var tab = tabs[0]; if (!/chrome:///.test(tab.url)){ // 只能在url不是"Chrome:// URL"开头的页面注入内容脚本 chrome.tabs.executeScript(tab.id, {file: "res/js/notification.js"}, function(){ chrome.tabs.executeScript(tab.id, {code: "notification("IHeader"+ partMessage +"")"}, function(log){ log[0] && console.log("[Notification]: 成功弹出通知"); }); }); } else { console.log("[Notification]: Cannot access a chrome:// URL"); } }); },1000); // 延迟1s的目的是为了调试时能够及时切换到其他的tab下,从而弹出Notification。 console.log("[扩展]:", data.reason); } });
以上,chrome.tabs.executeScript(integer tabId, object details)接口,用于动态注入内容脚本,且只能在url不是"Chrome:// URL"开头的页面注入。其中tabId参数用于指定目标标签页的id,details参数用于指定内容脚本的路径或语句,它的file属性指定脚本路径,code属性指定动态语句。若分别往同一个标签页注入多个脚本或语句,这些注入的脚本或语句处于同一个沙盒,即全局变量可以共享。
notification.js如下所示。
function notification(message) { if (!("Notification" in window)) { // 判断浏览器是否支持Notification功能 console.log("This browser does not support desktop notification"); } else if (Notification.permission === "granted") { // 判断是否授予通知的权限 new Notification(message); // 创建通知 return true; } else if (Notification.permission !== "denied") { // 首次向用户申请权限 Notification.requestPermission(function (permission) { // 申请权限 if (permission === "granted") { // 用户授予权限后, 弹出通知 new Notification(message); // 创建通知 return true; } }); } }
最终弹出通知如下。
国际化为了让全球都能使用你开发的扩展,国际化是必须的。从软件工程的角度讲,国际化就是将产品用户界面中可见的字符串全部存放在资源文件中,然后根据用户所处不同的语言环境,展示相应语言的视觉信息。Chrome从v17版本开始就提供了国际化标准A
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/89794.html
摘要:协议采用了请求响应模型。报头分为通用报头,请求报头,响应报头和实体报头。格式支持比键值对复杂得多的结构化数据,这一点也很有用。例如下面这段代码最终发送的请求是这种方案,可以方便的提交复杂的结构化数据,特别适合的接口。 一 前言 ----现在搞前端的不学好http有关的知识已经不行啦~笔者也是后知后觉,在搞node的时候意识到网络方面的薄弱,开始学起http相关知识。这一篇是非常基础的讲...
摘要:实时通讯越来越多应用于各个领域。实现原生实现对象一共支持四个消息和。是基于的实时通信库。服务器应该用包含相同数据的乓包应答客户端发送探测帧由服务器发送以响应数据包。主要用于在接收到传入连接时强制轮询周期。该间隔可通过配置修改。 随着web技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于各个领域。 HTTP是最常用的客户端与服务端的...
摘要:今天总结下与网络相关的知识,不是那么详细,但是包含了我认为重要的所有点。概要网络知识我做了个方面的总结,包括协议,协议,协议,协议,协议,,攻击,其他协议。跨域名如今被普遍用在网络中,例如等。拥塞窗口的大小又取决于网络的拥塞状况。 前言 无论是 C/S 开发还是 B/S 开发,无论是前端开发还是后台开发,网络总是无法避免的,数据如何传输,如何保证正确性和可靠性,如何提高传输效率,如何解...
阅读 1025·2021-11-22 13:53
阅读 1576·2021-11-17 09:33
阅读 2372·2021-10-14 09:43
阅读 2834·2021-09-01 11:41
阅读 2261·2021-09-01 10:44
阅读 2902·2021-08-31 09:39
阅读 1441·2019-08-30 15:44
阅读 1851·2019-08-30 13:02