# 关于两个微信小程序项目:AIxiaoda 和百无禁忌 Varmeta 的开发笔记
第一个小程序是一个小程序端问答小助手,功能和特点与简洁风格的 AI 智能助手应用如元宝、KIMI、DeepSeek 等基本类似,API 可以任选自己喜欢的,原型图较为简约。
第二个小程序是与产品经理朋友合作的创业项目,更小众和个性化,在 AI 助手这一扩展功能上,前端的实现在移植第一个小程序的基础上修改了风格与细节以贴合原型图要求。
记录一些技术点:
# 1. 流式输出的实现:
ChatGPT 时期一般认为小程序是不支持流式请求的,市面上大多数开发者的解决方案都是使用 websocket 来解决,还有一部分开发者是小程序嵌套网页解决这个问题。
流式输出思路:
使用 uni.request 的 enableChunked: true 配置,并通过 onChunkReceived 方法逐步接收和处理服务器返回的数据块。
uni.request 发起请求时,配置 enableChunked: true:启用分块传输模式,服务器会将数据分块发送,客户端可以逐步接收。
onChunkReceived:这是一个回调函数,每当接收到一个数据块时就会触发。客户端可以在这里处理每个数据块,将接收到的二进制数据块(chunk)转换为可读的字符串。
将分块数据追加到 question 数组中,并实时更新 UI。
微信小程序基础库从 3.7.1 版本开始内置了云开发 AI+ 能力,开发者可以直接通过小程序中的 wx.cloud.extend.AI 调用。
初始化
wx.cloud.init({ | |
env: "your-env-id" | |
}); |
创建指定的 AI 大模型
const model = wx.cloud.extend.AI.createModel("hunyuan-exp"); |
类型声明
function createModel(model: string): ChatModel; |
流式调用大模型生成文本
ChatModel.streamText () 流式调用时,生成的文本及其他响应数据会通过 SSE 返回,该接口的返回值对 SSE 做了不同程度的封装,开发者能根据实际需求获取到文本流和完整数据流。
const hy = wx.cloud.extend.AI.createModel("hunyuan-exp"); // 创建模型 | |
const res = await hy.streamText({ | |
data: { | |
model: "hunyuan-lite", | |
messages: [ | |
{ | |
role: "user", | |
content: "hi" | |
} | |
] | |
} | |
}); | |
for await (let str of res.textStream) { | |
console.log(str); // 打印生成的文本 | |
} | |
for await (let event of res.eventStream) { | |
console.log(event); // 打印每次返回的完整数据 | |
// 当大模型结束传输时,通常会发一条 [DONE] 数据,在此之后即可停止循环 | |
if (event.data === "[DONE]") { | |
break; | |
} | |
} |
调用大模型生成文本
ChatModel.generateText () 生成文本
const hy = wx.cloud.extend.AI.createModel("hunyuan-exp"); // 创建模型 | |
const res = await hy.generateText({ | |
model: "hunyuan-lite", | |
messages: [{ role: "user", content: "你好" }], | |
}); | |
console.log(res); | |
// { | |
// "id": "27dae91f4e9a4777782c61f89acf8ea4", | |
// "object": "chat.completion", | |
// "created": 1737602298, | |
// "model": "hunyuan-lite", | |
// "system_fingerprint": "", | |
// "choices": [ | |
// { | |
// "index": 0, | |
// "message": { | |
// "role": "assistant", | |
// "content": "你好!很高兴与你交流。请问有什么我可以帮助你的吗?无论是关于生活、工作、学习还是其他方面的问题,我都会尽力为你提供帮助。" | |
// }, | |
// "finish_reason": "stop" | |
// } | |
// ], | |
// "usage": { | |
// "prompt_tokens": 3, | |
// "completion_tokens": 33, | |
// "total_tokens": 36 | |
// }, | |
// "note": "以上内容为 AI 生成,不代表开发者立场,请勿删除或修改本标记" | |
// } | |
console.log(res.choices[0].message.content); | |
// 你好!很高兴与你交流。请问有什么我可以帮助你的吗?无论是关于生活、工作、学习还是其他方面的问题,我都会尽力为你提供帮助。 |
# 2. 输入框高度与文本的适应
// 输入框换行时触发 | |
const lineChange = (event)=>{ | |
// console.log(event); | |
const {height,lineCount} = event.detail | |
// 如果 >=2 行 | |
textareaValue.alignItems = lineCount >= 2 ? 'flex-end' : 'center' | |
// 如果 >=6 行,不再自动增高 | |
if(lineCount >= 6){ | |
textareaValue.autoHeight = false | |
textareaValue.height = height | |
}else{ | |
textareaValue.autoHeight = true | |
} | |
} | |
//textarea 的父级高度 | |
const textareaHeight = ref('') | |
// 获取 textarea 的父级高度 | |
onMounted(()=>{ | |
setTimeout(()=>{ | |
const query = uni.createSelectorQuery().in(instance); | |
query.select('.input-content').boundingClientRect((res)=>{ | |
textareaHeight.value = res.height + 'px' | |
}).exec() | |
},300) | |
}) |
动态调整输入框对齐方式:
- 当输入框的行数小于 2 行时,将输入框的对齐方式改为 center(居中对齐)。
(当用户只输入一行文本时,输入框的高度较小,居中对齐可以让输入框在父容器中看起来更加平衡和美观。) - 当输入框的行数大于等于 2 行时,将输入框的对齐方式改为 flex-end(底部对齐)。
(将输入框对齐方式改为 flex-end(底部对齐),可以确保输入框的底部始终固定,避免内容被遮挡,同时让用户更容易看到最新输入的内容。) - 当输入框的行数小于 6 行时,自动增高。
- 当输入框的行数大于等于 6 行时,停止自动增高,固定输入框的高度。
# 3. 小程序分包
为什么要使用分包?
主要原因就是微信小程序规定了主包大小不能超过 2M ,但我们随着开发的更新迭代,一个小程序往往是大于 2M 的。于是小程序提供了分包的解决方法,将一个完整的的小程序,在打包时分成不同功能或需求的分包,在用户使用时再加载对应的分包。
主包:使用分包后必须有一个主包,用于存放 TabBar 页面,以及一些公共的资源文件和 JS 脚本。
分包:从主包上拆分而来的文件,个人建议的的拆分方式:先根据 TabBar 页面拆分大的模块,再拆分每个 TabBar 内具体的小功能模块,这样拆分管理起来也更加清晰明了。
声明 subpackages 后,将按 subpackages 配置路径进行打包,subpackages 配置路径外的目录将被打包到主包中。也就是没指定分包的文件都会被打包到主包。
主包也可以有自己的 pages,即最外层的 pages 字段。
subpackage 的根目录不能是另外一个 subpackage 内的子目录。也就是不能在分包内放置另外一个另外一个分包,两者必须是平级的关系。
tabBar 页面必须在主包内。
# 4. 图片懒加载
image 标签里的 lazy-load 属性
lazyLoad: { | |
type: Boolean, | |
default: uni.$u.props.image.lazyLoad | |
}, | |
<image | |
v-if="!isError" | |
:src="src" | |
:mode="mode" | |
@error="onErrorHandler" | |
@load="onLoadHandler" | |
:show-menu-by-longpress="showMenuByLongpress" | |
:lazy-load="lazyLoad" | |
class="u-image__image" | |
:style="{ | |
borderRadius: shape == 'circle' ? '10000px' : $u.addUnit(radius), | |
width: $u.addUnit(width), | |
height: $u.addUnit(height) | |
}" | |
></image> |
# 5. 路由懒加载
const routes = [ | |
{ | |
path: '/home', | |
component: () => import('@/pages/Home.vue') // 按需加载 Home 组件 | |
}, | |
{ | |
path: '/about', | |
component: () => import('@/pages/About.vue') // 按需加载 About 组件 | |
} | |
]; |
在 UniApp 应用中,可以基于路由进行代码拆分,即根据用户访问的页面加载相应的代码资源,而非一次性加载所有页面的代码。
# 6. 设备兼容与显示自适应
使用 rpx ,rem,百分比,vh 与 vw 单位
uni.getSystemInfo 获取设备信息,动态调整逻辑
Flex 布局:使用 Flex 布局实现弹性盒子模型,适应不同屏幕尺寸。
媒体查询:在 CSS 中使用 @media 查询,针对不同屏幕尺寸设置样式。
多倍图:提供 2x、3x 等多倍图,适应高分辨率屏幕。
等等