Redux 基本原理,第 5 部分:UI 和 React
- Redux 儲存如何與 UI 合作
- 如何將 Redux 與 React 搭配使用
簡介
在 第 4 部分:儲存 中,我們看過如何建立 Redux 儲存、發送動作和讀取目前狀態。我們也看過儲存如何在內部運作、增強器和中間件如何讓我們自訂儲存以獲得額外的功能,以及如何加入 Redux DevTools 讓我們在發送動作時看到應用程式內部發生了什麼事。
在這個區段,我們將為待辦事項應用程式新增使用者介面。我們將了解 Redux 如何與 UI 層整體運作,並特別說明 Redux 如何與 React 共同運作。
請注意,此頁面和所有「基礎」教學課程都教導如何使用 我們現代化的 React-Redux 鉤子 API。舊式的 connect
API 仍然有效,但我們希望所有 Redux 使用者都能使用鉤子 API。
此外,本教學課程中的其他頁面故意顯示較舊式的 Redux 邏輯模式,這些模式需要比我們教導的「現代 Redux」模式更多的程式碼,而後者搭配 Redux Toolkit,是我們教導的用 Redux 建置應用程式的正確方法,目的是說明 Redux 背後的原則和概念。
請參閱 「Redux 基礎」教學課程,了解「如何正確使用 Redux」的完整範例,其中包含 Redux Toolkit 和 React-Redux 鉤子,適用於實際應用程式。
將 Redux 與 UI 整合
Redux 是獨立的 JS 函式庫。正如我們已經看到的,即使您沒有設定使用者介面,您也可以建立和使用 Redux 儲存。這也表示您可以將 Redux 與任何 UI 架構一起使用(甚至沒有任何 UI 架構),並在客戶端和伺服器上使用它。您可以使用 React、Vue、Angular、Ember、jQuery 或純粹的 JavaScript 編寫 Redux 應用程式。
話雖如此,Redux 是專門設計用來與 React 搭配使用。React 讓您可以將您的 UI 描述成您狀態的函數,而 Redux 包含狀態並根據動作更新狀態。
因此,我們將在這個教學課程中使用 React 來建置我們的待辦事項應用程式,並說明如何將 React 與 Redux 一起使用的基礎知識。
在我們開始之前,讓我們快速了解 Redux 如何與 UI 層互動。
Redux 與 UI 整合基礎
將 Redux 與任何 UI 層搭配使用,需要幾個一致的步驟
- 建立 Redux 儲存庫
- 訂閱更新
- 在訂閱回呼函式內
- 取得目前的儲存庫狀態
- 擷取此 UI 區段所需的資料
- 使用資料更新 UI
- 如有必要,使用初始狀態呈現 UI
- 透過發送 Redux 動作,回應 UI 輸入
讓我們回到 第 1 部分中看到的計數器應用程式範例,看看它是如何遵循這些步驟的
// 1) Create a new Redux store with the `createStore` function
const store = Redux.createStore(counterReducer)
// 2) Subscribe to redraw whenever the data changes in the future
store.subscribe(render)
// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')
// 3) When the subscription callback runs:
function render() {
// 3.1) Get the current store state
const state = store.getState()
// 3.2) Extract the data you want
const newValue = state.value.toString()
// 3.3) Update the UI with the new value
valueEl.innerHTML = newValue
}
// 4) Display the UI with the initial store state
render()
// 5) Dispatch actions based on UI inputs
document.getElementById('increment').addEventListener('click', function () {
store.dispatch({ type: 'counter/incremented' })
})
無論您使用哪個 UI 層,Redux 與每個 UI 的運作方式都相同。實際的實作通常會稍微複雜一些,以協助最佳化效能,但每次的步驟都是相同的。
由於 Redux 是獨立的函式庫,因此有不同的「繫結」函式庫可以協助您將 Redux 與特定 UI 架構搭配使用。這些 UI 繫結函式庫會處理訂閱儲存庫和在狀態變更時有效率地更新 UI 的詳細資料,讓您不必自己撰寫這些程式碼。
將 Redux 與 React 搭配使用
官方的 React-Redux UI 繫結函式庫 是獨立於 Redux 核心的一個套件。您需要另外安裝它
npm install react-redux
在本教學課程中,我們將介紹您需要將 React 和 Redux 搭配使用的最重要的模式和範例,並了解它們如何在我們的待辦事項應用程式中實際運作。
請參閱 https://react-redux.dev.org.tw 上的官方 React-Redux 文件,以取得如何將 Redux 和 React 搭配使用的完整指南,以及 React-Redux API 的參考文件。
設計元件樹
就像我們 根據需求設計狀態結構 一樣,我們也可以設計整體的 UI 元件組,以及它們在應用程式中彼此的關聯方式。
根據 應用程式的商業需求清單,我們至少需要這組元件
<App>
:呈現所有其他內容的根元件。<Header>
:包含「新增待辦事項」文字輸入和「完成所有待辦事項」核取方塊<TodoList>
:一個清單,顯示所有目前可見的待辦事項,根據篩選結果<TodoListItem>
:一個單一的待辦事項,有一個可以點擊的核取方塊,用於切換待辦事項的完成狀態,以及一個顏色類別選擇器
<Footer>
:顯示目前待辦事項的數量,以及用於根據完成狀態和顏色類別篩選清單的控制項
除了這個基本的元件結構之外,我們可以潛在用許多不同的方式來區分元件。例如,<Footer>
元件可以是一個較大的元件,或者它可以在內部包含多個較小的元件,例如 <CompletedTodos>
、<StatusFilter>
和 <ColorFilters>
。沒有單一的正確方式來區分這些元件,你會發現根據你的情況,寫較大的元件或將事物分成許多較小的元件會更好。
現在,我們將從這個小清單的元件開始,讓事情更容易追蹤。關於這一點,由於我們假設你已經知道 React,我們將跳過如何為這些元件撰寫佈局程式碼的詳細資訊,並專注於如何在 React 元件中實際使用 React-Redux 函式庫。
在我們開始新增任何與 Redux 相關的邏輯之前,以下是這個應用程式的初始 React UI
使用 useSelector
從儲存區讀取狀態
我們知道我們需要能夠顯示待辦事項清單。讓我們從建立一個 <TodoList>
元件開始,這個元件可以從儲存區讀取待辦事項清單,逐一迴圈,並為每個待辦事項項目顯示一個 <TodoListItem>
元件。
你應該熟悉React 勾子,例如 useState
,可以在 React 函式元件中呼叫它們,讓它們可以存取 React 狀態值。React 也讓我們撰寫自訂勾子,讓我們可以擷取可重複使用的勾子,在 React 內建勾子之上新增我們自己的行為。
與許多其他函式庫一樣,React-Redux 包含它自己的自訂勾子,你可以在自己的元件中使用它們。React-Redux 勾子讓你的 React 元件能夠透過讀取狀態和傳送動作,與 Redux 儲存區對話。
我們將探討的第一個 React-Redux 鉤子是 useSelector
鉤子,它讓你的 React 元件能從 Redux 儲存庫讀取資料。
useSelector
接受一個函式,我們稱之為選擇器函式。選擇器是一個函式,它將整個 Redux 儲存庫狀態作為其參數,從狀態中讀取一些值,並傳回該結果。
例如,我們知道我們的待辦事項應用程式的 Redux 狀態將待辦事項陣列儲存在 state.todos
中。我們可以撰寫一個小選擇器函式來傳回該待辦事項陣列
const selectTodos = state => state.todos
或者,我們可能想找出目前標記為「已完成」的待辦事項數量
const selectTotalCompletedTodos = state => {
const completedTodos = state.todos.filter(todo => todo.completed)
return completedTodos.length
}
因此,選擇器可以傳回 Redux 儲存庫狀態中的值,也可以傳回基於該狀態的衍生值。
讓我們將待辦事項陣列讀取到我們的 <TodoList>
元件中。首先,我們將從 react-redux
函式庫匯入 useSelector
鉤子,然後以一個選擇器函式作為其參數呼叫它
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodos = state => state.todos
const TodoList = () => {
const todos = useSelector(selectTodos)
// since `todos` is an array, we can loop over it
const renderedListItems = todos.map(todo => {
return <TodoListItem key={todo.id} todo={todo} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
export default TodoList
<TodoList>
元件第一次渲染時,useSelector
鉤子將呼叫 selectTodos
並傳入整個 Redux 狀態物件。選擇器傳回的任何值都將由鉤子傳回給你的元件。因此,我們元件中的 const todos
最終將包含我們的 Redux 儲存庫狀態中的相同 state.todos
陣列。
但是,如果我們發送一個像 {type: 'todos/todoAdded'}
的動作會發生什麼事?Redux 狀態將由簡約器更新,但我們的元件需要知道有些東西已經改變,以便它能使用新的待辦事項清單重新渲染。
我們知道我們可以呼叫 store.subscribe()
來偵聽儲存庫的變更,因此我們可以嘗試在每個元件中撰寫訂閱儲存庫的程式碼。但是,那很快就會變得非常重複且難以處理。
幸運的是,useSelector
會自動為我們訂閱 Redux 儲存庫!這樣,任何時候發送一個動作,它都會立即再次呼叫其選擇器函式。如果選擇器傳回的值與上次執行時不同,useSelector
將強制我們的元件使用新資料重新渲染。我們只需要在我們的元件中呼叫 useSelector()
一次,它就會為我們完成其餘的工作。
但是,這裡有一件非常重要的事情需要記住
useSelector
使用嚴格的 ===
參考比較來比較其結果,因此只要選擇器結果是新的參考,元件就會重新渲染!這表示,如果你在選擇器中建立新的參考並傳回它,你的元件可能會在每次動作被傳送時重新渲染,即使資料實際上沒有不同。
例如,將此選擇器傳遞給 useSelector
會導致元件總是重新渲染,因為 array.map()
總是傳回新的陣列參考
// Bad: always returning a new reference
const selectTodoDescriptions = state => {
// This creates a new array reference!
return state.todos.map(todo => todo.text)
}
我們將在本文稍後討論解決此問題的方法之一。我們還將討論如何使用「備忘」選擇器函式來改善效能並避免不必要的重新渲染,請參閱第 7 部分:標準 Redux 模式。
值得注意的是,我們不必將選擇器函式寫成一個獨立的變數。你可以直接在呼叫 useSelector
時寫入選擇器函式,如下所示
const todos = useSelector(state => state.todos)
使用 useDispatch
傳送動作
我們現在知道如何將資料從 Redux 儲存庫讀取到元件中。但是,我們如何從元件向儲存庫傳送動作?我們知道,在 React 之外,我們可以呼叫 store.dispatch(action)
。由於我們無法在元件檔案中存取儲存庫,因此我們需要一些方法才能在元件內部存取 dispatch
函式本身。
React-Redux useDispatch
勾子將儲存庫的 dispatch
方法作為其結果傳遞給我們。(事實上,勾子的實作確實是 return store.dispatch
。)
因此,我們可以在需要傳送動作的任何元件中呼叫 const dispatch = useDispatch()
,然後視需要呼叫 dispatch(someAction)
。
讓我們在我們的 <Header>
元件中嘗試這樣做。我們知道我們需要讓使用者輸入一些文字作為新的待辦事項,然後傳送包含該文字的 {type: 'todos/todoAdded'}
動作。
我們將撰寫一個典型的 React 表單元件,它使用「受控輸入」讓使用者輸入表單文字。然後,當使用者特別按下 Enter 鍵時,我們將傳送該動作。
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const handleChange = e => setText(e.target.value)
const handleKeyDown = e => {
const trimmedText = e.target.value.trim()
// If the user pressed the Enter key:
if (e.key === 'Enter' && trimmedText) {
// Dispatch the "todo added" action with this text
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
// And clear out the text input
setText('')
}
}
return (
<input
type="text"
placeholder="What needs to be done?"
autoFocus={true}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
)
}
export default Header
使用 Provider
傳遞 Store
我們的元件現在可以從 Store 讀取狀態,並將動作傳送至 Store。不過,我們仍然缺少了一些東西。React-Redux 勾子在哪裡以及如何找到正確的 Redux Store?勾子是一個 JS 函式,因此它無法自動從 store.js
匯入 Store。
相反地,我們必須明確告訴 React-Redux 我們要在元件中使用哪個 Store。我們透過在整個 <App>
周圍呈現一個 <Provider>
元件,並將 Redux Store 作為一個 prop 傳遞給 <Provider>
來執行此操作。我們執行此操作一次後,應用程式中的每個元件都可以在需要時存取 Redux Store。
讓我們將其新增至我們的 index.js
主檔案
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.render(
// Render a `<Provider>` around the entire `<App>`,
// and pass the Redux store to it as a prop
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
這涵蓋了將 React-Redux 與 React 搭配使用的主要部分
- 呼叫
useSelector
勾子在 React 元件中讀取資料 - 呼叫
useDispatch
勾子在 React 元件中傳送動作 - 在整個
<App>
元件周圍放置<Provider store={store}>
,以便其他元件可以與 Store 對話
我們現在應該可以實際與應用程式互動!以下是目前運作中的 UI
現在,讓我們看看我們可以在待辦事項應用程式中一起使用這些方法的幾種方式。
React-Redux 模式
全域狀態、元件狀態和表單
到目前為止,你可能會想:「我是否必須始終將我應用程式的全部狀態放入 Redux Store?」
答案是否。應用程式中需要的所有全域狀態都應該放入 Redux Store。只在一個地方需要的狀態應該保留在元件狀態中。
一個很好的例子就是我們之前撰寫的 <Header>
元件。我們可以透過在輸入的 onChange
處理常式中傳送動作並將其保留在我們的 reducer 中,將目前的文字輸入字串保留在 Redux Store 中。但是,這對我們沒有任何好處。文字字串唯一使用的地方是在 <Header>
元件中。
因此,將該值保留在 <Header>
元件中的 useState
勾子中是有道理的。
類似地,如果我們有一個名為 isDropdownOpen
的布林旗標,應用程式中的其他元件都不會在意它 - 它應該真正保留在此元件中。
在 React + Redux 應用程式中,你的全域狀態應該放入 Redux Store,而你的本地狀態應該保留在 React 元件中。
如果您不確定將資料放置在哪裡,以下是一些常見的經驗法則,用於確定應將哪種類型的資料放入 Redux
- 應用程式的其他部分是否關心這些資料?
- 您是否需要能夠根據這些原始資料建立進一步的衍生資料?
- 是否使用相同的資料來驅動多個元件?
- 您是否重視能夠將此狀態還原到某個時間點(例如,時間旅行除錯)?
- 您是否要快取資料(例如,如果資料已存在,則使用狀態中的資料,而不是重新要求資料)?
- 您是否要在熱重載 UI 元件時保持此資料一致(交換時可能會遺失其內部狀態)?
這也是一個很好的範例,說明如何思考 Redux 中的表單。大部分表單狀態可能不應該保留在 Redux 中。相反地,在編輯表單元件時將資料保留在表單元件中,然後在使用者完成時發送 Redux 動作來更新儲存。
在元件中使用多個選取器
目前只有我們的 <TodoList>
元件正在從儲存中讀取資料。讓我們看看 <Footer>
元件開始讀取一些資料的樣子。
<Footer>
需要知道三種不同的資訊
- 已完成待辦事項的數量
- 目前的「狀態」篩選器值
- 目前選取的「顏色」類別篩選器清單
我們如何將這些值讀取到元件中?
我們可以在一個元件中多次呼叫 useSelector
。事實上,這是一個好主意 - 每次呼叫 useSelector
都應該始終傳回最少量的狀態。
我們已經看過如何撰寫計算已完成待辦事項的選取器。對於篩選器值,狀態篩選器值和顏色篩選器值都存在於 state.filters
片段中。由於此元件需要兩個值,因此我們可以選取整個 state.filters
物件。
正如我們前面提到的,我們可以將所有輸入處理直接放入 <Footer>
,或者我們可以將其拆分為 <StatusFilter>
等個別元件。為了讓這個說明更簡短,我們將略過撰寫輸入處理的確切詳細資訊,並假設我們有較小的個別元件,這些元件會提供一些資料和變更處理常式作為道具。
根據此假設,元件的 React-Redux 部分可能如下所示
import React from 'react'
import { useSelector } from 'react-redux'
import { availableColors, capitalize } from '../filters/colors'
import { StatusFilters } from '../filters/filtersSlice'
// Omit other footer components
const Footer = () => {
const todosRemaining = useSelector(state => {
const uncompletedTodos = state.todos.filter(todo => !todo.completed)
return uncompletedTodos.length
})
const { status, colors } = useSelector(state => state.filters)
// omit placeholder change handlers
return (
<footer className="footer">
<div className="actions">
<h5>Actions</h5>
<button className="button">Mark All Completed</button>
<button className="button">Clear Completed</button>
</div>
<RemainingTodos count={todosRemaining} />
<StatusFilter value={status} onChange={onStatusChange} />
<ColorFilters value={colors} onChange={onColorChange} />
</footer>
)
}
export default Footer
透過 ID 選取清單項目中的資料
目前,我們的 <TodoList>
正在讀取整個 state.todos
陣列,並將實際的 todo 物件作為 prop 傳遞給每個 <TodoListItem>
元件。
這會運作,但存在潛在效能問題。
- 變更一個 todo 物件表示要建立 todo 及
state.todos
陣列的副本,而且每個副本都是記憶體中的新參考。 - 當
useSelector
視其結果為新參考時,它會強制其元件重新渲染。 - 因此,任何時候更新 一個 todo 物件(例如按一下它以切換其已完成狀態),整個
<TodoList>
父元件都會重新渲染。 - 接著,因為 React 預設會遞迴重新渲染所有子元件,這也表示 所有
<TodoListItem>
元件都會重新渲染,即使其中大部分實際上根本沒有變更!
重新渲染元件並非壞事,這是 React 知道是否需要更新 DOM 的方式。但是,如果清單太大,在實際上沒有變更任何內容時重新渲染大量元件可能會變得太慢。
有幾種方法可以嘗試修復此問題。一種選擇是將所有 <TodoListItem>
元件包覆在 React.memo()
中,這樣它們只會在其 prop 實際變更時重新渲染。這通常是改善效能的良好選擇,但它確實要求子元件在實際變更任何內容之前始終接收相同的 prop。由於每個 <TodoListItem>
元件都接收一個 todo 項目作為 prop,因此其中只有一個應該實際取得已變更的 prop 並必須重新渲染。
另一種選擇是讓 <TodoList>
元件僅從儲存區讀取 todo ID 陣列,並將這些 ID 作為 prop 傳遞給子 <TodoListItem>
元件。然後,每個 <TodoListItem>
都可以使用該 ID 找出它需要的正確 todo 物件。
讓我們試試看。
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodoIds = state => state.todos.map(todo => todo.id)
const TodoList = () => {
const todoIds = useSelector(selectTodoIds)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
這次,我們只在 <TodoList>
中從儲存區選取 todo ID 陣列,並將每個 todoId
作為 id
prop 傳遞給子 <TodoListItem>
。
接著,在 <TodoListItem>
中,我們可以使用那個 ID 值來讀取我們的待辦事項。我們也可以更新 <TodoListItem>
,根據待辦事項的 ID 來發送「已切換」動作。
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { availableColors, capitalize } from '../filters/colors'
const selectTodoById = (state, todoId) => {
return state.todos.find(todo => todo.id === todoId)
}
// Destructure `props.id`, since we only need the ID value
const TodoListItem = ({ id }) => {
// Call our `selectTodoById` with the state _and_ the ID value
const todo = useSelector(state => selectTodoById(state, id))
const { text, completed, color } = todo
const dispatch = useDispatch()
const handleCompletedChanged = () => {
dispatch({ type: 'todos/todoToggled', payload: todo.id })
}
// omit other change handlers
// omit other list item rendering logic and contents
return (
<li>
<div className="view">{/* omit other rendering output */}</div>
</li>
)
}
export default TodoListItem
不過,這裡有一個問題。我們之前說過,在選擇器中傳回新的陣列參考會導致元件每次都重新渲染,而我們現在在 <TodoList>
中傳回一個新的 ID 陣列。在這種情況下,如果我們切換待辦事項,ID 陣列的內容應該會相同,因為我們仍然顯示相同的待辦事項項目,我們沒有新增或刪除任何項目。但是,包含這些 ID 的陣列是一個新的參考,因此 <TodoList>
會在實際上不需要時重新渲染。
解決這個問題的其中一種方法是變更 useSelector
比較其值的方式,以查看它們是否已變更。useSelector
可以將比較函式作為其第二個引數。比較函式會使用舊值和新值呼叫,如果它們被視為相同,則傳回 true
。如果它們相同,useSelector
就不會讓元件重新渲染。
React-Redux 有個 shallowEqual
比較函式,我們可以使用它來檢查陣列內部的項目是否仍然相同。讓我們試試看
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodoIds = state => state.todos.map(todo => todo.id)
const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
現在,如果我們切換待辦事項項目,ID 清單將被視為相同,而 <TodoList>
不必重新渲染。單一的 <TodoListItem>
會取得更新的待辦事項物件並重新渲染,但其他所有項目仍會有現有的待辦事項物件,而且完全不必重新渲染。
如前所述,你也可以使用一種稱為 「暫存選擇器」 的特殊選擇器函式,以協助改善元件渲染,我們將在其他章節中探討如何使用它們。
你已學會的內容
我們現在有一個可用的待辦事項應用程式!我們的應用程式會建立一個儲存庫,使用 <Provider>
將儲存庫傳遞給 React UI 層,然後在我們的 React 元件中呼叫 useSelector
和 useDispatch
來與儲存庫對話。
試試自己實作其餘遺失的 UI 功能!以下是你需要新增的事項清單
- 在
<TodoListItem>
元件中,使用useDispatch
勾子來發送動作,以變更顏色類別並刪除待辦事項 - 在
<Footer>
中,使用useDispatch
勾子來發送動作,以將所有待辦事項標示為已完成、清除已完成的待辦事項,以及變更篩選值。
我們將在 第 7 部分:標準 Redux 模式 中介紹如何實作篩選。
讓我們看看現在應用程式的樣子,包括我們跳過的元件和章節,以保持簡潔
- Redux 商店可以用於任何 UI 層
- UI 程式碼總是訂閱商店,取得最新狀態,並重新繪製自身
- React-Redux 是 React 的官方 Redux UI 繫結函式庫
- React-Redux 安裝為一個獨立的
react-redux
套件
- React-Redux 安裝為一個獨立的
useSelector
鉤子讓 React 元件可以從商店讀取資料- 選擇器函式將整個商店
state
作為引數,並根據該狀態傳回一個值 useSelector
呼叫其選擇器函式,並傳回選擇器的結果useSelector
訂閱商店,並在每次執行動作時重新執行選擇器。- 每當選擇器結果變更時,
useSelector
會強制元件以新資料重新呈現
- 選擇器函式將整個商店
useDispatch
鉤子讓 React 元件可以將動作傳送至商店useDispatch
傳回實際的store.dispatch
函式- 您可以在元件內部視需要呼叫
dispatch(action)
<Provider>
元件讓其他 React 元件可以使用商店- 在您的整個
<App>
周圍呈現<Provider store={store}>
- 在您的整個
接下來是什麼?
現在我們的 UI 已經運作,是時候看看如何讓我們的 Redux 應用程式與伺服器通訊。在 第 6 部分:非同步邏輯 中,我們將討論非同步邏輯(例如逾時和 AJAX 呼叫)如何融入 Redux 資料流程。