[Tips on Ember 2] UI 规划与运用状况的关联处置惩罚

引子

SPA(单页面运用)的中间是什么?

自该范例运用降生以来我最多思索的题目就是这个。如今前端 SPA 框架满天飞,许多不是框架的也被称作框架,终究有什么代表性的层(layer)能让一个系统称得上是框架?

我的答案是路由,而路由的实质就是一个状况治理器。没有路由机制的系统不能称之为框架,而路由机制做得不好的框架也算不上好框架(但可以算是好的东西鸠合,比方 Angular——详见我在 Ruby China 上曾吐过的槽)。

为何这么说呢?我们都晓得 HTML 是无状况的(stateless),做一堆 HTML 页面拼在一同那不叫“运用”,顶多称之为“内容系统”;在之前,HTML 网站上的状况治理是由后端的 Session 加前端的 Cookies 合作完成的,到了 SPA 的时期 Session 不是必需的了(只管传统的 Session 机制也是可用的),UI 上的状况转移到了前端由 JavaScript 完全管控(由于 SPA 前后星散的特征),所之前端工程师担当起了更多的营业逻辑职责,相应的全部手艺链上也必需有一个牢靠的环节来协助他们做状况治理这件事变。

在前端框架的生长历程当中路由的降生是瓜熟蒂落的(基于一些新手艺的成熟,比方 HTML5 的History API 等等),然则运用开辟工程师关于路由的明白和注重却还远远不够。假如说传统的前端开辟是以页面为中间来入手的话,那末当代的 SPA 运用开辟就是以状况为中间来着手想象和开辟的。

Ember 就是一款非常注重路由组件的 SPA 框架,本文借由一个完成 UI 规划的例子来谈谈 UI 编程与路由的关联,只管这只是涉及到路由特征的一部份却也充足申明一些题目了。愿望这个例子能让更多前端工程师熟悉和明白路由的重要性,从而更好的想象与完成 SPA 运用的种种功用场景。

场景形貌

多半运用都有以下所述的 UI 想象:

  1. 多半视图在一个通用的规划内显现,比方典范的 Header + Main 的规划

  2. 平常视图须要一个特定的规划,比方登录和注册页面不须要 Header 等等

关于这些场景来讲,那些反复的 HTML 构造(如 Header 和 Footer)一定须要某种体式格局的笼统使得它们可以复用也许指定衬着照样不衬着。后端衬着手艺使用了一些机制(如 helpers 等) 来协助开辟者在视图层完成这些逻辑,比及返回给浏览器的时刻已经是完全的 HTML 了(固然也有 Turbolinks 如许融会了部份前端路由特征的新手艺,本文不做进一步形貌),这显然是不适合前端运用的场景的,由于关于 SPA 运用来讲用户替换 URLs 时须要在浏览器端立即拼装终究的完全视图,并不存在“预先衬着好的页面一同托付过来”这么一说。我们须要先思索一下高层想象,看看有什么机制可以应用的。

开端剖析

路由是怎样治理状况的?庞杂的话题简朴说:

In Ember.js, each of the possible states in your application is represented by a URL.
在 Ember.js 中,运用的每个可以的状况都是经由历程 URL 表现的。

这是官方文档里所总结的,我来试着举例表述一下:

假定当前有以下路由定义:

let Router = Ember.Router.extend()

Router.map(function() {
    this.route('dashboard', { path: '/dashboard' })
    this.route('signin', { path: '/signin' })
})

因而,当用户——

  1. 进入 /dashboard URL 的时刻,对应的 dashboard 路由最先接受运用的当前状况

  2. 进入 /signin URL 的时刻,对应的 signin 路由最先接受运用的当前状况

  3. 但更重要的是:一切的路由都有一个共有的顶级路由——application 路由,其重要性重要体如今:

    1. 它是唯一一个靠谱的可以用来治理全局局限状况的路由

    2. 它为一切子路由的视图衬着供应了模板的进口(outlet)

接着题目来了:假如说状况经由历程 URL 来表现,那末 UI 规划的差别怎样表现呢?比方:

  1. 进入 /dashboard URL 的时刻,我们须要 Header + Main 的规划

  2. 进入 /signin URL 的时刻,我们不须要 Header

  3. 不管何种情况,application 路由在个中的作用……?

第一次尝试

由于每个路由都邑衬着本身的模版,我们可以做一个最简朴的尝试:

{{!app/pods/application/template.hbs}}
{{outlet}}
{{!app/pods/dashboard/template.hbs}}
<header>...</header>
<main>
    ...
    {{outlet}}
</main>
{{!app/pods/signin/template.hbs}}
<main>
    ...
    {{outlet}}
</main>

虽然这么做可以见效,但是题目也是不言而喻的:假如涌现多个和 dashboard 一样的规划构造,我们将不能不屡次反复 <header></header>;曾 Ember 有 {{partial}} 如许的 helper 来做模版片断复用,然则第一,今后没有 {{partial}} 了,二来用 {{partial}} 做规划是毛病的挑选。

题目剖析

假如我们可以把题目场景简化为只需一种可以,比方“一切的视图都用 Header + Main 的规划”,那末处理计划可以简化为:

{{!app/pods/application/template.hbs}}
<header>...</header>
<main>
    {{outlet}}
</main>
<footer>...</footer>
{{!app/pods/dashboard/template.hbs}}
...
{{outlet}}
{{!app/pods/signin/template.hbs}}
...
{{outlet}}

那末再次恢复本来的场景请求,题目变成了:“进入 /signin 以后,怎样隐蔽 application 模版里的 <header></header>

第二次尝试

隐蔽模版里的片断,最简朴的要领可以这么做:

{{!app/pods/application/template.hbs}}
{{#if showNavbar}}
<header>...</header>
{{/if}}

<main>
    {{outlet}}
</main>

我们晓得模版内可接见的变量可以经由历程控制器来设置,但此时我不盘算建立 ApplicationController,由于路由里有一个 setupController 的钩子要领能帮我们设置控制器的(更重要的缘由是很快 Routable Components 将庖代如今的 route + controller + template 的分层系统,所以从如今最先最好尽量少的依托 controller),碰运气:

// app/pods/application/route.js
export default Ember.Route.extend({
    setupController(controller) {
        this._super(...arguments)
        controller.set('showNavbar', true)
    }),
})

如今一切的状况都邑显现 header 部份了,那怎样让 /signin 不显现呢?也许如许……?

// app/pods/signin/route.js
export default Ember.Route.extend({
    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set('showNavbar', false)
    }),
})

以下是测试效果(这里发起先写 Acceptance Test,省时间且不轻易讹夺),在每次革新页面后:

从…到…效果
//dashboard胜利
/dashboard/胜利
//signin胜利
/signin/失利
/dashboard/signin胜利
/signin/dashboard失利
/signin/dashboard失利
/dashboard/signin失利

我们在测试中增加了 /dashboard 的接见,然则我们并没有定义位于 DashboardRoute 里的 setupController 钩子,这是由于我们希冀 /dashboard 可以继续 / 的状况,不然一切的路由都要设置类似的 setupController 会把人累死,但是测试效果可以会让初学者以为摸不着头脑,我们试着剖析一下好了:

  1. //dashboard 都须要 showNavbar === true,所以正反都可以;

  2. 当自 /signin 革新页面的时刻,先实行了 ApplicationRoute 然后才是 SigninRoute,比及进入 / 的时刻,setupController 不会再次实行的;

  3. 同上;

  4. 同上。

题目剖析

这里最显著的题目就是 ApplicationRoute#setupController 这个钩子要领是不牢靠的,你只能保证它的第一次运转,一旦变成了在路由之间往返跳转就无效了。

实际上,setupController 的作用是将 model 钩子返回的效果绑定在对应的控制器上的,你可以扩大这个逻辑但也仅限于数据层面的设置。只需当挪用了 route#render() 且返回了与之前差别的 model 时 setupController 才会再次被挪用。

因而题目又变成了:有哪个钩子要领能保证在路由发生变化的时刻都可用?

路由的生命周期

这是一个非常重要但又很无趣的主题,我不想在这里反复那些可以经由历程浏览文档和亲测就可以得出的答案,不过我可以给出一份测试路由生命周期的完全代码片断:

https://gist.github.com/nightire/f766850fd225a9ec4aa2

把它们放进你的路由当中然后仔细观察吧。趁便给你一些履历之谈:

  1. 这个测试不要错过 ApplicationRoute,由于它是最特别的一个

  2. 其他的路由最少要同时测试两个,比方 IndexRouteTestRoute

  3. 不要只测试页面革新后的生命周期,还要尝试种种路由之间的互相过渡

测试完以后,你就会对全部路由系统有一个非常周全的相识了,这些体验会带给你一个重要的妙技,即是在未来你可以很轻易的定夺出完成一个功用应当从那里入手。关于我们这个例子来讲,比较重要的结论以下:

  1. ApplicationRoute 是一切路由的配合先祖,当你第一次进入运用程序——不管是从 / 进入照样从 /some/complicated/state 进入——ApplicationRoute 都是第一个实例化的路由,而且它 activated 就不会 deactivated 了(除非你手动革新浏览器)。因而我们可以把 ApplicationRoute 作为一个特别的永久激活的路由

  2. 假如你有运用逻辑依存于 ApplicationRoute#setupController,那末第一次进入就是唯一靠谱的时机——你不能希望这个钩子会在路由往返切换的时刻触发

  3. 然则其他路由上的 #setupController 钩子是会在每次过渡进来的时刻从新实行的

第三次尝试

基于以上剖析,我们可以调解我们的代码了:

// app/pods/application/route.js
export default Ember.Route.extend()
// app/pods/index/route.js and app/pods/dashboard/route.js
export default Ember.Route.extend({
    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set('showNavbar', true)
    },
})
// app/pods/signin/route.js
export default Ember.Route.extend({
    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set('showNavbar', false)
    },
})

我们把 ApplicationRoute#setupController 里的逻辑转移到了 IndexRoute#setupController 里去,就是由于当你接见 / 的时刻,ApplicationRoute#setupController 只会触发一次(第一次革新的时刻),而 IndexRoute#setupController 则可以保证每次都触发。如今,我们想象的场景可以完成了。

这个设定一最先看起来非常离奇,许多初学者都在这里被搞晕掉:“为何要有 IndexRoute?为何不直接用 ApplicationRoute?”

笼统路由

当我们刚最先打仗前端的路由机制时,我们很轻易把 ApplicationRoute/ 关联起来,可实际上真正和 / 关联的是 IndexRoute。假如你没有自行建立 IndexRoute,Ember 会帮你建立一个,但不管怎样 IndexRoute 都是必不可少的。

那末 ApplicationRoute 究竟扮演着一个什么样的角色呢?

先记着这个结论:在路由系统中,路由树中任何一个当前激活的途径都邑最少包含两个路由节点,而且个中一个必定是 ApplicationRoute这也恰是 ApplicationRoute 永久处于 activated 而永久不会 deactivate 的缘由地点。

举几个例子:

  1. 当接见 ‘/’ 时,路由树中当前激活的途径为:application => index

  2. 当接见 ‘/users/new’ 时,路由树中当前激活的途径为:application => users => new

  3. 当接见 ‘/posts/1/comments/1’ 时,路由树中当前激活的途径为:application => post => index => comment => index,也多是:application => posts => show => comments => show ——取决于你的路由划定规矩的写法

  4. 等等……

Ember 并没有为这个特别的 | 41b8a0714e572ed059c0e52d0e3c676c91 | 做一个明白的定义(然则| 41b8a0714e572ed059c0e52d0e3c676c92 |),不过在其他类似的路由系统里我们可以找到等价物——比方来自 | 41b8a0714e572ed059c0e52d0e3c676c93 |(Angular 生态圈里最优异的路由系统)里的笼统路由(Abstract Route)

Ember 的 ApplicationRoute 和 ui.router 的笼统路由非常类似,它们的共性包含:

  1. 都可以具有子路由

  2. 本身都不能被直接激活(不能位于路由树中当前激活途径的极点)

  3. 不能直接过渡,也就是 transition to;Ember 里会等价于过渡到 IndexRoute,ui.router 则会抛出非常

  4. 都有对应的模版、控制器、数据进口、生命周期钩子等等

  5. 当其下的恣意子路由被激活,作为父节点的笼统路由都邑被激活

固然,它们也有差别,比方说:你可以在 ui.router 的路由树中恣意定义笼统路由,不受数目和节点深度的限定,只需保证笼统路由不会位于某条途径的极点就是了;而 Ember Router 只需一个笼统路由(而且并没有明白的定义语法,只是行动类似——典范的鸭子范例想象嘛)且只能是 ApplicationRoute,你可以手动建立别的路由来模仿,然则 Ember Router 不会阻挠你过渡到这些路由,不像 ui.router 会抛出非常(这一点很轻易让初学者受阻)

实际上当你对 Ember Router 的明白日渐深切以后你会发明一切的嵌套路由(包含顶层路由)都是笼统路由,由于它们都邑隐式的建立对应的 | 41b8a0714e572ed059c0e52d0e3c676c98 | 作为该途径的顶节点,接见它们就即是接见它们的 | 41b8a0714e572ed059c0e52d0e3c676c99 |。我以为 Ember Router 的这个想象与 ui.router 比拟有利有弊:

  • 利:想象精致简朴,可以防止大批的 boilerplate 代码,路由的定义相对清晰简约

  • 弊:关于初学者来讲,由于不存在笼统路由的观点,很难深刻明白父子节点,特别是隐式 IndexRoute 的存在价值

这个计划充足圆满了吗?

不,还差一些。试想:当我们须要许多路由来构造运用程序的构造时,类似的 #setupController 岂不是要反复定义很屡次?怎样笼统这一逻辑让其变得易于复用和保护?

Thinking in Angular way(w/ ui.router)

在开辟 Angular 运用的时刻,类似场景的路由定义平常是如许的:

                   +----> layoutOne(with header) +----> childrenRoutes(like dashboard, etc.)       
                   |
                   |
application(root) -|
                   |
                   |
                   +----> layoutTwo(without header) +----> childrenRoutes(like signin, etc.)

我们用 Ember Router 也可以模仿如许的路由定义,完成一样的效果,代码类似:

// app/router.js
let Router = Ember.Router.extend({
  location: config.locationType,
})

Router.map(function() {
    // provide layout w/ <header></header>
    this.route('layoutOne', { path: '/' }, function() {
        this.route('dashboard', { resetNamespace: true })
        // ...
    })

    // provide layout w/o <header></header>
    this.route('layoutTwo', { path: '/' }, function() {
        this.route('signin', { resetNamespace: true })
        // ...
    })
})

然则个人非常不喜欢也不推重这么做,缘由是:

  1. 如许的路由定义写多了会很恶心

  2. 为了防止类似 /layoutOne/dashboard 如许的 URLs,不能不反复设定 path: '/' 来掩盖

    • ui.router 处理此题目依托的是 url pattern inheritence,由于每个路由的定义都必需指明 url 属性,所以也就习惯了

  3. 为了防止类似 layoutTwo.signin 如许的路由名字,不能不反复设定 resetNamespace: true

    • ui.router 处理此题目依托的是路由定义里的 parent 属性,所以子路由是可以离开定义的,不必嵌套也就无需 resetNamespace

对照两家的路由定义语法,各有优缺点吧,然则 Ember Router 向来都是以简明扼要著称的,至心不喜欢为了这个小小需求而把路由定义写得一塌糊涂

别的如许的路由想象还会致使 application 这个模版变成一个废料,除了 {{outlet}} 它啥也做不成,天生的 DOM Tree 里平白多一个标签看的人直恶心~

Thinking in Ember way

既然题目的实质是 #setupController 钩子须要反复定义,那末有无 Ember 作风方法来处理这一题目呢?

起首我们来考量一下 Mixin,你可以这么做:

// app/mixins/show-navbar.js
export default Ember.Mixin.create({
    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set('showNavbar', true)
    },
})

// app/mixins/hide-navbar.js
export default Ember.Mixin.create({
    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set('showNavbar', false)
    },
})
// app/pods/index/route.js and app/pods/dashboard/route.js
import ShowNavbarMixin from '../../mixins/show-navbar'

export default Ember.Route.extend(ShowNavbarMixin, {
    // ...
})

// app/pods/signin/route.js
import HideNavbarMixin from '../../mixins/hide-navbar'

export default Ember.Route.extend(HideNavbarMixin, {
    // ...
})

这么做倒也不是不可,然则——显著很蠢嘛——这和抽取两个要领然后随处挪用没有什么实质的区分,看起来我们须要的是某种程度上的继续与重写才对:

// somewhere in app/app.js
Ember.Route.reopen({
    // show navbar by default, can be overwriten when define a specific route
    withLayout: true,

    setupController() {
        this._super(...arguments)
        this.controllerFor('application').set(
            'showNavbar', this.get('withLayout')
        )
    },
})
// app/pods/index/route.js and app/pods/dashboard/route.js
// Do nothing if showNavbar: true is expected

// app/pods/signin/route.js
export default Ember.Route.extend({
    withLayout: false,
})

如许就好了,不须要分外的路由系统想象,就用 Ember 的对象系统便充足圆满。本文所形貌的这个例子实在非常简朴,我置信略有 Ember 履历的开辟者都能做出来,然则我的重点不在于这个例子,而在于对路由系统的一些论述和明白。这个例子来源自实在的事情,为了给同事诠释清晰最初的计划为何不可实在费了我好大工夫,因而我把全部梳理历程记录下来,愿望对初学者——特别是对 SPA 的中间还没有相识的初学者能有所助益吧。

基于事宜的处理计划

这个题目实在另有多种解法,基于事宜相应的解法我就在实际里演示了两种,不过比拟于上面的终究计划,它们照样稍微糙了些。在这里我写个中一种比较少见的,内里涉及到一些 Ember 的内部机制,权当是一个自创吧,思绪我就不多诠释了。

// app/mixins/hide-navbar.js
export default Ember.Mixin.create({
    hideNavbar: function() {
        this.set('showNavbar', false)
    }.on('init'),
})
// app/router.js
let Router = Ember.Router.extend({
    location: config.locationType,

    didTransition() {
        this._super(...arguments)

        let currentRoute = this.get('container')
        .lookup(`route:${this.get('currentRouteName')}`)

        this.get('container').lookup('controller:application').set(
            'showNavbar', _.isUndefined(currentRoute.get('showNavbar'))
        )
    }
})
// app/pods/signin/route.js
import HideNavbarMixin from '../../mixins/hide-navbar'

export default Ember.Route.extend(HideNavbarMixin, {
    // only use this mixin when you need to hide the Header
})

原文首发于 Ruby China 社区,转载请说明。

    原文作者:n͛i͛g͛h͛t͛i͛r͛e͛
    原文地址: https://segmentfault.com/a/1190000003727746
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞