為什麼defineProps巨集函數不需要從vue中import導入?

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

前言 我們每天寫vue代碼時都在用defineProps,但是你有沒有思考過下麵這些問題。為什麼defineProps不需要import導入?為什麼不能在非setup頂層使用defineProps?defineProps是如何將聲明的 props 自動暴露給模板? 舉幾個例子 我們來看幾個例子,分別 ...


前言

我們每天寫vue代碼時都在用defineProps,但是你有沒有思考過下麵這些問題。為什麼defineProps不需要import導入?為什麼不能在非setup頂層使用definePropsdefineProps是如何將聲明的 props 自動暴露給模板?

舉幾個例子

我們來看幾個例子,分別對應上面的幾個問題。

先來看一個正常的例子,common-child.vue文件代碼如下:

<template>
  <div>content is {{ content }}</div>
</template>

<script setup lang="ts">
defineProps({
  content: String,
});
</script>

我們看到在這個正常的例子中沒有從任何地方import導入defineProps,直接就可以使用了,並且在template中渲染了props中的content

我們再來看一個在非setup頂層使用defineProps的例子,if-child.vue文件代碼如下:

<template>
  <div>content is {{ content }}</div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const count = ref(10);

if (count.value) {
  defineProps({
    content: String,
  });
}
</script>

代碼跑起來直接就報錯了,提示defineProps is not defined

通過debug搞清楚上面幾個問題

在我的上一篇文章 vue文件是如何編譯為js文件 中已經帶你搞清楚了vue文件中的<script>模塊是如何編譯成瀏覽器可直接運行的js代碼,其實底層就是依靠vue/compiler-sfc包的compileScript函數。

當然如果你還沒看過我的上一篇文章也不影響這篇文章閱讀,這裡我會簡單說一下。當我們import一個vue文件時會觸發@vitejs/plugin-vue包的transform鉤子函數,在這個函數中會調用一個transformMain函數。transformMain函數中會調用genScriptCodegenTemplateCodegenStyleCode,分別對應的作用是將vue文件中的<script>模塊編譯為瀏覽器可直接運行的js代碼、將<template>模塊編譯為render函數、將<style>模塊編譯為導入css文件的import語句。genScriptCode函數底層調用的就是vue/compiler-sfc包的compileScript函數。

一樣的套路,首先我們在vscode的打開一個debug終端。
debug-terminal

然後在node_modules中找到vue/compiler-sfc包的compileScript函數打上斷點,compileScript函數位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js。在debug終端上面執行yarn dev後在瀏覽器中打開對應的頁面,比如:http://localhost:5173/ 。此時斷點就會走到compileScript函數中,我們在debug中先來看看compileScript函數的第一個入參sfcsfc.filename的值為當前編譯的vue文件路徑。由於每編譯一個vue文件都要走到這個debug中,現在我們只想debug看看common-child.vue文件,所以為了方便我們在compileScript中加了下麵這樣一段代碼,並且去掉了在compileScript函數中加的斷點,這樣就只有編譯common-child.vue文件時會走進斷點。
debug-common

compileScript函數

我們再來回憶一下common-child.vue文件中的script模塊代碼如下:

<script setup lang="ts">
defineProps({
  content: String,
});
</script>

我們接著來看compileScript函數的入參sfc,在上一篇文章 vue文件是如何編譯為js文件 中我們已經講過了sfc是一個descriptor對象,descriptor對象是由vue文件編譯來的。descriptor對象擁有template屬性、scriptSetup屬性、style屬性,分別對應vue文件的<template>模塊、<script setup>模塊、<style>模塊。在我們這個場景只關註scriptSetup屬性,sfc.scriptSetup.content的值就是<script setup>模塊中code代碼字元串,sfc.source的值就是vue文件中的源代碼code字元串。詳情查看下圖:

common-child

compileScript函數內包含了編譯script模塊的所有的邏輯,代碼很複雜,光是源代碼就接近1000行。這篇文章我們不會去通讀compileScript函數的所有功能,只會講處理defineProps相關的代碼。下麵這個是我簡化後的代碼:

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

  for (const node of scriptSetupAst.body) {
    if (node.type === "ExpressionStatement") {
      const expr = node.expression;
      if (processDefineProps(ctx, expr)) {
        ctx.s.remove(node.start + startOffset, node.end + startOffset);
      }
    }
    if (node.type === "VariableDeclaration" && !node.declare || node.type.endsWith("Statement")) {
      // ....
    }
  }

  ctx.s.remove(0, startOffset);
  ctx.s.remove(endOffset, source.length);

  let runtimeOptions = ``;
  const propsDecl = genRuntimeProps(ctx);
  if (propsDecl) runtimeOptions += `\n  props: ${propsDecl},`;

  const def =
    (defaultExport ? `\n  ...${normalScriptDefaultVar},` : ``) +
    (definedOptions ? `\n  ...${definedOptions},` : "");
  ctx.s.prependLeft(
    startOffset,
    `\n${genDefaultAs} /*#__PURE__*/${ctx.helper(
      `defineComponent`
    )}({${def}${runtimeOptions}\n  ${
      hasAwait ? `async ` : ``
    }setup(${args}) {\n${exposeCall}`
  );
  ctx.s.appendRight(endOffset, `})`);

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

compileScript函數中首先調用ScriptCompileContext類生成一個ctx上下文對象,然後遍歷vue文件的<script setup>模塊生成的AST抽象語法樹。如果節點類型為ExpressionStatement表達式語句,那麼就執行processDefineProps函數,判斷當前表達式語句是否是調用defineProps函數。如果是那麼就刪除掉defineProps調用代碼,並且將調用defineProps函數時傳入的參數對應的node節點信息存到ctx上下文中。然後從參數node節點信息中拿到調用defineProps巨集函數時傳入的props參數的開始位置和結束位置。再使用slice方法並且傳入開始位置和結束位置,從<script setup>模塊的代碼字元串中截取到props定義的字元串。然後將截取到的props定義的字元串拼接到vue組件對象的字元串中,最後再將編譯後的setup函數代碼字元串拼接到vue組件對象的字元串中。

ScriptCompileContext

ScriptCompileContext類中我們主要關註這幾個屬性:startOffsetendOffsetscriptSetupAsts。先來看看他的constructor,下麵是我簡化後的代碼。

import MagicString from 'magic-string'

class ScriptCompileContext {
  source = this.descriptor.source
  s = new MagicString(this.source)
  startOffset = this.descriptor.scriptSetup?.loc.start.offset
  endOffset = this.descriptor.scriptSetup?.loc.end.offset

  constructor(descriptor, options) {
    this.s = new MagicString(this.source);
    this.scriptSetupAst = descriptor.scriptSetup && parse(descriptor.scriptSetup.content, this.startOffset);
  }
}

在前面我們已經講過了descriptor.scriptSetup對象就是由vue文件中的<script setup>模塊編譯而來,startOffsetendOffset分別就是descriptor.scriptSetup?.loc.start.offsetdescriptor.scriptSetup?.loc.end.offset,對應的是<script setup>模塊在vue文件中的開始位置和結束位置。

descriptor.source的值就是vue文件中的源代碼code字元串,這裡以descriptor.source為參數new了一個MagicString對象。magic-string是由svelte的作者寫的一個庫,用於處理字元串的JavaScript庫。它可以讓你在字元串中進行插入、刪除、替換等操作,並且能夠生成準確的sourcemapMagicString對象中擁有toStringremoveprependLeftappendRight等方法。s.toString用於生成返回的字元串,我們來舉幾個例子看看這幾個方法你就明白了。

s.remove( start, end )用於刪除從開始到結束的字元串:

const s = new MagicString('hello word');
s.remove(0, 6);
s.toString(); // 'word'

s.prependLeft( index, content )用於在指定index的前面插入字元串:

const s = new MagicString('hello word');
s.prependLeft(5, 'xx');
s.toString(); // 'helloxx word'

s.appendRight( index, content )用於在指定index的後面插入字元串:

const s = new MagicString('hello word');
s.appendRight(5, 'xx');
s.toString(); // 'helloxx word'

我們接著看constructor中的scriptSetupAst屬性是由一個parse函數的返回值賦值,parse(descriptor.scriptSetup.content, this.startOffset)parse函數的代碼如下:

import { parse as babelParse } from '@babel/parser'

function parse(input: string, offset: number): Program {
  try {
    return babelParse(input, {
      plugins,
      sourceType: 'module',
    }).program
  } catch (e: any) {
  }
}

我們在前面已經講過了descriptor.scriptSetup.content的值就是vue文件中的<script setup>模塊的代碼code字元串,parse函數中調用了babel提供的parser函數,將vue文件中的<script setup>模塊的代碼code字元串轉換成AST抽象語法樹

parser

現在我們再來看compileScript函數中的這幾行代碼你理解起來就沒什麼難度了,這裡的scriptSetupAst變數就是由vue文件中的<script setup>模塊的代碼轉換成的AST抽象語法樹

const ctx = new ScriptCompileContext(sfc, options);
const startOffset = ctx.startOffset;
const endOffset = ctx.endOffset;
const scriptSetupAst = ctx.scriptSetupAst;

流程圖如下:
progress1

processDefineProps函數

我們接著將斷點走到for迴圈開始處,代碼如下:

for (const node of scriptSetupAst.body) {
  if (node.type === "ExpressionStatement") {
    const expr = node.expression;
    if (processDefineProps(ctx, expr)) {
      ctx.s.remove(node.start + startOffset, node.end + startOffset);
    }
  }
}

遍歷AST抽象語法樹,如果當前節點類型為ExpressionStatement表達式語句,並且processDefineProps函數執行結果為true就調用ctx.s.remove方法。這會兒斷點還在for迴圈開始處,在控制台執行ctx.s.toString()看看當前的code代碼字元串。
for-toString

從圖上可以看見此時toString的執行結果還是和之前的common-child.vue源代碼是一樣的,並且很明顯我們的defineProps是一個表達式語句,所以會執行processDefineProps函數。我們將斷點走到調用processDefineProps的地方,看到簡化過的processDefineProps函數代碼如下:

const DEFINE_PROPS = "defineProps";
function processDefineProps(ctx, node, declId) {
  if (!isCallOf(node, DEFINE_PROPS)) {
    return processWithDefaults(ctx, node, declId);
  }
  ctx.propsRuntimeDecl = node.arguments[0];
  return true;
}

processDefineProps函數中首先執行了isCallOf函數,第一個參數傳的是當前的AST語法樹中的node節點,第二個參數傳的是"defineProps"字元串。從isCallOf的名字中我們就可以猜出他的作用是判斷當前的node節點的類型是不是在調用defineProps函數,isCallOf的代碼如下:

export function isCallOf(node, test) {
  return !!(
    node &&
    test &&
    node.type === "CallExpression" &&
    node.callee.type === "Identifier" &&
    (typeof test === "string"
      ? node.callee.name === test
      : test(node.callee.name))
  );
}

isCallOf函數接收兩個參數,第一個參數node是當前的node節點,第二個參數test是要判斷的函數名稱,在我們這裡是寫死的"defineProps"字元串。我們在debug console中將node.typenode.callee.typenode.callee.name的值列印出來看看。
isCallOf

從圖上看到node.typenode.callee.typenode.callee.name的值後,可以證明我們的猜測是正確的這裡isCallOf的作用是判斷當前的node節點的類型是不是在調用defineProps函數。我們這裡的node節點確實是在調用defineProps函數,所以isCallOf的執行結果為true,在processDefineProps函數中是對isCallOf函數的執行結果取反。也就是!isCallOf(node, DEFINE_PROPS)的執行結果為false,所以不會走到return processWithDefaults(ctx, node, declId);

我們接著來看processDefineProps函數:

function processDefineProps(ctx, node, declId) {
  if (!isCallOf(node, DEFINE_PROPS)) {
    return processWithDefaults(ctx, node, declId);
  }
  ctx.propsRuntimeDecl = node.arguments[0];
  return true;
}

如果當前節點確實是在執行defineProps函數,那麼就會執行ctx.propsRuntimeDecl = node.arguments[0];。將當前node節點的第一個參數賦值給ctx上下文對象的propsRuntimeDecl屬性,這裡的第一個參數其實就是調用defineProps函數時給傳入的第一個參數。為什麼寫死成取arguments[0]呢?是因為defineProps函數只接收一個參數,傳入的參數可以是一個對象或者數組。比如:

const props = defineProps({
  foo: String
})

const props = defineProps(['foo', 'bar'])

記住這個在ctx上下文上面塞的propsRuntimeDecl屬性,後面生成運行時的props就是根據propsRuntimeDecl屬性生成的。

至此我們已經瞭解到了processDefineProps中主要做了兩件事:判斷當前執行的表達式語句是否是defineProps函數,如果是那麼將解析出來的props屬性的信息塞的ctx上下文的propsRuntimeDecl屬性中。

我們這會兒來看compileScript函數中的processDefineProps代碼你就能很容易理解了:

for (const node of scriptSetupAst.body) {
  if (node.type === "ExpressionStatement") {
    const expr = node.expression;
    if (processDefineProps(ctx, expr)) {
      ctx.s.remove(node.start + startOffset, node.end + startOffset);
    }
  }
}

遍歷AST語法樹,如果當前節點類型是ExpressionStatement表達式語句,再執行processDefineProps判斷當前node節點是否是執行的defineProps函數。如果是defineProps函數就調用ctx.s.remove方法將調用defineProps函數的代碼從源代碼中刪除掉。此時我們在debug console中執行ctx.s.toString(),看到我們的code代碼字元串中已經沒有了defineProps了:
remove-toString

現在我們能夠回答第一個問題了:

為什麼defineProps不需要import導入?

因為在編譯過程中如果當前AST抽象語法樹的節點類型是ExpressionStatement表達式語句,並且調用的函數是defineProps,那麼就調用remove方法將調用defineProps函數的代碼給移除掉。既然defineProps語句已經被移除了,自然也就不需要import導入了defineProps了。
progress2

genRuntimeProps函數

接著在compileScript函數中執行了兩條remove代碼:

ctx.s.remove(0, startOffset);
ctx.s.remove(endOffset, source.length);

這裡的startOffset表示script標簽中第一個代碼開始的位置, 所以ctx.s.remove(0, startOffset);的意思是刪除掉包含<script setup>開始標簽前面的所有內容,也就是刪除掉template模塊的內容和<script setup>開始標簽。這行代碼執行完後我們再看看ctx.s.toString()的值:
remove1

接著執行ctx.s.remove(endOffset, source.length);,這行代碼的意思是將</script >包含結束標簽後面的內容全部刪掉,也就是刪除</script >結束標簽和<style>模塊。這行代碼執行完後我們再來看看ctx.s.toString()的值:
remove2

由於我們的common-child.vuescript模塊中只有一個defineProps函數,所以當移除掉template模塊、style模塊、script開始標簽和結束標簽後就變成了一個空字元串。如果你的script模塊中還有其他js業務代碼,當代碼執行到這裡後就不會是空字元串,而是那些js業務代碼。

我們接著將compileScript函數中的斷點走到調用genRuntimeProps函數處,代碼如下:

let runtimeOptions = ``;
const propsDecl = genRuntimeProps(ctx);
if (propsDecl) runtimeOptions += `\n  props: ${propsDecl},`;

genRuntimeProps名字你應該已經猜到了他的作用,根據ctx上下文生成運行時的props。我們將斷點走到genRuntimeProps函數內部,在我們這個場景中genRuntimeProps主要執行的代碼如下:

function genRuntimeProps(ctx) {
  let propsDecls;
  if (ctx.propsRuntimeDecl) {
    propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim();
  }
  return propsDecls;
}

還記得這個ctx.propsRuntimeDecl是什麼東西嗎?我們在執行processDefineProps函數判斷當前節點是否為執行defineProps函數的時候,就將調用defineProps函數的參數node節點賦值給ctx.propsRuntimeDecl。換句話說ctx.propsRuntimeDecl中擁有調用defineProps函數傳入的props參數中的節點信息。我們將斷點走進ctx.getString函數看看是如何取出props的:

getString(node, scriptSetup = true) {
  const block = scriptSetup ? this.descriptor.scriptSetup : this.descriptor.script;
  return block.content.slice(node.start, node.end);
}

我們前面已經講過了descriptor對象是由vue文件編譯而來,其中的scriptSetup屬性就是對應的<script setup>模塊。我們這裡沒有傳入scriptSetup,所以block的值為this.descriptor.scriptSetup。同樣我們前面也講過scriptSetup.content的值是<script setup>模塊code代碼字元串。請看下圖:
block-content

這裡傳入的node節點就是我們前面存在上下文中ctx.propsRuntimeDecl,也就是在調用defineProps函數時傳入的參數節點,node.start就是參數節點開始的位置,node.end就是參數節點的結束位置。所以使用content.slice方法就可以截取出來調用defineProps函數時傳入的props定義。請看下圖:
slice

現在我們再回過頭來看compileScript函數中的調用genRuntimeProps函數的代碼你就能很容易理解了:

let runtimeOptions = ``;
const propsDecl = genRuntimeProps(ctx);
if (propsDecl) runtimeOptions += `\n  props: ${propsDecl},`;

這裡的propsDecl在我們這個場景中就是使用slice截取出來的props定義,再使用\n props: ${propsDecl},進行字元串拼接就得到了runtimeOptions的值。如圖:
runtimeOptions

看到runtimeOptions的值是不是就覺得很熟悉了,又有name屬性,又有props屬性。其實就是vue組件對象的code字元串的一部分。name拼接邏輯是在省略的代碼中,我們這篇文章只講props相關的邏輯,所以name不在這篇文章的討論範圍內。

現在我們能夠回答前面提的第三個問題了。

defineProps是如何將聲明的 props 自動暴露給模板?

編譯時在移除掉defineProps相關代碼時會將調用defineProps函數時傳入的參數node節點信息存到ctx上下文中。遍歷完AST抽象語法樹後,然後從上下文中存的參數node節點信息中拿到調用defineProps巨集函數時傳入props的開始位置和結束位置。再使用slice方法並且傳入開始位置和結束位置,從<script setup>模塊的代碼字元串中截取到props定義的字元串。然後將截取到的props定義的字元串拼接到vue組件對象的字元串中,這樣vue組件對象中就有了一個props屬性,這個props屬性在template模版中可以直接使用。

progress3

拼接成完整的瀏覽器運行時js代碼

我們再來看compileScript函數中的最後一坨代碼;

const def =
  (defaultExport ? `\n  ...${normalScriptDefaultVar},` : ``) +
  (definedOptions ? `\n  ...${definedOptions},` : "");
ctx.s.prependLeft(
  startOffset,
  `\n${genDefaultAs} /*#__PURE__*/${ctx.helper(
    `defineComponent`
  )}({${def}${runtimeOptions}\n  ${
    hasAwait ? `async ` : ``
  }setup(${args}) {\n${exposeCall}`
);
ctx.s.appendRight(endOffset, `})`);

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

這裡先調用了ctx.s.prependLeft方法給字元串開始的地方插入了一串字元串,這串拼接的字元串看著腦瓜子痛,我們直接在debug console上面看看要拼接的字元串是什麼樣的:
append

看到這串你應該很熟悉,除了前面我們拼接的nameprops之外還有部分setup編譯後的代碼,其實這就是vue組件對象的code代碼字元串的一部分。

當斷點執行完prependLeft方法後,我們在debug console中再看看此時的ctx.s.toString()的值是什麼樣的:
last-toString

從圖上可以看到vue組件對象上的name屬性、props屬性、setup函數基本已經拼接的差不多了,只差一個})結束符號,所以執行ctx.s.appendRight(endOffset, }));將結束符號插入進去。

我們最後再來看看compileScript函數的返回對象中的content屬性,也就是ctx.s.toString()content屬性的值就是vue組件中的<script setup>模塊編譯成瀏覽器可執行的js代碼字元串。
full

為什麼不能在非setup頂層使用defineProps

同樣的套路我們來debug看看if-child.vue文件,先來回憶一下if-child.vue文件的代碼。

<template>
  <div>content is {{ content }}</div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const count = ref(10);
if (count.value) {
  defineProps({
    content: String,
  });
}
</script>

將斷點走到compileScript函數的遍歷AST抽象語法樹的地方,我們看到scriptSetupAst.body數組中有三個node節點。
if-node-list

從圖中我們可以看到這三個node節點類型分別是:ImportDeclarationVariableDeclarationIfStatement。很明顯這三個節點對應的是我們源代碼中的import語句、const定義變數、if 模塊。我們再來回憶一下compileScript函數中的遍歷AST抽象語法樹的代碼:

function compileScript(sfc, options) {
   // 省略..
  for (const node of scriptSetupAst.body) {
    if (node.type === "ExpressionStatement") {
      const expr = node.expression;
      if (processDefineProps(ctx, expr)) {
        ctx.s.remove(node.start + startOffset, node.end + startOffset);
      }
    }

    if (
      (node.type === "VariableDeclaration" && !node.declare) ||
      node.type.endsWith("Statement")
    ) {
      // ....
    }
  }
  // 省略..
}

從代碼我們就可以看出來第三個node節點,也就是在if中使用defineProps的代碼,這個節點類型為IfStatement,不等於ExpressionStatement,所以代碼不會走到processDefineProps函數中,也不會執行remove方法刪除掉調用defineProps函數的代碼。當代碼運行在瀏覽器時由於我們沒有從任何地方import導入defineProps,當然就會報錯defineProps is not defined

總結

現在我們能夠回答前面提的三個問題了。

  • 為什麼defineProps不需要import導入?

    因為在編譯過程中如果當前AST抽象語法樹的節點類型是ExpressionStatement表達式語句,並且調用的函數是defineProps,那麼就調用remove方法將調用defineProps函數的代碼給移除掉。既然defineProps語句已經被移除了,自然也就不需要import導入了defineProps了。

  • 為什麼不能在非setup頂層使用defineProps

    因為在非setup頂層使用defineProps的代碼生成AST抽象語法樹後節點類型就不是ExpressionStatement表達式語句類型,只有ExpressionStatement表達式語句類型才會走到processDefineProps函數中,並且調用remove方法將調用defineProps函數的代碼給移除掉。當代碼運行在瀏覽器時由於我們沒有從任何地方import導入defineProps,當然就會報錯defineProps is not defined

  • defineProps是如何將聲明的 props 自動暴露給模板?

    編譯時在移除掉defineProps相關代碼時會將調用defineProps函數時傳入的參數node節點信息存到ctx上下文中。遍歷完AST抽象語法樹後,然後從上下文中存的參數node節點信息中拿到調用defineProps巨集函數時傳入props的開始位置和結束位置。再使用slice方法並且傳入開始位置和結束位置,從<script setup>模塊的代碼字元串中截取到props定義的字元串。然後將截取到的props定義的字元串拼接到vue組件對象的字元串中,這樣vue組件對象中就有了一個props屬性,這個props屬性在template模版中可以直接使用。

關註公眾號:前端歐陽,解鎖我更多vue乾貨文章,並且可以免費向我咨詢vue相關問題。
qrcode


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

-Advertisement-
Play Games
更多相關文章
  • XML 是一種用於存儲和傳輸數據的與軟體和硬體無關的工具。 什麼是XML? XML代表eXtensible Markup Language(可擴展標記語言)。XML是一種與HTML非常相似的標記語言。XML被設計用於存儲和傳輸數據。XML被設計成具有自我描述性。XML不執行任何操作,也許有點難理解, ...
  • HTML特性: 1.空白摺疊現象 1.文字間折為一個空格 <p>hello world!</p> 2.標簽內壁空白忽略 <p> hello world! </p> 2.轉義字元 <p>小於號&lt;</p> <p>大於號&gt;</p> <p>空格&nbsp;</p> <p>版權號&copy;</p ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、介紹 Generator 函數是 ES6 提供的一種非同步編程解決方案,語法行為與傳統函數完全不同 回顧下上文提到的解決非同步的手段: 回調函數 promise 那麼,上文我們提到promsie已經是一種比較流行的解決非同步方案,那麼為什麼 ...
  • 涉及的技術棧 vue3 vite bootstrap5 背景 在用bootstrap5的時候遇到一個問題,就是offcanvas在nav上的時候居然會有兩個背景BackDrop,關閉之後頁面上還有一個backdrop留在那 bootstrap5文檔裡面提供了幾個Method可以控制Offcanvas ...
  • 本文介紹三種使用純 CSS 實現星級評分的方式。每種都值得細品一番~ 五角星取自 Element Plus 的 svg 資源 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""> <path fill="c ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、介紹 定義: 用於定義基本操作的自定義行為 本質: 修改的是程式預設形為,就形同於在編程語言層面上做修改,屬於元編程(meta programming) 元編程(Metaprogramming,又譯超編程,是指某類電腦程式的編寫,這 ...
  • 前言 Vue3 作為一款現代的 JavaScript 框架,引入了許多新的特性和改進,其中包括 shallowRef 和 shallowReactive。這兩個功能在Vue 3中提供了更加靈活和高效的狀態管理選項,尤其適用於大型和複雜的應用程式。 Vue 3 的響應式系統 Vue3 引入了新的響應式 ...
  • 作為2024年最受歡迎的Vue.js組件庫之一,ViewDesign憑藉其現代化設計理念、強大功能和可定製性脫穎而出。這款開源UI組件庫提供了豐富的基礎組件、數據展示組件和交互反饋組件,涵蓋了大部分Web開發場景。同時,ViewDesign還具備良好的可訪問性、完善的文檔、活躍的社區支持,並對SEO... ...
一周排行
    -Advertisement-
    Play Games
  • 基於.NET Framework 4.8 開發的深度學習模型部署測試平臺,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等應用場景,同時支持圖像與視頻檢測。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runti... ...
  • 十年沉澱,重啟開發之路 十年前,我沉浸在開發的海洋中,每日與代碼為伍,與演算法共舞。那時的我,滿懷激情,對技術的追求近乎狂熱。然而,隨著歲月的流逝,生活的忙碌逐漸占據了我的大部分時間,讓我無暇顧及技術的沉澱與積累。 十年間,我經歷了職業生涯的起伏和變遷。從初出茅廬的菜鳥到逐漸嶄露頭角的開發者,我見證了 ...
  • C# 是一種簡單、現代、面向對象和類型安全的編程語言。.NET 是由 Microsoft 創建的開發平臺,平臺包含了語言規範、工具、運行,支持開發各種應用,如Web、移動、桌面等。.NET框架有多個實現,如.NET Framework、.NET Core(及後續的.NET 5+版本),以及社區版本M... ...
  • 前言 本文介紹瞭如何使用三菱提供的MX Component插件實現對三菱PLC軟元件數據的讀寫,記錄了使用電腦模擬,模擬PLC,直至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1. PLC開發編程環境GX Works2,GX Works2下載鏈接 https:// ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • 1、jQuery介紹 jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝 ...
  • 前言 之前的文章把js引擎(aardio封裝庫) 微軟開源的js引擎(ChakraCore))寫好了,這篇文章整點js代碼來測一下bug。測試網站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻譯js逆向(MD5加密,AES加密)附完整源碼 ...
  • 引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
  • 掌握使用Python進行文本英文統計的基本方法,並瞭解如何進一步優化和擴展這些方法,以應對更複雜的文本分析任務。 ...
  • 背景 Redis多數據源常見的場景: 分區數據處理:當數據量增長時,單個Redis實例可能無法處理所有的數據。通過使用多個Redis數據源,可以將數據分區存儲在不同的實例中,使得數據處理更加高效。 多租戶應用程式:對於多租戶應用程式,每個租戶可以擁有自己的Redis數據源,以確保數據隔離和安全性。 ...