about 2 years ago

上一篇Hero App的功能還停留在新增英雄與點選英雄查看資料,這一章會補齊大多數功能,像是Task任務的管理以及分配任務給英雄,最後還有一個登入帳號的功能。
功能補齊後接著使用Routing,透過URL顯示不同的頁面。
首先看加入Routing前的模樣

chap7 from yuanchieh on Vimeo.

以下註記幾個功能

偽Set功能

在Typescript中,尚未支援ES2015中的Set和Map功能,但有個簡單的方法可以達到效果
在Task.Model中,我希望同個任務不要有重複相同的Hero,同樣的Hero也不要出現重複Task,實作方法如下

export class Task{
    public heroList:{[key:number]:Hero} = {};
    constructor(public title:string, public description:string, public payment:number, public state:string = 'open', public id?:number){}
    addNewHero(h:Hero){
        this.heroList[h.id] = h;
    }
    deleteHero(h:Hero){
        delete this.heroList[h.id];
    }
}

每個Task都有自己的heroList,而heroList就是一個Object,而加入hero時將hero.id設為物件的屬性,這樣重複加入hero時就不會重複創造物件的屬性,達到Set的效果!

Drag & Drop

拖拉在使用上相當有趣且方便,我新增一個Manage Component來處理任務加入的功能,將Hero和Task都條列出來,透過拖曳加入任務
manage.html

<ul class="list">
    <li *ngFor="let t of taskList" (dragover)="dragOverHandler($event)" (drop)="dropHandler($event,t.id)">
        <div> {{t.title}}</div>
        <div>
            ~~~屬於該Task的Hero清單~~~
        </div>
    </li>
</ul>
<ul class="list">
    <li *ngFor="let h of heroList" draggable="true" (dragstart)="dragStartHandler($event, h)">
        <div>{{h.name}}</div>
    </li>
</ul>

可以看到在 被拖拉的物件上(Hero)必須加上draggable="true"還有(dragstart)="dragStartHandler($event, h)"這個事件
而在 被放入的物件(Task)必須宣告(dragover)="dragOverHandler($event)"(drop)="dropHandler($event,t.id)"
接著看ManageComponent中函示的宣告

   dragOverHandler(ev){
        ev.preventDefault();
    }
    dropHandler(ev,id){
        ev.preventDefault();
        //讓Todo item的tag與tagService連動

        let h = JSON.parse(ev.dataTransfer.getData('text/plain'));
        this.taskList[id].addNewHero(h);
        this.heroList[h.id].addNewTask(this.taskList[id]);
        console.log(this.taskList[id].heroList);
    }
    dragStartHandler(ev, h){
        ev.dataTransfer.dropEffect = 'copy';
        ev.dataTransfer.effectAllowe = 'copy';
        ev.dataTransfer.setData('text/plain',  JSON.stringify(h));
    }

dragOverHandler最為單純,只是加上ev.preventDefault(),這是為了避免拖拉過程中瀏覽器重載,一定要加!
dragStartHandler中必須定義拖拉,這裡我希望複製被拖拉的物件,所以設為copy,且為了傳遞資料給Task,我將被選取的Hero轉為string以setData傳遞(以物件傳會失敗)
dropHandlergetData準備接收資料,接著就將Hero放入該Task中
Drag&Drop傳遞過程中還有很多事件會發生,透過更細部的綁定可以增加頁面的互動性,可以上MDN查詢 Drag&Dop
其他部分就大同小異,接著就要進入Routing階段,這篇會提到 設定Routing檔、基本的URL跳轉、URL如何夾帶參數等

Routing 設定檔

首先看整個Routing的結構


最一開始/表示根目錄,接著使用者必須先登入account才能進到main,main就放task、hero等,透過nav進行切換
以下是版本二

chap7-2 from yuanchieh on Vimeo.

首先要新增app.routes.ts

const APP_ROUTES: Routes = [
    { path:'account', component: AccountComponent},
    { path:'main', component: MainComponent, children: MAIN_ROUTES},
    { path:'', component: AccountComponent, pathMatch: 'full'},
];

export const routes = RouterModule.forRoot(APP_ROUTES);

其中有幾個主要的東西,一個Routes(包含多個Route的陣列),以及最後輸出一個RouterModule.forRoot(APP_ROUTES)
首先看Route寫法,最基本必須包含path,path的宣告開頭不用"/"! 還有相對應路徑要渲染的Component
pathMatch: 'full' 表示路徑必須完全匹配才會跳轉,Angular2在匹配路徑時採優先匹配,也就是說對到了就直接跳轉。
接著在 app.module記得在imports 加上routes。
另外在 path:main 最後有一個 children,這表示 /main底下還有子路由。
最後輸出時要注意 如果是根路由(全App唯一一個)就必須用RouterModule.forRoot

接著看main.routes.ts的寫法

export const MAIN_ROUTES: Routes = [
    { path:'main/hero', component: HeroComponent},
    { path:'main/task', component: TaskComponent},
    { path:'main/manage', component: ManageComponent},
    { path:'main', component: TaskComponent}
];

注意,這裡只需要定義Routes就好。
記得在main.module.ts中import RouterModule

上述改了app.module.ts(加入 routes)、main.module.ts(加入RouterModule)並新增了app.routes.tsmain.routes.ts這兩個路由設定檔。
路由設定好了之後,Angular2怎麼知道路由要渲染的Component要加在頁面的哪裡呢?
這時候就需要<router-outlet></router-outlet>,Angular2看到這個Element會自動將Routing中的Component代換。
所以最後在app.component.html中加入

<section>
    <router-outlet></router-outlet>
</section>

同理在main.html也要加入,<my-nav>是我新增的NavComponent,待會用來當導覽列的。

<section>
    <my-nav></my-nav>
    <router-outlet></router-outlet>
</section>
使用<a>連結跳轉頁面,避免頁面重載

在版本二操作影片中,都是在瀏覽器直接輸入Url,這樣缺點是輸入後整個App會重新載入,所以在/main/manage中沒有顯示任何資料也是這個原因所造成的,接下來會加入NavBar,使用link點選跳轉頁面就不會有這個問題。
同時專案也做一些小調整,原本在HeroComponent中有自動創建一些Hero,這裡就全部都拿掉了
操作畫面可以看到,原先ManageComponent的問題解決了。

chap7-3 from yuanchieh on Vimeo.

以下是nav.html寫法

<ul>
    <li>
        <a [routerLink]="['/main/hero']">Hero</a>
    </li>
    <li>
        <a [routerLink]="['/main/task']">Task</a>
    </li>
    <li>
        <a [routerLink]="['/main/manage']">Manage</a>
    </li>
</ul>

anchorTag當中使用 routerLink,routerLink後面宣告的路徑方式有數種,一種是使用Array,如['/hello','world','2'] 這會等同於 使用字串"/hello/world/2",記得開頭要加"/"顯示為從根目錄開始。

以上是最基本的Routing方式,綁定在routes.ts並宣告在module中以及透過routerLink跳轉頁面。
接著 我希望Hero底下有個子路由,點選清單中的英雄後頁面跳轉至/main/hero/{id},頁面中顯示HeroDetailComponent;接著HeroDetailComponent讀取URL中的{id}並取出相對應的資料;
最後原本的say hello按鈕變成回到/main/hero的按鈕。

在URL中加入Params並讀取

首先在main.routes中加入這個新的routing path

{ path:'hero/:id', component: HeroDetailComponent},
{ path:'task/:id', component: TaskDetailComponent},

hero後的:id表示說這個字段儲存到id變數中,也就是說/main/hero/2,此時id為2的意思。
接著到HeroDetailComponenet中,原本是使用@Input和@Output與HeroComponent耦合在一塊,現在改用讀取Url方式取得selected Hero。

export class HeroDetailComponent implements OnInit,OnDestroy{
    private selectedHero:Hero;
    private subscribe:Subscription = new Subscription();
    constructor(private activatedRoute:ActivatedRoute, private router:Router, private heroService:HeroService){}
    values(obj) : Array<string> {
        return Object.values(obj);
    }
    ngOnInit(){
        this.subscribe = this.activatedRoute.params.subscribe((params)=>{
            console.log(params['id']);
            if(this.heroService.getHeroById(params['id'])!=null){
                this.selectedHero = this.heroService.getHeroById(params['id']);
            }
        });
    }
    ngOnDestroy(){
        this.subscribe.unsubscribe();
    }
    onClick(){
        this.router.navigate(['/main/hero']);
    }
}

首先看到 constructor,在先前Service篇談過 如果希望在Comp中使用Service必須在constructor中注入,這裡除了自定義的HeroService外,還加入了ActivatedRoute:用來讀取URL參數 以及 Router:用來控制路由

constructor(private activatedRoute:ActivatedRoute, private router:Router, private heroService:HeroService){}

注入Service後,取得路由參數的方法如下

private subscribe:Subscription = new Subscription();
ngOnInit(){
    this.subscribe = this.activatedRoute.params.subscribe((params)=>{
        console.log(params['id']);
        if(this.heroService.getHeroById(params['id'])!=null){
            this.selectedHero = this.heroService.getHeroById(params['id']);
        }
    });
}
ngOnDestroy(){
    this.subscribe.unsubscribe();
}

這裡使用到Angular2的依賴的外部Library之一 RxJS,RxJS主要提供Observer Pattern,簡單來說就是提供註冊Observable被觀察者,也是data提供方,被觀察者會定義何時資料產生(如 AJAX、按鈕按下)等,當有人想要取得這些資料,就必須透過subscribe訂閱方式,之後被觀察者有資料就會通知subscribe。
例如說 我有一個Observable是 按鈕按下後會發出"A"這個字元(data),那我可以定義一個Subscribe收到資料後打印出來 console.log(data),之後按鈕按下就會 執行console.log(data); //A
可以先理解成 功能比較多且可以使用多次的 Promise

RxJS之後在HTTP章節會在解釋,這裡只要先知道基本的觀念,另外在this.activatedRoute.params.subscribe()中放的是資料產生時的callback function,在function中我們取得了params這個參數,透過params['']裡頭放定義在routes中的參數名稱即可取得值。
最後避免memory leak必須在Component摧毀時註銷subscibe,關於Component的生命週期後續補充。

最後一個this.router.navigate([]),裡頭路由宣告方式類似於routerLink,可以由按鈕觸發頁面跳轉。

onClick(){
        this.router.navigate(['/main/hero']);
}
在URL中加入QueryParams

剛才使用的Params必須事先在routes宣告,所以在使用上比較制式;但如果希望傳遞一些較複雜參數如after='12/31/2015' & before='1/1/2017'這類型的無法符合當前的URL規範,也就無法適用Params寫法;
Angular2提供另一種方式Query Paramse,透過分號與原先的URL分隔,這是利用

Matrix URL notation: 雖然它不是HTML標準之一,但是各大瀏覽器都有支援,主要是在URL中提供提供獨立參數

範例大致如下:localhost:3000/heroes;id=15;foo=foo,URL中包含兩個QueryParams:id and foo
在使用上QueryParams不用宣告在Routes中,大大提升使用的方便性
實際寫法如下:

this.router.navigate(['/main/hero',id], {queryParams:{'name':"Hello", 'num':10}});

<a [routerLink]="['/main/manage']" [queryParams]="{'url':'manage'}" [preserveQueryParams]="true">Manage</a>

有兩種寫法,一種是在router.navigate中,在路由陣列後方加入queryParams並附上JSON物件;
在routerLink中,要加入queryParamsDirective才可以,至於preserveQueryParams是表示 當其他URL在跳轉時不要r將queryParams移除(注意用法,假設我從/hero?name=hero轉入/manage,而/manage有宣告自己的queryParams name="manage"且選擇保留queryParams,此時保留的會是轉入的/hero?name=hero)

讀取方式類似於Params,同樣適用Subscribe

this.subscribeQ =  this.activatedRoute.queryParams.subscribe((params)=>{
        console.log(params,params['name']);
});
在URL中加入fragment

如果文章很長,你可能會在每個段落加入id並從目錄中以fragment形式跳轉段落,加入fragment後URL在最後會加上#id
實際寫法跟QueryParams一樣

this.router.navigate(['/main/hero',id], {queryParams:{'name':"Hello", 'num':10}, fragment:id});

<a [routerLink]="['/main/manage']" [fragment]="'id'">Manage</a>

讀法如下

this.subscribeF =  this.activatedRoute.fragment.subscribe((f)=>{
        console.log(f);
});

最終完成影片

chap7-4 from yuanchieh on Vimeo.

總結

路由設定起來有點麻煩,但只要弄清楚

- 先設定好Route.ts 路由設定檔([{path:"" component:~}]) => import到Module中,並在HTML中加入router-outlet
- 路由設定上要考慮是否使用Params 格式化的參數
- 在使用路由上,兩種基本的跳轉方式 `router.navigate`和\<a [routerLink]>
- 選擇性加入`qeuryParams`和`fragment`並使用activatedRoute關注變化

下一章要探討路由的進階篇,先前設定了AccountComponent,理論上使用者要先登入才能使用後續的MainComponent,這部分的安全把關如何在NG2中實作?又如何在使用者表單尚未填寫完成前提醒使用者不能關閉呢?這在下一章都會提到。

← Firebase網頁教學[四]-Storage篇 Angular2系列文七-Routing進階篇 →
 
comments powered by Disqus