出于简单原则,使用的是hbuilder创建的vue3项目,默认模版,vue版本选择了3,自带支持了组合式声明、ts、scss这些(scss会提示你安装下载hbuilder插件),相对来说还是比较方便的。
下面介绍一些vue3版本的区别:
1. 小程序生命周期,组合式声明
<script lang="ts" setup>
import { onLoad, onShow } from "@dcloudio/uni-app";
// 获取路由参数
onLoad((option)=>{
const { 参数可以在这里解构出来 } = option
})
</script>
如果是想多平台兼容,可能需要额外注意 uniapp的生命周期,之前用taroV2开发多平台的时候,踩了很多百度小程序的坑。
2. Hbuilder的项目生产环境下移除console
项目根目录创建vite.config.js
文件,然后用Hbuilder发行小程序的时候就会自动移除console了,配置如下:
import {defineConfig} from "vite";
import uni from "@dcloudio/vite-plugin-uni";
export default defineConfig({
plugins: [
uni(),
],
esbuild: {
pure: ['console.log'], // 删除 console.log
drop: ['debugger'], // 删除 debugger
}
});
3. pinia持久化状态
安装pinia-plugin-persistedstate
,然后配置,代码如下:
// 自定义storage
export const customStorage = {
getItem: (key : string) => uni.getStorageSync(key),
setItem: (key : string, value : string) => uni.setStorageSync(key, value),
removeItem: (key : string) => uni.removeStorageSync(key)
}
export const useAppStore = defineStore('app-store', {
state: () => ({
count: 0
}),
getters: {},
actions: {
addCount(){
this.count++
}
},
// 重要的是这里
persist: { storage: customStorage }
}
4. 利用provide
给页面和组件传递属性或事件
创建一个provide.ts
,方便父子组件的引入
// provide.ts
import { InjectionKey } from 'vue'
export type HomeProvide = {
count : number
}
export const homeProvideKey:InjectionKey<HomeProvide> = Symbol('HomeProvide')
比如引入Home
页面,其他Home
页面内的子组件,可以直接获取或修改provide
内的属性
// home.vue
<view class="page">
{{ homeProvide.count }}
</view>
<script lang="ts" setup>
import { provide, reactive } from 'vue';
import { homeProvideKey } from './provide';
const homeProvide = reactive({
count: 0
})
provide(homeProvideKey, homeProvide)
</script>
5. 防止误操作快速点击,又不想麻烦增加loading,可以使用节流函数包裹
<script lang="ts" setup>
import { throttle } from 'radash'
const onThrottleSubmit = throttle({ interval: 300 }, onSubmit)
function onSubmit(){}
</script>
6. 对uniapp popup
的简单组件封装
<template>
<uni-popup ref="popupRef" :mask-click="false">
<view class="popup-main">
<view class="close-wrapper" @click="onClose">
<view class="icon-close"></view>
</view>
<view class="title">{{data.title}}</view>
<view class="content">{{data.desc}}</view>
<view class="btn-group">
<view class="btn-cancel" @click="()=>emit('onCancel')">
{{data.cancelButtonText}}
</view>
<view class="btn-submit" @click="()=>emit('onSubmit')">
{{data.confirmButtonText}}
</view>
</view>
</view>
</uni-popup>
</template>
<script lang="ts" setup>
import { ref, Ref, watch, watchEffect } from 'vue'
interface Props {
modelValue : Ref<boolean>
}
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
})
const emit = defineEmits(["update:modelValue", "onCancel", "onSubmit"]);
const popupRef = ref<any>(undefined)
// defineExpose({ ref: popupRef })
const status = {
1: {
title: '标题!',
desc: "描述",
cancelButtonText: "取消按钮",
confirmButtonText: "提交按钮",
}
}
watchEffect(() => {
if (!popupRef.value) return
if (props.modelValue) {
popupRef.value.open('center')
} else {
popupRef.value.close()
}
})
function onClose() {
popupRef.value.close()
emit("update:modelValue", false)
}
</script>
<style lang="scss" scoped>
.popup-main {
background-size: contain;
background-color: #ffffff;
min-height: 325rpx;
border-radius: 16rpx;
position: relative;
width: 590rpx;
display: flex;
flex-direction: column;
}
.title {
margin-top: 52rpx;
text-align: center;
font-weight: bold;
font-size: 34rpx;
color: #191919;
}
.content {
font-weight: 400;
font-size: 26rpx;
color: #666666;
margin-top: 16rpx;
text-align: center;
}
.btn-group {
display: flex;
justify-content: space-between;
margin: 0 48rpx;
position: absolute;
bottom: 36rpx;
left: 0;
right: 0;
}
.btn-cancel {
width: 235rpx;
height: 88rpx;
background: #FFFFFF;
border-radius: 16rpx;
border: 1rpx solid #FF4750;
font-weight: bold;
font-size: 32rpx;
color: #FF4750;
display: flex;
align-items: center;
justify-content: center;
}
.btn-submit {
width: 235rpx;
height: 88rpx;
background: #FF4750;
border-radius: 16rpx;
border: 1rpx solid #FF4750;
font-weight: bold;
font-size: 32rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.icon-close {
background: url('icon.png') no-repeat center top;
background-size: contain;
width: 48rpx;
height: 48rpx;
box-sizing: border-box;
}
.close-wrapper {
position: absolute;
right: 0;
top: 0;
padding: 20rpx;
margin: -20rpx;
}
</style>
7. canvas画布,微信小程序支持canvas2d
,百度小程序不支持
直接使用echarts
会报错,因为小程序canvas
的上下文和web不一样,但是也有方法集成,但是考虑到echarts
实在太大了,不太推荐小程序使用它。
如果想要省心一点、兼容全平台,可以使用ucharts,相比echarts
体积会稍微小一些,但不如echarts
好用,而且看具体的配置需要收费。
其实最推荐的是自己手写,但是需要额外花时间去了解canvas
。
工作中用到了雷达图
,边学、边练习手写了一部分,目前还比较粗糙,待优化:
// radar.vue
<template>
<view class="radar-chart">
<canvas class="chart" type="2d" canvas-id="chartCanvas" id="chartCanvas"</canvas>
</view>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
const numberOfSides = 5; // 五边形的边数
const angle = (Math.PI * 2) / numberOfSides; // 五边形的内角
const fontSize = 14;
const padding = 18;
const max = 100;
const data = [
{name: "分类一",value: 20},
{name: "分类二",value: 90},
{name: "分类三",value: 55},
{name: "分类四",value: 32},
{name: "分类五",value: 75},
];
onMounted(() => {
// 如果是组件内,则需要加this
// const query = uni.createSelectorQuery().in(this)
// const ctx = uni.createCanvasContext("chartCanvas", this);
const ctx = uni.createCanvasContext("chartCanvas");
const query = uni.createSelectorQuery()
query.select('#chartCanvas')
.fields({
// 支持type 2d 才有node
node: true,
size: true
}, (res : any) => { })
.exec((res) => {
const { width, height } = res[0]
const canvas = res[0].node
if (!canvas) {
throw new Error('未获取到canvas 2d节点')
}
const ctx = canvas.getContext('2d')
ctx.font = `${fontSize}px sans-serif`;
// 缩放canvas,实现高清图像
const dpr = uni.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
// #ifndef H5
ctx.scale(dpr, dpr)
// #endif
console.log('ctx---', ctx)
const centerX = width / 2
const centerY = height / 2
for (let i = 1; i < data.length; i++) {
// 正五边形外接圆半径
const radius = Math.min(canvas.width, canvas.height) * (i / (11 * dpr));
console.log('radius--', radius)
drawPolygon(ctx, centerX, centerY, radius, i);
if (i == data.length - 1) {
drawLine(ctx, centerX, centerY, radius);
drawRegion(ctx, centerX, centerY, radius);
}
}
})
})
function drawPolygon(ctx, centerX, centerY, radius, idx) {
// 开始绘制路径
ctx.beginPath();
// 依次连接每个顶点的坐标,绘制五边形的边
let x, y;
for (let i = 0; i < numberOfSides; i++) {
const percent = ((max / (data.length - 1)) * idx).toString();
// 将画笔移动到第一个顶点的坐标
if (i === 0) {
x = centerX + radius * Math.cos(-Math.PI / 2);
y = centerY + radius * Math.sin(-Math.PI / 2);
ctx.moveTo(x, y);
// 显示百分比
ctx.font = `${12}px sans-serif`
ctx.fillStyle = "#BEBDC3";
ctx.fillText(percent, x + 6, y + 10);
} else {
x = centerX + radius * Math.cos(angle * i - Math.PI / 2);
y = centerY + radius * Math.sin(angle * i - Math.PI / 2);
ctx.lineTo(x, y);
}
// 填充文字
if (idx === data.length - 1) {
ctx.font = `${fontSize}px sans-serif`;
const textX =
centerX + (radius + padding) * Math.cos(angle * i - Math.PI / 2);
const textY =
centerY + (radius + padding) * Math.sin(angle * i - Math.PI / 2);
// console.log("idx", idx, textX, textY);
ctx.textAlign = "center";
ctx.fillStyle = "#191919";
// ctx.fillText(data[i].name, textX, textY + fontSize / 2);
// ctx.measureText(data[i].name).width
// 优化文字显示距离
if (angle * i === 0) {
ctx.fillText(data[i].name, textX, textY + padding / 2);
} else if (angle * i > 0 && angle * i <= Math.PI / 2) {
ctx.fillText(data[i].name, textX + padding, textY + padding / 2);
} else if (angle * i > Math.PI / 2 && angle * i <= Math.PI) {
ctx.fillText(data[i].name, textX, textY + padding / 2);
} else if (angle * i > Math.PI && angle * i <= (Math.PI * 3) / 2) {
ctx.fillText(data[i].name, textX, textY + padding / 2);
} else {
ctx.fillText(data[i].name, textX - padding, textY + padding / 2);
}
}
}
ctx.strokeStyle = "#BEBDC3";
// 闭合路径,连接最后一个顶点和第一个顶点
ctx.closePath();
ctx.stroke();
return ctx
}
function drawLine(ctx, centerX, centerY, radius) {
ctx.beginPath();
for (let i = 0; i < numberOfSides; i++) {
// const x = centerX + radius * Math.cos(angle * i);
// const y = centerY + radius * Math.sin(angle * i);
const x = centerX + radius * Math.cos(angle * i - Math.PI / 2);
const y = centerY + radius * Math.sin(angle * i - Math.PI / 2);
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
// console.log(x, y);
}
ctx.closePath();
ctx.stroke();
return ctx
}
function drawRegion(ctx, centerX, centerY, radius) {
const points = [];
for (let i = 0; i < numberOfSides; i++) {
ctx.beginPath();
const ratio = data[i].value / max;
const x = centerX + radius * ratio * Math.cos(angle * i - Math.PI / 2);
const y = centerY + radius * ratio * Math.sin(angle * i - Math.PI / 2);
ctx.moveTo(centerX, centerY);
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fillStyle = "#FF4750";
ctx.fill();
ctx.closePath();
points.push({ x, y });
}
// 生成的点连线
ctx.beginPath();
points.reduce((prev, cur, curIdx, arr) => {
if (curIdx === 0) {
ctx.moveTo(cur.x, cur.y);
}
const next = curIdx === arr.length - 1 ? arr[0] : arr[curIdx + 1];
ctx.lineTo(next.x, next.y);
// console.log(prev, cur, curIdx);
}, undefined);
ctx.closePath();
ctx.fillStyle = "rgba(255,71,80,0.2)";
ctx.fill();
return ctx
}
</script>
<style lang="scss">
.radar-chart {
// width: 600rpx;
// height: 400rpx;
width: 100vw;
height: 400rpx;
}
.chart {
width: 100%;
height: 100%;
}
</style>
8. 集成PageSpy进行调试
参考pagespy官网文档进行集成,这里选择的是node.js
部署
npm全局安装pagespy
,然后执行page-spy-api
命令,会启动一个服务<host>:6752
// 安装1
npm install -g @huolala-tech/page-spy-api@latest
// 启动
page-spy-api
输入本地调试ip
地址以后就可以看到下图的界面,选择接入SDK,跟着步骤来就好了,默认是https,需要手动配置关闭一下!
,接入完成以后就可以在房间列表
看到接入的小程序了。
// main.js
new PageSpy({
api: '192.168.0.191:6752',
// 这里配置关闭HTTPS
enableSSL: false
})