React Native Web nasıl kullanılır? Monorepo kavramı Nedir?

Zafer Ayan
10 min readFeb 21, 2020

--

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.

monorepo’da bir bağımlılık iki modül için de ortak olarak kullanılıyorsa root içine alınır ve symlink aracılığıyla hoisting edilir.

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:

--

--

No responses yet