前面简单介绍了node与简单的应用,本文通过结合飞书官方文档 使用node对飞书多维表格进行简单的操作(获取token 查询多维表格recordid,删除多行数据,新增数据)
文章目录
前言
前两篇文章对node做了简单的介绍,结合飞书文档,目的是 获取接口数据覆盖到飞书多维表格中 (付 飞书官方文档地址)
工作流程简要概述:
0.飞书使用指定的API 操作多维表格,需要在后台新建应用并开通相关权限,获取该应用的 app_id 与 app_secret 。多维表格的 app_token 和 table_id 也要存好(介绍地址)。选择多维表格中右上角的三个点再点击更多,添加新建的应用。如图(添加后应用权限应为 可管理):
1. 向指定接口发送请求后获取json文件并将其格式内容转变为指定的结构并储存 ;
2. 向飞书发送请求获取token,此处使用 telenantToken (介绍文档 )存储获取的token ;
3. 通过 2. 中的 telenant token ,向飞书发送请求 ,获取指定多维表格当前的 recordID,并将获取到的 recordID 以指定格式存储到文件中 ; (文档地址)
4. 将 3. 中的recordID 作为参数,向多维表格发送请求删除当前多维表格的所有数据内容 ;
5. 读取 1. 中的数据 ,向 飞书多维表格发送请求,写入到多维表格中 。
其他的准备工作已经在前两篇文章有提及,此处不再赘述 。
本次使用node为考虑通行性可读性,引入axios而没有引入飞书官方npm包操作多维表格。所涉及的多维表格对应的apptoken与tableid可以替换为使用者自有的相关信息。
一、 按概述实现代码
0. 从接口获取json信息并储存
由于后期要将获取到的信息作为参数封装到请求体发送用以新增多维表格,fields的数据结构是map,想要对数据存储与引用也需要将获取到的map信息进行 序列化/反序列化 。获取的信息做了一些格式整理。之后将信息存储到指定位置的文件中,并暴露文件名称,地址。代码如下 :
const https = require('https') ;
let fs = require('fs');
let inforAddress = '.\\infor\\person-infor.txt' ;
https.get('https://aaa.bbb.ccc/api', function (res) {
var json = '';
res.on('data', function (d) {
json += d;
});
res.on('end',function(){
//获取到的数据
json = JSON.parse(json);
var jsonListData = json.list ;
let ws = fs.createWriteStream(inforAddress);
ws.write( '{ "records": [') ;
for(var index=0;index<jsonListData.length;index++){
ws.write('{ "fields": ' +"\r\n" ) ;
ws.write(' {' +"\r\n" ) ; infor = ' {' +"\r\n" ;
ws.write(' "参会码" '+':'+jsonListData[index].unicode+ ','+"\r\n") ; //参会码 飞书无引号
ws.write(' "姓名" '+':"'+jsonListData[index].name+'"'+ ','+"\r\n") ;
ws.write(' "手机号码" '+':"'+jsonListData[index].mobile+'"'+ ','+"\r\n") ;
ws.write(' "性别" '+':"'+jsonListData[index].field_6321+'"'+ ','+"\r\n") ;
ws.write(' "公司名称" '+':"'+jsonListData[index].field_6698+'"'+ ','+"\r\n") ;
ws.write(' "是否住宿" '+':"'+jsonListData[index].field_9073+'"'+ ','+"\r\n") ;
ws.write(' "房间类型" '+':"'+jsonListData[index].field_9567+'"'+ ','+"\r\n") ;
ws.write(' "房间号" '+':"'+jsonListData[index].field_8156+'"'+ ','+"\r\n") ;
ws.write(' "会议座位号" '+':"'+jsonListData[index].field_5028+'"'+ ','+"\r\n") ;
ws.write(' "晚宴桌号" '+':"'+jsonListData[index].field_4156+'"'+ ','+"\r\n") ;
ws.write(' "邀请单位" '+':"'+jsonListData[index].field_6281+'"'+ ','+"\r\n") ;
ws.write(' "民族" '+':"'+jsonListData[index].field_9997+'"'+ ','+"\r\n") ;
ws.write(' "席卡简称" '+':"'+jsonListData[index].field_1486+'"'+ ','+"\r\n") ;
ws.write(' "职务" '+':"'+jsonListData[index].field_5396+'"'+ ','+"\r\n") ;
ws.write(' "签到时间" '+':"'+jsonListData[index].signin_time+'"'+"\r\n" ) ;
ws.write( "\r\n"+"}"+ "\r\n"+ '\r\n') ; infor = "\r\n"+"}"+ "\r\n"+ '\r\n' ;
ws.write("}"+ "\r\n") ;
if(index<jsonListData.length-1){
ws.write(","+ "\r\n"+ '\r\n') ;
}
}
ws.write( ']'+"\r\n"+' }') ;
ws.end ;
});
}).on('error', function (e) {
console.error(e);
});
console.log("inforAddress "+inforAddress) ;
module.exports={ inforAddress }
上述代码的 URL做了隐藏,使用时可替换为需要的URL。需要注意的是,这个仅能作为测试代码使用,实际上,这段代码不能直接作为单独的模块 ,需要在写入新纪录的模块中整合该功能。
1. 从飞书获取token并存储
向飞书发送请求获取token,此处使用 telenantToken (介绍文档 )存储获取的token 。代码:
const axios = require('axios') ;
const fs = require('fs') ;
// 定义请求数据 request data
var data = JSON.stringify({
"app_id": 'cli_111111111110c',
"app_secret": 'xxxxxxxx',
});
// 定义请求参数 Papams setting
var options = {
url:'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
data: data
};
let tenant_access_token = "tenant_access_token---" ;
let addressTenantToken = '.\\infor\\tenant_access_token.txt' ;
async function getTenantToken(){
await axios(options)
.then(response => {
console.log("response: "+ response.data.tenant_access_token);
tenant_access_token = response.data.tenant_access_token ;
var tenantToken = fs.createWriteStream(addressTenantToken);
tenantToken.write(tenant_access_token) ;
tenantToken.end ;
return addressTenantToken ;
})
.catch(function (error) {
console.log("erro : "+error);
})
}
getTenantToken();
module.exports ={addressTenantToken,getTenantToken} ;
app_id app_secret替换为自己的应用信息即可。
2.获取多维表格 recordID并存储
通过 2. 中的 telenant token ,向飞书发送请求 ,获取指定多维表格当前的 recordID,并将获取到的 recordID 以指定格式存储到文件中 ; (文档地址)获取recordID并存储的代码:
const https = require('https') ;
const axios = require('axios') ;
const fs = require('fs') ;
const querystring= require('querystring');
let tenantToken = fs.readFileSync('.\\infor\\tenant_access_token.txt');
// console.log("tenantToken "+tenantToken) ;
var ws = fs.createWriteStream('.\\infor\\records.txt');
var recordsArray = new Array(); ;
var config = {
method: 'GET',
url: 'https://open.feishu.cn/open-apis/bitable/v1/apps/apptoken/tables/tableid/records?page_size=500',
headers: {
'Authorization': 'Bearer '+tenantToken
}
};
axios(config)
.then(function (response) {
var fieldsArr = new Array();
fieldsArr = (response.data.data.items );
fieldLen = fieldsArr.length;
for(var index = 0 ; index<fieldLen ;index++ ){
recordsArray[index] = (fieldsArr[index].record_id) ;
}
// console.log( "recordsArray "+ recordsArray); //.items.fields.record_id console.log( JSON.stringify(response.data.data.items.length ));
var recordsResult = JSON.stringify(recordsArray) ;
console.log( "recordsResult "+ recordsResult);
ws.write(recordsResult);
ws.end ;
})
.catch(function (error) {
console.log(error);
});
3. 删除当前记录
将 3. 中的recordID 作为参数,向多维表格发送请求删除当前多维表格的所有数据内容 ;
const axios = require('axios');
const fs = require('fs');
const querystring= require('querystring');
var rfs = fs.readFileSync('.\\infor\\records.txt');
console.log("rfs "+rfs) ;
//get records and tnant token from file then delete all data
let recordsArr =JSON.parse(rfs) ;
console.log("recordsArr "+recordsArr ) ;
let tenantToken = fs.readFileSync('.\\infor\\tenant_access_token.txt');
// get all infor from files by module fs ;
var data = JSON.stringify({
"records": recordsArr
// fs.readFileSync('.\\infor\\records.txt').toString()
});
var config = {
method: 'POST',
url: 'https://open.feishu.cn/open-apis/bitable/v1/apps/apptoken/tables/tableid/records/batch_delete',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+tenantToken
},
data : data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
console.log(error);
});
4. 写入新记录
整合 0. 中向接口发送信息后打包为规定格式的数据 并写入多维表格。
const https = require('https') ;
const axios = require('axios') ;
const fs = require('fs') ;
var querystring = require("querystring") ;
//map 转换为 object
function outputMap(mp){
let o ={} ;
for(const key of mp.keys()){
o[key] = mp.get(key)
}
return o ; //JSON.stringify(o) ;
}
//读取接口获取到的数据文件
var recordsData =new Array() ;
var personData = fs.readFileSync('.\\infor\\person-infor.txt') ;
var records = JSON.parse(personData) ;
//querystring 字符串转对象
let recordObject = new Object();
for(var index=0;index<records.length;index++){
records[index] = records[index].replace(/\{/g, "");
records[index] = records[index].replace(/\}/g, "");
recordObject[index] = records[index].replace(/^\"|\"$/g,'') ;
// recordObject[index] = querystring.parse(records[index]);
// console.log( recordObject[index] );
}
//将数据包装为指定的数据格式
function packData(fieldsData,dataLen){
var fieldsArr = new Array() ;
for(var index = 0;index<dataLen;index++){
fieldsArr.push( outputMap(new Map().set("fields", fieldsData[index]) ) ) ; //fieldsObj.push
}
var data = JSON.stringify({ //JSON.stringify
"records":fieldsArr }) ;
//console.log(data) ; //("length "+ Object.values(fieldsArr[5]["fields"])) ;
return data;
}
// data = data.replace(/[\\]/g,'');
// console.log("coonfig data "+ config.data ) ; //["records"][1].get("fields")
//将包装好的数据新增记录
function addRecords(config){
axios(config)
.then(function (response) {
console.log(config.data) ;
console.log(JSON.stringify(response.data));
})
.catch(function (error) {
console.log(error);
});
}
getInfor() ;
function getInfor(){
let itemsMap = new Map() ;
let recordsInfor = {} ;
// get infor data from this url and storage data to a file(inforAddress)
https.get('https://yourselfAPI/api', function (res) {
var json = '';
res.on('data', function (d) {
json += d;
});
res.on('end',function(){
json = JSON.parse(json);
// storage data to file(inforAddress)
var jsonListData = json.list ;
for(var index=0;index<jsonListData.length;index++){
itemsMap.set("参会码",jsonListData[index].unicode);
itemsMap.set("姓名",jsonListData[index].name);
itemsMap.set("手机号码",jsonListData[index].mobile);
itemsMap.set("性别",jsonListData[index].field_6321);
itemsMap.set("公司名称",jsonListData[index].field_6698);
itemsMap.set("是否住宿",jsonListData[index].field_9073);
itemsMap.set("房间类型",jsonListData[index].field_9567);
itemsMap.set("房间号",jsonListData[index].field_8156);
itemsMap.set("会议座位号",jsonListData[index].field_5028);
itemsMap.set("晚宴桌号",jsonListData[index].field_4156);
itemsMap.set("邀请单位",jsonListData[index].field_6281);
itemsMap.set("民族",jsonListData[index].field_9997);
itemsMap.set("席卡简称",jsonListData[index].field_1486);
itemsMap.set("职务",jsonListData[index].field_5396);
itemsMap.set("签到时间",jsonListData[index].signin_time);
recordsInfor[index] = outputMap(itemsMap) ;
}
// console.log( "dataLen") ;
var configData = packData(recordsInfor,jsonListData.length) ;
//将包装好的数据写入config配置中
var config = {
method: 'POST',
url: 'https://open.feishu.cn/open-apis/bitable/v1/apps/APPtoken/tables/tableid/records/batch_create',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+fs.readFileSync('.\\infor\\tenant_access_token.txt')// 'Bearer t-g10424dd5K5DVUHE3WFL5EV4AQPQWNS3SD6KX7GO'
},
data : configData
};
addRecords(config) ;
// console.log( (config.data) ) ;
});
}).on('error', function (e) {
console.error(e);
});
// console.log(recordsInfor[2]);
// return recordsInfor ;
}
二、步骤细节与优化补充
1. 指定数据结构生成
最开始,为了实现松耦合的目的,将各个功能尽可能独立,这样 查找,删除,新增多条数据,生成指定格式数据等功能都各自独立,可以方便以后有类似需求直接进行组合。所以查找,删除记录功能中记录的recordID,获取telnant token都存储 到了指定的TXT文档中。下面代码是错误示范:
//读取接口获取到的数据文件
var recordsData =new Array() ;
var personData = fs.readFileSync('.\\infor\\person-infor.txt') ;
var records = JSON.parse(personData) ;
//querystring 字符串转对象
let recordObject = new Object();
for(var index=0;index<records.length;index++){
records[index] = records[index].replace(/\{/g, "");
records[index] = records[index].replace(/\}/g, "");
recordObject[index] = records[index].replace(/^\"|\"$/g,'') ;
// recordObject[index] = querystring.parse(records[index]);
// console.log( recordObject[index] );
}
步骤0.将从指定API地址获取到的数据存储到硬盘中后新增多条记录, 将文档中的内容再打包为指定格式的数据,序列化后的字符串无法直接转为指定格式的object对象,这个string会直接变为Object的key,其val默认为空值,即 序列化后存储到硬盘中的信息无法反序列化生成指定格式的数据结构。
在将 获取到的数据打包的过程中,由于fields是一个map数据结构,不可以直接定义一个map,之后在遍历数组的过程中将map转换为对象后写入数组,如果这样,数组的每一个对象都会是最后一个map的信息。需要直接在数组新增元素时直接新建一个匿名的map,再进行其他操作。
//将数据包装为指定的数据格式
function packData(fieldsData,dataLen){
var fieldsArr = new Array() ;
for(var index = 0;index<dataLen;index++){
fieldsArr.push( outputMap(new Map().set("fields", fieldsData[index]) ) ) ; //fieldsObj.push
}
var data = JSON.stringify({ //JSON.stringify
"records":fieldsArr }) ;
//console.log(data) ; //("length "+ Object.values(fieldsArr[5]["fields"])) ;
return data;
}
2.使用exports 整合
之前的相关流程中各个步骤的代码都写在不同的文件中,要想整合相关功能,需要将之前的代码暴露出来,node应用exports功能教程
const axios = require('axios') ;
const fs = require('fs') ;
// 定义请求数据 request data
var data = JSON.stringify({
"app_id": 'cli_a508f2d7e17f900c',
"app_secret": 'iuyQeUFnYZbbK2MqYWT8MhyCShat4Y0A',
});
// 定义请求参数 Papams setting
var options = {
url:'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
data: data
};
let tenant_access_token = "tenant_access_token---" ;
let addressTenantToken = '.\\infor\\tenant_access_token.txt' ;
async function getTenantToken(){
await axios(options)
.then(response => {
console.log("response: "+ response.data.tenant_access_token);
tenant_access_token = response.data.tenant_access_token ;
var tenantToken = fs.createWriteStream(addressTenantToken);
tenantToken.write(tenant_access_token) ;
tenantToken.end ;
return addressTenantToken ;
})
.catch(function (error) {
console.log("erro : "+error);
})
}
getTenantToken();
module.exports ={addressTenantToken,getTenantToken} ;
最下方使用exports暴露getTenantToken()函数,其他模块即可引入使用该函数功能 。
const TenantTokenInfo = require('./getTenantToken') ;
const axios = require('axios') ;
const fs = require('fs') ;
TenantTokenInfo.getTenantToken() ;
组合后的各个功能代码在逻辑顺序上无法异步,需要使用计时器功能延时操作;引用外部模块相关函数后会执行两次(从测试结果看在最上方require后就会默认执行一次),结合前文,在引用该函数时在代码中require模块信息。 代码:
async function waiting(){
await new Promise((resolve) => setTimeout(resolve, 4500));
console.log("waiting...4.5s....") ;
}
async function assemble(){
await require('./getTenantToken').getTenantToken() ;
await waiting();
await require('./storageRecords').storageRecordsId();
await waiting();
await require('./deleteData').deleteRecords() ;
await waiting();
await require('./addRecords').addressInfor ;
}
assemble() ;
总结
在之前的代码中,可以显然地看到,在node代码执行过程中由于闭包特性与https模块含有回调函数的结合,相关数据不能自由使用,网络请求本身也带有异步特性,这并不契合直觉上的程序执行顺序。 序列化结合模块化一定程度上可实现数据内容自由传输;涉及到稍微复杂的数据结构,序列化后的数据无法进行操作,需要在内存层面直接调用数据。最后对各个功能的整合结果看,异步未必是最优解。涉及 计时器延时,异步,Promise 等操作可以参考 文章-内容补充 。