.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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...