ソースコード: https://github.com/aelassas/wexcommerce
デモ: https://wexcommerce.dynv6.net:8002
創造的な自由と技術的な管理を重視する開発者にとって、Shopify のような従来の e コマース プラットフォームは制限があると感じることがあります。 Shopify のテンプレートは迅速なセットアップを提供し、Storefront API はある程度の柔軟性を提供しますが、どちらのソリューションも現代の開発者が切望する完全なアーキテクチャ上の自由を提供するものではありません。
このアイデアは、境界のない構築、つまりあらゆる側面を制御できる完全にカスタマイズ可能な e コマース サイトを構築したいという願望から生まれました。
これを可能にした技術スタックは次のとおりです:
TypeScript には多くの利点があるため、設計上の重要な決定事項として TypeScript を使用することが決定されました。 TypeScript は強力な型指定、ツール、統合を提供し、その結果、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードが得られます。
私は強力なレンダリング機能のために Next.js を、柔軟なデータ モデリングのために MongoDB を、そして安全な支払い処理のために Stripe を選びました。
このスタックを選択すると、単にストアを構築するだけではなく、堅牢なオープンソース テクノロジーと成長する開発者コミュニティに支えられ、ニーズに合わせて進化できる基盤に投資することになります。
Next.js を使用してサイトを構築すると、ビジネスを拡大するための強固な基盤が得られます。コードの品質とドキュメントを維持しながら、パフォーマンス、セキュリティ、ユーザー エクスペリエンスに重点を置きます。定期的な更新と監視により、プラットフォームの競争力と信頼性が維持されます。
Next.js は、次の理由から優れた選択肢として際立っています。
ユーザーはフロントエンドから、利用可能な製品を検索し、カートに製品を追加してチェックアウトできます。
以下はフロントエンドのランディング ページです:
以下はフロントエンドの検索ページです:
以下はサンプル製品ページです:
以下は製品画像の全画面表示です:
以下はカートページです:
以下はチェックアウトページです:
以下はサインイン ページです:
以下はサインアップページです:
以下はユーザーが注文を確認できるページです:
それだけです!これらはフロントエンドのメインページです。
管理者は管理ダッシュボードから、カテゴリ、製品、ユーザー、注文を管理できます。
管理者は次の設定も管理できます:
以下はサインイン ページです:
以下は、管理者が注文を表示および管理できるダッシュボード ページです:
以下は管理者がカテゴリを管理するページです:
管理者が製品を表示および管理できるページは以下のとおりです:
以下は管理者が製品を編集するページです:
以下は製品画像の全画面表示です:
以下は設定ページです:
それだけです。これらは管理者ダッシュボードのメイン ページです。
この API は、MongoDB データベースへのアクセスを提供する Express を使用して RESTful API を公開する Node.js サーバー アプリケーションです。
API はフロントエンド、管理ダッシュボードによって使用され、モバイル アプリでも同様に使用されます。
API は、管理ダッシュボードとフロントエンドに必要なすべての機能を公開します。 API は MVC 設計パターンに従います。認証にはJWTが使用されます。商品や注文の管理に関連する機能など、認証が必要な機能と、認証されていないユーザー向けのカテゴリや利用可能な商品の取得など、認証を必要としない機能があります。
index.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 * 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 などのいくつかのモジュールをインポートします。次に、MongoDB データベースとの接続を確立します。次に、HTTPS 環境変数が true に設定されているかどうかを確認し、true に設定されている場合は、https モジュールと提供された秘密キーと証明書を使用して HTTPS サーバーを作成します。それ以外の場合は、http モジュールを使用して HTTP サーバーを作成します。サーバーは、PORT 環境変数で指定されたポートで待機します。
close 関数は、終了信号を受信したときにサーバーを正常に停止するように定義されています。サーバーと MongoDB 接続を閉じ、ステータス コード 0 でプロセスを終了します。最後に、プロセスが SIGINT、SIGTERM、または SIGQUIT シグナルを受信したときに呼び出される close 関数を登録します。
app.ts は API のメイン エントリ ポイントです:
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import userRoutes from './routes/userRoutes' import categoryRoutes from './routes/categoryRoutes' import productRoutes from './routes/productRoutes' import cartRoutes from './routes/cartRoutes' import orderRoutes from './routes/orderRoutes' import notificationRoutes from './routes/notificationRoutes' import deliveryTypeRoutes from './routes/deliveryTypeRoutes' import paymentTypeRoutes from './routes/paymentTypeRoutes' import settingRoutes from './routes/settingRoutes' import stripeRoutes from './routes/stripeRoutes' import wishlistRoutes from './routes/wishlistRoutes' 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('/', userRoutes) app.use('/', categoryRoutes) app.use('/', productRoutes) app.use('/', cartRoutes) app.use('/', orderRoutes) app.use('/', notificationRoutes) app.use('/', deliveryTypeRoutes) app.use('/', paymentTypeRoutes) app.use('/', settingRoutes) app.use('/', stripeRoutes) app.use('/', wishlistRoutes) i18n.locale = env.DEFAULT_LANGUAGE await helper.mkdir(env.CDN_USERS) await helper.mkdir(env.CDN_TEMP_USERS) await helper.mkdir(env.CDN_CATEGORIES) await helper.mkdir(env.CDN_TEMP_CATEGORIES) await helper.mkdir(env.CDN_PRODUCTS) await helper.mkdir(env.CDN_TEMP_PRODUCTS) export default app
まず、Express アプリを作成し、cors、compression、helmet、nocache などのミドルウェアをロードします。ヘルメットミドルウェアライブラリを利用して、さまざまなセキュリティ対策を設定します。また、productRoutes、orderRoutes、categoryRoutes、notificationRoutes、userRoutes など、アプリケーションのさまざまな部分のさまざまなルート ファイルもインポートします。最後に、Express ルートをロードし、アプリをエクスポートします。
API には 11 のルートがあります。各ルートには、MVC 設計パターンと SOLID 原則に従って独自のコントローラーがあります。以下は主なルートです:
各ルートを一つずつ説明するつもりはありません。たとえば、categoryRoutes を取り上げ、それがどのように作成されたかを見てみましょう:
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)) }
まず、Express Router を作成します。次に、その名前、メソッド、ミドルウェア、コントローラーを使用してルートを作成します。
routeNames には categoryRoutes ルート名が含まれています:
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 userRoutes from './routes/userRoutes' import categoryRoutes from './routes/categoryRoutes' import productRoutes from './routes/productRoutes' import cartRoutes from './routes/cartRoutes' import orderRoutes from './routes/orderRoutes' import notificationRoutes from './routes/notificationRoutes' import deliveryTypeRoutes from './routes/deliveryTypeRoutes' import paymentTypeRoutes from './routes/paymentTypeRoutes' import settingRoutes from './routes/settingRoutes' import stripeRoutes from './routes/stripeRoutes' import wishlistRoutes from './routes/wishlistRoutes' 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('/', userRoutes) app.use('/', categoryRoutes) app.use('/', productRoutes) app.use('/', cartRoutes) app.use('/', orderRoutes) app.use('/', notificationRoutes) app.use('/', deliveryTypeRoutes) app.use('/', paymentTypeRoutes) app.use('/', settingRoutes) app.use('/', stripeRoutes) app.use('/', wishlistRoutes) i18n.locale = env.DEFAULT_LANGUAGE await helper.mkdir(env.CDN_USERS) await helper.mkdir(env.CDN_TEMP_USERS) await helper.mkdir(env.CDN_CATEGORIES) await helper.mkdir(env.CDN_TEMP_CATEGORIES) await helper.mkdir(env.CDN_PRODUCTS) await helper.mkdir(env.CDN_TEMP_PRODUCTS) export default app
categoryController には、カテゴリに関する主要なビジネス ロジックが含まれています。コントローラーのソース コードは非常に大きいため、すべてを見ることはできませんが、コントローラー関数の作成を例に説明します。
以下はカテゴリモデルです:
import express from 'express' import multer from 'multer' import routeNames from '../config/categoryRoutes.config' import authJwt from '../middlewares/authJwt' import * as categoryController from '../controllers/categoryController' const routes = express.Router() routes.route(routeNames.validate).post(authJwt.verifyToken, categoryController.validate) routes.route(routeNames.checkCategory).get(authJwt.verifyToken, categoryController.checkCategory) routes.route(routeNames.create).post(authJwt.verifyToken, categoryController.create) routes.route(routeNames.update).put(authJwt.verifyToken, categoryController.update) routes.route(routeNames.delete).delete(authJwt.verifyToken, categoryController.deleteCategory) routes.route(routeNames.getCategory).get(authJwt.verifyToken, categoryController.getCategory) routes.route(routeNames.getCategories).get(categoryController.getCategories) routes.route(routeNames.getFeaturedCategories).get(categoryController.getFeaturedCategories) routes.route(routeNames.searchCategories).get(authJwt.verifyToken, categoryController.searchCategories) routes.route(routeNames.createImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], categoryController.createImage) routes.route(routeNames.updateImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], categoryController.updateImage) routes.route(routeNames.deleteImage).post(authJwt.verifyToken, categoryController.deleteImage) routes.route(routeNames.deleteTempImage).post(authJwt.verifyToken, categoryController.deleteTempImage) export default routes
カテゴリには複数の値があります。言語ごとに 1 つの値。デフォルトでは、英語とフランス語がサポートされています。
以下は価値モデルです:
export default { validate: '/api/validate-category', checkCategory: '/api/check-category/:id', create: '/api/create-category', update: '/api/update-category/:id', delete: '/api/delete-category/:id', getCategory: '/api/category/:id/:language', getCategories: '/api/categories/:language/:imageRequired', getFeaturedCategories: '/api/featured-categories/:language/:size', searchCategories: '/api/search-categories/:language', createImage: '/api/create-category-image', updateImage: '/api/update-category-image/:id', deleteImage: '/api/delete-category-image/:id', deleteTempImage: '/api/delete-temp-category-image/:image', }
値には言語コード (ISO 639-1) と文字列値が含まれます。
以下はコントローラー関数の作成です:
import { Schema, model } from 'mongoose' import * as env from '../config/env.config' const categorySchema = new Schema<env.Category>({ values: { type: [Schema.Types.ObjectId], ref: 'Value', validate: (value: any) => Array.isArray(value), }, image: { type: String, }, featured: { type: Boolean, default: false, }, }, { timestamps: true, strict: true, collection: 'Category', }) const Category = model<env.Category>('Category', categorySchema) export default Category
この関数では、リクエストの本文を取得し、本文で指定された値 (言語ごとに 1 つの値) を反復処理して、値を作成します。最後に、作成した値と画像ファイルに応じてカテゴリーを作成します。
フロントエンドは、Next.js と MUI で構築された Web アプリケーションです。フロントエンドから、ユーザーは利用可能な商品を検索し、カートに追加し、利用可能な配送方法と支払い方法に応じてチェックアウトに進むことができます。
フロントエンドは create-next-app で作成されました:
import { Schema, model } from 'mongoose' import * as env from '../config/env.config' const locationValueSchema = new Schema<env.Value>( { language: { type: String, required: [true, "can't be blank"], index: true, trim: true, lowercase: true, minLength: 2, maxLength: 2, }, value: { type: String, required: [true, "can't be blank"], index: true, trim: true, }, }, { timestamps: true, strict: true, collection: 'Value', }, ) const Value = model<env.Value>('Value', locationValueSchema) export default Value
Next.js では、ページは、pages ディレクトリ内の .js、.jsx、.ts、または .tsx ファイルからエクスポートされた React コンポーネントです。各ページは、ファイル名に基づいてルートに関連付けられます。
デフォルトでは、Next.js はすべてのページを事前レンダリングします。これは、クライアント側の JavaScript ですべてを実行するのではなく、Next.js が各ページの HTML を事前に生成することを意味します。プリレンダリングにより、パフォーマンスと SEO が向上します。
生成された各 HTML は、そのページに必要な最小限の JavaScript コードに関連付けられます。ページがブラウザによって読み込まれると、その JavaScript コードが実行され、ページが完全にインタラクティブになります。 (このプロセスを水和といいます。)
フロントエンドは SEO 最適化のためにサーバーサイド レンダリングを使用し、検索エンジンで商品のインデックスを作成できるようにします。
管理ダッシュボードは、Next.js と MUI で構築された Web アプリケーションです。管理者は管理ダッシュボードから、カテゴリ、製品、注文、ユーザーを管理できます。新しい注文が作成されると、管理者ユーザーは通知を受け取り、電子メールを受け取ります。
管理ダッシュボードも create-next-app で作成されました:
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)) }
それだけです!楽しんで読んでいただければ幸いです。
以上がゼロからストアフロントまで: 電子商取引サイトを構築する私の旅の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。