使用Node.js搭建靜態資源伺服器

来源:http://www.cnblogs.com/SheilaSun/archive/2017/08/02/7271883.html
-Advertisement-
Play Games

對於Node.js新手,搭建一個靜態資源伺服器是個不錯的鍛煉,從最簡單的返迴文件或錯誤開始,漸進增強,還可以逐步加深對http的理解。那就開始吧,讓我們的雙手沾滿網路請求! Note: 當然在項目中如果有使用express框架,用 "express.static" 一行代碼就可以達到目的了: 這裡我 ...


對於Node.js新手,搭建一個靜態資源伺服器是個不錯的鍛煉,從最簡單的返迴文件或錯誤開始,漸進增強,還可以逐步加深對http的理解。那就開始吧,讓我們的雙手沾滿網路請求!

Note:

當然在項目中如果有使用express框架,用express.static一行代碼就可以達到目的了:

app.use(express.static('public'))

這裡我們要實現的正是express.static背後所做工作的一部分,建議同步閱讀該模塊源碼。

基本功能

不急著寫下第一行代碼,而是先梳理一下就基本功能而言有哪些步驟。

  1. 在本地根據指定埠啟動一個http server,等待著來自客戶端的請求
  2. 當請求抵達時,根據請求的url,以設置的靜態文件目錄為base,映射得到文件位置
  3. 檢查文件是否存在
  4. 如果文件不存在,返回404狀態碼,發送not found頁面到客戶端
  5. 如果文件存在:
    • 打開文件待讀取
    • 設置response header
    • 發送文件到客戶端
  6. 等待來自客戶端的下一個請求

實現基本功能

代碼結構

創建一個nodejs-static-webserver目錄,在目錄內運行npm init初始化一個package.json文件。

mkdir nodejs-static-webserver && cd "$_"
// initialize package.json
npm init

接著創建如下文件目錄:

-- config
---- default.json
-- static-server.js
-- app.js

default.json

{
    "port": 9527,
    "root": "/Users/sheila1227/Public",
    "indexPage": "index.html"
}

default.js存放一些預設配置,比如埠號、靜態文件目錄(root)、預設頁(indexPage)等。當這樣的一個請求http://localhost:9527/myfiles/抵達時. 如果根據root映射後得到的目錄內有index.html,根據我們的預設配置,就會給客戶端發回index.html的內容。

static-server.js

const http = require('http');
const path = require('path');
const config = require('./config/default');

class StaticServer {
    constructor() {
        this.port = config.port;
        this.root = config.root;
        this.indexPage = config.indexPage;
    }

    start() {
        http.createServer((req, res) => {
            const pathName = path.join(this.root, path.normalize(req.url));
            res.writeHead(200);
            res.end(`Requeste path: ${pathName}`);
        }).listen(this.port, err => {
            if (err) {
                console.error(err);
                console.info('Failed to start server');
            } else {
                console.info(`Server started on port ${this.port}`);
            }
        });
    }
}

module.exports = StaticServer;

在這個模塊文件內,我們聲明瞭一個StaticServer類,並給其定義了start方法,在該方法體內,創建了一個server對象,監聽rquest事件,並將伺服器綁定到配置文件指定的埠。在這個階段,我們對於任何請求都暫時不作區分地簡單地返回請求的文件路徑。path模塊用來規範化連接和解析路徑,這樣我們就不用特意來處理操作系統間的差異。

app.js

const StaticServer = require('./static-server');

(new StaticServer()).start();

在這個文件內,調用上面的static-server模塊,並創建一個StaticServer實例,調用其start方法,啟動了一個靜態資源伺服器。這個文件後面將不需要做其他修改,所有對靜態資源伺服器的完善都發生在static-server.js內。

在目錄下啟動程式會看到成功啟動的log:

> node app.js

Server started on port 9527

在瀏覽器中訪問,可以看到伺服器將請求路徑直接返回了。

路由處理

之前我們對任何請求都只是向客戶端返迴文件位置而已,現在我們將其替換成返回真正的文件:

    routeHandler(pathName, req, res) {
        
    }

    start() {
        http.createServer((req, res) => {
            const pathName = path.join(this.root, path.normalize(req.url));
            this.routeHandler(pathName, req, res);
        }).listen(this.port, err => {
            ...
        });
    }

將由routeHandler來處理文件發送。

讀取靜態文件

讀取文件之前,用fs.stat檢測文件是否存在,如果文件不存在,回調函數會接收到錯誤,發送404響應。

    respondNotFound(req, res) {
        res.writeHead(404, {
            'Content-Type': 'text/html'
        });
        res.end(`<h1>Not Found</h1><p>The requested URL ${req.url} was not found on this server.</p>`);
    }

    respondFile(pathName, req, res) {
        const readStream = fs.createReadStream(pathName);
        readStream.pipe(res);
    }

    routeHandler(pathName, req, res) {
        fs.stat(pathName, (err, stat) => {
            if (!err) {
                this.respondFile(pathName, req, res);
            } else {
                this.respondNotFound(req, res);
            }
        });
    }

Note:

讀取文件,這裡用的是流的形式createReadStream而不是readFile,是因為後者會在得到完整文件內容之前將其先讀到記憶體里。這樣萬一文件很大,再遇上多個請求同時訪問,readFile就承受不來了。使用文件可讀流,服務端不用等到數據完全載入到記憶體再發回給客戶端,而是一邊讀一邊發送分塊響應。這時響應里會包含如下響應頭:

Transfer-Encoding:chunked

預設情況下,可讀流結束時,可寫流的end()方法會被調用。

MIME支持

現在給客戶端返迴文件時,我們並沒有指定Content-Type頭,雖然你可能發現訪問文本或圖片瀏覽器都可以正確顯示出文字或圖片,但這並不符合規範。任何包含實體主體(entity body)的響應都應在頭部指明文件類型,否則瀏覽器無從得知類型時,就會自行猜測(從文件內容以及url中尋找可能的擴展名)。響應如指定了錯誤的類型也會導致內容的錯亂顯示,如明明返回的是一張jpeg圖片,卻錯誤指定了header:'Content-Type': 'text/html',會收到一堆亂碼。

雖然有現成的mime模塊可用,這裡還是自己來實現吧,試圖對這個過程有更清晰的理解。

在根目錄下創建mime.js文件:

const path = require('path');

const mimeTypes = {
    "css": "text/css",
    "gif": "image/gif",
    "html": "text/html",
    "ico": "image/x-icon",
    "jpeg": "image/jpeg",
     ...
};

const lookup = (pathName) => {
    let ext = path.extname(pathName);
    ext = ext.split('.').pop();
    return mimeTypes[ext] || mimeTypes['txt'];
}

module.exports = {
    lookup
};

該模塊暴露出一個lookup方法,可以根據路徑名返回正確的類型,類型以‘type/subtype’表示。對於未知的類型,按普通文本處理。

接著在static-server.js中引入上面的mime模塊,給返迴文件的響應都加上正確的頭部欄位:

    respondFile(pathName, req, res) {
        const readStream = fs.createReadStream(pathName);
        res.setHeader('Content-Type', mime.lookup(pathName));
        readStream.pipe(res);
    }

重新運行程式,會看到圖片可以在瀏覽器中正常顯示了。

Note:

需要註意的是,Content-Type說明的應是原始實體主體的文件類型。即使實體經過內容編碼(如gzip,後面會提到),該欄位說明的仍應是編碼前的實體主體的類型。

添加其他功能

至此,已經完成了基本功能中列出的幾個步驟,但依然有很多需要改進的地方,比如如果用戶輸入的url對應的是磁碟上的一個目錄怎麼辦?還有,現在對於同一個文件(從未更改過)的多次請求,服務端都是勤勤懇懇地一遍遍地發送回同樣的文件,這些冗餘的數據傳輸,既消耗了帶寬,也給伺服器添加了負擔。另外,伺服器如果在發送內容之前能對其進行壓縮,也有助於減少傳輸時間。

讀取文件目錄

現階段,用url: localhost:9527/testfolder去訪問一個指定root文件夾下真實存在的testfolder的文件夾,服務端會報錯:

Error: EISDIR: illegal operation on a directory, read

要增添對目錄訪問的支持,我們重新整理下響應的步驟:

  1. 請求抵達時,首先判斷url是否有尾部斜杠
  2. 如果有尾部斜杠,認為用戶請求的是目錄
    • 如果目錄存在
      • 如果目錄下存在預設頁(如index.html),發送預設頁
      • 如果不存在預設頁,發送目錄下內容列表
    • 如果目錄不存在,返回404
  3. 如果沒有尾部斜杠,認為用戶請求的是文件
    • 如果文件存在,發送文件
    • 如果文件不存在,判斷同名的目錄是否存在
      • 如果存在該目錄,返回301,併在原url上添加上/作為要轉到的location
      • 如果不存在該目錄,返回404

我們需要重寫一下routeHandler內的邏輯:

    routeHandler(pathName, req, res) {
        fs.stat(pathName, (err, stat) => {
            if (!err) {
                const requestedPath = url.parse(req.url).pathname;
                if (hasTrailingSlash(requestedPath) && stat.isDirectory()) {
                    this.respondDirectory(pathName, req, res);
                } else if (stat.isDirectory()) {
                    this.respondRedirect(req, res);
                } else {
                    this.respondFile(pathName, req, res);
                }
            } else {
                this.respondNotFound(req, res);
            }
        });
    }

繼續補充respondRedirect方法:

    respondRedirect(req, res) {
        const location = req.url + '/';
        res.writeHead(301, {
            'Location': location,
            'Content-Type': 'text/html'
        });
        res.end(`Redirecting to <a href='${location}'>${location}</a>`);
    }

瀏覽器收到301響應時,會根據頭部指定的location欄位值,向伺服器發出一個新的請求。

繼續補充respondDirectory方法:

    respondDirectory(pathName, req, res) {
        const indexPagePath = path.join(pathName, this.indexPage);
        if (fs.existsSync(indexPagePath)) {
            this.respondFile(indexPagePath, req, res);
        } else {
            fs.readdir(pathName, (err, files) => {
                if (err) {
                    res.writeHead(500);
                    return res.end(err);
                }
                const requestPath = url.parse(req.url).pathname;
                let content = `<h1>Index of ${requestPath}</h1>`;
                files.forEach(file => {
                    let itemLink = path.join(requestPath,file);
                    const stat = fs.statSync(path.join(pathName, file));
                    if (stat && stat.isDirectory()) {
                        itemLink = path.join(itemLink, '/');
                    }                 
                    content += `<p><a href='${itemLink}'>${file}</a></p>`;
                });
                res.writeHead(200, {
                    'Content-Type': 'text/html'
                });
                res.end(content);
            });
        }
    }

當需要返回目錄列表時,遍歷所有內容,併為每項創建一個link,作為返迴文檔的一部分。需要註意的是,對於子目錄的href,額外添加一個尾部斜杠,這樣可以避免訪問子目錄時的又一次重定向。

在瀏覽器中測試一下,輸入localhost:9527/testfolder,指定的root目錄下並沒有名為testfolder的文件,卻存在同名目錄,因此第一次會收到重定向響應,併發起一個對目錄的新請求。

緩存支持

為了減少數據傳輸,減少請求數,繼續添加緩存支持。首先梳理一下緩存的處理流程:

  1. 如果是第一次訪問,請求報文首部不會包含相關欄位,服務端在發送文件前做如下處理:
    • 如伺服器支持ETag,設置ETag
    • 如伺服器支持Last-Modified,設置Last-Modified
    • 設置Expires
    • 設置Cache-Control頭(設置其max-age值)

    瀏覽器收到響應後會存下這些標記,併在下次請求時帶上與ETag對應的請求首部If-None-Match或與Last-Modified對應的請求首部If-Modified-Since

  2. 如果是重覆的請求:
    • 瀏覽器判斷緩存是否過期(通過Cache-ControlExpires確定)
      • 如果未過期,直接使用緩存內容,也就是強緩存命中,並不會產生新的請求
      • 如果已過期,會發起新的請求,並且請求會帶上If-None-MatchIf-Modified-Since,或者兼具兩者
      • 伺服器收到請求,進行緩存的新鮮度再驗證:
        • 首先檢查請求是否有If-None-Match首部,沒有則繼續下一步,有則將其值與文檔的最新ETag匹配,失敗則認為緩存不新鮮,成功則繼續下一步
        • 接著檢查請求是否有If-Modified-Since首部,沒有則保留上一步驗證結果,有則將其值與文檔最新修改時間比較驗證,失敗則認為緩存不新鮮,成功則認為緩存新鮮

        當兩個首部皆不存在或者驗證結果是不新鮮時,發送200及最新文件,併在首部更新新鮮度。

        當驗證結果是緩存仍然新鮮時(也就是弱緩存命中),不需發送文件,僅發送304,併在首部更新新鮮度

為了能啟用或關閉某種驗證機制,我們在配置文件里增添如下配置項:

default.json

{
    ...
    "cacheControl": true,
    "expires": true,
    "etag": true,
    "lastModified": true,
    "maxAge": 5
}

這裡為了能測試到緩存過期,將過期時間設成了非常小的5秒。

StaticServer類中接收這些配置:

class StaticServer {
    constructor() {
         ...
        this.enableCacheControl = config.cacheControl;
        this.enableExpires = config.expires;
        this.enableETag = config.etag;
        this.enableLastModified = config.lastModified;
        this.maxAge = config.maxAge;
    }

現在,我們要在原來的respondFile前橫加一杠,增加是要返回304還是200的邏輯。

    respond(pathName, req, res) {
        fs.stat(pathName, (err, stat) => {
            if (err) return respondError(err, res);
            this.setFreshHeaders(stat, res);
            if (this.isFresh(req.headers, res._headers)) {
                this.responseNotModified(res);
            } else {
                this.responseFile(pathName, res);
            }
        });

    }

準備返迴文件前,根據配置,添加緩存相關的響應首部。

    generateETag(stat) {
        const mtime = stat.mtime.getTime().toString(16);
        const size = stat.size.toString(16);
        return `W/"${size}-${mtime}"`;
    }

    setFreshHeaders(stat, res) {
        const lastModified = stat.mtime.toUTCString();
        if (this.enableExpires) {
            const expireTime = (new Date(Date.now() + this.maxAge * 1000)).toUTCString();
            res.setHeader('Expires', expireTime);
        }
        if (this.enableCacheControl) {
            res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`);
        }
        if (this.enableLastModified) {
            res.setHeader('Last-Modified', lastModified);
        }
        if (this.enableETag) {
            res.setHeader('ETag', this.generateETag(stat));
        }
    }

需要註意的是,上面使用了ETag弱驗證器,並不能保證緩存文件與伺服器上的文件是完全一樣的。關於強驗證器如何實現,可以參考etag包的源碼。

下麵是如何判斷緩存是否仍然新鮮:

    isFresh(reqHeaders, resHeaders) {
        const  noneMatch = reqHeaders['if-none-match'];
        const  lastModified = reqHeaders['if-modified-since'];
        if (!(noneMatch || lastModified)) return false;
        if(noneMatch && (noneMatch !== resHeaders['etag'])) return false;
        if(lastModified && lastModified !== resHeaders['last-modified']) return false;
        return true;
    }

需要註意的是,http首部欄位名是不區分大小寫的(但http method應該大寫),所以平常在瀏覽器中會看到大寫或小寫的首部欄位。

但是nodehttp模塊將首部欄位都轉成了小寫,這樣在代碼中使用起來更方便些。所以訪問header要用小寫,如reqHeaders['if-none-match']。不過,仍然可以用req.rawreq.rawHeaders來訪問原headers,它是一個[name1, value1, name2, value2, ...]形式的數組。

現在來測試一下,因為設置的緩存有效時間是極小的5s,所以強緩存幾乎不會命中,所以第二次訪問文件會發出新的請求,因為服務端文件並沒做什麼改變,所以會返回304。

304

現在來修改一下請求的這張圖片,比如修改一下size,目的是讓服務端的再驗證失敗,因而必須給客戶端發送200和最新的文件。

200

接下來把緩存有效時間改大一些,比如10分鐘,那麼在10分鐘之內的重覆請求,都會命中強緩存,瀏覽器不會向服務端發起新的請求(但network依然能觀察到這條請求)。

cache

內容編碼

伺服器在發送很大的文檔之前,對其進行壓縮,可以節省傳輸用時。其過程是:

  1. 瀏覽器在訪問網站時,預設會攜帶Accept-Encoding
  2. 伺服器在收到請求後,如果發現存在Accept-Encoding請求頭,並且支持該文件類型的壓縮,壓縮響應的實體主體(並不壓縮頭部),並附上Content-Encoding首部
  3. 瀏覽器收到響應,如果發現有Content-Encoding首部,按其值指定的格式解壓報文

對於圖片這類已經經過高度壓縮的文件,無需再額外壓縮。因此,我們需要配置一個欄位,指明需要針對哪些類型的文件進行壓縮。

default.json

{
    ...
    "zipMatch": "^\\.(css|js|html)$"
}

static-server.js

    constructor() {
        ...
        this.zipMatch = new RegExp(config.zipMatch);
    }

zlib模塊來實現流壓縮:

compressHandler(readStream, req, res) { const acceptEncoding = req.headers['accept-encoding']; if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) { return readStream; } else if (acceptEncoding.match(/\bgzip\b/)) { res.setHeader('Content-Encoding', 'gzip'); return readStream.pipe(zlib.createGzip()); } else if (acceptEncoding.match(/\bdeflate\b/)) { res.setHeader('Content-Encoding', 'deflate'); return readStream.pipe(zlib.createDeflate()); } }

因為配置了圖片不需壓縮,在瀏覽器中測試會發現圖片請求的響應中沒有Content-Encoding頭。

範圍請求

最後一步,使伺服器支持範圍請求,允許客戶端只請求文檔的一部分。其流程是:

  1. 客戶端向服務端發起請求
  2. 服務端響應,附上Accept-Ranges頭(值表示表示範圍的單位,通常是“bytes”),告訴客戶端其接受範圍請求
  3. 客戶端發送新的請求,附上Ranges頭,告訴服務端請求的是一個範圍
  4. 服務端收到範圍請求,分情況響應:
    • 範圍有效,服務端返回206 Partial Content,發送指定範圍內內容,併在Content-Range頭中指定該範圍
    • 範圍無效,服務端返回416 Requested Range Not Satisfiable,併在Content-Range中指明可接受範圍

請求中的Ranges頭格式為(這裡不考慮多範圍請求了):

Ranges: bytes=[start]-[end]

其中 start 和 end 並不是必須同時具有:

  1. 如果 end 省略,伺服器應返回從 start 位置開始之後的所有位元組
  2. 如果 start 省略,end 值指的就是伺服器該返回最後多少個位元組
  3. 如果均未省略,則伺服器返回 start 和 end 之間的位元組

響應中的Content-Range頭有兩種格式:

  1. 當範圍有效返回 206 時:

    Content-Range: bytes (start)-(end)/(total)
  2. 當範圍無效返回 416 時:

    Content-Range: bytes */(total)

添加函數處理範圍請求:

    rangeHandler(pathName, rangeText, totalSize, res) {
        const range = this.getRange(rangeText, totalSize);
        if (range.start > totalSize || range.end > totalSize || range.start > range.end) {
            res.statusCode = 416;
            res.setHeader('Content-Range', `bytes */${totalSize}`);
            res.end();
            return null;
        } else {
            res.statusCode = 206;
            res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${totalSize}`);
            return fs.createReadStream(pathName, { start: range.start, end: range.end });
        }
    }

Postman來測試一下。在指定的root文件夾下創建一個測試文件:

testfile.js

This is a test sentence.

請求返回前六個位元組 ”This “ 返回 206:

206

請求一個無效範圍返回416:

416

讀取命令行參數

至此,已經完成了靜態伺服器的基本功能。但是每一次需要修改配置,都必須修改default.json文件,非常不方便,如果能接受命令行參數就好了,可以藉助 yargs 模塊來完成。

var options = require( "yargs" )
    .option( "p", { alias: "port",  describe: "Port number", type: "number" } )
    .option( "r", { alias: "root", describe: "Static resource directory", type: "string" } )
    .option( "i", { alias: "index", describe: "Default page", type: "string" } )
    .option( "c", { alias: "cachecontrol", default: true, describe: "Use Cache-Control", type: "boolean" } )
    .option( "e", { alias: "expires", default: true, describe: "Use Expires", type: "boolean" } )
    .option( "t", { alias: "etag", default: true, describe: "Use ETag", type: "boolean" } )
    .option( "l", { alias: "lastmodified", default: true, describe: "Use Last-Modified", type: "boolean" } )
    .option( "m", { alias: "maxage", describe: "Time a file should be cached for", type: "number" } )
    .help()
    .alias( "?", "help" )
    .argv;

瞅瞅 help 命令會輸出啥:

help command

這樣就可以在命令行傳遞埠、預設頁等:

node app.js -p 8888 -i main.html

參考

  1. 使用Node.js搭建簡易Http伺服器
  2. 博文共賞:Node.js靜態文件伺服器實戰
  3. HTTP 206 Partial Content In Node.js

源碼

戳我的 GitHub repo: nodejs-static-webserver

博文也同步在 GitHub,歡迎討論和指正:使用Node.js搭建靜態資源伺服器


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

-Advertisement-
Play Games
更多相關文章
  • Location 對象屬性 屬性 | 描述 | hash| 設置或返回從井號 ( ) 開始的 URL(錨)。 host | 設置或返回主機名和當前 URL 的埠號。 hostname | 設置或返回當前 URL 的主機名。 href | 設置或返回完整的 URL。 pathname | 設置或返回 ...
  • 寫在前面 "AlloyCrop" 這個項目是8個月前發佈的,作為 "AlloyFinger" 的典型案例,發佈之後被BAT等其他公司廣泛使用。但是發佈之後,有兩個問題一直沒有抽出時間去解決: 裁剪圖像的解析度太小,是否可配? pinch雙指放大的時候,放大的中心不是雙指中心,是否可以優化? 現在很高 ...
  • 說這幾個屬性前 我說一下我的設備 我的設備有兩個,一個高度為1080的顯示器,一個高度為800的電腦 第一種:window.screen.height 這個方法是獲取用戶電腦屏幕的高度,是不關瀏覽器或者頂部工具欄跟底部工具欄的高度的 當我在高度為1080的我的顯示屏屏幕上列印 當我在我的電腦上列印 ...
  • 移動端橫向商品瀏覽、橫向滑動廣告位(CSS實現) 在手機上逛一些電商網站或者其他相同類型的網站時,會遇到橫向滑動的商品。如京東、淘寶等電商網站下。我們知道,這一般情況下為某個元素設置 做到,表示橫向或者豎向依據內容的多少自動出現滾動條。但是,在頁面中間出現橫向滾動條是十分不好看的.. 在不使用任何插 ...
  • 具體代碼如下: ...
  • 這是一道非常好的面試題, 考察以下知識點: 1,this的指向 2,原型(prototype)以及原型鏈 3,繼承 4,引用 要解出這道題,要理解以下幾句話就可以了: 1,每一個構造函數,都有一個原型[[prototype]]屬性 指向構造函數的原型對象 2,每一個實例生成的時候,都會在記憶體中產生一 ...
  • 前言 Express 是基於 Node.js 平臺的 web 應用開發框架,在學習了 Node.js 的基礎知識後,可以使用 Express 框架來搭建一個 web 應用,實現對資料庫的增刪查改。 資料庫選擇 MongoDB,它是一個基於分散式文件存儲的開源資料庫系統,Mongoose 是 Mong ...
  • 一直想寫這篇“十日談”,聊聊我對Web前端開發的體會,順便解答下周圍不少人的困惑和迷惘。我不打算聊太多技術,我想,通過技術的歷練,得到的反思應當更重要。 我一直認為自己是“初級”前端開發工程師,一方面我入道尚淺,只有短短幾年,另一方面我自知對技術的鑽研並不深入,可能是由於環境的原因,當然最重要的是, ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...