[MERN 項目實戰] MERN Multi-Vendor 電商平臺開發筆記(v2.0 從 bug 到結構優化的工程記錄)

[MERN 項目實戰] MERN Multi-Vendor 電商平臺開發筆記(v2.0 從 bug 到結構優化的工程記錄)

其實之前沒想著這么快就能把 2.0 的筆記寫出來的,之前的預期是,下一個階段會一直維持到將 MERN 項目寫完,畢竟后期很多東西都是 cv 了。不過沒想到,一個 frontend(2C 端的商城頁面)寫著寫著還是碰到了不少的問題

后端

這里其實就一個 routes 的路徑順序問題,我也是等到 v1 收尾了,又做了一點點 cleanup,在不同頁面來回切換的時候,發現請求的路徑不對,express 的 log 一直在顯示 string 是一個不合法的 ObjectId,然后找不到對應的數據

后面看了下,是 routes 的路徑問題,之前的寫法是:

routes.get("/something/:someId", getSomethingById);
routes.get("/something/sub-resource", getSubResource);

這種寫法,會將 sub-resource map 到 :someId 里,express 直接運行 getSomethingById ,最終因為 id 不匹配而拋出異常

數據庫

簡單的記錄一下這個用法:

SellerModel.findById(id).populate("shop");

我這里的 shop 和 seller 又 1-to-1 的關系,具體的 schema 關系如下:

import { Schema, model, Types, Document } from "mongoose";export interface IShop extends Document {seller: Types.ObjectId;name: string;country: string;state: string;city: string;image?: string;
}const shopSchema = new Schema<IShop>({seller: {type: Schema.Types.ObjectId,ref: "Seller",required: true,unique: true, // enforce one-to-one},name: { type: String, required: true },country: { type: String, required: true },state: { type: String, required: true },city: { type: String, required: true },image: { type: String, default: "" },},{ timestamps: true }
);export default model<IShop>("Shop", shopSchema);

Seller 內部的實現是差不多的,代碼太長了就不貼了。這種情況下,使用 populate 可以將 shop 的數據 map 到 seller 種的 shop 屬性——原本是一個 ObjectId 的字符串,這種情況就可以減少一個與后端的 API 請求,在真實的使用場景會很好的減少數據庫的壓力

Workspace/MonoRepo

前端的東西就比較多啦,畢竟這次主要折騰的就是 UI,而且還是比較難得,場景比較全面的 2C 端的 UI。這次寫完也確實發現一點問題,尤其是代碼重復的這個問題

鑒于 notion 的結構比較有限——只有 3 級,這里就把前端部分的問題細細拆成 workspace、React 相關、tailwind css 相關,

重復的業務邏輯……微前端是解決方法?

這里主要說的是 hooks,utils 和 componengs 三個組件,如:

雖然 frontend 尚且還沒有開始實現業務相關的邏輯,不過已經能夠看到有一些重復的使用,如:

  • cn.js → 一個簡單的 tailwind css 的 util 方法
  • Pagination → Pagination 的 UI 渲染
  • usePaginnationSearch → 實現了 debounce/search/pagination 的 hooks
  • 共通的 packages 等等

包括之后可能會涉及到的 auth 相關的邏輯……也的確是應該研究一下微前端是不是能夠很好的解決這個問題,尤其是兩個項目都是 React based,共通的 modules 太多了

沖突的 React 版本

這個問題的出現,是在嘗試使用一個 dependency 的時候發生的,具體的報錯大概就是 react 中的 useEffect 這個 hook 出現了問題,具體只記得是 Invalid hook call,但是記不太清細節了……有可能是 useEffect 被調用了兩次……不過最終發現,原因是 React 的版本發生了沖突:

? yarn list reactyarn list v1.22.22
warning Filtering by arguments is deprecated. Please use the pattern option instead.
├─ frontend@0.1.0
│  └─ react@19.1.0
└─ react@19.0.0
?  Done in 0.54s.

我之前有簡單搜索一下,這個問題的確是通過 turborepo 進行 monorepo 的管理出現的問題,尤其是我在兩個不同的時間段安裝了 dashboard 和 frontend 模塊,這導致兩個模塊中的 React 版本有了輕微的沖突。在兩個版本都出現在 node_modules 中,就會被識別成兩個不同的 React 實例

問題的關鍵在這里:

兩個 React 實例創建了不同的 context,以至于在某些 edge case 的情況下會拋出異常,即用串了 context,找不到自己原本應該調用的 context,然后觸發該異常。只能說在運行不同的 React 版本,沒有拋異常是運氣,拋了異常,就有可能是 production issue……

最后的解決方案是在 root dependency 中定義 React 的版本,在根目錄下運行 yarn install --force,重新安裝/管理依賴,解決問題。根目錄的 package.json 如下:

{"resolutions": {"react": "^19.1.0"}
}

運行過程&結果:

? yarn install --force
? yarn list react
yarn list v1.22.22
warning Filtering by arguments is deprecated. Please use the pattern option instead.
└─ react@19.1.0
?  Done in 0.57s.

??:在這種情況下,推薦的做法是不寫死 react 版本,而是用 peerDependencies 去更優化的管理版本

無法安裝依賴的根目錄

這算是一個補充吧,因為我自己其實都不知道還有這個限制

事情起因是,在新建 frontend 的時候,不小心在根目錄下運行了 yarn add 指令,然后 yarn/turborepo 拋出了這個異常:

error Running this command will add the dependency to the workspace root rather than the workspace itself, wh…

這里做個簡單的記錄

React

這里放一點只和 React 相關,范圍比較狹窄的內容

使用 env 改變 PORT

其實我不太清楚 .env 文件到底能夠重寫多少 React 的屬性,不過 port 算是蠻重要的一個,這里提一嘴

修改了 port 之后,turborepo 就可以同時運行 3000(dashboard ui) 和 3001(frontend ui) 了

React 項目文件結構如何設計

最初我們開始寫 React 的時候用的結構就不談了,說一下我們現在主要用的兩種,第一種是所有的相關聯的組件在 components 下,并且按照功能關聯,大體如下:


components/
├── features/                  # 頁面級組件
│   ├── home/
│   ├── products/
├── ui/                     # 原子化 UI 組件(通常無狀態)
│   ├── Button.tsx
│   ├── Input.tsx
│   └── Card.tsx
├── shared/                 # 可復用的復合組件(頁面級別也會引用)
│   ├── PageBanner.tsx
│   ├── ProductCard.tsx
│   └── Ratings.tsx
├── layout/                 # 頁面布局類組件
│   └── Navbar.tsx

另一種則是所有相關聯的組件在 pages 下,每個頁面不作為單獨的 jsx 文件,而是作為一個文件夾,存儲相關聯的組件,大體結構如下:

pages/
├── Home/
│   ├── index.tsx             # Home 頁面入口組件
│   ├── HeroBanner.tsx        # 頁面專屬組件
│   ├── useWelcomeData.ts     # 頁面專屬 hook
│   └── styles.module.css     # 頁面樣式
├── Products/
│   ├── index.tsx
│   ├── ProductList.tsx
│   ├── ProductFilter.tsx
│   ├── useProductQuery.ts
│   └── styles.module.css
├── Checkout/
│   ├── index.tsx
│   ├── AddressForm.tsx
│   ├── PaymentSummary.tsx
│   ├── useCheckout.ts
│   └── styles.module.css

需要注意的是,這種以頁面為中心的存儲方式,依然會保留 components 文件夾,并且在里面集中管理 shared、UI 之類相關組件,只是會將 features 中的內容放到頁面中

具體二者的存儲方式并沒有絕對意義上的優缺點,只能說必須要根據業務情況做分析。以我自己的項目為經驗,我個人感覺是:

  • B2C 適合第一種
    其主要原因是 B2C 的業務,結構相對而言更加的簡單,業務邏輯復用更多,比如說一個常見的商城項目,首頁會出現各種各樣的商品卡片、促銷商品,商家頁面中會出現商品卡片,商品頁面中會出現更多的 product 相關的組件。這種時候,在 components 下放一個商品相關的 feature,集中管理散落在各個頁面的復用組件
  • B2B 適合第二種
    與之對比的是 B2B 的業務,結構相對會更加的復雜,業務邏輯多與頁面進行綁定,鮮少會出現核心 UI 邏輯散落在不同頁面中。就算偶爾會出現這個情況,大多數也是作為 reference data 的存在,可以以該 UI 的主頁面作為 base 進行導入

React Router DOM

這個應該說在寫這個項目之前,我都沒有意識到會有這個問題,寫法大體如下:

<Linkto="/something"state={{someState: someState,}}
><button>something</button>
</Link>

在我看來這個代碼是沒問題的——或者說一直以來都是這么寫的,一直工作都沒什么問題,除了這個 state——主要是想嘗試一下新寫法,嘗試在 navigate 的時候將狀態帶到下一個頁面去,而不是使用 zustand/redux 進行全局化的管理,這樣清理狀態也比較方便

搜索了一下之后發現,這是 React Router DOM 在遵從了 HTML 的標準實現規范后出現的問題。本質上的邏輯是這樣的:

  1. Link 在渲染后成為 <a href=""></a>
  2. button 嵌套在了 a 標簽中

這就是問題

好吧,這么說還是不夠直白……具體要解釋原因,就得到 WHATWG——也就是現在 HTML 版本規范的組織——的官方文檔里

其中在 **3.2.5.2.7 interactive content 中提到:

3.2.5.2.7?Interactive content

Interactive content?is content that is specifically intended for user interaction.

  • a?(if the?href?attribute is present)
  • audio?(if the?controls?attribute is present)
  • button
  • details
  • embed
  • iframe
  • img?(if the?usemap?attribute is present)
  • input?(if the?type?attribute is?not?in the?Hidden?state)
  • label
  • select
  • textarea
  • video?(if the?controls?attribute is present)

在 4.10.6?The?button?element 中提到

4.10.6?The?button?element

Content model:Phrasing content, but there must be no?interactive content?descendant and no descendant with the?tabindex?attribute specified.

同樣在 stack overflow 上的一個 thread 也有討論過:**HTML Validation: Why is it not valid to put an interactive element inside an interactive element? ,這就能解決問題了:**

互動內容中嵌套互動內容是不合法的 HTML,這種實踐下的行為是不可預測的,有可能 button 的 event listener 捕捉了 a 標簽的重定向,反之亦然。工作那是運氣好,不工作才是默認的行為

這里最終的解決方法其實是用 onClick 綁定了 useNavigate,但是真正、最好、符合 accessibility 的解法,還是應該用 button+span+手寫樣式的方法去解決這個問題……

Tailwind CSS

之前主要上的是 tailwind css 的課,instructors 不管怎么說對 tailwind 還是比較專業的,因此學到了一些基礎,不過反思比較少。這次的 instructor 代碼寫的真的挺爛的,然后就發現已經學過的 tailwind css——或者說 css,其實還是有不少東西可以深挖的

基礎色的變化

雖然我發現在之前學習的過程中,大部分項目使用的是 hex,不過在做了 tailwind 之后,我發現其實 rgb 相對而言會更加的動態一些。以下面這個 button 為例:

至少 有兩種實現方法:

<buttonclassName={cn("w-[200px] h-[36px] px-4 py-1  rounded-md bg-[#059473] text-white","hover:shadow-lg hover:shadow-[#059473B2]")}
>Example
</button>

這里的 hover,其實還是以 base color,即 059473 做的變量,起主要就是修改了不透明度,也就是 hex 后面的兩位數字

對比起來是用 rgb 的實現:

<buttonclassName={cn("w-[200px] h-[36px] px-4 py-1 rounded-md text-white bg-[rgb(5,148,115)]","hover:shadow-lg hover:shadow-[rgba(5,148,115,0.7)]")}
>Example
</button>

可以看到,這種情況下,使用 rgb 是可以更加直觀地看到對于背景色的修改是多少。對于前端開發來說,這樣可以在選擇好 base 這種基礎顏色后,通過調整不透明程度的方法獲取一整套的顏色表——畢竟現在前端開發其實 UI/UX 的差別越來越大了。以我本人來說,根本搞不定 figma/adobe illustrator,更別說能夠拿出同樣的配色表

rgb 和 rgba 的搭配其實只能獲取一個淺色表,如果想要獲取深色的方法,可以:

  • 使用 hsl
    如下面的代碼:
              <div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,20%)] text-white">Dark Mode</div><div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,30%)] text-white">Base Mode</div><div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,45%)] text-black">Light Mode</div>
    
    效果如下:

    可以看到,換了亮度后就有了不同等級的顏色,其實也可以對標類似于 blue50-800 這樣的配置
  • 手動計算 rgb 的值,即每個數值乘以相同的系數
    這個只是我覺得理論上可以 work,實際操作可能會覺得比較麻煩沒做過的事情,而且我覺得這個操作對于純色的挑戰會比較大……

同樣的原理其實也可以用在 opacity 上。普通的 opacity 只能加一個透明度,但是如果在 div 上,添加一個大小完全一致的黑色遮照,通過控制遮照的透明度,也能夠完成 hover 后獲取一個更深的背景色這一方法——這時候就要善用 relative & absolute & :before or :after

兄弟組件也一起向上移動

這種情況用截圖說明比較容易:

可以看到,Base Mode 移動了的話,Light Mode 也會一起向上走,這是因為移動時用的是 mt:

          <div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,20%)] text-white hover:mt-2 transition-all duration-100">Dark Mode</div><div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,30%)] text-white hover:mt-2 transition-all duration-100">Base Mode</div><div class="w-[150px] h-[32px] my-5 bg-[hsl(164,93%,45%)] text-black hover:mt-2 transition-all duration-100">Light Mode

而使用 mt 會重新計算文檔流的位置,這種情況下,用 translate 會有更好的效果。translate 本身不會修改原有元素的位置,因此不會計算剩下所有文檔流的位置

eslint

主要是因為 instructor 有 typo,然后我發現 css 不起效,eslint 有蠻多的問題的,首先是 CRA 用的 eslint 還是 v8,但是現在 eslint 的官方已經出到了 v9,我在這個配置,出了很多的報錯,后面才發現是版本沖突的問題,導致 eslint 的配置也不一樣了——eslint 的配置文件名也不一樣

這里就按照 eslint v8 的配置,文件名還是 .eslintrc.js:

module.exports = {plugins: ["tailwindcss"],extends: ["react-app", "plugin:tailwindcss/recommended"],rules: {"tailwindcss/no-custom-classname": ["warn",{whitelist: ["header-top", "my-swiper", "custom_bullet"],},],"tailwindcss/classnames-order": "off",},
};

只要 VSCode 開啟了 eslint 的附件,那么,出現了 typo 之后,vscode 就會開始自動提示:

cn util

之前好像在 electron 里面提過這個 util,實現方法如下:

import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";export function cn(...inputs) {return twMerge(clsx(inputs));
}

cn 可以用這幾種方式加類名:

cn("plain string", true && "plain string", {"plain string": condition1,"plain stringq": condition2,
});

整體來說,使用 cn 動態管理類名會相對而言更加的直觀

gh

還是 github 的功能,研究了下發現還是還挺有意思的

gh template

templates 需要放在 .github/ISSUE_TEMPLATE 下,里面是 md 文檔,放一些描述/heading 即可

批量更新

不過這里用腳本跑的,代碼大體如下:

for i in 42 43 44 45 46 47
dogh issue edit $i --add-label "features,frontend,ui"
done

這樣就能一次更新 42-47,然后添加相同的 labels

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/80708.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/80708.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/80708.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

互斥量函數組

頭文件 #include <pthread.h> pthread_mutex_init 函數原型&#xff1a; int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 函數參數&#xff1a; mutex&#xff1a;指向要初始化的互斥量的指針。 attr&#xf…

互聯網的下一代脈搏:深入理解 QUIC 協議

互聯網的下一代脈搏&#xff1a;深入理解 QUIC 協議 互聯網是現代社會的基石&#xff0c;而數據在其中高效、安全地傳輸是其運轉的關鍵。長期以來&#xff0c;傳輸層的 TCP&#xff08;傳輸控制協議&#xff09;一直是互聯網的主力軍。然而&#xff0c;隨著互聯網應用場景的日…

全球城市范圍30米分辨率土地覆蓋數據(1985-2020)

Global urban area 30 meter resolution land cover data (1985-2020) 時間分辨率年空間分辨率10m - 100m共享方式保護期 277 天 5 時 42 分 9 秒數據大小&#xff1a;8.98 GB數據時間范圍&#xff1a;1985-2020元數據更新時間2024-01-11 數據集摘要 1985~2020全球城市土地覆…

【Vue】單元測試(Jest/Vue Test Utils)

個人主頁&#xff1a;Guiat 歸屬專欄&#xff1a;Vue 文章目錄 1. Vue 單元測試簡介1.1 為什么需要單元測試1.2 測試工具介紹 2. 環境搭建2.1 安裝依賴2.2 配置 Jest 3. 編寫第一個測試3.1 組件示例3.2 編寫測試用例3.3 運行測試 4. Vue Test Utils 核心 API4.1 掛載組件4.2 常…

數據湖的管理系統管什么?主流產品有哪些?

一、數據湖的管理系統管什么&#xff1f; 數據湖的管理系統主要負責管理和優化存儲在數據湖中的大量異構數據&#xff0c;確保這些數據能夠被有效地存儲、處理、訪問和治理。以下是數據湖管理系統的主要職責&#xff1a; 數據攝入管理&#xff1a;管理系統需要支持從多種來源&…

英文中日期讀法

英文日期的讀法和寫法因地區&#xff08;英式英語與美式英語&#xff09;和正式程度有所不同&#xff0c;以下是詳細說明&#xff1a; 一、日期格式 英式英語 (日-月-年) 寫法&#xff1a;1(st) January 2023 或 1/1/2023讀法&#xff1a;"the first of January, twenty t…

衡量矩陣數值穩定性的關鍵指標:矩陣的條件數

文章目錄 1. 定義2. 為什么要定義條件數&#xff1f;2.1 分析線性系統 A ( x Δ x ) b Δ b A(x \Delta x) b \Delta b A(xΔx)bΔb2.2 分析線性系統 ( A Δ A ) ( x Δ x ) b (A \Delta A)(x \Delta x) b (AΔA)(xΔx)b2.3 定義矩陣的條件數 3. 性質及幾何意義3…

4月22日復盤-開始卷積神經網絡

4月24日復盤 一、CNN 視覺處理三大任務&#xff1a;圖像分類、目標檢測、圖像分割 上游&#xff1a;提取特征&#xff0c;CNN 下游&#xff1a;分類、目標、分割等&#xff0c;具體的業務 1. 概述 ? 卷積神經網絡是深度學習在計算機視覺領域的突破性成果。在計算機視覺領…

【網絡原理】從零開始深入理解TCP的各項特性和機制.(三)

上篇介紹了網絡原理傳輸層TCP協議的知識,本篇博客給大家帶來的是網絡原理剩余的內容, 總體來說,這部分內容沒有上兩篇文章那么重要,本篇知識有一個印象即可. &#x1f40e;文章專欄: JavaEE初階 &#x1f680;若有問題 評論區見 ? 歡迎大家點贊 評論 收藏 分享 如果你不知道分…

解決qnn htp 后端不支持boolean 數據類型的方法。

一、背景 1.1 問題原因 Qnn 模型在使用fp16的模型轉換不支持類型是boolean的cast 算子&#xff0c;因為 htp 后端支持量化數據類型或者fp16&#xff0c;不支持boolean 類型。 ${QNN_SDK_ROOT_27}/bin/x86_64-linux-clang/qnn-model-lib-generator -c ./bge_small_fp16.cpp -b …

使用Three.js搭建自己的3Dweb模型(從0到1無廢話版本)

教學視頻參考&#xff1a;B站——Three.js教學 教學鏈接&#xff1a;Three.js中文網 老陳打碼 | 麒躍科技 一.什么是Three.js&#xff1f; Three.js? 是一個基于 JavaScript 的 ?3D 圖形庫&#xff0c;用于在網頁瀏覽器中創建和渲染交互式 3D 內容。它基于 WebGL&#xff0…

PostgreSQL WAL 冪等性詳解

1. WAL簡介 WAL&#xff08;Write-Ahead Logging&#xff09;是PostgreSQL的核心機制之一。其基本理念是&#xff1a;在修改數據庫數據頁之前&#xff0c;必須先將這次修改操作寫入到WAL日志中。 這確保了即使發生崩潰&#xff0c;數據庫也可以根據WAL日志進行恢復。 恢復的核…

git提交規范記錄,常見的提交類型及模板、示例

Git提交規范是一種約定俗成的提交信息編寫標準&#xff0c;旨在使代碼倉庫的提交歷史更加清晰、可讀和有組織。以下是常見的Git提交類型及其對應的提交模板&#xff1a; 提交信息的基本結構 一個標準的Git提交信息通常包含以下三個主要部分&#xff1a; Header?&#xff1a;描…

FastAPI系列06:FastAPI響應(Response)

FastAPI響應&#xff08;Response&#xff09; 1、Response入門2、Response基本操作設置響應體&#xff08;返回數據&#xff09;設置狀態碼設置響應頭設置 Cookies 3、響應模型 response_model4、響應類型 response_classResponse派生類自定義response_class 在“FastAPI系列0…

每日一題(小白)模擬娛樂篇33

首先&#xff0c;理解題意是十分重要的&#xff0c;我們是要求最短路徑&#xff0c;這道題可以用dfs&#xff0c;但是題目給出的數據是有規律的&#xff0c;我們可以嘗試模擬的過程使用簡單的方法做出來。每隔w數字就會向下轉向&#xff0c;就比如題目上示例的w6&#xff0c;無…

哈希封裝unordered_map和unordered_set的模擬實現

文章目錄 &#xff08;一&#xff09;認識unordered_map和unordered_set&#xff08;二&#xff09;模擬實現unordered_map和unordered_set2.1 實現出復用哈希表的框架2.2 迭代器iterator的實現思路分析2.3 unordered_map支持[] &#xff08;三&#xff09;結束語 &#xff08;…

Java學習-Java基礎

1.重寫與重載的區別 重寫發生在父子類之間,重載發生在同類之間構造方法不能重寫,只能重載重寫的方法返回值,參數列表,方法名必須相同重載的方法名相同,參數列表必須不同重寫的方法的訪問權限不能比父類方法的訪問權限更低 2.接口和抽象類的區別 接口是interface,抽象類是abs…

BG開發者日志0427:故事的起點

1、4月26日晚上&#xff0c;BG項目的gameplay部分開發完畢&#xff0c;后續是細節以及試玩版優化。 開發重心轉移到story部分&#xff0c;目前剛開始&#xff0c; 確切地說以前是長期擱置狀態&#xff0c;因為過去的四個月中gameplay部分優先開發。 --- 2、BG這個項目的起點…

頭歌實訓之游標觸發器

&#x1f31f; 各位看官好&#xff0c;我是maomi_9526&#xff01; &#x1f30d; 種一棵樹最好是十年前&#xff0c;其次是現在&#xff01; &#x1f680; 今天來學習C語言的相關知識。 &#x1f44d; 如果覺得這篇文章有幫助&#xff0c;歡迎您一鍵三連&#xff0c;分享給更…

【深度學習】多頭注意力機制的實現|pytorch

博主簡介&#xff1a;努力學習的22級計算機科學與技術本科生一枚&#x1f338;博主主頁&#xff1a; Yaoyao2024往期回顧&#xff1a;【深度學習】注意力機制| 基于“上下文”進行編碼,用更聰明的矩陣乘法替代笨重的全連接每日一言&#x1f33c;: 路漫漫其修遠兮&#xff0c;吾…