首页 > 代码库 > Ember.js的那些坑

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=(actionmyActiontrue)}}

对应的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 =http://www.mamicode.com/> this.set(‘value‘, data));
    }
});

调用这个component的template:

{{test}}
{{#if isDisplay}}
    {{my-component value=test action=(action setisDisplayfalse)}}
{{/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=‘buttonvalue=‘Add Itemclick=(actionaddItemlist.length)}}&nbsp;
    {{input type=‘buttonvalue=‘Toggle Listclick=‘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=‘buttonvalue=‘Update Test Valueclick=‘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的一些问题。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Ember.js的那些坑