vue3手写电商微信小程序( 微信开发者工具)【问题总结】
- 一、项目技术选型
- 二、踩坑总结:
- 1、样式穿透在微信开发者工具不适用:
- 2、`v-html`在微信开发者工具显示的是`rich-text`组件,导致样式不适用:`图片大小不自适应`,比如图片太大,只展示了一半
- 3、uni-app组件里的`checkbox-group`和`checkbox`写的样式在微信开发者工具里不匹配
- 4、蓝湖UI上的图片链接在微信开发者工具上不可用
- 5、问题: 元素隐式具有 “any“ 类型,因为类型为 “string“ 的表达式不能用于索引类型 “Object“。 在类型 “Object“ 上找不到具有类型为 “string“ 的参数的索引签名
- 6、ts对于类型定义这块比较严谨,所以在传prop要传一个对象时,要使用泛类型进行定义:
- 7、在calc时,在符号前面要增加空格,没有空格不会生效
- 8、小程序系列无法拦截原生tabbar及原生导航返回,如需拦截就需要自定义tabbar、header,在这里我们可以利用其他解决方案:
- 9、后端一次性返回大量数据,影响页面加载
- 10、uniapp微信小程序嵌入页面web-view配置
- 11、上传文件
- 12、长按保存图片至相册
- 三、总结:
一、项目技术选型
1、该项目技术栈:
vite3 + vue3.2 + pinia + typeScript
2、基础框架:
cli创建的Vue3/Vite项目 与 使用HBuilderX导入插件 的包有差异,请直接访问 开源地址
二、踩坑总结:
1、样式穿透在微信开发者工具不适用:
解决方案: 全局引入一个
scss
文件,在其内根据微信小程序开发者工具上的标签层级单独写样式
2、v-html
在微信开发者工具显示的是rich-text
组件,导致样式不适用:图片大小不自适应
,比如图片太大,只展示了一半
解决方案:
- html代码:
<rich-text class="content" :nodes="formatImg(state.info.detailMobileDesc)"</rich-text>
- JavaScript代码:
const formatImg = (html: string) => {
var newContent = html.replace(/<img[^>]*>/gi, match => {
let processed = null;
if (!match.match(/style=\"(.*)\"/gi)) {
processed = match.replace(
/\<img/g,
'<img style="width:100%;height:auto;display:block"',
);
} else {
processed = match.replace(
/style=\"(.*)\"/gi,
'style="width:100%;height:auto;display:block"',
);
}
return processed;
});
return newContent;
};
3、uni-app组件里的checkbox-group
和checkbox
写的样式在微信开发者工具里不匹配
审核元素发现在小程序开发者工具中,checkbox组件是被黑盒封装的,根本无法查看组件内部的类名
,也就是说无法知晓修改的类名具体情况了。于是查阅资料发现,小程序平台checkbox组件实际内部类名和h5端不一致(类名及代码如下):
<checkbox-group @change="checkboxChange">
<uni-swipe-action>
<uni-swipe-action-item
class="uni-list-cell"
v-for="(item, index) in state.cartList"
:key="item.sku"
>
<view class="content-box">
<view class="ceshi">
<checkbox
:class="{ checked: item.checked }"
:value="item.value"
:checked="item.checked"
@click="checkOne(item, index)"
/>
</view>
<view class="box">
<img
class="cellImage"
:src="JSON.parse(item.goodsImg)[0]"
alt=""
/>
<view class="right">
<p class="nameTitle">
{{ item.goodsName }}
</p>
<p class="modelSec">
{{ item.model }}
</p>
<view class="priceSele">
¥{{ item.price }}
<view class="numBox"
><uni-number-box
:min="1"
:max="9999999999"
:isMax="item.count >= 9999999999 ? true : false"
:isMin="item.count === 1"
v-model="item.count"
@change="changeValue"
/></view>
</view>
</view>
</view>
</view>
<template v-slot:right>
<view class="slot-button" @click="deleteCartItem(index)">
<image
class="thumbnail_4"
referrerpolicy="no-referrer"
src="http://p0.qhimg.com/t01fd5d8f0e8cfe01c4.png"
/>
<p> 删除 </p>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</checkbox-group>
所以,若要兼容多平台需要写两套不同类名的样式,同时配合uni-app中的条件注释
实现跨端兼容,通过在代码外层包裹条件注释,当ifdef
后面的平台类型是MP-WEIXIN
,代表下列代码只会在微信小程序端生效,其它平台不会执行:
/* #ifdef MP-WEIXIN */
uni-checkbox .uni-checkbox-input,
checkbox .wx-checkbox-input {
width: 35rpx;
height: 35rpx;
border-radius: 50% !important;
}
uni-checkbox .uni-checkbox-input,
checkbox .wx-checkbox-input.wx-checkbox-input-checked {
color: #fff !important;
background-color: #3873fa !important;
}
/* #endif */
关于条件注释,如果还不是很明白,可以查看:条件编译 解决各端差异 - uni-app官网 (dcloud.io)
4、蓝湖UI上的图片链接在微信开发者工具上不可用
需要将图片下载下来放在图床上生成链接,最后统计再上传至白名单更改域名
5、问题: 元素隐式具有 “any“ 类型,因为类型为 “string“ 的表达式不能用于索引类型 “Object“。 在类型 “Object“ 上找不到具有类型为 “string“ 的参数的索引签名
描述: 在写代码的时候,取一个对象某一个key对应的value值,但是写完之后发现
Typescript
报错了,错误内容就是如题,有点奇怪,特此去了解一下:
for (const key in obejct) {
// 处理...
obejct[key]
....
}
解决方案:
方案一:忽略
在tsconfig.json
中compilerOptions
里面新增忽略的代码,如下所示,添加后则不会报错:
"suppressImplicitAnyIndexErrors": true
方案二:声明
在定义的Interface里对其进行声明,如下所示,声明过后,也不会再报错:
interface DAMNU_ENABLE {
....
[key: string]: boolean, // 字段扩展声明
};
[key: string]: boolean, // 字段扩展声明 声明之后可以用方括号的方式去对象里边的值
方案三:对其使用keyof进行判断
TypeScript 允许我们遍历某种类型的属性,并通过
keyof
操作符提取其属性的名称。keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型
。
export function isValidKey(
key: string | number | symbol,
object: object
): key is keyof typeof object {
return key in object;
}
定义一个函数:isValidKey()
,然后对需要使用到的地方进行一次判断:
for (const key in obejct) {
if(isValidKey(key,obejct)){
// 处理...
obejct[key]
....
}
}
这三种方式都可以解决如题的报错信息,但是个人比较倾向于第三种,并不是其他两种不好,只是说,写第一种有一点点写any的感觉,不到万不得已不这么弄;第二种得把用到的属性字段类型都写出来,有点费事
6、ts对于类型定义这块比较严谨,所以在传prop要传一个对象时,要使用泛类型进行定义:
// 接收外部传参
interface propData {
goodsItem: {
goodsImg: string;
itemName: string;
model: string;
price: string;
count: string;
};
}
const props = defineProps<propData>();
7、在calc时,在符号前面要增加空格,没有空格不会生效
height: calc(100vh - 80rpx);
8、小程序系列无法拦截原生tabbar及原生导航返回,如需拦截就需要自定义tabbar、header,在这里我们可以利用其他解决方案:
业务场景: 在首页
home.vue
中点击底部tabbar
跳转至二级页面page.vue
,跳转时需要进行角色验证,判断是否有该模块权限,在这里没有办法监测tabbar
跳转,只能利用两个页面传值的方式进行监测:
page.vue
文件(uni.$emit('open');
)
onShow(() => {
// 判断是否是游客
const userStore = userInfoStoreStore();
const userInfo = userStore.getUserInfo;
if (!userInfo.userRole) {
uni.switchTab({
url: '/pages/home/index',
});
uni.$emit('open');
} else {
// 该页面其他操作
......
}
});
home.vue
文件:
onShow(() => {
// 游客角色点击产品
uni.$on('open',function(){
// 若是游客,首页做提示操作
.......
})
})
9、后端一次性返回大量数据,影响页面加载
虽然后端一次返回这么多数据,但用户的屏幕只能同时显示有限的数据。所以我们可以采用延迟加载的策略,根据用户的滚动位置动态渲染数据。
要获取用户的滚动位置,我们可以在列表末尾添加一个空节点空白。每当视口出现空白时,就意味着用户已经滚动到网页底部,这意味着我们需要继续渲染数据。
同时,我们可以使用getBoundingClientRect
来判断空白是否在页面底部。
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
// code as before
}
const container = ref<HTMLElement>() // container element
const blank = ref<HTMLElement>() // blank element
const list = ref<any>([])
const page = ref(1)
const limit = 200
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// List of real presentations
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
if (page.value > maxPage.value) return
const clientHeight = container.value?.clientHeight
const blankTop = blank.value?.getBoundingClientRect().top
if (clientHeight === blankTop) {
// When the blank node appears in the viewport, the current page number is incremented by 1
page.value++
}
}
onMounted(async () => {
const res = await getList()
list.value = res
})
</script>
<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}</span>
</div>
<div ref="blank"></div>
</div>
</template>
10、uniapp微信小程序嵌入页面web-view配置
在uniapp微信小程序中嵌入页面web-view通常需要配置以下内容:
a. 域名白名单
在微信小程序中,只有在白名单中的域名才能在web-view
中加载内容。因此,在嵌入web-view之前,您需要将要加载的 域名
添加到小程序的域名白名单中。可以使用微信开发者工具,在“项目”->“开发设置”->“服务器域名”
中添加域名,或者使用wx.setStorageSync()
方法动态添加.
b. 加载安全策略
为了确保在web-view中加载的内容是安全的,可以在web-view标签中添加一个’sandbox
’属性。此属性启用了加载安全策略,可以防止来自web-view中加载的网页在浏览器中执行JavaScript代码,从而增加了安全性。
<web-view :src="url" sandbox></web-view>
c. 指定web-view的高度和宽度
在嵌入web-view之前,请确保为其指定合适的高度和宽度。可以使用CSS样式或uni-app的组件API来指定web-view的大小。例如:
<web-view :src="url" style="height: 100%; width: 100%;"></web-view>
uni.createSelectorQuery().select('#web-view').boundingClientRect(res => {
console.log('webview size:', res.width, res.height);
}).exec();
d. 处理web-view事件
与普通微信小程序页面一样,可以在web-view中使用onLoad等生命周期方法或者事件监听来实现交互。例如:
<web-view :src="url" @load="onLoad"></web-view>
methods: {
onLoad(e) {
console.log(e);
}
}
11、上传文件
a、上传file:///var/mobile/Media/PhotoData/Mutations/DCIM/104APPLE/IMG_4307/Adjustments/FullSizeRender.mov这个路径的文件
在微信小程序中,无法直接上传本地文件系统中的文件
,包括file:///var/mobile/Media/PhotoData/Mutations/DCIM/104APPLE/IMG_4307/Adjustments/FullSizeRender.mov
这个路径下的文件。
- 如果你想要在微信小程序中上传
图片文件
,可以使用小程序提供的wx.chooseImage
接口来选择本地图片,并将图片上传到服务器。该接口会打开系统的相册或相机,让用户选择或拍摄一张图片,然后将图片的临时文件路径返回给小程序,你可以使用该路径来上传图片。
示例代码如下:
wx.chooseImage({
success: function(res) {
var tempFilePaths = res.tempFilePaths;
wx.uploadFile({
url: 'https://example.com/upload',
filePath: tempFilePaths[0],
name: 'file',
success: function(res) {
console.log(res.data);
}
})
}
})
- 如果你想要上传
视频文件
,可以使用wx.chooseVideo
接口来选择本地视频文件,并将视频文件上传到服务器。该接口会打开系统的相册或相机,让用户选择或拍摄一段视频,然后将视频的临时文件路径返回给小程序,你可以使用该路径来上传视频文件。
示例代码如下:
wx.chooseVideo({
sourceType: ['album', 'camera'],
maxDuration: 60,
camera: 'back',
success: function(res) {
var tempFilePath = res.tempFilePath;
wx.uploadFile({
url: 'https://example.com/upload',
filePath: tempFilePath,
name: 'file',
success: function(res) {
console.log(res.data);
}
})
}
})
12、长按保存图片至相册
需求:这个是一个商品详情页面,需要在详情部分实现长按保存图片(数据都是后端直接返回的html,需要从中截取出图片地址)
<rich-text
class="content"
:nodes="formatImg(state.info.detailMobileDesc || state.info.detailPcDesc)"
@longpress="matcheUrl"
></rich-text>
// 获取图片地址
const matcheUrl = () => {
const htmlString = state.info.detailMobileDesc || state.info.detailPcDesc;
const regex = /<img[^>]+src="([^">]+)"/g;
const matches = htmlString.match(regex);
if (matches) {
const imageUrls = matches.map((match: any) =>
match.replace(/<img[^>]+src="([^">]+)"/, '$1'),
);
// 实现长按下载
longtap(imageUrls);
}
};
const longtap = (imgList: any) => {
// 这里是上级要求不要用原地址,需要后端用nginx转一下
const updatedImageUrls = imgList.map((url: string) =>
url.replace(/^https?:\/\/[^/]+/, 'https://xxx/img'),
);
console.log(updatedImageUrls);
if (updatedImageUrls.length > 0) {
uni.showActionSheet({
itemList: ['保存全部图片'],
success: res => {
if (res.tapIndex === 0) {
// 使用uni.authorize()方法请求用户授权访问相册
uni.authorize({
scope: 'scope.writePhotosAlbum',
success: () => {
// 成功的话 去下载
for (let i = 0; i < updatedImageUrls.length; i++) {
uni.downloadFile({
url: updatedImageUrls[i],
success: res => {
if (res.statusCode === 200) {
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
if (i === updatedImageUrls.length - 1) {
uni.showToast({
title: '保存成功',
icon: 'success',
});
}
},
fail: err => {
console.log(err);
uni.showToast({
title: '保存失败',
icon: 'none',
});
},
});
}
},
fail: err => {
console.log(err);
uni.showToast({
title: '下载失败',
icon: 'none',
});
},
});
}
},
fail: () => {
// 失败的话 清收用户授权相册权限
uni.showModal({
title: '授权提示',
content: '您拒绝了相册权限,是否打开设置去授权?',
cancelText: '取消', // 取消按钮的文字
confirmText: '确认', // 确认按钮的文字
showCancel: true, // 是否显示取消按钮,默认为 true
confirmColor: '#f55850',
cancelColor: '#39B54A',
success: res => {
if (res.confirm) {
console.log('comfirm'); //点击确定之后执行的代码
uni.openSetting({
success(res) {
console.log(res.authSetting);
},
});
} else {
console.log('cancel'); //点击取消之后执行的代码
}
},
});
},
});
}
},
});
}
};
注意: 如果您的小程序是在微信公众平台中创建的,您需要在提交审核之前进行“
写入相册
”权限的设置。
三、总结:
后续还发现有什么问题我会继续更新博客,大家也可以在评论区沟通交流,拜拜!!!