如何實作分頁式的Progress Navigation?

Failed

參考效果

#

完整程式碼:Progress Navigation | Codepenopen_in_new

使用情境

#

在某些應用裡,偶爾會需要像是或是等long-term navigation的操作體驗,每一個步驟都不太能立刻完成,甚至需要等到下次登入時再繼續進行。最近要以React來實作這般功能時,發現不如預期中容易,關鍵在於此功能必須滿足:

容易分工維護及整合

#

從工程團隊的角度來看,程式碼本身應該要簡潔明確,核心的routing邏輯應該抽成獨立模組來維護,且不同team member應該要能平行且專注於刻出不同步驟的內頁。

保留擴充功能的彈性

#

根據產品需求,這個功能在未來可能加入新的擴充,例如:

  • user切換步驟的行為要被記錄或追蹤(例如背景傳送至Mixpanel或GA)
  • 容易在分頁內管理整體流程,隨時可以插入新的實驗性路徑(例如對男性使用者,要直接從跳至
  • 儲存目前進行到的步驟,以便下次登入時回到未完成的步驟

因此,架構上可以採用react-router來維護每個獨立的步驟,並且給定每個步驟的唯一路徑:

const App = () => (
<BrowserRouter>
<Switch>
<Route exact path="/page/1" component={StepOne} />
<Route exact path="/page/2" component={StepTwo} />
<Route exact path="/page/3" component={StepThree} />
<Route component={StepOne} />
</Switch>
</BrowserRouter>
)

步驟間的路由抽象化

#

每個步驟之間必須由一個獨立模組來管理跳頁,還要保持步驟內頁可以掌控整體流程的權限,還要監聽頁面切換以便蒐集user的操作行為,一個可靠的作法就是模仿react-router中的,自己實作一個步驟間路由的HOC,封裝react-router的 method,並且提供之類的method給步驟內頁元件來掌控整體流程:

const withProgressRouter = (WrappedComponent) => {
class HelperComponent extends Component {
goToPath = (path) => {
const { history } = this.props;
// 可以在此與backend互動
history.push(path);
}
render() {
return (
<WrappedComponent
goToPath={this.goToPath}
{...this.props}
/>
);
}
}
return withRouter(HelperComponent)
}

不同步驟之間共享的UI(例如)可以獨立成一個Component:

const ProgressRouter = ({ onBackClick, onNextClick, children }) => (
<>
<button onClick={onBackClick}>back</button>
<button onClick={onNextClick}>next</button>
<hr />
{children}
</>
)

使用方式

#

步驟內頁只要透過HOC提供的就能自由地控制整體流程,同時也能共享同一套切換步驟的UI,完全專注於步驟內頁的設計:

const StepOne = withProgressRouter(({ goToPath }) => (
<ProgressRouter
onBackClick={() => goToPath('/step/1')}
onNextClick={() => goToPath('/step/2')}
>
Step One
</ProgressRouter>
))
const StepTwo = withProgressRouter(({ goToPath }) => (
<ProgressRouter
onBackClick={() => goToPath('/step/1')}
onNextClick={() => goToPath('/step/3')}
>
Step Two
</ProgressRouter>
))
const StepThree = withProgressRouter(({ goToPath }) => (
<ProgressRouter
onBackClick={() => goToPath('/step/2')}
onNextClick={() => goToPath('/step/3')}
>
Step Three
</ProgressRouter>
))