React Native純乾貨總結

隨著項目也漸漸到了尾聲,之前的項目是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=//myapi.com
GOOGLE_MAPS_API_KEY=abcdefgh

在React Native程式中訪問,默認會讀取.env文件中的內容

import Config from "react-native-config";

Config.API_URL; // '//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 許可權請求

    // //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

    // //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 //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

//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
 * <//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']
    }
};

參考鏈接