react-native 基礎入門

基本概念

這裏以我的個人理解,快速過一下 React Native 中一些基本概念。如果和官方的理解有些偏差,還請指出。

1. 組件

React Native 主要是通過 Virtual Dom 來實現顯示頁面或者頁面中的模塊。可以通過 React.createClass() 來創建自己的 Dom,在 React 中稱之爲組件(Component)。創建之後,就可以直接像 HTML 標準標籤一樣使用了。如下:

var MyCustomComponent = React.createClass({  
    ...
});

// 然後就可以這樣使用
     <MyCustomComponent />  

到底什麼是一個組件?我的理解就是頁面上的一個邏輯單元。組件可以小到一個按鈕,也可以大到整個頁面,組件嵌套組合,就成了各種複雜的界面了。

2. 組件生命週期

類似於 Android 中的一個 View,它也有自己的生命週期,有自己的狀態。React 組件的數據保存在自己內部的 state 變量中。每個組件都有自己的生命週期,每個生命週期都有對應的回調,這個和 Android 中的 View 非常類似:

getInitialState:獲得初始化組件狀態,只調用一次
componentWillMount:組件將要加載,只調用一次
componentDidMount:組件加載完成並顯示出來了,也就是完成了一次繪製,只調用一次
render:繪製組件,可能調用多次。
具體要寫自己的頁面的話,要從哪裏入手呢?我們這裏就要來看一下 React.createClass() 是什麼的。這個方法可以輔助你創建一個組件,其中傳入創建組件的參數,就是自定義組件需要的內容。一個基本的自定義組件寫法如下:

var MyCustomComponent = React.createClass({  
    // 這裏返回一個對象,設置組件的初始化狀態,
    // 後面就可以通過 this.state 來獲得這個對象
    getInitialState: function() {
        return {
            key1: data1,
            key2: data2,
            ...
        };
    },
    // 這裏一般做一些和界面顯示無關的初始化操作
    componentWillMount: function() {
    },
    // 這裏一般做加載數據的操作
    componentDidMount: function() {
    },
    // 這是最重要的函數,用來繪製界面,
    // 所有的自定義組件,這個函數是必須提供的
    render: function() {
        return(
            <View>
                ...
            </View>
        );
    },
});

一個自定義組件基本上就是上面那樣定義了。只有 render 函數是必須的,其他都是可選的。

3. 組件的數據

繪製界面部分,一般情況下會根據組件的狀態 state 來繪製動態頁面,例如下面一個最簡單的例子:

render: function() {  
    return(
        <Text>{this.state.key1}</Text>
    );
}

這裏就是直接把狀態中的 key1 的值用 Text 組件直接顯示出來。

另外,React 組件中最重要的一個概念就是 state – 組件的狀態。除了前面的使用 getInitialState 方法來設置初始化狀態外。在界面邏輯處理或者事件交互的過程中,可以調用 this.setState(…) 方法來修改組件的狀態值。

如果在代碼中直接修改 state,React 就會把舊狀態和新狀態做一個 diff,找到變化的部分,然後對應找到和這個變化的值關聯的界面部分,請求重新繪製這個部分。例如剛纔的例子中,如果調用:

this.setState({key1: 'Hello world!'});  

界面上的 Text 內容馬上就會顯示出 Hello world!。

組件中還有一種數據:屬性(Property),這種數據可以通過 this.props 來直接獲取,例如非常常見的

<View style={{flex: 1}}>

這裏的 style 就是 View 這個組件的一個屬性。

那麼屬性(props)和狀態(state)兩種數據有什麼區別呢?一般 屬性 表示靜態的數據,組件創建後,就基本不變的內容,狀態 是動態數據。

4. React Native 佈局

關於 React Native 的佈局,實用的是 FlexBox 實現,類似網頁的 CSS 佈局方法,具體可以參考官方推薦的 A Complete Guide to Flexbox 和官方文檔 Flexbox。關於佈局說起原理比較簡單,但是要很靈活的寫出你想要的樣式,還是需要慢慢積累經驗。

另外,值得一提的是,React Native 中的樣式長度單位,是邏輯單位,概念和 Android 中的 dp 一樣。

以上就是 React Native 的基本邏輯,有了這些概念,我們就可以開始寫 APP 了。

APP 開發實踐

我們要實現的知乎日報的 APP 的主頁面是一個文章列表,左邊可以滑動出來抽屜,賬號信息和顯示主題列表。選擇主題列表,可以在列表頁更新對應主題的文章列表。點擊文章列表進入文章詳情。還有評論,點贊,登錄等功能初期並不計劃做。

1. 抽屜的實現

慶幸的是,官方提供了 DrawerLayoutAndroid 組件,這個組件其實就是對 Android 中的 DrawerLayout 的封裝。可以參考官方文檔,使用過 Native 版本的 DrawerLayout 話,很容易上手這個組件。主要代碼如下:

render: function() {  
  ...
  return (
    <DrawerLayoutAndroid
      ref={(drawer) => { this.drawer = drawer; }}
      drawerWidth={Dimensions.get('window').width - DRAWER_WIDTH_LEFT}
      keyboardDismissMode="on-drag"
      drawerPosition={DrawerLayoutAndroid.positions.Left}
      renderNavigationView={this._renderNavigationView}>
      <View style={styles.container}>
         ...
        {content}
      </View>
    </DrawerLayoutAndroid>
  );
}

其中 renderNavigationView 屬性,表示抽屜裏面顯示的內容。本項目的實現,可以參考:ListScreen.js。

2. 主頁文章列表

文章列表在 Android 可以用 ListView 實現,React Native 也很貼心提供了對應的組件 ListView。實用方法和 Android 原生的也類似,需要提供一個數據源 dataSource 和一個基本的繪製每行界面的函數。借用官方的一個代碼片段:

getInitialState: function() {  
  var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  return {
    dataSource: ds.cloneWithRows(['row 1', 'row 2']),
  };
},

render: function() {  
  return (
    <ListView
      dataSource={this.state.dataSource}
      renderRow={(rowData) => <Text>{rowData}</Text>}
    />
  );
},

這是一個最簡的 ListView 使用的例子。其實,React Native 提供的 ListView 比原生還要強大一些,提供了列表的 Section 的支持,列表可以分節,可以顯示每節的頭部,這個和 iOS 的 UITableView 類似。

因爲知乎日報的文章列表是按照日期分 Section 的。具體的使用方法在官方的例子 UIExplorer 中有例子。問項目中可以參考這個文件: ListScreen.js。

3. 詳情頁的實現

知乎日報的文章詳情頁是使用一個 WebView 顯示內容的。遺憾的是,React Native 官方在 Android 上並沒有提供 WebView 的支持。好在 React Native 很容易集成原生的組件:Native UI Components。我就按照官方文檔,導出一個 React 的 WebView 組件。Java 端的代碼如下:

public class ReactWebViewManager extends SimpleViewManager<WebView> {

    public static final String REACT_CLASS = "RCTWebView";

    @UIProp(UIProp.Type.STRING)
    public static final String PROP_URL = "url";

    @UIProp(UIProp.Type.STRING)
    public static final String PROP_HTML = "html";

    @UIProp(UIProp.Type.STRING)
    public static final String PROP_CSS = "css";

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    protected WebView createViewInstance(ThemedReactContext reactContext) {
        return new WebView(reactContext);
    }

    @Override
    public void updateView(final WebView webView, CatalystStylesDiffMap props) {
        super.updateView(webView, props);
        if (props.hasKey(PROP_URL)) {
            webView.loadUrl(props.getString(PROP_URL));
        }

        if (props.hasKey(PROP_HTML)) {
            String html = props.getString(PROP_HTML);
            if (props.hasKey(PROP_CSS)) {
                String css = props.getString(PROP_CSS);
                html = "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + css + "\" />" + html;
            }
            webView.loadData(html, "text/html; charset=utf-8", "UTF-8");
        }

    }
}

這裏我導出了一個簡單的 WebView,並暴露了 url, html, css 三個屬性。url 表示網頁要顯示的網頁地址,html 表示要加載的 HTML 字符串, css 表示網頁樣式鏈接。還要註冊這個 ReactWebViewManager 到 ReactInstanceManager 中。具體代碼可以看 MyReactPackage.java 和 MainActivity.java

在 JS 端,需要做對應的封裝:

class ObservableWebView extends React.Component {  
  ...

  render() {
    return <RCTWebView {...this.props} onChange={this._onChange} />;
  }
}

ObservableWebView.propTypes = {  
  url: PropTypes.string,
  html: PropTypes.string,
  css: PropTypes.string,
  onScrollChange: PropTypes.func,
};

var RCTWebView = requireNativeComponent('RCTWebView', ObservableWebView, {  
  nativeOnly: {onChange: true}
});

module.exports = ObservableWebView;  
然後就可以在 React 中使用了,如下:

var MyWebView = require('./WebView');

render: function() {  
    return (
      <View style={styles.container}>
        <MyWebView
          style={styles.content}
          html={this.state.detail.body}
          css={this.state.detail.css[0]}/>
      </View>
    );
}

這樣就能直接顯示了網頁內容,挺出乎我意料的簡單。

還有一個細節,官方客戶端,隨着 WebView 的滑動,頭部的 Image 也跟着往上收起來。這裏我們就要監聽 WebView 的滑動事件,然後來設置頭部的 Image 的跟隨移動。還好,官方文檔也提供了一個可以方便從 Native 往 React 傳遞事件的方法:Events。跟着文檔來,實現了一個 ObservableWebView,繼承於原生的 WebView,同時把滑動事件上報給 React:

// ObservableWebView.java
public class ObservableWebView extends WebView {  
    ...
    @Override
    protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt)
    {
        super.onScrollChanged(l, t, oldl, oldt);

        WritableMap event = Arguments.createMap();
        event.putInt("ScrollX", l);
        event.putInt("ScrollY", t);
        ReactContext reactContext = (ReactContext)getContext();
        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                getId(), "topChange", event);
    }
}

這裏在 onScrollChanged() 回調中,就是往 React 中報 topChange 事件。事件中包含 ScrollX 和 ScrollY 兩個值。這裏的 topChange 被映射到了 JS 的 onChange 事件。在 React 層就能這樣用了: ,這裏的 onChangeCallback 是一個自定義的回調函數。WebView 滑動的時候,就會回調到這個函數中來。爲了實用方便,這裏還可以做一些封裝,把 topChange 封裝爲我們關心的滑動事件 onScrollChange:

class ObservableWebView extends React.Component {  
  constructor() {
    super();
    this._onChange = this._onChange.bind(this);
  }

  _onChange(event: Event) {
    if (!this.props.onScrollChange) {
      return;
    }
    this.props.onScrollChange(event.nativeEvent.ScrollY);
  }

  render() {
    return <RCTWebView {...this.props} onChange={this._onChange} />;
  }
}

詳情可以參考:WebView.js。

這時,我們就可以在 React 組件中的 onScrollChange 事件回調中實現滑動詳情頁的頭部圖片的效果:

onWebViewScroll: function(event) {  
    // 這裏移動頭部的 Image
},
render: function() {  
    return (
        <View style={styles.container}>
            <MyWebView
              ...
              onScrollChange={this.onWebViewScroll}/>
            <Image
              ref={REF_HEADER}
              source={{uri: this.state.detail.image}}
              style={styles.headerImage} />
            {toolbar}
        </View>
    );
},

這裏的寫起來也很簡單。關鍵看一下 onWebViewScroll 函數的實現。最簡單的實現方法就是,通過 ScrollY 來設置組件的 state,來讓 React 自動觸發重繪。因爲事件上報非常頻繁,就會觸發大量的重繪,會帶來嚴重的性能問題。

React Native 提供了 Direct Manipulation,也就是直接操作組件,這種方式不會觸發重繪,效率會高很多。

onWebViewScroll: function(event) {  
      // 像素轉爲 React 中的大小單元
    var scrollY = -event / PIXELRATIO;
    var nativeProps = precomputeStyle({transform: [{translateY: scrollY}]});
    // 直接操作組件的屬性
    this.refs[REF_HEADER].setNativeProps(nativeProps);
},

到這裏,實現這個 React Native 版的知乎日報客戶端所涉及的技術點,基本都講完了。還有很多細節請參考源碼:ZhiHuDaily-React-Native,歡迎一起交流,和發 pull request 來一起完善這個項目。

總結
這篇文章幾百字就寫完了,看起來實現這個客戶端並不複雜。其實,這裏有遠超過我想象的坑,後面我應該還會寫一篇文章,來總結這個項目中遇到的坑。總體來說,React Native for Android 作爲初期的版本,實現一個簡單 APP 已經可行。但是它並不完善,如果想用在實際項目中,還需要慎重考量。

最後,大家可以關注這個項目:ZhiHuDaily-React-Native。希望能對開始關注 React Native 的同學有些幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章