【源码】Vue-i18n: 你知道国际化是怎么实现的么?

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

Vue-i18n 简单介绍以及使用

Vue I18n 是 Vue.js 的国际化插件,它可以轻松地将一些本地化功能集成到你的 Vue.js 应用程序中。

本文的源码阅读是基于版本 8.24.4 进行

我们来看一个官方的 demo

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>ES modules browser example</title>
    <script src="../../dist/vue-i18n.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{ $t('message.hello') }}</p>
    </div>
    <script type="module">
      // 如果使用模块系统 (例如通过 vue-cli),则需要导入 Vue 和 VueI18n ,然后调用 Vue.use(VueI18n)。
      import Vue from 'https://unpkg.com/vue@2.6.10/dist/vue.esm.browser.js'
      Vue.use(VueI18n)

      new Vue({
        // 通过 `i18n` 选项创建 Vue 实例
        // 通过选项创建 VueI18n 实例
        i18n: new VueI18n({
          locale: 'zh', // 设置地区
          // 准备翻译的语言环境信息
          // 设置地区信息
          messages: {
            en: {
              message: {
                hello: 'hello, I am Gopal'
              }
            },
            zh: {
              message: {
                hello: '你好,我是 Gopal 一号'
              }
            }
          }
        })
      }).$mount('#app')
    </script>
  </body>
</html>

使用上是比较简单的,本文我们深入了解 Vue-i18n 的工作原理,探索国际化实现的奥秘。包括:

  • 整体的 Vue-i18n 的架构是怎样的?
  • 上述 demo 是如何生效的?
  • 我们为什么可以直接在模板中使用 $t?它做了什么?
  • 上述 demo 是如何做到不刷新更新页面的?
  • 全局组件 和全局自定义指令的实现?

代码结构以及入口

我们看一下 Vue-18n 的代码结构如下

├── components/
│   ├── interpolation.js // <i18n> 组件的实现
│   └── number.js
├── directive.js // 全局自定义组件的实现
├── extend.js // 拓展方法
├── format.js // parse 和 compile 的核心实现
├── index.js // 入口文件
├── install.js // 注册方法
├── mixin.js // 处理各个生命周期
├── path.js
└── util.js

关于 Vue-18n 的整体架构,网上找到了一个比较贴切的图,如下。其中左侧是 Vue-i18n 提供的一些方法、组件、自定义指令等能力,右侧是 Vue-i18n 对数据的管理

入口文件为 index.js,在 VueI18n 类中的 constructor 中先调用 install 方法注册

// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #290
/* istanbul ignore if */
if (!Vue && typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

install 方法中,主要做了几件事,如下代码注释,后面还会提到,这里有一个大致的印象

// 在 Vue 的原型中拓展方法,代码在 extend.js 里
extend(Vue)
// 在 Vue 中通过 mixin 的方式混入
Vue.mixin(mixin)
// 全局指令
Vue.directive('t', { bind, update, unbind })
// 全局组件
Vue.component(interpolationComponent.name, interpolationComponent)
Vue.component(numberComponent.name, numberComponent)

注册完成后,会调用 _initVM,这个主要是创建了一个 Vue 实例对象,后面很多功能会跟这个 this._ vm 相关联

// VueI18n 其实不是一个 Vue 对象,但是它在内部建立了 Vue 对象 vm,然后很多的功能都是跟这个 vm 关联的
this._initVM({
  locale,
  fallbackLocale,
  messages,
  dateTimeFormats,
  numberFormats
})

_initVM (data: {
         locale: Locale,
         fallbackLocale: FallbackLocale,
         messages: LocaleMessages,
         dateTimeFormats: DateTimeFormats,
         numberFormats: NumberFormats
         }): void {
  // 用来关闭 Vue 打印消息的
  const silent = Vue.config.silent
  Vue.config.silent = true
  this._vm = new Vue({ data }) // 创建了一个 Vue 实例对象
  Vue.config.silent = silent
}

全局方法 $t 的实现

我们来看看 Vue-i18n 的 $t 方法的实现,揭开国际化翻译的神秘面纱

在 extent.js 中,我们看到在 Vue 的原型中挂载 $t 方法,这是我们为什么能够直接在模板中使用的原因。

// 在 Vue 的原型中挂载 $t 方法,这是我们为什么能够直接在模板中使用的原因
// 把 VueI18n 对象实例的方法都注入到 Vue 实例上
Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult {
  const i18n = this.$i18n
  // 代理模式的使用
  return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values)
}

看到的是调用 index.js 中的 $t 的方法

// $t 最后调用的方法
_t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
  if (!key) { return '' }
  const parsedArgs = parseArgs(...values)
  // 如果 escapeParameterHtml 被配置为 true,那么插值参数将在转换消息之前被转义。
  if(this._escapeParameterHtml) {
    parsedArgs.params = escapeParams(parsedArgs.params)
  }
  const locale: Locale = parsedArgs.locale || _locale
  // 翻译
  let ret: any = this._translate(
    messages, locale, this.fallbackLocale, key,
    host, 'string', parsedArgs.params
  )
}

_interpolate

回到主线,当调用 _translate 的时候,接着调用

this._interpolate(step, messages[step], key, host, interpolateMode, args, [key])

并返回

this._render(ret, interpolateMode, values, key)

_render 方法中,可以调用自定义方法去处理插值对象,或者是默认的方法处理插值对象。

_render (message: string | MessageFunction, interpolateMode: string, values: any, path: string): any {
  // 自定义插值对象
  let ret = this._formatter.interpolate(message, values, path)

  // If the custom formatter refuses to work - apply the default one
  if (!ret) {
    // 默认的插值对象
    ret = defaultFormatter.interpolate(message, values, path)
  }

  // if interpolateMode is **not** 'string' ('row'),
  // return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
  return interpolateMode === 'string' && !isString(ret) ? ret.join('') : ret
}

我们主要来看看默认的方法处理,主要是在 format.js 中完成

format.js 中的 parse 和 compile

format.js 实现了 BaseFormatter 类,这里使用 _caches 实现了一层缓存优化,也是常见的优化手段。下面的 没有插值对象的话,就直接返回 [message],就完成使命了。

export default class BaseFormatter {
  // 实现缓存效果
  _caches: { [key: string]: Array<Token> }

  constructor () {
    this._caches = Object.create(null)
  }

  interpolate (message: string, values: any): Array<any> {
    // 没有插值对象的话,就直接返回
    if (!values) {
      return [message]
    }
    // 如果存在 tokens,则组装值返回
    let tokens: Array<Token> = this._caches[message]
    if (!tokens) {
      // 没有存在 tokens,则拆分 tokens
      tokens = parse(message)
      this._caches[message] = tokens
    }
    return compile(tokens, values)
  }
}

当遇到如下的使用方式的时候

<p>{{ $t('message.sayHi', { name: 'Gopal' })}}</p>

主要涉及两个方法,我们先来看 parse,代码比较直观,可以看到本质上是遍历字符串,然后遇到有 {} 包裹的,把其中的内容附上类型拿出来放入到 tokens 里返回。

// 代码比较直观,可以看到本质上是遍历字符串,然后遇到有 {} 包裹的,把其中的内容附上类型拿出来放入到 tokens 里返回。
export function parse (format: string): Array<Token> {
  const tokens: Array<Token> = []
  let position: number = 0

  let text: string = ''
  while (position < format.length) {
    let char: string = format[position++]
    if (char === '{') {
      if (text) {
        tokens.push({ type: 'text', value: text })
      }

      text = ''
      let sub: string = ''
      char = format[position++]
      while (char !== undefined && char !== '}') {
        sub += char
        char = format[position++]
      }
      const isClosed = char === '}'

      const type = RE_TOKEN_LIST_VALUE.test(sub)
        ? 'list'
        : isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
          ? 'named'
          : 'unknown'
      tokens.push({ value: sub, type })
    } else if (char === '%') {
      // when found rails i18n syntax, skip text capture
      if (format[(position)] !== '{') {
        text += char
      }
    } else {
      text += char
    }
  }

  text && tokens.push({ type: 'text', value: text })

  return tokens
}

以上的 demo 的返回 tokens 如下:

[
    {
        "type": "text",
        "value": "hi, I am "
    },
    {
        "value": "name",
        "type": "named"
    }
]

还有 parse,就是将上述的组装起来

// 把一切都组装起来
export function compile (tokens: Array<Token>, values: Object | Array<any>): Array<any> {
  const compiled: Array<any> = []
  let index: number = 0

  const mode: string = Array.isArray(values)
    ? 'list'
    : isObject(values)
      ? 'named'
      : 'unknown'
  if (mode === 'unknown') { return compiled }

  while (index < tokens.length) {
    const token: Token = tokens[index]
    switch (token.type) {
      case 'text':
        compiled.push(token.value)
        break
      case 'list':
        compiled.push(values[parseInt(token.value, 10)])
        break
      case 'named':
        if (mode === 'named') {
          compiled.push((values: any)[token.value])
        } else {
          if (process.env.NODE_ENV !== 'production') {
            warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
          }
        }
        break
      case 'unknown':
        if (process.env.NODE_ENV !== 'production') {
          warn(`Detect 'unknown' type of token!`)
        }
        break
    }
    index++
  }

  return compiled
}

以上 demo 最后返回 ["hi, I am ", "Gopal"],最后再做一个简单的拼接就可以了,至此,翻译就可以成功了

Vue-i18n 是如何避免 XSS ?

上面 _t 方法中有一个 _escapeParameterHtml 。这里谈谈 escapeParams,其实是 Vue-i18n 为了防止 xss 攻击做的一个处理。如果 escapeParameterHtml 被配置为 true,那么插值参数将在转换消息之前被转义。

// 如果escapeParameterHtml被配置为true,那么插值参数将在转换消息之前被转义。
if(this._escapeParameterHtml) {
  parsedArgs.params = escapeParams(parsedArgs.params)
}
/**
 * Sanitizes html special characters from input strings. For mitigating risk of XSS attacks.
 * @param rawText The raw input from the user that should be escaped.
 */
function escapeHtml(rawText: string): string {
  return rawText
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

/**
 * Escapes html tags and special symbols from all provided params which were returned from parseArgs().params.
 * This method performs an in-place operation on the params object.
 *
 * @param {any} params Parameters as provided from `parseArgs().params`.
 *                     May be either an array of strings or a string->any map.
 *
 * @returns The manipulated `params` object.
 */
export function escapeParams(params: any): any {
  if(params != null) {
    Object.keys(params).forEach(key => {
      if(typeof(params[key]) == 'string') {
        // 处理参数,防止 XSS 攻击
        params[key] = escapeHtml(params[key])
      }
    })
  }
  return params
}

如何做到无刷新更新页面

我们发现,在 demo 中,我无论是修改了 locale 还是 message 的值,页面都不会刷新,但页面也是会更新数据。这个功能类似 Vue 的双向数据绑定,它是如何实现的呢?

new

这里 Vue-i18n 采用了观察者模式,我们上面提到过的 _initVM 方法中,我们会将翻译相关的数据 data 通过 new Vue 传递给 this._vm 实例。现在要做的就是去监听这些 data 的变化

Vue-i18n 的这一块的逻辑主要是在 mixin.js 文件中,在 beforeCreate 中调用 watchI18nData 方法,这个方法的实现如下:

// 为了监听翻译变量的变化
watchI18nData (): Function {
  const self = this
  // 使用 vue 实例中的 $watch 方法,数据变化的时候,强制刷新
  // 组件的 data 选项是一个函数。Vue 在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data 的形式存储在组件实例中
  return this._vm.$watch('$data', () => {
    self._dataListeners.forEach(e => {
      Vue.nextTick(() => {
        e && e.$forceUpdate()
      })
    })
  }, { deep: true })
}

其中 _dataListeners,我理解是一个个的实例(但我没想到具体的场景,在系统中使用 vue-18n new 多个实例?)。subscribeDataChangingunsubscribeDataChanging 就是用来添加和移除订阅器的函数

// 添加订阅器,添加使用的实例
subscribeDataChanging (vm: any): void {
  this._dataListeners.add(vm)
}

// 移除订阅器
unsubscribeDataChanging (vm: any): void {
  remove(this._dataListeners, vm)
}

它们会在 mixin.js 中的 beforeMountbeforeDestroy 中调用

// 精简后的代码  
// 在保证了_i18n 对象生成之后,beforeMount 和 beforeDestroy 里就能增加移除监听了
beforeMount (): void {
  const options: any = this.$options
  options.i18n = options.i18n || (options.__i18n ? {} : null)

  this._i18n.subscribeDataChanging(this)
},


  beforeDestroy (): void {
    if (!this._i18n) { return }
    const self = this
    this.$nextTick(() => {
      if (self._subscribing) {
        // 组件销毁的时候,去除这个实例
        self._i18n.unsubscribeDataChanging(self)
        delete self._subscribing
      }
    })
}

总结一下,在 beforeCreate 会去 watch data 的变化,并在 beforeMount 中添加订阅器。假如 data 变化,就会强制更新相应的实例更新组件。并在 beforeDestroy 中移除订阅器,防止内存溢出,整体流程如下图所示

全局自定义指令以及全局组件的实现

在 extent.js 中,我们提到了注册全局指令和全局组件,我们来看下如何实现的

// 全局指令
Vue.directive('t', { bind, update, unbind })
// 全局组件
Vue.component(interpolationComponent.name, interpolationComponent)
Vue.component(numberComponent.name, numberComponent)

全局指令 t

关于指令 t 的使用方法,详情参考官方文档

以下是示例:

<!-- 字符串语法:字面量 -->
<p v-t="'foo.bar'"></p>

<!-- 字符串语法:通过数据或计算属性绑定 -->
<p v-t="msg"></p>

<!-- 对象语法: 字面量 -->
<p v-t="{ path: 'hi', locale: 'ja', args: { name: 'kazupon' } }"></p>

<!-- 对象语法: 通过数据或计算属性绑定 -->
<p v-t="{ path: greeting, args: { name: fullName } }"></p>

<!-- `preserve` 修饰符 -->
<p v-t.preserve="'foo.bar'"></p>

directive.js 中,我们看到实际上是调用了 t 方法和 tc 方法,并给 textContent 方法赋值。(textContent 属性表示一个节点及其后代的文本内容。)

// 主要是调用了 t 方法和 tc 方法
if (choice != null) {
  el._vt = el.textContent = vm.$i18n.tc(path, choice, ...makeParams(locale, args))
} else {
  el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args))
}

在 unbind 的时候会清空 textContent

全局组件 i18n

i18n 函数式组件 使用如下:

<div id="app">
  <!-- ... -->
  <i18n path="term" tag="label" for="tos">
    <a :href="url" target="_blank">{{ $t('tos') }}</a>
  </i18n>
  <!-- ... -->
</div>

其源码实现 src/components/interpolation.js,其中 tag 表示外层标签。传 false, 则表示不需要外层。

export default {
  name: 'i18n',
  functional: true,
  props: {
    // 外层标签。传 false,则表示不需要外层
    tag: {
      type: [String, Boolean, Object],
      default: 'span'
    },
    path: {
      type: String,
      required: true
    },
    locale: {
      type: String
    },
    places: {
      type: [Array, Object]
    }
  },
  render (h: Function, { data, parent, props, slots }: Object) {
    const { $i18n } = parent

    const { path, locale, places } = props
    // 通过插槽的方式实现
    const params = slots()
    // 获取到子元素 children 列表
    const children = $i18n.i(
      path,
      locale,
      onlyHasDefaultPlace(params) || places
        ? useLegacyPlaces(params.default, places)
        : params
    )

    const tag = (!!props.tag && props.tag !== true) || props.tag === false ? props.tag : 'span'
    // 是否需要外层标签进行渲染
    return tag ? h(tag, data, children) : children
  }
}

注意的是:places 语法会在下个版本进行废弃了

function useLegacyPlaces (children, places) {
  const params = places ? createParamsFromPlaces(places) : {}

  if (!children) { return params }

  // Filter empty text nodes
  children = children.filter(child => {
    return child.tag || child.text.trim() !== ''
  })

  const everyPlace = children.every(vnodeHasPlaceAttribute)
  if (process.env.NODE_ENV !== 'production' && everyPlace) {
    warn('`place` attribute is deprecated in next major version. Please switch to Vue slots.')
  }

  return children.reduce(
    everyPlace ? assignChildPlace : assignChildIndex,
    params
  )
}

总结

总体 Vue-i18n 代码不复杂,但也花了自己挺多时间,算是一个小挑战。从 Vue-i18n 中,我学习到了

  • 国际化翻译 Vue-i18n 的架构组织和 $t 的原理,当遇到插值对象的时候,需要进行 parse 和 compile
  • Vue-i18n 通过转义字符避免 XSS
  • 通过观察者模式对数据进行监听和更新,做到无刷新更新页面
  • 全局自定义指令和全局组件的实现

参考

  • https://zhuanlan.zhihu.com/p/110112552
  • https://hellogithub2014.github.io/2018/07/17/vue-i18n-source-code/

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

 相关推荐

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

发布于:8月以前  |  398次阅读  |  详细内容 »
 目录