Electron 码上翻译从 0 到 100 拆解实现
码上翻译项目
版本管理
版本 | 发布时间 | 更新内容 | 码上翻译桌面应用下载(Windows) |
---|---|---|---|
v1.0.1 | 2022/12/05 | 1.增加安装包图标 2.增加托盘菜单 3.增加自动启动 | code-translate-1.0.1-Setup.exe |
v1.0.0 | 2022/12/03 | 1.实现选中文本按快捷键翻译 |
如何使用码上翻译
码上翻译,选中文本按快捷键即可翻译 (快捷键 Ctrl + C, Ctrl + 空格)
码上翻译原理步骤
1. 创建electron项目
2. 监听快捷键 Ctrl+空格
3. 用户选中并且拷贝要翻译的文本
4. 快捷键获取剪切板内容
5. 调用百度翻译接口接口,返回翻译内容
6. 打开窗口,展示翻译内容
安装 electron-quick-start 并启动
# Clone this repository
git clone https://github.com/electron/electron-quick-start
# Go into the repository
cd electron-quick-start
# Install dependencies
npm install
# Run the app
npm start
electron 官网例子
https://www.electronjs.org/zh/docs/latest/tutorial/quick-start
调整代码
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/>
<link href="./styles.css" rel="stylesheet" />
<title>马上翻译</title>
</head>
<body>
<div id="app">程序员李钟意 直播拆解实现码上翻译应用</div>
<script src="./renderer.js"></script>
</body>
</html>
main.js
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
frame: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
mainWindow.webContents.openDevTools();
}
electron 注册快捷键
electron 注册快捷键官网地址 https://www.electronjs.org/zh/docs/latest/api/accelerator
监听快捷键并且打开窗口
main.js
// Modules to control application life and create native browser window
const {app, BrowserWindow, globalShortcut} = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
// 如果窗口存在,那么直接展示
// https://www.electronjs.org/zh/docs/latest/api/browser-window#winisdestroyed
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
return;
}
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800, // 窗口大小宽度
height: 600, // 窗口大小高度
frame: false, // 无边框窗口
show: false, // 默认不展示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
mainWindow.webContents.openDevTools();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();
// 注册一个 'CommandOrControl+Y' 快捷键监听器.
globalShortcut.register('CommandOrControl+Space', createWindow);
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
打开窗口的时候修改位置
- 获取鼠标位置
https://www.electronjs.org/zh/docs/latest/api/screen#screengetcursorscreenpoint
- 修改窗口的位置(相对于屏幕)
https://www.electronjs.org/zh/docs/latest/api/browser-window#winsetboundsbounds-animate
let mainWindow;
function createWindow() {
// 获取鼠标在当前屏幕上的位置
const point = screen.getCursorScreenPoint();
// 如果窗口存在,那么直接展示
// https://www.electronjs.org/zh/docs/latest/api/browser-window#winisdestroyed
if (mainWindow && !mainWindow.isDestroyed()) {
console.log('point', point); // {x: number; y: number}
mainWindow.show();
mainWindow.setBounds({x: point.x, y: point.y, width: 300, height: 100});
return;
}
// Create the browser window.
mainWindow = new BrowserWindow({
width: 300, // 窗口大小宽度
height: 100, // 窗口大小高度
frame: false, // 无边框窗口
show: false, // 默认不展示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools();
}
进程间通信
main.js 是主进程
index.html 是渲染进程
preload.js 是预加载的 js
监听主进程发过来的消息
https://www.electronjs.org/zh/docs/latest/tutorial/ipc#2-通过预加载脚本暴露-ipcrendereron
preload.js
const {contextBridge, ipcRenderer} = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
});
main.js
let mainWindow;
function createWindow() {
// 如果窗口存在,那么直接展示
// https://www.electronjs.org/zh/docs/latest/api/browser-window#winisdestroyed
if (mainWindow && !mainWindow.isDestroyed()) {
console.log('point', point); // {x: number; y: number}
// 设置当前窗口展示的位置
mainWindow.setBounds({x: point.x, y: point.y, width: 800, height: 600});
// 给渲染进程发送 消息
mainWindow.webContents.send('show-translate', {data: point});
// 打开窗口
mainWindow.show();
return;
}
}
renderer.js
// https://www.electronjs.org/zh/docs/latest/tutorial/ipc#2-%E9%80%9A%E8%BF%87%E9%A2%84%E5%8A%A0%E8%BD%BD%E8%84%9A%E6%9C%AC%E6%9A%B4%E9%9C%B2-ipcrendereron
const appEL = document.getElementById('app');
window.electronAPI.onShowTranslate((_event, value) => {
console.log('value', value);
});
获取系统剪切板内容
https://www.electronjs.org/zh/docs/latest/api/clipboard
// Modules to control application life and create native browser window
const {app, BrowserWindow, globalShortcut, screen, clipboard} = require('electron');
const path = require('path');
let mainWindow;
function createWindow() {
// 获取鼠标在当前屏幕上的位置
const point = screen.getCursorScreenPoint();
// 如果窗口存在,那么直接展示
// https://www.electronjs.org/zh/docs/latest/api/browser-window#winisdestroyed
if (mainWindow && !mainWindow.isDestroyed()) {
console.log('point', point); // {x: number; y: number}
// 设置当前窗口展示的位置
mainWindow.setBounds({x: point.x, y: point.y, width: 800, height: 600});
// 获取剪切板内容
const text = clipboard.readText();
// console.log('text', text);
// 给渲染进程发送 消息
mainWindow.webContents.send('show-translate', {data: point, text});
// 打开窗口
mainWindow.show();
return;
}
}
百度翻译开放平台
百度翻译开放平台 - 文档与支持 - 接入服务
https://api.fanyi.baidu.com/doc/13
通用翻译 API 接入文档
http://api.fanyi.baidu.com/doc/21
安装 md5 和 uuid
npm i uuid md5 --save
# 创建 config.js , touch config.js (MacOS)
ni config.js
config.js 你要修改的文件,换成你的 appid 和 key
http://api.fanyi.baidu.com/manage/developer
config.js
module.exports = {
appid: '20221130001479050',
key: '8y8QWUlo3Oz5ceX3ClPI'
};
main.js
const {app, BrowserWindow, globalShortcut, screen, clipboard} = require('electron');
const path = require('path');
const {v4: uuidv4} = require('uuid');
const md5 = require('md5');
const config = require('./config.js');
let mainWindow;
function createWindow() {
// 获取鼠标在当前屏幕上的位置
const point = screen.getCursorScreenPoint();
// 如果窗口存在,那么直接展示
// https://www.electronjs.org/zh/docs/latest/api/browser-window#winisdestroyed
if (mainWindow && !mainWindow.isDestroyed()) {
console.log('point', point); // {x: number; y: number}
// 设置当前窗口展示的位置
mainWindow.setBounds({x: point.x, y: point.y, width: 800, height: 600});
// 获取剪切板内容
const text = clipboard.readText();
// console.log('text', text);
if (!text) {
console.log('text is empty', text);
return;
}
// \s匹配任何空白字符,包括空格、制表符、换页符等等
// fix 签名错误
const query = text.replace(/\s/gi, '');
const url = 'http://api.fanyi.baidu.com/api/trans/vip/translate';
const appid = config.appid; // 需要获取
const key = config.key; // 需要获取
const salt = uuidv4();
const sign = md5(`${appid}${query}${salt}${key}`);
// 给渲染进程发送 消息
mainWindow.webContents.send('show-translate', {
query,
url,
appid,
key,
salt,
sign
});
// 打开窗口
mainWindow.show();
return;
}
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800, // 窗口大小宽度
height: 600, // 窗口大小高度
frame: false, // 无边框窗口
show: false, // 默认不展示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
mainWindow.webContents.openDevTools();
}
renderer.js
// https://www.electronjs.org/zh/docs/latest/tutorial/ipc#2-%E9%80%9A%E8%BF%87%E9%A2%84%E5%8A%A0%E8%BD%BD%E8%84%9A%E6%9C%AC%E6%9A%B4%E9%9C%B2-ipcrendereron
const appEL = document.getElementById('app');
let lastQuery = ''; // 缓存上一次的查询内容
window.electronAPI.onShowTranslate((_event, value) => {
if (lastQuery === value.query) {
// 缓存上一次的查询内容
// 和现在的一样不执行代码
return;
}
console.log('value', value);
appEL.style = 'color: #141414;';
appEL.innerText = '正在翻译...';
});
接口请求实现百度翻译效果
增加 request 方法请求 百度翻译 api
// https://www.electronjs.org/zh/docs/latest/tutorial/ipc#2-%E9%80%9A%E8%BF%87%E9%A2%84%E5%8A%A0%E8%BD%BD%E8%84%9A%E6%9C%AC%E6%9A%B4%E9%9C%B2-ipcrendereron
const appEL = document.getElementById('app');
let lastQuery = ''; // 缓存上一次的查询内容
window.electronAPI.onShowTranslate((_event, value) => {
if (lastQuery === value.query) {
// 缓存上一次的查询内容
// 和现在的一样不执行代码
return;
}
console.log('value', value);
appEL.style = 'color: #141414;';
appEL.innerText = '正在翻译...';
request(value)
.then((response) => response.json())
.then((response) => {
console.log('response', response);
appEL.innerText = (response.trans_result[0] || {}).dst;
})
.catch((error) => {
appEL.innerText = error.message;
appEL.style = 'color: #f00;';
console.error('[error]', [error]);
});
});
// 通用翻译API接入文档 https://api.fanyi.baidu.com/doc/21
function request({url, appid, query, salt, sign}) {
// const url = "http://api.fanyi.baidu.com/api/trans/vip/translate";
// const appid = "20221130001479050";
// const key = "8y8QWUlo3Oz5ceX3ClPI";
// const salt = uuidv4();
// const sign = md5(`${appid}${query}${salt}${key}`);
return window.fetch(
// `${url}?q=${query}&from=en&to=zh&appid=${appid}&salt=${salt}&sign=${sign}`
`${url}?q=${query}&from=auto&to=auto&appid=${appid}&salt=${salt}&sign=${sign}`
);
}
结果报错
Refused to connect to '<URL>' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.
原因是 electron 安全策略
解决办法: 注释掉 html 的 meta 限制 http-equiv="Content-Security-Policy"
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
/> -->
<link href="./styles.css" rel="stylesheet" />
<title>马上翻译</title>
</head>
<body>
<div id="app">程序员李钟意 直播拆解实现码上翻译应用</div>
<script src="./renderer.js"></script>
</body>
</html>
百度翻译 接口报错
原因是请求接口的频率太快了
{"error_code": "54003", "error_msg": "Invalid Access Limit"}
限制接口访问,如果是相同的查询不请求接口
renderer.js
const appEL = document.getElementById('app');
let lastQuery = ''; // 缓存上一次的查询内容
window.electronAPI.onShowTranslate((_event, value) => {
if (lastQuery === value.query) {
// 缓存上一次的查询内容
// 和现在的一样不执行代码
return;
}
lastQuery = value.query;
console.log('value', value);
});
最后调整窗口和隐藏开发者工具
调整 窗口大小为 300* 100,隐藏开发者工具
let mainWindow;
function createWindow() {
if (mainWindow && !mainWindow.isDestroyed()) {
// 设置当前窗口展示的位置
mainWindow.setBounds({x: point.x, y: point.y, width: 300, height: 100});
}
// Create the browser window.
mainWindow = new BrowserWindow({
width: 300, // 窗口大小宽度
height: 100, // 窗口大小高度
frame: false, // 无边框窗口
show: false, // 默认不展示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools();
}
electron 打包
官方文档 https://www.electronjs.org/zh/docs/latest/tutorial/打包教程
yarn add --dev @electron-forge/cli
npx electron-forge import
启动项目
npm start
打包项目 免安装包
PS D:\Desktop\gitee.io\examples\code-translate> npm run package
> electron-quick-start@1.0.0 package
> electron-forge package
✔ Checking your system
✔ Preparing to package application
✔ Running packaging hooks
✔ Running generateAssets hook
✔ Running prePackage hook
✔ Packaging application
✔ Packaging for x64 on win32 [14s]
✔ Running postPackage hook
打包项目 exe
PS D:\Desktop\gitee.io\examples\code-translate> npm run make
> electron-quick-start@1.0.0 make
> electron-forge make
✔ Checking your system
✔ Loading configuration
✔ Resolving make targets
› Making for the following targets: squirrel
✔ Running package command
✔ Preparing to package application
✔ Running packaging hooks
✔ Running generateAssets hook
✔ Running prePackage hook
✔ Packaging application
✔ Packaging for x64 on win32 [15s]
✔ Running postPackage hook
✔ Running preMake hook
✔ Making distributables
✔ Making a squirrel distributable for win32/x64 [1m18s]
✔ Running postMake hook
› Artifacts available at: D:\Desktop\gitee.io\examples\code-translate\out\make
PS D:\Desktop\gitee.io\examples\code-translate>
bug 修复
失去焦点隐藏翻译窗口
main.js
let mainWindow;
function createWindow() {
// and load the index.html of the app.
mainWindow.loadFile('index.html');
mainWindow.webContents.on('blur', () => {
mainWindow.hide();
});
// Open the DevTools.
// mainWindow.webContents.openDevTools();
}
增加托盘菜单
https://electronjs.org/docs/api/tray
生成图标
electron-icon-builder https://github.com/safu9/electron-icon-builder
npm install electron-icon-builder --save-dev
./node_modules/.bin/electron-icon-builder --input=./logo.png --output=./
const {app, Menu, Tray} = require('electron');
let tray = null;
app.whenReady().then(() => {
tray = new Tray('/path/to/my/icon');
const contextMenu = Menu.buildFromTemplate([
{label: 'Item1', type: 'radio'},
{label: 'Item2', type: 'radio'},
{label: 'Item3', type: 'radio', checked: true},
{label: 'Item4', type: 'radio'}
]);
tray.setToolTip('This is my application.');
tray.setContextMenu(contextMenu);
});
增加系统托盘
移动icons
到src/icons/
src/tray/index.js
const {app, Menu, Tray} = require('electron');
const path = require('path');
let tray = null;
// 有退出按钮
module.exports = function useTray(mainWindow) {
tray = new Tray(path.join(__dirname, '../icons/win/icon.ico'));
const contextMenu = Menu.buildFromTemplate([
{
label: '重新启动',
click: () => {
// 打完包后生效
app.relaunch();
}
},
{
label: '退出',
click: () => {
// 退出的时候
// 如果还有其他行为
// 可以在这个方法里面执行
},
role: 'quit'
}
]);
tray.setToolTip('码上翻译');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWindow.show(); // 打开窗口
});
};
main.js
const useTray = require('./src/tray/index.js');
app.whenReady().then(() => {
createWindow();
// 系统托盘菜单
useTray(mainWindow);
});
bug 可以多开
单例运行
const gotTheLock = app.requestSingleInstanceLock();
// 单例运行
if (!gotTheLock) {
// 当第二个实例启动时直接退出
app.quit();
} else {
// coding
}
增加自启动(打开电脑就自动启动)
main.js
// https://www.electronjs.org/zh/docs/latest/api/app#appsetappusermodelidid-windows
app.setAppUserModelId('CodeTranslate');
// 开机自启动
app.setLoginItemSettings({
openAtLogin: true // 登录时打开应用程序, false 将应用从登录启动项中删除。 默认值为 false.
});
自定义安装图标
forge.config.js
// https://www.electronforge.io/guides/create-and-add-icons
const path = require('path');
const platform = process.platform !== 'darwin' ? 'win' : 'mac';
module.exports = {
packagerConfig: {
icon: path.join(__dirname, `./src/icons/${platform}/icon`)
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
iconUrl: path.join(__dirname, `./src/icons/${platform}/icon.ico`),
setupIcon: path.join(__dirname, `./src/icons/${platform}/icon.ico`)
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin']
},
{
name: '@electron-forge/maker-deb',
config: {
options: {
icon: path.join(__dirname, './src/icons/png/1024x1024.png')
}
}
},
{
name: '@electron-forge/maker-rpm',
config: {}
}
]
};
任务栏显示的图标
main.js
// Create the browser window.
mainWindow = new BrowserWindow({
icon: path.join(__dirname, './src/icons/png/1024x1024.png')
});
Windows 安装图标修改了没有变化
原因是 Windows 图标是有缓存的
清除 Windows 图标缓存
创建 icon.bat 内容如下,然后双击一下,清除 Windows 图标缓存
@REM % 进入图标缓存目录%
cd /c %userprofile%\AppData\Local\Microsoft\Windows\Explorer
@REM %关闭Windows资源管理器explorer%
taskkill /f /im explorer.exe
@REM %延时3s%
ping -n 3 127.0.0.1>nul
@REM %删除图标缓存数据库%
@REM attrib -h iconcache_.db
del iconcache_.db /a
@REM %延时3s%
ping -n 3 127.0.0.1>nul
@REM %重启Windows资源管理器explorer%
start explorer
参考链接
windows 清理图标缓存并重新加载 https://blog.csdn.net/nodeman/article/details/94555196
electron-forge 打包如何自定义应用图标和安装动画 https://segmentfault.com/q/1010000021004246
打出来的包很大
自定义快捷键
等待更新...
自动更新
等待更新...