深入理解Redux

来源:https://www.cnblogs.com/xiaohuochai/archive/2018/04/17/8652683.html
-Advertisement-
Play Games

[1]MVC [2]Flux [3]Redux [4]容器和展示 [5]react-redux [6]模塊化應用 [7]reselect [8]常見錯誤 ...


前面的話

  Redux是Flux思想的另一種實現方式。Flux是和React同時面世的。React用來替代jQuery,Flux用來替換Backbone.js等MVC框架。在MVC的世界里,React相當於V(view)的部分,只涉及頁面的渲染。一旦涉及應用的數據管理部分,還是交給Model和Controller。不過,Flux並不是一個MVC框架,它用一種新的思路來管理數據。本文將詳細介紹Redux的內容

 

MVC

  MVC是業界廣泛接受的一種前端應用框架類型,這種框架把應用分為三個部分:

  Model(模型)負責管理數據,大部分業務邏輯應該放在Model中

  View(視圖)負責渲染用戶頁面,應該避免在View中涉及業務邏輯

  Controller(控制器)負責接受用戶輸入,根據用戶輸入調用相應的Model部分邏輯,把產生的數據結果交給View部分,讓View渲染出必要的輸出

  MVC框架提出的數據流很理想,用戶請求先到達Controller,由Controller調用Model獲得數據,然後把數據交給View。但是,在實際框架實現中,總是允許View和Model直接通信

  然而,在MVC中讓View和Model直接對話就是災難

 

Flux

  Facebook用Flux框架來替代原有的MVC框架,這種框架包含四個部分:

  Dispatcher負責動作分發,維持Store之間的依賴關係

  Store負責存儲數據和處理數據相關邏輯

  Action驅動Dispatcher的javascript對象

  View視圖負責顯示用戶界面

  如果非要把Flux和MVC做一個對比。那麼,Flux的Dispatcher相當於MVC的Controller,Flux的store相當於MVC的model,Flux的View對應於MVC的View,Action對應給MVC框架的用戶請求

  1、Dispatcher

import {Dispatcher} from 'flux';
export default new Dispatcher();

  2、Action

  定義Action通常需要兩個文件,一個定義action的類型,一個定義action的構造函數。分成兩個文件的原因是在Store中會根據action類型做不同操作,也就有單獨導入action類型的需要

export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
import * as ActionTypes from './ActionTypes.js';
import AppDispatcher from './AppDispatcher.js';

export const increment = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  });
};

export const decrement = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  });
};

  3、Store

  一個Store也是一個對象,這個對象用來存儲應用狀態,同時還要接受Dispatcher派發的動作,根據動作來決定是否要更新應用狀態

  一個EventEmitter實例對象支持下列相關函數

emit函數:可以廣播一個特定事件,第一個參數是字元串類型的事件名稱
on函數:可以增加一個掛在這個EventEmitter對象特定事件上的處理函數,第一個參數是字元串類型的事件名稱,第二個參數是處理函數
removeListener函數: 和on函數做的事情相反,刪除掛在這個EventEmitter對象特定事件上的處理函數,和on函數一樣,第一個參數是事件名稱,第二個參數是處理函數

  [註意]如果要調用removeListener函數,就一定要保留對處理函數的引用

import AppDispatcher from '../AppDispatcher.js';
import * as ActionTypes from '../ActionTypes.js';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

const counterValues = {
  'First': 0,
  'Second': 10,
  'Third': 30
};


const CounterStore = Object.assign({}, EventEmitter.prototype, {
  getCounterValues: function() {
    return counterValues;
  },

  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  }

});

CounterStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    counterValues[action.counterCaption] ++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.counterCaption] --;
    CounterStore.emitChange();
  }
});

export default CounterStore;

  4、View

   存在於Flux框架中的React組件需要實現以下幾個功能

  (1)創建時讀取Store上狀態來初始化組件內部狀態

  (2)當Store上狀態發生變化時,組件要立刻同步更新內部狀態保持一致

  (3)View如果要改變Store狀態,必須而且只能派發action

// 父組件
class
ControlPanel extends Component { render() { return ( <div style={style}> <Counter caption="First" /> <Counter caption="Second" /> <Counter caption="Third" /> <hr/> <Summary /> </div> ); } } export default ControlPanel;
// 子組件
class Counter extends Component {
  constructor(props) {
    super(props);
    this.onChange = this.onChange.bind(this);
    this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
    this.onClickDecrementButton = this.onClickDecrementButton.bind(this);
    this.state = {count: CounterStore.getCounterValues()[props.caption]}
  }
  shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) || (nextState.count !== this.state.count);
  }
  componentDidMount() {
    CounterStore.addChangeListener(this.onChange);
  }
  componentWillUnmount() {
    CounterStore.removeChangeListener(this.onChange);
  }
  onChange() {
    const newCount = CounterStore.getCounterValues()[this.props.caption];
    this.setState({count: newCount});
  }
  onClickIncrementButton() {
    Actions.increment(this.props.caption);
  }
  onClickDecrementButton() {
    Actions.decrement(this.props.caption);
  }
  render() {
    const {caption} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button>
        <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button>
        <span>{caption} count: {this.state.count}</span>
      </div>
    );
  }
}
Counter.propTypes = {
  caption: PropTypes.string.isRequired
};
export default Counter;

【優勢】

  在Flux中,Store只有get方法,沒有set方法,根本不可能直接去修改其內部狀態,View只能通過get方法獲取Store的狀態,無法直接去修改狀態,如果View想要修改Store的狀態,只能派發一個action對象給Dispatcher

【不足】

  1、Store之間依賴關係

  在Flux的體系中,如果兩個Store之間有邏輯依賴關係,就必須用上Dispatcher的waitFor函數

  2、難以進行伺服器端渲染

  3、Store混雜了邏輯和狀態

 

Redux

  Redux的含義是Reducer+Flux。Reducer是一個電腦科學中的通用概念。以Javascript為例,數組類型有reduce函數,接受的參數是一個reducer,reducer做的事情就是把數組所有元素依次做規約,對每個元素都調用一次參數reducer,通過reducer函數完成規約所有元素的功能

  Flux的基本原則是單向數據流,Redux在此基礎上強調三個基本原則:

  1、唯一數據源

  2、保持狀態只讀

  3、數據改變只通過純函數完成

//actionTypes
export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
//actions
import * as ActionTypes from './ActionTypes.js';

export const increment = (counterCaption) => {
  return {
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  };
};

export const decrement = (counterCaption) => {
  return {
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  };
};
//store
import {createStore} from 'redux';
import reducer from './Reducer.js';

const initValues = {
  'First': 0,
  'Second': 10,
  'Third': 20
};

const store = createStore(reducer, initValues);

export default store;
//reducer
import * as ActionTypes from './ActionTypes.js';

export default (state, action) => {
  const {counterCaption} = action;

  switch (action.type) {
    case ActionTypes.INCREMENT:
      return {...state, [counterCaption]: state[counterCaption] + 1};
    case ActionTypes.DECREMENT:
      return {...state, [counterCaption]: state[counterCaption] - 1};
    default:
      return state
  }
}
// 父組件
class ControlPanel extends Component {
  render() {
    return (
      <div style={style}>
        <Counter caption="First" />
        <Counter caption="Second" />
        <Counter caption="Third" />
        <hr/>
        <Summary />
      </div>
    );
  }
}
// 子組件
class Counter extends Component {
  constructor(props) {
    super(props);
    this.onIncrement = this.onIncrement.bind(this);
    this.onDecrement = this.onDecrement.bind(this);
    this.onChange = this.onChange.bind(this);
    this.getOwnState = this.getOwnState.bind(this);
    this.state = this.getOwnState();
  }
  getOwnState() {
    return {value: store.getState()[this.props.caption]};
  }
  onIncrement() {
    store.dispatch(Actions.increment(this.props.caption));
  }
  onDecrement() {
    store.dispatch(Actions.decrement(this.props.caption));
  }
  onChange() {
    this.setState(this.getOwnState());
  }
  shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) ||
      (nextState.value !== this.state.value);
  }
  componentDidMount() {
    store.subscribe(this.onChange);
  }
  componentWillUnmount() {
    store.unsubscribe(this.onChange);
  }
  render() {
    const value = this.state.value;
    const {caption} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={this.onIncrement}>+</button>
        <button style={buttonStyle} onClick={this.onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}
Counter.propTypes = {
  caption: PropTypes.string.isRequired
};

 

容器和展示

  一個React組件基本上要完成以下兩個功能:

  1、讀取Store的狀態,用於初始化組件的狀態,同時還要監聽Store的狀態改變;當Store狀態發生變化時,需要更新組件狀態,從而驅動組件重新渲染;當需要更新Store狀態時,就要派發action對象

  2、根據當前props和state,渲染出用戶界面

  讓一個組件只專註做一件事。於是,按照這兩個功能拆分成兩個組件。這兩個組件是父子組件的關係。業界對於這樣的拆分有多種叫法,承擔第一個任務的組件,也就是負責和redux打交道的組件,處於外層,被稱為容器組件;只專業負責渲染界面的組件,處於內層,叫做展示組件

  展示組件,又稱為傻瓜組件,就是一個純函數,根據props產生結果。實際上,讓展示組件無狀態,只根據props來渲染結果,是拆分的主要目的之一。狀態全部交給容器組件去處理

function Counter (props){
    const {caption, onIncrement, onDecrement, value} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={onIncrement}>+</button>
        <button style={buttonStyle} onClick={onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}

  或者,直接使用解構賦值的方法

function Counter ({caption, onIncrement, onDecrement, value} ){
    return (
      <div>
        <button style={buttonStyle} onClick={onIncrement}>+</button>
        <button style={buttonStyle} onClick={onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}

 

React-redux

  react-redux遵循將組件分成展示組件和容器組件的規範。react-redux提供了兩個功能:

  1、Provider組件,可以讓容器組件預設可以取得state,而不用當容器組件層級很深時,一級級將state傳下去

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';

import ControlPanel from './views/ControlPanel';
import store from './Store.js';

import './index.css';

ReactDOM.render(
  <Provider store={store}>
    <ControlPanel/>
  </Provider>,
  document.getElementById('root')
);

  2、connect方法,用於從展示組件生成容器組件。connect的意思就是將這兩種組件連接起來

import React, { PropTypes } from 'react';
import * as Actions from '../Actions.js';
import {connect} from 'react-redux';

const buttonStyle = {
  margin: '10px'
};

function Counter({caption, onIncrement, onDecrement, value}) {
  return (
    <div>
      <button style={buttonStyle} onClick={onIncrement}>+</button>
      <button style={buttonStyle} onClick={onDecrement}>-</button>
      <span>{caption} count: {value}</span>
    </div>
  );
}

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired,
  value: PropTypes.number.isRequired
};

function mapStateToProps(state, ownProps) {
  return {
    value: state[ownProps.caption]
  }
}

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrement: () => {
      dispatch(Actions.increment(ownProps.caption));
    },
    onDecrement: () => {
      dispatch(Actions.decrement(ownProps.caption));
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

 

模塊化應用

  從架構出發,開始一個新應用時,有幾件事情是一定要考慮清楚的:

  1、代碼文件的組織結構

  2、確定模塊的邊界

  3、Store的狀態樹設計

【代碼文件的組織結構】

  Redux應用適合於按功能組織,也就是把完成同一應用功能的代碼放在一個目錄下,一個應用功能包含多個角色的代碼。在Redux中,不同的角色就是reducer、actions和視圖。而應用功能對應的就是用戶界面上的交互模塊

  以Todo應用為例,這個應用的兩個基本功能就是TodoList和Filter,所以代碼可以這樣組織:

todoList/
    actions.js
    actionTypes.js
    index.js
    reduce.js
    views/
        component.js
        container.js
filter/
    actions.js
    actionTypes.js
    index.js
    reduce.js
    views/
        component.js
        container.js

【模塊介面】

  不同功能模塊之間的依賴關係應該簡單而清晰,也就是所謂的保持模塊之間低耦合性;一個模塊應該把自己的功能封裝得很好,讓外界不要太依賴於自己內部的結構,這樣不會因為內部的變化而影響外部模塊的功能,這就是所謂的高內聚性

【狀態樹的設計】

  狀態樹的設計需要遵循如下幾個原則:

  1、一個模塊控制一個狀態節點

  2、避免冗餘數據

  3、樹形結構扁平

  對於Todo應用的狀態樹設計如下

{
  todos: [
    {
      text: 'first todo',
      completed: false,
      id: 0
    },
    {
      text: 'second todo',
      completed: true,
      id: 1
    },    
  ],
  // 'all'、'completed'、'uncompleted'
  filter: 'all'
}

 

reselect

  reselect庫的原理是只要相關狀態沒有改變,那就直接使用上一次的緩存結果。reselect用來創造選擇器,接收一個state作為參數的函數,返回的數據是某個mapStateToProps需要的結果

  首先,安裝reselect庫

npm install --save reselect

  reselect提供了創造選擇器的createSelector函數,這是一個高階函數,也就是接受函數為參數來產生一個新函數的函數

  createSelector 接收一個 input-selectors 數組和一個轉換函數作為參數。如果 state tree 的改變會引起 input-selector 值變化,那麼 selector 會調用轉換函數,傳入 input-selectors 作為參數,並返回結果。如果 input-selectors 的值和前一次的一樣,它將會直接返回前一次計算的數據,而不會再調用一次轉換函數。

import { createSelector } from 'reselect'

const getVisibilityFilter = (state) => state.visibilityFilter
const getTodos = (state) => state.todos

export const getVisibleTodos = createSelector(
  [ getVisibilityFilter, getTodos ],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  }
)

  在上例中,getVisibilityFilter 和 getTodos 是 input-selector。因為他們並不轉換數據,所以被創建成普通的非記憶的 selector 函數。但是,getVisibleTodos 是一個可記憶的 selector。他接收 getVisibilityFilter 和 getTodos 為 input-selector,還有一個轉換函數來計算過濾的 todos 列表

  reselect的典型應用如下所示

// selector
export const getCategories = state => {
  return state.category
}
export const getCategoriesSortByNumber = createSelector(getCategories, categories =>
  categories.sort((v1, v2) => {
    return v1.number - v2.number
  })
)
export const getCategoryDatas = createSelector(getCategoriesSortByNumber, categoriesSortByNumber => 
  categoriesSortByNumber.map(t => {
    return $_setChildren(categoriesSortByNumber, t)
  }).map(t => {
    return Object.assign(t, {
      index: $_getIndex(t.number),
      des: t.children.length ? t.children.length : '',
      title: t.name,
      key: t.number,
      className: 'styled-categorylist',
      url: t.children.length ? `/category/${t.number}` : '',
      parentUrl: `/category/${$_getParentNumber(t)}`,
      nextChildNumber: $_getFirstChildNumber(t)
    })
  })  
)
export const getCategoryDatasByNumber = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.reduce((obj, t) => {
    obj[t.number] = t
    return obj
  }, {})
)
export const getCategoryRootDatas = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.filter(t => {
    return Number(String(t.number).slice(2)) === 0
  })
)
export const getCategoryDatasById = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.reduce((obj, t) => {
    obj[t._id] = t
    return obj
  }, {})
)

 

常見錯誤

  在使用redux的過程中,會出現如下的常見錯誤

【錯誤:reducers不能觸發actions】

Uncaught Error: Reducers may not dispatch actions.

  一般來說,出現"Reducedrs may not dispatch actions"的錯誤,是因為reducer中出現路由跳轉語句,而跳轉到的語句正好發送了dispatch。從而,reducer不再是純函數

  錯誤代碼如下所示:

export const logIn = admin => ({type: LOGIN, admin})

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      // 將用戶信息保存到sessionStorage中
      sessionStorage.setItem('token', token)
      sessionStorage.setItem('user', JSON.stringify(user))
      // 跳轉到首頁
      history.push('/')
      return { token, user }
...

  有兩種解決辦法

  1、給路由跳轉語句設置延遲定時器,從而避免在當前reducer還沒有返回值的情況下,又發送新的dispatch

export const logIn = admin => ({type: LOGIN, admin})

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      // 將用戶信息保存到sessionStorage中
      sessionStorage.setItem('token', token)
      sessionStorage.setItem('user', JSON.stringify(user))
      // 跳轉到首頁
      setTimeout(() => {
        history.push('/')
      },0)
      return { token, user }
...

  2、將reducer中的邏輯放到dispatch中

export const logIn = (admin) => {
  let { token, user } = admin
  // 將用戶信息保存到sessionStorage中
  sessionStorage.setItem('token', token)
  sessionStorage.setItem('user', JSON.stringify(user)) 
  // 跳轉到首頁
  history.push('/')
  return {type: LOGIN, admin}
}

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      return { token, user }
...

【action函數中無法執行return後的語句】

   例如,在下麵代碼中,控制台只能輸入'111',而不能輸出'222'

export const updatePost = payload => {
  console.log('111')
  return dispatch => {
    console.log('222')
    fetchModule({
      dispatch,
      url: `${BASE_POST_URL}/${payload._id}`,
      method: 'put',
      data: payload,
      headers: { Authorization: sessionStorage.getItem('token') },
      success(result) {
        console.log(result)
        dispatch({ type: UPDATE_POST, doc: result.doc })
      }
    })
  }
}

  出現這個問題的原因非常簡單,是因為沒有使用this.props.updatePost,而直接使用了updatePost方法導致的

  加入如下語句既可解決

let { updatePost } = this.props

【redux中的state發生變化,但頁面沒有重新渲染】

  一般地,是因為增操作中,數組的展開運算符使用不當所至

   代碼如下所示,一定要將...state放到最後一個條目位置,才能使用頁面重新渲染

    case ADD_POST:
      return [action.doc, ...state]

  而對於對象的展開運算符,則需要把...state放到第一個條目位置,因為後面的條目會覆蓋展開的部分

return {...item, completed: !item.completed}

【reducer中不能使用undefined】

  1、reducer中state不能返回undefined,可以用null代替

// reducer
const filter = (state = null, action) => {
  switch (action.type) {
    case SHOW_FILTER:
      return action.filter
    default:
      return state
  }
}

   2、同樣地,action.filter表示空值,不能為undefined,用null代替

export const setFilter = filter => ({type: SHOW_FILTER, filter})
this
.props.setFilter(null)

 


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

-Advertisement-
Play Games
更多相關文章
  • 在JavaEE中,有一個Junit測試包 而在開發安卓中,我們要使用谷歌公司開發好的一些類 代碼如下: 這裡要測試一個計算器類: 在配置文件下添加這一行: 在外部加上這幾行: 寫一個測試類: 測試成功! ...
  • 1:線性佈局 2.相對佈局: 這兩種最常用,其他的如下: 3.幀佈局:重疊在一起 後兩種不常用:表格、絕對佈局,實際開發已過時 ...
  • android kl(key layout)文件是一個映射文件,是標準linux與anroid的鍵值映射文件,kl文件可以有很多個,但是它有一個使用優先順序: 如果你沒有為設備單獨定義kl文件,那麼就會使用預設的那個Generic.kl文件。 例如: 還有一點需要註意,我們怎麼知道這個vendor號和 ...
  • 註意設置許可權: 佈局: 上邊採用的是按鈕點擊事件第一種:內部類 第二種:匿名內部類 第三種:實現介面(多按鈕時候推薦使用) 第四種:聲明方法 ...
  • 最近發現越來越多的朋友,在使用PC端的小程式多客服系統,有的朋友也在後臺問,有沒有一款,手機端的工具,那樣才是真正的隨時隨地處理客戶的消息。 剛好,有看到一款比較用心的手機端工具,特點具有類似QQ聊天界面,溝通無障礙,使用無門檻,回覆消息,快速高效。重點是收到新消息提醒,有提示音。 芝麻小客服 體驗 ...
  • app 下載更新 file-downloader 文件下載庫的簡單介紹和使用 ...
  • Android Studio新建或者打開項目的時候,一直卡在Building "" Gradle project info 進度上不動,猜測是網路原因下載gradle不成功。 兩種解決方法: 1、使用本地distributionUrl:找到distributionUrl對應路徑(在gradle-wr ...
  • AFSecurityPolicy是AFNetworking中負責對https請求進行證書驗證的模塊,本文主要是要搞清楚它是如何工作的。 在介紹AFSecurityPolicy之前,我們先來瞭解一下https以及一些相關概念。 HTTPS 簡單來說,https是運行在SSL/TLS之上的http,是為 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...