在实际的项目中,你可能还需要 Redux 或者 Mobx 这样的数据流框架以及 React Router 这样的路由管理工具,Yo3 作为一个独立的 React UI 库,可以和任何已有的 React 工具搭配使用。

为了能够让路由能自由的在 SPA / 多页 下进行切换并提供统一的生命周期管理与数据传递方式,我们也参考 React Router 实现了一套 Router 解决方案 Yo-Router。她不仅支持传统的浏览器环境,还支持微信和 Qunar Hybrid 解决方案 Hy 2 环境。

本文将指引你用 Redux 和 Yo-Router 搭配 YApiYIcon 来构建一个简单的应用。

使用 ykit init yo 构建一个 Yo 3 的基本项目,完成后打开页面可以看到一个简单的具有页面跳转功能的 Demo,这个 Demo 其实已经很好的展示了如何用 Yo 3 和 Yo-Router 构建一个应用。

1、Demo解析 #

1.1 HTML 页面 #

我们首先来看看这个页面的 HTML 文件,它位于 src/html 目录下,可以看到这个页面的 css 和 js 的引用方式。这里的 js 文件分成了两部分:lib@VERSION.jsindex@VERSION.js。前者是基础的框架和组件代码,例如 React、Yo-Router 和一些常用的 Yo 3 组件,后者则是余下的代码。这样就把框架和业务代码分开了,不仅减小了 bundle 的总体积同时也加快了编译速度(通常情况下只会编译 index.js 文件,所以如果你更新了 React、Yo 3 这些框架,需要在项目根目录手动执行 ykit dll 以更新 lib 文件)。

<link rel="stylesheet" href="//q.qunarzz.com/yo-ykit-init/prd/page/index@VERSION.css">
<script type="text/javascript" src="//q.qunarzz.com/yo-ykit-init/prd/lib@VERSION.js"></script>
<script type="text/javascript" src="//q.qunarzz.com/yo-ykit-init/prd/page/index@VERSION.js"></script>

你可能注意到了 <head> 标签里有这么一段:

<meta cache-files="qp" href="//q.qunarzz.com/yo-ykit-init/prd/chunk@VERSION.json">

这个是针对代码分割的离线包配置,具体可见 附录1:关于 chunk 模式,关于代码分割后面还会提到。

1.2 JS 入口文件 #

接下来我们来看看 js 的入口文件 /src/index.js,这里有几个地方需要注意一下。

import 'babel-polyfill';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import '@qnpm/hysdk'; // 用来适配大客户端和微信,如果不需要可以去掉
import { Router, Route, IndexRoute, Link } from '$router';
import HomePage from './home';
import yoHistory from '../common/history';

const List = require.async('./list');
const Detail = require.async('./detail');

const nav = (text, right) => {
    const result = {
        title: { text, style: 'text' }
    }
    if (right) result.right = { text: right, style: 'text' }
    return result
}

const Root = () => (
    <Router history={yoHistory}>
        <Route path="/" navigation={nav('首页')}>
            <IndexRoute component={HomePage}/>
            <Route path="list" getComponent={List} navigation={nav('列表页', '点我')} />
            <Route path="detail" getComponent={Detail} navigation={nav('详情页')} />
        </Route>
    </Router>
);

ReactDOM.render(<Root />, document.getElementById('root'));

首先 HySdk 需要先于 Yo-Router 引入,因为在 Hy 2 环境和微信环境中 Yo-Router 依赖了 HySdk。

其次就是后面的 require.async 引用,这就是前面提到的代码分割动态引用,ykit 会将这些页面打成单独的 chunk 文件,只有在进入这个页面时才会去请求和加载,这样可以加快首页的渲染速度。

最后就是对应的 Route 配置,因为是异步的引用,所以对应 Route 的 component 也得用异步对应的语法:

<Route path="list" getComponent={List} navigation={nav('列表页', '点我')} />

Yo-Router 使用 getComponent 来引用异步加载的页面,后面的 navigation 项用来配置该页面的导航参数,与 hysdk/QunarAPI 的配置方式基本相同,详情可见 HySDK 附录:导航参数

1.3 Yo-Router 的 History 配置 #

import { createHashHistory } from '$router';

const yoHistory = createHashHistory({
    uniqueKey: 'create-unique-hybridId'
});

export default yoHistory;

Yo-Router 支持两种历史的创建方式,分别为 createHashHistorycreateBrowserHistory,其中前者是带 hash 的历史,后者是需要后端支持的不带 hash 的历史。这里用了 hash 方式并给项目指定了在 sessionStorage 里进行存储的一个唯一 key,Yo-Router 为了让在页面刷新时所有的历史记录也不被清空,将历史记录放到 sessionStorage 里进行管理。

关于 Yo-Router 的这部分内容,你可以到 基本使用:创建历史 获得更多信息。

1.4 Home 页面 #

import React, { Component } from 'react';
import { Scroller, Touchable } from '$yo-component';
import Header from '$component/header/index.js';
import yoHistory from '$common/history';
import './index.scss';

class HomePage extends Component {
    render() {
        return (
            <div className="yo-flex">
                <Header title="首页" left={false}/>
                <Scroller extraClass="flex">
                    <div className="m-content">
                        <Touchable touchClass="m-content-active" onTap={() => {
                            yoHistory.push('/list');
                        }}>
                            <div>
                                <p className="title">Hello World!</p>
                                <p className="notice">Try To Tap This Area!</p>
                            </div>
                        </Touchable>
                    </div>
                </Scroller>
            </div>
        )
    }
}
export default HomePage;

这里使用了 Yo 3 的 Touchable 和 Scroller 组件,这两个组件也是 Yo 3 最常用的组件。

Touchable 组件是一个"虚拟"组件,它的作用在于绑定点击事件,它并不会创建 DOM 节点,而是将点击的事件绑定到它的子组件上。我们推荐你优先使用 Touchable 来实现一切的点击效果,因为这样不仅能让你的代码更清晰而且也通过组件的复用解决了一些移动端的手势"顽疾"。

Scroller 组件是一个提供滚动效果的容器,它的作用在于统一安卓和 iOS 的滚动效果。因为 Yo 3 默认禁止了原生的滚动,所以你需要在所有需要滚动的组件外加上 Scroller。这里需要尤其注意的是滚动的容器组件的高度,它必须有确定的高度,否则它的高度可能会和内部的容器的高度一样,这样就无法正常的滚动了,这里在 Scroll 文档 里有详细的说明。

可以看到该页面在 Touchable 组件的 onTap 方法中执行了 yoHistory.push('/list');,通过 push 方法转跳到了之前 Router 注册的 list 页面,关于 Yo Router 的跳转 api 可以看 API: History 文档。

接下来的列表页和详情页大致相同,这里也就不再赘述了。

2、Redux 的绑定 #

在一个大型的项目中,类似 Redux 的数据流框架是很有必要的,下面我们来演示下怎么样在这个简单的 Demo 中引入 Redux 来管理它的数据。首先安装 Redux 相关的依赖:

npm i redux react-redux redux-thunk redux-logger -S

创建 store 对象,在 src/page 目录下创建 store.js 文件,这里添加了让 action 能支持异步的 redux-thunk 和方便在开发时候调试的 redux-logger 包。

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import rootReducer from '../reducers';

const middleware = [thunk];
if (process.env.NODE_ENV !== 'production') {
    middleware.push(createLogger()); // 非线上环境加载
}

const store = createStore(
    rootReducer,
    applyMiddleware(...middleware)
);

export default store;

然后修改下入口文件 index.js,将该 store 绑定到根组件上。

import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
    <Provider store={store}>
        <Root />
    </Provider>,
    document.getElementById('root')
);

接下来,我们在 src 目录下创建一个 reducers 目录并在该目录下新建 index.js 文件作为根 Reducer,再把 Demo 中 List 数据相关的代码复制过来,此时的代码如下:

let guid = -1;

function getArrayByLength(length) {
    var ret = [];
    for (var i = 0; i < length; i++) {
        ret[i] = null;
    }
    return ret;
}

function getRandomList(size) {
    return getArrayByLength(size).fill(1).map(num => parseInt(Math.random() * 100));
}

function getRandomDataSource(size) {
    return getRandomList(size).map(num => ({ text: num, key: ++guid }));
}

export default (state = {
    list: getRandomDataSource(25)
}, action) => {
    const { type, payload } = action;
    switch (type) {
        default: {
            return state;
        }
    }
};

现在让我们回到列表页,引入 react-redux 的 connect 方法并将 Redux 的数据绑定到组件的 props 中去。

import { connect } from 'react-redux';

...

const mapStateToProps = state => ({
    list: state.list
});

export default connect(
    mapStateToProps,
    null
)(Detail);

将 List 的 dataSource 属性值置为 props 中绑定的 Redux 数据项,此时 List 的初始数据项就是通过 Reducer 中得到的数据了。

render() {
    const { list } = this.props;
    return {
        <List
            dataSource={list}
        >
            ...
        </List>
    }
}

3、Mock 数据 #

接下来我们来实现刷新和加载更多的功能。在实现这个功能之前,我们首先将数据改造成 mock 的方式,毕竟在一个实际的项目里,这些数据都应该是从后端请求得到的。

对于 mock 我们推荐使用 YApi,它是我们开发的 API 管理平台,她提供了全方位的 API 管理功能,让团队的合作更加的高效,具体的使用可以参考 YApi 使用手册。这里我们已经建好了一个接口,它的 mock 地址为http://yapi.corp.qunar.com/mock/1239/api/list,接下来我们把请求这个接口所返回的数据作为列表页 List 组件的数据源。

首先我们修改下 reducers,增加覆盖更新(UPDATE_LIST,对应下拉刷新)和增量更新(CONCAT_LIST,对应加载更多)的 Action type:

export default (state = {
    list: []
}, action) => {
    const { type, payload } = action;
    switch (type) {
        case 'UPDATE_LIST': {
            return {
                ...state,
                list: payload.data
            }
            break;
        }
        case 'CONCAT_LIST': {
            return {
                ...state,
                list: state.list.concat(payload.data)
            }
            break;
        }
        default: {
            console.log(state);
            return state;
        }
    }
};

接下来编写 actions。在 src 目录下新建 actions 目录,在新建目录下创建 index.js:

export const fetchList = page => dispatch => (
    fetch('http://yapi.corp.qunar.com/mock/1239/api/list')
        .then(res =>res.json()
            .then(data => {
                if (page === 1) {
                    return dispatch({
                        type: 'UPDATE_LIST',
                        payload: data

                    })
                } else {
                    return dispatch({
                        type: 'CONCAT_LIST',
                        payload: data

                    })
                }
            }
        ))
);

将 action 方法绑定到列表页组件中。

import { fetchList } from '../../actions';

...

const mapStateToProps = state => ({
    list: state.list
});

const mapDispatchToProps = dispatch => ({
    fetchList: params => dispatch(fetchList(params))
});

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Detail);

在组件中调用绑定的 action 方法以触发请求:

class Detail extends Component {
    constructor() {
        super();
        this.page = 1;
    }

    refresh() {
        const { fetchList } = this.props;
        this.page = 1;
        fetchList(this.page);
    }

    fetch() {
        const { fetchList } = this.props;
        fetchList(++this.page);
    }

    componentDidMount() {
        this.fetch(this.page);
    }

    render() {
        const { list } = this.props;
        if (list && list.length > 0) {
            return (
                <div className="yo-flex">
                    <Header title="列表页" right={{
                        title: '点我',
                        onTap: () => alert('hello')
                    }}/>
                    <List
                        ref="list"
                        extraClass="flex m-list"
                        dataSource={list}
                        renderItem={(item, i) => <div>{ i + ':' + item.text}</div> }
                        infinite={true}
                        infiniteSize={20}
                        itemHeight={44}
                        usePullRefresh={true}
                        onRefresh={() => {
                            setTimeout(() => {
                                this.refresh();
                                this.refs.list.stopRefreshing(true);
                            }, 500);
                        }}
                        useLoadMore={true}
                        onLoad={() => {
                            setTimeout(() => {
                                this.fetch();
                                this.refs.list.stopLoading(true);
                            }, 500);
                        }}
                        itemExtraClass={(item, i) => {
                            return 'item ' + i;
                        }}
                        onItemTap={(item, i, ds) => {
                            yoHistory.push('/detail');
                        }}
                    />
                </div>
            );
        } else {
            return null;
        }
    }
}

现在列表页组件的数据就完全交给 Redux 来管理了,整个组件已经不再有自己的 state 了。

当然你也可以用 YKit 来实现数据的 mock 功能,具体可见 YKit:代理工具 的Mock服务章节。

4、添加 Iconfont #

现在我们来看看列表页的交互逻辑,在点击一个列表项后,就会进入对应的详情页。一般来讲,可点击进入下级页面的列表项应该在其右侧加一个右箭头 > ,这样可以从视觉上让用户感知到这个列表是可点击的。

由于 Yo 3 中已经内置了这个图标(Yo 3 自带字体图标一览),所以我们直接引用就好了。

<List>
    renderItem={(item, i) => [
        <div key={item.key} className="flex">{item.text}</div>,
        <i key={`${item.key}${item.key}`} className="yo-ico">&#xf07f;</i>
    ]}
    ...
</List>

可以看到这里返回了一个数组,这是 React 16 的语法(详情可见 React v16.0),Yo 3.1.x 已经全面转向 React 16 了。

现在你应该可以看到列表项的右箭头了,整个页面页显得更正式了一些。让我们更进一步,把左上角的那个“点我”文字换成客服图标,不过 Yo 3 里并没有内置这样的图标,所以我们不得不自己添加图标了。我们推荐使用 YIcon 来管理字体图标,Yo 3 的默认字体图标就是在托管在 YIcon 上的。

打开 YIcon 登陆,新建图标项目,我们把它叫做 hy_fe_demo,在公开项目中搜索一个图标,把它加到新建的图标项目中。让后选择配置 source 路径,这里首先要在 gitlab 仓库中 source 组里创建对应的项目(需要联系该组的管理员),再来填写该项目的路径,弄完后,点击同步 source 即可将 icon 文件同步到 gitlab 仓库和图标资源服务器(s.qunarzz.com)上。

弄好了这些后,我们需要将 Yo 3 的图标字体路径修改成我们新建的字体项目的资源路径,在 src/yo-config/core/config.scss 文件中加上:

$ico: (
    // {Boolean} 是否使用图标字体
    is-use:     true,
    // {String} 图标字体文件名
    font-name:  hy_fe_demo,
    // {Url} 图标字体路径
    font-path:  "//s.qunarzz.com/app_utils/fonts/1.0.0/"
);

替换“更多”为图标字体:

render() {
    ...
    return (
        <div className="yo-flex">
            <Header title="列表页" right={{ title: <i className="yo-ico">&#xe21c;</i>, onTap: () => alert('hello') }}/>
            <List>
                ...
            </List>
        </div>
    );
}

刷新一下页面,可以看到列表页右上角的更多变成了我们选择的图标,但是也可以看到之前的图标都失效了。这是因为我们新建的字体图标库没有引用 Yo 3 自带的图标。所以为了保险起见,我们把 Yo 3 用到的所有图标都复制到新建的图标项目中。

首先到图标库里选择 Yo,然后把所有的图标都加到购物车中。

点击购物车,选择保存到已有项目,选择我们方才新建的项目。

然后进入我们的图标项目中,选择同步 source。

因为之前已经建立了一个版本(1.0.0),这时 YIcon 会提醒我们选择升级的类型,这里我们选择小版本迭代,点击确认。同步成功后我们会看到图标的版本升到了 1.0.1。

因为版本升级了,所以我们得再去更改下图标字体的路径:

$ico: (
    // {Boolean} 是否使用图标字体
    is-use:     true,
    // {String} 图标字体文件名
    font-name:  hy_fe_demo,
    // {Url} 图标字体路径
    font-path:  "//s.qunarzz.com/app_utils/fonts/1.0.1/"
);

然后等图标资源服务器准备好了之后(大概 5 分钟左右,会有 QTalk 通知),刷新页面就能看到之前的图标恢复正常了。

这样整个教程就到此结束了。我们讲解了 Yo 3 组件 和 Yo-Router 的基本使用,引入了 Redux 数据流框架来管理我们的数据,使用 YApi 模拟了数组相关的后端接口,使用 YIcon 管理了该项目的图标。

有了这些,我们就能够真正的去开发一个比较复杂的项目了。当然在开发过程中,可能还会遇到一系列的问题和挑战,这些就需要我们去探索和解决了。总之,在使用过程中遇到的一些问题,欢迎向我们提出反馈,我们会力所能及的为你们提供支持。

最后,祝你开发愉快。:-)

5、更多相关 #