当前位置: 首页 > 后端技术 > Node.js

使用Angular和TypeScript构建Electron应用(5)

时间:2023-04-03 20:32:05 Node.js

这次我们开始关注Angular是如何构建前端路由和逻辑的。这与您之前熟悉的方式有些不同。同时,这部分内容非常充实。文件结构也相应改变。如有任何疑问,请参考本次代码变更的Commit。在进行新的开发之前,我们不妨对原有的爬虫代码做一些细微的改动。正式展示内容时,仅仅有标题和文章详情是不够的。发布者、发布日期等字段也根据实际爬取的页面和业务需要进行更改。为此,我在browser/task/ifeng.js中丰富了parseContent函数的代码://browser/task/ifeng.js//....parseContent(html){if(!html)return;const$=cheerio.load(html)consttitle=$('title').text()constdescription=$('meta[name="description"]').attr('content')constcontent=$('.yc_con_txt').html()consthot=$('span.js_joinNum').text()return{title:title,content:content,description:描述,hot:hot,createdAt:newDate()}}创建Angular子模块在Angular2中,模块是用来描述组件之间关系的文件,就像一棵树的树枝,所有的小树干都聚集在这里填充到模块中,模块使用一些独特的语法糖来描述它们之间的关系和依赖关系。当应用程序比较复杂时,树的分支往往不止一个,我们不可能把根模块中的所有文件都挂载下来。这样不优雅,会导致打包后的单个文件过大,影响首页加载速度。为此,我们可以在根模块上注册一些子模块来描述完全不同的可以自治的子模块。“Autonomy”是一个非常关键的点,这和Angular1.x中的概念非常相似。我们知道Angular1.x中的模块之间也是可以相互依赖的,每个模块/指令/服务都应该能够不受任何状态的影响,完成基本的逻辑。想象一下,我们需要考虑在添加命令之前为命令创建一个新的模板,创建几个新的变量并将它们放在模板中的某个位置等等,这肯定会使整体耦合性太强。在Angular2中,管道有“纯”和“不纯”的概念。更换不纯净的管道时,需要考虑更多的外部环境变化,当然效率也会大大降低。我们希望大部分函数、代码段、集合都能达到自治的标准,也就是大家常说的高内聚低耦合。主组件是用户浏览的主要部分。从界面设计上来说,至少可以分为两部分。第一个是一侧的菜单和用户信息显示,第二个是主显示区域。当然,你也可以给它添加一些隐藏和浮动的部分,弹出菜单。这里至少包含三个组件:菜单、列表和详细信息。我们首先用angular-cli命令生成它们:nggcomponentmain-detailnggcomponentmain-menunggcomponentmain-list组件准备好了,我们在src/app/main文件夹下添加新的模块和路由文件,并将原始组件转换为路由套接字://src/app/main/main.module.tssubmodulefileimport{CommonModule}from'@angular/common'import{NgModule}from'@angular/core'import{FormsModule}从'@angular/forms'导入{MainRoutingModule}从'./main.routing'导入{MainComponent}从'./main.component'导入{MainListComponent}从'./main-list/main-list.component'导入{MainDetailComponent}来自'./main-detail/main-detail.component';从'./main-menu/main-menu.component'@NgModule导入{MainMenuComponent}({声明:[MainComponent,MainListComponent,MainDetailComponent,MainMenuComponent,],导入:[CommonModule,FormsModule,MainRoutingModule],导出:[MainComponent],providers:[SanitizePipe]})exportclassMainModule{}//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'exportconstmainRoutes: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]})exportclassMainRoutingModule{}考虑到main.module是asubmodule路由生成的惰性模块,我们可以考虑在路由转向它的时候加载它。这时app.routing需要重写一条路由规则:{path:'main',loadChildren:'./main/main.module#MainModule',data:{preload:true}}。从现在开始,每当我们访问/mian路由时,Angular都会自动为我们加载新模块。当访问/mian/*时,main.routing.ts文件会开始检测路由地址,并切换到对应的页面组件。后续所有业务将集中在主干线上。为了项目的可读性,每个子路由的子页面组件应该写在主文件夹中。写组件和公共服务我在main下为组件写了一些样式。具体可以参考Commit。非常严谨和不可变的风格会让后续的逻辑重构望而却步,整体的提升和优化可以极大的提升项目进度。当应用程序准备好运行时,我们将回到这些问题。和登录类似,在每个组件下创建一个服务。需要记住的是,当前组件下的服务只被当前组件使用,并且写在组件的providers依赖列表中。如果你确实需要一个共享的或者对于状态存储的组件(单实例),你可以考虑共享文件夹。比如我们数据库里的文章详情现在都是html富文本格式。这些源数据不能在dom结构中直接解析,需要做一些安全处理。我们以这个函数为例来创建一个公共管道解析器。在shared/pipe/sanitize下创建一个管道:classSanitizePipeimplementsPipeTransform{constructor(privatedomSanitizer:DomSanitizer){}transform(value:any,args?:any):SafeHtml{returnthis.domSanitizer.bypassSecurityTrustHtml(value)}}我们使用了一种机会主义的方式来注入公共服务放到app.component的providers依赖列表中,因为根组件最多只会被创建一次,利用这个机制得到一个只会被实例化一次的服务。但这不是工程方式(很明显),结合上面提到的Angular模块机制,我们可以创建一个独立的shared模块来解决这些问题://src/app/shared/shared.module。tsimport{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:[]})exportclassSharedModule{staticforRoot():ModuleWithProviders{返回{ngModule:SharedModule,providers:[IpcRendererService]};forRoot静态方法是Angular2的约定。详情请参考官方文档。你只需要知道在app.module的imports依赖中调用SharedModule.forRoot(),而其他地方只依赖SharedModule。看他们不同的使用方式,很多人应该已经猜到模块是如何工作的了。不管这些,我们回到mian.module,注入依赖试试效果。在新的通信接口之前,我们约定接口语法为ipcRendererService.api('interfacename','parameter'),新的组件也可以参考这个方法发起请求。这里我们可能至少需要两个接口:this.ipcRendererService.api('list',page),this.ipcRendererService.api('detail',id)。想象一下,当list组件初始化的时候,调用list接口传入一个页码获取一些list数据,然后使用Angular的路由方法this.router.navigate(['/main/list',id])传递将list中的一个itemid传给详情页,详情页在初始化时从url中获取pageid,再次通过详情接口获取自己需要的文章详情数据。正常浏览完成。在Electron中向api添加方法时请稍等。我们在上一篇文章中讨论了异步函数。现在我们可以使用异步函数让路由更容易理解://browser/ipc/index.jsconst{ipcMain}=require('electron')constapi=require('./api')ipcMain.on('api',(event,actionName,...args)=>{constreply=(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:'应用程序发生错误'}))}})现在我们假设路由文件已经由asyncfunctions组成,首先将reply方法(replyfunction)放在External中,取消之前的对象merge。虽然使用了对象合并来避免对原生对象的侵入,但它并不是那么优雅。现在只考虑返回值无疑是最酷的方式!//browser/ipc/api/index.jsconstscreen=require('../../screen')constarticleService=require('../../service/article')module.exports={登录:async(e,user)=>{//todosomethingscreen.setSize(1000,720)return{msg:'ok'}},list:async(e,page)=>{try{constarticles=awaitarticleService.findArticlesForPage(page)//todo过滤文章returnarticles}catch(err){returnPromise.reject(err)}},detail:async(e,id)=>{try{constarticle=awaitarticleService.findArticleForID(id)returnarticle}catch(err){returnPromise.reject(err)}}}articleService是对原生数据库查询的封装。比起每次都写find/update方法和大量的参数,我建议大家把这些垃圾代码封装成更多语义化的函数,不管过去多久,当你再次阅读这段代码时,总能清楚地知道是什么你做到了,这很重要。此外,我将向您展示如何构建代码框架。单一的await带来的便利并没有想象的那么大,但是你的实际业务会涉及到多个查询、更新、过滤、遍历等操作。异步语法糖会给你带来极好的可读性!现在news-feed可以快速显示数据库中的列表:点击任意item进入详情,文章内容会在dom中被sanitize过滤解析。对于一个应用,比如无法登出和登录,浏览文章无法返回列表,无法下载文章内容/图片,不跳转到原文等,这些细节才是真正值得关注的重点,在接下来的几节中,我们将讨论如何添加这些逻辑并优化现有代码。