- 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 - 小程序中使用three.js
小程序中使用three.js 目前小程序支持了webgl, 同时项目中有相关3D展示的需求,所以考虑将three.js移植到小程序中。 但是小程序里面没有浏览器相关的运行环境如 window,document等。要想在小程序中使用three.js需要使用相应的移植版本。https://github.com/yannliao/three.js实现了一个在 three.js 的基本移植版, 目前测试支持了 包含 BoxBufferGeometry, CircleBufferGeometry, ConeBufferGeometry, CylinderBufferGeometry, DodecahedronBufferGeometry 等基本模型,OrbitControl, GTLFLoader, OBJLoader等。 使用 下载https://github.com/yannliao/three.js项目中build目录下的three.weapp.min.js到小程序相应目录,如: [图片] 在index.wxml中加入canvas组件, 其中需要手动绑定相应的事件,用于手势控制。 [代码]<view style="height: 100%; width: 100%;" bindtouchstart="documentTouchStart" bindtouchmove="documentTouchMove" bindtouchend="documentTouchEnd" > <canvas type="webgl" id="c" style="width: 100%; height:100%;" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" bindtouchcancel="touchCancel" bindlongtap="longTap" bindtap="tap"></canvas> </view> [代码] 在页面中引用three.js 和相应的Loader: [代码]import * as THREE from '../../libs/three.weapp.min.js' import { OrbitControls } from '../../jsm/loaders/OrbitControls' [代码] 在onLoad中获取canvas对象并注册到[代码]THREE.global[代码]中,[代码]THREE.global.registerCanvas[代码]可以传入id, 用于通过[代码]THREE.global.document.getElementById[代码]找到, 如果不传id默认使用canvas对象中的_canvasID, registerCanvas同时也会将该canvas选为当前使用canvas对象. 同时请在onUnload回调中注销canvas对象. 注意: THREE.global 中最多同时注册 5 个[代码]canvas[代码]对象, 并可以通过id找到. 注册的canvas对象, 会长驻内存, 如果不及时清理可能造成内存问题. [代码]THREE.global[代码]为[代码]three.js[代码] 的运行环境, 类似于浏览器中的window. [代码]Page({ data: { canvasId: '' }, onLoad: function () { wx.createSelectorQuery() .select('#c') .node() .exec((res) => { const canvas = THREE.global.registerCanvas(res[0].node) this.setData({ canvasId: canvas._canvasId }) // const canvas = THREE.global.registerCanvas('id_123', res[0].node) // canvas代码 }) }, onUnload: function () { THREE.global.unregisterCanvas(this.data.canvasId) // THREE.global.unregisterCanvas(res[0].node) // THREE.global.clearCanvas() }, [代码] 注册相关touch事件. 由于小程序架构原因, 需要手动绑定事件到THREE.global.canvas或者THREE.global.document上. 可以使用[代码]THREE.global.touchEventHandlerFactory('canvas', 'touchstart')[代码] 生成小程序的事件回调函数,触发默认canvas对象上的touch事件. [代码] { touchStart(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) }, touchMove(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) }, touchEnd(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) }, } [代码] 编写three.js代码, 小程序运行环境中没有requestAnimationFrame, 目前可以使用canvas.requestAnimationFrame之后会将requestAnimationFrame注入到THREE.global中. [代码]const camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); camera.position.z = 500; const scene = new THREE.Scene(); scene.background = new THREE.Color(0xAAAAAA); const renderer = new THREE.WebGLRenderer({ antialias: true }); const controls = new OrbitControls(camera, renderer.domElement); // controls.enableDamping = true; // controls.dampingFactor = 0.25; // controls.enableZoom = false; camera.position.set(200, 200, 500); controls.update(); const geometry = new THREE.BoxBufferGeometry(200, 200, 200); const texture = new THREE.TextureLoader().load('./pikachu.png'); const material = new THREE.MeshBasicMaterial({ map: texture }); // const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); // renderer.setSize(canvas.width, canvas.height); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(canvas.width, canvas.height); } function render() { canvas.requestAnimationFrame(render); // mesh.rotation.x += 0.005; // mesh.rotation.y += 0.01; controls.update(); renderer.render(scene, camera); } render() [代码] 完整示例: index.js [代码]import * as THREE from '../../libs/three.weapp.min.js' import { OrbitControls } from '../../jsm/loaders/OrbitControls' Page({ data: {}, onLoad: function () { wx.createSelectorQuery() .select('#c') .node() .exec((res) => { const canvas = THREE.global.registerCanvas(res[0].node) this.setData({ canvasId: canvas._canvasId }) const camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); camera.position.z = 500; const scene = new THREE.Scene(); scene.background = new THREE.Color(0xAAAAAA); const renderer = new THREE.WebGLRenderer({ antialias: true }); const controls = new OrbitControls(camera, renderer.domElement); // controls.enableDamping = true; // controls.dampingFactor = 0.25; // controls.enableZoom = false; camera.position.set(200, 200, 500); controls.update(); const geometry = new THREE.BoxBufferGeometry(200, 200, 200); const texture = new THREE.TextureLoader().load('./pikachu.png'); const material = new THREE.MeshBasicMaterial({ map: texture }); // const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); // renderer.setSize(canvas.width, canvas.height); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(canvas.width, canvas.height); } function render() { canvas.requestAnimationFrame(render); // mesh.rotation.x += 0.005; // mesh.rotation.y += 0.01; controls.update(); renderer.render(scene, camera); } render() }) }, onUnload: function () { THREE.global.unregisterCanvas(this.data.canvasId) }, touchStart(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) }, touchMove(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) }, touchEnd(e) { console.log('canvas', e) THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) }, touchCancel(e) { // console.log('canvas', e) }, longTap(e) { // console.log('canvas', e) }, tap(e) { // console.log('canvas', e) }, documentTouchStart(e) { // console.log('document',e) }, documentTouchMove(e) { // console.log('document',e) }, documentTouchEnd(e) { // console.log('document',e) }, }) [代码] index.wxml [代码]<view style="height: 100%; width: 100%;" bindtouchstart="documentTouchStart" bindtouchmove="documentTouchMove" bindtouchend="documentTouchEnd" > <canvas type="webgl" id="c" style="width: 100%; height:100%;" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd" bindtouchcancel="touchCancel" bindlongtap="longTap" bindtap="tap"></canvas> </view> [代码] 其他 全部示例在 https://github.com/yannliao/threejs-example three.js 库 https://github.com/yannliao/three.js loader 组件在 threejs-example 中的 jsm 目录中 欢迎提交PR和issue
2019-10-20 - 小程序用户头像昵称获取规则调整公告
更新时间:2022年11月9日由于 PC/macOS 平台「头像昵称填写能力」存在兼容性问题,对于来自低于2.27.1版本的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称。 更新时间:2022年9月28日考虑到近期开发者对小程序用户头像昵称获取规则调整的相关反馈,平台将接口回收的截止时间由2022年10月25日延期至2022年11月8日24时。 调整背景在小程序内,开发者可以通过 wx.login 接口直接获取用户的 openId 与 unionId 信息,实现微信身份登录,支持开发者在多个小程序或其它应用间匿名关联同一用户。 同时,为了满足部分小程序业务中需要创建用户的昵称与头像的诉求,平台提供了 wx.getUserProfile 接口,支持在用户授权的前提下,快速使用自己的微信昵称头像。 但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。 调整说明自 2022 年 10 月 25 日 24 时后(以下统称 “生效期” ),用户头像昵称获取规则将进行如下调整: 自生效期起,小程序 wx.getUserProfile 接口将被收回:生效期后发布的小程序新版本,通过 wx.getUserProfile 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的小程序版本不受影响,但如果要进行版本更新则需要进行适配。自生效期起,插件通过 wx.getUserInfo 接口获取用户昵称头像将被收回:生效期后发布的插件新版本,通过 wx.getUserInfo 接口获取用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”。生效期前发布的插件版本不受影响,但如果要进行版本更新则需要进行适配。通过 wx.login 与 wx.getUserInfo 接口获取 openId、unionId 能力不受影响。「头像昵称填写能力」支持获取用户头像昵称:如业务需获取用户头像昵称,可以使用「头像昵称填写能力」(基础库 2.21.2 版本开始支持,覆盖iOS与安卓微信 8.0.16 以上版本),具体实践可见下方《最佳实践》。小程序 wx.getUserProfile 与插件 wx.getUserInfo 接口兼容基础库 2.27.1 以下版本的头像昵称获取需求:对于来自低版本的基础库与微信客户端的访问,小程序通过 wx.getUserProfile 接口将正常返回用户头像昵称,插件通过 wx.getUserInfo 接口将正常返回用户头像昵称,开发者可继续使用以上能力做向下兼容。对于上述 3,wx.getUserProfile 接口、wx.getUserInfo 接口、头像昵称填写能力的基础库版本支持能力详细对比见下表: [图片] *针对低版本基础库,兼容处理可参考 兼容文档 请已使用 wx.getUserProfile 接口的小程序开发者和已使用 wx.getUserInfo 接口的插件开发者尽快适配。小游戏不受本次调整影响。 最佳实践小程序可在个人中心或设置等页面使用头像昵称填写能力让用户完善个人资料: [图片] 微信团队 2022年5月9日
2023-09-26 - 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24 - canvasToTempFilePath: fail canvas is empty 的坑
wx.canvasToTempFilePath 提示 canvasToTempFilePath: fail canvas is empty,遇到这个问题时搜了好多方案,发现一个都不管用,最后有看了下文档,才发现问题所在。 [图片] 仔细看下,文档中说当使用canvas 2d时 应该应该使用canvas该属性。 这就是问题所在啊。 当使用2d时根本不用传参 canvasId,直接传入canvas实例即可。 wx.canvasToTempFilePath({ x: 0, y: 0, width: 300, height: 200, destWidth: 100, destHeight: 100, canvas: this.canvas,//这里是重点,获取实例的时候保存为全局变量就行了 complete(res) { console.log(res) } })
2020-09-06 - 相机帧数据转base64
获取 canvas 及 canvas context // wxml type="2d" 是必须的 // js wx.createSelectorQuery() .select('#myCanvas').node().exec((res) => { this.myCanvas = res[0].node this.myCanvasCtx = this.myCanvas.getContext('2d') }) canvas 转 base64 let ctxImageData = this.myCanvasCtx.createImageData(width, height); ctxImageData.data.set(data) this.myCanvasCtx.putImageData(ctxImageData, 0, 0) let base64 = this.myCanvas.toDataURL("image/png", 0.7);
2023-08-03 - 解决微信 8.0.37 Canvas 2D 中 canvasContext.createImageData 无法绘制问题
最近安卓微信更新到 8.0.37 版本后,canvas 2D 有无法绘制问题出现;(微信小程序、微信小游戏均有出现) 微信小程序处理 1、完整代码片段:https://developers.weixin.qq.com/s/uEA9JLme7mIT 2、核心代码如下: /** * 苹果能绘制,安卓(微信版本 8.0.37)不能绘制 */ const imgData = canvasContext.createImageData(width, height) for (var i = 0; i < imgData.data.length; i += 4) { imgData.data[i + 0] = 0 imgData.data[i + 1] = 255 imgData.data[i + 2] = 0 imgData.data[i + 3] = 255 } console.log('imgData >>>>>>>>>>>>>>>', imgData) ctx.putImageData(imgData, 0, 0) /** * 修改后安卓苹果都能绘制 */ const u8Arr = new Uint8ClampedArray(width * height * 4) for (var i = 0; i < u8Arr.length; i += 4) { u8Arr[i + 0] = 0 u8Arr[i + 1] = 255 u8Arr[i + 2] = 0 u8Arr[i + 3] = 255 } console.log('u8Arr >>>>>>>>>>>>>>>', u8Arr) const imgData = canvas.createImageData(u8Arr, width, height) console.log('imgData >>>>>>>>>>>>>>>', imgData) canvasContext.putImageData(imgData, 0, 0) 3、备注 canvas.createImageData 方法需要三个参数: e:Unit8ClampedArray t:width r:height 关于这个方法需要三个参数,官方文档没有说明,强烈建议官方补全文档,不然全靠猜;最容易使用 canvasContext.createImageData 传参形式,结果返回 null 从红色方框内可以看出,canvas.createImageData 内部也是调用 canvasContext.createImageData 实现的;现在大部分安卓手机微信升级到 8.0.37,直接调用 canvasContext.createImageData 无法绘制,需调用 canvas.createImageData 才行;官方人员还请仔细排查一下! [图片] 微信小游戏处理 1、完整代码片段:https://developers.weixin.qq.com/s/czATiLmE7sIs 2、核心代码如下: /** * 苹果能绘制,安卓(微信版本 8.0.37)不能绘制 */ const width = 100, height = 100 const imgData = canvasContext.createImageData(width, height) for (var i = 0; i < imgData.data.length; i += 4) { imgData.data[i + 0] = 0 imgData.data[i + 1] = 255 imgData.data[i + 2] = 0 imgData.data[i + 3] = 255 } console.log('imgData >>>>>>>>>>>>>>>', imgData) canvasContext.putImageData(imgData, 20, 120) /** * 修改后安卓苹果都能绘制(模拟器报错,真机可以) */ const width = 100, height = 100 const u8Arr = new Uint8ClampedArray(width * height * 4) for (var i = 0; i < u8Arr.length; i += 4) { u8Arr[i + 0] = 0 u8Arr[i + 1] = 255 u8Arr[i + 2] = 0 u8Arr[i + 3] = 255 } console.log('u8Arr >>>>>>>>>>>>>>>', u8Arr) const imgData = wx.createImageData(u8Arr, width, height) // 基础库 2.24.6 开始支持 console.log('imgData >>>>>>>>>>>>>>>', imgData) canvasContext.putImageData(imgData, 20, 120) 3、备注 wx.createImageData 官方文档也未说明如何传参,建议官方补全 总结 canvasContext.createImageData 方法返回的 ImageData 无法绘制问题,应该是个 Bug,请官方及时排查修复!
2023-06-06 - iPad胶囊、分辨率兼容问题?
[图片] 请看上图 图1:腾讯文档 图2:小程序demo 想请问一下,为什么腾讯文档或者金山文档的小程序可以适配ipad分辨率,像胶囊、菜单栏,就可以正常的显示正常大小,而自己开发的不行,请问是不是哪里需要设置参数?
2023-08-04 - 微信小程序已上架的备案入口在哪里,官方公告说的是9月1号开放,现在已经9月2号了?
微信小程序已上架的备案入口在哪里,官方公告说的是9月1号开放,现在已经9月2号了,哪位知道入口在哪里。
2023-09-02 - 深入 CommonJs 与 ES6 Module
目前主流的模块规范 UMD CommonJs es6 module umd 模块(通用模块) [代码](function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.libName = factory()); }(this, (function () { 'use strict';}))); [代码] 如果你在[代码]js[代码]文件头部看到这样的代码,那么这个文件使用的就是 [代码]UMD[代码] 规范 实际上就是 amd + commonjs + 全局变量 这三种风格的结合 这段代码就是对当前运行环境的判断,如果是 [代码]Node[代码] 环境 就是使用 [代码]CommonJs[代码] 规范, 如果不是就判断是否为 [代码]AMD[代码] 环境, 最后导出全局变量 有了 [代码]UMD[代码] 后我们的代码和同时运行在 [代码]Node[代码] 和 [代码]浏览器上[代码] 所以现在前端大多数的库最后打包都使用的是 [代码]UMD[代码] 规范 CommonJs [代码]Nodejs[代码] 环境所使用的模块系统就是基于[代码]CommonJs[代码]规范实现的,我们现在所说的[代码]CommonJs[代码]规范也大多是指[代码]Node[代码]的模块系统 模块导出 关键字:[代码]module.exports[代码] [代码]exports[代码] [代码]// foo.js //一个一个 导出 module.exports.age = 1 module.exports.foo = function(){} exports.a = 'hello' //整体导出 module.exports = { age: 1, a: 'hello', foo:function(){} } //整体导出不能用`exports` 用exports不能在导入的时候使用 exports = { age: 1, a: 'hello', foo:function(){} } [代码] 这里需要注意 [代码]exports[代码] 不能被赋值,可以理解为在模块开始前[代码]exports = module.exports[代码], 因为赋值之后[代码]exports[代码]失去了 对[代码]module.exports[代码]的引用,成为了一个模块内的局部变量 模块导入 关键字:[代码]require[代码] [代码]const foo = require('./foo.js') console.log(foo.age) //1 [代码] 模块导入规则: 假设以下目录为 [代码]src/app/index.js[代码] 的文件 调用 [代码]require()[代码] [代码]./moduleA[代码] 相对路径开头 在没有指定后缀名的情况下 先去寻找同级目录同级目录:[代码]src/app/[代码] [代码]src/app/moduleA[代码] 无后缀名文件 按照[代码]javascript[代码]解析 [代码]src/app/moduleA.js[代码] js文件 按照[代码]javascript[代码]解析 [代码]src/app/moduleA.json[代码] json文件 按照[代码]json[代码]解析 [代码]src/app/moduleA.node[代码] node文件 按照加载的编译插件模块dlopen 同级目录没有 [代码]moduleA[代码] 文件会去找同级的 [代码]moduleA[代码]目录:[代码]src/app/moduleA[代码] [代码]src/app/moduleA/package.json[代码] 判断该目录是否有[代码]package.json[代码]文件, 如果有 找到[代码]main[代码]字段定义的文件返回, 如果 [代码]main[代码] 字段指向文件不存在 或 [代码]main[代码]字段不存在 或 [代码]package.json[代码]文件不存在向下执行 [代码]src/app/moduleA/index.js[代码] [代码]src/app/moduleA/index.json[代码] [代码]src/app/moduleA/index.node[代码] 结束 [代码]/module/moduleA[代码] 绝对路径开头 直接在[代码]/module/moduleA[代码]目录中寻找 规则同上 [代码]react[代码] 没有路径开头 没有路径开头则视为导入一个包 会先判断[代码]moduleA[代码]是否是一个核心模块 如[代码]path[代码],[代码]http[代码],优先导入核心模块 不是核心模块 会从当前文件的同级目录的[代码]node_modules[代码]寻找 [代码]/src/app/node_modules/[代码] 寻找规则同上 以导入[代码]react[代码]为例 [代码]先 node_modules 下 react 文件 -> react.js -> react.json -> react.node ->react目录 -> react package.json main -> index.js -> index.json -> index.node[代码] 如果没找到 继续向父目录的[代码]node_modules[代码]中找 [代码]/src/node_modules/[代码] [代码]/node_modules/[代码] 直到最后找不到 结束 [代码]require wrapper[代码] [代码]Node[代码]的模块 实际上可以理解为代码被包裹在一个[代码]函数包装器[代码]内 一个简单的[代码]require demo[代码]: [代码]function wrapper (script) { return '(function (exports, require, module, __filename, __dirname) {' + script + '\n})' } function require(id) { var cachedModule = Module._cache[id]; if(cachedModule){ return cachedModule.exports; } const module = { exports: {} } // 这里先将引用加入缓存 后面循环引用会说到 Module._cache[id] = module //当然不是eval这么简单 eval(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname') return module.exports } [代码] 也可以查看:node module 源码 从以上代码我们可以知道: 模块只执行一次 之后调用获取的 [代码]module.exports[代码] 都是缓存哪怕这个 [代码]js[代码] 还没执行完毕(因为先加入缓存后执行模块) 模块导出就是[代码]return[代码]这个变量的其实跟[代码]a = b[代码]赋值一样, 基本类型导出的是值, 引用类型导出的是引用地址 [代码]exports[代码] 和 [代码]module.exports[代码] 持有相同引用,因为最后导出的是 [代码]module.exports[代码], 所以对[代码]exports[代码]进行赋值会导致[代码]exports[代码]操作的不再是[代码]module.exports[代码]的引用 循环引用 [代码]// a.js module.exports.a = 1 var b = require('./b') console.log(b) module.exports.a = 2 // b.js module.exports.b = 11 var a = require('./a') console.log(a) module.exports.b = 22 //main.js var a = require('./a') console.log(a) [代码] 运行此段代码结合上面的[代码]require demo[代码],分析每一步过程: [代码]执行 node main.js -> 第一行 require(a.js)[代码],([代码]node[代码] 执行也可以理解为调用了require方法,我们省略[代码]require(main.js)[代码]内容) [代码]进入 require(a)方法: 判断缓存(无) -> 初始化一个 module -> 将 module 加入缓存 -> 执行模块 a.js 内容[代码],(需要注意 是先加入缓存, 后执行模块内容) [代码]a.js: 第一行导出 a = 1 -> 第二行 require(b.js)[代码](a 只执行了第一行) [代码]进入 require(b) 内 同 1 -> 执行模块 b.js 内容[代码] [代码]b.js: 第一行 b = 11 -> 第二行 require(a.js)[代码] [代码]require(a) 此时 a.js 是第二次调用 require -> 判断缓存(有)-> cachedModule.exports -> 回到 b.js[代码](因为[代码]js[代码]对象引用问题 此时的 [代码]cachedModule.exports = { a: 1 }[代码]) [代码]b.js:第三行 输出 { a: 1 } -> 第四行 修改 b = 22 -> 执行完毕回到 a.js[代码] [代码]a.js:第二行 require 完毕 获取到 b -> 第三行 输出 { b: 22 } -> 第四行 导出 a = 2 -> 执行完毕回到 main.js[代码] [代码]main.js:获取 a -> 第二行 输出 { a: 2 } -> 执行完毕[代码] 以上就是[代码]node[代码]的[代码]module[代码]模块解析和运行的大致规则 es6 module [代码]ES6[代码] 之前 [代码]javascript[代码] 一直没有属于自己的模块规范,所以社区制定了 [代码]CommonJs[代码]规范, [代码]Node[代码] 从 [代码]Commonjs[代码] 规范中借鉴了思想于是有了 [代码]Node[代码] 的 [代码]module[代码],而 [代码]AMD 异步模块[代码] 也同样脱胎于 [代码]Commonjs[代码] 规范,之后有了运行在浏览器上的 [代码]require.js[代码] [代码]es6 module[代码] 基本语法: export [代码]export * from 'module'; //重定向导出 不包括 module内的default export { name1, name2, ..., nameN } from 'module'; // 重定向命名导出 export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重命名导出 export { name1, name2, …, nameN }; // 与之前声明的变量名绑定 命名导出 export { variable1 as name1, variable2 as name2, …, nameN }; // 重命名导出 export let name1 = 'name1'; // 声明命名导出 或者 var, const,function, function*, class export default expression; // 默认导出 export default function () { ... } // 或者 function*, class export default function name1() { ... } // 或者 function*, class export { name1 as default, ... }; // 重命名为默认导出 [代码] [代码]export[代码] 规则 [代码]export * from ''[代码] 或者 [代码]export {} from ''[代码],重定向导出,重定向的命名并不能在本模块使用,只是搭建一个桥梁,例如:这个[代码]a[代码]并不能在本模块内使用 [代码]export {}[代码], 与变量名绑定,命名导出 [代码]export Declaration[代码],声明的同时,命名导出, Declaration就是: [代码]var[代码], [代码]let[代码], [代码]const[代码], [代码]function[代码], [代码]function*[代码], [代码]class[代码] 这一类的声明语句 [代码]export default AssignmentExpression[代码],默认导出, AssignmentExpression的 范围很广,可以大致理解 为除了声明[代码]Declaration[代码](其实两者是有交叉的),[代码]a=2[代码],[代码]i++[代码],[代码]i/4[代码],[代码]a===b[代码],[代码]obj[name][代码],[代码]name in obj[代码],[代码]func()[代码],[代码]new P()[代码],[代码][1,2,3][代码],[代码]function(){}[代码]等等很多 import [代码]// 命名导出 module.js let a = 1,b = 2 export { a, b } export let c = 3 // 命名导入 main.js import { a, b, c } from 'module'; // a: 1 b: 2 c: 3 import { a as newA, b, c as newC } from 'module'; // newA: 1 b: 2 newC: 3 // 默认导出 module.js export default 1 // 默认导入 main.js import defaultExport from 'module'; // defaultExport: 1 // 混合导出 module.js let a = 1 export { a } const b = 2 export { b } export let c = 3 export default [1, 2, 3] // 混合导入 main.js import defaultExport, { a, b, c as newC} from 'module'; //defaultExport: [1, 2, 3] a: 1 b: 2 newC: 3 import defaultExport, * as name from 'module'; //defaultExport: [1, 2, 3] name: { a: 1, b: 2, c: 3 } import * as name from 'module'; // name: { a: 1, b: 2, c: 3, default: [1, 2, 3] } // module.js Array.prototype.remove = function(){} //副作用 只运行一个模块 import 'module'; // 执行module 不导出值 多次调用module.js只运行一次 //动态导入(异步导入) var promise = import('module'); [代码] [代码]import[代码] 规则 [代码]import { } from 'module'[代码], 导入[代码]module.js[代码]的命名导出 [代码]import defaultExport from 'module'[代码], 导入[代码]module.js[代码]的默认导出 [代码]import * as name from 'module'[代码], 将[代码]module.js的[代码]的所有导出合并为[代码]name[代码]的对象,[代码]key[代码]为导出的命名,默认导出的[代码]key[代码]为[代码]default[代码] [代码]import 'module'[代码],副作用,只是运行[代码]module[代码],不为了导出内容例如 polyfill,多次调用次语句只能执行一次 [代码]import('module')[代码],动态导入返回一个 [代码]Promise[代码],[代码]TC39[代码]的[代码]stage-3[代码]阶段被提出 tc39 import [代码]ES6 module[代码] 特点 [代码]ES6 module[代码]的语法是静态的 [代码]import[代码] 会自动提升到代码的顶层 [代码]export[代码] 和 [代码]import[代码] 只能出现在代码的顶层,下面这段语法是错误的 [代码] //if for while 等都无法使用 { export let a = 1 import defaultExport from 'module' } true || export let a = 1 [代码] [代码]import[代码] 的导入名不能为字符串或在判断语句,下面代码是错误的 [代码]import 'defaultExport' from 'module' let name = 'Export' import 'default' + name from 'module' [代码] 静态的语法意味着可以在编译时确定导入和导出,更加快速的查找依赖,可以使用[代码]lint[代码]工具对模块依赖进行检查,可以对导入导出加上类型信息进行静态的类型检查 ####[代码]ES6 module[代码]的导出是绑定的 #### 使用 [代码]import[代码] 被导入的模块运行在严格模式下 使用 [代码]import[代码] 被导入的变量是只读的,可以理解默认为 [代码]const[代码] 装饰,无法被赋值 使用 [代码]import[代码] 被导入的变量是与原变量绑定/引用的,可以理解为 [代码]import[代码] 导入的变量无论是否为基本类型都是引用传递 [代码]// js中 基础类型是值传递 let a = 1 let b = a b = 2 console.log(a,b) //1 2 // js中 引用类型是引用传递 let obj = {name:'obj'} let obj2 = obj obj2.name = 'obj2' console.log(obj.name, obj2.name) // obj2 obj2 // es6 module 中基本类型也按引用传递 // foo.js export let a = 1 export function count(){ a++ } // main.js import { a, count } from './foo' console.log(a) //1 count() console.log(a) //2 // export default 是无法 a 的动态绑定 这一点跟 CommonJs 有点相似 都是值的拷贝 let a = 1; export default a // 可以用另一种方式实现 default 的动态绑定 let a = 1; export { a as default } export function count(){ a++ } // 就跟上面 main.js 一样 [代码] 上面这段代码就是 [代码]CommonJs[代码] 导出变量 和 [代码]ES6[代码] 导出变量的区别 es module 循环引用 [代码]// bar.js import { foo } from './foo' console.log(foo); export let bar = 'bar' // foo.js import { bar } from './bar' console.log(bar); export let foo = 'foo' // main.js import { bar } from './bar' console.log(bar) [代码] [代码]执行 main.js -> 导入 bar.js[代码] [代码]bar.js -> 导入 foo.js[代码] [代码]foo.js -> 导入 bar.js -> bar.js 已经执行过直接返回 -> 输出 bar -> bar is not defined, bar 未定义报错[代码] 我们可以使用[代码]function[代码]的方式解决: [代码]// bar.js import { foo } from './foo' console.log(foo()); export function bar(){ return 'bar' } // foo.js import { bar } from './bar' console.log(bar()); export function foo(){ return 'foo' } // main.js import { bar } from './bar' console.log(bar) [代码] 因为函数声明会提示到文件顶部,所以就可以直接在 [代码]foo.js[代码] 调用还没执行完毕的[代码]bar.js[代码]的 [代码]bar[代码] 方法,不要在函数内使用外部变量,因为变量还未声明([代码]let,const[代码])和赋值,[代码]var[代码] CommonJs 和 ES6 Module 的区别 其实上面我们已经说到了一些区别 [代码]CommonJs[代码]导出的是变量的一份拷贝,[代码]ES6 Module[代码]导出的是变量的绑定([代码]export default[代码] 是特殊的) [代码]CommonJs[代码]是单个值导出,[代码]ES6 Module[代码]可以导出多个 [代码]CommonJs[代码]是动态语法可以写在判断里,[代码]ES6 Module[代码]静态语法只能写在顶层 [代码]CommonJs[代码]的 [代码]this[代码] 是当前模块,[代码]ES6 Module[代码]的 [代码]this[代码] 是 [代码]undefined[代码] 易混淆点 模块语法与解构 [代码]module语法[代码]与[代码]解构语法[代码]很容易混淆,例如: [代码]import { a } from 'module' const { a } = require('module') [代码] 尽管看上去很像,但是不是同一个东西,这是两种完全不一样的语法与作用,ps:两个人撞衫了,穿一样的衣服你不能说这俩人就是同一个人 [代码]module[代码] 的语法: 上面有写 [代码]import/export { a } / { a, b } / { a as c} FromClause[代码] [代码]解构[代码] 的语法: [代码]let { a } = { a: 1 } let { a = 2 } = { } let { a: b } = { a: 1 } let { a: b = 2, ...res } = { name:'a' } let { a: b, obj: { name } } = { a: 1, obj: { name: '1' } } function foo({a: []}) {} [代码] 他们是差别非常大的两个东西,一个是模块导入导出,一个是获取对象的语法糖 导出语法与对象属性简写 同样下面这段代码也容易混淆 [代码]let a = 1 export { a } // 导出语法 export default { a } // 属性简写 导出 { a: 1 } 对象 module.exports = { a } // 属性简写 导出 { a: 1 } 对象 [代码] [代码]export default[代码] 和 [代码]module.exports[代码] 是相似的 ES6 module 支持 CommonJs 情况 先简单说一下各个环境的 [代码]ES6 module[代码] 支持 [代码]CommonJs[代码] 情况,后面单独说如何在不同环境中使用 因为 [代码]module.exports[代码] 很像 [代码]export default[代码] 所以 [代码]ES6模块[代码] 可以很方便兼容 [代码]CommonJs[代码] 在[代码]ES6 module[代码]中使用[代码]CommonJs[代码]规范,根据各个环境,打包工具不同也是不一样的 我们现在大多使用的是 [代码]webpack[代码] 进行项目构建打包,因为现在前端开发环境都是在 [代码]Node[代码] 环境原因,而 [代码]npm[代码] 的包都是 [代码]CommonJs[代码] 规范的,所以 [代码]webpack[代码] 对[代码]ES6[代码]模块进行扩展 支持 [代码]CommonJs[代码],并支持[代码]node[代码]的导入[代码]npm[代码]包的规范 如果你使用 [代码]rollup[代码],想在[代码]ES Module[代码]中支持[代码]Commonjs[代码]规范就需要下载[代码]rollup-plugin-commonjs[代码]插件,想要导入[代码]node_modules[代码]下的包也需要[代码]rollup-plugin-node-resolve[代码]插件 如果你使用 [代码]node[代码],可以在 [代码].mjs[代码] 文件使用 [代码]ES6[代码],也支持 [代码]CommonJs[代码] 查看 nodejs es-modules.md 在浏览器环境 不支持[代码]CommonJs[代码] node 与 打包工具[代码]webpack,rollup[代码]的导入 [代码]CommonJs[代码] 差异 [代码]// module.js module.export.a = 1 // index.js webpack rollup import * as a from './module' console.log(a) // { a: 1, default: { a:1 } } // index.mjs node import * as a from './module' console.log(a) // { default: { a:1 } } [代码] [代码]node[代码] 只是把 [代码]module.exports[代码] 整体当做 [代码]export default[代码] 打包工具除了把 [代码]module.export[代码] 整体当做 [代码]export default[代码],还把 [代码]module.export[代码] 的每一项 又当做 [代码]export[代码] 输出,这样做是为了更加简洁 [代码]import defaultExport from './foo'[代码], [代码]defaultExport.foo()[代码] [代码]import { foo } from './foo'[代码], [代码]foo()[代码] 使用 ES6 Module 可以在 es6module example 仓库中获取代码在本地进行测试验证 浏览器中使用 你需要起一个[代码]Web服务器[代码]来访问,双击本地运行 [代码]index.html[代码] 并不会执行 [代码]type=module[代码] 标签 我们可以对 [代码]script[代码] 标签的 [代码]type[代码] 属性加上 [代码]module[代码] 先定义两个模块 [代码]// index.js import module from './module.js' console.log(module) // 123 // module.js export default 123 [代码] 在[代码]html[代码]中内联调用 [代码]<!-- index.html --> <script type="module"> import module from './module.js' console.log(module) // 123 </script> [代码] 在[代码]html[代码]中通过 [代码]script[代码] 的 [代码]src[代码] 引用 [代码]<!-- index.html --> <script type="module" src="index.js"></script> // 控制台 123 [代码] 浏览器导入路径规则 [代码]https://example.com/apples.mjs[代码] [代码]http://example.com/apples.js[代码] [代码]//example.com/bananas[代码] [代码]./strawberries.mjs.cgi[代码] [代码]../lychees[代码] [代码]/limes.jsx[代码] [代码]data:text/javascript,export default 'grapes';[代码] [代码]blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f[代码] 补充: 不加 后缀名 找不到具体的文件 后端可以修改接口[代码]/getjs?name=module[代码]这一类的,不过后端要返回 [代码]Content-Type: application/javascript[代码] 确保返回的是[代码]js[代码],因为浏览器是根据 [代码]MIME type[代码] 识别的 因为 [代码]ES6 Module[代码] 在浏览器中兼容并不是很好兼容性表,这里就不介绍浏览器支持情况了,我们一般不会直接在浏览器中使用 Nodejs中使用 nodejs es-modules.md 在 [代码]Node v8.5.0[代码] 以上支持 [代码]ES Module[代码],需要 [代码].mjs[代码]扩展名 NOTE: DRAFT status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement ESM. At which time this draft would be moved to ACCEPTED. (上面链接可以知道 [代码]ES Module[代码]的状态是 [代码]DRAFT[代码], 属于起草阶段) [代码]// module.mjs export default 123 // index.mjs import module from './module.mjs' console.log(module) // 123 [代码] 我们需要执行 [代码]node --experimental-modules index.mjs[代码] 来启动 会提示一个 [代码]ExperimentalWarning: The ESM module loader is experimental.[代码]该功能是实验性的(此提示不影响执行) [代码]ES Module[代码] 中导入 [代码]CommonJs[代码] [代码]// module.js module.exports.a = 123 // module.exports 就相当于 export default // index.mjs import module from './module.js' console.log(module) // { a: 123 } import * as module from './module.js' console.log(module) // { get default: { a: 123 } } import { default as module } from './module.js'; console.log(module) // { a: 123 } import module from 'module'; // 导入npm包 导入规则与 require 差不多 [代码] 导入路径规则与[代码]require[代码]差不多 这里要注意 [代码]module[代码] 扩展名为 [代码].js[代码],[代码].mjs[代码]专属于 [代码]es module[代码],[代码]import form[代码]导入的文件后缀名只能是[代码].mjs[代码],在 [代码].mjs[代码]中 [代码]module[代码]未定义, 所以调用 [代码]module.exports,exports[代码] 会报错 [代码]node[代码]中 [代码]CommonJs[代码] 导入 [代码]es module[代码] 只能使用 [代码]import()[代码] 动态导入/异步导入 [代码]// es.mjs let foo = {name: 'foo'}; export default foo; export let a = 1 // cjs import('./es').then((res)=>{ console.log(res) // { get default: {name: 'foo'}, a: 1 } }); [代码] webpack中使用 从 [代码]webpack2[代码] 就默认支持 [代码]es module[代码] 了,并默认支持 [代码]CommonJs[代码],支持导入 [代码]npm[代码]包, 这里 [代码]import[代码] 语法上面写太多 就不再写了 rollup中使用 [代码]rollup[代码] 专注于 [代码]es module[代码],可以将 [代码]es module[代码] 打包为主流的模块规范,注意这里与 [代码]webpack[代码] 的区别,我们可以在 [代码]webpack[代码] 的 [代码]js[代码] 中使用 [代码]Commonjs[代码] 语法, 但是 [代码]rollup[代码] 不支持,[代码]rollup[代码]需要 [代码]plugin[代码] 支持,包括加载 [代码]node_modules[代码] 下的包 [代码]form 'react'[代码] 也需要 [代码]plugin[代码] 支持 可以看到 [代码]es module[代码] 在[代码]浏览器[代码]与[代码]node[代码]中兼容性差与实验功能的 我们大多时候在 打包工具 中使用 Tree-shaking 在最后我们说一下经常跟 [代码]es module[代码] 一起出现的一个名词 [代码]Tree-shaking[代码] [代码]Tree-shaking[代码] 我们先直译一下 树木摇晃 就是 摇晃树木把上面枯死的树叶晃下来,在代码中就是把没有用到的代码删除 [代码]Tree-shaking[代码] 最早由 [代码]rollup[代码] 提出,之后 [代码]webpack 2[代码] 也开始支持 这都是基于 [代码]es module[代码] 模块特性的静态分析 rollup 下面代码使用 [代码]rollup[代码] 进行打包: [代码]// module.js export let foo = 'foo' export let bar = 'bar' // index.js import { foo } from './module' console.log(foo) // foo [代码] 在线运行 我们可以修改例子与导出多种规范 打包结果: [代码]let foo = 'foo'; console.log(foo); // foo [代码] 可以看到 [代码]rollup[代码] 打包结果非常的简洁,并去掉了没有用到的 [代码]bar[代码] 是否支持对导入 [代码]CommonJs[代码] 的规范进行 [代码]Tree-shaking[代码]: [代码]// index.js import { a } from './module' console.log(a) // 1 // module.js module.exports.a = 1 module.exports.b = 2 [代码] 打包为 [代码]es module[代码] [代码]var a_1 = 2; console.log(a_1); [代码] 可以看到去掉了未使用的 [代码]b[代码] webpack 我们下面看看 [代码]webpack[代码] 的支持情况 [代码]// src/module.js export function foo(){ return 'foo' } export function bar(){ return 'bar' } // src/index.js import { foo } from './module' console.log(foo()) [代码] 执行 [代码]npx webpack -p[代码](我们使用webpack 4,0配置,-p开启生成模式 自动压缩) 打包后我们在打包文件搜索 [代码]bar[代码] 没有搜到,[代码]bar[代码]被删除 我们将上面例子修改一下: [代码]// src/module.js module.exports.foo = function (){ return 'foo' } module.exports.bar = function (){ return 'bar' } // src/index.js import { foo } from './module' console.log(foo()) [代码] 打包后搜索 [代码]bar[代码] 发现[代码]bar[代码]存在,[代码]webpack[代码] 并不支持对[代码]CommonJs[代码] 进行 [代码]Tree-shaking[代码] pkg.module [代码]webpack[代码] 不支持 [代码]Commonjs[代码] [代码]Tree-shaking[代码],但现在[代码]npm[代码]的包都是[代码]CommonJs[代码]规范的,这该怎么办呢 ?如果我发了一个新包是 [代码]es module[代码] 规范, 但是如果代码运行在 [代码]node[代码] 环境,没有经过打包 就会报错 有一种按需加载的方案 全路径导入,导入具体的文件: [代码]// src/index.js import remove from 'lodash/remove' import add from 'lodash/add' console.log(remove(), add()) [代码] 使用一个还好,如果用多个的话会有很多 [代码]import[代码] 语句 还可以使用插件如 [代码]babel-plugin-lodash, & lodash-webpack-plugin[代码] 但我们不能发一个库就自己写插件 这时就提出了在 [代码]package.json[代码] 加一个 [代码]module[代码] 的字段来指向 [代码]es module[代码]规范的文件,[代码]main -> CommonJs[代码],那么[代码]module - es module[代码] pkg.module [代码]webpack[代码] 与 [代码]rollup[代码] 都支持 [代码]pkg.module[代码] 加了 [代码]module[代码] 字段 [代码]webpack[代码] 就可以识别我们的 [代码]es module[代码],但是还有一个问题就是 [代码]babel[代码] 我们一般使用 [代码]babel[代码] 都会排除 [代码]node_modules[代码],所以我们这个 [代码]pkg.module[代码] 只是的 [代码]es6 module[代码]必须是编译之后的 [代码]es5[代码] 代码,因为 [代码]babel[代码] 不会帮我们编译,我们的包就必须是 拥有 es6 module 规范的 es5 代码 如果你使用了 [代码]presets-env[代码] 因为会把我们的代码转为 [代码]CommonJs[代码] 所以就要设置 [代码]"presets": [["env", {"modules":false}][代码] 不将[代码]es module[代码] 转为 [代码]CommonJs[代码] [代码]webpack[代码] 与 [代码]rollup[代码] 的区别 [代码]webpack[代码] 不支持导出 [代码]es6 module[代码] 规范,[代码]rollup[代码] 支持导出 [代码]es6 module[代码] [代码]webpack[代码] 打包后代码很多冗余无法直接看,[代码]rollup[代码] 打包后的代码简洁,可读,像源码 [代码]webpack[代码] 可以进行代码分割,静态资源处理,[代码]HRM[代码],[代码]rollup[代码] 专注于 [代码]es module[代码],[代码]tree-shaking[代码]更加强大的,精简 如果是开发应用可以使用 [代码]webpack[代码],因为可以进行代码分割,静态资源,[代码]HRM[代码],插件 如果是开发类似 [代码]vue[代码],[代码]react[代码] 等类库,[代码]rollup[代码] 更好一些,因为可以使你的代码精简,无冗余代码,执行更快,导出多种模块语法 结语 本文章介绍了 [代码]Commonjs[代码] 和 [代码]ES6 Module[代码],导入导出的语法规则,路径解析规则,两者的区别,容易混淆的地方,在不同环境的区别,在不同环境的使用,[代码]Tree-shaking[代码],与 [代码]webpack[代码],[代码]rollup[代码] 的区别 希望您读完文章后,能对前端的模块化有更深的了解 参考链接 ECMAScript® 2015 Language Specification sec-imports/sec-exports MDN import github nodejs lib/module github nodejs node-eps/002-es-modules nodejs docs modules Understanding ECMAScript 6 ECMAScript 6 入门 es6-modules-final
2019-07-24 - 云函数可以做http请求吗?
用wx.login获取openid需要到服务器交换一次 如果云开发的云函数可以做http请求,那就非常完美了。 试了一下,发现不行。 想问一下官方的大神,是原本就不行,还是我人品不够好? 代码如下: // 云函数入口函数 exports.main = async (event, context) => { request({ url: 'test.php', header: { 'content-type': 'application/json' // 默认值 }, method: 'GET', dataType: 'json', responseType: 'text', success: function(res) { console.log(res) }, fail: function(res) { console.log(res); }, complete: function(res) {} }) } 之前是些wx.request,错误提示说wx未定义。 然后改成request,说request未定义 在云函数内测试,结果如下 返回结果: {"errorCode":10004,"errorMessage":"user code exception caught","stackTrace":"ReferenceError: request is not defined\n at EventHandler.exports.main [as realHandler] (/var/user/index.js:8:3)\n at EventHandler.handle (/var/runtime/node8/bootstrap.js:238:34)\n at invoke (/var/runtime/node8/bootstrap.js:111:22)\n at Timeout.setTimeout [as _onTimeout] (/var/runtime/node8/bootstrap.js:61:9)\n at ontimeout (timers.js:475:11)\n at tryOnTimeout (timers.js:310:5)\n at Timer.listOnTimeout (timers.js:270:5)"} 那也就是说: 除了在文档内列出的,小程序端api和服务端api外,其他的都不能在云函数执行是吗?
2018-09-21 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10