CRUD dữ liệu, headless CMS

Login xong rồi thì làm gì? Tất nhiên là các thao tác xử lý dữ liệu. thường viết tắt là CRUD (Create Read Update Delete). Chúng ta sẽ làm tất cả trên Wordpress, tiếp nối 2 bài viết về login trước.

CRUD với việc Login bằng Ultimate User

Chúng ta cần tiến hành cài plugins Pods (Dùng để tạo thêm kiểu dữ liệu) và Admin Columns (thêm cột hiện thị trong admin)

Chúng ta sẽ VD bằng cách tạo thêm 1 thực thể là Tasks, có title và trạng thái hoàn thành.

Vào Pods và chọn Create New

Mặc định sẽ có sẵn Title (Text) và Content (HTML), chúng ta không cần dùng Content nên cần ẩn đi. Và thêm trường Trạng thái hoàn thành dạng checkbox bằng Add Field.

Lưu ý bật Read/Write via REST API

vào Advanced Options -> Supports bỏ chọn Editor (để bỏ Content). và sau đó Save Pod

Sau khi save xong chúng ta sẽ có 1 mục mới tên là Tasks ở Sidebar. Bấm Add New Task thì sẽ có giao diện như thế này

Tạo task xong thì khi ra giao diện list ta sẽ có:

Click vào nút bánh răng (Admin Columns) để thêm cột Is Completed

Mở lại Tasks ta sẽ thấy có cột mới xuất hiện:

Vậy là xong giao diện bên quản lý (admin). Giờ cần làm giao diện bên người dùng. Wordpress có cung cấp sẵn Restful API để chúng ta sử dụng, nên bên giao diện người dùng chúng ta sẽ dùng JS để gọi API sử dụng fetch.

Demo gọi 1 API

const requestOptions = {
  method: "GET",
};

fetch("http://huy-blog.local/wp-json/wp/v2/posts", requestOptions)
  .then((response) => response.json())
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

Đây là API gọi list các posts, kết quả trả về JSON như này:

Screenshot này gen từ Postman đã từng đề cập ở bài trước. Đoạn code JS trên có thể được gen tự động bằng Postman, truy cập mục Code snippet để copy.

Lưu ý: ở code snippet trên, phải sửa đoạn (response) => response.text() thành (response) => response.json() vì mặc định fetch có nhiều cách trả về dữ liệu. nếu để .text() thì sẽ chỉ trả về text thôi, nên nếu chắc chắn dữ liệu trả về là json thì hãy sử dụng .json()

Dòng quan trọng trong code là (result) => console.log(result) đây là lúc chúng ta đã lấy được dữ liệu và xử lý nó.

Để có thể biết được tất cả các Restful API được cung cấp bởi Wordpress. Cài thêm plugins WP API SwaggerUI và sau đó truy cập vào Settings -> Swagger để xem API doc

Giờ thử ghép code với task vừa tạo

Yeah kết quả đã hiện, chúng ta có thể làm các giao diện phức tạo hơn dựa vào dữ liệu này.

Bài tập: Đổi kiểu viết trên sang kiểu viết sử dụng state - setState - render.

Như hiện tại mới xong phần READ. Giờ sẽ đến phần UPDATE, chúng ta sẽ thực hiện giao diện đơn giản là click vào để toggle trạng thái hoàn thành.

Để làm được điều này cần đổi method sang PUT và kèm id của task trên URL. Khi đã Login với WP, thì mặc định mọi HTTP Request đều kèm Cookie chứa trạng thái đăng nhập, do đó khi dùng fetch chúng ta k cần truyền thông tin đó. Tuy nhiên cần phải truyền thêm headers X-WP-Nonce . Với bộ kit này thì đã làm sẵn biến _my_nonce để chúng ta sử dụng.

Xem video này:

Bài tập: Viết lại theo kiểu state - setState - render

Bài tập nâng cao: Thử tự tìm hiểu về async await và viết lại theo code sử dụng asyncawait thay cho .then()

Như vậy là đã xong 2 trường hợp READ và UPDATE. trường hợp CREATE và DELETE thì cũng khá tương tự chúng ta có thể dùng snippet code sau:

const deleteTask = (taskId) => {
    return fetch("http://huy-blog.local/wp-json/wp/v2/task/" + taskId , {
      method: 'DELETE',
      headers: {
        'X-WP-Nonce': _my_nonce,
      },
    })
      .then((response) => response.json())

  };

  const createTask = (data) => {
    return fetch("http://huy-blog.local/wp-json/wp/v2/task", {
      method: 'POST',
      headers: {
        'X-WP-Nonce': _my_nonce,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    })
      .then((response) => response.json())
  }

Bài tập: Ghép nốt 2 api delete và create này vào giao diện để có 1 trang quản lý task hoàn chỉnh.

CRUD với việc Login bằng Firebase Authentication

Các bước đầu với việc sử dụng plugins Pods và Admin Columns là tương tự. Điểm khác biệt là ở bước sử dụng API để CRUD dữ liệu. Vì khi login bằng Firebase Authentication, thì Wordpress sẽ không ghi nhận là login bằng wordpress, do đó sẽ k có cookie gì cả. việc truyền nonce sẽ là vô nghĩa.

Chúng ta sẽ cài thêm 1 plugins nữa là WordPress REST API Authentication

Sau khi cài và activate xong thì có giao diện như này:

Chúng ta sẽ dùng Basic Authentication -> Test Configuration -> điền user pass vào và bấm Test Configuration. Lúc này sẽ hiện ra Authorization header

Copy nó và cho vào header thay cho việc dùng nonce ở VD cũ.

Và nó sẽ chạy tương tự như cũ.

Tuy nhiên có 2 điều cần lưu ý:

  1. Do sử dụng Firebase để login nên phần Authorization này là từ tk admin và sẽ giống nhau ở mọi user. Do đó nếu viết trực tiếp như này trên web sẽ bị lộ tài khoản admin. KHÔNG dùng trực tiếp trên web kiểu này.

  2. Khi cài plugin này thì mặc định mọi api public đều k chạy được, nên nếu muốn quay lại dùng cách 1, thì phải deactive hoặc gỡ plugin này ra.

Vậy không dùng trực tiếp trên web, thì là dùng kiểu gì?

Việc sử dụng Login Firebase Authentication mục đích chính là để tách biệt giữa backend và frontend. Tách biệt sẽ giúp chúng ta có thể login trên cả app mobile nữa. Lúc này Wordpress đóng vai trò là Database/CMS để quản lý dữ liệu là chính, không nhất thiết phải xuất đầu lộ diện. Thuật ngữ gọi là Headless CMS.

Cách làm:

  • Tạo thêm 1 backend layer

  • Từ backend layer này gọi sang Wordpress để CRUD dữ liệu

  • Backend layer này tạo api và tương tác trực tiếp trên web thay cho Wordpress. kiểm tra tình trạng đăng nhập bằng Firebase.

Với cách này thì, Basic auth token không bao giờ bị lộ. Dưới đây mình sẽ giới thiệu 1 cách làm backend layer bằng JS.

Backend layer với Vercel

Một trong những vấn đề khá phổ biến của việc "Frontend dev bị assign làm 1 hệ thống có xử lý dữ liệu (aka Fullstack)" là "sử dụng 3rd service và bị lộ key". VD:

  • sử dụng Airtable để lưu contact từ form và bị lộ access token

  • sử dụng AWS S3 để upload file lên S3 và bị lộ access key.

Việc tạo backend layer như này sẽ giải quyết được tất cả các trường hợp cần ẩn key/token như vậy. Và Vercel là 1 service phù hợp nhất cho trường hợp này (Free & dễ deploy).

Để tạo Backend layer với Vercel thì cần thực hiện theo các bước:

Bước 1: Cài NodeJS version 18 hoặc cao hơn. Cài Yarn.

Bước 2: Cài Vercel CLI:

Mở terminal và gõ:

npm i -g vercel

Bước 3: Clone bộ code sau (cài git nếu chưa có sẵn):

git clone https://github.com/lequanghuylc/firebase-auth-vercel-backend-layer.git

Sau đó mở bằng trình soạn thảo code (VS Code):

Bước 4: Thực hiện các thao tác như chỉ dẫn trong file readme.md

Bước 5: Deploy thử và bạn sẽ có 1 domain để chạy API

Cấu trúc file chính là cấu trúc API.

Bài tập: Từ code html bài Login với Firebase, viết tiếp code để gọi API đến /clone-me/checkAuthen và in kết quả ra.

Gợi ý: firebase idToken chính là headers authorization.

Bước 6: Tạo thêm function để call sang Wordpress

Tạo file @common-utils/callWP.js


const WP_HOST_NAME = process.env.WP_HOST_NAME;
const WP_AUTH_TOKEN = process.env.WP_AUTH_TOKEN;

const getFullWPUrl = (path) => {
    const withQuery = path.includes('?') ? true : false;
    return WP_HOST_NAME + path + (
        withQuery ? '&mo_rest_api_test_config=basic_auth' : '?mo_rest_api_test_config=basic_auth'
    );
}

export const getWP = async (path) => {
    const url = getFullWPUrl(path);
    const response = await fetch(url, {
        method: 'GET',
        headers: {
            'Authorization': WP_AUTH_TOKEN,
        }
    });
    const json = await response.json();
    return json;
}

export const postWP = async (path, data) => {
    const url = getFullWPUrl(path);
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Authorization': WP_AUTH_TOKEN,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
    });
    const json = await response.json();
    return json;
}

export const putWP = async (path, data) => {
    const url = getFullWPUrl(path);
    console.log('url', url);
    const response = await fetch(url, {
        method: 'PUT',
        headers: {
            'Authorization': WP_AUTH_TOKEN,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
    });
    const json = await response.json();
    return json;
}

export const deleteWP = async (path) => {
    const url = getFullWPUrl(path);
    const response = await fetch(url, {
        method: 'DELETE',
        headers: {
            'Authorization': WP_AUTH_TOKEN,
        }
    });
    const json = await response.json();
    return json;
}

Sau đó tạo file .env ở thư mục gốc để điền các biến môi trường

WP_HOST_NAME="http://huy-blog.local/wp-json/wp/v2"
WP_AUTH_TOKEN="Basic YWRtaW46YWRtaW4="

Lưu ý: Nếu bạn dùng với LocalWP thì url của bạn chỉ truy cập được trên máy của b. Do đó nếu dùng domain của vercel sẽ không truy cập được. Phải dùng yarn start để chạy local.

Bước 7: Tạo api task và gọi đến các function trên

Và sửa code ở frontend 1 chút để đổi về gọi API từ backend là xong.

Bài tập: Hoàn thiện đủ 4 phương thức CRUD với kiểu sử dụng backend layer + firebase này.

Query và 1 số vấn đề nâng cao

Khi đã xử lý được CRUD cơ bản, thì vấn đề tiếp theo cần xử lý đó là phân quyền, và query nâng cao. Với logic thông thường thì 1 user chỉ được quyền quản lý các nội dung mà user đó tạo ra. Áp dụng vào ví dụ này, điều đó có nghĩa là:

  • Khi 1 user A tạo ra 1 task X mới, thì user đó sẽ nhìn thấy task đó trong list task. user B C D... không được thấy task X đó trong list task của họ.

  • User A được quyền sửa xóa task X, user B C D không có quyền, kể cả khi user B C D bằng cách nào đó có thể có được id của task X.

Để làm được điều này, chúng ta sẽ cần biết cách query nâng cao, để lấy ra đúng dữ liệu cần lấy để trả về. Với plugin rest-filter (đã kèm sẵn trong bộ kit ver mới, nếu bạn đang dùng ver cũ, thì hỏi lại mình gửi ver mới, hoặc cài thủ công bằng cách download từ github).

Sau khi cài vào chúng ta sẽ có thể query thông qua WP API như sau:

VD: Query lấy các các task chưa hoàn thành

VD: Query lấy các các task đã hoàn thành

VD: Query lấy các các task đã hoàn thành bởi user ID = 1

Để xem các params được hỗ trợ bởi "filter" thì có thể check trang này