如何使用JavaScript實現純前端讀取和匯出excel檔案

1. js-xlsx 介紹

由SheetJS出品的js-xlsx是一款非常方便的只需要純JS即可讀取和匯出excel的工具庫,功能強大,支援格式眾多,支援xls、xlsx、ods(一種OpenOffice專有表格檔案格式)等十幾種格式。本文全部都是以xlsx格式為例。

官方 後面會加上附件,有時候訪問github很慢
好記部落格線上演示地址

1.1 相容性

1.2 如何使用

dist目錄下有很多個JS檔案,一般情況下用xlsx.core.min.js就夠了,xlsx.full.min.js則是包含了所有功能模組。

直接script標籤引入即可:

1
<script type="text/javascript" src="./js/xlsx.core.min.js"></script>

2. 讀取excel

讀取excel主要是透過XLSX.read(data, {type: type});方法來實現,回傳一個叫WorkBook的物件,type主要取值如下:

  • base64: 以base64方式讀取;
  • binary: BinaryString格式(byte n is data.charCodeAt(n))
  • string: UTF8編碼的字串;
  • buffer: nodejs Buffer;
  • array: Uint8Array,8位無符號陣列;
  • file: 檔案的路徑(僅nodejs下支援);

2.1. 取得workbook物件

2.1.1. 讀取本地檔案

直接上程式碼:

1
2
3
4
5
6
7
8
9
10
// 讀取本地excel檔案
function readWorkbookFromLocalFile(file, callback) {<!-- -->
    var reader = new FileReader();
    reader.onload = function(e) {<!-- -->
        var data = e.target.result;
        var workbook = XLSX.read(data, {<!-- -->type: 'binary'});
        if(callback) callback(workbook);
    };
    reader.readAsBinaryString(file);
}

2.1.2. 讀取網路檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 從網路上讀取某個excel檔案,url必須同域,否則報錯
function readWorkbookFromRemoteFile(url, callback) {<!-- -->
    var xhr = new XMLHttpRequest();
    xhr.open('get', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function(e) {<!-- -->
        if(xhr.status == 200) {<!-- -->
            var data = new Uint8Array(xhr.response)
            var workbook = XLSX.read(data, {<!-- -->type: 'array'});
            if(callback) callback(workbook);
        }
    };
    xhr.send();
}

2.2. 詳解 workbook

2.2.1. Workbook Object

workbook裡面有什麼東西呢,我們列印出來看一下:

可以看到,SheetNames裡面儲存了所有的sheet名字,然後Sheets則儲存了每個sheet的具體內容(我們稱之為Sheet Object)。每一個sheet是透過類似A1這樣的鍵值儲存每個單元格的內容,我們稱之為單元格物件(Cell Object):

注意:日期格式取得,使用者填寫的真實值到w

2.2.2. Sheet Object

每一個Sheet Object表示一張表格,只要不是!開頭的都表示普通cell,否則,表示一些特殊含義,具體如下:

  • sheet[』!ref』]:表示所有單元格的範圍,例如從A1到F8則紀錄為A1:F8;
  • sheet[!merges]:存放一些單元格合併訊息,是一個陣列,每個陣列由包含s和e構成的物件組成,s表示開始,e表示結束,r表示行,c表示列;
  • 等等;

關於單元格合併,看懂下面這張圖基本上就沒問題了:

結果如下:

2.2.3. 單元格物件

每一個單元格是一個物件(Cell Object),主要有t、v、r、h、w等欄位(詳見官方):

  • t:表示內容型別,s表示string型別,n表示number型別,b表示boolean型別,d表示date型別,等等
  • v:表示原始值;
  • f:表示公式,如B2+B3;
  • h:HTML內容
  • w:格式化後的內容
  • r:富文字內容rich text
  • 等等

2.2.4. 讀取workbook

普通方法:

1
2
3
4
5
6
7
8
9
10
11
// 讀取 excel檔案
function outputWorkbook(workbook) {<!-- -->
    var sheetNames = workbook.SheetNames; // 工作表名稱集合
    sheetNames.forEach(name => {<!-- -->
        var worksheet = workbook.Sheets[name]; // 只能透過工作表名稱來取得指定工作表
        for(var key in worksheet) {<!-- -->
            // v是讀取單元格的原始值
            console.log(key, key[0] === '!' ? worksheet[key] : worksheet[key].v);
        }
    });
}

根據!ref確定excel的範圍,再根據!merges確定單元格合併(如果有),最後輸出整個table,比較麻煩,幸運的是,外掛自身已經寫好工具類XLSX.utils給我們直接使用,無需我們自己遍歷,工具類輸出主要包括如下:

有些不常用,常用的主要是:

  • XLSX.utils.sheet_to_csv:生成CSV格式
  • XLSX.utils.sheet_to_txt:生成純文字格式
  • XLSX.utils.sheet_to_html:生成HTML格式
  • XLSX.utils.sheet_to_json:輸出JSON格式
    常用的主要是sheet_to_csv或者sheet_to_html,轉csv的話會忽略格式、單元格合併等訊息,所以複雜表格可能不適用。轉html的話會保留單元格合併,但是生成的是程式碼,而不是

    ,需要對表格進行一些定製時不太方便,所以具體還是要視情況來採用合適的工具類。

這裡寫一個採用轉csv方式輸出結果的簡單範例,可點選這裡檢視線上DEMO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function readWorkbook(workbook)
{<!-- -->
    var sheetNames = workbook.SheetNames; // 工作表名稱集合
    var worksheet = workbook.Sheets[sheetNames[0]]; // 這裡我們唯讀取第一張sheet
    var csv = XLSX.utils.sheet_to_csv(worksheet);
    document.getElementById('result').innerHTML = csv2table(csv);
}

// 將csv轉換成簡單的表格,會忽略單元格合併,在第一行和第一列新增類似excel的索引
function csv2table(csv)
{<!-- -->
    var html = '<table>';
    var rows = csv.split('
');
    rows.pop(); // 最後一行沒用的
    rows.forEach(function(row, idx) {<!-- -->
        var columns = row.split(',');
        columns.unshift(idx+1); // 新增行索引
        if(idx == 0) {<!-- --> // 新增列索引
            html += '<tr>';
            for(var i=0; i<columns.length; i++) {<!-- -->
                html += '<th>' + (i==0?'':String.fromCharCode(65+i-1)) + '</th>';
            }
            html += '</tr>';
        }
        html += '<tr>';
        columns.forEach(function(column) {<!-- -->
            html += '<td>'+column+'</td>';
        });
        html += '</tr>';
    });
    html += '</table>';
    return html;
}

3. 匯出excel

匯出分為2種,一種是基於現有excel修改,一種是全新生成,前者比較簡單,我們這裡著重講後者。

3.1. 自己手寫程式碼生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// csv轉sheet物件
function csv2sheet(csv) {<!-- -->
    var sheet = {<!-- -->}; // 將要生成的sheet
    csv = csv.split('
');
    csv.forEach(function(row, i) {<!-- -->
        row = row.split(',');
        if(i == 0) sheet['!ref'] = 'A1:'+String.fromCharCode(65+row.length-1)+(csv.length-1);
        row.forEach(function(col, j) {<!-- -->
            sheet[String.fromCharCode(65+j)+(i+1)] = {<!-- -->v: col};
        });
    });
    return sheet;
}

// 將一個sheet轉成最終的excel檔案的blob物件,然後利用URL.createObjectURL下載
function sheet2blob(sheet, sheetName) {<!-- -->
    sheetName = sheetName || 'sheet1';
    var workbook = {<!-- -->
        SheetNames: [sheetName],
        Sheets: {<!-- -->}
    };
    workbook.Sheets[sheetName] = sheet;
    // 生成excel的配置項
    var wopts = {<!-- -->
        bookType: 'xlsx', // 要生成的檔案型別
        bookSST: false, // 是否生成Shared String Table,官方解釋是,如果開啟生成速度會下降,但在低版本IOS裝置上有更好的相容性
        type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    var blob = new Blob([s2ab(wbout)], {<!-- -->type:"application/octet-stream"});
    // 字串轉ArrayBuffer
    function s2ab(s) {<!-- -->
        var buf = new ArrayBuffer(s.length);
        var view = new Uint8Array(buf);
        for (var i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
        return buf;
    }
    return blob;
}

拿到上面的blob物件就可以直接下載了,參考JS跳出下載對話盒封裝好的openDownloadDialog方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 通用的開啟下載對話盒方法,沒有測試過具體相容性
 * @param url 下載地址,也可以是一個blob物件,必選
 * @param saveName 儲存檔名,可選
 */
function openDownloadDialog(url, saveName)
{<!-- -->
    if(typeof url == 'object' && url instanceof Blob)
    {<!-- -->
        url = URL.createObjectURL(url); // 建立blob地址
    }
    var aLink = document.createElement('a');
    aLink.href = url;
    aLink.download = saveName || ''; // HTML5新增的屬性,指定儲存檔名,可以不要後序,注意,file:///模式下不會生效
    var event;
    if(window.MouseEvent) event = new MouseEvent('click');
    else
    {<!-- -->
        event = document.createEvent('MouseEvents');
        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    }
    aLink.dispatchEvent(event);
}

所以,最終下載實現如下:

1
2
3
4
5
6
// 傳入csv,執行後就會跳出下載框
function exportExcel(csv) {<!-- -->
    var sheet = csv2sheet(csv);
    var blob = sheet2blob(sheet);
    openDownloadDialog(blob, '匯出.xlsx');
}

3.2. 利用官方工具類生成

其實上面這些程式碼都不需要我們手寫,官方已經提供好了現成的工具類給我們使用,主要包括:

  • aoa_to_sheet: 這個工具類最強大也最實用了,將一個二維陣列轉成sheet,會自動處理number、string、boolean、date等型別資料;
  • table_to_sheet: 將一個table dom直接轉成sheet,會自動識別colspan和rowspan並將其轉成對應的單元格合併;
  • json_to_sheet: 將一個由物件組成的陣列轉成sheet;

aoa_to_sheet範例:

1
2
3
4
5
6
7
var aoa = [
    ['姓名', '性別', '年齡', '註冊時間'],
    ['張三', '男', 18, new Date()],
    ['李四', '女', 22, new Date()]
];
var sheet = XLSX.utils.aoa_to_sheet(aoa);
openDownloadDialog(sheet2blob(sheet), '匯出.xlsx');

table_to_sheet就更簡單了,直接XLSX.utils.table_to_sheet($(『table』)[0])即可;

3.3. 處理單元格合併

一般來說,前端生成excel而不是csv最主要目的都是為了解決csv不能實現單元格合併的問題,要不然直接匯出csv檔案就好了,何必引入幾百kb的外掛。

假設我們要生成如下格式的excel檔案,其中A1-C1進行單元格合併:

程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
var aoa = [
    ['主要訊息', null, null, '其它訊息'], // 特別注意合併的地方後面預留2個null
    ['姓名', '性別', '年齡', '註冊時間'],
    ['張三', '男', 18, new Date()],
    ['李四', '女', 22, new Date()]
];
var sheet = XLSX.utils.aoa_to_sheet(aoa);
sheet['!merges'] = [
    // 設定A1-C1的單元格合併
    {<!-- -->s: {<!-- -->r: 0, c: 0}, e: {<!-- -->r: 0, c: 2}}
];
openDownloadDialog(sheet2blob(sheet), '單元格合併範例.xlsx');

需要注意的地方就是被合併的單元格要用null預留出位置,否則後面的內容(本例中是第四列其它訊息)會被覆蓋。

3.4. 自訂樣式

普通版本不支援定義字型、顏色、背景色等,有這個功能需要的可以使用pro版,好像是要收費的,因為官網沒看到下載地址。

3.5. 例項

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <script src="js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
        <script type="text/javascript" src="js/xlsx.core.min.js"></script>
        <script>
        window.onload = function() {<!-- -->
            $('#upload_excel').change(function(e) {<!-- -->
                var files = e.target.files;
                var fileReader = new FileReader();
                var excle = $("#upload_excel").val();
                if(excle == null) {<!-- -->
                    alert("未選擇Excel檔案");
                } else {<!-- -->
                    // 判斷是否是Excel格式
                    if(excle != '') {<!-- -->
                        //檔名可以帶空白
                        var reg = /^.*.(?:xls|xlsx)$/i;
                        //校驗不透過
                        if(!reg.test(excle)) {<!-- -->
                            alert("請上傳excel格式的檔案!");
                            return;
                        } else {<!-- -->
                            fileReader.onload = function(ev) {<!-- -->
                                try {<!-- -->
                                    var data = ev.target.result,
                                        workbook = XLSX.read(data, {<!-- -->
                                            type: 'binary'
                                        }), // 以二進位制流方式讀取得到整份excel表格物件
                                        persons = []; // 儲存取得到的資料
                                } catch(e) {<!-- -->
                                    alert('檔案型別不正確');
                                    return;
                                }

                                // 表格的表格範圍,可用於判斷表頭是否數量是否正確
                                var fromTo = '';
                                // 遍歷每張表讀取
                                for(var sheet in workbook.Sheets) {<!-- -->
                                    if(workbook.Sheets.hasOwnProperty(sheet)) {<!-- -->
                                        fromTo = workbook.Sheets[sheet]['!ref'];
                                        persons = persons.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet]));
                                    }
                                }
                                if(persons.length > 10000) {<!-- -->
                                    alert("Excel長度超過10000條,不能使用");
                                } else {<!-- -->
                                    _Result = persons;
                                    var id = "";
                                    if(persons.length == 0) {<!-- -->
                                        alert("匯入Excel中無資料");
                                    } else {<!-- -->
                                        //  新增在頁面表格中(新增程式碼就不舉例說明了)
                                    }
                                }
                            };
                        }
                    }

                }
                // 以二進位制方式開啟檔案
                fileReader.readAsBinaryString(files[0]);
                // 清空input 值 避免選擇同名字的excel 檔案不執行
                $("#upload_excel").val("")
            });
        }
        </script>
    </head>

    <body>
        <input type="file" id="upload_excel">
    </body>

</html>