- 开发者福音!面向Web场景的云开发服务正式开放!
【导语】 继支持小程序开发之后,云开发也支持Web使用啦!开发者们可以使用云开发提供的云端能力,直接开发网站应用,如PC端网页、公众号中的网页等。由此开发者可以在网站应用中借助云函数实现业务逻辑,通过与云数据库、对象存储以及CDN等产品联动,即可实现产品快速上线和迭代。快来体验吧! [图片] 首先,还是得跟各位小伙伴们介绍下功能强大的云开发到底是何方神圣! 云开发(Tencent CloudBase)是腾讯云为移动开发者提供的一站式后端云服务,无需搭建服务器,就可以轻松使用云端能力。基于云开发,开发者无需再关心服务器和底层设施运维,只需专注于代码逻辑和业务本身。 目前云开发支持 小程序开发 和 Web开发。 云开发三大基础功能 目前云开发提供了三大基础能力:云数据库、云存储及云函数。这三种能力可以构成较完整的后端开发能力。 云函数 在云端运行的代码,开发者只需编写自身业务逻辑代码。 云端运行:无需采购、部署、运维传统硬件,节约人力及成本。 高效开发:每个函数单独运行、部署,上传代码后即可自动部署,提升了独立开发和迭代的速度。 弹性伸缩:根据请求量实现毫秒级实时弹性伸缩,函数未执行不产生任何费用。 云数据库 文档型数据库包含多个近似于 JSON 数组的集合,数组中的对象是记录,格式为 JSON 文档。 简单易用:数据库 API 包含增删改查,操作简单;支持触发器,满足特殊场景。 权限控制:通过 API 在客户端内和云函数内进行数据操作,安全可靠。 云存储 在网站应用前端直接上传或下载云端文件,在云开发控制台可视化管理。 快速上传:提供文件存储空间,可在客户端和云函数端通过 API 使用存储。 权限管理:基于用户身份的安全控制,带权限管理的云端下载。 CDN 加速:存储内的文件,天然 CDN 加速,提升用户体验。 云开发优势 开发更简单 使用云函数时,用户只需编写最重要的核心业务代码,不再需要关心负载均衡、自动伸缩、网关等组件,也无需构建应用的后端服务,极大地降低了后端开发搭建的复杂性。无需手动配置,云函数即可根据请求量自动横向扩缩,自动安排合理的计算资源满足业务需求。 开发更高效快捷 云函数不要求特定框架或依赖,开发者可以专注于核心代码的开发。同时开发人员可以组成多个小团队,单个模块的开发无需了解其他团队的代码细节。 独立开发和迭代的速度变得前所未有的快,帮助用户把握住产品上线的黄金时间。开发可以使用云函数编写一些目的单一、逻辑独立的业务模块,因而可以完全复用已经成熟的第三方代码实现。 运维更省事 每个云函数都是单独运行、单独部署、单独伸缩,用户上传代码后即可自动部署,免除单体式应用部署升级难的问题。 用户不再需要对 OS 入侵、登录风险、文件系统安全、网络安全和端口监听做复杂的配置和管理,一切交由平台处理,平台通过定制化的容器保证每个用户的隔离性。 成本更低 按请求数和资源端运行收费,极大节约时间和成本,同时还提供一定量端免费额度。 如何开通云开发的Web端服务? 云开发Web端服务在腾讯云官网登录使用。目前,云开发Web 端仅支持通过微信公众号登录方式进行服务端访问授权,故开发者若要开发网站应用,需要先在微信公众平台/微信开放平台进行应用注册及开通。 ↓详细开通流程可参考下图↓ [图片] 进入云开发主页 [图片] 选择“微信公众号”登录 [图片] 点击授权 [图片] 进入云开发控制台,新建环境 [图片] 填写环境信息,开通成功! Quick Start——使用云开发Web SDK 快速开发网站应用 初始化云开发能力 [图片] 登录授权 云开发目前在 Web 端支持通过微信登录方式进行服务端访问授权,故开发者若要开发网站应用,需要先在微信公众平台/微信开放平台进行应用注册及开通。 除了微信公众平台和微信开放平台登录方式以外,云开发将陆续支持邮箱、QQ、匿名登录、自定义登录等多种登录方式,敬请期待。 [图片] 应用关联 要使用云开发提供的云函数、云存储和云数据库的功能,您需要先将云开发添加到您的网站应用中,即应用关联。将云开发提供的 Web 端 SDK 关联到您的网站应用,才能使客户端通过 SDK 操作后台资源。复制下方的代码片段,粘贴到您的 HTML 代码底部(要在其他 script 标记之前),即可将云开发添加至您的网站应用。 [图片] 域名授权 为了增加安全性,云开发的身份验证服务需要先对网站应用来源进行验证,也即是域名授权。只有已授权域名下的页面才可以使用 SDK 发起对云开发服务的访问。将网站完整域名添加到安全验证到白名单中,即完成授权。 [图片] 操作数据库 快速完成对数据库基础的 CRUD 及服务端时间(serverDate)、正则查找(regExp)和地理位置(geo)等特殊数据结构的使用。 [图片] 操作文件存储 快速完成文件上传、获取下载链接或者删除文件等操作。 [图片] 操作云函数 快速实现对云函数的安全调用。 [图片] 关于新开放Web端云开发服务有任何问题欢迎在下方留言,我们会尽快回复~ 如果你有关于使用云开发CloudBase相关的技术故事/技术实战经验想要跟大家分享,欢迎留言联系我们哦~比心! [图片]
2019-09-19 - 云开发支付的代码,有需要的进。
真机测试已通过。你照抄就行,保证可通过。 最新完美版本可供参考: https://developers.weixin.qq.com/community/develop/article/doc/0004c4a50a03107eaa79f03cc56c13 小程序端: wx.cloud.callFunction({ name: 'getPay' , data: { total_fee: parseFloat(0.01).toFixed(2) * 100, attach: 'anything', body: 'whatever' } }) .then( res => { wx.requestPayment({ appId: res.result.appid, timeStamp: res.result.timeStamp, nonceStr: res.result.nonce_str, package: 'prepay_id=' + res.result.prepay_id, signType: 'MD5', paySign: res.result.paySign, success: res => { console.log(res) } }) }) 云函数:getPay getPay目录下共两个文件: 1、index.js 2、package.json index.js代码如下: const key = "YOURKEY1234YOURKEY1234YOURKEY123"//这是商户的key,不是小程序的密钥,32位。 const mch_id = "1413090000" //你的商户号 //将以上的两个参数换成你的,然后以下可以不用改一个字照抄 const rp = require('request-promise') const crypto = require('crypto') function paysign({ ...args }) { let sa = [] for (let k in args) sa.push( k + '=' + args[k]) sa.push( 'key=' + key) return crypto.createHash('md5').update(sa.join('&'), 'utf8').digest('hex').toUpperCase() } exports.main = async (event, context) => { const appid = event.userInfo.appId const openid = event.userInfo.openId const attach = event.attach const body = event.body const total_fee = event.total_fee const notify_url = "https://whatever.com/notify" const spbill_create_ip = "118.89.40.200" const nonce_str = Math.random().toString(36).substr(2, 15) const timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp let formData = "<xml>" formData += "<appid>" + appid + "</appid>" formData += "<attach>" + attach + "</attach>" formData += "<body>" + body + "</body>" formData += "<mch_id>" + mch_id + "</mch_id>" formData += "<nonce_str>" + nonce_str + "</nonce_str>" formData += "<notify_url>" + notify_url + "</notify_url>" formData += "<openid>" + openid + "</openid>" formData += "<out_trade_no>" + out_trade_no + "</out_trade_no>" formData += "<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" formData += "<total_fee>" + total_fee + "</total_fee>" formData += "<trade_type>JSAPI</trade_type>" formData += "<sign>" + paysign({ appid, attach, body, mch_id, nonce_str, notify_url, openid, out_trade_no, spbill_create_ip, total_fee, trade_type: 'JSAPI' }) + "</sign>" formData += "</xml>" let res = await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST',body: formData}) let xml = res.toString("utf-8") if (xml.indexOf('prepay_id')<0) return xml let prepay_id = xml.split("<prepay_id>")[1].split("</prepay_id>")[0].split('[')[2].split(']')[0] let paySign = paysign({ appId: appid, nonceStr: nonce_str, package: ('prepay_id=' + prepay_id), signType: 'MD5', timeStamp: timeStamp }) return { appid, nonce_str, timeStamp, prepay_id, paySign } } package.json 代码如下: { "name": "getPay", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "youself", "license": "ISC", "dependencies": { "crypto": "^1.0.1", "request-promise": "^4.2.2" } } 最后选择:上传和部署:云端安装依赖。
2019-12-14 - # 使用小程序云开发API更新数组中的单个数组元素
使用小程序云开发API更新数组中的单个数组元素 看了看mongoDB的更新数据方式,找到了解决办法,解决方法如下,亲测可用: 第一种方法:使用位置操作符$ [代码]代码,条件更新写在云函数中 [代码] [图片] [代码]test_api集合原始数据如下 [代码] [图片] [代码]在云函数中执行1中的代码,数组users中id为1001的用户添加了一个新的属性test [代码] [图片] [代码]原理分析 [代码] where条件是查找数组中id属性为1001的用户 update中的使用’users.$.test’: ‘test’ 注意里面的$符号,在mongoDB中,这个符号叫做位置操作符,代表数组的下标,如下引自《mongoDB实战》 [图片] 第二种方法:直接使用数组下标 云函数代码 [图片] test_api集合原始数据如下 [图片] 代码执行后 [图片] 相对于第一种方法,这种方法更加简单,只不过users.1.test这种写法有点颠覆js和java中的属性书写规则,让人感觉怪怪的,在mongoDB中,也支持点数字这种写法。 一个可能的疑惑 可不可以写作’users[1].test’:‘test’,测试结果如下: [图片] [图片] 可以看到’user[1]'无法被识别为数组的第二个元素,而是作为了属性名新增了一个属性,结论:必须写成”点数字“不能写成“中括号” 结论: 经过测试,使用这两种种方法可以更新数组中的一个元素。 方法一适合在不知道数组元素下标的情况下根据查询条件更新元素; 方法二适合在知道数组元素下标的情况下更新元素; 当然也存在既知道元素下标也可以通过属性查到的情况,想用哪个就看心情了-.- 但是暂未找到查询返回数组中的一个元素的方法,再探索探索吧 ——。——
2019-03-06 - 云开发付费终于来啦!大吐槽
经过多次跳票,云开发付费终于来了, 更新到 开发工具(Nightly 1.02.1907242),在云控制台就可以看到本次控制台更新,(看不见更新的多重启几次开发工具 看下最重要的收费价格表 [图片] 更详细的计费文档等看这里 emmmmm… 果然,与半年前发的 预测云开发价格,相差不多,就是按月收费了 最主要的问题仍然存在,大吐槽 每个套餐额度间,跨度约 10倍 太过粗糙 不支持按量付费 套餐额度超出直接限制使用,不支持超出部分按量付费 按套餐定价,小程序主无法对所有套餐指标使用的淋漓尽致。 假如我仅使用云函数功能,那数据库、CDN等其实是收费又浪费的存在,并且,如云函数调用一旦稍微超过额度限制,就必须选择价格为 10倍左右的下一阶段套餐 云开发是腾讯云的产品,那我们从腾讯云官网看看,对应类似产品的定价 腾讯云 - 云函数: [图片] 腾讯云 - 云存储(每月文件预计占用 标准存储容量1.5TB,产生 CDN 回源流量500GB,读请求500万次、写请求20万次 案例): [图片] 腾讯云 - CDN: [图片] 可以看到对应云产品价格其实是并不高的,最重要的是支持按量、按带宽、按月等,灵活定制收费。 腾讯云产品集成到了小程序中,定价更高了,灵活收费模式也没有了。 这样的定价策略,云开发怎么成为小程序的基础设施。 不说了,我看知晓云开发文档去了 #2020/03/04 更新# 云开发已支持按需付费,大爱
2020-03-04 - 求助:微信小程序云开发,切换环境,小程序APP断与云函数对应不同数据库
求助:微信小程序云开发,切换环境,小程序APP断与云函数对应不同数据库,操作不同的数据,小程序app端js文件操作的是dev的数据可,而云函数操作的生产环境的数据库 本来昨天是好的,今天早上发现云函数是生产环境,然后今天就把它切到开发环境,结果效果是反的,切到开发环境后,云函数反而操作起了生产环境。配置如下图: [图片] [图片] 这里都配的是开发环境,app端是开发环境,云函数是生产环境,请问老师们,云函数怎么把环境切到开发环境 此问题还是没有解决,现在是写在小程序端的数据库操作能切换正常,但是云函数切换不过去。
2019-07-09 - Vue项目使用kbone打包成小程序的踩坑笔记
以下内容是初版kbone的笔记,现在kbone进行了大量的更新升级,以下配置方法大部分已经不适用新版kbone了,如需详细了解kbone的使用方法,请参考官方文档 kbone是大佬 @June 开发的一款Vue项目打包成小程序的工具(暂时只支持微信小程序) 已有的Vue项目只需通过简单的配置即可打包成小程序的代码,新项目也可以使用这个工具进行同步开发,项目中提供了很多示例供参考 Github连接:https://github.com/wechat-miniprogram/kbone 为什么选择kbone? 市面上有很多跨平台的框架,如: WePY、mpvue、uni-app、taro等 他们都有一些弊端和限制,现有的项目迁移不方便,对第三方组件库不兼容等 kbone作为一款工具,也可以说是一款webpack插件(Vue项目只需要引入 mp-webpack-plugin 插件,通过简单的webpack配置即可),无论是对新项目还是已有项目都是非常友好的,因为他完整的移植了Vue Runtime,通过webpack将Vue代码打包成小程序兼容的代码,通过适配器运行,所以大部分的第三方组件库是兼容的 如何使用? Github仓库中提供了很多示例,有兴趣的可以体验下,下面介绍下我的使用过程 创建项目 因为习惯了使用Vue Cli,所以我选择使用这个工具初始化一个项目,使用的版本是Vue Cli 3(这个工具提供了一套完全图形化的用户界面,这里就不介绍使用方法了,没有使用过的同学可以 点击这里 查看创建方法) 集成插件 kbone目前还没有提供Vue Cli的插件(大佬很忙,有能力的同学可以在Github上提PR),所以需要手动添加,具体的操作方式请参考QuickStart文档 安装 mp-webpack-plugin 插件 [代码]yarn add mp-webpack-plugin --dev # 或者 npm install mp-webpack-plugin --save-dev [代码] 在 src 目录中新增 main.mp.js 入口文件 [代码]import Vue from 'vue' import App from '@/App' import router from '@/router' import store from '@/store' // 需要将创建根组件实例的逻辑封装成方法 export default function createApp () { // 在小程序中如果要注入到 id 为 app 的 dom 节点上,需要主动创建 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) Vue.config.productionTip = false return new Vue({ router, store, render: h => h(App) }).$mount('#app') } [代码] 在根目录创建 miniprogram.config.js 文件,添加 mp-webpack-plugin 插件配置 [代码]module.exports = { // 页面 origin,默认是 https://miniprogram.default origin: '', // 填写项目中的图片资源地址,建议图片资源使用线上地址 // 入口页面路由,默认是 / entry: '/', // 页面路由,用于页面间跳转 router: { // 路由可以是多个值,支持动态路由 index: [] }, // 特殊路由跳转 redirect: { // 跳转遇到同一个 origin 但是不在 router 里的页面时处理方式,支持的值:webview - 使用 web-view 组件打开;error - 抛出异常;none - 默认值;什么都不做,router 配置项中的 key notFound: 'index', // 跳转到 origin 之外的页面时处理方式,值同 notFound accessDenied: 'index' }, // app 配置,同 https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#window app: { navigationStyle: 'custom' // 自定义navigation }, // 全局配置 global: {}, // 页面配置,可以为单个页面做个性化处理,覆盖全局配置 pages: {}, // 优化 optimization: { domSubTreeLevel: 5, // 将多少层级的 dom 子树作为一个自定义组件渲染,支持 1 - 5,默认值为 5 // 对象复用,当页面被关闭时会回收对象,但是如果有地方保留有对象引用的话,注意要关闭此项,否则可能出问题 elementMultiplexing: true, // element 节点复用 textMultiplexing: true, // 文本节点复用 commentMultiplexing: true, // 注释节点复用 domExtendMultiplexing: true, // 节点相关对象复用,如 style、classList 对象等 styleValueReduce: 5000, // 如果设置 style 属性时存在某个属性的值超过一定值,则进行删减 attrValueReduce: 5000 // 如果设置 dom 属性时存在某个属性的值超过一定值,则进行删减 }, // 项目配置,会被合并到 project.config.json projectConfig: { appid: '', // 填写小程序的AppId projectname: '' // 填写小程序的项目名称 }, // 包配置,会被合并到 package.json packageConfig: { name: '', // 项目名称 description: '', // 描述 author: '' // 作者信息 } } [代码] 在根目录创建 .env.mp 文件,添加 mp 环境变量 [代码]NODE_ENV = mp [代码] 修改 vue.config.js 文件,添加打包小程序的 webpack 配置 [代码]const path = require('path') function resolve (dir) { return path.join(__dirname, dir) } const webpack = require('webpack') const MpWebpackPlugin = require('mp-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { css: { extract: true }, outputDir: process.env.NODE_ENV === 'mp' ? './dist/mp/common' : './dist/web', configureWebpack: { resolve: { extensions: ['*', '.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') } } }, chainWebpack: config => { if (process.env.NODE_ENV === 'mp') { config .devtool('node') .entry('app').clear().add('./src/main.mp.js').end() .output.filename('[name].js') .library('createApp') .libraryExport('default') .libraryTarget('window').end() .target('web') .optimization.runtimeChunk(false) .splitChunks({ chunks: 'all', minSize: 1000, maxSize: 0, minChunks: 1, maxAsyncRequests: 100, maxInitialRequests: 100, automaticNameDelimiter: '~', name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } }).end() .plugins.delete('copy').end() .plugin('define').use(new webpack.DefinePlugin({ 'process.env.isMiniprogram': process.env.isMiniprogram // 注入环境变量,用于业务代码判断 })).end() .plugin('extract-css').use(new MiniCssExtractPlugin({ filename: '[name].wxss', chunkFilename: '[name].wxss' })).end() .plugin('mp-webpack').use(new MpWebpackPlugin(require('./miniprogram.config.js'))).end() } } } [代码] 修改 package.json 中的 scripts 属性,添加用于开发和打包的任务 [代码]# 开发 "mp-serve": "vue-cli-service build --watch --mode mp" # 打包 "mp-build": "vue-cli-service build --mode mp" [代码] 配置完成,尽情玩耍吧! 目前已知的问题 小程序中无法支持 getComputedStyle 和 getBoundingClientRect 的同步接口,只能异步,所以依赖这两个方法的第三方库,需要进行改造 因为大佬很忙目前还处于开发阶段,很多组件还没有适配,有能力和有精力的同学欢迎去Github提PR 再次奉上Github连接:https://github.com/wechat-miniprogram/kbone
2019-12-10 - 让小程序页面和自定义组件支持 computed 和 watch 数据监听器
习惯于 VUE 或其他一些框架的同学们可能会经常使用它们的 [代码]computed[代码] 和 [代码]watch[代码] 。 小程序框架本身并没有提供这个功能,但我们基于现有的特性,做了一个 npm 模块来提供 [代码]computed[代码] 和 [代码]watch[代码] 功能。 先来个 GitHub 链接:https://github.com/wechat-miniprogram/computed 如何使用? 安装 npm 模块 [代码]npm install --save miniprogram-computed [代码] 示例代码 [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, }) [代码] [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, sum: 2, }, watch: { 'a, b': function(a, b) { this.setData({ sum: a + b }) }, }, }) [代码] 怎么在页面中使用? 其实上面的示例不仅在自定义组件中可以使用,在页面中也是可以的——因为小程序的页面也可用 [代码]Component[代码] 构造器来创建! 如果你已经有一个这样的页面: [代码]Page({ data: { a: 1, b: 1, }, onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }) [代码] 可以先把它改成: [代码]Component({ data: { a: 1, b: 1, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 然后就可以用了: [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 应该使用 [代码]computed[代码] 还是 [代码]watch[代码] ? 看起来 [代码]computed[代码] 和 [代码]watch[代码] 具有类似的功能,应该使用哪个呢? 一个简单的原则: [代码]computed[代码] 只有 [代码]data[代码] 可以访问,不能访问组件的 [代码]methods[代码] (但可以访问组件外的通用函数)。如果满足这个需要,使用 [代码]computed[代码] ,否则使用 [代码]watch[代码] 。 想知道原理? [代码]computed[代码] 和 [代码]watch[代码] 主要基于两个自定义组件特性: 数据监听器 和 自定义组件扩展 。其中,数据监听器 [代码]observers[代码] 可以用来监听数据被 [代码]setData[代码] 操作。 对于 [代码]computed[代码] ,每次执行 [代码]computed[代码] 函数时,记录下有哪些 data 中的字段被依赖。如果下一次 [代码]setData[代码] 后这些字段被改变了,就重新执行这个 [代码]computed[代码] 函数。 对于 [代码]watch[代码] ,它和 [代码]observers[代码] 的区别不大。区别在于,如果一个 data 中的字段被设置但未被改变,普通的 [代码]observers[代码] 会触发,但 [代码]watch[代码] 不会。 如果遇到问题或者有好的建议,可以在 GitHub 提 issue 。
2019-07-24 - 预览不正常getApp()没返回globalData真机调试却可以
- 当前 Bug 的表现(可附上截图) 预览: [图片] 真机调试: [图片] - 预期表现 - 复现路径 - 提供一个最简复现 Demo [图片] 无论哪个页面 什么时候执行都有这个问题
2019-01-29 - 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