深入浅出 Yarn 包管理

发表于 3年以前  | 总阅读数:334 次

关于yarn

yarnnpm 一样也是 JavaScript 包管理工具,同样我们还发现有 cnpmpnpm 等等包管理工具,包管理工具有一个就够了,为什么又会有这么多轮子出现呢?

为什么是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体系架构

看完代码后给我最直观的就是yarn把面向对象的思想发挥的淋漓尽致

  • Configyarn相关配置实例

  • cli:全部yarn命令集合实例

  • registriesnpm源相关信息实例

  • 涉及 lock 文件、解析依赖包入口文件名、依赖包存储位置和文件名等

  • lockfileyarn.lock 对象

  • intergrity checker:用于检查依赖包下载是否正确

  • package resolver:用于解析 package.json 依赖包不同引用方式

  • package request:依赖包版本请求实例

  • package reference:依赖包关系实例

  • package fetcher:依赖包下载实例

  • package linker:依赖包文件管理

  • package hoister:依赖包扁平化实例

yarn工作流程

流程概要

这里我们已yarn add lodash 为例,看看一下yarn都在内部做了哪些事情。yarn在安装依赖包时会分为主要 5 个步骤:

  • checking:检查配置项(.yarnrc、命令行参数、package.json 信息等)、兼容性(cpu、nodejs 版本、操作系统等)是否符合约定
  • resolveStep:解析依赖包信息,并且会解析出整个依赖树上所有包的具体版本信息
  • fetchStep:下载全部依赖包,如果依赖包已经在缓存中存在则跳过下载,反之则下载对应依赖包到缓存文件夹内,当这一步都完成后代表着所有依赖包都已经存在缓存中了
  • linkStep:缓存的依赖包扁平化的复制副本到项目的依赖目录下
  • buildStep:对于一些二进制包,需要进行编译,在这一步进行

流程讲解

我们继续以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 日志。

  • config 会在 init 时,逐步向父级递归查询 package.json 是否配置了 workspace 字段
  • Tip:如果当前是 workspace 项目则yarn.lock 是以 workspace 根目录的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;
}

执行 add 指令

  • 根据上一步得到的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.jsondependenciesdevDependenciesoptionalDependencies 内所有依赖包名+版本号
  • 因为当前为 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);
          }
        }
}

resolveStep 获取依赖包

  1. 遍历首层依赖,调用 package resolverfind 方法获取依赖包的版本信息,然后递归调用 find,查找每个依赖下的 dependence 中依赖的版本信息。在解析包的同时使用一个 Set(fetchingPatterns)来保存已经解析和正在解析的 package
  2. 在具体解析每个 package 时,首先会根据其 namerange(版本范围)判断当前依赖包是否为被解析过(通过判断是否存在于上面维护的 set 中,即可确定是否已经解析过)
  3. 对于未解析过的包,首先尝试从 lockfile 中获取到精确的版本信息, 如果 lockfile 中存在对于的 package 信息,获取后,标记成已解析。如果 lockfile 中不存在该 package 的信息,则向 registry 发起请求获取满足 range 的已知最高版本的 package 信息,获取后将当前 package 标记为已解析
  4. 对于已解析过的包,则将其放置到一个延迟队列 delayedResolveQueue 中先不处理
  5. 当依赖树的所有 package 都递归遍历完成后,再遍历 delayedResolveQueue,在已经解析过的包信息中,找到最合适的可用版本信息

结束后,我们就确定了依赖树中所有 package 的具体版本,以及该包地址等详细信息。

  • 对第一层所有项目的依赖包获取最新的版本号(调用 package resolverinit 方法)
/**
 * 查找依赖包版本号
 */
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(.....),
            );
      }
}

fetchStep 下载依赖包

这里主要是对缓存中没有的依赖包进行下载。

  1. 已经在缓存中的依赖包,是不需要重新下载的,所以第一步先过滤掉本地缓存中已经存在的依赖包。过滤过程是根据 cacheFolder+slug+node_modules+pkg.name 生成一个 path,判断系统中是否存在该 path,如果存在,证明已经有缓存,不用重新下载,将它过滤掉。
  2. 维护一个 fetch 任务的 queue,根据resolveStep中解析出的依赖包下载地址去依次获取依赖包。
  3. 在下载每个包的时候,首先会在缓存目录下创建其对应的缓存目录,然后对包的 reference 地址进行解析。
  4. 因为 reference 的地址多种情况,如:npm 源、github 源、gitlab 源、文件地址等,所以yarn会根据 reference 地址调用对应的 fetcher 获取依赖包
  5. 将获取的 package 文件流通过 fs.createWriteStream写入到缓存目录下,缓存下来的是.tgz 压缩文件,再解压到当前目录下
  6. 下载解压完成后,更新 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;
  }
}

linkStep 移动文件

经过fetchStep后,我们本地缓存中已经有了所有的依赖包,接下来就是如何将这些依赖包复制到我们项目中的node_modules下。

  1. 在复制包之前,会先解析 peerDependences,如果找不到匹配的 peerDependences,进行 warning 提示
  2. 之后对依赖树进行扁平化处理,生成要拷贝到的目标目录 dest
  3. 对扁平化后的目标 dest 进行排序(使用 localeCompare 本地排序规则)
  4. 根据 flatTree 中的 dest(要拷贝到的目标目录地址),src(包的对应 cache 目录地址)中,执行将 copy 任务,将 packagesrc 拷贝到 dest

yarn对于扁平化其实非常简单粗暴,先按照依赖包名的 Unicode 做排序,然后根据依赖树逐层扁平化

Q&A

1.如何增加网络请求并发数量?

可以增加网络请求并发量:--network-concurrency <number>

2.网络请求总超时怎么办?

可以设置网络请求超时时长:--network-timeout <milliseconds>

3.为什么我修改了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 的代码,如果只修改 versionresolved 字段是不够的,因为yarn还会根据实际下载的内容生成的 integrityyarn.lock 文件的 integrity 字段做对比,如果不一致就代表本次下载是错误的依赖包。

4.在项目依赖中出现了同依赖包不同版本的情况,我要如何知道实际使用的是哪一个包?

首先我们要看是如何引用依赖包的。前置场景:

  • package.json 中依赖A@1.0.0B@1.0.0C@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

 相关推荐

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

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

发布于: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年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录