使用ExcelJS实现excel的前端导入导出功能(Vue3+TS)

侃侃vae 2024-09-11 16:33:01 阅读 62

公司项目需要导出表格为报表,尝试了xlsx、sheetjs以及js-xlsx各种库,但是总有不满足需求的点(比如设置行高列宽、居中、合并单元格、添加背景色等等)或者API太少写起来太繁琐,最后找到ExcelJS能够实现所有需求。

本人简书链接:https://www.jianshu.com/p/0c688c97719e

(csdn抄袭狗是真的恶心https://blog.csdn.net/weixin_50543490/article/details/131207514)

ExcelJS:读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。一个 Excel 电子表格文件逆向工程项目。

github中文文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md

我把用到的exceljs的导入导出功能按照需求封装成一个类,具体可以根据自己的项目需求去调整。

一、定义Excel类

<code>export class Excel { -- -->

blob?: Blob; // 导入的blob文件

worksheet?: ExcelJS.Worksheet; // 当前工作表

header: string[]; // 表头字段数组

constructor(blob?: Blob) {

this.blob = blob;

this.worksheet = undefined;

this.header = [];

}

}

二、导入excel方法

/**

* @description: blob转ArrayBuffer(用于后续生成文件数据)

* @return {Promise<ArrayBuffer>} ArrayBuffer

*/

private readFile(): Promise<ArrayBuffer> {

return new Promise<ArrayBuffer>((resolve, reject) => {

let reader = new FileReader();

if (!this.blob) {

reject('上传文件异常!');

} else {

reader.readAsArrayBuffer(this.blob);

reader.onload = (ev) => {

resolve(ev.target!.result as ArrayBuffer);

};

}

});

}

/**

* @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档)

* @return {Promise<ExcelJS.Workbook>}

*/

public async getWorkBook(): Promise<ExcelJS.Workbook> {

let buffer = await this.readFile();

const workbook = new ExcelJS.Workbook();

return await workbook.xlsx.load(buffer);

}

/**

* @description: 将 excel 第i张工作表的数据转为对象数据

* @param {number} i 工作表序号

* @param {string[]} header 表头字段数组

* @return {Promise<Record<(typeof header)[number], string>[]>}

* 传入表头作为字段的对象数组(每个元素对象对应每一行)

*/

public async importExcel(options: importExcelType): Promise<Record<(typeof header)[number], string>[]> {

const { i = 1, header } = options;

const workbook = await this.getWorkBook();

const worksheet = workbook.getWorksheet(i);

// excel导入后返回的数组

const excelList: Record<(typeof header)[number], string>[] = [];

worksheet.getSheetValues().filter(temp => !!temp?.length).forEach(item => {

// 移除空行

// 移除每行首个空元素

(item as string[]).shift();

// 定义临时对象存储每一行内容

let tempObj: Record<(typeof header)[number], string> = { };

(item as string[]).forEach((item2, index2) => {

tempObj[header[index2]] = item2;

})

excelList.push(tempObj);

})

return excelList

}

三、导出excel方法

/**

* @description: 导出excel,参数信息参考exceljs

* @param {*} data 数据

* @param {*} name 文件名

* @param {*} header 表头字段

* @param {*} customHeader 表头字段对应中文

* @param {*} sheetName 工作表名

* @param {*} title 标题

* @param {*} subTitle 副标题(日期)

* @param {*} password 冻结表格密码

* @param {*} mergeList 合并单元格数组

* @param {*} titleStyle 标题样式(按需补充方法)

* @param {*} subTitleStyle 小标题样式(按需补充方法)

* @param {*} headerStyle 表头字段样式(按需补充方法)

* @param {*} cellStyle 单元格样式(按需补充方法)

* @return {*}

*/

public async exportExcel(options: exportExcelType): Promise<void> {

const {

data,

name,

header,

customHeader,

sheetName = 'sheet1',

title = '',

subTitle = '',

password = '',

mergeList = [],

titleStyle,

subTitleStyle,

headerStyle,

cellStyle

} = options;

// 创建工作簿

const workbook = new ExcelJS.Workbook();

workbook.creator = '侃侃';

workbook.created = new Date();

// 添加sheet

this.worksheet = workbook.addWorksheet(sheetName, { properties: { tabColor: { argb: 'FF00FF00' } } });

this.header = header;

// 表头行序号

let headerRowId = 1;

if (!!title) headerRowId++;

if (!!subTitle) headerRowId++;

// 插入单元格数据

this.setCells(data, customHeader, cellStyle);

// 插入大标题

this.getTitle(title, titleStyle);

// 插入小标题

this.getSubTitle(subTitle, subTitleStyle);

// 处理表头

this.setHeaderStyle(headerRowId, data, headerStyle);

// 更多处理

this.handleDealExcel(password, mergeList, headerRowId);

// 导出excel(此处也可用file-saver将blob导出到excel文件 fs.saveAs(blob, name+'.xlsx');)

workbook.xlsx.writeBuffer().then((data) => {

let blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });

const a = document.createElement('a');

a.href = URL.createObjectURL(blob);

a.download = name + '.xlsx';

document.body.appendChild(a);

a.click();

document.body.removeChild(a);

window.URL.revokeObjectURL(a.href);

});

}

四、导出数据及格式方法汇总(按需补充)

1.插入单元格数据

/**

* @description:单元格数据处理

* @param {any} data 表格数据

* @param {string} customHeader 表头中文字段

* @return {*}

*/

private setCells(data: exportExcelType['data'], customHeader: string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>): void {

// 设置列,插入中文表头

const column: Partial<ExcelJS.Column>[] = [];

this.header.forEach((item, index) => {

column.push({

header: customHeader[index],

key: item,

width: style?.width || 25,

})

})

this.worksheet!.columns = column;

// 设置行,添加数据

this.worksheet?.addRows(data);

// 设置行高

this.worksheet?.eachRow({ includeEmpty: true }, (row, rowNumber) => {

row.height = style?.height || 20;

})

// 获取每一列数据,再依次对齐

this.worksheet!.columns.forEach(column => column.alignment = style?.alignment || { vertical: 'middle', horizontal: 'center', wrapText: true })

}

2.添加大标题

/**

* @description: 添加大标题

* @param {string} title 标题

* @param {any} style 标题样式

* @return {*}

*/

private getTitle(title: string, style?: Partial<ExcelJS.Row>): void {

if (!!title) {

// 插入标题

this.worksheet?.spliceRows(1, 0, [title]);

this.worksheet?.mergeCells(1, 1, 1, this.header.length);

// 调整标题样式

const titleRow = this.worksheet!.getRow(1);

// 高度

titleRow.height = style?.height || 40;

// 字体设置

titleRow.font = style?.font || {

size: 20,

bold: true

}

// 背景色

titleRow.fill = style?.fill || {

bgColor: {

'argb': 'FFFFFF00'

},

type: 'pattern',

pattern: 'none'

}

// 对齐方式

titleRow.alignment = style?.alignment || {

horizontal: "center",

vertical: "middle"

};

}

}

3.添加小标题

/**

* @description: 添加小标题

* @param {string} subTitle 标题

* @param {Partial} style 小标题样式

* @return {*}

*/

private getSubTitle(subTitle: string, style?: Partial<ExcelJS.Row>): void {

if (!!subTitle) {

this.worksheet?.spliceRows(2, 0, [subTitle]);

this.worksheet?.mergeCells(2, 1, 2, this.header.length);

// 调整标题样式

const subtitleRow = this.worksheet!.getRow(2);

// 高度

subtitleRow.height = style?.height || 20;

// 字体设置

subtitleRow.font = style?.font || {

size: 14,

}

// 背景色

subtitleRow.fill = style?.fill || {

bgColor: {

'argb': 'FFFFFF00'

},

type: 'pattern',

pattern: 'none'

}

// 对齐方式

subtitleRow.alignment = style?.alignment || {

horizontal: "right",

vertical: "middle"

};

}

}

4.设置表头样式

/**

* @description: 设置表头样式

* @param {number} num 表头在第几行

* @param {number} data 总数据

* @param {number} style 表头样式

* @return {*}

*/

private setHeaderStyle(num: number, data: any, style?: Partial<ExcelJS.Row>): void {

// 自动筛选器

this.worksheet!.autoFilter = {

from: {

row: num,

column: 1

},

to: {

row: data.length,

column: this.header.length

}

}

// 给表头添加背景色

let headerRow = this.worksheet!.getRow(num);

headerRow!.height = style?.height || 30;

// 通过 cell 设置背景色,更精准

headerRow?.eachCell((cell) => {

cell.fill = style?.fill || {

type: 'pattern',

pattern: 'solid',

fgColor: { argb: 'dde0e7' },

}

cell.font = style?.font || {

size: 12

}

})

}

其他处理

/**

* @description: 其他内容处理

* @param {string} password 密码

* @param {mergeListType} mergeList 合并行列数组

* @param {number} headerRowId 表头行序号

* @return {*}

*/

private handleDealExcel(password: string, mergeList: mergeListType[], headerRowId: number) {

// 添加工作表保护

if (!!password) {

this.worksheet?.protect(password, {

autoFilter: true,

selectLockedCells: false,

});

}

// 合并单元格

mergeList.forEach(item => {

// 行数为表格数据所在行行数+表头行序号headerRowId

const startRow = item.startRow + headerRowId;

const endRow = item.endRow + headerRowId;

this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn);

})

// 冻结前几行

this.worksheet!.views = [

{ state: 'frozen', xSplit: 0, ySplit: headerRowId }

];

}

6.合并单元格结构处理方法

/**

* @description: 合并单元格数组

* @param {Partial} merge1 需要合并的行/列起止对象数组1

* 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}]

* @param {Partial} merge2 需要合并的列/行起止对象数组2

* 例:[{ startColumn: 1, endColumn: 2 }]

* @return {*} mergeArr 合并后数组

* 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}]

*/

public merge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[] {

const mergeArr: any[] = [];

merge1.forEach(item1 => {

mergeArr.push(...merge2.map(item2 => {

return { ...item2, ...item1 };

}))

})

return mergeArr;

}

五、具体使用:

1.导入excel

// import.vue

<template>

<div class="import">code>

<el-upload

ref="uploadExcelRef"code>

action=""code>

drag

:auto-upload="false"code>

:on-change="uploadChange"code>

:limit="1"code>

>

<i class="el-icon-upload"></i>code>

<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>code>

</el-upload>

</div>

</template>

<script setup lang="ts">code>

import { -- --> Excel } from '@/utils/excel';

import { ElMessage } from 'element-plus';

import { ref } from 'vue';

// 上传组件ref

const uploadExcelRef = ref();

const uploadChange = async (file: any) => {

// 表头字段数组

const header = ['id', 'name', 'age'];

if (file.raw?.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'

&& file.raw?.type !== 'application/vnd.ms-excel') {

ElMessage.error('文件格式错误,请重新上传!')

// 移除上传的文件

uploadExcelRef.value!.handleRemove(file);

return false

}

// 对excel文件进行处理

try {

const excel = new Excel(file.raw);

// 导入文件获取数据

const res = await excel.importExcel({ header });

console.log(res);

} catch (err) {

console.log(err);

}

}

</script>

excel文件

Snipaste_2022-11-17_18-34-04.png

最终导入生成数据:

<code> [

{ -- --> id: "id", name: "姓名", age: "年龄" },

{ id: 1, name: "张三", age: 18 },

{ id: 2, name: "李四", age: 20 },

{ id: 3, name: "王五", age: 24 },

{ id: 4, name: "赵六", age: 26 },

{ id: 5, name: "刘七", age: 28 }

]

2.导出excel

// export.vue

<template>

<div class="export">code>

<button @click="exportExcel">导出</button>code>

</div>

</template>

<script setup lang="ts">code>

import { -- --> Excel, type mergeListType } from '@/utils/excel';

import { reactive } from 'vue';

const tableData = reactive([

{ id: 1, name: "张三", age: 18 },

{ id: 2, name: "张三", age: 18 },

{ id: 3, name: "王五", age: 24 },

{ id: 4, name: "赵六", age: 24 },

{ id: 5, name: "刘七", age: 24 }

])

const header = ['id', 'name', 'age'];

const customHeader = ['id', '姓名', '年龄'];

const mergeList: mergeListType[] = reactive([

{ startRow: 1, endRow: 2, startColumn: 3, endColumn: 3 },

{ startRow: 3, endRow: 5, startColumn: 3, endColumn: 3 }

])

const exportExcel = () => {

const excel = new Excel();

excel.exportExcel({

name: '个人信息',

title: '标题',

data: tableData,

header,

customHeader,

mergeList

})

}

</script>

最终生成的excel:

Snipaste_2022-11-17_18-55-21.png

六、excel.ts完整代码

<code>import ExcelJS from 'exceljs'

// 导入参数数据类型

export interface importExcelType { -- -->

/**

* 第 i 张工作表

*/

i?: number;

/**

* 表格表头字段数组

*/

header: readonly any[];

}

// 导出参数数据类型

export interface exportExcelType {

/**

* 数据

*/

data: { [key: string]: any }[];

/**

* 文件名

*/

name: string;

/**

* 表头字段

*/

header: string[];

/**

* 表头字段对应中文

*/

customHeader: string[];

/**

* 工作表名

*/

sheetName?: string;

/**

* 标题

*/

title?: string;

/**

* 小标题

*/

subTitle?: string;

/**

* 工作表保护密码

*/

password?: string;

/**

* 对齐方式

*/

alignment?: Partial<ExcelJS.Alignment>;

/**

* 合并单元格

*/

mergeList?: mergeListType[];

/**

* 标题样式

*/

titleStyle?: Partial<ExcelJS.Row>;

/**

* 小标题样式

*/

subTitleStyle?: Partial<ExcelJS.Row>;

/**

* 表头样式

*/

headerStyle?: Partial<ExcelJS.Row>;

/**

* 单元格统一样式

*/

cellStyle?: Partial<ExcelJS.Row & ExcelJS.Column>;

}

// 合并单元格数据类型

export interface mergeListType {

startRow: number;

startColumn: number;

endRow: number;

endColumn: number;

}

// exceljs相关方法

export class Excel {

blob?: Blob; // 导入的blob文件

worksheet?: ExcelJS.Worksheet; // 当前工作表

header: string[]; // 表头字段数组

constructor(blob?: Blob) {

this.blob = blob;

this.worksheet = undefined;

this.header = [];

}

/**

* @description: blob转ArrayBuffer(用于后续生成文件数据)

* @return {Promise<ArrayBuffer>} ArrayBuffer

*/

private readFile(): Promise<ArrayBuffer> {

return new Promise<ArrayBuffer>((resolve, reject) => {

let reader = new FileReader();

if (!this.blob) {

reject('上传文件异常!');

} else {

reader.readAsArrayBuffer(this.blob);

reader.onload = (ev) => {

resolve(ev.target!.result as ArrayBuffer);

};

}

});

}

/**

* @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档)

* @return {Promise<ExcelJS.Workbook>}

*/

public async getWorkBook(): Promise<ExcelJS.Workbook> {

let buffer = await this.readFile();

const workbook = new ExcelJS.Workbook();

return await workbook.xlsx.load(buffer);

}

/**

* @description: 获取第i张工作表

* @param {number} i 工作表序号

* @return {Promise<ExcelJS.Worksheet>} 返回第 i 个工作表

*/

public async getWorkSheet(i: number = 0): Promise<ExcelJS.Worksheet> {

const workbook = await this.getWorkBook();

return workbook.getWorksheet(i);

}

/**

* @description: 将 excel 第i张工作表的数据转为对象数据

* @param {number} i 工作表序号

* @param {string[]} header 表头字段数组

* @return {Promise<Record<(typeof header)[number], string>[]>} 传入表头作为字段的对象数组(每个元素对象对应每一行)

*/

public async importExcel(options: importExcelType): Promise<Record<(typeof header)[number], string>[]> {

const { i = 1, header } = options;

const workbook = await this.getWorkBook();

const worksheet = workbook.getWorksheet(i);

// excel导入后返回的数组

const excelList: Record<(typeof header)[number], string>[] = [];

worksheet.getSheetValues().filter(temp => !!temp?.length).forEach(item => { // 移除空行

// 移除每行首个空元素

(item as string[]).shift();

// 定义临时对象存储每一行内容

let tempObj: Record<(typeof header)[number], string> = { };

(item as string[]).forEach((item2, index2) => {

tempObj[header[index2]] = item2;

})

excelList.push(tempObj);

})

return excelList

}

/**

* @description: 导出excel,参数信息参考exceljs

* @param {*} data 数据

* @param {*} name 文件名

* @param {*} header 表头字段

* @param {*} customHeader 表头字段对应中文

* @param {*} sheetName 工作表名

* @param {*} title 标题

* @param {*} subTitle 副标题(日期)

* @param {*} password 冻结表格密码

* @param {*} mergeList 合并单元格数组

* @param {*} titleStyle 标题样式(按需补充方法)

* @param {*} subTitleStyle 小标题样式(按需补充方法)

* @param {*} headerStyle 表头字段样式(按需补充方法)

* @param {*} cellStyle 单元格样式(按需补充方法)

* @return {*}

*/

public async exportExcel(options: exportExcelType): Promise<void> {

const {

data,

name,

header,

customHeader,

sheetName = 'sheet1',

title = '',

subTitle = '',

password = '',

mergeList = [],

titleStyle,

subTitleStyle,

headerStyle,

cellStyle } = options;

// 创建工作簿

const workbook = new ExcelJS.Workbook();

workbook.creator = '侃侃';

workbook.created = new Date();

// 添加sheet

this.worksheet = workbook.addWorksheet(sheetName, { properties: { tabColor: { argb: 'FF00FF00' } } });

this.header = header;

// 表头行序号

let headerRowId = 1;

if (!!title) headerRowId++;

if (!!subTitle) headerRowId++;

// 插入单元格数据

this.setCells(data, customHeader, cellStyle);

// 插入大标题

this.getTitle(title, titleStyle);

// 插入小标题

this.getSubTitle(subTitle, subTitleStyle);

// 处理表头

this.setHeaderStyle(headerRowId, data, headerStyle);

// 更多处理

this.handleDealExcel(password, mergeList, headerRowId);

// 导出excel(此处也可用file-saver将blob导出到excel文件 fs.saveAs(blob, name+'.xlsx');)

workbook.xlsx.writeBuffer().then((data) => {

let blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });

const a = document.createElement('a');

a.href = URL.createObjectURL(blob);

a.download = name + '.xlsx';

document.body.appendChild(a);

a.click();

document.body.removeChild(a);

window.URL.revokeObjectURL(a.href);

});

}

/**

* @description: 合并单元格数组

* @param {Partial} merge1 需要合并的行/列起止对象数组1

* 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}]

* @param {Partial} merge2 需要合并的列/行起止对象数组2

* 例:[{ startColumn: 1, endColumn: 2 }]

* @return {*} mergeArr 合并后数组

* 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}]

*/

public merge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[] {

const mergeArr: any[] = [];

merge1.forEach(item1 => {

mergeArr.push(...merge2.map(item2 => {

return { ...item2, ...item1 };

}))

})

return mergeArr;

}

/**

* @description:单元格数据处理

* @param {any} data 表格数据

* @param {string} customHeader 表头中文字段

* @return {*}

*/

private setCells(data: exportExcelType['data'], customHeader: string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>): void {

// 设置列,插入中文表头

const column: Partial<ExcelJS.Column>[] = [];

this.header.forEach((item, index) => {

column.push({

header: customHeader[index],

key: item,

width: style?.width || 25,

})

})

this.worksheet!.columns = column;

// 设置行,添加数据

this.worksheet?.addRows(data);

// 设置行高

this.worksheet?.eachRow({ includeEmpty: true }, (row, rowNumber) => {

row.height = style?.height || 20;

})

// 获取每一列数据,再依次对齐

this.worksheet!.columns.forEach(column => {

column.alignment = style?.alignment || { vertical: 'middle', horizontal: 'center', wrapText: true }

})

}

/**

* @description: 添加大标题

* @param {string} title 标题

* @param {any} style 标题样式

* @return {*}

*/

private getTitle(title: string, style?: Partial<ExcelJS.Row>): void {

if (!!title) {

// 插入标题

this.worksheet?.spliceRows(1, 0, [title]);

this.worksheet?.mergeCells(1, 1, 1, this.header.length);

// 调整标题样式

const titleRow = this.worksheet!.getRow(1);

// 高度

titleRow.height = style?.height || 40;

// 字体设置

titleRow.font = style?.font || {

size: 20,

bold: true

}

// 背景色

titleRow.fill = style?.fill || {

bgColor: {

'argb': 'FFFFFF00'

},

type: 'pattern',

pattern: 'none'

}

// 对齐方式

titleRow.alignment = style?.alignment || {

horizontal: "center",

vertical: "middle"

};

}

}

/**

* @description: 添加小标题

* @param {string} subTitle 标题

* @param {Partial} style 小标题样式

* @return {*}

*/

private getSubTitle(subTitle: string, style?: Partial<ExcelJS.Row>): void {

if (!!subTitle) {

this.worksheet?.spliceRows(2, 0, [subTitle]);

this.worksheet?.mergeCells(2, 1, 2, this.header.length);

// 调整标题样式

const subtitleRow = this.worksheet!.getRow(2);

// 高度

subtitleRow.height = style?.height || 20;

// 字体设置

subtitleRow.font = style?.font || {

size: 14,

}

// 背景色

subtitleRow.fill = style?.fill || {

bgColor: {

'argb': 'FFFFFF00'

},

type: 'pattern',

pattern: 'none'

}

// 对齐方式

subtitleRow.alignment = style?.alignment || {

horizontal: "right",

vertical: "middle"

};

}

}

/**

* @description: 设置表头样式

* @param {number} num 表头在第几行

* @param {number} data 总数据

* @param {number} style 表头样式

* @return {*}

*/

private setHeaderStyle(num: number, data: any, style?: Partial<ExcelJS.Row>): void {

// 自动筛选器

this.worksheet!.autoFilter = {

from: {

row: num,

column: 1

},

to: {

row: data.length,

column: this.header.length

}

}

// 给表头添加背景色

let headerRow = this.worksheet!.getRow(num);

headerRow!.height = style?.height || 30;

// 通过 cell 设置背景色,更精准

headerRow?.eachCell((cell) => {

cell.fill = style?.fill || {

type: 'pattern',

pattern: 'solid',

fgColor: { argb: 'dde0e7' },

}

cell.font = style?.font || {

size: 12

}

})

}

/**

* @description: 其他内容处理

* @param {string} password 密码

* @param {mergeListType} mergeList 合并行列数组

* @param {number} headerRowId 表头行序号

* @return {*}

*/

private handleDealExcel(password: string, mergeList: mergeListType[], headerRowId: number) {

// 添加工作表保护

if (!!password) {

this.worksheet?.protect(password, {

autoFilter: true,

selectLockedCells: false,

});

}

// 合并单元格

mergeList.forEach(item => {

// 行数为表格数据所在行行数+表头行序号headerRowId

const startRow = item.startRow + headerRowId;

const endRow = item.endRow + headerRowId;

this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn);

})

// 冻结前几行

this.worksheet!.views = [

{ state: 'frozen', xSplit: 0, ySplit: headerRowId }

];

}

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。