本篇文章是使用 uniapp 开发 h5 与微信小程序的一些踩坑总结与记录(至于 app 的跨端开发,那还是使用 flutter 吧)。
开发
使用 vue3
新建项目
在新建项目时勾选 vue3:
在首次运行时,HBuilder 会自动安装相关插件:
props 接收路由传参
使用 vue3 创建的项目,路由传参可以通过 props
接收,这功能在 Vue Router 的官方文档也有提及。
样式
全局样式
在 uni.scss 文件定义或引入的 sass 变量,全局可以直接使用而无需 @import
引入。在 App.vue 页面的 <style>
标签内可以定义全局样式:
vue
<style>
@import url('@/common/styles/base.css');
</style>
<style lang="scss">
/*每个页面公共css */
@import 'uview-ui/index.scss';
page {
/* 各个页面的根元素样式 */
}
</style>
- 引入外部 css 文件可以使用
@import url
或@import
, 它们的区别在于@import url
可以引入网络文件,而@import
只能引入本地文件; - 在 sass 中,只能使用
@import
引入外部 scss 文件,且后缀 .scss 可以省略。如果使用了@import url
或者@import
引入的是 css 文件,则不会导入 sass 文件而只是作为普通的 css 语句原封不动地照搬到编译后生成的 css 文件中; - 使用 sass 时,HBuilder 会自动安装相关插件,然后在
<style lang="scss">
内直接编写 sass 代码即可; - 在 uniapp 中路径别名
@
无需自己配置,可以直接使用; - 使用
page
定义样式相当于给每个页面的根元素定义了样式:
vue
<style lang="scss">
page {
font-size: 28rpx;
color: red;
}
</style>
在小程序端每个页面的根元素就是 <page>
:
在 h5 端会生成自定义元素 <uni-page-body>
:
关于 scoped
在 h5 端,写在页面或组件的样式,默认就是只在当前页面或组件生效,不需要写 scoped
;但是在微信小程序端则需要明确写上 scoped
才可以为 css 指定作用域。
适配刘海屏(异型屏)
微信小程序端,有些页面头部的内容在 iphone6 等非刘海屏显示正常,在 iphone13 等异型屏会有问题。uniapp 中通过 css 变量 --status-bar-height
来设置状态栏高度,在小程序里固定为 25px
, 这是会有问题的。可以通过 uni.getSystemInfo
获取真实的 statusBarHeight
,然后通过在每个页面文件的第一个节点设置 <page-meta :page-style="pageStyle"></page-meta>
:
在 pageStyle
中设置获取到的真实的状态栏高度等 css 变量:
javascript
this.pageStyle =`--qy-status-bar-height: ${qyStatusBarHeight}px;`
在使用时,为了让 h5 端也可以找到 --qy-status-bar-height
,别忘了在样式文件中进行设置:
css
:root {
--qy-status-bar-height: 0px;
// ...
}
屏幕底部的安全区域也可以通过 uni.getSystemInfo
返回的结果中的 safeAreaInsets.bottom
得到:
内置组件 input 的 placeholder 样式
通过 placeholder-class 属性设置 placeholder 的样式类时,为了适配小程序端,需要将样式类定义在不带 scoped
的 <style>
里。
自定义组件
在微信小程序端,自定义组件在渲染时会比 h5 端多一级节点,比如有个 <ComCascader>
组件:
在微信小程序中就会在外层多一层 <com-cascader>
:
如果遇到 h5 端和小程序端样式效果不一致的情况,可以看看是不是这个问题(可以通过 virtualHost 配置)。
动态绑定 style
微信小程序动态绑定 style
,值得是数组,而不能直接是对象格式。
display
微信小程序中,自定义组件的默认 display
为 inline
,所以在添加 margin
等属性时,要改为诸如 block
方可生效。
gap
在 iPhone 真机上,flex 布局中,使用 gap
属性可能无效。
::v-deep
微信小程序的 ::v-deep
要生效,需要在 methods
同级添加如下代码:
javascript
options: {
styleIsolation: 'shared'
},
路由
我使用的是 uni-simple-router 这个插件,其获取 router 对象是通过 this.$Router
,获取 route 是通过 this.$Route
(注意大小写)。
图片
微信小程序不支持本地的背景图片,而是支持 base64 格式的图片或网络图片。如果你使用了本地图片,当图片小于 40 kb 时,uniapp 在编译时会自动转成 base64:
本地背景图片的引用路径建议使用 ~@
开头的绝对路径:background: url('~@/static/step.png') no-repeat;
。
图标
我通常在 iconfont 上找到合适的图标然后通过 symbol 引用的方式在项目中使用。这种用法使用的是 svg,但是微信小程序上只支持网络地址的 svg。所以我采取的方案是在选择图标时进行区分。
单色图标
对于如下所示的单色图标:
添加到一个项目,统一使用字体图标的 font-class 的方式引用,并对应封装了 <QyIcon>
:
vue
<!-- 纯色图标 -->
<template>
<view class="qy-icon-box">
<i class="iconfont" :class="iconfontClass" :style="[iconStyle]"></i>
</view>
</template>
<script>
import iconMixin from '@/mixins/iconMixin.js'
export default {
name: 'QyIcon',
mixins: [iconMixin]
}
</script>
<style lang="scss">
.qy-icon-box {
display: inline-block;
}
</style>
iconMixin.js 文件如下,主要是对样式的处理:
javascript
export default {
props: {
// icon 的名称
flag: {
type: String,
required: true
},
classArr: {
type: Array,
required: false
},
iconStyle: {
type: Object,
required: false
}
},
computed: {
iconfontClass() {
if (this.classArr) {
return [...this.classArr, this.flag]
} else {
return [this.flag]
}
}
}
}
使用时,如果传递的类名是定义在当前 vue 文件的,则需要添加 ::v-deep
方可生效:
vue
<template>
<QyIcon
flag="icon-hongbao"
:classArr="['icon-common']"
:iconStyle="{ marginBottom: '4rpx' }"
/>
</template>
<style lang="scss">
/* 类名要生效,需要添加 ::v-deep */
::v-deep .icon-common {
color: #999;
fontsize: 140rpx;
}
</style>
在设置字体图标项目时,字体格式可以只勾选 WOFF 和 TTF:
微信小程序官方文档的建议也就是选择这两个:
多色图标
对于类似下图所示的多色图标:
则添加到另一个项目,在 h5 端使用 svg 的 symbol 方式引用,在小程序端则依然采用字体图标的方式,对应封装的是 <QySvgIcon>
:
vue
<!-- 多色图标 -->
<template>
<view class="qy-icon-box">
<!-- H5 端使用 svg -->
<!-- #ifdef H5 -->
<svg class="icon" :class="classArr" :style="iconStyle" aria-hidden="true">
<use :xlink:href="'#' + flag" />
</svg>
<!-- #endif -->
<!-- 小程序端则改为使用字体图标 -->
<!-- #ifdef MP-WEIXIN -->
<i class="iconfont" :class="iconfontClass" :style="[iconStyle]"></i>
<!-- #endif -->
</view>
</template>
<script>
import iconMixin from '@/mixins/iconMixin.js'
export default {
name: 'QySvgIcon',
mixins: [iconMixin]
}
</script>
<style lang="scss">
.qy-icon-box {
display: inline-block;
}
</style>
使用时,如果多色图标在变为单色后依然可以使用,则直接使用(注意,该图标也需要添加到单色图标那个项目中去):
html
<QySvgIcon
flag="icon-social-paypal"
:iconStyle="{ fontSize: '28px', color: '#172C70' }"
/>
如果多色图标改为单色后不可用,则该图标仅仅需要存在于多色图标项目,在微信小程序端另外挑选其它近似的纯色图标:
html
<!-- #ifdef H5 -->
<QySvgIcon
flag="icon-jinbi"
:classArr="['mr_20']"
:iconStyle="{ fontSize: '38rpx' }"
/>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<QySvgIcon
flag="icon-jinbi2"
:classArr="['c_yellow mr_20']"
:iconStyle="{ fontSize: '38rpx' }"
/>
<!-- #endif -->
其它说明
- 之所以要分成 2 个项目,是为了尽可能减小 main.js 引入的图标相关文件的大小,减少打包体积:
javascript
// #ifdef H5
import './static/iconfont/iconfont.js' // 来自多色图标项目
// #endif
import './static/iconfont/iconfont.css' // 来自单色图标项目
- 对于 iconfont.css 文件,记得要更改
@font-face
里的文件引用路径,改为以~@
开头的绝对路径(一般在 css 中都是使用~@
而在 js 中使用@
):
css
@font-face {
font-family: 'iconfont';
src: url('~@/static/iconfont/iconfont.woff') format('woff'),
url('~@/static/iconfont/iconfont.ttf') format('truetype');
}
- 字体图标的文件组织如下:
当下载到本地的字体文件小于 40kb 时,在小程序端 uniapp 会自动将其转为 base64 格式以兼容小程序。
不支持动态组件
在微信小程序上是不支持使用 <component>
然后通过 is
属性来渲染动态组件的,这一点在 uniapp 的官方文档有说明:
不支持自定义指令
在微信小程序端是不支持 vue 的自定义指令的。
不能在子组件直接修改 props 接收的父组件的值
这句话乍看上去好像是句废话------这不就是 vue 官方文档的规范吗?但我在做一个页面时,使用到了 uView 框架的 Checkbox 复选框,该组件需要通过 v-model
绑定一个类型为布尔值的变量:
而我赋值的 item.checked
的 item
是从父组件传入的数组 unpaidList
的成员:
这么写在 H5 端一切正常,但是在微信小程序端会发现点击复选框没有效果。其原因就是点击复选框时会在子组件改变 item.checked
的值,相当于改变了 props
接收的父组件的 unpaidList
,所以会导致点击失效。解决办法就是在 change 事件发生时,向父组件发射 checkboxChange
事件,然后由父组件更改对应 item
的 checked
的值:
javascript
checkboxChange(e) {
this.$emit('checkboxChange', e)
}
值为 null 或 undefined
当接口传递的值为 null
或 undefined
时,微信小程序会直接显示 null 或 undefined,所以可以使用 v-if
做下判断,或是 vue2 中的 filter 做下处理。
表单的验证
使用 uView 的表单组件时,如果某一添加了必填验证的表单项有默认值,该值若是数字类型的:submitForm: { quantity: 1 }
,要改为字符串类型:submitForm: { quantity: '1' }
,否者提交表单时会报错:
<u-input>
组件的 @input
使用 uView 的 <u-input>
组件,添加的 @input
事件,在微信小程序端,当删除输入框内容时,删除到最后清空的那一下操作不会触发 @input
事件,解决办法是可以配合 watch
来监听输入框值的变化。
<text>
组件中的空格
<text>
组件中,空格最好使用
表示,如果直接使用空格键敲出的空格:
在编译后可能导致换行:
小程序分享按钮
如果要让小程序页面点击后弹出的菜单中分享按钮可用:
需要在页面添加如下代码:
javascript
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
运行
运行时如果没有自动打开浏览器或微信开发者工具
可以在 HBuilder X 中打开"运行"菜单:
在运行设置里设置浏览器或微信开发者工具的路径:
另外,运行小程序还需要在微信开发者工具开启服务端窗口:"设置 - 安全设置" 以让 HBuilder 可以启动微信开发者工具:
手动运行小程序
如果自动打开微信开发者工具失败,也可以直接通过微信开发者工具打开打包后的位于项目根目录下的 unpackage\dist\dev\mp-weixin 来手动运行小程序(dev 目录下的是开发版,build 目录下的是生产版)。
公众号网页调试
在微信开发者工具中,通过"项目 - 更换开发模式"切换:
然后在新打开的窗口上方地址栏输入网页的地址即可:
打包
减小主包的方法
在打包上传微信小程序时,会要求主包大小不能大于 1.5M,下面介绍几点减小主包的方法。
分包
在 manifest.json 开启分包:
json
"mp-weixin": {
"optimization": {
"subPackages": true
}
},
pages 目录下的页面会被放入主包,分包可以在 pages.json 中设置 subPackages
,注意 root
的值不能是 pages
:
json
// #ifdef MP-WEIXIN
"subPackages": [{
"root": "subPages",
"pages": [{
"path": "contact-information/contact-information",
"style": {
"navigationBarTitleText": "联系方式",
"enablePullDownRefresh": false
}
}]
}],
// #endif
"pages": [
// #ifdef H5
{
"path": "subPages/contact-information/contact-information",
"style": {
"navigationBarTitleText": "联系方式",
"enablePullDownRefresh": false
}
},
// #endif
]
如此,subPages 目录下的页面就会被放入分包。
按需引入 lodash
为了减小打包后的体积,要使用 lodash 中的哪个方法就按需引入哪个方法,比如 import cloneDeep from 'lodash/cloneDeep'
。
时间格式化
使用 day.js 而不是 moment.js 来实现时间格式化,因为前者的包体积按其官方说法只有 2 kb。
查看主包大小
在微信开发者工具点击"详情 - 基本信息 - 代码依赖分析"查看:
得到的结果如下所示:
代码质量分析
通过微信开发者工具的"详情 - 性能质量" tab,点击扫描代码质量即可:
报错汇总
[ app.json 文件内容错误] app.json: pages 不能为空
查看 pages.json 文件,删除自动生成的如下代码即可:
json
{
"path": "App",
"style": {}
}
watcher 报错
使用 uView 的表单组件时,遇到如下报错:
原因是某个 <u-input>
的 type
属性使用了 select
,在某些情况下,比如清空表单后,该表单项的值为 null
,uView 内部处理去掉多余空格时就会报错:
解决办法是让重置时 select 表单项的值为 ''
。
页面路径找不到
如果报错什么页面路径找不到,可以检查下路径的最前面是否缺少 /
。