摘要:嘗試了幾天,覺得這東西真心不錯,打算逐步替換過去的前端架構(gòu),但跟接觸其他新框架新技術(shù)一樣,都有各種坑等著去踩,當(dāng)然大多是因?yàn)椴粔蛄私夂投▌菟季S導(dǎo)致的,在這里做一個記錄整理。
嘗試了幾天 React,覺得這東西真心不錯,打算逐步替換過去的前端架構(gòu),但跟接觸其他新框架、新技術(shù)一樣,都有各種坑等著去踩,當(dāng)然大多是因?yàn)椴粔蛄私夂投▌菟季S導(dǎo)致的,在這里做一個記錄整理。
依賴的環(huán)境:
"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 等“新潮”的技術(shù),這方面算是個小白。所以為了系統(tǒng)的體驗(yàn)一番,用的都是目前較新的 react 版本。
一. 如何從服務(wù)器獲取數(shù)據(jù)首先,在目前的實(shí)際應(yīng)用中,頁面數(shù)據(jù)是來自于后端的 API,但是 React 組件是初始化后就開始 render,這個過程沒找到簡單的方法來打斷,那就先給一個空的或包含特定狀態(tài)(如加載中)的 state 讓 render 方法先返回一個再說,然后通過 AJAX 異步從服務(wù)端取回數(shù)據(jù),再次改變 state 觸發(fā)更新流程。同步通訊當(dāng)然也可以,但是強(qiáng)烈不推薦,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 }); // 異步加載數(shù)據(jù) this._loadData(props.params); }; shouldComponentUpdate =()=> { // 更新屬性請求數(shù)據(jù)時先不更新界面 return ! this._loading; }; _loadData =(req)=> { this._loading = true; let dat = toFormData(req); // 將普通對象轉(zhuǎn)為 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(
上面的異步加載過程還好理解,兩次 render 嘛。但也許你看過關(guān)于 React 組件生命周期的文章后,可能會疑問為什么要重寫 componentWillReceiveProps 方法而不直接在構(gòu)造方法里 _loadData 呢?后者當(dāng)然是可以的,這里有個“坑”,起初我理解每次 render 里
額外的,這里 fetch 需要注意,如果服務(wù)端需要會話且依賴 Cookie 里的會話 ID,務(wù)必加上 credentials: "include",否則 Cookie 不會傳遞,沒法正常工作。
2017/10/29 補(bǔ)充 fetch 需注意,首先取得的數(shù)據(jù)是一個 Response 對象,如果你在 Chrome 的控制臺網(wǎng)絡(luò)里看,響應(yīng)數(shù)據(jù)是空的,這是因?yàn)檫@時候還沒有開始獲取響應(yīng)的 body,只有在調(diào)用 .json() 或其他的數(shù)據(jù)解析、提取方法后,才會真正的讀取響應(yīng)數(shù)據(jù)。所以看到很多例子都是第一個 then 里 return xxx.json(),然后在第二個 then 里才開始正式對數(shù)據(jù)進(jìn)行處理。
二. 下級組件如何與上級通訊這個相對簡單,其實(shí)很多 React 的例子已經(jīng)間接的給出方法了,比如:
換位思考一下,把 button 換成我自定義的組件,在這個自定義組件里產(chǎn)生某個事件或某狀態(tài)改變時,調(diào)用 props 里注入進(jìn)來的方法就能達(dá)到通知上級的目的了。以分頁為例:
class XxxDemo extends Component { // 省略其他方法... render() { return ({/*其他懶得寫了*/}); }; } class Pager extends Component { // 省略其他方法... _gotoPage =(pn)=> { let params = this.props.params || {}; params.pn = pn; // 調(diào)用上級通過屬性傳遞過來的方法 this.props.onGoto(params); }; render() { let params = this.props.params || {}; let pn = params.pn ? parseInt(params.pn) : 1; return ( ); }; };
上面代碼寫得很不嚴(yán)謹(jǐn),真實(shí)場景至少得判斷一下邊界。至于 params 相關(guān)的代碼該放哪 Pager 級還是其父級,根據(jù)實(shí)際情況自行決定吧。
三. 上級組件如何與下級通訊我嘗試了一些方法,比如在 render 里把子組件賦給當(dāng)前組件對象的一個變量,但發(fā)現(xiàn)沒有叫 setState 也沒有 setProps 的方法,貌似是個叫 ReactCompositeComponentWrapper 的對象。然后試了直接 new 對應(yīng)的組件對象,放到 return 里面后報錯 “Objects are not valid as a React child”。
后來,偶然發(fā)現(xiàn) ref 這個屬性(抱歉,我很少仔細(xì)的讀文檔,習(xí)慣自己一點(diǎn)點(diǎn)試著來)。上面說過在列表中對組件加 key 來避免 Warning,那么這個 ref 就是另一個有特別意義的屬性,加上后,就可以利用 this.refs.XXX 來取得對應(yīng)的子組件對象了,然后當(dāng)你僅需要更新子組件的時候,就可以用 this.refs.XXX.setState 來更新狀態(tài)了。
這里需要注意兩點(diǎn),一是初始化流程未執(zhí)行完 render 時 refs 里是沒有子組件對象的,所以使用前務(wù)必判斷一下存不存在,不存在則走正常方式更新自己;二是并不存在 setProps 方法(至少我用的版本沒有),而且 props 對象也是只讀的,只能通過 state 來更新。
四. 跨層級組件間通訊在上一節(jié)中,實(shí)在沒招的時候我還嘗試過全局和局部“跳線”的方式,但全局“跳線”是程序員的忌諱,會讓程序結(jié)構(gòu)混亂不堪,就像一個長滿草的機(jī)箱。
但是一些例如全局通知之類的公共組件,還是可以注冊到全局環(huán)境的。這樣,只需在構(gòu)造方法里加上 global.XXX = this 或 window.XXX = this,就能在任意組件里,輕松的用 XXX.setState 來使其更新了。
實(shí)際開發(fā)中,比較好的方式,一個是所有公共組件都是主組件的子組件,在主組件的 componentDidMount 中將 this.refs.xxx 加入全局環(huán)境;另一方面,如果明確公共組件是唯一的且是自己可控的,也可以將公共組件作為主組件的同級,在構(gòu)造方法種注冊到全局環(huán)境。
當(dāng)然了,你也許會說為什么不逐層往下通過 props 傳遞給子組件呢?一個問題是首次 render 前在 refs 里拿不到組件對象(倒是可以把頂層組件對象往下傳,但不推薦);二是全局“跳線”只要合理利用就并非魔鬼,該是公共的何必藏著掖著呢。
那對于非全局的跨組件間互通呢?利用上面提到的 props,refs 都行。我個人推薦涉及事件的總是把事件處理函數(shù)通過 props 向下傳遞,然后在上層事件處理函數(shù)里利用 refs 通知另一個子組件變更狀態(tài)。這有點(diǎn)像傳統(tǒng) DOM 的事件冒泡(擴(kuò)散),你在外圍監(jiān)聽到下級 A 擴(kuò)散上來的事件,然后改變另一個下級 B。強(qiáng)烈不建議把上層組件對象直接傳下去,除非有什么特殊情況。
五. React-Router我用的 4.x 版,而網(wǎng)上搜到的文章多是針對之前版本的,包括搜索很靠前的http://www.ruanyifeng.com/blo...里介紹的。
4.x 版的 react-router 變化很大,首先,如果要在 web 環(huán)境用,依賴的包選 react-router-dom 即可;其次如果要使用瀏覽器歷史(路徑)來定義路由,應(yīng)當(dāng)使用 BrowserRouter 而不是在 Router 組件上設(shè)置 histroy={browserHistory}。精簡可用如下:
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; // 省略 import 其他組件... ReactDOM.render(六. ES6 bind, document.getElementById("root") );
看到五花八門的對象方法寫法,還有各種 bind,比如在構(gòu)造方法里 bind 的,方法尾巴上加 bind 的。作為一個“強(qiáng)迫癥患者”這是不能忍受的。發(fā)現(xiàn) ES6 的 ()=> 這個 lambda 語法有個神奇功能,就是自動把當(dāng)前 context 給 bind 上去,這太好了。那就統(tǒng)一寫成:
xxx =(arg1, arg2)=> { // pass... };
看上去整潔、漂亮,如丘比特之箭,哈哈。至于組件的 render,那就不必管了,反正自己是不會調(diào)用的,react 在調(diào)用的時候一定是 bind 好了的,就不操它的心了。
題外話,我找到一本《ES6 in Depth》的電子書,在 《Class》章節(jié)的例子里明確的不需要 bind(this),我也不知道 React 這里怎么回事,有清楚這個的希望能告訴我一下。
七. 導(dǎo)入模塊的非 js 資源導(dǎo)入模塊(JS)是 import "模塊名";,那想導(dǎo)入模塊里的非 JS 資源、比如 CSS 呢?比如 bootstrap 的 css,可以用 import "bootstrap/dist/css/bootstrap.css";,你可以簡單的理解為導(dǎo)入路徑(類似 PHP 的 INCLUDE_PATH 或 Java 的 CLASS_PATH)會包含當(dāng)前項目的 node_modules 目錄,而用非 ./,../ 等(如模塊名稱)開頭的路徑均到導(dǎo)入路徑中去搜索。
八. 與非 node 的服務(wù)端優(yōu)雅地通訊在開發(fā)階段,一個方法是你每次 AJAX 的 URL 總是帶上完整的域名和端口,使用這一的絕對 URL,只要確保你啟動的 node server 的域一致即可,避免了跨域問題。例如你的應(yīng)用服務(wù)端是 8080 端口,node server 是 3000 端口,接口 URL 寫成 http://localhost:8080/path/to/resource 即可,你可以把 http://localhost:8080 部分定義為一個常量,在正式發(fā)布時改為線上的域名。但是我不推薦這種方式。
我認(rèn)為更好的方式是在 package.json 中增加 proxy: "http://localhost:8080",AJAX URL 路徑就正常的 /path/to/resource 即可。經(jīng)實(shí)驗(yàn),proxy 還可以指向不同域,也就是說你可以愉快的指向你遠(yuǎn)程的 API 開發(fā)(測試)服務(wù)器,而不必在自己機(jī)器上安裝和啟動一個。
然后,可以設(shè)置 homepage: "/app/path" 這種,作用就相當(dāng)于給當(dāng)前應(yīng)用一個路徑前綴,這樣當(dāng)你發(fā)布到生產(chǎn)環(huán)境的 web 目錄下的 app/path 里時,import 的額外資源(圖片等)路徑就不會有問題。但是,這個 homepage 并不會影響到你的路由路徑,如果最終部署的位置不在網(wǎng)站根目錄,你還得老老實(shí)實(shí)的給你的路由路徑加上前綴;但好在 Route 設(shè)置可以嵌套,所以只需要在頂層設(shè)一個即可。
以上兩項設(shè)置后,build 時什么也不用改。
另外,標(biāo)準(zhǔn)的 react-scripts build 后是到項目下的 build 目錄,如果想在執(zhí)行 build 后直接發(fā)布到本地服務(wù)端 web 目錄,可以在 build 命令末尾增加 && rm -rf ../app/path && mv -f build ../app/path,這是針對 Mac OSX 和 Linux 的命令,Windows 應(yīng)該是 && del /F ..apppath && move build ..apppath(手頭沒 Windows 所以沒實(shí)驗(yàn))。
2017/10/29 補(bǔ)充 有時候服務(wù)端接口用到了會話,如果會話ID通過 Cookie 傳遞,而域名又沒法一致時(比如直接利用非本地的測試服務(wù)器),可以在本地架設(shè)一個 nginx 或 apache 再配置一個中間代理來作為跳板,將 cookie 傳遞過去??吹?node server 里也有 http proxy 之類的模塊,貌似這塊還挺完善,也可以考慮寫一個,有空了再研究。
九. 上非 node 服務(wù)端后刷新 react-route 路徑出現(xiàn) 404 錯誤頁其實(shí)這個很有意思,對服務(wù)端編程來說,單入口+路由 的模式已經(jīng)很常見,導(dǎo)致有的工作時間不長的服務(wù)端程序員都沒理解為什么會這樣,好像天然就如此一樣。所以當(dāng)前端程序員發(fā)現(xiàn)上了服務(wù)器后一刷新就 404,去找服務(wù)端程序員要個說法,服務(wù)端程序員也一臉懵逼的樣子。
首先解釋一下服務(wù)端的單入口是什么個情況。在很久很久以前(呵呵),比如 PHP 或 ASP 做的網(wǎng)站,頁面、增刪改查程序都是混合在一起的;后來搞 MVC,頁面歸到模板,與數(shù)據(jù)邏輯分離;再后來進(jìn)入初級的前后端分離,服務(wù)的歸服務(wù),頁面的歸頁面。后兩個階段,利用 apache 或 nginx 的 url rewrite 技術(shù)或 path-info 方法,后端程序的路徑就不再依賴于他在 web 目錄下的路徑,甚至完全跟對外的 web 不在一個目錄下,既清爽又安全。
好了,那么要讓后端怎么配置呢?這里假定我有一個前端單頁應(yīng)用在網(wǎng)站目錄的 static/app1 目錄。
apache 可以在 .htaccess 或?qū)?yīng)的
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^static/app1/(.*)$ static/app1/ [L]
nginx 可以在網(wǎng)站對應(yīng)的 conf 文件的 location / 中加入
if (!-e $request_filename) { rewrite ^/static/app1/.*$ /static/app1/index.html last; }
如果已經(jīng)存在這個 if 塊,則在塊首加入這個 rewrite 規(guī)則即可。
如果服務(wù)端是 Java Servlet (Tomcat, Jetty 等),可以使用第三方的 URLWrite 組件或類似我的 https://github.com/ihongs/Hon... 這樣寫個簡單的路徑過濾器,來將某個路徑前綴下的所有請求都交給該前綴目錄下的 index.html;說得直白點(diǎn),就是不管請求匹配到的哪個路徑,都輸出 index.html 的內(nèi)容。
但需特別注意,如果服務(wù)端也采用這種路由方式,這個路徑前綴一定要區(qū)分開,比如后端存在路徑 app1/resource1/ 那前端就不要使用 app1 這個路徑了。我的做法是所有前端靜態(tài)文件都在 static 目錄下,而后端絕對不會使用 static 這個前綴,也就不可能存在沖突了。
十. 附上前面提到的的 toFormData 函數(shù)/* 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; }
暫時就這些,總結(jié):React 讓前端代碼結(jié)構(gòu)性很強(qiáng),數(shù)據(jù)綁定的做法非常棒。之后再發(fā)現(xiàn)其他“坑”再補(bǔ)充。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/91762.html
摘要:畢竟是一個前端庫,所以對于這樣的對象還是有一定依賴的,但在下面用的形式寫組件的時候就會遇到上面的問題。參考上的這個問題,有這么一個簡單粗暴的解決方法目前上還沒有人給出更好的解決方法,如果哪位大大有找到的,麻煩補(bǔ)充一下。 document is not defined React畢竟是一個前端庫,所以對于document這樣的對象還是有一定依賴的,但在node-webkit下面用Com...
摘要:前言前一段時間,完成了公司的消息推送功能,使用的是極光推送,在配置的推送功能時,遇到了一個坑,記錄一下坑使用了極光推送官方的插件。 前言 前一段時間,完成了公司 app 的消息推送功能,使用的是極光推送,在配置 ios 的推送功能時,遇到了一個坑,記錄一下 坑 使用了極光推送官方的插件 jpush-react-native。按照文檔,將 ios 和 android 配置好,結(jié)果發(fā)現(xiàn) a...
閱讀 2800·2021-11-22 14:44
閱讀 541·2021-11-22 12:00
閱讀 3683·2019-08-30 15:54
閱讀 1570·2019-08-29 17:15
閱讀 1898·2019-08-29 13:50
閱讀 1107·2019-08-29 13:17
閱讀 3513·2019-08-29 13:05
閱讀 1181·2019-08-29 11:31