首页 > web前端 > js教程 > 正文

从零到英雄:我建立房产租赁网站和移动应用程序的旅程

Susan Sarandon
发布: 2024-11-11 16:09:03
原创
1040 人浏览过

内容

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

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

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

介绍

这个想法源于建立无边界的愿望 - 一个完全可定制和可操作的房产租赁网站和移动应用程序,其中每个方面都在您的控制之下:

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

技术要求:

  • 支付网关
    • 实施安全、国际支持的支付网关
    • 确保跨多个国家/地区和货币的兼容性
  • DevOps
    • 使用 Docker 容器进行部署以实现一致性和可扩展性
    • 托管在最小的基础设施上(1GB RAM 服务器)
    • 使用 Hetzner 或 DigitalOcean 等提供商将每月托管成本维持在 5 美元以下
    • 优化资源使用以实现高效运营

技术堆栈

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

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

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

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

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

React 因其以下优点而成为绝佳选择:

  1. 基于组件的架构
    • 让您将复杂的 UI 分解为更小的、可重复使用的部分
    • 使代码更易于维护且更易于测试
    • 实现更好的代码组织和可重用性
  2. 虚拟 DOM 性能
    • React 的虚拟 DOM 高效地仅更新必要的内容
    • 带来更快的页面加载和更好的用户体验
    • 减少不必要的重新渲染
  3. 丰富的生态系统
    • 庞大的预建组件库
    • 丰富的工具
    • 提供支持和资源的大型社区
  4. 丰富的开发人员经验
    • 热重载以获取即时反馈
    • 优秀的调试工具
    • JSX 让编写 UI 代码更加直观
  5. 行业支持
    • 由 Meta(以前的 Facebook)支持
    • 被许多大公司使用
    • 持续开发和改进
  6. 灵活性
    • 适用于小型和大型应用程序
    • 可以逐步集成到现有项目中
    • 支持多种渲染策略(客户端、服务端、静态)

快速概览

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

前端

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

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是注册页面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

管理仪表板

三类用户:

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

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

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

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

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是编辑预订的页面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下是编辑机构的页面。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

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

现场演示

前端

  • 网址:https://movinin.dynv6.net:3004/
  • 登录:jdoe@movinin.io
  • 密码:M00vinin

管理仪表板

  • 网址:https://movinin.dynv6.net:3003/
  • 登录:admin@movinin.io
  • 密码:M00vinin

手机应用程序

您可以在任何 Android 设备上安装 Android 应用程序。

使用设备扫描此代码

打开相机应用程序并将其指向此代码。然后点击出现的通知。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

如何在 Android 上安装移动应用程序

  • 在运行 Android 8.0(API 级别 26)及更高版本的设备上,您必须导航到“安装未知应用程序”系统设置屏幕,才能从特定位置(即您下载应用程序的网络浏览器)启用应用程序安装.

  • 在运行 Android 7.1.1(API 级别 25)及更低版本的设备上,您应该启用“未知来源”系统设置,可在“设置”>“设置”中找到。您设备的安全性。

另类方式

您还可以通过直接下载APK并将其安装在任何Android设备上来安装Android应用程序。

  • 下载APK
  • 登录:jdoe@movinin.io
  • 密码:M00vinin

应用程序编程接口

From Zero to Hero: My Journey Building a Property 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 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 '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
登录后复制

属性由以下部分组成:

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

下面是创建控制器函数:

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 应用程序。在前端,客户可以根据接送点和时间搜索可用的汽车,选择汽车并继续结账:

  • ./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 '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 构建的。

  • ./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 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'
从 '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>


          

            
        </rootsiblingparent></stripeproviderpublishablekey></navigationcontainerref></notifications.subscription>
登录后复制

以上是从零到英雄:我建立房产租赁网站和移动应用程序的旅程的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板