React Native Web nasıl kullanılır? Monorepo kavramı Nedir?
React Native Web kütüphanesi, RN bileşenleri ile cross platform uygulama yapmak için bileşenler sunar. Bu bileşenler daha sonra derlenerek (transpile) edilerek <div> karşılıklarına dönüşür ve HTML dokümana basılır. Bu yazıda da nasıl React Native Web kütüphanesinin kullanılacağına değineceğim. Ama öncelikle mobil ve web ortamını bir arada geliştireceğimiz monorepo kavramının ne olduğuna değinelim.
Monorepo nedir?
yarn veya npm ile init edilen her projenin bir package.json dosyası vardır ve bu projeler bir modül teşkil eder. Bu bağlamda, mobil ve web ortamı da aslında ayrı birer modül diyebiliriz. Cross platform geliştirim için bu modüller aslında ortak olarak bir kod seti kullanıyorlar. Örneğin react-native ile react-native-web temelinde aynı react kütüphanesini kullanacaktır ve ikisi için de ayrı ayrı node_modules dizininin tutulmasına gerek yoktur. Hâl böyle olunca iki package.json dosyasının bir noktadan yönetimi söz konusu olabilir. İşte bu şekilde tek noktadan paket yönetimine monorepo adı verilir. yarn’da yer alan workspaces kavramı ile proje içerisinde monorepo mimarisi kurulabilir.
Yarn workspaces nedir?
Yarn’daki workspaces kavramı tam olarak yarn projeleri için bir monorepo ortamı sunmayı amaçlıyor. Öncelikle workspace’leri kapsayan root projede aşağıdaki gibi bir package.json dosyası oluşturuluyor:
Workspace’ler doğası gereği publish edilmemek üzere oluşturulurlar. Çünkü bir projenin workspace’inin başka bir proje içerisinde kullanımı paketler arası uyumsuzlukları arttıracaktır. Buradaki "private”: true
özelliği de workspace’in kazara publish edilmesini engelliyor. Aslında bir çeşit koruma yöntemi olduğu için yarn ekibi tarafından zorunlu hale getirilmiş.
"workspaces":
ile projenin barındırdığı diğer modülleri belirliyoruz.
Şimdi mobile modülü için package.json dosyası aşağıdaki gibi olsun:
mobile modülünü paylaşımlı kullanacak olan web
modülünün package.json dosyası ise aşağıdaki gibi olacaktır:
yarn
komutu çalıştırılıp paketler yüklendiğinde aşağıdaki gibi bir dizin mimarisi oluşacaktır:
Not: web projesi herhangi bir modül tarafından kullanılmadığı için root dizinindeki node_modules’de web için ayrı bir dizin bulunmaz.
Gördüğünüz üzere her proje için ayrı bir node_modules yerine ana projede tutuluyor. Ayrıca web tarafında mobile modülü kullanılacağı için ana projede de bu bağımlılık eklenmiş. Fakat buradaki can alıcı kısım dizinin içeriğinin eklenmesi yerine symlink (kısayol) yer alarak, projenin asıl dosyalarına işaret etmektedir. Bu sayede ortak modüllerin kopyalanarak barındırılması yerine bir symlink yetiyor. Buna hoisting deniyor. Aslında symlink olayı metro bundler için problemli bir durum oluşturuyor. O konuya yazının ilerleyen kısımlarında değineceğiz.
Yarn workspaces olayı da bu kadar özetlenebilir. Şimdi gerçek bir projede react-native-web ile react-native’in nasıl ortaklaşa çalıştırılacağına değinelim.
Bağımlılıkların yüklenmesi
Projeyi anlatmaya geçmeden önce, bu yazıda bazı küçük yardımcı komutlar ile vscode’u açmadan terminal üzerinden işlerimizi kolaylıkla görebileceğiz. Örneğin sed komutu ile dosyayı direkt olarak terminalden düzenleyebiliriz. Tabii vscode üzerinden de ilerleyebilirsiniz. Ben pratiklik olması açısından bu yolu tercih ediyorum. Böylelikle workspace’i daha sonra otomatize ederek de oluşturabiliriz.
Öncelikle eğer macos kullanıyorsanız gnu-sed komutu işimize yarayacaktır:
brew install gnu-sed
sed’yi gnu-sed ile değiştirmek için aşağıdaki komutu çalıştırabilirsiniz:
PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"
Windows ortamında geliştirim yapıyorsanız WSL (Windows Subsystem for Linux) veya Cmder kullanmanızı öneririm.
Monorepo’nun oluşturulması
Öncelikle projemizi oluşturalım. Örnek olması açısından dizinin adını monorepo koydum. Siz tercihinize göre instagram, facebook gibi gerçek proje isimleri koyabilirsiniz:
mkdir monorepo && cd monorepo
Aşağıdaki komut sayesinde create-react-app cli’ının son hali sisteminize global olarak yüklenecek ve create-react-app CLI’ı kullanılarak Typescript’li bir react projesi oluşturulacaktır:
yarn create react-app web --typescript && cd web
react-native-web kütüphanesi aşağıdaki gibi yüklenir.
yarn add react-native-web
react-native için typescript tip tanımlamaları aşağıdaki gibi ekleyiniz:
yarn add -D @types/react-native
create-react-app’in ürettiği gereksiz dosyaların silelim ve projeyi vscode’da açalım:
cd src && rm logo.svg index.css App.css App.test.tsx && cd .. && code .
Projemiz klasik bir web projesi olmadığından dolayı, ReactDOM
ile artık işimiz kalmıyor. Bu nedenle React Native’in AppRegistry
sınıfı üzerinden uygulamamızı arayüze bindireceğiz. Bunun için index.tsx
’in içerisine aşağıdaki kodu yapıştıralım:
import { AppRegistry } from "react-native";
import App from "./App";AppRegistry.registerComponent("App", () => App);AppRegistry.runApplication("App", {
initialProps: {},
rootTag: document.getElementById("root")
});
CLI’ı kullanarak printf
ile de aynı işlemi gerçekleştirebilirsiniz:
printf 'import { AppRegistry } from "react-native";\nimport App from "./App";\nAppRegistry.registerComponent("App", () => App);\nAppRegistry.runApplication("App", {\n initialProps: {},\n rootTag: document.getElementById("root")\n});\n' > src/index.tsx
Şimdi değişiklikleri test etmek için App.tsx dosyasına gelelim ve aşağıdaki gibi değiştirelim:
import * as React from "react";
import { Text } from "react-native";const App: React.FC = () => {
return <Text>Merhaba Dünya</Text>;
};export default App;
printf komutu ile aşağıdaki gibi yapabilirsiniz:
printf 'import * as React from "react";\nimport { Text } from "react-native";\nconst App: React.FC = () => {\n return <Text>Merhaba Dünya</Text>;\n};\nexport default App;\n' > src/App.tsx
Aşağıdaki komutu çalıştırdığımızda artık ekranda uygulamanın ayağa kalktığını görebilirsiniz:
yarn start
Burada metni ekrana basmak için react native’de yer alan <Text>
bileşenini kullandık. Bu bileşen aslında html tarafında bir span veya p üretiyormuş gibi görünse de tarayıcıda inspect ettiğinizde <div>
elemanına dönüştüğünü görebilirsiniz. Bu gibi SEO işlemleri için expo’nun html-elements modülü inceleyebilirsiniz.
Android ve iOS için React Native projesinin oluşturulması
Ctrl+C ile işleyişi durdurarak monorepo içerisine geri dönelim ve mobile projesini oluşturalım.
cd .. && npx react-native init mobile --template react-native-template-typescript@6.2.0 && cd mobile
Çalıştığını ios emülatör’de kontrol edelim:
npx react-native run-ios
Monorepo’nun yapılandırılması
Ctrl+C ile işleyişi durduralım ve monorepo için mobile ve web modüllerini barındıracak packages
dizinini oluşturalım. Ardından web ve mobile dizinlerini packages altına taşıyalım:
cd .. && mkdir packages && mv web packages && mv mobile packages && code .
monorepo dizininde packages.json dosyasının oluşturulması için aşağıdaki şekilde init edelim:
yarn init -y
package.json içeriğine private özelliğini true haline getirelim ve workspaces özelliğini aşağıdaki etkinleştirelim:
sed -i ':a;N;$!ba;s/\n}/,\n "private": true,\n "workspaces": ["packages\/*"]\n}/' package.json
package.json’ın son hali aşağıdaki gibi olacaktır:
{
"name": "monorepo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": ["packages/*"]
}
monorepo ile kütüphanelerin tek bir noktada (root dizininde) tutulacağı için mobile ve web altındaki gereksiz node_modules dizinleri silelim:
rm -rf packages/mobile/node_modules && rm -rf packages/web/node_modules
yarn komutunu çalıştıralım ve ilgili kütüphanelerin monorepo’daki node_modules içerisine eklenmesini sağlayalım:
yarn
common modülünün oluşturulması
common modülü mobile ve web için ortak bileşenleri barındıracak. Bu dizinini oluşturalım ve içerisine giderek init edelim:
cd packages && mkdir common && cd common && yarn init -y
Bunun sonucu packages dizininde aşağıdaki gibi 3 dizin yer alacaktır:
Artık packages.json içeriğini yapılandırmaya geçebiliriz. Buradaki name alanına, @ işareti ile aşağıdaki gibi bir isim verelim. Bu sayede monorepo içerisinde scope oluşturarak kullanabileceğiz. İsim konusunda istediğiniz değeri verebilirsiniz. Örneğin projeniz instagram projesi ise @instagram/common şeklinde bir isim vermeniz daha anlamlı olacaktır:
sed -i 's/common/@monorepo\/common/' package.json
common/packages.json
dosyasının son hali aşağıdaki gibi olacaktır:
{
"name": "@monorepo/common",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
common modülüne react-native’i ekleyelim:
yarn add react-native
typescript ve definition’ları da aşağıdaki gibi devDependencies olarak ekleyelim:
yarn add -D typescript @types/react-native
src
dizini oluşturup içerisine web modülündeki App.tsx
dosyasını kopyalayalım ve bu dosyayı dışarıya sunacağımız için adını index.tsx
haline getirelim:
mkdir src && cp ../web/src/App.tsx ./src/index.tsx
web dizininden tsconfig.json
dosyasını kopyalayalım:
cp ../web/tsconfig.json tsconfig.json
Şimdi modülü dışarıya export edebilmek için tsconfig.json
dosyası üzerinde 5 farklı değişiklik yapalım:
sed -i 's/"allowJs": true,/"allowJs": false,/' tsconfig.json
sed -i 's/"module": "esnext",/"module": "commonjs",/' tsconfig.json
sed -i 's/"isolatedModules": true,/"isolatedModules": false,/' tsconfig.json
sed -i 's/"noEmit": true,/"noEmit": false,/' tsconfig.json
sed -i 's/"jsx": "react"/"jsx": "react",\n "declaration": true,\n "outDir": "dist"/' tsconfig.json
Değişikliklerin kalın harflerle belirtildiği tsconfig.json dosyasının son hali aşağıdaki gibi olacaktır:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"noEmit": false,
"jsx": "react",
"declaration": true,
"outDir": "dist"
},
"include": [
"src"
]
}
Yaptığımız değişiklikleri açıklamak gerekirse:
"allowjs": false
sayesinde sadece typescript olarak geliştirim yapacağız."module": "commonjs"
: typescript’ten javascript’e dönüştürülecek dosyaların her tarayıcı için genel geçer bir versiyonda olmasınıcommonjs
ile belirtiyoruz."isolatedModules": false
: Daha hızlı derleme açısından false yapıyoruz:"noemit": false
: js dosyalarının üretilmesi için false yapıyoruz."declaration": true
:.d.ts
uzantılı tip dosyalarının oluşturulmasını sağlıyoruz."outDir": "dist"
: oluşturulan js dosyalarının dist dizini içerisinde oluşturulmasını sağlıyoruz. Aksi halde src dizini içerisinde oluşturulur.
Projeyi tsc ile transpile etmek için package.json
içerisinde de scripts alanını aşağıdaki gibi ekleyelim ve main alanını da dist
içerisindeki index.js’i gösterecek şekilde ayarlayalım. watch komutu ile değişiklik olduğunda sürekli dinleyerek otomatik build edebiliriz:
{
"name": "@monorepo/common",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "rm -rf dist && tsc",
"watch": "tsc --watch"
},
"license": "MIT",
"dependencies": {
"react": "^16.12.0",
"react-native": "^0.61.5"
},
"devDependencies": {
"@types/react-native": "^0.61.16",
"typescript": "^3.7.5"
}
}
sed komutu ile değiştirmeyi tercih ediyorsanız aşağıdaki gibi kullanabilirsiniz:
sed 's/"main": "index.js",/"main": "dist\/index.js",\n "scripts": {\n "build": "rm -rf dist \&\& tsc",\n "watch": "tsc --watch"\n },/' package.json
Şimdi build ettiğimizde index.ts
ve index.d.ts
dosyaları oluşacaktır:
yarn build
Web ortamının common modülünü kullanacak şekilde ayarlanması
Şimdi web ortamının common modülünü kullanması için web/package.json
içerisideki dependencies kısmına "@monorepo/common": "1.0.0",
ekleyelim:
cd ../web/ && sed -i 's/"dependencies": {/\"dependencies": {\n "@monorepo\/common": "1.0.0",/' package.json
yarn ile common paketimizi bağlayalım:
yarn
App.tsx
dosyasını common modülü içerisinden kullanacağımız için artık buna ihtiyacımız kalmadı. Bu nedenle silebiliriz:
rm ./src/App.tsx
App sınıfının monorepo/common
‘dan import edilmesi içinindex.tsx
dosyası içerisinde import kısmında aşağıdaki gibi bir değişiklik yapalım:
sed -i 's/.\/App/@monorepo\/common/' src/index.tsx
sed komutunun çalıştırılması sonucuindex.tsx
dosyasının son hali aşağıdaki gibi olacaktır:
import { AppRegistry } from "react-native";
import App from "@monorepo/common";AppRegistry.registerComponent("App", () => App);AppRegistry.runApplication("App", {
initialProps: {},
rootTag: document.getElementById("root")
});
Not: Bu anda vscode içerisindeki local ts sunucusu @monorepo/common
’ı göremediği için altını kırmızı ile çizebilir. Bunun için vscode’da Cmd+Shift+P tuşlarına basınca çıkan menüde “Restart TS Server”’ı seçerseniz uyarı giderilecektir.
yarn start
komutunu çalıştırdığınızda site karşınıza gelecektir. Ancak common
modülündeki değişikliklerin geçerli olması için sürekli yarn build etmek gerekiyor. Bunu otomatik hale getirmek için common/package.json dizini altında scripts içerisine "watch”: “tsc --watch",
komutunu eklemiştik. Artık common dizinine giderek bunu kullanabiliriz:
cd ../common && yarn watch
watch komutu buradaki terminal sekmesinde devamlı bir şekilde çalışacaktır. Yeni terminal sekmesi açmak için iTerm2'de üstteki Shell menüsünün altında “Duplicate tab”’a basarak sekmeyi kopyalayalım. web dizini içerisine giderek yarn start
konutunu çalıştıralım:
cd ../web && yarn start
Artık otomatik olarak common modülü içerisinde gerçekleşen tüm değişiklikler derlenerek ekrana yansıyacaktır. common/src/index.tsx
üzerinde değişiklik yaparak tarayıcıda bu değişikliklerin ekranda da yer aldığını görebilirsiniz.
Artık web için common modülünü kullanabiliyoruz. Şimdi kodu mobil taraf için nasıl uyarlayacağımıza değinelim.
Mobil için workspace’in uyarlanması
2 terminal sekmesinde de bir şeyler çalıştığı için tekrar iTerm üzerinde duplicate tab ile sekmeyi kopyalayalım. Ardından mobile
dizinine gidelim ve App.tsx
’i silerek ./App
yerine @monorepo/common
‘dan yüklenmesini sağlayalım:
cd ../mobile/ && rm App.tsx && sed -i "s/'.\/App'/'@monorepo\/common'/" index.js
package.json içerisinde devDependencies kısmına @monorepo/common
ekleyelim.
sed -i 's/"devDependencies": {/\"devDependencies": {\n "@monorepo\/common": "1.0.0",/' package.json
Ayrıca react-native ile ilgili kütüphaneler mobile dizinine eklenmeyip root’taki node_modules’ten hoist edilmesi durumunda hatalar oluşabiliyor. Bunun için aşağıdaki gibi ”workspaces": {“nohoist": ["react-native", "react-native/**"]},
alanını ekleyelim:
sed -i 's/"name": "mobile",/"name": "mobile",\
"workspaces": { "nohoist": [ "react-native", "react-native\/**"] },/' package.json
yarn ile paketlerin yüklenmesini sağlayalım.
yarn
npx react-native run-ios
komutunu çalıştırdığınızda @monorepo/common
modülünü bulamadığı için hata verecektir.
Aslında workspaces kullandığımız için common klasörünün kısayolu projenin root dizinindeki node_modules içerisinde bulunuyor. Fakat metro bundler’da kısayol desteği henüz yer almamakta. Şimdi bu problemin nasıl gidereceğimize değinelim.
Symlinks (kısayol) için wml kullanımı
React Native için metro bundler’da symlinks desteği uzun zamandır yok. Bunun için symlinks yerine wix’in wml aracını kullanabiliriz. wml aracı, watchman’i uygun şekilde çalıştırarak, değişiklikleri alıp ilgili klasöre kopyalamaktadır.
Aşağıdaki gibi yükleyelim:
npm install -g wml
wml aslında kendi içerisinde watchman’i tetiklemektedir. Bu nedenle onu da ayrıca kurmak gerekiyor
brew install watchman
root dizinindeki node_modules içerisinde monorepo altında bulunan common symlink’ini silelim:
rm ../../node_modules/@monorepo/common
wml’e common proje dizinini mobile/node_modules/@monorepo/common içerisine kopyalaması için register edelim:
wml add ../common ../../node_modules/@monorepo/common
Not: Source folder is an npm package, add `node_modules` to ignored folders? mesajı için enter’a basmanız yeterlidir. Devamında wml start komutu ile çalıştırabilirsiniz:
wml start
Bu komut sayesinde common
dizini dinlenecek ve içeriği root dizinindeki node_modules
’a kopyalanacaktır. Packager’daki cache bazen problem yaratabiliyor bu nedenle ilk sefer için aşağıdaki gibi çalıştırmanız iyi olacaktır.
npx react-native run-ios -- --reset-cache
Çalıştığında aşağıdaki gibi görünecektir. Yazının kaymasına aldırmayın, <SafeAreaView> ile sardığımızda düzgün görünecektir.
Eğer windows ortamında çalışıyorsanız ve WSL (Windows Subsystem for Linux)’unuz yoksa Cmder CLI’ı hata verecektir. Hata vermesinin nedeni, bağımlı olduğu watchman’in windows ortamında hata vermesindendir. Bunun için grunt veya gulp gibi araçlar kullanılabilir. grunt konfigürasyonu şu şekilde yapılır:
1. GruntFile.js
dosyası common içerisine konur.
2. İlgili bağımlılıklar yüklenir:
npm install grunt --save-dev
npm install grunt-contrib-watch --save-dev
npm install grunt-sync --save
3. grunt --watch
komutu çalıştırılarak, her dosya değişikliğinde common modülünün mobile/node_modules
altına otomatik olarak kopyalanması sağlanır.
Proje yapımı sürecinde kullanılan tüm komutları aşağıdaki şekilde tek seferde arka arkaya çalıştırabilirsiniz:
Sonuç olarak
Yarn workspaces kullanarak bir react native uygulamasını hem web hem de mobil’de çalışacak şekilde ayarladık. Artık common modülünde yaptığınız değişiklikleri web’de görebilirsiniz ve oluşan projeyi deploy edebilirsiniz. Örneğin daha önce oluşturduğum React Native Instagram projesini uyarlayabilirsiniz.
Bu yazıda anlattıklarım ile ilgili soru ve önerilerinizi alttaki yorumlar kısmından yazabilirsiniz. Vakit ayırdığınız içi teşekkür ederim. Görüşmek üzere..
Kaynaklar:
- https://github.com/necolas/react-native-web
- https://github.com/brunolemos/react-native-web-monorepo
- https://dev.to/brunolemos/tutorial-100-code-sharing-between-ios-android--web-using-react-native-web-andmonorepo-4pej
- https://en.wikipedia.org/wiki/Monorepo
- https://classic.yarnpkg.com/en/docs/workspaces/
- https://www.youtube.com/watch?v=_CBYbEGvxYY