@acdlite/router 中文文档教程
Router
JavaScript 应用程序功能路由的实验。
npm install --save @acdlite/router
该名称是有意通用的,因为它仍处于实验阶段(可能永远)。
主要特点:
- A router is defined using composable middleware functions.
- A router is a function that turns a path into a state object. That's it. This allows for total separation of history management from route matching.
- Because history management is separate, the server-side API is identical to the client-side API.
Should I use this?
不
,也许吧。 我目前在一个副项目中使用它,但我不会推荐它用于任何生产应用程序。
How it works
本项目上下文中的“路由器”是一个接受路径和回调的函数。 路由器通过一系列中间件将路径传递给状态对象。 中间件完成后,将使用最终状态对象调用回调(同步或异步),该对象可用于呈现应用程序。
历史管理被认为是一个单独的问题——只需向路由器传递一个字符串。 在客户端,使用像 history 这样的项目。 在服务器上,使用您最喜欢的 Web 框架,例如 Express 或 Koa.
const router = createRouter(...middlewares)
router('/some/path', (error, state) => {
// Render app using state
})
Middleware
中间件是一个函数,它接受 Node 风格的回调(我们称它为监听器)并返回一个具有增强行为的新 Node 风格的回调。
type Listener = (error: Error, state: Object) => void
type Middleware = (next: Listener) => Listener
中间件的一个重要特性是它们是可组合的:
// Middlewares 1, 2, and 3 will run in sequence from left to right
const combinedMiddleware = compose(middleware1, middleware2, middlware3)
路由器中间件很像 Redux 中的中间件。 它用于在状态对象通过路由器时扩充状态对象。 下面是一个添加 query
字段的中间件示例:
import queryString from 'query-string'
const parseQuery = next => (error, state) => {
if (error) return next(error)
next(null, {
...state,
query: queryString.parse(state.search)
})
}
与 React props 和 Redux 状态一样,我们将路由器状态视为不可变的。
State object conventions
所有状态对象都应具有字段 path
、pathname
、search
和 hash
。 当您将路径字符串传递给路由器函数时,将从路径中提取其余字段。 反之亦然:如果您将初始状态对象传递给具有 pathname
、search
和 hash
的路由器函数,而不是路径字符串,添加了一个 path
字段。 这允许中间件依赖于这些字段而无需进行自己的解析。
还有两个具有特殊含义的附加字段:redirect
和 done
。 redirect
是不言自明的:中间件应该跳过任何带有 redirect
字段的状态对象,将其传递给下一个中间件。 类似地,带有done: true
的状态对象表示之前的中间件已经处理过它,不需要其他中间件进一步处理。 (在某些情况下,中间件可能适合处理 done
状态对象。)
处理所有这些特殊情况可能会变得乏味。 handle()
允许您创建处理特定情况的中间件。 它有点像 switch 语句或模式匹配。 示例
import { handle } from '@acdlite/router'
const middleware = handle({
// Handle error
error: next => (error, state) => {...}
// Handle redirect
redirect: next => (error, state) => {...}
// Handle done
done: next => (error, state) => {...}
// Handle all other cases
next: next => (error, state) => {...}
})
next()
是最常见的处理程序。
如果省略处理程序,则默认行为是将状态对象传递给下一个中间件,不变。
Proof-of-concept: React Router-like API
作为概念验证,react-router/
目录包含用于使用中间件实现类似 React Router API 的实用程序。 它支持:
- Nested route matching, with params
- Plain object routes or JSX routes
- Asynchronous route fetching, using
config.getChildRoutes()
- Asynchronous component fetching, using
config.getComponent()
- Index routes
尚未完成:
<Redirect>
routes
在内部,它使用了 React Router 的几种方法,因此路由匹配行为应该是相同的。
示例:
import { createRouter } from '@acdlite/router'
import { nestedRoute, getComponents, Route, IndexRoute } from '@acdlite/router/react-router'
import createHistory from 'history/lib/createBrowserHistory'
const reactRouter = createRouter(
nestedRoute(
<Route path="/" component={App}>
<Route path="post">
<IndexRoute component={PostIndex} />
<Route path=":id" component={Post} />
</Route>
</Route>
),
getComponents,
// ... add additional middleware, if desired
)
const history = createHistory()
// Listen for location updates
history.listen(location => {
// E.g. after navigating to '/post/123'
// Routers can accept either a path string or an object with `pathname`,
// `query`, and `search`, so we can pass the location object directly.
reactRouter(location, {
// Route was successful
done: (error, state) => {
// Returns a state object with info about the matched routes
expect(state).to.eql({
params: { id: '123' },
routes: [...] // Array of matching route config objects
components: [App, Post], // Array of matching components
// ...plus other fields from the location object
})
// Render your app using state...
},
// Handle redirects
redirect: (error, state) => {
history.replace(state.redirect)
},
// Handle errors
error: error => {
throw error
}
}
})
需要注意的一个关键点是服务器端 API 完全相同:不使用历史记录,只是将路径字符串直接传递给路由器,并实现 done()
, redirect()
和 error()
视情况而定。
另请注意,历史记录与您的路由逻辑之间没有相互依赖关系。
路由器返回匹配的组件,但您可以根据自己的喜好渲染它们。 一个简单的开始方法是使用 Recompose 的 nest()
功能:
const Component = nest(...state.components)
ReactDOM.render(<Component {...state.params} {...state.query} />)
这让你 90% 的方式与 React Router 保持一致。 组件和转换挂钩等便利设施需要重新实现,但相当简单。
Router
An experiment in functional routing for JavaScript applications.
npm install --save @acdlite/router
The name is intentionally generic because it's still in the experimental phase (perhaps forever).
Key features:
- A router is defined using composable middleware functions.
- A router is a function that turns a path into a state object. That's it. This allows for total separation of history management from route matching.
- Because history management is separate, the server-side API is identical to the client-side API.
See below for a proof-of-concept that mimics the React Router API.
Should I use this?
No.
Well, maybe. I'm currently using this in a side project, but I wouldn't recommend it for any production apps.
How it works
A "router" in the context of this project is a function that accepts a path and a callback. The router turns the path into a state object by passing it through a series of middleware. Once the middleware completes, the callback is called (either synchronously or asynchronously) with the final state object, which can be used to render an app.
History management is considered a separate concern — just pass the router a string. On the client, use a project like history. On the server, use your favorite web framework like Express or Koa.
const router = createRouter(...middlewares)
router('/some/path', (error, state) => {
// Render app using state
})
Middleware
A middleware is a function that accepts Node-style callback (we'll call it a listener) and returns a new Node-style callback with augmented behavior.
type Listener = (error: Error, state: Object) => void
type Middleware = (next: Listener) => Listener
An important feature of middleware is that they are composable:
// Middlewares 1, 2, and 3 will run in sequence from left to right
const combinedMiddleware = compose(middleware1, middleware2, middlware3)
Router middleware is much like middleware in Redux. It is used to augment a state object as it passes through a router. Here's an example of a middleware that adds a query
field:
import queryString from 'query-string'
const parseQuery = next => (error, state) => {
if (error) return next(error)
next(null, {
...state,
query: queryString.parse(state.search)
})
}
As with React props and Redux state, we treat router state as immutable.
State object conventions
All state objects should have the fields path
, pathname
, search
, and hash
. When you pass a path string to a router function, the remaining fields are extracted from the path. The reverse also works: if instead of a path string you pass an initial state object to a router function with pathname
, search
, and hash
, a path
field is added. This allows middleware to depend on those fields without having to do their own parsing.
There are two additional fields which have special meanings: redirect
and done
. redirect
is self-explanatory: a middleware should skip any state object with a redirect
field by passing it to the next middleware. Similarly, a state object with done: true
indicates that a previous middleware has already handled it, and it needs no further processing by remaining middleware. (There are some circumstances where it may be appropriate for a middleware to process a done
state object.)
Handling all these special cases can get tedious. The handle()
allows you to create a middleware that handles specific cases. It's a bit like a switch statement, or pattern matching. Example
import { handle } from '@acdlite/router'
const middleware = handle({
// Handle error
error: next => (error, state) => {...}
// Handle redirect
redirect: next => (error, state) => {...}
// Handle done
done: next => (error, state) => {...}
// Handle all other cases
next: next => (error, state) => {...}
})
next()
is the most common handler.
If a handler is omitted, the default behavior is to pass the state object through to the next middleware, unchanged.
Proof-of-concept: React Router-like API
As a proof-of-concept, the react-router/
directory includes utilities for implementing a React Router-like API using middleware. It supports:
- Nested route matching, with params
- Plain object routes or JSX routes
- Asynchronous route fetching, using
config.getChildRoutes()
- Asynchronous component fetching, using
config.getComponent()
- Index routes
Not yet completed:
<Redirect>
routes
Internally, it uses several of React Router's methods, so the route matching behavior should be identical.
Example:
import { createRouter } from '@acdlite/router'
import { nestedRoute, getComponents, Route, IndexRoute } from '@acdlite/router/react-router'
import createHistory from 'history/lib/createBrowserHistory'
const reactRouter = createRouter(
nestedRoute(
<Route path="/" component={App}>
<Route path="post">
<IndexRoute component={PostIndex} />
<Route path=":id" component={Post} />
</Route>
</Route>
),
getComponents,
// ... add additional middleware, if desired
)
const history = createHistory()
// Listen for location updates
history.listen(location => {
// E.g. after navigating to '/post/123'
// Routers can accept either a path string or an object with `pathname`,
// `query`, and `search`, so we can pass the location object directly.
reactRouter(location, {
// Route was successful
done: (error, state) => {
// Returns a state object with info about the matched routes
expect(state).to.eql({
params: { id: '123' },
routes: [...] // Array of matching route config objects
components: [App, Post], // Array of matching components
// ...plus other fields from the location object
})
// Render your app using state...
},
// Handle redirects
redirect: (error, state) => {
history.replace(state.redirect)
},
// Handle errors
error: error => {
throw error
}
}
})
A key thing to note is that the server-side API is exactly the same: instead of using history, just pass a path string directly to the router, and implement done()
, redirect()
and error()
as appropriate.
Also note that there's no interdependency between history and your routing logic.
The router returns the matched components, but it's up to you to render them how you like. An easy way to start is using Recompose's nest()
function:
const Component = nest(...state.components)
ReactDOM.render(<Component {...state.params} {...state.query} />)
That gets you 90% of the way to parity with React Router. Conveniences like the <Link>
component and transition hooks would need to be re-implemented, but are fairly straightforward.