跳到主要内容

项目二:通用框架

公共逻辑

npm / hook 封装

异常链路 npm 包

商业产品的核心链路中缺乏完善的监控机制。当出现问题直接展示兜底页,同时无法准确的监控到哪些用户触发了这个问题。

异常链路监控,对购买页的核心流程,接口、兜底页面展示增加统一格式的监控:

  • 接口异常状态码监控
  • 业务逻辑异常监控
  • 异常兜底页面曝光监控

风控拦截 npm 包

对于购买下单时存在的多种拦截和信息展示,封装为 npm 包方便扩展 / 下线处理。

  • 形式:toast、弹窗;
  • 文本:文字、图片;
  • 交互:按钮关闭、按钮跳转(h5 跳转、原生跳转);

Hook 封装

  • usePackages 定价、useBuy 购买、useFreePick 免费领取、useResult 结果查询;

共同的设计模式:

  1. 状态管理:使用  Pinia store  管理全局状态;在  hook 内部 refs 管理局部状态;
  2. 错误处理:集中错误报告机制  reportError:异常链路。
    • 提供降级体验和错误反馈:Toast + 跳转。
  3. 回调通式:使用回调函数 or Promise(async) 进行通知;标准化结构,state + data;
  4. 数据转换:统一的 API 接口请求、响应数据、业务模型的转换。
    • UI 与业务状态分离,状态结果返回给 view 层,view 进行 UI 拼装。
  5. 无生命周期:尽量不在 hook 中调用生命周期,而是暴露方法,在 view 层调用,防止隐成逻辑

差异化的内容:

  • usePackages 拉定价:决定购买页的初始化过程。
    • 定价拉取后,有的商业产品会根据定价结构,返回不同版本前端样式;
  • useResult 结果获取:决定购买页的结果。
    • 轮训机制,会先获取一次结果,然后最多 15 秒轮询获取;
    • 不同商业产品返回的结构数据结构,渲染 UI 不同,需要定制。

iVersion

组内前端业务工程中存在自有封装的 npm 包,这些 npm 包在多个业务工程中引用。为了更好地管理和追踪这些私有 npm 包的版本情况,需要开发一个工具来收集每个业务工程引用的 npm 包版本信息。这个工具将帮助技术团队了解各个业务工程当前使用的 npm 包版本情况,从而更好地在 npm 包版本进行大版本迭代时,进行维护和升级。

实现方式

  1. 打标库:@58fe/npm-track-scan
  2. 收集库:@58fe/npm-track-usage
  3. 采集脚本核心指令:npm ls @58fe/npm-track-scan -j
  4. 核心有两个库,打标库和采集库:
    • 打标库:标识这个库是需要被采集,被采集的 npm 包需要安装这个打标库,实际上这个库只是个标记不可以被 tree-shake 空库;
    • 采集库:是在前端项目打包后,执行某个脚本(npm ls 打标库名 -j),可以观察到当前项目使用了哪些装有打标库的 npm 包,收集他们的版本
  5. 执行脚本后会收集的字段有:
    • 项目名称(云效中的集群名或者 git 项目名)、项目 git 地址
    • 代码提交人、最后更新时间
    • 需要采集的 npm 库的名称
  6. 在项目上线时,执行 build 之前会先执行脚本,收集字段后会上传至服务器中,做到版本管理。

其他思路:

  • 埋点采样,通过埋点信息采样,只能看到分布概率,无法证实当前工程的具体版本;
  • 收集库全量上报:存在网络/ 服务器处理压力,以及业务工程依赖包的隐私问题,不利推广;

Monorepo 结构

整体结构

使用 Yarn Workspaces 管理的 monorepo 项目

  • app / pc:移动 / 桌面端页面,他们依赖 server 包。
  • server:公共服务,API 接口定义、Composables 组合式函数(hook)、共享 Store、util 工具;
  • 统一脚本:在根目录 package.json 中,定义:build 命令,一次打多个子包;

包管理工具

pnpm / Lerna / Yarn

  • 依赖提升:自动提升依赖到根 node_modules 中,避免多个子包安装同一个版本额依赖;
  • 扁平化 node_modules 结构:减少多层嵌套,尽量提升到根目录中;
  • 解决 "幽灵依赖" 问题:当一个包没有显式声明依赖,却因为别的包恰好装了,就能正常使用这个依赖。
    • 升级依赖行为不一致,测试通过但生产挂了。禁止不声明就使用,强制依赖。

打包和依赖管理

  1. 根目录执行 yarn build 触发所有子包的 build 脚本;
  2. app 和 pc 包都使用 Vue CLI 进行打包,各自有独立的 vue.config.js 配置;
  3. 依赖链接:子包对 server 包有依赖声明(package.json)中。当一个 package 依赖另一个 package 时,Yarn 会创建一个符号链接(symlink);
  4. 依赖追踪:子包对 server 中暴露的功能有 import 导入,那么在子包 build 时,依赖构建过程中会追踪到 server 中真实的源代码位置;
  5. 一起打包:webpack 会将 server 包中的相关代码一起打包到最终的 bundle 中;
    • server 包完全不需要单独构建。

monorepo 依赖管理

如何确定哪些依赖 npm 包应该放在根目录中,还是放在子包中呢?

共享开发工具,放在根目录中:

  • 代码规范工具:ESLint, Prettier, Husky 等;
  • 构建相关工具:concurrently, patch-package 等;
  • 提交规范工具:commitizen, commitlint 等;
  • 避免版本冲突的核心库:Vue、React、lodash、pinia 等;

放在子包的依赖:

  • 特定的功能:比如 M/PC 端使用不同的 UI 库;
  • 运行时依赖:
    • fetch-s 接口调用只在 server 层中使用;
    • echarts 只在 pc 中使用;

项目逻辑共享方法

方案一:单一工程内的组件 / Hook 共享

  • 优势:
    • 开发便捷,直接复用代码,能在工程中直接看到代码逻辑,维护与调试成本低;
    • 逻辑调整风险小,不同工程间有各自的备份,调整可以独立调整,即使有问题也不影响其他产品;
    • 对于不同工程而言,如果整体有 80% 逻辑相似,但 20% 逻辑需要定制,好扩展。
      • 比如商业产品的:拉定价 usePackages、下单 useBuy、免费领取 useFreePick、购买结果 useResult 这四个 hook。整体逻辑近似,但每个产品也有自身定制的部分。
  • 局限:
    • 仅限同仓库内使用,难以跨项目共享,依赖复制粘贴;需要更新通用逻辑时,迭代成本高;
    • 与业务代码耦合,通用性与复用度受限,没有良好的抽象逻辑;
    • 代码膨胀,每一个工程都有这一份相似的代码。

方案二:Monorepo 工程的 server 层

  • 优势:
    • 相比方案一,更相似的几个工程可以抽离 server 层,统一管理 hook 逻辑,共享配置和依赖,版本一致性更好。
  • 局限:
    • 工程构建相对复杂,不适合小工程使用,多人开发时需注意各自边界。
    • server 层和 view 层之间需增加数据通信。且有公共数据在 server,私有数据在 view,增加开发成本,项目体积难免增大。

方案三:NPM 包共享

  • 优势:
    • 版本管理规范,发布流程成熟,依赖管理方便;
    • 更适合沉淀稳定、抽象和复用度高,以及迭代相对平缓的通用能力;
    • 适合:多人、多项目需要长期维护的基础库、工具包、UI 组件库;
  • 局限:
    • 私有包管理,版本管理,灰度控制,npm 包发布和代码分支合并等,需要规范约束;
    • 版本更新时,具体业务工程也需更新 npm 包版本,然后重新发布才可同步升级;

方案四:SDK

  • 优势:
    • 更抽离具体业务,完全脱离具体框架,封装为 js 代码,易于跨端复用;
    • 通过 CDN 部署,可供项目直接引入;
    • 更新迭代、版本灰度管理,控制在 SDK 开发方,更好管理;
    • API 统一,文档化落地,更好推广;
  • 局限
    • 封装成本高,需要抽象设计良好;升级需统一控制版本,防止项目版本碎片化;
    • 失去了框架生命周期能力,额外适配需要增加成本;

方案五:Module Federation

  • 优势:
    • 运行时动态共享,支持应用间独立部署与版本独立;
    • 无需单独发布包,实时加载远程模块,提升部署灵活性;
  • 局限:
    • 最大问题:需要高版本管理器,比如 Webpack5,生态支持不完善;
    • 发布更新流程长,配置复杂,排查问题成本较高;

Svelte SDK

webpack 技术选型

  1. 构建一致性
    • Webpack 在开发环境和生产环境使用相同的构建流程,仅配置参数不同;
    • Vite 开发时使用原生 ES 模块(无 bundle),而生产构建使用 Rollup 打包,存在本质差异;
    • 对于 SDK 开发,一致性可以减少 "开发正常但生产异常" 的问题;
  2. UMD 输出支持
    • Webpack 对 UMD 构建有成熟支持,配置简单;Vite 主要针对 ESM,UMD 需配置打包插件
    • UMD 格式发布,以支持多种加载方式,保证兼容性,就是体积略大;
      • IIFE:浏览器支持,不符合 node 规范,需要兼容;
      • ESM:不支持 IE,node 12 支持;体积最小。

使用的 loader

  • babel-loader:ES6+ 代码转化为 ES5,配合 preset-env 等可以按需引入 polyfill;
  • svelte-loader-hot:框架。热更新 + svelte 组件转移为 Js 原生;
  • url-loader:< 8kb 图片转换为 base64 编码内联,减少 HTTP 请求;

CSS(按序):

  • style-loader:将 CSS 通过 <style> 标签注入到 DOM 中
  • css-loader:解析 CSS 文件中的  @import  和  url()  语句,允许 js 引入 css 文件;
  • postcss-loader:PostCSS 处理,自动增加浏览器前缀,px 转 vw 单位;
  • sass-loader:SCSS 转为 CSS;

使用的 plugin

  1. CleanWebpackPlugin:构建前清理 dist 目录;
  2. HtmlWebpackPlugin:基于  public/index.html  模板生成最终 HTML,并自动注入打包后的  JS;
  3. TerserPlugin:压缩和混淆 JavaScript 代码。生产环境下使用,移除注释、优化体积;
  4. BundleAnalyzerPlugin:生成包体积分析报告,可视化依赖关系和大小,帮助优化包体积;

打包执行流程

  1. 初始化阶段
    • 读取配置,准备插件和编译环境;
    • CleanWebpackPlugin  先执行,清理输出目录;
  2. 构建模块阶段
    • 从  entry (src/main.js) 开始,递归解析依赖;
    • 遇到  .js 文件时调用 babel-loader;
    • 遇到 .svelte 文件时先调用 svelte-loader-hot,再调用 babel-loader;
    • 遇到 .scss/.css 文件时按顺序调用: sass-loader → postcss-loader → css-loader → style-loader;
    • 遇到图片文件时调用 url-loader;
  3. 优化阶段
    • Tree-Shaking 删除多余无用的代码;
    • TerserPlugin 执行代码压缩和混淆;
  4. 输出阶段
    • HtmlWebpackPlugin 生成 HTML  文件并注入脚本;
    • 输出最终打包文件  live-business-list.js;
    • BundleAnalyzerPlugin (分析模式下) 生成包分析报告;

Svelte SDK 对比 Vue / React

  • 1. 无运行时,打包体积小。Svelte 是编译时框架,简单组件的 SDK 体积通常更小(如 2KB+);在主项目对体积敏感的场景使用
  • 2. 不侵入宿主框架。Svelte 编译后是纯 JS,无需宿主框架支持,避免与 Vue/React 冲突,适配多框架,适合不同技术背景团队接入
  • 3. 性能优秀。编译期优化,无虚拟 DOM,渲染与更新性能优于 React/Vue,适合 SDK 场景中动态渲染、控制 DOM 时的性能诉求。
  • 4. 部署简单。不依赖运行时,CDN 注入直接运行,不关心框架生命周期。

SDK 优势:

  1. 版本和灰度控制放在 SDK 开发者手中;
  2. 封装好的 API 和接入文档,方便业务工程直接导入。

SDK 局限:开发流程长,问题排查成本高。

❌ 劣势说明
生态不完善Svelte 生态小,周边库、开发插件不及 Vue/React,如状态管理、UI 组件库有限。
团队引入有学习成本团队熟悉 Vue/React,Svelte 使用门槛相对高,后续维护需学习成本,定制开发规范。
源码可读性与调试能力弱编译后代码不直观,SDK 出现问题时排查不如 Vue/React 直观。
局部功能适配难若宿主项目是 Vue/React,基本无法复用其状态管理、生命周期。

具体业务为什么用:

道具折扣卡、优惠券、商业产品列表

  • 通用功能。这三个功能相比承接的业务宿主,抽象度更高,整体逻辑自洽,适合封装;
  • 迭代节奏。功能迭代节奏更慢,更接近基础工程。
  • 样式风格。样式风格不依赖具体业务模块,而是通用 UI 设计。
  • 跨团队使用。提供底层服务,跨不同技术背景团队使用,高效接入 SDK,且不依赖宿主框架。
  • 高效接入。存量工程可以快速接入,无需关心实现原理,仅关注封装好的 API 和接入文档。
  • 功能简单。每个模块组件和功能相对简单,通常只有 3-5 个组件,对外 API 也控制在 5 个以内,即使 Svelte 没有成熟 UI 组件库,自行开发的成本也相对较低。