【笔记】electron + react + antd

发布时间 2023-03-22 21:11:08作者: H·c

electron

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

react

React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。

antd

一个非常流行的前端UI框架

安装electron

npm install electron --save-dev

修改国内镜像

npm config set registry https://registry.npm.taobao.org

查询是否成功修改

npm config get registry

下载 cnpm 全局使用

npm install -g cnpm --registry=https://registry.npm.taobao.org
如果无法使用  cnpm 
1.进⼊node.js莫⽬录找到cnpm⽂件的位置将其移动到于npm⽂件的同⼀⽂件夹下
2.再将cnpm和cnpm.cmd⽂件移⾄npm与npm.cmd所在的⽂件夹即可解决问题

electron 配置

{
  "name": "electron_gui",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",   // 入口文件
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "electron": "electron ."   // 主electron 执行程序
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "electron": "^19.0.5"
  }
}

热加载

cnpm i --save-dev electron-reloader

main.js

const { app, BrowserWindow ,Menu } = require('electron')

// 热加载
const reloader = require('electron-reloader')
reloader(module)

// app 窗口启动
app.on('ready', ()=>{
    // 创建窗口指定高和宽度
    const mainWindow = new BrowserWindow({
        width:500,
        height:500
    })
    // 加载 html 文件  会渲染到 窗口内
    mainWindow.loadFile('./src/index.html')

    // 直接打开 调试 或者 ctrl+shift+i 、 mac 调试用 openDevTools
    mainWindow.webContents.openDevTools()

    // 自定义模板 - 菜单栏
    const template = [
        {
            label: '文件',
            submenu:[
                {
                    label: '新建窗口'
                }
            ],
        },
        {
            label: '关于'
        }
    ]
    // 编译模板
    const menu = Menu.buildFromTemplate(template)

    // 设置菜单
    Menu.setApplicationMenu(menu)
})

在窗口中开发调试面板

windows
ctrl+shift+i

mac
mainWindow.webContents.openDevTools()

通过 template 模板加载生成 menu 菜单

const template = [
    {
        lable: '文件',
        //  子菜单
        submenu:[
            {
                label: '新建窗口'    
            }
        ]
    },
    {
        lable: '关于'
    }
]

const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMeun(menu)

加载主进程的代码

const { remote: { BrowserWindow } } = require('electron')

在css 里面的拖拽事件

使用拖拽

-webkit-app-region: drag;

禁用拖拽

-webkit-app-region: no-drag;

const {remote:{BrowserWindow} , shell } = require('electron')

点击新建窗口, 创建新窗口
const newWindow = document.querySelector('.new-windon')
newWindow.onclick = function(){
    new BrowserWindow({
            width: 300,
            heigth: 300
        })
}

// 点击 a 标签跳转,shell 使用外部浏览器打开窗口
const allA = document.querySelectorAll('a')
allA.forEach(item =>{
    item.onclick = function(e){
        // 阻止默认事件
        e.preventDefault()
        shell.openExternal(item.herf)
    }
})

通过 frame:false 创建无边框窗口

渲染进程和主线程通信

定义按钮
<div class="maxWindow no-drag" onclick="maxWindow()"></div>

事件函数
function maxWindow(){
    ipcRenderer.send('max-window')
}

主线程定义事件
ipcMain.on('max-window',()=>{
    mainWindow.maximize()
})

传参
let windowSize = 'unmax-window'
function maxWindow(){
    windowSize = windowSize === 'max-window'?'unmax-window':'max-window'
    ipcRenderer.send('max-window', windowSize)
}

接受参数
ipcMain.on('max-window',(event, arg)=>{
    console.log(arg)
    if (arg === 'unmax-window') return mainWindow.maximize();
    mainWindow.unmaximize()
})

electron 打包

安装electron-packager
cnpm i electron-packager -D


在 package.json里面添加

"build": "electrion-packager ./ appname --platform=win32 --arch=x64 --out ./outApp --overwrite --icon=./log.ico"

react 要操作系统代码,需要通过node 的 child_process 的exec 执行命令。

需要和进程通信,渲染进程->主进程

不能用import引入进来,也不能直接用require,

以下三种方法都会导致报错:
import { ipcRenderer } from 'electron'
import electron from 'electron'
const electron = require('electrion')

是因为:

require/exports 和 import/export 形式不一样,遵循的模块化也不一样。

require/exports是一种野生的规范。

require/exports 的用法只有以下三种简单的写法:
const fs = require('fs')
exports.fs = fs
module.exports = fs

而 import/export 的写法就多种多样:

import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'

export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'

正确引入

const electron = window.require('electron');
const {ipcRenderer} = electron;
console.log(ipcRenderer)
ipcRenderer.send('ipcSignal','hello world!');

通过react 页面前端和底层系统交互 windows

官方例子
// On Windows Only...
const { spawn } = require('node:child_process');
const bat = spawn('cmd.exe', ['/c', 'my.bat']);

bat.stdout.on('data', (data) => {
  console.log(data.toString());
});

bat.stderr.on('data', (data) => {
  console.error(data.toString());
});

bat.on('exit', (code) => {
  console.log(`Child exited with code ${code}`);
});


// OR...
const { exec, spawn } = require('node:child_process');
exec('my.bat', (err, stdout, stderr) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(stdout);
});

// Script with spaces in the filename:
const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true });
// or:
exec('"my script.cmd" a b', (err, stdout, stderr) => {
  // ...
});

自己实现

页面按钮功能
const handleCmd = ()=>{
        // react -> nodejs -> system
        ipcRenderer.send("svnUpdate")
    }

ipcRenderer.on('readSvndata',(event,arg)=>{
    // arg 才是返回的数据
    console.log("event: ",event, arg)
    alert(arg)
})


election -> main.js
const { ipcMain } = require('electron')
const {exec} = require('child_process')

// 监听渲染进程  node执行exec函数
ipcMain.on('svnUpdate',(event,arg)=>{
    exec("svn up $PATH", (err, stdout, stderr) => {
        mainWindow.webContents.send('readSvndata',stdout)
    })
})

react + antd

先安装react - 18

npm install -g create-react-app

npm install -g react-dom

修改package.json配置文件的scripts

    // 指定electron入口文件
    "main": "main.js",
    "homepage": ".",
    "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    // 启动程序
    "electron": "electron ."
  },

再在根目录里安装 electron

npm install electron --save-dev

新建一个main.js 用于配置 electron

// 引入electron并创建一个Browserwindow
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')

// 保持window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭.
let mainWindow

function createWindow () {
//创建浏览器窗口,宽高自定义具体大小你开心就好
    mainWindow = new BrowserWindow({width: 800, height: 600})

    /*
     * 加载应用-----  electron-quick-start中默认的加载入口
      mainWindow.loadURL(url.format({
        pathname: path.join(__dirname, './build/index.html'),
        protocol: 'file:',
        slashes: true
      }))
    */
    // 加载应用----适用于 react 项目
    mainWindow.loadURL('http://localhost:3000/');

    // 打开开发者工具,默认不打开
    // mainWindow.webContents.openDevTools()

    // 关闭window时触发下列事件.
    mainWindow.on('closed', function () {
        mainWindow = null
    })
}

// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法
app.on('ready', createWindow)

// 所有窗口关闭时退出应用.
app.on('window-all-closed', function () {
    // macOS中除非用户按下 `Cmd + Q` 显式退出,否则应用与菜单栏始终处于活动状态.
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', function () {
    // macOS中点击Dock图标时没有已打开的其余应用窗口时,则通常在应用中重建一个窗口
    if (mainWindow === null) {
        createWindow()
    }
})

// 你可以在这个脚本中续写或者使用require引入独立的js文件.

热加载

cnpm i --save-dev electron-reloader

react

react 框架核心

reactDom 专门做渲染

index.css 全局样式

./App 引入根组件

ReactDom.render 渲染根组件APP 到root dom节点上

react 18 问题

严格模式关闭

列表渲染

使用map

遍历列表时同样需要一个类型为number/string不可重复的key,提高diff性能

key 仅在内部使用,不会出现在真实的dom结合中

{ var.ma    p( v=> <li key={v.id} > {v.name} </li>) }

jsx 条件渲染

可以用 三元运算符,逻辑&&运算

{ bool && <span>123</span> }

行内样式 在元素内绑定

<span style={{color:'red',fontSize:'30px'}} >123</span>

类名样式

const style = {color:'red',fontSize:'30px'}
<span style={style} >123</span>

// css 样式
<span className='active' >123</span>

jsx 注意事项

1. jsx 必须有一个根节点,如果没有根节点,可以使用<></>代替
2. 所有标签必须闭合
3. jsx中使用驼峰命名,className 
4. jsx 支持多行换行,如果需要换行,需要使用()包裹,防止bug

函数组件约定

1.组件名称必须大写,react内部会判断是组件还是普通标签
2.函数组件必须有返回值,不渲染则返回 null
3.组件像标签一样可以被渲染到页面内,组件表示一段结构内容,
4. 可以<Hello/> 也可以 <Hello> </Hello> 

类组件创建渲染

import react 
class HellComponent extends React.Component{
    reder(){
        return <div>test class</div>
    }
}

1.类名称必须大写字母开头
2.类组件应该继承React.Component父类,可以获取父类中的方法属性
3.类组件必须提供render方法render方法必须有返回值,表示UI结构

事件绑定

语法
on + 事件名称={事件处理程序}
如

总结

1.编写组件就是编写原生js类或者函数

2.定义状态必须通过state实例属性的方法提供一个对象,名称固定 state

3.修改state中的任何属性都不可以通过直接赋值必须走 setState方法,是因为继承得到的

4.这里的this关键词

组件通讯

父传子
1.父组件提供传递的数据 - state
2.给子组件标签'添加属性'值为state中的数据
3.子组件中通过props接受父组件中传过来的数据
 - 1.类组件使用this.props获取props对象
 - 2.函数式组件直接通过参数获取props对象


1.props是只读对象
根据单项数据流的要求,子组件只能读取props中的数据,不能进行修改
2.props可以传递任意数据
数值,字符串,布尔值,数组,对象,函数,jsx
子传父:子组件调用父组件传递过来的函数,并把想要传递的数据当成函数实参
1.先在父组件内创建函数
2.把函数传到子组件内
3.子组件内调用,即可把子组件的数据传递给父组件

跨组件通信 Context

1.创建Context对象,导出provider和Consumer对象
    const {Provider, Consumer} = createContext()
2.使用Provider包裹根组件提供数据
<Provider value={this.state.message}
    ...根组件
</Provider>

3.需要用到数据的组件使用Consumer包裹获取数据
<Consumer>    
    { value => 基于context值进行渲染 }
</Consumer>

生命周期

图示生命周期
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

constructor:
创建组件时,最先执行,初始化的时候只执行一次

render:
每次组件渲染都会触发,不能调用setState()操作,否则无限调用

componentDidMount:
组件挂载(完成DOM渲染)后执行,初始化的时候执行一次

hooks

使函数拥有自己的状态

为函数组件提供状态
只能在函数组件中使用

useState

1. 导入useState函数 react
2. 执行这个函数并且传入初始值必须在函数组件汇总
3. [数据, 修改数据的方法]
4. 使用数据 修改数据

const [count, setCount] = useState(0)
1. useState 传过来的参数 作为count的初始值
2. [count, setCount] 这里的写法是一个解构赋值,useState返回值是一个数组
   顺序不可换。第一参数就是数据状态,第二参数就是修改数据的方法
3.setCount函数作用用来修改count依旧保持不能直接修改原值还是生成新值替换原值
   setCount(基于原值计算得到的新值)
4.count和setCount是一对 是绑定一起的 setCount只能用来修改对应的count值

组件的更新
调用setCount的时候更新过程
首次渲染 组件内部代码会被执行一次
其中useState也会跟着执行,这里重点注意,初始值只在首次渲染时生效
更新渲染 setCount 都会更新
1. app组件会再次渲染 这个函数会再次执行
2. useState再次执行 得到的新的count值不是而是修改之后的1,模板会用新值
 useState初始值只要在此渲染生效,后续只要调用setCount整个app中代买都会执行
 此时的count每次拿到的都是最新值时的count每次拿到的都是最新值


useState 注意事项
1. 只能出现在函数中
2. 不能嵌套在if/for 其他函数中
    react 按照hooks的调用顺序识别每一个hook

useEffect

做副作用处理
1. 导入useEffect 函数
2. 子啊函数组件中执行,传入回调,并定义副作用
3. 当我们通过修改状态更新组件时,副作用也会不断执行

依赖项控制副作用的执行时机

1.默认状态(无依赖相)
组件初始化的时候先执行一次,等到每次数据修改组件更新再次执行
2. 添加一个空数组依赖性
组件初始化的时候执行一次
3. 依赖特定相
组件初始化的适合执行一次,依赖的特定项发生变化会再次执行
useEffect(()=>{
        document.title = count    
    },[count])


注意事项
useEffect 回调函数中用到的数据(比如,count)就是依赖数据
就应该出现在依赖数组中,如果不添加依赖性会出BUG
hook的出现,就是不用生命周期的概念也能实现业务代码

初始化 或者变量被修改时 都会执行


useEffect - 清理副作用

function Test(){
    useEffect(()=>{
        let timer = setInterval(()=>{
            console.log('定时器执行')
        },1000)
        return ()=>{
            // 清理动作
            clearInterval(timer)
        }
    },[])  // [] 代表初始化只执行一次
    return(
        <div>this is test!</div>
    )
}

发送网络请求

useEffect
1. 不加依赖性 - 初始化 + 重新渲染
2. 加[]  - 初始化一次
3. 加特定依赖项 [count, name] - 首次执行+任意一个值的变化执行

!!!真实DOM渲染之后才会执行useEffect !!!

在内部单独定义一个函数,然后把这个函数包装成同步
useEffect(()=>{
    async function fetchData(){
        const res = await axios.get('url')
        console.log(res)
},[])

可以写多次 useEffect

原生 fetch web api
搜索 fetch mdc
fetch('url')
  .then( response => response.json() )
  .then( data => console.log(data) )

useRef

在函数组件中获取真实的dom元素对象,或者是组件对象

1. 导入 useRef 函数
2. 执行 useRef 函数并出传入null,返回值为一个对象 
  内部有一个current属性存放拿到的DOM对象, (组件实例)
3. 通过ref绑定 要获取的元素或者组件

组件实例  类组件
dom对象  标签

import { useEffect, useRef } from 'react'
function App(){
    const h1Ref = useRef(null)
    useEffect(()=>{
        console.log(h1Ref)
    },[])
    return(
        <div>
            <h1 ref={ h1Ref }>这是h1标签</h1>
        </div>
    )
}

useConext

context如果要传递的数据 只需要在整个应用初始化的时候传递一次就可以
就可以选择在当前文件里做数据提供, index.js  静态的

如果Context需要传递数据并且将来还需要在数据做修改,底层组件也需要一起改变
需要写到app.js   , 动态的
useState
1.导入useState函数 react
2.执行这个函数并且传入初始值必须在函数组件汇总
3.[数据, 修改数据的方法]
4.使用数据 修改数据
状态读取和修改
const [count, setCount] = useState(0)
1. useState 传过来的参数 作为count的初始值
2. [count, setCount] 这里的写法是一个解构赋值,useState返回值是一个数组
 顺序不可换。第一参数就是数据状态,第二参数就是修改数据的方法
3.setCount函数作用用来修改count依旧保持不能直接修改原值还是生成新值替换原值
 setCount(基于原值计算得到的新值)
4.count和setCount是一对 是绑定一起的 setCount只能用来修改对应的count值

组件的更新
调用setCount的时候更新过程
首次渲染 组件内部代码会被执行一次
其中useState也会跟着执行,这里重点注意,初始值只在首次渲染时生效
更新渲染 setCount 都会更新
1. app组件会再次渲染 这个函数会再次执行
2. useState再次执行 得到的新的count值不是而是修改之后的1,模板会用新值
useState初始值只要在此渲染生效,后续只要调用setCount整个app中代买都会执行
此时的count每次拿到的都是最新值

useCallback 避免重复渲染 可以用在 webSocket

用于得到一个固定引用值的函数,通常用它进行性能优化

该函数有两个参数:
1.函数useCallBack会固定该函数的引用,只要依赖项没有发生改变,则始终返回之前函数的地址
2.数组,记录依赖项(类似于useEffect)

该函数返回:引用相对固定的函数地址

useLayoutEffect(布局副作用)

布局副作用

  • useEffect 在浏览器渲染完成 后 执行
  • useLayoutEffect 在浏览器渲染 前 执行

特点:

  • useLayoutEffect 总是比 useEffect 先执行
  • useLayoutEffect 里面的任务最好影响了Layout(布局)

代码演示

import React, {useState, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
    setN(i=>i+1)
  }
  useEffect(()=>{
    console.log("useEffect")
  })
  useLayoutEffect(()=>{ // 改成 useEffect 试试
    console.log("useLayoutEffect")
  })
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意:为了用户体验最好优先使用useEffect

项目开始

脚手架: create-react-app
钩子: react hooks
状态管理: mobx
UI: antd v4
ajax: axios
路由: react-router-dom 和 history
css:  sass
npx create-react-app appname

目录结构
store: 存放mobx  -> index.js
utils: 工具      -> index.js
style: 样式      -> index.css
hooks: 钩子
pages: 路由
components: 通用组件

SCSS预处理器
react支持sass
SASS 是一种预编译的CSS,作用类似于less 
由于react中内置处理了SASS的配置,所以在CRA创建项目中可以直接使用SASS写样式
安装  -  在dev 环境中使用
yarn add sass -D  



配置路由
yarn add react-router-dom
配置路由
Layout 和 Login

function Layout() {
    return(
        <div>layout</div>
    )
}
export default Layout


app.js
import {BrowserRouter, Routes, Route} from "react-router-dom"
import Layout from "./pages/Layout";
import Login from "./pages/Login";

function App() {
  return (
      // 路由配置
  <BrowserRouter>
    <div className="App">
      <Routes>
          {/*创建路由path和组件规则*/}
          <Route path='/' element={<Layout/>}>index</Route>
          <Route path='/login' element={<Login/>}>login</Route>
      </Routes>
    </div>
  </BrowserRouter>
  );
}

export default App;

antd

yarn add antd

index.js
// 导入这个包 以免报错
import 'antd/dist/antd.min.css'
// 最后导入样式文件 以免被覆盖
import './index.scss';

别名路径

能够配置@路径 简化路径处理
craco 配置文件
1. CRA将所有工程化配置,都隐藏在react-scripts包中,所以项目中看不到任何配置信息
2. 如果要修改CRA的默认配置
 - 通过第三方库来修改   比如  @craco/craco   推荐
 - 通过执行 yarn eject 命令,释放 react-scripts 中的所有配置到项目中
1.安装
yarn add -D @craco/craco

2.在根项目目录中创建craco的配置文件
craco.config.js
并在配置文件中配置路径别名

3.修改package.json 中的脚本命令
4. 在代码中就可以通过 @ 来表示src目录的绝对路径
5. restart 生效


//添加自定义 webpack配置
const path = require('path')

module.exports={
    //webpack
    webpack:{
        // 别名配置
        alias:{
            // 约定使用 @ 表示 src 文件所在路径
            '@': path.resolve(__dirname, 'src')

         }
    }
}


package.json 修改   react-scripts -> craco 
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",

remember

<Form
    initialValues={{
        remember: true,
    }}
>

<Form.Item
    name="remember"
    valuePropName="checked"
>

axios

拦截器
yarn add axios

mobx
yarn add mobx mobx-react-lite

跨域

yarn add  http-proxy-middleware

const proxy = require('http-proxy-middleware');

module.exports = function (app) {
  // const url = `http://pv.sohu.com/cityjson?ie=utf-8`;
  // let promise = http.get(url);
  // console.log(promise)
  app.use(proxy('/api/', {
    target: 'http://127.0.0.1:9001',
    changeOrigin: true,
    ws: true,
    // headers: {'X-Real-IP': promise['cip']},
    headers: {'X-Real-IP': "1.1.1.1"},
    pathRewrite: {
      '^/api': ''
    }
  }))
};

使用 application/x-www-form-urlencoded format
默认情况下,axios将JavaScript对象序列化为JSON。

要以application / x-www-form-urlencoded格式发送数据,您可以使用以下选项之一。

最简单实用qs内置编码

const qs = require('qs');
axios.post('url', qs.stringify({data}))

路由鉴权

1. 判断token 是否存在
2. 如果存在直接渲染
3. 如果不存在重定向到登录路由


高阶组件
把一个组件,当另外一个组件的参数传入
然后通过一定的判断,返回新组件

import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
function AuthComponent({ children }){
    const isToken = getToken()
    if (isToken){
        return <>{children}</>
    }eles{
        return <Navigate to="/login" replace />
    }
}

export {
    AuthComponent
}

useNavigate()

所有带 use 是钩子函数,也只能在函数组件内 使用,

安装history 包

yarn add history

要使用 cookie 需要安装包

yarn add react-cookies

import cookie from 'react-cookies'

//设置cookie,第三个参数的意思是所有页面都能用这个cookie
cookie.save(key,value,{path:"/"})
// 加载名为cookieName的cookie信息
cookie.load(cookieName)
// 删除名为cookieName的cookie信息
cookie.remove(cookieName)

mobx 6

yarn add mobx mobx-react-lite   # 仅限函数式组件使用

概念

observable  定义一个存储state的可追踪字段(Proxy)
action    将一个方法标记为可以修改的state的action
computed  标记一个可以由state派生出新值并且缓存其输出的计算属性

监听属性

autorun  函数接受一个函数作为参数,每次当函数所观察的值被发生变化时,他都运行
当自己创建autorun时,他只会运行一次
mobx会自动收集并订阅所有的可观察属性,一旦有改变发生,autorun将会再次触发

autorun(()=>{
    console.log('counter.count', counter.count)
})


reaction 的使用
reaction 类似于autorun 但可以让你更加精细地控制要跟踪的可观察对象
它接受两个函数作为参数
1。data函数,起返回值将会作为第二个函数输入
2。回调函数
与autorun不同,reaction在初始化时不会自动运行
reaction(
    ()=> count.count
    (v,oldv)=>{
        console.log('变化了',v,oldv)
    }
)

异步处理

异步进程中mobx中不需要任何特殊处理,因为不论是何时引发的所有reactions都将会自动更新
因为可观察是可变的,因此在action执行过程中保持对她们的引用一般是安全的
如果可观察对象的修改不是在action函数中,控制台会报警告