我們已經看過 Titanium 和 PhoneGap 等框架,它們同樣能夠讓開發者使用網頁技術來建構行動 App 。這是一項優勢,因為開發者能夠將同一套技能同時運用於網頁及行動 App 的開發。不僅如此,同樣的基礎程式( Code Base )幾乎不必修改就能夠支援多個平台──亦即所謂的「只需撰寫一次,到處皆可執行」( Write Once, Run Everywhere )。但是一提到使用這些框架所建構的 App 的效能,它們的缺點就暴露出來了,所以儘管提供了一些很吸引人的功能,但是開發者仍然偏好建構原生的 App 。
React Native 與那些框架不同。 PhoneGap 之類的框架是將網頁內容包裝成 WebView ,形成 UI 元素,看起來其實跟原生的有些不一樣,而 React Native 則是使用 iOS 或 Android 原生支援的 JavaScript 元件,因此打造出來的 App 就跟原生的沒有兩樣。
如同 Facebook 的 Tom Occhino 在本文最後面的影音連結當中所說的, React Native 並非「只需撰寫一次,到處皆可執行」式的框架。透過本文的介紹,你將使用平台專屬的元件來打造 UI ,也因此無法拿同一份程式碼在 Android 上執行。 React Native 能夠提供給你的,是學好一套技能,然後運用這套技能來建構多種平台的 App ,就像 Occhino 進一步提到的, React Native 是一套「只需學習一次,到處皆可實作」( Learn Once, Write Anywhere )的框架。在本文中,我們將會介紹利用 React Native 框架來建構簡單 App 的完整開發流程。
開始使用
首先,我們必須在你的開發機器上安裝 React Native 。
在正式開始之前,我應該先提醒:你可以從 Github 取得 React Native 框架的程式碼。裡頭包含了一些能夠直接執行同時也能夠幫助學習的範例專案,比方說 2048 (知名的遊戲)、 Movies (影片瀏覽 App )、 SampleApp (空白的 React Native App )、 TicTacToe (知名的遊戲)和 UIExplorer (展示了所有你能夠用來建構 ListView 、 TabBar 、 MapView 和 Slider 等 App 功能的 React Native 元件)。這些範例有助於學習某些 UI 元素是如何透過 React Native 建構出來的,尤其是 UIExplorer App (展示了所有可能需要用到的 UI 元素)。但是其中有些 App 存在 Bug ,我曾經在嘗試做出某些動作時遭遇過幾次當機。不過它們仍然深具學習價值,更多關於 React Native 的資訊,可以參考這份文件。
現在來講如何安裝。 React Native 使用 Node.js 來建構 JavaScript 程式。假使你的電腦已經安裝過 Node.js 了,可以略過下列這些步驟,否則的話請繼續往下閱讀。
我們將使用 Homebrew 來安裝 Node.js 。這不是安裝 Node.js 的唯一方法,但是我覺得 Homebrew 是非常棒的套件管理工具。透過 Homebrew ,你將能夠非常簡單地安裝最新的特定版本套件、擁有某個套件的各種版本、選擇要使用的版本、更新和移除套件。要安裝 Homebrew ,請造訪其 官網並且依照網頁上的指示來進行安裝。下載的連結有可能會改變,所以在此就不貼出來了。
在安裝完 Homebrew 之後,接著在終端機視窗中貼上如下的命令,以便安裝 Node.js 。
brew install node
隨後安裝 watchman 。
brew install watchman
Watchman 是來自 Facebook 的檔案監控工具。 React Native 利用 Watchman 來偵測程式碼的變化,以便重新建構。
最後安裝 React Native CLI 工具,命令如下:
npm install -g react-native-cli
npm 是 Node Package Manager ( Node 套件管理員)。你可以將它想像成 RubyGems 之於 Ruby 、 CocoaPods 之於 iOS 以及 Gradle/Maven 之於 Java 等。基本上,它能夠讓你輕易地下載並管理專案所需的任何相依套件。
在終端機中,切換到你想要存放專案的資料夾底下,然後執行下列命令:
react-native init BookSearch
上述命令使用 CLI 工具來建立一個準備好可以建構與執行的 React Native 專案。在建立作業完成時,將會在終端機上看到一則訊息,指出可以在 Xcode 中開啟 BookSearch.xcodeproj ,並且如同往常般執行此 App 。如果你這麼做的話,模擬器將會啟動並且執行此 App 。此外還會再開啟另一個終端機視窗。當 React Native App 啟動時,將會從下列網址載入 JavaScript 應用程式。
http://localhost:8081/index.ios.bundle
被開啟的終端機將會啟動 React Packager 以及一台用來處理上述要求的伺服器。 React Packager 負責讀取和建構 JSX (稍後將會介紹)以及 JavaScript 程式碼。
在執行此 App 時,你應該會在模擬器中看到如下所示的畫面。假使你希望在實機上執行,那麼還需要再多進行幾個步驟。
歡迎畫面提供了一些非常重要的指示,請牢記在心:要編輯 App 的 UI ,應該編輯建立此專案時所產生的 index.ios.js 檔案,如果修改了這份 JavaScript 程式碼,那麼應該要透過 Command-R 來重新載入此 App ,以便看見所做的修改,如果想要進行更多的修改,則可以透過 Command-Control-Z 來開啟開發者功能表,裡面提供了諸如啟用即時重新載入( Live Reloading )與瀏覽器偵錯( Browser Debugging )等選項。
當你在閱讀本文時,一旦在模擬器上遭遇紅色畫面,請檢查模擬器上的錯誤訊息。它可以讓你知道到底是程式碼還是伺服器出了差錯。我遭遇過好幾次伺服器連線的問題,我在模擬器上得到的是「 Could not connect to the server 」(無法連接伺服器)錯誤訊息,接著檢查終端機,看到的是「 Process terminated 」(執行程序已終止)錯誤訊息。當遭遇此情況時,請關閉終端機視窗,停止 Xcode 中的 App ,然後再執行一次。至於其他因為程式碼的語法錯誤或是網路要求逾時(假使你的 App 會從網際網路擷取資料的話)而引起的錯誤,在修正問題之後只要重新載入應該就可以解決了。
如果按下鍵盤上的 Command-R 卻什麼事也沒發生的話,有可能是硬體鍵盤未與模擬器相連。連接方式是,從模擬器的功能表選取 Hardware > Keyboard > Connect Hardware Keyboard 。
假使做完了上述的動作卻仍未重新載入,那麼你可能需要重新啟動電腦。我曾經遭遇過一次,本來運作都很順利,突然之間就不會動了。後來我把電腦重新開機就沒事了。
現在讓我們開始來建構自己的 App 吧。開啟 index.ios.js
檔案。我建議使用適合用於網頁開發的 IDE 工具。你還是可以使用 Xcode ,但是你會發現不太合適,因為 Xcode 在程式碼格式化、自動完成或是語法錯誤標示方面的支援度都不高。適合 JavaScript 使用的 IDE ,可以閱讀這篇文章並且自行挑選。我使用的是 RubyMine ,不過任何支援 JavaScript 的 IDE 皆可使用。如果能夠安裝支援 JSX 的工具,那就更棒了。
開啟 index.ios.js
檔案之後,看到的程式碼將會建構出我們在執行中的 App 上面所看到的 UI 。你可以看到如下所示的程式碼區塊。
'use strict';
上述程式碼啟用了 Strict Mode (嚴格模式),將為 React Native 的 JavaScript 程式碼加入改良的錯誤處理能力。
var React = require('react-native');
上述程式碼載入了 react-native 模組,並將之指派給 React 變數。在呼叫模組中的函式之前,必須先在檔案中載入外部的模組。可以把這個動作想像成在 Swift 和 Objective-C 中匯入程式庫。
var {
AppRegistry,
StyleSheet,
Text,
View,
} = React;
上述程式碼稱為解構指派( Destructuring Assignment ),可以讓你為單一變數指派多個物件屬性。這使得那些物件屬性在整個檔案的範圍內都可以被參照到。上述的程式碼是選用的,但是如果不這麼做的話,每次你在程式碼當中想要使用某個元件時,就必須使用其完整名稱,例如「 React.AppRegistry 」而非「 AppRegistry 」,或是「 React.StyleSheet 」而非「 StyleSheet 」,諸如此類。
var BookSearch = React.createClass({
render: function() {
return (
Welcome to React Native!
To get started, edit index.ios.js
Press Cmd+R to reload,{'\n'}
Cmd+Control+Z for dev menu
);
}
});
上述程式碼建立了一個只擁有單一 render()
函式的類別。無論 render 當中定義了什麼,都將會輸出到螢幕上。上述程式碼使用了 JSX ( JavaScript Syntax eXtension , JavaScript 語法擴充)來建構 App 的 UI 。假使你曾經使用過 XML (或是 HTML ),那麼應該會覺得 JSX 看起來十分熟悉。它同樣需要起始與結尾標記,然後標記裡面也使用屬性來設定數值。在使用 React Native 時不一定要搭配 JSX ,你也可以單純只使用 JavaScript ,不過我之所以推薦 JSX ,是因為它簡化了樹狀結構的定義。假使你的 UI 擁有非常多的程式碼,那麼利用大型的 JSX 樹狀結構會比較容易閱讀。
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
上述程式碼是要套用於視圖內容的樣式。如果你曾經開發過網頁,也使用過 CSS ( Cascading Style Sheets ,串接樣式表)的話,那麼應該會覺得這看起來非常眼熟。 React Native 使用 CSS 來設定 App 的 UI 樣式。假使你深入研究那些 JSX 程式碼,將會發現每個樣式都有其用途。舉例而言, style={styles.container} 定義了 container (用來容納其他 UI 元件的外層視圖)的樣式。
AppRegistry.registerComponent('BookSearch', () => BookSearch);
上述程式碼定義了此 App 的進入點。也就是 JavaScript 開始執行的地方。
這是 React Native UI 的基本結構。我們所定義的每個視圖都會遵循相同的基本結構。
在本文中,我們所建立的 App 將可以讓你瀏覽圖書,並且檢視圖書明細,包括作者、書名和描述等。此外還能夠透過書名和作者來搜尋圖書。下列是此 App 的最終模樣。我們將使用 Google 圖書來充當資料。
加入分頁列
範例 App 包含了擁有 2 個項目的分頁列( Tab Bar )── Featured (注目新書)和 Search (搜尋)。我們將從新增第 1 個項目開始。
雖然可以把所有的程式碼都寫在 index.ios.js
檔案裡面,但是並不建議這麼做,因為隨著 App 程式碼的增長,檔案很快就會變得十分雜亂。我們會將類別建立成不同的檔案,以方便維護與管理。
在專案的根目錄中建立 2 個 JavaScript 檔案(與 index.ios.js 檔案位於相同位置)。分別命名為 Search.js 和 Featured.js 。開啟 Featured.js 並且加入下列的程式碼。
'use strict';
var React = require('react-native');
var {
StyleSheet,
View,
Text,
Component
} = React;
var styles = StyleSheet.create({
description: {
fontSize: 20,
backgroundColor: 'white'
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
class Featured extends Component {
render() {
return (
Featured Tab
);
}
}
module.exports = Featured;
上述程式碼你應該已經很熟悉了;跟前面曾經看過的非常類似。我們設定嚴格模式、載入 react-native 模組、建立視圖的樣式,並且利用 render()
函式來呈現 UI 輸出。程式碼的最後一行匯出了 Featured 類別,讓此類別也可以被其他檔案存取。請留意,我們宣告類別與函式的方式有別於 index.ios.js
中的範例。 JavaScript 擁有多種宣告類別與函式的方式。你可以依照自己的喜好來選擇。在本文剩下的篇幅當中,我們都將使用你剛才看到的這樣形式。
在樣式表定義中,我們看到了基本的 CSS 屬性。我們設定了文字的字型大小和背景顏色,並將外層視圖的內容置中。你或許會看不懂的是 flex: 1
這行。這是最近才新增到 CSS 規格中的伸縮方塊( Flexbox )。此處的 flex: 1
使得標記為 container 的元素只會佔用螢幕的剩餘空間(除了其他元素所佔用的空間之外),換句話說只會佔滿符合其內容大小的足夠空間。我們稍後會再更進一步介紹 flex 。若要學習更多有關 Flexbox 樣式的知識,可以參閱這份指南。
來到 Search.js 並且加入下列的程式碼。
'use strict';
var React = require('react-native');
var {
StyleSheet,
View,
Text,
Component
} = React;
var styles = StyleSheet.create({
description: {
fontSize: 20,
backgroundColor: 'white'
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});
class Search extends Component {
render() {
return (
Search Tab
);
}
}
module.exports = Search;
上述程式碼跟 Featured.js 長得很像,除了 Text 元件的文字之外。
刪除 index.ios.js 的全部內容,並且貼上下列的程式碼。
'use strict';
var React = require('react-native');
var Featured = require('./Featured');
var Search = require('./Search');
var {
AppRegistry,
TabBarIOS,
Component
} = React;
class BookSearch extends Component {
constructor(props) {
super(props);
this.state = {
selectedTab: 'featured'
};
}
render() {
return (
{
this.setState({
selectedTab: 'featured'
});
}}>
{
this.setState({
selectedTab: 'search'
});
}}>
);
}
}
AppRegistry.registerComponent('BookSearch', () => BookSearch);
在 index.ios.js 中,我們需要用到在先前建立模組的檔案中所匯出的那 2 個模組,並且指派給變數。在此類別中,我們指定了一個建構式,用來設定類別的狀態( state )。我們在此所使用的元件都具備了 state
變數。我們建立了名為 selectedTab
的屬性,並將其數值設定為「 featured 」。我們將使用「 featured 」來判斷應該使用哪個分頁。預設是 Featured 分頁。
在 render()
函式中,我們使用 TabBarIOS 元件來建立分頁列。別忘了將你要使用的元件加入到解構指派中,否則就得使用其全名了,例如 React.TabBarIOS 。
我們建立了 2 個分頁列項目。針對每個項目,我們都會設定其選取( selected )狀態,並且定義了在該項目被點擊時所要呼叫的函式。以 Featured 分頁為例,如果我們先前定義的 selectedTab
狀態的數值是「 featured 」的話,那麼 selected
便會被設定為 true ,否則便設定為 false 。針對 Search 分頁也是相同的設定邏輯,差別在於將會檢查 selectedTab
是否等於「 search 」。一旦 selected
被設定為 true ,該項目將會成為使用中的分頁。針對這 2 個分頁項目,我們使用的是系統的圖示。
請留意,我們在標記中使用了自訂的元件,用法就跟任何其他元件一樣。因為我們引入了對應的模組並且將之指派給變數,所以你可以在檔案的元件中直接使用該變數。結果如同元件類別的 render()
函式所示,它看起來就像檔案的一部分。附帶一提,我們使用了與個別類別相同的名稱,但是並沒有要求一定要這麼做,你也可以使用任何你喜歡的名稱。
當分頁列的項目被點擊時,定義於元件的 onPress
屬性的回呼函式將會被呼叫到。此函式將會設定 selectedTab
屬性的數值,此屬性最終將會用來決定使用中的是哪個分頁。
喚起模擬器,按下 Command-R 以便重新載入範例 App 。你應該會看到如下所示的畫面。
加入導覽列
接下來,我們將在範例 App 中加入導覽列( Navigation Bar )。在專案中加入 2 個新檔案。它們將會作為個別分頁在導覽堆疊( Navigation Stack )中的根視圖。分別將它們命名為 BookList.js 和 SearchBooks.js 。
在 BookList.js 中加入下列的程式碼。
'use strict';
var React = require('react-native');
var {
StyleSheet,
View,
Component
} = React;
var styles = StyleSheet.create({
});
class BookList extends Component {
render() {
return (
);
}
}
module.exports = BookList;
在 SearchBooks.js 中加入下列的程式碼。
'use strict';
var React = require('react-native');
var {
StyleSheet,
View,
Component
} = React;
var styles = StyleSheet.create({
});
class SearchBooks extends Component {
render() {
return (
);
}
}
module.exports = SearchBooks;
在這 2 個檔案中,我們建立了空白視圖的模組,並且匯出該模組。
修改 Featured.js 為如下所示。
'use strict';
var React = require('react-native');
var BookList = require('./BookList');
var {
StyleSheet,
NavigatorIOS,
Component
} = React;
var styles = StyleSheet.create({
container: {
flex: 1
}
});
class Featured extends Component {
render() {
return (
);
}
}
module.exports = Featured;
上述程式碼使用 NavigatorIOS 元件來建構導覽控制器。我們將其初始路徑( Initial Route )設定為 BookList 元件(這意味著 BookList 將成為根視圖),並且設定要在導覽列上顯示的標題。
接著修改 Search.js 為如下所示。
'use strict';
var React = require('react-native');
var SearchBooks = require('./SearchBooks');
var {
StyleSheet,
NavigatorIOS,
Component
} = React;
var styles = StyleSheet.create({
container: {
flex: 1
}
});
class Search extends Component {
render() {
return (
);
}
}
module.exports = Search;
就跟 Featured.js 一樣,上述程式碼建立了導覽控制器,設定其初始路徑以及標題。
重新載入此 App ,你應該會看到如下所示的畫面。
擷取並顯示資料
現在我們要開始在視圖上加入資料了。首先我們將建構填入假資料的視圖,稍後再透過 API 來使用真實的資料。
在 BookList.js 中,於檔案最開頭加入下列包含其他變數宣告在內的程式碼。
var FAKE_BOOK_DATA = [
{volumeInfo: {title: 'The Catcher in the Rye', authors: "J. D. Salinger", imageLinks: {thumbnail: 'http://books.google.com/books/content?id=PCDengEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api'}}}
];
修改解構指派為如下所示,以便引入更多我們將會使用到的元件。
var {
Image,
StyleSheet,
Text,
View,
Component,
} = React;
加入下列的樣式。
var styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
padding: 10
},
thumbnail: {
width: 53,
height: 81,
marginRight: 10
},
rightContainer: {
flex: 1
},
title: {
fontSize: 20,
marginBottom: 8
},
author: {
color: '#656565'
}
});
接著修改類別為如下所示。
class BookList extends Component {
render() {
var book = FAKE_BOOK_DATA[0];
return (
{book.volumeInfo.title}
{book.volumeInfo.authors}
);
}
}
重新載入此 App ,你應該會看到如下所示的畫面。
在上述程式碼中,我們建立了一個 JSON 物件,其格式非常類似於我們將從 API 呼叫中取得的物件。我們為此物件建立了屬性和數值,用來代表一本圖書。在此類別檔案中,我們使用假的資料只是為了取得第一個元素,並且用來填充我們的視圖。在此使用 Image 元件將圖片載入到視圖上面。請留意,我們是在樣式表中設定圖片的寬度和高度。假使沒有在樣式表中指定圖片的尺寸,那麼在視圖上將會看不到圖片。
針對 container
,我們設定了 flexDirection: 'row'
樣式。這麼做的話,此元素的子代也將繼承此樣式,亦即預設將會橫向排列而非縱向。請留意我們如何在元素裡面容納其他元素。在上述程式碼中,主容器裡面有 2 個子元素── Image 和 View 。 View 本身又擁有 2 個子元素──都是 Text 元件。
先放置 Image 元件然後才是 View 元件( rightContainer ),它們將會橫向緊鄰。針對 rightContainer ,我們設定其樣式為 flex: 1
。這麼做會讓該視圖佔據剩餘空間,而不會覆蓋到圖片。如果你想要目睹 flex 樣式的效果,可以為 rightContainer
加入下列的程式碼。
backgroundColor: 'red'
重新載入此 App ,將會看到空間已被 rightContainer
樣式的元件所佔滿。整個空間都被佔滿了,但是不會佔用到鄰近元素的空間。之所以沒有擴及整個螢幕,是因為外層容器設定了一些留白( Padding ),而圖片也設定了右側邊界( Margin )。
從 rightContainer
移除 flex: 1
並且重新載入此 App 。現在元件只會佔據放得下其內容的足夠空間。
假使將 thumbnail
和 rightContainer
的樣式設定為 flex: 2
,它們將會佔據同樣的寬度空間,比例為 2:2 (亦即 1:1 )。你可以指定任何你想要的數值,這些數值將會併入比例計算。
你可以嘗試幾種不同的比例,以決定自己喜歡的樣式。以本文為例,我們將繼續使用在為 rightContainer 加入紅色背景之前所設定的數值。
加入清單視圖
React Native 擁有名為 ListView (清單視圖)的元件,用來顯示可以捲動的一列一列資料──基本上以 iOS 的術語來講就是表格視圖。
開始動手吧,首先修改解構陳述式為如下所示,以便引入更多將會使用到的元件。
var {
Image,
StyleSheet,
Text,
View,
Component,
ListView,
TouchableHighlight
} = React;
接著加入下列的樣式表。
separator: {
height: 1,
backgroundColor: '#dddddd'
}
然後在 BookList 類別中加入下列的建構式。
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
})
};
}
最後加入下列的函式。
componentDidMount() {
var books = FAKE_BOOK_DATA;
this.setState({
dataSource: this.state.dataSource.cloneWithRows(books)
});
}
在建構式中,我們建立了 ListView.DataSource
物件,並將之指派給 dataSource
屬性。 DataSource 是一個介面,可以讓 ListView 用來決定在更新 UI 的過程中要變更哪幾列。我們帶入一個比較 2 列是否相同的函式,用來判斷資料內容的變化。
componentDidMount()
會在此元件載入到 UI 視圖時被呼叫到。當此函式被呼叫時,我們會將來自資料物件的資料設定給 datasource
屬性。
修改 render()
函式為如下所示。
render() {
return (
);
}
接著將下列函式加入到 BookList 類別中。
renderBook(book) {
return (
{book.volumeInfo.title}
{book.volumeInfo.authors}
);
}
上述程式碼在 render()
中建立了一個 ListView 元件。其 datasource
屬性被設定成我們先前所定義的 dataSource
屬性的數值,而 renderBook()
則被呼叫用來呈現 ListView 當中的資料列。
我們在 renderBook()
中使用了 TouchableHighlight 元件。這是一個包裹元件( Wrapper ),能夠讓視圖正確地回應點擊行為。在按下時,被包裹的視圖的不透明度會下降,使得底層顏色能夠透出來,讓視圖變得比較暗。透過這樣,假使你按下了 ListView 的某一列,將會看到被反白的顏色,就像我們在表格視圖中選取了某個儲存格時一樣。我們在資料列的底部加入了樣式為 separator (分隔線)的空白 View 元件。透過這樣的設定,此視圖將會呈現為單純的灰色橫線,方便區隔個別的資料列。
重新載入此 App ,你應該會看到只有一個儲存格的表格。
現在我們要將真實的資料載入到 App 中。
移除檔案中的 FAKE_BOOK_DATA
變數,取代為如下所示的程式碼。這是我們要用來載入資料的來源網址。
var REQUEST_URL = 'https://www.googleapis.com/books/v1/volumes?q=subject:fiction';
修改解構陳述式。
var {
Image,
StyleSheet,
Text,
View,
Component,
ListView,
TouchableHighlight,
ActivityIndicatorIOS
} = React;
加入下列的樣式。
listView: {
backgroundColor: '#F5FCFF'
},
loading: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}
修改建構式為如下所示。我們為元件的 state
物件新增了另一個屬性,用來判斷視圖是否正在載入中。
constructor(props) {
super(props);
this.state = {
isLoading: true,
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
})
};
}
修改 componentDidMount()
函式為如下所示,並且加入下列的 fetchData()
函式。 fetchData()
函式會呼叫 Google 圖書 API ,並且將從回應收到的資料設定給 dataSource
屬性。同時也將 isLoading 設定為 true 。
componentDidMount() {
this.fetchData();
}
fetchData() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
this.setState({
dataSource: this.state.dataSource.cloneWithRows(responseData.items),
isLoading: false
});
})
.done();
}
修改 render()
為如下所示,並加入下列的 renderLoadingView()
函式。我們加入了對於 isLoading
的檢查,如果其值為 true ,我們便傳回從 renderLoadingView()
返回的視圖。此視圖將會顯示一個活動指示器( Activity Indicator ),以及「 Loading books… 」字樣。當載入完成時,將會在表格中看到圖書清單。
render() {
if (this.state.isLoading) {
return this.renderLoadingView();
}
return (
);
}
renderLoadingView() {
return (
Loading books...
);
}
重新載入此 App ,你應該可以看到類似下圖的畫面。
加入明細視圖
假使點擊了表格中的儲存格,該儲存格將會被反白,但是什麼事情也沒有發生。我們將加入明細視圖( Detail View ),以便顯示選取的圖書的明細。
在專案中加入名為 BookDetail.js 的新檔案。貼上如下所示的程式碼。
'use strict';
var React = require('react-native');
var {
StyleSheet,
Text,
View,
Component,
Image
} = React;
var styles = StyleSheet.create({
container: {
marginTop: 75,
alignItems: 'center'
},
image: {
width: 107,
height: 165,
padding: 10
},
description: {
padding: 10,
fontSize: 15,
color: '#656565'
}
});
class BookDetail extends Component {
render() {
var book = this.props.book;
var imageURI = (typeof book.volumeInfo.imageLinks !== 'undefined') ? book.volumeInfo.imageLinks.thumbnail : '';
var description = (typeof book.volumeInfo.description !== 'undefined') ? book.volumeInfo.description : '';
return (
{description}
);
}
}
module.exports = BookDetail;
上述大部分的程式碼在前面都已經討論過了,在此就不再贅述。之前沒有看過的是 props
屬性,其用途是擷取資料。我們會透過設定此類別的 props
屬性來將資料傳遞進來。在上述程式碼中,我們取得資料之後,便用來填充視圖。
請留意,我們設定了 container
的上方邊界。不這麼做的話,視圖將會從螢幕最上方開始,可能導致某些元素被導覽列所遮蔽。
在 BookList.js 中加入下列的程式碼。
var BookDetail = require('./BookDetail');
修改 BookList 類別的 render() 函式中的 TouchableHighlight 為如下所示。
this.showBookDetail(book)} underlayColor='#dddddd'>
上述程式碼指定了會在資料列被點擊時觸發的回呼函式。將下列的函式複製到此類別中。其作用是將 BookDetail
視圖推入導覽堆疊中,並且設定導覽列的標題。此外還會將與特定資料列有關的圖書物件傳遞給 BookDetail
類別。
showBookDetail(book) {
this.props.navigator.push({
title: book.volumeInfo.title,
component: BookDetail,
passProps: {book}
});
}
重新載入此 App ,現在你應該能夠看見選取的圖書的明細了。
搜尋
我們已經完成 Featured 分頁的主從( Master-Detail )視圖了,現在輪到 Search 分頁,好讓使用者能夠利用 API 來查詢喜歡的圖書。
開啟 SearchBooks.js 並修改為如下所示。
'use strict';
var React = require('react-native');
var SearchResults = require('./SearchResults');
var {
StyleSheet,
View,
Text,
Component,
TextInput,
TouchableHighlight,
ActivityIndicatorIOS
} = React;
var styles = StyleSheet.create({
container: {
marginTop: 65,
padding: 10
},
searchInput: {
height: 36,
marginTop: 10,
marginBottom: 10,
fontSize: 18,
borderWidth: 1,
flex: 1,
borderRadius: 4,
padding: 5
},
button: {
height: 36,
backgroundColor: '#f39c12',
borderRadius: 8,
justifyContent: 'center',
marginTop: 15
},
buttonText: {
fontSize: 18,
color: 'white',
alignSelf: 'center'
},
instructions: {
fontSize: 18,
alignSelf: 'center',
marginBottom: 15
},
fieldLabel: {
fontSize: 15,
marginTop: 15
},
errorMessage: {
fontSize: 15,
alignSelf: 'center',
marginTop: 15,
color: 'red'
}
});
class SearchBooks extends Component {
constructor(props) {
super(props);
this.state = {
bookAuthor: '',
bookTitle: '',
isLoading: false,
errorMessage: ''
};
}
render() {
var spinner = this.state.isLoading ?
( ) :
( );
return (
Search by book title and/or author
Book Title:
Author:
Search
{spinner}
{this.state.errorMessage}
);
}
bookTitleInput(event) {
this.setState({ bookTitle: event.nativeEvent.text });
}
bookAuthorInput(event) {
this.setState({ bookAuthor: event.nativeEvent.text });
}
searchBooks() {
this.fetchData();
}
fetchData() {
this.setState({ isLoading: true });
var baseURL = 'https://www.googleapis.com/books/v1/volumes?q=';
if (this.state.bookAuthor !== '') {
baseURL += encodeURIComponent('inauthor:' + this.state.bookAuthor);
}
if (this.state.bookTitle !== '') {
baseURL += (this.state.bookAuthor === '') ? encodeURIComponent('intitle:' + this.state.bookTitle) : encodeURIComponent('+intitle:' + this.state.bookTitle);
}
console.log('URL: >>> ' + baseURL);
fetch(baseURL)
.then((response) => response.json())
.then((responseData) => {
this.setState({ isLoading: false});
if (responseData.items) {
this.props.navigator.push({
title: 'Search Results',
component: SearchResults,
passProps: {books: responseData.items}
});
} else {
this.setState({ errorMessage: 'No results found'});
}
})
.catch(error =>
this.setState({
isLoading: false,
errorMessage: error
}))
.done();
}
}
module.exports = SearchBooks;
在上述程式碼中,我們在建構式中設定了一些屬性: bookAuthor
、 bookTitle
、 isLoading
和 errorMessage
。我們隨後很快就會看到這些屬性的功用。
在 render()
函式中,我們檢查 isLoading
是否為 true
,如果是的話便建立活動指示器,否則便建立空白的視圖。稍後將會使用到此視圖。我們接著建立用來插入查詢條件的搜尋表單。用來輸入的元件是 TextInput
。我們為每個 TextInput
元件都指定了回呼函式,並在元件的數值發生變化時(亦即使用者輸入了一些文字)被觸發。當被呼叫到時, bookTitleInput()
和 bookAuthorInput()
回呼函式會將 bookAuthor
和 bookTitle
狀態屬性設定為使用者所輸入的資料。當使用者按下 Search 按鈕時,則會呼叫 searchBooks()
。請留意, React Native 並沒有按鈕元件。相反地,我們使用 TouchableHighlight
,並且以 Text
加以包裹,最後再用樣式使它看起來像顆按鈕。在點擊 Search 按鈕時,將會根據所輸入的資料來形成一串網址。使用者可以透過書名、作者或者書名加作者來進行搜尋。一旦有結果傳回, SearchResults 便會被推入導覽堆疊,否則便顯示錯誤訊息。此外我們也會將回應資料傳遞給 SearchResults 類別。
建立名為 SearchResults.js 的新檔案,並且貼上如下所示的程式碼。
'use strict';
var React = require('react-native');
var BookDetail = require('./BookDetail');
var {
StyleSheet,
View,
Text,
Component,
TouchableHighlight,
Image,
ListView
} = React;
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
title: {
fontSize: 20,
marginBottom: 8
},
author: {
color: '#656565'
},
separator: {
height: 1,
backgroundColor: '#dddddd'
},
listView: {
backgroundColor: '#F5FCFF'
},
cellContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
padding: 10
},
thumbnail: {
width: 53,
height: 81,
marginRight: 10
},
rightContainer: {
flex: 1
}
});
class SearchResults extends Component {
constructor(props) {
super(props);
var dataSource = new ListView.DataSource(
{rowHasChanged: (row1, row2) => row1 !== row2});
this.state = {
dataSource: dataSource.cloneWithRows(this.props.books)
};
}
render() {
return (
);
}
renderBook(book) {
var imageURI = (typeof book.volumeInfo.imageLinks !== 'undefined') ? book.volumeInfo.imageLinks.thumbnail : '';
return (
this.showBookDetail(book)}
underlayColor='#dddddd'>
{book.volumeInfo.title}
{book.volumeInfo.authors}
);
}
showBookDetail(book) {
this.props.navigator.push({
title: book.volumeInfo.title,
component: BookDetail,
passProps: {book}
});
}
}
module.exports = SearchResults;
上述大部分的程式碼我們都曾經使用過了,所以在此便不再贅述。上述程式碼透過 props
屬性來取得傳入此類別的資料,並且利用這些資料來填充所建立的 ListView
。
我在 API 中注意到一件事,當你透過作者來搜尋時,會得到一些只與作者本身有關,但是與圖書資料無關的結果。這意味著某幾列的 book.volumeInfo.imageLinks.thumbnail
和 book.volumeInfo.description
可能擁有未定義的數值。所以我們對此進行了查核,如果沒有圖片的話,就顯示空白的 Image 視圖。如果不這麼做的話,我們的 App 將會嘗試載入並不存在的圖片,導致當機。
我們使用先前所建立的相同 BookDetail
元件來顯示每本圖書的明細。在嘗試載入 BookDetail
視圖時,我們同樣應該進行上述缺漏資料的查核。開啟 BookDetail.js ,並修改其 render()
函式為如下所示。這段程式碼在填充視圖之前,將會檢查傳入的資料是否包含了圖片和描述。假使我們嘗試檢視沒有描述或圖片的圖書,相對區域將會是空白的。你或許會想要向使用者顯示錯誤訊息,不過我們在此省略此步驟。
render() {
var book = this.props.book;
var imageURI = (typeof book.volumeInfo.imageLinks !== 'undefined') ? book.volumeInfo.imageLinks.thumbnail : '';
var description = (typeof book.volumeInfo.description !== 'undefined') ? book.volumeInfo.description : '';
return (
{description}
);
}
重新載入此 App ,現在你應該能夠搜尋圖書了。
結語 Conclusion
儘管仍在發展中,但是 React Native 看起來很有希望能夠成為建構行動 App 的另一個選擇。它為網頁開發者開啟了一扇門,讓他們可以試試行動開發的水溫;即便是行動開發者,也能夠透過 React Native 來簡化其開發工作流程。
隨著 React Native 專案的成長,將可以更有趣地看出其走向以及能夠建構出什麼 App (支援 iOS 和 Android ──或許也還會涵蓋其他平台)。除此之外,假使你需要確認網頁技術是否能夠達到真正的原生體驗,那麼可以試試看這些以 React Native 所建構的 App : Facebook 廣告管理員(完全以 React Native 撰寫)以及 Facebook 社團(以 React Native 加 Objective-C 混合撰寫)。
「只需學習一次,到處皆可實作。」光憑這句話就值得好好學習如何使用 React Native 框架了。
若要學習更多有關 React Native 的知識,可以觀看下列的影片,以及閱讀這份文件。
供你學習參考,請從這裡下載 Xcode 專案。
你對於本文以及 React Native 有什麼心得呢?歡迎留言分享你的想法。
原文:Introduction to React Native: Building iOS Apps with JavaScript