Bình luận Facebook: Bạn là Pokémon nào? Sử dụng Nodejs, Heroku

Trong thời gian rảnh rỗi, tôi thường dành "một chút" thời gian để lướt Facebook. Gia đình và bạn bè tôi sống không ở gần nhau về mặt địa lý, đây là một cách tuyệt vời để xem cuộc sống của họ như thế nào. Tôi thấy có rất nhiều thông tin trên "tường" của tôi, từ các video hài hước tới những sự kiện sắp diễn ra và có cả "Hãy tag một người bạn vào đây người đó sẽ dẫn bạn đi ăn!" 👻 Tôi đã thấy một thứ khá thú vị, khi bạn comment một url tính năng preview của Facebook sẽ trả lại cho bạn một hình ảnh và một tiêu đề ngẫu nhiên. Cái đầu tiên tôi thấy là "Bạn là nhân vật nào trong truyện Tam quốc?". Tôi thấy đây làm một ý tưởng khá hay và thú vị, nó có thể làm tăng lượng truy cập cho một website. Ở một vài cái khác mà tôi thấy thì họ sẽ trạ lại ngẫu nhiên một bộ phim, một bài hát... Chúng ta sẽ thử làm một cái tương tự bằng Nodejs.

Ý tưởng

Chúng ta sẽ không làm lại theo ý tưởng mà tôi đã thấy, chúng ta sẽ làm cái gì đó vui nhộn một chút. "Bạn là Pokémon nào?" đúng rồi tôi thích Pokémon 😻 Khi có một người comment url app của chúng ta Fb sẽ hiển thị phẩn preview là ảnh một chú Pokémon và tên của nó. Giống như thế này

Tính năng xem trước của Facebook

Khi chúng ta bình luận có kèm theo một url, tính năng preview của Fb sẽ cho hiển thị trước thông tin về url đó. Những thông tin đó được Fb quy định để "Fb scrap" có thể lấy được các thông tin về một url, chúng ta cần nhúng các thẻ meta vào trang html mà url sẽ trỏ tới. Các bạn có thể đọc một tài liệu ở đây Webmaster sharing Quan trọng nhất cho bài viết là là các thẻ:

<meta property="og:image" content="img_url" />  
<meta property="og:title" content="title" />  

Hmmm, chúng ta sẽ cần phải có thông tin về "Tên của Pokémon" và hình ảnh của chúng.

Thu thập dữ liệu

Đầu tiên chúng ta sẽ cần có tập hợp thông tin của các con Pokémon, tôi cũng biết tên vài con 😄, có thể gõ tên từng con rồi "lên mạng" kiếm ảnh của nó. Nhưng thôi, đã lên mạng thì lên đó kiếm cả danh sách luôn cho nhanh Pokémon Database || List of Pokémon (yaoming). Chúng ta sẽ dùng dữ liệu từ trang này https://www.giantbomb.com/profile/wakka/lists/the-150-original-pokemon/59579 có ảnh to rõ nét và cấu trúc html rõ ràng (yaoming) Mục đích cuối của bước thu thập dữ liệu sẽ là có database danh sách của các Pokémon và ảnh tương ứng của chúng. Vì chúng ta dùng Node nên chúng ta sẽ dùng luôn Mongodb cho "hợp" (justforfun) Chỉ cần dùng một file .json là đủ demo rồi 😄 File json của chúng ta sẽ chứa thông tin cần thiết, cấu trúc có thể sẽ như này:

[
  {
    "name": "Pikachu",
    "image": "/uploads/scale_small/13/135472/Pikachu.png"
  }
]

Chúng ta có thể sử dụng link ảnh ở server của họ, nhưng ở đây chúng ta sẽ tải luôn ảnh về để trên server của mình. Và rút gọn cấu trúc nội dung file json sẽ là

[
    "Bulbasaur"
]

chứa danh sách tên của Pokémon, tên ảnh thì sẽ đặt trùng tên với Pokémon luôn (yaoming)

Chúng ta sẽ cần một phương thức để tải các file ảnh: File ảnh sẽ được lưu vào thư mục public/img

async function download(uri, filename) {
  return new Promise((resolve, reject) => {
    const request = require('request');
    request.head(uri, function (err, res, body) {
      if (err) {
        return reject(err);
      }
      request(uri).pipe(fs.createWriteStream(`./public/img/${filename}.png`)).on('close', (err) => {
        if (err) {
          return reject(err);
        }
        resolve();
      });
    });
  });
}

một hàm để lấy thông tin Pokémon và lưu ảnh tương ứng theo thông tin có được tử url ở trên.

async function getPokemons(url) {
  let html = await request(url);
  let $ = cheerio.load(html);
  let pokemons = [];
  $('div.img.imgboxart').each(async (i, ele) => {
    let $card = $(ele);
    let name = $card.next('h3').text().trim().split(' ')[1];
    pokemons.push(name);

    let imgUri = $card.find('img').attr('src');
    try {
      await download(imgUri, name);
    } catch(err) {
      console.log(err);
    }
    console.log('Catch: ', name);
  });
  return pokemons;
}

Nội dung toàn bộ file thực hiện việc thu thập dữ liệu crawler.js : Lấy thông tin từ 2 trang, lưu ảnh vào thư mục, thông tin lưu vào file pokemons.json

const request = require('request-promise');
const cheerio = require('cheerio');
const fs = require('fs');

const GENERATION_I = 'https://www.giantbomb.com/profile/wakka/lists/the-150-original-pokemon/59579/?page=1';
const GENERATION_II = 'https://www.giantbomb.com/profile/wakka/lists/the-150-original-pokemon/59579/?page=2';

(async () => {
  try {
    let pokemons = [
      await getPokemons(GENERATION_I),
      await getPokemons(GENERATION_II)
    ];
    pokemons = [...pokemons[0], ...pokemons[1]];
    
    // save file
    fs.writeFile(`./pokemons.json`, JSON.stringify(pokemons, null, 4), (err) => {
      if (err) {
        console.log(err);
      }
    });
  } catch (err) {
    console.log(err);
  }
})();

async function download(uri, filename) {
  return new Promise((resolve, reject) => {
    const request = require('request');
    request.head(uri, function (err, res, body) {
      if (err) {
        return reject(err);
      }
      request(uri).pipe(fs.createWriteStream(`./public/img/${filename}.png`)).on('close', (err) => {
        if (err) {
          return reject(err);
        }
        resolve();
      });
    });
  });
}

async function getPokemons(url) {
  let html = await request(url);
  let $ = cheerio.load(html);
  let pokemons = [];
  $('div.img.imgboxart').each(async (i, ele) => {
    let $card = $(ele);
    let name = $card.next('h3').text().trim().split(' ')[1];
    pokemons.push(name);

    let imgUri = $card.find('img').attr('src');
    try {
      await download(imgUri, name);
    } catch(err) {
      console.log(err);
    }
    console.log('Catch: ', name);
  });
  return pokemons;
}

Chạy lệnh node scrawler.js.

Xây dựng ứng dụng

Đã có dữ liệu, chúng ta sẽ xây dựng một http server đơn giản trả lại nội html với các thẻ FB seo tương ứng Nội dung file server.js

const express = require('express');
const app = express();
const POKEMONS = require('./pokemons.json')

function getRandomItem(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}

app.use(express.static('public'));

// set the view engine to ejs
app.set('view engine', 'ejs');

// use res.render to load up an ejs view file

// index page 
app.get('/', (req, res) => {
  let pokemon = req.query.pokemon || getRandomItem(POKEMONS);
  res.render('pages/index', {
    domain: process.env.DOMAIN_DEFAULT || './',
    pokemon,
  });
});

app.set('PORT', process.env.PORT || 8080);
app.listen(app.get('PORT'));
console.log(`${app.get('PORT')} is the magic port`);

chúng ta set static cho thư mục public để hiển thị file ảnh, dùng view engine là ejs. url index sẽ trả về nội dung của file index.ejs được bind dữ liệu: domain (domain để hiển thị file ảnh, Fb scraper sẽ hiển thị lỗi nếu bạn để link trong html là dạng relatively), pokemon là tên Pokémon.

Nội dung file index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta property="og:type" content="article" />
  <meta property="og:url" content="<%= domain %>/?pokemon=<%= pokemon %>" />
  <meta property="og:title" content="You are <%= pokemon %>" />
  <meta property="og:description" content="Like it? share with friends" />
  <meta property="og:image:type" content="image/png" />
  <meta property="og:image:width" content="300" />
  <meta property="og:image:height" content="300" />
  <meta property="og:image" content="<%= domain %>/img/<%= pokemon %>.png" />
  <meta itemprop="name" content="You are <%= pokemon %>" />
  <meta itemprop="description" content="Like it? share with friends" />
  <meta itemprop="image" content="<%= domain %>/img/<%= pokemon %>.png" />
  <meta name="twitter:card" content="photo" />
  <meta name="twitter:title" content="You are <%= pokemon %>" />
  <meta name="twitter:image" content="<%= domain %>/img/<%= pokemon %>.png" />
  <title>Which Pokémon Are You?</title>
</head>
<body>
</body>
</html>

nội dung không có gì, chủ yếu tập trung vào phần head

Đưa ứng dụng lên Heroku

Chúng ta cần public ứng dụng của chúng ta trên Internet. Heroku đáp ứng được các yêu cầu cơ bản của chúng ta: Có domain, deploy dễ dàng, free đủ dùng. B1. Đăng ký một tài khoản Heroku miễn phí (Khá dễ) B2. Đăng nhập dashboard của Heroku, tạo một app trên đó. Nhớ cấu hình biến môi trường DOMAIN_DEFAULT cho app ở mục Setting. Giá trị là domain của app https://your-app-name.herokuapp.com B3. Cài đặt Heroke CLI B4. Commit project, thêm mới remote Heroku repo (Trên trang chủ có hướng dẫn cho từng trường hợp cụ thể) B5. Push vào nhánh master trên heroku remote. Kiểm tra xem ứng dụng đã hoạt động chưa https://your-app-name.herokuapp.com/ Để kiểm tra tính năng preview của fb hoạt động thế nào, chúng ta có công cụ Facebook Debugger . Khi kiểm tra url của chúng ta, công cụ này sẽ cho ta xem trước kết quả, hay những lỗi đang mắc phải. Một số trường hợp khi push lên nhưng ứng dụng không được khởi chạy:

  • Không có file package.json (thiếu các thư viện cần cho ứng dụng)
  • Trong file package.json không mô tả start script của npm . trong trường hợp này "start": "node server.js",
  • Ứng dụng http server sử dụng sai cổng (PORT), đó là lý do chúng ta dùng cấu hình process.env.PORT

Kết quả

Dùng thử nào, đây là ứng dụng của mình https://fb-cmt-pokemon.herokuapp.com/, mình đã thử comment vào các bài viết FB nhưng chỉ hiện một "con", chắc là bị hệ thống cache lại rồi 😐 , sửa lại "mô tả vậy": "Hãy comment link này https://fb-cmt-pokemon.herokuapp .com?birthday=010194 (không có dấu cách) thay vào đó là ngày sinh của bạn để xem bạn là Pokémon". Nói là thay ngày sinh cho có chút "phong thủy" chứ thay bằng cái gì mà chẳng được (yaoming). Mã nguồn: https://github.com/hoangsetup/fb-cmt-pokemon


All Rights Reserved