使用Angular的八个理由(从14开始,让我们开始吧,独立组件)
最近,Angular在日本一直没有获得与Vue和React一样的流行程度。不过,自从Angular更新到14版本后,它也引入了一些划时代的功能,可以与React、Vue以及新兴的Svelte等竞争。尤其是在TypeScript近年来的兴盛下,Angular在美国市场上也显示出了相当的复苏势头。
这个功能被称为”独立组件”,它颠覆了以往Angular的常规想法(就像Vue的组合API或React的函数组件化一样具有飞跃的进步)。这个独立组件是指能够像Vue或React一样,使单一组件具有独立的功能。这样一来,在创建应用程序时就不再需要使用通用模块(app.module.ts),而且实现起来也变得容易。
然而,在Qiita上,并没有针对这个独立组件的专门文章,所以我借此机会创建了一篇。
在Angular14中,独立组件本身是评估版,在Angular15中变为稳定版。在基本的独立组件部分,没有太多的规格变更。然而,从Angular15开始,依赖注入在独立组件支持方面得到了全面改进,并且服务周围也得到了大幅改善。
报告的前半部分是基于信息收集结果和操作测试的报告,内容涉及到Angular15的变更点;后半部分则是基于实际开发经验的解释,以Angular15为基础。
独立组件的好处
我整理了一下,以海外网站的风格总结了独立组件的优点。
1:由于不依赖通用模块,可以实现轻量化。
这是最显著的优点。不依赖于通用模块意味着可以大幅减少应用程序的负担,根据海外网站的分析,内存消耗可以减少60%以上。换句话说,Angular也可以构建适合小型项目的项目或应用程序,就像Vue或React一样。
- Angular Standalone Components by examples
另外,听说也实现了通过轻量化来提高速度,但还在探索有效的分析结果信息。
2:任务分配和应用切换变得更加简便
过去,由于必须将各种设置记录在通用模块的配置文件app.module.ts中,因此切换应用程序或分配组件创建工作变得非常麻烦。此外,在Angular启动时可能会与其他通用模块产生冲突。但是,在使用独立组件的情况下,可以在main.ts中导入并控制所有必要信息。换句话说,可以轻松地切换项目或应用程序。
只需切换AppComponent导入位置并重新加载浏览器,即可变成另一个项目。
import { enableProdMode,importProvidersFrom} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
//import { AppComponent } from './project-one/app.component';
import { AppComponent } from './project-two/app.component'; //Aルートコンポーネントの切換
import { environment } from './environments/environment';
import {RouterModule} from '@angular/router';
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[]
}).catch(err => console.error(err));
3:只需一个CommonModule即可实现最低要求的功能。
如果使用独立组件,就可以标准实现CommonModule。而这个CommonModule将一揽子所需的组件创建模块,如ngModule、FormsModule、BrowserModule等,集成起来,这样就不需要逐一调用它们,可以在模板上轻松地使用ng指令和表单组件。
另外,由于Angular14将响应式表单从试验实现升级为正式实现,因此在使用它们方面非常方便。
4:只需简单定制所需使用的模块。
通用模块的设置文件app.module.ts不再是必需的,但除了CommonModule之外,有时还需要使用ReactiveFormsModule来替代RouterModule和FormsModule。此外,由于ngModule装饰器仍然存在,因此可以利用它来创建一个只集中所需的模块的库来启动应用程序。而且,由于不再需要模块的声明和组件的选择器指定,因此可以轻松地创建和连接它们。
import {NgModule} from '@angular/core';
import { CommonModule } from '@angular/common';
import {RouterModule } from '@angular/router'
import {ReactiveFormsModule} from '@angular/forms'
@NgModule({
//アプリケーション起動に必要なモジュール
imports:[
CommonModule, RouterModule, ReactiveFormsModule,
],
//外部で制御させたいモジュール
exports:[
CommonModule, RouterModule, ReactiveFormsModule,
],
})
export class CustomModule { } //必要なモジュールだけ集めて外部にエクスポートできる
如果您使用的组件已经导入了库,那么您可以省略繁琐的描述,从而比传统的描述更简洁地表示。
import { Component } from '@angular/core';
//import { CommonModule } from '@angular/common' //CommonModuleの呼出は不要
import { ActivatedRoute,Router } from '@angular/router' //RouterModuleの記述は不要
import { FormGroup,FormControl} from '@angular/forms' //ReactiveFormModuleの記述は不要
import { CustomModule } from '../custom.module' //先程設定したモジュールが内包される
@Component({
standalone: true,
imports: [CustomModule], //逐一、他のモジュールを記述する必要がなくなる
})
在这个库中,除了模块之外,还可以批量控制组件、属性指令和自定义管道(无法控制服务)。
5:可以为任意组件设置从属组件
在以前的版本中,我们需要在app.module.ts中定义要使用的所有组件,并且父子组件之间的依赖关系有时不太清晰。而现在,独立组件可以像Vue或React一样,可以使任意组件具有依赖的子组件,因此组件的父子关系更加清晰明确。
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {StandaloneChildComponent } from './standalone-child.component'; //子コンポーネント
@Component({
standalone: true,
imports: [CommonModule,StandaloneChildComponent], //ここに従属コンポーネントを記述
//従属関係にある子コンポーネント
template: `
<ul>
<app-standalone-child></app-standalone-child>
</ul>
`
})
请不要忘记在※imports属性中记录依赖的组件。
6:可以将路由控制分离、简化
由于app.module.ts不再是必需的,现在可以在main.ts中控制路由信息,并且可以在独立的文件中进行管理。此外,不需要将RouterModule导出到每个组件中。
import { Routes } from "@angular/router";
//各種コンポーネント
import {TopComponent} from '../top.component';
import {RouteOneComponent} from '../route-one.component';
import {RouteTwoComponent} from '../route-two.component';
import {RouteThreeComponent} from '../route-three.component';
export const Route: Routes = [
{path: '',component: TopComponent},
{path: 'one',component: RouteOneComponent},
{path: 'two',component: RouteTwoComponent,pathMatch: 'full'},
{path: 'three',component: RouteThreeComponent,pathMatch: 'full'},
]
只需将此配置文件加载到main.ts中,路由设置就完成了。
import { enableProdMode,importProvidersFrom} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './project-one/app.component';
import { environment } from './environments/environment';
import {RouterModule,Routes} from '@angular/router';
import {Route} from './app/routes/route-sample' //ルーティングの設定ファイル
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[importProvidersFrom(RouterModule.forRoot(Route))]
}).catch(err => console.error(err));
7:如果没有从属组件,则可以省略选择器。
这虽然很土,但这也是一个值得感激的功能。以前我们需要在app.module.ts中声明(并设置为declaration)然后调用,因此必须指定选择器名称来关联目标组件。但在独立的组件中,对于在模板上没有依赖关系的组件,可以省略选择器的描述。
进一步的声明不需要,这意味着您只需要将属性指令或自定义管道等设置为导入属性,就能直接进行控制。
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MessageDirective } from "../directives/message.directive"; //nameプロパティはmessage
import { TickerPipe } from "../pipes/ticker.pipe"; //nameプロパティはticker
@Component({
//selector: app-standalone-sub 従属関係がないコンポーネントは独立できるのでセレクタ指定不要
standalone: true,
template: `<p message>{{"従属コンポーネントがない場合はセレクタ不要"| ticker }}</p>`,
imports: [CommonModule,MessageDirective,TickerPipe], //必要なコンポーネント類はそのまま使える
})
8:可以灵活处理类组件上的变量。
由于不需要依赖ngModule,ngOnInit也不再是必需的。因此,在构造函数中可以处理订阅变量。由于类组件不再受ngOnInit控制,您可以在本地设置变量并直接返回到模板上(如果需要在DOM调用后控制,ngOnInit仍然可用,因此可根据需要使用)。
使用提供者进行服务分发、使用注射器进行接收等都变得不再必要,可以直接在构造函数中订阅服务(然而,请参考追加部分的”依赖注入”,因为这个部分在Angular15中得到了改善)。
@component{
standalone: true,
template: `<p>{{ subview }}{{ localview }}</p>`
}
export class SampleComponent{
subview: string
constructor(private svs:CustomService){
this.svs.sub.subscribe((v)=>{ this.subview = v}) //コンストラクタ上でサービスを購読できる
}
localview = "これをダイレクトに表示できる" //ローカル上に返す場合はそのままダイレクトに記述可能
}
以此类推,独立组件只有优点(当然也不是没有缺点,比如在Angular14时,无法分配服务,所以需要直接导入各种独立组件)。
由于独立组件是可选择使用的,因此如果不想要独立化,也可以与传统组件共存,从而实现部分灵活的改进和修复。
创建一个独立的组件
那么,让我们实际创建一个独立组件。
新建
通过ng命令可以轻松创建独立组件。g代表generate,c代表component的缩写形式。如果需要创建任意目录,请移除–flat选项。
#ng g c hoge-fuga --standalone --flat
如果按照这样的方式,将会自动创建以下组件。这个CommonModule将代替传统的NgModule,并同时兼容FormsModule、BrowserModule等。
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-hoge-fuga',
standalone: true,
imports: [CommonModule],
templateUrl: './hoge-fuga.component.html',
styleUrls: ['./hoge-fuga.component.css']
})
export class HogeFugaComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
然而,正如前面所述,独立组件并不一定需要ngOnInit函数,因此可以如下删除。另外,如果不存在依赖关系组件,则删除选择器属性也没有问题。
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-hoge-fuga', //場合によっては削除できる
standalone: true,
imports: [CommonModule],
templateUrl: './hoge-fuga.component.html',
styleUrls: ['./hoge-fuga.component.css']
})
//ngOninitを取り払う
export class HogeFugaComponent{
constructor() { }
}
创建更轻量化的独立组件
如果使用”minimal”选项创建独立组件,可以进一步减小体积(每个项目大约只有200KB,仅能实现最基本的应用程序)。
#ng g c hoge-piyo --standalone --minimal --inline-template --inline-style --routing=false --style=css
将现有的组件转换为独立运行的形式。
您也可以将现有的组件更改为独立运行的形式。我已经在更改点上添加了注释。
通过将①所示的standalone属性设置为true,可以宣言该组件为独立的,其中最重要的标志。
另外,由于BrowserModule之前是必需的,现在将全部集中在CommonModule中,所以第二点和第三点的描述是必需的。
import { CommonModule } from '@angular/common'; //②CommonModuleをインポートする
import { Component } from '@angular/core';
@Component({
//selector: 'app-before',
standalone: true, //①スタンドアロンコンポーネントの宣言
styleUrls: ['./m.component.css'],
template: `<p>従来のコンポーネントをスタンドアローンへ</p>`,
imports:[CommonModule], //③使用モジュールを記述
})
//implements onInitとしなくても、データを転送できる
export class BeforeComponent{
constructor()
{
}
Angular 15的更新内容。
在Angular 15中引入了一种名为”provideRouter”的方法来提高路由的速度。同时,通过使用这个方法,还可以省去一些不必要的库,使路由控制的代码变得更简洁。
import { enableProdMode} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/todo-views/app.component'
import { environment } from './environments/environment'
import {provideRouter} from '@angular/router'
import {Route} from './app/routes/app-todo'
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[provideRouter(Route)]
}).catch(err => console.error(err));
服务周围的改进(依赖注入)
我们引入provideRouter是有原因的。原因是为了改善在Angular14中被视为保留状态的服务周围。从Angular15开始,依赖注入已经适用于独立组件,因此从这方面来说才能发挥独立组件的真正作用(以前我们是通过构造函数来分发服务,但这在Angular中并不是推荐的方式)。因此,从Angular15开始,通过provider进行服务注入,并且使用inject进行接收被推荐。
这个服务注入可以在路由文件、应用程序、各种组件等任何地方进行注入,然后可以通过新引入的inject方法轻松地接收到服务。
该inject方法是用于接收数据并进行数据交互的方法,可以说它的功能与Vue3中的inject方法几乎相同(但与Vue3不同的是,它不支持直接输入的令牌,因此更加健壮)。
为服务注入能量
由于我们希望使用共同的组件来接收服务,所以我们将在应用程序中进行以下追加,并注入服务(如果省略了属性,则将分配服务类本身)。
import {HogeService} from './app/services/hoge-service' //インポートするサービス
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[
[HogeService], //ここを追記
provideRouter(Route),
],
}).catch(err => console.error(err));
如果以其他方式提供服务的话
从路由文件中注入
- 在路由文件中,通过以下方式追加内容,可以清晰区分是否向特定组件传递服务,从而实现对组件的灵活控制。
{path: 'hoge/:id',component: HogeComponent,pathMatch: 'full',
providers:[HogeService]}
從組件中注入
如果想要从单个组件中注入,请按以下方式操作。
@Component({
standalone: true,
imports: [CustomModule],
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css'],
providers:[HogeService],
})
领取服务
在接收服务的情况下,可以使用inject方法直接在独立组件中进行接收(不需要像以前一样追加到providers属性中)。然而,据说这种服务的接收只限于ngOnit,如果试图在独立组件中展开它的优势并出现错误。
import { Component,OnInit,inject } from '@angular/core';
import {HogeService} from '../services/hoge-service'
export class HogeComponent implements OnInit{
svs = inject(HogeService) //先程のサービスを受け取る
}
//サービスはngOnitでないと展開できない
ngOnInit(){
console.log(this.svs)
}
}
使用代币
直接分配服务会效率低下,并且存在着安全性的重大问题。因此,针对这种服务的注入,一般会发行令牌(类似于传输密码)来分发数据,这是一种常见的方法。
发行令牌并注入服务。
您可以从InjectionToken类中发行令牌。写入类型定义文件是个好主意。在这种情况下,请务必先导入InjectionToken。
import { InjectionToken } from '@angular/core'
export const token = new InjectionToken<any>("hoge") //hogeは任意のパスワード
将令牌与服务关联起来 yǔ qǐ)
在main.ts文件中,需要按照以下方式进行描述。通过在provide属性中设置令牌,可以将令牌与服务进行关联。此外,如果要注入任意值,可以使用useValue,但在本例中,我们将使用useClass来注入服务类本身。
import {HogeService} from './app/services/hoge-service' //分配したいサービスクラス
import {token} from './app/types/types_hoge' //受け取るためのトークン
bootstrapApplication(AppComponent,{
providers:[
[{provide:token,useClass:HogeService}],
provideRouter(Route),
]
//providers:[]
}).catch(err => console.error(err));
接受服务的一方
接收方可以使用令牌仅接收注入的服务,因此可以像Vue3一样自由地接收值。所以,在这里我们只需要导入令牌即可。
无法像Vue3那样直接接收通过` ※inject(‘hoge’)`写入的密码,因为传递的不是变量本身,而是一个称为注入令牌的对象。
import {token} from '../types/hoge_todo' //トークンのみをインポート
@Component({
standalone: true,
imports: [CustomModule],
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css'],
})
export class DetailTodoComponent implements OnInit{
svs = inject(token) //先程のhogeを引数に用いることでサービスを受け取ることができる
ngOnInit(){
console.log(this.svs)
}
}
如果想要注入任意值的话
如果想注入任何值,可以使用useValue,但是useValue不能单独使用。与provide相同,只要提供一个令牌,就可以传递任何值。对于接收JSON数据等情况,也可以使用这种方法。
另外,也可以在inject方法中添加类型定义。
import {token,token2} from '../types/types_hoge'
@Component({
standalone: true,
imports: [CustomModule],
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css'],
providers:[{provide: token2,useValue:'任意の値をここに記述'}]
})
export class HogeComponent implements OnInit{
svs = inject<any>(token)
val = inject<string>(token2) //useValueの値が展開される
ngOnInit(){
console.log(this.val) //任意の値をここに記述と表示される
}
这一方面与Angular8引入的依赖注入基本相同,几乎没有改变(也可以使用useFactory来传递表达式的计算结果)。
给我一个机会,让我试一试。
非常抱歉自夸,但在第8章中,我创建了专注于独立组件的文章,并以Todo应用为示例进行讲解。同时,该文章也介绍了响应式表单。
我打算根据这篇文章,制作一个简易的待办事项应用程序。原始的开发环境是Angular14,但之后进行了更新到Angular15,并重新构建后进行了测试确认。
这里没有详细解释RxJS或者导航器等内容。
设定main.ts
在独立组件中,如前所述,app.module.ts是不需要的。而 main.ts 扮演着重要的角色,它将负责控制路由。此外,如果准备好任意的路由文件,就可以方便地切换要连接的应用程序,这样分工也会变得非常容易。
然后这次我们希望将服务分配到整个系统中,所以最好在main.ts中进行设置。
请确保不要弄错,provideRlouter是在Angular15开始引入的,并且位于Angular/router模块中。
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import {RouterModule,provideRouter} from '@angular/router'; //追記する
import {TodoService} from './app/services/todo-service'; //注入したいサービス
import {token} from './app/todo-types/types_todo'; //サービス注入用のトークン
import {Route} from './app/routes/app-todo' //ルーティング情報を外部ファイル化する
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
[{provide:token,useClass:TodoService}], //分配したいサービス
provideRouter(Route), //ルーティング設定
}).catch(err => console.error(err));
设置路由信息的配置文件可以按照以下方式进行创建。
import { Routes } from "@angular/router";
//各種コンポーネント
import {TodoComponent} from '../todo-views/todo.component';
import {AddTodoComponent} from '../todo-views/add-todo.component';
import {EditTodoComponent} from '../todo-views/edit-todo.component';
import {DetailTodoComponent} from '../todo-views/detail-todo.component';
export const Route: Routes = [
{path: '',component: TodoComponent},
{path: 'add',component: AddTodoComponent},
{path: 'edit/:id',component: EditTodoComponent,pathMatch: 'full'},
{path: 'detail/:id',component: DetailTodoComponent,pathMatch: 'full'},
]
接下来我们将开始创建一个Todo应用的文件来进行项目,但在此之前,我们需要做另一个准备工作。
在图书馆进行控制
作为对前述的补充,独立组件不需要NgModule,但相反地,您可以仅导入SPA所需的模块,并将其作为库发送给各种组件(不需要声明)。请务必在导入的组件中使用exports来指定您希望使用的模块。如果忘记这一步,外部组件将无法使用各种模块。
※我们也可以将各种组件写入库中,但由于依赖关系不明确,所以这次我们不在这里设置组件。
import {NgModule} from '@angular/core';
import { CommonModule } from '@angular/common';
import {RouterModule } from '@angular/router' //ルーティング制御に必要
import {ReactiveFormsModule} from '@angular/forms' //リアクティブフォーム制御に必要
@NgModule({
imports:[
CommonModule, RouterModule, ReactiveFormsModule,
],
//外部で制御させたいモジュール
exports:[
CommonModule, RouterModule, ReactiveFormsModule,
],
})
export class CustomModule { }
如果想要在这个组件中使用,只需要导入库,就可以省略冗长的写法。现在已经准备好了,接下来开始创建Todo应用程序。
创建一个待办事项应用程序
以下是Todo应用程序的目录结构。●表示组件的一系列文件(组件,模板,样式,规范)。另外,这个应用程序最初是基于以下文章(使用Vue创建),但现在将其简化并转换成Angular。
■app
■routes
-app-todo.ts
■services
-todo-service.ts
■todo-components
-●todo-item //Todo各種の子コンポーネント
-●todos //Todoリストの親コンポーネント
■todo-types
-types-todo.ts //型定義とトークンを格納したファイル
■todo-views
-●add-todo //新規登録
-●app //ルートコンポーネント
-●detail-todo //詳細
-●edit-todo //編集
-●todo //トップ画面
custom.module.ts #ライブラリ
main.ts
根组件
以下是作为所有标准的根组件。需要注意的是,模板中使用了router-outlet标签来指示路由目标,所以也使用了RouterModule(该组件已包含在先前创建的库中)。
由于根组件与main.ts相关联,因此必须指定选择器名称。
import { Component } from '@angular/core';
import { CustomModule } from '../custom.module' //先程設定したモジュールが内包される
@Component({
selector: 'app-root', //main.tsに紐づいている
standalone: true,
imports: [CustomModule], //ここに追記する
template: `<router-outlet></router-outlet>`,
styleUrls: ['./app.component.css'],
})
export class AppComponent{
}
首页
首页看起来像这样。因为需要将其与Todos组件关联起来,所以我们要提前导入Todos组件。以前,我们必须通过模块来控制所有内容,但是现在我们可以通过独立的组件来调用它们。因此,组件之间的依赖关系非常明显。
另外,在這個頁面上實現了轉到新註冊畫面的按鈕。因此,這裡本來也需要導入RouterModule(如果不導入,畫面將無法轉換)。
import { Component } from '@angular/core';
import { CustomModule } from '../custom.module';
import { TodosComponent } from '../todo-components/todos.component' //Todo制御用のコンポーネント
@Component({
standalone: true,
imports: [CustomModule,TodosComponent], //必要なモジュールとコンポーネント
template:`
<h2>TODO一覧</h2>
<app-todos></app-todos><!-- Todosコンポーネントに紐づけ -->
<a routerLink="./add"><button>新規登録</button></a>
`,
styleUrls: ['./todo.component.css']
})
export class TodoComponent{}
定义模型的文件
定义文件如下所示。然而,由于Angular规范的原因,在部分方法控制中也使用了any。此外,还将在此处生成用于服务注入的令牌。
import { InjectionToken } from '@angular/core'
export const token = new InjectionToken<any>("token")
import { Observable } from 'rxjs'
//ベースとなる型(|でor条件を作成できる)
export type Status = 'waiting'|'working'|'completed'|'pending'
export type Data = {
id?: number
title?: string,
description?: string,
str_status?: Status, //上で定義した任意のパラメータ
}
//アレンジされた型
//任意のオブジェクトから独自の型定義を作成したい場合(ここではDataオブジェクトのうち、title、description、sutatusの3つのプロパティを用いたParams型を作成している)
export type Params = Pick<Data,'title'|'description'|'str_status'>
export interface Todos{
find(predicate: (item:Data) => boolean, thisArg?: any)
}
//注入されたサービス用
export interface Services{
sub: Observable<any>,
addTodo: (data:Data)=> void,
updTodo: (data:Data)=> void,
delTodo:(data:Data)=> void,
todoReducer: (mode:string,data:Data)=> void
}
任务列表控制
Todo列表的创建是由todos组件控制的。同时,该组件还具有控制每个Todo的子组件与父子关系,这也由独立组件简化了组件的关联(类似于Vue,React,Svelte等,使关系更加清晰)。另一方面,这个Todo列表是作为顶层页面的todo组件的从属关系,所以使用选择器指定从属关系是必需的。
此外,可以使用令牌获取从服务调用的待办事项数据,并在ngOnInit方法中进行处理。通过在服务中执行注册、修改和删除等操作,可以检测到更新后的数据,并能够订阅该数据。
import { Component, OnInit,inject } from '@angular/core'
import { CustomModule } from '../modules/custom.module'
import {TodoItemComponent } from './todo-item.component' //子コンポーネント
import { Todos,Services,token } from '../todo-types/types_todo'
@Component({
selector: 'app-todos', //従属する親コンポーネントを紐づけるために必須
standalone: true,
imports: [CustomModule,TodoItemComponent],
template: `
<ul>
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
></app-todo-item>
</ul>
`
,
styleUrls: ['../todo-components/todos.component.css']
})
export class TodosComponent implements OnInit{
svs = inject<Services>(token)
todos: Todos
ngOnInit(){
this.svs.sub.subscribe(v => this.todos = v) //更新を検知した値を購読
}
}
控制各个Todo
每个Todo的控制如下所示。因为这里是从父组件接收值,所以没有进行服务订阅。另外,这里也是todos组件的子组件,所以必须指定选择器。
import { Component, Input,inject } from '@angular/core';
import { Services,Data,token } from '../todo-types/types_todo'
import {Router } from '@angular/router'
import { CustomModule } from '../modules/custom.module'
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CustomModule],
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent{
svs = inject<Services>(token)
@Input() todo: Data
constructor(
private rtr: Router,
) {}
postcall(mode,data){
this.svs.todoReducer(mode,data)
}
getId(mode,id){
this.rtr.navigate([`${mode}/${id}`])
}
}
<!-- 各Todo子コンポーネント -->
<div class="card">
<div>
<a routerLink="/" (click)="getId('detail',todo.id)">
<span class="title">{{todo.title}}</span>
</a>
<span class="status">{{ todo.str_status }}</span>
</div>
<div class="action">
<a routerLink="/" (click)="getId('edit',todo.id)"><button>修正</button></a>
<button (click)="postcall('DEL_TODO',todo.id)">削除</button>
</div>
</div>
服务文件
我认为服务文件并没有太大的变化。在这种情况下,Observable很方便。但是要注意删除操作必须使用splice进行控制,否则无法检测到更新(filter是非破坏性的,即使进行控制,变量的值也不会更新)。
※由于我只是订阅了,所以不会发生问题,但如果使用pipe进行中继处理,Observable可能无法按预期运行。在这种情况下,可以使用Behavior Subject定期使用next方法来解决。
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { Data } from '../todo-types/types_todo'
const todos = <any>[]
@Injectable({
providedIn: 'root'
})
export class TodoService{
sub: Observable<any>
constructor(){
this.sub = new Observable((ob)=>{
ob.next(todos)
})
}
//データ登録
addTodo = (data)=>{
todos.push(data)
}
//データ更新
updTodo = (data)=>{
const idx = todos.findIndex((item)=>item.id == data.id)
todos[idx] = {...todos[idx],...data}
}
//データ削除
delTodo = (id)=>{
const idx = todos.findIndex((item)=>item.id == id)
todos.splice(idx,1)
}
todoReducer =(mode,data)=>{
switch(mode){
case "ADD_TODO": this.addTodo(data)
break
case "UPD_TODO": this.updTodo(data)
break
case "DEL_TODO": this.delTodo(data)
break
}
}
}
使用响应式表单来控制CRUD操作。
那么,现在开始讲解在Angular 13中试验性实现并在14中正式实现的响应式表单。简单来说,响应式表单是一种预先设计表单结构的功能,在Angular上可以清晰地组织表单控制,并且可以预先设置值或按组获取输入后的值。此外,在13中,TypeScript控制存在一些令人困扰的缺陷,但在14中已经得到改进。
在这里,对于独立组件的描述有一些注意事项。对于独立组件,需要导入名为ReactiveFormModule的模块(在本例中已包含在自定义组件中)。此外,还需要导入FormGroup、FormControl等各种对象(对于独立组件,FormsModule包含在CommonModule中,不需要指定)。
顺便提一下,这些各种CRUD用的组件各自独立(没有依赖关系),所以无需进行选择器的指定(有或没有都可以运行)。
然后,注册页面通过注入服务来接收数据,但只使用了sendData方法,所以不需要使用OnInit也可以。
创建新的Todo
import { Component,inject } from '@angular/core';
import { CustomModule } from '../modules/custom.module';
import {Router } from '@angular/router'
import { Data,Todos,Status,Services,token } from '../todo-types/types_todo'
import {FormGroup,FormControl} from '@angular/forms'
@Component({
standalone: true,
imports: [CustomModule],
templateUrl: './add-todo.component.html',
styleUrls: ['./add-todo.component.css']
})
export class AddTodoComponent{
svs = inject<Services>(token)
data: Data
constructor(
private rtr:Router,
) {}
//ダイレクトに変数を代入することもできる
date = new Date()
setid = this.date.getTime()
todoForm = new FormGroup({
id: new FormControl<number>(this.setid),
title: new FormControl<string>(''),
description: new FormControl<string>(''),
str_status: new FormControl<Status>('waiting')
})
sendData(mode:string){
const data = this.todoForm.value
this.svs.todoReducer(mode,data)
this.rtr.navigate(['/'])
}
}
模板的描述
模板不再支持双向绑定。取而代之的是必须使用formGroup属性和formControlName属性进行控制,formControlName属性的名称将成为所属于formGroup中指定的组名的表单名。这些属性需要与之前的组件文件中的表单设计相匹配。
具体而言,属于表单组名为todoForm的各种属性是title、description和str_status。
然而,如果要将它设为只读,就需要使用属性绑定,就像ID一样。
※我不知道原因,但组件通常是使用帕斯卡命名法(FormHoge),而属性则使用驼峰命名法(formHoge)进行编写。请注意。
<h2>TODOの作成</h2>
<form [formGroup]="todoForm" (ngSubmit)="sendData('ADD_TODO')">
<div>
<label for="title"> ID</label>
<input type="text" id="title" [value]="setid" readonly />
</div>
<div>
<label for="title"> タイトル</label>
<input type="text" id="title" formControlName="title" />
</div>
<div>
<label for="description"></label>
<textarea id="description" formControlName="description" ></textarea>
</div>
<div>
<label for="status">ステータス</label>
<select id="status" formControlName="str_status">
<option value="waiting">waiting</option>
<option value="working">working</option>
<option value="completed">completed</option>
<option value="pending">pending</option>
</select>
</div>
<button type="submit">作成する</button>
</form>
将输入表单的值传送
表单值的传递是通过嵌入在form标签中的ngSubmit指令中的sendData进行传输。sendData的写法如下,并存储在表单组的原型value属性中。然后只需将其发送到控制用的服务文件即可。
sendData(mode){
const data = this.todoForm.value //フォームに入力された値
const id = this.setid //idの値
this.svs.todoReducer(mode,{...data,id}) //サービス上のメソッドへ転送
this.rtr.navigate(['/']) //TOP画面へ遷移
}
更正图像
在修正页面的情况下,如何进行控制呢?在修正页面的情况下,必须先将相应的值返回到表单中,然后使用patchValue方法对目标表单属性执行更新操作,这将非常有帮助。
虽然也有一个叫做 setValue 的方法,但是这个方法需要设置所有属性。由于这次我们暂时不需要将id返回给模板,因此使用 setValue 反而会导致类型不匹配的错误。
另外,由于本次需要先从服务中接收数据,因此我们使用ngOnint作为接收器来进行patchValue操作。在此期间,类型为除了id以外的属性提取出的Params。
import { Component,OnInit,inject } from '@angular/core'; //OnInitを忘れない
import { CustomModule } from '../modules/custom.module';
import { ActivatedRoute,Router } from '@angular/router'
import { Data,Params,Status,Todos,Services,token } from '../todo-types/types_todo'
import { FormGroup,FormControl} from '@angular/forms'
@Component({
standalone: true,
imports: [CustomModule],
templateUrl: './edit-todo.component.html',
styleUrls: ['./edit-todo.component.css']
})
export class EditTodoComponent implements OnInit{
svs = inject<Services>(token)
todos: Todos
data: Data
id: number
todoForm = new FormGroup({
id: new FormControl<number>(null),
title: new FormControl<string>(''),
description: new FormControl<string>(''),
str_status: new FormControl<Status>('waiting')
})
constructor(
private rt: ActivatedRoute,
private rtr:Router,
) {}
ngOnInit(){
this.svs.sub.subscribe((v)=>{ this.todos = v})
this.id = Number(this.rt.snapshot.params.id)
this.data = this.todos.find((item:Data)=>item.id === this.id)
this.todoForm.patchValue(<Params>this.data)
}
//転送されたデータ
sendData(mode: string){
this.todoForm.patchValue({
id: this.data.id
})
const data = this.todoForm.value
this.svs.todoReducer(mode,data)
this.rtr.navigate(['/'])
}
backto(){
this.rtr.navigate(['/'])
}
}
在更新值时,请不要忘记对id应用patchValue。获取输入表单的值取决于返回id之后的操作。
顺便提一下,TypeScript的优点是可以定义类型而无需赋值,因此下面的写法也是可以的。如果想要逐一确认返回表单的值,可以使用前面提到的方法;如果没有那么多表单需要确认,可以使用以下的方法就可以了。
this.todoForm.patchValue(<Params>this.data)
编辑界面的模板。
编辑用的模板与注册用的模板没有太大区别。虽然是用于编辑,但表单的值会由反应式表单控制,所以只需要在formGroup中使用属性绑定即可满足需求。
<h2>TODOの編集</h2>
<form [formGroup]="todoForm" (ngSubmit)="sendData('UPD_TODO')">
<div>
<label for="title"> タイトル</label>
<input type="text" id="title" formControlName="title" />
</div>
<div>
<label for="description"></label>
<textarea id="description" formControlName="description" ></textarea>
</div>
<div>
<label for="status">ステータス</label>
<select id="status" formControlName="str_status">
<option value="waiting">waiting</option>
<option value="working">working</option>
<option value="completed">completed</option>
<option value="pending">pending</option>
</select>
</div>
<button type="submit">更新する</button>
</form>
<button (click)="backto()">戻る</button>
详细画面(属性指令和自定义管道)
由于详细画面控制组件不需要表单控制,因此其描述也很简单。不过,在这里我们使用了属性指令和自定义管道,它们分别需要进行单独的声明。
只要导入这些,它们也可以同样地使用。
import { Component,OnInit,inject } from '@angular/core';
import { CustomModule } from '../custom.module';
import {ActivatedRoute,Router } from '@angular/router'
import { Data,Todos,Services,token } from '../todo-types/types_todo'
import { TableDirective } from '../directives/table.directive' //属性ディレクティブ
import { SetBrPipe } from '../pipes/set.br.pipe' //カスタムパイプ
@Component({
standalone: true,
imports: [CustomModule,SetBrPipe,TableDirective], //使用するコンポーネント類を記述する
templateUrl: './detail-todo.component.html',
styleUrls: ['./detail-todo.component.css']
})
export class DetailTodoComponent implement OnInit{
svs = inject<Services>(token)
todos: Todos
id: number
data: Data
constructor(
private rt: ActivatedRoute,
private rtr:Router,
) {}
ngOnInit(){
this.svs.sub.subscribe((v)=>{ this.todos = v})
const id = Number(this.rt.snapshot.params.id)
this.data = this.todos.find((item)=>item.id === id)
}
//ルートコンポーネントに戻る
backto(){
this.rtr.navigate(['/'])
}
}
詳細畫面的範本
详细画面与表格不同,直接返回变量。
表格标签使用属性指令(bs_table具有任意属性)来控制Bootstrap。此外,为了反映文本框中的换行,我们使用自定义管道来替换换行符,并使用属性绑定的[innerHTML]。
<h2>TODOの詳細</h2>
<ng-template *ngIf="data.title ==''; then t else f"></ng-template>
<ng-template #t>
<div>ID:{{ data.id }}のTODOが見つかりませんでした</div>
</ng-template>
<ng-template #f>
<table bs_table>
<thead>
<tr>
<th>タイトル</th>
<th>説明</th>
<th>ステータス</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ data.title }}</td>
<td [innerHTML]="data.description | setbr"></td>
<td>{{ data.str_status }}</td>
</tr>
</tbody>
</table>
</ng-template>
<button (click)="backto()">戻る</button>
在独立组件中使用属性指令
属性指令在独立组件上只能使用被独立化的指令。但是,通过进行独立化,可以很方便地在组件上通过导入来使用,所以独立指令更加方便。
顺便说一下,独立组件是Bootstrap的标准实现。
import { Directive,HostBinding } from '@angular/core';
@Directive({
selector: '[bs_table]', //タグに付与するプロパティの名称
standalone: true
})
export class TableDirective {
@HostBinding("class") //class属性にバインド
elementClass = "table table-bordered border-primary" //使用したいBootstrapのプロパティ
}
创建属性指令的命令
当使用属性指令时,同样需要进行standalone声明。d是指令的缩写形式。另外,如果不添加–flat参数,则会创建额外的directives文件夹。
# cd directives
# ng g d 任意のディレクティブ名 --flat --standalone
在独立组件中使用自定义管道。
在独立组件中,RxJS有很大的行为变化。这个变化是Observable不再实时启动,只是普通地订阅是没有问题的,但是中继处理用的pipe无法正常工作(只在加载时启动)。但是如果使用BehaviorSubject,则没有问题。
作为替代方案,建议使用自定义管道。独立组件可以使用自定义管道进行独立化,并且只需像组件一样导入即可。
在这里,我们正在将输入到文本区域中的值通过自定义管道将换行代码转换为br标签。
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'setbr',
standalone: true
})
export class SetBrPipe implements PipeTransform {
transform( str : string){
return str.replace(/\r?\n/g, '<br>') //テキストエリアの改行コードをbrタグに変換
}
}
自定义管道创建的命令
创建自定义管道时,同样需要声明standalone。p是自定义管道的缩写形式。另外,如果没有添加–flat选项,则会创建多余的pipes文件夹。
# cd pipes
# ng g p 任意のパイプ名 --flat --standalone
将属性指令和自定义管道集成到库中。
正如前述,由于库可以包含指令和管道等,因此如果存在频繁使用的指令和管道,统一管理是一个不错的选择。
import {NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {RouterModule,Router } from '@angular/router'
import {ReactiveFormsModule } from '@angular/forms'
import {ButtonDirective} from '../directives/button.directive' //ボタンデザイン制御のディレクティブ
@NgModule({
imports:[
CommonModule,
RouterModule,
ReactiveFormsModule,
ButtonDirective, //追加
],
exports: [
CommonModule,
RouterModule,
ReactiveFormsModule,
ButtonDirective, //追加
],
})
export class CustomModule {}
只需要在您想要使用的组件上进行导入即可。
填補出缺陷
延迟加载
在Angular中,您也可以像Next或Nuxt那样进行延迟加载(即在链接时才进行加载的加载方式)。只需按照以下方式进行编写即可。您只需编写回调函数即可。我已经将修正页面和详细页面的控制组件改为延迟加载。
载入组件:()=> 调用组件的表达式或回调函数。
import { Routes } from "@angular/router";
//各種コンポーネント
import {TodoComponent} from '../todo-views/todo.component';
import {AddTodoComponent} from '../todo-views/add-todo.component';
const lazy_EditTodoComponent = import('../todo-views/edit-todo.component').then(c => c.EditTodoComponent)
const lazy_DetailTodoComponent = import('../todo-views/detail-todo.component').then(c => c.DetailTodoComponent)
export const Route: Routes = [
{path: '',component: TodoComponent},
{path: 'add',component: AddTodoComponent},
{path: 'edit/:id',
loadComponent: ()=> lazy_EditTodoComponent, //コールバック関数にしてもよい
pathMatch: 'full'},
{path: 'detail/:id',
loadComponent: ()=> lazy_DetailTodoComponent, //コールバック関数にしてもよい
pathMatch: 'full'},
]
通常情况下,人们常常将表达式描述为以下方式,但我认为这样会使得使用的组件变得分散,个人觉得很难理解。
loadComponent: import('../todo-views/detail-todo.component').then(c => c.DetailTodoComponent),
使用`loadChildren`可以将路由文件的每个子组件化,并进行延迟加载。 在这种情况下,路径匹配是必需的条件。