Cải tiến code JavaScript trong WordPress

Thời gian qua mình đã có một trải nghiệm thú vị về việc cải tiến một mã nguồn JavaScript lớn của một plugin WordPress. Phần code ban đầu của plugin này được viết theo phong cách cũ với jQuery trong một file lớn. Bằng việc sử dụng ES6 và Webpack, mình đã có thể tách nó thành nhiều module và cải thiện cấu trúc code. Giờ đây, code mới trở nên dễ đọc, dễ duy trì và tất nhiên, có ít bug hơn. Trong bài viết này, mình sẽ hướng dẫn các bạn cách làm của mình.

Code được viết theo kiểu cũ

Trước khi đi vào các bước tái cấu trúc code, cùng nhìn lại vào code JavaScript thông thường trong WordPress.

Chúng ta đã quá quen thuộc với những dòng code này trong plugin và theme WordPress:

jQuery( function( $ ) {
    // Many variables go here.
    var $element = $( '.some-selector' );

    // A large code goes here.
    $element.each( function() {
        // Do something.
    } )

    // Many functions go here.
    function myFunc {
        // Some code.
    }
 
    myFunc();
} );

Mình liên tục gặp những dòng code này và thường xuyên tự viết chúng. File JavaScript này chỉ giống như những nhóm code được chạy theo thứ tự. Logic ở trong file cũng giống như những gì chúng ta nghĩ ra. Nói chung thì không có vấn đề gì cả. Code vẫn ổn và chạy bình thường.

Tuy nhiên thì sẽ có một vấn đề lớn khi mà code ngày càng nhiều, file JS sẽ lớn hơn và sẽ khó để đọc và bảo trì hơn. Đôi khi, bạn thậm chí còn không biết phải nhìn vào đâu để tìm bug.

Vậy thì chúng ta nên làm gì?

Nhóm logic vào các hàm hoặc đối tượng

Có một số giải pháp cho vấn đề trên. Cách thông dụng mà mình thường thấy (và cũng thường làm) là nhóm logic vào các hàm. Đoạn code dưới đây mô tả ý tưởng đó. Cấu trúc của nó tốt hơn và dễ đọc hơn:

( function( $ ) {
    function a() {
        // Some code.
    }

    function b {
        // Some code.
    }

    // Run when document is ready.
    $( function() {
        a();
        b();
    } );
} )( jQuery );

Vậy tại sao code này lại tốt hơn? Bởi vì nó tách logic thành các hàm và làm file JavaScript của bạn giống như một tập hợp các hàm. Mỗi một logic nghiệp vụ được lưu trữ ở một hàm và chúng đều tách rời nhau. Cho nên khi có bug, bạn sẽ dễ dàng biết mình cần tìm ở đâu để sửa lỗi.

Ngoài ra, có rất nhiều cách tương tự, như là thay vì viết nhiều hàm, bạn có thể nhóm các hàm tương tự vào một đối tượng như này:

( function( $ ) {
    var Tabs = {
        open: function () {
            // Some code.
        },
        close: function() {
            // Some code.
        }
    }

    function b {
        // Some code.
    }

    // Run when document is ready.
    $( function() {
        Tabs.open(); // Or Tabs.close();
        b();
    } );
} )( jQuery );

Lưu ý: bạn có thể nhận thấy rằng viết một đối tượng như này bằng JavaScript rất giống với một class tĩnh bằng PHP nhỉ?

Mặc dù những cách thức này đều hoạt động và giúp code dễ đọc hơn, chúng lại không giúp giải quyết vấn đề khi bạn có một mã nguồn lớn. Bạn hãy thử tưởng tượng xem, nếu bạn có một file JavaScript 10 nghìn dòng, bạn sẽ rất rất khó để sửa lỗi ở file đó. Và sẽ ra sao nếu hai hoặc nhiều developer cùng xử lý nhiều bug khác nhau? Họ sẽ phải xử lý cùng một file lớn đúng không? Quả thật là một cơn ác mộng khi phải gộp code vào với nhau.

Tách code thành nhiều file

Có một cách thông dụng để loại bỏ một file JavaScript đơn lớn đó là tách nó thành nhiều file JavaScript nhỏ hơn và chèn chúng vào WordPress như thế này:

wp_enqueue_script( 'one', plugin_dir_url( __FILE__ ) . '/js/one.js', ['jquery'], '1.0', true );
wp_enqueue_script( 'two', plugin_dir_url( __FILE__ ) . '/js/two.js', ['one'], '1.0', true );
wp_enqueue_script( 'three', plugin_dir_url( __FILE__ ) . '/js/three.js', ['one', 'two'], '1.0', true );

Code trong từng file bây giờ đã ít hơn trước. Tuy nhiên bạn vẫn sẽ gặp phải một vấn đề nếu như bạn chèn nhiều file JavaScript vào front end, bởi vì nó có thể sẽ làm giảm hiệu suất của trang web, ví dụ như tốc độ tải trang.

Code này sẽ khiến trình duyệt tạo 3 request đến server khi tải trang và tất cả đều là render-blocking. Vì vậy, trang web của bạn sẽ chậm hơn một chút so với việc chỉ có một yêu cầu.

Bây giờ thì vấn đề cần giải quyết đơn giản hơn trước, bạn chỉ cần tìm cách tách code trong khi vẫn chỉ tạo một yêu cầu duy nhất.

May mắn rằng chúng ta có thể làm điều đó với các module trong EcmaScript và Webpack một cách đơn giản.

Module

Cùng quay trở lại với file lớn mà chúng ta có. Bước đầu tiên là hãy tách code thành những module nhỏ hơn.

Trong trường hợp của mình, mình đã làm với MB Views, một extension của plugin Meta Box. Ở trên màn hình chỉnh sửa, có những phần sau:

  • Modals
  • Tabs
  • Inserter panel
  • Code editor
  • Location rules

Đây là ảnh chụp màn hình nơi bạn có thể thấy một modal, inserter panel (ở hình nền phía sau) và code editor.

Chèn Map setting

Trước đó, mình đã viết mỗi phần trên như một đối tượng với phương thức của chúng, giống như cách tiếp cận thứ hai minh có nói ở trên. Tuy nhiên, việc này khiến mình phải code nhiều hơn dự kiến. Vậy nên, mình bắt đầu di chuyển code của mỗi thứ phần một file JavaScript tách biệt và cấu trúc chúng như những module.

Đây là một ví dụ của module Modals. Code này được đặt trong file modals.js

const modals = [...document.querySelectorAll( '.mbv-modal' )];

const Modals = {
    bindEvents: () => document.addEventListener( 'click', Modals.clickHandle ),
    clickHandle: e => {
        const el = e.target;

        if ( el.parentNode.classList.contains( 'mbv-modal-trigger' ) ) {
            e.preventDefault();
            Modals.openTarget( el.parentNode );
        }
        if ( el.classList.contains( 'mbv-modal__overlay' ) ) {
            e.preventDefault();
            Modals.closeCurrent( el );
        }
        if ( el.classList.contains( 'mbv-modal__close' ) ) {
            e.preventDefault();
            Modals.closeCurrent( el );
        }
    },
    closeCurrent: el => el.closest( '.mbv-modal' ).classList.add( 'hidden' ),
    openTarget: el => {
        Modals.closeAll();
        Modals.open( el.dataset.modal );
    },
    open: name => modals.find( modal => modal.dataset.modal === name ).classList.remove( 'hidden' ),
    closeAll: () => modals.forEach( modal => modal.classList.add( 'hidden' ) ),
}

export default Modals;

Và ở phần file index.js chính, mình nhập module như sau:

import Modals from './modals.js';
// Import more modules.

Modals.bindEvents();
// Other code.

Như bạn thấy, code của module này rất giống với code thường khi bạn tách nó bằng kiểu cũ thông thường. Điểm khác biệt duy nhất chính là từ khóa export, nơi mà bạn xuất những thứ bạn cần sang một file khác.

Module là một chuẩn trong EcmaScript 2015 (ES6). Việc viết code như này có nghĩa là chúng ta đang sử dụng JavaScript kiểu mới trong plugin của mình. Để hiểu hơn về module ES6, bạn có thể tham khảo bài viết này.

Module còn được gọi là export/import ở ES6 bởi đó là cách mà các module giao tiếp với nhau. Toàn bộ những ý tưởng của module ES6 như sau:

  • Bạn có thể viết mọi thứ thành module
  • Code cho mỗi module được lưu trữ ở một file riêng lẻ
  • Ở trong một module, bạn chỉ cần xuất những gì bạn cần như là biến, hàm, đối tượng,hoặc lớp, … bất cứ thứ gì bạn cần.
  • Ở trong file JavaScript chính, bạn chỉ cần nhập những gì bạn cần và sử dụng nó. Bạn cũng có thể đổi tên hàm được nhập, biến (trừ việc xuất mặc định), vậy nên chúng không gây trùng các biến global khác.

Điều hay ho về các module chính là việc ở file module, bạn có thể viết bất cứ thứ gì bạn muốn, không chỉ những thứ được xuất. Bạn có thể khai báo biến tạm thời hoặc hàm helper và chạy chúng bằng một hàm bạn sẽ xuất đi. Những biến tạm thời và hàm helper này sẽ không được xuất và không có sẵn trong file JavaScript chính (nơi bạn nhập).

Trong ví dụ trên, mình đã viết những những modal thành một module. Trong file đó, mình đã tạo một đối tượng Modals mà mình đã xuất. Cũng có một biến tạm thời modals mà mình đã dùng để lưu trữ tất cả các yếu tố của modal. Biến này sẽ không được xuất và không có ở trong file JavaScript chính.

Webpack

Module cho phép chúng ta tách code thành những phần nhỏ hơn, giúp ích rất nhiều trong việc bảo trì chúng. Tuy vậy, để làm những module (xuất và nhập) hoạt động, chúng ta cần một gói các công cụ như là Webpack.

Webpack là một công cụ mạnh để đóng gói, gộp nhiều file JavaScript thành một file. Do vậy, nó làm điều ngược lại của việc tách code.

Giả sử chúng ta có những module sau với nội dung tương tự với phần trước.

  • modals.js
  • tabs.js
  • Panel.js

Chúng ta cũng có một file index.js chính với nội dung như sau:

import Modals from './modals.js';
import Panels from './panels.js';
import Tabs from './tabs.js';

Modals.bindEvents();
Panels.bindEvents();
Tabs.bindEvents();

Để làm cho file index.js hoạt động trong trình duyệt, chúng ta cần Webpack để đóng gói tất cả các module vào thành file và biên soạn chúng thành ES5. Kết quả đạt được là một file như bundle.js bao gồm nội dung của tất cả các file ở trên (trong khi vẫn giữ được logic của việc xuất/nhập, hay nói ngắn một cách ngắn gọn, chúng vẫn hoạt động).

Webpack giúp giải quyết các vấn đề của việc gửi nhiều yêu cầu đến server với nhiều wp_enqueue_script mà mình đề cập ở trên.

Để bắt đầu với Webpack, mình có một lời khuyên mà mình rất muốn các bạn làm theo đó là đọc tài liệu chính thức hướng dẫn bắt đầu sử dụng của công cụ này. Bởi vì khi tìm các bài hướng dẫn trên mạng, bạn có thể bị bối rối bởi nhiều thứ phức tạp, nhất là việc cài đặt trong khi nó không cần thiết. Mọi thứ thậm chí còn phức tạp hơn khi bạn đọc hướng dẫn về cả Webpack và Babel! Babel là một công cụ khác giúp chuyển code JavaScript kiểu mới thành ES5 cho trình duyệt hiểu.

Trong trường hợp của mình, mình quyết định bỏ hỗ trợ cho IE. Điều đó có nghĩa là mình có thể thoải mái sử dụng:

  • Spread operator
  • letconst
  • Hàm arrow
  • Các lớp

Và nhiều thứ khác nữa. Giống như những code mẫu ở trên, nó hoạt động tốt trên mọi trình duyệt.

Do đó, mình quyết định không dùng Babel mà chỉ dùng Webpack để đóng gói code. Vậy nên quá trình cài đặt của mình trở nên vô cùng đơn giản như sau:

Đầu tiên, mình cài Webpack với npm install webpack webpack-cli --save-dev. Sau đó, mình có một file package.json với nội dung sau:

{
    "devDependencies": {
        "webpack": "^4.41.6",
        "webpack-cli": "^3.3.11"
    }
}

Sau, đó mình đã tạo một file cấu hình cho Webpack webpack.config.js với nội dung:

const path = require('path');

module.exports = {
    devtool: '',
    entry: './assets/js/index.js',
    output: {
        filename: 'bundle.js',
    path: path.resolve(__dirname, 'assets'),
    }
};

Xong rồi đó! Quá trình cài đặt đã được tối giản và tiêu chuẩn. Không có gì cầu kì cả. Bạn có thể hiểu file cấu hình Webpack một cách dễ dàng: nó lấy tệp chính index.js và xuất ra tệp bundle.js.

Để chạy Webpack, mình đơn giản chỉ cần chạy lệnh webpack từ terminal và gộp các file thành file bundle.js.

Sau đó, những gì mình cần làm là chỉ cần chèn file đó vào plugin WordPress của mình:

wp_enqueue_script( 'bundle', plugin_dir_url() . '/js/bundle.js', ['jquery'], '1.0', true );

jQuery?

Một trong số những vấn đề phổ biến nhất khi chuyển đổi code kiểu cũ thành code kiểu mới đó là làm sao để viết jQuery code trong module hay có cách nào để phát hiện khi văn bản sẵn sàng, v …v …

Câu hỏi này đã xuất hiện trong đầu mình nhiều lần khi mình đang code. Đầu tiên, mình cố loại bỏ jQuery code. Như bạn thấy ở module Modals trên, hoàn toàn không có jQuery luôn! Mọi thứ đều là JavaScript thuần. Bạn có thể làm nhiều thứ với JavaScript thuần, ví dụ như:

  • Duyệt qua DOM
  • Thêm các sự kiện với el.addEventListener
  • Tìm các phần tử với el.querySelector
  • Thêm hoặc xóa các lớp CSS bằng el.classList
  • Ajax với fetch

Bạn có thể tham khảo website You might not need jQuery để có thêm hướng dẫn cực kỳ hữu ích. Gần đây, mình đã tìm được một hướng dẫn tốt hơn và mới cập nhập hơn với rất nhiều ví dụ tại HTML DOM. Sử dụng những kỹ thuật này, bạn có thể gần như làm 90% công việc cần làm với jQuery.

Nhưng nếu như bạn thật sự rất cần jQuery thì sao? Điều đó không hề khó như bạn nghĩ. Mọi thứ rất đơn giản: cứ sử dụng nó thôi!

Giả sử như bạn đã khai báo jQuery như một phần phụ thuộc khi bạn chèn tệp bundle.js như sau:

wp_enqueue_script( 'bundle', plugin_dir_url() . '/js/bundle.js', ['jquery'], '1.0', true );

Sau đó trong file JavaScript (cả ở modules và file chính), bạn có thể dùng jQuery như này:

import Tabs from './tabs.js';

const $ = jQuery;

let cssSettings = $.extend( true, {}, wp.codeEditor.defaultSettings );
cssSettings.codemirror.mode = 'css';
cssSettings.codemirror.theme = 'oceanic-next';
const cssEditor = wp.codeEditor.initialize( 'mbv-post-excerpt', cssSettings ).codemirror;
cssEditor.setSize( null, '480px' );

Tabs.bindEvents();

Đoạn code này được lấy từ MB Views, nơi mà mình cần dùng CodeMirror cho editor. Thư viện CodeMirror trong WordPress được chứa trong đối tượng wp.codeEditor với nhiều cài đặt khác nhau và mình cần sử dụng hàm extend từ jQuery để tạo một bản sao của đối tượng. Mình có thể sử dụng một thư viện thuần JavaScript khác, nhưng khi jQuery có sẵn thì tại sao mình lại không sử dụng nó? Nó cũng giảm kích thước của tệp bundle.js Nữa đấy!

Bên cạnh đấy thì mình cũng gán jQuery cho biến local $ mà chỉ có sẵn trên module. Mình cho rằng mẹo này khá là hay bởi bạn không cần lặp lại việc viết jQuery mọi lúc, chỉ cần dùng $ là đủ.

Bạn có thể sử dụng nó như thường mà không cần cài đặt gì cả.

Lời cuối

Với module ES6 và Webpack, mình đã có thể:

  • Tách code thành những file nhỏ hơn để có cấu trúc tốt hơn, cũng như là dễ đọc và duy trì.
  • Nâng cao hiệu suất bằng cách chèn chỉ một file JavaScript đơn lẻ
  • Viết code JavaScript kiểu mới nhờ vào ES6
  • Làm việc với jQuery khi thực sự cần

Kết quả của việc này đó là mình đã có một bộ các file JavaScript nhỏ nơi mình có thể dễ dàng tìm bug. Nó cũng đã giúp team mình làm việc trong cùng một dự án mà mỗi người làm về một tính năng riêng với code của mỗi người ở một file riêng.

Với việc bắt đầu một dự án về Gutenberg vài năm trước, mình đã cảm nhận được một luồng gió mới ở code WordPress, cả PHP và JavaScript. Việc cải tiến code không chỉ là một câu hỏi “có” hoặc “không” nữa. Nó chỉ là “như thế nào” mà thôi. Mình hy vọng rằng bài hướng dẫn này sẽ giúp bạn phần nào trả lời câu hỏi đó.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *