背景
目前我的小程序在腾讯云开发cloudbase的计费方式是基础套餐+按量付费,每月费用算下来虽然在100左右,但是也觉得没必要,因为之前时间比较紧张没有仔细研究,tx的优惠目前是有个套餐截止日期,不知道过了之后会怎么样。在群里和@Night
讨论完计费规则之后,才搞明白是怎么收费的。还有对于@carsonyang
大佬的信任,萌生迁移念头。感谢laf的免费时长,让我足够时间测试。
面对挑战
我目前小程序的功能不是很复杂,几个云函数、4个集合,有两个集合比较大200M左右数据,最后程序部分使用新的云函数完成旧功能支持。直接列出比较共性的迁移的点:
- 用户登录状态保持如何实现?
- 云数据如何迁移?
- 云数据库索引如何创建?
- 如何发布新版本?
迁移步骤
针对上述面临挑战各个步骤做出方案:
用户登录状态保持
直接贴上云函数代码,好像是从帖子里找到的,记不得了。
import cloud from "@lafjs/cloud";
import { createHash } from "crypto";
exports.main = async function (ctx: FunctionContext) {
const username = ctx.body?.username || "";
const password = ctx.body?.password || "";
// check user login
const db = cloud.database();
const res = await db
.collection("users")
.where({
username: username,
password: createHash("sha256").update(password).digest("hex"),
})
.getOne();
if (!res.data) return { error: "invalid username or password" };
// generate jwt token
const user_id = res.data._id;
const payload = {
uid: user_id,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7,
};
const access_token = cloud.getToken(payload);
return {
uid: res.data._id,
access_token: access_token,
};
};
这段代码使用了JWT作为登录校验,返回的access_token可以作为登录信息凭证,顺便研究了一下,如果相同代码那不就可以伪造了token了嘛,后来发现有个SERVER_SECRET
在应用设置-> 环境变量里,可以自行查看哈。
云数据如何迁移
之前也想将tcb里的数据库做个备份,但是一直没时间研究。这次也想找个现成工具处理一下,结果没找到。在论坛里找到一个laf的备份迁移工具,GitHub - nightwhite/BackupLaf ,BackupDB.ts
他这个是迁移不同环境的,借鉴了一下他的思想和代码,将tcb的数据存成JSON格式,然后将文件存储到laf的OSS,在用云函数导入laf的数据库中。我直接贴一下我的代码吧。
const tcb = require('@cloudbase/node-sdk');
const fs = require('fs');
// 1. 直接使用下载的私钥文件
const app = tcb.init({
env: '<env>', // 您的环境id
secretId: '<secretId>',
secretKey: '<secretKey>',
credentials: require('./config/tcb_custom_login_key.json'), // 直接使用下载的私钥文件
});
const BackupDBPath = "out"
const DbName = "<DbName>"
const pageSize = 1000;
const db = app.database();
async function load_data() {
const count = await db.collection(DbName).count();
let total = count.total
// 1. 分页;
//计算需分几次取
const batchTimes = Math.ceil(total / pageSize);
//批量获取数据
let start = 0
//如果查询到批次
const batchRes = await db.collection("BackupDB").where({DbName: DbName}).orderBy('createAt', 'desc').limit(1).get()
if (batchRes.data[0]) {
start = batchRes.data[0].Batch
}
let dbInfo = {}
dbInfo[DbName] = total
for (let i = start; i < batchTimes; i++) {
console.log(`Begin loading page: ${i}/${batchTimes}`)
try {
const res = await db.collection(DbName).skip(i * pageSize).limit(pageSize).get();
const filename = `${BackupDBPath}/${DbName}/${i}.json`
const stream = fs.createWriteStream(filename);
stream.write(JSON.stringify(res.data));
stream.end();
stream.on('finish', () => {
console.log(`Data written to ${filename} successfully!`);
});
// 记录插入表的批次,保存到数据库
console.log(`插入${DbName}表第${i}批数据成功`);
const batchRes = await db.collection("BackupDB").where({DbName: DbName}).orderBy('createAt', 'desc').limit(1).get()
if (!batchRes.data[0]) {
await db.collection("BackupDB").add({
DbName: DbName,
Batch: i,
pageSize: pageSize,
createAt: new Date()
})
} else {
await db.collection("BackupDB").where(
{
DbName: DbName,
}
).update({
DbName: DbName,
Batch: i,
pageSize: pageSize,
createAt: new Date()
})
}
} catch (error) {
console.log(error);
return {data: "备份出错:" + error};
}
}
try {
const filename = `${BackupDBPath}/${DbName}/info.json`
const stream = fs.createWriteStream(filename);
stream.write(JSON.stringify(dbInfo));
stream.end();
stream.on('finish', () => {
console.log(`Data written to ${filename} successfully!`);
});
} catch (error) {
console.log(error);
return {data: "备份出错:" + error};
}
}
load_data()
将集合数据以JSON数组形式存入本地文件夹。尝试了一下,我的pagesize 设置 5000好像还不行,所以最后还是1k每页的数据存储的。
虽然laf提供文件夹上传,但是我有个集合存了122w的数据,也就是说光json文件就1k多个,用网页上传实在是慢,研究了下用本地代码上传。
先用云函数获取STS的秘钥:生成云存储临时令牌 (STS) | laf 云开发,代码什么不用改, 搞上去调用即可。然后上传文件:
const {S3, PutObjectCommand} = require('@aws-sdk/client-s3');
const {Cloud} = require("laf-client-sdk");
const fs = require('fs');
const path = require('path');
const dirName = '<dirName>'
const filePath = `./out/${dirName}`
const BUCKET_PATH_PREFIX = `BackupDB/${dirName}`
const APPID = "<appid>"; // Laf 应用 appid
async function upload_files() {
new Cloud({
baseUrl: `https://${APPID}.laf.run`,
getAccessToken: () => "",
});
// sts 生成的秘钥部分,贴进来
const sts = {
"credentials": {
"AccessKeyId": "<AccessKeyId>",
"SecretAccessKey": "<SecretAccessKey>",
"SessionToken": "<SessionToken>",
"Expiration": "2023-05-06T21:19:02.000Z"
},
"endpoint": "https://oss.laf.run",
"region": "cn-hz"
}
const s3Client = new S3({
endpoint: sts.endpoint,
region: sts.region,
credentials: {
accessKeyId: sts.credentials.AccessKeyId,
secretAccessKey: sts.credentials.SecretAccessKey,
sessionToken: sts.credentials.SessionToken,
expiration: sts.credentials.Expiration,
},
forcePathStyle: true,
});
// 遍历文件夹 filePath
const files = fs.readdirSync(filePath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(filePath, files[i]);
const fileStream = fs.createReadStream(filename);
fileStream.on('error', function (err) {
console.log('File Error', err);
});
// bucket name prefixed with appid
const bucket = `${APPID}-<bucket name>`;
const cmd = new PutObjectCommand({
Bucket: bucket,
Key: `${BUCKET_PATH_PREFIX}/${files[i]}`,
// Body: "Hello from laf oss!", // 文件内容可以是二进制数据,也可以是文本数据,或者是 File 对象
Body: fileStream,
ContentType: "application/json",
});
const res = await s3Client.send(cmd);
console.log(res);
}
}
upload_files()
其实还原数据部分也是参考GitHub - nightwhite/BackupLaf,里面的ReductionDB.ts
,稍微做了修改,以下为云函数代码:
/**
* 本函数在新的1.0Laf上运行,用于还原数据库
* bucket 配置为新Laf的存储桶名称
*
* 需要先在老1.0的云函数中运行备份云函数,将数据备份到新Laf的存储桶中后在运行本云函数
*/
import cloud from "@lafjs/cloud";
import { EJSON } from 'bson'
import { Document, OptionalId } from "mongodb";
const db = cloud.database();
const bucket = `https://<appid>-<bucket name>`; // 请替换为你的存储桶名称,填目标迁移laf的存储桶名称,打开读写权限
const bucketURL = "oss.laf.run"; // 请替换为你的目标迁移laf的oss域名
const DbName = '<db name>' //集合名
export async function main(ctx: FunctionContext) {
const info_json = `${bucket}.${bucketURL}/BackupDB/${DbName}/info.json`
// 数据库info.json
console.log(info_json)
const info: {
[key: string]: number;
} = (await cloud.fetch(info_json))
// } = (await cloud.fetch('https://lpwk4o-blog-test.oss.laf.run/BackupDB/rb_card_info/info.json'))
.data;
// 遍历数据库
for (const [key, value] of Object.entries(info)) {
//按1000条分批次插入
const batchTimes = Math.ceil(value / 1000);
// 遍历数据库中的表
let start = 0;
//如果查询到批次
const batchRes = await db
.collection("ReductionDB")
.where({ DbName: key })
.getOne();
if (batchRes.data) {
start = batchRes.data.Batch;
}
for (let i = start; i < batchTimes; i++) {
try {
const data = (
await cloud.fetch(
`${bucket}.${bucketURL}/BackupDB/${key}/${i}.json`
)
).data;
// 插入数据
const collection = cloud.mongo.db.collection(key);
// 将字符串解析为对象数组
const strData = JSON.stringify(data);
const dataArray = EJSON.parse(strData, { relaxed: false }) as OptionalId<Document>[];
await collection.insertMany(dataArray);
console.log(`插入${key}表第${i}批数据成功`);
// 记录插入表的批次,保存到数据库
const batchRes = await db
.collection("ReductionDB")
.where({ DbName: key })
.getOne();
if (batchRes.data) {
await db.collection("ReductionDB").doc(batchRes.data._id).update({ Batch: i, createAt: new Date() })
} else {
await db.collection("ReductionDB").add({
DbName: key,
Batch: i,
createAt: new Date()
});
}
} catch (error) {
console.log("插入失败:", error);
return { data: error };
}
}
}
// 记录日志
console.log("全部数据库恢复完成");
return { data: "全部数据库恢复完成" };
}
至此数据库就迁移完成了。后续面临的一些增量数据再进行特殊化的根据条件生成数据文件,直接上传迁移即可,因为集合里记录了迁移进度,没太大问题。
云数据库索引如何创建
这部分应该是laf在完善之中,像一些高级的功能目前使用MongoDB的原生方式执行
cloud.mongo.db.collection("test").createIndex({"name":1})
:
import cloud from '@lafjs/cloud'
export async function main(ctx: FunctionContext) {
const db = cloud.database()
// insert data
await db.collection('test').add({ name: "hello laf" })
// get data
// const res = await db.collection('test').getOne()
// const res = cloud.mongo.db.collection("test").createIndex({"name":1})
const resIndexs = cloud.mongo.db.collection("test").indexes()
return resIndexs
}
如何发布新版本
其实这部分也大概说了,无非会产生一些增量数据,这个要对比插入,还有要根据小程序新旧版本进行一些兼容性测试。要开发者自行把控了。
总结
总体上还是要想想办法的,我毕竟是个后端,JS,TS玩的不是很溜,献丑了,欢迎交流。