yarn
yarn
和 npm
一样也是 JavaScript
包管理工具,同样我们还发现有 cnpm
、 pnpm
等等包管理工具,包管理工具有一个就够了,为什么又会有这么多轮子出现呢?
yarn
?它和其它工具的区别在哪里?Tip:这里对比的npm
是指npm2
版本
npm
区别yarn
在下载和安装依赖包采用的是多线程的方式,而 npm
是单线程的方式执行,速度上就拉开了差距yarn
会在用户本地缓存已下载过的依赖包,优先会从缓存中读取依赖包,只有本地缓存不存在的情况才会采取远端请求的方式;反观npm
则是全量请求,速度上再次拉开差距yarn
把所有的依赖躺平至同级,有效的减少了相同依赖包重复下载的情况,加快了下载速度而且也减少了node_modules
的体积;反观npm
则是严格的根据依赖树下载并放置到对应位置,导致相同的包多次下载、node_modules
体积大的问题cnpm
区别cnpm
国内镜像速度更快(其他工具也可以修改源地址)cnpm
将所有项目下载的包收拢在自己的缓存文件夹中,通过软链接把依赖包放到对应项目的node_modules
中pnpm
区别yarn
一样有一个统一管理依赖包的目录pnpm
保留了npm2
版本原有的依赖树结构,但是node_modules
下所有的依赖包都是通过软连接的方式保存yarn
来认识yarn
一个项目的依赖包需要有指定文件来说明,JavaScript
包管理工具使用 package.json
做依赖包说明的入口。
{
"dependencies": {
"lodash": "4.17.20"
}
}
以上面的 package.json
为例,我们可以直接识别 package.json
直接下载对应的包。
import fetch from 'node-fetch';
function fetchPackage(packageJson) {
const entries = Object.entries(packageJson.dependencies);
entries.forEach(async ([key, version]) => {
const url = `https://registry.`yarn`pkg.com/${key}/-/${key}-${version}.tgz`,
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Couldn't fetch package "${reference}"`);
}
return await response.buffer();
});
}
接下来我们再看看另外一种情况:
{
"dependencies": {
"lodash": "4.17.20",
"customer-package": "../../customer-package"
}
}
"customer-package": "../../customer-package"
在我们的代码中已经不能正常工作了。所以我们需要做代码的改造:
import fetch from 'node-fetch';
import fs from 'fs-extra';
function fetchPackage(packageJson) {
const entries = Object.entries(packageJson.dependencies);
entries.forEach(async ([key, version]) => {
// 文件路径解析直接复制文件
if ([`/`, `./`, `../`].some(prefix => version.startsWith(prefix))) {
return await fs.readFile(version);
}
// 非文件路径直接请求远端地址
// ...old code
});
}
目前我们的代码可以正常的下载固定版本的依赖包、文件路径。但是例如:"react": "^15.6.0"
这种情况我们是不支持的,而且我们可以知道这个表达式代表了从 15.6.0
版本到 15.7.0
内所有的包版本。理论上我们应该安装在这个范围中最新版本的包,所以我们增加一个新的方法:
import semver from 'semver';
async function getPinnedReference(name, version) {
// 首先要验证版本号是否符合规范
if (semver.validRange(version) && !semver.valid(version)) {
// 获取依赖包所有版本号
const response = await fetch(`https://registry.`yarn`pkg.com/${name}`);
const info = await response.json();
const versions = Object.keys(info.versions);
// 匹配符合规范最新的版本号
const maxSatisfying = semver.maxSatisfying(versions, reference);
if (maxSatisfying === null)
throw new Error(
`Couldn't find a version matching "${version}" for package "${name}"`
);
reference = maxSatisfying;
}
return { name, reference };
}
function fetchPackage(packageJson) {
const entries = Object.entries(packageJson.dependencies);
entries.forEach(async ([name, version]) => {
// 文件路径解析直接复制文件
// ...old code
let realVersion = version;
// 如果版本号以 ~ 和 ^ 开头则获取最新版本的包
if (version.startsWith('~') || version.startsWith('^')) {
const { reference } = getPinnedReference(name, version);
realVersion = reference;
}
// 非文件路径直接请求远端地址
// ...old code
});
}
那么这样我们就可以支持用户指定某个包在一个依赖范围内可以安装最新的包。
现实远远没有我们想的那么简单,我们的依赖包还有自己的依赖包,所以我们还需要递归每一层依赖包把所有的依赖包都下载下来。
// 获取依赖包的dependencies
async function getPackageDependencies(packageJson) {
const packageBuffer = await fetchPackage(packageJson);
// 读取依赖包的`package.json`
const packageJson = await readPackageJsonFromArchive(packageBuffer);
const dependencies = packageJson.dependencies || {};
return Object.keys(dependencies).map(name => {
return { name, version: dependencies[name] };
});
}
现在我们可以通过用户项目的 package.json
获取整个依赖树上所有的依赖包。
可以下载依赖包还不够的,我们要把文件都转移到指定的文件目录下,就是我们熟悉的node_modules
里。
async function linkPackages({ name, reference, dependencies }, cwd) {
// 获取整个依赖树
const dependencyTree = await getPackageDependencyTree({
name,
reference,
dependencies,
});
await Promise.all(
dependencyTree.map(async dependency => {
await linkPackages(dependency, `${cwd}/`node_modules`/${dependency.name}`);
})
);
}
我们虽然可以根据整个依赖树下载全部的依赖包并放到了node_modules
里,但是我们发现依赖包可能会有重复依赖的情况,导致我们实际下载的依赖包非常冗余,所以我们可以把相同依赖包放到一个位置,这样就不需要重复下载。
function optimizePackageTree({ name, reference, dependencies = [] }) {
dependencies = dependencies.map(dependency => {
return optimizePackageTree(dependency);
});
for (let hardDependency of dependencies) {
for (let subDependency of hardDependency.dependencies)) {
// 子级依赖是否和父级依赖存在相同依赖
let availableDependency = dependencies.find(dependency => {
return dependency.name === subDependency.name;
});
if (!availableDependency) {
// 父级依赖不存在时,把依赖插入到父级依赖
dependencies.push(subDependency);
}
if (
!availableDependency ||
availableDependency.reference === subDependency.reference
) {
// 从子级依赖中剔除相同的依赖包
hardDependency.dependencies.splice(
hardDependency.dependencies.findIndex(dependency => {
return dependency.name === subDependency.name;
})
);
}
}
}
return { name, reference, dependencies };
}
我们通过逐级递归一层层将依赖从层层依赖展平,减少了重复的依赖包安装。截止到这一步我们已经实现了简易的yarn了~
看完代码后给我最直观的就是yarn
把面向对象的思想发挥的淋漓尽致
Config:yarn
相关配置实例
cli:全部yarn
命令集合实例
registries:npm
源相关信息实例
涉及 lock 文件、解析依赖包入口文件名、依赖包存储位置和文件名等
lockfile:yarn.lock
对象
intergrity checker:用于检查依赖包下载是否正确
package resolver:用于解析 package.json
依赖包不同引用方式
package request:依赖包版本请求实例
package reference:依赖包关系实例
package fetcher:依赖包下载实例
package linker:依赖包文件管理
package hoister:依赖包扁平化实例
yarn
工作流程这里我们已yarn add lodash
为例,看看一下yarn
都在内部做了哪些事情。yarn
在安装依赖包时会分为主要 5 个步骤:
.yarnrc
、命令行参数、package.json
信息等)、兼容性(cpu、nodejs 版本、操作系统等)是否符合约定我们继续以yarn add lodash
为例
yarnrc
文件// 获取`yarn`rc文件配置
// process.cwd 当前执行命令项目目录
// process.argv 用户指定的`yarn`命令和参数
const rc = getRcConfigForCwd(process.cwd(), process.argv.slice(2));
/**
* 生成Rc文件可能存在的所有路经
* @param {*} name rc源名
* @param {*} cwd 当前项目路经
*/
function getRcPaths(name: string, cwd: string): Array<string> {
// ......other code
if (!isWin) {
// 非windows环境从/etc/`yarn`/config开始查找
pushConfigPath(etc, name, 'config');
// 非windows环境从/etc/`yarn`rc开始查找
pushConfigPath(etc, `${name}rc`);
}
// 存在用户目录
if (home) {
// `yarn`默认配置路经
pushConfigPath(CONFIG_DIRECTORY);
// 用户目录/.config/${name}/config
pushConfigPath(home, '.config', name, 'config');
// 用户目录/.config/${name}/config
pushConfigPath(home, '.config', name);
// 用户目录/.${name}/config
pushConfigPath(home, `.${name}`, 'config');
// 用户目录/.${name}rc
pushConfigPath(home, `.${name}rc`);
}
// 逐层向父级遍历加入.${name}rc路经
// Tip: 用户主动写的rc文件优先级最高
while (true) {
// 插入 - 当前项目路经/.${name}rc
unshiftConfigPath(cwd, `.${name}rc`);
// 获取当前项目的父级路经
const upperCwd = path.dirname(cwd);
if (upperCwd === cwd) {
// we've reached the root
break;
} else {
cwd = upperCwd;
}
}
// ......read rc code
}
/**
* -- 索引位置
*/
const doubleDashIndex = process.argv.findIndex(element => element === '--');
/**
* 前两个参数为node地址、`yarn`文件地址
*/
const startArgs = process.argv.slice(0, 2);
/**
* `yarn`子命令&参数
* 如果存在 -- 则取 -- 之前部分
* 如果不存在 -- 则取全部
*/
const args = process.argv.slice(2, doubleDashIndex === -1 ? process.argv.length : doubleDashIndex);
/**
* `yarn`子命令透传参数
*/
const endArgs = doubleDashIndex === -1 ? [] : process.argv.slice(doubleDashIndex);
在初始化的时候,会分别初始化 config
配置项、reporter
日志。
package.json
是否配置了 workspace
字段workspace
项目则yarn.lock
是以 workspac
e 根目录的yarn.lock
为准this.workspaceRootFolder = await this.findWorkspaceRoot(this.cwd);
// `yarn`.lock所在目录,优先和workspace同级
this.`lockfile`Folder = this.workspaceRootFolder || this.cwd;
/**
* 查找workspace根目录
*/
async findWorkspaceRoot(initial: string): Promise<?string> {
let previous = null;
let current = path.normalize(initial);
if (!await fs.exists(current)) {
// 路经不存在报错
throw new MessageError(this.reporter.lang('folderMissing', current));
}
// 循环逐步向父级目录查找访问`package.json`\`yarn`.json是否配置workspace
// 如果任意层级配置了workspace,则返回该json所在的路经
do {
// 取出`package.json`\`yarn`.json
const manifest = await this.findManifest(current, true);
// 取出workspace配置
const ws = extractWorkspaces(manifest);
if (ws && ws.packages) {
const relativePath = path.relative(current, initial);
if (relativePath === '' || micromatch([relativePath], ws.packages).length > 0) {
return current;
} else {
return null;
}
}
previous = current;
current = path.dirname(current);
} while (current !== previous);
return null;
}
yarn.lock
地址读取yarn.lock
文件。package.json
中的生命周期执行对应 script
脚本/**
* 按照`package.json`的script配置的生命周期顺序执行
*/
export async function wrapLifecycle(config: Config, flags: Object, factory: () => Promise<void>): Promise<void> {
// 执行preinstall
await config.executeLifecycleScript('preinstall');
// 真正执行安装操作
await factory();
// 执行install
await config.executeLifecycleScript('install');
// 执行postinstall
await config.executeLifecycleScript('postinstall');
if (!config.production) {
// 非production环境
if (!config.disablePrepublish) {
// 执行prepublish
await config.executeLifecycleScript('prepublish');
}
// 执行prepare
await config.executeLifecycleScript('prepare');
}
}
package.json
的 dependencies
、devDependencies
、optionalDependencies
内所有依赖包名+版本号workspace
项目,还需要读取 workspace
项目中所有子项目的 package.json
的相关依赖workspace
项目则读取的为项目根目录的 package.json
// 获取当前项目目录下所有依赖
pushDeps('dependencies', projectManifestJson, {hint: null, optional: false}, true);
pushDeps('devDependencies', projectManifestJson, {hint: 'dev', optional: false}, !this.config.production);
pushDeps('optionalDependencies', projectManifestJson, {hint: 'optional', optional: true}, true);
// 当前为workspace项目
if (this.config.workspaceRootFolder) {
// 收集workspace下所有子项目的`package.json`
const workspaces = await this.config.resolveWorkspaces(workspacesRoot, workspaceManifestJson);
for (const workspaceName of Object.keys(workspaces)) {
// 子项目`package.json`
const workspaceManifest = workspaces[workspaceName].manifest;
// 将子项目放到根项目dependencies依赖中
workspaceDependencies[workspaceName] = workspaceManifest.version;
// 收集子项目依赖
if (this.flags.includeWorkspaceDeps) {
pushDeps('dependencies', workspaceManifest, {hint: null, optional: false}, true);
pushDeps('devDependencies', workspaceManifest, {hint: 'dev', optional: false}, !this.config.production);
pushDeps('optionalDependencies', workspaceManifest, {hint: 'optional', optional: true}, true);
}
}
}
package resolver
的 find
方法获取依赖包的版本信息,然后递归调用 find
,查找每个依赖下的 dependence
中依赖的版本信息。在解析包的同时使用一个 Set(fetchingPatterns)
来保存已经解析和正在解析的 package
。package
时,首先会根据其 name
和 range
(版本范围)判断当前依赖包是否为被解析过(通过判断是否存在于上面维护的 set
中,即可确定是否已经解析过)lockfile
中获取到精确的版本信息, 如果 lockfile
中存在对于的 package 信息,获取后,标记成已解析。如果 lockfile
中不存在该 package
的信息,则向 registry 发起请求获取满足 range 的已知最高版本的 package
信息,获取后将当前 package
标记为已解析delayedResolveQueue
中先不处理package
都递归遍历完成后,再遍历 delayedResolveQueue
,在已经解析过的包信息中,找到最合适的可用版本信息结束后,我们就确定了依赖树中所有 package
的具体版本,以及该包地址等详细信息。
package resolver
的 init
方法)/**
* 查找依赖包版本号
*/
async find(initialReq: DependencyRequestPattern): Promise<void> {
// 优先从缓存中读取
const req = this.resolveToResolution(initialReq);
if (!req) {
return;
}
// 依赖包请求实例
const request = new PackageRequest(req, this);
const fetchKey = `${req.registry}:${req.pattern}:${String(req.optional)}`;
// 判断当前是否请求过相同依赖包
const initialFetch = !this.fetchingPatterns.has(fetchKey);
// 是否更新`yarn`.lock标志
let fresh = false;
if (initialFetch) {
// 首次请求,添加缓存
this.fetchingPatterns.add(fetchKey);
// 获取依赖包名+版本在`lockfile`的内容
const `lockfile`Entry = this.`lockfile`.getLocked(req.pattern);
if (`lockfile`Entry) {
// 存在`lockfile`的内容
// 取出依赖版本
// eq: concat-stream@^1.5.0 => { name: 'concat-stream', range: '^1.5.0', hasVersion: true }
const {range, hasVersion} = normalizePattern(req.pattern);
if (this.is`lockfile`EntryOutdated(`lockfile`Entry.version, range, hasVersion)) {
// `yarn`.lock版本落后
this.reporter.warn(this.reporter.lang('incorrect`lockfile`Entry', req.pattern));
// 删除已收集的依赖版本号
this.removePattern(req.pattern);
// 删除`yarn`.lock中对包版本的信息(已经过时无效了)
this.`lockfile`.removePattern(req.pattern);
fresh = true;
}
} else {
fresh = true;
}
request.init();
}
await request.find({fresh, frozen: this.frozen});
}
for (const depName in info.dependencies) {
const depPattern = depName + '@' + info.dependencies[depName];
deps.push(depPattern);
promises.push(
this.resolver.find(......),
);
}
for (const depName in info.optionalDependencies) {
const depPattern = depName + '@' + info.optionalDependencies[depName];
deps.push(depPattern);
promises.push(
this.resolver.find(.......),
);
}
if (remote.type === 'workspace' && !this.config.production) {
// workspaces support dev dependencies
for (const depName in info.devDependencies) {
const depPattern = depName + '@' + info.devDependencies[depName];
deps.push(depPattern);
promises.push(
this.resolver.find(.....),
);
}
}
这里主要是对缓存中没有的依赖包进行下载。
cacheFolder+slug+node_modules+pkg.name
生成一个 path
,判断系统中是否存在该 path
,如果存在,证明已经有缓存,不用重新下载,将它过滤掉。fetch
任务的 queue
,根据resolveStep
中解析出的依赖包下载地址去依次获取依赖包。reference
的地址多种情况,如:npm 源、github 源、gitlab 源、文件地址等,所以yarn
会根据 reference
地址调用对应的 fetcher
获取依赖包package
文件流通过 fs.createWriteStream
写入到缓存目录下,缓存下来的是.tgz
压缩文件,再解压到当前目录下lockfile
文件/**
* 拼接缓存依赖包路径
* 缓存路径 + `npm`源-包名-版本-integrity + `node_modules` + 包名
*/
const dest = config.generateModuleCachePath(ref);
export async function fetchOneRemote(
remote: PackageRemote,
name: string,
version: string,
dest: string,
config: Config,
): Promise<FetchedMetadata> {
if (remote.type === 'link') {
const mockPkg: Manifest = {_uid: '', name: '', version: '0.0.0'};
return Promise.resolve({resolved: null, hash: '', dest, package: mockPkg, cached: false});
}
const Fetcher = fetchers[remote.type];
if (!Fetcher) {
throw new MessageError(config.reporter.lang('unknownFetcherFor', remote.type));
}
const fetcher = new Fetcher(dest, remote, config);
// 根据传入的地址判断文件是否存在
if (await config.isValidModuleDest(dest)) {
return fetchCache(dest, fetcher, config, remote);
}
// 删除对应路径的文件
await fs.unlink(dest);
try {
return await fetcher.fetch({
name,
version,
});
} catch (err) {
try {
await fs.unlink(dest);
} catch (err2) {
// what do?
}
throw err;
}
}
经过fetchStep
后,我们本地缓存中已经有了所有的依赖包,接下来就是如何将这些依赖包复制到我们项目中的node_modules
下。
peerDependences
,如果找不到匹配的 peerDependences
,进行 warning
提示dest
dest
进行排序(使用 localeCompare
本地排序规则)dest
(要拷贝到的目标目录地址),src
(包的对应 cache
目录地址)中,执行将 copy
任务,将 package
从 src
拷贝到 dest
下
yarn
对于扁平化其实非常简单粗暴,先按照依赖包名的 Unicode 做排序,然后根据依赖树逐层扁平化
可以增加网络请求并发量:--network-concurrency <number>
可以设置网络请求超时时长:--network-timeout <milliseconds>
yarn.lock
中某个依赖包的版本号还是不可以?"@babel/code-frame@^7.0.0-beta.35":
version "7.0.0-beta.55"
resolved "https://registry.`yarn`pkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.55.tgz#71f530e7b010af5eb7a7df7752f78921dd57e9ee"
integrity sha1-cfUw57AQr163p993UveJId1X6e4=
dependencies:
"@babel/highlight" "7.0.0-beta.55"
我们随机截取了一段yarn.lock
的代码,如果只修改 version
和 resolved
字段是不够的,因为yarn
还会根据实际下载的内容生成的 integrity
和yarn.lock
文件的 integrity
字段做对比,如果不一致就代表本次下载是错误的依赖包。
首先我们要看是如何引用依赖包的。前置场景:
package.json
中依赖A@1.0.0
,B@1.0.0
,C@1.0.0
D@2.0.0
依赖 C2.0.0
D@1.0.0
依赖C@1.0.0
A@1.0.0
依赖D@1.0.0
B@1.0.0
依赖D@2.0.0
首先我们根据当前依赖关系和yarn
安装特性可以知道实际安装结构为:
|- A@1.0.0
|- B@1.0.0
|--- D@2.0.0
|----- C@2.0.0
|- C@1.0.0
|- D@1.0.0
D
实际为D@1.0.0
B
代码中未直接声明依赖 C
,但是却直接引用了 C
相关对象方法(因为 B
直接引用了 D
,且 D
一定会引用 C
,所以 C
肯定存在)。此时实际引用非C@2.0.0
,而是引用的C@1.0.0
。webpack
查询依赖包是访问node_modules
下符合规则的依赖包,所以直接引用了C@1.0.0
我们可以通过yarn list
来检查是否存在问题。
参考资料
[1]yarn官网: https://www.yarnpkg.cn/
[2]我 fork 的yarn源码加了部分中文注释: https://github.com/supergaojian/%60yarn%60
[3]从源码角度分析yarn安装依赖的过程: https://jishuin.proginn.com/p/763bfbd29d7e
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/0YBkibyySRcBsvpwsELCqA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。