字节跳动小程序 【开发】 自学总结

准备
  • 微信小程序开发文档 官网 - https://developers.weixin.qq.com/miniprogram/dev/framework/
    由于字节跳动小程序很类似微信小程序,但是文档的详细程度却差一些,所以需要微信小程序开发文档做对照
开发阶段

目录结构同微信小程序类似,app.json 为全局设置。

  1. 认证注意事项
    企业开发者:适用于企业,个体工商户,政府组织,海外机构等其他机构。每个企业主体可以验证【 10 个企业小程序 】。
    特别注意:认证主体后续需与支付主体、广告结算主体保持一致

此外,如果在广告中心开通了激励广告需要注意

1. 目前激励视频广告仅支持抖音端,接入需判断接入宿主及宿主版本, 在不支持的宿主及版本上需要将结果直接展示给用户。
2. 展示广告前向用户说明激励广告规则,明确告知用户看完视频广告后能获得相应奖励。
3. 一个页面最多出现一个激励视频广告。
4. 需要用户主动操作点击按钮,才能创建和获取激励视频广告。
5. 需判断广告异常情况,如不支持激励视频的低版本用户、广告调用失败等情况,应直接给予奖励。
激励广告规范

0-1. 关于激励广告
//--app.js

globalData:{
    'videoAd': null,
},
rewardedVideoAd(){
    var _that = this;
    var _videoAd = _that.globalData.videoAd;

    //兼容
    //目前只能在抖音使用该方法,今日头条等宿主暂不支持
    let version = tt.getSystemInfoSync().SDKVersion;
    if(_that.compareVersion(version,'1.57.0')){
      if(_videoAd==null){
        //初始化
        if(tt.createRewardedVideoAd){
            _videoAd = tt.createRewardedVideoAd({
                adUnitId: '申请的激励广告ID'
            });
            
            //广告显示成功,先解除绑定close事件的监听器,为后续添加准备
            if(typeof _videoAd != 'undefined'){
              _videoAd.offClose((res)=>{
                console.log('广告组件解绑');
              });
            };

            // return _videoAd;
            _that.globalData.videoAd = _videoAd;
        }else{
            tt.showModal({
                title: "提示",
                content:
                "当前客户端版本过低,无法使用该功能,请升级客户端或关闭后重启更新。"
            });
            // return null;
            _that.globalData.videoAd = null;
        }
      };
    }else{
      tt.showModal({
        title: '提示',
        content: '当前版本过低,无法获取激励广告功能,请升级到最新版本后重试。'
      });
    }; 
},
compareVersion(v1, v2){
    v1 = v1.split('.')
    v2 = v2.split('.')
    const len = Math.max(v1.length, v2.length)
    while(v1.length < len){
      v1.push('0')
    }
    while(v2.length < len){
      v2.push('0')
    }
    for(let i = 0; i < len; i++){
      const num1 = parseInt(v1[i])
      const num2 = parseInt(v2[i])
      if (num1 > num2){
        return true;
      }else if(num1 < num2){
        return false;
      }
    }
},

//需求页面,调用广告

//获取激励广告唯一对象      
var rewardedVideoAdObj = app.globalData.videoAd;

//用户点击触发的激励广告
showRewardedVideoAd(){
    //广告
    if(rewardedVideoAdObj){
      //版本符合,看广告
      rewardedVideoAdObj.show().then((res) => {
        rewardedVideoAdObj.offClose(res=>{});
        rewardedVideoAdObj.onClose(res => {
          clearTimeout(rewardedVideoAdObj.iTimer);
          rewardedVideoAdObj.iTimer = setTimeout(function(){
            if(res.isEnded) {
              // console.log('给与奖励');
              //观看完广告 -> 奖励流程
              adAllow();
            }else{
              // console.log('广告未观看完毕');
            }
          },500);
        });
      })
      .catch(err => {
        console.log("广告组件出现问题", err);
        //发生错误看不了广告 -> 奖励流程
        adAllow();
      });
    }else{
      //版本低,看不了广告 -> 奖励流程
      adAllow();
    }
  },
  //观看完激励广告的奖励流程
adAllow(){
  dosomething...
},

0-2. 关于插屏广告
值得注意的是:

  • 不能打断用户的完整操作过程,例:不能在快速的信息流下拉刷新过程中插入广告
  • 不能在用户刚打开小程序时就插入广告,即便不在首屏,也需要等待一会儿再进入广告所在页面
    否则报错,APP刚刚启动,广告不能显示
//抖音插屏广告
interstitialAd(){
    //目前只能在抖音使用该方法,今日头条等宿主暂不支持
    //插屏广告组件每次创建都会返回一个全新的实例,默认是隐藏的,需要调用 InterstitialAd.show 将其显示
    //基础库 1.70.0 开始支持本方法
    let version = tt.getSystemInfoSync().SDKVersion;
    if(app.compareVersion(version,'1.70.0')){
      //创建插屏广告
      // console.log('创建插屏广告');
      let interstitialAd = tt.createInterstitialAd({
        adUnitId: "19j5e8eiiae4h5fa17",
      });

      if(typeof interstitialAd != 'undefined'){
        interstitialAd.load().then(() => {
          interstitialAd.show();
        }).catch((err) => {
          console.log(err);
        });
      };
    }else{
      tt.showModal({
        title: '提示',
        content: '当前版本过低,无法获取插屏广告功能,请升级到最新版本后重试。'
      });
    };
}

0-3. 关于上下架
手动下架的小程序(小游戏),需要再次审核,审核通过后,方可再次上架
暂时没有微信的 临时下架功能,如 下架进行维护,再自己操作上架,而无需审核

全局设置
  1. 如增加Tab需要设置图标,
    图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效,不支持网络图片
"tabBar": {
    "color":"#666666",
    "selectedColor":"#2a76ff",
    "backgroundColor":"#FFF",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "标题1",
        "iconPath":"pages/resource/images/icon1.png",
        "selectedIconPath":"pages/resource/images/icon1_ac.png"
      },
      {
        "pagePath": "pages/second/index",
        "text": "标题2",
        "iconPath":"pages/resource/images/icon3.png",
        "selectedIconPath":"pages/resource/images/icon3_ac.png"
      }
    ]
  }
  1. 页面跳转分为标签跳转和程序跳转
    标签跳转
<navigator 
  url="navigate?title=navigate" 
  hover-class="navigator-hover"
>
...
</navigator>

navigator-hover 默认为 {background-color: rgba(0, 0, 0, 0.1); opacity: 0.7;},<navigator/>的子节点背景色应为透明色。

2-0-1. 删除 navigator 默认点击样式
法1

.navigator-hover { background-color:rgba(0,0,0,0); opacity:1;}

法2

<navigator hover-class="none" url="..."></navigator>

程序跳转

tt.navigateTo({
      url: '/pages/match_expert/index?planid='+_planId,      //绝对路径
      url: '../match_expert/index?planid='+_planId,                //相对路径
      success(res) {
        console.log(`${res}`);
      },
      fail(res) {
        console.log(`navigateTo调用失败`);
      }
 });

【注】: 不能跳转到 TabBar 页面

2-0. 跳转到TabBar的方法

tt.switchTab({
        url: '../my/index',
        success(res){},
        fail(err){
          console.log(err);
          console.log(`navigateTo调用失败`);
        }
});

2-1. 跳转只要到达对应文件夹即可,不用指定到xxx.ttml
2-2. 获取跳转的get参数 , 在被跳转页的js文件,生命周期onload参数中获取

onLoad: function(option){
    let pageId = option.detailId || "";
    if(pageId){
      //获取信息
      this.getMes(pageId);
    }
  }

2-3. 如果需要跳转的页面,在app.json中的tabBar内,则需要使用

tt.switchTab({
        url: `pages/my`,
        success(res){},
        fail(err) {
          console.log(`switchTab调用失败`);
        }
});

2-4. 返回上一页,如果目标页面为非TAB页,则可以使用

tt.navigateBack({
  delta: 1,
  success(res) {
    console.log(`${res}`);
  },
  fail(res) {
    console.log(`navigateBack调用失败`);
  },
});
  1. 小程序对Javascript语法的 支持程度 - https://microapp.bytedance.com/dev/cn/mini-app/develop/framework/mini-app-runtime/javascript-support

  2. 模块化,提取公共脚本
    导出

// common.js
function hello(name) {
  console.log(`Hello ${name} !`);
}
module.exports.hello = hello;

导入

var common = require("common.js");
Page({
  helloWorld: function() {
    common.hello("world");
  }
})
  1. 标签有限,基本这三个就够用了
    view - div text - span image - img

5-1. 如果渲染的文本内包含html标签,则需要使用rich-text。官方连接文档 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/component/basic-content/rich-text
如:

<rich-text nodes="{{someData}}"></rich-text>
  1. 布局单位与实际操作
    单位 rpx 据说可以根据屏幕比例变化
    | 设备 | rpx 换算 px (屏幕宽度/750) | px 换算 rpx (750/屏幕宽度) |
    | iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
    建议:
    设计师可以用 iPhone6 作为视觉稿的标准。
    即使用PSD尺寸为750px的设计稿,然后1:1测量,单位使用rpx即可

  2. 图片标签新裁切属性属性,同web中CSS3的object-fit

7-1. 如果希望图片宽度100%,高度自适应,
则需要给图片添加mode="widthFix"和样式 width;100% 即可,
如果不设置mode="widthFix",高度样式不设置,那么会图片会走默认的系统预制高度
如果不设置mode="widthFix",高度样式设置auto,那么高度为0
两种都不是图片高度自适应

<image src="../resource/images/banner.jpg" mode="widthFix" />

7-2. 如果希望固定宽高内,不镂空,则需要图片设置宽高尺寸后,使用 mode使用aspectFill 即可

  1. 渲染转译符 增加属性decode
    <view><text decode> </text></view>

  2. 调试工具缓存有时候很大,通过开发者工具清除缓存按钮不好使,直接退出再重进,即可

9-1. setData给对象内属性赋值

Page({
  data: {
    gameData:{
      level:0, //评价等级
      totalScore:0, //总分数
      questionIndex:0 //当前题目
    }
  }
});

设置数据
this.setData({
    ['gameData.totalScore']: 1000
});

9-2. setData给对象内属性赋值,且子对象为变量

data:{
    totalMatchData:[ [],[],[],[],[] ]
}

//indexNum为数组索引,且为变量
var _mdata =  'totalMatchData['+indexNum+']';
_that.setData({
  [_mdata]: xxx
});

BUG:这种方式如果设置多个动态数据,只有最后一个会生效???!!!!

  1. 返回顶部 按钮怎么操作页面
  • 针对scroll-view
    可以使用重置这个属性的值 scroll-into-view ,值为页面上目标点的元素的id即可,注意这个元素必须是 scroll-view 包含结构内的
    注:
    如果元素的在底部触发了bindscrolltolower触底行为,通过scroll-into-view指向这个元素的ID,
    会将scorll-view拉回到这个元素位置,但是其实这样做,会多滚动一屏的高度,需要修正这个位置
    所以,可以在这个元素上添加position:absolute定位元素到 负一屏的高度,如果还有偏差可以用padding做二次修正

补充:

  • 对于<scroll-view> 必须要添加一个高度限制,否则效果不会生效
  • 指定锚点元素的id (不能以数字开头)
  • 如果在<scroll-view>内动态添加循环元素,该组件会回到 scrollTop 为 0 的位置,需要将 scroll-view 组件的全部子元素包裹一层 view 可避免该问题 (官方提示)
  • 锚点ID元素可以放置在循环元素内部
//--- ttml
<!-- scroll-view -->
<scroll-view
    class="scrollViewBox"
    scroll-y
    scroll-with-animation
    scroll-into-view="{{returnTopEle}}"
>
    <view class="mainTopShow style2Box">
        <!--阵容-->
        <text id="topEleTar"></text>
        <include src="../../resource/template/match_detail_2.ttml" />
    </view>
</scroll-view>

//--- js
data: {
    returnTopEle:"topEleTar"
},
returnFn(){
  this.setData({
    returnTopEle:"topEleTar"
  });
}
  • 针对非scroll-view
    使用API
tt.pageScrollTo({
  scrollTop: 0,
  duration: 1000,
  success(res) {
    //console.log(`pageScrollTo调用成功`);
  },
  fail(res) {
    console.log(`pageScrollTo调用失败`);
  }
});
  1. 网络请求需要配置白名单 位置 - https://developer.toutiao.com/dev/cn/mini-app/develop/api/other/network-request-reference
    特别注意:线上环境,网络请求仅支持 https 协议的 URL

  2. 小程序点击,不支持事件处理函数传参,带上参数会认定整体为 函数名,报错未定义
    解决通过自定义属性

<view class="selectBtns pos1 ac" bindtap="selectClickFn" data-index="1">
    <view class="selectBtn1Bg">
        <text>罗纳尔多</text>
    </view>
</view> 

事件处理函数默认参数为事件对象,通过其可以过得自定义属性

selectClickFn(ev){
    console.log(ev.currentTarget.dataset.index); //1
}

12-1. 阻止事件冒泡的行为要通过修改事件本身来做,不支持修饰符,不支持事件对象stopPropagation

触摸点击事件: bindtap --> catchtap 即可
小程序中存在冒泡的事件:

冒泡事件

注:
事件绑定后没有反应的情况,可能是以下情况

  • text标签内嵌套text标签,在里面的text上绑定事件,无法触发
  1. canvas在小程序(非小游戏)中,存在开发工具渲染正常,真机尺寸错误的情况
    表现 以 iphone6 为基准绘制的页面,在iphone7 plus上表现 canvas变小,在小米5上表现 canvas变大
    因此需要对canvas绘图尺寸进行二次修改
    如:
tt.getSystemInfoSync().windowWidth -- 真机宽度 
tt.getSystemInfoSync().windowHeight -- 真机高度 

开发者工具中,如选择iphone6为开发参照,则获取设备宽高做参照
以iphone6为例
375 -- 模拟器上宽度 
603 -- 模拟器上高度 

在真机上,模拟器数据需要修正的比例值
宽度修正比例值 = 真机宽度 / 模拟器上宽度 
高度修正比例值 = 真机高度 / 模拟器上高度 

【实操】: iphoneX修正异常、如果修正后的高度大于宽度,需要调整高度为宽度的数值
再次测试,iPhone与小米真机表现一致了


修正cy值
  1. 小程序支持的canvas属性 字节跳动小程序官网 - https://microapp.bytedance.com/dev/cn/mini-app/develop/api/interface/canvas-draw/tt.createcanvascontext

  2. 小程序图片预加载
    循环数组数据创建图片,通过图片的bindloadbinderror的变化,来判断加载进度与是否加载完毕,
    暂时没有发现 new Image() 或类似 小游戏的 tt.createImage 的东西

<!-- ttml -->
<!--加载图片-->
<view class="onlyLoadImgArr">
    <view tt:if="{{allImgArr}}" tt:for="{{allImgArr}}" tt:for-index="idx" tt:key="*this">
        <image bindload="bindloadFn" binderror="binderrorFn" src="{{item}}" >
    </view>    
</view>

<!--js-->
//图片加载
bindloadFn(res){
    let _that = this;
    if(res.type=="load"){
      loadNum+=1;
      this.setData({
        allImgLoad:Math.floor(loadNum/sumLoadNum*100)
      });
      if(loadNum==sumLoadNum){
        this.setData({
          allLoadText:'加载完毕'
        });
        setTimeout(function(){
          _that.setData({
            scene:2
          });
        },800);
      }
    }
},
binderrorFn(err){
    console.log('图片加载错误');
    console.log(err);
}
  1. 音频
  • 分为背景音频
const backgroundAudioManager = tt.getBackgroundAudioManager();
backgroundAudioManager.src = "https://xxx/0000-0001.mp3";
BackgroundAudioManager.play();
BackgroundAudioManager.pause();
BackgroundAudioManager.stop();
//音频加载回调
BackgroundAudioManager.onWaiting(function callback)
//音频播放中
paused  --- boolean | 当前音频是否处于暂停状态,只读
  • 普通音频
const innerAudioContext = tt.createInnerAudioContext();
innerAudioContext.src = "https://someaudiourl";
innerAudioContext.volume = 0.5;  //范围 0~1。默认为 1 只读
innerAudioContext.onPlay(() => {
  console.log("开始播放回调");
});
innerAudioContext.pause();
InnerAudioContext.stop();
  1. 动画 animate.css 需要修改下后缀名 如 .ttss
  <!--触发动画-->
  <view class="{{scene==2? 'bounce animated':' '}}"></view>
  1. 获取 基础库版本 (' 论坛提交问题需要基础库版本号 ');
    所有参数 - https://developer.toutiao.com/dev/cn/mini-app/develop/api/device/system-information/tt.getsysteminfo
//获取基础库版本
tt.getSystemInfo({
      success(res) {
        console.log(res.SDKVersion);
      }
});
  1. 全局数据的获取与设置
    在app.js中
//获取
onLaunch: function () {
    console.log(this.globalData.some_data);  // 10
  },
  //全局数据
  globalData: {
    some_data:10
  },
  ...

//设置
this.globalData.some_data= 1000;  

在非app.js中

const app = getApp()
//获取
console.log(app.globalData.some_data);

//设置
app.globalData.some_data = 1000;

19-1. 关于onLaunch生命周期,有且仅加载一次,可以通过二维码带参数,参数的获取方式为
以自定义参数'channel'为例

if(options.query && options.query.channel){
    _that.globalData.channel = options.query.channel;
};

返回参数的种类 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/foundation/lifecycle/tt-get-launch-options-sync/

参数的种类

19-2. 当页面通过二维码加载出错时候,需要重定向

onPageNotFound(res) {
    //如果不是 tabbar 页面
    tt.redirectTo({
      url: "pages/index/index",
    }); 
    //如果是 tabbar 页面,请使用 tt.switchTab
    tt.switchTab({
      url: "pages/index/index",
    }); 
},
  1. 获取自定义属性
    20-1. 在非组件标签上挂载属性时候,可以使用 event.currentTarget.dataset.index来获取;
//--ttml
<view class="nav" data-index="100" bindtap="navClickFn">
  <text>按钮1</text>
</view>
//--js
function navClickFn(event){
  if(typeof event.currentTarget.dataset.index!= 'undefined'){
    let _index = event.currentTarget.dataset.index;
    console.log(_index); //100
  }
}

20-2.获取组件
区别与使用普通标签,在组件上挂载data属性,可以使用 event.target.dataset.index来获取; 如: pinker组件

  1. 导入ttml模板
<include src="../../resources/ttml/header.ttml" />
  1. InnerAudioContext与BackgroundAudioManager冲突,音效会终止BackgroundAudioManager背景音
    解决的方法:
    将背景音频(BackgroundAudioManager)用音效(InnerAudioContext)代替,
//背景音函数
let bgsrc = 'https://www.aaa.com/bgm1.mp3';
function createBGMObj(_src){
  var bgmOb = tt.createInnerAudioContext();
  bgmOb.src = _src;
  bgmOb.autoplay = true;
  bgmOb.loop = true;
  //音频加载中
  bgmOb.onWaiting(function(res){ });
  //音频可播放
  bgmOb.onCanplay(function(){
    bgmOb.play(); 
  });
  return bgmOb;
};

const gameBGM = createBGMObj(bgsrc);

if(!gameBGM.paused){
      this.setData({
        bgmBtnStatue:true //音频播放的UI类名
      });
      gameBGM.stop && gameBGM.stop();
      console.log('暂停');
    }else{
      this.setData({
        bgmBtnStatue:false //音频暂停的UI类名
      });
      gameBGM.play && gameBGM.play();
      console.log('播放');
}
  1. 小程序轮播图指示点颜色 ( 默认: indicator-color ,选中:indicator-active-color)
    更多参数配置 - https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html
<swiper
    indicator-dots="true"
    indicator-color="rgba(255,255,255,0.5)"
    indicator-active-color="#f60"
    autoplay="true"
    interval="3000"
    duration="500"
>
    <block tt:for="{{bannerResult}}">
        <swiper-item>
            <view class="swiper-item">
                <image src="{{item.list_pic}}" mode="aspectFill" />
                <text class="mes">{{item.title}}</text>
            </view>
        </swiper-item>
    </block>
</swiper>
  1. 修改swiper容器高度
//--ttml
<view class="bannerBox">
   <swiper
    indicator-dots="true"
    indicator-color="rgba(0,0,0,0.5)"
    indicator-active-color="#f60"
   >
...

//--ttss
.homePage .ourCup .bannerBox -- 包裹swiper容器高度
.homePage .ourCup .bannerBox tt-swiper  -- swiper自身高度
  1. 背景图 -- ( 图片精灵可以放在这里 )
    样式中使用本地背景图,可以将背景图文件与app.json同级,然后通过
background:url('resource/image/bell.png') no-repeat left top;

或者使用 base64、或者网络图片

  1. 下载远程图片至本机相册
//--- ttml
<button type="primary" bindtap="downloadImgFn">下载</button>
//--- js
  downloadImgFn(){
    var _src = 'https://www.xxx.com/upload/images/temp/3.d5aabd23.jpg'
    tt.getImageInfo({
      src: _src,
      success(res) {
        var _path = res.path
        tt.saveImageToPhotosAlbum({
          filePath: _path,
          success(res) {
            console.log(`saveImageToPhotosAlbum调用成功`);
          },
          fail(res) {
            console.log(`saveImageToPhotosAlbum调用失败`);
          }
        });
      }
    });
  }

26-1. 方式2,通过 tt.downloadFile 下载网络图片至本地缓存,获取tempFilePath,然后通过
tt.saveImageToPhotosAlbum,配置上参数tempFilePath,下载至本地相册

这个过程中会询问用户相册的权限,此外需要配置下载文件白名单
且白名单为https协议,不能加端口号,否则失效

白名单不要加端口号

  1. 组件 switch
    修改背景色 使用 属性 color设置
    修改大小,使用transform:scale(xxx) 调整

  2. 定位为fixed的元素不要放在scroll-view中,真机不会生效(不像模拟器)

  3. 表单输入框、文本域限制字数
    ttml

<view class="topArea">
    <textarea
        class="textareaBox"
        placeholder="请输入您的意见或建议"
        placeholder-style="color:#c9c9c9;"
        bindinput="textareaInputFn"
        bindblur="textareaBlurFn"
        value="{{txtValue1}}"
        maxlength="{{limitNum1}}"
    />
    <view class="textareaBtm">
        <text>{{txtLength1}}/{{limitNum1}}</text>
    </view>
</view>

js

data: {
    txtValue1:'',
    txtLength1:0,
    limitNum1:100
  },
textareaInputFn(event){
    var txtObj = event.detail;
    var _txt = txtObj.value;
    var _txtLength = txtObj.cursor;
    if(_txt.length == this.data.limitNum1){
      commonTool.showPop('字数已经达到上限');
    }
    //限制字数
    var _txt2 = _txt.substring(0,this.data.limitNum1);
    var _txtLength2 = _txt2.length;
    //事件对象属性修改
    txtObj.value = _txt2;
    txtObj.cursor = _txtLength2;
    this.setData({
      txtValue1: _txt2,
      txtLength1: _txtLength2
    });
  },
  textareaBlurFn(event){},

注:textarea不像input,无法通过setData来改变绑定其上的value数据,真实限制字数通过maxlength属性生效

29-1. event.detail.cursor 为光标位置
通过输入框光标位置,也可以判断字数达到上限与否

[注]: textarea 背景色BUG
Bug:当在 <textarea>外面包裹一个元素且设置有背景值颜色时,在 Android 手机上背景颜色是不能在<textarea>上生效的。此处可以通过直接在 <textarea> 上设置与包裹元素相同的背景颜色解决。

if(event.detail.cursor > 40){
  ...do something
}

29-2. 表单 input 的type 可以设置多种

说明 最低支持版本
text 文本输入键盘 1.0.0
number 数字输入键盘 1.0.0
digit 带小数点的数字键盘 1.0.0
  1. 真机与模拟器在include上渲染不一
    模拟器可以使用<include src="../../resource/template/xxx.ttml" />,引入模板文件;
    但是在真机上无法完成渲染,
    解决方法,将内容从include中拿出来

  2. 纯文本中存在\r\n,需要渲染出换行效果,
    pre-wrap : 保留空白符序列,但是正常地进行换行。

<view style="white-space:pre-wrap">
      {{item.scoreTabData.description}}
</view>
  1. tt:for操作报错Polymer::Attributes: couldn't decode Array as JSON:
    解决方法,
    修改循环单位名字 如: tt:for-item="seasonItem"

  2. 多重三目运算

matchInsetItem.status=='Uncertain'?'待定':(matchInsetItem.status=='Postponed'?'推迟':(matchInsetItem.status=='Played'?'结束':''))
  1. swiper与按钮联动
    点击按钮,切换swiper的current,联动触发swiper其bindchange事件,
    相反的,切换swiper,并不会触发按钮的bindtap事件,
    所以关联是单向的,最终会体现在swiper的bindchange上
    因此,为了防止swiper中嵌套scroll-view后,纵向滑动scroll-view引起事件,触发swiper横向操作误判
    可以操作索引后,仅在swiper的bindchange上,开启延迟定时器,触发事件

  2. 【优化】
    tt:if 显示隐藏 -- 渲染成本高 [ 初始化渲染使用,不频繁显示消失 ]
    通过样式控制显示隐藏 -- 渲染成本低 [ 频繁切换显示、消失使用 ]

  3. 隐藏loading,弹出toast,存在冲突
    loading 的实现基于 toast,所以hideLoading也会将 toast 隐藏。
    解决:

app.showPop('登录成功',400);
setTimeout(()=>{
    app.hideLoadPop();
},800);
  1. 授权被拒,无法拉起再次授权
  • 常规情况下,是主观还是意外拒绝授权后,是无法再次拉起授权的。需要退出小程序,二次进入,才可重新打开授权
    但是,可以通过openSetting,来拉起,( 设置页面只包含用户请求过的权限 )
  • 通过一个自定义按钮来触发即可
tt.openSetting({
    success:function(res){
        ... 内部逻辑
    },
    fail:function(err){
        ...        
    }
});

37-1. 授权被拒后,内部逻辑,可以根据用户最新授权变化,再进行业务
具体的授权列表 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/open-interface/set/auth-setting/

tt.openSetting({
    success:function(res){
        if(res.authSetting['scope.userInfo']){
          //再次登录逻辑
        }
    },
    fail:function(err){
        ...        
    }
});

注:

  • 不能在初始页面就进行用户强制授权,
  • 授权需要用户点击按钮,弹出浮层,其中解释需要用户什么信息,以及提供两个按钮,确认授权、取消授权,在点击确认授权后,在调用官方的使用用户信息授权的API
  • 在拉起授权API后,如果用户点击取消授权时候,需要单独提供一个手动授权的按钮,因为之前的授权API很长的一段时间会直接选择禁止授权(出于防止用户被骚扰的原因),如果用户想选择确认授权,需要通过一个按钮点击调用授权菜单,用户自行去打开授权即可
授权的一个流程
  • scope列表:
scope 对应接口 描述
scope.userInfo tt.getUserInfo 是否授权用户信息
scope.userLocation tt.getLocation,tt.openLocation 是否授权地理位置
scope.address tt.chooseAddress 是否授权通讯地址
scope.record tt.getRecorderManager.start 是否授权录音功能
scope.album tt.saveImageToPhotosAlbum,tt.saveVideoToPhotosAlbum 是否授权保存到相册
scope.camera tt.scanCode,tt.chooseImage,tt.chooseVideo 是否授权摄像头
  1. 上传图片
    (第一步).需要拉起本地相册,【需要用户授权】
tt.chooseImage({
      sourceType: ["album"],
      count:1,
      success(res) {
        this.setData({
          imagePath: res.tempFilePaths[0],
          imageFile: res.tempFiles[0]
        });
      },
      fail(res) {
        console.log(`chooseImage调用失败`);
      }
});

(第二步).上传文件

let fileTask = tt.uploadFile({
      url: _baseUrl+'api/file',
      filePath: (上一步的imagePath),
      name: "file",
      success(res) {
        if(res.statusCode === 200){
          //将用户头像在数据库内修改
          ....
        }
      },
      fail(err){
        console.log(err);
        console.log(`uploadFile调用失败`);
      }
});

38-1. 上传图片分为2步操作,选择图片部分可以一次选择多张,但是上传图片目前API只支持单张上传

  1. 输入框修改内容如何优化
    通过bindinput修改数据,但是不要使用setData,降低设置数据性能消耗,将其值存在变量中
    在bindblur时候,进行setData,将变量赋值到对应数据上去;
    此外bindblur时候setData需要时间,不是立即完成的,所以需要开启延迟定时器,再去setData

  2. 版本更新
    要点:1.需要做低版本兼容 2.上一个版本需要有此代码,此代码才有反应,否则没有返回

//获取版本信息
if(tt.getUpdateManager){
  var updateManager = tt.getUpdateManager();
  updateManager.onCheckForUpdate(function(res) {
    // 请求完新版本信息的回调
    console.log("onCheckForUpdate", res.hasUpdate);
    if (res.hasUpdate) {
      tt.showToast({
        title: "即将有更新请留意"
      });
    }
  });
  updateManager.onUpdateReady(() => {
    tt.showModal({
      title: "更新提示",
      content: "新版本已经准备好,是否立即使用?",
      success: function(res) {
        if (res.confirm) {
          // 调用 applyUpdate 应用新版本并重启
          updateManager.applyUpdate();
        } else {
          tt.showToast({
            icon: "none",
            title: "小程序下一次「冷启动」时会使用新版本"
          });
        }
      }
    });
  });
  updateManager.onUpdateFailed(() => {
    tt.showToast({
      title: "更新失败,下次启动继续..."
    });
  });
}else{
  tt.showModal({
    title: "提示",
    content:
      "当前客户端版本过低,无法使用该功能,请升级客户端或关闭后重启更新。"
  });
}
  1. 生命周期
  • navigateTo, redirectTo 只能打开非 tabBar 页面。
  • switchTab 只能打开 tabBar 页面。
  • reLaunch 可以打开任意页面。
    同级AB栏目之间的 TAB切换
    A ~> B A.onHide(), B.onLoad(), B.onShow()
    C ~> A C为A详细页 C.onUnload(), A.onShow()
    一般情况,先onload,后续同级TAB切换为onShow ; TAB进去非TAB详细页则,触发onLoad,onShow
  1. 跳转返回上一步
    tt.redirectTo不能跳转到 TabBar 页面
    但是 redirectTo方法生成的页面,可以返回到 TabBar 页面
backBtnFn(){
    tt.navigateBack({
      delta: 1,
      success(res) {
        // console.log(`${res}`);
      },
      fail(res) {
        console.log(`navigateBack调用失败`);
      }
    });
}
  1. 下拉刷新、上拉加载
    (1).配置app.json中window属性,enablePullDownRefresh 与 onReachBottomDistance
"window":{
    "navigationBarTextStyle":"black",
    "navigationBarBackgroundColor": "#fff",    
    "backgroundTextStyle": "dark",
    "navigationBarTitleText": "xxxxxxx",
    "enablePullDownRefresh": true,
    "onReachBottomDistance":50
  },

(2).在页面的js中配置

//下拉刷新
onPullDownRefresh(){
    console.log('onPullDownRefresh');
    tt.stopPullDownRefresh();
},
  //上拉加载
onReachBottom(){
    console.log('加载下一页');
}

(3).不能使用scroll-view组件,需要使用普通view
(4).返回顶部,使用tt.pageScrollTo()

  1. 小程序分享
    (1). 后台配置 - (设置-基础设置-分享设置),配置图文之后提审,等待审核
    (2). js页面内配置 onShareAppMessage 事件处理函数,使用上面分享信息
Page({
  ...
  onShareAppMessage (option) {
    // option.from === 'button'
    return {
      title: '这是要转发的小程序标题',
      desc: '这是默认的转发文案,用户可以直接发送,也可以在发布器内修改',
      path: '/pages/index/index?from=sharebuttonabc&otherkey=othervalue', // ?后面的参数会在转发页面打开时传入onLoad方法
      imageUrl: 'https://e.com/e.png', // 支持本地或远程图片,默认是小程序 icon
      templateId: '这是开发者后台设置的分享素材模板id',
      success () {
        console.log('转发发布器已调起,并不意味着用户转发成功,微头条不提供这个时机的回调');
      },
      fail () {
        console.log('转发发布器调起失败');
      }
    }
  },
  onLoad (query) {
    if (query.from === 'sharebuttonabc') {
      // do something...
    }
  }
});

44-2.小程序 通过自定义按钮分享
(1). 按钮的分享 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/open-interface/share/retweet
(2). <button open-type="share">转发到微头条</button>

44-3. 如果判断是用户点击的详情页,还是通过分享过来的详情页
通过app.js内的launch,或者小程序内的onload 来获取参数

注:小程序调试期间,怎么在launch中测试传递过来的参数?
官方说明 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/other/param-use/
开发者工具内,就可以配置初始参数

小程序配置自定义参数

44-3. 分享内容定义的优先级
今日头条/头条极速版:

| 场景 | 优先级 |
| 端内分享 | 代码指定 > 模板指定 > 平台默认 |
| 端外分享 | 模板指定 > 平台默认 |

在脚本中还是设置标题以及描述为空,这样在开发者后台通过切换激活的分享模板,可以修改模板内容
但是修改新的分享模板,仍需要在代码中配置,怎么都会影响修改分享的效率

  1. 关于登录授权流程
    先使用tt.login获取临时登录code凭证,在把code、appid、secret发送后台,获取openid(openid 是用户的唯一标识)
    后台流程地址 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/server/log-in/code-2-session/
    获取openid
  1. 企业级账号中 webview配置
    首先,开发设置 - webview域名 - 下载检验文件
    其次,将授权文件放置在需要配置域名的根路径下,确保可以通过域名+检验文件,可以访问文件内容即可

  2. 同时一次同时设置多个数据,如果涉及缓动动画与表单焦点聚焦,可能会引起渲染问题,
    解决方式,先设置有动画的数据,再添加一个演示定时器,设置获取焦点的数据

  3. 浮层上有表单,如果设置点击空白区域使浮层消失,那么点击表单输入(bindinput),会引起冒泡行为,造成浮层关闭。
    解决方法:
    第一步,通过设置表单的焦点数据(focus),以及丢失焦点事件(bindfocus),在点击浮层之外的空白区域时候,判断输入框是否存在焦点状态(focus数据情况),如果存在就不要做浮层消失,否则就叫浮层消失
    第二步,表单输入框失去焦点事件中(bindfocus),将表单的焦点属性数据(focus)重置,使浮层可以被正常关闭
    解决方法2:百度一下,未解决
    给表单元素添加catchtap事件,事件处理函数为空,但是效果是,表单无法输入文字了,????

  4. scroll-view 如果希望其内部横向无限滚动,只需要设置两个样式
    其一,scroll-view容器设置 width:100%;
    其二,scroll-view内部包裹一个滚动元素,为其设置 display:inline-block; white-space:nowrap;

  5. 页面滚动,导航的吸顶效果
    准备:
    第一,判断方法存在与否 if(tt.createSelectorQuery){ ... }
    说明:
    一般新增的 api 在低版本基础库上是不存在的,贸然调用会导致错误。建议做如下判断:

if (tt.navigateToMiniProgram) {
  tt.navigateToMiniProgram();
} else {
  tt.showModal({
    title: "提示",
    content:
      "当前客户端版本过低,无法使用该功能,请升级客户端或关闭后重启更新。",
  });
}

第二,在一个web-view中绑定滚动事件 bindscroll="scrollFn"
第三,在滚动事件中,调用选择元素的方法,判断其距离顶部距离
然后改变数据,渲染导航fixed类名存在与否

if(domQuery){
  domQuery.select('#filterhead').boundingClientRect(rect=>{
    // console.log(rect.top);
    if(rect.top<=0){
      if(!_isFilterFixed){
        _that.setData({
          "isFilterFixed":true
        });
      }
    }else{
      if(_isFilterFixed){
        _that.setData({
          "isFilterFixed":false
        });
      }
    }
  }).exec();
}

第四,注意尽量节流,添加判断条件,除非必要,否则不要setData吸顶与否的类名
否则,由于滚动事件触发频率极快,因此即使设置的是相同的数据,也会造成导航在状态之间切换,因为setData也是需要消耗时间的,且这个时间可能要大于触发事件的间隔频率

  1. 组件配置
    第一,先配置【子组件】信息,新建对应的ttml、ttss、js、json
    组件json需要配置
{
  "component": true
}

组件的js内要设置组件信息

Component({
  properties: {
    //通过父组件属性,传递至子组件内的数据
    diyattr: {
      type: String,
      value: "默认标题文案",
    }
  },
  data:{
    // 组件内部数据
    defaultStates: { ...  }
  },
  methods:{
    // 自定义方法,可以将数据100通过自定义事件'myevent',传递给父组件
    fn1(){
      this.triggerEvent(
        'myevent', '传递的数据', ‘配置的数据(bubbles、composed、capturePhase)’
      )
    }
  }
});

链接:配置事件选项 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/framework/custom-component/correspondence-and-event-among-components

第二,配置【父组件】信息,新建对应的json
配置组件简称与路径

{
  "usingComponents": {
    "my-component": "/components/test/index"
  }
}

在ttml内使用组件

<my-component 
    diyattr="传递给子组件内的数据~"
    bindmyevent="子组件自定义事件名myevent"
    class="comp {{isShow?'compShow':''}}"
>
    <view>通过slot传递到子组件内的数据</view>
</my-component>

如果配置了json,如果不写东西,会报错
所以得写点什么

{  "component": false }
{  "usingComponents": {} }

在自定义事件bindmyevent的事件处理函数中,通过event的detail获取传递的数据

51-1. 自定义组件无法使用全局样式,则在组件js中配置

options: {
    addGlobalClass: true
}

即可

51-2. 组件无法使用组件外样式,在组件内样式,只能使用当前组件上的类名
有些样式不允许在组件内使用
Some selectors are not allowed in component ttss, including ID selectors, and attribute selectors

  1. 作用域 js
    第一、导出js,声明 文件夹 /resources/js/md5.js
...xxxx....
module.exports.hex_md5 = hex_md5;

第二、引入js,在/app.js中

let mdjs = require("/resources/js/md5.js");
App({
  md5Fn(str){
    return mdjs.hex_md5(str);
  }
})

第三、外部页面使用 js中

app.md5Fn('xxxx')

css作用域:将外部css引入app.ttss

/** app.ttss **/
@import "common.ttss";
  1. 两个非TAB页面之间跳转,数据状态不同步
    如:
    详细页通过路由(tt.navigateBack)返回列表页,在列表页触发了onShow钩子函数,但是数据不会重新加载
    这样会引起一个问题:
    如果详细页点击取消关注,返回列表页,不会重新渲染,因为数据有缓存,列表页不会重新渲染
    解决方法:
    将关注列表内容放到localStorge内,如果有操作关注的东西,及时改变localStorage,来作为变化参照凭证

  2. 二级联动pinker组件,TAB页面之间跳转,会出现问题
    表现: 是联动数据一级内容,为之前某二级列表内容,而非标题
    解决: 页面,onshow生命周期 重置联动pinker初始索引

  3. 转发与分享
    第一步,开发者后台配置小程序分享,过审后,获得分享ID
    第二步,添加标签
    <button open-type="share">转发到微头条</button>
    第三部,配置分享触发生命周期函数 官方示例 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/open-interface/share/retweet -- 配置参数,其中包括上面获得的分享ID参数

  4. 支付接入
    先获取订单orderid、再获取orderInfo(后台返回),最后调用tt.pay
    其中orderInfo里的 risk_info是字符串格式,里面是json,注意符号 ' 与 " 的使用。

risk_info: "{'ip':'xxx.xxx.xxx.xxx'}"

支付宝官方工具 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/open-interface/payment/mini-app-pay-plugin-reference/faq/
通过orderInfo内的alipay_url,可以查询支付宝支付,能否拉起。

  1. 浮层上如果有一个textarea, 如果通过修改class类名方式,控制浮层的显示与消失,那么textarea会不显示文字,且无法获得焦点,拉起手机键盘。
    解决方法,通过tt:if 来控制显示层的显示与隐藏,那么textarea就可以操作了

  2. 在小程序中,使用第三方客服拆件,用于聊天功能
    方法一,其核心是调用小程序官方组件<button type='default' open-type='contact'> 打开客服 </button>
    芝麻小客服 接入引导视频 - https://ossxkf.oss-cn-beijing.aliyuncs.com/douyinjieru.mp4
    芝麻小客服 官方链接地址 - https://www.kancloud.cn/wikizhima/wikixkf/1835308
    客服按钮配置 - https://www.kancloud.cn/wikizhima/wikixkf/1010958

方法二,其核心是使用webview
芝麻小客服 接入引导视频 - https://ossxkf.oss-cn-beijing.aliyuncs.com/20.mp4
小程序需要企业账号(需上传校验文件至服务器根目录,完成校验),且三方平台必须是付费用户才可以设置自定义域名(付费用户包括标准版、专业版、企业版等付费版本)
配置自定义域名--官方连接 - https://xiaokefu.com.cn/yun/yunAdmin/yunDomain?parent_nav_label=yundomain&nav_label=yundomain&wechatapp_id=201539&channel_id=10740
自定义域名也可以传递参数 - https://www.kancloud.cn/wikizhima/wikixkf/1109086

  1. 微信小程序迁移字节跳动小程序
    提前申请一个字节小程序,获取appid,开发者工具迁移按钮,依照要求输入微信小程序项目地址、字节小程序输出地址、appid、小程序名即可进行迁移
    后续工作:
    手动进行的语法转化
1. tt:for-items 修改为  tt:for ; tt:key修改为tt:for-index="idx";
2. navigator跳转,在pages下的页面需要在app.json中注册,但是微信小程序好像在pages下二级文件夹文件无需配置,这个在字节跳动小程序不行
3. 组件不一样,如pinker
  1. 小程序 时间对象 开发工具正常,但是iphone真机报错
    new Date("2019-07-24 19:57") 应该写成 new Date("2019/07/24 19:57")
    需要对时间字符串处理下,如 new Date(tm.replace(/-/g,'/'))

  2. 图片预览的轮播API 地址 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/media/picture/tt-preview-image
    预览一组图片,可以理解为一个封装好的 图片pop banner

62.通过抖音挂载能力,可以抖音发布小视频时候,挂载小程序。
这一功能需要提前申请


抖音挂载能力

代码内调用 ( 仅针对当前页面有效,属于二次修改分享内容,默认分享是开发者后台首次申请的配置文案 )

onShareAppMessage (option) {
    // option.from === 'button'
    return {
      title: app.globalData.shareMes.tit,
      desc: app.globalData.shareMes.txt,
      path: app.globalData.shareMes.path, // ?后面的参数会在转发页面打开时传入onLoad方法
      imageUrl: '', // 支持本地或远程图片,默认是小程序 icon
      templateId: app.globalData.shareMes.templateId,
      success(){
        //console.log('转发发布器已调起,并不意味着用户转发成功,微头条不提供这个时机的回调');
      },
      fail(){
        console.log('转发发布器调起失败');
      }
    }
  }
  1. 图片安全性验证
    上传图片时候,需要在tt.uploadFile时候操作
    【注】:
  • 今日头条 客户端,上传图片至服务器后,将服务器图片发送至头条进行验证,需要压缩体积至1.4MB以下,否则报错 error3 图片下载失败
  • 需要将头条域名https://developer.toutiao.com添加至白名单
    流程图

    //-- app.js
globalData:{
  imgCheckURL:'https://developer.toutiao.com/api/apps/censor/image',
  accessToken: '',
  accessTokenExpireTime: ''
},
/*
* ajaxFn
* options : {
*   "url": xxx, "data": {}, "method": xxx, "dataType":xxx, "sucFn": xxx, "errFn": xxx
* }
*/
ajaxFn(options,isOutLink){
  var _that = this;
  tt.request({
    url: isOutLink ? options.url : (_that.globalData.baseURL + '/' + options.url),
    data: options.data || null,
    header: {
      "content-type": "application/json",
    },
    method: options.method || "GET",
    dataType: "json", //返回数据的类型,支持 json、string
    success(res){
      options.sucFn && options.sucFn(res);
    },
    fail(res){
      options.errFn && options.errFn(res);
    }
  });
},
/*token 是小程序级别 token,不要为每个用户单独分配一个 token,会导致 token 校验失败。建议每小时更新一次即可
  * 检查access token是否过期,过期则重新获取token,否则返回当前token
  * cbFn 里面参数返回access token
  */
  checkTokenExpireFn(cbFn){
    //时间戳 统一换算秒
    let _that = this;
    let nowTimeStamp = new Date().getTime()/1000;
    let oldTimeStamp = _that.globalData.accessTokenExpireTime;
    //一小时一换的话 3600 秒
    if(oldTimeStamp!=''){
      //检查过期与否
      if(nowTimeStamp - oldTimeStamp>=3600){
        //过期了,重新获取token
        _that.getAccessTokenFn(function(res){
          //设置token,以及过期时间戳
          if(res.statusCode==200){
            _that.globalData.accessToken = res.data.access_token;
            _that.globalData.accessTokenExpireTime = nowTimeStamp;
            //回调
            cbFn && cbFn(res.data.access_token);
          };
        },function(err){
          _that.showPop('获取token失败',1000,'fail');
          console.log(err);
          //回调
          cbFn && cbFn('');
        });
      }else{
        //没过期
        cbFn && cbFn(_that.globalData.accessToken);
      } 
    }else{
      //初次使用,直接去获取token,等同过期
      //过期了,重新获取token
      _that.getAccessTokenFn(function(res){
        //设置token,以及过期时间戳
        if(res.statusCode==200){
          _that.globalData.accessToken = res.data.access_token;
          _that.globalData.accessTokenExpireTime = nowTimeStamp;
          //回调
          cbFn && cbFn(res.data.access_token);
        };
      },function(err){
        _that.showPop('获取token失败',1000,'fail');
        console.log(err);
        //回调
        cbFn && cbFn('');
      });
    }
  },
  /*获取access token*/
  getAccessTokenFn(_sucFn,_errFn){
    var _that = this;
    _that.ajaxFn({
      "url": 'https://developer.toutiao.com/api/apps/token?appid=xxxxxx&secret=xxxxxx&grant_type=client_credential',
      "data": null,
      "method":"GET",
      "dataType":"json",
      "sucFn":function(res){
        _sucFn && _sucFn(res);
      },
      "errFn":function(err){
        _errFn && _errFn(err);
      }
    },true);
  },
  /*检测图片安全性*/
  checkImgSafeFn(_accessToken,_imgSrc,_sucFn,_errFn){
    var _that = this;
    _that.ajaxFn({
      "url": _that.globalData.imgCheckURL,
      "data": {
        'app_id': 'xxxxxxxxxxxxxx',
        'access_token': _accessToken,
        'image': _imgSrc
      },
      "method":"POST",
      "dataType":"json",
      "sucFn":function(res){
        _sucFn && _sucFn(res);
      },
      "errFn":function(err){
        _errFn && _errFn(err);
      }
    },true);
  },
  /*图片安全验证-报错相关*/
  checkImgSafeErrFn(sensitiveArr){
    let errObj = {
      'porn':'图片涉黄',
      'cartoon_leader': '领导人漫画',
      'anniversary_flag': '特殊标志',
      'sensitive_flag': '敏感旗帜',
      'sensitive_text': '敏感文字',
      'leader_recognition': '敏感人物',
      'bloody': '图片血腥',
      'fandongtaibiao': '未准入台标',
      'plant_ppx': '图片涉毒',
      'high_risk_social_event': '社会事件',
      'high_risk_boom': '爆炸',
      'high_risk_money': '人民币',
      'high_risk_terrorist_uniform': '极端服饰',
      'high_risk_sensitive_map': '敏感地图',
      'great_hall': '大会堂',
      'cartoon_porn': '色情动漫',
      'party_founding_memorial':'建党纪念'
    };
    let errMes = [];
    if(sensitiveArr.length>0){
      sensitiveArr.forEach((item,idx)=>{
        if(item.hit){
          if(typeof errObj[item.model_name] != 'undefined'){
            errMes.push(errObj[item.model_name]);
          }
        }
      });
    }
    return errMes;
  },

使用:

//检测图片是否包含违法违规内容
app.checkTokenExpireFn(function(_token){
  if(typeof _token != 'undefined'){
    //有token,可以验证图片
    app.checkImgSafeFn(_token, 在线图片src ,function(res){
      //验证图片安全性
      if(res.statusCode==200){
        if(res.data.error==0){
          //图片审查调用成功
          var errMes = app.checkImgSafeErrFn(res.data.predicts);
          if(errMes.length>0){
            //图片存在违规信息
            app.showPop('违规:'+errMes.join('、'),1500,'fail');
          }else{
            //图片健康=>通过
            app.showPop("上传成功",600);
          }
        }else{
          //图片审查调用失败
          app.showPop('图片验证接口错误',1500,'fail');
        }

        ...
      };  
    },function(err){
      console.log(err);
    })
  }else{
    //无token,无法验证图片
    app.showPop('token获取失败',1500,'fail');
    ....
  }
});
  1. pinker 市区县
    //--ttml
<picker
  mode="multiSelector"
  bindchange="bindMultiPickerChange"
  bindcolumnchange="bindMultiPickerColumnChange"
  value="{{multiIndex2}}"
  range="{{multiArray2}}"
  data-pinkergroupnum="2"
>
<view class="flexBox flexYC">
  <view class="pickerBox">
    <view class="picker">{{multiArray2[0][multiIndex2[0]]}}</view>
    <view class="pickerBoxIco bgCover btmCorIco"></view>
  </view>
  <view class="pickerBox">
    <view class="picker">{{multiArray2[1][multiIndex2[1]]}}</view>
    <view class="pickerBoxIco bgCover btmCorIco"></view>
  </view>
  <view class="pickerBox">
    <view class="picker">{{multiArray2[2][multiIndex2[2]]}}</view>
    <view class="pickerBoxIco bgCover btmCorIco"></view>
  </view>
</view>
</picker>

//-- 用于pinker展示的文案的数据multiArray 格式

 //-- 省
[     
    "北京", "河北", ...
],
//-- 市
[     
    [  "北京" ],
    [  "石家庄",  "保定", ... ],
],
//-- 区
[
    [
      [ "东城区", "西城区",... ],
    ],
    [
      [ "长安区", "桥东区",... ],
      [ "新市区", "涿州区",... ]
    ],
],

//--js (多个三级联动pinker),通过pinker标签dataset来判断修改值

//pinker       -- 城市pinker列表
let cityLists = []; // 用于存储pinker的展示文案的结构

Page({
data: {
    'multiData': [],  //原始数据
    'multiArray1': [ ],  //用于pinker展示的文案
    'multiIndex1': [0, 0],  //用于切换pinker展示的文案索引
    'multiArray2': [ ],  //用于pinker展示的文案
    'multiIndex2': [0, 0, 0], //用于切换pinker展示的文案索引
},
....
//地区pinker多选- 确定
bindMultiPickerChange(event) {
    var _that = this;
    if(typeof event.currentTarget.dataset.pinkergroupnum != 'undefined'){
      var _pinkergroupnum = event.currentTarget.dataset.pinkergroupnum;
      //获取区域表索引
      if(_pinkergroupnum == '1'){
        _that.setData({
          "multiIndex1": event.detail.value,
        });
      }else if(_pinkergroupnum == '2'){
        _that.setData({
          "multiIndex2": event.detail.value,
        });
      };
    };
},
//地区pinker多选- 切换
bindMultiPickerColumnChange(event) {
    if(typeof event.currentTarget.dataset.pinkergroupnum != 'undefined'){
      var _pinkergroupnum = event.currentTarget.dataset.pinkergroupnum;
      var _multiArray = [];
      var _multiIndex = [];

      //获取区域表索引
      if(_pinkergroupnum == '1'){
        _multiArray = this.data.multiArray1;
        _multiIndex = this.data.multiIndex1;
      
        switch(event.detail.column){
          case 0:
            _multiIndex[0] = event.detail.value;
            _multiIndex[1] = 0;
            _multiArray[1] = cityLists[1][_multiIndex[0]];
            break;
          case 1:
            _multiIndex[1] = event.detail.value;
            break; 
        };

        //设置修改
        this.setData({
          'multiArray1': _multiArray,
          'multiIndex1': _multiIndex
        });
      }else if(_pinkergroupnum == '2'){
        _multiArray = this.data.multiArray2;
        _multiIndex = this.data.multiIndex2;

        switch(event.detail.column){
          case 0:
            _multiIndex[0] = event.detail.value;
            _multiIndex[1] = 0;
            _multiArray[1] = cityLists[1][_multiIndex[0]];
            _multiIndex[2] = 0;
            _multiArray[2] = cityLists[2][_multiIndex[0]][_multiIndex[1]];
            break;
          case 1:
            _multiIndex[1] = event.detail.value;
            _multiIndex[2] = 0;
            _multiArray[2] = cityLists[2][_multiIndex[0]][_multiIndex[1]];
            break;
          case 2:
            _multiIndex[2] = event.detail.value;
            break;  
        };

        //设置修改
        this.setData({
          'multiArray2': _multiArray,
          'multiIndex2': _multiIndex
        });
      };
    };
},

64-2. 根据地理位置修改pinker的展示
tt.getLocation获取经纬度,然后调用后台接口,返回省市区的ID,根据三个ID,从原始JSON获取索引位置,然后修改pinker的索引位置
在这一步需要注意的是:

  • 省市区不仅要修改索引,而且要修改pinker对应的展示列表(不是原始JSON)
//省市区对应id列表,索引位置
var _indexArr = [-1,-1,-1]; 

...
过滤原始数据,找到省市区对应ID的索引,并依次赋值到_indexArr 
...

//-- cityLists为原始JSON数据
if(_indexArr[0]!=-1){
  //-- 省
  _multiIndex2[0] = _indexArr[0];
  _multiArray2[0] = cityLists[0];
  if(_indexArr[1]!=-1){
    //-- 市
    _multiIndex2[1] = _indexArr[1];
    _multiArray2[1] = cityLists[1][_indexArr[0]];
    if(_indexArr[2]!=-1){
      //-- 区
      _multiIndex2[2] = _indexArr[2];
      _multiArray2[2] = cityLists[2][_indexArr[0]][_indexArr[1]];
    }
  }
};

省市区数组格式 见上面 (用于pinker展示的文案的数据multiArray 格式)

  • 获取地理位置,授权失败时候,需要注意给三级联动pinker设置初始索引,否则授权失败报错
//--获取用户坐标
tt.getLocation({
  success(res) {
    _that.setData({
      'userlongitude': res.longitude,
      'userlatitude': res.latitude
    });
  },
  fail(res) {
    //显示手动授权信息
    _that.setData({
      'multiIndex2': [0, 0, 0]
    });
    console.log(`getLocation调用失败~`);
  },
});
  1. 自定义导航
  • 配置 spp.json - window
"window":{
    "navigationStyle":"custom"
},
  • 配置 app.js
onLaunch:function(){
    //版本信息  
    let version = tt.getSystemInfoSync().SDKVersion;
    let _that = this;  

//获取状态栏高度
    let _SystemInfo = tt.getSystemInfoSync();
    if(_SystemInfo.statusBarHeight){
      _that.globalData.sysBarHeight = _SystemInfo.statusBarHeight;
    };

    //获取胶囊按钮的空间信息
    //基础库 1.25.0 开始支持本方法。
    if(_that.compareVersion(version,'1.25.0')){
      var _statusBarHeight = _that.globalData.sysBarHeight;
      var ButtonBoundingRes = tt.getMenuButtonBoundingClientRect();
      if(ButtonBoundingRes){
        _that.globalData.ButtonBoundingHright = (Number(ButtonBoundingRes.top) - _statusBarHeight)*2 + Number(ButtonBoundingRes.height);
      };
    }else{
      tt.showModal({
        title: '提示',
        content: '当前版本过低,无法获取胶囊按钮尺寸功能,请升级到最新版本后重试。'
      });
    };
},
globalData:{
    'sysBarHeight':'',
    'ButtonBoundingHright':'',
},
//对比版本号,返回布尔值,做兼容使用
compareVersion(v1, v2){
  v1 = v1.split('.')
  v2 = v2.split('.')
  const len = Math.max(v1.length, v2.length)
  while(v1.length < len){
    v1.push('0')
  }
  while(v2.length < len){
    v2.push('0')
  }
  for(let i = 0; i < len; i++){
    const num1 = parseInt(v1[i])
    const num2 = parseInt(v2[i])
    if (num1 > num2){
      return true;
    }else if(num1 < num2){
      return false;
    }
  }
},
  • 引入 ttml 组件
<mypagehead
        showLeftBtn="{{true}}"
        headTit="发布爆料-编辑"
></mypagehead>
  • 组件 ttml
<!-- 导航 -->
<view 
  class="topHead" 
  style="padding-top:{{sysBarHeightNum}}px"
>
  <view 
    class="headInset flexBox flexXC flexYC"
    style="height:{{ButtonBoundingHright}}px"
  >   
    <!-- 后退按钮 -->
    <view 
      tt:if="{{showLeftBtn}}"
      class="iconMenu"
    >   
        <!-- 后退 -->
        <view 
          bindtap="backFn"
          class="iconTag bgCover iconBackWhite"
        ></view>
        <!-- 返回主页 -->
    </view>
    <!-- 标题 -->
    <view class="tit">
        <text>{{headTit}}</text>
    </view>
  </view>
</view>
  • 组件 js
const app = getApp()

Component({
  properties:{
    showLeftBtn:{
      type: Boolean,
      value: false
    },
    headTit:{
      type: String,
      value: '缺省值标题'
    }
  },
  data:{
    // component internal data
    'defaultStates': { },
    'sysBarHeightNum': 0,
    'ButtonBoundingHright': 0
  },
  methods:{
    backFn(){
      app.jumpBack(1);
    }
  },
  lifetimes:{
    //组件生命周期函数,在组件实例进入页面节点树时执行
    attached(){
      //获取系统状态栏高度
      this.setData({
        'sysBarHeightNum': app.globalData.sysBarHeight,
        'ButtonBoundingHright': app.globalData.ButtonBoundingHright
      });
    }
  }
});
  1. 关于视频的上传
    调用文件上传接口, 进度条100%后,后台要立即返回数据,否则会报错 '挂起'
    字节系不同平台,不用手机 上传的视频格式是不一样的 官方兼容表 - https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/component/media-component/video/
    因此后续可能需要考虑 (不同手机上传视频的兼容问题,是否不用系统手机上传的视频,大家无法相互看到)
    //--选择视频、报错相关
tt.chooseVideo({
  sourceType: ["album", "camera"],
  compressed: true,
  maxDuration: 180,
  success: (res) => {
    let { duration, width, height, size } = res;
    _that.setData({
      ['upVideo.videoSrc']: res.tempFilePath,
      ['upVideo.description']: `视频时长:${duration}s; 视频大小:${Math.floor(size/(1024*1024))}MB`,
      ['upVideo.videoDuration']: Math.ceil(duration)
    });
    //错误信息-大于180s
    if(duration>180){
      _that.setData({
        ['formValidation.videoErr']: '视频时长超过3分钟'
      })
      return false;
    }
    //单位字节 B -> KB -> MB
    if(size/(1024*1024)>180){
      _that.setData({
        ['formValidation.videoErr']: '视频时长超过180MB'
      })
      return false;
    }
    //没有问题上传视频
    _that.uploadVideoFn();
  },
  fail: (err) => {
    app.showPop('选择视频失败',600,'none');
    _that.setData({
      ['formValidation.videoErr']: '上传视频不能为空'
    });
    console.log(err);
  },
  complete: (res) => {
    console.log("完成选择");
  },
});
  1. 上传视频文件引起报错,返回statusCode: 404
    chooseVideo的配置参数,如果使用压缩compressed: true,
    IOS系统,可以被压缩,但是设置为false, 则不会被压缩,(与官方说的,无论设置什么都压缩,不太一样 iphone7 plus)
    Android系统,不管怎么设置都不会被压缩

后台IIS系统,.net有个默认的配置限制最大28.6M,会造成 前端uploadFile直接报错 statusCode 404,上传进度为0
前端报错,后端还不能捕获到错误 官方提问的帖子 - https://forum.microapp.bytedance.com/mini-app/posts/608695821e04b37e5eaf9f2d

  1. scroll-view横向布局时候,通过scroll-view包裹一个类名为navInset的view容器
    在一般设备上没有问题,但是在iphone 12 Pro Max上,会出现折行问题
  • 问题的布局方式:
.navInset {  display:inline-block;  white-space:nowrap; }
.navInset .navItem { display:inline-block; padding:0 20rpx; margin-right:4rpx; white-space:nowrap;}  
  • 解决的布局方式:
.navInset {  display:-webkit-flex; display:-moz-flex;  display:-ms-flexbox; display:-webkit-box;  display:-moz-box; display:flex; }
.navInset .navItem { display:inline-block; padding:0 20rpx; margin-right:4rpx; white-space:nowrap;}  
  1. 小程序评级标准有官方认定,评级的频率为
    平台会在存在提审版本后的 7 天内再次安排评级,达到对应级别即可解锁各特殊能力的申请渠道

  2. 关于审核被拒,奖励金额相关
    由于现金奖励被拒绝,可以通过话费奖励的方式,来解决这个审核问题,但是值得注意的是,单次话费奖励金额不得超过50元
    官方审核说明 - https://microapp.bytedance.com/docs/zh-CN/mini-app/operation/Industrynorms/motivate/

......占位,填坑........

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容