Vue.js-自定义指令

学习笔记:自定义指令自定义指令自定义指令的注册方法分为全局注册和局部注册,比如注册一个v-focus指令,用于在<input>、<textarea>元素初始化时自动获得焦点,两种写法分别是://全局注册Vue.directive(‘focus’, {});//局部注册new Vue({ el: ‘#app’, directives: { focus: {} }});自定义指令的选项是由几个钩子函数组成,每个都是可选的:bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于document中)。update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。componentUpdate:被绑定元素所在模板完成一次更新周期时调用。unbind:只调用一次,指令与元素解绑时调用。可以根据需求在不同的钩子函数内完成逻辑代码。在元素插入父节点时就调用,用到的最好是inserted。<div id=“app”> <input type=“text” v-focus></div>Vue.directive(‘focus’, { inserted(el) { el.focus(); }});new Vue({ el: ‘#app’,});打开页面,input输入框自动获得焦点,成为可输入状态。每个钩子函数都有几个参数可用:el 指令所绑定的元素,可以用来直接操作DOMbinding 一个对象,包含以下属性:name 指令名,不包括v-前缀value 指令的绑定值oldValue 指令绑定的前一个值,仅在update和componentUpdate钩子中可用。无论值是否改变都可用。expression 绑定值的字符串形式。arg 传给指令的参数。modifiers 一个包含修饰符的对象。vnode Vue编译生成的虚拟节点。oldVnode 上一个虚拟节点仅在update和componentUpdate钩子中可用。<p data-height=“265” data-theme-id=“0” data-slug-hash=“QxKQqY” data-default-tab=“html,result” data-user=“whjin” data-embed-version=“2” data-pen-title=“Vue-自定义指令” class=“codepen”>See the Pen Vue-自定义指令 by whjin (@whjin) on CodePen.</p><script async src=“https://static.codepen.io/ass...;></script>如果需要多个值,自定义指令可以传入一个JavaScript对象字面量。<div id=“app”> <div v-test="{msg:‘hello’,name:‘Andy}"></div></div>开发一个实时时间转换指令v-time<p data-height=“265” data-theme-id=“0” data-slug-hash=“wXoMpg” data-default-tab=“html,result” data-user=“whjin” data-embed-version=“2” data-pen-title=“Vue-实时时间转换指令” class=“codepen”>See the Pen Vue-实时时间转换指令 by whjin (@whjin) on CodePen.</p><script async src=“https://static.codepen.io/ass...;></script>Time.getFormatTime()方法就是自定义指令v-time所需要的,入参为毫秒级时间戳,返回已经整理好时间格式的字符串。在bind钩子里,将指令v-time表达式的值binding.value作为参数传入TimeFormatTime()方法得到格式化时间,再通过el.innerHTML写入指令所在元素。定时器el.__timeout__每分钟触发一次,更新时间,并且在unbind钩子里清除掉。总结:在编写自定义指令时,给DOM绑定一次性事件等初始动作,建议在bind钩子内完成,同时要在unbind内解除相关绑定。 ...

March 18, 2019 · 1 min · jiezi

Vue.js-表单与v-model

学习笔记:表单与v-model表单与v-modelVue.js提供了v-model指令,用于在表单类元素上双向绑定数据。使用v-model后,表单控件显示的值只依赖所绑定的数据,不再关心初始化时的value属性,对于在<textarea></textarea>之间插入的值,也不会生效。使用v-model时,如果是用中文输入法输入中文,一般在没有选定词组前,也就是在拼音阶段,Vue时不会更新数据的,当敲下汉字时才会触发更新。如果希望总是实时更新,可以用@input替代v-model。事实上,v-model也是一个特殊的语法糖,只不过它会在不同的表单上智能处理。单选按钮单选按钮在单独使用时,不需要v-model,直接使用v-bind把你当一个布尔类型的值,为真时选中,为否时不选。<div id=“app”> <input type=“radio” :checked=“picked” id=“radio”> <label for=“radio”>单选按钮</label></div>如果是组合使用来实现互斥选择的效果,就需要v-model配合value来使用。<div id=“app”> <p> <input type=“radio” v-model=“picked” value=“html” id=“html”> <label for=“html”>HTML</label> </p> <p> <input type=“radio” v-model=“picked” value=“js” id=“js”> <label for=“js”>JavaScript</label> </p> <p> <input type=“radio” v-model=“picked” value=“css” id=“css”> <label for=“css”>CSS</label> </p> <p>选择的项:{{picked}}</p></div>复选框复选框单独使用时,也是用v-model绑定一个布尔值。组合使用时,也是v-model与value一起,多个勾选框都绑定到同一个数组类型的数据,value的值在数组中,就会选中这一项。这一过程也是双向的,在勾选时value得知也会自动push到这个数组中。选择列表选择列表就是下来选择器,分为单选和多选两种方式。<option>是备选项,如果含有value属性,v-model就会优先匹配value值;如果没有,就会直接匹配<option>的text。在业务中,<option>经常用v-for动态输出,value和text也是用v-bind动态输出。绑定值在业务中,有时需要绑定一个动态的数据,这时可以使用v-bind实现。单选按钮<div id=“app”> <input type=“radio” v-model=“picked” :value=“value”> <p>{{picked}}</p> <p>{{value}}</p></div>data: { picked: false, value: 123},在选中时,app.picked===app.value,值都是123。复选框<div id=“app”> <input type=“checkbox” v-model=“toggle” :true-value=“value1” :false-value=“value2”> <p>{{toggle}}</p> <p>{{value1}}</p> <p>{{value2}}</p></div>选择列表<div id=“app”> <select v-model=“selected”> <option :value="{number:123}">123</option> </select> {{selected.number}}</div>当选中时,app.selected是一个Object,所以app.selected.number===123。修饰符与事件的修饰符类似,v-model也有修饰符,用于控制数据同步的时机。.lazy在输入框中,v-model默认是在input事件中同步输入框的数据,使用修饰符.lazy会转变为在change事件中同步。<div id=“app”> <input type=“text” v-model.lazy=“message”> <p>{{message}}</p></div>这时,message并不是实时变化,而是在失焦或按回车键时才更新。.number使用修饰符.number可以将输入转换成Number类型,否则输入的数字,但它的类型其实是String,在数字输入框时比较有用。<div id=“app”> <input type=“number” v-model.number=“message”> <p>{{typeof message}}</p></div>.trim修饰符.trim可以自动过滤输入的首尾空格。<input type=“text” v-model.trim=“message”> ...

March 18, 2019 · 1 min · jiezi

Vue.js-方法与事件

学习笔记:方法与事件方法与事件@click调用得方法名后可以不跟括号(),如果该方法有参数,默认会将原生事件对象event传入。这种在HTML元素上监听事件的设计看似将DOM与JavaScript紧耦合,违背分离的原理,实则刚好相反。因为通过HTML就可以知道调用的是哪个方法,将逻辑与DOM解耦,便于维护。最重要的是,当viewModel销毁时,所有的事件处理器都会自动销毁,无需自己处理。Vue提供了一个特殊变量$event,用于访问原生DOM事件。<div id=“app”> <a href=“https://www.apple.com/" @click=“handleClick(‘禁止打开’,$event)">打开链接</a></div>修饰符Vue支持以下修饰符:.stop.prevent.capture.self.once具体用法如下:修饰符功能使用示例阻止单击事件冒泡<a @click.stop=“handle”></a>提交事件不再重载页面<form @submit.prevent=“handle”></form>修饰符可以串联<a @click.stop.prevent=“handle”></a>只有修饰符<form @submit.prevent></form>添加事件侦听器时使用事件捕获模式<div @click.capture=“handle”>…</div>只当事件在该元素本身(不是子元素)触发时执行回调<div @click.self=“handle”>…</div>只触发一次,组件同样适用<div @click.once=“handle”>…</div>在表单元素上监听键盘事件时,还可以使用按键修饰符。修饰符功能使用示例只有在keyCode是13时调用vm.submit()<input @keyup.13=“submit”>除了具体的某个keyCode外,Vue还提供了一些快捷名称:.enter.tab.delete(补货“删除”和“退格”键).esc.space.up.down.left.right这些按键修饰符也可以组合使用,或和鼠标一起配合使用:.ctrl.alt.shift.meta

March 18, 2019 · 1 min · jiezi

Vue.js-计算属性和class与style绑定

学习笔记:前端开发文档计算属性所有的计算属性都以函数的形式写在Vue实例中的computed选项内,最终返回计算后的结果。计算属性的用法在一个计算属性中可以完成各种复杂的逻辑,包括运算、函数调用等,只要最终返回一个结果即可。计算属性还可以依赖多个Vue实例的数据,只要其中任一数据变化,计算属性就会重新执行,视图也会更新。每一个计算属性都包含一个getter和一个setter。绝大多数情况下,只会用默认的getter方法读取一个计算属性,在业务中很少用到setter,所以在声明一个计算属性时,可以直接使用默认的写法,不必将getter和setter都声明。计算属性除了简单的文本插值外,还经常用于动态地设置元素的样式名称class和内联样式style。当使用组件时,计算属性也经常用来动态传递props。计算属性还有两个很实用的小技巧容易被忽略:一是计算属性可以依赖其他计算属性;二是计算属性不仅可以依赖当前Vue实例的数据,还可以依赖其他实例的数据。<div id=“app1”></div> <div id=“app2”> {{reverseText}}</div>var app1 = new Vue({ el: “#app1”, data: { text: ‘123,456’ },});var app2 = new Vue({ el: “#app2”, computed: { reverseText: function () { //这里依赖的是实例app1的数据text return app1.text.split(’,’).reverse().join(’,’); } }});计算属性缓存没有使用计算属性,在methods中定义了一个方法实现了相同的效果,甚至该方法还可以接受参数,使用起来更灵活。使用计算属性的原因在于它的依赖缓存。一个计算属性所依赖的数据发生变化时,它才会重新取值,在上例中只要text值不改变,计算属性也就不更新。但是methods则不同,只要重新渲染,它就会被调用,因此函数也会被执行。使用计算属性还是methods取决于你是否需要缓存,当遍历大数组和做大量计算时,应当使用计算属性,除非你不希望得到缓存。v-bind及class与style绑定v-bind的主要用法是动态更新HTML元素上的属性。在数据绑定中,v-bind最常见的两个应用就是元素的样式名称class和内联样式style的动态绑定。绑定class的几种方式对象语法给v-bind:class设置一个对象,可以动态地切换class:<div id=“app”> <div :class="{‘active’:‘isActive’}">测试文字</div></div>new Vue({ el: “#app”, data: { isActive: true },});对象中也可以传入多个属性,动态切换class。另外,:class可以与普通class共存。<div class=“static” :class="{‘active’:‘isActive’,’error’:isError}">测试文字</div>data: { isActive: true, isError: false}当:class的表达式过长或逻辑复杂时,还可以绑定一个计算属性。当条件多于两个时,都可以使用data或computed。除了计算属性,也可以直接绑定一个Object类型的数据,或者使用类似计算属性的methods。数组语法当需要应用多个class时,可以使用数组语法,给:class绑定一个数组,应用一个class列表:<div id=“app”> <div :class="[activeCls,errorCls]">测试文字</div></div>new Vue({ el: “#app”, data: { activeCls: ‘active’, errorCls: ’error’ }});// 结果<div class=“active error”>测试文字</div>也可以使用三元表达式来根据条件切换class:<div :class="[isActive ? activeCls : ‘’,errorCls]">测试文字</div>new Vue({ el: “#app”, data: { isActive: true, activeCls: ‘active’, errorCls: ’error’ }});当class有多个条件时,可以在数组语法中使用对象语法:<div id=“app”> <div :class="[{‘active’:isActive},errorCls]">测试文字</div></div>使用计算属性给元素动态设置类名,在业务中经常用到,尤其是在写复用的组件时,所以在开发过程中,如果表达式较长或逻辑复杂,应该尽可能地优先使用计算属性。在组件中使用如果直接在自定义组件上使用class或:class,样式规则会直接应用到这个组件的根元素上。Vue.component(‘my-component’, { template: &lt;p class="article"&gt;一些文本&lt;/p&gt;});然后在调用这个组件时,应用对象语法或数组语法给组件绑定class:<div id=“app”> <my-component :class="{‘active’:isActive}"></my-component></div>这种用法仅适用于自定义组件的最外层是一个根元素,否则会无效。当不满足这种条件或需要给具体的子元素设置类名时,应当使用组件的props来传递。绑定内联样式使用:style可以给元素绑定内联样式,方法与:class类似,也有对象语法和数组语法,很像直接在元素上写CSS。<div id=“app”> <div :style="{‘color’:color, ‘fontSize’:fontSize+‘px’}">文本</div></div>new Vue({ el: “#app”, data: { color: ‘red’, fontSize: 14 }});一般把样式写在data或computed中:<div id=“app”> <div :style=“styles”>文本</div></div>new Vue({ el: “#app”, data: { styles: { color: ‘red’, fontSize: 16 + ‘px’ } }});在实际业务中,:style的数组语法并不常用,可以写在一个对象里面,而较为常用的是计算属性。另外,使用:style时,Vue.js会自动给特殊的CSS属性名称增加前缀,比如transform。 ...

March 18, 2019 · 1 min · jiezi

Vue.js-开篇

学习笔记:Vue.js基础知识基础知识构造函数Vue的根实例new Vue({}),并启动Vue应用。var app = Vue({ el: “#app”, data: {}, methods: {}});变量app代表这个Vue实例。其中,必不可少的选项是el,用于指定一个页面中已存在的DOM元素来挂载Vue实例,可以是HTMLElement,也可以是CSS选择器。var app = Vue({ el: document.getElementById(‘app’)}); 挂载成功后,可以通过app.$el访问该元素。Vue提供了很多常用的实例属性和方法,都以$开头。data选项用于声明应用内需要双向绑定的数据。建议所有会用到的数据都预先在data内声明,提升业务的可维护性。Vue实例new Vue({}),这里可以使用app代理了data对象里的所有属性,可以这样访问data中的数据:console.log(app.name);除了显式地声明数据外,还可以指向一个已有的变量,并且它们之间默认建立了双向绑定,当修改其中任意一个时,另一个也会跟着变化。var myData = { a: 1};var app = Vue({ el: “#app”, data: myData});app.a = 2;console.log(myData.a);//2myData.a = 3;console.log(app.a);//3生命周期Vue的生命周期钩子:created:实例创建完成后调用,此阶段完成了数据的观测等,但未挂载,$el还不可用。(需要初始化处理一些数据时会比较有用)mounted:el挂载到实例上后调用,第一个业务逻辑会在这里开始。beforeDestroy:实例销毁之前调用。主要解绑一些使用addEventListener监听的事件等。这些钩子与el和data类似,也是作为选项写入Vue实例中,并且钩子的this指向的是调用它的Vue实例。插值与表达式使用(Mustache语法){{}}是最基本的文本插值方法,它会自动将我们双向绑定的数据实时显示出来。v-html直接输出HTML,而不是将数据解析后的纯文本。<div id=“app”><span v-html=“link”></span></div>new Vue({ el: “#app”, data: { link: ‘<a href="#">this is a link.</a>’ }});link的内容将会被渲染成一个a标签,而不是纯文本。如果将用户产生的内容使用v-html输出后,有可能导致XSS攻击,所以要在服务端对用户提交的内容进行处理,一般可将<>转义。如果想要显示{{}}标签,不进行替换,使用v-pre即可跳过这个元素和它的子元素的编译过程。在{{}}中除了简单的绑定属性值外,还可以使用JavaScript表达式进行简单的运算、三元运算等。Vue只支持单个表达式,不支持语句和流程控制。在表达式中不能使用用户自定义的全局变量,只能使用Vue白名单内的全局变量,例如Math和Date。过滤器Vue.js支持在{{}}插值的尾部添加一个管道符(|)对数据进行过滤,经常用户格式化文本,比如字母全部大写、货币千位使用逗号分隔等。过滤的规则是自定义的,通过给Vue实例添加选项filter来设置。<div id=“app”> {{date | formatDate}}</div>过滤器也可以串联,而且可以接收参数:<!–串联–>{{message | filterA | filterB}}<!–接收参数–>{{message | filterA(‘arg1’,‘arg2’)}}过滤器应当用于处理简单的文本转换,如果要实现更为复杂的数据转换,应该使用计算属性。指令事件指令(Directives)是Vue.js模板中最常用的一项功能,它带有前缀v-。指令的主要职责就是当其表达式的值改变时,相应地将某些行为应用到DOM上。v-bind的基本用途是动态更新HTML元素上的属性,比如id、class等。另一个非常重要的指令就是v-on,用来绑定事件监听器。在普通元素上,v-on可以监听原生的DOM事件,除了click外还有dbclick、keyup、mousemove等。表达式可以是一个方法名,这些方法都写在Vue市里的methods属性内,并且是函数的形式,这些函数的this指向的是当前Vue实例本身,因此可以直接使用this.xxx的形式访问或修改数据。Vue.js将methods里的方法进行代理,可以像访问Vue数据一样调用方法:<div id=“app”> <p v-if=“show”>这是一段为本</p> <button @click=“handleClose”>点击隐藏</button></div>new Vue({ el: “#app”, data: { show: true }, methods: { handleClose: function () { this.close() }, close: function () { this.show = false } }});在handleClose方法中直接通过this.close()调用了close()函数。var app = new Vue({ el: “#app”, data: { show: true }, methods: { init: function (text) { console.log(text); }, }, mounted: function () { this.init(‘在初始化时调用’); }});app.init(‘通过外部调用’);语法糖语法糖是指在不影响功能的情况下,添加某种方法实现同样的效果,从而方便程序开发。Vue.js的v-bind和v-on指令都提供了语法糖,也可以说是缩写,比如v-bind缩写成:,多用于a、img标签;v-on缩写成@,所用于input、button标签。 ...

March 18, 2019 · 1 min · jiezi

基于Azkaban的任务定时调度实践

本文由云+社区发表作者:maxluo一、Azkaban介绍Azkaban是LinkedIn开源的任务调度框架,类似于JavaEE中的JBPM和Activiti工作流框架。Azkaban功能和特点:1,任务的依赖处理。2,任务监控,失败告警。3,任务流的可视化。4,任务权限管理。常见的任务调度框架有Apache Oozie、LinkedIn Azkaban、Apache Airflow、Alibaba Zeus,由于Azkaban具有轻量可插拔、友好的WebUI、SLA告警、完善的权限控制、易于二次开发等优点,也得到了广泛应用。下图为Azkaban的架构图,主要有三部分组成:Azkaban Webserver、Azkaban Executor、 DB。Webserver主要负责权限验证、项目管理、作业流下发等工作;Executor主要负责作业流/作业的具体执行以及搜集执行日志等工作;MySQL用于存储作业/作业流的执行状态信息。图中所示的是单executor场景,但是实际应用中大部分的项目使用的都是多executor场景。1.1 作业流执行过程 Azkaban webserver会根据搜集起来的Executor的状态选择一个合适的任务运行节点,并将任务推送给该节点,管理并运行该工作流的所有job。1.2 部署模式Azkaban支持三种部署模式,分别用于学习和测试,高可用部署方式。solo-server模式DB使用的是一个内嵌的H2,Web Server和Executor Server运行在同一个进程里。这种模式包含Azkaban的所有特性,但一般用来学习和测试。two-server模式DB使用的是MySQL,MySQL支持master-slave架构,Web Server和Executor Server运行在不同的进程中。分布式multiple-executor模式DB使用的是MySQL,MySQL支持master-slave架构,Web Server和Executor Server运行在不同机器上,且有多个Executor Server。1.3 编译部署编译环境yum install git yum install gcc-c++yum install java-1.8.0-openjdk-devel下载源码&解压mkdir –p /data/azkaban/installcd /data/azkabanwget https://github.com/azkaban/azkaban/archive/3.42.0.tar.gzmv 3.42.0.tar.gz azkaban-3.42.0.tar.gztar -zxvf azkaban-3.42.0.tar.gz编译cd azkaban-3.42.0./gradlew build installDist -x testsolo-server模式部署下面为了部署测试简单,采用solo-server模式进行部署。cd /data/azkaban/install tar -zxvf ../azkaban-3.42.0/azkaban-solo-server/build/distributions/azkaban-solo-server-0.1.0-SNAPSHOT.tar.gz -C .修改时区cd /data/azkaban/install/azkaban-solo-server-0.1.0-SNAPSHOTtzselect #选择Asia/Shanghaivim ./conf/azkaban.propertiesdefault.timezone.id=Asia/Shanghai #修改时区启动./bin/azkaban-solo-start.sh注:启动/关闭必须进到/data/azkaban/install/azkaban-solo-server-0.1.0-SNAPSHOT/目录。登录http://ip:port/监听端口具体见配置./conf/azkaban.properties:jetty.port=8081IP为服务器地址。用户名见配置./conf/azkaban-users.xml, 具有admin角色的用户名是azkaban,密码是azkaban:<azkaban-users> <user groups=“azkaban” password=“azkaban” roles=“admin” username=“azkaban”/> <user password=“metrics” roles=“metrics” username=“metrics”/> <role name=“admin” permissions=“ADMIN”/> <role name=“metrics” permissions=“METRICS”/></azkaban-users>详细配置方法内容见:https://azkaban.github.io/azk…二、Azkaban与数仓集群的网络互通目前Azkaban与云产品Snova网络互通基于两个事实:1,Azkaban Executor的服务器能够访问外网或者能够访问Snova的服务端IP。2,Snova提供外网IP访问的能力。下图为网络连通示意图:Azkaban Executor在执行运行job时,其脚本或者命令通过公网IP访问Snova。接下来分步骤讲解如何基于Azkaban的工作流。三、前期准备工作3.1 Snova集群创建外网IP在Snova集群控制台,基础配置页面,点击“申请外网地址”,等待运行成功后,会看到访问该集群的外网IP地址。3.2 添加Snova访问地址白名单在Snova控制台,集群详情页,配置页,新建白名单如下所示。为什么要建这个访问白名单?为了系统安全,Snova默认情况是拒绝不在白名单的地址或者用户访问数据库。即配置IP白名单CIDR地址为xx.xx.xx.xx/xx,包括所有Azkaban Executor的所有IP或者网段。3.3 用户授权在3.2章节中,建议单独创建一个用户用于SCF的任务调度和计算。因此需要授权该用户访问对应数据库和表的权限。创建用户CREATE USER scf_visit WITH LOGIN PASSWORD ‘scf_passwd’;并设置用户访问密码。数据库表授权GRANT ALL on t1 to scf_visit;四、定时调度任务http://node1:8081/index登录Azkaban,Create Project=>Upload 上一步生成的zip包 =>execute flow执行一步步操作即可。具体步骤可以见 参考文档:https://www.cnblogs.com/qingy…4.1 创建工程4.2 创建jobjob1文件名:job.job,必须以.job结尾。内容如下:type=commandcommand=echo “job1"retries=5注:type类型及使用方式见https://azkaban.github.io/azk…job2type=commanddependencies=job1retries=5command=echo “job2 xx"command.1=ls –al注:dependencies为该job依赖的任务文件名(不包括.job后缀)。如果依赖多个,则以逗号分隔,如job2,job5。job3type=commanddependencies=job2,job5command=sleep 60job5type=commanddependencies=job1command=pwdjob6type=commanddependencies=job3command=sh /data/shell/admin.sh psqlx其中/data/shell/admin.sh ,注意作用可以封装用户功能代码,脚本内容如下,实现读取表中的数据,并进行打印:function psqlx() { result=PGPASSWORD=scf_passwd psql -h xx.xx.xx.xx -p xx -U scf_visit -d postgres <<EOF select * from t1; EOF echo $result }4.3上传job压缩包压缩所有job文件到一个zip包中。注意:所有文件必须在压缩包的根目录中,没有子目录,如下:4.3运行查询执行过程和结果。4.4设置周期调度在调试成功完成后,可以设置周期调度计划,比如每天定时进行工作流的调度,完成运行计划。五、实践总结对市面上最流行的两种调度器,给出以下详细对比。知名度比较高的应该是Apache Oozie。5.1 对比从功能上来对比 两者均可以调度linux命令、mapreduce、spark、pig、java、hive、java程序、脚本工作流任务 两者均可以定时执行工作流任务从工作流定义上来对比 1、Azkaban使用Properties文件定义工作流 2、Oozie使用XML文件定义工作流从工作流传参上来对比 1、Azkaban支持直接传参,例如${input} 2、Oozie支持参数和EL表达式,例如${fs:dirSize(myInputDir)}从定时执行上来对比 1、Azkaban的定时执行任务是基于时间的 2、Oozie的定时执行任务基于时间和输入数据从资源管理上来对比 1、Azkaban有较严格的权限控制,如用户对工作流进行读/写/执行等操作 2、Oozie暂无严格的权限控制5.2 应用场景对于数据分析基本上可以概括为三个步骤: 一、数据导入。二、数据计算。三、数据导出。三个类型的任务可能是多个并发运行,且任务依赖。因此Azkaban基本上能满足以上的任务调度管理和运行场景需求。首先创建一个job1,用于用户数据导入,比如从cos导入,任务内容执行以下SQL命令。insert into gp_table select * from cos_table;数据的导入也可以通过其他导入工具,如DataX将其他数据库的数据周期性的导入Snova数据仓库中。因此只需把DataX部署到Azkaban Executor机器对应目录,并进行调用即可其次,创建job2,用户数据计算分析。该步骤可以是多个job多次运行的结果,也可以是并发运行。最后,可以把计算结果出库到应用数据库。insert into cos_table select * from gp_table;5.2 不足1,Azkaban目前Job粒度的失败重试理解相对复杂,在Projects->Executions找到对应的执行失败的Id,选择该执行实例ID,进入详情,点击重新运行,则会生成一个全新的工作流实例ID,而不是重新运行原来失败的实例ID,新的实例ID从失败的job开始运行,已经成功运行的直接跳过,不再运行。2,job通过shell命令启动复杂的程序,shell返回成功,并不代表程序运行成功。3,job运行管理容错性不足,当一个job提交一个运行任务后,此时重启或者executor进程挂掉,该任务将出现状态失败的情况,实际可能任务已经运行成功。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

March 14, 2019 · 1 min · jiezi

手把手教你实现一个引导动画

本文由云+社区发表作者:陈纪庚前言最近看了一些文章,知道了实现引导动画的基本原理,所以决定来自己亲手做一个通用的引导动画类。我们先来看一下具体的效果:点这里原理通过维护一个Modal实例,使用Modal的mask来隐藏掉页面的其他元素。根据用户传入的需要引导的元素列表,依次来展示元素。展示元素的原理:通过cloneNode来复制一个当前要展示元素的副本,通过当前元素的位置信息来展示副本,并且通过z-index属性来让其在ModalMask上方展示。大致代码如下: const newEle = target.cloneNode(true); const rect = target.getBoundingClientRect(); newEle.style.zIndex = ‘1001’; newEle.style.position = ‘fixed’; newEle.style.width = ${rect.width}px; newEle.style.height = ${rect.height}px; newEle.style.left = ${rect.left}px; newEle.style.top = ${rect.top}px; this.modal.appendChild(newEle);当用户点击了当前展示的元素时,则展示下一个元素。原理听起来是不是很简单?但是其实真正实现起来,还是有坑的。比如说,当需要展示的元素不在页面的可视范围内如何处理。当要展示的元素不在页面可视范围内,主要分为三种情况:展示的元素在页面可视范围的上边。展示的元素在页面可视范围的下边。展示的元素在可视范围内,可是展示不全。由于我是通过getBoundingClientRect这个api来获取元素的位置、大小信息的。这个api获取的位置信息是相对于视口左上角位置的(如下图)。对于第一种情况,这个api获取的top值为负值,这个就比较好处理,直接调用window.scrollBy(0, rect.top)来将页面滚动到展示元素的顶部即可。而对于第二、三种情况,我们可以看下图从图片我们可以看出来,当rect.top+rect.height < window.innerHeight的时候,说明展示的元素不在视野范围内,或者展示不全。对于这种情况,我们也可以通过调用window.scrollBy(0, rect.top)的方式来让展示元素尽可能在顶部。对上述情况的调节代码如下:// 若引导的元素不在页面范围内,则滚动页面到引导元素的视野范围内adapteView(ele) { const rect = ele.getBoundingClientRect(); const height = window.innerHeight; if (rect.top < 0 || rect.top + rect.height > height) { window.scrollBy(0, rect.top); }}接下来,我们就来一起实现下这个引导动画类。第一步:实现Modal功能我们先不管具体的展示逻辑实现,我们先实现一个简单的Modal功能。class Guidences { constructor() { this.modal = null; this.eleList = []; } // 入口函数 showGuidences(eleList = []) { // 允许传入单个元素 this.eleList = eleList instanceof Array ? eleList : [eleList]; // 若之前已经创建一个Modal实例,则不重复创建 this.modal || this.createModel(); } // 创建一个Modal实例 createModel() { const modalContainer = document.createElement(‘div’); const modalMask = document.createElement(‘div’); this.setMaskStyle(modalMask); modalContainer.style.display = ’none’; modalContainer.appendChild(modalMask); document.body.appendChild(modalContainer); this.modal = modalContainer; } setMaskStyle(ele) { ele.style.zIndex = ‘1000’; ele.style.background = ‘rgba(0, 0, 0, 0.8)’; ele.style.position = ‘fixed’; ele.style.top = 0; ele.style.right = 0; ele.style.bottom = 0; ele.style.left = 0; } hideModal() { this.modal.style.display = ’none’; this.modal.removeChild(this.modalBody); this.modalBody = null; } showModal() { this.modal.style.display = ‘block’; }}第二步:实现展示引导元素的功能复制一个要展示元素的副本,根据要展示元素的位置信息来放置该副本,并且将副本当成Modal的主体内容展示。class Guidences { constructor() { this.modal = null; this.eleList = []; } // 允许传入单个元素 showGuidences(eleList = []) { this.eleList = eleList instanceof Array ? eleList : [eleList]; this.modal || this.createModel(); this.showGuidence(); } // 展示引导页面 showGuidence() { if (!this.eleList.length) { return this.hideModal(); } // 移除上一次的展示元素 this.modalBody && this.modal.removeChild(this.modalBody); const ele = this.eleList.shift(); // 当前要展示的元素 const newEle = ele.cloneNode(true); // 复制副本 this.modalBody = newEle; this.initModalBody(ele); this.showModal(); } createModel() { // … } setMaskStyle(ele) { // … } initModalBody(target) { this.adapteView(target); const rect = target.getBoundingClientRect(); this.modalBody.style.zIndex = ‘1001’; this.modalBody.style.position = ‘fixed’; this.modalBody.style.width = ${rect.width}px; this.modalBody.style.height = ${rect.height}px; this.modalBody.style.left = ${rect.left}px; this.modalBody.style.top = ${rect.top}px; this.modal.appendChild(this.modalBody); // 当用户点击引导元素,则展示下一个要引导的元素 this.modalBody.addEventListener(‘click’, () => { this.showGuidence(this.eleList); }); } // 若引导的元素不在页面范围内,则滚动页面到引导元素的视野范围内 adapteView(ele) { const rect = ele.getBoundingClientRect(); const height = window.innerHeight; if (rect.top < 0 || rect.top + rect.height > height) { window.scrollBy(0, rect.top); } } hideModal() { // … } showModal() { // … }}完整的代码可以在点击这里调用方式const guidences = new Guidences();function showGuidences() { const eles = Array.from(document.querySelectorAll(’.demo’)); guidences.showGuidences(eles);}showGuidences();总结除了使用cloneNode的形式来实现引导动画外,还可以使用box-shadow、canvas等方式来做。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

March 13, 2019 · 2 min · jiezi

Web前端面试技术点

常规问题:一般来说会问如下几方面的问题:做过最满意的项目是什么?项目背景为什么要做这件事情?最终达到什么效果?你处于什么样的角色,起到了什么方面的作用?在项目中遇到什么技术问题?具体是如何解决的?如果再做这个项目,你会在哪些方面进行改善?技术二面主要判断技术深度及广度你最擅长的技术是什么?你觉得你在这个技术上的水平到什么程度了?你觉得最高级别应该是怎样的?浏览器及性能一个页面从输入 URL 到页面加载完的过程中都发生了什么事情?越详细越好 (这个问既考察技术深度又考察技术广度,其实要答好是相当难的,注意越详细越好)谈一下你所知道的页面性能优化方法?这些优化方法背后的原理是什么?除了这些常规的,你还了解什么最新的方法么?如何分析页面性能?其它除了前端以外还了解什么其它技术么?对计算机基础的了解情况,比如常见数据结构、编译原理等技术点沟通:HTML+CSS1、盒子模型,块级元素和行内元素特性与区别。 2、行内块的使用,兼容性解决。 3、清除浮动的方式以及各自的优劣。4、文档流的概念、定位的理解以及z-index计算规则&浏览器差异性。 5、CSS选择器以及优先级计算。 6、常用的CSS hack。7、遇到的兼容性问题与解决方法。 8、垂直水平居中的实现方式。9、常用布局的实现(两列布局、三列适应布局,两列等高适应布局等)。Javascript1、犀牛书封面的犀牛属于神马品种?(蛋逼活跃气氛用。。。) 2、常用的浏览器内核。 3、常用的DOM操作,新建、添加、删除、移动、查找等。4、String于Array常用方法。 5、设备与平台监测。 6、DOM的默认事件、事件模型、事件委托、阻止默认事件、冒泡事件的方式等。7、jQuery的bind、live、on、delegate的区别(考察点与上一条重叠,切入点不同)。8、JS变量提升、匿名函数、原型继承、作用域、闭包机制等。 9、对HTTP协议的理解。 10、Ajax的常用操作,JS跨域的实现原理。HTML:语义标签语义化CSS:动态居中动画Bootstrap 样式类Preprocessor兼容性 Hack与特征检测CSS3属性与性能JS:Name hoistingPrototypeClosureMain loopPromiseDelegationCross domainMobile:渐进增强移动端交互兼容性问题Debug工具 方法主体是看简历发挥,对方写什么就问什么为主:框架、库、浏览器工作原理、NLP、算法、HTTP… 辅助问题几乎是我个人必备的问题:为什么做前端,学习前端过程。1、跟什么人在一起工作 2、过去项目的挑战 3、自学的途径3个问题基本上就知道这个人的能力水平和瓶颈了,人的很多局限都是被环境限制的,通过闲聊中夹杂的不经意的问题,候选人的画像就已经很鲜明了。处于当前的环境多长时间,有没有突破环境限制的行动,就能评估出潜力和眼界。什么浏览器兼容、作用域、框架等等的东西不会,不记得都可以学,要不了多长时间,关键还是有没有潜力、有没有好的习惯。在能力方面:对 HTML / CSS / JavaScript 具有专家级别的知识;有较熟练使用 AngularJS / Ember.js / jQuery 或者其它类库的经验;较熟悉第三方组件(插件)生态环境及具体案例;有较熟练使用 Jade / Swig / Handlebars / Mustache 或者其它模板引擎的经验;有较熟练使用 SASS 或者其它 CSS 预处理器的经验;有较熟练使用 CoffeeScript 的经验;对 CSS / JavaScript 设计模式有很好的认识及应用;对常用数据结构和算法熟悉;有使用 GruntJS / GulpJS 任务运行器的经验;有使用 Yeoman 生成器的经验;有诸如 Bower / Volo / JSPM 等前端静态资源包管理器使用经验;熟悉本地及远程(甄姬)调试操作;有 Git 的使用经验;Q:简单介绍下 React / Vue 的生命周期A:几个钩子函数基本能报出来(如果不讲究按顺序、按挂载/更新区分、能把单词用英文念出来并且念对的话),稍微深入一点问下各个阶段都做了什么,一半以上就“不太清楚”了。更有甚者我问React,对方回答 created、mounted,提醒之后还觉得自己没错的。Q:【React】定义一个组件时候,如何决定要用 Functional 还是 Class?A:简单的用 Function,复杂的用 Class。(不能算错吧……但也不能算答到点子上)追问怎么界定“复杂”,答不上来。Q:【React】HOC、(非)受控组件、shouldComponentUpdate、React 16 的变化A:不清楚、没接触过。Q:【Vue】介绍一下计算属性,和 data、methods、watch 的异同A:基本都能巴拉一些,说的大部分都对,但就是说不到最关键的“当且仅当计算属性依赖的 data 改变时才会自动计算”。Q:【Vue】为什么 SFC 里的 data 必须是一个函数返回的对象,而不能就只是一个对象?A:我承认这个问题有点小难,有一定的区分度,不是每个人都有关注过,但是官方文档有说明这一点,但凡看过的肯定有印象。即便没完整看过文档,在初次学习的过程中难道就不觉得奇怪吗?“学而不思”的人和“学而思”的人,区别还是挺大的。Q:CSS 选择器的权重A:经典问题了吧?背都能背出来吧?伪类、伪元素分不清楚,只知道内联、!important、ID、Class之间的顺序,加上其它的就懵了,而且只说谁大于谁,讲不出具体的计算方法。单层选择器比较还行,几个叠加起来就迷糊了。Q:JS 有哪几种原始类型A:基础题,能说上来几个,答不全,主要问题集中在 null 和 undefined 没考虑进去、对象和数组算不算原始类型、以及 Symbol很多人不知道。Q:ES 2015+ 有哪些新特性A:这题可以说的很多,根据应聘者的回答去展开,可以很容易地看出应聘者有没有系统地学习过这方面的东西,以及有没有持续地去跟进语言标准的发展。但这一题能回答的比较好的,寥寥无几,大部分是遇到问题然后零零散散现学的,不够全面、也不够深入,简单用过,但稍微问点细节就只有“尴尬而不失礼仪的微笑”了。Q:工程化工具的使用(Webpack、ESLint、Yarn、Git、……)A:基本都有所接触,但只是“用过”,算不上“会用”,一切顺利还好,真遇到问题了,立马就懵。Q:Node.jsA:写过 Demo 的水平。(比较初级)Q:未来 1~2 年的职业规划、下一步最想学的技术、最希望往什么方向发展、怎么看待XXX技术A:大部分人对自己没有一个明确的态度和规划。说白了就是还没从学校里出来,什么都等着别人来安排。通用技能有哪些(请看如下图)? ...

March 9, 2019 · 1 min · jiezi

一个思维转换,让Echarts能够绘制不刻度均匀数值轴

最近项目组接到了许多对图表有特殊要求的需求,比如今天说的这个呈现光纤的中断时长分布的需求:光纤的中断大部分落在30分钟之内,但偶尔遇到特殊情况时,会出现中断一两天甚至更长的时间,如果把这些时长分布放在一个刻度均匀的数轴上,势必造成大部分的指标(此处为柱状图)特别短的问题,因此,希望建立一个刻度一开始均匀分布,比如10min、20min,到60min之后立刻升为2hour、1day这样的需求。一开始拿到这个任务,我的第一反应,是查找Echarts的配置项手册,希望通过在yAxis上做手脚来解决问题。然鹅事情总是没有想象的顺利,echart对y轴对配置只能指定几种type:‘category’、‘value’和‘log’,虽然配置为log对数轴也可以解决较大值对较小值对影响的问题,但当值较小时也无法通过长度区分开,不够完美。庆幸的是,在echarts的GitHub官网上,pissang大大给出了一个思路:深受启发!为什么一定要依赖Echarts本身给予解决方案呢?完全可以自己构造一个新的分布呀!不多说,开始码:第一步:利用yAixs.axisLabel.formatter伪造一个不均匀坐标轴先给大家看看具体的代码://利用formatter将y轴上本来为70(min),80(min)的点强制改为2hour和1dayformatter:function(value) { let item=’’; if(value==70){ item=‘2hour’ }else if(value===80){ item=‘1Day’ }else{ item=value+’ min’ } return item} 第二步:将数据映射到一个特定的分布这句话的意思,其实就是自己构造一个函数,将原始数据里较大的数值转换成一个小数值我是这样做的://模拟数据,其中的200、150是应该落在2hour和1day之间,所以映射后的数据应该落在70min到80min之间let data = [10, 15, 4, 20, 200, 150, 19,70,1441];function formatData(arr){ //自己构造一个用来映射data到均匀数轴上的方法 for(let i=0;i<arr.length;i++){ if(arr[i]>60&&arr[i]<=120){ let percent1=(arr[i]-60)/120; arr[i]=percent110+60; }else if(arr[i]>120&&arr[i]<1440){ let percent2 =(arr[i]-120)/1440; console.log(percent2); arr[i]=percent210+70; } } return arr;}上面这个formatData,其实就是对data数组里超过60的数据进行改造,如果这个数据超过了60min且小于120min(2hour),就按照这个数据在60到120段应该有的比例,映射到60到70段里,而超过2hour且小于1day的数据,则同样按这个方法,计算映射到70到80段里这样一来,任务已经基本完成啦!第三步:将数据反映射为原来的值但是现在在图表里,我们拿到的是映射后的数据,如果此时的tooltip是开放的,那么用户在tooltip里读到的数据就不是原来的200min,150min这种的了,而变成了一个70min到80min之间的较小数据。怎么办?只能在每个需要展示数据的地方,严防死守,将这几条特殊的数据反计算回去咯!tooltip:{ formatter:function(params) { //由于在tooltip里需要展示原始的数据,所以要把映射后的数据反计算回去 let str=params.name+’:’+params.value; if(params.value>=60&&params.value<70){ let percent = (params.value-60)/10 let value = Math.round(percent120+60);//注意此处的percent已经是个浮点数了,所以得到value之前要用四舍五入取整才行 str=params.name+’ : ‘+value; }else if(params.value>70&&params.value<80){ let percent = (params.value-70)/10; let value =Math.round(percent1440+120); str=params.name+’ : ‘+value; } return ‘<div style=“width:70px;height:50px;display:flex;align-items:center”>\ <span style=“background-color:#D53A35;width:15px;height:15px;display:inline-block;border-radius:50%"></span>\ <span>’+str+’</span>\ </div>’ }}嗯,现在就可以了,接下来上一个效果图炫耀一下:一切看上去都是那么的完美,其实这种方法当然还是瑕疵的,你能看出来吗:) ...

March 8, 2019 · 1 min · jiezi

vueX10分钟入门

通过本文你将:1.知道什么是vueX.2.知道为什么要用VueX.3.能跑一个VueX的例子。4.了解相关概念,面试的时候能说出一个所以然5.项目中用Vuex知道该学什么东西。好,走起。1.什么是vueX?Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。翻译成人话,Vuex是vuejs的官方管理数据状态的库。官网:https://vuex.vuejs.org/zh/2.为什么用它?举个例子,你用vue开发一个app,不同的组件,你都需要用户信息,还有一些公用的数据,你每一个组件请求一遍浪费性能,你不请求组件间属性和参数传来传去,你自己维护很墨迹,麻烦也容易出错。好吧,你觉得干不好或者麻烦,那么vueX帮你解决这个事儿。这个没什么复杂的,大学图书馆,自助借还书,每次都把书乱放,维护很麻烦,怎么办,都还给图书馆管理员,图书馆管理员统一管理调配。ok,图书管理员就是VueX.3.怎么用?1.安装npm install vuex –save2.初始化store.js,(vue-cli安装项目目录不墨迹),一般放到src/store/store.js下面,初始化代码,相当于搞了一个图书管理员。import Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export const store = new Vuex.Store({})3.写需要的组件创建一个Form.vue组件,怼下面的内容,<template> <div> <label for=“dabinge666”>你喜欢彬哥哪一点</label> <input name=“dabinge666”> </div></template>创建一个展示组件Display.vue<template> <div> <p>我喜欢彬哥:</p> </div></template>打开App.vue,删掉没用的东西,直接怼下面的代码,<template> <div id=“app”> <Form/> <Display/> </div></template><script>import Form from ‘./components/Form’import Display from ‘./components/Display’export default { name: ‘App’, components: { Form, Display }}</script>到这里,架子就搭好了。4.增加各种需要的东西,import Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export const store = new Vuex.Store({ state: { love: ’’ }, mutations: { change(state, love) { state.love = love } }, getters: { love: state => state.love }})这里注意,你不用去管这些破概念,你就照猫画虎,我写啥你抄啥,抄几遍,你就知道数据流向了。你不知道鼠标叫mouse,也不影响你玩电脑。love就是你喜欢我的东西,相当于一个变量,被传来传去的一会。好了,就这么简单可以用了。4.使用VueX打开main.js,导入,然后用。import { store } from ‘./store/store’new Vue({ el: ‘#app’, store, components: { App }, template: ‘<App/>’})到这里就相当于图书管理员上岗等着学生来还书了,来啊,互相伤害啊!5.我来了……既然搞数据,躲不开刚才我们的搞的表单组件,打开Form.vue<template> <div> <label for=“dabinge666”>你喜欢彬哥哪一点?</label> //输入:离我远一点 <input @input=“changed” name=“dabinge666”> </div></template><script>export default { methods: { changed: function(event) { //大声喊出你的对彬哥的爱,让整个图书馆都听到 this.$store.commit(‘change’, event.target.value) } }}</script>打开,Display.vue<template> <div> <p>我喜欢彬哥: {{ $store.getters.love }}</p> </div></template>漂亮,如果你运行成功,你就会发现,页面里面出现,我喜欢彬哥:离我远一点。告辞! ...

March 4, 2019 · 1 min · jiezi

又一轮子?Typescript+React+Redux,放弃saga,支持服务器渲染同构

你是原生Redux用户?有没有觉得写Redux太繁琐了?你是dvaJS用户?有没有觉得redux-saga概念太多,且yield无法返回TS类型?试试react-coat吧:项目地址:https://github.com/wooline/react-coat// 仅需一个类,搞定 action、reducer、effect、loadingclass ModuleHandlers extends BaseModuleHandlers { @reducer protected putCurUser(curUser: CurUser): State { return {…this.state, curUser}; } @reducer public putShowLoginPop(showLoginPop: boolean): State { return {…this.state, showLoginPop}; } @effect(“login”) // 使用自定义loading状态 public async login(payload: {username: string; password: string}) { const loginResult = await sessionService.api.login(payload); if (!loginResult.error) { this.updateState({curUser: loginResult.data}); Toast.success(“欢迎您回来!”); } else { alert(loginResult.error.message); } } // uncatched错误会触发@@framework/ERROR,监听并发送给后台 @effect(null) // 不需要loading,设置为null protected async ["@@framework/ERROR"](error: CustomError) { if (error.code === “401”) { this.dispatch(this.actions.putShowLoginPop(true)); } else if (error.code === “301” || error.code === “302”) { this.dispatch(this.routerActions.replace(error.detail)); } else { Toast.fail(error.message); await settingsService.api.reportError(error); } } // 监听自已的INIT Action,做一些异步数据请求 @effect() protected async “app/INIT” { const [projectConfig, curUser] = await Promise.all([ settingsService.api.getSettings(), sessionService.api.getCurUser() ]); this.updateState({ projectConfig, curUser, }); }}react-coat 特点集成 react、redux、react-router、history 等相关框架仅为以上框架的糖衣外套,不改变其基本概念,无强侵入与破坏性结构化前端工程、业务模块化,支持按需加载同时支持 SPA(单页应用)和 SSR(服务器渲染)使用 typescript 严格类型,更好的静态检查与智能提示开源微框架,源码不到千行,几乎不用学习即可上手与 Dva 的异同引入 ActionHandler 观察者模式,更优雅的处理模块之间的协作去除 redux-saga,使用 async、await 替代,简化代码的同时对 TS 类型支持更全面原生使用 typescript 组织和开发,更全面的类型安全路由组件化、无 Page 概念、更自然的 API 和更简单的组织结构更大的灵活性和自由度,不强封装脚手架等支持 SPA(单页应用)和 SSR(服务器渲染)快速切换,支持模块异步按需加载和同步加载快速切换差异示例:使用强类型组织所有 reducer 和 effect// Dva中常这样写dispatch({ type: ‘moduleA/query’, payload:{username:“jimmy”}} })//本框架中可直接利用ts类型反射和检查:this.dispatch(moduleA.actions.query({username:“jimmy”}))差异示例:State 和 Actions 支持继承// Dva不支持继承// 本框架可以直接继承class ModuleHandlers extends ArticleHandlers<State, PhotoResource> { constructor() { super({}, {api}); } @effect() protected async parseRouter() { const result = await super.parseRouter(); this.dispatch(this.actions.putRouteData({showComment: true})); return result; } @effect() protected async ModuleNames.photos + “/INIT” { await super.onInit(); }}差异示例:在 Dva 中,因为使用 redux-saga,假设在一个 effect 中使用 yield put 派发一个 action,以此来调用另一个 effect,虽然 yield 可以等待 action 的派发,但并不能等待后续 effect 的处理:// 在Dva中,updateState并不会等待otherModule/query的effect处理完毕了才执行effects: { * query (){ yield put({type: ‘otherModule/query’,payload:1}); yield put({type: ‘updateState’, payload: 2}); }}// 在本框架中,可使用awiat关键字, updateState 会等待otherModule/query的effect处理完毕了才执行class ModuleHandlers { async query (){ await this.dispatch(otherModule.actions.query(1)); this.dispatch(thisModule.actions.updateState(2)); }}差异示例:如果 ModuleA 进行某项操作成功之后,ModuleB 或 ModuleC 都需要 update 自已的 State,由于缺少 action 的观察者模式,所以只能将 ModuleB 或 ModuleC 的刷新动作写死在 ModuleA 中:// 在Dva中需要主动Put调用ModuleB或ModuleC的Actioneffects: { * update (){ … if(callbackModuleName===“ModuleB”){ yield put({type: ‘ModuleB/update’,payload:1}); }else if(callbackModuleName===“ModuleC”){ yield put({type: ‘ModuleC/update’,payload:1}); } }}// 在本框架中,可使用ActionHandler观察者模式:class ModuleB { //在ModuleB中兼听"ModuleA/update" action async [“ModuleA/update”] (){ …. }}class ModuleC { //在ModuleC中兼听"ModuleA/update" action async [“ModuleA/update”] (){ …. }}遵循规则:M 和 V 之间使用单向数据流整站保持单个 StoreStore 为 Immutability 不可变数据改变 Store 数据,必须通过 Reducer调用 Reducer 必须通过显式的 dispatch ActionReducer 必须为 pure function 纯函数有副作用的行为,全部放到 Effect 函数中每个 reducer 只能修改 Store 下的某个节点,但可以读取所有节点路由组件化,不使用集中式配置快速上手及 Demo本框架上手简单8 个新概念:Effect、ActionHandler、Module、ModuleState、RootState、Model、View、Component4 步创建:exportModel(), exportView(), exportModule(), createApp()3 个 Demo,循序渐进:入手:Helloworld进阶:SPA(单页应用)升级:SPA(单页应用)+SSR(服务器渲染) ...

February 28, 2019 · 2 min · jiezi

破境Angular(三)Angular构件之模块

一、知识点Angular模块核心知识点如下:1.模块的作用。2.模块各个元数据的含义和作用3.模块有哪些分类,分类原则4.模块的惰性加载机制5.开发时如何对模块进行规划二、模块作用首先,模块作为一个容器,有封装代码的作用,组件、指令、管道、服务的根宿主均是模块。其次,一个模块可以导入其他模块,并导出其他模块的组件、指令、管道和服务,这种导入、导出能力可以向后传递,使得后续模块不必重复导入相同的模块,例如:1.假设模块A已经导出本模块的指令和服务,使得其他模块可以使用2.模块B导入了模块A,并导出模块A的指令和服务3.模块C导入模块B后则可以使用模块A的指令和服务而不需要再次导入A这种能力使得可以规划一个share模块来统一导出公共的通用构件,其他模块只需要导入share模块则可。三、模块元数据模块元数据如下:1.@NgModule是一个装饰器,声明某个类是Angular模块,看起来很像Java的注解,但实际有很大不同,前者用于在编译期给编译器编译代码,后者用于在运行期控制代码逻辑。2.declarations: 声明属于该模块的组件、指令和管道3.entryComponents:可以动态加载进视图的组件列表,一般是根组件4.providers:需要提供依赖注入的服务列表5.imports: 要导入的其他模块6.exports: 导出的组件,指令,管道。只有先导出,其他模块再导入本模块后这些构件才能被其他模块使用。四、模块分类根据模块的作用不同进行模块分类有利于代码维护,Angular模块分为以下几类:1.特性模块,完成特定的特性功能的模块,例如订单模块,排课模块2.路由特性模块,带路由的特性模块3.路由模块,专门实现路由功能的模块4.服务模块,提供公共服务的模块,如HTTP请求服务5.UI模块,用于封装公共的UI组件,例如表格组件,穿梭框组件。五、惰性加载在开发过程中经常可见惰性加载的例子,如在数据量大时,树的加载通常只加载一级节点数据,当有需要时才加载子节点数据。惰性加载的目的是缩短单次交互的时间,提升客户体验。为了避免将所有模块代码一次加载到客户端,Angular支持模块惰性加载,只有带路由的特性模块才能惰性加载。特性加载的实现如下:六、模块规划在开始编写项目代码前和项目开发过程中,应先做模块规划再编写代码,而不是整个项目只有一个特性模块和一个路由模块来完成所有事情。模块规划主要参考模块分类以及单一职责原则:1.先划分好特性,再按照特性划分特性模块2.每个特性的路由模块独立3.拆分单独的服务模块,并根据服务的作用维度不同,继续拆分和聚合4.拆分独立的UI组件模块5.划分需要惰性加载和急性加载的模块.End下期预告:【破境Angular(四)Angular构件之服务】专题链接:破境Angular(一)初识Angular破境Angular(二)Angular构件之模块关注Java栈及其衍生技术,通过实战经验分享,传播Java栈技术和提高Java栈开发效率。

February 26, 2019 · 1 min · jiezi

破境Angular(二)Angular构件

一、Angular构件Angular的构件如下:1.模块是一个容器,用于存放代码块;可导入其他模块中导入的功能;导出指定的功能。2.组件定义和控制屏幕上的一片区域,构成一个视图3.服务是一个明确定义了用途的类,例如加密,鉴权4.指令可用于控制视图中DOM树的展现以及数据绑定5.管道的作用是做数据转换,例如转换货币单位,大小写。管道的功能也可以通过服务来实现,管道的写法更简洁,优雅,例如:{{name | Uppercase}}。二、Angular构件关系Angular的构件关系如下:1.一个模块可以导入其他模块,也可以导出其他模块2.模块包含组件,指令,管道,完成特定的功能3.组件可以包含组件,组件嵌套构成一颗组件树4.组件可以使用服务来处理特定的业务逻辑5.组件代码由HTML模板,CSS样式代码和Type Script代码构成,展现一个视图6.组件可使用指令来控制DOM树的展现和数据绑定7.组件可使用管道做数据格式转换,例如数字精度转换,货币单位转换,大小写转换.End下期预告:【破境Angular(三)Angular构件之模块】专题链接:破境Angular(一)初识Angular关注Java栈及其衍生技术,通过实战经验分享,传播Java栈技术和提高Java栈开发效率。

February 25, 2019 · 1 min · jiezi

Vue.js基础教程

文章链接:Vue.js基础教程开发工具准备:根据个人喜欢选择IDE,我使用的是WebStorm,推荐使用Atom和VSCode;安装git base和node.js;安装vue-cli,命令npm i -g @vue/cli;新建vue-cli项目:方法一:通过图形界面进行安装vue ui;方法二:通过命令行安装vue create project-name运行项目npm run serve,端口8080。<!–more–>双向绑定v-model双向绑定大多用于表单事件,通过监听使用者输入的事件来更新内容。现阶段大部分工作在App.vue上,template与普通写法一致,js写法:export default { name: ‘app’, data() { return { title: ‘vue.js’, myname: ‘请输入名字’ } }}去掉空白符.trim直接在v-model后加上.trim即可。<input type=“text” v-model.trim=“name” value=“name”>懒加载.lazy在离开input时才更新输入的内容,在v-model后加上.lazy即可。限定输入数字.number在v-model后加上.number即可。遍历v-for遍历有一个基本的模板:<div id=“app”> <ul v-for="(item,index) in member" :key=“index”> <li>{{item}}</li> </ul></div>组件component在App.vue中引入components中的组件:<template> <div id=“app”> <Card /> </div></template><script> import Card from ‘./components/Card’ export default { components: { Card } }</script>数据传递props<template> <div id=“app”> <Card :cardData=“cardData”/> </div></template>其中:cardData=“cardData"是这个组件的核心,用于绑定属性cardData。其他数据展示工作放在Card.vue组件中进行。JS ResultEDIT ON <template> <div class=“card_wall”> <div class=“card” v-for=“item in cardData” :key=“item.name”> <div class=“card_title”>{{item.name}}</div> <div class=“card_body”> <p>生日:{{item.birthday}}</p> <p>E-mail:{{item.mail}}</p> </div> </div> </div></template><script> export default { props: { cardData: { type: Array, required: true } } }</script>这里解析一下<div class=“card_wall”></div>包裹<div class=“card”></div>主要是方便后期应用扩展,以及让应用整体更稳定。生命周期我不喜欢用官网的生命流程图来讲解这个内容,使用文字表达更加让思维清晰。初始化:设置数据监听,编译模板,挂载到DOM并在数据变化时更新DOM等;生命周期钩子:其实就是一个过程处理,类似于服务站。生命周期钩子简介beforeCreate:实例初始化created:实例建立完成(可以取得$data)beforeMount:模板挂载之前(还没有生成html)mounted:模板挂载完成beforeUpdate:如果data发生变化,触发组件更新,重新渲染updated:更新完成beforeDestroy:实例销毁之前(实例还可以使用)destroyed:实例已销毁(所有绑定被解除、所有事件监听器被移除、所有子实例被移除)生命周期钩子用得最多的是mounted,主要用在调用属性、方法的时候,指令v-once指令第一次渲染完成后变为静态内容,其下的所有子元素都是这样的效果。v-pre指令v-pre指令会让指定元素被忽略。v-cloak指令v-cloak指令用于去除页面渲染数据时出现闪现的情况,使用方法:<template> <div id=“app”> <div v-cloak>${ item.title }</div> </div></template><style> [v-cloak] { display: none; }</style>v-html指令v-html指令会把html标签渲染成DOM显示在页面上。v-html指令只能对可信任的用户使用,否则容易受到XSS攻击。动画Vue动画一般在真正需要使用的情况下才加入页面,推荐在CSS中使用动画。加入渐变的时机v-if条件渲染v-show条件显示动态组件组件的根节点渐变的分类v-enter定义进入渐变时开始的样式。只存在组件插入前,组件插入后就移除。v-enter-active定义进入渐变的过程效果,可以设定渐变过程的时间(duration)和速度曲线(easing curve)。在组件被插入前生效,在完成动画时移除。v-enter-to定义进入渐变后结束的样式。在组件被插入后生效,同时v-enter被移除,并在完成进入渐变动画时移除。v-leave定义离开渐变时开始的样式。在触发组件离开渐变时生效,接着马上移除。v-leave-active定义离开渐变的过程效果,可以设定渐变过程的时间(duration)和速度曲线(easing curve)。在触发组件离开渐变时生效,在完成动画时移除。v-leave-to定义离开渐变后结束的样式。在触发组件离开渐变时生效,同时v-enter被移除,并在完成离开渐变动画时移除。transition自定义名称<template> <div id=“app”> <div class=“main”> <button @click=“change = !change”>縮放控制器</button> <transition name=“zoom”> <div v-if=“change” class=“ant_man_style”></div> </transition> </div> </div></template>.zoom-enter, .zoom-leave-to {width: 0px;height: 0px;}.zoom-enter-active, .zoom-leave-active {transition: width 1s, height 1s;}animation可以使用CSS中的animation动画设计更为华丽的效果。.zoom-leave-active {animation: special_effects 1.5s;}.zoom-enter-active {animation: special_effects 1.5s reverse;}@keyframes special_effects {}transition自定义动画类别除了在<transition>中设定name自定义前缀(属性),还可以预设动画类别。enter-class定义进入动画时开始的样式。enter-active-class定义进入动画的过程效果。enter-to-class定义进入动画后结束的样式。leave-class定义离开动画时开始的样式。leave-active-class定义离开动画的过程效果。leave-to-class定义离开动画后结束的样式。以上六个自定义属性优先级别高于一般渐变类别。CSS动画库:Animation.cssJavaScript钩子<transition>还可以绑定JavaScriptHooks,除了单独使用,也能结合CSS transition和animations一起使用。beforeEnter(el)在进入渐变或动画前生效。enter(el,callback)在进入渐变或动画的组件插入时生效。afterEnter(el)在进入渐变或动画结束时生效。enterCanceled(el)在未完成渐变或动画时取消。beforeLeave(el)在离开渐变或动画前生效。leaveCancelled(el)在未完成渐变或动画时取消。<transition @before-enter=“beforeEnter” @enter=“enter” @after-enter=“afterEnter” @enter-cancelled=“enterCancelled” @before-leave=“beforeLeave” @leave=“leave” @after-leave=“afterLeave” @leave-cancelled=“leaveCancelled”> <div v-if=“change” class=“ant_man_style”></div></transition>在enter和leave中必须使用done,不然它们会同时生效,动画也会瞬间完成。设定初始载入时的渐变如果想要设定一开始载入画面时组件的渐变效果,可以通过设定appear属性来实现。appear-class载入时开始的样式。appear-to-class载入过程的样式。appear-active-class载入结束时样式。<transition appear appear-class=“show-appear-class” appear-to-class=“show-appear-to-class” appear-active-class=“show-appear-active-class”></transition>先在<transition>标签内加入appear,接着类似自定义动画可以给class name命名。初始载入JavaScript HooksbeforeAppear载入前appear载入时afterAppear载入后appearCancelled取消载入(载入开始后)<div id=“app”> <transition appear @before-appear=“beforeAppear” @appear=“appear” @after-appear=“afterAppear” @appear-cancelled=“appearCancelled”> <div v-if=“change” class=“ant_man_style”></div> </transition></div>key对相同的标签元素使用key进行区分。渐变模式in-out和out-inin-out模式新加入的元素做渐变进入。渐变进入结束后,原存在的元素再渐变离开。out-in模式原存在的元素渐变离开。渐变离开结束后,新元素再渐变进入。<transition mode=“out-in”></transition><transition mode=“in-out”></transition>列表过渡<transition-group>会渲染出一个html标签,预设是<span>,也可以选择自定义tag为其他标签。无法使用(渐变模式in-out和out-in),因为不再是元素之间来回切换。每个元素需要设定一个key值,不能重复。列表乱数排序<transition-group>能够改变数组的排序,使用前需要先安装shufflenpm i –save lodash.shufflelet shuffle = require(’lodash.shuffle’)过滤器filterfilters串联filter可以同时串联多个filter函数。filters参数$emit父组件可以使用props把数据传递给子组件。子组件可以使用$emit触发父组件的自定义事件。 ...

February 22, 2019 · 1 min · jiezi

QQ音乐的动效歌词是如何实践的?

本文由云+社区发表作者:QQ音乐技术团队一、 背景1. 现状歌词浏览已经成为音乐app的标配,展示和动画效果也基本上大同小异,主要是单行的逐字染色的卡拉OK效果和多行的滚动效果。当然,我们也不例外。2. 目标我们的目标十分明确,一是提升歌词的基础体验,二是在此基础上,能提供差异化的VIP特效,来吸引用户开通VIP。二、探索技术方案经过多次的需求评审和沟通讨论,各方在需求的目标和细节上也达成了初步的统一。 产品的希望 :效果炫酷,能实现逐字动画(位移,翻转,渐隐渐现,模糊,粒子特效等),可配置等。开发的思考: 技术架构方案,性能挑战等,接下来我们简单介绍一下确定技术方案的过程。1. 技术方案选型这里最初的思路有两个方向,升级现有歌词组件和开发全新歌词组件。所谓知已知彼,百战不殆, 通过对移动端面主流竞品的技术方案和PC端类似方案的技术调研与分析。最终将技术方案锁定在以下三种:现有歌词组件升级Shader序列帧动画ASS序列帧动画2. 备选技术方案介绍下面简单介绍一下三种方案的原理和特点,如下表所示: 总的来说,就是在原生动画开发和帧动画方案中进行选择。3. 技术方案对比以下主要是从是否实现特效,开发的难度,方案的性能,实现的成本,跨平台等方面对比三种方案,具体细节如下表所示:4. 确定方案通过以上几个维度的综合考量:现有歌词组件基本上无法实现逐字动画。Shader帧动画开发周期长,实现成本高,逐字动画支持不是很好。ASS实现逐字动画,可通过植入动画标签实现复杂的特效,有开源支持,且跨平台。综上所述,ASS方案性价比最高。最终方案也确定采用ASS序列帧动画方案。三、 技术架构1. ASS技术工作原理介绍前面简单介绍了一下什么ASS字幕和帧动画的原理。我们知道ASS是一种字幕文件格式,属于高级字幕,可以制作出华丽的特效字幕。所以,要想在电影或者视频上显示ASS效果,首先要做的是编写ASS特效文件,然后再将ASS特效文件解析成序列帧动画的位图,最后将这些位图按照特定的顺序和一定的帧率进行播放,就能看到各种特效的动画。如下图所示:2. 如何接入ASS方案2.1 合成如下下图所示:,首先,需要准备展示内容(字幕或者歌词内容),比如一个文本文件,有了最基本的文本文件,怎么转换成ASS解析器能解析的ASS文件呢?答案是打K值,打K值是指给字幕文件加上时间轴属性。而是什么K值呢,就是ASS中K拉OK的效果标签代码,即每行甚至每个字的时间坐标。有了打完K值的ASS文件,我们就可以在视频播放器中浏览,也就有了最基本的逐字染色动画。如果要开发更复杂的特效,就需要加入更多的特效标签。而这一部分,就可以通过脚本加上动画模板(动效模板就是具有特定动画效果的ASS文件),将动画标签注入到打完K值ASS文件中,生成最终的ASS特效文件。至此,一个具有特效的ASS文件就诞生了。2.2 解析解析的过程相对比较简单。解析一个ASS文件,不仅需要ASS文件本身,还需要知道ASS文件是用什么字体合成的。这里补充一下,前面合成的时候,其中的动画模板也是需要指定是使用哪种字体来合成的。因为这里会涉及到字体的大小,间距等,对动画效果和排版的影响。然后,再回到解析上来,通过ASS文件加上字体库就可以解析生成特定序列的帧动画位图。3. 技术架构 最终方案的技术架构:功能上划分如下,后负责存储和合成;客户端负责解析和绘制,呈现用户最终的动画效果。4. 通用性上面提到了这套方案的通用性和易复用的特点。那除了动效歌词之外,我们还可以做些什么呢?首先,我们脱离业务对架构进行更高一层的抽象,梳理出了更通用的架构方。这里还需要补充一点,“字体库”,从字面上理解应该是一堆字体的容器,所以字体库应该是保存了一大堆的文字信息等。但其实不仅是文字也可以是图形,所以我们的动画效果可以不只是针对文字的,还可以设计一些图形动画效果。所以,这里可以有更多的想像空间。前面解析的过程我们提到,解析出一帧帧的图,就拿去直接播放了,这样我们就能实时看到动画效果。那如果把这些图片保存下来,根据业务需求在需要的时候再播放呢。这里就可以拆分出实时渲染和离线渲染两种方案。这里的渲染提供了两种方案:1. 实时渲染 将解析出来的位图立即绘制到屏幕上。适用场景:实时要求高的场景。特点: 对系统性能消耗大,需要注意当前场景的性能开销。2. 离线渲染将解析出来的位图保存到磁盘上,并可以此基础上建立序列帧动画的资源管理。适用场景:适用于异步化的场景。特点: 建议采用异步线程在后台处理,减少对主线程消耗。大家可以根据各自业务场景和特点灵活选择或者组合使用这两种方案。以上主要是介绍动效歌词技术方案的实现原理与架构介绍。四、技术难点与挑战在开发过程中,我们遇到了两个重要的问题:一个是在运行复杂的效果时,动画效果出现了肉眼可见的卡顿;另一个则是内存的问题,即使是比较简单的效果播放以后也会占用大量的内存。本文后半部分将重点阐述K歌是如何解决这两个问题的。1. 卡顿问题描述我们选取了一个较为复杂的效果,包含了大量的烟雾、花瓣等动画元素 及 位移、形变与模糊等效果,它的每一帧画面约由1000个元素构成。 在三星Note 3(Android 5.0,4核,ARMv7)上运行起来平均只能达到7帧的效果。2. 解码与渲染的过程为了解决上述问题,我们需要对ASS由文本文件到渲染至屏幕的整个过程有基本的认识。这里以Android为例(Ios在渲染的处理上略有不同,而其它是一致的),先看JNI的接口:private native int decodeFrame(long time, int[] pixels);Java层会传入时间戮time及名为pixels的Int数组,time代表当前需要获取哪个时间点的动画效果,libass接着会对与这一时间点有关的每一行文本进行解析,生成一个或多个的小图,从而得到一系列的图片,然后合成到一个大图里面去,最终通过像素拷贝的方式把合成后的结果输出到pixels,回到Java以后,再把pixels设置至Bitmap,最后交给Canvas进行渲染。 3. 过程耗时分析通过对各关键过程的打点并运行前述复杂效果,我们得到了各过程的耗时占比:解析46%、合成37%、输出与渲染各8%,其它1%。分解到每一帧并以毫秒计算则如下表: 接下来,我们将会按解析、合成、输出、渲染这样的顺序来逐步优化。4. 卡顿优化实践1)过滤透明小图前面提到,每一行ass文本都会生成一个或多个的小图,这是因为一个文字会被拆解成文体、边框及背景三个部分,除此之外,libass并不关心这些构成部分的颜色及透明度。这就导致了这样的一个问题:Dialogue: 1,0:00:00.00,0:01:00.00,Default,,0,0,0,fx,{\pos(120,100)\1a&HFF&\blur3}全民K歌以上ass文本所实现的是一个文字镂空效果: 1a&HFF&表示文字主体是完全透明的,而这样的一个透明的元素,libass依然会生成一个小图对它进行各种各样的处理,但这是完全没有必要的,于是我们对libass进行了第一点改造:不再生成无效的透明小图,提高ass解析效率的同时也减少了内存的分配,对后续合成的处理也有正向的影响 2)像素透明度判断在合成的处理中,需要遍历小图的每一个像素并拆分为ARGB4个通道进行颜色的运算dstA = (255 * 255 - (255 - k) * (255 - dstA)) / 255; dstB = (k * b + (255 - k) * dstB) / 255; dstG = (k * g + (255 - k) * dstG) / 255; dstR = (k * r + (255 - k) * dstR) / 255;与普通的图片合成不同,在歌词动效的场景中,小图由文字或点线之类的图形构成,往往存在着大量的透明像素及完全不透明像素,可通过判断来减少这部分的合成运算:if(k == 0){ // 完全透明,跳过 continue;} if(k == 255){ // 完全不透明,直接使用小图颜色 dst = color; continue;}测试了5个在K歌上线的动效,合成时间减少了10%~50%。3)简化计算虽然通过透明度的判断减少了一定计算,但无法完全避免。以Alpha通道的计算为例,包含了2次乘法、1次除法和3次的减法,而除法是特别耗时的。所以,对于这些必要的计算,我们进行了简化,先进行等式变换:dstA = (255 * 255 - (255 - k) * (255 - dstA)) / 255; = (255 - (255 - k) * (255 - dstA) / 255);然后利用255 - x = ~x及x / 255 ≈ x >> 8进行替换,得到简化后的结果:dstA = ~((~k) * (dstA)) >> 8);可见,一次计算变成了1次乘法与4次位运算,测得合成时间减少了26%。4)并行计算经过上述几项优化,合成速度快了许多,但这还不够。在合成的算法中,像素点与像素点间是没有任何联系的,所以可以通过并行计算的方式来提高合成的效率。我们采用了NEON的解决方案,利用CPU专用模块的128位寄存器同时对多个像素点进行计算,因32位色彩中ARGB各占8位,再考虑乘法处理后可能达到的16位,由此,可用128位寄存器同时处理8个像素点的计算,实现约8倍的加速效果,对CPU和帧率可起到明显的作用。 具体实现如下: 5)合成优化前后对比至此,合成的优化告一段落,每一帧的合成耗时由原来的52ms,降到了3ms以内 6)取消像素拷贝输出的过程实际上只是做了一次像素拷贝的操作,把合成后的大图输出到JNI传入的Int数组里面去,除了耗时以后,还会产生额外的一次Native内存分配,于是,我们优化了这个过程,让合成直接在Int数组进行,这样就把原来输出的11ms完全去掉了 前面提到,数据到了Java层,还会调用Bitmap的setPixels方法把像素信息传给Bitmap,最后才交给Canvas进行绘制,而这里的setPixels做的事跟刚刚输出的过程一样,会把像素点全都拷贝一次。所以,我们希望把这一过程的拷贝也给取消掉,但Java并没有提供接口给我们去获取Bitmap的Buffer,也就采用了反射的方案,优化后,渲染耗时降低了65%。 7)双缓冲异步渲染我们知道,卡顿的原因在于处理一帧的耗时太久,达不到我们想要的帧率要求,那很容易会想到,我们是否可以使用多线程同时处理多帧数据呢?结果是失败了,因为libass是单例的模式,同时处理多个时间点的解析合成会导致其内部一些状态的错乱,并以crash告终。虽然解码无法使用多线程,但渲染与libass无关,还是可以拿出来放到一个单独的线程去处理的。这就引入了一个新的问题,解码与渲染两个线程都会操作同一块内存,一边在写、一边在读,数据容易出错。于是,我们多申请了一块内存,一个解码用,一个渲染用,每次解码完成时进行交换,我们的双缓冲异步渲染方案就这样出现了 这一实现让libass不需要等待渲染的完成就可以进行下一帧数据的解码,有效地提高了动效的帧率8)卡顿优化效果汇总经历上述各项优化后,前述复杂动效在低端机Note 3上由原来的7帧达到15帧 2. 内存问题描述在不干预内存的情况下,在一个3分多钟的作品上播放了K歌线上的一个普通效果,期间内存的变化见下图:内存增量达到了180M,且主要是Native层的内存,这是我们面临的一个很严重的问题,有OOM的风险,系统也有可能因此产生频繁的GC而引起卡顿1)深入内存分配通过对libass源码的阅读,我们了解到了更为详细的ASS解析过程 每一行动效文本在libass中被定义一个事件,先是对事件中的动画标签及参数进行解析,得到某一瞬间的所有属性值后创建文字或图形的轮廓;接着是对它进行栅格化的处理,后续还有拼接、模糊等处理,最终生成小图并进行重排,就得到了卡顿问题中所说的一系列小图。 在这样的一个过程中,内存分配主要消耗在栅格化和拼接这2个过程中,且libass内部已经实现了一套完整的缓存管理机制,只是其默认缓存较大,分别为128M和64M,总大小达到了192M,再加上些其它的内存分配,最大会占用超过200M的内存才会趋于平稳。除此之外,libass还提供了接口给我们设置缓存的大小,但只能设置总的缓存大小,不能自定义Bitmap和Composite Bitmap分别是多少,其内部会按2:1进行分配。有了对libass的认识,内存问题也就变成了:如何寻找一个合适的缓存总大小 及 内存的2:1分配是否适合我们的场景。2)寻找合适的缓存总大小统计动效在一次播放的过程中查询缓存的次数M,查询后命中的次数为N,从而得到缓存命中率N/M。下图横轴表示了我们给libass设置的缓存总大小,纵轴则是2类缓存的命中率 通过上面的曲线,我们可以得到2个结论:1. 随着缓存总大小的增加,新增内存所获得的收益逐渐变小,对于K歌的场景,设置4M16M比较合理; 2. Bitmap 与 Composite Bitmap 的分配不合理,可将更多的内存用于Composite Bitmap。2)寻找合适的缓存比例从K歌线上的10几个动效中,随机选取了5个,统计各个动效处理1500帧数据对2类缓存的访求并制成了表格 通过表格的数据可以看到,Composite Bitmap需要更大的缓存,平均约为Bitmap的1.8倍,于是我们把libass内2:1的分配规则调整为了1:1.8,最终使用8M的内存基本上达到了原来16M的效果 3)内存优化效果设置缓存大小后,内存增长得到了控制且处于稳定状态;而调整分配比例提高了缓存命中率,减少了CPU在内存分配与栅格化等处理上的耗时。 小结本文主要介绍了动效歌词开发的关键技术和优化策略。技术方案经历了数次讨论和预研,采用了并行计算大幅减少运算时间,优化了编译策略解决了跨平台问题。在架构设计上,也充分考虑性能,跨平台,可扩展,组件化,复用性等各方面的因素。在该方案的落地实现过程中,团队的John、Harvey、Wing、 Comic,、Jerry、rey等同学通力合作,付出了不懈的努力!此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

February 19, 2019 · 1 min · jiezi

React-flux杂记

简介 Flux是一种搭建WEB客户端的应用架构,更像是一种模式而不是一个框架。 特点 单向数据流 与MVC的比较 1.传统的MVC如下所示(是一个双向数据流模型)用户触发事件View通知Controller执行相关逻辑Controller通知Modal需要数据Modal返回数据给ControllerController再通知View更新2.前端中的MVC 因为前端中视图和事件逻辑通常结合在一起, 即正常情况下是这样的 M <-> VC 然而这样在复杂的页面中容易造成下面的情况,跟踪数据的变化变得很困难3.Flux强制单向流,Model集中成Store, View通过Action, Action通过Dispatch更新Store, Flux 可以认为是MVC的一种改进, 更适合React或者说更适合前端的一种架构模式。

February 12, 2019 · 1 min · jiezi

React-redux基础

前言在学习了React之后, 紧跟着而来的就是Redux了~ 在系统性的学习一个东西的时候, 了解其背景、设计以及解决了什么问题都是非常必要的。接下来记录的是, 我个人在学习Redux时的一些杂七杂八Redux是什么通俗理解https://www.zhihu.com/questio…介绍先从官方的一句介绍看起:Redux is a predictable state container for JavaScript apps. (Redux是Javascript应用程序的可预测状态容器。)当然,假如你在这之前并没有接触过相关的状态管理库或者框架, 看到这句话时是非常的懵逼的, 不过可以带着这句话来一步步探索背景随着Javascript单页面应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。 – Redux文档上面这一大段引用概况起来就是一句话, state(状态)在什么时候什么地方,因为什么而变化成了一个不受控制的过程。(这不能忍,状态如果无法预测以及控制)那么Redux就是试图让state的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。核心概念1.Redux使用普通的对象来描述state,这个对象就是Modal。 2.要想更新 state 中的数据,你需要发起一个 action。Action 就是一个普通 JavaScript 对象用来描述发生了什么。 3.为了把 action 和 state 串起来,开发一些函数,这就是 reducer。reducer 只是一个接收 state 和 action,并返回新的 state 的函数。 三大准则只有一个state树。state是只读的,只能通过action改变。reducer是纯函数,没有副作用。了解到这些后,其实已经多少能明白Redux is a predictable state container for JavaScript apps. (Redux是Javascript应用程序的可预测状态容器。)这句话,为什么是可预测的? 因为只有一个state树,并且它是只读的,而且只能通过action来改变(改变的过程变得清晰可追踪),并且获取state(状态)只能通过reducer,而reducer是一个纯函数(此处了解state是重点),没有副作用,也就意味着我们能知道我们最终得到的state是什么样的。api简介[createStore(reducer, [preloadedState], [enhancer])](https://www.redux.org.cn/docs… 创建store的函数,返回一个对象, 包含getStatedispatchsubscribegetReducerreplaceReducer等方法 combineReducers(reducers) 合并多个reducer applyMiddleware(…middlewares) 中间件处理,在 实际的dispatch前调用一系列中间件, 类似于koa bindActionCreators(actionCreators, dispatch) 绑定action和dispatch compose(…functions) 函数式编程中常见的方法, compose(funcA, funcB, funcC) => compose(funcA(funcB(funcC())))React-redux介绍Redux官方提供的 React 绑定库。 具有高效且灵活的特性。动机React是以组件化的形式开发。为了组件的复用以及代码的清晰,通常我们将组件分为容器组件以及UI组件。关于容器组件和UI组件,推荐阅读该文章,而引入了React-redux可以很好的帮助我们分离容器组件和UI组件。为什么选择react-reduxreact-redux是官方提供的绑定库,由redux开发者维护,可以很好的与redux保持同步。它鼓励组件分离。react-redux协助我们分离容器组件和UI组件,通过提供API连接store(提供数据)和UI组件,并且使得UI组件不需要知道存在Redux(复用)。性能优化。虽然React速度很快,但是re-redering是非常消耗性能的,而react-redux的内部做了许多性能优化。社区支持,因为是官方指定的绑定库,所以拥有大量的使用者,社区活跃度高,问题也容易解决。api简介<Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。 store: 应用程序中唯一的 Redux store 对象 connect(mapStateToProps, mapDispatchToProps, mergeProps, options) mapStateToProps(state, [ownProps]): stateProps: 映射state作为UI组件的props mapDispatchToProps(dispatch, [ownProps]): dispatchProps: 映射dispatch作为UI组件的props mergeProps(stateProps, dispatchProps, ownProps): props: 如果指定这个函数, 即合并mapStateToPropsmapDIspatchToPropsoweProps作为UI组件的props options: 定制 connector 的行为Redux存在的问题与其说缺点,不如说是Redux的优势而造成的不可避免的劣势,问题应该辩证地看纯净。Redux只支持同步,让状态可预测,方便测试。 但不处理异步、副作用的情况,而把这个丢给了其他中间件,诸如redux-thunkredux-promiseredux-saga等等,选择多也容易造成混乱啰嗦。那么写过Redux的人,都知道actionreducer以及你的业务代码非常啰嗦,模板代码非常多。但是~,这也是为了让数据的流动清晰明了。性能。粗暴地、级联式刷新视图(使用react-redux优化)。分型。原生 Redux-react 没有分形结构,中心化 store;Redux的最佳实践vuex(dva)事实上,如果用过vuex或者dva的话, 个人觉得还是会比较偏向于这种用法。比起Redux的啰嗦,dva帮忙简化了很多步骤。具体的实现后续补充这里先补充一点,vuex不是immutable,所以对于时间旅行这种业务不太友好。Redux的实现浅析前言Redux的代码相对比较简单,容易理解, 源码的解读推荐看这篇文章, 本段主要是对代码里一些个人觉得比较有意思的点进行分析createStore在这里看出,redux即使是在内部,也是函数式编程~ 当我们传入了一个enhancer函数(即中间件),会把createStore本身当成参数传给enhancer然后返回一个新的函数来调用 即 fn => fn 暴露出的subscribe函数也是挺有意思的, 首先是isSubscribed这个变量, 其实就是一种非常基础的闭包使用, 然后是每次订阅或者取消订阅的时候,都会在dispatch之前保存一次快照, 然后当前的dispatch用的是上一份快照,而下一个dispatch则是使用当前这一份的快照 compose非常简洁的写出了函数式编程的一个常用函数(…args) => f(g(h(…args))). combineReducer可以看出,每一次action都会重新计算所有的reducer~ 但如果不是非常巨大的state树,并且拆分了很多模块,个人认为其实影响不大 bindActionCreator和applyMiddleware相对容易理解, 这里就不赘述啦 ...

February 12, 2019 · 1 min · jiezi

React-Redux进阶(像VUEX一样使用Redux)

前言Redux是一个非常实用的状态管理库,对于大多数使用React库的开发者来说,Redux都是会接触到的。在使用Redux享受其带来的便利的同时, 我们也深受其问题的困扰。redux的问题之前在另外一篇文章Redux基础中,就有提到以下这些问题纯净。Redux只支持同步,让状态可预测,方便测试。 但不处理异步、副作用的情况,而把这个丢给了其他中间件,诸如redux-thunkredux-promiseredux-saga等等,选择多也容易造成混乱啰嗦。那么写过Redux的人,都知道actionreducer以及你的业务代码非常啰嗦,模板代码非常多。但是,这也是为了让数据的流动清晰明了。性能。粗暴地、级联式刷新视图(使用react-redux优化)。分型。原生 Redux-react 没有分形结构,中心化 store里面除了性能这一块可以利用react-redux进行优化,其他的都是开发者不得不面对的问题,对于代码有洁癖的人,啰嗦这一点确实是无法忍受的。方案目标如果你使用过VUEX的话, 那么对于它的API肯定会相对喜欢很多,当然,vuex不是immutable,所以对于时间旅行这种业务不太友好。不过,我们可以自己实现一个具有vuex的简洁语法和immutable属性的redux-x(瞎命名)。 先看一下我们想要的目标是什么样的? 首先, 我们再./models里面定义每个子state树,里面带有namespace、state、reducers、effects等属性, 如下:export default { // 命名空间 namespace: ‘common’, // 初始化state state: { loading: false, }, // reducers 同步更新 类似于vuex的mutations reducers: { updateLoadingStatus(state, action) { return { …state, loading: action.payload } }, }, // reducers 异步更新 类似于vuex的actions efffects: { someEffect(action, store) { // some effect code … … // 将结果返回 return result } }}通过上面的实现,我们基本解决了Redux本身的一些瑕疵1.在effects中存放的方法用于解决不支持异步、副作用的问题 2.通过合并reducer和action, 将模板代码大大减少 3.具有分型结构(namespace),并且中心化处理如何实现暴露的接口redux-x首先,我们只是在外层封装了一层API方便使用,那么说到底,传给redux的combineReducers还是一个redux对象。另外一个则是要处理副作用的话,那就必须使用到了中间件,所以最后我们暴露出来的函数的返回值应该具有上面两个属性,如下:import reduxSimp from ‘../utils/redux-simp’ // 内部实现import common from ‘./common’ // models文件下common的状态管理import user from ‘./user’ // models文件下user的状态管理import rank from ‘./rank’ // models文件下rank的状态管理const reduxX = reduxSimp({ common, user, rank})export default reduxXconst store = createStore( combineReducers(reduxX.reducers), // reducers树 {}, applyMiddleware(reduxX.effectMiddler) // 处理副作用中间件)第一步, 我们先实现一个暴露出来的函数reduxSimp,通过他对model里面各个属性进行加工,大概的代码如下:const reductionReducer = function() { // somecode }const reductionEffects = function() { // somecode }const effectMiddler = function() { // somecode }/** * @param {Object} models /const simplifyRedux = (models) => { // 初始化一个reducers 最后传给combinReducer的值 也是最终还原的redux const reducers = {} // 遍历传入的model const modelArr = Object.keys(models) modelArr.forEach((key) => { const model = models[key] // 还原effect reductionEffects(model) // 还原reducer,同时通过namespace属性处理命名空间 const reducer = reductionReducer(model) reducers[model.namespace] = reducer }) // 返回一个reducers和一个专门处理副作用的中间件 return { reducers, effectMiddler }}还原effects对于effects, 使用的时候如下(没什么区别):props.dispatch({ type: ‘rank/fundRankingList_fetch’, payload: { fundType: props.fundType, returnType: props.returnType, pageNo: fund.pageNo, pageSize: 20 }})还原effects的思路大概就是先将每一个model下的effect收集起来,同时加上命名空间作为前缀,将副作用的key即type 和相对应的方法value分开存放在两个数组里面,然后定义一个中间件,每当有一个dispatch的时候,检查key数组中是否有符合的key,如果有,则调用对应的value数组里面的方法。// 常量 分别存放副作用的key即type 和相对应的方法const effectsKey = []const effectsMethodArr = [] /* * 还原effects的函数 * @param {Object} model /const reductionEffects = (model) => { const { namespace, effects } = model const effectsArr = Object.keys(effects || {}) effectsArr.forEach((effect) => { // 存放对应effect的type和方法 effectsKey.push(namespace + ‘/’ + effect) effectsMethodArr.push(model.effects[effect]) })}/* * 处理effect的中间件 具体参考redux中间件 * @param {Object} store /const effectMiddler = store => next => (action) => { next(action) // 如果存在对应的effect, 调用其方法 const index = effectsKey.indexOf(action.type) if (index > -1) { return effectsMethodArr[index](action, store) } return action}还原reducersreducers的应用也是和原来没有区别:props.dispatch({ type: ‘common/updateLoadingStatus’, payload: true })代码实现的思路就是最后返回一个函数,也就是我们通常写的redux函数,函数内部遍历对应命名空间的reducer,找到匹配的reducer执行后返回结果/* * 还原reducer的函数 * @param {Object} model 传入的model对象 /const reductionReducer = (model) => { const { namespace, reducers } = model const initState = model.state const reducerArr = Object.keys(reducers || {}) // 该函数即redux函数 return (state = initState, action) => { let result = state reducerArr.forEach((reducer) => { // 返回匹配的action if (action.type === ${namespace}/${reducer}) { result = model.reducers[reducer](state, action) } }) return result }}最终代码最终的代码如下,加上了一些错误判断:// 常量 分别存放副作用的key即type 和相对应的方法const effectsKey = []const effectsMethodArr = []/* * 还原reducer的函数 * @param {Object} model 传入的model对象 /const reductionReducer = (model) => { if (typeof model !== ‘object’) { throw Error(‘Model must be object!’) } const { namespace, reducers } = model if (!namespace || typeof namespace !== ‘string’) { throw Error(The namespace must be a defined and non-empty string! It is ${namespace}) } const initState = model.state const reducerArr = Object.keys(reducers || {}) reducerArr.forEach((reducer) => { if (typeof model.reducers[reducer] !== ‘function’) { throw Error(The reducer must be a function! In ${namespace}) } }) // 该函数即redux函数 return (state = initState, action) => { let result = state reducerArr.forEach((reducer) => { // 返回匹配的action if (action.type === ${namespace}/${reducer}) { result = model.reducers[reducer](state, action) } }) return result }}/* * 还原effects的函数 * @param {Object} model /const reductionEffects = (model) => { const { namespace, effects } = model const effectsArr = Object.keys(effects || {}) effectsArr.forEach((effect) => { if (typeof model.effects[effect] !== ‘function’) { throw Error(The effect must be a function! In ${namespace}) } }) effectsArr.forEach((effect) => { // 存放对应effect的type和方法 effectsKey.push(namespace + ‘/’ + effect) effectsMethodArr.push(model.effects[effect]) })}/* * 处理effect的中间件 具体参考redux中间件 * @param {Object} store /const effectMiddler = store => next => (action) => { next(action) // 如果存在对应的effect, 调用其方法 const index = effectsKey.indexOf(action.type) if (index > -1) { return effectsMethodArr[index](action, store) } return action}/* * @param {Object} models */const simplifyRedux = (models) => { if (typeof models !== ‘object’) { throw Error(‘Models must be object!’) } // 初始化一个reducers 最后传给combinReducer的值 也是最终还原的redux const reducers = {} // 遍历传入的model const modelArr = Object.keys(models) modelArr.forEach((key) => { const model = models[key] // 还原effect reductionEffects(model) // 还原reducer,同时通过namespace属性处理命名空间 const reducer = reductionReducer(model) reducers[model.namespace] = reducer }) // 返回一个reducers和一个专门处理副作用的中间件 return { reducers, effectMiddler }}export default simplifyRedux思考如何结合Immutable.js使用? ...

February 12, 2019 · 3 min · jiezi

React-redux进阶之Immutable.js

Immutable.jsImmutable的优势1. 保证不可变(每次通过Immutable.js操作的对象都会返回一个新的对象) 2. 丰富的API 3. 性能好 (通过字典树对数据结构的共享)<br/>Immutable的问题1. 与原生JS交互不友好 (通过Immutable生成的对象在操作上与原生JS不同,如访问属性,myObj.prop1.prop2.prop3 => myImmutableMap.getIn([‘prop1’, ‘prop2’, ‘prop3’])。另外其他的第三方库可能需要的是一个普通的对象) 2. Immutable的依赖性极强 (一旦在代码中引入使用,很容易传播整个代码库,并且很难在将来的版本中移除) 3. 不能使用解构和对象运算符 (相对来说,代码的可读性差) 4. 不适合经常修改的简单对象 (Immutable的性能比原生慢,如果对象简单,并且经常修改,不适合用) 5. 难以调试 (可以采用 Immutable.js Object Formatter扩展程序协助) 6. 破坏JS原生对象的引用,造成性能低下 (toJs每次都会返回一个新对象)<br/>原生Js遇到的问题原生Js遇到的问题// 场景一var obj = {a:1, b:{c:2}};func(obj);console.log(obj) //输出什么??// 场景二var obj = ={a:1};var obj2 = obj;obj2.a = 2;console.log(obj.a); // 2console.log(obj2.a); // 2代码来源:https://juejin.im/post/5948985ea0bb9f006bed7472// ajax1this.props.a = { data: 1,}// ajax2nextProps.a = { data: 1,}//shouldComponentUpdate()shallowEqual(this.props, nextProps) // false// 数据相同但是因为引用不同而造成不必要的re-rederning由于Js中的对象是引用类型的,所以很多时候我们并不知道我们的对象在哪里被操作了什么,而在Redux中,因为Reducer是一个纯函数,每次返回的都是一个新的对象(重新生成对象占用时间及内存),再加上我们使用了connect这个高阶组件,官方文档中虽然说react-redux做了一些性能优化,但终究起来,react-redux只是对传入的参数进行了一个浅比较来进行re-redering(为什么不能在mapStateToProps中使用toJs的原因)。再进一步,假如我们的state中的属性嵌套了好几层(随着业务的发展),对于原来想要的数据追踪等都变得极为困难,更为重要的是,在这种情况下,我们一些没有必要的组件很可能重复渲染了多次。 <br/>总结起来就是以下几点(问题虽少,但都是比较严重的):1. 无法追踪Js对象 2. 项目复杂时,reducer生成新对象性能低 3. 只做浅比较,有可能会造成re-redering不符合预期(多次渲染或不更新)<br/>为什么不使用深比较或许有人会疑惑,为什么不使用深比较来解决re-redering的问题,答案很简单,因为消耗非常巨大~ 想象一下,如果你的参数复杂且巨大, 对每一个进行比较是多么消耗时间的一件事~ <br/>使用Immutable解决问题项目复杂后, 追踪困难 使用Immutable之后,这个问题自然而然就解决了。所谓的追踪困难,无非就是因为对象是mutable的,我们无法确定它到底何时何处被改变,而Immutable每次都会保留原来的对象,重新生成一个对象,(与redux的纯函数概念一样)。但也要注意写代码时的习惯:// javascriptconst obj = { a: 1 }function (obj) { obj.b = 2 …}// Immutableconst obj = Map({ a : 1 })function (obj) { const obj2 = obj.set({ ‘b’, 2 })}<br/>reducer生成新对象性能差 当项目变得复杂时,每一次action对于生成的新state都会消耗一定的性能,而Immutable.js在这方面的优化就很好。或许你会疑惑为什么生成对象还能优化?请往下看~ 在前面就讲到,Immutable是通过字典树来做==结构共享==的 (图片来自网络) 这张图的意思就是immutable使用先进的tries(字典树)技术实现结构共享来解决性能问题,当我们对一个Immutable对象进行操作的时候,ImmutableJS会只clone该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。<br/>re-rendering不符合预期 其实解决这个问题是我们用Immutable的主要目的,先从浅比较说起 浅比较引起的问题在这之前已经讲过,事实上,即使Immutable之后,connect所做的依然是浅比较,但因为Immutable每次生成的对象引用都不同,哪怕是修改的是很深层的东西,最后比较的结果也是不同的,所以在这里解决了第一个问题,==re-rendering可能不会出现==。 但是, 我们还有第二个问题, ==没必要的re-rendering==,想要解决这个问题,则需要我们再封装一个高阶组件,在这之前需要了解下Immutable的 is API// is() 判断两个immutable对象是否相等immutable.is(imA, imB);这个API有什么不同, ==这个API比较的是值,而不是引用==,So: 只要两个值是一样的,那么结果就是trueconst a = Immutable.fromJS({ a: { data: 1, }, b: { newData: { data: 1 } }})const target1 = a.get(‘a’)const target2 = a.getIn([‘b’, ’newData’])console.log(Immutable.is(target1, target2)) //is比较的依据就是每个值的hashcode// 这个hashcode就相当于每个值的一个ID,不同的值肯定有不同的ID,相同的ID对应着的就是相同的值。也就是说,对于下面的这种情况, 我们可以不用渲染// ajax1this.props.a = { data: 1,}// ajax2nextProps.a = { data: 1,}//shouldComponentUpdate()Immutable.is(this.props, nextProps) // true最后, 我们需要封装一个高阶组件来帮助我们统一处理是否需要re-rendering的情况//baseComponent.js component的基类方法import React from ‘react’;import {is} from ‘immutable’;class BaseComponent extends React.Component { constructor(props, context, updater) { super(props, context, updater); } shouldComponentUpdate(nextProps, nextState) { const thisProps = this.props || {}; const thisState = this.state || {}; nextState = nextState || {}; nextProps = nextProps || {}; if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) { return true; } for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } } for (const key in nextState) { if (!is(thisState[key], nextState[key])) { return true; } } return false; }}export default BaseComponent;代码来源链接:https://juejin.im/post/5948985ea0bb9f006bed7472<br/>使用Immutable需要注意的点使用Immutable需要注意的点1. 不要混合普通的JS对象和Immutable对象 (不要把Imuutable对象作为Js对象的属性,或者反过来) 2. 对整颗Reudx的state树作为Immutable对象 3. 除了展示组件以外,其他地方都应该使用Immutable对象 (提高效率,而展示组件是纯组件,不应该使用) 4. 少用toJS方法 (一个是因为否定了Immutable,另外则是操作非常昂贵) 5. 你的Selector应该永远返回Immutable对象 (即mapStateToProps,因为react-redux中是通过浅比较来决定是否re-redering,而使用toJs的话,每次都会返回一个新对象,即引用不同)<br/>通过高阶组件,将Immutable对象转为普通对象传给展示组件1. 高阶组件返回一个新的组件,该组件接受Immutable参数,并在内部转为普通的JS对象 2. 转为普通对象后, 新组件返回一个入参为普通对象的展示组件import React from ‘react’import { Iterable } from ‘immutable’export const toJS = WrappedComponent => wrappedComponentProps => { const KEY = 0 const VALUE = 1 const propsJS = Object.entries(wrappedComponentProps).reduce( (newProps, wrappedComponentProp) => { newProps[wrappedComponentProp[KEY]] = Iterable.isIterable( wrappedComponentProp[VALUE] ) ? wrappedComponentProp[VALUE].toJS() : wrappedComponentProp[VALUE] return newProps }, {} ) return <WrappedComponent {…propsJS} />}import { connect } from ‘react-redux’import { toJS } from ‘./to-js’import DumbComponent from ‘./dumb.component’const mapStateToProps = state => { return { // obj is an Immutable object in Smart Component, but it’s converted to a plain // JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript // object. Because it’s still an Immutable.JS object here in mapStateToProps, though, // there is no issue with errant re-renderings. obj: getImmutableObjectFromStateTree(state) }}export default connect(mapStateToProps)(toJS(DumbComponent))参考<html>Immutable.js 以及在 react+redux 项目中的实践<br/>Using Immutable.JS with Redux<br/>不变应万变-Immutable优化React<br/>React-Redux分析<br/></html> ...

February 12, 2019 · 2 min · jiezi

React-Router 杂记

三种Router的区别1. HashRouter: 即对应url中的hash值,如xx.com/#/a、xx.com/#/a/b, 服务器对任务url都返回同一个url,具体的路径由浏览器区分,因为浏览器不会发送hash后面的值给服务器。 2. BrowserRouter:如果是BrowseRouter即url变成这样,xx.com/a、xx.com/a/b, 所以要对服务器配置不同的url返回不同的资源。3. MemoryRouter: 就是没有URL的情况,比如(React Native)。react-router的哲学 https://github.com/rccoder/bl...1. 动态路由,每一个route都是一个组件,更好的配合React 2. 路由嵌套react-router和redux问题 有时候,当location改变,组件并没有更新(子路由组件或者activity link),主要是因为:1.组件直接通过redux的connect 2.该组件不是路由组件,也就是没有这样的代码 <Route component={SomeConnectedThing}/>原因是redux内部实现了shouldComponentUpdate,但又没有从react-router接收到props,意味着不会改变。解决办法:// beforeexport default connect(mapStateToProps)(Something)// afterimport { withRouter } from ‘react-router-dom’export default withRouter(connect(mapStateToProps)(Something))

February 12, 2019 · 1 min · jiezi

React-生命周期杂记

前言自从React发布Fiber之后,更新速度日新月异,而生命周期也随之改变,虽然原有的一些生命周期函数面临废弃,但理解其背后更新的机制也是一种学习在这里根据官方文档以及社区上其他优秀的文章进行一个对于生命周期的总结,大致上分为以下三个模块新老生命周期的区别为什么数据获取要在componentDidMount中进行为什么要改变生命周期新老生命周期的区别新的生命周期增加了static getDerivedStateFromProps()以及getSnapshotBeforeUpdate(),废弃了原有的componentWillMount()、componentWillUpdate()以及componentWillReceiveProps(), 分别如以下图 原生命周期: 新生命周期(图引用自React v16.3之后的组件生命周期函数): 为什么数据获取要在componentDidMount中进行作者一开始也喜欢在React的willMount函数中进行异步获取数据(认为这可以减少白屏的时间),后来发现其实应该在didMount中进行。首先,分析一下两者请求数据的区别: componentWillMount获取数据:执行willMount函数,等待数据返回执行render函数执行didMount函数数据返回, 执行renderdidMount获取数据:执行willMount函数执行render函数执行didMount函数, 等待数据返回数据返回, 执行render很明显,在willMount中获取数据,可以节省时间(render函数和didMount函数的执行时间),但是为什么我们还要在didMount中获取数据如果使用服务端渲染的话,willMount会在服务端和客户端各自执行一次,这会导致请求两次(接受不了~),而didMount只会在客户端进行在Fiber之后, 由于任务可中断,willMount可能会被执行多次willMount会被废弃,目前被标记为不安全节省的时间非常少,跟其他的延迟情况相比,这个优化可以使用九牛一毛的形容(为了这么一点时间而一直不跟进技术的发展,得不偿失),并且render函数是肯定比异步数据到达先执行,白屏时间并不能减少关于第一点,如果你想在服务端渲染时先完成数据的展示再一次性给用户,官方的推荐做法是用constructor代替willMount为什么要改变生命周期从上面的生命周期的图中可以看出,被废弃的三个函数都是在render之前,因为fiber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次另外的一个原因则是,React想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,我们可以从新增加以及即将废弃的生命周期分析入手componentWillMoun首先这个函数的功能完全可以使用componentDidMount和constructor来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在constructor中执行,除此之外,如果我们在willMount中订阅事件,但在服务端这并不会执行willUnMount事件,也就是说服务端会导致内存泄漏所以componentWillMount完全可以不使用,但使用者有时候难免因为各种各样的情况(如作者犯浑)在componentWillMount中做一些操作,那么React为了约束开发者,干脆就抛掉了这个APIcomponentWillReceiveProps在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWillReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。 本段引用自React v16.3 版本新生命周期函数浅析及升级方案为了解决这些问题,React引入了第一个新的生命周期static getDerivedStateFromProps可以先看一下两者在使用上的区别: 原有的代码 新的代码 这样看似乎没有什么改变,特别是当我们把this,tabChange也放在didUpdate中执行时(正确做法),完全没有不同,但这也是我们一开始想说的,React通过API来约束开发者写出更好的代码,而新的使用方法有以下的优点getDSFP是静态方法,在这里不能使用this,也就是一个纯函数,开发者不能写出副作用的代码开发者只能通过prevState而不是prevProps来做对比,保证了state和props之间的简单关系以及不需要处理第一次渲染时prevProps为空的情况基于第一点,将状态变化(setState)和昂贵操作(tabChange)区分开,更加便于 render 和 commit 阶段操作或者说优化。componentWillUpdate与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调。但不论是 componentWillReceiveProps 还是 componentWillUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类似,componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中的回调迁移至 componentDidUpdate 就可以解决这个问题。本段引用自React v16.3 版本新生命周期函数浅析及升级方案另外一种情况则是我们需要获取DOM元素状态,但是由于在fiber中,render可打断,可能在willMount中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决getSnapshotBeforeUpdategetSnapshotBeforeUpdate(prevProps, prevState) // 返回的值作为componentDidUpdate的第三个参数与willMount不同的是, getSnapshotBeforeUpdate会在最终确定的render执行之前执行,也就是能保证其获取到的元素状态与didUpdate中获取到的元素状态相同,这里官方提供了一段参考代码: 总结随着React Fiber的落地,许多功能都将开始改变,但本质上是换汤不换药,很多时候都是React为了开发者写出更好的代码而做的改变,当然这也是React的厉害之处,通过框架来约束开发者!

February 12, 2019 · 1 min · jiezi

React-setState杂记

前言在看React的官方文档的时候, 发现了这么一句话,State Updates May Be Asynchronous,于是查询了一波资料, 最后归纳成以下3个问题setState为什么要异步更新,它是怎么做的?setState什么时候会异步更新, 什么时候会同步更新?既然setState需要异步更新, 为什么不让用户可以同步读到state的新值,但更新仍然是异步?常见场景下的异步更新以下是官方文档的一个例子, 调用了3次incrementCount方法, 期望this.state.count的值是3, 但最后却是1incrementCount() { this.setState({count: this.state.count + 1});}handleSomething() { // Let’s say this.state.count starts at 0. this.incrementCount(); this.incrementCount(); this.incrementCount(); // When React re-renders the component, this.state.count will be 1, but you expected 3. // This is because incrementCount() function above reads from this.state.count, // but React doesn’t update this.state.count until the component is re-rendered. // So incrementCount() ends up reading this.state.count as 0 every time, and sets it to 1. // The fix is described below!}那么就可以引出第一个问题setState为什么要异步更新,它是怎么做的?深入源码你会发现:(引用程墨老师的setState何时同步更新状态)在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。然后我在网上引用了这张图(侵删) 从结论和图都可以得出, setState是一个batching的过程, React官方认为, setState会导致re-rederning, 而re-rederning的代价是昂贵的, 所以他们会尽可能的把多次操作合并成一次提交。以下这段话是Dan在Issue中的回答: 中心意思大概就是: 同步更新setState并re-rendering的话在大部分情况下是无益的, 采用batching会有利于性能的提升, 例如当我们在浏览器插入一个点击事件时,父子组件都调用了setState,在batching的情况下, 我们就不需要re-render两次孩子组件,并且在退出事件之前re-render一次即可。 那么如果我们想立即读取state的值, 其实还有一个方法, 如下代码: 因为当传入的是一个函数时,state读取的是pending队列中state的值incrementCount() { this.setState((state) => { // Important: read state instead of this.state when updating. return {count: state.count + 1} });}handleSomething() { // Let’s say this.state.count starts at 0. this.incrementCount(); this.incrementCount(); this.incrementCount(); // If you read this.state.count now, it would still be 0. // But when React re-renders the component, it will be 3.}当然, 仔细看React文档的话, 可以发现, State Updates May Be Asynchronou里面有一个may的字眼,也就是可能是异步更新, 因而引出第二个问题setState什么时候会异步更新, 什么时候会同步更新?其实从第一个问题中我们就知道,React是根据isBatchingUpdates来合并更新的, 那么当调用setState的方法或者函数不是由React控制的话, setState自然就是同步更新了。 简单的举下例子:如componentDidMount等生命周期以及React的事件即为异步更新,这里不显示具体代码。如自定义的浏览器事件,setTimeout,setInterval等脱离React控制的方法, 即为同步更新, 如下(引用程墨老师的setState何时同步更新状态)componentDidMount() { document.querySelector(’#btn-raw’).addEventListener(‘click’, this.onClick);}onClick() { this.setState({count: this.state.count + 1}); console.log(’# this.state’, this.state);}// ……render() { console.log(’#enter render’); return ( <div> <div>{this.state.count} <button id=“btn-raw”>Increment Raw</button> </div> </div> )}有的人也会想能不能React依然合并更新, 但用户可以同步读取this.state的值, 这个问题在React的一个Issue上有提到, 也是我们的第三个问题既然setState需要异步更新, 为什么不让用户可以同步读到state的新值,但更新仍然是异步?这个问题可以直接在Dan的回答中得到:This is because, in the model you proposed, this.state would be flushed immediately but this.props wouldn’t. And we can’t immediately flush this.props without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).大概意思就是说: 如果在应用中,this.state的值是同步,但是this.props却不是同步的。因为props只有当re-rendering父组件后才传给子组件,那么如果要props变成同步的, 就需要放弃batching。 但是batching不能放弃。 ...

February 12, 2019 · 2 min · jiezi

React-事件机制杂记

前提最近通过阅读React官方文档的事件模块,有了一些思考和收获,在这里记录一下~调用方法时需要手动绑定this先从一段官方代码看起: 代码中的注释提到了一句话:This binding is necessary to make this work in the callbackthis的绑定是必须的,其实这一块是比较容易理解的, 因为这并不是React的一个特殊点, 而是Javascript这门语言的特性。 可以看到,调用的是this.handleClick函数,handleClick函数里面又读取到了this属性,但是该函数的调用位置又是在render函数里面,render返回的是一个JSX,最后经过babel编译成调用React.createElement函数,在这之前,我们掌握的是this永远指向的是最后调用它的对象,经过这样的一个转换, 实际上this最后指向的是undeined了, 那么调用handleClick函数自然会报错。当然,如果你不在函数里面使用this的话,通常会没事,但并不建议这么做。 关于this的指向与function的原理,推荐阅读 how functions work in JavaScript 既然知道了是因为this的指向原因而采用绑定的做法,那当然可以用箭头函数来解决了,箭头函数中的this是在定义函数的时候绑定,也就是说this是继承自父执行上下文,如下:这样this也能达到我们的预期效果 合成事件SyntheticEvent先从官方上的一段话看起,他的意思是合成事件是React根据W3C标准定义的,无需担心浏览器之间的差异Here, e is a synthetic event. React defines these synthetic events according to the W3C spec, so you don’t need to worry about cross-browser compatibility样看起来React的合成事件只是兼容浏览器? 答案当然是远远不止啦!在探寻其优点之前,我们先看一下其是怎样的一个机制。React的事件机制其实网上有很多同学都分析过了, 他并没有将事件注册在对应的元素或者组件上面,而是通过委托的方式,将所有的事件都注册到了document对象上,并统一调用一个dispatch回调函数,其流程图如下 我们也可以从一个实际的简单例子看看:我们把回调函数绑定到了button上,但是在事件上却没有看到button元素, 但是却有document,并且可以看到他的回调函数就是dispatchInteractiveEvent 最后触发事件的回调函数时,在原生的DOM会传入一个事件属性event,但是因为React将 所有事件委托给document处理, 那么这个event就和我们想要的不一样,如target指向的是document,于是React就有了自己的一个合成事件,通过一个叫SyntheticEvent的基类来生成所需要的事件属性,并传入回调函数作为方法。说到底,React就是把所有事件委托给document处理, 那么这样做有什么好处:可以统一在组件挂载和卸载时做处理 只需要注册一个事件即可,节省内存开销 可以手动控制事件流程,特别是对state的batch处理(参考React系列的setState)可以统一在组件挂载和卸载时做处理只需要注册一个事件即可,节省内存开销可以手动控制事件流程,特别是对state的batch处理(参考React系列的setState)事件属性会在事件调用后被回收,即不能异步访问老规矩,先上一段代码: 可以看到在setTimeout函数中,访问事件属性是null。这是为啥?其实这也是合成事件的一个优化手段。 React会在事件调用完成后清理掉属性,否则每点击一次就生成一个事件,那么内存的开销会越来越大,具体的代码可以在后面的源码分析中看到: 当然了, React也可以手动设置不回收,如下:If you want to access the event properties in an asynchronous way, you should call event.persist() on the event我们可以通过调用event,persist来设置不回收。事件机制的源码分析注册阶段首先在某一个任务单元fiber调用compeleteWork函数时, React会判断其是否具有事件属性, 如果有则调用ensureListeningTo函数ensureListeningTo函数主要是获取到document对象, 并调用listenTo函数 listerTo函数 主要是通过调用trapBubbledEvent或者trapCapturedEvent将事件放在document事件上监听 trapBubbledEvent主要是监听事件, 但也可以看出, 所有事件最后触发的都是注册在document上的dispatch函数 调用阶段dispatch函数, 主要是获取实际触发的元素以及对应的fiber, 最后调用batchedUpdates函数, batchedUpdates函数里面的逻辑主要是关于setState的,这里主要是看事件机制, 只要知道最后调用的是handleTopLevel(bookkeeping)就好 handleTopLevel函数主要是拿到需要触发事件的相关fiber, 并调用runExtractedEventsInBatch函数 extractEvents函数是一个生成React事件的函数,React事件是通过继承一个通用类SyntheticEvent生成的,如一个鼠标事件的生成 React事件内部做了优化, 只要生成过SyntheticMouseEvent类, 就会再释放事件的时候将这个类存储起来,在下一个事件触发时可以直接使用 React生成事件后, 会调用accumulateTwoPhaseDispatches(event)函数,该函数一直追溯下去, 最后会调用traverseTwoPhase函数,traverseTwoPhase函数主要是获取祖先组件的fiber, 并进行捕获和冒泡的阶段处理 accumulateDirectionalDispatches函数相对简单, 就是把fiber上对应的事件函数赋值给evnet的_dispatchListeners属性 React事件获取完成后, 回到runExtractedEventsInBatch函数继续调用runEventsInBatch(events, false); 函数的中间作了一系列的处理, 但最后执行的是executeDispatchesAndRelease函数executeDispatchesAndRelease函数会在执行完事件后判断用户是否有设置不销毁事件, 如果没有, 则销毁事件并保存事件类, 一个事件类实例一次并重复使用, 这也是为什么官方提到事件属性只能在当前循环中读到 继续往下走, 最后执行的函数是invokeGuardedCallbackDev, 该函数通过注册一个自定义的元素<react>和自定义的事件, 并触发它来达到执行回调函数的功能 流程总结通过Fiber中的属性, 将事件统一委托 注册到document上,并为document注册相应的事件回调函数 dispatch函数。先获取实际触发元素对应的fiber.生成相应的React事件属性event,将对应的回调函数赋值给event._dispatchListeners, 将fiber赋值给event._dispatchInstances通过fiber向上遍历, 找到所有的祖先fiber, 并按原生事件的机制先捕获后冒泡的执行事件注册一个react节点, 为其注册一个监听事件并触发来执行事件回调函数最后,根据用户的设置, 决定是否释放事件。 ...

February 12, 2019 · 1 min · jiezi

原生 js 实现一个前端路由 router

效果图:项目地址:https://github.com/biaochenxuying/route效果体验地址:1. 滑动效果: https://biaochenxuying.github.io/route/index.html2. 淡入淡出效果: https://biaochenxuying.github.io/route/index2.html1. 需求因为我司的 H 5 的项目是用原生 js 写的,要用到路由,但是现在好用的路由都是和某些框架绑定在一起的,比如 vue-router ,framework7 的路由;但是又没必要为了一个路由功能而加入一套框架,现在自己写一个轻量级的路由。2. 实现原理现在前端的路由实现一般有两种,一种是 Hash 路由,另外一种是 History 路由。2.1 History 路由History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。属性History.length 是一个只读属性,返回当前 session 中的 history 个数,包含当前页面在内。举个例子,对于新开一个 tab 加载的页面当前属性返回值 1 。History.state 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态而的方式。方法History.back()前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).Note: 当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。History.forward()在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1).Note: 当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。History.go(n)通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为 -1的时候为上一页,参数为 1 的时候为下一页. 当整数参数超出界限时 ( 译者注:原文为 When integerDelta is out of bounds ),例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为 -1,那么这个方法没有任何效果也不会报错。调用没有参数的 go() 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为 url 参数的 IE 有点不同)。history.pushState() 和 history.replaceState()这两个 API 都接收三个参数,分别是a. 状态对象(state object) — 一个JavaScript对象,与用 pushState() 方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate 事件都会被触发,并且事件对象的state 属性都包含历史记录条目的状态对象的拷贝。b. 标题(title) — FireFox 浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。c. 地址(URL) — 新的历史记录条目的地址。浏览器不会在调用 pushState() 方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的 URL 不一定是绝对路径;如果是相对路径,它将以当前 URL 为基准;传入的 URL 与当前 URL 应该是同源的,否则,pushState() 会抛出异常。该参数是可选的;不指定的话则为文档当前 URL。相同之处: 是两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。不同之处在于: pushState 会增加一条新的历史记录,而 replaceState 则会替换当前的历史记录。例子:本来的路由 http://biaochenxuying.cn/执行:window.history.pushState(null, null, “http://biaochenxuying.cn/home");路由变成了: http://biaochenxuying.cn/hot详情介绍请看:MDN2.2 Hash 路由我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,但是路由里的 # 不叫锚点,我们称之为 hash。现在的前端主流框架的路由实现方式都会采用 Hash 路由,本项目采用的也是。当 hash 值发生改变的时候,我们可以通过 hashchange 事件监听到,从而在回调函数里面触发某些方法。3. 代码实现3.1 简单版 - 单页面路由先看个简单版的 原生 js 模拟 Vue 路由切换。原理监听 hashchange ,hash 改变的时候,根据当前的 hash 匹配相应的 html 内容,然后用 innerHTML 把 html 内容放进 router-view 里面。这个代码是网上的:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no”> <meta name=“author” content=”"> <title>原生模拟 Vue 路由切换</title> <style type=“text/css”> .router_box, #router-view { max-width: 1000px; margin: 50px auto; padding: 0 20px; } .router_box>a { padding: 0 10px; color: #42b983; } </style></head><body> <div class=“router_box”> <a href="/home" class=“router”>主页</a> <a href="/news" class=“router”>新闻</a> <a href="/team" class=“router”>团队</a> <a href="/about" class=“router”>关于</a> </div> <div id=“router-view”></div> <script type=“text/javascript”> function Vue(parameters) { let vue = {}; vue.routes = parameters.routes || []; vue.init = function() { document.querySelectorAll(".router").forEach((item, index) => { item.addEventListener(“click”, function(e) { let event = e || window.event; event.preventDefault(); window.location.hash = this.getAttribute(“href”); }, false); }); window.addEventListener(“hashchange”, () => { vue.routerChange(); }); vue.routerChange(); }; vue.routerChange = () => { let nowHash = window.location.hash; let index = vue.routes.findIndex((item, index) => { return nowHash == (’#’ + item.path); }); if (index >= 0) { document.querySelector("#router-view").innerHTML = vue.routes[index].component; } else { let defaultIndex = vue.routes.findIndex((item, index) => { return item.path == ‘’; }); if (defaultIndex >= 0) { window.location.hash = vue.routes[defaultIndex].redirect; } } }; vue.init(); } new Vue({ routes: [{ path: ‘/home’, component: “<h1>主页</h1><a href=‘https://github.com/biaochenxuying’>https://github.com/biaochenxuying</a>” }, { path: ‘/news’, component: “<h1>新闻</h1><a href=‘http://biaochenxuying.cn/main.html’>http://biaochenxuying.cn/main.html</a>” }, { path: ‘/team’, component: ‘<h1>团队</h1><h4>全栈修炼</h4>’ }, { path: ‘/about’, component: ‘<h1>关于</h1><h4>关注公众号:BiaoChenXuYing</h4><p>分享 WEB 全栈开发等相关的技术文章,热点资源,全栈程序员的成长之路。</p>’ }, { path: ‘’, redirect: ‘/home’ }] }); </script></body></html>3.2 复杂版 - 内联页面版,带缓存功能首先前端用 js 实现路由的缓存功能是很难的,但像 vue-router 那种还好,因为有 vue 框架和虚拟 dom 的技术,可以保存当前页面的数据。要做缓存功能,首先要知道浏览器的 前进、刷新、回退 这三个操作。但是浏览器中主要有这几个限制:没有提供监听前进后退的事件不允许开发者读取浏览记录用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url所以要自定义路由,解决方案是自己维护一份路由历史的记录,存在一个数组里面,从而区分 前进、刷新、回退。url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A),所以给每个路由添加一个 key 值来区分相同路由的不同实例。这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。3.2.1 route.js3.2.1.1 跳转方法 linkTo像 vue-router 那样,提供了一个 router-link 组件来导航,而我这个框架也提供了一个 linkTo 的方法。 // 生成不同的 key function genKey() { var t = ‘xxxxxxxx’ return t.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0 var v = c === ‘x’ ? r : (r & 0x3 | 0x8) return v.toString(16) }) } // 初始化跳转方法 window.linkTo = function(path) { if (path.indexOf("?") !== -1) { window.location.hash = path + ‘&key=’ + genKey() } else { window.location.hash = path + ‘?key=’ + genKey() } }用法://1. 直接用 a 标签<a href=’#/list’ >列表1</a>//2. 标签加 js 调用方法<div onclick=‘linkTo("#/home")’>首页</div>// 3. js 调用触发linkTo("#/list")3.2.1.2 构造函数 Router定义好要用到的变量function Router() { this.routes = {}; //保存注册的所有路由 this.beforeFun = null; //切换前 this.afterFun = null; // 切换后 this.routerViewId = “#routerView”; // 路由挂载点 this.redirectRoute = null; // 路由重定向的 hash this.stackPages = true; // 多级页面缓存 this.routerMap = []; // 路由遍历 this.historyFlag = ’’ // 路由状态,前进,回退,刷新 this.history = []; // 路由历史 this.animationName = “slide” // 页面切换时的动画 }3.2.1.3 实现路由功能包括:初始化、注册路由、历史记录、切换页面、切换页面的动画、切换之前的钩子、切换之后的钩子、滚动位置的处理,缓存。Router.prototype = { init: function(config) { var self = this; this.routerMap = config ? config.routes : this.routerMap this.routerViewId = config ? config.routerViewId : this.routerViewId this.stackPages = config ? config.stackPages : this.stackPages var name = document.querySelector(’#routerView’).getAttribute(‘data-animationName’) if (name) { this.animationName = name } this.animationName = config ? config.animationName : this.animationName if (!this.routerMap.length) { var selector = this.routerViewId + " .page" var pages = document.querySelectorAll(selector) for (var i = 0; i < pages.length; i++) { var page = pages[i]; var hash = page.getAttribute(‘data-hash’) var name = hash.substr(1) var item = { path: hash, name: name, callback: util.closure(name) } this.routerMap.push(item) } } this.map() // 初始化跳转方法 window.linkTo = function(path) { console.log(‘path :’, path) if (path.indexOf("?") !== -1) { window.location.hash = path + ‘&key=’ + util.genKey() } else { window.location.hash = path + ‘?key=’ + util.genKey() } } //页面首次加载 匹配路由 window.addEventListener(’load’, function(event) { // console.log(’load’, event); self.historyChange(event) }, false) //路由切换 window.addEventListener(‘hashchange’, function(event) { // console.log(‘hashchange’, event); self.historyChange(event) }, false) }, // 路由历史纪录变化 historyChange: function(event) { var currentHash = util.getParamsUrl(); var nameStr = “router-” + (this.routerViewId) + “-history” this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : [] var back = false, refresh = false, forward = false, index = 0, len = this.history.length; for (var i = 0; i < len; i++) { var h = this.history[i]; if (h.hash === currentHash.path && h.key === currentHash.query.key) { index = i if (i === len - 1) { refresh = true } else { back = true } break; } else { forward = true } } if (back) { this.historyFlag = ‘back’ this.history.length = index + 1 } else if (refresh) { this.historyFlag = ‘refresh’ } else { this.historyFlag = ‘forward’ var item = { key: currentHash.query.key, hash: currentHash.path, query: currentHash.query } this.history.push(item) } console.log(‘historyFlag :’, this.historyFlag) // console.log(‘history :’, this.history) if (!this.stackPages) { this.historyFlag = ‘forward’ } window.sessionStorage[nameStr] = JSON.stringify(this.history) this.urlChange() }, // 切换页面 changeView: function(currentHash) { var pages = document.getElementsByClassName(‘page’) var previousPage = document.getElementsByClassName(‘current’)[0] var currentPage = null var currHash = null for (var i = 0; i < pages.length; i++) { var page = pages[i]; var hash = page.getAttribute(‘data-hash’) page.setAttribute(‘class’, “page”) if (hash === currentHash.path) { currHash = hash currentPage = page } } var enterName = ’enter-’ + this.animationName var leaveName = ’leave-’ + this.animationName if (this.historyFlag === ‘back’) { util.addClass(currentPage, ‘current’) if (previousPage) { util.addClass(previousPage, leaveName) } setTimeout(function() { if (previousPage) { util.removeClass(previousPage, leaveName) } }, 250); } else if (this.historyFlag === ‘forward’ || this.historyFlag === ‘refresh’) { if (previousPage) { util.addClass(previousPage, “current”) } util.addClass(currentPage, enterName) setTimeout(function() { if (previousPage) { util.removeClass(previousPage, “current”) } util.removeClass(currentPage, enterName) util.addClass(currentPage, ‘current’) }, 350); // 前进和刷新都执行回调 与 初始滚动位置为 0 currentPage.scrollTop = 0 this.routes[currHash].callback ? this.routes[currHash].callback(currentHash) : null } this.afterFun ? this.afterFun(currentHash) : null }, //路由处理 urlChange: function() { var currentHash = util.getParamsUrl(); if (this.routes[currentHash.path]) { var self = this; if (this.beforeFun) { this.beforeFun({ to: { path: currentHash.path, query: currentHash.query }, next: function() { self.changeView(currentHash) } }) } else { this.changeView(currentHash) } } else { //不存在的地址,重定向到默认页面 location.hash = this.redirectRoute } }, //路由注册 map: function() { for (var i = 0; i < this.routerMap.length; i++) { var route = this.routerMap[i] if (route.name === “redirect”) { this.redirectRoute = route.path } else { this.redirectRoute = this.routerMap[0].path } var newPath = route.path var path = newPath.replace(/\s*/g, “”); //过滤空格 this.routes[path] = { callback: route.callback, //回调 } } }, //切换之前的钩子 beforeEach: function(callback) { if (Object.prototype.toString.call(callback) === ‘[object Function]’) { this.beforeFun = callback; } else { console.trace(‘路由切换前钩子函数不正确’) } }, //切换成功之后的钩子 afterEach: function(callback) { if (Object.prototype.toString.call(callback) === ‘[object Function]’) { this.afterFun = callback; } else { console.trace(‘路由切换后回调函数不正确’) } } }3.2.1.4 注册到 Router 到 window 全局 window.Router = Router; window.router = new Router();完整代码:https://github.com/biaochenxu…3.2.2 使用方法3.2.2.1 js 定义法callback 是切换页面后,执行的回调<script type=“text/javascript”> var config = { routerViewId: ‘routerView’, // 路由切换的挂载点 id stackPages: true, // 多级页面缓存 animationName: “slide”, // 切换页面时的动画 routes: [{ path: “/home”, name: “home”, callback: function(route) { console.log(‘home:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>首页</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/list")’>列表</a></div><div class=‘height’>内容占位</div>” document.querySelector("#home").innerHTML = str } }, { path: “/list”, name: “list”, callback: function(route) { console.log(’list:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>列表</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/detail")’>详情</a></div>” document.querySelector("#list").innerHTML = str } }, { path: “/detail”, name: “detail”, callback: function(route) { console.log(‘detail:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>详情</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/detail2")’>详情 2</a></div><div class=‘height’>内容占位</div>” document.querySelector("#detail").innerHTML = str } }, { path: “/detail2”, name: “detail2”, callback: function(route) { console.log(‘detail2:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>详情 2</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/home")’>首页</a></div>” document.querySelector("#detail2").innerHTML = str } }] } //初始化路由 router.init(config) router.beforeEach(function(transition) { console.log(‘切换之 前 dosomething’, transition) setTimeout(function() { //模拟切换之前延迟,比如说做个异步登录信息验证 transition.next() }, 100) }) router.afterEach(function(transition) { console.log(“切换之 后 dosomething”, transition) }) </script>3.2.2.2 html 加 <script> 定义法id=“routerView” :路由切换时,页面的视图窗口data-animationName=“slide”:切换时的动画,目前有 slide 和 fade。class=“page”: 切换的页面data-hash="/home":home 是切换路由时执行的回调方法window.home : 回调方法,名字要与 data-hash 的名字相同<div id=“routerView” data-animationName=“slide”> <div class=“page” data-hash="/home"> <div class=“page-content”> <div id=“home”></div> <script type=“text/javascript”> window.home = function(route) { console.log(‘home:’, route) // var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>首页</h2> <input type=‘text’> <div><a href=’#/list’ >列表1</div></div><div class=‘height’>内容占位</div>” var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>首页</h2> <input type=‘text’> <div><div href=‘javascript:void(0);’ onclick=‘linkTo("#/list")’>列表</div></div><div class=‘height’>内容占位</div>” document.querySelector("#home").innerHTML = str } </script> </div> </div> <div class=“page” data-hash="/list"> <div class=“page-content”> <div id=“list”></div> <div style=“height: 700px;border: solid 1px red;background-color: #eee;margin-top: 20px;">内容占位</div> <script type=“text/javascript”> window.list = function(route) { console.log(’list:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>列表</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/detail")’>详情</a></div>” document.querySelector("#list”).innerHTML = str } </script> </div> </div> <div class=“page” data-hash="/detail"> <div class=“page-content”> <div id=“detail”></div> <script type=“text/javascript”> window.detail = function(route) { console.log(‘detail:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>详情</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/detail2")’>详情 2</a></div><div class=‘height’>内容占位</div>” document.querySelector("#detail").innerHTML = str } </script> </div> </div> <div class=“page” data-hash="/detail2"> <div class=“page-content”> <div id=“detail2”></div> <div style=“height: 700px;border: solid 1px red;background-color: pink;margin-top: 20px;">内容占位</div> <script type=“text/javascript”> window.detail2 = function(route) { console.log(‘detail2:’, route) var str = “<div><a class=‘back’ onclick=‘window.history.go(-1)’>返回</a></div> <h2>详情 2</h2> <input type=‘text’> <div><a href=‘javascript:void(0);’ onclick=‘linkTo("#/home")’>首页</a></div>” document.querySelector("#detail2”).innerHTML = str } </script> </div> </div> </div> <script type=“text/javascript” src="./js/route.js"></script> <script type=“text/javascript”> router.init() router.beforeEach(function(transition) { console.log(‘切换之 前 dosomething’, transition) setTimeout(function() { //模拟切换之前延迟,比如说做个异步登录信息验证 transition.next() }, 100) }) router.afterEach(function(transition) { console.log(“切换之 后 dosomething”, transition) }) </script>参考项目:https://github.com/kliuj/spa-…5. 最后项目地址:https://github.com/biaochenxuying/route博客常更地址1 :https://github.com/biaochenxuying/blog博客常更地址2 :http://biaochenxuying.cn/main.html足足一个多月没有更新文章了,因为项目太紧,加班加班啊,趁着在家有空,赶紧写下这篇干货,免得忘记了,希望对大家有所帮助。如果您觉得这篇文章不错或者对你有所帮助,请点个赞,谢谢。微信公众号:BiaoChenXuYing分享 前端、后端开发等相关的技术文章,热点资源,随想随感,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript ...

January 29, 2019 · 8 min · jiezi

webpack引入第三方库的方式,以及注意事项

一般情况下,我们不用担心所使用的第三方库,在npm管理仓库中找不到。如果需要某一个库,如:jquery,可以直接运行npm install jquery脚本命令来安装这个项目所需要的依赖;然后,在使用jquery的模块文件中,通过import $ from ‘jquery’或者var $ = require(‘jquery’)来引入。这是常用的在webpack构建的项目中引入第三方库的方式。注:为了更好的演示示例代码,示例是在nodemon这篇文章的基础上操作的。但是,在不同的场景下,对webpack构建的项目有不同的需求:项目的体积足够小(cdn)如果是webapck的处理方式,可参考webapck——实现构建输出最小这篇文章。使用非webapck的处理方式,如:CDN。操作也很简单,如果使用html-webpack-plugin直接在模板文件template/index.html中引入某个cdn(如:boot CDN)上的某个第三方库(如:jquery),参考代码如下:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>third party</title></head><body> <script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script></body></html>然后,在module.js中使用jquery即可,参考代码如下:require(’./module.css’);module.exports = function() { $(document.body).append(’<h1>hello webpack</h1>’) }最后,运行npm run test,构建结束后,你会在浏览器中看到hello webpack字样,背景是红色的页面效果。全局环境下使用第三方库(provide-plugin or imports-loader)为了避免每次使用第三方库,都需要用import或者require()引用它们,可以将它们定义为全局的变量。而webpack的ProvidePlugin内置的插件,可以解决这个问题。详情可参考ProvidePlugin这篇文章的介绍。避免于cdn引用的jquery冲突,这里使用lodash。首先,安装lodash依赖,命令如下:yarn add lodash –dev然后,在webpack.config.js中,添加如下代码:new webpack.ProvidePlugin({ _: ’lodash’}),其次,在module.js中添加如下代码:…var arr = [1, 2, 3, 4, 5 ,6];// provide-plugin$(document.body).append(’<h1>’ + .concat(arr, ‘’) + ‘</h1’);…最后,运行npm run test脚本命令,构建完成后,你就可以浏览器的页面中增加了1,2,3,4,5,6,。如果,你想指定lodash的某个工具函数可以全局使用,如:.concat,首先,像下面这样修改webapck.config.js,代码如下:…new webpack.ProvidePlugin({ // _: ’lodash’, concat: [’lodash’, ‘concat’]}),…然后,修改module.js,代码如下:…var arr = [1, 2, 3, 4, 5 ,6];// provide-plugin// $(document.body).append(’<h1>’ + .concat(arr, ‘’) + ‘</h1’);$(document.body).append(’<h1>’ + _concat(arr, ‘’) + ‘</h1’);…如果不喜欢用插件的,也可以考虑使用import-loader,它也可以实现相同的目的。为了避免不必要的干扰,可以使用underscore来演示。首先,安装imports-loader依赖,命令如下:yarn add imports-loader –dev然后,安装underscore依赖,命令如下:yarn add underscore其次,在webapck.config.js中添加如下代码:…module: { rules: [ { test: require.resolve(‘underscore’), use: ‘imports-loader?=underscore’ }, … ]},…注:underscore和lodash都是用的是单例的模式开发的,它们实例化的构造函数的名字都是,为了作区分,需要对其中一个做一下改变。imports-loader对这个标识起别名有点儿困难,而provide-plugin则没有这个问题,可以定一个个性化的别名。修改webpack.config.js,代码如下:new webpack.ProvidePlugin({ // _: ’lodash’, // _concat: [’lodash’, ‘concat’], __: ’lodash’}),可以为lodash定义为__与underscore的_作区分。然后,修改module.js,代码如下:…// provide-plugin// $(document.body).append(’<h1>’ + _.concat(arr, ‘’) + ‘</h1’);// $(document.body).append(’<h1>’ + _concat(arr, ‘’) + ‘</h1’);$(document.body).append(’<h1>’ + __.concat(arr, ‘~’) + ‘</h1’);…最后,保存所有的文件,可以下浏览器中看到相似的结果(保存后,nodemon自启动浏览器)。cdn与externals之前遇到了一些externals的问题,为什么要详细的说,是因为很多人不明白它到底用来干什么的。场景再现:之前,有一个项目使用了jquery,由于这个库的比较经典,它在应用的各个模块中被频繁引用。使用的方式如下:import $ from ‘jquery’或者var $ = require(‘jquery’)结果是构建结束后,文件比较大。那么考虑使用cdn,如上文描述的那样。这样需要删除import或require的引用,同时删除安装的jquery依赖,但是由于项目结构比较乱,模块比较多,为了避免造成少改或者漏改的问题,会造成应用出错。该怎么办呢?有的人说,不删除jquery依赖,那么使用cdn的目的就没有意义了。而使用external则可以解决这个问题。可以在module.js文件中添加如下代码:…var $ = require(‘jquery’)…然后,保存文件,发现构建输出提示如下的错误:ERROR in ./module.jsModule not found: Error: Can’t resolve ‘jquery’ in ‘E:\workspace\me\webpack-play\demo\example-1’ @ ./module.js 3:0-23 @ ./main.js @ multi (webpack)-dev-server/client?http://localhost:8080 ./main.js模块module.js中的jquery不能被解析。紧接着,在webpack.config.js中添加如下代码:externals: { jquery: ‘jQuery’, jquery: ‘$’},其中jquery代表的是require(‘jquery’)中的jquery,而jQuery和$代表的是jquery这个库自身提供的可是实例化的标识符。其它的库的cdn化,修改类似jquery。但是,如果在项目一开始就决定用cdn的话,就不要在使用jquery的模块中,使用var $ = require(‘jquery’) 或 import $ from ‘jquery’;,虽然这样做不会报错,但是如果出于某方面的考虑,后期可能会引入jquery依赖,那么就必须使用var $ = require(‘jquery’) 或 import $ from ‘jquery’;。参考源代码 ...

January 14, 2019 · 1 min · jiezi

【开发必看】你真的了解回流和重绘吗?

本文由云+社区发表回流和重绘可以说是每一个web开发者都经常听到的两个词语,可是可能有很多人不是很清楚这两步具体做了什么事情。最近有空对其进行了一些研究,看了一些博客和书籍,整理了一些内容并且结合一些例子,写了这篇文章,希望可以帮助到大家。浏览器的渲染过程本文先从浏览器的渲染过程来从头到尾的讲解一下回流重绘,如果大家想直接看如何减少回流和重绘,优化性能,可以跳到后面。(这个渲染过程来自MDN)浏览器渲染过程添加描述从上面这个图上,我们可以看到,浏览器渲染过程如下:解析HTML,生成DOM树,解析CSS,生成CSSOM树将DOM树和CSSOM树结合,生成渲染树(Render Tree)Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会会写一篇博客)渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。生成渲染树渲染树构建为了构建渲染树,浏览器主要完成了以下工作:从DOM树的根节点开始遍历每个可见节点。对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。根据每个可见节点以及其对应的样式,组合生成渲染树。第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:一些不会渲染输出的节点,比如script、meta、link等。一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。从上面的例子来讲,我们可以看到span标签的样式有一个display:none,因此,它最终并没有在渲染树上。注意:渲染树只包含可见的节点回流前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:<!DOCTYPE html><html> <head> <meta name=“viewport” content=“width=device-width,initial-scale=1”> <title>Critial Path: Hello world!</title> </head> <body> <div style=“width: 50%"> <div style=“width: 50%">Hello world!</div> </div> </body></html>我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)回流重绘最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。何时发生回流重绘我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:添加或删除可见的DOM元素元素的位置发生变化元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。页面一开始渲染的时候(这肯定避免不了)浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)注意:回流一定会触发重绘,而重绘不一定会回流根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。浏览器的优化机制现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:offsetTop、offsetLeft、offsetWidth、offsetHeightscrollTop、scrollLeft、scrollWidth、scrollHeightclientTop、clientLeft、clientWidth、clientHeightgetComputedStyle()getBoundingClientRect具体可以访问这个网站:https://gist.github.com/pauli…以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。减少回流和重绘好了,到了我们今天的重头戏,前面说了这么多背景和理论知识,接下来让我们谈谈如何减少回流和重绘。最小化重绘和重排由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。考虑这个例子const el = document.getElementById(’test’);el.style.padding = ‘5px’;el.style.borderLeft = ‘1px’;el.style.borderRight = ‘2px’;例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:使用cssTextconst el = document.getElementById(’test’); el.style.cssText += ‘border-left: 1px; border-right: 2px; padding: 5px;’;修改CSS的classconst el = document.getElementById(’test’);el.className += ’ active’;批量修改DOM当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:使元素脱离文档流对其进行多次修改将元素带回到文档中。该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流重绘,因为它已经不在渲染树了。有三种方式可以让DOM脱离文档流:隐藏元素,应用修改,重新显示使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。考虑我们要执行一段批量插入节点的代码:function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement(’li’); li.textContent = ’text’; appendToElement.appendChild(li); }}const ul = document.getElementById(’list’);appendDataToElement(ul, data);如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。我们可以使用这三种方式进行优化:隐藏元素,应用修改,重新显示这个会在展示和隐藏节点的时候,产生两次回流function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement(’li’); li.textContent = ’text’; appendToElement.appendChild(li); }}const ul = document.getElementById(’list’);ul.style.display = ’none’;appendDataToElement(ul, data);ul.style.display = ‘block’;使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档const ul = document.getElementById(’list’);const fragment = document.createDocumentFragment();appendDataToElement(fragment, data);ul.appendChild(fragment);将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。const ul = document.getElementById(’list’);const clone = ul.cloneNode(true);appendDataToElement(clone, data);ul.parentNode.replaceChild(clone, ul);对于上面这三种情况,我写了一个demo在safari和chrome上测试修改前和修改后的性能。然而实验结果不是很理想。原因:原因其实上面也说过了,现代浏览器会使用队列来储存多次修改,进行优化,所以对这个优化方案,我们其实不用优先考虑。避免触发同步布局事件上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:function initP() { for (let i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + ‘px’; }}这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:const width = box.offsetWidth;function initP() { for (let i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = width + ‘px’; }}同样,我也写了个demo来比较两者的性能差异。你可以自己点开这个demo体验下。这个对比的性能差距就比较明显。对于复杂动画效果,使用绝对定位让其脱离文档流对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。这个我们就直接上个例子。打开这个例子后,我们可以打开控制台,控制台上会输出当前的帧数(虽然不准)。添加描述从上图中,我们可以看到,帧数一直都没到60。这个时候,只要我们点击一下那个按钮,把这个元素设置为绝对定位,帧数就可以稳定60。css3硬件加速(GPU加速)比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!划重点:1. 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。2. 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。本篇文章只讨论如何使用,暂不考虑其原理,之后有空会另外开篇文章说明。如何使用常见的触发硬件加速的css属性:transformopacityfiltersWill-change效果我们可以先看个例子。我通过使用chrome的Performance捕获了动画一段时间里的回流重绘情况,实际结果如下图:添加描述从图中我们可以看出,在动画进行的时候,没有发生任何的回流重绘。如果感兴趣你也可以自己做下实验。重点使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。css3硬件加速的坑当然,任何美好的东西都是会有对应的代价的,过犹不及。css3硬件加速还是有坑的:如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。总结本文主要讲了浏览器的渲染过程、浏览器的优化机制以及如何减少甚至避免回流和重绘,希望可以帮助大家更好的理解回流重绘。参考文献渲染树构建、布局及绘制高性能Javascript此文已由作者授权腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

January 14, 2019 · 1 min · jiezi

「译」setState如何知道它该做什么?

本文翻译自:How Does setState Know What to Do?原作者:Dan Abramov如果有任何版权问题,请联系shuirong1997@icloud.com当你在组件中调用setState时,你觉得会发生什么?import React from ‘react’;import ReactDOM from ‘react-dom’;class Button extends React.Component { constructor(props) { super(props); this.state = { clicked: false }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState({ clicked: true }); } render() { if (this.state.clicked) { return <h1>Thanks</h1>; } return ( <button onClick={this.handleClick}> Click me! </button> ); }}ReactDOM.render(<Button />, document.getElementById(‘container’));当然,React会用{ clicked: true} 这条状态重新渲染组件并且更新匹配到的DOM,然后返回<h1>Thanks</h1>元素。听起来似乎简洁明了。但别急,React(或者说React DOM)是怎么做的?更新DOM听起来像是React DOM的事儿,但别忘了我们调用的可是this.setState(),它是React的东西,可不是React DOM的。另外,我们的基类React.Component是被定义在React内部。所以问题来了:React.Component内部的setState怎么能去更新DOM呢?事先声明:就像我的其他博客,你不需要熟练掌握React。这篇博客是为那些想要看看面纱之后是什么东西的人准备的。完全可选!我们或许会认为React.Component类已经包含了DOM更新逻辑。但如果这是事实,那this.setState是如何工作在其他环境中呢?比如:在React Native App中的组件也能继承React.Component,他们也能像上面一样调用this.setState(),并且React Native工作在Android和iOS的原生视图而不是DOM中。你可能也对React Test Renderer 或 Shallow Renderer比较熟悉。这两个测试渲染器让你可以渲染一般的组件并且也能在他们中调用this.setState,但他们可都不使用DOM。如果你之前使用过一些渲染器比如说React ART,你可能知道在页面中使用超过一个渲染器是没什么问题的。(比如:ART组件工作在React DOM 树的内部。)这会产生一个不可维持的全局标志或变量。所以React.Component以某种方式将state的更新委托为具体的平台(译者注:比如Android, iOS),在我们理解这是如何发生之前,让我们对包是如何被分离和其原因挖得更深一点吧!这有一个常见的错误理解:React “引擎"在react包的内部。这不是事实。事实上,从 React 0.14开始对包进行分割时,React包就有意地仅导出关于如何定义组件的API了。React的大部分实现其实在“渲染器”中。渲染器的其中一些例子包括:react-dom,react-dom/server,react-native,react-test-renderer,react-art(另外,你也可以构建自己的)。这就是为什么react包帮助很大而不管作用在什么平台上。所有它导出的模块,比如React.Component,React.createElement,React.Children和Hooks,都是平台无关的。无论你的代码运行在React DOM、React DOM Server、还是React Native,你的组件都可以以一种相同的方式导入并且使用它们。与之相对的是,渲染器会暴露出平台相关的接口,比如ReactDOM.render(),它会让你可以把React挂载在DOM节点中。每个渲染器都提供像这样的接口,但理想情况是:大多数组件都不需要从渲染器中导入任何东西。这能使它们更精简。大多数人都认为React“引擎”是位于每个独立的渲染器中的。许多渲染器都包含一份相同的代码—我们叫它“调节器”,为了表现的更好,遵循这个步骤 可以让调节器的代码和渲染器的代码在打包时归到一处。(拷贝代码通常不是优化“打包后文件”(bundle)体积的好办法,但大多数React的使用者一次只需要一个渲染器,比如:react-dom(译者注:因此可以忽略调节器的存在))The takeaway here 是react包仅仅让你知道如何使用React的特性而无需了解他们是如何被实现的。渲染器(react-dom,react-native等等)会提供React特性的实现和平台相关的逻辑;一些关于调节器的代码被分享出来了,但那只是单独渲染器的实现细节而已。现在我们知道了为什么react和react-dom包需要为新特定更新代码了。比如:当React16.3新增了Context接口时,React.createContext()方法会在React包中被暴露出来。但是React.createContext()实际上不会实现具体的逻辑(译者注:只定义接口,由其他渲染器来实现逻辑)。并且,在React DOM和React DOM Server上实现的逻辑也会有区别。所以createContext()会返回一些纯粹的对象(定义如何实现):// 一个简单例子function createContext(defaultValue) { let context = { _currentValue: defaultValue, Provider: null, Consumer: null }; context.Provider = { $$typeof: Symbol.for(‘react.provider’), _context: context }; context.Consumer = { $$typeof: Symbol.for(‘react.context’), _context: context, }; return context;}你会在某处代码中使用<MyContext.Provider>或<MyContext.Consumer>,那里就是决定着如何处理他们的渲染器。React DOM会用A方法追踪context值,但React DOM Server或许会用另一个不同的方法实现。所以如果你将react升级到16.3+,但没有升级react-dom,你将使用一个还不知道Provider和Consumer类型的渲染器,这也就旧版的react-dom可能会报错:fail saying these types are invalid的原因。同样的警告也会出现在React Native中,但是不同于React DOM,一个新的React版本不会立即产生一个对应的React Native版本。他们(React Native)有自己的发布时间表。大概几周后,渲染器代码才会单独更新到React Native库中。这就是为什么新特性在React Native生效的时间会和React DOM不同。Okay,那么现在我们知道了react包不包含任何好玩的东西,并且具体的实现都在像react-dom,react-native这样的渲染器中。但这并不能回答我们开头提出的问题。React.Component里的setState()是如何和对应的渲染器通信的呢?答案是每个渲染器都会在创建的类中添加一个特殊的东西,这个东西叫updater。它不是你添加的东西—恰恰相反,它是React DOM,React DOM Server 或者React Native在创建了一个类的实例后添加的:// React DOM 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMUpdater;// React DOM Server 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactDOMServerUpdater;// React Native 中是这样const inst = new YourComponent();inst.props = props;inst.updater = ReactNativeUpdater;从 setState的实现就可以看出,它做的所有的工作就是把任务委托给在这个组件实例中创建的渲染器:// 简单例子setState(partialState, callback) { // 使用updater去和渲染器通信 this.updater.enqueueSetState(this, partialState, callback);}React DOM Server 可能想忽略状态更新并且警告你,然而React DOM和React Native将会让调节器的拷贝部分去 处理它。这就是尽管this.setState()被定义在React包中也可以更新DOM的原因。它调用被React DOM添加的this.updater并且让React DOM来处理更新。现在我们都比较了解“类”了,但“钩子”(Hooks)呢?当人们第一次看到 钩子接口的提案时,他们常回想:useState是怎么知道该做什么呢?这一假设简直比对this.setState()的疑问还要迷人。但就像我们如今看到的那样,setState()的实现一直以来都是模糊不清的。它除了传递调用给当前的渲染器外什么都不做。所以,useState钩子做的事也是如此。这次不是updater,钩子(Hooks)使用一个叫做“分配器”(dispatcher)的对象,当你调用React.useState()、React.useEffect()或者其他自带的钩子时,这些调用会被推送给当前的分配器。// In React (simplified a bit)const React = { // Real property is hidden a bit deeper, see if you can find it! __currentDispatcher: null, useState(initialState) { return React.__currentDispatcher.useState(initialState); }, useEffect(initialState) { return React.__currentDispatcher.useEffect(initialState); }, // …};单独的渲染器会在渲染你的组件之前设置分配器(dispatcher)。// In React DOMconst prevDispatcher = React.__currentDispatcher;React.__currentDispatcher = ReactDOMDispatcher;let result;try { result = YourComponent(props);} finally { // Restore it back React.__currentDispatcher = prevDispatcher;}React DOM Server的实现在这里。由React DOM和React Native共享的调节器实现在这里。这就是为什么像react-dom这样的渲染器需要访问和你调用的钩子所使用的react一样的包。否则你的组件将找不到分配器!如果你有多个React的拷贝在相同的组件树中,代码可能不会正常工作。然而,这总是造成复杂的Bug,因此钩子会在它耗光你的精力前强制你去解决包的副本问题。如果你不觉得这有什么,你可以在工具使用它们前精巧地覆盖掉原先的分配器(__currentDispatcher的名字其实我自己编的但你可以在React仓库中找到它真正的名字)。比如:React DevTools会使用一个特殊的内建分配器来通过捕获JavaScript调用栈来反映(introspect)钩子。不要在家里重复这个(Don’t repeat this at home.)(译者注:可能是“不要在家里模仿某项实验”的衍生体。可能是个笑话,但我get到)这也意味着钩子不是React固有的东西。如果在将来有很多类库想要重用相同的基础钩子,理论上来说分配器可能会被移到分离的包中并且被塑造成优秀的接口—会有更少让人望而生畏的名称—暴露出来。在实际中,我们更偏向去避免过于仓促地将某物抽象,直到我们的确需要这么做。updater和__currentDispatcher都是泛型程序设计(依赖注入/dependency injection)的绝佳实例。渲染器“注入”特性的实现。就像setState可以让你的组件看起来简单明了。当你使用React时,你不需要考虑它是如何工作的。我们期望React用户去花费更多的时间去考虑它们的应用代码而不是一些抽象的概念比如:依赖注入。但如果你曾好奇this.setState()或useState()是怎么知道它们该做什么的,那我希望这篇文章将帮助到你。 ...

January 9, 2019 · 2 min · jiezi

ReactNative: 使用Animted API实现向上滚动时隐藏Header组件

想先推荐一下近期在写的一个React Native项目,名字叫 Gakki :是一个Mastodon的第三方客户端 (Android App)预览写在前面本来我也不想造这个轮子的,奈何没找到合适的组件。只能自己上了~思路很清楚: 监听滚动事件,动态修改Header组件和Content组件的top值(当然,他们默认都是position:relative)。接下来实现的时候遇到了问题,我第一个版本是通过动态设置state来实现,即:/** * 每次滚动时,重新设置headerTop的值 /onScroll = event =>{ const y = event.nativeEvent.contentOffset.y if (y >= 270) return // headerTop即是Header和Content的top样式对应的值 this.setState({ headerTop: y })}这样虽然能实现,但是效果不好:明显可以看到在上滑的过程中,Header组件一卡一卡地向上方移动(一点都不流畅)。因为就只能另寻他法了:动画React Native 提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation (笔者注:这次没有用到它)Animated 相关API介绍首先,这儿有一个简单“逐渐显示”动画的DEMO,需要你先看完(文档很简单明了且注释清楚,没必要Copy过来)。在看懂了DEMO的基础上,我们还需要了解两个关键的API才能实现完整的效果:1. interpolate插值函数。用来对不同类型的数值做映射处理。当然,这儿是文档说明,可能看了更不清楚:Each property can be run through an interpolation first. An interpolation maps input ranges to output ranges, typically using a linear interpolation but also supports easing functions. By default, it will extrapolate the curve beyond the ranges given, but you can also have it clamp the output value.翻译:每个属性可以先经过插值处理。插值对输入范围和输出范围之间做一个映射,通常使用线性插值,但也支持缓和函数。默认情况下,如果给定数据超出范围,他也可以自行推断出对于的曲线,但您也可以让它箝位输出值(P.S. 最后一句可能翻译错误,因为没搞懂clamp value指的是什么, sigh…)举个例子:在实现一个图片旋转动画时,输入值只能是这样的:this.state = { rotate: new Animated.Value(0) // 初始化用到的动画变量}…// 这么映射是因为style样式需要的是0deg这样的值,你给它0这样的值,它可不能正常工作。因为必定需要一个映射处理。this.state.rotate.interpolate({ // 将0映射成0deg,1映射成360deg。当然中间的数据也是如此映射。 inputRange: [0, 1], outputRange: [‘0deg’, ‘360deg’]})2. Animated.event一般动画的输入值都是默认设定好的,比如前面DEMO中的逐渐显示动画中的透明度:开始是0,最后是1。这是已经写死了的。但如果有些动画效果需要的不是写死的值,而是动态输入的呢,比如:手势(上滑、下滑,左滑,右滑…)、其它事件。那就用到了Animated.event。直接看一个将滚动事件的y值(滚动条距离顶部高度)和我们的动画变量绑定起来的例子:// 这段代码表示:在滚动事件触发时,将event.nativeEvent.contentOffset.y 的值动态绑定到this.state.headerTop上// 和最前面我通过this.setState动态设置的目的一样,但交给Animated.event做就不会造成视觉上的卡顿了。onScroll={Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } }])}关于API更多的说明请移步文档完整代码import React, { Component } from ‘react’import { StyleSheet, Text, View, Animated, FlatList } from ‘react-native’class List extends Component { onScroll = event => { // 显示和隐藏Header组件的动画在 滚动条距离顶部距离小于270 时起作用 // 移除这个限制就是另一种效果了,可以自己想一想 if (this.props.onScroll) { if (event.nativeEvent.contentOffset.y >= 270) return this.props.onScroll(event) } } render() { // 模拟列表数据 const mockData = [ ‘富强’, ‘民主’, ‘文明’, ‘和谐’, ‘自由’, ‘平等’, ‘公正’, ‘法治’, ‘爱国’, ‘敬业’, ‘诚信’, ‘友善’ ] return ( <FlatList onScroll={this.onScroll} data={mockData} renderItem={({ item }) => ( <View style={styles.list}> <Text>{item}</Text> </View> )} /> ) }}export default class AnimatedScrollDemo extends Component { constructor(props) { super(props) this.state = { headerTop: new Animated.Value(0) } } onScroll = event => { if (event.nativeEvent.contentOffset.y >= 270) return Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } } ]) } render() { const top = this.state.headerTop.interpolate({ inputRange: [0, 270], outputRange: [0, -50] }) return ( <View style={styles.container}> <Animated.View style={{ top: top }}> <View style={styles.header}> <Text style={style.text}>linshuirong.cn</Text> </View> </Animated.View> {/ 在oHeader组件上移的同时,列表容器也需要同时向上移动,需要注意下。 */} <Animated.View style={{ top: top }}> <List onScroll={Animated.event([ { nativeEvent: { contentOffset: { y: this.state.headerTop } } } ])} /> </Animated.View> </View> ) }}const styles = StyleSheet.create({ container: { flex: 1 }, list: { height: 80, backgroundColor: ‘pink’, marginBottom: 1, alignItems: ‘center’, justifyContent: ‘center’, color: ‘white’ }, header: { height: 50, backgroundColor: ‘#3F51B5’, alignItems: ‘center’, justifyContent: ‘center’ }, text: { color: ‘white’ }}) ...

December 25, 2018 · 2 min · jiezi

封装框架的实践

最近在尝试着封装一个框架,碍于种种原因,先从简单的入手吧。基于vue和elementUI封装的框架,集成 数据存储localforage、字体图标库font-awesome、css拓展语言scss、网络请求axios等模块,为了让业务开发更专注于数据驱动。项目源码地址:https://gitee.com/g2333/data_…使用场景1. 环境 框架基于vue2.0开发,故开发环境也需要nodejs和vue-cli。2. 拓展和维护 为使框架本身易拓展和维护,项目采用vue-cli封装,在开发和使用过程都不打包,保持程序的可读性,同时也方便在引用该模块时可简单的修改配置文件和源码。3. 便捷使用 在一个全新的vue-cli初始化项目中, 安装模块(在vue项目路径下npm i modulecomponents), 引用模块(在vue项目的main.js中添加import ‘modulecomponents/index.js’) 测试使用(比如使用框架暴露的方法dataTool.alert(‘测试成功’))项目配置1. 依赖模块 框架本身依赖有如下模块: elementUI 框架的主力,用于组件封装和方法的调用、 localforage 数据存储,用于存储前端的大量数据、 font-awesome 字体图标库、 scss css拓展语言、 axios 网络请求2. 设置项目入口 修改package.json文件,添加main字段,指向项目入口(“main”: “mc/index.js”),修改private字段,设置为开源(“private”: false)3. 项目初始化 为了让框架方便引用,故在初始化文件index.js(框架项目开发过程使用indexdsForDev.js),自动引入依赖和全局变量的挂载4. 文件提交 设置项目.gitignore文件忽略node_modules避免在协同开发时因为环境不一致导致的webpack报错 设置项目.npmignore文件忽略发布时非必要的文件,减少模块的体积封装的模块1. 组件 组件基于elementUI封装,项目中封装的组件为避免命名冲突,都以mc-为前缀开头。 计划封装的组件有如下: 表格mc-table、 表单mc-form、 树列表mc-tree、 对话框mc-dialog、 上下文菜单mc-contentmenu、 按钮组mc-btns、 流图mc-flow、 下拉选框mc-select、 附件上传mc-upload//在界面上显示一个表单<mc-form :object=“form”></mc-form>//表单对象,描述表单的结构和数据form: new mc.Form({ structure: [{ label: ‘测试’, name: ’test’, }], data: { test: ‘hello world’, }}) 除框架封装的组件外,依旧支持使用elementUI组件2. 全局方法 为了方便开发,较为常用的方法被挂载在全局变量dataTool的属性中,比如 请求方法:ajax请求httpReq、文件导出exportFile、文件上传uploadFile; 提示类方法:警告弹框alert、边角提示notify、确认输入框confirm、锁屏加载loading等; 调用组件类方法:打开弹窗openDialog、关闭弹窗closeDialog、打开上下文菜单openContextmenu、关闭上下文菜单closeContextmenu等; 数据处理:对象类型的克隆和过滤objClone、时间格式的转化formatTime、cookie的添加setCookie等; 原型链上的方法:获取表格新增的一行数据Array.newTableRow、数组元素位置交换Array.swap等; 事件方法:注册事件addEvent、触发事件emitEvent、取消事件cancelEvent等;//打开上下文菜单,点击导出文件,将请求的内容导出成flow.json文件dataTool.openContextmenu(event,[{ text: ‘导出文件’, icon: ‘fa fa-download’, color: ‘blue’, click: ()=>{ const reqObj = {url:‘http://rap2api.taobao.org/app/mock/22119/FUNC=getFlow’, params: {}, type:‘mock’}; dataTool.httpReq(reqObj).then(res=>{ dataTool.exportFile({fileName: ‘flow.json’,data: JSON.stringify(res.CONTENT)}); }); }}])3. 配置文件 封装的组件各有一份默认配置文件,方便全局调整组件的参数。 封装的组件既支持组件类的默认参数修改,也兼容修改单个实例和继承组件类export default { //表单类的配置文件 btns: [], //表单底部栏按钮 topBtns: [], //表单顶部栏按钮 hiddenRows: [], //隐藏的行 topBtnStyle: ‘’, bottomBtnStyle: ’text-align:right’, dialogEdit: false, //是否开启普通字符串类型的弹窗编辑功能 showRules: true, //是否显示表单规则验证 style: “margin: 10px;”, inline: false, labelWidth: “50px”, labelPosition: “right”, size: “small”, autoComplete: ‘on’, spellcheck: false, readOnly: false, extBtnIcon: ’el-icon-more’, textArea: { size: { minRows: 1, maxRows: 10}, resize: ’none’, }, tag: { input: ‘’, type: ‘warning’, closeTransition: false, appendWord: ’ + New Tag’, }, inputStyle: ‘width:100%’, dataType: { //采用小写,减少枚举数量 bool: [‘bool’,‘boolean’,‘switch’], checkboxGroup: [‘checkboxgroup’,‘checkbox’], radio: [‘radio’], select: [‘singleenum’,‘multiselect’,‘multienum’], time: [’time’], date: [‘date’,‘datetime’,‘datetimerange’,‘daterange’], button: [‘button’,‘btn’], tag: [’tags’,’tag’], input: [’’,‘input’,‘string’,’text’,’textarea’,’number’,‘float’,‘password’,‘double’,‘int’,‘integer’,’long’,‘search’,’extinput’], component: [‘mc-table’], },}开发记录1. 项目结构 整体项目的规划整理在一个xmind文件中,方便记录开发进度和了解项目的整体大纲,这是图片版 http://qpic.cn/dDPbFwEeD (请在复制粘贴到浏览器的地址栏中访问)2. 使用文档 为了记录开发进度和形成规范,项目开发的使用说明和修改会记录在石墨文档https://shimo.im/sheet/K8QPjP…3. 版本控制 使用git作为版本控制,项目的源码托管在码云上https://gitee.com/g2333/data_… 既方便协同开发,也方便代码版本控制框架更新1. 项目更新 修改后的源码在测试成功后,修改package.json中的版本号,将代码推送到码云上,然后通过npm发布新版本2. 模块更新 通过npm update modulecomponents指令更新模块,即可使用最新版功能 ...

December 9, 2018 · 1 min · jiezi

(译)React hooks:它不是一种魔法,只是一个数组——使用图表揭秘提案规则

原文地址:https://medium.com/@ryardley/…译文:染陌 (Github)译文地址:https://github.com/answershuto/Blog转载请著名出处我是一名hooks API的忠实粉丝,然而它对你的使用会有一些奇怪的约束,所以我在本文中使用一个模型来把原理展示给那些想去使用新的API却难以理解它的规则的人。警告:Hooks 还处于实验阶段本文提到的 Hooks API 还处于实验阶段,如果你需要的是稳定的 React API 文档,可以从这里找到。解密 Hooks 的工作方式我发现一些同学苦苦思索新的 Hooks API 中的“魔法”,所以我打算尝试着去解释一下,至少从表层出发,它是如何工作的。Hooks 的规则React 核心团队在Hooks的提案中提出了两个在你使用Hooks的过程中必须去遵守的主要规则。请不要在循环、条件或者嵌套函数中调用 Hooks都有在 React 函数中才去调用 Hooks后者我觉得是显而易见的,你需要用函数的方式把行为与组件关联起来才能把行为添加到组件。然而对于前者,我认为它会让人产生困惑,因为这样使用 API 编程似乎显得不那么自然,但这就是我今天要套索的内容。Hooks 的状态管理都是依赖数组的为了让大家产生一个更清晰的模型,让我们来看一下 Hooks 的简单实现可能是什么样子。需要注意的是,这部分内容只是 API 的一种可能实现方法,以便读者更好地趣理解它。它并不是 API 实际在内部的工作方式,而且它只是一个提案,在未来都会有可能发生变化。我们应该如何实现“useState()”呢?让我们通过一个例子来理解状态可能是如何工作的。首先让我们从一个组件开始:代码地址/* 译:https://github.com/answershuto /function RenderFunctionComponent() { const [firstName, setFirstName] = useState(“Rudi”); const [lastName, setLastName] = useState(“Yardley”); return ( <Button onClick={() => setFirstName(“Fred”)}>Fred</Button> );}Hooks API 背后的思想是你可以将一个 setter 函数通过 Hook 函数的第二个参数返回,用该函数来控制 Hook 管理的壮体。所以 React 能用这个做什么呢?首先让我们解释一下它在 React 内部是如何工作的。在执行上下文去渲染一个特殊组件的时候,下面这些步骤会被执行。这意味着,数据的存储是独立于组件之外的。该状态不能与其他组件共享,但是它拥有一个独立的作用域,在该作用域需要被渲染时读取数据。(1)初始化创建两个空数组“setters”与“state”设置指针“cursor”为 0(2)首次渲染首次执行组件函数每当 useState() 被调用时,如果它是首次渲染,它会通过 push 将一个 setter 方法(绑定了指针“cursor”位置)放进 setters 数组中,同时,也会将另一个对应的状态放进 state 数组中去。(3)后续渲染每次的后续渲染都会重置指针“cursor”的位置,并会从每个数组中读取对应的值。(4)处理事件每个 setter 都会有一个对应的指针位置的引用,因此当触发任何 setter 调用的时候都会触发去改变状态数组中的对应的值。以及底层的实现这是一段示例代码:代码地址let state = [];let setters = [];let firstRun = true;let cursor = 0;function createSetter(cursor) { return function setterWithCursor(newVal) { state[cursor] = newVal; };}/ 译:https://github.com/answershuto /// This is the pseudocode for the useState helperexport function useState(initVal) { if (firstRun) { state.push(initVal); setters.push(createSetter(cursor)); firstRun = false; } const setter = setters[cursor]; const value = state[cursor]; cursor++; return [value, setter];}/ 译:https://github.com/answershuto */// Our component code that uses hooksfunction RenderFunctionComponent() { const [firstName, setFirstName] = useState(“Rudi”); // cursor: 0 const [lastName, setLastName] = useState(“Yardley”); // cursor: 1 return ( <div> <Button onClick={() => setFirstName(“Richard”)}>Richard</Button> <Button onClick={() => setFirstName(“Fred”)}>Fred</Button> </div> );}// This is sort of simulating Reacts rendering cyclefunction MyComponent() { cursor = 0; // resetting the cursor return <RenderFunctionComponent />; // render}console.log(state); // Pre-render: []MyComponent();console.log(state); // First-render: [‘Rudi’, ‘Yardley’]MyComponent();console.log(state); // Subsequent-render: [‘Rudi’, ‘Yardley’]// click the ‘Fred’ buttonconsole.log(state); // After-click: [‘Fred’, ‘Yardley’]为什么说顺序很重要呢?如果我们基于一些外部条件或是说组件的状态去改变 Hooks 在渲染周期的顺序,那会发生什么呢?让我们做一些 React 团队禁止去做的事情。代码地址let firstRender = true;function RenderFunctionComponent() { let initName; if(firstRender){ [initName] = useState(“Rudi”); firstRender = false; } const [firstName, setFirstName] = useState(initName); const [lastName, setLastName] = useState(“Yardley”); return ( <Button onClick={() => setFirstName(“Fred”)}>Fred</Button> );}我们在条件语句中调用了 useState 函数,让我们看看它对整个系统造成的破坏。糟糕组件的首次渲染到此为止,我们的变量 firstName 与 lastName 依旧包含了正确的数据,让我们继续去看一下第二次渲染会发生什么事情。糟糕的第二次渲染现在 firstName 与 lastName 这两个变量全部被设置为“Rudi”,与我们实际的存储状态不符。这个例子的用法显然是不正确的,但是它让我们知道了为什么我们必须使用 React 团队规定的规则去使用 Hooks。React 团队制定了这个规则,是因为如果不遵循这套规则去使用 Hooks API会导致数据有问题。思考 Hooks 维护了一些列的数组,所以你不应该去违反这些规则所以你现在应该清除为什么你不应该在条件语句或者循环语句中使用 Hooks 了。因为我们维护了一个指针“cursor”指向一个数组,如果你改变了 render 函数内部的调用顺序,那么这个指针“cursor”将不会匹配到正确的数据,你的调用也将不会指向正确的数据或句柄。因此,有一个诀窍就是你需要思考 Hooks 作为一组需要一个匹配一致的指针“cursor”去管理的数组(染陌译)。如果做到了这一点,那么采用任何的写法它都可以正常工作。总结希望通过上述的讲解,我已经给大家建立了一个关于 Hooks 的更加清晰的思维模型,以此可以去思考新的 Hooks API 底层到底做了什么事情。请记住,它真正的价值在于能够关注点聚集在一起,同时小心它的顺序,那使用 Hooks API 会很高的回报。Hooks 是 React 组件的一个很有用的插件,这也佐证了为何大家为何对此感到如此兴奋。如果你脑海中形成了我上述的这种思维模型,把这种状态作为一组数组的存在,那么你就会发现在使用中不会打破它的规则。我希望将来再去研究一下 useEffects useEffects 方法,并尝试将其与 React 的生命周期进行比较。这篇文章是一篇在线文档,如果你想要参与贡献或者有任何有误的地方,欢迎联系我。你可以在 Twitter 上面 fllow 我(Rudi Yardley)或者在Github找到我。染陌 译:https://github.com/answershuto ...

November 25, 2018 · 2 min · jiezi

Release ng-alain 2.0

从计划2.0开始足足进行近四个月,其中发布过八个版本。当初给2.0做的愿景基本上达到要求,当然一切都还是那句话:【让开发者更加专注于业务】。ng-zorro-antd 提供的大量的基础组件,当你熟悉这些组件以后,开发 Angular 会是一种“爽”体验,然而对于中后台而言部分高频繁组件在大多数场景下显得有点臃肿。所以 2.0 变更主要从两个方面:使 CURD 操作更“自然”开发体验更友好响应式开发CURD提供一组 Simple 系列组件:sv:查看se:编辑st:数据表格(原 simple-table 重新重构)以及基于 JSON Schema 的动态表单 sf,这四个 Simple 系列组件相比较 ng-zorro-antd 的原始写法,更易编写、阅读,基本上可以满足大多数场景;但它们并非用来替代原始的写法,特别是 st 与 sf 它们并不适合复杂交互,此时,依然应该优先使用原始方式。除此之外,2.0 对部分输入属性及接口的多态性、内聚性做一些变更。属性多态性当构建一个数据表格时,表格的数据源可能来自远程数据或本地静态数据,但我们不应该过度的将数据源做成两个不同属性加以区分,他们只是不同的数据来源而已,但对于表格而言是统一:<table [data]=“url”></table><table [data]=“list”></table>属性内聚性同一个功能的属性应该更内聚,例如我们表述一个HTTP请求时,包含:请求方法、请求域、参数等,这些属性应该统一在一个对象值体现,HttpClient 请求就是一个非常好的例子。<st [reqMethod]="‘GET’" [reqParams]="{ a: 1 }"></st><st [req]="{ method: ‘get’, params: { a: 1 } }"></st>响应式开发意指开发过程中如何使用最小的方式构建符合移动端的中后台,ng-alain 默认提供一套 响应式服务 规则,它服务于最基础的CURD组件:se、sv 等。例如:当你希望构建一行两列的表单,并且若屏幕小于 <576px 将自动转化成一列,则只需要这样:<div se-container=“2”> <se label=“Field1”></se> <se label=“Field2”></se></div>当然这一切只是简化 nz-row、nz-col 的运用而已,如果你希望布局也延续这种方式可以使用 sg 组件。除此之外,ng-alain 也将 CSS 做为开发语言非常重要的组成部分,并且将这些语言特征转化成独立的单元类,如果你是采用 VSCODE 可借由 ng-alain snippets 提供的智能提醒,减少理解它们的成本。升级Angular Cli 提供的 ng update 命令让我们可以大胆重构、改进你的组件,并且用户升级只需要简单的一行命令就可以完全升级。从 1.x 升级至 2.x 虽然无法改变 ts 代码方面的动作,但基本上可以完成 HTML 方面的升级,主要还是 ts 代码的解析无法像 HTML 那样预期。而 ng-alain 的 1.x 升至 2.0 也只需要一行命令而已,有关更多细节,请参考升级指引。未来直到 ng-zorro-antd 下一大版本更新之前,2.0 保持一段时间的休息期,不再会有新功能。之后,会根据 ng-zorro-antd 的进度,对 ng-alain 做一次大的性能重构。新尝试ng-alain 正在尝试提供商业主题服务,有兴趣可以参阅。(完) ...

November 21, 2018 · 1 min · jiezi

React 重要的一次重构:认识异步渲染架构 Fiber

Diff 算法熟悉 react 的朋友都知道,在 react 中有个核心的算法,叫 diff 算法。web 界面由 dom 树组成,不同的 dom 树会渲染出不同的界面。react 使用 virtual dom 来表示 dom 树,而 diff 算法就是用于比较 virtual dom 树的区别,并更新界面需要更新的部分。diff 算法和 virtual dom 的完美结合的过程被称为 reconciler,这可是 react 攻城拔寨的绝对利器。有了 reconciler,开发者可以脱身操作真实的 dom 树,只需要向 react 描述界面的状态,而 react 会帮助你高效的完成真正 dom 操作。在 react16 之前的 reconciler 叫 stack reconciler,fiber 是 react 新的 reconciler,这次更新到 fiber 架构是一次重量级的核心架构的替换,react 为了完成这次替换已经准备了两三年的时间了。那么 fiber 究竟有什么好的呢?Fiber 为何出现不知道大家有没有遇到过这样的情况,点击一个页面的按钮时感觉到页面没有任何的反应,让你怀疑电脑是不是死机了,然后你快速切出浏览器,发现电脑并没有死机,于是再切回浏览器,这时候才发现页面终于更新了。为什么会出现这种情况?在多数情况下,可能是因为浏览器忙着执行相关的 js 代码,导致浏览器主线程没有及时响应用户的操作或者没有及时更新界面。下面这张图就表示了这种现象,你的公司只有一个程序员 (main thread),当这个程序员在执行你的任务 (your code) 时,处于沉浸式编程的状态,无法响应外部的其他事件,什么下班吃饭,都是不存在的。这就像浏览器忙着执行 js 代码的时候,不会去执行页面更新等操作。本着顾客是上帝的原则,作为一名优秀的开发者,怎么能够允许出现这种情况降低用户的体验呢。因此 react 团队引入了异步渲染这个概念,而采用 fiber 架构可以实现这种异步渲染的方式。原先的 stack reconciler 像是一个递归执行的函数,从父组件调用子组件的 reconciler 过程就是一个递归执行的过程,这也是为什么被称为 stack reconciler 的原因。当我们调用 setState 的时候,react 从根节点开始遍历,找出所有的不同,而对于特别庞大的 dom 树来说,这个递归遍历的过程会消耗特别长的时间。在这个期间,任何交互和渲染都会被阻塞,这样就给用户一种“死机”的感觉。fiber 的出现解决了这个问题,它把 reconciler 的过程拆分成了一个个的小任务,并在完成了小任务之后暂停执行 js 代码,然后检查是否有需要更新的内容和需要响应的事件,做出相应的处理后再继续执行 js 代码。这样就给了用户一种应用一直在运行的感觉,提高了用户的体验。Fiber 如何做到异步渲染在做显示方面的工作时,经常会听到一个目标叫 60 帧,这表示的是画面的更新频率,也就是画面每秒钟更新 60 次。这是因为在 60 帧的更新频率下,页面在人眼中显得流畅,无明显卡顿。每秒钟更新 60 次也就是每 16ms 需要更新一次页面,如果更新页面消耗的时间不到 16ms,那么在下一次更新时机来到之前会剩下一点时间执行其他的任务,只要保证及时在 16ms 的间隔下更新界面就完全不会影响到页面的流畅程度。fiber 的核心正是利用了 60 帧原则,实现了一个基于优先级和 requestIdleCallback 的循环任务调度算法。requestIdleCallback 是浏览器提供的一个 api,可以让浏览器在空闲的时候执行回调,在回调参数中可以获取到当前帧剩余的时间,fiber 利用了这个参数,判断当前剩下的时间是否足够继续执行任务,如果足够则继续执行,否则暂停任务,并调用 requestIdleCallback 通知浏览器空闲的时候继续执行当前的任务。function fiber(剩余时间) { if (剩余时间 > 任务所需时间) { 做任务; } else { requestIdleCallback(fiber); }}fiber 还会为不同的任务设置不同的优先级,高优先级任务是需要马上展示到页面上的,比如你正在输入框中输入文字,你肯定希望你的手指在键盘上敲下每一个按键时,输入框能立马做出反馈,这样你才能知道你的输入是否正确,是否有效。低优先级的任务则是像从服务器传来了一些数据,这个时候需要更新页面,比如这篇文章喜欢的人数+1 或是评论+1,这并不是那么紧急的更新,延迟 100-200ms 并不会有多大差别,完全可以在后面进行处理。fiber 会根据任务优先级来动态调整任务调度,优先完成高优先级的任务。{ Synchronous: 1, // 同步任务,优先级最高 Task: 2, // 当前调度正执行的任务 Animation 3, // 动画 High: 4, // 高优先级 Low: 5, // 低优先级 Offscreen: 6, // 当前屏幕外的更新,优先级最低}在 fiber 架构中,有一种数据结构,它的名字就叫做 fiber,这也是为什么新的 reconciler 叫做 fiber 的原因。fiber 其实就是一个 js 对象,这个对象的属性中比较重要的有 stateNode、tag、return、child、sibling 和 alternate。Fiber = { tag // 标记任务的进度 return // 父节点 child // 子节点 sibling // 兄弟节点 alternate // 变化记录 …..};我们可以看出 fiber 基于链表结构,拥有一个个指针,指向它的父节点子节点和兄弟节点,在 diff 的过程中,依照节点连接的关系进行遍历。fiber 可能存在的问题在 fiber 中,更新是分阶段的,具体分为两个阶段,首先是 reconciliation 的阶段,这个阶段在计算前后 dom 树的差异,然后是 commit 的阶段,这个阶段将把更新渲染到页面上。第一个阶段是可以打断的,因为这个阶段耗时可能会很长,因此需要暂停下来去执行其他更高优先级的任务,第二个阶段则不会被打断,会一口气把更新渲染到页面上。由于 reconciliation 的阶段会被打断,可能会导致 commit 前的这些生命周期函数多次执行。react 官方目前已经把 componentWillMount、componentWillReceiveProps 和 componetWillUpdate 标记为 unsafe,并使用新的生命周期函数 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 进行替换。还有一个问题是饥饿问题,意思是如果高优先级的任务一直插入,导致低优先级的任务无法得到机会执行,这被称为饥饿问题。对于这个问题官方提出的解决方案是尽量复用已经完成的操作来缓解。相信官方也正在努力提出更好的方法去解决这个问题。文 / Xss编 / 荧声本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:https://knownsec-fed.com/2018…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:乐趣区。欢迎留言讨论,我们会尽可能回复。感谢您的阅读。 ...

November 15, 2018 · 2 min · jiezi

一份超级详细的Vue-cli3.0使用教程[赶紧来试试!]

前言在vue-cli 2.X的时候,也写过一篇类似的文章,在八月份的时候vue-cli已经更新到了3.X,新版本的脚手架,功能灰常强大,试用过后非常喜欢,写篇教程来帮助各位踩一下坑。游泳、健身了解一下:博客、前端积累文档、公众号、GitHub主要内容:零配置启动/打包一个.vue文件详细的搭建过程重点推荐:使用图形化界面创建/管理/运行项目安装:卸载旧版本:如果你事先已经全局安装了旧版本的vue-cli(1.x 或 2.x),你需要先卸载它:npm uninstall vue-cli -gNode版本要求:3.x需要在Node.js8.9或更高版本(推荐8.11.0+),点击这里可以安装node大多数人都安装过了node,使用下面的命令行查询你的node版本:node -v如果你的版本不够,可以使用下面的命令行来把Node版本更新到最新的稳定版:npm install -g n // 安装模块 这个模块是专门用来管理node.js版本的n stable // 更新你的node版本mac下,更新版本的时候,如果提示你权限不够:sudo n stable // 我就遇到了安装vue-cli:npm install -g @vue/cli // 安装cli3.xvue –version // 查询版本是否为3.x如果cli3.x用的不舒服,cli3也能使用2.x模板:npm install -g @vue/cli-init // 安装这个模块// 就可以使用2.x的模板:vue init webpack my-project零配置启动/打包一个.vue文件:安装扩展:npm install -g @vue/cli-service-global安装完扩展之后,可以随便找个文件夹建一个如下方示例的.vue文件,然后跑起来:vue serve App.vue // 启动服务vue build App.vue // 打包出生产环境的包并用来部署如下图,只需一个.vue文件,就能迅速启动一个服务:如图所示,服务启动的时候回生成一个node_modules包,稍微测试了一下,服务支持ES6语法和热更新,打包的时候会生成一个dist文件夹。(新建一个test.vue文件也只有一个node_modules/dist文件夹)这是个很棒的功能,用于开发一个库、组件,做一些小demo等都是非常适合的!第一次创建项目:1. 命令行:vue create hello-cli3 hello-cli3是文件夹名字,如果不存在会自动创建文件夹,如果存在会安装到那个文件夹中。相比2.x的时候需要自己手动创建一个文件夹,这里也算是一个小优化吧。2. 选择模板:一开始只有两个选项: default(默认配置)和Manually select features(手动配置)默认配置只有babel和eslint其他的都要自己另外再配置,所以我们选第二项手动配置。在每次选择手动配置之后,会询问你是否保存配置,也就是图片中的koro选项,这样以后我们在进行创建项目的时候只需使用原先的配置就可以了,而不用再进行配置。3. 选择配置:根据你的项目需要来选择配置,空格键是选中与取消,A键是全选? Check the features needed for your project: (Press <space> to select, to toggle all, to invert selection) // 检查项目所需的功能:(按<space>选择,切换所有,反转选择)( ) TypeScript // 支持使用 TypeScript 书写源码 ( ) Progressive Web App (PWA) Support // PWA 支持 ( ) Router // 支持 vue-router ( ) Vuex // 支持 vuex ( ) CSS Pre-processors // 支持 CSS 预处理器。 ( ) Linter / Formatter // 支持代码风格检查和格式化。 ( ) Unit Testing // 支持单元测试。 ( ) E2E Testing4. 选择css预处理器:如果你选择了Css预处理器选项,会让你选择这个? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):// 选择CSS预处理器(默认支持PostCSS,Autoprefixer和CSS模块):SCSS/SASS LESS Stylus5. 是否使用路由的history模式:这里我建议选No,这样打包出来丢到服务器上可以直接使用了,后期要用的话,也可以自己再开起来。选yes的话需要服务器那边再进行设置。Use history mode for router? (Requires proper server setup for index fallback in production) // 路由使用history模式?(在生产环境中需要适当的服务器设置以备索引)6. 选择Eslint代码验证规则:> ESLint with error prevention only ESLint + Airbnb config ESLint + Standard config ESLint + Prettier7. 选择什么时候进行代码规则检测:建议选保存就检测,等到commit的时候,问题可能都已经积累很多了。之前写了篇VsCode保存时自动修复Eslint错误推荐一下。? Pick additional lint features: (Press <space> to select, to toggle all, to invert selection)( ) Lint on save // 保存就检测 ( ) Lint and fix on commit // fix和commit时候检查8. 选择e2e测试:? Pick a E2E testing solution: (Use arrow keys)❯ Cypress (Chrome only) Nightwatch (Selenium-based) 9. 把babel,postcss,eslint这些配置文件放哪:通常我们会选择独立放置,让package.json干净些? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)In dedicated config files // 独立文件放置 In package.json // 放package.json里10. 是否保存配置:Save this as a preset for future projects? (Y/n) // 是否记录一下以便下次继续使用这套配置// 选保存之后,会让你写一个配置的名字:Save preset as: name // 然后你下次进入配置可以直接使用你这次的配置了11. 下载依赖12. webpack配置的目录不见了:一起来看一下新项目的结构(下图),会发现2.x的webpack配置的目录不见了,也就是没有build、config这两个文件夹了:这种方式的优势对小白来说非常友好,不会一上来就两个文件夹,一堆文件,看着脑袋都大了。然后在引用抄别人的配置的时候,也非常方便,直接将文件复制过来就好了。在自定义一下webpack的配置,我们需要在根目录新建一个vue.config.js文件,文件中应该导出一个对象,然后进行配置,详情查阅官方文档// vue.config.jsmodule.exports = { // 选项…}还有一些小变动像:static文件夹改为public了,router文件夹变成了单个文件之类的(我之前一直这么做,嘿嘿)。13.启动项目:启动项目:npm run serve // 不是之前的 npm run dev打开http://localhost:8080:使用图形化界面创建/管理/运行项目:启动图形化界面vue ui 这是个全局的命令 在哪个文件夹都可以打开界面(下图),重要的项目可以收藏起来(置顶):创建项目和导入项目:目录选中之后,导入项目点击下面的导入就可以了。创建项目,填一个文件夹名字:然后选一下预先保存好的设置就可以了,非常方便,建议采用图形界面来创建项目:项目管理:当我们点击hello -cli3项目,就会进入项目管理的界面1. 仪表盘:这个仪表盘,主要是为了我们操作方便而设置的可以点击右上角的按钮,来添加/移动这些功能选项。2. vue-cli3.x插件:vue-cli3的插件功能,详情了解官方文档cli3插件安装的过程:3. 项目依赖直接在图形界面管理依赖很舒服了!安装依赖的时候,要记得选择开发依赖/运行依赖!4. 项目配置可以对cli进行一些配置、Eslint规则修改:5. 任务:serve 运行项目,点击直接运行,再也不用输入命令了!可以清楚的看到各个模块用了多久,方便我们针对性的进行优化:build 打包项目:这里主要展示了图表的功能,比以前2.x生成报告更加直观,超级棒!6. 其他夜间风格界面,我更喜欢这个界面直接打开编辑器,很棒了!还有一些乱七八糟的按钮结语可以说很认真了,希望大家看完能够有些收获,赶紧试试新版的vue-cli吧!希望看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励。博客、前端积累文档、公众号、GitHub以上2018.11.10参考资料:vue-cli3官方文档vue-cli3.0搭建与配置 ...

November 15, 2018 · 2 min · jiezi

iOS自动布局——Masonry详解

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由鹅厂新鲜事儿发表于云+社区专栏作者:oceanlong | 腾讯 移动客户端开发工程师前言UI布局是整个前端体系里不可或缺的一环。代码的布局是设计语言与用户视觉感受沟通的桥梁,不论它看起来多么简单或是琐碎,但不得不承认,绝大部分软件开发的问题,都是界面问题。那么,如何高效的完成UI开发,也是软件行业一直在克服的问题。所以,软件界面开发的核心点即是:如何减少UI设计稿的建模难度和减少建模转化到代码的实现难度最初iOS提供了平面直角坐标系的方式,来解决布局问题,即所谓的手动布局。平面直角坐标系确实是一套完备在理论,这在数学上已经验证过了,只要我们的屏幕还是平面,它就肯定是有效的。但有效不一定高效,我们在日常的生活中,很少会用平面直角坐标系来向人描述位置关系。更多的是依靠相对位置。所幸,iOS为我们提供自动布局的方法,来解决这一困境。自动布局的基本理念其实说到本质,它和手动布局是一样的。对一个控件放在哪里,我们依然只关心它的(x, y, width, height)。但手动布局的方式是,一次性计算出这四个值,然后设置进去,完成布局。但当父控件或屏幕发生变化时,子控件的计算就要重新来过,非常麻烦。因此,在自动布局中,我们不再关心(x, y, width, height)的具体值,我们只关心(x, y, width, height)四个量对应的约束。约束那么何为约束呢?obj1.property1 =(obj2.property2 * multiplier)+ constant value子控件的某一个量一定与另一个控件的某一个量呈线性关系,这就是约束。那么,给(x, y, width, height)四个量,分别给一个约束,就可以确定一个控件的最终位置。 //创建左边约束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];这一段代码即是:控件(blueView)的 x = rootView的x * 1.0 + 20这里一定要注意,这样的一条约束,涉及了子控件和父控件,所以这条约束一定要添加到父控件中。添加约束的规则:如果两个控件是父子控件,则添加到父控件中。如果两个控件不是父子控件,则添加到层级最近的共同父控件中。示例 //关闭Autoresizing blueView.translatesAutoresizingMaskIntoConstraints = NO; //创建左边约束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc]; //创建右边约束 NSLayoutConstraint *rightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-20]; [self.view addConstraint:rightLc]; //创建底部约束 NSLayoutConstraint *bottomLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-20]; [self.view addConstraint:bottomLc]; //创建高度约束 NSLayoutConstraint *heightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50]; [blueView addConstraint: heightLc];我们注意到,自动布局其实工作分两步:创建视图的约束将约束添加到合适的位置约束关系从上面的描述中,已经非常清晰了。那么如何寻找约束添加的合适位置呢?到这里,我们只是解决了如何减少UI设计稿的建模难度的问题,显然,减少建模转化到代码的实现难度这个效果没能达成。关于如何解决减少建模转化到代码的实现难度的问题,开源库上面的代码,我们可以看到,虽然自动布局已经比手动布局优雅不少了,但它依然行数较多。每条约束大约都需要三行代码,面对复杂的页面,这样开发出来,会很难阅读。Masonry则为我们解决了这个问题。Masonry地址引入Masonry我们选择使用Cocoapods的方式。引入比较简单:我们先在工程目录下,创建Podfile文件:2.编辑Podfile其中,‘IosOcDemo’就是我们工程的名字,根据需要,我们自行替换。3.添加依赖完成后,执行指令pod install。CocoaPods就会为我们自动下载并添加依赖。实践这样的一个代码,用手动布局,我们大致的代码应该是这样:-(void)initBottomView{ self.bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; self.bottomButtons = [[NSMutableArray alloc]init]; _bottomBarView.backgroundColor = [UIColor yellowColor]; [self addSubview:_bottomBarView]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame:CGRectZero]; button.backgroundColor = [UIColor redColor]; [_bottomButtons addObject:button]; [self addSubview:button]; }}-(void)layoutBottomView{ _bottomBarView.frame = CGRectMake(20, _viewHeight - 200, _viewWidth - 40, 200); for (int i = 0 ; i < 3; i++) { UIButton button = _bottomButtons[i]; CGFloat x = i * (_viewWidth - 40 - 20 * 4) / 3 + 20(i+1) + 20; CGFloat y = _viewHeight - 200; CGFloat width = (_viewWidth - 40 - 20 * 4) / 3; CGFloat height = 200; button.frame = CGRectMake(x, y, width, height); }}我们来看一下,在Masonry的帮助下,我们可以把刚刚的代码写成什么样的: -(void)initBottomView{ _bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; _bottomBarView.backgroundColor = [UIColor yellowColor]; _bottomBarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_bottomBarView]; [_bottomBarView mas_makeConstraints:^(MASConstraintMaker make) { make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self); }]; _bottomButtons = [[NSMutableArray alloc]init]; for(int i = 0 ; i < 3 ; i++) { UIButton button = [[UIButton alloc]initWithFrame: CGRectZero]; button.backgroundColor = [UIColor redColor]; button.translatesAutoresizingMaskIntoConstraints = NO; [_bottomButtons addObject:button]; [_bottomBarView addSubview:button]; [button mas_makeConstraints:^(MASConstraintMaker make) { if (i == 0) { make.left.mas_equalTo(20); }else{ UIButton previousButton = _bottomButtons[i-1]; make.left.equalTo(previousButton.mas_right).with.offset(20); } make.top.mas_equalTo(_bottomBarView.mas_top); make.width.equalTo(_bottomBarView.mas_width).with.multipliedBy(1.0f/3).offset(-204/3); make.height.equalTo(_bottomBarView.mas_height); }]; }}我们可以看到在Masonry的封装下,代码变得非常简练易读,需要行数略有增加,但是计算过程减少了,我们能更加关注于多个UIView间的位置关系,这与当前的UI设计语言是契合的。所以Masonry能否让我们更直观地表达UI。源码解读Masonry的封装很有魅力,那么,我们可以简单地来看一下,它是如何封装的。我们再仔细看一下Masonry的API会发现,我们是直接在UIView上进行调用的。也就是说,Masonry对UIView进行了扩展。在View+MASUtilities.h中:#if TARGET_OS_IPHONE || TARGET_OS_TV #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MAS_VIEW_CONTROLLER UIViewController #define MASEdgeInsets UIEdgeInsets然后在View+MASAdditions.h中,我们看到了Masonry的扩展:#import “MASUtilities.h”#import “MASConstraintMaker.h”#import “MASViewAttribute.h”/ * Provides constraint maker block * and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs /@interface MAS_VIEW (MASAdditions)/ * following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute */@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;@property (nonatomic, strong, readonly) MASViewAttribute mas_baseline;@property (nonatomic, strong, readonly) MASViewAttribute (^mas_attribute)(NSLayoutAttribute attr);…/ * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created MASConstraints */- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;一些,适配的代码,我省略了,先看核心代码。在刚刚的例子中,我们正是调用的mas_makeConstraints方法。- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install];}mas_makeConstraints方法比较简单,只是封装了MASConstraintMaker初始化,设置约束和安装。这里的block就是我们刚刚在外层设置的约束的函数指针。也就是这一串:^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.height.mas_equalTo(50); make.bottom.equalTo(self.view).with.offset(-10); }由于约束条件的设置比较复杂,我们先来看看初始化和安装。初始化- (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self;}初始化的代码比较简单,将传入的view放入MASConstraintMaker成员,然后创建MASConstraintMaker的约束容器(NSMutableArray)。安装- (NSArray *)install { if (self.removeExisting) { NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view]; for (MASConstraint *constraint in installedConstraints) { [constraint uninstall]; } } NSArray *constraints = self.constraints.copy; for (MASConstraint *constraint in constraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; return constraints;}安装的代码分为三块:判断是否需要移除已有的约束。如果需要,会遍历已有约束,然后逐个uninstallcopy已有的约束,遍历,并逐一installremove掉所有约束,并将已添加的constraints返回。install的方法,还是继续封装到了Constraint中,我们继续跟进阅读:我们会发现Constraint只是一个接口,Masonry中对于Constraint接口有两个实现,分别是:MASViewConstraint和MASCompositeConstraint。这两个类,分别是单个约束和约束集合。在上面的例子中,我们只是对单个UIView进行约束,所以我们先看MASViewConstraint的代码。以下代码MASViewConstraint进行了一定程度的简化,省略了一些扩展属性,只展示我们的例子中,会执行的代码:- (void)install { if (self.hasBeenInstalled) { return; } … MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; // alignment attributes must have a secondViewAttribute // therefore we assume that is refering to superview // eg make.left.equalTo(@10) if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) { secondLayoutItem = self.firstViewAttribute.view.superview; secondLayoutAttribute = firstLayoutAttribute; } MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; layoutConstraint.priority = self.layoutPriority; layoutConstraint.mas_key = self.mas_key; if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @“couldn’t find a common superview for %@ and %@”, self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; } MASLayoutConstraint *existingConstraint = nil; … else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; }}自动布局是一种相对布局,所以,绝大部分情况下,需要两个UIView(约束方与参照方)。在上面的方法中:firstLayoutItem是约束方,secondLayoutItem是参照方firstLayoutAttribute是约束方的属性,secondLayoutAttribute是参照方的属性。MASLayoutConstraint就是NSLayoutConstraint的子类,只是添加了mas_key属性。到这里,我们就与系统提供的API对应上了。 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];再看看我们之前用系统API完成的例子,是不是格外熟悉?那么接下来,我们就是要阅读 make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);是如何变成firstLayoutItem, secondLayoutItem, firstLayoutAttribute, secondLayoutAttribute和layoutRelation的。约束条件的设置回到前面的:- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install];}我们接下来,就要看block的实现:block其实是一个函数指针。此处真正调用的方法是: make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);我们挑选其中一个,来看看源码实现:left- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];}- (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];}- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; if ([constraint isKindOfClass:MASViewConstraint.class]) { //replace with composite constraint NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint;}在对单个view添加约束时,constraint为nil。我们直接生成了一个新约束newConstraint。它的firstViewAttribute就是我们传入的NSLayoutAttributeLeftequalTo- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); };}- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @“Redefinition of constraint relation”); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) { MASViewConstraint *viewConstraint = [self copy]; viewConstraint.layoutRelation = relation; viewConstraint.secondViewAttribute = attr; [children addObject:viewConstraint]; } MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self.delegate; [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @“Redefinition of constraint relation”); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } };}此处,我们依然先看attribute不是NSArray的情况。这里在单个属性的约束中,就比较简单了,将relation和attribue传入MASConstraint对应的成员。在上面介绍install方法时,我们就曾提到过: MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant];firstLayoutItem和secondLayoutItem在install方法中已收集完成,此时,经过left和equalTo我们又收集到了:firstViewAttribute、secondViewAttribute和layoutRelation胜利即在眼前。- (MASConstraint * (^)(CGFloat))offset { return ^id(CGFloat offset){ self.offset = offset; return self; };}- (void)setOffset:(CGFloat)offset { self.layoutConstant = offset;}通过OC的set语法,Masonry将offset传入layoutConstant。至此,layoutConstraint就完成了全部的元素收集,可以使用添加约束的方式,只需要解决最后一个问题,约束添加到哪里呢?我们似乎在调用时,并不需要关心这件事情,那说明框架帮我们完成了这个工作。closestCommonSuperview我们在MASViewConstraint中,可以找到这样一段: if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @“couldn’t find a common superview for %@ and %@”, self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; }注意到,closetCommonSuperview就是Masonry为我们找到的最近公共父控件。- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view { MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view; while (!closestCommonSuperview && secondViewSuperview) { MAS_VIEW *firstViewSuperview = self; while (!closestCommonSuperview && firstViewSuperview) { if (secondViewSuperview == firstViewSuperview) { closestCommonSuperview = secondViewSuperview; } firstViewSuperview = firstViewSuperview.superview; } secondViewSuperview = secondViewSuperview.superview; } return closestCommonSuperview;}实现也比较简单。至此,我们完成了所有准备,就可以开始愉快的自动布局啦。以上就是Masonry对iOS自动布局封装的解读。如有问题,欢迎指正。问答iOS:如何使用自动布局约束?相关阅读走进 MasonryiOS自动布局框架之MasonryiOS学习——布局利器Masonry框架源码深度剖析 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 30, 2018 · 5 min · jiezi

用第三方库带来的刚性成本

我之前一直在思考,在项目中引入第三方库会给我们带来什么,在你为什么用或不用框架?这篇文章中wo说了使用框架(库)带给我们的好处和不使用给我们的好处是什么,但是并未详细说明使用第三方库,它本身又会带来什么问题。通过查找资料,我在Hard Costs of Third-Party Scripts 这篇文章中找到了答案。第三方库带来的影响就是它降低了用户体验效果,增加了 用户成本。这篇文章从7个方面说明了使用第三方库可能带来的问题,文中说的问题你大多数也都遇到过。译文如下:我对第三方库与用户体验成本之间的关系感兴趣。我做的每个客户端,平均大约使用30个第三方库。但开发者却因为 如果我们将 async 它们全部加载呢? 减少了对它们的争论。虽然这是一个很好的反驳,但是仍然存在将它传递给用户的成本。这就是我要讲的主题。用户成本 是什么?最常见答案可能是隐私。但就隐私被侵犯这事儿来说,从来没有明确界限。人们对个人数据暴露也有不同的容忍度。在讨论第三库的时,隐私就变得有点儿笨拙党的感觉。虽然讨论它们很有必要,但我希望将隐私与第三份库分开,并专注于应用程序带来的成本。为了更好地了解 用户成本,我在Twitter上发了一个话题:假设我们有一个包含约300个第三方库的站点,所有库都负责加载为异步,用户体验将面临什么?1. aync代码在你自己编写的代码之前到达如果异步代码在你自己编写的代码完成下载之前到达,将阻止HTML,CSS和JS解析。2. DNS查询成本今年早些时候,我看到 webhint 的 AntónMolleda 做的 一些数字统计 ,它表明即使3次DNS查询,也会在3G渲染预算上降低5s/170kb,额外需要10kb。有一些HTTP存档数据表明更多的第三方库会增加加载时间,这似乎证实了这个数字。3. CPU占用第三方库可能会阻止你自己编写的代码的性能。它占用了大量的可用的CPU周期,你自己编写的代码处于明显的 下风。4. 网络占用Paul Irish提到了类似于CPU占用的网络占用,他说:“你的更重要的自己编写的代码的网络请求会占用较少的带宽。” 虽然你的浏览器有5个连接线程可用于网络请求,但你自己编写的代码的任何部分或单个页面需要另一个文件或者懒加载的图像,它可能需要排队等一段时间。5. 用户数据和电池成本手机的数据需要成本,HTTP请求也会消耗手机电池的电量。我认为无论是用户有意识地,还是由市场的无形大手的引导,最终将追求非数据消费和电池电量消耗的替代方案(例如,使用FB Messenger的微信)6. 重载事件它和CPU占用类似,但大多使用scroll,resize,或click的库可能会降低页面在浏览器中的自适应性,在极端情况下可能会导致布局的崩溃。7. 调试覆盖范围增加虽然这会增加组织代码的成本,但它可能会变成影响用户体验的成本。更多库会增加调试的覆盖范围。如果出现问题,你是喜欢检查5个地方的问题还是300个?我最近遇到了由第三方库的问题而导致的错误,找出导致问题的库,花费了我一整天的时间,但我仍然不知道该如何将该库引入构建,而不产生错误。虽然300个第三方库,这听起来有点夸张,但是有约50或者约150个第三方请求呢?我现在有几个应用就是这个样子。Alexa Top 50的统计结果是平均约为18个第三方库。我坚信即使有了第三方提供的解决方案,你也可能会遇到部分或全部上述的问题。总结我想这里我们可以得到这样一个结论,那就是单页应用程序和它每个路由的JS构建包可能最有可能被网络和CPU争用所扼杀。你的初始有效负载可能会获得100%的CPU,但后续页面可能会争夺其中的一小部分。但这取决于你的构建策略,如果构建策略是完美的,那么构建包会很好的加载并及时执行。不仅是SPA可能会遇到这样的问题,其它任何资源都可能延迟加载,并缓慢的执行。特别是网络不靠谱的情况下,所以这不应该让你感到非常惊讶的。如果你想到我遗漏的任何东西,请告诉我。我想我们都在这艘沉没的第三方船上,我们彼此都需要摆脱它。我在gihub[article 中开辟了一个想法模块],我会陆续添加一些相似的文章,希望得到大家的支持。

October 29, 2018 · 1 min · jiezi

高级 Vue 组件模式 (7)

07 使用 State Initializers目标到目前为止,仅从 toggle 组件自身的角度来看,它已经可以满足大多数的业务场景了。但我们会发现一个问题,就是当前 toggle 组件的状态对于调用者来说,完全是黑盒状态,即调用者无法初始化,也无法更改组件的开关状态,这在一些场景无法满足需求。对于无法初始化开关状态的问题,倒是很好解决,我们可以在 toggle 组件声明一个 prop 属性 on 来代表组件的默认开关状态,同时在 mounted 生命周期函数中将这个默认值同步到组件 data 相应的属性中去。对于无法更改开关状态的问题,似乎无法简单通过声明一个 prop 属性的方式来解决,并且如果我们期望的更改逻辑是异步的话,同样无法满足。因此这篇文章着重来解决这两个问题:toggle 组件能够支持开关状态的初始化功能toggle 组件能够提供一个 reset 方法以供重置开关状态重置开关状态可以以异步的方式进行实现初始化开关状态为了使 toggle 组件能够支持默认状态的传入,我们采用声明 prop 属性的方式,如下:on: { type: Boolean, default: false}之后在其 mounted 生命周期对开关状态进行同步,如下:mounted() { this.status.on = this.on; }这样当我们期望 toggle 以开的状态进行渲染时,可以这样调用组件:<toggle :on=“true” @toggle=“onToggle”> …</toggle>重置开关状态为了能够从外部更改 toggle 组件的开关状态,我们可以在组件内部声明一个观测 on prop 属性的监听器,比如:watch: { on(val){ // do something… }}但如果这么做,会存在一个问题,即目标中关于开关状态的更改逻辑的编写者是组件调用者,而 watch 函数的编写者是组件实现者,由于实现者无法预知调用者更改状态的逻辑,所以使用 watch 是无法满足条件的。让我们换一个角度来思考问题,既然实现者无法预知调用者的逻辑,何不把重置开关状态的逻辑全部交由调用者来实现?别忘了 Vue 组件也是可以传入 Function 类型的 prop 属性的,如下:onReset: { type: Function, default: () => this.on},这样就将提供重置状态的逻辑暴露给了组件调用者,当然,如果调用者没有提供相关重置逻辑,组件内部会自动降级为使用 on 属性来作为重置的状态值。组件内部额外声明一个 reset 方法,在其内部重置当前的开关状态,如下:reset(){ this.status.on = this.onReset(this.status.on) this.$emit(“reset”, this.status.on)}这里会首先以当前开关状态为参数,调用 onReset 方法,再将返回值赋值给当前状态,并触发一个 reset 事件以供父组件订阅。之后在 app 组件中,可以按如下方式传入 onReset 函数,并编写具体的重置逻辑:// template<toggle :on=“false” @toggle=“onToggle” :on-reset=“resetToTrue”>…</toggle>// script…resetToTrue(on) { return true;},…运行效果如下:支持异步重置在实现同步重置的基础上,实现异步重置十分简单,通常情况下,处理异步较好的方式是使用 Promise,使用 callback 也可以,使用 Observable 也是不错的选择,这里我们选择 Promise。由于要同时处理同步和异步两种情况,只需把同步情况视为异步情况即可,比如以下两种情况在效果上是等价的:// syncthis.status.on = this.onReset(this.status.on)// asyncPromise.resolve(this.onReset(this.status.on)) .then(on => { this.status.on = on })而 onReset 函数如果返回的是一个 Promise 实例的话,Promise.resolve 也会正确解析到当它为 fullfill 状态的值,这样关于 reset 方法我们改版如下:reset(){ Promise.resolve(this.onReset(this.status.on)) .then(on => { this.status.on = on this.$emit(“reset”, this.status.on) })}在 app 组件中,可以传入一个异步的重置逻辑,这里就不贴代码了,直接上一个运行截图,组件会在点击重置按钮后 1 秒后,重置为开状态:成果你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-7总结Function 类型的 prop 属性在一些情况下非常有用,比如文章中提及的状态初始化,这其实是工厂模式的一种体现,在其他的框架中也有体现,比如 React 中,HOC 中提及的 render props 就是一种比较具体的应用,Angular 在声明具有循环依赖的 Module 时,可以通过 () => Module 的方式进行声明等等。目录github gist ...

October 26, 2018 · 1 min · jiezi

你为什么用或不用框架?

最近,在持续关注一个Twitter话题,就是 Why do people decide to use frameworks? ,这个话题是由Nicole Sullivan提出的。刚开始,我对这个问题也没有太在意,也就是随意的看了看,正如提问者Nicole Sullivan说的那样,我刚开始也觉得这是一个愚蠢的问题。但是这个问题就像蒲公英的种子一样,就这么在我的脑海里扎了根,截至到我这写这篇文章之前,我都有一直关注这个问题,并且在思考这个问题。虽然这个问题看似简单,你或多或少都能回答出那么一两点,但是我想你可能自己也对自己的回答不太满意吧?不管你怎么想的,但我渐渐收起了我从一开始的轻视态度,开始正视这个问题。在这里,我还要感谢Nicole Sullivan,是她的这个话题,让我对为什么使用框架有了全方位的了解。为什么用可以集中精力在业务的实现,而不用把过多的精力和人力用在代码功能逻辑的实现上。可以避免由我们自己写带来的很多bug。可以暂时快速的解决掉某一问题,以待以后的进一步解决。可以避免写技术文档和介绍功能实现给团队成员的问题。可以极大的缩短开发的周期。因为成熟的框架本身就是完善的解决方案。一般它们都有自己的生态系统,有众多技术达人参与。这样我们在使用中,不仅有完善的技术文档可以随时查看,遇到问题也有地方问,最重要的一点是不用自己设计、整理、验证技术方案了,你之需要深入了解它的生态系统即可。避免了bikeshedding现象(它的意思是说:‘总在一些没有意义的问题上争论,而有意忽视哪些真正需要解决的难点/痛点问题’)的出现。为什么不用不用的其中一个原因,就是用框架的成本太高。夸张一点说,可能就这一点就就盖过了它所有的优点,但要用一个框架一定要考虑它的成本。对于一个团队来说,首先需要专门招聘一些精通这个框架的开发人员(前端/后端)和维护人员,再加之没有一个框架是万能的,如果下一个项目使用另一个框架是否意味着另招一批开发人员,这样的代价不是所有的企业都能承受;对个人来说,学习一个框架需要花费大量的时间和精力,你不仅要学习框架本身,你还要了解它的生态系统,关注它的各方面咨询,尤其是版本更新,它往往带有对过去框架存在问题的改进,如果升级版就可以移除自己解决原框架存在问题而写的补丁(这些补丁有大有小,也可能引入了其他依赖),这样就带来另一个问题,项目的迁移问题,像angular一样它现在的版本已经到了9.x,但现在有相当一部分还在用着1.x,angualr虽好,但是它也给开发人员带来了巨大麻烦,学习曲线太陡是一方面,要了解的东西太多(知识面的广度)是另一个重要方面。当然一直使用一个框架,并进行深度挖掘的技术团队,受益良多,但这样的团队又有多少。除了成本,就要考虑项目的规模和复杂度问题。不能一个就五六个简单页面的项目,你就引入一个框架吧。此外使用一个框加,往往会使用它配套的部件,如:引入vue,一些用惯了vue-router,vuex,在项目中自然而然的引入这些东西,这些在简单的仙姑中往往没有必要。这也是开发这些框架的核心团队为什么尽量的缩减核心框架功能的原因,而把一些次要功能或三级功能独立出来。这些由主框架、功能库、主题库、工具库、以及辅助开发的工具库等组成的集合,就是该框架的生态系统。开发人员要保持理智国内的一些基层开发人员普遍存在不理智的现象,跟风现象比较严重。应该注意这些:技术比较火,并不代表技术方案的完美。好的技术框架我不一定都要会,但要有一个框架我十分精通。别人会的,我不一定要非得精通,但我会的要保证别人一定要不如我。学习某一个技术不是一两天或者一两个月的事儿,技术都是积累来的。不要把大神神话,它们也是从 小白 成长起来的。要保持对技术的热度,而不是蹭技术的热度。结束语其实,不管你是否使用框架,抑或你对框架持有什么样的态度,你都要明白你选择的出发点儿是什么或者说动机是什么。

October 25, 2018 · 1 min · jiezi

高级 Vue 组件模式 (6)

06 通过 directive 增强组件内容目标之前的五篇文章中,switch 组件一直是被视为内部组件存在的,细心的读者应该会发现,这个组件除了帮我们提供开关的交互以外,还会根据当前 toggle 的开关状态,为 button 元素增加 aria-expanded 属性,以 aira 开头的属性叫作内容增强属性,它用于描述当前元素的某种特殊状态,帮助残障人士更好地浏览网站内容。但是,作为组件调用者,未必会对使用这种相关属性对网站内容进行增强,那么如何更好地解决这个问题呢?答案就是使用 directive。我们期望能够显示地声明当前的元素是一个 toggler 职能的组件或者元素,这个组件或者元素,可以根据当前 toggle 组件的开关状态,动态地更新它本身的 aria-expanded 属性,以便针对无障碍访问提供适配。实现简单实现首先创建一个 toggler 指令函数,如下:export default function(el, binding, vnode) { const on = binding.value if (on) { el.setAttribute(aria-expanded, true); } else { el.removeAttribute(aria-expanded, false); }}这个指令函数很简单,就是通过传入指令的表达式的值来判定,是否在当前元素上增加一个 aria-expanded 属性。之后再 app 引入该指令,如下:directives: { toggler}之后就可以在 app 组件的模板中使用该指令了,比如:<custom-button v-toggler=“status.on” ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>一切都将按预期中运行,当 toggle 组件的状态为开时,custom-button 组件的根元素会增加一个 aria-expanded=“true” 的内容增强属性。Note: 这里关于指令的引入,使用的函数简写的方式,会在指令的 bind 和 update 钩子函数中触发相同的逻辑,vue 中的指令包含 5 个不同的钩子函数,这里就不赘述了,不熟悉的读者可以通过阅读官方文档来了解。注入当前组件实例上文中的指令会通过 binding.value 来获取 toggle 组件的开关状态,这样虽然可行,但在使用该指令时,custom-button 本身的 prop 属性 on 已经代表了当前的开关状态,能否直接在指令中获取当前所绑定的组件实例呢?答案是可以的。指令函数的第三个参数即为当前所绑定组件的虚拟 dom 节点实例,其 componentInstance 属性指向当前组件实例,所以可以将之前的指令改版如下:export default function(el, binding, vnode) { const comp = vnode.componentInstance; const on = binding.value || comp.on; if (on) { el.setAttribute(aria-expanded, true); } else { el.removeAttribute(aria-expanded, false); }}这样,即使不向指令传入表达式,它也可以自动去注入当前修饰组件所拥有的 prop 属性 on 的值,如下:<custom-button v-toggler ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>提供更多灵活性指令函数的第二个参数除了可以获取传入指令内部的表达式的值以外,还有其他若干属性,比如 name、arg、modifiers等,详细说明可以去参考官方文档。为了尽可能地使指令保证灵活性,我们期望可以自定义无障碍属性 aria 的后缀名称,比如叫做 aria-on,这里我们可以通过 arg 这个参数轻松实现,改版如下:export default function(el, binding, vnode) { const comp = vnode.componentInstance; const suffix = binding.arg || “expanded”; const on = binding.value || comp.on; if (on) { el.setAttribute(aria-${suffix}, true); } else { el.removeAttribute(aria-${suffix}, false); }}可以发现,这里通过 binding.arg 来获取无障碍属性的后缀名称,并当没有传递该参数时,降级至 expanded。这里仅仅是为了演示,读者有兴趣的话,还可以利用 binding 对象的其他属性提供更多的灵活性。成果最终的运行结果就不用语言描述了,直接截了一个图,是 toggle 组件开关状态为开时的截图:你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-6总结关于指令的概念,我自身还是在 angularjs(v1.2以下版本) 中第一次接触,当时其实不兴组件化开发这个概念,指令本身的设计理念也是基于增强这个概念的,即增强某个 html 标签。到后来兴起了组件化开发的开发思想,指令似乎是随着 angularjs 的没落而消失了踪影。但仔细想想的话,web 开发流程中,并不是所有的场景都可以拿组件来抽象和描述的,比如说,你想提供一个类似高亮边框的公用功能,到底如何来按组件化的思想抽象它呢?这时候使用指令往往是一个很好的切入点。因此,当你面临解决的问题,颗粒度小于组件化抽象的粒度,同时又具备复用性,那就大胆的使用指令来解决它吧。目录github gist ...

October 22, 2018 · 1 min · jiezi

基于TensorFlow Serving的深度学习在线预估

一、前言随着深度学习在图像、语言、广告点击率预估等各个领域不断发展,很多团队开始探索深度学习技术在业务层面的实践与应用。而在广告CTR预估方面,新模型也是层出不穷: Wide and Deep[1]、DeepCross Network[2]、DeepFM[3]、xDeepFM[4],美团很多篇深度学习博客也做了详细的介绍。但是,当离线模型需要上线时,就会遇见各种新的问题: 离线模型性能能否满足线上要求、模型预估如何镶入到原有工程系统等等。只有准确的理解深度学习框架,才能更好地将深度学习部署到线上,从而兼容原工程系统、满足线上性能要求。本文首先介绍下美团平台用户增长组业务场景及离线训练流程,然后主要介绍我们使用TensorFlow Serving部署WDL模型到线上的全过程,以及如何优化线上服务性能,希望能对大家有所启发。二、业务场景及离线流程2.1 业务场景在广告精排的场景下,针对每个用户,最多会有几百个广告召回,模型根据用户特征与每一个广告相关特征,分别预估该用户对每条广告的点击率,从而进行排序。由于广告交易平台(AdExchange)对于DSP的超时时间限制,我们的排序模块平均响应时间必须控制在10ms以内,同时美团DSP需要根据预估点击率参与实时竞价,因此对模型预估性能要求比较高。2.2 离线训练离线数据方面,我们使用Spark生成TensorFlow[5]原生态的数据格式tfrecord,加快数据读取。模型方面,使用经典的Wide and Deep模型,特征包括用户维度特征、场景维度特征、商品维度特征。Wide 部分有 80多特征输入,Deep部分有60多特征输入,经过Embedding输入层大约有600维度,之后是3层256等宽全连接,模型参数一共有35万参数,对应导出模型文件大小大约11M。离线训练方面,使用TensorFlow同步 + Backup Workers[6]的分布式框架,解决异步更新延迟和同步更新性能慢的问题。在分布式ps参数分配方面,使用GreedyLoadBalancing方式,根据预估参数大小分配参数,取代Round Robin取模分配的方法,可以使各个PS负载均衡。 计算设备方面,我们发现只使用CPU而不使用GPU,训练速度会更快,这主要是因为尽管GPU计算上性能可能会提升,但是却增加了CPU与GPU之间数据传输的开销,当模型计算并不太复杂时,使用CPU效果会更好些。同时我们使用了Estimator高级API,将数据读取、分布式训练、模型验证、TensorFlow Serving模型导出进行封装。使用Estimator的主要好处在于:单机训练与分布式训练可以很简单的切换,而且在使用不同设备:CPU、GPU、TPU时,无需修改过多的代码。Estimator的框架十分清晰,便于开发者之间的交流。初学者还可以直接使用一些已经构建好的Estimator模型:DNN模型、XGBoost模型、线性模型等。三、TensorFlow Serving及性能优化3.1 TensorFlow Serving介绍TensorFlow Serving是一个用于机器学习模型Serving的高性能开源库,它可以将训练好的机器学习模型部署到线上,使用gRPC作为接口接受外部调用。TensorFlow Serving支持模型热更新与自动模型版本管理,具有非常灵活的特点。下图为TensorFlow Serving整个框架图。Client端会不断给Manager发送请求,Manager会根据版本管理策略管理模型更新,并将最新的模型计算结果返回给Client端。TensorFlow Serving架构,图片来源于TensorFlow Serving官方文档 美团内部由数据平台提供专门TensorFlow Serving通过YARN分布式地跑在集群上,其周期性地扫描HDFS路径来检查模型版本,并自动进行更新。当然,每一台本地机器都可以安装TensorFlow Serving进行试验。在我们站外广告精排的场景下,每来一位用户时,线上请求端会把该用户和召回所得100个广告的所有信息,转化成模型输入格式,然后作为一个Batch发送给TensorFlow Serving,TensorFlow Serving接受请求后,经过计算得到CTR预估值,再返回给请求端。部署TensorFlow Serving的第一版时,QPS大约200时,打包请求需要5ms,网络开销需要固定3ms左右,仅模型预估计算需要10ms,整个过程的TP50线大约18ms,性能完全达不到线上的要求。接下来详细介绍下我们性能优化的过程。3.2 性能优化3.2.1 请求端优化线上请求端优化主要是对一百个广告进行并行处理,我们使用OpenMP多线程并行处理数据,将请求时间性能从5ms降低到2ms左右。#pragma omp parallel for for (int i = 0; i < request->ad_feat_size(); ++i) { tensorflow::Example example; data_processing();}3.2.2 构建模型OPS优化在没有进行优化之前,模型的输入是未进行处理的原格式数据,例如,渠道特征取值可能为:‘渠道1’、‘渠道2’ 这样的string格式,然后在模型里面做One Hot处理。最初模型使用了大量的高阶tf.feature_column对数据进行处理, 转为One Hot和embedding格式。 使用tf.feature_column的好处是,输入时不需要对原数据做任何处理,可以通过feature_column API在模型内部对特征做很多常用的处理,例如:tf.feature_column.bucketized_column可以做分桶,tf.feature_column.crossed_column可以对类别特征做特征交叉。但特征处理的压力就放在了模型里。 为了进一步分析使用feature_column的耗时,我们使用tf.profiler工具,对整个离线训练流程耗时做了分析。在Estimator框架下使用tf.profiler是非常方便的,只需加一行代码即可。with tf.contrib.tfprof.ProfileContext(job_dir + ‘/tmp/train_dir’) as pctx: estimator = tf.estimator.Estimator(model_fn=get_model_fn(job_dir), config=run_config, params=hparams) 下图为使用tf.profiler,网络在向前传播的耗时分布图,可以看出使用feature_column API的特征处理耗费了很大时间。优化前profiler记录, 前向传播的耗时占总训练时间55.78%,主要耗费在feature_column OPS对原始数据的预处理 为了解决特征在模型内做处理耗时大的问题,我们在处理离线数据时,把所有string格式的原生数据,提前做好One Hot的映射,并且把映射关系落到本地feature_index文件,进而供线上线下使用。这样就相当于把原本需要在模型端计算One Hot的过程省略掉,替代为使用词典做O(1)的查找。同时在构建模型时候,使用更多性能有保证的低阶API替代feature_column这样的高阶API。下图为性能优化后,前向传播耗时在整个训练流程的占比。可以看出,前向传播的耗时占比降低了很多。优化后profiler记录,前向传播耗时占总训练时间39.53%3.2.3 XLA,JIT编译优化TensorFlow采用有向数据流图来表达整个计算过程,其中Node代表着操作(OPS),数据通过Tensor的方式来表达,不同Node间有向的边表示数据流动方向,整个图就是有向的数据流图。XLA(Accelerated Linear Algebra)是一种专门对TensorFlow中线性代数运算进行优化的编译器,当打开JIT(Just In Time)编译模式时,便会使用XLA编译器。整个编译流程如下图所示: TensorFlow计算流程 首先TensorFlow整个计算图会经过优化,图中冗余的计算会被剪掉。HLO(High Level Optimizer)会将优化后的计算图 生成HLO的原始操作,XLA编译器会对HLO的原始操作进行一些优化,最后交给LLVM IR根据不同的后端设备,生成不同的机器代码。JIT的使用,有助于LLVM IR根据 HLO原始操作生成 更高效的机器码;同时,对于多个可融合的HLO原始操作,会融合成一个更加高效的计算操作。但是JIT的编译是在代码运行时进行编译,这也意味着运行代码时会有一部分额外的编译开销。网络结构、Batch Size对JIT性能影响[7]上图显示为不同网络结构,不同Batch Size下使用JIT编译后与不使用JIT编译的耗时之比。可以看出,较大的Batch Size性能优化比较明显,层数与神经元个数变化对JIT编译优化影响不大。在实际的应用中,具体效果会因网络结构、模型参数、硬件设备等原因而异。3.2.4 最终性能经过上述一系列的性能优化,模型预估时间从开始的10ms降低到1.1ms,请求时间从5ms降到2ms。整个流程从打包发送请求到收到结果,耗时大约6ms。模型计算时间相关参数:QPS:1308,50line:1.1ms,999line:3.0ms。下面四个图分别为:耗时分布图显示大部分耗时控制在1ms内;请求次数显示每分钟请求大约8万次,折合QPS为1308;平均耗时时间为1.1ms;成功率为100%3.3 模型切换毛刺问题通过监控发现,当模型进行更新时,会有大量的请求超时。如下图所示,每次更新都会导致有大量请求超时,对系统的影响较大。通过TensorFlow Serving日志和代码分析发现,超时问题主要源于两个方面,一方面,更新、加载模型和处理TensorFlow Serving请求的线程共用一个线程池,导致切换模型时候无法处理请求;另一方面,模型加载后,计算图采用Lazy Initialization方式,导致第一次请求需要等待计算图初始化。模型切换导致请求超时问题一主要是因为加载和卸载模型线程池配置问题,在源代码中:uint32 num_load_threads = 0;uint32 num_unload_threads = 0;这两个参数默认为 0,表示不使用独立线程池,和Serving Manager在同一个线程中运行。修改成1便可以有效解决此问题。模型加载的核心操作为RestoreOp,包括从存储读取模型文件、分配内存、查找对应的Variable等操作,其通过调用Session的run方法来执行。而默认情况下,一个进程内的所有Session的运算均使用同一个线程池。所以导致模型加载过程中加载操作和处理Serving请求的运算使用同一线程池,导致Serving请求延迟。解决方法是通过配置文件设置,可构造多个线程池,模型加载时指定使用独立的线程池执行加载操作。对于问题二,模型首次运行耗时较长的问题,采用在模型加载完成后提前进行一次Warm Up运算的方法,可以避免在请求时运算影响请求性能。这里使用Warm Up的方法是,根据导出模型时设置的Signature,拿出输入数据的类型,然后构造出假的输入数据来初始化模型。通过上述两方面的优化,模型切换后请求延迟问题得到很好的解决。如下图所示,切换模型时毛刺由原来的84ms降低为4ms左右。优化后模型切换后,毛刺降低四、总结与展望本文主要介绍了用户增长组基于Tensorflow Serving在深度学习线上预估的探索,对性能问题的定位、分析、解决;最终实现了高性能、稳定性强、支持各种深度学习模型的在线服务。在具备完整的离线训练与在线预估框架基础之后,我们将会加快策略的快速迭代。在模型方面,我们可以快速尝试新的模型,尝试将强化学习与竞价结合;在性能方面,结合工程要求,我们会对TensorFlow的图优化、底层操作算子、操作融合等方面做进一步的探索;除此之外,TensorFlow Serving的预估功能可以用于模型分析,谷歌也基于此推出What-If-Tools来帮助模型开发者对模型深入分析。最后,我们也会结合模型分析,对数据、特征再做重新的审视。参考文献[1] Cheng, H. T., Koc, L., Harmsen, J., Shaked, T., Chandra, T., Aradhye, H., … & Anil, R. (2016, September). Wide & deep learning for recommender systems. In Proceedings of the 1st Workshop on Deep Learning for Recommender Systems (pp. 7-10). ACM.[2] Wang, R., Fu, B., Fu, G., & Wang, M. (2017, August). Deep & cross network for ad click predictions. In Proceedings of the ADKDD'17 (p. 12). ACM. [3] Guo, H., Tang, R., Ye, Y., Li, Z., & He, X. (2017). Deepfm: a factorization-machine based neural network for ctr prediction. arXiv preprint arXiv:1703.04247.[4] Lian, J., Zhou, X., Zhang, F., Chen, Z., Xie, X., & Sun, G. (2018). xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems. arXiv preprint arXiv:1803.05170.[5] Abadi, M., Barham, P., Chen, J., Chen, Z., Davis, A., Dean, J., … & Kudlur, M. (2016, November). TensorFlow: a system for large-scale machine learning. In OSDI (Vol. 16, pp. 265-283).[6] Goyal, P., Dollár, P., Girshick, R., Noordhuis, P., Wesolowski, L., Kyrola, A., … & He, K. (2017). Accurate, large minibatch SGD: training imagenet in 1 hour. arXiv preprint arXiv:1706.02677.[7] Neill, R., Drebes, A., Pop, A. (2018). Performance Analysis of Just-in-Time Compilation for Training TensorFlow Multi-Layer Perceptrons.作者简介仲达,2017年毕业于美国罗彻斯特大学数据科学专业,后在加州湾区Stentor Technology Company工作,2018年加入美团,主要负责用户增长组深度学习、强化学习落地业务场景工作。鸿杰,2015年加入美团点评。美团平台与酒旅事业群用户增长组算法负责人,曾就职于阿里,主要致力于通过机器学习提升美团点评平台的活跃用户数,作为技术负责人,主导了美团DSP广告投放、站内拉新等项目的算法工作,有效提升营销效率,降低营销成本。廷稳,2015年加入美团点评。在美团点评离线计算方向先后从事YARN资源调度及GPU计算平台建设工作。招聘美团DSP是美团在线数字营销的核心业务方向,加入我们,你可以亲身参与打造和优化一个可触达亿级用户的营销平台,并引导他们的生活娱乐决策。同时,你也会直面如何精准,高效,低成本营销的挑战,也有机会接触到计算广告领域前沿的AI算法体系和大数据解决方案。你会和美团营销技术团队一起推动建立流量运营生态,支持酒旅、外卖、到店、打车、金融等业务继续快速的发展。我们诚邀有激情、有想法、有经验、有能力的你,和我们一起并肩奋斗!参与美团点评站外广告投放体系的实现,基于大规模用户行为数据,优化在线广告算法,提升DAU,ROI, 提高在线广告的相关度、投放效果。欢迎邮件wuhongjie#meituan.com咨询。 ...

October 12, 2018 · 2 min · jiezi

高级 Angular 组件模式 (7)

07 使用 Content Directives原文: Use Content Directives因为父组件会提供所有相关的 UI 元素(比如这里的 button),所以 toggle 组件的开发者可能无法满足组件使用者的一些附加需求,比如,在一个自定义的开关控制元素上增加 aria 属性。如果 toggle 组件能够提供一些 hooks 方法或指令给组件使用者,这些 hooks 方法或指令能够在自定义的开关元素上设置一些合理的默认值,那将是极好的。目标提供一些 hooks 方法或指令给组件使用者,使其可以与所提供的 UI 元素交互并修改它们。实现我们通过实现一个 [toggler] 指令来负责向组件使用者提供的自定义元素增加 role=“switch” 和 aria-pressed 属性。这个 [toggler] 指令拥有一个 [on] input 属性(并与 <switch> 组件共享),该属性将决定 aria-pressed 属性的值是 true 还是 false。成果stackblitz演示地址译者注到这里已经是第七篇了,也许你已经发现,Angular 中很多开发模式或者理念,都和 Directive 脱不了干系。Angular 中其本身推崇组件化开发,即把一切 UI 概念当做 Component 来看待,但仔细思考的话,这其实是有前提的,即这个 UI 概念一般是由一个或多个 html 元素组成的,比如一个按钮、一个表格等。但是在前端开发中,小于元素这个颗粒度的概念也是存在的,比如上文提及的 aira 属性便是其中之一,如果也为将这些 UI 概念抽象化为一个组件,就未免杀鸡用牛刀了,因此这里使用 Directive 才是最佳实践,其官方文章本身也有描述,Directive 即为没有模板的 Component。从组件开发者的角度来看的话,Directive 也会作为一种相对 Component 更加轻量的解决方案,因为与其提供封装良好、配置灵活、功能完备(这三点其实很难同时满足)的 Component,不如提供功能简单的 Directive,而将部分其他工作交付组件使用者来完成。比如文章中所提及的,作为组件开发者,无法预先得知组件使用者会怎样管理开关元素以及它的样式,因此提供一些 hooks 是很有必要的,而 hooks 这个概念,一般情况下,都会是相对简单的,比如生命周期 hook、调用过程 hook、自定义属性 hook 等,在这里,我们通过 Directive 为自定义开关元素增加 aria 属性来达到提供自定义属性 hook 的目标。 ...

October 9, 2018 · 1 min · jiezi

鹅厂优文 | ReactJS一点通

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由鹅厂新鲜事儿发表于云+社区专栏作者:卢文喆 腾讯云 UI工程师导语 | 当React 刚开始红的时候,一直觉得 JSX 的设计思想极其独特,属于革命性的创新,它性能出众,代码逻辑却非常简单,所以,受到很多开发者的关注和使用,认为它可能是将来 Web 开发的主流工具。React 最早起源于 Facebook 的一个内部项目,因为公司对现有的 JavaScript MVC 框架都不满意,就决定自己开发一套,用来架设 Instagram 的网站。开发完成后,发现这套东西很好用,就在2013年5月开源了。那么 React 优势在哪里呢?首先:虚拟 DOM,在 DOM 树的状态需要发生变化时,虚拟 DOM 机制会将同一Event loop前后的 DOM树进行对比,如果两个 DOM 树存在不一样的地方,那么 React 仅仅会针对这些不一样的区域来进行响应的 DOM 修改,从而实现最高效的 DOM 操作和渲染。 比如,我们修改了 DOM 树上一些节点或 UI 组件对应绑定的 state,React 会即刻将其标记为“脏状态”,在一个 Event loop 结束时,React 会计算得出 DOM 树上需要修改的地方及其最终的状态,并仅仅针对这些地方进行一次性的重新渲染。于是好处显而易见,并非每修改一次组件的 state,就会重新渲染一次,而是在 Event loop 结束后做一次计算,减少冗余的 DOM 操作。另外 React 只针对需要修改的地方来做新的渲染,而非重新渲染整个 DOM 树,自然效率很高。其次:组件可嵌套,而且,可以模版化 —— 其实在 React 里提及的“组件”,常规是一些可封装起来、复用的 UI 模块,可以理解为“带有细粒度UI功能的部分DOM区域”。然后我们可以把这些组件层层嵌套起来使用,当然这样组件间会存在依赖关系。至于模块化,类似于 ejs 那样可以作为独立的模块被引用到页面上来复用,它可以直接把 UI 组件当作脚本模块那样来使用,完全可以配合 CommonJS、AMD、CMD 等规范来 require 需要的组件模块,并处理好它们的依赖关系。基于上述的两点,React 很自然的就获得一部分开发者的青睐。不过在这之前得先理清两件事情:1. React 是一个纯 View 层,不擅长于和动态数据打交道,因此它不同于,也替代不了常规的框架;2. React 很擅长于处理组件化的页面,在页面上搭组件的形式有点像搭积木一样,因此用上React的项目需求常规为界面组件化。简单点说,React组件应该具有如下特征:(1)可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的 UI 组件;(2)可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景;(3)可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护;组件化一直是网页开发的利器,许多开发者最希望能够最大程度的重复使用过去的开发的组件,避免重复造轮子。在 React 中组件就是一切,前端开发可能需要花点时间转变思维,尤其过去我们往往习惯将 HTML 、CSS 和 JavaScript 分离,现在却要把它们都封装在一起。以下是一般 React Component 书写的主要两种方式:1.使用 ES6 的 Class// 注意组件首字母需要大写class MyComponent extends React.Component { // render 是 Class based 元件唯一必須的方法(method) render() { return ( <div>Hello, World!</div> ); }}// 將 <MyComponent /> 组件插入 id 為 app 的 DOM 元素中ReactDOM.render(<MyComponent/>, document.getElementById(‘app’));2.使用 Functional Component 写法// 使用 arrow function 来设计 Functional Component 让 UI 设计更便捷,避免互相干扰(side effect)const MyComponent = () => ( <div>Hello, World!</div>);// 將 <MyComponent /> 组件插入 id 為 app 的 DOM 元素中ReactDOM.render(<MyComponent/>, document.getElementById(‘app’));前面说到 React 有独有的 JSX 语法,那么到底什么是 JSX 呢?JSX在ECMAScript的基础上提供了类似于XML的扩展。 JSX和HTML有点像,但也有不一样的地方。例如,HTML中的class属性在JSX中 为className。其他不一样的地方,你可以参考FB的HTML Tags vs. React Components 这篇文章。但是由于浏览器原生并不支持JSX,因此我们需要将其编译为JS,有很多方法能够 完成这个任务,后面我们会提到这些方法。此外,Babel也能够讲JSX编译为JS。一些参考资料:JSX in depthOnline JSX compilerBabel: How to use the react transformer一般而言 JSX 通常有两种使用方式:1.使用 browserify 或 webpack 等 CommonJS bundler 并整合 babel 预处理2.在浏览器端做解析请大家注意JSX的语法书写方式:<!DOCTYPE html><html> <head> <meta charset=“UTF-8” /> <title>Hello React!</title> <!– 請先载入 index.html 中引入 react.js, react-dom.js 和 babel-core 的 browser.min.js –> <script src=“https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.min.js"></script> <script src=“https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.min.js"></script> <script src=“https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script> </head> <body> <div id=“example”></div> <script type=“text/babel”> // 代码写在这里! ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById(’example’) ); </script> </body></html>一般载入 JSX 方式有:內嵌<script type=“text/babel”> ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById(’example’) );</script>从外部引入 <script type=“text/jsx” src=“main.jsx”></script> 总结:以上都是我对 React 简单的了解,包括 React 的优势、组件化的特征、React Component 的方法、以及 React 中为何要使用 JSX,以及 JSX 基本概念和用法。在 React 里,所有的事物都是以 Component 为基础,通常会将同一个 Component 相关的资源放在一起,而在撰写 React Component 时我们常会使用 JSX 的方式来提升书写效率。 JSX 是一种语法类似 XML 的 ECMAScript 语法扩充,可以发挥 JavaScript 的强大能力,放弃蹩脚的模板语言。当然 JSX 并非强制使用,你也可以选择不用,因为最终 JSX 的内容都会转化成 JavaScript。以上就是对 React 入门的部分理解。问答如何扩展React.js组件?相关阅读AI从入门到放弃:CNN的导火索,用MLP做图像分类识别?开发效率太低?您可能没看这篇文章微信车票背后的设计故事 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 1, 2018 · 2 min · jiezi

比官方文档更易懂的Vue.js教程!包你学会!

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦本文由蔡述雄发表于云+社区专栏蔡述雄,现腾讯用户体验设计部QQ空间高级UI工程师。智图图片优化系统首席工程师,曾参与《众妙之门》书籍的翻译工作。目前专注前端图片优化与新技术的探研。2016年,乃至接下来整个2017年,如果你要问前端技术框架什么最火,那无疑就是前端三巨头:React、Angular、Vue。没错,什么jQuery,seaJs,gulp等都逐渐脱离了热点。面试的时候不吹上一点新技术,好像自己就不是搞前端的似的。当然,希望大家都是抱着好学的心来开始一门学艺的,不管怎样,求求你,请接着看下去吧本系列文将会通过很多一目了然的例子和一个实战项目——组件库,来帮助大家学习Vue,一步一步来,毕竟这篇文章还有接下来的【升学篇】和【结业篇】呢。什么是Vue.js不管你想不想了解,你只需要大概知道,Vue就是和jQuery一样是一个前端框架,它的中心思想就是数据驱动,像远古时代的老前辈jQuery是结构驱动,什么意思呢,以前我们写代码时常用$(’.dom’).text(‘我把值改变了’),这种写法先要获得结构,然后再修改数据更新结构,而Vue的做法直接就是this.msg=“我改变了”,然后msg就会同步到某个结构上,视图管理抽象为数据管理,而不是管理dom结构了。不懂没关系,慢慢来。还有一点必须要知道的是,Vue是国人写的,技术文档也妥妥的是中文,想到这我就有学习的动力。搭建环境工欲善其事必先利其器,我们的学习计划从学会搭建Vue所需要的环境开始,node和npm的环境不用说是必须的,现在前端流程化很热门,基本上新的技术都会在这套流程的基础上做开发,我们只需要站在巨人的XX上装就可以了。我假设你的机子上已经有了最新的node和npm了,那我们就只需要执行以下命令:$ npm install -g vue-cli构建完了之后,随便进入一个我们事先准备好的目录,比如demo目录,然后在目录中做初始化操作:$ vue init webpack myProjectwebpack参数是指myProject这个项目将会在开发和完成阶段帮你自动打包代码,比如将js文件统一合成一个文件,将CSS文件统一合并压缩等。要是不知道webpack的话,建议先了解下为好,当然不了解也不影响我们接着往下走。init的过程中会问你给项目定义一些描述,版本之类的信息,可以不管,一直输入y确定跳过,完成之后出现以下界面,红框部分会提示你接下来要做的操作,按照它的提示继续敲代码就对了。cd myProjectnpm installnpm run devnpm install 是安装项目所需要的依赖,简单理解就是安装一些必要的插件,需要等一段时间;npm run dev 是开始执行我们的项目了,一旦执行这个命令之后,等一小会,浏览器应该会自动帮你打开一个tab为http://localhost:8080/#/的链接,这个链接就是我们本地开发的项目主页了,如果没有,说明出错了。请移步到评论区回复吧。。。(PS:开发完成后执行npm run build会编译我们的源代码生成最终的发布代码,在dist目录下)看看Vue都给我们生成一些什么文件,这其中我们需要关注的是以下文件package.json保存一些依赖信息,config保存一些项目初始化配置,build里面保存一些webpack的初始化配置,index.html是我们的首页,除了这些,最关键的代码都在src目录中,index在很多服务器语言中都是预设为首页,像index.htm,index.php等;打开build目录中的webpack.base.conf.js,会看到这样的代码说明我们的入口js文件在src目录中的main.js,接下来我们就分析下这些初始化代码先;跟着代码走Vue的核心架构,按照官方解释和个人理解,主要在于组件和路由两大模块,只要理解了这两大模块的思想内容,剩下API使用就只是分分钟的事情了。所以在我的系列文中,会围绕组件和路由教大家开发一个前端组件库,这个过程也是我个人学习的练手项目,个人觉得一步步做下来之后,对Vue的理解就可以算是出师了,胜过读10遍书籍文档,那是后话了,先让我们看看最基本的Vue生成的默认代码吧!// The Vue build version to load with the import command// (runtime-only or standalone) has been set in webpack.base.conf with an alias.import Vue from ‘vue’import App from ‘./App’import router from ‘./router’Vue.config.productionTip = false/ eslint-disable no-new /new Vue({ el: ‘#app’, router, template: ‘<App/>’, components: { App }})先是第一句import Vue from ‘vue’这句很好理解,就像你要引入jQuery一样,vue就是jquery-min.js,然后Vue就是$;然后又引入了./App文件,也就是目录中和main.js同级的App.vue文件;在Vue中引入文件可以直接用import,文件后缀名可以是.vue,这是Vue自己的文件类型,之前说的webpack会将js和css文件打包,同样的道理,在webpack中配置vue插件后(项目默认配置),webpack就可以将.vue类型的文件整合打包,这和nodeJs中require差不多的道理。说回App.vue这个文件,这是一个视图(或者说组件和页面),想象一下我们的index.html中什么也没有,只有一个视图,这个视图相当于一个容器,然后我们往这个容器中放各种各样的积木(其他组件或者其他页面),App.vue中的内容我们后面说;import router from ‘./router’这句代码引入一段路由配置,同样的后边说(很快就说到的不用急)接下来的 new Vue实例化,其实就相当于平时我们写js时候常用的init啦,然后声明el:’#app’,意思是将所有视图放在id值为app这个dom元素中,components表明引入的文件,即上述的App.vue文件,这个文件的内容将以<App/>这样的标签写进去#app中,总的来说,这段代码意思就是将App.vue放到#app中,然后以<App/>来指代我们的#app。import Vue from ‘vue’import App from ‘./App’/引入App这个组件/import router from ‘./router’/引入路由配置/Vue.config.productionTip = false/ eslint-disable no-new */new Vue({ el: ‘#app’,/最后效果将会替换页面中id为app的div元素/ router,/使用路由/ template: ‘<App/>’,/告知页面这个组件用这样的标签来包裹着,并且使用它/ components: { App }/告知当前页面想使用App这个组件/})单页面组件好了,现在打开我们的App.vue文件,在Vue中,官网叫它做组件,单页面的意思是结构,样式,逻辑代码都写在同一个文件中,当我们引入这个文件后,就相当于引入对应的结构、样式和JS代码,这不就是我们做前端组件化最想看到的吗,从前的asp、php也有这样的文件思想。<template> <div id=“app”> <img src="./assets/logo.png"> <router-view></router-view> </div></template><script>export default { name: ‘app’}</script><style>#app { font-family: ‘Avenir’, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;}</style>node端之所以能识别.vue文件,是因为前面说的webpack在编译时将.vue文件中的html,js,css都抽出来合成新的单独的文件。单页面组件会在后面的实战中完整体现,这里先不做过多描述;看到我们文件内分为三大部分,分别是<template><script><style>,很好理解结构,脚本,样式;script就像node一样暴露一些接口,可以看到我们的template标签中除了一张图片之外就只有一行代码:<router-view></router-view><template> <div id=“app”> <img src="./assets/logo.png"> <router-view></router-view> </div></template>回看我们的浏览器页面中,图片是有了,可下面的文本和链接的代码写在哪里了呢?这里就要开始涉及路由了。路由这里补充下路由的大致概念:传统的php路由是由服务器端根据一定的url规则匹配来返回给前端不同的页面代码,如以下地址https://isux.tencent.com/abou…注意这里只有about和recruit,这些不带xxx.html的地址其实是服务器端经过一层封装指定到某些文件上去。同样的道理,前端也可以根据带锚点的方式实现简单路由(不需要刷新页面)https://zhitu.isux.us/index.p…其中#mac就是我们的锚点路由,注意开始我们在浏览器中打开的地址:http://localhost:8080/#/,路由让我们可以访问诸如http://localhost:8080/#/about/ 或者 http://localhost:8080/#/recruit这些页面的时候不带刷新,直接展示。现在回到我们刚才打开的App.vue文件中看这行代码<router-view></router-view>这句代码在页面中放入一个路由视图容器,当我们访问http://localhost:8080/#/about/的时候会将about的内容放进去,访问http://localhost:8080/#/recruit的时候会将recruit的内容放进去如此看来,无论我们打开http://localhost:8080/#/about/ 还是http://localhost:8080/#/recruit页面中的图片都是公用部分,变得只是路由器里面的内容,那么路由器的内容谁来控制呢?前面说的src/main.js中有一句引入路由器的代码。import router from ‘./router’现在就让我们打开router目录下的js文件。import Vue from ‘vue’import Router from ‘vue-router’import Hello from ‘@/components/Hello’import About from ‘@/components/about’import Recruit from ‘@/components/recruit’Vue.use(Router)export default new Router({ routes: [ { path: ‘/’, name: ‘Hello’, component: Hello}, { path: ‘/about’, name: ‘about’, component: About}, { path: ‘/recruit’, name: ‘recruit’, component: Recruit} ]})前面先引入了路由插件vue-router,然后显式声明要用路由 Vue.use(Router) 。注意到Hello,About等都是页面(也可以是组件),接着注册路由器,然后开始配置路由。路由的配置应该一目了然,给不同的path分配不同的页面(或组件,页面和组件其实是一样的概念),name参数不重要只是用来做识别用的。看到这里就可以明白,前面说的红色框的内容,其实就是Hello里面的内容,打开components目录下的Hello.vue就能明白了。到这里就可以完成路由的配置,我个人习惯喜欢把页面放在pages目录下,组件放在components目录下,可能有人会问如果要访问http://localhost:8080/#/about/me的话要如何配置呢,很简单只要给路由加多一个子路由配置,如下:{ path: ‘/blog’, name: ‘blog’, component: Blog, children: [ { path: ‘/’, component: page1 }, { path: ‘info’, component: page2 } ] }访问/blog的时候会访问Blog页面,Blog里面放个路由器就可以了,然后访问http://localhost:8080/#/blog/的时候会往路由容器中放置page1的内容,访问http://localhost:8080/#/blog/info的时候会往路由容器中放置page2的内容//blog.vue<template> <div>公用部分</div> <router-view></router-view></template>小结贯穿我们刚才学习的过程,从初始化到页面展示,Vue的页面架构流程大概是这样的总结下前面讲的内容先:搭建环境代码逻辑单页面组件(简单带过)路由子路由以上的流程就是我们刚开始接触Vue时候的简单介绍,在之前就说过学习Vue能掌握组件和路由的基本概念之后,对于我们后续了解他的工作机制有着很大的帮助,本篇章我们只是简单介绍了单页面组件,在下一篇文章中,我们将通过一个实战的项目,来充分了解组件化在Vue构建中的重要性。时间不早了,我也该回去睡觉了,消化一下,我们下一篇文章见~~~文末附上所有相关代码和官方文档地址~~~http://cn.vuejs.org/v2/guide/附件:src.zip问答vue.js 怎么使用插件?相关阅读Vue.js 实战总结Angular和Vue.js 深度对比0基础菜鸟学前端之Vue.js 【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

September 26, 2018 · 1 min · jiezi