前端實(shí)現(xiàn)(excel)xlsx文件預(yù)覽的詳細(xì)步驟
1. 概述
接到一個(gè)任務(wù),是要前端實(shí)現(xiàn)文件預(yù)覽效果,百度了一圈,發(fā)現(xiàn)也沒有什么好的方法可以完美的將表格渲染出來(lái)。在前端中有sheetjs和exceljs可以對(duì)xlsx文件進(jìn)行解析,本來(lái)一開始我用的是sheetjs,但是在樣式獲取上遇到了麻煩,所以我改用了exceljs,不過(guò)很難受,在樣式獲取時(shí)同樣遇到了不小的麻煩,但是我懶得換回sheetjs了,那就直接使用exceljs吧。
要實(shí)現(xiàn)xlsx文件預(yù)覽效果,我的想法是使用一個(gè)庫(kù)對(duì)xlsx文件進(jìn)行解析,然后使用另一個(gè)庫(kù)對(duì)解析出來(lái)的數(shù)據(jù)在頁(yè)面上進(jìn)行繪制,綜上,我采用的方案是:exceljs+handsontable
2. 實(shí)現(xiàn)步驟
2.1 安裝庫(kù)
使用命令: npm i exceljs handsontable @handsontable/react
2.2 使用exceljs解析數(shù)據(jù)并使用handsontable進(jìn)行渲染
直接貼代碼了:
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';
// 注冊(cè)模塊
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)
// 第一個(gè)工作表
const worksheet = workbook.getWorksheet(1);
// 遍歷工作表中的所有行(包括空行)
const sheetData = [];
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
// console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
// 使用row.values獲取每一行的值時(shí)總會(huì)多出一條空數(shù)據(jù)(第一條),這里我把它刪除
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'// 一定得加這個(gè),handsontable是收費(fèi)的,加了這個(gè)才能免費(fèi)用
/>
</div>
</>
)
}
到這里,已經(jīng)實(shí)現(xiàn)了從xlsx文件中獲取數(shù)據(jù),并使用handsontable將表格中的數(shù)據(jù)渲染出來(lái),示例結(jié)果如下,如果只需要將數(shù)據(jù)顯示出來(lái),并不需要將樣式什么的一起復(fù)現(xiàn)了,那到這里就已經(jīng)結(jié)束了!

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

但是實(shí)際操作的時(shí)候卻遇到了問(wèn)題,先說(shuō)excel中的顏色,在選擇顏色時(shí),應(yīng)該都會(huì)打開下面這個(gè)選項(xiàng)框吧,如果你選擇的是標(biāo)準(zhǔn)色,它獲取到的顏色就是十六進(jìn)制,但是如果你選擇主題中的顏色,那就是另一種結(jié)果了,并且還會(huì)有不同的深暗程度tint,這就很難受了!

隨后在控制臺(tái)中打印了workbook,發(fā)現(xiàn)它把主題返回了,可以通過(guò)work._themes.theme1獲取,不過(guò)獲取到的是xml格式的字符串,由于xml我沒學(xué),我不會(huì),所以我就把它轉(zhuǎn)換成json來(lái)進(jìn)行處理了。
第一步
安裝xml轉(zhuǎn)json的庫(kù): npm i fast-xml-parser
import {XMLParser} from 'fast-xml-parser'
// 將主題xml轉(zhuǎn)換成json
const themeXml = workbook._themes.theme1;
const options = {
ignoreAttributes: false,
attributeNamePrefix: '_'
}
const parser = new XMLParser(options);
const json = parser.parse(themeXml)
setThemeJson(json);
其實(shí)它的theme好像是固定的,也可以在一些格式轉(zhuǎn)換的網(wǎng)站中直接轉(zhuǎn)換成json然后放到一個(gè)json文件中,讀取就行,我這里就直接放到一個(gè)state中了!
第二步
接下來(lái)就是重頭戲了!設(shè)置單元格樣式…
首先安裝一個(gè)處理顏色的庫(kù)color,用來(lái)根據(jù)tint獲得不同明暗程度的顏色: npm i color
下面是獲取顏色的函數(shù):
// 根據(jù)主題和明暗度獲取顏色
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;
}
// 根據(jù)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){ // 標(biāo)準(zhǔn)色
// 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);
}
}
}
然后設(shè)置handonsontable的單元格的一些樣式:顏色、加粗、下劃線、邊框balabala…的
順帶把行高和列寬一起設(shè)置了,這個(gè)還比較簡(jiǎn)單,就一筆帶過(guò)了…
3.2 合并單元格
從獲取到的sheet中有一個(gè)_meages屬性,該屬性中存放了表格中所有的合并單元格區(qū)域,所以只需要將它們重新渲染在handsontable中就好。

然后就實(shí)現(xiàn)了表格的一些基本功能的預(yù)覽,結(jié)果如下圖:

3. 總結(jié)(附全代碼)
其實(shí)這個(gè)的本質(zhì)主要就是通過(guò)ecxeljs解析表格文件的數(shù)據(jù),然后通過(guò)handsontable將它們重新繪制在頁(yè)面上,個(gè)人覺得這種方法并不好,因?yàn)楸砀窭锏牟僮魈嗔艘阉鼈円灰焕L制工作量實(shí)在是太大了,而且很麻煩,我這里把表格的一些常用到的功能實(shí)現(xiàn)了預(yù)覽,還有想表格里放圖片什么的都沒有實(shí)現(xiàn),如果有需要,可以根據(jù)需求再進(jìn)行進(jìn)行寫。
我寫的其實(shí)還有一點(diǎn)bug,單元格的邊框樣式我只設(shè)置了solid和dashed,但事實(shí)上excel中單元格的邊框有12種樣式,而且還有對(duì)角線邊框,設(shè)置起來(lái)好麻煩,我就不弄了,大家用的時(shí)候注意一下哈,有需要的話可以自己修改一下!
附上全部代碼:
/**
* 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';
// 注冊(cè)模塊
registerAllModules();
// 根據(jù)主題和明暗度獲取顏色
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;
}
// 根據(jù)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){ // 標(biāo)準(zhǔn)色
// 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);
}
}
}
// 設(shè)置邊框
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() {
// 表格數(shù)據(jù)
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){
// 其實(shí)還有雙下劃線,但是雙下劃綫css中沒有提供直接的設(shè)置方式,需要使用額外的css設(shè)置,所以我也就先懶得弄了
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';
}
}
}
// 對(duì)齊
if('alignment' in cellProperties){
if('horizontal' in cellProperties.alignment){ // 水平
// 這里我直接用handsontable內(nèi)置類做了,設(shè)置成類似htLeft的樣子。
//(handsontable)其實(shí)至支持htLeft, htCenter, htRight, htJustify四種,但是其是它還有centerContinuous、distributed、fill,遇到這幾種就會(huì)沒有效果,也可以自己設(shè)置,但是我還是懶的弄了,用到的時(shí)候再說(shuō)吧
const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1);
td.classList.add(`ht${name}`);
}
if('vertical' in cellProperties.alignment){ // 垂直
// 這里我直接用handsontable內(nèi)置類做了,設(shè)置成類似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獲取每一行的值時(shí)總會(huì)多出一條空數(shù)據(jù)(第一條),這里我把它刪除
const row_values = row.values.slice(1);
sheetData.push(row_values)
});
setData(sheetData);
// 將主題xml轉(zhuǎn)換成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獲取的行高不是像素值,事實(shí)上,它是23px - 13.8 的一個(gè)映射。所以需要將它轉(zhuǎn)化為像素值
return sheet.getRow(index+1).height * (23 / 13.8);
}
return 23;// 默認(rèn)
}}
colWidths={function(index){
if(sheet.getColumn(index+1).width){
// exceljs獲取的列寬不是像素值,事實(shí)上,它是81px - 8.22 的一個(gè)映射。所以需要將它轉(zhuǎn)化為像素值
return sheet.getColumn(index+1).width * (81 / 8.22);
}
return 81;// 默認(rèn)
}}
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>
</>
)
}總結(jié)
到此這篇關(guān)于前端實(shí)現(xiàn)(excel)xlsx文件預(yù)覽的文章就介紹到這了,更多相關(guān)前端實(shí)現(xiàn)excel文件預(yù)覽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用location.hash實(shí)現(xiàn)跨域iframe自適應(yīng)
其他一些類似js跨域操作問(wèn)題也可以按這個(gè)思路去解決,需要的朋友可以測(cè)試下。2010-05-05
關(guān)于驗(yàn)證碼在IE中不刷新的快速解決方法
下面小編就為大家?guī)?lái)一篇關(guān)于驗(yàn)證碼在IE中不刷新的快速解決方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
JS實(shí)現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問(wèn)題解決詳解
這篇文章主要為大家詳細(xì)介紹了JavaScript如何調(diào)用后臺(tái)接口實(shí)現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問(wèn)題的解決辦法,需要的小伙伴可以參考一下2023-07-07
JavaScript獲取flash對(duì)象與網(wǎng)上的有所不同
關(guān)于js獲取flash對(duì)象,網(wǎng)上有非常多的例子,但不是我想要的,經(jīng)測(cè)試整理,因此本文誕生了2014-04-04
利用js判斷手機(jī)是否安裝某個(gè)app的多種方案
這篇文章主要介紹了利用js檢測(cè)手機(jī)是否安裝某個(gè)app的多種方案,當(dāng)判斷后如果安裝了直接打開,如果有沒有安裝將自動(dòng)跳轉(zhuǎn)到下載的界面,有需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-02-02
javascript框架設(shè)計(jì)之瀏覽器的嗅探和特征偵測(cè)
這篇文章主要介紹了javascript框架設(shè)計(jì)之瀏覽器的嗅探和特征偵測(cè)的相關(guān)資料,需要的朋友可以參考下2015-06-06
JS前端框架關(guān)于重構(gòu)的失敗經(jīng)驗(yàn)分享
關(guān)于重構(gòu)JS前端框架的失敗經(jīng)驗(yàn)接下來(lái)與大家分享一下,感興趣的你可不要錯(cuò)過(guò)了哈,畢竟是經(jīng)驗(yàn)之談哈2013-03-03

