HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

调试模式提示Uncaught SyntaxError: Unexpected token ?解决经验分享

nvue

全局搜索??运算符,使用||替代??运算符,或更改为其他方式进行运算

全局搜索??运算符,使用||替代??运算符,或更改为其他方式进行运算

uniapp 蓝牙无法监听多个特征的问题解决办法

uniapp 教程

问题描述:

在开发app时,项目需要蓝牙同时订阅某个服务下的多个特征值的变化时,直接使用uni.notifyBLECharacteristicValueChange只能订阅一个特征值的变化。

具体实现:

1.将 this.onBLECharacteristicValueChange();放到onLoad()中注册,确保该方法只注册一次,如果该方法注册了多次,蓝牙发送的数据会重复上传。

  1. 在蓝牙的订阅函数notifyBLECharacteristicValueChange()中设置定时器来轮流订阅蓝牙的多个特征值,具体如下:
  2. 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()中注册,确保该方法只注册一次,如果该方法注册了多次,蓝牙发送的数据会重复上传。

  1. 在蓝牙的订阅函数notifyBLECharacteristicValueChange()中设置定时器来轮流订阅蓝牙的多个特征值,具体如下:
  2. 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不起作用的原因之一

uniapp

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 表格组件 ,赶快进来看看效果吧!

ui组件 组件

Table 表格

用于展示多条结构类似的数据, 可对数据进行排序等操作。

地址

Github
文档网站
插件市场
QQ群

先看交互效果

基础用法

通过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

地址

Github
文档网站
插件市场
QQ群

继续阅读 »

Table 表格

用于展示多条结构类似的数据, 可对数据进行排序等操作。

地址

Github
文档网站
插件市场
QQ群

先看交互效果

基础用法

通过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

地址

Github
文档网站
插件市场
QQ群

收起阅读 »

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自定义基座,离线打包的生成方法

App离线打包 自定义基座
// what's uapp  
const uapp = 'universal app'

uapp 是一款跨平台APP开发工具箱,所有积累都来自多年产品开发中的不断实践。开发者仅需写一套代码,就能横扫所有平台。

制作了一个七分钟视频,包含了android,ios 手机平台上,离线打包,自定义基座的制作方法,视频首秀,喜欢的小伙伴帮忙点赞支持

https://www.ixigua.com/7294197408232276495

继续阅读 »
// what's uapp  
const uapp = 'universal app'

uapp 是一款跨平台APP开发工具箱,所有积累都来自多年产品开发中的不断实践。开发者仅需写一套代码,就能横扫所有平台。

制作了一个七分钟视频,包含了android,ios 手机平台上,离线打包,自定义基座的制作方法,视频首秀,喜欢的小伙伴帮忙点赞支持

https://www.ixigua.com/7294197408232276495

收起阅读 »

uni-app 开发中,监听 input 键盘事件获取不到按下按键值怎么办?

uniapp 教程

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工具?

  1. 多个小程序平台CI工具的配置文件、配置项等有所差异,其能力也有所不同,
  2. uni-app存在构建的步骤,使用各自小程序平台的CI工具则需要在构建产物中各自创建配置文件。
  3. 钉钉小程序的DingTalk Design CLI,不支持指定版本号,而uni-mini-ci支持(至于为什么支持,可以去看一下uni-mini-ci的文档)。

所以使用uni-mini-ci可以让多平台的小程序持续集成统一化。

在哪些场景使用?

  1. Github Actions 小程序持续集成
  2. Jenkins 小程序持续集成
  3. GitLab CI/CD 小程序持续集成
  4. 本地上传多个小程序平台

借助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)

继续阅读 »

背景

在没有CI工具帮助的时候,我们使用uni-app开发小程序通常会在小程序发版时先进行 build 构建,然后在小程序开发者工具中打开构建产物,然后进行上传代码的操作。这种人力运维的稳定性是不可控的,而且当我们一次要发布多个平台或者多个小程序时,这种人力运维的工作方式将浪费我们大量的时间。

为了解决这个问题,微信推出了miniprogram-ci,开发者可不打开小程序开发者工具,独立使用 miniprogram-ci 进行小程序代码的上传、预览等操作,随后支付宝、钉钉等其他小程序厂商也跟着推出了各自的CI工具,这一举措将开发者从繁琐的上传流程中解放了出来。

uni-mini-ci是一款支持微信、钉钉、支付宝、企业微信等小程序平台的持续集成工具。它集成了多个平台的ci工具一次配置发布到多端,让开发者可以轻松地将应用程序发布到多个小程序平台上。

为什么不直接使用各自平台的CI工具?

  1. 多个小程序平台CI工具的配置文件、配置项等有所差异,其能力也有所不同,
  2. uni-app存在构建的步骤,使用各自小程序平台的CI工具则需要在构建产物中各自创建配置文件。
  3. 钉钉小程序的DingTalk Design CLI,不支持指定版本号,而uni-mini-ci支持(至于为什么支持,可以去看一下uni-mini-ci的文档)。

所以使用uni-mini-ci可以让多平台的小程序持续集成统一化。

在哪些场景使用?

  1. Github Actions 小程序持续集成
  2. Jenkins 小程序持续集成
  3. GitLab CI/CD 小程序持续集成
  4. 本地上传多个小程序平台

借助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

VUE3 使用 uni.openLocation 浏览器不会打开新页面。会在原页面调用地图组件遮盖,这样使用浏览器自带返回按钮导致路由变化,页面始终被地图组件遮盖。(地图组件无法被浏览器返回事件关闭)

VUE3 使用 uni.openLocation 浏览器不会打开新页面。会在原页面调用地图组件遮盖,这样使用浏览器自带返回按钮导致路由变化,页面始终被地图组件遮盖。(地图组件无法被浏览器返回事件关闭)

【解决】您的应用在运行时,未见向用户告知权限申请的目的,向用户索取(相机)等权限,不符合华为应用市场审核标准。

应用上架

简约解决方式:

安卓app:不唤起权限,检测当前权限是否允许了

uni.showModal({  
    title: "温馨提示",  
    content: "将申请相机权限,用于扫码功能",  
    success: (res) => {  
        if (res.confirm) {  
            // 唤起相册、相机、扫码、地理位置之类的api,如:uni.scanCode  
        }  
    }  
})

更好的解决办法(实现如下图功能):可能有bug,仅供参考安卓权限说明蒙层

↓↓↓ 各位大佬点点赞

继续阅读 »

简约解决方式:

安卓app:不唤起权限,检测当前权限是否允许了

uni.showModal({  
    title: "温馨提示",  
    content: "将申请相机权限,用于扫码功能",  
    success: (res) => {  
        if (res.confirm) {  
            // 唤起相册、相机、扫码、地理位置之类的api,如:uni.scanCode  
        }  
    }  
})

更好的解决办法(实现如下图功能):可能有bug,仅供参考安卓权限说明蒙层

↓↓↓ 各位大佬点点赞

收起阅读 »