Phần 3: Xây dựng frontend với Angular 6

Ngày 03 tháng 1 năm 2019 | 154 views

Chào anh em, sau kỳ nghỉ tết dương lịch mình đã quay trở lại và ăn hại hơn xưa rồi đây. Như đã hứa, lần này mình sẽ xây dựng Frontend cho ứng dụng chat bằng angular 6 nhé!

Bước đầu tiên chắc chắn là phải cài đặt angular-cli rồi

npm install -g @angular/cli

và tiếp theo là build project. Ở đây mình đặt tên project là front-chat

ng new front-chat

Trong khi build project mình sẽ chọn có sử dụng router và dùng SCSS. Ok, vậy là đã có project. Các bạn có thể chạy thử bằng lệnh: 

ng serve

Mình sẽ giải thích qua cấu trúc thư mục project của angular như sau:

Trong khuôn khổ bài viết, các bạn chỉ cần quan tâm các phần sau:

1. App: Nơi chứa source code. Ban đầu sẽ mặc định có file: app.module.ts; app.component.ts; app.component.scss; app.component.html và app-routing.module.ts. Trong đó:

app.module.ts: Đăng ký các module thư viện
app.component.ts: Component đầu tiên của ứng dụng, nơi chứa các mã typescript
app.component.scss: file style cho component App
app.component.html: file html hiển thị của component App
app-routing.module.ts: file điều hướng ứng dụng

2. Package.json: khai báo các thư viện sử dụng

3. Assets: tương tự phần /public trên backend. Nơi chứa các phần media như ảnh, âm thanh...

Vì là ứng dụng sử dụng socket nên cần cài đặt thư viện socket.io-client và thư viện feathersjs, feathers-reactive

npm i @feathersjs/authentication-client

npm i @feathersjs/feathers

npm i @feathersjs/socketio-client

npm i feathers-reactive

Ấu kề! Giờ bắt tay vào code nào! :D

Trước tiên chúng ta sẽ tạo ra service feather để sử dụng socket io. Run lệnh sau:

ng generate service /service/feathers

Khởi tạo và add socket io và tạo ra 3 function: service(); authenticate(); logout().

feathers.service.ts

@Injectable({
  providedIn: 'root'
})
export class FeathersService {
  private _feathers = feathers();                     // init socket.io
  private _socket = io('http://localhost:3030');      // init feathers

  constructor() {
    this._feathers
      .configure(feathersSocketIOClient(this._socket))  // add socket.io plugin
      .configure(feathersAuthClient({                   // add authentication plugin
        storage: window.localStorage
      }))
      .configure(feathersRx({                           // add feathers-reactive plugin
        idField: '_id'
      }));
  }

  // expose services
  public service(name: string) {
    return this._feathers.service(name);
  }

  // expose authentication
  public authenticate(credentials?): Promise<any> {
    return this._feathers.authenticate(credentials);
  }

  // expose logout
  public logout() {
    return this._feathers.logout();
  }
}

*Lưu ý: Chúng ta không cần quan tâm tới file .spec.ts vì nó là file được generate tự động

Chúng ta cũng sẽ tạo service phục vụ cho việc login và logout. Chạy lệnh sau:

ng generate service /service/auth

auth.service.ts

export class AuthService {

    constructor(private feathers: FeathersService, private router: Router) { }
    public logIn(credentials?): Promise<any> {
      return this.feathers.authenticate(credentials);
    }

    public logOut() {
      this.feathers.logout();
      this.router.navigate(['/']);
    }
}

Việc khai báo các biến "private feathers: FeathersService, private router: Router" trong hàm contructor sẽ tạo ra các instance của các service và có thể sử dụng bất cứ đâu trong class.

Để tạo ra một rào chắn bắt buộc phải login mới có thể đi tiếp vào khu vực chat, chúng ta cần 1 guard.

ng generate guard /guards/auth

auth.guard.ts

export class AuthGuard implements CanActivate {

  constructor(private router: Router, private auth: AuthService){}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.auth
      .logIn()
      .then(() => true)
      .catch(() => {
        this.router.navigate(['/login']);
        return false;
      });
  }
}

Tiếp theo mình sẽ tạo form login, tức là cần tạo component login. Chạy lệnh sau để tạo 1 component:

ng generate component /components/login

// Kết quả
CREATE src/app/components/login/login.component.html (24 bytes)
CREATE src/app/components/login/login.component.spec.ts (621 bytes)
CREATE src/app/components/login/login.component.ts (266 bytes)
CREATE src/app/components/login/login.component.scss (0 bytes)
UPDATE src/app/app.module.ts (668 bytes)

Bây giờ mình sẽ tạo ra 1 form login đơn giản. Các bạn có thể tìm example trên mạng hoặc tùy ý custom nó. Mình sẽ lấy đại một cái trên mạng cho nhanh :D

login.component.html

<div class="container">
  <div class="login-form">
    <div class="main-div">
    <h2 >Chatting Application</h2>
    <form>
      <div class="form-group">
        <input #email class="form-control" id="exampleInputEmail1" type="email" name="email" placeholder="Email">
      </div>
      <div class="form-group">
          <input #password class="form-control" id="exampleInputPassword1" type="password" name="password" placeholder="Password">
      </div>
      <button type="submit" class="btn btn-primary" (click)="login(email.value, password.value)">Login</button>
    </form>
    <ul>
        <li *ngFor="let m of messages">{{m}}</li>
    </ul>
  </div>
</div>

Sau khi tạo xong component các bạn có thể thấy chúng ta sẽ không thể xem mặt mũi của login component vì ta chưa khai báo router cho nó. Vì vậy cần khai báo router cho nó trong file app.routing.module.ts

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: 'login',
        component: LoginComponent
      }
    ]
  }
];

Như vậy các bạn có thể chạy thử và gõ vào trình duyệt: localhost:4200/login để xem màn hình login. 

Tiếp theo là phần viết xử lý cho login component. Đồng thời mình cũng tạo màn hình chat (component chat) sau khi đăng nhập và add router cho nó:

export class LoginComponent implements OnInit {

  messages: string[] = [];
  constructor(private feathers: FeathersService, private router: Router) { }

  ngOnInit() {
    document.body.classList.add('bg-img');
  }

  login(email: string, password: string) {
    if (!email || !password) {
      this.messages.push('Incomplete credentials!');
      return;
    }

    // try to authenticate with feathers
    this.feathers.authenticate({
      strategy: 'local',
      email,
      password
    })
      // navigate to base URL on success
      .then(() => {
        this.router.navigate(['/']);
      })
      .catch(err => {
        this.messages.unshift('Wrong credentials!');
      });
  }

}

Tạo component chat:

ng generate component /components/chat

Add vào router trong file app.routing.module.ts

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'chat'
      },
      {
        path: 'chat',
        component: ChatComponent,
        canActivate: [AuthGuard]
      },
      {
        path: 'login',
        component: LoginComponent
      }
    ]
  }
];

*Lưu ý: tất cả các service và component muốn sử dụng được cần khai báo trong file app.module.ts nhé!

import { AuthService } from './service/auth.service';
import { FeathersService } from './service/feathers.service';
import { LoginComponent } from './components/login/login.component';
import { ChatComponent } from './components/chat/chat.component';
import { AuthGuard } from './guards/auth.guard';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    ChatComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule
  ],
  providers: [
    AuthService,
    FeathersService,
    AuthGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Như vậy là xong thằng login. Các bạn có thể test thử sau khi login nó có nhảy vào màn hình chat.component.html hay không.

Phần tiếp theo mình sẽ vẽ màn hình chat và xử lý message chat!

Đầu tiên mình sẽ lên mạng và tải vài cái ảnh avatar về và ném nó vào 1 thư mục /images trong /assets

Layout giao diện chat sẽ chia làm 4 phần như hình sau:

chat.component.html

<div class="flex flex-row flex-1 clear">
      <aside class="sidebar col col-3 flex flex-column flex-space-between" *ngIf="users$ | async as users">
        <header class="flex flex-row flex-center">
          <h4 class="font-300 text-center">
            <span class="font-600 online-count">{{users.length}}</span> users
          </h4>
        </header>

        <ul class="flex flex-column flex-1 list-unstyled user-list">
          <li *ngFor="let user of users">
            <a class="block relative" href="#">
              <img [src]="user.avatar" alt="" class="avatar">
              <span class="absolute username">{{user.email}}</span>
            </a>
          </li>
        </ul>
        <footer class="flex flex-row flex-center">
          <a href="#" id="logout" class="button button-primary" (click)="logOut()">
            Sign Out
          </a>
        </footer>
      </aside>

      <div class="flex flex-column col col-9">
        <main class="chat flex flex-column flex-1 clear" #chat [scrollTop]="chat.scrollHeight - chat.clientHeight">
          <div class="message flex flex-row" *ngFor="let message of messages$ | async">
            <img [src]="message.user.avatar" [alt]="message.user.email" class="avatar">
            <div class="message-wrapper">
              <p class="message-header">
                <span class="username font-600">{{message.user.email}}</span>
                <span class="sent-date font-300">{{message.createdAt | date:'MMM dd, hh:mm:ss'}}</span>
              </p>
              <p class="message-content font-300">{{message.text}}</p>
            </div>
          </div>
        </main>

        <form class="flex flex-row flex-space-between" id="send-message">
          <input #message type="text" name="text" class="flex flex-1">
          <button class="button-primary" (click)="sendMessage(message.value); message.value = ''">Send</button>
        </form>
      </div>
    </div>

Phần xử lý javascript:

chat.component.ts

export class ChatComponent implements OnInit {

  messages$: Observable<any[]>;
  users$: Observable<any[]>;

  ngOnInit() {
    // chỉ để style lại. Bỏ background của màn hình đăng nhập
    document.body.classList.remove('bg-img');
  }

  constructor(private data: DataService, private auth: AuthService) {
    // get messages from data service
    this.messages$ = data.messages$().pipe(
        // our data is paginated, so map to .data
        map((m: Paginated<any>) => m.data),
        // reverse the messages array, to have the most recent message at the end
        // necessary because we get a descendingly sorted array from the data service
        map(m => m.reverse()),
      );

    // get users from data service
    this.users$ = data.users$().pipe(
        // our data is paginated, so map to .data
        map((u: Paginated<any>) => u.data)
      );
  }

  sendMessage(message: string) {
    this.data.sendMessage(message);
  }

  logOut() {
    this.auth.logOut();
  }

}

Cuối cùng các bạn có thể chạy và chat thử. Ngoài ra các bạn có thể phát triển ứng dụng lên với phần đăng ký user, status active or not active...

Có bất kỳ câu hỏi nào thì vui lòng comment ở dưới nhé. Thanks :D

Source Code:

Backend: https://github.com/tuyenlehuu/FeathersJS.git 

Frontend: https://github.com/tuyenlehuu/Angular-6-chat-app.git

Phần 2: Xây dựng backend với FeatherJS

Ngày 28 tháng 12 năm 2018

28-12-2018
172
Tìm hiểu về Node Js cơ bản

Ngày 03 tháng 9 năm 2017

03-09-2017
296