在本博客中,我们将引导您完成使用 Angular 作为前端并使用 Tailwind CSS 进行样式创建 URL 缩短应用程序的过程。 URL 缩短器是一个方便的工具,可以将长 URL 转换为更短、更易于管理的链接。该项目将帮助您了解如何使用现代 Web 开发技术构建功能齐全且美观的 Web 应用程序。
要学习本教程,您应该对 Angular 有基本的了解,并对 Tailwind CSS 有一定的了解。确保您的计算机上安装了 Node.js 和 Angular CLI。
首先,通过在终端中运行以下命令来创建一个新的 Angular 项目:
ng new url-shortener-app cd url-shortener-app
接下来,在您的 Angular 项目中设置 Tailwind CSS。通过 npm 安装 Tailwind CSS 及其依赖项:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
通过更新 tailwind.config.js 文件来配置 Tailwind CSS:
module.exports = { content: [ "./src/**/*.{html,ts}", ], theme: { extend: {}, }, plugins: [], }
将 Tailwind 指令添加到您的 src/styles.scss 文件中:
@tailwind base; @tailwind components; @tailwind utilities;
创建 URL 模型来定义 URL 数据的结构。添加新文件 src/app/models/url.model.ts:
export type Urls = Url[]; export interface Url { _id: string; originalUrl: string; shortUrl: string; clicks: number; expirationDate: string; createdAt: string; __v: number; }
创建一个服务来处理与 URL 缩短相关的 API 调用。添加新文件 src/app/services/url.service.ts:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Url, Urls } from '../models/url.model'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UrlService { private apiUrl = environment.apiUrl; constructor(private http: HttpClient) {} shortenUrl(originalUrl: string): Observable<Url> { return this.http.post<Url>(`${this.apiUrl}/shorten`, { originalUrl }); } getAllUrls(): Observable<Urls> { return this.http.get<Urls>(`${this.apiUrl}/urls`); } getDetails(id: string): Observable<Url> { return this.http.get<Url>(`${this.apiUrl}/details/${id}`); } deleteUrl(id: string): Observable<Url> { return this.http.delete<Url>(`${this.apiUrl}/delete/${id}`); } }
生成一个用于缩短 URL 的新组件:
ng generate component shorten
将组件的 HTML (src/app/shorten/shorten.component.html) 更新为如下所示:
<div class="max-w-md mx-auto p-4 shadow-lg rounded-lg mt-4"> <h2 class="text-2xl font-bold mb-2">URL Shortener</h2> <form [formGroup]="urlForm" (ngSubmit)="shortenUrl()"> <div class="flex items-center mb-2"> <input class="flex-1 p-2 border border-gray-300 rounded mr-4" formControlName="originalUrl" placeholder="Enter your URL" required /> <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" type="submit"> Shorten </button> </div> @if (urlForm.get('originalUrl')?.invalid && (urlForm.get('originalUrl')?.dirty || urlForm.get('originalUrl')?.touched)) { <div class="text-red-500" role="alert" aria-live="assertive"> @if (urlForm.get('originalUrl')?.errors?.['required']) { URL is required. } @if (urlForm.get('originalUrl')?.errors?.['pattern']) { Invalid URL format. Please enter a valid URL starting with http:// or https://. } </div> } </form> @if (errorMsg) { <div class="p-4 bg-red-100 rounded mt-4"> <p class="text-red-500">{{ errorMsg }}</p> </div> } @if (shortUrl) { <div class="p-4 bg-green-100 rounded"> <p>Shortened URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + shortUrl" target="_blank">{{ shortUrl }}</a> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 border border-slate-950 rounded hover:bg-gray-300" (click)="copyUrl(redirectUrl + shortUrl)">Copy</button> @if (copyMessage) { <span class="text-green ml-2">{{ copyMessage }}</span> } </p> </div> } </div> <div class="max-w-md mx-auto mt-4 p-2"> <h2 class="text-2xl font-bold mb-4">All URLs</h2> @if (isloading) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> Loading... </div> </div> } @else if (error) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> <p class="text-red-500">{{ error }}</p> </div> </div> } @else { @if (urls.length > 0 && !isloading && !error) { <ul> @for (url of urls; track $index) { <li class="p-2 border border-gray-300 rounded mb-2"> <div class="flex justify-between items-center"> <div> URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + url.shortUrl" target="_blank">{{ url.shortUrl }}</a> </div> <div class="flex justify-between items-center"> <button class="px-2 py-1 bg-blue-200 text-blue-800 rounded hover:bg-blue-300" (click)="showDetails(url.shortUrl)">Details</button> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" (click)="copyListUrl(redirectUrl + url.shortUrl, $index)">{{ copyIndex === $index ? 'Copied' : 'Copy' }}</button> <button class="ml-2 px-2 py-1 bg-red-200 text-red-800 rounded hover:bg-red-300" (click)="prepareDelete(url.shortUrl)">Delete</button> </div> </div> </li> } </ul> } @else { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> No URLs found. </div> </div> } } </div> @if (showDeleteModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">Confirm Deletion</h3> <p class="mb-4">Are you sure you want to delete this URL?</p> <button class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" (click)="confirmDelete()">Yes, Delete</button> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400 ml-2" (click)="showDeleteModal = false">Cancel</button> </div> </div> } @if (showDetailsModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">URL Details</h3> @if (isLoading) { <p class="mb-4">Loading...</p> } @else { <p class="mb-4">Short URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + selectedUrl.shortUrl" target="_blank">{{ selectedUrl.shortUrl }}</a></p> <p class="mb-4">Original URL: <a class="text-blue-500 hover:text-blue-600" [href]="selectedUrl.originalUrl" target="_blank">{{ selectedUrl.originalUrl }}</a></p> <p class="mb-4">Clicks: <span class="text-green-500">{{ selectedUrl.clicks }}</span></p> <p class="mb-4">Created At: {{ selectedUrl.createdAt | date: 'medium' }}</p> <p class="mb-4">Expires At: {{ selectedUrl.expirationDate | date: 'medium' }}</p> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400" (click)="showDetailsModal = false">Close</button> } </div> </div> }
更新组件的 TypeScript 文件 (src/app/shorten/shorten.component.ts) 以处理表单提交和 API 交互:
import { Component, inject, OnInit } from '@angular/core'; import { UrlService } from '../services/url.service'; import { FormControl, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Url } from '../models/url.model'; import { environment } from '../../environments/environment'; import { DatePipe } from '@angular/common'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-shorten', standalone: true, imports: [DatePipe, ReactiveFormsModule], templateUrl: './shorten.component.html', styleUrl: './shorten.component.scss', }) export class ShortenComponent implements OnInit { shortUrl: string = ''; redirectUrl = environment.apiUrl + '/'; copyMessage: string = ''; copyListMessage: string = ''; urls: Url[] = []; showDeleteModal = false; showDetailsModal = false; urlToDelete = ''; copyIndex: number = -1; selectedUrl: Url = {} as Url; isLoading = false; isloading = false; error: string = ''; errorMsg: string = ''; urlForm: FormGroup = new FormGroup({}); private unsubscribe$: Subject<void> = new Subject<void>(); urlService = inject(UrlService); ngOnInit() { this.urlForm = new FormGroup({ originalUrl: new FormControl('', [ Validators.required, Validators.pattern('^(http|https)://.*$'), ]), }); this.getAllUrls(); } shortenUrl() { if (this.urlForm.valid) { this.urlService.shortenUrl(this.urlForm.value.originalUrl).pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('Shortened URL: ', response); this.shortUrl = response.shortUrl; this.getAllUrls(); }, error: (error) => { console.error('Error shortening URL: ', error); this.errorMsg = error?.error?.message || 'An error occurred!'; }, }); } } getAllUrls() { this.isloading = true; this.urlService.getAllUrls().pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('All URLs: ', response); this.urls = response; this.isloading = false; }, error: (error) => { console.error('Error getting all URLs: ', error); this.isloading = false; this.error = error?.error?.message || 'An error occurred!'; }, }); } showDetails(id: string) { this.showDetailsModal = true; this.getDetails(id); } getDetails(id: string) { this.isLoading = true; this.urlService.getDetails(id).subscribe({ next: (response) => { // console.log('URL Details: ', response); this.selectedUrl = response; this.isLoading = false; }, error: (error) => { console.error('Error getting URL details: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } copyUrl(url: string) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyMessage = 'Copied!'; setTimeout(() => { this.copyMessage = ''; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyMessage = 'Failed to copy URL'; }); } copyListUrl(url: string, index: number) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyListMessage = 'Copied!'; this.copyIndex = index; setTimeout(() => { this.copyListMessage = ''; this.copyIndex = -1; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyListMessage = 'Failed to copy URL'; }); } prepareDelete(url: string) { this.urlToDelete = url; this.showDeleteModal = true; } confirmDelete() { // Close the modal this.showDeleteModal = false; // Delete the URL this.deleteUrl(this.urlToDelete); } deleteUrl(id: string) { this.urlService.deleteUrl(id).subscribe({ next: (response) => { // console.log('Deleted URL: ', response); this.getAllUrls(); }, error: (error) => { console.error('Error deleting URL: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } }
<router-outlet></router-outlet>
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ], };
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./shorten/shorten.component').then((m) => m.ShortenComponent), }, ];
您已经使用 Angular 和 Tailwind CSS 成功构建了 URL 缩短器应用程序。该项目演示了如何集成现代前端技术来创建功能强大且时尚的 Web 应用程序。借助 Angular 的强大功能和 Tailwind CSS 实用优先的方法,您可以轻松构建响应灵敏且高效的 Web 应用程序。
请随意通过添加用户身份验证等功能来扩展此应用程序。祝您编码愉快!
访问 GitHub 存储库以详细探索代码。
以上是使用 Angular 和 Tailwind CSS 构建 URL 缩短应用程序的详细内容。更多信息请关注PHP中文网其他相关文章!