第六期:如何通過知曉雲將數據表導出爲 Excel 文件

作者:知曉雲 - 小程序開發快人一步
來源:知曉課堂

在日常的工作中,常常需要根據運營需求對數據進行各種格式的處理和導出。導出後,不少人偏愛將數據放入 excel在進行處理。

一般來說,處理數據導出時需要對數據進行一些運算整理。在以前,處理的方式是在一臺獨立的服務器上跑腳本。

而現在有了知曉雲,不再需要維護服務器,直接寫代碼就能把相關事都都丟給雲函數。 本文將介紹通過知曉云云函數來實現將數據表導出爲 excel 文件的功能,並使用 webpack 和 mincloud 將代碼打包上傳到知曉雲。

技術棧:

一、項目搭建

項目文件結構:

export-excel-file
├── index.js
├── package.json
├── src
│ └── index.js
├── webpack.config.js
└── yarn.lock

項目搭建與雲函數代碼打包示例文檔基本一致。項目搭建好後,還需要安裝以下依賴(兩種安裝方式選其一即可):

// 使用 yarn 安裝  
yarn add node-xlsx mincloud  
// 使用 npm 安裝  
npm install --save node-xlsx minclou ` 

修改 deploy 腳本,如下:

// package.json  
...  
"scripts": {   "build": "webpack --mode production",  
   "predeploy": "npm run build",  
   "deploy": "mincloud deploy export-excel-file ../"  
 },  
 ...  

最終我們會使用以下兩個命令來部署和測試:

npm run deploy  // 部署到知曉雲  
mincloud invoke export-excel-file  // 測試已經部署到知曉雲上的雲函數 

二、將數據表導出爲 excel 文件

我們需要準備兩張表:

  • order: 訂單表 (新建字段:name, price)
  • export_task:導出任務記錄表 (新建字段:file_download_link)

知曉雲的雲函數調用有同步和異步兩種方式,同步調用的最大超時時間爲 5 s,異步調用的則爲 300 s。

假定 order 訂單表有十萬條數據,由於知曉雲單次拉取數據的最大限制爲 1000 條,所以需要分批獲取數據,加上後續可能需要對數據進行處理,所花費的時間將會超過 5 s,因此對該雲函數的調用將採用異步的方式。這時候就需要 export_task 導出任務記錄表來對導出任務進行管理了。

export_task 表對導出任務進行管理的流程如下:

  1. 調用雲函數時在 export_task 表中創建一條記錄 A,此時記錄 A 中的 file_download_link 字段值爲空,同時拿到記錄 A 的 id,記這個 id 爲 jobId

  2. 進行 order 表數據查詢,excel 文件生成,文件上傳等操作,拿到文件下載鏈接

  3. 之後根據 jobId 來更新第一步創建的記錄,保存文件下載鏈接到 file_download_link 字段中

  4. 更新完後就能在 export_task 表中拿到文件下載鏈接

通過上面的準備和分析,對導出 excel 文件操作分爲以下 4 個步驟:

  1. order 訂單表數據獲取

  2. 使用獲取的數據在雲函數環境下創建 excel 文件

  3. 將創建出的 excel 文件上傳到知曉雲

  4. 保存文件下載鏈接到 export_task 表中的 file_download_link 字段

完整代碼如下:

const fs = require('fs')
const xlsx = require('node-xlsx')


const EXPORT_DATA_CATEGORY_ID = '5c711e3119111409cdabe6f2'    // 文件上傳分類 id
const TABLE_ID = {
  order: 66666,         // 訂單表
  export_task: 66667,   // 導出任務記錄表
}


const TMP_FILE_NAME = '/tmp/result.xlsx'  // 本地臨時文件路徑,以 /tmp 開頭,具體請查看:https://doc.minapp.com/support/technical-notes.html (雲函數的臨時文件存儲)
const ROW_NAME = ['name', 'price']        // Excel 文件列名配置
const MAX_CONNECT_LIMIT = 5               // 最大同時請求數
const LIMIT = 1000                        // 單次最大拉取數據數
let result = []


/**
 * 更新導出記錄中的 file_download_link 字段
 * @param {*} tableID 
 * @param {*} recordId 
 * @param {*} fileLink 
 */
function updateExportJobIdRecord(tableID, recordId, fileLink) {
  let Schame = new BaaS.TableObject(tableID)
  let schame = Schame.getWithoutData(recordId)


  schame.set('file_download_link', fileLink)
  return schame.update()
}


/**
 * 創建數據導出任務
 * 設置初始 file_download_link 爲空
 * 待導出任務執行完畢後將文件下載地址存儲到 file_download_link 字段中
 * @param {*} tableID 
 */
function createExportJobIdRecord(tableID) {
  let Schame = new BaaS.TableObject(tableID)
  let schame = Schame.create()
  return schame.set({file_download_link: ''}).save().then(res => {
    return res.data.id
  })
}


/**
 * 獲取總數據條數
 * @tableId {*} tableId
 */
function getTotalCount(tableId) {
  const Order = new BaaS.TableObject(tableId)
  return Order.count()
    .then(num => {
      console.log('數據總條數:', num)
      return num
    })
    .catch(err => {
      console.log('獲取數據總條數失敗:', err)
      throw new Error(err)
    })
}


/**
 * 分批拉取數據
 * @param {*} tableId 
 * @param {*} offset 
 * @param {*} limit 
 */
function getDataByGroup(tableId, offset = 0, limit = LIMIT) {
  let Order = new BaaS.TableObject(tableId)
  return Order.limit(limit).offset(offset).find()
    .then(res => {
      return res.data.objects
    })
    .catch(err => {
      console.log('獲取分組數據失敗:', err)
      throw new Error(err)
    })
}


/**
 * 創建 Excel 導出文件
 * @param {*} sourceData 源數據
 */
function genExportFile(sourceData = []) {
  const resultArr = []
  const rowArr = []


  // 配置列名
  rowArr.push(ROW_NAME)


  sourceData.forEach(v => {
    rowArr.push(
      ROW_NAME.map(k => v[k])
    )
  })


  resultArr[0] = {
    data: rowArr,
    name: 'sheet1',    // Excel 工作表名
  }


  const option = {'!cols': [{wch: 10}, {wch: 20}]}    // 自定義列寬度
  const buffer = xlsx.build(resultArr, option)
  return fs.writeFile(TMP_FILE_NAME, buffer, err => {
    if (err) {
      console.log('創建 Excel 導出文件失敗')
      throw new Error(err)
    }
  })
}


/**
 * 上傳文件
 */
function uploadFile() {
  let MyFile = new BaaS.File()
  return MyFile.upload(TMP_FILE_NAME, {category_id: EXPORT_DATA_CATEGORY_ID})
    .catch(err => {
      console.log('上傳文件失敗')
      throw new Error(err)
    })
}


module.exports = async function(event, callback) {
  try {
    const date = new Date().getTime()
    const groupInfoArr = []
    const groupInfoSplitArr = []
    const [jobId, totalCount] = await Promise.all([createExportJobIdRecord(TABLE_ID.export_task), getTotalCount(TABLE_ID.order)])
    const groupSize = Math.ceil(totalCount / LIMIT) || 1


    for (let i = 0; i < groupSize; i++) {
      groupInfoArr.push({
        offset: i * LIMIT,
        limit: LIMIT,
      })
    }


    console.log('groupInfoArr:', groupInfoArr)


    const length = Math.ceil(groupInfoArr.length / MAX_CONNECT_LIMIT)


    for (let i = 0; i < length; i++) {
      groupInfoSplitArr.push(groupInfoArr.splice(0, MAX_CONNECT_LIMIT))
    }


    console.log('groupInfoSplitArr:', groupInfoSplitArr)


    const date0 = new Date().getTime()
    console.log('處理分組情況耗時:', date0 - date, 'ms')


    let num = 0


    // 分批獲取數據
    const getSplitDataList = index => {
      return Promise.all(
        groupInfoSplitArr[index].map(v => {
          return getDataByGroup(TABLE_ID.order, v.offset, v.limit)
        })
      ).then(res => {
        ++num
        result.push(...Array.prototype.concat(...res))
        if (num < groupInfoSplitArr.length) {
          return getSplitDataList(num)
        } else {
          return result
        }
      })
    }


    Promise.all([getSplitDataList(num)]).then(res => {
      const date1 = new Date().getTime()
      console.log('結果條數:', result.length)
      console.log('分組拉取數據次數:', num)
      console.log('拉取數據耗時:', date1 - date0, 'ms')


      genExportFile(result)


      const date2 = new Date().getTime()
      console.log('處理數據耗時:', date2 - date1, 'ms')


      uploadFile().then(res => {
        const fileLink = res.data.file_link
        const date3 = new Date().getTime()
        console.log('上傳文件耗時:', date3 - date2, 'ms')
        console.log('總耗時:', date3 - date, 'ms')


        updateExportJobIdRecord(TABLE_ID.export_task, jobId, fileLink)
          .then(() => {
            const date4 = new Date().getTime()
            console.log('保存文件下載地址耗時:', date4 - date3, 'ms')
            console.log('總耗時:', date4 - date, 'ms')


            callback(null, {
              message: '保存文件下載地址成功',
              fileLink,
            })
          })
          .catch(err => {
            callback(err)
          })
      }).catch(err => {
        console.log('上傳文件失敗:', err)
        throw new Error(err)
      })
    })
  } catch (err)

三、部署並測試

跟 npm 一樣,部署前需要先登錄,請參照文檔配置。

使用以下命令即可將雲函數部署到知曉雲:

 run deploy  

執行結果如下:

使用以下的命令來測試:

mincloud invoke export-excel-filebr  

執行結果如下:

export_task 表記錄:

上傳到知曉雲的 excel 文件如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8xgPW2iK-1588065248145)(https://cloud-minapp-11491.cloud.ifanrusercontent.com/1h3eeGNECR14zCdk.png)]

文件內容:

四、參考文檔

知曉雲開發文檔:https://doc.minapp.com/

node-xlsx 文檔:https://www.npmjs.com/package/node-xlsx

五、源碼

倉庫地址:https://github.com/ifanrx/export-excel-file

相關閱讀

第一期:快速實現圖片爬蟲
第二期:快速生成分享海報
第三期:處理微信卡券消息
第四期:自動回覆客服消息
第五期:生成帶參數的二維碼

關注「知曉雲」公衆號,點擊菜單欄「知曉雲」-「知曉課堂」,獲取更多開發教程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章