Node.js 搭建一个 API 接口服务(实战)

发表于 4年以前  | 总阅读数:484 次

前言

因为最近打算自己搭建一个自己的博客系统,用来记录日常的学习和提升一下写作水平,所以能就打算自己搭建一下前后端项目。在网上找了下,也没有找到合适(现成)的项目,所以就打算自己动手来搭建一下。这篇文章主要描述如何搭建一个node的API接口服务。

技术栈简述

网上的node框架也挺多的,用的较多的有egg,express,koa等框架,框架间各有利弊,最后均衡下来,还是决定使用可拓展性比较强的koa2来搭建项目,加上最近在学习typescript,最后决定使用的技术栈就是 koa+typescript+mysql+mongodb来搭建项目。

为什么要用node

最主要的一点是其他语言咱也不会啊。。。

言归正传,Node.js是一个运行在服务端的框架,它底层使用的是V8引擎,它的速度非常快,并且作为一个前端的后端服务语言,还有其他吸引人的地方:

  1. 异步I/O
  2. 事件驱动
  3. 单线程
  4. 跨平台

而且,最最最最重要的一点就是,node是由JavaScript开发的,极大的降低了前端同学的学习成本。

Koa

koa是Express的原班人马打造的一个新的框架。相对于express来说koa更小,更有表现力更加健壮。当然,前面说的都是虚的,其实真正吸引我的是koa通过es6的写法,利用async函数,解决了express.js中地狱回调的问题,并且koa不像express一样自带那么多中间件,对于一个私有项目来说,无疑是极好的,还有一个特点就是koa独特的中间件流程控制,也就是大家津津乐道的koa洋葱模型。

关于洋葱模型,大概归纳起来就是两点

  1. context的保存和传递
  2. 中间件的管理和next的实现

(图片来源于网络)

上面两张图很清晰的展示了洋葱模型的工作流程,当然,具体的原理实现的话与本篇无关,就不在深入描述了,有兴趣的同学可以自己到网上搜一下哈。

Typescript

网上特别多关于“为什么要用Typescript开发”,“Typescript开发的好处和坏处”,“为什么不用Typescript开发”等等的争论和文章,有兴趣的同学也可以去说道说道哈。

本次项目用ts主要是出于以下几点考虑:

  • 本人在持续的学习ts中,“纸上得来终觉浅,绝知此事要躬行”,需要更多的ts实战才能加深对ts的了解
  • 自己的项目,想用什么就用什么
  • 写起来逼格会相对高一些
  • Ts有诸多js中没有的东西,譬如泛型接口抽象等等
  • 良好的模块管理
  • 强类型语音,个人感觉比js开发服务端项目更合适
  • 有良好的错误提示机制,可以避免很多开发阶段的低级错误
  • 约束开发习惯,使得代码更优雅规范

最后记住一点,适合自己的才是最好的

Mysql

MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一

Mongodb

为什么用了mysql还要用mongodb呢?其实主要是因为使用的是jwt来做一个身份认证,由于用到中间件没有提供刷新过期时间的API,而又想要实现一个自动续命的功能,所以使用mongodb来辅助完成自动续命的功能。并且,一些用户身份信息或埋点信息可以存在mongo中

PM2

PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单

项目搭建

我主要把项目分为:框架,日志,配置,路由,请求逻辑处理,数据模型化这几个模块

以下是一个项目的目录结构:

├── app                         编译后项目文件
  ├── node_modules                依赖包
  ├── static                      静态资源文件
  ├── logs                       服务日志
  ├── src                         源码
  │   ├── abstract                    抽象类
  │   ├── config                      配置项
  │   ├── controller                  控制器
  │   ├── database                    数据库模块
  │   ├── middleware                  中间件模块
  │   ├── models                    数据库表模型
  │   ├── router                      路由模块 - 接口
  │   ├── utils                       工具
  │   ├── app.ts                  koa2入口
  ├── .eslintrc.js                eslint 配置
  ├── .gitignore                  忽略提交到git目录文件
  ├── .prettierrc                 代码美化
  ├── ecosystem.config.js         pm2 配置
  ├── nodemon.json                nodemon 配置
  ├── package.json                依赖包及配置信息文件
  ├── tsconfig.json               typescript 配置
  ├── README.md                   描述文件

话不多说,接下来跟着代码来看项目

创建一个koa应用

俗话说的好:人无头不走。项目中也会有个牵着项目走的头,这就是入口app.ts,接下来咱就结合代码看看它是怎么做这个头的

import Koa, { ParameterizedContext } from 'koa'
import logger from 'koa-logger'
// 实例化koa
const app = new Koa()
app.use(logger())
// 答应一下响应信息
app.use(async (ctx, next) => {
  const start = (new Date()).getDate();
  let timer: number
  try {
    await next()
    timer = (new Date()).getDate()
    const ms = timer - start
    console.log(`method: ${ctx.method}, url:${ctx.url} - ${ms}ms`)
  } catch (e) {
    timer = (new Date()).getDate()
    const ms = timer - start
    console.log(`method: ${ctx.method}, url:${ctx.url} - ${ms}ms`)
  }
})
// 监听端口并启动
app.listen(config.PORT, () => {
  console.log(`Server running on http://localhost:${config.PORT || 3000}`)
})
app.on('error', (error: Error, ctx: ParameterizedContext) => {
  // 项目启动错误
  ctx.body = error;
})
export default app

到了这一步,我们就已经可以启动一个简单的项目了

  1. npm run tsc 编译ts文件
  2. node app.js 启动项目

接下来在浏览器输入http://localhost:3000就能在控制台看到访问日志了。当然,做到这一步还是不够的,因为我们开发过程中总是伴随着调试,所以需要更方便的开发环境。

本地开发环境

本地开发使用nodemon来实现自动重启,因为node不能直接识别ts,所以需要用ts-node来运行ts文件。

// nodemon.json
{
  "ext": "ts",
  "watch": [ // 需要监听变化的文件
    "src/**/*.ts",
    "config/**/*.ts",
    "router/**/*.ts",
    "public/**/*",
    "view/**/*"
  ],
  "exec": "ts-node --project tsconfig.json src/app.ts" // 使用ts-node来执行ts文件
}
// package.json
"scripts": {
  "start": "cross-env NODE_ENV=development nodemon -x"
}

本地调试

因为有的时候需要看到请求的信息,那我们又不能在代码中添加console.log(日志)这样效率低又不方便,所以我们需要借助编辑器来帮我们实现debug的功能。这边简单描述一下怎么用vscode来实现debug的。

  • tsconfig.json中开启sourceMap
  • 为ts-node注册一个vsc的debug任务,修改项目的launch.json文件,添加一个新的启动方式
  • launch.json
{
  "name": "Current TS File",
  "type": "node",
  "request": "launch",
  "args": [
    "${workspaceRoot}/src/app.ts" // 入口文件
  ],
  "runtimeArgs": [
    "--nolazy",
    "-r",
    "ts-node/register"
  ],
  "sourceMaps": true,
  "cwd": "${workspaceRoot}",
  "protocol": "inspector",
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}
  • F9 代码中断点
  • F5 开始调试代码

引入接口路由

上面我们已经创建了一个koa应用了,接下来就使用需要引入路由了:

// app.ts
import router from './router'
import requestMiddleware from './middleware/request'
app
  .use(requestMiddleware) // 使用路由中间件处理路由,一些处理接口的公用方法
  .use(router.routes())
  .use(router.allowedMethods())

// router/index.ts
import { ParameterizedContext } from 'koa'
import Router from 'koa-router'
const router = new Router()
// 接口文档 - 这边使用分模块实现路由的方式
router.use(路由模块.routes())
...
router.use(路由模块.routes())
// 测试路由连接
router.get('/test-connect', async (ctx: ParameterizedContext) => {
  await ctx.body = 'Hello Frivolous'
})
// 匹配其他未定义路由
router.get('*', async (ctx: ParameterizedContext) => {
  await ctx.render('error')
})
export default router

定义数据库模型

  1. 使用sequlize作为mysql的中间件
// 实例化sequelize
import { Sequelize } from 'sequelize'
const sequelizeManager = new Sequelize(db, user, pwd, Utils.mergeDefaults({
    dialect: 'mysql',
    host: host,
    port: port,
    define: {
      underscored: true,
      charset: 'utf8',
      collate: 'utf8_general_ci',
      freezeTableName: true,
      timestamps: true,
    },
    logging: false,
  }, options));
}
// 定义表结构
import { Model, ModelAttributes, DataTypes } from 'sequelize'
// 定义用户表模型中的字段属性
const UserModel: ModelAttributes = {
  id: {
    type: DataTypes.INTEGER,
    allowNull: false,
    primaryKey: true,
    unique: true,
    autoIncrement: true,
    comment: 'id'
  },
  avatar: {
    type: DataTypes.INTEGER,
    allowNull: true
  },
  nick_name: {
    type: DataTypes.STRING(50),
    allowNull: true
  },
  email: {
    type: DataTypes.STRING(50),
    allowNull: true
  },
  mobile: {
    type: DataTypes.INTEGER,
    allowNull: false
  },
  gender: {
    type: DataTypes.STRING(35),
    allowNull: true
  },
  age: {
    type: DataTypes.INTEGER,
    allowNull: true
  },
  password: {
    type: DataTypes.STRING(255),
    allowNull: false
  }
}
// 定义表模型
sequelizeManager.define(modelName, UserModel, {
  freezeTableName: true, // model对应的表名将与model名相同
  tableName: modelName,
  timestamps: true,
  underscored: true,
  paranoid: true,
  charset: "utf8",
  collate: "utf8_general_ci",
})

根据上面的代码,我们就已经定义好一个user表了,其他表也可以按照这个来定义。不过这个项目除了使用mysql,也还有用到mongo,接下来看看mongodb怎么用

  1. 使用mongoose作为mongodb的中间件
<// mongoose入口
import mongoose from 'mongoose'
const uri = `mongodb://${DB.host}:${DB.port}`
mongoose.connect('mongodb://' + DB_STR)
mongoose.connection.on('connected', () => {
  log('Mongoose connection success')
})
mongoose.connection.on('error', (err: Error) => {
  log('Mongoose connection error: ' + err.message)
})
mongoose.connection.on('disconnected', () => {
  log('Mongoose connection disconnected')
})
export default mongoose

// 定义表模型
import mongoose from '../database/mongoose'
const { Schema } = mongoose
const AccSchema = new Schema({}, {
  strict: false, // 允许传入未定义字段
  timestamps: true, // 默认会带上createTime/updateTime
  versionKey: false // 默认不带版本号
})
export default AccSchema

// 定义模型
mongoose.model('AccLog', AccSchema)

实现接口

好了,上面我们已经定义好表模型了,接下来就是激动人心的接口实现了。我们通过一个简单的埋点接口来实现一下,首先需要分析埋点工具实现的逻辑:

  1. 因为埋点信息都是非关系型的,所以使用mongodb来存储埋点信息
  2. 因为这个就是一个单纯的记录接口,所以需要设计的比较通用 - 即除了关键几个字段,调用方传什么就保存什么
  3. 埋点行为对用户来说是无感知的,所以不设计反馈信息,如果埋点出错也是由内部处理

好了,了解这个埋点的功能之后,就开始来实现这个简单的接口了:

// route.ts 定义一个addAccLog的接口
router.post('/addAccLog', AccLogController.addAccLog)
// AccLogController.ts 实现addAccLog接口
class AccLogRoute extends RequestControllerAbstract {
  constructor() {
    super()
  }
  // AccLogController.ts
  public async addAccLog(ctx: ParameterizedContext): Promise<void> {
    try {
      const data = ctx.request.body
      const store = Mongoose.model(tableName, AccSchema, tableName)
      // disposeAccInsertData 方法用来处理日志信息,有些字段嵌套太要扁平化深或者去除空值冗余字段
      const info = super.disposeAccInsertData(data.logInfo)
      // 添加日志
      const res = await store.create(info)
      // 不需要反馈
      // super.handleResponse('success', ctx, res)
    } catch (e) {
      // 错误处理 - 比如说打个点,记录埋点出错的信息,看看是什么原因导致出错的(根据实际的需求来做)
      // ...
    }
  }
  // ...
}
export default new AccLogRoute()

说到这边,不得不提一句哈,就是路由可以引入装饰器写法,这样能减少重复工作和提高效率,有兴趣的同学可以看我上一篇博客哈。这边贴一下装饰器写法的代码:

@Controller('/AccLogController')
class AccLogRoute {
  @post('/addAccLog')
  @RequestBody({}) 
  async addAccLog(ctx: ParameterizedContext, next: Function) {
    const res = await store.create(info)
    handleResponse('success', ctx, res)
  }
}

这一对比,是不是看出装饰器的好处了呢。

jwt身份验证

这边使用jsonwebtoken来做jwt校验

import { sign, decode, verify } from 'jsonwebtoken'
import { ParameterizedContext } from 'koa'
import { sign, decode, verify } from 'jsonwebtoken'
import uuid from 'node-uuid'

import IController from '../interface/controller'
import config from '../config'
import rsaUtil from '../util/rsaUtil'
import cacheUtil from '../util/cacheUtil'

interface ICode {
  success: string,
  unknown: string,
  error: string,
  authorization: string,
}

interface IPayload {
  iss: number | string; // 用户id
  login_id: number | string; // 登录日志id
  sub?: string;
  aud?: string;
  nbf?: string;
  jti?: string;
  [key: string]: any;
}

abstract class AController implements IController {
  // 服务器响应状态
  // code 状态码参考 https://www.cnblogs.com/zqsb/p/11212362.html
  static STATE = {
    success: { code: 200, message: '操作成功!' },
    unknown: { code: -100, message: '未知错误!' },
    error: { code: 400, message: '操作失败!' },
    authorization: { code: 401, message: '身份认证失败!' },
  }

  /**
   * @description 响应事件
   * @param {keyof ICode} type
   * @param {ParameterizedContext} [ctx]
   * @param {*} [data]
   * @param {string} [message]
   * @returns {object}
   */
  public handleResponse(
    type: keyof ICode,
    ctx?: ParameterizedContext,
    data?: any,
    message?: string
  ): object {
    const res = AController.STATE[type]
    const result = {
      message: message || res.message,
      code: res.code,
      data: data || null,
    }
    if (ctx) ctx.body = result
    return result
  }
  /**
   * @description 注册token
   * @param {IPayload} payload
   * @returns {string}
   */
  public jwtSign(payload: IPayload): string {
    const { TOKENEXPIRESTIME, JWTSECRET, RSA_PUBLIC_KEY } = config.JWT_CONFIG
    const noncestr = uuid.v1()
    const iss = payload.iss
    // jwt创建Token
    const token = sign({
      ...payload,
      noncestr
    }, JWTSECRET, { expiresIn: TOKENEXPIRESTIME, algorithm: "HS256" })
    // 加密Token
    const result = rsaUtil.pubEncrypt(RSA_PUBLIC_KEY, token)
    const isSave = cacheUtil.set(`${iss}`, noncestr, TOKENEXPIRESTIME * 1000)
    if (!isSave) {
      throw new Error('Save authorization noncestr error')
    }
    return `Bearer ${result}`
  }
  /**
   * @description 验证Token有效性,中间件
   * 
   */
  public async verifyAuthMiddleware(ctx: ParameterizedContext, next: Function): Promise<any> {
    // 校验token
    const { JWTSECRET, RSA_PRIVATE_KEY, IS_AUTH, IS_NONCESTR } = config.JWT_CONFIG
    if (!IS_AUTH && process.env.NODE_ENV === 'development') {
      await next()
    } else {
      // 如果header中没有身份认证字段,则认为校验失败
      if (!ctx.header || !ctx.header.authorization) {
        ctx.response.status = 401
        return
      }
      // 获取token并且解析,判断token是否一致
      const authorization: string = ctx.header.authorization;
      const scheme = authorization.substr(0, 6)
      const credentials = authorization.substring(7)
      if (scheme !== 'Bearer') {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'Wrong authorization prefix')
        return;
      }
      if (!credentials) {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'Request header authorization cannot be empty')
        return;
      }

      const token = rsaUtil.priDecrypt(RSA_PRIVATE_KEY, credentials)
      if (typeof token === 'object') {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'authorization is not an object')
        return;
      }
      const isAuth = verify(token, JWTSECRET)
      if (!isAuth) {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'authorization token expired')
        return;
      }
      const decoded: string | { [key: string]: any } | null = decode(token)
      if (typeof decoded !== 'object' || !decoded) {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'authorization parsing failed')
        return;
      }
      const noncestr = decoded.noncestr
      const exp = decoded.exp
      const iss = decoded.iss
      const cacheNoncestr = cacheUtil.get(`${iss}`)
      if (IS_NONCESTR && noncestr !== cacheNoncestr) {
        ctx.response.status = 401;
        this.handleResponse('authorization', ctx, null, 'authorization signature "noncestr" error')
        return;
      }
      if (Date.now() / 1000 - exp < 60) {
        const options = { ...decoded };
        Reflect.deleteProperty(options, 'exp')
        Reflect.deleteProperty(options, 'iat')
        Reflect.deleteProperty(options, 'nbf')
        const newToken = AController.prototype.jwtSign(options as IPayload)
        ctx.append('token', newToken)
      }
      ctx.jwtData = decoded
      await next()
    }
  }
}
export default AController
//  授权装饰器代码
public auth() {
  return (target: any, name?: string, descriptor?: IDescriptor) => {
    if (typeof target === 'function' && name === undefined && descriptor === undefined) {
      target.prototype.baseAuthMidws = super.verifyAuthMiddleware;
    } else if (typeof target === 'object' && name && descriptor) {
      descriptor.value.prototype.baseAuthMidws = super.verifyAuthMiddleware;
    }
  }
}

这样,我们就完成了一个jwt授权的模块了,我们用也很简单,以addAccLog接口为例

class AccLogRoute {
  @auth() // 只要➕这一行代码就可以
  @post('/addAccLog')
  ...
}

接口文档

既然我们已经写好接口了,那总要有一份可参阅的文档输出,这时候就想到了swagger,接下来咱们就把swagger引入到我们的项目中吧。

  • 入口
// swagger入口
import swaggerJSDoc from 'swagger-jsdoc'
import config from '../config'
const { OPEN_API_DOC } = config
// swagger definition
const swaggerDefinition = {
  // ...
}
const createDOC = (): object => {
  const options = {
    swaggerDefinition: swaggerDefinition,
    apis: ['./src/controller/*.ts']
  }
  return OPEN_API_DOC ? swaggerJSDoc(options) : null
}
export default createDOC
// 怎么
  • 配置示例 - 这边一定要注意格式
 @swagger Tips: 必须要声明,不然代码不会把此处生成为文档
 definitions:
   Login: // 接口名
     required: // 必填参数
       - username
       - password
     properties: // 可选参数
       username:
         type: string
       password:
         type: string
       path:
         type: string
  • swagger官方配置工具
  • 推荐一个vscode插件 - facility插件,用来快速生成注释

Mock数据

使用mock来生成测试数据

日志

日志模块本来打算是用log4.js来做的,后来感觉做的日志模块还没达到预期,所以就决定先暂时用pm2的日志系统来代替log4。这边就先不贴log4相关的代码了

部署

使用pm2来部署项目,这边展示一下配置文件

Tips

  • error_file 错误日志输出
  • out_file 正常日志输出
  • script 入口文件 - 以打包过后的js文件作为入口
// pm2.json
{
  "apps": {
    "name": "xxx",
    "script": "./app/server.js",
    "cwd": "./",
    "args": "",
    "interpreter_args": "",
    "watch": true,
    "ignore_watch": [
      "node_modules",
      "logs",
      "app/lib"
    ],
    "exec_mode": "fork_mode",
    "instances": 1,
    "max_memory_restart": 8,
    "error_file": "./logs/pm2-err.log",
    "out_file": "./logs/pm2-out.log",
    "merge_logs": true,
    "log_date_format": "YYYY-MM-DD HH:mm:ss",
    "max_restarts": 30,
    "autorestart": true,
    "cron_restart": "",
    "restart_delay": 60,
    "env": {
      "NODE_ENV": "production"
    }
  }
}

// package.json
"scripts": {
  // 生产环
  "prod": "pm2 start pm2.json"
}

配置好pm2之后,我们只要在package.json中配置pm2 start pm2.json就可以实现启动pm2进程了

结束语

虽然是一个简单的接口服务器,但是需要考虑的东西也是很多,而且因为很多插件都是第一次接触,所以整个项目实现的过程还是蛮坎坷的,基本上是那种摸石头过河。不过痛并快乐着吧,虽然困难很多,但是过程中也学到了不少新的知识点,大概了解了一个简单的后端服务项目所承载的重量。

原文地址:https://juejin.im/post/5eb3e1b4e51d45244e7c2d09 作者:T谷子

 相关推荐

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

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

发布于: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次阅读  |  详细内容 »
 目录