dva 实践笔记二
作者: dkvirus 发表于: 2018-07-05 22:49:06 最近更新: 2018-08-01 22:07:54

六、dva 中 react-router4.0 使用说明

1. dva 与 react-router 关系

dva 升级到 2.0 版本以后,也将内部使用的 dva/router 从 react-router3.0 升级到了 react-router4.0

react-router4.0 让路由变得更简单,最大特点就是可以路由嵌套,可是如果照搬使用 react-router4.0 的写法,你会发现在 dva 中是行不通的,查看 dva/router 的源码可以看到:

1
2
3
// dva/router.js
module.exports = require('react-router-dom');
module.exports.routerRedux = require('react-router-redux');

其中第一行导出的 react-router-dom 就是 react-router@4.0 文件,第二行导出的 react-router-redux 是 react-router 配合 redux 使用的中间库。因为 dva 中使用到了 redux,所以我们在配置的时候还需要注意到这一点。

由于 dva 将 react-router-domreact-router-redux 都封装到了 dva/router 中,在使用 react-router 和 redux 里面的东西时只需引入 dva/router 这个包即可。

2. dva 中使用 router4.0

router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { routerRedux, Route } from 'dva/router';
import Example from 'routes/Example';

const { ConnectedRouter } = routerRedux;

function RouterConfig({ history, app }) {

return (
<ConnectedRouter history={history}>
<Route path="/" component={Example} />
</ConnectedRouter>
);

}

export default RouterConfig;

说明:

  • Route 为 react-router-dom 内的标签
  • ConnectedRouter 为 react-router-redux 内的对象 routerRedux 的标签,作用相当于 react-router-dom 中的 BrowserRouter 标签,作用为连接 redux 使用。

3. react-router4.0 常用标签总结

英文不错的建议直接去官网读一手文档,对于理解很有帮助。
react-router@4.0 文档 API

以下内容仅为作者在阅读时总结的内容。

3.1 BrowerRouter

1
2
<BrowserRouter basename="/calendar"/>
<Link to="/today"/> // 渲染为 <a href="/calendar/today">

3.2 Route

  • route 的三种写法
    • <Route component>
    • <Route render>
    • <Route children>
  • route 传给组件的参数
    • match 跟爸爸有关
      • isExact 是否完全匹配
      • params 参数,一般指path中的参数
      • path 中 path 属性的值,与浏览器地址栏 url 进行匹配,“/topics/:topicId”
      • url Link 或者 a 标签 中跳转的地址,一般情况下为浏览器地址栏中地址,如果有 basename,浏览器地址栏为 basename + url 的值,“/topics/components”
    • location 跟自己有关
      • pathname 与 match 中的 url 属性相同,“/topics/components”
      • hash: “” 不知所云,换成 hashHistory 也没有值
      • key: “” 随机生成一个6位的字符串,唯一喔
      • search: “” 参数
    • history
      • -history对象,可以通过代码控制前进、后退
  • route 属性
    • path 匹配的路径 “/topics/:topicId”
    • exact 完全匹配
    • strict 结尾斜线匹配

3.3 Redirect

redirect 重定向到新页面会在历史记录栈中替换当前页面,也就是点击浏览器中后退按钮,不会从新页面回到当前页面。

  • to 值为字符串,跳转到新位置,自执行。
  • to 值为对象,最终会转换为一个位置地址。
  • push 值为 true 时不会替换当前页面,而是在历史记录栈中新增一条记录。
  • from 相当于 <route> 中的 path 属性,匹配 url 地址,匹配成功,跳转到另一个 url 地址。
  • to 值为字符串
    该字符串值会与 <route> 中的 path 属性值进行匹配,如果匹配成功,跳转 <route> 中的 component 属性值。
  • to 值为对象
    对象值最终也会转换为 url 值。to 属性相当于 <a> 标签中的 href 属性。

    1
    2
    3
    4
    5
    6
    <Link to={{
    pathname: '/courses',
    search: '?sort=name',
    hash: '#the-hash',
    state: { fromDashboard: true }
    }}/>
  • replace
    相当于微信小程序中的 redirect 属性
    设置为 true 表示替换 history 中的当前页面,设置为 false 为在 history 中新增一条历史记录。

3.5 switch

  • 只会运行其中一个 <route>
  • 子元素只能是 <route> 或者 <redirect>

七、dva 中编码式路由使用说明

1. 路由跳转

引入 dva/router,使用 routerReux 对象的 push 方法控制,值为要跳转的路由地址。routerReux 就是 dva/router 第二个导出的 react-router-redux 包对象。

示例:跳转到 /user 路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
// models > app.js
import { routerRedux } from 'dva/router';

export default {
// ...
effects: {
// 路由跳转
* redirect ({ payload }, { put }) {
yield put(routerRedux.push('/user'));
},
}
// ...
}

2. 携带参数

2.1 传参

routerRedux.push 方法的第二个参数填写参数对象。如:跳转到 /user 路由,并携带参数 {name: 'dkvirus', age: 20}

1
2
3
4
5
6
7
8
9
10
11
12
13
// models > app.js
import { routerRedux } from 'dva/router';

export default {
// ...
effects: {
// 路由跳转
* redirect ({ payload }, { put }) {
yield put(routerRedux.push('/user', {name: 'dkvirus', age: 20}));
},
}
// ...
}

2.2 接收参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// models > user.js
export default {
subscriptions: {
/**
* 监听浏览器地址,当跳转到 /user 时进入该方法
* @param dispatch 触发器,用于触发 effects 中的 query 方法
* @param history 浏览器历史记录,主要用到它的 location 属性以获取地址栏地址
*/
setup ({ dispatch, history }) {
history.listen((location) => {
console.log('location is: %o', location);
console.log('重定向接收参数:%o', location.state)
// 调用 effects 属性中的 query 方法,并将 location.state 作为参数传递
dispatch({
type: 'query',
payload: location.state,
})
});
},
},
effects: {
*query ({ payload }, { call, put }) {
console.log('payload is: %o', payload);
}
}
// ...
}

在 user.js 中 subscriptions 属性会监听路由。当 app.js 中通过代码跳转到 /user 路由,models>user.js>subscriptions 属性中的 setup 方法会被触发,location 记录着相关信息。打印如下。

1
2
3
4
5
6
7
8
9
location is: Object
hash: ""
key: "kss7as"
pathname: "/user"
search: ""
state: {name: "bob", age: 21}
重定向接收参数:Object
age:21
name:"bob"

可以看到 location.state 就是传递过来的参数。在 subscriptions 中可以使用 dispatch 触发 effects 中的方法同时传递参数。

需要注意的事,在 dva@1. 版本中,要获取对象还要用 location.query 对象,而到了 dva@2. 就变成了 location.state 对象。

八、 dva 中页面复用使用说明

本文针对有 dva 开发经验的阅读,没实际开发过可能看着有点晕。

1. 要做的事

最近经过尝试,发现页面也可以进行复用。如下是最最普通的后台管理系统,左边的一个菜单对应右边的一个页面,该页面内有基本的增删改查功能。现在可以将这样一个页面发布成 npm 包,在其他系统如果也需要这个页面,直接 install 对应的包通过简单配置即可。

图1:普通后管系统

我有这个想法源自我开发过好几个后管系统,每个后管系统都有基础的权限管理功能,也就是 用户管理角色管理机构管理。每次我都需要重新开发,哪怕是复制粘贴代码也显示很麻烦。要是其中一个后管系统的权限管理功能有一个地方需要修改,还要去其它后管系统一个个修改。如果将权限管理功能涉及的代码打成 npm 包,每次只需要修改 npm 包对应的那部分代码,之后发布新的版本,用到权限管理菜单的后管系统只需要简单的更新到最新版本即可,这显得很人性化。

dva 可以很方便的做到这一点,下面就介绍 dva 中页面的复用步骤以及注意事项。

2. 思路

2.1 路由改变 pathname —— 菜单数组

通过路由改变浏览器地址栏中 pathname。

dva 中使用的路由是 react-router@4.0,其中有个标签是这样的:<Link to="/user">,它的目的是将浏览器地址栏中 pathname 替换成 /user

图2:菜单树

对于菜单树,每个菜单项都有名称(name),有的菜单项还有图标(icon),菜单有父子关系(parentId),如果菜单是父级菜单,是没有路由的,点击父级菜单只会打开折叠显示所有子菜单,如果菜单是子菜单,拥有路由属性(router),点击菜单会改变浏览器地址栏中的 pathname 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
let menus = [
{
id: '1',
name: 'List Page',
parentId: null,
},
{
id: '11',
name: 'Table List',
router: '/table-list',
parentId: '1',
},
];

通过组装如上的数据结构生成菜单树的 html 代码。如果需要添加新的菜单,只需要追加一个数组即可。数据源变化,左边菜单树会自动新增一项菜单。

1
2
3
4
5
6
7
// ......
const newMenu = [{
id: '2',
name: 'Form Page',
parentId: null,
}]
menus = menus.concat(newMenu);

2.2 dva 监听 pathname 变化并显示对应页面 —— 路由数组

(1)routes 目录和 models 目录

使用 dva-cli 创建的项目有三个很特别的目录:modelsroutesservices

其中 routes 目录下的内容是用 react jsx 语法写的 页面。也就是图1中点击菜单后右边显示的页面。

models 目录提供数据,如右面页面中的表格内容是一个数组,就是由 models 提供给 routes 的,然后 routes 中把这些数据转化为表格。一个 models 对应一个 routes。

(2)dva 监听 pathname 变化并显示对应页面

dva 提供一个 API 叫做注册路由表 app.router(({ history, app }) => RouterConfig)

1
2
3
4
5
6
7
8
9
10
import { Router, Route } from 'dva/router';
// 引入 routes 目录里的“页面”
import TableList from './routes/table-list/index.js'
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/table-list" component={TableList } />
<Router>
);
});

上述代码意思是当 pathname 变成 /table-list 时,就显示 routes 目录下的 TableList 页面。(还记得 pathname 在哪里改变吗?上面路由中有介绍)这里涉及两个东东:path页面,如果页面需要数据,事实上还需要一个 models

实际编码中可以对上述代码稍作修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';
import dynamic from 'dva/dynamic';
import { routerRedux, Route, Switch, Redirect } from 'dva/router';

const { ConnectedRouter } = routerRedux;

function RouterConfig({ history, app }) {
const error = dynamic({
app,
component: () => import('./routes/system/error/index'),
});

// >>>>>>>>>>>>>>>路由数组 START <<<<<<<<<<<<<<<
let routes = [
{
path: '/table-list',
models: () => [import('./models/table-list.index.js')],
component: () => import('./routes/table-list.index.js'),
},
];
// >>>>>>>>>>>>>>>路由数组 END <<<<<<<<<<<<<<<

// 将数据源转换为 <Router > 标签的形式........
return (
<ConnectedRouter history={history}>
<Page>
<Switch>
<Route path="/" exact render={() => (<Redirect to="/dashboard" />)} />
{
routes.map(({ path, ...dynamics }, key) => (
<Route
exact
key={key}
path={path}
component={dynamic({ app, ...dynamics })}
/>
))
}
<Route component={error} />
</Switch>
</Page>
</ConnectedRouter>
);
}

export default RouterConfig;

这里将 path页面 也抽离为数组作为数据源,在通过处理转换为 dva 提供的 API。

上述代码中,已经成功将 path(也就是 pathname) 与 routes 目录下的页面和 models 目录下提供数据的容器绑定到了一起。此时通过 <Link to="/table-list">将浏览器地址栏 pathname 变换成 /table-list 时就可以显示对应页面。

2.3 导出菜单数组和路由数组

经过上面的分析,一个菜单涉及两部分内容:菜单数组决定如何改变 pathname,路由数组决定监听到 pathname 变化时显示什么页面。

只需要将这两个数组导出,在其它后管系统中追加改变数据源即可实现页面的复用。

在 src 目录下新建文件 export.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const menus = [
{
id: '2',
name: 'Form Page',
parentId: null,
}
];

// 注意这里的路径使用相对路径
const routes = [
{
path: '/table-list',
models: () => [import('./models/table-list.index.js')],
component: () => import('./routes/table-list.index.js'),
},
];

const baseObj = {
baseMenus: menus,
baseRoutes: routes
}

export default baseObj ;

修改 package.json 设置入口文件位置:

1
2
3
4
{
"name": "test-user",
"main": "src/export.js"
}

接着就可以发布 npm 包了。对发布过程不了解的可以跳转 npm 发布包流程及填坑指南

在其它后管系统中引用也很简单,将路由数组和菜单数组追加到相应位置即可。

3. 踩坑

3.1 引入 npm 包运行报错

引入 npm 包之后运行会报如下错误。原因是 node_modules 目录下的 npm 包里用到了 es6 的语法,虽然 roadhog 能自动将 es6 代码处理为 es5 ,但这并不包括 node_modules 目录下的代码。

1
2
3
4
You may need an appropriate loader to handle this file type.
| import React from 'react';
| /*
| import { render } from 'react-dom'

解决方法: 修改 .roadhogrc.js 文件,添加 extraBabelIncludes 属性,其中 test-user 为你发布的 npm 包名称。该属性意思是让 roadhog 将 node_modules/test-user 代码中的 es6 处理为 es5,这样就可以正常启动运行了。

1
2
3
4
5
6
7
export default {
"entry": "src/index.js",
"extraBabelIncludes": [
"node_modules/test-user"
],
// ..........
}

3.2 npm 包进行页面复用注意事项

  • 所有后管系统前端框架要一样。npm 包将业务模块的代码抽出,这样其实就是将代码位置挪到 npm 包里,然后在当前后管系统中添加一个路径引用而已;
  • 测试发现,外层后管系统使用 roadhog 可以配置一些路径别名,npm 包里的代码是可以调用到外层的这些别名的,这意味着 npm 包里的 npm 可以与外层系统公用一份配置文件;
  • npm 包里的 Mock 数据没法直接使用。这是因为 roadhog 在启动项目时会自动调用 .roadhogrc.mock.js 文件中的 mock 文件,除非你在该文件中引入 npm 包里的 mock 文件。

3.3 npm 包样式问题

(2018年3月9日更新)最近发现一个比较恶心的问题,打好的 npm 包在其它后管项目中引用,npm 包里如果样式是这么写的。在 index.js 中引入 index.less,会发现 index.less 中的样式不起作用;要是把样式直接在 index.js 以内联的方式 style 发现是可以的。推测原因,外部引入 npm 包,不会执行 .less 文件。

1
2
3
4
- routes
- user
- index.js
- index.less

九、dva 解决退出系统 state 值未清空

使用 dva 开发的后管系统,当退出系统时数据容器 models 中的 state 值并不会清空,造成重新登入系统仍然可以看到上一次登入系统时的数据。下面举例其中一种场景进行说明。

1. 问题

使用 admin 用户登入系统,进入 用户管理 菜单。

表格中总共有三条数据:

表格中总共有三条数据

通过名称进行过滤查询,查的一条数据:

用户列表

现在退出系统,使用 guest 用户再次登入系统,进入 用户管理 菜单,会发现竟然还是一条数据,也就是之前 admin 用户过滤查询之后的数据。

不同用户之间数据竟然无意间进行了共享,这是不被允许的。

2. 分析原因

使用 admin 用户登入,通过名称过滤查询得到一条数据,该数据被放在一个叫做 数据容器 (models 目录下的 state 里面)中,只要 数据容器 值变了,页面也就相应改变。所以页面由三条数据变成了一条数据。

admin 用户退出系统时,清空了 cookie,但是并没有清空数据容器 (models >
state) 中的值,这就导致 guest 用户登入后 数据容器 (models 目录下的 state 里面)中依然还是那一条数据,所以页面还是之前 admin 用户查询的那条数据。

搞清楚这一点,解决这个问题也就只需在退出系统时,手动清空 数据容器(models 目录下的 state 里面)中的值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// models > app.js
export default {
namespace: 'app',
// 数据容器即这里面的属性
state: {
userList: [],
},
effects: {
* logout: ({ payload = {} }, { put, call }) {
// 清空 cookie ,退出到登录页面
}
}
}

3. 解决方法

实践不可行操作方式

src > index.js 中添加 onReducer 钩子函数,监听当触发 app/logout 退出系统时,手动清空数据容器中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ..............

const app = dva({
history: createHistory(),
onError (error) {
message.error(error.message);
},
// >>>>>>>>>>>>>>>>>>> add start >>>>>>>>>>>>>>>>>>>>>>>>
onReducer: r => (state, action) => {
const newState = r(state, action);
// 'app/logout' 为 models 目录文件中 effect 中的方法名
if (action.payload && action.payload.actionType === 'app/logout') {
return { app: newState.app, loading: newState.loading, routing: newState.routing };
}
return newState;
},
// >>>>>>>>>>>>>>>>>>> add end >>>>>>>>>>>>>>>>>>>>>>>>
});

// ..............

对于这种写法不理解的可以手动打印下几个变量的值看看(我也不理解为啥这么写,dva API_zh-CN.md 里提供的写法)。(使用 onReducer 钩子函数在实践应用中并不能达到预期效果。

实践可行的操作方式

想要在注销系统时将 dva 中 state 数据清空,可以刷新页面。每次刷新页面都会重新将 html 以及 js 在浏览器中加载一次,同时运行 app.start() 方法启动 dva 这个步骤。使用 window.location.pathname 跳转 / 路由(location 方法会刷新页面)。

1
2
3
4
5
*logout ({ payload }, { call, put }) {
// 跳转路由,刷新页面,此时 dva 中 state 数据也会在内存中重新刷新
window.location.pathname = '/';
message.success('注销成功。');
},

在路由文件 router.js 中控制跳转:

1
2
3
4
5
6
7
<LocaleProvider locale={zhCN}>
<ConnectedRouter history={history}>
<Switch>
<Redirect exact from='/' to='/login' />
</Switch>
</ConnectedRouter>
</LocaleProvider>
首页
友链
归档
dkvirus
动态
RSS