“整洁架构”和商家前端的重构之路|得物技术

发表于 2年以前  | 总阅读数:404 次

1. 背景

团队归属于后方业务支撑部门,组内的项目都以pc中后台应用为主。对比移动端应用,代码库比较庞大,业务逻辑也相对复杂。在持续的迭代过程中,我们发现当前的代码仓库仍然有不少可以优化的点:

  • 可以减弱对ui框架的依赖

21年前端平台决定技术栈统一迁移到React生态,后续平台的基础建设也都围绕React展开,这就使得商家使用Vue生态做开发的系统面临技术栈迁移的难题,将业务逻辑和UI框架节藕变得异常重要。

  • 代码风格可以更加统一

随着代码量和团队成员的增加,应用里风格迥异的代码也越来越多。为了能够持续迅速的进行迭代,团队急需一套统一的顶层代码架构设计方案。

  • 可以集成自动化测试用例

随着业务变得越来越复杂,在迅速的迭代过程中团队需要频繁地对功能进行回归,因此我们对于自动化单测用例的诉求也变的越来越强烈。

为了完成以上的优化,四组对现有的应用架构做了一次重构,而重构的核心就是整洁架构。

2. 整洁架构(The Clean Architecture)

整洁架构(The clean architecture)是由 Robert C. Martin (Uncle Bob)在2012年提出的一套代码组织的理念,其核心主要是依据各部分代码作用的不同将其拆分成不同的层次,在各层次间制定了明确的依赖原则,以达到以下目的:

1 . 与框架无关:无论是前端代码还是服务端代码,其逻辑本身都应该是独立的,不应该依赖于某一个第三方框架或工具库。一套独立的代码可以把第三方框架等作为工具使用。

2 . 可测试:代码中的业务逻辑可以在不依赖ui、数据库、服务器的情况下进行测试

3 . 和ui无关:代码中的业务逻辑不应该和ui做强绑定。比如把一个web应用切换成桌面应用,业务逻辑不应该受到影响。

4 . 和数据库无关:无论数据库用的是mysql还是mongodb,无论其怎么变,都不该影响到业务逻辑。

5 . 和外部服务无关:无论外部服务怎么变,都不影响到使用该服务的业务逻辑。

为了实现以上目的,整洁架构把应用划分成了entities、use cases、interface adapters(MVC、MVP等)、Web/DB等至少四层。这套架构除了分层之外,在层与层之间还有一个非常明确的依赖关系,外层的逻辑依赖内层的逻辑

Entity

entities封装了企业级的业务逻辑和规则。entities没有什么固定的形式,无论是一个对象也好,是一堆函数的集合也好,唯一的标准就是能够被企业的各个应用所复用。

Use Case

entities封装了企业里最通用的一部分逻辑,而应用各自的业务逻辑就都封装在use case里面。日常开发中最常见的对于某个模型的crud操作就属于usecase这一层。

Interface Adapter

这一层类似于胶水层,需要负责内圈的entity和use case同外圈的external interfaces之间的数据转化。需要把外层服务的数据转化成内层entity和usecase可以消费的数据,反之亦然。如上面图上画的,这一层有时候可能很简单(一个转化函数), 有时候可能复杂到包含一整个MVC/MVP的架构。

External Interfaces

我们需要依赖的外部服务,第三方框架,以及需要糊的页面UI都归属在这一层。这一层完全不感知内圈的任何逻辑,所以无论这一层怎么变(ui变化),都不应该影响到内圈的应用层逻辑(usecase)和企业级逻辑(entity)。

依赖原则

在整洁架构的原始设计中,并不是强制一定只能写这么四层,根据业务的需要还可以拆分的更细。不过无论怎么拆,都需要遵守前面提到的从外至内的依赖原则。即entity作为企业级的通用逻辑,不能依赖任何模块。而外层的ui等则可以使用usecase、entity。

3. 重构

前面介绍了当前代码库目前的一些具体问题,而整洁架构的理念正好可以帮助我们优化代码可维护性。

作为前端,我们的业务逻辑不应该依赖视图层(ui框架及其生态),同时应当保证业务逻辑的独立性和可复用性(usecase & entity)。最后,作为数据驱动的端应用,要保证应用视图渲染和业务逻辑等不受数据变动的影响(adapter & entity)。

根据以上的思考,我们对“整洁架构”做了如下落地。

Entities

对于前端应用来说,在entity层我们只需要将服务端的生数据做一层简单的抽象,生成一个贫血对象给后续的渲染和交互逻辑使用。

interface IRawOrder {
  amount: number
  barCode: string
  orderNo: string
  orderType: string
  skuId: number
  deliveryTime: number
  orderTime: number
  productImg: string
  status: number
}

export default function buildMakeOrder({
  formatTimestamp,
  formatImageUrl,
}: {
  formatTimestamp: (timestamp: number, format?: string) => string
  formatImageUrl: (
    image: string,
    config?: { width: number; height: number },
  ) => string
}) {
  return function makeOrder(raw?: IRawOrder) {
    if (!raw || !raw.orderNo) {
       Monitor.warn('脏数据')
       return null;
    }
    return {
      amount: raw.amount,
      barCode: raw.barCode,
      orderNo: raw.orderNo,
      orderType: raw.orderType,
      skuId: raw.skuId,
      status: raw.status,
      statusDescription: selectStatusDescription(raw.status),
      deliveryTime: formatTimestamp(raw.deliveryTime),
      orderTime: formatTimestamp(raw.orderTime),
      productImg: formatImageUrl(raw.productImg),
    }
  }
}

function selectStatusDescription(status: number): string {
  switch (status) {
    case 0:
      return '待支付'
    case 1:
      return '待发货'
    case 2:
      return '待收货'
    case 3:
      return '已完成'
    default:
      return ''
  }
}

以上是商家后台订单模型的entity工厂函数,工厂主要负责对服务端返回的生数据进行加工处理,让其满足渲染层和逻辑层的要求。除了抽象数据之外,可以看到在entity工厂还对数据进行了校验,将脏数据、不符合预期的数据全部处理掉或者进行兜底(具体操作要看业务场景)。

有一点需要注意的是,在设计entity的时候(尤其是基础entity)需要考虑复用性。举个例子,在上面orderEntity的基础上,我们通过简单的组合就可以生成一个虚拟商品订单entity:

import { makeOrder } from '@/entities'

export default function buildMakeVirtualOrder() {
  return function makeVirtualOrder(raw?: IRawPresaleOrder) {
     const order = makeOrder(raw)

     if(! order || !raw.virtualOrderType) {
         Monitor.warn('脏数据')
         return null
     }

     return {
         ...order,
         virtualOrderType: raw.virtualOrderType,
         virtualOrderDesc: selectVirtualOrderDesc(raw.virtualOrderType)
     }
  }
}

如此一来,我们就通过entity层达到了2个目的:

1 . 把前端的逻辑和服务端接口数据隔离开,无论服务端怎么变,前端后续的渲染、业务代码不需要变,我们只需要变更entitiy工厂函数;并且经过entity层处理过后,所有流入后续渲染&交互逻辑的数据都是可靠的;对于部分异常数据,前端应用可以第一时间发现并报警。

2 . 通过对业务模型进行抽象,实现了模块间的组合、复用。另外,抽象出的entity对代码的维护性也有非常大的帮助,开发者可以非常直观的知道所使用的entity所包含的所有字段。

Usecase

usecase这一层即是围绕entity展开的一系列crud操作,以及为了页面渲染做的一些联动(通过ui store实现)。由于当前架构的原因(没有bff层),usecase还可能承担部分微服务串联的工作。

举个例子,商家后台订单页面在渲染前有一堆准备逻辑:

1 . 根据route的query参数以及一些商家类型参数来决定默认选中哪个tab

2 . 根据是国内商家还是境外商家,调用对应的供应商接口来更新供应商下拉框

现在大致的实现是:

{
    mounted() {
        const { subType } = this.$route.query
        /*
            7-15行处理了几种分支链路场景下对subType的赋值问题
        */
        if (Number(subType) === 0 || subType) {
          this.subType = subType.toString()
        } else {
          if (this.user.merchant.typeId === 4) {
            this.subType = this.tabType.cross
          } else {
            this.subType = this.tabType.ordinarySpot
          }
        }

        /*
            getAllLogisticsCarrier有没有对subType赋值呢?光看这段代码完全不确定
        */
        this.getAllLogisticsCarrier()
        /*
            21-22行又多出来一个分支需要对subType进行再次赋值
        */
        if (this.isPersonPermission && !this.crossUser) {
          this.subType = this.tabType.warehouse
        }
    },

    getAllLogisticsCarrier() {
        let getCarrier = API.getAllLogisticsCarrier
        if (this.crossUser) {
          getCarrier = API.getOrderShipAllLogistics
        }

        getCarrier({}).then(res => {
          if (res.code === 200) {
            const options = []

            .......... // 给options赋值

            this.options2 = options

          }
        })
    },
}

我们能看到7-15、24-125行对this.subType进行了赋值。但由于我们无法确定20行的函数是否也对this.subType进行了赋值,所以光凭mounted函数的代码我们并不能完全确定subType的值究竟是什么,需要跳转到getAllLogisticsCarrier函数确认。这段代码在这里已经做了简化,实际的代码像getAllLogisticsCarrier这样的调用还有好几个,要想搞清楚逻辑就得把所有函数全看一遍,代码的可读性一般。同时,由于函数都封装在ui组件里,因此要想给函数覆盖单测的话也需要一些改造。

为了解决问题,我们将这部分逻辑都拆分到usecase层:


// prepare-order-page.ts
import { tabType } from '@/constants'

interface IParams {
  subType?: number
  merchantType: number
  isCrossUser: boolean
  isPersonPermission: boolean
}

/*
    做依赖倒置主要是为了方便后续的单测和复用
*/
export default function buildPrepareOrderPage({
  queryLogisticsCarriers,
}: {
  queryLogisticsCarriers: () => Promise<{ carriers: ICarrires }>
}) {
  return async function prepareOrderPage(params: IParams) {
    const activeTab = selectActiveTab(params)

    const { carriers } = queryLogisticsCarriers(params.isCrossUser)

    return {
      activeTab,
      carriers,
    }
  }
}

function selectActiveTab({
  subType,
  isCrossUser,
  isPersonPermission,
  merchantType,
}: IParams) {
  if (isPersonPermission && !isCrossUser) {
    return tabType.warehouse
  }

  if (Number(subType) === 0 || subType) {
    return subType.toString()
  }

  if (merchantType === 4) {
    return tabType.cross
  }

  return tabType.ordinarySpot
}

// query-logistics-carriers
export default function buildQueryLogisticsCarriers({
  fetchAllLogisticsCarrier,
  fetchOrderShipAllLogistics,
}: {
  fetchAllLogisticsCarrier: () => Promise<{ data: {carriers: ICarrires }}>
  fetchOrderShipAllLogistics: () => Promise<{ data: {carriers: ICarrires }}>
}) {
  return async function queryLogisticsCarriers(isCrossUser: boolean) {
    if (isCrossUser) {
      return fetchAllLogisticsCarrier()
    }

    return fetchOrderShipAllLogistics()
  }
}

// index.vue
{
    mounted() {
        const {activeTab, carriers} = prepareOrderPage(params)

        this.subType = activeTab;
        this.options = buildCarrierOptions(carriers) // 将carries转换成下拉框option
    }
}

首先,可以看到所有usecase一定是一个纯函数,不会存在副作用的问题。

其次,prepareOrderPage usecase专门为订单页定制,拆分后一眼就能看出来订单页的准备工作需要干决定选中的tab和拉取供应商列表两件事情。而另一个拆分出来的queryLogisticsCarriers则是封装了商家后台跨境、国内两种逻辑,后续无论跨境还是国内的逻辑如何变更,其影响范围被限制在了queryLogisticsCarriers函数,我们需要对其进行功能回归;而对于prepareOrderPage来说,queryLogisticsCarriers只是() => Promise<{ carriers: ICarrires }>的一个实现而已,其内部调用queryLogisticsCarriers的逻辑完全不受影响,不需要进行回归。

最后,而由于我们做了依赖倒置,我们可以非常容易的给usecase覆盖单测:



import buildPrepareOrderPage from '@/utils/create-goods';

function init() {
  const queryLogisticsCarriers = jest.fn();

  const prepareOrderPage = buildPrepareOrderPage({ queryLogisticsCarriers });

  return {
    prepareOrderPage,
    queryLogisticsCarriers,
  };
}

describe('订单页准备逻辑', () => {
  it('当用户是国内商家且在入仓白名单上,在打开订单页时,默认打开入仓tab', async () => {
    const { prepareOrderPage } = init();
    const params = {
        merchantType: 2
        isCrossUser: false
        isPersonPermission: true
    }

    const { activeTab } = await prepareOrderPage(params)

    expect(activeTab).toEqual({tabType.warehouse});
  });

   it('当用户是跨境商家,在打开订单页时,默认打开跨境tab', async () => {
    const { prepareOrderPage } = init();
    const params = {
        merchantType: 4
        isCrossUser: true
        isPersonPermission: true
    }

    const { activeTab } = await prepareOrderPage(params)

    expect(activeTab).toEqual({tabType.cross});
  });

  ......
});

单测除了进行功能回归之外,它的描述(demo里使用了Given-When-Then的格式,由于篇幅的原因,关于单测的细节在后续的文章再进行介绍)对于了解代码的逻辑非常非常非常有帮助。由于单测和代码逻辑强行绑定的缘故,我们甚至可以将单测描述当成一份实时更新的业务文档。

除了方便写单测之外,在通过usecase拆分完成之后,ui组件真正成为了只负责“ui”和监听用户交互行为的组件,这为我们后续的React技术栈迁移奠定了基础;通过usecase我们也实现了很不错的模块化,对于使用比较多的一些entity,他的crud操作可以通过独立的usecase具备了在多个页面甚至应用间复用的能力。

Adapter

上面usecase例子中的fetchAllLogisticsCarrier就是一个adapter,这一层起到的作用是将外部系统返回的数据转化成entity,并以一种统一的数据格式返回回来。

这一层很核心的一点即是可以依赖entity的工厂函数,将接口返回的数据转化成前端自己设计的模型数据,保证流入usecase和ui层的数据都是经过处理的“干净数据”。除此之外,通常在这一层我们会用一种固定的数据格式返回数据,比如例子中的 {success: boolean, data?: any}。这样做主要是为了抹平对接多个系统带来的差异性,同时减少多人协作时的沟通成本。

type Request = (url: string, params: Record<string, any>) => Promise<any>;
import makeCarrier from '@/entities/makeCarrier'


export default function buildFetchAllLogisticsCarrier({request}: {request: Request}) {
  return async function fetchAllLogisticsCarrier() {
    // TODO: 异常处理
    const response = await request('/fakeapi', info)

    if (!response || !resposne.code === 200) {
        return { 
            success: false
        }
    }

    return {
      success: true,
      data: {
          carriers: response.list?.map(makeCarrier)
      }
    }
  }
}

通过Adapter + entity的组合,我们基本形成了前端应用和后端服务之间的防腐层,使得前端可以在完全不清楚接口定义的情况下完成ui渲染、usecase等逻辑的开发。在服务端产出定义后,前端只需要将实际接口返回适配到自己定义的模型(通过entity)即可。这一点对前端的测试周提效非常非常非常重要,因为防腐层的存在,我们可以在测试周完成需求评审之后根据 prd 的内容 设计 出业务模型,并以此完成需求开发,在真正进入 研发 周后只需要和服务端对接完成adapter这一层的适配即可。

在实践过程中,我们发现在对接同一个系统的时候(对商家来说就是stark服务)各个adapter对于异常的处理几乎一模一样(上述的11-15行),我们可以通过Proxy对其进行抽离实现复用。当然,后续我们也完全有机会根据接口定义来自动生成adapter。

UI

在经过前面的拆分之后,无论咱们的UI层用React还是Vue来写,要做的工作都很简单了:

1 . 监听交互事件并调用对应的usecase来进行响应

2 . 通过usecase来获取entity数据进行渲染

由于entity已经做了过滤和适配处理,所以在ui层我们可以放心大胆的用,不需要再写一堆莫名其妙的判断逻辑。另外由于entity是由前端自己定义的模型,无论开发过程中服务端接口怎么变,受影响的都只有entity工厂函数,ui层不会受到影响。

最后,在ui层我们还剩下令人头痛的技术栈迁移问题。整个团队目前使用vue的项目有10个,按迭代频率和项目规模迁移的方案可以分为两类:

  • 迭代频繁的大应用:主要包括代码行数较多、逻辑较为复杂的几个中大型应用。这些应用想要一把梭直接完成迁移成本极高,但同时每个迭代又有相当的需求。基于这种情况,对于这三个应用我们采取了微前端的方式进行迁移。每个应用分别起一个对应的React应用,对于新页面以及部分逻辑已经完全和ui解藕迁移成本不高的业务,都由React应用来承接,最后通过module federation的方式实现融合。

  • 迭代不频繁的小应用:剩下的应用均是复杂度不高的小应用,这部分应用迭代的需求不多,以维护为主。因此我们的方案是对现有逻辑进行整洁架构重构,在ui和逻辑分层之后直接对ui层进行替换完成迁移。

4. 后续

通过整洁架构我们形成了统一的编码规范,在前端应用标准化的道路上迈下了坚实的一步。可以预见的是整个标准化的过程会非常漫长,我们会陆续往标准中增加新的规范使其更加完善,短期内在规划中的有:

  • 单测即文档:上面提到了usecase通过依赖倒置来配合单测落地,后续团队期望将一些业务逻辑的实现细则通过单测的描述来进行沉淀,解决业务文档实时性的问题。

  • 完善监控体系:前端常遇到的3种异常包括 代码逻辑异常、性能瓶颈(渲染卡顿、内存不足等)、数据导致异常。对于数据异常,我们可以在entity层映射的过程中加入对异常数据的埋点上报来填补目前监控的空白。(代码逻辑异常通过sentry已经监控,性能监控对于中后台应用不需要)

后续在标准逐渐稳定之后,我们也期望基于稳定的规范进行一些工程化的实践(比如根据mooncake文档自动生成adapter层、基于usecase实现功能开关等),敬请期待。

参考链接:

The Clean Architecture:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Module Federation:https://webpack.js.org/concepts/module-federation/

Anti-corruption Layer pattern:https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Sgr6El88eqjCDaRFxIVFQA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录