.13-淺析webpack源碼之WatcherManager模塊

来源:http://www.cnblogs.com/QH-Jimmy/archive/2017/12/19/8066501.html
-Advertisement-
Play Games

從模塊流可以看出,這個NodeWatchFileSystem模塊非常深,這裡暫時不會深入到chokidar模塊,有點太偏離本系列文章了,從WatcherManager開始講解。 流程如圖: 源碼非常簡單,包括一個工廠函數與兩個原型方法,整理如下: 包含一個容器類和三個實例方法,每一次調用watchF ...


  從模塊流可以看出,這個NodeWatchFileSystem模塊非常深,這裡暫時不會深入到chokidar模塊,有點太偏離本系列文章了,從WatcherManager開始講解。

  流程如圖:

  源碼非常簡單,包括一個工廠函數與兩個原型方法,整理如下:

var path = require("path");

class WatcherManager {
    constructor() {
        // 監視容器
        this.directoryWatchers = {};
    };
    getDirectoryWatcher(directory, options) {
        var DirectoryWatcher = require("./DirectoryWatcher");
        options = options || {};
        // 目錄路徑拼接參數 這個有夠厲害的
        // 假設directory為lib options不傳 拼接後為'lib {}'
        var key = directory + " " + JSON.stringify(options);
        if (!this.directoryWatchers[key]) {
            // 根據監視路徑生成一個DirectoryWatcher實例
            this.directoryWatchers[key] = new DirectoryWatcher(directory, options);
            // 監聽監視關閉事件
            this.directoryWatchers[key].on("closed", function() {
                delete this.directoryWatchers[key];
            }.bind(this));
        }
        // 返回對應的實體類
        return this.directoryWatchers[key];
    };
    // 路徑 參數 開始事件
    watchFile(p, options, startTime) {
        // 返回目錄名作為根目錄
        // lib/newFile.js => lib
        var directory = path.dirname(p);
        // 生成實例並調用watch方法
        // 由於上面返回的是實體類 這裡可以進行鏈式調用
        return this.getDirectoryWatcher(directory, options).watch(p, startTime);
    };
    watchDirectory(directory, options, startTime) {
        return this.getDirectoryWatcher(directory, options).watch(directory, startTime);
    };
}

module.exports = new WatcherManager();

  包含一個容器類和三個實例方法,每一次調用watchFile或watchDirectory方法時會在容器中添加一個目錄監視信息,在關閉監視事會刪除對應的信息。

 

  主流方法還是引用的DirectoryWatcher模塊,從構造函數開始詳細看源碼:

function DirectoryWatcher(directoryPath, options) {
    // 繼承EventEmitter
    EventEmitter.call(this);
    // 獲取配置
    this.options = options;
    // 根目錄
    this.path = directoryPath;
    // 根目錄下的文件信息
    this.files = Object.create(null);
    // 根目錄下的文件夾信息
    this.directories = Object.create(null);
    // 目錄下的文件所有監聽器容器
    this.watchers = Object.create(null);
    // 初始化監視器 跳過
    this.watcher = chokidar.watch(directoryPath, { /*options*/ });
    // 事件監聽
    this.watcher.on("add", this.onFileAdded.bind(this));
    this.watcher.on("addDir", this.onDirectoryAdded.bind(this));
    this.watcher.on("change", this.onChange.bind(this));
    this.watcher.on("unlink", this.onFileUnlinked.bind(this));
    this.watcher.on("unlinkDir", this.onDirectoryUnlinked.bind(this));
    this.watcher.on("error", this.onWatcherError.bind(this));
    // 初次掃描標記
    this.initialScan = true;
    // 對整個文件夾進行監視 僅在傳入監視路徑為文件夾時置true
    this.nestedWatching = false;
    this.initialScanRemoved = [];
    // 初始化掃描
    this.doInitialScan();
    // 記錄watchers中監聽器數量
    this.refs = 0;
}

  這裡可以分為幾塊內容:

1、繼承nodejs的事件模塊

2、獲取傳進來的路徑與配置參數

3、根據參數初始化一個watcher對象,並對文件操作做事件監聽

4、初始化掃描

  watcher對象的生成過程暫時不考慮,太深入會偏離主線任務。

 

初始化掃描

  在構造函數中會對傳進來的路徑進行掃描,源碼如下:

DirectoryWatcher.prototype.doInitialScan = function doInitialScan() {
    // 讀取根目錄
    fs.readdir(this.path, function(err, items) {
        // 即使報錯仍然置initialScan標記為false
        if (err) {
            this.initialScan = false;
            return;
        }
        // items為到根目錄下所有文件的文件名組成的數組
        // 同時包含文件與文件夾
        async.forEach(items, function(item, callback) {
            // 將路徑與文件名進行拼接獲取完整路徑
            var itemPath = path.join(this.path, item);
            // 獲取文件信息
            fs.stat(itemPath, function(err2, stat) {
                // 該方法僅支持初次掃描
                if (!this.initialScan) return;
                if (err2) {
                    callback();
                    return;
                }
                // 處理文件
                if (stat.isFile()) {
                    if (!this.files[itemPath])
                        this.setFileTime(itemPath, +stat.mtime, true);
                }
                // 處理文件夾 
                else if (stat.isDirectory()) {
                    if (!this.directories[itemPath])
                        this.setDirectory(itemPath, true, true);
                }
                callback();
            }.bind(this));
        }.bind(this), function() {
            // 回調函數中處理標記initialScan標記
            this.initialScan = false;
            this.initialScanRemoved = null;
        }.bind(this));
    }.bind(this));
};

  代碼十分易懂,基本上都是fs模塊的方法,主要分為以下幾步:

1、讀取指定根目錄下所有文件

2、將文件名與當前路徑進行拼接獲取完整路徑,然後嘗試獲取文件信息

3、分別處理文件與文件夾

  這裡的流程可以用一個案例測試,首先目錄如圖:

  a.js是執行JS文件,lib是用來測試的文件夾,包含幾個js文件和一個空文件夾。

  測試代碼如下:

// a.js
const fs = require('fs');
const async = require('async');
const path = require('path');

// 讀取文件夾
fs.readdir('./lib', (err, items) => {
    // 這裡沒有傳路徑 所以用process.cwd()模擬
    // 這裡需要拼接一下路徑
    const absPath = path.join(process.cwd(), 'lib');
    // items => ['DirectoryWatcher.js','fileDirectory',...,'watchpack.js']
    async.forEach(items, (item, callback) => {
        // 第一個元素拼接後為d:\workspace\doc\lib\DirectoryWatcher.js
        const itemPath = path.join(absPath, item);
        fs.stat(itemPath, (err2, stat) => {
            // 處理文件
            if (stat.isFile()) {
                console.log('Find file,the name is: ' + item);
            }
            // 處理文件夾 
            else if (stat.isDirectory()) {
                console.log('Find directory,the name is: ' + item);
            }
        });
    });
});

  執行JS文件後輸出如圖:

  可以看到,通過該方法可以區別開文件與文件夾,然後分類處理。

 

  下麵看兩種處理方法。

setFileTime

// this.setFileTime(itemPath, +stat.mtime, true);
// itemPath => 路徑 
// +stat.mtime => 修改時間
// 是否初始化 => true
DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    // 獲取當前時間
    var now = Date.now();
    var old = this.files[filePath];
    // 初始化取文件修改時間與當前時間的較小值
    // 否則files = {path:[now,mtime]}
    // 鍵為文件路徑 值為數組 包含當前時間與上一次修改時間
    this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime];
    // 這裡的FS_ACCURACY是假設操作可能的運行時間
    // 嘗試通過加一點點來更精確修改時間
    if (mtime)
        mtime = mtime + FS_ACCURACY;
    if (!old) {
        if (mtime) {
            if (this.watchers[withoutCase(filePath)]) { /**/ }
        }
    } else if (!initial && mtime && type !== "add") {
        /**/
    } else if (!initial && !mtime) { /**/ }
    // 初始化不會有watchers
    if (this.watchers[withoutCase(this.path)]) { /**/ }
};

  從名字也能看出這個方法的作用就是設置時間,在初始化的情況下,會在files容器中註冊,鍵為文件路徑,值為當前時間與修改時間。

  由於watchers對象此時為null,所以後面的代碼並不會進入,後面再討論。

setDirectory

// this.setDirectory(itemPath, true, true);
DirectoryWatcher.prototype.setDirectory = function setDirectory(directoryPath, exist, initial, type) {
    if (directoryPath === this.path) {
        if (!initial && this.watchers[withoutCase(this.path)]) { /**/ }
    } else {
        var old = this.directories[directoryPath];
        // 初次掃描
        if (!old) {
            if (exist) {
                // 預設為false
                if (this.nestedWatching) {
                    this.createNestedWatcher(directoryPath);
                } else {
                    // 根目錄在監聽器容器中的值預設設置為true
                    this.directories[directoryPath] = true;
                }
                if (!initial && this.watchers[withoutCase(this.path)]) { /**/ }
            }
        } else { /**/ }
    }
};

  在初始化的掃描中,根目錄下所有的文件夾也會在對應的容器中註冊一個鍵,值為true。

  其餘代碼在初始化並不會執行,後面再講。

  在經過doInitialScan初始化之後,files、directories容器會被填充進對應的鍵值對,存儲文件與文件夾的路徑信息。

 

watch

  無論是watchFile還是watchDirectory都在初始化後直接調用了watch方法對具體文件進行了監視,這裡分析該處源碼:

DirectoryWatcher.prototype.watch = function watch(filePath, startTime) {
    // 將路徑小寫
    // 第一次監視指定路徑會初始化一個空數組
    this.watchers[withoutCase(filePath)] = this.watchers[withoutCase(filePath)] || [];
    // 記數
    this.refs++;
    // 生成一個內部輔助類
    var watcher = new Watcher(this, filePath, startTime);
    // 監聽closed事件
    watcher.on("closed", function() {
        // 刪除對應的watcher
        var idx = this.watchers[withoutCase(filePath)].indexOf(watcher);
        this.watchers[withoutCase(filePath)].splice(idx, 1);
        // 當對應watcher數組為空時直接刪除該鍵
        if (this.watchers[withoutCase(filePath)].length === 0) {
            delete this.watchers[withoutCase(filePath)];
            // 如果觸發了文件夾的closed事件 關閉文件夾的監視
            if (this.path === filePath)
                this.setNestedWatching(false);
        }
        // 當watchers為空時調用類的close方法
        if (--this.refs <= 0)
            this.close();
    }.bind(this));
    // 加進去
    this.watchers[withoutCase(filePath)].push(watcher);
    var data;
    // 當監視文件路徑為一個文件夾時
    // 文件夾的修改時間應該為內部文件中修改時間最新的
    if (filePath === this.path) {
        this.setNestedWatching(true);
        data = false;
        // 取出所有文件的時間信息中最新的
        Object.keys(this.files).forEach(function(file) {
            var d = this.files[file];
            if (!data)
                data = d;
            else
                data = [Math.max(data[0], d[0]), Math.max(data[1], d[1])];
        }, this);
    }
    // 取對應文件信息 
    else {
        data = this.files[filePath];
    }
    // node中的非同步函數
    process.nextTick(function() {
        if (data) {
            // 相等說明是初始化階段 修正時間
            var ts = data[0] === data[1] ? data[0] + FS_ACCURACY : data[0];
            if (ts >= startTime)
                watcher.emit("change", data[1]);
        }
        // 監視的文件路徑之前被移除過
        else if (this.initialScan && this.initialScanRemoved.indexOf(filePath) >= 0) {
            watcher.emit("remove");
        }
    }.bind(this));
    return watcher;
};

class Watcher {
    constructor() {
        EventEmitter.call(this);
        this.directoryWatcher = directoryWatcher;
        this.path = filePath;
        this.startTime = startTime && +startTime;
        this.data = 0;
    };
    // 也不知道檢測啥的
    checkStartTime(mtime, initial) {
        if (typeof this.startTime !== "number") return !initial;
        var startTime = this.startTime;
        return startTime <= mtime;
    };
    // 此方法觸發closed事件
    close() {
        this.emit("closed");
    };
}

  內部的Watcher對象負責對應路徑文件的操作事件響應。

  watch有兩種情形,一種是普通的文件監視,一種是對文件夾的監視。

  如果是普通的文件監視,直接生成一個Watcher監聽器,然後將該監聽器加入已有目錄監視容器對應的watchers容器中。

  如果是傳入的是文件夾,其實相當於一個初始化,會對根目錄下文件夾內容做監視,代碼如下:

DirectoryWatcher.prototype.setNestedWatching = function(flag) {
    if (this.nestedWatching !== !!flag) {
        this.nestedWatching = !!flag;
        if (this.nestedWatching) {
            Object.keys(this.directories).forEach(function(directory) {
                // 對根目錄下所有文件夾路徑調用該方法
                this.createNestedWatcher(directory);
            }, this);
        } else {
            // 關閉文件夾監視
            Object.keys(this.directories).forEach(function(directory) {
                this.directories[directory].close();
                this.directories[directory] = true;
            }, this);
        }
    }
};

DirectoryWatcher.prototype.createNestedWatcher = function(directoryPath) {
    // 文件夾信息容器的值設為一個DirectoryWatcher實例
    // startTime設為1
    this.directories[directoryPath] = watcherManager.watchDirectory(directoryPath, this.options, 1);
    // 實例監聽change事件
    this.directories[directoryPath].on("change", function(filePath, mtime, type) {
        // 文件夾改變時觸發對應的監聽器
        if (this.watchers[withoutCase(this.path)]) {
            this.watchers[withoutCase(this.path)].forEach(function(w) {
                if (w.checkStartTime(mtime, false)) {
                    w.emit("change", filePath, mtime, type);
                }
            });
        }
    }.bind(this));
};

 

fs.watch

  下麵開始講解文件操時作觸發的事件處理,其中包括文件與文件夾的操作。

  先簡要介紹下nodejs原生的watch系統,官方文檔:https://nodejs.org/dist/latest-v8.x/docs/api/fs.html#fs_fs_watch_filename_options_listener。

  通過引入nodejs中的fs模塊,通過調用fs.watch方法可以對文件進行監視,具體的api如下:

const fs = reqire('fs');
fs.watch(filename /*文件名*/ , options /*配置參數 可忽略*/ , listener /*監聽器*/ )

  這裡的filename可以是文件,也可以是一個目錄。

  options有三個可選參數:

persistent:文件如果在被監視,進程是否應該繼續進行,預設為true

recursive:是否監視所有子目錄,預設為false

encoding:指定傳給監聽器文件名的字元編碼,預設為'uft-8'

  監聽器則是一個函數,有兩個參數,分別為事件類型與對應的文件名。

  這裡用了小案例來進行演示,代碼如下:

const fs = require('fs');
fs.watch('./lib', ((event, filename) => {
    console.log('event type is: ' + event);
    console.log('the relative filename is: ' + filename);
}));

  目錄結構可參考上圖,執行node指令後終端會被掛起,等待變化。

  此時新建一個文件,如圖:

  在新建成功的時候,會發現監聽器被觸發,列印信息如圖:

  修改文件內容,列印信息如圖:

  根據官方文檔,事件只有rename與change兩種,無論是添加、刪除、重命名都會觸發rename事件,而修改文件內容會觸發change事件。

  所以很明顯,框架內部對事件類型進行了細粒度更大的劃分,將rename分解為增加文件/文件夾,刪除文件/文件夾四種情況。

  實現的原理根據上面的代碼也很容易想到,可以根據文件名與files、directories容器中的鍵做比對,區分文件與文件夾,根據修改時間,區分是新建還是刪除。

 

  下麵可以看構造函數中對特殊文件操作的監聽器。

add

// 增加文件時觸發的事件
this.watcher.on("add", this.onFileAdded.bind(this));

DirectoryWatcher.prototype.onFileAdded = function onFileAdded(filePath, stat) {
    // filePath => 文件路徑
    // stat => fs.stat(...)
    // 檢測文件是否在監視目錄中
    if (filePath.indexOf(this.path) !== 0) return;
    if (/[\\\/]/.test(filePath.substr(this.path.length + 1))) return;
    // 設置文件修改時間信息
    this.setFileTime(filePath, +stat.mtime, false, "add");
};

  可以看出,進行簡單的文件合法性檢測後,還是進入了setFileTime函數,不過這一次的init標記為false,並且有對應的eventType。

  這一次setFileTime的流程如下:

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    var now = Date.now();
    // 初始化的值會被獲取
    var old = this.files[filePath];
    // initial是false 所以值為[now,mtime]
    this.files[filePath] = [initial ? Math.min(now, mtime) : now, mtime];

    // ...
};

  一句話概括就是,add情況下,只會在files容器中註冊該文件的信息。

 

addDir => 在directories容器中註冊該文件夾

 

change

DirectoryWatcher.prototype.onChange = function onChange(filePath, stat) {
    // ...
    // 會根據mtime值修改FS_ACCURACY
    ensureFsAccuracy(mtime);
    // 仍然進入此函數
    this.setFileTime(filePath, mtime, false, "change");
};

function ensureFsAccuracy(mtime) {
    if (!mtime) return;
    // 當mtime為小數時才會跳過
    if (FS_ACCURACY > 1 && mtime % 1 !== 0)
        FS_ACCURACY = 1;
    // 0-9或非10的倍數
    else if (FS_ACCURACY > 10 && mtime % 10 !== 0)
        FS_ACCURACY = 10;
    // 0-99或非100倍數
    else if (FS_ACCURACY > 100 && mtime % 100 !== 0)
        FS_ACCURACY = 100;
    else if (FS_ACCURACY > 1000 && mtime % 1000 !== 0)
        FS_ACCURACY = 1000;
    else if (FS_ACCURACY > 2000 && mtime % 2000 !== 0)
        FS_ACCURACY = 2000;
}

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    // ...
    if (!old) { /*...*/ }
    // change 
    else if (!initial && mtime && type !== "add") {
        if (this.watchers[withoutCase(filePath)]) {
            this.watchers[withoutCase(filePath)].forEach(function(w) {
                w.emit("change", mtime, type);
            });
        }
    }
    // remove 
    else if (!initial && !mtime) { /*...*/ }
    // 如果監視了根目錄
    if (this.watchers[withoutCase(this.path)]) {
        this.watchers[withoutCase(this.path)].forEach(function(w) {
            // 根目錄觸發change事件
            if (!initial || w.checkStartTime(mtime, initial)) {
                w.emit("change", filePath, mtime, initial ? "initial" : type);
            }
        });
    }
};

  這裡有一個ensureFsAccuracy函數,這裡預設的FS_ACCURACY為10000,而mtime一般都是很大的整數,所以這個函數的作用有待研究。

  可以看到change事件除了設置文件的時間信息,同時也對watchers中每個監聽器觸發了change事件。

  最後,如果根目錄設置了監視,由於監視文件在根目錄中,所以根目錄必定也發生了改變,所以根目錄的所有監視器也會同時觸發change事件。

 

unlink

DirectoryWatcher.prototype.onFileUnlinked = function onFileUnlinked(filePath) {
    // ...
    // 註意第二個參數mtime為null
    this.setFileTime(filePath, null, false, "unlink");
    // 記錄被刪除的文件路徑
    if (this.initialScan) {
        this.initialScanRemoved.push(filePath);
    }
};

DirectoryWatcher.prototype.setFileTime = function setFileTime(filePath, mtime, initial, type) {
    // ...
    if (!old) { /**/ }
    // 觸發remove事件
    else if (!initial && mtime && type !== "add") {
        if (this.watchers[withoutCase(filePath)]) {
            this.watchers[withoutCase(filePath)].forEach(function(w) {
                w.emit("change", mtime, type);
            });
        }
    } else if (!initial && !mtime) { /**/ }
    if (this.watchers[withoutCase(this.path)]) { /**/ }
};

  當刪除文件時,傳入的mtime會置null,所以會對所有的watcher觸發remove。

  另外,這裡被刪除的文件路徑會被記錄到initialScan中。

 

unlinkDir

DirectoryWatcher.prototype.onDirectoryUnlinked = function onDirectoryUnlinked(directoryPath) {
    // ...
    // 這裡調用文件夾的刪除
    this.setDirectory(directoryPath, false, false, "unlink");
    if (this.initialScan) {
        this.initialScanRemoved.push(directoryPath);
    }
};

DirectoryWatcher.prototype.setDirectory = function setDirectory(directoryPath, exist, initial, type) {
    if (directoryPath === this.path) { /**/ }
    // 刪除文件夾
    else {
        var old = this.directories[directoryPath];
        if (!old) { /**/ }
        else {
            if (!exist) {
                if (this.nestedWatching)
                    this.directories[directoryPath].close();
                // 刪除容器中的文件夾信息
                delete this.directories[directoryPath];
                if (!initial && this.watchers[withoutCase(this.path)]) { /*...*/ }
            }
        }
    }
};

  在nestedWatching參數為false的情況下,這裡是直接從文件夾信息容器中刪除對應信息,否則會調用watcher對應的close方法。

 

error

DirectoryWatcher.prototype.onWatcherError = function onWatcherError( /* err */ ) {};

  源碼中,這個事件監聽器並沒有任何內容,需要自定義。

 

  由於這節內容比較多,這裡做一個簡單的內容總結,也幫助自己複習:

  watcherManager模塊

1、有一個directoryWatchers容器保存已監視目錄信息

2、getDirectoryWatcher方法會根據監視路徑與options參數生成容器鍵,如果存在對應的值直接返回,否則新建一個DirectoryWatcher實體類註冊到容器中,並監聽closed事件,觸發時自動刪除該鍵

3、WatchFile、WatchDirectory分別處理文件、文件夾的監視,會同時調用getDirectoryWatcher方法與返回實體類的watch方法

4、在WatchFile中,監視文件所在的文件夾會作為根目錄傳入實例化的參數中,且只會監視根目錄的該文件

5、若傳入文件夾,則該文件夾目錄下所有文件與文件夾都會被監視

 

  DirectoryWatcher模塊

1、包含多個容器,分別為:

files:保存根目錄下所有文件信息

directories:保存根目錄下所有文件夾信息

initialScanRemoved:記錄已被刪除的文件或文件夾路徑

watchers:指定目錄下監聽器容器,其中鍵為監視文件的路徑,值為監聽器

2、內部細分了原生nodejs的rename、change事件,分別為add、addDir、change、unlink、unlinkDir

3、觸發了對應路徑文件的事件,會依次觸發watchers中對應路徑數組中所有監聽器

 

  完結!  

 


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

-Advertisement-
Play Games
更多相關文章
  • 1、APP跳轉 2、APP功能跳轉 3、系統功能調用 1、APP跳轉 1、設置App的URL Types(別的APP打開你的APP的地址)(項目 - TARGETS - APP icon - info - (拉到最下)URL Types ) URL Schemes:填上,你要給其他應用調用的URL地 ...
  • HBuilder開發iPad程式不能全屏顯示的解決方法: targets選擇HBuilder=>Deployment Info=> devices選擇Universal即可 ...
  • 1.現有的幾種埋點技術的實現原理和優劣分析 (1)代碼埋點:將收集數據的代碼直接寫在需要的地方,當用戶點擊某個控制項或者打開某個頁面時調用到該部分代碼完成數據的收集。 優勢:準確性高,收集數據和發送數據都能精確控制,同時能方便的設置自定義屬性,自定義控制項,自定義View等。 劣勢:埋點工作量大,更新代 ...
  • 項目開發中,有時候我們需要將本地的文件上傳到伺服器,簡單的幾張圖片還好,但是針對iPhone裡面的視頻文件進行上傳,為了用戶體驗,我們有必要實現斷點上傳。其實也不是真的斷點,這裡我們只是模仿斷點機制。 需求 既然需要上傳文件,那最好要有一個上傳列表界面,方面用戶對上傳中的文件進行實時管理。這裡我簡單 ...
  • 一直以來,大家都苦惱怎麼實現微信公眾帳號可以接入客服,也因此很多第三方介面平臺也開發客服系統CRM系統,不過不是操作複雜就是成本太高。今天分享一個低成本又簡便的方法,讓你的公眾帳號接入QQ客服。下麵介紹具體的方法:1、首先,這個方法目前微信最新的手機版本都是適用的,測試過微信5.0安卓版本還有點問題 ...
  • 在最近的移動端佈局當中,最炙手可熱的方式便是使用rem進行元素的佈局。以下便是從最近的文章中所總結出來的一點東西。 首先,我們必須有以下的疑問: rem的本質是什麼? rem如何實現自適應佈局? 如何根據設計稿來調整rem的值? rem佈局是能純CSS還是必須JS進行輔助? 接著,我們來稍微解答或者 ...
  • JavaScript是世界上最流行的腳本語言,因為你在電腦、手機、平板上瀏覽的所有的網頁,以及無數基於HTML5的手機App,交互邏輯都是由JavaScript驅動的。 簡單地說,JavaScript是一種運行在瀏覽器中的解釋型的編程語言。 在Web世界里,只有JavaScript能跨平臺、跨瀏覽器 ...
  • 各種遍歷對象的方法返回值的不同 前置代碼: function Obj() { // 直接在this上添加屬性 this.prop_this = 'prop_this'; // 在this上添加symbol屬性 this[Symbol.for('prop_symbol')] = 'prop_symbo ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...