GithubHelp home page GithubHelp logo

blog's Introduction

考拉前端团队博客

https://blog.kaolafed.com/

  • 发文章以 Issue 的形式进行,注意格式
  • 定期有人会归档到 source/_posts,需要人肉判断下 <!-- more --> 插入位置
  • 本地 hexo g 看下是否能正确生成静态文件
  • hexo d 部署静态站到 gh-pages 分支
  • Git Push 本次新增/修改

blog's People

Contributors

elcarim5efil avatar fengzilong avatar force2008 avatar int64ago avatar jiangxiaoxin avatar kaola-blog-bot avatar kaolafed avatar nupthale avatar rabbitpl avatar tianyn1990 avatar yubaoquan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

feedback 开发过程中遇到的问题及解决

由来:

后台系统多, 用户也多.用户在使用过程中对系统中不合理或影响工作效率的地方, 需要有一个反馈的渠道.

如果每个系统都做一个反馈收集页, 将是重复的工作, 会耗费大量的人力.

因此, 最好能有一个统一的东西, 插入到需要收集反馈的系统中.

术语:

宿主系统: 用户实际使用的系统;

反馈系统: 收集用户反馈并统一存放供开发人员调查的系统;

插件脚本: 由反馈系统对外提供的一个js文件, 由宿主系统在页面中引入.

需求描述:

宿主系统中的每个页面中, 要出现一个按钮, 点击按钮, 弹出一个弹窗, 用户在弹窗中编辑反馈信息. 反馈信息包括页面地址, 所属系统, 当前用户, 反馈的文本, 截图, 以及点赞或踩等. 其中所属系统, 页面地址和用户名, 不需要用户手动输入. 用户编辑完这些信息后, 点击确定, 统一提交到反馈系统, 然后弹窗自动关闭.

问题与解决方案

按钮嵌入宿主系统全部页面

向宿主系统的全部页面中嵌入一个按钮, 肯定不能手动编辑所有的页面文件, 鉴于系统中所有的页面都会有导航条()这一特点, 决定将添加按钮的逻辑写到一个js文件中, 即插件脚本. 将此js放到公用组件的节点中, 即可实现全部页面嵌入按钮. 至于这个js文件, 因为js支持跨域请求, 可存放在反馈系统中.这样做的好处是避免每个系统拷贝一份这个js文件的代码, 统一管理, 统一修改.

系统间通信

如何将反馈信息在不同系统之间传送? 想到如下几种方案:

1.将反馈信息提交到宿主系统后端, 宿主系统再调用反馈系统的接口, 进行通信;

2.通过前端发送跨域请求, 直接将信息发送到反馈系统;

3.将弹窗中加入一个反馈系统的iframe, 用户在iframe中编辑信息, 直接提交;

第一种方案的缺点是需要每个系统添加后端通信的逻辑, 工作量大. 如果反馈系统的通信逻辑以后有改动, 每个系统都要改;

第二个方案的缺点是需要有人去配置nginx的跨域信息, 比较麻烦. 而且表单校验, 发送数据以及一些弹窗校验等工作, 放在一个需要没有依赖的js中去完成, 相当于裸着写逻辑, 代码量将会很大, 而且不好维护.

第三种方案相当于打开了一个反馈系统的页面, 只是看起来是一个宿主系统的弹窗. 遇到的问题是, 用户提交完成后iframe向外部传送信息比较麻烦. 单这个比起上面两种方案来说, 更加容易解决.

因此选择第三种方案.

modal模态弹窗

一下是比较常见的弹窗使用场景和需求:

1.用户在提交反馈信息时, 如果有必须要填的字段没有填, 比如反馈的问题类型没选, 需要中断提交并提醒用户.

2.管理员在删除一个反馈问题时, 需要二次确认.

3.不同的弹窗, 内容可能会不一样, 但外框和遮罩层的样式以及打开和关闭的逻辑在一般情况下是一样的, 因此重复的逻辑和样式需要可继承;

4.用于二次确认的弹窗, 需要知道用户点击的是确定还是取消.

在regular中, 继承和组合是不冲突的, 即可以在继承父组件的同时, 只重写父组件中的slot部分, 然后塞到父组件的模板中, 并在使用中也可以轻松的拿到new出来的弹窗引用, 如下:

//父组件 baseModal.js
define([
   'pro/baseComponent',
   'text./modalTemplate.html'
], function(base, template) {
    return base.extend({
        template: template
        //...
    });
});

//父组件模板 modalTemplate.html
<div class="mask">
   <div class="head">{title}</div>
   <div class="body">{slot}</div>
   <div class="footer">
       <button>Confirm</button>
       <button>Cancel</button>
   </div>
</div>


//子组件
define([
   'pro/baseModal',
   'text!./slot.html'
], function(base, tpl) {
    return base.extend({
        content: tpl
        //...
    })
})

//使用时 main.js

//...
var modal = new Modal({
    //...
});
modal.$on('confirm', someFun);
modal.$on('cancel', someFun);
//...

但是在vue中, 子组件继承父组件后, 并不继承父组件的模板, 即template需要完全重新写. 这样的拷贝背离了我们继承的初衷, 因此只能使用组合, 即所谓的子组件, 并不是继承了父组件, 而是在模板中组合进一个父组件, 然后将自己的模板塞到父组件的slot中.
vue推荐将modal事先写好在template中, 用的时候设置一个flag使其显示出来, 作者解释说之所以这样, 是为了时模板看起来清晰明了, 详见vue作者的解释
但这样产生一个问题, 直接将使用了slot的modal写到模板中, 无法拿到起引用, 即下面这样写是无效的.具体原因官方文档的解释并没有看懂.

<div>
   <modal ref="xxx"></modal>
</div>

既然拿不到引用, modal关闭时用户点击的是确定还是取消, 自然不知道了. 而且在使用时设置显示modal的逻辑比较大同小异, 因此将控制弹窗打开关闭的逻辑抽出来放到mixin中.
至于弹窗关闭的事件, 刚开始做的时候自己也没想到好的解决方法, 参考了一下饿了么的弹窗组件, 他们的做法是, 在打开弹窗的方法中return一个promise, 将promise的resolve和reject方法包装到一个对象中压一个任务栈中, 当组合的父弹窗发出Confirm或cancel事件时, 从栈中弹出最上面的任务, 如果confirm, 则执行任务对象的resolve方法.
代码如下:

//父组件 modal.vue
<style>
   //...
</style>
<template>
   <div class="mask">
       <div class="head">{title}</div>
       <div class="body">
           <slot></slot>
       </div>
       <div class="footer">
       <button @click="confirm">Confirm</button>
       <button @click="cancel">Cancel</button>
       </div>
   </div>
</template>
<script>
   import xxx
   
   export default {
       //xxx
   }
</script>

//mixin modal.js
module.exports = {
   data() {
       return {
           modalConfig: {
               status: 'modal-info',
               title: '提示',
               show: false,
               onlyClose: false
           },
           questionQueue: []
       };
   },
   methods: {
       /**
        *
        * @param msg 文本内容
        * @param status modal-warning modal-success modal-error modal-info
        */
       showWithOnlyCloseBtn(msg, status) {
           this.modalConfig = {
               show: true,
               status: status,
               title: '提示',
               content: msg,
               onlyClose: true
           };
       },
       showWarning(msg) {
           this.showWithOnlyCloseBtn(msg, 'modal-warning');
       },
       showError(msg) {
           this.showWithOnlyCloseBtn(msg, 'modal-danger');
       },
       showInfo(msg) {
           this.showWithOnlyCloseBtn(msg, 'modal-info');
       },
       showSuccess(msg) {
           this.showWithOnlyCloseBtn(msg, 'modal-success');
       },
       ask4Sure(msg) {
           this.modalConfig = {
               show: true,
               status: 'modal-info',
               title: '提示',
               content: msg,
               onlyClose: true
           };
           return new Promise((resolve, reject) => {
               this.questionQueue.push({
                   confirm: resolve,
                   cancel: reject
               });
           });
       },
       onConfirm() {
           this.destroy();
           if (!this.questionQueue.length) return;
           let task = this.questionQueue.shift();
           task.confirm();
       },
       onCancel() {
           this.destroy();
           if (!this.questionQueue.length) return;
           let task = this.questionQueue.shift();
           task.cancel();
       },
       destroy() {
           this.modalConfig.show = false;
       }
   }
};


//子组件 subModal.vue
 <style>
   //...
</style>
<template>
   <modal @cancel="cancel" @confirm="confirm">
       <div>xxxxxx</div>
   </modal>
</template>
<script>
   import Modal from './modal.vue';
   
   export default {
       components: {
           modal
       },
       methods: {
           cancel() {
               this.$emit('cancel');
           },
           confirm() {
               this.$emit('confirm');
           }
       }
   }
   
</script>

//使用时 main.js
//...
methods: {
   onRemoveSystem(system) {
       this.ask4Sure( '确定删除此系统吗?')
           .then(() => {
               this.removeSystem(system);
           })
           .catch(() => {
               console.info('取消删除');
           });
   },
},
//...

###自动获取当前环境信息

一般来说, 每个系统的导航组件上都会有一句欢迎语, xx用户, 你好blabla... 于是, 只要将用户名所在的标签加上一个id, 再将这个id放到插件脚本的dataset中, 插件脚本就会在执行时获取到用户名. 然后在打开iframe时, 将获取到的用户名, 系统一名等信息以参数的方式放到iframe的src中, 这样反馈系统即可从location.search中获取到这些信息.

提交反馈后自动关闭弹窗

反馈的提交动作是在iframe中触发的. 不同源的iframe无法向其父window发送信息.

在网上查到一种比较繁琐的方式, 在子iframe中再嵌入一个与父容器同源的iframe, 由这个第三者iframe充当中转. 考虑到这种方式也比较复杂, 于是再想其他的解决方案.

iframe有一个onload事件可以执行父容器传入的回调. 可不可以将要发送的信息放在iframe的src中然后通过onload来传输呢, 这样不就可以实现父容器知道用户何时提交了么.

只要在用户提交信息后, 将iframe跳转到另一个页面, 就可以触发onload了, 然后把要传出来的信息放到页面url中, 父容器通过event对象取获取, 一切大功告成.

然而经过实验, 在iframe跳转页面时, 的确触发了onload. 但是event对象中的src还是新建iframe时传入的src, 不会更新成新的src.

再回来结合业务场景思考, iframe在触发第一次onload时, 一定是刚新建好弹窗, 而第二次, 一定是用户提交成功后的跳转. 再往下就是我们将iframe隐藏起来并跳转会返回编辑页等待用户下一次打开"弹窗"提交反馈.

简而言之, 奇数次的onload是跳转到反馈编辑页, 偶数次的onload是用户提交了反馈信息. 于是, 在偶数次触发onload的时候, 在插件脚本中将iframe隐藏起来, 即达到了用户提交反馈后自动关闭弹窗的效果.

后记

目前的feedback还只是实现一些基本功能的状态, 很多细节没有覆盖到, 如果后期增加业务, 可能会遇到更多问题. to be continued...

重构与模式系列(一)简化函数

重构与模式系列(一)简化函数

函数多长合适?

Functions should do one thing
每一个函数应该都只做一件事情,如果一个函数做了过多的事情,那么它极为不稳定,这种耦合性得到的是低内聚和脆弱的设计,且不方便维护与阅读。
在人的常规思维中,总是习惯把一组相关的行为放到一起,如何正确的进行分离并不容易。

Functions should only be one level of abstraction
如果这个函数内有多步操作,但这些操作都在同一个抽象层上,那么我们还是认为这个函数只做了一件事

看个不错的demo:

我们需要从我们的注册用户列表中查看我们的用户,并且筛选出活跃用户,并向他们发送一封邮件。

Bad:

// 一般我们的书写风格 按照逻辑顺序写下去, 几件事情杂糅在了一起
function emailClients(clients) {
  clients.forEach(function (client, index) {
    var _clientRecord = database.lookup(client);
    if(_clientRecord.isActive()){
      email(client);
    }
  })
}

Good

// 让一个函数只干一件事情 单一职责
function emailClients(clients) {
  	clients.filter(isClientActive)
    	   .forEach(email)	
}

function isClientActive(client) {
  var _clientRecord = database.lookup(client);
  return _clientRecord.isActive();
}

拆解过长函数,有哪些可用模式

我们要简化过长的函数,那么我们可以使用哪些模式来优化?《重构与模式》一书中提到面对过长的函数,我们可以考虑使用下面几种模式:

  • 提炼函数
  • 策略模式(去替换过多的if条件分支)
  • 闭包与高阶函数
  • 模板方法模式

提炼函数

提炼函数大致的**就是将我们过长的函数拆分为小的函数片段,确保改函数内的函数片段的处理在同一层面上。随便找了一个regular预览图片组件里面的例子。

HTML结构如下:

{#if showPreview}
<div class="m-image-gallery-mask"></div>
<ul
	class="m-image-gallery"
	style="-webkit-transform: translate3d({ wrapperOffsetX }px,0,0);"
	ref="wrapper" on-click={this.onClose($event)}
>
	{#if prev}
        <li
            class="m-image-gallery-item"
            style="-webkit-transform: translate3d(-{ windowWidth }px,0,0);width: { windowWidth }px;"
            ref="prev"
        >
            <div class="m-image-gallery-img-wrapper">
                <img class="m-image-gallery-img" src="{ prev || _1x1 }" alt="">
            </div>
        </li>
	{/if}

	<li class="m-image-gallery-item" style="width: { windowWidth }px;" ref="current">
		<div class="m-image-gallery-img-wrapper">
			<img
				class="m-image-gallery-img"
				style="-webkit-transform: scale({ scale }) translate3d({ offsetX }px,{ offsetY }px,0);"
				src="{ current || _1x1 }"
				on-load="{ this.onCurrentLoaded() }"
				alt="预览图"
				ref="v"
			/>
		</div>
	</li>

	{#if next}
        <li class="m-image-gallery-item" style="-webkit-transform: translate3d({ windowWidth }px,0,0);transform: translate3d({ windowWidth }px,0,0);width: { windowWidth }px;" ref="next">
            <div class="m-image-gallery-img-wrapper">
                <img class="m-image-gallery-img" src="{ next || _1x1 }" alt="">
            </div>
        </li>
	{/if}
</ul>
{/if}
.....
onTouchMove: function (e) {
            // 触摸touchmove
           var  _touches = e.touches,
                _ret = isEdgeWillAway(_v),
                _data = this.data;

            e.preventDefault();
            (!this.touchLength) && (this.touchLength = e.touches.length);
            if (this.touchLength === 1) {
                this.deltaX = _touches[0].pageX - this.initPageX;
                this.deltaY = _touches[0].pageY - this.initPageY;
                if (_ret.left) {
                    // 图片将要往右边移动
                    _data.wrapperOffsetX = this.startOrgX + this.deltaX;
                    _data.prevShow = true;
                } else if (_ret.right) {
                    // 图片将要往左边移动
                    _data.wrapperOffsetX = this.startOrgX + this.deltaX;
                    _data.nextShow = true;
                }
                this.$update();
            }else if (this.touchLength === 2) {
               //如果是两个手指 进行缩放控制
              ....
            }
        },
....         

可以看到在touchMove的函数很长,我们需要对这个函数进行提炼, 大致应该是下面这样

....
onTouchMove: function(e){
  	  // 触摸touchmove
           var  _touches = e.touches,
                _ret = isEdgeWillAway(_v),
                _data = this.data;

           e.preventDefault();
           (!this.touchLength) && (this.touchLength = e.touches.length);
		   if ( this.touchLength === 1 ) {
                      //  this.$emit('setTranslate');
                     //   移动图片	
                     this.setMove(...);
		   }else if ( this.touchLength === 2) {
                    //  this.$emit('setScale');
                    //  缩放图片  
                    this.setScale(...);
		   }		
}

包括一些事件的绑定,我们通过下面的书写方式相比于直接写cb function也能更好地解耦。

initEvent: function () {
            if (!this.data.showPreview) return;
            _wrapper.addEventListener('touchstart', this.onTouchStart.bind(this));
            _wrapper.addEventListener('touchmove', this.onTouchMove.bind(this));
            _wrapper.addEventListener('touchend', this.onTouchEnd.bind(this));

            this.$on('animateLeft', this.onAnimateLeft.bind(this));
            this.$on('animateRight', this.onAnimateRight.bind(this));
            this.$on('animateReset', this.onAnimateReset.bind(this));
},
onTouchStart: function(){
  .....
},  

策略模式

当我们代码中有较多的if条件分支时,我们一般会选择策略模式进行重构。
策略模式的核心就是封装变化的部分,把策略的使用与策略的实现隔离开来,一个策略类,一个上下文类,依据不同的上下文返回不同的策略。

例如:

// 比如小球的动画
// 策略类
var tween = {
  //@params t: 已运行时间  b: 原始位置 c: 目标位置 d: 持续总时间
  //@return 返回元素此时应该处于的位置
  linear: function (t, b, c, d) {
    return c * t / d + b;
  },
  easeIn: function (t, b, c, d) {
    return c * (t/=d) * t + b
  },
  ....
}

var Animation = function () {
}

Animation.prototype.start = function (target, config) {
  var _timeId;
  this.startTime = +new Date;// 开始时间
  this.duration = config.duration;// 动画持续时间
  this.orgPos = target.getBoundingClientRect()[config.property];// 元素原始的位置
  this.easing = tween[config.type];// 使用的动画算法
  this.endPos = config.endPos;// 元素目标位置
  _timeId = setInterval(function(){// 启动定时器,开始执行动画
    if(!this.step()){// 如果动画已经结束,清除定时器
      clearInterval(_timeId);
    }
  }.bind(this), 16);
}

Animation.prototype.step = function () {
	var _now = +new Date,// 当前时间
        _dur = _now - this.startTime,// 已运行时间
        _endPos;
       
  	_endPos = this.easing(_dur, this.orgPos, this.endPos, this.duration);// 此时应该在的位置
    this.update(_endPos);// 更新小球的位置
}

类似的,其他经典的例子还有验证规则的策略模式的写法。

可以看下 《Javascript设计模式与开发实践》P84 表单规则校验的例子

善用高阶函数和闭包

闭包

避免声明许多全局变量,通过闭包我们来存储变量

// 利用高阶函数避免写全局变量
pro.__isWeiXinPay = (function(){
          var UA = navigator.userAgent;
          var index = UA.indexOf("MicroMessenger");
          var _isWeiXinPay = (index!=-1 && +UA.substr(index+15,3)>=5);
          // window._isWeiXin = index!=-1;
          return function(){
            return _isWeiXinPay;
          }
})();

高阶函数

高阶函数是指至少满足下列条件之一的函数:

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

高阶函数在我们编码时无形中被使用,善用高阶函数可以使我们代码写的更加漂亮。

通过高阶函数实现AOP

在Js中实现AOP,都是指把一个函数动态织入到另一个函数中,比如

Function.prototype.before = function (beforefn) {
  var _self = this;
  return function () {
    beforefn.apply(this, arguments);// 执行before函数
    return _self.apply(this, arguments);// 执行本函数
  }
}

Function.prototype.after = function (beforefn) {
  var _self = this;
  return function () {
    var ret = _self.apply(this, arguments);// 执行本函数
    afterfn.apply(this, arguments);// 执行before函数
    return ret;
  }
}

var func = function () {
  console.log('hahaha');
};

func = func.before(function(){
  console.log(1);
}).after(function(){
  console.log(2);
})

有了aop以后,可以帮助我们把原来耦合在一起的长函数进行拆解,再利用模板模式我们可以达到意想不到的效果,见下节。

模板方法模式

如果我们有一些平行的子类, 各个子类之间有一些相同的行为,也有一些不同的行为。相同的行为可以被搬移到另外一个单一的地方。在模板方法模式中,子类实现中相同的部分可以上移到父类,而将不同的部分待由子类去实现。模板方法就是这样的模式。

模板方法模式由两部分组成,抽象类和实现类,我们把抽出来的共同部分放到抽象类中,变化的方法抽成抽象方法,方法的具体实现由子类去实现,先看《设计模式实践》书中的一个例子:

var Beverage = function(param){
  var boilWater = function () {
    console.log('把水煮开');// 共同的方法
  };
  var brew = param.brew || function(){
    throw new Error('必选传递brew方法');// 需要子类具体实现
  };
  var pourInCup = param.pourInCup || function(){
    throw new Error('必选传递pourInCup方法');
  };
  var addCondiments = param.addCondiments || function(){ 
    throw new Error( '必选传递addCondiments方法' );
  };
  
  var F = function(){}
  F.prototype.init = function(){
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  }
  return F;
}

var Coffee = Beverage({
  brew: function(){
    console.log('用沸水泡咖啡');
  },
  pourInCup: function(){
    console.log('把咖啡倒进杯子');
  },
  addCondiments: function(){
    console.log('加糖和牛奶');
  }
});

var Tea = Beverage({
  brew: function(){
    console.log('用沸水泡茶叶');
  },
  pourInCup: function(){
    console.log('把茶倒进杯子');
  },
  addCondiments: function(){
    console.log('加柠檬');
  }
});

var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();

在业务中使用模板方法和上面的AOP我们可以将我们的代码有效解耦,例如下面的rgl.module.js 是所有自定义模块的基类。

 var BaseList = BaseComponent.extend({
        config: function () {
            this.data.loading = true;
        },
	initRequestEvents: function () {
          var data = this.data,
               dataset = data.dataset||{};
          data.requestUrl = this.getRequestUrl(dataset);
          this.onRequestCustomModuleData(data.requestUrl);
	},
   	onRequestCustomModuleData: function () {
            if(!requestUrl) return;
            var self = this,
                data = this.data;

            this.$request(requestUrl,{
                method: 'GET',
                type: 'json',
                norest: true,
                onload: this.cbRequestCustomModuleData._$bind(this)._$aop(function(){
                    if(data.loadingElem && data.loadingElem[0]) 	 e._$remove(data.loadingElem[0]);
                },function(){
                    self.finishRequestData();
             }),// 这里就是模板模式方法与aop的结合使用
             onerror: function(){
                data.loading = false;
             }
            });
   	   },
   	   cbRequestCustomModuleData: f,// 提供给子类具体实现的接口 子类继承BaseComponent自己具体实现
   	   finishRequestData: f	// 提供给子类具体实现的接口 子类继承BaseComponent自己具体实现
 });
 var BottomModule =  BaseModule.extend({
            template: tpl,
            config: function(data){
                _.extend(data, {
                    clickIndexArray:[],
                    isStatic: false
                });
            },
   	    init: function(){
                this.initRequestEvents();
            },
            cbRequestCustomModuleData: function(data){
              ......// 具体实现
            }
};              

下一节将会看看重复代码的问题及可参考模式。

【参考书籍】

SVG动画实践篇-无中生有的线条动画

说明

这个动画实现的是线条动画,主要用到的是 SVG 的 path 标签。

<path> 标签命令

使用 <path> 标签的 d 属性标识路径集合,勾画线条的形状。

  • M = moveto
  • L = lineto
  • H = horizontal lineto
  • V = vertical lineto
  • C = curveto
  • S = smooth curveto
  • Q = quadratic Belzier curve
  • T = smooth quadratic Belzier curveto
  • A = elliptical Arc
  • Z = closepath

例如:

<svg width="300" height="300" version="1.2" xml:space="default">
    <path d="M0 0 L150 100 V200 H100" stroke="#f00" stroke-width="1"/>   
</svg>

动画实现

步骤一: 定义SVG线条

定义SVG线条,除了使用 d 属性定义路径外,还需要用到两个重要的属性, stroke-dasharray 和 stroke-dashoffset, 这两个属性值可以在 path 标签上定义,也可以在样式表中定义。

  • stroke-dasharray 定义短划线和缺口的长度,实现画虚线的效果。例如4px 2px/4px,2px,数与数之间可用空白或逗号隔开。
  • stroke-dashoffset 标识的是整个路径的偏移值。

svg代码如下:

<svg width="500" height="200" version="1.2" xml:space="default">
    <path id="path" d="M0,150c0,0,0-61,72-44c0,0-47,117,81,57s5-110,10-67s-51,77.979-50,33.989" stroke="#f00" stroke-width="1" stroke-dasharray="4px,2px" stroke-dashoffset="10px" fill="none"/>
</svg>

步骤二: 给path标签使用CSS3动画

定义 css3 的 animation,通过改变 path 标签的 stroke-dasharray 或 stroke-dashoffset 值来使路径动起来。
path 路径的长度可使用 js 的 document.getElementById(‘path’).getTotalLength() 来获得。

方法一: 改变 stroke-dasharray 来实现动画

css 代码如下:

#path{
    -webkit-animation:slide 2s linear infinite;
}


@keyframes slide {
    0%{
        stroke-dasharray:0 511px;   /* 511px 为整个路径的长度 */
    }
    100%{
        stroke-dasharray:511px 511px;
    }
}
  • stroke-dasharray:0 511px; 实线宽度为0,空隙宽度为整个path路径的宽度,所以刚开始路径没有实线,是不可见的。
  • stroke-dasharray:511px 511px; 实线宽度为整个 path 路径长度,所以整条路径可见。
  • css3 animation 动画定义路径从不可见到可见的变化。

方法二: 改变 stroke-dashoffset 来实现动画

css 代码如下:

#path{
    stroke-dasharray:511px 511px;
    -webkit-animation:slide2 2s linear infinite;
}

@keyframes slide2 {
    0%{
        stroke-dashoffset:511px;
    }
    100%{
        stroke-dashoffset:0px;
    }
}
  • stroke-dasharray:511px 511px; 给 path 标签定义实线宽度和空隙宽度都为整个path 的长度。这个时候如果不用动画,则线条会全部展示。
  • 0%{stroke-dashoffset:511px;} path 路径左偏移 511px, 则会显示 511px 的空隙宽度。此时路径没有实线,是不可见的。
  • 100%{stroke-dashoffset:0px;} path 路径偏移量为0,则恢复到最初始状态,显示全部的实线。
  • css3 animation 动画定义路径从不可见到可见的变化。

多条 path 的动画或文字动画

  • 使用 symbol 定义和 use 实例化来画出SVG路径。
  • 使用 CSS3 的 animation 属性来修改实例化路径的 stroke-dasharray 或 stroke-dashoffset 的值,从而实现动画效果。
  • 可新建多个同样的 SVG 路径,并且每个路径的颜色和动画效果都不一样,最终形成错落的完整的动画。

git: https://github.com/rainnaZR/svg-animations/tree/master/src/pages/step2/path
参考资料: http://www.alloyteam.com/2017/02/the-beauty-of-the-lines-break-lines-svg-animation/

SVG动画基础篇

参考资料:
http://www.w3school.com.cn/svg/index.asp

https://msdn.microsoft.com/zh-cn/library/gg193979

动画效果参考:https://github.com/rainnaZR/svg-animations

简介

SVG 指可伸缩矢量图形。是使用 XML 来描述二维图形和绘图程序的语言。

SVG 代码的根元素是以 <svg> 元素开始,</svg>结束。width 和 height 属性可设置 SVG 文档的宽度和高度。version 属性可定义所使用的 SVG 版本,xmlns 属性可定义 SVG 命名空间。

使用说明

如何画圆

<svg width="300px" height="300px" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <circle cx="100" cy="100" r="40" stroke="black" stroke-width="2" fill="#f60"/>
</svg>
  • SVG 的 <circle> 用来创建一个圆。
  • cx 和 cy 属性定义圆中心的 x 和 y 坐标。如果忽略这两个属性,那么圆点会被设置为 (0, 0)。
  • r 属性定义圆的半径。
  • stroke 和 stroke-width 属性控制如何显示形状的轮廓,也就是边框。stroke定义边框的颜色,stroke-width定义边框的宽度。
  • fill 属性设置形状内的颜色,也就是背景色(填充色)。

如何画椭圆形

<svg width="300" height="300" version="1.2" xml:space="default">
    <ellipse cx="100" cy="100" rx="50" ry="80" style="fill:#f60;stroke:#000;stroke-width:5;"/>
</svg>
  • SVG的<ellipse>用于创建一个椭圆形。
  • cx 属性定义圆心的 x 坐标。
  • cy 属性定义圆心的 y 坐标。
  • rx 属性定义水平半径。
  • ry 属性定义垂直半径。

如何画矩形

<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <rect x="50" y="50" rx="20" ry="20" width="300" height="100" style="fill:rgb(0,0,255);fill-opacity:.5;stroke-width:2;stroke:#f60;stroke-opacity:.5;opacity:0.6" />
</svg>
  • SVG 的 <rect> 用来创建一个矩形。
  • x 属性定义矩形的左侧位置(矩形到浏览器窗口左侧的距离)。
  • y 属性定义矩形的顶端位置(矩形到浏览器窗口顶端的距离)。
  • rx 和 ry 属性可使矩形产生圆角。
  • rect 元素的 width 和 height 属性可定义矩形的高度和宽度。
  • style 属性用来定义 CSS 属性。
  • CSS 的 fill 属性定义矩形的填充颜色(背景色)。
  • CSS 的 fill-opacity 属性定义填充颜色透明度。
  • CSS 的 stroke 代表边框的颜色。
  • CSS 的 stroke-width 代表边框的宽度。
  • CSS 的 stroke-opacity 代表边框颜色的透明度。
  • CSS 的 opacity 定义整个元素的透明度。

如何画线条

<svg width="300" height="300" version="1.2" xml:space="default">
    <line x1="50" y1="50" x2="200" y2="200" style="stroke:#f00;stroke-width:10"/>
</svg>
  • SVG的<line>标签用于创建线条。
  • x1 属性表示线条起始的 x 坐标。
  • y1 属性表示线条起始的 y 坐标。
  • x2 属性表示线条结束的 x 坐标。
  • y2 属性表示线条结束的 y 坐标。
  • CSS 的 stroke 表示边框的颜色,这里也就是指线条的颜色。
  • CSS 的 stroke-width 表示线条的宽度。

如何画多边形

<svg width="300" height="300" version="1.2" xml:space="default">
    <polygon points="50,50 250,50 150,150" style="fill:#f60;stroke-width:5;stroke:#000;"/>
</svg>
  • SVG 的<polygon>用来创建含有不少于三个边的图形。
  • points 定义多边形每个角的 x 和 y 坐标。

如何画折线

<svg width="300" height="300">
    <polyline points="0,0 50,0 50,50 100,50 100,100 150,100 150,150" style="fill:#f60;stroke:#000;stroke-width:5"/>
</svg>
  • SVG 的 <polyline> 标签用来画折线。
  • points 属性定义每个角的 x 和 y 坐标。

如何画复杂路径

<svg width="300" height="300" version="1.2" xml:space="default">
    <path d="M0 0 L150 100 V200 H100 Z"/>   
</svg>
  • SVG 的 <path> 用来定义路径。
  • M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Belzier curve,T = smooth quadratic Belzier curveto,A = elliptical Arc,Z = closepath,以上所有命令均允许小写字母。大写表示绝对定位,小写表示相对定位。

如何给元素定义滤镜

<svg width="500" height="500" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <filter id="Gaussian_Blur">
            <feGaussianBlur in="SourceGraphic" stdDeviation="3" />
        </filter>
    </defs>

    <circle cx="100" cy="100" r="40" stroke="black" stroke-width="2" fill="#f60" style="filter:url(#Gaussian_Blur);"/>
</svg>
  • 使用 <filter> 标签用来定义 SVG 滤镜, <filter> 标签必须嵌套在 <defs> 内。
  • <filter> 上的 id 属性为滤镜定义一个唯一的名称。(同一滤镜可被文档中的多个元素使用)
  • <feGaussianBlur> 标签定义滤镜效果。
  • <feGaussianBlur> 标签的 in="SourceGraphic" 定义了由整个图像创建效果。
  • <feGaussianBlur> 标签的 stdDeviation 属性定义模糊的程度。
  • 在元素的样式上添加 filter:url('#滤镜ID名') 来使用滤镜。

SVG 中,可使用的滤镜如下:feBlend, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap, feFlood, feGaussianBlur, feImage, feMerge, feMorphology, feOffset, feSpecularLighting, feTile, feTurbulence, feDistantLight, fePointLight, feSpotLight

SVG滤镜 说明
feColorMatrix 应用matrix转换
feComponentTransfer 执行数据的 component-wise 重映射(没懂)
feOffset 相对当前图像的移动位置
feMerge 创建累积而上的图像

如何给元素添加渐变效果

<svg width="800" height="800" version="1.2" xml:space="default">
    <defs>
        <linearGradient id="orange_white" x1="0%" y1="0%" x2="0%" y2="100%">
            <stop offset="0%" style="stop-color:#f60;stop-opacity:1;"/>
            <stop offset="100%" style="stop-color:#fff;stop-opacity:1;"></stop>
        </linearGradient>
        
        <radialGradient id="orange_blue" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
            <stop offset="0%" style="stop-color:#f00;stop-opacity:1;"/>
            <stop offset="100%" style="stop-color:blue;stop-opacity:1;"/>
        </radialGradient>
    </defs>

    <rect height="200 " width="200" style="fill:url(#orange_white);stroke:#000;stroke-width:5;" />
    <circle r="50" cx="300" cy="300" style="fill:url(#orange_blue)" />

线性渐变

  • 使用 <linearGradient> 可用来定义 SVG 的线性渐变。<linearGradient> 内嵌在 <defs> 标签中。
  • <linearGradient> 标签的 id 属性定义渐变的唯一名称。
  • <linearGradient> 标签的 x1、x2、y1、y2 属性可定义渐变的开始和结束位置。如果x1 == x2,y1 != y2,则为垂直渐变。如果 x1 != x2,y1 == y2,则为水平渐变。如果x1 != x2,y1 != y2,则为角形渐变。
  • 通过 <stop> 标签来规定每种颜色的渐变属性。渐变的颜色可以有多种。
  • <stop> 标签的 offset 属性定义渐变的开始和结束位置。
  • <stop> 标签的 style 属性里可定义stop-color,stop-opacity等属性。
  • 元素通过 fill:url(#渐变ID) 来使用渐变效果。

放射性渐变

  • 使用 <radialGradient> 可用来定义 SVG 的放射性渐变。
  • <radialGradient> 标签的 id 属性定义渐变的唯一名称。
  • <radialGradient> 标签的 cx、cy 和 r 属性定义外圈,而 fx 和 fy 定义内圈。
  • 通过 <stop> 标签来规定每种颜色的渐变属性。渐变的颜色可以有多种。

给元素加超链接

<a xlink:href="http://www.jd.com" target="_blank">
    <rect x="20" y="20" width="250" height="250" style="fill:blue;stroke:pink;stroke-width:5;opacity:0.9"/>
</a>

如何给元素添加动画

<svg width="800" height="800">
    <circle r="100" cx="200" cy="400" fill="#f60">
        <animate attributeName="opacity" attributeType="CSS" from="1" to="0" dur="5s" repeatCount="indefinite"/>
        <animate attributeName="r" attributeType="XML" begin="0s" dur="5s" from="100" to="150" repeatCount="indefinite"/>
        <animateMotion path="M 0 0 L 100 100" dur="5s" fill="freeze" repeatCount="indefinite"/>
    </circle>

    <rect x="400" y="400" width="200" height="200" style="fill:#f00;">
        <animateTransform attributeName="transform" attributeType="XML" type="rotate" from="-30" to="0" begin="1s" dur="5s" fill="freeze"/>
        <animateTransform attributeName="transform" attributeType="XML" type="scale" from="1" to="0.5" additive="sum" begin="1s" dur="5s" fill="freeze"/>
    </rect>
</svg>
  • <animate> 标签给元素添加动画。
  • <animateMotion> 标签使元素沿着动作路径移动。
  • <animateTransform> 标签对元素进行动态的属性转换。

结合CleanCode的一次代码重构实践

类目参谋近期进行版本迭代,添加新功能。原本代码开发已久,可优化空间较大也为了便于日后的维护和拓展,决定页面重构。我参与了其中交易构成页面的代码重构。(数据门户 - 类目参谋 - 交易分析 - 交易构成)

目标:函数拆解,组件降耦,健壮逻辑,bugfix

步骤

  1. 线上请求分析,online-bug记录
  2. 阅读原代码,功能、模块拆分
  3. 按模块重构代码,连线上检验重构结果
  4. clean code
  5. 静态代码检测

重构

1. 线上请求分析

多次请求、频繁请求

  1. 切换维度tab,连续2次获取数据列表 /dealConstituteDetail

    1. 改变一级维度重新获取列表
    2. 筛选器multiFilter监听dimension -> 改变 -> 重置筛选器 -> 触发请求。

    第一个触发是合理的。第二个触发应该有前提,如果原先没有使用筛选器(下图为筛选器),则不需要触发请求

  2. 每一次切换维度tab,都会获取二级维度 /secondDimensions

    请求返回二级维度列表 = '未选择' + 一级维度列表 - 当前tab维度

    所有的请求都是这样的返回,因此根据当前一级维度动态更新二级维度列表

  3. 选择日期,发出2-3次 /dealConstituteDetail 和1次 /secondDimensions

    1. /dealConstituteDetail 一级维度重置,触发请求
    2. /secondDimensions 获取二级维度
    3. /dealConstituteDetail 对比时间重置,触发请求

    第三次请求不合理:修改基础时间不应该使对比时间选择器触发请求。两个组件耦合在一起。

报错、页面异常


4. [选择指标] 选择少于6个指标,更换时间类型,页面报错
5. [选择指标] 选择除'uv'、'平均转化率'之外的6个以上的指标,第4和第6个表头字段被换成'UV/日均UV'和'转化率/平均转化率',跟底下的数据对不上,显然错误
6. 只有[选择指标]中的前6个有排序
7. 页面宽度不够时,对比时间样式异常

2. CleanCode

  1. 'period'设置默认值交给函数
  2. 'periodType'采用枚举
  3. 'periodMap'改为数组
  4. 'comparePeriod'使用key-val原本应该是考虑到本页中有其他模块的情况,建议模块自理,消除耦合
  5. 'tradeConstituteSort.sortType' 0指代不明,使用枚举,使赋值语义化var DESC = 0; sortType = DESC;
// befor
eu.extend(data, {
    "period": data.period||u._$format(new Date(+now - 24 * 3600 * 1000), 'yyyy-MM-dd') + '~' + u._$format(new Date(+now - 24 * 3600 * 1000), 'yyyy-MM-dd'), // 默认近1天
    "periodType": data.periodType||0, // 时间段类型,0 'day' 自然日,1 '7day' 近7天,2 '30day' 近30天,3 'month' 自然月
    "periodMap": {
        "0": 'day', // 自然日
        "1": 'recent7', // 近7天
        "2": 'recent30', // 近30天
        "3": 'month' // 自然月
    },
    "comparePeriod": {
        "tradeConstitute": "" //交易构成
    },
    "tradeConstituteSort": {    //默认排序字段
        "dimensionValue": "sales",
        "sortType": 0
    },
    ...
});
// after
eu.extend(data, {
    period: ut.getYesterday(),
    periodMap: ['day', 'recent7', 'recent30', 'month'],
    periodType: +PERIOD_TYPE.DAY,
    comparePeriod: '',
    sortInfo: {
        dimensionValue: 'sales',
        sortType: DESC,
    },
    ...
});
  1. 函数应根据调用顺序排放(不在主流程中且不与其他函数有过多交集的函数可置后,如导出文件功能)

  2. 注释掉的代码,原代码中有多处

  3. 多余的注释

    //如果没有搜索信息 则清空搜索栏
    if (!searchText) {
        data.searchText = "";
    } else if(searchText !== true) {
        data.searchText = searchText;
    }
    
  4. 代码过长

    getTradeConstitute获取交易构成明细,函数40几行。做了几件事情,应拆解:

    1. 处理传参
    2. 获取类目名称
    3. 请求参数声明
    4. 设置回调函数
  5. 重复的功能相似的代码

    获取交易构成明细 与 导出文件 的参数基本一致,声明了两次。

  6. 不应在公共组件堆叠当前模块特有的组件

dealComposition <- multifilter <- multiSelector。将multi-两个组件放到dealComposition模块

  1. 参数意义尽量直接
getTradeDataBySort: function(sort, notRequest) {
    ...
    if (!notRequest) {  // !不请求 = 请求
        this.getTradeConstitute(undefined, undefined, sort, true);
    }
},

换成doRequest更直接

  1. 函数参数过多

getTradeConstitute这个方法带四个参数,html和js中调用常有undefined实参传入,阅读的时候会很困惑。

getTradeConstitute: function (currentPage, filters, sort, searchText) {...}

// html中使用该函数
on-filterSearch={this.getTradeConstitute(undefined, $event, undefined, undefined)}

建议将形参能去则去,请求数据时从组件的状态中获取参数或者根据不同情形重置参数。

3. 静态代码检测

书写规范

  1. js中的字符串、文件路径采用'单引号'
  2. 统一文件命名,采用.分隔 tradeComposition -> deal.composition
  3. object.key不用引号或者使用单引号
  4. 统一缩进
  5. 弱等== 改为 强等===

多余变量

  1. 大量未使用的变量

4. 组件自治要彻底

<checkBoxHub source={metrics} defaulted={defaultMetrics}
             selected={tradeMetrics} periodType={periodType}>
</checkBoxHub>

// checkBoxHub
getCheckedBox: function() {
    ...
    for (var i = 0; i < data.checkBox.length; i++) {
        if (data.checkBox[i].checked){
            if( i === 3 && data.periodType!=0){
                data.checkBox[i].name = "日均UV";
            }
            ...
        }
    }
    ...
},

periodType不同的值会影响checkBoxHub组件字段和表头展示'UV/转化率'或者'日均UV/平均转化率';而在父组件中也有这一功能的代码

// 初始化表头数据
if(data.periodType !=0){
    uvValue = '日均UV';
    tpValue = "平均转化率";
}else{
    uvValue = 'UV';
    tpValue = "转化率";
}

...

//监听时间段 修改交易构成表头
self.$watch('periodType',function(newValue,oldValue){
    if (newValue != undefined && oldValue != undefined) {
        if(newValue != 0){
            self.data.tradeMetrics[3].dimensionName = '日均UV';
            self.data.tradeMetrics[5].dimensionName = '平均转化率';
        }else{
            self.data.tradeMetrics[3].dimensionName = 'UV';
            self.data.tradeMetrics[5].dimensionName = '转化率';
        }
    }
});

checkBoxHub和父组件都没有把功能、逻辑做完整,代码不健壮,耦合度高;

后面watch里的赋值也直接引发了第4、5个线上bug。重构时将这一部分的代码全部在checkBoxHub中处理,降低耦合,只留periodType做接口

!!能不watch就不watch,尽量避免在watch中发请求

总结

  1. 该页面有几个明显的问题:功能代码混乱,部分函数较长,大量冗余代码和错误代码,在线上还有明显bug。针对这些问题,着手理清功能和模块,拆解函数,组件间降低耦合,根据cleancode的原则整理代码,遵守静态代码规范。
  2. 造成代码质量不高的原因可能是最初开发的人员对于regular的书写不够娴熟,代码也没有清晰的设计,组件接口设计不合理。除此之外,不能忽视的是后来的维护者。面对需求更改或者bugfix的时候只是想显式地快速地实现功能(但是也引发了bug),而忽略了代码本身可优化的空间,错过了当时重构的时机。
  3. 之前芳芳总结重构的时候说的很对——“重构从来不是一次性行为,是我们需要不断进行的工作。多人维护以及不断的功能迭代之后,代码多多少少都会有优化的空间,所以在你看到不合理或任何值得重构的地方时,行动起来吧。”

聊聊 Chrome DevTools 之 快捷键 (shortcuts)

快捷键(shortcuts)

工欲善其事,必先利其器。——《论语·卫灵公》

Chrome DevTools 的快捷键,可以帮助开发者在日常开发的过程中节约时间(甚至可以说是大量的时间,具体看天赋咯)。

下面使用表格的方式,列举每个快捷方式在Windows/Linux和Mac下相应的快捷按键。

  • 注:有些快捷键是在全局有效的,而有些只是在某一个面板生效。

1、打开DevTools

功能 Windows / Linux Mac
打开 Chrome DevTools F12, Ctrl + Shift + I Cmd + Opt + I
打开/切换 审查元素模式和浏览模式 Ctrl + Shift + C Cmd + Shift + C
打开 Chrome DevTools ,并聚焦在 console 上 Ctrl + Shift + J Cmd + Opt + J
审查审查器 (取消第一个审查器的停靠后再按键) Ctrl + Shift + J Cmd + Opt + J
  • 注:
    • 非快捷键打开DevTools
      1. 打开浏览器窗口右上方的菜单(三个点),选择 -> 更多工具 > 开发者工具
      2. 在任意的页面元素中右键,选择 -> 检查

2、DevTools下全局方式

功能 Windows / Linux Mac
打开 settings(console和sources下无效) ?, F1 Shift + ?
下一个面板 Ctrl + ] Cmd + ]
上一个面板 Ctrl + [ Cmd + [
标签历史中后退 Ctrl + Alt + [ Cmd + Alt + [
标签历史中前进 Ctrl + Alt + ] Cmd + Alt + ]
跳转至标签页 1-9 (需要在设置中开启) Ctrl + 1~9 Cmd + 1~9
打开/关闭 Console 或 关闭设置对话框 Esc Esc
刷新页面 F5, Ctrl + R Cmd + R
强制刷新页面,清除缓存内容 Ctrl+F5, Ctrl + Shift + R Cmd + Shift + R
当前文件或标签页搜索文字 Ctrl + F Cmd + F
所有资源中搜索文字 Ctrl + Shift + F Cmd + Alt + F
搜索文件(除了 Timeline面板) Ctrl + O, Ctrl + O Cmd + O, Cmd + O
恢复默认字体大小 Ctrl + 0 Shift + 0
放大 Ctrl + + Shift + +
缩小 Ctrl + - Shift + -

3、Elements(DOM节点面板)

功能 Windows / Linux Mac
撤销改动 Ctrl + Z Cmd + Z
恢复改动 Ctrl + Y Cmd + Y, Cmd + Shift + Z
选中节点(不会去展开) ↑,↓ ↑,↓
伸缩展开元素 ←,→ ←,→
编辑元素属性 Enter Enter
隐藏元素 H H
Edit as HTML F2

4、Elements(Styles样式侧边栏)

功能 Windows / Linux Mac
跳转到css具体行数 Ctrl + 单击某个CSS属性/选择器 Cmd + 单击某个CSS属性/选择器
循环切换颜色定义(rgb/a、#、hsl) Shift + 单击颜色选择器 Shift + 单击颜色选择器
查看属性提示(一般与spotlight冲突) Ctrl + 空格 Cmd + 空格
编辑下一个 / 上一个属性 Tab, Shift + Tab Tab, Shift + Tab
增大 / 减小属性值(+1 / -1) ↑,↓ ↑,↓
增大 / 减小属性值 (+10 / -10 ) Shift + ↑, Shift + ↓ Shift + ↑, Shift + ↓
增大 / 减小属性值 (+100 / -100) Shift + PgUp, Shift + PgDown Shift + PgUp, Shift + PgDown
增大 / 减小属性值 (+0.1 / -0.1) Alt + ↑, Alt + ↓ Opt + ↑, Opt + ↓
    • :hov——模拟元素伪类 (:active, :hover, :focus, :visited)
    • .cls——快速编辑元素class

5、Console

功能 Windows / Linux Mac
下一个提示 Tab Tab
上一个提示 Shift + Tab Shift + Tab
使用提示
上/下一个命令/行 ↑,↓ ↑,↓
清除控制台记录 Ctrl + L Cmd + K, Opt + L
多行输入 Shift + Enter Ctrl + Enter
执行 Enter Enter
    • console 中右键单击
      • XMLHTTPRequest 记录: 打开后可查看 XHR 记录
      • Filter 过滤: 隐藏或显示所有来自脚本文件的消息
      • Clear console 清除: 清除所有的 console 消息

6、资源(Sources)面板

功能 Windows / Linux Mac
中断/恢复脚本执行 F8, Ctrl + \ F8, Cmd + \
跳过下一个函数 F10, Ctrl + ' F10, Cmd + '
跳入下一个函数 F11, Ctrl + ; F11, Cmd + ;
跳出当前函数 Shift + F11, Ctrl + Shift + ; Shift + F11, Cmd + Shift + ;
Select next call frame Ctrl + . Opt + .
Select previous call frame Ctrl + , Opt + ,
切换断点状态 单击行数, Ctrl + B 单击行数, Cmd + B
编辑断点调节 右键单击行数 右键单击行数
Delete individual words Alt + Delete Opt + Delete
注释某行或选择文字 Ctrl + / Cmd + /
保存本地的更改 Ctrl + S Cmd + S
保存所有的更改 Ctrl + Shift + S Cmd + Shift + S
跳转到某行 Ctrl + G Ctrl + G
跳转到某行(Jump to line number) Ctrl + P -> :number Cmd + P -> :number
按文件名搜索文件 Ctrl + O Cmd + O
跳转到某列 Ctrl + O + : + : Cmd + O + : + :
打开 member Ctrl + Shift + O Cmd + Shift + O
切换 console 并评估( evaluate?) Sources 面板中选中的代码 Ctrl + Shift + E Cmd + Shift + E
关闭当前激活的标签 Alt + W Opt + W
运行代码片段 Ctrl + Enter Cmd + Enter
切换注释 Ctrl + / Cmd + /

7、Timeline Panel & Profiles Panel

功能 Windows / Linux Mac
开启/停止 记录 Ctrl + E Cmd + E
保存时间轴数据 Ctrl + S Cmd + S
加载时间轴数据 Ctrl + O Cmd + O
开启/停止 记录 Ctrl + E Cmd + E

8、Chrome Browser 快捷键(非 DevTools)

以下的 Chrome 快捷键在日常使用中非常有用,它并不是特意为 DevTools开发的.

功能 Windows / Linux Mac
(页面查找)寻找下一个 Ctrl + G Cmd + G
(页面查找)寻找上一个 Ctrl + Shift + G Cmd + Shift + G
在隐身模式下打开一个新窗口 Ctrl + Shift + N Cmd + Shift + N
开启或关闭书签栏 Ctrl + Shift + B Cmd + Shift + B
查看历史记录 Ctrl + H Cmd + Y
查看下载记录 Ctrl + J Cmd + Shift + J
查看任务管理器 Shift + ESC Shift + ESC
标签浏览历史中的下一个页面 Alt + Right Alt + Right
标签浏览历史中的上一个页面 Backspace, Alt + Left Backspace, Alt + Left
高亮地址栏内容 F6, Ctrl + L, Alt + D Cmd + L, Alt + D
在地址栏输入一个 ? 后可以将它作为你的默认搜索引擎使用(英文输入法下) Ctrl + K, Ctrl + E Cmd + K, Cmd + E

小结

虽然不会使用或不知道快捷键并不影响开发,它只是解放了(大)部分依赖鼠标的操作,但是从开发效率以及熟练度上来说,还是非常建议使用的,毕竟,“工欲善其事,必先利其器”(首尾呼应,满分。鼓掌.jpg)。

一道面试题目引发的思考

起因

多列布局是前端一个经典的反复被提及的面试题目,最典型的即两列,左列定宽菜单栏,右列变宽为内容区域。

通常得到的答案无外乎左列浮动定宽,然后右列或浮动,或设置外边距,或绝对定位等等。偶尔会有面试者给出设置右列overflow属性的答案,心里就会有些惊喜,继而会继续追问,为什么这么设置就能实现效果,期待能有进一步惊喜,但基本大部分面试者都止步于这样设置,并不清楚原因。非常少的面试者会提到这样设置能够触发块级格式化上下文(Block Formatting Conext, BFC),如果继续追问触发BFC的原因,几乎没有一个面试者能给出比较满意的答案。

本文就是由这道面试题目引发的一些思考。针对设置overflow属性这一方法,做进一步的探讨。

overflow属性

overflow属性最常见的一个使用场景就是规定当内容溢出元素框时发生的事情。可能的值如下:

  • visible 默认值。元素内容不会被裁剪,元素框之外的内容仍然会呈现。
  • hidden 元素内容会被裁剪,并且元素 框之外的内容是不可见的。
  • scroll 元素内容会被裁剪,但是浏览器会显示滚动条以便查看其余的内容。
  • auto 浏览器自动处理元素内容的溢出,如果元素内容被裁剪,则浏览器会显示滚动条以便查看其余的内容。
  • inherit 规定应该从父元素继承 overflow 属性的值。

除此之外,也会经常看到通过overflow属性实现的一些效果,比如清除浮动,以及上面提到的两栏布局的实现。这些效果的实现,可能跟overflow属性的本意相差甚远,就像两种不相关的事务被硬生生的牵扯到了一起。其实不然,CSS Spec规范文档中还明确记录着overflow属性的另外一个重要作用。

overflow属性跟布局的关系

The CSS2.1 spec mandates that overflow other than visible establish a new "block formatting context".

CSS2.1规范中已经明确提出,设置overflow属性(非visible)能触发块级格式化上下文(Block Formatting Conext, BFC)。

BFC是个很大的话题,此处不展开,这里给出一个简化不精确的解释,BFC概念的引入,一定程度是为了特殊情况下布局计算的方便,元素触发BFC之后,其作用就相当于一个根容器,其内部的子元素会跟外界完全隔离开,子元素布局以及尺寸相关的计算都会以该元素容器为基础。

为什么overflow非visible值会触发BFC?

首先,设置overflow属性为visible的话,是一种默认情况,就相当于正常默认的布局,所有超出元素框的内容仍然会正常显示,不会被裁剪,也不会出现滚动条。但对于其它几种值的话(hidden, scroll, auto),元素的内容可能会被裁剪,此时,对于某些情况下可能出现的特殊布局处理就会出现争议。

比如对于垂直方向紧贴着的两个元素A和B,其中元素A中浮动的子元素可能会遮住元素B的部分文字区域,此时如果元素B的overflow属性设置为visible,则内容会包裹在元素A浮动子元素的周围,这种情况比较容易理解,如下图。

overflow属性设置为visible

图1 overflow属性设置为visible

但当元素B的overflow属性设置为非visible的值时,各版本规范的规定就会出现差异。

CSS2.0规范规定,设置非visible属性值后,元素B的内容仍然包裹浮动元素,如图2所示。

overflow属性设置为novisible,CSS2.0规范中的处理

图2 overflow属性设置为novisible,CSS2.0规范中的处理

此后如果元素B内容发生滚动,每次滚动行为,元素B中发生折叠的内容(图3中元素B中文字内容滚动后发生变化)全部要重新计算重绘,实际上这将会带来很大的性能问题,对滚动体验也会造成比较大的影响。

overflow属性设置为novisible,CSS2.0规范中发生滚动时的处理

图3 overflow属性设置为novisible,CSS2.0规范中发生滚动时的处理

但这里存在进一步的疑问,即使按此规范的约定,元素B内容滚动时存在性能以及体验问题,但是非visible属性中的hidden值,难以理解,元素内容已经被裁剪掉了,为什么跟其它值auto, scroll归为一类?这里面就存在一个误区,overflow设置为hidden值并不代表内容不可滚动,此时浏览器只不过没有提供可滚动的UI,被"裁剪"掉的内容可以通过JavaScript脚本来控制滚动,这也是脚本模拟滚动条的基础。比如,可以通过JavaScript脚本设置元素的scrollTop实现图4的效果,更友好的方式可以自定义一个滚动条。

overflow属性设置为hidden,CSS2.0规范中发生滚动时的处理

图4 overflow属性设置为hidden,CSS2.0规范中发生滚动时的处理

事实上各大浏览器厂商也都没有遵照CSS2.0来实现这一部分规范。取而代之,实现的是CSS2.1中的规范内容,即当元素B的overflow属性设置为非visible值时会触发BFC,元素B会创建自己的块级格式化上下文,并会被整体推向右侧,如图5所示。

overflow属性设置为nonvisible,CSS2.1规范中的处理

图5 overflow属性设置为nonvisible,CSS2.1规范中的处理

备注 上面各图均来自于参考文献3

收尾

事实上,一些常见的其它布局技巧也都是基于上述的原理点,比如overflow属性非visible值可以用于清除浮动。如果一个面试者,能够比较清楚地讲出上面的各点,相信每个面试官心里面都会比较惊喜,上面只是自己的一些想法,可能会有些许的钻牛角尖,但单从这种对细节的钻研把控程度,候选人就一定不会太差,对候选人来说必然会有很大程度的加分。

上面只是针对两列布局这道题目一种方案的单方面探讨,这种方案有哪些优缺点等等都未提及,如果对每种方案都进行类似程度的拓展,将会发现这其中会涵盖很多前端知识点,所以看似简单的题目其实并不简单。越发觉得前端领域的水很深,伙伴们一起来努力探索实践吧!

参考文献

Written by Hong

Vuex 源码分析


title: Vuex 源码分析
date: 2017-05-23

本文解读的Vuex版本为2.3.1

Vuex代码结构

Vuex的代码并不多,但麻雀虽小,五脏俱全,下面来看一下其中的实现细节。

源码分析

入口文件

入口文件src/index.js:

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

这是Vuex对外暴露的API,其中核心部分是Store,然后是install,它是一个vue插件所必须的方法。Store
和install都在store.js文件中。mapState、mapMutations、mapGetters、mapActions为四个辅助函数,用来将store中的相关属性映射到组件中。

install方法

Vuejs的插件都应该有一个install方法。先看下我们通常使用Vuex的姿势:

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)

install方法的源码:

export function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

方法的入参_Vue就是use的时候传入的Vue构造器。
install方法很简单,先判断下如果Vue已经有值,就抛出错误。这里的Vue是在代码最前面声明的一个内部变量。

let Vue // bind on install

这是为了保证install方法只执行一次。
install方法的最后调用了applyMixin方法。这个方法定义在src/mixin.js中:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

方法判断了一下当前vue的版本,当vue版本>=2的时候,就在Vue上添加了一个全局mixin,要么在init阶段,要么在beforeCreate阶段。Vue上添加的全局mixin会影响到每一个组件。mixin的各种混入方式不同,同名钩子函数将混合为一个数组,因此都将被调用。并且,混合对象的钩子将在组件自身钩子之前。

来看下这个mixin方法vueInit做了些什么:
this.$options用来获取实例的初始化选项,当传入了store的时候,就把这个store挂载到实例的$store上,没有的话,并且实例有parent的,就把parent的$store挂载到当前实例上。这样,我们在Vue的组件中就可以通过this.$store.xxx访问Vuex的各种数据和状态了。

Store构造函数

Vuex中代码最多的就是store.js, 它的构造函数就是Vuex的主体流程。

  constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }

依然,先来看看使用Store的通常姿势,便于我们知道方法的入参:

export default new Vuex.Store({
  state,
  mutations
  actions,
  getters,
  modules: {
    ...
  },
  plugins,
  strict: false
})

store构造函数的最开始,进行了2个判断。

assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

这里的assert是util.js里的一个方法。

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

先判断一下Vue是否存在,是为了保证在这之前store已经install过了。另外,Vuex依赖Promise,这里也进行了判断。
assert这个函数虽然简单,但这种编程方式值得我们学习。
接着往下看:

const {
  plugins = [],
  strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}

这里使用解构并设置默认值的方式来获取传入的值,分别得到了plugins, strict 和state。传入的state也可以是一个方法,方法的返回值作为state。

然后是定义了一些内部变量:

// store internal state
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

this._committing 表示提交状态,作用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改state。
this._actions 用来存放用户定义的所有的 actions。
this._mutations 用来存放用户定义所有的 mutatins。
this._wrappedGetters 用来存放用户定义的所有 getters。
this._modules 用来存储用户定义的所有modules
this._modulesNamespaceMap 存放module和其namespace的对应关系。
this._subscribers 用来存储所有对 mutation 变化的订阅者。
this._watcherVM 是一个 Vue 对象的实例,主要是利用 Vue 实例方法 $watch 来观测变化的。
这些参数后面会用到,我们再一一展开。

继续往下看:

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

如同代码的注释一样,绑定Store类的dispatch和commit方法到当前store实例上。dispatch 和 commit 的实现我们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测所有的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,否则会有一定的性能开销。

构造函数的最后:

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
Vuex的初始化核心

installModule

使用单一状态树,导致应用的所有状态集中到一个很大的对象。但是,当应用变得很大时,store 对象会变得臃肿不堪。

为了解决以上问题,Vuex 允许我们将 store 分割到模块(module)。每个模块拥有自己的 state、mutation、action、getters、甚至是嵌套子模块——从上至下进行类似的分割。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

在进入installModule方法之前,有必要先看下方法的入参this._modules.root是什么。

this._modules = new ModuleCollection(options)

这里主要用到了src/module/module-collection.js 和 src/module/module.js

module-collection.js:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.root = new Module(rawRootModule, false)

    // register all nested modules
    if (rawRootModule.modules) {
      forEachValue(rawRootModule.modules, (rawModule, key) => {
        this.register([key], rawModule, false)
      })
    }
  }
  ...
}

module-collection的构造函数里先定义了实例的root属性,为一个Module实例。然后遍历options里的modules,依次注册。

看下这个Module的构造函数:

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  ...
}

这里的rawModule一层一层的传过来,也就是new Store时候的options。
module实例的_children目前为null,然后设置了实例的_rawModule和state。

回到module-collection构造函数的register方法, 及它用到的相关方法:

register (path, rawModule, runtime = true) {
  const parent = this.get(path.slice(0, -1))
  const newModule = new Module(rawModule, runtime)
  parent.addChild(path[path.length - 1], newModule)

  // register nested modules
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}

get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

addChild (key, module) {
  this._children[key] = module
}

get方法的入参path为一个数组,例如['subModule', 'subsubModule'], 这里使用reduce方法,一层一层的取值, this.get(path.slice(0, -1))取到当前module的父module。然后再调用Module类的addChild方法,将改module添加到父module的_children对象上。

然后,如果rawModule上有传入modules的话,就递归一次注册。

看下得到的_modules数据结构:

扯了一大圈,就是为了说明installModule函数的入参,接着回到installModule方法。

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)

通过path的length来判断是不是root module。

来看一下getNamespace这个方法:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}

又使用reduce方法来累加module的名字。这里的module.namespaced是定义module的时候的参数,例如:

export default {
  state,
  getters,
  actions,
  mutations,
  namespaced: true
}

所以像下面这样定义的store,得到的selectLabelRule的namespace就是'selectLabelRule/'

export default new Vuex.Store({
  state,
  actions,
  getters,
  mutations,
  modules: {
    selectLabelRule
  },
  strict: debug
})

接着看installModule方法:

// register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

传入了namespaced为true的话,将module根据其namespace放到内部变量_modulesNamespaceMap对象上。

然后

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state)
  })
}

getNestedState跟前面的getNamespace类似,也是用reduce来获得当前父module的state,最后调用Vue.set将state添加到父module的state上。

看下这里的_withCommit方法:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

this._committing在Store的构造函数里声明过,初始值为false。这里由于我们是在修改 state,Vuex 中所有对 state 的修改都会用 _withCommit函数包装,保证在同步修改 state 的过程中 this._committing 的值始终为true。这样当我们观测 state 的变化时,如果 this._committing 的值不为 true,则能检查到这个状态修改是有问题的。

看到这里,可能会有点困惑,举个例子来直观感受一下,以 Vuex 源码中的 example/shopping-cart 为例,打开 store/index.js,有这么一段代码:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

这里有两个子 module,cart 和 products,我们打开 store/modules/cart.js,看一下 cart 模块中的 state 定义,代码如下:

const state = {
  added: [],
  checkoutStatus: null
}

运行这个项目,打开浏览器,利用 Vue 的调试工具来看一下 Vuex 中的状态,如下图所示:

来看installModule方法的最后:

const local = module.context = makeLocalContext(store, namespace, path)

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

module.forEachAction((action, key) => {
  const namespacedType = namespace + key
  registerAction(store, namespacedType, action, local)
})

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

local为接下来几个方法的入参,我们又要跑偏去看一下makeLocalContext这个方法了:

/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (!store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (!store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

就像方法的注释所说的,方法用来得到局部的dispatch,commit,getters 和 state, 如果没有namespace的话,就用根store的dispatch, commit等等

以local.dispath为例:
没有namespace为''的时候,直接使用this.dispatch。有namespace的时候,就在type前加上namespace再dispath。

local参数说完了,接来是分别注册mutation,action和getter。以注册mutation为例说明:

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(local.state, payload)
  })
}

根据mutation的名字找到内部变量_mutations里的数组。然后,将mutation的回到函数push到里面。
例如有这样一个mutation:

mutation: {
  increment (state, n) {
    state.count += n
  }
}

就会在_mutations[increment]里放入其回调函数。

commit

前面说到mutation被放到了_mutations对象里。接下来看一下,Store构造函数里最开始的将Store类的dispatch和commit放到当前实例上,那commit一个mutation的执行情况是什么呢?

  commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      console.error(`[vuex] unknown mutation type: ${type}`)
      return
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (options && options.silent) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

方法的最开始用unifyObjectStyle来获取参数,这是因为commit的传参方式有两种:

store.commit('increment', {
  amount: 10
})

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({
  type: 'increment',
  amount: 10
})
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)

  return { type, payload, options }
}

如果传入的是对象,就做参数转换。
然后判断需要commit的mutation是否注册过了,this._mutations[type],没有就抛错。
然后循环调用_mutations里的每一个mutation回调函数。
然后执行每一个mutation的subscribe回调函数。

Vuex辅助函数

Vuex提供的辅助函数有4个:

以mapGetters为例,看下mapGetters的用法:

代码在src/helpers.js里:

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (!(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})


function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

normalizeNamespace方法使用函数式编程的方式,接收一个方法,返回一个方法。
mapGetters接收的参数是一个数组或者一个对象:

computed: {
// 使用对象展开运算符将 getters 混入 computed 对象中
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
    // ...
  ])
}
mapGetters({
  // 映射 this.doneCount 为 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})

这里是没有传namespace的情况,看下方法的具体实现。
normalizeNamespace开始进行了参数跳转,传入的数组或对象给map,namespace为'' , 然后执行fn(namespace, map)
接着是normalizeMap方法,返回一个数组,这种形式:

{
  key: doneCount,
  val: doneTodosCount
}

然后往res对象上塞方法,得到如下形式的对象:

{
  doneCount: function() {
    return this.$store.getters[doneTodosCount]
  }
}

也就是最开始mapGetters想要的效果:

by kaola/fangwentian

Clean Code 阅读总结

1 开始

本文是在阅读 clean code 时的一些总结,原书是基于 Java 的,这里将其中的一些个人认为实用性较强且容易与日常业务开发结合的一些原则重新进行整理,并参考了 clean-code-javascript 一文给出了一些代码实例,希望本文能够给日常开发编码和重构作出一些参考。

2 有意义的命名

2.1 名副其实

变量取名要花心**想,不要贪图方便,过于简略的名称,时间长了以后就难以读懂。

// bad
var d = 10;
var oVal = 20;
var nVal = 100;


// good
var days = 10;
var oldValue = 20;
var newValue = 100;

2.2 避免误导

命名不要让人对变量的信息 (类型,作用) 产生误解。

accounts 和 accountList,除非 accountList 真的是一个 List 类型,否则 accounts 会比 accountList 更好。因此像 List,Map 这样的后缀,不要随意使用。

// bad
var platformList = {
    web: {},
    wap: {},
    app: {},
};


// good
var platforms = {
    web: {},
    wap: {},
    app: {},
};

2.3 做有意义的区分

用明确的意义去表述变量直接的区别。

很多情况下,会有存在 product,productData,productInfo 之类的命名,Data 和 Info 很多情况下并没有明显的区别,不如直接就使用 product。

// bad
var goodsInfo = {
    skuDataList: [],
};

function getGoods(){};          // 获取商品列表
function getGoodsDetail(id){};  // 通过商品ID获取单个商品


// good
var goods = {
    skus: [],
};

function getGoodsList(){};      // 获取商品列表
function getGoodsById(id){};    // 通过商品ID获取单个商品

2.4 使用读得出来的名称

缩写要有个度,比如像 DAT 这样的写法,到底是 DATA 还是 DATE...

// bad
var yyyyMMddStr = eu.format(new Date(), 'yyyy-MM-dd');
var dat = null;
var dev = 'Android';


// good
var todaysDate = eu.format(new Date(), 'yyyy-MM-dd');
var data = null;
var device = 'Android';

2.5 使用可搜索的名称

可搜索的名称能够帮助快速定位代码,尤其对于一些数字状态码,不建议直接使用数值,而是使用枚举。

// bad
var param = {
    periodType: 0,
};


// good
const HOUR = 0, DAY = 1;
var param = {
    periodType: HOUR,
};

2.6 避免使用成员前缀

把类和函数做得足够小,消除对成员前缀的需要。因为长期以后,前缀在人们眼里会变得越来越不重要。

2.7 添加有意义的语境

对于某些名称,在不同语境下可能代表不同的含义,最好为它添加有意义的语境。

firstName,lastName,street,houseNumber,city,state,zipcode 一连串变量放在一起可以判断是一个地址,但是如果将这些变量单独拎出来,有些变量名意义就不明确了。这时可以添加语境明确其意义,如 addrFirstName,addrLastName,addrState。

当然也不要随意添加语境,这样只会让变量名变得冗长。

// bad
var firsName, lastName, city, zipcode, state;
var sku = {
    skuName: 'sku0',
    skuStorage: 'storage0',
    skuCost: '10',
};


// good
var addrFirsName, addrLastName, city, zipcode, addrState;
var sku = {
    name: 'sku0',
    storage: 'storage0',
    cost: '10',
};

2.8 变量名从一而终

变量名取名多花一点时间,如果这一对象会在多个函数,模块中使用,就应该使用一致的变量名,否则每次看到这个对象,都需要重新去理清变量名,造成阅读障碍。

// bad
function searchGoods(searchText) {
    getList({
        keyword: searchText,
    });
}
function getList(option) {

}

// good
function searchGoods(keyword) {
    getList({
        keyword: keyword,
    });
}

function getList(keyword) {}

3 函数

3.1 短小

短小是函数的第一规则,过长的函数不仅会造成阅读困难,在维护的时候难度也会增加。短小,要求每个函数做尽可能少的事情,同时减少代码的嵌套和缩进,要知道,代码的嵌套和缩减同样会带来阅读的困难。

// bad
function initPage(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

// good
function initPage(initParams) {
    initDimension(initParams);
    initStandardMedium(initParams);
    initPlanQueryString(initParams);
}
function initDimension(initParams) {
    var data = this.data;
    if ('dimension' in initParams) {
        data.dimension = initParams.dimension;
        data.tab.source.some(function(item, index){
            if (item.value === data.dimension) {
                data.tab.defaultIndex = index;
            }
        });
    }
}
function initStandardMedium(initParams) {
    var data = this.data;
    if ('standardMedium' in initParams) {
        data.hasStandardMedium = true;
        data.filterParams[data.dimension].standardMedium = initParams.standardMedium;
    }
}
function initPlanQueryString() {
    var data = this.data;
    if ('plan' in initParams || 'name' in initParams) {
        data.filterParams[data.dimension].planQueryString = initParams.plan || initParams.name;
    } else if ('traceId' in initParams) {
        data.filterParams[data.dimension].planQueryString = 'id:' + initParams.traceId;
    }
}

3.2 只做一件事情

函数应该做一件事情,做好这件事,只做这一件事。

如果函数只是做了该函数名下同一个抽象层上的步骤,则函数还是只做了一件事。当函数中出现另一抽象层级所做的事情时,则可以将这部分拆成另一层级的函数,因此缩小函数。

当一个函数可以被划分成多个区段时(代码块)时,这就说明了这个函数做了太多事情。

// bad
function onTimepickerChange(type, e) {
    if(type === 'base') {
        // do base type logic...
    } else if (type === 'compare') {
        // do compare type logic...
    }
    // do other stuff...
}

// good
function onBaseTimepickerChange(e) {
    // do base type logic
    this.doOtherStuff();
}

function onCompareTimepickerChange(e) {
    // do compare type logic
    this.doOtherStuff();
}

function doOtherStuff(){}

3.3 每个函数一个抽象层级

一个函数中不应该混杂了多个抽象层级,即同一级别的步骤才放到一个函数中,因为通过这些步骤就能完整地完成一件事情。

回到之前提到变量命名的问题,一个变量或函数,其作用域余越广,就越需要一个有意义的名字来对其进行描述,提高可读性,减少在阅读代码时还需要去查询定义代码的频率,有些时候有意义的名字就可能需要更多的字符,但这是值得的。但对于小范围使用的变量和函数,可以适当缩短名称。因为过长的名称,某些时候反而会增加阅读的困难。

可以通过向下原则划分抽象层级

程序就像是一系列 TO 起头的段落,每一段都描述当前层级,并引用位于下一抽象层级的后续 TO 起头段落
- 如果要完成 A,需要完成 B,完成 C;
- 要完成 B,需要完成 D;
- 要完成 C,需要完成 E;

函数名明确了其作用,获取一个图表和列表,函数中各个模块的逻辑进行了划分,明确各个函数的分工, 拆分的函数名直接表明了每个步骤的作用, 不需要额外的注释和划分。在维护的时候, 可以快速的定位各个步骤, 而不需要在一个长篇幅的函数中需找对应的代码逻辑.

实际业务例子, 数据门户-流量看板-流量总览的一个获取趋势图和右边列表的例子。选择一个通过 tab 选择不同的指标,不同的指标影响的趋势图和右边列表的内容,两个模块的数据合并到一个请求中得到。流水账的写法可以将函数写成下面的样子,这种写法有几个明显的缺点:

  • 长。通常情况下趋势图配置可能就需要20多行,整个函数加起来,轻易就超过50行了;
  • 函数名不准确。函数名仅表明是获取一个图表的,但实际上还获取了右边列表数据并进行了配置;
  • 函数层级混乱,还可以进行更细的划分;

根据向下原则

// bad
getChart: function(){
    var data = this.data;
    var option = {
        url: '/chartUrl',
        param: {
            dimension: data.dimension,
            period: data.period,
            comparePeriod: data.comparePeriod,
            periodType: data.periodType,
        },
        fn: function(json){
            var data = this.data;
            // 设置图表
            data.chart = json.data.chart;
            data.chart.config = {
                //... 大量的图表配置,可能有20多行
            }
            // 设置右边列表
            data.sideList = json.data.list;
        }
    };
    // 获取请求参数
    this.fetchData(option);
},

// good
getChartAndSideList: function(){
    var option = {
        url: '/chartUrl',
        param: this.getChartAndSideListParam();
        fn: function(json){
            this.setChart(json);
            this.setSideList(json);
        }
    };
    this.fetchData(option);
},

3.4 switch语句

switch语句会让代码变得很长,因为switch语句天生就是要做多件事情,当状态不断增加的时候,switch语句也会不断增加。因此可能把取代switch语句,或者将其放在较低的层级.

放在底层的意思,可以理解为将其埋藏到抽象工厂地下,利用抽象工厂返回内涵不同的方法或对象来进行处理.

3.5 减少函数的参数

函数的参数越多,不仅注释写得长,使用的时候容易使得函数参数发生错位。当函数参数过多时,可以考虑以参数列表或者对象的形式传入.

数据门户里面的一个例子:

// bad
function getSum(a [, b, c, d, e ...]){}


// good
function getSum(arr){}
// bad
function exportExcel(url, param, onsuccess, onerror){}


// good
/**
 * @param option
 *    @property url
 *    @property param
 *    @property onsucces
 *    @property onerror
 */
function exportExcel(option){}

参数尽量少,最好不要超过 3 个

3.6 取个好名字

函数应该取个好一点的名字,适当使用动词和关键字可以提高函数的可读性。例如:

一个判断是否在某个区间范围的函数,取名为 within,从名称上可以容易判断出函数的作用,但是这仍然不是最好的,因为这个函数带有三个参数,无法一眼看出这个函数三个参数之间的关系,是 b <= a && a<= c,还是 a <= b && b <= c ?

或许可以通过更改参数名来表达三个参数的关系,这个必须看到函数的定义后才可能得知函数的用法.

如果再把名字改一下,从名字就可以容易得知三个参数依次的关系,当然这个名字可能会很长,但如果这个函数需要大范围地使用,较长的名字换来更好的可读性,这一代价是值得的.

// bad
function within(a, b, c){}

// good
function assertWithin(val, min, max){}

// good
function assertValWithinMinAndMax(val, min, max){}

3.7 无副作用

一个有副作用的函数,通常都是是非纯函数,这意味着函数做的事情其实不止一件,函数所产生的副作用被隐藏了,函数调用者无法直接通过函数名来明确函数所做的事请.

4 注释

4.1 好注释

法律信息,提供信息的注释,对意图的解释,阐释,警示,TODO,放大(放大某种看似不合理代码的重要性),公共 API 注释

尽量让函数,变量变得刻度,不要依赖注释来描述,对于复杂难懂的部分才适当用注释说明.

4.2 坏注释

喃喃自语,多余的注释(例如本来函数名就能够说明意图,还要加注释),误导性注释,循规式注释(为了规范去加注释,其实函数名和参数名已经可以明确信息了),日志式注释(记录无用修改日志的注释),废话注释

4.3 原则

  1. 能用函数或变量说明时,就别用注释,这就意味着要花点时间取个好名字
// bad
var d = 10;     // 天数

// good
var days = 10;
  1. 注释掉的代码不要留,重要的代码是不会被注释掉的

数据门户-实时概况里面的一段代码,/src/javascript/realTimeOverview/components/index.js

// bad
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    // 2016.10.31 modify:产品改动,选择品牌分布的时候不显示二级类目
    // if (dimension.dimensionId == '6') {
    //     data.columns[0][0].name = dimension.dimensionName;
    //     data.columns[0].splice(1, 0, {name:'二级类目', value:'secCategoryName', noSort: true});
    // } else {
        this.handle('util.setTableHeader');
    // }
    this.handle('refreshComposition');
};

// good
function dimensionChanged(dimension){
    var data = this.data.keyDealComposition;
    data.selectedDimension = dimension;
    this.handle('util.setTableHeader');
    this.handle('refreshComposition');
};
  1. 不要在注释里面加入太多信息,没人会看

  2. 非公用函数,没有必要加过多的注释说明,冗余的注释会使代码变得不够紧凑,增加阅读障碍

// bad
/**
 * 设置表格表头
 */
function setTableHeader(){},

// good
function setTableHeader(){},
  1. 括号后的注释
// bad
function doSomthing(){
    while(!buffer.isEmpty()) {  // while 1
        // ...
        while(arr.length > 0) {  // while 2
            // ...
            if() {

            }
        } // while 2
    } // while 1
}
  1. 不需要日志式,归属式注释,相信版本控制系统
// bad
/**
 * 2016.12.03 bugfix, by xxxx
 * 2016.11.01 new feature, by xxxx
 * 2016.09.12 new feature, by xxxx
 * ...
 */


// bad
/**
 * created by xxxx
 * modified by xxxx
 */
function addSum() {}

/**
 * created by xxxx
 */
function getAverage() {
    // modified by xxx
}
  1. 尽量别用用位置标记
// bad

/*************** Filters ****************/

///////////// Initiation /////////////////

5 格式

5.1 垂直方向

  1. 相关代码紧凑显示,不同部分的用空格隔开
// bad
function init(){
    this.data.chartView = this.$refs.chartView;
    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });
    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (self.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}

// good
function init(){
    this.data.chartView = this.$refs.chartView;

    this.$parent.$on('inject', function () {
        this.dataConvert(this.data.source);
        this.draw();
    });

    this.$watch('source', function (newValue, oldValue) {
        if (newValue && newValue != this.data.initValue) {
            this.dataConvert(newValue);
            this.draw();
        } else if (!newValue) {
            if (this.data.chartView) {
                this.data.chartView.innerHTML = '';
            }
        }
    }, true);
}
  1. 不要在代码中加入太多过长的注释,阻碍代码阅读
// bad
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        var data = this.data;
        this.checkAllList(status);
        this.checkSigList(status);
        data.checked.list = [];
        if(status){
            // 当全选的时候先清空列表, 然后在利用Array.push添加选中项
            // 如果在全选的时候不能直接checked.list = dataList
            // 因为这样的话后面对checked.list的操作就相当于对dataList直接进行操作
            // 利用push可以解决这一个问题
            data.sigList.forEach(function(item,i){
                data.checked.list.push(item.data.item);
            })
        }
        this.$emit('check', {
            sender: this,
            index: CHECK_ALL,
            checked: status,
        });
    },
});

// good
BaseComponent.extend({
    checkAll: function(status){
        status = !!status;
        this.checkAllList(status);
        this.checkSigList(status);
        this.clearCheckedList();
        if(status){
            this.updateCheckedList();
        }

        this.emitCheckEvent(CHECK_ALL, status);
    },
});
  1. 函数按照依赖顺序布局,被调用函数应该紧跟调用函数
// bad
function updateModule() {}
function updateFilter() {}
function reset() {}
function refresh() {
    updateFilter();
    updateModule();
}

// good
function refresh() {
    updateFilter();
    updateModule();
}
function updateFilter() {}
function updateModule() {}
function reset() {}
  1. 相关的,相似的函数放在一起
// bad
function onSubmit() {}
function refresh() {}
function onFilterChange() {}
function reset() {}

// good
function onSubmit() {}
function onFilterChange() {}

function refresh() {}
function reset() {}
  1. 变量声明靠近其使用位置
// bad
function (x){
    var a = 10, b = 100;
    var c, d;

    a = (a-b) * x;
    b = (a-b) / x;
    c = a + b;
    d = c - x;
}

// good
function (x){
    var a = 10, b = 100;

    a = (a-b) * x;
    b = (a-b) / x;

    var c = a + b;
    var d = c - x;
}

5.2 水平方向

  1. 运算符号之间空格,但是要注意运算优先级
// bad
var v = a + (b + c) / d + e * f;

// good
var v = a + (b+c)/d + e*f;
  1. 变量水平对齐意义不大,应该让其靠近
// bad
var a       = 1;
var sku     = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

// good
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;

5.4 对于短小的if,while语句,也要尽量保持缩进

突然间改变缩进的规律,很容易就会被阅读习惯欺骗

// bad
if(empty){return;}


// good
if(empty){
    return;
}

// bad
while(cli.readCommand() != -1);
app.run();


// good
while(cli.readCommand() != -1)
;

app.run();

6 实际业务代码中的应用

庞大的config函数

对于一些较为复杂的组件或页面组件,需要定义很多属性,同时又要对这部分属性进行初始化和监听,像下面这段代码。在好几个大型的页面里面都看到了类似的代码,config 方法少的有 100行,多的有 400行。

config 方法基本就是一个组件的入口,在进行维护的时候一般都会先读 config 方法,但是对于这么长的函数,很容易第一眼就懵了。

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: 0,
            periodType: 0,
            dimensionType: 1,
            dealConstituteCompare:false,
            dealConstituteSort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            dealConstituteDecorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
            // ...下面还有几百行关于其他模块的属性, flow, hotSellRank等
        });

        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });

        // 这里还有一部分异步请求代码...
        this.refresh();
    },
})

针对上述这段代码代码,明显的缺点是:

  • 太长
  • 变量命名有冗余信息,且搜索性差
  • 变量(属性)太多
  • 做的事情太多,初始化组件属性,添加监听方法,还有一些业务逻辑代码

这对这些可以作出一些改进:

  • 使用枚举代替数值
  • config内只保留一切作为范围加大属性的直接初始化代码,其余针对于模块的属性将通过调用 initData 方法来初始化
  • initData 进一步根据模块划分初始化方法
  • 对于属于摸个模块的属性,则将其划分到同一个对象上,减少组件上挂载的属性数量,同时也简化了属性的命名
  • 监听方法同样是通过 addWatchers 初始化
  • 初始化过程中需要执行的部分逻辑,尽可能放在 init 等组件实例化后执行
const TAB_A = 0, TAB_B = 1;
const HOUR = 0, DAY = 1;
const DIMENSION_A = 0, DIMENSION_B = 1;
const DISABLE = false, ENABLE = true;

Component.extend({
    template: tpl,
    config: function(data){
        eu.extend(data, {
            tabChartTab: TAB_A,
            periodType: HOUR,
            dimensionType: DIMENSION_B,
        });

        this.initData();
        this.addWatchers();
    },

    initData: function(){
        this.initDealConsitiuteData();
        this.initFlowData();
        this.initHotSellRank();
    },

    initDealConsitiuteData: function(){
        this.data.dealConstitute = {
            compare: DISABLE,
            sort: {
                dimensionValue: 'sales',
                sortType: 0,
            },
            decorate: {
                noCompare:[],
                progress: ['salesPercent'],
                sort:[
                ]
            },
            defaultMetrics: [
            ],
        }
    },

    addWatchers: function(){
        this.$watch('periodType', function(){
            // ...
        });

        this.$watch('topCategoryId', function(){
            // ...
        });
    },

    init: function(){
        // 部分初始化要执行的逻辑
        this.refresh();
    },

})

其实按照上面进行优化以后,代码的可读性是有所提高,但由于这是一个页面组件,代码行数极多,修改后方法变得更多了,仍然不便于阅读。所以,针对于这种大型的页面,更适当的做法是,将页面拆分为几个模块,将业务逻辑拆分,减少每个模块的代码量,提高可读性。而对于不可再拆分的组件或模块,如果仍然包含大量需要初始化的属性,上述例子就可以作为参考了。

7 总结

本文整理的几个要点:

  • 写代码就像写故事,里面各个角色 (变量,函数) 的名字要取得好,才读得流畅;
  • 函数要短小,不要混杂太多不相关,不同层级的逻辑;
  • 注释要精简准确,能不写就不要写;
  • 代码布局要向报纸学习,排版注意垂直与水平方向的间隔,联系紧密的布局要紧凑;

就算是经验老道的大神,也很难一遍就能写出简洁的代码,所以要勤于对代码进行重构,边写代码边修改。代码只有在经过一遍一遍修改和锤炼以后,才会逐渐地变得简洁和精致。

8 参考

  1. Clean Code
  2. clean-code-javascript

来我们探讨下低版本regularJs中redux的接入姿势

论低版本regularJs中redux的接入姿势

之前的项目由于业务的复杂度,都不大需要组件间的状态管理,加上复杂度仅一点点的工程一般轻量级的pubsub-js就能应付,所以一直没有使用redux的机会,借由最近的一个‘搭建系统’后台的这个项目,接入了redux,这里抛砖引玉,跟大家探讨下如何在低版本的regularJs中接入redux吧

一言不合先丢代码吧 regular-redux-lowversion

coming soon...

前端重构感想

考拉PC前端重构之路

@(重构)[组件化|拆分]

代码重构是一个产品不断的功能迭代过程中,不可避免的一项工作。所谓重构,是在不改变程序的输入输出即保证现有功能的情况下,对内部实现进行优化和调整。 每个开发人员从业生涯中,或多或少的做过重构工作。小到重写一个功能函数、业务组件,大到重构一个复杂功能模块或整站重构。

重构是需要花费一定成本和精力的,尤其是一个有各种历史遗留问题(用的老旧框架或工具)、又糅杂了各种业务逻辑(有些功能逻辑连需求方都未必清楚)、且可读性及维护性都很差的重要功能模块,比如考拉的下单页,不动,每次功能迭代的时候都有想撞墙的心,大动,又是不小的工作量,且有一定的风险。当然长痛不如短痛,长远来看,重构势在必行。

why 重构

  • 设计不合理or全无设计 :原来的实现方式不合理,或者全无模块拆分、组件提取意识的开发模式,纯粹的业务逻辑堆叠式开发,会导致功能无法重用,代码冗余,不利于维护。
  • **页面结构与功能实现耦合 ** :脚本文件中夹杂着各种html结构,表现和行为不分离,导致代码可读性和维护性大大下降。(我不会告诉你旧版考拉下单页的入口脚本有2000多行,cry~)。
  • 代码难以理解 :页面没有清晰的入口函数,函数调用关系混乱,无论改bug还是迭代新功能,面对难以理解的代码,开发效率低下。
  • 代码引起性能问题:不合理的实现方式或者大量无用冗余的代码,引起了明显性能问题的,需要即时重构优化。
  • 框架or类库更新:前端的技术框架日新月异,在选择或者淘汰不合适项目的第三方库的时候,也涉及到重构工作。

when 重构

重构工作其实是随时都可进行的。当你觉得代码可读性变差、重用性及可维护性降低时,都应该有意识的去做重构。在大部分的项目中,以下将是重构的合理时间点:

  • 功能迭代时:当添加一个功能时,发现原有的设计无法满足新需求,可以考虑重构;在设计功能组件的时候,可以为未来可能的需求预留接口;
  • 修复bug的时候:bug产生之后,需要思考是否是不合理的编码导致的问题而不单单是以解决bug算完事(当然线上重大bug自然以即时修复为第一要务),可以借助修复bug的契机,把业务逻辑整理清晰,必要时可以找后端配合改接口;
  • code Review阶段:此阶段距离提测时间往往较近,但如果是明显不合理的实现思路,宁愿delay也要重新设计实现;如果前期思考充分,一般不会出现此类情况,大部分是在给其他人review的时候,发现不可理解、可读性差,这也是需要进一步修改重写的。

当然在需求紧急的时候,是不适合做重构的。


do重构

以考拉下单页重构为例。

但凡重构,必须要对已有业务逻辑有较充分的了解,评估重构的影响面及可能的风险, 列出基本功能点(也可作为QA的测试回归点),保证重构后不要有功能遗漏,不改变现有功能。

如何了解已有业务逻辑?

  • 跑线上流程;(有些特殊逻辑,模拟困难)
  • 对照现有的交互稿;(不断的功能迭代,可能找不到完整的交互稿)
  • 读懂已有代码;(全面了解业务逻辑最好的方法,但是耗时)
  • 询问之前的开发者;(如已离职,查找是否有交接文档之类)

功能模块拆分

在对业务逻辑有充分了解之后,可以进行功能拆分,把可重用的、功能独立的业务逻辑作为组件或公用模板提取,同时需要考虑组件之间的相互影响。拆分之后,可以多人并行开发,约定好外部调用组件的接口即可。
以下是下单页的功能模块拆分:

enter image description here

根据同步字段orderType,来确定是普通商品订单还是账号充值订单,并初始化对应的组件;根据同步信息,异步获取下单信息(含商品信息及结算信息);如果是普通商品订单,服务端会根据用户所选地址进行拆单,返回拆单后的商品和结算信息。按功能拆分后,组件及组件的嵌套关系如下:

enter image description here

下单页目录结构:

javascript
|——components
|  |——address/address.js //地址控件
|  |——checkcode/checkcode.js //验证码控件	
|——page/order
|  |——order_confirm.js //入口脚本
|  |——components
|  |  |——confirmGoods.js+html //确认商品信息
|  |  |——settlement.js+html //结算信息
|  |  |——exchangeCoupon.js+html //兑换优惠券
|  |  |——rechargeInfo.js+html //账号充值
|  |  |——invoiceInfo.js+html //发票信息
|  |  |——invoice.js+html //设置发票
|  |  |——bean.html //考拉豆抵扣
|  |  |——feeList.html //运费税费列表
|  |  |——gift.html //赠品信息
|  |  |——useCoupon.js+html //使用优惠券
|  |  |——submitCB.js //提交回调
|  |——widget
|  |  |——popCoupon.js //弹窗领券

after 重构

  • 入口明确,调用关系清晰,查找方便
  • 功能实现与结构不耦合,可读性增强
  • 组件化后,有利于功能重用
  • 提升了编码体验,维护及功能迭代不再感觉煎熬o(^▽^)o

last

重构从来不是一次性行为,是我们需要不断进行的工作。多人维护以及不断的功能迭代之后,代码多多少少都会有优化的空间,所以在你看到不合理或任何值得重构的地方时,行动起来吧,去优化,不要等到重构的成本更大时再进行,因为那会更痛,不要问我怎么知道。

chrome插件开发简介(二)——如何添“加浏览器扩展白名单”

chrome插件开发简介(二)——如何添“加浏览器扩展白名单”

没有在Chrome应用商店web store上架发布的插件,如果没有添加到白名单里,下一次重启Chrome就会被禁用,而且无法手动启用,除非删掉重新添加。
可以通过添加白名单可以一劳永逸地解决这个问题。

下面我们以“将日报插件添加到白名单”为例,讲解步骤

Mac下

mac系统下设置白名单比较简单,下载com.google.Chrome.mobileconfig,后,双击安装即可(过程中可能会要求输入系统管理员密码)

当然,为适配本插件,该文件内容已经更改:

1.文本编辑打开文件 com.google.Chrome.mobileconfig

2.找到 <array> 之下的 <string> 标签,然后在里面输入插件的id

3.保存之后,双击运行这个文件,期间可能会要求输入管理员密码,输入即可

4.重启浏览器

windows下

1.Win + R(或者打开左下角运行)输入gpedit.msc

2.本地计算机策略 > 计算机配置 > 管理模板,右键管理模板,选择添加/删除模板。

image

 
 

3.点击添加,将下载的chrome.adm(下载地址:res/chrome.adm)添加进来。

image

 
 

4.添加模板完成后,找到经典管理模板(ADM),点击进入,选择Google > Google Chrome

image

 
 

5.打开后如下图所示,选择扩展程序,点击进入,配置扩展程序安装白名单

image

  ↓↓↓

image

 
 

6.点击下图中的显示按钮,将我们安装插件后的ID添加到那个值列表中,点击确定返回即可。比如我们的是 nldeakmmeccgpaiacgpmabnjaenfdbkn

image

id信息如下:打开chrome的插件管理界面(地址栏 chrome://extensions),找到这个插件

image

这里需要提一点,如果未开启开发者模式,可能会看不到这个id

image

 
 

如何编写Hexo主题

最开始折腾Hexo的时候感觉这东西很神奇,通过他和github搭配就能生成免费的静态博客,而且还有丰富的主题可以选择,当我刚入Hexo的时候默认主题是landscape,后来又使用过NexT,是一款很漂亮的主题,但是除此之外,还有很多好看的主题,我很好奇这些主题都是怎么写出来的,于是乎就仿照landscape主题开始研究,写自己的主题,也就是我自己的博客正在用的主题,项目地址在这里

完成一个Hexo的主题其实很简单,和写静态页面差不多,只是内容部分通过Hexo的变量去获取,而且Hexo还内置了一些辅助函数帮你快速方便地完成繁琐的处理。

起步

在写代码之前要先把项目结构搭建好,一个Hexo主题的项目名就是主题名字本身,项目内的目录结构如下: (生成树形图是用的tree, mac上直接brew install tree就可以了,以前不写都不知道囧)

.
├── _config.yml   //记录主题配置信息
├── layout        //存放布局模板文件
│   └── _partial  //布局文件中可共用的模板
└── source        //静态资源文件夹
    ├── css
    ├── fonts
    ├── js
    └── sass

项目结构搞好就可以开始写代码了!因为当初我是仿landscape写的,而且ejs也是我之前看nodejs时就接触过的,因此就直接用ejs写模板文件了,样式使用了sass (scss

布局

编写布局文件(layout.ejs)

模板文件在layout文件夹下,文件名对应Hexo中的模板名,有index,post,page,archive,category,tag几种,对于普通的header + content + footer的页面结构,headerfooter往往是可以复用的,因此我们可以使用layout.ejs进行布局,动态的内容使用body变量去动态渲染,所以我的layout.ejs大概长这样:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
    <title><%= config.title %></title>
    <%- css('css/style') %>
</head>
<body>
    <%- partial('_partial/header') %>
    <div class="main">
        <%- body %>
    </div>
    <%- partial('_partial/footer') %>
    <%- js('js/index.js') %>
</body>
</html>

partial,jscss是Hexo提供的辅助函数,后面再说。

其他模板文件

每一个模板文件对应的是一种布局,当你使用hexo new <title>的时候,其实忽略了一个参数,完整的命令是hexo new [layout] <title>,这个layout就决定了文章使用何种方式布局,比如创建一个自己简介的About页面,hexo new page "about"其实就是使用了page布局。每种布局对应到我们的模板文件上就是index.ejs(首页),post.ejs(文章),archive.ejs(归档),tag.ejs(标签归档),page.ejs(分页)

如果更直观一点,url和模板的对应关系是这样的:

Url Description Layout
/ 首页 index.ejs
/yyyy/mm/dd/:title/ 文章 post.ejs
/archives/ 归档 archive.ejs
/tags/:tagname/ 某个标签的归档 tag.ejs
/:else/ 其他 page.ejs

index.ejs

首页一般是一些博文的摘要和一个分页器,通过Hexo的page变量拿到页面的数据渲染即可,这里我们不直接在index.ejs中写HTML结构,新建一个_partial/article.ejs,将文章数据传给子模板渲染,然后再额外传入一个参数{index: true},对后面的post.ejspage.ejs加以区分,让子模板能正确渲染。最后,index.ejs大致是这样的:

//index.ejs
<% page.posts.each(function(post, index){ %>
	<%- partial('_partial/article', {index: true, post: post}) %>
<% }) %>
<div class="pagination">
	<%- paginator({ total: Math.ceil(site.posts.length / config.per_page)}) %>
</div>

post.ejs

文章模板和首页差不多,只是对应的是一篇具体的文章,所以就把文章传入,再额外传入{index: false}告诉子模板不要按首页的方式去渲染就好了。就一行代码(因为都在子模板里 XD

//post.ejs
<%- partial('_partial/article', {index: false, post: page}) %>

page.ejs

我个人对Page模板其实是有点懵逼的,在我自己的实践中是添加about(hexo new page "about")页面后,访问/about会走分页布局,实际上这个页面对应的内容是/source/about里的index.md,也相当于对文章的渲染,因此我把Page模板也写成了和文章模板一样:

//page.ejs
<%- partial('_partial/article', {index: false, post: page}) %>

_partial/article.ejs

前面一共有三处共用了article模板,另外page和post的一样的,所以实际上只有两种情况:主页(index: true)和非主页(index: false)。对应的_partial/article.ejs里只要判断这个值就可以正确渲染了,基本结构如下:

//_partial/article.ejs
<% if(index){ %>
	//index logic...
<% }else{ %>
	//post or page logic...
<% } %>

tag.ejs

标签归档页内容很少,直接用Hexo的辅助函数list_tags生成一个标签的列表就ok了:

//tag.ejs
<%- list_tags() %>

归档页模板和首页差不多,归档页只需要展示文章标题和最后的分页器就好:

//archive.ejs
<div class="archive">
  <% var lastyear; %>
  <% page.posts.each(function(post){ %>
    <% var year = post.date.year() %>
    <% if(lastyear !== year){ %>
      <h4 class="year"><%= year %></h4>
      <% lastyear = year %>
    <% } %>
    <div class="archive_item">
      <a class="title" href="<%- url_for(post.path) %>"><%= post.title %></a>
      <span class="date"><%= post.date.format('YYYY-MM-DD') %></span>
    </div>
  <% }) %>
  <div class="pagination">
    <%- paginator({ total: Math.ceil(site.posts.length / config.per_page)}) %>
  </div>
</div>

至此,模板文件就写好了,对于category 模板就放弃了,感觉比较鸡肋。。。

变量

其实在模板文件中我们已经看到了page.post,site.posts.length,config.per_page等等,页面的内容就是根据这些变量获取的,由Hexo提供,拿来直接用,Hexo提供了很多变量,但不是都很常用,一般就用到以下变量:

  • site: 对应整个网站的变量,一般会用到site.posts.length制作分页器
  • page: 对应当前页面的信息,例如我在index.ejs中使用page.posts获取了当前页面的所有文章而不是使用site.posts
  • config: 博客的配置信息,博客根目录下的_config.yml
  • theme: 主题的配置信息,对于主题根目录下的_config.yml

辅助函数(Helper)

制作一个分页器,我们需要知道文章的总数和每页展示的文章数,然后通过循环生成每个link标签,还要根据当前页面判断link标签的active状态,但是在Hexo中这些都不用我们自己来做了!Hexo提供了paginator这一辅助函数帮助我们生成分页器,只需要将文章总数site.posts.length和每页文章数config.per_page传入就可以生成了。

其他的Helper:

  • list_tags([options]): 快速生成标签列表
  • js(path/to/js), css(path/to/css) 用来载入静态资源,path可以是字符串或数组(载入多个资源),默认会去source文件夹下去找。
  • partial(path/to/partial) 引用字模板,默认会去layout文件夹下找。

样式

知道了Hexo的渲染方式,我们就可以使用HTML标签+CSS样式个性化我们的主题了,推荐大家使用CSS预处理语言的一种来写样式,这样就可以通过预处理语言自身的特点让样式更灵活。

其他

添加对多说和Disqus的支持

评论是很常用的功能,不如就直接在我们的主题里支持了,然后通过配置变量决定是否开启,评论区跟在文章内容下面,对于这种三方的代码块,最好也以partial的方式提取出来,方便移除或是替换。

//_partial/article.ejs
<section class='post-content'>
    <%- post.content %>
</section>
//评论部分,post.comments判断是否开启评论,config.duoshuo_shortname
和config.disqus_shortname来判断启用那种评论插件,这里优先判断了多说
<% if(post.comments){ %>
    <section id="comments">
    <% if (config.duoshuo_shortname){ %>
            <%- partial('_partial/duoshuo') %>
        <% }else if(config.disqus_shortname){ %>
            <%- partial('_partial/disqus') %>
        <% } %>
    </section>
<% } %>

再将多说和Disqus提供的js脚本代码放在_partial/duoshuo.ejs_partial/disqus.ejs下就ok了~

使用highlight.js提供代码高亮

highlight.js提供了多种语言的支持和多种皮肤,用法也很简单,载入文件后调用初始化方法,一切都帮你搞定,对于使用那种皮肤,喜好因人而异,我们干脆在主题的配置文件中做成配置项让用户自己选择:

//showonne/_config.yml
...other configs

# highlight.js
highlight_theme: zenburn

对应的layout.ejs中:

样式文件通过CDN引入,因为不同皮肤对应不同的文件名,所以十分灵活。

最后

当初是对应着landscape照葫芦画瓢写的,最近回头来发现一些不合理的地方,所以就又改了改,也对应着写了这么一篇总结,接下来准备再把样式划分一下,对于颜色这类样式通过变量的方式提取出来,也变得可配置,能让主题更灵活一些。

参考资源

了解辅助函数
模板
Hexo中的变量
Hexo主题列表
Hexo使用多说教程
How to use highlight.js

API与NEI定义规范


title: API与NEI定义规范
date: 2017-05-15

API规范

API规范包括路径命名规范、请求方式规范,请求参数规范、返回数据规范与特殊结构规范五部分;

路径命名规范

路径的定义主要考虑以下因素:

  1. 为了方便快速定位前端文件,目前供应链前端组的页面文件路径是与url的路径完全对应的,如果url中存在变量id,则这种关联就无法实现;所以不建议使用restful的路径命名方式;

  2. 一般情况下,路径深度不超过3层,理想情况下都定义三层,如/quotation/manage/list,如果工程所有接口前都以/api或者/backend等开头,则从第二层开始计数,不超过3层,最大长度为/api/quotation/manage/list;

  3. 建议每层的命名不超过两个英文单词,两个单词时,命名格式为驼峰,如auditOrder,如果可以一个单词描述清楚就不要使用两个单词组合;

请求方式规范

对于资源的操作,请使用以下常用的请求方式(括号中为对应的SQL命令):

  • GET(SELECT):从服务器取出资源(一项或多项)
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性,改变部分属性)。
  • DELETE(DELETE):从服务器删除资源。

请求参数规范

POST请求时,请求参数可以为FORMDATA或者JSON格式,一般情况下以参数的个数以及复杂度判断使用哪种方式,如果参数都为非对象的简单类型(不包括数组,对象),并且个数小于5个,则使用FORMDATA, 后端使用@RequestParam读取,如果结构复杂,个数较多,则前端参数传JSON格式,后端使用@requestbody将参数映射为一个对象

返回数据规范

包括两部分:http状态码与返回结果数据

HTTP状态码

  • 200 OK - [GET]:服务器成功返回用户请求的数据
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

返回数据

{
    “code”: 200,
    "message": "操作成功",
    "error": ""
}
  • code: http状态码无法满足需求时,请使用code描述错误类型;200表示成功,错误code如下,无法满足时,请继续按照分类对后面的数字进行扩展

    用户登陆相关:
    401 表示用户没有登陆(令牌、用户名、密码错误)
    403 表示用户得到授权(与401错误相对),但是访问是被禁止的
    1001 用户名或者密码错误
    1002 用户名不存在
    1003 注册时,用户名已经存在
    1004 注册时,存在非法的数据格式
    1005 账户已经被冻结
    1006 验证码错误
    
    操作相关200x:
    2001 查询/修改/删除/添加/操作失败
    2003 后端服务依赖不可用
    
  • message:给用户看的错误信息,该信息会展示在用户界面上

  • error:给开发看的错误信息,为了方便定位问题,可以将一些错误细节填入该字段,出现问题时,可以快速定位问题; error不在用户界面上显示,开发可以在开发者工具中的请求返回值中查看

  • result:如果操作成功,result为请求返回的数据,格式为json对象;常见的结构如下:

    {
        “result”: {
            "vo1": {},
            "vo2": {}
        }
    }
    如果请求结果为列表,则结构固定为:变量名为list,分页变量为pagination,请不要修改
    {
        "result": {
            "list": [],
            "pagination": {
                "total": 100
            }
        }
    }
    如果需要返回多个列表,则格式为: a,b可替换为有含义的单词
    {
        "result": {
            "aList": [],
            "aPagination": {
                "total": 10
            },
            "bList": [],
            "bPagination": {
                "total": 20
            }
        }
    }
    

特殊结构规范

  1. 为方便后端逻辑处理,前后端日期格式统一为:long型;包括前端传后端的参数和后端回传的数据;
  2. 下拉选择的kv命名为id与name,因为regular-ui库的默认值为id和name;
  3. 变量命名尽量简短,类名/接口名等已经说明含义的情况下,字段名不要加前缀,如“forwarderQuotationInfoChargeVo”,可以优化为:“infoVo”;
  4. 列表需要分页时,前端传给后端的分页信息变量名固定为:pageSize表示一页显示多少条,pageNo当前是第几页;服务端返回的总数变量名为total;老工程可能名称定义的不统一,如果是调用其他工程接口获取到的数据,需要后端转换为此种格式再传给前端;
  5. 枚举数字定义从1开始;全部为0,其他、不存在等为-1;boolean意义的枚举,1表示是,0表示否;
  6. 表单页面,如下拉选择,传给后端的是Number类型的id,非编辑状态下回显的时候,除了id字段, 还需要返回转换为字符串的变量,这个变量命名统一为variableName(id类型的变量名后面加Name);

NEI操作规范

准备工作

  1. 在新工程开始的时候,先按照API定义规范中的约定,定义返回数据结构BaseReturnData,其中result是variable类型,在使用BaseReturnData结构的地方,填入具体的数据类型;
  2. 如果后端在工程代码中已经定义好了数据结构,在nei定义时,可以选择从Java Bean导入数据结构,如果使用这种方式,代码中的注释必须为以下几种,其余格式的注释不会被识别为字段描述;

一个完整的例子

  1. 新建一个页面
  2. 新建页面中,先填写名称和路径(注意路径定义需要按照API定义规范中的约定定义),如果访问页面需要参数,如/order/manage/detail?orderId=397,参数定义也需要填写;然后新建一个模板,模板路径可以直接填写页面的url路径,如果有同步返回的数据,需要在预填数据中定义;除了nei要求的必填项外,还需要填写标签,标签名称定义为本次任务的任务名;如订单优化二期,方便查看的人筛选本次定义的接口;红色框为需要注意的地方,类型必须正确,描述能完整的理解字段的含义;
  3. 保存页面,切换到资源-》异步接口,新建异步接口,请按照API定义规范中的约定选择请求方式GET/POST/PUT等,标签直接选择在页面中创建的标签;异步接口中需要填写请求信息和响应信息,请求信息中,如果后端需要接收FormData格式的数据,需要如图设置请求头,如果不设置,默认以json格式传参,请求数据参数中必须选择正确的参数类型,字段描述必填, 如果返回值是枚举或者id,需要在描述中书名id对应的含义;

JS模块化 - 浅谈 CommonJS require 函数实现

何以模块化

最早的前端,没有模块加载规范,
只能在HTML中通过<script>来引入js文件,同时无法区分函数来源于哪个js文件,而且要用过多全局变量。
而随着前端工程复杂度的提升,使用这种方式已经无法满足日益增长的开发需求,js的模块化应运而生。

CommonJS 是属于 Node.js 的模块化方案,最早是叫 ServerJS,随着 Node.js 的火爆发展而成名的。Module1.0 规范在 Node.js 上实践的很好。

而 JavaScript 在当时(ES6 Modules 规范还未诞生)是没有模块化方案的,所以又更名从 CommonJS,想要统一服务端与客户端的模块加载方案。

但是,require 函数是同步的,在浏览器端由于网络的瓶颈而不适用。于是,AMD 和 CMD 的规范相继涌现,和 CommonJS 一起服务于 JavaScript 模块化。
而正是规则的不统一,这也是目前兼容方案 UMD 会出现的原因。

不过AMD 和 CMD 的浏览器端模块化,有很明显的问题:

  1. 导致浏览器端请求数过多;
  2. 受限于网络,所有模块都成功加载完只是一个承诺。

现如今,当打包这一环节被引入了前端工程化,CommonJS 以与服务端可以类库共用和 NPM(Node Package Manager) 这个后台的优势,成为了 es5 JavaScript 模块化的首选

本文内容,是从 require 函数这个切面,带读者了解到 CommonJS 模块化的原理,所有代码整理在 git 仓库

简介

CommonJS 是一个旨在构建涵盖web服务器端、桌面应用、命令行app和浏览器JS的JS生态系统。

标准

CommonJS 的标准符合 module1.1 规范,暴露给使用者的有三个全局变量:

  1. require 是一个全局方法,用来加载模块
  2. exports 一个全局对象,用来导入模块的属性或方法
  3. module 一个全局对象。涵盖当前模块的必要信息,有一个只读的id属性,有一个uri属性,还有其它的一些命名规范,可以查看 CommonJS 规范的文档

面向 require 这个切面

写一个 main 函数

本文讲的是如何模拟一个 $require 函数,先来捋一捋 require 函数的主逻辑

根据这个逻辑,我们先写一个 main 函数,以及定义一些需要的接口

// 缓存模块 require 的结果
var cached = {}
// 初始化的时候就会装载进来
var coreModule = {}

// 获取入口 js
// 用于后续的相对路径转绝对路径
var ENTRYJS = path.resolve(process.cwd(), process.argv[1])
var stack = new Stack(getParent(ENTRYJS))

function $require(pathname) {
    // 是否已存在缓存
    if (cached[pathname]) {
        return cached[pathname]
    }

    // 是否核心模块
    if (coreModule[pathname]) { 
        cached[pathname] = coreModule[pathname]
        return cached[pathname]
    }
    
    // 获取到模块的真实位置
    // 1, 如果是绝对路径,则转换相对路径
    // 2. 如果是包,则查找是否存在 package.json,并读取 main 属性
    var targetModule = getModuleLocation(stack, pathname)

    if (-1 === targetModule) {
        throw new Error('模块' + pathname + '未找到') // 包未找到
    }

    // 1. 如果是目录,则添加 /index
    // 2. 如果是文件,先检测文件是否存在
    // 3. 若不存在,则检测添加后缀名后的文件是否存在, ['', '.js', '.json', '.node']
    var targetFile = getRealPath(targetModule)
    if (-2 === targetFile) {
        throw new Error('模块' + pathname + '未找到') // 文件未找到
    }

    stack.push(getParent(targetFile))

    // 找到具体的文件,并执行,执行完成赋值到缓存中
    cached[pathname] = _require(targetFile)
    stack.pop()
        
    return cached[pathname]
}


// 需要实现的接口
// 数据结构 - 栈
function Stack() {}
// 获取文件的所在目录的绝对路径
function getParent(pathname) {}


// 查找模块的所在位置
function getModuleLocation(pathname) {}
// 从模块的所在位置,获取到真实的 js 路径
function getRealPath(pathname) {}


// 纯粹的模块执行处理函数
function _require(pathname) {}

至此已经有一个不可执行的 require 函数了,逻辑中会依次判断是否有缓存,是否核心模块,加载相应文件以及加载执行模块并缓存。

可以看到没有具体实现,只有接口被定义出来,这种编码方式,同样可以借鉴到在其他的开发需求中:

  1. 在开始编码前,进行尽可能合理的功能模块划分,可以让代码逻辑清晰,减少重复步骤 (DRY),并增强后期的代码可维护性
  2. 定义你需要哪些接口。如果是比较复杂的功能,且不是独立开发的话,这一环节做的好坏,合理地划分与合理地分配,决定团队合作开发是否可以配合恰当

逐一实现接口

这一步骤,主要是对上述过程需要的接口进行实现。
**上是一个分而治之的**,实现一个很复杂的东西比较困难,但是实现具体的功能要求,且在一定输入输出的限制下,每个人都能轻易的写出符合需求的算法,并进行调优。

1. 数据结构与工具函数类

a. 栈 -- 存储当前模块的所在目录
function Stack(...args) {
    this._stack = new Array(...args);
}
Stack.prototype = {
    top: function () {
        return this._stack .slice(-1)[0]
    },
    push: function (...args) {
        this._stack.push(...args)
    },
    pop: function () {
        this._stack.pop()
    },
    constructor: Stack
}

这个栈的作用是存放当前模块的所在目录,用于模块内 require 函数传入相对路径时,解析成绝对路径

b. 获取文件所在目录

function getParent(pathname) {
    return path.parse(pathname).dir
}

2. 具体的模块文件查找逻辑

a. 检测模块类型与定位包的位置

这个函数要做下面的事情

  1. 检测模块类型:绝对路径,相对路径 或是 在 node_modules
  2. 如果是模块,则需要就近寻找 node_modules 有无这个模块,并且读取 pacakge.jsonmain属性
function getModuleLocation(pathname) {
    var moduleType = getModuleType(pathname)
    var map = {
        [MODULE_TYPE.ABSOLUTE_PATH]: function () {
            return pathname
        },
        [MODULE_TYPE.RELEALITIVE_PATH]:: function () {
            return path.resolve(stack.top(), pathname)
        },
        [MODULE_TYPE.IN_NODE_MODULES]: function () {
            var parent = stack.top()
            while (!fs.lstatSync(path.resolve(parent, 'node_modules', pathname))) {
                parent = getParent(pathname)
                if (!parent) {
                    return -1
                }
            }
            // 从包名 到 绝对路径
            pathname = path.resolve(stack.top(), 'node_modules', pathname)
            
            // 输出包的 main 文件
            pathname = getPackageMain(pathname)
            
            return pathname
        }
    }

    return map[moduleType]()
}
/**************** 分割线 *******************/
// 以下部分,是 getModuleLocation 自身需要实现的接口,往往是开发过程中自行提炼的,其他模块不通用
var MODULE_TYPE = {
    "ABSOLUTE_PATH": 1,
    "RELEALITIVE_PATH": 2,
    "IN_NODE_MODULES": 0
}

function getModuleType(pathname) {
    if (path.isAbsolute(pathname)) {
        return MODULE_TYPE.ABSOLUTE_PATH
    }

    if (pathname.MODULE_TYPE('.')) {
        return MODULETYPE.RELEALITIVE_PATH
    }

    return MODULE_TYPE.IN_NODE_MODULES
}

function getPackageMain(pathname) {
    try {
        var packageJson = fs.readFileSync(path.resolve(pathname, 'package.json')).toString('utf-8')
        var json = JSON.parse(packageJson)    
        if (json.main) {
            return path.resolve(pathname, json.main)
        }
    } catch(e) {
        return pathname
    }
}

b. 定位引用的真实路径

  1. 如果是目录,则添加'/index'后缀
  2. 对 '.js','.node', '.json' 可能有的后缀省略进行补齐
function getRealPath(pathname) {
    var list = ['', '.js', '.json', '.node']
    var stats

    try {
        stats = fs.lstatSync(pathname)    
    } catch (e) {
        return -2
    }

    if (stats) {
        if (stats.isFile()) {
            return pathname    
        } else if (stats.isDirectory()) {
            pathname = path.join(pathname, 'index')
        }
    }

    for(var i = 0; i < list.length; i++) {
        var fullname = pathname + item
        try {
            fs.lstatSync(fullname)
            return fullname
        } catch (e) {}
    }

    return -2
}

3. _require 函数

这个函数是 CommonJS 模块化的核心体现,理解这个函数,对 moduleexports 的实际使用也会有帮助

  1. Node.js 的模块化实质是为每个模块的 js 包裹一层 'function module_exports(){}' 用以隔离作用域;
  2. module / exports / require 被作为传参而传入,而 exports 实质是 module.exports 的引用
  3. __dirname / __filename ,其实是这个模块内被先于执行函数定义的变量
  4. JS new Function 的特性常用来动态定义和执行某个方法,这边也同样用来执行模块
function _require(pathname) {
    // 定义一个Module对象
    var Module = function() {
        this.exports = {}

        // 添加其他必须属性,如 id
    }
    
    // 引入nodejs 文件模块 下面是nodejs中原生的require方法
    var fs = require('fs')

    // 同步读取该文件
    var sourceCode = fs.readFileSync(pathname, 'utf8')

    // 字符串转换成函数
    var moduleExportsFunc = new Function('module', 'exports', `${sourceCode}; return module.exports;`)

    // 实例化一个Module 里面有一个exports属性
    var module = new Module()

    // 把module 和 它内部的module.exports都作为参数传进去 
    // 并得到挂在到module.exports 或 exports上的功能
    var res = moduleExportsFunc(module, module.exports)

    // 最终我们拿到了path代表的文件模块提供的API
    return res 
}

留下一些问题留给同学们思考:

  1. 如果出现异步 require 的情况,由于当前模块已经执行完,会清空存储模块目录的 stack ,会出现相对路径查找失败的问题,如何解决?
  2. 当发生循环依赖的时候,CommonJS 内部的加载流程是是否会陷入死循环,如果不会那会带来什么其他影响?(且听下回分解)

End

本文是我参考一些 Node.js require 特性后,利用 JavaScript 简单的实现后的总结,主要提供一个思路。
本文中实现的 $require 函数存放在了 git 仓库 MockRequire

至此,我们粗暴地模拟了 Node.js 中的 require,多谢阅读,如有疑问,欢迎指出

Regular实现动画的痛点

Regular实现动画的痛点

一个需求

PC首页增加一个模块如下图:左侧区域3个强推品牌轮播,右侧10个个性化推荐品牌展示,右上角换一批,点击发请求获取10个新品牌展示。

左侧我们有轮播组件可以直接拿来用,右侧的换一批获取到新数据后直接替换老数据,触发脏检查regular自动刷新右侧列表,实现起来还是比较简单。
需求图

优化需求

交互说搞点动画吧,我们网站太缺了。我很同意,所以乐意花点时间做。他说效果参照某猫,如下:
某猫翻牌效果
确实比较酷炫,值得借鉴。

思路

实现这个效果主要有3个需要思考的点:

  • 单个品牌翻转动画的实现
  • 全部品牌翻转的时序控制
  • 如何在翻转的过程中替换数据

单个翻转

看到这类dom动画是不是第一反应animation、transition、transform等?翻转后品牌变了,是否要两个节点互相backface-visibility:hidden;?又考虑到PC首页要保证较好的低版本浏览器兼容,然后考虑降级方案。。。

其实此处有更好的方案。这个是比较简单的翻转效果,其实用width也能实现。通过将品牌节点的width从100%->0->100%,该节点会先压缩到0宽不可见,然后再展开到原宽度,视觉上看和rotateY并无差异。即保证了效果,也兼容了低版本浏览器,一举两得。

简单的动画可以多考虑用兼容性更好的css属性实现,尤其是在PC端。

width的变化可以用nej的缓动函数封装util/animation/bezier轻松实现。为了与regular结合,扩展了regular的animation指令

 Regular.animation("brand-rotate", function(step){
  var param = step.param, // 传入brand-rotate的参数
      element = step.element, //触发节点
      ...;
  // 它只接受一个done函数作为参数,标志着本步骤的结束
  return function(done){ 
    doAnimation(element,done);
  }
})
<!--品牌节点-->
<div class='brand' r-animation=
       "on:brand-rotate;
        brand-rotate:;
         ">
    ...
</div>

在需要翻转的时候$emit("brand-rotate")即可。

时序控制

当数据更新时,所有品牌并不是同时翻转,而是根据品牌所在位置按照一定的延时间隔,从左上角到右下角依次翻转。各个品牌翻转的次序可以根据其位置计算得出,这个不难。接下来的任务就是控制品牌在该翻转的时候翻转。
翻转次序

regular文档的animation指令一章确有delay相关参数,

<!--7. wait: duration-->
<!--等待数秒再进入下一个步骤-->
<div class='box animated' r-animation=
     "on:click; 
        class: swing ;
        wait: 2000 ;
        class: shake">
  wait: click me
</div>

这正是我想要的,我想我应该这么写就能按序翻转了:

{#list brandList as brand}
    <!--品牌节点-->
    <div class='brand' r-animation=
           "on:brand-rotate;
            wait:{brand_index|getDelayMs};
            brand-rotate:;
             ">
        ...
    </div>
{/list}

然鹅,报错了。。。如下图:
报错
value中包含表达式,变当做表达式解析成一个对象,然后这里直接在对象上调用trim(),就报错了,看来此处value只支持String类型,也就是说wait值只能写死,不能用表达式。。。因此delay方法只能自己通过setTimeout、requestAnimationFrame等实现了。

看了一下regular在github上的最新代码,此处已被机智的波神修复了,也早有人提过issue看来工程里的regular需要升级了!

数据更新

这听起来是最容易的,MVVM做的就是从数据到视图自动更新,我们只需关心数据,而无需操心如何更新。然鹅和动画结合后就变得不那么容易了。

数据的更新是在动画进行到一半时触发的,width渐变成0-->更新品牌数据-->width渐变成100%。
当更新数据时,数据对应的节点将会被刷新成新节点并恢复成模板中初始的样子,也就是说节点动画被终止、样式被重置,width直接从0变成100%而没有过渡。

当我们的用js操作regular模板节点时,会与regular自身的视图更新相冲突,这是问题所在。

想过把有动画的部分纯用自己的脚本操作节点实现翻转、数据更新,那就是一夜回到解放前(jQuery时代),奈何节点上还有事件绑定、节点数据修改等操作,想想都麻烦。

最终有两个可行方案:

  1. 把动画逻辑和更新数据完全分开
  • 动画:width 100% -> 0
  • width==0时,直接js修改图片url和说明文案
  • 动画:width 0 -> 100%(以上全是为了视觉上的翻转效果)
  • 等全部动画执行完,整体更新brandList数据
  • 自动刷新视图、绑上各种事件(虽说最后dom节点是都刷新了,但是刷新前后显示的图片、文案都是一样的,所以视觉上并看不出)
  1. 取巧的方案。
  • 设置品牌节点的css样式为width=0
  • 模块首次出现时,需要执行动画width 0 -> 100%,使之出现
  • 当换一批时,执行动画:width 100% -> 0
  • 当有节点width==0时,就刷新这个单个品牌的数据
  • 这个品牌数据被更新,样式被重置成初始的样子width=0(正好现在width也是0,所以视觉上并无变化)
  • 然后执行动画width 0 -> 100%

既然运用了regular,如果非必须,还是不要直接操作dom节点的好。 所以最终选择了第二种。效果如下图:

最终效果

痛点

这个需求的实现可以看到regular实现动画的一些局限性:

  • r-animation比较适合控制增删class、修改css,从而触发简单的css3动画,如果要实现比较复杂的动画,还是需要直接操作dom节点,并依赖专业的js动画库。
  • 动画操作和regular刷新视图会互相影响,应尽量把两块操作分开,一次只做一件事,按序进行。密集的串插进行可能会导致意想不到的问题。
  • 波神也提到“动画使用不当是可能与组件本身的数据-UI状态产生冲突,应该尽量使用on/emit的组合,而少使用when/call的组合来实现动画序列”,为的就是避免状态更新而对动画状态产生影响。

用Nodejs开发命令行工具

Table of Contents generated with DocToc

前言

找到合适的工具包,开发nodejs命令行工具是很容易的

准备工作

  • nodejs v6.10.1
  • npm v3.10.10

版本号是我当前使用的版本,可自行选择

Hello World

分4步:

  • index.js
  • package.json
  • 根目录下执行npm link
  • 执行命令nhw => hello world

touch index.js创建一个index.js文件,内容如下:

#! /usr/bin/env node

console.log('hello world')

npm init创建一个package.json文件,之后修改成如下:

{
    "name": "npmhelloworld", 
    "bin": {
        "nhw": "index.js" 
    },
    "preferGlobal": true,
    "version": "1.0.0",
    "description": "",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "jerryni <[email protected]>",
    "license": "ISC"
}

内容详解

index.js

#! /usr/bin/env node

http://stackoverflow.com/questions/33509816/what-exactly-does-usr-bin-env-node-do-at-the-beginning-of-node-files

这句话是一个shebang line实例, 作用是告诉系统运行这个文件的解释器是node;
比如,本来需要这样运行node ./file.js,但是加上了这句后就可以直接./file.js运行了

package.json

{
    // 模块系统的名字,如require('npmhelloworld')
    "name": "npmhelloworld", 
    "bin": {
        "nhw": "index.js" // nhw就是命令名 ,index.js就是入口
    },
    
    // 加入 安装的时候, 就会有-g的提示了
    "preferGlobal": true,


    // 去掉main: 'xxx.js'  是模块系统的程序入口
    "version": "1.0.0",
    "description": "",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "jerryni <[email protected]>",
    "license": "ISC"
}

npm link命令

执行后,控制台里面会有以下输出:

/usr/local/bin/nhw -> /usr/local/lib/node_modules/npmhelloworld/index.js
/usr/local/lib/node_modules/npmhelloworld -> /Users/nirizhe/GitHub/npmhelloworld

解释:创建了2个软链接分别放到系统环境变量$PATH目录里,nhw命令和npmhellworld模块。npm link在用户使用的场景下是不需要执行的,用户使用npm i -g npmhellworld命令安装即可。

发布项目到npm官网供大家使用

设置npm用户名,没有的话先到npm官方网站注册一个:

npm set init.author.name "Your Name"
npm set init.author.email "[email protected]"
npm set init.author.url "http://yourblog.com"

npm adduser

项目根目录运行:

npm publish

注意:

每次发布需要修改package.json中的版本号,否则无法发布

如何处理命令行参数

当下比较流程的几个工具对比

这边使用yargs

npm install --save yargs

请看之前我实战一段代码

大概是这个样子:

var argv = yargs
    .option('name', {
        type: 'string',
        describe: '[hostName] Switch host by name',
        alias: 'n'
    })
    .option('list', {
        boolean: true,
        default: false,
        describe: 'Show hostName list',
        alias: 'l'
    })
    .option('close', {
        type: 'string',
        describe: 'close certain host',
        alias: 'c'
    })
    .example('chost -n localhost', 'Switch localhost successfully!')
    .example('chost -c localhost', 'Close localhost successfully!')
    .example('chost -l', 'All host name list: xxx')
    .help('h')
    .alias('h', 'help')
    .epilog('copyright 2017')
    .argv

效果:
yargs

单元测试

推荐使用mocha

var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

执行

$ ./node_modules/mocha/bin/mocha

  Array
    #indexOf()
      ✓ should return -1 when the value is not present


  1 passing (9ms)

让你的项目显得正规(github badges)

实际效果就是这些小图标:

badges

这里可以找到各式各样的badges:
https://github.com/badges/shields

持续集成(CI)和代码覆盖率

travis
Coverage Status

travis-ci:

  • 用github帐号登录,这时网站上列出你github上的项目
  • 在项目根目录放.travis.yml这个文件, 并写好简单的配置
language: node_js
node_js:
  - "6"
  • 在项目的package.json中添加测试脚本, 因为travis默认会执行npm test
  "scripts": {
    "test": "mocha"
  }

在travis设置成功后,继续覆盖率的处理:

  • 安装2个依赖

npm install istanbul coveralls --save-dev

  • travis的配置文件中添加
language: node_js
node_js:
  - "6"
after_script:
  - npm run coverage
  • 修改package.json中的测试脚本
  "scripts": {
    "test": "node ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha",
    "coverage": "cat ./coverage/lcov.info | coveralls"
  }

常用的库

shelljs

Portable Unix shell commands for Node.js
在nodejs里用unix命令行

chalk

Terminal string styling done right
给命令行输出上色

参考资料

http://javascriptplayground.com/blog/2015/03/node-command-line-tool/
https://medium.freecodecamp.com/writing-command-line-applications-in-nodejs-2cf8327eee2#.3yg7f98dv
http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html
https://gist.github.com/coolaj86/1318304

repaint与reflow

repaint与reflow

本文是阅读Rendering:repaint,reflow,restyleRepaint 、Reflow 的基本认识和优化网站性能优化之后的笔记记录;

什么是Repaint/Reflow


上图是浏览器解析的大概过程,归纳为4个步骤:

  1. 解析HTML以构建DOM树:渲染引擎开始解析HTML文档,转换树中的html标签或js生成的标签到DOM节点,它被称为 -- DOM树。
  2. 构建渲染树:解析CSS(包括外部CSS文件和样式元素以及js生成的样式),根据CSS选择器计算出节点的样式,创建另一个树 —- Render树。
  3. 布局渲染树: 从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标。
  4. 绘制渲染树: 遍历渲染树,每个节点将使用UI后端层来绘制。

下面以一个例子说明这个过程:

<html>
<head>
  <meta name="viewport" content="width=device-width" >
  <link href="style.css" rel="stylesheet">
</head>
<body>
  <p>
    Hello <span>web performance</span> students!
  </p>
  <div>
    <img src="awe-some-photo.jpg">
  </div>
</body>
</html>
  1. 根据浏览器的解析过程,浏览器首先会将上面的html解析为如下的一个dom树:

  2. 根据css样式表生成CSSOM:

  3. DOM + CSSOM = RenderTree

然后浏览器会计算每个元素的属性,确定如何展示页面;根据样式和js构建render树, 如下:

对比dom树与render树,可以发现少了head和被隐藏的元素;因为render树只包括可见的元素部分,具体的说,是从page的(0,0)坐标到(window.innerWidth, window.innerHeight)构成的矩形区域;

从上面的流程中,可以看到页面初始化时,至少有一次layout和paint;这个过程是不可缺少的,那么我们通常所提到的reflow和repaint是什么?

reflow: 当页面上有一些信息变化时,如大小、位置变化;浏览器可能会需要重新计算元素的展示样式,这个过程称为reflow;
repaint: 当元素的位置、大小等属性确定好后,浏览器会把这些元素重新绘制一遍,这个过程称为repaint;

引起Repaint和Reflow的一些操作

Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。

以下行为会造成reflow或者repaint:

  1. 当对元素进行增删改操作时;
  2. 当元素的位置变化时;
  3. 新增stylesheet,修改样式表时;
  4. window resize或者滚动的时候;
  5. 修改网页字体时;
  6. 通过display:none隐藏展示元素时会造成reflow和repaint,通过visibility:hidden;只会repaint,因为位置没变化;

影响 layout 的属性

宽高 边距 位置 表现 边框 定位 字体
width padding position display border text-align font-size
height margin top float border-width overflow-y font-weight

影响repaint的属性

背景 边框 其他
background border-style color
background-image border-radius visibility
background-repeat outline text-decoration
background-position outline-style box-shadow
background-size outline-color
-- outline-width

优化建议

  1. 不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className:
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";
// better
el.className += " theclassname";
  1. 把 DOM 离线后修改。如:
    a> 使用 documentFragment 对象在内存里操作 DOM。
    b> 先把 DOM 给 display:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后再把他显示出来。
    c> clone 一个 DOM 节点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。

  2. 不要把 DOM 节点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。

  3. 尽可能修改靠近叶子的节点。当然,改变层级比较底的 DOM节点有可能会造成大面积的 reflow,但是也可能影响范围很小。

  4. 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是会大大减小 reflow 。

  5. 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。

如何理解JS中的闭包.md

如何理解JS中的闭包?

对于函数是一等对象的语言来说,下面两种典型的情况,会存在变量如何取值的问题。

case a. 将函数当参数传递

var x = 15;
function A () {
    console.log(x);
}

function B (fn) {
    var x = 20;
    fn();
}

B(A);  // 15, but not 20

case b. 将函数作为返回值

function B () {
    var x = 10;
    function A () {
        console.log(x);
    }
    
    return A;
}
    
var x = 20;
B()();  // 10, but not 20

从语言实现层面来看,JS中闭包是解决上面两种情况下变量取值的一种机制。

对于闭包常见的描述,比如缓存变量等,只不过是对上面语言实现特性的典型应用。

JS中变量作用域使用的是静态作用域(static scope),在JS引擎解析JS代码时,各个函数能访问到的作用域以及相应的变量已经定了,并且不会再改变。比如上面函数A定义的时候,其能访问到的变量已经定了。对于case a函数A声明的时候已经决定了其只能访问到全局作用域,并且后续不会发生改变,即始终只能访问到全局变量x和全局函数声明B,访问不到函数B中的局部变量x,不管其后续如何执行,打印出来的结果总是全局变量x的值;对于case b函数A函数B执行的时候才被声明,其能访问到的作用域,首先是函数B的局部作用域(局部变量x),然后是全局作用域(全局变量x和全局函数声明B),所以函数A打印的始终是局部变量x的值,而不管其后续如何执行。

通过上面的描述,我们也可以推断闭包名字的由来

闭包(closure)就像是对作用域的一种闭合(closing),在函数定义的时候,其作用域已经决定了,并且后续使用不会再改变,即作用域已经闭合了。

通过case acase b的对比,我们可以进一步给出闭包更加广义层面的定义,JS中任何一个函数都是闭包,而不仅仅是case b中那种教科书上面狭义的定义,全局函数也是闭包,是对全局作用域的一种闭合。

Reference

Written by Hong

文字垂直居中

文字垂直居中方案

在工作中,我们经常碰到“文字垂直居中”的问题,需要居中的文字有单行,也有多行,在各种情况下的垂直居中分别该怎么解决?

单行文字

  • 容器高度固定 height + line-height

在固定高度的容器中,单行文字垂直居中,只需要将文字的行距设置成容器的高度即可。

.vertical-line {
   height: 100px;
   line-height: 100px;
}

DEMO


  • 容器高度不定 table + table-cell + vertical-align: middle

当盛放单行文字的容器高度不固定,上面的方法就不起作用了,此时就只能采用表格样式了。其实这种方法也适用于多行文字的情况。

div {
 display: table;
}

p {
 display: table-cell;
 vertical-align: middle;
}
<div>
 <p>
   p的高度随div高度变化而变化,单行文字垂直居中
 </p>
</div>

DEMO


多行文字

多行文字垂直居中相比单行文字来说,就有点复杂了,最先想到的,就是上面所说的容器高度不固定下的居中方法。

接下来,对于垂直居中,我们能想到的属性是vertical-align:middle,再配合display: inline-block,应该是可以的:

<div>
    <p>
        多行文字垂直居中<br>
        多行文字垂直居中<br>
        多行文字垂直居中
    </p>
</div>
p {
    display: inline-block;
    vertical-align: middle;
}

然而写出来,并没有垂直居中的效果:

DEMO


其实,vertical-align的对齐,在非display:table下,是需要有参照物的,所以我们想到在容器里边添加一个高度等于容器高度、宽度为0的参照物,哎,对了,:before要登场了:

div:before {
    content: '';
    display: inline-block;
    vertical-align: middle;
    width: 0;
    height: 100%;
}
p {
    display: inline-block;
    vertical-align: middle;
}

然而,想法是美好的,现实是残酷的,本来想着垂直居中的文字,直接出去了:

这个时候可能想到是不是文字内容区域太宽了,好,开始调整宽度:

但是当选中之后,却发现,文字区域前面有间距:

why?还能不能好好渲染了,哎,等会,前面的before好像算文字哎,难道是font-size的问题?把外层容器的font-size设为0:

果然,这样就很和谐了嘛 DEMO


当然,上面的方法兼容性是很好的,IE8+都是没有问题的,但是table布局不方便,display: inline-block + vertical-align:middle 处理方式有点繁琐。如果你做的需求可以不用管低版本浏览器,那好,往下看:

css3的flex是为布局为布局而生的,先拿它试试:

display: flex;
align-items: center;
justify-content: center;

对于,长宽已知的元素,水平垂直居中可以采用下面的方式来居中:

//假设 width:100px,height:60px;
position: absolute;
top: 50%;
margin-top: -30px;
left: 50%;
margin-left: -50px;

可能现在有人会想到 margin-top: -50%,不好意思,这个50%是父元素的,这样写等于又把它放回去了。但是,transform: translate好像平移的是自己,试试?

position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);

没毛病!

以上差不多就是几种常见的未知高度垂直居中的方法,时间仓促,多提意见。

CORS 跨域请求

跨域资源共享 CORS(Cross-origin resource sharing)

CORS就是,允许浏览器向跨源服务器,发送XMLHttpRequest请求,并获取请求返回内容

支持

CORS需要浏览器和服务器同时支持。
目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

OPTIONS 预请求

检测 —— 客户端发出的请求是否被服务端允许

当客户端发送跨域请求时,浏览器会自发地先发送一个Method为OPTIONS的请求
请求和你发送请求的URL一致,且不带任何参数
OPTIONS请求通过,则发送真正的请求,否则将不再发送真正请求

optioons预请求

跨域配置

基本功能配置

客户端无需配置

服务端配置

String origin = request.getHeader("Origin"); //获取请求源
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); //允许上述Method的跨域请求
response.setHeader("Access-Control-Allow-Origin", origin); //允许当前源的跨域请求

Cookie和TLS客户端证书配置

跨域请求默认不发送Cookie以及TLS客户端证书信息,需要手动配置

客户端配置

xhr.withCredentials = true

服务端配置

response.setHeader("Access-Control-Allow-Credentials", "true");

TIP

1、OPTIONS请求不会发送Cookie
2、跨域请求发送的Cookie为请求域下的Cookie

问题解决

登录权限判断

异步请求有时会需要根据Cookie来判断是否登录,而options请求是不发送cookie的。
这时,就需要服务端仅针对options请求不做cookie校验。
服务端可以通过下面两点来进行判断
1、method == options
2、Access-Control-Allow-Headers 包含 X-Requested-With (判断是ajax,需要客户端添加)

JavaScript设计模式

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案

当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。

比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。

下面介绍几种在JavaScript中常见的几种设计模式:

1.单例模式

单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。

class CreateUser {
    constructor(name) {
        this.name = name;
        this.getName();
    }
    getName() {
         return this.name;
    }
}
// 代理实现单例模式
var ProxyMode = (function() {
    var instance = null;
    return function(name) {
        if(!instance) {
            instance = new CreateUser(name);
        }
        return instance;
    }
})();
// 测试单体模式的实例
var a = new ProxyMode("aaa");
var b = new ProxyMode("bbb");
// 因为单体模式是只实例化一次,所以下面的实例是相等的
console.log(a === b);    //true

2.策略模式

策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。

策略模式的目的就是将算法的使用算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。

/*策略类*/
var levelOBJ = {
    "A": function(money) {
        return money * 4;
    },
    "B" : function(money) {
        return money * 3;
    },
    "C" : function(money) {
        return money * 2;
    } 
};
/*环境类*/
var calculateBouns =function(level,money) {
    return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000

3.代理模式

代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

常用的虚拟代理形式:某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例:使用虚拟代理实现图片懒加载)

图片懒加载的方式:先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面。

var imgFunc = (function() {
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
var proxyImage = (function() {
    var img = new Image();
    img.onload = function() {
        imgFunc.setSrc(this.src);
    }
    return {
        setSrc: function(src) {
            imgFunc.setSrc('./loading,gif');
            img.src = src;
        }
    }
})();
proxyImage.setSrc('./pic.png');

使用代理模式实现图片懒加载的优点还有符合单一职责原则。减少一个类或方法的粒度和耦合度。

4.中介者模式

中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。

例如:现实生活中,航线上的飞机只需要和机场的塔台通信就能确定航线和飞行状态,而不需要和所有飞机通信。同时塔台作为中介者,知道每架飞机的飞行状态,所以可以安排所有飞机的起降和航线安排。

中介者模式适用的场景:例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};

5.装饰者模式

装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。

例如:现有4种型号的自行车分别被定义成一个单独的类,如果给每辆自行车都加上前灯、尾灯、铃铛这3个配件,如果用类继承的方式,需要创建4*3=12个子类。但如果通过装饰者模式,只需要创建3个类。

装饰者模式适用的场景:原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。

例:用AOP装饰函数实现装饰者模式

Function.prototype.before = function(beforefn) {
    var self = this;    //保存原函数引用
    return function(){  //返回包含了原函数和新函数的 '代理函数'
        beforefn.apply(this, arguments);    //执行新函数,修正this
        return self.apply(this,arguments);  //执行原函数
    }
}
Function.prototype.after = function(afterfn) {
    var self = this;
    return function(){
        var ret = self.apply(this,arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}
var func = function() {
    console.log('2');
}
//func1和func3为挂载函数
var func1 = function() {
    console.log('1');
}
var func3 = function() {
    console.log('3');
}
func = func.before(func1).after(func3);
func();

JavaScript并发模型与Event Loop

并发模型可视化描述

model.svg

如上图所示,Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack),程序中代码依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的队列(queue)中【即JavaScript执行引擎的主线程拥有一个执行栈/堆和一个任务队列】。

栈(stack) : 函数调用会形成了一个堆栈帧
堆(heap) : 对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。
队列(queue) : 一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,则从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着该消息处理结束。

为了更清晰地描述Event Loop,参考下图的描述:

model.png

首先,我们对图中的一些名词稍加解释

  1. queue : 如上文的解释,值得注意的是,除了IO设备的事件(如load)会被添加到queue中,用户操作产生 的事件(如click,touchmove)同样也会被添加到queue中。队列中的这些事件会在主线程的执行栈被清空时被依次读取(队列先进先出,即先被压入队列中的事件会被先执行)。
  2. callback : 被主线程挂起来的代码,等主线程执行队列中的事件时,事件对应的callback代码就会被执行

【注:因为主线程从"任务队列"中读取事件的过程是循环不断的,因此这种运行机制又称为Event Loop(事件循环)】

下面我们通过setTimeout来看看单线程的JavaScript执行引擎是如何来执行该方法的。

  1. JavaScript执行引擎主线程运行,产生heap和stack
  2. 从上往下执行同步代码,log(1)被压入执行栈,因为log是webkit内核支持的普通方法而非WebAPIs的方法,因此立即出栈被引擎执行,输出1
  3. JavaScript执行引擎继续往下,遇到setTimeout()t异步方法(如图,setTimeout属于WebAPIs),将setTimeout(callback,5000)添加到执行栈
  4. 因为setTimeout()属于WebAPIs中的方法,JavaScript执行引擎在将setTimeout()出栈执行时,注册setTimeout()延时方法交由浏览器内核其他模块(以webkit为例,是webcore模块)处理
  5. 继续运行setTimeout()下面的log(3)代码,原理同步骤2
  6. 当延时方法到达触发条件,即到达设置的延时时间时(5秒后),该延时方法就会被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立
  7. JavaScript执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行。
  8. 将队列的第一个回调函数重新压入执行栈,执行回调函数中的代码log(2),原理同步骤2,回调函数的代码执行完毕,清空执行栈
  9. JavaScript执行引擎继续轮循队列,直到队列为空
  10. 执行完毕
    console.log(1);
    setTimeout(function() {
        console.log(2);
    },5000);
    console.log(3);

    //输出结果:
    //1
    //3
    //2

Macrotasks 和 Microtasks

基本上,一个完整的事件循环模型就讲完了。现在我们来重点关注一下队列。
异步任务分为两种:Macrotasks 和 Microtasks。

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
  • Microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

Macrotasks 和 Microtasks有什么区别呢?我们以setTimeout和Promises来举例。

    console.log('1');
    setTimeout(function() {
      console.log('2');
    }, 0);
    Promise.resolve().then(function() {
      console.log('3');
    }).then(function() {
      console.log('4');
    });
    console.log('5');
    //输出结果:
    //1
    //5
    //3
    //4
    //2

原因是Promise中的then方法的函数会被推入 microtasks 队列,而setTimeout的任务会被推入 macrotasks 队列。在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。
结论如下:

  1. microtask会优先macrotask执行
  2. microtasks会被循环提取到执行引擎主线程的执行栈,直到microtasks任务队列清空,才会执行macrotask

【注:一般情况下,macrotask queues 我们会直接称为 task queues,只有 microtask queues 才会特别指明。】

【参考链接】

JavaScript 运行机制详解:再谈Event Loop
并发模型与Event Loop
【转向Javascript系列】从setTimeout说事件循环模型
异步 JavaScript 之理解 macrotask 和 microtask

基于PhantomFlow的自动化UI测试

基于PhantomFlow的自动化UI测试

本文的目录结构:

  1. 自动化测试的意义
  2. 可测试方向分析
  3. 竞品分析&技术选型
  4. PhantomFlow介绍
  5. 持续集成

自动化测试的意义

  • 一个项目最终会经过快速迭代走向以维护为主的状态,在合理的时机以合理的方式引入自动化测试能有效减少人工维护成本。
  • 自动化的收益 = 迭代次数 * 全手动执行成本 – 首次自动化成本 – 维护次数 * 维护成本
  • 另一方面,当我们需要对代码进行重构或者完善,在修改结束时我们如何确定项目仅仅是被重构了,而不是被改写了?此时测试将是一根救命稻草,它是一个衡量标准,告诉开发人员这么做是否将改变结果。

可测试方向分析

前端自动化测试的方向有:

  • 单元测试
  • UI回归测试
  • 功能测试
  • 性能测试
单元测试
  • 在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
  • 单元测试已经有非常完善的工具体系,借用2016 JavaScript 之星的图,常用的单元测试框架有:

UI回归测试

UI回归测试通常采用的方法是像素对比:

  • 像素对比基本的**认为,如果网站没有因为你的改动而界面错乱,那么在截图上测试页面应当跟正常页面保持一致。
  • 像素对比比较出名的工具是PhantomCSS,它结合了 CasperJS 截图和 ResembleJs 图像对比分析。从易用性和对比效果来说是很不错的。
像素对比 - PhantomCSS

初次运行的时候,会截图并作为baseline,后面再运行的时候,再生成截图,并与baseline比较,生成diff结果。

像素对比需要注意的事项:

  • 推荐对某些区域进行测试而不是整个页面。图像越大对比越慢。
  • 如果测试区域内有动态元素,可以通过选择器来隐藏。
  • 界面对比只是一个环节,需与其他测试相结合,合理结合才是关键。
功能测试
  • 仅仅对界面进行测试是不够的,即使界面正确,功能不正确也是断然不能接受的。
  • 最直接的功能测试就是通过模拟用户操作流程来判断页面的展现是否符合预期。
  • 有时,我们需要浏览器处理网页,但并不需要浏览,比如生成网页的截图、抓取网页数据等操作。PhantomJS的功能,就是提供一个浏览器环境,你可以把它看作一个“虚拟浏览器”,除了不能浏览,其他与正常浏览器一样。它的内核是WebKit引擎,不提供图形界面,我们可以用它完成一些特殊的用途。
PhantomJS和CasperJS
  • CasperJS是对PhantomJS的封装,提供了更加易用的API, 增强了测试等方面的支持。
  • 如下图,很方便的实现了一个百度贴吧自动发帖的功能。

性能测试
  • 性能测试通常来测试网站的性能,如白屏时间、首屏时间等。
  • 通常的工具有:chrome devtool,PageSpeed等在线测试网站。

考虑到我们主题是nek-ui组件库的测试,性能测试的部分,这里不做赘述。

竞品分析&技术选型

我们的测试对象是NEK-UI组件库,这一部分分析了其他组件库的测试方法并选择了最终的测试方案。

RegualrUI测试方案分析:

RegularUI使用的测试方案是karma + mocha的黄金搭档

这种方式存在的问题:

  • 没有UI部分的测试,这也就是单元测试与UI测试的差别。
  • 虽然可以通过调用组件的某些方法,达到用户操作同样的效果,但是跟真实的用户操作还是有差别的。比如,这个时候,template的这个方法根本没绑定,或者传参错误,这种情况是覆盖不到的。
Ant-design测试方案分析:
  • Ant-design是蚂蚁金服的一套企业级的 UI 设计语言和 React 实现,目前是Github上一个很火的项目:

  • Ant-design作为一个基于react的组件库,使用的测试框架是同样出自Facebook的Jest。

  • Ant-design使用的是Jest中称为snapshot testing的测试方案。

  • Jest的官方文档上介绍到,Jest的Snapshot Testing与典型的snapshot test不同,不是生成截图并比较图片的差异,而是直接输出React tree 的最终渲染dom结构。

Snnpshot Testing介绍:

再来看看Ant-design中的实际使用:

测试某个组件的时候,就会引入改组件文件夹里demo文件夹下的所有md文件,这个md文件是组件的各种示例,同时也用于ant-design的官方文档。然后,使用enzyme和enzyme-to-json提供的方法经过render->renderToJson->toMatchSnapshot, 第一次运行的时候会输出如下的.snap文件:

这个文件要随着代码一起提交到仓库,下次运行测试的时候,就和这个.snap文件做比较。

当然仅仅测试dom结构不变是不够的,ant-design的测试里,还有模拟用户操作的测试。如下两个文件,demo.test.js是上面的snapshot部分,index.test.js是模拟用户操作部分。

Index.test.js里做了什么呢?

在组件上绑定事件方法,然后模拟事件,判断方法是否被调用。

这种方式存在的问题:

  • DOM结构不变并不完全等于样式不变。
  • 很多相关工具都是React专用。

分析完了2个组件库的测试方案,那么我们期望的测试方案应该包含什么呢?

  • 组件库同一般的纯JS库不用的地方,使得单纯的单元测试是不够用的,最好要包含UI测试的部分。
  • 有模拟用户操作的部分。
  • 能方便的管理test case。

基于此,我们最终选择了PhantomFlow。

PhantomFlow介绍

  • PhantomFlow是基于决策树(decision tree)的ui test 框架,是对PhantomJS、CasperJS、PhantomCSS的包装。

  • PhantomFlow假定如果页面正常,那么在相同的操作下,每次页面所展现的应该是一样的。基于这点,使用者只需要定义一系列的操作流程和决策分支,然后利用PhantomCSS进行截图和图像对比。最后将测试结果在一个可视化报表中展现出来。

这里采用倒序的方式先来看一下PhantomFlow生成的测试报告,再介绍具体的使用:

这是PhantomFlow的母公司Huddle在他们实际的业务中使用的报告截图:

同时PhantomFlow也提供了单独查看某一个操作流的功能:

图中的每一条线代表一个用户操作流。绿色的点表示截图对比通过,红色的点表示截图对比失败,灰色的点表示这仅仅是PhantomFlow流程中的一步,并没有真正的操作。
黄色的表示是一个操作,但是操作里面并没有进行截图。我们只要关心其中绿色的点和红色的点。

PhantomFlow是基于决策树的,那么什么是决策数呢?没必要吧它想的那么神秘,我们可以认为它就是普通的流程图。

PhantomFlow方法介绍
  • flow (string, callback):初始化一个test  suite,回调函数中可以包含step, chance 和 decision。

  • step (string, callback):一个单独的步骤,回调函数中可以包含PhantomCSS的截图,CasperJs的操作事件和断言

  • decision (object):定义一个用户的决定,参数是一个对象,key用来描述decision的名称,value是一个function,里面可以包含后续的decision, chance和step

  • chance (object):功能上同decision一样,只是在语义上区分decision,用来描述不是用户主动的行为。

step对应决策树中的矩形,表示用户具体的某一个操作。decision和chance对应决策树中的菱形,表示用户的选择。

这是用PhantomFlow描述用户喝咖啡的一个场景:

PhantomFlow在NEK-UI组件测试中的使用
  • 以ui.select组件为例:

PhantomFlow提供了简单的方法来描述用户的操作流,具体的操作使用回调函数里的CasperJS来完成:

    function goToPage() {
        casper.thenOpen("http://localhost:9001/test/index.html", function() {
            this.echo('PageTitle: ' + this.getTitle());
            phantomCSS.turnOffAnimations();
        });
    }

    function injectModule(json) {
        casper.evaluate(function(json) {
            console.log(JSON.stringify(json));
            new NEKUI.UISelect(json).$inject('#module');
        }, json);
        casper.onConsoleMessage = function(msg) {
            console.log(msg);
        }
    }

    function goToModule() {
        casper.waitForSelector(
            '#module .u-select2',
            function success() {
                phantomCSS.screenshot('#module .u-select2');
                casper.test.pass('Should see the uiselect module' );
            },
            function timeout() {
                casper.test.fail('Should see the uiselect module');
            }
        )
    }

    function clickModule() {
        casper.click('#module .dropdown_hd');
        casper.waitForSelector(
            '#module .dropdown_bd',
            function success() {
                phantomCSS.screenshot('body');
                casper.test.pass('Should see the options of module');
            },
            function timeout() {
                casper.test.fail('Should see the options of module');
            }
        )
    }

    function selectAnOption(optionIndex) {
        casper.click('#module .m-listview li:nth-child(' + (optionIndex+1) + ')');
        phantomCSS.screenshot('body');
    }

测试报告使用介绍:

运行测试的常用参数:

在npm test后带上如下参数即可

  • report:打开浏览器,生成测试报告。

  • debug:输出更多的log信息,强制切换到单线程运行。

  • earlyexit: 默认为false,设置为true的话,遇到第一个failure就会终止测试。

  • threads:设置多线程来运行测试,默认为4。

常用CasperJS方法介绍
  • casper.thenOpen(String location[, mixed options]): 用来打开一个地址,当网页加载完成之后,执行一个方法。

  • casper.waitForSelector(String selector[, Function then, Function onTimeout, Number timeout]):等到DOM里有一个元素匹配选择器,可以传入成功的方法和失败的方法,和等待的毫秒数(默认5000)。

  • casper.click(String selector, [Number|String X, Number|String Y]):在匹配选择器的第一个元素上执行一次click

  • casper.mouseEvent(String type, String selector, [Number|String X, Number|String Y]):在匹配选择器的第一个元素上触发鼠标事件。支持的事件有:mouseup、mousedowm、click、dblclick、mousemove、mouseover、moustout、mouseenter、mouseleave and contextmenu

  • casper.getHTML([String selector, Boolean outer]):获取匹配选择器里的元素的内容。

  • casper.evaluate(Function fn[, arg1[, arg2[, …]]]):在打开的当前页面环境下执行方法。

  • casper.test.fail(String message):添加一个fail test。

  • casper.test.pass(String message):添加一个pass test。

  • casper.test.assertEquals(mixed testValue, mixed expected[, String message]):断言两个值严格相等。

使用PhantomFlow要注意的地方
  • 数据确定性:同样的测试用例在组件上运行多次,产生的结果应该相同。如果测试方法里面包含有Date.now()这种“数据不确定”的因素,会导致每次运行测试,页面显示的都不相同,这个时候可以引入sinon,用它的stub来托管数据不确定的方法。
  • 适当的添加断言:截图测试的特性决定了baseline一定要正确。假如首次运行的时候截图就错误,后面的运行错误一样是不会报错的。因此需要添加一些dom取值断言。

持续集成

  • 经常手动执行npm test?很麻烦有没有
  • 别人项目里的这两个徽章怎么来的?这两个徽章是项目可靠度的体现。

  • 持续集成(Continuous integration,CI),一种软件工程流程,指工程师将自己对于软件的复本,每天集成数次到主干上。在测试驱动开发(TDD)的做法中,通常还会搭配自动单元测试。

Travis CI

Travis-ci是一款持续集成服务,它能够很好地与Github结合,每当代码更新时自动地触发集成过程。Travis CI

打开Travis CI的官网,用Github账号登录。

选择需要打开Travis CI服务的仓库:

开通了服务的仓库,每当有push代码的时候,Travis CI就会为我们执行相关的操作。这里可以查看运行的进度和结果等。

在Github提交记录里也会显示CI运行的结果。

要告诉Tracvis执行什么,需要在我们的项目里添加一个.travis.yml文件,其最简单的配置如下:

这里指定了CI运行的语言,语言版本,哪些分支,install执行npm install, script是具体的操作部分,这里让CI执行 npm test。

Travis CI在执行完之后,会将结果邮件通知给用户, 默认规则如下:

By default, email notifications are sent to the committer and the commit author when they are members of the repository, 
that is they have

 - push or admin permissions for public repositories.
 - pull, push or admin permissions for private repositories.
Emails are sent when, on the given branch:

 - a build was just broken or still is broken.
 - a previously broken build was just fixed.

关于travis ci的生命周期等更多配置可以查阅这里

Coveralls 代码覆盖率托管平台
  • 代码覆盖率通常被用来衡量测试好换的指标。Coveralls就是将测试导出的覆盖率文件进行分析,以可视化的形式展现出来的一个工具。
  • 使用Coveralls的项目包括:React、Express、Gulp、Ant-design等等。

同样的使用Github账号登陆:

选择开启服务的仓库:

在项目的package.json文件script里添加一条coverage的命令, 即将istanbul等覆盖率工具生成的lcov文件给coveralls:

在travis.yml文件的after_script中运行npm run coverage,告诉CI服务器执行这条命令:

总结

  • 自动化测试不仅能有效的减少人工维护成本,同时为代码的维护迭代提供保障。

  • 前端自动化测试的方向有:单元测试、UI回归测试、功能测试、性能测试。

  • RegularUI采用karma+mocha的单元测试,ant-design使用Jest的snapshot测试与模拟用户的功能测试相结合的方式。

  • PhantomFlow是基于决策树的,对PhantomJS, CasperJS, PhantomCSS的包装。以简单的方式描述用户操作流。并配以CasperJS的页面操作,PhantomCSS的截图,达到非常好的自动化测试效果。

  • 测试时要保证数据的确定性和添加适当的断言。

  • CI是一种好的软件工程**。Travis CI简单易用,解放了开发人员手动运行测试,非常值得在项目中引入。

浅谈浏览器本地存储-indexedDB

indexedDB 是 HTML5 提供的一种本地存储,一般用户保存大量用户数据并要求数据之间有搜索需要的场景,当网络断开的时候,可以做一些离线应用,它比 SQL 方便,不用去写一些特定的语句对数据进行操作,数据格式为 json。

步骤:

1. 创建数据库,并指定数据库的版本号

var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
var request = indexedDB.open(databasename, version);
request.onerror = function(e){
    // 创建失败回调函数
};
request.onsuccess = function(e){
    // 创建成功回调函数
};
request.onupgradeneededd = function(e){
    // 当数据库改变是回调函数
};

注意这里的版本号只可以是整数

2. 建立对象存储空间

request.onupgradeneeded = function(event) { 
  var db = event.target.result;
  var objectStore = db.createObjectStore("name", { keyPath: "id" });
};

3. 数据的增、删、改、查

// 增
addData:function(db,storename,data){
    var store = store = db.transaction(storename,'readwrite').objectStore(storename),request;
    for(var i = 0 ; i < data.length;i++){
        request = store.add(data[i]);
        request.onerror = function(){
            console.error('add添加数据库中已有该数据')
        };
        request.onsuccess = function(){
            console.log('add添加数据已存入数据库')
        };
    }

}

添加数据,重复添加会报错

// 删
deleteData:function(db,storename,key){
    var store = store = db.transaction(storename,'readwrite').objectStore(storename);
    store.delete(key)
    console.log('已删除存储空间'+storename+'中'+key+'记录');
}
// 改
putData:function(db,storename,data){
    var store = store = db.transaction(storename,'readwrite').objectStore(storename),request;
    for(var i = 0 ; i < data.length;i++){
        request = store.put(data[i]);
        request.onerror = function(){
            console.error('put添加数据库中已有该数据')
        };
        request.onsuccess = function(){
            console.log('put添加数据已存入数据库')
        };
    }
}

重复添加已经存在的数据会更新原有数据

// 查
getDataByKey:function(db,storename,key){
    var store = db.transaction(storename,'readwrite').objectStore(storename);
    var request = store.get(key);
    request.onerror = function(){
        console.error('getDataByKey error');
    };
    request.onsuccess = function(e){
        var result = e.target.result;
        console.log('查找数据成功')
        console.log(result);
    };
}

根据存储空间的键找到对应数据,本例为 id

4. 关闭数据库

db.close();

2.使用场景

掌握了使用步骤之后,我们来结合它的优、缺点谈谈其使用场景。

  • 不适合过大的数据存储,浏览器对 indexDB 有 50M 大小的限制
  • 不适合对兼容性要求高的项目
  • 不适合存储敏感数据
  • 当用户清除浏览器缓存的时候可能出现问题
  • indexedDB 受到同源策略的限制

参考: indexedDB API

考拉升级https经验

一、为什么要升级https

  1. 保护用户数据传输过程中的安全。
  2. 运营商网络劫持问题越来越严重,各种广告插入、强制跳App下载等手段严重影响用户体验,如果有违法内容或者用户被骗,还会牵连到考拉。
  3. Apple要求iOS平台App在2017.1.1开始强制启用https,否则不允许上架App Store;启用https的应用如果要在各平台都正常,需应用内部webview打开的站点也全部启用https。
  4. 随着web标准的更新,越来越多的新特性(比如Service Worker等)要求站点开启HTTPS才能使用;

二、目标

  • 调整考拉前端代码,使其可同时支持https,http访问;
  • 与日常开发同步进行,不影响日常开发;
  • 稳步启用https,http双协议支持,最大限度避免https上线导致的问题;

三、前端做了哪些调整

升级范围为考拉前台所有站点,包括wap、web等工程,历史代码量大;主要是将原写死http协议的地方改造成相对协议,使其可同时兼容http及https服务;

1. 页面内资源协议修改

页面资源包括几部分: 静态资源(js,css,ui图片,视频等等),动态内容修改

  1. 静态资源
  • js: 调整js内部的http资源引用为相对协议,但是传递给外部应用的数据则根据 location.protocol 自动补全为当前页面使用的协议; 其次调整页面对js的引用,使js资源引用为相对协议;
  • css:调整mcss中图片等资源引用URL为相对协议;调整页面对css的引用为相对协议;
  • 打包调整: 调整打包工具配置,使其输出静态资源为相对协议;
  • 图片: 调整js、ftl中的图片缩略函数,区别是否为考拉和nos的图片,判断是否支持https做相应缩略处理,防止外部图片不支持https而显示不出来;全工程查找并替换硬编码图片标签src属性;
  • 外部库调整: 比如NEJ的一些模块或者工具,在URL改为相对协议是,认为其不是一个绝对地址,按相对协议解析会报错,会导致资源加载问题; 比如文件上传功能,在fallback 到flash方案时,flash信息都是写死为http,也做了调整;
  1. 动态内容

后端给数据,前端显示类型的内容:

  • 图文详情: 用正则匹配图文详情里面的img,video标签,并替换http协议为相对协议;
  • 页面内部插值输出: 调整js、ftl缩略图过滤器,为未使用过滤器的图片等资源增加过滤器,自动进行相对协议处理;
  1. 页面跳转

由于部分运营配置的页面,都是直接配置了http链接,用户点击页面的链接很容易跳回到http,不能长期在https下使用,于是我们在页面的a标签点击时自动根据当前页面协议,调整a标签的href属性的协议为当前协议,来支持正常跳转。

2. CSP规则调整

原CSP规则限制了页面内部加载的资源,协议必须一直,但是https上线初期缓和内容难以避免,所以在CSP中增加了规则,允许浏览器加载混合内容(仅限图片、视频);

3. 外部关联调整

  • 微信支付: 调整微信支付后台登记的安全域名信息,增加https url,使其同时支持双协议; 但是此调整不能在业务高峰期做,否则可能影响线上支付;

四、升级过程

这里仅仅列出主要的步骤,细节还比较多:

  1. 前端修改双协议支持后的代码上线,并让cdn支持https
  2. 公司内网双协议打开进行内部小范围试用
  3. 线上双协议打开,但是不开启https强制跳转
  4. 线上功能级别灰度启用https,让用户可以从一些页面开始使用https
  5. 全部放开

五、后续其它可以做的

  1. 上线h2, 已经在做了,很快开始内部测试
  2. 域名收敛
  3. 其它优化等

by 渔樵

webpack构建之hash缓存的利用

webpack构建之hash缓存的利用

众所周知,优化一个页面的方法之一就是利用浏览器的缓存机制,在文件名不改变的情况下,使客户端不用频繁向服务端请求和重复下载相同的资源,节约了流量的开销。

现在前端的主要构建工具就是使用webpack,现在就说说使用webpack时遇到的hash的坑。

在生产环境发布时,为了利用缓存,都会加一个标识,webpack打包时也可加入这个功能,对于一个基本的应用,最开始的用法估计这样,见如下代码

module.exports = {
    entry: {
        index: path.resolve(__dirname, '../src/js/index.js'),
        index2: path.resolve(__dirname, '../src/js/index2.js'),
        vendor: ['react', 'react-dom'] //第三方模块
    },

    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].[hash:8].js',
        publicPath: '/'
    },
    
    module: {
        rules: [{
            test: /\.jsx?$/,
            use: ['babel-loader'],
            exclude: /node_modules/
        }
        //省略其他loader
        ]
    },
    
    plugins: [
    	 new CommonsChunkPlugin({
            name: 'vendor'
        })
        //省略其他plugin
    ] 
}

打包结果如下:
hash1

这里的hash是每次编译时所计算出来的一个值,因此当任何一个文件修改,hash都将会改变,而且所打包出来的文件名也将不一样了,这样对于需频繁发布的项目不是很友好,会造成每次有新版本,用户浏览器都将重新下载所有的文件,为了避免这种情况,webpack还提供了chunkhash(:这里提取出的css文件hash值不一样,是因为使用的是extract-text-webpack-plugin插件,它提供了自己的一个contenhash,也是对于css文件建议的一种用法,保证了css有自己独立的hash,不会受到js文件的干扰)

将上述代码中的hash换成chunkhash后打包结果如下:
hash2
看到每个chunk有自己单独的hash值,此时只修改某一个模块里的文件,将不会影响到其他的模块打包出的hash值了,这样也就能充分利用hash缓存了。


你以为到此处就结束了吗,too young too simple!

你可以试试改动一个文件,打包后vendor的hash值每次都是在变化的,第三方模块是最不长改动的模块,更应该被缓存住,可为什么vendor的chunkhash总是变化,是因为webpack runtime由于entry对应的Id变化而发生了变化,chunkhash的计算又依赖于runtime,因此vendor的chunkhash也发生了变化。

为了解决这个问题,我们首先得保证chunkId的稳定,参考webpack2的文档caching,可以使用HashedModuleIdsPlugin的插件(webpack1也可以使用,但需要将HashedModuleIdsPlugin.js自行引入),然后就是创建一个额外的chunk来提取runtime,就是文档中说的manifest,部分代码如下

plugins: [
    new CommonsChunkPlugin({
	names: ['vendor', 'manifest'],
        minChunks: Infinity
    }),
    new webapck.HashedModuleIdsPlugin()
]

打包后将多出一个很小的manifest.js的文件,但保证了vendor的hash没有改变,对于manifest的内容我们需要优先引入,在此可以借助inline-manifest-webpack-plugin将manifest内容内联进html文件中,以免多发一次js的请求,使用方式可直接参考文档。

至此对于hash的变化才算真正结束,才能达到利用hash的变化真正的控制住缓存。

PS. 建议不要用webpack-md5-hash,会有坑的,见webpack-md5-hash-issue

参考资料:

webpack的cache

extract-text-webpack-plugin

webpack-md5-hash

http缓存

inline-manifest-webpack-plugin

webpack-md5-hash-issue

当产品/后端/QA/你自己说了这些话,就要警惕了!


title: 当产品/后端/QA/你自己说了这些话,就要警惕了!
date: 2017-05-15

如下观点不一定适用于所有场景

欢迎小伙伴踊跃补充!!

产品

1、『这个功能很简单,跟XX保持一致就行』

跟什么一致啊,小姐姐请把话说清楚,然后明确到交互稿上吧;

2、『加个小需求呗,很简单的(已经快上线了)』

一旦涉及到业务逻辑,要拉QA、后端一起评审,评估清楚影响范围,不怕花时间,就怕线上bug;

3、『(交互稿)这个地方要这样那样改一下』

一定要求产品更新交互稿(方便QA测试、留下证据、以后产品开发QA都有可能查阅,用处非常大),更新之后再看一看交互,确保自己的理解跟产品是一致的

4、『这个字段/文案前端写死就行了』

无论前端写死还是后端传,要认识到「我比较懒」这个实际情况;

如果产品要求三端/两端一致,文案让后端拼好直接传就很合适了

后端

1、『接口定义好了发给你』

一定要用NEI!没用过?没关系,就让小哥哥来手把手的教教你;

接口定义好之后,要先对一遍,有问题可以马上修改,避免开发过程中发现缺少字段,或字段不便于使用;

2、『别急,接口我就快整理好了,今天一定能给你(开发N天后,后端进度>50%)』

先定义接口再开发是原则问题。不单是口头定义,而是在NEI上详细的定下来。如果实在定不了,可以先定个v1版,后面再进行调整;

3、『sortType为0是综合,2是新品,5是价格,balabala』

业务逻辑尽量封装到后端,前端模块尽可能通用,只负责根据后端数据进行渲染,尽量与业务逻辑解耦(尤其是已经/未来会通用的组件)。

在这个例子中,价格(sortType===5)是一种交互效果,其他的是另一种交互效果,那么要求后端通过新增一个字段,对这两种交互进行区分,前端就不需要关心sortType的具体含义了;

QA

1、『部署失败了,你看看[.png](**.java报错)』

对于容易分辨归属(前后端)的问题,教会QA如何分辨它们,当他的分类能力提高,对所有人(尤其是自己)的效率提升都很有帮助。还可以教给他们如何通过查看同步/异步数据定位问题;

2、『模块A我测出来个bug,你改一下,(一个小时后),模块C也有问题,你再看看』

建议在互相不影响的情况下,A模块测出问题后,先测试其他模块/页面,最后一起交给开发来修改,这样会减少打断开发手头的工作,QA也可以集中精力测试;

自己

1、『qa妹子好,我这里有个小改动希望搭车上线』

搭车上线:代码提交到QA的另一个任务的分支中,一起上线

一般不建议搭车,单独提个任务拉分支很难么,还能提高表面上的业绩呢;

2、『这个任务我们自测(开发自提需求)』

自测的任务在codereview/resolve之后,要尽快跟QA要测试资源测掉,避免QA以为你已经自测过,而将未测的代码上线;

同时:开发自提任务JIRA描述标准,需要有下面几块内容:

  • 背景和目的
  • 任务内容
  • 其他注意事项

未完待续,欢迎补充!!

by tianyanan

FormData介绍与使用

FormData

XMLHttpRequest Level 2添加了一个新的接口FormData.在开发中使用后,觉得是form表单的加强版。可以异步的,灵活的发送数据,并且包括上传二进制文件

创建FormData

创建FormData的方式

  • 直接创建
var formData = new FormData();
formData.append('businessId', '3049');
formData.append('companyName', 'test');
  • 利用form节点初始化
<form enctype="multipart/form-data" method="post" name="fileinfo">
  <label>Your email address:</label>
  <input type="email" autocomplete="on" autofocus name="userid" placeholder="email" required size="32" maxlength="64" /><br />
  <label>Custom file label:</label>
  <input type="text" name="filelabel" size="12" maxlength="32" /><br />
  <label>File to stash:</label>
  <input type="file" name="file" required />
</form>
var fData = new FormData(document.forms.namedItem('fileinfo'));

method

append

FormData.append()会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键,如果存在则会加在原有的值后面.即类似于数组的2号位

formData.append(name,value);
formData.append(name,value,fileName);

name: key(键)
value: 值
fileName: 当value为Blob或者file的时候,fileName为文件名

delete

FormData.delete()会删除一对键值

formData.delete(name);

entries

FormData.entries()会返回一个iterator对象,我们可以通过它遍历FormData的键值.

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('companyName', 'test');
for(var pair of formData.entries()) {
   console.log(pair[0]+ ', '+ pair[1]); 
}

结果为

businessId, 3049
companyName, test

get

FormData.get()用于获取key所对应的value的首位值

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('businessId', '3050');
console.log(formData.get('businessId'));

结果为

3049

getAll

FormData.getAll()用于获取key所对应的value
的全部值。如果对应的值超过一个,则会返回一个数组.

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('businessId', '3050');
console.log(formData.get('businessId'));`

结果为

['3049','3050'];

has

FormData.has()用于检查是否含有指定的key。返回一个布尔值,true有,false即没有

var formData = new FormData();
formData.has('businessId'); //false
formData.append('businessId','3049');
formData.has('businessId'); //true

keys

FormData.keys()会返回一个iterator对象,对于遍历所有的key

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('companyName', 'test');
for (var key of formData.keys()) {
   console.log(key); 
}

结果为

businessId
companyName

set

FormData.set()方法会对FormData对象里的某个key设置一个新的值,如果该key不存在,则添加。如果存在,不管之前key所对应的值有几个,都会被完全覆盖。

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('businessId', '3050');
formData.set('businessId','3051');
console.log(formData.getAll('businessId'));

结果为

['3051']

values

FormData.values()会返回一个iterator对象,values

var formData = new FormData();
formData.append('businessId', '3049');
formData.append('companyName', 'test');
for(var value of formData.values()) {
   console.log(value); 
}

结果为

3049
test

兼容性

Feature Chrome Firfox(Gecko) Intenet Explorer Opera Safari
Basic support 7+ 4.0(2.0) 10+ 12+ 5+
append with filename (Yes) 22.0(22.0) ? ? ?
delete, get, getAll, has, set Behind Flag Not supported Not supported (Yes) Not supported

深入了解层叠上下文和层叠顺序

前话:z-index只是CSS层叠上下文和层叠顺序中的一个马仔

1. 层叠上下文 stacking context

当元素具有了层叠上下文,这个元素在z轴上就“高人一等”,离用户更近

2. 层叠水平 stacking level

  • 所有元素都有
  • 决定同一层叠上下文中的元素在z轴上的显示顺序
  • 比较两个元素的层叠水平,必须是这两个元素在同一个层叠上下文中,否则没有意义
  • z-index可以影响层叠水平(只是影响,不是决定)

3. 层叠顺序 stacking order

层叠顺序是规则(在同一个层叠上下文中的元素,对应下面规则的序号越大,位置越高)

  1. 层叠上下文 background/border
  2. 负z-index
  3. block块级元素
  4. float浮动元素
  5. inline/inline-block行内元素
  6. z-index:auto或者z-index:0(不依赖z-index的层叠上下文 看6.3)
  7. 正z-index

1通常是装饰属性;34是布局,5是内容——所以行内元素具有较高的层叠序号

4. 务必牢记的层叠准则

  1. 谁大谁上:具有明显的层叠水平标识的时候(如z-index),层叠水平高的元素在上面(官大的压死官小的)

  2. 后来居上:层叠水平一致、层叠顺序相同时,DOM流后面的元素会覆盖前面的元素(长江后浪)

5. 层叠上下文的特性(尽情把它当成一个特殊元素)

  1. 层叠上下文的层叠水平比普通元素高
  2. 层叠上下文可以阻断元素的混合模式
  3. 层叠上下文可以嵌套,内部层叠上下文及其子元素均受制于外部的层叠上下文
  4. 层叠上下文和兄弟元素相互独立
  5. 每个层叠上下文自成体系,不同层叠上下文元素发生重叠的时候,以父级层叠上下文的层叠顺序决定层叠水平

6. 创建层叠上下文

  1. 页面根元素具有层叠上下文——根层叠上下文

  2. z-index:int 的定位元素

    position:relative || absolute || fixed;声明的定位元素,当z-index不是auto的时候,会创建层叠上下文

    举栗子:定位元素divz-index:auto,是普通元素,则内部元素就按z-index大小决定层叠顺序;但是当z-index: 0,div就是层叠上下文元素。特性5和“后来居上”的准则作用下img的index失效。

  3. 其他CSS3属性

    1. z-index不为auto的flex-item

      • 条件1:父级需要是flex/inline-flex
      • 条件2:子元素z-index不为auto

      满足上面两个条件的flex-item成为层叠上下文元素。看栗子

    2. opacity值不为1

      半透明元素具有层叠上下文 后面几个属性的例子都在这里哟

    3. transform不是none

    4. mix-blend-mode不是normal

      类似于PS的混合模式

    5. filter不是none

    6. isolation:isolate

      isolation是为mix-blend-mode而生的属性;mix-blend-mode混合默认z轴所有层叠在下面的元素,例子中box也被混合了。但是加上isolation:isolate,box就具有层叠上下文,层叠水平变高,不被混合

    7. will-changed指定的属性值为上面任意一个

      will-change是提高页面滚动、动画等渲染性能的属性 了解一下

    8. -webkit-overflow-scrolling:touch

      CSS3针对iOS的新属性,激活iOS平滑滚动

7. 层叠上下文与层叠顺序

  1. 一旦普通元素具有层叠上下文,层叠顺序就会变高。其顺序要分两种情况:

    1. 层叠上下文元素不依赖z-index数值,可以看作z-index:auto或者z-index:0,排在正z-index后面
    2. 层叠上下文元素依赖z-index数值,则由数值决定
  2. 定位元素层叠在普通元素之上,因为一旦成为定位元素,z-index自动生效,默认z-index:auto也可以看作z-index:0;所以会覆盖block、inline、float元素

  3. 层叠上下文元素的层叠顺序是z-index:auto级别

    <img id="img1" src="" style="transform:scale(1);">
    <img id="img2" src="" style="position:relative">
    

    上面两个图片,img1是层叠上下文元素层叠顺序是z-index:auto,img2是定位元素z-index:auto;所以根据DOM流的位置决定了层叠表现,“后来居上”。

解决问题

做用户留存表格,需要实现鼠标hover时所在列和行高亮呈现十字效果。参考网上的代码发现高亮有问题:鼠标只能往下移动或者左右移动,不能向上移动。

简化代码看栗子

  • 原因:在::after伪元素还没有加z-index的时候,由于“后来居上”的渲染层叠顺序,::after之前的DOM元素在其宽高之内的会被覆盖
  • 解决:在table加transform:scale(1)触发table具有层叠上下文;在::after加z-index: -1

根据层叠顺序,负z-index元素 > 层叠上下文。

所以伪元素可以被用户看见但又不至于阻止鼠标的hover行为。(nice)

参考

segmentfault row_and_column_highlight

深入理解CSS中的层叠上下文和层叠顺序——张鑫旭

CSS Masonry Layouts(1)—— multi-columns

CSS Masonry Layouts(1)—— multi-columns

Masonry Layouts —— 瀑布流布局,核心是一个网格布局,每行包含的内容列表的高度是不可控的,并且,每个内容列表呈堆栈状排列。称作瀑布流,最关键的是——各堆栈间没有多余的间距差(将整个布局看做一个大的容器,最新进来的内容元素,永远在高度最短的那一列上)。具体如下图所示:

注:上图是 二次元社区GACHA 的插画频道,该瀑布流插件基于NEJ,交互要求是:每一列固定宽度,根据页面大小显示不同的列,页面宽度,重排,预加载一屏,滚动加载。设计的思路是:

  1. 计算列数:根据当前页面(容器)宽度,计算一行可以排放几个元素
  2. 初始堆栈:建立列堆栈数组,用于存放各个列堆栈的高度
  3. 最小索引:得到列堆栈数组中的最小值和其对应索引
  4. 元素定位:所有元素 absolute 定位,设置后续加入元素的 top 值为列堆栈数组中的最小值,同时设置它的 left 值为该索引对应列的 left 值
  5. 更新堆栈:更新列堆栈数组中该索引下元素的值( no.4 中的 top 值加元素本身的的高度)
  • 注:上面略去了对数据的预处理(包括图片的等比缩放以及属性的二次处理等)以及滚动加载的处理过程,对于预加载一屏,由于元素高度的不可控性,只能根据瀑布流元素的平均高度行预估计,然后推算出一屏需要元素的范围,然后请求相应数量。

简化版应该是下面这样的:

但是,这样处理,表现层的东西依赖 javaScript 来处理,有点多余(当时确实是唯一的办法),能不能用样式来搞定呢?

首先,会想到 float 或者 inline-block ,但是它们都没办法很好的控制列表之间的间距。最终得到的效果就像下面这样:

那,除此之外,就没有单纯用 css 可以搞定的方法了吗?近几年,css 的技术更新频繁,出现了很多新的布局方法:multi-columnsFlexboxGrid。用上面提到的布局方法能否实现瀑布流呢?

multi-columns

multi-columns 产生之初,是用来实现文本多列排列,类似报纸、杂志的文本排列方式。multi-columns布局方式跟瀑布流的有些类似:

demo

三列:

四列:

猛一看很完美。但是,与使用 js 实现对比之下:

  • multi-columns 的元素是纵向排列的(在对元素先后次序有要求的情况下不理想)
  • 随着容器宽度变化,明显发现出现比较大的空白区域(从三列到四列)

第一个问题,multi-columns 的这种布局方式,决定了只能纵向排列;对于第二个问题:

multi-columns 简略布局**

首先,只有在多列元素集含有块级元素、并且避免在元素内部断行并产生新列的时候,才会涉及到布局,上面的瀑布流例子,要是不避免在元素内部断行并产生新列,将会是这样的:

只有将属性 break-inside 设置为 avoid,才会有块级元素的效果。

回到上面说到的空白区域的问题,multi-columns 的整体布局会受到几个因素的影响:

总体而言,优先级:自身属性 > 容器属性。

当容器的高度小于按照 column-count 布局后的列的高度,会发生元素断裂、跨列的现象;当容器的高度大于按照 column-count 布局后的列的高度,单个列的高度会由最优布局算法生成。

所谓最优布局算法,简单来说,就是自适应:

  • 首先取 column-count ,如果元素(block)个数不超过 column-count ,布局成一横排;

  • 若是元素(block)个数超过 column-count ,首先在第一列增加 block 元素,而后以第一列的高度为标准,来填充后续各列(此时会发生列填充不满的情况);

  • 若是之后元素中发现一个高度较高的X元素,对于布局的调整会以这个较高的X元素为分界线,不会将后面的低高度元素排列到X元素之前:

就会出现较大空白区域的情况。
所以,出现空白区域的根本原因是:multi-columns 布局的特点是按列布局、顺序计算、顺序排列,前面有较大空白区域,不会用后续元素去填补。

想要具体了解 multi-columns 布局,请参考:


总结:

multi-columns 瀑布流布局,适用于:

  1. 元素不存在优先级,或者优先级要求较低
  2. 元素高度差异不明显(防止出现大片空白区域)
  3. 不兼容低版本浏览器(最重要的)

除了 multi-columnsFlexbox 以及 Grid 也可以运用到瀑布流布局中来。未完,待续。。。

新活动模板方案改版总结

活动模板自上次改版以来,在不停的优化,这次又新改版了一次,主要是增加了自定义模块这个版块。先具体介绍下实现思路。

改版前

所有的活动模块数据通过前端JST模板渲染,如果是同步模块(比如图片,导航等)会直接渲染,如果是异步模块(商品,品牌,秒杀等)则滚动异步发请求加载。

优点:减轻服务端渲染模板的压力。滚动异步加载,分散服务端请求的压力。

缺点: 当活动模板越来越多时,模块文件耦合还是比较严重,嵌套较深,活动模板外的其他页面无法复用活动模块。模版的js文件包括所有依赖,打包后文件也较大。

改版后

特点

模块的按需加载:考虑到修改成本和时间关系,以前老模版不做修改,只改新加的模版。每个模版包含模版自身的html,js,css等资源文件。打包后每个模版对应一个html文件。活动页面配置了哪些模块,就会动态去异步加载该模块对应的html文件。没有配置的模块,对应的资源文件不会被加载。

模块复用:一个模块的完整显示需要两个条件,一是该模块对应的html文件,二是该模块需要显示的数据。满足这两个条件,该模块就可以在任意页面上复用。第一个条件很容易满足,每个模块对应的html路径是固定的,第二个条件则是通过异步请求去获取数据,异步请求中需要模块id作为参数。这些数据都可以由后端同步给出。

实现步骤

  1. 页面加载时,如果该页面中存在自定义模块,会根据同步给出的模块数据,请求这个模块指定的html文件。同步数据结构如下:
{
    "zoneKind": 23,
    "locateId": "1803",
    "zoneConfigMap": {
        "mid": "123456458",
        "moduleSrc": "/other/album/index.html",   //模块对应的html文件名 固定
        "moduleType": 20001   //模块类型值 固定
    }
}

跟之前的模块相比,同步数据里少了很多字段,只保留了必要字段。大大减少了文件的体积。

  1. 模块的html文件加载完成后,执行onload回调函数,解析html文件中的内容,html文件中内容用标签textarea表示,类型为name属性的值,根据不同的类型去解析标签里的内容。比如js类型,会在页面中添加script标签插入到页面中,css类型会将样式代码内联到页面中等。html中内容参考如下:
<textarea name="css" data-src="/src/javascript/html/goods/comment.goods/index.css"></textarea>
<!-- @TEMPLATE -->
<textarea name="js" data-src="/src/javascript/html/goods/comment.goods/index.js"></textarea>
<!-- /@TEMPLATE -->
  1. html文件中包含的js脚本里为模块的处理逻辑,当JS文件请求完成后,会触发load方法。此时不同环境的处理逻辑不一样,在开发环境下,define需要依赖其他的脚本,所以define的回调函数可能比load触发要晚,但是在生产环境里,文件被打包后同步执行脚本,define的回调函数比load要早执行。

    以生产环境为例,define回调里最后将模块类存到数组里,然后执行脚本的load方法,也就是从数组中取出保存的模块类,实例化该模块类,并插入到页面的节点中。开发环境的逻辑相反,先触发脚本的load方法,将模块需要放置的节点保存到数组里,然后触发模块脚本内部的define回调函数,从数组中取出模块的容器节点,实例化该模块类,展示模块。详细逻辑可以查看源码:/src/javascript/html/load.module.js

//define 回调里的操作
if(DEBUG){    //开发环境
  var parent = __klassStack.pop();  //模块节点
  this.__allocateModule(klass,parent);
  var url  = scriptUrlStack.pop();
  var theSameKlassInStack = postionUrlKlass(url,scriptUrlStack);
  if(theSameKlassInStack.length){
    for(var j=theSameKlassInStack.length-1;j>=0;j--){
      var parentBox = __klassStack.splice(j,1);
      this.__allocateModule(klass,parentBox[0]);
    }
  }
}else{    //生产环境
  __klassStack.push(klass);
}
//脚本load回调函数
if(DEBUG){
  var url = script.src;
  scriptUrlStack.push(url);
  __klassStack.push(_options.box);
}else{
  var _klass = __klassStack.pop();
  self.__allocateModule(_klass,_options.box);
}
  1. 脚本包含模块的内部处理逻辑,模块脚本加载完后,什么时候需要发异步请求获取模块数据。之前考虑做滚动加载,在模块脚本内部添加scroll事件,监听该模块是否满足显示的条件,满足了就触发脚本内获取异步接口的方法,发现加了滚动事件后在客户端内滚动多次,会导致客户端崩溃。所以现在的处理是直接发了请求,后面等想到优化方案再改。发异步请求需要的参数通过容器节点的dataset来获取。

  2. 通过异步请求获取到数据后,要如何渲染视模块而定。目前有两种方式可供选择,regularjs和jst两种前端框架。如果是jst渲染,则需要在模块对应的html文件中添加字符串片段。使用regularjs开发时,可将模块尽量拆分成小组件,最终模块会由不同的组件组合,也提高了组件的复用性。所有模块的脚本都继承自一个公共的模块脚本文件,模块共用的方法会放在共用脚本里。

遗留问题

虽然说这种方案已经实现了,但还是有些问题,还有很多优化空间,这里简单整理下遗留的问题,待后续再修改。先实现功能,后优化性能。

浅谈HTML5 Web Worker

浅谈HTML5 Web Worker

前言

总所周知,Javascript是运行在单线程环境中,也就是说无法同时运行多个脚本。假设用户点击一个按钮,触发了一段用于计算的Javascript代码,那么在这段代码执行完毕之前,页面是无法响应用户操作的。但是,如果将这段代码交给Web Worker去运行的话,那么情况就不一样了:浏览器会在后台启动一个独立的worker线程来专门负责这段代码的运行,因此,页面在这段Javascript代码运行期间依然可以响应用户的其他操作。

Web Worker是什么鬼?

Web Worker 是HTML5标准的一部分,这一规范定义了一套 API,它允许一段JavaScript程序运行在主线程之外的另外一个线程中。

Web Worker 规范中定义了两类工作线程,分别是专用线程Dedicated Worker和共享线程 Shared Worker,其中,Dedicated Worker只能为一个页面所使用,而Shared Worker则可以被多个页面所共享,下文会重点介绍Dedicated Worker。

快速上手

创建worker

只需调用Worker() 构造函数并传入一个要在 worker 线程内运行的脚本的URI,即可创建一个新的worker。

var myWorker = new Worker("my_task.js");

// my_task.js中的代码 
var i = 0;
function timedCount(){
    i = i+1;
    postMessage(i);
    setTimeout(timedCount, 1000);
}
timedCount();

另外,通过URL.createObjectURL()创建URL对象,可以实现创建内嵌的worker

var myTask = `
    var i = 0;
    function timedCount(){
        i = i+1;
        postMessage(i);
        setTimeout(timedCount, 1000);
    }
    timedCount();
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));

这样,就可以结合NEJ、Webpack进行模块化管理、打包了。

注意:传入 Worker 构造函数的参数 URI 必须遵循同源策略。

提示:本文所有的示例代码均可直接拷贝到chrome控制台中运行。

传递数据 onmessage 、postMessage(data)

Worker 与其主页面之间的通信是通过 onmessage 事件和 postMessage() 方法实现的。

在主页面与 Worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 Worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 Worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。

也就是说,Worker 与其主页面之间只能单纯的传递数据,不能传递复杂的引用类型:如通过构造函数创建的对象等。并且,传递的数据也是经过拷贝生成的一个副本,在一端对数据进行修改不会影响另一端。

var myTask = `
    onmessage = function (e) {
        var data = e.data;
        data.push('hello');
        console.log('worker:', data); // worker: [1, 2, 3, "hello"]
        postMessage(data);
    };
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));

myWorker.onmessage = function (e) {
    var data = e.data;
    console.log('page:', data); // page: [1, 2, 3, "hello"]
    console.log('arr:', arr); // arr: [1, 2, 3]
};

var arr = [1,2,3];
myWorker.postMessage(arr);

通过可转让对象来传递数据

前面介绍了简单数据的传递,其实还有一种性能更高的方法来传递数据,就是通过可转让对象将数据在主页面和Worker之间进行来回穿梭。可转让对象从一个上下文转移到另一个上下文而不会经过任何拷贝操作。这意味着当传递大数据时会获得极大的性能提升。和按照引用传递不同,一旦对象转让,那么它在原来上下文的那个版本将不复存在。该对象的所有权被转让到新的上下文内。例如,当你将一个 ArrayBuffer 对象从主应用转让到 Worker 中,原始的 ArrayBuffer 被清除并且无法使用。它包含的内容会(完整无差的)传递给 Worker 上下文。

var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
  uInt8Array[i] = i;
}

console.log(uInt8Array.length); // 传递前长度:33554432

var myTask = `
    onmessage = function (e) {
        var data = e.data;
        console.log('worker:', data);
    };
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));
myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

console.log(uInt8Array.length); // 传递后长度:0

Worker上下文

Worker执行的上下文,与主页面执行时的上下文并不相同,最顶层的对象并不是window,而是个一个叫做WorkerGlobalScope的东东,所以无法访问window、以及与window相关的DOM API,但是可以与setTimeout、setInterval等协作。

WorkerGlobalScope作用域下的常用属性、方法如下:

1、self

我们可以使用 WorkerGlobalScope 的 self 属性来或者这个对象本身的引用

2、location

  location 属性返回当线程被创建出来的时候与之关联的 WorkerLocation 对象,它表示用于初始化这个工作线程的脚步资源的绝对 URL,即使页面被多次重定向后,这个 URL 资源位置也不会改变。

3、close

  关闭当前线程

4、importScripts

  我们可以通过importScripts()方法通过url在worker中加载库函数

5、XMLHttpRequest

  有了它,才能发出Ajax请求

6、setTimeout/setInterval以及addEventListener/postMessage

终止 terminate()

在主页面上调用terminate()方法,可以立即杀死 worker 线程,不会留下任何机会让它完成自己的操作或清理工作。另外,Worker也可以调用自己的 close() 方法来关闭自己

    // 主页面调用
    myWorker.terminate();
    
    // Worker 线程调用
    self.close();

处理错误

当 worker 出现运行时错误时,它的 onerror 事件处理函数会被调用。它会收到一个实现了 ErrorEvent 接口名为 error的事件。该事件不会冒泡,并且可以被取消;为了防止触发默认动作,worker 可以调用错误事件的 preventDefault() 方法。

错误事件有三个实用的属性:filename - 发生错误的脚本文件名;lineno - 出现错误的行号;以及 message - 可读性良好的错误消息。

var myTask = `
    onmessage = function (e) {
        var data = e.data;
        console.log('worker:', data);
    };
    
    // 使用未声明的变量
    arr.push('error');
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));
myWorker.onerror = function onError(e) {
    // ERROR: Line 8 in blob:http://www.cnblogs.com/490a7c32-7386-4d6e-a82b-1ca0b1bf2469: Uncaught ReferenceError: arr is not defined
    console.log(['ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join(''));
}

总结

最后总结下Web Worker为javascript带来了什么,以及典型的应用场景。

强大的计算能力

可以加载一个JS进行大量的复杂计算而不挂起主进程,并通过postMessage,onmessage进行通信,解决了大量计算对UI渲染的阻塞问题。

典型应用场景

1、数学运算

Web Worker最简单的应用就是用来做后台计算,对CPU密集型的场景再适合不过了。

2、图像处理

通过使用从中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同Workers来做计算,对图像进行像素级的处理,再把处理完成的图像数据返回给主页面。

3、大数据的处理

目前mvvm框架越来越普及,基于数据驱动的开发模式也越愈发流行,未来大数据的处理也可能转向到前台,这时,将大数据的处理交给在Web Worker也是上上之策了吧。

参考文献

chrome插件开发简介(一)——开发入门

chrome插件开发简介(一)——开发入门

chrome作为目前最流行的浏览器,备受前端推崇,原因除了其对于前端标准的支持这一大核心原因之外,还有就是其强大的扩展性,
基于其开发规范实现的插件如今已经非常庞大,在国内也是欣欣向荣,
如天猫开发了大量的扩展,用于检测页面质量以及页面性能,淘宝开发了许多的扩展以供运营工具的优化等等。其强大性不言而喻。

这里我们来讲一下其插件开发

基础概念

  1. 与chrome应用类似,chrome扩展主要是用于扩充chrome浏览器的功能。他体现为一些文件的集合,包括前端文件(html/css/js),配置文件manifest.json。
    主要采用JavaScript语言进行编写。

个别扩展可能会用到DLL和so动态库,不过出于安全以及职责分离的考虑,在后续的标准中,将会被舍弃,这里不再赘述

  1. chrome扩展能做的事情:
  • 基于浏览器本身窗口的交互界面
  • 操作用户页面:操作用户页面里的dom
  • 管理浏览器:书签,cookie,历史,扩展和应用本身的管理,其中甚至修改chrome的一些默认页面,地址栏关键字等,包括浏览器外观主题都是可以更改的

这里需要着重提一下,前端开发者喜欢的DevTools工具,在这里也能进行自定义

  • 网络通信:http,socket,UDP/TCP等
  • 跨域请求不受限制:
  • 常驻后台运行
  • 数据存储:采用3种方式(localStorage,Web SQL DB,chrome提供的存储API(文件系统))
  • 扩展页面间可进行通信:提供runtime相关接口
  • 其他功能:下载,代理,系统信息,媒体库,硬件相关(如usb设备操作,串口通信等),国际化
  1. chrome扩展的限制:
  • 环境限制:基本上功能性操作都需要通过chrome提供的API来完成,这跟实际的页面js又有一些差异,看上去并没有那么的完全自由。
    如chrome扩展的页面脚本,可以获取并操作页面dom,但是,出于安全性的考虑,页面脚本的域是独立区分开来的,即js成员变量不共享。
    即:插入到用户页面的js脚本可执行,但是与原始脚本不同执行域,互相不会影响。
    content_script不能使用除了chrome.extension之外的chrome.* 的接口,不能访问它所在扩展中定义的函数和变量,不能访问web页面或其它content script中定义的函数和变量,不能做cross-site XMLHttpRequests

  • chrome扩展,其功能受限于chrome的API,比如说文件系统,必须通过chrome的fileSystem接口,而这些接口仅仅只是对html5已有的文件系统接口的扩展,
    它允许Chrome应用读写硬盘中用户选择的任意位置,而HTML5本身提供的文件系统接口则只能在沙箱中读写文件,并不能获取用户磁盘中真正的目录。

  • 其他,一些潜在的会引起漏洞的操作,或者说不建议的操作,会被禁止掉。
    比如,想要通过用js代码主动唤起打开devtools面板,这个是不允许的。
    https://bugs.chromium.org/p/chromium/issues/detail?id=112277

可以看到回复:

We only allow explicit devtools opening.

举个栗子:apple:

下面以一个简单的例子来简述一下我们开发扩展过程中会遇到哪些问题

这个栗子主要用于去自定义用户页面的样式(比如此处为改滚动条样式),插入一个自定义的脚本到用户页面中执行(此处为输出一些简单信息)

1.编辑扩展程序所需要的主要文件夹

文件结构说明

 ./
 ├─ manifest.json //扩展的配置项
 ├─ Custom.js     //自定义js脚本
 ├─ Custom.css    //自定义css样式
 ├─ icon.png      //扩展程序的icon
 └─ popup.html    //扩展的展示弹窗
  • 自定义js脚本:浏览器中执行,但并非真正意义上的“插入”,与原来页面的js域隔离开

例如,custom.js中的window跟页面js中的window不是同一个对象,变量也无法共享。

console.log('执行init');
console.log(document.title);
console.log(document.getElementById("abc"));
// 实例函数,可以供popup.html中调用
function helloWorld(name){
    console.log(`${name} say 'hello world!'`);
    alert(`${name} say 'hello world!'`);
}
//...

这样的脚本路径将以 chrome://extensions/扩展的id串码/path/to/you/js/xxx.js出现在控制台调试时候。

image

  • 自定义样式:同样,css也不是真正的插入到页面的document中,而是浏览器的样式生效策略中会加入这些样式规则

image

/* 重置滚动条 */
::-webkit-scrollbar {width:4px!important;height:7px;}
::-webkit-scrollbar-button {display:none;}
::-webkit-scrollbar-thumb {border-radius:3px;background: #45A5DB;}
::-webkit-scrollbar-track {width:4px;height:7px;background:transparent;}
::-webkit-scrollbar-track-piece {background:transparent;}

/* ... */
  • 小窗口popup.html:这里面是一个按钮,点击后去调用custom.js里面的helloWorld函数
<!doctype html>
<html>
  <head>
    <title>hello world</title>
  </head>
  <body>
    <h2>hello world</h2>
	<p><button onclick="dealClick('王小明')">按钮</button></p>
    <script>
        function dealClick(name){
            //这里值得注意,大部分功能性操作,比如执行脚本,执行函数,都是不可以直接执行,而需要通过chrome.*这样方式进行
            chrome.tabs.executeScript(
                null,
                {
                    code: `helloWorld('${name}')` // 这里调用的是上面的custom.js重定义好的function
                }
            );
        }
    </script>
  </body>
</html>
  • 核心配置文件manifest.json
{
  "name": "扩展名称",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "扩展描述",
  "icons" : {           // 扩展的icon
    "16" : "icon.png",
    "48" : "icon.png",
    "128" : "icon.png"
  },
  "browser_action": {   // browser_action表示程序图标会出现在地址栏右侧,若要出现在地址栏,则写成page_action
    "default_title": "日报工具",
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "content_scripts": [  //content_scripts是在Web页面内运行的javascript脚本。
                        //通过使用标准的DOM,它们可以获取浏览器所访问页面的详细信息,并可以修改这些信息。
    {                   //这里的值是数组,可以针对多个站点进行不同的操作配置
      "matches": [
        "http://www.google.com/*"
      ],
      "css": [
        "custom.css"
      ],
      "js": [
        "custom.js"
      ],
      "all_frames": true,
      "run_at": "document_idle"
    }
  ],
  "permissions": [   //一些权限的配置,
    "cookies",       //比如cookie权限,比如系统通知权限,类似于notify这样的东西,在window系统上未右下角的小气泡
    "notifications"
  ]
}

page_action,browser_action类型的扩展对应位置

image

2.打包生成插件包

浏览器打开 chrome://extensions/(或者‘更过工具->扩展程序’),左上角有一个 打包扩展程序 按钮

image

然后,将生成的*.crx文件拖到浏览器即可完成安装。

到这步插件其实已经差不多了,当然,这里的功能都比较简单,你可以自己尝试一些更高级的功能

由于自己DIY的扩展是没有发布到web app store的,所以下次打开浏览器时会被禁掉,解决方法见第二篇文章《chrome插件开发简介(二)——如何添“加浏览器扩展白名单”》

SVG Sprite 使用简介

SVG简介

SVG即可缩放矢量图形 (Scalable Vector Graphics)的简称, 是一种用来描述二维矢量图形的XML标记语言. SVG图形不依赖于分辨率, 因此图形不会因为放大而显示出明显的锯齿边缘.

icon sprite

当我们需要使用多个icon的时候, 为了节省请求和方便管理, 通常会把icon合并到一个文件中, 在使用时再通过一定的方法从icon集合文件中取出所需的图形并显示. 目前使用得最多的应该就是我们所熟悉的CSS Sprite和Icon Font.

CSS Sprite

CSS Sprite的原理是将多个icon按一定规律整理到一个图片文件中, 使用时利用background-imagebackground-position将图片中特定部分显示出来. CSS Sprite技术已经被广泛应用了很长的一段时间, 目前有许多自动化生成Sprite图片和CSS文件的工具, 例如(gulp.spritesmith)[https://github.com/twolfson/gulp.spritesmith].

.icon1 {
    background-image: url(/res/icon1.png)
}
.icon1-increase {
    background-position: -10px -10px;
}
<i class="icon1 icon1-increase"/>

CSS Sprite技术成熟, 兼容性好, 但是缺点也比较明显. 如在实际需求中, 对应形状相同但颜色不同的icon, 就需要为不同颜色的icon各保存一份; 有时候需要对已有icon放大显示时, 发现锯齿严重, 那么又要再保存一份放大版的icon. 因此, Sprite文件会随着时间越变越大, 同时内容越来越乱, 逐渐变得难以管理.

Icon Font

Icon Font的基本原理是将Icon定义为图片字体, 在CSS中用@font-face引入Icon Font自定义字体, 再利用font-family和字符码显示出指定的图标.

@font-face {
    font-family: 'iconfont';
    src: url(/res/icon2.ttf) format('truetype');
}
.icon2 {
    font-family: 'iconfont';
}
<i class="icon2">&#33</i>

由于使用的是字体, 因此可以通过color, font-size设置icon的样式. Icon Font拥有比CSS Sprite图片更小的文件体积, 维护也比图片更方便, 但是icon font通常只能使用单一的颜色, 字体文件生成也比CSS Sprite更复杂.

SVG Sprite

通常在使用SVG的时候, 我们是直接写到svg标签当中:

    <svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 3 2">
        <rect width="1" height="2" x="0" fill="#008d46" />
        <rect width="1" height="2" x="1" fill="#ffffff" />
        <rect width="1" height="2" x="2" fill="#d2232c" />
    </svg>

此时SVG图形会直接在页面当中显示. SVG属性中, 可以利用(symbol)[https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/symbol]封装图形, 并利用(use)[https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/use]将其实例化, 从而实现SVG Sprite的功能.

SVG Sprite实例:

<svg style="height:0;width:0;display:none;" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <symbol id="icon-italy" width="150" height="100" viewBox="0 0 3 2">
        <rect width="1" height="2" x="0" fill="#008d46" />
        <rect width="1" height="2" x="1" fill="#ffffff" />
        <rect width="1" height="2" x="2" fill="#d2232c" />
    </symbol>

    <symbol id="icon-france" width="150" height="100" viewBox="0 0 3 2">
        <rect width="1" height="2" x="0" fill="#002496" />
        <rect width="1" height="2" x="1" fill="#ffffff" />
        <rect width="1" height="2" x="2" fill="#ee2839" />
    </symbol>
</svg>

<svg><use xlink:href="#icon-italy"/></svg>
<svg><use xlink:href="#icon-france"/></svg>

这样就实现了SVG Sprite.

原理

通过devtool可以观察到, use标签是利用shadow dom实现的.

use.shadowdom

它通过xlink:href这个XML的attribute引用指定的SVG symbol, 在渲染时指定symbol标签中的内容就会被渲染显示在页面当中. 这意味着, 如果无法直接对use标签中的shadow dom进行访问和修改. 例如像use#rect这样的选择器是无法生效的, 因此不能通过一般的css选择器对use中图形不同的部分进行控制.

CSS样式

更多的时候, 我们通常都只需要改变图标的大小和颜色, 在SVG Sprite当中, 实现起来并不复杂.

  1. 大小

通过改变svg容器的大小, 可以轻松地对图标大小进行控制.

.icon{
    width: 120px;
    height: 80px;
}
.icon.icon-small{
    width: 60px;
    height: 40px;
}
<svg class="icon"><use xlink:href="#icon-italy"/></svg>
<svg class="icon icon-small"><use xlink:href="#icon-italy"/></svg>

svg.icon.size

  1. 颜色
  • 单色

颜色由于不能直接对use的shadow root中的图形标签进行选择, 因此在为图标定义颜色时需要利用fill: inherit这个一属性.

svg path{
    fill: inherit;
}
.icon2-green{
    fill: #008d46;
}
.icon2-red{
    fill: #dc352f;
}
<svg class="icon2 icon2-green">
    <use xlink:href="#icon-increase"/>
</svg>
<svg class="icon2 icon2-red">
    <use xlink:href="#icon-increase"/>
</svg>

svg.icon.color

利用inherit, 还可以定义stroke-width, stroke等属性.

  • 多色

除了对图标整体颜色进行定义以外, 还可以根据需要对图形不同部分进行颜色定义. 这里使用到了css 的自定义属性(CSS Custom Properties).

这里需要对icon.svg进行修改, 将图形个部分的颜色设置为fill: var(--*[, default])的形式.

<symbol id="icon-flag" width="150" height="100" viewBox="0 0 3 2">
    <rect width="1" height="2" x="0" style="fill: var(--color0, #008d46)" />
    <rect width="1" height="2" x="1" style="fill: var(--color1, #fff)"/>
    <rect width="1" height="2" x="2" style="fill: var(--color2, #d2232c)"/>
</symbol>

定义css样式

.flag-belgium {
    --color0: #201b18;
    --color1: #f1ee3d;
    --color2: #dc352f;
}
<svg class="icon">
    <use xlink:href="#icon-flag"/>
</svg>
<svg class="icon flag-belgium">
    <use xlink:href="#icon-flag"/>
</svg>

svg.icon.color.parts

除此以外, 还有其他一些样式定义的形式, 更详细的内容可以阅读Styling SVG Content with CSS.

实际使用

引用外部svg

上文介绍的是使用内联svg, 但其实还可以使用外部svg的.

<svg class="icon">
    <use xlink:href="/res/svg/icon.svg#icon-flag"/>
</svg>

然而这种方法不兼容IE9~10, 但如果非要使用外部svg的形式, 可以引入一个pollyfillsvg4everybody, 当运行在不支持外部svg的情况下, 这个pollyfill会通过异步请求加载svg委文件并将其注入到页面当中, 将引用方法转换成内联svg.

在ftl中使用

可以在svg.ftl中定义一系列macro, 用以加载.svg文件.

<#macro svgicon path>
    <#include "${path}">
</#macro>

<#macro svgicon3 >
    <@svgicon "./icon3.svg"/>
</#macro>

在页面中引用demo.ftl:

<@svgicon1/>
<svg>
    <use xlink:href="#icon-italy"/>
</svg>

在regular中使用

xlink:href是使用了命名空间的XML特性, 如果是写在.html页面的标签, 该特性能够正常被浏览器解析并完成svg渲染. 如果该svg变量是通过DOM API创建出来的话, 则需要使用特定的方法进行处理(SVG with USE tag not rendering).

即需要利用createElementNSsetAttributeNS方法在创建的同时声明命名空间.

var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#icon-increase');
document.querySelector('#svgid').appendChild(use);

只要是需要动态创建use元素, 都需要使用上面这种方法才能使SVG Sprite中的元素实例化. 但在目前的Regular(0.4.3)中, 不会对命名空间进行处理, 这意味着, 如果直接将<svg><use xlink:href/></svg>写在Regular组件的模版中时, 该标签是无法正常渲染的. 为此可以增加一个指令r-xlink:href以完成手动设置命名空间的操作.

Regular.directive('r-xlink:href', function (elem, val) {
    if (val&& val.type === 'expression') {
        this.$watch(val, function (newVal) {
            elem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', newVal);
        });
    } else {
        elem.setAttributeNS('http://www.w3.org/1999/xlink', 'href', val);
    }
});

那么在组件模版中就可以像在普通.html中那样使用SVG Sprite了. 当然, 首先得确保SVG Sprite被写到页面中.

<svg>
    <use r-xlink:href="#icon-italy"/>
</svg>

在Regular的SVG 实践中得到了郑海波大神的指点, 否则得绕更大的路才能把问题解决, 在此表示感谢.

小结

对比前文提到的CSS Sprite和Icon Font, SVG有着明显的优势:

  • 放大缩小不会失真
  • 大小, 颜色等属性自定义灵活
  • 体积小, 同时管理方便

虽然SVG Sprite有着高度的灵活性, 但于此同时, SVG兼容性有待考究, 同时其渲染性能也不及图片和字体那么高, 可能在某些情况下不适用. 不过在一般的场景中, svg sprite还能够给开发带来很大的便利的.

参考

  1. SVG元素参考
  2. SVG with USE tag not rendering
  3. 未来必热:SVG Sprite技术介绍
  4. Styling SVG Content with CSS
  5. Icon System with SVG Sprites

让我们来谈谈人生:关于技术人员能力的瞎想

技术人总有一个很完美的假想,开发能力够强,就是真正能力强。

但现实并不是这么完美,人的精力有限,能真正投入的事情不多,况且工作中还有一大堆业务需求要展开。

**的火花不时能迸发出来,但是真正执行下去的有多少? 很多时候我们发现了能提高转化率,提高用户体验的点子,但是我们忙于业务需求开发,对于这类不急、没有需求方,没有人来催着上线,但是却暗藏金子的点子,最终都在忙忙碌碌中忽略了;

业务需求重要,但是提高转化率、提高效率的点子同样也重要,关键看我们技术人员如何去衡量;
技术人员的考核里面虽然没有转化率这项指标,而是完成了多少需求。完成产品提过来的需求,技术扮演的是支持部门的角色,就算完美收工,功劳也不算在技术部门,这只是技术部门完成了自己的本职工作。
而如果技术部门投入一部分时间去完成自己认为重要的事情,比如主导完成提高转化率的优化并取得好的效果,这个就体现出技术部门直接的价值来了;

从大的点来说,要求技术部门能与公司有着共同的目标,去推动公司蓬勃发展。有了共同的目标,技术人员才能真正主动去思考哪些东西是重要的,哪些事可以暂缓的;从而将时间都花在正确的方向上,只有这样,技术人员才能逐渐直接体现其价值,否则,技术会慢慢沦为运营、产品部门的开发工具。

现阶段大部分技术人员的感受是公司的销售数据的提高与否,与技术人员没有直接关联,只要按部就班完成新功能就好,但如果想成为一名杰出的员工,这是远远不够的。

沟通,技术人员更擅长于机器打交道,而不擅长去提出并推动完成自己的主张、想法; 完成业务开发,被动的沟通也能完成。而要进行新工具开发,解决流程问题等创新工作,更加考验个人的沟通能力,一个新的工具,我们需要考虑大家是否有这样的需求,其次要推广给大家使用,还需要接收不同人的反馈来不断完善,以让大家都接受。

所以,能力是最基本的要求,还要由足够的主动性,想法设法去推动事情往前走,最终达到自己期望的结果; 这个过程,正是权衡主次,表达想法,并让他人、上级领导理解并接受,持之以恒推进的过程。简单的来说,就是 技术能力+主动+执行力

如今国内互联网风口一出,四面八方蜂拥而上的形势,点子已经不重要,重要的是谁的执行力强,做的更好,笑到最后;

by 渔樵

从 regular 的角度看 vue


title: 从 regular 的角度看 vue
date: 2017-05-05

vue + vuex + vue-router 一些使用方式文档都已经介绍得非常详细了。
https://cn.vuejs.org/v2/guide
http://router.vuejs.org/zh-cn/essentials/getting-started.html
https://vuex.vuejs.org/zh-cn/
下面说一点关于 vue 和 regular 的相同点和不同点

  1. vue 的插值和 regular 的插值语法类似,但是 vue 的绑定一次是 v-once 而且这个指令会直接影响整个节点上的数据。
    事件和条件渲染、列表渲染的语法都不太一样,没有 regular 的语法那么自由,在属性上的绑定用的是指令 v-bind: 而且字段用“”包起来,比如<div v-bind:id="dynamicId" v-bind:href="dynamicId"></div>

个人感觉这里这么做是因为 regular 和 vue 对模板的渲染过程是不一样的,这个后面再说。对于插值 vue 仅仅只能处理文本,我猜这个插值最后也是转换成了指令来做的,并没有类似 regular 中的 expression 对象。
对于条件渲染 vue 的语法是 v-if,比如<p v-if="seen">Now you see me</p>表达式的真假直接控制整个节点(而且必须控制整个节点)的插入和移除,这么做的话如果想要实现 regular 里面的<p>Now you see {#if seen} me {/if}</p>就得给 me 这个文本包一层节点, 因为 vue 对于 model => view 的过程是把 template string 先解析成 dom 树, 然后解析各种组件和指令(template里面就只剩指令了),指令都是和 dom 树里面的某个节点绑定的。
对于列表渲染也是的

<ul id="example-1">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

是对整个节点的重复,对于数组来说 v-for="(index, item) in items" index 就相当于 regular 里面的 item_index,对于对象来说 v-for="(value, key, index) in object"
,对于 vue 里面的过滤器,只能用在插值和 v-bind 的指令中。
对于事件处理器,不同于 regular 里面的 on-xxx={this.huidiao($event)}, vue 里面用的是指令 v-on:xxx="huidiao($event)"
2. vue 中双向绑定的实现和 regular 中的脏值检测不一样,是用 definePorperty 重写了 getter/setter函数来实现的,并没有像 regular 中的 $update 一样的全局解药,所以在 vue 中所有的修改必须让 vue 知道。对于一些数组和对象的操作,有一些 vue 是检测不到的,

1. 当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
2. 当你修改数组的长度时,例如: vm.items.length = newLength

所以得用 vue 暴露出来的方法来修改数据。为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果, 同时也将触发状态更新:

Vue.set(example1.items, indexOfItem, newValue)
example1.items.splice(indexOfItem, 1, newValue)

为了解决第二类问题,你也同样可以使用 splice:

example1.items.splice(newLength)

对比一些思路:
regular 的 compile 过程是把 template string 整个的去编译,每当碰到插值,或者指令里面的数据时会生成一个 expression 对象,并生成一个 watcher 推到脏值检测的队列里面,每次进入 digest 过程是就逐个的去检测。
vue 的 compile 过程是将 template string 编译成一棵 dom 树,然后去匹配所有的指令,将指令和对应的 dom 节点关联起来,每一次匹配到表达式的时候也会生成一个 watcher ,在 vue 的 data 函数阶段会用 definePorperty 将每个数据的 getter/setter 重写。这里用订阅者模式,当该字段赋值之后 set 函数触发,发出消息,节点的 watcher 是订阅者,收到消息之后执行对应的操作。
3. regular 的 config 方法在 vue 中可以用 data 方法来替代
4. regular 中的 init 基本上和 vue 中的 beforeMount 方法一样
5. vue 中组件接受参数需要显示的声明 props ,这个字段的值是一个数组,包含所有接收的值以及值的一些信息(非必填),比如

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})
  1. vue 中所用到的字段都必须在 data 函数中显示的声明,如果在模板中使用了没有被定义的数据 vue 会有警告
  2. vue 中一个组件用到另外一个组件也得显示的声明一个 components ,包含用到的所有组件,比如
components: {
  'component-name': Component,
}
  1. 在 regular 中只要声明了组件的 name 属性,那么这个组件就自动被注册到全局去了,但是在 vue 里面是没有这么一个属性的,如果想要注册到全局只能Vue.component('my-component', {})
  2. vue 的方法都放在 methods 里面,不过调用的时候都是 this.xxx
  3. 在 vue 中数据不是 this.data.xxx 而是直接 this.xxx
  4. 在给内嵌组件传递参数的时候,这个时候参数是一个属性,应该用 v-bind:message="messgae"
  5. vue 在组件之前的数据默认是单向的,但是如果传入的是一个对象那么子组件修改这个对象会影响到父组件。传入的参数在子组件内部是不推荐修改的,如果要改的话最好是在子组件内部定义另外一个变量,用计算属性或者直接定义一个一样的变量去代替他。如果子组件想要把修改的数据传回去,就得自定义事件去做,自定义事件和 regular 里面的自定义事件类似,比如
<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>

Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  },
  beforeMount() {
    var vm = new Counter();
    vm.$on('increment', this.incrementTotal);
  },
})
  1. 对于组件之间的通信,如果是非父子组件会有点麻烦,这个时候可以进入一个中间组件专门用来通信使用,regular 中也适用
var bus = new Vue()


// 触发组件 A 中的事件
bus.$emit('id-selected', 1)


// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

前端工程-从原理到轮子之JS模块化

目前,一个典型的前端项目技术框架的选型主要包括以下三个方面:

  1. JS模块化框架。(Require/Sea/ES6 Module/NEJ)
  2. 前端模板框架。(React/Vue/Regular)
  3. 状态管理框架。(Flux/Redux)
    系列文章将从上面三个方面来介绍相关原理,并且尝试自己造一个简单的轮子。

本篇介绍的是JS模块化
JS模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的**分为CommonJS、AMD和CMD。有关三者的区别,大家基本都多少有所了解,而且资料很多,这里就不再赘述。

模块化的核心**:

  1. 拆分。将js代码按功能逻辑拆分成多个可复用的js代码文件(模块)。
  2. 加载。如何将模块进行加载执行和输出。
  3. 注入。能够将一个js模块的输出注入到另一个js模块中。
  4. 依赖管理。前端工程模块数量众多,需要来管理模块之间的依赖关系。

根据上面的核心**,可以看出要设计一个模块化工具框架的关键问题有两个:一个是如何将一个模块执行并可以将结果输出注入到另一个模块中;另一个是,在大型项目中模块之间的依赖关系很复杂,如何使模块按正确的依赖顺序进行注入,这就是依赖管理。

下面以具体的例子来实现一个简单的基于浏览器端AMD模块化框架(类似NEJ),对外暴露一个define函数,在回调函数中注入依赖,并返回模块输出。要实现的如下面代码所示。

define([
    '/lib/util.js', //绝对路径
    './modal/modal.js', //相对路径
    './modal/modal.html',//文本文件
], function(Util, Modal, tpl) {
	/*
	* 模块逻辑
	*/
	return Module;
})

1. 模块如何加载和执行

先不考虑一个模块的依赖如何处理。假设一个模块的依赖已经注入,那么如何加载和执行该模块,并输出呢?
在浏览器端,我们可以借助浏览器的script标签来实现JS模块文件的引入和执行,对于文本模块文件则可以直接利用ajax请求实现。
具体步骤如下:

  • 第一步,获取模块文件的绝对路径
    要在浏览器内加载文件,首先要获得对应模块文件的完整网络绝对地址。由于a标签的href属性总是会返回绝对路径,也就是说它具有把相对路径转成绝对路径的能力,所以这里可以利用该特性来获取模块的绝对网络路径。需要指出的是,对于使用相对路径的依赖模块文件,还需要递归先获取当前模块的网络绝对地址,然后和相对路径拼接成完整的绝对地址。代码如下:
var a = document.createElement('a');
a.id = '_defineAbsoluteUrl_';
a.style.display = 'none';
document.body.appendChild(a);

function getModuleAbsoluteUrl(path) {
    a.href = path;
    return a.href;
}

function parseAbsoluteUrl(url, parentDir) {
    var relativePrefix = '.',
        parentPrefix = '..',
        result;
    if (parentDir && url.indexOf(relativePrefix) === 0) {
        // 以'./'开头的相对路径
        return getModuleAbsoluteUrl(parentDir.replace(/[^\/]*$/, '') + url);
    }
    if (parentDir && url.indexOf(parentPrefix) === 0) {
        // 以'../'开头的相对路径
        return getModuleAbsoluteUrl(parentDir.replace(/[\/]*$/, '').replace(/[\/]$/, '').replace(/[^\/]*$/, '') + url);
    }
    return getModuleAbsoluteUrl(url);
}
  • 第二步,加载和执行模块文件

对于JS文件,利用script标签实现。代码如下:

var head = document.getElementsByTagName('head')[0] || document.body;

function loadJsModule(url) {
    var script = document.createElement('script');
    script.charset = 'utf-8';
    script.type = 'text/javascript';
    script.onload = script.onreadystatechange = function() {
        if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
            /*
             * 加载逻辑, callback为define的回调函数, args为所有依赖模块的数组
             * callback.apply(window, args);
             */
            script.onload = script.onreadystatechange = null;
        }  
    };
}

对于文本文件,直接用ajax实现。代码如下:

var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),
      textContent = '';
	  
xhr.onreadystatechange = function(){
    var DONE = 4, OK = 200;
    if(xhr.readyState === DONE){
        if(xhr.status === OK){
             textContent = xhr.responseText; // 返回的文本文件
        } else{
            console.log("Error: "+ xhr.status); // 加载失败
        }
    }
}

xhr.open('GET', url, true);// url为文本文件的绝对路径
xhr.send(null);

2. 模块依赖管理

一个模块的加载过程如下图所示。

  • 状态管理
    从上面可以看出,一个模块的加载可能存在以下几种可能的状态。
  1. 加载(load)状态,包括未加载(preload)状态、加载(loading)状态和加载完毕(loaded)状态。
  2. 正在加载依赖(pending)状态。
  3. 模块回调完成(finished)状态。
    因此,需要为每个加载的模块加上状态标志(status),来识别目前模块的状态。
  • 依赖分析
    在模块加载后,我们需要解析出每个模块的绝对路径(path)、依赖模块(deps)和回调函数(callback),然后也放在模块信息中。模块对象管理逻辑的数据模型如下所示。
{
	path: 'http://asdas/asda/a.js',
	deps: [{}, {}, {}],
	callback: function(){ },
	status: 'pending'
}
  • 依赖循环
    模块很可能出现循环依赖的情况。也就是a模块和b模块相互依赖。依赖分为强依赖弱依赖强依赖是指,在模块回调执行时就会使用到的依赖;反之,就是弱依赖。对于强依赖,会造成死锁,这种情况是无法解决的。但弱依赖可以通过现将一个空的模块引用注入让一个模块先执行,等依赖模块执行完后,再替换掉就可以了。强依赖弱依赖的例子如下:
//强依赖的例子
//A模块
define(['b.js'], function(B) {
  // 回调执行时需要直接用到依赖模块
   B.demo = 1;
   // 其他逻辑
});
//B模块
define(['a.js'], function(A) {
   // 回调执行时需要直接用到依赖模块
   A.demo = 1;
   // 其他逻辑
});
// 弱依赖的例子
// A模块
define(['b.js'], function(B) {
    // 回调执行时不会直接执行依赖模块
    function test() {
        B.demo = 1;
    }
    return {testFunc: test}
});
//B模块
define(['a.js'], function(A) {
    // 回调执行时不会直接执行依赖模块
    function test() {
        A.demo = 1;
    }
    return {testFunc: test}
});

3. 对外暴露define方法

对于define函数,需要遍历所有的未处理js脚本(包括内联外联),然后执行模块的加载。这里对于内联外联脚本中的define,要做分别处理。主要原因有两点:

  1. 内敛脚本不需要加载操作。
  2. 内敛脚本中define的模块的回调输出是不能作为其他模块的依赖的。
var handledScriptList = [];
window.define = function(deps, callback) {
    var scripts = document.getElementsByTagName('script'),
        defineReg = /s*define\s*\(\[.*\]\s*\,\s*function\s*\(.*\)\s*\{/,
        script;

    for (var i = scripts.length - 1; i >= 0; i--) {
        script = list[i];
        if (handledScriptList.indexOf(script.src) < 0) {
            handledScriptList.push(script.src);
            if (script.innerHTML.search(defineReg) >= 0) {
                // 内敛脚本直接进行模块依赖检查。
            } else {
                // 外联脚本的首先要监听脚本加载
            }
        }
    }
};

上面就是对实现一个模块化工具所涉及核心问题的描述。完整的代码点我

意外发现regular v0.4.3的一个bug,它竟然这么干的。

哈哈,标题很有吸引力吧。

言归正传,前几天在页面搭建工程下进行开发时,碰到了一个很困惑的问题,很容易复现,也很容易理解,就直接上代码吧:

let Suggest = Regular.extend({
    template: '<div>标题:{schemeData.title}</div><div>Regular版本:{version}</div>',
    config(data){
        data.schemeData = Object.assign({}, {
            title: '默认标题'
        }, data.schemeData);
        // 这里很奇怪:强制修改标题,regular v0.4.3的表现和v0.5.2不一致
        data.schemeData.title = '正常标题';
        this.supr(data);
    }
});

let Page = Regular.extend({
    template: '<suggest schemeData={schemeData} version={version}></suggest>',
    config(data){
        data.version = '0.4.3';
        data.schemeData = {
            title: '不正常标题'
        };
        this.supr(data);
    }
}).component('suggest', Suggest);

let page = new Page({data: {}});
page.$inject('#app');

这段代码在regular v0.4.3下,输出的结果是:

<div>标题:不正常标题</div>
<div>Regular版本:0.4.3</div>

请注意这里输出的标题是: "不正常标题"

可以看到,我在子组件的config方法中有强制覆盖父组件传进来的title:

// 这里很奇怪:强制修改标题,regular v0.4.3的表现和v0.5.2不一致
data.schemeData.title = '正常标题';

但是很明显,没有起到我想要的效果。

0.4.3对应的demo可以在这里查看: https://codepen.io/lidong639/pen/wddQzM

同样的代码在0.5.2下却可以按照期望输出想要的结果:https://codepen.io/lidong639/pen/QvvVpB

起初,我以为是使用Object.assign方法引起的,因为Object.assign方法会返回一个新的对象的引用,这样就和父组件传进来的对象的引用断开了,并且,可以使用工程封装的extend方法进行验证:

_.extend(data.schemeData, {
    title: '默认标题'
});

将Object.assign改成_.extend后,输出的结果符合预期:https://codepen.io/lidong639/pen/NjjEgb

但是,同样的代码在 v0.4.3 和 v0.5.2 下表现不一致,可以从侧面印证这是regular的一个bug,并且 v0.4.3 以上的版本都修复了这个问题。

暂且这样下结论吧,是否0.4.3的本意就是如此设计,还没来得及仔细分析,如果有知道内情的同学,请发表下您的看法吧!

vuejs学习之路

最近在做的种草项目,其中的管理后台采用了vue.js。为什么会选用vue呢,一是因为一个管理后台,不需要考虑兼容性、seo这些前台系统需要关心的点,后台系统的交互形式又特别适合做成一个单页应用,而vue不兼容IE8及以下版本,vue的route库很好的支持单页应用,适合我们的项目;再者,当然是因为vue是目前前端圈子很火很fashion的技术框架呀,截至目前有接近7k的fork,开发者们的参与热情堪称空前,技术社区氛围也很好,持续的版本更新,是一款被各大小项目考验过的稳定框架,岂有不用之理?

在此之前,我也没有过vue的项目实践经验,当初自己任性的敲定用vue的时候,内心还是很忐忑的,毕竟实际的开发时间也就一周。不过,学习一个新的框架或技术最好的方式,还是要运用到实际中,去学去写,才有更深的认识和体验。在项目时间很紧迫的情况下,会逼着自己快速的学习一个新框架,当然也因为要保证提测时间而没有做过的思考设计,在使用和理解这个框架的时候,也会遇到一些问题。

下面记录下,我在本项目中使用vuejs的过程。

体验vue

在确认选用vue之前,我没有完整的看过教程或api文档,只是先去使用了他的命令行工具,本地跑起来一个demo工程,来揭开vue神秘的面纱,直观的初步认识下这个框架。简单体验完之后,我就决定用vue了,原因有三:

  • 使用命令行工具,可以快速搭建一个单页应用,这正是我所想要的,只需要在初始化你的项目时选择Install vue-router就可以了;命令行工具生成的工程目录结构,也是符合前端开发习惯的,如图1。
  • vue提供了一套完整的前端项目构建方式,包括不同生产环境的webpack打包配置,还可以配置eslint、unit test等可选项,可以说是省去了我们很多的前期准备和后顾之忧。
  • 在生产环境下,可以启动一个带热重载、保存时静态检查的服务器,做到前后端完全分离开发。

总之,使用vue可以构建一个目前比较主流的现代化的前端开发环境。

image 图1

开起我的vue工程

在体验过vue的简单实例之后,我依然没有立马去通读开发教程,而是在自己的工程里,用命令行搭建我的webapp。此间,参照了成功实践过vue的数据营销系统,借用了其mock数据的配置方式,了解他们系统中单页应用中模块与路由的配置,知道了vuex及其中的state、action、mutation等概念,老实说,一下子接触到这许多内容,非常凌乱,想来只有官方教程才能解惑了,但我并不急着去读文档,因为我想结合工程带着疑问再去看文档,先把页面框架搭起来。

单从工程目录中,基本能知道index.html是单页应用的承载页,main.js是页面入口脚本。在main.js里初始化了一个vue实例:

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 }
})

其中传入了router配置以及App.vue模板,

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view></router-view>
  </div>
</template>

其中有一个<router-view>组件,用来渲染路径匹配到的视图组件。首先,开始编写我的页面模板。常规的后台有顶栏、侧边栏和主内容区,其中主内容区是需要根据不同的路由切换的,可以用<router-view>组件,页面模板如下:

<template>
  <div id="app">
    <header></header>
    <div class="g-body" :class="{noHasSideNav: noHasSideNav}">
      <side-nav @toggle="toggleNav"></side-nav>
      <router-view></router-view>
      <span class="u-totop" @click="goToTop">Top</span>
    </div>
  </div>
</template>

其中headerside-nav分别是顶栏和侧边导航栏组件。再配上对应的路由,基本搭好了一个单页应用的框架。
接下来,带着工程中已经接触的概念和疑问,去看官方教程。

学习官方教程

vue的教程很完整,由浅入深。集中花半天一天的时间,能基本看完。但在工作中抽出这么完整的时间有难度,而且一口气看完容易精神疲劳。可以选择重点的章节重点看,比如组件章节,其他章节快速翻阅,等用到的时候再来搜查细看。另外vue-router、vueX这些也需要花时间去看文档,理解设计意图,再结合实践到项目中去。

组件与功能开发

通过教程对vue有了更多了解以后,可以边学边试着开发主要功能,并编写通用组件。
开始我的第一个组件,状态Tab切换:

<template>
  <div class="m-statustabs">
    <ul class="tablst f-cb">
      <li :class="['tabitm',activeStatus==lb.status?'is-active':'']" v-for="lb in labelList" @click="tabclick(lb.status, $event)">{{lb.label}}</li>
    </ul>
  </div>
</template>

<script>
  import { mapMutations, mapGetters, mapState } from 'vuex'
  export default {
    name: 'status-tab',
    props: {
      labelList: Array
    },
    data () {
      return {
        activeStatus: this.labelList[0]&&this.labelList[0].status
      }
    },
    methods: {
      tabclick (val,evt) {
        if(val===this.activeStatus)
          return
        this.activeStatus = val
        this.$emit('tabchange',this.activeStatus)
      }
    }
  }
</script>

很简单的一个tab切换功能,需要设置传入的参数props,再绑定点击事件,事件中处理状态值,并对外emit事件,在父组件中使用此组件:

<status-tab :labelList="statusList" @tabchange="onTabChange"></status-tab>

之后就是具体功能的开发以及其他组件的设计实现。期间也遇到过一些问题,先网络查找定位,自己解决不了的,询问同事,在此特别感谢鸡苗同学,在我使用vuejs的过程中,给予的指导与启发,比如在使用v-for循环输入列表后,删除数组项没有更新到视图的问题,后与鸡苗一起讨论定位,给列表设置key属性后能正常删除,后自己定位到是因为组件内部用了v-once指令,导致视图不再更新,此指令要慎用,能确保节点不需要更新的,才加此指令。

总结

截至目前,种草后台1期的功能已开发联调完成。但我对vue只是到了一个会使用基本功能的阶段,之前教程看得粗浅,有些项目中没涉及到的功能并没有去细看,还有此框架的设计思路及内部实现,都还没有去深入了解,这都是后续需要继续进行的事。

学习BFC


title: 学习BFC
date: 2017-05-22

什么是BFC

BFC全称是Block Formatting Context,即块格式化上下文。它是CSS2.1规范定义的,关于CSS渲染定位的一个概念。要明白BFC到底是什么,首先来看看什么是视觉格式化模型。

视觉格式化模型

视觉格式化模型(visual formatting model)是用来处理文档并将它显示在视觉媒体上的机制,它也是CSS中的一个概念。

视觉格式化模型定义了盒(Box)的生成,盒主要包括了块盒、行内盒、匿名盒(没有名字不能被选择器选中的盒)以及一些实验性的盒(未来可能添加到规范中)。盒的类型由display属性决定。

块盒(block box)

块盒有以下特性:

  • 当元素的CSS属性displayblocklist-itemtable时,它是块级元素 block-level;
  • 视觉上呈现为块,竖直排列;
  • 块级盒参与(块格式化上下文);
  • 每个块级元素至少生成一个块级盒,称为主要块级盒(principal block-level box)。一些元素,比如<li>,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。

行内盒(inline box)

  • 当元素的CSS属性display的计算值为inlineinline-blockinline-table时,称它为行内级元素;
  • 视觉上它将内容与其它行内级元素排列为多行;典型的如段落内容,有文本(可以有多种格式譬如着重),或图片,都是行内级元素;
  • 行内级元素生成行内级盒(inline-level boxes),参与行内格式化上下文(inline formatting context)。同时参与生成行内格式化上下文的行内级盒称为行内盒(inline boxes)。所有display:inline的非替换元素生成的盒是行内盒;
  • 不参与生成行内格式化上下文的行内级盒称为原子行内级盒(atomic inline-level boxes)。这些盒由可替换行内元素,或 display 值为 inline-blockinline-table 的元素生成,不能拆分成多个盒;

匿名盒(anonymous box)

匿名盒也有份匿名块盒与匿名行内盒,因为匿名盒没有名字,不能利用选择器来选择它们,所以它们的所有属性都为inherit或初始默认值;

如下面例子,会创键匿名块盒来包含毗邻的行内级盒:

<div>
	Some inline text
	<p>followed by a paragraph</p>
	followed by more inline text.
</div>

三个定位方案

在定位的时候,浏览器就会根据元素的盒类型和上下文对这些元素进行定位,可以说盒就是定位的基本单位。定位时,有三种定位方案,分别是常规流,浮动已经绝对定位。

常规流(Normal flow)

  • 在常规流中,盒一个接着一个排列;
  • 块级格式化上下文里面, 它们竖着排列;
  • 行内格式化上下文里面, 它们横着排列;
  • positionstaticrelative,并且floatnone时会触发常规流;
  • 对于静态定位(static positioning),position: static盒的位置是常规流布局里的位置
  • 对于相对定位(relative positioning),position: relative,盒偏移位置由这些属性定义topbottomleftandright即使有偏移,仍然保留原有的位置,其它常规流不能占用这个位置。

浮动(Floats)

  • 盒称为浮动盒(floating boxes);
  • 它位于当前行的开头或末尾;
  • 导致常规流环绕在它的周边,除非设置 clear 属性;

绝对定位(Absolute positioning)

  • 绝对定位方案,盒从常规流中被移除,不影响常规流的布局;
  • 它的定位相对于它的包含块,相关CSS属性:topbottomleftright
  • 如果元素的属性positionabsolutefixed,它是绝对定位元素;
  • 对于position: absolute,元素定位将相对于最近的一个relativefixedabsolute的父元素,如果没有则相对于body

块格式化上下文

到这里,已经对CSS的定位有一定的了解了,从上面的信息中也可以得知,块格式上下文是页面CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域

BFC的创建方法

  • 根元素或其它包含它的元素;
  • 浮动 (元素的float不为none);
  • 绝对定位元素 (元素的positionabsolutefixed);
  • 行内块inline-blocks(元素的 display: inline-block);
  • 表格单元格(元素的display: table-cell,HTML表格单元格默认属性);
  • overflow的值不为visible的元素;
  • 弹性盒 flex boxes (元素的display: flexinline-flex);

但其中,最常见的就是overflow:hiddenfloat:left/rightposition:absolute。也就是说,每次看到这些属性的时候,就代表了该元素以及创建了一个BFC了。

BFC的范围

BFC的范围在MDN中是这样描述的。

A block formatting context contains everything inside of the element creating it that is not also inside a descendant element that creates a new block formatting context.

中文的意思一个BFC包含创建该上下文元素的所有子元素,但不包括创建了新BFC的子元素的内部元素。

这段看上去有点奇怪,我是这么理解的,加入有下面代码,class名为.BFC代表创建了新的块格式化:

<div id='div_1' class='BFC'>
	<div id='div_2'>
		<div id='div_3'></div>
		<div id='div_4'></div>
	</div>
	<div id='div_5' class='BFC'>
		<div id='div_6'></div>
		<div id='div_7'></div>
	</div>
</div>

这段代码表示,#div_1创建了一个块格式上下文,这个上下文包括了#div_2#div_3#div_4#div_5。即#div_2中的子元素也属于#div_1所创建的BFC。但由于#div_5创建了新的BFC,所以#div_6#div_7就被排除在外层的BFC之外。

我认为,这从另一方角度说明,一个元素不能同时存在于两个BFC中

BFC的一个最重要的效果是,让处于BFC内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。这是利用BFC清除浮动所利用的特性,关于清除浮动将在后面讲述。

如果一个元素能够同时处于两个BFC中,那么就意味着这个元素能与两个BFC中的元素发生作用,就违反了BFC的隔离作用,所以这个假设就不成立了。

BFC的效果

就如刚才提到的,BFC的最显著的效果就是建立一个隔离的空间,断绝空间内外元素间相互的作用。然而,BFC还有更多的特性:

Floats, absolutely positioned elements, block containers (such as inline-blocks, table-cells, and table-captions) that are not block boxes, and block boxes with 'overflow' other than 'visible' (except when that value has been propagated to the viewport) establish new block formatting contexts for their contents.

In a block formatting context, boxes are laid out one after the other, vertically, beginning at the top of a containing block. The vertical distance between two sibling boxes is determined by the 'margin' properties. Vertical margins between adjacent block-level boxes in a block formatting context collapse.

In a block formatting context, each box's left outer edge touches the left edge of the containing block (for right-to-left formatting, right edges touch). This is true even in the presence of floats (although a box's line boxes may shrink due to the floats), unless the box establishes a new block formatting context (in which case the box itself may become narrower due to the floats).

简单归纳一下:

  1. 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流);
  2. 处于同一个BFC中的元素相互影响,可能会发生margin collapse;
  3. 每个元素的margin box的左边,与容器块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此;
  4. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然;
  5. 计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算;
  6. 浮动盒区域不叠加到BFC上;

这么多性质有点难以理解,但可以作如下推理来帮助理解:html的根元素就是<html>,而根元素会创建一个BFC,创建一个新的BFC时就相当于在这个元素内部创建一个新的<html>,子元素的定位就如同在一个新<html>页面中那样,而这个新旧html页面之间时不会相互影响的。

上述这个理解并不是最准确的理解,甚至是将因果倒置了(因为html是根元素,因此才会有BFC的特性,而不是BFC有html的特性),但这样的推理可以帮助理解BFC这个概念。

从实际代码来分析BFC

讲了这么多,还是比较难理解,所以下面通过一些例子来加深对BFC的认识。

实例一

<style>
	* {
		margin: 0;
		padding: 0;
	}
	.left{
		background: #73DE80;	/* 绿色 */
		opacity: 0.5;
		border: 3px solid #F31264;
		width: 200px;
		height: 200px;
		float: left;
	}
	.right{						/* 粉色 */
		background: #EF5BE2;
		opacity: 0.5;
		border: 3px solid #F31264;
		width:400px;
		min-height: 100px;
	}
	.box{
		background:#888;
		height: 100%;
		margin-left: 50px;
	}
</style>
<div class='box'>
	<div class='left'> </div>
	<div class='right'> </div>
</div>

显示效果:

绿色框('#left')向左浮动,它创建了一个新BFC,但暂时不讨论它所创建的BFC。由于绿色框浮动了,它脱离了原本normal flow的位置,因此,粉色框('#right')就被定位到灰色父元素的左上角(特性3:元素左边与容器左边相接触),与浮动绿色框发生了重叠。

同时,由于灰色框('#box')并没有创建BFC,因此在计算高度的时候,并没有考虑绿色框的区域(特性6:浮动区域不叠加到BFC区域上),发生了高度坍塌,这也是常见问题之一。

实例二

现在通过设置overflow:hidden来创建BFC,再看看效果如何。

.BFC{
	overflow: hidden;
}

<div class='box BFC'>
	<div class='left'> </div>
	<div class='right'> </div>
</div>

灰色框创建了一个新的BFC后,高度发生了变化,计算高度时它将绿色框区域也考虑进去了(特性5:计算BFC的高度时,浮动元素也参与计算);

而绿色框和红色框的显示效果仍然没有任何变化。

实例三

现在,现将一些小块添加到粉色框中,看看效果:

<style>
	.little{
		background: #fff;
		width: 50px;
		height: 50px;
		margin: 10px;
		float: left;
	}
</style>

<div class='box BFC'>
	<div class='left'> </div>
	<div class='right'>
		<div class='little'></div>
		<div class='little'></div>
		<div class='little'></div>
	</div>
</div>

由于粉色框没有创建新的BFC,因此粉色框中白色块受到了绿色框的影响,被挤到了右边去了。先不管这个,看看白色块的margin。

实例四

利用同实例二中一样的方法,为粉色框创建BFC:

<div class='box BFC'>
	<div class='left'> </div>
	<div class='right BFC'>
		<div class='little'></div>
		<div class='little'></div>
		<div class='little'></div>
	</div>
</div>

一旦粉色框创建了新的BFC以后,粉色框就不与绿色浮动框发生重叠了,同时内部的白色块处于隔离的空间(特性4:BFC就是页面上的一个隔离的独立容器),白色块也不会受到绿色浮动框的挤压。

总结

以上就是BFC的分析,BFC的概念比较抽象,但通过实例分析应该能够更好地理解BFC。在实际中,利用BFC可以闭合浮动(实例二),防止与浮动元素重叠(实例四)。同时,由于BFC的隔离作用,可以利用BFC包含一个元素,防止这个元素与BFC外的元素发生margin collapse。

参考

视觉格式化模型_MDN

块格式化上下文_MDN

CSS之BFC详解

W3C block-formatting

从 r-model 来看 regularjs

从 r-model 来分析 regularjs

大家都知道 r-model 是一个指令,关于指令的语法可以看源码中关于 r-model 的部分

Regular.directive("r-model", {
  param: ['throttle', 'lazy'],
  link: function( elem, value, name, extra ){
    var tag = elem.tagName.toLowerCase();                                                  // 标签名
    var sign = tag;                                                                 
    if(sign === "input") sign = elem.type || "text";                                       // 
    else if(sign === "textarea") sign = "text";
    if(typeof value === "string") value = this.$expression(value);

    if( modelHandlers[sign] ) return modelHandlers[sign].call(this, elem, value, extra);
    else if(tag === "input"){
      return modelHandlers.text.call(this, elem, value, extra);
    }
  }
})

关键的是 link 函数,elem 是绑定这个指令的 dom 节点, value 是指令后面的值,如果这个值是字符串那么 value 也是字符串,如果这个值是插值,那个这个 value 就是一个 expression 对象,这个对象是在 parse 过程中产生的, regular 在 parse 的时候每次解析到一个插值就会生成一个 expression 对象。
expression 对象长这样,包含一个 get 方法和 set 方法

expression {
	"type": "expression",
	"touched": "true",
	"once": "false",
	"get": function anonymous(c, e) {
		var d = c.data;e = e||'';return (c._sg_('testWatch', d, e))
	},
	"set": function (ctx, value, ext) {
		expr.set = new Function(_.ctxName, _.setName, _.extName, _.prefix + setbody)
		return expr.set(ctx, value, ext);
	}
}

分析 r-model 源码,主要是获取绑定 r-model 的 dom 节点,然后匹配到对应的方法上,这个 map 长这样

var modelHandlers = {
  "text": initText,
  "select": initSelect,
  "checkbox": initCheckBox,
  "radio": initRadio
}

如果这个 value 的值是一个字符串,r-model 指令还会主动将字符串转换成 expression 对象,所以 r-model={test} 和 r-model="test" 是一样的。关于每种节点的类型需要处理的方法其实大同小异,找一个分析一下。

function initSelect( elem, parsed, extra){
  var self = this;
  var wc = this.$watch(parsed, function(newValue){
    var children = elem.getElementsByTagName('option');
    for(var i =0, len = children.length ; i < len; i++){
      if(children[i].value == newValue){
        elem.selectedIndex = i;
        break;
      }
    }
  }, STABLE);

  function handler(){
    parsed.set(self, this.value);
    wc.last = this.value;
    self.$update();
  }

  dom.on( elem, "change", handler );
  
  if(parsed.get(self) === undefined && elem.value){
    parsed.set(self, elem.value);
  }

  return function destroy(){
    dom.off(elem, "change", handler);
  }
}

这个就是 r-model 对于 select 节点的处理方法。比如现在有一个<select r-model={selectModel}> ... </select>,执行 initSelect 方法。
首先实现 model => view 的绑定,主要方法就是上面 $watch 的部分,watch 插值,一旦插值的值变化就执行方法遍历 select 的 option ,找到 option 的 value 等于插值的值的那个 index 然后将其设置成选中状态,如果是 input 就更简单,直接将 input 的 value 设置成插值的值。然后实现 view => model 的绑定,这部分主要是通过原生 dom 方法 onchange 来实现的,给绑定了 r-model 指令的 dom 节点绑定 onchange 事件,那么这个节点改变的时候就会触发方法,将 dom 的 value 赋值给对应的插值。
就这样实现了双向的绑定,view => model 这部分实现比较简单。就不过多讲解。后面主要看看 model => view 中的 $watch 部分到底是怎么实现的

首先来看看 $watch 这个函数长什么样子,有点长

$watch: function(expr, fn, options){
    var get, once, test, rlen, extra = this.__ext__; //records length
    if(!this._watchers) this._watchers = [];
    if(!this._watchersForStable) this._watchersForStable = [];

    options = options || {};
    if(options === true){
       options = { deep: true }
    }
    var uid = _.uid('w_');
    if(Array.isArray(expr)){
      var tests = [];
      for(var i = 0,len = expr.length; i < len; i++){
          tests.push(this.$expression(expr[i]).get)
      }
      var prev = [];
      test = function(context){
        var equal = true;
        for(var i =0, len = tests.length; i < len; i++){
          var splice = tests[i](context, extra);
          if(!_.equals(splice, prev[i])){
             equal = false;
             prev[i] = _.clone(splice);
          }
        }
        return equal? false: prev;
      }
    }else{
      if(typeof expr === 'function'){
        get = expr.bind(this);      
      }else{
        expr = this._touchExpr( parseExpression(expr) );
        get = expr.get;
        once = expr.once;
      }
    }

    var watcher = {
      id: uid, 
      get: get, 
      fn: fn, 
      once: once, 
      force: options.force,
      // don't use ld to resolve array diff
      diff: options.diff,
      test: test,
      deep: options.deep,
      last: options.sync? get(this): options.last
    }


    this[options.stable? '_watchersForStable': '_watchers'].push(watcher);
    
    rlen = this._records && this._records.length;
    if(rlen) this._records[rlen-1].push(watcher)
    // init state.
    if(options.init === true){
      var prephase = this.$phase;
      this.$phase = 'digest';
      this._checkSingleWatch( watcher);
      this.$phase = prephase;
    }
    return watcher;
  },

首先这个函数接收三个参数,在最上面 r-model 的方法里面也可以看到,第一个参数是 watch 的插值,类型是 expression 对象,第二个参数是 watch 的值发生改变之后要执行的函数,第三个参数是一些 option ,可以看到在 initSelect 方法里面这个参数传递的是一个对象 { stable: true } ,看函数体,首先创建了一些变量,然后初始化了两个数组 '_watchersForStable', '_watchers',经过一些判断, 第一个参数是否是数组,是否是函数,最后生成一个 watcher ,watcher 长这样

var watcher = {
  id: uid, 
  get: get, 
  fn: fn, 
  once: once, 
  force: options.force,
  // don't use ld to resolve array diff
  diff: options.diff,
  test: test,
  deep: options.deep,
  last: options.sync? get(this): options.last
}

注意 watcher 里面有维护一个 last 字段,这个字段表示这个 watcher 上一次的值。还有一个 fn 字段,这个字段就是改变之后要执行的函数。
然后判断 option 里面的 stable 是否是 true ,如果是就 push 进 _watchersForStable,不是就 push 进 _watchers,猜测,前面那个数组是保存 r-model 的 watcher 对象,后面那个是保存用户自己写的 watcher 对象。然后 return 这个 watcher 对象,到此函数结束。
$watch 主要做的事情就是把需要 watch 的 expression 对象 push 到 '_watchersForStable' 或者 '_watchers' 数组中。
很明显这个 $watch 函数并不能检测到值的变化。真正检测到这个变化的是脏值检测的过程。
触发脏值检测过程的函数是 $update ,函数长这样

$update: function(){
	var rootParent = this;
	do{
	  if(rootParent.data.isolate || !rootParent.$parent) break;
	  rootParent = rootParent.$parent;
	} while(rootParent)

	var prephase =rootParent.$phase;
	rootParent.$phase = 'digest'

	this.$set.apply(this, arguments);

	rootParent.$phase = prephase

	rootParent.$digest();
	return this;
},

这个函数的主要作用就是找到根节点 rootParent ,然后 rootParent.$digest()
脏值检测的主要过程在 $digest 函数中,这个函数长这样

$digest: function(){
    if(this.$phase === 'digest' || this._mute) return;
    this.$phase = 'digest';
    var dirty = false, n =0;
    while(dirty = this._digest()){

      if((++n) > 20){ // max loop
        throw Error('there may a circular dependencies reaches')
      }
    }
    // stable watch is dirty
    var stableDirty =  this._digest(true);

    if( (n > 0 || stableDirty) && this.$emit) {
      this.$emit("$update");
      if (this.devtools) {
        this.devtools.emit("flush", this)
      }
    }
    this.$phase = null;
},

这个函数首先将 $phase 字段置为 digest 代表在脏值检测的阶段,然后执行 _digest() 方法,来看看 _digest 方法

_digest: function(stable){
  var watchers = !stable? this._watchers: this._watchersForStable;
  var dirty = false, children, watcher, watcherDirty;
  var len = watchers && watchers.length;
  if(len){
    var mark = 0, needRemoved=0;
    for(var i =0; i < len; i++ ){
      watcher = watchers[i];
      var shouldRemove = !watcher ||  watcher.removed;
      if( shouldRemove ){
        needRemoved += 1;
      }else{
        watcherDirty = this._checkSingleWatch(watcher);
        if(watcherDirty) dirty = true;
      }
      // remove when encounter first unmoved item or touch the end
      if( !shouldRemove || i === len-1 ){
        if( needRemoved ){
          watchers.splice(mark, needRemoved );          
          len -= needRemoved;
          i -= needRemoved;
          needRemoved = 0;
        }
        mark = i+1;
      }
    }
  }
  // check children's dirty.
  children = this._children;
  if(children && children.length){
    for(var m = 0, mlen = children.length; m < mlen; m++){
      var child = children[m];
      if(child && child._digest(stable)) dirty = true;
    }
  }
  return dirty;
},

这个函数首先判断是否传入一个 stable 来决定是用 _watchers 还是 _watcherForStable ,然后遍历数组里面的每一个 watcher 来看这个 watcher 是否是脏的,是否是脏的是用 _checkSingleWatch 这个函数来判断的,这个函数有点长不方便贴,主要是一个 diff 功能,根据 watcher 里面的 last 字段用来获取上次的值和 watcher.get() 方法用来获取当前的值,做 diff 之后如果是脏的就将 now 赋值给 last 然后执行 watcher 的回调函数 fn ,最后 return 一个 true。
遍历完整个数组之后再遍历这个 rootParent 的 children 继续执行这个方法,将这整个 rootParent 遍历完之后,过程中如果有一个 watcher 是脏的,那么整个脏值检测的过程会返回脏,如果整个过程返回脏就会再来执行一遍脏值检测的过程直到整个过程返回不脏为止,如果20次之后还是返回脏就抛出异常,循环依赖。
脏值检测的整个过程就是这样的。
那么剩下最后一个问题了,$update 到底是在什么时候触发的。除了我们手动去执行 $update 以外,平常的一些操作,比如给按钮绑定一个 on-click 事件,在事件里面去改变 data 的值,比如 this.data.testWatch = 'xxx',比如在 ajax 请求的回调里面去改变 data 的值,比如在 setTimeout 里面去改变 data 的值,在这些个过程中我们并没有手动去 $update 但是 regular 会自动的进入 $update ,那么问题来了,regular 到底是在什么时候进入的,他怎么知道我们改变了 data 的值呢?
regular 在自定义事件,比如 on-xxx 的回调函数触发之前和之后都执行了一遍 $update 然后 regular 包装了一个 timeout 方法,就是把 setTimeout 之后加了一个 $update ,但是异步操作还是不会自动的去 $update 所以我们平时的工程里面都封装了一个 $request 这个函数除了封装了标准的 ajax 以外还触发了一遍 $update。思路就是 regular 接管了大部分可能改变 data 的入口,并主动的去触发 $update 。

Promise对象剖析

作为已经成为标准规范的Promise对象,如何使用想必大家都很了解能够正确的应用,本文从分析Promise对象和then方法源码入手来探索Promise到底是如何实现,文章篇幅有限,如有不对的地方欢迎指正。

Promise对象和then方法

根据ES2016标准实现的简单promise对象

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
function Promise(fn) {
    var self = this;
    self.state = PENDING;
    self.value = null;
    self.handlers = [];

    function fulfill(result) {
       if(self.state === PENDING) {
            self.state = FULFILLED;
            self.value = result;
            for(var i = 0; i<self.handlers.length; i++) {
                self.handlers[i](result);
            }
        }
    }
    function reject(err) {
        if(self.state === PENDING) {
            self.state = REJECTED;
            self.value = err;
        }
    }
    fn && fn(fulfill,reject);
}

Promise.prototype.then = function(onResolved, onRejected) {
    var self = this;
    return new Promise(function(resolve, reject) {
        var onResolvedFade = function(val) {
            var ret = onResolved ? onResolved(val) : val;
                resolve(ret);
        };
        var onRejectedFade = function(val) {
            var ret = onRejected ? onRejected(val) : val;
                reject(ret);
        };
        self.handlers.push(onResolvedFade);
        if(self.state === FULFILLED) {
            onResolvedFade(self.value);
        }
        if(self.state === REJECTED) {
            onRejectedFade(self.value);
        }
    })
}

Promise对象

  • 三个私有属性
    • state:用来保存当前执行的状态
    • value:保存异步操作返回的值
    • handles:用来存异步回调的容器,在状态改变为fulfill或reject的时候遍历handles中的值(函数)来执行相应的方法
  • 两个内置方法fulfill和reject
    • 变化状态,并执行对应的逻辑
  • 一个参数
    • 接受一个回调函数,用内置的方法fulfill和reject作为该回调函数的参数

then方法

then方法接受两个参数,返回一个新的Promise对象,主要实现点在传入到Promise对象中的回调函数。

  • 该回调函数内部封装了两个函数,即成功和失败时要执行的函数。然后将成功的回调存到then方法上下文作用域的Promise对象中的handlers容器中(该实现方法只用成功回调作例子)
  • onResolvedFade和onRejectedFade
    • onResolvedFade封装了传入到then方法的第一个参数,运行该函数,调用resolve,并将返回值作为then返回新的Promise对象内置函数fulfill的参数,作为下一次回调的初始值。
    • onRejectedFade和onResolvedFade同理

一个例子

function async(value){
    var pms = new Promise(function(resolve, reject){
        setTimeout(function(){
            resolve(value);
        }, 1000);
    });
    return pms;
}

async(1).then(function(result){
    console.log('the result is ',result);//the result is 1
    return result;
}).then(function(result){
    console.log(++result);//2
});

async函数返回一个Promise对象,传入该Promise的参数模拟异步操作,1s后执行resole方法。定义了两个then方法,分别传入了成功时的回调函数。根据刚才的promise函数和then方法分析,第一个then方法的第一个参数会被存到当前then上下文作用域的Promise对象的handlers中,第二个then方法的第一个参数会被存到当前then上下文作用域的promise对象(也就是第一个then返回的新的Promise对象),等待1s后,执行async返回的promise对象中的fulfill方法,然后依次执行then定义的回调函数。

所以Promise的原理到底是什么

  • 通过定义then方法中的成功和异步回调,存到当前then方法上下文作用域的promise对象的handlers容器中,当异步返回结果时,调用该promise的fulfill方法,改变状态,链式调用定义好的回调函数,达到将异步当同步写的目的。

总结

  • 要搞懂Promise,最主要的是搞懂Promise构造器的写法,和then函数的封装。

  • 要用好Promise,最主要的是封装好要传入Promise中的匿名函数或者函数表达式。

  • 有几个调用就有几个Promise对象,每个then方法传入的函数都是前一个Promise成功或者失败的异步回调。保存到每一个promise中的handlers中的函数都是闭包

  • 第一个调用返回的值是后一个调用then方法传入函数的参数

  • 定义promise与then的时候就像按次序放置的多米诺骨牌,当我们推动第一张骨牌的时候,也就是异步结果返回成功时,定义在then方法中的函数会开始链式调用。

EyeDropper 开发实践

1. 什么是 EyeDropper

Chrome Devtools 的颜色提取器 EyeDropper,用惯了 Chrome 的前端开发者并不陌生。

image

但它并不支持在页面中使用,想在页面中使用只能自己实现一个。

那么接下来就介绍一下如何自己实现一个 EyeDropper。

2. 原理解读

要实现 EyeDropper,必须先学习一下基本的色彩知识。

物品被光线照射并反射出来,被人的眼睛接收,进而传递到人脑中形成对「色彩」的认知,称之为人的「视觉效应」。

色彩的三属性

1. 色相(hue)

最最基本的颜色术语、通常用来表示物体的颜色。

当我们说红、绿、黄时,我们说的就是色相。将色相按照波谱顺序排列,首位相连形成环状则为「色相环」。虽然人们习惯将其分为七种颜色:红、橙、黄、绿、青、蓝、紫,但实际上的光谱应该是连续的。

image

2. 饱和度(saturation)

指在特定的光照条件下颜色是如何呈现的,也就是色彩的鲜艳程度。

饱和度取决于颜色中含色成分和消色成分(灰色)的比例,即纯度最低的是灰色(无彩色)。高纯度表现为生机朝气,低纯度表现为厚重沉稳。

3. 明度(value)

也被称作亮度,它是指颜色的明亮程度。

在任何颜色中添加白色,明度上升,添加黑色,明度下降。明度相差越远的两种颜色搭配,色彩之间的交界感就越明显,视觉上也就越清晰。

三者可以简单用下图综合表示:

image

3. 方案设计

1. 模块梳理

理解了基础的颜色原理后就好办事了,拿 Chrome Devtools EyeDropper 分析:

image

  1. 饱和度和亮度选择器。
  2. 色相选择器。
  3. 透明度选择器。
  4. 色彩转换器。点击可以在 RGBA、HSL 和 HEX 之间切换。
  5. 调色板。点击直接选择不同的特定颜色。
  6. 取色器。在屏幕上直接选择需要的颜色。

结合上述色彩知识,加上这块分析就可以开始进入代码层面的设计。

2. 实际划分

在组件化大行其道的时代,以网易惯用的 Regular 进行组件化开发。

根据模块划分,划分基础的:

  • 饱和度和亮度选择组件;
  • 色相选择组件;
  • 透明度选择组件;
  • 色彩输入及转换器组件。

考虑简洁性,「取色器」及「调色板」不做实现。

4. 组件实现

1. 色彩获取功能实现

  • 饱和度和亮度选择组件(就是 EyeDropper 最上面那块)

① 该组件以色相组件选择的色相(hue)为背景,若是想直接使用 hue 作为 CSS 背景,需要使用 hsl(hue, saturation, value) 格式,设置为:

hsl(hue, 100%, 50%)

此时饱和度应设为 100%,因为饱和度为 0% 时为灰色,100% 时为原色。

image

而亮度是指颜色偏向于白色还是黑色。50%的亮度值表示颜色位于黑色和白色中间,这时颜色会基本保持原来的颜色不变。

image

② 同时利用线性渐变 linear-gradient 做色层叠加实现。

.saturation-white {
	background: linear-gradient(to right, #fff, rgba(255,255,255,0));
}

.saturation-black {
	background: linear-gradient(to top, #000, rgba(0,0,0,0));
}
  • 色相组件(取颜色的那个条)

上面谈到了色相环的概念,色相组件利用的就是色相环原理。

将 EyeDropper 中的色相条与色相环对比,是不是有异曲同工之妙?答对了,直接将圆环拍平即可。

获取色值时只需要获取当前位置对于最左端的百分比,换算成圆环角度。

Math.round(360 * percent / 100);
  • 色彩输入及转换器组件

其中总共有 RGBA、HSL 和 HEX 三种格式的切换。

在 Regualr 中,内嵌组件的传入属性会挂在子组件的 data 上,并实现数据绑定。但考虑数据处理的统一性,在所有颜色的获取处并不对颜色做处理,而是通过事件传递的方式,统一 $emit 到外层做统一的色值转换。

this.$emit('change', {
    h: hue,
    s: saturation,
    v: bright,
    a: alpha
});

// 外层接收后统一处理
this.$on('change', processor);

接收到不同格式的颜色后,利用 tinycolor2 对颜色进行处理,使所有格式转换为同一种颜色。

var color = tinycolor(colors);
var hsl = color.toHsl();
var hsv = color.toHsv();

2. 需要注意的问题

  • 在使用鼠标拖动选择颜色时,需要做好节流处理,避免爆炸;
  • 由于 JS 对于浮点数的奇妙控制,需要在输入框做好统一截断处理;
  • 在鼠标拖动的时间绑定及解绑中,注意绑定对象与解绑对象的一致性。

代码可以 戳我 查看,具体实现如下:

image

附录:

  1. EyeDropper 介绍

  2. Totally Tooling Tips with Addy Osmani & Matt Gaunt

实现一个简单的html ast解析算法


title: 实现一个简单的html ast解析算法
date: 2017-05-23

概述

html ast解析算法的过程是将一段html字符串,解析成一个javascript对象。

示例字符串

<div>
    <input r-value="{name}" />
    <p>
        <span></span>
        <span style="display:block;">
            描述信息2
            <span>{name}</span>
        </span>
    </p>
</div>

思路分析

整体实现分析:

  1. 通过正则匹配出token,设置node,添加到ast对象中,同时截取掉剩余的字段
  2. 主要的token: 开标签<div>,<p>等,自闭合标签<input />,闭合标签</div>, </p>
  3. 实现范围可以先从标签名开始,再添加属性,再解析文本(textNode)

代码思路:

  1. 用一个数组储存树结构,children表示分支
  2. 先匹配到一个div开标签,push进数组
  3. 匹配到input自闭合标签放入div.children
  4. 匹配到p标签放入div.children
  5. 匹配到span放入p.children
  6. 匹配到</span>结束标签,表示后面的标签需要加入到p.children而不是span.children
  7. 递归到没有开标签为止

代码实现

分布解析的动态demo,代码每一步都有详细的解释:

online demo

github源码

by Kaola nrz

SVG动画实践篇-音量变化效果

说明

这个动画的效果就是多个线条的高度发生变化,使用了两种写法(css,svg)来实现。

CSS实现

  • 定义线条的节点,可以使用伪元素实现。
  • 使用 CSS3 的 animation 属性给元素定义动画样式。
  • 每个元素定义的动画的延时时间不固定。
@-webkit-keyframes slide{
    0%{height:0;}
    100%{height:50px;}
}
.m-box .line:nth-child(1){
    -webkit-animation:slide 1.2s linear .5s infinite alternate;
}
.m-box .line:nth-child(3){
    -webkit-animation:slide 1.2s linear .75s infinite alternate;
}

SVG实现

使用animate元素来实现。原理一样,通过改变元素的高度。

  • x="20",通过改变 x 坐标的值来给动画元素定位。(这里指的橙色线条)
  • 修改 animate 标签上的 begin 属性值来定义元素动画的延时时间。
  • svg 动画无法像 CSS 动画一样,定义轮流反向播放动画的效果。所以动画有些生硬。
<svg width="300" height="300" version="1.2" xml:space="default">
    <rect height="0" width="5" rx="2.5" style="fill:#f60;">
        <animate attributeName="height" attributeType="XML" from="0" to="50" begin="0.5s" dur="1.2s" calcMode="linear" repeatCount="indefinite" />
    </rect>
    <rect height="0" width="5" rx="2.5" x="10" style="fill:#f60;">
        <animate attributeName="height" attributeType="XML" from="0" to="50" begin="0s" dur="1.2s" calcMode="linear" repeatCount="indefinite" />
    </rect>
    <rect height="0" width="5" rx="2.5" x="20" style="fill:#f60;">
        <animate attributeName="height" attributeType="XML" from="0" to="50" begin="0.75s" dur="1.2s" calcMode="linear" repeatCount="indefinite" />
    </rect>
    <rect height="0" width="5" rx="2.5" x="30" style="fill:#f60;">
        <animate attributeName="height" attributeType="XML" from="0" to="50" begin="0.25s" dur="1.2s" calcMode="linear" repeatCount="indefinite" />
    </rect>
    <rect height="0" width="5" rx="2.5" x="40" style="fill:#f60;">
        <animate attributeName="height" attributeType="XML" from="0" to="50" begin="0.5s" dur="1.2s" calcMode="linear" repeatCount="indefinite" />
    </rect>
</svg>

结论

  • svg 动画无须定义样式,完全通过定义标签的属性来定义动画。
  • svg 动画不能定义轮流反向播放动画的效果。

git 地址:https://github.com/rainnaZR/svg-animations/tree/master/src/pages/step2/volumn

实践中的前后端分离


title: 实践中的前后端分离
date: 2017-05-11 23:07:48

相信前后端分离这个词,早已流传甚广,大家一些自己的理解,但可能有些人的观点有稍许偏差:我们要搞 SPA,全AJAX,那才是前后端分离了。

什么是前后端分离

我们来聊聊什么是前后端分离。

先来看一张WEB系统前后端架构模型图。

WEB系统前后端架构模型

从图中可以清晰的看到,前后端的界限是按照浏览器和服务器的划分。那么我们经常会发现一些问题:

  1. 模板层归属前端还是后端?
  2. 模板强依赖于后端渲染,前端开发需要等待后端开发吗?

通常情况,模板层归属于前端,因为让后端人员来接触他们不擅长的样式和 js 交互是很蛋疼的事情。

那么,作为前端开发的我们在实际的开发场景中又会遇到以下问题:

  1. 环境:进行本地开发,需要起后端环境,如 Tomcat、PHP,影响开发效率
  2. 流程:前端开发先开发 html,再将 html 改写成指定的模板语法(俗称套模板),影响开发效率
  3. 接口:
    • 接口定义一般使用 word 文档,前端开发时不好理解接口字段,影响开发效率
    • 接口变更需要重新编写文档,并重新发送,影响开发效率
    • 文档散落,影响接口维护
  4. 联调:
    • 联调过程变得很复杂,尤其是没有做热部署的Java工程,改视图还需要重启Tomcat,影响前端联调效率
  5. 效益:
    • 前端开发更关注用户体验,而后端只希望关注数据可靠,为实现如响应式、ssr之类的一些交互,前端需要掌控一定的请求响应能力
    • 如果前后端对接的方式转变成为纯粹的 JSON 交换,对于提升开发效率、更清晰的职责与接口复用都是有好处的

出现影响开发效率的事情,就说明现有的模式存在问题,显然问题的解题思路需要我们重新思考“前后端”的定义。此时,前后端分离的概念便应运而生,目的是将前后端开发人员的合作方式调节到大家都尽可能舒适的姿势。

有哪些实现方案

SPA

SPA
全称 Single Page Application,使用前端路由的方式代替后端的 controller,并使用前端模板代替后端的模板引擎渲染,使用 restful api 实现与后端的数据交互。

在这个方案中,前后端的交互被转换成了纯粹的 http 方式的 JSON 串交互。

SPA 的优势:

  1. 环境:前端开发者不需要本地起后端环境
  2. 流程:独立的前端开发方式,由于后端返回纯 JSON ,前端想要模拟请求响应的话,只需启动一个纯静态的服务器,响应 JSON 格式的 html 即可
  3. 联调:清晰的对接方式,JSON 对于前后端来说都是比较纯粹的
  4. 效益:对于用户来说,用户体验的提升

SPA 的劣势

  1. SEO 弱
  2. 首屏加载慢,等所有 js 加载完才能出首屏
  3. 前端需要处理一些本不需要在这一层处理的事情,如权限控制交给前端控制

综上,SPA 是一个可以解决前后端分离的有效方案,对于无 SEO 要求的项目大可以尝试。

开发阶段的分离 -- Mock && Proxy

顾名思义,开发阶段的前后端分离,需要依赖工具实现,通常把这个工具叫做 Mock Server(如笔者所开发的一款 Mock Server -- Foxman)。

Mock Server 提供功能

基础功能

  • 拦截同步请求,取 JSON 格式的 Mock 数据,结合本地 Template,通过模板渲染引擎渲染,得出响应的页面
  • 拦截异步请求,取 JSON 格式的 Mock 数据响应

这里我们需要抽象以上操作为两个函数,利于理解:

  1. SyncView = TemplateEngine(Template, MockData)
  2. AsyncView = MockDataTransform(MockData)

优化功能

  • Living Reload -- 监听本地文件,发生修改则通知(一般使用 websocket)浏览器更新资源
  • 修改响应头 -- Mock 阶段,可以做到 js 修改响应情况
  • 代理 -- 前面提到了两个函数,代理的指责是将原本取自本地的 MockData,改成了从服务端以 http 的方式取得的数据

开发流程

我们将一个项目开发划分为三个阶段:接口定义,开发,联调。正好可以和我们 “Mock”、 “Proxy” 两个工具契合。
让我们通过实际的场景来表述这种前后端的合作方式。

接口定义

我们接到一个需求,实现某个功能。在我们理清楚具体的功能之后,应该与后端定义接口及返回,包括:

  1. 有哪些页面,页面的请求路径,模板位置,以及后端返回给我们的 Model 内容
  2. 有哪些 Ajax 接口,Ajax接口 的请求路径,以及后端返回的 JSON 内容

在制定完接口后,我们需要按照 Mock Server 的要求,创建 Mock 文件,并往里面填入与后端约定好的 JSON 数据,并与后端确认。

显然我们的开发中,接口定义变成了一件很具体的事情,而开发阶段可以使用这份 Mock 数据,并做到 Mock 数据即接口文档

开发阶段

在我们完成接口定义后,我们期望的是无打扰、自治的一个开发体验。

正如上图所示,开发阶段前端开发可以完全与后端开发人员隔绝,也不需要本地启动后端环境,我们要做的,只是按照先前指定的接口及本地的 Mock 文件进行需求的开发。

而在开发过程中,遵循 html -> css -> js 的开发顺序,Foxman 拥有一个很人性化的 live reload(更改css 之会 reload css),总之接口定义合理的话,这一步会很顺畅。

联调阶段

在我们开发完页面后,我们期望的是与后端进行联合调试,已验证功能开发是否存在缺陷,即联调阶段。

在这步骤中,我们只需要更换 SyncView = TemplateEngine(Template, MockData) 的 MockData,将原本响应自 Mock 文件的请求,转发到真实的目标服务器(在联调阶段会是 开发主机 或者 测试机)。

此处代理和转发,笔者已抽象成了的另外一个库 koa-api-forward,欢迎交流和使用。

Mock && Proxy 优势

  1. 环境:
    • 前端开发者不需要本地起后端环境
  2. 流程:
    • 独立的前端开发方式,Mock 与 Proxy 结合,流程清晰
    • 前端可以在本地调试 view 层,大幅度提升前端的联调效率
  3. 联调:
    • 清晰的对接方式,JSON 实现前后端来说都是比较纯粹的
  4. 效益:
    • 方便开发的同时,保持线上系统的无侵入

Mock && Proxy 劣势

  1. 未真正掌握线上的接口响应,实现一些前端交互需求(响应式)时仍依赖后端,或无法进行(如 ssr)

Node.js 中间层

https://github.com/genify/ita1024/raw/master/res/pic-9s.gif

这个模式自然时结合了前面的 Proxy。大家都知道 Node.js Server 里面强调一个 中间件的 概念,对应到设计模式的职责链模式。即只处理自己能处理的情况,否则,继续往后传递,直到被处理。

这个方案中,Proxy 作为了中间件体系中的最后一层,用以转发请求,而在这之前依次是中间件的错误处理、静态资源的响应、路由拦截(routers) 等等。

Node.js 拥有一定的接口控制能力,如处理 PC/Mobile 的响应式渲染,或是 Server-Side-Render 等等。

Node.js 中间层优势

  1. 环境:
    • 前端开发者不需要本地起后端环境
  2. 联调:
    • 清晰的对接方式,JSON 实现前后端来说都是比较纯粹的
  3. 效益:
    • 可渐进式,前期可以将请求全部转发后端服务器,而后可以逐步将 Node.js 层作为用户的直接数据交换层
    • 职责分明,后端服务化,Node.js 层处理接口用户相关的页面响应 及 数据交换
    • 可组合性,后端服务化,Node.js 负责组合拼装,实现接口可复用率

Node.js 中间层劣势

  • 开发阶段仍需要 Mock 支持,如果将 Mock 方式整合进 Node.js 中间层,则造成 Node.js 中间层职责不纯粹
  • 对现有系统的渐进式改造是个较为漫长的过程

总结

还是那句话,所有的前后端分离方案,都是为了前后端开发人员的合作方式调节到大家都尽可能舒适的姿势。

那么一个不错的实践是,我们可以将 (Mock && Proxy) 与 Node.js 中间层 两个方案结合:

  1. Mock && Proxy 只依靠抽象出来的工具,在前端开发阶段,继续使用,避免造成 Node.js 中间层职责不纯粹
  2. Node.js 中间层的存在可以解决(Mock && Proxy)方案的劣势

未完待续。。。

参考资料

by 君羽

自定义webpack配置实战总结


title: <自定义webpack配置实战总结>
date: 2017-05-24

背景

在优惠券二期的任务中,需要进行整体重构。 在和yubaoquan讨论之后,我们希望的技术栈是这样:

  • 打包用webpack2 (比1有一些优势)
  • js用es2015
  • css用mcss
  • js框架regularjs
  • ui框架nek-ui(基于regularjs)

上面这些问题不是很大。

有个比较麻烦的地方就是,前端和后端的连接方式有所改变,详情请看下文。

以前的方式

此项目是非spa项目,所以会有多个页面。

比如,根目录下有2个页面:

/WEB-INF/ftl/pageA.ftl
/WEB-INF/ftl/pageB.ftl

pageA中引入js/pageA/entry.jsentry.js再引入业务js。pageB同理。

后端controller中连接到pageA的方式很简单,只要return "/WEB-INF/ftl/pageA"这样即可。

备注:
ftl文件:是freemarker(后端用的模板引擎)的模板文件,可以理解为html页面

现在的方式

与之前的不同就是, 每个entry.js都要经过一次webpack打包,再重新注入到ftl中,这里会面临2个问题:

  • 如何将打包完成的entry.js注入到ftl文件中
  • 如何配置webpack多入口

项目不断扩大,添加了新页面,配置又要重新写一次吗?迎面而来第3个问题:

  • 如何动态生成webpack配置

解决方案

如何将打包完成的entry.js注入到ftl文件中

由于之前接触过htmlwebpackPlugin插件,可以动态的生成html文件,并将entry.js插入到html文件中。那么生成ftl文件也应该没问题,查看了文档之后,确实只需要修改filename就行了。

如何配置webpack多入口

在github上找到了答案

简单解释下:

  • entry中加入多个字段如page1, page2;
  • output中路径加入[name]的动态字段
  • htmlwebpackPlugin配置中,设置chunks字段为入口的配置的字段即可

如何动态生成webpack配置

大家熟悉的webpack.config.js文件:

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        filename: "bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style!css" }
        ]
    }
};

可以看出这个是commonjs语法,导出一个对象,那我们就可以对这个对象进行一些预处理,最后再导出这个经过处理的新对象即可。

就像这样:

let myConfig = {}

myConfig.entry = "./entry.js"
// ....anything you want
module.exports = myConfig

实际开发中遇到的问题记录

commonjs导出异步对象

由于遍历文件夹,返回文件是一个异步操作,所以模块导出需要导出一个promise;

代码如下:

module.exports = new Promise((resolve, reject) => {
    recursive(PAGE_PATH, (err, files) => {
        // Files is an array of filename
        let filerFilesFunc = file => /\.ftl$/.test(file);

        if (err) {
            console.log(err);
            return;
        }
        files = files.filter(filerFilesFunc);
        resolve(generateWebpackConfig(files, baseWebpackConfig));
    });
});

ftl中使用相对路径,路径找不到

stackoverflow上的答案

需要设置publicPath属性为根目录(请根据实际需求修改):

output: {
    publicPath: '/' 
}

其他webpack插件介绍

ExtractTextPlugin: 可以把样式模块提取成单独的css文件
CommonsChunkPlugin: 可以把共用的模块提取成单个文件,不同入口之间可以通用
chunkhash: 单独给文件创建hashes,缓存文件

结语

目前项目已上线,中间很多坑都踩过,其中很多问题都是yubaoquan解决的,合作愉快(握手)。webpack.config.js代码在这code here,供大家参考,有什么问题可以一起讨论

by Kaola nrz

从 history 到 SPA

前后端分离目前算是一种共识了,特别是前端 MV* 框架的兴起,前端更倾向于按照框架写代码,之后打包再交由后端 render。因此,如果传统的根据路由打包为多个入口会比较蛋疼,单一入口的路由管理算是 SPA 的一个关键部分了,而这里离不开 history


history

比较有意思的是,互联网根基不应该是光纤或者电脑之类,应该是 URL,任何互联网资源都有唯一的 URL(说的不严谨,请不要深究,下同),所以可以说 URL 是互联网资源的身份证。在资源跳转的时候可以轻松地前进后退,因为浏览器里有个 history 结构管理着这一切。history 的实现有点类似于栈,点击链接是 push,后退是 pop,但是也不是栈,因为你也可以前进,或者直接手输 URL 进行 replace。

但是,不管是如何转换(push/pop/replace/...)说白了都是对资源的一种替换(这里资源我们暂时当作 HTML 页面吧)以及资源依赖的一些其它资源(可以理解为 js/css/images 等),每次页面的切换(URL变动)都是资源的加载过程。这逻辑上当然没有什么问题,但是如果切换的两个页面有 90% 的内容和资源都一样呢,在没有正确缓存的情况下,全局刷新是对资源的一种极大的浪费。

从浏览器角度来说,它是没有优雅的方式去判断两个页面之间的差异然后只加载新内容的,因为页面的差异情景太多,这锅不应该让浏览器背。但是,换种思路呢,如果 history 的切换变得可编程,这一切就都不是事了,因为写代码的人肯定很清楚差异是什么。

三种 history

为了更好的对 history 进行编程,著名的 history 对此做了个封装,其提供了三种形式的 history,对外暴露出相同的基础操作,底层实现是不同的,适用场景也不一样:

  • browserHistory 是基于原生 HTML5 history API 的封装,这更接近我们大多数时候看得 URL,用 path 来区分,这更利于 SEO,并且在使用(如分享,收藏等)的时候坑更少。虽然对浏览器版本有点要求,但是仍然是首选
  • hashHistory 可以兼容性更高,这个更像我们平时说的单页,因为它的 path 不变,用的是 # 号去区分资源,如:example.com/#/some/path
  • memoryHistory 并不是为浏览器准备的,因为它的 url 并不是从地址栏读取的,其实根本没有地址栏的概念,自身在内存里模拟一个 history 环境,主要用于 React-Native

SPA 里的路由

说完 history 了,可以说 SPA 了,使用上 SPA 体验并不会是单页的感觉,大多也是很多页,甚至很多你根本感知不出是否是单页,因为 SPA 更多的是指实现方式的一个入口,而不是表现上的。引入 MV* **后,这里页会被当作模块,所以如果可以控制在不同的 URL 时展示不同的模块,就可以实现一个单页系统。

所以,一个 SPA 可以简单到:

return `{#if path = '/home'}<Home />{#else}<Error />{/if}`

表现上就是访问 /home 就显示首页,访问其它页面就显示错误页面,这不就是我们过去几十年用浏览器的方式吗?所以我说 SPA 并不是表现上的单入口。

由于 hashHistory 放弃了用 path 去定位资源,所以其有着天然 SPA 的基因,使用上并不会有太多疑问。browserHistory 应用于单页系统的时候需要配置下服务端,可以对任何 path 都返回同一个页面。接下来的过程可以想象下,主要三种场景:

  • 手动改变地址栏:这种没什么特别过程,就是一个全新加载过程,资源加载完后应用根据 URL 渲染出合适组件(页面)
  • 通过导航前进后退:这会抛出 popstate 等事件,应用根据事件更新 history 栈(不精确)信息(地址栏 URL 也会被改变),最后渲染出正确组件
  • 页面链接过去:在单页应用里,应用内链接并不会真的写 <a href=""> 链接,会把点击事件转为设置 history 操作,同样 URL 跟随改变,最后渲染出目标页面

结论

  • SPA 在使用上并不会单页
  • 优先使用 browserHistory,除非你无服务端的控制权(如把静态页面放在 CDN 上)
  • 浏览器如果暴露更多可编程接口,会极大丰富开发模式

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.