项目介绍
本项目为本人angular练习练手项目,是基于 Angular 的 Web 应用,用于展示和搜索 Bangumi 上的动画,使用 API 来自 Bangumi API。
本项目使用 GitHub Actions 自动部署到 GitHub Pages。
项目名称
my-angular-project-test
项目目的
- 部署一个基于 Angular 的静态网站
- 练习 GitHub Actions 自动部署
- 调用API实现功能
- 使用Cognito进行用户认证
- 使用拦截器处理请求
- 使用守卫保护页面
项目技术栈
- Angular 16
- TypeScript
- HTML
- CSS
- GitHub Actions
- Cognito
环境准备
环境要求
- Node.js 版本 20 或更高
- Angular CLI
安装步骤
-
安装 Node.js
<https://nodejs.org/en/download/> -
安装 Angular CLI
npm install -g @angular/cli -
安装项目
git clone <https://github.com/dreaife/my-angular-project-test.git>cd my-angular-project-testnpm install
项目结构
目录结构
本项目使用Angular CLI 创建,结构如下:
my-angular-project-test/├── src/│ ├── app/│ │ ├── environment/│ │ │ ├── environment.ts│ │ ├── components/│ │ │ ├── login/│ │ │ ├── home/│ │ │ ├── search/│ │ ├── guards/│ │ │ ├── auth.guard.ts│ │ ├── interceptors/│ │ │ ├── auth.interceptor.ts│ │ ├── services/│ │ │ ├── auth.service.ts│ │ │ ├── bgm.service.ts│ │ ├── app.component.ts│ ├── index.html│ ├── main.ts├── ...其中:
src/app目录为项目的主要目录,包含所有组件、服务、拦截器、守卫等。src/environments目录为环境配置文件,包含开发环境和生产环境配置。src/components目录为项目的主要组件,包含所有页面组件。login组件为登录页面,调用cognito的sdk进行登录;home组件为动画日历页面,通过调用bgm.service.getCalendar获取数据并展示;search组件为搜索页面,通过调用bgm.service.search获取数据并展示。
src/guards目录为项目的主要守卫,包含auth.guard.ts守卫,用于保护需要登录的页面, 如果未登录则重定向到登录页面。src/interceptors目录为项目的主要拦截器,包含auth.interceptor.ts拦截器,用于在请求中添加认证信息。src/services目录为项目的主要服务,包含auth.service.ts服务,用于处理登录、登出等操作;bgm.service.ts服务,用于调用 Bangumi API。src/main.ts为项目的主入口文件,用于启动 Angular 应用。
关键功能实现
使用Cognito进行用户认证
在 src/app/services/auth.service.ts 中,使用Cognito的sdk进行用户认证。
在使用cognito之前,需要现在AWS Cognito中创建用户池,自定义Cognito验证域名,并创建应用客户端,获取客户端ID。
用获取到的ID在 src/app/environment/environment.ts 中,配置Cognito的配置信息。
登录
通过cognitoUser.authenticateUser方法进行登录,成功后将idToken或accessToken存储到sessionStorage中。
tips:
对于未验证的用户,需要先重写新密码。此时需要重写newPasswordRequired方法,通过设置resolve({ newPasswordRequired: true, cognitoUser }),在登录页面中切换展示内容,提示用户进行新密码设置。
代码实现:
signIn(username: string, password: string): Promise<any> { const authenticationDetails = new AuthenticationDetails({ Username: username, Password: password });
const userData = { Username: username, Pool: this.userPool }; const cognitoUser = new CognitoUser(userData);
return new Promise((resolve, reject) => { cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) => { // 获取 Tokens const idToken = result.getIdToken().getJwtToken(); const accessToken = result.getAccessToken().getJwtToken(); const refreshToken = result.getRefreshToken().getToken();
// console.log('idToken', idToken); // console.log('accessToken', accessToken); // console.log('refreshToken', refreshToken);
// 将 idToken 或 accessToken 存储到 sessionStorage 作为 userToken sessionStorage.setItem('userToken', accessToken);
// 保存 Tokens 或在需要的地方使用 resolve({ idToken, accessToken, refreshToken });
// 登录成功后重定向到主页 this.router.navigate(['/']); }, onFailure: (err) => { reject(err.message || JSON.stringify(err)); }, newPasswordRequired: (userAttributes, requiredAttributes) => { // 触发新密码需求,提示前端进行新密码设置 resolve({ newPasswordRequired: true, cognitoUser }); } }); }); }用户设置新密码时,调用completeNewPassword方法,通过cognitoUser.completeNewPasswordChallenge方法设置新密码。
// 设置新密码方法 completeNewPassword(cognitoUser: CognitoUser, newPassword: string): Promise<any> { return new Promise((resolve, reject) => { cognitoUser.completeNewPasswordChallenge(newPassword, {}, { onSuccess: (session) => resolve(session), onFailure: (err) => reject(err.message || JSON.stringify(err)) }); }); }注册
通过cognitoUser.signUp方法进行注册,成功后将用户名和密码存储到cognito中。将页面重定向到登录页面。
// 注册方法 signUp(username: string, password: string, email: string): Promise<any> { return new Promise((resolve, reject) => { const attributeList : CognitoUserAttribute[] = []; attributeList.push(new CognitoUserAttribute({ Name: 'email', Value: email }));
this.userPool.signUp(username, password, attributeList, [], (err, result) => { if (err) { reject(err.message || JSON.stringify(err)); } else { resolve(result?.user); } }); }); }登出
通过cognitoUser.signOut方法进行登出,登出后删除sessionStorage中的userToken。
logout() { // 登出 this.userPool.getCurrentUser()?.signOut(); sessionStorage.removeItem('userToken'); this.router.navigate(['/login']); }登录页面
登录页面为 src/app/components/login/login.component.ts,使用cognito的sdk进行登录,成功后将idToken或accessToken存储到sessionStorage中。
页面通过authMode控制页面显示内容,authMode有以下几种:
- login:登录页面
- register:注册页面
- forgotPassword:忘记密码页面
- confirmSignUp:验证页面
- resetPassword:重置密码页面
当点击相应按钮时,调用authService的switchMode方法切换authMode,从而切换页面显示内容。
页面实现:
switchMode(mode: 'login' | 'register' | 'forgotPassword' | 'confirmSignUp' | 'resetPassword') { this.authMode = mode; this.message = '';}
onSubmit() { if (this.authMode === 'login') { this.authService.signIn(this.username, this.password).then( (resp) => { if (resp.newPasswordRequired) { // 初次登录需要重置密码,显示浮窗 this.showNewPasswordModal = true; this.cognitoUser = resp.cognitoUser; } else { // 登录成功 this.message = '登录成功!'; } }).catch(err => { this.message = `登录失败:${err}`; }); } else if (this.authMode === 'register') { this.authService.signUp(this.username, this.password, this.email).then( () => { this.message = '注册成功!请检查邮箱并输入验证码。'; this.authMode = 'confirmSignUp'; }, (err) => (this.message = `注册失败:${err}`) ); } else if (this.authMode === 'forgotPassword') { this.authService.forgotPassword(this.username).then( () => { this.message = '验证码已发送,请检查邮箱并输入验证码和新密码。'; this.authMode = 'resetPassword'; }, (err) => (this.message = `发送验证码失败:${err}`) ); } else if (this.authMode === 'confirmSignUp') { this.authService.confirmSignUp(this.username, this.code).then( () => (this.message = '验证成功!请登录。'), (err) => (this.message = `验证失败:${err}`) ); } else if (this.authMode === 'resetPassword') { this.authService.confirmPassword(this.username, this.code, this.newPassword).then( () => { this.message = '密码重置成功!请使用新密码登录。'; this.authMode = 'login'; // 切换回登录页面 }, (err) => (this.message = `密码更新失败:${err}`) ); } }动画日历页面
动画日历页面为 src/app/components/home/home.component.ts,通过调用bgm.service.getCalendar获取数据并展示。
当页面初始化时,调用ngOnInit方法,获取数据并展示。
ngOnInit() : void { // this.bgmService.getCalendar().subscribe(data => console.log(data)); // this.bgmService.getSubject('482850').subscribe(data => console.log(data)); this.bgmService.getCalendar().subscribe((data:any[]) => { this.weeklyData = Array(7).fill(null).map((_, index) => ({ day: this.daysOfWeek[index], items: data .find((d: any) => d.weekday.id === index + 1) ?.items.filter((item: any) => item.collection?.doing >= 100) || [] })); }); }
navigateToItem(id: string) { this.router.navigate(['/items', id]); }
// 显示浮窗并加载数据 openModal(itemId: string): void { this.bgmService.getSubject(itemId).subscribe((data) => { this.selectedItem = data; this.showModal = true; }); } // 关闭浮窗 closeModal(): void { this.showModal = false; this.selectedItem = null; }
// 辅助方法:查找 infobox 中的官方网站 URL getOfficialWebsite(): string | null { if (!this.selectedItem || !this.selectedItem.infobox) return null; const website = this.selectedItem.infobox.find((info: any) => info.key === '官方网站'); return website ? website.value : null; }搜索页面
搜索页面为 src/app/components/search/search.component.ts,通过调用bgm.service.search获取数据并展示。
页面通过title接收搜索关键词,通过options控制页面显示内容,options有以下几种:
- limit:每页显示条数
- type:类型
- meta_tags:元标签
- tag:标签
- air_date:放送日期
- rating:评分
- rank:排名
- nsfw:是否包含成人内容
- page:页码
向API发送请求时,将title和options重构后发送。请求重构如下:
// 搜索方法 onSearch(): void { if (this.searchQuery.trim()) { this.isLoading = true; this.errorMessage = '';
// 配置搜索选项 const options = { limit: this.limit, type: this.type, meta_tags: this.meta_tags, tag: this.tag, air_date: this.air_date, rating: this.rating, rank: this.rank, nsfw: this.nsfw, page: this.page };
this.bgmService.searchSubject(this.searchQuery, options).subscribe( (response: any) => { this.searchResults = response.data; // 提取 data 数组 this.totalResults = response.total; // 提取总数 this.isLoading = false; }, (error) => { this.errorMessage = '搜索失败,请重试。'; this.isLoading = false; } ); } }拦截器添加认证信息
在 src/app/interceptors/auth.interceptor.ts 中,通过拦截器在请求中添加认证信息。
拦截器实现:
export const authInterceptor: HttpInterceptorFn = (req, next) => { const authToken = environment.bgm.authToken;
if (req.url.startsWith('<https://api.bgm.tv/v0>')) { # 如果请求地址以https://api.bgm.tv/v0开头,则添加认证信息 const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${authToken}`, # 添加认证信息 } }); return next(authReq); } return next(req);};在使用拦截器时,需要在 app.config.ts 中配置拦截器。
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor]) ) ]};守卫保护页面
在 src/app/guards/auth.guard.ts 中,通过守卫保护需要登录的页面,如果未登录则重定向到登录页面。
守卫实现:
export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router);
if (authService.isLoggedIn()) { return true; } else { // 没有登录,重定向到 /login router.navigate(['/login']); return false; }};在使用守卫时,需要在路由配置中使用守卫。
{ path: '', component: HomeComponent, canActivate: [authGuard] }, # 主页需要登录项目启动
-
启动项目
ng serve -
访问地址
项目部署
-
本地构建项目
ng build -
自动化部署
本项目使用 GitHub Actions 自动部署到 GitHub Pages,每次 push 代码到 GitHub 后,GitHub Actions 会检测到 push 事件并自动构建项目并部署到 GitHub Pages。
编写配置文件
.github/workflows/main.yml,用于配置 GitHub Actions 自动部署项目。内容如下:
# GitHub Actions 工作流,用于将项目部署到 GitHub Pagesname: Deploy to GitHub Pages# 触发条件:当推送到 master 分支时触发on:push:branches:- master # 或者你要监控的分支名称jobs:build-and-deploy:# 使用最新的 Ubuntu 作为运行环境runs-on: ubuntu-lateststeps:# 第一步:检出代码- name: Checkout codeuses: actions/checkout@v3# 第二步:设置 Node.js 环境- name: Setup Node.jsuses: actions/setup-node@v3with:node-version: '20' # 请根据项目需求修改 Node.js 版本# 第三步:安装项目依赖- name: Install dependenciesrun: npm install# 第四步:生成环境配置文件 environment.ts- name: Generate environment.tsrun: |# 创建 src/app/environment 目录(如果不存在)mkdir -p src/app/environment# 生成 environment.ts 文件,包含 Cognito 和 Bangumi API 的配置信息echo "export const environment = {production: true,cognito: {userPoolId: '$COGNITO_USER_POOL_ID',clientId: '$COGNITO_CLIENT_ID',domain: '$COGNITO_DOMAIN'},bgm: {url: '<https://api.bgm.tv/v0>',authToken: '$BGM_AUTH_TOKEN',userAgent: 'dreaife/my-angular-project-test'}};" > src/app/environment/environment.ts # 生成环境配置文件# 列出生成的文件以确认ls src/app/environment# 第五步:构建项目- name: Build projectrun: npm run build -- --configuration production --base-href "/my-angular-project-test/" # 构建项目# 第六步:部署到 GitHub Pages- name: Deploy to GitHub Pagesuses: JamesIves/github-pages-deploy-action@v4with:# browser 为构建输出的文件夹,内部文件包含 index.htmlfolder: dist/my-angular-project/browser # 请根据实际输出路径填写token: ${{ secrets.TOKEN }}# 环境变量配置,使用 GitHub Secrets 存储敏感信息env:COGNITO_USER_POOL_ID: ${{ secrets.COGNITO_USER_POOL_ID }}COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }}COGNITO_DOMAIN: ${{ secrets.COGNITO_DOMAIN }}BGM_AUTH_TOKEN: ${{ secrets.BGM_AUTH_TOKEN }}GITHUB_TOKEN: ${{ secrets.TOKEN }}
Project Introduction
This project is my Angular practice project, a web application based on Angular used to display and search for animations on Bangumi, with APIs sourced from the Bangumi API.
This project uses GitHub Actions to automatically deploy to GitHub Pages.
Project Name
my-angular-project-test
Project Goals
- Deploy a static website based on Angular
- Practice automatic deployment with GitHub Actions
- Implement features by calling APIs
- Use Cognito for user authentication
- Use an interceptor to handle requests
- Use guards to protect pages
Tech Stack
- Angular 16
- TypeScript
- HTML
- CSS
- GitHub Actions
- Cognito
Environment Setup
Environment Requirements
- Node.js version 20 or higher
- Angular CLI
Installation Steps
-
Install Node.js
<https://nodejs.org/en/download/> -
Install Angular CLI
npm install -g @angular/cli -
Install the project
git clone <https://github.com/dreaife/my-angular-project-test.git>cd my-angular-project-testnpm install
Project Structure
Directory Structure
This project was created with Angular CLI and the structure is as follows:
my-angular-project-test/├── src/│ ├── app/│ │ ├── environment/│ │ │ ├── environment.ts│ │ ├── components/│ │ │ ├── login/│ │ │ ├── home/│ │ │ ├── search/│ │ ├── guards/│ │ │ ├── auth.guard.ts│ │ ├── interceptors/│ │ │ ├── auth.interceptor.ts│ │ ├── services/│ │ │ ├── auth.service.ts│ │ │ ├── bgm.service.ts│ │ ├── app.component.ts│ │ ├── index.html│ │ ├── main.ts│ ├── ...Among them:
src/appdirectory is the main directory of the project, containing all components, services, interceptors, guards, etc.src/environmentsdirectory contains environment configuration files, including development and production configurations.src/componentsdirectory is the main set of components, containing all page components.- The
logincomponent is the login page, invoking Cognito’s SDK to log in; - The
homecomponent is the animation calendar page, fetching data viabgm.service.getCalendarand displaying it; - The
searchcomponent is the search page, fetching data viabgm.service.searchand displaying it.
- The
src/guardsdirectory contains the main guards, includingauth.guard.ts, used to protect pages that require login; if not logged in, redirects to the login page.src/interceptorsdirectory contains the main interceptors, includingauth.interceptor.ts, used to attach authentication information to requests.src/servicesdirectory contains the main services, includingauth.service.tsfor handling login, logout, etc.;bgm.service.tsfor calling the Bangumi API.src/main.tsis the main entry file to start the Angular application.
Key Feature Implementations
User Authentication with Cognito
In src/app/services/auth.service.ts, Cognito’s SDK is used to authenticate users.
Before using Cognito, you need to create a user pool in AWS Cognito, customize Cognito domain, and create an app client to obtain the client ID.
Configure Cognito using the obtained ID in src/app/environment/environment.ts.
Login
Login is performed via the cognitoUser.authenticateUser method. On success, the idToken or accessToken is stored in sessionStorage.
tips:
For unverified users, you need to reset the password. At this time, the newPasswordRequired method should be triggered, by setting resolve({ newPasswordRequired: true, cognitoUser }) and switching the UI content on the login page to prompt the user to set a new password.
Code implementation:
signIn(username: string, password: string): Promise<any> { const authenticationDetails = new AuthenticationDetails({ Username: username, Password: password });
const userData = { Username: username, Pool: this.userPool }; const cognitoUser = new CognitoUser(userData);
return new Promise((resolve, reject) => { cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) => { // Get Tokens const idToken = result.getIdToken().getJwtToken(); const accessToken = result.getAccessToken().getJwtToken(); const refreshToken = result.getRefreshToken().getToken();
// console.log('idToken', idToken); // console.log('accessToken', accessToken); // console.log('refreshToken', refreshToken);
// Store the idToken or accessToken in sessionStorage as userToken sessionStorage.setItem('userToken', accessToken);
// Save tokens or use them where needed resolve({ idToken, accessToken, refreshToken });
// Redirect to the home page after successful login this.router.navigate(['/']); }, onFailure: (err) => { reject(err.message || JSON.stringify(err)); }, newPasswordRequired: (userAttributes, requiredAttributes) => { // Trigger new password requirement, prompt front-end to set a new password resolve({ newPasswordRequired: true, cognitoUser }); } }); }); }To set a new password, call the completeNewPassword method and use cognitoUser.completeNewPasswordChallenge to set the new password.
// Set new password method completeNewPassword(cognitoUser: CognitoUser, newPassword: string): Promise<any> { return new Promise((resolve, reject) => { cognitoUser.completeNewPasswordChallenge(newPassword, {}, { onSuccess: (session) => resolve(session), onFailure: (err) => reject(err.message || JSON.stringify(err)) }); }); }Sign Up
Registration is performed via cognitoUser.signUp; on success, the username and password are stored in Cognito. The page is redirected to the login page.
// Sign up method signUp(username: string, password: string, email: string): Promise<any> { return new Promise((resolve, reject) => { const attributeList : CognitoUserAttribute[] = []; attributeList.push(new CognitoUserAttribute({ Name: 'email', Value: email }));
this.userPool.signUp(username, password, attributeList, [], (err, result) => { if (err) { reject(err.message || JSON.stringify(err)); } else { resolve(result?.user); } }); }); }Sign Out
Sign out via cognitoUser.signOut, then remove the userToken from sessionStorage and navigate to login.
logout() { // Sign out this.userPool.getCurrentUser()?.signOut(); sessionStorage.removeItem('userToken'); this.router.navigate(['/login']); }Login Page
The login page is src/app/components/login/login.component.ts, using Cognito’s SDK to log in, and on success storing the idToken or accessToken in sessionStorage.
The page content is controlled by authMode, which can be the following:
- login: login page
- register: registration page
- forgotPassword: forgot password page
- confirmSignUp: verification page
- resetPassword: reset password page
Clicking the corresponding button calls the switchMode method of authService to switch authMode and reveal the corresponding content.
Page implementation:
switchMode(mode: 'login' | 'register' | 'forgotPassword' | 'confirmSignUp' | 'resetPassword') { this.authMode = mode; this.message = '';}
onSubmit() { if (this.authMode === 'login') { this.authService.signIn(this.username, this.password).then( (resp) => { if (resp.newPasswordRequired) { // First-time login requires password reset, show modal this.showNewPasswordModal = true; this.cognitoUser = resp.cognitoUser; } else { // Login successful this.message = 'Login successful!'; } }).catch(err => { this.message = `Login failed: ${err}`; }); } else if (this.authMode === 'register') { this.authService.signUp(this.username, this.password, this.email).then( () => { this.message = 'Registration successful! Please check your email and enter the verification code.'; this.authMode = 'confirmSignUp'; }, (err) => (this.message = `Registration failed: ${err}`) ); } else if (this.authMode === 'forgotPassword') { this.authService.forgotPassword(this.username).then( () => { this.message = 'Verification code has been sent. Please check your email and enter the code and new password.'; this.authMode = 'resetPassword'; }, (err) => (this.message = `Failed to send verification code: ${err}`) ); } else if (this.authMode === 'confirmSignUp') { this.authService.confirmSignUp(this.username, this.code).then( () => (this.message = 'Verification successful! Please log in.'), (err) => (this.message = `Verification failed: ${err}`) ); } else if (this.authMode === 'resetPassword') { this.authService.confirmPassword(this.username, this.code, this.newPassword).then( () => { this.message = 'Password reset successful! Please log in with the new password.'; this.authMode = 'login'; // Switch back to login page }, (err) => (this.message = `Password update failed: ${err}`) ); } }Animation Calendar Page
The animation calendar page is src/app/components/home/home.component.ts, which fetches data via bgm.service.getCalendar and displays it.
On page initialization, the ngOnInit method is invoked to fetch and display data.
ngOnInit() : void { // this.bgmService.getCalendar().subscribe(data => console.log(data)); // this.bgmService.getSubject('482850').subscribe(data => console.log(data)); this.bgmService.getCalendar().subscribe((data:any[]) => { this.weeklyData = Array(7).fill(null).map((_, index) => ({ day: this.daysOfWeek[index], items: data .find((d: any) => d.weekday.id === index + 1) ?.items.filter((item: any) => item.collection?.doing >= 100) || [] })); }); }
navigateToItem(id: string) { this.router.navigate(['/items', id]); }
// Show modal and load data openModal(itemId: string): void { this.bgmService.getSubject(itemId).subscribe((data) => { this.selectedItem = data; this.showModal = true; }); } // Close modal closeModal(): void { this.showModal = false; this.selectedItem = null; }
// Helper: find the official website URL in the infobox getOfficialWebsite(): string | null { if (!this.selectedItem || !this.selectedItem.infobox) return null; const website = this.selectedItem.infobox.find((info: any) => info.key === '官方网站'); return website ? website.value : null; }Search Page
The search page is src/app/components/search/search.component.ts, which fetches data via bgm.service.search and displays it.
The page uses the title to receive the search keyword and an options object to control the content shown. Options include:
- limit: items per page
- type: category
- meta_tags: meta tags
- tag: tags
- air_date: air date
- rating: rating
- rank: ranking
- nsfw: include adult content
- page: page number
When sending a request to the API, the title and options are restructured and sent. The request restructuring is as follows:
// Search method onSearch(): void { if (this.searchQuery.trim()) { this.isLoading = true; this.errorMessage = '';
// Configure search options const options = { limit: this.limit, type: this.type, meta_tags: this.meta_tags, tag: this.tag, air_date: this.air_date, rating: this.rating, rank: this.rank, nsfw: this.nsfw, page: this.page };
this.bgmService.searchSubject(this.searchQuery, options).subscribe( (response: any) => { this.searchResults = response.data; // extract data array this.totalResults = response.total; // extract total this.isLoading = false; }, (error) => { this.errorMessage = 'Search failed, please try again.'; this.isLoading = false; } ); } }Interceptor: Add Authentication Information
In src/app/interceptors/auth.interceptor.ts, an interceptor is used to attach authentication information to requests.
Interceptor implementation:
export const authInterceptor: HttpInterceptorFn = (req, next) => { const authToken = environment.bgm.authToken;
if (req.url.startsWith('<https://api.bgm.tv/v0>')) { # If the request URL starts with https://api.bgm.tv/v0, add auth info const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${authToken}`, # Add auth information } }); return next(authReq); } return next(req);};When using the interceptor, configure it in app.config.ts.
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor]) ) ]};Guard: Protect Pages
In src/app/guards/auth.guard.ts, guards protect pages that require login; if not logged in, redirect to the login page.
Guard implementation:
export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router);
if (authService.isLoggedIn()) { return true; } else { // Not logged in, redirect to /login router.navigate(['/login']); return false; }};When using the guard, apply it in the route configuration.
{ path: '', component: HomeComponent, canActivate: [authGuard] }, # Home page requires loginProject Startup
-
Start the project
ng serve -
Access URL
Access URL: http://localhost:4200/
Project Deployment
-
Local build
ng build -
Automated deployment
This project uses GitHub Actions to automatically deploy to GitHub Pages. Every time code is pushed to GitHub, GitHub Actions will detect the push event, automatically build the project, and deploy to GitHub Pages.
Create the configuration file
.github/workflows/main.ymlto configure GitHub Actions automatic deployment of the project.The content is as follows:
# GitHub Actions workflow to deploy the project to GitHub Pagesname: Deploy to GitHub Pages
# Trigger: when pushing to the master branchon: push: branches: - master # Or the branch you want to monitor
jobs: build-and-deploy: # Use the latest Ubuntu as the runtime runs-on: ubuntu-latest steps: # Step 1: Check out the code - name: Checkout code uses: actions/checkout@v3
# Step 2: Set up Node.js environment - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '20' # Modify Node.js version as needed for the project
# Step 3: Install project dependencies - name: Install dependencies run: npm install
# Step 4: Generate environment.ts - name: Generate environment.ts run: | # Create the src/app/environment directory (if it doesn't exist) mkdir -p src/app/environment
# Generate environment.ts with Cognito and Bangumi API configuration echo "export const environment = { production: true, cognito: { userPoolId: '$COGNITO_USER_POOL_ID', clientId: '$COGNITO_CLIENT_ID', domain: '$COGNITO_DOMAIN' }, bgm: { url: '<https://api.bgm.tv/v0>', authToken: '$BGM_AUTH_TOKEN', userAgent: 'dreaife/my-angular-project-test' } };" > src/app/environment/environment.ts # Generate environment configuration file
# List the generated files to confirm ls src/app/environment
# Step 5: Build the project - name: Build project run: npm run build -- --configuration production --base-href "/my-angular-project-test/" # Build the project
# Step 6: Deploy to GitHub Pages - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 with: # folder is the built output folder, containing index.html folder: dist/my-angular-project/browser # Fill in with the actual output path token: ${{ secrets.TOKEN }}
# Environment variable configuration, store sensitive information using GitHub Secrets env: COGNITO_USER_POOL_ID: ${{ secrets.COGNITO_USER_POOL_ID }} COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }} COGNITO_DOMAIN: ${{ secrets.COGNITO_DOMAIN }} BGM_AUTH_TOKEN: ${{ secrets.BGM_AUTH_TOKEN }} GITHUB_TOKEN: ${{ secrets.TOKEN }}
プロジェクト紹介
本プロジェクトは私の Angular 練習用のプロジェクトで、Angular をベースとした Web アプリケーションです。Bangumi に掲載されているアニメを表示・検索するためのもので、API は Bangumi API から提供されています。
本プロジェクトは GitHub Actions を使用して、GitHub Pages に自動デプロイします。
プロジェクト名
my-angular-project-test
プロジェクトの目的
- Angular をベースにした静的サイトをデプロイする
- GitHub Actions の自動デプロイを練習する
- API 呼び出しで機能を実現する
- Cognito を使用してユーザー認証を行う
- インターセプターを使用してリクエストを処理する
- ガードを使用してページを保護する
プロジェクト技術スタック
- Angular 16
- TypeScript
- HTML
- CSS
- GitHub Actions
- Cognito
環境準備
環境要件
- Node.js バージョン 20 以上
- Angular CLI
インストール手順
-
Node.js をインストール
<https://nodejs.org/en/download/> -
Angular CLI をインストール
npm install -g @angular/cli -
プロジェクトを取得
git clone <https://github.com/dreaife/my-angular-project-test.git>cd my-angular-project-testnpm install
プロジェクト構成
ディレクトリ構成
本プロジェクトは Angular CLI を使用して作成され、構造は以下のとおりです:
my-angular-project-test/├── src/│ ├── app/│ │ ├── environment/│ │ │ ├── environment.ts│ │ ├── components/│ │ │ ├── login/│ │ │ ├── home/│ │ │ ├── search/│ │ ├── guards/│ │ │ ├── auth.guard.ts│ │ ├── interceptors/│ │ │ ├── auth.interceptor.ts│ │ ├── services/│ │ │ ├── auth.service.ts│ │ │ ├── bgm.service.ts│ │ ├── app.component.ts│ ├── index.html│ ├── main.ts├── ...以下の通りに:
src/appディレクトリはプロジェクトの主要ディレクトリで、すべてのコンポーネント、サービス、インターセプター、ガード等を含みます。src/environmentsディレクトリは環境設定ファイルで、開発環境と本番環境の設定を含みます。src/componentsディレクトリはプロジェクトの主要コンポーネントで、すべてのページコンポーネントを含みます。loginコンポーネントはログインページで、Cognito の SDK を使用してログインします。homeコンポーネントはアニメーションカレンダーのページで、bgm.service.getCalendarを呼び出してデータを取得・表示します。searchコンポーネントは検索ページで、bgm.service.searchを呼び出してデータを取得・表示します。
src/guardsディレクトリはプロジェクトの主要なガードを含み、auth.guard.tsガードを含み、ログインが必要なページを保護します。未ログインの場合はログインページへリダイレクトします。src/interceptorsディレクトリはプロジェクトの主要なインターセプターを含み、auth.interceptor.tsインターセプターを含み、リクエストに認証情報を追加します。src/servicesディレクトリはプロジェクトの主要なサービスで、auth.service.tsサービスはログイン・ログアウトなどの処理を担当します。bgm.service.tsサービスは Bangumi API を呼び出すためのものです。src/main.tsはプロジェクトのメインエントリーファイルで、Angular アプリケーションを起動するためのものです。
关键功能实现
使用Cognito进行用户认证
Cognito を使用したユーザー認証
src/app/services/auth.service.ts で、Cognito の SDK を使用してユーザー認証を行います。
Cognito を使用する前に、AWS Cognito でユーザプールを作成し、カスタム Cognito バリデーションドメインを設定し、アプリクライアントを作成してクライアントIDを取得する必要があります。
取得した ID を src/app/environment/environment.ts に配置して、Cognito の設定情報を行います。
ログイン
ログイン
signIn(username: string, password: string): Promise<any> { const authenticationDetails = new AuthenticationDetails({ Username: username, Password: password });
const userData = { Username: username, Pool: this.userPool }; const cognitoUser = new CognitoUser(userData);
return new Promise((resolve, reject) => { cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) => { // 获取 Tokens const idToken = result.getIdToken().getJwtToken(); const accessToken = result.getAccessToken().getJwtToken(); const refreshToken = result.getRefreshToken().getToken();
// console.log('idToken', idToken); // console.log('accessToken', accessToken); // console.log('refreshToken', refreshToken);
// 将 idToken 或 accessToken 存储到 sessionStorage 作为 userToken sessionStorage.setItem('userToken', accessToken);
// 保存 Tokens 或在需要的地方使用 resolve({ idToken, accessToken, refreshToken });
// 登录成功后重定向到主页 this.router.navigate(['/']); }, onFailure: (err) => { reject(err.message || JSON.stringify(err)); }, newPasswordRequired: (userAttributes, requiredAttributes) => { // 触发新密码需求,提示前端进行新密码设置 resolve({ newPasswordRequired: true, cognitoUser }); } }); }); }用户设置新密码时,调用completeNewPassword方法,通过cognitoUser.completeNewPasswordChallenge方法设置新密码。
// 设置新密码方法 completeNewPassword(cognitoUser: CognitoUser, newPassword: string): Promise<any> { return new Promise((resolve, reject) => { cognitoUser.completeNewPasswordChallenge(newPassword, {}, { onSuccess: (session) => resolve(session), onFailure: (err) => reject(err.message || JSON.stringify(err)) }); }); }注册
通过cognitoUser.signUp方法进行注册,成功后将用户名和密码存储到cognito中。将页面重定向到登录页面。
// 注册方法 signUp(username: string, password: string, email: string): Promise<any> { return new Promise((resolve, reject) => { const attributeList : CognitoUserAttribute[] = []; attributeList.push(new CognitoUserAttribute({ Name: 'email', Value: email }));
this.userPool.signUp(username, password, attributeList, [], (err, result) => { if (err) { reject(err.message || JSON.stringify(err)); } else { resolve(result?.user); } }); }); }登出
通过cognitoUser.signOut方法进行登出,登出后删除sessionStorage中的userToken。
logout() { // 登出 this.userPool.getCurrentUser()?.signOut(); sessionStorage.removeItem('userToken'); this.router.navigate(['/login']); }ログインページ
ログインページは src/app/components/login/login.component.ts で、Cognito の SDK を使用してログインを実行し、成功時には idToken または accessToken を sessionStorage に保存します。
ページは authMode で表示内容を制御します。authMode には以下の種類があります:
- login:ログインページ
- register:登録ページ
- forgotPassword:パスワードを忘れたページ
- confirmSignUp:確認ページ
- resetPassword:パスワードをリセットするページ
対応するボタンをクリックすると、authService の switchMode メソッドを呼び出して authMode を切り替え、表示内容を切り替えます。
ページの実装:
switchMode(mode: 'login' | 'register' | 'forgotPassword' | 'confirmSignUp' | 'resetPassword') { this.authMode = mode; this.message = '';}
onSubmit() { if (this.authMode === 'login') { this.authService.signIn(this.username, this.password).then( (resp) => { if (resp.newPasswordRequired) { // 初回ログインでパスワード変更が必要、モーダルを表示 this.showNewPasswordModal = true; this.cognitoUser = resp.cognitoUser; } else { // ログイン成功 this.message = 'ログイン成功!'; } }).catch(err => { this.message = `ログイン失敗:${err}`; }); } else if (this.authMode === 'register') { this.authService.signUp(this.username, this.password, this.email).then( () => { this.message = '登録に成功しました!メールを確認して認証コードを入力してください。'; this.authMode = 'confirmSignUp'; }, (err) => (this.message = `登録に失敗しました:${err}`) ); } else if (this.authMode === 'forgotPassword') { this.authService.forgotPassword(this.username).then( () => { this.message = '認証コードを送信しました。メールを確認して認証コードと新しいパスワードを入力してください。'; this.authMode = 'resetPassword'; }, (err) => (this.message = `認証コードの送信に失敗しました:${err}`) ); } else if (this.authMode === 'confirmSignUp') { this.authService.confirmSignUp(this.username, this.code).then( () => (this.message = '認証成功! ログインしてください。'), (err) => (this.message = `認証に失敗しました:${err}`) ); } else if (this.authMode === 'resetPassword') { this.authService.confirmPassword(this.username, this.code, this.newPassword).then( () => { this.message = 'パスワードのリセットに成功しました! 新しいパスワードでログインしてください。'; this.authMode = 'login'; // ログインページに切り替え }, (err) => (this.message = `パスワードの更新に失敗しました:${err}`) ); } }アニメーションカレンダーページ
アニメーションカレンダーページは src/app/components/home/home.component.ts で、bgm.service.getCalendar を呼び出してデータを取得・表示します。
ページが初期化されると、ngOnInit メソッドを呼び出してデータを取得・表示します。
ngOnInit() : void { // this.bgmService.getCalendar().subscribe(data => console.log(data)); // this.bgmService.getSubject('482850').subscribe(data => console.log(data)); this.bgmService.getCalendar().subscribe((data:any[]) => { this.weeklyData = Array(7).fill(null).map((_, index) => ({ day: this.daysOfWeek[index], items: data .find((d: any) => d.weekday.id === index + 1) ?.items.filter((item: any) => item.collection?.doing >= 100) || [] })); }); }
navigateToItem(id: string) { this.router.navigate(['/items', id]); }
// 显示浮窗并加载数据 openModal(itemId: string): void { this.bgmService.getSubject(itemId).subscribe((data) => { this.selectedItem = data; this.showModal = true; }); } // 关闭浮窗 closeModal(): void { this.showModal = false; this.selectedItem = null; }
// 辅助方法:查找 infobox 中的官方网站 URL getOfficialWebsite(): string | null { if (!this.selectedItem || !this.selectedItem.infobox) return null; const website = this.selectedItem.infobox.find((info: any) => info.key === '官方网站'); return website ? website.value : null; }検索ページ
検索ページは src/app/components/search/search.component.ts で、bgm.service.search を呼び出してデータを取得・表示します。
ページは title で検索キーワードを受け取り、options で表示内容を制御します。options には次の種類があります:
- limit:1 ページあたりの表示件数
- type:タイプ
- meta_tags:メタタグ
- tag:タグ
- air_date:放送日
- rating:評価
- rank:ランク
- nsfw:アダルト内容を含むか
- page:ページ番号
API へリクエストを送る際、title と options を組み直して送信します。リクエストの再構築は以下のとおりです:
// 検索方法 onSearch(): void { if (this.searchQuery.trim()) { this.isLoading = true; this.errorMessage = '';
// 検索オプションを設定 const options = { limit: this.limit, type: this.type, meta_tags: this.meta_tags, tag: this.tag, air_date: this.air_date, rating: this.rating, rank: this.rank, nsfw: this.nsfw, page: this.page };
this.bgmService.searchSubject(this.searchQuery, options).subscribe( (response: any) => { this.searchResults = response.data; // data 配列を抽出 this.totalResults = response.total; // 総数を抽出 this.isLoading = false; }, (error) => { this.errorMessage = '検索に失敗しました。再試行してください。'; this.isLoading = false; } ); } }インターセプターによる認証情報の付与
src/app/interceptors/auth.interceptor.ts では、インターセプターを介してリクエストに認証情報を追加します。
インターセプターの実装:
export const authInterceptor: HttpInterceptorFn = (req, next) => { const authToken = environment.bgm.authToken;
if (req.url.startsWith('<https://api.bgm.tv/v0>')) { # もしリクエストURLがhttps://api.bgm.tv/v0で始まる場合、認証情報を追加 const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${authToken}`, # 認証情報を追加 } }); return next(authReq); } return next(req);};インターセプターを使用する際には app.config.ts でインターセプターを設定する必要があります。
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor]) ) ]};ガードによるページ保護
src/app/guards/auth.guard.ts で、ログインが必要なページを保護します。未ログインの場合はログインページへリダイレクトします。
ガードの実装:
export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router);
if (authService.isLoggedIn()) { return true; } else { // 未ログインの場合、/loginへリダイレクト router.navigate(['/login']); return false; }};ルーティング設定でガードを使用します。
{ path: '', component: HomeComponent, canActivate: [authGuard] }, # 主页需要登录プロジェクト起動
-
プロジェクトを起動
ng serve -
アクセス先
アクセス先:http://localhost:4200/
プロジェクトデプロイ
-
ローカルでプロジェクトをビルド
ng build -
自動デプロイ
本プロジェクトは GitHub Actions を使用して GitHub Pages に自動デプロイします。コードを GitHub にプッシュするたび、GitHub Actions がプッシュイベントを検出し、自動的にプロジェクトをビルドして GitHub Pages にデプロイします。
設定ファイル
.github/workflows/main.ymlを作成して、GitHub Actions による自動デプロイを設定します。内容は以下のとおりです:
# GitHub Actions ワークフロー,用途: プロジェクトを GitHub Pages にデプロイname: Deploy to GitHub Pages
# トリガー条件: master ブランチへプッシュされたときon: push: branches: - master # または監視するブランチ名
jobs: build-and-deploy: # 最新の Ubuntu を実行環境として使用 runs-on: ubuntu-latest steps: # 第1歩:コードをチェックアウト - name: Checkout code uses: actions/checkout@v3
# 第2歩:Node.js 環境を設定 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '20' # プロジェクトの要件に応じて Node.js のバージョンを変更
# 第3歩:プロジェクトの依存をインストール - name: Install dependencies run: npm install
# 第4歩:環境設定ファイル environment.ts を生成 - name: Generate environment.ts run: | # src/app/environment ディレクトリを作成(存在しない場合) mkdir -p src/app/environment
# environment.ts ファイルを生成し、Cognito と Bangumi API の設定情報を含める echo "export const environment = { production: true, cognito: { userPoolId: '$COGNITO_USER_POOL_ID', clientId: '$COGNITO_CLIENT_ID', domain: '$COGNITO_DOMAIN' }, bgm: { url: '<https://api.bgm.tv/v0>', authToken: '$BGM_AUTH_TOKEN', userAgent: 'dreaife/my-angular-project-test' } };" > src/app/environment/environment.ts # 環境設定ファイルを生成
# 生成したファイルを列挙して確認 ls src/app/environment
# 第5歩:プロジェクトをビルド - name: Build project run: npm run build -- --configuration production --base-href "/my-angular-project-test/" # プロジェクトをビルド
# 第6歩:GitHub Pages にデプロイ - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 with: # browser はビルド出力のフォルダ、内部ファイルには index.html が含まれる folder: dist/my-angular-project/browser # 実際の出力パスに従ってください token: ${{ secrets.TOKEN }}
# 環境変数の設定、機密情報は GitHub Secrets に保存します
env: COGNITO_USER_POOL_ID: ${{ secrets.COGNITO_USER_POOL_ID }} COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }} COGNITO_DOMAIN: ${{ secrets.COGNITO_DOMAIN }} BGM_AUTH_TOKEN: ${{ secrets.BGM_AUTH_TOKEN }} GITHUB_TOKEN: ${{ secrets.TOKEN }}部分信息可能已经过时









