Vue3.5中解構props,讓父子組件通信更加絲滑

来源:https://www.cnblogs.com/heavenYJJ/p/18416088
-Advertisement-
Play Games

前言 在Vue3.5版本中響應式 Props 解構終於正式轉正了,這個功能之前一直是試驗性的。這篇文章來帶你搞清楚,一個String類型的props經過解構後明明應該是一個常量了,為什麼還沒丟失響應式呢?本文中使用的Vue版本為歐陽寫文章時的最新版Vue3.5.5 關註公眾號:【前端歐陽】,給自己一 ...


前言

在Vue3.5版本中響應式 Props 解構終於正式轉正了,這個功能之前一直是試驗性的。這篇文章來帶你搞清楚,一個String類型的props經過解構後明明應該是一個常量了,為什麼還沒丟失響應式呢?本文中使用的Vue版本為歐陽寫文章時的最新版Vue3.5.5

關註公眾號:【前端歐陽】,給自己一個進階vue的機會

看個demo

我們先來看個解構props的例子。

父組件代碼如下:

<template>
  <ChildDemo name="ouyang" />
</template>

<script setup lang="ts">
import ChildDemo from "./child.vue";
</script>

父組件代碼很簡單,給子組件傳了一個名為name的prop,name的值為字元串“ouyang”。

子組件的代碼如下:

<template>
  {{ localName }}
</template>

<script setup lang="ts">
const { name: localName } = defineProps(["name"]);
console.log(localName);
</script>

在子組件中我們將name給解構出來了並且賦值給了localName,講道理解構出來的localName應該是個常量會丟失響應式的,其實不會丟失。

我們在瀏覽器中來看一下編譯後的子組件代碼,很簡單,直接在network中過濾子組件的名稱即可,如下圖:
network

從上面可以看到原本的console.log(localName)經過編譯後就變成了console.log(__props.name),這樣當然就不會丟失響應式了。

我們再來看一個另外一種方式解構的例子,這種例子解構後就會丟失響應式,子組件代碼如下:

<template>
  {{ localName }}
</template>

<script setup lang="ts">
const props = defineProps(["name"]);
const { name: localName } = props;
console.log(localName);
</script>

在上面的例子中我們不是直接解構defineProps的返回值,而是將返回值賦值給props對象,然後再去解構props對象拿到localName
network2

從上圖中可以看到這種寫法使用解構的localName時,就不會在編譯階段將其替換為__props.name,這樣的話localName就確實是一個普通的常量了,當然會丟失響應式。

這是為什麼呢?為什麼這種解構寫法就會丟失響應式呢?彆著急,我接下來的文章會講。

從哪裡開下手?

既然這個是在編譯時將localName處理成__props.name,那我們當然是在編譯時debug了。

還是一樣的套路,我們在vscode中啟動一個debug終端。
debug-terminal

在之前的 通過debug搞清楚.vue文件怎麼變成.js文件文章中我們已經知道了vue文件中的<script>模塊實際是由vue/compiler-sfc包的compileScript函數處理的。

compileScript函數位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js

找到compileScript函數就可以給他打一個斷點了。

compileScript函數

debug終端上面執行yarn dev後在瀏覽器中打開對應的頁面,比如: http://localhost:5173/  。此時斷點就會走到compileScript函數中。

在我們這個場景中簡化後的compileScript函數代碼如下:

function compileScript(sfc, options) {
  const ctx = new ScriptCompileContext(sfc, options);
  const scriptSetupAst = ctx.scriptSetupAst;

  // 2.2 process <script setup> body
  for (const node of scriptSetupAst.body) {
    if (node.type === "VariableDeclaration" && !node.declare) {
      const total = node.declarations.length;
      for (let i = 0; i < total; i++) {
        const decl = node.declarations[i];
        const init = decl.init;
        if (init) {
          // defineProps
          const isDefineProps = processDefineProps(ctx, init, decl.id);
        }
      }
    }
  }

  // 3 props destructure transform
  if (ctx.propsDestructureDecl) {
    transformDestructuredProps(ctx);
  }

  return {
    //....
    content: ctx.s.toString(),
  };
}

在之前的 為什麼defineProps巨集函數不需要從vue中import導入?文章中我們已經詳細講解過了compileScript函數中的入參sfc、如何使用ScriptCompileContext類new一個ctx上下文對象。所以這篇文章我們就只簡單說一下他們的作用即可。

  • 入參sfc對象:是一個descriptor對象,descriptor對象是由vue文件編譯來的。descriptor對象擁有template屬性、scriptSetup屬性、style屬性,分別對應vue文件的<template>模塊、<script setup>模塊、<style>模塊。

  • ctx上下文對象:這個ctx對象貫穿了整個script模塊的處理過程,他是根據vue文件的源代碼初始化出來的。在compileScript函數中處理script模塊中的內容,實際就是對ctx對象進行操作。最終ctx.s.toString()就是返回script模塊經過編譯後返回的js代碼。

搞清楚了入參sfc對象和ctx上下文對象,我們接著來看ctx.scriptSetupAst。從名字我想你也能猜到,他就是script模塊中的代碼對應的AST抽象語法樹。如下圖:
scriptSetupAst

從上圖中可以看到body屬性是一個數組,分別對應的是源代碼中的兩行代碼。

數組的第一項對應的Node節點類型是VariableDeclaration,他是一個變數聲明類型的節點。對應的就是源代碼中的第一行:const { name: localName } = defineProps(["name"])

數組中的第二項對應的Node節點類型是ExpressionStatement,他是一個表達式類型的節點。對應的就是源代碼中的第二行:console.log(localName)

我們接著來看compileScript函數中的外層for迴圈,也就是遍歷前面講的body數組,代碼如下:

function compileScript(sfc, options) {
  // ...省略
  // 2.2 process <script setup> body
  for (const node of scriptSetupAst.body) {
    if (node.type === "VariableDeclaration" && !node.declare) {
      const total = node.declarations.length;
      for (let i = 0; i < total; i++) {
        const decl = node.declarations[i];
        const init = decl.init;
        if (init) {
          // defineProps
          const isDefineProps = processDefineProps(ctx, init, decl.id);
        }
      }
    }
  }
  // ...省略
}

我們接著來看外層for迴圈裡面的第一個if語句:

if (node.type === "VariableDeclaration" && !node.declare)

這個if語句的意思是判斷當前的節點類型是不是變數聲明並且確實有初始化的值。

我們這裡的源代碼第一行代碼如下:

const { name: localName } = defineProps(["name"]);

很明顯我們這裡是滿足這個if條件的。

接著在if裡面還有一個內層for迴圈,這個for迴圈是在遍歷node節點的declarations屬性,這個屬性是一個數組。

declarations數組屬性表示當前變數聲明語句中定義的所有變數,可能會定義多個變數,所以他才是一個數組。在我們這裡只定義了一個變數localName,所以 declarations數組中只有一項。

在內層for迴圈,會去遍歷聲明的變數,然後從變數的節點中取出init屬性。我想聰明的你從名字應該就可以看出來init屬性的作用是什麼。

沒錯,init屬性就是對應的變數的初始化值。在我們這裡聲明的localName變數的初始化值就是defineProps(["name"])函數的返回值。

接著就是判斷init是否存在,也就是判斷變數是否是有初始化值。如果為真,那麼就執行processDefineProps(ctx, init, decl.id)判斷初始化值是否是在調用defineProps。換句話說就是判斷當前的變數聲明是否是在調用defineProps巨集函數。

processDefineProps函數

接著將斷點走進processDefineProps函數,在我們這個場景中簡化後的代碼如下:

function processDefineProps(ctx, node, declId) {
  if (!isCallOf(node, DEFINE_PROPS)) {
    return processWithDefaults(ctx, node, declId);
  }
  // handle props destructure
  if (declId && declId.type === "ObjectPattern") {
    processPropsDestructure(ctx, declId);
  }
  return true;
}

processDefineProps函數接收3個參數。

  • 第一個參數ctx,表示當前上下文對象。

  • 第二個參數node,這個節點對應的是變數聲明語句中的初始化值的部分。也就是源代碼中的defineProps(["name"])

  • 第三個參數declId,這個對應的是變數聲明語句中的變數名稱。也就是源代碼中的{ name: localName }

為什麼defineProps巨集函數不需要從vue中import導入?文章中我們已經講過了這裡的第一個if語句就是用於判斷當前是否在執行defineProps函數,如果不是那麼就直接return false

我們接著來看第二個if語句,這個if語句就是判斷當前變數聲明是不是“對象解構賦值”。很明顯我們這裡就是解構出的localName變數,所以代碼將會走到processPropsDestructure函數中。

processPropsDestructure函數

接著將斷點走進processPropsDestructure函數,在我們這個場景中簡化後的代碼如下:

function processPropsDestructure(ctx, declId) {
  const registerBinding = (
    key: string,
    local: string,
    defaultValue?: Expression
  ) => {
    ctx.propsDestructuredBindings[key] = { local, default: defaultValue };
  };

  for (const prop of declId.properties) {
    const propKey = resolveObjectKey(prop.key);
    registerBinding(propKey, prop.value.name);
  }
}

前面講過了這裡的兩個入參,ctx表示當前上下文對象。declId表示變數聲明語句中的變數名稱。

首先定義了一個名為registerBinding的箭頭函數。

接著就是使用for迴圈遍歷declId.properties變數名稱,為什麼會有多個變數名稱呢?

答案是解構的時候我們可以解構一個對象的多個屬性,用於定義多個變數。

prop屬性如下圖:
prop

從上圖可以看到prop中有兩個屬性很顯眼,分別是keyvalue

其中key屬性對應的是解構對象時從對象中要提取出的屬性名,因為我們這裡是解構的name屬性,所以上面的值是name

其中value屬性對應的是解構對象時要賦給的目標變數名稱。我們這裡是賦值給變數localName,所以上面他的值是localName

接著來看for迴圈中的代碼。

執行const propKey = resolveObjectKey(prop.key)拿到要從props對象中解構出的屬性名稱。

將斷點走進resolveObjectKey函數,代碼如下:

function resolveObjectKey(node: Node) {
  switch (node.type) {
    case "Identifier":
      return node.name;
  }
  return undefined;
}

如果當前是標識符節點,也就是有name屬性。那麼就返回name屬性。

最後就是執行registerBinding函數。

registerBinding(propKey, prop.value.name)

第一個參數為傳入解構對象時要提取出的屬性名稱,也就是name。第二個參數為解構對象時要賦給的目標變數名稱,也就是localName

接著將斷點走進registerBinding函數,他就在processPropsDestructure函數裡面。

function processPropsDestructure(ctx, declId) {
  const registerBinding = (
    key: string,
    local: string,
    defaultValue?: Expression
  ) => {
    ctx.propsDestructuredBindings[key] = { local, default: defaultValue };
  };
  // ...省略
}

ctx.propsDestructuredBindings是存在ctx上下文中的一個屬性對象,這個對象裡面存的是需要解構的多個props。

對象的key就是需要解構的props。

key對應的value也是一個對象,這個對象中有兩個欄位。其中的local屬性是解構props後要賦給的變數名稱。default屬性是props的預設值。

在debug終端來看看此時的ctx.propsDestructuredBindings對象是什麼樣的,如下圖:
propsDestructuredBindings

從上圖中就有看到此時裡面已經存了一個name屬性,表示props中的name需要解構,解構出來的變數名為localName,並且預設值為undefined

經過這裡的處理後在ctx上下文對象中的ctx.propsDestructuredBindings中就已經存了有哪些props需要解構,以及解構後要賦值給哪個變數。

有了這個後,後續只需要將script模塊中的所有代碼遍歷一次,然後找出哪些在使用的變數是props解構的變數,比如這裡的localName變數將其替換成__props.name即可。

transformDestructuredProps函數

接著將斷點層層返回,走到最外面的compileScript函數中。再來回憶一下compileScript函數的代碼,如下:

function compileScript(sfc, options) {
  const ctx = new ScriptCompileContext(sfc, options);
  const scriptSetupAst = ctx.scriptSetupAst;

  // 2.2 process <script setup> body
  for (const node of scriptSetupAst.body) {
    if (node.type === "VariableDeclaration" && !node.declare) {
      const total = node.declarations.length;
      for (let i = 0; i < total; i++) {
        const decl = node.declarations[i];
        const init = decl.init;
        if (init) {
          // defineProps
          const isDefineProps = processDefineProps(ctx, init, decl.id);
        }
      }
    }
  }

  // 3 props destructure transform
  if (ctx.propsDestructureDecl) {
    transformDestructuredProps(ctx);
  }

  return {
    //....
    content: ctx.s.toString(),
  };
}

經過processDefineProps函數的處理後,ctx.propsDestructureDecl對象中已經存了有哪些變數是由props解構出來的。

這裡的if (ctx.propsDestructureDecl)條件當然滿足,所以代碼會走到transformDestructuredProps函數中。

接著將斷點走進transformDestructuredProps函數中,在我們這個場景中簡化後的transformDestructuredProps函數代碼如下:

import { walk } from 'estree-walker'

function transformDestructuredProps(ctx) {
  const rootScope = {};
  let currentScope = rootScope;
  const propsLocalToPublicMap: Record<string, string> = Object.create(null);

  const ast = ctx.scriptSetupAst;

  for (const key in ctx.propsDestructuredBindings) {
    const { local } = ctx.propsDestructuredBindings[key];
    rootScope[local] = true;
    propsLocalToPublicMap[local] = key;
  }

  walk(ast, {
    enter(node: Node) {
      if (node.type === "Identifier") {
        if (currentScope[node.name]) {
          rewriteId(node);
        }
      }
    },
  });

  function rewriteId(id: Identifier) {
    // x --> __props.x
    ctx.s.overwrite(
      id.start! + ctx.startOffset!,
      id.end! + ctx.startOffset!,
      genPropsAccessExp(propsLocalToPublicMap[id.name])
    );
  }
}

transformDestructuredProps函數中主要分為三塊代碼,分別是for迴圈、執行walk函數、定義rewriteId函數。

我們先來看第一個for迴圈,他是遍歷ctx.propsDestructuredBindings對象。前面我們講過了這個對象中存的屬性key是解構了哪些props,比如這裡就是解構了name這個props。

接著就是使用const { local } = ctx.propsDestructuredBindings[key]拿到解構的props在子組件中賦值給了哪個變數,我們這裡是解構出來後賦給了localName變數,所以這裡的local的值為字元串"localName"。

由於在我們這個demo中只有兩行代碼,分別是解構props和console.log。沒有其他的函數,所以這裡的作用域只有一個。也就是說rootScope始終等於currentScope

所以這裡執行rootScope[local] = true後,currentScope對象中的localName屬性也會被賦值true。如下圖:
currentScope

接著就是執行propsLocalToPublicMap[local] = key,這裡的local存的是解構props後賦值給子組件中的變數名稱,key為解構了哪個props。經過這行代碼的處理後我們就形成了一個映射,後續根據這個映射就能輕鬆的將script模塊中使用解構後的localName的地方替換為__props.name

propsLocalToPublicMap對象如下圖:
propsLocalToPublicMap

經過這個for迴圈的處理後,我們已經知道了有哪些變數其實是經過props解構來的,以及這些解構得到的變數和props的映射關係。

接下來就是使用walk函數去遞歸遍歷script模塊中的所有代碼,這個遞歸遍歷就是遍歷script模塊對應的AST抽象語法樹。

在這裡是使用的walk函數來自於第三方庫estree-walker

在遍歷語法樹中的某個節點時,進入的時候會觸發一次enter回調,出去的時候會觸發一次leave回調。

walk函數的執行代碼如下:

walk(ast, {
  enter(node: Node) {
    if (node.type === "Identifier") {
      if (currentScope[node.name]) {
        rewriteId(node);
      }
    }
  },
});

我們這個場景中只需要enter進入的回調就行了。

enter回調中使用外層if判斷當前節點的類型是不是IdentifierIdentifier類型可能是變數名、函數名等。

我們源代碼中的console.log(localName)中的localName就是一個變數名,當遞歸遍歷AST抽象語法樹遍歷到這裡的localName對應的節點時就會滿足外層的if條件。

在debug終端來看看此時滿足外層if條件的node節點,如下圖:
node

從上面的代碼可以看到此時的node節點中對應的變數名為localName。其中startend分別表示localName變數的開始位置和結束位置。

我們回憶一下前面講過了currentScope對象中就是存的是有哪些本地的變數是通過props解構得到的,這裡的localName變數當然是通過props解構得到的,滿足裡層的if條件判斷。

最後代碼會走進rewriteId函數中,將斷點走進rewriteId函數中,簡化後的代碼如下:

function rewriteId(id: Identifier) {
  // x --> __props.x
  ctx.s.overwrite(
    id.start + ctx.startOffset,
    id.end + ctx.startOffset,
    genPropsAccessExp(propsLocalToPublicMap[id.name])
  );
}

這裡使用了ctx.s.overwrite方法,這個方法接收三個參數。

第一個參數是:開始位置,對應的是變數localName在源碼中的開始位置。

第二個參數是:結束位置,對應的是變數localName在源碼中的結束位置。

第三個參數是想要替換成的新內容。

第三個參數是由genPropsAccessExp函數返回的,執行這個函數時傳入的是propsLocalToPublicMap[id.name]

前面講過了propsLocalToPublicMap存的是props名稱和解構到本地的變數名稱的映射關係,id.name是解構到本地的變數名稱。如下圖:
overwrite

所以propsLocalToPublicMap[id.name]的執行結果就是name,也就是名為name的props。

接著將斷點走進genPropsAccessExp函數,簡化後的代碼如下:

const identRE = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/;
function genPropsAccessExp(name: string): string {
  return identRE.test(name)
    ? `__props.${name}`
    : `__props[${JSON.stringify(name)}]`;
}

使用正則表達式去判斷如果滿足條件就會返回__props.${name},否則就是返回__props[${JSON.stringify(name)}]

很明顯我們這裡的name當然滿足條件,所以genPropsAccessExp函數會返回__props.name

那麼什麼情況下不會滿足條件呢?

比如這樣的props:

const { "first-name": firstName } = defineProps(["first-name"]);
console.log(firstName);

這種props在這種情況下就會返回__props["first-name"]

執行完genPropsAccessExp函數後回到ctx.s.overwrite方法的地方,此時我們已經知道了第三個參數的值為__props.name。這個方法的執行會將localName重寫為__props.name

ctx.s.overwrite方法執行之前我們來看看此時的script模塊中的js代碼是什麼樣的,如下圖:
before

從上圖中可以看到此時的代碼中console.log裡面還是localName

執行完ctx.s.overwrite方法後,我們來看看此時是什麼樣的,如下圖:
after

從上圖中可以看到此時的代碼中console.log裡面已經變成了__props.name

這就是在編譯階段將使用到的解構localName變數變成__props.name的完整過程。

這會兒我們來看前面那個例子解構後丟失響應式的例子,我想你就很容易想通了。

<script setup lang="ts">
const props = defineProps(["name"]);
const { name: localName } = props;
console.log(localName);
</script>

在處理defineProps巨集函數時,發現是直接解構了返回值才會進行處理。上面這個例子中沒有直接進行解構,而是將其賦值給props,然後再去解構props。這種情況下ctx.propsDestructuredBindings對象中什麼都沒有。

後續在遞歸遍歷script模塊中的所有代碼,發現ctx.propsDestructuredBindings對象中什麼都沒有。自然也不會將localName替換為__props.name,這樣他當然就會丟失響應式了。

總結

在編譯階段首先會處理巨集函數defineProps,在處理的過程中如果發現解構了defineProps的返回值,那麼就會將解構的name屬性,以及name解構到本地的localName變數,都全部一起存到ctx.propsDestructuredBindings對象中。

接下來就會去遞歸遍歷script模塊中的所有代碼,如果發現使用的localName變數能夠在ctx.propsDestructuredBindings對象中找的到。那麼就說明這個localName變數是由props解構得到的,就會將其替換為__props.name,所以使用解構後的props依然不會丟失響應式。

關註公眾號:【前端歐陽】,給自己一個進階vue的機會

另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • p-limit 是一個控制請求併發數量的庫,他的整體代碼不多,思路挺好的,很有學習價值; 舉例 當我們同時發起多個請求時,一般是這樣做的 Promise.all([ requestFn1, requestFn2, requestFn3 ]).then(res =>{}) 或者 requestFn1( ...
  • 記得去年年初,我就折騰了“我的足跡”功能,當時就想在標記點 markers 中添加圖片,但苦於冇技術,就擱淺了,只實現了 markers 文字描述。今天終於讓我找到了大佬的教程,實現了 markers 中添加描述、多圖片、指定鏈接,用的還是熟悉的 Jvectormap 。 先給大佬曝光一下: 空木白 ...
  • title: Nuxt Kit API :路徑解析工具 date: 2024/9/22 updated: 2024/9/22 author: cmdragon excerpt: 摘要:本文介紹了Nuxt Kit中用於解析路徑的API工具,包括resolvePath、resolveAlias、find ...
  • Easy-Blog 是一套集成文章發表、頁面創建、知識庫管理、博客後臺管理等功能於一體的博客系統。Github項目地址:https://github.com/fecommunity/easy-blog, 歡迎Star。 ...
  • title: Nuxt Kit中的 Nitro 處理程式 date: 2024/9/21 updated: 2024/9/21 author: cmdragon excerpt: 摘要:本文詳細介紹了在Nuxt 3框架中使用Nitro伺服器引擎的實踐,包括創建處理程式處理HTTP請求、路由和中間件的 ...
  • title: Nuxt Kit 中的模板處理 date: 2024/9/20 updated: 2024/9/20 author: cmdragon excerpt: 摘要:本文詳細介紹了在Nuxt 3框架中,使用Nuxt Kit進行模板處理的方法,包括理解模板基本概念、使用addTemplate動 ...
  • title: Nuxt Kit 中的插件:創建與使用 date: 2024/9/19 updated: 2024/9/19 author: cmdragon excerpt: 摘要:本文介紹了在 Nuxt 3 框架中使用 Nuxt Kit 創建和管理插件的方法,包括使用addPlugin註冊插件、創 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 一.webpack和vite的區別 1.構建速度不同 Webpack: Webpack的構建速度相對較慢,尤其在大型項目中,因為它需要分析整個依賴圖,進行多次文件掃描和轉譯。 Vite: Vite以開發模式下的極速構建著稱。它利用ES模塊的特性 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...