[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.mzph.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;吾…