在 React 的世界裏」一切都是組件「, 組件能夠映射做函數式編程中的函數,React 的組件和函數同樣的靈活的特性不只僅能夠用於繪製 UI,還能夠用於封裝業務狀態和邏輯,或者非展現相關的反作用, 再經過組合方式組成複雜的應用. 本文嘗試解釋用 React 組件的思惟來處理常見的業務開發場景.javascript
系列目錄html
目錄前端
響應式
編程在很長一段時期裏,高階組件都是加強和組合 React 組件的最流行的方式. 這個概念源自於函數式編程的高階函數. 高階組件能夠定義爲: 高階組件是函數,它接收原始組件並返回原始組件的加強/填充版本:vue
const HOC = Component => EnhancedComponent;
複製代碼
首先要明白咱們爲何須要高階組件:java
React 的文檔說的很是清楚, 高階組件是一種用於複用組件邏輯模式. 最爲常見的例子就是 redux 的connect
和 react-router 的 withRouter
. 高階組件最初用於取代 mixin(瞭解React Mixin 的前世此生). 總結來講就是兩點:react
高階組件的一些實現方法主要有兩種:git
屬性代理(Props Proxy)
: 代理傳遞給被包裝組件的 props, 對 props 進行操做. 這種方式用得最多. 使用這種方式能夠作到:github
反向繼承(Inheritance Inversion)
: 高階組件繼承被包裝的組件. 例如:spring
function myhoc(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render();
}
};
}
複製代碼
能夠實現:編程
實際上高階組件能作的不止上面列舉的, 高階組件很是靈活, 全憑你的想象力. 讀者能夠了解 recompose這個庫, 簡直把高階組件玩出花了.
總結一下高階組件的應用場景:
高階組件相關文檔在網上有不少, 本文不打算展開描述. 深刻了解高階組件
高階組件的一些規範:
包裝顯示名字以便於調試
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {
/* ... */
}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製代碼
使用 React.forwardRef 來轉發 ref
使用'高階函數'來配置'高階組件', 這樣可讓高階組件的組合性最大化. Redux 的 connect 就是典型的例子
const ConnectedComment = connect(
commentSelector,
commentActions,
)(Comment);
複製代碼
當使用 compose 進行組合時就能體會到它的好處:
// 🙅 不推薦
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// ✅ 使用compose方法進行組合
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的
const enhance = compose(
// 這些都是單獨一個參數的高階組件
withRouter,
connect(commentSelector),
);
const EnhancedComponent = enhance(WrappedComponent);
複製代碼
轉發全部不相關 props 屬性給被包裝的組件
render() {
const { extraProp, ...passThroughProps } = this.props;
// ...
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
複製代碼
命名: 通常以 with*命名, 若是攜帶參數, 則以 create*命名
Render Props(Function as Child) 也是一種常見的 react 模式, 好比官方的 Context API 和 react-spring 動畫庫. 目的高階組件差很少: 都是爲了分離關注點, 對組件的邏輯進行復用; 在使用和實現上比高階組件要簡單, 在某些場景能夠取代高階組件. 官方的定義是:
是指一種在 React 組件之間使用一個值爲函數的 prop 在 React 組件間共享代碼的簡單技術
React 並無限定任何 props 的類型, 因此 props 也能夠是函數形式. 當 props 爲函數時, 父組件能夠經過函數參數給子組件傳遞一些數據進行動態渲染. 典型代碼爲:
<FunctionAsChild>{() => <div>Hello,World!</div>}</FunctionAsChild>
複製代碼
使用示例:
<Spring from={{ opacity: 0 }} to={{ opacity: 1 }}>
{props => <div style={props}>hello</div>}
</Spring>
複製代碼
某種程度上, 這種模式相比高階組件要簡單不少, 無論是實現仍是使用層次. 缺點也很明顯:
再開一下腦洞. 經過一個 Fetch 組件來進行接口請求:
<Fetch method="user.getById" id={userId}>
{({ data, error, retry, loading }) => (
<Container>
{loading ? (
<Loader />
) : error ? (
<ErrorMessage error={error} retry={retry} />
) : data ? (
<Detail data={data} />
) : null}
</Container>
)}
</Fetch>
複製代碼
在 React Hooks 出現以前, 爲了給函數組件(或者說 dumb component)添加狀態, 一般會使用這種模式. 好比 react-powerplug
官方文檔
大部分狀況下, 組件表示是一個 UI 對象. 其實組件不僅僅能夠表示 UI, 也能夠用來抽象業務對象, 有時候抽象爲組件能夠巧妙地解決一些問題.
舉一個例子: 當一個審批人在審批一個請求時, 請求發起者是不能從新編輯的; 反之發起者在編輯時, 審批人不能進行審批. 這是一個鎖定機制, 後端通常使用相似心跳機制來維護這個'鎖', 這個鎖能夠顯式釋放,也能夠在超過必定時間沒有激活時自動釋放,好比頁面關閉. 因此前端一般會使用輪詢機制來激活鎖.
通常的實現:
class MyPage extends React.Component {
public componentDidMount() {
// 根據一些條件觸發, 可能還要監聽這些條件的變化,而後中止加鎖輪詢. 這個邏輯實現起來比較囉嗦
if (someCondition) {
this.timer = setInterval(async () => {
// 輪詢
tryLock();
// 錯誤處理,能夠加鎖失敗...
}, 5000);
}
}
public componentWillUnmount() {
clearInterval(this.timer);
// 頁面卸載時顯式釋放
releaseLock();
}
public componentDidUpdate() {
// 監聽條件變化,開始或中止鎖定
// ...
}
}
複製代碼
隨着功能的迭代, MyPage 會變得愈來愈臃腫, 這時候你開始考慮將這些業務邏輯抽取出去. 通常狀況下經過高階組件或者 hook 來實現, 但都不夠靈活, 好比條件鎖定這個功能實現起來就比較彆扭.
有時候考慮將業務抽象成爲組件, 可能能夠巧妙地解決咱們的問題, 例如 Locker:
/**
* 鎖定器
*/
const Locker: FC<{ onError: err => boolean, id: string }> = props => {
const {id, onError} = props
useEffect(() => {
let timer
const poll = () => {
timer = setTimeout(async () => {
// ...
// 輪詢,處理異常等狀況
}, 5000)
}
poll()
return () => {
clearTimeout(timer)
releaseLock()
}
}, [id])
return null
};
複製代碼
使用 Locker
render() {
return (<div>
{someCondition && <Locker id={this.id} onError={this.handleError}></Locker>}
</div>)
}
複製代碼
這裏面有一個要點:咱們將一個業務抽象爲了一個組件後,業務邏輯有了和組件同樣的生命週期。如今組件內部只需關心自身的邏輯,好比只關心資源請求和釋放(即 How),而什麼時候進行,什麼條件進行(即 When)則由父級來決定, 這樣就符合了單一職責原則。 上面的例子父級經過 JSX 的條件渲染就能夠動態控制鎖定, 比以前的實現簡單了不少
我的以爲 hooks 對於 React 開發來講是一個革命性的特性, 它改變了開發的思惟和模式. 首先要問一下, "它解決了什麼問題? 帶來了什麼新的東西?"
hooks 首先是要解決高階組件或者 Render Props 的痛點的. 官方在'動機'上就說了:
問題: React 框架自己並無提供一種將可複用的邏輯注入到組件上的方式/原語. RenderProps 和高階組件只是'模式層面(或者說語言層面)'的東西:
此前的方案: 高階組件和 Render Props。這些方案都是基於組件自己的機制
hooks 如何解決:
問題:
此前的解決方法: 高階組件和 Render Props 或者狀態管理器. 分割抽離邏輯和 UI, 切割成更小粒度的組件
hooks 如何解決: Hooks 容許您根據相關部分(例如設置訂閱或獲取數據)將一個組件分割成更小的函數,而不是強制基於生命週期方法進行分割。你還能夠選擇使用一個 reducer 來管理組件的本地狀態,以使其更加可預測
Hooks 帶來的新東西: hook 旨在讓組件的內部邏輯組織成可複用的更小單元,這些單元各自維護一部分組件‘狀態和邏輯’。
圖片來源於twitter(@sunil Pai)
一種新的組件編寫方式. 和此前基於 class 或純函數組件的開發方式不太同樣, hook 提供了更簡潔的 API 和代碼複用機制, 這使得組件代碼變得更簡短. 例如 👆 上圖就是遷移到 hooks 的代碼結構對比, 讀者也能夠看這個演講(90% Cleaner React).
更細粒度的狀態控制(useState). 之前一個組件只有一個 setState 集中式管理組件狀態, 如今 hooks 像組件同樣, 是一個邏輯和狀態的聚合單元. 這意味着不一樣的 hook 能夠維護本身的狀態.
無論是 hook 仍是組件,都是普通函數.
高階組件之間只能簡單嵌套複合(compose), 而多個 hooks 之間是平鋪的, 能夠定義更復雜的關係(依賴).
更容易進行邏輯和視圖分離. hooks 自然隔離 JSX, 視圖和邏輯之間的界限比較清晰, 這使得 hooks 能夠更專一組件的行爲.
淡化組件生命週期概念, 將原本分散在多個生命週期的相關邏輯聚合起來
一點點'響應式編程'的味道, 每一個 hooks 都包含一些狀態和反作用,這些數據能夠在 hooks 之間傳遞流動和響應, 見下文
跨平臺的邏輯複用. 這是我本身開的腦洞, React hooks 出來以後尤雨溪就推了一個vue-hooks試驗項目, 若是後面發展順利, hooks 是可能被用於跨框架複用?
一個示例: 無限加載列表
通常 hooks 的基本代碼結構爲:
function useHook(options) {
// ⚛️states
const [someState, setSomeState] = useState(initialValue);
// ⚛️derived state
const computedState = useMemo(() => computed, [dependencies]);
// ⚛️refs
const refSomething = useRef();
// ⚛️side effect
useEffect(() => {}, []);
useEffect(() => {}, [dependencies]);
// ⚛️state operations
const handleChange = useCallback(() => {
setSomeState(newState)
}, [])
// ⚛️output
return <div>{...}</div>
}
複製代碼
自定義 hook 和函數組件的代碼結構基本一致, 因此有時候hooks 寫着寫着原來越像組件, 組件寫着寫着越像 hooks. 我以爲能夠認爲組件就是一種特殊的 hook, 只不過它輸出 Virtual DOM.
一些注意事項:
總結 hooks 的經常使用場景:
學習 hooks:
響應式
編程Vue
的非侵入性響應式系統是其最獨特的特性之一, 能夠按照 Javascript 的數據操做習慣來操做組件狀態, 而後自動響應到頁面中. 而 React 這邊則提供了 setState, 對於複雜的組件狀態, setState 會讓代碼變得的又臭又長. 例如:
this.setState({
pagination: {
...this.state.pagination,
current: defaultPagination.current || 1,
pageSize: defaultPagination.pageSize || 15,
total: 0,
},
});
複製代碼
後來有了mobx, 基本接近了 Vue 開發體驗:
@observer
class TodoView extends React.Component {
private @observable loading: boolean;
private @observable error?: Error;
private @observable list: Item[] = [];
// 衍生狀態
private @computed get completed() {
return this.list.filter(i => i.completed)
}
public componentDidMount() {
this.load();
}
public render() {
/// ...
}
private async load() {
try {
this.error = undefined
this.loading = true
const list = await fetchList()
this.list = list
} catch (err) {
this.error = err
} finally {
this.loading = false
}
}
}
複製代碼
其實 mobx 也有挺多缺點:
代碼侵入性. 全部須要響應數據變更的組件都須要使用 observer 裝飾, 屬性須要使用 observable 裝飾, 以及數據操做方式. 對 mobx 耦合較深, 往後切換框架或重構的成本很高
兼容性. mobx v5 後使用 Proxy 進行重構, Proxy 在 Chrome49 以後才支持. 若是要兼容舊版瀏覽器則只能使用 v4, v4 有一些坑, 這些坑對於不瞭解 mobx 的新手很難發現:
因而 hooks 出現了, 它讓組件的狀態管理變得更簡單直接, 並且它的思想也很接近 mobx 響應式編程哲學:
狀態 是驅動應用的數據. 例如 UI 狀態或者業務領域狀態
function Demo() {
const [list, setList] = useState<Item[]>([]);
// ...
}
複製代碼
任何 源自狀態而且不會再有任何進一步的相互做用的東西就是衍生。包括用戶視圖, 衍生狀態, 其餘反作用
function Demo(props: { id: string }) {
const { id } = props;
// 取代mobx的observable: 獲取列表, 在掛載或id變更時請求
const [value, setValue, loading, error, retry] = usePromise(
async id => {
return getList(id);
},
[id],
);
// 衍生狀態: 取代mobx的computed
const unreads = useMemo(() => value.filter(i => !i.readed), [value]);
// 衍生反作用: value變更後自動持久化
useDebounce(
() => {
saveList(id, value);
},
1000,
[value],
);
// 衍生視圖
return <List data={value} onChange={setValue} error={error} loading={loading} retry={retry} />;
}
複製代碼
因此說 hook 是一個革命性的東西, 它可讓組件的狀態數據流更加清晰. 換作 class 組件, 咱們一般的作法多是在 componentDidUpdate
生命週期方法中進行數據比較, 而後命令式地觸發一些方法. 好比 id 變化時觸發 getList, list 變化時進行 saveList.
hook 彷佛在淡化組件生命週期的概念, 讓開發者更專一於狀態的關係, 以數據流的方式來思考組件的開發. Dan Abramov在編寫有彈性的組件也提到了一個原則"不要阻斷數據流", 證明了筆者的想法:
不管什麼時候使用 props 和 state,請考慮若是它們發生變化會發生什麼。在大多數狀況下,組件不該以不一樣方式處理初始渲染和更新流程。這使它可以適應邏輯上的變化。
讀者能夠看一下awesome-react-hooks, 這些開源的 hook 方案都挺有意思. 例如rxjs-hooks, 巧妙地將 react hooks 和 rxjs 結合的起來:
function App(props: { foo: number }) {
// 響應props的變更
const value = useObservable(inputs$ => inputs$.pipe(map(([val]) => val + 1)), 200, [props.foo]);
return <h1>{value}</h1>;
}
複製代碼
就如 react 官方文檔說的: "咱們的 React 使用了數以千計的組件,然而卻還未發現任何須要推薦你使用繼承的狀況。", React 偏向於函數式編程的組合模式, 面向對象的繼承實際的應用場景不多.
當咱們須要將一些傳統的第三方庫轉換成 React 組件庫時, 繼承就可能派上用場. 由於這些庫大部分是使用面向對象的範式來組織的, 比較典型的就是地圖 SDK. 以百度地圖爲例:
百度地圖有各類組件類型: controls, overlays, tileLayers. 這些類型都有多個子類, 如上圖, overlay 有 Label, Marker, Polyline 等這些子類, 且這些子類有相同的生命週期, 都是經過 addOverlay 方法來渲染到地圖畫布上. 咱們能夠經過繼承的方式將他們生命週期管理抽取到父類上, 例如:
// Overlay抽象類, 負責管理Overlay的生命週期
export default abstract class Overlay<P> extends React.PureComponent<OverlayProps & P> {
protected initialize?: () => void;
// ...
public componentDidMount() {
// 子類在constructor或initialize方法中進行實例化
if (this.initialize) {
this.initialize();
}
if (this.instance && this.context) {
// 渲染到Map畫布中
this.context.nativeInstance!.addOverlay(this.instance);
// 初始化參數
this.initialProperties();
}
}
public componentDidUpdate(prevProps: P & OverlayProps) {
// 屬性更新
this.updateProperties(prevProps);
}
public componentWillUnmount() {
// 組件卸載
if (this.instance && this.context) {
this.context.nativeInstance!.removeOverlay(this.instance);
}
}
// ...
// 其餘通用方法
private forceReloadIfNeed(props: P, prevProps: P) {
...
}
}
複製代碼
子類的工做就變得簡單不少, 聲明本身的屬性/事件和實例化具體類:
export default class Label extends Overlay<LabelProps> {
public static defaultProps = {
enableMassClear: true,
};
public constructor(props: LabelProps) {
super(props);
const { position, content } = this.props;
// 聲明支持的屬性和回調
this.extendedProperties = PROPERTIES;
this.extendedEnableableProperties = ENABLEABLE_PROPERTIES;
this.extendedEvents = EVENTS;
// 實例化具體類
this.instance = new BMap.Label(content, {
position,
});
}
}
複製代碼
代碼來源於 react-bdmap
固然這個不是惟一的解決方法, 使用高階組件和 hooks 一樣可以實現. 只不過對於本來就採用面向對象範式組織的庫, 使用繼承方式會更加好理解
模態框是應用開發中使用頻率很是高組件,尤爲在中後臺管理系統中. 可是在 React 中用着並非特別爽, 典型的代碼以下:
const Demo: FC<{}> = props => {
// ...
const [visible, setVisible] = useState(false);
const [editing, setEditing] = useState();
const handleCancel = () => {
setVisible(false);
};
const prepareEdit = async (item: Item) => {
// 加載詳情
const detail = await loadingDeatil(item.id);
setEditing(detail);
setVisible(true);
};
const handleOk = async () => {
try {
const values = await form.validate();
// 保存
await save(editing.id, values);
// 隱藏
setVisible(false);
} catch {}
};
return;
<>
<Table
dataSource={list}
columns={[
{
text: '操做',
render: item => {
return <a onClick={() => prepareEdit(item)}>編輯</a>;
},
},
]}
/>
<Modal visible={visible} onOk={handleOk} onCancel={handleHide}>
{/* 表單渲染 */}
</Modal>
</>;
};
複製代碼
上面的代碼太醜了, 不相關邏輯堆積在一個組件下 ,不符合單一職責. 因此咱們要將模態框相關代碼抽取出去, 放到EditModal中:
const EditModal: FC<{ id?: string; visible: boolean; onCancel: () => void; onOk: () => void }> = props => {
// ...
const { visible, id, onHide, onOk } = props;
const detail = usePromise(async (id: string) => {
return loadDetail(id);
});
useEffect(() => {
if (id != null) {
detail.call(id);
}
}, [id]);
const handleOk = () => {
try {
const values = await form.validate();
// 保存
await save(editing.id, values);
onOk();
} catch {}
};
return (
<Modal visible={visible} onOk={onOk} onCancel={onCancel}>
{detail.value &&
{
/* 表單渲染 */
}}
</Modal>
);
};
/**
* 使用
*/
const Demo: FC<{}> = props => {
// ...
const [visible, setVisible] = useState(false);
const [editing, setEditing] = useState<string | undefined>(undefined);
const handleHide = () => {
setVisible(false);
};
const prepareEdit = async (item: Item) => {
setEditing(item.id);
setVisible(true);
};
return;
<>
<Table
dataSource={list}
columns={[
{
text: '操做',
render: item => {
return <a onClick={() => prepareEdit(item)}>編輯</a>;
},
},
]}
/>
<EditModal id={editing} visible={visible} onOk={handleHide} onCancel={handleHide}>
{' '}
</EditModal>
</>;
};
複製代碼
如今編輯相關的邏輯抽取到了 EditModal 上,可是 Demo 組件還要維護模態框的打開狀態和一些數據狀態。一個複雜的頁面可能會有不少模態框,這樣的代碼會變得愈來愈噁心, 各類 xxxVisible 狀態滿天飛. 從實際開發角度上將,模態框控制的最簡單的方式應該是這樣的:
const handleEdit = item => {
EditModal.show({
// 🔴 經過函數調用的方式出發彈窗. 這符合對模態框的習慣用法, 不關心模態框的可見狀態. 例如window.confirm, wx.showModal().
id: item.id, // 🔴 傳遞數據給模態框
onOk: saved => {
// 🔴 事件回調
refreshList(saved);
},
onCancel: async () => {
return confirm('確認取消'); // 控制模態框是否隱藏
},
});
};
複製代碼
這種方式在社區上也是有爭議的,有些人認爲這是 React 的反模式,@欲三更在Modal.confirm 違反了 React 的模式嗎?就探討了這個問題。 以圖爲例:
圖片一樣出自欲三更文章紅線表示時間驅動(或者說時機驅動), 藍線表示數據驅動。欲三更認爲「哪怕一個帶有明顯數據驅動特點的 React 項目,也存在不少部分不是數據驅動而是事件驅動的. 數據只能驅動出狀態,只有時機才能驅動出行爲, 對於一個時機驅動的行爲,你非得把它硬坳成一個數據驅動的狀態,你不以爲很奇怪嗎?」. 他的觀點正不正確筆者不作評判, 可是某些場景嚴格要求‘數據驅動’,可能會有不少模板代碼,寫着會很難受.
So 怎麼實現?
能夠參考 antd Modal.confirm的實現, 它使用ReactDOM.render
來進行外掛渲染,也有人使用Context API來實現的. 筆者認爲比較接近理想的(至少 API 上看)是react-comfirm這樣的:
/**
* EditModal.tsx
*/
import { confirmable } from 'react-confirm';
const EditModal = props => {
/*...*/
};
export default confirmable(EditModal);
/**
* Demo.tsx
*/
import EditModal from './EditModal';
const showEditModal = createConfirmation(EditModal);
const Demo: FC<{}> = props => {
const prepareEdit = async (item: Item) => {
showEditModal({
id: item.id, // 🔴 傳遞數據給模態框
onOk: saved => {
// 🔴 事件回調
refreshList(saved);
},
onCancel: async someValues => {
return confirm('確認取消'); // 控制模態框是否隱藏
},
});
};
// ...
};
複製代碼
使用ReactDOM.render
外掛渲染形式的缺點就是沒法訪問 Context,因此仍是要妥協一下,結合 Context API 來實現示例:
擴展
Context 爲組件樹提供了一個傳遞數據的方法,從而避免了在每個層級手動的傳遞 props 屬性.
Context 在 React 應用中使用很是頻繁, 新的Context API也很是易用. Context 經常使用於如下場景:
Context 的做用域是子樹, 也就是說一個 Context Provider 能夠應用於多個子樹, 子樹的 Provider 也能夠覆蓋父級的 Provider 的 value. 基本結構:
import React, {useState, useContext} from 'react'
export inteface MyContextValue {
state: number
setState: (state: number) => void
}
const MyContext = React.createContext<MyContextValue>(
{
state: 1,
// 設置默認值, 拋出錯誤, 必須配合Provider使用
setState: () => throw new Error('請求MyContextProvider組件下級調用')
}
)
export const MyContextProvider: FC<{}> = props => {
const [state, setState] = useState(1)
return <MyContext.Provider value={{state, setState}}>{props.children}</MyContext.Provider>
}
export function useMyContext() {
return useContext(MyContext)
}
export default MyContextProvider
複製代碼
Context 默認值中的方法應該拋出錯誤, 警告不規範的使用
擴展:
對於函數式編程範式的 React 來講,不可變狀態有重要的意義.
不可變數據具備可預測性。可不變數據可讓應用更好調試,對象的變動更容易被跟蹤和推導.
就好比 Redux, 它要求只能經過 dispatch+reducer 進行狀態變動,配合它的 Devtool 能夠很好的跟蹤狀態是如何被變動的. 這個特性對於大型應用來講意義重大,由於它的狀態很是複雜,若是不加以組織和約束,你不知道是哪一個地方修改了狀態, 出現 bug 時很難跟蹤.
因此說對於嚴格要求單向數據流的狀態管理器(Redux)來講,不可變數據是基本要求,它要求整個應用由一個單一的狀態進行映射,不可變數據可讓整個應用變得可被預測.
不可變數據還使一些複雜的功能更容易實現。避免數據改變,使咱們可以安全保留對舊數據的引用,能夠方便地實現撤銷重作,或者時間旅行這些功能
能夠精確地進行從新渲染判斷。能夠簡化 shouldComponentUpdate 比較。
實現不可變數據的流行方法:
筆者比較喜歡 immer,沒有什麼心智負擔, 按照 JS 習慣的對象操做方式就能夠實現不可變數據。
傳統的路由主要用於區分頁面, 因此一開始前端路由設計也像後端路由(也稱爲靜態路由)同樣, 使用對象配置方式, 給不一樣的 url 分配不一樣的頁面組件, 當應用啓動時, 在路由配置表中查找匹配 URL 的組件並渲染出來.
React-Router v4 算是一個真正意義上符合組件化思惟的路由庫, React-Router 官方稱之爲‘動態路由’, 官方的解釋是"指的是在應用程序渲染時發生的路由,而不是在運行應用程序以外的配置或約定中發生的路由", 具體說, <Route/>
變成了一個普通 React 組件, 它在渲染時判斷是否匹配 URL, 若是匹配就渲染指定的組件, 不匹配就返回 null.
這時候 URL 意義已經不同了, URL 再也不是簡單的頁面標誌, 而是應用的狀態; 應用構成也再也不侷限於扁平頁面, 而是多個能夠響應 URL 狀態的區域(可嵌套). 由於思惟轉變很大, 因此它剛出來時並不受青睞. 這種方式更加靈活, 因此選擇 v4 不表明放棄舊的路由方式, 你徹底能夠按照舊的方式來實現頁面路由.
舉個應用實例: 一個應用由三個區域組成: 側邊欄放置多個入口, 點擊這些入口會加載對應類型的列表, 點擊列表項須要加載詳情. 三個區域存在級聯關係
首先設計可以表達這種級聯關係的 URL, 好比/{group}/{id}
, URL 設計通常遵循REST 風格, 那麼應用的大概結構是這樣子:
// App
const App = () => {
<div className="app">
<SideBar />
<Route path="/:group" component={ListPage} />
<Route path="/:group/:id" component={Detail} />
</div>;
};
// SideBar
const Sidebar = () => {
return (
<div className="sidebar">
{/* 使用NavLink 在匹配時顯示激活狀態 */}
<NavLink to="/message">消息</NavLink>
<NavLink to="/task">任務</NavLink>
<NavLink to="/location">定位</NavLink>
</div>
);
};
// ListPage
const ListPage = props => {
const { group } = props.match.params;
// ...
// 響應group變化, 並加載指定類型列表
useEffect(() => {
load(group);
}, [group]);
// 列表項也會使用NavLink, 用於匹配當前展現的詳情, 激活顯示
return <div className="list">{renderList()}</div>;
};
// DetailPage
const DetailPage = props => {
const { group, id } = props.match.params;
// ...
// 響應group和id, 並加載詳情
useEffect(() => {
loadDetail(group, id);
}, [group, id]);
return <div className="detail">{renderDetail()}</div>;
};
複製代碼
擴展