景初

景初 查看完整档案

天津编辑天津科技大学  |  计算机应用技术 编辑阿里巴巴  |  前端研发工程师 编辑 ifibercc.com 编辑
编辑

前端 / 数据挖掘 / CCIE / 炉石
ifibercc@gmail.com

个人动态

景初 收藏了文章 · 2020-01-03

移动端Web页面适配方案

更新:完整js代码和sass mixin已上传于gitHub,点击此处可获取

===================================================

移动端Web页面,即常说的H5页面、手机页面、webview页面等。

手机设备屏幕尺寸不一,做移动端的Web页面,需要考虑在安卓/IOS的各种尺寸设备上的兼容,这里总结的是针对移动端设备的页面,设计与前端实现怎样做能更好地适配不同屏幕宽度的移动设备。

适配的目标

引用一文章的描述:

在不同尺寸的手机设备上,页面“相对性的达到合理的展示(自适应)”或者“保持统一效果的等比缩放(看起来差不多)”。

概念理解

在做适配之前,需要先理解一些概念。对于不理解的地方,可以搜索更多文章看看,本文总结的也是摘抄了其他文章的描述,本文末有附相关链接。

viewport视口

viewport是严格的等于浏览器的窗口。viewport与跟viewport有关的meta标签的关系,详细建议读一读这篇文章:移动前端开发之viewport的深入理解viewport与布局的关系,可以看下这篇文章:在移动浏览器中使用viewport元标签控制布局

visual viewport 可见视口 屏幕宽度
layout viewport 布局视口 DOM宽度
ideal viewport 理想适口:使布局视口就是可见视口
设备宽度(visual viewport)与DOM宽度(layout viewport), scale的关系为:

  • (visual viewport)= (layout viewport)* scale

获取屏幕宽度(visual viewport)的尺寸:window. innerWidth/Height
获取DOM宽度(layout viewport)的尺寸:document. documentElement. clientWidth/Height

设置理想视口:把默认的layout viewport的宽度设为移动设备的屏幕宽度,得到理想视口(ideal viewport):

<meta name="viewport" content="width=device-width,initial-scale=1">

物理像素(physical pixel)

物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。所谓的一倍屏、二倍屏(Retina)、三倍屏,指的是设备以多少物理像素来显示一个CSS像素,也就是说,多倍屏以更多更精细的物理像素点来显示一个CSS像素点,在普通屏幕下1个CSS像素对应1个物理像素,而在Retina屏幕下,1个CSS像素对应的却是4个物理像素。关于这个概念,看一张"田"字示意图就会清晰了。

CSS像素

CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。CSS像素顾名思义就是我们写CSS时所用的像素。

设备像素比dpr(device pixel ratio)

设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:

设备像素比 = 物理像素 / 设备独立像素

在Retina屏的iphone上,devicePixelRatio的值为2,也就是说1个css像素相当于2个物理像素。通常所说的二倍屏(retina)的dpr是2, 三倍屏是3。

viewport中的scale和dpr是倒数关系。
获取当前设备的dpr:

  • JavaScript: window.devicePixelRatio
  • CSS: -webkit-device-pixel-ratio, -webkit-min-device-pixel-ratio, -webkit-max-device-pixel-ratio。不同dpr的设备,可根据此做一些样式适配(这里只针对webkit内核的浏览器和webview)。

设备独立像素dip与设备像素dp

dip(device independent pixels,设备独立像素)与屏幕密度有关。dip可以用来辅助区分视网膜设备还是非视网膜设备。
dp(device pixels, 设备像素)。

谢谢@游鱼与鱼指出:dp与dip是有区别的

安卓设备根据屏幕像素密度可分为ldpi、mdpi、hdpi、xhdpi等不同的等级。规定以160dpi为基准,1dp=1px。如果密度是320dpi,则1dp=2px,以此类推。
IOS设备:从IPhone4开始为Retina屏

  • CSS像素与设备独立像素之间的关系依赖于当前的缩放等级。

屏幕像素密度PPI(pixel per inch)

屏幕像素密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。

屏幕密度 = 对角线分辨率/屏幕尺寸

概念关系图

屏幕尺寸、屏幕分辨率-->对角线分辨率/屏幕尺寸-->屏幕像素密度PPI
                                             |
              设备像素比dpr = 物理像素 / 设备独立像素dip(dp)
                                             |
                                       viewport: scale
                                             |
                                          CSS像素px

CSS像素与设备像素“田字图解”
屏幕尺寸示意图

前端实现相关方式

下面大致列下前端在实现适配上常采用的方式。百分比、em单位的使用就不必说了。

viewport

设置理想视口

<meta name="viewport" content="width=width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

设置理想视口,使得DOM宽度(layout viewport)与屏幕宽度(visual viewport)一样大,DOM文档主宽度即为屏幕宽度。1个CSS像素(1px)由多少设备像素显示由具体设备而不同。

动态设置视口缩放为1/dpr

对于安卓,所有设备缩放设为1,对于IOS,根据dpr不同,设置其缩放为dpr倒数。设置页面缩放可以使得1个CSS像素(1px)由1个设备像素来显示,从而提高显示精度;因此,设置1/dpr的缩放视口,可以画出1px的边框。

不管页面中有没有设置viewport,若无,则设置,若有,则改写,设置其scale为1/dpr。

(function (doc, win) {
  var docEl = win.document.documentElement;
  var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var dpr = 0;
  var scale = 0;

  // 对iOS设备进行dpr的判断,对于Android系列,始终认为其dpr为1
  if (!dpr && !scale) {
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/[iphone|ipad]/gi);
    var devicePixelRatio = win.devicePixelRatio;

    if(isIPhone) {
      dpr = devicePixelRatio;
    } else {
      drp = 1;
    }
    
    scale = 1 / dpr;
  }

  /**
    * ================================================
    *   设置data-dpr和viewport
    × ================================================
    */

  docEl.setAttribute('data-dpr', dpr);
  // 动态改写meta:viewport标签
  if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
    document.documentElement.firstElementChild.appendChild(metaEl);
  } else {
    metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
  }

})(document, window);

px单位的适配

设置动态缩放视口后,在iPhone6上,缩放为0.5,即CSS像素2px最终显示效果为1px,而在scale=1的设备,CSS像素1px显示效果为1px,那么,为了达到显示效果一致,以px为单位的元素(比如字体大小),其样式应有适配不同dpr的版本,因此,在动态设置viewport: scale的时候,同时在html根元素上加上data-dpr=[dpr]但是这种方式还是不够,如果dpr为2,3之外的其他数值,px就没办法适配到。因此我会选择都用rem为单位进行适配。
样式示例:

.p {
    font-size: 14px;

  [data-dpr="2"] & {
    font-size: 14px * 2;
  }

  [data-dpr="3"] & {
    font-size: 14px * 3;
  }
}

为写样式方便,可以借助sass的mixin写代码片段:

// 适配dpr的字体大小
@mixin font-dpr($font-size){
  font-size: $font-size;

  [data-dpr="2"] & {
      font-size: $font-size * 2;
  }

  [data-dpr="3"] & {
      font-size: $font-size * 3;
  }
}
@mixin px-dpr($property, $px) {
  #{$property}: $px;

  [data-dpr="2"] & {
    #{$property}: $px * 2;
  }

  [data-dpr="3"] & {
    #{$property}: $px * 3;
  }
}

// 使用
@include font-dpr(14px);
@include px-dpr(width, 40px); @include px-dpr(height, 40px);

设置缩放视口与设置理想视口有什么不同

问题:viewport设为理想视口(scale=1),基本已经满足适配,为什么要动态设置viewport缩放?
原因:iPhone6为例,dpr为2,缩放设为0.5,则DOM宽度为750,缩放后显示刚好为屏幕宽度375,而总的CSS像素其实是750,与设备像素一致,这样1px的CSS像素,占用的物理像素也是1;而viewport设置缩放为1的理想视口情况下,DOM宽度为375,显示也刚好是屏幕宽度,然而1px的CSS像素,占用的物理像素是2。这样说来,这样设置可以实现1px的线条在二倍屏的显示。因为: CSS像素与设备像素的关系依赖于屏幕缩放。
验证:设备:iPhone6,
在scale=0.5时,1px边框显示效果;
在scale=1.0时,1px边框显示效果;
在scale=0.5时,2px边框显示效果;
通过对比后发现,在scale=0.5时,1px的线比scale=1.0要细,这也就解决了1px线条的显示问题。

rem(一个CSS单位)

定义:font size of the root element.

这个单位的定义和em类似,不同的是em是相对于父元素,而rem是相对于根元素。rem定义是根元素的font-size, 以rem为单位,其数值与px的关系,需相对于根元素<html>的font-size计算,比如,设置根元素font-size=16px, 则表示1rem=16px。关于rem更多的解读,建议可以阅读本文末附的腾讯一团队的文章《web app变革之rem》。

根据这个特点,可以根据设备宽度动态设置根元素的font-size,使得以rem为单位的元素在不同终端上以相对一致的视觉效果呈现。

选取一个设备宽度作为基准,设置其根元素大小,其他设备根据此比例计算其根元素大小。比如使得iPhone6根元素font-size=16px。

设 备设备宽度根元素font-size/px宽度/rem
iPhone5320js计算所得--
iPhone63751623.4375
i6 Plus414js计算所得--
-360js计算所得--
根元素fontSize公式:width/fontSize = baseWidth/baseFontSize

其中,baseWidth, baseFontSize是选为基准的设备宽度及其根元素大小,width, fontSize为所求设备的宽度及其根元素大小

动态设置根元素fontSize

/**
  * 以下这段代码是用于根据移动端设备的屏幕分辨率计算出合适的根元素的大小
  * 当设备宽度为375(iPhone6)时,根元素font-size=16px; 依次增大;
  * 限制当为设备宽度大于768(iPad)之后,font-size不再继续增大
  * scale 为meta viewport中的缩放大小
  */
(function (doc, win) {
  var docEl = win.document.documentElement;
  var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
  /**
    * ================================================
    *   设置根元素font-size
    * 当设备宽度为375(iPhone6)时,根元素font-size=16px; 
    × ================================================
    */
  var refreshRem = function () {
    var clientWidth = win.innerWidth
                      || doc.documentElement.clientWidth
                      || doc.body.clientWidth;

    console.log(clientWidth)
    if (!clientWidth) return;
    var fz;
    var width = clientWidth;
    fz = 16 * width / 375;
    docEl.style.fontSize = fz + 'px';
  };

  if (!doc.addEventListener) return;
  win.addEventListener(resizeEvt, refreshRem, false);
  doc.addEventListener('DOMContentLoaded', refreshRem, false);
  refreshRem();

})(document, window);

rem计算(px2rem)

对于需要使用rem来适配不同·屏幕的元素,使用rem来作为CSS单位,为了方便,可以借助sass写一个函数计算px转化为rem, 写样式时不必一直手动计算。sass函数的使用若不熟悉可看下这篇文章:如何编写自定义Sass 函数, 也可以使用sass的mixin来写,个人觉得用函数写更适合。

/* 
 * 此处 $base-font-size 具体数值根据设计图尺寸而定
 * flexible中设置的标准是【fontSize=16px, when 屏幕宽度=375】,因此,按此标准进行计算,
 * 若设计图为iPhone6(375*667)的二倍稿,则$base-font-size=32px
 *
 */ 
@function px2rem($px, $base-font-size: 32px) {
  @if (unitless($px)) {
    @warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
    @return px2rem($px + 0px); // That may fail.
  } @else if (unit($px) == rem) {
    @return $px;
  }
  @return ($px / $base-font-size) * 1rem;
}

// 使用,eg:
font-size: px2rem(18px);

问题思考

我之前一直在想一个问题,选取哪个设备来做基准、屏幕宽度等分为多少比较合适,设计图给多大宽度的版本?被选取作为基准的设备,应当就是前端需要设计提供的设计图版本,这样可以避免一些尺寸上的纠缠,而等分为多少等分,除了考虑方便设计,是否需要考虑其他问题?对于根元素font-size没有手动设置的情况,1rem究竟等于多少?

了解到的一些事实:

  • 某些Android设备会丢掉 rem 小数部分(具体是哪些设备,搜到的文章中没有说),那么1rem对应的px少些,在这些安卓设备上显示误差就会较小,当然如果不存在会丢掉小数这个问题,这一说也就不必考虑了。
  • 未设置font-size情况下,1rem的大小具体看浏览器的实现,默认的根元素大小是font-size=16px
  • 目前一般会选取iPhone6作为基准,设计图便要iPhone6的二倍图
  • 当动态缩放视口为1/dpr, 计算所得的根元素fontSize也会跟着缩放,即若理想视口(scale=1), iPhone6根元素fontSize=16px; 若scale=0.5, iPhone6根元素fontSize=32px; 因此设置视口缩放应放于设置根元素fontSize之前。

flex布局

flex布局对于屏幕适配也很有帮助,有些地方通过flex布局的实现方式,效果会比较合理。
关于flex布局,暂时不了解的建议阅读阮一峰老师的教程,分语法和实践两篇,讲得很清晰易懂实用。
Flex布局教程:语法篇

vm/vh:CSS单位

vw(view-width), vh(view-height) 这两个单位是CSS新增的单位,表示视区宽度/高度,视区总宽度为100vw, 总高度为100vh。

视区指浏览器内部的可视区域大小:window.innerWidth/Height

一些问题

upsampling/downsampling

DownSampling: 大图放入比图片尺寸小的容器中时,出现像素分割成就近色

不同scale显示同一图片基本无问题;
同一sacle,不同倍数图,存在色差(Downsampling)

关于这个我还不是很了解,暂时记一下。

手淘的实现方案

下面主要根据我的理解摘录了手淘公开的实现方案,详细可以去gitHub搜索查看,文末也附了链接。
图解设计与前端协作方案:图:手机淘宝团队适配协作模式
[淘宝手淘团队h5页面终端适配开源库:lib-flexible]()

方案关键点:

  • 动态改写<meta name="viewport">标签
  • 给<html>元素添加data-dpr属性,并且动态改写data-dpr的值
  • 给<html>元素添加font-size属性,并且动态改写font-size的值

通过一段JS代码根据设备的屏幕宽度、dpr设置根元素的data-dpr和font-size, 这段JS代码要在所有资源加载之前执行,建议做内联处理。

各种元素(文本、图片)处理方案参考:
图:怎样让你的网站适应视网膜分辨率

px转rem的mixin

// 使用sass的混合宏
// 淘宝手淘的方案里,i6(375pt)屏幕宽度为10rem,即font-size=75px, scale=0.5 因设计图为二倍图,$base-font-size=75px
@mixin px2rem($property,$px-values,$baseline-px:16px,$support-for-ie:false){
    //Conver the baseline into rems
    $baseline-rem: $baseline-px / 1rem * 1;
    //Print the first line in pixel values
    @if $support-for-ie {
        #{$property}: $px-values;
    }
    //if there is only one (numeric) value, return the property/value line for it.
    @if type-of($px-values) == "number"{
        #{$property}: $px-values / $baseline-rem;
    }
    @else {
        //Create an empty list that we can dump values into
        $rem-values:();
        @each $value in $px-values{
            // If the value is zero or not a number, return it
            @if $value == 0 or type-of($value) != "number"{
                $rem-values: append($rem-values, $value / $baseline-rem);
            }
        }
        // Return the property and its list of converted values
        #{$property}: $rem-values;
    }
}

小结

  • 适配不同屏幕宽度以及不同dpr,通过动态设置viewport(scale=1/dpr) + 根元素fontSize + rem, 辅助使用vw/vh等来达到适合的显示;
  • 若无需适配可显示1px线条,也可以不动态设置scale,只使用动态设置根元素fontSize + rem + 理想视口;
  • 当视口缩放,计算所得的根元素fontSize也会跟着缩放,即若理想视口(scale=1), iPhone6根元素fontSize=16px; 若scale=0.5, iPhone6根元素fontSize=32px; 因此不必担心rem的计算;
  • !!css单位:以前我认为这样比较好:适配元素rem为单位,正文字体及边距宜用px为单位;现在认为全部用rem即可,包括字体大小,不用px
  • px为单位的元素,需根据dpr有不同的大小,如大小12px, dpr=2则采用24px, 使用sass mixin简化写法;
  • 配合scss函数,简化px2rem转换,且易于维护(若需修改$base-font-size, 无需手动重新计算所有rem单位);
  • px2rem函数的$base-font-size只跟根元素fontSize的基准(此文中是【fontSize=16px when 375】)以及设计图的大小有关,按此基准,若设计图为iPhone6二倍稿,则$base-font-size=32px,参数传值直接为设计图标注尺寸;
  • 使用iPhone6(375pt)二倍设计图:宽度750px;
  • 切图使用三倍精度图,以适应三倍屏(这个目前我还没有实际应用过)

相关链接:

查看原文

景初 赞了文章 · 2017-10-16

剖析Vue原理&实现双向绑定MVVM

本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm

相信大家对mvvm双向绑定应该都不陌生了,一言不合上代码,下面先看一个本文最终实现的效果吧,和vue一样的语法,如果还不了解双向绑定,猛戳Google

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>

<script data-original="./js/observer.js"></script>
<script data-original="./js/watcher.js"></script>
<script data-original="./js/compile.js"></script>
<script data-original="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

效果:
图片描述

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

上述流程如图所示:
图片描述

1、实现Observer

ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,let's do it
我们知道可以利用Obeject.defineProperty()来监听属性变动
那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。。相关代码可以是这样:

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
图片描述

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attrv-text便是指令,而other-attr不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
如果有点乱,可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

ok, Watcher也已经实现了,完整代码
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈

至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

最后,感谢您的阅读!

查看原文

赞 1344 收藏 1806 评论 152

景初 回答了问题 · 2017-04-17

解决VS code 保存时自动格式化的问题

正确答案在这里
是JS-CSS-HTML Formatter在搞鬼,但又不能删掉他,解决措施是

  1. 按F1

  2. 输入Formatter Config

  3. 第一行加入 "onSave": false,

  4. 保存,搞定

关注 9 回答 4

景初 赞了回答 · 2017-04-10

vs code 修改 emmet 兼容JSX 文件

"emmet.syntaxProfiles": { "javascript": "html" },

// 更推荐下面的方式:支持 className

"emmet.syntaxProfiles": { "javascript": "jsx" },

关注 6 回答 4

景初 赞了文章 · 2016-12-27

前后端分离开发模式的 mock 平台预研

原文地址

引入

mock(模拟): 是在项目测试中,对项目外部或不容易获取的对象/接口,用一个虚拟的对象/接口来模拟,以便测试。

背景

前后端分离

  • 前后端仅仅通过异步接口(AJAX/JSONP)来编程

  • 前后端都各自有自己的开发流程,构建工具,测试集合

  • 关注点分离,前后端变得相对独立并松耦合

前后端分离.png

开发流程

  • 后台编写和维护接口文档,在 API 变化时更新接口文档

  • 后台根据接口文档进行接口开发

  • 前端根据接口文档进行开发

  • 开发完成后联调和提交测试

开发流程.png

面临问题

  • 没有统一的文档编写规范,导致文档越来越乱,无法维护和阅读

  • 开发中的接口增删改等变动,需要较大的沟通成本

  • 对于一个新需求,前端开发的接口调用和自测依赖后台开发完毕

  • 将接口的风险后置,耽误项目时间

解决方法

  • 接口文档服务器 -- 解决接口文档编辑和维护的问题

  • mock 数据 -- 解决前端开发依赖真实后台接口的问题

接口文档服务器

功能

接口编辑功能

接口书写转换为接口文档.png

  • 类型2:提供在线的接口编辑平台,进行可交互的接口编辑
    接口文档服务器.png

接口查看功能

  • 提供友好的接口文档查看功能

用法

  • 后台开发人员进行接口文档编写
    -- 定义接口路径、接口上传字段、接口返回字段、字段含义、字段类型、字段取值

  • 前端开发人员查看接口文档

优点

  • 统一管理和维护接口文档
    -- 提供了接口导入、接口模块化、接口版本化、可视化编辑等功能

  • 接口文档规范,可读性强,减少前后端接口沟通成本

前端 mock 方法回顾

前端开发过程中,使用 mock 数据来模拟接口的返回,对开发的代码进行业务逻辑测试。解决开发过程中对后台接口的依赖。

硬编码数据

将 mock 数据写在代码中。

示例

// $.ajax({
//   url: ‘https://cntchen.github.io/userInfo’,
//   type: 'GET',
//   success: function(dt) {
    var dt = {
      "isSuccess": true,
      "errMsg": "This is error.",
      "data": {
        "userName": "Cntchen",
        "about": "FE"
      },
    };
    if (dt.isSuccess) {
      render(dt.data);
    } else {
      console.log(dt.errMsg);
    }
//   },
//   fail: function() {}
// });

优点

  • 可以快速修改测试数据

痛点

  • 无法模拟异步的网络请求,无法测试网络异常

  • 肮代码,联调前需要做较多改动,增加最终上真实环境的切换成本
    -- 添加网络请求,修改接口、添加错误控制逻辑

  • 接口文档变化需要手动更新

请求拦截 & mock 数据

hijack(劫持)接口的网络请求,将请求的返回替换为代码中的 mock 数据。

实例

jquery-mockjax

The jQuery Mockjax Plugin provides a simple and extremely flexible interface for mocking or simulating ajax requests and responses

优点

  • 可以模拟异步的网络请求

  • 可以快速修改测试数据

痛点

  • 依赖特定的框架,如Jquery

  • 增加最终上真实环境的切换成本

  • 接口文档变换需要手动更新

本地 mock 服务器

将 mock 数据保存为本地文件。在前端调试的构建流中,用 node 开本地 mock 服务器,请求接口指向本地 mock 服务器,本地 mock 服务器 response mock 文件。

mock 文件

.mock
├── userInfo.json
├── userStars.json
├── blogs.json
└── following.json

接口调用

https://github.com/CntChen/userInfo --> localhost:port/userInfo

优点

  • 对代码改动较小,联调测试只需要改动接口 url

  • 可以快速修改测试数据

痛点

  • json 文件非常多

  • 接口文档变化需要手动更新

代理服务器

  • 使用 charlesfiddler 作为代理服务器

  • 使用代理服务器的 map(映射)& rewrite(重写) 功能

map local

  • 接口请求的返回映射为本地 mock 数据
    https://github.com/CntChen/userInfo --> localPath/userInfo

map local.png

  • 编辑映射规则
    map rule.png

map remote

  • 接口请求的返回映射为另一个远程接口的调用
    map remote.png

rewrite

  • 修改接口调用的 request 或 response,添加/删除/修改 HTTP request line/response line/headers/body
    rewrite data.png

  • 解决跨域问题
    使用 map 后,接口调用的 response 不带 CORS headers,跨域请求在浏览器端会报错。需要重写接口返回的 header,添加 CORS 的字段。

rewrite cors.png

优点

  • 前端直接请求真实接口,无需修改代码

  • 可以修改接口返回数据

痛点

  • 需要处理跨域问题

  • 一个变更需要代理服务器进行多处改动,配置效率低下

  • 不支持 HTTP method 的区分
    -- CORS 的 preflight 请求(OPTION)也会返回数据

  • 需要有远程接口或本地 mock 文件,与使用本地 mock 文件相同的痛点

mock 平台

接口文档服务器

使用接口文档服务器来定义接口数据结构

接口服务器.jpg

mock服务器

mock 服务器根据接口文档自动生成 mock 数据,实现了接口文档即API

mock服务器.jpg

优点

  • 接口文档自动生成和更新 mock 数据

  • 前端代码联调时改动小

缺点

  • 可能存在跨域问题

业界实践

公司实践

没有找到公司级别的框架,除了阿里的 RAP。可能原因:

  • 非关键性、开创性技术,没有太多研究价值

  • 许多大公司是小团队作战,没有统一的 mock 平台

  • 已经有一些稳定的接口,并不存在后台接口没有开发完成的问题
    -- 而我们想探究的问题是:前后端同时开发时的 mock

github 开源库

  • faker.js
    随机生成固定字段的 mock 数据,如emaildateimages等,支持国际化。

  • blueprint

A powerful high-level API design language for web APIs.

一种使用类markdown语法的接口编写语言,使用json-schema和mson作为接口字段描述。有完善的工具链进行接口文件 Edit,Test,Mock,Parse,Converter等。

Swagger是一种 Rest API 的简单但强大的表示方式,标准的,语言无关,这种表示方式不但人可读,而且机器可读。可以作为 Rest API 的交互式文档,也可以作为 Rest API 的形式化的接口描述,生成客户端和服务端的代码。 --Swagger:Rest API的描述语言

定义了一套接口文档编写语法,然后可以自动生成接口文档。相关项目: Swagger Editor ,用于编写 API 文档。Swagger UI restful 接口文档在线自动生成与功能测试软件。点击查看Swagger-UI在线示例

WireMock is a simulator for HTTP-based APIs. Some might consider it a service virtualization tool or a mock server. It supports testing of edge cases and failure modes that the real API won't reliably produce.

商业化方案

  • apiary
    商业化方案,blueprint开源项目的创造者。界面化,提供mock功能,生成各编程语言的调用代码(跟 postman 的 generate code snippets类似)。

其他实践

API Evangelist(API 布道者)

总结

对于前后端分离开发方式,已经有比较成熟的 mock 平台,主要解决了2个问题:

  • 接口文档的编辑和维护

  • 接口 mock 数据的自动生成和更新

后记

预研时间比较有限,有一些新的 mock 模式或优秀的 mock 平台没有覆盖到,欢迎补充。
笔者所在公司选用的平台是 RAP,后续会整理一篇 RAP 实践方面的文章。
问题来了:你开发中的 mock 方式是什么?

References

  • 图解基于node.js实现前后端分离

http://yalishizhude.github.io...

  • TestDouble(介绍 mock 相关的概念)

http://martinfowler.com/bliki...

  • There Are Four API Design Editors To Choose From Now

https://apievangelist.com/201...

  • 联调之痛--契约测试

http://www.ituring.com.cn/art...

  • Swagger:Rest API的描述语言

https://zhuanlan.zhihu.com/p/...

  • Swagger - 前后端分离后的契约

http://www.cnblogs.com/whitew...

  • Swagger UI教程 API 文档神器 搭配Node使用

http://www.jianshu.com/p/d662...

END


查看原文

赞 14 收藏 35 评论 9

景初 赞了文章 · 2016-12-27

前端的 mock server

在一个中大型项目中,你不可能一边写着前端一边写后端。全栈太难 :)

像rails那样的开发模式已经很不适合当前的环境了。所有的项目都嚷嚷着前后端分离,那就只能这么干

我之前在做大学狗们的时候,在mock数据这一块曾经特别难受

虽说整个前后端我都能掌控,但是因为整个前端是一个repo,后端又是一个,我在开发的时候又不能开着两个编辑器(有一段时间这么干过),而且十分不想在自己电脑上安装那么多东西。

一开始的解决方案很扯淡:

后端Mock方案

不想在自己电脑上安装那就连远端服务器吧。反正学生优惠的Server超级便宜,而且再开个二级域名没有任何损失。

说干就干。

在远端开发服务器上先把后端拉下来,搞数据库。是laravel做的,所以mock数据也还是挺轻松的。整个一套弄下来了。

然后给Nginx加上跨域的header。

好了,到这里服务端就完成了。

虽然很不舒服,但还是能忍受对吧。然而扯淡的在前端

前端要发请求,所以每个请求的url都是http://dev.foo.com/,而生产环境服务器又是http://www.bar.com/

我想出了一个"聪明"的法子:在所有请求前面加上一个prefix,在dev环境就设置成http://dev.foo.com/, 生产环境就改过来。这样所有请求的prefix就是个变量,在release之前替换一下就可以了!

天才!

就像是这样

// release前修改
const prefix = 'http://dev.foo.com/'

// 其他文件中
fetch(`${prefix}/api/users`).then(res => res.json()).then(data => todo(data))

然后我改字符串的时候就哭了☹ 如果你愿意读一下源码,你会体会到我当时崩溃的心情,这里还残留着这个方案的痕迹(要改太多地方了)

不过说真的,虽然这套方案问题相当大,然而它确实是有用的,支撑了我好几个月。

难以忍受这套方案的同时我也在寻找好的解决方案。

前端Mock方案

因为我是在校生嘛,没办法了解到大公司的开发方式。在这个痛点发生以后就一直关注这方面的内容。

我一直想在webpack-dev-server这边做个中间层,把这个server做成完整后端那种的,包含路由什么的,直接返回json。

因为一直考虑其他的事情,一直拖着没做,另外也觉得webpack这套东西好像也有点儿复杂,不太愿意碰。

其实还有个问题,我相信mock这一块大公司肯定碰到的比我早,为什么我没有搜索到这样的包?是他们不愿意这么做还是有更好的解决方案?

最近总算是找到了个还算靠谱的一套方案,流程是这样的:

首先开一个mock server,只有路由功能,返回假数据。

在webpack-dev-server中加上proxy,把对server的请求都转发给proxy,不存在跨域的东西,可以很逼真的模拟。

这套方案就很棒,完全不用修改请求url。

clipboard.png

说干就干:

$ npm install --save faker
$ npm install -g json-server

在项目目录下创建mock目录,然后做路由和数据

// mock/db.js
'use strict'
const faker = require('faker')

module.exports = function() {
    let data = {
        'activity': [
            {
                id: 0,
                title: faker.lorem.words(),
                img: faker.image.image()
            }
        ]
    }
    return data
}

路由文件,主要把对/api/*的请求转到/*,主要是简单一些

{
    "/api/": "/"
}

然后把这个mock server 起一下吧

$ json-server mock/db.js --routes mock/routes.json --port 9999

剩下的是webpack那边的配置了。核心是这些:

const config = require('./webpack.config')
config.devServer = {
    hot: true,
    inline: true,
    proxy: {
        '/api/*': {
            target: 'http://127.0.0.1:9999',
            secure: false
        }
    }
}
module.exports = config

好了,配置也可以了。

$ webpack-dev-server --process --colors --hot --inline --devtool eval --config webpack.dev.config.js

所有的事情都做完了,只剩下测试了

找个入口文件测试一下:

fetch('/api/activity').then(res => res.json()).then(data => console.log(data))

ok。把我折腾了这么几个月的前后端总算是彻底分开了

问题

这套流程的最大问题在于json-server这么个东西,因为是纯粹的RESTful的server。同样是上面的配置为例:

GET /api/activity
POST /api/activity {title: 'foo', image: '/foo.jpg'}
PUT /api/activity/1 {title: 'bar', image: '/bar.jpg'}
DELETE /api/activity/1

对RESTful有了解就明白了,分别对应的是获取, 创建, 更新, 删除操作

当然还有更多的json-server的设置,比如查询,关系什么的,底下我会给链接。

这些东西可以说设计是很不错的。然而也是问题。

老系统完全不能用。或者设计不够好的系统根本不能用。

可能后端就任性!就不遵守REST API,那么这个前端mock只能靠routes.json来调整,然而更多的情况是没办法调整的。

所以啊,这个mock server方案对后端要求很严格

References

原文:前端的 mock server

查看原文

赞 3 收藏 17 评论 2

景初 发布了文章 · 2016-11-28

vue实现表格合并

1. 场景

这两天一个项目,属于子需求吧,就是要做一个页面放个简单的banner下面是张大表格用来显示数据项,纯粹为了view层操作方便,就用了vue做渲染。
然而,对方最近又提出了一个恶心需求,需要相邻的相同值的行数据项进行单元格合并,这就醉了。

由于使用的是vue,想到MVVM是要用数据驱动的思想,所以考虑在Model做手脚,而不是渲染出数据来后做DOM操作,当然基本的CSS还是要有的。因此这个方法对所有
数据驱动的框架都有效,比如说Angular和React。最后的实现效果是这样的:
图片描述

2. 思路

原本的正常表格的代码长这样:

<tr v-for="item in items">
    <td width="3%">{{ $index + 1 }}</td>
    <td width="15%">{{item.bsO_Name}}</td>
    <td width="8%" :class="{'overtime': overtime(item.GathDt)}">{{item.GathDt | time}}</td>
    <td width="5%">{{item.F1}}</td>
    <td width="5%">{{item.F2}}</td>
    <td width="5%">{{item.F4}}</td>
    <td width="5%">{{item.F3}}</td>
    <td width="5%">{{item.F5}}</td>
    <td width="5%">{{item.F6}}</td>
    <td width="5%">{{item.F7}}</td>
    <td width="5%">{{item.F8}}</td>
    <td width="5%">{{item.F9}}</td>
    <td width="5%">{{item.F10}}</td>
</tr>

先拿正常的表格来做测试,原生的<td>标签就有rowspan属性支持单元格行合并,属性值指的是向下合并多少行,其实就相当于在本行中向下又添加了几个单元格。
因为,如果接下来的一行还会渲染的话就会被挤下去,因此,下面被合并的单元格要隐藏掉,通过display: none;css控制即可。

因此,每个<td>标签需要带有两个属性值,rowspandisplay来控制每一个单元格的合并行数和是否显示。代码变成这样了

<tr v-for="item in items">
    <td width="3%">{{ $index + 1 }}</td>
    <td width="10%" :rowspan="item.bsO_Namespan" :class="{hidden: item.bsO_Namedis}">{{item.bsO_Name}}</td>
    <td width="8%"  :rowspan="item.GathDtspan"   :class="{hidden: item.GathDtdis}" :class="{overtime: overtime(item.GathDt)}">{{item.GathDt | time}}</td>
    <td width="5%"  :rowspan="item.F1span"       :class="{hidden: item.F1dis}">{{item.F1}}</td>
    <td width="5%"  :rowspan="item.F2span"       :class="{hidden: item.F2dis}">{{item.F2}}</td>
    <td width="5%"  :rowspan="item.F3span"       :class="{hidden: item.F3dis}">{{item.F3}}</td>
    <td width="5%"  :rowspan="item.F4span"       :class="{hidden: item.F4dis}">{{item.F4}}</td>
    <td width="5%"  :rowspan="item.F5span"       :class="{hidden: item.F5dis}">{{item.F5}}</td>
    <td width="10%" :rowspan="item.F6span"       :class="{hidden: item.F6dis}">{{item.F6}}</td>
    <td width="8%"  :rowspan="item.F7span"       :class="{hidden: item.F7dis}" :class="{overtime: overtime(item.F7)}">{{item.F7 | time}}</td>
    <td width="5%"  :rowspan="item.F8span"       :class="{hidden: item.F8dis}">{{item.F8}}</td>
    <td width="5%"  :rowspan="item.F9span"       :class="{hidden: item.F9dis}">{{item.F9}}</td>
    <td width="5%"  :rowspan="item.F10span"      :class="{hidden: item.F10dis}">{{item.F10}}</td>
    <td width="5%"  :rowspan="item.F11span"      :class="{hidden: item.F11dis}">{{item.F11}}</td>
</tr>

其中,这两个属性有一些特征:

  • 要显示的单元格rowspan为>1的值,记录接下来的行数

  • 要显示的单元格display为true

  • 接下来不显示的单元格rowspan为1且display为false

  • 只有一行数据的单元格rowspan为1且display为true

实际上就是设计一个算法,对于输入的表格数组,每个数据项添加两个属性,rowspan和display,并且计算出**rowspan的值为
本列中以下相同值的行数,以及依据rowspan的值计算display的值是否显示**,最后将此改变后的数组输出。

3. show me code

function combineCell(list) {
    for (field in list[0]) {
        var k = 0;
        while (k < list.length) {
            list[k][field + 'span'] = 1;
            list[k][field + 'dis'] = false;
            for (var i = k + 1; i <= list.length - 1; i++) {
                if (list[k][field] == list[i][field] && list[k][field] != '') {
                    list[k][field + 'span']++;
                    list[k][field + 'dis'] = false;
                    list[i][field + 'span'] = 1;
                    list[i][field + 'dis'] = true;
                } else {
                    break;
                }
            }
            k = i;
        }
    }
    return list;
}

4. 总结

代码实际上很短很简单,主要借助的是kmp的思想,定义一个指针k,开始指向第一个值,然后向下比较,以此对rowspan和display设置,
若遇到不相同的值则判断为跳出,进行下一个循环,通知指针k加上这个过程中运算的行数,进行跳转,然后比较下一个单元格的值,和kmp的指针跳转判断相同字符串一样的原理。

通过combineCell()这个函数就可以将网络请求回来的数据进行过滤,附加上相应的值后再对vue监视的数组进行赋值操作就可以了。
实际上此方法不仅适用于vue,数据驱动的框架都可以,包括Angular和React,要想实现表格合并,对请求回来的值过滤一下就OK。

原文链接:原文 欢迎访问本人博客:House of Cards

查看原文

赞 6 收藏 21 评论 4

景初 发布了文章 · 2016-08-31

浅谈script标签的defer和async

1. 什么鬼

今天在做一个小需的时候,忽然看到前辈一句吊炸天的代码

    <script data-original="#link("xxxx/xx/home/home.js")" type="text/javascript" async defer></script>

卧槽,竟然同时有asyncdefer属性,心想着肯定是前辈老司机的什么黑科技,两个一块儿肯定会发生什么神奇化学反应,于是赶紧怀着一颗崇敬的心去翻书翻文档,先复习一下各自的定义。

2. 调查一番

先看看asyncdefer各自的定义吧,翻开红宝书望远镜,是这么介绍的

2.1 defer

这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。

HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoad时间触发前执行,因此最好只包含一个延迟脚本。

2.2 async

这个属性与defer类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。

第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。

概括来讲,就是这两个属性都会使script标签异步加载,然而执行的时机是不一样的。引用segmentfault上的一个回答中的一张图图片描述蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

也就是说async是乱序的,而defer是顺序执行,这也就决定了async比较适用于百度分析或者谷歌分析这类不依赖其他脚本的库。从图中可以看到一个普通的<script>标签的加载和解析都是同步的,会阻塞DOM的渲染,这也就是我们经常会把<script>写在<body>底部的原因之一,为了防止加载资源而导致的长时间的白屏,另一个原因是js可能会进行DOM操作,所以要在DOM全部渲染完后再执行。

2.3 really?

然而,这张图(几乎是百度搜到的唯一答案)是不严谨的,这只是规范的情况,大多数浏览器在实现的时候会作出优化。

来看看chrome是怎么做的

《WebKit技术内幕》:

  1. 当用户输入网页URL的时候,WebKit调用其资源加载器加载该URL对应的网页。

  2. 加载器依赖网络模块建立连接,发送请求并接受答复。

  3. WebKit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取的。

  4. 网页被交给HTML解释器转变成一系列的词语(Token)。

  5. 解释器根据词语构建节点(Node),形成DOM树。

  6. 如果节点是JavaScript代码的话,调用JavaScript引擎解释并执行。

  7. JavaScript代码可能会修改DOM树的结构。

  8. 如果节点需要依赖其他资源,例如图片、CSS、视频等,调用资源加载器来加载他们,但是他们是异步的,不会阻碍当前DOM树的继续创建;如果是JavaScript资源URL(没有标记异步方式),则需要停止当前DOM树的创建,直到JavaScript的资源加载并被JavaScript引擎执行后才继续DOM树的创建。

所以,通俗来讲,chrome浏览器首先会请求HTML文档,然后对其中的各种资源调用相应的资源加载器进行异步网络请求,同时进行DOM渲染,直到遇到<script>标签的时候,主进程才会停止渲染等待此资源加载完毕然后调用V8引擎对js解析,继而继续进行DOM解析。我的理解如果加了async属性就相当于单独开了一个进程去独立加载和执行,而defer是和将<script>放到<body>底部一样的效果。

3. 实验一发

3.1 demo

为了验证上面的结论我们来测试一下

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.css" rel="stylesheet">
        <link href="http://cdn.staticfile.org/foundation/6.0.1/css/foundation.css" rel="stylesheet">
        <script data-original="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.js"></script>
        <script data-original="http://libs.baidu.com/backbone/0.9.2/backbone.js"></script>
        <script data-original="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
    </head>
    <body>
        ul>li{这是第$个节点}*1000
    </body>
    </html>

一个简单的demo,从各个CDN上引用了2个CSS3个JS,在body里面创建了1000个li。通过调整外部引用资源的位置和加入相关的属性利用chrome的Timeline进行验证。

3.2 放置在<head>

图片描述
异步加载资源,但会阻塞<body>的渲染会出现白屏,按照顺序立即执行脚本

3.3 放置在<body>底部

图片描述
异步加载资源,等<body>中的内容渲染完毕后且加载完按顺序执行JS

3.3 放置在<head>头部并使用async

图片描述
异步加载资源,且加载完JS资源立即执行,并不会按顺序,谁快谁先上

3.4 放置在<head>头部并使用defer

图片描述
异步加载资源,在DOM渲染后之后再按顺序执行JS

3.5 放置在<head>头部并同时使用asyncdefer

图片描述
表现和async一致,开了个脑洞,把这两个属性交换一下位置,看会不会有覆盖效果,结果发现是一致的 = =、

综上,在webkit引擎下,建议的方式仍然是把<script>写在<body>底部,如果需要使用百度谷歌分析或者不蒜子等独立库时可以使用async属性,若你的<script>标签必须写在<head>头部内可以使用defer属性

4. 兼容性

那么,揣摩一下前辈的心理,同时写上的原因是什么呢,兼容性?

上caniuse,async在IE<=9时不支持,其他浏览器OK;defer在IE<=9时支持但会有bug,其他浏览器OK;现象在这个issue里有描述,这也就是“望远镜”里建议只有一个defer的原因。所以两个属性都指定是为了在async不支持的时候启用defer,但defer在某些情况下还是有bug。

The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the synchronous blocking behavior that is the default.

5. 结论

其实这么讲来,最稳妥的办法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,高枕无忧,不要搞什么deferasync的花啦~

目前只研究了chrome的webkit的渲染机制,Firefox和IE的有待继续研究,图片和CSS以及其他外部资源的渲染有待研究。

更多信息在 这里

参考

查看原文

赞 61 收藏 86 评论 13

景初 赞了文章 · 2016-08-27

react-redux-express异步前后端数据交互(面向初学者,高手勿进)

花了整整三天的时间来解决一个非常非常小的问题.想要把一点心得体会记录下来.
首先是问题的提出:前端如果是react,后端是express,如何进行数据的交互.

1.总体思路

以前接触express的时候前端模板用的是ejs,那时候就有些不理解的地方.最为不理解的几个问题是:前端和后端怎么配合?特别是前端特别复杂的时候,难道还是全用模板吗?如果前端用了框架呢?这些问题对于大部分开发者或者非初级学习者来说都不算问题,但是对于初学者来说,如果不能完整地知道这些概念和数据流动的方式,学起来就会有些"心虚".

所以在接触了expressreact 之后,我强烈地想知道两者是怎么进行数据的交互的.我想要的技术栈是:react-redux-webpack-express .在google和github上找了很久都没有找到合适的,最后才发现,其实官网的那个已经是最好的例子Async.

目前得到的比较好的方式是用异步的方式,通过前端ajax来发出请求,后端接收并处理请求,返回相应的数据(在这里不讲述服务器渲染).在这里的ajax如果引入jq会显得太笨重,所以按照官网的方法用 isomorphic-fetch

而因为引入了redux,所以要把ajax写在哪里是一个问题. google了一下,发现大家都认为写在action里面最好(官网也是这么做的),所以就直接这么做吧.(跟着官网没错)

下面就以一个非常非常简单的例子为切入点,功能如下:有一个input和一个button,在input里面输入,点击按钮,把input的内容上传服务器(POST). 同时下面有一个列表,从服务器上获取数据并在react中渲染(GET). 非常非常非常简单.

2.GET方法

把ajax全部写在action里面, 异步action需要用到中间件. 有关中间件最好的文章我认为是这一篇, 里面讲了applyMiddleware 的实现原理和例子(其实有点像俄罗斯套娃,把原本的dispatch慢慢加强,比如可以用logger加一点日志辅助找bug) 这里要用到一个叫做thunk的中间件(实现原理很简洁,可以自己找来琢磨)

下面的代码从服务器中获取列表. 其中的fetch的用法可以看这里, 这里也用到了promise对象用于处理异步事件,这个可以看阮一峰大神的这篇文章.

export const fetchList = () => {
    return dispatch => {
        dispatch({ type:"REQUEST_LIST" })
        return fetch(`/list`, {
            header: {
                "dataType": "json"
            }
        })
            .then(response => {
                return response.json()
            })
            .then(json => {
                dispatch(receiveList(json.items))
            }
        )
    }
}

3.POST方法

POST方法与GET大同小异,不过在server写代码的时候要用上body-parser, 不然有可能请求会变成undefined,写法是这样的.

具体的代码如下: POST方法相对复杂一点点.

export const postAddItem = (text) => {
    return dispatch => {
        dispatch({type: "loadAddItem", text})
        fetch('/send', {
            method: 'POST',
            headers: {
                'Content-Type': "application/json",
                'Accept': "application/json",
                'Content-Type': "application/json"
            },
            body: JSON.stringify({ item: text })
        }).then(res => {
            if(res.ok) {
                dispatch({ type: "ADD_ITEM", text })
                console.log("POST SUCCESS");
            } 
        }, e => {
            dispatch({type: "loadAddItem", text})
            alert("POST ERROR");
        })
    }
}

这些代码都是根据官网上Async的代码改的.
所以要真正掌握redux, 官网文档和例子要熟读啊...

4.关于调试

关于调试我没有什么值得分享的(我也在找比较方便的调试方法TAT,跪求推荐!!), 不过一个这几天下来总结了"肉眼debug"的思路就是: 看数据怎么流,从哪里开始变得不符合要求.之前在写的时候就是connect的地方开始有问题,结果死活找不出为什么渲染不出来...明明在logger上看到已经获取到了数据...

5.总结

个人感觉如果要"打通前后端"(起码理解吧),一定要认真理解redux,基本概念,异步,中间件(整个官网的内容很丰富,要多读..) 不过基础也很重要!最基础的es6,ajax等一定要会...
自己写的粗糙的例子代码在此

(第一次写文章,本人是小白,有什么说得不对的不好的,感谢提出!)

查看原文

赞 6 收藏 13 评论 5

景初 关注了问题 · 2016-08-12

解决有 npm script 还需要 gulp 么?

npm script & gulp grunt 打包区别

如提

关注 2 回答 1

认证与成就

  • 获得 86 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-14
个人主页被 938 人浏览