HbuilderX 创建项目
Uni-App 和 H5+App 区别:
那么开发的时候,是选 Uni-App 还是 H5+APP 呢?
- 如果是想开发小程序的,顺便可以生成android 和 IOS 应用的,可以选择
uni-app模式,这时开发小程序转成的APP。 H5+APP混合开发模式, 传统HTML+DIV+CSS 布局,如果你只是想开发APP, 建议使用H5+APP开发模式,网页加原生APP的模式,后期扩展第三方SDK也更方便。
运行到小程序开发工具
- 设置小程序开发工具
- hbuder配置
运行到浏览器
- 需要先点击(最外层的)项目文件夹, 再选择运行到浏览器
- 修改运行配置 添加浏览器安装路径
UI组件 (vant-weapp)
vant-weapp 下载, 创建文件将dist包改为vant
引入样式
全局引入
修改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 icon组件下的index.wxss
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"
/>
关于登录持久化
- App.vue中的
globalData在h5中和vuex一样当页面刷新就会重置 - 基于第一点 将信息同时存到本地 (或者添加路由拦截跟vue项目一样处理:没有就重新获取)
h5无法获取完整的响应头
路由
在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组件规范(免注册,直接使用)
- 只要组件安装在项目的components目录下或
uni_modules目录下,并符合components/组件名称/组件名称.vue目录结构。就可以不用引用、注册,直接在页面中使用。 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>
- 在
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 */
- 在page页面中跟vue一样使用 /deep/ 即可
<style lang="scss" scoped>
.contact {
/deep/ .uni-icons.uniui-contact {
color: red ;
background-color: lawngreen;
}
}
</style>
- 在自定义组件中使用的第三方组件样式修改
-
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
IOS safe-area-inset-*
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)
- 聚焦问题 聚焦: focus: boolean 光标位置: cursor: number
- 对
type为number、digit无效- 想要光标回到最后 (使用v-if重新渲染
且input宽度不能为0) 场景:验证码 [0][0][0][0][0][0]
- 想要光标回到最后 (使用v-if重新渲染
例子: 点击其他地方聚焦输入框且光标在最后(类似: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;
- 键盘显示页面向上顶 input 必须是显示的 不能visibility:hidden、 display: none、 opacity: 0
开发环境和生产环境
if (process.env.NODE_ENV === 'development') {
console.log('开发环境');
} else {
console.log('生产环境');
}