阅读视图
互联互通工具持续丰富,公募大力布局香港市场ETF
绩优基金一季报密集披露:多只基金规模大增,看多AI相关产业链
EIA称美国石油出口总量跃升至纪录高位
美国启动非法关税退款流程,已有上亿美元申请待处理
联合利华以448亿美元出售食品业务,并以12亿美元收购Grüns
欧盟委员会初步认定元宇宙平台公司调整政策后仍违规
特斯拉完成AI5自动驾驶芯片流片
小鹏GX上市,预售价39.98万元
OpenAI据悉计划推出ChatGPT广告新定价方案
美国财长贝森特不确定沃什能否在5月16日前得到提名确认
美国上周原油库存减少91.3万桶,市场预估为增加15.4万桶
美股三大指数收盘涨跌不一,纳指11连涨创收盘新高
ps, ai, ae插件都可以用html和js开发了
Adobe ScriptUI 学习之旅:从标记语言到跨应用交互
1. 缘起:对 Duik 插件的探索
作为 After Effects 用户,我一直对 Duik 这个强大的角色动画插件库充满好奇。Duik 提供了丰富的骨骼动画工具,极大地简化了角色动画的制作流程。然而,当我尝试深入学习它的代码结构时,却遇到了一些挑战:
- 文档收费:完整的 Duik 文档需要付费购买
- 代码结构问题:Duik 没有采用模块化设计,而是大量使用全局变量作为功能实现方式,导致代码可读性差,难以维护和扩展
- 学习曲线陡峭:对于初学者来说,理解其代码逻辑较为困难
2. 寻找官方文档的历程
为了更好地理解 Adobe 脚本开发,我开始从 After Effects 的帮助菜单中寻找相关文档。有趣的是,Adobe 官方并没有直接提供完整的 ScriptUI 文档,而是在帮助菜单的链接中指向了第三方资源:
- ScriptUI 文档:extendscript.docsforadobe.dev - 这是 Adobe 应用通用的 UI 构建工具
- After Effects 脚本指南:ae-scripting.docsforadobe.dev - 专注于 AE 相关的脚本开发
通过这些文档,我了解到 ScriptUI 不仅适用于 After Effects,还可以在其他 Adobe 应用中使用,并且支持跨应用交互,这为我的开发思路打开了新的大门。
3. 痛点:ScriptUI 构建的复杂性
在学习过程中,我发现 ScriptUI 的构建方式相对繁琐:
- 需要通过脚本代码手动创建每个 UI 组件
- 布局管理需要编写大量代码
- 修改 UI 结构时需要调整多处代码
Duik 虽然抽象了一些方法来简化这个过程,但对于我来说仍然不够直观。我开始思考:是否可以使用更简洁的标记语言来描述 UI,然后编译成 ScriptUI 代码?
4. 解决方案:标记语言到 ScriptUI 的转换
基于这个想法,我创建了一个代码仓库,实现了从 HTML 标记语言到 Adobe ScriptUI JSX 代码的转换。在开发初期,我考虑过使用 Vue Compiler 来处理标记语言,但后来发现 Vue 支持的特性和 HTML 差别较大,而且我们并不需要 HTML 的全部功能,因此最终决定自己实现一个轻量级的转换引擎。
核心功能
- HTML 标记语言支持:使用熟悉的 HTML 标签来描述 UI 结构
- 组件映射:将 HTML 标签映射到对应的 ScriptUI 组件
- 事件处理:支持内联 onClick 事件绑定
- 实时编译:通过文件监视器实时将 HTML 变更编译为 JSX
项目结构
项目采用 monorepo 结构,包含三个核心包:
- adobe-scriptui-html-to-jsx:核心转换引擎,负责将 HTML 标记转换为 ScriptUI JSX 代码
- adobe-scriptui-watcher:文件监视器,监视 HTML 文件变化并自动触发编译
- adobe-scriptui-ae-helper:示例应用,展示了如何使用该工具构建实际的 After Effects 助手工具
这种拆包设计的目的是为了模块化和可扩展性,现在主要用于 After Effects,但未来可以轻松扩展到其他 Adobe 应用中。
5. 实际应用:AE 助手工具
通过这个工具,我构建了一个功能丰富的 After Effects 助手工具,包含:
- 弹性表达式:为属性添加弹性动画效果
- 常用表达式:快速应用 loop、wiggle、random 等常用表达式
- AI 文件导入:支持从 Illustrator 导入文件到 After Effects
- 用户友好的界面:通过 HTML 标记语言快速构建和修改 UI
6. 未来展望
这个项目只是我对 Adobe 脚本开发探索的开始。未来,我计划:
业务调研与痛点解决
- 表达式管理系统:针对设计师使用表达式的痛点,开发一个更智能的表达式管理系统。目前设计师们通常需要从文本文件中复制粘贴表达式,修改参数困难,应用到其他属性时容易出错,最终只好放弃使用表达式而回到关键帧动画。
- 可视化表达式编辑器:开发一个可视化的表达式编辑器,让设计师可以通过界面调整参数,而不需要直接编写代码。
功能抽象
- 跨应用组件库:构建一套适用于所有 Adobe 应用的组件库
- 模板系统:创建可复用的 UI 模板
- 跨应用工作流:实现不同 Adobe 应用之间的无缝交互
开发流程优化
- AI 辅助开发:利用 AI 工具生成初始代码,然后根据文档进行调整
- 可视化编辑器:开发一个可视化 UI 编辑器,进一步简化开发流程
- 社区贡献:鼓励社区贡献,共同完善这个工具
7. 结语
通过对 Adobe ScriptUI 的学习和探索,我不仅解决了 UI 构建的痛点,还为 Adobe 脚本开发提供了一种新的思路。标记语言到 ScriptUI 的转换,使得 UI 开发变得更加直观和高效。
虽然这个项目还在不断发展中,但它已经展示了如何通过创新的方法来简化复杂的开发流程。我相信,随着更多功能的加入和社区的参与,这个工具将成为 Adobe 脚本开发者的有力助手,同时也能帮助设计师们更轻松地使用和理解表达式,提升动画制作的效率和质量。
提示:如果你也对 Adobe 脚本开发感兴趣,欢迎访问我的 GitHub 仓库 adobe-script-ui-helper,一起探索 Adobe 脚本开发的无限可能!
每日一题-距离最小相等元素查询🟡
给你一个 环形 数组 nums 和一个数组 queries 。
对于每个查询 i ,你需要找到以下内容:
- 数组
nums中下标queries[i]处的元素与 任意 其他下标j(满足nums[j] == nums[queries[i]])之间的 最小 距离。如果不存在这样的下标j,则该查询的结果为-1。
返回一个数组 answer,其大小与 queries 相同,其中 answer[i] 表示查询i的结果。
示例 1:
输入: nums = [1,3,1,4,1,3,2], queries = [0,3,5]
输出: [2,-1,3]
解释:
- 查询 0:下标
queries[0] = 0处的元素为nums[0] = 1。最近的相同值下标为 2,距离为 2。 - 查询 1:下标
queries[1] = 3处的元素为nums[3] = 4。不存在其他包含值 4 的下标,因此结果为 -1。 - 查询 2:下标
queries[2] = 5处的元素为nums[5] = 3。最近的相同值下标为 1,距离为 3(沿着循环路径:5 -> 6 -> 0 -> 1)。
示例 2:
输入: nums = [1,2,3,4], queries = [0,1,2,3]
输出: [-1,-1,-1,-1]
解释:
数组 nums 中的每个值都是唯一的,因此没有下标与查询的元素值相同。所有查询的结果均为 -1。
提示:
1 <= queries.length <= nums.length <= 1051 <= nums[i] <= 1060 <= queries[i] < nums.length
整洁架构三连问:是什么,怎么做,为什么要用
整洁架构问答
Q1:什么是整洁架构?
由 Robert C. Martin(Uncle Bob)提出,核心思想:业务逻辑独立于框架、UI、数据库等外部细节,依赖关系只能从外层指向内层。
四层结构
┌─────────────────────────────────┐
│ Frameworks & Drivers │ ← Web、数据库、UI(最易变)
│ ┌─────────────────────────┐ │
│ │ Interface Adapters │ │ ← 控制器、网关、Presenter
│ │ ┌───────────────────┐ │ │
│ │ │ Application │ │ │ ← 用例(Use Cases)
│ │ │ Business Rules │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ Enterprise │ │ │ │ ← 实体(Entities)最稳定
│ │ │ │ Business │ │ │ │
│ │ │ │ Rules │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └───────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
- 实体层:核心业务规则,零依赖
- 用例层:编排实体完成应用逻辑
- 接口适配层:数据转换,连接内外
- 框架与驱动层:具体技术实现
关键收益
- 可测试:业务逻辑不依赖外部,独立单元测试
- 可替换:换数据库/框架不影响核心逻辑
- 可维护:修改外层不影响内层
一句话:让业务逻辑成为核心,让数据库、框架等成为可插拔的细节。
Q2:整洁架构和普通架构有什么区别?
普通架构按技术类型分层,整洁架构按依赖方向分层。
直观对比
普通架构(传统分层)
src/
├── components/ ← UI 组件
├── api/ ← 接口调用
├── store/ ← 状态管理
├── utils/ ← 工具函数
└── views/ ← 页面
依赖方向:随意交叉
组件 ←→ store ←→ api
↕ ↕ ↕
utils ←─ views ←─ components
整洁架构
src/
├── domain/ ← 纯业务(零依赖)
├── application/ ← 用例编排
├── infrastructure/ ← 技术实现
└── ui/ ← 界面渲染
依赖方向:只从外向内
ui → application → domain
infrastructure → application → domain
↘ ↓ ↗
任何外层都可以依赖内层
内层永远不知道外层存在
具体差异对比
| 维度 | 普通架构 | 整洁架构 |
|---|---|---|
| 分层依据 | 按技术类型(组件、API、工具) | 按依赖方向(外层依赖内层) |
| 业务逻辑位置 | 散落在组件、store、utils | 集中在 domain 层 |
| 框架耦合 | 业务逻辑依赖 Vue/React | domain 层零框架依赖 |
| 改数据库 | 改 api/ + store/ + 组件 | 只改 infrastructure/ |
| 换框架 | 几乎重写 | 只重写 ui/ 层 |
| 单元测试 | 需要 mock 框架、mount 组件 | 直接测纯 TS 函数 |
| API 字段变了 | 改多处 | 只改 Repository 转换逻辑 |
代码对比:同一个需求
普通架构写法
// store/cart.ts
import axios from 'axios'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
async function addItem(productId: string) {
// 业务规则散落在 store 里
if (items.value.length >= 20) {
throw new Error('购物车已满')
}
// 直接耦合 axios
const res = await axios.post('/api/cart', { productId })
// 直接操作原始 JSON
items.value.push(res.data)
}
return { items, addItem }
})
<!-- CartPage.vue -->
<script setup>
import { useCartStore } from '@/store/cart'
const cartStore = useCartStore()
// 组件里也有业务逻辑
const canAdd = computed(() => cartStore.items.length < 20)
</script>
问题:业务规则在 store 和组件里都有,换框架全废,换 axios 要改 store。
整洁架构写法
// domain/aggregates/Cart.ts — 纯 TS,零依赖
export class Cart {
private items: CartItem[] = []
addItem(item: CartItem): void { /* 规则在这里 */ }
canAddMore(): boolean { return this.items.length < 20 }
}
// application/usecases/AddToCartUseCase.ts — 编排
export class AddToCartUseCase {
constructor(private repo: CartRepository) {}
async execute(id: string) {
const cart = await this.repo.getCart()
cart.addItem(item) // 调领域方法
await this.repo.save(cart) // 持久化
}
}
// infrastructure/repositories/ApiCartRepository.ts — 适配
export class ApiCartRepository implements CartRepository {
async getCart() { /* axios → 领域对象 */ }
async save(cart) { /* 领域对象 → axios */ }
}
<!-- ui/pages/CartPage.vue — 只渲染 -->
<script setup>
const { addItem } = useCart() // composable 调用用例
</script>
好处:业务规则只在 Cart.ts,换框架/换 axios 都不影响它。
什么时候用哪个
| 场景 | 建议 |
|---|---|
| 简单 CRUD、几个页面 | 普通架构够用,别过度设计 |
| 中等复杂度、有业务规则 | 提取 domain 层,用 composable 做应用层 |
| 复杂业务(交易、审批、计费) | 完整整洁架构 + DDD |
一句话总结:普通架构按技术分文件夹,整洁架构按依赖方向分层。前者简单但业务逻辑散落各处,后者前期成本高但业务逻辑内聚、可替换、可测试。
Q3:目前 Vue CLI 是不是都是整洁架构?
不是。 Vue CLI 生成的项目默认不符合整洁架构。
Vue CLI 默认结构
src/
├── components/ ← 组件(UI + 业务逻辑混在一起)
├── views/ ← 页面(UI + 业务逻辑混在一起)
├── router/ ← 路由
├── store/ ← Vuex/Pinia(状态管理)
├── api/ ← API 调用
├── utils/ ← 工具函数
└── App.vue
为什么不是整洁架构
| 整洁架构要求 | Vue CLI 默认 | 问题 |
|---|---|---|
| 依赖从外指向内 | 各层互相 import,方向混乱 | 无依赖规则约束 |
| 领域层纯 TS,零框架依赖 | 业务逻辑写在 .vue 里 |
领域层耦合了 Vue |
| 用例层编排领域对象 | 没有 use case 层 | 业务逻辑散落在组件和 store |
| Repository 接口在领域层定义 | 直接在组件里调 axios | 数据获取和 UI 耦合 |
典型 Vue 组件的问题:
<script setup>
// ❌ UI、业务规则、API 调用全混在一起
import axios from 'axios'
const cart = ref([])
async function addItem(id) {
if (cart.value.length >= 20) { // 业务规则在组件里
alert('购物车已满')
return
}
const res = await axios.post('/api/cart', { id }) // 直接调 API
cart.value.push(res.data)
}
</script>
Vue CLI 能改成整洁架构吗
能,但需要手动改造,Vue CLI 不会自动帮你分层:
src/
├── domain/ ← 手动添加:纯 TS 领域层
│ ├── value-objects/
│ ├── entities/
│ ├── aggregates/
│ └── repositories/ ← 接口定义
├── application/ ← 手动添加:用例层
│ ├── usecases/
│ └── composables/
├── infrastructure/ ← 手动添加:适配层
│ ├── repositories/ ← Repository 实现
│ └── api/
├── ui/ ← 原 components/views,只负责渲染
│ ├── components/
│ └── pages/
├── router/
└── App.vue
一句话总结
Vue CLI 是脚手架工具,只管项目初始化和构建配置,不管架构分层。默认生成的项目是传统分层(按技术类型分),不是整洁架构(按依赖方向分)。整洁架构需要开发者自己设计和实施。
前端整洁架构详解
前端整洁架构详解
以电商购物车系统为例,逐层讲解整洁架构在前端中的落地方式。
架构分层总览
┌──────────────────────────────────┐
│ UI 框架层(Vue/React 组件) │ ← 最外层:页面、组件
│ ┌────────────────────────────┐ │
│ │ 接口适配层 │ │ ← Store/Composable、API 适配、Repository 实现
│ │ ┌────────────────────┐ │ │
│ │ │ 应用层 │ │ │ ← 用例:编排领域逻辑
│ │ │ ┌────────────┐ │ │ │
│ │ │ │ 领域层 │ │ │ │ ← 纯 JS/TS:实体、值对象、规则
│ │ │ └────────────┘ │ │ │
│ │ └────────────────────┘ │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
依赖规则:依赖关系只能从外层指向内层,内层完全不知道外层的存在。
1. 领域层(纯 JS/TS,零依赖)
最内层,不引入任何框架,不 import Vue/React,不 import axios,纯业务逻辑。
1.1 值对象 — 封装校验和不可变概念
// domain/value-objects/Money.ts
export class Money {
constructor(private amount: number, private currency: string = 'CNY') {
if (amount < 0) throw new Error('金额不能为负数');
}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('币种不同');
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
get value(): number { return this.amount; }
toString(): string { return `${this.currency} ${this.amount.toFixed(2)}`; }
}
// domain/value-objects/Email.ts
export class Email {
constructor(private value: string) {
if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value)) {
throw new Error('邮箱格式不合法');
}
}
get domain(): string { return this.value.split('@')[1]; }
toString(): string { return this.value; }
}
为什么用值对象? 校验逻辑内聚,不会散落在各个组件里。任何地方拿到一个 Money,就保证它是合法的。
1.2 实体 — 有标识,有生命周期
// domain/entities/CartItem.ts
import { Money } from '../value-objects/Money';
export class CartItem {
constructor(
public readonly productId: string, // 唯一标识
public readonly name: string,
private unitPrice: Money,
private quantity: number,
) {
if (quantity <= 0) throw new Error('数量必须大于0');
}
get totalPrice(): Money {
return this.unitPrice.multiply(this.quantity);
}
changeQuantity(newQty: number): CartItem {
return new CartItem(this.productId, this.name, this.unitPrice, newQty);
}
}
1.3 聚合根 — 保证一致性边界
// domain/aggregates/Cart.ts
import { Money } from '../value-objects/Money';
import { CartItem } from '../entities/CartItem';
export class Cart {
private items: CartItem[] = [];
addItem(item: CartItem): void {
const existing = this.items.find(i => i.productId === item.productId);
if (existing) {
// 已存在则合并数量
this.items = this.items.map(i =>
i.productId === item.productId
? i.changeQuantity(i.quantity + item.quantity)
: i
);
} else {
this.items.push(item);
}
}
removeItem(productId: string): void {
this.items = this.items.filter(i => i.productId !== productId);
}
get total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.totalPrice),
new Money(0),
);
}
get itemCount(): number {
return this.items.length;
}
// 业务规则:购物车最多 20 件商品
canAddMore(): boolean {
return this.items.length < 20;
}
}
关键点:所有业务规则都在这里——最多 20 件、合并同类商品、金额计算。组件不需要知道这些规则。
1.4 Repository 接口(领域层定义)
// domain/repositories/CartRepository.ts
import { Cart } from '../aggregates/Cart';
export interface CartRepository {
getCart(): Promise<Cart>;
save(cart: Cart): Promise<void>;
clear(): Promise<void>;
}
接口在领域层定义,实现在适配层——这是依赖反转的核心,让领域层不依赖具体的数据获取方式。
2. 应用层(用例 — 编排领域对象)
这一层只做编排,不包含业务规则本身。它协调领域对象完成一个完整操作。
2.1 添加商品到购物车
// application/usecases/AddToCartUseCase.ts
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { CartRepository } from '../../domain/repositories/CartRepository';
export class AddToCartUseCase {
constructor(private cartRepo: CartRepository) {}
async execute(productId: string, name: string, price: number, qty: number): Promise<Cart> {
// 1. 从仓库获取当前购物车
const cart = await this.cartRepo.getCart();
// 2. 业务规则检查(规则在领域对象里,用例只调用)
if (!cart.canAddMore()) {
throw new Error('购物车已满,最多 20 件商品');
}
// 3. 创建领域对象
const item = new CartItem(productId, name, new Money(price), qty);
// 4. 执行领域操作
cart.addItem(item);
// 5. 持久化
await this.cartRepo.save(cart);
return cart;
}
}
2.2 结算下单
// application/usecases/CheckoutUseCase.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { OrderRepository } from '../../domain/repositories/OrderRepository';
import { Order } from '../../domain/aggregates/Order';
export class CheckoutUseCase {
constructor(
private cartRepo: CartRepository,
private orderRepo: OrderRepository,
) {}
async execute(userId: string): Promise<Order> {
const cart = await this.cartRepo.getCart();
if (cart.itemCount === 0) {
throw new Error('购物车为空,无法下单');
}
// 领域逻辑:创建订单
const order = Order.createFromCart(cart, userId);
await this.orderRepo.save(order);
await this.cartRepo.clear();
return order;
}
}
用例层的特点:读数据 → 调领域方法 → 存数据。像导演一样编排,但不自己写业务规则。
3. 接口适配层(Store/Composable + API 适配)
3.1 Repository 实现(JSON ↔ 领域对象转换)
// infrastructure/repositories/ApiCartRepository.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { cartApi } from '../api/CartApi';
export class ApiCartRepository implements CartRepository {
async getCart(): Promise<Cart> {
// API 返回的是原始 JSON,需要转换成领域对象
const raw = await cartApi.fetchCart();
const cart = new Cart();
for (const item of raw.items) {
cart.addItem(new CartItem(item.productId, item.name, new Money(item.price), item.qty));
}
return cart;
}
async save(cart: Cart): Promise<void> {
// 领域对象 → 原始 JSON,给 API
const payload = {
items: cart.items.map(i => ({
productId: i.productId,
name: i.name,
price: i.unitPrice.value,
qty: i.quantity,
})),
};
await cartApi.updateCart(payload);
}
async clear(): Promise<void> {
await cartApi.clearCart();
}
}
关键:API 返回的 JSON → 领域对象的转换在这里完成。领域层永远不碰原始 JSON。
3.2 API 层(最外层细节)
// infrastructure/api/CartApi.ts
import axios from 'axios';
export const cartApi = {
fetchCart: () => axios.get('/api/cart').then(r => r.data),
updateCart: (data: any) => axios.put('/api/cart', data).then(r => r.data),
clearCart: () => axios.delete('/api/cart').then(r => r.data),
};
3.3 Composable(连接用例和 UI)
// application/composables/useCart.ts
import { ref } from 'vue';
import { AddToCartUseCase } from '../usecases/AddToCartUseCase';
import { ApiCartRepository } from '../../infrastructure/repositories/ApiCartRepository';
const cartRepo = new ApiCartRepository();
const addToCartUseCase = new AddToCartUseCase(cartRepo);
export function useCart() {
const cart = ref<Cart | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
async function addItem(productId: string, name: string, price: number, qty: number) {
loading.value = true;
error.value = null;
try {
cart.value = await addToCartUseCase.execute(productId, name, price, qty);
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
return { cart, loading, error, addItem };
}
4. UI 框架层(组件 — 只负责渲染)
<!-- ui/components/ProductCard.vue -->
<script setup lang="ts">
import { useCart } from '@/application/composables/useCart';
const { addItem, loading, error } = useCart();
function handleAdd() {
addItem(product.id, product.name, product.price, 1);
}
</script>
<template>
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>¥{{ product.price }}</p>
<button :disabled="loading" @click="handleAdd">
加入购物车
</button>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
组件只做三件事:展示数据、捕获用户操作、调用用例。零业务逻辑。
依赖关系总览
ProductCard.vue ← 只渲染,零业务逻辑
↓ 调用
useCart() ← 状态管理 + 调用用例
↓ 调用
AddToCartUseCase ← 编排领域对象
↓ 使用
Cart / CartItem / Money ← 纯业务规则
↓ 通过接口
CartRepository (接口) ← 领域层定义接口
↓ 实现
ApiCartRepository ← 适配层实现,转换 JSON ↔ 领域对象
↓ 调用
cartApi (axios) ← 最外层:HTTP 请求细节
箭头方向 = 依赖方向,全部从外指向内。内层完全不知道外层存在。
项目目录结构
src/
├── domain/ # 领域层(纯 TS,零框架依赖)
│ ├── value-objects/
│ │ ├── Money.ts
│ │ └── Email.ts
│ ├── entities/
│ │ └── CartItem.ts
│ ├── aggregates/
│ │ ├── Cart.ts
│ │ └── Order.ts
│ └── repositories/
│ ├── CartRepository.ts # 接口定义
│ └── OrderRepository.ts
│
├── application/ # 应用层(用例编排)
│ ├── usecases/
│ │ ├── AddToCartUseCase.ts
│ │ └── CheckoutUseCase.ts
│ └── composables/
│ └── useCart.ts
│
├── infrastructure/ # 接口适配层
│ ├── repositories/
│ │ └── ApiCartRepository.ts # Repository 实现
│ └── api/
│ └── CartApi.ts # axios 调用
│
├── ui/ # UI 框架层
│ ├── components/
│ │ └── ProductCard.vue
│ └── pages/
│ └── CartPage.vue
这样做的好处
| 场景 | 传统做法 | 整洁架构 |
|---|---|---|
| 换框架 Vue→React | 重写所有业务逻辑 | 只重写 UI 层,领域层直接复用 |
| API 字段名变了 | 改几十个组件 | 只改 ApiCartRepository 的转换逻辑 |
| 加新业务规则 | 在组件里到处加 if | 在领域对象里加,组件无感知 |
| 单元测试 | mock axios、mount 组件 | 直接测 Cart.addItem(),纯函数测试 |
| 后端复用 | 不可能 | 领域层是纯 TS,Node 端可直接引用 |
实际项目中的权衡
不是所有前端项目都需要完整分层:
| 项目规模 | 建议做法 |
|---|---|
| 简单页面/CRUD | 分离 API 调用和 UI 即可,不必过度设计 |
| 中等复杂度 | 提取领域层(纯 TS),用 Composable 做应用层 |
| 复杂业务系统 | 完整分层 + DDD 建模(如交易系统、审批流) |
核心原则
领域层必须是纯 JS/TS,不依赖任何框架。 这样你的业务逻辑可以:
- 在 Vue 和 React 之间迁移
- 在 Node 后端复用(同构)
- 独立写单元测试,不需要 mock 组件
框架是细节,业务逻辑才是核心。
我花一天时间Vibe Coding的开源AI工具,一键检测你的电脑能跑哪些AI大模型
最近一直在深耕 AI Agent 与大模型应用,比如 JitKnow AI 知识库、JitWord协同AI文档、Pxcharts 超级表格,同时也持续在给大家分享 GitHub 上真正能落地、能解决实际问题的优质AI开源项目。
今天和大家分享一款我花了一天时间做的开源工具——ai-detector。
它是一款完全运行在浏览器端的免费硬件检测工具。能自动读取你电脑的内存、CPU、GPU 信息,智能匹配 21+ 主流开源 AI 大模型的兼容性,并给出本地运行速度预估,帮你在 5 秒内找到最适合自己电脑的 AI 模型。
无需安装、无需登录、无需上传任何数据,全程 100% 本地运行。
ps:最近小龙虾很火,但是又担心自己电脑配置不够的朋友,可以使用这款线上工具检测一下电脑适合哪些模型,告别AI恐惧啦~老规矩,先上开源地址。
github:github.com/MrXujiang/a…
下面就和大家详细分享一下这款 AI Coding 出来的开源项目。
先上一个基础的功能演示比如我想在我的电脑里部署一个本地AI模型,但是又担心我的电脑配置不够,那么直接运行这个项目:
点击开始检测, 不到5s,就会给出自己电脑的性能和适合运行哪些模型的详细报告:
分析的非常准确,可能是我电脑年久失修,只给出了28分。。。ai-detector 还会为我们推荐基于当前电脑,适合运行的模型推荐:
不仅如此,它还会对目前主流的数十个开源模型,对当前电脑进行分析评测,分析出部署这些大模型的性能,风险等信息,如下:
对于比较吃电脑性能的模型,它会给我们全面的分析:
最后会基于我们的硬件配置,估算各模型生成速度(tokens/秒),并输出可视化的分析报表:
核心能力总结
下面和大家总结一下 ai-detector 的核心能力和亮点。
- 硬件自动检测
-
系统内存通过
navigator.deviceMemory读取 RAM 大小 -
CPU 核心数通过
navigator.hardwareConcurrency读取逻辑核数 -
GPU 型号通过 WebGL
WEBGL_debug_renderer_info扩展识别显卡 - 综合跑分基于内存与 CPU 计算 0-100 综合评分,直观了解你的 AI 能力等级
2. 21+ 大模型兼容性分析
覆盖当前最主流的开源大模型系列,一键发现哪些能跑、哪些跑不动:
| 系列 | 代表模型 | 参数规模 |
|---|---|---|
| 🦙 Llama | TinyLlama、Llama 3.2、Llama 3.1 | 1.1B ~ 70B |
| 🌐 Qwen | Qwen2.5 3B/7B/14B/32B/72B | 3B ~ 72B |
| 💎 Phi | Phi-3 Mini、Phi-3 Medium | 3.8B ~ 14B |
| 🌬️ Mistral | Mistral 7B | 7B |
| 🧠 DeepSeek | DeepSeek-R1、DeepSeek Coder | 7B ~ 70B |
| 👁️ 多模态 | LLaVA 7B、MiniCPM-V 8B | 7B ~ 8B |
| 💫 Gemma | Gemma 2 2B | 2B |
- 支持兼容性三级分类
- 😊 流畅运行内存充裕,可稳定高速推理
- ⚠️ 勉强运行:内存刚好满足,速度偏慢
- ❌ 内存不足:当前配置无法加载该模型
- 运行速度排行
检测完成后自动生成可运行模型的速度排行榜,按 tokens/秒从高到低排列,帮你优先选出响应最快的模型。
- 量化模式切换
- Q4 量化内存占用更低,普通设备首选
- Q8 量化精度更高,内存需求约为 Q4 的 2 倍
- 一键切换,实时刷新所有模型兼容状态
-
个性化模型推荐基于我们的硬件配置,自动从模型库中精选 最均衡、速度最快、能力最强 三款推荐,省去选择烦恼。
-
一键复制检测报告
生成包含硬件配置、综合评分、可运行模型的文本报告,方便分享或咨询。
完整使用流程总结
为了让大家轻松上手使用,我总结了一下7步使用法,大家可以参考一下:
- 打开页面 → 点击「开始硬件检测」按钮
- 等待扫描(约 3~5 秒)→ 自动完成内存、CPU、GPU 检测
- 查看结果 → 获得综合跑分与设备等级评价
- 浏览推荐 → 查看「为你推荐」区块,获取最适合你的 3 款模型
- 筛选模型 → 在「模型列表」中按兼容状态、类型筛选,支持关键词搜索
- 切换量化 → 尝试切换 Q4 / Q8 量化,查看内存需求变化
- 复制报告 → 一键复制检测结果,方便保存或分享
为什么要做这个开源项目这里分析一张图,大家就知道了:
| 特性 | AI -Detector | 其他工具 |
|---|---|---|
| 需要安装 | ❌ 无需安装 | ✅ 通常需要 |
| 需要登录 | ❌ 无需注册 | ✅ 通常需要 |
| 数据上传 | ❌ 完全不上传 | ⚠️ 部分上传 |
| 模型覆盖 | ✅ 21+ 主流模型 | ⚠️ 覆盖有限 |
| 速度预估 | ✅ tokens/s 量化估算 | ⚠️ 通常无此功能 |
| 升级建议 | ✅ 智能提示内存升级方案 | ❌ 无 |
| 量化切换 | ✅ Q4 / Q8 实时切换 | ❌ 无 |
| 开源免费 | ✅ MIT 协议 | ⚠️ 多数收费 |
主要是为了让任何没有技术基础的人,也能轻松拥有专业级AI模型选型能力,告别AI焦虑。
目前已开源,大家可以免费使用:
github:github.com/MrXujiang/a…
DEMO演示地址:jitword.com/ai-detector