标签前端下的文章

Jerry Bendy 发布于 02月23, 2017

Yarn vs npm: 你需要知道的一切

Yarn 是 Facebook, Google, Exponent 和 Tilde 开发的一款新的 JavaScript 包管理工具。就像我们可以从官方文档了解那样,它的目的是解决这些团队使用 npm 面临的少数问题,即:

  • 安装的时候无法保证速度/一致性
  • 安全问题,因为 npm 安装时允许运行代码

阅读全文 »

Jerry Bendy 发布于 11月07, 2016

【译】巧用 CSS 变量实现自动前缀

原文 http://lea.verou.me/2016/09/autoprefixing-with-css-variables/

最近,当我在制作 markapp.io 这个小网站的时候,我想出一个巧妙的技巧用在 CSS 变量上,我们可以天然地使用它们的动态本质。让我们看一下当你想使用一个属性,但是这个属性有不同的版本,一个无前缀的标准版和一个或多个有前缀的版本的情形。在这里我举一个例子,比如我们使用clip-path目前需要同时使用无前缀的版本和一个-webkit-前缀的版本,我的这个方法可以适用于这种情况,不管这个 CSS 属性是什么,有多少种前缀,只要它不论什么前缀的值都是同样的就可以。

第一步是在所有元素上定义一个 --clip-path 属性,值是 initial。这样阻止这个属性在每一次使用的时候被继承,而由于 * 没有特异性,任何使用了 --clip-path 的声明都能被覆盖。然后你定义所有不同版本的属性名,值为 var(--clip-path)

* {
  --clip-path: initial;
  -webkit-clip-path: var(--clip-path);
  clip-path: var(--clip-path);
}

这样,在我们需要使用 clip-path 的地方,我们都用 --clip-path 替代,它可以正常工作:

header {
  --clip-path: polygon(0% 0%, 100% 0%, 100% calc(100% - 2.5em), 0% 100%);
}

甚至连 !important 都可以正常工作,因为它影响 CSS 变量的级联。此外,如果由于某些原因你想要明确地设置 -webkit-clip-path,你也可以正常写,这也是因为 * 是零特异性(意味着是最低优先级的选择符——译者注)。这种用法的主要缺点是要求浏览器必须同时支持你使用的属性和 CSS 变量才能正常工作。不过,除了 Edge 之外,所有的浏览器都支持 CSS 变量Edge 也在准备支持它。除了上面这个问题,我没发现其它的缺点了(除了显然必须使用与标准属性有些不同的属性之外),但是如果你有发现别的坑,请在评论里面留言让我知道!

我想,CSS 变量的巧妙用法还有许多有待发掘。我想要知道这个技巧是否能改进一下让它支持自定义 css 属性全写,比如将 box-shadow 分开成 --box-shadow-x--box-shadow-y 等等,但是目前我还没想到好办法。你有好办法吗?😉


本文转自:十年踪迹的博客 英文原文地址:http://lea.verou.me/2016/09/autoprefixing-with-css-variables/

阅读全文 »

Jerry Bendy 发布于 09月07, 2016

【分享】手机淘宝的flexible设计与实现

看到小黑的文章 关于webapp中的文字单位的一些捣腾 感觉很赞。尤其是,他提到了手机淘宝的meta,所以觉得要讲讲我们这方面的一些实践。

手机淘宝从2014年中开始,全面推行flexible设计。什么叫flexible呢?其实flexible就是responsive的低端形态和基础。对我们来说,最直观的感受就是,在超宽屏幕上,网页显示不会两边留白。以前pc时代大家经常讲的流体布局,其实就是一种flexible design。只不过,流体的表述角度是实现,flexible的表述角度是结果,为了跟高大上的responsive保持一致,我们这里使用了flexible这个说法。

讨论方案之前,需要先了解三个关键概念:

  • 单位英寸像素数(Pixel Per Inch,PPI):现实世界的一英寸内像素数,决定了屏幕的显示质量
  • 设备像素比率(Device Pixel Ratio,DPR):物理像素与逻辑像素(px)的对应关系
  • 分辨率(Resolution):屏幕区域的宽高所占像素数

当我们决定不同屏幕的字体和尺寸的单位时,屏幕的这几个参数非常重要。

场景1——Resolution适配

一张banner图片,当你面对不同的屏幕时你希望它的行为是怎样的?

在这个场景中,我们主要需要面对的是分辨率适配问题,考虑到多数网页都是纵向滚动的,在不同的屏幕尺寸下,banner的行为应该是总是铺满屏幕宽度以及总是保持宽高比

最自然的思路是使用百分比宽度,但是假如使用百分比宽度,即width:100%,我们又有两种思路来实现固定宽高比:一是利用img标签的特性,只设宽度等图片加载完,这种方法会导致大量的重排,并且非固定高度会导致懒加载等功能难以实现,所以果断放弃;二是使用before伪元素的margin撑开高度,这种方法是比较干净的纯css实现,但是不具备任何复用性而且要求特定html结构,所以也只好放弃了。

于是,剩下最合适的办法是使用其它相对单位,本来最合适的单位是vw,它的含义是视口宽度,但是这个单位存在严重的兼容问题,所以也只好放弃。

最后我们只好配合js来做,硬算也是一条路,但是同样不具备任何可复用性,最终我们选择了rem,我们用js给html设置一个跟屏幕宽度成正比的font-size,然后把元素宽高都用rem作为单位。

这是我们目前的线上方案了,它是一个近乎Hack的用法,已知的问题包括:

  • 某些Android机型会丢掉rem小数部分
  • 占用了rem单位
  • 不是纯css方案

场景2——PPI适配

一段文字,当你面对不同的屏幕时你希望它的行为是怎样的?

显然,我们在iPhone3G和iPhone4的Retina屏下面,希望看到的文字尺寸是相同的,也就是说,我们不希望文字在Retina屏尺寸变小,此外,我们在大屏手机上,希望看到更多文字,以及,现在绝大多数的字体文件,是自带一些点阵尺寸的,通常是16px和24px,所以我们不希望出现13px、15px这样的奇葩尺寸

这样的特征决定了,场景1中的rem方案,不适合用到段落文字上。所以段落文字应该使用px作为单位,考虑到Retina,我们利用media query来指定不同的字体,考虑到dpr判定的兼容性,我们用宽度替换来代替:

.a {
    font-size:12px
}
@media (min-width: 401px){
    .a {
        font-size:24px
    }
}

另一种场景,一些标题性文字,希望随着屏幕宽而增大的,我们可以仍然使用rem作为单位。超过35px(个人直观感受)的文字,已经不用太考虑点阵信息了,靠字体的矢量信息也能渲染的很好。

场景3——DPR匹配

一个区块,设计稿上有1像素边框,当你面对不同的屏幕时你希望它的行为是怎样的?

这个场景,需求很简单,设计师希望在任何屏幕上这条线都是1物理像素

好吧,当然这个问题的答案不是写1px那么简单。在retina屏下面,如果你写了这样的meta

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

你将永远无法写出1px宽度的东西,除此之外,inline的SVG等元素,也会按照逻辑像素来渲染,整个页面的清晰度会打折。

所以,手机淘宝用JS来动态写meta标签,代码类似这样:

var metaEl = doc.createElement('meta');
var scale = isRetina ? 0.5:1;
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
    document.documentElement.firstElementChild.appendChild(metaEl);
} else {
    var wrap = doc.createElement('div');
    wrap.appendChild(metaEl);
    documen.write(wrap.innerHTML);
}

结语

总的来说,手机淘宝的flexible方案是综合运用rem和px两种单位+js设置scale和html字体。

这些JS的内容,可以在我们开源的库ml中找到:

https://github.com/amfe/lib.flexible


分享自:前端乱炖(http://www.html-js.com/article/Like-the-winter-flexible-design-and-implementation-of-the-mobile-phone-Taobao-cold

阅读全文 »

Jerry Bendy 发布于 08月30, 2016

【译】十个可以使用 ES6 代替的 Lodash 特性

Lodash 应该算是目前在 npm 上被依赖的最多的包了吧,但是如果你使用 ES6,也许你不再需要它。在这篇文章中,我们将尝试使用一些 ES6 的新特性来解决几种常见的问题。

1. Map, Filter, Reduce

这些方法使转换数据变得轻而易举,而且非常通用。我们可以使用 ES6 的箭头函数语法,帮助我们用更简短的方式代替 Lodash 的语法。

_.map([1, 2, 3], function(n) { return n * 3; });
// [3, 6, 9]
_.reduce([1, 2, 3], function(total, n) { return total + n; }, 0);
// 6
_.filter([1, 2, 3], function(n) { return n <= 2; });
// [1, 2]

// becomes

[1, 2, 3].map(n => n * 3);
[1, 2, 3].reduce((total, n) => total + n);
[1, 2, 3].filter(n => n <= 2);

不仅如此,如果你使用 ES6 的 polyfill,我们还可以使用 findsomeevery 以及 reduceRight 等方法。

2. Head & Tail

解构语法 允许我们轻而易举的获取一个列表的头部或尾部,不需要依赖任何函数。

_.head([1, 2, 3]);
// 1
_.tail([1, 2, 3]);
// [2, 3]

// becomes

const [head, ...tail] = [1, 2, 3];

也可以使用类似的方法达到 initiallast 的效果。

_.initial([1, 2, 3]);
// -> [1, 2]
_.last([1, 2, 3]);
// 3

// becomes

const [last, ...initial] = [1, 2, 3].reverse();

如果你介意 reverse 改变了原来的数组,还可以使用另一个解构将原数组复制一份。

const xs = [1, 2, 3];
const [last, ...initial] = [...xs].reverse();

3. Rest & Spread

restspread 函数允许我们定义可以接收可变数量参数的函数。使用 ES6 可以更完美的支持 rest 和 spread。

var say = _.rest(function(what, names) {
  var last = _.last(names);
  var initial = _.initial(names);
  var finalSeparator = (_.size(names) > 1 ? ', & ' : '');
  return what + ' ' + initial.join(', ') +
    finalSeparator + _.last(names);
});

say('hello', 'fred', 'barney', 'pebbles');
// "hello fred, barney, & pebbles"

// becomes

const say = (what, ...names) => {
  const [last, ...initial] = names.reverse();
  const finalSeparator = (names.length > 1 ? ', &' : '');
  return `${what} ${initial.join(', ')} ${finalSeparator} ${last}`;
};

say('hello', 'fred', 'barney', 'pebbles');
// "hello fred, barney, & pebbles"

4. Curry (柯里化)

Without a higher level language such as TypeScript or Flow, we can’t give our functions type signatures which makes currying quite difficult. When we receive curried functions it’s hard to know how many arguments have already been supplied and which we will need to provide next. With arrow functions we can define curried functions explicitly, making them easier to understand for other programmers.

function add(a, b) {
  return a + b;
}
var curriedAdd = _.curry(add);
var add2 = curriedAdd(2);
add2(1);
// 3

// becomes

const add = a => b => a + b;
const add2 = add(2);
add2(1);
// 3

These explicitly curried arrow functions are particularly important for debugging.

var lodashAdd = _.curry(function(a, b) {
  return a + b;
});
var add3 = lodashAdd(3);
console.log(add3.length)
// 0
console.log(add3);
//function wrapper() {
//  var length = arguments.length,
//  args = Array(length),
//  index = length;
//
//  while (index--) {
//    args[index] = arguments[index];
//  }…

// becomes

const es6Add = a => b => a + b;
const add3 = es6Add(3);
console.log(add3.length);
// 1
console.log(add3);
// function b => a + b

If we’re using a functional library like lodash/fp or ramda then we can also use arrows to remove the need for the auto-curry style.

_.map(_.prop('name'))(people);

// becomes

people.map(person => person.name);

5. Partial

Like with currying, we can use arrow functions to make partial application easy and explicit.

var greet = function(greeting, name) {
  return greeting + ' ' + name;
};

var sayHelloTo = _.partial(greet, 'hello');
sayHelloTo('fred');
// "hello fred"

// becomes

const sayHelloTo = name => greet('hello', name);
sayHelloTo('fred');
// "hello fred"

It’s also possible to use rest parameters with the spread operator to partially apply variadic functions.

const sayHelloTo = (name, ...args) => greet('hello', name, ...args);
sayHelloTo('fred', 1, 2, 3);
// "hello fred"

6. Operators

Lodash comes with a number of functions that reimplement syntactical operators as functions, so that they can be passed to collection methods.

In most cases, arrow functions make them simple and short enough that we can define them inline instead.

_.eq(3, 3);
// true
_.add(10, 1);
// 11
_.map([1, 2, 3], function(n) {
  return _.multiply(n, 10);
});
// [10, 20, 30]
_.reduce([1, 2, 3], _.add);
// 6

// becomes

3 === 3
10 + 1
[1, 2, 3].map(n => n * 10);
[1, 2, 3].reduce((total, n) => total + n);

7. Paths

Many of Lodash’s functions take paths as strings or arrays. We can use arrow functions to create more reusable paths instead.

var object = { 'a': [{ 'b': { 'c': 3 } }, 4] };

_.at(object, ['a[0].b.c', 'a[1]']);
// [3, 4]
_.at(['a', 'b', 'c'], 0, 2);
// ['a', 'c']

// becomes

[
  obj => obj.a[0].b.c,
  obj => obj.a[1]
].map(path => path(object));

[
  arr => arr[0],
  arr => arr[2]
].map(path => path(['a', 'b', 'c']));

Because these paths are “just functions”, we can compose them too.

const getFirstPerson = people => people[0];
const getPostCode = person => person.address.postcode;
const getFirstPostCode = people => getPostCode(getFirstPerson(people));

We can even make higher order paths that accept parameters.

const getFirstNPeople = n => people => people.slice(0, n);

const getFirst5People = getFirstNPeople(5);
const getFirst5PostCodes = people => getFirst5People(people).map(getPostCode);

8. Pick

The pick utility allows us to select the properties we want from a target object. We can achieve the same results using destructuring and shorthand object literals.

var object = { 'a': 1, 'b': '2', 'c': 3 };

return _.pick(object, ['a', 'c']);
// { a: 1, c: 3 }

// becomes

const { a, c } = { a: 1, b: 2, c: 3 };

return { a, c };

9. Constant, Identity, Noop

Lodash provides some utilities for creating simple functions with a specific behaviour.

_.constant({ 'a': 1 })();
// { a: 1 }
_.identity({ user: 'fred' });
// { user: 'fred' }
_.noop();
// undefined

We can define all of these functions inline using arrows.

const constant = x => () => x;
const identity = x => x;
const noop = () => undefined;

Or we could rewrite the example above as:

(() => ({ a: 1 }))();
// { a: 1 }
(x => x)({ user: 'fred' });
// { user: 'fred' }
(() => undefined)();
// undefined

10. Chaining & Flow

Lodash provides some functions for helping us write chained statements. In many cases the built-in collection methods return an array instance that can be directly chained, but in some cases where the method mutates the collection, this isn’t possible.

However, we can define the same transformations as an array of arrow functions.

_([1, 2, 3])
 .tap(function(array) {
   // Mutate input array.
   array.pop();
 })
 .reverse()
 .value();
// [2, 1]

// becomes

const pipeline = [
  array => { array.pop(); return array; },
  array => array.reverse()
];

pipeline.reduce((xs, f) => f(xs), [1, 2, 3]);

This way, we don’t even have to think about the difference between tap and thru. Wrapping this reduction in a utility function makes a great general purpose tool.

const pipe = functions => data => {
  return functions.reduce(
    (value, func) => func(value),
    data
  );
};

const pipeline = pipe([
  x => x * 2,
  x => x / 3,
  x => x > 5,
  b => !b
]);

pipeline(5);
// true
pipeline(20);
// false

Conclusion

Lodash is still a great library and this article only offers a fresh perspective on how the evolved version of JavaScript is allowing us to solve some problems in situations where we would have previously relied on utility modules.

Don’t disregard it, but instead—next time you reach for an abstraction—think about whether a simple function would do instead!


via sitePoint

阅读全文 »

Jerry Bendy 发布于 08月30, 2016

【译】React on ES6+

本文由冰翼博客翻译自 babelJs.io 原文作者: Steven Luscher (Github

在重新设计 Instagram Web 的最近一年里,我们享受到很多使用 ES6+ 新特性写 React 组件的好处。下面我整理了一些可以方便写 React 应用语言特性,相信这会使工作变得更轻松愉快。

迄今为止我想在写组件时最能直观看到的变化就是 ES6+ 类的使用。关于 ES6 的类定义语言可 参考这里。现在,我们可以写一个继承自 React.Component 的类来取代 React.createClass 的写法。

class Photo extends React.Component {
  render() {
    return <img alt={this.props.caption} src={this.props.src} />;
  }
}

当然,你会发现一些细小的变化 —— 在定义类的时候可以使用一些更简短的语法:

// The ES5 way
var Photo = React.createClass({
  handleDoubleTap: function(e) { … },
  render: function() { … },
});


// The ES6+ way
class Photo extends React.Component {
  handleDoubleTap(e) { … }
  render() { … }
}

尤其是我们去掉了两个圆括号和一个分号,每一个方法的声明我们都省略了一个冒号、一个 function 关键字和一个逗号。

几乎所有的生命周期方法,(componentWillMount 除外)都可以使用新的类语法定义。componentWillMount 需要写在组件的初始化代码中。

// The ES5 way
var EmbedModal = React.createClass({
  componentWillMount: function() { … },
});


// The ES6+ way
class EmbedModal extends React.Component {
  constructor(props) {
    super(props);
    // 这里可以进行一些组件初始化的工作,componentWillMount 也移到这里执行
  }
}

属性初始化

在 ES6+ 类的世界中,propTypes 以及组件的默认值都可以作为类本身的静态属性存在。类似的,组件中初始化 state 也可以使用 ES7 的 属性初始化

// The ES5 way
var Video = React.createClass({
  getDefaultProps: function() {
    return {
      autoPlay: false,
      maxLoops: 10,
    };
  },
  getInitialState: function() {
    return {
      loopsRemaining: this.props.maxLoops,
    };
  },
  propTypes: {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  },
});


// The ES6+ way
class Video extends React.Component {
  static defaultProps = {
    autoPlay: false,
    maxLoops: 10,
  }
  static propTypes = {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  }
  state = {
    loopsRemaining: this.props.maxLoops,
  }
}

ES7 的属性初始化代码将会在类的构造函数中被加载,在这里 this 指向类对象本身,所以在初始化 state 的代码中可以直接使用 this.props。尤其是我们不再需要使用一个 getter 的函数来定义 prop 的默认值和 state 对象。

箭头函数

React.createClass 会执行一些额外的绑定工作以确保在组件内部 this 能够指向组件本身。

// 自动绑定
var PostInfo = React.createClass({
  handleOptionsButtonClick: function(e) {
    // 这里, 'this' 指向组件本身
    this.setState({showOptionsModal: true});
  },
});

当我们使用 ES6+ 的类语法时就不会再需要 React.createClass 提供的这种辅助绑定,下面可以看到我们将要自己手动执行绑定的过程:

// 在任何需要的地方都需要手动绑定
class PostInfo extends React.Component {
  constructor(props) {
    super(props);
    // 手动绑定方法到组件实例
    this.handleOptionsButtonClick = this.handleOptionsButtonClick.bind(this);
  }
  handleOptionsButtonClick(e) {
    // 需要确保 this 指向组件实例
    this.setState({showOptionsModal: true});
  }
}

幸运的时,结合两个 ES6+ 的新特性 —— 箭头函数语法 和属性初始化语法将会使这种绑定变得轻而易举:

class PostInfo extends React.Component {
  handleOptionsButtonClick = (e) => {
    this.setState({showOptionsModal: true});
  }
}

ES6 的箭头函数内部没有自己独立的 this,所以就使用了外层的 this,加上 ES7 的属性初始化语法中的 this 总是指向类实例本身,所以函数内的 this 自然也就指向了类实例。可以 点击这里 查看它是如何工作的。

*注:如果上面的短网址打不开的话可以 点击这里 打开 babel 的 REPL 环境,并在左侧输入以下内容,右侧即可显示编译后的内容:

class PostInfo extends React.Component {
  handleOptionsButtonClick = (e) => {
   this.setState({showOptionsModal: true});
  }
}

动态属性名 & 模板字符串

其中一个 对象字面量增强 的特性使我们有可以分配一个分离的属性名,在此之前我们可能会像下面这样定义 state 的一部分:

var Form = React.createClass({
  onChange: function(inputName, e) {
    var stateToSet = {};
    stateToSet[inputName + 'Value'] = e.target.value;
    this.setState(stateToSet);
  },
});

现在我们可以在写一个对象字面量的时候直接使用一个 Javascript 表达式来表示对象的 key。这里我们使用一个 模板字符串 来表示新 state 的 key:

class Form extends React.Component {
  onChange(inputName, e) {
    this.setState({
      [`${inputName}Value`]: e.target.value,
    });
  }
}

解构 & 属性展开

通常,当我们组合组件时,我们可能会想从父组件中传递很多 prop 到子组件,但不一定是全部。基于 ES6+ 的 解构赋值语法 和 JSX 的 属性展开语法 ,现在我们可以更随意的去写:

class AutoloadingPostsGrid extends React.Component {
  render() {
    var {
      className,
      ...others,  // 包含 this.props 的全部,除了 className 
    } = this.props;
    return (
      <div className={className}>
        <PostsGrid {...others} />
        <button onClick={this.handleLoadMoreClick}>Load more</button>
      </div>
    );
  }
}

我们也可以结合 JSX 属性展开语法和普通属性,利用一个简单的优先规则实现覆盖和默认。 This element will acquire the className “override” even if there exists a className property in this.props:

<div {...this.props} className="override">
  …
</div>

This element will regularly have the className “base” unless there exists a className property in this.props to override it:

<div className="base" {...this.props}>
  …
</div>

感谢阅读

希望你可以享受更多 ES6+ 新特性带来的写 React 组件的乐趣。感谢所有为这篇文章做过贡献的人,更要感谢 Babel 的团队让我们可以在今天用到这么多未来的新特性。

阅读全文 »

Jerry Bendy 发布于 08月29, 2016

6 种方法在 React 中绑定 javascript 的 this 关键字(ES6/ES7)

Javascript 中的 this 关键字对很多 JS 开发者来说都是令人疑惑、头痛东西,很多时候往往搞不清楚某个 this 究竟指的是谁,尤其是在多层回调嵌套的情况下,OH GOD!!

It’s trivial for some other code to rebind the context of the function you’re working with―using the new keyword and some of the methods that are built onto Functon.prototype. This introduces an entire class of confusing scenarios and often you’ll see callback driven code scattered with calls to .bind(this).

问题

因为 React 使用 this 关键字在内部引用组件上下文,也就带来了关于 this 的一些困惑。你很可以见过或写过下面这样的代码:

this.setState({ loading: true });

fetch('/').then(function loaded() {
  this.setState({ loading: false });
});

这段代码会返回一个 TypeError,因为 this.setState is not a function。这是因为在 promise 的回调中,函数执行的上下文已经改变,this 指向了错误的对象(这时 this 指回调函数本身)。下面我们来看下如何避免这种问题的发生。

解决方案

有一些可选的解决方案已经被使用了很多年,还有一些是仅适用于 React 的,甚至有些方案在浏览器环境下是不能使用的。我们先来看一下。

1. 为 this 创建别名

这个应该算是被使用最多的一种方法了吧。在函数的顶部为 this 创建一个别名,并在组件内部通过这个别名访问 this

var component = this;
component.setState({ loading: true });

fetch('/').then(function loaded() {
  component.setState({ loading: false });
});

这种方法很轻量,并且易于理解。使用一个有意义的别名可以很轻松的使用上下文 this

2. bind this

第二种方法是运行时在回调函数上绑定我们自己的上下文环境。

this.setState({ loading: true });

fetch('/').then(function loaded() {
  this.setState({ loading: false });
}.bind(this));

从 ES5 开始,所有 Javascript 函数都有一个bind 方法,可以允许在函数执行的时候重新为函数内部绑定一个 this。一旦函数绑定了上下文 this 就不可以被重写,也就意味着绑定的上下文环境总是正确的。

这种方式对于其它语言的开发者来说可能很难理解,尤其在有多层函数嵌套时使用 bind,这时候就只能凭借自己的理解和记忆来记住究竟哪个 this 指的哪个对象。

3. React 组件方法

React 可以使用 createClass 创建一个组件类对象,组件类对象里的方法会自动绑定到组件的上下文环境,所以你可以直接在这里使用 this。这样可以允许把回调直接写在组件里。

React.createClass({
  componentWillMount: function() {
    this.setState({ loading: true });

    fetch('/').then(this.loaded);
  },
  loaded: function loaded() {
    this.setState({ loading: false });
  }
});

如果你的组件内部不需要做太多工作的话,想必这会是一种非常优雅的方案。这能允许你使用命名的方法、可以使你的代码更加扁平化,并且忘记 this 上下文这回事。事实上,如果你尝试在组件方法中使用 .bind(this), React 将会警告你正在做一件不必要的工作。

bind(): You are binding a component method to the component. React does this for you automatically in a high-performance way, so you can safely remove this call.

很重要的是,这种方式并不适用于 ES6 类语法的创建的 React 组件。 如果你正在使用 ES6 的类语法创建 React 组件,可以尝试下后面的方法。

4. ES2015(ES6)箭头语法

ES2015 中新加入了箭头语法用来更方便的写函数表达式。箭头语法除了可以以更简短的方式写函数外,还有一些很重要的特性,例如箭头语法创建的函数体中没有 this 而总是使用外部的 this 对象。

this.setState({ loading: true });

fetch('/').then(() => {
  this.setState({ loading: false });
});

无论多少层的函数嵌套,箭头函数中总是使用正确的 this 上下文。

很不幸的是,这样我们将无法命名我们的函数。这会使调试变得更困难,调用栈中将会显示这是一个 (anonymous function) (匿名函数)。

如果你使用 Babel 转换 ES6 的代码到 ES5,你将会发现一些比较有趣的现象。

  • 有些情况下编译器会通过变量名推理出函数名
  • 编译器是通过为 this 创建别名的方法来保持上下文
const loaded = () => {
  this.setState({ loading: false });
};

// 将会被编译为

var _this = this;
var loaded = function loaded() {
  _this.setState({ loading: false });
};

5. ES2016 (ES7)的绑定语法

这是一个 ES7 的提案,ES2016(ES7)的绑定语法,使用两个冒号(::)作为新的操作符。绑定操作符要求左侧是一个值,并且右侧是需要处理的函数,使用这种语法相当于把双冒号左侧的数值绑定到右侧处理函数的 this 上下文中。

下面通过 map 来举个简单的例子:

function map(f) {
  var mapped = new Array(this.length);

  for(var i = 0; i < this.length; i++) {
    mapped[i] = f(this[i], i);  
  }

  return mapped;
}

不同于 lodash,我们不需要在参数中传递数据,这使 map 函数看起来更像是数据的一个成员方法。

[1, 2, 3]::map(x => x * 2)
// [2, 4, 6]

是不是曾经像下面这下写过?

[].map.call(someNodeList, myFn);
// or
Array.from(someNodeList).map(myFn);

ES7 绑定操作符允许你直接在类数组结构上使用 map 函数。

someNodeList::map(myFn);

我们也可以在 React 组件中使用这种语法:

this.setState({ loading: true });

fetch('/').then(this::() => {
  this.setState({ loading: false });
});

可能我会是第一个站出来承认这种语法会让人觉得恐惧的人。

了解这个操作符会觉得很有趣,虽然它不是专为这种使用场景而生的。它解决了很多由于.bind(this)产生的缺点(事实上 babel 最终还是会将它编译成 .bind(this)),在解决很多层的嵌套代码中 this 的问题时可以放心的使用这种方式。当然这可能会使用其它开发人员有些困惑。

React component context probably isn’t the future of the bind operator, but if you are interested take a look at some of the great projects where it’s being used to great effect (such as mori-ext).

6. 函数指定

有些函数可以在执行时手动为它指定上下文的 this,例如 map,它接受的最后一个参数将会作为回调函数内的 this

items.map(function(x) {
  return <a onClick={this.clicked}>x</a>;
}, this);

虽然这可以解决问题,但却不存在通用性。因为大部分的函数是不能接受重新指定的 this 的。

总结

上面我们说了一些上下文中使用正确的 this 的方法。如果担心性能问题,为 this 创建别名将会是最快的方法(由于箭头函数在编译后与创建别名相同,所以使用 ES6 的箭头函数也是很好的选择)。当然,也许直到你的界面有上万个组件也许都不会看到这种性能差别,也许到那时 this 的问题也不会成为真正的瓶颈。

If you’re more concerned about debugging, then use one of the options that allows you to write named functions, preferably component methods as they’ll handle some performance concerns for you too.

At Astral Dynamics, we’ve found a reasonable compromise between mostly using named component methods and arrow functions, but only when we write very short inline functions that won’t cause issues with stack traces. This allows us to write components that are clear to debug, without losing the terse nature of arrow functions when they really count.

Of course, this is mostly subjective and you might find that you prefer to baffle your colleagues with arrow functions and bind syntax. After all, who doesn’t love reading through a codebase to find this?

this.setState({ loading: false });

fetch('/')
  .then((loaded = this::() => {
    var component = this;
    return this::(() =>
      this::component.setState({ loaded: false });
    }).bind(React);
  }.bind(null)));

via sitepoint

阅读全文 »

Jerry Bendy 发布于 07月30, 2016

【分享】ES6 你可能不知道的事 - 基础篇

ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生。

首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性你可能不知道的事,希望能为各位同学正确使用ES6,提供一些指导。

对于 ES6,有些同学已经在项目中有过深入使用了,有些则刚刚开始认识他,但不论你是属于哪一类,相信这篇文章都有适合你的部分。针对文章中的问题或不同意见,欢迎随时拍砖、指正。

正文

Let + Const

这个大概是开始了解 ES6 后,我们第一个感觉自己完全明白并兴致勃勃的开始使用的特性。

以如下方式使用的同学请举下手?

// 定义常量
const REG_GET_INPUT = /^\d{1,3}$/;

// 定义配置项
let config = {
  isDev : false,
  pubDir: './admin/'
}

// 引入 gulp
let gulp    = require('gulp');

// 引入gulp相关插件
let concat  = require('gulp-concat');
let uglify  = require('gulp-uglify');
let cssnano = require('gulp-cssnano');

很多人看完概念之后,第一印象都是:“const 是表示不可变的值,而 let 则是用来替换原来的 var 的。”

所以就会出现上面代码中的样子;一段代码中出现大量的 let,只有部分常量用 const 去做定义,这样的使用方式是错误的。

你可能不知道的事

const 的定义是不可重新赋值的值,与不可变的值(immutable value)不同;const 定义的 Object,在定义之后仍可以修改其属性。

所以其实他的使用场景很广,包括常量、配置项以及引用的组件、定义的 “大部分” 中间变量等,都应该以const做定义。反之就 let 而言,他的使用场景应该是相对较少的,我们只会在 loop(for,while 循环)及少量必须重定义的变量上用到他。

猜想:就执行效率而言,const 由于不可以重新赋值的特性,所以可以做更多语法静态分析方面的优化,从而有更高的执行效率。

所以上面代码中,所有使用 let 的部分,其实都应该是用 const 的。

Template Strings(字符串模板)

字符串模板是我刚接触ES6时最喜欢的特性之一,他语法简洁,语义明确,而且很好的解决了之前字符串拼接麻烦的问题。

因为他并不是 “必须” 的,而且原有的字符串拼接思维根深蒂固,导致我们很容易忽视掉他。

使用实例

我们先来看看他的一般使用场景:

const start = 'hi all';

const getName = () => {
  return 'jelly';
};

const conf = {
  fav: 'Coding'
};

// 模板
const msg = `${start}, my name is ${getName()}, ${conf.fav} is my favourite`;

你可能不知道的事

// 1. 与引号混用
const wantToSay = `I'm a "tbfed"`;

// 2. 支持多行文本
const slogan = 
`
I have a dream today!
`;

// 比较适合写HTML
const resultTpl = 
`
  <section>
    <div>...</div>
  </section>
`;

Enhanced Object Literals(增强的对象字面量)

增强的对象字面量是 ES6 中的升华功能,他设计了很多简写,这些简写不但保留了明确的语义,还减少了我们多余的代码量。

当他的使用成为一个习惯时,我们会看到自己代码变得更为优雅。

你可能不知道的事

const _bookNum = 4;

const basicConfig = {
  level: 5
}

const config = {
  // 直接指定原型对象
  __proto__: basicConfig,

  // 属性简写
  _bookNum,

  // 方法简写
  getBookNum() {
    return this.bookNum;
  }
}

Arrows and Lexical This(箭头函数)

箭头函数是ES6中的一个新的语法特性,他的用法简单,形态优雅,备受人们青睐。

大多数同学初识这个特性时,更多的仅仅用它作为函数定义的简写,这其实就有些屈才了。

// 未使用箭头函数的写法
{
  ...

  addOptions: function (options) {

    var self = this;

    options.forEach(function(name, opts){

      self[name] = self.addChild(name, opts);

    });

  } 
}

// 使用箭头函数后的写法
{
  ...

  addOptions: function (options) {

    options.forEach((name, opts) => {

      this[name] = this.addChild(name, opts);

    });

  } 
}

可以注意到上下两段代码的区别。

在未使用箭头函数前,我们在过程函数中使用父级 this,需要将其显式缓存到另一个中间变量中,因为过程函数有独立的 this 变量,会覆盖父级;使用箭头函数后,不但简写了一个过程函数( forEach 的参数),还省略掉了 this 的中间变量的定义。

原因:箭头函数没有独立执行上下文( this ),所以其内部引用 this 对象会直接访问父级。

插播:原来我们定义这个中间变量还有一个有趣的现象,就是明明千奇百怪,例如 self, that, me, _that, _me, Self...,快站出来说说你用过哪个,还是哪几个~

当然,从这块我们也可以看出,箭头函数是无法替代全部 function 的使用场景的,例如我们需要有独立 this 的函数。

另注:作者原文的评论中有人提到 forEach 方法中是可以不用写 that = this 这样的语句的,因为 forEach 可以接受可选的第二个参数用于重新指定 callback 中的 this 指向。即,使用 options.forEach( callback, this ) 即可。具体可参见MDN上的说明

你可能不知道的事

  • 箭头函数不但没有独立 this,他也没有独立的 arguments,所以如果需要取不定参的时候,要么使用 function,要么用 ES6 的另一个新特性 rest(具体在 rest 中会有详解)。
  • 箭头函数语法很灵活,在只有一个参数或者只有一句表达式做方法体时,可以省略相应括号。
// 完整写法
const getOptions = (name, key) => {
  ...
}

// 省略参数括号
const getOptions = key => {
  ... 
}

// 省略参数和方法体括号
const getOptions = key => console.log(key);

// 无参数或方法体,括号不能省略
const noop = () => {};

有个简单小栗子,这一灵活的语法在写连续的Promise链式调用时,可以使代码更加优雅

gitPromise
  .then(() => git.add())
  .then(() => git.commit())
  .then(() => git.log())
  .then((msg) => {
      ...
  })
  .then(() => git.push())
  .catch((err) => {
      utils.error(err);
  });

Destructuring(解构)

解构这个特性可以简单解读为分别定义,用于一次定义多个变量,常常用于分解方法返回对象为多个变量,分别使用。

使用过ES6的同学应该或多或少接触过这个特性,但是你可能不知道它如下几个用法:

你可能不知道的事

const bookSet = ['UED', 'TB fed', 'Not find'];
const bookCollection = () => {
  return {book1: 'UED', book2: 'TB fed'};
};

// 1. 解构也可以设置默认值
const {book1, book3 = 'Not find'} = bookCollection();

// 2. 解构数组时候是可以跳过其中某几项的
const [book1,,book3] = bookSet;  // book1 = 'UED', book3 = 'Not find'

// 3. 解构可以取到指定对象的任何属性,包括它包含的方法
const {length: setLength} = bookSet;  // setLength = 3

Rest + Spread

Rest 和 Spread 主要是应用 ... 运算符,完成值的聚合和分解。

你可能不知道的事

// 1. rest 得到的是一个真正的数组而不是一个伪数组
const getOptions = function(...args){
  console.log(args.join); // function
};

// 2. rest 可以配合箭头函数使用,达到取得所有参数的目的
const getOptions = (...args) => {
  console.log(args); // array
};

// 3. spread 可以用于解构时,聚合所得的值
const [opt1, ...opts] = ['one', 'two', 'three', 'four'];

// 4. spread 可以用于数组定义
const opts = ['one', 'two', 'three', 'four'];
const config = ['other', ...opts];

Classes

ES6 中实现的一个语法糖,用于简化基于原型集成实现类定义的场景。

虽然有很多人不太喜欢这个特性,认为它作为一个简单增强扩展,并没有其他语言 class 应有的特点。 但是就我自己观点来看,还是感觉这样一种写法确实比原有的原型继承的写法语义更清晰、明确,而且语法更简单。

同样,可能有些用法是你之前容易忽略掉的,在此做个补充。

你可能不知道的事

// 1. 静态变量
// ES6 的类定义实现了静态方法的定义,但静态变量呢?
// 可以用如下方式实现: 
class TbFedMembers{
  static get HuaChen(){
    return 'jelly';
  }
}
TbFedMembers.HuaChen; // "化辰"

// 2. 私有属性(私有属性有多种实现方式,只谈及其中一种)
// 闭包
const TbFedMembers = (() => {
  const HuaChen = 'jelly';

  return class{
    getOneMemberName(){
      return HuaChen;
    }
  };
})();

Promises

Promise 不只是一个对象、一个语法,他更是一种异步编程方式的变化 相信使用过 ES6 的同学都已经开始尝试了 Promise,甚至在不支持ES6的时候,已经开始使用一些基于 Promise 思想的开源框架。

那么我们之前用 Promise 究竟用的对么?有什么需要注意的点呢?

你可能不知道的事


// 1. 多个异步任务同时执行用 Promise.all,顺序执行使用链式调用
// Promise.all
Promise
  .all([jsBuildPromise, cssBuildPromise])
  .then(() => {
    ...
  });

// chain
jsBuildPromise
  .then(() => cssBuildPromise)
  .then(() => {
    ...
  });


// 2. Promise 的链式调用需要每一个过程返回一个 Promise 对象才能保证顺序执行
gitPromise
  .then(() => git.add())  // 正确,箭头函数简写
  .then(() => {
    git.commit(); // 错误,函数返回 undefined,会立即执行下一过程
  })
  .then(() => {
    return git.log(); // 正确
  });


// 3. Promise 需要调用 catch 方法来捕获错误,而且过程内的错误不会阻塞后续代码执行
new Promise(() => {
  f;  // not define error !
})
.catch((err) => {
  console.log(err)  // show 'f is not define'
});
console.log('error test');  // 此行可以被正常执行

结语

基础篇主要是讲了我们最常用的一些特性,后续如果大家感兴趣,还可以再来个 “进阶篇”,最后,希望文章中的部分内容可以对大家理解和使用 ES6 有所帮助。

参考资料

文章分享自淘宝FED团队化辰的文章, 原文地址:http://taobaofed.org/blog/2016/07/22/es6-basics/

感谢作者的分享!

阅读全文 »

Jerry Bendy 发布于 04月26, 2016

【译】创建自定义angularJS指令(七)- 使用 $asyncValidators 创建唯一值指令

AngularJs


  1. 基础
  2. 独立作用域
  3. 独立作用域和函数参数
  4. transclude与restrict
  5. link函数
  6. 使用控制器
  7. Creating a Unique Value Directive using $asyncValidators

在上一篇文章中我演示了如何创建一个唯一值校验的指令来确定一个 email 地址是否已经被使用过。在 AngularJS 1.3+ 以上的版本中增加了许多新的特性可以使指令的代码变得更加整洁并且更易于使用。在这篇文章中,我将会更新之前的代码,尝试一下新的特性。下面展示的代码是 Customer Manager Standard 中的一部分,你可以在 Github 上看到完整的代码。

下面的截图是运行后的一部分,在截图中,email 地址已经被其它用户使用,导致显示错误信息。

指令使用以下代码绑定到 email 输入框中:

<input type="email" name="email" 
        class="form-control"
        data-ng-model="customer.email"
        data-ng-model-options="{ updateOn: 'blur' }"
        data-wc-unique
        data-wc-unique-key="{{customer.id}}"
        data-wc-unique-property="email"
        data-ng-minlength="3"
        required />

You can see that a custom directive named wc-unique is applied as well as 2 properties named wc-unique-key and wc-unique-property (I prefer to add the data- prefix but it’s not required). Before jumping into the directive let’s take a look at an AngularJS factory that can be used to check if a value is unique or not. 通过上面的代码你可以看到我们使用了一个叫做 wc-unique 的自定义指令,以及两个属性 wc-unique-keywc-unique-property (我更喜欢为添加 data- 前缀,虽然这不是必须的)。在跳到指令代码之前,我们先看下 AngularJS 中可以用来检查一个值是否唯一的 factory 。

创建 Factory

唯一值校验需要依赖后端服务,AngularJS 提供了一些可以与后端通信的服务,如 $http$resource。在这个例子中,我会使用一个叫做 dataService 自定义的 factory,依赖 $http 服务进行 Ajax 操作与后端通信。它内部使用一个叫做 checkUniqueValue() 的方法处理唯一值验证。要注意一下,我并没有特别去区分 "factory" 与 "service",因为它们从根本上来说所做的事情其实是一样的,只是对我来说 "service" 可能更好听一些(个人爱好)。

(function () {

    var injectParams = ['$http', '$q'];

    var customersFactory = function ($http, $q) {
        var serviceBase = '/api/dataservice/',
            factory = {};

        factory.checkUniqueValue = function (id, property, value) {
            if (!id) id = 0;
            return $http.get(serviceBase + 'checkUnique/' + id + '?property=' + property + 
              '&value=' + escape(value)).then(
                function (results) {
                    return results.data.status;
                });
        };

        //More code follows

        return factory;
    };

    customersFactory.$inject = injectParams;

    angular.module('customersApp').factory('customersService', customersFactory);

}());

创建唯一值指令

我创建了一个叫做 wcUnique 的指令用于处理唯一值的验证(wc 是指 Wahlin Consulting,作者所在的公司名)。这是一个相当简单的指令,仅限于被用作一个属性。指令的外壳基本如下:

function () {

    var injectParams = ['$q', 'dataService'];

    var wcUniqueDirective = function ($q, dataService) {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, element, attrs, ngModel) {

                //Validation code goes here

            }
        };
    };

    wcUniqueDirective.$inject = injectParams;

    angular.module('customersApp').directive('wcUnique', wcUniqueDirective);

}());

As the directive is loaded the link() function is called which gives access to the current scope, the element the directive is being applied to, attributes on the element, and the ngModelController object. If you’ve built custom directives before (hopefully you’ve been reading my series on directives!) you’ve probably seen scope, element and attrs before but the 4th parameter passed to the link() function may be new to you. If you need access to the ngModel directive that is applied to the element where the custom directive is attached to you can “require” ngModel (as shown in the code above). ngModel will then be injected into the link() function as the 4th parameter and it can be used in a variety of ways including validation scenarios. One of the new properties available in ngModelController with AngularJS 1.3+ is $asyncValidators (read more about it here) which we’ll be using in this unique value directive.

Here’s the complete code for the wcUnique directive:

(function () {

    var injectParams = ['$q', '$parse', 'dataService'];

    var wcUniqueDirective = function ($q, $parse, dataService) {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, element, attrs, ngModel) {
                ngModel.$asyncValidators.unique = function (modelValue, viewValue) {
                    var deferred = $q.defer(),
                        currentValue = modelValue || viewValue,
                        key = attrs.wcUniqueKey,
                        property = attrs.wcUniqueProperty;

                    //First time the asyncValidators function is loaded the
                    //key won't be set  so ensure that we have 
                    //key and propertyName before checking with the server 
                    if (key && property) {
                        dataService.checkUniqueValue(key, property, currentValue)
                        .then(function (unique) {
                            if (unique) {
                                deferred.resolve(); //It's unique
                            }
                            else {
                                deferred.reject(); //Add unique to $errors
                            }
                        });
                    }
                    else {
                        deferred.resolve(); //Ensure promise is resolved if we hit this 
                     }

                    return deferred.promise;
                };
            }
        };
    };

    wcUniqueDirective.$inject = injectParams;

    angular.module('customersApp').directive('wcUnique', wcUniqueDirective);

}());

You’ll notice that the ngModel parameter that is injected into the link() function is used to access a property named $asyncValidators. This property allows async operations to be performed during the data validation process which is perfect when you need to go back to the server to check if a value is unique. In this case I created a new validator property named unique that is assigned to a function. The function creates a deferred object and returns the promise. From there the code grabs the current value of the input that we’re trying to ensure is unique and also grabs the key and property attribute values shown earlier.

The key represents the unique key for the object (ultimately the unique identifier for the record). This is used so that we exclude the current object when checking for a unique value across objects on the server. The property represents the name of the object property that should be checked for uniqueness by the back-end system.

Once the variables are filled with data, the key and property values are passed along with the element’s value (the value of the textbox for example) to a function named checkUniqueValue() that’s provided by the dataService factory shown earlier. This triggers an Ajax call back to the server which returns a true or false value. If the server returns that the value is unique we’ll resolve the promise that was returned. If the value isn’t unique we’ll reject the promise. A rejection causes the unique property to be added to the $error property of the ngModel so that we can use it in the view to show and hide and error message.

Showing Error Messages

The unique property added to the $error object can be used to show and hide error messages. In the previous section it was mentioned that the $error object is updated but how do you access the $error object? When using AngularJS forms, a name attribute is first added to the <form> element as shown next:

<form name="editForm">

The editForm value causes AngularJS to create a child controller named editForm that is associated with the current scope. In other words, editForm is added as a property of the current scope (the scope originally created by your controller). The textbox shown earlier has a name attribute value of email which gets converted to a property that is added to the editForm controller. It’s this email property that gets the $error object. Let’s look at an example of how we can check the unique value to see if the email address is unique or not:

<div class="col-md-2">
    Email:
</div>
<div class="col-md-10">
    <!-- type="email" causing a problem with Breeze so using regex -->
    <input type="email" name="email" 
            class="form-control"
            data-ng-model="customer.email"
            data-ng-model-options="{ updateOn: 'blur' }"
            data-wc-unique
            data-wc-unique-key="{{customer.id}}"
            data-wc-unique-property="email"
            data-ng-minlength="3"
            required />
    <!-- Show error if touched and unique is in error -->
    <span class="errorMessage" ng-show="editForm.email.$touched && editForm.email.$error.unique">
        Email already in use
    </span>
</div>

Notice that the ngShow directive (on the span at the bottom of the code) checks the editForm property of the current scope and then drills down into the email property. It checks if the value is touched using the $touched property (this property is in AngularJS 1.3+ and reports if the target control lost focus – it has nothing to do with a touch screen) and if the $error.unique value is there or not. If editform.email.$error.unique exists then we have a problem and the user entered an email that is already in use. It’s a little bit confusing at first glance since we’re checking if unique is added to the $error object which means the email is already in use (the unique property is in error). If it’s not on the $error object then then everything is OK and the user entered a unique value.

The end result is the red error message shown next:

Conclusion

Directives provide a great way to encapsulate functionality that can be used in views. In this post you’ve seen a simple AngularJS unique directive that can be applied to textboxes to check if a specific value is unique or not and display a message to the user. To see the directive in an actual application check out the Customer Manager sample application at https://github.com/DanWahlin/CustomerManagerStandard.


From ASP.net

阅读全文 »

Jerry Bendy 发布于 04月26, 2016

【译】创建自定义angularJS指令(六)- 使用控制器

AngularJs


  1. 基础
  2. 独立作用域
  3. 独立作用域和函数参数
  4. transclude与restrict
  5. link函数
  6. 使用控制器
  7. Creating a Unique Value Directive using $asyncValidators

在这个 AngularJS 指令系列的文章中你已经了解到一些指令的关键部分,但还没有任何指令与控制器绑定相关的内容。控制器在 AngularJS 中的典型用途就是把路由和视图联系在一起,在指令中也是如此。事实上,在指令中使用控制器通常会使代码看起来更简洁,并且更易于维护。当然,指令中的控制器是可选的,如果你喜欢用简单的方式创建指令,你会发现控制器在很多情况下是适用的,并且更好用。使用控制器会让指令看起来更像是“子视图”。

在这篇文章中我将会简单的讲解下如何会配控制器到指令,以及控制器在指令中扮演折角色。先看一个不使用控制器的指令的例子:

不使用控制器的指令

指令提供一些不同的方式用于生成 HTML、管理数据以及处理额外的任务等。在指令需要处理大量 DOM 操作时,使用 link 方法是很好的实践。下面是一个使用 link 方法的例子:

(function() {

  var app = angular.module('directivesModule');

  app.directive('domDirective', function () {
      return {
          restrict: 'A',
          link: function ($scope, element, attrs) {
              element.on('click', function () {
                  element.html('You clicked me!');
              });
              element.on('mouseenter', function () {
                  element.css('background-color', 'yellow');
              });
              element.on('mouseleave', function () {
                  element.css('background-color', 'white');
              });
          }
      };
  });

}());

往指令中添加一个控制器对于指令中的 DOM 操作和事件处理来说没有任何意义。虽然例子中可以通过在视图中添加 AngularJS 指令(如 ngClick)和控制器的方式完成相同的功能,但如果 DOM 操作是指令整体要完成的工作的话我们还是没有理由要使用控制器的。

在你需要进行一些简单的 DOM 操作,或整合数据到 HTML, 或处理事件等的时候,添加控制器可以很大程度上简化你的代码。下面通过一个简单的指令的例子来理解上面这句话。例子中生成一个列表,并且可以通过按钮添加项目到列表中。下图是可能的输出:

有很多种方法可以做到这种效果。一个典型的 DOM 为中心的方法是使用 link 函数处理指令中显示的内容。请记住,有很多方法可以做到这一点,总的目标是展示 DOM 为中心的代码(尽可能简单):

(function() {

  var app = angular.module('directivesModule');

  app.directive('isolateScopeWithoutController', function () {

      var link = function (scope, element, attrs) {

              //Create a copy of the original data that’s passed in              
              var items = angular.copy(scope.datasource);

              function init() {
                  var html = '<button id="addItem">Add Item</button><div></div>';
                  element.html(html);

                  element.on('click', function(event) {
                      if (event.srcElement.id === 'addItem') {
                          addItem();
                          event.preventDefault();
                      }
                  });
              }

              function addItem() {
                  //Call external function passed in with &
                  scope.add();

                  //Add new customer to the local collection
                  items.push({
                      name: 'New Directive Customer'
                  });

                  render();
              }

              function render() {
                  var html = '<ul>';
                  for (var i=0,len=items.length;i<len;i++) {
                      html += '<li>' + items[i].name + '</li>'
                  }
                  html += '</ul>';                  

                  element.find('div').html(html);
              }

              init();
              render();        
      };


      return {
          restrict: 'EA',
          scope: {
              datasource: '=',
              add: '&',
          },
          link: link
      };
  });

}());

虽然这些代码完成了这个功能,却是使用了一种类型于 jQuery 插件的思路来写的,使用一种作者称之为 “control-oriented” 的方式,即标签名称和/或ID在代码中普遍存在。手动操作 DOM 算是一种好的方法,尤其是在一些特殊的场景下(为性能考虑),但这绝不是我们构建 Angualar 应用程序的方式。这种混合的写法会使代码变得凌乱并让指令变得臃肿。

在上面的代码中,当点击按钮时,addItem()函数被调用,添加一个新的元素并重新render()render()函数创建一个<ul>标签和多个<li>标签。虽然这样看起来是没有什么问题,事实上这会导致代码的碎片化,将会使以后的维护工作变得非常困难。或许在这种很小的指令里面看起来问题还不算严重,在以后需要给指令添加或修改功能时这个问题才会日渐突出。

还有在这段代码中有一个更细微的问题。当调用scope.add()或其它方式修改了任何父范围内作用域的值后还必须要调用$scope.$apply()应用更改(具体原因在这篇文章中就不详述了,但这是绝对需要考虑的因素)。最后,指令并不像文章开头提到的“子视图”的概念——它只是一串代码。在这个例子中控制器能如何更好的帮助我们吗?Let's take a look。

给指令添加控制器和视图

上一节中的指令能够完成任务,但我们更愿意写一个标准的 AngularJS 视图,使用数据驱动的方式改变 DOM。通过在指令中使用控制器和视图,我们就可以像写一般的视图一样写指令。

下面的例子把之前的指令代码转换为控制器的形式,有没有感觉很清新?

(function() {

  var app = angular.module('directivesModule');

  app.directive('isolateScopeWithController', function () {

    var controller = ['$scope', function ($scope) {

          function init() {
              $scope.items = angular.copy($scope.datasource);
          }

          init();

          $scope.addItem = function () {
              $scope.add();

              //Add new customer to directive scope
              $scope.items.push({
                  name: 'New Directive Controller Item'
              });
          };
      }],

      template = '<button ng-click="addItem()">Add Item</button><ul>' +
                 '<li ng-repeat="item in items">{{ ::item.name }}</li></ul>';

      return {
          restrict: 'EA', //Default in 1.3+
          scope: {
              datasource: '=',
              add: '&',
          },
          controller: controller,
          template: template
      };
  });

}());

这个指令可以以下面任一种方式使用:

属性: <div isolate-scope-with-controller datasource="customers" add="addCustomer()"></div>

元素: <isolate-scope-with-controller datasource="customers" add="addCustomer()">
         </isolate-scope-with-controller>

通读上面的代码你会发现通过控制器与视图绑定的方式写指令格外简单,就像在写一个子视图一样。这样写的好处就在于完全避开了 DOM 操作,现在的代码可以认为是与 DOM 无关的,无疑这会减少很多开发和维护成本。

视图使用 template 属性定义,控制器使用 controller 属性定义。当然视图还可以使用 templateUrl 从外部文件导入,没必要都直接写在指令代码里面。当视图文件有很多代码时,templateUrl$templateCache 会是更好的选择。

继续上面的例子,视图中经常会调用其它指令,如 ng-clickng-repeat,也会使用如 {% raw %}{{ ... }}{% endraw %}ng-bind 这种形式的数据绑定。避免 DOM 操作的好处在这里更加明显。控制器中注入 $scope 并定义 items 属性,视图中使用 ng-repeatitems 中循环并生成 <li>标签。当点击添加按钮时,会调用 $scope 中的 addItem() 方法并添加一个新的项目到 items 中。因为 addItem() 是由 ng-click 调用的,所以父级作用域在使用 $scope.add 的时候无需关心前面提到的关于 $scope.$apply() 的任何问题。

通常在指令需要很高的性能的情况下更建议使用原始的 DOM 操作的方式,直接操作 DOM 总是比使用控制器要快很多(可以避免很多额外的操作)。如果你参加我的讲座将可能会听到我经常说这么一名话:“使用正确的工具做正确的事情(Use the right tool for the right job)”,我从来不会相信有任何一种工具可以适用所有场景,并知道每一种情形和应用都是独一无二的。

This thought process definitely applies to directives since there are many different ways to write them. In many situations I’m happy with how AngularJS performs and know about the pitfalls to avoid so I prefer the controller/view type of directive whenever possible. It makes maintenance much easier down the road since you can leverage existing Angular directives in the directive’s view and modify the view using a controller and scope. If, however, I was trying to maximize performance and eliminate the use of directives such as ng-repeat then going the DOM-centric route with the link function might be a better choice. Again, choose the right tool for the right job.

在指令中使用 controllerAs

如果你是 controllerAs 语法的追捧者你将会为可以在指令中使用同样的语法而感到惊喜。当你在指令中定义了一个 指令定义对象(DDO, Directive Definition Object),你就可以为这个指令添加 controllerAs 属性。从 Angular 1.3 开始,你还需要添加一个 bindToController 属性来确保是绑定到控制器而不是作用域。下面是一个使用 controllerAs 语法的例子:

(function() {

  var app = angular.module('directivesModule');

  app.directive('isolateScopeWithControllerAs', function () {

      var controller = function () {

              var vm = this;

              function init() {
                  vm.items = angular.copy(vm.datasource);
              }

              init();

              vm.addItem = function () {
                  vm.add();

                  //Add new customer to directive scope
                  vm.items.push({
                      name: 'New Directive Controller Item'
                  });
              };
      };    

      var template = '<button ng-click="vm.addItem()">Add Item</button>' +
                     '<ul><li ng-repeat="item in vm.items">{{ ::item.name }}</li></ul>';

      return {
          restrict: 'EA', //Default for 1.3+
          scope: {
              datasource: '=',
              add: '&',
          },
          controller: controller,
          controllerAs: 'vm',
          bindToController: true, //required in 1.3+ with controllerAs
          template: template
      };
  });

}());

注意例子里面的控制器别名 vmviewModel 的缩写)赋给到 controllerAs 属性,并且同时在控制器和视图中被使用。bindToController 设置为 true 用于确保属性绑定到控制器而不是作用域。这种方式相对于前面所述的方法来说要显得更加的简洁,还允许你在视图中使用点语法(如 vm.customers),所以更推荐这种用法。

总结

使用控制器在一些场景下会使指令代码变得更加的简练、直观。虽然使用控制器通常来说不是必要的,但从“子视图”的概念中解放出来,让指令做更多有价值的操作,通常会让你的代码更具可维护性。下一篇文章我们来讨论一些可以在指令中使用的额外的功能,如 $asyncValidators


From ASP.net

阅读全文 »