背景
团队是做保险业务相关,微信小程序的项目是要借鉴“绩效新书”微信公众号保险查询工具,包含保险搜索、利益演示、重疾病条款对比、保费对比等功能,需要爬虫、前后端以及算法等各方面去实现相关功能,服务人员主要是给内部保险业务人员使用。整个项目的技术调研子在年前开始,在年后开工之前也进行一部分调研。
技术方案
微信小程序的开发是基于京东的Taro框架,使用了vue3+pinia进行状态管理,UI库使用了配套的京东的nutui,图表库使用了字节跳动的@visactor/vchart。一般都是taro+react的方案,社区里面插件的数量来说,还是react部分多一些,但是整个前端组还是vue为主,很少会react,所以为了更容易维护采用了此种方案。 涉及到文件上传、个性化设置等内webview内嵌页面,统一由单独的web项目维护,使用了vite + vue3 + pinia,UI库选择vant,使用了px2vw保证移动端兼容性。
开发问题总结
自定义头部导航栏
在整个项目中,为了尽量贴合设计稿,需要在头部插入查询组件、背景图片、自定义标题的样式和位置,因为微信小程序的导航栏无法插入图片,组件,无法修改标题的样式,所以选择部分页面使用自定义导航栏组件 主要具体的实现思路有很多,可以查看具体相关文章,比如:【Taro开发】-自定义导航栏NavBar(五)
此处只是简单记录实现思路,所以不会讲的很详细,但是本质就是两点:
- 系统的状态栏的高度的计算(刘海屏等异性屏的处理)
- 头部导航栏的标题定位和右侧胶南留出宽度
- 头部导航栏的扩展和自定义是否显示返回、回到主页等处理
解决方案:
- 利用Taro的getSystemInfoSync()获取的statusBarHeight获取导航栏高度
- 利用Taro的getMenuButtonBoundingClientRect()获取头部胶囊的位置和宽高,获取胶囊位置是为了右侧留出空白和计算整个标题的位置处理
- 在前两步骤没有问题的情况下,可以通过slot和props来达到拓展的目的,同时为了方便重复计算,第一二的结果会保存在project级别的store中
代码实现:
<template>
<view
id="basicNavBar"
:class="[styles.basicNavBarContainer, state.configStyle.ios ? styles.ios : styles.android, props.class]"
:style="{
height: `${state.configStyle.navBarHeight + state.configStyle.navBarExtendHeight}px`
}"
>
<slot name="navbarBg"/>
<view
:class="styles.navBarInner"
:style="{
top: state.configStyle.contentGroupTop,
height: state.configStyle.contentGroupHeight,
}"
>
<view :class="styles.otherInnerGroup">
<view
v-if="!props.isDisplayNoneBack"
:class="styles.leftGroup"
>
<Left
v-if="!props?.customBackIcon"
:class="[styles.goBackIcon,
!isShowBackIcon && styles.hiddenIcon,
isShowHomeIcon && styles.displayIcon
]"
@click="handleBackClick"
></Left>
<slot
v-else
name="backIcon"
>
</slot>
<Home
v-if="isShowHomeIcon"
@click="handleGoHomeClick"
:class="[styles.goHomeIcon]"
>
</Home>
</view>
<view :class="styles.middleGroup">
<view v-if="props.title" :class="styles.title">
{{ props.title }}
</view>
<slot name="title"></slot>
</view>
<view :class="styles.rightGroup">
<slot name="right" />
</view>
</view>
</view>
</view>
</template>
<script setup>
import styles from './index.module.scss';
import { getSystemInfo, isFunction } from './utils';
import Taro from '@tarojs/taro';
import {
defineProps, defineEmits,
reactive, computed,
} from 'vue';
import {Left, Home} from "@nutui/icons-vue-taro";
import {useProjectStore} from "@/stores/project";
const projectStore = useProjectStore();
const emits = defineEmits([
'back',
'home',
])
const props = defineProps({
// 是否设置自定义回退,如果设置自定义回退,则直接使用插槽
customBackIcon: {
type: Boolean,
default: false,
},
isDisplayNoneBack: {
type: Boolean,
default: false,
},
onHome: {
type: Function,
default: () => null,
},
extClass: {
type: String,
default: '',
},
class: {
type: String,
default: '',
},
background: {
type: String,
default: "#ffffff",
}, //导航栏背景
color: {
type: String,
default: "#000000",
},
title: {
type: String,
default: '',
},
back: {
type: Boolean,
default: false,
},
home: {
type: Boolean,
default: false,
},
iconTheme: {
type: String,
default: "black",
},
delta: {
type: Number,
default: 1,
},
})
const isShowBackIcon = computed(() => {
return (props?.back && !props?.home) || (props?.back && props?.home)
})
const isShowHomeIcon = computed(() => {
return (!props?.back && props?.home) || (props?.back && props?.home);
})
// 直接获取全局状态处理
let globalSystemInfo = getSystemInfo();
const state = reactive({
configStyle: setStyle(globalSystemInfo),
})
function setStyle(systemInfo) {
const {
statusBarHeight,
navBarHeight,
capsulePosition,
navBarExtendHeight,
ios,
windowWidth
} = systemInfo;
let contentGroupTop = `${capsulePosition?.top || 0}px`;
let contentGroupHeight = `${capsulePosition?.height || 0}px`;
const { back, home, title, color } = props;
let rightDistance = windowWidth - capsulePosition.right; //胶囊按钮右侧到屏幕右侧的边距
let leftWidth = windowWidth - capsulePosition.left; //胶囊按钮左侧到屏幕右侧的边距
let navigationbarinnerStyle = [
`color:${color}`,
`height:${navBarHeight + navBarExtendHeight}px`,
`padding-top:${statusBarHeight}px`,
`padding-right:${leftWidth}px`,
`padding-bottom:${navBarExtendHeight}px`
].join(";");
let navBarLeft = [];
if ((back && !home) || (!back && home)) {
navBarLeft = [
`width:${capsulePosition.width}px`,
`height:${capsulePosition.height}px`,
`margin-left:0px`,
`margin-right:${rightDistance}px`
].join(";");
} else if ((back && home) || title) {
navBarLeft = [
`width:${capsulePosition.width}px`,
`height:${capsulePosition.height}px`,
`margin-left:${rightDistance}px`
].join(";");
} else {
navBarLeft = [`width:auto`, `margin-left:0px`].join(";");
}
// 强行更新一次高度
projectStore.updateNavBarHeight(navBarHeight)
return {
contentGroupTop,
contentGroupHeight,
navigationbarinnerStyle,
navBarLeft,
navBarHeight,
capsulePosition,
navBarExtendHeight,
ios,
rightDistance
};
}
/**
* 作用: 自定义回退逻辑
*/
function handleBackClick() {
emits('back');
const pages = Taro.getCurrentPages();
if (pages.length >= 2) {
Taro.navigateBack({
delta: props.delta
});
}
}
/**
* 作用: 自定义返回主页逻辑
*/
function handleGoHomeClick() {
emits('home');
Taro.reLaunch({
url: '/pages/index/index',
});
}
</script>
滚动吸顶效果处理
受限于微信本身的渲染规则,在web层面一般的吸顶效果处理,在小程序上会出现一些奇怪的问题,比如position:sticky的定位问题,监听scroll进行js的样式处理导致实际真机滚动起来很卡顿(本质是小程序的视图层和逻辑层不断阻塞导致),在开发过程中,图中类似效果采用两种办法之后实际效果并不好,所以使用Taro.createIntersectionObserver()判断元素是否相交进行组件显示隐藏控制能达到丝滑的效果,主要思路:将吸顶组件进行隐藏,判断是否判断该相交的元素固定,当判断相交的元素和页面或者其他基准组件能够触发intersection事件的时候,进行显示隐藏
简单代码处理:
<view
:class="styles.compareInsuranceContainer"
>
<CustomTabList
v-show="isShowFixedTabList && store?.compareDetailList?.length"
:class="styles.fixedTabList"
:style="{
top: `${projectStore.navBarHeight}px`,
zIndex: 2,
}"
v-model="selectedTabValue"
:defaultValue="selectedTabValue"
@change="handleTabChange"
ref="ShowAndHiddenTabListRef"
id="showAndHiddenTabList"
/>
</view>
<script>
...
let customObserve1 = null;
const isShowFixedTabList = ref(false);
// 注意销毁,不然会导致小程序内存泄露
watch(() => tabListGroupRef.value, async (newVal, oldVal) => {
if (newVal) {
Taro.nextTick(() => {
customObserverTabList();
})
} else {
customObserve1 && customObserve1.disconnect();
}
})
function customObserverTabList() {
const pageDefaultObserverOptions = {
initialRatio: 1,
thresholds: [0, 1],
}
// 进行相交滚动处理
customObserve1 = Taro.createIntersectionObserver(null, pageDefaultObserverOptions)
.relativeToViewport({
top: -(projectStore.navBarHeight + 40),
})
.observe('#tabListGroup', (res) => {
const rect = res.intersectionRect;
const ratio = res.intersectionRatio;
if (!ratio) {
isShowFixedTabList.value = true;
} else {
isShowFixedTabList.value = false;
}
})
}
</script>
图表处理
有的开发看到上面的页面布局之后,会想到为什么不用小程序的scroll-view,滚动性能会更好,其实本质是为了底部canvas图表,scroll-view本质是不断渲染视线节点,不渲染离屏的节点提高性能,而这出现一个问题,就是canvas在离屏的情况下,canvas节点没有创建,导致vchart的图表无法渲染,导致图表空白,这是一个坑。
为什么使用字节@visactor/vchart的图表库,而不用echart这种成熟的方案,主要是图表库的配色和设计稿相比差异较大,不能很好的还原设计稿。但是在使用vchart图表库的时候,还是发现了问题:
+ 微信小程序层面需要二次封装,才能支持vue3的语法,官方提供了vchart-react的封装,没有提供vue3的处理,再看了相关实现之后,自己封装了一下使用,本质就是预留canvas给chart本体、tooltip等指定canvas,指定运行环境和图片设定配置,可以看代码实现
+ vchart图表库较大,一定要分包使用,同时一定要注意,分包使用的时候,相关环境、plugin是否按需引入,不然导致图表显示不完整等问题
代码实现:
<script setup>
import { registerWXEnv } from '@visactor/vchart/esm/env';
import { VChart, vglobal } from '@visactor/vchart/esm/core';
import { registerAreaChart } from '@visactor/vchart/esm/chart';
import {
registerCanvasTooltipHandler,
} from '@visactor/vchart/esm/plugin';
import {
registerMarkLine,
registerCartesianLinearAxis,
registerCartesianBandAxis,
registerTooltip,
registerCartesianCrossHair,
registerDiscreteLegend,
} from '@visactor/vchart/esm/component';
import Taro from '@tarojs/taro';
import {ref, watch} from 'vue';
import styles from './index.module.scss';
let chart = null, colorMap = {};
// 注册图表和组件
VChart.useRegisters([
registerWXEnv,
registerCanvasTooltipHandler,
registerMarkLine,
registerAreaChart,
registerCartesianLinearAxis,
registerCartesianBandAxis,
registerTooltip,
registerCartesianCrossHair,
registerDiscreteLegend,
]);
const props = defineProps({
canvasId: {
type: String,
default: '',
},
isShowChart: {
type: Boolean,
default: true,
},
dataSource: {
type: Array,
default: () => [],
}
})
// 设置标记处理
const getMarkLineStyleConfig = (fill) => ({
endSymbol: {
style: {
angle: 180,
fill,
dy: -5
}
},
label: {
dx: -4,
// dy: -20,
dy: -12,
width: 28,
labelBackground: {
padding: 4,
style: {
fill,
borderRadius: 5
}
},
style: {
fill: '#fff',
fontSize: 8,
fontWeight: 400,
}
}
})
const chartContainer = ref(null);
const handleEvent = (event) => {
if (chart) {
Object.defineProperty(event, 'chart', {
writable: false,
// value: chartConfig.vchart.getCanvas() // Tip: 必须设置
value: chart.getCanvas() // Tip: 必须设置
});
chart.getStage().window.dispatchEvent(event);
}
};
const getDomRef = async () => {
return new Promise(resolve => {
Taro.nextTick(() => {
Taro.createSelectorQuery()
.select(`#${props.canvasId}`)
.boundingClientRect(domref => {
resolve(domref);
})
.exec();
});
});
};
const getMarkLineConfigListByDataSource = (dataSource = []) => {
const rateConfig = new Array(9).fill(null).reduce((preItem,item, index) => {
if (!index) {
preItem.push(index + 1);
}
preItem.push(index + 2);
return preItem;
}, []);
// const rateConfig = new Set();
const recordRateByKeyMap = new Map();
const res = dataSource.reduce((preItem, currItem, index) => {
const currRate = Math.floor(currItem.value / currItem.initValue);
// 初始化record
if (!recordRateByKeyMap.has(currItem.key)) {
recordRateByKeyMap.set(currItem.key, []);
}
// rateConfig.find(item => item === currRate)
if (rateConfig.find(item => item === currRate)) {
// const fillColor = '#1777FF';
const fillColor = colorMap?.[currItem.key] || '#1777FF';
const markLineStyleConfig = getMarkLineStyleConfig(fillColor)
// 抽取当前标记区间
const config = {
...markLineStyleConfig,
relativeSeriesId: currItem.key,
label: {
text: `x${currRate}`,
...markLineStyleConfig.label
},
x: (relativeSeriesData) => {
const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
if (markLineSeriesData) {
return markLineSeriesData.time;
}
},
y: (relativeSeriesData) => {
const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
if (markLineSeriesData) {
return 0;
}
},
y1: (relativeSeriesData) => {
const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
if (markLineSeriesData) {
return markLineSeriesData.value;
}
},
line: {
style: {
opacity: 0,
}
}
}
// 判断当前是否重复
if (!recordRateByKeyMap.get(currItem.key).includes(currRate)) {
recordRateByKeyMap.set(currItem.key, [...recordRateByKeyMap.get(currItem.key), currRate])
preItem.push(config);
}
}
return preItem;
}, []);
return res;
}
function setFillColorMap(dataSource = []) {
const keySet = new Set();
const keyList = dataSource.reduce((preItem, currItem) => {
if (!keySet.has(currItem.key)) {
keySet.add(currItem.key);
preItem.push(currItem.key);
}
return preItem;
}, []);
const colorList = ['#1777FF', '#00DB8B', '#FF7726']
const configObj = keyList.reduce((preItem, currItem, index) => {
preItem[currItem] = colorList[index];
return preItem;
}, {});
colorMap = configObj;
return configObj;
}
const getChartSpec = (dataSource = []) => {
const spec = {
// type: 'area',
type: 'line',
data: {
id: 'lineAndAreaChart',
values: dataSource
},
title: {
visible: false,
},
stack: false,
xField: 'time',
yField: 'value',
// seriesField: 'name',
seriesField: 'key',
axes: [
{
type: 'band',
orient: 'bottom', // 声明显示的位置
tick: {
tickStep: 9
},
},
{
type: 'linear',
orient: 'left', // 声明显示的位置
domainLine: {
visible: true,
},
label: {
formatMethod: function(text, datum) {
const val = Number(text);
if (val >= 100000000) {
return `${text.toString().slice(0, -8)}亿`
}
if (val >= 10000) {
return `${text.toString().slice(0, -4)}万`
}
return text;
}
}
}
],
scrollBar: [
{
orient: 'bottom',
field: 'time',
roam: true,
round: true,
}
],
line: {
style: {
curveType: 'monotone',
lineWidth: 2,
stroke: datum => {
return colorMap?.[datum.key] || '#1777FF';
},
}
},
point: {
style: {
size: 0,
fill: 'white',
stroke: null,
},
},
area: {
style: {
fillOpacity: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOpacity: 0.4,
shadowBlur: 4,
shadowOffsetY: 4,
stroke: datum => {
return colorMap?.[datum.key] || '#1777FF';
},
},
},
legends: [
{
visible: true,
position: 'middle',
orient: 'top',
layout: 'vertical',
item: {
shape: {
style: {
symbolType: 'roundLine',
size: 10,
opacity: 1,
lineWidth: 0.5,
},
stroke: datum => {
return colorMap?.[datum.key] || '#1777FF';
},
},
label: {
formatMethod: (text, item, index) => {
// return item?.name || '-';
const name = props.dataSource.find(item => item.key === text)?.name
return name || '-';
}
},
}
},
],
// 设置倍率标注
markLine: getMarkLineConfigListByDataSource(dataSource),
// 自定义tooltip
tooltip: {
mark: {
visible: false,
},
dimension: {
position: 'tl',
shapeType: 'square',
shapeHollow: false,
shapeFill: datum => {
return colorMap?.[datum.key] || '#1777FF';
},
title: {
value: (datum) => {
return `第${datum?.time || '-'}年`;
}
},
content: {
key: (datum) => {
if (datum.name?.length > 8) {
return `${datum.name.slice(0, 8)}...`
}
return `${datum.name}`
},
value: (datum) => {
return `${datum.value}`
},
}
},
style: {
panel: {
padding: [8, 10],
backgroundColor: 'rgba(255, 255, 255, .7)',
shadow: {
// color: 'rgba(0,0,0,0.15)',
x: 0,
y: 0,
blur: 10,
spread: 5,
}
},
titleLabel: {
fontSize: 10,
fontFamily: 'PingFangSC, PingFang SC',
fill: '#1777FF',
fontWeight: 500,
},
keyLabel: {
fontSize: 10,
fontFamily: 'Times New Roman',
fill: '#333333',
fontWeight: 400,
spacing: 10,
maxWidth: 220,
},
valueLabel: {
fontSize: 10,
fill: '#333333',
fontWeight: 400,
spacing: 10
},
}
}
};
return spec;
}
function renderChart() {
Taro.nextTick(async () => {
const spec = getChartSpec(props.dataSource);
const domRef = await getDomRef();
domRef.id = props.canvasId;
// registerWXEnv();
await vglobal.setEnv('wx', {
domref: domRef,
force: true,
canvasIdLists: [props.canvasId, `${props.canvasId}Tooltip`, `${props.canvasId}Hidden`],
freeCanvasIdx: 2,
component: undefined
})
const defaultVtChartConfig = {
mode: 'wx',
modeParams: {
force: true, // 是否强制使用 canvas 绘制
canvasIdLists: [props.canvasId, `${props.canvasId}Tooltip`, `${props.canvasId}Hidden`], // canvasId 列表
tooltipCanvasId: `${props.canvasId}Tooltip`, // tooltip canvasId
freeCanvasIdx: 2 // 自由 canvas 索引
},
renderCanvas: props.canvasId,
dpr: Taro.getSystemInfoSync().pixelRatio,
}
// chartConfig.vchart = new VChart(spec,{
chart = new VChart(spec,{
...defaultVtChartConfig,
modeParams: {
...defaultVtChartConfig.modeParams,
domref: domRef,
},
// animation: false,
renderCanvas: props.canvasId,
});
chart.renderAsync();
});
}
function updateChartBySpec(dataSource = props.dataSource) {
chart.updateSpec(getChartSpec(dataSource));
chart.renderAsync();
}
watch(() => props.dataSource, async (newVal, oldVal) => {
if (newVal) {
colorMap = setFillColorMap(newVal);
if (!chart) {
await renderChart();
} else {
updateChartBySpec(newVal);
}
}
}, {
deep: true,
immediate: true,
})
</script>
<template>
<view v-if="props.canvasId" :class="styles.lineChartContainer">
<canvas
type="2d"
:canvasId="`${props.canvasId}Tooltip`"
:id="`${props.canvasId}Tooltip`"
:class="styles.style_cs_tooltip_canvas"
></canvas>
<canvas
type="2d"
ref="chartContainer"
:canvasId="props.canvasId"
:id="props.canvasId"
height="880rpx"
width="100%"
@touchstart="handleEvent"
@touchmove="handleEvent"
@touchend="handleEvent"
></canvas>
<canvas
type="2d"
:canvasId="`${props.canvasId}Hidden`"
:id="`${props.canvasId}Hidden`"
:class="[styles.style_cs_canvas, styles.style_cs_canvas_hidden]"
></canvas>
</view>
</template>
<style scoped>
</style>
文件上传
微信小程序自带uoloadFile()的文件上传很奇怪,并不是web层面可以直接读取文件系统,而是读取微信聊天记录列表、选取上传文件。为了达到和web端一样的打开系统的文件管理,所以决定涉及到上传的页面部分,单独开发一个web项目单独部署,使用viewview嵌套进行上传处理。这里涉及到三个问题:
- web端页面要使用h5的开发规范,保证移动端的兼容性,包括px2vw处理、UI库的选择等
- web端页面和小程序的认证,可以通过webview的url传递token,来保证认证的有效性
- web端内嵌页面和小程序的通知交互,可以直接使用引入微信官方的jweixin-1.3.2.js来解决,可以直接调 wx?.miniProgram直接获取微信小程序操作
- 因为涉及到webview的内嵌,所以需要在微信开发者平台,给webview嵌套的url设置白名单
自定义分享处理
因为业务设定相关,需要拦截微信小程序自带的分享功能,配合单独的分享标题、分享的过期时间等配置,所以需要自定义分享流程,抽取公共组件。遇到两个问题:
- 停止右上角胶南里面自带的分享功能,可以使用Taro.hideShareMenu()禁用胶南的分享,同时在config.js中配置enableShareAppMessage: true开启自定义分享
- 分享配置不符合要求的时候,中断分享流程,触发表单校验,这里使用Taro的useShareAppMessage的钩子,传递分享表单的配置参数,如果表单配置不符合,可以直接在该方法中throw Error中断分享
<template>
<view
:class="styles.shareCompareInsuranceBlockContainer"
>
<view
v-if="props?.isShowShareSetting"
:class="styles.settingBtn"
@click="handleShowResultPopup"
>
<Setting
color="#999999"
:class="styles.icon"
></Setting>
<view :class="styles.msg">展示设置</view>
</view>
<view :class="[styles.shareBtnGroup, props?.shareBtnClassConfig?.outsideBtnGroupClass]">
<view :class="[styles.shareBtn, props?.shareBtnClassConfig?.outsideBtnClass]" @click="handleShowShareSettingDashBoard">
<image
:class="styles.bg"
:src="ShareBtnBg"
/>
<view :class="[styles.msg, props?.shareBtnClassConfig?.outsideBtnClass]">
{{ props?.shareBtnTextConfig?.outsideBtnText || '分享' }}
</view>
</view>
</view>
<nut-popup
key="showResultSettingPopUp"
v-model:visible="isShowResultSettingPopup"
title="展示设置修改"
position="bottom"
:catch-move="true"
:z-index="10000"
closeable
round
overlay
>
<view
v-if="isShowResultSettingPopup"
:class="styles.showResultFormGroup"
>
<view :class="styles.title">展示设置修改</view>
<slot name="showResultSetting"></slot>
</view>
</nut-popup>
<nut-popup
key="showShareSettingPopUp"
v-model:visible="isShowShareSettingActionSheet"
@choose="handleActionSheetSubmit"
:catch-move="true"
closeable
round
overlay
:style="{
width: '100vw'
}"
:z-index="10000"
position="bottom"
>
<view
v-if="isShowShareSettingActionSheet"
:class="styles.settingFormGroup"
>
<ShareSettingForm
:imageUrl="props?.shareMessageConfig?.imageUrl || ''"
ref="shareSettingFormRef"
/>
<view :class="styles.demoBlock">
</view>
<view :class="styles.warningMsg">
分享后,公众号会提醒您有谁看过
</view>
<view :class="styles.shareBtnGroup">
<view :class="[styles.shareBtn, props?.shareBtnClassConfig?.outsideBtnClass]">
<button
style="width: 100%"
open-type="share"
:class="styles.coverShareBtn"
>
<image
:class="styles.bg"
:src="ShareBtnBg"
/>
<view
open-type='share'
:class="[styles.msg, props?.shareBtnClassConfig?.outsideBtnClass]"
>
{{ props?.shareBtnTextConfig?.insideBtnText || '分享' }}
</view>
</button>
</view>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import Taro, { useShareAppMessage } from '@tarojs/taro'
import { ref, defineProps, defineExpose } from 'vue';
import styles from "./index.module.scss"
import ShareBtnBg from "@/assets/components/shareBtnBg.png";
import { Setting } from "@nutui/icons-vue-taro";
import ShareSettingForm from '@/components/form/shareSetting';
import ShareCardIcon from '@/assets/components/shareCardIcon.png';
import { useProjectStore } from '@/stores';
const projectStore = useProjectStore();
const isShowShareSettingActionSheet = ref(false);
const isShowResultSettingPopup = ref(false);
const shareSettingFormRef = ref(null);
const props = defineProps({
pageName: {
type: String,
default: '',
},
myShareSettingId: {
type: Number,
default: 0,
},
isShowShareSetting: {
type: Boolean,
default: true,
},
shareBtnClassConfig: {
type: Object,
default: () => ({
outsideBtnClass: {},
insideBtnClass: {},
outsideBtnGroupClass: {},
}),
},
customShareParams: {
type: Function,
default: () => {},
},
customSharePayload: {
type: Function,
default: () => {}
},
shareBtnTextConfig: {
type: Object,
default: () => ({
outsideBtnText: '分享',
insideBtnText: '分享'
})
},
shareMessageConfig: {
type: Object,
default: () => null,
}
})
function handleShowShareSettingDashBoard() {
isShowShareSettingActionSheet.value = true;
}
function handleActionSheetSubmit() {
}
function generateShareParams(shareParamsObj = {}) {
const result = Object.keys(shareParamsObj).reduce((preItem, currItem) => {
preItem += `${currItem}=${shareParamsObj[currItem]}&`;
return preItem;
}, '?');
return result;
}
// 存储本地分享配置
function handleSaveShareSettingInStorage(shareConfigObj) {
const value = JSON.stringify(shareConfigObj);
Taro.setStorageSync('shareConfig', value);
}
/**
* 作用: 清除本地配置
*/
function handleClearShareSettingInStorage() {
Taro.clearStorageSync('shareConfig');
}
useShareAppMessage(async (res) => {
let title = '';
let formData = {};
const shareSettingData = await shareSettingFormRef?.value?.submit();
const valid = shareSettingData.valid;
if (!valid) throw new Error();
formData = shareSettingData.formData;
title = formData?.title || title;
let shareSettingPayload = {
data: {
pageName: props.pageName,
shareTitle: formData?.title || '',
rememberNext: formData.isRemember ? 1 : 0,
expireTimeType: formData.expireTime,
isShowCard: formData.isShowPersonCard,
}
}
// 没有选择保存配置,直接强制挂空
if (!formData.isRemember) {
shareSettingPayload = {
data: {
pageName: props.pageName,
shareTitle: '',
rememberNext: 0,
expireTimeType: '3天有效',
isShowCard: 1,
}
}
}
if (!props?.myShareSettingId) {
await projectStore.addMyShareSetting(shareSettingPayload);
} else {
// 调用更新接口,覆盖当前配置
shareSettingPayload.data.id = props.myShareSettingId;
await projectStore.updateMyShareSetting(shareSettingPayload);
}
const shareParams = await props?.customShareParams(formData) || null;
const paramsStr = shareParams ? generateShareParams(shareParams) : '';
const sharePageConfig = {
title: formData?.title || props?.shareMessageConfig?.title || title,
path: `${props?.shareMessageConfig?.path || ''}${paramsStr}`,
imageUrl: props?.shareMessageConfig?.imageUrl || ShareCardIcon,
}
return sharePageConfig;
})
function handleShowResultPopup() {
isShowResultSettingPopup.value = true;
}
function updateShareSettingFormData(formData) {
if (shareSettingFormRef?.value) {
shareSettingFormRef.value.updateFormData(formData);
}
}
function handleCloseSettingPopup() {
isShowResultSettingPopup.value = false;
}
defineExpose({
handleCloseSettingPopup,
updateShareSettingFormData,
})
</script>
其他问题总结
taro中抽取公共依赖,减小分包大小
在config/index.js中,自定义webpackChain配置,覆盖原有taro的配置
optimizeMainPackage: {
enable: true,
},
webpackChain(chain) {
const taroBaseReg = /@tarojs[\/][a-z]+/;
// 配置京东按需加载
chain.plugin("unplugin-vue-components").use(
ComponentsPlugin({
resolvers: [NutUIResolver()],
})
);
// 准备抽取vchart的配置文件
chain.merge({
optimization: {
runtimeChunk: {
name: config.isBuildPlugin ? "plugin/runtime" : "runtime",
},
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
common: {
name: config.isBuildPlugin ? "plugin/common" : "common",
minChunks: 2,
priority: 1,
},
vendors: {
name: config.isBuildPlugin ? "plugin/vendors" : "vendors",
minChunks: 2,
maxSize: 2000000,
test: (module) => {
return (
/[\/]node_modules[\/]/.test(module.resource) &&
!/[\/]@visactor[\/]/.test(module.resource)
);
},
// exclude: [/[\/]@visactor/],
priority: 10,
},
// 抽取vchart部分
myVChart: {
name: config.isBuildPlugin ? "plugin/myVChart" : "myVChart",
test: (module) => {
return /[\/]@visactor[\/]vchart[\/]esm/.test(
module.resource
);
},
minChunks: 2,
priority: 10,
},
taro: {
name: config.isBuildPlugin ? "plugin/taro" : "taro",
test: (module) => {
return taroBaseReg.test(module.context);
},
priority: 100,
},
},
},
},
});
chain.merge({
module: {
rule: {
mjsScript: {
test: /.mjs$/,
include: [/pinia/],
use: {
babelLoader: {
loader: require.resolve("babel-loader"),
},
},
},
},
},
});
},
taro中自定义配置环境变量
需要将环境变量放置在defineConstants配置内部
神策数据埋点配置导致分享链接参数异常
需要注意默认配置项中的allow_amend_share_path,神策会自动插入一些埋点参数覆盖其他参数,设置为false不会覆盖分享参数