雨水-electron项目实战登录

雨水象征着生机活力和丰沃繁茂,随着气温的回升和降水的增多,植物开始生长,动物也开始活跃起来,大自然呈现出一片生机勃勃的景象。这代表着春天的到来,万物复苏,充满了生机和活力

上一篇:立春-如何初始化electron项目 已经初始化electron+vue3的项目

这一篇文章将实战登录(类似钉钉)多窗口切换登录

设计初稿分析

分析:

  • 登录窗体大致: 500*600尺寸
  • 登录后的内容页窗体: 1100*700尺寸
  • electron应用如何请求接口登录?
  • 如何保持登录,打开app会直接到登录后到内容页?
  • 登录后是如何销毁前面一个窗体的?
  • 退出登录后如何返回登录窗体?

上面这些会在下面的讲解和实现中一步步教会你...

开发准备

参考前文实现初始化:juejin.cn/post/748043... 如果是react,请用react的社区的npm包

  1. 从登录页 ---> 主页: 我们是需要vue-router
  2. UI 我们选用element-plus
npm 复制代码
    npm install vue-router --save
    npm install element-plus --save
    npm install -D sass-embedded
    npm install normalize.css --save

建立路由

路由配置

  1. 在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
   

改造文件

  1. 改造renderer/src/App.vue
ts 复制代码
    <template>
      <RouterView />
    </template>

    <script setup lang="ts">
    import { RouterView } from 'vue-router'
    </script>
  1. 改造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')
  1. 改造renderer/assets/main.css
css 复制代码
@import 'normalize.css';

#root {
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  color: #000;
}
  1. 增加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>
  1. 增加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
  })
}
  1. 根据上面图解,我们需要本地存储的: electron-settings(简单的用这个npm包)
  2. 我们需要暴露 登录和退出 方法给到渲染进程,来通知主进程切换该有的窗体
  3. 如何调用请求

传统的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()
  }
})
  

改造渲染进程的文件

  1. 改造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
  1. 在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>

 
  1. 改造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通信的最佳实践

相关推荐
代码猎人2 分钟前
如何实现一个三角形
前端
龙国浪子2 分钟前
从点到线,从线到画:Canvas 画笔工具的实现艺术
前端·electron
代码猎人3 分钟前
什么是margin重叠,如何解决
前端
TeamDev5 分钟前
使用 Vue.js 构建 Java 桌面应用
java·前端·vue.js
DongHao5 分钟前
跨域问题及解决方案
前端·javascript·面试
持续升级打怪中6 分钟前
Vue项目中Axios全面封装实战指南
前端·javascript·vue.js
heyCHEEMS7 分钟前
为什么放弃 v-if 选择 v-show?为什么组件越用越卡?
前端
百罹鸟9 分钟前
【react 高频面试题—核心原理篇】:useEffect 的依赖项如果是数组或对象(引用类型),会有什么问题?如何解决?
前端·react.js·面试
hibear10 分钟前
Smart Ticker - 支持任意字符的高性能文本差异动画滚动组件
前端·vue.js·react.js
脱氧核糖核酸10 分钟前
2026了你还只会写点prompt?从AI提示词到可控自动化的演进之路
前端