资讯专栏INFORMATION COLUMN

React 填“坑”记

libxd / 3143人阅读

摘要:尝试了几天,觉得这东西真心不错,打算逐步替换过去的前端架构,但跟接触其他新框架新技术一样,都有各种坑等着去踩,当然大多是因为不够了解和定势思维导致的,在这里做一个记录整理。

尝试了几天 React,觉得这东西真心不错,打算逐步替换过去的前端架构,但跟接触其他新框架、新技术一样,都有各种坑等着去踩,当然大多是因为不够了解和定势思维导致的,在这里做一个记录整理。

依赖的环境:

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

在此之前,虽说接触了 JS 十几年,但并不太了解 node.js,npm,vue,ES6 等“新潮”的技术,这方面算是个小白。所以为了系统的体验一番,用的都是目前较新的 react 版本。

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

首先,在目前的实际应用中,页面数据是来自于后端的 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 (
加载中...
); } // 组织列表 let listHtml = []; for (let info of this.state.list) { listHtml.push(
  • {info.name}
  • ); } return (
      {listHtml}
    ); }; }

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

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

    2017/10/29 补充 fetch 需注意,首先取得的数据是一个 Response 对象,如果你在 Chrome 的控制台网络里看,响应数据是空的,这是因为这时候还没有开始获取响应的 body,只有在调用 .json() 或其他的数据解析、提取方法后,才会真正的读取响应数据。所以看到很多例子都是第一个 then 里 return xxx.json(),然后在第二个 then 里才开始正式对数据进行处理。

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

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

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

    class XxxDemo extends Component {
        // 省略其他方法...
        render() {
            return (
                
    {/*其他懒得写了*/}
    ); }; } 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 (
    ); }; };

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

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

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

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

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

    四. 跨层级组件间通讯

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

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

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

    当然了,你也许会说为什么不逐层往下通过 props 传递给子组件呢?一个问题是首次 render 前在 refs 里拿不到组件对象(倒是可以把顶层组件对象往下传,但不推荐);二是全局“跳线”只要合理利用就并非魔鬼,该是公共的何必藏着掖着呢。

    那对于非全局的跨组件间互通呢?利用上面提到的 props,refs 都行。我个人推荐涉及事件的总是把事件处理函数通过 props 向下传递,然后在上层事件处理函数里利用 refs 通知另一个子组件变更状态。这有点像传统 DOM 的事件冒泡(扩散),你在外围监听到下级 A 扩散上来的事件,然后改变另一个下级 B。强烈不建议把上层组件对象直接传下去,除非有什么特殊情况。

    五. React-Router

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

    4.x 版的 react-router 变化很大,首先,如果要在 web 环境用,依赖的包选 react-router-dom 即可;其次如果要使用浏览器历史(路径)来定义路由,应当使用 BrowserRouter 而不是在 Router 组件上设置 histroy={browserHistory}。精简可用如下:

    import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
    // 省略 import 其他组件...
    
    ReactDOM.render(
        
            
                
                
            
        ,
        document.getElementById("root")
    );
    六. ES6 bind

    看到五花八门的对象方法写法,还有各种 bind,比如在构造方法里 bind 的,方法尾巴上加 bind 的。作为一个“强迫症患者”这是不能忍受的。发现 ES6 的 ()=> 这个 lambda 语法有个神奇功能,就是自动把当前 context 给 bind 上去,这太好了。那就统一写成:

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

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

    题外话,我找到一本《ES6 in Depth》的电子书,在 《Class》章节的例子里明确的不需要 bind(this),我也不知道 React 这里怎么回事,有清楚这个的希望能告诉我一下。

    七. 导入模块的非 js 资源

    导入模块(JS)是 import "模块名";,那想导入模块里的非 JS 资源、比如 CSS 呢?比如 bootstrap 的 css,可以用 import "bootstrap/dist/css/bootstrap.css";,你可以简单的理解为导入路径(类似 PHP 的 INCLUDE_PATH 或 Java 的 CLASS_PATH)会包含当前项目的 node_modules 目录,而用非 ./,../ 等(如模块名称)开头的路径均到导入路径中去搜索。

    八. 与非 node 的服务端优雅地通讯

    在开发阶段,一个方法是你每次 AJAX 的 URL 总是带上完整的域名和端口,使用这一的绝对 URL,只要确保你启动的 node server 的域一致即可,避免了跨域问题。例如你的应用服务端是 8080 端口,node server 是 3000 端口,接口 URL 写成 http://localhost:8080/path/to/resource 即可,你可以把 http://localhost:8080 部分定义为一个常量,在正式发布时改为线上的域名。但是我不推荐这种方式。

    我认为更好的方式是在 package.json 中增加 proxy: "http://localhost:8080",AJAX URL 路径就正常的 /path/to/resource 即可。经实验,proxy 还可以指向不同域,也就是说你可以愉快的指向你远程的 API 开发(测试)服务器,而不必在自己机器上安装和启动一个。

    然后,可以设置 homepage: "/app/path" 这种,作用就相当于给当前应用一个路径前缀,这样当你发布到生产环境的 web 目录下的 app/path 里时,import 的额外资源(图片等)路径就不会有问题。但是,这个 homepage 并不会影响到你的路由路径,如果最终部署的位置不在网站根目录,你还得老老实实的给你的路由路径加上前缀;但好在 Route 设置可以嵌套,所以只需要在顶层设一个即可。

    以上两项设置后,build 时什么也不用改。

    另外,标准的 react-scripts build 后是到项目下的 build 目录,如果想在执行 build 后直接发布到本地服务端 web 目录,可以在 build 命令末尾增加 && rm -rf ../app/path && mv -f build ../app/path,这是针对 Mac OSX 和 Linux 的命令,Windows 应该是 && del /F ..apppath && move build ..apppath(手头没 Windows 所以没实验)。

    2017/10/29 补充 有时候服务端接口用到了会话,如果会话ID通过 Cookie 传递,而域名又没法一致时(比如直接利用非本地的测试服务器),可以在本地架设一个 nginx 或 apache 再配置一个中间代理来作为跳板,将 cookie 传递过去。看到 node server 里也有 http proxy 之类的模块,貌似这块还挺完善,也可以考虑写一个,有空了再研究。

    九. 上非 node 服务端后刷新 react-route 路径出现 404 错误页

    其实这个很有意思,对服务端编程来说,单入口+路由 的模式已经很常见,导致有的工作时间不长的服务端程序员都没理解为什么会这样,好像天然就如此一样。所以当前端程序员发现上了服务器后一刷新就 404,去找服务端程序员要个说法,服务端程序员也一脸懵逼的样子。

    首先解释一下服务端的单入口是什么个情况。在很久很久以前(呵呵),比如 PHP 或 ASP 做的网站,页面、增删改查程序都是混合在一起的;后来搞 MVC,页面归到模板,与数据逻辑分离;再后来进入初级的前后端分离,服务的归服务,页面的归页面。后两个阶段,利用 apache 或 nginx 的 url rewrite 技术或 path-info 方法,后端程序的路径就不再依赖于他在 web 目录下的路径,甚至完全跟对外的 web 不在一个目录下,既清爽又安全。

    好了,那么要让后端怎么配置呢?这里假定我有一个前端单页应用在网站目录的 static/app1 目录。

    apache 可以在 .htaccess 或对应的 中加入

    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^static/app1/(.*)$ static/app1/ [L]

    nginx 可以在网站对应的 conf 文件的 location / 中加入

    if (!-e $request_filename)
    {
      rewrite ^/static/app1/.*$ /static/app1/index.html last;
    }

    如果已经存在这个 if 块,则在块首加入这个 rewrite 规则即可。

    如果服务端是 Java Servlet (Tomcat, Jetty 等),可以使用第三方的 URLWrite 组件或类似我的 https://github.com/ihongs/Hon... 这样写个简单的路径过滤器,来将某个路径前缀下的所有请求都交给该前缀目录下的 index.html;说得直白点,就是不管请求匹配到的哪个路径,都输出 index.html 的内容。

    但需特别注意,如果服务端也采用这种路由方式,这个路径前缀一定要区分开,比如后端存在路径 app1/resource1/ 那前端就不要使用 app1 这个路径了。我的做法是所有前端静态文件都在 static 目录下,而后端绝对不会使用 static 这个前缀,也就不可能存在冲突了。

    十. 附上前面提到的的 toFormData 函数
    /* global FormData */
    
    import jQuery from "jquery";
    
    export function toFormData (req) {
        if (req instanceof FormData) {
            return req;
        }
        if (req instanceof jQuery) {
            return new FormData(req[0]);
        }
        if (req && req.elements) {
            return new FormData(req);
        }
        
        let dat  = new FormData();
        if (jQuery.isPlainObject (req)) {
            for (let k in req) {
                dat.append(k, req[ k ]);
            }
        } else if (jQuery.isArray(req)) {
            for (let o of req) {
                dat.append(o.name, o.value);
            }
        } else if ( req !== undefined ) {
            throw new Error("Can not conv `"+req+"` to FormData");
        }
        return dat;
    }

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

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/91762.html

    相关文章

    • React开发过程中遇到的一些(踩多少多少)

      摘要:毕竟是一个前端库,所以对于这样的对象还是有一定依赖的,但在下面用的形式写组件的时候就会遇到上面的问题。参考上的这个问题,有这么一个简单粗暴的解决方法目前上还没有人给出更好的解决方法,如果哪位大大有找到的,麻烦补充一下。 document is not defined React毕竟是一个前端库,所以对于document这样的对象还是有一定依赖的,但在node-webkit下面用Com...

      amuqiao 评论0 收藏0
    • React Native 极光推送(ios)

      摘要:前言前一段时间,完成了公司的消息推送功能,使用的是极光推送,在配置的推送功能时,遇到了一个坑,记录一下坑使用了极光推送官方的插件。 前言 前一段时间,完成了公司 app 的消息推送功能,使用的是极光推送,在配置 ios 的推送功能时,遇到了一个坑,记录一下 坑 使用了极光推送官方的插件 jpush-react-native。按照文档,将 ios 和 android 配置好,结果发现 a...

      Travis 评论0 收藏0

    发表评论

    0条评论

    libxd

    |高级讲师

    TA的文章

    阅读更多
    最新活动
    阅读需要支付1元查看
    <