整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          nodejs篇html批量轉pdf

          文概述:如何使用 nodejs 在服務端將 html 批量轉成 pdf 并客戶端下載。

          目標一:使用 node 在服務端實現 html 批量轉成 pdf


          分為兩步:

          1. 找段 html 把它轉成 pdf。
          2. 循環執行第一步就達到了批量操作目的。
          • 準備html 片段:<div>111</div>
          • 將html 片段轉 pdf 。

          自己轉(存疑)?或者找個中間件。自己轉的話。。。算了,找插件。

          網上搜到了 html-pdf,看了下周下載量接近 7W,版本迭代 21 個,最近更新時間 latest,不錯,整體滿足心里預期。

          api使用如圖:

          提供了三種接口:文件、stream 流、buffer。不同返回值類型,使用 toFile()需要 2 個參數:文件地址(末尾要有文件名)、回調函數。

          toFile()結果會在指定文件夾生成 pdf 文件;

          toStream()、toBuffer()可以在回調里拿到pdf文件數據流

          目標二:將 PDF 傳到客戶端


          怎么傳輸?

          經過分析歸納,可以濃縮為 3 個點:

          1. 傳輸者:誰是傳輸者?它肯定是個主動行為,就像點擊事件你必須先有一個點擊動作一樣。由此聯想:接口,回調,node,req,res,send,end,前端 success 接收 data,res 浮現而出。
          2. 傳輸方法:原生 res.end(),express 框架 res.send(),res.sendFile(),res.download(),fs 模塊 res.createWriteStream(),管道 pipe()。
          3. 傳輸內容:PDF 文件,stream 流,buffer,二進制流,下載鏈接,base64 碼。

          以下分析可以略過直接到結果:

          分析:測試用的是 express 框架,那么可以排除原生 res.end()(存疑),為啥排除它?有 send 我干嘛還用 end;將 pdf 傳向客戶端又不是下載或寫入文件,排除 createWriteStream()方法;PDF 文件可以自己跑過去嗎?肯定不行!如果啥都不動能不能成功下載文件?也可以。鏈接下載,好像挺萬能。base64 能用嗎?。。。好像是圖片在用!不知道對文件好使不好使(存疑)。

          篩選后:

          傳輸方法:res.send()、管道 pipe();

          傳輸內容:stream 流、buffer、下載鏈接;

          使用下載鏈接:

          經過分析了,下載鏈接肯定能實現這個功能!為什么呢?只要把 pdf 的文件路徑用變量存下來,然后返回給前端,拿著絕對路徑地址模擬點擊下載應該就能實現!但這種方法并沒有在真正意義上跟要傳輸的內容打交道,所以還值得繼續探索。


          最終決定使用,管道pipe()發送stream流輸出PDF文件流到客戶端

          目標三:客戶端可以成功接收并自動下載


          網上搜索有:Windows.open()方法,iframe 等。感覺都有點偏。

          本文使用a標簽實現,其他方法另行嘗試。

          ajax + a標簽

          $.ajax({
              url:'url',
              responseType:'blob',
              success (res) {
                  console.log(res.toString('utf-8'));
                  // 創建 blob 對象,解析流數據
              // , {
                   // 如何后端沒返回下載文件類型,則需要手動設置:type: 'application/pdf;chartset=UTF-8' 表示下載文檔為 pdf,如果是 word 則設置為 msword,excel 為 excel
              //         type: 'application/pdf;chartset=UTF-8'
              //     }
                  const blob = new Blob([res]);
                  const a = document.createElement('a')
              //     // 兼容 webkix 瀏覽器,處理 webkit 瀏覽器中 href 自動添加 blob 前綴,默認在瀏覽器打開而不是下載
                  const URL = window.URL || window.webkitURL
              //     // 根據解析后的 blob 對象創建 URL 對象
                  const herf = URL.createObjectURL(blob)
                  // 下載鏈接
                  // a.href = 'url'
                  a.href = herf
                  // // 下載文件名,如果后端沒有返回,可以自己寫 a.download = '文件.pdf'
                  a.download = '文件.pdf'
                  // document.body.appendChild(a)
                  a.click()
                  // document.body.removeChild(a)
                  // 在內存中移除 URL 對象
                  window.URL.revokeObjectURL(herf)
              }
          })

          傳統jQuery接收數據流后,需要使用new Blob([res])再處理才能繼續使用,

          而,fetch也需要,不過簡便多了

          fetch+ a標簽

          //1.請求
          let res = await fetch([地址]);
          //2.解析
          let data = res.blob()
          //3.創建a標簽
          let eleA = document.createElement('a')
          //4.創建鼠標事件對象
          let e = document.createEvent("MouseEvents")
          //5.初始化事件對象
          e.initEvent("click", false, false)
          ?
          ?
          document.body.appendChild(eleA)
          eleA.download = 'index.zip'
          eleA.href = data
          //給指定的元素,執行事件 click 事件
          eleA.dispatchEvent(e)
          document.body.removeChild(eleA)

          報錯:

          eleA.href直接使用 blob 數據作為下載鏈接是不行的,必須使用 window.URL 對象,果然無知者無畏,更改后的代碼如下:

          //1.請求
          let res=await fetch([地址]);
          //2.解析
          let data=await res.blob();
          const a = document.createElement('a')
          const URL = window.URL || window.webkitURL
          const herf = URL.createObjectURL(data)
          a.href = herf
          a.download = '文件.zip'
          a.click()
          window.URL.revokeObjectURL(herf)

          使用 oFile() 測試

          server.get('/', async (req, res) => {
              toPdf(res);
          });
          ?
          ?
          function toPdf (res) {
              let archive = archiver('zip', {
              zlib: { level: 9 } // 設置壓縮級別
              });
              archive.on('error', function(err){
                  throw err;
              });
              archive.pipe(res);
              for (let i = 0; i < 100; i++) {
                  let html = `<div style="width: 100px;height:100px;background:#fff;color:red;font-size:16px;font-weight:bold">這是第 <span>${i}</span> 個 pdf</div>`;
                  pdf.create(html, options).toFile(`./static/${i}.pdf`, () => {
                       archive.glob('static/*')
                       archive.finalize()
                  })
              }
          }

          看命令窗口偶爾還會報 Queue Closed 錯誤;

          觀察發現程序一邊轉 pdf,一邊下載,而且是按照順序轉換下載有 0.1.2.3...,最后壓縮返回,這個過程循環很少時發現沒問題,但次數增加很多后如20,100次,當for循環到最后一次時,直接執行archiver.finalize(),完。。。結束了,所以造成Queue Closed原因是沒限制archiver.finalize()執行時機???

          找證據:

          官方文檔顯示:

          // finalize the archive (ie we are done appending files but streams have to finish yet)

          // 完成存檔

          // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand

          // 調用此方法后,可能會立即觸發“ close”,“ end”或“ finish”,因此請事先注冊

          archive.finalize();

          果然,,

          添加結束條件:

          pdf.create(html, options).toFile(`./static/${i}.pdf`, () => {
               if (i === 99) {
                 archive.glob('static/*')
                 archive.finalize()
               }
          })

          執行成功,Queue Closed沒出現,并且在瀏覽器自動下載了一個壓縮包,打開后

          發現只有 91 個文件,缺少了最后幾個文件

          for 循環明明執行了 100 次。百思不得解,開始我以為跟網絡有關系,畢竟有傳言只要(網絡)夠快什么(隊列關閉)錯誤都追不上你。后來我發現用筆記本執行程序這個錯誤會頻繁出現,

          查看了 html-pdf 源碼猜測會不會是同步在阻塞,導致循環結束后 PDF 生成文件還未完成導致 Queue Closed,怎么解決呢,閉包!具體原理未知(存疑),

          經過修改

          ((html, i) => {
            pdf.create(html).toFile(`./static/${i}.pdf`, () => {
                  if (i === 49) {
                      archive.glob('static/*')
                      archive.finalize();
                  }
              })
          })(html, i)

          成功!!!打開壓縮文件查看文件總數,

          一切正常,Good!

          使用 toStream()測試

          (function(html, i) {
              pdf.create(html).toStream((err, stream) => {
                  archive.append(stream, { name: `${i}.pdf`});
                  if (i === 99) {
                      archive.finalize();
                  }
              });
          })(html, i)

          我去,第一次跑果然有問題,反復執行多次總是缺少前幾個文件

          第一次跑,少了第一個文件 1.pdf:

          第二次,循環執行 50 次,結果只有 45 個成功,前 5 個失敗:

          命令行打印也證明了這一點。。。每次失敗個數竟然還不同,但還好有個規律它們都是前幾個

          。。。

          繼續執行,竟然還有其他報錯類型。。。無語。。

          大致意思是:輸入資源必須是 Stream 流或者 Buffer,可能是在使用 archiver.append 時塞入了 undefined 之類的,給它上個保險,如果值存在才執行

          stream && archive.append(stream, { name: `${i}.pdf`});

          綜合問題共有 2 個:1.并不是所有 html 片段都進行了轉換 PDF 的操作,可能會隨機出現遺漏,比如 45.46.突然就到 48;2.即使所有 html 都進行了轉換操作,還總是缺少前面幾個文件。

          問題一解決:

          html-pdf 源碼如下圖

          研究發現 html-pdf 的 toStream 應該是一個異步方法,查看源碼后 stream.on('end')也證明了這一點。由于不是順序生成 stream 流,那么最后一個流生成并不代表所有都完成,所以當用 i===99 判斷結束就有問題,可能會跳過某一個不執行轉換PDF需要len=50來避免,然而添加過后,每個文件都進行了PDF轉換,但是stream顯示為undefined,進而PDF文件也總是少前面幾個,導致 i=99 出現時還有好多 toStream 沒有完成,SO 第一個問題就出現了;(注:不過異步操作也因此避免同步帶來方法一的問題:隊列關閉錯誤。)

          原因找到,解決辦法就是添加變量手動強行控制進程:

          如圖,len 初始 100,減為 0 時代表所有都已經轉換,可以結束

          let len = 100;
          然后,開始計數,不到最后一個完成 stream 流轉換不結束
          pdf.create(html).toStream((err, stream) => {
              len--;
              stream && archive.append(stream, { name: `${i}.pdf`});
              if (len === 0) {
                  archive.finalize();
              }
          });

          命令行打印下錯誤信息看看怎么回事控制臺

          問題二解決:

          先打印下錯誤信息 console.log('err:', err)


          暴露了,PDF generation timeout. 一個 timeout 已經夠了,能夠說明很多問題,html-pdf 提供了轉換超時限制,時間超出 timeout 自然無法成功轉換成 stream 流輸出,解決辦法更簡單:

          官方文檔,給出了一個配置項 options:{}對象,其中就有 timeout 設置,我們可以視情況放大此參數,

          // 一分鐘內轉 pdf 不成功,則視為失敗
          let options = {
              timeout: 60000
          }
          pdf.create(html, options).toStream((err, stream) => {
              。。。。。。
          });

          繼續測試,終于完成。。。

          結語:

          1.本文走了許多彎路,踩了多多個坑;

          2.文中標記存疑的地方依然有很多,都是等待去學習理解的地方;

          3.對于名詞、問題的解釋描述不夠精準透徹,需要深度挖掘對知識點的認知;

          4.對問題的解決方式不夠標準、熟練,這才是造成多走彎路的原因;

          5.雖然解決問題才是我們的最終目的,但是仍需追求解決方式的多樣化,找到問題的根源 格物致知 才能給自己醍醐灌頂之感;

          文轉載自微信公眾號「全棧修仙之路」,作者阿寶哥。轉載本文請聯系全棧修仙之路公眾號。

          在日常工作中,文件上傳是一個很常見的功能。在上傳文件時,我們可以選擇上傳單個文件,也可以通過設置 multiple 屬性來上傳多個文件。

          本文阿寶哥將介紹如何上傳目錄及如何壓縮目錄并上傳,壓縮目錄的功能是通過 JSZip 這個庫來實現。利用這個庫還可以實現在線預覽 ZIP 文件的功能,感興趣的小伙伴可以閱讀 JavaScript 如何在線解壓 ZIP 文件? 這篇文章。下面我們先來介紹如何實現壓縮目錄并上傳的功能。

          一、瀏覽器端

          1.1 選擇目錄

          在瀏覽器端,要實現壓縮目錄并上傳的功能。首先我們要先實現選擇目錄的功能,要實現該功能,我們可以直接使用 HTMLInputElement 元素的 webkitdirectory 屬性:

          <input type="file" id="uploadFile" webkitdirectory /> 

          當設置了 webkitdirectory 屬性之后,我們就可以選擇目錄了。當阿寶哥選擇了 useAxios 目錄之后,就會顯示以下確認框:

          點擊上傳按鈕之后,我們就可以獲取文件列表。列表中的文件對象上含有一個 webkitRelativePath 屬性,用于表示當前文件的相對路徑。在進行目錄壓縮的時候,我們就會使用到該屬性。

          雖然通過 webkitdirectory 屬性可以很容易地實現選擇目錄的功能,但在實際項目中我們還需要考慮它的兼容性。比如在 IE 11 以下的版本就不支持該屬性,其它瀏覽器的兼容性如下圖所示:

          (圖片來源 —— https://caniuse.com/?search=webkitdirectory)

          1.2 壓縮目錄

          在 JavaScript 如何在線解壓 ZIP 文件? 這篇文章中,阿寶哥介紹了在瀏覽器端如何使用 JSZip 這個庫實現在線解壓 ZIP 文件的功能。JSZip 這個庫除了可以解析 ZIP 文件之外,它還可以用來創建和編輯 ZIP 文件。這里阿寶哥基于 JSZip 庫提供的 API,封裝了一個 generateZipFile 函數:

          function generateZipFile( 
            zipName, files, 
            options = { type: "blob", compression: "DEFLATE" } 
          ) { 
            return new Promise((resolve, reject) => { 
              const zip = new JSZip(); 
              for (let i = 0; i < files.length; i++) { // 添加目錄中包含的文件 
                zip.file(files[i].webkitRelativePath, files[i]); 
              } 
              zip.generateAsync(options).then(function (blob) { // 生成zip文件 
                zipName = zipName || Date.now() + ".zip"; 
                const zipFile = new File([blob], zipName, { 
                  type: "application/zip", 
                }); 
                resolve(zipFile); 
              }); 
            }); 
          } 

          在以上代碼中,我們使用 file(name, data [,options]) 方法,把目錄中的文件依次添加到 zip 對象中,然后再通過 generateAsync 方法來生成 ZIP 文件。在生成 ZIP 文件時,我們可以設置該文件的類型。這里我們設置的默認類型為 blob 類型,除了支持 blob 類型之外,它還支持 base64、uint8array 和 arraybuffer 等類型。

          1.3 上傳壓縮 ZIP 文件

          在壓縮目錄生成 ZIP 文件之后,我們就可以通過 XMLHttpRequest 或 fetch API 來上傳壓縮文件。下面阿寶哥將以 axios 為例,來實現文件上傳的功能。

          html 代碼

          <input type="file" id="uploadFile" webkitdirectory /> 
          <button id="submit" onclick="uploadFile()">上傳文件</button> 

          js 代碼

          const uploadFileEle = document.querySelector("#uploadFile"); 
          const uploadOptions = { needZip = true }; 
           
          const request = axios.create({ 
            baseURL: "http://localhost:3000/", 
            timeout: 5000, 
          }); 
           
          async function uploadFile({ needZip } = uploadOptions) { 
            if (!uploadFileEle.files.length) return; 
            let fileList = uploadFileEle.files; 
            if (needZip) { // 對目錄進行ZIP壓縮 
              let webkitRelativePath = fileList[0].webkitRelativePath; 
              let zipFileName = webkitRelativePath.split("/")[0] + ".zip"; 
              fileList = [await generateZipFile(zipFileName, fileList)]; 
            } 
            uploadFiles({ // 上傳文件列表 
              url: "/upload/multiple", 
              files: fileList, 
            }); 
          } 

          在 uploadFile 函數中,如果有啟用目錄壓縮功能,我們就會調用 generateZipFile 函數生成 ZIP 文件,如果沒有的話,就會直接調用 uploadFiles 函數來上傳目錄中的所有文件,當然你也可以對文件列表進行過濾,比如限制文件類型或文件的大小等。

          下面我們來看一下 uploadFiles 函數的具體實現:

          function uploadFiles({ url, files, fieldName = "file" }) { 
            if (!url || !files.length) return; 
            let formData = new FormData(); 
            for (let i = 0; i < files.length; i++) { 
              formData.append(fieldName, files[i], files[i].name); 
            } 
            return request.post(url, formData); 
          } 

          在 uploadFiles 函數中,我們通過創建 FormData 對象來保存文件的信息,然后通過 request(axios 實例)來執行上傳操作。

          二、服務器端

          2.1 接收 ZIP 文件

          在服務端要實現文件上傳功能也比較簡單,這里阿寶哥以 koa 為例來實現文件上傳的功能。如果你對 koa 還不了解的話,建議你先大致瀏覽一下 koa 的官方文檔。

          const path = require("path"); 
          const Koa = require("koa"); 
          const cors = require("@koa/cors"); 
          const multer = require("@koa/multer"); 
          const Router = require("@koa/router"); 
           
          const app = new Koa(); 
          const router = new Router(); 
          const UPLOAD_DIR = path.join(__dirname, "/public/upload"); 
           
          const storage = multer.diskStorage({ 
            destination: async function (req, file, cb) { // 設置文件的存儲目錄 
              cb(null, UPLOAD_DIR); 
            }, 
            filename: function (req, file, cb) { // 設置文件名 
              cb(null, `${file.originalname}`); 
            }, 
          }); 
           
          const multerUpload = multer({ storage }); 
           
          router.get("/", async (ctx) => { 
            ctx.body = "壓縮文件目錄上傳示例(阿寶哥)"; 
          }); 
           
          router.post( 
            "/upload/multiple", 
            multerUpload.fields([ 
              { 
                name: "file", 
              }, 
            ]), 
            async (ctx, next) => { 
              ctx.body = { 
                status: "success", 
                msg: "文件上傳成功", 
              }; 
            } 
          ); 
           
          // 注冊中間件 
          app.use(cors()); 
          app.use(router.routes()).use(router.allowedMethods()); 
           
          app.listen(3000, () => { 
            console.log("app starting at port 3000"); 
          }); 

          在以上代碼中,我們通過 @koa/multer 這個中間件來處理文件上傳,對該中間件感興趣的小伙伴,可以自行閱讀官方文檔。接下來,我們來繼續討論另一個問題 —— 如何接收目錄并按照文件目錄結構進行存放?

          2.2 接收文件目錄

          前面我們已經知道,當 input[type="file"] 使用了 webkitdirectory 屬性之后,返回 File 對象的 webkitRelativePath 屬性就會存放當前文件相對于當前目錄的相對路徑:

          因此當我們在服務端處理文件目錄上傳的功能時,我們就可以通過該屬性來創建對應的目錄結構,具體的處理邏輯如下所示:

          const fse = require("fs-extra"); 
           
          const storage = multer.diskStorage({ 
            destination: async function (req, file, cb) { 
              // 把useAxios@demo.vue中的@替換為路徑分隔符 
              let relativePath = file.originalname.replace(/@/g, path.sep);  
              let index = relativePath.lastIndexOf(path.sep);  
              let fileDir = path.join(UPLOAD_DIR, relativePath.substr(0, index)); // 生成文件路徑 
              await fse.ensureDir(fileDir); // 確保當前目錄存在 
              cb(null, fileDir); 
            }, 
            filename: function (req, file, cb) { 
              let parts = file.originalname.split("@"); // 對路徑進行拆分 
              cb(null, `${parts[parts.length - 1]}`); // 獲取文件名 
            }, 
          }); 

          為什么 originalname 文件原始名稱會包含 @ 符號呢?這樣因為使用 useAxios/demo.vue 這種路徑形式時,是不能獲取到完整的路徑名稱,只能獲取到文件名。為了解決這個問題,阿寶哥在上傳文件時,手動把文件相對路徑中的 / 符號替換為 @ 然后再進行上傳,對應的處理邏輯如下:

          function uploadFiles({ url, files, fieldName = "file" }) { 
            if (!url || !files.length) return; 
            let formData = new FormData(); 
            for (let i = 0; i < files.length; i++) { 
              formData.append(fieldName, files[i], files[i].webkitRelativePath.replace(/\//g, "@")); 
            } 
            return request.post(url, formData); 
          } 

          好的,壓縮目錄上傳和目錄上傳已經介紹完了,感興趣的小伙伴可以動手試試看。由于完整的示例代碼內容比較多,阿寶哥就不放具體的代碼了。有需要的小伙伴,可以訪問以下地址瀏覽示例代碼。

          https://gist.github.com/semlinker/af57349c16d203cc2ec845d4b5a6b445

          注意:以上代碼僅供參考,請根據實際業務進行調整。

          三、總結

          本文阿寶哥介紹了如何利用 input[type="file"] 元素的 webkitdirectory 屬性來實現選擇目錄的功能,然后利用 JSZip 這個庫來實現目錄壓縮,最后通過 axios 來上傳目錄壓縮后的 ZIP 文件 。此外,阿寶哥還介紹了如何使用 koa 來實現接收目錄并按照文件目錄結構進行存放的功能。

          四、參考資源

          JSZip 官方文檔

          MDN - webkitdirectory

          JavaScript 如何在線解壓 ZIP 文件?


          主站蜘蛛池模板: 精品免费国产一区二区| 色天使亚洲综合一区二区| 香蕉视频一区二区| 蜜臀AV一区二区| asmr国产一区在线| 亚洲字幕AV一区二区三区四区| 国产无套精品一区二区| 福利一区国产原创多挂探花| 亚洲国产成人久久一区二区三区| 国产精品视频一区二区三区无码| 国产亚洲自拍一区| 小泽玛丽无码视频一区| 在线免费一区二区| 国产精品一区二区毛卡片| 精品少妇ay一区二区三区 | 四虎在线观看一区二区| 亚洲电影一区二区| 中文字幕精品无码一区二区| 国产高清精品一区| 一区二区在线免费视频| 丰满人妻一区二区三区视频| 国产激情一区二区三区四区| 日本精品啪啪一区二区三区| 午夜一区二区在线观看| av一区二区三区人妻少妇| 国产成人久久一区二区不卡三区| 无码人妻一区二区三区兔费 | 综合久久一区二区三区 | 丰满人妻一区二区三区视频53| 精品国产免费一区二区| 国产在线观看精品一区二区三区91| 中文字幕一区二区三区人妻少妇| 国内精品一区二区三区东京| 极品尤物一区二区三区| 亚洲国产成人久久一区二区三区 | 免费无码一区二区三区蜜桃| 国产一区二区三区高清在线观看 | 亚洲天堂一区在线| 在线日产精品一区| 麻豆精品人妻一区二区三区蜜桃| 手机福利视频一区二区|