- 如何通过小程序实现跨平台开发
背景 前段时间要做一系列的测试工具,需要在多平台: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 - 用 HTM 实现小程序 SVG
写在前面 今天你可以在小程序中使用 Cax 引擎高性能渲染 SVG! SVG 是可缩放矢量图形(Scalable Vector Graphics),基于可扩展标记语言,用于描述二维矢量图形的一种图形格式。它由万维网联盟制定,是一个开放标准。SVG 的优势有很多: SVG 使用 XML 格式定义图形,可通过文本编辑器来创建和修改 SVG 图像可被搜索、索引、脚本化或压缩 SVG 是可伸缩的,且放大图片质量不下降 SVG 图像可在任何的分辨率下被高质量地打印 SVG 可被非常多的工具读取和修改(比如记事本) SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性、可编程星更强 SVG 完全支持 DOM 编程,具有交互性和动态性 而支持上面这些优秀特性的前提是 - 需要支持 SVG 标签。比如在小程序中直接写: [代码]<svg width="300" height="150"> <rect bindtap="tapHandler" height="100" width="100" style="stroke:#ff0000; fill: #0000ff"> </rect> </svg> [代码] 上面定义了 SVG 的结构、样式和点击行为。但是小程序目前不支持 SVG 标签,仅仅支持加载 SVG 之后 作为 background-image 进行展示,如 [代码]background-image: url("data:image/svg+xml.......)[代码],或者 base64 后作为 background-image 的 url。 直接看在小程序种使用案例: [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { renderSVG(html` <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>`, 'svg-a', this) }, tapHandler: function () { console.log('你点击了 rect') } }) [代码] 其中的 svg-a 对应着 wxml 里 cax-element 的 id: [代码]<view class="container"> <cax-element id="svg-c"></cax-element> </view> [代码] 声明组件依赖 [代码]{ "usingComponents": { "cax-element":"../../cax/index" } } [代码] 小程序中显示效果: [图片] 可以使用 [代码]width[代码],[代码]height[代码],[代码]bounds-x[代码] 和 [代码]bounds-y[代码] 设置绑定事件的范围,比如: [代码]<path width="100" height="100" bounds-x="50" bounds-y="50" /> [代码] 需要注意的是,元素的事件触发的包围盒受自身或者父节点的 transform 影响,所以不是绝对坐标的 rect 触发区域。 再来一个复杂的例子,用 SVG 绘制 Omi 的 logo: [代码]renderSVG(html` <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>`, 'svg-a', this) [代码] 小程序种显示效果: [图片] 在 omip 和 mps 当中使用 cax 渲染 svg,你可以不用使用 htm。比如在 omip 中实现上面两个例子: [代码] renderSVG( <svg width="300" height="220"> <rect bindtap="tapHandler" height="110" width="110" style="stroke:#ff0000; fill: #ccccff" transform="translate(100 50) rotate(45 50 50)"> </rect> </svg>, 'svg-a', this.$scope) [代码] [代码]renderSVG( <svg width="300" height="220"> <g transform="translate(50,10) scale(0.2 0.2)"> <circle fill="#07C160" cx="512" cy="512" r="512"/> <polygon fill="white" points="159.97,807.8 338.71,532.42 509.9,829.62 519.41,829.62 678.85,536.47 864.03,807.8 739.83,194.38 729.2,194.38 517.73,581.23 293.54,194.38 283.33,194.38 "/> <circle fill="white" cx="839.36" cy="242.47" r="50"/> </g> </svg>, 'svg-a', this.$scope) [代码] 需要注意的是在 omip 中传递的最后一个参数不是 [代码]this[代码],而是 [代码]this.$scope[代码]。 在 mps 中,更加彻底,你可以单独创建 svg 文件,通过 import 导入。 [代码]//注意这里不能写 test.svg,因为 mps 会把 test.svg 编译成 test.js import testSVG from '../../svg/test' import { renderSVG } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { renderSVG(testSVG, 'svg-a', this) } }) [代码] 比如 test.svg : [代码]<svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg> [代码] 会被 mps 编译成: [代码]const h = (type, props, ...children) => ({ type, props, children }); export default h( "svg", { width: "300", height: "300" }, h("rect", { bindtap: "tapHandler", x: "0", y: "0", height: "110", width: "110", style: "stroke:#ff0000; fill: #0000ff" }) ); [代码] 所以总结一下: 你可以在 mps 中直接使用 import 的 SVG 文件的方式使用 SVG 你可以直接在 omip 中使用 JSX 的使用 SVG 你可以直接在原生小程序当中使用 htm 的方式使用 SVG 这就完了?远没有,看 cax 在小程序中的这个例子: [图片] 详细代码: [代码]renderSVG(html` <svg width="300" height="200"> <path d="M 256,213 C 245,181 206,187 234,262 147,181 169,71.2 233,18 220,56 235,81 283,88 285,78.7 286,69.3 288,60 289,61.3 290,62.7 291,64 291,64 297,63 300,63 303,63 309,64 309,64 310,62.7 311,61.3 312,60 314,69.3 315,78.7 317,88 365,82 380,56 367,18 431,71 453,181 366,262 394,187 356,181 344,213 328,185 309,184 300,284 291,184 272,185 256,213 Z" style="stroke:#ff0000; fill: black"> <animate dur="32s" repeatCount="indefinite" attributeName="d" values="......太长,这里省略 paths........" /> </path> </svg>`, 'svg-c', this) [代码] 再试试著名的 SVG 老虎: [图片] path 太长,就不贴代码了,可以点击这里查看 pasiton 标签 [代码]import { html, renderSVG } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="200" height="200"> <pasition duration="200" bindtap=${this.changePath} width="100" height="100" from="M28.228,23.986L47.092,5.122c1.172-1.171,1.172-3.071,0-4.242c-1.172-1.172-3.07-1.172-4.242,0L23.986,19.744L5.121,0.88 c-1.172-1.172-3.07-1.172-4.242,0c-1.172,1.171-1.172,3.071,0,4.242l18.865,18.864L0.879,42.85c-1.172,1.171-1.172,3.071,0,4.242 C1.465,47.677,2.233,47.97,3,47.97s1.535-0.293,2.121-0.879l18.865-18.864L42.85,47.091c0.586,0.586,1.354,0.879,2.121,0.879 s1.535-0.293,2.121-0.879c1.172-1.171,1.172-3.071,0-4.242L28.228,23.986z" to="M49.1 23.5H2.1C0.9 23.5 0 24.5 0 25.6s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 24.5 50.3 23.5 49.1 23.5zM49.1 7.8H2.1C0.9 7.8 0 8.8 0 9.9c0 1.1 0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1C51.2 8.8 50.3 7.8 49.1 7.8zM49.1 39.2H2.1C0.9 39.2 0 40.1 0 41.3s0.9 2.1 2.1 2.1h47c1.1 0 2.1-0.9 2.1-2.1S50.3 39.2 49.1 39.2z" from-stroke="red" to-stroke="green" from-fill="blue" to-fill="red" stroke-width="2" /> </svg>`, 'svg-c', this) this.pasitionElement = svg.children[0] }, changePath: function () { this.pasitionElement.toggle() } }) [代码] pasiton 提供了两个 path 和 颜色 相互切换的能力,最常见的场景比如 menu 按钮和 close 按钮点击后 path 的变形。 举个例子,看颜色和 path 同时变化: [图片] 线性运动 这里举一个在 mps 中使用 SVG 的案例: [代码]import { renderSVG, To } from '../../cax/cax' Page({ tapHandler: function(){ this.pause = !this.pause }, onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` , 'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height/2 rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 this.pause = false this.interval = setInterval(()=>{ if(!this.pause){ rect.rotation++ svg.stage.update() } },15) }) [代码] 效果如下: [图片] 组合运动 [代码]import { renderSVG, To } from '../../cax/cax' Page({ onLoad: function () { const svg = renderSVG(html` <svg width="300" height="300"> <rect bindtap="tapHandler" x="0" y="0" height="110" width="110" style="stroke:#ff0000; fill: #0000ff" /> </svg>` ,'svg-a', this) const rect = svg.children[0] rect.originX = rect.width/2 rect.originY = rect.height rect.x = svg.stage.width/2 rect.y = svg.stage.height/2 var sineInOut = To.easing.sinusoidalInOut To.get(rect) .to().scaleY(0.8, 450, sineInOut).skewX(20, 900, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(450) .to().scaleY(1, 450, sineInOut) .wait(900) .cycle().start() To.get(rect) .wait(900) .to().scaleY(0.8, 450, sineInOut).skewX(-20, 900, sineInOut) .cycle() .start() To.get(rect) .wait(1350) .to().scaleY(1, 450, sineInOut) .cycle() .start() setInterval(() => { rect.stage.update() }, 16) } }) [代码] 效果如下: [图片] 其他 vscode 安装 lit-html 插件使 htm 的 html[代码]内容[代码] 高亮 还希望小程序 SVG 提供什么功能可以开 issues告诉我们,评估后通过,我们去实现! Cax Github 参考文档
01-04 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23 - wx-open-launch-weapp vue 或者react ios上需要刷新才显示?
目前发现react和vue 的h5在Android上一切正常跳转,到ios上了wx-open-launch-weapp需要刷新页面才显示。
2020-07-13