網頁中Office和pdf相關檔案匯出

豐臣正一發表於2020-11-22

最近被派去維護和開發一些做了一半、年久失修的專案。有一部分內容是關於word檔案匯出,順帶著把excel、pdf檔案的匯出也調研下吧,我想未來開發我應該會遇到的,遂做了下筆記分享給需要的人。

由於專案年久失修,所以你可能已經猜到了。是的,本文章基於JQuery以及JQuery相關的外掛進行開發實踐,如果後面空下來有時間我會進一步出Vue、Angular、React相關的例子。閱讀本篇文章你將獲得:

  • JQuery外掛的封裝
  • 基於JQuery外掛WordExport及其衍生外掛的使用
  • 基於JQuery外掛tableExport及其衍生外掛的使用
  • 一種直奔原始碼解決問題的處事思想
  • 匯出相關檔案中文亂碼的解決方法
  • 匯出相關圖片不全的解決方法
  • 媒體查詢列印也不失為一種好的選擇

emmm,本文關於表格的匯出,絕大部分是基於table這個元素得到的。

word相關匯出

依賴

  • Jquery.js
  • FileSaver.js
  • jquery.wordexport.js

核心程式碼

	$(document).ready(function ($) {
		$('.word-export').click(function (event) {
			$('#page-content').wordExport('警情研判');
		});
	});

這裡就是給.word-export這個類繫結一個點選事件,然後其執行的內容是呼叫wordExport方法匯出word。

需求是實現一張形如樓下的網頁匯出

起初看到這樣一個頁面,我內心是拒絕用table佈局的,其一是之前學前端看到一些前端說table元素佈局的一些弊端,比如佔更多位元組、下載就會延遲、阻塞瀏覽器渲染、影響內部元素佈局、不利於搜尋引擎爬取等等, 其二是我對table不是特別熟悉。所以一開始看到樓上的效果,我是喜歡用grid網格佈局或者flex彈性佈局來實現的。所以,就先用彈性佈局出一版吧。

先說下思路吧,左側那個表格類別和轄區我一開始是覺得用canvas繪圖比較合適,表格整體用flex佈局實現,其他同類項用flex:1進行均分,flex:1flex-growflex-shrinkflex-basis的簡寫,具體的可以看下阮老師寫的flex佈局http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html。然後你會遇到表格間距不一樣的問題,我是這麼解決的,每次我只畫表格最小單元的左邊框和上邊框,那麼到最後它是不是就剩下最大的那個表格的右邊框和下邊框,這樣子就解決了。大致的思路是這樣子了,實現的效果如下:

但是這個實現匯出的結果有些不盡如人意,其一因為匯出的是word,格式問題在所難免。其二是回到我們最初的依賴,這裡是依賴於jquery.wordexport.js這個匯出word的外掛,所以發現預期與理想不符,我們就需要區閱讀原始碼來找到答案。

嗯,看到76行Todo那裡開始往下看,我們找到了原因。

匯出的結果這裡就補貼了,不堪入目,有興趣可以看下這個專案的demo地址如下:https://codepen.io/ataola/pen/xxONVQv

既然,用flex佈局達不到預期,那麼grid佈局也沒有必要去試了。那要不卑微碼農屈服下吧,去學下table元素的表格佈局。

這次我們同樣實現了樓上的效果,略微有點不同的是,我這裡沒再用canvas實現左上角的效果,而是用position絕對定位和transformrotate屬性去實現。這次稍微有點word的樣子,沒有糊噠噠的一坨了。

但是這個效果顯然是不理想的,咦,邊框404了。

這個版本的專案地址是:https://codepen.io/ataola/pen/eYzaxZy

我們先思考下,看了之前的原始碼,再看了我這個的原始碼,我突然有個不成熟的想法。之前我是用載入相關css,然後用類或者id選擇器去控制其樣式,要不簡單粗暴一點,直接style一把梭,好,那我們就試試吧。

最後,我得到了我想要的效果,雖然也還是有點瑕疵,畢竟word嘛,追求格式的完美,不容易變形、請使用pdf,哈哈。

這個的demo地址:https://codepen.io/ataola/pen/vYKwPZx

以上是我抽離出表格模組,單獨閹割出來的版本。比較綜合的一個版本,請訪問這個地址:http://zhengjiangtao.cn/show/office/export-word.html

excel相關匯出

做完樓上這個模組,總感覺意猶未盡,比如表格我很容易聯想到excel、格式不易變形我很容易聯想到pdf,要不再往下走走。

我們要實現這樣一個效果,可以匯出xlsxlsxcsvxmltxtjsonsql檔案格式的功能,這裡我分別準備了三個測試用例,複雜表格、中文表格、英文表格,如下:

依賴

  • jquery.js
  • FileSaver.js
  • xlsx.js(非必須,匯出xlsx格式需要)
  • tableExport.js(依賴Jquery)

核心程式碼

$(document).ready(function () {
  $('#exportExcelOneXls').click(function () {
    $('#table').tableExport({ type: 'excel', fileName: '警情研判', tableName: 'myTableName' });
  });

  $('#exportExcelOneXlsx').click(function () {
    $('#table').tableExport({ type: 'xlsx', fileName: '警情研判', tableName: 'myTableName' });
  });

  $('#exportExcelOneCsv').click(function () {
    $('#table').tableExport({ type: 'csv', fileName: '警情研判' });
  });

  $('#exportExcelOneXml').click(function () {
    $('#table').tableExport({
      type: 'xml',
      fileName: '警情研判',
      mso: { fileFormat: 'xmlss', worksheetName: ['楊凌區每日警情統計'] }
    });
  });

  $('#exportExcelOneTxt').click(function () {
    $('#table').tableExport({ type: 'txt', fileName: '警情研判' });
  });

  $('#exportExcelOneJson').click(function () {
    $('#table').tableExport({ type: 'json', fileName: '警情研判' });
  });

  $('#exportExcelOneSql').click(function () {
    $('#table').tableExport({ type: 'sql', fileName: '警情研判' });
  });

});

大致就是給相應的按鈕繫結相應的點選事件,然後呼叫tableExport去下載相應檔案格式的檔案。

專案地址如下:http://zhengjiangtao.cn/show/office/export-excel.html

踩坑

這裡大致遇到這麼些問題,我這裡進行總結下,解決問題的思路,大致都指向一點,那就是看原始碼、改原始碼

  • 匯出csv亂碼

原始碼252行: if (defaults.type === 'csv' || defaults.type === 'tsv' || defaults.type === 'txt')

先找到觸發下載csv檔案指向的相關邏輯

原始碼325行-332行

 saveToFile(
     csvData,
     defaults.fileName + '.' + defaults.type,
     'text/' + (defaults.type === 'csv' ? 'csv' : 'plain'),
     'utf-8',
     '',
     defaults.type === 'csv' && defaults.csvUseBOM
 );

嗯,程式呼叫了saveToFile這個函式,如果你和我一樣用VSCode開發的話,按住CTRL+滑鼠左鍵進入函式相關實現,

2443行,找到了,給它來個特寫

 function saveToFile(data, fileName, type, charset, encoding, bom) {
      var saveIt = true;
      if (typeof defaults.onBeforeSaveToFile === 'function') {
        saveIt = defaults.onBeforeSaveToFile(data, fileName, type, charset, encoding);
        if (typeof saveIt !== 'boolean') saveIt = true;
      }

      if (saveIt) {
        try {
          blob = new Blob([data], { type: type + ';charset=' + charset });
          saveAs(blob, fileName, bom === false);
          if (typeof defaults.onAfterSaveToFile === 'function') defaults.onAfterSaveToFile(data, fileName);
        } catch (e) {
          downloadFile(
            fileName,
            'data:' +
              type +
              (charset.length ? ';charset=' + charset : '') +
              (encoding.length ? ';' + encoding : '') +
              ',',
            bom ? '\ufeff' + data : data
          );
        }
      }
    }

blob = new Blob([data], { type: type + ';charset=' + charset });這行,應該是其轉換成二進位制時編碼出了問題, 修改下

  if (defaults.type === 'csv') {
      blob = new Blob([(defaults.type == 'csv' && defaults.csvUseBOM ? '\ufeff' : '') + csvData], {
      type: 'text/' + (defaults.type == 'csv' ? 'csv' : 'plain') + ';charset=utf-8'
      });
  } else {
  	blob = new Blob([data], { type: type + ';charset=' + charset });
  }

這裡是因為筆者試過,用txt開啟csv,然後將其編碼改成帶BOM的UTF8可以顯示中文,所以這麼改。

注意這裡的邏輯,我並沒有把作者原來的那句話幹掉,而是判斷了csv格式的情況,這樣是比較嚴謹的,因為作者這樣寫自然有其道理,我們改原始碼的目的是為了實現我們需求的功能而不是幹掉原來的,因為有可能引發其他問題的,年輕人要講碼德,耗子尾汁,哈哈哈。

備註:由於我用了prettier進行相關的格式化,所以這裡的程式碼行數僅作參考

pdf相關匯出

因為tableExport這個外掛,如果有JsPDFjsPDF-Autoablepdfmake的加持的話,它可以實現pdf檔案的匯出,這裡我們實踐下吧。

需求是實現一張形如樓下的網頁匯出:

依賴

  • jquery.js
  • FileSaver.js
  • jsPdf.js
  • jsPDF.Autoable.js
  • pdfmake.js
  • tableExport.js

核心程式碼

$(document).ready(function () {
  $('#exportPdfOneJs').click(function () {
    $('#table').tableExport({
      type: 'pdf',
      fileName: '警情研判',
      jspdf: {
        orientation: 'p',
        margins: { right: 20, left: 20, top: 30, bottom: 30 },
        autotable: { styles: { fillColor: 'inherit', textColor: 'inherit', fontStyle: 'inherit' }, tableWidth: 'wrap' }
      }
    });
  });

  $('#exportPdfOneAutotable').click(function () {
    $('#table').tableExport({
      type: 'pdf',
      fileName: '警情研判',
      jspdf: {
        orientation: 'l',
        format: 'a3',
        margins: { left: 10, right: 10, top: 20, bottom: 20 },
        autotable: { styles: { fillColor: 'inherit', textColor: 'inherit' }, tableWidth: 'auto' }
      }
    });
  });

  $('#exportPdfOnePdfMake').click(function () {
    $('#table').tableExport({
      type: 'pdf',
      fileName: '警情研判',
      pdfmake: { enabled: true, docDefinition: { pageOrientation: 'landscape' } }
    });
  });
});

邏輯同樓上,分別用了三種外掛實現了三種匯出,其中前兩種對中文支援不友好,第三章pdfmake加上相關字型檔案的加持,可以匯出可以看的中文版。

專案地址如下:http://zhengjiangtao.cn/show/office/export-pdf.html

踩坑

  • pdfmake匯出中文亂碼顯示 “口”

原始碼112行-121行

 pdfmake: {
        enabled: false, // true: Use pdfmake as pdf producer instead of jspdf and jspdf-autotable
        docDefinition: {
          pageOrientation: 'portrait', // 'portrait' or 'landscape'
          defaultStyle: {
            font: 'ZCOOLXiaoWei' // Default font is 'Roboto' (needs vfs_fonts.js to be included)
          } // For an arabic font include mirza_fonts.js instead of vfs_fonts.js
        }, // For a chinese font include either gbsn00lp_fonts.js or ZCOOLXiaoWei_fonts.js instead of vfs_fonts.js
        fonts: {}
 },

之前defaultStyleRoboto是不支援中文的,好在作者寫了註釋,我們把它替換成站酷的字型ZCOOLXiaoWei,好了,這下子匯出正常了。

emmm,講道理就實踐來看,瀏覽器列印出來的pdf是最穩的,所以這裡我有個不成熟的想法,就是利用媒體查詢加上window自帶的列印去實現這個功能。

核心程式碼如下:

@media print {
    .media-screen, .export-pdf-operate  {
        display: none;
    }
    #tableBox {
        width: 920px;
        margin: 0 auto;
    }
}

列印時利用媒體查詢隱藏掉不相關的元素,然後利用window.print()函式去列印相關的內容。

圖片相關匯出

依賴

  • jquery.js
  • html2canavs.js
  • tableexport.js

核心程式碼

$('#exportPdfTwoHtmlTwoCanvas').click(function () {
    $('#tableTwo').tableExport({
    type: 'png',
    fileName: '初三二班成績排名'
    });
});

邏輯同樓上。

踩坑

  • html2canvas截圖不全

通過查閱相關文獻,我知道了,原因大概就是可能沒有載入完全就開始截圖了,然後位置不對。既然是這樣,那大概是兩種思路,第一種,加延遲(治標不治本,萬一檔案很大涼涼), 第二種,重置截圖位置(友好一點,截圖完給它復原下)

我們雙管齊下,翻到原始碼913行

 setTimeout(() => {
        const pageYOffset = window.pageYOffset;
        window.pageYOffset = 0;
        const htmlScrollTop = document.documentElement.scrollTop;
        document.documentElement.scrollTop = 0;
        const bodyScrollTop = document.body.scrollTop;
        document.body.scrollTop = 0;
        html2canvas($(el)[0]).then(function (canvas) {
          var image = canvas.toDataURL();
          var byteString = atob(image.substring(22)); // remove data stuff
          var buffer = new ArrayBuffer(byteString.length);
          var intArray = new Uint8Array(buffer);

          for (var i = 0; i < byteString.length; i++) intArray[i] = byteString.charCodeAt(i);

          if (defaults.outputMode === 'string') return byteString;
          if (defaults.outputMode === 'base64') return base64encode(image);

          if (defaults.outputMode === 'window') {
            window.open(image);
            return;
          }

          saveToFile(buffer, defaults.fileName + '.png', 'image/png', '', '', false);
          window.pageYOffset = pageYOffset;
          document.documentElement.scrollTop = htmlScrollTop;
          document.body.scrollTop = bodyScrollTop;
        });
      }, 5000);

大致是這樣子的,加了5秒延遲,然後截圖是置scrollToppageYOffset為0,然後截圖完給它復現會去。

地址如下:http://zhengjiangtao.cn/show/office/export-pdf.html

JQuery外掛的封裝

看完樓上這些,我大致也知道怎麼封裝一個JQuery外掛了,這裡分享下思路

大致是搞了一個自執行函式,然後$.fn後面跟一個外掛函式的實現,用$.extend去實現引數的繼承。這裡我們實現的一個函式效果是列印出該元素除了函式以外的style屬性。

程式碼如下:

/*
 * @Author: ataola
 * @Date: 2020-11-22 17:08:19
 * @Last Modified by: ataola
 * @Last Modified time: 2020-11-22 18:21:45
 */
'use strict';
(function ($) {
  $.fn.printStyle = function (options) {
    console.log('function printStyle start ========>');
    const el = this;
    const defaults = {
      color: 'red'
    };
    $.extend(true, defaults, options);
    const style = $(el).get(0).style;
    const div = document.createElement('div');
    for (const attr in style) {
      const val = getStyle($(el).get(0), attr);
      if (!(val instanceof Function)) {
        const res = `${attr}: ${val}`;
        div.innerHTML = div.innerHTML + res + '<br/>';
        console.log(res);
      }
    }
    div.style.color = defaults.color;
    document.body.appendChild(div);
    console.log('function printStyle end <========');
  };

  function getStyle(obj, attr) {
    if (obj.currentStyle) {
      return obj.currentStyle[attr];
    } else {
      return getComputedStyle(obj, false)[attr];
    }
  }
})(jQuery);

效果如下:

地址如下:http://zhengjiangtao.cn/show/jquery/plugin.html

看到這裡,再回到之前word的那個例子,你大概就能明白實現word高度還原,其實是挺複雜的。。。。。。

因為好像沒有API讓我們去獲取選擇器上所定義的相關css屬性,而你直接寫在元素的style上是直接可以讀到的,style的權重(1000)也很高。

以上就是今天的全部內容,感謝閱讀!

參考文獻

FileSaver.js: https://github.com/eligrey/FileSaver.js

JQuery-Word-Export: https://github.com/markswindoll/jQuery-Word-Export

tableExport.jquery.plugin: https://github.com/hhurz/tableExport.jquery.plugin

pdfmake: https://github.com/bpampuch/pdfmake

html2canvas: https://github.com/niklasvh/html2canvas

html2canvas截圖不全: https://www.jianshu.com/p/88f07d5c5c70

相關文章