- 如何打造一份0 bug 的js代码!
说到程序开发,bug总是如影随形,开发过程中50%的时间在debug,30%是在修之前发布了的bug,毫不奇怪。 如何把bug见到最少,甚至是0 bug呢?看似遥不可及,但实际是可以追求的,方法就是完整科学的测试,事实上,测试和使用是证明代码没bug的唯一方式。 测试又分为白盒测试跟黑盒测试,一般来说,产品上线前经过测试同学测试做的功能测试都是黑盒测试,毕竟测试同学不可能了解所有的代码,而能做白盒测试的,都是最了解代码的人,也就是写代码的程序员。而只有最了解代码的人也才能写出最完善的测试用例。而单元测试就是白盒测试里面最重要的方法之一。 很多程序员,特别是前端程序员是没有写单元测试的习惯。诚然,写单元测试其实是比较费劲的事情,特别在前端领域,涉及大量ui及交互操作,写单元测试尤为困难。但是近年来js模块化越演越烈,模块化的同时也使得写单元测试变得更容易了。 本文就致力于探讨在当前的开发环境下做单元测试的一些实践方法。 先介绍下被测试的对象 —— 一个js工具库,这个公共库有几个比较重要的标签:ES6,移动端,浏览器端。 从最简单的讲起吧,js的测试框架其实不算少,比较有名的有mocha,Jasmine,Jest等,基本用法都比较简单明了,看看官方文档就大概能写出来一些测试用例了。下面我主要使用比较强大mocha来作为主要的测试框架。 下面是我项目中的一份test.js: [代码]import * as lib from './index'; import chai from 'chai'; let expect = chai.expect; describe('testcase',function(){ it('single one',function(){ let a = 'aer'; expect(a).to.be.a('string'); }); it('test /common/getUrlParam',function(){ let ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com#a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','https://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('b','http://vip.qq.com?a=1&b=2'); expect(ret).to.equal('2'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1#a=2'); expect(ret).to.equal('2'); }); }); [代码] ES6 这份test.js包含两个测试用例,第一用例是用于测试的,单纯试用下测试框架跟断言库的功能,第二个用例是对库文件中一个模块的一个方法的测试用例,估计前端大佬们看看方法名大概都能看懂是什么东西,我就不多说了。写好test.js后就跑了试试看,运行mocha,然后马上就报错了: [代码]JUSTYNCHEN-MC0:gxh-lib-es6 justynchen$ ./node_modules/.bin/mocha /path/to/your/project/somelib/test.js:1 (function (exports, require, module, __filename, __dirname) { import * as lib from './index'; ^ SyntaxError: Unexpected token * at new Script (vm.js:79:7) at createScript (vm.js:251:10) at Object.runInThisContext (vm.js:303:10) at Module._compile (internal/modules/cjs/loader.js:656:28) ... [代码] 我当前的node版本是v10.13.0(对,近日node的LTS版本已经升级到10.x了,没升的同学赶紧玩玩吧),按理说是默认支持import的。仔细一看报错,原来mocha不是直接执行test.js,而是把test.js的内容放到了一个沙箱里面执行的。那就有点蛋疼了,就算node新版本已经支持了也没法直接使用。第一想法就是先把代码编译了,然后再做测试,可是测试的代码都是编译后的代码,就算测试出什么问题,还要经过sourcemap才能找到源码中出错的位置,想想都蛋疼。 官方当然是不会这么蠢的,稍微找了下官方的方案,不难找到对es6的支持。babel提供了一个register,给到不同的应用去做转换,mocha同样也可以使用这个register先转换然后再跑。命令就变成了这个: [代码]mocha --require babel-core/register [代码] 同时,package.json里面的选项也要加上babel的选项: [代码]"babel": { "presets": [ "stage-3", "latest" ], }, [代码] 当然,相关的包(babel,babel-core)也要同时装上,大家都懂的后面我就不提了,缺啥装啥就对了,后面提到的工具如没特别说明都是指npm包。 到这里相关的资料还比较好找,接下来就是干货了。 browser 运行上面改造过的mocha命令,是不是就ok了呢?当然没那么顺利,这里遇到了第二个坑: [代码]./node_modules/.bin/mocha --require babel-core/register /path/to/your/project/somelib/common/cache.js:15 var storage = window[storageType]; ^ ReferenceError: window is not defined at initStorage (/Users/justynchen/....../cache.js:10:16) at Object.<anonymous> (/Users/justynchen/....../cache.js:41:16) at Module._compile (internal/modules/cjs/loader.js:688:30) [代码] 前面也说了这份库是给移动端浏览器用的,其中就免不了使用一些浏览器的API,这些API在node里面都是不存在的。解决方案有两个: 直接不测试使用了浏览器API的代码,使用前先做检测并return掉。 找一个模拟浏览器的环境,让浏览器的API也能正常执行。 方案一是我们不愿意看到的,特别是一份浏览器用的库,不测试浏览器相关的特性那跟咸鱼有什么区别【手动狗头】。那就按方案二的思路想走吧,想到node模拟浏览器的环境,脑中浮现的第一个名词估计大部分人跟我都一样 —— electron。作为业界最著名的“没有界面的浏览器”,用在这里再合适不过了。但是该怎么用呢,稍作搜索,果然已经有前人做了相应的工作,有一个electron-mocha的工具刚好就是把这两个东西合了起来。 然后命令就变成了这样: [代码]electron-mocha --renderer --require babel-core/register [代码] 然后终于得到了我们想要的结果: [代码]JUSTYNCHEN-MC0:somelib justynchen$ ./node_modules/.bin/electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam 2 passing (37ms) [代码] 完美~(请自动脑补金星脸) 可是,就这么完了是否有点意犹未尽? 是的,就是缺了点什么东西,说好的0 bug呢,写了测试用例就能保证0 bug了么?肯定不是的,如果有的地方就是有bug,只是用例没写好,并没有覆盖到有问题的地方怎么办?只有写“全”了的测试用例才能保证0 bug。如何确保用例写全了呢?请往下看。 代码测试覆盖率 这里引入一个概念,叫代码测试覆盖率,大概意思就是说,你的测试用例到底覆盖了多少的代码。理想情况下,肯定只有100%覆盖所有代码的用例,才能说自己经过测试的代码是0 bug的,当然现实中100%总是很难的,一般覆盖到90%以上已经是比较理想的情况了。 JS也有统计代码测试覆盖率的库 —— Istanbul。这库名也很有意思,库名直译是伊斯坦布尔,没错,就是那个正常中国人可能名字都没听说过的中东城市。这个地方有个特产是毯子,然后这个库的作者就想,覆盖就是毯子该做的事情嘛,脑洞一开就把库名起作Istanbul了。 继续“稍微读下文档”,哦,原来这个库有一个命令行工具,nyc,装上然后放到执行命令的前面就能做覆盖率统计了。然后就有了下面的命令 [代码]nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register [代码] 其中reporter是定制化报告的内容,默认是text,lcov就是生成一个网页版的覆盖率报告。 然而,跑完之后是酱婶儿的 [代码]JUSTYNCHEN-MC0:somelib justynchen$ ./node_modules/.bin/nyc ./node_modules/.bin/electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam ✓ aidMaker test 3 passing (18ms) ----------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------|----------|----------|----------|----------|-------------------| All files | 0 | 0 | 0 | 0 | | ----------|----------|----------|----------|----------|-------------------| [代码] 苍天大地啊,为啥啥都没有。。。。 这里就又开始苦逼的查资料环节,不得不吐槽一下,这个资料是真不好查,中文资料没有就算了,反正早习惯了,文档翻遍了还是没有。。。那就过分了。然后查了下别的资料,基本都是mocha跟Istanbul一起用的,也没有electron-mocha相关的。 最后还是找到了github的issue里面,果然有人是跟我有类似问题,找了几个提了没啥回音的,终于找到一个maintainer的回复。里面指向了一个插件 —— babel-plugin-istanbul。 皇天不负有心人,得益于之前已经引入了babel,这里只要加上这个插件就ok了,照例先装包,package.json里面的babel配置加上这个参数 [代码]"babel": { "presets": [ "stage-3", "latest" ], "env": { "test": { "plugins": [ "istanbul" ] } } }, [代码] 然后按照插件的README改一下命令,最终得到了这个 [代码]cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register [代码] 然后一跑。。。发现,x你x的。。。跟前面的输出一毛一样,啥的没有!!! 冷静一下,再回头看看maintainer回复的原话 Basically, yes. Note that if you’re using babel already you can now just use the excellent istanbul-plugin to instrument your code. If you do this, you really only need to write out the __coverage__ object after the tests have run. (I write them out for both renderer and main thread tests, then combine them using nyc report from the command line). 里面提到了一个奇怪的参数__coverage__,又查一波资料,在这个项目的某些commit comment里面看到这个__coverage__的蛛丝马迹,这东西好像是一个全局参数,那这个参数有啥用咧?管他有用没用,打出来看看再说,然后就在test.js里面吧这个参数打了下,发现,卧槽,还真有,而且里面不就是覆盖率的数据么???? 嗯,有数据。。。怎么生成报告呢?作者写的语焉不详。。。啥叫write out这个参数然后配合nyc report命令就能用了,write到啥地方啊,咋配合啊!!! 这个时候就想到了Istanbul的一些特性,其实它是会在测试后生成一个.nyc_output的文件夹的,打开一看,里面不就是一些json么!那是不是直接write进去就好了呢?文件该叫啥名字咧,原来的文件都是hash命名的,这hash哪来的呀。不管了写了再说,然后得到如下test.js [代码]import * as lib from './index'; import chai from 'chai'; import fs from 'fs'; let expect = chai.expect; describe('testcase',function(){ it('single one',function(){ let a = 'aer'; expect(a).to.be.a('string'); }); it('test /common/getUrlParam',function(){ let ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com#a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('a','https://vip.qq.com?a=1'); expect(ret).to.equal('1'); ret = lib.common.utils.getUrlParam('b','http://vip.qq.com?a=1&b=2'); expect(ret).to.equal('2'); ret = lib.common.utils.getUrlParam('a','http://vip.qq.com?a=1#a=2'); expect(ret).to.equal('2'); }); after(function() { fs.writeFileSync('./.nyc_output/coverage.json',JSON.stringify(__coverage__)); }); }); [代码] 终于生效了 [代码]JUSTYNCHEN-MC0:somelib justynchen$ tnpm run test > @tencent/somelib@1.0.1 test /path/to/your/project/..... > cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text electron-mocha --renderer --require babel-core/register testcase ✓ single one ✓ test /common/getUrlParam 3 passing (26ms) ----------------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------------------|----------|----------|----------|----------|-------------------| All files | 5.96 | 3.3 | 3.6 | 6.02 | | gxh-lib-es6 | 0 | 0 | 0 | 0 | | index.js | 0 | 0 | 0 | 0 | | gxh-lib-es6/business | 16.89 | 8.84 | 3.45 | 16.89 | | aid-maker.js | 80 | 66.67 | 100 | 80 | 325,340,341,344 | cgi-handler.js | 0 | 0 | 0 | 0 |... 57,159,160,161 | index.js | 0 | 0 | 0 | 0 | | pay.js | 0 | 0 | 0 | 0 |... 86,88,89,91,97 | .... [代码] 至此,终于可以说出那句 完美~ 附录 整体架构图 [图片] 后记:整个单元测试的技术其实都没什么困难的,基本上都有库可以用,主要把时间都花在了查询资料上面。写本文的时候好像是遇到问题马上就找到了解决方案,其实真实情况是,几乎遇到每个坑都会试了至少一两个走不通的方案,最后才找到正确的方案的,所以对于不经常关注社区的人来说,单靠文档是很难解决所有的问题的。单从前端领域来看,前端的技术日新月异,再完善的文档都很快会跟不上发展的步伐,还是要靠多关注社区的动向,甚至多参与社区的讨论和建设才不至于在需要用某些技术的时候无从下手。
2019-03-29 - 小程序自定义组件知多少
自定义组件 why 代码的复用 在起初小程序只支持 Page 的时候,就会有这样蛋疼的问题:多个页面有相同的组件,每个页面都要复制粘贴一遍,每次改动都要全局搜索一遍,还说不准哪里改漏了就出翔了。 组件化设计 在前端项目中,组件化是很常见的方式,某块通用能力的抽象和设计,是一个必备的技能。组件的管理、数据的管理、应用状态的管理,这些在我们设计的过程中都是需要去思考的。当然你也可以说我就堆代码就好了,不过一个真正的码农是不允许自己这么随便的! 所以,组件化是现代前端必须掌握的生存技能! 自定义组件的实现 一切都从 Virtual DOM 说起 前面《解剖小程序的 setData》有讲过,基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。 自定义组件一样的双线程,所以一样滴基于 Virtual DOM 来实现通信。那在这里,Virtual DOM 的一些基本知识(包括生成 VD 对象、Diff 更新等),就不过多介绍啦~ Shadow DOM 模型 基于 Virtual DOM,我们知道在这样的设计里,需要一个框架来支撑维护整个页面的节点树相关信息,包括节点的属性、事件绑定等。在小程序里,Exparser 承担了这个角色。 前面《关于小程序的基础库》也讲过,Exparser 的主要特点包括: 基于 Shadow DOM 模型 可在纯 JS 环境中运行 Shadow DOM 是什么呢,它就是我们在写代码时候写的自定义组件、内置组件、原生组件等。Shadow DOM 为 Web 组件中的 DOM 和 CSS 提供了封装。Shadow DOM 使得这些东西与主文档的 DOM 保持分离。 简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装 HTML 组件(类似 vue 组件,将 html,css,js 独立部分提取)。 例如我们定义了一个自定义组件叫[代码]<my-component>[代码],你在开发者工具可以见到: [图片] [代码]#shadow-root[代码]称为影子根,DOM 子树的根节点,和文档的主要 DOM 树分开渲染。可以看到它在[代码]<my-component>[代码]里面,换句话说,[代码]#shadow-root[代码]寄生在[代码]<my-component>[代码]上。[代码]#shadow-root[代码]可以嵌套,形成节点树,即称为影子树(Shadow Tree)。 像这样: [图片] Shadow Tree 拼接 既然组件是基于 Shadow DOM,那组件的嵌套关系,其实也就是 Shadow DOM 的嵌套,也可称为 Shadow Tree 的拼接。 Shadow Tree 拼接是怎么做的呢?一切又得从模版引擎讲起。 我们知道,Virtual DOM 机制会将节点解析成一个对象,那这个对象要怎么生成真正的 DOM 节点呢?数据变更又是怎么更新到界面的呢?这大概就是模版引擎做的事情了。 《前端模板引擎》里有详细描述模版引擎的机制,通常来说主要有这些: DOM 节点的创建和管理:[代码]appendChild[代码]/[代码]insertBefore[代码]/[代码]removeChild[代码]/[代码]replaceChild[代码]等 DOM 节点的关系(嵌套的处理):[代码]parentNode[代码]/[代码]childNodes[代码] 通常创建后的 DOM 节点会保存一个映射,在更新的时候取到映射,然后进行处理(通常包括替换节点、改变内容[代码]innerHTML[代码]、移动删除新增节点、修改节点属性[代码]setAttribute[代码]) 在上面的图我们也可以看到,在 Shadow Tree 拼接的过程中,有些节点并不会最终生成 DOM 节点,例如[代码]<slot>[代码]这种。 但是,常用的前端模版引擎,能直接用在小程序里吗? 双线程的难题 自定义组件渲染流程 双线程的设计,给小程序带来了很多便利,安全性管控力都拥有了,当然什么鬼东西都可以比作一把双刃剑,双线程也不例外。 我们知道,小程序分为 Webview 和 JS 双线程,逻辑层里是没法拿到真正的 DOM 节点,也没法随便动态变更页面的。那在这种情况下,我们要怎么去使用映射来更新模版呢(因为我们压根拿不到 Webview 节点的映射)? 所以在双线程下,其实两个线程都需要保存一份节点信息。这份节点信息怎么来的呢?其实就是我们需要在创建组件的时候,通过事件通知的方式,分别在逻辑层和视图层创建一份节点信息。 同时,视图层里的组件是有层级关系的,但是 JS 里没有怎么办?为了维护好父子嵌套等节点关系,所以我们在 逻辑层也需要维护一棵 Shadow Tree。 那么我们自定义组件的渲染流程大概是: 组件创建。 逻辑层:先是 wxml + js 生成一个 JS 对象(因为需要访问组件实例 this 呀),然后是 JS 其中节点部分生成 Virtual DOM,拼接 Shadow Tree 什么的,最后通过底层通信通知到 视图层 视图层:拿到节点信息,然后吭哧吭哧开始创建 Shadow DOM,拼接 Shadow Tree 什么的,最后生成真实 DOM,并保留下映射关系 组件更新。 这时候我们知道,不管是逻辑层,还是视图层,都维护了一份 Shadow Tree,要怎么保证他们之间保持一致呢? 让 JS 和 Webview 的组件保持一致 为了让两边的 Shadow Tree 保持一致,可以使用同步队列来传递信息。(这样就不会漏掉啦) 同步队列可以,每次变动我们就往队列里塞东西就好了。不过这样还会有个问题,我们也知道 setData 其实在实际项目里是使用比较频繁的,要是像 Component 的 observer 里做了 setData 这类型的操作,那不是每次变动会导致一大堆的 setDate?这样通信效率会很低吧? 所以,其实可以把一次操作里的所有 setData 都整到一次通信里,通过排序保证好顺序就好啦。 Page 和 Component Component 是 Page 的超集 事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含[代码]usingComponents[代码]定义段。 来自官方文档-Component 所以,基于 Component 是 Page 的超集,那么其实组件的渲染流程、方式,其实跟页面没多大区别,应该可以一个方式去理解就差不多啦。 页面渲染 既然页面就是组件,那其实页面的渲染流程跟组件的渲染流程基本保持一致。 视图层渲染,可以参考7.4 视图层渲染说明。 结束语 其实很多新框架新工具出来的时候,经常会让人眼前一亮,觉得哇好厉害,哇好高大上。 但其实更多时候,我们需要挖掘新事物的核心,其实大多数都是在原有的事物上增加了个新视角,从不一样的视角看,看到的就不一样了呢。作为一名码农,我们要看到不变的共性,变化的趋势。
2019-04-01 - 如何通过小程序实现跨平台开发
背景 前段时间要做一系列的测试工具,需要在多平台:iOS、android、H5、公众号、小程序都实现。功能基本一样,就是在支付步骤需要区分平台,用对应的支付方式支付。本文讨论如何用一套小程序代码实现上述5个平台的开发。 效果图(左边为小程序,右边为浏览器): [图片] omi-mp的介绍 omi-mp是腾讯前端框架omi的一个工具集,其目的是在于将小程序代码转成H5/Web,具体可以参见omi-mp的介绍Github。 omi-mp的转换并不是完全兼容小程序所有特性的,只支持了一小部分小程序API,并且存在了一些兼容特性,因此就需要开发者在开发小程序代码时,更多的以开发H5/Web的思路开发。 思路图和实现步骤 [图片] 先实现小程序代码 1.初始化omi-mp目录工程: [代码]npm i omi-cli -g omi init-mp {工程名称} cd {工程名称} npm install [代码] 2.把小程序项目拷贝到[代码]src-mp[代码]目录。 3.建议边实现小程序的过程中,不断的检验生成的H5的正确性,避免在最后阶段检验,否则如果出现问题,将不好定位,本地运行H5命令: [代码]npm start //开发 [代码] 再将小程序打包成H5/Web 打包H5/Web命令: [代码]npm run build //发布 [代码] 发布需要确认域名,修改[代码]package.json[代码]文件,修改[代码]"build": "PUBLIC_URL={发布域名} node scripts/build.js"[代码] 如果存在部分js文件丢失,可以尝试执行 [代码]gulp copyThen [代码] 公众号直接加载H5 公众号本质上也属于H5。 iOS/android App通过内嵌网页加载H5 iOS通过MKWebView加载H5。 android通过WebView加载H5。 H5和原生App的交互部分可以通过JSBridge或者URL拦截实现 小程序代码如何区分平台 综上所述,除去部分iOS/android的原生代码外,基本所有的逻辑都是放在小程序里,按不同平台实现不同逻辑,小程序可以通过UserAgent以及Dom区分: 如果Dom树不存在[代码]window[代码]或者[代码]document[代码],为小程序平台; iOS/android App内嵌网页可以自定义特殊的UserAgent,小程序代码可以通过此来区分iOS/android App平台; 微信App内嵌浏览器的UserAgent会带入[代码]MicroMessenger/[代码]关键字,可以按此区分公众号平台; 其余为H5/Web平台; [代码]if (typeof window == 'undefined') { // 小程序 } else { if (navigator.userAgent.userAgent.indexOf('ios-app') != -1 || navigator.userAgent.userAgent.indexOf('android-app') != -1) { // iOS/android } else if (this.globalData.userAgent.indexOf('MicroMessenger/') != -1) { // 公众号 } else { // Web } } [代码] iOS/android原生代码如何桥接 1.iOS/android需要先设置特殊UserAgent让小程序代码知道是iOS/android平台 iOS: [代码]NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"ios-app", @"UserAgent", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary]; [代码] android: [代码]webView.getSettings().setUserAgentString("android-app"); [代码] 2.以URL拦截为例,小程序代码在当处于iOS/android平台的情况下,可以发送特殊URL,让iOS/android原生代码处理: 小程序: [代码]if (navigator.userAgent.userAgent.indexOf('ios-app') != -1 || navigator.userAgent.userAgent.indexOf('android-app') != -1) { let url = 'app://pay?' + query; window.location.href = url; } [代码] 3.iOS/android拦截URL特殊处理 iOS: [代码]- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURL * url = navigationAction.request.URL; if ([url.absoluteString hasPrefix:@"app://pay"]) { // do something... decisionHandler(WKNavigationActionPolicyCancel); return; } decisionHandler(WKNavigationActionPolicyAllow); } [代码] android: [代码]webView.setWebViewClient(new WebViewClient(){ public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("app://pay")) { // do something... return true; } return false; } }); [代码] omi-mp的部分缺陷和一些踩过的坑 1.目前omi-mp只支持部分小程序部分API: [代码]- wx.request - wx.navigateTo - wx.navigateBack - wx.getSystemInfo - wx.getSystemInfoSync - wx.setNavigationBarTitle - this.setData - this.triggerEvent [代码] 如果用了其他的API,那么将会在输出H5报错。 2.支持组件,但是不支持组件的函数直接调用,替代方案可以用mitt,在page和component之间用mitt消息传递。 3.如果需要同时输出H5和Web,那么需要同时绑定[代码]click[代码]和[代码]tap[代码]事件(小程序只能绑定tap,Web只能绑定click,H5两者都可以),但是同时绑定又将会造成在H5的情况下,[代码]click[代码]和[代码]tap[代码]都会回调,导致两次调用。解决办法是可以在[代码]click[代码]和[代码]tap[代码]的地方同时加上判断,避免两次调用: [代码]handleTap(e) { if (!((typeof window == 'undefined') && e.type === "tap")) { return; } else if (!((typeof window != 'undefined') && e.type === "click")) { return; } // do something... } [代码] 4.wxml不支持Object字段的遍历处理。 5.如果编译不过,那么确认下是不是wxml中存在了一些特殊关键字,与omi的重了,导致失败。 6.即便在H5的场景下也无法使用[代码]document.getElementById()[代码]。但是有替代方案,只是比较麻烦。 7.还有其它缺陷,有些忘了,待补充。 结语 omi-mp是一个不错的工具,在小程序不断变大变强的今天,能做到一套小程序代码,多端运行,降低开发成本。这里尤其感谢dntzhang的大力支持,希望omi越做越好。
2019-04-12