[譯] 怎麼做:React Native 網頁應用。一場開心的掙扎

fatbin發表於2018-12-14

一個關於製作通用應用的簡短而詳細的教程

[譯] 怎麼做:React Native 網頁應用。一場開心的掙扎

你醒來。陽光燦爛,鳥兒在歌唱。沒有戰爭,沒有飢餓,程式碼可以輕易地被原生和 web 環境共享。是不是很贊?但很不幸,僅僅是後者,希望雖已經在地平線上,但仍然有一些事情需要我們去完成。

為什麼你需要關心?

如今在技術縮寫的海洋裡面,PWA(漸進式 Web 應用程式)是一個重要的三字詞語,但是它仍然有缺點。有很多被迫在開發原生應用以外還要開發 web 版的案例,其中也有很多技術難題。Ian Naylor 寫了一篇很棒的關於這個的文章

但是,對於你的電子商務生意,僅僅開發一個原生應用也是一個大錯誤。因此製作一個能夠在所有地方工作的軟體似乎是一個合乎邏輯的操作。你可以減少工作時間,以及生產、維護的費用。這就是為什麼我開始了這個小小的實驗。

這是一個簡單的用於線上訂餐的電子商務通用應用例子。在此之上,我建立了一個樣板,用於將來的專案以及更深入的實驗。

[譯] 怎麼做:React Native 網頁應用。一場開心的掙扎

Papu — 一個可用於安卓、iOS、web 的訂餐 APP

檢查一下你的基本模組

我們使用 React 來開展我們的工作,因此我們應該將應用邏輯與 UI 分離。使用類似 Redux/MobX/other 這樣的狀態管理系統是最好的選擇。這將使得我們的業務邏輯能在多個平臺之間複用。

檢視部分則是另外一個難題。為了構建你的應用的介面,你需要有一套通用的基本模組。他們需要能同時在 web 與原生環境下使用。不幸的是,web 上有著一套不一樣的東西。

<div>這是一個標準的 web 上的容器</div>
複製程式碼

而在原生上

<View>你好!我是 React Native 裡面的一個基礎容器</View>
複製程式碼

有些聰明的人想到了如何解決這個問題。我最喜歡的解決方案之一就是由 Nicolas Gallagher 製作的偉大的 React Native Web 庫。不僅僅是因為通過它能夠讓你在 web 上使用 React Native 元件(不是全部元件!)來解決基本模組的問題。它還暴露了一些 React Native 的 API,比如 Geolocation,Platform,Animated,AsyncStorage 等。快來 RNW guides 這裡看一些很棒的示例。

首先是一個樣板

我們已經知道如何解決基本模組的問題了,但是我們仍然要試著將 web 頁與原生的生產環境『粘』在一起。在我的專案中,我使用了 RN 的初始化指令碼(沒有展示在這裡),並且對於 web 部分我使用了 create-react-app。首先我通過 create-react-app rnw_web 建立了一個專案,然後通過 react-native init raw_native 建立了另一個。接著我在一個新的專案資料夾裡面,『科學怪人式』地將他們的 package.json 合併成一個,並在上面執行 yarn. 最終的 package 檔案長這樣:

{
  "name": "rnw_boilerplate",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.5.1",
    "react-art": "^16.5.1",
    "react-dom": "^16.5.1",
    "react-native": "0.56.0",
    "react-native-web": "^0.9.0",
    "react-navigation": "^2.17.0",
    "react-router-dom": "^4.3.1",
    "react-router-modal": "^1.4.2"
  },
  "devDependencies": {
    "babel-jest": "^23.4.0",
    "babel-preset-react-native": "^5",
    "jest": "^23.4.1",
    "react-scripts": "1.1.5",
    "react-test-renderer": "^16.3.1"
  },
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest",
    "start-ios": "react-native run-ios",
    "start-web": "react-scripts start",
    "build": "react-scripts build",
    "test-web": "react-scripts test --env=jsdom",
    "eject-web": "react-scripts eject"
  }
}
複製程式碼

React Native Web 樣板的 package.json 檔案(在這個版本里面沒有導航)

你需要將所有在 web 和 native 目錄裡的原始碼檔案複製到新的統一專案目錄中。

[譯] 怎麼做:React Native 網頁應用。一場開心的掙扎

需要複製到新專案的資料夾

下一步,我們將 App.js 與 App.native.js 放到我們新建立的 src 資料夾中。感謝 webpack 我們可以通過檔案擴充名來告訴打包器哪些檔案用在哪些地方。這對於使用分離的 App 檔案至關重要,因為我們準備使用不同的方式進行應用導航。

// App.js - WEB
import React, { Component } from "react";
import { View } from "react-native";
import WebRoutesGenerator from "./NativeWebRouteWrapper/index";
import { ModalContainer } from "react-router-modal";
import HomeScreen from "./HomeScreen";
import TopNav from "./TopNav";
import SecondScreen from "./SecondScreen";
import UserScreen from "./UserScreen";
import DasModalScreen from "./DasModalScreen";

const routeMap = {
  Home: {
    component: HomeScreen,
    path: "/",
    exact: true
  },
  Second: {
    component: SecondScreen,
    path: "/second"
  },
  User: {
    component: UserScreen,
    path: "/user/:name?",
    exact: true
  },
  DasModal: {
    component: DasModalScreen,
    path: "*/dasmodal",
    modal: true
  }
};

class App extends Component {
  render() {
    return (
      <View>
        <TopNav />
        {WebRoutesGenerator({ routeMap })}
        <ModalContainer />
      </View>
    );
  }
}

export default App;
複製程式碼

給 web 的 App.js. 這裡使用 react-router 進行導航。

// App.js - React Native

import React, { Component } from "react";
import {
  createStackNavigator,
  createBottomTabNavigator
} from "react-navigation";
import HomeScreen from "./HomeScreen";
import DasModalScreen from "./DasModalScreen";
import SecondScreen from "./SecondScreen";
import UserScreen from "./UserScreen";

const HomeStack = createStackNavigator({
  Home: { screen: HomeScreen, navigationOptions: { title: "Home" } }
});

const SecondStack = createStackNavigator({
  Second: { screen: SecondScreen, navigationOptions: { title: "Second" } },
  User: { screen: UserScreen, navigationOptions: { title: "User" } }
});

const TabNav = createBottomTabNavigator({
  Home: HomeStack,
  SecondStack: SecondStack
});

const RootStack = createStackNavigator(
  {
    Main: TabNav,
    DasModal: DasModalScreen
  },
  {
    mode: "modal",
    headerMode: "none"
  }
);

class App extends Component {
  render() {
    return <RootStack />;
  }
}

export default App;
複製程式碼

給 React Native 的 App.js. 這裡使用了 react-navigation。

我就是這樣製作了一個簡單的樣板以及給應用構造了一個框架。你可以通過克隆我的 github 倉庫來試一下我那個乾淨的樣板。

下一步我們將通過加入路由/導航系統來讓它複雜一些。

導航的問題與解決方案

除非你的應用只有一個頁面,否則你需要一些導航。現在(2018 年 9 月)只有一種能夠在 web 與原生中都能用的方法:React Router。在 web 中這是一個導航方法,但對於 React Native 來說不完全是。

React Router Native 缺少頁面過渡動畫,對後退按鈕的支援(安卓),模態框,導航條等等。而其他的庫則提供這些功能,例如 React Navigation.

我把它用在了我的專案中,但是你可以用其他的。於是我把 React Router 用在 web 端,把 React Navigation 用在原生。但這又導致了一個新問題。導航,以及傳參,在這兩個導航庫中有著很大不同。

為了保持在所有地方都有著更多的原生體驗這個 React Native Web 的精神,我通過製作網頁路由並將它們包裹在一個 HOC 裡面來解決這個問題。這樣能暴露出類似 React Navigation 的 API。

這使得我們無需給兩個『世界』分別製作元件即可實現在頁面之間導航。 第一步是建立一個用於 web 路由的路由 map 物件:

import WebRoutesGenerator from "./NativeWebRouteWrapper"; //用於生成 React Router 路由並將其包裹在一個 HOC 中的自定義函式

import WebRoutesGenerator from "./NativeWebRouteWrapper"; //用於生成 React Router 路由並將其包裹在一個 HOC 中的自定義函式

const routeMap = {
  Home: {
    screen: HomeScreen,
    path: '/',
    exact: true
  },
  Menu: {
    screen: MenuScreen,
    path: '/menu/sectionIndex?'
  }
}

//在 render 方法中
<View>
  {WebRoutesGenerator({ routeMap })}
</View>
複製程式碼

這個語法與 React Navigation 的 navigator 建構函式的一樣,除了多了一個 React Router 特定的選項。然後,通過我的輔助函式,我建立了一個 react-router 路由。並將其包裹在一個 HOC 中。這回將頁面元件拷貝一份,並在其 props 中新增一個 navigation 屬性。這模擬了 React Navigation 並暴露出一些方法,像是 navigate(), goBack(), getParam()

模態框

通過它的 createStackNavigator React Navigation 提供了一個選項,讓頁面像一個模態框一樣從底部滑出。為了在 web 端實現這個,我使用了由 Dave Foley 寫的 React Router Modal 庫。為了將某個頁面用作模態框,首先你需要在路由 map 中新增一個模態框選項:

const routeMap = {
  Modal: {
    screen: ModalScreen,
    path: '*/modal',
    modal: true //路由會用 ModalRoute 元件來渲染這個路由
  }
}
複製程式碼

此外你還需要新增一個 react-router-modal 庫中的 <ModalContainer /> 元件到你的應用中。這是模態框將會被渲染的地方。

頁面之間導航

感謝我們自定義的 HOC(暫時稱之為 NativeWebRouteWrapper,話說這真是一個糟糕的名字),我們可以使用一套跟 React Navigation 中的幾乎一樣的函式來實現在 web 端進行頁面切換:

const { product, navigation } = this.props
<Button 
  onPress={navigation.navigate('ProductScreen', {id: product.id})} 
  title={`Go to ${product.name}`}
/>
<Button 
  onPress={navigation.goBack}
  title="Go Back"
/>
複製程式碼

回到棧中的上一個頁面

在 React Navigation 中,你可以回到導航棧中的前 n 個頁面。然而在 React Router 中則做不到,因為這裡沒有棧。為了解決這個問題,你需要引入一個自定義的 pop 函式,以及傳一些引數進去。

import pop from '/NativeWebRouteWrapper/pop'

render() { 
  const { navigation } = this.props
  return (
    <Button
      onPress={pop({screen: 'FirstScreen', n: 2, navigation})}
      title="Go back two screens" 
    />
  )
}
複製程式碼

screen —— 頁面名字(在 web 端給 React Router 使用的) n —— 需要返回多少個頁面(給 React Navigation 使用的) navigation —— 導航物件

結果

如果你想試一下這個想法,我製作了兩個樣板。

第一個只是一個給 web 與原生的通用生產環境。你可以在這裡找到。

第二個則是第一個的加強版,新增了導航的解決方案。放到了這裡

另外還有一個基於這個想法的叫做 papu 的 demo 應用。它有很多 bug 以及死衚衕,不過你可以製作你自己的版本並在你的瀏覽器和手機上檢視,感受一下是怎麼工作的。

下一步

我們真的很需要一個通用的導航庫來使我們更容易地製作類似專案。讓 React Navigation 也能用在 web 環境會是很讚的事情(事實上今天你就可以做到,不過這會是一次坎坷的旅途 —— 可以到這裡瞭解一下

感謝你花時間閱讀!如果你喜歡這篇文章,希望你能分享出去。這是我的推特 有什麼問題請在下方評論 ?

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章