vue.js響應式原理解析與實現—實現v-model與{{}}指令

来源:https://www.cnblogs.com/chenjg/archive/2018/08/28/9548473.html
-Advertisement-
Play Games

"上一節" 我們已經分析了vue.js是通過Object.defineProperty以及發佈訂閱模式來進行數據劫持和監聽,並且實現了一個簡單的demo。今天,我們就基於上一節的代碼,來實現一個MVVM類,將其與html結合在一起,並且實現v model以及{{}}語法。 tips:本節新增代碼(去 ...


上一節我們已經分析了vue.js是通過Object.defineProperty以及發佈訂閱模式來進行數據劫持和監聽,並且實現了一個簡單的demo。今天,我們就基於上一節的代碼,來實現一個MVVM類,將其與html結合在一起,並且實現v-model以及{{}}語法。

tips:本節新增代碼(去除註釋)在一百行左右。使用的Observer和Watcher都是延用上一節的代碼,沒有修改

接下來,讓我們一步步來,實現一個MVVM類。

構造函數

首先,一個MVVM的構造函數如下(和vue.js的構造函數一樣):

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
}

和vue.js一樣,有它的data屬性以及el元素。

初始化操作

vue.js可以通過this.xxx的方法來直接訪問this.data.xxx的屬性,這一點是怎麼做到的呢?其實答案很簡單,它是通過Object.defineProperty來做手腳,當你訪問this.xxx的時候,它返回的其實是this.data.xxx。當你修改this.xxx值的時候,其實修改的是this.data.xxx的值。具體可以看如下代碼:

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
  // 初始化
  init() {
    // 對this.data進行數據劫持
    new Observer(this.data);
    // 傳入的el可以是selector,也可以是元素,因此我們要在這裡做一層處理,保證this.$el的值是一個元素節點
    this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
    // 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
    for (let key in this.data) {
      this.defineReactive(key);
    }
  }
    
  defineReactive(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.data[key];
      },
      set(newVal) {
        this.data[key] = newVal;
      }
    })
  }
  // 是否是屬性節點
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

在完成初始化操作後,我們需要對this.$el的節點進行編譯。目前我們要實現的語法有v-model和{{}}語法,v-model這個屬性只可能會出現在元素節點的attributes里,而{{}}語法則是出現在文本節點里。

fragment

在對節點進行編譯之前,我們先考慮一個現實問題:如果我們在編譯過程中直接操作DOM節點的話,每一次修改DOM都會導致DOM的迴流或重繪,而這一部分性能損耗是很沒有必要的。因此,我們可以利用fragment,將節點轉化為fragment,然後在fragment里編譯完成後,再將其放回到頁面上。

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
  
  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 將fragment返回到頁面中
    document.body.appendChild(fragment);
  }
  // 將節點轉為fragment,通過fragment來操作DOM,可以獲得更高的效率
  // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的迴流或重繪,而將其放在fragment里,修改fragment不會導致DOM迴流和重繪
  // 當在fragment一次性修改完後,在直接放回到DOM節點中
  node2Fragment() {
    const fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = this.$el.firstChild) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }
}

實現v-model

在將node節點轉為fragment後,我們來對其中的v-model語法進行編譯。

由於v-model語句只可能會出現在元素節點的attributes里,因此,我們先判斷該節點是否為元素節點,若為元素節點,則判斷其是否是directive(目前只有v-model),若都滿足的話,則調用CompileUtils.compileModelAttr來編譯該節點。

編譯含有v-model的節點主要有兩步:

  1. 為元素節點註冊input事件,在input事件觸發的時候,更新vm(this.data)上對應的屬性值。
  2. 對v-model依賴的屬性註冊一個Watcher函數,當依賴的屬性發生變化,則更新元素節點的value。
class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
  
  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 將fragment返回到頁面中
    document.body.appendChild(fragment);
  }
  
  compile(node) {
    if (this.isElementNode(node)) {
      // 若是元素節點,則遍歷它的屬性,編譯其中的指令
      const attrs = node.attributes;
      Array.prototype.forEach.call(attrs, (attr) => {
        if (this.isDirective(attr)) {
          CompileUtils.compileModelAttr(this.data, node, attr)
        }
      })
    }
    // 若節點有子節點的話,則對子節點進行編譯
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }
  // 是否是屬性節點
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 檢測屬性是否是指令(vue的指令是v-開頭)
  isDirective(attr) {
    return attr.nodeName.indexOf('v-') >= 0;
  }
}

const CompileUtils = {
  // 編譯v-model屬性,為元素節點註冊input事件,在input事件觸發的時候,更新vm對應的值。
  // 同時也註冊一個Watcher函數,當所依賴的值發生變化的時候,更新節點的值
  compileModelAttr(vm, node, attr) {
    const { value: keys, nodeName } = attr;
    node.value = this.getModelValue(vm, keys);
    // 將v-model屬性值從元素節點上去掉
    node.removeAttribute(nodeName);
    node.addEventListener('input', (e) => {
      this.setModelValue(vm, keys, e.target.value);
    });
      
    new Watcher(vm, keys, (oldVal, newVal) => {
      node.value = newVal;
    });
  },
  /* 解析keys,比如,用戶可以傳入
  *  <input v-model="obj.name" />
  *  這個時候,我們在取值的時候,需要將"obj.name"解析為data[obj][name]的形式來獲取目標值
  */
  parse(vm, keys) {
    keys = keys.split('.');
    let value = vm;
    keys.forEach(_key => {
      value = value[_key];
    });
    return value;
  },
  // 根據vm和keys,返回v-model對應屬性的值
  getModelValue(vm, keys) {
    return this.parse(vm, keys);
  },
  // 修改v-model對應屬性的值
  setModelValue(vm, keys, val) {
    keys = keys.split('.');
    let value = vm;
    for(let i = 0; i < keys.length - 1; i++) {
      value = value[keys[i]];
    }
    value[keys[keys.length - 1]] = val;
  },
}

實現{{}}語法

{{}}語法只可能會出現在文本節點中,因此,我們只需要對文本節點做處理。如果文本節點中出現{{key}}這種語句的話,我們則對該節點進行編譯。在這裡,我們可以通過下麵這個正則表達式來對文本節點進行處理,判斷其是否含有{{}}語法。

const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
console.log(textReg.test('sss'));
console.log(textReg.test('aaa{{  name  }}'));
console.log(textReg.test('aaa{{  name  }} {{ text }}'));

若含有{{}}語法,我們則可以對其處理,由於一個文本節點可能出現多個{{}}語法,因此編譯含有{{}}語法的文本節點主要有以下兩步:

  1. 找出該文本節點中所有依賴的屬性,並且保留原始文本信息,根據原始文本信息還有屬性值,生成最終的文本信息。比如說,原始文本信息是"test {{test}} {{name}}",那麼該文本信息依賴的屬性有this.data.test和this.data.name,那麼我們可以根據原本信息和屬性值,生成最終的文本。
  2. 為該文本節點所有依賴的屬性註冊Watcher函數,當依賴的屬性發生變化的時候,則更新文本節點的內容。
class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
  
  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 將fragment返回到頁面中
    document.body.appendChild(fragment);
  }
  
  compile(node) {
    const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
    if (this.isTextNode(node)) {
      // 若是文本節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
      let textContent = node.textContent;
      if (textReg.test(textContent)) {
        // 對於 "test{{test}} {{name}}"這種文本,可能在一個文本節點會出現多個匹配符,因此得對他們統一進行處理
        // 使用 textReg來對文本節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
        const matchs = textContent.match(textReg);
        CompileUtils.compileTextNode(this.data, node, matchs);
      }
    }
    // 若節點有子節點的話,則對子節點進行編譯
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }
  // 是否是文本節點
  isTextNode(node) {
    return node.nodeType === 3;
  }
}

const CompileUtils = {
  reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  // 編譯文本節點,並註冊Watcher函數,當文本節點依賴的屬性發生變化的時候,更新文本節點
  compileTextNode(vm, node, matchs) {
    // 原始文本信息
    const rawTextContent = node.textContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      console.log(rawTextContent);
      new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
    });
    this.updateTextNode(vm, node, matchs, rawTextContent);
  },
  // 更新文本節點信息
  updateTextNode(vm, node, matchs, rawTextContent) {
    let newTextContent = rawTextContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      const val = this.getModelValue(vm, keys);
      newTextContent = newTextContent.replace(match, val);
    })
    node.textContent = newTextContent;
  }
}

結語

這樣,一個具有v-model和{{}}功能的MVVM類就已經完成了。代碼地址點擊這裡。有興趣的小伙伴可以上去看下(也可以star or fork下哈哈哈)。

這裡也有一個簡單的樣例(忽略樣式)。

接下來的話,可能會繼續實現computed屬性,v-bind方法,以及支持在{{}}裡面放表達式。如果覺得這個文章對你有幫助的話,麻煩點個贊,嘻嘻。

最後,貼上所有的代碼:

class Observer {
  constructor(data) {
    // 如果不是對象,則返回
    if (!data || typeof data !== 'object') {
      return;
    }
    this.data = data;
    this.walk();
  }

  // 對傳入的數據進行數據劫持
  walk() {
    for (let key in this.data) {
      this.defineReactive(this.data, key, this.data[key]);
    }
  }
  // 創建當前屬性的一個發佈實例,使用Object.defineProperty來對當前屬性進行數據劫持。
  defineReactive(obj, key, val) {
    // 創建當前屬性的發佈者
    const dep = new Dep();
    /*
    * 遞歸對子屬性的值進行數據劫持,比如說對以下數據
    * let data = {
    *   name: 'cjg',
    *   obj: {
    *     name: 'zht',
    *     age: 22,
    *     obj: {
    *       name: 'cjg',
    *       age: 22,
    *     }
    *   },
    * };
    * 我們先對data最外層的name和obj進行數據劫持,之後再對obj對象的子屬性obj.name,obj.age, obj.obj進行數據劫持,層層遞歸下去,直到所有的數據都完成了數據劫持工作。
    */
    new Observer(val);
    Object.defineProperty(obj, key, {
      get() {
        // 若當前有對該屬性的依賴項,則將其加入到發佈者的訂閱者隊列里
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (val === newVal) {
          return;
        }
        val = newVal;
        new Observer(newVal);
        dep.notify();
      }
    })
  }
}

// 發佈者,將依賴該屬性的watcher都加入subs數組,當該屬性改變的時候,則調用所有依賴該屬性的watcher的更新函數,觸發更新。
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    if (this.subs.indexOf(sub) < 0) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    })
  }
}

Dep.target = null;

// 觀察者
class Watcher {
  /**
   *Creates an instance of Watcher.
   * @param {*} vm
   * @param {*} keys
   * @param {*} updateCb
   * @memberof Watcher
   */
  constructor(vm, keys, updateCb) {
    this.vm = vm;
    this.keys = keys;
    this.updateCb = updateCb;
    this.value = null;
    this.get();
  }

  // 根據vm和keys獲取到最新的觀察值
  get() {
    // 將Dep的依賴項設置為當前的watcher,並且根據傳入的keys遍歷獲取到最新值。
    // 在這個過程中,由於會調用observer對象屬性的getter方法,因此在遍歷過程中這些對象屬性的發佈者就將watcher添加到訂閱者隊列里。
    // 因此,當這一過程中的某一對象屬性發生變化的時候,則會觸發watcher的update方法
    Dep.target = this;
    this.value = CompileUtils.parse(this.vm, this.keys);
    Dep.target = null;
    return this.value;
  }

  update() {
    const oldValue = this.value;
    const newValue = this.get();
    if (oldValue !== newValue) {
      this.updateCb(oldValue, newValue);
    }
  }
}

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }

  // 初始化
  init() {
    // 對this.data進行數據劫持
    new Observer(this.data);
    // 傳入的el可以是selector,也可以是元素,因此我們要在這裡做一層處理,保證this.$el的值是一個元素節點
    this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
    // 將this.data的屬性都綁定到this上,這樣用戶就可以直接通過this.xxx來訪問this.data.xxx的值
    for (let key in this.data) {
      this.defineReactive(key);
    }
  }

  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    document.body.appendChild(fragment);
  }
  // 將節點轉為fragment,通過fragment來操作DOM,可以獲得更高的效率
  // 因為如果直接操作DOM節點的話,每次修改DOM都會導致DOM的迴流或重繪,而將其放在fragment里,修改fragment不會導致DOM迴流和重繪
  // 當在fragment一次性修改完後,在直接放回到DOM節點中
  node2Fragment() {
    const fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = this.$el.firstChild) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }

  defineReactive(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.data[key];
      },
      set(newVal) {
        this.data[key] = newVal;
      }
    })
  }

  compile(node) {
    const textReg = /\{\{\s*\w+\s*\}\}/gi; // 檢測{{name}}語法
    if (this.isElementNode(node)) {
      // 若是元素節點,則遍歷它的屬性,編譯其中的指令
      const attrs = node.attributes;
      Array.prototype.forEach.call(attrs, (attr) => {
        if (this.isDirective(attr)) {
          CompileUtils.compileModelAttr(this.data, node, attr)
        }
      })
    } else if (this.isTextNode(node)) {
      // 若是文本節點,則判斷是否有{{}}語法,如果有的話,則編譯{{}}語法
      let textContent = node.textContent;
      if (textReg.test(textContent)) {
        // 對於 "test{{test}} {{name}}"這種文本,可能在一個文本節點會出現多個匹配符,因此得對他們統一進行處理
        // 使用 textReg來對文本節點進行匹配,可以得到["{{test}}", "{{name}}"]兩個匹配值
        const matchs = textContent.match(textReg);
        CompileUtils.compileTextNode(this.data, node, matchs);
      }
    }
    // 若節點有子節點的話,則對子節點進行編譯。
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }
  
  // 是否是屬性節點
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 是否是文本節點
  isTextNode(node) {
    return node.nodeType === 3;
  }

  isAttrs(node) {
    return node.nodeType === 2;
  }
  // 檢測屬性是否是指令(vue的指令是v-開頭)
  isDirective(attr) {
    return attr.nodeName.indexOf('v-') >= 0;
  }

}

const CompileUtils = {
  reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  // 編譯文本節點,並註冊Watcher函數,當文本節點依賴的屬性發生變化的時候,更新文本節點
  compileTextNode(vm, node, matchs) {
    // 原始文本信息
    const rawTextContent = node.textContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      console.log(rawTextContent);
      new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
    });
    this.updateTextNode(vm, node, matchs, rawTextContent);
  },
  // 更新文本節點信息
  updateTextNode(vm, node, matchs, rawTextContent) {
    let newTextContent = rawTextContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      const val = this.getModelValue(vm, keys);
      newTextContent = newTextContent.replace(match, val);
    })
    node.textContent = newTextContent;
  },
  // 編譯v-model屬性,為元素節點註冊input事件,在input事件觸發的時候,更新vm對應的值。
  // 同時也註冊一個Watcher函數,當所依賴的值發生變化的時候,更新節點的值
  compileModelAttr(vm, node, attr) {
    const { value: keys, nodeName } = attr;
    node.value = this.getModelValue(vm, keys);
    // 將v-model屬性值從元素節點上去掉
    node.removeAttribute(nodeName);
    new Watcher(vm, keys, (oldVal, newVal) => {
      node.value = newVal;
    });
    node.addEventListener('input', (e) => {
      this.setModelValue(vm, keys, e.target.value);
    });
  },
  /* 解析keys,比如,用戶可以傳入
  *  let data = {
  *    name: 'cjg',
  *    obj: {
  *      name: 'zht',
  *    },
  *  };
  *  new Watcher(data, 'obj.name', (oldValue, newValue) => {
  *    console.log(oldValue, newValue);
  *  })
  *  這個時候,我們需要將keys解析為data[obj][name]的形式來獲取目標值
  */
  parse(vm, keys) {
    keys = keys.split('.');
    let value = vm;
    keys.forEach(_key => {
      value = value[_key];
    });
    return value;
  },
  // 根據vm和keys,返回v-model對應屬性的值
  getModelValue(vm, keys) {
    return this.parse(vm, keys);
  },
  // 修改v-model對應屬性的值
  setModelValue(vm, keys, val) {
    keys = keys.split('.');
    let value = vm;
    for(let i = 0; i < keys.length - 1; i++) {
      value = value[keys[i]];
    }
    value[keys[keys.length - 1]] = val;
  },
}

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

-Advertisement-
Play Games
更多相關文章
  • javascript基礎修煉(4)——UMD規範的代碼推演 1. UMD規範 地址:https://github.com/umdjs/umd 規範,就是所有規範里 長得最醜的那個 ,沒有之一!!!它是為了讓模塊同時相容 和`CommonJs UMD ES harmony`的統一的規範後,它也將退出歷 ...
  • # vue項目的搭建 本文章主要講述vue項目的搭建,在搭建vue項目前需要準備一些材料。 1.[node](https://nodejs.org/zh-cn/)安裝 npm:在你安裝node的時候一般node已經自帶了npm,所以忽略。 webpack:npm install webpack -g ...
  • ps:由於公司網站配置的測試環境被百度爬蟲抓取,干擾了線上正常環境的使用,剛好看到每次搜索淘寶時,都會有一句由於robots.txt文件存在限制指令無法提供內容描述,於是便去學習了一波 1.原來一般來說搜索引擎爬取網站時都會,先讀取下robots.txt文件,並依照裡面所設定的規則去爬取網站(當然是 ...
  • 一、前言 初入Vue.js的新世界,總歸是要瞭解些涉及到的新概念。菜鳥誕生的第一課,開眼看看Vue的新世界~~~ 學習系列目錄地址:https://www.cnblogs.com/danvic712/p/9549100.html 倉儲地址:https://github.com/Lanesra712/ ...
  • 一、前言 biu biu biu,從上家辭職後,在複習著.NET的相關知識點,準備著面試。同時呢也學著使用ASP.NET Core 2.0 WebAPI和Vue.js搭建一個前後端分離的項目,嗯,都是之前沒有接觸過,正在現學這些東西。因為主要還是會側重於後端,所以可能前端的東西不會看的很深入,如果有 ...
  • 爛筆頭開始記錄小知識點啦~ 瀏覽器要載入 ES6模塊,: <script type="module" src="./foo.js"></script> 非同步載入,相當與defer屬性。可以另外設置async屬性。 ES6 模塊也允許內嵌在網頁中,語法行為與載入外部腳本完全一致。 <script ty ...
  • 小程式上傳圖片要先瞭解他其中的各個屬性值,在https://developers.weixin.qq.com/miniprogram/dev/api/media-picture.html內有詳細的介紹 今天在這裡主要來講下如何去上傳圖片併進行壓縮,瞭解下以下屬性值 先來看下頁面展示(點擊上傳圖片,從 ...
  • 因為下個項目中要用到一些倒計時的功能,所以就提前準備了一下,省的到時候出現一下界面不友好和一些其他的事情。正好趁著這個機會也加深一下html5中的多線程worker的用法和理解。 Worker簡介 JavaScript 語言採用的是單線程模型,也就是說,所有任務只能在一個線程上完成,一次只能做一件事 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...