React Native 原生混合路由解決方案

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 RN 出來前許多公司都已經有一套完整的 App,可能業務複雜、依賴繁多,在這種情況下,將原有的 App 推翻重寫明顯是不切實際的,成本和風險都較高。所以如何進行混合性開發則至關重要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 RN 官方建議的路由框架爲 react-navigation,這個框架大部分邏輯是 javascript 編寫的,RN 頁面的棧管理由 JS 端控制,所以當原生和 RN 頁面混合開發時,由於原生棧和 RN 棧不一致,導致棧管理較混亂。出現 RN - 原生 - 原生 - RN 跳轉場景時,若爲單一的 RN 容器管理,則需要在返回時判斷上一個頁面爲 RN 還是原生,嵌入基類修改;若爲多 RN 容器管理,則也需要在跳轉時區分是跳轉原生還是 RN。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外網上還比較廣泛使用是 react-native-navigation 框架,它使用 iOS 和 Android 原生的基礎 Api。但是混合開發的問題還是和 react-navigation 一致,JS 自己管理棧和原生棧不統一,導致棧管理複雜,跳轉需要做額外的處理。所以通過對市面上的一些路由分析,我們通過對 react-native-navigation 框架的改造,拋棄了 JS 端的棧管理,將一個 RN 頁面對應一個容器,使得 RN 的棧管理融入原生的棧管理中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"WYNavigation"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"主要概念"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們採用單引擎多容器的模式。前期我們已經對現有的 RN 代碼進行了拆包,分爲一個 common 包和 n 個 business 包。並且原生工程中每個頁面都是以 RouteUrl(類似瀏覽器地址)的方式進行跳轉的。這兩點爲我們的方案提供了技術支持。共用一個 common 包使得我們的代碼共享一個 RN 引擎,RouteUrl 的模式也使得我們在跳轉頁面時不需要去關心是跳轉原生還是 RN 頁面。下面是打開一個 RN 頁面的流程:和 react-native-navigation 一樣,我們會將所有的 RN 頁面在 Navigation 進行註冊,Test 爲這個 RN 頁面的對外名稱。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Navigation.registerComponent(\"Test\", () => TestScreen)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在原生主工程啓動時會先預加載 common 包,當需要打開一個 RN 頁面時,原生會先打開一個 RN 容器,根據傳遞過來的 PageName 找到對應的 business 包加載,再將 PageName 傳到 RN 端,RN 端根據 PageName 找到對應的 RN 頁面顯示,容器根據找到的 RN 頁面中的靜態變量初始化原生導航欄,這樣因爲該 RN 容器等於一個原生頁面,這個容器中只有一個 RN 頁面,所以一個 RN 頁面等於一個原生容器,從而將該 RN 頁面嵌入到了原生導航棧中。這樣就實現了 RN 和原生導航棧的統一。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/00\/00a7b66cae62acc83d7f237a73193a84.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"組件、頁面、容器的理解"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們要說 3 個概念:組件 (Component):RN 上具有獨立功能的單位,一個頁面由多個組件組成。頁面 (Screen):UI 意義上的頁面,包括導航欄及內容顯示。容器 (RNContainer):原生用來容納 RN 的容器,用於顯示 RN 頁面。我們的方案是多個組件 = 一個頁面 = 一個容器 = 一個原生頁面,所有 RN 頁面上的 push 和 pop 實際上最後都會經過容器的原生跳轉,這樣處理就相當於 RN 頁面進行跳轉和返回時和原生並沒有什麼區別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/81\/81112c12c47a5dcb2a953d361f39f054.webp","alt":"Image","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"路由的跳轉"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合我們現有的 App 路由模式,我們統一 iOS 和 Android 兩端容器的協議,將 RN 頁面名稱通過參數的形式進行傳遞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"原生跳轉到 RN"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從原生頁面跳轉到 RN 頁面時,需要指定一個入參 pageName,即要跳轉的 RN 頁面的名稱。如果需要傳參,定義一個字典,鍵爲 param,值是傳遞的參數。如下,MyPost 是一個 RN 頁面的名字。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ 原生跳轉 RN\nNSDictionary *params = @{@\"pageName\": @\"MyPost\", @\"param\": param};\n[WYMediator routeURL:WYURL(@\"xxx\/rncontainer\") withParams:par];\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ 原生跳轉\nExtParams param = new ExtParams();\nparam.putStringExtra(\"PageName\", \"MyPost\");\nparam.putStringExtra(\"PageParam\", pageParams);\nRouter.startRoute(activity, \"xxx\/rncontainer\", param);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"RN 跳轉到原生"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從 RN 頁面跳轉到原生頁面,也遵循我們的路由協議。這個過程是原生實現的,它會調用原生的路由組件,從 RN 容器跳轉到指定的原生頁面。如下,name 指的是原生頁面對應的協議,passProps 則是需要跳轉到的原生頁面需要的參數,沒有則可不寫。最後一個參數是回調函數,當執行 push 方法時,路由會根據傳入的 componentId 將回調函數保存在 JS 端,原生頁面關閉後回到當前 RN 頁面時,路由會根據當前頁面的 component 取出回調函數並執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Navigation.push(this.props.componentId, {\n    name:\"xxxx\",\n    passProps:{patientId:'11111', patientName:'張三'}\n}, (componentId, params) => {\n    \/\/ 返回當前頁面的回調函數\n});\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"RN 跳轉到 RN"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN 之間的跳轉和 RN 跳轉到原生相同,通過 push 的方法傳遞 name、passProps 和 callback 回調,只不過這裏的 name 不再是原生的協議,而是以下注冊的名稱。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Navigation.registerComponent(\"Test\", () => TestScreen);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"RN 頁面返回"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要關閉當前 RN 頁很簡單,只要調用 pop 方法就行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Navigation.pop(this.props.componentId, passProps:{})\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"passProps 指的是需要傳遞迴上一個頁面的參數,用於之前 push 中的 callback 方法的回調,沒有則可不寫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"容器生命週期"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN 的組件 Component 有自己的生命週期:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"constructor() \/\/ 構造方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"componentWillMount() \/\/ 即將加載"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"componentDidMount() \/\/ 加載完成"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"componentWillUnmount() \/\/ 組件已被卸載"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是有時候我們光這些並不能滿足我們的業務需求,對於一個 RN 頁面對用一個容器來說,容器的生命週期也至關重要,我們需要在容器的隱藏顯示過程中做點其他事,所以我們在新的路由框架中加入了容器的生命週期:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"containerWillAppear() \/\/ 容器即將顯示"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"containerDidAppear() \/\/ 容器正在顯示"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"containerWillDisappear() \/\/ 容器即將消失"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"containerDidDisappear() \/\/ 容器正在隱藏"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"容器的生命週期和組件的生命週期沒有必要先後順序,大多數情況下是在當容器再次顯示時需要做一些數據更新時使用,所以可能會調用多次。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"導航欄配置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前導航欄的配置是同 react-native-navigation 框架一樣,採用 Tree 的樣式將導航參數傳入,原生對其進行設置。基本的使用方式如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class MyScreen extends Component {\n  static options(passProps) {\n    return {\n      topBar: {\n        title: {\n       text: '我的頁面'\n      },\n        leftButtons: [\n          {\n            id: 'buttonOne',\n            icon: require('icon.png')\n          }\n        ],\n        rightButtons: [],\n      }\n    };\n  }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN 頁面加載的時候,會根據註冊的 PageName 獲取到當前頁面的 Component,上面的例子就是指 MyScreen,從而獲取到對應的靜態變量 options,再將它傳遞到當前的 RN 容器中,RN 容器會根據裏面的配置生成導航欄進行顯示,這樣使用原生的導航欄可以保證頁面和原生高度重合,而且也不用爲了修改原生導航欄而新增橋接類,方便高效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結及問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 WYNavigation 已經在微醫生項目中投入使用,使用過程中未發現明顯 bug,這一方案以一個簡單的方式很好的解決了複雜混合場景中導航棧混亂的問題。但是由於目前相關業務涉及到的頁面形式單一,所以對於其他 tab、modal 等涉及到多個 RN 頁面平級的情況未做過多的深入,但是在未來我們希望能儘快的補充這方面的缺失,把它作爲一個通用的解決方案,用於更多的 RN 和原生混合開發的項目中,希望感興趣的小夥伴和我們一起交流分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:黃麗麗"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:https:\/\/mp.weixin.qq.com\/s\/W75sT2px7P9rHJUrmTwY4g"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:React Native 原生混合路由解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:微醫大前端技術 - 微信公衆號 [ID:wed_fed]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章