- iPhone 11 Pro Max 无法长按识别二维码
长按的时候会触发 iphone11自带的图片预览功能,没法出现长按识别二维码功能。有没有什么办法可以规避掉这个问题
2020-05-09 - Vue.js双向绑定原理和MVVM框架的简单实现
前言 由于近两年前端技术变革速度太快,vue不论针对web项目开发,网站制作,还是app,小程序开发,都越来越流行,其便捷性及易用程度都让你不得不考虑去学习。这里就围绕着Vue.js框架的双向数据绑定的实现原理进行一个剖析。 内容 前端主流MVVM框架双向绑定原理简述: [代码]1、AngularJs(脏检测机制): 脏检查机制,主要是依据 $watch 对象来监测数据是否更新。 2、Vue.js(前端数据对象劫持): Vue.js是通过数据劫持结合发布者-订阅者模式的方式来实现的。 3、React(手动触发绑定): React本身并没有提到双向绑定的概念,但是可以利用setState api对states数据进行更新,从而实现数据层于视图层的同步更新(结构更新,重新“渲染”子树(虚拟DOM),找出最小改动步骤,打包DOM操作,对真实DOM树进行修改)。 [代码] what is mvvm ? [代码]Model:数据模型,存储数据 View:带特殊属性、指令的 html 模板 ViewModel:依靠模板上面的指令,修改model数据后自动渲染view视图 [代码] Vue.js实现双向绑定的核心API: Object.defineProperty(),这个es5提供的API是实现双向绑定的核心,最主要的作用是重写数据的get、set方法,当我们访问或设置对象的属性的时候,都会触发相对应的函数动一些手脚做点我们自己想做的事情,实现“数据劫持”(发布者-订阅者模式)的效果。 详细用法 & 解释: https://www.jianshu.com/p/8fe1382ba135 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 一个简单例子 [代码] let obj = { name: 'JesBrian' }; let age = 23; Object.defineProperty(obj, 'age', { configurable: false, // 是否可以删除属性 默认不能删除 enumerable: true, // 是否可以被枚举 默认是不能被枚举(遍历) // get,set 设置时不能设置 writable和value,要一对一对设置,交叉设置/同时存在会报错 // value: 23, // 设置属性的值 // writable: true, // 是否可以修改对象 get() { // 获取 obj.age 的时候就会调用get方法 getToDo(age); return age; }, set(val) { // 将值重新赋给 obj.age setToDo(val, age); age = val; } }); function getToDo (val) { console.log('get - ' + val); }; function setToDo (nVal, oVal) { console.log('set - nVal = ' + nVal + ', oVal = ' + oVal); }; console.log(obj.age); // 23 delete obj.age; // configurable设为false 删除无效 console.log(obj.age); // 23 obj.age = 24; console.log(obj.age); // 24 for (let key in obj) { // 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的,需要设置enumerable为true才可以 否则只能拿到singer 属性 console.log(key); // name, age } [代码] 实现的思路: 实现数据监听器Observer,用Object.defineProperty()重写数据的get、set,值更新就在set中通知订阅者更新数据。 实现模板编译Compile,深度遍历dom树,对每个元素节点的指令模板进行替换数据以及订阅数据。 实现Watch用于连接Observer和Compile,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。 简单的实现一个MVVM框架: View 含有属性和指令的模板 [代码] <div id="app"> <input type="text" v-model="name"> <h3 v-bind="name"></h3> <input type="text" v-model="testData1"> <h3>{{ testData1 }}</h3> <input type="text" v-model="testData2"> <h3>{{ testData2 }}</h3> </div> [代码] 调用方法,初始化实例 [代码] window.onload = function () { var app = new MiniVue({ el: '#app', // dom data: { // 数据 testData1: 'Mini Vue', testData2: '双向绑定', name: 'JesBrian' } }) } [代码] 实现MiniVue类 [代码] function MiniVue(options = {}) { this.$options = options; // 配置挂载 this.$el = document.querySelector(options.el); // 获取挂载的dom元素 this._data = options.data; // 数据挂载 this._watcherTpl = {}; // watcher池 this._observer(this._data); // 传入数据,执行函数,重写数据的get set this._compile(this.$el); // 传入dom,执行函数,编译模板 发布订阅 }; [代码] MiniVue里具体实现的发布-订阅 [代码] // Watch类: // 1、在模板编译_compile()阶段发布订阅 // 2、在赋值操作的时候,更新视图 function Watcher(el, vm, val, attr) { this.el = el; // 指令对应的DOM元素 this.vm = vm; // myVue实例 this.val = val; // 指令对应的值 this.attr = attr; // dom获取值,如value获取input的值 / innerHTML获取dom的值 this.update(); // 更新视图 } Watcher.prototype.update = function () { this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图 } // 数据监听器_observer: // 用Object.defineProperty()遍历data重写所有属性的get set。然后在给对象的某个属性赋值的时候,就会触发set。在set中我们可以监听到数据的变化,然后就可以触发watch更新视图。 MiniVue.prototype._observer = function (obj) { var _this = this; Object.keys(obj).forEach(key => { // 遍历数据 _this._watcherTpl[key] = { // 每个数据的订阅池() _directives: [] }; var value = obj[key]; // 获取属性值 var watcherTpl = _this._watcherTpl[key]; // 数据的订阅池 Object.defineProperty(_this._data, key, { // 双向绑定最重要的部分 重写数据的set get configurable: true, // 可以删除 enumerable: true, // 可以遍历 get() { console.log(`${key}获取值:${value}`); return value; // 获取值的时候 直接返回 }, set(newVal) { // 改变值的时候 触发set console.log(`${key}更新:${newVal}`); if (value !== newVal) { value = newVal; for (let item of watcherTpl._directives) { // 遍历订阅池 item.update(); // 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图 } } } }) }) } [代码] MiniVue的_compile模板编译实现 [代码] // 首先是深度遍历dom树,遍历每个节点以及子节点。 // 将模板中的变量替换成数据,初始化渲染页面视图。 // 把指令绑定的属性添加到对应的订阅池中 // 一旦数据有变动,收到通知,更新视图。 MiniVue.prototype._compile = function (el) { var _this = this, nodes = el.children; // 获取要挂载app的dom for (var i = 0, len = nodes.length; i < len; i++) { // 遍历dom节点 var node = nodes[i]; if (node.children.length) { _this._compile(node); // 递归深度遍历 dom树 } // 如果有v-model属性,并且元素是input或者textarea,监听它的input事件 if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) { node.addEventListener('input', (function (key) { var attVal = node.getAttribute('v-model'); // 获取v-model绑定的值 _this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅在set的时候更新数据 node, _this, attVal, 'value' )); return function () { _this._data[attVal] = nodes[key].value; // input值改变的时候将新值赋给数据触发set 进而触发watch 更新视图 } })(i)); } if (node.hasAttribute('v-bind')) { // v-bind指令 var attrVal = node.getAttribute('v-bind'); // 绑定的data _this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据 node, _this, attrVal, 'innerHTML' )) } var reg = /\{\{\s*([^}]+\S)\s*\}\}/g, txt = node.textContent; // 正则匹配{{}} if (reg.test(txt)) { node.textContent = txt.replace(reg, (matched, placeholder) => { // matched匹配的文本节点包括{{}}, placeholder 是{{}}中间的属性名 var getName = _this._watcherTpl; // 所有绑定watch的数据 getName = getName[placeholder]; // 获取对应watch 数据的值 if (!getName._directives) { // 没有事件池, 创建事件池 getName._directives = []; } getName._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅在set的时候更新数据 node, _this, placeholder, 'innerHTML' )); return placeholder.split('.').reduce((val, key) => { return _this._data[key]; //获取数据的值触发get返回当前值 }, this.$el); }); } } } [代码] 扩展: ES6 新的 API Proxy MDN 详细文档 阮一峰es6要点总结——Proxy 为什么 Vue 3 要将 Object.defineProperty 转为使用 Proxy Object.defineProperty 无法检测数组的变化 Object.defineProperty 当对象属性增删的时候,是监控不到的 (Vue.$set()) 使用 Proxy 重构 _observer 原型方法 [代码] MiniVue.prototype._observer = function (obj) { var _this = this // 把代理器返回的对象存到 this._data 里面 for (let key in obj) { // 遍历数据 _this._watcherTpl[key] = { // 每个数据的订阅池() _directives: [] }; _this._data = new Proxy(obj, { set(target, key, value) { console.log(`${key}更新:${value}`); // 利用 Reflect 还原默认的赋值操作 let res = Reflect.set(target, key, value) var watcherTpl = _this._watcherTpl[key]; // 数据的订阅池 for (let item of watcherTpl._directives) { // 遍历订阅池 item.update(); // 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图 } return res } }) } } [代码] 总结: 很好,经过一轮的大费周折,我们终于简单的实现了一个MVVM的框架,成功的实现了双向数据绑定的效果。我们在日常使用轮子的时候不仅仅要会用,更重要的是知道这个轮子的实现原理,并且对其进行深入的探究,甚至是自己模仿思路重新造一个轮子。
2019-07-24 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - iOS封停,按照要求修改后,申诉为什么被驳回
此小程序因iOS上涉及虚拟支付被封,目前修改处理后已上线,线上版本已去除iOS支付按钮和引导等字样。 但是现在申诉别驳回,下面是驳回详情👇 [图片] 请问图片里这个页面下面的文案有问题吗?为什么给驳回了。 [图片] 如果有问题的话,我把下面那个浮层去掉的话能申诉通过吗? 但是现在申诉入口找不到怎么办? 希望官方人员给个回复啊,拜托了🙏
2018-08-31