引言
从这篇文章开始,我们通过一个小项目来实践键盘和输入法相关的开发要点。这是一个在线输入法(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)— 开发实战(一)
评论