- 发文章以 Issue 的形式进行,注意格式
- 定期有人会归档到
source/_posts
,需要人肉判断下<!-- more -->
插入位置 - 本地
hexo g
看下是否能正确生成静态文件 hexo d
部署静态站到gh-pages
分支- Git Push 本次新增/修改
kaola-fed / blog Goto Github PK
View Code? Open in Web Editor NEWkaola blog
kaola blog
source/_posts
,需要人肉判断下 <!-- more -->
插入位置hexo g
看下是否能正确生成静态文件hexo d
部署静态站到 gh-pages
分支后台系统多, 用户也多.用户在使用过程中对系统中不合理或影响工作效率的地方, 需要有一个反馈的渠道.
如果每个系统都做一个反馈收集页, 将是重复的工作, 会耗费大量的人力.
因此, 最好能有一个统一的东西, 插入到需要收集反馈的系统中.
宿主系统
: 用户实际使用的系统;
反馈系统
: 收集用户反馈并统一存放供开发人员调查的系统;
插件脚本
: 由反馈系统对外提供的一个js文件, 由宿主系统在页面中引入.
宿主系统中的每个页面中, 要出现一个按钮, 点击按钮, 弹出一个弹窗, 用户在弹窗中编辑反馈信息. 反馈信息包括页面地址, 所属系统, 当前用户, 反馈的文本, 截图, 以及点赞或踩等. 其中所属系统, 页面地址和用户名, 不需要用户手动输入. 用户编辑完这些信息后, 点击确定, 统一提交到反馈系统, 然后弹窗自动关闭.
向宿主系统的全部页面中嵌入一个按钮, 肯定不能手动编辑所有的页面文件, 鉴于系统中所有的页面都会有导航条()这一特点, 决定将添加按钮的逻辑写到一个js文件中, 即插件脚本. 将此js放到公用组件的节点中, 即可实现全部页面嵌入按钮. 至于这个js文件, 因为js支持跨域请求, 可存放在反馈系统中.这样做的好处是避免每个系统拷贝一份这个js文件的代码, 统一管理, 统一修改.
如何将反馈信息在不同系统之间传送? 想到如下几种方案:
1.将反馈信息提交到宿主系统后端, 宿主系统再调用反馈系统的接口, 进行通信;
2.通过前端发送跨域请求, 直接将信息发送到反馈系统;
3.将弹窗中加入一个反馈系统的iframe, 用户在iframe中编辑信息, 直接提交;
第一种方案的缺点是需要每个系统添加后端通信的逻辑, 工作量大. 如果反馈系统的通信逻辑以后有改动, 每个系统都要改;
第二个方案的缺点是需要有人去配置nginx的跨域信息, 比较麻烦. 而且表单校验, 发送数据以及一些弹窗校验等工作, 放在一个需要没有依赖的js中去完成, 相当于裸着写逻辑, 代码量将会很大, 而且不好维护.
第三种方案相当于打开了一个反馈系统的页面, 只是看起来是一个宿主系统的弹窗. 遇到的问题是, 用户提交完成后iframe向外部传送信息比较麻烦. 单这个比起上面两种方案来说, 更加容易解决.
因此选择第三种方案.
一下是比较常见的弹窗使用场景和需求:
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();
}
我们要简化过长的函数,那么我们可以使用哪些模式来优化?《重构与模式》一书中提到面对过长的函数,我们可以考虑使用下面几种模式:
提炼函数大致的**就是将我们过长的函数拆分为小的函数片段,确保改函数内的函数片段的处理在同一层面上。随便找了一个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;
}
})();
高阶函数是指至少满足下列条件之一的函数:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
高阶函数在我们编码时无形中被使用,善用高阶函数可以使我们代码写的更加漂亮。
在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 的 path 标签。
使用 <path> 标签的 d 属性标识路径集合,勾画线条的形状。
例如:
<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线条,除了使用 d 属性定义路径外,还需要用到两个重要的属性, stroke-dasharray 和 stroke-dashoffset, 这两个属性值可以在 path 标签上定义,也可以在样式表中定义。
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>
定义 css3 的 animation,通过改变 path 标签的 stroke-dasharray 或 stroke-dashoffset 值来使路径动起来。
path 路径的长度可使用 js 的 document.getElementById(‘path’).getTotalLength() 来获得。
css 代码如下:
#path{
-webkit-animation:slide 2s linear infinite;
}
@keyframes slide {
0%{
stroke-dasharray:0 511px; /* 511px 为整个路径的长度 */
}
100%{
stroke-dasharray:511px 511px;
}
}
css 代码如下:
#path{
stroke-dasharray:511px 511px;
-webkit-animation:slide2 2s linear infinite;
}
@keyframes slide2 {
0%{
stroke-dashoffset:511px;
}
100%{
stroke-dashoffset:0px;
}
}
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/
参考资料:
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 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 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 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 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 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 width="300" height="300" version="1.2" xml:space="default">
<path d="M0 0 L150 100 V200 H100 Z"/>
</svg>
<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>
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)" />
<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>
类目参谋近期进行版本迭代,添加新功能。原本代码开发已久,可优化空间较大也为了便于日后的维护和拓展,决定页面重构。我参与了其中交易构成页面的代码重构。(数据门户 - 类目参谋 - 交易分析 - 交易构成)
目标:函数拆解,组件降耦,健壮逻辑,bugfix
切换维度tab,连续2次获取数据列表 /dealConstituteDetail
每一次切换维度tab,都会获取二级维度 /secondDimensions
请求返回二级维度列表 = '未选择' + 一级维度列表 - 当前tab维度
所有的请求都是这样的返回,因此根据当前一级维度动态更新二级维度列表
选择日期,发出2-3次 /dealConstituteDetail 和1次 /secondDimensions
第三次请求不合理:修改基础时间不应该使对比时间选择器触发请求。两个组件耦合在一起。
4. [选择指标] 选择少于6个指标,更换时间类型,页面报错
5. [选择指标] 选择除'uv'、'平均转化率'之外的6个以上的指标,第4和第6个表头字段被换成'UV/日均UV'和'转化率/平均转化率',跟底下的数据对不上,显然错误
6. 只有[选择指标]中的前6个有排序
7. 页面宽度不够时,对比时间样式异常
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,
},
...
});
函数应根据调用顺序排放(不在主流程中且不与其他函数有过多交集的函数可置后,如导出文件功能)
注释掉的代码,原代码中有多处
多余的注释
//如果没有搜索信息 则清空搜索栏
if (!searchText) {
data.searchText = "";
} else if(searchText !== true) {
data.searchText = searchText;
}
代码过长
getTradeConstitute获取交易构成明细,函数40几行。做了几件事情,应拆解:
重复的功能相似的代码
获取交易构成明细 与 导出文件 的参数基本一致,声明了两次。
不应在公共组件堆叠当前模块特有的组件
dealComposition <- multifilter <- multiSelector。将multi-两个组件放到dealComposition模块
getTradeDataBySort: function(sort, notRequest) {
...
if (!notRequest) { // !不请求 = 请求
this.getTradeConstitute(undefined, undefined, sort, true);
}
},
换成doRequest更直接
getTradeConstitute这个方法带四个参数,html和js中调用常有undefined实参传入,阅读的时候会很困惑。
getTradeConstitute: function (currentPage, filters, sort, searchText) {...}
// html中使用该函数
on-filterSearch={this.getTradeConstitute(undefined, $event, undefined, undefined)}
建议将形参能去则去,请求数据时从组件的状态中获取参数或者根据不同情形重置参数。
'单引号'
tradeComposition -> deal.composition
<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做接口
工欲善其事,必先利其器。——《论语·卫灵公》
Chrome DevTools 的快捷键,可以帮助开发者在日常开发的过程中节约时间(甚至可以说是大量的时间,具体看天赋咯)。
下面使用表格的方式,列举每个快捷方式在Windows/Linux和Mac下相应的快捷按键。
功能 | 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 |
功能 | 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 + - |
功能 | Windows / Linux | Mac |
---|---|---|
撤销改动 | Ctrl + Z | Cmd + Z |
恢复改动 | Ctrl + Y | Cmd + Y, Cmd + Shift + Z |
选中节点(不会去展开) | ↑,↓ | ↑,↓ |
伸缩展开元素 | ←,→ | ←,→ |
编辑元素属性 | Enter | Enter |
隐藏元素 | H | H |
Edit as HTML | F2 |
功能 | 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 + ↓ |
功能 | Windows / Linux | Mac |
---|---|---|
下一个提示 | Tab | Tab |
上一个提示 | Shift + Tab | Shift + Tab |
使用提示 | → | → |
上/下一个命令/行 | ↑,↓ | ↑,↓ |
清除控制台记录 | Ctrl + L | Cmd + K, Opt + L |
多行输入 | Shift + Enter | Ctrl + Enter |
执行 | Enter | Enter |
功能 | 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 + / |
功能 | Windows / Linux | Mac |
---|---|---|
开启/停止 记录 | Ctrl + E | Cmd + E |
保存时间轴数据 | Ctrl + S | Cmd + S |
加载时间轴数据 | Ctrl + O | Cmd + O |
开启/停止 记录 | Ctrl + E | Cmd + E |
以下的 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属性实现的一些效果,比如清除浮动,以及上面提到的两栏布局的实现。这些效果的实现,可能跟overflow属性的本意相差甚远,就像两种不相关的事务被硬生生的牵扯到了一起。其实不然,CSS Spec规范文档中还明确记录着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的话,是一种默认情况,就相当于正常默认的布局,所有超出元素框的内容仍然会正常显示,不会被裁剪,也不会出现滚动条。但对于其它几种值的话(hidden, scroll, auto),元素的内容可能会被裁剪,此时,对于某些情况下可能出现的特殊布局处理就会出现争议。
比如对于垂直方向紧贴着的两个元素A和B,其中元素A中浮动的子元素可能会遮住元素B的部分文字区域,此时如果元素B的overflow属性设置为visible,则内容会包裹在元素A浮动子元素的周围,这种情况比较容易理解,如下图。
图1 overflow属性设置为visible
但当元素B的overflow属性设置为非visible的值时,各版本规范的规定就会出现差异。
CSS2.0规范规定,设置非visible属性值后,元素B的内容仍然包裹浮动元素,如图2所示。
图2 overflow属性设置为novisible,CSS2.0规范中的处理
此后如果元素B内容发生滚动,每次滚动行为,元素B中发生折叠的内容(图3中元素B中文字内容滚动后发生变化)全部要重新计算重绘,实际上这将会带来很大的性能问题,对滚动体验也会造成比较大的影响。
图3 overflow属性设置为novisible,CSS2.0规范中发生滚动时的处理
但这里存在进一步的疑问,即使按此规范的约定,元素B内容滚动时存在性能以及体验问题,但是非visible属性中的hidden值,难以理解,元素内容已经被裁剪掉了,为什么跟其它值auto, scroll归为一类?这里面就存在一个误区,overflow设置为hidden值并不代表内容不可滚动,此时浏览器只不过没有提供可滚动的UI,被"裁剪"掉的内容可以通过JavaScript脚本来控制滚动,这也是脚本模拟滚动条的基础。比如,可以通过JavaScript脚本设置元素的scrollTop实现图4的效果,更友好的方式可以自定义一个滚动条。
图4 overflow属性设置为hidden,CSS2.0规范中发生滚动时的处理
事实上各大浏览器厂商也都没有遵照CSS2.0来实现这一部分规范。取而代之,实现的是CSS2.1中的规范内容,即当元素B的overflow属性设置为非visible值时会触发BFC,元素B会创建自己的块级格式化上下文,并会被整体推向右侧,如图5所示。
图5 overflow属性设置为nonvisible,CSS2.1规范中的处理
备注 上面各图均来自于参考文献3
事实上,一些常见的其它布局技巧也都是基于上述的原理点,比如overflow属性非visible值可以用于清除浮动。如果一个面试者,能够比较清楚地讲出上面的各点,相信每个面试官心里面都会比较惊喜,上面只是自己的一些想法,可能会有些许的钻牛角尖,但单从这种对细节的钻研把控程度,候选人就一定不会太差,对候选人来说必然会有很大程度的加分。
上面只是针对两列布局这道题目一种方案的单方面探讨,这种方案有哪些优缺点等等都未提及,如果对每种方案都进行类似程度的拓展,将会发现这其中会涵盖很多前端知识点,所以看似简单的题目其实并不简单。越发觉得前端领域的水很深,伙伴们一起来努力探索实践吧!
本文解读的Vuex版本为2.3.1
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中的相关属性映射到组件中。
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的各种数据和状态了。
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))
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]里放入其回调函数。
前面说到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提供的辅助函数有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
时的一些总结,原书是基于 Java 的,这里将其中的一些个人认为实用性较强且容易与日常业务开发结合的一些原则重新进行整理,并参考了 clean-code-javascript 一文给出了一些代码实例,希望本文能够给日常开发编码和重构作出一些参考。
变量取名要花心**想,不要贪图方便,过于简略的名称,时间长了以后就难以读懂。
// bad
var d = 10;
var oVal = 20;
var nVal = 100;
// good
var days = 10;
var oldValue = 20;
var newValue = 100;
命名不要让人对变量的信息 (类型,作用) 产生误解。
accounts 和 accountList,除非 accountList 真的是一个 List 类型,否则 accounts 会比 accountList 更好。因此像 List,Map 这样的后缀,不要随意使用。
// bad
var platformList = {
web: {},
wap: {},
app: {},
};
// good
var platforms = {
web: {},
wap: {},
app: {},
};
用明确的意义去表述变量直接的区别。
很多情况下,会有存在 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获取单个商品
缩写要有个度,比如像 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';
可搜索的名称能够帮助快速定位代码,尤其对于一些数字状态码,不建议直接使用数值,而是使用枚举。
// bad
var param = {
periodType: 0,
};
// good
const HOUR = 0, DAY = 1;
var param = {
periodType: HOUR,
};
把类和函数做得足够小,消除对成员前缀的需要。因为长期以后,前缀在人们眼里会变得越来越不重要。
对于某些名称,在不同语境下可能代表不同的含义,最好为它添加有意义的语境。
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',
};
变量名取名多花一点时间,如果这一对象会在多个函数,模块中使用,就应该使用一致的变量名,否则每次看到这个对象,都需要重新去理清变量名,造成阅读障碍。
// bad
function searchGoods(searchText) {
getList({
keyword: searchText,
});
}
function getList(option) {
}
// good
function searchGoods(keyword) {
getList({
keyword: keyword,
});
}
function getList(keyword) {}
短小是函数的第一规则,过长的函数不仅会造成阅读困难,在维护的时候难度也会增加。短小,要求每个函数做尽可能少的事情,同时减少代码的嵌套和缩进,要知道,代码的嵌套和缩减同样会带来阅读的困难。
// 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;
}
}
函数应该做一件事情,做好这件事,只做这一件事。
如果函数只是做了该函数名下同一个抽象层上的步骤,则函数还是只做了一件事。当函数中出现另一抽象层级所做的事情时,则可以将这部分拆成另一层级的函数,因此缩小函数。
当一个函数可以被划分成多个区段时(代码块)时,这就说明了这个函数做了太多事情。
// 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(){}
一个函数中不应该混杂了多个抽象层级,即同一级别的步骤才放到一个函数中,因为通过这些步骤就能完整地完成一件事情。
回到之前提到变量命名的问题,一个变量或函数,其作用域余越广,就越需要一个有意义的名字来对其进行描述,提高可读性,减少在阅读代码时还需要去查询定义代码的频率,有些时候有意义的名字就可能需要更多的字符,但这是值得的。但对于小范围使用的变量和函数,可以适当缩短名称。因为过长的名称,某些时候反而会增加阅读的困难。
可以通过向下原则划分抽象层级
程序就像是一系列 TO 起头的段落,每一段都描述当前层级,并引用位于下一抽象层级的后续 TO 起头段落
- 如果要完成 A,需要完成 B,完成 C;
- 要完成 B,需要完成 D;
- 要完成 C,需要完成 E;
函数名明确了其作用,获取一个图表和列表,函数中各个模块的逻辑进行了划分,明确各个函数的分工, 拆分的函数名直接表明了每个步骤的作用, 不需要额外的注释和划分。在维护的时候, 可以快速的定位各个步骤, 而不需要在一个长篇幅的函数中需找对应的代码逻辑.
实际业务例子, 数据门户-流量看板-流量总览的一个获取趋势图和右边列表的例子。选择一个通过 tab 选择不同的指标,不同的指标影响的趋势图和右边列表的内容,两个模块的数据合并到一个请求中得到。流水账的写法可以将函数写成下面的样子,这种写法有几个明显的缺点:
根据向下原则
// 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);
},
switch语句会让代码变得很长,因为switch语句天生就是要做多件事情,当状态不断增加的时候,switch语句也会不断增加。因此可能把取代switch语句,或者将其放在较低的层级.
放在底层的意思,可以理解为将其埋藏到抽象工厂地下,利用抽象工厂返回内涵不同的方法或对象来进行处理.
函数的参数越多,不仅注释写得长,使用的时候容易使得函数参数发生错位。当函数参数过多时,可以考虑以参数列表或者对象的形式传入.
数据门户里面的一个例子:
// 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 个
函数应该取个好一点的名字,适当使用动词和关键字可以提高函数的可读性。例如:
一个判断是否在某个区间范围的函数,取名为 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){}
一个有副作用的函数,通常都是是非纯函数,这意味着函数做的事情其实不止一件,函数所产生的副作用被隐藏了,函数调用者无法直接通过函数名来明确函数所做的事请.
法律信息,提供信息的注释,对意图的解释,阐释,警示,TODO,放大(放大某种看似不合理代码的重要性),公共 API 注释
尽量让函数,变量变得刻度,不要依赖注释来描述,对于复杂难懂的部分才适当用注释说明.
喃喃自语,多余的注释(例如本来函数名就能够说明意图,还要加注释),误导性注释,循规式注释(为了规范去加注释,其实函数名和参数名已经可以明确信息了),日志式注释(记录无用修改日志的注释),废话注释
// bad
var d = 10; // 天数
// good
var days = 10;
数据门户-实时概况里面的一段代码,/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');
};
不要在注释里面加入太多信息,没人会看
非公用函数,没有必要加过多的注释说明,冗余的注释会使代码变得不够紧凑,增加阅读障碍
// bad
/**
* 设置表格表头
*/
function setTableHeader(){},
// good
function setTableHeader(){},
// bad
function doSomthing(){
while(!buffer.isEmpty()) { // while 1
// ...
while(arr.length > 0) { // while 2
// ...
if() {
}
} // while 2
} // while 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
}
// bad
/*************** Filters ****************/
///////////// Initiation /////////////////
// 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);
}
// 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);
},
});
// bad
function updateModule() {}
function updateFilter() {}
function reset() {}
function refresh() {
updateFilter();
updateModule();
}
// good
function refresh() {
updateFilter();
updateModule();
}
function updateFilter() {}
function updateModule() {}
function reset() {}
// bad
function onSubmit() {}
function refresh() {}
function onFilterChange() {}
function reset() {}
// good
function onSubmit() {}
function onFilterChange() {}
function refresh() {}
function reset() {}
// 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;
}
// bad
var v = a + (b + c) / d + e * f;
// good
var v = a + (b+c)/d + e*f;
// bad
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;
// good
var a = 1;
var sku = goodsInfo.sku;
var goodsId = goodsInfo.goodsId;
突然间改变缩进的规律,很容易就会被阅读习惯欺骗
// bad
if(empty){return;}
// good
if(empty){
return;
}
// bad
while(cli.readCommand() != -1);
app.run();
// good
while(cli.readCommand() != -1)
;
app.run();
对于一些较为复杂的组件或页面组件,需要定义很多属性,同时又要对这部分属性进行初始化和监听,像下面这段代码。在好几个大型的页面里面都看到了类似的代码,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();
},
})
针对上述这段代码代码,明显的缺点是:
这对这些可以作出一些改进:
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();
},
})
其实按照上面进行优化以后,代码的可读性是有所提高,但由于这是一个页面组件,代码行数极多,修改后方法变得更多了,仍然不便于阅读。所以,针对于这种大型的页面,更适当的做法是,将页面拆分为几个模块,将业务逻辑拆分,减少每个模块的代码量,提高可读性。而对于不可再拆分的组件或模块,如果仍然包含大量需要初始化的属性,上述例子就可以作为参考了。
本文整理的几个要点:
就算是经验老道的大神,也很难一遍就能写出简洁的代码,所以要勤于对代码进行重构,边写代码边修改。代码只有在经过一遍一遍修改和锤炼以后,才会逐渐地变得简洁和精致。
之前的项目由于业务的复杂度,都不大需要组件间的状态管理,加上复杂度仅一点点的工程一般轻量级的pubsub-js就能应付,所以一直没有使用redux的机会,借由最近的一个‘搭建系统’后台的这个项目,接入了redux,这里抛砖引玉,跟大家探讨下如何在低版本的regularJs中接入redux吧
一言不合先丢代码吧 regular-redux-lowversion
coming soon...
@(重构)[组件化|拆分]
代码重构是一个产品不断的功能迭代过程中,不可避免的一项工作。所谓重构
,是在不改变程序的输入输出即保证现有功能的情况下,对内部实现进行优化和调整。 每个开发人员从业生涯中,或多或少的做过重构工作。小到重写一个功能函数、业务组件,大到重构一个复杂功能模块或整站重构。
重构是需要花费一定成本和精力的,尤其是一个有各种历史遗留问题(用的老旧框架或工具)、又糅杂了各种业务逻辑(有些功能逻辑连需求方都未必清楚)、且可读性及维护性都很差的重要功能模块,比如考拉的下单页,不动,每次功能迭代的时候都有想撞墙的心,大动,又是不小的工作量,且有一定的风险。当然长痛不如短痛,长远来看,重构势在必行。
重构工作其实是随时都可进行的。当你觉得代码可读性变差、重用性及可维护性降低时,都应该有意识的去做重构。在大部分的项目中,以下将是重构的合理时间点:
当然在需求紧急的时候,是不适合做重构的。
以考拉下单页重构为例。
但凡重构,必须要对已有业务逻辑有较充分的了解,评估重构的影响面及可能的风险, 列出基本功能点(也可作为QA的测试回归点),保证重构后不要有功能遗漏,不改变现有功能。
如何了解已有业务逻辑?
在对业务逻辑有充分了解之后,可以进行功能拆分,把可重用的、功能独立的业务逻辑作为组件或公用模板提取,同时需要考虑组件之间的相互影响。拆分之后,可以多人并行开发,约定好外部调用组件的接口即可。
以下是下单页的功能模块拆分:
根据同步字段orderType,来确定是普通商品订单还是账号充值订单,并初始化对应的组件;根据同步信息,异步获取下单信息(含商品信息及结算信息);如果是普通商品订单,服务端会根据用户所选地址进行拆单,返回拆单后的商品和结算信息。按功能拆分后,组件及组件的嵌套关系如下:
下单页目录结构:
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 //弹窗领券
重构从来不是一次性行为,是我们需要不断进行的工作。多人维护以及不断的功能迭代之后,代码多多少少都会有优化的空间,所以在你看到不合理或任何值得重构的地方时,行动起来吧,去优化,不要等到重构的成本更大时再进行,因为那会更痛,不要问我怎么知道。
没有在Chrome应用商店web store上架发布的插件,如果没有添加到白名单里,下一次重启Chrome就会被禁用,而且无法手动启用,除非删掉重新添加。
可以通过添加白名单可以一劳永逸地解决这个问题。
下面我们以“将日报插件添加到白名单”为例,讲解步骤
mac系统下设置白名单比较简单,下载com.google.Chrome.mobileconfig,后,双击安装即可(过程中可能会要求输入系统管理员密码)
当然,为适配本插件,该文件内容已经更改:
1.文本编辑打开文件 com.google.Chrome.mobileconfig
2.找到 <array> 之下的 <string> 标签,然后在里面输入插件的id
3.保存之后,双击运行这个文件,期间可能会要求输入管理员密码,输入即可
4.重启浏览器
1.Win
+ R
(或者打开左下角运行)输入gpedit.msc
2.本地计算机策略 > 计算机配置 > 管理模板,右键管理模板,选择添加/删除模板。
3.点击添加,将下载的chrome.adm(下载地址:res/chrome.adm)添加进来。
4.添加模板完成后,找到经典管理模板(ADM),点击进入,选择Google > Google Chrome
5.打开后如下图所示,选择扩展程序,点击进入,配置扩展程序安装白名单
↓↓↓
6.点击下图中的显示按钮,将我们安装插件后的ID添加到那个值列表中,点击确定返回即可。比如我们的是 nldeakmmeccgpaiacgpmabnjaenfdbkn
id信息如下:打开chrome的插件管理界面(地址栏 chrome://extensions),找到这个插件
这里需要提一点,如果未开启开发者模式,可能会看不到这个id
最开始折腾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
文件夹下,文件名对应Hexo中的模板名,有index
,post
,page
,archive
,category
,tag
几种,对于普通的header + content + footer
的页面结构,header
和footer
往往是可以复用的,因此我们可以使用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
,js
和css
是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 |
首页一般是一些博文的摘要和一个分页器,通过Hexo的page
变量拿到页面的数据渲染即可,这里我们不直接在index.ejs
中写HTML结构,新建一个_partial/article.ejs
,将文章数据传给子模板渲染,然后再额外传入一个参数{index: true}
,对后面的post.ejs
和page.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>
文章模板和首页差不多,只是对应的是一篇具体的文章,所以就把文章传入,再额外传入{index: false}
告诉子模板不要按首页的方式去渲染就好了。就一行代码(因为都在子模板里 XD
//post.ejs
<%- partial('_partial/article', {index: false, post: page}) %>
我个人对Page模板其实是有点懵逼的,在我自己的实践中是添加about
(hexo new page "about"
)页面后,访问/about
会走分页布局,实际上这个页面对应的内容是/source/about
里的index.md
,也相当于对文章的渲染,因此我把Page模板也写成了和文章模板一样:
//page.ejs
<%- partial('_partial/article', {index: false, post: page}) %>
前面一共有三处共用了article模板,另外page和post的一样的,所以实际上只有两种情况:主页(index: true
)和非主页(index: false
)。对应的_partial/article.ejs
里只要判断这个值就可以正确渲染了,基本结构如下:
//_partial/article.ejs
<% if(index){ %>
//index logic...
<% }else{ %>
//post or page logic...
<% } %>
标签归档页内容很少,直接用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
。制作一个分页器,我们需要知道文章的总数和每页展示的文章数,然后通过循环生成每个link标签,还要根据当前页面判断link标签的active状态,但是在Hexo中这些都不用我们自己来做了!Hexo提供了paginator
这一辅助函数帮助我们生成分页器,只需要将文章总数site.posts.length
和每页文章数config.per_page
传入就可以生成了。
list_tags([options])
: 快速生成标签列表js(path/to/js)
, css(path/to/css)
用来载入静态资源,path可以是字符串或数组(载入多个资源),默认会去source
文件夹下去找。partial(path/to/partial)
引用字模板,默认会去layout
文件夹下找。知道了Hexo的渲染方式,我们就可以使用HTML标签+CSS样式个性化我们的主题了,推荐大家使用CSS预处理语言的一种来写样式,这样就可以通过预处理语言自身的特点让样式更灵活。
评论是很常用的功能,不如就直接在我们的主题里支持了,然后通过配置变量决定是否开启,评论区跟在文章内容下面,对于这种三方的代码块,最好也以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提供了多种语言的支持和多种皮肤,用法也很简单,载入文件后调用初始化方法,一切都帮你搞定,对于使用那种皮肤,喜好因人而异,我们干脆在主题的配置文件中做成配置项让用户自己选择:
//showonne/_config.yml
...other configs
# highlight.js
highlight_theme: zenburn
对应的layout.ejs
中:
样式文件通过CDN引入,因为不同皮肤对应不同的文件名,所以十分灵活。
当初是对应着landscape
照葫芦画瓢写的,最近回头来发现一些不合理的地方,所以就又改了改,也对应着写了这么一篇总结,接下来准备再把样式划分一下,对于颜色这类样式通过变量的方式提取出来,也变得可配置,能让主题更灵活一些。
了解辅助函数
模板
Hexo中的变量
Hexo主题列表
Hexo使用多说教程
How to use highlight.js
API规范包括路径命名规范、请求方式规范,请求参数规范、返回数据规范与特殊结构规范五部分;
路径的定义主要考虑以下因素:
为了方便快速定位前端文件,目前供应链前端组的页面文件路径是与url的路径完全对应的,如果url中存在变量id,则这种关联就无法实现;所以不建议使用restful的路径命名方式;
一般情况下,路径深度不超过3层,理想情况下都定义三层,如/quotation/manage/list,如果工程所有接口前都以/api或者/backend等开头,则从第二层开始计数,不超过3层,最大长度为/api/quotation/manage/list;
建议每层的命名不超过两个英文单词,两个单词时,命名格式为驼峰,如auditOrder,如果可以一个单词描述清楚就不要使用两个单词组合;
对于资源的操作,请使用以下常用的请求方式(括号中为对应的SQL命令):
POST请求时,请求参数可以为FORMDATA或者JSON格式,一般情况下以参数的个数以及复杂度判断使用哪种方式,如果参数都为非对象的简单类型(不包括数组,对象),并且个数小于5个,则使用FORMDATA, 后端使用@RequestParam读取,如果结构复杂,个数较多,则前端参数传JSON格式,后端使用@requestbody将参数映射为一个对象
包括两部分:http状态码与返回结果数据
{
“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
}
}
}
最早的前端,没有模块加载规范,
只能在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 的浏览器端模块化,有很明显的问题:
现如今,当打包这一环节被引入了前端工程化,CommonJS 以与服务端可以类库共用和 NPM(Node Package Manager) 这个后台的优势,成为了 es5 JavaScript 模块化的首选
本文内容,是从 require
函数这个切面,带读者了解到 CommonJS 模块化的原理,所有代码整理在 git 仓库
CommonJS 是一个旨在构建涵盖web服务器端、桌面应用、命令行app和浏览器JS的JS生态系统。
CommonJS 的标准符合 module1.1 规范,暴露给使用者的有三个全局变量:
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 函数了,逻辑中会依次判断是否有缓存,是否核心模块,加载相应文件以及加载执行模块并缓存。
可以看到没有具体实现,只有接口被定义出来,这种编码方式,同样可以借鉴到在其他的开发需求中:
这一步骤,主要是对上述过程需要的接口进行实现。
**上是一个分而治之的**,实现一个很复杂的东西比较困难,但是实现具体的功能要求,且在一定输入输出的限制下,每个人都能轻易的写出符合需求的算法,并进行调优。
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
函数传入相对路径时,解析成绝对路径
function getParent(pathname) {
return path.parse(pathname).dir
}
这个函数要做下面的事情
node_modules
内pacakge.json
的 main
属性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
}
}
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
}
这个函数是 CommonJS 模块化的核心体现,理解这个函数,对 module
和 exports
的实际使用也会有帮助
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
}
留下一些问题留给同学们思考:
require
的情况,由于当前模块已经执行完,会清空存储模块目录的 stack ,会出现相对路径查找失败的问题,如何解决?CommonJS
内部的加载流程是是否会陷入死循环,如果不会那会带来什么其他影响?(且听下回分解)本文是我参考一些 Node.js require 特性后,利用 JavaScript 简单的实现后的总结,主要提供一个思路。
本文中实现的 $require 函数存放在了 git 仓库 MockRequire 中
至此,我们粗暴地模拟了 Node.js 中的 require,多谢阅读,如有疑问,欢迎指出
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时代),奈何节点上还有事件绑定、节点数据修改等操作,想想都麻烦。
最终有两个可行方案:
既然运用了regular,如果非必须,还是不要直接操作dom节点的好。 所以最终选择了第二种。效果如下图:
这个需求的实现可以看到regular实现动画的一些局限性:
Table of Contents generated with DocToc
找到合适的工具包,开发nodejs命令行工具是很容易的
版本号是我当前使用的版本,可自行选择
分4步:
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"
}
#! /usr/bin/env node
这句话是一个shebang line实例, 作用是告诉系统运行这个文件的解释器是node;
比如,本来需要这样运行node ./file.js
,但是加上了这句后就可以直接./file.js
运行了
{
// 模块系统的名字,如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"
}
执行后,控制台里面会有以下输出:
/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 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
推荐使用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)
实际效果就是这些小图标:
这里可以找到各式各样的badges:
https://github.com/badges/shields
.travis.yml
这个文件, 并写好简单的配置language: node_js
node_js:
- "6"
"scripts": {
"test": "mocha"
}
在travis设置成功后,继续覆盖率的处理:
npm install istanbul coveralls --save-dev
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"
}
Portable Unix shell commands for Node.js
在nodejs里用unix命令行
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
本文是阅读Rendering:repaint,reflow,restyle、Repaint 、Reflow 的基本认识和优化和网站性能优化之后的笔记记录;
解析HTML以构建DOM树:
渲染引擎开始解析HTML文档,转换树中的html标签或js生成的标签到DOM节点,它被称为 -- DOM树。构建渲染树:
解析CSS(包括外部CSS文件和样式元素以及js生成的样式),根据CSS选择器计算出节点的样式,创建另一个树 —- Render树。布局渲染树:
从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标。绘制渲染树:
遍历渲染树,每个节点将使用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>
然后浏览器会计算每个元素的属性,确定如何展示页面;根据样式和js构建render树, 如下:
对比dom树与render树,可以发现少了head和被隐藏的元素;因为render树只包括可见的元素部分,具体的说,是从page的(0,0)坐标到(window.innerWidth, window.innerHeight)构成的矩形区域;
从上面的流程中,可以看到页面初始化时,至少有一次layout和paint;这个过程是不可缺少的,那么我们通常所提到的reflow和repaint是什么?
reflow: 当页面上有一些信息变化时,如大小、位置变化;浏览器可能会需要重新计算元素的展示样式,这个过程称为reflow;
repaint: 当元素的位置、大小等属性确定好后,浏览器会把这些元素重新绘制一遍,这个过程称为repaint;
Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。
以下行为会造成reflow或者repaint:
宽高 | 边距 | 位置 | 表现 | 边框 | 定位 | 字体 |
---|---|---|---|---|---|---|
width | padding | position | display | border | text-align | font-size |
height | margin | top | float | border-width | overflow-y | font-weight |
背景 | 边框 | 其他 |
---|---|---|
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 |
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// better
el.className += " theclassname";
把 DOM 离线后修改。如:
a> 使用 documentFragment 对象在内存里操作 DOM。
b> 先把 DOM 给 display:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后再把他显示出来。
c> clone 一个 DOM 节点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
不要把 DOM 节点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
尽可能修改靠近叶子的节点。当然,改变层级比较底的 DOM节点有可能会造成大面积的 reflow,但是也可能影响范围很小。
为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是会大大减小 reflow 。
千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
对于函数是一等对象的语言来说,下面两种典型的情况,会存在变量如何取值的问题。
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 a
和case b
的对比,我们可以进一步给出闭包更加广义层面的定义,JS中任何一个函数都是闭包,而不仅仅是case b
中那种教科书上面狭义的定义,全局函数也是闭包,是对全局作用域的一种闭合。
在工作中,我们经常碰到“文字垂直居中”的问题,需要居中的文字有单行,也有多行,在各种情况下的垂直居中分别该怎么解决?
在固定高度的容器中,单行文字垂直居中,只需要将文字的行距设置成容器的高度即可。
.vertical-line {
height: 100px;
line-height: 100px;
}
当盛放单行文字的容器高度不固定,上面的方法就不起作用了,此时就只能采用表格样式了。其实这种方法也适用于多行文字的情况。
div {
display: table;
}
p {
display: table-cell;
vertical-align: middle;
}
<div>
<p>
p的高度随div高度变化而变化,单行文字垂直居中
</p>
</div>
多行文字垂直居中相比单行文字来说,就有点复杂了,最先想到的,就是上面所说的容器高度不固定下的居中方法。
接下来,对于垂直居中,我们能想到的属性是vertical-align:middle,再配合display: inline-block,应该是可以的:
<div>
<p>
多行文字垂直居中<br>
多行文字垂直居中<br>
多行文字垂直居中
</p>
</div>
p {
display: inline-block;
vertical-align: middle;
}
然而写出来,并没有垂直居中的效果:
其实,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就是,允许浏览器向跨源服务器,发送XMLHttpRequest请求,并获取请求返回内容
CORS需要浏览器和服务器同时支持。
目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
检测 —— 客户端发出的请求是否被服务端允许
当客户端发送跨域请求时,浏览器会自发地先发送一个Method为OPTIONS的请求
请求和你发送请求的URL一致,且不带任何参数
OPTIONS请求通过,则发送真正的请求,否则将不再发送真正请求
客户端无需配置
服务端配置
String origin = request.getHeader("Origin"); //获取请求源
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); //允许上述Method的跨域请求
response.setHeader("Access-Control-Allow-Origin", origin); //允许当前源的跨域请求
跨域请求默认不发送Cookie以及TLS客户端证书信息,需要手动配置
客户端配置
xhr.withCredentials = true
服务端配置
response.setHeader("Access-Control-Allow-Credentials", "true");
1、OPTIONS请求不会发送Cookie
2、跨域请求发送的Cookie为请求域下的Cookie
异步请求有时会需要根据Cookie来判断是否登录,而options请求是不发送cookie的。
这时,就需要服务端仅针对options请求不做cookie校验。
服务端可以通过下面两点来进行判断
1、method == options
2、Access-Control-Allow-Headers 包含 X-Requested-With (判断是ajax,需要客户端添加)
设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。
比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。
下面介绍几种在JavaScript中常见的几种设计模式:
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。
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
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
策略模式的目的就是将算法的使用算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类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
代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。
常用的虚拟代理形式:某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例:使用虚拟代理实现图片懒加载)
图片懒加载的方式:先通过一张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');
使用代理模式实现图片懒加载的优点还有符合单一职责原则。减少一个类或方法的粒度和耦合度。
中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。
例如:现实生活中,航线上的飞机只需要和机场的塔台通信就能确定航线和飞行状态,而不需要和所有飞机通信。同时塔台作为中介者,知道每架飞机的飞行状态,所以可以安排所有飞机的起降和航线安排。
中介者模式适用的场景:例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发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);
};
装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。
例如:现有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执行引擎的主线程运行的时候,产生堆(heap)和栈(stack),程序中代码依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的队列(queue)中【即JavaScript执行引擎的主线程拥有一个执行栈/堆和一个任务队列】。
栈(stack) : 函数调用会形成了一个堆栈帧
堆(heap) : 对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。
队列(queue) : 一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,则从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着该消息处理结束。
首先,我们对图中的一些名词稍加解释:
【注:因为主线程从"任务队列"中读取事件的过程是循环不断的,因此这种运行机制又称为Event Loop(事件循环)】
下面我们通过setTimeout来看看单线程的JavaScript执行引擎是如何来执行该方法的。
console.log(1);
setTimeout(function() {
console.log(2);
},5000);
console.log(3);
//输出结果:
//1
//3
//2
基本上,一个完整的事件循环模型就讲完了。现在我们来重点关注一下队列。
异步任务分为两种:Macrotasks 和 Microtasks。
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 队列清空。
结论如下:
【注:一般情况下,macrotask queues 我们会直接称为 task queues,只有 microtask queues 才会特别指明。】
JavaScript 运行机制详解:再谈Event Loop
并发模型与Event Loop
【转向Javascript系列】从setTimeout说事件循环模型
异步 JavaScript 之理解 macrotask 和 microtask
本文的目录结构:
前端自动化测试的方向有:
UI回归测试通常采用的方法是像素对比:
初次运行的时候,会截图并作为baseline,后面再运行的时候,再生成截图,并与baseline比较,生成diff结果。
像素对比需要注意的事项:
考虑到我们主题是nek-ui组件库的测试,性能测试的部分,这里不做赘述。
我们的测试对象是NEK-UI组件库,这一部分分析了其他组件库的测试方法并选择了最终的测试方案。
RegularUI使用的测试方案是karma + mocha的黄金搭档
这种方式存在的问题:
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里做了什么呢?
在组件上绑定事件方法,然后模拟事件,判断方法是否被调用。
这种方式存在的问题:
分析完了2个组件库的测试方案,那么我们期望的测试方案应该包含什么呢?
基于此,我们最终选择了PhantomFlow。
PhantomFlow是基于决策树(decision tree)的ui test 框架,是对PhantomJS、CasperJS、PhantomCSS的包装。
PhantomFlow假定如果页面正常,那么在相同的操作下,每次页面所展现的应该是一样的。基于这点,使用者只需要定义一系列的操作流程和决策分支,然后利用PhantomCSS进行截图和图像对比。最后将测试结果在一个可视化报表中展现出来。
这里采用倒序的方式先来看一下PhantomFlow生成的测试报告,再介绍具体的使用:
这是PhantomFlow的母公司Huddle在他们实际的业务中使用的报告截图:
同时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提供了简单的方法来描述用户的操作流,具体的操作使用回调函数里的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。
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]):断言两个值严格相等。
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的生命周期等更多配置可以查阅这里
同样的使用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 是 HTML5 提供的一种本地存储,一般用户保存大量用户数据并要求数据之间有搜索需要的场景,当网络断开的时候,可以做一些离线应用,它比 SQL 方便,不用去写一些特定的语句对数据进行操作,数据格式为 json。
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){
// 当数据库改变是回调函数
};
注意这里的版本号只可以是整数
request.onupgradeneeded = function(event) {
var db = event.target.result;
var objectStore = db.createObjectStore("name", { keyPath: "id" });
};
// 增
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
db.close();
掌握了使用步骤之后,我们来结合它的优、缺点谈谈其使用场景。
参考: indexedDB API
升级范围为考拉前台所有站点,包括wap、web等工程,历史代码量大;主要是将原写死http协议的地方改造成相对协议,使其可同时兼容http及https服务;
页面资源包括几部分: 静态资源(js,css,ui图片,视频等等),动态内容修改
后端给数据,前端显示类型的内容:
由于部分运营配置的页面,都是直接配置了http链接,用户点击页面的链接很容易跳回到http,不能长期在https下使用,于是我们在页面的a标签点击时自动根据当前页面协议,调整a标签的href属性的协议为当前协议,来支持正常跳转。
原CSP规则限制了页面内部加载的资源,协议必须一直,但是https上线初期缓和内容难以避免,所以在CSP中增加了规则,允许浏览器加载混合内容(仅限图片、视频);
这里仅仅列出主要的步骤,细节还比较多:
by 渔樵
众所周知,优化一个页面的方法之一就是利用浏览器的缓存机制,在文件名不改变的情况下,使客户端不用频繁向服务端请求和重复下载相同的资源,节约了流量的开销。
现在前端的主要构建工具就是使用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
]
}
这里的hash是每次编译时所计算出来的一个值,因此当任何一个文件修改,hash都将会改变,而且所打包出来的文件名也将不一样了,这样对于需频繁发布的项目不是很友好,会造成每次有新版本,用户浏览器都将重新下载所有的文件,为了避免这种情况,webpack还提供了chunkhash(注:这里提取出的css文件hash值不一样,是因为使用的是extract-text-webpack-plugin插件,它提供了自己的一个contenhash,也是对于css文件建议的一种用法,保证了css有自己独立的hash,不会受到js文件的干扰)
将上述代码中的hash换成chunkhash后打包结果如下:
看到每个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
如下观点不一定适用于所有场景
欢迎小伙伴踊跃补充!!
跟什么一致啊,小姐姐请把话说清楚,然后明确到交互稿上吧;
一旦涉及到业务逻辑,要拉QA、后端一起评审,评估清楚影响范围,不怕花时间,就怕线上bug;
一定要求产品更新交互稿(方便QA测试、留下证据、以后产品开发QA都有可能查阅,用处非常大),更新之后再看一看交互,确保自己的理解跟产品是一致的;
无论前端写死还是后端传,要认识到「我比较懒」这个实际情况;
如果产品要求三端/两端一致,文案让后端拼好直接传就很合适了
一定要用NEI!没用过?没关系,就让小哥哥来手把手的教教你;
接口定义好之后,要先对一遍,有问题可以马上修改,避免开发过程中发现缺少字段,或字段不便于使用;
先定义接口再开发是原则问题。不单是口头定义,而是在NEI上详细的定下来。如果实在定不了,可以先定个v1版,后面再进行调整;
业务逻辑尽量封装到后端,前端模块尽可能通用,只负责根据后端数据进行渲染,尽量与业务逻辑解耦(尤其是已经/未来会通用的组件)。
在这个例子中,价格(sortType===5)是一种交互效果,其他的是另一种交互效果,那么要求后端通过新增一个字段,对这两种交互进行区分,前端就不需要关心sortType的具体含义了;
对于容易分辨归属(前后端)的问题,教会QA如何分辨它们,当他的分类能力提高,对所有人(尤其是自己)的效率提升都很有帮助。还可以教给他们如何通过查看同步/异步数据定位问题;
建议在互相不影响的情况下,A模块测出问题后,先测试其他模块/页面,最后一起交给开发来修改,这样会减少打断开发手头的工作,QA也可以集中精力测试;
搭车上线:代码提交到QA的另一个任务的分支中,一起上线
一般不建议搭车,单独提个任务拉分支很难么,还能提高表面上的业绩呢;
自测的任务在codereview/resolve之后,要尽快跟QA要测试资源测掉,避免QA以为你已经自测过,而将未测的代码上线;
同时:开发自提任务JIRA描述标准,需要有下面几块内容:
未完待续,欢迎补充!!
by tianyanan
XMLHttpRequest Level 2添加了一个新的接口FormData.在开发中使用后,觉得是form表单的加强版。可以异步的,灵活的发送数据,并且包括上传二进制文件
创建FormData的方式
var formData = new FormData();
formData.append('businessId', '3049');
formData.append('companyName', 'test');
<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'));
FormData.append()会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键,如果存在则会加在原有的值后面.即类似于数组的2号位
formData.append(name,value);
formData.append(name,value,fileName);
name: key(键)
value: 值
fileName: 当value为Blob或者file的时候,fileName为文件名
FormData.delete()会删除一对键值
formData.delete(name);
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
FormData.get()用于获取key所对应的value的首位值
var formData = new FormData();
formData.append('businessId', '3049');
formData.append('businessId', '3050');
console.log(formData.get('businessId'));
结果为
3049
FormData.getAll()用于获取key所对应的value
的全部值。如果对应的值超过一个,则会返回一个数组.
var formData = new FormData();
formData.append('businessId', '3049');
formData.append('businessId', '3050');
console.log(formData.get('businessId'));`
结果为
['3049','3050'];
FormData.has()用于检查是否含有指定的key。返回一个布尔值,true有,false即没有
var formData = new FormData();
formData.has('businessId'); //false
formData.append('businessId','3049');
formData.has('businessId'); //true
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
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']
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层叠上下文和层叠顺序中的一个马仔
当元素具有了层叠上下文,这个元素在z轴上就“高人一等”,离用户更近
层叠顺序是规则(在同一个层叠上下文中的元素,对应下面规则的序号越大,位置越高)
1通常是装饰属性;34是布局,5是内容——所以行内元素具有较高的层叠序号
谁大谁上:具有明显的层叠水平标识的时候(如z-index),层叠水平高的元素在上面(官大的压死官小的)
后来居上:层叠水平一致、层叠顺序相同时,DOM流后面的元素会覆盖前面的元素(长江后浪)
页面根元素具有层叠上下文——根层叠上下文
z-index:int 的定位元素
position:relative || absolute || fixed;
声明的定位元素,当z-index不是auto的时候,会创建层叠上下文
举栗子:定位元素divz-index:auto
,是普通元素,则内部元素就按z-index大小决定层叠顺序;但是当z-index: 0
,div就是层叠上下文元素。特性5和“后来居上”的准则作用下img的index失效。
其他CSS3属性
z-index不为auto的flex-item
满足上面两个条件的flex-item成为层叠上下文元素。看栗子
opacity值不为1
半透明元素具有层叠上下文 后面几个属性的例子都在这里哟
transform不是none
mix-blend-mode不是normal
类似于PS的混合模式
filter不是none
isolation:isolate
isolation是为mix-blend-mode而生的属性;mix-blend-mode混合默认z轴所有层叠在下面的元素,例子中box也被混合了。但是加上isolation:isolate,box就具有层叠上下文,层叠水平变高,不被混合
will-changed指定的属性值为上面任意一个
will-change是提高页面滚动、动画等渲染性能的属性 了解一下
-webkit-overflow-scrolling:touch
CSS3针对iOS的新属性,激活iOS平滑滚动
一旦普通元素具有层叠上下文,层叠顺序就会变高。其顺序要分两种情况:
定位元素层叠在普通元素之上,因为一旦成为定位元素,z-index自动生效,默认z-index:auto也可以看作z-index:0;所以会覆盖block、inline、float元素
层叠上下文元素的层叠顺序是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时所在列和行高亮呈现十字效果。参考网上的代码发现高亮有问题:鼠标只能往下移动或者左右移动,不能向上移动。
简化代码看栗子
transform:scale(1)
触发table具有层叠上下文;在::after加z-index: -1
。根据层叠顺序,负z-index元素 > 层叠上下文。
所以伪元素可以被用户看见但又不至于阻止鼠标的hover行为。(nice)
这是一个集合,主要有关 Javascript 模块化的原理说明
Masonry Layouts —— 瀑布流布局,核心是一个网格布局,每行包含的内容列表的高度是不可控的,并且,每个内容列表呈堆栈状排列。称作瀑布流,最关键的是——各堆栈间没有多余的间距差(将整个布局看做一个大的容器,最新进来的内容元素,永远在高度最短的那一列上)。具体如下图所示:
注:上图是 二次元社区GACHA 的插画频道,该瀑布流插件基于NEJ,交互要求是:每一列固定宽度,根据页面大小显示不同的列,页面宽度,重排,预加载一屏,滚动加载。设计的思路是:
absolute
定位,设置后续加入元素的 top
值为列堆栈数组中的最小值,同时设置它的 left
值为该索引对应列的 left 值top
值加元素本身的的高度)简化版应该是下面这样的:
但是,这样处理,表现层的东西依赖 javaScript
来处理,有点多余(当时确实是唯一的办法),能不能用样式来搞定呢?
首先,会想到 float
或者 inline-block
,但是它们都没办法很好的控制列表之间的间距。最终得到的效果就像下面这样:
那,除此之外,就没有单纯用 css
可以搞定的方法了吗?近几年,css
的技术更新频繁,出现了很多新的布局方法:multi-columns
、Flexbox
、Grid
。用上面提到的布局方法能否实现瀑布流呢?
multi-columns
产生之初,是用来实现文本多列排列,类似报纸、杂志的文本排列方式。multi-columns
的列布局方式跟瀑布流的有些类似:
三列:
四列:
猛一看很完美。但是,与使用 js 实现对比之下:
multi-columns
的元素是纵向排列的(在对元素先后次序有要求的情况下不理想)第一个问题,multi-columns
的这种布局方式,决定了只能纵向排列;对于第二个问题:
首先,只有在多列元素集含有块级元素、并且避免在元素内部断行并产生新列的时候,才会涉及到布局,上面的瀑布流例子,要是不避免在元素内部断行并产生新列,将会是这样的:
只有将属性 break-inside
设置为 avoid
,才会有块级元素的效果。
回到上面说到的空白区域的问题,multi-columns
的整体布局会受到几个因素的影响:
总体而言,优先级:自身属性 > 容器属性。
当容器的高度小于按照 column-count
布局后的列的高度,会发生元素断裂、跨列的现象;当容器的高度大于按照 column-count
布局后的列的高度,单个列的高度会由最优布局算法生成。
所谓最优布局算法,简单来说,就是自适应:
column-count
,如果元素(block)个数不超过 column-count
,布局成一横排;column-count
,首先在第一列增加 block 元素,而后以第一列的高度为标准,来填充后续各列(此时会发生列填充不满的情况);就会出现较大空白区域的情况。
所以,出现空白区域的根本原因是:multi-columns
布局的特点是按列布局、顺序计算、顺序排列,前面有较大空白区域,不会用后续元素去填补。
想要具体了解 multi-columns
布局,请参考:
multi-columns
瀑布流布局,适用于:
除了 multi-columns
, Flexbox
以及 Grid
也可以运用到瀑布流布局中来。未完,待续。。。
活动模板自上次改版以来,在不停的优化,这次又新改版了一次,主要是增加了自定义模块这个版块。先具体介绍下实现思路。
所有的活动模块数据通过前端JST模板渲染,如果是同步模块(比如图片,导航等)会直接渲染,如果是异步模块(商品,品牌,秒杀等)则滚动异步发请求加载。
优点:减轻服务端渲染模板的压力。滚动异步加载,分散服务端请求的压力。
缺点: 当活动模板越来越多时,模块文件耦合还是比较严重,嵌套较深,活动模板外的其他页面无法复用活动模块。模版的js文件包括所有依赖,打包后文件也较大。
模块的按需加载:考虑到修改成本和时间关系,以前老模版不做修改,只改新加的模版。每个模版包含模版自身的html,js,css等资源文件。打包后每个模版对应一个html文件。活动页面配置了哪些模块,就会动态去异步加载该模块对应的html文件。没有配置的模块,对应的资源文件不会被加载。
模块复用:一个模块的完整显示需要两个条件,一是该模块对应的html文件,二是该模块需要显示的数据。满足这两个条件,该模块就可以在任意页面上复用。第一个条件很容易满足,每个模块对应的html路径是固定的,第二个条件则是通过异步请求去获取数据,异步请求中需要模块id作为参数。这些数据都可以由后端同步给出。
{
"zoneKind": 23,
"locateId": "1803",
"zoneConfigMap": {
"mid": "123456458",
"moduleSrc": "/other/album/index.html", //模块对应的html文件名 固定
"moduleType": 20001 //模块类型值 固定
}
}
跟之前的模块相比,同步数据里少了很多字段,只保留了必要字段。大大减少了文件的体积。
<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 -->
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);
}
脚本包含模块的内部处理逻辑,模块脚本加载完后,什么时候需要发异步请求获取模块数据。之前考虑做滚动加载,在模块脚本内部添加scroll事件,监听该模块是否满足显示的条件,满足了就触发脚本内获取异步接口的方法,发现加了滚动事件后在客户端内滚动多次,会导致客户端崩溃。所以现在的处理是直接发了请求,后面等想到优化方案再改。发异步请求需要的参数通过容器节点的dataset来获取。
通过异步请求获取到数据后,要如何渲染视模块而定。目前有两种方式可供选择,regularjs和jst两种前端框架。如果是jst渲染,则需要在模块对应的html文件中添加字符串片段。使用regularjs开发时,可将模块尽量拆分成小组件,最终模块会由不同的组件组合,也提高了组件的复用性。所有模块的脚本都继承自一个公共的模块脚本文件,模块共用的方法会放在共用脚本里。
虽然说这种方案已经实现了,但还是有些问题,还有很多优化空间,这里简单整理下遗留的问题,待后续再修改。先实现功能,后优化性能。
总所周知,Javascript是运行在单线程环境中,也就是说无法同时运行多个脚本。假设用户点击一个按钮,触发了一段用于计算的Javascript代码,那么在这段代码执行完毕之前,页面是无法响应用户操作的。但是,如果将这段代码交给Web Worker去运行的话,那么情况就不一样了:浏览器会在后台启动一个独立的worker线程来专门负责这段代码的运行,因此,页面在这段Javascript代码运行期间依然可以响应用户的其他操作。
Web Worker 是HTML5标准的一部分,这一规范定义了一套 API,它允许一段JavaScript程序运行在主线程之外的另外一个线程中。
Web Worker 规范中定义了两类工作线程,分别是专用线程Dedicated Worker和共享线程 Shared Worker,其中,Dedicated Worker只能为一个页面所使用,而Shared Worker则可以被多个页面所共享,下文会重点介绍Dedicated 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控制台中运行。
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执行的上下文,与主页面执行时的上下文并不相同,最顶层的对象并不是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()方法,可以立即杀死 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作为目前最流行的浏览器,备受前端推崇,原因除了其对于前端标准的支持这一大核心原因之外,还有就是其强大的扩展性,
基于其开发规范实现的插件如今已经非常庞大,在国内也是欣欣向荣,
如天猫开发了大量的扩展,用于检测页面质量以及页面性能,淘宝开发了许多的扩展以供运营工具的优化等等。其强大性不言而喻。
这里我们来讲一下其插件开发
个别扩展可能会用到DLL和so动态库,不过出于安全以及职责分离的考虑,在后续的标准中,将会被舍弃,这里不再赘述
这里需要着重提一下,前端开发者喜欢的DevTools工具,在这里也能进行自定义
环境限制:基本上功能性操作都需要通过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.
下面以一个简单的例子来简述一下我们开发扩展过程中会遇到哪些问题
这个栗子主要用于去自定义用户页面的样式(比如此处为改滚动条样式),插入一个自定义的脚本到用户页面中执行(此处为输出一些简单信息)
./
├─ manifest.json //扩展的配置项
├─ Custom.js //自定义js脚本
├─ Custom.css //自定义css样式
├─ icon.png //扩展程序的icon
└─ popup.html //扩展的展示弹窗
例如,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出现在控制台调试时候。
/* 重置滚动条 */
::-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;}
/* ... */
<!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>
{
"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类型的扩展对应位置
浏览器打开 chrome://extensions/(或者‘更过工具->扩展程序’),左上角有一个 打包扩展程序
按钮
然后,将生成的*.crx文件拖到浏览器即可完成安装。
到这步插件其实已经差不多了,当然,这里的功能都比较简单,你可以自己尝试一些更高级的功能
由于自己DIY的扩展是没有发布到web app store的,所以下次打开浏览器时会被禁掉,解决方法见第二篇文章《chrome插件开发简介(二)——如何添“加浏览器扩展白名单”》
SVG即可缩放矢量图形 (Scalable Vector Graphics)的简称, 是一种用来描述二维矢量图形的XML标记语言. SVG图形不依赖于分辨率, 因此图形不会因为放大而显示出明显的锯齿边缘.
当我们需要使用多个icon的时候, 为了节省请求和方便管理, 通常会把icon合并到一个文件中, 在使用时再通过一定的方法从icon集合文件中取出所需的图形并显示. 目前使用得最多的应该就是我们所熟悉的CSS Sprite和Icon Font.
CSS Sprite的原理是将多个icon按一定规律整理到一个图片文件中, 使用时利用background-image
和background-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定义为图片字体, 在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">!</i>
由于使用的是字体, 因此可以通过color
, font-size
设置icon的样式. Icon Font拥有比CSS Sprite图片更小的文件体积, 维护也比图片更方便, 但是icon font通常只能使用单一的颜色, 字体文件生成也比CSS 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实现的.
它通过xlink:href
这个XML的attribute
引用指定的SVG symbol
, 在渲染时指定symbol
标签中的内容就会被渲染显示在页面当中. 这意味着, 如果无法直接对use
标签中的shadow dom进行访问和修改. 例如像use#rect
这样的选择器是无法生效的, 因此不能通过一般的css选择器对use
中图形不同的部分进行控制.
更多的时候, 我们通常都只需要改变图标的大小和颜色, 在SVG Sprite当中, 实现起来并不复杂.
通过改变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>
颜色由于不能直接对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>
利用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>
除此以外, 还有其他一些样式定义的形式, 更详细的内容可以阅读Styling SVG Content with CSS.
上文介绍的是使用内联svg, 但其实还可以使用外部svg的.
<svg class="icon">
<use xlink:href="/res/svg/icon.svg#icon-flag"/>
</svg>
然而这种方法不兼容IE9~10, 但如果非要使用外部svg的形式, 可以引入一个pollyfillsvg4everybody, 当运行在不支持外部svg的情况下, 这个pollyfill会通过异步请求加载svg委文件并将其注入到页面当中, 将引用方法转换成内联svg.
可以在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>
xlink:href
是使用了命名空间的XML特性, 如果是写在.html
页面的标签, 该特性能够正常被浏览器解析并完成svg渲染. 如果该svg变量是通过DOM API创建出来的话, 则需要使用特定的方法进行处理(SVG with USE tag not rendering).
即需要利用createElementNS
和setAttributeNS
方法在创建的同时声明命名空间.
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还能够给开发带来很大的便利的.
技术人总有一个很完美的假想,开发能力够强,就是真正能力强。
但现实并不是这么完美,人的精力有限,能真正投入的事情不多,况且工作中还有一大堆业务需求要展开。
**的火花不时能迸发出来,但是真正执行下去的有多少? 很多时候我们发现了能提高转化率,提高用户体验的点子,但是我们忙于业务需求开发,对于这类不急、没有需求方,没有人来催着上线,但是却暗藏金子的点子,最终都在忙忙碌碌中忽略了;
业务需求重要,但是提高转化率、提高效率的点子同样也重要,关键看我们技术人员如何去衡量;
技术人员的考核里面虽然没有转化率这项指标,而是完成了多少需求。完成产品提过来的需求,技术扮演的是支持部门的角色,就算完美收工,功劳也不算在技术部门,这只是技术部门完成了自己的本职工作。
而如果技术部门投入一部分时间去完成自己认为重要的事情,比如主导完成提高转化率的优化并取得好的效果,这个就体现出技术部门直接的价值来了;
从大的点来说,要求技术部门能与公司有着共同的目标,去推动公司蓬勃发展。有了共同的目标,技术人员才能真正主动去思考哪些东西是重要的,哪些事可以暂缓的;从而将时间都花在正确的方向上,只有这样,技术人员才能逐渐直接体现其价值,否则,技术会慢慢沦为运营、产品部门的开发工具。
现阶段大部分技术人员的感受是公司的销售数据的提高与否,与技术人员没有直接关联,只要按部就班完成新功能就好,但如果想成为一名杰出的员工,这是远远不够的。
沟通,技术人员更擅长于机器打交道,而不擅长去提出并推动完成自己的主张、想法; 完成业务开发,被动的沟通也能完成。而要进行新工具开发,解决流程问题等创新工作,更加考验个人的沟通能力,一个新的工具,我们需要考虑大家是否有这样的需求,其次要推广给大家使用,还需要接收不同人的反馈来不断完善,以让大家都接受。
所以,能力是最基本的要求,还要由足够的主动性,想法设法去推动事情往前走,最终达到自己期望的结果; 这个过程,正是权衡主次,表达想法,并让他人、上级领导理解并接受,持之以恒推进的过程。简单的来说,就是 技术能力+主动+执行力;
如今国内互联网风口一出,四面八方蜂拥而上的形势,点子已经不重要,重要的是谁的执行力强,做的更好,笑到最后;
by 渔樵
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 的相同点和不同点
v-once
而且这个指令会直接影响整个节点上的数据。<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
}
}
}
})
components: {
'component-name': Component,
}
Vue.component('my-component', {})
this.xxx
this.data.xxx
而是直接 this.xxx
v-bind:message="messgae"
<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);
},
})
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})
目前,一个典型的前端项目技术框架的选型主要包括以下三个方面:
- JS模块化框架。(Require/Sea/ES6 Module/NEJ)
- 前端模板框架。(React/Vue/Regular)
- 状态管理框架。(Flux/Redux)
系列文章将从上面三个方面来介绍相关原理,并且尝试自己造一个简单的轮子。
本篇介绍的是JS模块化。
JS模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的**分为CommonJS、AMD和CMD。有关三者的区别,大家基本都多少有所了解,而且资料很多,这里就不再赘述。
模块化的核心**:
根据上面的核心**,可以看出要设计一个模块化工具框架的关键问题有两个:一个是如何将一个模块执行并可以将结果输出注入到另一个模块中;另一个是,在大型项目中模块之间的依赖关系很复杂,如何使模块按正确的依赖顺序进行注入,这就是依赖管理。
下面以具体的例子来实现一个简单的基于浏览器端的AMD模块化框架(类似NEJ),对外暴露一个define函数,在回调函数中注入依赖,并返回模块输出。要实现的如下面代码所示。
define([
'/lib/util.js', //绝对路径
'./modal/modal.js', //相对路径
'./modal/modal.html',//文本文件
], function(Util, Modal, tpl) {
/*
* 模块逻辑
*/
return Module;
})
先不考虑一个模块的依赖如何处理。假设一个模块的依赖已经注入,那么如何加载和执行该模块,并输出呢?
在浏览器端,我们可以借助浏览器的script标签来实现JS模块文件的引入和执行,对于文本模块文件则可以直接利用ajax请求实现。
具体步骤如下:
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);
{
path: 'http://asdas/asda/a.js',
deps: [{}, {}, {}],
callback: function(){ },
status: 'pending'
}
//强依赖的例子
//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}
});
对于define函数,需要遍历所有的未处理js脚本(包括内联和外联),然后执行模块的加载。这里对于内联和外联脚本中的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 {
// 外联脚本的首先要监听脚本加载
}
}
}
};
上面就是对实现一个模块化工具所涉及核心问题的描述。完整的代码点我。
哈哈,标题很有吸引力吧。
言归正传,前几天在页面搭建工程下进行开发时,碰到了一个很困惑的问题,很容易复现,也很容易理解,就直接上代码吧:
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的本意就是如此设计,还没来得及仔细分析,如果有知道内情的同学,请发表下您的看法吧!
最近在做的种草项目,其中的管理后台采用了
vue.js
。为什么会选用vue呢,一是因为一个管理后台,不需要考虑兼容性、seo这些前台系统需要关心的点,后台系统的交互形式又特别适合做成一个单页应用,而vue不兼容IE8及以下版本,vue的route库很好的支持单页应用,适合我们的项目;再者,当然是因为vue是目前前端圈子很火很fashion的技术框架呀,截至目前有接近7k的fork,开发者们的参与热情堪称空前,技术社区氛围也很好,持续的版本更新,是一款被各大小项目考验过的稳定框架,岂有不用之理?
在此之前,我也没有过vue的项目实践经验,当初自己任性的敲定用vue的时候,内心还是很忐忑的,毕竟实际的开发时间也就一周。不过,学习一个新的框架或技术最好的方式,还是要运用到实际中,去学去写,才有更深的认识和体验。在项目时间很紧迫的情况下,会逼着自己快速的学习一个新框架,当然也因为要保证提测时间而没有做过的思考设计,在使用和理解这个框架的时候,也会遇到一些问题。
下面记录下,我在本项目中使用vuejs的过程。
在确认选用vue之前,我没有完整的看过教程或api文档,只是先去使用了他的命令行工具,本地跑起来一个demo工程,来揭开vue神秘的面纱,直观的初步认识下这个框架。简单体验完之后,我就决定用vue了,原因有三:
一个带热重载、保存时静态检查
的服务器,做到前后端完全分离开发。总之,使用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>
其中header
和side-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全称是Block Formatting Context,即块格式化上下文。它是CSS2.1规范定义的,关于CSS渲染定位的一个概念。要明白BFC到底是什么,首先来看看什么是视觉格式化模型。
视觉格式化模型(visual formatting model)是用来处理文档并将它显示在视觉媒体上的机制,它也是CSS中的一个概念。
视觉格式化模型定义了盒(Box)的生成,盒主要包括了块盒、行内盒、匿名盒(没有名字不能被选择器选中的盒)以及一些实验性的盒(未来可能添加到规范中)。盒的类型由display
属性决定。
块盒有以下特性:
display
为block
,list-item
或 table
时,它是块级元素 block-level;<li>
,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。display
的计算值为inline
,inline-block
或inline-table
时,称它为行内级元素;display:inline
的非替换元素生成的盒是行内盒;display
值为 inline-block
或 inline-table
的元素生成,不能拆分成多个盒;匿名盒也有份匿名块盒与匿名行内盒,因为匿名盒没有名字,不能利用选择器来选择它们,所以它们的所有属性都为inherit
或初始默认值;
如下面例子,会创键匿名块盒来包含毗邻的行内级盒:
<div>
Some inline text
<p>followed by a paragraph</p>
followed by more inline text.
</div>
在定位的时候,浏览器就会根据元素的盒类型和上下文对这些元素进行定位,可以说盒就是定位的基本单位。定位时,有三种定位方案,分别是常规流,浮动已经绝对定位。
position
为static
或relative
,并且float
为none
时会触发常规流;position: static
,盒的位置是常规流布局里的位置;position: relative
,盒偏移位置由这些属性定义top
,bottom
,left
andright
。即使有偏移,仍然保留原有的位置,其它常规流不能占用这个位置。top
,bottom
,left
及right
;position
为absolute
或fixed
,它是绝对定位元素;position: absolute
,元素定位将相对于最近的一个relative
、fixed
或absolute
的父元素,如果没有则相对于body
;到这里,已经对CSS的定位有一定的了解了,从上面的信息中也可以得知,块格式上下文是页面CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。
float
不为none
);position
为absolute
或fixed
);inline-blocks
(元素的 display: inline-block
);display: table-cell
,HTML表格单元格默认属性);overflow
的值不为visible
的元素;display: flex
或inline-flex
);但其中,最常见的就是overflow:hidden
、float:left/right
、position:absolute
。也就是说,每次看到这些属性的时候,就代表了该元素以及创建了一个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还有更多的特性:
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).
简单归纳一下:
这么多性质有点难以理解,但可以作如下推理来帮助理解:html的根元素就是<html>
,而根元素会创建一个BFC,创建一个新的BFC时就相当于在这个元素内部创建一个新的<html>
,子元素的定位就如同在一个新<html>
页面中那样,而这个新旧html页面之间时不会相互影响的。
上述这个理解并不是最准确的理解,甚至是将因果倒置了(因为html是根元素,因此才会有BFC的特性,而不是BFC有html的特性),但这样的推理可以帮助理解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。
大家都知道 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对象和then方法源码入手来探索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);
}
})
}
then方法接受两个参数,返回一个新的Promise对象,主要实现点在传入到Promise对象中的回调函数。
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,最主要的是搞懂Promise构造器的写法,和then函数的封装。
要用好Promise,最主要的是封装好要传入Promise中的匿名函数或者函数表达式。
有几个调用就有几个Promise对象,每个then方法传入的函数都是前一个Promise成功或者失败的异步回调。保存到每一个promise中的handlers中的函数都是闭包
第一个调用返回的值是后一个调用then方法传入函数的参数
定义promise与then的时候就像按次序放置的多米诺骨牌,当我们推动第一张骨牌的时候,也就是异步结果返回成功时,定义在then方法中的函数会开始链式调用。
Chrome Devtools 的颜色提取器 EyeDropper,用惯了 Chrome 的前端开发者并不陌生。
但它并不支持在页面中使用,想在页面中使用只能自己实现一个。
那么接下来就介绍一下如何自己实现一个 EyeDropper。
要实现 EyeDropper,必须先学习一下基本的色彩知识。
物品被光线照射并反射出来,被人的眼睛接收,进而传递到人脑中形成对「色彩」的认知,称之为人的「视觉效应」。
最最基本的颜色术语、通常用来表示物体的颜色。
当我们说红、绿、黄时,我们说的就是色相。将色相按照波谱顺序排列,首位相连形成环状则为「色相环」。虽然人们习惯将其分为七种颜色:红、橙、黄、绿、青、蓝、紫,但实际上的光谱应该是连续的。
指在特定的光照条件下颜色是如何呈现的,也就是色彩的鲜艳程度。
饱和度取决于颜色中含色成分和消色成分(灰色)的比例,即纯度最低的是灰色(无彩色)。高纯度表现为生机朝气,低纯度表现为厚重沉稳。
也被称作亮度,它是指颜色的明亮程度。
在任何颜色中添加白色,明度上升,添加黑色,明度下降。明度相差越远的两种颜色搭配,色彩之间的交界感就越明显,视觉上也就越清晰。
三者可以简单用下图综合表示:
理解了基础的颜色原理后就好办事了,拿 Chrome Devtools EyeDropper 分析:
结合上述色彩知识,加上这块分析就可以开始进入代码层面的设计。
在组件化大行其道的时代,以网易惯用的 Regular 进行组件化开发。
根据模块划分,划分基础的:
考虑简洁性,「取色器」及「调色板」不做实现。
① 该组件以色相组件选择的色相(hue)为背景,若是想直接使用 hue 作为 CSS 背景,需要使用 hsl(hue, saturation, value) 格式,设置为:
hsl(hue, 100%, 50%)
此时饱和度应设为 100%,因为饱和度为 0% 时为灰色,100% 时为原色。
而亮度是指颜色偏向于白色还是黑色。50%的亮度值表示颜色位于黑色和白色中间,这时颜色会基本保持原来的颜色不变。
② 同时利用线性渐变 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();
代码可以 戳我 查看,具体实现如下:
html ast解析算法的过程是将一段html字符串,解析成一个javascript对象。
<div>
<input r-value="{name}" />
<p>
<span></span>
<span style="display:block;">
描述信息2
<span>{name}</span>
</span>
</p>
</div>
整体实现分析:
<div>
,<p>
等,自闭合标签<input />
,闭合标签</div>
, </p>
等代码思路:
div
开标签,push进数组</span>
结束标签,表示后面的标签需要加入到p.children而不是span.children分布解析的动态demo,代码每一步都有详细的解释:
by Kaola nrz
这个动画的效果就是多个线条的高度发生变化,使用了两种写法(css,svg)来实现。
@-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;
}
使用animate元素来实现。原理一样,通过改变元素的高度。
<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>
git 地址:https://github.com/rainnaZR/svg-animations/tree/master/src/pages/step2/volumn
相信前后端分离这个词,早已流传甚广,大家一些自己的理解,但可能有些人的观点有稍许偏差:我们要搞 SPA,全AJAX,那才是前后端分离了。
我们来聊聊什么是前后端分离。
先来看一张WEB系统前后端架构模型图。
从图中可以清晰的看到,前后端的界限是按照浏览器和服务器的划分。那么我们经常会发现一些问题:
通常情况,模板层归属于前端,因为让后端人员来接触他们不擅长的样式和 js 交互是很蛋疼的事情。
那么,作为前端开发的我们在实际的开发场景中又会遇到以下问题:
出现影响开发效率的事情,就说明现有的模式存在问题,显然问题的解题思路需要我们重新思考“前后端”的定义。此时,前后端分离的概念便应运而生,目的是将前后端开发人员的合作方式调节到大家都尽可能舒适的姿势。
全称 Single Page Application,使用前端路由的方式代替后端的 controller,并使用前端模板代替后端的模板引擎渲染,使用 restful api 实现与后端的数据交互。
在这个方案中,前后端的交互被转换成了纯粹的 http 方式的 JSON 串交互。
综上,SPA 是一个可以解决前后端分离的有效方案,对于无 SEO 要求的项目大可以尝试。
顾名思义,开发阶段的前后端分离,需要依赖工具实现,通常把这个工具叫做 Mock Server(如笔者所开发的一款 Mock Server -- Foxman)。
这里我们需要抽象以上操作为两个函数,利于理解:
我们将一个项目开发划分为三个阶段:接口定义,开发,联调。正好可以和我们 “Mock”、 “Proxy” 两个工具契合。
让我们通过实际的场景来表述这种前后端的合作方式。
我们接到一个需求,实现某个功能。在我们理清楚具体的功能之后,应该与后端定义接口及返回,包括:
在制定完接口后,我们需要按照 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,欢迎交流和使用。
这个模式自然时结合了前面的 Proxy。大家都知道 Node.js Server 里面强调一个 中间件的 概念,对应到设计模式的职责链模式。即只处理自己能处理的情况,否则,继续往后传递,直到被处理。
这个方案中,Proxy 作为了中间件体系中的最后一层,用以转发请求,而在这之前依次是中间件的错误处理、静态资源的响应、路由拦截(routers) 等等。
Node.js 拥有一定的接口控制能力,如处理 PC/Mobile 的响应式渲染,或是 Server-Side-Render 等等。
还是那句话,所有的前后端分离方案,都是为了前后端开发人员的合作方式调节到大家都尽可能舒适的姿势。
那么一个不错的实践是,我们可以将 (Mock && Proxy) 与 Node.js 中间层 两个方案结合:
未完待续。。。
by 君羽
在优惠券二期的任务中,需要进行整体重构。 在和yubaoquan讨论之后,我们希望的技术栈是这样:
上面这些问题不是很大。
有个比较麻烦的地方就是,前端和后端的连接方式有所改变,详情请看下文。
此项目是非spa项目,所以会有多个页面。
比如,根目录下有2个页面:
/WEB-INF/ftl/pageA.ftl
/WEB-INF/ftl/pageB.ftl
pageA中引入js/pageA/entry.js
,entry.js
再引入业务js。pageB同理。
后端controller中连接到pageA的方式很简单,只要return "/WEB-INF/ftl/pageA"
这样即可。
备注:
ftl文件:是freemarker(后端用的模板引擎)的模板文件,可以理解为html页面
与之前的不同就是, 每个entry.js都要经过一次webpack打包,再重新注入到ftl中,这里会面临2个问题:
项目不断扩大,添加了新页面,配置又要重新写一次吗?迎面而来第3个问题:
由于之前接触过htmlwebpackPlugin
插件,可以动态的生成html文件,并将entry.js插入到html文件中。那么生成ftl文件也应该没问题,查看了文档之后,确实只需要修改filename就行了。
在github上找到了答案
简单解释下:
大家熟悉的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
由于遍历文件夹,返回文件是一个异步操作,所以模块导出需要导出一个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));
});
});
需要设置publicPath
属性为根目录(请根据实际需求修改):
output: {
publicPath: '/'
}
ExtractTextPlugin: 可以把样式模块提取成单独的css文件
CommonsChunkPlugin: 可以把共用的模块提取成单个文件,不同入口之间可以通用
chunkhash: 单独给文件创建hashes,缓存文件
目前项目已上线,中间很多坑都踩过,其中很多问题都是yubaoquan解决的,合作愉快(握手)。webpack.config.js
代码在这code here,供大家参考,有什么问题可以一起讨论
by Kaola nrz
前后端分离目前算是一种共识了,特别是前端 MV* 框架的兴起,前端更倾向于按照框架写代码,之后打包再交由后端 render。因此,如果传统的根据路由打包为多个入口会比较蛋疼,单一入口的路由管理算是 SPA 的一个关键部分了,而这里离不开 history。
比较有意思的是,互联网根基不应该是光纤或者电脑之类,应该是 URL,任何互联网资源都有唯一的 URL(说的不严谨,请不要深究,下同),所以可以说 URL 是互联网资源的身份证。在资源跳转的时候可以轻松地前进后退,因为浏览器里有个 history 结构管理着这一切。history 的实现有点类似于栈,点击链接是 push,后退是 pop,但是也不是栈,因为你也可以前进,或者直接手输 URL 进行 replace。
但是,不管是如何转换(push/pop/replace/...)说白了都是对资源的一种替换(这里资源我们暂时当作 HTML 页面吧)以及资源依赖的一些其它资源(可以理解为 js/css/images 等),每次页面的切换(URL变动)都是资源的加载过程。这逻辑上当然没有什么问题,但是如果切换的两个页面有 90% 的内容和资源都一样呢,在没有正确缓存的情况下,全局刷新是对资源的一种极大的浪费。
从浏览器角度来说,它是没有优雅的方式去判断两个页面之间的差异然后只加载新内容的,因为页面的差异情景太多,这锅不应该让浏览器背。但是,换种思路呢,如果 history 的切换变得可编程,这一切就都不是事了,因为写代码的人肯定很清楚差异是什么。
为了更好的对 history 进行编程,著名的 history 对此做了个封装,其提供了三种形式的 history,对外暴露出相同的基础操作,底层实现是不同的,适用场景也不一样:
browserHistory
是基于原生 HTML5 history API 的封装,这更接近我们大多数时候看得 URL,用 path 来区分,这更利于 SEO,并且在使用(如分享,收藏等)的时候坑更少。虽然对浏览器版本有点要求,但是仍然是首选hashHistory
可以兼容性更高,这个更像我们平时说的单页,因为它的 path 不变,用的是 #
号去区分资源,如:example.com/#/some/path
memoryHistory
并不是为浏览器准备的,因为它的 url 并不是从地址栏读取的,其实根本没有地址栏的概念,自身在内存里模拟一个 history 环境,主要用于 React-Native 里说完 history 了,可以说 SPA 了,使用上 SPA 体验并不会是单页的感觉,大多也是很多页,甚至很多你根本感知不出是否是单页,因为 SPA 更多的是指实现方式的一个入口,而不是表现上的。引入 MV* **后,这里页会被当作模块,所以如果可以控制在不同的 URL 时展示不同的模块,就可以实现一个单页系统。
所以,一个 SPA 可以简单到:
return `{#if path = '/home'}<Home />{#else}<Error />{/if}`
表现上就是访问 /home
就显示首页,访问其它页面就显示错误页面,这不就是我们过去几十年用浏览器的方式吗?所以我说 SPA 并不是表现上的单入口。
由于 hashHistory
放弃了用 path 去定位资源,所以其有着天然 SPA 的基因,使用上并不会有太多疑问。browserHistory
应用于单页系统的时候需要配置下服务端,可以对任何 path 都返回同一个页面。接下来的过程可以想象下,主要三种场景:
popstate
等事件,应用根据事件更新 history 栈(不精确)信息(地址栏 URL 也会被改变),最后渲染出正确组件<a href="">
链接,会把点击事件转为设置 history 操作,同样 URL 跟随改变,最后渲染出目标页面browserHistory
,除非你无服务端的控制权(如把静态页面放在 CDN 上)A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.