使用Angular與TypeScript構建Electron應用(五)

此次咱們開始關注Angular怎樣構建前端路由與邏輯,它與你之前熟悉的方式有一些區別,同時這部份內容很是充實,路由發生變化後原有的文件結構也隨之變化,有疑問請參見本次代碼變動的Commitjavascript

在進行新的開發以前咱們不妨對原有的爬蟲代碼作一些輕微的更改,在正式顯示這些內容時,僅僅有標題與文章詳情是遠遠不夠的,能夠加入相似於摘要、描述、閱讀量、發表人、發表日期等等字段,具體也根據實際爬取的頁面與業務需求更改,爲此我豐富了browser/task/ifeng.js中parseContent函數的代碼:html

// browser/task/ifeng.js
// ....
parseContent (html){
        if (!html) return;
        const $ = cheerio.load(html)
        const title = $('title').text()
        const description = $('meta[name="description"]').attr('content')
        const content = $('.yc_con_txt').html()
        const hot = $('span.js_joinNum').text()
        return {
            title: title,
            content: content,
            description: description,
            hot: hot,
            createdAt: new Date()
        }
    }

建立Angular子模塊

在Angular2中,模塊是用來描述各個組件之間關係的文件,就像是樹的枝幹,全部小的枝幹都聚集至此,在模塊中填充,模塊用一些特有的語法糖來描述它們之間的關係與依賴。在應用複雜時,樹的枝幹每每不止一根,咱們不可能將全部的文件所有掛載在根模塊中,這樣既不優雅也會致使打包的單個文件過大,影響頁面首次加載速度。爲此,咱們能夠在根模塊上註冊一些子模塊,用來描述徹底不一樣且可以獲得自治的子模塊。前端

「自治」是很是關鍵的一點,這很像Angular1.x中的概念。咱們知道在Angular1.x中module也是能夠互相依賴的,每個模塊/指令/服務都應當可以不受任何狀態影響完成基礎邏輯。想象一下,咱們加入指令前須要考慮爲指令新建一個模板,新建幾個變量放在模板的某個位置等等,這確定會使總體耦合性過強。在Angular2中pipe便有『純』與『非純』的概念,非純的管道在變動時就須要考慮更多的外部環境變化,固然效率也會大大降低。咱們但願大部分的函數、代碼段、集合都能達到自治的標準,這也是你們常說的高內聚低耦合。java

main組件是用戶瀏覽的主體部分,在界面設計上它至少能夠分爲兩個部分,首先是一側的菜單與用戶信息顯示,其次是主要顯示區域,固然你還能夠爲它增長一些隱藏、懸浮、彈出菜單。這裏至少包含三個組件:菜單、列表、詳情,咱們先用angular-cli命令生成它們:git

ng g component main-detail
ng g component main-menu
ng g component main-list

組件準備就緒,咱們在src/app/main文件夾下新增模塊與路由文件,並把原有的組件改造爲路由插座:github

// src/app/main/main.module.ts 子模塊文件
import {CommonModule} from '@angular/common'
import {NgModule} from '@angular/core'
import {FormsModule} from '@angular/forms'
import {MainRoutingModule} from './main.routing'

import {MainComponent} from './main.component'
import {MainListComponent} from './main-list/main-list.component'
import {MainDetailComponent} from './main-detail/main-detail.component';
import {MainMenuComponent} from './main-menu/main-menu.component'

@NgModule({
    declarations: [
        MainComponent,
        MainListComponent,
        MainDetailComponent,
        MainMenuComponent,
    ],
    imports: [
        CommonModule,
        FormsModule,
        MainRoutingModule
    ],
    exports: [MainComponent],
    providers: [
        SanitizePipe
    ]
})
export class MainModule {
}
// src/app/main/mian.routing/ts 路由文件
import {NgModule} from '@angular/core'
import {Routes, RouterModule} from '@angular/router'

import {MainComponent} from './main.component'
import {MainListComponent} from './main-list/main-list.component'
import {MainDetailComponent} from './main-detail/main-detail.component'


export const mainRoutes: Routes = [{
    path: '', component: MainComponent,
    children: [{
        path: '', redirectTo:'list',pathMatch:'full'
    },{
        path: 'list', component: MainListComponent
    },{
        path: 'list/:id', component: MainDetailComponent
    }]
}]

@NgModule({
    imports: [RouterModule.forChild(mainRoutes)],
    exports: [RouterModule]
})
export class MainRoutingModule {
}

子模塊也須要被根模塊檢測到才能在編譯時被歸入,這裏考慮到main.module是一個子路由產生的懶模塊,咱們能夠考慮在路由轉向它時纔開始加載。這時app.routing須要改寫一條路由規則:{path: 'main', loadChildren: './main/main.module#MainModule', data: {preload: true}}數據庫

從如今開始,每當咱們訪問/mian路由時Angular會自動爲咱們加載新的模塊,在訪問/mian/*時,main.routing.ts文件會開始檢測路由地址並切換到相應的頁面組件上。後面全部的業務都將專一於main路由中,爲了項目的可讀性,每一個子路由工做的子頁面組件,都應當寫在main文件夾下。api

編寫組件與公共服務

我爲main下的組件寫了一些樣式,具體能夠參考Commit,它看起來有些簡陋但並無關係,在編寫應用時不能把注意力過於集中在某一點上,一開始寫出很是嚴謹、不可變的樣式會使隨後的邏輯重構畏首畏尾,總體式的推動、優化能夠大大提高項目進度。等到應用可以運行時咱們再回過頭來考慮這些問題。安全

與登陸類似,在每一個組件下建立一個service,須要記住的是,當前組件下的service僅僅只供給當前組件使用,它被寫在組件的providers依賴列表裏,若是你真的須要一個共享或狀態存儲(單次實例)的組件,能夠考慮shared文件夾。舉個例子來講,如今咱們的數據庫中文章詳情是html富文本格式,這些源數據是不可以被直接解析在dom結構中的,還須要作一些安全化處理,咱們以這個功能爲例,建立一個公共的pipe解析器。
shared/pipe/sanitize下建立一個pipe:bash

import {Pipe, PipeTransform} from '@angular/core'
import {DomSanitizer, SafeHtml} from '@angular/platform-browser'

@Pipe({
    name: 'sanitize'
})
export class SanitizePipe implements PipeTransform {

    constructor (private domSanitizer:DomSanitizer){}

    transform (value: any, args?: any): SafeHtml{
        return this.domSanitizer.bypassSecurityTrustHtml(value)
    }

}

前面在建立公共service時咱們使用了一種投機取巧的方式,便是將公共service注入在app.component的providers依賴列表中,由於根組件最多隻會建立一次,藉此機制拿到一個只會被實例化一次的服務。但這不是工程化的作法(顯而易見),結合上文所提到Angular的module機制,咱們能夠爲shared創建一個獨立的module,用來解決這些問題:

// src/app/shared/shared.module.ts

import {NgModule, ModuleWithProviders} from '@angular/core'
import {CommonModule} from '@angular/common'
import {FormsModule} from '@angular/forms'

import {IpcRendererService} from './service/ipcRenderer'
import {SanitizePipe} from './pipe/sanitize'

@NgModule({
    imports: [
        CommonModule,
        FormsModule
    ],
    declarations: [
        SanitizePipe
    ],
    exports: [
        SanitizePipe
    ],
    providers: [
    ]
})

export class SharedModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: SharedModule,
            providers: [IpcRendererService]
        };
    }
}

forRoot靜態方法是Angular2的一個公約,具體能夠參見官方文檔,你們只須要知道的是在app.module的imports依賴中調用SharedModule.forRoot(),而其餘地方僅僅依賴SharedModule便可。看它們不一樣的使用方法不少人應該已經猜出module是怎樣工做的了,先無論這些,讓咱們回到mian.module裏注入依賴項試試效果。

新的通訊接口

在此以前,咱們約定了接口語法爲ipcRendererService.api('接口名', '參數'),新增的組件裏也參考此方式發起請求便可,這裏咱們可能至少須要兩個接口:this.ipcRendererService.api('list', page)this.ipcRendererService.api('detail', id)。想象一下,在列表組件初始化時調用list接口傳入一個頁碼得到一些列表數據,而後使用Angular的路由方法this.router.navigate(['/main/list', id])把列表中某一項的id傳至詳情頁面,詳情頁面在初始化時從url上取得頁面id,再次經過detail接口獲取本身須要的文章詳情數據。一次正常的瀏覽就完成了。
在給Electron中的api增長方法時先等等,上一篇文章咱們聊到Async函數,如今咱們可使用async函數來時路由更簡單易懂一些:

// browser/ipc/index.js

const {ipcMain} = require('electron')
const api = require('./api')

ipcMain.on('api', (event, actionName, ...args) =>{
    const reply = (replayObj, status = 'success') =>{
        event.sender.send(`${actionName}reply`, replayObj, status);
    }
    if (api[actionName]){
        api[actionName](event, ...args)
            .then(res => reply(res))
            .catch(err => reply({message: '應用出現了錯誤'}))
    }
})

如今咱們假設路由文件已是async函數構成的,先將回複方法(reply函數)放在外部,取消以前的對象合併。雖然前面使用對象合併避免對侵入原生對象,但也並非那麼優雅,如今只考慮返回值無疑是最酷的作法!

// browser/ipc/api/index.js
const screen = require('../../screen')
const articleService = require('../../service/article')

module.exports = {
    login: async (e, user) =>{
        // todo something
        screen.setSize(1000, 720)
        return {msg: 'ok'}
    },
    list: async (e, page) =>{
        try{
            const articles = await articleService.findArticlesForPage(page)
            // todo filter articles
            return articles
        } catch (err){
            return Promise.reject(err)
        }
    },
    detail: async (e, id) =>{
        try{
            const article = await articleService.findArticleForID(id)
            return article
        } catch (err){
            return Promise.reject(err)
        }
    }
}

articleService是原生數據庫查詢的封裝,相比於每次寫find/update方法與大量參數,我更建議你們把這些垃圾代碼統一封裝成更富有語義性的函數,不管過去多久,你再次讀到這段代碼時總能很清楚的知道本身作了什麼,這很關鍵。
另外,我給你們展現的是代碼框架如何搭建,單個await帶來的便利性沒有想象的大,但你在實際業務中會涉及屢次查詢、更新、篩選、遍歷操做,async語法糖會給你帶來極高的可讀性!

如今news-feed已經可以快速顯示出數據庫裏的列表:
列表demo

點擊任何一項進入詳情,文章內容都被sanitize.pipe過濾解析在dom裏:
詳情demo

最後

固然,news-feed還存在不少問題,甚至還不能稱之爲一個應用,好比不能註銷登陸、瀏覽文章時沒法返回列表、沒法下載文章內容/圖片、沒有跳轉到原文等等。這些細節是真正值得注意的重點,後面幾節咱們都會一塊兒討論怎樣添加這些邏輯並優化現有的代碼。

相關文章
相關標籤/搜索