雨水象征着生机活力和丰沃繁茂,随着气温的回升和降水的增多,植物开始生长,动物也开始活跃起来,大自然呈现出一片生机勃勃的景象。这代表着春天的到来,万物复苏,充满了生机和活力
上一篇:立春-如何初始化electron项目 已经初始化electron+vue3的项目
这一篇文章将实战登录(类似钉钉)多窗口切换登录
设计初稿分析
分析:
- 登录窗体大致: 500*600尺寸
- 登录后的内容页窗体: 1100*700尺寸
- electron应用如何请求接口登录?
- 如何保持登录,打开app会直接到登录后到内容页?
- 登录后是如何销毁前面一个窗体的?
- 退出登录后如何返回登录窗体?
上面这些会在下面的讲解和实现中一步步教会你...
开发准备
参考前文实现初始化:juejin.cn/post/748043... 如果是react,请用react的社区的npm包
- 从登录页 ---> 主页: 我们是需要vue-router的
- UI 我们选用element-plus
npm
npm install vue-router --save
npm install element-plus --save
npm install -D sass-embedded
npm install normalize.css --save
建立路由
路由配置
- 在renderer/src 建立/router/index.ts 文件夹和文件
ts
import { createRouter, createWebHistory } from 'vue-router'
const Home = () => import('@renderer/home/index.vue')
const Layout = () => import('@renderer/layout/index.vue')
const Login = () => import('@renderer/views/login/index.vue')
const routes = [
{
path: '/',
component: Layout, // 这个路由是主内容的
name: 'home',
meta: { requiresAuth: true },
children: [{ path: '', component: Home, name: 'index' }]
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
改造文件
- 改造renderer/src/App.vue
ts
<template>
<RouterView />
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
- 改造renderer/src/main.ts
ts
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import 'element-plus/dist/index.css'
import router from '@renderer/router/index'
const app = createApp(App)
app.use(router)
app.mount('#app')
- 改造renderer/assets/main.css
css
@import 'normalize.css';
#root {
height: 100vh;
width: 100vw;
overflow: hidden;
color: #000;
}
- 增加renderer/src/layout文件夹,并添加index.vue
ts
<template>
<div class="layout">
<div class="left-bar">
<div class="nav-bar">
<div class="menu">
<div class="nav-item">
<el-icon style="padding-right: 6px"><Monitor /></el-icon>主页
</div>
</div>
</div>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
</template>
<script setup lang="ts">
import { Monitor } from '@element-plus/icons-vue'
import { ElIcon } from 'element-plus'
</script>
<style scoped lang="scss">
.layout {
display: flex;
height: 100vh;
display: flex;
.left-bar {
height: 100%;
flex-shrink: 0;
width: 120px;
background-color: #eaf4ff;
display: flex;
align-items: center;
flex-direction: column;
.nav-bar {
flex: 1;
width: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.nav-item {
margin-bottom: 8px;
height: 30px;
width: 100px;
display: flex;
font-size: 14px;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
&:hover {
background-color: #fff;
font-weight: bold;
color: #000;
}
}
.active {
background-color: #fff;
font-weight: bolder;
color: #000;
}
}
}
.content {
flex: 1;
}
}
</style>
- 增加renderer/src/views文件夹,并添加home文件夹
ts
// renderer/src/views/home/index.vue
<template>
<div class="home">欢迎来到主页</div>
</template>
<style scoped>
.home {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
好了,我们启动项目,如下就是改正确了:
主页我们实现了,那么我们如何实现登录窗体呢?
登录跳转实现
我们先分析下最初的主进程文件src/main/index.ts
ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1100,
height: 700,
minWidth: 1100, // 设置最小宽度
minHeight: 700, // 设置最小高度
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
app.whenReady().then(() => {
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
- 当应用准备就绪(
whenReady),首先监听'browser-window-created'事件,对每一个新创建的窗口应用快捷键优化。 - 调用
createWindow函数创建主窗口来加载渲染进程的UI界面。 - 应用激活时(macOS上的点击Dock图标),如果没有其他窗口存在,则再次创建一个新窗口。
- 当所有窗口关闭时,依据平台决定是否退出应用(在macOS上不自动退出)。
createWindow其实是一个关键,他是一个主窗体,那我们其实可以再建立一个loginwindow
新建一个函数createLoginWindow
ts
let loginWindow = null
function createLoginWindow(): void {
loginWindow = new BrowserWindow({
width: 1100,
height: 700,
minWidth: 1100, // 设置最小宽度
minHeight: 700, // 设置最小高度
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
loginWindow.on('ready-to-show', () => {
mainWindow.show()
})
loginWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
loginWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
loginWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
loginWindow.on("closed", () => {
loginWindow = null
})
}
- 根据上面图解,我们需要本地存储的: electron-settings(简单的用这个npm包)
- 我们需要暴露 登录和退出 方法给到渲染进程,来通知主进程切换该有的窗体
- 如何调用请求
传统的web应用就是一个渲染页面,可以直接在里面进行http请求等等,而且是有域名的,后端也可以设置cros黑白名单。
但是electron如果在渲染进程操作是极为不安全的且会跨域的,所以请求方法都是放在主进程,渲染进程聚是一个纯渲染ui,请求以及桌面内部操作都是放到了主进程来实现
基本的操作:(你可以理解为发布-订阅模式) 真实通信可以去看看:www.electronjs.org/docs/latest...
好了我们回到实现登录:
改造主进程文件
本地简单储存包:npm install electron-settings --save (复杂可以用用SQLite)
日志记录包: npm install electron-log --save
ts
// 简单改造下src/main/index.ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
const settings = require("electron-settings")
const log = require("electron-log")
let loginWindow:any
let mainWindow:any
function createLoginWindow() {
loginWindow = new BrowserWindow({
width: 500,
height: 600,
show: false,
resizable: false, // 禁止调整窗口大小
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false
}
})
loginWindow.on("ready-to-show", () => {
loginWindow.show()
})
loginWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: "deny" }
})
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
loginWindow.loadURL(process.env["ELECTRON_RENDERER_URL"])
} else {
loginWindow.loadFile(join(__dirname, "../renderer/index.html"))
}
loginWindow.on("closed", () => {
loginWindow = null
})
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1100,
height: 700,
minWidth: 1100,
minHeight: 700,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
// 方便调式
// is.dev && mainWindow.webContents.openDevTools()
mainWindow.on("closed", () => {
mainWindow = null
})
}
// 进程间的一些方法
function setupIpcSystemHandlers() {
ipcMain.handle("loginIn", async (event, params) => {
log.info("登录成功----params", params)
const { password, username } = params // 这里接受到渲染进程的数据
try {
// Todo 这里用axios调用请求来处理
let response = {
data: {
access_token: 'token__123433',
},
success: true,
}
log.info("登录成功----token", response.data)
if (response.data) {
settings.setSync("token", response.data.access_token) // 存到应用本地数据库
ipcMain.emit("main-login-success") // 通知登录成功了
} else {
return { message: '登录失败', success: false, data: null }
}
return response // 返回请求数据
} catch (error: any) {
log.error("Uncaught Exception:", error)
return { message: error.message, success: false, data: null } // 返回错误信息
}
})
ipcMain.handle("get-token", async () => {
const token = settings.get("token")
return token
})
}
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
const token = settings.getSync("token") // 获取到本地的token
if (token) {
loginWindow && loginWindow.close()
createMainWindow()
} else {
mainWindow && mainWindow.close() // 关
createLoginWindow()
}
// 监听登录成功事件
ipcMain.on("main-login-success", () => {
// 销毁登录窗口并创建主窗口
log.info("login-success")
if (loginWindow) {
loginWindow.close()
}
createMainWindow()
})
// 监听退出登录事件
ipcMain.on("main-logout-success", () => {
log.info("logout-success")
if (mainWindow) {
mainWindow.close()
}
createLoginWindow()
})
setupIpcSystemHandlers() // 加载进来一些进程方法
// 唤起判断
app.on("activate", function () {
const token = settings.getSync("token")
if (BrowserWindow.getAllWindows().length === 0) {
if (token) {
createMainWindow()
} else {
createLoginWindow()
}
}
})
// 监听第二个实例的打开事件
app.on("second-instance", () => {
// 如果已经打开了窗口,聚焦到它
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
改造渲染进程的文件
- 改造renderer/src/router/index.ts
ts
import { createRouter, createWebHistory } from 'vue-router'
const Home = () => import('@renderer/views/home/index.vue')
const Layout = () => import('@renderer/layout/index.vue')
const Login = () => import("@renderer/views/login/index.vue")
const routes = [
{
path: '/',
component: Layout, // 这个路由是主内容的
name: 'home',
meta: { requiresAuth: true },
children: [{ path: '', component: Home, name: 'index' }]
},
{
path: '/login', // 这个路由是登录界面的
name: 'login',
component: Login,
meta: { requiresAuth: false }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 加入全局前置守卫
router.beforeEach(async (to, from, next) => {
// 获取主进程存储在本地的token
const token = await window.electron.ipcRenderer.invoke('get-token')
// 如果目标路由需要认证且没有 token,重定向到登录页
if (to.meta.requiresAuth && !token) {
next({ name: 'login' })
}
// 如果已有 token 且要去登录页,重定向到首页
else if ( token && to.name === 'login') {
next({ name: 'home'})
}
else {
next() // 继续路由跳转
}
})
export default router
- 在renderer/views 增加login/index.vue页面
增加退出功能
xml
<template>
<div class="login-wrap">
<el-form
ref="ruleFormRef"
:model="loginForm"
:rules="loginRules"
label-width="80px"
class="login-form-wrap"
>
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名/手机号" v-model="loginForm.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
type="password"
v-model="loginForm.password"
placeholder="请输入密码"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" :loading="loading">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { ref, toRaw } from 'vue'
import { useRouter } from 'vue-router'
import type { FormInstance } from 'element-plus'
import { ElForm, ElFormItem, ElInput, ElButton,ElMessage} from 'element-plus'
const router = useRouter()
const loginForm = ref({
username: '',
password: ''
})
const loading = ref(false)
const ruleFormRef = ref<FormInstance>()
const loginRules = ref({
username: [
{ required: true, message: '请输入用户名/手机号' }
// { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
],
password: [{ required: true, message: '请输入密码' }]
})
const onSubmit = async () => {
if (!ruleFormRef.value) return
await ruleFormRef.value.validate(async (valid, fields) => {
if (valid) {
loading.value = true
const res = await window.electron.ipcRenderer.invoke('loginIn', toRaw(loginForm.value))
if (res.success) {
window.electron.ipcRenderer.send('main-login-success')
setTimeout(() => {
loading.value = false
window.electron.ipcRenderer.send('main-login-success')
router.push('/')
}, 500)
} else {
loading.value = false
ElMessage.error('登录失败,请检查用户名和密码')
}
} else {
loading.value = false
console.log('error submit!', fields)
}
})
}
</script>
<style scoped>
.login-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
position: relative;
.setting {
position: absolute;
right: 10px;
top: 5px;
width: 50px;
display: flex;
justify-content: flex-end;
cursor: pointer;
}
}
.login-form-wrap {
width: 350px;
padding: 35px;
border-radius: 5px;
}
</style>
- 改造renderer/src/layout/index.ts
ts
<template>
<div class="layout">
<div class="left-bar">
<div class="nav-bar">
<div class="menu">
<div class="nav-item">
<el-icon style="padding-right: 6px"><Monitor /></el-icon>主页
</div>
</div>
<div class="system">
<div class="nav-item" @click="handleLogout">
<el-icon style="padding-right: 6px"><Monitor /></el-icon>退登
</div>
</div>
</div>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { Monitor } from '@element-plus/icons-vue'
import { ElIcon } from 'element-plus'
const router = useRouter()
const handleLogout = async() => {
const res = await window.electron.ipcRenderer.invoke('logout')
if (res) {
router.push('/login')
}
}
</script>
<style scoped lang="scss">
.layout {
display: flex;
height: 100vh;
display: flex;
.left-bar {
height: 100%;
flex-shrink: 0;
width: 120px;
background-color: #eaf4ff;
display: flex;
align-items: center;
flex-direction: column;
.menu {
display: flex;
justify-content: space-between;
flex-direction: column;
}
.nav-bar {
flex: 1;
width: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.nav-item {
margin-bottom: 8px;
height: 30px;
width: 100px;
display: flex;
font-size: 14px;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
&:hover {
background-color: #fff;
font-weight: bold;
color: #000;
}
}
.active {
background-color: #fff;
font-weight: bolder;
color: #000;
}
}
}
.content {
flex: 1;
}
}
</style>
至此上面改造完成,重启项目
输入点击登录:
以上基本是一个完整的在electron实现登录和登出的项目初始
有错误之处请指正...
下一篇将讲解最佳实践的http请求以及一些ipc通信的最佳实践