+29

Chúng Ta Đã Quên Mất Những Điều Cơ Bản Về Frontend

Trong cơn bão của các xu hướng và mô hình mới nhất, có vẻ như chúng ta đã quên đi những kiến thức nền tảng của lập trình frontend. Điều này không chỉ xảy ra với các lập trình viên mới vào nghề, mà ngay cả những người có kinh nghiệm cũng đôi khi bị cuốn theo làn sóng của các công nghệ mới mà bỏ qua những nguyên tắc cơ bản.

Thông thường, mình vẫn nghĩ rằng điều đó không quá tệ, những lập trình viên tài năng vẫn có thể làm được hầu hết mọi thứ. Tuy nhiên, gần đây mình đã nhận ra một số điều khiến mình phải thay đổi cách nhìn về ngành công nghiệp này. Có vẻ như giữa vô số xu hướng, mô hình và những điều mới mẻ, chúng ta đã quên mất xương sống của lập trình frontend.

Trong bài viết này, mình muốn chia sẻ một vài đoạn code từ một dự án gần đây và thử giải thích suy nghĩ của mình về chúng. Mình hy vọng rằng qua những ví dụ cụ thể này, chúng ta có thể cùng nhau nhìn nhận lại cách chúng ta đang làm việc và có thể tìm ra cách để cân bằng giữa việc áp dụng công nghệ mới và giữ vững những nguyên tắc cơ bản.

1. Sự phức tạp hóa vô tận

Hãy xem xét một ví dụ về component Modal trong React. Đây là một component phổ biến và thường được sử dụng trong nhiều ứng dụng web. Ban đầu, chúng ta có thể có một component Modal đơn giản như sau:

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <button className="modal-close" onClick={onClose}>
          X
        </button>
        {children}
      </div>
    </div>
  );
};

Component này hoạt động tốt trong hầu hết các trường hợp. Tuy nhiên, một ngày nọ, một yêu cầu mới xuất hiện: "Chúng ta cần animation khi mở và đóng modal". Một dev trong team đã đề xuất giải pháp sau:

const Modal = ({ isOpen, onClose, children }) => {
  const [isAnimating, setIsAnimating] = useState(false);
  const modalRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      setIsAnimating(true);
      modalRef.current.classList.add('modal-enter');
      setTimeout(() => {
        modalRef.current.classList.remove('modal-enter');
        modalRef.current.classList.add('modal-enter-active');
      }, 10);
    } else {
      modalRef.current.classList.add('modal-exit');
      setTimeout(() => {
        modalRef.current.classList.remove('modal-exit');
        modalRef.current.classList.add('modal-exit-active');
        setTimeout(() => {
          setIsAnimating(false);
        }, 300);
      }, 10);
    }
  }, [isOpen]);

  if (!isOpen && !isAnimating) return null;

  return (
    <div className="modal-overlay" ref={modalRef}>
      <div className="modal-content">
        <button className="modal-close" onClick={onClose}>
          X
        </button>
        {children}
      </div>
    </div>
  );
};

Giải pháp này hoạt động, nhưng nó đã biến một component đơn giản thành một mớ hỗn độn của các state, effect, và setTimeout. Nó khó đọc, khó bảo trì, và dễ gây ra lỗi.

Trong quá trình code review, một dev khác đề xuất sử dụng thư viện animation như react-spring để giải quyết vấn đề. Tuy nhiên, điều này lại thêm một dependency mới vào project và tăng kích thước bundle.

Khi được hỏi ý kiến, mình đã đề xuất một giải pháp đơn giản hơn nhiều:

const Modal = ({ isOpen, onClose, children }) => {
  return (
    <div className={`modal-overlay ${isOpen ? 'is-open' : ''}`}>
      <div className="modal-content">
        <button className="modal-close" onClick={onClose}>
          X
        </button>
        {children}
      </div>
    </div>
  );
};

Và CSS:

.modal-overlay {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.modal-overlay.is-open {
  opacity: 1;
  visibility: visible;
}

.modal-content {
  transform: translateY(-20px);
  transition: transform 0.3s ease;
}

.modal-overlay.is-open .modal-content {
  transform: translateY(0);
}

Giải pháp này không chỉ đơn giản hơn nhiều, mà còn dễ đọc, dễ bảo trì, và hiệu quả. Nó sử dụng CSS transitions để xử lý animation, không cần thêm bất kỳ state hoặc effect phức tạp nào.

Đây là một ví dụ điển hình về cách chúng ta thường có xu hướng phức tạp hóa vấn đề và quên đi những giải pháp đơn giản, hiệu quả mà các công nghệ cơ bản như CSS có thể cung cấp.

Các dev đã quá quen với việc giải quyết mọi vấn đề bằng JavaScript và React hooks đến nỗi họ quên mất rằng CSS có thể xử lý animations một cách đơn giản và hiệu quả. Đây là một ví dụ về việc chúng ta đã quên mất những kiến thức cơ bản về frontend như thế nào.

2. Những sai lầm kinh điển

Trong quá trình phát triển web, chúng ta thường gặp phải những sai lầm cơ bản mà đôi khi chính bản thân chúng ta cũng không nhận ra. Hãy cùng xem xét một ví dụ điển hình về việc xử lý layout responsive.

Giả sử chúng ta cần tạo một grid layout linh hoạt, có thể thay đổi số cột dựa trên kích thước màn hình. Nhiều developer sẽ nghĩ ngay đến việc sử dụng JavaScript để tính toán và điều chỉnh layout:

function adjustGrid() {
  const container = document.querySelector('.grid-container');
  const items = container.querySelectorAll('.grid-item');
  const containerWidth = container.offsetWidth;
  const itemWidth = 200; // Giả sử mỗi item có chiều rộng 200px
  const columns = Math.floor(containerWidth / itemWidth);
  
  items.forEach((item, index) => {
    item.style.left = `${(index % columns) * itemWidth}px`;
    item.style.top = `${Math.floor(index / columns) * itemWidth}px`;
  });
}

window.addEventListener('resize', adjustGrid);
adjustGrid();

Đoạn code trên sẽ hoạt động, nhưng nó có nhiều vấn đề:

  1. Hiệu suất kém: Mỗi khi cửa sổ thay đổi kích thước, hàm này sẽ được gọi, gây ra nhiều tính toán không cần thiết.
  2. Khó bảo trì: Nếu muốn thay đổi kích thước item hoặc thêm các breakpoint mới, chúng ta phải sửa đổi code JavaScript.
  3. Không tận dụng được sức mạnh của CSS hiện đại.

Thay vào đó, chúng ta có thể sử dụng CSS Grid để đạt được kết quả tương tự một cách đơn giản và hiệu quả hơn nhiều:

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}

Đoạn CSS này sẽ tự động điều chỉnh số cột dựa trên kích thước container, mà không cần bất kỳ JavaScript nào. Nó cũng linh hoạt hơn, dễ bảo trì hơn và hiệu suất tốt hơn.

Đây chỉ là một ví dụ về cách chúng ta thường quên đi sức mạnh của CSS và overengineer giải pháp bằng JavaScript. Trong nhiều trường hợp, một vài dòng CSS có thể thay thế hàng chục dòng JavaScript phức tạp.

3. Cội nguồn của mọi tội lỗi

Một trong những nguyên nhân chính dẫn đến việc chúng ta quên đi những điều cơ bản là xu hướng "hype-driven development". Chúng ta thường bị cuốn theo những công nghệ mới nhất, những framework hot nhất mà quên đi rằng đôi khi, giải pháp đơn giản nhất lại là tốt nhất.

Hãy xem xét ví dụ về việc quản lý state trong một ứng dụng web đơn giản. Nhiều developer sẽ ngay lập tức nghĩ đến việc sử dụng một thư viện quản lý state phức tạp như Redux:

// actions.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

// reducer.js
const initialState = { count: 0 };

export default function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, count: state.count + 1 };
    case DECREMENT:
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

// Component.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

Đoạn code trên sử dụng Redux để quản lý một state đơn giản là một số đếm. Mặc dù nó hoạt động tốt, nhưng nó quá phức tạp cho một tác vụ đơn giản như vậy. Thay vào đó, chúng ta có thể sử dụng React's built-in useState hook:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

Đoạn code này ngắn gọn hơn, dễ hiểu hơn và hoàn toàn đủ cho nhu cầu của chúng ta. Nó cũng giúp giảm kích thước bundle và cải thiện hiệu suất.

Điều này không có nghĩa là Redux là xấu hoặc không nên sử dụng. Nó chỉ đơn giản là một ví dụ về việc chúng ta thường overengineer giải pháp của mình mà quên đi những công cụ đơn giản nhưng mạnh mẽ mà chúng ta đã có sẵn.

4. Quên mất accessibility

Một trong những khía cạnh quan trọng nhất của frontend development mà chúng ta thường quên là accessibility (khả năng tiếp cận). Trong quá trình tạo ra các giao diện phức tạp và đẹp mắt, chúng ta thường bỏ qua việc đảm bảo rằng trang web của chúng ta có thể được sử dụng bởi tất cả mọi người, bao gồm cả những người khuyết tật.

Ví dụ, hãy xem xét một modal dialog đơn giản:

<div class="modal">
  <h2>Xác nhận đặt hàng</h2>
  <p>Bạn có chắc chắn muốn đặt hàng không?</p>
  <button onclick="confirmOrder()">Đồng ý</button>
  <button onclick="closeModal()">Hủy</button>
</div>

Mặc dù đoạn code này có vẻ ổn, nhưng nó có một số vấn đề về accessibility:

  1. Không có cách nào để đóng modal bằng bàn phím (ví dụ: phím Esc).
  2. Focus không được quản lý đúng cách khi modal mở.
  3. Screen readers không biết đây là một dialog.

Chúng ta có thể cải thiện nó như sau:

<div class="modal" role="dialog" aria-labelledby="modal-title" aria-describedby="modal-description">
  <h2 id="modal-title">Xác nhận đặt hàng</h2>
  <p id="modal-description">Bạn có chắc chắn muốn đặt hàng không?</p>
  <button onclick="confirmOrder()">Đồng ý</button>
  <button onclick="closeModal()">Hủy</button>
</div>
function openModal() {
  const modal = document.querySelector('.modal');
  const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const firstFocusableElement = focusableElements[0];
  const lastFocusableElement = focusableElements[focusableElements.length - 1];

  modal.style.display = 'block';
  firstFocusableElement.focus();

  modal.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
      closeModal();
    }
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === firstFocusableElement) {
          lastFocusableElement.focus();
          e.preventDefault();
        }
      } else {
        if (document.activeElement === lastFocusableElement) {
          firstFocusableElement.focus();
          e.preventDefault();
        }
      }
    }
  });
}

Những thay đổi này giúp cải thiện đáng kể khả năng tiếp cận của modal:

  1. Thêm các thuộc tính ARIA giúp screen readers hiểu đây là một dialog.
  2. Quản lý focus khi modal mở, giúp người dùng bàn phím có thể điều hướng dễ dàng.
  3. Thêm khả năng đóng modal bằng phím Esc.

Đây chỉ là một ví dụ nhỏ về cách chúng ta có thể cải thiện accessibility. Trong thực tế, có rất nhiều khía cạnh khác cần được xem xét, như contrast ratio, alt text cho hình ảnh, semantic HTML, v.v.

5. Lạm dụng JavaScript thay vì chú trọng vào CSS và HTML cơ bản

Trong cơn bão của các framework JavaScript phức tạp, nhiều lập trình viên frontend có xu hướng giải quyết mọi vấn đề bằng JavaScript, ngay cả khi có các giải pháp đơn giản hơn bằng CSS hoặc HTML thuần túy. Hãy xem xét ví dụ sau về một modal đơn giản:

const Modal = ({ isOpen, onClose, children }) => {
  const [modalPosition, setModalPosition] = useState({ top: 0, left: 0 });

  useEffect(() => {
    const updatePosition = () => {
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;
      const modalWidth = 300;
      const modalHeight = 200;

      setModalPosition({
        top: (windowHeight - modalHeight) / 2,
        left: (windowWidth - modalWidth) / 2
      });
    };

    window.addEventListener('resize', updatePosition);
    updatePosition();

    return () => window.removeEventListener('resize', updatePosition);
  }, []);

  if (!isOpen) return null;

  return (
    <div 
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
      }}
    >
      <div 
        style={{
          position: 'absolute',
          top: `${modalPosition.top}px`,
          left: `${modalPosition.left}px`,
          width: '300px',
          padding: '20px',
          backgroundColor: 'white',
        }}
      >
        {children}
        <button onClick={onClose}>Đóng</button>
      </div>
    </div>
  );
};

Trong ví dụ trên, chúng ta đã sử dụng JavaScript để tính toán vị trí của modal và cập nhật nó khi cửa sổ thay đổi kích thước. Tuy nhiên, đây là một cách tiếp cận phức tạp và không cần thiết. Thay vào đó, chúng ta có thể đạt được kết quả tương tự bằng cách sử dụng CSS hiện đại:

const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
        <button onClick={onClose}>Đóng</button>
      </div>
    </div>
  );
};
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  width: 300px;
  padding: 20px;
  background-color: white;
}

Giải pháp CSS này không chỉ ngắn gọn hơn mà còn hiệu quả hơn về mặt hiệu suất. Nó tận dụng các tính năng CSS hiện đại như flexbox để căn giữa modal mà không cần JavaScript.

Ví dụ này minh họa rõ ràng cách mà việc tập trung quá mức vào JavaScript có thể dẫn đến việc bỏ qua những giải pháp đơn giản và hiệu quả hơn bằng CSS. Là một lập trình viên frontend, việc nắm vững CSS và HTML là rất quan trọng để tạo ra các giao diện người dùng hiệu quả và dễ bảo trì.

Bằng cách sử dụng CSS và HTML một cách thông minh, chúng ta có thể:

  1. Giảm độ phức tạp của code
  2. Cải thiện hiệu suất trang web
  3. Tăng khả năng bảo trì và mở rộng
  4. Tận dụng các tính năng tích hợp sẵn của trình duyệt

Vì vậy, thay vì luôn nghĩ đến JavaScript như một giải pháp đầu tiên, hãy xem xét liệu CSS hoặc HTML có thể giải quyết vấn đề một cách đơn giản và hiệu quả hơn không. Điều này không chỉ giúp cải thiện chất lượng code của bạn mà còn giúp bạn trở thành một lập trình viên frontend toàn diện hơn.

6. Kết luận: Một vài lời khuyên quan trọng

Tình hình này có vẻ thực sự buồn. Các dev bắt đầu quên đi những công nghệ cơ bản và có xu hướng mất đi tư duy phản biện giữa tất cả các công nghệ và phương pháp tiếp cận mới.

Tuy nhiên, theo ý kiến của mình, đây không phải là một vấn đề quá khó để giải quyết. Để kết luận tất cả những điều trên, mình muốn đưa ra những điểm đơn giản sau. (Hãy cho mình phản hồi của bạn về chúng nhé!)

  1. Dành thời gian và hiểu rõ JavaScript thuần. Có một nền tảng vững chắc giúp dễ dàng phát hiện nguyên nhân thực sự đằng sau các lỗi và sửa chúng một cách phù hợp.

  2. Học HTML và CSS một cách sâu sắc. Bạn có thể khám phá rất nhiều thuộc tính, bộ chọn và những thứ hữu ích khác có thể thay thế hàng tấn code JavaScript. Chỉ cần nhớ lại ví dụ với bộ chọn :empty.

  3. Phát triển kỹ năng tư duy phản biện của bạn. Tất nhiên, trưởng nhóm của bạn đã dạy bạn một số nguyên tắc và thực hành tốt nhất định. Tuy nhiên, bạn không thể mù quáng tuân theo chúng vì nó sẽ chỉ dẫn bạn đi sai hướng. Thay vào đó, hãy cố gắng hiểu tại sao một thứ lại như thế này chứ không phải như thế kia.

  4. Nhớ về SOLID, YAGNI, KISS và các nguyên tắc khác. Nếu một nhiệm vụ đơn giản trở thành một cơn ác mộng với một giải pháp khó hiểu - hãy dừng lại và suy nghĩ lại từ một góc độ khác. Có lẽ bạn đã đi quá sâu vào một giải pháp và quên mất điều gì đó hiển nhiên.

Cảm ơn bạn đã đọc!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.