React Native簡直太火了,國內大公司都在爭先恐后的嘗鮮,讓人難以相信這是誕生剛剛一年的開源項目。正因為它的年輕,在使用它進行開發時難免會遇到這樣那樣的坑,因此,我們邀請了《React Native入門與實戰》的作者之一,魅族高級研發經理魏曉軍來為我們解析RN開發中的痛點。本文分享的是在環境搭建和擴展中會遇到的問題與解決方案。
React Native的出現,為APP開發者們帶來了沖動和激情,令Native開發者和Web開發者都為之癡迷。瞬間各類技術論壇、技術社區甚至出版社都爭先報道其相關內容。然而對于一般的初學者來說,最簡單要求莫過于按照官方提供的向導來完成基于React Native的處女之作。就是這么簡單的一個要求,卻把無數開發者拒之門外。其中原因在于初學者在按照教程搭建的過程中,總會出現這樣那樣的問題,給開發者們的銳氣重重一擊。下文將以其中一些比較突出的問題為起點,從開發前和開發中兩方面來加以分析并給出相應的解決方案,希望能夠給廣大開發者們提供些許實戰經驗,少走一些不必要的彎路。
按照官方的向導,在Homebrew 、Watchman、Flow、nvm、node等運行環境安裝完后,要做的第一件事情便是安裝React Native命令行工具并由其初始化React Native項目。代碼如下:
$ npm install -g react-native-cli$ react-native init AwesomePRoject
然而就是這么兩句簡短的代碼,卻運行的也不是那么的順利。經常會聽到開發者們抱怨安裝個命令行工具也要翻墻。
對于初學者們,最捷徑的辦法,莫過于參考別人的項目。經常會遇到這么一個場景, 項目中要用到一個slider組件,聰明的開者們很快想到了github,在其上一搜發現還真不少,于是乎興高采烈的將其整個項目clone下來,找到其中的slider組件,納入自己的項目中,簡單的添加了幾句引入代碼,便急沖沖的執行CMD+R,瞬間模擬器內一片紅屏,分析原因,最后發現是fontsize不支持數字了一定要以字符串的方式設置,這是多么沉痛的打擊。
玩了一段時間的React Native開發者們應該也會發現,隨著開發出來的React Native項目的增多,電腦的存儲空間會越來越小了,翻開React Native的歷代版本可以看到,gzip之后70M左右,解壓后在350M左右。這樣的體積大小,幾個項目下來,幾個G的空間瞬間沒了。
那么歸納起來主要是以下幾類問題制約著開發者們。
React Native 命令行環境搭建困難
React Native 初始化項目困難
React Native 版本升級經常帶來API不支持
隨著React Native 項目的增多,占用空間越大
原因分析
React Native 命令行環境搭建困難,主要是慢,為什么呢,從代碼
npm install -g react-native-cli
中可以了解到,這是在從npm服務器上拉取react-native-cli。所以慢的原因便是因為npm服務器不在國內。聰明的國人已給出了解決辦法,通過翻墻來解決此問題。更高興的是npm提供了一個register的屬性,可以讓開發者自由的設置鏡像地址。開發者們最常用的便是淘寶的鏡像地址。據統計國內比較常用的鏡像地址有:
http://r.cnpmjs.org/http://registry.npm.taobao.org/http://registry.npmjs.eu/http://registry.npmjs.org.au/http://npm.strongloop.com/https://registry.nodejitsu.com/http://registry.npmjs.pt/
這么多眼花繚亂的地址,的要感謝國人開源意識的強大,是他們給開發者們帶來了福音,讓開發者們再也不用擔心下載不到nodejs包了。
這是官網初始化React Native項目的代碼:
react-native init AwesomeProject
可以看到這是通過"react-native init"這個命令來進行初始化的。那“react-native”這個命令又是從哪里來的呢,起初給筆者帶來很大的困惑,上面只安裝了“react-native-cli”這個node包,怎么會冒出個“react-native”命令而不是“react-native-cli”命令呢。我們翻開“react-native-cli”的安裝目錄,mac上是在:
/usr/local/lib/node_modules/react-native-cli
目錄,打開其中的“package.json”文件,可以看到有這么一段代碼:
"bin": { "react-native": "index.js"},
這里簡單介紹下bin屬性,"bin"是由多個“{ 命令名:文件名 }”組成的一個map。在安裝的時候會將每個“命令名”鏈接到prefix/bin(全局初始化)或者./node_modules/.bin/(本地初始化)。上面代碼在安裝的時候,會將index.js鏈接到/usr/local/bin/react-native。這樣使用"react-native init"進行初始化的困惑也就可以理解啦。那么這個“react-native init”究竟做了什么呢。繼續跟蹤,打開index.js,其中的部分代碼片段:
if (args[0] === 'init') { if (args[1]) { init(args[1]); } else { } } else { ...... }可以看出,index.js其實只對“init”方法做了處理,具體到“init”方法中又做了些什么呢,截取了部分主要代碼如下:
1、fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson));2、run('npm install --save react-native', function(e) {
其中代碼1是動態生成package.json,代碼2是在本地安裝react-native模塊。到此,之前提到的“React Native 初始化項目困難”也就不難理解啦,原因都是npm惹的禍。
既然已經看到這里啦,那就順帶完整的介紹下react-native這個命令。除了“react-native init”命令外,官網還提供了 “react-native bundle”、“react-native run-android”等命令,而index.js文中卻只有“init”命令。那其他命令又是怎么來的呢。帶著疑問,筆者又找到了如下的代碼:
var CLI_MODULE_PATH = function() { return path.resolve( process.cwd(), 'node_modules', 'react-native', 'cli' );};var cli;try { cli = require(CLI_MODULE_PATH());} catch(e) {}if (cli) { cli.run();} else {......}打開“CLI_MODULE_PATH”所指的cli文件,其指向了“module.exports = require('./local-cli/cli.js');”,繼續打開“/local-cli/cli.js”部分代碼如下:
看到這里,也就眼前豁然開朗起來,原來除“init”命令外,其它命令都在這里啦,這讓筆者明白了一件事情:也就是說“init”可以在任何地方使用,而其他命令只能在React Native項目的根目錄下使用 ,不得不佩服Facebook設計之巧妙。 與此同時,另一件重要的事情也就顯得清晰可見了。那就是在“React Native”的開發中,其實有兩個“react-native”,一個是的“react-native-cli”生成的全局的react-native命令,一個是在初始化項目時安裝的“react-native”模塊,即在應用開發中通過“require('react-native')”所引用的模塊。這兩個簡單區別便是,一個是全局的模塊,一個是局部的模塊。全局模塊中只提供了一個“init”方法,而局部模塊提供了除“init”方法外的所有命令以及React Native開發中用到的所有功能。
經過上面對"react-native-cli"與“react-native”的分析,可以看出Facebook應該是推薦“react-native”模塊局部化,所以不論在React Native項目初始化的過程中,還是clone已有的React Native項目,都需要在當前項目下下載和安裝“react-native”模塊,使得React Native 項目占用的空間越來越大。
解決辦法
1、私有NPM搭建
雖然上面講的國內鏡像,已使NPM的下載速度很快啦,但是仍然不如在自己內網架設個NPM私有服務器,給團隊成員提供更快捷的下載速度。架設私服,除了速度快以外,還有一個重要的原因就是一些內部的隱私模塊也可以發在私服上供內部成員使用。 市面上的NPM私服也有很多,這里推薦的是一個叫“sinopia”的NPM私服。“sinopia”的做法是優先從自己的倉庫中拉取模塊,如果發現沒有,便從遠端的NPM服務器拉取。也許有的開發者早已注意到,這個私服其實在“react-native-cli”的NPM庫中react-native-cli就有介紹,筆者猜想應該是Facebook也推薦諸位使用“sinopia”來搭建NPM私服吧。“sinopia”的github地址為:https://github.com/rlidwka/sinopia。“sinopia”的搭建比較簡單,步驟如下:
-安裝命令:
$ npm install -g sinopia
-啟動命令
$ sinopia
啟動后的日志:
warn --- config file - /Users/youname/.config/sinopia/config.yamlwarn --- http address - http://localhost:4873/
日志中的“config file”為“sinopia”的配置地址,“http address”為“sinopia”的主頁地址。打開“/Users/youname/.config/sinopia/config.yaml”可以看到默認的配置信息如下:
如果想看更完整的配置可以參考這里https://github.com/rlidwka/sinopia/blob/master/conf/full.yaml。
-創建新用戶
$ npm adduser --registry http://localhost:4873
按照命令行中的提示,依次輸入Username、Passworld、Email即可完成用戶的創建。
-設置npm鏡像地址
$ npm set registry http://localhost:4873/
-發布npm包
在發布模塊前,需要先登錄
$ npm adduserUsername:xxxPassworld:xxxEmail:[email protected]
登錄完成后,便可進入待發布模塊的根目錄進行發布了。
$ npm publish
若沒有“package.json”文件的話,需先執行“npm init”進行創建,然后執行上面的命令即可將模塊推送到自己的NPM服務器上了。這樣我們在安裝該模塊的時候,便是從自己的NPM服務器上下載了。
-遠端訪問
上面只是在本地搭起了NPM服務器,只能通過本地來訪問,如果要做到遠端訪問的話,需要這樣來啟動“sinopia”:
$ sinopia -l IP地址:端口
-配置React Native的sinopia服務器
對于react-native的配置,官網建議修改packages和max_body_size的配置如下:
......packages: 'react-native': allow_access: $all allow_publish: $all 'react-native-cli': allow_access: $all allow_publish: $all '*': allow_access: $all proxy: npmjsmax_body_size: '50mb'從修改中可以看到,主要是對模塊發布做了限制,只允許發布‘react-native'和'react-native-cli'模塊,其他模塊一概不允許發布,猜想應該是怕將其他模塊覆蓋掉吧。對max_body_size的設置,主要是出于對模塊大小的考慮,避免產生"request entity too large"的錯誤,因為默認的大小為1mb。
服務器配好后,接下來就需要將react-native模塊和react-native-cli模塊發布上去了。為了方便起見,我們建立如下的目錄結構:
react_native_modules react-native v0.21.0 node_modules react-native v0.20.0 node_modules react-native react-native-cli v1.0.0 node_modules react-native-cli其中react_native_modules為我們在用戶目錄下創建文件夾。之所以設計成這樣的結構是為了我們方便現在NPM服務器上的模塊。如我們要下載0.19.0版本的react-native模塊,我們只需要創建react_native_modules/react-native/v0.19.0文件夾,然后在該文件夾中執行
$ npm install [email protected]
即可完成從NPM服務器上對該模塊的下載。接下來進入到node_modules/react-native目錄,執行
$ npm set registry http://host:port/ //要記得切換到sinopia服務器哦,否則會將模塊發在NPM服務器上而不是sinopia服務器上$ npm publish
實現多版本管理
如果說sinopia是用來解決速度的問題,那么多版本的管理可以說是用來解決體積的問題。做過node.js開發的同學,都清楚nvm,它是nodejs的版本管理工具,甚至包括React Native的官網也有談到使用nvm來安裝node.js。在react-native版本迭代如此頻繁的階段,居然沒有react-native的版本管理工具,這讓開發人員們很是受傷。所以,這里將嘗試著設計一個react-native的版本管理工具,我們可以親切的叫它rnvm(react-native version manager)。在了解rnvm的思路前,先了解下rnvm的使用場景.
-rnvm的使用場景
rnvm如其名字中的那樣,主要是對react-native的版本進行管理的。那么它的使用場景都有哪些呢。這的從一個React Native項目的的獲得方式說起。通常情況下有如下幾種方式:
a、通過react-native命令初始化項目獲得
b、通過從github上clone獲得
c、通過拷貝獲得
對于a中的使用場景,在react-native初始化項目的時候,正常情況下rnvm是插不上手的。如果真要用rnvm,需要侵入/usr/local/lib/node_modules/react-native-cli/index.js文件,將run('npm install --save react-native'改為run('rnvm use ',或者也可以給rnvm添加一個init命令來取代react-native init命令,使用方式為rnvm init AwesomeProject。
對于b、c場景,可以直接使用rnvm命令進行處理。
然而,這并不是rvnm的優勢。rnvm的核心思想是將react-native模塊安裝在全局目錄下,這樣每個React Native項目在使用的時候,不需要在本地目錄中安裝一份,只需要調用全局目錄中的react-native即可,給開發者節省了不少的空間。再者rnvm給React Native項目中的對react-native版本的使用帶來了靈活性,所以rnvm更適合多React Native項目的開發。
-rnvm的目錄結構
prefix_node_modules node_modules react-native react-native-cli react_native_modules react-native v0.21.0 node_modules react-native v0.20.0 node_modules react-native react-native-cli v1.0.0 node_modules react-native-cli還是在用戶目錄下創建react_native_modules、prefix_node_modules兩個目錄結構。react_native_modules的目錄和上面sinopia發布模塊用的是一樣的結構,都是用來存放模塊的。默認的全局安裝目錄在“/usr/local/lib/node_modules”,這里的prefix_node_modules目錄就是用替換原有的全局安裝目錄,這樣做的好處是不需要每次裝全局模塊時都要sudo。
-rnvm的執行流程
這里需要結合一個場景來分析rnvm的執行流程,某天開發人員從github clone了一份別人寫的React Native的代碼,重命名為mycloneproject,想在本地運行起來,正常情況下應該是進入mycloneproject項目的根目錄,然后執行npm install。這樣就會將package.json中指定的所有依賴模塊都安裝在當前目錄的node_modules目錄中。那么使用rnvm是怎么安裝的呢。
現在假設rnvm只有一個use命令,格式為 rnvm use version。
具體的執行代碼如下:
$ cd mycloneproject$ npm config set prefix ~/prefix_node_modules/node_modules$ npm set registry http://host:port/$ rnvm use 0.20.0
在這個過程中發生了些什么呢?
a、先進入mycloneproject項目的根目錄。
b、設置全局模塊安裝目錄為~/prefix_node_modules/node_modules。
c、設置npm的鏡像指向自己的sinopia服務器,這樣之后的所有npm命令就會從sinopia服務器獲取模塊了。
d、執行rnvm use命令。代碼中看到可以使用全局“rnvm”命令,這就要求rnvm是一個node.js的模塊,且該模塊實現了package.jon中的bin配置,使其支持全局安裝。
e、rnvm接收到兩個參數之后的行為:
在拿到參數后,rvnm會去react_native_modules/react-native中查找是否有v0.20.0目錄,如果有則進入該目錄,并執行npm link
如果沒有,則創建v0.20.0目錄,并進入該目錄執行npm install [email protected]。
執行完后,進入node_modules/react-native中,執行npm link
接著在回到mycloneproject項目根目錄,執行npm link react-native,然后在執行npm install。這樣mycloneproject項目的依賴模塊就都安裝完畢,且使用了0.20.0版本的react-native,
這便是一個rnvm的基本執行流程。當然,這里也可能會有些特殊情況,如只使用rnvm use不傳版本信息,這樣rnvm就需要先分析package.json中的react-native的版本信息,并結合npm info react-native獲取來的版本信息進行處理,得出最終需要的版本信息,然后在執行rnvm use 最終的版本信息。
這只是個use命令的執行分析。也可以像nvm一樣,實現rnvm install、rnvm ls、rnvm current等命令。
rnmv的github地址:https://github.com/GammaGos/rnvm/
3、完整架構
圖1 基于rnvm的開發架構圖
基于上面的圖形,這里做簡短的描述。總體分為兩個大的部分,一個是server端,一個是client端。server端是指sinopia服務所在的端,主要負責提供NPM私有服務。在搭建該端的時候,需將常用的react-native版本和react-native-cli版本都推送到該服務器上,便于之后客戶端的使用。client端是指用戶端也就是開發者端。該端負責React Native項目的構建。該端屬于消費端是主戰場。 在上圖中,該端主要發生的邏輯為:
開發者先構建了一個React Native ProjectA項目然后使用rnvm來安裝依賴模塊,rnmv接著在指定的目錄下判斷是否有對應的模塊,有的話會先找到對應的模塊,然后再去模塊根目錄下做npm link的操作,然后回到React Native ProjectA項目的根目錄,執行npm link react-native操作,接著執行npm instal的操作。如果沒有找到對應模塊的話,會向sinopia服務器發送請求,請求下載需要的模塊,并放入指定的目錄中,待模塊下載完畢后, 執行4中的操作。如果sinopia服務器也沒有的話,會像npm服務器發起請求待模塊下載完后,執行4中的操作。然后,在將改模塊publish到sinopia服務器上。開發中
面臨的問題
通常項目中,App需要開發Android和iOS兩個版本,經常會用到一些圖片,并需要將這些圖片打入App中。當開發iOS版本時,需要手動加載這些圖片資源到xcode中。當開發Android版本時又需要手動的加載一次。這樣,當某天某個圖片需要更新時,就需要對Android和iOS都進行修改。如果要是能夠讓兩個版本引用同一個圖片,那么就會使開發變得簡便。
解決辦法
起初的想法
我們可以借助shell腳本創建、搬運、解析文件的能力,加上一些自定義的規則,來實現Andorid、iOS兩個版本引用同一個圖片的功能。
下面來簡短的介紹下實現思想。
iOS版本在Images.xcassets文件夾中創建符合規則的圖片文件以及文件夾。Android版本在drawable-hdpi,mdpi等文件夾中創建符合規則的圖片。那么這個規則是什么呢,我們可以通過使用json形式的congfig文件來定義這個規則,格式如下:
{ "resources":[ { //資源的別名 "name":"rose", //資源的類型 "type":"image", //資源路徑,可以相對也可以絕對 "url":"resources/image/rose.png", ...... }, { "name":"flower", "type":"image", "url":"http://host/path/imagename.jpg" ...... } ]}然后再借助Shell的jq插件,通過解析剛才定義的congfig文件來獲得約定的規則,獲得規則的主要shell代碼如下:
......index=0;flag=0;while ((flag<=0));doread imgname <<< $(cat ./../../resources/image/resource.json |./jq '.[]' |./jq '.['$index']'|./jq '.name')read url <<< $(cat ./../../resources/image/resource.json |./jq '.[]' |./jq '.['$index']'|./jq '.url') done /**省略創建圖片的代碼**/
待獲得規則后,就可以根據規則生成各個版本對應的圖片。到此,shell腳本的主體邏輯已經介紹完成,是時候把它融合到兩個版本的實際項目中運行了。
在Android版本中,我們可以通過開發一個Unix executable文件,來封裝自己的run-android運行命令,代碼如下:
/**省略前面解析json與創建圖片的代碼**/cd ../<android項目路徑>/react-native run-android
待啟動Android項目后,圖片順利的讀取到了。
在iOS版本中,我們可以通過開發一個shell腳本,并把它添加到Xcode項目的run script phase中,待啟動iOS項目后,卻發現資源文件根本讀不到。這是為什么呢?
原因分析
-iOS中React Native項目啟動順序:
在啟動React Native Xcode項目時,會先加載項目所依賴的React項目,接著運行React項目中事先定義好的run script phase,最后運行packger.sh。
其中packger.sh中我們看到如下的代碼:
node "$THIS_DIR/../local-cli/cli.js" start "$@"接著我們找到了cli.js,看到里面調用了好多模塊。其中default.config.js模塊指定了JS和資源的加載路徑,server.js模塊除了指定server監聽的默認端口外還有檢測node版本等功能,runServer.js模塊用來啟動server。
待server啟動成功后,才運行到iOS native code。也就是這個時候,才會運行Xcode項目中,事先定義好的run script phase中指定的shell腳本,而在這個時候,在shell腳本中創建資源路徑是沒有用的。所以就會出現了上面資源文件讀不到的情況。
-Android中React Native項目啟動順序:
首先執行上面封裝好的Unix executable文件,該文件中會調用資源文件生成的代碼,將資源文件生成。
然后在該文件中會繼續再執行react-native run-android命令,此時根據react-native-cli模塊的package.json中bin的定義,調用node.js執行$prefix/react-native-cli/index.js。
在index.js中會先加載cli.js模塊然后運行其run方法。在cli.js模塊中做的工作和上面分析的iOS中的cli.js做的工作是一樣的。
待server啟動成功后,才會運行到Android native code,所以運行封裝好的Unix executable是不會導致資源失效的,因為資源生成代碼已經在react-native run-android命令運行之前被執行過了。
最終的解決辦法
為了讓資源生成的代碼執行順序提前,可以先增加一個名為AppPrepare的Command類型項目,來運行此Shell。然后在Xcode項目Target Dependencies中添加AppPrepare項目,這樣就會先運行AppPrepare的項目后才會運行Xcode項目,從而達到了我們的目的。
轉載:http://www.infoq.com/cn/articles/react-native-solution-dev-environment
新聞熱點
疑難解答