原始碼:https://github.com/aelassas/movinin
示範:https://movinin.dynv6.net:3004
這個想法源於建立無邊界的願望 - 一個完全可自訂和可操作的房地產租賃平台,其中每個方面都在您的控制之下:
這是使其成為可能的技術堆疊:
由於 TypeScript 具有眾多優點,因此做出了使用 TypeScript 的關鍵設計決定。 TypeScript 提供強大的類型、工具和集成,從而產生高品質、可擴展、更具可讀性和可維護性的程式碼,並且易於調試和測試。
我選擇React是因為它強大的渲染能力,MongoDB是為了靈活的資料建模,而Stripe是為了安全的支付處理。
選擇此堆疊,您不僅僅是在建立網站和行動應用程式 - 您正在投資一個可以根據您的需求不斷發展的基礎,並得到強大的開源技術和不斷發展的開發者社群的支持。
在本部分中,您將看到前端、管理儀表板和行動應用程式的主頁。
在前端,客戶可以搜尋可用房產、選擇房產並結帳。
下面是前端的主頁,客戶可以在其中輸入位置點和時間,並搜尋可用的屬性。
以下是主頁的搜尋結果,客戶可以在其中選擇出租房產。
以下是客戶可以查看房產詳情的頁面:
以下是該物業的圖片視圖:
下面是結帳頁面,客戶可以在其中設定租賃選項和結帳。如果顧客未註冊,可以同時結帳和註冊。如果他尚未註冊,他將收到一封確認和啟動電子郵件以設定密碼。
以下是登入頁面。在生產中,身份驗證 cookie 是 httpOnly、簽署的、安全且嚴格的 sameSite。這些選項可防止 XSS、CSRF 和 MITM 攻擊。身份驗證 cookie 也可以透過自訂中間件免受 XST 攻擊。
以下是註冊頁面。
以下是客戶可以查看和管理他的預訂的頁面。
以下是客戶可以查看預訂詳細資訊的頁面。
以下是客戶可以看到他的通知的頁面。
以下是客戶可以管理其設定的頁面。
以下是客戶可以更改密碼的頁面。
就是這樣。這是前端的主要頁面。
三類使用者:
該平台旨在與多個機構合作。每個機構都可以透過管理儀表板管理其財產、客戶和預訂。該平台也可以只與一個機構合作。
管理員可以從後端建立和管理代理商、飯店、地點、客戶和預訂。
建立新代理商時,他們會收到一封電子郵件,提示他們建立帳戶以存取管理儀表板,以便他們可以管理其財產、客戶和預訂。
以下是管理儀表板的登入頁面。
下面是儀表板頁面,管理員和代理商可以在其中查看和管理預訂。
如果預訂狀態發生變化,相關客戶將收到通知和電子郵件。
下面是顯示和管理屬性的頁面。
以下是管理員和機構可以透過提供圖像和房產資訊來建立新房產的頁面。如需免費取消,請將其設為 0。否則,請設定該選項的價格,或者如果您不想包含它,則將其留空。
以下是管理員和機構可以編輯屬性的頁面。
以下是管理員可以管理客戶的頁面。
如果代理商想要從管理儀表板建立預訂,以下是建立預訂的頁面。否則,當從前端或行動應用程式完成結帳流程時,會自動建立預訂。
以下是編輯預訂的頁面。
以下是管理代理商的頁面。
以下是建立新代理商的頁面。
以下是編輯機構的頁面。
以下是查看代理商屬性的頁面。
以下是查看客戶預訂的頁面。
以下是管理員和機構可以管理其設定的頁面。
還有其他頁面,但這些是管理儀表板的主頁。
就是這樣。這是管理儀表板的主要頁面。
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,如果是,則使用 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 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
以下是房產類型:
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 }
屬性由以下部分組成:
以下是建立控制器函數:
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 應用程式。在前端,客戶可以根據接送點和時間搜尋可用的汽車,選擇汽車並繼續結帳:
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 建構的。
TypeScript 類型定義定義於:
./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中文網其他相關文章!