使用JS框架Angular进行数据处理和整合版本
这是将前一篇文章的续篇按照各个框架进行分类,以便更容易追踪时代潮流变化的作品。
- jQuery愛好家のためのVue.js、React、Angular入門
我希望这次能够解释更深入,包括了上次无法完全弥补的组件周围更深入的操作。同时,在之前我们没有看到Angular具体的模块和组件的具体作用,为了获得实践能力,我希望能深入研究这些方面。
当前这篇内容是带有动作验证的文章,基于 Angular12 的信息,力求使其具有一定的通用性。从 Angular13 开始,将推出渲染引擎的加速化(采用 Ivy),以及响应式表单的试验实施;从 Angular14 开始,除了正式实施响应式表单,还将增加一项划时代的功能——独立组件(不再需要传统的模块)。因此,第8章中我们将专门创建与此相关的文章。
※这次学习的内容是
-
- 5章 コンポーネント制御(電卓)
-
- 6章 ルーティング制御(買い物かご)
-
- 7章 スタイル制御(写真検索システム)
- 8章 スタンドアロンコンポーネントとリアクティブフォーム(Todoアプリ) ※Angular14使用
就是这样。
import構文的共同规则
在那之前,我们应该先了解JavaScript的import语法规则。
-
- A:import Fuga from ‘./Hoge’
- B:import {Fuga} from ‘./Hoge’
如果不理解这两个区别,以后可能会出现许多错误。A表示将外部文件Hoge以名称Fuga进行定义(通常与导入文件名相同,没有特意更改的必要)。而B表示使用由外部文件Hoge中定义的对象Fuga。所以,以下的例子中
import React,{useState,useEffect,useContext} from 'React'
这是一个命令,使用名称为React的外部文件,并使用对象useState、useEffect和useContext。
练习5:组件控制(计算器)
使用子组件在这里,我们将创建一个简单的计算器来制作父组件。为此,需要控制按下和按下时间的推按钮,但是,通过将推按钮的部件转化为子组件,可以更高效地构建系统。
在进行之前,为什么要将父子组件拆分和分层呢?答案是为了避免冗余的描述。
◆使用Angular控制组件
如果使用Angular进行组件控制,稍微有点复杂。
必须在由app-root控制的Angular.component.ts文件中链接父组件。然后,通过父组件将值传递给子组件,再由子组件传递给父组件进行交互。
此外,虽然理论上可以在子组件中定义值,但最终仍需要在父组件和子组件之间进行同步,因此建议在父组件中定义值。
创建应用程序
在Angular中,通常使用”ng”命令来创建应用程序、模块和组件。因此,如果想要创建一个Angular应用程序,只需要使用以下命令即可。
html # ng new 任意のアプリケーション名
由于Angular应用程序的体量较大,因此最好不要像Vue和React那样创建多个项目。如果想要创建一个用于学习的测试环境,可以针对单个应用程序,随时更改后面提到的父组件和模块。
◆调用模块 mó
Angular-cli 在这个过程中调用组件。从脚本中调用模块,然后从模块中调用组件。如果你想创建和控制任意模块, 如路由控制等,你可以调整这里,但基本上保持 app 模块不变可能更好。
主要.ts → 应用模块.ts → 应用组件.ts
如果您想要将任何模块文件(app.module.ts)与之关联,您可以按照以下方式修改main.ts文件。
import { AppModule } from './app/app.module'; //ここで任意のモジュールファイルを呼び出す
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule) //ここのモジュールを書き換える
.catch(err => console.log(err));
创建任意模块的ng命令如下:
app # ng g module hoge --flat --module=hoge
在中文中,将组件与模块绑定起来。
要在Angular中使用组件,需要在模块控制文件(app.module.ts)中将组件与其关联。此外,模板是从组件文件中调用的。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CalcComponent } from './calc.component'; //親コンポーネントのアーキテクチャー
import { SetkeyComponent } from './setkey.component'; //子コンポーネントのアーキテクチャー
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [
CalcComponent, //使用するコンポーネントを宣言しておく
SetkeyComponent, //使用するコンポーネントを宣言しておく
],
imports: [
BrowserModule,
NgbModule,
FormsModule,
FontAwesomeModule,
],
providers: [],
bootstrap: [ CalcComponent, SetkeyComponent ] //bootstrapも設定しておく
})
export class AppModule { }
另外,创建任意组件文件的命令如下(g是generate的缩写命令)。
app # ng g component hoge
运行这个命令后,将创建一个新的文件夹,并在其中设置以下4个文件的一组。
■Hoge
- hoge.component.css //CSS制御用
- hoge.component.html //テンプレートファイル用
- hoge.component.spec.ts
- hoge.component.ts //コンポーネント制御用
如果希望在同一目录下创建,请添加以下命令。这样,将在同一目录下生成4个文件(如果在单个应用程序中存在多个模块,则需要指定模块)。
app # ng g component hoge --flat --module=hoge.module
◆父组件和app-root
与父组件文件关联的模板文件如下所示。如果仔细观察,您会发现它们与Vue的Options API具有相似的结构。
另外,请确保父组件的选择器仍然是app-root(app-root模板与src目录下的index.html相关联,这里被调用为父组件)。如果没有进行正确的关联,将会显示找不到app-root模板的警告提示。
源自Stack Overflow
- Angular2 – Error: The selector “app-root” did not match any elements
调用模板将包括以下部分。
@component({
selector: 'app-root', //親テンプレートの場合はapp-rootとしておく
templateUrl: './calc.component.html', //テンプレートファイルの呼び出し
styleUrls: ['./calc.component.css'] //テンプレートに適用するcss
})
import { Component, OnInit } from '@angular/core';
import { Data } from '../class/calc'; // 追加
let data= {
lnum: null,
cnum: 0 ,
sum: 0 ,
str: '',
sign: '',
}
let vals=[
[['7','7'],['8','8'],['9','9'],['div','÷']],
[['4','4'],['5','5'],['6','6'],['mul','×']],
[['1','1'],['2','2'],['3','3'],['sub','-']],
[['0','0'],['eq','='],['c','C'],['add','+']]
]
@Component({
selector: 'app-root', //indexに紐づいたrootコンポーネント
templateUrl: './calc.component.html', //app-rootに表示させたい親コンポーネント
styleUrls: ['./calc.component.css']
})
export class CalcComponent implements OnInit{
public data = data
public vals = vals
constructor() {}
ngOnInit() {}
//子コンポーネントから値の受け取り
onReceive(data){
this.data = data
}
}
◆模板文件
对于与父组件文件相关联的模板文件,其情况如下:通过*ngFor指令循环控制子组件app-setkey的创建并生成推送键。
<div>
<div *ngFor="let val of vals">
<app-setkey [dataFromParent]='data' [vByP]='v' (ev)="onReceive($event)" *ngFor="let v of val"></app-setkey>
</div>
<div>
<p>打ち込んだ文字:{{data.str}}</p>
<p>合計:{{data.sum}}</p>
</div>
</div>
◆传递值从父组件到子组件
请再次强调一下,Angular中使用属性绑定[dataFromParent]=’data’。通过这个语法,可以将变量传递给子组件。
◆接收子组件传递给父组件的值
另外,在Angular中,可以使用事件绑定(ev)=”onReceive($event)”来接收子组件传递给父组件的值。
另外,还有一个用于接收的方法
onReceive(data){
this.data = data
}
顺带提一下,在组件文件中指定组件名是不必要的,因为这已经在模块中定义了。
◆用于定义变量data的文件
因为变量data是一个对象,所以在TypeScript中,对于每个成员都需要详细指定类型。因此,我们将先创建一个定义文件。
由于TypeScript本身可以自动推断变量的类型,所以现在可以简单地编写定义文件。
export type Data
{
lnum: number;
cnum: number;
sum: number;
str: string;
sign: string;
}
◆ 控制子组件
子组件没有使用外部调用模板的方式,而是故意使用template属性来编写。在这种情况下,应该用反引号将其包围起来,例如`<任意的html>`。
另外,app-setkey模板会成为刚才父组件中模板所描述的组件。
import { Component, OnInit,Input,Output,EventEmitter } from '@angular/core';
@Component({
selector: 'app-setkey', //親のテンプレートで定義した子コンポーネントのセレクタ
template:
`<button type="button" [value]="vByP[0]" (click)="getChar(vByP[0],vByP[1])">
{{vByP[1]}}
</button>`
})
export class SetkeyComponent implements OnInit{
@Input() dataFromParent;
@Input() vByP: string;
@Output() ev = new EventEmitter<any>();
public data = []
constructor(){}
ngOnInit(){}
getChar(chr: string,strtmp:string){
let data = this.dataFromParent
let lnum = data.lnum
let cnum = data.cnum
let sum = data.sum
let sign = data.sign
let str = data.str + strtmp
if(chr.match(/[0-9]/g)!== null){
let num = parseInt(chr)
cnum = cnum * 10 + num //数値が打ち込まれるごとに桁をずらしていく
}else if(chr.match(/(c|eq)/g) == null){
if(lnum != null){
lnum = this.calc(sign,lnum,cnum)
}else{
if(chr == "sub"){
lnum = 0
}
lnum = cnum
}
sign = chr
cnum = 0
}else if( chr == "eq"){
lnum = this.calc(sign,lnum,cnum)
sum = lnum
}else{
lnum = null
cnum = 0
sum = 0
str = ''
}
data.lnum = lnum
data.cnum = cnum
data.sum = sum
data.str = str
data.sign = sign
this.ev.emit(data) //子コンポーネントからの転送処理
}
calc(mode: string,lnum: number,cnum: number){
switch(mode){
case "add": lnum = cnum + lnum
break;
case "sub": lnum = lnum - cnum
break;
case "mul": lnum = lnum * cnum
break;
case "div": lnum = lnum / cnum
break;
}
return lnum
}
}
◆ 在亲子组件之间进行数据交流
◆两种解码器和事件发射器
在Angular中,要实现从父组件传值给子组件和从子组件传值给父组件,我们需要分别从模块中调用Input装饰器和Output装饰器这两种装饰器。另外,在子组件传值给父组件时,我们需要事先定义一个EventEmitter(事件发射器)。
◆从父组件向子组件传递值
要将值从父组件传递给子组件,只需使用Input装饰器并设置属性绑定。此外,要在方法中调用值,只需使用this.xxxx即可获取值。
@Input() dataFromParent; //dataFromParentは親コンポーネントのテンプレートに記載したプロパティバインディングの値
@Input() vByP: string; //vByPはngForディレクティブでループさせたvals内の値
将值从子组件传递给父组件。
要将值从子组件传递给父组件,可以使用Output装饰器,设置事件绑定的值,然后使用EventEmitter传递具体值。
◆输出解码器 qì)
输出解码器用于设置子组件向父组件传递值的事件,并按以下方式进行描述。
@Output() ev = new EventEmitter<any>();
在这里,ev 是在父组件模板中定义的事件绑定的值,后跟 EventEmitter 后面是传递的值的类型。然而,在 TypeScript 中没有 Object 类型,因此在这里我们使用了 Any 来适应任何类型(因为 dataFromParent 变量是在父组件中定义的,所以这没有问题)。
◆发射方法
由于emit()方法是EventEmitter内的方法,所以需要从Output解码器的原型中进行调用,写法如下所示。
this.ev.emit(data)
数据变量是一个经过控制的变量,它与父组件模板中的事件绑定相对应,因此可以在onReceive方法中接收它。
◆要注意的事项
另外,关于输入解码器,只能控制设定为app-root的app.component的父组件。
<app-calc></app-calc>
请注意,即使您冒然尝试从index.html进行控制,也完全不起作用(我亲身花了一整天的时间在这上面)
第五次演習的总结
将组件分解的目的是将冗余的元素进行汇总并进行模板化,以避免不必要的描述。同时,这需要进行变量传递处理。
简而言之,可以总结为这样。
-
- 事前に使用する全コンポーネントをモジュールに追記する。
-
- 親コンポーネントのセレクタにはapp-rootを定義しておく。
-
- 変数定義はどちらのコンポーネントに定義しても動くが、なるべく親コンポーネントに記述するのが望ましい。
-
- 親から子を呼び出す場合は、moduleファイルに必要なコンポーネントを全部記述する。
-
- 親から子への値の受け渡しは、親コンポーネントのテンプレートにプロパティバインディングを設定し、子コンポーネントにInputデコーダを用い、値を受け取る。
- 子から親への値の受け渡しは、Outputデコーダを設定し、受け渡したい変数にEventEmitterのemitメソッドを用い、親コンポーネントのテンプレートに設定したイベントバインディングを用いて、親コンポーネントから任意のメソッドで受け取る。
购买篮子的路由练习6
我們過去已經說明了父子組件之間的控制,但僅僅是針對單個頁面。然而,在現實世界中,網頁和應用程序可以自由地切換多個頁面。負責控制這一切的機制就是路由功能。
在框架中,路由是指有一个基本的链接源组件,可以通过指定路径来切换组件。更专业地说,它是将URI分发给各种组件文件,并显示与这些文件相关联的界面的机制,这也被称为单页面应用程式(SPA)。
Angular是一个拥有标准实现路由功能的框架,需要在模块中添加导入语法。该框架具有显示链接标签和链接目标的标签,并存在用于路径描述的属性”to”。此外,还提供了用于数据传输和接收的库,我们可以使用它们进行数据的传递和接收。
◆ 使用Angular进行路由控制。
Angular的路由配置在模板中只需写入链接路径,其他部分都需要在模块中进行配置。
在Angular的情况下,文件结构如下所示(仅提取了最基本的必需处理内容)。
■app
- ■main-navigation
- main-navigation.component.ts //子コンポーネント(ナビ部分のアーキテクチャー)
- main-navigation.component.html //テンプレート(ここにルーティング用のリンクを記述)
- ■pages
- products.component.ts //商品一覧(ここに商品を入れるボタンがある)
- products.component.html //商品一覧のテンプレート
- cart.component.ts //買い物かご(ここに商品差し戻し、購入のボタンがある)
- cart.component.html / /買い物かご(ここに商品差し戻し、購入のボタンがある)
- app.component.ts //親コンポーネント
- app.component.html //親コンポーネント
- app.service.ts //共通の処理制御用(サービス)
- app.module.ts //ルーティング制御用
- shop-context.ts //商品情報の格納
※■是一个目录。
那么,我们要像以前一样使用商品页面上的产品和购物车页面,按顺序依次查看。
为了控制路由,重要的点是
import {RouterModule,Routes} from '@angular/router';
我们来调用这个路由器模块吧。接下来,我们将使用Routes对象来定义路由。
Hoge:路由 = []
这样,我们定义了对象,并对每个采用的路由目标进行了定义,其中path属性(如果是父目录则为”),并且定义了使用的组件component属性。
const Route: Routes = [
{path: '',component: ProductsComponent},
{path: '/cart',component: CartComponent},
]
将此路由设置设置为模块的imports属性。通过这样做,可以立即将路由设置应用于路由器。
RouterModule.forRoot(Route) 可以被改写为:
RouterModule.forRoot(Route) 的原生中文翻译为:使用Route参数初始化RouterModule。
另外,在exports属性中也需要记录RouterModule。
import { BrowserModule } from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule,Routes} from '@angular/router'; //ルーティングを制御するためのモジュール
import {AppComponent } from './app.component';
import {MainNavigationComponent } from './main-navigation/main-navigation.component'; //ナビ
import {ProductsComponent} from './pages/products.component'; //商品ページ
import {CartComponent} from './pages/cart.component'; //買い物かごページ
//ルーティングの定義
const Route: Routes = [
{path: '',component: ProductsComponent},
{path: 'pages/cart',component: CartComponent},
]
@NgModule({
declarations:[
AppComponent,
MainNavigationComponent,
ProductsComponent,
CartComponent,
],
imports:[
BrowserModule,
RouterModule.forRoot(Route) //この引数に先程定義したルーティング名を代入
],
exports:[RouterModule], //ルーター設定に必要
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
◆模板描述
模板的结构如下所示,与Vue的链接标签和浏览器标签非常相似。通过在a标签中设置routerLink属性来控制链接,并通过router-outlet标签来控制路由的目标。
<div class="main-navigation">
<ul>
<li><a routerLink="/">Products</a></li>
<li><a routerLink="./cart">Cart({{storage.cart.length}})</a></li>
</ul>
</div>
<!-- ルーティング先で反映される領域 -->
<router-outlet></router-outlet>
◆使用RxJS在SPA中进行数据交流
如果在Angular中的组件之间没有父子关系,并且需要在它们之间交换数据,最有效的方式是从一个名为AppService的共享处理服务中导入值。这个共享处理服务在一个名为app.service.ts的文件中定义。
然后,据说Angular回避的决定因素之一就是RxJS成为必需。RxJS非常复杂,深入到足以出版一本书的程度,但简单来说,它是Angular中用于在网络中自由交换数据的库。使用它可以使路由组件之间的数据交互更加顺畅。
※RxJS是Reactive extensions for JavaScript的缩写。而这部分RxJS,在Angular中是最频繁更新规范,语法也变得更简洁(语法糖)化。特别是在Angular6之后,写法大大简化,能够轻松创建网络监视和更新的原型,所以我认为不会像以前那样被人远离这个框架了。
◆使用BehaviorSubject在特定的时间点更新数据。
首先,我们可以使用BehaviorSubject来传递相对简单且定期更新的数据。使用BehaviorSubject的效率非常高,特别是在我们希望在固定的时间间隔内执行更新操作时。具体流程是先准备一个变量cnt来存储商品数量,然后将该变量的值传递给BehaviorSubject。
公共订阅 `sub` 的初始数据为 `cnt`。
为变量sub(名称任意)创建一个原型。在原型sub中,利用共同调用的变量内的方法next发送路由信息。
这个.sub.next(cnt2)《cnt2是更新后的数据》。
通过将更新后的值分配给与原型相关联的next方法,我们可以自动进行同步。需要注意的是,next是执行同步数据的最终更新方法,通过这个方法处理的数据可以作为更新后的信息通过各个组件展开到模板上。
※Injectable方法指示了该service.ts文件可以在任何其他组件文件中使用,通过指定provider: root,这意味着它可以在任何应用程序中使用。换句话说,通过编写此代码,可以无缝地在模块中的任何文件之间传递数据并进行同步。
import { Injectable, OnInit } from '@angular/core'
import { BehaviorSubject,Observable } from 'rxjs'
import Storage,{Product} from './shop-context'
const STORAGE_KEY = "storage_key"
export type CntT = {
"cart": number,
"article": number,
}
const cnt = <CntT>{"cart":0,"article":0} //ナビに表示する個数
@Injectable({
providedIn: 'root'
})
//export class AppService implements OnInit{
export class AppService{
public state =[]
public storage = []
public sub = new BehaviorSubject<any>(cnt) //同期を取りたいデータの設定
su2b: Observable<any>
constructor(){
const storage = Storage()
this.sub2 = new Observable((ob)=>{
ob.next(storage)
})
}
//買い物かごの調整
addProductToCart(product:Product,state){
let cartIndex = null
const stat = state
//買い物かごの調整
const updatedCart = stat.cart;
const updatedItemIndex = updatedCart.findIndex(
item => item.id === product.id
);
if (updatedItemIndex < 0) {
updatedCart.push({ ...product, quantity: 1,stock: 0 });
cartIndex = updatedCart.length -1 //カートの最後尾
} else {
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity++;
updatedCart[updatedItemIndex] = updatedItem;
cartIndex = updatedItemIndex //加算対象のインデックス
}
stat.cart = updatedCart
//商品在庫の調整
const updatedProducts = stat.products //商品情報
const productid = updatedCart[cartIndex].id //在庫減算対象の商品
const productIndex = updatedProducts.findIndex(
p => productid === p.id
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock-- //在庫の減算
updatedProducts[productIndex] = tmpProduct
stat.products = updatedProducts
//合計金額の調整
const total = stat.total
const sum = this.getSummary(updatedCart,total)
stat.total = sum
state = {...state,stat}
this.state = state
}
//カートから商品の返却
removeProductFromCart(productId: string,state){
const stat = state
const updatedCart = [...stat.cart];
const updatedItemIndex = updatedCart.findIndex(item => item.id === productId);
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity--
if (updatedItem.quantity <= 0) {
updatedCart.splice(updatedItemIndex, 1);
} else {
updatedCart[updatedItemIndex] = updatedItem;
}
stat.cart = updatedCart
//商品在庫の調整
const updatedProducts = [...stat.products] //商品情報
const productIndex = updatedProducts.findIndex(
p => p.id === productId
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock++ //在庫の加算
updatedProducts[productIndex] = tmpProduct
stat.products = updatedProducts
//合計金額の調整
let sum = this.getSummary(updatedCart,stat.total)
stat.total = sum
state = {...state,stat}
this.state = state
}
//購入手続き
buyIt(articles,state){
const stat = state
let updatedArticles = [...articles] //所持品
let tmp_cart = [...stat.cart]
for( let cart of tmp_cart){
let articlesIndex = articles.findIndex(
a => a.id === cart.id
)
if (articlesIndex < 0) {
updatedArticles.push(cart);
} else {
const tmpArticles = { ...articles[articlesIndex] }
tmpArticles.quantity++;
updatedArticles[articlesIndex] = tmpArticles;
}
}
stat.articles = updatedArticles
let summary = this.getSummary(tmp_cart,stat.total)
let rest = stat.money - summary
stat.money = rest
tmp_cart.splice(0)
summary = 0
stat.cart = tmp_cart
stat.total = summary
state = {...state,stat}
this.state = state
}
getSummary(cart,total){
const sum = cart.reduce((total,{price = 0,quantity})=> total + price * quantity,0)
return sum
}
cartItemNumber = (ctx)=>{
return ctx.cart.reduce((count,curItem)=>{
return count + curItem.quantity //買い物かごの個数
},0)
}
reducer(mode,selected,storage){
let value = []
switch(mode){
case "add": this.addProductToCart(selected,storage)
break
case "remove": this.removeProductFromCart(selected,storage)
break
case "buy": this.buyIt(selected,storage)
break
}
let cnt_tmp = cnt
cnt_tmp.cart = this.cartItemNumber(storage)
this.sub.next(cnt) //処理を更新する
localStorage.setItem(STORAGE_KEY,JSON.stringify(this.state))
}
}
※只需在Angular中添加返回最后一个变量的部分,就可以重用在Vue和React中使用的方法(尽管在之前的文章中我们故意避免使用解构赋值,但在Angular中使用解构赋值也没有问题)。
◆ 实时更新数据(可观察的)
在RxJS中,还有一种被频繁使用的东西,叫做Observable。商品信息是在这里进行控制的。之前提到的BehaviorSubject是用来反映数据更新的,而这次的Observable则是用来观察和监测数据的动态变化,就像它的和译“观察”、“监视”所表达的意思一样。因此,通过实时监控已设置的数据值,我们可以检测到其变化。因此,如果想要在不确定的时间点进行数据更新,那么不需要reducer方法的next设置,Observable会非常方便。
若要监视数据,请在constructor()中设置Observable。 第7章介绍了constructor的用法,它在Angular的DOM生成之前运行,所以必须提前设置,否则会导致无法处理数据的错误发生。
※模板内容相同
import { Observable } from 'rxjs' //Observable使用を明記する
export class AppService{
public state =<any>[]
sub2: Observable<any> //各種コンポーネントへの展開用
constructor(){
const storage = Storage()
const this.sub2 = new Observable((ob)=>{
ob.next(storage) //同期を取りたいデータを送信
})
}
/*略*/
}
需要注意的是ob是用于监视的对象,sub2是用于描述的对象。因此,如果要在上述的AppService.ts中使用next方法来反映数据,应该使用this.ob.next(BehaviorSubject仅传输更新信息)。
顺便提一下,订阅的意思就是通过subscribe来接收。
◆组件部分的描述
以下是接收订阅数据的组件方面的控制。您需要在构造函数中定义要使用的组件,并通过从先前的方法中调用变量storage来在ngOnInit内共享数据,在模块中与路由相关联。
this.appService.sub.subscribe((v) => {
this.storage = v;
})
这里是关键部分,appService是之前的公共文件,sub是用于同步处理的原型。通过subscibe方法展开的变量v,作为this.storage返回到模板中。此外,postcall函数是与各种模板相关的操作购物车的通用方法,用于与reducer方法进行桥接,包括添加到购物车、从购物车中移除、进行购买等各种处理。
import { Component, OnInit } from '@angular/core';
import { AppService } from "../appservice" //共通制御用のメソッド
@Component({
templateUrl: './products.component.html',
styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
public storage: []
constructor(private appService: AppService) {}
ngOnInit(){
this.appService.sub2.subscribe(v => this.storage = v) //テンプレートに変数を返す
}
//app.serviceに記述されたappService.reducerへの橋渡し
postcall(mode,sel,storage){
this.appService.reducer(mode,sel,storage) //処理振り分け用のメソッド
}
}
◆模板的写作
在模板中,我们会编写用于共享处理的事件绑定后置调用的代码。postcall方法在AppService组件上被定义为处理分支的reducer的桥梁。
请注意,* ng模块需要在模块文件中进行预定义,否则无法启动。
<section class="products">
<ul>
<li *ngFor="let product of storage.products; index as i">
<div >
<strong>{{ product.title }}</strong> - {{ product.price}}円
<ng-template *ngIf="product.stock > 0; then rest"></ng-template>
<ng-template #rest>【残り{{product.stock}}個】 </ng-template>
</div>
<div>
<button (click)="postcall('add',product,storage)">かごに入れる</button>
</div>
</li>
</ul>
</section>
◆利用of和pipe对更新的数据进行加工处理
现在,我们不仅仅是将更新数据直接传递到模板中,还可以对其进行加工并传递到模板中。这就是所谓的of和pipe功能。现在,让我们使用pipe功能,将购物车的总价分成含税费用和内税费用两部分。
import { of,pipe } from 'rxjs' //追記
import { map } from 'rxjs/operators' //追記
/*中略*/
export class CartComponent{
storage:Storage
totaltax: number
constructor(private appService:AppService)
{
const totaltax = this.totaltax //事前に代入する変数を取得
this.appService.sub2.subscribe(v => this.storage = v)//購読されたデータ(サービスからの更新データ)
//ofはループ制御のメソッド
of(this.storage.total)
.pipe(
map((x:number): number => Math.floor(x * 1.1) ) //消費税をかける
).subscribe((x:number):number => this.totaltax = x) //加工処理した値を返す
}
可以这样表达:使用”of”,你可以描述要进行循环处理的对象,或者使用”of(a,b,c)”这样指定多个参数。”pipe”是用于在循环处理的值上进行中继加工的方法。在”pipe”中,有各种处理操作符存储在rxjs/operators中,所以这次我们使用map操作符进行加工处理,将消费税添加到每个总和值(this.storage.total)上。通过”subscribe”方法返回加工后的值,并将其返回到模板上的totaltax。
因此,只要利用這個管道,就可以在本地控制本來不應該存在於通用數據中的變量,例如total。
<section class="cart" >
<p *ngIf="storage.cart.length <= 0">No Item in the Cart!</p>
<ul>
<li *ngFor="let cartitem of storage.cart ; index as i">
<div>
<strong>{{ cartitem.title }}</strong> - {{ cartitem.price }}円
({{ cartitem.quantity }})
</div>
<div>
<button (click)="postcall('remove',cartitem.id,storage)">買い物かごから戻す(1個ずつ)</button>
</div>
</li>
</ul>
<h3>合計: {{ totaltax }}円(内税{{ storage.total }}円)</h3>
<h3>所持金: {{storage.money}}円</h3>
<button (click)="postcall('buy',storage.articles,storage)"
*ngIf="storage.cart.length >0 && storage.money - storage.total >= 0">購入</button>
</section>
顺便提一下,还有一个名为tap的操作符,它可以参考处理过程中的值,与值无关。
import { of,pipe } from 'rxjs'
import { map,tap } from 'rxjs/operators'
/*中略*/
constructor(private appService:AppService)
{
of(this.storage.total)
.pipe(
map((x:number): number => Math.floor(x * 1.1) ), //消費税をかける
tap((x:number): void => console.log(x)) //税込価格を参照する
).subscribe((x:number):number => this.totaltax = x) //加工処理した値を返す
}
虽然有许多其他可以用于DataFrame的操作符,但基本上都是使用map函数。
参考开发博客
-
- 【Angular】Observableの中間処理をつくろう!【pipe,tap,map】
- RxJS6便利なよく使うOperatorsの使い方まとめ
◆※获取模块中的数据的方法
也可以通过模块传递数据。之前的变量数据是从任意服务传输来的,但是通过路由接收数据需要使用ActivatedRoute对象。
首先,我们需要在模块中写下想要传输的数据。
const Route: Routes = [
{path: '',component: ProductsComponent,data{title: "Angularでルーティング"}},
請呼叫 ActivatedRoute 物件並在建構子中定義它。然而,如果你要從模組的 data 屬性中取得該物件,你可以使用 snapshot 方法來輕鬆地取得資料,而不是使用 subscribe 方法(Angular7或更新版本)。
import { Component, OnInit } from '@angular/core';
import {ActivatedRoute} from '@angular/router' //ActivatedRouteオブジェクトを使用
import { AppService } from "../appservice"
@Component({
templateUrl: './products.component.html',
styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
public storage: []
public title: ''
constructor(private route: ActivatedRoute,private appService: AppService) {} //コンストラクタに定義しておく
ngOnInit(){
this.title = this.route.snapshot.data.title //モジュールから受け渡されたデータ(title)
this.appService.sub.subscribe(v => this.storage = v) //app.serviceから受け渡されたデータ(同期処理用のプロトタイプsub)
}
postcall(mode,sel,storage){
this.appService.reducer(mode,sel,storage)
}
}
只需在模板内使用{{title}}进行替换,模块中传递的值将被赋予该位置。
展示详细页面(发送和接收参数)。
通过学习如何接收来自该模块的参数,我们可以尝试接收和处理任意的参数。对于Angular中的情况,如果要将参数嵌入到模板中,是无法直接发送到URL的,因此仍然需要通过事件控制来嵌入参数,这就需要使用Angular中的Router对象的navigate方法。
接收传递动态参数
因此,根据上述情况,由于在to属性中嵌入变量无法成功,我们将更改为在点击事件中发送参数给getId。
<div >
<a routerLink="/" (click)="getId(product.id)"><strong>{{ product.title }}</strong></a>
- {{ product.price}}円
<ng-template *ngIf="product.stock > 0; then rest"></ng-template>
<ng-template #rest>【残り{{product.stock}}個】 </ng-template>
</div>
继续说组件部分。 navigate 方法的使用很简单,看起来可以发送各种参数,但如果想要简单地嵌入参数,只需将链接设置为与模块设置的目标路径相同即可。
路由导航至[要嵌入的路径]
import {ActivatedRoute,Router} from '@angular/router' //Routerを追記する
export class ProductsComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router, //これを追記する
private appService: AppService
) {}
/*略*/
getId(id:string){
id = id.replace("p","")
this.router.navigate([`detail/${id}`]) //routerからnavigateを呼び出す
}
/*略*/
}
◆传递给模块的参数
该模块传递的参数被命名为:id,并通过该值进行获取。
import {DetailComponent} from './pages/detail.component' //追記
const Route: Routes = [
{path: '',component: ProductsComponent},
{path: 'detail/:id',component: DetailComponent}, //追記
{path: 'cart',component: CartComponent,pathMatch: 'full'},
]
@NgModule({
declarations:[
AppComponent,
MainNavigationComponent,
ProductsComponent,
CartComponent,
DetailComponent, //追記
],
/*略*/
})
◆请在详细页面领取
可以使用之前提到的snapshot方法来在详细页面中接收。请注意,这次接收的不是路由器,而是路由(与Vue的useRoute和useRouter的关系相同)。
import { Component, OnInit } from '@angular/core';
import {ActivatedRoute} from '@angular/router' //ActivatedRouteオブジェクトを使用
import { AppService } from "../appservice"
@Component({
selector: 'app-detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.css']
})
export class DetailComponent implements OnInit {
public storage:any = []
public item: any = []
constructor(private route: ActivatedRoute,private appService: AppService) { }
ngOnInit(){
const id = this.route.snapshot.params.id //先程転送したパラメータ
const selid = `p${id}` //検索条件に引っかかるように値を合わせる
this.appService.sub2.subscribe(v => this.storage = v) //サービスから受け取ったデータ
const item = this.storage.products.find((item)=>item.id === selid) //一致するアイテムを取得
this.item = item //テンプレートに返す
}
}
如果需要传递查询参数(已修正)
那么,我们尝试在Angular中通过查询传递数据。由于Angular的navigate方法无法使用“?”字符,以下描述是必需的。
router.navigate([destination path route, {queryParams: {parameter name: parameter value}})]:
路由导航至[目标路径路由,{查询参数:{参数名称:参数值}}]。
getId(id:string){
this.router.navigate(["detail",{queryParams:{'id':id}})
}
模块只需记录路径。
const Route: Routes = [
{path: '',component: ProductsComponent},
{path: 'detail',component: DetailComponent},
在中文中,对于接收端组件,最简单的方法是按照以下方式进行描述。通过以下描述,您可以一次性获取任意的查询参数。
路由快照.queryParams.任意的参数。(lu you kuai zhao . queryParams . ren yi de can shu)
ngOnInit(){
const selid = this.route.snapshot.queryParams.id
this.appService.sub2.subscribe(v => this.storage = v)
const item = this.storage.products.find((item)=>item.id === selid)
this.item = item
}
实现返回按钮
如果要从路由目标返回到前一页,则可以使用Location对象的back方法,并进行事件绑定。
import { Component, OnInit } from '@angular/core';
import { Location } from '@angular/common'; //追記する
import {ActivatedRoute} from '@angular/router' //ActivatedRouteオブジェクトを使用
import { AppService } from "../appservice"
@Component({
selector: 'app-detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.css']
})
export class DetailComponent implements OnInit {
public storage:any = []
public item: any = []
constructor(
private route: ActivatedRoute,
private appService: AppService,
private location: Location, //追記する
) { }
/*略*/
btBackTo(){
this.location.back() //戻る制御
}
}
<ul>
<li>{{item.title}}</li>
</ul>
<button (click)="btBackTo()">戻る</button>
第六次演习总结
■路由总结
-
- リンクは
第七次练习:样式控制(图片搜索系统)
JS框架的吸引力在于可以实时控制样式属性。因此,我在Angular中使用font-awesome图标,为通过大幅重写的照片搜索系统中的喜欢的图片添加了”喜欢!”功能(将心形图标着为红色)。
此外,若要使用font-awesome,需要先在项目中进行安装。
◆使用Angular来控制样式
现在我们尝试通过Angular来进行控制,使用Angular进行类控制非常简单。有多种方法可以实现,其中最常用的方法是利用属性绑定,将属性值赋给style属性,从而将其应用。
<ul class="ulDatas" >
<li class="liData" *ngFor="let item of hits">
<label class="lbHit">{{ item.name }}
<label (click)="colorSet(item.id)">
<fa-icon [icon]="faHeart" [style.color]="item.act"></fa-icon>
</label>
</label>
<br />
<img class="imageStyle" [src]="'../assets/img/'+item.src">
</li>
</ul>
此外,可以通过以下方式来控制click事件中colorSet的内容。
const active = '' //アイコン塗りつぶし対象のカラー
/*中略*/
export class WorldComponent{
/*中略*/
public active = "red"
public selecteditem = []
//スタイル制御
colorSet(id: number){
let cities = [...this.cities]
let active = this.active
let selecteditem = cities.find(function(item,idx){
return item.id == id
})
cities.filter(function(item,idx){
if(item.id == id){
if(selecteditem.act !== active){
selecteditem.act = active
}else{
selecteditem.act = ''
}
}
})
this.cities = cities
}
}
如第4章所述,Angular不会重新生成对象,因此可以通过常规的JS赋值方法来实现同步处理。然而,一旦熟悉了原理,使用分割赋值方式会更容易编写代码。
◆关于Angular的构造函数和ngOnInit
抱歉说明来晚了,但我会简要解释构造函数和ngOnInit。
构造函数是Typescript中实现的一个方法,可在Angular加载之前执行处理操作,还可以在生成DOM之前执行预处理操作。
ngOnInit是一种生命周期钩子,用于在DOM生成后进行初始化处理的方法。在这里,我们将取得的states和cities存储起来,通过在ngOnInit中进行这些操作,可以立即使用数据。
在Angular中,似乎没有一般存在用于监视事件的监视属性。然而,与其他JS框架不同,Angular在视图和架构中进行IO交互,因此只需编写方法即可实现类似的功能。此外,似乎还有使用RxJS的方法,如果有机会我也可以进行解释。
import { Component,OnInit } from '@angular/core';
import * as lodash from 'lodash';
import { faHeart } from '@fortawesome/free-solid-svg-icons';
import state_json from '../json/state.json';
import city_json from '../json/city.json';
const countries:any=[
{ab:"US",name:"United States"},
{ab:"JP",name:"Japan"},
{ab:"CN",name:"China"},
]
const states:any = []
const cities:any = []
const hit_cities:any = []; //選択でヒット
const hit_cities_by_state:any = []; //エリア検索でヒットした都市を絞り込み
const hit_cities_by_word:any = []; //文字検索でヒットした都市を絞り込み
const sel_country:string = '' //選択した国
const sel_state:string = '' //選択したエリア
const word:string = ''; //検索文字
const active:string = '' //アイコン塗りつぶし対象のカラー
@Component({
selector: 'app-world',
templateUrl: './world.component.html',
styleUrls: ['./world.component.css']
})
export class WorldComponent implements OnInit{
public faHeart = faHeart;
public countries = []
public states = []
public cities = []
public sel_country = sel_country
public sel_state = sel_state
public word = ''
public hit_cities = []
public hit_cities_by_state = []
public hit_cities_by_word = []
public active = "red"
public selecteditem = []
constructor(){}
ngOnInit(){
this.countries = countries
this.states = state_json
this.cities = city_json
}
//スタイル制御
colorSet(id: number){
let cities = [...this.cities]
let active = this.active
let selecteditem = cities.find(function(item,idx){
return item.id == id
})
cities.filter(function(item,idx){
if(item.id == id){
if(selecteditem.act !== active){
selecteditem.act = active
}else{
selecteditem.act = ''
}
}
})
this.cities = cities
}
//国から該当するエリアを絞り込み
selectCountry(sel_country){
const states = this.states
const opt_states = states.filter((item,key)=>{ return item.country == sel_country })
this.states = opt_states
}
//エリアから該当する都市を絞り込み
selectState(sel_state){
const hit_cities_by_state = this.cities.filter((item)=>{ return item.state == sel_state })
this.hit_cities_by_state = hit_cities_by_state
this.watcher()
}
//フリーワード検索
searchWord(word){
const hit_cities_by_word = this.cities.filter((item,key)=>{
item = item.name.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
return item.includes(word) && word
})
this.word = word
this.hit_cities_by_word = hit_cities_by_word
this.watcher()
}
//論理積を求める(ただのメソッド)
watcher(){
const hit_cities_by_state = this.hit_cities_by_state
const hit_cities_by_word = this.hit_cities_by_word
const len_state = hit_cities_by_state.length
const len_word = hit_cities_by_word.length
let hits = []
if(len_state > 0 && len_word > 0 ){
hits = lodash.intersection(hit_cities_by_state, hit_cities_by_word)
}else if(len_state > 0){
hits = hit_cities_by_state
}else if(len_word > 0){
hits = hit_cities_by_word
}else{
hits = []
}
this.hit_cities = hits
}
clear(){
this.sel_state = ''
this.sel_country = ''
this.word = ''
this.hit_cities = []
}
}
<label> 国の選択 </label>
<select id="sel1" [(ngModel)]="sel_country"(change)="selectCountry(sel_country)">
<option value=''>-- 国名を選択 --</option>
<option *ngFor="let item of countries" [value]="item.ab">
{{item.name}}
</option>
</select>
<label> エリアの選択</label>
<select id="sel2" [(ngModel)]="sel_state" (change)="selectState(sel_state)" >
<option value=''>-- エリアを選択 --</option>
<option [value]="item.code" *ngFor="let item of states">
{{item.name}}
</option>
</select>
<br/>
<h4>検索文字を入力してください</h4>
<input type="text" id="word" [(ngModel)]="word" (input)="searchWord(word)" />
<button type="button" (click)="clear()">clear</button>
<div>ヒット数:{{ hit_cities.length }}件</div>
<ul class="ulDatas" >
<li class="liData" *ngFor="let item of hit_cities">
<label class="lbHit">{{ item.name }}
<label (click)="colorSet(item.id)">
<fa-icon [icon]="faHeart" [style.color]="item.act"></fa-icon>
</label>
</label>
<br />
<img class="imageStyle" [src]="'../assets/img/'+item.src">
</li>
</ul>
练习8 单独组件和响应式表单(Todo应用)
◆独立组件
Angular14引入了一项颠覆以往Angular常识、可与React、Vue、Svelte等新势力竞争的划时代功能,称为”Standalone Component”。使用这个Standalone Component,可以带来各种优势。
-
- モジュールに依存しないので、アプリケーションを大幅に軽量化できる。つまり、小規模プロジェクトにも適したプロジェクトやコンポーネントを作成できる。
-
- app.module.tsが不要になるので、作業の分担やアプリケーションの切換が簡単になる
-
- ngModule、FormsModule、BrowserModuleは不要になる。
-
- また、使用したいモジュールだけをカスタマイズできる
-
- 任意のコンポーネントの従属関係を明確化できる
-
- ルーティング制御を簡略化できる
-
- 従属コンポーネントが存在しない場合はセレクタを省略できる
- ローカル上の変数はダイレクトに返すことができる
一切利益区区。此外,由于可以随意使用独立组件,所以如果不想要独立化,也可以同时使用传统组件。
顺便说一句,这个页面是参考以下技术类网站(英文)制作的。我做了很多搜索,但只有这个页面能几乎完整地获取到我想要的信息。
- Head Start With Standalone Components In Angular
◆创建独立组件。
使用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 {
}
}
然而,似乎独立组件并不一定需要ngOnit,所以可以安全地将其移除。另外,如果不存在依赖关系的组件,删除选择器属性也没有问题。
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']
})
export class HogeFugaComponent{
constructor() { }
}
◆将现有组件转变为独立的独立组件
您也可以将现有的组件改为独立使用。我已在修改处加上了注释。
在之前使用Angular进行数据交互时,需要执行任意模块*ngHoge,但现在它们都将集中到CommonModule中。此外,在展开类组件时,不再需要逐一编写ngOninit。
通过显示在①处的标记控制,将此标记设置为true,您可以将此组件声明为独立组件。导入RouterModule是因为模板中使用了router-outlet标签。
import { CommonModule } from '@angular/common'; //汎用モジュールNgModuleに代わるモジュール群
import { Component } from '@angular/core';
import { AppService,CntT } from "../appservice";
import {RouterModule} from '@angular/router';
@Component({
selector: 'app-main-navigation',
standalone: true, //①スタンドアロンコンポーネントの宣言
styleUrls: ['./main-navigation.component.css'],
templateUrl: './main-navigation.component.html',
imports:[RouterModule,CommonModule], //使用モジュールを記述
})
//implements onInitとしなくても、データを転送できる
export class MainNavigationComponent{
public cnt:CntT //出力するオブジェクトは型を厳格化しておくこと
constructor(private appService: AppService)
{
this.appService.sub.subscribe(v =>{ this.cnt = v })
}
}
此外,局部变量不仅可以直接返回值,而且如果在模板中没有指定选择器,则不需要定义选择器名称。
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {ActivatedRoute,Router} from '@angular/router'
import { AppService } from "../appservice"
import Storage from '../shop-context'
@Component({
standalone: true,
templateUrl: './products.component.html',
styleUrls: ['./products.component.css'],
imports: [CommonModule],
})
export class ProductsComponent{
storage:Storage
constructor(
private router: Router,
private appservice: AppService,
){
this.appservice.sub2.subscribe(v => this.storage = v) //serviceによって展開された汎用変数
}
title = "Angularでルーティング" //このようにローカル上で制御される値はそのまま返せる
getId(id:string){
this.router.navigate(["detail"],{queryParams:{'id':id}})
}
postcall(mode,sel,storage){
this.appsvs.reducer(mode,sel,storage)
}
}
主要.ts文件的控制
现在让我们来看一下如何在main.ts中进行编写。对于独立组件来说,也可以直接进行路由设置的编写。
import { enableProdMode,importProvidersFrom} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import {RouterModule,Routes} from '@angular/router';
//各種コンポーネント
import {ProductsComponent} from './app/pages/products.component';
import {CartComponent} from './app/pages/cart.component';
import {DetailComponent} from './app/pages/detail.component';
if (environment.production) {
enableProdMode();
}
//ルーティング制御
const Route: Routes = [
{path: '',component: ProductsComponent},
{path: 'detail',component: DetailComponent},
{path: 'cart',component: CartComponent,pathMatch: 'full'},
]
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[importProvidersFrom(RouterModule.forRoot(Route))]
}).catch(err => console.error(err));
如您在最底部的“standalone的Bootstrap”中提到,如果将以下代码添加到providers属性中,您就可以实现路由控制。这样一来,您就可以省去从依赖组件中导出控制的麻烦。
bootstrapApplication(AppComponent,{
providers:[importProvidersFrom(RouterModule.forRoot(Route))]
}).catch(err => console.error(err));
这次,我想用Angular制作一个Todo应用程序。由于这个Todo应用程序包含由路由控制的兄弟组件和父子关系组件,因此我认为您将更加了解这个独立组件的便利性。另外,我还将介绍Angular 14中的另一个亮点,即响应式表单。
更改路由信息
从main.ts可以控制路由的意思是可以自由切换SPA。因此,只需创建任意的路由文件,就可以轻松切换到想要连接的应用程序,这样分工也会变得非常容易。
import { enableProdMode,importProvidersFrom} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import {RouterModule,Routes} from '@angular/router';
import {Route} from './app/routes/shopping-cart' //ルーティング情報を外部ファイル化する
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[importProvidersFrom(RouterModule.forRoot(Route))]
}).catch(err => console.error(err));
这是如何创建外部文件的路由信息。
import { Routes } from "@angular/router";
//各種コンポーネント
import {ProductsComponent} from '../pages/products.component';
import {CartComponent} from '../pages/cart.component';
import {ArticlesComponent} from '../pages/articles.component';
import {DetailComponent} from '../pages/detail.component';
export const Route: Routes = [
{path: '',component: ProductsComponent},
{path: 'detail',component: DetailComponent},
{path: 'cart',component: CartComponent,pathMatch: 'full'},
{path: 'articles',component: ArticlesComponent,pathMatch: 'full'},
]
现在我们已经完成了购物篮应用程序的切换准备,所以接下来我们将在main.ts中创建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 { }
只需要导入自定义模块,就可以省略冗长的写法,以便使用所需的组件。
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import {TodoService} from '../services/todo-service'
import { ActivatedRoute,Router } from '@angular/router' //RouterModuleの記述は不要
import { Data } from '../todo-types/types_todo'
import { FormGroup,FormControl} from '@angular/forms' //ReactiveFormModuleの記述は不要
import { CustomModule } from '../custom.module' //先程設定したモジュールが内包される
@Component({
selector: 'app-edit-todo',
standalone: true,
imports: [CustomModule], //逐一、他のモジュールを記述する必要がなくなる
/*後略*/
那么,我们现在开始制作一个Todo应用程序。
制作一个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 //トップ画面
main.ts
custom.module.ts #カスタムモジュール
根据这一基础,首先需要设置路由文件。
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'},
]
根据这个,我们只需要改变main.ts中的导入文件(路由文件和根组件),以下组件将成为Todo应用程序的组件。
import { enableProdMode,importProvidersFrom} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
//import { AppComponent } from './app/app.component';
import { AppComponent } from './app.component'; //ルートコンポーネントは編集しない方が利便性が高い
import { environment } from './environments/environment';
import {RouterModule,Routes} from '@angular/router';
//import {Route} from './app/routes/shopping-cart'
import {Route} from './app/routes/app-todo' //切り換えたルーティング設定ファイル
if (environment.production) {
enableProdMode();
}
// StandaloneのBootstrap
bootstrapApplication(AppComponent,{
providers:[importProvidersFrom(RouterModule.forRoot(Route))]
}).catch(err => console.error(err));
根组件
以下是作为所有根组件的参考标准的路由组件。请注意,模板中使用了router-outlet标签来指示路由目标,因此也使用了RouterModule。
import { Component, OnInit } from '@angular/core';
import { CustomModule } from '../custom.module' //先程設定したモジュールが内包される
import {TodoService} from '../services/todo-service'
@Component({
selector: 'app-root',
standalone: true,
imports: [CustomModule], //ここに追記する
template: `<router-outlet></router-outlet>`,
styleUrls: ['./app.component.css'],
})
export class AppComponent{
}
首页
首页将呈现如下。由于需要将首页与Todos组件关联起来,所以请确保提前导入Todos组件。以往,所有这些都必须从模块中进行控制,但通过独立组件,可以独立调用组件。因此,组件之间的依赖关系更加清晰易懂。
此外,在此画面上实现了用于转到新注册画面的按钮。因此,需要导入RouterModule(如果不导入它,将无法进行画面转换)。
import { Component, OnInit } from '@angular/core';
import { CustomModule } from '@angular/common';
import { TodosComponent } from '../todo-components/todos.component' //Todo制御用のコンポーネント
import {TodoService} from '../services/todo-service'
import {RouterModule } from '@angular/router' //新規登録へのリンク
@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{
constructor() {
}
}
待办事项列表控制
所有的Todo列表由todos组件控制。此组件管理每个Todo的子组件并建立父子关系,通过独立组件实现了组件的简洁连接(类似于Vue、React、Svelte的关联呈现更加清晰)。另一方面,这个Todo列表是首页的todo组件的从属关系,所以必须使用选择器来指定从属关系。
此外,獨立組件不必要求ngOnInit控制,因此可以在構造函數中處理從服務器調用的Todo數據。通過在服務器上進行註冊、修改和刪除等處理,可以檢測到更新後的數據並訂閱它。
import { Component, OnInit } from '@angular/core'
import { CustomModule } from '@angular/common' //カスタムモジュール
import {TodoItemComponent } from './todo-item.component //子コンポーネント
import {TodoService} from '../services/todo-service' //データや更新情報を格納したサービスファイル
import { Data } 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{
todos: any
constructor(private svs: TodoService) {
this.svs.sub.subscribe(v => this.todos = v) //更新を検知した値を購読
}
}
服务文件
我认为服务文件没有太大变化。在这种情况下,Observable非常方便。然而,如果不使用splice控制删除操作,就无法检测到更新(因为filter是非破坏性的,即使进行控制,变量的值也不会更新)。所以请注意。
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)=>{
const date =new Date()
data.id = date.getTime()
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
}
}
}
控制各个待办事项
每个Todo的控制如下。这里从父组件接收值,所以不需要订阅服务。此外,这里也是todos组件的子组件,所以必须进行选择器指定。
import { Component, Input } from '@angular/core';
import {TodoService} from '../services/todo-service'
import {Router } from '@angular/router'
import { CustomModule } from '../custom.module'
@Component({
selector: 'app-todo-item', //被従属コンポーネントなのでセレクタ名は必須
standalone: true,
imports: [CustomModule],
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent{
@Input() todo
constructor(
private svs:TodoService,
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>
使用响应式表单来控制CRUD。
在Angular13中进行了试验性实施,然后在Angular14中正式实施的响应式表单的解释从这里开始。简单地说,响应式表单是一种预先设计表单结构的功能,在Angular上可以将表单控制更加清晰地分组,并且可以预先设置值或者按组获取输入后的值。此外,在13版本中存在一些TypeScript控制方面的问题,但在14版本中已进行了改进。
在这里,有一些需要注意的点关于独立组件的描述。对于独立组件,在代码中需要引入一个名为ReactiveFormModule的模块(在示例中已包含在自定义组件中)。此外,还需要导入一些各种对象,如FormGroup和FormControl(对于独立组件,不需要导入FormsModule)。
新建Todo注册
import { Component } from '@angular/core';
import { CustomModule } from '../custom.module';
import {TodoService} from '../services/todo-service'
import {Router } from '@angular/router'
import { Data,Todos,Params,Status } 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{
todos: Todos
data: Data
constructor(
private svs:TodoService,
private rtr:Router,
) {
this.svs.sub.subscribe((v)=>{ this.todos = v})
}
//ダイレクトに変数を代入することもできる
date = new Date()
setid = this.date.getTime()
todoForm = new FormGroup({
title: new FormControl<string>(''),
description: new FormControl<string>(''),
str_status: new FormControl<Status>('waiting')
})
sendData(mode){
const data = this.todoForm.value
const id = this.setid
this.svs.todoReducer(mode,{...data,id})
this.rtr.navigate(['/'])
}
}
模板的描述
模板不再支持双向绑定,必须使用formGroup和formControlName这两个属性进行控制。formControlName属性上的名称将成为所属于formGroup中指定的组名的表单名称。这些属性必须与之前在组件文件中设计的表单相匹配。
具体来说,属于表单组名为todoForm的各种属性包括标题、描述和状态。
※我不知道原因,但组件使用帕斯卡命名法(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 //フォームに入力された値
this.svs.todoReducer(mode,data) //サービス上のメソッドへ転送
this.rtr.navigate(['/']) //TOP画面へ遷移
}
修正文件的控制
那么,对于修正文件,应该如何进行控制呢?在修正文件的情况下,需要首先将相应的值返回到表单中,这时可以使用patchValue方法,它会对目标表单属性进行更新操作。
有一个名为setValue的方法,但是它需要设置所有属性。但是这次我们暂时不需要返回id到模板中,如果使用setValue反而会出现类型不匹配的错误。
import { Component } from '@angular/core';
import { CustomModule } from '../custom.module';
import {TodoService} from '../services/todo-service'
import { ActivatedRoute,Router } from '@angular/router'
import { Data,Params,Todos } from '../todo-types/types_todo'
import { FormGroup,FormControl} from '@angular/forms'
@Component({
selector: 'app-edit-todo',
standalone: true,
imports: [CustomModule],
templateUrl: './edit-todo.component.html',
styleUrls: ['./edit-todo.component.css']
})
export class EditTodoComponent{
todos: any
data: Data
todoForm = new FormGroup({
id: new FormControl(null),
title: new FormControl(''),
description: new FormControl(''),
str_status: new FormControl('')
})
constructor(
private svs:TodoService,
private rt: ActivatedRoute,
private rtr:Router,
) {
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)
this.todoForm.patchValue(<Params>{
title: this.data.title,
description: this.data.description,
str_status: this.data.str_status,
})
}
sendData(mode){
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。获取通过value输入的表单值需要在返回id后进行操作。
顺便提一下,TypeScript 的优点之一是可以在不进行赋值的情况下定义类型,因此也可以使用以下的写法来处理。如果需要逐一确认表单返回的值,可以使用前面提到的方法;如果表单中并不存在到那个阶段为止的内容,那么可以采用以下的方法。
this.todoForm.patchValue(<Params>this.data)
编辑界面模板
编辑用的模板与注册用的模板没有区别。虽然是用于编辑,但是表单的值会由ReactiveForm来控制,因此只需要使用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>
如果Angular 14的启动或更新速度较慢,补救措施如下:
如果在Angular.json中启动或更新很慢的情况下,调整以下内容似乎会有所帮助。虽然这是关于Angular 12的文章,但通过使用ng update进行更新时,似乎会继承12的配置。
启动时间已经从之前的约30秒,缩短到了大约3秒左右。
在升级到Angular v12之后,加快开发服务器的发展速度。
“`json:Angular.json
{
“$schema”: “./node_modules/@angular/cli/lib/config/schema.json”,
“version”: 1,
“newProjectRoot”: “projects”,
“projects”: {
“MyAngular”: {
“root”: “”,
“sourceRoot”: “src”,
“projectType”: “application”,
“prefix”: “app”,
“schematics”: {},
“targets”: {
“build”: {
“builder”: “@angular-devkit/build-angular:browser”,
“options”: {
“outputPath”: “dist/MyAngular”,
“index”: “src/index.html”,
“main”: “src/main.ts”,
“polyfills”: “src/polyfills.ts”,
“tsConfig”: “src/tsconfig.app.json”,
“assets”: [
“src/favicon.ico”,
“src/assets”
],
“styles”: [
“src/styles.css”,
{
“input”: “./node_modules/bootstrap/dist/css/bootstrap.css”,
“inject”: true
}
],
“scripts”: []
},
“configurations”: {
“production”: {
“fileReplacements”: [
{
“replace”: “src/environments/environment.ts”,
“with”: “src/environments/environment.prod.ts”
}
],
“optimization”: false,
“outputHashing”: “all”,
“sourceMap”: true,
“namedChunks”: false,
“aot”: true,
“extractLicenses”: false,
“vendorChunk”: true,
“buildOptimizer”: false,
}
},
“defaultConfiguration”: “production”,
},
“serve”: {
“builder”: “@angular-devkit/build-angular:dev-server”,
“defaultConfiguration”: ”,
“options”: {
“browserTarget”: “MyAngular:build”,
“host”: “0.0.0.0”,
“port”: 8080,
},
“configurations”: {
“production”: {
“browserTarget”: “MyAngular:build:production”
}
}
},
“extract-i18n”: {
“builder”: “@angular-devkit/build-angular:extract-i18n”,
“options”: {
“browserTarget”: “MyAngular:build”
}
},
“test”: {
“builder”: “@angular-devkit/build-angular:karma”,
“options”: {
“main”: “src/test.ts”,
“polyfills”: “src/polyfills.ts”,
“tsConfig”: “src/tsconfig.spec.json”,
“karmaConfig”: “src/karma.conf.js”,
“styles”: [
“src/styles.css”
],
“scripts”: [],
“assets”: [
“src/favicon.ico”,
“src/assets”
]
}
},
“lint”: {
“builder”: “@angular-devkit/build-angular:tslint”,
“options”: {
“tsConfig”: [
“src/tsconfig.app.json”,
“src/tsconfig.spec.json”
],
“exclude”: [
“**/node_modules/**”
]
}
}
}
},
“MyAngular-e2e”: {
“root”: “e2e/”,
“projectType”: “application”,
“targets”: {
“e2e”: {
“builder”: “@angular-devkit/build-angular:protractor”,
“options”: {
“protractorConfig”: “e2e/protractor.conf.js”,
“devServerTarget”: “MyAngular:serve”
},
“configurations”: {
“production”: {
“devServerTarget”: “MyAngular:serve:production”
}
}
},
“lint”: {
“builder”: “@angular-devkit/build-angular:tslint”,
“options”: {
“tsConfig”: “e2e/tsconfig.e2e.json”,
“exclude”: [
“**/node_modules/**”
]
}
}
}
}
},
“defaultProject”: “MyAngular”,
“cli”: {
“analytics”: false
}
}
“`
Angular.json
“`json:Angular.json
{
“$schema”: “./node_modules/@angular/cli/lib/config/schema.json”,
“version”: 1,
“newProjectRoot”: “projects”,
“projects”: {
“MyAngular”: {
“root”: “”,
“sourceRoot”: “src”,
“projectType”: “application”,
“prefix”: “app”,
“schematics”: {},
“targets”: {
“build”: {
“builder”: “@angular-devkit/build-angular:browser”,
“options”: {
“outputPath”: “dist/MyAngular”,
“index”: “src/index.html”,
“main”: “src/main.ts”,
“polyfills”: “src/polyfills.ts”,
“tsConfig”: “src/tsconfig.app.json”,
“assets”: [
“src/favicon.ico”,
“src/assets”
],
“styles”: [
“src/styles.css”,
{
“input”: “./node_modules/bootstrap/dist/css/bootstrap.css”,
“inject”: true
}
],
“scripts”: []
},
“configurations”: {
“production”: {
“fileReplacements”: [
{
“replace”: “src/environments/environment.ts”,
“with”: “src/environments/environment.prod.ts”
}
],
“optimization”: false,
“outputHashing”: “all”,
“sourceMap”: true,
“namedChunks”: false,
“aot”: true,
“extractLicenses”: false,
“vendorChunk”: true,
“buildOptimizer”: false,
}
},
“defaultConfiguration”: “production”,
},
“serve”: {
“builder”: “@angular-devkit/build-angular:dev-server”,
“defaultConfiguration”: ”,
“options”: {
“browserTarget”: “MyAngular:build”,
“host”: “0.0.0.0”,
“port”: 8080,
},
“configurations”: {
“production”: {
“browserTarget”: “MyAngular:build:production”
}
}
},
“extract-i18n”: {
“builder”: “@angular-devkit/build-angular:extract-i18n”,
“options”: {
“browserTarget”: “MyAngular:build”
}
},
“test”: {
“builder”: “@angular-devkit/build-angular:karma”,
“options”: {
“main”: “src/test.ts”,
“polyfills”: “src/polyfills.ts”,
“tsConfig”: “src/tsconfig.spec.json”,
“karmaConfig”: “src/karma.conf.js”,
“styles”: [
“src/styles.css”
],
“scripts”: [],
“assets”: [
“src/favicon.ico”,
“src/assets”
]
}
},
“lint”: {
“builder”: “@angular-devkit/build-angular:tslint”,
“options”: {
“tsConfig”: [
“src/tsconfig.app.json”,
“src/tsconfig.spec.json”
],
“exclude”: [
“**/node_modules/**”
]
}
}
}
},
“MyAngular-e2e”: {
“root”: “e2e/”,
“projectType”: “application”,
“targets”: {
“e2e”: {
“builder”: “@angular-devkit/build-angular:protractor”,
“options”: {
“protractorConfig”: “e2e/protractor.conf.js”,
“devServerTarget”: “MyAngular:serve”
},
“configurations”: {
“production”: {
“devServerTarget”: “MyAngular:serve:production”
}
}
},
“lint”: {
“builder”: “@angular-devkit/build-angular:tslint”,
“options”: {
“tsConfig”: “e2e/tsconfig.e2e.json”,
“exclude”: [
“**/node_modules/**”
]
}
}
}
}
},
“defaultProject”: “MyAngular”,
“cli”: {
“analytics”: false
}
}
“`