Electron+Reat Hooks 開發一個ToDoList桌面端程序

開始

安裝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 [email protected] --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 。

至於如何安裝組件庫可以自行查看:https://material-ui.com/zh/ge...

整體目錄結構如下圖:

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就完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章