Maison > Article > interface Web > De zéro à vitrine : mon parcours pour créer un site de commerce électronique
Code source : https://github.com/aelassas/wexcommerce
Démo : https://wexcommerce.dynv6.net:8002
Pour les développeurs qui apprécient la liberté de création et le contrôle technique, les plateformes de commerce électronique traditionnelles comme Shopify peuvent sembler restrictives. Bien que les modèles de Shopify offrent une configuration rapide et que leur API Storefront offre une certaine flexibilité, aucune des deux solutions n'offre la liberté architecturale complète dont les développeurs modernes recherchent.
L'idée est née d'un désir de construire sans frontières – un site de commerce électronique entièrement personnalisable où chaque aspect est sous votre contrôle :
Voici la pile technologique qui a rendu cela possible :
Une décision clé en matière de conception a été prise d'utiliser TypeScript en raison de ses nombreux avantages. TypeScript offre un typage, des outils et une intégration puissants, ce qui donne lieu à un code de haute qualité, évolutif, plus lisible et maintenable, facile à déboguer et à tester.
J'ai choisi Next.js pour ses puissantes capacités de rendu, MongoDB pour une modélisation flexible des données et Stripe pour le traitement sécurisé des paiements.
En choisissant cette pile, vous ne construisez pas seulement un magasin : vous investissez dans une fondation qui peut évoluer avec vos besoins, soutenue par des technologies open source robustes et une communauté de développeurs croissante.
Construire un site avec Next.js fournit une base solide pour développer une entreprise. Concentrez-vous sur les performances, la sécurité et l'expérience utilisateur tout en maintenant la qualité du code et la documentation. Des mises à jour et une surveillance régulières garantiront que la plateforme reste compétitive et fiable.
Next.js se démarque comme un excellent choix grâce à :
Depuis le frontend, l'utilisateur peut rechercher des produits disponibles, ajouter des produits au panier et finaliser le paiement.
Ci-dessous la page de destination du frontend :
Ci-dessous la page de recherche du frontend :
Vous trouverez ci-dessous un exemple de page de produit :
Vous trouverez ci-dessous une vue plein écran des images du produit :
Ci-dessous la page du panier :
Ci-dessous la page de paiement :
Vous trouverez ci-dessous la page de connexion :
Ci-dessous la page d'inscription :
Ci-dessous la page où l'utilisateur peut consulter ses commandes :
C'est ça ! Ce sont les pages principales du frontend.
À partir du tableau de bord d'administration, les administrateurs peuvent gérer les catégories, les produits, les utilisateurs et les commandes.
Les administrateurs peuvent également gérer les paramètres suivants :
Vous trouverez ci-dessous la page de connexion :
Vous trouverez ci-dessous la page du tableau de bord à partir de laquelle les administrateurs peuvent voir et gérer les commandes :
Vous trouverez ci-dessous la page à partir de laquelle les administrateurs gèrent les catégories :
Vous trouverez ci-dessous la page à partir de laquelle les administrateurs peuvent voir et gérer les produits :
Vous trouverez ci-dessous la page à partir de laquelle les administrateurs modifient les produits :
Vous trouverez ci-dessous une vue plein écran des images du produit :
Vous trouverez ci-dessous la page des paramètres :
C'est tout. Ce sont les pages principales du tableau de bord d'administration.
L'API est une application serveur Node.js qui expose une API RESTful utilisant Express qui donne accès à la base de données MongoDB.
L'API est utilisée par le frontend, le tableau de bord d'administration et sera également utilisée par l'application mobile.
L'API expose toutes les fonctions nécessaires au tableau de bord d'administration et au frontend. L'API suit le modèle de conception MVC. JWT est utilisé pour l'authentification. Certaines fonctions nécessitent une authentification comme les fonctions liées à la gestion des produits et des commandes, et d'autres ne nécessitent pas d'authentification comme la récupération des catégories et des produits disponibles pour les utilisateurs non authentifiés :
index.ts est sur le serveur principal :
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)) }
Il s'agit d'un fichier TypeScript qui démarre un serveur à l'aide de Node.js et Express. Il importe plusieurs modules, notamment dotenv, process, fs, http, https, mongoose et app. Il établit ensuite une connexion avec la base de données MongoDB. Il vérifie ensuite si la variable d'environnement HTTPS est définie sur true et, si tel est le cas, crée un serveur HTTPS à l'aide du module https ainsi que de la clé privée et du certificat fournis. Sinon, il crée un serveur HTTP à l'aide du module http. Le serveur écoute sur le port spécifié dans la variable d'environnement PORT.
La fonction close est définie pour arrêter gracieusement le serveur lorsqu'un signal de terminaison est reçu. Il ferme le serveur et la connexion MongoDB, puis quitte le processus avec un code d'état de 0. Enfin, il enregistre la fonction de fermeture à appeler lorsque le processus reçoit un signal SIGINT, SIGTERM ou SIGQUIT.
app.ts est le point d'entrée principal de l'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
Tout d'abord, nous créons une application Express et chargeons des middlewares tels que cors, compression, casque et nocache. Nous avons mis en place diverses mesures de sécurité à l'aide de la bibliothèque middleware du casque. Nous importons également divers fichiers de route pour différentes parties de l'application telles que productRoutes, orderRoutes, CategoryRoutes, notificationRoutes, userRoutes. Enfin, nous chargeons les itinéraires Express et exportons l'application.
Il y a 11 routes dans l'API. Chaque route possède son propre contrôleur suivant le modèle de conception MVC et les principes SOLID. Ci-dessous les principaux itinéraires :
Nous n'allons pas expliquer chaque itinéraire un par un. Prenons par exemple la catégorieRoutes et voyons comment elle a été réalisée :
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)) }
Tout d’abord, nous créons un routeur express. Ensuite, on crée des routes en utilisant son nom, sa méthode, ses middlewares et son contrôleur.
routeNames contient les noms d'itinéraires de catégorieRoutes :
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 contient la principale logique métier concernant les catégories. Nous n'allons pas voir tout le code source du contrôleur car il est assez volumineux mais nous prendrons par exemple la fonction de création de contrôleur.
Ci-dessous le modèle de catégorie :
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
Une catégorie a plusieurs valeurs. Une valeur par langue. Par défaut, les langues anglaise et française sont prises en charge.
Ci-dessous le modèle de valeur :
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', }
Une valeur a un code de langue (ISO 639-1) et une valeur de chaîne.
Ci-dessous se trouve la fonction de création de contrôleur :
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
Dans cette fonction, on récupère le corps de la requête, on parcourt les valeurs fournies dans le corps (une valeur par langue) et on crée une Value. Enfin, nous créons la catégorie en fonction des valeurs créées et du fichier image.
Le frontend est une application Web construite avec Next.js et MUI. Depuis le frontend, l'utilisateur peut rechercher les produits disponibles, les ajouter au panier et procéder au paiement en fonction des modes de livraison et de paiement disponibles.
Le frontend a été créé avec 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
Dans Next.js, une page est un composant React exporté à partir d'un fichier .js, .jsx, .ts ou .tsx dans le répertoire des pages. Chaque page est associée à un itinéraire en fonction de son nom de fichier.
Par défaut, Next.js pré-rend chaque page. Cela signifie que Next.js génère du HTML pour chaque page à l'avance, au lieu de tout faire via JavaScript côté client. Le pré-rendu peut entraîner de meilleures performances et un meilleur référencement.
Chaque HTML généré est associé au code JavaScript minimal nécessaire pour cette page. Lorsqu'une page est chargée par le navigateur, son code JavaScript s'exécute et rend la page entièrement interactive. (Ce processus est appelé hydratation.)
le frontend utilise le rendu côté serveur pour l'optimisation du référencement afin que les produits puissent être indexés par les moteurs de recherche.
Le tableau de bord d'administration est une application Web construite avec Next.js et MUI. Depuis le tableau de bord d'administration, les administrateurs peuvent gérer les catégories, les produits, les commandes et les utilisateurs. Lorsqu'une nouvelle commande est créée, l'utilisateur administrateur reçoit une notification et reçoit un e-mail.
Le tableau de bord d'administration a également été créé avec 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)) }
C'est ça ! J'espère que vous avez apprécié la lecture.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!