用Electron与WTMPlus Vue3版 结合写桌面应用
这段学习了下Electron,想着如何用Electron承载前后端分离开发模式的前端来做桌面应用,然后跟后端进行通信,我写下我的趟坑历程;
一,借助Electron.Net,根据教程在WTM 上添加几句代码,代码能跑起来,但是登录的时候就登录不进去,
在asp.net core 项目目录下
PM> Install-Package ElectronNET.API
修改文件 Program.cs
public static IHostBuilder CreateWebHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { config.AddInMemoryCollection(new Dictionary<string, string> { { "HostRoot", hostingContext.HostingEnvironment.ContentRootPath } }); }) .ConfigureLogging((hostingContext, logging) => { logging.ClearProviders(); logging.AddConsole(); logging.AddWTMLogger(); }) .ConfigureWebHostDefaults(webBuilder => { // 新增 webBuilder.UseElectron(args); webBuilder.UseStartup<Startup>(); }); }
修改文件 Startup.cs
public void Configure(IApplicationBuilder app, IOptionsMonitor<Configs> configs, IHostEnvironment env) { ... Task.Run(async () => await Electron.WindowManager.CreateWindowAsync()); }
安装工具:
dotnet tool install ElectronNET.CLI -g
运行工具:
electronize init
electronize start
可以看到
Start Electron Desktop Application... Arguments: dotnet publish -r win-x64 -c "Debug" --output "F:\Projects\MyTest3\MyTest3\obj\Host\bin" /p:PublishReadyToRun=true /p:PublishSingleFile=true --no-self-contained 用于 .NET 的 Microsoft (R) 生成引擎版本 16.11.2+f32259642 版权所有(C) Microsoft Corporation。保留所有权利。 正在确定要还原的项目… 已还原 F:\Projects\MyTest3\MyTest3.ViewModel\MyTest3.ViewModel.csproj (用时 2.57 sec)。 已还原 F:\Projects\MyTest3\MyTest3.DataAccess\MyTest3.DataAccess.csproj (用时 2.57 sec)。 已还原 F:\Projects\MyTest3\MyTest3\MyTest3.csproj (用时 2.57 sec)。 已还原 F:\Projects\MyTest3\MyTest3.Model\MyTest3.Model.csproj (用时 2.57 sec)。 MyTest3.Model -> F:\Projects\MyTest3\MyTest3.Model\bin\Debug\net5.0\MyTest3.Model.dll MyTest3.DataAccess -> F:\Projects\MyTest3\MyTest3.DataAccess\bin\Debug\net5.0\MyTest3.DataAccess.dll MyTest3.ViewModel -> F:\Projects\MyTest3\MyTest3.ViewModel\bin\Debug\net5.0\MyTest3.ViewModel.dll MyTest3 -> F:\Projects\MyTest3\MyTest3\bin\Debug\net5.0\win-x64\MyTest3.dll v14.17.5 Performing first-run Webpack build... One CLI for webpack must be installed. These are recommended choices, delivered as separate packages: - webpack-cli (https://github.com/webpack/webpack-cli) The original webpack full-featured CLI. We will use "yarn" to install the CLI via "yarn add -D". Do you want to install 'webpack-cli' (yes/no): > @wtm/vue3@0.1.0 build F:\Projects\MyTest3\MyTest3\ClientApp > vue-cli-service build --report ------------------------------------ create font ------------------------------------ - Building for production... WARNING Compiled with 2 warnings涓嬪崍3:54:32 warning asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: img/fontawesome-webfont.acf3dcb7.svg (437 KiB) js/app.5232d32f.js (415 KiB) js/chunk-1a2489ea.2d9338db.js (1.86 MiB) js/chunk-7e280e55.af6a40d1.js (1.04 MiB) css/chunk-vendors.eec29255.css (479 KiB) js/chunk-vendors.62a45cc3.js (1.69 MiB) warning entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance. Entrypoints: app (2.63 MiB) css/chunk-vendors.eec29255.css js/chunk-vendors.62a45cc3.js css/app.c7e28233.css js/app.5232d32f.js File Size Gzipped build\js\chunk-1a2489ea.2d9338db.js 1901.80 KiB 404.49 KiB build\js\chunk-vendors.62a45cc3.js 1733.19 KiB 507.63 KiB build\js\chunk-7e280e55.af6a40d1.js 1069.18 KiB 335.53 KiB build\js\app.5232d32f.js 415.06 KiB 67.47 KiB build\css\chunk-vendors.eec29255.css 478.58 KiB 57.59 KiB build\css\chunk-1a2489ea.0ed08d25.css 175.45 KiB 27.90 KiB build\css\app.c7e28233.css 62.05 KiB 23.98 KiB build\css\chunk-7e280e55.c4ac372b.css 0.25 KiB 0.21 KiB Images and other types of assets omitted. DONE Build complete. The build directory is ready to be deployed. INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html MyTest3 -> F:\Projects\MyTest3\MyTest3\obj\Host\bin\ node_modules missing in: F:\Projects\MyTest3\MyTest3\obj\Host\node_modules Start npm install... npm install up to date in 2.516s 8 packages are looking for funding run `npm fund` for details ElectronHostHook handling started... Invoke electron.cmd - in dir: F:\Projects\MyTest3\MyTest3\obj\Host\node_modules\.bin electron.cmd "..\..\main.js" Electron Socket IO Port: 8000 Electron Socket started on port 8000 at 127.0.0.1 ASP.NET Core Port: 8001 stdout: Use Electron Port: 8000 stdout: info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[63] User profile is available. Using 'C:\Users\CFQGZZ\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest. stdout: info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:8001 stdout: ASP.NET Core host has fully started. info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. stdout: info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production stdout: info: Microsoft.Hosting.Lifetime[0] Content root path: F:\Projects\MyTest3\MyTest3\obj\Host\bin\ ASP.NET Core Application connected... global.electronsocket N8RLpFYfsAQOP1SzAAAA 2021-12-19T07:54:55.341Z stdout: BridgeConnector connected!
可惜登录失败,
stdout: info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] => SpanId:f4bcd5605fae374d, TraceId:562bec272799ec4c8a1d178d080b37e6, ParentId:0000000000000000 => ConnectionId:0HME2OJD021JD => RequestPath:/api/_Account/LoginJwt RequestId:0HME2OJD021JD:00000002 Executed endpoint 'WalkingTec.Mvvm.Admin.Api.AccountController.LoginJwt (MyTest3)' stdout: fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1] => SpanId:f4bcd5605fae374d, TraceId:562bec272799ec4c8a1d178d080b37e6, ParentId:0000000000000000 => ConnectionId:0HME2OJD021JD => RequestPath:/api/_Account/LoginJwt RequestId:0HME2OJD021JD:00000002 An unhandled exception has occurred while executing the request. System.InvalidOperationException: Cannot create a DbSet for 'FrameworkUser' because this type is not included in the model for the context. at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityType() at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityQueryable() at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Linq.IQueryable.get_Provider() at System.Linq.Queryable.Where[TSource](IQueryable`1 source, Expression`1 predicate) at WalkingTec.Mvvm.Admin.Api.AccountController.LoginJwt(SimpleLogin loginInfo) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at WalkingTec.Mvvm.Mvc.WtmMiddleware.InvokeAsync(HttpContext context, IOptionsMonitor`1 configs) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
怀疑打包的时候 MyTest3.Model.dll 没打包进去,但是试了好久没弄明白,有用过Electron.Net的朋友不知道有没有碰过这个问题;因为这个问题,加上最新版本Electron对安全的加强,渲染进程不能直接调用Nodejs的API,Electron.Net支持的还不够,就放弃了;
那就不用第三方框架,直接用Electron加载前端脚本;
步骤一:新建一个脚本 myconfig.js
myconfig.js
let getBaseUrl = function () { return window.ipcRenderer.webApi; } export { getBaseUrl }
步骤二:修改 config.ts
import Bowser from 'bowser'; import lodash from 'lodash'; import { BindAll } from 'lodash-decorators'; import { configure } from "mobx"; import { getBaseUrl } from '@/cfg/myconfig.js' .... // readonly target = ''// lodash.get(window, '__xt__env.target', process.env.target); readonly target = getBaseUrl()
不直接使用 target=window.ipcRenderer.webApi 是因为 ts编译会通不过;window没有 ipcRenderer 属性;
步骤三:在ClientApp目录里面运行
npm run build
步骤四:为了省事,准备一个Electron Vue3模板项目,我有使用串口通信的需要,于是使用 https://gitee.com/chiugi/vue3-electron-serialport,,改名为 wtm-vue3-electron在此基础上进行修改
修改 background.js
import { app, protocol, BrowserWindow, session, ipcMain, } from 'electron'; import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; // import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; // import serialport from 'serialport'; const path = require('path'); const isDevelopment = process.env.NODE_ENV !== 'production'; app.allowRendererProcessReuse = false; // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } }, ]); ipcMain.on('serialport', (event, arg) => { console.log('ipcMain', arg); event.reply('asynchronous-reply', `ipcMain replay${arg}`); }); let fs=require("fs"); const configFileName= path.join(__dirname, 'myconfig.json'); if (!fs.existsSync(configFileName)) { let data= { "webApi":"http://localhost:5000", }; let jsonObj=JSON.stringify(data); fs.writeFile(configFileName,jsonObj,function (err) { if(err){ console.log(err); }else{ console.log("file success!!!") } }) } async function createWindow() { // Create the browser window. const win = new BrowserWindow({ //frame: false, //无边框 width: 800, height: 600, // titleBarStyle: 'hidden', // titleBarOverlay: { // color: '#2f3241', // symbolColor: '#74b1be' // }, webPreferences: { // contextIsolation: true,//弃用,默认true // Use pluginOptions.nodeIntegration, leave this alone // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info // nodeIntegration: true, // process.env.ELECTRON_NODE_INTEGRATION before webSecurity: false, //注意,通过这个设置才能支持跨域,才能正常跟WebAPI通信 //preload: path.join(__dirname, '../src/preload.js'), preload: path.join(__dirname, 'preload.js'), }, }); if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); if (!process.env.IS_TEST) win.webContents.openDevTools(); } else { createProtocol('app'); // Load the index.html when not in development win.loadURL('app://./index.html'); } } ipcMain.on('async-get-webapi', function(event, arg) { // arg是从渲染进程返回来的数据 console.log(arg); // let fs=require('fs') // 使用fs模块 // 这里是传给渲染进程的数据 fs.readFile(configFileName,"utf8",(err,data)=>{ if(err){ event.sender.send('async-get-webapi-reply', "读取失败"); }else{ event.sender.send('async-get-webapi-reply', data); } }) }); ipcMain.on('sync-get-webapi', (event, arg) => { console.log(arg) // prints "ping" fs.readFile(configFileName,"utf8",(err,data)=>{ if(err){ event.returnValue=''; }else{ event.returnValue=data } }) }) // Quit when all windows are closed. app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { // 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(); }); // 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.on('ready', async () => { if (isDevelopment && !process.env.IS_TEST) { // Install Vue Devtools // try { // await installExtension(VUEJS_DEVTOOLS); // session.defaultSession.loadExtension( // path.resolve(__dirname, '../../vue-devtools/shells/chrome'), // 这个是刚刚build好的插件目录 // ); // } catch (e) { // console.error('Vue Devtools failed to install:', e.toString()); // } // 记得预先安装 npm install vue-devtools const ses = session.fromPartition('persist:name'); try { // The path to the extension in 'loadExtension' must be absolute await ses.loadExtension(path.resolve('node_modules/vue-devtools/vender')); } catch (e) { console.error('Vue Devtools failed to install:', e.toString()); } } ipcMain.on('synchronous-message', (event, arg) => { console.log(arg) // prints "ping" event.returnValue = 'pong' }) createWindow(); }); // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { app.quit(); } }); } else { process.on('SIGTERM', () => { app.quit(); }); } }
preload.js
// window.ipc = require('ipc'); const { contextBridge, ipcRenderer } = require('electron'); const SerialPort = require('serialport'); // import crc16 from './crc16.js'; const crc16 = require('./crc16'); const bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const crcResult = crc16(bytes, 9); console.log('crc16=', crcResult); // window.ipcRenderer = ipcRenderer; let serialIns = null; let myWebApi=""; function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } contextBridge.exposeInMainWorld( 'serialPort', { getPorts: () => SerialPort.list(), getSerInstance: (comName, baudRate, func) => { if (serialIns) { if (serialIns.isOpen) { serialIns.close(); } } serialIns = new SerialPort(comName, { baudRate: parseInt(baudRate, 10), }, (err) => { console.log(err); }); // console.log(serialIns); serialIns.on('data', (data) => { //const _data = `${data}`; const _data = ab2str(data); console.log('receive', data,_data); //func(_data); func(data); //返回数组 }); }, write: (sendData, fun) => { serialIns.write(sendData, (error) => { fun(error); if (error) { return console.log('Error on write: ', error.message); } }); }, }, ); // ipcRenderer.on("async-get-webapi-reply", function(event, arg) { // // 这里的arg是从主线程请求的数据 // console.log("render+" + arg); // //let config=JSON.parse(arg); // //myWebApi=config.webApi; // }); // 这里的会传递回给主进程,这里的第一个参数需要对应着主进程里on注册事件的名字一致 // ipcRenderer.send("async-get-webapi", "传递回去ping"); contextBridge.exposeInMainWorld('myAPI', { doAThing: () => { console.log('myAPI'); }, }); ipcRenderer.on('asynchronous-reply', (event, arg) => { console.log(arg); // prints "pong"}) }); contextBridge.exposeInMainWorld( 'ipcRenderer', { receive: (channel, func) => { ipcRenderer.on(channel, (event, ...args) => func(...args)); }, sendData: (data) => { ipcRenderer.send('serialport', data); console.log('ipcRenderer send', data); }, webApi:JSON.parse(ipcRenderer.sendSync('sync-get-webapi', 'webapi')).webApi, }, );
crc16.js
function crc16(str, len) { const buf = Buffer.from(str); let crc = 0xFFFF; for (let i = 0; i < len; i += 1) { crc ^= buf[i]; for (let j = 0; j < 8; j += 1) { if (crc & 0x01) { crc = (crc >> 1) ^ 0xa001; continue; } crc >>= 1; } } return crc; } module.exports = crc16;
步骤五:把 WTM项目build之后生成的文件 ClientApp\build下的文件复制到 wtm-vue3-electron 项目下面目录 public 里面,并且把 wtm-vue3-electron目录下的 src/preload.js,src/crc16.js文件复制到 public下面
wtm-vue3-electron 的 public 目录
在 wtm-vu3-electron 下运行
npm run electron:build
打包成功后:
win-unpacked
启动 wtm-vue3-electron.exe
启动后端程序
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[63] User profile is available. Using 'C:\Users\CFQGZZ\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest. info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Now listening on: https://localhost:5001 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production info: Microsoft.Hosting.Lifetime[0] Content root path: F:\Projects\MyTest3\MyTest3\bin\Debug\net5.0\win-x64
点登录:
在 resources/app 目录下有 myconfig.json,在里面可以修改webapi路径,那前后端就可以分开部署了。
{"webApi":"http://localhost:5000"}
总结:主要碰到的问题就是在Electron里面如何跨域,另外就是怎么读取配置文件,获取WebAPI路径。希望对大家有点帮助。