调试模式提示Uncaught SyntaxError: Unexpected token ?解决经验分享
全局搜索??运算符,使用||替代??运算符,或更改为其他方式进行运算
全局搜索??运算符,使用||替代??运算符,或更改为其他方式进行运算
uniapp 蓝牙无法监听多个特征的问题解决办法
问题描述:
在开发app时,项目需要蓝牙同时订阅某个服务下的多个特征值的变化时,直接使用uni.notifyBLECharacteristicValueChange只能订阅一个特征值的变化。
具体实现:
1.将 this.onBLECharacteristicValueChange();放到onLoad()中注册,确保该方法只注册一次,如果该方法注册了多次,蓝牙发送的数据会重复上传。
- 在蓝牙的订阅函数notifyBLECharacteristicValueChange()中设置定时器来轮流订阅蓝牙的多个特征值,具体如下:
-
notifyBLECharacteristicValueChange() { let _this = this; let deviceId = _this.mDevice.deviceId;//这里是蓝牙设备id let serviceId = SERVICE_UUID;//这里是蓝牙的服务UUID let reads = readUUID3;//这里是需要订阅的特征值UUID的数组,[特征值1,特征值2,特征值n] let num = numberReadUUID;//这里是需要订阅的特征值的个数 // let characteristicId = reads[0]; let countConnect = 0;//连接次数统计 let cID=0;//要订阅的特征值的编号 _this.notifyCycleTimer = setInterval(()=>{ let characteristicId = reads[cID];//要订阅的特征值 countConnect = countConnect+1; if(countConnect>10){//连接超过10次没有连接成功就直接返回 uni.hideLoading(); clearInterval(_this.notifyCycleTimer);//注册超时,清理掉定时器 closeBLEConnection();//注册超时就断开蓝牙的连接 closeBluetoothAdapter();//注册超时就关闭蓝牙适配器 uni.showModal({ title: '提示', content: '蓝牙连接失败,请重试!', showCancel:false, complete:function(){ //返回上一界面 uni.navigateBack({ delta:1 }); } }); } uni.notifyBLECharacteristicValueChange({ state: true, // 启用 notify 功能 deviceId, serviceId, characteristicId, success(res) { console.log('notifyBLECharacteristicValueChange success:' + characteristicId); cID=cID+1;//订阅成功就订阅下一个特征 if(cID==num){ uni.hideLoading(); clearInterval(_this.notifyCycleTimer);//注册监听成功就清理掉定时器 //返回上一界面 uni.navigateBack({ delta:1 }); } }, fail(e) { console.log("订阅失败"); } }); },500);//每隔500ms执行一次订阅 },
问题描述:
在开发app时,项目需要蓝牙同时订阅某个服务下的多个特征值的变化时,直接使用uni.notifyBLECharacteristicValueChange只能订阅一个特征值的变化。
具体实现:
1.将 this.onBLECharacteristicValueChange();放到onLoad()中注册,确保该方法只注册一次,如果该方法注册了多次,蓝牙发送的数据会重复上传。
- 在蓝牙的订阅函数notifyBLECharacteristicValueChange()中设置定时器来轮流订阅蓝牙的多个特征值,具体如下:
-
notifyBLECharacteristicValueChange() { let _this = this; let deviceId = _this.mDevice.deviceId;//这里是蓝牙设备id let serviceId = SERVICE_UUID;//这里是蓝牙的服务UUID let reads = readUUID3;//这里是需要订阅的特征值UUID的数组,[特征值1,特征值2,特征值n] let num = numberReadUUID;//这里是需要订阅的特征值的个数 // let characteristicId = reads[0]; let countConnect = 0;//连接次数统计 let cID=0;//要订阅的特征值的编号 _this.notifyCycleTimer = setInterval(()=>{ let characteristicId = reads[cID];//要订阅的特征值 countConnect = countConnect+1; if(countConnect>10){//连接超过10次没有连接成功就直接返回 uni.hideLoading(); clearInterval(_this.notifyCycleTimer);//注册超时,清理掉定时器 closeBLEConnection();//注册超时就断开蓝牙的连接 closeBluetoothAdapter();//注册超时就关闭蓝牙适配器 uni.showModal({ title: '提示', content: '蓝牙连接失败,请重试!', showCancel:false, complete:function(){ //返回上一界面 uni.navigateBack({ delta:1 }); } }); } uni.notifyBLECharacteristicValueChange({ state: true, // 启用 notify 功能 deviceId, serviceId, characteristicId, success(res) { console.log('notifyBLECharacteristicValueChange success:' + characteristicId); cID=cID+1;//订阅成功就订阅下一个特征 if(cID==num){ uni.hideLoading(); clearInterval(_this.notifyCycleTimer);//注册监听成功就清理掉定时器 //返回上一界面 uni.navigateBack({ delta:1 }); } }, fail(e) { console.log("订阅失败"); } }); },500);//每隔500ms执行一次订阅 },
globalEvent不起作用;引发globalEvent不起作用的原因之一
Android原生插件
现象:插件的方法能正常运行,就是发送了mUniSDKInstance.fireGlobalEventCallback,uni这边的globalEvent监听不到消息,也不报错。
在onLoad中,接收页面传递参数时未判空,离线打包无任何错误提示,打包插件uni也不会有错误提示,即
//引发不起作用的原因
onLoad(options){
if (JSON.parse(options.print)) {
console.log("TO DO")
}
globalEvent.addEventListener('myEvent', (e) => {
console.log("myEvent:", JSON.stringify(e))
});
}
正确方式;
对options进行判空
onLoad(options){
if(optins!=null){
if (JSON.parse(options.print)) {
console.log("TO DO")
}
globalEvent.addEventListener('myEvent', (e) => {
console.log("myEvent:", JSON.stringify(e))
});
}
} Android原生插件
现象:插件的方法能正常运行,就是发送了mUniSDKInstance.fireGlobalEventCallback,uni这边的globalEvent监听不到消息,也不报错。
在onLoad中,接收页面传递参数时未判空,离线打包无任何错误提示,打包插件uni也不会有错误提示,即
//引发不起作用的原因
onLoad(options){
if (JSON.parse(options.print)) {
console.log("TO DO")
}
globalEvent.addEventListener('myEvent', (e) => {
console.log("myEvent:", JSON.stringify(e))
});
}
正确方式;
对options进行判空
onLoad(options){
if(optins!=null){
if (JSON.parse(options.print)) {
console.log("TO DO")
}
globalEvent.addEventListener('myEvent', (e) => {
console.log("myEvent:", JSON.stringify(e))
});
}
} 收起阅读 »
Wot Design Uni 增加 Table 表格组件 ,赶快进来看看效果吧!
Table 表格
用于展示多条结构类似的数据, 可对数据进行排序等操作。
地址
先看交互效果
基础用法
通过data设置表格数据。
<wd-table :data="dataList">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
const dataList = reactive([
{
name: '赵云',
school: '武汉市阳逻妇幼保健学院',
major: '计算机科学与技术专业'
},
{
name: '孔明',
school: '武汉市阳逻卧龙学院',
major: '计算机科学与技术专业'
},
{
name: '刘备',
school: '武汉市阳逻编织学院',
major: '计算机科学与技术专业'
}
])
固定列
通过fixed设置表格列是否固定展示,默认false。
::warning 提示
目前仅支持固定在左侧,且固定列组件的排列顺序要和实际想要展示的顺序相同。
::
<wd-table :data="dataList">
<wd-table-col prop="name" label="姓名" fixed></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
斑马纹
通过stripe设置表格是否展示斑马纹,默认true。
<wd-table :data="dataList" :stripe="false">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
边框
通过border设置表格是否展示边框,默认true。
<wd-table :data="dataList" :border="false">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
表格高度
通过height设置表格高度,默认为80vh。
<wd-table :data="dataList" height="328px">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
排序事件
当存在列参与排序时,点击会触发sort-method排序事件。
<wd-table :data="dataList" @sort-method="handleSort">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所" :sortable="true"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
function handleSort(e) {
console.log('这里是排序事件')
}
自定义列模板
自定义列的显示内容,可组合其他组件使用。
通过 Scoped slot 可以获取到 row 的数据,用法参考 demo。
<wd-table :data="dataList" @sort-method="handleSort">
<wd-table-col prop="name" label="姓名" fixed="true" width="320rpx" :sortable="true"></wd-table-col>
<wd-table-col prop="grade" label="分数" width="220rpx" :sortable="true">
<template #value="{row}">
<view class="custom-class">
<text>{{ row.grade }}</text>
<text>同比{{ row.compare }}</text>
</view>
</template>
</wd-table-col>
<wd-table-col prop="hobby" label="一言以蔽之" :sortable="true"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
<wd-table-col prop="gender" label="性别"></wd-table-col>
<wd-table-col prop="graduation" label="学成时间"></wd-table-col>
</wd-table>
import { ref } from 'vue'
const dataList = ref<Record<string, any>[]>([
{
name: '张飞',
school: '武汉市阳逻杀猪学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 56,
compare: '10%',
hobby: '燕人张飞在此!'
},
{
name: '关羽',
school: '武汉市阳逻绿豆学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 66,
compare: '11%',
hobby: '颜良文丑,以吾观之,如土鸡瓦犬耳。'
},
{
name: '刘备',
school: '武汉市阳逻编织学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 45,
compare: '1%',
hobby: '我得空明,如鱼得水也'
},
{
name: '赵云',
school: '武汉市阳逻妇幼保健学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 69,
compare: '14%',
hobby: '子龙,子龙,世无双'
},
{
name: '孔明',
school: '武汉市阳逻卧龙学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 88,
compare: '21%',
hobby: '兴汉讨贼,克复中原'
},
{
name: '姜维',
school: '武汉市阳逻停水停电技术学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 87,
compare: '32%',
hobby: '我计不成,乃天命也!'
}
])
/**
* 排序
* @param e
*/
function handleSort(e) {
dataList.value = dataList.value.reverse()
}
.custom-class {
height: 80rpx;
width: 220rpx;
display: flex;
flex-direction: col;
align-items: center;
}
Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| data | 显示的数据 | Array | - | - | 0.0.39 |
| border | 是否带有边框 | boolean | - | true | 0.0.39 |
| stripe | 是否为斑马纹表 | boolean | - | true | 0.0.39 |
| height | Table 的高度,默认为80vh |
string | - | 80vh |
0.0.39 |
| rowHeight | 行高 | number / string |
- | 50 | 0.0.39 |
| showHeader | 是否显示表头 | boolean | - | true | 0.0.39 |
| ellipsis | 是否超出2行隐藏 | boolean | - | true | 0.0.39 |
TableColumn Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| prop | 字段名称,对应列内容的字段名 | string | - | - | 0.0.39 |
| label | 显示的标题 | string | - | - | 0.0.39 |
| width | 对应列的宽度,单位为px | number / string | - | 100 | 0.0.39 |
| sortable | 是否开启列排序 | boolean | - | false | 0.0.39 |
| fixed | 是否固定本列 | boolean | - | false | 0.0.39 |
| align | 列的对齐方式 | AlignType | left, center, right | left | 0.0.39 |
地址
Table 表格
用于展示多条结构类似的数据, 可对数据进行排序等操作。
地址
先看交互效果
基础用法
通过data设置表格数据。
<wd-table :data="dataList">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
const dataList = reactive([
{
name: '赵云',
school: '武汉市阳逻妇幼保健学院',
major: '计算机科学与技术专业'
},
{
name: '孔明',
school: '武汉市阳逻卧龙学院',
major: '计算机科学与技术专业'
},
{
name: '刘备',
school: '武汉市阳逻编织学院',
major: '计算机科学与技术专业'
}
])
固定列
通过fixed设置表格列是否固定展示,默认false。
::warning 提示
目前仅支持固定在左侧,且固定列组件的排列顺序要和实际想要展示的顺序相同。
::
<wd-table :data="dataList">
<wd-table-col prop="name" label="姓名" fixed></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
斑马纹
通过stripe设置表格是否展示斑马纹,默认true。
<wd-table :data="dataList" :stripe="false">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
边框
通过border设置表格是否展示边框,默认true。
<wd-table :data="dataList" :border="false">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
表格高度
通过height设置表格高度,默认为80vh。
<wd-table :data="dataList" height="328px">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
排序事件
当存在列参与排序时,点击会触发sort-method排序事件。
<wd-table :data="dataList" @sort-method="handleSort">
<wd-table-col prop="name" label="姓名"></wd-table-col>
<wd-table-col prop="school" label="求学之所" :sortable="true"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
</wd-table>
function handleSort(e) {
console.log('这里是排序事件')
}
自定义列模板
自定义列的显示内容,可组合其他组件使用。
通过 Scoped slot 可以获取到 row 的数据,用法参考 demo。
<wd-table :data="dataList" @sort-method="handleSort">
<wd-table-col prop="name" label="姓名" fixed="true" width="320rpx" :sortable="true"></wd-table-col>
<wd-table-col prop="grade" label="分数" width="220rpx" :sortable="true">
<template #value="{row}">
<view class="custom-class">
<text>{{ row.grade }}</text>
<text>同比{{ row.compare }}</text>
</view>
</template>
</wd-table-col>
<wd-table-col prop="hobby" label="一言以蔽之" :sortable="true"></wd-table-col>
<wd-table-col prop="school" label="求学之所"></wd-table-col>
<wd-table-col prop="major" label="专业"></wd-table-col>
<wd-table-col prop="gender" label="性别"></wd-table-col>
<wd-table-col prop="graduation" label="学成时间"></wd-table-col>
</wd-table>
import { ref } from 'vue'
const dataList = ref<Record<string, any>[]>([
{
name: '张飞',
school: '武汉市阳逻杀猪学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 56,
compare: '10%',
hobby: '燕人张飞在此!'
},
{
name: '关羽',
school: '武汉市阳逻绿豆学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 66,
compare: '11%',
hobby: '颜良文丑,以吾观之,如土鸡瓦犬耳。'
},
{
name: '刘备',
school: '武汉市阳逻编织学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 45,
compare: '1%',
hobby: '我得空明,如鱼得水也'
},
{
name: '赵云',
school: '武汉市阳逻妇幼保健学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 69,
compare: '14%',
hobby: '子龙,子龙,世无双'
},
{
name: '孔明',
school: '武汉市阳逻卧龙学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 88,
compare: '21%',
hobby: '兴汉讨贼,克复中原'
},
{
name: '姜维',
school: '武汉市阳逻停水停电技术学院',
major: '计算机科学与技术专业',
gender: '男',
graduation: '2022年1月12日',
grade: 87,
compare: '32%',
hobby: '我计不成,乃天命也!'
}
])
/**
* 排序
* @param e
*/
function handleSort(e) {
dataList.value = dataList.value.reverse()
}
.custom-class {
height: 80rpx;
width: 220rpx;
display: flex;
flex-direction: col;
align-items: center;
}
Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| data | 显示的数据 | Array | - | - | 0.0.39 |
| border | 是否带有边框 | boolean | - | true | 0.0.39 |
| stripe | 是否为斑马纹表 | boolean | - | true | 0.0.39 |
| height | Table 的高度,默认为80vh |
string | - | 80vh |
0.0.39 |
| rowHeight | 行高 | number / string |
- | 50 | 0.0.39 |
| showHeader | 是否显示表头 | boolean | - | true | 0.0.39 |
| ellipsis | 是否超出2行隐藏 | boolean | - | true | 0.0.39 |
TableColumn Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|---|---|---|---|---|---|
| prop | 字段名称,对应列内容的字段名 | string | - | - | 0.0.39 |
| label | 显示的标题 | string | - | - | 0.0.39 |
| width | 对应列的宽度,单位为px | number / string | - | 100 | 0.0.39 |
| sortable | 是否开启列排序 | boolean | - | false | 0.0.39 |
| fixed | 是否固定本列 | boolean | - | false | 0.0.39 |
| align | 列的对齐方式 | AlignType | left, center, right | left | 0.0.39 |
地址
收起阅读 »uniapp对微信小程序异步加载分包组件的做法--vite插件方案
微信原生文档见 文档
社区有方案,是在style中写入
这里的方案是使用vite插件,在编译完成后,对目标插件进行代码注入 (仅适用vue3 + vite) , 目前运行在uniapp 微信小程序上
目录结构:
/project-config
componentPlaceholder
componentPlaceholder.js
vite.uni-component-placeholder.js
vite插件代码
/**
实现了 uni编译完成后 , 处理 componentPlaceholder
*/
var fs = require('fs');
var path = require('path');
class ProcessComponentPlaceholder{
constructor(){
this.destFolder = process.env.UNI_OUTPUT_DIR;
}
process(){
let dev_fold = process.env.UNI_INPUT_DIR;
let dir = dev_fold+"/project-config/componentPlaceHolder/"
let files = fs.readdirSync(dir, 'utf-8');
let fileMap = {}
files.some((f) => {
let p = dir + "/" + f;
let stat = fs.lstatSync(p);
if (!stat.isDirectory()) {
fileMap[f] = p;
}
})
this.processNodes(fileMap)
}
processNodes(fileMap){
console.log("处理异步组件引用componentPlaceHolder",fileMap)
for(let jsonFileName in fileMap){
let path = fileMap[jsonFileName];
if(!path.lastIndexOf(".js")<0)continue;
let obj = require(path);
// console.log("读componentPlaceHolder", obj)
this.processOneConfig(obj);
}
console.table("componentPlaceHolder处理完毕")
}
processOneConfig(config){
for(let f in config){ //某个配置文件
let weixinJSONFile = this.destFolder+f+".json";
fs.readFile(weixinJSONFile,'utf8',(err, data)=>{
if (err) {
return console.log('componentPlaceHolder文件读取失败,失败原因是:' + err)
}
let weixinJSON = JSON.parse(data);
// console.log("读componentPlaceHolder",weixinJSON);
//准备合并配置
let usingComponents = weixinJSON["usingComponents"]||{};
let componentPlaceholder = weixinJSON["componentPlaceholder"]||{};
let customConfig = config[f];
for(let tag in customConfig) {
let tagVal = customConfig[tag];
let path = tagVal.path;
let replace = tagVal.replace;
// console.log(weixinJSONFile+ " " +tag+" "+path )
if(!usingComponents[tag]){
usingComponents[tag]="../.."+path; //这里的双层目录有必要可能动态算相对层级,根据项目自身情况而定
}
if(!componentPlaceholder[tag]){
componentPlaceholder[tag] = replace;
}
}
weixinJSON.usingComponents = usingComponents;
weixinJSON.componentPlaceholder = componentPlaceholder;
fs.writeFileSync(weixinJSONFile, JSON.stringify(weixinJSON,null,4))
}) ;
}
}
}
export default (options)=> {
var name = 'vite-plugin-copy-uniapp_config';
return {
name: name,
enforce: 'post',
closeBundle:()=>{ //buildEnd之后运行
options.forEach(function(option) {
let processor = new ProcessComponentPlaceholder();
processor.process();
});
}
};
}
配置文件 componentPlaceholder.js 代码:
/**
配置:
{
"某个包的组件路径,不带.vue后缀":{
"组件名,一般为文件名不带.vue和路径":{
path:"引用某个包的组件路径,不带.vue后缀",
replace:"未加载完成时的替换组件,比如view或某个全局组件"
}
}
}
*/
module.exports = {
"/pages/index/index":{
"tabbar-me":{
path:"/package-my/pages/my/my",
replace:"view"
},
},
}
vite.config.js
import viteComponentPlaceHolder from "./project-config/vite.uni-component-placeholder.js"
plugins.push(viteComponentPlaceHolder([{}]))
export default defineConfig({
plugins
});
启动时会有日志
08:48:43.602 处理异步组件引用componentPlaceHolder {
08:48:43.609 'componentPlaceholder.js': '/project-config/componentPlaceHolder//componentPlaceholder.js'
08:48:43.610 }
微信原生文档见 文档
社区有方案,是在style中写入
这里的方案是使用vite插件,在编译完成后,对目标插件进行代码注入 (仅适用vue3 + vite) , 目前运行在uniapp 微信小程序上
目录结构:
/project-config
componentPlaceholder
componentPlaceholder.js
vite.uni-component-placeholder.js
vite插件代码
/**
实现了 uni编译完成后 , 处理 componentPlaceholder
*/
var fs = require('fs');
var path = require('path');
class ProcessComponentPlaceholder{
constructor(){
this.destFolder = process.env.UNI_OUTPUT_DIR;
}
process(){
let dev_fold = process.env.UNI_INPUT_DIR;
let dir = dev_fold+"/project-config/componentPlaceHolder/"
let files = fs.readdirSync(dir, 'utf-8');
let fileMap = {}
files.some((f) => {
let p = dir + "/" + f;
let stat = fs.lstatSync(p);
if (!stat.isDirectory()) {
fileMap[f] = p;
}
})
this.processNodes(fileMap)
}
processNodes(fileMap){
console.log("处理异步组件引用componentPlaceHolder",fileMap)
for(let jsonFileName in fileMap){
let path = fileMap[jsonFileName];
if(!path.lastIndexOf(".js")<0)continue;
let obj = require(path);
// console.log("读componentPlaceHolder", obj)
this.processOneConfig(obj);
}
console.table("componentPlaceHolder处理完毕")
}
processOneConfig(config){
for(let f in config){ //某个配置文件
let weixinJSONFile = this.destFolder+f+".json";
fs.readFile(weixinJSONFile,'utf8',(err, data)=>{
if (err) {
return console.log('componentPlaceHolder文件读取失败,失败原因是:' + err)
}
let weixinJSON = JSON.parse(data);
// console.log("读componentPlaceHolder",weixinJSON);
//准备合并配置
let usingComponents = weixinJSON["usingComponents"]||{};
let componentPlaceholder = weixinJSON["componentPlaceholder"]||{};
let customConfig = config[f];
for(let tag in customConfig) {
let tagVal = customConfig[tag];
let path = tagVal.path;
let replace = tagVal.replace;
// console.log(weixinJSONFile+ " " +tag+" "+path )
if(!usingComponents[tag]){
usingComponents[tag]="../.."+path; //这里的双层目录有必要可能动态算相对层级,根据项目自身情况而定
}
if(!componentPlaceholder[tag]){
componentPlaceholder[tag] = replace;
}
}
weixinJSON.usingComponents = usingComponents;
weixinJSON.componentPlaceholder = componentPlaceholder;
fs.writeFileSync(weixinJSONFile, JSON.stringify(weixinJSON,null,4))
}) ;
}
}
}
export default (options)=> {
var name = 'vite-plugin-copy-uniapp_config';
return {
name: name,
enforce: 'post',
closeBundle:()=>{ //buildEnd之后运行
options.forEach(function(option) {
let processor = new ProcessComponentPlaceholder();
processor.process();
});
}
};
}
配置文件 componentPlaceholder.js 代码:
/**
配置:
{
"某个包的组件路径,不带.vue后缀":{
"组件名,一般为文件名不带.vue和路径":{
path:"引用某个包的组件路径,不带.vue后缀",
replace:"未加载完成时的替换组件,比如view或某个全局组件"
}
}
}
*/
module.exports = {
"/pages/index/index":{
"tabbar-me":{
path:"/package-my/pages/my/my",
replace:"view"
},
},
}
vite.config.js
import viteComponentPlaceHolder from "./project-config/vite.uni-component-placeholder.js"
plugins.push(viteComponentPlaceHolder([{}]))
export default defineConfig({
plugins
});
启动时会有日志
08:48:43.602 处理异步组件引用componentPlaceHolder {
08:48:43.609 'componentPlaceholder.js': '/project-config/componentPlaceHolder//componentPlaceholder.js'
08:48:43.610 }
七分钟掌握 uni-app自定义基座,离线打包的生成方法
// what's uapp
const uapp = 'universal app'
uapp 是一款跨平台APP开发工具箱,所有积累都来自多年产品开发中的不断实践。开发者仅需写一套代码,就能横扫所有平台。
制作了一个七分钟视频,包含了android,ios 手机平台上,离线打包,自定义基座的制作方法,视频首秀,喜欢的小伙伴帮忙点赞支持
// what's uapp
const uapp = 'universal app'
uapp 是一款跨平台APP开发工具箱,所有积累都来自多年产品开发中的不断实践。开发者仅需写一套代码,就能横扫所有平台。
制作了一个七分钟视频,包含了android,ios 手机平台上,离线打包,自定义基座的制作方法,视频首秀,喜欢的小伙伴帮忙点赞支持
https://www.ixigua.com/7294197408232276495
收起阅读 »uni-app 开发中,监听 input 键盘事件获取不到按下按键值怎么办?
uniapp 开发 H5 时,无法监听按钮键盘事件的原因以及解决方法。
问题描述:
不少 uni-app 开发者在使用 input 组件时,监听 keyup 事件时,获取不到键盘的 keyCode。编写的代码如下:
<template>
<input @keyup="handleKeyUp">
</template>
但是在 handleKeyUp() 方法里获取不到键盘的编码,出现这个问题的原因是 uni-app 的内置组件 <input> 其实是封装过的,编译为 h5 时不是 html 原生的 input 元素,所以才无法监听原生的键盘事件。
解决方法参考这个:uni-app input 键盘事件获取按键值
uniapp 开发 H5 时,无法监听按钮键盘事件的原因以及解决方法。
问题描述:
不少 uni-app 开发者在使用 input 组件时,监听 keyup 事件时,获取不到键盘的 keyCode。编写的代码如下:
<template>
<input @keyup="handleKeyUp">
</template>
但是在 handleKeyUp() 方法里获取不到键盘的编码,出现这个问题的原因是 uni-app 的内置组件 <input> 其实是封装过的,编译为 h5 时不是 html 原生的 input 元素,所以才无法监听原生的键盘事件。
解决方法参考这个:uni-app input 键盘事件获取按键值
收起阅读 »uni-app通过SSE支持流式效果
uni-app支持SSE
因为uni.request没有办法支持SSE,为此尝试了各种方案,着了大急。
现将尝试的各种方案进行一个整理,若有疏漏请大家帮忙补充。
H5
因为Web端运行在浏览器内核上,SSE的支持是比较完备的,可以使用axios、@microsoft/fetch-event-source 等实现,各种案例也比较完善因此不再赘述。
微信小程序
微信小程序的SSE方案参考的是《微信小程序除了WebSocket其他思路实现流传输文字(打字机)效果》
因为我们是在uniapp中实现,所以在原文方案的基础上使用的uni相关的API来实现,考虑到要实现停止和兼容H5的的接口,最后引入了abort-controller 和 @uni-helper/uni-network来进行封装。
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
/**
* 二进制解析成文本
* @param data 二进制数据
* @returns 文本
*/
export function decodeArrayBuffer(data: ArrayBuffer | undefined) {
if (!data) {
return ''
}
return decodeUsingURIComponent(data)
}
/**
* URIComponent解码二进制流(不用引入额外包)
* @param data 二进制流
* @returns 文本
*/
function decodeUsingURIComponent(data: ArrayBuffer) {
const uint8Array = new Uint8Array(data)
let text = String.fromCharCode(...uint8Array)
try {
text = decodeURIComponent(escape(text))
} catch (e) {
console.error('decodeUsingURIComponent: Can not decodeURI ', text)
}
return text
}
type onStreamReceivedListener = (text: string) => void
export function fetchStreamChat(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
const onHeadersReceived = (response?: { headers?: UnHeaders }) => {
console.log('fetchStreamChat.onHeadersReceived: ', response?.headers)
}
const onChunkReceived = (response?: { data?: ArrayBuffer }) => {
const text = decodeArrayBuffer(response?.data)
listener?.(text)
}
return post<string>({
url: '/openai/completions/stream',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
token: 'your-token'
},
data: {
content: params.prompt,
scene: params.uuid,
source: 'gpt4',
},
responseType: 'arraybuffer',
enableChunked: true,
onHeadersReceived,
onChunkReceived,
signal: signal
})
}
特殊注意以上代码使用时对abort-controller的引入方式
import AbortController from 'abort-controller/dist/abort-controller'
let controller = new AbortController()
const onResponseListener = async (responseText: string) => {
console.log('==response==\n', responseText)
}
await fetchStreamChat({ prompt, uuid }, controller.signal, onResponseListener)
后端NGINX配置
# 注意这里只配置代理发送接口,不然其他接口也会受影响
location /openai/completions/stream {
# ...more config
proxy_set_header Transfer-Encoding "";
chunked_transfer_encoding on;
proxy_buffering off;
}
APP
App目前看到的方案最多,但是目前为止没有找到很合适的方案。有更好的方案请各位大佬补充Thanks♪(・ω・)ノ
1. plus.net.XMLHttpRequest
参考方案 《XMLHttpRequest模块管理网络请求》,具体代码如下
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
type onStreamReceivedListener = (text: string) => void
export class CanceledError extends Error {
constructor(message?: string) {
super(message ?? 'canceled')
}
}
export function fetchStreamChatForApp(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
return new Promise((resolve, reject) => {
// 梳理好请求数据
const token = 'your-token'
const data = JSON.stringify({
content: params.prompt,
scene: params.uuid,
source: 'gpt3.5',
})
// 处理资源释放
let onCanceled: UnCancelTokenListener
const done = () => {
signal?.removeEventListener?.('abort', onCanceled)
}
// 封装请求
// @ts-ignore
let xhr: plus.net.XMLHttpRequest | undefined
// @ts-ignore
xhr = new plus.net.XMLHttpRequest()
xhr.withCredentials = true
// 配置终止逻辑
if (signal) {
signal.addEventListener?.('abort', () => {
console.log('fetchStreamChatForApp signal abort')
xhr.abort()
})
}
let nLastIndex = 0
xhr.onreadystatechange = function () {
console.log(`onreadystatechange(${xhr.readyState}) → `)
if (xhr.readyState === 4) {
if (nLastIndex < xhr.responseText.length) {
const responseText = xhr.responseText as string
// 处理 HTTP 数据块
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
}
}
if (xhr.status === 200) {
resolve({ code: ResultCode.SUCCESS, msg: 'end' })
done()
} else {
reject(new Error(xhr.statusText))
done()
}
}
}
xhr.onprogress = function (event: any) {
const responseText = xhr.responseText
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
console.log('onprogress ', chunk)
}
}
xhr.onerror = function (error: any) {
console.error('Network Error:', error)
reject(error)
done()
}
// 配置请求
xhr.open('POST', 'https://your-site/api/openai/completions/stream')
xhr.setRequestHeader('Accept', 'text/event-stream')
xhr.setRequestHeader('token', token)
xhr.setRequestHeader('User-Agent', 'Mobile')
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Host', 'mapi.lawvector.cn')
xhr.setRequestHeader('Connection', 'keep-alive')
// 处理终止逻辑
if (signal) {
onCanceled = cancel => {
console.log('fetchStreamChatForApp onCanceled ', cancel)
if (!xhr) {
return
}
reject(new CanceledError('canceled'))
xhr.abort()
xhr = undefined
}
// @ts-expect-error no types
signal?.aborted ? onCanceled() : signal?.addEventListener('abort', onCanceled)
}
xhr.send(data)
})
}
当前方案经验证,可以从流式接口获取到数据,但是流式效果不太好,而且从网上汇总来的信息来看,plus.net存在较多问题,比如《plus.net.XMLHttpRequest()在苹果端移动网络环境下不能使用》等。因此 不推荐 plus.net方案
2. event-source-polyfill
参考方案《OpenAI流式请求实现方案》、《react + ts + event-source-polyfill 实现方案》、《Vue中使用eventSource处理ChatGPT聊天SSE长连接获取数据》
实际App上运行发现报错 TypeError: XMLHttpRequest is not a constructor
哪位大佬可以解决上述问题请补充,不胜感激!
3. fetch-event-source
参考方案《js调用SSE客户端》、《fetch-event-source源码解析》、ChatGPT-SSE流式响应
经过验证发现单独引入@microsoft/fetch-event-source 会抛出异常
从ChatGPT-SSE流式响应分析应该是需要结合renderjs进行使用。
目前推荐使用该方案~
4. App原生语言插件
参考《EventSource (sse)等自定义网络请求 》
因为该插件目前仅支持Android,不推荐。
理论上,原生插件是一定能够解决这个问题,期待大佬们开发更完善的原生插件。
uni-app支持SSE
因为uni.request没有办法支持SSE,为此尝试了各种方案,着了大急。
现将尝试的各种方案进行一个整理,若有疏漏请大家帮忙补充。
H5
因为Web端运行在浏览器内核上,SSE的支持是比较完备的,可以使用axios、@microsoft/fetch-event-source 等实现,各种案例也比较完善因此不再赘述。
微信小程序
微信小程序的SSE方案参考的是《微信小程序除了WebSocket其他思路实现流传输文字(打字机)效果》
因为我们是在uniapp中实现,所以在原文方案的基础上使用的uni相关的API来实现,考虑到要实现停止和兼容H5的的接口,最后引入了abort-controller 和 @uni-helper/uni-network来进行封装。
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
/**
* 二进制解析成文本
* @param data 二进制数据
* @returns 文本
*/
export function decodeArrayBuffer(data: ArrayBuffer | undefined) {
if (!data) {
return ''
}
return decodeUsingURIComponent(data)
}
/**
* URIComponent解码二进制流(不用引入额外包)
* @param data 二进制流
* @returns 文本
*/
function decodeUsingURIComponent(data: ArrayBuffer) {
const uint8Array = new Uint8Array(data)
let text = String.fromCharCode(...uint8Array)
try {
text = decodeURIComponent(escape(text))
} catch (e) {
console.error('decodeUsingURIComponent: Can not decodeURI ', text)
}
return text
}
type onStreamReceivedListener = (text: string) => void
export function fetchStreamChat(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
const onHeadersReceived = (response?: { headers?: UnHeaders }) => {
console.log('fetchStreamChat.onHeadersReceived: ', response?.headers)
}
const onChunkReceived = (response?: { data?: ArrayBuffer }) => {
const text = decodeArrayBuffer(response?.data)
listener?.(text)
}
return post<string>({
url: '/openai/completions/stream',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
token: 'your-token'
},
data: {
content: params.prompt,
scene: params.uuid,
source: 'gpt4',
},
responseType: 'arraybuffer',
enableChunked: true,
onHeadersReceived,
onChunkReceived,
signal: signal
})
}
特殊注意以上代码使用时对abort-controller的引入方式
import AbortController from 'abort-controller/dist/abort-controller'
let controller = new AbortController()
const onResponseListener = async (responseText: string) => {
console.log('==response==\n', responseText)
}
await fetchStreamChat({ prompt, uuid }, controller.signal, onResponseListener)
后端NGINX配置
# 注意这里只配置代理发送接口,不然其他接口也会受影响
location /openai/completions/stream {
# ...more config
proxy_set_header Transfer-Encoding "";
chunked_transfer_encoding on;
proxy_buffering off;
}
APP
App目前看到的方案最多,但是目前为止没有找到很合适的方案。有更好的方案请各位大佬补充Thanks♪(・ω・)ノ
1. plus.net.XMLHttpRequest
参考方案 《XMLHttpRequest模块管理网络请求》,具体代码如下
import type { UnCancelTokenListener, UnGenericAbortSignal, UnHeaders } from '@uni-helper/uni-network'
type onStreamReceivedListener = (text: string) => void
export class CanceledError extends Error {
constructor(message?: string) {
super(message ?? 'canceled')
}
}
export function fetchStreamChatForApp(
params: { prompt: string; uuid: string },
signal?: UnGenericAbortSignal,
listener?: onStreamReceivedListener
) {
return new Promise((resolve, reject) => {
// 梳理好请求数据
const token = 'your-token'
const data = JSON.stringify({
content: params.prompt,
scene: params.uuid,
source: 'gpt3.5',
})
// 处理资源释放
let onCanceled: UnCancelTokenListener
const done = () => {
signal?.removeEventListener?.('abort', onCanceled)
}
// 封装请求
// @ts-ignore
let xhr: plus.net.XMLHttpRequest | undefined
// @ts-ignore
xhr = new plus.net.XMLHttpRequest()
xhr.withCredentials = true
// 配置终止逻辑
if (signal) {
signal.addEventListener?.('abort', () => {
console.log('fetchStreamChatForApp signal abort')
xhr.abort()
})
}
let nLastIndex = 0
xhr.onreadystatechange = function () {
console.log(`onreadystatechange(${xhr.readyState}) → `)
if (xhr.readyState === 4) {
if (nLastIndex < xhr.responseText.length) {
const responseText = xhr.responseText as string
// 处理 HTTP 数据块
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
}
}
if (xhr.status === 200) {
resolve({ code: ResultCode.SUCCESS, msg: 'end' })
done()
} else {
reject(new Error(xhr.statusText))
done()
}
}
}
xhr.onprogress = function (event: any) {
const responseText = xhr.responseText
if (responseText) {
const textLen = responseText.length
const chunk = responseText.substring(nLastIndex)
nLastIndex = textLen
listener?.(chunk)
console.log('onprogress ', chunk)
}
}
xhr.onerror = function (error: any) {
console.error('Network Error:', error)
reject(error)
done()
}
// 配置请求
xhr.open('POST', 'https://your-site/api/openai/completions/stream')
xhr.setRequestHeader('Accept', 'text/event-stream')
xhr.setRequestHeader('token', token)
xhr.setRequestHeader('User-Agent', 'Mobile')
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Host', 'mapi.lawvector.cn')
xhr.setRequestHeader('Connection', 'keep-alive')
// 处理终止逻辑
if (signal) {
onCanceled = cancel => {
console.log('fetchStreamChatForApp onCanceled ', cancel)
if (!xhr) {
return
}
reject(new CanceledError('canceled'))
xhr.abort()
xhr = undefined
}
// @ts-expect-error no types
signal?.aborted ? onCanceled() : signal?.addEventListener('abort', onCanceled)
}
xhr.send(data)
})
}
当前方案经验证,可以从流式接口获取到数据,但是流式效果不太好,而且从网上汇总来的信息来看,plus.net存在较多问题,比如《plus.net.XMLHttpRequest()在苹果端移动网络环境下不能使用》等。因此 不推荐 plus.net方案
2. event-source-polyfill
参考方案《OpenAI流式请求实现方案》、《react + ts + event-source-polyfill 实现方案》、《Vue中使用eventSource处理ChatGPT聊天SSE长连接获取数据》
实际App上运行发现报错 TypeError: XMLHttpRequest is not a constructor
哪位大佬可以解决上述问题请补充,不胜感激!
3. fetch-event-source
参考方案《js调用SSE客户端》、《fetch-event-source源码解析》、ChatGPT-SSE流式响应
经过验证发现单独引入@microsoft/fetch-event-source 会抛出异常
从ChatGPT-SSE流式响应分析应该是需要结合renderjs进行使用。
目前推荐使用该方案~
4. App原生语言插件
参考《EventSource (sse)等自定义网络请求 》
因为该插件目前仅支持Android,不推荐。
理论上,原生插件是一定能够解决这个问题,期待大佬们开发更完善的原生插件。
还在手动上传小程序? uni-mini-ci帮你一键发布多平台uni-app小程序
背景
在没有CI工具帮助的时候,我们使用uni-app开发小程序通常会在小程序发版时先进行 build 构建,然后在小程序开发者工具中打开构建产物,然后进行上传代码的操作。这种人力运维的稳定性是不可控的,而且当我们一次要发布多个平台或者多个小程序时,这种人力运维的工作方式将浪费我们大量的时间。
为了解决这个问题,微信推出了miniprogram-ci,开发者可不打开小程序开发者工具,独立使用 miniprogram-ci 进行小程序代码的上传、预览等操作,随后支付宝、钉钉等其他小程序厂商也跟着推出了各自的CI工具,这一举措将开发者从繁琐的上传流程中解放了出来。
uni-mini-ci是一款支持微信、钉钉、支付宝、企业微信等小程序平台的持续集成工具。它集成了多个平台的ci工具一次配置发布到多端,让开发者可以轻松地将应用程序发布到多个小程序平台上。
为什么不直接使用各自平台的CI工具?
- 多个小程序平台
CI工具的配置文件、配置项等有所差异,其能力也有所不同, uni-app存在构建的步骤,使用各自小程序平台的CI工具则需要在构建产物中各自创建配置文件。- 钉钉小程序的DingTalk Design CLI,不支持指定版本号,而
uni-mini-ci支持(至于为什么支持,可以去看一下uni-mini-ci的文档)。
所以使用uni-mini-ci可以让多平台的小程序持续集成统一化。
在哪些场景使用?
- Github Actions 小程序持续集成
- Jenkins 小程序持续集成
- GitLab CI/CD 小程序持续集成
- 本地上传多个小程序平台
借助Jenkins等工具可以将开发者从构建、上传小程序的工作中,彻底解放出来,只需动动手指,即可实现小程序的上传。
示例与实践
Jenkins示例
这里是一个简单的Jenkins示例,其中关于.minicirc的配置可见uni-mini-ci,示例中的变量都可以在jenkins中定义。
这里jenkins环境要全局安装
uni-mini-ci## 安装依赖 npm install
检查配置文件是否存在
if [ -f .minicirc ]; then
如果文件存在,删除它
rm .minicirc
fi
将配置写入配置文件
echo '{
"dd": {
"appid": "'$miniAppId'",
"token": "'$token'",
"projectPath": "dist/build/mp-alipay",
"autoincrement": '$autoincrement'
},
"weixin": {
"appid": "'$appid'",
"privateKeyPath": "build/ci_keys/private.'$appid'.key",
"projectPath": "dist/build/mp-weixin",
"setting": {
"minifyJS": true,
"minifyWXML": true,
"minifyWXSS": true,
"minify": true
},
"version": "'$version'",
"desc": "'$desc'"
}' > .minicirc
构建钉钉并上传
npm run build:mp-dingtalk
minici --platform dd
构建微信并上传
npm run build:mp-weixin
minici --platform weixin
### Github Actions 示例
Github Actions与jenkins的实现基本一致,不过Github Actions是使用`.yml`作为配置文件的。
```yaml
name: Upload To Weixin Alipay
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Dependencies
run: |
npm i uni-mini-ci -g
yarn install
- shell: bash
env:
PRIVATE_KEY: ${{ secrets.MP_PRIVATE_KEY }}
APPID: ${{ secrets.MP_APPID }}
TOOLID: ${{ secrets.ALI_TOOL_ID }}
ALI_APPID: ${{ secrets.ALI_APPID }}
ALI_PRIVATE_KEY: ${{ secrets.ALI_PRIVATE_KEY }}
run: |
echo "$PRIVATE_KEY" > private.key
echo '{
"alipay": {
"appid": "'$ALI_APPID'",
"toolId":"'$TOOLID'",
"privateKey": "'$ALI_PRIVATE_KEY'",
"projectPath": "dist/build/mp-alipay",
"autoincrement":true
},
"weixin": {
"appid": "'$APPID'",
"privateKeyPath": "private.key",
"projectPath": "dist/build/mp-weixin",
"setting": {
"minifyJS": true,
"minifyWXML": true,
"minifyWXSS": true,
"minify": true
}
},
"version": "",
"desc": ""
}' > .minicirc
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: "14.x"
- name: upload
run: |
npm run build:mp-alipay
minici --platform alipay
# 构建微信并上传
npm run build:mp-weixin
minici --platform weixin
总结
通过使用uni-mini-ci,我们简化了使用uni-app开发的小程序的发布流程。借助Github Actions、Jenkins、GitLab CI/CD等工具的能力,我们可以实现小程序的自动化构建和部署,提高开发效率,使开发者能够更专注于业务逻辑的开发。
相关文章
背景
在没有CI工具帮助的时候,我们使用uni-app开发小程序通常会在小程序发版时先进行 build 构建,然后在小程序开发者工具中打开构建产物,然后进行上传代码的操作。这种人力运维的稳定性是不可控的,而且当我们一次要发布多个平台或者多个小程序时,这种人力运维的工作方式将浪费我们大量的时间。
为了解决这个问题,微信推出了miniprogram-ci,开发者可不打开小程序开发者工具,独立使用 miniprogram-ci 进行小程序代码的上传、预览等操作,随后支付宝、钉钉等其他小程序厂商也跟着推出了各自的CI工具,这一举措将开发者从繁琐的上传流程中解放了出来。
uni-mini-ci是一款支持微信、钉钉、支付宝、企业微信等小程序平台的持续集成工具。它集成了多个平台的ci工具一次配置发布到多端,让开发者可以轻松地将应用程序发布到多个小程序平台上。
为什么不直接使用各自平台的CI工具?
- 多个小程序平台
CI工具的配置文件、配置项等有所差异,其能力也有所不同, uni-app存在构建的步骤,使用各自小程序平台的CI工具则需要在构建产物中各自创建配置文件。- 钉钉小程序的DingTalk Design CLI,不支持指定版本号,而
uni-mini-ci支持(至于为什么支持,可以去看一下uni-mini-ci的文档)。
所以使用uni-mini-ci可以让多平台的小程序持续集成统一化。
在哪些场景使用?
- Github Actions 小程序持续集成
- Jenkins 小程序持续集成
- GitLab CI/CD 小程序持续集成
- 本地上传多个小程序平台
借助Jenkins等工具可以将开发者从构建、上传小程序的工作中,彻底解放出来,只需动动手指,即可实现小程序的上传。
示例与实践
Jenkins示例
这里是一个简单的Jenkins示例,其中关于.minicirc的配置可见uni-mini-ci,示例中的变量都可以在jenkins中定义。
这里jenkins环境要全局安装
uni-mini-ci## 安装依赖 npm install
检查配置文件是否存在
if [ -f .minicirc ]; then
如果文件存在,删除它
rm .minicirc
fi
将配置写入配置文件
echo '{
"dd": {
"appid": "'$miniAppId'",
"token": "'$token'",
"projectPath": "dist/build/mp-alipay",
"autoincrement": '$autoincrement'
},
"weixin": {
"appid": "'$appid'",
"privateKeyPath": "build/ci_keys/private.'$appid'.key",
"projectPath": "dist/build/mp-weixin",
"setting": {
"minifyJS": true,
"minifyWXML": true,
"minifyWXSS": true,
"minify": true
},
"version": "'$version'",
"desc": "'$desc'"
}' > .minicirc
构建钉钉并上传
npm run build:mp-dingtalk
minici --platform dd
构建微信并上传
npm run build:mp-weixin
minici --platform weixin
### Github Actions 示例
Github Actions与jenkins的实现基本一致,不过Github Actions是使用`.yml`作为配置文件的。
```yaml
name: Upload To Weixin Alipay
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Dependencies
run: |
npm i uni-mini-ci -g
yarn install
- shell: bash
env:
PRIVATE_KEY: ${{ secrets.MP_PRIVATE_KEY }}
APPID: ${{ secrets.MP_APPID }}
TOOLID: ${{ secrets.ALI_TOOL_ID }}
ALI_APPID: ${{ secrets.ALI_APPID }}
ALI_PRIVATE_KEY: ${{ secrets.ALI_PRIVATE_KEY }}
run: |
echo "$PRIVATE_KEY" > private.key
echo '{
"alipay": {
"appid": "'$ALI_APPID'",
"toolId":"'$TOOLID'",
"privateKey": "'$ALI_PRIVATE_KEY'",
"projectPath": "dist/build/mp-alipay",
"autoincrement":true
},
"weixin": {
"appid": "'$APPID'",
"privateKeyPath": "private.key",
"projectPath": "dist/build/mp-weixin",
"setting": {
"minifyJS": true,
"minifyWXML": true,
"minifyWXSS": true,
"minify": true
}
},
"version": "",
"desc": ""
}' > .minicirc
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: "14.x"
- name: upload
run: |
npm run build:mp-alipay
minici --platform alipay
# 构建微信并上传
npm run build:mp-weixin
minici --platform weixin
总结
通过使用uni-mini-ci,我们简化了使用uni-app开发的小程序的发布流程。借助Github Actions、Jenkins、GitLab CI/CD等工具的能力,我们可以实现小程序的自动化构建和部署,提高开发效率,使开发者能够更专注于业务逻辑的开发。
相关文章
支持多平台小程序的uni-app持续集成工具 - 掘金 (juejin.cn)
收起阅读 »uni.openLocation和VUE3有兼容问题
VUE3 使用 uni.openLocation 浏览器不会打开新页面。会在原页面调用地图组件遮盖,这样使用浏览器自带返回按钮导致路由变化,页面始终被地图组件遮盖。(地图组件无法被浏览器返回事件关闭)
VUE3 使用 uni.openLocation 浏览器不会打开新页面。会在原页面调用地图组件遮盖,这样使用浏览器自带返回按钮导致路由变化,页面始终被地图组件遮盖。(地图组件无法被浏览器返回事件关闭)
【解决】您的应用在运行时,未见向用户告知权限申请的目的,向用户索取(相机)等权限,不符合华为应用市场审核标准。
简约解决方式:
uni.showModal({
title: "温馨提示",
content: "将申请相机权限,用于扫码功能",
success: (res) => {
if (res.confirm) {
// 唤起相册、相机、扫码、地理位置之类的api,如:uni.scanCode
}
}
})
更好的解决办法(实现如下图功能):可能有bug,仅供参考安卓权限说明蒙层
↓↓↓ 各位大佬点点赞





