集成指南
最近更新:2024-09-29 17:47:02
browser-api
electron-vite + vue3 + TS + element-plus
开发环境
node: 20.17.0
electron: 31.0.2
vue: 3.4.30
搭建 electron-vite 项目
electron-vite官网
npm create @quick-start/electron@latest
✔ Project name: … <electron-app>
✔ Select a framework: › vue
✔ Add TypeScript? … No / Yes
✔ Add Electron updater plugin? … No / Yes
✔ Enable Electron download mirror proxy? … No / Yes
Scaffolding project in ./<electron-app>...
Done.
npm install
step1: 安装node-addon-api
npm install node-addon-api
npm install --save-dev node-gyp
step2: 配置sdk *.cc文件(2025-01-02)
#include <napi.h>
#include <string>
#include <cwchar>
#include <windows.h> // 包含 windows.h
#include "yl_sdk/yl_sdk_def.h"
#include "yl_sdk/yl_sdk.h"
#include <iostream>
// 辅助函数:将 UTF-8 字符串转换为宽字符字符串
std::wstring Utf8ToWide(const char* utf8Str) {
int wideLength = MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, nullptr, 0);
std::wstring wideStr(wideLength, L'\0'); // 宽字符串
// 再次调用 MultiByteToWideChar,这次指定宽字符串的长度
MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, &wideStr[0], wideLength);
return wideStr;
}
// 保存全局的 JavaScript 回调函数
Napi::ThreadSafeFunction g_browserEventCallback;
class BrowserEventDelegateImpl : public YLSDK::BrowserEventDelegate {
public:
void OnBrowserOpening(const char* envId, int process) {
// std::cout << "OnBrowserOpening: " << envId << std::endl;
if (g_browserEventCallback) {
std::string envIdIn = envId;
g_browserEventCallback.BlockingCall([envIdIn, process](Napi::Env env, Napi::Function jsCallback) {
jsCallback.Call({
Napi::String::New(env, "OnBrowserOpening"),
Napi::String::New(env, envIdIn),
Napi::Number::New(env, process)
});
});
}
}
void OnBrowserOpenResult(const char* envId, int code, const char* errMsg) {
// std::cout << "OnBrowserOpenResult: " << envId << std::endl;
if (g_browserEventCallback) {
std::string envIdIn = envId;
std::string errMsgIn = errMsg;
g_browserEventCallback.BlockingCall([envIdIn, code, errMsgIn](Napi::Env env, Napi::Function jsCallback) {
jsCallback.Call({
Napi::String::New(env, "OnBrowserOpenResult"),
Napi::String::New(env, envIdIn),
Napi::Number::New(env, code),
Napi::String::New(env, errMsgIn)
});
});
}
}
void OnBrowserClosed(const char* envId) {
// std::cout << "OnBrowserClosed: " << envId << std::endl;
if (g_browserEventCallback) {
std::string envIdIn = envId;
g_browserEventCallback.BlockingCall([envIdIn](Napi::Env env, Napi::Function jsCallback) {
jsCallback.Call({
Napi::String::New(env, "OnBrowserClosed"),
Napi::String::New(env, envIdIn)
});
});
}
}
};
std::unique_ptr<BrowserEventDelegateImpl> g_delegateImpl;
// 注册 JavaScript 回调
void RegisterEventCallback(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsFunction()) {
Napi::TypeError::New(env, "Expected a callback function").ThrowAsJavaScriptException();
return;
}
Napi::Function jsCallback = info[0].As<Napi::Function>();
g_browserEventCallback = Napi::ThreadSafeFunction::New(
env,
jsCallback,
"EventCallback",
0, // Unlimited queue size
1 // Only one thread will use this function
);
}
// NAPI 包装 - 初始化 SDK
Napi::Object InitSDKWrapped(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// 确保传递了初始化参数
if (info.Length() < 1 || !info[0].IsObject()) {
Napi::TypeError::New(env, "Expected InitParam object").ThrowAsJavaScriptException();
return Napi::Object::New(env);
}
const Napi::Object initParamObj = info[0].As<Napi::Object>();
YLSDK::InitParam initParam;
// 检查必需的字段
if (!initParamObj.Get("appId").IsString() || !initParamObj.Get("appSecret").IsString()) {
Napi::TypeError::New(env, "appId and appSecret must be strings").ThrowAsJavaScriptException();
return Napi::Object::New(env);
}
std::wstring strBrandingName = Utf8ToWide(initParamObj.Get("brandingName").As<Napi::String>().Utf8Value().c_str());
std::wstring strAppId = Utf8ToWide(initParamObj.Get("appId").As<Napi::String>().Utf8Value().c_str());
std::wstring strAppSecret = Utf8ToWide(initParamObj.Get("appSecret").As<Napi::String>().Utf8Value().c_str());
initParam.brandingName = strBrandingName.c_str();
initParam.appId = strAppId.c_str();
initParam.appSecret = strAppSecret.c_str();
if (initParamObj.Has("appIcon") && initParamObj.Get("appIcon").IsString()) {
std::wstring appIcon = Utf8ToWide(initParamObj.Get("appIcon").As<Napi::String>().Utf8Value().c_str());
initParam.appIcon = appIcon.c_str();
}
if (initParamObj.Has("browserCoresDir") && initParamObj.Get("browserCoresDir").IsString()) {
std::wstring browserCoresDir = Utf8ToWide(initParamObj.Get("browserCoresDir").As<Napi::String>().Utf8Value().c_str());
initParam.browserCoresDir = browserCoresDir.c_str();
}
if (initParamObj.Has("cacheDir") && initParamObj.Get("cacheDir").IsString()) {
std::wstring cacheDir = Utf8ToWide(initParamObj.Get("cacheDir").As<Napi::String>().Utf8Value().c_str());
initParam.cacheDir = cacheDir.c_str();
}
if (!g_delegateImpl) {
g_delegateImpl = std::move(std::make_unique<BrowserEventDelegateImpl>());
}
// 调用 C++ 的 InitSDK 函数,确保使用命名空间
YLSDK::SDKError errCode = YLSDK::InitSDK(initParam, g_delegateImpl.get());
// 返回结果对象
Napi::Object result = Napi::Object::New(env);
result.Set("code", static_cast<int>(errCode));
return result;
}
// NAPI 包装 - 获取 SDK 版本号
Napi::String GetSDKVersionWrapped(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
const char* version = YLSDK::GetSDKVersion();
return Napi::String::New(env, version);
}
// NAPI 包装 - 获取 SDK 信息
Napi::Object GetSDKInfoWrapped(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
YLSDK::SDKInfo sdkInfo;
// 确保使用命名空间调用 GetSDKInfo
YLSDK::SDKError errCode = YLSDK::GetSDKInfo(sdkInfo);
Napi::Object result = Napi::Object::New(env);
result.Set("code", static_cast<int>(errCode));
if (errCode == YLSDK::SDKERR_SUCCESS) {
result.Set("port", sdkInfo.port);
result.Set("sdkVersion", sdkInfo.sdkVersion);
result.Set("versionCode", sdkInfo.versionCode);
result.Set("platform", sdkInfo.platform);
result.Set("bitness", sdkInfo.bitness);
}
return result;
}
// 初始化模块
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "InitSDK"), Napi::Function::New(env, InitSDKWrapped));
exports.Set(Napi::String::New(env, "GetSDKVersion"), Napi::Function::New(env, GetSDKVersionWrapped));
exports.Set(Napi::String::New(env, "GetSDKInfo"), Napi::Function::New(env, GetSDKInfoWrapped));
exports.Set(Napi::String::New(env, "RegisterEventCallback"), Napi::Function::New(env, RegisterEventCallback));
return exports;
}
NODE_API_MODULE(addon, Init)
step3: 配置 binding.gyp
{
"targets": [
{
"target_name": "browser", # 项目目标名称,通常是要生成的模块名
"sources": ["src/native/yl-sdk.cc"], # 源代码文件路径,这里指定了 C++ 文件
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")", # 使用 node-addon-api 的 include 路径
"src/native/yl-sdk" # 自定义的 include 目录,包含原生 SDK 头文件等
],
"libraries": [
"<(module_root_dir)/src/native/yl-sdk/yl_sdk.lib" # 链接的库文件路径,指定 yl_sdk 的 .lib 文件
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")" # 添加 node-addon-api 作为依赖,加载其 gyp 文件
],
"cflags!": [ "-fno-exceptions" ], # 编译器标志:禁用 C 的异常处理支持
"cflags_cc!": [ "-fno-exceptions" ], # 编译器标志:禁用 C++ 的异常处理支持
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS" # 定义预处理宏,禁用 N-API 的 C++ 异常支持,减少开销
],
"msvs_settings": { # Microsoft Visual Studio 的特定设置,仅在 Windows 下生效
"VCCLCompilerTool": {
"AdditionalOptions": [
'/std:c++17', # (可选)可以打开以使用 C++17 标准
"/utf-8" # 编译器选项:将源文件编码设置为 UTF-8,以支持多语言字符
]
}
}
}
]
}
step4: yl_sdk.dll(开发)
yl_sdk.dll拷贝到项目根目录下
electron.vite.config.ts
export default defineConfig(() => {
return {
main: {
plugins: [
viteStaticCopy({
targets: [
{
src: 'src/native/yl_sdk/yl_sdk.dll', // 源文件路径
dest: __dirname // 拷贝到输出目录的根目录
}
]
})
],
}
...
}
})
step5: 文件复制(2025-01-02)
将yl_sdk.dll输出到安装包中
将browser目录输出到安装包中
将plugins目录输出到安装包中
将icon目录输出到安装包中
创建scripts\afterPack.js
const path = require('path')
const fs = require('fs-extra')
module.exports = async function (context) {
// 复制sdk文件
const source = path.resolve(__dirname, '../src/native/yl_sdk/yl_sdk.dll')
const destination = path.join(context.appOutDir, 'yl_sdk.dll')
try {
await fs.copy(source, destination)
console.log('yl_sdk.dll successfully copied to dist/win-unpacked.')
} catch (err) {
console.error('Error copying yl_sdk.dll:', err)
}
// 复制浏览器内核
const browserSource = path.resolve(__dirname, '../browser')
const browserDestination = path.join(context.appOutDir, 'browser')
try {
await fs.copy(browserSource, browserDestination)
console.log('browser successfully copied to dist/win-unpacked.')
} catch (err) {
console.error('Error copying browser:', err)
}
const pluginsSource = path.resolve(__dirname, '../plugins')
const pluginsDestination = path.join(context.appOutDir, 'plugins')
try {
await fs.copy(pluginsSource, pluginsDestination)
console.log('plugins successfully copied to dist/win-unpacked.')
} catch (err) {
console.error('Error copying plugins:', err)
}
const iconSource = path.resolve(__dirname, '../icon')
const iconDestination = path.join(context.appOutDir, 'icon')
try {
await fs.copy(iconSource, iconDestination)
console.log('icon successfully copied to dist/win-unpacked.')
} catch (err) {
console.error('Error copying icon:', err)
}
}
electron-builder.yml
...
afterPack: ./scripts/afterPack.js # 自定义打包后处理
step6: 生成*.node文件
输出build\Release\browser.node
npx node-gyp configure
npx node-gyp build
step7: electron引入sdk(2025-01-02)
companyName、appIcon、browserCoresDir、cacheDir非必填参数
src\main\browser\browserManager.ts
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
import GModel from '@main/mvc/GModel'
import browserAddon from '@build/Release/browser.node'
import { IAppInfo } from '@src2/type/type'
import { IInitParam } from './type'
class browserManager {
/** 单例模式的全局模型 */
private _GM: GModel
/** 浏览器 SDK 通信端口 */
port = 0
constructor() {
this._GM = GModel.getInstance()
}
/**
* 绑定浏览器 SDK 并初始化
* @param evt Electron 主进程事件
* @param appInfo 应用信息
*/
bind(evt: Electron.IpcMainEvent, appInfo: IAppInfo): void {
/** 初始化响应数据 */
const initParam: IInitParam = {
companyName: 'companyid',
brandingName: appInfo.brandingName,
appId: appInfo.appId,
appSecret: appInfo.appSecret,
appIcon: is.dev ? join(__dirname, '../../icon/chrome2.png') : join(process.resourcesPath, '../icon/chrome2.png'),
browserCoresDir: is.dev ? join(__dirname, '../../browser') : join(process.resourcesPath, '../browser'),
cacheDir: 'C:/test'
}
// 判断是否已初始化
if (this.port !== 0) {
this._GM.mainWindow?.webContents.send('update-counter', this.port)
evt.reply('bind-complete', { code: -2, message: '已绑定过' })
return
}
/**
* 注册事件回调函数
* @param type 事件类型 (打开、打开中、关闭)
* @param args 可变参数,与事件类型相关
*/
browserAddon.RegisterEventCallback((type: string, ...args) => {
console.log('js type: ', type, args)
// 转发给前台页面
this._GM.mainWindow?.webContents.send('browser-operate', {
type,
data: args
})
})
// 调用 SDK 初始化方法
const initRes = browserAddon.InitSDK(initParam)
if (initRes && initRes.code === 0) {
// 获取SDK配置信息
const initData = browserAddon.GetSDKInfo()
if (initData && initData.code === 0) {
console.log('initData', initData)
this.port = initData.port
this._GM.mainWindow?.webContents.send('update-counter', this.port)
evt.reply('bind-complete', { code: 200, message: '绑定成功' })
} else {
evt.reply('bind-complete', { code: -1, message: '绑定失败' })
}
}
}
}
export default browserManager
src\main\browser\type.d.ts
/** 初始化入参 */
export interface IInitParam {
/** 公司id */
companyName?: string
/**
* app名称
* @description 必需:是,英文字母或数字,最好不要有空格:如UshopBrowser.
*/
brandingName: string
appId: string
appSecret: string
/**
* 应用图标
* @description 本地文件全路径. 如未指定,默认为SDK所在目录 app_icon_48x48.png
*/
appIcon?: string
/**
* 浏览器内核所在路径
* @description 如未指定,,默认为sDK所在目录
*/
browserCoresDir?: string
/**
* 浏览器沙盒缓存路径
* @description 如未指定,默认为C:\\Users\\[User Name]\\AppData\\LocaL\\[brandingName]
*/
cacheDir?: string
}
step8: electron + sdk通讯
src\main\browser\browserManager.ts
/**
* 注册事件回调函数
* @param type 事件类型 (打开、打开中、关闭)
* @param args 可变参数,与事件类型相关
*/
browserAddon.RegisterEventCallback((type: string, ...args) => {
console.log('js type: ', type, args)
// 转发给前台页面
this._GM.mainWindow?.webContents.send('browser-operate', {
type,
data: args
})
})
...
src\renderer\src\App.vue
/**
* 浏览器打开中的回调
* @param envId 环境 ID 列表
* @param process 打开进度 (0-100)
*/
const onBrowserOpening = (envId: string[], process: number) => {
console.log('Browser is opening:', envId, 'Process:', process)
}
/**
* 浏览器打开结果的回调
* @param envId 环境 ID
* @param code 状态码 (200 表示成功)
* @param errMsg 错误信息 (仅当 code 非 200 时存在)
*/
const onBrowserOpenResult = (envId: string, code: number, errMsg: string) => {
if (code === 200) {
console.log('Browser opened successfully:', envId)
} else {
console.error('Failed to open browser:', envId, 'Error Code:', code, 'Message:', errMsg)
}
}
/**
* 浏览器关闭的回调
* @param envId 环境 ID
*/
const onBrowserClosed = (envId: string) => {
console.log('Browser closed:', envId)
}
window.main.onBrowserOperate((info) => {
const { type, data } = info
// 通过mitt事件下发通知
switch (type) {
case BrowserEvent.OPEN:
onBrowserOpenResult(data[0], data[1], data[2])
break
case BrowserEvent.OPENING:
onBrowserOpening(data[0], data[1])
break
case BrowserEvent.CLOSE:
onBrowserClosed(data[0])
break
default:
console.warn(`Unhandled event type: ${type}`)
}
})
step9: 启动子环境自定义数据
通过post启动浏览器接口,custom_data参数
object 入参
{
"env_id": "2114469d98f0462d9ffca8ceb******",
"urls": [
"www.baidu.com"
],
"append_cmd": "",
"cookies": "",
"remote_debugging": 0,
"remote_debugging_address": "",
"custom_data": "{name:'test', id:'*******'}"
}
string入参
{
"env_id": "2114469d98f0462d9ffca8ceb******",
"urls": [
"www.baidu.com"
],
"append_cmd": "",
"cookies": "",
"remote_debugging": 0,
"remote_debugging_address": "",
"custom_data": "显示测试信息"
}
step10: 插件
直接拷贝到plugins目录下
获取当前环境数据
// 自定义数据
localStorage.getItem('customData')
// 环境id
localStorage.getItem('browserId')
step11: 启动显示页面
修改plugins\Env
默认启动调用页面plugins\Env\fingerprint.html
step12: 修改头部显示信息
代理显示信息
chrome.fb为SDK变量
/**
* 向客户端发送信息
* @param type 类型
* @param text 提示文字
*
* @description
*
* 210; //'{"text":"---.---.---.--- / -- --"}'
*
* 210; //'{"text":"获取中~~~"}'
*
* 210; //'{"text":"检测中"}'
*
* 212; //'{"ip":"183.157.193.229", "area":"浙江 杭州"}'
*
*/
chrome.fb.sendMessageToClient(210, '检测中')
manifest.json
加入fb操作权限
{
...
"permissions": [ "fb", ...]
...
}
step13: 缓存路径(2025-01-02)
默认缓存路径 C:\Users\用户名...\AppData\Local\app名称
参考 step7: electron引入sdk
/** 初始化响应数据 */
const initParam: IInitParam = {
brandingName: appInfo.brandingName,
appId: appInfo.appId,
appSecret: appInfo.appSecret,
appIcon: is.dev ? join(__dirname, '../../icon/chrome2.png') : join(process.resourcesPath, '../icon/chrome2.png'),
browserCoresDir: is.dev ? join(__dirname, '../../browser') : join(process.resourcesPath, '../browser'),
cacheDir: 'C:/test'
}
// 调用 SDK 初始化方法
const initRes = browserAddon.InitSDK(initParam)
Demo
实例
实例以最简单的方式,列举了初始化、获取sdk信息、通知页面等功能。
src\main\index.ts
主进程接收消息
let initData
const initParam = {
brandingName: "YunBrowser",
appId: "f88a13fb0649476c093cb0***************",
appSecret: "52efeb6bd6789f5144******************"
}
// 初始化SDK
const initRes = browserAddon.InitSDK(initParam)
if (initRes && initRes.code === 0) {
// 获取SDK配置信息
initData = browserAddon.GetSDKInfo()
if (initData && initData.code === 0) {
mainWindow.webContents.send('update-counter', initData.port)
// mainWindow.webContents.send('update-counter', 50230)
}
}
src\preload\index.ts
消息中转
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
const notify = {
send: (data) => {
console.log('channel data', data, data.appId)
ipcRenderer.send('bind-app', data)
},
on: (func) => {
ipcRenderer.on('bind-complete', (_event, ...args) => {
func(...args)
})
},
off: () => {
ipcRenderer.removeAllListeners('bind-complete')
}
}
const main = {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value),
onBrowserOperate: (callback) => ipcRenderer.on('browser-operate', (_event, value) => callback(value))
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('main', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('notify', notify)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.main = main
// @ts-ignore (define in dts)
window.api = api
// @ts-ignore (define in dts)
window.notify = notify
}
src\renderer\src\App.vue
接收主进程消息
<template>
<Layout>
<router-view />
</Layout>
</template>
<script setup lang="ts">
import { configStore } from '@/store/config'
import Layout from '@/components/layout/index.vue'
import { BrowserEvent } from '@/type/enum'
// console.log(window.electron.process.versions)
const configS = configStore()
const appInfo = localStorage.getItem('appInfo')
if (appInfo) {
configS.bindApp(JSON.parse(appInfo))
}
/**
* 浏览器打开中的回调
* @param envId 环境 ID 列表
* @param process 打开进度 (0-100)
*/
const onBrowserOpening = (envId: string[], process: number) => {
console.log('Browser is opening:', envId, 'Process:', process)
}
/**
* 浏览器打开结果的回调
* @param envId 环境 ID
* @param code 状态码 (200 表示成功)
* @param errMsg 错误信息 (仅当 code 非 200 时存在)
*/
const onBrowserOpenResult = (envId: string, code: number, errMsg: string) => {
if (code === 200) {
console.log('Browser opened successfully:', envId)
} else {
console.error('Failed to open browser:', envId, 'Error Code:', code, 'Message:', errMsg)
}
}
/**
* 浏览器关闭的回调
* @param envId 环境 ID
*/
const onBrowserClosed = (envId: string) => {
console.log('Browser closed:', envId)
}
window.main.onBrowserOperate((info) => {
const { type, data } = info
// 通过mitt事件下发通知
switch (type) {
case BrowserEvent.OPEN:
onBrowserOpenResult(data[0], data[1], data[2])
break
case BrowserEvent.OPENING:
onBrowserOpening(data[0], data[1])
break
case BrowserEvent.CLOSE:
onBrowserClosed(data[0])
break
default:
console.warn(`Unhandled event type: ${type}`)
}
})
window.main.onUpdateCounter((value: number) => {
console.log(value)
configS.setPort(value)
})
</script>
Development
安装依赖,并通过yl-sdk.cc生成build\Release\browser.node文件
```bash
npm i
运行项目
```bash
npm run dev
Build
# For windows
$ npm run build:win
# For macOS
$ npm run build:mac
# For Linux
$ npm run build:linux