背景
目前我的小程序在腾讯云开发 cloudbase 的计费方式是基础套餐 + 按量付费,每月费用算下来虽然在 100 左右,但是也觉得没必要,因为之前时间比较紧张没有仔细研究,tx 的优惠目前是有个套餐截止日期,不知道过了之后会怎么样。在群里和 < code>@Night 讨论完计费规则之后,才搞明白是怎么收费的。还有对于 < code>@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 了嘛,后来发现有个 < code>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 ()
其实还原数据部分也是参考 < a href="https://github.com/nightwhite/BackupLaf" rel="ugc noopener nofollow" target="_blank" rel="ugc noopener nofollow" target="_blank">GitHub - nightwhite/BackupLaf,里面的 < code>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 的原生方式执行 < br>
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 玩的不是很溜,献丑了,欢迎交流。