前端實現(excel)xlsx文件預覽的詳細步驟
1. 概述
接到一個任務,是要前端實現文件預覽效果,百度了一圈,發(fā)現也沒有什么好的方法可以完美的將表格渲染出來。在前端中有sheetjs和exceljs可以對xlsx文件進行解析,本來一開始我用的是sheetjs,但是在樣式獲取上遇到了麻煩,所以我改用了exceljs,不過很難受,在樣式獲取時同樣遇到了不小的麻煩,但是我懶得換回sheetjs了,那就直接使用exceljs吧。
要實現xlsx文件預覽效果,我的想法是使用一個庫對xlsx文件進行解析,然后使用另一個庫對解析出來的數據在頁面上進行繪制,綜上,我采用的方案是:exceljs+handsontable
2. 實現步驟
2.1 安裝庫
使用命令: npm i exceljs handsontable @handsontable/react
2.2 使用exceljs解析數據并使用handsontable進行渲染
直接貼代碼了:
import Excel from 'exceljs'
import { useState } from 'react';
import { HotTable } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.css';
import { textRenderer, registerRenderer } from 'handsontable/renderers';
// 注冊模塊
registerAllModules();
export default function XLSXPreView() {
const [data, setData] = useState([]);
const handleFile = async (e) => {
const file = e.target.files[0];
const workbook = new Excel.Workbook();
await workbook.xlsx.load(file)
// 第一個工作表
const worksheet = workbook.getWorksheet(1);
// 遍歷工作表中的所有行(包括空行)
const sheetData = [];
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
// console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
// 使用row.values獲取每一行的值時總會多出一條空數據(第一條),這里我把它刪除
const row_values = row.values.slice(1);
sheetData.push(row_values)
});
setData(sheetData);
}
return (
<>
<input type="file" onChange={handleFile}/>
<div id='table_view'>
<HotTable
data={data}
readOnly={true}
rowHeaders={true}
colHeaders={true}
width="100vw"
height="auto"
licenseKey='non-commercial-and-evaluation'// 一定得加這個,handsontable是收費的,加了這個才能免費用
/>
</div>
</>
)
}
到這里,已經實現了從xlsx文件中獲取數據,并使用handsontable將表格中的數據渲染出來,示例結果如下,如果只需要將數據顯示出來,并不需要將樣式什么的一起復現了,那到這里就已經結束了!

但事實上,這并不是我要做到效果,我的xlsx里面還有樣式什么的,也需要復現,頭疼??
3. 其它的雜七雜八
3.1 單元格樣式
事實上,在exceljs解析xlsx文件時,它順帶一起把樣式獲取到了,通過worksheet.getCell(1, 1).style可以獲取對應單元格的樣式,如下,背景色存放在fill.fgColor中,字體顏色存放在font.color中,這樣的話只需要將這些樣式一一賦值給handsontable組件再添加樣式就好了。

但是實際操作的時候卻遇到了問題,先說excel中的顏色,在選擇顏色時,應該都會打開下面這個選項框吧,如果你選擇的是標準色,它獲取到的顏色就是十六進制,但是如果你選擇主題中的顏色,那就是另一種結果了,并且還會有不同的深暗程度tint,這就很難受了!

隨后在控制臺中打印了workbook,發(fā)現它把主題返回了,可以通過work._themes.theme1獲取,不過獲取到的是xml格式的字符串,由于xml我沒學,我不會,所以我就把它轉換成json來進行處理了。
第一步
安裝xml轉json的庫: npm i fast-xml-parser
import {XMLParser} from 'fast-xml-parser'
// 將主題xml轉換成json
const themeXml = workbook._themes.theme1;
const options = {
ignoreAttributes: false,
attributeNamePrefix: '_'
}
const parser = new XMLParser(options);
const json = parser.parse(themeXml)
setThemeJson(json);
其實它的theme好像是固定的,也可以在一些格式轉換的網站中直接轉換成json然后放到一個json文件中,讀取就行,我這里就直接放到一個state中了!
第二步
接下來就是重頭戲了!設置單元格樣式…
首先安裝一個處理顏色的庫color,用來根據tint獲得不同明暗程度的顏色: npm i color
下面是獲取顏色的函數:
// 根據主題和明暗度獲取顏色
const getThemeColor = (themeJson, themeId, tint) => {
let color = '';
const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
switch (themeId) {
case 0:
color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
break;
case 1:
color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
break;
case 2:
color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
break;
case 3:
color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
break;
default:
color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
break;
}
// 根據tint修改顏色深淺
color = '#' + color;
const colorObj = Color(color);
if(tint){
if(tint>0){// 淡色
color = colorObj.lighten(tint).hex();
}else{ // 深色
color = colorObj.darken(Math.abs(tint)).hex();
}
}
return color;
}
// 獲取顏色
const getColor = (obj, themeJson) => {
if('argb' in obj){ // 標準色
// rgba格式去掉前兩位: FFFF0000 -> FF0000
return '#' + obj.argb.substring(2);
}else if('theme' in obj){ // 主題顏色
if('tint' in obj){
return getThemeColor(themeJson, obj.theme, obj.tint);
}else{
return getThemeColor(themeJson, obj.theme, null);
}
}
}
然后設置handonsontable的單元格的一些樣式:顏色、加粗、下劃線、邊框balabala…的
順帶把行高和列寬一起設置了,這個還比較簡單,就一筆帶過了…
3.2 合并單元格
從獲取到的sheet中有一個_meages屬性,該屬性中存放了表格中所有的合并單元格區(qū)域,所以只需要將它們重新渲染在handsontable中就好。

然后就實現了表格的一些基本功能的預覽,結果如下圖:

3. 總結(附全代碼)
其實這個的本質主要就是通過ecxeljs解析表格文件的數據,然后通過handsontable將它們重新繪制在頁面上,個人覺得這種方法并不好,因為表格里的操作太多了要把它們一一繪制工作量實在是太大了,而且很麻煩,我這里把表格的一些常用到的功能實現了預覽,還有想表格里放圖片什么的都沒有實現,如果有需要,可以根據需求再進行進行寫。
我寫的其實還有一點bug,單元格的邊框樣式我只設置了solid和dashed,但事實上excel中單元格的邊框有12種樣式,而且還有對角線邊框,設置起來好麻煩,我就不弄了,大家用的時候注意一下哈,有需要的話可以自己修改一下!
附上全部代碼:
/**
* exceljs + handsontable
*/
import Excel from 'exceljs'
import { useState } from 'react';
import { HotTable } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.css';
import { textRenderer, registerRenderer } from 'handsontable/renderers';
import {XMLParser} from 'fast-xml-parser'
import Color from 'color';
// 注冊模塊
registerAllModules();
// 根據主題和明暗度獲取顏色
const getThemeColor = (themeJson, themeId, tint) => {
let color = '';
const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
switch (themeId) {
case 0:
color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
break;
case 1:
color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
break;
case 2:
color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
break;
case 3:
color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
break;
default:
color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
break;
}
// 根據tint修改顏色深淺
color = '#' + color;
const colorObj = Color(color);
if(tint){
if(tint>0){// 淡色
color = colorObj.lighten(tint).hex();
}else{ // 深色
color = colorObj.darken(Math.abs(tint)).hex();
}
}
return color;
}
// 獲取顏色
const getColor = (obj, themeJson) => {
if('argb' in obj){ // 標準色
// rgba格式去掉前兩位: FFFF0000 -> FF0000
return '#' + obj.argb.substring(2);
}else if('theme' in obj){ // 主題顏色
if('tint' in obj){
return getThemeColor(themeJson, obj.theme, obj.tint);
}else{
return getThemeColor(themeJson, obj.theme, null);
}
}
}
// 設置邊框
const setBorder = (style) =>{
let borderStyle = 'solid';
let borderWidth = '1px';
switch (style) {
case 'thin':
borderWidth = 'thin';
break;
case 'dotted':
borderStyle = 'dotted';
break;
case 'dashDot':
borderStyle = 'dashed';
break;
case 'hair':
borderStyle = 'solid';
break;
case 'dashDotDot':
borderStyle = 'dashed';
break;
case 'slantDashDot':
borderStyle = 'dashed';
break;
case 'medium':
borderWidth = '2px';
break;
case 'mediumDashed':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'mediumDashDotDot':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'mdeiumDashDot':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'double':
borderStyle = 'double';
break;
case 'thick':
borderWidth = '3px';
break;
default:
break;
}
// console.log(borderStyle, borderWidth);
return [borderStyle, borderWidth];
}
export default function XLSXPreView() {
// 表格數據
const [data, setData] = useState([]);
// 表格
const [sheet, setSheet] = useState([]);
// 主題
const [themeJson, setThemeJson] = useState([]);
// 合并的單元格
const [mergeRanges, setMergeRanges] = useState([]);
registerRenderer('customStylesRenderer', (hotInstance, td, row, column, prop, value, cellProperties) => {
textRenderer(hotInstance, td, row, column, prop, value, cellProperties);
// console.log(cellProperties);
// 填充樣式
if('fill' in cellProperties){
// 背景顏色
if('fgColor' in cellProperties.fill && cellProperties.fill.fgColor){
td.style.background = getColor(cellProperties.fill.fgColor, themeJson);
}
}
// 字體樣式
if('font' in cellProperties){
// 加粗
if('bold' in cellProperties.font && cellProperties.font.bold){
td.style.fontWeight = '700';
}
// 字體顏色
if('color' in cellProperties.font && cellProperties.font.color){
td.style.color = getColor(cellProperties.font.color, themeJson);
}
// 字體大小
if('size' in cellProperties.font && cellProperties.font.size){
td.style.fontSize = cellProperties.font.size + 'px';
}
// 字體類型
if('name' in cellProperties.font && cellProperties.font.name){
td.style.fontFamily = cellProperties.font.name;
}
// 字體傾斜
if('italic' in cellProperties.font && cellProperties.font.italic){
td.style.fontStyle = 'italic';
}
// 下劃線
if('underline' in cellProperties.font && cellProperties.font.underline){
// 其實還有雙下劃線,但是雙下劃綫css中沒有提供直接的設置方式,需要使用額外的css設置,所以我也就先懶得弄了
td.style.textDecoration = 'underline';
// 刪除線
if('strike' in cellProperties.font && cellProperties.font.strike){
td.style.textDecoration = 'underline line-through';
}
}else{
// 刪除線
if('strike' in cellProperties.font && cellProperties.font.strike){
td.style.textDecoration = 'line-through';
}
}
}
// 對齊
if('alignment' in cellProperties){
if('horizontal' in cellProperties.alignment){ // 水平
// 這里我直接用handsontable內置類做了,設置成類似htLeft的樣子。
//(handsontable)其實至支持htLeft, htCenter, htRight, htJustify四種,但是其是它還有centerContinuous、distributed、fill,遇到這幾種就會沒有效果,也可以自己設置,但是我還是懶的弄了,用到的時候再說吧
const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1);
td.classList.add(`ht${name}`);
}
if('vertical' in cellProperties.alignment){ // 垂直
// 這里我直接用handsontable內置類做了,設置成類似htTop的樣子。
const name = cellProperties.alignment.vertical.charAt(0).toUpperCase() + cellProperties.alignment.vertical.slice(1);
td.classList.add(`ht${name}`);
}
}
// 邊框
if('border' in cellProperties){
if('left' in cellProperties.border && cellProperties.border.left){// 左邊框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.left.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.left.color){
color = getColor(cellProperties.border.left.color, themeJson);
}
td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
}
if('right' in cellProperties.border && cellProperties.border.right){// 左邊框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.right.style);
// console.log(row, column, borderWidth, borderStyle);
let color = '';
if(cellProperties.border.right.color){
color = getColor(cellProperties.border.right.color, themeJson);
}
td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
}
if('top' in cellProperties.border && cellProperties.border.top){// 左邊框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.top.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.top.color){
color = getColor(cellProperties.border.top.color, themeJson);
}
td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
}
if('bottom' in cellProperties.border && cellProperties.border.bottom){// 左邊框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.bottom.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.bottom.color){
color = getColor(cellProperties.border.bottom.color, themeJson);
}
td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
}
}
});
const handleFile = async (e) => {
const file = e.target.files[0];
const workbook = new Excel.Workbook();
await workbook.xlsx.load(file)
const worksheet = workbook.getWorksheet(1);
// const sheetRows = worksheet.getRows(1, worksheet.rowCount);
setSheet(worksheet)
// console.log(worksheet.getCell(1, 1).style);
// 遍歷工作表中的所有行(包括空行)
const sheetData = [];
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
// console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
// 使用row.values獲取每一行的值時總會多出一條空數據(第一條),這里我把它刪除
const row_values = row.values.slice(1);
sheetData.push(row_values)
});
setData(sheetData);
// 將主題xml轉換成json
const themeXml = workbook._themes.theme1;
const options = {
ignoreAttributes: false,
attributeNamePrefix: '_'
}
const parser = new XMLParser(options);
const json = parser.parse(themeXml)
setThemeJson(json);
// 獲取合并的單元格
const mergeCells = [];
for(let i in worksheet._merges){
const {top, left, bottom, right} = worksheet._merges[i].model;
mergeCells.push({ row: top-1, col: left-1, rowspan: bottom-top+1 , colspan: right-left+1})
}
setMergeRanges(mergeCells)
console.log(worksheet);
}
return (
<>
<input type="file" onChange={handleFile}/>
<div id='table_view'>
<HotTable
data={data}
readOnly={true}
rowHeaders={true}
colHeaders={true}
width="100vw"
height="auto"
licenseKey='non-commercial-and-evaluation'
rowHeights={function(index) {
if(sheet.getRow(index+1).height){
// exceljs獲取的行高不是像素值,事實上,它是23px - 13.8 的一個映射。所以需要將它轉化為像素值
return sheet.getRow(index+1).height * (23 / 13.8);
}
return 23;// 默認
}}
colWidths={function(index){
if(sheet.getColumn(index+1).width){
// exceljs獲取的列寬不是像素值,事實上,它是81px - 8.22 的一個映射。所以需要將它轉化為像素值
return sheet.getColumn(index+1).width * (81 / 8.22);
}
return 81;// 默認
}}
cells={(row, col, prop) => {
const cellProperties = {};
const cellStyle = sheet.getCell(row+1, col+1).style
if(JSON.stringify(cellStyle) !== '{}'){
// console.log(row+1, col+1, cellStyle);
for(let key in cellStyle){
cellProperties[key] = cellStyle[key];
}
}
return {...cellProperties, renderer: 'customStylesRenderer'};
}}
mergeCells={mergeRanges}
/>
</div>
</>
)
}總結
到此這篇關于前端實現(excel)xlsx文件預覽的文章就介紹到這了,更多相關前端實現excel文件預覽內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript在form表單中使用button按鈕實現submit提交方法
在form表單提交中,使用button來間接完成submit的提交更為靈活。接下來通過本文給大家介紹JavaScript在form表單中使用button按鈕實現submit提交方法,需要的朋友參考下吧2017-01-01
Uniapp微信小程序實現全局事件監(jiān)聽并進行數據埋點的方法
niapp起源?uni-app是一個使用Vue.js開發(fā)所有前端應用的框架,下面這篇文章主要給大家介紹了關于Uniapp微信小程序實現全局事件監(jiān)聽并進行數據埋點的相關資料,需要的朋友可以參考下2022-11-11
JavaScript之class繼承_動力節(jié)點Java學院整理
這篇文章主要介紹了JavaScript之class繼承,新的關鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單,有興趣的可以了解一下2017-07-07
在javascript將NodeList作為Array數組處理的方法
在Web前端編程中,我們通常會通過document.getElementsByTagName的方法取出一組相同標簽的dom元素。2010-07-07
ES2020讓代碼更優(yōu)美的運算符 (?.) (??)
這篇文章主要介紹了ES2020讓代碼更優(yōu)美的運算符 (?.) (??),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01
原生JavaScript實現的簡單省市縣三級聯(lián)動功能示例
這篇文章主要介紹了原生JavaScript實現的簡單省市縣三級聯(lián)動功能,結合完整實例形式分析了javascript聯(lián)動菜單的實現方法,涉及javascript事件響應及頁面元素動態(tài)操作相關實現技巧,需要的朋友可以參考下2017-05-05

