掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(上)

news2024/11/17 3:54:33

前言

众所周知,在vue中使用scoped可以避免父组件的样式渗透到子组件中。使用了scoped后会给html增加自定义属性data-v-x,同时会给组件内CSS选择器添加对应的属性选择器[data-v-x]。这篇我们来讲讲vue是如何给CSS选择器添加对应的属性选择器[data-v-x]。注:本文中使用的vue版本为3.4.19@vitejs/plugin-vue的版本为5.0.4

打包领取欧阳平时写文章都会参考的vue源码资料、解锁我更多vue原理文章

看个demo

我们先来看个demo,代码如下:

<template>
  <div class="block">hello world</div>
</template>

<style scoped>
.block {
  color: red;
}
</style>

经过编译后,上面的demo代码就会变成下面这样:

<template>
  <div data-v-c1c19b25 class="block">hello world</div>
</template>

<style>
.block[data-v-c1c19b25] {
  color: red;
}
</style>

从上面的代码可以看到在div上多了一个data-v-c1c19b25自定义属性,并且css的属性选择器上面也多了一个[data-v-c1c19b25]

可能有的小伙伴有疑问,为什么生成这样的代码就可以避免样式污染呢?

.block[data-v-c1c19b25]:这里面包含两个选择器。.block是一个类选择器,表示class的值包含block[data-v-c1c19b25]是一个属性选择器,表示存在data-v-c1c19b25自定义属性的元素。

所以只有class包含block,并且存在data-v-c1c19b25自定义属性的元素才能命中这个样式,这样就能避免样式污染。

并且由于在同一个组件里面生成的data-v-x值是一样的,所以在同一组件内多个html元素只要class的值包含block,就可以命中color: red的样式。

接下来我将通过debug的方式带你了解,vue是如何在css中生成.block[data-v-c1c19b25]这样的属性选择器。

@vitejs/plugin-vue

还是一样的套路启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
debug-terminal

假如vue文件编译为js文件是一个毛线团,那么他的线头一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通过这个线头开始debug我们就能够梳理清楚完整的工作流程。
vite-config

vuePlugin函数

我们给上方图片的vue函数打了一个断点,然后在debug终端上面执行yarn dev,我们看到断点已经停留在了vue函数这里。然后点击step into,断点走到了@vitejs/plugin-vue库中的一个vuePlugin函数中。我们看到简化后的vuePlugin函数代码如下:

function vuePlugin(rawOptions = {}) {
  return {
    name: "vite:vue",
    // ...省略其他插件钩子函数
    transform(code, id, opt) {
      // ..
    }
  };
}

@vitejs/plugin-vue是作为一个plugins插件在vite中使用,vuePlugin函数返回的对象中的transform方法就是对应的插件钩子函数。vite会在对应的时候调用这些插件的钩子函数,vite每解析一个模块都会执行一次transform钩子函数。更多vite钩子相关内容查看官网。

我们这里只需要看transform钩子函数,解析每个模块时调用。

由于解析每个文件都会走到transform钩子函数中,但是我们只关注index.vue文件是如何解析的,所以我们给transform钩子函数打一个条件断点。如下图:
conditional-breakpoint

然后点击Continue(F5),vite服务启动后就会走到transform钩子函数中打的断点。我们可以看到简化后的transform钩子函数代码如下:

function transform(code, id, opt) {
  const { filename, query } = parseVueRequest(id);
  if (!query.vue) {
    return transformMain(
      code,
      filename,
      options.value,
      this,
      ssr,
      customElementFilter.value(filename)
    );
  } else {
    const descriptor = getDescriptor(filename);
    if (query.type === "style") {
      return transformStyle(
        code,
        descriptor,
        Number(query.index || 0),
        options.value
      );
    }
  }
}

首先调用parseVueRequest函数解析出当前要处理的文件的filenamequery,在debug终端来看看此时这两个的值。如下图:
query

从上图中可以看到filename为当前处理的vue文件路径,query的值为空数组。所以此时代码会走到transformMain函数中。

transformMain函数

将断点走进transformMain函数,在我们这个场景中简化后的transformMain函数代码如下:

async function transformMain(code, filename, options) {
  const { descriptor } = createDescriptor(filename, code, options);

  const { code: templateCode } = await genTemplateCode(
    descriptor
    // ...省略
  );

  const { code: scriptCode } = await genScriptCode(
    descriptor
    // ...省略
  );

  const stylesCode = await genStyleCode(
    descriptor
    // ...省略
  );

  const output = [scriptCode, templateCode, stylesCode];
  let resolvedCode = output.join("\n");
  return {
    code: resolvedCode,
  };
}

我们在 通过debug搞清楚.vue文件怎么变成.js文件文章中已经深入讲解过transformMain函数了,所以这篇文章我们不会深入到transformMain函数中使用到的每个函数中。

首先调用createDescriptor函数根据当前vue文件的code代码字符串生成一个descriptor对象,简化后的createDescriptor函数代码如下:

const cache = new Map();

function createDescriptor(
  filename,
  source,
  { root, isProduction, sourceMap, compiler, template }
) {
  const { descriptor, errors } = compiler.parse(source, {
    filename,
    sourceMap,
    templateParseOptions: template?.compilerOptions,
  });
  const normalizedPath = slash(path.normalize(path.relative(root, filename)));
  descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
  cache.set(filename, descriptor);
  return { descriptor, errors };
}

首先调用compiler.parse方法根据当前vue文件的code代码字符串生成一个descriptor对象,此时的descriptor对象主要有三个属性templatescriptSetupstyle,分别对应的是vue文件中的<template>模块、<template setup>模块、<style>模块。

然后调用getHash函数给descriptor对象生成一个id属性,getHash函数代码如下:

import { createHash } from "node:crypto";
function getHash(text) {
  return createHash("sha256").update(text).digest("hex").substring(0, 8);
}

从上面的代码可以看出id是根据vue文件的路径调用node的createHash加密函数生成的,这里生成的id就是scoped生成的自定义属性data-v-x中的x部分。

然后在createDescriptor函数中将生成的descriptor对象缓存起来,关于descriptor对象的处理就这么多了。

接着在transformMain函数中会分别以descriptor对象为参数执行genTemplateCodegenScriptCodegenStyleCode函数,分别得到编译后的render函数、编译后的js代码、编译后的style代码。

编译后的render函数如下图:
templateCode

从上图中可以看到template模块已经编译成了render函数

编译后的js代码如下图:
scriptCode

从上图中可以看到script模块已经编译成了一个名为_sfc_main的对象,因为我们这个demo中script模块没有代码,所以这个对象是一个空对象。

编译后的style代码如下图:
stylesCode

从上图中可以看到style模块已经编译成了一个import语句。

最后就是使用换行符\ntemplateCodescriptCodestylesCode拼接起来就是vue文件编译后的js文件啦,如下图:
resolvedCode

想必细心的同学已经发现有地方不对啦,这里的style模块编译后是一条import语句,并不是真正的css代码。这条import语句依然还是import导入的index.vue文件,只是加了一些额外的query参数。

?vue&type=style&index=0&lang.css:这个query参数表明当前import导入的是vue文件的css部分。

还记得我们前面讲过的transform钩子函数吗?vite每解析一个模块都会执行一次transform钩子函数,这个import导入vue文件的css部分,当然也会触发transform钩子函数的执行。

第二次执行transform钩子函数

当在浏览器中执行vue文件编译后的js文件时会触发import "/Users/xxx/index.vue?vue&type=style&index=0&lang.css"语句的执行,导致再次执行transform钩子函数。

transform钩子函数代码如下:

function transform(code, id, opt) {
  const { filename, query } = parseVueRequest(id);
  if (!query.vue) {
    return transformMain(
      code,
      filename,
      options.value,
      this,
      ssr,
      customElementFilter.value(filename)
    );
  } else {
    const descriptor = getDescriptor(filename);
    if (query.type === "style") {
      return transformStyle(
        code,
        descriptor,
        Number(query.index || 0),
        options.value
      );
    }
  }
}

由于此时的query中是有vue字段,所以!query.vue的值为false,这次代码就不会走进transformMain函数中了。在else代码在先执行getDescriptor函数拿到descriptor对象,getDescriptor函数代码如下:

function getDescriptor(filename) {
  const _cache = cache;
  if (_cache.has(filename)) {
    return _cache.get(filename);
  }
}

我们在第一次执行transformMain函数的时候会去执行createDescriptor函数,他的作用是根据当前vue文件的code代码字符串生成一个descriptor对象,并且将这个descriptor对象缓存起来了。在getDescriptor函数中就是将缓存的descriptor对象取出来。

由于query中有type=style,所以代码会走到transformStyle函数中。

transformStyle函数

接着将断点走进transformStyle函数,代码如下:

async function transformStyle(code, descriptor, index, options) {
  const block = descriptor.styles[index];
  const result = await options.compiler.compileStyleAsync({
    ...options.style,
    filename: descriptor.filename,
    id: `data-v-${descriptor.id}`,
    source: code,
    scoped: block.scoped,
  });

  return {
    code: result.code,
  };
}

从上面的代码可以看到transformStyle函数依然不是干活的地方,而是调用的@vue/compiler-sfc包暴露出的compileStyleAsync函数。

在调用compileStyleAsync函数的时候有三个参数需要注意:sourceidscoped

source字段的值为code,值是当前css代码字符串。

id字段的值为data-v-${descriptor.id},是不是觉得看着很熟悉?没错他就是使用scoped后vue帮我们自动生成的html自定义属性data-v-x和css选择属性选择器[data-v-x]

其中的descriptor.id就是在生成descriptor对象时根据vue文件路径加密生成的id。

scoped字段的值为block.scoped,而block的值为descriptor.styles[index]。由于一个vue文件可以写多个style标签,所以descriptor对象的styles属性是一个数组,分包对应多个style标签。我们这里只有一个style标签,所以此时的index值为0。block.scoped的值为style标签上面是否有使用scoped

直到进入compileStyleAsync函数之前代码其实一直都还在@vitejs/plugin-vue包中执行,真正干活的地方是在@vue/compiler-sfc包中。

@vue/compiler-sfc

接着将断点走进compileStyleAsync函数,代码如下:

function compileStyleAsync(options) {
  return doCompileStyle({
    ...options,
    isAsync: true,
  });
}

从上面的代码可以看到实际干活的是doCompileStyle函数。

doCompileStyle函数

接着将断点走进doCompileStyle函数,在我们这个场景中简化后的doCompileStyle函数代码如下:

import postcss from "postcss";

function doCompileStyle(options) {
  const {
    filename,
    id,
    scoped = false,
    postcssOptions,
    postcssPlugins,
  } = options;
  const source = options.source;
  const shortId = id.replace(/^data-v-/, "");
  const longId = `data-v-${shortId}`;
  const plugins = (postcssPlugins || []).slice();

  if (scoped) {
    plugins.push(scopedPlugin(longId));
  }

  const postCSSOptions = {
    ...postcssOptions,
    to: filename,
    from: filename,
  };
  let result;
  try {
    result = postcss(plugins).process(source, postCSSOptions);
    return result.then((result) => ({
      code: result.css || "",
      // ...省略
    }));
  } catch (e: any) {
    errors.push(e);
  }
}

doCompileStyle函数中首先使用const定义了一堆变量,我们主要关注sourcelongId

其中的source为当前css代码字符串,longId为根据vue文件路径加密生成的id,值的格式为data-v-x。他就是使用scoped后vue帮我们自动生成的html自定义属性data-v-x和css选择属性选择器[data-v-x]

接着就是判断scoped是否为true,也就是style中使用有使用scoped。如果为true,就将scopedPlugin插件push到plugins数组中。从名字你应该猜到了这个plugin插件就是用于处理css scoped的。

最后就是执行result = postcss(plugins).process(source, postCSSOptions)拿到经过postcss转换编译器处理后的css。

可能有的小伙伴对postcss不够熟悉,我们这里来简单介绍一下。

postcss 是 css 的 transpiler(转换编译器,简称转译器),它对于 css 就像 babel 对于 js 一样,能够做 css 代码的分析和转换。同时,它也提供了插件机制来做自定义的转换。

在我们这里主要就是用到了postcss提供的插件机制来完成css scoped的自定义转换,调用postcss的时候我们传入了source,他的值是style模块中的css代码。并且传入的plugins插件数组中有个scopedPlugin插件,这个自定义插件就是vue写的用于处理css scoped的插件。

在执行postcss对css代码进行转换之前我们在debug终端来看看此时的css代码是什么样的,如下图:
before-postcss

从上图可以看到此时的css代码还是和我们源代码是一样的,并没有css选择属性选择器[data-v-x]

scopedPlugin插件

scopedPlugin插件在我们这个场景中简化后的代码如下:

const scopedPlugin = (id = "") => {
  return {
    postcssPlugin: "vue-sfc-scoped",
    Rule(rule) {
      processRule(id, rule);
    },
    // ...省略
  };
};

这里的id就是我们在doCompileStyle函数中传过来的longId,也就是生成的css选择属性选择器[data-v-x]中的data-v-x

在我们这个场景中只需要关注Rule钩子函数,当postcss处理到选择器开头的规则就会走到Rule钩子函数。

我们这里需要在使用了scoped后给css选择器添加对应的属性选择器[data-v-x],所以我们需要在插件中使用Rule钩子函数,在处理css选择器时手动给选择器后面塞一个属性选择器[data-v-x]

Rule钩子函数打个断点,当postcss处理到我们代码中的.block时就会走到断点中。在debug终端看看rule的值,如下图:
rule

从上图中可以看到此时rule.selector的值为.block,是一个class值为block的类选择器。

processRule函数

将断点走进processRule函数中,在我们这个场景中简化后的processRule函数代码如下:

import selectorParser from "postcss-selector-parser";

function processRule(id: string, rule: Rule) {
  rule.selector = selectorParser((selectorRoot) => {
    selectorRoot.each((selector) => {
      rewriteSelector(id, selector, selectorRoot);
    });
  }).processSync(rule.selector);
}

前面我们讲过rule.selector的值为.block,通过重写rule.selector的值可以将当前css选择器替换为一个新的选择器。在processRule函数中就是使用postcss-selector-parser来解析一个选择器,进行处理后返回一个新的选择器。

processSync方法的作用为接收一个选择器,然后在回调中对解析出来的选择器进行处理,最后将处理后的选择器以字符串的方式进行返回。

在我们这里processSync方法接收的选择器是字符串.block,经过回调函数处理后返回的选择器字符串就变成了.block[data-v-c1c19b25]

我们接下来看selectorParser回调函数中的代码,在回调函数中会使用selectorRoot.each去遍历解析出来的选择器。

为什么这里需要去遍历呢?

答案是css选择器可以这样写:.block.demo,如果是这样的选择器经过解析后,就会被解析成两个选择器,分别是.block.demo

在each遍历中会调用rewriteSelector函数对当前选取器进行重写。

rewriteSelector函数

将断点走进rewriteSelector函数,在我们这个场景中简化后的代码如下:

function rewriteSelector(id, selector) {
  let node;
  const idToAdd = id;

  selector.each((n) => {
    node = n;
  });

  selector.insertAfter(
    node,
    selectorParser.attribute({
      attribute: idToAdd,
      value: idToAdd,
      raws: {},
      quoteMark: `"`,
    })
  );
}

rewriteSelector函数中each遍历当前selector选择器,给node赋值。将断点走到each遍历之后,我们在debug终端来看看selector选择器和node变量。如下图:
selector

在这里selector是container容器,node才是具体要操作的选择器节点。

比如我们这里要执行的selector.insertAfter方法就是在selector容器中在一个指定节点后面去插入一个新的节点。这个和操作浏览器DOM API很相似。

我们再来看看要插入的节点,selectorParser.attribute函数的作用是创建一个attribute属性选择器。在我们这里就是创建一个[data-v-x]的属性选择器,如下图:
attribute

所以这里就是在.block类选择器后面插入一个[data-v-c1c19b25]的属性选择器。

我们在debug终端来看看执行insertAfter函数后的selector选择器,如下图:
after-selector

将断点逐层走出,直到processRule函数中。我们在debug终端来看看此时被重写后的rule.selector字符串的值是什么样的,如下图
after-postcss

原来rule.selector的值为.block,通过重写rule.selector的值可以将.block类选择器替换为一个新的选择器,而这个新的选择器是在原来的.block类选择器后面再塞一个[data-v-c1c19b25]属性选择器。

总结

这篇文章我们讲了当使用scoped后,vue是如何给组件内CSS选择器添加对应的属性选择器[data-v-x]。主要分为两部分,分别在两个包里面执行。

  • 第一部分为在@vitejs/plugin-vue包内执行。

    • 首先会根据当前vue文件的路径进行加密算法生成一个id,这个id就是添加的属性选择器[data-v-x]中的x

    • 然后就是执行transformStyle函数,这个transformStyle并不是实际干活的地方,他调用了@vue/compiler-sfc包的compileStyleAsync函数。并且传入了idcode(css代码字符串)、scoped(是否在style中使用scoped)。

  • 第二部分在@vue/compiler-sfc包执行。

    • compileStyleAsync函数依然不是实际干活的地方,而是调用了doCompileStyle函数。

    • doCompileStyle函数中,如果scoped为true就向plugins数组中插入一个scopedPlugin插件,这个是vue写的postcss插件,用于处理css scoped。然后使用postcss转换编译器对css代码进行转换。

    • postcss处理到选择器开头的规则就会走到scopedPlugin插件中的Rule钩子函数中。在Rule钩子函数中会执行processRule函数。

    • processRule函数中会使用postcss-selector-parser包将当前选择器替换为一个新的选择器,新的选择器和原来的选择器的区别是在后面会添加一个属性选择器[data-v-x]。其中的x就是根据当前vue文件的路径进行加密算法生成的id

在下一篇文章中我们会讲vue是如何给html元素增加自定义属性data-v-x

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1868198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenFAST软件中linux-gnu,linux-intel,macos-gnu,vtk,windows-intel文件的作用

在OpenFAST中&#xff0c;5MW_Land_DLL_WTurb目录下的这五个文件夹分别有不同的用途&#xff0c;主要是为了支持不同操作系统和平台的编译和仿真工作。以下是每个文件夹的总结及其作用&#xff1a; linux-gnu 作用&#xff1a;包含用于GNU编译器套件&#xff08;GCC&#xff09…

私有化部署 Dify 并快速搭建 AI 应用

Dify介绍 Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI 工作流、RAG 管道、Agent、模型管理、可观测性功能等&#xff0c;让您可以快速从原型到生产。以下是其核心功能列表&#xff1a; 1. 工作流: 在画布上构建和测试功能强大的 AI 工作流程&#xff0c;利用…

大数据之路 读书笔记 Day2

大数据之路 读书笔记 Day2 日志采集——浏览器的页面采集 一、分类 #mermaid-svg-ar0WySJJTNk7KvqN {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ar0WySJJTNk7KvqN .error-icon{fill:#552222;}#mermaid-svg-ar0…

keil仿真,查看函数执行时间和执行次数

Execution Profiler执行档案器 The Execution Profiler records timing and execution statistics about instructions for the complete program code. To view the values in the Editor or Disassembly Window, use Show Time or Show Calls from the menu Debug — Executi…

IND83081芯片介绍(一)

一、芯片介绍 IND83081是indiemicro推出的一款高性能的汽车矩阵LED照明控制器&#xff0c;集成了四个子模块&#xff0c;每个子模块包含三个串联的MOSFET开关&#xff0c;每个开关均可通过12位PWM内部信号控制&#xff0c;可配置的上升和下降速率及相位移以实现精确控制&#x…

昇思25天学习打卡营第1天|快速入门-Mnist手写数字识别

学习目标&#xff1a;熟练掌握MindSpore使用方法 学习心得体会&#xff0c;记录时间 了解MindSpore总体架构 学会使用MindSpore 简单应用时间-手写数字识别 一、MindSpore总体架构 华为MindSpore为全场景深度学习框架&#xff0c;开发高效&#xff0c;全场景统一部署特点。 …

如何ubuntu安装wine/deep-wine运行exe程序(包括安装QQ/微信/钉钉)

1.失败的方法&#xff1a; ubuntu22.04尝试下面这个链接方法没有成功&#xff0c; ubuntu22.04安装wine9.0_ubuntu 22.04 wine-CSDN博客 上面链接里面也提供了wine官方方法&#xff0c;链接如下&#xff1a;https://wiki.winehq.org/Ubuntu_zhcn 但是运行最后一个命令时候报…

win10 C:\Users\Administrator

win10 C:\Users\Administrator C:\Users\Administrator\Documents\ C:\Users\Administrator\Pictures C:\Users\Administrator\Favorites C:\Users\Administrator\Links C:\Users\Administrator\Videos

【C++】————内存管理

作者主页&#xff1a; 作者主页 本篇博客专栏&#xff1a;C 创作时间 &#xff1a;2024年6月26日 一、C内存分布 我们先来看一串代码&#xff1a; int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVar 1;int localVar 1;int num1[10] …

电脑怎么设置锁屏密码?这3个方法你知道吗

在日常生活中&#xff0c;电脑已成为我们工作和娱乐的重要工具。为了保护个人信息和数据安全&#xff0c;设置锁屏密码是必不可少的一步。通过设置锁屏密码&#xff0c;您可以有效防止未经授权的访问&#xff0c;确保电脑上的隐私和数据不被泄露。本文将详细介绍电脑怎么设置锁…

STM32_hal_STM32Cude_实现RT—Thread系统

1stm32cude下载系统 1.-2下载显示绿色的为下载成功 2为项目导入系统---点击如下选项 2-1选中如下 意思为 kemel 系统内核 shell shell的实现 device 设备 2-2可以看到项目选项中多了如图选项 3实现led闪烁 3-1 定义两个引脚用于控制led 3-2选择时钟源 3-3更改延迟函数…

Planned independent reguirement can only be maintained via the network

背景&#xff1a;用户上线ps系统&#xff0c;物料用策略70跑需求 但是因为通用料被改了策略&#xff0c;改成其他的了&#xff0c;影响到计划独立需求了。 如果用户不需要了哪个料就会把数量改为0&#xff0c;或者直接删掉物料。之前建议是改成0&#xff0c;这样还有个记录在…

【Python机器学习】交互特征与多项式特征

对于线性模型来说&#xff0c;想要丰富特征&#xff0c;还有一种方法是添加原始数据的交互特征和多项式特征。这种特征工程通常用于统计建模&#xff0c;但也经常用于实际的机器学习应用中。 交互特征 上一篇的例子里&#xff0c;线性模型对wave数据集的的每个箱子都学到一个…

基于稀疏矩阵方法的剪枝压缩模型方案总结

1.简介 1.1目的 在过去的一段时间里&#xff0c;对基于剪枝的模型压缩的算法进行了一系列的实现和实验&#xff0c;特别有引入的稀疏矩阵的方法实现了对模型大小的压缩&#xff0c;以及在部分环节中实现了模型前向算法的加速效果&#xff0c;但是总体上模型加速效果不理想。所…

从零到一打造自己的大模型:模型训练

前言 最近看了很多大模型&#xff0c;也使用了很多大模型。对于大模型理论似乎很了解&#xff0c;但是好像又缺点什么&#xff0c;思来想去决定自己动手实现一个 toy 级别的模型&#xff0c;在实践中加深对大语言模型的理解。 在这个系列的文章中&#xff0c;我将通过亲手实践…

【面试题】Spring面试题

目录 Spring Framework 中有多少个模块&#xff0c;它们分别是什么&#xff1f;Spring框架的设计目标、设计理念&#xff1f;核心是什么&#xff1f;Spring框架中都用到了哪些设计模式&#xff1f;Spring的核心机制是什么&#xff1f;什么是Spring IOC容器&#xff1f;什么是依…

竞赛选题 python区块链实现 - proof of work工作量证明共识算法

文章目录 0 前言1 区块链基础1.1 比特币内部结构1.2 实现的区块链数据结构1.3 注意点1.4 区块链的核心-工作量证明算法1.4.1 拜占庭将军问题1.4.2 解决办法1.4.3 代码实现 2 快速实现一个区块链2.1 什么是区块链2.2 一个完整的快包含什么2.3 什么是挖矿2.4 工作量证明算法&…

鸿蒙面试心得

自疫情过后&#xff0c;java和web前端都进入了冰河时代。年龄、薪资、学历都成了找工作路上躲不开的门槛。 年龄太大pass 薪资要高了pass 学历大专pass 好多好多pass 找工作的路上明明阳关普照&#xff0c;却有一种凄凄惨惨戚戚说不清道不明的“优雅”意境。 如何破局&am…

修复:cannot execute binary file --- ppc64le 系统架构

前言&#xff1a; 修复node_exporter,引用pprof包&#xff0c;对源码编译后在 Linux 系统下执行程序运行时&#xff0c;发生了报错&#xff0c;报错信息&#xff1a;cannot execute binary file: Exec format error。 开始以为编译有问题&#xff0c;检查发现&#xff1b;该l…

正规的外盘期货开户指南避坑!

一&#xff1a;最正规最靠谱的外盘期货开户方式。那就是直开香港账户&#xff0c;需要基本证件、护照、境外卡等。 如果你满足以上条件&#xff0c;可以直接在香港外盘期货公司的营业部或线上官网开户。 优点&#xff1a;安全正规&#xff0c;银期转账。 缺点&#xff1a;保…