Angular 5 開發一個有道翻譯

orangexc發表於2017-11-02

力爭國內 Angular 5 第一篇輪子

Github:github.com/OrangeXC/ud…
Link: incompetent-plantation.surge.sh/

最近輪子造的比較多,意在給初學者一個參考例子,目前反饋來看,如果技術棧不符,很少有人會點進來讀,以後可以考慮轉換博文型別了。

之前寫過一篇 Angular2 從搭建環境到開發,在 segmentfault 上得到了 2016 年第四季度的 top writer 文章表裡第四名,如今已經 angular 5

給大家的常規印象就是,大版本跳躍會帶來 breaking change,因為 angular 從 1.x 到 2.x 簡直是兩個框架,不對,就是兩個框架。

angular 1.x 叫 angular.js 而 angular 2.x 以後就叫 angular,兩個版本分別託管在兩個 github repo。

更讓我比較驚訝的是 angular.js 的 star 近乎 angular 的 double,而且社群更繁榮,本文苦在找能配合 angular 5 使用的元件,因為框架剛升級,相應的元件都還未更新,下文會告訴大家一個小技巧。

angular 2 到 4 到 5,的元件數成冪指數遞減,但是好在可以輕鬆向後擴充套件,如果熟悉 angular 2 那麼本專案完全可以看懂。

其實寫輪子看文件誰都會寫,我儘量多說些坑點,讓開發者少踩坑。

擼起袖子開整。

搭建開發環境

npm install -g @angular/cli@1.5.0複製程式碼

這裡直接把版本指向 1.5.0

ng new PROJECT-NAME
cd PROJECT-NAME複製程式碼

這時依賴已經安裝完成,執行 ng -v,可以看到如下

ng serve複製程式碼

預設 4200 埠,就可以看到初始化頁面了。

安裝過程可能較長,建議本地先安裝 yarn,安裝依賴的時候 cli 會自動使用 yarn 裝依賴,會快不少。

到這就可以開發了。

開發

udao 詞典的公開介面已經廢棄,這裡拿來的介面是非官方的,支援的功能有限

這裡明確要用的 UI 庫是 ng-bootstrap,loading 用的是 ngx-loading

安裝時會有依賴版本不符的警告,如下

但是勉強能用,前面說想找到合適的元件庫比較困難,這裡講個小技巧,去 google 搜 angular [some component] 基本都是 angular 1.x 的元件,那麼根據歷史分析元件命名有 ng- ng2-,到了 4 大家感覺心累所以乾脆叫 ngx-,搜尋直接搜 ngx-[some component]

這裡為什麼是 ng-bootstrap 而沒選 ngx-bootstrap 呢,這裡真的有 ngx-bootstrap,因為 ng-bootstrap 只支援 bootstrap4,後者支援 3 和 4,為了避免版本糾紛,直接用了 ng-bootstrap

說到這遠遠不能證明 angular 5 可以用這個庫,我的評判標準是 angular 4,如果支援 angular 4,那麼 90% 支援 angular 5,因為改動確實不大。

路由

目前此專案只涉及到 4 個路由。

  • / 主頁
  • /translate 翻譯
  • /search 模糊搜尋
  • /detail/:word 單詞詳情

app.module.ts 下面定義路由

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }, {
    path: 'translate',
    component: TranslateComponent
  }, {
    path: 'search',
    component: SearchComponent
  }, {
    path: 'detail/:word',
    component: DetailComponent
  }
]複製程式碼

這裡說下路由跳轉相關的問題,在 angular 5 裡依然分為 a 標籤的跳轉和 js 跳轉

  • a 標籤的寫法
@Directive({ selector: ':not(a)[routerLink]' })
class RouterLink {
  queryParams: {[k: string]: any}
  fragment: string
  queryParamsHandling: QueryParamsHandling
  preserveFragment: boolean
  skipLocationChange: boolean
  replaceUrl: boolean
  set routerLink: any[]|string
  set preserveQueryParams: boolean
  onClick(): boolean
  get urlTree: UrlTree
}複製程式碼

本專案例子:

<li class="nav-item">
  <a class="nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">主頁</a>
</li>
<li class="nav-item">
  <a class="nav-link" routerLink="/translate" routerLinkActive="active">翻譯</a>
</li>
<li class="nav-item">
  <a class="nav-link" routerLink="/search" routerLinkActive="active">搜尋</a>
</li>複製程式碼

這裡注意一個坑,第一個 li 標籤,多了 [routerLinkActiveOptions]="{exact: true}",如果不加的話,會導致 / 路由下,active 不觸發的情況。

  • js 寫法
class Router {
  constructor(rootComponentType: Type<any>|null, urlSerializer: UrlSerializer, rootContexts: ChildrenOutletContexts, location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler, config: Routes)
  get events: Observable<Event>
  get routerState: RouterState
  errorHandler: ErrorHandler
  navigated: boolean
  urlHandlingStrategy: UrlHandlingStrategy
  routeReuseStrategy: RouteReuseStrategy
  onSameUrlNavigation: 'reload'|'ignore'
  config: Routes
  initialNavigation(): void
  setUpLocationChangeListener(): void
  get url: string
  resetConfig(config: Routes): void
  ngOnDestroy(): void
  dispose(): void
  createUrlTree(commands: any[], navigationExtras: NavigationExtras = {}): UrlTree
  navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}): Promise<boolean>
  navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}): Promise<boolean>
  serializeUrl(url: UrlTree): string
  parseUrl(url: string): UrlTree
  isActive(url: string|UrlTree, exact: boolean): boolean
}複製程式碼

本專案例子:

gotoDetail ({ entry }) {
  this.router.navigate([`/detail/${entry}`])
}複製程式碼

兩個例子相比文件的概覽都是最簡單的用法,有需要的話可以看下其它方法,基本可以滿足所有的路由需求。

請求

這裡不同於 vue 和 react,angular 提供了前端全棧的解決方案,包含了 http 模組,只需要在 app.module.ts 裡面引入

import { HttpClientModule } from '@angular/common/http'

// ...
imports: [
  HttpClientModule
]
// ...複製程式碼

請求的語法也很簡單,具體可以到 github 看程式碼。

這裡說一個小坑,在實現 detail 路由的時候在 ngOnInit 鉤子裡拿到當前路由引數進行請求,改變路由時沒有觸發請求更新,最後改版如下。

程式碼如下

ngOnInit () {
  this.route.params.subscribe((params) => {
    this.loading = true

    const apiURL = `https://dict.youdao.com/jsonapi?q=${params['word']}`

    this.http.get(`/?url=${encodeURIComponent(apiURL)}`)
    .subscribe(res => {
      // set component data

      this.loading = false
    })
  })
}複製程式碼

之前無效是因為沒寫 this.route.params.subscribe((params) => {}),所以每次不會觸發監聽

這裡的 subscribe 會一直監聽 this.route.params 的變化。

請求路徑

如同 axios 的 baseURL,在請求時我們不希望每個請求都寫完整路徑,需要配置全域性的 baseURL 來使得請求路徑簡短。

angular 裡面需要一個 @Injectable,熟悉的概念——依賴注入,關於細則有跟多文章介紹,這裡說下針對此需求的解決方案

@Injectable()
export class ExampleInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const url = 'https://proxy-oagpwnbkpe.now.sh'

    req = req.clone({
      url: url + req.url
    })

    return next.handle(req)
  }
}
// ...
providers: [
  AppComponent,
  { provide: HTTP_INTERCEPTORS, useClass: ExampleInterceptor, multi: true }
]
// ...複製程式碼

這段程式碼解決了 baseURL 的問題。

請求轉發

注意到上一節的這裡 const url = 'https://proxy-oagpwnbkpe.now.sh',根路徑不是有道的路徑。

還是做了一層 node 的 proxy 處理。跨域問題還是要處理。

node 服務的程式碼也十分簡單,這裡使用了 fly 進行 node 端請求

程式碼如下

const express = require('express')
const fly = require('flyio')
const app = express()

app.use('/', async (req, res) => {
  const data = await fly.get(req.query.url).then(res => res.data)

  res.set('Access-Control-Allow-Origin', '*')

  res.send(data)
})

app.listen(process.env.PORT || 3001)複製程式碼

重點是在返回頭 set 一個 Access-Control-Allow-Origin: *,這樣瀏覽器就不會攔截請求了。

資料流動

detail 頁面,拆分了 5 個子元件,當然父子元件是十分簡單的單向資料流

例:父元件的 html 如下

<app-detail-phrs-list-tab [simple]="simple" [ec]="ec"></app-detail-phrs-list-tab>複製程式碼

子元件的 component.ts 如下

export class DetailPhrsListTabComponent {
  @Input() simple
  @Input() ec
}複製程式碼

就可以使用 @Input 取到父元件傳進來的值了,說到這裡全域性的狀態管理怎麼做,要看下專案的複雜度

簡單的全域性狀態管理可以建立一個 global.ts,再建立依賴注入,如下

// globals.ts
import { Injectable } from '@angular/core';

@Injectable()
export class Globals {
  role: string = 'test';
}複製程式碼

在元件中可以這樣呼叫

// hello.component.ts
import { Component } from '@angular/core';
import { Globals } from './globals';

@Component({
  selector: 'hello',
  template: 'The global role is {{globals.role}}',
  providers: [Globals]
})

export class HelloComponent {
  constructor(private globals: Globals) {}
}複製程式碼

另一種方式是 SPA 開發者熟悉的全域性狀態管理庫,如 flex, redux

angular 也提供了 angular-redux,複雜應用中建議使用。

打包上線

打包命令圍繞 ng build,提供幾種配置引數,這裡不贅述,

部署這裡使用的是 surge

友情提示:不要將私有專案部署到此類公開服務,弊端很多。

總結

不論哪種前端框架,都有它的長處,由於此專案較小,到這裡沒機會釋放 rxjs 的威力,angular-cli 預設裝了這個庫,處理複雜的非同步資料流非常高效,寫了好多輪子,畢竟還是樣例,但是,折騰不能停。

相關文件

本次 angular 5 更新相關文件如下

官方文件:next.angular.io/docs
官方部落格:blog.angular.io/version-5-0…
官方cli:github.com/angular/ang…

儘量翻牆檢視,國內 angular.cn/ 文件還沒更新。

相關文章