通過實踐來聊聊利用Node怎麼實現內容壓縮

2022-03-08 22:00:07
利用怎麼實現內容壓縮?下面本篇文章給大家通過實踐來聊聊Node側實現內容壓縮(gzip/br/deflate)的方法,希望對大家有所幫助!

在檢視自己的應用紀錄檔時,發現進入紀錄檔頁面後總是要幾秒鐘才會載入(介面沒做分頁),於是開啟網路面板檢視

1.png

2.png

這才發現介面返回的資料都沒有被壓縮,本以為介面用Nginx反向代理了,Nginx會自動幫我做這一層(這塊後面探究一下,理論上是可行的)

這裡的後端是 服務

本文就分享一下 HTTP資料壓縮相關知識以及在Node側的實踐

前置知識

下面的使用者端均指瀏覽器

accept-encoding

3.png

使用者端在向伺服器端發起請求時,會在請求頭(request header)中新增accept-encoding欄位,其值標明使用者端支援的壓縮內容編碼格式

content-encoding

4.png

伺服器端在對返回內容執行壓縮後,通過在響應頭(response header)中新增content-encoding,來告訴瀏覽器內容實際壓縮使用的編碼演演算法

deflate/gzip/br

deflate是同時使用了LZ77演演算法與哈夫曼編碼(Huffman Coding)的一個無失真資料壓縮演演算法。

gzip 是基於 DEFLATE 的演演算法

br指代Brotli,該資料格式旨在進一步提高壓縮比,對文字的壓縮相對deflate能增加20%的壓縮密度,而其壓縮與解壓縮速度則大致不變

zlib模組

Node.js包含一個zlib 模組,提供了使用 GzipDeflate/Inflate、以及 Brotli 實現的壓縮功能

這裡以gzip為例分場景列舉多種使用方式,Deflate/InflateBrotli使用方式一樣,只是API不一樣

基於stream的操作

5.png

基於buffer的操作

6.png

引入幾個所需的模組

const zlib = require('zlib')
const fs = require('fs')
const stream = require('stream')
const testFile = 'tests/origin.log'
const targetFile = `${testFile}.gz`
const decodeFile = `${testFile}.un.gz`

檔案的解/壓縮

解/壓縮結果檢視,這裡使用du指令直接統計解壓縮前後結果

# 執行
du -ah tests

# 結果如下
108K    tests/origin.log.gz
2.2M    tests/origin.log
2.2M    tests/origin.log.un.gz
4.6M    tests

基於流(stream)的操作

使用createGzipcreateUnzip

  • 注:所有 zlib API,除了那些顯式同步的 API,都使用 Node.js 內部執行緒池,可以看做是非同步的
  • 因此下面的範例中的壓縮和解壓程式碼應分開執行,否則會報錯

方式1: 直接利用範例上的pipe方法傳遞流

// 壓縮
const readStream = fs.createReadStream(testFile)
const writeStream = fs.createWriteStream(targetFile)
readStream.pipe(zlib.createGzip()).pipe(writeStream)

// 解壓
const readStream = fs.createReadStream(targetFile)
const writeStream = fs.createWriteStream(decodeFile)
readStream.pipe(zlib.createUnzip()).pipe(writeStream)

方式2: 利用stream上的pipeline,可在回掉中單獨做其它的處理

// 壓縮
const readStream = fs.createReadStream(testFile)
const writeStream = fs.createWriteStream(targetFile)
stream.pipeline(readStream, zlib.createGzip(), writeStream, err => {
    if (err) {
        console.error(err);
    }
})

// 解壓
const readStream = fs.createReadStream(targetFile)
const writeStream = fs.createWriteStream(decodeFile)
stream.pipeline(readStream, zlib.createUnzip(), writeStream, err => {
    if (err) {
        console.error(err);
    }
})

方式3: Promise化pipeline方法

const { promisify } = require('util')
const pipeline = promisify(stream.pipeline)

// 壓縮
const readStream = fs.createReadStream(testFile)
const writeStream = fs.createWriteStream(targetFile)
pipeline(readStream, zlib.createGzip(), writeStream)
    .catch(err => {
        console.error(err);
    })

// 解壓
const readStream = fs.createReadStream(targetFile)
const writeStream = fs.createWriteStream(decodeFile)
pipeline(readStream, zlib.createUnzip(), writeStream)
    .catch(err => {
        console.error(err);
    })

基於Buffer的操作

利用 gzipunzip API,這兩個方法包含同步非同步型別

  • 壓縮
    • gzip
    • gzipSync
  • 解壓
    • unzip
    • unzipSync

方式1:readStreamBuffer,然後進行進一步操作

  • gzip:非同步
// 壓縮
const buff = []
readStream.on('data', (chunk) => {
    buff.push(chunk)
})
readStream.on('end', () => {
    zlib.gzip(Buffer.concat(buff), targetFile, (err, resBuff) => {
        if(err){
            console.error(err);
            process.exit()
        }
        fs.writeFileSync(targetFile,resBuff)
    })
})
  • gzipSync:同步
// 壓縮
const buff = []
readStream.on('data', (chunk) => {
    buff.push(chunk)
})
readStream.on('end', () => {
    fs.writeFileSync(targetFile,zlib.gzipSync(Buffer.concat(buff)))
})

方式2: 直接通過readFileSync讀取

// 壓縮
const readBuffer = fs.readFileSync(testFile)
const decodeBuffer = zlib.gzipSync(readBuffer)
fs.writeFileSync(targetFile,decodeBuffer)

// 解壓
const readBuffer = fs.readFileSync(targetFile)
const decodeBuffer = zlib.gzipSync(decodeFile)
fs.writeFileSync(targetFile,decodeBuffer)

文字內容的解/壓縮

除了對檔案壓縮,有時候也許要對傳輸的內容進行直接進行解壓縮

這裡以壓縮文字內容為例

// 測試資料
const testData = fs.readFileSync(testFile, { encoding: 'utf-8' })

基於流(stream)操作

這塊就考慮 string => buffer => stream的轉換就行

string => buffer

const buffer = Buffer.from(testData)

buffer => stream

const transformStream = new stream.PassThrough()
transformStream.write(buffer)

// or
const transformStream = new stream.Duplex()
transformStream.push(Buffer.from(testData))
transformStream.push(null)

這裡以寫入到檔案範例,當然也可以寫到其它的流裡,如HTTP的Response(後面會單獨介紹)

transformStream
    .pipe(zlib.createGzip())
    .pipe(fs.createWriteStream(targetFile))

基於Buffer操作

同樣利用Buffer.from將字串轉buffer

const buffer = Buffer.from(testData)

然後直接使用同步API進行轉換,這裡result就是壓縮後的內容

const result = zlib.gzipSync(buffer)

可以寫入檔案,在HTTP Server中也可直接對壓縮後的內容進行返回

fs.writeFileSync(targetFile, result)

Node Server中的實踐

這裡直接使用Node中 http 模組建立一個簡單的 Server 進行演示

在其他的 Node Web 框架中,處理思路類似,當然一般也有現成的外掛,一鍵接入

7.png

const http = require('http')
const { PassThrough, pipeline } = require('stream')
const zlib = require('zlib')

// 測試資料
const testTxt = '測試資料123'.repeat(1000)

const app = http.createServer((req, res) => {
    const { url } = req
    // 讀取支援的壓縮演演算法
    const acceptEncoding = req.headers['accept-encoding'].match(/(br|deflate|gzip)/g)

    // 預設響應的資料型別
    res.setHeader('Content-Type', 'application/json; charset=utf-8')

    // 幾個範例的路由
    const routes = [
        ['/gzip', () => {
            if (acceptEncoding.includes('gzip')) {
                res.setHeader('content-encoding', 'gzip')
                // 使用同步API直接壓縮文字內容
                res.end(zlib.gzipSync(Buffer.from(testTxt)))
                return
            }
            res.end(testTxt)
        }],
        ['/deflate', () => {
            if (acceptEncoding.includes('deflate')) {
                res.setHeader('content-encoding', 'deflate')
                // 基於流的單次操作
                const originStream = new PassThrough()
                originStream.write(Buffer.from(testTxt))
                originStream.pipe(zlib.createDeflate()).pipe(res)
                originStream.end()
                return
            }
            res.end(testTxt)
        }],
        ['/br', () => {
            if (acceptEncoding.includes('br')) {
                res.setHeader('content-encoding', 'br')
                res.setHeader('Content-Type', 'text/html; charset=utf-8')
                // 基於流的多次寫操作
                const originStream = new PassThrough()
                pipeline(originStream, zlib.createBrotliCompress(), res, (err) => {
                    if (err) {
                        console.error(err);
                    }
                })
                originStream.write(Buffer.from('<h1>BrotliCompress</h1>'))
                originStream.write(Buffer.from('<h2>測試資料</h2>'))
                originStream.write(Buffer.from(testTxt))
                originStream.end()
                return
            }
            res.end(testTxt)
        }]
    ]
    const route = routes.find(v => url.startsWith(v[0]))
    if (route) {
        route[1]()
        return
    }

    // 兜底
    res.setHeader('Content-Type', 'text/html; charset=utf-8')
    res.end(`<h1>404: ${url}</h1>
    <h2>已註冊路由</h2>
    <ul>
        ${routes.map(r => `<li><a href="${r[0]}">${r[0]}</a></li>`).join('')}
    </ul>
    `)
    res.end()
})

app.listen(3000)

更多node相關知識,請存取:!

以上就是通過實踐來聊聊利用Node怎麼實現內容壓縮的詳細內容,更多請關注TW511.COM其它相關文章!