
最大资源网列表及剧集数据采集
【
zuidazy_list(page){
var pattern = new RegExp('-' + "(.?)" + '.html' +"."+ '_blank">' + "(.?)" + '</a' +"."+ '<span class="xing_vb5">' + "(.?)" + '</span>' +"."+ '<span class="xing_vb(6|7)">' + "(.?)" + '</span>',"g");
let data=[];
uni.request({
method:"GET",
url:'http://zuidazy.net/?m='+page+'.html',
success: (result) => {
if(result.statusCode==200){
// var data = result.data.match(/\/>(.)\$(.*)<\/li>/g)
while (pattern.exec(result.data) != null){
data.push({"id":RegExp.$1,
"title":RegExp.$2,
"type":RegExp.$3,
"times":RegExp.$5});
this.zuidazy_info(RegExp.$1)
// result.push(RegExp.$1);
}
}
},
fail: (e) => {
console.log(JSON.stringify(e))
}
});
console.log(data)
},
zuidazy_info(id){
let d1=[];
let d2=[];
let d3=[];
uni.request({
method:"GET",
url:'http://zuidazy.net/?m='+id+'.html',
success: (result) => {
if(result.statusCode==200){
var data = result.data.match(/\/>(.*)\$(.*)<\/li>/g)
for(var i=0;i<data.length;i++){
if(data[i].indexOf('.m3u8')>-1){
d1.push(this.getdata(data[i]))
}else if(data[i].indexOf('.mp4')>-1){
d2.push(this.getdata(data[i]))
}else{
d3.push(this.getdata(data[i]))
}
}
}
},
fail: (e) => {
console.log(JSON.stringify(e))
}
});
console.log([d1,d2,d3])
},
】
【
zuidazy_list(page){
var pattern = new RegExp('-' + "(.?)" + '.html' +"."+ '_blank">' + "(.?)" + '</a' +"."+ '<span class="xing_vb5">' + "(.?)" + '</span>' +"."+ '<span class="xing_vb(6|7)">' + "(.?)" + '</span>',"g");
let data=[];
uni.request({
method:"GET",
url:'http://zuidazy.net/?m='+page+'.html',
success: (result) => {
if(result.statusCode==200){
// var data = result.data.match(/\/>(.)\$(.*)<\/li>/g)
while (pattern.exec(result.data) != null){
data.push({"id":RegExp.$1,
"title":RegExp.$2,
"type":RegExp.$3,
"times":RegExp.$5});
this.zuidazy_info(RegExp.$1)
// result.push(RegExp.$1);
}
}
},
fail: (e) => {
console.log(JSON.stringify(e))
}
});
console.log(data)
},
zuidazy_info(id){
let d1=[];
let d2=[];
let d3=[];
uni.request({
method:"GET",
url:'http://zuidazy.net/?m='+id+'.html',
success: (result) => {
if(result.statusCode==200){
var data = result.data.match(/\/>(.*)\$(.*)<\/li>/g)
for(var i=0;i<data.length;i++){
if(data[i].indexOf('.m3u8')>-1){
d1.push(this.getdata(data[i]))
}else if(data[i].indexOf('.mp4')>-1){
d2.push(this.getdata(data[i]))
}else{
d3.push(this.getdata(data[i]))
}
}
}
},
fail: (e) => {
console.log(JSON.stringify(e))
}
});
console.log([d1,d2,d3])
},
】
收起阅读 »
蓝牙小票打印机 代码片段分享
var main = plus.android.runtimeMainActivity()
var BluetoothAdapter = plus.android.importClass(
'android.bluetooth.BluetoothAdapter'
)
var IntentFilter = plus.android.importClass('android.content.IntentFilter')
var BluetoothDevice = plus.android.importClass(
'android.bluetooth.BluetoothDevice'
)
var BAdapter = new BluetoothAdapter.getDefaultAdapter()
BAdapter.isEnabled()
BAdapter.enable()
plus.bluetooth.openBluetoothAdapter({
success: function(e) {
console.log('open success: ' + JSON.stringify(e))
plus.bluetooth.startBluetoothDevicesDiscovery({
success: function(e) {
console.log('start discovery success: ' + JSON.stringify(e))
},
fail: function(e) {
console.log('start discovery failed: ' + JSON.stringify(e))
}
})
},
fail: function(e) {
console.log('open failed: ' + JSON.stringify(e))
}
})
plus.bluetooth.getBluetoothDevices({
success: function(e) {
var devices = e.devices
console.log('get devices success: ' + e.length)
for (var i in devices) {
console.log(i + ': ' + JSON.stringify(devices[i]))
}
},
fail: function(e) {
console.log('get devices failed: ' + JSON.stringify(e))
}
})
plus.bluetooth.stopBluetoothDevicesDiscovery({
success: function(e) {
console.log('stop discovery success: ' + JSON.stringify(e))
plus.bluetooth.closeBluetoothAdapter({
success: function(e) {
console.log('close success: ' + JSON.stringify(e))
},
fail: function(e) {
console.log('close failed: ' + JSON.stringify(e))
}
})
},
fail: function(e) {
console.log('stop discovery failed: ' + JSON.stringify(e))
}
})
UUID = plus.android.importClass('java.util.UUID')
uuid = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB') //不需要更改
device = BAdapter.getRemoteDevice('DC:1D:30:34:62:A6') //这里是蓝牙打印机的蓝牙地址
plus.android.importClass(device)
bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid)
plus.android.importClass(bluetoothSocket)
bluetoothSocket.connect() //连接
bluetoothSocket.isConnected()
var outputStream = bluetoothSocket.getOutputStream()
plus.android.importClass(outputStream)
// 打印字符串
var s = plus.android.importClass('java.lang.String')
var string = new s('测试数据' + new Date().getMilliseconds() + '\n\n\n\n') //必须以创建字符串对象的形式创建对象,否则返回NULL
var bytes = string.getBytes('gbk')
outputStream.write(bytes)
outputStream.flush()
let Util = {
RESET: [27, 64], // 复位
ALIGN_LEFT: [27, 97, 0], // 左对齐
ALIGN_CENTER: [27, 97, 1], // 居中
ALIGN_RIGHT: [27, 97, 2], // 右对齐
BOLD: [27, 69, 1], // 加粗
BOLD_CANCEL: [27, 69, 0], //取消加粗
DOUBLE_HEIGHT_WIDTH: [29, 33, 17], //宽高加倍
DOUBLE_WIDTH: [29, 33, 16], //宽加倍
DOUBLE_HEIGHT: [29, 33, 1], //高加倍
NORMAL: [29, 33, 0], //字体不放大
LINE_SPACING_DEFAULT: [27, 50] // 设置默认间距
}
function setFormat(command) {
outputStream.write(command)
outputStream.flush()
}
function PrintText(text) {
var s = plus.android.importClass('java.lang.String')
var string = new s(text)
var bytes = string.getBytes('gbk')
outputStream.write(bytes)
outputStream.flush()
}
// 获取字符长度
function GetLength(str) {
return str.replace(/[\u0391-\uFFE5]/g, 'aa').length //先把中文替换成两个字节的英文,在计算长度
}
// 输出两列,不能超过32字符
function printTowData(text1, text2) {
let textLength1 = GetLength(text1)
let textLength2 = GetLength(text2)
let blankLength = 32 - textLength1 - textLength2
let blank = ''
for (let i = 0; i < blankLength; i++) {
blank = blank + ' '
}
return text1 + blank + text2
}
PrintText(printTowData('哈哈哈', '嘿嘿嘿'))
var main = plus.android.runtimeMainActivity()
var BluetoothAdapter = plus.android.importClass(
'android.bluetooth.BluetoothAdapter'
)
var IntentFilter = plus.android.importClass('android.content.IntentFilter')
var BluetoothDevice = plus.android.importClass(
'android.bluetooth.BluetoothDevice'
)
var BAdapter = new BluetoothAdapter.getDefaultAdapter()
BAdapter.isEnabled()
BAdapter.enable()
plus.bluetooth.openBluetoothAdapter({
success: function(e) {
console.log('open success: ' + JSON.stringify(e))
plus.bluetooth.startBluetoothDevicesDiscovery({
success: function(e) {
console.log('start discovery success: ' + JSON.stringify(e))
},
fail: function(e) {
console.log('start discovery failed: ' + JSON.stringify(e))
}
})
},
fail: function(e) {
console.log('open failed: ' + JSON.stringify(e))
}
})
plus.bluetooth.getBluetoothDevices({
success: function(e) {
var devices = e.devices
console.log('get devices success: ' + e.length)
for (var i in devices) {
console.log(i + ': ' + JSON.stringify(devices[i]))
}
},
fail: function(e) {
console.log('get devices failed: ' + JSON.stringify(e))
}
})
plus.bluetooth.stopBluetoothDevicesDiscovery({
success: function(e) {
console.log('stop discovery success: ' + JSON.stringify(e))
plus.bluetooth.closeBluetoothAdapter({
success: function(e) {
console.log('close success: ' + JSON.stringify(e))
},
fail: function(e) {
console.log('close failed: ' + JSON.stringify(e))
}
})
},
fail: function(e) {
console.log('stop discovery failed: ' + JSON.stringify(e))
}
})
UUID = plus.android.importClass('java.util.UUID')
uuid = UUID.fromString('00001101-0000-1000-8000-00805F9B34FB') //不需要更改
device = BAdapter.getRemoteDevice('DC:1D:30:34:62:A6') //这里是蓝牙打印机的蓝牙地址
plus.android.importClass(device)
bluetoothSocket = device.createInsecureRfcommSocketToServiceRecord(uuid)
plus.android.importClass(bluetoothSocket)
bluetoothSocket.connect() //连接
bluetoothSocket.isConnected()
var outputStream = bluetoothSocket.getOutputStream()
plus.android.importClass(outputStream)
// 打印字符串
var s = plus.android.importClass('java.lang.String')
var string = new s('测试数据' + new Date().getMilliseconds() + '\n\n\n\n') //必须以创建字符串对象的形式创建对象,否则返回NULL
var bytes = string.getBytes('gbk')
outputStream.write(bytes)
outputStream.flush()
let Util = {
RESET: [27, 64], // 复位
ALIGN_LEFT: [27, 97, 0], // 左对齐
ALIGN_CENTER: [27, 97, 1], // 居中
ALIGN_RIGHT: [27, 97, 2], // 右对齐
BOLD: [27, 69, 1], // 加粗
BOLD_CANCEL: [27, 69, 0], //取消加粗
DOUBLE_HEIGHT_WIDTH: [29, 33, 17], //宽高加倍
DOUBLE_WIDTH: [29, 33, 16], //宽加倍
DOUBLE_HEIGHT: [29, 33, 1], //高加倍
NORMAL: [29, 33, 0], //字体不放大
LINE_SPACING_DEFAULT: [27, 50] // 设置默认间距
}
function setFormat(command) {
outputStream.write(command)
outputStream.flush()
}
function PrintText(text) {
var s = plus.android.importClass('java.lang.String')
var string = new s(text)
var bytes = string.getBytes('gbk')
outputStream.write(bytes)
outputStream.flush()
}
// 获取字符长度
function GetLength(str) {
return str.replace(/[\u0391-\uFFE5]/g, 'aa').length //先把中文替换成两个字节的英文,在计算长度
}
// 输出两列,不能超过32字符
function printTowData(text1, text2) {
let textLength1 = GetLength(text1)
let textLength2 = GetLength(text2)
let blankLength = 32 - textLength1 - textLength2
let blank = ''
for (let i = 0; i < blankLength; i++) {
blank = blank + ' '
}
return text1 + blank + text2
}
PrintText(printTowData('哈哈哈', '嘿嘿嘿'))
收起阅读 »

uni-app各环节版本兼容性说明
此文档停止维护,最新的文档见:uni版本说明
uni-app是一个跨度很大的产品,支持多种开发ide、HBuilderX有正式版和alpha版、支持cli和普通项目、支持很多端、有云打包app和本地打包app、编译器模式支持老的模板模式和新的自定义组件模式。
它们都会升级,如果出现一个升级、另一个未升级的情况,可能会运行异常。
正常情况,HBuilderX升级后,其自带的app运行基座、uni-app编译器、云打包配套引擎会同步升级。但在开发者使用cli创建项目、使用自定义基座、使用5+sdk离线打包时,就需要手动维护版本更新。
我们经常遇到的误报问题是:“我的HBuilderX已经是最新版了,升级日志里提到的已解决问题,为什么没有仍然存在?”,其实有的是因为cli项目下编译器是旧的、有的是因为本地打包的sdk是旧的,有的是因为自定义基座的版本是旧的。
-
如果你使用cli创建项目(即项目根目录是package.json),不管用什么ide,即便是用HBuilderX,切记cli项目的编译器是在项目下的,HBuilderX不管怎么升级都不会影响编译器版本。你需要手动npm update来升级编译器。以及如果你想要安装less、scss等预编译器,也需要自己npm安装在项目下,而不是在HBuilderX的插件管理里安装。
-
如果你使用离线打包,请注意HBuilderX升级后,真机运行基座和云打包对应引擎跟随HBuilderX升级,而你的sdk需要手动升级。sdk的版本升级一般滞后HBuilderX正式版升级一两天。在这里下载最新版
-
如果你使用自定义基座,之前制作的自定义基座是不会跟随HBuilderX升级的,升级HBuilderX后你应该重新制作新版自定义基座。
-
如果你使用wgt升级,新版HBuilderX编译的wgt,运行到之前的runtime上,一定要先测试好,看有没有兼容性问题。如果有问题,就不要wgt升级,整包升级。
-
考虑到向下兼容,uni-app编译器在升级为新的自定义组件模式后,同时保留了对老编译模式的向下兼容。
在HBuilderX alpha版中,App端一定会使用新编译器,不理会manifest配置。
在HBuilderX 正式版中,新创建的项目会使用新编译器,老项目不会强制使用,而是开发者自己在manifest里配置开启。 -
如果你使用其他ide开发uni-app,会经常因为拼错单词而运行失败,因为经过webpack编译一道,很多错误反应的不够直观,排错时间很长,不如从开始就依赖有良好提示的HBuilderX,避免敲错单词。
-
云打包的引擎版本说明
自HBuilderX 3.9起,云打包机版本保留了多个可用的版本,具体可用的云端打包版本参考下方的版本对应表。
打包时,服务端会根据用户使用的HBuilderX或cli版本去匹配最合适的打包机,规则如下(匹配优先级从高到低):- HBuilderX或cli版本与云端版本完全一致;
- HBuilderX或cli版本的大版本号与云端一致时,使用该大版本的最新版本;如打包机上有[3.92, 3.93, 3.99, 4.01, 4.10],则3.91使用3.99打包机。
- 当以上规则无法匹配时,使用云端最新版本。
举个例子:
当云打包机有以下版本[3.7.11,3.8.12,3.92, 3.93, 3.99, 4.01, 4.15]可用时:
- 如果HBuilderX使用3.7.11,3.8.12,3.92, 3.93, 3.99, 4.08, 4.15这些版本时,版本号完全匹配,直接使用同版本号打包机;
- 如果HBuilderX使用3.7.3,则云端打包机使用3.7.11版本;HBuilderX使用3.91,则云端打包机使用3.99版本;HBuilderX使用4.01版本,则云端打包机使用4.08版本;
- 如果HBuilderX使用3.7以下的版本,则云端打包机使用最新版本4.15。
很多人在报问题时不说清自己的情况,导致别人给出错误的回答。
很多人在搜问题时没注意看条件,导致使用了并不适用于自己的错误方案。
要想问题少,推荐使用HBuilderX完成一切工作,包括创建项目、运行编译、云打包app。
因为在这套体系里,官方会对很多常见的问题做出提醒和引导,减少问题的概率。随着HBuilderX的升级,uni-app编译器、真机运行基座、云打包引擎都会升级。
版本列表
下面提供 uni-app
开发中各产品的版本对应表:
<iframe src="https://dev.dcloud.net.cn/product/versions/view" width="100%" height="760px" frameborder="0" scrolling="no" style="border: 1px solid #eee; border-radius: 4px;"> </iframe>
此文档停止维护,最新的文档见:uni版本说明
uni-app是一个跨度很大的产品,支持多种开发ide、HBuilderX有正式版和alpha版、支持cli和普通项目、支持很多端、有云打包app和本地打包app、编译器模式支持老的模板模式和新的自定义组件模式。
它们都会升级,如果出现一个升级、另一个未升级的情况,可能会运行异常。
正常情况,HBuilderX升级后,其自带的app运行基座、uni-app编译器、云打包配套引擎会同步升级。但在开发者使用cli创建项目、使用自定义基座、使用5+sdk离线打包时,就需要手动维护版本更新。
我们经常遇到的误报问题是:“我的HBuilderX已经是最新版了,升级日志里提到的已解决问题,为什么没有仍然存在?”,其实有的是因为cli项目下编译器是旧的、有的是因为本地打包的sdk是旧的,有的是因为自定义基座的版本是旧的。
-
如果你使用cli创建项目(即项目根目录是package.json),不管用什么ide,即便是用HBuilderX,切记cli项目的编译器是在项目下的,HBuilderX不管怎么升级都不会影响编译器版本。你需要手动npm update来升级编译器。以及如果你想要安装less、scss等预编译器,也需要自己npm安装在项目下,而不是在HBuilderX的插件管理里安装。
-
如果你使用离线打包,请注意HBuilderX升级后,真机运行基座和云打包对应引擎跟随HBuilderX升级,而你的sdk需要手动升级。sdk的版本升级一般滞后HBuilderX正式版升级一两天。在这里下载最新版
-
如果你使用自定义基座,之前制作的自定义基座是不会跟随HBuilderX升级的,升级HBuilderX后你应该重新制作新版自定义基座。
-
如果你使用wgt升级,新版HBuilderX编译的wgt,运行到之前的runtime上,一定要先测试好,看有没有兼容性问题。如果有问题,就不要wgt升级,整包升级。
-
考虑到向下兼容,uni-app编译器在升级为新的自定义组件模式后,同时保留了对老编译模式的向下兼容。
在HBuilderX alpha版中,App端一定会使用新编译器,不理会manifest配置。
在HBuilderX 正式版中,新创建的项目会使用新编译器,老项目不会强制使用,而是开发者自己在manifest里配置开启。 -
如果你使用其他ide开发uni-app,会经常因为拼错单词而运行失败,因为经过webpack编译一道,很多错误反应的不够直观,排错时间很长,不如从开始就依赖有良好提示的HBuilderX,避免敲错单词。
-
云打包的引擎版本说明
自HBuilderX 3.9起,云打包机版本保留了多个可用的版本,具体可用的云端打包版本参考下方的版本对应表。
打包时,服务端会根据用户使用的HBuilderX或cli版本去匹配最合适的打包机,规则如下(匹配优先级从高到低):- HBuilderX或cli版本与云端版本完全一致;
- HBuilderX或cli版本的大版本号与云端一致时,使用该大版本的最新版本;如打包机上有[3.92, 3.93, 3.99, 4.01, 4.10],则3.91使用3.99打包机。
- 当以上规则无法匹配时,使用云端最新版本。
举个例子:
当云打包机有以下版本[3.7.11,3.8.12,3.92, 3.93, 3.99, 4.01, 4.15]可用时:
- 如果HBuilderX使用3.7.11,3.8.12,3.92, 3.93, 3.99, 4.08, 4.15这些版本时,版本号完全匹配,直接使用同版本号打包机;
- 如果HBuilderX使用3.7.3,则云端打包机使用3.7.11版本;HBuilderX使用3.91,则云端打包机使用3.99版本;HBuilderX使用4.01版本,则云端打包机使用4.08版本;
- 如果HBuilderX使用3.7以下的版本,则云端打包机使用最新版本4.15。
很多人在报问题时不说清自己的情况,导致别人给出错误的回答。
很多人在搜问题时没注意看条件,导致使用了并不适用于自己的错误方案。
要想问题少,推荐使用HBuilderX完成一切工作,包括创建项目、运行编译、云打包app。
因为在这套体系里,官方会对很多常见的问题做出提醒和引导,减少问题的概率。随着HBuilderX的升级,uni-app编译器、真机运行基座、云打包引擎都会升级。
版本列表
下面提供 uni-app
开发中各产品的版本对应表:
<iframe src="https://dev.dcloud.net.cn/product/versions/view" width="100%" height="760px" frameborder="0" scrolling="no" style="border: 1px solid #eee; border-radius: 4px;"> </iframe>

本地uni-app原生插件提交云端打包
从HBuilderX1.9.0开始uni-app原生插件可以直接在插件市场绑定应用后,直接云端打包生效,避免下载uni-app原生插件到本地后再提交云端打包。
同时也继续支持将插件下载到本地后再提交云端打包,通常在以下情况使用这种方式:
- uni-app原生插件开发者,开发后配置提交云端打包验证插件的包格式是否正确
- uni-app原生插件使用者,需要对插件自定义修改(如插件的资源等)
获取本地uni-app原生插件
插件市场下载免费uni-app原生插件
可以登录uni原生插件市场,在免费的插件详情页中点击“下载for离线打包”下载原生插件(zip格式),解压到HBuilderX的uni-app项目下的“nativeplugins”目录(如不存在则创建),以下是“DCloud-RichAlert”插件举例,它的下载地址是:https://ext.dcloud.net.cn/plugin?id=36
下载解压后目录结构如下:

开发者自己开发uni-app原生插件
原生插件开发完成后按指定格式压缩为zip包,参考uni-app原生插件格式说明文档。
按上图的格式配置到uni-app项目下的“nativeplugins”目录。
uni-app原生插件本地配置
将原生插件配置到uni-app项目的“nativeplugins”下,还需要在manifest.json文件的“App原生插件配置”项下点击“选择本地插件”,在列表中选择需要打包生效的插件:
保存后,重新提交云端打包生效
关于云端打包资源大小超限的说明
云端打包资源大小限制40M,如果超限每次打包需支付一定费用。40M—100M,每次打包10元,每增加100M费用增加10元。请登录开发者中心(https://dev.dcloud.net.cn),选择“打包服务”- “App大小超限充值”进行自助充值后,再提交打包。
如果是为了开发uni-app原生插件提交插件市场前的测试打包,大小超限也需付费才能云端打包,插件上线通过审核以后可以申请退还相关打包费用。
提供开发者账号及插件链接地址发邮件到 bd@dcloud.io 申请,谢谢!
使用 Windows 系统提交 iOS 本地插件打包报错的解决方法
如果使用 mac 提交本地插件打包正常,而使用 Windows 系统提交 iOS 本地插件打包时报错,是因为插件包使用的某个 .framework 库中存现软链接,在 Windows 系统上解压后导致软链接失效,打包时导致这个库缺失,所以会报错
报错如下图:
解决办法:
1.依次查看插件iOS目录下面的 .framework 库文件,下面以 UTDID.framework
为例,打开后发现 UTDID、Resources、Headers 大小都为 1kb,说明这几个文件既是软链(在Windows上加压后被转换为文本文件,无法链接到真实文件)
2.用记事本打开这几个文件,里面写的就是链接的真实文件目录,(链接的是
/Versions/A/
目录中的文件)3.把真实文件拷贝到这个库的根目录,将其他文件全部删除即可,最终的库文件
然后重新提交打包即可;
使用uni-app原生插件参考:uni-app原生插件(native plugin)使用说明
更多uni-app原生插件文档参考:uni-app原生插件开发指南
从HBuilderX1.9.0开始uni-app原生插件可以直接在插件市场绑定应用后,直接云端打包生效,避免下载uni-app原生插件到本地后再提交云端打包。
同时也继续支持将插件下载到本地后再提交云端打包,通常在以下情况使用这种方式:
- uni-app原生插件开发者,开发后配置提交云端打包验证插件的包格式是否正确
- uni-app原生插件使用者,需要对插件自定义修改(如插件的资源等)
获取本地uni-app原生插件
插件市场下载免费uni-app原生插件
可以登录uni原生插件市场,在免费的插件详情页中点击“下载for离线打包”下载原生插件(zip格式),解压到HBuilderX的uni-app项目下的“nativeplugins”目录(如不存在则创建),以下是“DCloud-RichAlert”插件举例,它的下载地址是:https://ext.dcloud.net.cn/plugin?id=36
下载解压后目录结构如下:
开发者自己开发uni-app原生插件
原生插件开发完成后按指定格式压缩为zip包,参考uni-app原生插件格式说明文档。
按上图的格式配置到uni-app项目下的“nativeplugins”目录。
uni-app原生插件本地配置
将原生插件配置到uni-app项目的“nativeplugins”下,还需要在manifest.json文件的“App原生插件配置”项下点击“选择本地插件”,在列表中选择需要打包生效的插件:
保存后,重新提交云端打包生效
关于云端打包资源大小超限的说明
云端打包资源大小限制40M,如果超限每次打包需支付一定费用。40M—100M,每次打包10元,每增加100M费用增加10元。请登录开发者中心(https://dev.dcloud.net.cn),选择“打包服务”- “App大小超限充值”进行自助充值后,再提交打包。
如果是为了开发uni-app原生插件提交插件市场前的测试打包,大小超限也需付费才能云端打包,插件上线通过审核以后可以申请退还相关打包费用。
提供开发者账号及插件链接地址发邮件到 bd@dcloud.io 申请,谢谢!
使用 Windows 系统提交 iOS 本地插件打包报错的解决方法
如果使用 mac 提交本地插件打包正常,而使用 Windows 系统提交 iOS 本地插件打包时报错,是因为插件包使用的某个 .framework 库中存现软链接,在 Windows 系统上解压后导致软链接失效,打包时导致这个库缺失,所以会报错
报错如下图:
解决办法:
1.依次查看插件iOS目录下面的 .framework 库文件,下面以 UTDID.framework
为例,打开后发现 UTDID、Resources、Headers 大小都为 1kb,说明这几个文件既是软链(在Windows上加压后被转换为文本文件,无法链接到真实文件)
2.用记事本打开这几个文件,里面写的就是链接的真实文件目录,(链接的是
/Versions/A/
目录中的文件)3.把真实文件拷贝到这个库的根目录,将其他文件全部删除即可,最终的库文件
然后重新提交打包即可;
使用uni-app原生插件参考:uni-app原生插件(native plugin)使用说明
更多uni-app原生插件文档参考:uni-app原生插件开发指南

uni-app模板编译模式和自定义组件编译模式差异说明
更新:这2种编译模式均已下线。本文已过期
uni-app 自 1.8版本开始,同时支持两种编译模式:
template模板模式
:老框架模式,借鉴自mpvue
,将用户编写的Vue
组件,编译为WXML
中的模板(template),变相实现组件化开发,性能差,支持 Vue 语法少,比如不支持过滤器。自定义组件模式
:新框架模式,DCloud自研,将用户编写的Vue
组件,编译为微信小程序自定义组件,实现了更高的性能及更完善的 Vue 语法支持。同时在App端提供了独立的js引擎,大幅提升性能。
如何切换编译模式
在 manifest.json
的源码视图里配置是否启用新编译器, manifest.json
-> %platform%
-> usingComponents
切换编译模式。
%platform%
是指平台名称
// manifest.json
{
// ...
/* App平台特有配置 */
"app-plus": {
"usingComponents":true //是否启用`自定义组件模式`,为true表示新的`自定义组件模式` ,否则为`template模板模式`
}
/* 微信小程序特有配置 */
"mp-weixin": {
"usingComponents":true //是否启用`自定义组件模式`,为true表示新的`自定义组件模式`,否则为`template模板模式`
}
// ...
}
在HBuilderX的manifest的可视化界面,每个平台的也都有配置勾选。
若manifest.json
配置文件中,未明确指定编译模式(即未配置%platform%
-> usingComponents
),则uni-app
默认策略如下:
HbuilderX 2.3.0
正式版之前的版本为保证历史项目兼容,默认使用老编译器(template模板模式
),即不配置的情况下效果等同于usingComponents: false
,而HbuilderX 2.3.0
正式版之后,默认会启用新编译器(自定义组件模式
),即不配置的情况下效果等同于usingComponents: true
,从HbuilderX 2.3.8
正式版开始将停止支持非自定义组件模式,届时,新版HBuilderX真机运行和云打包,都不再支持非自定义组件模式。HBuilderX的云打包,只向下保留2个版本。再升级1个版本后,老版打包机也不再支持非自定义组件模式。详见https://ask.dcloud.net.cn/article/36385- HBuilderX Alpha 为开发者提供最新技术尝鲜,默认会启用新编译器(
自定义组件模式
),即不配置的情况下效果等同于usingComponents: true
- CLI 开发模式下的,默认编译策略同 HBuidlerX 正式版策略
Tips:
- 切换编译环境之后,请重新运行项目
非自定义组件模式升级为自定义组件模式注意
自定义组件模式虽然兼容大部分老模式的写法,但也有部分不兼容。老项目升级时,可能需要修改部分代码,请详细参考自定义组件模式开发注意事项:https://ask.dcloud.net.cn/article/35851
不同编译模式支持的Vue语法差异
自定义组件模式
和模板模式
都不支持的 vue 语法:
- class不支持绑定Obejct变量(使用字符串的形式绑定)
- 不支持事件修饰符:prevent、passive(在App与小程序平台,使用stop修饰符,既可以阻止冒泡也能阻止默认行为)
- 不支持render、inline-template、X-Templates、keep-alive、transition
- 不支持使用 Vue.use 的方式注册全局组件(在main.js使用Vue.component的方式引入)
template模板模式
除了不支持如上Vue语法外,额外还不支持如下语法:
- 不支持过滤器
filter
- 不支持比较复杂的
JavaScript
渲染表达式 - 不支持在
template
内使用methods
中的函数 - 不支持
v-html
- 不支持
v-slot
新语法及后备内容 - 不支持解构插槽
Prop
设置默认值 - 组件不支持原生事件绑定,如:
@tap.native
- 组件不支持通过class绑定样式
问题反馈
- 如果在使用 uni-app 的过程中遇到问题,先认真阅读下 如何准确地反馈 uni-app 的问题 再发帖咨询。
- 在不要在本文章下留言报bug,请发布新帖,并将帖子地址复制到回复栏,并简短说明bug。
扩展阅读
更新:这2种编译模式均已下线。本文已过期
uni-app 自 1.8版本开始,同时支持两种编译模式:
template模板模式
:老框架模式,借鉴自mpvue
,将用户编写的Vue
组件,编译为WXML
中的模板(template),变相实现组件化开发,性能差,支持 Vue 语法少,比如不支持过滤器。自定义组件模式
:新框架模式,DCloud自研,将用户编写的Vue
组件,编译为微信小程序自定义组件,实现了更高的性能及更完善的 Vue 语法支持。同时在App端提供了独立的js引擎,大幅提升性能。
如何切换编译模式
在 manifest.json
的源码视图里配置是否启用新编译器, manifest.json
-> %platform%
-> usingComponents
切换编译模式。
%platform%
是指平台名称
// manifest.json
{
// ...
/* App平台特有配置 */
"app-plus": {
"usingComponents":true //是否启用`自定义组件模式`,为true表示新的`自定义组件模式` ,否则为`template模板模式`
}
/* 微信小程序特有配置 */
"mp-weixin": {
"usingComponents":true //是否启用`自定义组件模式`,为true表示新的`自定义组件模式`,否则为`template模板模式`
}
// ...
}
在HBuilderX的manifest的可视化界面,每个平台的也都有配置勾选。
若manifest.json
配置文件中,未明确指定编译模式(即未配置%platform%
-> usingComponents
),则uni-app
默认策略如下:
HbuilderX 2.3.0
正式版之前的版本为保证历史项目兼容,默认使用老编译器(template模板模式
),即不配置的情况下效果等同于usingComponents: false
,而HbuilderX 2.3.0
正式版之后,默认会启用新编译器(自定义组件模式
),即不配置的情况下效果等同于usingComponents: true
,从HbuilderX 2.3.8
正式版开始将停止支持非自定义组件模式,届时,新版HBuilderX真机运行和云打包,都不再支持非自定义组件模式。HBuilderX的云打包,只向下保留2个版本。再升级1个版本后,老版打包机也不再支持非自定义组件模式。详见https://ask.dcloud.net.cn/article/36385- HBuilderX Alpha 为开发者提供最新技术尝鲜,默认会启用新编译器(
自定义组件模式
),即不配置的情况下效果等同于usingComponents: true
- CLI 开发模式下的,默认编译策略同 HBuidlerX 正式版策略
Tips:
- 切换编译环境之后,请重新运行项目
非自定义组件模式升级为自定义组件模式注意
自定义组件模式虽然兼容大部分老模式的写法,但也有部分不兼容。老项目升级时,可能需要修改部分代码,请详细参考自定义组件模式开发注意事项:https://ask.dcloud.net.cn/article/35851
不同编译模式支持的Vue语法差异
自定义组件模式
和模板模式
都不支持的 vue 语法:
- class不支持绑定Obejct变量(使用字符串的形式绑定)
- 不支持事件修饰符:prevent、passive(在App与小程序平台,使用stop修饰符,既可以阻止冒泡也能阻止默认行为)
- 不支持render、inline-template、X-Templates、keep-alive、transition
- 不支持使用 Vue.use 的方式注册全局组件(在main.js使用Vue.component的方式引入)
template模板模式
除了不支持如上Vue语法外,额外还不支持如下语法:
- 不支持过滤器
filter
- 不支持比较复杂的
JavaScript
渲染表达式 - 不支持在
template
内使用methods
中的函数 - 不支持
v-html
- 不支持
v-slot
新语法及后备内容 - 不支持解构插槽
Prop
设置默认值 - 组件不支持原生事件绑定,如:
@tap.native
- 组件不支持通过class绑定样式
问题反馈
- 如果在使用 uni-app 的过程中遇到问题,先认真阅读下 如何准确地反馈 uni-app 的问题 再发帖咨询。
- 在不要在本文章下留言报bug,请发布新帖,并将帖子地址复制到回复栏,并简短说明bug。
扩展阅读
收起阅读 »
求助,uni-app按钮点击没反应
<template>
<!--orderinfo{{cartIds}}{{amount}}-->
<view class="parent">
<view class="view-address">
<view class="view-addressleft">
<image src="/images/white_loc.png"></image>
<view class="view-addressleft-info">
<text class="text-name">收货人:{{ address.consignee }}</text>
<text class="text-address">收货地址:{{ address.address }}</text>
</view>
</view>
<view class="view-addressright">
<text class="text-mobile">{{ address.mobile }}</text>
<image src="/images/white_hint.png"></image>
</view>
</view>
<view v-for="(item,index) in cartList" :key="index" class="container carts-list">
<view v-if="item.show == 1" class="view-title">
<image src="/images/icon_order.png"></image>
<text>供应商:{{ item.store_name }}</text>
</view>
<view v-if="item.show == 1" class="class-line"></view>
<view class="carts-item">
<view>
<!-- 缩略图 -->
<image class="carts-image" src="{{item.image}}" mode="aspectFill" />
</view>
<view class="carts-text">
<!-- 商品标题 -->
<text class="carts-title">{{ item.goods_name }}</text>
<text class="carts-key-name">{{ item.spec_key_name }}</text>
<view class="carts-subtitle">
<!-- 价格 -->
<!--<text class="carts-price">{{item.goods_price}}</text>
-->
<!-- 数量加减 -->
</view>
</view>
<view class="carts-right">
<text class="text-red">¥{{ item.member_goods_price }}</text>
<text class="text-price">x{{ item.goods_num }}</text>
</view>
</view>
</view>
<view class="class-line1"></view>
<view class="view-freemoney">
<!--
<text class="text-hint">优 惠 劵:</text>
<input bindchange="bindChange" placeholder-style="color:#999999" class="input-money" placeholder="输入余额"/>
-->
<radio color="red" class="radio-style" value="0" checked="{{check[0]}}" />
<picker value="{{index}}" range="{{coupon}}">
<view class="view-picker">{{ cv }}</view>
</picker>
<text class="text-hint">或者</text>
<radio color="red" class="radio-style" value="1" checked="{{check[1]}}" />
<input placeholder-style="color:#999999" class="input-money" placeholder="直接输入优惠劵" style="width:200rpx" />
</view>
<view class="view-freemoney">
<text class="text-hint">使用余额:</text>
<input placeholder-style="color:#999999" class="input-money" placeholder="输入余额" />
<text class="btn-use" >使用</text>
<text class="text-hint1">您的可用余额{{ freemoney }}</text>
</view>
<view class="view-freemoney">
<text class="text-hint">使用积分:</text>
<input placeholder-style="color:#999999" class="input-money" placeholder="输入积分" />
<text class="btn-use">使用</text>
<text class="text-hint1">您的可用积分{{ pay_points }}</text>
</view>
<view class="class-line1"></view>
<view class="view-price">
<view class="view-price1">
<text class="text-price1">商品总额:</text>
<text class="text-red">¥{{ totalPrice.total_fee }}元</text>
</view>
<view hidden class="view-price1">
<text class="text-price1">邮费:</text>
<text class="text-red">¥20元</text>
</view>
<view class="view-price1">
<text class="text-price1">应付总金额:</text>
<text class="text-red">¥{{ totalPrice.total_fee }}元</text>
</view>
</view>
<view class="text-save" @longtap="longtap">提交订单</view>
</view>
</template>
<script>
export default {
methods:{
clickTest: function(e){
console.log(e);
console.log('click');
},
longtap: function(e){
console.log(e);
console.log('longtap');
}
}
}
</script>
<style lang="scss">
/ pages/order/ordersubmit/index.wxss /
.view-address {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
height: 150rpx;
background: #3aa0b7;
}
.view-addressleft {
display: flex;
flex-direction: row;
padding: 20rpx 0 0 3;
justify-content: flex-start;
align-items: center;
width: 450rpx;
}
.view-addressleft image {
width: 50rpx;
height: 50rpx;
}
.view-addressleft-info {
display: flex;
flex-direction: column;
margin-left: 10rpx;
justify-content: space-around;
align-items: flex-start;
height: 85rpx;
}
.text-name {
font-size: 29rpx;
color: #ffffff;
}
.text-address {
font-size: 29rpx;
color: #ffffff;
}
.view-addressright {
height: 50rpx;
margin-right: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
}
.text-mobile {
font-size: 29rpx;
color: #ffffff;
}
.view-addressright image {
width: 30rpx;
height: 30rpx;
margin-left: 15rpx;
}
.view-title {
display: flex;
flex-direction: row;
height: 75rpx;
align-items: center;
}
.view-title image {
width: 50rpx;
height: 50rpx;
}
.view-title text {
font-size: 29rpx;
color: black;
}
.class-line {
width: 670rpx;
margin-left: 0rpx;
height: 2rpx;
background: #f4f4f4;
}
/外部容器/
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
box-sizing: border-box;
}
/整体列表/
.carts-list {
display: flex;
flex-direction: column;
padding: 0 40rpx;
}
/每行单元格/
.carts-item {
display: flex;
flex-direction: row;
height: 150rpx;
/width属性解决标题文字太短而缩略图偏移/
width: 100%;
border-bottom: 1px solid #eee;
padding: 30rpx 0;
justify-content: space-between;
}
/左部图片/
.carts-image {
width: 130rpx;
height: 150rpx;
border-width: 3rpx;
border-style: solid;
border-color: #ccc;
}
/右部描述/
.carts-text {
margin-left: 10rpx;
width: 370rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.carts-right {
height: 100%;
width: 110rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/右上部分标题/
.carts-title {
font-size: 27rpx;
color: #444;
line-height: 38rpx;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.carts-key-name {
margin-top: 10rpx;
font-size: 27rpx;
color: #999999;
}
/右下部分价格与数量/
.carts-subtitle {
font-size: 25rpx;
color: darkgray;
padding: 0 20rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
}
/价格/
.carts-price {
color: #f60;
font-size: 14px;
line-height: 22px;
margin-top: 12rpx;
}
/商品加减按钮/
/stepper容器/
.stepper {
border: 1px solid #ccc;
border-radius: 3px;
width: 80px;
height: 26px;
margin: 0 auto;
}
/加号与减号/
.stepper text {
width: 19px;
line-height: 26px;
text-align: center;
float: left;
}
/数值/
.stepper input {
color: black;
float: left;
margin: 0 auto;
width: 40px;
height: 26px;
text-align: center;
font-size: 12px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
/普通样式/
.stepper .normal {
color: black;
}
/禁用样式/
.stepper .disabled {
color: #ccc;
}
/复选框样式/
.carts-list icon {
margin-top: 60rpx;
margin-right: 20rpx;
}
/底部按钮/
.carts-footer {
width: 100%;
height: 80rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: 0;
border-top: 1px solid #eee;
background: white;
}
/复选框/
.carts-footer icon {
margin-left: 20rpx;
margin-top: 20rpx;
}
/全选字样/
.carts-footer text {
font-size: 30rpx;
margin-left: 8rpx;
line-height: 10rpx;
}
/立即结算按钮/
.carts-footer .button {
line-height: 80rpx;
text-align: center;
width: 220rpx;
height: 80rpx;
background-color: #f60;
color: white;
font-size: 36rpx;
border-radius: 0;
border: 0;
}
.carts-footer-left {
display: flex;
flex-direction: row;
align-items: center;
}
.carts-footer-left text {
margin-top: 20rpx;
}
.text-price {
font-size: 32rpx;
color: #999999;
}
.text-red {
font-size: 32rpx;
color: orangered;
}
.image-delete {
width: 50rpx;
height: 60rpx;
}
.class-line1 {
width: 750rpx;
height: 20rpx;
background: #f4f4f4;
}
.view-price {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
}
.view-price1 {
margin-top: 30rpx;
margin-right: 20rpx;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.text-price1 {
font-size: 29rpx;
color: grey;
}
.text-save {
width: 680rpx;
align-self: center;
font-size: 32rpx;
margin-top: 30rpx;
color: #ffffff;
text-align: center;
height: 70rpx;
border-radius: 9rpx;
background: orange;
line-height: 70rpx;
margin-bottom: 40rpx;
}
.parent {
display: flex;
flex-direction: column;
width: 100%;
}
.view-freemoney {
width: 100%;
height: 100rpx;
background: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #eee;
}
.text-hint {
font-size: 28rpx;
color: #000000;
margin-left: 40rpx;
}
.text-hint1 {
font-size: 28rpx;
color: #000000;
margin-left: 20rpx;
flex-shrink: 0;
flex-grow: 0;
}
.input-money {
font-size: 26rpx;
color: #999999;
margin-left: 6rpx;
border-style: solid;
border-width: 1rpx;
padding-left: 15rpx;
width: 140rpx;
}
.btn-use {
line-height: 55rpx;
background: red;
color: #ffffff;
text-align: center;
font-size: 28rpx;
padding-left: 10rpx;
margin-left: 5rpx;
padding-right: 10rpx;
}
.radio-style {
font-size: 28rpx;
color: red;
margin-left: 40rpx;
}
.text-redhint {
font-size: 28rpx;
color: red;
}
.text-select {
font-size: 28rpx;
color: #999999;
width: 200rpx;
height: 40rpx;
line-height: 28rpx;
}
.view-picker {
border-style: solid;
border-color: #999999;
padding-left: 20rpx;
display: flex;
flex: row;
height: 60rpx;
justify-content: flex-start;
align-items: center;
width: 200rpx;
border-width: 1rpx;
font-size: 28rpx;
color: #999999;
}
</style>
<template>
<!--orderinfo{{cartIds}}{{amount}}-->
<view class="parent">
<view class="view-address">
<view class="view-addressleft">
<image src="/images/white_loc.png"></image>
<view class="view-addressleft-info">
<text class="text-name">收货人:{{ address.consignee }}</text>
<text class="text-address">收货地址:{{ address.address }}</text>
</view>
</view>
<view class="view-addressright">
<text class="text-mobile">{{ address.mobile }}</text>
<image src="/images/white_hint.png"></image>
</view>
</view>
<view v-for="(item,index) in cartList" :key="index" class="container carts-list">
<view v-if="item.show == 1" class="view-title">
<image src="/images/icon_order.png"></image>
<text>供应商:{{ item.store_name }}</text>
</view>
<view v-if="item.show == 1" class="class-line"></view>
<view class="carts-item">
<view>
<!-- 缩略图 -->
<image class="carts-image" src="{{item.image}}" mode="aspectFill" />
</view>
<view class="carts-text">
<!-- 商品标题 -->
<text class="carts-title">{{ item.goods_name }}</text>
<text class="carts-key-name">{{ item.spec_key_name }}</text>
<view class="carts-subtitle">
<!-- 价格 -->
<!--<text class="carts-price">{{item.goods_price}}</text>
-->
<!-- 数量加减 -->
</view>
</view>
<view class="carts-right">
<text class="text-red">¥{{ item.member_goods_price }}</text>
<text class="text-price">x{{ item.goods_num }}</text>
</view>
</view>
</view>
<view class="class-line1"></view>
<view class="view-freemoney">
<!--
<text class="text-hint">优 惠 劵:</text>
<input bindchange="bindChange" placeholder-style="color:#999999" class="input-money" placeholder="输入余额"/>
-->
<radio color="red" class="radio-style" value="0" checked="{{check[0]}}" />
<picker value="{{index}}" range="{{coupon}}">
<view class="view-picker">{{ cv }}</view>
</picker>
<text class="text-hint">或者</text>
<radio color="red" class="radio-style" value="1" checked="{{check[1]}}" />
<input placeholder-style="color:#999999" class="input-money" placeholder="直接输入优惠劵" style="width:200rpx" />
</view>
<view class="view-freemoney">
<text class="text-hint">使用余额:</text>
<input placeholder-style="color:#999999" class="input-money" placeholder="输入余额" />
<text class="btn-use" >使用</text>
<text class="text-hint1">您的可用余额{{ freemoney }}</text>
</view>
<view class="view-freemoney">
<text class="text-hint">使用积分:</text>
<input placeholder-style="color:#999999" class="input-money" placeholder="输入积分" />
<text class="btn-use">使用</text>
<text class="text-hint1">您的可用积分{{ pay_points }}</text>
</view>
<view class="class-line1"></view>
<view class="view-price">
<view class="view-price1">
<text class="text-price1">商品总额:</text>
<text class="text-red">¥{{ totalPrice.total_fee }}元</text>
</view>
<view hidden class="view-price1">
<text class="text-price1">邮费:</text>
<text class="text-red">¥20元</text>
</view>
<view class="view-price1">
<text class="text-price1">应付总金额:</text>
<text class="text-red">¥{{ totalPrice.total_fee }}元</text>
</view>
</view>
<view class="text-save" @longtap="longtap">提交订单</view>
</view>
</template>
<script>
export default {
methods:{
clickTest: function(e){
console.log(e);
console.log('click');
},
longtap: function(e){
console.log(e);
console.log('longtap');
}
}
}
</script>
<style lang="scss">
/ pages/order/ordersubmit/index.wxss /
.view-address {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
height: 150rpx;
background: #3aa0b7;
}
.view-addressleft {
display: flex;
flex-direction: row;
padding: 20rpx 0 0 3;
justify-content: flex-start;
align-items: center;
width: 450rpx;
}
.view-addressleft image {
width: 50rpx;
height: 50rpx;
}
.view-addressleft-info {
display: flex;
flex-direction: column;
margin-left: 10rpx;
justify-content: space-around;
align-items: flex-start;
height: 85rpx;
}
.text-name {
font-size: 29rpx;
color: #ffffff;
}
.text-address {
font-size: 29rpx;
color: #ffffff;
}
.view-addressright {
height: 50rpx;
margin-right: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
}
.text-mobile {
font-size: 29rpx;
color: #ffffff;
}
.view-addressright image {
width: 30rpx;
height: 30rpx;
margin-left: 15rpx;
}
.view-title {
display: flex;
flex-direction: row;
height: 75rpx;
align-items: center;
}
.view-title image {
width: 50rpx;
height: 50rpx;
}
.view-title text {
font-size: 29rpx;
color: black;
}
.class-line {
width: 670rpx;
margin-left: 0rpx;
height: 2rpx;
background: #f4f4f4;
}
/外部容器/
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
box-sizing: border-box;
}
/整体列表/
.carts-list {
display: flex;
flex-direction: column;
padding: 0 40rpx;
}
/每行单元格/
.carts-item {
display: flex;
flex-direction: row;
height: 150rpx;
/width属性解决标题文字太短而缩略图偏移/
width: 100%;
border-bottom: 1px solid #eee;
padding: 30rpx 0;
justify-content: space-between;
}
/左部图片/
.carts-image {
width: 130rpx;
height: 150rpx;
border-width: 3rpx;
border-style: solid;
border-color: #ccc;
}
/右部描述/
.carts-text {
margin-left: 10rpx;
width: 370rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.carts-right {
height: 100%;
width: 110rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/右上部分标题/
.carts-title {
font-size: 27rpx;
color: #444;
line-height: 38rpx;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.carts-key-name {
margin-top: 10rpx;
font-size: 27rpx;
color: #999999;
}
/右下部分价格与数量/
.carts-subtitle {
font-size: 25rpx;
color: darkgray;
padding: 0 20rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
}
/价格/
.carts-price {
color: #f60;
font-size: 14px;
line-height: 22px;
margin-top: 12rpx;
}
/商品加减按钮/
/stepper容器/
.stepper {
border: 1px solid #ccc;
border-radius: 3px;
width: 80px;
height: 26px;
margin: 0 auto;
}
/加号与减号/
.stepper text {
width: 19px;
line-height: 26px;
text-align: center;
float: left;
}
/数值/
.stepper input {
color: black;
float: left;
margin: 0 auto;
width: 40px;
height: 26px;
text-align: center;
font-size: 12px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
/普通样式/
.stepper .normal {
color: black;
}
/禁用样式/
.stepper .disabled {
color: #ccc;
}
/复选框样式/
.carts-list icon {
margin-top: 60rpx;
margin-right: 20rpx;
}
/底部按钮/
.carts-footer {
width: 100%;
height: 80rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: 0;
border-top: 1px solid #eee;
background: white;
}
/复选框/
.carts-footer icon {
margin-left: 20rpx;
margin-top: 20rpx;
}
/全选字样/
.carts-footer text {
font-size: 30rpx;
margin-left: 8rpx;
line-height: 10rpx;
}
/立即结算按钮/
.carts-footer .button {
line-height: 80rpx;
text-align: center;
width: 220rpx;
height: 80rpx;
background-color: #f60;
color: white;
font-size: 36rpx;
border-radius: 0;
border: 0;
}
.carts-footer-left {
display: flex;
flex-direction: row;
align-items: center;
}
.carts-footer-left text {
margin-top: 20rpx;
}
.text-price {
font-size: 32rpx;
color: #999999;
}
.text-red {
font-size: 32rpx;
color: orangered;
}
.image-delete {
width: 50rpx;
height: 60rpx;
}
.class-line1 {
width: 750rpx;
height: 20rpx;
background: #f4f4f4;
}
.view-price {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
}
.view-price1 {
margin-top: 30rpx;
margin-right: 20rpx;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.text-price1 {
font-size: 29rpx;
color: grey;
}
.text-save {
width: 680rpx;
align-self: center;
font-size: 32rpx;
margin-top: 30rpx;
color: #ffffff;
text-align: center;
height: 70rpx;
border-radius: 9rpx;
background: orange;
line-height: 70rpx;
margin-bottom: 40rpx;
}
.parent {
display: flex;
flex-direction: column;
width: 100%;
}
.view-freemoney {
width: 100%;
height: 100rpx;
background: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #eee;
}
.text-hint {
font-size: 28rpx;
color: #000000;
margin-left: 40rpx;
}
.text-hint1 {
font-size: 28rpx;
color: #000000;
margin-left: 20rpx;
flex-shrink: 0;
flex-grow: 0;
}
.input-money {
font-size: 26rpx;
color: #999999;
margin-left: 6rpx;
border-style: solid;
border-width: 1rpx;
padding-left: 15rpx;
width: 140rpx;
}
.btn-use {
line-height: 55rpx;
background: red;
color: #ffffff;
text-align: center;
font-size: 28rpx;
padding-left: 10rpx;
margin-left: 5rpx;
padding-right: 10rpx;
}
.radio-style {
font-size: 28rpx;
color: red;
margin-left: 40rpx;
}
.text-redhint {
font-size: 28rpx;
color: red;
}
.text-select {
font-size: 28rpx;
color: #999999;
width: 200rpx;
height: 40rpx;
line-height: 28rpx;
}
.view-picker {
border-style: solid;
border-color: #999999;
padding-left: 20rpx;
display: flex;
flex: row;
height: 60rpx;
justify-content: flex-start;
align-items: center;
width: 200rpx;
border-width: 1rpx;
font-size: 28rpx;
color: #999999;
}
</style>

hbuilder快速换行及查询单词代码快捷键
图一是未换行的代码,然后按ctrl + enter就会变成图二,轻轻松松换行不用动鼠标。
图三是按ctrl + F会跳出下面的框,可以查找也可以替换。
图一是未换行的代码,然后按ctrl + enter就会变成图二,轻轻松松换行不用动鼠标。
图三是按ctrl + F会跳出下面的框,可以查找也可以替换。

关于 1.9.0 调用 API 返回 Promise 对象不正确的临时解决方案
更新:此问题已于1.9.2版本修复,更新新版即可。
=============以下为历史内容==============
首先对于本次 1.9.0 更新引发的返回 Promise 对象不正确的问题表示抱歉,同时也感谢及时反馈问题的小伙伴。
解决方案
HBuilderX
首先,找到 HBuilderX 的安装目录,并访问至 HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/
目录。
然后,下载附件压缩包解压。
最后,使用解压出来的资源,覆盖 HBuilderX 插件目录下的同名资源,即 uni-app-plus、uni-mp-weixin 目录。
cli
cli 创建的项目,访问 项目根目录/node_modules/@dcloudio/
并替换即可。
也可以通过 npm update
更新依赖来解决此问题。
参考文档
关于 uni 对于 Promise 的封装策略,详细请参考:Promise 封装
1.9.1
若更新至 1.9.1 后,在 H5 平台依旧存在问题,请下载附件中的文件,仅覆盖 uni-h5 目录的资源即可。
最后
使用 HBuilderX 请先使用此方案临时解决下问题,今日会更新一个版本来彻底修复此问题。
更新:此问题已于1.9.2版本修复,更新新版即可。
=============以下为历史内容==============
首先对于本次 1.9.0 更新引发的返回 Promise 对象不正确的问题表示抱歉,同时也感谢及时反馈问题的小伙伴。
解决方案
HBuilderX
首先,找到 HBuilderX 的安装目录,并访问至 HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/
目录。
然后,下载附件压缩包解压。
最后,使用解压出来的资源,覆盖 HBuilderX 插件目录下的同名资源,即 uni-app-plus、uni-mp-weixin 目录。
cli
cli 创建的项目,访问 项目根目录/node_modules/@dcloudio/
并替换即可。
也可以通过 npm update
更新依赖来解决此问题。
参考文档
关于 uni 对于 Promise 的封装策略,详细请参考:Promise 封装
1.9.1
若更新至 1.9.1 后,在 H5 平台依旧存在问题,请下载附件中的文件,仅覆盖 uni-h5 目录的资源即可。
最后
使用 HBuilderX 请先使用此方案临时解决下问题,今日会更新一个版本来彻底修复此问题。
收起阅读 »
HBuilderX的几点萌新问题[是我使用姿势不对吗]
我用的是Mac版最新HBuilderX 1.9.0.20190412。
刚开始使用,体验如下:
- 文件改动没有任何的差异化标记。因为以前使用的编辑器里面都有文件是否改动,哪一行改动的标记,能看出这一次与上一次git提交后的差异。现在编辑器里面无法看,无法快速定位到改动过的地方,不得不通过git的gui工具去查看哪里改动了;
- 规范和错误提示并不是很友好的感觉。我写的代码,可能存在很多不规范的地方,虽然可能代码正确,但是不太符合规范,比如命名/空格/过时的语法等,但是没有提示我。然后我在编辑器里面随便敲错误的代码,并不能提示我错误,比如变量未定义就使用。编辑器里面也没有任何错误标记。而是需要我保存的时候进行代码检测,然后告诉我F4提示定位到下一出错误。虽然一般也不会写出什么神级错误,但是很显然有个时候会出现拼写错误的时候,这种情况似乎检测不出。if (plkj) {hello},这种东西在js文件里面没有任何的提示。我保存也不提示错误。plkj hello都没有定义;
- 书写vue文件的时候,我随便敲一个未引入的组建<hello-k></hello-k>也没有给我任何提示,我ctrl+s保存也没有触发什么提示。也没有那种比较便捷的<hello-k>然后输入</就自动补全的功能。因为我以前用WebStorm,各方面都体验很好。
因为刚刚入手,不知道这些问题是我使用的姿势不对,还是编辑器就是这么用的。不管怎么样,希望越做越好。
谢谢。
我用的是Mac版最新HBuilderX 1.9.0.20190412。
刚开始使用,体验如下:
- 文件改动没有任何的差异化标记。因为以前使用的编辑器里面都有文件是否改动,哪一行改动的标记,能看出这一次与上一次git提交后的差异。现在编辑器里面无法看,无法快速定位到改动过的地方,不得不通过git的gui工具去查看哪里改动了;
- 规范和错误提示并不是很友好的感觉。我写的代码,可能存在很多不规范的地方,虽然可能代码正确,但是不太符合规范,比如命名/空格/过时的语法等,但是没有提示我。然后我在编辑器里面随便敲错误的代码,并不能提示我错误,比如变量未定义就使用。编辑器里面也没有任何错误标记。而是需要我保存的时候进行代码检测,然后告诉我F4提示定位到下一出错误。虽然一般也不会写出什么神级错误,但是很显然有个时候会出现拼写错误的时候,这种情况似乎检测不出。if (plkj) {hello},这种东西在js文件里面没有任何的提示。我保存也不提示错误。plkj hello都没有定义;
- 书写vue文件的时候,我随便敲一个未引入的组建<hello-k></hello-k>也没有给我任何提示,我ctrl+s保存也没有触发什么提示。也没有那种比较便捷的<hello-k>然后输入</就自动补全的功能。因为我以前用WebStorm,各方面都体验很好。
因为刚刚入手,不知道这些问题是我使用的姿势不对,还是编辑器就是这么用的。不管怎么样,希望越做越好。
谢谢。

个推基于 Apache Pulsar 的优先级队列方案
作者:个推平台研发工程师 祥子
一、业务背景
在个推的推送场景中,消息队列在整个系统中占有非常重要的位置。
当 APP 有推送需求的时候, 会向个推发送一条推送命令,接到推送需求后,我们会把APP要求推送消息的用户放入下发队列中,进行消息下发;当同时有多个APP进行消息下发时,难免会出现资源竞争的情况, 因此就产生了优先级队列的需求,在下发资源固定的情况下, 高优先级的用户需要有更多的下发资源。
二、基于 Kafka 的优先级队列方案
针对以上场景,个推基于 Kafka 设计了第一版的优先级队列方案。Kafka 是 LinkedIn 开发的一个高性能、分布式消息系统;Kafka 在个推有非常广泛的应用,如日志收集、在线和离线消息分发等。
架构
在该方案中,个推将优先级统一设定为高、中、低三个级别。具体操作方案如下:
-
对某个优先级根据 task (单次推送任务)维度,存入不同的 Topic,一个 task 只写入一个 Topic,一个 Topic 可存多个 task;
-
消费模块根据优先级配额(如 6:3:1),获取不同优先级的消息数,同一优先级轮询获取消息;这样既保证了高优先级用户可以更快地发送消息,又避免了低优先级用户出现没有下发的情况。
二、Kafka 方案遇到的问题
随着个推业务的不断发展,接入的 APP 数量逐渐增多,第一版的优先级方案也逐渐暴露出一些问题:
- 当相同优先级的 APP 在同一时刻推送任务越来越多时,后面进入的 task 消息会因为前面 task 消息还存在队列情况而出现延迟。如下图所示, 当 task1 消息量过大时,在task1 消费结束前,taskN 将一直处于等待状态。
- Kafka 在 Topic 数量由 64 增长到 256 时,吞吐量下降严重,Kafka 的每个 Topic、每个分区都会对应一个物理文件。当 Topic 数量增加时,消息分散的落盘策略会导致磁盘 IO 竞争激烈,因此我们不能仅通过增加 Topic 数量来缓解第一点中的问题。
基于上述问题,个推进行了新一轮的技术选型, 我们需要可以创建大量的 Topic, 同时吞吐性能不能比 Kafka 逊色。经过一段时间的调研,Apache Pulsar 引起了我们的关注。
三、为什么是 Pulsar
Apache Pulsar 是一个企业级的分布式消息系统,最初由 Yahoo 开发,在 2016 年开源,并于2018年9月毕业成为 Apache 基金会的顶级项目。Pulsar 已经在 Yahoo 的生产环境使用了三年多,主要服务于Mail、Finance、Sports、 Flickr、 the Gemini Ads platform、 Sherpa (Yahoo 的 KV 存储)。
架构
Topic 数量
Pulsar 可以支持百万级别 Topic 数量的扩展,同时还能一直保持良好的性能。Topic 的伸缩性取决于它的内部组织和存储方式。Pulsar 的数据保存在 bookie (BookKeeper 服务器)上,处于写状态的不同 Topic 的消息,在内存中排序,最终聚合保存到大文件中,在 Bookie 中需要更少的文件句柄。另一方面 Bookie 的 IO 更少依赖于文件系统的 Pagecache,Pulsar 也因此能够支持大量的主题。
消费模型
Pulsar 支持三种消费模型:Exclusive、Shared 和Failover。
Exclusive (独享):一个 Topic 只能被一个消费者消费。Pulsar 默认使用这种模式。
Shared(共享):共享模式,多个消费者可以连接到同一个 Topic,消息依次分发给消费者。当一个消费者宕机或者主动断开连接时,那么分发给这个消费者的未确认(ack)的消息会得到重新调度,分发给其他消费者。
Failover (灾备):一个订阅同时只有一个消费者,可以有多个备份消费者。一旦主消费者故障,则备份消费者接管。不会出现同时有两个活跃的消费者。
Exclusive和Failover订阅,仅允许一个消费者来使用和消费每个订阅的Topic。这两种模式都按 Topic 分区顺序使用消息。它们最适用于需要严格消息顺序的流(Stream)用例。
Shared 允许每个主题分区有多个消费者。同一个订阅中的每个消费者仅接收Topic分区的一部分消息。Shared最适用于不需要保证消息顺序队列(Queue)的使用模式,并且可以按照需要任意扩展消费者的数量。
存储
Pulsar 引入了 Apache BookKeeper 作为存储层,BookKeeper 是一个专门为实时系统优化过的分布式存储系统,具有可扩展、高可用、低延迟等特性。具体介绍,请参考 BookKeeper官网。
Segment
BookKeeper以 Segment (在 BookKeeper 内部被称作 ledger) 作为存储的基本单元。从 Segment 到消息粒度,都会均匀分散到 BookKeeper 的集群中。这种机制保证了数据和服务均匀分散在 BookKeeper 集群中。
Pulsar 和 Kafka 都是基于 partition 的逻辑概念来做 Topic 存储的。最根本的不同是,Kafka 的物理存储是以 partition 为单位的,每个 partition 必须作为一个整体(一个目录)存储在某个 broker 上。 而 Pulsar 的 partition 是以 segment 作为物理存储的单位,每个 partition 会再被打散并均匀分散到多个 bookie 节点中。
这样的直接影响是,Kafka 的 partition 的大小,受制于单台 broker 的存储;而 Pulsar 的 partition 则可以利用整个集群的存储容量。
扩容
当 partition 的容量达到上限后,需要扩容的时候,如果现有的单台机器不能满足,Kafka 可能需要添加新的存储节点,并将 partition 的数据在节点之间搬移达到 rebalance 的状态。
而 Pulsar 只需添加新的 Bookie 存储节点即可。新加入的节点由于剩余空间大,会被优先使用,接收更多的新数据;整个扩容过程不涉及任何已有数据的拷贝和搬移。
Broker 故障
Pulsar 在单个节点失败时也会体现同样的优势。如果 Pulsar 的某个服务节点 broker 失效,由于 broker 是无状态的,其他的 broker 可以很快接管 Topic,不会涉及 Topic 数据的拷贝;如果存储节点 Bookie 失效,在集群后台中,其他的 Bookie 会从多个 Bookie 节点中并发读取数据,并对失效节点的数据自动进行恢复,对前端服务不会造成影响。
Bookie 故障
Apache BookKeeper 中的副本修复是 Segment (甚至是 Entry)级别的多对多快速修复。这种方式只会复制必须的数据,这比重新复制整个主题分区要精细。如下图所示,当错误发生时, Apache BookKeeper 可以从 bookie 3 和 bookie 4 中读取 Segment 4 中的消息,并在 bookie 1 处修复 Segment 4。所有的副本修复都在后台进行,对 Broker 和应用透明。
当某个 Bookie 节点出错时,BookKeeper会自动添加可用的新 Bookie 来替换失败的 Bookie,出错的 Bookie 中的数据在后台恢复,所有 Broker 的写入不会被打断,而且不会牺牲主题分区的可用性。
四、基于 Pulsar 的优先级队列方案
在设计思路上,Pulsar 方案和 Kafka 方案并没有多大区别。但在新方案中,个推技术团队借助 Pulsar 的特性,解决了 Kafka 方案中存在的问题。
- 根据 task 动态生成 Topic,保证了后进入的 task 不会因为其他 task 消息堆积而造成等待情况。
- 中高优先级 task 都独享一个 Topic,低优先级 task 共享 n 个 Topic。
- 相同优先级内,各个 task 轮询读取消息,配额满后流转至下一个优先级。
- 相同优先级内, 各个 task 可动态调整 quota, 在相同机会内,可读取更多消息。
- 利用 Shared 模式, 可以动态添加删除 consumer,且不会触发 Rebalance 情况。
- 利用 BookKeeper 特性,可以更灵活的添加存储资源。
五、Pulsar 其他实践
- 不同 subscription 之间相对独立,如果想要重复消费某个 Topic 的消息,需要使用不同的 subscriptionName 订阅;但是一直增加新的 subscriptionName,backlog 会不断累积。
- 如果 Topic 无人订阅,发给它的消息默认会被删除。因此如果 producer 先发送,consumer 后接收,一定要确保 producer 发送之前,Topic 有 subscription 存在(哪怕 subscribe 之后 close 掉),否则这段时间发送的消息会导致无人处理。
- 如果既没有人发送消息,又没有人订阅消息,一段时间后 Topic 会自动删除。
- Pulsar 的 TTL 等设置,是针对整个 namespace 起效的,无法针对单个 Topic。
- Pulsar 的键都建立在 zookeeper 的根目录上,在初始化时建议增加总节点名。
- 目前 Pulsar 的 java api 设计,消息默认需要显式确认,这一点跟 Kafka 不一样。
- Pulsar dashboard 上的 storage size 和 prometheus 上的 storage size (包含副本大小)概念不一样。
- 把
dbStorage_rocksDB_blockCacheSize
设置的足够大;当消息体量大,出现backlog 大量堆积时, 使用默认大小(256M)会出现读耗时过大情况,导致消费变慢。 - 使用多 partition,提高吞吐。
- 在系统出现异常时,主动抓取 stats 和 stats-internal,里面有很多有用数据。
- 如果业务中会出现单 Topic 体量过大的情况,建议把
backlogQuotaDefaultLimitGB
设置的足够大(默认10G), 避免因为默认使用producer_request_hold
模式出现 block producer 的情况;当然可以根据实际业务选择合适的backlogQuotaDefaultRetentionPolicy
。 - 根据实际业务场景主动选择 backlog quota。
- prometheus 内如果发现读耗时为空情况,可能是因为直接读取了缓存数据;Pulsar 在读取消息时会先读取 write cache, 然后读取 read cache;如果都没有命中, 则会在 RocksDB 中读取条目位子后,再从日志文件中读取该条目。
- 写入消息时, Pulsar 会同步写入 journal 和 write cache;write cache 再异步写入日志文件和 RocksDB; 所以有资源的话,建议 journal 盘使用SSD。
六、总结
现在, 个推针对优先级中间件的改造方案已经在部分现网业务中试运行,对于 Pulsar 的稳定性,我们还在持续关注中。
作为一个2016 年才开源的项目,Pulsar 拥有非常多吸引人的特性,也弥补了其他竞品的短板,例如跨地域复制、多租户、扩展性、读写隔离等。尽管在业内使用尚不广泛, 但从现有的特性来说, Pulsar 表现出了取代 Kafka 的趋势。在使用 Pulsar 过程中,我们也遇到了一些问题, 在此特别感谢翟佳和郭斯杰(两位均为 Stream Native 的核心工程师、开源项目 Apache Pulsar 的 PMC 成员)给我们提供的支持和帮助。
参考文献:
[1] 比拼 Kafka, 大数据分析新秀Pulsar 到底好在哪(https://www.infoq.cn/article/1UaxFKWUhUKTY1t_5gPq)
[2] 开源实时数据处理系统Pulsar:一套搞定Kafka+Flink+DB(https://juejin.im/post/5af414365188256717765441)
作者:个推平台研发工程师 祥子
一、业务背景
在个推的推送场景中,消息队列在整个系统中占有非常重要的位置。
当 APP 有推送需求的时候, 会向个推发送一条推送命令,接到推送需求后,我们会把APP要求推送消息的用户放入下发队列中,进行消息下发;当同时有多个APP进行消息下发时,难免会出现资源竞争的情况, 因此就产生了优先级队列的需求,在下发资源固定的情况下, 高优先级的用户需要有更多的下发资源。
二、基于 Kafka 的优先级队列方案
针对以上场景,个推基于 Kafka 设计了第一版的优先级队列方案。Kafka 是 LinkedIn 开发的一个高性能、分布式消息系统;Kafka 在个推有非常广泛的应用,如日志收集、在线和离线消息分发等。
架构
在该方案中,个推将优先级统一设定为高、中、低三个级别。具体操作方案如下:
-
对某个优先级根据 task (单次推送任务)维度,存入不同的 Topic,一个 task 只写入一个 Topic,一个 Topic 可存多个 task;
-
消费模块根据优先级配额(如 6:3:1),获取不同优先级的消息数,同一优先级轮询获取消息;这样既保证了高优先级用户可以更快地发送消息,又避免了低优先级用户出现没有下发的情况。
二、Kafka 方案遇到的问题
随着个推业务的不断发展,接入的 APP 数量逐渐增多,第一版的优先级方案也逐渐暴露出一些问题:
- 当相同优先级的 APP 在同一时刻推送任务越来越多时,后面进入的 task 消息会因为前面 task 消息还存在队列情况而出现延迟。如下图所示, 当 task1 消息量过大时,在task1 消费结束前,taskN 将一直处于等待状态。
- Kafka 在 Topic 数量由 64 增长到 256 时,吞吐量下降严重,Kafka 的每个 Topic、每个分区都会对应一个物理文件。当 Topic 数量增加时,消息分散的落盘策略会导致磁盘 IO 竞争激烈,因此我们不能仅通过增加 Topic 数量来缓解第一点中的问题。
基于上述问题,个推进行了新一轮的技术选型, 我们需要可以创建大量的 Topic, 同时吞吐性能不能比 Kafka 逊色。经过一段时间的调研,Apache Pulsar 引起了我们的关注。
三、为什么是 Pulsar
Apache Pulsar 是一个企业级的分布式消息系统,最初由 Yahoo 开发,在 2016 年开源,并于2018年9月毕业成为 Apache 基金会的顶级项目。Pulsar 已经在 Yahoo 的生产环境使用了三年多,主要服务于Mail、Finance、Sports、 Flickr、 the Gemini Ads platform、 Sherpa (Yahoo 的 KV 存储)。
架构
Topic 数量
Pulsar 可以支持百万级别 Topic 数量的扩展,同时还能一直保持良好的性能。Topic 的伸缩性取决于它的内部组织和存储方式。Pulsar 的数据保存在 bookie (BookKeeper 服务器)上,处于写状态的不同 Topic 的消息,在内存中排序,最终聚合保存到大文件中,在 Bookie 中需要更少的文件句柄。另一方面 Bookie 的 IO 更少依赖于文件系统的 Pagecache,Pulsar 也因此能够支持大量的主题。
消费模型
Pulsar 支持三种消费模型:Exclusive、Shared 和Failover。
Exclusive (独享):一个 Topic 只能被一个消费者消费。Pulsar 默认使用这种模式。
Shared(共享):共享模式,多个消费者可以连接到同一个 Topic,消息依次分发给消费者。当一个消费者宕机或者主动断开连接时,那么分发给这个消费者的未确认(ack)的消息会得到重新调度,分发给其他消费者。
Failover (灾备):一个订阅同时只有一个消费者,可以有多个备份消费者。一旦主消费者故障,则备份消费者接管。不会出现同时有两个活跃的消费者。
Exclusive和Failover订阅,仅允许一个消费者来使用和消费每个订阅的Topic。这两种模式都按 Topic 分区顺序使用消息。它们最适用于需要严格消息顺序的流(Stream)用例。
Shared 允许每个主题分区有多个消费者。同一个订阅中的每个消费者仅接收Topic分区的一部分消息。Shared最适用于不需要保证消息顺序队列(Queue)的使用模式,并且可以按照需要任意扩展消费者的数量。
存储
Pulsar 引入了 Apache BookKeeper 作为存储层,BookKeeper 是一个专门为实时系统优化过的分布式存储系统,具有可扩展、高可用、低延迟等特性。具体介绍,请参考 BookKeeper官网。
Segment
BookKeeper以 Segment (在 BookKeeper 内部被称作 ledger) 作为存储的基本单元。从 Segment 到消息粒度,都会均匀分散到 BookKeeper 的集群中。这种机制保证了数据和服务均匀分散在 BookKeeper 集群中。
Pulsar 和 Kafka 都是基于 partition 的逻辑概念来做 Topic 存储的。最根本的不同是,Kafka 的物理存储是以 partition 为单位的,每个 partition 必须作为一个整体(一个目录)存储在某个 broker 上。 而 Pulsar 的 partition 是以 segment 作为物理存储的单位,每个 partition 会再被打散并均匀分散到多个 bookie 节点中。
这样的直接影响是,Kafka 的 partition 的大小,受制于单台 broker 的存储;而 Pulsar 的 partition 则可以利用整个集群的存储容量。
扩容
当 partition 的容量达到上限后,需要扩容的时候,如果现有的单台机器不能满足,Kafka 可能需要添加新的存储节点,并将 partition 的数据在节点之间搬移达到 rebalance 的状态。
而 Pulsar 只需添加新的 Bookie 存储节点即可。新加入的节点由于剩余空间大,会被优先使用,接收更多的新数据;整个扩容过程不涉及任何已有数据的拷贝和搬移。
Broker 故障
Pulsar 在单个节点失败时也会体现同样的优势。如果 Pulsar 的某个服务节点 broker 失效,由于 broker 是无状态的,其他的 broker 可以很快接管 Topic,不会涉及 Topic 数据的拷贝;如果存储节点 Bookie 失效,在集群后台中,其他的 Bookie 会从多个 Bookie 节点中并发读取数据,并对失效节点的数据自动进行恢复,对前端服务不会造成影响。
Bookie 故障
Apache BookKeeper 中的副本修复是 Segment (甚至是 Entry)级别的多对多快速修复。这种方式只会复制必须的数据,这比重新复制整个主题分区要精细。如下图所示,当错误发生时, Apache BookKeeper 可以从 bookie 3 和 bookie 4 中读取 Segment 4 中的消息,并在 bookie 1 处修复 Segment 4。所有的副本修复都在后台进行,对 Broker 和应用透明。
当某个 Bookie 节点出错时,BookKeeper会自动添加可用的新 Bookie 来替换失败的 Bookie,出错的 Bookie 中的数据在后台恢复,所有 Broker 的写入不会被打断,而且不会牺牲主题分区的可用性。
四、基于 Pulsar 的优先级队列方案
在设计思路上,Pulsar 方案和 Kafka 方案并没有多大区别。但在新方案中,个推技术团队借助 Pulsar 的特性,解决了 Kafka 方案中存在的问题。
- 根据 task 动态生成 Topic,保证了后进入的 task 不会因为其他 task 消息堆积而造成等待情况。
- 中高优先级 task 都独享一个 Topic,低优先级 task 共享 n 个 Topic。
- 相同优先级内,各个 task 轮询读取消息,配额满后流转至下一个优先级。
- 相同优先级内, 各个 task 可动态调整 quota, 在相同机会内,可读取更多消息。
- 利用 Shared 模式, 可以动态添加删除 consumer,且不会触发 Rebalance 情况。
- 利用 BookKeeper 特性,可以更灵活的添加存储资源。
五、Pulsar 其他实践
- 不同 subscription 之间相对独立,如果想要重复消费某个 Topic 的消息,需要使用不同的 subscriptionName 订阅;但是一直增加新的 subscriptionName,backlog 会不断累积。
- 如果 Topic 无人订阅,发给它的消息默认会被删除。因此如果 producer 先发送,consumer 后接收,一定要确保 producer 发送之前,Topic 有 subscription 存在(哪怕 subscribe 之后 close 掉),否则这段时间发送的消息会导致无人处理。
- 如果既没有人发送消息,又没有人订阅消息,一段时间后 Topic 会自动删除。
- Pulsar 的 TTL 等设置,是针对整个 namespace 起效的,无法针对单个 Topic。
- Pulsar 的键都建立在 zookeeper 的根目录上,在初始化时建议增加总节点名。
- 目前 Pulsar 的 java api 设计,消息默认需要显式确认,这一点跟 Kafka 不一样。
- Pulsar dashboard 上的 storage size 和 prometheus 上的 storage size (包含副本大小)概念不一样。
- 把
dbStorage_rocksDB_blockCacheSize
设置的足够大;当消息体量大,出现backlog 大量堆积时, 使用默认大小(256M)会出现读耗时过大情况,导致消费变慢。 - 使用多 partition,提高吞吐。
- 在系统出现异常时,主动抓取 stats 和 stats-internal,里面有很多有用数据。
- 如果业务中会出现单 Topic 体量过大的情况,建议把
backlogQuotaDefaultLimitGB
设置的足够大(默认10G), 避免因为默认使用producer_request_hold
模式出现 block producer 的情况;当然可以根据实际业务选择合适的backlogQuotaDefaultRetentionPolicy
。 - 根据实际业务场景主动选择 backlog quota。
- prometheus 内如果发现读耗时为空情况,可能是因为直接读取了缓存数据;Pulsar 在读取消息时会先读取 write cache, 然后读取 read cache;如果都没有命中, 则会在 RocksDB 中读取条目位子后,再从日志文件中读取该条目。
- 写入消息时, Pulsar 会同步写入 journal 和 write cache;write cache 再异步写入日志文件和 RocksDB; 所以有资源的话,建议 journal 盘使用SSD。
六、总结
现在, 个推针对优先级中间件的改造方案已经在部分现网业务中试运行,对于 Pulsar 的稳定性,我们还在持续关注中。
作为一个2016 年才开源的项目,Pulsar 拥有非常多吸引人的特性,也弥补了其他竞品的短板,例如跨地域复制、多租户、扩展性、读写隔离等。尽管在业内使用尚不广泛, 但从现有的特性来说, Pulsar 表现出了取代 Kafka 的趋势。在使用 Pulsar 过程中,我们也遇到了一些问题, 在此特别感谢翟佳和郭斯杰(两位均为 Stream Native 的核心工程师、开源项目 Apache Pulsar 的 PMC 成员)给我们提供的支持和帮助。
参考文献:
[1] 比拼 Kafka, 大数据分析新秀Pulsar 到底好在哪(https://www.infoq.cn/article/1UaxFKWUhUKTY1t_5gPq)
[2] 开源实时数据处理系统Pulsar:一套搞定Kafka+Flink+DB(https://juejin.im/post/5af414365188256717765441)
收起阅读 »
音频播放,兼容IOS网络路径播放和IOS静音模式下播放。
<div class="playAudio">播放</div>
<div class="stopAudio">停止</div>
<audio style="display:none;" id="audioPlay" src="http://fjdx.sc.chinaz.net/Files/DownLoad/sound1/201812/10962.mp3"></audio>
var audio;
document.addEventListener('plusready', function(){
//防止IOS静音模式下播放没有声音,先播放一个静音的音效
var loadAudio = plus.audio.createPlayer( "/audio/load.mp3" );
loadAudio.play();
audio = document.getElementById("audioPlay");
});
$(function(){
$(".playAudio").click(function(){
audio.load();
audio.oncanplay = function () {
audio.play();
console.log(audio.duration); //音效时长
}
});
$(".stopAudio").click(function(){
audio.pause();
});
});
<div class="playAudio">播放</div>
<div class="stopAudio">停止</div>
<audio style="display:none;" id="audioPlay" src="http://fjdx.sc.chinaz.net/Files/DownLoad/sound1/201812/10962.mp3"></audio>
var audio;
document.addEventListener('plusready', function(){
//防止IOS静音模式下播放没有声音,先播放一个静音的音效
var loadAudio = plus.audio.createPlayer( "/audio/load.mp3" );
loadAudio.play();
audio = document.getElementById("audioPlay");
});
$(function(){
$(".playAudio").click(function(){
audio.load();
audio.oncanplay = function () {
audio.play();
console.log(audio.duration); //音效时长
}
});
$(".stopAudio").click(function(){
audio.pause();
});
});
收起阅读 »