uniapp注意点

777 阅读3分钟

HbuilderX 创建项目

官网-项目类型说明

Uni-App 和 H5+App 区别:

image.png

那么开发的时候,是选 Uni-App 还是 H5+APP 呢?

  1. 如果是想开发小程序的,顺便可以生成android 和 IOS 应用的,可以选择uni-app模式,这时开发小程序转成的APP。
  2. H5+APP混合开发模式, 传统HTML+DIV+CSS 布局,如果你只是想开发APP,  建议使用H5+APP开发模式,网页加原生APP的模式,后期扩展第三方SDK也更方便。

运行到小程序开发工具

  1. 设置小程序开发工具

uniapp-wx-anquan.png

  1. hbuder配置

uniapp-run-peizhi.png

运行到浏览器

  1. 需要先点击(最外层的)项目文件夹, 再选择运行到浏览器
  2. 修改运行配置 添加浏览器安装路径

liulanqi.png

UI组件 (vant-weapp)

vant-weapp 下载, 创建文件将dist包改为vant

vant-weapp-mulu

引入样式

vabt-weapp-css.png

全局引入

vant-peizhi

修改page.json

"globalStyle": {
		"navigationBarTextStyle": "white",
		"navigationBarTitleText": "uni-app",
		"navigationBarBackgroundColor": "#5c8cbc",
		"backgroundColor": "#F8F8F8",
		"usingComponents": {
			"van-action-sheet": "/wxcomponents/vant/action-sheet/index",
			"van-area": "/wxcomponents/vant/area/index",
//			"van-badge": "/wxcomponents/vant/badge/index",
//			"van-badge-group": "/wxcomponents/vant/badge-group/index",
			"van-button": "/wxcomponents/vant/button/index",
			"van-card": "/wxcomponents/vant/card/index",
			"van-cell": "/wxcomponents/vant/cell/index",
			"van-cell-group": "/wxcomponents/vant/cell-group/index",
			"van-checkbox": "/wxcomponents/vant/checkbox/index",
			"van-checkbox-group": "/wxcomponents/vant/checkbox-group/index",
			"van-col": "/wxcomponents/vant/col/index",
			"van-dialog": "/wxcomponents/vant/dialog/index",
			"van-field": "/wxcomponents/vant/field/index",
			"van-goods-action": "/wxcomponents/vant/goods-action/index",
			"van-goods-action-icon": "/wxcomponents/vant/goods-action-icon/index",
			"van-goods-action-button": "/wxcomponents/vant/goods-action-button/index",
			"van-icon": "/wxcomponents/vant/icon/index",
			"van-loading": "/wxcomponents/vant/loading/index",
			"van-nav-bar": "/wxcomponents/vant/nav-bar/index",
			"van-notice-bar": "/wxcomponents/vant/notice-bar/index",
			"van-notify": "/wxcomponents/vant/notify/index",
			"van-panel": "/wxcomponents/vant/panel/index",
			"van-popup": "/wxcomponents/vant/popup/index",
			"van-overlay": "/wxcomponents/vant/overlay/index",
			"van-progress": "/wxcomponents/vant/progress/index",
			"van-radio": "/wxcomponents/vant/radio/index",
			"van-radio-group": "/wxcomponents/vant/radio-group/index",
			"van-row": "/wxcomponents/vant/row/index",
			"van-slider": "/wxcomponents/vant/slider/index",
			"van-stepper": "/wxcomponents/vant/stepper/index",
			"van-steps": "/wxcomponents/vant/steps/index",
			"van-submit-bar": "/wxcomponents/vant/submit-bar/index",
			"van-swipe-cell": "/wxcomponents/vant/swipe-cell/index",
			"van-switch": "/wxcomponents/vant/switch/index",
//			"van-switch-cell": "/wxcomponents/vant/switch-cell/index",
			"van-tab": "/wxcomponents/vant/tab/index",
			"van-tabs": "/wxcomponents/vant/tabs/index",
			"van-tabbar": "/wxcomponents/vant/tabbar/index",
			"van-tabbar-item": "/wxcomponents/vant/tabbar-item/index",
			"van-tag": "/wxcomponents/vant/tag/index",
			"van-toast": "/wxcomponents/vant/toast/index",
			"van-transition": "/wxcomponents/vant/transition/index",
			"van-tree-select": "/wxcomponents/vant/tree-select/index",
			"van-datetime-picker": "/wxcomponents/vant/datetime-picker/index",
			"van-rate": "/wxcomponents/vant/rate/index",
			"van-collapse": "/wxcomponents/vant/collapse/index",
			"van-collapse-item": "/wxcomponents/vant/collapse-item/index",
			"van-picker": "/wxcomponents/vant/picker/index"
		}
	},

页面直接使用

<van-button type="info">vant</van-button>

vant 运行到h5报错

vant-error.png

解决

格式化 vant icon组件下的index.wxss

icon-wxss.png

van-uploader

accept : media 同时上传照片和视频(可拍摄) h5不兼容 (底层实现的是chooseMedia)

重写 cus-upload-media 只针对照片和视频


<template>
  <view class="burst-wrap">
    <block v-if="filesList.length">
      <view v-for="(file,index) in filesList" :key="index" class="uni-uploader__file">
        <view class="file-item">
          <video
              v-if="file.type === 'video'"
              :src="file.url"
              :poster="file.thumbTempFilePath"
              class="full-box"
          />
          <image
              v-else-if="file.type === 'image'"
              :src="file.url"
              :data-src="file.tempFilePath"
              @tap="previewImage(file.url)"
              class="full-box"
          />
          <text
              v-if="showDelIcon"
              class="icon-close uni-icons uniui-closeempty"
              @click="delFile(index)"
          />
        </view>
      </view>
    </block>
    <view
        v-if="!disabled && filesList.length < limit"
        class="uni-uploader__input-box"
        :class="{'fix-width': isFixWidth }"
        @tap="startUpload"
    >
      <view class="upload-btn">
        <uni-icons type="plusempty" size="30" />
        <view class="text">添加图片/视频</view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  props: {
    value: {
      type: Array,
      default: () => []
    },
    disabled: {
      type: Boolean,
      default: false
    },
    delIcon: {
      type: Boolean,
      default: true
    },
    accept: {
      type: String,
      default: 'media'  // image media video
    },
    camera: {
      type: String,
      default: 'back' // 前后置摄像头 'front'、'back',默认'back'
    },
    compressed: {
      type: Boolean,
      default: true // 视频选择 是否压缩所选的视频源文件,默认值为 true,需要压缩
    },
    limit: {
      type: [String, Number],
      default: 6,
    },
    maxDuration: {
      type: Number,
      default: 30 // 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间
    },
    mediaType: {
      type: Array,
      default: () => ['image', 'video'] // 文件类型
    },
    sourceType: {
      type: Array,
      default: () => ['album', 'camera'] // 图片和视频选择的来源
    },
    sizeType: {
      type: Array,
      default: () => ['original', 'compressed'] // 仅对 mediaType 为 image 时有效 是否压缩所选文件
    },
    complete: {
      type: Function,
      default: () => {}
    }
  },
  data() {
    return {}
  },
  computed: {
    filesList: {
      get () {
        return [...this.value]
      },
      set (v) {
        this.$emit('input', v)
      }
    },
    isFixWidth() {
      return !!this.filesList.length
    },
    showDelIcon() {
      if (this.disabled) return false
      return this.delIcon
    }
  },
  methods: {
    delFile(i) {
      const file = this.filesList.splice(i, 1)
      this.$emit('delete', file[0])
      this.filesList = [...this.filesList]
    },
    async startUpload () {
      if (this.disabled) return
      const [err, res] = await this.chooseFile()
      console.log('res-->>>>', res, err)

      if (res) {
        res.tempFiles.forEach(item => {
          item.url = item.tempFilePath
          item.type = item.fileType
        })
        this.filesList.push(...res.tempFiles)
        this.$emit('select', res)
        // this.filesList = [...this.filesList]
      }
    },
    chooseFile() {
      const { accept } = this
      return new Promise( (resolve, reject) => {
        switch (accept) {
          case 'image':
            this.chooseImg(resolve)
            break;
          case 'media':
            this.chooseMedia(resolve)
            break;
          case 'video':
            this.chooseVideo(resolve)
            break;
          default:
            console.error('没有找到对应文件类型:' + accept)
            resolve([{ errMsg: '没有找到对应文件类型:' + accept }, null])
            break;
        }
      });
    },
    chooseMedia (resolve) {
      // #ifdef H5
      uni.showActionSheet({
        title:"选择上传类型",
        itemList: ['图片','视频'],
        success: (res) => {
          console.log(res)
          if(res.tapIndex === 0){
            this.chooseImg(resolve)
          } else {
            this.chooseVideo(resolve)
          }
        }
      })
      // #endif

      // #ifdef MP-WEIXIN
      const { sourceType, limit, sizeType, mediaType, maxDuration, camera, complete } = this
      uni.chooseMedia({
        count: limit * 1 - this.filesList.length,
        mediaType,
        sourceType,
        sizeType,
        maxDuration,
        camera,
        success: res => {
          return resolve([null, res])
        },
        fail: err => resolve([err, null]),
        complete
      });
      // #endif
    },
    chooseImg (resolve) {
      const { sourceType, limit, sizeType, complete } = this
      uni.chooseImage({
        count: limit * 1 - this.filesList.length,
        sourceType,
        sizeType,
        success: res => {
          const data = {
            errMsg: res.errMsg,
            tempFiles: res.tempFiles.map(item => ({
              size: item.size,
              tempFilePath: item.path,
              fileType: 'image'
            }))
          }
          resolve([null, data])
        },
        fail: err => resolve([err, null]),
        complete
      });
    },
    chooseVideo (resolve) {
      const { sourceType, compressed, maxDuration, camera, complete } = this
      uni.chooseVideo({
        sourceType,
        compressed,
        maxDuration,
        camera,
        success: res => {
          const { errMsg, tempFile, ...rest } = res
          let data = {
            errMsg,
            tempFiles: [{ ...rest, tempFilePath: res.tempFilePath, fileType: 'video' }]
          }
          // #ifdef H5
          if (tempFile.type.indexOf('video') === 0) {
            resolve([null, data])
          } else {
            resolve([{ errMsg: '选择文件格式不对:' + tempFile.type }, null])
          }
          return
          // #endif
          resolve([null, data])
        },
        fail: err => resolve([err, null]),
        complete
      });
    },
    previewImage(src) {
      //预览图片
      uni.previewImage({
        current: src,
        urls: [src]
      })
    },
  }
}
</script>

<style lang="scss" scoped>
.burst-wrap{
  padding: 0 20upx;
  display: flex;
  flex-wrap: wrap;
  margin: -5px;

  .uni-uploader__file {
    position: relative;
    padding-top: 33.33%;
    width: 33.33%;
    height: 0;
  }
  .icon-close {
    position: absolute;
    right: 0;
    top: 0;
    width: 24upx;
    height: 26upx;
    background-color: #313336;
    border-radius: 0 0 0 60%;
    font-size: 24upx;
    color: #fff;
    z-index: 1;
    &.uniui-closeempty:before {
      position: absolute;
      right: 3px;
      top: -3px;
      content: "x";
    }
  }
  .full-box {
    width: 100%;
    height: 100%;
  }
  .file-item {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: calc(100% - 10upx);
    height: calc(100% - 10upx);
  }

  .uni-uploader__input-box {
    //margin: 0 20upx;
    //padding-top: 45upx;
    //height: 200upx;
    position: relative;
    padding-top: 33.33%;
    width: 100%;
    height: 0;
    background-color: #f0f0f0;
    color: #969696;
    font-size: 28upx;
    text-align: center;
    /deep/ .uni-icons.uniui-plusempty {
      color: #969696 !important;
    }
    &.fix-width {
      width: 33.33%;
    }
    .upload-btn {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: calc(100% - 10upx);
    }
  }
}
</style>

使用

<cusUpload
    v-model="process.fileList"
    limit="9"
    :disabled="status == 'read'"
    :del-icon="showDelIcon(rootIndex)"
    @delete="mediaDelete"
    @select="afterRead"
/>

关于登录持久化

  1. App.vue中的 globalData 在h5中和vuex一样当页面刷新就会重置
  2. 基于第一点 将信息同时存到本地 (或者添加路由拦截跟vue项目一样处理:没有就重新获取)

h5无法获取完整的响应头

token.png

路由

h5中 页面刷新后 navigateBack 失效 处理 (自定义头部导航)

let canNavBack = getCurrentPages()
if( canNavBack && canNavBack.length>1) {
  uni.navigateBack()
} else {
  history && history.back();
}

路由拦截 针对vue-cli创建的uni项目 (使用插件)

{
  "name": "uniapp",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {},
  "dependencies": {
    "uni-read-pages": "^1.0.5",
    "uni-simple-router": "^2.0.2"
  }
}

easycom组件规范(免注册,直接使用)

  1. 只要组件安装在项目的components目录下或uni_modules目录下,并符合components/组件名称/组件名称.vue目录结构。就可以不用引用、注册,直接在页面中使用。
  2. easycom是自动开启的,不需要手动开启。

文件别名

无需配置 @ 默认是从根目录开始

import common from '@/components/tabbar.js';

修改第三方组件的样式

<uni-icons class="contact" type="contact" size="30"></uni-icons>

注意H5和小程序编译的结果是不一样的

uni-cions的源码是这样的:(只有一个text标签)

<text :style="{ color: color, 'font-size': iconSize }" class="uni-icons" @click="_onClick">{{unicode}}</text>

h5编译结果: (自定义类名和源码的类名是再同一级的元素中)

<uni-text data-v-a2e81f6e="" data-v-76f421a0="" class="uni-icons contact uniui-contact" style="color: rgb(51, 51, 51); font-size: 30px;"><span></span></uni-text>

小程序编译结果: (自定义类名是源码的类名的父级)

<uni-iconsclass="contact" style="color: rgb(51, 51, 51); font-size: 30px;"><text class="uni-icons uniui-contact"></text></uni-text>
  1. uni.scss 中全局修改 兼容h5和微信小程序
/* #ifdef H5 */
.contact.uni-icons.uniui-contact {
  color: red !important;
  background-color: lawngreen;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.contact /deep/ .uni-icons.uniui-contact {
  color: red !important;
  background-color: lawngreen;
}
/* #endif */
  1. 在page页面中跟vue一样使用 /deep/ 即可
<style lang="scss" scoped>
.contact {
  /deep/ .uni-icons.uniui-contact {
    color: red ;
    background-color: lawngreen;
  }
}
</style>
  1. 自定义组件中使用的第三方组件样式修改
  • h5中更vue项目一样处理即可

  • 微信小程序中只能在全局改

export default {
    // 在自定义组件中添加
    options: {
      styleIsolation: 'shared', // 解除样式隔离
    }
}

UPX 单位

uni-app 使用 upx 作为默认尺寸单位, upx 是相对于基准宽度的单位,可以根据屏幕宽度进行自适应。 uni-app 规定屏幕基准宽度750upx。

开发者可以通过设计稿基准宽度计算页面元素 upx 值,设计稿 1px 与框架样式 1upx 转换公式如下: 设计稿 1px / 设计稿基准宽度 = 框架样式 1upx / 750upx

举例说明:

若设计稿宽度为 640px,元素 A 在设计稿上的宽度为 100px,那么元素 A 在 uni-app 里面的宽度应该设为:750 * 100 / 640,结果为:117upx。 若设计稿宽度为 375px,元素 B 在设计稿上的宽度为 200px,那么元素 B 在 uni-app 里面的宽度应该设为:750 * 200 / 375,结果为:400upx

1.动态绑定的 style 不支持直接使用 upx。(h5可以, 微信小程序不行, 建议直接使用rpx)
<!-- - 静态upx赋值生效 -->
<view class="test" style="width:200upx"></view>
<!-- - 动态绑定不生效 -->
<view class="test" :style="{width:winWidth + 'upx;'}"></view>
2.使用 uni.upx2px(Number) 转换为 px 后再赋值。
<template>
    <view>
        <view  :style="{width: h_Width}">
        </view>
    </view>
</template>

<script>
    export default {
        computed: {
            h_Width() {
                return uni.upx2px(750 / 2) + 'px';
            }
        }
    }
</script>

官方推荐使用rpx替代upx

在设置文件mainfest.json里开启px转rpx(默认关闭),所有的px可一键转换为rpx

  • 注意 rpx 是和宽度相关的单位,屏幕越宽,该值实际像素越大。如不想根据屏幕宽度缩放,则应该使用 px 单位。
  • 如果开发者在字体或高度中也使用了 rpx ,那么需注意这样的写法意味着随着屏幕变宽,字体会变大、高度会变大。如果你需要固定高度,则应该使用 px 。
  • rpx不支持动态横竖屏切换计算,使用rpx建议锁定屏幕方向

在 Hbuilder 中自动将 px 转化成 rpx

参考文章 px2rpx

IOS safe-area-inset-*

参考1 参考2 苹果官方文档

padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom) / 2;  /* 兼容 iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom) / 2;  /* 兼容 iOS >= 11.2 */

input (层级很高 z-index也无法调整,需要使用cover-view)

  1. 聚焦问题 聚焦: focus: boolean 光标位置: cursor: number
  • typenumber、digit 无效
    • 想要光标回到最后 (使用v-if重新渲染input宽度不能为0) 场景:验证码 [0][0][0][0][0][0]

例子: 点击其他地方聚焦输入框且光标在最后(类似:label for)

<input v-if="show" :focus="isfocus" v-model="code" type="number" maxlength="6" placeholder="" />
// 隐藏软键盘 (ios bug:isfocus为true后不会弹出键盘)
if (uni.getSystemInfoSync().platform === 'ios') {
    uni.hideKeyboard();
}
// 先失焦 再聚焦 否则只有第一次能聚焦 聚焦后默认在第一位
this.isfocus = false;
this.show = false
await this.$nextTick()
this.show = true
await this.$nextTick()
this.isfocus = true;
  1. 键盘显示页面向上顶 input 必须是显示的 不能visibility:hidden、 display: none、 opacity: 0

开发环境和生产环境

运行环境管理

if (process.env.NODE_ENV === 'development') {
	console.log('开发环境');
} else {
	console.log('生产环境');
}