uniapp踩坑心得
node-sass安装失败
现象:
使用cli构建的uniapp项目依赖的是node-sass,这个玩意儿要从github下载文件,而且安装依赖还会检查python环境vsbuild-tools等等,挺难安装成功的.解决方案:
使用dart-sass替代:1
2npm un node-sass
npm i node-sass@npm:dart-sass -D替换依赖以后,还需要用
::v-deep
替换全局代码中的/deep/
样式穿透
全局样式加载报错
现象:
在main.js
全局引用样式文件时,打包会报错1
App平台 v3 模式暂不支持在 js 文件中引用"xxx.scss" 请改在 style 内引用
解决方案:
删掉main.js
中的相关import,修改App.vue1
2
3<style lang="scss">
@import '@/assets/styles/common.scss';
</style>
打包APP资源无法批量注册自定义的全局组件
现象:
在电脑上开发调试时,在components
文件夹下的index.js
中,批量注册了全局组件使用正常,但是打包APP资源后,全局组件都没有注册上解决方案:
修改pages.json
中的easycom
设置,将全局组件加入到自定义规则里去:1
2
3
4
5
6
7
8{
"easycom": {
"autoscan": false,
"custom": {
"font-icon": "@/components/global/FontIcon.vue"
}
}
}
打包APP资源后,由于svg-sprite-loader白屏
现象:
使用svg-sprite-loader
开发了全局的svg组件,在电脑上开发调试时使用正常,但是打包APP资源后,APP就白屏了,并且报错1
[WX_KEY_EXCEPTION_WXBRIDGE] exception: TypeError: undefined is not an object (evaluating 'n.documentElement')
解决方案:
在APP端不直接使用svg,可以将svg上传iconfont.cn,然后导出字体样式css文件(字体格式使用base64),通过fonticon来显示矢量图标
拆分pages.json,避免该文件在git提交合并时总有冲突
现象:
pages.json
记录了所有路由,多人开发项目时,这个文件经常发生冲突,而且由于这个文件行数非常多,解决冲突时看的头晕眼花很无语解决方案:
只适用于vue2的项目,vue3项目uniapp暂时没实现相关功能
a) 清除pages.json
里面的无用内容1
2
3
4
5
6
7
8{
"pages": [
{
"path": "views/index/index"
}
],
"globalStyle": {}
}b) 在
src
目录创建pages.js
1
2
3
4module.exports = (pagesJson, loader) => {
delete require.cache[require.resolve('./router/index')]
return require('./router/index')
}c) 在
src/router
目录创建index.js
和globalSetting.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// globalSetting.js
const components = {
// #ifndef H5
'font-icon': '@/components/global/FontIcon.vue',
// #endif
}
module.exports = {
globalStyle: {
navigationBarBackgroundColor: '#f7f7f7',
navigationBarTextStyle: 'black',
},
tabBar: {
color: '#707070',
selectedColor: '#1196db',
backgroundColor: '#fff',
borderStyle: 'white',
list: [
{
pagePath: 'views/index/index',
text: 'Home',
iconPath: 'static/images/tabGray/home.png',
selectedIconPath: 'static/images/tabColor/home.png',
},
{
pagePath: 'views/message/index',
text: 'Message',
iconPath: 'static/images/tabGray/message.png',
selectedIconPath: 'static/images/tabColor/message.png',
},
{
pagePath: 'views/index/apps',
text: 'App',
iconPath: 'static/images/tabGray/apps.png',
selectedIconPath: 'static/images/tabColor/apps.png',
},
{
pagePath: 'views/index/cart',
text: 'Cart',
iconPath: 'static/images/tabGray/shop-cart.png',
selectedIconPath: 'static/images/tabColor/shop-cart.png',
},
{
pagePath: 'views/user/login',
text: 'User',
iconPath: 'static/images/tabGray/user.png',
selectedIconPath: 'static/images/tabColor/user.png',
},
],
},
easycom: {
autoscan: false,
custom: components,
},
}
// index.js
// 将路由定义到多个文件中,避免像pages.json那样集中到一个文件容易冲突
// import home from './modules/home'
// import other from './modules/other'
// ...
delete require.cache[require.resolve('./globalSetting')]
const globalSetting = require('./globalSetting')
const pages = {
...globalSetting,
pages: [
{
path: 'views/index/index', // 首页
},
],
subPackages: [],
}
// pages.pages = [...home]
// pages.subPackages = [...other]
module.exports = pages
未依赖vue-router,用全局layout组件解决页面模版问题
现象:
uniapp的路由跳转用的是一系列uniapp的API,而非vue-router
,vue-router
里很实用的路由嵌套就不能用了解决方案:
自定义全局组件Layout.vue
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78<template>
<view v-doc-title="customTitle" :class="{ container: true, 'has-navbar': isDefaultNavbar }">
<uni-nav-bar
v-if="customTitle"
:title="customTitle"
:left-icon="isTabPage ? '' : 'left'"
:border="false"
backgroundColor="linear-gradient(to right, #ebe717, #7ec6bc)"
color="#fff"
class="custom-navbar"
shadow
statusBar
@clickLeft="onClickBack"
/>
<view class="header">
<slot name="header" />
</view>
<view class="main">
<slot />
</view>
<view class="footer">
<slot name="footer" />
</view>
</view>
</template>
<script>
import UniNavBar from './UniNavBar'
export default {
name: 'Layout',
components: { UniNavBar },
props: {
isTabPage: {
type: Boolean,
default: false,
},
isDefaultNavbar: {
type: Boolean,
default: true,
},
customTitle: {
type: String,
},
},
methods: {
onClickBack(e) {
this.$emit('click-left', e)
this.$Router.back()
},
},
}
</script>
<style lang="scss" scoped>
.container {
height: calc(100vh - env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
&.has-navbar {
height: calc(100vh - 44px - env(safe-area-inset-bottom));
padding-top: env(safe-area-inset-top);
}
.custom-navbar {
position: relative;
z-index: 999;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
}
</style>
封装this.$Router
现象:
用惯了vue-router的this.$router进行路由导航,用uniapp的API不习惯解决方案:
自己封装1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210// main.js
import Vue from 'vue'
import App from './App'
import router from './utils/router'
Vue.config.productionTip = false
Vue.prototype.$Router = router
App.mpType = 'app'
const app = new Vue({
...App,
})
app.$mount()
// utils/router
import qs from 'qs'
import globalSetting from '@/router/globalSetting' // 此文件定义了pages.json中的tabBar设置
const isTab = (url) => {
if (!globalSetting.tabBar || !globalSetting.tabBar.list || globalSetting.tabBar.list.length === 0) {
return false
}
const path = url.includes('?') ? url.split('?')[0] : url
return globalSetting.tabBar.list.findIndex((ele) => ele.pagePath === path) > -1
}
const encodeSymbol = (obj) => {
const ob = {}
for (const [key, value] of Object.entries(obj)) {
if (/\W+/.test(key)) {
console.error('路由参数名不能为特殊字符!')
return
}
let val = value
if (
Object.prototype.toString.call(value) === '[object Object]'
|| Object.prototype.toString.call(value) === '[object Array]'
) {
val = JSON.stringify(value)
}
ob[key] = val
}
return qs.stringify(ob)
}
const checkUrl = (location) => {
let url = ''
if (typeof location === 'string') {
url = location
}
if (typeof location === 'object' && location) {
url = location.path
}
if (!url) {
console.error('路由参数错误!')
return false
}
if (url.startsWith('/')) {
url = url.substr(1)
}
const inTab = isTab(url)
if (typeof location === 'object' && location) {
let query = location.query || location.params
if (query) {
query = encodeSymbol(query)
}
if (query) {
url = url.includes('?') ? `${url}&${query}` : `${url}?${query}`
}
}
if (inTab && url.includes('?')) {
console.error('切换Tab时不支持传递参数!')
return false
}
if (!url.startsWith('/')) {
url = `/${url}`
}
return {
url,
isTab: inTab,
}
}
export default {
push: (location, onComplete, onAbort) => {
const route = checkUrl(location)
if (!route) {
if (typeof onAbort === 'function') {
onAbort()
}
return
}
return new Promise((resolve, reject) => {
const api = route.isTab ? uni.switchTab : uni.navigateTo
api({
url: route.url,
success: (res) => {
if (typeof onComplete === 'function') {
onComplete(res)
}
resolve(res)
},
fail: (err) => {
if (typeof onAbort === 'function') {
onAbort(err)
}
reject(err)
},
})
})
},
replace: (location, onComplete, onAbort) => {
const route = checkUrl(location)
if (!route) {
if (typeof onAbort === 'function') {
onAbort()
}
return
}
return new Promise((resolve, reject) => {
const api = route.isTab ? uni.switchTab : uni.redirectTo
api({
url: route.url,
success: (res) => {
if (typeof onComplete === 'function') {
onComplete(res)
}
resolve(res)
},
fail: (err) => {
if (typeof onAbort === 'function') {
onAbort(err)
}
reject(err)
},
})
})
},
replaceAll: (location, onComplete, onAbort) => {
const route = checkUrl(location)
if (!route) {
if (typeof onAbort === 'function') {
onAbort()
}
return
}
return new Promise((resolve, reject) => {
uni.reLaunch({
url: route.url,
success: (res) => {
if (typeof onComplete === 'function') {
onComplete(res)
}
resolve(res)
},
fail: (err) => {
if (typeof onAbort === 'function') {
onAbort(err)
}
reject(err)
},
})
})
},
back: (onComplete, onAbort) => {
return new Promise((resolve, reject) => {
uni.navigateBack({
delta: 1,
success: (res) => {
if (typeof onComplete === 'function') {
onComplete(res)
}
resolve(res)
},
fail: (err) => {
if (typeof onAbort === 'function') {
onAbort(err)
}
reject(err)
},
})
})
},
forward: () => {
console.error('路由暂不支持前进功能!')
},
go: (delta, onComplete, onAbort) => {
if (typeof delta !== 'number' || delta > -1) {
console.error('路由暂不支持前进功能!')
return
}
return new Promise((resolve, reject) => {
uni.navigateBack({
delta: Math.abs(delta),
success: (res) => {
if (typeof onComplete === 'function') {
onComplete(res)
}
resolve(res)
},
fail: (err) => {
if (typeof onAbort === 'function') {
onAbort(err)
}
reject(err)
},
})
})
},
}
自定义H5项目入口html模板
现象:
HBuilder创建的项目需要修改manifest.json
文件,设置H5模板的地址解决方案:
cli创建的项目可以直接修改public/index.html
,此文件只对H5生效1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title><%= htmlWebpackPlugin.options.title %></title>
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
</head>
<body>
<noscript>
<strong>Please enable JavaScript to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.3/index.js"></script>
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole()
</script>
</html>
H5项目状态栏高度不对
现象:
statusBar组件在APP上显示没问题,在H5里高度为0解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71<template>
<view :style="{ height: statusBarHeight }" class="uni-status-bar">
<slot />
</view>
</template>
<script>
export default {
name: 'UniStatusBar',
data() {
return {
statusBarHeight: '0',
}
},
beforeMount() {
this.getHeight()
// #ifdef H5
if (uni?.webView) {
this.getSystemInfo()
} else {
document.addEventListener('UniAppJSBridgeReady', () => {
this.getSystemInfo()
})
}
window.addEventListener('message', this.handleMessage)
// #endif
},
beforeDestroy() {
// #ifdef H5
window.removeEventListener('message', this.handleMessage)
// #endif
},
methods: {
getHeight() {
let height = uni?.getSystemInfoSync ? uni.getSystemInfoSync().statusBarHeight : 0
if (!height) {
height = uni.getStorageSync('STATUSBAR_HEIGHT') || 0
}
this.statusBarHeight = `${height}px`
},
getSystemInfo() {
if (!uni?.webView) {
return
}
uni.webView.postMessage({
data: {
name: 'system',
data: {},
},
})
},
handleMessage(e) {
if (e.data.name === 'system') {
uni.setStorageSync('STATUSBAR_HEIGHT', e.data.data.statusBarHeight)
this.getHeight()
// #ifdef H5
window.removeEventListener('message', this.handleMessage)
// #endif
}
},
},
}
</script>
<style lang="scss">
.uni-status-bar {
// width: 750rpx;
height: 20px;
// height: var(--status-bar-height);
}
</style>
自定义下拉刷新,上拉分页加载
现象:
移动端不像PC端那样有分页组件,一般都是采用下拉刷新,上拉分页加载解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259<template>
<uni-list class="list">
<scroll-view
v-if="dataList && dataList.length > 0"
scroll-y
refresher-enabled
class="list-wrapper"
:refresher-triggered="refreshing"
@scrolltolower="getDataList"
@refresherrefresh="onRefresh"
>
<slot v-bind:dataList="dataList">
<uni-list-item
v-for="item in dataList"
:key="item.id"
:title="item.title"
:note="item.note"
:rightText="item.title"
:ellipsis="1"
:badgeText="item.title"
direction="column"
showBadge
showArrow
thumb="/static/images/logo.png"
clickable
@click="onClick(item)"
/>
</slot>
<uni-load-more :status="loading" :content-text="contentText" @clickLoadMore="getDataList" />
</scroll-view>
<no-data v-else />
</uni-list>
</template>
<script>
import NoData from './NoData'
import UniList from './UniList'
import UniListItem from './UniListItem'
import UniLoadMore from './UniLoadMore'
import { formatDate } from '@/utils/date-format'
export default {
name: 'RefreshList',
components: { NoData, UniList, UniListItem, UniLoadMore },
props: {
requestInstance: {
type: Function,
},
pageKey: {
type: String,
default: 'page',
},
pageSizeKey: {
type: String,
default: 'pageSize',
},
params: {
type: Object,
default: () => {},
},
},
data() {
return {
page: 1,
pageSize: 15,
refreshing: false,
loading: 'more', // more loading noMore
dataList: [],
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中...',
contentnomore: '没有更多',
},
}
},
beforeMount() {
this.getDataList()
},
watch: {
params: {
deep: true,
handler(val) {
console.log('watch', val)
this.getDataList(true)
},
},
},
methods: {
onRefresh() {
this.refreshing = true
this.getDataList(true)
},
getDataList(force = false) {
console.log('getDataList', force)
if (force === true) {
this.page = 1
this.loading = 'more'
}
if (this.loading !== 'more') {
return
}
this.loading = 'loading'
const params = this.getParams()
const api = typeof this.requestInstance === 'function' ? this.requestInstance : this.mockApi
api(params)
.then((res) => {
this.dataList = this.page === 1 ? res.data.list : this.dataList.concat(res.data.list)
this.page++
this.loading = res.data.isLastPage ? 'noMore' : 'more'
if (this.refreshing) {
this.refreshing = false
}
})
.catch((err) => {
this.loading = 'noMore'
if (this.refreshing) {
this.refreshing = false
}
})
},
mockApi() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this.page > 10) {
reject()
} else {
const arr = Array.from({ length: this.pageSize }, (val, key) => key + 1)
resolve({
data: {
isLastPage: false,
list: arr.map((ele) => {
return {
id: ele + (this.page - 1) * this.pageSize,
title: `${ele}`,
note: new Array(20).fill(ele).toString(),
time: formatDate(new Date()),
}
}),
},
})
}
}, 1000)
})
},
getParams() {
let obj = []
if (this.params instanceof Array) {
obj = [...this.params]
}
obj[this.pageKey] = this.page
obj[this.pageSizeKey] = this.pageSize
return obj
},
onClick(item, $event) {
this.$emit('item-click', item, $event)
},
},
}
</script>
<style lang="scss" scoped>
.list {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
::v-deep .uni-list {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
&-wrapper {
flex: 1;
overflow-y: auto;
}
}
</style>
<template>
<layout customTitle="列表演示页面" :isDefaultNavbar="false">
<template #header>
<uni-search-bar
v-model="searchVal"
:radius="44"
placeholder="搜索"
cancelButton="none"
bgColor="#eee"
@confirm="onSearch"
/>
</template>
<template #default>
<refresh-list :params="searchParams" :request-instance="api" class="list">
<template v-slot="{ dataList }">
<uni-list-chat
v-for="item in dataList"
:key="item.id"
:title="item.title"
:note="item.note"
:rightText="item.title"
:ellipsis="1"
:time="item.time"
:badgeText="item.unread"
badgePositon="left"
avatarCircle
:avatar="item.avatar"
clickable
@click="log(item)"
/>
</template>
</refresh-list>
</template>
<template #footer>
<view class="footer">
<button type="primary">刷新</button>
</view>
</template>
</layout>
</template>
<script>
import { lists } from '@/api/index'
export default {
name: 'Lists1',
data() {
return {
searchVal: '',
searchParams: {
keyword: '',
},
}
},
methods: {
api(params) {
return lists(params)
},
onSearch() {
this.$set(this.searchParams, 'keyword', this.searchVal)
},
log(...args) {
console.log(args)
},
},
}
</script>
<style lang="scss" scoped>
.list {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.footer {
padding: 10px;
}
</style>