代碼層面探索前端性能

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/09/26/17729524.html
-Advertisement-
Play Games

最近在做性能優化,具體優化手段,網上鋪天蓋地,這裡就不重覆了。性能優化可分為以下幾個維度:代碼層面、構建層面、網路層面。本文主要是從代碼層面探索前端性能,主要分為以下 4 個小節。使用 CSS 替代 JS、深度剖析 JS、前端演算法、電腦底層 ...


前言

最近在做性能優化,具體優化手段,網上鋪天蓋地,這裡就不重覆了。

性能優化可分為以下幾個維度:代碼層面、構建層面、網路層面。
本文主要是從代碼層面探索前端性能,主要分為以下 4 個小節。

  • 使用 CSS 替代 JS

  • 深度剖析 JS

  • 前端演算法

  • 電腦底層

使用 CSS 替代 JS

這裡主要從動畫和 CSS 組件兩個方面介紹。

CSS 動畫

CSS2 出來之前,哪怕要實現一個很簡單的動畫,都要通過 JS 實現。比如下麵紅色方塊的水平移動:

水平移動

對應 JS 代碼:

let redBox = document.getElementById('redBox')
let l = 10

setInterval(() => {
    l+=3
    redBox.style.left = `${l}px`
}, 50)



1998 年的 CSS2 規範,定義了一些動畫屬性,但由於受當時瀏覽器技術限制,這些特性並沒有得到廣泛的支持和應用。

直到 CSS3 的推出,CSS 動畫得到了更全面地支持。同時,CSS3 還引入了更多的動畫效果,使得 CSS 動畫在今天的 Web 開發中得到了廣泛的應用。

那麼 CSS3 都能實現什麼動畫,舉幾個例子:

  • 過渡(Transition)- 過渡是 CSS3 中常用的動畫效果之一,通過對一個元素的某些屬性進行變換,使元素在一段時間內從一個狀態平滑地過渡到另一個狀態。

  • 動畫(Animation)- 動畫是 CSS3 中另一個常用的動畫效果,其用於為一個元素添加一些複雜的動畫效果,可以通過關鍵幀(@keyframes)來定義一串動畫序列。

  • 變換(Transform)- 變換是 CSS3 中用於實現 2D/3D 圖形變換效果的一種技術,包括旋轉、縮放、移動、斜切等效果。

把上面的例子改寫成 CSS 代碼如下:

#redBox {
    animation: mymove 5s infinite;
}

@keyframes mymove
{
    from {left: 0;}
    to {left: 200px;}
}



同樣的效果,用樣式就能實現,何樂而不為呢。

需要指出的是,CSS 的動畫仍在不斷發展和改進,隨著新的瀏覽器特性和 CSS 版本的出現,CSS 動畫的特性也在不斷地增加和優化,以滿足日益複雜的動畫需求和更好的用戶體驗。

CSS 組件

在一些知名的組件庫中,有些組件的大部分 props 是通過修改 CSS 樣式實現的,比如 Vant 的Space組件。

Props 功能 CSS 樣式
direction 間距方向 flex-direction: column;
align 對齊方式 align-items: xxx;
fill 是否讓 Space 變為一個塊級元素,填充整個父元素 display: flex;
wrap 是否自動換行 flex-wrap: wrap;

再比如 Ant Design 的Space組件。

Props 功能 CSS 樣式
align 對齊方式 align-items: xxx;
direction 間距方向 flex-direction: column;
size 間距大小 gap: xxx;
wrap 是否自動換行 flex-wrap: wrap;

這類組件完全可以封裝成 SCSS 的 mixin 實現(LESS 也一樣),既能減少項目的構建體積(兩個庫的 Space 組件 gzip 後的大小分別為 5.4k 和 22.9k),又能提高性能。

查看組件庫某個組件的體積,可訪問連接

比如下麵的 space mixin:

/* 
* 間距
* size: 間距大小,預設是 8px
* align: 對齊方式,預設是 center,可選 start、end、baseline、center
* direction: 間距方向,預設是 horizontal,可選 horizontal、vertical
* wrap: 是否自動換行,僅在 horizontal 時有效,預設是 false
*/
@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {
    display: inline-flex;
    gap: $size;

    @if ($direction == 'vertical') {
        flex-direction: column;
    }

    @if ($align == 'center') {
        align-items: center;
    }

    @if ($align == 'start') {
        align-items: flex-start;
    }

    @if ($align == 'end') {
        align-items: flex-end;
    }

    @if ($align == 'baseline') {
        align-items: baseline;
    }

    @if ($wrap == true) {
        @if $direction == 'horizontal' {
            flex-wrap: wrap;
        }
    }
}



類似的組件還有 Grid、Layout 等。

再說下圖標,下麵是 Ant Design 圖標組件的第一屏截圖,有很多僅用 HTML + CSS 就可以輕鬆實現。

Ant Design 方向圖標

實現思路:

  • 優先考慮只使用樣式實現

  • 僅靠樣式滿足不了,就先增加一個標簽,通過這個標簽和它的兩個偽元素 ::before 和 ::after 實現

  • 一個標簽實在不夠,再考慮增加額外的標簽

比如實現一個支持四個方向的實心三角形,僅用幾行樣式就可以實現(上面截圖是 4 個圖標):

/* 三角形 */
@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {
    width: 0;
    height: 0;
    border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;

    $doubleBorderWidth: 2 * $borderWidth;
    
    $borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};

    @if($direction == 'up') {
        border-bottom: $borderStyle;
    }

    @if($direction == 'down') {
        border-top: $borderStyle;
    }

    @if($direction == 'left') {
        border-right: $borderStyle;
    }

    @if($direction == 'right') {
        border-left: $borderStyle;
    }
}



總之,能用 CSS 實現的就不用 JS,不僅性能好,而且還跨技術棧,甚至跨端。

深度剖析 JS

介紹完了 CSS,再來看 JS,主要從基本語句和框架源碼兩個方面深入。

if-else 語句的優化

先瞭解下 CPU 是如何執行條件語句的。參考如下代碼:

const a = 2
const b = 10
let c
if (a > 3) {
    c = a + b
} else {
    c = 2 * a
}



CPU 執行流程如下:

條件語句

我們看到,在執行到指令 0102 時候,由於不滿足 a > 3 這個條件,就直接跳轉到 0104 這個指令去執行了;而且,電腦很聰明,如果它在編譯期間發現 a 永遠不可能大於 3,它就會直接刪除 0103 這條指令,然後,0104 這條指令就變成了下一條指令,直接順序執行,也就是編譯器的優化。

那麼回到正題,假如有以下代碼:

function check(age, sex) {
    let msg = ''
    if (age > 18) {
        if (sex === 1) {
            msg = '符合條件'
        } else {
            msg = ' 不符合條件'
        }
    } else {
        msg = '不符合條件'
    }
}



邏輯很簡單,就是篩選出 age > 18 並且 sex == 1 的人,代碼一點兒問題都沒有,但是太啰嗦,站在 CPU 的角度來看,需要執行兩次跳轉操作,當 age > 18 時,就進入內層的 if-else 繼續判斷,也就意味著再次跳轉。

其實我們可以直接優化下這個邏輯(通常我們也是這樣做的,但是可能知其然而不知其所以然):

function check(age, sex){
    if (age > 18 && sex ==1) return '符合條件'
    return '不符合條件'
}



所以,邏輯能提前結束就提前結束,減少 CPU 的跳轉。

Switch 語句的優化

其實 switch 語句和 if-else 語句的區別不大,只不過寫法不同而已,但是,switch 語句有個特殊的優化,那就是數組。

參考以下代碼:

function getPrice(level) {
    if (level > 10) return 100
    if (level > 9) return 80
    if (level > 6) return 50
    if (level > 1) return 20
    return 10
}



我們改成 switch 語句:

function getPrice(level) {
    switch(level)
        case 10: return 100
        case 9: return 80
        case 8: 
        case 7: 
        case 6: return 50
        case 5:
        case 4: 
        case 3:
        case 2: 
        case 1: return 20
        default: return 10
}



看著沒啥區別,其實編譯器會把它優化成一個數組,其中數組的下標為 0 到 10,不同下標對應的價格就是 return 的數值,也就是:

Swich 數組

而我們又知道,數組是支持隨機訪問的,速度極快,所以,編譯器對 switch 的這個優化就會大大提升程式的運行效率,這可比一條一條執行命令快多了。

那麼,我還寫個毛的 if-else 語句啊,我直接全部寫 switch 不就行了?

不行!因為編譯器對 switch 的優化是有條件的,它要求你的 code 必須是緊湊的,也就是連續的。

這是為什麼呢?因為我要用數組來優化你啊,你如果不是緊湊的,比如你的 code 是 1、50、51、101、110,我就要創建一個長度 110 的數組來存放你,只有這幾個位置有用,豈不是浪費空間!

所以,我們在使用 switch 的時候,儘量保證_code 是緊湊的數字類型_的。

迴圈語句的優化

其實迴圈語句跟條件語句類似,只不過寫法不同而已,迴圈語句的優化點是以減少指令為主。

我們先來看一個中二的寫法:

function findUserByName(users) {
   let user = null
   for (let i = 0; i < users.length; i++) {
       if (users[i].name === '張三') {
           user = users[i]
       }
   }
   return user
}



如果數組長度是 10086,第一個人就叫張三,那後面 10085 次遍歷不就白做了,真拿 CPU 不當人啊。

你直接這樣寫不就行了:

function findUserByName(users) {
    for (let i = 0; i < users.length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}



這樣寫效率高,可讀性強,也符合我們上述的_邏輯能提前結束就提前結束_這個觀點。CPU 直接感謝你全家。

其實,這裡還有一點可以優化的地方,就是我們的數組長度可以提取出來,不必每次都訪問,也就是這樣:

function findUserByName(users) {
    let length = users.length
    for (let i = 0; i < length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}



這看起來好像有點吹毛求疵了,確實是,但是如果考慮到性能的話,還是有點用的。比如有的集合的 size() 函數,不是簡單的屬性訪問,而是每次都需要計算一次,這種場景就是一次很大的優化了,因為省了很多次函數調用的過程,也就是省了很多個 call 和 return 指令,這無異是提高了代碼的效率的。尤其是在迴圈語句這種容易量變引起質變的情況下,差距就是從這個細節拉開的。

函數調用過程參考:

函數調用

對應代碼如下:

let a = 10
let b = 11

function sum (a, b) {
    return a + b
}



說完了幾個基礎語句,再來看下我們經常使用的框架內部,很多地方的性能都值得探索。

diff 演算法

Vue 和 React 中都使用了虛擬 DOM,當執行更新時,要對比新舊虛擬 DOM。如果沒有任何優化,直接嚴格 diff 兩顆樹,時間複雜度是 O(n^3),根本不可用。所以 Vue 和 React 必須使用 diff 演算法優化虛擬 DOM:

Vue2 - 雙端比較:

Vue2 - 雙端比較

類似上面的圖:

  • 定義 4 個變數,分別為:oldStartIdx、oldEndIdx、newStartIdx 和 newEndIdx

  • 判斷 oldStartIdx 和 newStartIdx 是否相等

  • 判斷 oldEndIdx 和 newEndIdx 是否相等

  • 判斷 oldStartIdx 和 newEndIdx 是否相等

  • 判斷 oldEndIdx 和 newStartIdx 是否相等

  • 同時 oldStartIdx 和 newStartIdx 向右移動;oldEndIdx 和 newEndIdx 向左移動

Vue3 - 最長遞增子序列:

Vue3 - 最長遞增子序列

整個過程是基於 Vue2 的雙端比較再次進行優化。比如上面這個截圖:

  • 先進行雙端比較,發現前面兩個節點(A 和 B)和最後一個節點(G)是一樣的,不需要移動

  • 找到最長遞增子序列 C、D、E(新舊 children 都包含的,最長的順序沒有發生變化的一組節點)

  • 把子序列當成一個整體,內部不用進行任何操作,只需要把 F 移動到它的前面,H 插入到它的後面即可

React - 僅右移:

React - 僅右移

上面截圖的比較過程如下:

  • 遍歷 Old 存下對應下標 Map

  • 遍歷 New,b 的下標從 1 變成了 0,不動(是左移不是右移)

  • c 的下標從 2 變成了 1,不動(也是左移不是右移)

  • a 的下標從 0 變成了 2,向右移動,b、c 下標都減 1

  • d 和 e 位置沒變,不需要移動

總之,不管用什麼演算法,它們的原則都是:

  • 只比較同一層級,不跨級比較

  • Tag 不同則刪掉重建(不再去比較內部的細節)

  • 子節點通過 key 區分(key 的重要性)

最後也都成功把時間複雜度降低到了 O(n),才可以被我們實際項目使用。

setState 真的是非同步嗎

很多人都認為 setState 是非同步的,但是請看下麵的例子:

clickHandler = () => {
    console.log('--- start ---')

    Promise.resolve().then(() => console.log('promise then'))

    this.setState({val: 1}, () => {console.log('state...', this.state.val)})

    console.log('--- end ---')
}

render() {
    return <div onClick={this.clickHandler}>setState</div>
}



實際列印結果:

setState 列印結果

如果是非同步的話,state 的列印應該在微任務 Promise 後執行。

為瞭解釋清這個原因,必須先瞭解 JSX 里的事件機制。

JSX 里的事件,比如 onClick={() => {}},其實叫合成事件,區別於我們常說的自定義事件:

// 自定義事件
document.getElementById('app').addEventListener('click', () => {})



合成事件都是綁定在 root 根節點上,有前置和後置操作,拿上面的例子舉例:

function fn() { // fn 是合成事件函數,內部事件同步執行
    // 前置
    clickHandler()
    
    // 後置,執行 setState 的 callback
}



可以想象有函數 fn,裡面的事件都是同步執行的,包括 setState。fn 執行完,才開始執行非同步事件,即 Promise.then,符合列印的結果。

那麼 React 為什麼要這麼做呢?
因為要考慮性能,如果要多次修改 state,React 會先合併這些修改,合併完只進行一次 DOM 渲染,避免每次修改完都渲染 DOM。

所以 setState_本質是同步_,日常說的“非同步”是不嚴謹的。

前端演算法

講完了我們的日常開發,再來說說演算法在前端中的應用。

友情提示:演算法一般都是針對大數據量而言,區別於日常開發。

能用值類型就不用引用類型

先來看一道題。

求 1-10000 之間的所有對稱數,例如:0, 1, 2, 11, 22, 101, 232, 1221...

思路 1 - 使用數組反轉、比較:數字轉換為字元串,再轉換為數組;數組 reverse,再 join 為字元串;前後字元串進行對比。

function findPalindromeNumbers1(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        // 轉換為字元串,轉換為數組,再反轉,比較
        const s = i.toString()
        if (s === s.split('').reverse().join('')) {
            res.push(i)
        }
    }

    return res
}



思路 2 - 字元串頭尾比較:數字轉換為字元串;字元串頭尾字元比較。

function findPalindromeNumbers2(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        const s = i.toString()
        const length = s.length

        // 字元串頭尾比較
        let flag = true
        let startIndex = 0 // 字元串開始
        let endIndex = length - 1 // 字元串結束
        while (startIndex < endIndex) {
            if (s[startIndex] !== s[endIndex]) {
                flag = false
                break
            } else {
                // 繼續比較
                startIndex++
                endIndex--
            }
        }

        if (flag) res.push(res)
    }

    return res
}



思路 3 - 生成翻轉數:使用 % 和 Math.floor 生成翻轉數;前後數字進行對比(全程操作數字,沒有字元串類型)。

function findPalindromeNumbers3(max) {
    const res = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        let n = i
        let rev = 0 // 存儲翻轉數

        // 生成翻轉數
        while (n > 0) {
            rev = rev * 10 + n % 10
            n = Math.floor(n / 10)
        }

        if (i === rev) res.push(i)
    }

    return res
}



性能分析:越來越快

  • 思路 1- 看似是 O(n),但數組轉換、操作都需要時間,所以慢

  • 思路 2 VS 思路3 - 操作數字更快(電腦原型就是計算器)

總之,儘量不要轉換數據結構,尤其數組這種有序結構,儘量不要用內置 API,如 reverse,不好識別複雜度,數字操作最快,其次是字元串。

儘量用“低級”代碼

還是直接上一道題。

輸入一個字元串,切換其中字母的大小寫
如,輸入字元串 12aBc34,輸出字元串 12AbC34

思路 1 - 使用正則表達式。

function switchLetterCase(s) {
    let res = ''

    const length = s.length
    if (length === 0) return res

    const reg1 = /[a-z]
    const reg2 = /[A-Z]

    for (let i = 0; i < length; i++) {
        const c = s[i]
        if (reg1.test(c)) {
            res += c.toUpperCase()
        } else if (reg2.test(c)) {
            res += c.toLowerCase()
        } else {
            res += c
        }
    }

    return res
}



思路 2 - 通過 ASCII 碼判斷。

function switchLetterCase2(s) {
    let res = ''

    const length = s.length
    if (length === 0) return res

    for (let i = 0; i < length; i++) {
        const c = s[i]
        const code = c.charCodeAt(0)

        if (code >= 65 && code <= 90) {
            res += c.toLowerCase()
        } else if (code >= 97 && code <= 122) {
            res += c.toUpperCase()
        } else {
            res += c
        }
    }

    return res
}



性能分析:前者使用了正則,慢於後者

所以,儘量用“低級”代碼,慎用語法糖、高級 API 或者正則表達式。

電腦底層

最後說一些前端需要瞭解的電腦底層。

從“記憶體”讀數據

我們通常說的:從記憶體中讀數據,就是把數據讀入寄存器中,但是我們的數據不是直接從記憶體讀入寄存器的,而是先讀入一個高速緩存中,然後才讀入寄存器的。

寄存器是在 CPU 內的,也是 CPU 的一部分,所以 CPU 從寄存器讀寫數據非常快。

這是為啥呢?因為從記憶體中讀數據太慢了。

你可以這麼理解:CPU 先把數據讀入高速緩存中,以備使用,真正使用的時候,就從高速緩存中讀入寄存器;當寄存器使用完畢後,就把數據寫回到高速緩存中,然後高速緩存再在合適的時機將數據寫入到存儲器。

CPU 運算速度非常快,而從記憶體讀數據非常慢,如果每次都從記憶體中讀寫數據,那麼勢必會拖累 CPU 的運算速度,可能執行 100s,有 99s 都在讀取數據。為瞭解決這個問題,我們就在 CPU 和存儲器之間放了個高速緩存,而 CPU 和高速緩存之間的讀寫速度是很快的,CPU 只管和高速緩存互相讀寫數據,而不管高速緩存和存儲器之間是怎麼同步數據的。這樣就解決了記憶體讀寫慢的問題。

二進位的位運算

靈活運用二進位的位運算不僅能提高速度,熟練使用二進位還能節省記憶體。

假如給定一個數 n,怎麼判斷 n 是不是 2 的 n 次方呢?

很簡單啊,直接求餘就行了。

function isPowerOfTwo(n) {
    if (n <= 0) return false
    let temp = n
    while (temp > 1) {
        if (temp % 2 != 0) return false
        temp /= 2
    }
    return true
}



嗯,代碼沒毛病,不過不夠好,看下麵代碼:

function isPowerOfTwo(n) {
    return (n > 0) && ((n & (n - 1)) == 0)
}



大家可以用 console.time 和 console.timeEnd 對比下運行速度便知。

我們可能還會看到一些源碼裡面有很多 flag 變數,對這些 flag 進行按位與或按位或運算來檢測標記,從而判斷是否開啟了某個功能。他為什麼不直接用布爾值呢?很簡單,這樣效率高還節省記憶體。

比如 Vue3 源碼中的這段代碼,不僅用到了按位與和按位或,還用到了左移:

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}


if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
  ...
}


if (hasDynamicKeys) {
      patchFlag |= PatchFlags.FULL_PROPS
    } else {
    if (hasClassBinding) {
      patchFlag |= PatchFlags.CLASS
    }
    if (hasStyleBinding) {
      patchFlag |= PatchFlags.STYLE
    }
    if (dynamicPropNames.length) {
      patchFlag |= PatchFlags.PROPS
    }
    if (hasHydrationEventBinding) {
      patchFlag |= PatchFlags.HYDRATE_EVENTS
    }
}



結語

文章從代碼層面講解了前端的性能,有深度維度的:

  • JS 基礎知識深度剖析

  • 框架源碼

也有廣度維度的:

  • CSS 動畫、組件

  • 演算法

  • 電腦底層

希望能讓大家拓寬前端性能的視野,如果對文章感興趣,歡迎留言討論~~~

作者:京東零售 楊進軍

來源:京東雲開發者社區 轉載請註明來源


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

-Advertisement-
Play Games
更多相關文章
  • 奇富科技(原360數科)是人工智慧驅動的信貸科技服務平臺,致力於憑藉智能服務、AI研究及應用、安全科技,賦能金融機構提質增效,助推普惠金融高質量發展,讓更多人享受到安全便捷的金融科技服務。作為國內領先的信貸科技服務品牌,累計註冊用戶數2億多。 奇富科技之前使用的是自研的任務調度框架,基於Python ...
  • 本篇作為 OPPO主題組件調試與預覽 文檔的補充,因為它真的很簡單而且太老,一些命令已發生變化😪 此圖片來自官網 一、調試前準備 1. PC 端下載 adb命令工具 下載 下載地址 https://adbdownload.com/,或從其他地方下載也可 解壓,放在你想放的文件夾下 配置環境變數 右 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 前兩天我的新同事告訴我一個困擾著他的問題,就是低代碼平臺中存在很多模塊,這些模塊的渲染是由模塊自身處理的,簡言之就是組件請求了自己的數據,一個兩個模塊還好,要是一次請求了幾十個模塊,就會出現請求阻塞的問題,而且模塊的請求都特別大。 ...
  • 頁面效果 具體實現 新增 1、監聽滑鼠抬起事件,通過window.getSelection()方法獲取滑鼠用戶選擇的文本範圍或游標的當前位置。 2、通過 選中的文字長度是否大於0或window.getSelection().isCollapsed (返回一個布爾值用於描述選區的起始點和終止點是否位於 ...
  • 標簽組件的效果如下 組件作用 這是一個div,包含了兩個文本框,後面是添加和刪除按鈕 添加按鈕複製出新的div,除了文本框沒有內容,其它都上面一樣 刪除按鈕將當前行div刪除 組件實現 <template> <div> <template v-for="(item,index) in tags"> ...
  • 用到的技術 父組件向子組件的傳值 類型檢查和預設值:您可以為props指定類型檢查和預設值。這可以確保傳遞給子組件的數據符合期望的類型,以及在沒有傳遞數據時具有合理的預設值。例如: props: { message: { type: String, default: 'Default Message ...
  • 首先,先說說我要實現的內容:如下圖,點“新增”會添加一個灰框內容,form表單是一個數組,一個灰框為一個對象,各對象保存時各自校驗自己表單里的內容,互不幹擾! 上頁面代碼(看部分代碼就懂了): 1 <div v-for="(item,index) in formList" :key="index"> ...
  • 我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 本文作者:空山 什麼是沙箱 沙箱即 SandBox,它是一種安全機制,用於嚴格控制訪問資源。通過在程式中創建一個獨立的運行環境,把一些來源不可信、具有破壞力或者又是無法 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...