一个辅助v-model的自定义指令
有这样的需求,一个input输入框只能输入特定长度的内容,超过则无法输入。你可能会说不是有maxlength之类的属性嘛。但是maxlength只能在input[type=text]中使用,如果只能输入数字maxlength便会失效。
有没有办法干预v-model的默认行为,v-model有一些修饰符(lazy、number、trim),但是这些远远不够,vue2中也没有提供自定义修饰符的功能。怎么办呢?可以写一个自定义指令来辅助v-model,就叫它v-limit-input。
首先想到的是监听keydown事件,然后通过e.preventDefault()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div id="app"> <input type="text" v-model="value" @keydown="keyDown"> </div> </template> <script > var app = new Vue({ el:"#app", data(){ return { value:"123" } }, methods:{ keyDown(e){ e.preventDefault() } } }) </script>
|
这段代码在pc上完美运行,但是在移动端是没有用的,原因是移动端keydown事件触发时值已经输入到输入框里面了,这时再preventDefault已经晚了。所以只能替换input.value 来限制输入。
1 2 3 4 5
| onKeyDown(e){ if(e.target.value.length >= this.limit){ this.el.value = this.el.value.slice(0,this.limit) } }
|
这段代码在pc上也是完美运行,但是在移动端安卓手机上,在keydown事件中立马改input.value是不生效的,需要延迟更改。
1 2 3 4 5 6 7
| onKeyDown(e){ if(e.target.value.length >= this.limit){ setTimeout(()=>{ this.el.value = this.el.value.slice(0,this.limit) },100) } }
|
这样就可以了。但是这样做有个新问题,我们是通过手动更改dom的方式修改input.value,并且是延时更改,完美绕过了vue的响应式系统,vue并不知道input.value已经改变了。
就在我就要放弃时,忽然灵机一动,可以使用自定义事件呀。
1 2 3 4 5 6 7 8 9
| onKeyDown(e){ if(e.target.value.length >= this.limit){ setTimeout(()=>{ this.el.value = this.el.value.slice(0,this.limit) const event = new Event('input') this.el.dispatchEvent(event) },100) } }
|
这样就能触发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
| <template> <div id="app"> <input type="number" v-model="value" v-limit-input="5"> </div> </template> <script > class LimitInput{ constructor(el,limit=3){ this.el = el this.limit = parseInt(limit) this.event = new Event('input')
this.onKeyDown = this.onKeyDown.bind(this) this.el.addEventListener('keydown',this.onKeyDown) } onKeyDown(e){ if(e.target.value.length >= this.limit){ setTimeout(()=>{ this.el.value = this.el.value.slice(0,this.limit)
this.el.dispatchEvent(this.event) },100) } } destroy(){ this.el.removeEventListener('keydown',this.onKeyDown) } }
LimitInput.instances = new Map()
LimitInput.setInstance = (time,instance)=>{ LimitInput.instances.set(time,instance) }
LimitInput.getInstance = (time)=>{ LimitInput.instances.get(time) }
export default { el:"#app", data(){ return { value:"123" } }, methods:{
}, directives:{ 'limit-input':{ bind(el,{value}){ const ins = new LimitInput(el,value) const time = Date.now() el.dataset.time = time LimitInput.setInstance(time+'',ins) }, unbind(el){ const time = el.dataset.time const ins = LimitInput.getInstance(time) if(ins) ins.destroy() } } } } </script>
|
真是希望越大,失望就越大,this.el.value = this.el.value.slice(0,this.limit) 这句代码有问题。假如用户在输入框的前面或者中间输入值,替换的结果就很令人费解。卒。
关于小程序web组件与原生组件的层级问题
最近做小程序,有一些动画需求,固然小程序有自己的动画api,但是,考虑到时间问题,直接用lottie动画了。
lottie-miniprogram
虽然一些复杂动画,渲染出来效果略差,但是大多数场景下效果还可以。跟web上使用方法差不多,只能用canvas渲染。
但是这样做有个问题,组件的层级问题,在lottie动画之上,很可能出现modal、toast之类的自定义组件(小程序自带的太难看),而canvas作为原生组件,层级是高于web组件的,为了解决这个问题,微信小程序推出了同层渲染,懂原生的可以去网上搜一下相关技术,貌似android/ios各自有各自的实现。
jsx1 2 3 4 5
| <Canvas type={'2d'} canvasId={'xxx'}> <View> <Text>cover</Text> </View> </Canvas>
|
写了一段Taro的伪代码,像上面那样,cover会覆盖在原生组件canvas之上。但是大多数场景,基于代码的可读性,我不想把某个modal写在一个毫不相干的canvas内部,还有没有其他办法,当然有。
jsx1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <Canvas type={'2d'} id={'animate'} canvasId={'animate'} ref={ref=>{ }}> </Canvas> <Canvas type={'2d'} id={'xxx'} canvasId={'xxx'}> <Modal> <View> <Text> modal content </Text> </View> </Modal> </Canvas>
|
像上面那样,既然原生组件的层级高于web组件,那么就把modal组件变成原生组件(基于同层渲染),这样做可以解决一部分问题,但又迎来了新问题,套在canvas内部的modal组件在出现时是没有动画的。
假如是裸的modal组件,出现时该怎么添加动画,可以参照taro-ui的做法。
modal.scss
出乎意料,用的是css的transition,挺简洁的思路。这样做,在android上是没有问题的。但是在iOS上,给canvas设置了visibility: hidden,canvas并不会消失,而是像幽灵一样盖在页面上,使页面上的其他元素不可交互(可能是android/ios实现同层渲染的技术细节不一样导致的)。
这可怎么办,最后还是利用css解决了。办法就是给Modal外面的Canvas绝对定位,然后宽高都为0,使其在视觉上消失,但在template代码层次上,还是符合同层渲染的规则。这样就解决了以上问题。
偶遇一段js代码
最近在学java(进度真龟速),看到java的try catch,发现有段代码很奇怪,用js写出来是这样的
1 2 3 4 5 6 7 8 9 10 11 12
| function test(a){ try { a == 1 return 1 } catch (e){ return 2 } finally { return 3 } }
console.log(test(1));
|
结果是3,这还真是我曾经遇到的一道面试题。这么多年过去了,我不知道这道题的答案对我的技术有啥影响。事实上,我根本就不会写出这样的代码,想都想不到,这些出题人的出题角度真奇怪。不问真正有用的知识,反而是通过一些奇奇怪怪的东西来“筛选”人。
突然想到一个面试题
前几天写小程序有一场景是这样的:首页分为好几个模块,每个模块的数据分别由不同的接口负责,这种场景很自然的想到Promise.all,速度快,还能很好的控制loading。但是Promise.all有个特性就是一旦某个promise reject了,整个all任务都会reject,在这种场景下Promise.all的这种特性显然是不能接受的。
试想一下,假设有10个promise,第二个reject了,剩下8个全不执行了,这样的体验确实不太好。那怎么改进呢,我们需要自己造一个Promise.all,需要达到这样的效果,假设某个promise reject了,不结束整个任务,但是记录错误信息。
这让我想起了一道面试题,面试官的意思大概是:有一个批量上传的tasks,假设某个任务上传失败了,不影响其他的任务,等上传结束后,需要知道成功与失败的情况。
上代码
1 2 3 4 5 6 7 8 9 10
| Promise.friendlyAll = (promises=[])=>{ const newPromises = promises.map((promise,index)=>{ return promise.then(res=>{ return Promise.resolve({err:null,data:res}) }).catch(err=>{ return Promise.resolve({err:err,data:null}) }) }) return Promise.all(newPromises) }
|
java学习笔记
初始化arrayList
java里的arrayList其实就是js里的“数组“,js是没有真正意义的数组的。那java怎样初始化一个arrayList,标椎的语法是这样的
1 2 3 4
| List<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); System.out.println(arrayList);
|
好难受,就不能像js那样 const array = [“a”,”b”,”c”] 在定义变量的时候初始化一堆元素吗?经过一番搜索还真有这样的api,需要这样写
1 2
| List<String> arrayList = Arrays.asList("a","b"); System.out.println(arrayList);
|
看起来没什么问题,但是只要试图操作arrayList就会报错
1 2 3
| List<String> arrayList = Arrays.asList("a","b"); arrayList.remove("b"); System.out.println(arrayList);
|

意思是说,Arrays.asList返回一个固定大小的列表,所以必须这样写
1 2 3
| List<String> arrayList = new ArrayList<>(Arrays.asList("a","b")); arrayList.remove(1); System.out.println(arrayList);
|
不小心创建的git submodule
事情是这样的。一个项目本来是我独立维护的(项目A,有自己的.git),结果被告知需要放到别的项目内部,然后copy大法,整个目录搬过去,想都没想git add,后果就是这个目录会被add成一个git submodule,然后病急乱投医,下意识的删掉项目A下的.git目录。
这样问题就出现了。如果不执行其他的操作,这样push上去的代码项目A还是一个submodule,需要执行git rm –cached 子模块名称,注意子模块名称要写完整路径(假如项目A目录嵌套较深的话),执行完这条命令后,git会认为项目A是个正常的目录,而不是submodule,但是webstorm还是会把它当成submodule,需要打开Settings -> Version Control -> Directory Mappings 删掉多余的映射,这样webstorm也正常了。
上述做法由于直接删除了项目A的.git目录,所以项目A以前的 git history会全部丢失,如果不想丢失,参见这个问题
微信小程序线上出问题了
事情是这样的。还是小程序(Taro写的),有这样一个场景,小程序冷启动时需要先调一个接口(login)拿用户的权限以及其他必要的信息(类似于自动登录),后续接口http header里需要带上这些信息。这在网页里太正常不过了,但是小程序和普通的单页面web应用不一样,小程序需要配一个入口页,假设是pages/index/index,冷启动时app.js和index几乎是同时执行的。
网上推荐的做法都是在app.js的onLaunch里自动login,通过上面的分析可以得出,app.js和index的执行顺序是无法保证的。有没有其他办法呢,当然有,你可能需要一个空页面充当“路由器”(就叫launch好了),把入口页换成这个,在这个页面里自动登录然后再redirectTo。实际执行过程中这个页面会一闪而过,时间取决于login接口的快慢,所以最好在上面放点东西装饰装饰。
有一些场景是必须保证顺序的,比如通过外部的带参小程序码跳入某一特定页面(就叫pages/foo/foo),以前都是直接配置二维码跳转链接为pages/foo/foo,现在全都先进到launch,再由launch通过二维码参数决定进到哪个页面。
小程序有个比较坑的地方,就是小程序没有发布,通过微信自带的扫一扫不能打开小程序。为此,我们专门提供了内部扫码,即先通过wx.scanCode,然后还是跳转到launch,这样进入launch的方式就有两种,在launch的onShow周期里我写了这样一段代码
经过后来仔细的看文档,发现未发布的小程序也能扫码进入,只不过后端生成二维码时需要指定env_version=trial,文档在这里
1 2
| const {query} = Taro.getLaunchOptionsSync() const {scene,page} = Object.keys(query).length === 0 ? Taro.getCurrentInstance().router.params : query
|
乍一看,没啥问题,优先检查getLaunchOptionsSync有没有参数,如果没有,再检查launch的路由信息。实际上Taro会缓存getLaunchOptionsSync的参数,只要没有再launch,后续再访问Taro.getLaunchOptionsSync().query都是有值的,所以把逻辑反过来就可以了。
1 2 3
| const {query} = Taro.getLaunchOptionsSync() const {params} = Taro.getCurrentInstance().router const {scene,page} = Object.keys(params).length === 0 ? query : params
|
尝鲜android jetpack compose
听说android那边也开始流行声明式UI了,而且强制kotlin,不管怎样,先找个demo跑起来。网上偶遇一大神的TodoList demo,可惜没有源码。所以只能照着人家的视频,咔咔咔手敲代码,经过一番思想斗争(kotlin语法还是太陌生),代码终于没有错误了,但是却编译不通过

看错误,好像说是什么依赖的版本不一致,但是作为新手,我又不知道在什么地方改,此处省略一万字。最后发现我机器上的kotlin版本和build.gradle plugins里声明的kotlin版本不一致,改成一样就可以了。


这时又报了另一个错,说是compose版本与kotlin版本不一致,有了前车之鉴,这次顺利多了,于是搜到这个网站 ,问题解决。
如何在小程序里(taro)使用antv f2
为什么要用antv f2,其他的像echarts、ucharts之类的不行吗?相比于后两个,f2的文档友好,专为移动端而生,并且体积也小。
f2官网有个“如何在小程序中使用”的示例,不过这个指的是在原生小程序中如何使用f2,和taro的用法还不太一样。网上有人写了一个兼容taro的库,taro-f2 ,看起来挺美好的,但是看commit三年都没更新了,issues里也是各种与taro3不兼容的声音。
抱着试一试的态度,下载taro-f2并运行,报了各种错,抱着不见黄河不落泪的态度,去看F2Canvas的实现。
代码不是很长,如果不考虑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 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
| import Taro from '@tarojs/taro' import {Canvas} from '@tarojs/components' import PropTypes from 'prop-types'; import Renderer from './lib/renderer'; import './f2-canvas.css';
interface F2CanvasPropTypes { onCanvasInit: (canvas: any, width: number, height: number, $scope: any) => any, }
function randomStr (long: number): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const maxPos = chars.length; var string = ''; for (var i = 0; i < long; i++) { string += chars.charAt(Math.floor(Math.random() * maxPos)); } return string; }
export default class F2Canvas extends Component<F2CanvasPropTypes> { static defaultProps = { onCanvasInit: () => {}, }; static propTypes = { onCanvasInit: PropTypes.any, };
state = { width: '100%', height: '100%;', };
id: string = 'f2-canvas-' + randomStr(16); canvas: any; static fixF2: (F2: any) => any;
componentWillMount () { if (process.env.TARO_ENV !== 'h5' ) { setTimeout(()=>{ const query = Taro.createSelectorQuery() query.select('#'+this.id).boundingClientRect().exec(res => { const ctx = Taro.createCanvasContext(this.id, this.$scope); const canvasWidth = res[0].width; const canvasHeight = res[0].height; const canvas = new Renderer(ctx, process.env.TARO_ENV); this.canvas = canvas; this.props.onCanvasInit(canvas, canvasWidth, canvasHeight, this.$scope); }); },1) } }
componentDidMount () { }
componentWillUnmount () { }
componentDidShow () { }
componentDidHide () { }
touchStart(e){ if (this.canvas) { this.canvas.emitEvent('touchstart', [e]); } } touchMove(e){ if (this.canvas) { this.canvas.emitEvent('touchmove', [e]); } } touchEnd(e){ if (this.canvas) { this.canvas.emitEvent('touchend', [e]); } } press(e){ if (this.canvas) { this.canvas.emitEvent('press', [e]); } }
htmlCanvas(canvas){ if(!canvas) return; setTimeout(() => { this.canvas = canvas; this.props.onCanvasInit(canvas, canvas.offsetWidth, canvas.offsetHeight, this.$scope) }, 1) }
render () { const id = this.id; if (process.env.TARO_ENV === 'h5') { return <canvas ref={this.htmlCanvas.bind(this)} style={{ width: this.state.width, height: this.state.height }} className={'f2-canvas ' + id}></canvas> } if (process.env.TARO_ENV !== 'h5') { return <Canvas style={'width: '+this.state.width+'; height:'+this.state.height} className='f2-canvas' canvasId={id} id={id} onTouchStart={this.touchStart.bind(this)} onTouchMove={this.touchMove.bind(this)} onTouchEnd={this.touchEnd.bind(this)} onLongPress={this.press.bind(this)} />; } } }
|
明白了原理,再来看为什么报错,首先我用的是taro vue语法,他这个组件明显是react写的,很明显不兼容,而且即使是react,他这个写法也早就不兼容了,所以我把f2-canvas.tsx大概翻译了一下,搞了个f2-canvas.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 79 80 81 82 83 84 85 86 87 88 89
| <template> <canvas :style="{width,height}" class='f2-canvas' :canvasId="id" :id="id" :onTouchStart="touchStart" :onTouchMove="touchMove" :onTouchEnd="touchEnd" :onLongPress="press" /> </template> <script> import Taro from '@tarojs/taro' import Renderer from "./renderer";
function randomStr (long) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const maxPos = chars.length; var string = ''; for (var i = 0; i < long; i++) { string += chars.charAt(Math.floor(Math.random() * maxPos)); } return string; }
export default { data(){ return { width: '100%', height: '100%;', id: 'f2-canvas-' + randomStr(16), TARO_ENV: process.env.TARO_ENV } }, props:{ onCanvasInit:{ type: Function, default: ()=>{ return ()=>{} } } }, beforeMount() { if (process.env.TARO_ENV !== 'h5' ) { setTimeout(() => { const query = Taro.createSelectorQuery() query.select('#' + this.id).fields({node:true,size:true}).exec(res => {
const ctx = Taro.createCanvasContext(this.id); const canvasWidth = res[0].width; const canvasHeight = res[0].height; const canvas = new Renderer(ctx, process.env.TARO_ENV); this.canvas = canvas; this.onCanvasInit(canvas, canvasWidth, canvasHeight); }); }, 300) } }, methods:{ touchStart(e){ if (this.canvas) { this.canvas.emitEvent('touchstart', [e]); } }, touchMove(e){ if (this.canvas) { this.canvas.emitEvent('touchmove', [e]); } }, touchEnd(e){ if (this.canvas) { this.canvas.emitEvent('touchend', [e]); } }, press(e){ if (this.canvas) { this.canvas.emitEvent('press', [e]); } } } } </script> <style> :host, .f2-canvas{ width: 100%; height: 100%; } </style>
|
renderer.ts
1 2 3 4 5 6 7 8 9 10 11
| import Renderer from 'taro-f2/dist/weapp/components/f2-canvas/lib/renderer'
export default class CustomRenderer extends Renderer { addEventListener(t,e){ this.addListener(t,e) } removeEventListener(t,e){ this.removeListener(t,e) } }
|
这样就可以像taro-f2例子里那样用了,还有一点要注意,文档里有这一行:
看一下这个fixF2做了什么,源码
看起来像是打了一些补丁,查阅小程序文档,貌似从基础库2.9.0开始缺失的方法已经补齐了。所以不加fixF2也可以。
这样f2就可以在小程序里用了,不过这种方法也有瑕疵,f2-canvas中的canvas组件不能用2d模式,略遗憾。
2022.09.22 补充2d模式
taro-f2小程序端的实现完全照搬了wx-f2,而后者github上的demo是支持2d模式的,代码 。再来看看初始化chart的代码片段
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
| ready() { const query = wx.createSelectorQuery().in(this); query.select('.f2-canvas') .fields({ node: true, size: true }) .exec(res => { const { node, width, height } = res[0]; const context = node.getContext('2d'); const pixelRatio = wx.getSystemInfoSync().pixelRatio; node.width = width * pixelRatio; node.height = height * pixelRatio;
const config = { context, width, height, pixelRatio }; const chart = this.data.onInit(F2, config); if (chart) { this.chart = chart; this.canvasEl = chart.get('el'); } }); }
onInitChart(F2, config){ const chart = new F2.Chart(config); }
|
看到没有,初始化Chart时只需提供context即可,这样连Renderer都省了,这样就可以摆脱taro-f2,最终的代码
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
| <template> <view :style="{height:'200px'}"> <canvas :style="{width,height}" class='f2-canvas' :id="id" type="2d" /> </view> </template> <script> import Taro from "@tarojs/taro"; import F2 from "@antv/f2/lib/index-all";
function randomStr (long) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const maxPos = chars.length; var string = ''; for (var i = 0; i < long; i++) { string += chars.charAt(Math.floor(Math.random() * maxPos)); } return string; } export default { data(){ return { width: '100%', height: '100%;', id: 'f2-canvas-' + randomStr(16), } }, onReady(){ const query = Taro.createSelectorQuery() query.select('#' + this.id).fields({node:true,size:true}).exec(res => {
const { node, width, height } = res[0]; const context = node.getContext('2d'); const pixelRatio = Taro.getSystemInfoSync().pixelRatio;
node.width = width * pixelRatio; node.height = height * pixelRatio;
const config = { context, width, height, pixelRatio };
this.drawPie(F2, config); }); }, methods:{ drawPie(F2,config){ const data = [ { category:'蛋白质', value: 62.8, proportion: 17.7, recommended_ratio: '10-15', a:'1' }, { category:'脂肪', value: 22.5, proportion: 6.4, recommended_ratio: '10-15', a:'1' }, { category:'碳水化合物', value: 268.8, proportion: 75.9, recommended_ratio: '10-15', a:'1' } ] const chart = new F2.Chart(config); chart.source(data) chart.coord('polar',{ transposed: true }) chart.legend(false) chart.axis('value',{ label(){} }) chart.pieLabel({ sidePadding: 40, label1(data,color){ return { text: data.value, color } }, label2(data,color){ return { text: data.category, color } } }) chart.interval().position('a*value').adjust('stack').color('category',['#1890FF', '#13C2C2', '#2FC25B'])
chart.render() }, } } </script>
|
完美。
2022.11.13 补充高版本antv f2用法
之前的实现看起来挺完美的,实际上有两个隐患:
隐患1:上述用法只适用于3.x版本的f2,而最新的版本都是4.x,低版本的bug官方不再维护了。
隐患2:基于隐患1,确实发现了一些问题,所有touch相关的操作都有bug,例如tooltip功能。
所以又抱着试一试的态度,尝试在微信小程序里引入4.x版本的f2。经过几天的挣扎,终于成功了。先吐槽一下阿里,f2实际上是支持小程序的,只不过文档混乱,3.x版本与4.x版本关于小程序的兼容竟然是两份代码,并且处在不同的仓库,也没有任何说明。
3.x
4.x
4.x关键代码 , 只看这一行,其他的和3.x差不多。作者封装了个原生组件,如果要在taro内部使用,直接引这个原生组件就可以了。4.x最大的亮点就是支持jsx,我们要在上层代码中通过onRender把jsx转换成可运行js,wx-f2调用onRender然后生成canvas即可。
antv/f2暴露出一个createElement方法,然后我们在上层代码中这样写就可以了
1 2 3 4 5
| <f2 onRender={()=>{ return createElement(Line,{ data:data }) }}>
|
实际上这种写法不太行,有两个原因,一个是数据都是异步返回的(除非data是写死的),f2 ready执行时data基本上是空的,另一个原因是,在taro里面引入原生组件props是不支持函数的,参见这个issues 。所以要么像issue里面那样欺骗编译器,在运行时动态传递函数,要么用自定义事件通讯。我觉得后者好一点,来看代码
f2原生组件的实现
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
| import { Canvas } from '@antv/f2';
function wrapEvent(e) { if (!e) return;
if (!e.preventDefault) { e.preventDefault = function () {}; }
return e; }
Component({
data: {}, ready: function ready() { var _this = this;
var query = wx.createSelectorQuery().in(this); query.select('.f2-canvas').fields({ node: true, size: true }).exec(function (res) { var _res$ = res[0], node = _res$.node, width = _res$.width, height = _res$.height; var context = node.getContext('2d'); var pixelRatio = wx.getSystemInfoSync().pixelRatio;
node.width = width * pixelRatio; node.height = height * pixelRatio;
var canvas = new Canvas({ pixelRatio: pixelRatio, width: width, height: height, context: context, }); canvas.render(); _this.canvas = canvas; _this.canvasEl = canvas.canvas.get('el'); _this.triggerEvent("init", { canvas }); }); }, observers: { '**': function _() { var canvas = this.canvas, data = this.data; if (!canvas) return; var children = data.onRender(data); canvas.update({ children: children }); } }, lifetimes: { detached: function detached() { var canvas = this.canvas; if (!canvas) return; canvas.destroy(); } },
methods: { click: function click(e) { var canvasEl = this.canvasEl;
if (!canvasEl) { return; }
var event = wrapEvent(e);
event.touches = [e.detail]; canvasEl.dispatchEvent('click', event); }, touchStart: function touchStart(e) {
var canvasEl = this.canvasEl;
console.log(e,canvasEl)
if (!canvasEl) { return; }
canvasEl.dispatchEvent('touchstart', wrapEvent(e)); }, touchMove: function touchMove(e) { var canvasEl = this.canvasEl;
if (!canvasEl) { return; }
canvasEl.dispatchEvent('touchmove', wrapEvent(e)); }, touchEnd: function touchEnd(e) { var canvasEl = this.canvasEl;
if (!canvasEl) { return; } canvasEl.dispatchEvent('touchend', wrapEvent(e)); } } });
|
上游代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default ()=>{ return ( <View style={{ height: Taro.pxTransform(300) }}> <f2 onInit={({detail})=>{ const {canvas} = detail //也可以保存canvas,在必要的时候update canvas.update({ children: createElement(Line,{ data: chartData }) }); }}/> </View> ) }
|
上游line组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import {Chart, Axis, Line, Legend,Tooltip} from '@antv/f2';
export default (props) => { const { data } = props; return ( <Chart data={data}> <Axis field="date" tickCount={5} type={'timeCat'} /> <Axis field="cost" tickCount={5} /> <Line x="date" y="cost" color="type" /> <Legend position="bottom" clickable={true} /> <Tooltip/> </Chart> ); };
|
比上次更完美一点。
2022.11.14 针对昨天的成果进行补充
昨天的做法看起来挺好的,但是目前只能用于Taro react,原因就是编译jsx出现了问题,react是用babel编译jsx的,而vue为了兼容jsx,搞了个@vue/babel-preset-jsx,这玩意会把line组件编译成functional component,编译结果大概长这样
1 2 3 4
| { "functional": true, "render": ()=>{} }
|
把这玩意再交给f2的createElement处理,内部直接报错了。所以要么不要在taro vue中用f2,要么不要用jsx。
有没有更好的解决方案,当然有,我们可以写两份babel config,一份针对普通的js文件,一份针对chart函数,为了把chart函数与普通的js文件彻底分开,自定义一个chart文件,比如上面的line组件(line.js),现在就叫line.chart。然后改一下根目录的babel.config.js
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { presets: [ ['taro', { framework: 'vue', ts: true }] ], exclude:[ /\.chart$/ ] }
|
config/index.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
| const config = { mini:{ webpackChain(chain,webpack){ chain.merge({ module: { rule: { chart: { test: /\.chart$/, use: [{ loader: 'babel-loader', options: { presets: [ ['taro', { framework: 'vue', ts: true, vueJsx:false }] ], plugins:[ [ '@babel/plugin-transform-react-jsx', { "runtime": "automatic", "importSource": "@antv/f2" } ] ] } }] } } } }) }, } }
|
这样就好了。
2022.11.20 更新
目前来说,在微信小程序中使用antv f2解决方案已经稳定了,考虑过写一个npm 包来给taro使用,毕竟网上那个已经n久不更新了,但是回头又想写这个东西意义不大,毕竟f2官方已经给出了原生组件demo,把这玩意再翻译成taro代码,taro还分vue与react版本,最后写出来一大堆重复代码,所以我决定仅仅贴一下我目前的代码可能更好。
原生组件
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
| import { Canvas } from '@antv/f2';
function wrapEvent(e) { if (!e) return;
if (!e.preventDefault) { e.preventDefault = function () {}; }
return e; }
Component({
properties:{ childrenData:{ type: Object } }, data: { id:Math.random().toString(36).slice(-8) }, ready: function ready() { var _this = this; setTimeout(()=>{ var query = wx.createSelectorQuery().in(this); query.select(`#f2-canvas-${this.data.id}`).fields({ node: true, size: true }).exec(function (res) { var _res$ = res[0], node = _res$.node, width = _res$.width, height = _res$.height; var context = node.getContext('2d'); var pixelRatio = wx.getSystemInfoSync().pixelRatio;
node.width = width * pixelRatio; node.height = height * pixelRatio; var canvas = new Canvas({ pixelRatio: pixelRatio, width: width, height: height, context: context, children: _this.data.childrenData }); canvas.render(); _this.canvas = canvas;
_this.canvasEl = canvas.canvas.get('el'); }); },300) }, observers: { '**': function _() { if (!this.canvas) return; this.canvas.update({ children: this.data.childrenData }); } }, lifetimes: { detached: function detached() { var canvas = this.canvas; if (!canvas) return; canvas.destroy(); } },
methods: { click: function click(e) { const canvasEl = this.canvasEl; if (!canvasEl) { return; } const event = wrapEvent(e); const { detail, target } = e; const { x, y } = detail; const { offsetLeft = 0, offsetTop = 0 } = target; event.touches = [{ x: x - offsetLeft, y: y - offsetTop }]; canvasEl.dispatchEvent('click', event); }, touchStart: function touchStart(e) {
var canvasEl = this.canvasEl;
if (!canvasEl) { return; }
canvasEl.dispatchEvent('touchstart', wrapEvent(e)); }, touchMove: function touchMove(e) { var canvasEl = this.canvasEl;
if (!canvasEl) { return; }
canvasEl.dispatchEvent('touchmove', wrapEvent(e)); }, touchEnd: function touchEnd(e) { var canvasEl = this.canvasEl;
if (!canvasEl) { return; }
canvasEl.dispatchEvent('touchend', wrapEvent(e)); } } });
|
1 2 3 4 5 6 7 8 9 10 11 12
| <canvas type="2d" class="f2-canvas" id="{{'f2-canvas-'+id}}" bindtap="click" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" > <slot></slot> <!-- 不加slot会报一个没有slot的警告--> </canvas>
|
1 2 3 4
| .f2-canvas { width: 100%; height: 100%; }
|
上游代码(vue),别忘了babel配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <view class="charts"> <f2 :children="charChildren"/> </view> </template> <script> export default { data(){ return { charChildren:null } }, created(){ fetch().then(res=>{ this.charChildren = createElement(LineChart,{ data: res }) }) } } </script>
|
react的写法大同小异。
前两天写chartgpt web clinet,遇到了一点问题
我们知道chartgpt官网有个很好的效果就是,ai回答的文字是一个字一个字蹦出来的,这并不是前端刻意用css模拟的效果。
由于chartgpt不对中国开放,虽然可以科学上网,但是由于使用的人太多了,问答老是error,后来听说用api访问很稳定,事实也是如此。于是咔咔咔经过几天的奋斗终于完成了一个简单的问答界面,结果被同事吐槽响应太慢了。
后端并不是我的强项,然而chartgpt的api是支持流(stream)的,一旦与chartgpt的服务器连接,响应结果会逐个token吐给前端,刚才说的一个字一个字蹦出来的效果就是利用这一特性做的,这样不仅提高了用户体验还加快了访问速度。
请求chartgpt api用到了EventSource 这项技术,简言之,这个api会提供一个message钩子,token就是通过这个逐一吐给前端的。前端实现也很简单,以前是等所有结果全部返回来一次渲染出来,现在需要在message里不停的拼字符串,然后setState。
然而实际使用中发现ai的回答老是丢字,最初以为是markdown渲染器的问题,在换了好几个markdown渲染器,但是问题依然存在后,开始考虑是我代码有问题,毕竟没有开启stream时是没有这个问题的。
后来经过一番挣扎,发现是react setState自动合并导致的问题,我们知道在react里面多次调用setState,react会合并这些更新,然后只执行一次更新,实际项目里也没人会刻意多次调用setState,但是这个stream太快了,在一次event loop里竟然有2到3次setState,这样react为了性能考虑,自然会合并更新,这就是丢字的原因。
知道了原因,解决方案也很简单,就是让setState不发生合并,据观察,同一个event loop里的seState会发生合并,用setTimeout把每次的setState分开,就不会发生合并了(react18之前在setTimeout里面的多个setState是同步执行的,不会发生更新合并,react18即使是setTimeout里面的更新也是会合并的)。但是一次更新只渲染一个字有点太浪费了,有没有办法能控制stream的频率,这种场景rxjs再适合不过了(一直久仰大名,这次终于找到独一无二的应用场景了)。
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
| let sub const source = new Observable(obsrver=>{ fetchEventSource(`${HOST_URL}/api/chat`,{ ...opts, signal: controller.signal, onopen(){ obsrver.next({ type:'open' }) }, onmessage(message){ if (message.data === '[DONE]'){ obsrver.next({ type:'complete', msgId, conversationId, reply }) obsrver.complete() controller.abort();
} else { const msg = JSON.parse(message.data) if(typeof msg === 'string'){ obsrver.next({ type:'message', message: msg, }) } else { msgId = msg.messageId || '' conversationId = msg.conversationId || '' reply = msg.response } } }, onerror:obsrver.error, onclose:()=>{ sub.unsubscribe() source = null } }) }) sub = source.pipe(bufferCount(10)).subscribe({ next:()=>{}, error:()=>{} })
|
用bufferCount缓存stream,每10次缓存一次,然后吐给下游。
如何从小米云存储中提取出视频
最近遇到一点事,有个变态和合租房半年前已经搬走的室友有纠纷,关键是这人明知人已经搬走了,还继续骚扰住在这个地址的其他人。骚扰方法就是一半夜的在美团跑腿上这一类平台上下单,随便编个故事让骑手去敲门,很大力的那种,直到里面有人开门,大晚上的把人吵醒开门去给骑手解释,关键是这人脑子有病,都说了他找的人已经搬走了,还让把人找出来,这让人怎么找。况且,我没有义务配合他以这种方式“找人”,要不是多次经历这个事,我绝不相信现在这个所谓的法治社会还存在这样的事情。
你大半夜的爬起来跟这种人出去耗,本来就烦人,过一会他通过同样的方法换个骑手又来了,一晚上能来好几次,有一段时间不来了,在你快把他忘了的时候又来了,简直就是精神折磨,中国的基层民警处理这种事总是让人“眼前一亮”,最后我决定惹不起躲得起,早些走人。
租的自如的房子,合同没到期走人算违约,这种事怎么会是租客违约,最后双方协商提供一些证据看能不能申请无违约金换租,幸好,我们门上有摄像头(以防万一提前挂上去的)。
再说一下这个小米摄像头的云存储,介绍的时候挺华丽的,等你用到它的时候会发现它保存的视频只能在特定app上观看,能下载,但是无法导出,呵呵,不能导出为什么要下载,还好天无绝人之路,在android手机的以下路径可以找到下载的视频:
1
| Android/data/com.xiaomi.smarthome/files/1027072538
|
找到以后发现是一堆类型为ts的视频文件,好吧,只能先弄到电脑上然后合并了。
合并的过程中试了好多软件都失败了,后来发现ts片段里面有个key文件,于是怀疑这些视频是加密的,去问chatgpt如何合并加密的ts视频,chatgpt说用以下命令:
1
| ffmpeg -i input1.ts -i input2.ts -encryption_key file:keyfile.key -c copy output.ts
|
由于我的片段很多,这样一个一个的得写到猴年马月去,于是换了个命令:
1
| ffmpeg -f concat -safe 0 -i concat.txt -c copy output.ts
|
可以写个脚本扫描某文件夹下的文件,生成这条命令
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
| const fs = require('fs'); const path = require('path'); const { exec } = require('child_process');
const folderPath = './sources/92876675426027008'; const keyFileName = '1027072538.key';
const keyFilePath = path.join(__dirname,folderPath, keyFileName); if (!fs.existsSync(keyFilePath)) { console.error(`Error: Key file ${keyFileName} not found.`); return; }
const concatFilePath = path.join(__dirname,folderPath, 'concat.txt'); fs.writeFileSync(concatFilePath, '');
fs.readdir(folderPath, (err, files) => { if (err) throw err;
const tsFiles = files.filter((file) => path.extname(file) === '.ts'); if (tsFiles.length < 2) { console.error('Error: Not enough TS files found.'); return; }
const concatFileContent = tsFiles.map((file) => `file '${path.join(__dirname,folderPath, file)}'`).join('\n'); fs.writeFileSync(concatFilePath, concatFileContent);
const outputFilePath = path.join(__dirname,folderPath, 'output.ts'); const ffmpegCommand = `ffmpeg -f concat -safe 0 -i "${concatFilePath}" -encryption_key file:"${keyFilePath}" -c copy "${outputFilePath}"`;
console.log('ffmpegCommand',ffmpegCommand);
console.log(`Merging ${tsFiles.length} TS files with ${keyFileName}...`); exec(ffmpegCommand, (error, stdout, stderr) => { if (error) { console.error(`Error: ${error.message}`); return; } if (stderr) { console.error(`Error: ${stderr}`); return; } console.log(`Success: ${stdout}`); });
});
|
为了方便沟通,可以把合并完了的ts文件转成mp4
1
| ffmpeg -i input.ts -c:v libx264 -preset slow -crf 22 -c:a copy output.mp4
|
有chatgpt就是方便,以前需要自己去试错的,现在只要给它描述自己的想法,让它去试错,效率大大提升。