我的 React 最佳實踐

来源:https://www.cnblogs.com/dtux/archive/2022/11/02/16850113.html
-Advertisement-
Play Games

There are a thousand Hamlets in a thousand people's eyes. 威廉·莎士比亞 免責聲明:以下充滿個人觀點,辯證學習 React 目前開發以函數組件為主,輔以 hooks 實現大部分的頁面邏輯。目前數棧的 react 版本是 16.13.1,該版本 ...


There are a thousand Hamlets in a thousand people's eyes. ----- 威廉·莎士比亞

免責聲明:以下充滿個人觀點,辯證學習

React 目前開發以函數組件為主,輔以 hooks 實現大部分的頁面邏輯。目前數棧的 react 版本是 16.13.1,該版本是支持 hooks 的,故以下實踐是 hooks 相關的最佳實踐。

前置理解

首先,應當明確 React 所推崇的函數式編程以及 f(data) = UI 是什麼?

函數式編程

函數式編程是什麼?這裡的函數並非 JavaScript 中的函數,或任何語言中的函數。此處的函數是數學概念中的函數。讓我們來回憶一下數學中的函數是什麼。

file

在數學概念中,函數即一種特殊的映射,如何理解映射

以一元二次方程為例, f(x) 是一種映射關係,給定一個的 x ,則存在 y 與之對應。

我們將一元二次方程理解為一個黑盒,該黑盒存在一個輸入,一個輸出,在輸入端我們給入一個值 x = 2 則輸出端必然會給出一個 y = 4

f(data)=UI

在瞭解了數學中函數的概念後,將其概念套用到 React 中,我們就可以明白 f(data)=UI 到底指什麼意思?

結論:個人理解,將當前組件內部的所有邏輯視為一個黑盒,該黑盒有且僅有一個輸入,有且僅有一個輸出,輸入端為 props,輸出端則是當前組件的 UI ,不同的輸入會決定不同的輸出,就像把 x = 1x = 2 給到所得到的結果是不一樣的一樣。而將這樣的組件組合起來就是每一個頁面,即 React 應用。

業務開發

目前絕大部分的後臺管理系統,不僅僅是數棧,包括所有 ERP 系統,圖書管理系統等等。其主體頁面大致可以包括一下三類:

  • 篩選條件(Filter)+ 表格(Table)
  • 表單(Form)相關的新增,編輯功能
  • 概覽頁面(Overview),包括一些圖表和表格

第一類

file

以上是這次在資產中負責開發的文件治理規則頁面,屬於是第一類的典型頁面,那這一類的頁面應該如何開發才是最佳實踐?

首先,我們將這一類頁面抽象成如下結構
file

如此一來,我們不難實現如下的 dom 結構

<div className="container">
  <div className="header">
    <div className="filter">篩選區</div>
    <div className="buttonGroup">按鈕區</div>
  </div>
  <div className="content">表格區</div>
</div>

接下來,我們需要補充各個區域的內容。

篩選區通常會有一些篩選項,這裡我們有兩個選擇,如果比較複雜的篩選項,如存在 5 個以上或存在聯動的交互,則選擇通過 Form 來實現,而上圖中這種比較簡單的則可以直接實現。
這裡我們不難觀察到我們需要 3 個 value 值來實現這 3 個輸入框或下拉框的受控模式,這裡我們通過聲明一個 filter 的變數,將這 3 個值做一個整合。

function (){
  const [filter, setFilter] = useState({
    a: undefined,
    b: undefined,
    c: undefined
  });
  
  return <><>
}

提問:為什麼要 3 個值都放入到一個變數里?

答:1. 減少冗長的定義。 2. 為後續鋪墊。

聲明完 3 個值後,我們把組件填入

function (){
  const [filter, setFilter] = useState({
    a: undefined,
    b: undefined,
    c: undefined
  });

  return (
    <div className="container">
      <div className="header">
        <div className="filter">
          <Input value={filter.a} onChange={(e) => setFilter(f => ({...f, a: e.target.value})} />
          <Input value={filter.b} onChange={(e) => setFilter(f => ({...f, b: e.target.value})} />
          <Input value={filter.c} onChange={(e) => setFilter(f => ({...f, c: e.target.value})} />
        </div>
        <div className="buttonGroup">按鈕區</div>
      </div>
      <div className="content">表格區</div>
    </div>
  )
}

接著,我們實現表格區,有經驗的同學可以知道,一個 Table 需要 dataSource 數據,columnspaginationtotal,有時候還需要 selectedRowsorterfilter

我們先考慮 dataSource 和 columns。首先,我們先考慮數據獲取,實現 getDataSourceList

interface ITableProps {}

function (){
  const [filter, setFilter] = useState({
    a: undefined,
    b: undefined,
    c: undefined
  });
  const [loading, setLoading] = useState(false);
  const [dataSource, setDataSource] = useState<ITableProps>([]);

  const getDataSourceList = () => {
    setLoading(true);
    Promise.then(() => {
      /// xxxx
    }).finally(() => {
      setLoading(false);
    })
  };

  return (
    <div className="container">
      <div className="header">
        <div className="filter">
          <Input value={filter.a} onChange={(e) => setFilter(f => ({...f, a: e.target.value})} />
          <Input value={filter.b} onChange={(e) => setFilter(f => ({...f, b: e.target.value})} />
          <Input value={filter.c} onChange={(e) => setFilter(f => ({...f, c: e.target.value})} />
        </div>
        <div className="buttonGroup">按鈕區</div>
      </div>
      <div className="content">表格區</div>
    </div>
  )
}

可以看到,我們新增了 loading 變數,用來優化交互的過程,新增了 ITableProps 類型,用來聲明表格數據的類型,此時應當和服務端的同學溝通,從介面文檔中獲取到相關的數據結構,並補全這一塊的類型

假設我們此時已經完成了類型的補全,那接下來我們需要完善 columns

const columns: ColumnType<ITableProps>[] = [];

這裡存在部分分歧,有部分人認為需要加上 useMemo
為什麼不加 useMemo?

  1. 因為 useMemo 主要是為了緩存計算量比較大的值,而此處並沒有計算量,只是一個變數的聲明而已
  2. 如果重覆聲明 useMemo 的話,是否會導致 Table 的重覆渲染。我認為過早的性能優化不如不優化,如果真的存在這樣子的問題再進行優化也不急。
  3. 如果這裡加上 useMemo 在某些情況下會有比較多的 depths 需要加到依賴項里,個人感覺這不優雅

在完善 columns 後,我們繼續剛纔提到的 Paginationtotal 欄位。
這裡我們需要聲明兩個變數

const [pagination, setPagination] = useState({current: 1, pageSize: 20});
const [total, setTotal] = useState(0);

思考:這裡的 total 和 pagination 是否可以合併成一個變數?

答:可以,但是我不願意,因為我們後面會有存在如下代碼,會導致無限迴圈

useEffect(() => { 
  // getDataSourceList 中存在改變 total 的值,會導致無限迴圈
  // 如果要解決這個問題,則需要在 depths 中分別寫 current 和 pageSize
  // 我不願意
  getDataSourceList();
},[pagination]);

然後接下來我們要實現請求功能,不難總結出我們需要在以下幾種情況下做請求:

  • 頁面初始化
  • Pagination 改變進行請求
  • 篩選條件改變進行請求
  • Table 的 filter 或 sorter 發生改變進行請求
  • 交互修改數據後進行請求(如刪除,新增等)

除了最後這個暫時不做考慮,第三第四項的請求我們可以再細分為如圖所示
file

那麼,就可以和前兩項進行合併,總結如下:

  • 頁面初始化
  • Pagination 改變

將上述思路轉化為代碼可得

function (){
  const [filter, setFilter] = useState<Record<string, any>>({
    a: undefined,
    b: undefined,
    c: undefined,
    d: undefined,
    sorter: undefined
  });
  const [pagination, setPagination] = useState({ current: 1, pageSize: 20 });

  const getDataSourceList = () => {};

  useEffect(() => {
    setPagination(p => ({ ...p, current: 1 }));
  }, [filter]);

  useEffect(() => {
    getDataSourceList();
  }, [pagination]);
}

到這裡,我們已經實現了絕大部分主要功能,接下來我們簡單實現一個 selectedRows即可。

function (){
  const [selectedRows, setSelectedRows] = useState([]);

  return (
    <Table
      rowSelection={{
        selectedRowKeys,
        onChange: (selected) => setSelectedRows(selected)
      }}
    />
  )
}

問:為什麼這裡的 rowSelection 不提出來,放到 useMemo 里去?

至此,一個滿足業務的一類業務相關的框架代碼已經編寫完成。
到這裡之後,一類業務頁面還有剩下什麼東西需要開發?

第二類

二類頁面的特點通常是新增和編輯復用同一個頁面,需要 Form 表單。

除此之外,通常二類頁面有通過 Drawer 或者 Modal 或者跳轉路由的方式,但這不影響代碼的書寫。
不論是 Drawer 還是 Modal 還是路由的方式,我們都需要將表單內容抽離出一個新的組件。

首先,我們看一個比較普遍且較為簡單的新增或編輯頁面
file

可以觀察到,這個表單通過 Steps 組件分割成了 3 個步驟

這裡我們需要明確一個思想,即上面強調的 ,那麼這裡不論是新增還是編輯,其差異只存在於 data 中是否存在 id 值。
這裡有兩種做法,第一種做法是 3 個步驟只用一個 Form,第二種做法是 3 個步驟 3 個 Form。我個人比較喜歡第一種做法。理由如下:

  • 因為 data 是一份的,如果用第二種做法,需要將一份 data 拆分成三份
  • 倘若出現在第一步中填寫某個值會影響第二步中的下拉項,如果是第二種做法,還需要將 Form 的值傳給第二步的組件。第一步的做法可以直接通過 form.getFieldValue('xxx')
  • 無它,就是圖個簡潔

比較複雜的表單通常都有聯動情況,比如數據同步任務的表單,或者這裡的數據源和表選擇的交互。
這一類交互在 antd@3 中通常實現起來會比較的繁瑣,在 antd@4中善用 dependenciesonValuesChanged可以很好地解決這一類問題。

{[TRINO, KINGBASEES8, SAPHANA1X].includes(dataSourceType) && (
  <FormItem
    name="schemaName"
    label="選擇Schema"
    initialValue={schemaName}
    rules={[
      {
        required: true,
        message: '請選擇Schema',
      },
    ]}
    >
    <Select
      showSearch
      style={{
        width: '100%',
      }}
      onSelect={this.onSchemaChange}
      onPopupScroll={this.handleSchemaScroll}
      onSearch={this.handleSchemaSearch}
      >
      {this.renderSchemaListOption(currentSchema)}
    </Select>
  </FormItem>
)}

如上代碼所示, schema 的欄位只有在所選擇的數據源是 xxx 這幾種情況下才會展示,如果我們按照上述代碼的寫法的話,需要在 state 中新增 dataSourceType欄位
那如果用 dependencies的話,可以改成如下寫法:

<FormItem noStyle dependencies={['sourceId']}>
  {({ getFieldValue }) => (
  isXXXXX(options.find(o => o.sourceId === getFieldValue('sourceId'))?.type) && (
    <FormItem
      name="schemaName"
      label="選擇Schema"
      initialValue={schemaName}
      rules={[
        {
          required: true,
          message: '請選擇Schema',
        },
      ]}
      >
      <Select
        showSearch
        style={{
          width: '100%',
        }}
        onSelect={this.onSchemaChange}
        onPopupScroll={this.handleSchemaScroll}
        onSearch={this.handleSchemaSearch}
        >
        {this.renderSchemaListOption(currentSchema)}
      </Select>
    </FormItem>
  )
)}
</FormItem>

更進一步,可以把 find 抽象一個 getTypeBySourceId函數出來,即優化了可維護性,又減少了變數聲明。

除此之外,還有下拉菜單的聯動,如 A 的選擇會引起 B 的下拉菜單獲取,B 的下拉菜單可能又會引起 C 的下拉菜單改變,如此鏈路下去,會導致聲明的 handleChange 函數又多又長

function(){
  const handleAChanged = () => {};
  
  const handleBChanged = () => {};
  
  const handleCChanged = () => {};
  
  return (
    <Form>
      <FormItem name ="a">
        <Select onChange={handleAChanged} />
      </FormItem>
      <FormItem name ="b">
        <Select onChange={handleBChanged} />
      </FormItem>
      <FormItem name ="c">
        <Select onChange={handleCChanged} />
      </FormItem>
    </Form>
  )	
}

那我們可以藉助 antd@4onValuesChanged函數,來把所有相關組件的 onChange 做合併,即如下:

function(){
   const handleFormFieldChanged = (changed: Partial<IFormFieldProps>) => {
    if('a' in changed){
      // do something about a
      getOptionsForB();
      form.resetFields(['b', 'c']);
    }

    if('b' in changed){
      // do something about b
      getOptionsForC();
      form.resetFields(['c']);
    }

    if('c' in changed){
      // do something about c
      getOptionsForD();
    }
  }
  
  return (
    <Form onValuesChanged={handleFormFieldChanged}>
      <FormItem name ="a">
        <Select />
      </FormItem>
      <FormItem name ="b">
        <Select />
      </FormItem>
      <FormItem name ="c">
        <Select />
      </FormItem>
    </Form>
  )	
}

同時,在這個函數里我們也可以順便把 reset 的操作做了

到這裡,我們大致完成了 Form 表單的架構思路。接下來,我們需要處理新增和編輯的區分。
通常來說,新增是不需要賦初始值的,而編輯是需要賦初始值的。
這裡需要註意的點在於,我們所理解的初始值並不是 Form 組件中 initialValue 的含義。(至少我認為不是)
我認為 Form 組件的生命周期應當分為如下部分:

  • 初始化階段(該階段 Form 表單用 initialValue把表單 UI 渲染出來)
  • 賦值階段(該階段用戶通過 set 操作把初始值賦給表單的 state)
  • 交互階段
  • 提交階段

如上階段中說明所示,我認為初始值的操作應當通過 set 操作完成,那代碼實現起來應當如下所示:

function(){
  useEffect(() => {
    if(router.record.id){
      setLoading(true);
      api.getxxx({recordId: record.id}).then(res => {
        if(res.success){
          form.setFieldsValue({
            a: res.data.a,
            b: res.data.b
          })
        }
      }).finally(() => {
        setLoading(false);
      });
    }
  }, []);
}

把編輯的賦值操作全都放到 useEffect 中執行,統一了書寫的地方,有利於後期的維護。

總結後,可以得出整個 Form 表單頁面的概覽大致如下圖所示
file

相信各位同學到這裡之後,面對一個表單的原型圖,心裡已經有一個大致的偽代碼的實現了。

第三類

通常概覽頁面的要素是,統計數據、時間選擇、圖表。這一類頁面由於通常是作為用戶第一個打開的頁面,所以需要額外註意的是 loading 狀態的展示。

需要註意的是這裡的 loading 會存在以下幾種情況:

  • 整個頁面的 loading,這種情況通常是全局的一個時間選擇器,然後同時獲取統計數據和圖表,需要藉助 Promise.allPromise.allSettled 實現。
  • 分區域的 loading,如圖表區有圖表區的 loading,表格區有表格的 loading,統計數據區有統計數據區的 loading,這種時候需要把 loading 更加細分,為每一個區域增加 loading 變數,確保各個請求都有 loading 狀態展示

其他沒什麼好說的,主要是 CSS 的要求會更高。大致的偽代碼會如下:

function(){
  const [options, setOptions] = useState({...defaultOptions});
  const [loading, setLoading] = useState(false);
  const [timeRange, setTimeRange] = useState([moment(), moment()]);
  const [statistic, setStatistic] = useState({
    a: 0,
    b: 0,
  });

  const getStatistic = () => {
    return new Promise((resolve) => {
      setStatistic({a: 1000, b: 30});
      resolve();
    });
  };

  const getCharts = () => {
    return new Promise((resolve) => {
      options.xxx = xxxx;
      setOptions({...options});
    });
  };

  useEffect(() => {
    setLoading(true);
    Promise.all([getStatistic(), getCharts()]).finally(() => {
      setLoading(false);
    });
  }, [timeRange]);


  return (
    <>
      <DateRange value={timeRange} onChange={(val) => setTimeRange(val)} />
      <Statistic />
      <LineCharts />
    </>
  )
}

代碼開發

通常來說,我個人的習慣是先實現需求,再進行代碼優化和分割,模塊的提取等。所以以下優化都是基於所有業務邏輯已經完成的情況下。

hooks

通常,在完成業務後,一個組件內部會包含大量的 hooks 相關的東西。通常優化手段如下:

  • 減少 useState的使用,將不影響渲染的數據放到 useRef 里去,甚至說常量可以放到函數外部。同時,如果是同一個種類的可以進行合併
  • 減少 useMemo 的時候,普通的賦值或聲明或簡單的計算完全不需要引入 useMemo,可以在複雜的計算時加上 useMemo
  • 避免 useCallback 的使用,目前想到的 useCallback 的場景,只有addEventListener的時候,其餘情況下大部分都用不到。
  • useEffect 可以進行寫多個的,所以有些時候不同的邏輯可以放到不同的 useEffect 里去
  • useRef 可以大量持有,useRefcover 的場景遠大於 ref
  • useContext在簡單場景下完全可以替代 redux,但是有性能問題。所以複雜場景下,建議是配合use-context-selector使用,或者選擇其他狀態管理工具,如:recoil
  • useLayoutEffectuseEffect在絕大部分應用情況下沒有差異,只需要直接使用 useEffect 即可。目前考慮前者的唯一不可替代性僅存在於「閃爍」場景。
  • useReducer 一般來說用不到,其使用場景應該是當存在多個數據,而某一個參數的改變會引起其他數據同時改變,從而引起頁面重渲染。
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
  • useImperativeHandler在通常情況下避免使用,使用場景應該只有兩種。第一種是要做 ref 的轉發,比如封裝了一個組件,需要把組件內部的某一個 input 的 ref 轉發給父組件。第二種是封裝了一個組件,該組件內部實現邏輯是非 JSX 的,存在實例,則需要通過該 hooks 把相關實例轉發給父組件。
  • useDebugValue不熟
  • useDeferredValue不熟
  • useTransition不熟
  • useId不熟
  • useSyncExternalStore不熟
  • useInsertionEffect不熟

Ant Design 相關

眾所周知,React 庫搭配 Ant Design 食用是後臺管理系統的高效的原因之一。
Ant Design 相關實踐如下:

  • 巧用 Space 組件,個人感覺有點類似於簡單場景的 Grid 組件
  • 複雜表單經常使用 Steps 做步驟分割,個人更加推薦 Steps+Form 而不是 Steps.item+Form
  • AutoComplete 個人感覺有很多問題,譬如數據回填高亮等問題,個人感覺不如直接 Input
  • Form組件比較複雜,且配合其他組件的用法比較多,如 Form+Steps Form+Tabs Form+Modal 等,需要註意 inactiveDestroy + preserve={false}。另外需要註意 requiredMarkrequired 的區別,validator 和其他 rules 的區別,setFieldsValuesetFields 的區別
  • Input等輸入控制項需要註意 placeholder 的值,Select 需要額外主要 allowSearch
  • Select個人習慣直接通過 options 賦值,而不是 children(原因:相比 jsx 會有更加好的渲染性能)
  • select可以通過 dropdownRender自定義下拉內容
  • Tree 組件面向的場景比較複雜的情況下,個人覺得 antd 的 tree 組件不好用,偏向於自己手寫
  • 所有的 popup 相關組件都可以設置 getPopupContainer,該屬性用來修改組件渲染位置,如果遇到彈出層沒有隨滾動條滾動可以設置,但是設置完可能需要考慮 overflow:hidden 的問題
  • Table組件的 rowKey 屬性很重要,必加。
  • 需要註意 Table + Modal 的寫法,不要把 Modal 寫到操作列裡面去
  • ModalDrawer組件不推薦用 visible && <Modal />的寫法,會導致動畫丟失。建議在寫 Modal 或者 Drawer 的時候,把「空狀態」考慮進去。同時可以配合 ModaldestroyOnClose屬性可以讓每次 Modal 打開內容都是新內容。(PS:Form 除外)
  • Spin 可以多,但不能少

其他

  • 愛惜你的 div,不要動不動就搞個 div,過多的層級結構會影響載入速度的。量變引起質變
  • 儘量避免 ref 的使用,在通常情況下如果考慮用 ref 解決問題的話,那可能代表了你的思路不對或代碼設計不對。
  • 有時候,可以用 IIFE。做到不脫離當前的上下文,又實現了相關邏輯的提取。比函數中定義函數更好一些。

總結

以上的相關實踐,是本人在日積月累中總結和摸索出來的。
如有雷同,說明你和我有一樣的感受。


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

-Advertisement-
Play Games
更多相關文章
  • ECMAScript簡介 ECMAScript6.0 ,簡稱 ES6 。ECMAScript 是一種由 ECMA 國際通過 ECMA-262 標準化的腳本,為 JavaScript 語言的下一代標準, 在 2015 年 6 月正式發佈。 類比於 Java ,經典程度堪比 JDK 1.8 版本。 但是 ...
  • 先製作一個正方形,讓圓點在正方形的最外側 <style> body { margin: 0; } .loading { width: 200px; height: 200px; background: skyblue; margin: 100px auto 0px; position: relati ...
  • 第四期 · 將部分數據存儲至Mysql,使用axios通過golang搭建的http伺服器獲取數據。 新建資料庫 DROP DATABASE VUE; create database if not exists vue; use vue; JSON TO MYSQL JSON to MySQL (t ...
  • 1、媒體元素 音頻和視頻 <!-- 音頻和視頻 src:資源路徑 controls:控制條 autoplay:自動播放--> <video src="" controls outoplay></video><audio src="" controls outoplay></autio> 2、頁面結構 ...
  • 好家伙,本篇介紹如何實現"刪"功能 來看效果, 資料庫 (自然是沒什麼毛病) "增"搞定了,其實"刪"非常簡單 (我不會告訴你我是為了水一篇博客才把他們兩個分開寫,嘿嘿) 邏輯簡潔明瞭: 首先,看見你要刪除的數據,點"刪除", 隨後,①拿到當前這條數據的Id,向後臺發請求網路, 然後,②後端刪除該字 ...
  • 怎麼樣子盒子能撐起父盒子? 從行內元素跟塊元素來看: 一般情況下,行內元素只能包含數據和其他行內元素。 而塊級元素可以包含行內元素和其他塊級元素. 塊級元素內部可以嵌套塊級元素或行內元素。 建議行內元素裡面只嵌套行內元素。 行內元素只能包含內容或者其它行內元素,寬度和長度依據內容而定,不可以設置,可 ...
  • ####事件組成,事件三要素 1.事件源:事件觸發的按鈕,比如滑鼠點擊某個圖標跳轉頁面,那個圖標就稱為事件源。 比如, <button>我是一個按鈕,也是事件源</button> 2.事件類型:事件觸發的方式,怎麼觸發一個事件,比如滑鼠點擊(oncilck),滑鼠經過,還是按下鬆開觸發。 3.事件處 ...
  • ES標準下中的let,var和const let會報重覆聲明,var則比較隨意,重不重覆無所謂 // 使用 var 的時候重覆聲明變數是沒問題的,只不過就是後面會把前面覆蓋掉 var num = 100 var num = 200 // 使用 let 重覆聲明變數的時候就會報錯了 let num = ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...