laf背景概述

目前,我的小程序运行在腾讯云 Cloudbase 平台上,采用基础套餐+按量付费的计费方式。虽然每月费用大约在 100 元左右,但我认为这是不必要的。此前由于时间紧迫,我并未对腾讯云的优惠政策进行深入了解。在与@白夜讨论了计费规则后,我对收费机制有了更清晰的认识。另外,由于对@米开朗基杨大神的信任,我开始考虑迁移项目。感谢 Laf 提供的免费试用时长,让我有足够的时间进行测试。

laf应对挑战

我的小程序功能较为简单,包括几个云函数和 4 个集合。其中两个集合的数据量较大,约为 200M。最后,我使用新的云函数实现了旧功能的支持。以下是我在迁移过程中需要解决的问题:

  1. 如何实现用户登录状态的保持?
  2. 如何进行云数据的迁移?
  3. 如何创建云数据库索引?
  4. 如何发布新版本?

迁移方案

针对上述挑战,我制定了以下解决方案:

用户登录状态保持

以下是云函数代码:

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

云数据迁移

为了迁移云数据,我参考了一个 Laf 备份迁移工具——GitHub - nightwhite/BackupLaf。我使用此工具将腾讯云的数据以 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
虽然 Laf 提供文件夹上传,但是我有个集合存了 122w 的数据,也就是说光 JSON 文件就 1k 多个,用网页上传实在是慢,研究了下用本地代码上传。

先用云函数获取 STS 的秘钥:生成云存储临时令牌 (STS) | Laf 云开发,代码什么不用改,搞上去调用即可。然后上传文件:

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 玩得不是很溜,献丑了,欢迎交流。

引用链接
[1]
GitHub - nightwhite/BackupLaf: https://github.com/nightwhite/BackupLaf

关于 Laf

Laf 是一款为所有开发者打造的集函数、数据库、存储为一体的云开发平台,助你像写博客一样写代码,随时随地发布上线应用!3 分钟上线 ChatGPT 应用!

🌟GitHub:https://github.com/labring/laf

🏠官网(国内):https://laf.run

🌎官网(海外):https://laf.dev

💻开发者论坛:https://forum.laf.run
sealos 以kubernetes为内核的云操作系统发行版,让云原生简单普及

laf 写代码像写博客一样简单,什么docker kubernetes统统不关心,我只关心写业务!


sealyun
289 声望56 粉丝

技术填坑者,爬起来接着填