0

Hướng dẫn tạo REST API không cần máy chủ

Nếu bạn là lập trình viên Front-End và muốn thể hiện kỹ năng của mình, việc sử dụng GitHub Pages hoặc Netlify để hiển thị ứng dụng có thể gặp phải một số hạn chế. Thay vào đó, bạn có thể tạo REST API ngay trong trình duyệt mà không cần bất kỳ máy chủ nào. Với cách này, bạn có thể thể hiện kỹ năng của mình trong các ứng dụng tương tác với phần backend được lưu trữ ở những nơi bạn không thể truy cập vào phía máy chủ.

Sau đây tôi sẽ chia sẻ kiến thức hữu ích này cho các bạn. Đầu tiên có một số khái niệm mà chúng ta cần làm rõ.

Service Worker là gì?

API trình duyệt cho phép bạn tạo các phản hồi HTTP thuần túy trong trình duyệt cho các yêu cầu HTTP được gọi là Service Worker. API này chủ yếu được tạo ra để chặn các yêu cầu HTTP có nguồn gốc từ trình duyệt và phục vụ chúng từ bộ nhớ cache.

Điều này cho phép bạn tạo các ứng dụng được gọi là PWA hoạt động khi bạn không có kết nối internet. Vì vậy, bạn có thể sử dụng chúng khi đang di chuyển trên tàu, nơi bạn có thể có internet không ổn định. Khi bạn ngoại tuyến, các yêu cầu HTTP có thể được lưu trữ và gửi đến máy chủ thực khi bạn trực tuyến trở lại.

Nhưng đây không phải là tất cả những gì Service Worker có thể làm. Với Service Worker, bạn có thể tạo các yêu cầu HTTP chưa từng tồn tại. Nó có thể chặn bất kỳ yêu cầu HTTP nào, ví dụ: khi bạn mở hình ảnh trong tab mới hoặc sử dụng AJAX (như với fetch API).

Cách đăng ký Service Worker

Service Worker cần được viết trong một tệp riêng biệt (thường được gọi là sw.js, nhưng bạn có thể đặt tên bất kỳ).

Vị trí của tệp đó rất quan trọng. Nó nên được đặt trong thư mục gốc của ứng dụng của bạn, thường là trong thư mục gốc của tên miền.

Để đăng ký một service worker, bạn cần thực thi đoạn mã này:

if ('serviceWorker' in navigator) {
  var scope = location.pathname.replace(/\/[^\/]+$/, '/')
  navigator.serviceWorker.register('sw.js', { scope })
    .then(function(reg) {
       reg.addEventListener('updatefound', function() {
         var installingWorker = reg.installing;
         console.log('A new service worker is being installed:',
                     installingWorker);
       });
       // registration worked
       console.log('Registration succeeded. Scope is ' + reg.scope);
    }).catch(function(error) {
      // registration failed
      console.log('Registration failed with ' + error);
    });
}

Điều này sẽ cài đặt một service worker có thể bắt đầu chặn các yêu cầu HTTP.

LƯU Ý: Service worker chỉ hoạt động với HTTPS và localhost.

Cách tạo phản hồi HTTP cơ bản

API của Service Worker rất đơn giản - bạn có một sự kiện được gọi là fetch và bạn có thể phản hồi sự kiện đó bằng bất kỳ phản hồi nào:

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);
    if (url.pathname === '/api/hello/') {
        const headers = {
            'Content-Type': 'text/plain'
        };
        const msg = 'Hello, Service Worker!'
        event.respondWith(textResponse(msg, headers));
   }
});

function textResponse(string, headers) {
    const blob = new Blob([string], {
        type: 'text/plain'
    });
    return new Response(blob, { headers });
}

Với đoạn mã này, bạn có thể mở URL /api/hello/ và nó sẽ hiển thị văn bản "Hello, Service Worker!" dưới dạng tệp văn bản.

Ngoài ra, một điều quan trọng: nếu bạn muốn sử dụng Service Worker ngay sau khi nó được cài đặt, bạn cần thêm đoạn mã này:

self.addEventListener('activate', (event) => {
  event.waitUntil(clients.claim());
});

Thông thường, Service Worker chỉ chặn các yêu cầu sau khi bạn refresh trang. Đoạn mã này buộc chấp nhận các yêu cầu ngay sau khi cài đặt.

LƯU Ý: Với service worker, bạn cũng có thể chặn yêu cầu được gửi đến các tên miền khác nhau. Nếu bạn có ứng dụng của mình trên GitHub Pages, bạn có thể chặn các yêu cầu đến bất kỳ miền nào. Bởi vì không có kiểm tra tên miền, đoạn mã này:

await fetch('https://example.com/api/hello').then(res => res.text())

cũng sẽ trả về Hello, Service Worker!.

Cách tạo một dự án cơ bản

Bạn sẽ tạo một cái gì đó hữu ích hơn bằng cách tạo một dự án React với xác thực người dùng rất đơn giản.

Lưu ý rằng điều này không an toàn theo bất kỳ cách nào, vì thông tin người dùng và mật khẩu sẽ hiển thị trong mã. Nhưng nó có thể cho thấy rằng bạn biết cách tương tác với API trong React.

1. Thiết lập Vite

Đầu tiên, bạn cần thiết lập một ứng dụng React đơn giản với Vite.

Để sử dụng Vite, bạn cần cài đặt Node.js. Nếu bạn không có, bạn có thể đọc cách cài đặt nó từ bài viết này.

Sau đó, bạn cần chạy lệnh này từ terminal:

npm create vite@latest

Tôi đã chọn tên auth, React và JavaScript. Đây là kết quả đầu ra tôi nhận được:

✔ Project name: … auth
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /home/kuba/auth...

Done. Now run:

  cd auth
  npm install
  npm run dev

Tiếp theo là sửa đổi tệp vite.config.js, vì vậy Vite sẽ biết cách xây dựng tệp service worker/

Đây là tệp cấu hình mà Vite đã tạo:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

Bạn cần sửa đổi tệp cấu hình để bao gồm đoạn mã này:

import { join } from "node:path";
import { buildSync } from "esbuild";

export default defineConfig({
  plugins: [
    react(),
    {
      apply: "build",
      enforce: "post",
      transformIndexHtml() {
        buildSync({
          minify: true,
          bundle: true,
          entryPoints: [join(process.cwd(), "src", "sw.js")],
          outfile: join(process.cwd(), "dist", "sw.js"),
        });
      },
    },
  ]
})

Bạn cần bao gồm cả hai lần nhập và bạn có thể thay thế cấu hình hiện có bằng cấu hình ở trên. Bạn cũng có thể thêm mã trong dấu ngoặc nhọn vào một mảng plugin.

2. Sử dụng thư viện Wayne

Sau đó, bạn cần tạo một tệp Service Worker có tên sw.js. Bạn sẽ sử dụng thư viện Wayne thay vì tự viết các route. Điều này sẽ đơn giản hóa mã.

Đầu tiên, bạn cần cài đặt Wayne:

npm install @jcubic/wayne

Sau đó, bạn có thể tạo một tệp có tên sw.js (Lưu ý: bạn đã đặt thư mục "src" trong tệp vite.config.js, vì vậy bạn nên lưu tệp đó trong thư mục đó).

import { Wayne } from '@jcubic/wayne';

const app = new Wayne();

app.get('/api/hello/', (req, res) => {
   res.text('Hello, Service Worker!');
});

Đoạn mã này sẽ hoạt động chính xác giống như ví dụ trước của chúng ta.

3. Cài đặt Service Worker

Bây giờ, điều cuối cùng bạn cần làm để thiết lập service worker của mình là đăng ký nó. Bạn có thể sử dụng mã mà bạn đã thấy trước đó, nhưng bây giờ bạn sẽ sử dụng một thư viện cho việc này.

Đầu tiên, bạn cần cài đặt nó:

npm install register-service-worker

Và cập nhật src/main.jsx bằng mã này:

import { register } from "register-service-worker";

register(`./sw.js`);

Điều cuối cùng là xây dựng dự án bằng cách thực hiện:

npm run build

LƯU Ý: chế độ dev sẽ không hoạt động với service worker - bạn cần xây dựng dự án.

Các hướng dẫn thiết lập Service Worker với Vite dựa trên bài viết này.

4. Kiểm tra trên máy chủ Web

Để kiểm tra dự án của bạn, bạn có thể sử dụng lệnh này:

npx http-server -p 3000 ./dist/

Điều này sẽ tạo một máy chủ HTTP đơn giản, nơi bạn có thể kiểm tra ứng dụng của mình.

LƯU Ý: nếu bạn mở tệp index.html trong trình duyệt (như bằng cách kéo và thả), service worker sẽ không hoạt động. Điều này là do giao thức file:// có rất nhiều hạn chế. Đó là lý do tại sao bạn cần một máy chủ web.

Nếu bạn kiểm tra ứng dụng trong trình duyệt bằng cách mở URL: http://127.0.0.1:3000, nó sẽ chạy mã đăng ký service worker và bạn sẽ có thể truy cập ngay vào điểm cuối HTTP giả của chúng tôi: http://127.0.0.1:3000/api/hello/. Nó sẽ hiển thị văn bản:

Hello, Service Worker!

LƯU Ý: để đơn giản hóa việc thử nghiệm, bạn có thể thêm "http-server -p 3000 ./dist/" vào tệp package.json vào scripts:

"serve": "http-server -p 3000 ./dist/",

Hãy nhớ rằng package.json là một tệp JSON, vì vậy bạn không thể đặt dấu phẩy ở cuối nếu đây là tập lệnh cuối cùng.

Để làm cho nó hoạt động, bạn cần cài đặt gói:

npm install http-server

Bây giờ bạn có thể chạy máy chủ với npm run serve.

LƯU Ý: nếu bạn truy cập URL: http://127.0.0.1:3000/api/hello (bạn có thể đọc 127.0.0.1 là gì trong bài viết này), bạn sẽ nhận được lỗi từ http-server. Điều này là do route bạn đã tạo trong service worker đã sử dụng dấu gạch chéo ở cuối. Để khắc phục điều này, bạn có thể thêm chuyển hướng:

app.get('/api/hello', (req, res) => {
   res.redirect(301, req.url + '/');
});

Cách thêm xác thực React

Bây giờ, sau khi đã thiết lập mọi thứ, bạn có thể thêm một điểm cuối xác thực thực và kết nối nó với ứng dụng React của mình.

1. Tạo mã thông báo JWT

Chúng tôi sẽ sử dụng mã thông báo JWT phổ biến để xác thực. Bạn có thể đọc thêm về chúng trong bài viết này.

Đầu tiên, bạn cần cài đặt thư viện JWT:

npm install jose

Sau đó, bạn cần tạo một tệp mới có tên jwt.js trong thư mục src:

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(
  'cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2'
);

const alg = 'HS256';

const jwt = {
    sign: (payload) => {
        return new SignJWT(payload)
            .setProtectedHeader({ alg })
            .setIssuedAt()
            .setIssuer('https://freecodecamp.org')
            .setAudience('https://freecodecamp.org')
            .setExpirationTime('2h')
            .sign(secret)
    },
    verify: async (token) => {
        const { payload } = await jwtVerify(token, secret, {
            issuer: 'https://freecodecamp.org',
            audience: 'https://freecodecamp.org',
        });
        return payload;
    }
};

export default jwt;

Đoạn mã này là Mô-đun ES sử dụng thư viện mã thông báo jose JWT để tạo mã thông báo mới, jwt.sign. Nó xác minh rằng mã thông báo là chính xác với jwt.verify và nó cũng trả về payload, vì vậy bạn có thể trích xuất bất kỳ thứ gì bạn lưu trong mã thông báo.

Bạn có thể đọc thêm về thư viện jose từ tài liệu - các liên kết đến tài liệu có trong README.

LƯU Ý: Do giới hạn của Service Worker, chúng tôi không thể tạo xác thực thực tế trong đời thực, trong đó mã thông báo truy cập được lưu trữ trong cookie (Service Worker không cho phép tạo cookie) và sử dụng mã thông báo refresh để cập nhật mã thông báo truy cập.

2. Thêm API xác thực

Bây giờ, bạn có thể sử dụng các hàm trước đó để tạo một điểm cuối API:

import jwt from './jwt';

app.post('/api/login', async (req, res) => {
    const { username, password } = await req.json() ?? {};
    if (username === 'demo' && password === 'demo') {
        const token = await jwt.sign({ username });
        res.json({ result: token });
    } else {
        res.json({ error: 'Invalid username or password' });
    }
});

Đoạn mã này sẽ xác minh rằng tên người dùng và mật khẩu là chính xác (cả hai đều bằng "demo") và tạo mã thông báo JWT mới. Nếu tên người dùng hoặc mật khẩu không chính xác, nó sẽ trả về lỗi.

3. Thêm xác thực vào React

Bạn đã tạo một ứng dụng React với Vite, vì vậy bạn cần sử dụng JSX để thêm logic xác thực giao diện người dùng.

Đầu tiên, bạn tạo một hàm trợ giúp sẽ gửi yêu cầu HTTP đến điểm cuối /api/login với Fetch API:

function login(username, password) {
    return fetch('/api/login', {
        method: 'post',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username,
            password
        })
    }).then(res => res.json());
}

Tiếp theo, bạn cần tạo một biểu mẫu cơ bản:

<form>
  <div>
    <label for="user">username</label>
    <input id="user" />
  </div>
  <div>
    <label for="password">password</label>
    <input id="password" type="password" />
  </div>
  <button>login</button>
</form>

Và thêm một chút kiểu dáng:

form {
  display: inline-flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-end;
}

label::after {
  content: ":";
}

label {
  width: 100px;
  display: inline-block;
  text-align: right;
  margin-right: 10px;
}

Tiếp theo, bạn cần một hàm xác thực mà bạn sẽ thêm vào sự kiện onSubmit. Bạn sẽ sử dụng hai biến trạng thái cho mã thông báo và lỗi:

function App() {
  const [token, setToken] = useState(null);
  const [error, setError] = useState(null);

  async function auth(event) {
    event.preventDefault();

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
    } else if (res.error) {
      setError(res.error);
    }
  }

Để lấy tên người dùng và mật khẩu từ biểu mẫu, bạn có thể sử dụng refs. Bạn cũng có thể chỉ hiển thị biểu mẫu khi mã thông báo chưa được đặt:

function App() {
  const [token, setToken] = useState(null);
  const [error, setError] = useState(null);
  const userRef = useRef();
  const passwordRef = useRef();

  async function auth(event) {
    event.preventDefault();
    const username = userRef.current.value;
    const username = passwordRef.current.value;

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
    } else if (res.error) {
      setError(res.error);
    }
  }

  return (
    <div>
      <div className="card">
        {!token && (
          <form onSubmit={auth}>
            <div>
              <label for="user">username</label>
              <input id="user" ref={userRef}/>
            </div>
            <div>
              <label for="password">password</label>
              <input id="password" ref={passwordRef} type="password"/>
            </div>
            <button>login</button>
          </form>
        )}
        {error && <p className="error">{ error }</p>}
      </div>
    </div>
  );
}

Bây giờ bạn có thể kiểm tra ứng dụng. Nếu bạn nhập tên người dùng và mật khẩu, chúng sẽ không được đặt lại.

Bạn có thể khắc phục sự cố này bằng cách đặt giá trị ref thành một chuỗi trống ở cuối hàm:

userRef.current.value = '';
passwordRef.current.value = '';

Có một lỗi khác. Nếu bạn nhập sai tên người dùng hoặc mật khẩu, bạn sẽ gặp lỗi. Nhưng sau đó, nếu bạn nhập đúng mật khẩu, lỗi sẽ không được xóa. Để khắc phục sự cố này, bạn cần đặt lại trạng thái lỗi khi đặt mã thông báo:

async function auth(event) {
    event.preventDefault();
    const username = userRef.current.value;
    const username = passwordRef.current.value;

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
      setError(null);
    } else if (res.error) {
      setError(res.error);
    }
    userRef.current.value = '';
    passwordRef.current.value = '';
  }

Điều tiếp theo bạn có thể làm là trích xuất tên người dùng từ mã thông báo. Điều này cũng sẽ xác minh rằng mã thông báo là chính xác trong ứng dụng React của bạn. Bạn cần sử dụng hook useEffect để chạy mã khi mã thông báo thay đổi:

import jwt from './jwt';

  // ...
  const [username, setUsername] = useState(null);

  useEffect(() => {
    jwt.verify(token).then(payload => {
      const { username } = payload;
      setUsername(username);
    }).catch(e => {
      setError(e.message);
    });
  }, [token]);

  // ...

Nếu bạn chạy đoạn mã này, bạn sẽ gặp lỗi: Compact JWS must be a string or Uint8Array.

Lý do là hook useEffect sẽ được kích hoạt khi mã thông báo là null. Trước khi bạn xác minh, bạn cần kiểm tra xem mã thông báo đã được đặt chưa:

useEffect(() => {
    if (token !== null) {
      jwt.verify(token).then(payload => {
        const { username } = payload;
        setUsername(username);
      }).catch(e => {
        setError(e.message);
      });
    }
  }, [token]);

Tiếp theo, bạn có thể hiển thị tên người dùng sau khi người dùng đăng nhập:

{token && (
  <div>
    <p>Welcome {username}</p>
  </div>
)}

Các bước tiếp theo

Điều cuối cùng chúng ta có thể làm là lưu mã thông báo trong localStorage và thêm nút đăng xuất.

Bạn có thể cải thiện điều này và thêm nhiều điểm cuối hơn, chẳng hạn như nhận dữ liệu thực mà bạn sẽ lưu trong tệp sw.js. Bạn có thể lưu trữ dữ liệu trong IndexedDB, vì vậy nó sẽ tồn tại như trong một ứng dụng thực.

IndexedDB không có API đẹp lắm, nhưng có những thư viện bổ sung thêm sự trừu tượng trên nó, bạn có thể tìm ở trên mạng nhé.

Hy vọng bài viết này giúp ích cho bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí