歡迎光臨
每天分享高質量文章

深入淺出 React Native:使用 JavaScript 構建原生應用(下)

新增元件狀態

每個 React 元件都帶有一個key-value儲存的狀態物件,你必須在元件渲染之前設定其初始狀態。

在 SearchPage.js 中,我們對 SearchPage 類中,render方法前新增以下的程式碼。

constructor(props) {

super(props);

this.state = {

searchString: ‘london’

};

}

現在你的元件擁有一個狀態變數:searchString ,且初始值被設定為 london 。

這時候你需要利用起元件中的狀態了。在render方法中,用以下的程式碼替換TextInput元素中的內容:

style={styles.searchInput}

value={this.state.searchString}

placeholder=’Search via name or postcode’/>

這一步設定了 TextInput 元件 value 屬性的值,這個值用於把狀態變數 searchString 的當前值作為展示給使用者的文字。我們已經考慮初始值的設定了,但如果使用者編輯這裡的文字會發生什麼呢?

第一步需要建立一個方法來處理事件。在 SearchPage 類中新增以下的程式碼:

onSearchTextChanged(event) {

console.log(‘onSearchTextChanged’);

this.setState({ searchString: event.nativeEvent.text });

console.log(this.state.searchString);

}

上面的程式碼從 events 中取出了 text 屬性的值,並用於更新元件的狀態。這裡面也添加了一些有用的除錯程式碼。

當文字改變時,需要讓這個方法被呼叫,呼叫後的文字會透過 render 函式傳回到元件中。因此我們需要在標簽上新增一個 onChange 屬性,新增後的標簽如下所示:

style={styles.searchInput}

value={this.state.searchString}

onChange={this.onSearchTextChanged.bind(this)}

placeholder=’Search via name or postcode’/>

當使用者更改文字時,會呼叫 onChange 上 的函式;在本例中,則是 onSearchTextChanged 。

註意:你估計會對 bind(this) 陳述句有疑問。在 JavaScript 中,this 這個關鍵字有點不同於大多數其他語言;在 Swift 表示 “自身”。在這種情況中,bind 可以確保在 onSearchTextChanged 方法中, this 可以作為元件實體的取用。有關更多資訊,請參見MDN this頁面。

在你再次掃清你的應用程式之前,還有一個步驟:在 return 前新增以下陳述句,列印一條日誌來記錄 render() 函式的呼叫:

console.log(‘SearchPage.render’);

你會從這些日誌陳述句中學到一些很有趣的東西!:]

回到你的模擬器,然後按Cmd + R。您現在應該看到文字輸入的初始值為 “london” ,編輯一下文字,從而在 Xcode 控制臺中產生一些日誌:

註意看上面的截圖,日誌列印的順序看起來有些奇怪:

第一次呼叫 render() 函式用於設定檢視。當文字變化時, onSearchTextChanged 函式被呼叫。之後,透過更新元件的狀態來反映輸入了新的文字,這會觸發另一次 render 。 onSearchTextChanged() 函式也會被呼叫,會將改變的字串打印出來。每當應用程式更新任何 React 元件,將會觸發整個UI層的重新繪製,這會呼叫你所有元件的 render 方法。這是一個好主意,因為這樣做把元件的渲染邏輯,從狀態變化影響UI這一過程中完全解耦出來。

與其他大多數 UI 框架所不同的是,你既不需要在狀態改變的時候去手動更新 UI ,或使用某種型別的系結框架,來建立某種應用程式狀態和它的 UI 表現的關聯;例如,我的文章中講的,透過ReactiveCocoa實現MVVM樣式。

在 React 中,你不再需要擔心 UI 的哪些部分可能受到狀態變化的影響;你的整個應用程式的 UI,都可以簡單地表示為一個函式的狀態。

此時,你可能已經發現了這一概念中一個根本性的缺陷。是的,非常準確——效能!

你肯定不能在 UI 變化時,完全拋棄掉整個 UI 然後重新繪製吧

?這就是 React 高明的地方了。每當 UI 渲染出來後,render 方法會傳回一顆檢視渲染樹,並與當前的 UIKit 檢視進行比較。這個稱之為 reconciliation 的過程的輸出是一個簡單的更新串列, React 會將這個串列應用到當前檢視。這意味著,只有實際改變了的部分才會重新繪製。

這個令人拍案叫絕的嶄新概念讓ReactJS變得獨特——virtual-DOM(檔案物件模型,一個web檔案的檢視樹)和 reconciliation 這些概念——被應用於iOS應用程式。

稍後你可以整理下思路,之後,在剛才的應用中你仍然有一些工作要做。日誌程式碼增加了程式碼的繁瑣性,已經不需要了,所以刪除掉日誌程式碼。

初始化搜尋功能

為了實現搜尋功能,你需要處理 “Go” 按鈕的點選事件,呼叫對應的 API,並提供一個視覺效果,告訴使用者正在做查詢。

在 SearchPage.js 中,在建構式中把初始狀態更新成:

this.state = {

searchString: ‘london’,

isLoading: false

};

新的 isLoading 屬性將會記錄是否有請求正在處理的狀態。

在 render 開始的新增如下邏輯:

var spinner = this.state.isLoading ?

(

hidden=’true’

size=’large’/> ) :

( );

這是一個三元運運算元,與 if 陳述句類似,即根據元件 isLoading 的狀態,要麼新增一個 indicator,要麼新增一個空的 view。因為整個元件會不停地更新,所以你自由地混合 JSX 和 JavaSript 程式碼。

回到用 JSX 定義搜尋介面的地方,在圖片的下麵新增:

{spinner}

給渲染“Go”的 TouchableHighlight 標記新增如下的屬性:

onPress={this.onSearchPressed.bind(this)}

接下來,新增下麵這兩個方法到 SearchPage 類中:

_executeQuery(query) {

console.log(query);

this.setState({ isLoading: true });

}

onSearchPressed() {

var query = urlForQueryAndPage(‘place_name’, this.state.searchString, 1);

this._executeQuery(query);

}

_executeQuery() 之後會進行真實的查詢,現在的話就是簡單輸出一條資訊到控制檯,並且把 isLoading 設定為對應的值,這樣 UI 就可以顯示新的狀態了。

提示:JavaScript 的類並沒有訪問修飾符,因此沒有 “私有” 的該奶奶。因此常常會發現開發者使用一個下劃線作為方法的字首,來說明這些方法是私有方法。

當 “Go” 按鈕被點選時,onSearchPressed() 將會被呼叫,開始查詢。

最後,新增下麵這個工具函式在定義 SearchPage 類的上面:

function urlForQueryAndPage(key, value, pageNumber) {

var data = {

country: ‘uk’,

pretty: ‘1’,

encoding: ‘json’,

listing_type: ‘buy’,

action: ‘search_listings’,

page: pageNumber

};

data[key] = value;

var querystring = Object.keys(data)

.map(key => key + ‘=’ + encodeURIComponent(data[key]))

.join(‘&’);

return ‘http://api.nestoria.co.uk/api?’ + querystring;

};

這個函式並不依賴 SearchPage,因此被定義成了一個獨立的函式,而不是類方法。他首先透過 data 來定義查詢字串所需要的引數,接著將 data 轉換成需要的字串格式,name=value 對,使用 & 符號分割。語法 => 是一個箭頭函式,又一個對 JavaScript 語言的擴充套件,提供了這個便捷的語法來建立一個匿名函式。

回到模擬器,Cmd+R,重新載入應用,點選 “Go” 按鈕。你可以看到 activity indicator 顯示出來,再看看 Xcode 的控制檯:

activity indicator 渲染了,並且作為請求的 URL 出現在輸出中。把 URL 複製到瀏覽器中訪問看看得到的結果。你會看到大量的 JSON 物件。別擔心——你不需要理解它們,之後會使用程式碼來解析之。

提示:應用使用了 Nestoria 的 API 來做房產的搜尋。API 傳回的 JSON 資料非常的直白。但是你也可以看看檔案瞭解更多細節,請求什麼 URL 地址,以及傳回資料的格式。

下一步就是從應用中發出請求。

執行 API 請求

還是 SearchPage.js 檔案中,更新建構式中的初始 state 新增一個message 變數:

this.state = {

searchString: ‘london’,

isLoading: false,

message: ”

};

在 render 內部,將下麵的程式碼新增到 UI 的底部:

{this.state.message}

你需要使用這個為使用者展示多種資訊。

在 SearchPage 類內部,將以下程式碼新增到 _executeQuery() 底部:

fetch(query)

.then(response => response.json())

.then(json => this._handleResponse(json.response))

.catch(error =>

this.setState({

isLoading: false,

message: ‘Something bad happened ‘ + error

}));

這裡使用了 fetch 函式,它是 Web API 的一部分。和 XMLHttpRequest 相比,它提供了更加先進的 API。非同步響應會傳回一個 promise,成功的話會轉化 JSON 並且為它提供了一個你將要新增的方法。

最後一步是將下麵的函式新增到 SearchPage:

_handleResponse(response) {

this.setState({ isLoading: false , message: ” });

if (response.application_response_code.substr(0, 1) === ‘1’) {

console.log(‘Properties found: ‘ + response.listings.length);

} else {

this.setState({ message: ‘Location not recognized; please try again.’});

}

}

如果查詢成功,這個方法會清除掉正在載入標識並且記錄下查詢到屬性的個數。

註意:Nestoria 有很多種傳回碼具備潛在的用途。比如,202 和 200 會傳回最佳位置串列。當你建立完一個應用,為什麼不處理一下這些,可以為使用者呈現一個可選串列。

儲存專案,然後在模擬器中按下 Cmd+R,嘗試搜尋 ‘london’;你會在日誌資訊中看到 20 properties were found。然後隨便嘗試搜尋一個不存在的位置,比如 ‘narnia’,你會得到下麵的問候語。

是時候看一下這20個房屋所對應的真實的地方,比如倫敦!

結果顯示

建立一個新的檔案,命名為 SearchResults.js,然後加上下麵這段程式碼:

‘use strict’;

var React = require(‘react-native’);

var {

StyleSheet,

Image,

View,

TouchableHighlight,

ListView,

Text,

Component

} = React;

你肯定註意到啦,這裡用到了 require 陳述句將 react-native 模組引入其中,還有一個重構賦值陳述句。

接著就是加入搜尋結果的元件:

class SearchResults extends Component {

constructor(props) {

super(props);

var dataSource = new ListView.DataSource(

{rowHasChanged: (r1, r2) => r1.guid !== r2.guid});

this.state = {

dataSource: dataSource.cloneWithRows(this.props.listings)

};

}

renderRow(rowData, sectionID, rowID) {

return (

underlayColor=’#dddddd’>

{rowData.title}

);

}

render() {

return (

dataSource={this.state.dataSource}

renderRow={this.renderRow.bind(this)}/>

);

}

}

上述的程式碼裡用到了一個特定的元件 – ListView – 它能將資料一行行地呈現出來,並放置在一個可滾動的容器內,和 UITableView 很相似。透過 ListView.DataSource 將 ListView 的資料引入,還有一個函式來顯示每一行UI。

在構建資料源的同時,你還需要一個函式用來比較每兩行之間是否重覆。 為了確認串列資料的變化,在 reconciliation 過程中ListView 就會使用到這個函式。在這個實體中,由 Nestoria API 傳回的房屋資料都有一個guid 屬性,它就是用來測試資料變化的。

現在將模組匯出的程式碼新增至檔案末尾:

module.exports = SearchResults;

將下麵這段程式碼加到 SearchPage.js 較前的位置,不過要在 require 陳述句的後面哦:

var SearchResults = require(‘./SearchResults’);

這樣我們就能在 SearchPage 類中使用剛剛加上的 SearchResults 類。

還要把 _handleResponse 方法中的 console.log 陳述句改成下麵這樣:

this.props.navigator.push({

title: ‘Results’,

component: SearchResults,

passProps: {listings: response.listings}

});

SearchResults 元件透過上面的程式碼傳入串列裡。在這裡用的是 push方法確保搜尋結果全部推進導航棧中,這樣你就可以透過 ‘Back’ 按鈕傳回到根頁面。

回到模擬器,按下 Cmd+R 掃清頁面,然後試試看我們的搜尋。估計你會得到類似下麵這樣的結果:

耶!你的搜尋實現了呢,不過這搜尋結果頁面的顏值也太低了,不要擔心,接下來給它化化妝。

可點選樣式

這些 React Native 的原生程式碼現在應該理解起來輕車熟路了,所以本教程將會加快速度。

在 SearchResults.js 中,destructuring 宣告後面新增以下陳述句來定義樣式:

var styles = StyleSheet.create({

thumb: {

width: 80,

height: 80,

marginRight: 10

},

textContainer: {

flex: 1

},

separator: {

height: 1,

backgroundColor: ‘#dddddd’

},

price: {

fontSize: 25,

fontWeight: ‘bold’,

color: ‘#48BBEC’

},

title: {

fontSize: 20,

color: ‘#656565’

},

rowContainer: {

flexDirection: ‘row’,

padding: 10

}

});

這些定義了每一行的樣式。

接下來修改 renderRow() 如下:

renderRow(rowData, sectionID, rowID) {

var price = rowData.price_formatted.split(‘ ‘)[0];

return (

this.rowPressed(rowData.guid)}

underlayColor=’#dddddd’>

£{price}

numberOfLines={1}>{rowData.title}

);

}

這個操作修改了傳回的價格,將已經格式了化的”300000 GBP”中的GBP字尾刪除。然後它透過你已經很熟悉的技術來渲染每一行的 UI 。這一次,透過一個 URL 來提供縮圖的資料, React Native 負責在主執行緒之外解碼這些資料。

同時要註意 TouchableHighlight 元件中 onPress屬性後使用的箭頭函式;它用於捕獲每一行的 guid。

最後一步,給類新增一個方法來處理按下操作:

rowPressed(propertyGuid) {

var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];

}

該方法透過使用者觸發的屬性來定位。目前該方法沒有做任何事,你可以稍後處理。現在,是時候欣賞你的大作了。

回到模擬器,按下 Cmd + R 檢視結果:

看起來好多了——儘管你會懷疑是否任何人都能承受住在倫敦的代價!

是時候嚮應用程式新增最後一個檢視了。

房產詳情檢視

新增一個新的檔案 PropertyView.js 到專案中,在檔案的頂部新增如下程式碼:

‘use strict’;

var React = require(‘react-native’);

var {

StyleSheet,

Image,

View,

Text,

Component

} = React;

信手拈來了吧!

接著新增如下樣式:

var styles = StyleSheet.create({

container: {

marginTop: 65

},

heading: {

backgroundColor: ‘#F8F8F8’,

},

separator: {

height: 1,

backgroundColor: ‘#DDDDDD’

},

image: {

width: 400,

height: 300

},

price: {

fontSize: 25,

fontWeight: ‘bold’,

margin: 5,

color: ‘#48BBEC’

},

title: {

fontSize: 20,

margin: 5,

color: ‘#656565’

},

description: {

fontSize: 18,

margin: 5,

color: ‘#656565’

}

});

然後加上元件本身:

class PropertyView extends Component {

render() {

var property = this.props.property;

var stats = property.bedroom_number + ‘ bed ‘ + property.property_type;

if (property.bathroom_number) {

stats += ‘, ‘ + property.bathroom_number + ‘ ‘ + (property.bathroom_number > 1

? ‘bathrooms’ : ‘bathroom’);

}

var price = property.price_formatted.split(‘ ‘)[0];

return (

source={{uri: property.img_url}} />

£{price}

{property.title}

{stats}

{property.summary}

);

}

}

render() 前面部分對資料進行了處理,與通常的情況一樣,API 傳回的資料良莠不齊,往往有些欄位是缺失的。這段程式碼透過一些簡單的邏輯,讓資料更加地規整一些。

render 剩餘的部分就非常直接了。它就是一個簡單的這個狀態不可變狀態的函式。

最後在檔案的末尾加上如下的 export:

module.exports = PropertyView;

傳回到 SearchResults.js 檔案,在頂部,require React 的下麵,新增一個新的 require 陳述句。

var PropertyView = require(‘./PropertyView’);

接下來更新 rowPassed(),新增跳轉到新加入的 PropertyView:

rowPressed(propertyGuid) {

var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];

this.props.navigator.push({

title: “Property”,

component: PropertyView,

passProps: {property: property}

});

}

你知道的:回到模擬器,Cmd + R,一路透過搜尋點選一行到房產詳情介面:

物廉價美——看上去很不錯哦!

應用即將完成,最後一步是允許使用者搜尋附近的房產。

地理位置搜尋

在 Xcode 中,開啟 Info.plist 新增一個新的 key,在編輯器內部單擊滑鼠右鍵並且選擇 Add Row。使用NSLocationWhenInUseUsageDescription 作為 key 名並且使用下麵的值:

PropertyFinder would like to use your location to find nearby properties

下麵是當你添加了新的 key 後,所得到的屬性串列:

你將把這個關鍵的細節提示呈現給使用者,方便他們請求訪問當前位置。

開啟 SearchPage.js,找到用於渲染 Location 按鈕的TouchableHighlight,然後為其新增下麵的屬性值:

onPress={this.onLocationPressed.bind(this)}

當你用手指輕點這個按鈕,會呼叫 onLocationPressed —— 接下來會定義這個方法。

將下麵的程式碼新增到 SearchPage 類中:

onLocationPressed() {

navigator.geolocation.getCurrentPosition(

location => {

var search = location.coords.latitude + ‘,’ + location.coords.longitude;

this.setState({ searchString: search });

var query = urlForQueryAndPage(‘centre_point’, search, 1);

this._executeQuery(query);

},

error => {

this.setState({

message: ‘There was a problem with obtaining your location: ‘ + error

});

});

}

透過 navigator.geolocation 檢索當前位置;這是一個 Web API 所定義的介面,所以對於每個在瀏覽器中使用 location 服務的使用者來說這個介面都應該是一致的。React Native 框架藉助原生的 iOS location 服務提供了自身的 API 實現。

如果當前位置很容易獲取到,你將呼叫第一個箭頭函式;這會向Nestoria 傳送一個 query。如果出現錯誤則會得到一個基本的出錯資訊。

因為你已經改變了屬性串列,你需要重新啟動這個應用以看到更改。抱歉,這次不可以 Cmd+R。請中斷 Xcode 中的應用,然後建立和執行專案。

在使用基於位置的搜尋前,你需要指定一個被 Nestoria 資料庫改寫的位置。在模擬器選單中,選擇 Debug\Location\Custom Location … 然後輸入 55.02 維度和 -1.42 經度,這個坐標是英格蘭北部的一個景色優美的海邊小鎮,我經常在那給家裡打電話。

警示:我們可以正常地使用位置搜尋功能,不過可能有部分同學不能使用(在訪問時傳回 access denied 錯誤)—— 我們尚不確定其原因,可能是 React Native 的問題?如果誰遇到了同樣的問題並且已經結果,煩請告訴我們。這裡沒有倫敦那樣值得炫耀 —— 不過更加經濟!:]

下一步行動?

完成了第一個 React Native 應用呢,恭喜你!你可以下載本教程的完整程式碼,親自來試試看。

如果已經接觸過 Web 開發了,你會發現使用 JavaScript 和 React 來定義與原生 UI 相連線的介面和導航是多麼地容易。而如果你曾經開發過原生 App,我相信在使用 React Native 的過程裡你會感受到它種種好處:快速的應用迭代,JavaScript 的引入以及清晰地使用 CSS 定義樣式。

也許下次做 App 的時候,你可以試試這個框架?或者說,你依然堅持使用 Swift 或者 Objective-C?無論之後你的選擇是怎麼樣的,我都希望讀完這篇文章的你有所收穫,還能把這些收穫融入到你的專案當中是最好的啦。


原文出處:Colin Eberhardt

譯文出處:前端外刊評論

連結:http://zhuanlan.zhihu.com/FrontendMagazine/19996445



贊(0)

分享創造快樂