Navi 是React的一个新种类的路由。它让你声明式地将URL绘制至内容,即使那个内容是异步获取的。
React团队刚发布的新API,叫做Hooks。太神奇了。它可以让你声明模型状态和副作用。你或许已经在网上读到它了,所以我不准备跟你讲hooks自身,但是…
新的API带来了新的可能。直接入话题,Navi的新<Router>
组建使用Hooks和Suspense来让路由的工作比以前更简单。它使得各种各样的东西成为可能 - 你甚至只用三行代码就可以添加动画加载过渡。
所以你怎么使用这些超级强大的hooks?我们将用一分钟的时间来完成它,在这之前,什么是<Router>
(路由)?
一个<Router routes />
可以连接到多少个路由上
你想要连接多少路由就连接多少,因为Navi让你动态按需import()
整个路由树。但是,它是怎么实现的呢?
Navi用于声明routes的函数中有招数。对于简单的routes,你可以只使用Navi的mount()
和route()
函数。但是对于大的内容块,你可以使用async/await
在异步数据和视图中声明依赖项 - 或者你甚至可以使用lazy()
将整个路由树分割开。
<Router routes={
mount({
'/': route({
title: 'My Shop',
getData: () => api.fetchProducts(),
view: <Landing />,
}),
'/products': lazy(() => import('./productsRoutes')),
})
} />
如果你看一下这个例子,你会看到你已经获得了一个有两个routes的<Router>
,包括一个shop的落地页和一个懒加载/products
的URL。
让我们来构建这个shop剩下的部分。
你的下一步,你会需要决定在哪里来渲染当前route的视图元素。为了做到这一点,你只是将一个<View />
元素放到你的<Router>
。
ReactDOM.render(
<Router routes={routes}>
<Layout>
<View />
</Layout>
</Router>,
document.getElementById('root')
)
很简单,对吧?但是,等一下…如果你看一个懒加载的/products
URL呢?这个route将通过一个import()
被加载,然后返回一个Promise,且这首先不会渲染任何东西。幸运的是,React的新<Suspense>
特性让你可以声明式地等待promise去resolve。所以只将你的<View>
折叠到一个<Suspense>
标签内,你就可以得到释放了。
(译者注:以下代码效果见:https://frontarm.com/demoboard/?id=e2701e9e-306c-48d8-90cc-0ee9b1dc7eb3)
// index.js
import { mount, route, lazy } from 'navi'
import React, { Suspense } from 'react'
import ReactDOM from 'react-dom'
import { Router, View } from 'react-navi'
import api from './api'
import Landing from './Landing'
import Layout from './Layout'
const routes =
mount({
'/': route({
title: "Hats 'n' Flamethrowers 'r' Us",
getData: () => api.fetchProducts(),
view: <Landing />,
}),
'/product': lazy(() => import('./product')),
})
ReactDOM.render(
<Router routes={routes}>
<Layout>
<Suspense fallback={null}>
<View />
</Suspense>
</Layout>
</Router>,
document.getElementById('root')
)
// production.js
import React from 'react'
import { mount, route } from 'navi'
import api from './api'
export default mount({
'/:id': route({
async getView(request) {
let product = await api.fetchProduct(request.params.id)
return <Product product={product} />
}
})
})
function Product({ product }) {
return (
<article className='Product'>
<span className='Product-emoji'>{product.emoji}</span>
<h1>{product.title}</h1>
<span>{product.price}</span>
</article>
)
}
// Landing.js
import React from 'react'
import { Link, useCurrentRoute } from 'react-navi'
export default function Landing() {
// useCurrentRoute 返回最近加载的Route对象
let { data } = useCurrentRoute()
let productIds = Object.keys(data)
return (
<ul>
{productIds.map(id =>
<li key={id}>
<Link
href={`/product/${id}`}
prefetch={null}>
{data[id].title}
</Link>
</li>
)}
</ul>
)
}
// Layout.js
import BusyIndicator from 'react-busy-indicator@1.0.0'
import React from 'react'
import { Link, useLoadingRoute } from 'react-navi'
export default function Layout({ children }) {
// 如果有一个route还没有完成加载,它可以使用`useLoadingRoute`恢复
let loadingRoute = useLoadingRoute()
return (
<div className="Layout">
{/* 这个组件在一个延迟之后显示一个加载提示 */}
<BusyIndicator isBusy={!!loadingRoute} delayMs={200} />
<header className="Layout-header">
<h1 className="Layout-title">
<Link href='/' prefetch={null}>
Hats 'n' Flamethrowers 'r' Us
</Link>
</h1>
</header>
<main>
{children}
</main>
</div>
)
}
// api.js
import { NotFoundError } from 'navi'
const db = {
hat: {
emoji: '🧢',
title: 'Hat',
price: '$50.00',
},
flamethrower: {
emoji: '🔥🔫',
title: 'Not a flamethrower',
price: '$500.00',
},
}
export default {
fetchProduct: async (id) => {
await delay(100)
let product = await db[id]
if (!product) {
throw new NotFoundError()
}
return product
},
fetchProducts: async () => {
await delay(100)
return db
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
兄弟,只给我看hooks?
好的,你已经看过了怎么渲染一个route的视图。但是你有没有注意到你的route也定义了一个getData()
函数?
route({
title: 'My Shop',
getData: () => api.fetch('/products'),
view: <Landing />,
})
你怎么获得数据呢?使用React hooks
!
Navi的hook useCurrentRoute()
可以被任何函数式组件调用,且使用一个<Router>
标签来渲染。它返回一个Route
对象,这个对象包含Navi知道的关于当前URL的任何东西。
(译者注:以下代码效果见:https://frontarm.com/demoboard/?id=86c7576d-e276-43a3-b948-2507c72c090e)
// index.js
import { mount, route, lazy } from 'navi'
import React, { Suspense } from 'react'
import ReactDOM from 'react-dom'
import { Router, View } from 'react-navi'
import api from './api'
import Landing from './Landing'
import Layout from './Layout'
const routes =
mount({
'/': route({
title: "Hats 'n' Flamethrowers 'r' Us",
getData: () => api.fetchProducts(),
view: <Landing />,
}),
'/product': lazy(() => import('./product')),
})
ReactDOM.render(
<Router routes={routes}>
<Layout>
<Suspense fallback={null}>
<View />
</Suspense>
</Layout>
</Router>,
document.getElementById('root')
)
// product.js
import React from 'react'
import { mount, route } from 'navi'
import api from './api'
export default mount({
'/:id': route({
async getView(request) {
let product = await api.fetchProduct(request.params.id)
return <Product product={product} />
}
})
})
function Product({ product }) {
return (
<article className='Product'>
<span className='Product-emoji'>{product.emoji}</span>
<h1>{product.title}</h1>
<span>{product.price}</span>
</article>
)
}
//Landing.js
import React from 'react'
import { Link, useCurrentRoute } from 'react-navi'
export default function Landing() {
// useCurrentRoute返回最近加载的Route对象
let route = useCurrentRoute()
let data = route.data
let productIds = Object.keys(data)
console.log('views', route.views)
console.log('url', route.url)
console.log('data', route.data)
console.log('status', route.status)
return (
<ul>
{productIds.map(id =>
<li key={id}>
<Link href={`/product/${id}`}>{data[id].title}</Link>
</li>
)}
</ul>
)
}
// Layout.js
import BusyIndicator from 'react-busy-indicator@1.0.0'
import React from 'react'
import { Link, useLoadingRoute } from 'react-navi'
export default function Layout({ children }) {
// 如果有一个route还没有完成加载,它可以使用`useLoadingRoute`恢复
let loadingRoute = useLoadingRoute()
return (
<div className="Layout">
{/* 这个组件在一个延迟之后显示一个加载提示 */}
<BusyIndicator isBusy={!!loadingRoute} delayMs={200} />
<header className="Layout-header">
<h1 className="Layout-title">
<Link href='/' prefetch={null}>
Hats 'n' Flamethrowers 'r' Us
</Link>
</h1>
</header>
<main>
{children}
</main>
</div>
)
}
// api.js
import { NotFoundError } from 'navi'
const db = {
hat: {
emoji: '🧢',
title: 'Hat',
price: '$50.00',
},
flamethrower: {
emoji: '🔥🔫',
title: 'Not a flamethrower',
price: '$500.00',
},
}
export default {
fetchProduct: async (id) => {
await delay(100)
let product = await db[id]
if (!product) {
throw new NotFoundError()
}
return product
},
fetchProducts: async () => {
await delay(100)
return db
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
Ok,到这里都很好。但是想象一下,你只是刚点击了一个链接到被动态引入的/products
,它将需要一些时间来获取route
,所以这时候你将显示什么?
可视化加载routes
当routes需要很长时间加载时,你会想向用户展示一些加载指示的东西,而且有一些方法你可以使用。一种方法是在初始化加载的时候,使用<Suspense>
显示一个fallback。但是这看起来有点糟糕。
你想要做的是在下一个route加载的时候,在当前页面上显示一个加载条。好吧,除非这个过渡只需要100ms。然后你可能只想在下一页准备好之前,一直显示当前页面,因为花费100ms显示一个加载条看起来也很糟糕。
这只是一个问题。用目前可用的工具来做这个事是非常难,对吧?但实际上,你只需要往上面的demo中添加3行代码,使用useLoadingRoute()
hook 和 react-busy-indicator
包。
(译者注:以下代码效果见:https://frontarm.com/demoboard/?id=fa65eb4c-c5f2-45ed-bafd-dd4a71adb86e)
// index.js
import { mount, route, lazy } from 'navi'
import React, { Suspense } from 'react'
import ReactDOM from 'react-dom'
import { Router, View } from 'react-navi'
import api from './api'
import Landing from './Landing'
import Layout from './Layout'
const routes =
mount({
'/': route({
title: "Hats 'n' Flamethrowers 'r' Us",
getData: () => api.fetchProducts(),
view: <Landing />,
}),
'/product': lazy(() => import('./product')),
})
ReactDOM.render(
<Router routes={routes}>
<Layout>
<Suspense fallback={null}>
<View />
</Suspense>
</Layout>
</Router>,
document.getElementById('root')
)
// product.js
import React from 'react'
import { mount, route } from 'navi'
import api from './api'
export default mount({
'/:id': route({
async getView(request) {
let product = await api.fetchProduct(request.params.id)
return <Product product={product} />
}
})
})
function Product({ product }) {
return (
<article className='Product'>
<span className='Product-emoji'>{product.emoji}</span>
<h1>{product.title}</h1>
<span>{product.price}</span>
</article>
)
}
// Landing.js
import React from 'react'
import { Link, useCurrentRoute } from 'react-navi'
export default function Landing() {
// useCurrentRoute 返回最近加载过的Route对象
let { data } = useCurrentRoute()
let productIds = Object.keys(data)
return (
<ul>
{productIds.map(id =>
<li key={id}>
<Link
href={`/product/${id}`}
prefetch={null}>
{data[id].title}
</Link>
</li>
)}
</ul>
)
}
// Layout.js
import BusyIndicator from 'react-busy-indicator@1.0.0'
import React from 'react'
import { Link, useLoadingRoute } from 'react-navi'
export default function Layout({ children }) {
// 如果有一个route还没有完成加载,它可以使用`useLoadingRoute`恢复
let loadingRoute = useLoadingRoute()
return (
<div className="Layout">
{/* 这个组件在一个延迟之后显示一个加载提示 */}
<BusyIndicator isBusy={!!loadingRoute} delayMs={200} />
<header className="Layout-header">
<h1 className="Layout-title">
<Link href='/' prefetch={null}>
Hats 'n' Flamethrowers 'r' Us
</Link>
</h1>
</header>
<main>
{children}
</main>
</div>
)
}
// api.js
import { NotFoundError } from 'navi'
const db = {
hat: {
emoji: '🧢',
title: 'Hat',
price: '$50.00',
},
flamethrower: {
emoji: '🔥🔫',
title: 'Not a flamethrower',
price: '$500.00',
},
}
export default {
fetchProduct: async (id) => {
await delay(1000)
let product = await db[id]
if (!product) {
throw new NotFoundError()
}
return product
},
fetchProducts: async () => {
await delay(100)
return db
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
去尝试在这些页面间多点击几次。有没有察觉到返回到首页的过渡多么的顺滑?顺滑到你都没有察觉到它实际上有100ms的延迟?很好!这才是你的用户真正想要的体验。
它是这样工作的:useCurrentRoute()
返回最近加载完成的route,useLoadingRoute()
返回任何已经请求但是还没加载完成的route。或者,如果用户没有点击一个链接,它会返回undefined
。
想要在页面加载的时候显示一个加载条?那就调用useLoadingRoute()
,检查下如果有返回值,就渲染一个加载条(如果有)!你可以用CSS transitions完成剩下的工作。
更巧妙的技巧
我不打算现在就扔给你一整套的Guide,API参考和与其他工具整合的文档。你现在在看一篇博文,所以你可能没有时间查看所有有意思的信息。让我来问你一个问题:
如果route没有加载,会发生什么?
异步数据和视图的一个事情是,它们不工作。幸运的是,React有一个处理这种不工作的问题的工具:Error Boundaries(异常边界)。
让我们倒回一会到包裹你的<View/>
的标签<Suspense>
。当<View />
遇到一个还没加载完成的route,它抛出一个promise,实际上请求React显示一会的fallback。你可以想象一下,<Suspense>
捕获那个promise,然后在这个promise被resolve的时候,重新渲染它的子组件。
类似地,如果<View />
发现getView()
或getData()
已经抛出了一个异常,它会将这个异常重新抛出来。实际上,如果这个router遇到一个404页面消失了很长时间的异常,<View />
也将会抛出它。这些异常可以被Error Boundary组件捕获。就绝大部分而言,你将需要编写你自己的error boundary,但是Navi包含一个<NotFoundBoundary>
来显示它做了什么:
(译者注:以下代码效果见:https://frontarm.com/demoboard/?id=ddcba310-c4ea-4fb2-81e3-95f30f22f6a5)
// index.js
import { mount, route, lazy } from 'navi'
import React, { Suspense } from 'react'
import ReactDOM from 'react-dom'
import { Router, View } from 'react-navi'
import api from './api'
import Landing from './Landing'
import Layout from './Layout'
const routes =
mount({
'/': route({
title: "Hats 'n' Flamethrowers 'r' Us",
getData: () => api.fetchProducts(),
view: <Landing />,
}),
'/product': lazy(() => import('./product')),
})
ReactDOM.render(
<Router routes={routes}>
<Layout>
<Suspense fallback={null}>
<View />
</Suspense>
</Layout>
</Router>,
document.getElementById('root')
)
// product.js
import React from 'react'
import { mount, route } from 'navi'
import api from './api'
export default mount({
'/:id': route({
async getView(request) {
let product = await api.fetchProduct(request.params.id)
return <Product product={product} />
}
})
})
function Product({ product }) {
return (
<article className='Product'>
<span className='Product-emoji'>{product.emoji}</span>
<h1>{product.title}</h1>
<span>{product.price}</span>
</article>
)
}
// Landing.js
import React from 'react'
import { Link, useCurrentRoute } from 'react-navi'
export default function Landing() {
// useCurrentRoute 返回最近加载完成的Route对象
let { data } = useCurrentRoute()
let productIds = Object.keys(data)
return (
<ul>
{productIds.map(id =>
<li key={id}>
<Link
href={`/product/${id}`}
prefetch={null}>
{data[id].title}
</Link>
</li>
)}
</ul>
)
}
// Layout.js
import BusyIndicator from 'react-busy-indicator@1.0.0'
import React from 'react'
import { Link, NotFoundBoundary, useLoadingRoute } from 'react-navi'
export default function Layout({ children }) {
// 如果有一个route还没有完成加载,它可以使用`useLoadingRoute`恢复
let loadingRoute = useLoadingRoute()
return (
<div className="Layout">
{/* 这个组件在一个延迟之后显示一个加载提示 */}
<BusyIndicator isBusy={!!loadingRoute} delayMs={200} />
<header className="Layout-header">
<h1 className="Layout-title">
<Link href='/'>
Hats 'n' Flamethrowers 'r' Us
</Link>
</h1>
</header>
<main>
<NotFoundBoundary render={renderNotFound}>
{children}
</NotFoundBoundary>
</main>
</div>
)
}
function renderNotFound() {
return (
<div className='Layout-error'>
<h1>404 - Not Found</h1>
</div>
)
}
// api.js
import { NotFoundError } from 'navi'
const db = {
hat: {
emoji: '🧢',
title: 'Hat',
price: '$50.00',
},
flamethrower: {
emoji: '🔥🔫',
title: 'Not a flamethrower',
price: '$500.00',
},
}
export default {
fetchProduct: async (id) => {
await delay(100)
let product = await db[id]
if (!product) {
throw new NotFoundError()
}
return product
},
fetchProducts: async () => {
await delay(100)
return db
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
但这不是全部!!
Ok,我想这篇文章已经超时了,但是有一些更详细的文档:
- Using URL Parameters
- Requests, Routes and Matchers
- Nested Routes and Views
- Programmatic navigation
- Guarding routes with authentication
- Improving SEO with static rendering
- Using Navi with react-router
- Using Navi with react-helmet
- Get a head start with a create-react-app based starter
你也可以从Navi库下examples目录下看到示例的完整代码,包括: