这段学习了下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路径。希望对大家有点帮助。