ソースコード: https://github.com/aelassas/movinin
デモ: https://movinin.dynv6.net:3004
このアイデアは、あらゆる側面をコントロールできる、完全にカスタマイズ可能で運用可能な不動産賃貸 Web サイトとモバイル アプリを境界線なく構築したいという願望から生まれました。
技術要件:
これを可能にした技術スタックは次のとおりです:
TypeScript には多くの利点があるため、設計上の重要な決定事項として TypeScript を使用することが決定されました。 TypeScript は強力な型指定、ツール、統合を提供し、その結果、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードが得られます。
私は、強力なレンダリング機能のために React を、柔軟なデータ モデリングのために MongoDB を、そして安全な支払い処理のために Stripe を選びました。
このスタックを選択すると、Web サイトやモバイル アプリを構築するだけでなく、堅牢なオープンソース テクノロジーと成長する開発者コミュニティに支えられ、ニーズに合わせて進化できる基盤に投資することになります。
React は、次の理由から優れた選択肢として際立っています。
このセクションでは、フロントエンド、管理ダッシュボード、モバイル アプリのメイン ページが表示されます。
顧客はフロントエンドから利用可能な物件を検索し、物件を選択してチェックアウトできます。
以下はフロントエンドのメイン ページで、顧客はここで場所と時間を指定し、利用可能なプロパティを検索できます。
以下は、お客様が賃貸物件を選択できるメインページの検索結果です。
お客様が物件の詳細を閲覧できるページは以下の通りです:
以下は物件の画像です:
以下は、顧客がレンタル オプションを設定してチェックアウトできるチェックアウト ページです。顧客が登録されていない場合は、チェックアウトと登録を同時に行うことができます。まだ登録していない場合は、パスワードを設定するための確認およびアクティベーション電子メールが送信されます。
以下はサインインページです。運用環境では、認証 Cookie は httpOnly、署名付き、安全かつ厳密な SameSite です。これらのオプションは、XSS、CSRF、および MITM 攻撃を防止します。認証 Cookie は、カスタム ミドルウェアを通じて XST 攻撃からも保護されます。
以下はサインアップページです。
以下は、顧客が予約を確認および管理できるページです。
以下は、お客様が予約の詳細を確認できるページです。
以下は、顧客が通知を確認できるページです。
以下は、お客様が設定を管理できるページです。
以下は、顧客がパスワードを変更できるページです。
それだけです。それがフロントエンドのメインページです。
3 つのタイプのユーザー:
このプラットフォームは複数の代理店と連携できるように設計されています。各代理店は、管理ダッシュボードからその施設、顧客、予約を管理できます。このプラットフォームは、1 つの代理店のみと連携することもできます。
管理者はバックエンドから、代理店、施設、場所、顧客、予約を作成および管理できます。
新しい代理店が作成されると、施設、顧客、予約を管理できるように、管理ダッシュボードにアクセスするためのアカウントを作成するよう求めるメールが届きます。
以下は管理者ダッシュボードのサインイン ページです。
以下は、管理者や代理店が予約を確認および管理できるダッシュボード ページです。
予約のステータスが変更された場合、関連する顧客に通知とメールが届きます。
以下はプロパティが表示され、管理できるページです。
以下は、管理者や代理店が画像や物件情報を提供して新しい物件を作成できるページです。無料でキャンセルする場合は、0 に設定します。それ以外の場合は、オプションの価格を設定するか、オプションを含めたくない場合は空のままにしてください。
以下は、管理者と代理店がプロパティを編集できるページです。
以下は、管理者が顧客を管理できるページです。
以下は、代理店が管理ダッシュボードから予約を作成したい場合に予約を作成するページです。それ以外の場合、フロントエンドまたはモバイル アプリからチェックアウト プロセスが完了すると、予約は自動的に作成されます。
以下は予約を編集するページです。
以下は代理店を管理するページです。
以下は新しい代理店を作成するページです。
以下は代理店を編集するページです。
以下は代理店の物件を表示するページです。
以下は顧客の予約を確認するページです。
以下は、管理者と代理店が設定を管理できるページです。
他にもページがありますが、これらが管理者ダッシュボードのメインページです。
それだけです。これが管理者ダッシュボードのメイン ページです。
Android アプリは任意の Android デバイスにインストールできます。
カメラ アプリを開き、このコードをポイントします。次に、表示される通知をタップします。
Android 8.0 (API レベル 26) 以降を実行しているデバイスでは、[不明なアプリのインストール] システム設定画面に移動して、特定の場所 (つまり、アプリのダウンロード元の Web ブラウザー) からのアプリのインストールを有効にする必要があります。 .
Android 7.1.1 (API レベル 25) 以下を実行しているデバイスでは、[設定] > [提供元不明のシステム] 設定を有効にする必要があります。デバイスのセキュリティ。
APK を直接ダウンロードして Android デバイスにインストールすることで、Android アプリをインストールすることもできます。
API は、管理ダッシュボード、フロントエンド、モバイル アプリに必要なすべての機能を公開します。 API は MVC 設計パターンに従っています。認証にはJWTが使用されます。プロパティ、予約、顧客の管理に関連する機能など、認証が必要な機能と、認証されていないユーザーが場所や利用可能なプロパティを取得するなど、認証を必要としない機能があります。
index.ts は API のメイン エントリ ポイントです:
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
これは、Node.js と Express を使用してサーバーを起動する TypeScript ファイルです。 dotenv、process、fs、http、https、mongoose、app などのいくつかのモジュールをインポートします。次に、HTTPS 環境変数が true に設定されているかどうかを確認し、true に設定されている場合は、https モジュールと提供された秘密キーと証明書を使用して HTTPS サーバーを作成します。それ以外の場合は、http モジュールを使用して HTTP サーバーを作成します。サーバーは、PORT 環境変数で指定されたポートで待機します。
close 関数は、終了信号を受信したときにサーバーを正常に停止するように定義されています。サーバーと MongoDB 接続を閉じ、ステータス コード 0 でプロセスを終了します。最後に、プロセスが SIGINT、SIGTERM、または SIGQUIT シグナルを受信したときに呼び出される close 関数を登録します。
app.ts は API のメイン エントリ ポイントです:
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import agencyRoutes from './routes/agencyRoutes' import bookingRoutes from './routes/bookingRoutes' import locationRoutes from './routes/locationRoutes' import notificationRoutes from './routes/notificationRoutes' import propertyRoutes from './routes/propertyRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' import * as helper from './common/helper' const app = express() app.use(helmet.contentSecurityPolicy()) app.use(helmet.dnsPrefetchControl()) app.use(helmet.crossOriginEmbedderPolicy()) app.use(helmet.frameguard()) app.use(helmet.hidePoweredBy()) app.use(helmet.hsts()) app.use(helmet.ieNoOpen()) app.use(helmet.noSniff()) app.use(helmet.permittedCrossDomainPolicies()) app.use(helmet.referrerPolicy()) app.use(helmet.xssFilter()) app.use(helmet.originAgentCluster()) app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' })) app.use(helmet.crossOriginOpenerPolicy()) app.use(nocache()) app.use(compression({ threshold: 0 })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.json({ limit: '50mb' })) app.use(cors()) app.options('*', cors()) app.use(cookieParser(env.COOKIE_SECRET)) app.use(allowedMethods) app.use('/', agencyRoutes) app.use('/', bookingRoutes) app.use('/', locationRoutes) app.use('/', notificationRoutes) app.use('/', propertyRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) i18n.locale = env.DEFAULT_LANGUAGE helper.mkdir(env.CDN_USERS) helper.mkdir(env.CDN_TEMP_USERS) helper.mkdir(env.CDN_PROPERTIES) helper.mkdir(env.CDN_TEMP_PROPERTIES) helper.mkdir(env.CDN_LOCATIONS) helper.mkdir(env.CDN_TEMP_LOCATIONS) export default app
まず、MongoDB 接続文字列を取得し、MongoDB データベースとの接続を確立します。次に、Express アプリを作成し、cors、compression、helmet、nocache などのミドルウェアを読み込みます。ヘルメットミドルウェアライブラリを利用して、さまざまなセキュリティ対策を設定しています。また、supplierRoutes、bookingRoutes、locationRoutes、notificationRoutes、propertyRoutes、userRoutes など、アプリケーションのさまざまな部分のさまざまなルート ファイルもインポートします。最後に、Express ルートをロードし、アプリをエクスポートします。
API には 8 つのルートがあります。各ルートには、MVC 設計パターンと SOLID 原則に従って独自のコントローラーがあります。以下は主なルートです:
各ルートを一つずつ説明するつもりはありません。たとえば、propertyRoutes を取り上げ、それがどのように作成されたかを見てみましょう。ソース コードを参照して、すべてのルートを確認できます。
ここに propertyRoutes.ts があります:
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
まず、Express Router を作成します。次に、名前、メソッド、ミドルウェア、コントローラーを使用してルートを作成します。
routeNames には propertyRoutes ルート名が含まれています:
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import agencyRoutes from './routes/agencyRoutes' import bookingRoutes from './routes/bookingRoutes' import locationRoutes from './routes/locationRoutes' import notificationRoutes from './routes/notificationRoutes' import propertyRoutes from './routes/propertyRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' import * as helper from './common/helper' const app = express() app.use(helmet.contentSecurityPolicy()) app.use(helmet.dnsPrefetchControl()) app.use(helmet.crossOriginEmbedderPolicy()) app.use(helmet.frameguard()) app.use(helmet.hidePoweredBy()) app.use(helmet.hsts()) app.use(helmet.ieNoOpen()) app.use(helmet.noSniff()) app.use(helmet.permittedCrossDomainPolicies()) app.use(helmet.referrerPolicy()) app.use(helmet.xssFilter()) app.use(helmet.originAgentCluster()) app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' })) app.use(helmet.crossOriginOpenerPolicy()) app.use(nocache()) app.use(compression({ threshold: 0 })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.json({ limit: '50mb' })) app.use(cors()) app.options('*', cors()) app.use(cookieParser(env.COOKIE_SECRET)) app.use(allowedMethods) app.use('/', agencyRoutes) app.use('/', bookingRoutes) app.use('/', locationRoutes) app.use('/', notificationRoutes) app.use('/', propertyRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) i18n.locale = env.DEFAULT_LANGUAGE helper.mkdir(env.CDN_USERS) helper.mkdir(env.CDN_TEMP_USERS) helper.mkdir(env.CDN_PROPERTIES) helper.mkdir(env.CDN_TEMP_PROPERTIES) helper.mkdir(env.CDN_LOCATIONS) helper.mkdir(env.CDN_TEMP_LOCATIONS) export default app
propertyController には、場所に関する主要なビジネス ロジックが含まれています。コントローラーのソース コードは非常に大きいため、すべてを見ることはできませんが、コントローラー関数の作成を例に説明します。
以下はプロパティ モデルです:
import express from 'express' import multer from 'multer' import routeNames from '../config/propertyRoutes.config' import authJwt from '../middlewares/authJwt' import * as propertyController from '../controllers/propertyController' const routes = express.Router() routes.route(routeNames.create).post(authJwt.verifyToken, propertyController.create) routes.route(routeNames.update).put(authJwt.verifyToken, propertyController.update) routes.route(routeNames.checkProperty).get(authJwt.verifyToken, propertyController.checkProperty) routes.route(routeNames.delete).delete(authJwt.verifyToken, propertyController.deleteProperty) routes.route(routeNames.uploadImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], propertyController.uploadImage) routes.route(routeNames.deleteImage).post(authJwt.verifyToken, propertyController.deleteImage) routes.route(routeNames.deleteTempImage).post(authJwt.verifyToken, propertyController.deleteTempImage) routes.route(routeNames.getProperty).get(propertyController.getProperty) routes.route(routeNames.getProperties).post(authJwt.verifyToken, propertyController.getProperties) routes.route(routeNames.getBookingProperties).post(authJwt.verifyToken, propertyController.getBookingProperties) routes.route(routeNames.getFrontendProperties).post(propertyController.getFrontendProperties) export default routes
以下はプロパティタイプです:
const routes = { create: '/api/create-property', update: '/api/update-property', delete: '/api/delete-property/:id', uploadImage: '/api/upload-property-image', deleteTempImage: '/api/delete-temp-property-image/:fileName', deleteImage: '/api/delete-property-image/:property/:image', getProperty: '/api/property/:id/:language', getProperties: '/api/properties/:page/:size', getBookingProperties: '/api/booking-properties/:page/:size', getFrontendProperties: '/api/frontend-properties/:page/:size', checkProperty: '/api/check-property/:id', } export default routes
プロパティは次のもので構成されます:
以下はコントローラー関数の作成です:
import { Schema, model } from 'mongoose' import * as movininTypes from ':movinin-types' import * as env from '../config/env.config' const propertySchema = new Schema<env.Property>( { name: { type: String, required: [true, "can't be blank"], }, type: { type: String, enum: [ movininTypes.PropertyType.House, movininTypes.PropertyType.Apartment, movininTypes.PropertyType.Townhouse, movininTypes.PropertyType.Plot, movininTypes.PropertyType.Farm, movininTypes.PropertyType.Commercial, movininTypes.PropertyType.Industrial, ], required: [true, "can't be blank"], }, agency: { type: Schema.Types.ObjectId, required: [true, "can't be blank"], ref: 'User', index: true, }, description: { type: String, required: [true, "can't be blank"], }, available: { type: Boolean, default: true, }, image: { type: String, }, images: { type: [String], }, bedrooms: { type: Number, required: [true, "can't be blank"], validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, bathrooms: { type: Number, required: [true, "can't be blank"], validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, kitchens: { type: Number, default: 1, validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, parkingSpaces: { type: Number, default: 0, validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, size: { type: Number, }, petsAllowed: { type: Boolean, required: [true, "can't be blank"], }, furnished: { type: Boolean, required: [true, "can't be blank"], }, minimumAge: { type: Number, required: [true, "can't be blank"], min: env.MINIMUM_AGE, max: 99, }, location: { type: Schema.Types.ObjectId, ref: 'Location', required: [true, "can't be blank"], }, address: { type: String, }, price: { type: Number, required: [true, "can't be blank"], }, hidden: { type: Boolean, default: false, }, cancellation: { type: Number, default: 0, }, aircon: { type: Boolean, default: false, }, rentalTerm: { type: String, enum: [ movininTypes.RentalTerm.Monthly, movininTypes.RentalTerm.Weekly, movininTypes.RentalTerm.Daily, movininTypes.RentalTerm.Yearly, ], required: [true, "can't be blank"], }, }, { timestamps: true, strict: true, collection: 'Property', }, ) const Property = model<env.Property>('Property', propertySchema) export default Property
フロントエンドは、Node.js、React、MUI、TypeScript で構築された Web アプリケーションです。フロントエンドから、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。
TypeScript の型定義は、パッケージ ./packages/movinin-types で定義されます。
App.tsx はメインの反応アプリです:
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
各ルートをロードするために React 遅延読み込みを使用しています。
フロントエンドの各ページについては説明しませんが、ソース コードを参照して各ページを確認することができます。
このプラットフォームは、Android および iOS 用のネイティブ モバイル アプリを提供します。モバイル アプリは React Native、Expo、TypeScript で構築されています。フロントエンドと同様に、モバイル アプリでも、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。
顧客は、予約がバックエンドから更新されるとプッシュ通知を受け取ります。プッシュ通知は、Node.js、Expo Server SDK、Firebase を使用して構築されています。
TypeScript の型定義は次の場所で定義されます:
./mobile/types/ は、次のように ./mobile/tsconfig.json にロードされます。
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import agencyRoutes from './routes/agencyRoutes' import bookingRoutes from './routes/bookingRoutes' import locationRoutes from './routes/locationRoutes' import notificationRoutes from './routes/notificationRoutes' import propertyRoutes from './routes/propertyRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' import * as helper from './common/helper' const app = express() app.use(helmet.contentSecurityPolicy()) app.use(helmet.dnsPrefetchControl()) app.use(helmet.crossOriginEmbedderPolicy()) app.use(helmet.frameguard()) app.use(helmet.hidePoweredBy()) app.use(helmet.hsts()) app.use(helmet.ieNoOpen()) app.use(helmet.noSniff()) app.use(helmet.permittedCrossDomainPolicies()) app.use(helmet.referrerPolicy()) app.use(helmet.xssFilter()) app.use(helmet.originAgentCluster()) app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' })) app.use(helmet.crossOriginOpenerPolicy()) app.use(nocache()) app.use(compression({ threshold: 0 })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.json({ limit: '50mb' })) app.use(cors()) app.options('*', cors()) app.use(cookieParser(env.COOKIE_SECRET)) app.use(allowedMethods) app.use('/', agencyRoutes) app.use('/', bookingRoutes) app.use('/', locationRoutes) app.use('/', notificationRoutes) app.use('/', propertyRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) i18n.locale = env.DEFAULT_LANGUAGE helper.mkdir(env.CDN_USERS) helper.mkdir(env.CDN_TEMP_USERS) helper.mkdir(env.CDN_PROPERTIES) helper.mkdir(env.CDN_TEMP_PROPERTIES) helper.mkdir(env.CDN_LOCATIONS) helper.mkdir(env.CDN_TEMP_LOCATIONS) export default app
App.tsx は React Native アプリのメイン エントリ ポイントです:
「react-native-gesture-handler」をインポートします import React, { useCallback, useEffect, useRef, useState } from 'react' import { RootSiblingParent } from 'react-native-root-siblings' import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native' import { StatusBar as ExpoStatusBar } from 'expo-status-bar' import { SafeAreaProvider } から 'react-native-safe-area-context' import { Provider } from 'react-native-paper' import * as SplashScreen from 'expo-splash-screen' import * 'expo-notifications' からの通知として import { StripeProvider } から '@ストライプ/ストライプ-反応-ネイティブ' './components/DrawerNavigator' から DrawerNavigator をインポートします import * './common/helper' からヘルパーとして import * as NoticeService from './services/NotificationService' import * as UserService from './services/UserService' './context/GlobalContext' から { GlobalProvider } をインポートします import * as env from './config/env.config' Notices.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true、 shouldPlaySound: true、 shouldSetBadge: true、 })、 }) // // アプリコンポーネントの宣言前にネイティブのスプラッシュ画面が自動非表示になるのを防ぎます // SplashScreen.preventAutoHideAsync() .then((result) => console.log(`SplashScreen.preventAutoHideAsync() が成功しました: ${result}`)) .catch(console.warn) // エラーを明示的にキャッチして検査することは良いことです const App = () => { const [appIsReady, setAppIsReady] = useState(false) const responseListener = useRef<Notifications.Subscription>() const NavigationRef = useRef<navigationcontainerref>>(null) useEffect(() => { const register = async () => { const loggedIn = await UserService.loggedIn() if (ログイン) { const currentUser = await UserService.getCurrentUser() if (currentUser?._id) { await helper.registerPushToken(currentUser._id) } それ以外 { helper.error() } } } // // プッシュ通知トークンを登録する // 登録する() // // このリスナーは、ユーザーが通知をタップするか通知を操作するたびに起動されます (アプリがフォアグラウンド、バックグラウンド、または強制終了されたときに機能します) // responseListener.current = Notices.addNotificationResponseReceivedListener(async (応答) => { 試す { if (navigationRef.current) { const { データ } = 応答.通知.リクエスト.コンテンツ if (data.booking) { if (data.user && data.notification) { await NoticeService.markAsRead(data.user, [data.notification]) } NavigationRef.current.navigate('予約', { id: data.booking }) } それ以外 { NavigationRef.current.navigate('通知', {}) } } } キャッチ (エラー) { helper.error(エラー、偽) } }) return() => { Notices.removeNotificationSubscription(responseListener.current!) } }、[]) setTimeout(() => { setAppIsReady(true) }、500) const onReady = useCallback(async () => { if (appIsReady) { // // これは、スプラッシュ スクリーンをすぐに非表示にするように指示します。これを後で呼び出すと // `setAppIsReady` の場合、アプリの起動中に空白の画面が表示される場合があります。 // 初期状態をロードし、最初のピクセルをレンダリングします。そこで代わりに、 // ルート ビューがすでに存在していることがわかったら、スプラッシュ スクリーンを非表示にします。 // レイアウトを実行しました。 // SplashScreen.hideAsync() を待つ } }, [appIsReady]) if (!appIsReady) { nullを返す } 戻る ( <グローバルプロバイダー> <セーフエリアプロバイダー> <プロバイダ> <StripeProvider publicableKey={env.STRIPE_PUBLISHABLE_KEY} MerchantIdentifier={env.STRIPE_MERCHANT_IDENTIFIER}> <RootSiblingParent> <NavigationContainer ref={navigationRef} onReady={onReady}> <expostatusbar> <p>モバイル アプリの各画面については説明しませんが、ソース コードを参照して各画面を確認することができます。</p> <h2> 管理者ダッシュボード </h2> <p>管理者ダッシュボードは、Node.js、React、MUI、TypeScript で構築された Web アプリケーションです。管理者はバックエンドから、サプライヤー、車両、場所、顧客、予約を作成および管理できます。新しいサプライヤーがバックエンドから作成されると、管理ダッシュボードにアクセスして車両の保有車両と予約を管理するためにアカウントを作成するよう求めるメールが送信されます。</p> <ul> <li>./backend/assets/ フォルダーには CSS と画像が含まれています。</li> <li>./backend/pages/ フォルダーには React ページが含まれています。</li> <li>./backend/components/ フォルダーには React コンポーネントが含まれています。</li> <li>./backend/services/ には API クライアント サービスが含まれています。</li> <li>./backend/App.tsx は、ルートを含むメインの React アプリです。</li> <li>./backend/index.tsx は、管理ダッシュボードのメイン エントリ ポイントです。</li> </ul> <p>TypeScript の型定義は、パッケージ ./packages/movinin-types で定義されます。</p> <p>管理ダッシュボードの App.tsx は、フロントエンドの App.tsx と同様のロジックに従います。</p> <p>管理者ダッシュボードの各ページについては説明しませんが、ソース コードを参照して各ページを確認することができます。</p> <h2> 興味のあるスポット </h2> <p>React Native と Expo を使用してモバイル アプリを構築するのは非常に簡単です。 Expo では、React Native を使用したモバイル開発が非常に簡単になります。</p> <p>バックエンド、フロントエンド、モバイル開発に同じ言語 (TypeScript) を使用すると、非常に便利です。</p> <p>TypeScript は非常に興味深い言語であり、多くの利点があります。 JavaScript に静的型付けを追加することで、多くのバグを回避し、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードを生成できます。</p> <p>それだけです!この記事を楽しんで読んでいただければ幸いです。</p> <h2> リソース </h2> <ol> <li>概要</li> <li>建築</li> <li>インストール中 (セルフホスト型)</li> <li>インストール(VPS)</li> <li> インストール(Docker) <ol> <li>Docker イメージ</li> <li>SSL</li> </ol> </li> <li>ストライプのセットアップ</li> <li>モバイルアプリを構築</li> <li> デモデータベース <ol> <li>Windows、Linux、macOS</li> <li>ドッカー</li> </ol> </li> <li>ソースから実行</li> <li> モバイルアプリを実行する <ol> <li>前提条件</li> <li>手順</li> <li>プッシュ通知</li> </ol> </li> <li>通貨を変更</li> <li>新しい言語を追加</li> <li>単体テストとカバレッジ</li> <li>ログ</li> </ol> </expostatusbar></navigationcontainerref>
以上がゼロからヒーローへ: 不動産賃貸ウェブサイトとモバイル アプリを構築する私の旅の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。