uni-app、Vue3 + ucharts 图表 H5 无法渲染

文章已收录到 github,欢迎 Watch 和 Star。
简介
从问题定位开始,到给框架(uni-app)提 issue、出解决方案(PR),再到最后的思考,详细记录了整个过程。
前序
当你在业务中不幸踩了开源框架的某些坑,这是你的不幸,但这同时也是你的幸运,因为这是你给自己简历中增加亮点的绝佳机会。
而给开源社区贡献 PR 是你证明自己技术侧拥有 P7 实力的绝佳方式,P7 的评判标准无非是业务和技术,业务上有收益,技术上有深度和广度(别人有的你能做的更好,别人没有的你能有)。
这次整个过程历时 3-4 天,在此之前我也没读过 uni-app 和 ucharts 的源码,所以这里把整个过程分享出来也是给大家一个解决问题的思路。
环境
- uni-app cli 版本 3.0.0-alpha-3030820220114011
- hbuilder 版本 3.3.8.20220114-alpha
- ucharts 版本 uni-modules 2.3.7-20220122
现象
uni-app、vue3 + ucharts 绘制图表,开发环境正常,但是打包上线后,H5 无法绘制图表,也不报任何错误。
| 开发 | 线上 | |
|---|---|---|
| APP | 正常 | 正常 |
| H5 | 正常 | 无法绘制 |
问题定位
给 ucharts 的社区提 issue,经过交流,维护者 “怀疑“ 是 uni-app 的 vue3 的 renderjs 有问题,但是他也给不了一个肯定的答复,让去 uni-app 的社区提 issue 而且示例中不能用 ucharts。个人对于该回答持怀疑态度,于是决定自己去定位问题。
怀疑是 ucharts 的 bug
- ucharts 视图部分的关键代码
<view ...其它属性 :prop="uchartsOpts" :change:prop="rdcharts.ucinit">
<canvas ...属性 />
</view>
这里有一个知识点需要补充:当 prop 发生改变,change:prop 的回调会被调用,这是 uni-app 框架提供的能力,但官方文档没有提及,从源码中可以看到。
- 看了 ucharts 的源码,绘制图表时的代码执行过程如下:

可是打包后的 H5 线上环境,当执行 this.uchartsOpts = newConfig 之后却没有触发 change:prop 事件,所以这看起来似乎是 uni-app 的 view 组件有问题
感谢 ucharts 官方,在定位问题过程中,和社区进行交流后,ucharts 免费赠送了一个永久超级会员,感谢 !!
view 组件的 prop 和 change:prop
提供如下示例:
<template>
<view>
<view :prop="counter" :change:prop="changeProp"></view>
<view>{{ msg }}</view>
</view>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
const counter = ref(1)
const msg = ref('hello')
function changeProp() {
msg.value = 'hello' + counter.value
}
// @ts-ignore
let timer = null
onMounted(() => {
timer = setInterval(() => {
counter.value += 1
}, 1000)
})
onBeforeUnmount(() => {
// @ts-ignore
clearInterval(timer)
})
</script>
<style>
</style>
| H5 开发环境 | H5 打包后 | |
|---|---|---|
| vue2 | 正常 | 正常 |
| vue3 | 正常 | change:prop 未执行 |
因为开发环境没有问题,所以在开发环境中通过在 change:prop 方法中打断点,查看调用栈,找到触发 change:prop 回调的方法,再一步步往上看,终于发现了 uni-app 重写渲染器(render 函数)的地方,在 @dcloudio/uni-h5-vue/dist/vue.runtime.esm.js 中。
通过阅读 uni-app 的源码,得到如下内容:
响应式数据发生变化,触发 vue 的响应式更新。比如你的响应式数据作为元素的 prop 属性传递,则在 patch 阶段会触发 patchProps 方法, 触发该方法后,方法内判断新老 props 是否发生改变,如果变了,则遍历新的 props 对象,将其中的每个属性、值和老的对比,如果不相等 或者 props 的 key 为 change:xx 则直接调用 patchProp 方法,如果 __UNI_FEATURE_WXS__为真并且 props 的 key 为 change: 开头,则调用 patchWxs,patchWxs 方法最终会通过 nextTick 调用 change:prop 的回调方法。
以下为上述执行过程的流程图:

最终定位到问题就出在 __UNI_FEATURE_WXS__上,发现开发环境中它是 true,但是打包后就变成了 false。
__UNI_FEATURE_WXS__
__UNI_FEATURE_WXS__是一个全局变量,所以肯定是通过 vite 的 define 选项进行设置的。
于是接下来的目的就是需要找到 __UNI_FEATURE_WXS__是在什么地方进行设置的。可以全局搜该变量,然后找到在 @dcloudio/uni-cli-shared 包中找到一个叫 initFeatures 的方法,该方法中声明了一个 features 对象:
const {
wx,
wxs,
// ...其它变量
} = extend(
initManifestFeature(options),
// ... 其它方法
)
const features = {
// vue
__VUE_OPTIONS_API__: vueOptionsApi, // enable/disable Options API support, default: true
__VUE_PROD_DEVTOOLS__: vueProdDevTools, // enable/disable devtools support in production, default: false
// uni
__UNI_FEATURE_WX__: wx, // 是否启用小程序的组件实例 API,如:selectComponent 等(uni-core/src/service/plugin/appConfig)
__UNI_FEATURE_WXS__: wxs, // 是否启用 wxs 支持,如:getComponentDescriptor 等(uni-core/src/view/plugin/appConfig)
// ... 其它属性
}
看了该对象的设置没什么问题,wxs在开发和生产环境下都是 true。那接下来就需要找到谁调用了 initFeatures 方法,而且可能调用完了以后通过判断当前命令,比如:执行 build 时,将 __UNI_FEATURE_WXS__设置为了 false。
刚开始想正向推导。vite-plugin-uni 是 uni-app 提供给 vite 的一个插件框架,uni-app 中的 vite 配置都来自于这里。
插件当中的 uni 插件提供了 config 选项,config 选项的值是调用 createConfig 方法返回的函数,该函数会返回一个对象,该对象会和 vite 的配置做深度合并;该对象有 define 选项,该选项的值为 createDefine 函数的返回值,该返回值是一个对象,其中调用了 initDefine,再往下看发现不对,然后路 走死了。
发现上面正向推导的方式走不通以后,于是开始反向推导,即全局搜索,都有哪些地方调用了 initFeatures,然后一步步的往下推,得到如下正确的流程图:

经过最终的调试,发现 启动开发环境和打包时最终的调用路径是:uniH5Plugin -> createConfig -> configDefine -> initFeatures。
而最终的问题也就是出在了 initFeatures 方法调用的 initManifestFeature 方法中。
答案
最终定位到出问题的地方在 @dcloudio/uni-cli-shared/src/vite/features.ts 文件的 initManifestFeature 方法中。有如下对比:
- github 仓库的最新代码,版本号:3.0.0-alpha-3030820220114011
if (command === 'build') {
// TODO 需要预编译一遍?
// features.wxs = false
// features.longpress = false
}
- 已发版的代码,最高版本号:3.0.0-alpha-3031120220208001
if (command === 'build') {
// TODO 需要预编译一遍?
features.wxs = false;
features.longpress = false;
}
已发版的版本居然高于仓库内的最新版本号。查看 npm 上的发布版本信息:

发现版本号发生了回退。这几次回退的版本号都是不符合规范的版本号,而且其中可能携带了 bug,比如上面提到的最高版本。
发版出现版本号不符合规范的情况是由于项目还没有一个规范的发版流程导致的,但是已经是 alpha 版本了,这种低级错误还是应该避免的。
更致命的操作是,回退版本号。uni-app 目前每次升级都是升级的最小版本号后面的数值,而业务项目的 package.json 都是 "@dcloudio/uni-app": "^xxx" 的形式,这就意味着,你每次重新装包(比如自动化部署时)或者升级包时,都会更新到这个存在 bug 的高版本,这就会导致线上系统报 bug。
解决方案
所以这里正确的处理方式是重新发一个更高版本的包,而不是回退版本。因为该操作会导致用户线上的系统出 bug,即以下代码无法正常执行:
<view :prop="msg" :change:prop="cb"></view>
当正常情况下,当 msg 改变后,change:prop 的回调会执行。但是这个携带 bug 的高版本包,在打包时(npm run build)将 __UNI_FEATURE_WXS__设置为了 false,导致 change:prop 的回调不会被调用。
总结
代码可以回退,但是版本号不要回退,应该基于当前稳定版本,重新发一版版本号更高的版本。
于是就给官方提了 issue 和 解决方案。
结果
官方已采纳该解决方案,基于当前稳定版重新发布一版版本号更高的版本。
思考
针对 uni-app 这种处于 alpha 版本的框架,项目内部也确实不应该继续使用 ^ 符号,还是应该将版本号写死为最新的 tag 版本,因为总跟随 alpha 的最新版,确实可能会踩坑。
链接

文章已收录到 github,欢迎 Watch 和 Star。
uni-app、Vue3 + ucharts 图表 H5 无法渲染的更多相关文章
- Hybrid APP之Native和H5页面交互原理
Hybrid APP之Native和H5页面交互原理 Hybrid APP的关键是原生页面与H5页面直接的交互,如下图,痛过JSBridge,H5页面可以调用Native的api,Native也可调用 ...
- 关于APP,原生和H5开发技术的争论 APP开发技术选型判断依据
关于APP,原生和H5开发技术的争论 App的开发技术,目前流行的两种方式,原生和Html5.原生分了安卓平台和ios平台(还有小众的黑莓.死去的塞班就不说了),H5就是Html5. 目前争论不休的问 ...
- APP中内嵌H5页面为什么不能下载?
在APP中内嵌H5页面,若页面上存在下载链接,没有任何反应,为什么呢? 原因是app中内嵌的H5页面是WebView解析的,什么是WebView呢? 在Android手机中内置了一款高性能webkit ...
- Hybrid APP基础篇(三)->Hybrid APP之Native和H5页面交互原理
本文已经不维护,新地址: http://www.cnblogs.com/dailc/p/8097598.html 说明 Hybrid模式原生和H5交互原理 目录 前言 参考来源 前置技术要求 楔子 A ...
- uni app中使用自定义图标库
项目中难免会用到自定义图标,那在uni app中应该怎么使用呢? 首先, 将图标目录放在static资源目录下: 在main.js中引入就可以全局使用了 import '@/static/icon-o ...
- 前端提升生产力系列三(vant3 vue3 移动端H5下拉刷新,上拉加载组件的封装)
| 在日常的移动端开发中,经常会遇到列表的展示,以及数据量变多的情况下还会有上拉和下拉的操作.进入新公司后发现移动端好多列表,但是在看代码的时候发现,每个列表都是单独的代码,没有任何的封装,都是通过v ...
- uni app 零基础小白到项目实战-1
uni-app是一个使用vue.js开发跨平台应用的前端框架. 开发者通过编写vue.js代码,uni-app将其编译到Ios,android,微信小程序等多个平台,保证其正确并达到优秀体验. Uni ...
- android电子书App、自定义图表、仿腾讯漫画App、仿淘宝优惠券、3D选择容器等源码
Android精选源码 仿支付宝记账本功能,饼状图:数字键盘 android一款功能完善的电子书应用源码 Android自定义图标库,使用方便,扩展性强 android 3D立体无限旋转容器源码 an ...
- 关于APP,原生和H5开发技术的争论
App的开发技术,目前流行的两种方式,原生和Html5.原生分了安卓平台和ios平台(还有小众的黑莓.死去的塞班就不说了),H5就是Html5. 目前争论不休的问题,在早先前争论CS,BS架构的软件系 ...
随机推荐
- spring boot 使用 mybatis 开启事务回滚 的总结
1.前言 以前没有使用mybatis,可以关闭自动提交,然后做sql操作,对操作进行catch捕获异常, 如果没有异常则commit 提交 ,有异常则 rollback 回滚,新增的数据则删除 ,修改 ...
- vue - public 引入 <script>报错 Uncaught SyntaxError: Unexpected token '<'
1.现象 原本我是直接在母版引入 <script type="application/javascript" src="static/config.js" ...
- vue中computed的作用以及用法
在vue中computed是计算属性,主要作用是把数据存储到内存中,减少不必要的请求,还可以利用computed给子组件的data赋值. 参考地址:https://www.jianshu.com/p/ ...
- vue 因为使用scope后选择器和标签出现[data-v
使用scope的以后出现datd-v,例如: <div data-v-2311c06a class="button-warp"> <button data-v-2 ...
- 自动化集成:Pipeline整合Docker+K8S
前言:该系列文章,围绕持续集成:Jenkins+Docker+K8S相关组件,实现自动化管理源码编译.打包.镜像构建.部署等操作:本篇文章主要描述流水线集成K8S用法. 一.背景描述 分布式服务的部署 ...
- leetcode 206. 反转链表 及 92. 反转链表 II
206. 反转链表 问题描述 反转一个单链表. 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1-> ...
- JuiceFS 在理想汽车的使用和展望
理想汽车是中国新能源汽车制造商,设计.研发.制造和销售豪华智能电动汽车,于 2015 年 7 月创立,总部位于北京,已投产的自有生产基地位于江苏常州,通过产品创新及技术研发,为家庭用户提供安全及便捷的 ...
- tarjan全家桶
tarjan 全家桶 关于tarjan 它太强了 CCCOrz dfs树&low dfs树:在图上做不重复经过同一点的dfs,经过的边与点形成一棵树.于是图上所有点都被这棵树包含,一部分边被包 ...
- Docker 与 K8S学习笔记(十九)—— Pod的配置管理
我们在部署应用时常常会考虑将应用程序与配置文件相分离,这样可以使应用程序更好的复用,并且通过不同配置也能实现更灵活的功能.将应用制作成镜像后,我们可以在启动容器时通过环境变量或挂载文件的方式注入,但是 ...
- Java如何对一个对象进行深拷贝
Java如何对一个对象进行深拷贝? Posted by Wudashan on October 14, 2018 深拷贝实现代码:https://github.com/wudashan/java-de ...