建立 React Web 和 React Native 的 Monorepo

使用 React Native 開發 APP 的一大好處就是可以與 React Web 共用程式,除了 UI, Routing 等無法共用以外,商業邏輯、hooks、utils、api 層處理,通通可以共用,要達到此目的我們可以使用 Monorepo,如果不知道 Monorepo 是什麼的可以參考 Fireship 的影片,概念上就是將所有的專案都放在同一個 repo 下達到方便共用的目的

Monorepos - How the Pros Scale Huge Software Projects // Turborepo vs Nx

1. 建立專案

這我們要做的文件夾結構,各個專案放在 packages 的資料夾內,其中的 common 資料夾是放 app 與 web 共用的程式

├── package.json
└── packages
├── app
├── common
└── web

1.1 建立文件夾與各專案

cd 到 packages 目錄後,使用 CLI 工具建立各專案

  • APP 的部分使用 React Native CLI,這裡我們設定使用 TypeScript 的模板
npx react-native init app --template react-native-template-typescript
  • Web 的部分使用 Create-React-App
npx create-react-app web --template typescript

2. 設定 Yarn Workspace

先移除各專案中的 node_modules

rm -rf packages/*/node_modules/

這理我們使用 Yarn Workspace 來建立 Monorepo,當然也可以使用其它的工具像是 Nx, Turborepo, Lerna 等,有機會之後試玩看看在寫文章與大家分享差別在哪

設定專案根目錄的 package.json,加上 private 為 true 和workspaces 屬性

{
"private": true,
"name": "todo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/react-native", "**/react-native/**"]
}
}

根目錄的 package.json

其中比較特別的是 nohoist 屬性,因為 yarn workspaces 會將所有的 dependencies 都安裝在專案根目錄中的 node_modules 裡,來避免底下的專案安裝同樣的包,來達到共享依賴包的機制,yarn 在 packages 的專案裡的 node_modules 會創建 symlinks (軟鏈結),來讓專案可以找到需要的依賴包

但是 react-native 所使用的 metro bundler 沒有支援 symlinks 的功能,所以 react-native 專案需要的套件必須在 app 目錄裡的 node_moudles 裡,metro 在打包時才找得到

3. 設定共用專案

在 packages 目錄新增一個 common 資料夾,然後使用 npm init -y 來新增一個 npm 模組,更改 package.json 來調整套件名稱,方便之後在 app 或 web 的專案裡辨識

如果要用 TypeScript 的話,記得將 package.json 裡的 main 屬性改為 index.ts,這會設定這個專案的主要檔案為 index.ts

{
"name": "<你的專案名稱>/common",
"version": "1.0.0",
"description": "",
"main": "index.ts", <-- 如果有用 TypeScript 的話,記得改這裡
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

common 的 package.json

然後試著新增一個 index.ts,export 一個 function 方便我們測試

export function testFunction () {
console.log('test function fire');
}

index.ts

4. 使用共用專案

在 app 的 pacakge.json 中新增共用依賴包

"dependencies": {
"react": "17.0.2",
"react-native": "0.68.1",
+ "<你的專案>/common": "1.0.0"
},

然後跑一次 yarn 指令來安裝依賴包

到這裡我們還不能使用 common 包裡的程式,我們需要設定 metro 來讀取根目錄的 node_modules,因為我們的 common 包會被放在那

const path = require('path');

module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
},
}),
},
resolver: {
extraNodeModules: {
'node_modules': path.resolve(__dirname, '..', '..')
}
},
watchFolders: [
path.resolve(path.join(__dirname, '../..'))
]
};

我們主要設定了

  • resolver 裡的 extraNodeModules 為根目錄的 node_modules
  • 設定 watchFolders 監看整個專案

5. 測試看看吧

我們分別在 app 跟 web 的 App.tsx 元件裡使用剛剛從 common 裡 export 的 function

測試 App

import { testFunction } from '@todo/common'

const App = () => {
testFunction()
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};

return (
<SafeAreaView style={backgroundStyle}>
// ... 以下省略
</SafeAreaView>
);
};

Image.png

測試 Web

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { testFunction } from '@todo/common';

function App() {
testFunction()
return (
// ...省略
);
}

export default App;

web 的 App.tsx

Image.png

Recursive Component 是什麼? AWS Fargate 入門
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×