2024-12-26 Angular19創(chuàng)建一個基礎項目

快樂的日子就會忘記寫文章夺颤,痛苦的生活又讓我想起了這里。
用當前最新版的Angular19重寫一個舊項目。

一被去、環(huán)境準備

1、Nodejs 不低于 v18.19.1

2奖唯、NPM鏡像設置

npm config set registry = "https://registry.npmmirror.com"

3惨缆、安裝Angular最新版本

npm install -g @angular/cli@latest

如果存在舊版本,需要先卸載丰捷,再安裝

npm uninstall -g @angular/cli
npm cache clean --force
npm install -g @angular/cli@latest

查看當前版本

ng version

二坯墨、創(chuàng)建項目

項目功能比較簡單,只有一個登錄病往,一個照片展示畅蹂,但是按照習慣,我還是分成了兩個模塊

1荣恐、CLI生成基礎框架

ng new project-name

這里就先一路默認選項了
安裝好之后進入項目根目錄液斜,啟動項目

ng serve

一切正常

2累贤、創(chuàng)建兩個懶加載模塊:一個認證模塊,一個主布局模塊少漆。加上 --routing 參數(shù)臼膏,可同時創(chuàng)建對應的路由模塊

ng g m modules/auth --routing
ng g c modules/auth
ng g m modules/layout --routing
ng g c modules/layout 

3、配置主路由示损,修改 app/app.routes.ts 文件渗磅,導航到上面創(chuàng)建好的兩個模塊中

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'auth',
    loadChildren: () =>
      import('./modules/auth/auth.module').then((m) => m.AuthModule),
  },
  {
    path: '',
    loadChildren: () =>
      import('./modules/layout/layout.module').then((m) => m.LayoutModule),
  },
];

修改根模板文件 app/app.component.html,路由到指定模塊

<router-outlet />

4检访、創(chuàng)建登錄頁始鱼,畫廊頁

ng g c pages/login
ng g c pages/gallery

配置路由

auth模塊

默認導航到登錄頁。如果有注冊頁脆贵,也在這里配置医清,目前沒開放注冊,因此略過
app/modules/auth/auth-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthComponent } from './auth.component';
import { LoginComponent } from '../../pages/login/login.component';

const routes: Routes = [
  {
    path: '',
    component: AuthComponent,
    children: [
      { path: 'login', component: LoginComponent },
      { path: '**', redirectTo: 'login', pathMatch: 'full' },
    ],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AuthRoutingModule {}

AuthComponent也需要配置路由標簽卖氨。先引入RouterOutlet
app/modules/auth/auth.component.ts

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-auth',
  imports: [RouterOutlet],
  templateUrl: './auth.component.html',
  styleUrl: './auth.component.css',
})
export class AuthComponent {}

再修改模板文件
app/modules/auth/auth.component.html

<div class="auth-container">
  <router-outlet />
</div>

調(diào)整css会烙,讓內(nèi)容居中展示
app/modules/auth/auth.component.css

.auth-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.7);
}
layout模塊

默認路由到畫廊頁,如果業(yè)務上有例如訂單筒捺、商品柏腻、人員等等頁面,也在這里配置系吭。一個網(wǎng)站通常會有相同的頁頭五嫂、頁腳、側滑導航菜單等功能肯尺,都放在layout里沃缘。只把中心位置留給具體的業(yè)務組件。(但是當前項目沒有這些需求蟆盹,略過)
app/modules/layout/layout-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LayoutComponent } from './layout.component';
import { GalleryComponent } from '../../pages/gallery/gallery.component';

const routes: Routes = [
  {
    path: '',
    component: LayoutComponent,
    children: [
      {
        path: 'gallery',
        component: GalleryComponent,
      },
      { path: '**', redirectTo: 'gallery', pathMatch: 'full' },
    ],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class LayoutRoutingModule {}

app/modules/layout/layout.component.ts

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-layout',
  imports: [RouterOutlet],
  templateUrl: './layout.component.html',
  styleUrl: './layout.component.css',
})
export class LayoutComponent {}

app/modules/layout/layout.component.html

<!-- 這里可以放header -->
<p>layout works!</p>
<router-outlet />
<!-- 這里可以放footer -->

5孩灯、創(chuàng)建環(huán)境配置

ng g environments

配置好開發(fā)環(huán)境和正式環(huán)境的接口地址
environments/environment.development.ts

export const environment = {
    serverAddress: 'http://127.0.0.1:5001',//這是我本地nestjs服務的地址
};

environments/environment.ts

export const environment = {
  serverAddress: 'https://正式地址',
};

到此,主要結構有了逾滥,再來就是補充具體功能

三峰档、功能實現(xiàn)

1、創(chuàng)建數(shù)據(jù)模型

用戶信息結構

ng g interface models/user
export interface User {
  id: number;
  account: string;
  nickname: string;
  avatar: string;
  token: string;
}

畫廊信息結構

ng g interface models/gallery
export interface Gallery {
  id: number;
  url: string;
  created_at: string;
}

登錄參數(shù)結構

ng g interface models/login.dto
export interface LoginDto {
  account: string;
  password: string;
}

畫廊查詢參數(shù)結構

ng g interface models/gallery-query.dto
export interface GalleryQueryDto {
  [key: string]: any;
  start_time?: number;
  end_time?: number;
  size?: number;
  page?: number;
}

基礎接口返回結構

ng g interface models/response-base.dto
export interface ResponseBaseDto<T> {
  code: number;
  data: T;
  msg: string;
}

分頁接口返回結構

ng g interface models/response-page.dto
export interface ResponsePageDto<T> {
  code: number;
  data: Array<T>;
  msg: string;
  total: number;
}

2寨昙、創(chuàng)建常量

用來配置服務端的接口地址

ng g class constants/server-url
import { environment } from '../../environments/environment';

export class ServerUrl {
  public static readonly login = `${environment.serverAddress}/exhibition-hall/v1/users/login`;
  public static readonly gallery = `${environment.serverAddress}/exhibition-hall/v1/h5/gallery`;
}

3讥巡、創(chuàng)建服務,用來進行接口調(diào)用舔哪、數(shù)據(jù)存儲等功能

ng g s services/auth
ng g s services/gallery
ng g s services/storage

auth服務用來登錄

import { Injectable } from '@angular/core';
import { LoginDto } from '../models/login.dto';
import { HttpClient } from '@angular/common/http';
import { ResponseBaseDto } from '../models/response-base.dto';
import { User } from '../models/user';
import { ServerUrl } from '../constants/server-url';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(private readonly http: HttpClient) {}

  login(loginDto: LoginDto) {
    return this.http.post<ResponseBaseDto<User>>(ServerUrl.login, loginDto);
  }
}

storage服務用來存取本地數(shù)據(jù)

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class StorageService {
  constructor() {}

  public get<T>(key: string): T | null {
    const data = localStorage.getItem(key);
    if (data === null) {
      return null;
    }
    return JSON.parse(data);
  }

  public set<T>(key: string, value: T): T | null {
    if (value === undefined) {
      return null;
    }
    localStorage.setItem(key, JSON.stringify(value));
    return value;
  }
}

gallery服務用來獲取照片列表

這里先補充一個工具類欢顷,用來處理參數(shù)對象轉字符串

ng g class utils
export class Utils {
  public static queryParams2str(
    params: Record<string, number | string | boolean | null>
  ): string {
    const querys: Array<string> = [];
    Object.keys(params).forEach((key) => {
      querys.push(`${key}=${params[key]}`);
    });
    return querys.join('&');
  }
}

然后在gallery服務中使用

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { GalleryQueryDto } from '../models/gallery-query.dto';
import { ResponsePageDto } from '../models/response-page.dto';
import { Gallery } from '../models/gallery';
import { ServerUrl } from '../constants/server-url';
import { Utils } from '../utils';

@Injectable({
  providedIn: 'root',
})
export class GalleryService {
  constructor(private readonly http: HttpClient) {}

  list(galleryQueryDto: GalleryQueryDto) {
    return this.http.get<ResponsePageDto<Gallery>>(
      `${ServerUrl.gallery}?${Utils.queryParams2str(galleryQueryDto)}`
    );
  }
}

尼瑪,剛寫完的內(nèi)容發(fā)布就丟失了W皆椤抬驴!

冷靜一下炼七,重新寫吧

4、創(chuàng)建HTTP請求響應攔截器

當接口調(diào)用前布持,自動注入token豌拙。在接口響應后,如果token失效题暖,則進行續(xù)期或跳轉登錄頁按傅。這里的token有效期很長,略過續(xù)期功能

ng g interceptor utils/auth
import { HttpInterceptorFn, HttpStatusCode } from '@angular/common/http';
import { inject } from '@angular/core';
import { StorageService } from '../services/storage.service';
import { User } from '../models/user';
import { StorageKey } from '../constants/storage-key';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const user = inject(StorageService).get<User>(StorageKey.USER);
  const router = inject(Router);
  const token = 'Bearer ' + user?.token;
  const newReq = req.clone({
    headers: req.headers.append('Authorization', token),
  });

  return next(newReq).pipe(
    catchError((err) => {
      if (err.status === HttpStatusCode.Unauthorized) {
        router.navigate(['/auth/login']);
      }
      return throwError(() => err);
    })
  );
};

創(chuàng)建好之后在layout模塊中使用攔截器胧卤,auth模塊不需要
layout.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { LayoutRoutingModule } from './layout-routing.module';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from '../../utils/auth.interceptor';

@NgModule({
  declarations: [],
  imports: [CommonModule, LayoutRoutingModule],
  providers: [provideHttpClient(withInterceptors([authInterceptor]))],
})
export class LayoutModule {}

但是auth模塊也需要使用http請求的唯绍,因此在根模塊配置一個全局的http服務
app.config.ts

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import {
  provideHttpClient,
  withInterceptorsFromDi,
} from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideAnimationsAsync(),
    provideHttpClient(withInterceptorsFromDi()),
  ],
};

這樣auth就會使用全局配置的http服務,而layout中會用自己局部的http覆蓋全局的

5枝誊、創(chuàng)建路由守衛(wèi)

如果發(fā)現(xiàn)客戶端沒有存儲token况芒,直接跳登錄頁

ng g guard utils/auth 
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { StorageService } from '../services/storage.service';
import { User } from '../models/user';
import { StorageKey } from '../constants/storage-key';

export const authGuard: CanActivateFn = (route, state) => {
  const result = !!inject(StorageService).get<User>(StorageKey.USER)?.token;
  if (!result) {
    inject(Router).navigate(['/auth/login']);
  }
  return result;
};

6、UI

安裝官方的UI庫 Angular Material

ng add @angular/material

安裝完需要重啟服務

登錄頁

去官網(wǎng)cv幾個組件侧啼,拼一個登錄頁出來
MatFormFieldModule 表單項
MatInputModule 輸入框
MatIconModule 圖標
MatButtonModule 登錄按鈕
MatSnackBar 消息彈窗
login.component.ts

import {
  ChangeDetectionStrategy,
  Component,
  inject,
  signal,
} from '@angular/core';
import {
  FormControl,
  FormGroup,
  FormsModule,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { AuthService } from '../../services/auth.service';
import { StorageService } from '../../services/storage.service';
import { Router } from '@angular/router';
import { User } from '../../models/user';
import { StorageKey } from '../../constants/storage-key';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'app-login',
  imports: [
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  templateUrl: './login.component.html',
  styleUrl: './login.component.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent {
  private snackBar = inject(MatSnackBar);
  hide = signal(true);

  form = new FormGroup({
    account: new FormControl('', [Validators.required]),
    password: new FormControl('', [Validators.required]),
  });

  constructor(
    private authService: AuthService,
    private storageService: StorageService,
    private router: Router
  ) {}

  clickEvent(event: MouseEvent) {
    this.hide.set(!this.hide());
    event.stopPropagation();
    return false;
  }

  onSubmit() {
    if (!this.form.valid) {
      return;
    }
    const { account, password } = this.form.value;
    this.authService
      .login({
        account: account || '',
        password: password || '',
      })
      .subscribe((data) => {
        if (data.code != 0) {
          this.snackBar.open(`${data.code} ${data.msg}`, '', {
            duration: 3000,
            verticalPosition: 'top',
            panelClass: 'snackbar',
          });
        } else {
          this.storageService.set<User>(StorageKey.USER, data.data);
          this.router.navigate(['gallery']);
        }
      });
  }
}

login.component.html

<form class="login-form" (ngSubmit)="onSubmit()">
  <mat-form-field class="login-form-item">
    <mat-label>賬號</mat-label>
    <input
      matInput
      placeholder="請輸入賬號"
      [formControl]="form.controls.account"
      required
    />
  </mat-form-field>

  <mat-form-field class="login-form-item">
    <mat-label>密碼</mat-label>
    <input
      matInput
      placeholder="請輸入密碼"
      [formControl]="form.controls.password"
      [type]="hide() ? 'password' : 'text'"
      autocomplete
      required
    />
    <button
      mat-icon-button
      matSuffix
      (click)="clickEvent($event)"
      [attr.aria-label]="'Hide password'"
      [attr.aria-pressed]="hide()"
    >
      <mat-icon>{{ hide() ? "visibility_off" : "visibility" }}</mat-icon>
    </button>
  </mat-form-field>

  <button class="btn-login" mat-flat-button type="submit">登錄</button>
</form>

login.component.css

* {
    --mat-sys-surface-variant: white;
}

.login-form {
    background-color: white;
    border-radius: 16px;
    display: flex;
    flex-direction: column;
    width: 20rem;
    height: 14rem;
    padding: 2rem 3rem;
}

.btn-login {
    margin-top: 1rem;
}

登錄頁就做好了


登錄頁

畫廊頁

使用了開源庫ngx-masonry
安裝

npm install ngx-masonry masonry-layout

gallery.component.ts

import { Component } from '@angular/core';
import { Gallery } from '../../models/gallery';
import { GalleryService } from '../../services/gallery.service';
import { NgxMasonryModule } from 'ngx-masonry';
import { NgFor, NgIf } from '@angular/common';

@Component({
  selector: 'app-gallery',
  imports: [NgxMasonryModule, NgFor, NgIf],
  templateUrl: './gallery.component.html',
  styleUrl: './gallery.component.css',
})
export class GalleryComponent {
  galleries: Array<Gallery> = [];

  constructor(
    private readonly galleryService: GalleryService
  ) {
    this.refreshData();
  }

  refreshData() {
    this.galleryService
      .list({})
      .subscribe((data) => {
        if (data.code == 0) {
          this.galleries = data.data || [];
        }
      });
  }
}

gallery.component.html

<ngx-masonry>
  <div ngxMasonryItem class="masonry-item" *ngFor="let gallery of galleries">
    <img *ngIf="gallery.url" class="gallery-img" [src]="gallery.url" />
  </div>
</ngx-masonry>

gallery.component.css

.masonry-item {
    width: calc(20% - 0.5rem);
    margin: 0.25rem;
}

.gallery-img {
    max-width: 100%;
    border-radius: 0.5rem;
}

畫廊頁也好了


畫廊頁

接下來就剩一些業(yè)務上的細節(jié)調(diào)整了牛柒。

至此堪簿,搭建完成

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痊乾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子椭更,更是在濱河造成了極大的恐慌哪审,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虑瀑,死亡現(xiàn)場離奇詭異湿滓,居然都是意外死亡,警方通過查閱死者的電腦和手機舌狗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門叽奥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痛侍,你說我怎么就攤上這事朝氓。” “怎么了主届?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵赵哲,是天一觀的道長。 經(jīng)常有香客問我君丁,道長枫夺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任绘闷,我火速辦了婚禮橡庞,結果婚禮上较坛,老公的妹妹穿的比我還像新娘。我一直安慰自己扒最,他們只是感情好燎潮,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扼倘,像睡著了一般确封。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上再菊,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天爪喘,我揣著相機與錄音,去河邊找鬼纠拔。 笑死秉剑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的稠诲。 我是一名探鬼主播侦鹏,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼臀叙!你這毒婦竟也來了略水?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤劝萤,失蹤者是張志新(化名)和其女友劉穎渊涝,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體床嫌,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡跨释,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厌处。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳖谈。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖阔涉,靈堂內(nèi)的尸體忽然破棺而出缆娃,到底是詐尸還是另有隱情,我是刑警寧澤洒敏,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布龄恋,位于F島的核電站,受9級特大地震影響凶伙,放射性物質(zhì)發(fā)生泄漏郭毕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一函荣、第九天 我趴在偏房一處隱蔽的房頂上張望显押。 院中可真熱鬧扳肛,春花似錦、人聲如沸乘碑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兽肤。三九已至套腹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間资铡,已是汗流浹背电禀。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留笤休,地道東北人尖飞。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像店雅,于是被迫代替她去往敵國和親政基。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容