浏览器 extension 插件开发系列(09) -- popup以及其他前端页面的启动

前言

在本插件中,popup页面以及其他前端页面的启动都差不多。

ps: 也有一些小差别,就是Safaripopup页面和其他的前端页面获取background对象的形式不一样。浏览器 extension 插件开发系列(07) -- 获取各浏览器端的背景页

popup.html 页面为例, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!doctype html>
<head>
<meta charset="UTF-8">
<title>AirDroid</title>
<!-- build:css css/popup.min.css -->
<link rel="stylesheet" type="text/css" href="js/lib/bootstrap/css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="js/lib/bootstrap/css/bootstrap-theme.css"/>
<link rel="stylesheet" type="text/css" href="styles/css/popup.css"/>
<!-- /build -->
</head>
<body>
<!-- build:js js/libs.min.js -->
<script type="text/javascript" src="js/lib/jquery/jquery.js"></script>
<script type="text/javascript" src="js/lib/jquery/jquery.toast.js"></script>
<script type="text/javascript" src="js/lib/jquery/jquery.clipboard.js"></script>
<script type="text/javascript" src="js/lib/underscore/underscore.js"></script>
<!-- /build -->
<!-- build:js js/popup.min.js -->
<script type="text/javascript" src="js/util/tplHelper.js"></script>
<!--<script type="text/javascript" src="tpls/jst.js"></script>-->
<script type="text/javascript" src="js/web/base.js"></script>
<script type="text/javascript" src="js/web/popup.js"></script>
<script type="text/javascript" src="js/lib/bootstrap/js/bootstrap.js"></script>
<!-- /build -->
</body>

这边除了 base.jspopup.js 这个两个业务逻辑js,其他都是第三方库,因此只要看这两个js就行了。其中 popup 又是继承 base,因此先看 base 这个基类。

base.js 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
;
(function () {
//todo 这边为了兼容Firefox extension 下的前后台调用,所有在前台调用后台的函数,都必须是用defer方式来调用的
var fireFoxEvent = (self && self.port) || {};
window.BasePage = {
// 统计信息
rollingLog: [],
funCbObj: {},
extend: function (child) {
return $.extend(true, {}, this, child);
},

init: function () {
var self = this,
args = arguments;
$(document).ready(function () {
// 这时候要根据浏览器来获取背景对象
if (window.chrome) {
var bg = chrome.extension.getBackgroundPage();
self.setUpBackgroundPage(bg);
self.inited(args);
} else if (window.safari) {
// 如果是 popup这种 popover页面,那么可以直接访问后台的全局变量
if (safari.extension.globalPage) {
var gp = safari.extension.globalPage.contentWindow;
self.setUpBackgroundPage(gp);
self.inited(args);
} else {
// 其他的,像 reply 这种新建的窗体,就只能用数据传输的方式
safari.self.addEventListener('message', function(e) {
var eventName = e.name;
console.log("消息过来了,名字为:" + eventName);
if (eventName == 'page_data') {
var proxy = safari.self.tab;
e.message = _.isString(e.message) ? JSON.parse(e.message) : e.message;
self.setUpBackgroundPage(e.message, proxy.dispatchMessage);
self.inited(args);
}else{
if(self.funCbObj[eventName]){
self.funCbObj[eventName](e.message);
delete self.funCbObj[eventName];
}
}
}, false);

safari.self.tab.dispatchMessage('page_init');
}
} else {
// 这个是要传过来的事件。所有的事件都可以自己定
fireFoxEvent.on('page_data', function(data) {
// 传过来 AirDroid对象
self.setUpBackgroundPage(data.detail);
// 接下来绑定可以传递到前台的触发事件
_.each(_.values(self.EventType),function(item){
fireFoxEvent.emit("page_register_handle",{name:item});
});
self.inited(args);
});
// 触发页面初始化事件,让后台传递AirDroid数据过来
fireFoxEvent.emit('page_init');
}
});
},
// 初始化结束的处理事件
inited: function(args){
var self = this;
// 绑定事件
self.delegateEvents(self.events);
var browser = "";
if(window.chrome){
browser = "chrome";
}else if(window.safari){
browser = "safari";
}else {
browser = "firefox";
}
$("body").addClass(browser);
self.afterInit && self.afterInit.apply(self, args);
},
//设置弹出框的大小
setBodySize: function(width, height){
var self = this;
if(self.Airdroid.Util.Browser.chrome){

}else if(self.Airdroid.Util.Browser.safari){
width && (safari.extension.popovers[0].width = width);
height && (safari.extension.popovers[0].height = height);
}else {
// firefox
this.eventObj.emit("resize",{
width: width,
height: height
})
}
},
// 设置背景对象
setUpBackgroundPage: function(data, dispatcher){
var self = this;
this.Airdroid = data.Airdroid;
window.Airdroid = this.Airdroid;
// 事件对象
if(window.chrome){
this.eventObj = data;
}else if(window.safari){
this.eventObj = data;
if(dispatcher){
// 说明是非popover页面
// 同时要绑定对应的事件触发器
var addSafariTriggerHandle = function(obj){
if(obj.funListenerObjs){
_.each(obj.funListenerObjs,function(funId,name){
// 重新定义函数
obj[name] = function(){
console.log("调用函数,函数名为%s, id为 %s", name, funId);
var defer = $.Deferred();
// 加入回调队列
var deferDoneId = _.uniqueId("defer_done_" + name + "_");
var deferFailId = _.uniqueId("defer_fail_" + name + "_");
self.funCbObj[deferDoneId] = function(data){
self.log(deferDoneId);
defer.resolve(data);
};
self.funCbObj[deferFailId] = function(data){
self.log(deferDoneId);
defer.resolve(data);
};
console.log("参数为==》" + JSON.stringify(Array.prototype.slice.apply(arguments)))
// 触发函数
safari.self.tab.dispatchMessage(funId,{
arg: Array.prototype.slice.apply(arguments),
done: deferDoneId,
fail: deferFailId
});
return defer;
};
})
}
_.each(obj,function(value,key){
if(_.isObject(value)){
addSafariTriggerHandle(value);
}
})
};
addSafariTriggerHandle(this.Airdroid);
}
}else{
// firefox 事件触发器
this.eventObj = fireFoxEvent;
// 同时要绑定对应的事件触发器
var addFireFoxTriggerHandle = function(obj){
if(obj.funListenerObjs){
_.each(obj.funListenerObjs,function(funId,name){
// 重新定义函数
// 这时候,要把组件的监听对象,传到page页面去
self.eventObj.emit('page_bind_handle',funId);
obj[name] = function(){
var defer = $.Deferred();
var deferDoneId = _.uniqueId("defer_done_" + name + "_");
var deferFailId = _.uniqueId("defer_fail_" + name + "_");
var deferAlwayId = _.uniqueId("defer_alway_" + name + "_");
// 一次性监听
self.eventObj.once(deferDoneId,function(data){
//self.log(deferDoneId);
defer.resolve(data.detail);
});
self.eventObj.once(deferFailId,function(data){
//self.log(deferFailId);
defer.reject(data.detail);
});
self.eventObj.once(deferAlwayId,function(data){
//self.log(deferAlwayId);
defer.reject(data.detail);
});

// 触发函数
self.eventObj.emit(funId,{
arg: Array.prototype.slice.apply(arguments),
done: deferDoneId,
fail: deferFailId,
always: deferAlwayId
});
return defer;
};
})
}
_.each(obj,function(value,key){
if(_.isObject(value)){
addFireFoxTriggerHandle(value);
}
})
};
addFireFoxTriggerHandle(this.Airdroid);
}
this.util= this.Airdroid.Util;
this.server = this.Airdroid.Server;
this.account = this.Airdroid.Account;
this.EventType = this.Airdroid.Event.TYPE;
},
// 事件处理
addListener: function(name, fun){
var self = this;
if(this.eventObj){
if(window.chrome || window.safari){
this.eventObj.addEventListener(name, function (event) {
_.isFunction(fun) && fun(event.detail);
},false);
}else{
// firefox
this.eventObj.on(name, function(event){
_.isFunction(fun) && fun(event.detail);
})
}
}
},
// 统计记录
log: function(message){
var self = this;
var line;
if (message instanceof Object || message instanceof Array) {
line = message;
} else {
line = new Date().toLocaleString() + ' - ' + message;
}

console.log(line);
self.rollingLog.push(JSON.stringify(line));

if (self.rollingLog.length > 200) {
self.rollingLog.shift();
}
// 输出来
// var rollingDom = $("#rollingOutput");
// if(rollingDom.length == 0){
// $("body").append(rollingDom = $("<div id='rollingOutput'></div>"));
// }
// rollingDom.html(rollingDom.html() + "<br>" + message);
},
// 绑定事件
delegateEvents: function (events) {
var $parent, method, matches, eventName, selector;
events = events || {};
$parent = $('body');
for (var key in events) {
if (events.hasOwnProperty(key)) {
method = this[events[key]];
if (!method) throw new Error('Event "' + events[key] + '" does not exist');
matches = key.match(/^(\S+)\s*(.*)$/);
eventName = matches[1];
selector = matches[2];
method = _.bind(method, this);
if (selector === '') {
$parent.bind(eventName, method);
} else {
$parent.delegate(selector, eventName, method);
}
}
}
}
};
})();

其实base主要就做两件事:

  1. 获取背景页的对象
  2. 建立前端与背景页相互通信的事件驱动

1.获取背景页的对象。

获取各自浏览器的背景页对象(以下伪代码) 浏览器 extension 插件开发系列(07) -- 获取各浏览器端的背景页

1
2
3
var bg = getBrowserBackgroundPage();  // 这时候就可以得到Airdroid这个对象
setUpBackgroundPage(bg); // 将获得的这个背景页的Airdroid对象赋给当前前端页面的Airdroid变量,还有一些其他的,比如server,account之类的
inited(args) // base初始化结束,轮到popup初始化

第一个步骤在之前的章节中有说过,这边就不提了。这边主要讲第二个步骤,即setUpBackgroundPage
在这个步骤中,除了将背景页的Airdroid对象赋给前端的Airdroid对象之后。更要对事件驱动的对象进行设置。即 eventObj

说到这个对象 eventObj,就必须得说道base要做的第二个事情,就是前端和背景页相互通信的事件驱动。

说是相互通信,但是为了更好的兼容各个浏览器,目前全部采用前端监听的方式, 由背景页来触发这个事件。 即 addListener 这个方法。比如监听用户登入,登出,消息过来之类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 手机通知推送过来
self.addListener(self.EventType.notification_change, function (data) {
...
});

self.addListener(self.EventType.push_change, function (data) {
...
});

self.addListener(self.EventType.signed_in, function (data) {
// 如果不是在前端登的,比如后台自动登录,那么要切换页面
});

self.addListener(self.EventType.signed_out, function (data) {
// 如果在其他地方登录,就重新刷新
});

addListener 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addListener: function(name, fun){
var self = this;
if(this.eventObj){
if(window.chrome || window.safari){
this.eventObj.addEventListener(name, function (event) {
_.isFunction(fun) && fun(event.detail);
},false);
}else{
// firefox
this.eventObj.on(name, function(event){
_.isFunction(fun) && fun(event.detail);
})
}
}
},

代码还是很好理解的, 就是对于ChromeSafari,就采用 addEventListener的方式来监听。如果是Firefox的话,就采用on的方式来监听。

那么问题来了,eventObj这个对象是怎么来的,为什么会有两种不同的方法?? 其实这个对象eventObj就是在setUpBackgroundPage这个方法里面设置的,然后这边会根据不同的浏览器来处理:

  1. Chrome 代码如下:
    1
    this.eventObj = data;

代码最简单,直接将background的对象赋给eventObj就行了。

因为在前端页面用backgroundwindow对象进行addEventListener进行监听的话,如果是在背景页进行window.dispatchEvent进行触发的话,是可以生效的。

  1. Safari 代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    this.eventObj = data;
    if(dispatcher){
    // 说明是非popover页面
    // 同时要绑定对应的事件触发器
    var addSafariTriggerHandle = function(obj){
    if(obj.funListenerObjs){
    _.each(obj.funListenerObjs,function(funId,name){
    // 重新定义函数
    obj[name] = function(){
    console.log("调用函数,函数名为%s, id为 %s", name, funId);
    var defer = $.Deferred();
    // 加入回调队列
    var deferDoneId = _.uniqueId("defer_done_" + name + "_");
    var deferFailId = _.uniqueId("defer_fail_" + name + "_");
    self.funCbObj[deferDoneId] = function(data){
    self.log(deferDoneId);
    defer.resolve(data);
    };
    self.funCbObj[deferFailId] = function(data){
    self.log(deferDoneId);
    defer.resolve(data);
    };
    console.log("参数为==》" + JSON.stringify(Array.prototype.slice.apply(arguments)))
    // 触发函数
    safari.self.tab.dispatchMessage(funId,{
    arg: Array.prototype.slice.apply(arguments),
    done: deferDoneId,
    fail: deferFailId
    });
    return defer;
    };
    })
    }
    _.each(obj,function(value,key){
    if(_.isObject(value)){
    addSafariTriggerHandle(value);
    }
    })
    };
    addSafariTriggerHandle(this.Airdroid);
    }

如果是popup页面的话,那就跟Chrome一样,直接 eventObj=data。 如果是其他的页面,即dispatcher是有值的。那么就要绑定对应的触发器,这个触发器是跟backgroundevents事件模型相挂钩的,后面讲到事件模型的时候,会讲到。

  1. Firefox 代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // firefox 事件触发器
    this.eventObj = fireFoxEvent;
    // 同时要绑定对应的事件触发器
    var addFireFoxTriggerHandle = function(obj){
    if(obj.funListenerObjs){
    _.each(obj.funListenerObjs,function(funId,name){
    // 重新定义函数
    // 这时候,要把组件的监听对象,传到page页面去
    self.eventObj.emit('page_bind_handle',funId);
    obj[name] = function(){
    var defer = $.Deferred();
    var deferDoneId = _.uniqueId("defer_done_" + name + "_");
    var deferFailId = _.uniqueId("defer_fail_" + name + "_");
    var deferAlwayId = _.uniqueId("defer_alway_" + name + "_");
    // 一次性监听
    self.eventObj.once(deferDoneId,function(data){
    //self.log(deferDoneId);
    defer.resolve(data.detail);
    });
    self.eventObj.once(deferFailId,function(data){
    //self.log(deferFailId);
    defer.reject(data.detail);
    });
    self.eventObj.once(deferAlwayId,function(data){
    //self.log(deferAlwayId);
    defer.reject(data.detail);
    });

    // 触发函数
    self.eventObj.emit(funId,{
    arg: Array.prototype.slice.apply(arguments),
    done: deferDoneId,
    fail: deferFailId,
    always: deferAlwayId
    });
    return defer;
    };
    })
    }
    _.each(obj,function(value,key){
    if(_.isObject(value)){
    addFireFoxTriggerHandle(value);
    }
    })
    };
    addFireFoxTriggerHandle(this.Airdroid);

Firefox的处理方式跟 Safari 不是 popup 页面的其他页面很像。也是跟backgroundevents模型有关系,后面一起讲。

2.建立前端与背景页相互通信的事件驱动

具体查看: 浏览器 extension 插件开发系列(10) -- 事件驱动模型


系列文章:
浏览器 extension 插件开发系列(01) -- 前言和确认需求
浏览器 extension 插件开发系列(02) -- Chrome 插件的启动以及调试
浏览器 extension 插件开发系列(03) -- Firefox 插件的启动以及调试
浏览器 extension 插件开发系列(04) -- Safari 插件的添加以及调试
浏览器 extension 插件开发系列(05) -- Safari 插件申请开发者证书
浏览器 extension 插件开发系列(06) -- 各浏览器导航栏按钮的配置的点击出现的panel
浏览器 extension 插件开发系列(07) -- 获取各浏览器端的背景页
浏览器 extension 插件开发系列(08) -- 背景页启动和登录持久化
浏览器 extension 插件开发系列(09) -- popup以及其他前端页面的启动
浏览器 extension 插件开发系列(10) -- 事件驱动模型
浏览器 extension 插件开发系列(11) -- 登录模块(包括第三方登录和弹框)
浏览器 extension 插件开发系列(12) -- 实现右键菜单推送消息
浏览器 extension 插件开发系列(13) -- 实现消息过来出现桌面通知
浏览器 extension 插件开发系列(14) -- 点击reply出现回复小窗口
浏览器 extension 插件开发系列(15) -- chrome多文件上传(拖拽上传或者点击上传)
浏览器 extension 插件开发系列(16) -- Firefox 遇到的问题
浏览器 extension 插件开发系列(17) -- Safari 遇到的问题