尝试了几天 React,以为这东西真心不错,打算逐步替换过去的前端架构,但跟接触其余新框架、新技术同样,都有各类坑等着去踩,固然大可能是由于不够了解和定势思惟致使的,在这里作一个记录整理。javascript
依赖的环境:css
"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 版本。html
首先,在目前的实际应用中,页面数据是来自于后端的 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 的介绍上是能自动用列表索引做为键)。vue
额外的,这里 fetch 须要注意,若是服务端须要会话且依赖 Cookie 里的会话 ID,务必加上 credentials: "include"
,不然 Cookie 不会传递,无法正常工做。java
2017/10/29 补充 fetch 需注意,首先取得的数据是一个 Response 对象,若是你在 Chrome 的控制台网络里看,响应数据是空的,这是由于这时候尚未开始获取响应的 body,只有在调用 .json()
或其余的数据解析、提取方法后,才会真正的读取响应数据。因此看到不少例子都是第一个 then 里 return xxx.json()
,而后在第二个 then 里才开始正式对数据进行处理。node
这个相对简单,其实不少 React 的例子已经间接的给出方法了,好比:react
<button onClick={this.onBtn1Click}>点我</button>
换位思考一下,把 button 换成我自定义的组件,在这个自定义组件里产生某个事件或某状态改变时,调用 props 里注入进来的方法就能达到通知上级的目的了。以分页为例:jquery
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 级仍是其父级,根据实际状况自行决定吧。nginx
我尝试了一些方法,好比在 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 = this
或 window.XXX = this
,就能在任意组件里,轻松的用 XXX.setState 来使其更新了。
实际开发中,比较好的方式,一个是全部公共组件都是主组件的子组件,在主组件的 componentDidMount 中将 this.refs.xxx 加入全局环境;另外一方面,若是明确公共组件是惟一的且是本身可控的,也能够将公共组件做为主组件的同级,在构造方法种注册到全局环境。
固然了,你也许会说为何不逐层往下经过 props 传递给子组件呢?一个问题是首次 render 前在 refs 里拿不到组件对象(却是能够把顶层组件对象往下传,但不推荐);二是全局“跳线”只要合理利用就并不是魔鬼,该是公共的何须藏着掖着呢。
那对于非全局的跨组件间互通呢?利用上面提到的 props,refs 都行。我我的推荐涉及事件的老是把事件处理函数经过 props 向下传递,而后在上层事件处理函数里利用 refs 通知另外一个子组件变动状态。这有点像传统 DOM 的事件冒泡(扩散),你在外围监听到下级 A 扩散上来的事件,而后改变另外一个下级 B。强烈不建议把上层组件对象直接传下去,除非有什么特殊状况。
我用的 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( <Router> <Switch> <Route path="/xxx" component={Xxx}/> <Route path="/xxx/:id" component={XxxXx}/> </Switch> </Router>, document.getElementById("root") );
看到五花八门的对象方法写法,还有各类 bind,好比在构造方法里 bind 的,方法尾巴上加 bind 的。做为一个“强迫症患者”这是不能忍受的。发现 ES6 的 ()=>
这个 lambda 语法有个神奇功能,就是自动把当前 context 给 bind 上去,这太好了。那就统一写成:
xxx =(arg1, arg2)=> { // pass... };
看上去整洁、漂亮,如丘比特之箭,哈哈。至于组件的 render,那就没必要管了,反正本身是不会调用的,react 在调用的时候必定是 bind 好了的,就不操它的心了。
题外话,我找到一本《ES6 in Depth》的电子书,在 《Class》章节的例子里明确的不须要 bind(this),我也不知道 React 这里怎么回事,有清楚这个的但愿能告诉我一下。
导入模块(JS)是 import '模块名';
,那想导入模块里的非 JS 资源、好比 CSS 呢?好比 bootstrap 的 css,能够用 import 'bootstrap/dist/css/bootstrap.css';
,你能够简单的理解为导入路径(相似 PHP 的 INCLUDE_PATH 或 Java 的 CLASS_PATH)会包含当前项目的 node_modules 目录,而用非 ./
,../
等(如模块名称)开头的路径均到导入路径中去搜索。
在开发阶段,一个方法是你每次 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 ..\\app\\path && move build ..\\app\\path
(手头没 Windows 因此没实验)。
2017/10/29 补充 有时候服务端接口用到了会话,若是会话ID经过 Cookie 传递,而域名又无法一致时(好比直接利用非本地的测试服务器),能够在本地架设一个 nginx 或 apache 再配置一个中间代理来做为跳板,将 cookie 传递过去。看到 node server 里也有 http proxy 之类的模块,貌似这块还挺完善,也能够考虑写一个,有空了再研究。
其实这个颇有意思,对服务端编程来讲,单入口+路由 的模式已经很常见,致使有的工做时间不长的服务端程序员都没理解为何会这样,好像自然就如此同样。因此当前端程序员发现上了服务器后一刷新就 404,去找服务端程序员要个说法,服务端程序员也一脸懵逼的样子。
首先解释一下服务端的单入口是什么个状况。在好久好久之前(呵呵),好比 PHP 或 ASP 作的网站,页面、增删改查程序都是混合在一块儿的;后来搞 MVC,页面归到模板,与数据逻辑分离;再后来进入初级的先后端分离,服务的归服务,页面的归页面。后两个阶段,利用 apache 或 nginx 的 url rewrite 技术或 path-info 方法,后端程序的路径就再也不依赖于他在 web 目录下的路径,甚至彻底跟对外的 web 不在一个目录下,既清爽又安全。
好了,那么要让后端怎么配置呢?这里假定我有一个前端单页应用在网站目录的 static/app1 目录。
apache 能够在 .htaccess 或对应的 <Directory> 中加入
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 这个前缀,也就不可能存在冲突了。
/* 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 让前端代码结构性很强,数据绑定的作法很是棒。以后再发现其余“坑”再补充。