集成指南

最近更新: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

全新服务市场

亿级流量来解锁,开拓企业新格局

立即免费加入
售前咨询
2885542529
17767162660

添加客服微信咨询产品


扫一扫关注云登小程序


扫一扫关注云登公众号


扫码获取专属企业定制

在线咨询