- json2canvas:使用JSON生成小程序海报
作者:诗人的咸鱼 原文:小程序生成分享海报,一个json就够了。同时支持web Fundebug经授权转载,版权归原作者所有。 需求 在项目里写过几个canvas生成分享海报页面后,觉得这是个重复且冗余的工作.于是就想有没有能通过类似json直接生成海报的库. 然后就在github找到到两个项目: wxa-plugin-canvas,不太喜欢配置文件的写法.就没多去了解 mp_canvas_drawer,使用方式就比较符合直觉,不过可惜功能有点少. 然后就想着能不能自己再造个轮子.于是就有了这个项目 json2canvas,你可以简单的理解为是mp_canvas_drawer的增强版吧. json2canvas canvas绘制海报,写个json就够了. 项目的canvas绘制是基于cax实现的.所以天然的带来一个好处,json2canvas同时支持小程序和web 功能 支持缩放. 如果设计稿是750,而画布只有375时.你不需要任何换算,只需要将scale设置为0.5即可. 支持文本(长文本自动换行,感谢 coolzjy@v2ex 提供的正则 https://regexr.com/4f12l ,优化了换行的计算方式(不会粗暴的折断单词)) 支持图片(圆角) 支持圆型,矩形,矩形圆角 支持分组(cax里很好用的一个功能) 同时支持小程序和web 示例 web-demo 界面左边的json,可以进行编辑,直接看效果哟~ 小程序-demo [代码]git clone https://github.com/willnewii/json2canvas.git 微信开发者工具导入项目 example/weapp/ [代码] 小程序安装 [代码]npm i json2canvas 微信开发者工具->工具->构建npm [代码] 在需要使用的界面引入Component [代码]{ "usingComponents": { "json2canvas":"/miniprogram_npm/json2canvas/index" } } [代码] 效果图 想要生成一个这样的海报,需要怎么做?(红框是图片元素,蓝框是文字元素,其余的是一张背景图。) [图片] 一个json就搞定.具体支持的元素和参数,请查看项目readme [代码]{ "width": 750, "height": 1334, "scale": 0.5, "children": [ { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/bg_concerts_1.jpg", "width": 750, "height": 1334 }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 100, "x": 48, "y": 44, "isCircular": true, }, { "type": "circle", "r": 50, "lineWidth": 5, "strokeStyle": "#CCCCCC", "x": 48, "y": 44, }, { "type": "text", "text": "歌词本", "font": "30px Arial", "color": "#FFFFFF", "x": 168, "y": 75, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/medal_concerts_1.png", "width": 300, "x": "center", "y": 361 }, { "type": "text", "text": "一生活一场 五月天", "font": "38px Arial", "color": "#FFFFFF", "x": "center", "y": 838, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "text", "text": "北京6场,郑州2场,登船,上班,听到你想听的歌了吗?", "font": "24px Arial", "color": "#FFFFFF", "x": "center", "y": 888, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "rect", "width": 750, "height": 193, "fillStyle": "#FFFFFF", "x": 0, "y": "bottom" }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 117, "height": 117, "x": 47, "y": 1180 }, { "type": "text", "text": "长按识别小程序二维码", "font": "26px Arial", "color": "#858687", "x": 192, "y": 1202 }, { "type": "text", "text": "加入五月天 永远不会太迟", "font": "18px Arial", "color": "#A4A5A6", "x": 192, "y": 1249 }] } [代码] 问题反馈 有什么问题可以直接提issue
2019-06-29 - docker快速入门
1. 介绍docker是什么Docker使用go基于linux lxc(linux containers)技术实现的开源容器,诞生于2013年年初,最开始叫dotcloud公司,13年年底改名为docker inc。 2017年下载次数达到了百亿次,估值达13亿美元,通过对应用封装(Packaging)、分发(Distribution)、部署(Deployment)、运行(Runtime)全生命周期管理,达到“一次封装,到处运行” [图片] 为何使用docker?Docker直译码头工人,将各种大小和形状的物品装进船里。这对从事软件行业的人来说,听起来很熟悉,花了大量时间和精力把一个应用放在另一个应用里 [图片] docker出现之前,对不同环境的安装、配置、维护工作量很多,如部署,配置文件,crontab,依赖等等。 使用docker,无需关心环境,只需要一些配置就能构建镜像,而部署则用一条run命令 [图片] 虚拟机 vs 容器 虚拟机需要有额外的虚拟机管理应用和虚拟机操作系统层,操作系统层不仅占用空间而且运行速度也相对慢 docker容器是在本机操作系统层面上实现虚拟化,因此很轻量,速度接近原生系统速度 [图片] 虚拟机启动速度是分钟级别,性能较弱、内存和硬盘占用大,一个物理机最多跑几十个虚拟机,但它的隔离性比较好。 docker启停都是秒级实现,内存和硬盘占用非常小,单机支持上千个容器,在ibm服务器上可运行上万个容器 容器跟虚机相比,有着巨大的优势 [图片] docker优点 只关心应用:以往我们需要关心操作系统、软件、项目,有了docker我们可以只关心应用而不是操作系统,docker发展迅速,基于docker的paas平台也层出不穷,使得我们能更方便的使用docker 快速交付:docker可在秒级提供沙箱环境,开发,测试,运维使用完全相同的环境来部署代码 微服务:docker有助于将一个复杂系统分解,让用户用更离散的方式思考服务 离线开发:将服务编排在笔记本中移动办公,使用docker可在本机秒级别启动一个本地开发环境 降低调试成本:在测试和上线时产生无效的类、有问题的依赖、缺少的配置等问题,docker可让一个问题调试和环境重现变得更简单 CD:docker让持续交付实现变得更容易,特别是对于蓝绿部署就更简单。 第一版上线时,需要上第二版新功能,两个版本功能会有冲突,这时用docker实现蓝绿部署就非常方便了 如:可以部署两个版本同时在线,新版本测试没问题了把老版本流量切到新版本就可以了 迁移:可以很快的迁移到其他云或服务器 与传统虚拟机方式相比,容器化方式在很多场景下都是存在极为明显的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 容器化方式在很多场景下都有极大的优势。无论是开发、测试、运维都应该尽快掌握docker,尽早享受其带来的巨大便利 [图片] 概念再来了解docker非常关键的概念,这样才能理解docker容器整个生命周期 [图片] 概念—镜像 镜像(类)=文件系统+数据,我常常用开发语言中的类比作镜像,对象比作容器 镜像由多个层加上一些docker元数据组成,容器运行着由镜像定义的系统 [图片] 概念—容器容器(对象)=镜像运行实例 容器是镜像的运行实例,可以使用同一个镜像运行多个实例。如图所示,一个ubuntu docker镜像产生了三个ubuntu容器,docker利用容器运行和隔离应用 [图片] 从读写角度来说,镜像是只读的,容器是在镜像上添加了一层可读写的文件系统 [图片] [图片] 概念—层层=文件变更集合 像传统虚机应用,每个应用都需要拷贝一份文件副本,运行成百上千上磁盘空间会迅速耗光,而docker采用写时复制来减少磁盘空间,当一个运行中的容器要写入一个文件时,它会把该文件复制到新区域来记录这次的修改,在执行docker提交时将这次修改记录下并产生一个新的层。docker分层解决大规模使用容器时碰到的磁盘和效率问题 [图片] 概念—仓库docker借鉴了大量git优秀的经验。docker仓库分公有库和私有库,最大的公开仓库是docker hub,国内也有很多仓库源 [图片] 2. 创建第一个docker应用通过创建一个docker应用来看看docker是怎么方便使用的 创建docker镜像方式 创建docker有四种方式 [图片] 但最常用的docker命令+手工提交和Dockerfile的方式 [图片] 对于我们来说Dockerfile是最常用也是最有用的 “dockerfile” [图片] 那创建一个docker应用只需要三步:编写dockerfile、构建镜像、运行容器 编写dockerfile那我们就开始用dockerfile来创建一个应用 Dockerfile是包含一系列命令的文本文件,这个文件包含6条命令 1、FROM是使用php官方镜像,左边是镜像名字,右边是标签名字,标签名字不写默认是latest 2、声明维护人员 3、RUN运行一条linux命令,我们把php代码重定向到/tmp/index.php 4、EXPOSE声明要开放的端口 5、WORKDIR启动容器后默认目录 6、CMD容器启动后,默认执行的命令,相当于应用的入口,用php自带的webserver监听8000 [图片] 构建镜像使用docker build命令生成镜像,—tag指定镜像的名字,左边是名字,右边是标签,最后有个.表示在当前目录查找Dockerfile 可以看到,每个命令都会有个输入输出,输入是命令,输出是给到层的id,所以,基本上每个命令都会产生一个层 最后提示镜像构建成功,并打上镜像标签 [图片] 运行容器第三,使用docker run命令运行镜像,-p将容器的8000端口映射到本机8000端口,—name给容器起个名字 用curl对本机8000端口请求,服务器返回当前时间,说明我们构建的容器运行成功了 [图片] 请求本地8000端口,服务器返回当前时间 [图片] dockerfile常用命令其实Dockerfile常用命令就5个:from、add、run、workdir、cmd 创建docker应用步骤编写dockerfile 构建镜像 运行容器 使用docker应用步骤拉取镜像 运行容器 dockerfile最佳实践精简镜像用途 尽量让每个镜像的用途单一 选择合适基础镜像 选择以alpine、busybox等基础的镜像 busybox:号称操作系统里的瑞士军刀,只有……这么大,但却有一百多常用命令 如果你的目标是小而精,busybox是首选,因为它已经精简到没有bash,使用的是ash,一个兼容posix的shell [图片] Alpine:你的目标是小但是又有一些工具的话,可以选择alpine,它是一个面向安全的轻量linux发行版,它关注安全、性能和资源效能,比busybox功能更完善,还提供apk查询和安装软件包,大小只有2-3兆 [图片] 很多官方的镜像都有alpine的镜像,像刚刚使用的php镜像 [图片] 提供维护者信息 正确使用版本 使用明确的版本号,而非依赖于默认的latest,避免环境不一致导致的问题 [图片] 删除临时文件 如安装软件后的安装包,如上图2、3步骤 提高生成速度 如内容不变的指令尽量放在前面,这样可以复用 减少镜像层数 多条命令写在一起,使生成的镜像层数少,如上图2、3步骤 恰当使用multi-stage 保证最终生成镜像最小化 3. 常用命令search想使用一个镜像,用这个命令就可以了,默认按评分排序 official如果是ok表示是官方镜像 Auto标示它是否用dickerfile进行自动化镜像构建 [图片] pull一旦确定一个镜像,通过对其名称执行docker pull来下载 标签默认是latest,严格来讲,镜像的仓库名还应该添加仓库地址的,默认是registry.hub.docker.com Docker images命令查找下载的镜像 [图片] run使用docker run运行一个容器,it表示用交互式方式运行,最后表示要执行的命令 [图片] 其实更常用的方式是以后台方式来执行,这时用d参数在后台运行 运行后用exec命令进去到容器 [图片] tagDocker tag给镜像一个新tag名字 Docker images查看centos镜像,把centos:latest打上centos:yeedomliu,这时再看会有3个centos,latest和yeedomliu的镜像id是相同的 把centos:yeedomliu删除,再查看latest还会存在,最后用rmi命令删除latest就会真正把latest镜像删除掉 如果相同镜像存在多个标签,只有最后一次的rmi命令会真正删除镜像 [图片] psPs可以查看运行中的容器 [图片] rmi删除一个镜像,同一个镜像id的不同标签的镜像,使用rmi删除最后一个镜像才会真正删除这个镜像 [图片] rm删除docker容器,如果运行中的容器需要加-f [图片] diff容器启动后文件变化情况 [图片] logs查看容器运行后的日志 [图片] cp我们想从容器里面拷贝文件到宿主机,或相反的过程就可以用到cp命令 [图片] container prune随着使用docker时间越长,停止状态下的容器会越来越多,这些都会占据磁盘空间 [图片] image prune未被打标签的镜像可以用image prune命令清理 [图片] system prune/df如果你觉得刚刚两条命令执行起来麻烦,可以用docker system prune一条命令搞定 另外用system df查看docker磁盘空间 [图片] 4. 实战了解了docker基础知识后,可进入相对实战的环节 本地开发 常见问题 架构 优化 本地开发 我们的项目使用了很多服务,如redis/mysql/mongodb等等,如果一个个运行起来,还加上配置,容易出手,也比较麻烦 kitematic:与使用命令行管理本地容器相比,你更想使用图形工具对容器管理,官方推出的容器管理工具,通过它可以查找镜像、创建容器、配置、启停容器等管理 [图片] [图片] 这是配置容器端口和宿主机端口,目录,网络等映射界面 [图片] docker-composecompose定位是“定义和运行多个docker容器的应用”,前身fig,目前仍然兼容fig格式的模板文件。 一条命令可以把一个复杂的应用启动起来 日常工作中,经常碰到多个容器相互完成某项任务 [图片] docker-compose示例1 默认模板文件名叫docker-compose.yml,结构很简单,每个顶级元素为服务名称,次级信息为配置信息 这里使用了redis/mongodb/mysql/nginx镜像,分别给它们映射了本地目录、端口、密码等信息,nginx镜像需要使用redis/mysql等服务,用links命令连接进来 [图片] docker-compose示例2 如果在本地开发,每个项目都可以像之前说的那样配置,这里提供了另外一种做法 我把公共的资源在一开始就启动,每个项目里只启动nginx镜像并关联其它的服务即可 公共服务compose [图片] 项目compose [图片] 常见问题 主进程:docker启动第一个进程称主进程,就是id为1的进程,这个进程退出就意味着容器退出,所以想要使docker作为服务使用,这个进程是不能退出的 expose命令是声明暴露的端口,运行时用-P才会生效。一般ports命令是做真正的端口映射,比较常用 架构 安装了docker的主机,一般在一个私有网络上 1、调用docker客户端可以从守护进程获取信息或发送指令 2、docker守护进程使用http协议接收来自docker客户端的请求 3、私有docker注册中心存储docker镜像 4、docker hub是由docker公司运营的最大的公共注册中心 互联网上也存在其他公共的注册中心 调用 Docker客户端可以从守护进程获取信息或给它发送指令。守护进程是一个服务器,它使用 HTTP协议接收来自客户端的请求并返回响应。相应地,它会向其他服务发起请求来发送和接收镜像,使用的同样是 HTTP协议。该服务器将接收来自命令行客户端或被授权连接的任何人的请求。守护进程还负责在幕后处理用户的镜像和容器,而客户端充当的是用户与 REST风格 API之间的媒介。 理解这张图的关键在于,当用户在自己的机器上运行 Docker时,与其进行交互的可能是自己机器上的另一个进程,或者甚至是运行在内部网络或互联网上的服务。 [图片] 优化 使用小镜像:一般来说,使用小的镜像都相对比较优秀,如官方的镜像基本上都有基于alpine的镜像 事后清理:删除镜像里软件包或一些临时文件,减小镜像大小 命令写一行:多个命令尽量写在一起有助于减少层数,也会减少镜像的大小 脚本安装:使用脚本进行初始化时,可以有效减少dockerfile的命令,同时带来另外的问题,可读性不好并且构建镜像时缓存不了 扁平化镜像:构建镜像过程中,可能会涉及到一些敏感信息,或者用了上面的办法镜像依然很大,可以试试这个办法 docker export 容器名或容器id | docker import - 镜像标签 multi-stage:从docker 17.05版本开始,docker支持multi-stage(多阶段构建),特别适合编译型语言,如我在一个镜像下编译,在另外一个很小的系统运行,如下图,go项目在golang环境下编译,在alpine环境下运行 [图片]
2019-03-27 - Web直播,你需要先知道这些
转自IMWeb社区,原文链接 Web直播,你需要先知道这些 直播知识小科普 一个典型的直播流程:录制->编码->网络传输(推流->服务器处理->CDN分发)->解码->播放 IPB:一种常用的视频压缩方案,用I帧表示关键帧,B帧表示前向差别帧,P帧表示双向差别帧 GOP (Group of Pictures):GOP 越长(I帧之间的间隔越大),B 帧所占比例越高,编码的率失真性能越高。虽然B帧压缩率高,但解码时CPU压力会更大。 音视频直播质量好坏的主要指标:内容延时、卡顿(流畅度)、首帧时长 音视频直播需要克服的主要问题:网络环境、多人连麦、主辅路、浏览器兼容性、CDN支持等 MSE(Media Source Extensions):W3C 标准API,解决 HTML5 的流问题(HTML5 原生仅支持播放 mp4/webm 非流格式,不支持 FLV),允许JavaScript动态构建 [代码]<video>[代码] 和 [代码]<audio>[代码] 的媒体流。可以用MediaSource.isTypeSupported() 判断是否支持某种MINE类型。在ios Safari中不支持。 [图片] 文件格式/封装格式/容器格式:一种承载视频的格式,比如flv、avi、mpg、vob、mov、mp4等。而视频是用什么方式进行编解码的,则与Codec相关。举个栗子,MP4格式根据编解码的不同,又分为nMP4、fMP4。nMP4是由嵌套的Boxes 组成,fMP4格式则是由一系列的片段组成,因此只有后者不需要加载整个文件进行播放。 Codec:多媒体数字信号编码解码器,能够对音视频进行压缩(CO)与解压缩( DEC ) 。CODEC技术能有效减少数字存储占用的空间,在计算机系统中,使用硬件完成CODEC可以节省CPU的资源,提高系统的运行效率。 常用视频编码:MPEG、H264、RealVideo、WMV、QuickTime。。。 常用音频编码:PCM、WAV、OGG、APE、AAC、MP3、Vorbis、Opus。。。 现有方案比较 RTMP协议 基于TCP adobe垄断,国内支持度高 浏览器端依赖Flash进行播放 2~5秒的延迟 RTP协议 Real-time Transport Protocol,IETF于1996提出的一个标准 基于UDP 实时性强 用于视频监控、视频会议、IP电话 CDN厂商、浏览器不支持 HLS 协议 Http Live Streaming,苹果提出的基于HTTP的流媒体传输协议 HTML5直接支持(video),适合APP直播,PC断只有Safari、Edge支持 必须是H264+AAC编码 因为传输的是切割后的音视频片段,导致内容延时较大 [图片] flv.js Bilibli开源,解析flv数据,通过MSE封装成fMP4喂给video标签 编码为H264+AAC 使用HTTP的流式IO(fetch或stream)或WebSocket协议流式的传输媒体内容 2~5秒的延迟,首帧比RTMP更快 WebRTC协议 [图片] 1、Google力推,已成为W3C标准 2、现代浏览器支持趋势,X5也支持(微信、QQ) [图片] 3、基于UDP,低延迟,弱网抗性强,比flv.js更有优势 方案 CPU占用 帧率 码率 延时 首帧 flv.js 0.4 30 700kbit/s 1.5s 2s WebRTC 1.9 30 700kbit/s 0.7s 1.5s 4、支持Web上行能力 5、编码为H264+OPUS 6、提供NAT穿透技术(ICE) **实际情况下,当用户数量很大时,对推流设备的性能要求很高,复杂的权限管理也难以实现,采用P2P的架构基本不可行。对于个别用户提供上行流、海量用户只进行拉流的场景,腾讯课堂实现了一种P2S的解决方案。**进一步学习可阅读jaychen的系列文章《WebRTC直播技术》。 [图片] 小程序+直播 技术方案 基于RTMP,官方说底层使用HTTP/2的一种内部传输机制,但又说是基于UDP的,这就搞不懂了。。。 live-pusher 和 live-player 没有限制第三方云服务 可直接使用腾讯云视频直播能力,只需配置好推流url、播放url即可 推流url: [图片] 播放url: [图片] 下面是我根据官网教程搭建的一个音视频小程序,搭建过程简单,同一个局域网下直播体验也很流畅(读者也可直接搜索腾讯视频云小程序进行体验): [图片] 前端核心代码还是相当简洁的: live-pusher组件:设置好url推流地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] live-player组件:设置后src音视频地址(仅支持 flv, rtmp 格式)等参数即可,使用bindstatechange获取播放状态变化 [图片] 能否和WebRTC同时使用? 对于腾讯课堂的应用场景,老师上课推流采用的是RTMP协议,考虑到WebRTC目前只能用于PC端拉流,那么在移动端能否让用户可以直接通过小程序来观看直播课呢?我觉得在技术层面可行的,接入小程序直播对于扩大平台影响力、社交圈分享、提高收费转化都会产生很大的帮助。难点在于复杂的权限控制、多路音视频流、多人连麦等问题,比如权限控制只能单独放到房间控制逻辑中完成,而音视频流本身缺乏这种校验;主辅路的切换还需要添加单独的信令控制,同时在小程序中加入相应的判断逻辑。 补充:最近看到已经有小程序的webrtc方案了,基于live-player、live-pusher组件,加入腾讯云强大的音视频后台服务,官方提供了一套封装度更高的自定义组件方案 —— <webrtc-room> ,甚至可以和Chrome打通。详情可以参考WebRTC 互通、webrtc-room [图片] 参考文章 HTTP 协议入门 使用flv.js做直播 面向未来的直播技术-WebRTC【视频、PPT】 小程序音视频能力技术负责人解读“小程序直播” 小程序开发简易教程 小程序音视频解读
2019-03-26 - Wxml2Canvas -- 快速生成小程序分享图通用方案
Wxml2Canvas库,可以将指定的wxml节点直接转换成canvas元素,并且保存成分享图,极大地提升了绘制分享图的效率。目前被应用于微信游戏圈、王者荣耀、刺激战场助手等小程序中。 github地址:https://github.com/wg-front/wxml2canvas 一、背景 随着小程序应用的日渐成熟,多处场景需要能够生成分享图便于用户进行二次传播,从而提升小程序的传播率以及加强品牌效应。 对于简单的分享图,比如固定大小的背景图加几行简短文字构成的分享小图,我们可以利用官方提供的canvas接口将元素直接绘制, 虽然繁琐了些,但能满足基本要求。 对于复杂的分享图,比如用户在微信游戏圈发表完话题后,需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制。又如王者荣耀助手小程序,需要将十人对局的详细战绩绘制成分享图,包含英雄数据、装备、技能、对局结果等信息,要绘制100多张图片和大量的文字信息,如果依旧使用官方的接口一步一步绘制,对开发者来说简直就是一场噩梦。我们急需一种通用、高效的方式完成上述的工作。 在这样的背景下,wxml2cavnas诞生了,作为一种分享图绘制的通用方案,它不仅能快速的绘制简单的固定小图,还能直接将wxml元素真实地转换成canvas元素,并且适配各种机型。无论是复杂的图文混排的富文本内容,还是展现形式多样的战绩结果页,都可以利用wxml2cavnas完美地快速绘制并生成所期望的分享图片。 二、Wxml2Canvas介绍及示例 1. 介绍 Wxml2Cavnas库,是一个生成小程序分享图的通用方案,提供了两种绘制方式: 封装基础图形的绘制接口,包括矩形、圆形、线条、图片、圆角图片、纯文本等,使用时只需要声明元素类型并提供关键数据即可,不需要再关注canvas的具体绘制过程; wxml直接转换成canvas元素,使用时传入待绘制的wxml节点的class类名,并且声明绘制此节点的类型(图片、文字等),会自动读取此节点的computedStyle,利用这些数据完成元素的绘制。 2. 生成图示例 下面是两张极端复杂的分享图。 2.1 游戏圈话题 [图片] 点击查看完整长图 2.2.2 王者荣耀战绩 [图片] 点击查看完整大图 三、小程序的特性及局限 小程序提供了如下特性,可供我们便捷使用: measureText接口能直接测量出文本的宽度; SelectorQuery可以查询到节点对应的computedStyle。 利用第一条,我们在绘制超长文本时便于文本的省略或者换行,从而避免文字溢出。 利用第二条,我们可以根据class类名,直接拿到节点的样式,然后将style转换成canvas可识别的内容。 但是和html的canvas相比,小程序的canvas局限性很多。主要体现在如下几点: 不支持base64图片; 图片必须下载到本地后才能绘制到画布上; 图片域名需要在管理平台加入downFile安全域名; canvas属于原生组件,在移动端会置于最顶层; 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容以及图片节点的链接。 针对以上问题,我们需要将base64图片转换jpg或png格式的图片,实现图片的统一下载逻辑,并且离屏绘制内容。针对第五条,好在SelectorQuery可以获取到节点的dataset属性,所以我们需要在待绘制的节点上显示地声明其类型(imgae、text等),并且显示地传入文本内容或图片链接,后文会有示例。 四、Wxml2Canvas使用方式 1. 初始化 首先在wxml中创建canvas节点,指定宽高: [代码] <canvas canvas-id="share" style="height: {{ height * zoom }}px; width: {{ width * zoom }}px;"> </canvas> [代码] 引入代码库,创建DrawImage实例,并传入如下参数: [代码] let DrawImage = require('./wxml2canvas/index.js'); let zoom = this.device.windowWidth / 375; let width = 375; let height = width * 3; let drawImage = new DrawImage({ element: 'share', // canvas节点的id, obj: this, // 在组件中使用时,需要传入当前组件的this width: width, // 宽高 height: height, background: '#161C3A', // 默认背景色 gradientBackground: { // 默认的渐变背景色,与background互斥 color: ['#17326b', '#340821'], line: [0, 0, 0, height] }, progress (percent) { // 绘制进度 }, finish (url) { // 画完后返回url }, error (res) { console.log(res); // 画失败的原因 } }); [代码] 所有的数字参数均以iphone6为基准,其中参数width和height决定了canvas画布的大小,规定值是在iphone6机型下的固定数值; zoom参数的作用是控制画布的缩放比例,如果要求画布自适应,则应传入 windowWidth / 375,windowWidth为手机屏幕的宽度。 2. 传入数据,生成图片 执行绘制操作: [代码] drawImage.draw(data, this); [代码] 执行绘制时需要传入数据data,数据的格式分为两种,下面展开介绍。 2.1 基础图形 第一种为基础的图形、图文绘制,直接使用官方提供接口,下面代码是一个基本的格式: [代码] let data = { list: [{ type: 'image', url: 'https://xxx', class: 'background_image', // delay: true, x: 0, y: 0, style: { width: width, height: width } }, { type: 'text', text: '文字', class: 'title', x: 0, y: 0, style: { fontSize: 14, lineHeight: 20, color: '#353535', fontFamily: 'PingFangSC-Regular' } }] } [代码] 如上,type声明了要元素的类型,有image、text、rect、line、circle、redius_image(圆角图)等,能满足绝大多数情况。 class类名指定了使用的样式,需要在style中写出,符合css样式规范。 delay参数用来异步绘制元素,会把此元素放在第二个循环中绘制。 x,y用来指定元素的起始坐标。 将css样式与元素分离的目的是便于管理与复用。 此种方式每个元素都相互独立,互不影响,能够满足自由度要求高的情况,可控性高。 2.2 wxml转换 第二种方式为指定wxml元素,自动获取,下面是示例: [代码] let data = { list: [{ type: 'wxml', class: '.panel .draw_canvas', limit: '.panel' x: 0, y: 0 }] } [代码] 如上,type声明为wxml时,会查找所有类名为draw_canvas的节点,并且加入到绘制队列中。 class传入的第一个类名限定了查询的范围,可以不传,第二个用来指定查找的节点,可以定义为任意不影响样式展现的通用类名。 limit属性用来限定相对位置,例如,一个文本的位置(left, top) = (50, 80), class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80)绘制。 由于小程序节点元素查询接口的局限,无法直接获取节点的文本内容和图片标签的src属性,也无法直接区分是文本还是图片,但是可以获取到dataset,所以我们需要在节点上显示地声明data-type来指明类型,再声明data-text传入文字或data-url传入图片链接。下面是个示例: [代码] <view class="panel"> <view class="panel__img draw_canvas" data-type="image" data-url="https://xxx"></view> <view class="panel__text draw_canvas" data-type="text" data-text="文字">文字</view> </view> [代码] 如上,会查询到两个节点符合条件,第一个为image图片,第二个为text文本,利用SelectorQuery查询它们的computedStyle,分别得到left、top、width、height等数据后,转换成canvas支持的格式,完成绘制。 除此之外,下面的示例功能更加丰富: [代码] <view class="panel"> <view class="panel__text draw_canvas" data-type="background-image" data-radius="1" data-shadow="" data-border="2px solid #000"></view> <view class="panel__text draw_canvas" data-type="text" data-background="#ffffff" data-padding="2 3 0 0" data-delay="1" data-left="10" data-top="10" data-maxlength="4" data-text="这是个文字">这是个文字</view> </view> [代码] 如上,第一个data-type为background-image,表示读取此节点的背景图片,因为可以通过computedStyle直接获取图片链接,所以不需要显示传入url。声明data-radius属性,表示要将此图绘成乘圆形图片。data-border属性表示要绘制图片的边框,虽然也可以通过computedStyle直接获取,但是为了避免非预期的结果,还是要声明传入,border格式应符合css标准。此外,图片的box-shadow等样式都会根据声明绘制出来。 第二个文本节点,声明了data-background,则会根据节点的位置属性给文字增加背景。 data-padding属性用来修正背景的位置和宽高。data-delay属性用来延迟绘制,可以根据值的大小,来控制元素的层级,data-left和data-top用来修正位置,支持负值。data-maxlength用来限制文本的最大长度,超长时会截取并追加’…’。 此外,data-type还有inline-text,inline-image等行内元素的绘制,其实现较为复杂,会在后文介绍。 五、Wxml2Canvas实现原理 1. 绘制流程 整个绘制流程如下: [图片] 因为小程序的限制,只能在画布上绘制本地图片,所以统一先对图片提前下载,然后再绘制,为了避免图片重复下载,内部维护一个图片列表,会对相同的图片链接去重,减少等待时间。 2. 基本图形的实现 基础图形的绘制比较简单,内部实现只是对基础能力的封装,使用者不用再关注canvas的绘制过程,只需要提供关键数据即可,下面是一个图片绘制的实现示例: [代码] function drawImage (item, style) { if(item.delay) { this.asyncList.push({item, style}); }else { if(item.y < 0) { item.y = this.height + item.y * zoom - style.height * zoom; }else { item.y = item.y * zoom; } if(item.x < 0) { item.x = this.width + item.x * zoom - style.width * zoom; }else { item.x = item.x * zoom; } ctx.drawImage(item.url, item.x, item.y, style.width * zoom, style.height * zoom); ctx.draw(true); } } [代码] 如上,x,y值坐标支持传入负值,表示从画布的底部和右侧计算位置。 3. Wxml转Canvas元素的实现 3.1 computedStyle的获取 首先需要获取wxml的样式,代码示例如下: [代码] query.selectAll(`${item.class}`).fields({ dataset: true, size: true, rect: true, computedStyle: ['width', 'height', ...] }, (res) => { self.drawWxml(res); }) [代码] 3.2 块级元素的绘制 对于声明为image、text的元素,默认为块级元素,它们的绘制都是独立进行的,不需要考虑其他的元素的影响,以wxml节点为圆形的image为例,下面是部分代码: [代码] if(sub.dataset.type === 'image') { let r = sub.width / 2; let x = sub.left + item.x * zoom; let y = sub.top + item.y * zoom; let leftFix = +sub.dataset.left || 0; let topFix = +sub.dataset.top || 0; let borderWidth = sub.borderWidth || 0; let borderColor = sub.borderColor; // 如果是圆形图片 if(sub.dataset.radius) { // 绘制圆形的border if(borderWidth) { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setStrokeStyle(borderColor) ctx.setLineWidth(borderWidth) ctx.stroke() ctx.closePath() } // 绘制圆形图片的阴影 if(sub.boxShadow !== 'none') { ctx.beginPath() ctx.arc(x + r, y + r, r + borderWidth, 0, 2 * Math.PI) ctx.setFillStyle(borderColor); setBoxShadow(sub.boxShadow); ctx.fill() ctx.closePath() } // 最后绘制圆形图片 ctx.save(); ctx.beginPath(); ctx.arc((x + r), (y + r) - limitTop, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(url, x + leftFix * zoom, y + topFix * zoom, sub.width, sub.height); ctx.closePath(); ctx.restore(); }else { // 常规图片 } } [代码] 如上,块级元素的绘制和基础图形的绘制差异不大,理解起来也很容易,不再多述。 3.3 令人头疼的行内元素的绘制 当wxml的data-type声明为inline-image或者inline-text时,我们认为是行内元素。行内元素的绘制是一个难点,因为元素之前存在关联,所以不得不考虑各种临界情况。下面展开细述。 3.3.1 纯文本换行 对于长度超过一行的行内元素,需要计算出合适的换行位置,下图所示的是两种临界情况: [图片] [图片] 如上图所示,第一种情况为最后一行只有一个文字,第二种情况最后一行的文字长度和宽度相同。虽然长度不同,但都可通过下面代码绘制: [代码] let lineNum = Math.ceil(measureWidth(text) / maxWidth); // 文字行数 let sinleLineLength = Math.floor(text.length / lineNume); // 向下取整,保证多于实际每行字数 let currentIndex = 0; // 记录文字的索引位置 for(let i = 0; i < lineNum; i++) { let offset = 0; // singleLineLength并不是精确的每行文字数,要校正 let endIndex = currentIndex + sinleLineLength + offset; let single = text.substring(currentIndex, endIndex); // 截取本行文字 let singleWidth = measureWidth(single); // 超长时,左移一位,直至正好 while(singleWidth > maxWidth) { offset--; endIndex = currentIndex + sinleLineLength + offset; single = text.substring(currentIndex, endIndex); singleWidth = measureWidth(single); } currentIndex = endIndex; ctx.fillText(single, item.x, item.y + i * style.lineHeight); } // 绘制剩余的 if(currentIndex < text.length) { let last = text.substring(currentIndex, text.length); ctx.fillText(last, item.x, item.y + lineNum * style.lineHeight); } [代码] 为了避免计算太多次,首先算出大致的行数,求出每行的文字数,然后移位索引下标,求出实际的每行的字数,再下移一行继续绘制,直到结束。 3.3.2 非换行的图文混排 [图片] 上图是一个包含表情图片和加粗文字的混排内容,当使用Wxml2Canvas查询元素时,会将第一行的内容分为五部分: 文本内容:这是段文字; 表情图片:发呆表情(非系统表情,image节点展现); 表情图片:发呆表情; 文本内容:这也; 加粗文本内容:是一段文字,这也是文字。 对于这种情况,执行查询computedStyle后,会返回相同的top值。我们把top值相同的元素聚合在一起,认为它们是同一行内容,事实也是如此。因为表情大小的差异以及其他影响,默认规定top值在±2的范围内都是同一行内容。然后将top值的聚合结果按照left的大小从左往右排列,再一一绘制,即可完美还原此种情况。 3.3.3 换行的图文混排 当混排内容出现了换行情况时,如下图所示: [图片] 此时的加粗内容占据了两行,当我们依旧根据top值归类时,却发现加粗文字的left值取的是第二行的left值。这就导致加粗文字和第一部分的文字的top值和left值相同,如果直接绘制,两部分会发生重叠。 为了避免这种尴尬的情况,我们可以利用加粗文字的height值与第一部分文字的height值比较,显然前者是后者的两倍,可以得知加粗部分出现了换行情况,直接将其放在同组top列表的最后位置。换行的部分根据lineHeight下移绘制,同时做记录。 最后一部分的文本内容也出现了换行情况,同样无法得到真正的起始left值,并且其top值与上一部分换行后的top值相同。此时应该将他的left值追加加粗换行部分的宽度,正好得到真正的left值,最后再绘制。 大多数的行内元素的展现形式都能以上述的逻辑完美还原。 六、总结 基于基础图形封装和wxml转换这两种绘制方式,可以满足绝大多数的场景,能够极大地减少工作量,而不需要再关注内部实现。在实际使用中,二者并非孤立存在,而更多的是一起使用。 [图片] 如上图所示,对于列表内容我们利用wxml读取绘制,对于下部的白色区域,不是wxml节点内容,我们可以使用基础图形绘制方式实现。二者的结合更加灵活高效。 目前Wxml2Canvas已经在公司内部开源,不久会放到github上,同时也在不断完善中,旨在实现更多的样式展现与提升稳定性和绘制速度。 如果有更好的建议与想法,请联系我。
2019-02-28 - 小程序开发之 web-view 的进阶玩法
背景 半年前写过一个项目,在京东手机的小程序里内嵌老罗的锤子发布会的活动页。前几天老罗又发布了他的加湿器,而这份关于锤子项目的迟到的总结,经过这几天在全网搜索的相关问题来看,依然有必要写一下。 本文主要从 web-view 与 JSSDK 的实现来展开,顺带过一下 web-view 的基础,最后在文末发放一些实用的小糖豆。 ok,废话不多说,开始吧。 web-view 中可用的 JSSDK 接口 1. web-view 的本质 与 JSSDK 我们已知,web-view 是闭环的小程序对外开启的一扇窗户,是小程序承载网页链接的容器。web-view 就像小程序里内置的浏览器内核,可以运行网页,其实 web-view 本质即是微信的 WKWebview 的实现。 区别:小程序框架系统包括两部分,视图层和逻辑层,两者对应的技术实现分别是 webview 和 JSCore;web-view本质上就是一个浏览器,承载网页,包括视图和逻辑实现。 [图片] 微信Webview 不仅应用于小程序的 web-view,也应用于公众号等所有微信里可以打开网页的位置。 微信Webview 不仅集成了普通 webkit 引擎的基础功能,还注入了微信JSBridge(JS-SDK)相关的脚本,提供给开发者更多高效的能力,如:拍照、语音、位置基于手机系统的能力;扫一扫、微信分享、卡券、支付等微信个性化能力。 而基于微信Webview 的 web-view 组件,除开放承载页面的功能外,也被赋予了一些 JS-SDK 的使用能力,尽管有一定的限制,但整理来看也使 web-view 的能力变得强大了。 2. web-view 中可用的 JSSDK 接口 本文从两个维度介绍 JSSDK 的接口。只简单列举几个,更多可支持的接口还请查看web-view的文档。 [图片] 1) 可用的通用JSSDK接口 接口模块 接口说明 接口名称 判断客户端是否支持js checkJSApi 图像接口 拍照或上传 chooseImage 图像接口 预览图片 previewImage 图像接口 上传/下载图片 uploadImage/downloadImage 图像接口 获取本地图片 getLocalImgData 音频接口 开始/停止录音 startRecord/stopRecord 音频接口 播放/暂停/停止语音 playVoice/pauseVoice/stopVoice 地理位置 使用内置地图/获取地理位置 getLocation/openLocation 蓝牙接口 开启/关闭/监听 start/stopSearchBeacons/onSearchBeacons 扫码 微信扫一扫 scanQRCode 卡券 列表/添加/打开 chooseCard/addCard/openCard 长按 小程序圆形码 智能接口 识别音频 translateVoice 2)与小程序相关的接口 除通用的JSSDK接口外,web-view 还支持和小程序跳转相关的接口,比如:navigateTo、redirectTo、switchTab、navigateBack,类似这种。具体写法都是加上 wx.miniProgram. 这样。 此外,关于小程序和web-view 的通信需要请求:wx.miniProgram.postMessage 方法,小程序侧进行侦听即可,具体方法可参看公众号的另一篇文章。 3)这些接口的可用性? 能够运行这些接口的最前提的条件就是,需要在小程序的环境里进行。那如何判断是否在小程序的环境里? 有两种方法: wx.miniProgram.getEnv [代码]wx.miniProgram.getEnv(function(res) { console.log(res.miniprogram) // true }) [代码] window.__wxjs_environment [代码]console.log(window.__wxjs_environment === 'miniprogram') [代码] web-view 如何使用 JSSDK 接口? 了解 web-view 可用的API 后,我们知道,在嵌入的H5 页面里,可以从相机里选择图片,或使用扫一扫的功能,那具体我们该如何实现呢? [图片] 微信公众平台给出了详尽的 JS-SDK 的实现方法,我们这里将几个当年踩过的要点给出。 1)绑定域名。 首先需要登录相关联的公众号,嗯!你没有看错,确实是公众号,(有很多人吐槽此事,但这确实是目前的事实。)登录后进入“公众号设置”-“功能设置”,填写“JS接口安全域名”。 [图片] 这一行为,建立了网页域名和 appId 之间的绑定关系,即,该appId 下可以打开这几个域名白名单里的网页。 同web-view 的业务域名配置一样,也需要将生成的校验文件拷贝到域名指向的 web 服务器的目录下。 2)签名的实现。 [图片] 签名是进入下一步的必要条件,这部分交由后端实现,了解它会提升你们的联调能力。 第一步,获取微信网页授权,拿到access_token值。 接口: https请求方式: [代码]GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET [代码] 参数 是否必须 说明 grant_type 是 获取access_token填写client_credential appid 是 第三方用户唯一凭证 secret 是 第三方用户唯一凭证密钥,即appsecret 公众号和小程序均可以使用 AppID 和 AppSecret 调用本接口来获取access_token。 AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。 需要设置IP白名单:登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。小程序无需配置IP白名单。 入口: [图片] 具体设置: [图片] 第二步,获得jsapi_ticket。 用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket): [代码]https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi [代码] [代码]{ "errcode":0, "errmsg":"ok", "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA", "expires_in":7200 } [代码] 第三步,生成签名。 初始字段:noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分)。 [代码]noncestr=Wm3WZYTPz0wzccnW jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg timestamp=1414587457 url=http://mp.weixin.qq.com?params=value [代码] 中间过程:对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串。 [代码]jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value [代码] 生成签名:对生成的字符串作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。 [代码]0f9de62fce790f9a083d5c99e95740ceb90c27ed [代码] 3)当前页面注入权限验证配置。 所有需要使用 JSSDK 的页面必须首先注入配置信息,否则无法调用 JSSDK 的 API。在2)中我们有生成的签名和生成签名依赖的时间戳和随机串,这些都是我们进行配置的必要入参。具体如下: [代码]wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: '', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '',// 必填,签名 jsApiList: [] // 必填,需要使用的JS接口列表 }); [代码] 其中 apilist 是本页面里支持使用的 JSSDK 的 API,如果不声明,是不可以被调用使用的。 在下一步4)中会有一个实例。 4)万事俱备之后 config 接口是异步请求的,所以增加 ready 的接口来判定config 是否已经具备;同时,也提供了error 接口来实现 config 失败后的处理。 哈哈,以上基本就是 JSSDK 在小程序 web-view 里的打通实现,因为中间牵扯前后端的实现,看起来篇幅长且凌乱,不知道你还能不能看到这里呢? 如果你是前端童鞋,先跟你报个喜,绑定域名和实现签名这些都不是你需要做的事情,把这些丢给你的后端童鞋,让他们来实现吧。但我还是提个醒,这些我们有必要知道,因为你在打通 web-view 和 JSSDK 时,可能无从下手。 5)一个例子帮你搞定 以下我们用一个例子,把前端方案的实现串起来,给大家一个直观的展示。 引入微信 JSSDK 库: [图片] 请求后端生成签名的接口,获取相关config依赖的字段: [图片] 在ready后,使用 wx.chooseImage() 选择本地相册的图片。 [图片] ok,到此为止,我们基本把 JSSDK 在 web-view 里的应用实现介绍完了。 总的来说,这是一个看起来比较简单,但实现起来可能有很多不可知问题的事情,我们当年苦于不了解需要在公众号下申请各种资源,不知道使用哪个 JSSDK 走了许多弯路,后来还踩过设置IP白名单的问题。这些都还是比较好解决的,本文也基本把这些很详尽的做了描述。在签名的实现中,还会有很多问题,还好我们当时也根据文档附录中的指点一一解除。 长征路漫漫,还好有分享~ 糖豆:web-view 在实际场景中的应用问题 [图片] 在这些和 web-view 相伴相生的日子里,也逐渐总结了一些问题,大致分为了几类,列举如下: 1. 开放能力的问题: 是否开放支付,是否支持直播,是否支持h5里添加分享按钮? 答案否! 插件禁用 webview 组件 个人账号可以测试,无法上线 小程序无法读取 web-view 的 cookie。 小程序不可以触发模板消息 类似以上问题,都可以归结为小程序的 web-view 对 JSSDK 的开放能力,须严格按微信的JSSDK提供的能力检查是否可行。 能否嵌套了其他的第三方网站的页面?答案:一切h5里引入的链接都需要加业务域名。 喜报:支持打开相关联公众号的文章!! web-view 对外部网页的开放能力都是基于业务域名的白名单设置! 感谢开放关联公众号的打开权限,运营小伙伴可以玩起来。 2. 白屏问题? 紧接上文,强化一下业务域名配置问题。很多业务域名配置失败的直接症状就是白屏,然后给一句话,引用非业务域名。问题直接明了!那如何解决? 1)优先查看业务配置是否ok,是否是https等。2)不能解决问题继续查看其他证书,如TSL版本。具体可查阅小程序HTTPS证书的约定。 3. 跳转问题。 首页是web-view 时没有回退按钮?层级过深时,回退过多? 这个是经常被问到的问题,首页web-view 没有回退,官方没有给出很明确的说法;而非首页的 web-view 一旦路由跳转建立起来,web-view 里的路由就默认加入了路由栈中。有什么好办法解决? 这个已跟小程序没关系了,唤醒你在浏览器里的处理吧。 4. 缓存、网页不刷新问题? 这个也可以归结为浏览器里如何处理缓存的问题,类似浏览器网页中的缓存,可以通过时间戳。另外看看是不是你的CDN 缓存了内容? 5. 分享问题? 分享接口拿到的 webViewUrl 只是第一个 url,如果你的页面已经进入到它的子页面中,再次分享时,这时候你分享得到的 url 没有变化。这时候 你需要在子页面中使用 postMessage,捕获到当前页面的路径,传递给小程序。当用户触发分享时,只要读取消息队列最近的一条数据即可。 记得拼接参数时需要encodeURIComponent。 6 web-view 改宽高? 不可以。默认铺满整屏。web-view 和 其他UI组件的关系是互斥的,并且小程序会优先选择展示 web-view。如果你想放多个web-view,很抱歉,它默认只展示第一个web-view 的内容,还会在控制台给个大大的报错。 7 web-view 提示“未绑定网页开发者”? 没有开发该网页的权限!需要在公众号的开发者工具里绑定开发者微信号。 8 web-view 的校验文件是什么格式? 官方回答:是一个含有普通字符串的txt文件,只是一个随机的字符串,与appid无关。放心使用~ 好啦~以上是对 web-view 常见问题的总结,总结不敢保证全对,另外一定是会过时的,所以,这只是你遇到问题的一个思考方向,具体方案还要请参看小程序官方文档。 如果你有更好的方案,欢迎回复我们,超级无敌感谢! 最后,感谢你能够读到这里,也愿你不枉此行。 扩展阅读 [1] 微信JS-SDK说明文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115 [2] 微信小程序开放能力-web-view https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html 文章原载于公众号:全栈探索。
2019-02-21 - Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)
[图片] 前言 笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序、2 个支付宝小程序。 之所以选用 Taro,解决微信小程序原生开发的痛点是一方面,另一方面团队也有多端统一开发的诉求,Taro 无疑是当时支持最好的。另外 React 也符合个人及团队的整体技术栈,可显著降低团队学习成本。 可以说,Taro 在小程序端、H5 端支持程度已经不错,也有不少上线实例可以查看,但在 React Native 的支持上,Github 中公开的项目在 RN 这块均未适配: [图片] 这种现况可以理解,毕竟要做到多端统一是有一定难度的,需准确把握各端差异,并做出合理取舍,而 Taro 虽以多端为设计目标,可重心在小程序端,没有对多端做出一定的开发约束,无从下手也便正常。笔者曾在 2018 iWeb 峰会 - 厦门站做过《多端统一开发实践》的分享,提到用 Taro 开发 RN 端的坑与大体思路,并加以实践。 结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨本文的重点:Taro 开发多端应用的正确姿势。 相关代码已开源:https://github.com/js-newbee/taro-yanxuan。 在线预览 小程序端已支持微信小程序、支付宝小程序,但无法提供在线版,请 clone 代码本地运行。 H5 端、RN 端可在线预览(直接调用了网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录): 小程序 H5 - 访问链接 React Native 请 clone 代码本地运行 [图片] Expo Snacks 如下是 React Native 的运行截图: 首页、分类 二级分类、详情 购物车、个人 [图片] [图片] [图片] 样式管理 样式管理是多端开发的首要挑战,因为 React Native 与一般 Web 样式支持度差异较大,上述几个未适配 RN 的多端项目多数已栽在样式上了,用到了大量 RN 不支持的样式,这种情况再要去兼容 RN 无异于重写页面,想必也是有心无力了。这也是本文所强调的,需把握正确的多端开发姿势。 样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制,核心可以用三点来概括: 使用 Flex 布局 基于 BEM 写样式 采用 style 属性覆盖组件样式 使用 Flex 布局 在进一步阐述之前,需先了解 RN 端几个影响样式方案的主要差异: [代码]display[代码] 只有 [代码]flex / none[代码],[代码]position[代码] 只有 [代码]relative / absolute[代码]; 不支持标签选择器、子代选择器、伪元素,不支持 [代码]background: url()[代码] 等; 文本要用 [代码]Text[代码] 标签包裹,文本的样式不能加在 [代码]View[代码] 标签上,只能加在 [代码]Text[代码] 标签上。 使用 Flex 布局,不单单是因为 RN 的 [代码]View[代码] 标签有默认样式 [代码]display: flex; flex-direction: column[代码],更重要的是 Flex 可以解决幽灵空白问题: [代码]// View 标签高度不会是 100px,图片下方会有几像素空白,称为幽灵空白 <View> <Image src={...} style={{ height: '100px' }} </View> [代码] 常规解决方案是在 View 标签上设置 [代码]font-size / line-height: 0[代码], 或 Image 标签 [代码]display: inline-block[代码] 等,但这些在 RN 中都不支持,给 View 标签设置 [代码]display: flex[代码] 算是唯一可靠方案了。 何况 Flex 布局能力强大,为啥不用呢?只需要注意一点,RN 中 View 标签默认主轴方向是 [代码]column[代码],如果不将其他端改成与 RN 一致,就需要在所有用到 [代码]display: flex[代码] 的地方都显式声明主轴方向。 基于 BEM 写样式 RN 实际上只支持一种样式声明方式,即声明 style 属性: [代码]<View style={{ height: '100%' }} [代码] 这也导致 Taro 在 RN 端基本只支持 class 选择器这一种写法(最终编译成对象字面量),BEM(Block Element Modifier)在此处就恰如其分的发挥了作用: 避免样式冲突(RN、小程序样式独立,但 H5 不是) 自解释、语义化 例如每行 2 个元素的列表,每行最后 1 个元素有特定样式,用伪元素选择器 [代码]:nth-child(even)[代码] 很容易实现,在 RN 中就需要自行计算了: [代码]{list.map((item, index) => ( <View className={classNames('block__element', index % 2 === 1 && 'block__element--even' )} /> )} [代码] 基于 BEM 写 class 样式,不依赖其他选择器,虽然会让代码稍显繁琐,但也能保证多端都是行得通的,不存在支持问题。 采用 style 属性覆盖组件样式 小程序、RN 在页面、组件间传递样式时均有问题: [代码]// 目前 Taro RN 端还未实现往组件传递 className 对应样式 <CompA compClass='my-style' /> // CompA,样式不生效 <View className={this.props.compClass} /> [代码] 上述场景小程序虽可通过组件外部样式 externalClasses 实现,但官网文档有强调 “在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况”;用全局样式倒是可以,但这样样式就不好维护了。 那么,通过 style 传递、覆盖组件样式也就成了唯一可选方案了。需要注意一点,样式文件是会经过编译处理兼容多端的,但 style 方式需要运行时兼容: [代码]<Comp style={postcss({ background: '#fff' })} /> // 简单演示,如 RN 不支持 background,需改成 background-color function postcss(style) { const { background, ...restStyle } = style const newStyle = {} if (background) { newStyle.backgroundColor = background } return { ...newStyle, ...restStyle } } [代码] 从这个角度看,styled-components 或许是多端开发的最佳样式方案,然而 Taro 还不支持。另外微信小程序官方文档中有提到 “尽量避免将静态的样式写进 style 中,以免影响渲染速度”,全部样式都写到 style 属性中恐怕不靠谱,但只用来覆盖少量样式不见得会有太大影响。 样式兼容 即便是把握了如上样式管理思路,多端样式差异的问题依然存在,例如 [代码]white-space: nowrap[代码] 这个样式在 RN 端会报错,Taro 有提供解决方案: [代码].text { /*postcss-pxtransform rn eject enable*/ white-space: nowrap; /*postcss-pxtransform rn eject disable*/ } [代码] 但项目中不止一处会有这个问题,都这样写实在不太美观,可以用 Sass mixins 稍微封装下: [代码]@mixin eject($attr, $value) { /*postcss-pxtransform rn eject enable*/ #{$attr}: $value; /*postcss-pxtransform rn eject disable*/ } .text { @includes eject(white-soace, nowrap); } [代码] Sass mixins 并不能解决差异,但对于部分各端不兼容的样式,通过 Sass mixins 统一处理是比较合理的方式,代码相对美观也方便维护。 端能力差异 相较于样式,端能力的差异倒是还好,各端差异是客观存在的,更不用说 RN 在 iOS 与 Android 上就已存在大量差异。 应对端能力差异,要么改变实现思路,例如 RN 端还不支持 [代码]Taro.(get/set)StorageSync[代码],那就改用 [代码]async / await[代码] + [代码]Taro.(get/set)Storage[代码] 实现,要么就得使用环境判断方式了。 Taro 提供 [代码]process.env.TARO_ENV[代码] 用于环境判断,多数小的差异都可以用这种方式来解决: [代码]function foo() { if (process.env.TARO_ENV === 'weapp') { // 微信小程序逻辑 } if (process.env.TARO_ENV === 'h5') { // H5 逻辑 } if (process.env.TARO_ENV === 'rn') { // RN 逻辑 } } [代码] 这个时候也比较考验开发者的封装能力了,一般是建议将这些差异逻辑的判断统一起来,例如在 src/utils 中进行封装,对外提供一致的接口,尽量不要在业务页面中杂糅太多的判断。 而对于简单的环境判断处理不了的问题,就只能动用原生开发了,例如 Taro 还不支持 RN 端的 WebView 组件,就需要自己用原生 RN 实现: [代码]// Taro 页面,根据环境引入 RN 原生页面 import { WebView } from '@tarojs/components' const WebViewRN = process.env.TARO_ENV === 'rn' ? require('./rn').default : null export default class extends Component { render() { return process.env.TARO_ENV === 'rn' ? <WebViewRN src={this.url} /> : <WebView src={this.url} /> } } // 原生 RN 页面,从 react-native 引入 WebView import Taro, { Component } from '@tarojs/taro' import { WebView } from 'react-native' export default class WebViewRN extends Component { render() { return <WebView source={{ uri: this.props.src }} /> } } [代码] [代码]process.env.TARO_ENV[代码] 的处理是编译时而不是运行时,也就是说若不是编译 RN,上述用原生写的 RN 页面不会被打包,保证了编译成其他端时不会引入不支持的内容。 原生页面能够引入,多端问题也就有了基本的实现保障。 Taro RN 端的坑 Taro RN 端目前小问题还是不少的,本项目开发过程中也顺带解了几个 bug: [图片] 除此之外还有好几个问题,时间关系还未提 pr 解决,暂且先绕过,但其中有两个坑还是值得一说的。 onClick RN 的 View 标签不支持 onClick ,但这又是很通常的需求,原生解决方式是套一层 Touchable 组件,如: [代码]<TouchableOpacity onPress={this.handlePress}> <View>{...}</View> </TouchableOpacity> [代码] 而 Taro 是引入 [代码]PanResponder[代码] 响应用户操作: [代码]<View {...PanResponder.carete({ ...})} style={wrapperStyle} > <WrappedComponent style={innerStyle} /> </View> [代码] 问题在于这样多嵌套了一层 View,并把样式拆分成 wrapperStyle、innerStyle 分别应用,但样式拆分有问题,导致绑定 onClick 之后元素的样式错乱了,这点在开发过程中还是相当坑的。 宽高自适应 onClick 的问题也还好,改改样式能绕过去,宽高自适应的坑就比较尴尬了。 小程序、H5 可用 [代码]rpx / em[代码] 实现自适应,而 RN 的自适应方案麻烦些,一般需通过 [代码]Dimensions[代码] 获取宽高再进行换算。Taro.pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,即 [代码]width: 100px[代码] 会被编译成 [代码]width: 50[代码],而不是 [代码]width: Taro.pxTransform(100)[代码],无法适配不同的屏幕尺寸。 因此,目前 Taro RN 端还不好做到自适应,要么非百分比的宽高都用 style + Taro.pxTransform(),要么就得自己写个脚本去处理编译后的样式文件。 这两个问题都提了 issue 2204 2205,有需要的可以关注下解决进度 Taro H5 端的坑 Taro 对 H5 端的支持度尚可,若仅仅想要实现兼容小程序与 H5,也仍建议采用 BEM 写样式 + style 属性覆盖组件样式的方案,可以有效规避小程序自定义组件的诸多局限,只是在 CSS 特性上就不用像 RN 那样拘束,transform、伪元素等使用起来无压力。 另外就是小程序、RN 都没有跨域问题,但 H5 会有,这个可通过 devServer.proxy 解决,以及编译打包的静态资源是固定文件名,建议改成带 hash 值方便缓存管理,这些配置在项目里的 src/config 中都能找到,就不再复述了。 H5 端的坑更多的是集中在内置组件不够完善、端能力缺失较多,毕竟 Taro 的设计是以微信小程序为基准,去补充其他端的差异,编译成小程序就是直接用的小程序内置组件,但在 H5 端就需要一整套功能对等的内置组件,Taro 要做到一致所需的繁杂细节也可想而知。 举一个比较明显的坑来说,就是还不支持 [代码]Taro.switchTab()[代码],暂时只能用如下方式先绕过: [代码]if (process.env.TARO_ENV === 'h5') { Taro.navigateBack({ delta: Taro.getCurrentPages().length - 1 }) setTimeout(() => { Taro.redirectTo({ url }) }, 100) } [代码] 好在官方已计划在接下来的 1.3 版本重构 H5 TabBar,到时这个问题也就解决了。 其他 要做到多端统一,能说的细节点实在太多,上述实现思路虽然简单,但背后也都是隐含着对各端差异的斗争与取舍,本文也仅是列出最基本的几点,用于阐述 Taro 多端开发的核心思路。 本项目代码没有做过多封装,方便阅读,也实现了足够多的样式细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 [代码]// TODO[代码](Taro 还未支持的)、[代码]// NOTE[代码](开发技巧、注意事项)注明了,更多内容就有待各位去实践、体会了。 [图片] 总结 如前言所说,Taro 虽然是以多端为设计目标,但重心是小程序端,RN 端目前的支持情况不算特别理想。但充分理解多端差异、掌握正确的多端开发姿势(特别是样式管理方面,避免项目成型后再去兼容需要大动刀斧)之后,在简单的项目上是完全可以一展拳脚的。 若说 2 个礼拜开发一个小程序,是稀疏平常的事,但 2 个礼拜即搞定了小程序端(微信、支付宝、百度等等),还搞定了 H5、React Native 端,后续更新也只要改一处地方,这产出、维护效率就实在太惊人了,这大抵也就是 “Write once, run anywhere” 的魅力所在(虽然在前端领域极容易发展成 “Write once, debug everywhere” 😂) 相信随着小程序热度不断上升,还会有更多优秀的开源框架、解决方案涌现。而我们不倾向于造轮子,更关注基于现有方案如何更好地去开发多端应用。若有兴趣的前端小伙伴,不妨加入我们,一起搞事 caiminxing#qudian.com 😁 本文由趣店 FED 出品,首发于趣店技术学院;项目开源地址 https://github.com/js-newbee/taro-yanxuan。
2019-02-21 - 在公用Util文件中给小程序页面变量赋值
util文件为小程序公用的一个方法文件,我们可以将程序内经常用的方法写在这个文件中,通过其他文件引用,可以达到一个方法全局使用的方法,减少代码量,一定程度上减少维护时间.这是util中是没有办法用this.setData()方法去设置变量值的.因为他不属于任何一个页面.那我们如果想要在util文件的方法中给页面赋值怎么实现呢? 这里用到了微信公用方法:getCurrentPages() 之前做过一个案例希望可以加载页面的时候自动加载底部导航栏实现自定义热更新的小程序底部导航栏,如果分开写的话每个导航页面都需要写一个方法,就考虑想用Util写一个公共方法 代码如下: //获取底部导航方法 function settabbar() { wx.request({ url: getApp().globalData.url + ‘syssetting/gettabbar’,//获取底部导航图标,文字信息 method: ‘POST’, header: {// 设置请求的 header ’content-type’: ‘application/x-www-form-urlencoded’ }, data: { appid: getApp().globalData.appid }, success: function (res) { wx.hideLoading() var pages = getCurrentPages(); //这里使用了getCurrentPages()方法,该方法是用来获取浏览记录的 console.log(pages) //打印结果如下图 var prevPage = pages[pages.length - 1] prevPage.setData({ tabbar: res.data, page:’/’+ prevPage.route }) }, fail: function (res) { wx.showToast({ title: ‘请求失败’, }) }, }) } [图片] 我们可以看到结果中的route表示了当前打开的页面 如果我们再首页点进商品详情页再打印出路由记录时会显示如下结果: [图片] 我们会发现变量pages为2个元素的数组了,展开第二个元素可以看到现在打开的商品详情页,第一个元素的路径为首页路径 这样我们可以得到的在Util页面中标示当前页面为pages[pages.length - 1] var prevPage = pages[pages.length - 1] prevPage.setData({ tabbar: res.data, page:’/’+ prevPage.route }) 我们用这个方法就可以在util页面中给引用的页面赋值了!!! PS:util文件的编写方法 和暴露接口的方法请参照小程序文档
2021-04-15