Justin's Words

使用有道翻译 api 做 Chrome 扩展

花了点时间做了个 Chrome 扩展(Github),用的是有道翻译 api

Manifest

manifest.json

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
{
"manifest_version": 2,

"name": "Youdao Dict",
"description": "有道词典 Chrome 扩展",
"version": "0.0.1",

"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png",
"256": "icon.png"
},

"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"http://fanyi.youdao.com/"
],

"content_scripts": [
{
"matches": ["*://*/*"],
"all_frames": true,
"css": ["css/popover.css"],
"js": ["js/popover.js"]
}
]
}
icons

chrome://extensions/ 的图标

browser_action

在扩展栏的图标和点击后弹出的页面,没有设置这个值则不会出现在扩展栏中。

permissions

有权限访问哪些地址。

content_scripts

在特定页面下可以进行操作,操作时使用的 js 文件和 css 文件,可以使用正则表达式。

all_frames 表示是否在所有框架上生效,默认为 false,只在顶级框架生效。

js/popup.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
function translate(query, callback, errCallback) {
var url = 'http://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
var req = new XMLHttpRequest();
req.open('GET', url + encodeURIComponent(query));
req.responseType = 'json';
req.onload = function () {
var res = req.response;
callback(res);
};
req.onerror = function () {
errCallback('Network error.');
return;
};
req.send();
}

function renderResult(resultJson) {
isLoad(false);
var basic = document.getElementById('basic');
while (basic.nextElementSibling != null) {
basic.nextElementSibling.remove();
}

if (resultJson === undefined || resultJson === null || resultJson === '') {
basic.textContent = 'Nothing found.';
return;
}
if (typeof resultJson === 'string') {
basic.textContent = resultJson.toString();
return;
}
if (resultJson.basic === '' || resultJson.basic === undefined) {
basic.textContent = resultJson.translation[0];
return;
}

basic.textContent = resultJson.basic.explains[0];
var webTranslation = resultJson.web;
for (var i = 0; i < webTranslation.length; i++) {
var pEle = document.createElement('P');
pEle.className = 'web-translation';
pEle.textContent = webTranslation[i].key + ': ' + webTranslation[i].value.join(', ');
document.getElementById('resultField').appendChild(pEle);
}
}

function isLoad(isLoading) {
document.getElementById('loading').hidden = !isLoading;
document.getElementById('resultBox').hidden = isLoading;
}

function processResult(queryInput) {
var query = queryInput.value;
if (query === undefined || query === null || query === '') return;

isLoad(true);
translate(query, function (res) {
renderResult(res);
}, function (errMessage) {
renderResult(errMessage);
});
}

document.addEventListener('DOMContentLoaded', function (ev) {
var form = document.forms.namedItem('dictForm');
var queryInput = form.query;
var timeout;
queryInput.focus();

form.addEventListener('submit', function (ev) {
ev.preventDefault();
processResult(queryInput);
});

queryInput.addEventListener('keyup', function (ev) {
if (ev.keyCode === 13) return;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}

timeout = setTimeout(function () {
processResult(queryInput);
}, 700);
});
});

只解释重要部分。

异步请求翻译结果。需要对输入做 encodeURIComponent 处理,这里我指定返回的是 json,成功则触发 onload 并对 response 进行处理,失败则触发 onerror

translate(query, callback, errCarllback)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function translate(query, callback, errCallback) {
var url = 'http://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
var req = new XMLHttpRequest();
req.open('GET', url + encodeURIComponent(query));
req.responseType = 'json';
req.onload = function () {
var res = req.response;
callback(res);
};
req.onerror = function () {
errCallback('Network error.');
return;
};
req.send();
}

绑定 keyup 动作,并在 keyup 触发后 700 毫秒之内再无另一个 keyup 动作后开始执行相关函数,不过得判断下是否按下的键为 Enter,是则直接结束,否则就和上面绑定 Submit 重复了,没必要。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
queryInput.addEventListener('keyup', function (ev) {
if (ev.keyCode === 13) return;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}

timeout = setTimeout(function () {
processResult(queryInput);
}, 700);
});

Popover

这是双击翻译或 Ctrl 翻译选择区域功能。

popover.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
function translate(query, callback, errCallback) {
if (window.location.protocol.indexOf('https') < 0) {
var url = 'http://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
} else {
var url = 'https://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
}
var req = new XMLHttpRequest();
req.open('GET', url + encodeURIComponent(query));
req.responseType = 'json';
req.onload = function () {
var res = req.response;
callback(res);
};
req.onerror = function () {
errCallback('Network error.');
return;
};
req.send();
}

function getPosition(event) {
console.assert(event instanceof Event, 'Not a valid instance of Event');
event = event || window.event; // IE-ism
var eventDoc, doc, body;

// If pageX/Y aren't available and clientX/Y are,
// calculate pageX/Y - logic taken from jQuery.
// (This is to support old IE)
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;

event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0 );
}
return {
"left": event.pageX,
"top": event.pageY
};
}

function getPositionByRange() {
if (!window.getSelection()) return;

var sel, range, rects, rect, x, y;
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0).cloneRange();
if (range.getClientRects) {
range.collapse(true);
rects = range.getClientRects();
if (rects.length > 0) {
rect = range.getClientRects()[0];
}
x = rect.left;
y = rect.top;
}
}
return {
"left": x,
"top": y
}
}

function materialShow(num) {
if (num > 100) return;
console.log(num);
setTimeout(function () {
materialShow(num + 10);
}, 500);
}

function initialPop(ev) {
if (document.getElementById('cardWrapper')) {
var cardWrapper = document.getElementById('cardWrapper');
cardWrapper.parentNode.removeChild(cardWrapper);
}

//var pos = getPosition(ev);
var pos = getPositionByRange();
var html =
'<div id="cardWrapper" class="card-wrapper" style="left: ' + (pos.left - 0) + 'px; top: ' + (pos.top + window.scrollY + 20) + 'px;">' +
'<div class="card blue-grey darken-1">' +
'<div class="card-content white-text">' +
'<span class="card-title">Loading...</span>' +
'</div>' +
'</div>' +
'</div>';
var dummy = document.createElement('DIV');
dummy.innerHTML = html;
var cardNode = dummy.childNodes;
document.body.appendChild(cardNode[0]);

var cardWrapper = document.getElementById('cardWrapper');
document.addEventListener('click', function (event) {
if (cardWrapper.parentNode) {
cardWrapper.parentNode.removeChild(cardWrapper);
}
});
cardWrapper.addEventListener('click', function (event) {
event.stopPropagation();
});
}

function renderPop(resJson) {
var cardWrapper = document.getElementById('cardWrapper');
var cardTitle = cardWrapper.getElementsByClassName('card-title')[0];

if (resJson === undefined || resJson === null || resJson === '') {
cardTitle.textContent = 'Nothing found.';
return;
}
if (typeof resJson === 'string') {
cardTitle.textContent = resJson.toString();
return;
}
if (resJson.basic === '' || resJson.basic === undefined) {
cardTitle.textContent = resJson.translation[0];
return;
}

cardTitle.textContent = resJson.basic.explains[0];
var webTranslation = resJson.web;

for (var i = 0; i < webTranslation.length; i++) {
var pEle = document.createElement('P');
pEle.className = 'web-translation';
pEle.textContent = webTranslation[i].key + ': ' + webTranslation[i].value.join(', ');
cardTitle.parentNode.appendChild(pEle);
}
}

document.addEventListener('dblclick', function (ev) {
var text = window.getSelection().toString().trim();
if (text === null || text === undefined || text.length === 0) return;

initialPop(ev);
translate(text, function (res) {
renderPop(res);
}, function (errMessage) {
renderPop(errMessage);
});
});

document.addEventListener('keydown', function (ev) {
//console.log(String.fromCharCode(ev.keyCode));
if (!ev.ctrlKey) return;
var text = window.getSelection().toString().trim();
if (text === null || text === undefined || text.length === 0) return;

initialPop(ev);
translate(text, function (res) {
renderPop(res);
}, function (errMessage) {
renderPop(errMessage);
});
});

这里的异步请求得注意当前页面是否 https 协议,如果是那么异步请求地址也得是 https 协议,如下:

1
2
3
4
5
if (window.location.protocol.indexOf('https') < 0) {
var url = 'http://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
} else {
var url = 'https://fanyi.youdao.com/openapi.do?keyfrom=YoungdzeBlog&key=498418215&type=data&doctype=json&version=1.1&q=';
}

有两种方法可以获取当前坐标,不过一个是相对页面的坐标,一个是相对浏览器的坐标,由于按 Ctrl 时不能通过 Event 获取坐标,所以统一通过 Selection 获取。

通过 Event 获取:

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
function getPosition(event) {
console.assert(event instanceof Event, 'Not a valid instance of Event');
event = event || window.event; // IE-ism
var eventDoc, doc, body;

// If pageX/Y aren't available and clientX/Y are,
// calculate pageX/Y - logic taken from jQuery.
// (This is to support old IE)
if (event.pageX == null && event.clientX != null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;

event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0 );
}
return {
"left": event.pageX,
"top": event.pageY
};
}

通过 Selection Range 获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getPositionByRange() {
if (!window.getSelection()) return;

var sel, range, rects, rect, x, y;
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0).cloneRange();
if (range.getClientRects) {
range.collapse(true);
rects = range.getClientRects();
if (rects.length > 0) {
rect = range.getClientRects()[0];
}
x = rect.left;
y = rect.top;
}
}
return {
"left": x,
"top": y
}
}

将一个节点删除,可以通过 parentNode 获取其父节点再通过其父节点使用 removeChild(childNode) 删除该节点:

1
2
3
4
if (document.getElementById('cardWrapper')) {
var cardWrapper = document.getElementById('cardWrapper');
cardWrapper.parentNode.removeChild(cardWrapper);
}

需要往 body 添加节点时用 document.body.innerHTML 会使已经选择的区域失去,解决 trick:

1
2
3
4
var dummy = document.createElement('DIV');
dummy.innerHTML = html;
var cardNode = dummy.childNodes;
document.body.appendChild(cardNode[0]);

阻止冒泡:

1
2
3
4
5
6
7
8
9
var cardWrapper = document.getElementById('cardWrapper');
document.addEventListener('click', function (event) {
if (cardWrapper.parentNode) {
cardWrapper.parentNode.removeChild(cardWrapper);
}
});
cardWrapper.addEventListener('click', function (event) {
event.stopPropagation();
});

取得选择的区域文字,并判断是否为空,是则结束函数:

1
2
3
4
5
document.addEventListener('dblclick', function (ev) {
var text = window.getSelection().toString().trim();
if (text === null || text === undefined || text.length === 0) return;
...
});

判断按下的是否 Ctrl

1
if (ev.ctrlKey) {}