本文介绍了基于 Meteor 1.6.x + Ant Design Table + recompose + react-router-dom v4 实现分页的整个流程,从项目创建到最终实现完分页效果每个步骤都非常详细,同时也提供了 git 提交记录,用来让大家学习和分析每一步代码的变化。项目地址:https://github.com/nmgwddj/meteor-pagination

最终效果

创建项目

1
2
3
4
5
6
7
8
9
~ meteor create --full meteor-pagination
Created a new Meteor app in 'meteor-pagination'.

To run your new app:
cd meteor-pagination
meteor

If you are new to Meteor, try some of the learning resources here:
https://www.meteor.com/tutorials

为了保守起见我们初始化以下 git 仓库,commit 一次。

1
2
~ cd meteor-pagination
~ git init

整理项目目录

默认创建的项目不是 react 结构的,我们需要删除掉无用的文件,并修改一下项目,使其支持 react,参考 Meteor 官方 React 教程 首先执行如下命令,安装 react 和 react-dom 组件,你也可以使用 npm 来安装 package,我比较习惯用 yarn。安装完成后删除无用的文件,见此次提交

1
yarn add react react-dom --save

创建 Layout

虽然是个很简洁的项目,但我还是想把创建的整个步骤跟大家描述清楚,避免初学者在学习的过程中遇到各种各样的问题。所以我也会写清楚创建 Layout 和 Router 的过程。 Layout 我们使用 ant design 的组件来实现,所以首先我们要引入 antdreact-router-dom,并在 .meteor/packages 文件中删除项目自带的 kadira:flow-router。避免与 react-router-dom 引起冲突。

1
2
yarn add antd --save
yarn add react-router-dom --save

/imports/startup/client/index.js 中引入 antd 的 css 样式表,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import { Meteor } from 'meteor/meteor'
import { render } from 'react-dom'

import App from '../../ui/pages/App'

import 'antd/dist/antd.css'

Meteor.startup(() => {
render (
<App />,
document.getElementById('render-target')
)
})

/imports/ui/layouts 目录下创建一个 index.js 文件,用来编写 Layout 的代码。

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
import React from 'react'
import Layout from 'antd/lib/layout'
import Menu from 'antd/lib/menu'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

import Home from '../components/Home'
import Links from '../components/Links'

const { Header, Content, Footer } = Layout

const MainLayout = () => {
return (
<Layout className='layout'>
<Header>
<div className='logo' />
<Menu
theme='dark'
mode='horizontal'
defaultSelectedKeys={['home']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key='home'><Link to='/'>Home</Link></Menu.Item>
<Menu.Item key='links'><Link to='/links'>Links</Link></Menu.Item>
</Menu>
</Header>
<Content style={{ padding: '50px 50px 0' }}>
<div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
<Route exact path='/' render={({ match }) => <Home />} />
<Route path='/links' render={({ match }) => <Links />} />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Ant Design ©2016 Created by Ant UED
</Footer>
</Layout>
)
}

export default MainLayout

修改 /imports/ui/pages/App.js 代码,引入 react-router,并导入了 MainLayout 这个 Layout。如下所示:

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import { BrowserRouter as Router, Route } from 'react-router-dom'

import MainLayout from '../layouts'

const App = () =>
<Router>
<Route path='/' component={MainLayout} />
</Router>

export default App

在 MainLayout 中,我们引入了两个组件,一个是 Home,一个是 Links,所以我们还要在 /imports/ui/components/ 下创建 Home.js 和 Links.js,两个文件分别实现为一个简单的组件,然后我们提交一下,见本次提交

订阅数据显示表格

Layout 和 Router 都完成了,接下来我们就在 Links 组件中添加一个表格,用来显示项目默认创建时给生成的 links 数据。首先添加订阅数据所需要的包。

1
meteor add react-meteor-data

然后修改 /imports/ui/components/Links.js 文件,添加 ant design 的 Table 组件并订阅 links collection 数据,这里要注意,我们把组件名由 Links 修改为 LinksTable,避免和导入的 Links collection 名字冲突。代码如下所示:

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
47
48
49
50
51
52
53
54
import React from 'react'
import Table from 'antd/lib/table'
import { withTracker } from 'meteor/react-meteor-data'

import { dateToString } from '../../lib/helpers'
import { Links } from '../../api/links/links'

const { Column } = Table

const LinksTable = ({ dataSource, isReady }) => {
return (
<Table dataSource={dataSource} loading={!isReady}>
<Column
title='Title'
key='title'
dataIndex='title'
/>
<Column
title='URL'
key='url'
dataIndex='url'
render={(t, r) => <a href={t} target='_blank'>{t}</a>}
/>
<Column
title='CreatedAt'
key='createdAt'
dataIndex='createdAt'
render={(t, r) => dateToString(t)}
/>
</Table>
)
}

export default withTracker(() => {
// 订阅数据
const linksHandle = Meteor.subscribe('links.all')
const links = Links.find({}).fetch()
const dataSource = []

// 遍历数据,增加 key 用于表格显示
if (Array.isArray(links)) {
links.map(link => {
dataSource.push({
key: link._id,
...link
})
})
}

return {
isReady: linksHandle.ready(),
dataSource
}
})(LinksTable)

组件中使用了一个方法是 dateToString,用来转换日期,所以我们在项目 /imports 目录下我们新建了一个 lib 目录,存放了一个 helpers 文件,用来放一些常用的方法,因为使用到了 moment,所以我们要添加一下 moment。

1
yarn add mement --save

修改完成后,展示的效果如下:

增加数据量

上面我们已经成功显示了所有 links 中的数据,但是数据量有点小,不方便我们测试分页效果,所以修改一下 /imports/startup/server/fixtures.js 的代码,让程序初始化的时候就默认生成 500 条数据,方便我们调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Fill the DB with example data on startup

import { Meteor } from 'meteor/meteor'
import { Links } from '../../api/links/links.js'

Meteor.startup(() => {
// if the Links collection is empty
Links.remove({})
for (let i = 0; i < 500; i++) {
Links.insert({
title: `Link title (${i})`,
url: `https://www${i}.google.hk.com/`,
createdAt: new Date()
})
}
})

实现按页数订阅数据

在实现分页之前,我们首先要修改一下 publish 的方法,增加一个 currentPage 参数和一个 pageSize 参数,用来在发布数据的时候,制定发布的数据量和数据段。代码如下:

1
2
3
4
5
6
Meteor.publish('links.all', function (currentPage = 1, pageSize = 10) {
return Links.find({}, {
limit: pageSize, // 订阅默认的 10 条数据
skip: (currentPage - 1) * pageSize // 跳过当前页-1 * 每页数据的数据量,与传统分页没有什么区别
})
});

然后就是前端的修改了,首先我们要给 Table 设置一个分页器(默认是有的,但是我们要个性化一下),如下图: 我们个性化了 Table 的分页功能,指定了默认的数据总数、当前页和点击分页按钮时触发的回调函数。这里大家可能看到多出来三个数据,分别是 linksCountcurrentPagesetCurrentPage。这三个数据哪里来的呢?分别起到什么作用呢? 其实他们是我们自己创建的状态,用来记录数据的总数和当前页以及一个设置当前页的函数方法,这些是由一个叫做 recompose 的包创建的,添加 recompose 包。

1
yarn add recompose --save

然后创建两个状态,如下所示:

1
2
3
4
const enhance = compose(
withState('currentPage', 'setCurrentPage', 1),
withState('linksCount', 'setLinksCount', 0)
)

withState 的第一个参数是状态的名字,第二个参数是设置状态的函数名字,第三个方法是状态的初始值。随后我们将两个状态绑定到组件,这样我们就可以在组件中使用这两个状态并且可以调用两个修改状态的方法来修改状态。其实所谓的绑定就是用我们创建好的 enhance 包裹了一下之前写好的 withTracker。:

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
export default enhance(
withTracker(({ currentPage, setLinksCount }) => {
// 订阅数据
const linksHandle = Meteor.subscribe('links.all', currentPage)
const links = Links.find({}).fetch()
const dataSource = []
Meteor.call('links.count', (err, result) => {
if (!err) {
// 设置数据总量
setLinksCount(result)
} else {
console.log(err.message)
}
})

// 用于调试,查看订阅了多少数据
if (Array.isArray(links) && links.length !== 0) {
console.log(links)
}

// 遍历数据,增加 key 用于表格显示
if (Array.isArray(links)) {
links.map(link => {
dataSource.push({
key: link._id,
...link
})
})
}

return {
isReady: linksHandle.ready(),
dataSource
}
})(LinksTable)
)

此时 withTracker 的 props 就增加了两个状态和两个修改状态的方法,我们可以通过 props 把他们导出来使用。可以看到 withTracker 函数中已经有了变化,我们增加了一个 meteor.call,来从服务端获取数据的总数。因为这里我们仅需要一个当前页,用来订阅数据,还需要一个修改数据总数的方法 setLinksCount 用来 meteor.call 返回后设置数据总数(获取到数据总数后传递给表格 pagination 做参数,可以让分页器显示一共有多少页)。服务端实现的获取总数的方法如下:

1
2
3
'links.count' () {
return Links.find().count()
}

这样我们就把整个流程串了起来,首先创建了两个状态,分别是当前页和数据总数,分别传递给组件用于显示当前在第几页和 withTracker 来订阅数据。withTracker 中还使用了 setLinksCount 方法来设置数据总数的状态,让 Table 表格可以看到分页器下面所有页数的按钮(如果不这样做只能看到第一页的按钮),最后在点击分页器第几页按钮时出发 onChange 调用了 setCurrentPage 来设置 currentPage 的数据,此时 currentPage 变化,withTracker 也收到变化通知,会重新到后台订阅数据,重新订阅后,数据发生改变,表格的数据也随之改变。这里是最后一次提交。 以上就是整个分页的流程,如果有不明白的地方,欢迎大家一起讨论。