Search…

RequireJS cho Người Mới Bắt Đầu

23/09/202018 min read
Khái niệm, chức năng, cách module hoá JavaScript, cách tạo module với RequireJS và cách ứng dụng vào dự án

Bài viết cung cấp những kiến thức cơ bản về RequireJS, trả lời được những câu hỏi như: "RequireJS là gì?", "Tại sao phải sử dụng RequireJS?", "Cách module hóa JavaScript?", "Làm thế nào để tạo module với RequireJS và ứng dụng nó vào dự án?", ...

RequireJS là gì?

logo

RequireJS (https://requirejs.org) là một thư viện JavaScript dùng để cải thiện tốc độ thực thi và tối ưu source code được viết bằng JavaScript nhờ việc áp dụng tư tưởng module hóa, nó cũng là một trong những thành phần chính có trong framework DurandalJS(1) – dùng để tạo single-page aplication (SPA). Tiếp cận RequireJS giúp có được những khái niệm cơ bản từ đó dễ dàng tìm hiểu các framework khác như Angular, ReactJS, ...

Để hiểu RequireJS là gì cách tốt nhất nên xem qua những lợi ích mà nó mang lại cũng như cách thức để hiện thực nó.

Tại sao phải sử dụng RequireJS?

Dưới đây là các các công dụng mà RequireJS mang lại:

  • Modules hóa
  • Optimizer
  • Testing

Modules hóa

Với phong cách xây dựng website truyền thống, khi muốn thực thi JavaScript, dùng thẻ <script> nhúng vào trong phần head hoặc cuối phần body.

<script src="../assets/js/libs/jquery.js"></script>
<script src="../assets/js/libs/bootstrapjs"></script>
<script src="../assets/js/components/zoom-image.js"></script>
<script src="../assets/js/components/search-products.js"></script>
<script src="../assets/js/main.js"></script>

Phương pháp này phù hợp với các hệ thống website vừa và nhỏ, nếu muốn áp dụng cho hệ thống phức tạp thì sẽ gặp nhiều khó khăn như:

  • Sự phụ thuộc (dependencies): Mặc định trình duyệt sẽ thực thi các file .js một cách đồng bộ (async = false) theo trình tự, tức là file script nào được viết trước thì sẽ được thực thi hoàn chỉnh rồi đến file kế tiếp. Như vậy, với đoạn code trên, khi muốn dùng Bootstrap thì phải thực thi jQuery trước đó. Xét trường hợp file main.js có sử dụng lại các phương thức đã được định nghĩa trong zoom-image.js, thì zoom-image.js phải được đặt trước file main.js. Và cứ thế, khi có nhiều script, phải ghi nhớ thứ tự để đặt các file một cách chính xác. Điều này sẽ gây ra nhiều khó khăn khi mở rộng hoặc muốn điều chỉnh tính năng.
  • Tải các script không cần thiết: Khi hệ thống có nhiều trang mỗi trang thực hiện các tính năng riêng biệt, sẽ cần nhiều file JavaScript để xử lý riêng phù hợp cho từng trang. Cách xử lý truyền thống như trên sẽ bị tình trạng tốn nhiều tài nguyên tải (bandwidth) và thực thi (ram) các script không cần thiết. Xét ví dụ bên trên, tại trang chi tiết sản phẩm sẽ có file zoom-image.js xử lý việc zoom ảnh đại diện cho sản phẩm, còn file search-products.js xử lý các tác vụ ở trang tìm kiếm. Hai file này hoạt động riêng lẻ không ảnh hưởng lẫn nhau nên việc tải và xử lý zoom-image.js ở trang tìm kiếm hay ngược lại là không cần thiết.

Để giải quyết các vấn đề này và giúp code tối ưu hơn nữa, khái niệm module hóa JavaScript (tách các chương trình JavaScript và đóng gói chúng thành các module riêng biệt) được ra đời. Một số thư viện hỗ trợ việc module hóa JavaScript như: CommonJS, AMD (Asynchronous Module Definitions), Webpack, Babel. RequireJS sử dụng phương pháp AMD.

Việc hiện thực AMD có nhiều lợi ích:

  • Cải tiến hiệu suất website: Bằng việc chia nhỏ thành nhiều file JavaScript thực hiện các chức năng riêng lẻ, chỉ tải và thực thi những script nào thật sự cần thiết.
  • Hạn chế lỗi: Cho phép quản lý sự phụ thuộc giữa các file JavaScript một cách dễ dàng, giảm tối đa việc gọi thực thi các thuộc tính hay các phương thức mà chưa được định nghĩa.
  • Dễ sửa lỗi, cập nhật và duy trì: Việc chia nhỏ các chức năng thành từng module sẽ có rất nhiều thuận lợi cho việc xác định, thu hẹp và vá các lỗi về logic. Dễ dàng thêm mới tính năng mà không làm ảnh hưởng đến cả hệ thống. Khi một module bị lỗi thì các module sử dụng nó sẽ không được thực thi, chương trình một phần nào sẽ được duy trì “ổn định”.

Bài viết sẽ tìm hiểu cách module hóa code JavaScript với RequireJS bằng việc áp dụng các phương pháp mà RequireJS có thể hỗ trợ (AMD, CommonJS).

Optimizer

Tối ưu hóa là một tính năng cũng khá hữu ích của RequireJS dành cho các hệ thống lớn khi có nhiều tập tin JavasSript và CSS:

  • Đối với JavaScript: RequireJS hỗ trợ kết hợp (combine) những đoạn liên quan với nhau thành từng file rồi tự động nén chúng lại (minify). Điều này giúp giảm băng thông do chỉ cần tải một vài tập tin cần thiết.
  • Đối với CSS: Tối ưu bằng cách tự động minify (xóa khoảng trắng và các chú thích) giúp giảm dung lượng, tăng tốc độ tải trang.

Testing

RequireJS có thể tương thích với nhiều Javascript Testing Frameworks như BusterJS, Mocha, NodeUnit, QUnit và Jasmine nhờ vậy việc kiểm thử tính đúng đắn của chương trình được diễn ra một cách thuận lợi hơn.

Tóm lại, RequireJS sẽ giúp:

  • Chia nhỏ thành các module JavaScript, dễ dàng quản lý sự phụ thuộc và mối liên hệ giữa chúng
  • Cải thiện đáng kể tốc độ cũng như tối ưu các đoạn code JavaScript
  • Dễ dàng kiểm thử chương trình

Cách module hóa sử dụng RequireJS

Xét một ví dụ đơn giản, sử dụng thư viện jQuery và Bootstrap để xây dựng một trang có hiệu ứng tooltip khi hover qua button.

Thêm file xử lý của RequireJS (require.js)

| index.html
|
\---assets
    +---css
    |       bootstrap.css
    |
    \---js
        |   main.js
        |
        +---components
        |       tooltip-button.js
        |
        \---libs
                bootstrap.js
                jquery.js
                require.js

Ở phần này, cho thấy được kết quả khác biệt giữa phong cách code truyền thống so với việc áp dụng thư viện RequireJS để tối ưu.

File index.html (cách code truyền thống)

<!DOCTYPE html>
<head>
    <link rel="stylesheet" href="./assets/css/bootstrap.css">
</head>
<body>
    <div class="container">
        <h1>Tradition Sample Page: on hover show tooltip button</h1>
        <button class="btn btn-default" data-toggle="tooltip" title="Tooltip on top">
            Hover me
        </button>
    </div>

    <!-- code something -->
<script src="./assets/js/libs/jquery.js"></script> <script src="./assets/js/libs/bootstrap.js"></script> <script src="./assets/js/components/tooltip-button.js"></script> <script src="./assets/js/main.js"></script> </body> </html>

Phần này nếu vô tình thay đổi thứ tự thực thi của bootstrap.js trước jquery.js thì sẽ gây ra lỗi và được thông báo bởi trình duyệt trong màn hình Console Uncaught Error: Bootstrap's JavaScript requires jQuery at bootstrap.js:8

ccccc

File index.html (sử dụng RequireJS)

<!DOCTYPE html>
<head>
    <script data-main="./assets/js/main" src="./assets/js/libs/require.js"></script>
</head>
<body>
    <div class="container">
        <h1>RequireJS Sample Page: on hover show tooltip button</h1>
        <button class="btn btn-default" data-toggle="tooltip" title="Tooltip on top">
            Hover me
        </button>
    </div>
</body>
</html>

File main.js

// Code something 
// ...
// Goi ham thuc thi tooltip
var b = button();
b.tooltipFunc();

File main.js (sử dụng RequireJS)

// code something 
// ...

function loadCss(url) {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = url; document.getElementsByTagName("head")[0].appendChild(link);
}

// load bootstrap css
loadCss("assets/css/bootstrap.css")

// Cau hinh RequireJS requirejs.config({
// thiết lập đường dẫn thư mục chứa các file javascript cần thực thi
baseUrl: 'assets/js/libs',

// gán tên đại diện cho module
// tên này dùng để gọi sử dụng trong hàm require() và define()
// nếu không xác định rõ tên thì mặc định sẽ lấy đúng tên file
// chương trình sẽ tự động thêm thành phần mở rộng (.js)
paths: {
jquery: "jquery",
// có thể bỏ đi, mặc định sẽ tự lấy đúng tên
bootstrap: "bootstrap",
// gán tên đại diện nằm ở thư mục khác
tooltip: "../components/tooltip-button"
},

// quy định sự phụ thuộc
// bootstrap chỉ được load sau khi jquery được load thành công
shim: {
bootstrap: {
deps: [ "jquery" ]
}
},
});

// gọi hàm thực thi require(["tooltip"], function(tooltip) {
tooltip.tooltipFunc();
})

File tooltip-button.js

function button(){
    return {
        tooltipFunc : function () {
            $('[data-toggle="tooltip"]').tooltip()
        }
    }
}

File tooltip-button.js (sử dụng RequireJS)

// Gọi và thực thi các module jquery và bootstrap 
// Biến $ là đối số đại diện cho module jquery,
// Do bootstrap chỉ cần thực thi hàm tooltip() nên không cần thiết
// phải khai báo đối số đại diện
define(['jquery','bootstrap'], function($) {
    'use strict';

    // được định nghĩa tương tự
    return {
        tooltipFunc : function () {
             $('[data-toggle="tooltip"]').tooltip()
       }
    }
});

Kết quả cuối cùng sử dụng 2 phương pháp đều giống nhau

a222
a1111

2 phiên bản của ví dụ trên: modules-javascript-example.zip

Kiến thức về RequireJS

Thiết lập môi trường (load JavaScript files)

Để sử dụng RequireJS chỉ cần có tập tin require.js là đủ.

Có thể tải tại đây hoặc cài đặt thông qua Node.js.

  • Tải phiên bản Node.js mới nhất tại đây (nếu chưa cài đặt)
  • Chạy lệnh command (cmd) trong thư mục muốn cài đặt project: npm install -g requirejs

+---.bin
|
\---requirejs
    |   package.json
    |   README.md
    |   require.js
    |
    \---bin
            r.js

Điểm bắt đầu chương trình (data-main entry point)

Sau khi có require.js, thêm vào project. 

|   index.html
|
\---assets
    +---css
    |       bootstrap.css
    |
    \---js
        |   main.js
        |
        +---components
        |       tooltip-button.js
        |
        \---libs
                bootstrap.js
                jquery.js
                require.js

Đầu tiên, định nghĩa trang index.html để load RequireJS

<!--index.html-->
<html>
<head>
    <script data-main="assets/js/main" src="assets/js/libs/require.js"></script>
</head>
<body>
    <h1>RequireJS Sample Page</h1>
</body>
</html>

Nếu muốn sử dụng CDN (Content Delivery Netword) không muốn tải require.js về máy thì có thể sửa lại thành src="https://requirejs.org/docs/release/2.3.5/comments/require.js" chương trình vẫn thực thi một cách chính xác.

Sau khi require.js được thực thi xong, RequireJS sẽ tìm đến thuộc tính data-main, thuộc tính này dùng để xác định điểm bắt đầu của chương trình (entry point). Với trường hợp bên trên thì file assets/js/main.js sẽ được load một cách không đồng bộ (async) và là tập tin JavaScript bắt đầu cho chương trình.

Cũng có một cách khác để xác định điểm bắt đầu chương trình là lợi dụng việc load module thông qua định nghĩa module, nhưng cách này không được khuyến khích.

<!--index.html-->
<!DOCTYPE html>
<head>
    // không cần thuộc tính data-main
    <script src="assets/js/libs/require.js"></script>
    <script>
        // yêu cầu thực thi file assets/js/main.js
        // có thể thay thế thuộc tính data-main
        require(['assets/js/main'], function () {
            // none
        });
    </script>
</head>
<body>
    <h1> RequireJS Sample Page </h1>
</body>
</html>

Cấu hình chương trình (configuration options)

Trong ví dụ trên, tập tin assets/js/main.js là tập tin JavaScript chính của chương trình, thông thường sẽ dùng để cấu hình các đường dẫn và sự phụ thuộc (dependencies) giữa các module cho ứng dụng.

// cấu hình RequireJS
requirejs.config({

    // thiết lập đường dẫn thư mục chứa các file javascript cần thực thi
    baseUrl: 'assets/js/libs',

    // gán tên đại diện cho module
    // tên này dùng để gọi sử dụng trong hàm require() và define()
    // nếu không xác định rõ tên thì mặc định sẽ lấy đúng tên file
    // chương trình sẽ tự động thêm thành phần mở rộng (.js)
    paths: {
        jquery: "jquery",
        // jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min',

        bootstrap: "bootstrap",
        // gán tên đại diện nằm ở thư mục khác
        tooltip: "../components/tooltip-button"
    },

    // quy định sự phụ thuộc
    // bootstrap chỉ được load khi jquery được load thành công
    shim: {
        bootstrap: {
            deps: ["jquery"]
        }
    },
});

RequireJS hỗ trợ các tùy chọn cấu hình sau:

  • baseURL
    • Thiết lập đường dẫn thư mục chứa các tập tin JavaScript cần thực thi.
    • Nó sẽ không được thực thi đối với các đường dẫn bắt đầu bằng dấu “/” (slash), giao thức http/https (protocol) hoặc kết thúc bằng đuôi mở rộng .js.
    • Nếu baseURL không được sử dụng để khái báo trong hàm requirejs.config() thì đường dẫn mặc định sẽ được lấy từ thuộc tính data-main, như vậy sẽ có kết quả là baseUrl: 'assets/js' đối với ví dụ trên.
  •  paths:
    • Dùng để gán (ánh xạ) một thư viện hoặc một module cho một biến mới, biến này sẽ làm đại diện cho thư viện hoặc module đó khi muốn gọi nó trong quá trình định nghĩa một module khác.
    • Trong ví dụ trên, biến jquery sẽ là tên đại diện cho thư viện jQuery, biến tooltip sẽ là tên đại diện cho module được định nghĩa trong assets/js/components/tooltip-button.js. Việc tạo biến đại diện này làm đơn giản hơn trong việc gọi thư viện hoặc module khi muốn sử dụng chúng trong quá trình định nghĩa module khác.
    • Sử dụng các biến đại diện để gọi thực thi
define(['jquery','bootstrap', 'tooltip'], function($, btp, tooltip) {
    // Sử dụng thư viện jquery.
    // Sử dụng thư viện bootstrap.
    // Sử dụng các phương thức đã 
    // được định nghĩa trong moudle assets/js/components/tooltip-button.js.
});
  • Không tạo biến đại diện, phải chỉ rõ đường dẫn để gọi thực thi
define(['jquery','bootstrap', '../components/tooltip-button'], 
  function($, btp, tooltip) {
    // Sử dụng thư viện jquery.
    // Sử dụng thư viện bootstrap.
    // Sử dụng các phương thức đã 
    // được định nghĩa trong moudle assets/js/components/tooltip-button.js.
});
  • Đường dẫn chứa thư viện hoặc module trong thuộc tính paths này nó sẽ phụ thuộc vào đường dẫn đã thiết lập ở thuộc tính baseURL trước đó. Ở ví dụ này, khi đã thiết lập baseUrl: 'assets/js/libs'paths: { jquery: "jquery"} điều này có thể hiểu là biến jquery sẽ đại diện cho assets/js/libs/jquery.js, còn paths: { tooltip: "../components/tooltip-button"} thì biến tooltip sẽ đại diện cho assets/js/components/tooltip-button.js ( dùng “../” để trở về thư mục js ). Các bạn nên xem lại cấu trúc thư mục trong phần ví dụ để hiểu rõ cách xác định đường dẫn.
  •  Có thể sử dụng giao thức http/https (protocol) khi định nghĩa thuộc tính paths
paths: {
    // jquery: "jquery", // sử dụng ở local
    // sử dụng CDN, không có thành phần mở rộng .js
    jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min',
    bootstrap: "bootstrap",
    tooltip: "../components/tooltip-button"
},
  • Đường dẫn không cần thành phần mở rộng .js, khi thực thi chương trình sẽ tự động thêm .js vào mỗi tập tin JavaScript.
  • shim:
    • Dùng để thiết lập các sự phụ thuộc của các thư viện hoặc module theo kiểu đồng bộ (non AMD) và dùng khai báo giá trị toàn cục cho thư viện hoặc module đó (exporting their global values).
    • Giả sử backbone.js, underscore.js, jquery.jsbootstrap.js đã được cấu hình đường dẫn ở thuộc tính baseURL (hoặc paths), các bạn xem comment để hiểu rõ hơn.
requirejs.config({
    shim: {
        'bootstrap': {
            // Quy định sự phụ thuộc là
            // jquery.js sẽ thực thi trước bootstrap.js 
            deps: ['jquery'],
       },
       'backbone': {
            //Các thư viện được khai báo trong thuộc tính deps
            //sẽ được thực thi trước backbone.js
            deps: ['underscore', 'jquery'],
            //Khi thực thi xong, biến toàn cục "Backbone" có thể sử dụng
            //để gọi thư viện này trong lúc định nghĩa module .
            exports: 'Backbone'
        },
    }
});
  • map:
    • Dùng để thiết lập cho việc sử dụng module có nhiều phiên bản làm sao phù hợp với từng trường hợp cụ thể.
    • Có ví dụ như sau, dựa vào thư viện Bootstrap để tùy biến và cá nhân hóa lại thanh menu. Tất cả các code xử lý được lưu ở custom-menu-v3.js ứng với phiên bản Bootstrap ver3. Sau này, Bootstrap cập nhật lên ver4, tôi cũng cập nhật theo, vì vậy tạo custom-menu-v4.js ứng với Bootstrap ver4. Tôi sẽ áp dụng thuộc tính map và sẽ có cấu trúc thư mục như bên dưới
xxxxxxot_20180810011046
requirejs.config({
    map: {
        'menu/custom-menu-v3': {
            'boostrap': 'bootstrap.v3'
        },
        'menu/custom-menu-v4': {
            'boostrap': 'bootstrap.v4'
        }
    }
});

// Định nghĩa ở custom-menu-v3.js
define(['boostrap'], function (b3) {
    // 'boostrap' -> bootstrap.v3.js
    // Các thuộc tính và phương thức
    // trong bootstrap.v3.js sẽ thực thi 
})

// Định nghĩa ở custom-menu-v4.js
define(['boostrap'], function (b4) {
    // 'boostrap' -> bootstrap.v4.js
    // Các thuộc tính và phương thức
    // trong bootstrap.v4.js sẽ thực thi 
})

Ứng với mỗi tập tin JavaScript tùy biến menu thì RequireJS sẽ thực thi phiên bản Bootstrap một cách tương ứng mà chỉ cần gọi tên đại diện chung là bootstrap (xem comment trên code ví dụ). Tất nhiên, cũng có thể gọi như cách thông thường là define([‘bootstrap.v3.js']…) hoặc define([‘bootstrap.v4.js']…) ở mỗi tập tin JavaScript dùng để tùy biến menu, nhưng cách này thật sự không tối ưu đối với chương trình lớn mà lại có nhiều phiên bản khác nhau. Việc sử dụng thuộc tính map giúp dễ điều chỉnh, tránh sửa code ở nhiều nơi nếu có vấn đề phát sinh.

requirejs.config({
    config: {
        // phải tên cùng với tên file contact.js
        'contact': {
            GoogMapKey: 'ABAB-CD-XX'
        }
    }
});

// contact.js
define(['module'], function (module) {
    var key = module.config().GoogMapKey;
    // có giá trị là 'ABAB-CD-XX'
})
  • Muốn thiết lập thông tin cho module được định nghĩa trong tập tin contact.js thì phải khai báo cùng tên là contact, sử dụng thông tin được gán thông qua phương thức module.config(), thông tin thực thi thành công chỉ khi được gọi trong module tương ứng.
  • Ví dụ: requirejs-config-property-example.zip

Định nghĩa module (define a module)

RequireJS cho phép tải và thực thi module một cách nhanh chóng, không lo lắng về sự phụ thuộc giữa các module và có thể tải nhiều phiên bản của một module trong cùng một trang. Tùy theo mục đính sử dụng, có các cú pháp định nghĩa như sau:

Định nghĩa module cặp Name và Value (simple name/value pairs)

Nếu module cần tên đại diện và giá trị thì sử dụng cú pháp sau:

define({
    studentID: "15526969",
    studentName: "Kong Joo Woo"
});

Định nghĩa như một phương thức (definition functions)

Nếu module không có các phụ thuộc (tức là không sử dụng thư viện hoặc module khác) và muốn định nghĩa như một phương thức:

define(function () {
    //Do setup work here

    return {
        studentID: "15526969",
        studentName: "Kong Joo Woo"
    }
});

Định nghĩa với các phụ thuộc (defining functions with dependencies)

Nếu muốn sử dụng các thư viện và các module khác đã được định nghĩa để tạo ra module mới:

define(["./input", "./insertDatabase"], 
   function(inp, ins) {
        
      return {
        // example
        studentID: "15526969",
        studentName: "Kong Joo Woo",
      
         addStudent: function() {
            inp.inputStudent(this);
            ins.insertToDadabase(this);
         }
      
      }
   }
);

Phương thức define() có 2 tham số, tham số đầu tiên là mảng các thư viện hoặc module cần sử dụng, tham số thứ hai dùng để định nghĩa một phương thức như thông thường có thể trả về là một phương thức (function) hoặc là một đối tượng (object). Các đối số trong phương thức này là đại diện cho từng module hoặc thư viện đã thêm vào ở tham số đầu tiên và nó phải sắp xếp đúng theo đúng thứ tự tương ứng với mảng đã thêm thì mới sử dụng chính xác.

Trong ví dụ trên:

  • inp đại diện cho module input.js
  • ins đại diện cho module insertDatabase.js
  • Phương thức trong module sẽ trả về một đối tượng có 2 thuộc tính (studentID, studentName) và 1 phương thức (addStudent).

Định nghĩa module trả về một phương thức (defining a module as a function)

Khi định nghĩa một module không chỉ trả về đối tượng như ở ví dụ trên mà còn có thể trả về một phương thức.

define(["./getDatabase"],
    function (getD) {
        return function (student) {
            return getD.getStudentName(student.studentID);
        }
    }
);

Định nghĩa theo phong cách CommonJS

Ngoài việc thêm các thư viện hoặc module bằng mảng thì sử dụng require() để thực hiện.

define(function (require, exports, module) {
    var ins = require('./input'),
        inp = require('./insertDatabase');

    //Return the module value
    return function (student) {
               inp.inputStudent(student);
               ins.insertToDadabase(student);
    };
});

Định nghĩa với tên module (define a module with a name)

Đối số đầu tiên sẽ dùng để khai báo tên của module, trong ví dụ là utils/add

define("utils/add", ["./input"], function (inp) {
      // something
    }
);

Như vậy, tùy theo mục đích sử dụng có thể lựa chọn các định nghĩa phù hợp như module chỉ gồm cặp name/value, hoặc có thể trả về một đối tượng hay một phương thức.

Mỗi module chỉ nên định nghĩa trong một tập tin JavaScript riêng biệt, điều này làm cho việc gọi sử dụng module được thuận lợi do chỉ cần xác định đường dẫn (name-to-file-path).

Sử dụng require() và define()

Những ví dụ trên sử dụng phương thức require()define(). Hai phương thức này có điểm chung là dùng để tải các phụ thuộc (các thư viện hoặc module).

Tuy nhiên:

  • require() dùng để gọi thực thi các thuộc tính hay các phương thức trong module đã được định nghĩa và nó thường đặt trong main.js 
  • define() sử dụng để định nghĩa module và giúp module có thể tái sử dụng dễ dàng.

Phương thức require() và requirejs() đều giống nhau.

require === requirejs // true

Các keyword hữu ích

  • single-page application (SPA)
  • AMD module javascript
  • async sync defer attributes

(1) Framework DurandalJS: được sử dụng để tạo Single-page app (SPA). DurandalJS được tạo từ 3 thành phần: RequireJS (hiện thực module), KnockoutJS (bindding dữ liệu) và jQuery (cung cấp các tiện ích)

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024