引言
从这篇文章开始,我们通过一个小项目来实践键盘和输入法相关的开发要点。这是一个在线输入法(Online IME)工具,功能类似 Google 提供的一个在线输入工具[1]。有了这类工具,你可以在 Web 页面里面直接使用输入法输入,而不依赖本地设备是否安装输入法。完整代码可以访问这里[2]。
Google Input Tools
功能与技术点
这个在线输入法工具有以下功能点:
涉及技术点有:
键盘事件的处理
输入法事件的处理
组合键的处理
UI 控件的操作
输入法服务端的实现
这篇文章我们先实现最基本的功能,然后在下篇文章中丰富一些细节。这里提一下服务端的实现,因为我们相当于自己实现了一个输入法,因此需要一个数据库来提供输入字符到候选字词的映射。一般本地安装的输入法(比如搜狗、百度等)都会有自己的词库,而我们这个是在线的输入,因此需要通过 API 来获取相应的字词。这里我直接使用了 Google Input Tools 提供的公开 API。
基本实现
我们的实现分前后两端。前端会展示在线输入法基本的 UI 界面,包括输入的文本框、组字框和候选框。然后给文本框绑定需要的事件(比如 keydown),捕获并将用户输入的拼音发送到服务端。
服务端会调用 Google 的输入服务返回候选词并返回。从服务端拿到候选词后,前端解析结果并调整 UI 的样式(展示、移动候选框)。最后还要提交用户输入的结果到文本框完成输入。
前端实现
首先我们需要一个 textarea 来测试输入。为了模拟本地输入法的 UI,我们还需要一个组字框(ID 是ime-buffer
,用于容纳拼音),和一个候选框(ID 是candidate-contaier
,用于容纳候选字词列表):
<div id ="app-container">
<textarea id="input-area"></textarea>
<div id="ime-container" class="ime-container">
<span id="ime-buffer" contenteditable="true" class="ime-buffer" spellcheck="false" tabindex="0"></span>
</div>
<div id="candidate-contaier"></div>
</div>
复制代码
这里我用了一个可编辑(contenteditable=true
)的 span,当然你也可以直接用 input 等控件。接着我们在 textarea 上绑定一些键盘事件,用于监听用户的输入:
...
inputArea.addEventListener('keydown', evt => {
console.log("keydown: ", evt);
currentCursorPos = getCaretCoordinates(inputArea, inputArea.selectionEnd);
console.log('Curret cursor at: ', currentCursorPos);
});
...
复制代码
这里我们还调用了一个getCaretCoordinates
用于获取当前输入光标在屏幕上的位置,这可以帮助我们移动组字框和候选列表的位置,使其用于跟随光标,获得好的用户体验。不过获取光标位置的操作容易有兼容性问题,因此我采用了一个第三方的实现,可以参考这里[3]。每次用户有输入,我们就更新一下当前光标的位置。
随后就要把输入的拼音发送给服务端处理:
const url = `/candidate?text=${text}`;
isIMEActive = true;
fetch(url).then(res => {
if (res.status === 200) {
res.json().then(data => {
console.log("Candidate data: ", data);
});
}
});
复制代码
我们通过/candidate
路由处理发送过来的text
,并将结果转换为 JSON 对象。注意这里还使用了一个isIMEActive
变量,用于记录当前 IME 是否启用的状态。因为正在使用输入法与未使用输入法时,键的表现是不同的。比如,直接输入空格键就会得到一个空格,而在输入法启用时,它会提交当前第一个候选词,并不会产生空格。
当服务器返回结果时,我们要先用数据填充候选框,并将组字框和候选框调整到光标的位置:
function setCandidates(data) {
let dataArray = JSON.parse(data);
currentCandidates = dataArray[1][0][1] || [];
let resultStr = "";
currentCandidates.forEach((candidate, index) => {
resultStr += `${index + 1}. ${candidate} `;
});
candidateWindow.innerText = resultStr;
moveCandidateWindow();
}
function moveCandidateWindow() {
...
imeContainer.style.left = imeLeft + 'px';
imeContainer.style.top = imeTop + 'px';
candidateWindow.style.left = candidateLeft + 'px';
candidateWindow.style.top = candidateTop + 'px';
}
复制代码
最后前端还要处理用户提交输入的过程,包括按数字键选择目标字词、按空格键选择第一个词和按回车键取消选择。此时应该更新 textarea 的内容,并情况所有的控件:
function endComposition(index) {
let isEnter = index === undefined ? true : false
inputArea.value += (!isEnter ? currentCandidates[index - 1] : textInput.innerText);
currentCandidates = [];
textInput.innerText = "";
textBuffer = "";
candidateWindow.innerText = "";
isIMEActive = false;
...
}
复制代码
以上就是前端的大致实现。
服务端实现
对于服务端来说,先利用 express 起一个 HTTP server,并监听一个端口:
const express = require('express');
const https = require('https');
const app = express();
app.listen(2022);
...
复制代码
然后发起对 Google Input Tool 的 API 请求即可:
...
const options = {
hostname: 'inputtools.google.com',
port: 443,
path: '/request?itc=zh-t-i0-pinyin&num=11&cp=0&cs=1&ie=utf-8&oe=utf-8&app=demopage',
method: 'GET'
};
const requestIMECandidate = function(req, res, callback) {
const text = req.query.text;
options.path += `&text=${text}`;
return https.request(options, res => {
let body = '';
res.on('data', chunk => {
body = body + chunk;
});
res.on('end',function(){
if (res.statusCode != 200) {
callback("Api call failed with response code " + res.statusCode);
} else {
callback(body);
}
});
});
};
复制代码
总结
以上实现只是覆盖了基本的功能,搭建出了基本的 UI 和事件处理框架。下一篇文章我会关注其中的一些细节,比如输入法功能键、组合键的处理,以及其他注意事项。最后的实现结果预览如下:
在线输入法Demo
参考阅读
[1] Google Input Tools
[2] Online-IME Demo
[3] Get Caret Position
系列导航
如果您对这个系列感兴趣,可以通过下面的导航找到对应文章👇🏻。
Web 键盘输入法应用开发指南(1)— 基本概念
Web 键盘输入法应用开发指南(2)— 键盘事件
Web 键盘输入法应用开发指南(3)— 输入法事件
Web 键盘输入法应用开发指南(4)— 组合键
Web 键盘输入法应用开发指南(5)— 实战技巧
Web 键盘输入法应用开发指南(6)— 开发实战(一)
评论