Ember.js的那些坑

用了一年Ember.js,从2.3到2.10,因为数据量大,以及项(xu)目(qiu)的复(bian)杂(tai)性踩了不少坑,感觉再也不会爱了。在把这个锅甩出去之前,小小总结一下,以示后人,知己知彼方能百战百胜。注意,这篇我只吐槽。

首先

肯定要吐槽一下压缩后仍旧占用几兆的巨无霸内核JS代码。光这点来说,Ember绝对不适合移动端以及小型项目的开发。哪怕像我参与的这个平台级项目,对于这个大小也是深感蛋疼。而且,Ember的默认配置还是只压缩成vender.js与app.js两个文件而已。

此外,Ember最大的错误恐怕是步了Angular 1的后尘,选择了双向绑定加observer模式。谷歌肯定是被坑惨了,咬牙抛弃了Angular 1。讲真,这个写起来是很爽,被坑得更爽,最终花不少时间在调查bug原因,想办法避免这个模式的死穴上。在此就不细讲了,有机会另写一篇。另外据说有Ember版redux和flux,有兴趣的话可以试一下。

.sendAction()和.send()

刚开始用的时候,经常搞不清这两个方法。。。

简单来说,区别在于:

- .sendAction()只存在于Ember.Component类,.send()则在Ember.Route, Ember.Controller, Ember.Component中都存在。

- 两者的第一个参数均为action名字,但是.sendAction(actionName)中的actionName接受的是Ember.Component根级的key,只有当对应这个key的值是function类型才会工作,没有这个key或者值类型不对也不会报错。.send(actionName)中的action则接受actions: {}中定义的一个action名称,没有的话会报错。

js

export default Ember.Component.extend({

myFunc() {},

callCallMyFunc() {

this.send(‘callMyFunc‘);

},

actions: {

callMyFunc() {

this.sendAction(‘myFunc‘);

},

callCallMyFunc() {

this.send(‘callMyFunc‘);

}

}

});

- 另外Ember.Route中的.send(){{action}} helper可以向上一级Route的action传递。

父级Route:

js

export default Ember.Route.extend({

actions: {

parentAction() {}

}

});

子级Route:

js

export default Ember.Route.extend({

actions: {

callParentAction() {

this.send(‘parentAction‘);

}

}

});

参数的传递的陷阱

直接上例子。假设我有一个component-a内容如下:

Component JS:

export default Ember.Component.extend({
    display: null,
    click() {
        this.sendAction(‘click‘);
    }
});

Template:

{{display}}

然后我在一个template中调用了该component-a, 并传递了一个action:

{{component-a action=(action ‘myAction‘ true)}}

对应的controller为:

export default Ember.Controller.extend({
    actions: {
        myAction(param1 = false, param2 = true) {
            this.set(‘display‘, param1 ? param2 : null);
        }
    }
});

所以运行结果是:

<div>click</div>

这个结果不难预料。坑就在于这个component里的sendAction不能随便增减参数,否则所有用到这个action回调的地方都会悲剧。为什么提这个呢?因为我碰到好多次了,写新需求的时候发现回调里需要传递更多信息,最初写component的时候并没有考虑到这么多。这个时候传一个object的好处就体现出来了。

Ember.run.debounce()的正确用法

可参考回答:http://stackoverflow.com/questions/18954068/how-to-achieve-the-correct-closure-using-ember-run-debounce

Ember的官方文档只是粗略地写了:

debounce (target, method, args*, wait, immediate)

Delay calling the target method until the debounce period has elapsed with no additional debounce calls. If debounce is called again before the specified time has elapsed, the timer is reset and the entire period must pass again before the target method is called.

This method should be used when an event may be called multiple times but the action should only be called once when the event is done firing. A common example is for scroll events where you only want updates to happen once scrolling has ceased.

嗯。。。从头到尾没有提过不能是匿名函数啊有木有。作为一个新用户看到这个感叹它的神奇,然而并没有,Ember判断是不是同一个方法只是简单地判断引用而已。所以传匿名函数是木有用的童鞋们。

Ember.set, Ember.trySet,有必要并存吗?

也许你都没有注意到有一个叫Ember.trySet()的方法。。。官方文档:

Error-tolerant form of Ember.set. Will not blow up if any part of the chain is undefined, null, or destroyed.

This is primarily used when syncing bindings, which may try to update after an object has been destroyed.

就是说,在一个object被destroy之后,运行的.set(),会报错,用Ember.trySet()就不会。

什么时候会碰到这种情况?举个例子:

我有一个component叫my-component:

export default Ember.Component.extend({
    store: Ember.inject.service(),
    value: null,
    click() {
        this.sendAction();
        this.get(‘store‘).findAll(‘myModel‘).then(data => this.set(‘value‘, data));
    }
});

调用这个component的template:

{{test}}
{{#if isDisplay}}
    {{my-component value=test action=(action set ‘isDisplay‘ false)}}
{{/if}}

然后当你点击这个component的时候就杯具了。因为在点击的时候,我先设置isDisplay为false,然后在Promise的会调里才设置component的某个值,但早在Ember re-render template时已将my-component实例destroy掉,你就在set一个已经destroy了的object,这会报错。但有时候你必须要应对这种情况,所以此时用该用Ember.trySet(this, ‘value‘, data)替代。

然并卵,Ember.trySet在我所知的2.10有bug,依旧会报错。。。心好累。

有一种替代方法是判断this.get(‘isDestroyed‘) || this.get(‘isDestroying‘),这个目前来看还是比较靠谱的。但是真的要每次都这么写一遍吗?或者说,真的有必要分set和trySet吗?

引以为豪的observer和computed是bug的巢穴

observer与computed同样都依赖其他key,是个被动的角色,但两者是完全不一样的。来看个例子:

my-component.js:

export default Ember.Component.extend({
    value: 0,
    testValue: 0,
    observeValue: Ember.observer(‘value‘, function() {
        return this.set(‘testValue‘, this.get(‘value‘) + 1);
    }),
    testValue2: 0,
    computedValue: Ember.computed(‘value‘, function() {
        return this.set(‘testValue2‘, this.get(‘value‘) + 1);
    })
});

my-component.hbs:

<p>value: {{value}}</p>
<p>testValue: {{testValue}}</p>
<p>testValue2: {{testValue}}</p>
{{yield computedValue observeValue}}

以上我定义了一个my-component,假设我在某个页面调用了它:

{{input value=test}}
{{my-component value=test}}

当这个页面被初始化的时候,它会建一个my-component的实例,并在my-component初始化时将value传递给这个实例。显示的结果为:

<p>value: </p>
<p>testValue: 0</p>
<p>testValue2: 0</p>

当我在textbox里输入1,则显示为:

<p>value: 1</p>
<p>testValue: 2</p>
<p>testValue2: 0</p>

为啥?

test的初始值是undefined, 属性值的传递是在初始化时发生,observeValue中的回调方法不会被触发。而computedValue呢,它的回调是只有在被’get’的时候(在template中调用,或是某处运行了this.get(‘computedValue‘))才会被触发。

若想初始化时便触发observeValue,就要改成:

...
    observeValue: Ember.on(‘init‘, Ember.observer(‘value‘, function() {
        return this.set(‘testValue‘, this.get(‘value‘) + 1);
    })),
    // 或是
    // observeValue: Ember.observer(‘value‘, function() {
    //  return this.set(‘testValue‘, this.get(‘value‘) + 1);
    // }).on(‘init),
...

改完后刷新显示结果:

<p>value: </p>
<p>testValue: NaN</p>
<p>testValue2: 0</p>

若想触发computedValue的回调,则可以需要调用它:

{{input value=test}}
{{#my-component value=test as |computedValue|}}
    <p>computedValue: {{computedValue}}</p>
{{/my-component}}

改完后显示结果:

<p>value: </p>
<p>testValue: NaN</p>
<p>testValue2: NaN</p>
<p>computedValue: NaN</p>

文本框输入1后显示结果:

<p>value: 1</p>
<p>testValue: 2</p>
<p>testValue2: 2</p>
<p>computedValue: 2</p>

另外,’get’ observeValue并不会如computed一样拿到testValue的值。

还有值得注意的一点是,千万不要把computed当普通值使用!举个例子:

...
    value: 0,
    computedValue: Ember.computed(‘value‘, function() {
        return this.get(‘value‘) + 1;
    }),
    observeComputedValue: Ember.observer(‘computedValue, function() {
        alert(‘computedValue changed!‘);
    })
...

要是这个computedValue没有被’get’过,这个alert就永远都不会被触发。

得绕过的动态依赖

碰到过一个难题。。。让我上个代码:

Controller:

export default Ember.Controller.extend({
    listType: ‘list‘,
    list: [0, 1, 2, 3, 4, 5, 6, 7],
    // 原始列表中的偶数
    filteredList: Ember.computed.filter(‘list.[]‘, function(item) {
        return item % 2 === 0;
    }),
    actions: {
        addItem(item) {
            this.get(‘list‘).addObject(item);
        },
        toggleList() {
            this.set(‘listType‘, this.get(‘listType‘) === ‘filteredList‘ ? ‘list‘ : ‘filteredList‘);
        }
    }
});

Template

<div>
    {{input type=‘button‘ value=‘Add Item‘ click=(action ‘addItem‘ list.length)}}&nbsp;
    {{input type=‘button‘ value=‘Toggle List‘ click=‘toggleList‘}}
</div>
{{#each (get listType) as |item|}}
    <div>{{item}}</div>
{{/each}}

这个例子说的是我有2个列表,在这个template里我可能需要切换正在显示的列表,而且这2个列表中的内容可能都会发生变化。

在Template中我使用了{{get listType}} helper来实现这一点。但假设此时多了需要在controller中根据目前显示的列表做一些计算操作的需求,比如:

...
    displayListLength: Ember.computed(‘listType‘, ‘list.length‘, ‘filteredList.length‘, function() {
        return this.get(this.get(‘listType‘)).length;
    }),
...

这个写法无疑看起来很愚蠢,需要同时依赖所有可选项才能得到正确的值。这正是因为这里真正的依赖项是会变化的,依赖项的值也是会变化的,也就是在Controller里实现{{get listType}}能做到的事情是要付出很大代价的。。。最佳的方案是一定要绕过这种情况。此处改进方案是把{{#each}}那部分代码作为component独立,这样就能将当前显示的列表作为一个单一的依赖项来处理了。

一不小心就会出现,却很难修的Modified twice异常

Ember为了避免在一个run loop的render前后设置同一个值,导致触发多次重复渲染,减慢显示速度,在检测到这种情况时会抛出异常。每次碰到这个”Modified twice”异常都想shi。因为Ember就只管抛异常,那个值触发了异常,但不会让你知道具体哪行导致了这个结果。想知道?哼哼……靠猜靠推理咯。

绕过这个异常的最简单的方法是利用Ember.run.next(),但是要小心使用后带来的副作用。。。总之写起来步步维艰。(一时想不起来例子了,找到再补吧)

需要手动清理的数值

Controller以及Route的实例在某个版本后就不自动清理了,因为Ember说自动回收会造成很多问题。不自动回收也造成很多问题啊妈蛋!

举例。假设有这么一个route: /post/:postid

Route:

export default Ember.Route.extend({
    model(params) {
        return this.store.findRecord(‘post‘, params.postid);
    }
});

Controller

export default Ember.Controller.extend({
    test: 0,
    actions: {
        updateTest() {
            this.set(‘test‘, this.get(‘model.id‘));
        }
    }
});

Template

{{input type=‘button‘ value=‘Update Test Value‘ click=‘updateTest‘}}

然后我的操作步骤是:

1. 打开/post/1

2. 点击“Update Test Value”按钮

3. 在没有刷新的情况下兜兜转转又打开了/post/8

那么这个时候test的值是什么呢?答案是1。

这种情况下我通常会通过Ember.Route中的afterModelsetupController复原这些属性值来解决这个问题。

无连接错误与其他错误的区分

在无网络连接情况下的报错和服务器报错都会进Promise.catch回调中。如果用了Ember.RSVP.allSettled,则state都为’rejected’。

当年纯真无知的我们没有想到处理这种情况,以至于断网状态下操作也会显示类似“服务器发生错误”这种错误提示。。。

目前所知最好的解决方式是利用jQuery:

$.ajaxSetup({
    beforeSend(xhr, hash) {
        if (!navigator.onLine) {
          alert(‘Network error!‘);
          return xhr.abort();
        }
    }
});

不过要注意目前浏览器判断网络是否在线的支持非常有限:http://caniuse.com/#feat=online-status

Ember Data

用Ember-data的RESTAdapter/Serializer简直是大错特错。。。现在Ember团队都已经放弃了它,转而推荐JSONAdapter/Serializer了。。

假设我定义了个my-model:

export default DS.Model({
    name: DS.attr(‘string‘),
    count: DS.attr(‘number‘),
    isPublic: DS.attr(‘boolean‘),
    extData: DS.attr(‘json‘),   // 自定义json transform
    collection: DS.belongsTo(‘collection‘),
    models: DS.hasMany(‘item-model‘),
    computedName: Ember.computed(‘name‘, ‘count‘, function() {
        return `${this.get(‘name‘)} (${this.get(‘count‘)})`;
    })
});

store.createRecord()可能跟期望的不同

假设我新建了一条记录如下:

const newModel = this.get(‘store‘).createRecord(‘my-model‘, {
    name: ‘test name‘,
    count: 0,
    isPublic: 1
});

然后当你获取这个newModel的时候就发现,里面只有你传进去的三个键。。其他都是undefined。哪怕是boolean类型的isPublic也不会被转化为true/false,computedName更不会有。

有dirty/clean状态的只有string, number, boolean和date这四种基础类型

这就意味着以下这些属性或方法对自定义类型或者relationShip是不管用的:

- .hasDirtyAttributes

- .rollBackAttribute()

- .rollBackAttributes()

- .changedAttributes()

store.pushPayload()没有返回值

除了createRecord之外,想要将一条数据记录手动放进store里还可以用store.pushPayload()。不同的是,这个方法接受的是服务器返回的原始格式。

然而,最让人不解的是,这货没有返回值。其实是有的,要开Ember feature flag才行。正常情况下,需要this.get(‘store‘).peekRecord(MODEL_NAME, id)来帮助完成。

在route.deactivate使用store.unloadRecord(), store.unloadAll()所带来的烦恼

有时为了避免内存的数据引起一些显示上的问题,会在route的deactivate事件里使用.unloadRecord()和.unloadAll(),然后发现这个坑有点大。

比如说我有/todo/today/todo/tomorrow两个route。

routes/todo/today.js:

export default Ember.Route.extend({
    model() {
        return this.store.query(‘todo‘, {
           type: ‘today‘
        });
    },
    deactivate() {
        this.unloadAll(‘todo‘);
    }
});

templates/todo/today.hbs:

{{#link-to ‘todo.tomorrow‘}}Todo of Tomorrow{{/link-to}}
...

routes/todo/tomorrow.js:

export default Ember.Route.extend({
    model() {
        return this.store.query(‘todo‘, {
           type: ‘tomorrow‘
        });
    }
});

templates/todo/tomorrow.hbs:

<ol>
    {{#each model as |todo|}}
        <li>{{todo.todoContent}}</li>
    {{/each}}
</ol>

然后我从/todo/today点击“Todo of Tomorrow”的链接到/todo/tomorrow的时候发现尴尬了,应该有内容的,却是啥都木有。。。也就是说莫名的在下一个route运行到请求model这一步并把记录写进内存时deactivate hook里的.unloadAll()还没有运行完或者在此之后才运行,导致我请求的记录从store里被干掉了。。。

DS.PromiseObject(), DS.PromiseArray()的问题

这我觉得应该是个bug。不知道哪个版本起这两个class就出怪问题了。

正常用法:

Controller

...
    promiseItem: Ember.computed(‘model‘, function() {
        return DS.PromiseObject.create({
            promise: this.get(‘store‘).findRecord(‘post‘, this.get(‘model.postid‘))
        });
    }),
...

Template

{{promiseItem.title}}

从升级到某个版本之后,偶尔会有情况要这样才能读到值:

{{promiseItem.content.title}}

暂时就先那么多了,下次有心情了吐槽一下ember-engine,以及写addon,写component的一些问题。

时间: 2024-10-12 19:50:13

Ember.js的那些坑的相关文章

ember.js的环境(window)安装及创建应用

现如今.我们经常都可以看到复杂的JavaScript应用程序,由于这些应用程序变得越来越复杂,一长串的jQuery回调语句或者通过应用程序在各个状态执行不同的函数调用,这些做法都会变得无法再让人接受,这导致了JavaScript开发人员开始寻找一种组织和效率更优秀的开发方式.实现组织和效率的其中一个最常用的架构模式,就是我们熟知的Model View Controller (MVC)模式,这种模式鼓励开发人员将其应用程序的不同部分分割为更易于管理的模块,我们不必使用一个函数直接调用数据库,通过创

[Ember] Ember.js Templates

In this lesson, we'll go over some of the basics of Ember.js templates and how they work with controllers. Generate a controller: ember g controller hello Generate a Template: ember g template application Template syntax: application.hbs: {{#if showN

点燃圣火! Ember.js 的初学者指南

转自:http://www.adobe.com/cn/devnet/html5/articles/flame-on-a-beginners-guide-to-emberjs.html 作者 Andy Matthews 现在,到处都可以看到复杂的 JavaScript 应用程序. 由于这些应用程序变得越来越复杂,一长串的 jQuery 回调语句,或者通过应用程序在各个点执行不同的函数调用,这些都变得无法再让人接受. 这导致了 JavaScript 开发人员了解到传统的软件程序员已经知道了几十年的问

Ember.js入门教程、博文汇总

第一章 对象模型 Ember.js 入门指南——类的定义.初始化.继承 Ember.js 入门指南——类的扩展(reopen) Ember.js 入门指南——计算属性(compute properties) Ember.js 入门指南——观察者(observer) Ember.js 入门指南——绑定(bingding) Ember.js 入门指南——枚举(enumerables) Ember.js 入门指南之七第一章对象模型小结 第二章 模板 Ember.js 入门指南——handlebars基

Ember.js 入门指南——属性传递

1,传递参数到组件上 每个组件都是相对独立的,因此任何组件所需的数据都需要通过组件的属性把数据传递到组件中. 比如上篇<Ember.js 入门指南--组件定义>的第三点"{{component item.pn post=item}}"就是通过属性post把数据传递到组件foo-component或者bar-component上.如果在index.hbs中是如下方式调用组件那么渲染之后的页面是空的. {{component item.pn}} 请读者自己修改index.hbs

Ember.js 目前非常流行的H5框架

Ember.js是一个MVC的JavaScript框架,由Apple前雇员创建的SproutCore 2.0改名进化而来,号称「A framework for creating ambitious web applications」. 简介 Emberjs--一个用于创建 web 应用的 JavaScript MVC 框架,采用基于字符串的Handlebars模板,支持双向绑定.观察者模式.计算属性(依赖其他属性动态变化).自动更新模板.路由控制.状态机等. Ember使用自身扩展的类来创建Em

Ember.js 的视图层

本指导会详尽阐述 Ember.js 视图层的细节.为想成为熟练 Ember 开发者准备,且包 含了对于入门 Ember 不必要的细节. Ember.js 有一套复杂的用于创建.管理并渲染连接到浏览器 DOM 上的层级视图的系 统.视图负责响应诸如点击.拖拽以及滚动等的用户事件,也在视图底层数据变更时更 新 DOM 的内容. 视图层级通常由求值一个 Handlebars 模板创建.当模板求值后,会添加子视图.当 那些 子视图求值后,会添加它们的子视图,如此递推,直到整个层级被创建. 即使你并没有在

MVC、MVP、MVVM、Angular.js、Knockout.js、Backbone.js、React.js、Ember.js、Avalon.js 概念摘录

转自:http://www.cnblogs.com/xishuai/p/mvc-mvp-mvvm-angularjs-knockoutjs-backbonejs-reactjs-emberjs-avalonjs.html MVC MVC(Model-View-Controller),M 是指业务模型,V 是指用户界面,C 则是控制器,使用 MVC 的目的是将 M 和 V 的实现代码分离,从而使同一个程序可以使用不同的表现形式. 交互方式(所有通信都是单向的): View 传送指令到 Contro

Ember.js 入门指南——控制器(controller)

ember new chapter5_controllers cd chapter5_controllers ember server 从本篇开始进入第五章控制器,controller在Ember2.0开始越来越精简了,职责也更加单一--处理逻辑. 下面是准备工作. 从新创建一个Ember项目,仍旧使用的是Ember CLI命令创建. 在浏览器执行项目,看到如下信息说明项目搭建成功. Welcome to Ember 1,控制器简介 控制器与组件非常相似,由此,在未来的新版本中很有可能组件将会完