Angular7 + Koa2 开发个人博客

前言

Angular7 + Koa2 开发个人博客,跟 vue2 一样,大体结构差不多,只是 Angular7 的代码编写方式有所不同,不同归不同,万变不离其宗。这篇文章不会细读 Angular,只是抛砖引玉,把在开发中的主要代码分享出来。

前端 Angular7

简单的准备

这里从安装开始,安装 CLI 脚手架

npm install -g @angular/cli

通过 ng new 命令初始化项目

ng new angular-blog

启用项目

cd 切换到 angular-blog  目录,运行下面的命令,即可启动项目

ng serve --open

了基本的准备已经完成,下面我们来开始讲前端 Angular 项目,毕竟这是一个博客涉及到数据请求,而在开发阶段我们不可能为了看效果第次都得打个包,我们直接使用脚手架给我们搭好的微服务(http://localhost/4200)来启动项目。此时我们本地的后端服务 Koa2  一般也会使用 http://localhost/8080 来进行访问,端口号看你自己怎么定了。所以这里我们首选要解决的就是跨说域问题。

在 Angular 项目根目录下新建一个 proxy.conf.json 文件内容为:

{
  "/api/*": {
    "target": "http://localhost:3432/",
    "secure": "false"
  }
}

接下来我们重启项目,并使用这个配置文件的配置信息

ng serve --open --proxy-config ./proxy.conf.json

这样前端就可以正常访问后端服务了。

下面是前端主要的目录结构

src
   ├──
   │  app
   │     ├── app-routing.module.ts
   │     ├── app.component.html
   │     ├── app.component.less 
   │     ├── app.component.ts 
   │     ├── app.module.ts 
   │     ├── components // 公用组件
   │     │    └── pagination // 分页
   │     │       ├── pagination.component.html
   │     │       ├── pagination.component.less
   │     │       └── pagination.component.ts
   │     ├── home // 主页
   │     │  ├── home.component.html
   │     │  ├── home.component.less
   │     │  └── home.component.ts
   │     ├── login // 登录页
   │     │  ├── login.component.html
   │     │  ├── login.component.less
   │     │  └── login.component.ts
   │     │
   │     .....
   │     .....  // 还有很多页面
   │     .....
   │     │
   │     ├── lib // 插件库
   │     │  └── home // 编辑器
   │     │      ├── index.css
   │     │      └── index.js
   │     ├── services // 服务
   │     │  ├── post.service.ts
   │     │  ├── category.service.ts
   │     │  ├── book.service.ts
   │     │  └── user.service.ts    
   │     ├── shared // 共享目录
   │     │  ├── directives // 指认
   │     │  ├── 
   │     │  └── services   // 服务
   │     │     └── interceptor.service.ts
   │     ├── store // 状态管理
   │     │  ├── actions
   │     │  │  └── user.action.ts
   │     │  ├── reducers
   │     │  │  └── user.reducer.ts
   │     │  └── states 
   │     │     └── user.state.ts
   │     ├── utils // 工具库
   │     │  └── localStorage.ts // 本地红缓存
   │     ├── App.vue
   │     └── main.js
   ├── index.html
   ├── assets // 存放静态资源
   │  └── less // 样式文件
   │    ├── variable.less // 变量
   │    ├── reset.less    // 重置
   │    ├── base.less     // 基本样式
   │    ......
   │    └── plugins.less  // 插件样式
   └── styles.less // 这个是全局样式表

结构大体就是上面这样子,下面我们来看看相关的页面代码

src/index.html

<!doctype html>
<html lang="en" style="height:100%">
<head>
  <meta charset="utf-8">
  <title>AngularBlog</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body style="height:100%">
  <app-root></app-root>
</body>
</html>

index.html 文件代码没多大改动,只是添加了一个行间样式。

src/style.less

/* You can add global styles to this file, and also import other style files */
@import 'assets/less/variable.less';
@import 'assets/less/reset.less';
@import 'assets/less/base.less';
@import 'assets/less/makeup.less';
@import 'assets/less/constructor.less';
@import 'assets/less/plugins.less';

在 style.less 中引入共用的样式文件。供全局使用。

src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store'
import { UserReducer } from './store/reducers/user.reducer'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
import { FormsModule } from '@angular/forms'
import { HomeComponent } from './home/home.component'
import { LoginComponent } from './login/login.component'
import { MemberComponent } from './member/member.component'
import { AboutComponent } from './about/about.component'
import { PostViewComponent } from './post/post-view.component'
import { PostEditComponent } from './post/post-edit.component';
import { BookComponent } from './book/book.component'
import { BookViewComponent } from './book/book-view.component'
import { InterceptorService } from './shared/services/interceptor.service'
import { BookService } from '../app/services/book.service'
import { CategoryService } from '../app/services/category.service'
import { PostService } from '../app/services/post.service'
import { UserService } from './services/user.service'
import { FilesService } from './services/files.service'
import { PaginationComponent } from './components/pagination/pagination.component'
import { HighlightDirective } from './shared/directives/highlight.directive';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    LoginComponent,
    MemberComponent,
    AboutComponent,
    PostViewComponent,
    PostEditComponent,
    BookComponent,
    BookViewComponent,
    PaginationComponent,
    HighlightDirective
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ user: UserReducer }),
    AppRoutingModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true },
    BookService,
    PostService,
    UserService,
    CategoryService,
    FilesService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

src/app/app.component.html

<div class="container is-compact has-category">
  <div class="header">
    <div class="header-main fx-cell">
      <div class="fx-cell fx-ai-ct fx-cell-bd">
        <ul class="fx-cell offset-right-4">
          <li class="hand" routerLinkActive="menu-active" [routerLinkActiveOptions]="{exact:true}" [routerLink]="['/']" (click)="fetchBooks()">首页</li>
          <li class="hand" routerLinkActive="menu-active" [routerLink]="['/about']">关于</li>
          <li class="hand" routerLinkActive="menu-active" [routerLink]="['/member']">会员通道</li>
        </ul>
        <div></div>
      </div>
      <div class="fx-cell fx-ai-ct">
        <span class="hand offset-right-4" *ngIf="loginUser.id">欢迎:<span [routerLink]="['/people']">{{ loginUser.name }}</span></span>
        <a *ngIf="!loginUser.id" class="hand offset-right-4 text-white" [routerLink]="['/login']">登陆</a>
        <a *ngIf="loginUser.id"class="hand offset-right-4 text-white" (click)="loginOut()">登出</a>
      </div>
    </div>
    <div class="fx-box pannel-shadow category">
      <div class="pannel-category">
        <ul>
          <li *ngFor="let item of book">
            <a (click)="goGook(item.id)" class="home-header-nav-link hand">{{item.name}}</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
  <main class="main">
    <div class="main-area">
      <router-outlet></router-outlet>
    </div>
  </main>
</div>

src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { BookService, Book } from '../app/services/book.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from "@angular/router"
import { Store } from '@ngrx/store';
import { Observable, Subscription } from 'rxjs';
import { LocalStorage } from './utils/localStorage';
import { UserAction } from './store/actions/user.action';
import { User } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})

export class AppComponent implements OnInit {
  loginUserState$: Observable<User>;
  loginUser:User
  private loginUserStateSubscription: Subscription;
  constructor(private bookService: BookService, private router: Router, private store: Store<User>) {
    this.loginUserState$ = store.select('user');
   }
  book: Book[];
  title = 'angular-blog';
  ngOnInit(): void {
    this.fetchBooks()
    const localUser = LocalStorage.getItem('user');
    localUser && this.userAction (localUser) // 如果缓存中存在用户则更新 loginUser 状态
    this.loginUserStateSubscription = this.loginUserState$.subscribe((state) => { 
      // 订阅 loginUserState$ 只要用户信息发生变量,更新 loginUser 对象
      this.loginUser = state
    });
  }
  fetchBooks() {
    this.bookService.fetchBooks().subscribe(response => {
      if (response && response.code === 1) {
        this.book = response.data
      }
    }, (error: HttpErrorResponse) => {
      console.log(error);
      if (error instanceof Error) { // 普通错误
        console.log(`front-end error,${error.error}`)
      } else {
        console.log(`back-end error,${error.error}`)
      }
    })
  }
  loginOut(){ // 登出
    this.userAction ({})
    LocalStorage.removeItem('user');
    LocalStorage.removeItem('token');
    this.router.navigate(['/'])
  }
  goGook (id){
    this.router.navigate(['book'],{
      queryParams:{id}
    });
  }
  userAction (user) { 
    this.store.dispatch(new UserAction(user));
  }
  ngOnDestroy() {  // 销毁时取消订阅
    this.loginUserStateSubscription.unsubscribe();
  }
}