用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路径。希望对大家有点帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值