Xây dựng ứng dụng Real Time với VueJS, ES2015 và Webpack

Vue.js là một framework cho việc xây dựng các ứng dụng web sử dụng một phương pháp tiếp cận dựa trên Component. Nó tập trung chủ yếu vào lớp "View" của mô hình MVC truyền thống và bao hàm trong đó là các đặc trưng của ReactJS, Angular hoặc Ember, nó có thể dể dàng sử dụng để tích hợp vào các thư viện khác hoặc vào dự án đã có sẵn.

Ứng dụng thời gian thực với VueJS

Vue.js sau khi khá thành công với Version đầu của mình, gần đây tiếp tục công bố Version 2, và để kiểm tra điều đó chúng ta sẽ xây dựng một ứng dụng tìm kiếm trên Twitter thời gian thực để sử dụng nó. API này cho phép chúng ta tìm kiếm Twitter với các điều kiện cụ thể và có các sự kiện Pusher kích hoạt bất cứ khi nào một tweet mới được tìm thấy. Ứng dụng của chúng ta sẽ cho phép người nhập nhiều điều khoản và sau đó hiển thị các tweet bất cứ khi nào chúng ta có được dữ liệu mới từ streamer Twitter.

Bắt đầu với Vue.js và ECMAScript 2015

Vue.js có thể được viết trong ECMAScript 5 nhưng cho những kinh nghiệm tốt nhất thì bạn nên viết ECMAScript 6. Đây là khuyến cáo của Vue.js và cũng có nghĩa là chúng ta có thể viết một số mã gọn gàng hơn bằng cách sử dụng một số tính năng mới mạnh mẽ của ngôn ngữ. Để làm điều này chúng ta cần phải bỏ ra một vài phút thiết lập môi trường cho nó. Nếu bạn muốn để có được và chạy một cách nhanh chóng, bạn có thể lấy mã từ GitHub và làm theo các hướng dẫn trong README để có được nó chạy nó ở local.

Webpack

Chúng tôi sẽ sử dụng Webpack cho đóng gói ứng dụng. Webpack có thể từ một ứng dụng JavaScript và gộp nó tất cả cùng với sự phụ thuộc của nó. Quan trọng hơn đối với chúng tôi, chúng ta có thể cấu hình nó để chạy tất cả các tập tin của chúng tôi thông qua Babel. Babel sẽ lấy mã ES6 của chúng ta và chuyển đổi nó thành ES5, vì vậy chúng ta có thể viết sử dụng các tính năng mới mà không phải lo lắng về việc hỗ trợ trình duyệt. Đáng chú ý rằng Vue.js không yêu cầu bạn phải làm điều này, và nếu bạn muốn tránh cài đặt Webpack cho Vue.js thì có thể có một số cách khác mà bạn có thể bắt đầu với thư viện. Chúng ta sẽ theo cách tiếp cận bằng Webpack; hãy tạo một dự án mới với NPM và sau đó cài đặt dependency của chúng ta:

npm init -y
npm install --save-dev webpack babel-loader babel-preset-es2015 babel-core live-server

Cuối cùng chúng ta cần phải cấu hình Webpack, đóng gói tất cả file JavaScript của chúng ta. Để làm điều này chúng ta tạo ra webpack.config.js:

module.exports = {
  entry: './app/main',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      }
    }]
  }
}

Với cấu hình này, Webpack sẽ bắt đầu từ app/main.js. Từ đó sau đó nó sẽ đi qua tất cả các file trong ứng dụng và tìm bất kỳ tập tin JavaScript nào mà chúng ta cần. TIếp theo từng file sẽ qua babel-loader, và được chuyển thành es2015 mà chúng ta cài đặt trước đó. Webpack sau đó sẽ biên dịch ứng dụng của chúng ta vào bundle.js. Cuối cùng, chúng ta hãy cài đặt Vue:

npm install --save vue

Bây giờ chúng ta đã sẵn sàng để bắt đầu viết mã nguồn! Trước tiên, hãy tạo index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Vue Twitter Streaming</title>
  </head>
  <body>
    <div id="app">
      <app-component></app-component>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

Điều đầu tiên cần lưu ý là ở phía dưới chúng ta nạp file bundle.js, phần mã nguồn của chúng ta sẽ nằm ở đây. Thứ hai là trong div#app chúng ta có <app-component></app-component>. Đây sẽ là một thành phần mà chúng ta tạo sử dụng Vue.js. Vue.js đòi hỏi rằng khi bạn tạo ra nó, bạn cung cấp cho nó một phần tử HTML mà cả ứng dụng hoạt động bên trong đó. Trong trường hợp của chúng ta, chúng ta sẽ sử dụng các phần tử div#app. Để làm điều này, đầu tiên tạo ra app/main.js. Ở đây chúng ta sẽ khởi Vue.js:

import Vue from 'vue';
// don't worry, we haven't created this yet!
import AppComponent from './components/app-component/app-component';

new Vue({
  el: '#app',
  components: {
    'app-component': AppComponent
  }
});

Cách Vue.js hoạt động là chúng ta khởi tạo ứng dụng Vue của chúng ta vào một phần tử trên trang. Sau đó chúng ta nói với Vue những thành phần tùy chỉnh có thể tìm thấy trong mã nguồn. Chúng ta sẽ tạo hai thư mục khác:

mkdir app/components
mkdir app/components/app-component

Bây giờ, tạo ra app/components/app-component/app-component.js:

import Vue from 'vue';

const AppComponent = Vue.extend({
  template: '<h1>Hello World</h1>',
});

export default AppComponent;

Chúng ta sử dụng Vue.extend để tạo ra các component và trong trường hợp này có thể truyền vào options, template hoặc là đoạn mã HTML. Bây giờ chúng ta chỉ cần chạy một vài lệnh để đóng gói ứng dụng của chúng ta lại và chạy. Mở một cửa sổ lệnh và chạy:

webpack -w

Việc này sẽ bắt đầu Webpack nhưng đặt nó trong chế độ theo dõi, vì vậy nó sẽ tiếp tục theo dõi các tập tin và đóng gọi lại khi cần. Tốt nhất là Webpack thể tìm ra chính xác những gì đã thay đổi và không thể đóng gói lại toàn bộ ứng dụng của bạn, để quá trình làm việc được thực hiện nhanh chóng. Trong một cửa sổ lệnh khác ./node_modules/.bin/live-server --port=3004 để chạy ứng dụng của bạn tại máy trên cổng 3004 (có thể chọn bất kì cổng nào). Sau đó, truy cập vào http://locahost:3004 thì sẽ hiển thị Hello World trên trang.

Xây dựng App Component

Ứng dụng của chúng ta sẽ làm được:

  • Hiển thị form tìm kiếm trên Tweet
  • Thu thập thông tin người dùng tìm kiếm để hiển thị lên trang

Hãy bắt đầu với form để người dùng có thể cho chúng ta biết những gì họ muốn tìm kiếm. Bây giờ chúng ta sẽ cần thêm nhiều mã HTML, tôi không muốn đưa nóvào trong tập tin JavaScript của chúng ta. Hãy chuyển nó vào một tập tin riêng biệt, app/components/app-component/app-component-template.html. Bây giờ, chỉ cần đặt

Hello World

vào trong đó. Tiếp theo, chúng ta sẽ cấu hình Webpack để nạp các tập tin văn bản thô. Để làm điều này, chúng ta sẽ cài đặt plugin raw-loader:

npm install --save-dev raw-loader

Bây giờ, cập nhật webpack.config.js để cấu hình bộ nạp mới:

module.exports = {
  entry: './app/main',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      }
    }, {
      test: /\.html$/,
      loader: 'raw'
    }]
  }
}

Chúng ta bảo với Webpack rằng bất cứ khi nào một tập tin phù hợp với mẫu regex /.html$/, có nghĩa là các tập tin kết thúc bằng .html, cần phải chạy nó thông qua raw-loader. Bây giờ, chúng ta hãy cập nhật app-component.js để nạp các template:

import Vue from 'vue';
import template from './app-component-template.html';

const AppComponent = Vue.extend({
  template,
  // the above is ES6 shorthand for:
  // template: template
});

export default AppComponent;

Nếu bạn khởi động lại Webpack (bạn phải khởi động lại nó bất cứ khi nào bạn thêm một plugin mới), bạn vẫn sẽ thấy "Hello World", tuy nhiên bây giờ chúng ta đã tách HTML ra một file riêng biệt và chúng ta đang để ở một vị trí tốt hơn nhiều. Hãy thêm HTML chúng ta cần cho form:

<div>
  <div id="search-form">
    <form v-on:submit.prevent="newSubscription">
      <input class="swish-input" v-model="newSearchTerm" placeholder="JavaScript" />
      <button class="bright-blue-hover btn-white">Search</button>
    </form>
  </div>
</div>

Lưu ý hai ràng buộc cụ thể Vue.js. Đầu tiên, v-on:submit.prevent liên kết với function newSubscription (mà chúng ta sẽ định nghĩa tiếp sau) được gọi khi form của chúng ta được gửi. Thêm vào .prevent nói cho Vue.js biết rằng nó sẽ ngăn chặn các hành động mặc định. Thứ hai, v-model, trên mỗi input, thiết lập một ràng buộc giữa các giá trị của đầu vào và biến newSearchTerm mà chúng ta sẽ sử dụng trong component của chúng ta.

const AppComponent = Vue.extend({
  template,
  data() {
    return {
      newSearchTerm: '',
      channels: []
    }
  },
  methods: {
    newSubscription() {
      this.channels.push({
        term: this.newSearchTerm,
        active: true
      });
      this.newSearchTerm = '';
    }
  }
});

Đầu tiên chúng tôi định nghĩa dữ liệu khi khởi tạo trạng thái đầu của component. Trong trường hợp của chúng ta, chúng tôi sẽ đặt newSearchTerm cho một chuỗi rỗng và xác định một mảng trống rỗng của các kênh, đó là những gì chúng ta sẽ thêm vào khi người dùng tìm kiếm một nội dung mới. Thứ hai, chúng ta định nghĩa function newSubscription trong đối tượng methods. Vue đòi hỏi bạn phải tạo ra method trong đối tượng methods, giúp giữ cho các component Vue của bạn gọn gàng và dễ dàng hơn để làm việc. Khi người dùng điền vào form và gửi chúng ta thêm một kênh mới vào mảng, và tiếp theo là xóa nội dung trong input. Mỗi kênh có hai đặc tính: term và active. Sau đó chúng ta sẽ bổ sung thêm chức năng để có thể chuyển đổi một kênh sang trạng thái không hoạt động, lúc này chúng ta sẽ dừng thêm kết quả mới cho kênh đó.

Danh sách các kênh và kết quả của chúng

Tiếp theo, hãy cập nhật các template cho các component để nó có thể hiển thị thông tin của mỗi kênh và cho phép chúng ta dừng lại và loại bỏ một kênh.

<div class="container tweets-container">
  <div id="channels-list">
    <div class="channel" v-for="channel in channels">
      <h3>
        <img class="twitter-icon" src="img/twitter.png" width="30" />
        Tweets for {{ channel.term }}
      </h3>
      <div id="subscription-controls">
        <button v-on:click.prevent="toggleSearch(channel)">
          {{channel.active ? 'Stop' : 'Restart'}} Stream
        </button>
        <button v-on:click.prevent="clearSearch(channel)">
          Remove Results
        </button>
      </div>
      <subscription-component
        :channel="channel"
        :pusher="pusher"></subscription-component>
    </div>
  </div>
</div>

Đầu tiên, lưu ý cách chúng ta sử dụng v-for để lặp và tạo ra một thẻ div mới cho mỗi kênh:

<div class="channel" v-for="channel in channels">

Điều này sẽ báo cho Vue tạo ra một thẻ div mới cho mỗi kênh trong mảng kênh. HTML trong thẻ div có chứa các tiêu đề của các kênh - sử dụng {{content}} để in dữ liệu sang HTML - và sau đó thêm một số nút để tạm dừng và loại bỏ một kênh, cùng với subscription-component, chúng ta sẽ làm sau đó.

<h3>
  <img class="twitter-icon" src="img/twitter.png" width="30" />
  Tweets for {{ channel.term }}
</h3>
<div id="subscription-controls">
  <button v-on:click.prevent="toggleSearch(channel)">
    {{channel.active ? 'Stop' : 'Restart'}} Stream
  </button>
  <button v-on:click.prevent="clearSearch(channel)">
    Remove Results
  </button>
</div>

Sau đó chúng ta sử dụng v-on:click.prevent để gắng sự kiện click của cả hai nút. Dưới đây chúng ta gắng nó vào một lời gọi hàm, và truyền vào trong kênh như là đối số cho hàm. Chúng ta định nghĩa hai method là toggleSearch và clearSearch. Chúng sẽ được đặt vào phần methods của app-component.

methods: {
  ...
  toggleSearch(channel) {
    for (let ch of this.channels) {
      if (ch.term === channel.term) {
        ch.active = !ch.active;
        break;
      }
    }
  },
  clearSearch(channel) {
    this.channels = this.channels.filter((ch) => {
      return ch.term !== channel.term;
    });
  }

toggleSearch tìm kiếm kênh và chuyển nó thành trạng thái activeform hoặc ngược lại. clearSearch trả về danh sách các kênh đã loại bỏ kênh đã chọn.

Component cho kênh

Cuối cùng chúng ta phải tạo subscription-component.Component này sẽ có trách nhiệm lấy các kết quả được gửi qua Pusher và hiển thị chúng lên trên màn hình. app-component chúng ta thêm như sau:

<subscription-component
  :channel="channel"
  :pusher="pusher"></subscription-component>

Đầu tiên, chúng ta cần phải tạo ra subscription-component và khai báo trong app-component. Tạo thư mục app/components/subscription-component, file subscription-component.js và subscription-component-template.html. Đầu tiên chúng ta tạo file js:

import Vue from 'vue';
import template from './subscription-component-template.html';

const SubscriptionComponent = Vue.extend({
  template,
  props: [
    'channel',
    'pusher'
  ],
  data() {
    return {
      tweets: []
    }
  },

  // component methods and definitions here
});

export default SubscriptionComponent;

Trước hết chúng ta khai báo template và các thuộc tính của nó. Đây là một tính năng thực sự có lợi vì nó giúp mỗi component tự quản lý chính nó. Ngoài ra chúng ta cũng thiết lập trạng thái ban đầu bằng cách định nghĩa hàm data, trong đó trả về tweets là một mảng rỗng. Bây giờ, chúng ta có thể quay trở lại app-component.js và khai báo subscription-component.

import Vue from 'vue';
import template from './app-component-template.html';
import SubscriptionComponent from '../subscription-component/subscription-component';

const AppComponent = Vue.extend({
  template,
  components: {
    'subscription-component': SubscriptionComponent
  },
  data() {
    return {
      newSearchTerm: '',
      channels: []
    }
  },
  ...
}

Thay đổi tiếp theo trong app-component cài đặt và cấu hình thư viện PusherJS. Đầu tiên, là cài đặt nó:

npm install --save pusher-js

Và sau đó khai báo vào app-component. Chúng ta sẽ thiết lập trong hàm created, hàm này sẽ tự động gọi khi component được khởi tạo. Đây là một nơi tuyệt vời để thực hiện bất kỳ thiết lập nào cần thiết.

import Vue from 'vue';
import Pusher from 'pusher-js';
import template from './app-component-template.html';
import SubscriptionComponent from '../subscription-component/subscription-component';

const AppComponent = Vue.extend({
  template,
  components: {
    'subscription-component': SubscriptionComponent
  },
  created() {
    this.pusher = new Pusher(YOUR_PUSHER_KEY_HERE);
  }
  ...
}

Chúng ta đã hoàn thành app-component và có thể tập trung vào subscription-component. Trước hết, chúng ta hãy thêm vào template. Chỉnh sửa subscription-component-template.html để nó như dưới đây:

<div>
  <ul class="channel-results channel-{{channel.term}}">
    <li v-for="result in tweets">
      <p class="white">{{ result.tweet.text }}</p>
    </li>
  </ul>
</div>

Một lần nữa chúng ta sử dụng v-for qua mỗi tweet và làm hiển thị nó lên trang web. Chúng ta sẽ cập nhật các tweet khi tweet mới đến từ Pusher, và VueJS sẽ hiển thị chúng vào DOM.

Nhận thông báo từ một kênh

Khi subscription component được tạo ra, chúng ta muốn gọi khởi tạo của PusherJS và đăng ký một kênh Pusher sử dụng từ khóa tìm kiếm của chúng ta đã được khai báo từ trước. Trước hết, chúng ta hãy định nghĩa hàm subscribeToChannel. Hãy nhớ rằng trong VueJS bất kỳ hàm nào đều phải nằm trong đối tượng methods:

methods: {
  subscribeToChannel() {
    this.pusherChannel = this.pusher.subscribe(btoa(this.channel.term));
    this.pusherChannel.bind('new_tweet', (data) => {
      this.newTweet(data); // Don't worry, we haven't defined this func yet!
    });
    this.subscribed = true;
  }
}

Để đăng ký vào các kênh, chúng ta phải sử dụng btoa để mã hóa base64 từ khóa tìm kiếm. Bởi vì Pusher có quy định về những gì có thể và không thể được sử dụng trong các tên kênh. Để cho phép người dùng tìm kiếm bất cứ điều gì trên Twitter trong từ khóa tìm kiếm, chúng ta phải mã hóa các từ khóa tìm kiếm, để đảm bảo an toàn trên kênh Pusher. Sau đó chúng ta lắng nghe sự kiện new_tweet. Cuối cùng, chúng ta thêm vào hàm created mà chỉ đơn giản là sẽ gọi phương thức mới này:

created() {
  this.subscribeToChannel()
}

Danh sách các tweet mới

Bây giờ chúng ta hãy định nghĩa newTweet sẽ lấy dữ liệu từ các sự kiện Pusher và thêm nó vào mảng tweets:

methods: {
  ...
  newTweet(data) {
    this.tweets.push(data);
    this.$nextTick(() => {
      const listItem = document.querySelector(`.channel-${this.channel.term}`);
      listItem.scrollTop = listItem.scrollHeight;
    });
  }
}

Phần mã tiếp theo dùng để kéo danh sách chứa các tweet của chúng ta lên trên để hiển thị tweet mới nhất. Để làm điều này chúng ta cần phải can thiệp vào VueJS để biết khi nào hiện thị một tweet mới, đó là hàm $nextTick(). Chúng ta định nghĩa chức năng cho nó và đảm bảo để nó chạy sau khi DOM đã được cập nhật.

Ngưng cập nhật

Nếu người dùng hủy đăng ký bằng cách nhấn vào nút "Remove" chúng ta đã thực hiện trước đó, chúng ta cần dọn dẹp một chút. Nếu chúng ta chỉ đơn giản bỏ nó ra khỏi mảng thì chúng ta sẽ gặp rắc rối; mỗi component sẽ đăng ký vào một kênh Pusher nhưng sẽ hủy đăng ký khi nó đã được xóa. Điều này sẽ gây ra tốn bộ nhớ trong một thời gian dài. Rất may Vue giúp chúng ta một lần nữa - chúng ta có thể định nghĩa một hàm beforeDestroy nó sẽ được gọi trước khi các thành phần loại bỏ từ DOM - đây là một cơ hội tuyệt vời để ta dọn dẹp sạch những gì còn xót lại. Hãy đinh nghĩa hàm unsubscribe để hủy đăng ký một kênh:

methods: {
  unsubscribe() {
    this.pusherChannel.unsubscribe(btoa(this.channel.term));
    this.pusherChannel && this.pusherChannel.unbind();
    this.subscribed = false;
  },
  ...
 }

Chúng tôi gọi hủy bỏ đăng ký, nó sẽ hủy đăng ký kênh và sau đó loại bỏ các sựu kiện lắng nghe. Sau đó chúng tôi thiết lập subscribed săng false. Sau đó chúng ta thêm vào beforeDestroy để gọi hàm mới này:

beforeDestroy() {
  this.unsubscribe();
}

Bật tắt giữa đăng ký và hủy đăng ký kênh

Trước đó chúng ta đã làm nút cho phép người dùng hủy đăng ký một kênh, nhưng sau đó nhấn nút một lần nữa để thay đổi nó sang trạng thái hoạt động lại. Chúng ta cần phải lắng nghe khi nào xảy ra và hành động. Để làm điều này chúng ta có thể theo dõi thuộc tính channel.active. VueJS cho phép chúng ta định nghĩa để theo dõi một đối tượng. Ở đây, từ khóa sẽ là thuộc tính mà t muốn theo dõi, và các giá trị sẽ là hàm được gọi khi nó thay đổi.

watch: {
  'channel.active': function() {
    if (!this.channel.active && this.subscribed) {
      this.unsubscribe();
    } else if (this.channel.active && !this.subscribed) {
      this.subscribeToChannel();
    }
  }

Tùy thuộc vào nếu các kênh đang hoạt động và nếu chúng ta đã đăng ký hay không, chúng ta bỏ đăng ký hoặc đăng ký cho phù hợp.

Kết thúc

Cuối cùng, chúng ta đã hoàn tất tất cả! Cú pháp của VueJS rất sáng sủa. Tài liệu của nó hoàn toàn tuyệt vời và có thể giúp ta xây dựng nhanh nhất một ứng dụng. Tôi hy vọng bài viết này đã cho bạn một số cảm nhận về VueJS và tôi rất muốn khuyên bạn nên làm việc với nó nữa. Mã nguồn của của bài viết được đưa lên đây để mọi người có thể tham khảo. Nguồn: https://blog.pusher.com/exploring-real-time-apps-with-vuejs-es2016-and-webpack/