0
0
Lập trình
TT

Hướng Dẫn Tối Ưu Cache Templates Handlebars Đúng Cách

Đăng vào 1 tháng trước

• 10 phút đọc

Giới Thiệu

Khi làm việc với Handlebars trong ứng dụng Node.js, nhiều nhà phát triển gặp vấn đề với việc templates được biên dịch lại cho mỗi yêu cầu, dẫn đến hiệu suất giảm. Bài viết này sẽ hướng dẫn bạn cách thiết lập caching cho templates Handlebars một cách hiệu quả trong khoảng 10 phút.

Vấn Đề Bạn Có Thể Gặp Phải

Nếu bạn đang sử dụng Handlebars trong ứng dụng Node.js và nhận thấy hiệu suất không như mong muốn, có thể là do việc biên dịch templates mỗi lần có yêu cầu mới. Hãy cùng điểm qua những gì bạn sẽ học được trong bài viết này:

  • Cách thiết lập caching cho templates Handlebars
  • Đăng ký partials và helpers một cách chính xác
  • Render templates với layouts mà không làm giảm hiệu suất
  • Một số vấn đề cần lưu ý mà tôi ước ai đó đã chỉ cho tôi

Thời gian thực hiện: ~10 phút
Độ khó: Khá dễ nếu bạn đã biết cơ bản về Handlebars

Những Gì Cần Chuẩn Bị

  • Dự án Node.js với Handlebars đã được cài đặt
  • Kiến thức cơ bản về templates Handlebars
  • Khoảng 10 phút rảnh rỗi

Nếu bạn chưa cài đặt Handlebars, hãy chạy lệnh sau: npm install handlebars

Vấn Đề Chi Tiết

Dưới đây là cách mà hầu hết chúng ta làm khi bắt đầu với Handlebars:

javascript Copy
// Cách này hoạt động nhưng chậm
const fs = require('fs');
const Handlebars = require('handlebars');

app.get('/users/:id', (req, res) => {
  // Đọc và biên dịch trên mỗi yêu cầu - thật tệ!
  const templateSource = fs.readFileSync('./views/user.hbs', 'utf8');
  const template = Handlebars.compile(templateSource);
  const html = template({ user: userData });
  res.send(html);
});

Tại Sao Điều Này Là Vấn Đề?

  • Đọc từ hệ thống tệp trên mỗi yêu cầu
  • Biên dịch template mỗi lần
  • Các partials bị xử lý lại liên tục
  • Máy chủ của bạn đang làm nhiều hơn mức cần thiết

Giải Pháp: Xây Dựng Hệ Thống Cache Đơn Giản

Hãy cùng khắc phục vấn đề này từng bước một. Dưới đây là cách tiếp cận sạch mà tôi thường sử dụng:

Bước 1: Tạo Cache Cho Templates

javascript Copy
// templateCache.js
const fs = require('fs');
const path = require('path');
const Handlebars = require('handlebars');

class TemplateCache {
  constructor(viewsPath) {
    this.viewsPath = viewsPath;
    this.templates = new Map();
    this.partials = new Map();
    this.isProduction = process.env.NODE_ENV === 'production';
  }

  // Tải và biên dịch một template
  getTemplate(templateName) {
    // Trong môi trường phát triển, luôn tải lại (để dễ dàng gỡ lỗi)
    if (!this.isProduction || !this.templates.has(templateName)) {
      const templatePath = path.join(this.viewsPath, `${templateName}.hbs`);
      const templateSource = fs.readFileSync(templatePath, 'utf8');
      const compiled = Handlebars.compile(templateSource);
      this.templates.set(templateName, compiled);
    }

    return this.templates.get(templateName);
  }

  // Đăng ký một partial
  registerPartial(partialName, partialPath = null) {
    const actualPath = partialPath || path.join(this.viewsPath, 'partials', `${partialName}.hbs`);

    if (!this.isProduction || !this.partials.has(partialName)) {
      const partialSource = fs.readFileSync(actualPath, 'utf8');
      Handlebars.registerPartial(partialName, partialSource);
      this.partials.set(partialName, true);
    }
  }

  // Đăng ký nhiều partials cùng lúc
  registerPartialsFromDir(partialsDir) {
    const partialFiles = fs.readdirSync(partialsDir);
    partialFiles.forEach(file => {
      if (file.endsWith('.hbs')) {
        const partialName = path.basename(file, '.hbs');
        const partialPath = path.join(partialsDir, file);
        this.registerPartial(partialName, partialPath);
      }
    });
  }

  // Xóa cache (hữu ích cho phát triển)
  clearCache() {
    this.templates.clear();
    this.partials.clear();
  }
}

module.exports = TemplateCache;

Ưu Điểm Của Cách Tiếp Cận Này

  • Caching trong môi trường sản xuất, luôn tươi mới trong phát triển
  • Lưu trữ đơn giản dựa trên Map
  • Xử lý cả templates và partials
  • Dễ dàng xóa cache khi cần thiết

Bước 2: Đăng Ký Các Helpers

javascript Copy
// helpers.js
const Handlebars = require('handlebars');

function registerHelpers() {
  // Helper định dạng ngày
  Handlebars.registerHelper('formatDate', function(date) {
    if (!date) return '';
    return new Date(date).toLocaleDateString();
  });

  // Helper điều kiện đơn giản
  Handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
    return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
  });

  // Helper JSON để gỡ lỗi
  Handlebars.registerHelper('json', function(context) {
    return JSON.stringify(context, null, 2);
  });

  // Helper cắt ngắn văn bản
  Handlebars.registerHelper('truncate', function(str, length) {
    if (!str || str.length <= length) return str;
    return str.substring(0, length) + '...';
  });
}

module.exports = { registerHelpers };

Bước 3: Thiết Lập Bộ Render Chính

javascript Copy
// renderer.js
const path = require('path');
const TemplateCache = require('./templateCache');
const { registerHelpers } = require('./helpers');

class TemplateRenderer {
  constructor(viewsPath) {
    this.cache = new TemplateCache(viewsPath);
    this.layoutsPath = path.join(viewsPath, 'layouts');

    // Đăng ký helpers một lần
    registerHelpers();

    // Đăng ký các partials thông dụng
    this.registerCommonPartials();
  }

  registerCommonPartials() {
    const partialsPath = path.join(this.cache.viewsPath, 'partials');
    if (require('fs').existsSync(partialsPath)) {
      this.cache.registerPartialsFromDir(partialsPath);
    }
  }

  // Render template không có layout
  render(templateName, data = {}) {
    const template = this.cache.getTemplate(templateName);
    return template(data);
  }

  // Render template với layout
  renderWithLayout(templateName, layoutName, data = {}) {
    // Đầu tiên render template chính
    const content = this.render(templateName, data);

    // Sau đó render layout với nội dung
    const layoutTemplate = this.cache.getTemplate(`layouts/${layoutName}`);
    return layoutTemplate({ ...data, body: content });
  }

  // Phương thức trợ giúp cho tích hợp Express
  expressRender(templateName, data, layoutName = null) {
    if (layoutName) {
      return this.renderWithLayout(templateName, layoutName, data);
    }
    return this.render(templateName, data);
  }
}

module.exports = TemplateRenderer;

Kết Hợp Tất Cả Lại Với Nhau

Dưới đây là cách sử dụng nó trong ứng dụng Express của bạn:

javascript Copy
// app.js
const express = require('express');
const path = require('path');
const TemplateRenderer = require('./renderer');

const app = express();

// Khởi tạo bộ render
const renderer = new TemplateRenderer(path.join(__dirname, 'views'));

// Sử dụng trong các route
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id); // Lấy dữ liệu
    const html = renderer.renderWithLayout('user', 'main', { 
      user, 
      title: `User: ${user.name}` 
    });
    res.send(html);
  } catch (error) {
    console.error('Lỗi render:', error);
    res.status(500).send('Đã có lỗi xảy ra');
  }
});

// Đối với phản hồi API hoặc templates đơn giản
app.get('/user-card/:id', async (req, res) => {
  const user = await getUserById(req.params.id);
  const html = renderer.render('user-card', { user });
  res.send(html);
});

app.listen(3000, () => {
  console.log('Máy chủ đang chạy với caching templates!');
});

Cấu Trúc Tệp Template Của Bạn

Dưới đây là cách tôi tổ chức các tệp template:

Copy
views/
├── layouts/
│   ├── main.hbs          # Layout chính
│   └── admin.hbs         # Layout admin
├── partials/
│   ├── header.hbs        # Partial header
│   ├── footer.hbs        # Partial footer
│   └── user-card.hbs     # Component user card
├── user.hbs              # Trang chi tiết người dùng
├── users.hbs             # Trang danh sách người dùng
└── home.hbs              # Trang chính

Ví dụ về layout (views/layouts/main.hbs):

html Copy
<!DOCTYPE html>
<html>
<head>
    <title>{{title}}</title>
    <meta charset="utf-8">
</head>
<body>
    {{> header}}

    <main>
        {{{body}}}
    </main>

    {{> footer}}
</body>
</html>

Ví dụ về template (views/user.hbs):

html Copy
<div class="user-profile">
    <h1>{{user.name}}</h1>
    <p>Email: {{user.email}}</p>
    <p>Joined: {{formatDate user.createdAt}}</p>

    {{#ifEquals user.role "admin"}}
        <div class="admin-badge">Administrator</div>
    {{/ifEquals}}
</div>

Những Vấn Đề Thường Gặp

1. Thời gian Đăng Ký Partial

javascript Copy
// ❌ Điều này có thể không hoạt động - partial chưa được đăng ký
app.get('/page', (req, res) => {
  renderer.cache.registerPartial('new-partial');
  const html = renderer.render('page', data); // Có thể thất bại
});

// ✅ Đăng ký partials khi khởi động
const renderer = new TemplateRenderer('./views');
// Tất cả partials đã được đăng ký trong quá trình khởi tạo

2. Caching Trong Phát Triển So Với Sản Xuất

javascript Copy
// Trong phát triển, bạn muốn templates mới
// Lớp TemplateCache tự động xử lý điều này dựa trên NODE_ENV

// Nhưng nếu bạn cần làm mới thủ công trong phát triển:
if (process.env.NODE_ENV !== 'production') {
  app.get('/clear-cache', (req, res) => {
    renderer.cache.clearCache();
    res.send('Cache đã được xóa');
  });
}

3. Vấn Đề Về Ngữ Cảnh của Helpers

javascript Copy
// ❌ Điều này có thể không hoạt động như mong đợi
Handlebars.registerHelper('userLink', function(user) {
  return `<a href="/users/${user.id}">${user.name}</a>`;
});

// ✅ Tốt hơn - xử lý dữ liệu thiếu
Handlebars.registerHelper('userLink', function(user) {
  if (!user || !user.id) return '';
  return new Handlebars.SafeString(`<a href="/users/${user.id}">${user.name || 'Unknown'}</a>`);
});

Kiểm Tra Hiệu Suất Nhanh

Muốn kiểm tra xem caching của bạn có hoạt động không? Thêm một số timing đơn giản:

javascript Copy
// Thêm điều này vào renderer
render(templateName, data = {}) {
  const start = Date.now();
  const template = this.cache.getTemplate(templateName);
  const html = template(data);
  const duration = Date.now() - start;

  if (process.env.NODE_ENV !== 'production') {
    console.log(`Rendered ${templateName} in ${duration}ms`);
  }

  return html;
}

Bạn sẽ thấy:

  • Yêu cầu đầu tiên: Có thể 10-50ms (biên dịch + render)
  • Các yêu cầu sau: 1-5ms (chỉ render)

Khi Nào Cách Tiếp Cận Này Hoạt Động Tốt

Hoàn hảo cho:

  • Ứng dụng render phía máy chủ
  • Tạo mẫu email
  • Tạo PDF từ HTML
  • Trình tạo trang tĩnh
  • Giao diện quản trị

Có thể là quá nhiều cho:

  • Ứng dụng một trang (bạn có thể đang sử dụng framework frontend)
  • APIs chỉ trả về JSON
  • Các trang rất đơn giản với 2-3 templates

Các Cách Tiếp Cận Thay Thế

Sử Dụng Express-Handlebars

Nếu bạn muốn một cái gì đó tích hợp sẵn:

javascript Copy
const exphbs = require('express-handlebars');

app.engine('hbs', exphbs({
  defaultLayout: 'main',
  extname: '.hbs',
  // Caching được xử lý tự động trong sản xuất
  cache: process.env.NODE_ENV === 'production'
}));

app.set('view engine', 'hbs');

Khi nào tôi sử dụng điều này: Khi tôi muốn ít mã tùy chỉnh hơn và không bận tâm đến việc thêm phụ thuộc.

Trình Giám Sát Tệp Trong Phát Triển

Để tự động xóa cache trong quá trình phát triển:

javascript Copy
// Chỉ trong phát triển
if (process.env.NODE_ENV !== 'production') {
  const chokidar = require('chokidar');
  chokidar.watch('./views').on('change', () => {
    renderer.cache.clearCache();
    console.log('Cache templates đã được xóa');
  });
}

Kết Luận

Cách tiếp cận caching này đã hoạt động tốt cho tôi trong nhiều ứng dụng sản xuất. Các lợi ích chính:

  • Render nhanh hơn sau lần tải đầu tiên
  • Linh hoạt - bạn có thể cache những gì bạn muốn, khi bạn muốn
  • Thân thiện với phát triển - templates mới khi bạn đang lập trình
  • Dễ hiểu - không có phép màu, chỉ là maps và đọc tệp

Việc thiết lập chỉ mất vài phút, nhưng cải thiện hiệu suất thường rõ rệt, đặc biệt nếu bạn có các templates phức tạp với nhiều partials.

Danh sách kiểm tra nhanh:

  • ✅ Templates cache trong sản xuất, tải lại trong phát triển
  • ✅ Partials được đăng ký khi khởi động
  • ✅ Helpers được đăng ký một lần
  • ✅ Render layout hoạt động đúng
  • ✅ Xử lý lỗi đã được thực hiện

Về Cách Tiếp Cận Này

Tôi đã sử dụng mẫu này trong một vài dự án khác nhau - từ công cụ nội bộ nhỏ đến các ứng dụng lớn hướng tới khách hàng. Đây không phải là cách duy nhất để caching Handlebars, nhưng nó đã hoạt động đáng tin cậy cho tôi và khá dễ hiểu và sửa đổi.

Nếu bạn thử nghiệm điều này, hãy cho tôi biết kết quả nhé! Luôn muốn nghe về trải nghiệm của người khác với hiệu suất template.


Viết trong khi gỡ lỗi việc render template chậm - chúng ta đều đã từng trải qua!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào