nightwhite 16cc16f61f
doc: add international English section (#1308)
* doc: add international English section

* doc: del operator.md

* doc: some old error

* doc: fix path errors

* doc: modify the default readme language

* doc: Part of the internationalization of changes

* doc: keep English readme

* doc: delete readme_en.md
2023-06-25 16:36:23 +08:00

16 KiB
Raw Blame History

title
云函数常见问题

{{ $frontmatter.title }}

这里是云函数开发过程中可能会遇到的一些问题。欢迎 Pr

toc

前端跨域

laf 云函数默认是没有域名限制的,但是部分同学在开发过程中发现,还是会出现跨域问题。可以将 withCredentials 设置为 false

下面是 3 种常见的withCredentials 设置为 false的方法:

// 方式一
axios.defaults.withCredentials = false

// 方式二
axios.get('http://example.com', {
  withCredentials: false 
})

// 方式三
XMLHttpRequest.prototype.withCredentials = false

云函数延迟执行

import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {
  const startTime = Date.now()
  console.log(startTime)
  await sleep(5000) // 延迟 5 秒
  console.log(Date.now() - startTime)
}

async function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(resolve, ms)
  );
}

云函数设置某请求超时时间

import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {
  // 如果 getData 的异步操作在 4 秒内完成并返回,则 responseText 为 getDat 的返回值
  // 如果 4 秒内未完成,则 responseText 为'',不影响 getData 的实际运行
  const responseText = await Promise.race([
    getData(),
    sleep(4000).then(() => ''),
  ]);
  console.log(responseText, Date.now())
}

async function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(resolve, ms)
  );
}

async function getData(){
  // 某个异步操作,以下通过 sleep 模拟超过 4 秒的情况
  await sleep(5000)
  const text = "getData 的返回值"
  console.log(text, Date.now())
  return text
}

云函数对接公众号简单示例

以下代码只兼容明文模式

import * as crypto from 'crypto'
import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {
  const { signature, timestamp, nonce, echostr } = ctx.query;
  const token = '123456'; // 这里的 token 自定义,需要对应微信后台的配置的 token

  // 验证消息是否合法,若不合法则返回错误信息
  if (!verifySignature(signature, timestamp, nonce, token)) {
    return 'Invalid signature';
  }

  // 如果是首次验证,则返回 echostr 给微信服务器
  if (echostr) {
    return echostr;
  }

  // 处理接收到的消息
  const payload = ctx.body.xml;
  // 如果接收的是文本
  if (payload.msgtype[0] === 'text') {
    // 公众号发什么回复什么
    return toXML(payload, payload.content[0]);
  }
}

// 校验微信服务器发送的消息是否合法
function verifySignature(signature, timestamp, nonce, token) {
  const arr = [token, timestamp, nonce].sort();
  const str = arr.join('');
  const sha1 = crypto.createHash('sha1');
  sha1.update(str);
  return sha1.digest('hex') === signature;
}

// 返回组装 xml
function toXML(payload, content) {
  const timestamp = Date.now();
  const { tousername: fromUserName, fromusername: toUserName } = payload;
  return `
  <xml>
    <ToUserName><![CDATA[${toUserName}]]></ToUserName>
    <FromUserName><![CDATA[${fromUserName}]]></FromUserName>
    <CreateTime>${timestamp}</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[${content}]]></Content>
  </xml>
  `
}

云函数合成图片

需要先安装依赖 canvas

import { createCanvas } from 'canvas'

export async function main(ctx: FunctionContext) {
  const canvas = createCanvas(200, 200)
  const context = canvas.getContext('2d')

  // Write "hello!"
  context.font = '30px Impact'
  context.rotate(0.1)
  context.fillText('hello!', 50, 100)

  // Draw line under text
  var text = context.measureText('hello!')
  context.strokeStyle = 'rgba(0,0,0,0.5)'
  context.beginPath()
  context.lineTo(50, 102)
  context.lineTo(30 + text.width, 102)
  context.stroke()

  // Write "Laf!"
  context.font = '30px Impact'
  context.rotate(0.1)
  context.fillText('Laf!', 50, 150)
  console.log(canvas.toDataURL())
  return `<img src= ${canvas.toDataURL()} />`
}

云函数合成带中文字体的图片

先把支持中文字体的字体文件上传到云存储,获得云存储的字体下载地址,然后保存到临时文件中,再去通过 canvas 合成。

:::tip Laf 应用重启后,临时文件会清空哦~ :::

import { createCanvas, registerFont } from 'canvas'
import fs from 'fs'
import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {

  const url = ''  // 字体文件网址
  const dest = '/tmp/fangzheng.ttf'  // 本地保存路径

  if (fs.existsSync(dest)) {
    console.log('File exists!')
  } else {
    console.log('File does not exist')
    const res = await cloud.fetch({
      method: 'get',
      url,
      responseType: 'arraybuffer'
    })
    fs.writeFileSync(dest, Buffer.from(res.data))
  }

  registerFont('/tmp/fangzheng.ttf', { family: 'fangzheng' })
  const canvas = createCanvas(200, 200)
  const context = canvas.getContext('2d')

  // Write "你好!"
  context.font = '30px fangzheng'
  context.rotate(0.1)
  context.fillText('你好!', 50, 100)

  // Draw line under text
  var text = context.measureText('hello!')
  context.strokeStyle = 'rgba(0,0,0,0.5)'
  context.beginPath()
  context.lineTo(50, 102)
  context.lineTo(30 + text.width, 102)
  context.stroke()

  // Write "Laf!"
  context.font = '30px Impact'
  context.rotate(0.1)
  context.fillText('Laf!', 50, 150)
  console.log(canvas.toDataURL())
  return `<img src= ${canvas.toDataURL()} />`
}

云函数防抖

通过 Laf 云函数的全局缓存可以很方便的设置防抖

以下是一个简单的防抖例子,前端请求时,需要在 header 中带上用户 token。

// 云函数生成 Token
const accessToken_payload = {
  // 除了示例的,还可以加别的参数
  uid: login_user[0]._id, //一般是 user 表的_id
  role: login_user[0].role, //如果没有 role可以不要
  exp: (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7) * 1000, //7天过期
}
const token = cloud.getToken(accessToken_payload)
console.log(token)
import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {
  const FunctionName = ctx.request.params.name
  const sharedName = FunctionName + ctx.user.uid
  let lastCallTime = cloud.shared.get(sharedName)
  console.log(lastCallTime)
  if (lastCallTime > Date.now()) {
    console.log("请求太快了")
    return '请求太快了'
  }
  cloud.shared.set(sharedName, Date.now() + 1000)
  // 原有逻辑

  // 逻辑完毕后删除全局缓存
  cloud.shared.delete(sharedName)
}

云函数域名验证

部分微信服务需要验证 MP 开头的 txt 文件的值,以判断域名是否有权限

可以新建一个该文件名的云函数,如:MP_123456789.txt

直接返回该文本的内容

import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {
  // 这里直接返回文本内容
  return 'abcd...'
}

Laf 应用 IP 池IP 白名单)

下满例子为使用的是 laf.run 的情况。使用 laf.dev 或其他,下面命令需要更换域名。

  • Windows 可在 CMD 中执行 nslookup laf.run

  • Mac 可在终端中执行 nslookup laf.run

可看到全部 IP 池

云函数发送阿里云短信验证码

使用阿里云短信接口,编写发送短信验证码的云函数。 需要你提供阿里云短信服务的 AccessKeySecretKey,以及短信模板 ID。

创建 sendsms 云函数,添加依赖 @alicloud/dysmsapi20170525编写以下代码

import Dysmsapi, * as dysmsapi from "@alicloud/dysmsapi20170525";
import * as OpenApi from "@alicloud/openapi-client";
import * as Util from "@alicloud/tea-util";

const accessKeyId = "xxxxxxxxxxxxxx";
const accessKeySecret = "xxxxxxxxxxxxxx";
const signName = "XXXX";
const templateCode = "SMS_xxxxx";
const endpoint = "dysmsapi.aliyuncs.com";


export default async function (ctx: FunctionContext) {
  const { phone, code } = ctx.body;

  const sendSmsRequest = new dysmsapi.SendSmsRequest({
    phoneNumbers: phone,
    signName,
    templateCode,
    templateParam: `{"code":${code}}`,
  });

  const config = new OpenApi.Config({ accessKeyId, accessKeySecret, endpoint });
  const client = new Dysmsapi(config);
  const runtime = new Util.RuntimeOptions({});
  const res = await client.sendSmsWithOptions(sendSmsRequest, runtime);
  return res.body;
};

云函数微信 native 支付

Native 支付适用于 PC 网站、实体店单品或订单、媒体广告支付等场景。微信官方介绍

Native 支付服务端下单之后微信支付会返回一个 code-url, 然后前端接收这个返回值,直接把这个 code-url 在前端 生成一个二维码即可用微信扫描支付。

  • code-url 结构类似:weixin://wxpay/bizpayurl?pr=aIQrOYOzz

  • notify_url 也可以写一个 laf 云函数来接受支付结果的通知。此处不包含 notify_url 代码。

创建 wx-pay 云函数,安装依赖 wxpay-v3,代码如下:

import cloud from "@lafjs/cloud";
const Payment = require("wxpay-v3");

export default async function (ctx: FunctionContext) {
  const { goodsName, totalFee, payOrderId } = ctx.body;

  // create payment instance
  const payment = new Payment({
    appid: "应用 ID",
    mchid: "商户 id",
    private_key: getPrivateKey(),
    serial_no: "序列号",
    apiv3_private_key: "api v3 密钥",
    notify_url: "付退款结果通知的回调地址",
  });

  // 下单
  const result = await payment.native({
    description: goodsName,
    out_trade_no: payOrderId,
    amount: {
      total: totalFee,
    },
  });
  return result;
};

function getPrivateKey() {
  const key = `-----BEGIN PRIVATE KEY-----
HOBHEk+4cdiPcvhowhC8ii7838DP4qC+18ibL/KAySWyZjUC/keOr4MxhxQ1T+OV
...
...
475J8ALCRltkgTSxicoXS7SpjLqvIH2FPpv2BI+qQ3nOmAugsRkeH9lZdC/nSC0m
uI205SwTsTaT70/vF90AwQ==
-----END PRIVATE KEY-----
`;
  return key;
}

云函数支付宝支付

创建 aliPay 云函数,安装依赖 alipay-sdk,编写以下代码:

import cloud from "@lafjs/cloud";
import alipay from "alipay-sdk";
const AlipayFormData = require("alipay-sdk/lib/form").default;


export default async function (ctx: FunctionContext) {
  const { totalFee, goodsName, goodsDetail, payOrderId } = ctx.body;

  const ali = new alipay({
    appId: "2016091800536572",
    signType: "RSA2",
    privateKey: "MIIEow......Yf2Mlz6xqG/Aq",
    alipayPublicKey: "MIIBI......IDAQAB",
    gateway: "https://openapi.alipaydev.com/gateway.do", //沙箱测试网关
  });

  const formData = new AlipayFormData();
  formData.setMethod("get");
  formData.addField(
    "notifyUrl",
    "https://APPID.laf.run/alipay_notify_callback"
  );
  formData.addField("bizContent", {
    subject: goodsName,
    body: goodsDetail,
    outTradeNo: payOrderId,
    totalAmount: totalFee,
  });

  const result = await ali.exec(
    "alipay.trade.app.pay",
    {},
    { formData: formData }
  );

  return {
    code: 0,
    data: result,
  };
};

云函数发送邮件

使用 SMTP 服务发送邮件

创建 sendmail 云函数,安装依赖 nodemailer,代码如下:

import nodemailer from 'nodemailer'

// 邮件服务器配置
const transportConfig = {
  host: 'smtp.exmail.qq.com', // smtp 服务地址,示例腾讯企业邮箱地址
  port: 465,  // smtp 服务端口,一般服务器未开此端口,需要手动开启
  secureConnection: true, // 使用了 SSL
  auth: {
    user: 'sender@xx.com',  // 发件人邮箱,写你的邮箱地址即可
    pass: 'your password',  // 你设置的 smtp 专用密码或登录密码每家服务不相同QQ 邮箱需要开启并配置授权码,即这里的 pass
  }
}

// 邮件配置
const mailOptions = {
  from: '"SenderName" <sender@xx.com>', // 发件人
  to: 'hi@xx.com', // 收件人
  subject: 'Hello',   // 邮件主题
  html: '<b>Hello world?</b>'  // html 格式邮件正文
  // text: 'hello'  // 文本格式有限正文
}

export default async function (ctx: FunctionContext) {
  const transporter = nodemailer.createTransport(transportConfig)

  transporter.sendMail(mailOptions, (error, info) => {
    if (error) {
      return console.log(error);
    }
    console.log('Message sent: %s', info.messageId);
    return info.messageId
  })
};

云函数上传文件到微信公众号临时素材

公众号回复图片或者文件,需要先把文件上传到临时素材才可以回复。

以下例子为,通过文件 URL 上传到临时素材

import fs from 'fs'
const request = require('request');
const util = require('util');
const postRequest = util.promisify(request.post);

export default async function (ctx: FunctionContext) {
  const res = await UploadToWinxin(url)
  console.log(res)
}

async function UploadToWinxin(url) {
  const res = await cloud.fetch.get(url, {
    responseType: 'arraybuffer'
  })
  fs.writeFileSync('/tmp/tmp.jpg', Buffer.from(res.data))
  // 这里 getAccess_token 不做演示
  const access_token = await getAccess_token()
  const formData = {
    media: {
      value: fs.createReadStream('/tmp/tmp.jpg'),
      options: {
        filename: 'tmp.png',
        contentType: 'image/png'
      }
    }
  };
  // 由于 axios 上传微信素材有 BUG所以这里使用 request 封装 post 请求来上传
  const uploadResponse = await postRequest({
    url: `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${access_token}&type=image`,
    formData: formData
  });
  return uploadResponse.body
 }

Laf 云函数的鉴权,获取 tokentoken 过期处理

原帖:https://forum.laf.run/d/535

流程: 1.云函数生成 token 返回给前端。 2.前端请求时带上 token。 3.云函数中根据 ctx.user 来判断是否传 token 是否过期。

示例代码: 1.云函数生成 token 返回给前端。

import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {

  const payload = {
    // uid 一般用表里用户的 id 这里演示随便写
    uid: 1,
    // 这里做演示 过期时间设置为 10s 
    // 这样写就是过期时间 7 天 exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7,
    exp: Math.floor(Date.now() / 1000) + 10,
  };

  // 生成 access_token
  const access_token = cloud.getToken(payload);

  return { access_token }
}

2.前端请求时带上 token。 第一种使用 laf-client-sdk.

import { Cloud } from "laf-client-sdk"; // 引入 laf-client-sdk
import axios from "axios";

// 创建 cloud 对象
const cloud = new Cloud({
  baseUrl: "", // 填你的云函数地址如https://appid.laf.dev
  // 传入 access_token 从本地缓存中取出 access_token
  getAccessToken: () => localStorage.getItem("access_token"),
});
// invoke 调用云函数时会自动带上 access_token
const res = await cloud.invoke("test");

第二种通过 axios

import axios from "axios";

const token = localStorage.getItem("access_token");
axios({
  method: "get",
  url: "functionUrl",
  headers: {
    // 这里带上 token
    Authorization: `Bearer ${token}`,
  },
})

3.云函数中根据 ctx.user 来判断是否传 token 是否过期。

import cloud from '@lafjs/cloud'

export async function main(ctx: FunctionContext) {

  console.log(ctx.user)
  // 如果前端传了 token 并且没过期: { uid: 1, exp: 1683861234, iat: 1683861224 }
  // 如果前端没传 token 或者 token 不在有效期null

}