这次我们开始关注Angular怎样构建前端路由与逻辑,它与你以前熟悉的方式有一些区别,同时这部分内容非常充实,路由发生变化后原有的文件结构也随之变化,有疑问请参见本次代码变更的Commit。

在进行新的开发之前我们不妨对原有的爬虫代码做一些轻微的更改,在正式显示这些内容时,仅仅有标题与文章详情是远远不够的,可以加入类似于摘要、描述、阅读量、发表人、发表日期等等字段,具体也根据实际爬取的页面与业务需求更改,为此我丰富了browser/task/ifeng.js中parseContent函数的代码:

// 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便有『纯』与『非纯』的概念,非纯的管道在变更时就需要考虑更多的外部环境变化,当然效率也会大大下降。我们希望大部分的函数、代码段、集合都能达到自治的标准,这也是大家常说的高内聚低耦合。

main组件是用户浏览的主体部分,在界面设计上它至少可以分为两个部分,首先是一侧的菜单与用户信息显示,其次是主要显示区域,当然你还可以为它增加一些隐藏、悬浮、弹出菜单。这里至少包含三个组件:菜单、列表、详情,我们先用angular-cli命令生成它们:

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

组件准备就绪,我们在src/app/main文件夹下新增模块与路由文件,并把原有的组件改造为路由插座:

// 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文件夹下。

编写组件与公共服务

我为main下的组件写了一些样式,具体可以参考Commit,它看起来有些简陋但并没有关系,在编写应用时不能把注意力过于集中在某一点上,一开始写出非常严谨、不可变的样式会使随后的逻辑重构畏首畏尾,整体式的推进、优化可以大大提升项目进度。等到应用能够运行时我们再回过头来考虑这些问题。

与登录相似,在每个组件下创建一个service,需要记住的是,当前组件下的service仅仅只供给当前组件使用,它被写在组件的providers依赖列表里,如果你真的需要一个共享或状态存储(单次实例)的组件,可以考虑shared文件夹。举个例子来说,现在我们的数据库中文章详情是html富文本格式,这些源数据是不能够被直接解析在dom结构中的,还需要做一些安全化处理,我们以这个功能为例,创建一个公共的pipe解析器。
shared/pipe/sanitize下创建一个pipe:

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已经能够快速显示出数据库里的列表:

点击任何一项进入详情,文章内容都被sanitize.pipe过滤解析在dom里:

最后

当然,news-Feed还存在很多问题,甚至还不能称之为一个应用,比如不能注销登录、浏览文章时无法返回列表、无法下载文章内容/图片、没有跳转到原文等等。这些细节是真正值得注意的重点,后面几节我们都会一起讨论怎样添加这些逻辑并优化现有的代码。

使用Angular与TypeScript构建Electron应用(五)的更多相关文章

  1. HTML5 播放 RTSP 视频的实例代码

    目前大多数网络摄像头都是通过 RTSP 协议传输视频流的,但是 HTML 并不标准支持 RTSP 流。本文重点给大家介绍HTML5 播放 RTSP 视频的实例代码,需要的朋友参考下吧

  2. HTML5给汉字加拼音收起展开组件的实现代码

    HTML 规范是 W3C 与 WHATWG 合作共同产出的,HTML5 因此也不例外,这篇文章主要介绍了HTML5给汉字加拼音收起展开组件的实现代码,需要的朋友可以参考下

  3. 关闭iOS原生MPVolumeView音频路由菜单

    我正在使用MPVolumeView允许用户在使用我的应用程序时控制他喜欢的音频路径.该代码显示了该视图:当用户点击音频路由按钮时,会出现一个带有可用选项的菜单.问题:显示音量视图的屏幕可能需要隐藏,因为我的应用程序处理各种事件,我想同时隐藏音频路由菜单我的问题:有没有人知道是否可以手动关闭MPVolumeView的音频路由选择菜单而无需用户按下取消按钮?解决方法在iOS8上,您可以使用以下使用私有API的代码

  4. iOS:使用蓝牙音频输出(kAudioSessionProperty_OverrideCategoryEnableBluetoothInput)AudioSession

    >如果有可用的A2DP设备,我的音频路由将始终自动切换到kAudioSessionOutputRoute_BluetoothA2DP路由.如何防止此路线更改?我希望你们中的一些人可以帮助我解决这些问题.这对我对CoreAudio的整体理解,特别是AudioSession框架,真的有帮助.解决方法AudioSession是一项棘手的业务.1.BluetoothHFPaudiooutputisonlypossibleincaseofAudioSessionkAudioSessionCategory_PlayA

  5. Swift项目的main函数为何消失了?如何把它找出来?

    如果在app启动时放置断点,可以看到应用程序调用栈,如下图main函数作为程序启动后的第二个函数被调用,然后在main函数中再启动UIApplication,并绑定AppDelegateSwift项目中的main函数为何隐藏创建过Swift项目的童鞋应该会发现,项目中没有一个名为main.swift的文件,为什么app的入口没有了?swift中main函数的Process.unsafeArgv参数变成了一串十六进制数字,应该也是代表应用程序地址。

  6. Swift 语言指南

    想快速找到优秀开源项目的开发者,可以访问我们额外整理的《Swift项目精选》。希望快速找到其中精选文章,可以访问《Swift文章精选》。其中包括:Swift概括,SwiftProgrammingLanguage,UsingSwiftwithCocoaandObjective-C2.SwiftProgrammingLanguage苹果官方文档:在线版(英文)|iBooks版(英文)爱好者翻译版:在线版(中文)By@Swift中文翻译组|PDF版By@老码团队|百度阅读版相关文档BasicOperators-

  7. Swift3.0 Swift2.3 获取IP地址 获取网关地址

    最近需要在Swift项目中获取路由器的网关地址,在网上找了半天的代码也没发现太多有价值的东西,而且大多都是OC代码,很少有Swift的相关代码,只找到了一个通过Swift代码获取设备IP的代码,最后实在没办法只能曲线救国了。下面上代码:思路就是把获取到的设备IP地址的最后一位手动修改为”1”,前面三位不需要修改,比如我的手机ip地址是192.168.31.212,所以网关地址就是192.168.31.1。最近苹果更新了Swift3.0,这里更新一下代码。

  8. Swift中方法(method)所谓的lazy绑定简介

    我们知道在ruby之类的动态语言中对象方法可以先从类中预先抽取,然后再应用到某个具体对象上.这称为无绑定的method对象,也可以叫做lazy绑定.下面举个例子:以上在ruby中创建了一个类A,我们可以用它的实例调用test方法:我们也可以先创建一个无绑定的method,然后绑定到特定的实例上去:最后一句是倒数第二句的语法糖,效果是一样的.那么如果是类方法呢?那更简单了,直接用Obj.method()即可:无论你承认与否,ruby比swift还要简洁,更具有美感!

  9. Swift3.0服务端开发(二) 静态文件添加、路由配置以及表单提交

    今天博客中就来聊一下Perfect框架的静态文件的添加与访问,路由的配置以及表单的提交。也就是webroot的文件目录变地方了。后方的尾随闭包是对响应的处理。action的地址就是我们在服务器端配置的路由地址“127.0.0.1:8181/login”,而表单提交的方式是POST。

  10. Swift Web 开发之 Vapor - 路由二

    路由参数Vapor提倡使用类型安全的路由参数来接收数据,我们可以在路由方法中使用Swift类型来指定参数类型,Vapor会在内部解析并将参数返回给闭包以供使用,非常方便。Swift中处处有协议,路由参数也是如此,我们所见例子中的Int其实就是Vapor给实现了StringInitializable协议,当然String也已经默认实现。throw另外一大特性就是可以直接在路由中抛出异常,我们可以throw任何遵从Swift.Error协议的对象,当然Vapor已经为我们封装好了几个常用的Error来方便我们

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部