- 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - 你不知道的小程序系列之生命周期执行顺序
再次开始之前先问几个问题: 你是否知道[代码]Page[代码]生命周期 与 [代码]pagelifetimes[代码] 生命周期执行顺序? 你是否知道[代码]behaviors[代码]中的生命周期与组件生命周期执行顺序? 你是否知道[代码]Page[代码]生命周期 与 组件[代码]pagelifetimes[代码]生命周期执行顺序? 要回答上面的问题,首先我们看看小程序生命周期有哪些: App onLaunch onShow onHide Page onLoad onShow onReady onHide onUnload Component created attached ready moved detached 想一下加载一个页面(包含组件)的加载顺序,按照直觉小程序加载顺序应该是这样的加载顺序(以下列子中[代码]Component[代码]都是同步组件): App(onLaunch) -> Page(onLoad) -> Component(created) 但其实并不然,小程序的加载顺序是这样的: 首先执行 [代码]App.onLaunch[代码] -> [代码]App.onShow[代码] 其次执行 [代码]Component.created[代码] -> [代码]Component.attached[代码] 再执行 [代码]Page.onLoad[代码] -> [代码]Page.onShow[代码] 最后 执行 [代码]Component.ready[代码] -> [代码]Page.onReady[代码] 其实也不难理解微信这么设计背后的逻辑,我们先看下官方的的生命周期: [图片] 可以看到,在页面[代码]onLoad[代码]之前会有页面[代码]create[代码]阶段,这其中就包含了组件的初始化,等组件初始化完成之后,才会执行页面的[代码]onLoad[代码], 之后页面[代码]ready[代码]事件也是在组件[代码]ready[代码]之后才触发的。 下面我们来看看 [代码]Behavior[代码], [代码]Behavior[代码] 与 [代码]Vue[代码]中的 [代码]mixin[代码] 类似,猜想下其中的执行顺序: Behavior.created => Component.created 测试下来和预期相符,其实在[代码]Vue[代码]的文档中有一段这样的描述: 另外,混入对象的钩子将在组件自身钩子之前调用。 这样的设计和主流设计保持一致。接下来我们看看 [代码]pageLifetimes[代码],有[代码]show[代码]和[代码]hide[代码]生命周期对应页面的展示与隐藏,预期的执行顺序: pageLifetime.show => Page.onShow 测试下来也和预期相符,那么我们可以推断出如下的结论: 当页面中包含组件时,组件的生命周期(包括pageLifetimes)总是优先于页面,[代码]Behaviors[代码]生命周期优先于组件的生命周期。但其实有个例外:页面退出堆栈,当页面[代码]unload[代码]时会执行如下顺序: Page.onUnload => Component.detached 看了以上的分析你应该知道了答案,最后做个总结(demo): [图片] 最后的最后布置个作业 异步组件(异步渲染的组件,通常是通过if条件判断是否渲染)的生命周期执行顺序是怎样的,pagelifetimes会不会执行?
2020-01-10 - 云开发可以做 企业付款可以到零钱吗?
企业付款到零钱功能提供由商户付款至用户微信零钱的能力 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 云开发可以做 企业付款可以到零钱吗?
2020-08-07 - 小程序音视频合成初探
小程序音视频 最近使用了一下微信音视频相关 api, 这绝对是一件振奋人心的事, 让视频的合成在小程序端就能完成, 以下是我使用这些 api 的一些记录。 以下代码片段可以直接在开发者工具中预览 视频合成 音视频合成主要用到 [代码]wx.createMediaContainer()[代码] 方法, 该方法会返回一个 [代码]MediaContainer[代码] 对象, 可以将视频源传入到容器中, 对视频轨和音频轨进行一些操作。 下面以合成视频为例: [代码]const mediaContainer = wx.createMediaContainer(); const { tempFilePath } = this.data; // 本地视频文件地址 // 将视频源传入轨道当中 mediaContainer.extractDataSource({ source: tempFilePath, success: (res) => { // 返回的结果中的 tracks 是一个类数组对象, 第一项是音频轨道, 第二项是视频轨道 const [audioTrack, mediaTrack] = res.tracks; mediaContainer.addTrack(mediaTrack); // 将视频轨道加入到待合成容器中 mediaTrack.slice(1000, 5000); // 截取视频轨道中第 1-5 秒的视频 mediaContainer.addTrack(audioTrack); // 将音频轨道加入到待合成容器中 audioTrack.slice(1000, 5000); // 截取音频轨道中第 1-5 秒的视频 // 导出合成容器中的音频和视频 mediaContainer.export({ success: (res) => { // 拿到导出之后的视频 console .log(res.tempFilePath); }, }); }, }); [代码] 视频解码和录制 如果想要给视频加滤镜和贴图可以采用, [代码]VideoDecoder[代码] + [代码]MediaRecorder[代码] + [代码]WebGL[代码] 的方式, 通过 [代码]VideoDecoder[代码] 将视频解码, 获取视频的每一帧画面, 再绘制到 [代码]canvas[代码] 上, 再通过 [代码]glsl[代码] 着色器给画面加滤镜。同时用 [代码]MediaRecorder[代码] 去录制 [代码]canvas[代码] 上的画面, 最后可以导出一段视频。 以下是将一个视频的前十秒加上黑白滤镜合成出来的主要代码: [代码]let w = 300 let h = 200 const vs = ` attribute vec3 aPos; attribute vec2 aVertexTextureCoord; varying highp vec2 vTextureCoord; void main(void){ gl_Position = vec4(aPos, 1); vTextureCoord = aVertexTextureCoord; } ` const fs = ` varying highp vec2 vTextureCoord; uniform sampler2D uSampler; #ifdef GL_ES precision lowp float; #endif void main(void) { vec4 color = texture2D(uSampler, vTextureCoord); float gray = 0.2989*color.r + 0.5870*color.g + 0.1140*color.b; gl_FragColor = vec4(gray, gray, gray, color.a); } ` const vertex = [ -1, -1, 0.0, 1, -1, 0.0, 1, 1, 0.0, -1, 1, 0.0 ] const vertexIndice = [ 0, 1, 2, 0, 2, 3 ] const texCoords = [ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 ] function createShader(gl, src, type) { const shader = gl.createShader(type) gl.shaderSource(shader, src) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Error compiling shader: ' + gl.getShaderInfoLog(shader)) } return shader } function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") if (!gl) { console.error('Unable to get webgl context.') return } const info = wx.getSystemInfoSync() gl.canvas.width = width //info.pixelRatio * width gl.canvas.height = height // info.pixelRatio * height gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) const vertexShader = createShader(gl, vs, gl.VERTEX_SHADER) const fragmentShader = createShader(gl, fs, gl.FRAGMENT_SHADER) const program = gl.createProgram() gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Unable to initialize the shader program.') return } gl.useProgram(program) const texture = gl.createTexture() gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, texture) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.bindTexture(gl.TEXTURE_2D, null) buffers.vertexBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buffers.vertexBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW) buffers.vertexIndiceBuffer = gl.createBuffer() gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.vertexIndiceBuffer) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndice), gl.STATIC_DRAW) const aVertexPosition = gl.getAttribLocation(program, 'aPos') gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(aVertexPosition) buffers.trianglesTexCoordBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buffers.trianglesTexCoordBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW) const vertexTexCoordAttribute = gl.getAttribLocation(program, "aVertexTextureCoord") gl.enableVertexAttribArray(vertexTexCoordAttribute) gl.vertexAttribPointer(vertexTexCoordAttribute, 2, gl.FLOAT, false, 0, 0) const samplerUniform = gl.getUniformLocation(program, 'uSampler') gl.uniform1i(samplerUniform, 0) return (arrayBuffer, width, height) => { gl.bindTexture(gl.TEXTURE_2D, texture) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } wx.createSelectorQuery().select('#video').context(res => { const video = this.video = res.context // 获取 VideoContext video.pause() const decoder = wx.createVideoDecoder() // 创建解码器 const dpr = 1 const webgl = wx.createOffscreenCanvas() const render = createRenderer(webgl, w, h) console.log('webgl', webgl.id, w, h, dpr, webgl.width, webgl.height) const fps = 25 const recorder = wx.createMediaRecorder(webgl, { fps, videoBitsPerSecond: 3000, timeUpdateInterval: 30 }) const startTime = Date.now() let counter = 0 let loopStopped = false let timeCnt = 0 setTimeout(() => { recorder.stop(); decoder.stop(); }, 3000); function loop() { const renderTime = (counter + 1) * (1000 / fps) if (loopStopped || renderTime > 10100) { console.log('recorder stop.', timeCnt, Date.now() - startTime) recorder.stop() return } const ts = Date.now() const imageData = decoder.getFrameData() if (imageData) { render(new Uint8Array(imageData.data), w * dpr, h * dpr) timeCnt += Date.now() - ts counter++ } console.log('render end', counter, Date.now() - ts); recorder.requestFrame(loop) } recorder.on('start', () => { console.log('start render') loopStopped = false loop() }) recorder.on('timeupdate', ({ currentTime }) => { console.log('timeupdate', currentTime) }) recorder.on('stop', (res) => { console.log('recorder finished.', timeCnt, Date.now() - startTime, res) this.setData({ distSrc: res.tempFilePath }) recorder.destroy() }) decoder.on('start', () => { console.log('decoder start 2', decoder.seek) decoder.on('seek', () => { console.log('decoder seeked') recorder.start() }) decoder.seek(0) }) decoder.start({ source: this.data.src, // 这里是一个视频的本地路径, 可通过 wx.chooseVideo 获取 }) }).exec() [代码] 不过此时录制出来的视频是没有声音, 需要通过上面讲到的 [代码]MediaContainer[代码] 截取前 10s 的音频, 将录制出的视频和音频合成得到一段完整的视频。[代码]CanvasRenderingContext2D[代码] 的 [代码]drawImage[代码] 方法 2.10.0 起支持传入通过 [代码]SelectorQuery[代码] 获取的 video 对象, 所以对视频的操作可以先使用 canvas 预览, 再使用 [代码]MediaRecorder[代码] 进行录制。 小程序音视频相关 api 就介绍到这儿, 更多具体的文档, 可以参考微信官方文档。这些 api 大大方便了开发者对音视频的处理, 也期待更多小程序音视频 api 的开放。
2020-07-01 - 总结的一些血与泪的教训
云函数 云函数的上传流程 云函数必须先保存后上传,因为编译器不会在上传的时候自动保存。 保存的方法可以是CTRL+S或者是编译都可以,CTRL+SHIFT+S和编译是等效的;上传的方式可以是对文件的增量更新或者说部署云函数。主要是顺序必须先保存后上传。 云函数部署对于触发器并没有效果,触发器得单独上传(仍然需要先编译)。 如何知道云函数是否执行出错了 云开发→云函数→日志,读取日志,可以读取服务器返回的数据,所以云函数中最好用try{}catch(e){}结构,然后return JSON.stringify(e),当然你不这样也没问题,因为服务器会自动吧错误写进日志 云函数中console.log()的内容会写在日志的末尾。 当然,你也可以使用云函数的本地调试功能(来自Littlesnail大佬的分享),但是本地环境是缺少wx-server-sdk这个包的,需要自己用npm装,大家可以自行衡量。 如何知道云函数是否真正执行完了 之前一直在python平台的我根本没接触过node.js这种异步事件驱动的语言,被异步快整疯了 首先说结论,你得把所有返回Promise类型的方法都加上await关键字,并函数变为async类型。 所谓的Promise类型是指的一个虚的对象,他传递给某些函数后,对那些函数进行一个“保证”,保证过会会给那些函数数据。因此那些函数会先等着,等Promise返回数据后,提醒这些函数开始工作了,然后上一个工作完提醒下一个直到函数执行完。因为有些操作,比如数据库的读取是需要时间的,所以要是一直等着会阻塞整个线程。(node.js似乎是单线程的,这算是一直对多线程的补偿方案吧) Promise对象有三种状态。Pending,表示正在运行中;fulfilled,表示完成然后会自动提醒下一个函数;rejected,表示失败,当然如果你用try catch了就没事。 重点来了,对于async的云函数来说,执行到最后或者return就相当于结束了,然而这个时候如果函数内仍然有处于“保证中”(Pending)的Promise对象,云函数是不会等它执行完才返回的,它会直接返回。所以你必须加一个关键字,await,表示下面的语句都会等着这个异步操作完成再工作,也就是阻塞住。这样才能真正使得云函数完全执行完。 如何判断什么函数返回的是Promise对象,在开发者工具的console里面输入一下这个函数就行了,实在不行typeof。 PS:以前我曾经想过,如果吧云函数的async标签去掉,是否能实现不需要await也能正常运行呢,答案是,你想的美,去掉后人家该Promise还是Promise,不会给你变成现实(特朗普:恶意的zz隐喻,举报了)。所谓的async标签是指的我警告你我这玩意是异步的,你能用then,但是大多数情况你都不会去用then,所以,老老实实await吧。 你们就不要挣扎了,老老实实吧所有出现异步的位置都async和await吧 你标了async的函数,调用它的函数也得加async哦,而且调用的时候也得await 【async-await地狱】 小细节 云函数的运行环境中你不能获取到函数的信息,this.name,this.toString(),或者window.decodePathName都是undefine 数据库 为什么有时候数据库的get没用请确定你的权限是否有误,在 云开发控制台-数据库-权限设置 中可以调整,一般来说选第一个权限 为什么有时候数据库的update没用 首先当然是排查是不是因为在云函数中你却没有加await,数据库的几乎所有操作全是异步的,返回的基本都是Promise对象,另一点是小程序中的db.collection('seats')对象是动态对象,你不能这样写 db_seats=db.collection('seats') db_seats.where({}) 如果你在下面的语句继续用db_seats来直接对数据库做一些操作,相当于你创建了一个数据库的本地副本,你get()数据当然是有用的,但是可能不是最新的数据,但是你add()和update()都是没用的。你必须每次都用db.collection('xxx')来调用xxx数据库。 (实际上后来经过调查发现,add有时候是可以的,但是update保证每次都不行,不知道为啥) ————(以下为更新内容)———— 事实上还有一种更加广泛的错误 一定要注意,doc函数查到的是数据库的索引的_id字段,而不是_openid字段,你拿openid去doc一定找不到东西。 所以如果你一开始只有openid这个数据,那你最好先用where找到对应数据的_id,然后再doc它的id (为什么不直接where后接update?where返回的是一个数组,where接update相当于一次性修改一组数据,只有服务器端,也就是云函数有这个权限,小程序的前端只能先查询再修改) 数据库传入的数据 只有数据库的where(),update(),add(),传入的JSON数据中,只有data是必选属性,其他属性像是success,fail都是非必要的,如果你不用错误处理的话就不用加 then怎么用 数据库的then(),或者说所有的异步操作的then,你把函数放在then()里面表示到时间后完成某项操作,比如 //获取所有data const seats_list = await db.collection('seats').where({ used: true }) .get().then(res => { // return res.data console.log(res.data) }) 相当于吧收到的参数命名为res,然后等res异步返回后,会在=>{}里面处理一些只跟res有关的操作,这当然是有用的,但是问题在于then里面是一个闭包,你console.log当然是可以的,但是你要在里面用this.setData()就不行了,这个闭包的this可不是指的你这个外面的对象,他就是指的它点前面的对象,也就是get()后返回的对象。 所以想在外面用的话,正确操作是吧注释去掉,也就是return res或者return res.data,然后这样的话,外面的seats_list 就能被赋值为res或者res.data了,然后接下来的语句尽情对seats_list 操作,千万别忘了加await,否则你懂的。 WXML 为啥我的wx:if不管用 请注意一件事,那就是。。。WXML里面的任何函数部分,都是不能有空格的,比如这样写 点击选座位 点击选座位 点击选座位 点击选座位 点击选座位 无论你空格加在哪里,只要被""括起来,加空格都会使得wx:if失效,相信小伙伴们此时已经反应过来了,没错,这就是JS那个傻逼机制,非空字符串相当于true > Boolean(" ") > true 我猜wx:if的机制只是简单的做了下拆分,然后配对了下,并不能实际把它当程序运行(甚至可能正则都没用),所以如果遇到了它没拆成功的字符串,它就干脆暴力返回原字符串,当然被识别为true了。 整体 Js的所有变量都需要预先var数据库的Data和云函数的Data JS的Date()函数很迷,它返回的是tm一个String,当前时间的String,你必须用var new才能构造一个真正的Date对象而不是一个字符串,也就是按照下面方法使用,加注释的都是错误的用法。 var nowdate=new Date() // Date().getDate() // Date(一个Date对象).getDate() // Date(一个字符串).getDate() 另外就是无论你在哪调用这个函数,他创建的都是你调用端的时间,而不是云端的时间,就算你在云函数调用也一样,你电脑或者手机是几点他创建的就是几点,我都服了。 云端的时间只能用db.serverDate()来构建,但是问题在于db.serverDate()是tmd一个指令,他返回的不是一个Date,而是类似db.commend那种类型的东西,给传入服务器的data用。 //更新nowdate为服务器时间 await db.collection('seats').where().update({ data:{ nowdate:db.serverDate() } 也就是你平时用是没JB用的,你只能老老实实去用JS的Date方法请求世界时然后倒时区。 总结 以后想到了继续补充,欢迎大佬分享
2020-05-11 - 小程序云函数中调用 db.serverDate() 获取的值, 如何格式化到 yyyy-mm-dd?
如题,已经测试了一些方法,但都没有办法获取到想要的格式。 查看数据库给db.serverDate() 的返回值类型为 Date,以为可以调用 getFullYear() 等方法,没想到提示报错,db.serverDate(...).getFullYear is not a function 如果使用 new Date 替代 db.serverDate(),使用下面的函数可以获取到想要的格式,但是有时区问题。 function processDate(_date) { var y = _date.getFullYear(); var m = _date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = _date.getDate(); d = d < 10 ? ('0' + d) : d; return y + '-' + m + '-' + d; }; 发现 db.serverDate 返回值可以准确获取正确时区(我们有个开发人员在北美,云函数中db.serverDate() 能准确获取到他那边的时区)。 谁能分享一下 db.serverDate 的格式处理方法么,谢谢
2020-03-11 - 浅析growingio无埋点数据采集的实现原理——剧情版
1. 背景 我厂开发的小程序最近接入了付费产品growingio,号称可以实现无埋点采集用户行为,包含用户操作、页面访问、停留时间等,可直接追踪用户的使用路径。由于我厂用的原生小程序开发方式,所有接了他们的原生小程序的sdk。接入方式也挺简单: [代码]import gio from 'path/to/giosdk' gio('setConfig', ourConfig) App({ // xxx }) [代码] 编译后,看到network里各种搜集到的数据被上传,满心欢喜,心想:哟! 果然是付费的,省心。万事大吉,打完收工!结果,意向不到的事情发生了… 我厂只考虑微信小程序,曾用过wepy,mpvue,效果都不太理想,后来大神空降,自己撸了一套适用原生小程序的框架,事件处理已经有一套封装,然后现在接了gio后就开始糟了😅。 2. 入坑 首先,其他功能测试期间,发生了莫名奇妙的问题:什么视频暂停不了、定时器停不掉、音频播放停不下来,各种匪夷所思。后仔细排查,发现是由于小程序生命周期函数onLoad执行了两次,我插!这什么鬼,在排除我们自身原因后,发现跟接了gio有关,注释gio代码,执行一次,开启就执行2次😟 其次,用户操作事件数据确有上传,但是一次点击,竟然产生了多次数据,导致很多重复。 咋办?产品的🔪还架在脖子上,砍需求,砍需求是不可能的这样子。只能去看gio sdk的源码了,看能否找到解决方案。结果还没开始,就遇到大坑,gio sdk是闭源项目,找他们的对接人又一直不提供项目源码,想想也是,毕竟指着卖钱呢,于是乎,只能在压缩混淆后的代码找办法了 3. 强行排查 首先采用代码反压缩,把压缩后的代码转成勉强能看的代码,其实只是格式化了下,变量名和写法仍然是压缩的表现,就像这种: [代码]if ( VdsInstrumentAgent.initPlatformInfo(gioGlobal.platformConfig), VdsInstrumentAgent.observer = t, VdsInstrumentAgent.pageHandlers.forEach(function (t) { VdsInstrumentAgent.defaultPageCallbacks[t] = function () { VdsInstrumentAgent.observer.pageListener(this, t, arguments) } }), VdsInstrumentAgent.appHandlers.forEach(function (t) { VdsInstrumentAgent.defaultAppCallbacks[t] = function () { VdsInstrumentAgent.observer.appListener(this, t, arguments) } }), gioGlobal.platformConfig.canHook ) { const t = gioGlobal.platformConfig.hooks; t.App && !gioGlobal.growingAppInited && (App = function () { return VdsInstrumentAgent.GrowingApp(arguments[0]) }, gioGlobal.growingAppInited = !0), t.Page && !gioGlobal.growingPageInited && (Page = function () { return VdsInstrumentAgent.GrowingPage(arguments[0]) }, gioGlobal.growingPageInited = !0), t.Component && !gioGlobal.growingComponentInited && (Component = function () { return VdsInstrumentAgent.GrowingComponent(arguments[0]) }, gioGlobal.growingComponentInited = !0), t.Behavior && !gioGlobal.growingBehaviorInited && (Behavior = function () { return VdsInstrumentAgent.GrowingBehavior(arguments[0]) }, gioGlobal.growingBehaviorInited = !0) } [代码] 简直神清气爽,😓 没办法,在说了N句卧槽后,只能按住自己躁动不安的心,强行阅读源码。 最开始很好奇为什么,只写那两行代码,竟然就能实现数据收集,事件不是要在wxml里面写bindtap之类的吗?怎么不写就能知道我点了呢?难道有什么方法知道我写的bindtap的函数,怎么实现的呢?还有onShow,onLoad生命周期函数之类,就那两行,它怎么知道什么时候执行了onShow? 4.实现原理 重写Page,App方法: [代码]Page() { return VdsInstrumentAgent.GrowingPage(arguments[0]); } App() { return VdsInstrumentAgent.GrowingApp(arguments[0]); } VdsInstrumentAgent.GrowingPage = function (t) { return t._growing_page_ = !0, VdsInstrumentAgent.originalPage(VdsInstrumentAgent.instrument(t)) } VdsInstrumentAgent.GrowingApp = function (t) { return t._growing_app_ = !0, VdsInstrumentAgent.originalApp(VdsInstrumentAgent.instrument(t)) } /* * VdsInstrumentAgent.originalPage * VdsInstrumentAgent.originalApp * 重写前的Page和App */ [代码] 处理Page、App的参数,如果是函数,处理函数,并且提供默认的生命周期函数 [代码]VdsInstrumentAgent.instrument = function (t) { for (let e in t){ if("function" == typeof t[e]){ t[e] = this.hook(e, t[e]); } }; return t._growing_app_ && VdsInstrumentAgent.appHandlers.map(function (e) { t[e] || (t[e] = VdsInstrumentAgent.defaultAppCallbacks[e]) }), t._growing_page_ && VdsInstrumentAgent.pageHandlers.map(function (e) { t[e] || e === gioGlobal.platformConfig.lisiteners.page.shareApp || (t[e] = VdsInstrumentAgent.defaultPageCallbacks[e]) }), t; } [代码] 处理函数时,如果函数的第一个参数存在,并且有currentTarget或者target的属性,并且函数type属性(鸭式辩型),且是需要捕获的事件(“onclick”, “tap”, “longpress”, “blur”, “change”, “submit”, “confirm”, “getuserinfo”, “getphonenumber”, “contact”),,就增加监听函数,用于捕获事件,这里便收集了用户的操作事件。 [代码]hook: function (t, e) { return function () {t let i, n = arguments ? arguments[0] : void 0; // 收集用户操作事件 if (n && (n.currentTarget || n.target) && -1 != VdsInstrumentAgent.actionEventTypes.indexOf(n.type)){ try { VdsInstrumentAgent.observer.actionListener(n, t); } catch (t) { console.error(t) } } const o = gioGlobal.platformConfig.lisiteners.app, s = gioGlobal.platformConfig.lisiteners.page; if ( // 非生命周期函数直接调用 this._growing_app_ &&t !== o.appShow ? (i = e.apply(this, arguments)): this._growing_page_ && -1 === [s.pageShow, s.pageClose, s.pageLoad, s.pageHide, s.tabTap].indexOf(t) ? (i = e.apply(this, arguments)) : this._growing_app_ || this._growing_page_ || (i = e.apply(this, arguments)), // 需要收集的App生命周期函数 this._growing_app_ && -1 !== VdsInstrumentAgent.appHandlers.indexOf(t)) { try { VdsInstrumentAgent.defaultAppCallbacks[t].apply(this, arguments) } catch (t) { console.error(t) } t === o.appShow && (i = e.apply(this, arguments)) } // 需要收集的Page生命周期函数 if (this._growing_page_ && -1 !== VdsInstrumentAgent.pageHandlers.indexOf(t)) { let n = Array.prototype.slice.call(arguments); i && n.push(i); try { VdsInstrumentAgent.defaultPageCallbacks[t].apply(this, n) } catch (t) { console.error(t) } - 1 !== [s.pageShow, s.pageClose, s.pageLoad, s.pageHide, s.tabTap].indexOf(t) ? (i = e.apply(this, arguments)) : setShareResult(i) } return i } } [代码] 生命周期函数的处理,也在这里进行统一监听,defaultAppCallbacks,defaultPageCallbacks里面存的是各种监听函数 [代码]// VdsInstrumentAgent.pageHandlers // pageHandlers: ["onLoad", "onShow", "onShareAppMessage", "onTabItemTap", "onHide", "onUnload"], VdsInstrumentAgent.pageHandlers.forEach(function (t) { VdsInstrumentAgent.defaultPageCallbacks[t] = function () { VdsInstrumentAgent.observer.pageListener(this, t, arguments) } }), // VdsInstrumentAgent.appHandlers // appHandlers: ["onShow", "onHide", "onError"], VdsInstrumentAgent.appHandlers.forEach(function (t) { VdsInstrumentAgent.defaultAppCallbacks[t] = function () { VdsInstrumentAgent.observer.appListener(this, t, arguments) } }), [代码] 监听器里处理事件的分发 [代码]pageListener(t, e, i) { const n = gioGlobal.platformConfig.lisiteners.page; if ( t.route || (t.route = this.info.getPagePath(t)), e === n.pageShow) { const e = getDataByPath(t, "$page.query"); Utils.isEmpty(e) || "quickApp" !== gioGlobal.gio__platform || this.currentPage.addQuery(t, e), this.isPauseSession ? this.isPauseSession = !1 : (this.currentPage.touch(t), this.useLastPageTime && (this.currentPage.time = this.lastPageEvent.tm, this.useLastPageTime = !1), this.sendPage(t)) } else if (e === n.pageLoad) { const e = i[0]; Utils.isEmpty(e) || "quickApp" === gioGlobal.gio__platform || this.currentPage.addQuery(t, e) } else if (e === n.pageHide) this.growingio._observer && this.growingio._observer.disconnect(); else if (e === n.pageClose) this.currentPage.pvar[`${this.currentPage.path}?${this.currentPage.query}`] = void 0; else if (e === n.shareApp) { let e = null, n = null; 2 > i.length ? 1 === i.length && (i[0].from ? e = i[0] : i[0].title && (n = i[0])) : (e = i[0], n = i[1]), this.pauseSession(), this.sendPageShare(t, e, n) } else if ("onTabItemTap" === e) { this.sendTabClick(i[0]) } } [代码] 根据生命周期函数名,处理onShow、onHide之类,并传入需要的参数。这里便处理了用户的操作路径的数据,比如:进入某个页面、退出某个页面、在哪个页面调了分享、点了tab之类。 至此,大致的运行原理已经明白了。 5. 解决问题 点击一次按钮产生多次数据:根据上面的运行原理,可知gio事件收集是根据函数的第一个参数来判断的,由于我们内部框架有事件的统一封装,粗略代码如下: [代码]events: { test1: 'fnTest1', test2: 'fnTest2', test3: 'fnTest3', test4: 'fnTest4', }, bindEvent(e){ let id = e.target.dataset.id; if (id in this.events){ this[this.events[id]].call(this, e) } }, fnTest1(e){ console.log(e.target.dataset.id) }, fnTest2(e) { console.log(e.target.dataset.id) }, fnTest2(e) { console.log(e.target.dataset.id) }, fnTest3(e) { console.log(e.target.dataset.id) } [代码] bindEvent是wxml里面统一写的事件方法,根据dataset-id来分发事件按,这里如果点击了test1,按照gio的收集原理,会搜集bindEvent,fnTest1,产生2次数据,而我们最终想要的是fnTest1。在实际情况下,由于bindEvent里还有其他封装,导致数据不止2次。知道原因,这个问题就很好解决,我们为事件对象(第一个参数)增加了一个growingIgnore属性,内部统一封装的事件对象growingIgnore = true,再修改gio,上面运行原理第3步,hook函数、收集用户事件处来过滤。 生命周期函数执行2次:跟我们内部封装和Object.assign有关,示意代码如下: [代码]let a ={name: 'jojo'} let b = Object.assign(a, {age: 27}) // a === b ? // 内部处理 App(appOptions) Page(Object.assign(appOptions, pageOptions)) [代码] 导致,调App时gio加个app标记,在Page里面也能获取到,在gio内部,执行生命周期函数时,区分不开是page还是app。上面运行原理第3步,hook函数、收集用户事件处,非生命周期函数直接调用和需要收集的Page生命周期函数,会存在2次调用。知道了原因,也就很好处理了,在原理第1步重写方法时,调app做app标记时,也要重置page标记,调page时同理: [代码]VdsInstrumentAgent.GrowingPage = function (t) { return t._growing_page_ = !0, t._growing_app_ = !1, VdsInstrumentAgent.originalPage(VdsInstrumentAgent.instrument(t)) } VdsInstrumentAgent.GrowingApp = function (t) { return t._growing_app_ = !0, t._growing_page_ = !1,VdsInstrumentAgent.originalApp(VdsInstrumentAgent.instrument(t)) } [代码] 至此,问题解决…这下终于打完收工了,🍎🍎🍎
2020-07-03 - 小程序的防盗思考
之前写了一篇《用webpack编译小程序》的文章,有人留言问到关于小程序防盗,加密,混淆等等问题,我想了一下午,发现这个真的是一个问题(呵呵),这篇文章来分享下我的思考 https://juejin.im/post/5b0e431f51882515497d979f 上文是一篇关于小程序反编译的介绍,作者通过一顿猛如虎的操作,成功的扒下了一个小程序的源码(好像是滴滴)。文末作者提到了一点安全相关的问题,为增加了反编译者的阅读难度可以通过babel来压缩混淆js文件。 我想通过各种框架(wepy, mpvue等)来编写的小程序,框架应该都经过了babel来处理输出,反编译者拿到的应该都是uglifyjs后的文件,之所以仍然有这些问题,应该还是有相当一部分的同学使用的原生小程序语法来进行的开发的吧! 盗版 如果我是一个盗版者,我这么做的目的当然是为了增加我的收益,毕竟成本小,我会找很多比较热门,流量高的项目,扒下来,加上我的广告(嘿嘿),我不太在乎小程序的质量,只要它能正常运行。 镜像盗版 镜像盗版是成本最小的一种方式,扒一个小程序,clone成n个项目,上线,收钱。镜像盗版不理会源代码是否混淆或者加密,不需要弄懂逻辑,仅仅需要小程序能正常运行,即便不完整。 数据盗版 这一类是比较有追求的盗版,因为搭建一套数据服务成本较高,因此扒一个小程序,将api抽离出来,自己开发界面,避开被人投诉,神不知鬼不觉的利用别人的数据服务(想到这,有点激动)。 数据保护 不论那种盗版,最终都需要将数据呈现给用户,因此如何保护数据是防盗的一个关键点。 关键字防盗方案 PC/H5我们有一种经济的操作方法,将关键字秘钥(如公钥,keyword)非周期性通过后端php/node/java等服务渲染输出(明文),浏览器端则通过算法生成token来解开相关数据或接口。注意非周期性这个描述(周期性输出也可以,但破解成本变小了)。 小程序并不能如上操作,但小程序可以通过非周期性更新版本,来输出不同的关键字秘钥(如写死在app.globalData中),这必然增加了盗版者的破解成本,当正版更新秘钥后,盗版小程序不更新将不能正常获得数据,也就断了盗版的根 云函数方案 这个是我重点想介绍的。云端小程序面向的是没钱,没资源又想搞点啥挣点外快的开发者,正规小程序当然都必须有自己的后端数据服务,但并不妨碍使用云端服务来提供支持。 云端很安全,这是我的推论,我也深信这是正确的。之所以安全,我想应该是微信app一定封装了一些和腾讯云间通信的某种不可告人的秘密吧。试想一下,如果不同小程序之间的云服务被串了,我的小程序用你的云资源,这。。这。。这,估计腾讯自己都会玩不下去了。 基于云端很安全的前提,我们可以把关键的秘钥数据存放在云端,每一次小程序初始化时去云端将关键秘钥取下来生成接口访问token,这样即便盗版者扒到了小程序,但云服务的隔离性使得盗版者不能获取关键秘钥,从而盗版者成功的扒到了一套UI 云服务的缺点是每天有访问次数上限,我记得是50万次/天,这完全不够啊~~ , 嗯,老铁,666,双击点赞 结束 以上种种,本人并没有实践过,仅限于分享,我们现在主要做开源,盗版,不存在的啊~~~ 如果你想了解下我们的架构,可以看这里 https://github.com/webkixi/aotoo-hub 如果你想使用我们的架构, 不怕死的看这里 https://www.agzgz.com 如果你还想看看我们的小程序,骚一骚下面这个 [图片]
2019-06-06 - 如何只使用一个云函数搞定一个庞大而复杂的系统
吐槽 翻遍社区的文章,关于云开发的干货,少之又少,大部分都还是官方文档的搬来搬去,没啥营养,是时候放出一点技术"干货"了(有经验的开发者都能想到的方案)! 正题 小程序云开发的云函数的最大限制是 [代码]50[代码] 个,假设每个接口都使用 [代码]1[代码] 个云函数的话,有 [代码]10[代码] 张表,每张表都有 [代码]增删改查[代码] 四个接口,那么就会有 [代码]40[代码] 个接口,再加上一些其他接口,差不多刚刚好够用,那如果有 [代码]20[代码] 张表,甚至更多的表、更多的接口呢?对于中小型的小程序来说足够使用,那如果一个非常庞大而复杂的系统该怎么办呢? 而且每一个云函数的运行环境是独立的,想要共享一些数据也不是特别方便,那么有没有什么办法突破这样的限制呢? 其实解决方案很简单,只需要一点点的 [代码]OOP[代码] 思想和利用 [代码]JavaScript[代码] 的特性,一个云函数就可以搞定所有的接口。 具体的实现请往下看。 思路 云函数的运行环境是 [代码]Nodejs[代码] , 那么使用的语言就是 [代码]JavaScript[代码] ,可以充分的利用 [代码]JavaScript[代码] 的特性。 [代码]JavaScript[代码] 中的 [代码]属性访问表达式[代码] 有两种语法 [代码]expression . identifier expression [ expression ] [代码] 第一种写法是一个表达式后跟随一个句点 [代码].[代码] 和一个标识符。表达式指定对象,标识符则指定需要访问的属性的名称。 第二种写法是使用方括号 [代码][][代码],方括号内是另一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或者代表要访问数组元素的索引。 不管使用哪种形式的属性访问表达式,在 [代码].[代码] 和 [代码][][代码] 之前的表达式总是会首先计算。 虽然 [代码].[代码] 的写法更加简单,但这种方式只适用于要访问的属性名称的合法标识符,并需要准确知道访问的属性的名字,如果属性的名称是一个保留字或者包含空格和标点符号,或者是一个数字(对于数组来说),则必须使用方括号 [代码][][代码] 的写法。当属性名是通过运算得出的值而不是固定值的时候,这时也必须使用方括号 [代码][][代码] 写法。 感谢社区大神 @卢霄霄 提供参考资料,详见 [代码]JavaScript权威指南[代码] (犀牛书)4.4章节。 可以使用 [代码][][代码] 的形式来完成动态的属性访问。具体实现请往下看。 实现 上面说了太多废话了,下面直接开干吧。 新建云函数 在云开发目录中新建一个云函数,我这里命名为 [代码]cloud[代码]。 打开 [代码]index.js[代码] 文件你会看到下面这段代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } [代码] 这个云函数仅作为入口使用,上面提到了云函数的运行环境是 [代码]Nodejs[代码] 那么 [代码]Nodejs[代码] 的特性也是可以使用的,这里主要用到的是全局对象 [代码]global[代码],详见文档 在文件中,写入一些必要的全局变量,主要还是云数据库方面的,方便后面使用。 在初始化后面插入代码 [代码]global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate [代码] 这样就可以在同一个云函数环境中直接访问这些全局变量。 创建公共类 然后新建一个文件夹,我这里命名为 [代码]controllers[代码] ,这个文件夹用于存放所有的接口。 在 [代码]controllers[代码] 中新建一个 [代码]base-controller.js[代码] 文件,创建一个叫做 [代码]BaseController[代码] 的类,用于提供一些公用的方法。 内容如下: [代码]class BaseController { /** * 调用成功 */ success (data) { return { code: 0, data } } /** * 调用失败 */ fail (erroCode = 0, msg = '') { return { erroCode, msg, code: -1 } } } module.exports = BaseController [代码] 看到这里大家可能有点没看懂在做什么,那么请继续往下看。 创建接口 假设创建一些要操作用户相关的的接口,可以在 [代码]controllers[代码] 文件夹中新建一个 [代码]user-controller.js[代码] 的文件,创建一个名为 [代码]UserController[代码] 的类,并继承上面的 [代码]BaseController[代码] 类,内容如下: [代码]const BaseController = require('./base-controller.js') class UserController extends BaseController { // ... } module.exports = UserController [代码] 可以在这个类中编写所有关于 [代码]user[代码] 的接口方法。 编写接口 假设要分页查询用户信息,可以在 [代码]UserController[代码] 类中创建一个 [代码]list[代码] 方法。 代码如下: [代码]async list (data) { const { pageIndex, pageSize } = data let result = await db.collection('users') .skip((pageIndex - 1) * pageSize) .limit(pageSize) .get() .then(result => this.success(result.data)) .catch(() => this.fail([])) return result } [代码] 由于上面已经定义了全局变量 [代码]db[代码] 所以在 [代码]UserController[代码] 中无需引入 [代码]wx-server-sdk[代码] 引入接口类 写到这里接口已经完成了,还需要再引入这些接口类才可以进行访问。在 [代码]index.js[代码] 中引入 [代码]user-controller.js[代码] [代码]const User = require('./controllers/user-controller.js') [代码] 然后创建一个 [代码]api[代码] 变量,[代码]new[代码] 一个 [代码]User[代码] 实例 [代码]const api = { user: new User() } [代码] 在 [代码]main[代码] 方法中调用 [代码]UserController[代码] 中的方法。 [代码]exports.main = async (event, context) => { const { data } = event let result = await api['user']['list'](data) return result } [代码] 写到这里基本已经完成了接口的调用,但想要一个云函数动态调用所有接口还需要做一些改动。 动态调用接口 刚开始的时候介绍了 [代码]属性访问表达式[代码],限制稍微改动一下 [代码]main[代码] 方法 [代码]exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } [代码] 在小程序调用云函数时,需要传入 [代码]controller[代码]、[代码]action[代码] 和 [代码]data[代码] 参数即可 [代码]const result = await wx.cloud.callFunction({ name: 'cloud', data: { controller, action, data } }) [代码] 完整 [代码]index.js[代码] 文件的代码 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') const User = require('./controllers/user-controller.js') const api = { user: new User() } // 初始化 cloud cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate // 云函数入口 exports.main = async (event, context) => { exports.main = async (event, context) => { const { controller, action, data } = event const result = await api[controller][action](data) return result } } [代码] 其他实现 云开发官方团队打造的轮子 tcb-router
2020-05-26