搜索
首页web前端js教程从零到店面:我的房产租赁平台搭建之旅

内容

  1. 简介
  2. 技术堆栈
  3. 快速概述
  4. API
  5. 前端
  6. 移动应用程序
  7. 管理仪表板
  8. 兴趣点
  9. 资源

源代码:https://github.com/aelassas/movinin

演示:https://movinin.dynv6.net:3004

介绍

这个想法源于建立无边界的愿望 - 一个完全可定制和可操作的房地产租赁平台,其中每个方面都在您的控制之下:

  • 拥有 UI/UX:设计独特的客户体验,而无需克服模板限制
  • 控制后端:实现完美匹配需求的自定义业务逻辑和数据结构
  • 掌握 DevOps:使用首选工具和工作流程部署、扩展和监控应用程序
  • 自由扩展:添加新功能和集成,无需平台限制或额外费用

技术堆栈

这是使其成为可能的技术堆栈:

  • 打字稿
  • Node.js
  • MongoDB
  • 反应
  • MUI
  • 世博会
  • 条纹
  • 码头工人

由于 TypeScript 具有众多优点,因此做出了使用 TypeScript 的关键设计决定。 TypeScript 提供强大的类型、工具和集成,从而产生高质量、可扩展、更具可读性和可维护性的代码,并且易于调试和测试。

我选择React是因为它强大的渲染能力,MongoDB是为了灵活的数据建模,而Stripe是为了安全的支付处理。

通过选择此堆栈,您不仅仅是在构建网站和移动应用程序 - 您正在投资一个可以根据您的需求不断发展的基础,并得到强大的开源技术和不断发展的开发者社区的支持。

快速概览

在本部分中,您将看到前端、管理仪表板和移动应用程序的主页。

前端

在前端,客户可以搜索可用房产、选择房产并结账。

下面是前端的主页,客户可以在其中输入位置点和时间,并搜索可用的属性。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是主页的搜索结果,客户可以在其中选择出租房产。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是客户可以查看房产详情的页面:

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是该物业的图片视图:

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是结帐页面,客户可以在其中设置租赁选项和结帐。如果顾客未注册,可以同时结账和注册。如果他尚未注册,他将收到一封确认和激活电子邮件以设置密码。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是登录页面。在生产中,身份验证 cookie 是 httpOnly、签名的、安全且严格的 sameSite。这些选项可防止 XSS、CSRF 和 MITM 攻击。身份验证 cookie 也可以通过自定义中间件免受 XST 攻击。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是注册页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是客户可以查看和管理他的预订的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是客户可以查看预订详细信息的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是客户可以看到他的通知的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是客户可以管理其设置的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是客户可以更改密码的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

就是这样。这是前端的主要页面。

管理仪表板

三类用户:

  • 管理员:他们拥有对管理仪表板的完全访问权限。他们什么都能做。
  • 机构:他们对管理仪表板的访问权限有限。他们只能管理自己的财产、预订和客户。
  • 客户:他们只能访问前端和移动应用程序。他们无法访问管理仪表板。

该平台旨在与多个机构合作。每个机构都可以通过管理仪表板管理其财产、客户和预订。该平台也可以只与一个机构合作。

管理员可以从后端创建和管理代理机构、酒店、地点、客户和预订。

创建新代理机构时,他们会收到一封电子邮件,提示他们创建帐户以访问管理仪表板,以便他们可以管理其财产、客户和预订。

下面是管理仪表板的登录页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是仪表板页面,管理员和代理机构可以在其中查看和管理预订。

From Zero to Storefront: My Journey Building a Property Rental Platform

如果预订状态发生变化,相关客户将收到通知和电子邮件。

下面是显示和管理属性的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是管理员和机构可以通过提供图像和房产信息来创建新房产的页面。如需免费取消,请将其设置为 0。否则,请设置该选项的价格,或者如果您不想包含它,则将其留空。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是管理员和机构可以编辑属性的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是管理员可以管理客户的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

如果代理机构想要从管理仪表板创建预订,下面是创建预订的页面。否则,当从前端或移动应用程序完成结帐过程时,会自动创建预订。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是编辑预订的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是管理代理机构的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是创建新代理机构的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是编辑机构的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是查看代理机构属性的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

以下是查看客户预订的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

下面是管理员和机构可以管理其设置的页面。

From Zero to Storefront: My Journey Building a Property Rental Platform

还有其他页面,但这些是管理仪表板的主页。

就是这样。这是管理仪表板的主要页面。

应用程序编程接口

From Zero to Storefront: My Journey Building a Property Rental Platform

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 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,如果是,则使用 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 原则。主要路线如下:

  • userRoutes:提供与用户相关的REST功能
  • agencyRoutes:提供与机构相关的REST功能
  • countryRoutes:提供与国家相关的REST功能
  • locationRoutes:提供与位置相关的REST函数
  • propertyRoutes:提供与属性相关的REST函数
  • bookingRoutes:提供与预订相关的REST功能
  • notificationRoutes:提供与通知相关的REST功能
  • stripeRoutes:提供与Stripe支付网关相关的REST功能

我们不会一一解释每条路线。例如,我们将以 propertyRoutes 为例,看看它是如何制作的。您可以浏览源代码并查看所有路由。

这是 propertyRoutes.ts:

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

首先,我们创建一个 Express Router。然后,我们使用名称、方法、中间件和控制器创建路由。

routeNames 包含 propertyRoutes 路由名称:

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

propertyController 包含有关位置的主要业务逻辑。我们不会看到控制器的所有源代码,因为它相当大,但我们将以创建控制器函数为例。

以下是房产模型:

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
</env.property></env.property>

以下是房产类型:

export interface Property extends Document {
  name: string
  type: movininTypes.PropertyType
  agency: Types.ObjectId
  description: string
  image: string
  images?: string[]
  bedrooms: number
  bathrooms: number
  kitchens?: number
  parkingSpaces?: number,
  size?: number
  petsAllowed: boolean
  furnished: boolean
  minimumAge: number
  location: Types.ObjectId
  address?: string
  price: number
  hidden?: boolean
  cancellation?: number
  aircon?: boolean
  available?: boolean
  rentalTerm: movininTypes.RentalTerm
}

属性由以下部分组成:

  • 名字
  • A 类型(公寓、商业、农场、住宅、工业、地块、联排别墅)
  • 创建它的机构的参考
  • 描述
  • 主图
  • 其他图片
  • 卧室数量
  • 浴室数量
  • 厨房数量
  • 停车位数量
  • A 尺寸
  • 租赁最低年龄
  • 地点
  • 地址(可选)
  • 价格
  • 租赁期限(每月、每周、每日、每年)
  • 取消价格(设置为0免费包含,不想包含则留空,或者设置取消价格)
  • 表示是否允许携带宠物的标志
  • 指示房产是否配备家具的标志
  • 指示属性是否隐藏的标志
  • 指示空调是否可用的标志
  • 指示该房产是否可供出租的标志

下面是创建控制器函数:

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、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/movinin-types 中定义。

App.tsx 是主要的 React 应用程序:

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

我们使用 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/movininTypes.ts

./mobile/types/ 加载到 ./mobile/tsconfig.json 中,如下所示:

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))
}

App.tsx 是 React Native 应用程序的主要入口点:

导入'react-native-gesture-handler'
从 'react' 导入 React, { useCallback, useEffect, useRef, useState }
从 'react-native-root-siblings' 导入 { RootSiblingParent }
从'@react-navigation/native'导入{NavigationContainer,NavigationContainerRef}
从“expo-status-bar”导入 { StatusBar as ExpoStatusBar }
从 'react-native-safe-area-context' 导入 { SafeAreaProvider }
从“react-native-paper”导入{Provider}
从“expo-splash-screen”导入 * as SplashScreen
导入 * 作为来自“expo-notifications”的通知
从 '@stripe/stripe-react-native' 导入 { StripeProvider }
从 './components/DrawerNavigator' 导入 DrawerNavigator
从 './common/helper' 导入 * 作为助手
从'./services/NotificationService'导入*作为NotificationService
从 './services/UserService' 导入 * 作为 UserService
从 './context/GlobalContext' 导入 { GlobalProvider }
从 './config/env.config' 导入 * 作为 env

通知.setNotificationHandler({
  handleNotification: async() =>; ({
    应该显示警报:真,
    应该播放声音:真,
    应该设置徽章:真,
  }),
})

//
// 防止本机启动画面在应用程序组件声明之前自动隐藏
//
SplashScreen.preventAutoHideAsync()
  .then((结果) => 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 寄存器 = async() => {
      const LoggedIn = 等待 UserService.loggedIn()
      如果(登录){
        const currentUser = 等待 UserService.getCurrentUser()
        if (当前用户?._id) {
          等待 helper.registerPushToken(currentUser._id)
        } 别的 {
          helper.error()
        }
      }
    }

    //
    // 注册推送通知令牌
    //
    登记()

    //
    // 每当用户点击通知或与通知交互时就会触发此侦听器(当应用程序处于前台、后台或终止时有效)
    //
    responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
      尝试 {
        如果(navigationRef.current){
          const { 数据 } = 响应.通知.请求.内容

          如果(数据.预订){
            if (data.user && data.notification) {
              等待NotificationService.markAsRead(data.user, [data.notification])
            }
            navigationRef.current.navigate('预订', { id: data.booking })
          } 别的 {
            navigationRef.current.navigate('通知', {})
          }
        }
      } 捕获(错误){
        helper.error(错误,错误)
      }
    })

    返回() => {
      Notifications.removeNotificationSubscription(responseListener.current!)
    }
  }, [])

  setTimeout(() => {
    设置应用程序已就绪(true)
  }, 500)

  const onReady = useCallback(async () => {
    如果(应用程序已就绪){
      //
      // 这告诉启动屏幕立即隐藏!如果我们之后调用这个
      // `setAppIsReady`,那么当应用程序运行时我们可能会看到一个空白屏幕
      // 加载其初始状态并渲染其第一个像素。所以相反,
      // 一旦我们知道根视图已经隐藏了启动屏幕
      // 执行布局。
      //
      等待 SplashScreen.hideAsync()
    }
  }, [应用程序已就绪])

  如果(!appIsReady){
    返回空值
  }

  返回 (
    
      
        
          <stripeproviderpublishablekey>;
            <rootsiblingparent>
              <navigationcontainer ref="{navigationRef}" onready="{onReady}">
                



<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>


          

            
        </navigationcontainer></rootsiblingparent></stripeproviderpublishablekey></navigationcontainerref></notifications.subscription>

以上是从零到店面:我的房产租赁平台搭建之旅的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
JavaScript数据类型:浏览器和nodejs之间是否有区别?JavaScript数据类型:浏览器和nodejs之间是否有区别?May 14, 2025 am 12:15 AM

JavaScript核心数据类型在浏览器和Node.js中一致,但处理方式和额外类型有所不同。1)全局对象在浏览器中为window,在Node.js中为global。2)Node.js独有Buffer对象,用于处理二进制数据。3)性能和时间处理在两者间也有差异,需根据环境调整代码。

JavaScript评论:使用//和 / * * / * / * /JavaScript评论:使用//和 / * * / * / * /May 13, 2025 pm 03:49 PM

JavaScriptusestwotypesofcomments:single-line(//)andmulti-line(//).1)Use//forquicknotesorsingle-lineexplanations.2)Use//forlongerexplanationsorcommentingoutblocksofcode.Commentsshouldexplainthe'why',notthe'what',andbeplacedabovetherelevantcodeforclari

Python vs. JavaScript:开发人员的比较分析Python vs. JavaScript:开发人员的比较分析May 09, 2025 am 12:22 AM

Python和JavaScript的主要区别在于类型系统和应用场景。1.Python使用动态类型,适合科学计算和数据分析。2.JavaScript采用弱类型,广泛用于前端和全栈开发。两者在异步编程和性能优化上各有优势,选择时应根据项目需求决定。

Python vs. JavaScript:选择合适的工具Python vs. JavaScript:选择合适的工具May 08, 2025 am 12:10 AM

选择Python还是JavaScript取决于项目类型:1)数据科学和自动化任务选择Python;2)前端和全栈开发选择JavaScript。Python因其在数据处理和自动化方面的强大库而备受青睐,而JavaScript则因其在网页交互和全栈开发中的优势而不可或缺。

Python和JavaScript:了解每个的优势Python和JavaScript:了解每个的优势May 06, 2025 am 12:15 AM

Python和JavaScript各有优势,选择取决于项目需求和个人偏好。1.Python易学,语法简洁,适用于数据科学和后端开发,但执行速度较慢。2.JavaScript在前端开发中无处不在,异步编程能力强,Node.js使其适用于全栈开发,但语法可能复杂且易出错。

JavaScript的核心:它是在C还是C上构建的?JavaScript的核心:它是在C还是C上构建的?May 05, 2025 am 12:07 AM

javascriptisnotbuiltoncorc; saninterpretedlanguagethatrunsonenginesoftenwritteninc.1)javascriptwasdesignedAsalightweight,解释edganguageforwebbrowsers.2)Enginesevolvedfromsimpleterterterpretpreterterterpretertestojitcompilerers,典型地提示。

JavaScript应用程序:从前端到后端JavaScript应用程序:从前端到后端May 04, 2025 am 12:12 AM

JavaScript可用于前端和后端开发。前端通过DOM操作增强用户体验,后端通过Node.js处理服务器任务。1.前端示例:改变网页文本内容。2.后端示例:创建Node.js服务器。

Python vs. JavaScript:您应该学到哪种语言?Python vs. JavaScript:您应该学到哪种语言?May 03, 2025 am 12:10 AM

选择Python还是JavaScript应基于职业发展、学习曲线和生态系统:1)职业发展:Python适合数据科学和后端开发,JavaScript适合前端和全栈开发。2)学习曲线:Python语法简洁,适合初学者;JavaScript语法灵活。3)生态系统:Python有丰富的科学计算库,JavaScript有强大的前端框架。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)