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

最终效果

创建项目

~ 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 一次。

~ cd meteor-pagination
~ git init

整理项目目录

默认创建的项目不是 react 结构的,我们需要删除掉无用的文件,并修改一下项目,使其支持 react,参考 Meteor 官方 React 教程

首先执行如下命令,安装 react 和 react-dom 组件,你也可以使用 npm 来安装 package,我比较习惯用 yarn。安装完成后删除无用的文件,见此次提交

yarn add react react-dom --save

创建 Layout

虽然是个很简洁的项目,但我还是想把创建的整个步骤跟大家描述清楚,避免初学者在学习的过程中遇到各种各样的问题。所以我也会写清楚创建 Layout 和 Router 的过程。

Layout 我们使用 ant design 的组件来实现,所以首先我们要引入 antdreact-router-dom,并在 .meteor/packages 文件中删除项目自带的 kadira:flow-router。避免与 react-router-dom 引起冲突。

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

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

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 的代码。

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。如下所示:

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 数据。首先添加订阅数据所需要的包。

meteor add react-meteor-data

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

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。

yarn add mement --save

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

增加数据量

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

// 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 参数,用来在发布数据的时候,制定发布的数据量和数据段。代码如下:

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 包。

yarn add recompose --save

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

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

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

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 做参数,可以让分页器显示一共有多少页)。服务端实现的获取总数的方法如下:

'links.count' () {
  return Links.find().count()
}

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

以上就是整个分页的流程,如果有不明白的地方,欢迎大家一起讨论。

macOS 启动项大家众所周知的是 系统偏好设置->用户与群组->登录项 里面的设置,但有时候我通过这里无法找到系统自动启动的应用程序,比如 Microsoft Office AutoUpdate 组件,它会在系统启动时自动加载,但登录项里面却看不见。所以网络上搜索一下,摘录了部分重要的内容,原文在此:
https://www.jianshu.com/p/49dabd8ec9bb

继续阅读

本文介绍了 LNMP 环境搭建的 wecenter 如何开启伪静态的方法,从后台开启到配置 nginx 的配置文件,本文都有详细介绍。

开启后台伪静态功能

登录到后台开启伪静态功能,在全局设置-站点功能中。如果你之前开启过伪静态功能,但是没有配置伪静态规则,那么此时可能无法访问后台,显示 404。那么你可以直接跳过这一步,因为你已经开启了伪静态功能,直接去修改一下 nginx 的配置文件就可以了。

修改 nginx 的配置文件

LNMP 环境为我们自动创建好了站点的配置文件,我是通过 lnmp vhost add 脚本来添加的站点,生成的配置文件在 /usr/local/nginx/conf/vhost/站点名称.conf,如果你在使用 lnmp vhost add 命令创建站点时就已经启用了伪静态规则,那么 lnmp 会让你指定一个伪静态的规则文件。如这里所示:https://lnmp.org/faq/lnmp-vhost-add-howto.html,我创建站点的时候选择的是 other 这个规则。你也可以通过查看 /usr/local/nginx/conf/vhost/站点名称.conf 中的配置判断当前使用了哪个规则,如下图:

我们使用的是 other 规则,但实际这个规则什么内容都没有,规则的文件在 /usr/local/nginx/conf 目录。用 vim 打开 /usr/local/nginx/conf/other.conf 文件,写入如下内容然后保存:

location / {
        if (!-e $request_filename)
        {
                rewrite (.*) /index.php;
        }
}

保存成功后,我们就成功配置完成 nginx 的伪静态规则了。此时执行命令 /etc/init.d/nginx restart 来重启 nginx 让规则生效。

如果你在使用 lnmp 创建站点的时候没有选择开启伪静态,那么你可以根据这篇文章手动进行添加:https://lnmp.org/faq/lnmp-vhost-add-howto.html(建议学习一下 vim 的基本用法,让自己可以更自由的在 linux 中翻滚)

锚点是通过在界面中增加一些特征(比如 id),然后在 URL 地址后面加上 #id 就可以访问到指定页面的指定位置,这样可以让我们快速跳转到页面的某个位置,但是在 react-router 中这种方法遇到了问题,因为 react-router 会把 # 当做是 hash 来处理。导致即使跳转到指定页面后,# 后面的锚点也不生效。针对这个问题,在 react-router 的一个 issue 中大家也展开了激烈的讨论。以下是我看过以后整理的几种解决办法。

继续阅读

本文阐述了如何在一个使用了 react-router 的 react 项目中合理的使用 antd-mobile tabbar 功能。在 antd-mobile 官方的例子中可以看到,只需要将不同的组件放置到每个 TabBar.Item 里面就可以了,这样就可以实现简单的切换效果,但是存在几个问题。

一个是切换过程中,路由是不会跟着切换的。比如我们想分享一个地址,当其他人打开这个地址时自动就跳转到第二个 tab 上。如果按上面的方法做是无法实现的。

另外一个问题是这样的设计不太符合大型项目的框架设计,我们往往会制作一些 layouts,给不同的组件匹配不同的 layout。如果按上面介绍的方法做,也是不好实现的。

综合以上两点问题,再加上 Google 了一些资料后,写下本文,以帮助更多遇到类似问题的人。

解决方案

首先定义四个路由分别指定不同的 component,要注意的是这四个路由都统一使用一个 layout,这也就解决了一些大型项目中分多种 layout 的问题。如下代码所示:

<Router history={browserHistory}>
  {/* MainLayout 中包含了 antd-mobile tabbar */}
  <Route path='/' component={MainLayout}>
    {/* 默认跳转到 questions 页面 */}
    <IndexRedirect to='/questions' />
    <Route path='/questions' component={Questions} />
    <Route path='/activities' component={Activities} />
    <Route path='/videos' component={Videos} />
    <Route path='/mine' component={Mine} />
  </Route>
</Router>

随后我们看一下 mainLayout 的代码:

const MainLayout = ({children}) => {
  const pathname = children.props.location.pathname

  return (
    <TabBar
      unselectedTintColor='#949494'
      tintColor='#33A3F4'
      barTintColor='white'
    >
      <TabBar.Item
        title='问答'
        key='questions'
        icon={<div className='questions-icon' />}
        selectedIcon={<div className='questions-selected-icon' />}
        selected={pathname === '/questions'}
        onPress={() => {
          browserHistory.push('/questions')
        }}
      >
        { pathname === '/questions' ? children : null }
      </TabBar.Item>
      <TabBar.Item
        title='活动'
        key='activities'
        icon={<div className='activities-icon' />}
        selectedIcon={<div className='activities-selected-icon' />}
        selected={pathname === '/activities'}
        onPress={() => {
          browserHistory.push('/activities')
        }}
      >
        { pathname === '/activities' ? children : null }
      </TabBar.Item>
      <TabBar.Item
        title='视频'
        key='videos'
        icon={<div className='videos-icon' />}
        selectedIcon={<div className='videos-selected-icon' />}
        selected={pathname === '/videos'}
        onPress={() => {
          browserHistory.push('/videos')
        }}
      >
        { pathname === '/videos' ? children : null }
      </TabBar.Item>
      <TabBar.Item
        title='我的'
        key='mine'
        icon={<div className='mine-icon' />}
        selectedIcon={<div className='mine-selected-icon' />}
        selected={pathname === '/mine'}
        onPress={() => {
          browserHistory.push('/mine')
        }}
      >
        { pathname === '/mine' ? children : null }
      </TabBar.Item>
    </TabBar>
  )
}

这里重点的代码就是 pathname === '/questions' ? children : null,根据当前路由判断加载不同的 component,并且在点击任何一个按钮的时候,自动跳转到指定的路由上。其中 selected 属性也根据路由动态的变换样式。路由传递给 mainLayout 是一个 children,这个 children 中就包含了组件的信息,我们根据路由的不同加载即可。

总结

这样处理后无论我们直接访问 URL 还是点击 tabbar 下面的任意按钮,不但可以切换页面,路由也会随之变动。最重要的是我们套用了 layout,让项目看起来更加合理。

在远程工作中,并不是所有项目都是从头开始的,有很多项目是已经做了一部分,或者需要按着其要求来创建项目和编码的。所以这其中就有一些公司或者团队会使用一些代码规范,以保证无论是公司内部还是远程工作的同事都可以保持一致的代码规范,让代码不会过于凌乱。

继续阅读