使用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还存在不少问题,甚至还不能称之为一个应用,好比不能注销登陆、浏览文章时没法返回列表、没法下载文章内容/图片、没有跳转到原文等等。这些细节是真正值得注意的重点,后面几节咱们都会一块儿讨论怎样添加这些逻辑并优化现有的代码。

相关文章
相关标签/搜索