进阶使用
生命周期
自动注入
对于普通的业务逻辑,可以直接使用 withRouter(Component, { lifecycle: true })
的配置来装饰组件,让组件自动注入如下四个生命周期:
inited
:对应 hy 的 getInitData 方法,在页面被打开且带有数据时触发。获取参数内容为:{ type: 'push', payload: data }
;actived
:对应 hy 的 onShow 方法,在页面激活时触发,不管是 push 打开还是 pop 返回都会触发。获取参数内容为:{ type: 'show' }
;received
:对应 hy 的 onReceiveData 方法,在页面从后面的页面 pop 返回且带有数据时触发。获取参数内容为:{ type: 'pop', payload: data }
;deactived
:对应 hy 的 onHide 方法,在页面失活的时候触发。
import { withRouter } from '$router'; // $router为hy2项目特有的别名配置,也可以直接引入node_modules/yo-router
class A extends Component {
inited(data) {
console.log(data)
}
received(data) {
// 这里默认绑定了 this
this.setState({ })
}
render() {
return (
<div>hello</div>
)
}
}
export default withRouter(A, { lifecycle: true })
手动注入
自动注入在大部分场景下都能正常使用,如果要自定义激活配置,可以采用手动注入的方法。这里依然需要使用 withRouter
装饰器,该装饰器会给组件的 props
注入 router
对象,该对象拥有 setRouteLifecycleHook
方法,可以自行设置生命周期。
我们设置的生命周期主要分两个生命周期:active
与 deactive
,其中 active
生命周期分三种:show
、push
和 pop
,分别表示页面激活、页面接受打开时数据和页面接受上一个页面关闭回传的数据。
class A extends Component {
componentDidMount() {
router.setRouteLifecycleHook(
props.route, // 传入本组件对应的当前路由
'active', // 传入生命周期名称,支持两种生命周期:'active' 和 'deactive'
(data) => { // 生命周期回调,其中 active 的生命周期会带一个 type,表示该页面的数据是通过 push 还是 pop 获得的,已便数据的接收
switch (data.type) {
case 'push':
this.getInitData()
return
case 'pop':
this.onReceiveData()
return
default:
this.onActive()
return
}
}
)
router.setRouteLifecycleHook(
props.route,
'deactive',
this.onDeactive.bind(this)
)
}
getInitData() {
// 获取页面初始数据逻辑
}
onReceiveData() {
// 获取页面返回数据逻辑
}
onActive() {
// 页面加载激活监听
}
onDeactive() {
// 页面失活监听
}
}
export default withRouter(A)
代码分割与异步路由加载
对于大型单页应用、多页应用以及 hy 上的应用来说,在一个页面中加载全部的静态资源都是一种浪费,因此我们在 router 中支持了根据页面按需引入页面内所需的资源。
代码分割
代码分割 是 webpack 提供的一种异步加载静态资源的方式,每次 require.ensure
的调用会生成一个加载点,被异步引入的资源会生成一个非入口分块(non-entry chunks)。
代码分割让我们保证了页面入口资源大小可以得到保证,对于单页应用来说,页面的资源是异步递增的过程;对于多页应用来说,每个页面的资源也可以得到控制,仅包含自身页面的业务逻辑。这种方式与之前的全部资源全量引入相比,各有其优劣,但是对于大型项目来说,使用代码分割绝对是最好的选择。
由于代码分割可能会导致资源过于分散的问题,因此找到一个控制代码分割的点就尤为重要;而 router 本身就是一个视图控制工具,用来控制代码分割的方式和位置最为合适。
代码分割的基本场景如下:
require.ensure(['some'], function() {
var a = require('module-a');
// ...
});
这里的 require.ensure
是告诉 webpack/ykit,让其构建分块代码,而不是将资源直接打包到 bundle 中去,其中第一个参数是分块代码依赖的资源,主要用于分块代码内部没有引入依赖、或多个引入代码的公共依赖,在我们路由场景中基本不会用到,所以写个空数组就行。
也许有同学会关心加载的多个 chunk 之间,如果存在公共代码应该怎么处理?这里可以使用 webpack 的 CommonsChunkPlugin 插件,或者在 hy2 项目里使用 ykit dll
进行公共依赖的构建。
异步路由加载
在 router 里,我们不仅支持 页面组件的异步加载,同时也支持 整个子路由的动态加载。当然,这样的加载方式会给之前 JSX 的声明式的路由构建方式带来一些不便,这种场景下使用纯对象的路由构建更加方便,当然对于纯组建的异步加载,可以尝试我们提供的 require.async
语法糖。
组件可以定义 getChildRoutes
、getIndexRoute
与 getComponents
方法,这些获取子路由和组件的方法是按需且异步的。
异步加载组件
使用异步方法加载组件,应该是项目初期最常见的一种需求,我们先看下同步加载的写法:
import User from './User'
<Route path="/user" component={User} />
这里表示在跳转到路由 /user
时,进行组件 User
的渲染,但实际上由于我们是实现 import 了该组件,因此它的全部 js 逻辑实际上已经引入到页面内部了。
如果要将其转换为动态加载,可以这样编写:
<Route path="/user" getComponent={(nextState, cb) => {
require.ensure([], () => cb(null, require('./User')))
}} />
这里要注意的一点是,getComponent
方法传入回调的第一参数是 nextState
,是路由中通过历史传递的 location.state
,这个历史 state
目前仅在单页的 browserHistory 中存在,在多页场景和 hash 场景中都不推荐使用,我们推荐使用通过 生命周期 的方式跨页面传递数据,因此不推荐使用这个参数。
第二个参数 cb
就是用来异步获取组件的回调,其中该回调第一个参数是错误参数,第二个参数是组件。如果使用原生的方式进行组件动态加载,那么要注意在导出组件时不要使用 ES6 的 export default
语法,或者在 require
的时候带上 default
属性:
即可以这样写:
// file - User
export default User
<Route path="/user" getComponent={(nextState, cb) => {
// 注意这里是 default
require.ensure([], () => cb(null, require('./User').default))
}} />
也可以这样写:
// file - User
module.exports = User
<Route path="/user" getComponent={(nextState, cb) => {
// 注意这里是 default
require.ensure([], () => cb(null, require('./User')))
}} />
如果觉得麻烦,在使用了 @qnpm/ykit-config-hy2">ykit-config-hy2 的前提下可以尝试我们提供的 require.async
语法糖,用法如下:
const User = require.async('./User');
<Route path="/user" getComponent={User} />
我们处理了 ES6 模块加载的问题,也隐藏了复杂的异步逻辑。
异步加载首页路由、子路由
异步加载首页路由、子路由的方式同异步加载组件类似,只不过 cb
回调传递的第二个参数为首页路由对象和子路由数组。之前同步配置的方式如下:
import A from './routes/A'
import B from './routes/B'
import Index from './routes/Index'
<Route path="course/:courseId">
<IndexRoute component={Index}>
<Route path="/a" component={A} />
<Route path="/b" component={B} />
</Route>
而异步配置的方式如下:
<Route
path="course/:courseId"
getIndexRoute={(nextState, callback) => {
require.ensure([], function (require) {
callback(null, {
component: require('./routes/Index'),
})
})
}}
getChildRoutes={(nextState, callback) => {
require.ensure([], function (require) {
callback(null, [
require('./routes/A'),
require('./routes/B')
])
})
}}
/>
这样可以保证整个页面的子路由结构都是异步载入的,可以在具体的页面内部再进行路由的配置。
注意,在异步配置首页路由时,需要传入一个路由对象,而配置子路由时,需要传入一个数组对象。
目前在这种场景下我们还没有提供更方便的语法糖配置,在后续的版本中我们会加入便捷的配置方式。
ViewStack
<ViewStack />
是 yo-router 内置的一个组件,用于管理在单页场景下,每个页面渲染的层级和顺序。react-router 并没有提供视图堆栈的功能,当用户打开 A -> B -> C 三个页面时,A、B 页面会被依次销毁,最终展现的只有 C 页面;而在我们 yo-router 的默认场景下,这三个页面会按照顺序堆叠在我们的视图堆栈中,通过 zIndex
属性控制他们的显示层级。
之所以引入这一概念,是为了要保持 ScrollView 等组件的滚动条位置,而 react-router 并没有很好的解决这一问题(通过其他的插件可以让 router 保持浏览器的滚动条位置,但始终无法记录 ScrollView 或 ListView 组件本身的位置)。
为了实现视图堆栈,我们像 QApp 一样采用了 <yorouter-root>
作为视图根容器,使用 <yorouter-view>
来包裹每一个页面的 component,实现的效果如下图:
/**
* +-----------------------+
* | yorouter-root |
* | |
* | +---------------+ |
* | | yorouter-view | |
* | | | |
* | | +---------+ | |
* | | | A | | |
* | | +---------+ | |
* | +---------------+ |
* | |
* | +---------------+ |
* | | yorouter-view | |
* | | | |
* | | +---------+ | |
* | | | B | | |
* | | +---------+ | |
* | +---------------+ |
* | |
* +-----------------------+
*/
<Route path="/">
<Route path="a" component={A} />
<Route path="b" component={B} />
</Route>
如果不想使用这一特性,可以通过在 <Router>
组件上设置 useViewStack
为 false
来关闭它,但要注意,一旦关闭使用之后,之前设置的 extraRootClass
、extraRootStyle
和每个路由单独设置的 extraClass
、extraStyle
均会失效:因为这些设置实际上是针对 <yorouter-root>
与 <yorouter-view>
的设置,当不使用视图堆栈时,就不会在页面上生成 <yorouter-root>
和 <yorouter-view>
组件。