- 微信开发者工具2.9.3基础库bindInput 失效问题
微信开发者工具上Input组件bindInput属性在2.9.3基础库无法触发是已知问题,已修复灰度上线中,尚未灰度到的开发者可自行调节到2.9.3以下基础库开发。真机不受影响
2019-11-22 - 开发者工具input不触发bindinput事件
开发者工具中input组件的事件bindinput不触发,手机上调试是正常的。 以至于搜索功能、编辑功能无法在开发者工具上正常运行。。。。。。。 [图片] [图片] [图片] 不触发bindinput。。。 差点就哭出来了,搞了两个小时没想到是基础库的问题把基础库从2.9.3改到2.9.2就好了
2019-11-14 - 实际运营内容与名称、简介不符,被拒绝8次怎么办?
你好,你的小程序实际运营内容与名称、简介不符,请上架符合小程序名称、简介描述的正式内容/商品,并保持服务内容与图文一致。 请根据上述原因对小程序进行修改,并重新提交代码审核。 [图片] [图片] [图片] 上传的就是游戏攻略,为什么每次都不过呢???
2019-10-24 - 实际运营内容与名称、简介不符,很多次被拒绝怎么办?
你好,你的小程序实际运营内容与名称、简介不符,请上架符合小程序名称、简介描述的正式内容/商品,并保持服务内容与图文一致。 请根据上述原因对小程序进行修改,并重新提交代码审核。
2019-10-23 - scroll-view中的页面会出现整个页面往左偏移问题
- 当前 Bug 的表现(可附上截图)[图片] - 预期 表现[图片] 使用scroll-view中页面会发生向左偏移。导致右边留下空白部分。不知道是什么原因导致的。但是当把scroll-view中的样式:display:block去掉,恢复正常,但是这样导致页面无法使用scroll-into-view。 [图片]
2019-05-21 - 初试小程序接入three.js
看着小程序下的canvas日渐完善,特别是2.7.0库下新增了WebGL,终于可以摆脱原来用wx.createCanvasContext创建的2d上下文(不知为何在使用魔改后three.js中的CanvasRenderer渲染画面就是很慢,捕获JavaScript Profiler看着就是慢在draw方法上)。 不过理想很丰满,现实很骨感,想要在小程序上用three.js依然要来个大改造。让我们开始吧 官方文档里提供了一段如何获取WebGL Context的代码: [代码]Page({[代码][代码] [代码][代码]onReady() {[代码][代码] [代码][代码]const query = wx.createSelectorQuery()[代码][代码] [代码][代码]query.select([代码][代码]'#myCanvas'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const gl = canvas.getContext([代码][代码]'webgl'[代码][代码])[代码][代码] [代码][代码]console.log(gl)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]我们就从这里入手 首先先写个wxml: [代码]<[代码][代码]canvas[代码] [代码]type[代码][代码]=[代码][代码]"webgl"[代码] [代码]id[代码][代码]=[代码][代码]"webgl"[代码] [代码]width[代码][代码]=[代码][代码]"{{canvasWidth||(320*2)}}"[代码] [代码]height[代码][代码]=[代码][代码]"{{canvasHeight||(504*2)}}"[代码] [代码]style[代码][代码]=[代码][代码]'width:{{canvasStyleWidth||"320px"}};height:{{canvasStyleHeight||"504px"}};'[代码] [代码]bindtouchstart[代码][代码]=[代码][代码]'onTouchStart'[代码] [代码]bindtouchmove[代码][代码]=[代码][代码]'onTouchMove'[代码] [代码]bindtouchend[代码][代码]=[代码][代码]'onTouchEnd'[代码][代码]></[代码][代码]canvas[代码][代码]>[代码]其中width和height是设置画布大小的,style中的width和height是设置画布的实际渲染大小的 然后js: [代码]onReady:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]query = wx.createSelectorQuery().select([代码][代码]'#webgl'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]var[代码] [代码]canvas = res[0].node;[代码][代码] [代码][代码]requestAnimationFrame = canvas.requestAnimationFrame;[代码][代码] [代码][代码]canvas.width = canvas._width;[代码][代码] [代码][代码]canvas.height = canvas._height;[代码][代码] [代码][代码]canvas.style = {};[代码][代码] [代码][代码]canvas.style.width = canvas.width;[代码][代码] [代码][代码]canvas.style.height = canvas.height;[代码][代码] [代码][代码]self.init(canvas);[代码][代码] [代码][代码]self.animate();[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码]先模拟dom构造一个canvas对象,然后传入init方法中,我们在这里创建场景、相机、渲染器等 [代码]init: [代码][代码]function[代码] [代码](canvas) {[代码][代码]...[代码][代码] [代码][代码]camera = [代码][代码]new[代码] [代码]THREE.PerspectiveCamera(20, canvas.width / canvas.height, 1, 10000);[代码][代码] [代码][代码]scene = [代码][代码]new[代码] [代码]THREE.Scene();[代码][代码]...[代码][代码] [代码][代码]renderer = [代码][代码]new[代码] [代码]THREE.WebGLRenderer({ canvas: canvas, antialias: [代码][代码]true[代码] [代码]});[代码][代码] [代码][代码]}[代码]这样一个最基础的三维场景就搭好了,然后继续执行animate方法,开始渲染场景 [代码]animate:[代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]requestAnimationFrame([代码][代码]this[代码][代码].animate);[代码][代码] [代码][代码]this[代码][代码].render();[代码][代码] [代码][代码]}[代码]接下来尝试跑一下three.js提供的例子 webgl_geometry_colors : [图片] 锯齿问题比较严重,暂时没找到解决办法,但总体来说还是可以的,至少场景渲染出来了 由于暂时没想到如何改造CanvasTexture,我把例子中的 [代码]var[代码] [代码]canvas = document.createElement( [代码][代码]'canvas'[代码] [代码]);[代码][代码]canvas.width = 128;[代码][代码]canvas.height = 128;[代码][代码]var[代码] [代码]context = canvas.getContext( [代码][代码]'2d'[代码] [代码]);[代码][代码]var[代码] [代码]gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 );[代码][代码]gradient.addColorStop( 0.1, [代码][代码]'rgba(210,210,210,1)'[代码] [代码]);[代码][代码]gradient.addColorStop( 1, [代码][代码]'rgba(255,255,255,1)'[代码] [代码]);[代码][代码]context.fillStyle = gradient;[代码][代码]context.fillRect( 0, 0, canvas.width, canvas.height );[代码][代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.CanvasTexture( canvas );[代码]替换成 webgl_geometries 例子中的TextureLoader [代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.TextureLoader().load(canvas,[代码][代码]'../../textures/UV_Grid_Sm.jpg'[代码][代码]);[代码]可能有人会发现load方法中传入的参数多了一个canvas,因为小程序提供的api没法直接创建Image对象,仅有一个Canvas.createImage()方法可以创建Image对象。因此我们还需要改造一下TextureLoader中的load方法,先看一下原版中的load方法: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( url, [代码][代码]function[代码] [代码]( image ) {[代码]其中实际调用了ImageLoader来加载图片,在看看ImageLoader: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]image = document.createElementNS( [代码][代码]'http://www.w3.org/1999/xhtml'[代码][代码], [代码][代码]'img'[代码] [代码]);[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemError( url );[代码][代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]image.addEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.addEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]document.createElementNS这种东西肯定是没法存在的,没办法,把canvas传进来用createImage方法创建Image对象,改造后: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码], canvas);[代码][代码] [代码][代码]var[代码] [代码]image = canvas.createImage();[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码][代码] [代码][代码]scope.manager.itemError( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//image.addEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.addEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = onImageLoad;[代码][代码] [代码][代码]image.onerror = onImageError;[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]然后TextureLoader的load方法也改一下传参顺序: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( canvas,url, [代码][代码]function[代码] [代码]( image ) {[代码]OK! 这个例子代码我放在https://github.com/leo9960/xcx_threejs,大家可以接着研究一下。潜力还是比较大的,比如我拿它搞了个全景展示 [图片] ---------------------------------------------------------------------- 2019.5.26 新上传了全景展示的范例,基于panolens.js,欢迎围观
2019-05-26 - 【求问】小程序使用满意度
今天进入一个小程序 发现在页面下方自动弹出一个使用满意度的打分窗口 如下图 给该小程序打分 1-5 分 同一个小程序 并不是每个人进去都会有弹窗 所以想问一下这个评分弹窗的触发条件是什么以及这个评分对后续小程序有哪些影响[图片]
2019-05-22 - 小程序多端框架全面测评
最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11日)已发布稳定版为标准进行讨论。 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: [代码]uni-app[代码] > [代码]Taro[代码] > [代码]chameleon[代码] > [代码]WePY[代码]、[代码]mpvue[代码] [图片] 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码] 则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: [代码]chameleon[代码] > [代码]Taro[代码]、[代码]uni-app[代码] > [代码]mpvue[代码]、[代码]WePY[代码] [图片] 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: [代码]WePY[代码] > [代码]uni-app[代码] 、[代码]taro[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] [图片] 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: [代码]Taro[代码] > [代码]mpvue[代码] 、 [代码]uni-app[代码] > [代码]WePY[代码] > [代码]chameleon[代码] 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: [代码]uni-app[代码] > [代码]Taro[代码]、[代码]WePY[代码]、[代码]mpvue[代码] > [代码]chameleon[代码] [图片] 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: [代码]Taro[代码] > [代码]WePY[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] > [代码]uni-app[代码] 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码] 不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-03-19 - 使用云服务开发抢蛋糕小游戏的经验谈(附防作弊机制)
作者:于振宇 我们在 LeanCloud 成立五周年之际,发布了一款名为《LeanCloud 周年游戏》的微信小游戏。 游戏玩起来很简单,参与者要在 15 秒内从迅速掉落的蛋糕和炸弹中点中尽可能多的蛋糕来得分,蛋糕有好几种,分值也不一样,而误点到炸弹就要扣分。游戏一结束参与者能在排行榜中看到自己的名次,我们给前 50 名都设置了奖品。 游戏截图: [图片] 排行榜截图: [图片] 没玩过的朋友可以搜索「LeanCloud 周年游戏」体验一下。 这个项目开发周期大概为一周,包含客户端开发 2 天 + 服务端 1 天 + 调试 2 天。 接下来我会从客户端、服务端、作弊检测三方面来梳理关键的技术细节,希望能够为游戏开发者或感兴趣的朋友提供一些思路。 在开发环境方面,客户端主要使用了 Cocos Creator 来编辑构建「微信小游戏」项目,服务端使用了 LeanCloud 的云存储、云引擎和排行榜等服务,这些我都会在后面详细介绍。 客户端 先说引擎和编辑器。选取 Cocos Creator 的原因是当在编辑器中构建不同平台项目时,它的友好程度一直都比较好,而且 LeanCloud 也为 Cocos Creator 做了适配。我们游戏的玩法比较简单,无需过多解释,所以接下来我会从客户端资源、状态机、暂停、LeanCloud SDK 和微信这些方面来展开描述。 资源 在游戏运行过程中,加载资源、实例化、销毁 Node 等任何耗时操作都可能造成游戏卡顿,影响体验,特别是在低端机器上这种现象会更加明显。所以我们应该对资源进行预加载或者预实例化。 对于加载资源,通常是在场景切换时,对旧场景资源进行卸载,并对新场景资源进行预加载。 在 Cocos 中,通过 cc.loader 可以很方便地对单个资源、资源列表和资源目录进行加载和缓存。而对于 Node 的实例化和销毁,则要根据 Node 的生命周期进行区分。如果频繁生成和销毁的 Node,我们可以在加载阶段通过对象池技术预先实例化一部分,这样当在游戏过程中需要实例化 Node 时,就不需要实例化,而是从对象池中获取;在不需要时,不进行销毁操作,而是放回至对象池中等待下次使用。如弹幕游戏中的飞机和子弹等。在我们的游戏中,我们也对生成的蛋糕应用了「对象池」技术来避免游戏中可能出现的卡顿。庆幸的是,Cocos 已经提供了这项功能。 状态机 在游戏运行过程中,游戏主体(或角色)都会有很多的状态,比如英雄的空闲、移动、攻击、死亡等,因此通常会引入「状态机」模式对游戏对象进行设计。我们为抢蛋糕游戏引入了 machina 库作为状态机的框架,将整个游戏主体划分为初始化、准备、进行中、结束四个状态。 通过状态机,我们可以更加清楚地跟踪游戏在过程中的变化,并可以通过事件在不同的状态下做出不同的处理。 暂停 在游戏过程中,我们经常会需要暂停游戏,比如在抢蛋糕游戏结束时不再生成新的蛋糕和位置移动。 不同的游戏引擎的暂停方式有所不同。通过 Cocos 的文档,我们找到了引擎提供的 cc.director.pause() / cc.director.resume() 接口,但是尝试之后发现很多局限性,比如在暂停之后 Widget 适配会不起作用,ScrollView 拖拽不回弹等情况。 于是我们决定通过 Component.update(dt) 生命周期和状态机在游戏中自行控制 Node 的更新。主要思路是在全局游戏的 update() 生命周期里,将更新事件交由状态机,只有在游戏进入「进行中」状态时才处理更新事件,而在其他状态下则忽略更新事件。 更新过程为先获取场景下的所有 CakeCtrl 对象,调用自定义 onUpdate(dt) 方法进行更新(注意不是 update(dt) 生命周期方法)。 [代码]// 游戏状态: play: { ... update: function (dt) { const cakeCtrls = this._scene.getComponentsInChildren(CakeCtrl); cakeCtrls.forEach((cakeCtrl) => { cakeCtrl.onUpdate(dt); }); } ... }, [代码] LeanCloud SDK LeanCloud SDK 在大部分平台都做了适配,可以很方便地接入 LeanCloud 云服务。 开发者在使用 Cocos Creator 时一般在浏览器进行调试开发,当完成后再发布到微信环境。但不同环境下 LeanCloud SDK 略有不同,为了方便使用,你可以通过封装来隐藏加载不同版本 SDK 的细节。 比如在浏览器环境下,加载 leancloud-storage 库;而发布在微信小游戏环境下,则加载 leancloud-storage/dist/av-weapp-min.js 库。 [代码]if (cc.sys.browserType === cc.sys.BROWSER_TYPE_WECHAT_GAME) { AV = require("leancloud-storage/dist/av-weapp-min.js"); } else { AV = require("leancloud-storage"); } [代码] 另外,如果我们需要使用微信授权登录,为了方便在浏览器下调试,我们也可以将 login() 封装成不同的实现,统一逻辑层调用。 比如在浏览器环境下,使用账号 + 密码方式登录;而在微信小游戏环境下,使用微信授权登录。 [代码]login() { return new Promise((resolve, reject) => { // 微信登录 if (cc.sys.browserType === cc.sys.BROWSER_TYPE_WECHAT_GAME) { AV.User.loginWithWeapp() .then(user => { ... }) .catch(error => { reject(error); }); } else { // 使用默认账号登录,开发调试使用 AV.User.logIn("1if7jp52qx9771hllat1rvfqt", "123") .then(user => { ... }) .catch(error => { reject(error); }); } }); }, [代码] 更多关于 SDK 的文档,请访问 LeanCloud 文档。 微信 因为我们的游戏在排行榜中需要获取玩家的头像和昵称,所以需要使用到微信的获取用户信息(昵称、头像)接口。这里要吐槽一下,微信新版的 SDK 已经不允许用「弹框授权」来直接获取信息了,而需要使用「固定类型的」微信小程序按钮获取。但是这一机制有对微信旧版 SDK 又不可用,所以我们需要根据微信版本,确定通过哪种机制拿到微信授权。 如果是旧版本的微信,则可以直接调用获取用户信息接口;而如果是新版本的微信,则需要渲染出微信授权按钮,通过按钮的点击事件再获取。这里你可能需要面对小程序的渲染和 Cocos 的渲染机制不一致的问题。 所以,这里还用到了一个小窍门——将微信小程序的授权按钮设置为透明,覆盖到 Cocos 场景中的按钮之上,当按钮被点击时,系统会先将点击事件传递到微信小程序,在微信小程序回调中处理完成之后再交由游戏中处理。 服务端 在服务端开发中,我们主要使用了 LeanCloud 的云存储、云引擎和排行榜服务。 存储 在存储方面,主要使用了 3 张表: _User:存储用户信息,LeanCloud 内置表。 UserInfo:存储用户的详细信息,用于邮寄奖励。 Game:存储玩家每局游戏的数据。 为了保证游戏安全,只有用户信息是通过 LeanCloud 存储 SDK 直接操作的。而游戏相关的数据,都是通过 LeanCloud SDK 请求到云引擎中处理后保存的。参考文档 云引擎 云引擎是 LeanCloud 推出的服务端托管平台。通常比较关键的数据,我们推荐不要使用 SDK 直接操作,而是通过云引擎进行操作。参考文档 在抢蛋糕游戏中,为了保证游戏安全,我们在游戏结束后并没有在客户端直接通过 LeanCloud SDK 上传分数到排行榜,而是将游戏参数发送到云引擎,通过云引擎分析后再确定是否写入到排行榜。具体流程: 游戏开始,向服务端请求游戏数据,服务端会返回本局游戏的 id 和蛋糕数据;而对于「多次」作弊的玩家,将不返回游戏数据。 游戏结束,客户端将本局游戏的参数提交给服务端,包括:本局游戏 id、分数、蛋糕点击数量、时间戳、签名、蛋糕点击索引序列。 服务端对游戏数据进行合法性检测,如果通过则更新排行榜,否则丢弃并标记用户作弊(作弊检测方法会在后面有详细介绍)。 排行榜 排行榜是 LeanCloud Play 为游戏开发者提供的一项新的服务。它除了能提供方便的数据更新接口,还提供了排行榜成绩更新、榜单管理等配置。参考文档 在抢蛋糕游戏中,除了使用常规的「更新玩家成绩」之外,还用到了对作弊玩家进行「榜单移除」的操作。 更新玩家成绩 [代码]... // 提交分数 scoreInLeaderBoard = calcScoreInLeaderBoard(score); AV.Leaderboard.updateStatistics(currentUser, { free: scoreInLeaderBoard }) 标记作弊玩家,并移除榜单成绩 /** * 标记用户作弊 * @param {*} user 用户 */ function markUser(user) { let cheat = user.get("cheat") ? user.get("cheat") : 0; console.log(`cheat: ${cheat}`); cheat += 1; user.set("cheat", cheat); user.save(); if (cheat > MAX_CHEAT_COUNT) { // 如果超过最大作弊次数,则清除榜单 AV.Leaderboard.deleteStatistics(user, ["score"]) .then(() => { console.log(`remove ${user} statistics`); }) .catch(console.error); } } [代码] 作弊检测 对于面向程序员制作的游戏,我们猜测到大家可能会通过技术手段来获取更高分数。为了增加大家破解的趣味性,我们也提供了一些作弊检测机制供大家突破——通过运行时作弊检测和离线数据分析生成了最终的榜单数据。 运行时作弊检测 具体过程: 在游戏开始时,客户端向服务端发起开始请求,服务端随机生成本局游戏的蛋糕序列(共 200 个,游戏频率为每 0.1s 生成 1 个),将当前时间戳、用户、蛋糕序列保存至 Game 对象。 将 Game 对象 id和蛋糕序列下发至客户端,客户端根据蛋糕序列生成蛋糕,在游戏过程中,记录玩家点击蛋糕索引。 游戏结束后,将 Game 对象 id、分数、每种蛋糕点击的数量、结束时间戳、签名(md5(id + score + timestamp))和蛋糕点击索引序列发送给服务端。 服务端接收到参数后,对数据进行校验。 校验包括: 提交参数是否完整(基础检测) 分数和蛋糕点击数量是否匹配(逻辑检测) 分数和蛋糕索引是否匹配(逻辑检测) 服务端重新计算签名是否匹配(防止修改明文参数) 验证游戏时长是否合理(超过 2 倍游戏时长,则认为玩家可能是在分析请求) 对于检测到作弊的玩家,本局游戏成绩将不会更新排行榜,并记录 1 次作弊,超过 10 次作弊的玩家,将不能请求到游戏开始时的数据。 离线数据分析 运行时作弊检测并不足以抵挡住广大开发者破解的热情,很快就有用户梳理清楚了协议参数。所以在运行时检测后,我们又默默记下了用户的参数,用于离线分析。 校验包括: 验证游戏时长是否小于 1 倍游戏时长(游戏至少需要 18 秒完成,15秒游戏 + 3秒倒计时,有些同学竟然 2 秒就把游戏结束请求发来了) 蛋糕点击索引是否有重复(逻辑判断,有些 Android 插件可以让游戏卡住,使同一个蛋糕被点击 N 次,则会叠加多次分数) 蛋糕点击索引是否超过允许最大值(排行榜中有位 500+ 分的朋友通过解包,分析协议,通过模拟请求,顺利通过了上述检测,但是竟然在请求中把 200 个蛋糕索引全部赋值了,而正常游戏中最多只能点击到 150 个,即 15 秒 x 每秒 10 个) 致命缺陷 这类游戏是没办法防住按键(触摸)精灵的。如果通过「图像识别 + 自动点击脚本」可以轻松点击完所有的蛋糕并有效避开炸弹,则可以通过上述检测。 有人说如果服务端运算可不可以,思路是屏幕点击的坐标交由服务端运算,但是对于按键精灵类的脚本还是无法避免,并且还会增加项目的开发量(服务端要对不同分辨率和坐标做一些处理)。如果其他同学有办法做更有效的检测,希望能反馈到 LeanCloud 论坛,大家共同讨论。 以上便是我们此次开发小游戏的心得体会,希望能对大家有所帮助。
2019-04-28 - 这文档是敌特派来的人写的吗?目的就是为了整死我们搬砖工吗?
事情是这样的。 我司一小破程序,打开时类似这样,显示一个logo,一个标题 [图片] 经过一个2秒的动画效果,logo和标题就移动到上面部分了,同时渐显出来一个loading组件,这些都是使用小程序的Animation API实现的。 [图片] [图片] 现在需求来了。 我们想在首屏渲染后。在图标往上移的动画执行周期中,将背景色缓慢从蓝色变为白色。 (别问为什么要变背景色,我们准备待会加完班拿上弹弓组团去打设计师家玻璃了) [图片] 有朋友会说了,这不是很简单嘛,弄个定时器去替换class不就行了? 我只想说,no no no。朋友,我们搬砖就要有搬砖的样子嘛。 什么时间搬,搬多少,什么时间停,都要严谨嘛。 天真的我,想当然的就拍着胸脯向BOSS表示小意思啦。 [图片] naive的我心里想着 肯定会有动画执行开始和结束一个callback接口的嘛 然鹅,、翻遍了小程序文档里关于动画的各个段落之后才发现 [图片] 神马?? 我不信!一定是我的眼刚刚瞎了,我要再看一遍。 [图片] [图片] [图片] [图片] [图片] PS 看,多么言简意赅的文档! 在看多了外面那些"妖艳贱货"的文档后,如此小清新的文档,还真让我这老司机虎躯一震。 // TODO 我当即在心里暗暗发誓,我一定要强烈建议我司将此文档规范引进并在我司大范围实践,太他【文明用语】高效了。 END PS 在我不懈的努力下 在某毒找到了一篇关于动画重置的实例 [图片] [图片] 哦也,三七三十一,一定是我聋了才没看见这么大个接口 同事心里还在做自我批判,怎么能轻易的就甩锅给腾讯爸爸。 祭出我的Ctrl+F大法 [图片] 果然。还是我太天真。竟然没有搜到 0/0? 在经过了一番苦苦的某毒搜索之后,猛然意识到,或许是我姿势不对? [图片] 谢天谢地,博客园诚不我欺。确实有这个东东。 我默默的打开了唯一的一条搜索结果学习了起来。你猜怎么着? [图片] 我发现了腾讯爸爸藏起来的彩蛋。 哇,没想到小程序团队这么调皮。 在动画相关的所有文档里,竟然半个字都没提有这几个事件。保密工作做的很到位。表扬。5星好评。 [图片] 根据文档,照猫画虎。 [图片] [图片] 控制台没有任何反应 [图片] 一定是我姿势不对,我换换姿势。 [图片] [图片] 一顿操作猛如虎,然鹅发现并没有什么卵用。 [图片] [图片] [图片] 我盯着这条说明,默默的给自己点上了一根烟后陷入了痛苦的沉思。 期间我尝试了各种姿势,都没有找到关于WXSS animation到底是个什么鬼。 我只知道有Animation这个动画API。或许他俩是一个东西? 但是为什么Animation里没有关于它的只言片语? [图片] 既然Animation里没有写,肯定是另外一套体系吧? 灵光一闪, oh no,别又是腾讯爸爸调皮了把文档藏起来了吧。 [图片] [图片] 经过地毯式的搜索及换遍了各种姿势想要跟我的小程序互动一把后。 [图片] [图片] [图片] 我选择死亡。 [图片] [图片] 我想起那天夕阳下调的微信小程序,那是我逝去的青春。。。 IDE: v1.02.1901230 Library: 2.4.2
2019-01-28 - 小程序的个别使用这用一直出现闪屏怎么解决
小程序的个别使用这用一直出现闪屏,还有些一直打不开,出现request:fail ssl的,这些都怎么解决啊,求大神指导
2018-12-23 - 7.0版本微信小程序重新打开小程序会出现闪屏(图片重渲染)问题
问题: 7.0 版本微信 小程序 点击右上角按钮退出微信,再进入时,出现闪屏问题。 备注: Android 环境下,退出,再点击小程序logo进入,重复2次以后会闪屏。ios没问题。 切出后,点击其他小程序或者打开聊天,再进入小程序也不会出问题。 麻烦官方看到后,解决下这个问题,页面使用图片背景时,这个问题特别明显,影响到了正常使用。
2019-01-08 - wx.navigateBack的失败回调方法未执行 @官方
当页面只有一个时,调用wx.navigateBack失败,然后回调函数未执行, fail 或者complete 都没执行
2018-11-30 - 微信审核问题---求官方解答
尊敬的微信官方人员: 你好。 我的审核失败原因: 你的小程序"AI视推展示",提审时间2019-02-18 16:42:30,代码发布审核未通过,原因如下: 1:小程序内容不符合规则: (1):你好。你的小程序【企业圈】模块涉嫌混淆官方服务功能,请把名称、页面内容资料清空。 我已经把 “企业圈”,改为了 “企业动态”。还是审核不通过,请问官方人员还应该如何修改。 [图片]
2019-02-19