Electron+Reat Hooks 开发一个ToDoList桌面端程序

1,474 阅读4分钟

开始

安装react脚手架并初始化项目

cnpm install -g create-react-app
create-react-app electron-react-today
cd electron-react-today
npm start

此时项目已经运行在 :localhost:3000

安装 electron

electron 7.0.0 实在太坑爹了

使用6.1.2没有问题。

cnpm install --save-dev electron@6.1.2 --verbose

新建main.js

// 引入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, 'index.html'),
      protocol: 'file:',
      slashes: true
    }))
  */
  // 加载应用----适用于 react 项目
  mainWindow.loadURL('http://localhost:3000/')
  
 // 加载本地html
 // mainWindow.loadFile('./index.html')   
    
  // 打开开发者工具,默认不打开
  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()
  }
})

启动项目

package.json文件中添加:

{
    "main": "main.js",
    "scripts": {
      "electron-start": "electron ."
  },
}

然后执行:

npm run electron-start

看到如下页面,终于第一步完成了。

K4qYc9.md.png

React Hooks

  • useState声明状态变量

    const [ count , setCount ] = useState(0);
    
  • useEffect代替常用生命周期函数

    // 第一次渲染和每次更新都会被执行 并且是异步执行的
    useEffect(()=>{
      console.log(`useEffect=>You clicked ${count} times`)
    })
    
    // 当传空数组[]时,就是当组件将被销毁时才进行解绑,
    // 这也就实现了componentWillUnmount的生命周期函数
    useEffect(()=>{
        console.log('useEffect=>老弟你来了!Index页面')
        return ()=>{
            console.log('老弟,你走了!Index页面')
        }
    },[])
    
  • useContext 实现数据共享(父子组件传值)

    可以通过这个hook传递useReducer产生的dispatch函数。

    也就是不直接传递数据,而是传递修改数据的方法,在根组件中通过reducer修改状态。

    父组件

    import React, { useState, createContext } from 'react';
    import { Button } from '@material-ui/core';
    // 引入子组件
    import Num from './Num';
    
    // 创建上下文对象
    const CounterContext = createContext();
    
    export default function Counter() {
        
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>点击了 {count} 次</p>
          <Button
            variant="contained"
            color="primary"
            onClick={() => setCount(count + 1)}
          >
            点我加一
          </Button>
          <CounterContext.Provider value={count}>
            <Num />
          </CounterContext.Provider>
        </div>
    );
    }
    
    // 导出上下文对象
    export { CounterContext };
    

    子组件

    import React, { useContext } from 'react';
    
    // 引入父组件的上下文
    import { CounterContext } from './Counter';
    
    export default function Count() {
      const count = useContext(CounterContext);
      return <h2>{count}</h2>
    }
    
  • useReducer 实现对复杂状态对象的管理

    使用场景

    对某个state有很多种操作

    子组件需要修改上层组件的值,可以传递一个dispatch函数

    const reducer = (state, action) => {
        switch(action.type) {
           case: "xx":
              ....
        }
    }
    
    const [state, dispatch] = useReducer(/*reducer函数*/ reducer, /*初始值*/ initialVal);
    
    dispatch({ type: 'xx', val: '' });
    

当然还有其他的Hooks,例如用于性能优化的useMemo就不说了。

ToDoList 应用

有了以上内容就可以开发一个简单的TODoList应用了。

K4qJ1J.png

使用到的内容:material-ui 和 上文中的React Hooks 。

至于如何安装组件库可以自行查看:material-ui.com/zh/getting-…

整体目录结构如下图:

K4qa0x.png

App.js 文件: 承载状态数据和组件。

import React, { useReducer, createContext } from 'react';
import { initialTodos, filterReducer, todosReducer } from './reducer/index';

import Filter from './components/Filter';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';

// 导出共享对象
export const AppContext = createContext();

function App() {
  const [todos, dispatchTodos] = useReducer(todosReducer, initialTodos);
  const [filterVal, dispatchFilter] = useReducer(filterReducer, 'ALL');

  return (
    <div style={{ margin: '20px 30px 0', maxWidth: 450 }}>
      <AppContext.Provider value={dispatchTodos}>
        <AddTodo />
        <Filter dispatch={dispatchFilter} />
        <TodoList filterVal={filterVal} todos={todos} />
      </AppContext.Provider>
    </div>
  );
}

export default App;

/reducer/index.js 文件: 完成对数据的修改。

import uuid from 'uuid';

export const initialTodos = [
  {
    id: uuid(),
    label: '学习React Hooks',
    complete: false,
  }, {
    id: uuid(),
    label: '吃饭睡觉',
    complete: true,
  }
];

export const filterReducer = (state, action) => {
  switch (action.type) {
    case 'SHOW_ALL':
      return 'ALL';
    case 'SHOW_COMPLETE':
      return 'COMPLETE';
    case 'SHOW_INCOMPLETE':
      return 'INCOMPLETE';
    default:
      throw Error();
  }
}

export const todosReducer = (state, action) => {
  switch (action.type) {
    case 'CHECK_TODO':
      return state.map(todo => {
        if (todo.id === action.id) {
          todo.complete = !todo.complete
        }
        return todo;
      });
    case 'DELETE_TODO':
      console.log(state, action.id)
      return state.filter(todo => todo.id !== action.id);
    case 'ADD_TODO':
      return state.concat(action.todo);
    default:
      throw Error();
  }
}

/components/AddTodo.js: 输入框,添加任务。

import React, { useState, useContext } from 'react';
import { Input, Button } from '@material-ui/core';
import uuid from 'uuid';
import { AppContext } from '../App';

export default function AddTodo() {

  const dispatch = useContext(AppContext);

  const handleSubmit = () => {
    if (!task) return;
    setTask('');
    dispatch({ type: 'ADD_TODO', todo: { id: uuid(), label: task, complete: false } });
  }

  const [task, setTask] = useState('');

  return (
    <div style={{ display: 'flex' }}>
      <Input
        value={task}
        style={{ flex: 1 }}
        onChange={(e) => setTask(e.target.value)}
        inputProps={{ 'aria-label': 'description' }}
      />
      <Button color="primary" onClick={handleSubmit}>添加</Button>
    </div>
  )
}

/components/Filter.js: 筛选任务。

import React from 'react';
import { Button } from '@material-ui/core';

export default function Filter({ dispatch }) {

  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', margin: '20px 0' }}>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_ALL' })}>全部</Button>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_COMPLETE' })}>已完成</Button>
      <Button color="primary" onClick={() => dispatch({ type: 'SHOW_INCOMPLETE' })}>未完成</Button>
    </div>
  )
}

/components/TodoList.js: 展示所有的任务。

import React, { useContext } from 'react';
import { List, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Checkbox, ListItemIcon } from '@material-ui/core';
import { Delete as DeleteIcon } from '@material-ui/icons';
import { AppContext } from '../App'

export default function TodoList({ todos, filterVal }) {

  const dispatch = useContext(AppContext);

  const deleteTodo = (item) => {
    dispatch({ type: 'DELETE_TODO', id: item.id });
  }

  const checkTodo = (item) => {
    dispatch({ type: 'CHECK_TODO', id: item.id });
  }

  // 过滤 todos
  const filteredTodos = () => {
    if (filterVal === 'ALL') return todos;
    if (filterVal === 'COMPLETE') {
      return todos.filter(todo => todo.complete);
    }
    if (filterVal === 'INCOMPLETE') {
      return todos.filter(todo => !todo.complete);
    }
    return [];
  }

  return (
    <List component="nav" aria-label="secondary mailbox folders">
      {
        filteredTodos().map(item => (
          <ListItem key={item.id} button>
            <ListItemIcon>
              <Checkbox
                edge="start"
                checked={item.complete}
                onChange={() => checkTodo(item)}
                disableRipple
              />
            </ListItemIcon>
            <ListItemText primary={item.label} />
            <ListItemSecondaryAction>
              <IconButton onClick={() => deleteTodo(item)} edge="end" aria-label="delete">
                <DeleteIcon />
              </IconButton>
            </ListItemSecondaryAction>
          </ListItem>
        ))
      }
    </List>
  )
}

至此,一个简单的ToDoList就完成了。