React 填「坑」記

嘗試了幾天 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 = 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(
    <Router>
        <Switch>
            <Route path="/xxx" component={Xxx}/>
            <Route path="/xxx/:id" component={XxxXx}/>
        </Switch>
    </Router>,
    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 ..\\app\\path && move build ..\\app\\path(手頭沒 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 或對應的 <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 這個前綴,也就不可能存在衝突了。

十. 附上前面提到的的 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 讓前端代碼結構性很強,數據綁定的作法很是棒。以後再發現其餘「坑」再補充。

相關文章
相關標籤/搜索