原始碼:https://github.com/aelassas/bookcars
示範:https://bookcars.dynv6.net:3002
這個想法源於建立無邊界的願望 - 一個完全可定制的汽車租賃網站和行動應用程序,其中每個方面都在您的控制之下:
這是使其成為可能的技術堆疊:
由於 TypeScript 具有眾多優點,因此做出了使用 TypeScript 的關鍵設計決定。 TypeScript 提供強大的類型、工具和集成,從而產生高品質、可擴展、更具可讀性和可維護性的程式碼,並且易於調試和測試。
我選擇React是因為它強大的渲染能力,MongoDB是為了靈活的資料建模,而Stripe是為了安全的支付處理。
選擇此堆疊,您不僅僅是在建立網站和行動應用程式 - 您正在投資一個可以根據您的需求不斷發展的基礎,並得到強大的開源技術和不斷發展的開發者社群的支持。
React 因其以下優點而成為絕佳選擇:
在本部分中,您將看到前端、管理儀表板和行動應用程式的主頁。
在前端,使用者可以搜尋可用的汽車、選擇汽車並結帳。
下面是前端主頁,使用者可以選擇上下車地點和時間,並搜尋可用的車輛。
以下是主頁的搜尋結果,用戶可以在其中選擇租車。
下面是結帳頁面,使用者可以在其中設定租賃選項和結帳。如果用戶未註冊,可以同時結帳和註冊。如果他尚未註冊,他將收到一封確認和啟動電子郵件以設定密碼。
以下是登入頁面。在生產中,身份驗證 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 * 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,如果是,則使用 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、壓縮、頭盔和 nocache 等中間件。我們使用頭盔中間件庫設置了各種安全措施。我們也為應用程式的不同部分匯入各種路線文件,例如 sellerRoutes、bookingRoutes、locationRoutes、notificationRoutes、carRoutes 和 userRoutes。最後,我們載入 Express 路線並匯出應用程式。
api中有8條路由。每條路線都有自己的控制器,遵循 MVC 設計模式和 SOLID 原則。主要路線如下:
我們不會一一解釋每條路線。我們將以 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 }
一個國家有多種價值觀。每種語言一個。預設支援英語和法語。
以下是 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
在此函數中,我們檢索請求的正文,迭代正文中提供的值(每種語言一個值)並建立一個 LocationValue。最後,我們根據建立的位置值建立國家/地區。
前端是一個使用 Node.js、React、MUI 和 TypeScript 建立的 Web 應用程式。在前端,客戶可以根據接送點和時間搜尋可用的汽車,選擇汽車並繼續結帳:
TypeScript 類型定義在套件 ./packages/bookcars-types 中定義。
App.tsx 是主要的 React 應用程式:
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 建構的。
TypeScript 類型定義定義於:
./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' 從 '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({ handlerNotification: 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/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> </navigationcontainer></rootsiblingparent></stripeproviderpublishablekey></navigationcontainerref></notifications.subscription>
以上是從零到店面:我建立汽車租賃網站和行動應用程式的旅程的詳細內容。更多資訊請關注PHP中文網其他相關文章!