React Native純乾貨總結

糊糊糊糊糊了發表於2022-02-22

隨著專案也漸漸到了尾聲,之前的專案是mobile開發,採用的是React Native。為即將要開始做RN專案或者已經做過的小夥伴可以參考借鑑,也順便自己做一下之前專案的總結。

文章比較長,可以選擇自己感興趣的章節瞭解下。

專案整體技術棧:

  • React Native
  • React Navigation
  • Firebase
  • Jotai(專案後期加入)
  • Typescript
  • Jest

1. 配置專案絕對路徑

一般的專案都會有一個原始碼目錄src ,並且隨著專案的膨脹,會出現越來越多的子目錄,這時候如果不加控制的話,會出現一大堆類似這種../../.. 的相對路徑,找檔案也不是非常方便。

1.1 沒有整合Typescript

You can simply create a package.json file inside the folder you want to import it. Yes, at a glance this probably seems a little weird, but with practice, you get used to it. It should have a single property with the folder name as its value. Let’s create our package.json file inside the components folder.

配置絕對路徑需要做的事就是在你需要引入的檔案中新增一個package.json

{
  "name": "components"
}

然後你就可以使用了

import MyComponent from "components/MyComponent";

1.2 整合了Typescript

如果專案整合了typescript,可能還需要做一些額外的操作,首先新增一個庫:react-native-typescript-transformer

yarn add --dev react-native-typescript-transformer

然後需要配置tsconfig.json

{
  "compilerOptions": {
    "target": "es2015",
    "jsx": "react",
    "noEmit": true,
    "moduleResolution": "node",
    "paths": {
      "src/*": ["./*"],
    },
  },
  "exclude": [
    "node_modules",
  ],
}

然後在metro.config.js 中配置transformer.babelTransformerPath(我在專案中後來去掉了這個配置,發現也能使用??)

module.export = {
  transformer: {
    babelTransformerPath: require.resolve('react-native-typescript-transformer')
  }
};

通過上面的配置,你就可以使用絕對路徑了。

1.3 一些問題

ESLint: 'react-native' should be listed in the project's dependencies. 
Run 'npm i -S react-native' to add it(import/no-extraneous-dependencies)

no extraneous dependencies

'import/no-extraneous-dependencies': [
  'error',
  { packageDir: path.join(__dirname) }
]

1.4 自動在intellij中啟用絕對路徑

Editor → Code Style → Typescript → Imports → Use paths relative to tsconfig.json

auto import

2. 圖示處理

React Native不支援動態的require,所以圖示處理的方法就是提前引入,動態生成。

首先將需要的圖示放在assets 目錄下,然後依次引入只有匯出,生成型別宣告(為了能夠智慧提示)。下面是一個例子

// icons.ts

import AlertIcon from 'xxx/assets/icons/alert.svg';

export const ICONS = {
  alert: AlertIcon
  // ...
}

export type IconNameType =
  | 'alert';

編寫一個Icon元件,在元件中根據傳入屬性引入

const percentToHex = (percent: number) => {
    const formattedPercent = Math.max(0, Math.min(100, percent));
    const intValue = Math.round((formattedPercent / 100) * 255);
    const hexValue = intValue.toString(16);
    return hexValue.padStart(2, '0').toUpperCase();
};

const Icon = ({
    style,
    name,
    size = spacing.lg,
    color = definedColor.text.primary,
    opacity = 100,
    testId,
    width = size,
    height = size
}: IconProps) => {
    const RequiredIcon = ICONS[name];
    const colorWithOpacity = `${color}${percentToHex(opacity)}`;

    return (
        <RequiredIcon
            style={[styles.icon, style]}
            testID={testId}
            width={width}
            height={height}
            color={colorWithOpacity}
        />
    );
};

// usage
import Icon from 'components/icon/Icon';

<Icon
  style={styles.icon}
  name="group"
  color={color.brand.primary}
  size={spacing.lg}
  opacity={100}
/>

為了方便之後的圖示引入,同時編寫了一個指令碼,在每次加入圖示的時候自動重新重新整理icons.ts檔案。

// scripts/update-icon.js

const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const { upperFirst, camelCase } = require('lodash');

const root = path.resolve(__dirname, '..');
const iconsDir = path.resolve(root, 'assets/icons');
const destPath = path.resolve(root, 'src/constants/icons.ts');
const indent = '    ';

const getFolderIcons = (iconFolderPath) => {
    const iconNames = [];
    fs.readdirSync(iconFolderPath).forEach((file) => {
        if (file.endsWith('.svg')) {
            iconNames.push(file.replace('.svg', ''));
        }
    });
    return iconNames.sort();
};

const assembleTypes = (iconNames) => {
    let code = iconNames.map((name) => `${indent}| '${name}'`).join(`\n`);
    code = `export type IconNameType =\n${code};\n`;
    return code;
};

const formatIconFile = () => {
    const command = `npx prettier --write ${destPath}`;
    exec(command, (error) => {
        if (error) {
            console.error('error', error.message);
            process.exit(1);
        }
    });
};

const assembleIcons = (iconNames) => {
    const imports = [];
    const transformer = (name) => {
        const icon = `${upperFirst(camelCase(name))}Icon`;
        imports.push(
            `import ${icon} from 'xxx/assets/icons/${name}.svg';\n`
        );
        return `${indent}'${name}': ${icon},\n`;
    };
    let code = iconNames.map(transformer).join('');

    code = `${imports.join('')}\nexport const ICONS = {\n${code}}`;
    return code;
};

const generateScript = (relativeFilePath, script) => {
    const autoGeneratedNote =
        "// This is auto generated by script, please don't modify it manually!\n\n";
    const scriptContent = autoGeneratedNote + script;
    const absolutePath = path.resolve(__dirname, '..', relativeFilePath);
    fs.writeFile(absolutePath, scriptContent, (err) => {
        if (err) {
            console.error(`Failed: writing to ${relativeFilePath}`);
            process.exit(1);
        }
        // eslint-disable-next-line no-console
        console.info(`Success: Writing to ${relativeFilePath}`);
        formatIconFile();
    });
};

const iconNames = getFolderIcons(iconsDir);
const iconTypesContent = assembleTypes(iconNames);
const icons = assembleIcons(iconNames);
const iconsContent = `${icons}\n\n${iconTypesContent}`;

generateScript('src/constants/icons.ts', iconsContent);

然後在package.json中配置兩個執行命令

{
    "scripts": {
        "update-icon": "node ./scripts/update-icon.js",
        "watch:update-icon": "watch 'yarn update-icon' ./assets/icons"
    }
}

3. React Native適配

為了更好的視覺與使用者操作體驗,目前流行的移動端適配方案,在大小上都是進行寬度適配,在佈局上垂直方向自由排列。這樣做的好處是:保證在頁面上元素大小都是按設計圖進行等比例縮放,內容恰好只能鋪滿螢幕寬度;垂直方向上內容如果超出螢幕,可以通過手指上滑下拉檢視頁面更多內容。

在iPhone 6/7/8 Plus(414X736)機型上,渲染一個設計圖375尺寸元素的話,很容易計算出,我們實際要設定的寬度應為:375 * 414/375 = 414。這裡的414/375就是裝置邏輯畫素寬度比例。

公式: WLR = 裝置寬度邏輯畫素/設計圖寬度

WLR(width logic rate 縮寫),散裝英語,哈哈。在這裡,裝置的寬度邏輯畫素我建議用Dimensions.get('window').width獲取,具體緣由,後面會進行解釋。

那麼,在目標裝置上要設定的尺寸計算公式就是:size = 設定圖上元素size * WLR

3. 1 區域性盒子全部按比例

區域性盒子全部按比例。意思就是RN頁面中的元素大小、位置、內外邊距等涉及尺寸的地方,全部按上述一比例中的尺寸計算公式進行計算。如下圖所示:

responsive

這樣渲染出來的效果,會最大限度的保留設計圖的大小與佈局設計效果。

4. 根據環境變數載入不同的配置

4.1 Setup

yarn add react-native-config

cd ios && pod install

4.2 基本用法

在專案的根路徑下建立一個.env 檔案

API_URL=https://myapi.com
GOOGLE_MAPS_API_KEY=abcdefgh

在React Native程式中訪問,預設會讀取.env檔案中的內容

import Config from "react-native-config";

Config.API_URL; // 'https://myapi.com'
Config.GOOGLE_MAPS_API_KEY; // 'abcdefgh'

? 請記住,這個模組不會對祕密進行混淆或加密包裝,所以不要在.env中儲存敏感金鑰基本上不可能防止使用者對移動應用程式的祕密進行逆向工程,所以在設計你的應用程式(和API)時要考慮到這一點。

4.3 多環境配置

為多環境建立不同的檔案,比如.env.dev.env.staging.env.prod,然後在檔案中配置不同的變數。

預設情況下 react-native-config 會讀取.env檔案中的內容,如果需要讀取不同環境中的配置檔案,則需要在指令碼中指定讀取的檔案。

$ ENVFILE=.env.staging react-native run-ios           # bash
$ SET ENVFILE=.env.staging && react-native run-ios    # windows
$ env:ENVFILE=".env.staging"; react-native run-ios    # powershell

4.3.1 整合到Fastlane中

使用fastlane提供的action或者直接設定

## action
## TODO ???
environment_variable

## manully set
ENV['ENVFILE'] = '.env.staging'

4.3.2 注意

如果採用的是fastlane [lane] --env [beta|production],這個需要你將.env檔案放置在你的fastlane資料夾中,這點和react-native-config不能搭配使用

Using react-native-config with fastlane

5. 除錯工具

5.1 React Native Debugger

brew install --cask react-native-debugger

如果想監聽不同的埠,可以使用 CMD + T 來建立一個新視窗。

5.2 Flipper

如果想擁有更加全面的除錯功能,可以使用Flipper

brew install --cask flipper

5.2.1 使用最新的Flipper SDK

By default React Native might ship with an outdated Flipper SDK. To make sure you are using the latest version, determine the latest released version of Flipper by running npm info flipper.

Android:

  1. Bump the FLIPPER_VERSION variable in android/gradle.properties, for example: FLIPPER_VERSION=0.103.0.
  2. Run ./gradlew clean in the android directory.

iOS:

  1. Call use_flipper with a specific version in ios/Podfile, for example: use_flipper!({ 'Flipper' => '0.103.0' }).
  2. Run pod install in the ios directory.

6. 視覺迴歸測試

目前支援RN的視覺迴歸測試並不是特別多,就算是有,畫素的比較也經常出現偏差,要麼就是特別複雜。這裡列幾種web端以及RN端常用的視覺測試工具。

Web端

  • BackStopJs
  • Storybook (@storybook/addon-storyshots)
  • Chromatic
  • jsdom-screenshot + jest-image-snaphost

React Naitve端

  • Storybook + Loki (React Native Supported), 使用loki可能實現影像比較,但是比較會經常出現偏差,另一方面,影像比較需要開啟模擬器,在CI上build可能會比較耗資源
  • React-Native-View-Shot + Jest-Image-Snapshot(這個需要手動實現)

7. Mobile E2E

我在專案中沒有寫過React Native的E2E測試,這裡只作下記錄

  • Appium
  • Detox

8. 模擬器

8.1 檢視模擬器名稱

xcrun simctl list devices

simctl list

8.2 新增新的模擬器

WindowDevices and Simulators+

add simctl

new simctl

8.3 模擬網路

Sign in with your Apple ID

下載 Additional Tools for Xcode 12.5 ,找到Hardware資料夾,進入後就能見到Network Link Conditioner.prefPane檔案,系統的sytem preference會出現Network Link Conditioner ,模擬各種網路

9. Firebase

9.1 Api設計

整體的設計還是比較中規中矩,抽離Service單獨呼叫呼叫Repository層,在Service層中處理關於超時以及如何使用快取的一些策略,跟firesotre打交道的操作全部封裝到firestore模組中。

api design

這裡主要列舉兩個常用到的方法


get: async <T>(path: string, opts?: DataServiceGetOptions<T>) => {
  const options = { ...DEFAULT_OPTIONS, ...opts };
  const fetchOptions = pick(opts, FILTER_FIELDS) as FetchOptions;
  const ref = await getRef<T>(path, fetchOptions);
  const { strategy } = options;

  // 選擇快取策略
  const getOptions = await buildGetOptions(strategy);

  // 處理超時
  const resultData = await handleTimout(
    () => getSnapshots(ref, getOptions),
    (snapshot: GetSnapshot<T>) => getDataFromSnapshot<T>(snapshot),
    options
  );

  if (isCacheFirst(strategy)) {
    if (strategy.cacheKey && getOptions.source === SOURCE_TYPE.SERVER) {
      // 首次更新cache key的時間戳
      await updateLocalUpdatedTime(strategy.cacheKey);
    }
  }

  return resultData;
}

onSnapshot: <T>(
  path: string,
  onNext: OnNextHandler<T>,
  options?: FetchOptions<T>
): (() => void) => {
  const ref = getRef<T>(path, options);
  // 得到的ref型別不一致,作為查詢來說,選擇一種型別就好
  return (ref as Query<T>).onSnapshot((snapshots) => {
      const data = getDataFromSnapshot(snapshots) as DocsResultData<T>;
      onNext(data);
  });
}

// usage

// 快取優先
export const getData = () =>
  DataService.get<Data>(`${Collections.Data}/xxx`, {
    strategy: { cacheKey: 'DATA' }
  });

// 只走快取
export const getData2 = (userId: string) =>
  DataService.get<Data>(`${Collections.Data}/${userId}`, {
    strategy: 'cacheOnly'
  });

// 只走伺服器
export const getData2 = (userId: string) =>
  DataService.get<Data>(`${Collections.Data}/${userId}`, {
    wheres: [['property1', '==', 'xxx']]
    strategy: 'serverOnly'
  });

export const onDataChange = (
    docPath: string,
    onNext: OnNextHandler<Data>
) => DataService.onSnapshot<Data>(docPath, onNext);

一些tips

  • 獲取單個doc和列表的時候,可以將返回的結果處理成一致的,比如獲取單個doc,可以使用res[0]來獲取

  • 再返回的doc中,加上id,方便查詢

    export type WithDocId<T> = T & { docId: string };
    
  • 有關聯的collection,儲存路徑進行查詢,無需直接儲存引用ref

    export type Path<T> = string; // just for label return value
    
    export interface Collection1 {
        ref1: Path<Collection2>;
        ref2: Path<Collection3>;
    }
    

9.2 Notification

這裡採用的是firebase的messaging模組做訊息推送。

9.2.1 許可權請求

    // https://rnfirebase.io/messaging/ios-permissions#reading-current-status

    import messaging from '@react-native-firebase/messaging';

    async function requestUserPermission() {
      const authorizationStatus = await messaging().requestPermission();

      if (authorizationStatus === messaging.AuthorizationStatus.AUTHORIZED) {
        console.log('User has notification permissions enabled.');
      } else if (authorizationStatus === messaging.AuthorizationStatus.PROVISIONAL) {
        console.log('User has provisional notification permissions.');
      } else {
        console.log('User has notification permissions disabled');
      }
    }

9.2.2 傳送訊息

在傳送訊息之前,我們需要知道往哪臺手機傳送訊息,就需要知道裝置的token,所以首先得獲取裝置token

    // https://rnfirebase.io/messaging/server-integration#saving-tokens
    
    import React, { useEffect } from 'react';
    import messaging from '@react-native-firebase/messaging';
    import auth from '@react-native-firebase/auth';
    import firestore from '@react-native-firebase/firestore';
    import { Platform } from 'react-native';
    
    async function saveTokenToDatabase(token) {
      // Assume user is already signed in
      const userId = auth().currentUser.uid;
    
      // Add the token to the users datastore
      await firestore()
        .collection('users')
        .doc(userId)
        .update({
          tokens: firestore.FieldValue.arrayUnion(token),
        });
    }
    
    function App() {
      useEffect(() => {
        // Get the device token
        messaging()
          .getToken()
          .then(token => {
            return saveTokenToDatabase(token);
          });
    
        // If using other push notification providers (ie Amazon SNS, etc)
        // you may need to get the APNs token instead for iOS:
        // if(Platform.OS == 'ios') { messaging().getAPNSToken().then(token => { return saveTokenToDatabase(token); }); }
    
        // Listen to whether the token changes
        return messaging().onTokenRefresh(token => {
          saveTokenToDatabase(token);
        });
      }, []);
    }
    POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1
    
    Content-Type: application/json
    Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
    
    {
       "message":{
          "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
          "data":{},
          "notification":{
            "body":"This is an FCM notification message!",
            "title":"FCM Message"
          }
       }
    }

9.2.3 處理互動

    import React, { useState, useEffect } from 'react';
    import messaging from '@react-native-firebase/messaging';
    import { NavigationContainer, useNavigation } from '@react-navigation/native';
    import { createStackNavigator } from '@react-navigation/stack';
    
    const Stack = createStackNavigator();
    
    function App() {
      const navigation = useNavigation();
      const [loading, setLoading] = useState(true);
      const [initialRoute, setInitialRoute] = useState('Home');
    
      useEffect(() => {
        // Assume a message-notification contains a "type" property in the data payload of the screen to open
        messaging().onNotificationOpenedApp(remoteMessage => {
          console.log(
            'Notification caused app to open from background state:',
            remoteMessage.notification,
          );
          navigation.navigate(remoteMessage.data.type);
        });
    
        // Check whether an initial notification is available
        messaging()
          .getInitialNotification()
          .then(remoteMessage => {
            if (remoteMessage) {
              console.log(
                'Notification caused the app to open from quit state:',
                remoteMessage.notification,
              );
              setInitialRoute(remoteMessage.data.type);// e.g. "Settings"}
            setLoading(false);
          });
      }, []);
    
      if (loading) {
        return null;
      }
    
      return (
        <NavigationContainer>
          <Stack.Navigator initialRouteName={initialRoute}>
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Settings" component={SettingsScreen} />
          </Stack.Navigator>
        </NavigationContainer>);
    }

如果整合了React Native Navigation

https://reactnavigation.org/docs/deep-linking

    const config = {
      screens: {
        [HOME_STACK_SCREEN]: {
          screens: {
            initialRouteName: HOME,
            Page: {
              path: 'path/:key',
              parse: {
                key: (parameters: string) => JSON.parse(parameters)
              }
            }
          }
        },
        NoMatch: '*'
      }
    };
    
    export const linking: LinkingOptions<ReactNavigation.RootParamList> = {
      prefixes: [PREFIX],
      config,
      getInitialURL: async () => {
        const url = await Linking.getInitialURL();
    
        if (url != null) {
          return url;
        }
    
        const message = await getInitialNotification();
        return handleNotification(message);
      },
      subscribe: (listener) => {
        const onReceiveURL = ({ url }: { url: string }) => listener(url);
        Linking.addEventListener('url', onReceiveURL);
    
        const unsubscribe = onNotification(async (message) => {
          // URL need contains [PREFIX][path/:key]
          const url = await handleNotification(message);
          if (url) {
            listener(url);
          }
        });
    
        return () => {
          Linking.removeEventListener('url', onReceiveURL);
          unsubscribe();
        };
      }
    };
    
    function App() {
      return (
        <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
          {/* content */}
        </NavigationContainer>
      );
    }

9.2.4 Badge顯示

firebase messaging的訊息分為兩種,一種是帶notification 欄位的(這種情況傳送訊息會會自動有訊息彈窗),一種是data-only的(就是不帶notification欄位的)

在官方的文件中,在後臺監聽訊息到來,需要使用setBackgroundMessageHandler這個callback

Cloud Messaging | React Native Firebase

    // index.js
    import { AppRegistry } from 'react-native';
    import messaging from '@react-native-firebase/messaging';
    import notifee from '@notifee/react-native';
    import App from './App';
    
    // Register background handler
    messaging().setBackgroundMessageHandler(async remoteMessage => {
      console.log('Message handled in the background!', remoteMessage);
    	await notifee.incrementBadgeCount();
    });
    
    AppRegistry.registerComponent('app', () => App);
Foreground Background Quit
Notification onMessage setBackgroundMessageHandler setBackgroundMessageHandler
Notification + Data onMessage setBackgroundMessageHandler setBackgroundMessageHandler
Data onMessage setBackgroundMessageHandler setBackgroundMessageHandler

根據文件上來看,帶有notification的訊息,在後臺情況下也能在setBackgroundMessageHandler函式中監聽到,但是就實際的測試情況來看,貌似不太行(應該是這個原因,專案太久了,不太知道當時測試的情況了),所以不得不迫使我們採用data-only的訊息來傳送,那麼這時處理訊息的彈窗就需要自己來實現了。這裡有幾個庫可以推薦

Firebase Admin SDK
    // Data-only message
    
    admin.messaging().sendToDevice(
      [], // device fcm tokens...
      {
        data: {
          owner: JSON.stringify(owner),
          user: JSON.stringify(user),
          picture: JSON.stringify(picture),
        },
      },
      {
        // Required for background/quit data-only messages on iOS
        contentAvailable: true,
        // Required for background/quit data-only messages on Android
        priority: 'high',
      },
    );
Rest API
    {
      "message": {
        "token": "your token",
        "data": {
          "payload": "your payload",
          "type": "0"
        },
        "apns": {
          "payload": {
            "aps": {
              "content-available": 1
            }
          }
        }
      }
    }
在應用啟用時清除badge數量

push notification app badges in v6.x · Issue #3419 · invertase/react-native-firebase

    // AppDelegate.m
    
    - (void)applicationDidBecomeActive:(UIApplication *)application {
      [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
    }

push notification app badges in v6.x · Issue #3419 · invertase/react-native-firebase

solution for iOS works fine, what's left is android, looking at Shourtcut Badger but if it appears abandoned then I might be a bit skeptical using it, will try to test on android and see if it works fine though.

If you're looking for a way to reset the badge at Launch you can use the solution above for iOS, however, this does not reset the badge count if the app was not in quiet mode at the point of being launched.

As for Android, you can check out Shourtcut Badger, hope this helps.

10. 在pre-commit hook中讓tsc只檢查你改動的檔案

配置了lint-stage用來在提交程式碼時,只檢查你改動的檔案,但是對於tsc --noEmit卻發現每次在提交程式碼的時候,進行了整個codebase的檢查,問題的原因在一篇blog中發現,摘錄如下

The issue I found - and at the time of writing, it is still being discussed - is that lint-staged would pass each staged file to the npx tsc command like npx tsc --noEmit file1.ts file2.ts and that causes TypeScript to simply ignore your tsconfig.json.

解決的辦法是使用tsc-files這個庫,原理就是在每次檢查的時候,使用—project指定生成的config,而摒棄專案自帶的tsconfig

10.1 手動實現

// .lintstagedrc.js

const fs = require('fs');

const generateTSConfig = (stagedFilenames) => {
    const tsconfig = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8'));
    tsconfig.include = stagedFilenames;
    fs.writeFileSync('tsconfig.lint.json', JSON.stringify(tsconfig));
    return 'tsc --noEmit --project tsconfig.lint.json';
};

module.exports = {
    '*.{ts,tsx}': ['eslint --ext .ts,.tsx --fix', generateTSConfig]
};

11. 遇到的一些問題

11.1 Unable to lookup item 'Path' in SDK 'iphoneos'

Luckily enough, the fix is very simple: open a new terminal window and type the following command:

sudo xcode-select --print-path

If XCode is installed, you should see a wrong installation path, such as /Library/Developer/ or something like that: it’s very likely that your XCode installation is located somewhere else – such as in the /Applications/ folder. If that’s so, you can fix your issue by typing the following command:

sudo xcode-select --switch /Applications/Xcode.app

If the xcode-select command return a “not found” error, it most likely means that XCode is not installed: just install it and try again.

Mac - XCode - SDK "iphoneos" cannot be located - how to fix

11.2 Invariant Violation: Native module cannot be null. - RNFBNativeEventEmitter

[?] Invariant Violation: Native module cannot be null. - RNFBNativeEventEmitter · Issue #4265 · invertase/react-native-firebase

jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')

11.3 ReferenceError: Property 'Intl' doesn't exist, js engine: hermes

dayjs/huxon → Intl

Plan A (not working)

Intl polyfills for React Native 0.63 and hermes

For those who have the same problem. I've posted the same on reddit.

I was finally able to get intl to work updating react native from 0.63.3 to 0.64.1 and installing the polyfills from @formatjs and import them in index.js in the order shown in the graph

Plan B (not working)

Intl Support · Issue #23 · facebook/hermes

// index.js

import { AppRegistry } from 'react-native';
import App from './App.tsx';
import { name as appName } from './app.json';

if (global.HermesInternal) {
    require('@formatjs/intl-getcanonicallocales/polyfill');
    // require('@formatjs/intl-locale/polyfill');
    // require('@formatjs/intl-pluralrules/polyfill');
    // require('@formatjs/intl-pluralrules/locale-data/en');
    // require('@formatjs/intl-numberformat/polyfill');
    // require('@formatjs/intl-numberformat/locale-data/en');
    // require('@formatjs/intl-datetimeformat/polyfill');
    // require('@formatjs/intl-datetimeformat/locale-data/en');
    // require('@formatjs/intl-datetimeformat/add-golden-tz');
}

AppRegistry.registerComponent(appName, () => App);

Plan C (work)

use dateformat library to format date only.

moment-timezone

11.4 Port 8080 is not open on localhost, could not start Firestore Emulator.

Port 8080 is not open on localhost, could not start Firestore Emulator.

> lsof -i :8080

COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    57931 yhhu  200u  IPv6 0x6ad5a0f10de7daa1      0t0  TCP localhost:http-alt (LISTEN)
java    57931 yhhu  202u  IPv6 0x6ad5a0f107a7d761      0t0  TCP localhost:56123->localhost:http-alt (ESTABLISHED)
java    57931 yhhu  203u  IPv6 0x6ad5a0f107a7caa1      0t0  TCP localhost:http-alt->localhost:56123 (ESTABLISHED)

# kill
kill 57931

11.5 clear cache

RN < 0.50

watchman watch-del-all
rm -rf $TMPDIR/react-* 
rm -rf node_modules/
npm cache clean
npm install
npm start -- --reset-cache

RN ≥ 0.50

watchman watch-del-all
rm -rf $TMPDIR/react-native-packager-cache-*
rm -rf $TMPDIR/metro-bundler-cache-*
rm -rf node_modules/
npm cache clean
npm install
npm start -- --reset-cache

RN ≥ 0.63

watchman watch-del-all
rm -rf node_modules
npm install
rm -rf /tmp/metro-*
npm run start --reset-cache

npm >= 5

watchman watch-del-all
rm -rf $TMPDIR/react-*
rm -rf node_modules/
npm cache verify
npm install
npm start -- --reset-cache

windows

del %appdata%\Temp\react-native-*
cd android
gradlew clean
cd ..
del node_modules/
npm cache clean --force
npm install
npm start -- --reset-cache

windows

11.6 React Native Storybook doesn't work with iOS/Android Hermes Engine in React Native v0.64.0

React Native Storybook doesn't work with iOS/Android Hermes Engine in React Native v0.64.0 · Issue #152 · storybookjs/react-native

解決辦法 inlineRequires: false

/**
 * Metro configuration for React Native
 * <https://github.com/facebook/react-native>
 *
 * @format
 */

module.exports = {
    transformer: {
        getTransformOptions: async () => ({
            transform: {
                experimentalImportSupport: false,
                **inlineRequires: false**
            }
        }),
        babelTransformerPath: require.resolve(
            'react-native-typescript-transformer'
        )
    }
};

11.7 Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.

createDefaultProgram: true

error: Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. · Issue #967 · typescript-eslint/typescript-eslint

module.exports = {
    root: true,
    parser: '@typescript-eslint/parser',
    parserOptions: {
        createDefaultProgram: true,
        project: './tsconfig.json',
        sourceType: 'module'
    },
    extends: ['@react-native-community', 'airbnb-typescript', 'prettier'],
    rules: {
        semi: ['error', 'always'],
        'import/no-extraneous-dependencies': ['off'],
        'import/prefer-default-export': ['off'],
        'react/require-default-props': ['off'],
        'react/jsx-props-no-spreading': ['off']
    }
};

參考連結

相關文章