脚手架
项目使用 electron-vite 脚手架搭建
ps:还有一个框架是 electron-vite ,这个框架我发现与pixi库有冲突,无法使用,如果不用pixi也可以用这个脚手架。
node 版本建议18+
----------------------------------------------------------------------------------------
运行live2D相关依赖
1.pixi.js
npm install [email protected]
// pixi 后面可能运行会报错,提示需要安装unsafe-eval
// 需要注意pixi/unsafe-eval 需要安装与pixi一致的版本
npm install @pixi/[email protected]
2. pixi-live2d-display
npm install pixi-live2d-display
3. live2D官方SDK
如果需要兼容老版本模型需要引入2.0版sdk
因为使用Vite,SDK引入不能使用import,需要在index.html 中使用script标签引入
需要注意的是文件存放路径,否则打包后会找不到文件,这里笔者是在renderer文件夹下创建了public文件夹,将渲染进程需要使用的静态资源存放在里面。
----------------------------------------------------------------------------------------
VUE 路由设置与Element-Plus安装
npm install vue-router
npm install element-plus
安装后新建相关文件夹与文件
需要注意的是路由模式要使用hash模式
router/index.ts
import {
createRouter,
createWebHashHistory,
type RouteLocationNormalized,
createWebHistory
} from "vue-router";
import routes from "./routes";
const router = createRouter({
// hash路由模式
history: createWebHashHistory(),
// History路由模式
// history: createWebHistory(),
routes
});
export interface toRouteType extends RouteLocationNormalized {
meta: {
title?: string;
noCache?: boolean;
};
}
router.beforeEach((to: toRouteType, from, next) => {
next();
});
router.afterEach(() => {
});
export default router;
router/routes.ts
import Layout from "../layout/index.vue";
import type { RouteRecordRaw } from "vue-router";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "root",
component: Layout,
redirect: "live2D",
children: [
{
path: "live2D",
name: "live2D",
component: () => import("../views/live2D/index.vue"),
meta: {
title: "live2D"
}
}
]
}
];
export default routes;
layout/index.vue
<script setup lang="ts">
import { computed } from "vue";
const cachedViews = computed(() => {
return [];
});
</script>
<template>
<div class="app-wrapper">
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
<style scoped>
.app-wrapper {
position: relative;
height: 100%;
width: 100%;
}
</style>
App.vue 加入router-view标签
<template>
<router-view />
</template>
<style></style>
renderer渲染进程 下的main.ts 引入相关包
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router/index";
import ElementPlus from 'element-plus';
import './assets/element.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
const app = createApp(App);
app.use(router);
app.use(ElementPlus,{locale:zhCn});
app.mount('#app')
main主进程 下的 main.ts 跳转修改如下
默认为跳转根目录,跟目录的redirect配置的页面,需要指定页面使用hash拼接路由与参数。
参数在路由后加?xxx=xxx&yyy=yyy
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
await mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
// 跳转指定页面
// await recordsListWindow.loadURL(process.env['ELECTRON_RENDERER_URL']+`/#/xxxx`);
} else {
await mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
// 跳转指定页面
// await recordsListWindow.loadFile(join(__dirname, '../renderer/index.html'),{hash:'/xxxx'});
}
测试使用
在renderer/views文件夹下创建 views/live2D/index.vue,设置模型容器
<script setup lang="ts" name="live2D">
import { reactive,ref,onMounted } from 'vue'
import * as PIXI from 'pixi.js'
import * as pixiFnPatch from "@pixi/unsafe-eval"
import { Live2DModel } from 'pixi-live2d-display'
import jsonFile from '/model/xxx/xxx.model3.json?url'
// 全局注册
let windowRef:any = window;
windowRef.PIXI = PIXI;
// 修复@pixi/unsafe-eval无法正常安装问题
pixiFnPatch.install(PIXI);
async function initLive2D(){
let model:any = await Live2DModel.from(jsonFile);
const app = new PIXI.Application({
view: document.getElementById('live2d-canvas') as HTMLCanvasElement,
width: 100,
height: 300,
autoStart:true,
backgroundAlpha:0
});
app.stage.addChild(model);
// app.renderer.backgroundColor = 0x0161639;
// transforms 模型方位
model.x = -10; // 方位(单位像素)
model.y = -20
// model.rotation = Math.PI
// model.skew.x = Math.PI
model.scale.set(0.6) // 缩放
model.anchor.set(0, 0) // 锚点,以画布中心下方为中心点,x,y(单位:倍)
model.on('hit', (hitAreas) => {
// if (hitAreas.includes('body')) {
// model.motion('tap_body')
// }
})
}
onMounted(() => {
initLive2D();
})
</script>
<template>
<div class='canvas-wrap'>
<canvas id="live2d-canvas" class="live2d-canvas" width="100" height="300"></canvas>
</div>
</template>
<style scoped>
.canvas-wrap{
width: 100%;
height: 100%;
cursor: move;
-webkit-app-region: drag;
}
.live2d-canvas{
width: 100%;
height: 100%;
}
</style>
主进程中的main.ts文件创建主窗口
// 创建主窗体参数做如下更改
const { width,height} = screen.getPrimaryDisplay().workAreaSize;
const mainWindow = new BrowserWindow({
x: width - 150,
y: height - 300,
width: 100,
height: 300,
show: false,
maximizable: false,
minimizable: false,
resizable: false,
fullscreenable: false,
frame: false,
transparent: true,
hasShadow: false,
alwaysOnTop: true,
titleBarStyle: 'customButtonsOnHover',
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
webSecurity:false // 禁用同源策略
}
})
----------------------------------------------------------------------------------------
测试效果-运行/打包
// 运行
npm run dev
// 打包 需要管理员权限
// 打包后在根目录的 dist 下有安装包和安装后的文件夹
npm run build:win
具体模型交互使用相关API参数进行设置即可
----------------------------------------------------------------------------------------
配置vue页面demo
1.views文件夹下新建demo/index.vue
2.router.ts新增相关路由
{
path: "demo",
name: "demo",
component: () => import("../views/demo/index.vue"),
meta: {
title: "demo"
}
},
3. 在主进程下的main.ts新增托盘菜单配置做测试
// 系统托盘图标目录
const appTray = new Tray(icon);
const menuTemplate = [
{
id: '1',
label: '查看demo',
click: async function () {
let recordsListWindow = new BrowserWindow({
width: 1000,
height: 600,
title: 'demo',
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true
}
});
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
await recordsListWindow.loadURL(process.env['ELECTRON_RENDERER_URL']+`/#/demo`);
}
else {
await recordsListWindow.loadFile(join(__dirname, '../renderer/index.html'),{hash:'/demo'});
}
recordsListWindow.setTitle('demo');
//打开开发者工具
if(is.dev) recordsListWindow.webContents.openDevTools({mode:'detach'});
}
},
{
id: '2',
label: '退出',
click: function(){
app.quit();
}
}
];
// 图标的上下文菜单
const contextMenu = Menu.buildFromTemplate(menuTemplate);
// 设置此托盘图标的悬停提示内容
appTray.setToolTip('demo');
appTray.setTitle('demo');
// 设置此图标的上下文菜单
appTray.setContextMenu(contextMenu);
配置完成后托盘图标右键即可出现菜单,点击后会创建新窗口显示对应路由下的vue文件
----------------------------------------------------------------------------------------
开发中可能出现的报错
1. tsc 代码检测报错
在package.json 中把相关脚本的tsc检测关闭
----------------------------------------------------------------------------------------
2.打包后静态资源无法访问
检查是否使用绝对路径,最好的方法是将渲染进程的静态资源都放在public文件夹下
渲染进程资源处理
主进程资源处理
----------------------------------------------------------------------------------------
3.模型拖拽与鼠标事件冲突
有时候会遇到既要模型能拖动,也要能右键出现菜单的需求。
有两种解决方法:
(1):设置区域拖动,只有部分区域能触发拖动
能拖动的元素设置:-webkit-app-region: drag;
要触发鼠标事件的元素设置:-webkit-app-region: no-drag;
(2):不用-webkit-app-region: drag;属性来拖动
具体代码如下:
前端页面做事件监听
const moveIng = ref(false);
const startX = ref(0);
const startY = ref(0);
const lastWidth = ref(0);
const lastHeight = ref(0);
function move (event:any){
if (!moveIng.value) return;
const x:any = window.screenX + event.clientX - startX.value
const y:any = window.screenY + event.clientY - startY.value
// 调用主进程函数
window.api.moveBounds(parseInt(x), parseInt(y), lastWidth.value, lastHeight.value);
}
window.addEventListener('mousedown',(event:any)=>{
event.preventDefault();
moveIng.value = true;
startX.value = parseInt(event.clientX);
startY.value = parseInt(event.clientY);
lastWidth.value = window.outerWidth;
lastHeight.value = window.outerHeight;
document.addEventListener('mousemove', move);
});
window.addEventListener('mouseup',(event:any)=>{
event.preventDefault();
if (!moveIng.value) return
document.removeEventListener('mousemove', move)
moveIng.value = false
});
window.addEventListener('contextmenu',()=>{
if (!moveIng.value) return
document.removeEventListener('mousemove', move)
moveIng.value = false
});
中间层 preload.ts
moveBounds: (x:any,y:any,width:any,height:any) => {
ipcRenderer.send('moveBounds',x,y,width,height);
}
主进程 main.ts
// 监听渲染线程窗体移动同时改变主进程位置
ipcMain.on('moveBounds', (event:any, x:any, y:any, width:any, height:any) => {
if(event.frameId!=mainWindow.webContents.id) return;
let newBounds = {
x: parseInt(x),
y: parseInt(y),
width: parseInt(width),
height: parseInt(height),
}
mainWindow.setBounds(newBounds)
})
----------------------------------------------------------------------------------------
4.右键自定义菜单
// 给主窗口添加右键菜单
const contextRightMenu = Menu.buildFromTemplate(menuTemplate);
mainWindow.webContents.on("context-menu", (e:any) => {
e.preventDefault();
contextRightMenu.popup();
});
----------------------------------------------------------------------------------------
5.在使用本地静态图片资源时报错
Refused to load the script xxxxxx because it violates the following Content Security Policy directive:"script-src 'self' xxxxxxxxxxxxx"
需要在index.html 中修改meta标签
<meta http-equiv="Content-Security-Policy" content="default-src *; img-src * 'self' data: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' *; style-src 'self' 'unsafe-inline' *">
----------------------------------------------------------------------------------------
6.修改打包图标
安装 electron-icon-builder 包
配置package.json,新增脚本,修改input路径为自己项目的路径
"build-icon": "electron-icon-builder --input=./resources/icon.png --output=build --flatten"
运行脚本即可生成
npm run build-icon