技术栈

主页 > 前端开发 >

React 的“坑”

尝试了一个星期的 React,觉得这东西不错,打算逐步替换过去的前端架构,但跟接触各种其他框架一样,都有各种坑等着去踩,当然大多是因为思维定势、不熟悉导致的,在这里做一个整理。

我的依赖开发环境:

"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-router-dom": "^4.2.2",
"react-scripts": "1.0.13"

都是最新的组件,仅为体验,比较干净。

一. 如何从服务器获取数据

首先,在目前的实际应用中,页面数据是来自于后端的 API,但是 React 组件是初始化后就开始 render,这个过程没找到简单的方法来打断,那就先给一个空的或包含特定状态(如加载中)的 state 让 render 方法先返回一个再说,然后通过 AJAX 异步从服务端取回数据,再次改变 state 触发更新流程。同步通讯当然也可以,但是强烈不推荐,As of jQuery 1.8, the use of async: false with jqXHR ($.Deferred) is deprecated

class XxxList extends Component {
    constructor(props) {
        super(props);
        this.state = {};
        
        this.componentWillReceiveProps(props);
    };

    componentWillReceiveProps =(props)=> {
        // 显示加载提示
        this.setState({
            ern : -1
        });
    
        // 异步加载数据
        this._loadData(props.params);
    };
    
    shouldComponentUpdate =()=> {
        // 更新属性请求数据时先不更新界面
        return ! this._loading;
    };

    _loadData =(req)=> {
        this._loading = true;
        let dat = toFormData(req); // 将普通对象转为 FormData, 这是自定义的方法
        fetch(XXX_LOAD_URL, {
            body: dat,
            method: "POST",
            credentials: "include"
        })
        .then(rsp => {
            return rsp.json();
        })
        .then(rst => {
            this._loading = false;
            this.setState({
                list: rst.list,
                page: rst.page
            });
        });
    };

    render() {
        if (this.state.ern == -1) {
            return (<div>加载中...</div>);
        }
    
        // 组织列表
        let listHtml = [];
        for (let info of this.state.list) {
            listHtml.push(
                <li key={info.id}>{info.name}</li>
            );
        }

        return (
            <ul>
                {listHtml}
            </ul>
        );
    };
}

上面的异步加载过程还好理解,两次 render 嘛。但也许你看过关于 React 组件生命周期的文章后,可能会疑问为什么要重写 componentWillReceiveProps 方法而不直接在构造方法里 _loadData 呢?后者当然是可以的,这里有个“坑”,起初我理解每次 render 里 <XxxComponent/> 都是在 new 一个组件,但经过调试发现并不是,组件仅初始化了一次,之后再进入那个代码就是更新组件的 props 了。也许这就是为什么在组织列表时要给个 key 了,不给就报 Warning(按 React 的介绍上是能自动用列表索引作为键)。

额外的,这里 fetch 需要注意,如果服务端需要会话且依赖 Cookie 里的会话 ID,务必加上 credentials: "include",否则 Cookie 不会传递,没法正常工作。

二. 下级组件如何与上级通讯

这个相对简单,其实很多 React 的例子已经间接的给出方法了,比如:

<button onClick={this.onBtn1Click}>点我</button>

换位思考一下,把 button 换成我自定义的组件,在这个自定义组件里产生某个事件或某状态改变时,调用 props 里注入进来的方法就能达到通知上级的目的了。以分页为例:

class XxxDemo extends Component {
    // 省略其他方法...
    render() {
        return (
            <div>
                {/*其他懒得写了*/}
                <Pager onGoto={this._loadData} params={this.props.params}/>
            </div>
        );
    };
}
class Pager extends Component {
    // 省略其他方法...
    _gotoPage =(pn)=> {
        let params = this.props.params || {};
        params.pn = pn;
        // 调用上级通过属性传递过来的方法
        this.props.onGoto(params);
    };
    render() {
        let params = this.props.params || {};
        let pn = params.pn ? parseInt(params.pn) : 1;
        
        return (
            <div>
                <button onClick={this._gotoPage.bind(this, pn - 1)}>上一页</button>
                <button onClick={this._gotoPage.bind(this, pn + 1)}>下一页</button>
            </div>
        );
    };
};

上面代码写得很不严谨,真实场景至少得判断一下边界。至于 params 相关的代码该放哪 Pager 级还是其父级,根据实际情况自行决定吧。

三. 上级组件如何与下级通讯

我尝试了一些方法,比如在 render 里把子组件赋给当前组件对象的一个变量,但发现没有叫 setState 也没有 setProps 的方法,貌似是个叫 ReactCompositeComponentWrapper 的对象。然后试了直接 new 对应的组件对象,报错 “Objects are not valid as a React child”。

后来,偶然发现 ref 这个属性(抱歉,我很少仔细的读文档,习惯自己一点点试着来)。上面说过在列表中对组件加 key 来避免 Warning,那么这个 ref 就是另一个有特别意义的键了,加上他后,就可以利用 this.refs.REF_VALUE 来取得对应的字组件对象了,然后当你仅需要更新子组件的时候,就可以用 this.refs.XXX.setState 来更新状态了。

这里需要注意两点,一是初始化流程未执行完 render 时 refs 里是没有子组件对象的,所以使用前务必判断一下存不存在,不存在则走正常方式更新自己;二是并不存在 setProps 方法(至少我用的版本没有),而 props 对象也是只读的,只能通过 state 来更新。

四. 跨层级组件间通讯

在上一节中,实在没招的时候我还尝试过全局和局部“跳线”的方式,但全局“跳线”是程序员的忌讳,会让程序结构混乱不堪,就像一个长满草的机箱。

但是一些例如全局通知之类的公共组件,还是可以注册到全局环境的。这样,只需在构造方法里加上 global.XXX = thiswindow.XXX = this,就能在任意组件里,轻松的用 XXX.setState 来使其更新了。

实际开发中,比较好的方式,一个是所有公共组件都是主组件的子组件,在主组件的 componentDidMount 中将 this.refs.xxx 加入全局环境;另一方面,如果明确公共组件是唯一的且是自己可控的,也可以将公共组件作为主组件的同级,在构造方法种注册到全局环境。

当然了,你也许会说为什么不逐层往下通过 props 传递给子组件呢?一个问题是 render 前在 refs 里拿不到组件对象;二是全局“跳线”并非魔鬼,该是公共的何必藏着掖着呢。

五. React-Router

我用的 4.x 版,而网上搜到的文章多是针对之前版本的,包括搜索很靠前的http://www.ruanyifeng.com/blo...里介绍的,4.x 变化很大。

首先,如果要在 web 环境用,依赖的报选 react-router-dom 即可;其次如果要使用浏览器历史(路径)来定义路由,应当使用 BrowserRouter 而不是在 Router 组件上设置 histroy 属性。精简可用如下:

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
// 省略 import 其他组件...

ReactDOM.render(
    <Router>
        <Switch>
            <Route path="/xxx" component={Xxx}/>
            <Route path="/xxx/:id" component={XxxXx}/>
        </Switch>
    </Router>,
    document.getElementById("root")
);

六. ES6 bind

这个锅应该不能算到 React 头上,但是真让人感觉蛋疼。然后就看到五花八门的写法,比如在构造方法里 bind 的,方法尾巴上加 bind 的;类方法定义也是各种式样。作为一个“强迫症患者”这是不能忍受的,发现 ES6 的 ()=> 这个 lambda 语法有个神奇功能,就是自动把当前 context 给 bind 上去,这太好了。那就统一写成:

    xxx =(arg1, arg2)=> {
        // pass...
    };

看上去整洁、漂亮,如丘比特之箭,哈哈。至于组件的 render,那就不必管了,反正自己是不会调用的,react 在调用的时候一定是 bind 好了的,就不操它的心了。


暂时就这些,总结:React 让前端代码结构性很强,数据绑定的做法非常棒。之后再发现其他“坑”再补充。

责任编辑:admin  二维码分享:
本文标签: react-routerJavaScriptreact.js
点击我更换图片

评论列表