ゼロから店頭まで: レンタカー Web サイトとモバイル アプリを構築する私の旅

Mary-Kate Olsen
リリース: 2024-11-26 00:26:12
オリジナル
615 人が閲覧しました

コンテンツ

  1. はじめに
  2. 技術スタック
  3. 概要
  4. API
  5. フロントエンド
  6. モバイルアプリ
  7. 管理者ダッシュボード
  8. 名所
  9. リソース

ソースコード: https://github.com/aelassas/bookcars

デモ: https://bookcars.dynv6.net:3002

導入

このアイデアは、あらゆる面をコントロールできる、完全にカスタマイズ可能なレンタカー Web サイトとモバイル アプリを境界線なく構築したいという願望から生まれました。

  • UI/UX を所有する: テンプレートの制限と戦うことなく、独自の顧客エクスペリエンスをデザインします
  • バックエンドの制御: 要件に完全に一致するカスタム ビジネス ロジックとデータ構造を実装します
  • マスター DevOps: 優先ツールとワークフローを使用してアプリケーションをデプロイ、拡張、監視します
  • 自由に拡張: プラットフォームの制約や追加料金なしで、新しい機能や統合を追加します

技術スタック

これを可能にした技術スタックは次のとおりです:

  • TypeScript
  • Node.js
  • MongoDB
  • 反応
  • ムイ
  • ネイティブに反応
  • エキスポ
  • ストライプ
  • ドッカー

TypeScript には多くの利点があるため、設計上の重要な決定事項として TypeScript を使用することが決定されました。 TypeScript は強力な型指定、ツール、統合を提供し、その結果、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードが得られます。

私は、強力なレンダリング機能のために React を、柔軟なデータ モデリングのために MongoDB を、そして安全な支払い処理のために Stripe を選びました。

このスタックを選択すると、Web サイトやモバイル アプリを構築するだけでなく、堅牢なオープンソース テクノロジーと成長する開発者コミュニティに支えられ、ニーズに合わせて進化できる基盤に投資することになります。

React は、次の理由から優れた選択肢として際立っています。

  1. コンポーネントベースのアーキテクチャ
    • 複雑な UI をより小さく再利用可能な部分に分割できます
    • コードの保守性が向上し、テストが容易になります
    • コードの編成と再利用性を向上させます
  2. 仮想 DOM パフォーマンス
    • React の仮想 DOM は必要なものだけを効率的に更新します
    • ページの読み込みが速くなり、ユーザー エクスペリエンスが向上します
    • 不必要な再レンダリングを削減します
  3. 豊かなエコシステム
    • 事前に構築されたコンポーネントの膨大なライブラリ
    • 豊富なツール
    • サポートとリソースのための大規模なコミュニティ
  4. 強力な開発者経験
    • 即時フィードバックのためのホットリロード
    • 優れたデバッグ ツール
    • JSX により、UI コードの記述がより直感的になります
  5. 業界サポート
    • Meta (旧 Facebook) によって支援されています
    • 多くの大手企業が使用しています
    • 継続的な開発と改善
  6. 柔軟性
    • 小規模なアプリケーションと大規模なアプリケーションの両方に適しています
    • 既存のプロジェクトに段階的に統合可能
    • 複数のレンダリング戦略 (クライアント側、サーバー側、静的) をサポート

概要

このセクションでは、フロントエンド、管理ダッシュボード、モバイル アプリのメイン ページが表示されます。

フロントエンド

ユーザーはフロントエンドから利用可能な車を検索し、車を選択してチェックアウトできます。

以下はフロントエンドのメイン ページで、ユーザーはここで乗車と降車の場所と時間を選択し、利用可能な車を検索できます。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、ユーザーがレンタカーを選択できるメインページの検索結果です。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、ユーザーがレンタル オプションを設定してチェックアウトできるチェックアウト ページです。ユーザーが登録されていない場合は、チェックアウトと登録を同時に行うことができます。まだ登録していない場合は、パスワードを設定するための確認およびアクティベーション電子メールが届きます。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下はサインインページです。運用環境では、認証 Cookie は httpOnly、署名付き、安全かつ厳密な SameSite です。 これらのオプションは、XSS、CSRF、および MITM 攻撃を防止します。 認証 Cookie は、カスタム ミドルウェアを通じて XST 攻撃からも保護されます。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下はサインアップページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、ユーザーが予約を確認および管理できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、ユーザーが予約の詳細を確認できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下はお問い合わせページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下はレンタカーの場所のページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、顧客が通知を表示および管理できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

他にもページがありますが、これらがフロントエンドのメインページです。

管理者ダッシュボード

ユーザーには 3 つのタイプがあります:

  • 管理者: 管理者ダッシュボードへの完全なアクセス権を持っています。彼は何でもできます。
  • サプライヤー: 管理者ダッシュボードへのアクセスが制限されています。彼が管理できるのは自分の車と予約だけです。
  • ユーザー: フロントエンドとモバイル アプリにのみアクセスできます。彼は管理ダッシュボードにアクセスできません。

プラットフォームは複数のサプライヤーと連携できるように設計されています。各サプライヤーは、管理ダッシュボードから自社の車両と予約を管理できます。このプラットフォームは、1 つのサプライヤーのみと連携することもできます。

管理者ユーザーは、管理者ダッシュボードから、サプライヤー、車両、場所、ユーザー、予約を作成および管理できます。

管理者ユーザーが新しいサプライヤーを作成すると、サプライヤーは管理ダッシュボードにアクセスするためのアカウントを作成するための自動メールを受信し、車両の保有車両と予約を管理できるようになります。

以下は管理者ダッシュボードのサインイン ページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、管理者とサプライヤーが予約を確認および管理できる管理者ダッシュボードのダッシュボード ページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は車両群を表示・管理できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、管理者とサプライヤーが画像と車の情報を提供して新しい車を作成できるページです。車種オプションを無料で付ける場合は、該当車種オプションに0を設定してください。それ以外の場合は、オプションの価格を設定するか、オプションを含めたくない場合は空のままにしておきます。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、管理者とサプライヤーが車を編集できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は、管理者がプラットフォーム ユーザーを管理できるページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

以下は予約を編集するページです。

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

他にもページがありますが、これらが管理者ダッシュボードのメインページです。

API

From Zero to Storefront: My Journey Building a Car Rental Website and Mobile App

API は、管理ダッシュボード、フロントエンド、モバイル アプリに必要なすべての機能を公開します。 API は MVC 設計パターンに従っています。認証にはJWTが使用されます。車、予約、顧客の管理に関連する機能など、認証が必要な機能と、認証されていないユーザー向けの位置情報や利用可能な車の取得など、認証を必要としない機能があります。

  • ./api/src/models/ フォルダーには MongoDB モデルが含まれています。
  • ./api/src/routes/ フォルダーには Express ルートが含まれています。
  • ./api/src/controllers/ フォルダーにはコントローラーが含まれています。
  • ./api/src/middlewares/ フォルダーにはミドルウェアが含まれています。
  • ./api/src/config/env.config.ts には、構成と TypeScript タイプの定義が含まれています。
  • ./api/src/lang/ フォルダーにはローカリゼーションが含まれています。
  • ./api/src/app.ts はルートがロードされるメインサーバーです。
  • ./api/index.ts は API のメイン エントリ ポイントです。

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 * as env from './config/env.config'
import * as databaseHelper from './common/databaseHelper'
import app from './app'
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 supplierRoutes from './routes/supplierRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import carRoutes from './routes/carRoutes'
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('/', supplierRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', carRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

await helper.mkdir(env.CDN_USERS)
await helper.mkdir(env.CDN_TEMP_USERS)
await helper.mkdir(env.CDN_CARS)
await helper.mkdir(env.CDN_TEMP_CARS)
await helper.mkdir(env.CDN_LOCATIONS)
await helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app
ログイン後にコピー
ログイン後にコピー

まず、MongoDB 接続文字列を取得し、MongoDB データベースとの接続を確立します。次に、Express アプリを作成し、cors、compression、helmet、nocache などのミドルウェアを読み込みます。ヘルメットミドルウェアライブラリを利用して、さまざまなセキュリティ対策を設定します。また、supplierRoutes、bookingRoutes、locationRoutes、notificationRoutes、carRoutes、userRoutes など、アプリケーションのさまざまな部分のさまざまなルート ファイルもインポートします。最後に、Express ルートをロードし、アプリをエクスポートします。

API には 8 つのルートがあります。各ルートには、MVC 設計パターンと SOLID 原則に従って独自のコントローラーがあります。以下は主なルートです:

  • userRoutes: ユーザーに関連する REST 機能を提供します
  • supplierRoutes: サプライヤーに関連する REST 機能を提供します
  • countryRoutes: 国に関連する REST 関数を提供します
  • locationRoutes: 場所に関連する REST 関数を提供します
  • carRoutes: 車に関連する REST 機能を提供します
  • bookingRoutes: 予約に関連する REST 関数を提供します
  • notificationRoutes: 通知に関連する REST 関数を提供します
  • StripeRoutes: Stripe ペイメントゲートウェイに関連する REST 機能を提供します

各ルートを一つずつ説明するつもりはありません。たとえば、countryRoutes を取り上げ、それがどのように作成されたかを見てみましょう:

import express from 'express'
import routeNames from '../config/countryRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as countryController from '../controllers/countryController'

const routes = express.Router()

routes.route(routeNames.validate).post(authJwt.verifyToken, countryController.validate)
routes.route(routeNames.create).post(authJwt.verifyToken, countryController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, countryController.update)
routes.route(routeNames.delete).delete(authJwt.verifyToken, countryController.deleteCountry)
routes.route(routeNames.getCountry).get(authJwt.verifyToken, countryController.getCountry)
routes.route(routeNames.getCountries).get(authJwt.verifyToken, countryController.getCountries)
routes.route(routeNames.getCountriesWithLocations).get(countryController.getCountriesWithLocations)
routes.route(routeNames.checkCountry).get(authJwt.verifyToken, countryController.checkCountry)
routes.route(routeNames.getCountryId).get(authJwt.verifyToken, countryController.getCountryId)

export default routes
ログイン後にコピー
ログイン後にコピー

まず、Express Router を作成します。次に、名前、メソッド、ミドルウェア、コントローラーを使用してルートを作成します。

routeNames には countryRoutes ルート名が含まれています:

const routes = {
  validate: '/api/validate-country',
  create: '/api/create-country',
  update: '/api/update-country/:id',
  delete: '/api/delete-country/:id',
  getCountry: '/api/country/:id/:language',
  getCountries: '/api/countries/:page/:size/:language',
  getCountriesWithLocations: '/api/countries-with-locations/:language/:imageRequired/:minLocations',
  checkCountry: '/api/check-country/:id',
  getCountryId: '/api/country-id/:name/:language',
}

export default routes
ログイン後にコピー
ログイン後にコピー

countryController には、国に関する主要なビジネス ロジックが含まれています。コントローラーのソース コードは非常に大きいため、すべてを見ることはできませんが、コントローラー関数の作成を例に説明します。

以下は国別モデルです:

import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const countrySchema = new Schema<env.Country>(
  {
    values: {
      type: [Schema.Types.ObjectId],
      ref: 'LocationValue',
      required: [true, "can't be blank"],
      validate: (value: any): boolean => Array.isArray(value),
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Country',
  },
)

const Country = model<env.Country>('Country', countrySchema)

export default Country
ログイン後にコピー
ログイン後にコピー

以下は env. Country TypeScript タイプです:

export interface Country extends Document {
  values: Types.ObjectId[]
  name?: string
}
ログイン後にコピー

国には複数の価値観があります。言語ごとに 1 つ。デフォルトでは、英語とフランス語がサポートされています。

以下は LocationValue モデルです:

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 * as env from './config/env.config'
import * as databaseHelper from './common/databaseHelper'
import app from './app'
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))
}
ログイン後にコピー
ログイン後にコピー

以下は env.LocationValue TypeScript タイプです:

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 supplierRoutes from './routes/supplierRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import carRoutes from './routes/carRoutes'
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('/', supplierRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', carRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

await helper.mkdir(env.CDN_USERS)
await helper.mkdir(env.CDN_TEMP_USERS)
await helper.mkdir(env.CDN_CARS)
await helper.mkdir(env.CDN_TEMP_CARS)
await helper.mkdir(env.CDN_LOCATIONS)
await helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app
ログイン後にコピー
ログイン後にコピー

LocationValue には言語コード (ISO 639-1) と文字列値があります。

以下はコントローラー関数の作成です:

import express from 'express'
import routeNames from '../config/countryRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as countryController from '../controllers/countryController'

const routes = express.Router()

routes.route(routeNames.validate).post(authJwt.verifyToken, countryController.validate)
routes.route(routeNames.create).post(authJwt.verifyToken, countryController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, countryController.update)
routes.route(routeNames.delete).delete(authJwt.verifyToken, countryController.deleteCountry)
routes.route(routeNames.getCountry).get(authJwt.verifyToken, countryController.getCountry)
routes.route(routeNames.getCountries).get(authJwt.verifyToken, countryController.getCountries)
routes.route(routeNames.getCountriesWithLocations).get(countryController.getCountriesWithLocations)
routes.route(routeNames.checkCountry).get(authJwt.verifyToken, countryController.checkCountry)
routes.route(routeNames.getCountryId).get(authJwt.verifyToken, countryController.getCountryId)

export default routes
ログイン後にコピー
ログイン後にコピー

この関数では、リクエストの本文を取得し、本文で指定された値 (言語ごとに 1 つの値) を反復処理して、LocationValue を作成します。最後に、作成した位置の値に基づいて国を作成します。

フロントエンド

フロントエンドは、Node.js、React、MUI、TypeScript で構築された Web アプリケーションです。フロントエンドから、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。

  • ./frontend/src/assets/ フォルダーには CSS と画像が含まれています。
  • ./frontend/src/pages/ フォルダーには React ページが含まれています。
  • ./frontend/src/components/ フォルダーには React コンポーネントが含まれています。
  • ./frontend/src/services/ には API クライアント サービスが含まれています。
  • ./frontend/src/App.tsx は、ルートを含むメインの React アプリです。
  • ./frontend/src/index.tsx はフロントエンドのメイン エントリ ポイントです。

TypeScript の型定義は、パッケージ ./packages/bookcars-types で定義されます。

App.tsx はメインの反応アプリです:

const routes = {
  validate: '/api/validate-country',
  create: '/api/create-country',
  update: '/api/update-country/:id',
  delete: '/api/delete-country/:id',
  getCountry: '/api/country/:id/:language',
  getCountries: '/api/countries/:page/:size/:language',
  getCountriesWithLocations: '/api/countries-with-locations/:language/:imageRequired/:minLocations',
  checkCountry: '/api/check-country/:id',
  getCountryId: '/api/country-id/:name/:language',
}

export default routes
ログイン後にコピー
ログイン後にコピー

各ルートをロードするために React 遅延読み込みを使用しています。

フロントエンドの各ページについては説明しませんが、必要に応じてソース コードを開いて各ページを確認できます。

モバイルアプリ

このプラットフォームは、Android および iOS 用のネイティブ モバイル アプリを提供します。モバイル アプリは React Native、Expo、TypeScript で構築されています。フロントエンドと同様に、モバイル アプリでも、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。

顧客は、予約がバックエンドから更新されるとプッシュ通知を受け取ります。プッシュ通知は、Node.js、Expo Server SDK、Firebase を使用して構築されています。

  • ./mobile/assets/ フォルダーには画像が含まれています。
  • ./mobile/screens/ フォルダーには、React Native のメイン画面が含まれています。
  • ./mobile/components/ フォルダーには React Native コンポーネントが含まれています。
  • ./mobile/services/ には API クライアント サービスが含まれています。
  • ./mobile/App.tsx はメインの React Native アプリです。

TypeScript の型定義は次の場所で定義されます:

  • ./mobile/types/index.d.ts
  • ./mobile/types/env.d.ts
  • ./mobile/miscellaneous/bookcarsTypes.ts

./mobile/types/ は、次のように ./mobile/tsconfig.json にロードされます。

import { Schema, model } from 'mongoose'
import * as env from '../config/env.config'

const countrySchema = new Schema<env.Country>(
  {
    values: {
      type: [Schema.Types.ObjectId],
      ref: 'LocationValue',
      required: [true, "can't be blank"],
      validate: (value: any): boolean => Array.isArray(value),
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Country',
  },
)

const Country = model<env.Country>('Country', countrySchema)

export default Country
ログイン後にコピー
ログイン後にコピー

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 = 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/bookcars-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>
ログイン後にコピー

以上がゼロから店頭まで: レンタカー Web サイトとモバイル アプリを構築する私の旅の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート