UMD คืออะไร? + วิธีใช้ RequireJS และ Browserify โหลด JavaScript Module

Suranart Niamcome
SiamHTML
Published in
5 min readDec 7, 2014

เนื่องจาก JavaScript plugin ที่จะใช้ใน Bootstrap 4 ที่กำลังจะออกในอีกไม่กี่วันข้างหน้านี้ รองรับ UMD ด้วย ผมจึงจะขออธิบายสักหน่อยว่า UMD หรือ Universal Module Definition มันคืออะไร แล้วมีข้อดีอะไรบ้าง แต่การจะทำความเข้าใจ UMD ได้นั้น ผมคงต้องเล่าความเป็นมาของมันพอสมควร เรามาเริ่มกันเลยดีกว่าครับ

ทุกวันนี้เราเขียน JS กันอย่างไร ?

ปัญหาหนึ่งในการเขียน JavaScript ก็คือการจัดการเกี่ยวกับ Dependency ครับ สมมติเราจะใช้ plugin ของ jQuery ก็จะได้ว่า Dependency ของ plugin นี้ก็คือ jQuery นั่นเอง เพราะถ้าไม่มีมัน plugin ตัวนี้ก็จะไม่สามารถทำงานได้ ดังนั้น เวลาเราเขียนโค้ด เราก็จะต้องโหลด jQuery เข้ามาก่อน jQuery plugin เสมอ แบบนี้ครับ

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/jquery.plugin.js"></script>

จะสังเกตว่าเราจะต้องมาพะวงกับลำดับการโหลด script ครับ บางคนอาจมองว่าไม่เห็นเปนไรเลย ก็แค่โหลด Dependency มาก่อนไง แต่ลองนึกดูว่า หาก scale ของงานเรานั้นใหญ่เอามากๆ Dependency มันไม่ได้มีแค่นี้หรอกนะครับ และยิ่ง script บางตัว อาจใช้ Dependency ร่วมกันด้วย เราก็จะยิ่งปวดหัวเข้าไปใหญ่

รู้จักกับ CommonJS

เพื่อแก้ปัญหาที่ว่านี้ CommonJS ซึ่งเป็นกลุ่มที่ตั้งขึ้นมาเพื่อสร้างสภาพแวดล้อมที่เหมาะสมในการเขียน JavaScript จึงได้กำหนดรูปแบบของการเขียน JavaScript Module ขึ้นมาครับ โดยสาเหตุที่ต้องสร้าง JavaScript Module นั้น ก็เพื่อวัตถุประสงค์ดังนี้

  • แบ่งโค้ดออกเป็น Unit ย่อยๆ เพื่อให้สามารถนำไปใช้ได้ง่าย
  • ในแต่ละ ​Unit นั้น ก็สามารถเรียกใช้ Unit อื่นๆ ได้เช่นกัน

ทีนี้เราลองมาดูรูปแบบการเขียน JavaScript Module ที่ CommonJS เค้าแนะนำกันครับ

// ไฟล์ myModule.js
function print(message){
console.log(message);
}
exports.print = print;

หรือจะเขียนแบบสั้นๆ แบบนี้ก็ได้ครับ
// ไฟล์ myModule.js
exports.print = function(message){
console.log(message);
};
แบบนี้จะได้ว่าโมดูลที่ชื่อ myModule ของเรามี method ชื่อ print ที่เอาไว้แสดงข้อความออกมาทาง console ครับทีนี้ลองมาดูตัวอย่างการเรียกใช้โมดูลอื่นหรือ Dependency กันบ้างครับ ก่อนอื่นให้เราลองสร้างโมดูลง่ายๆ ขึ้นมาอีกอัน// ไฟล์ text.js
exports.capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
จะได้ว่าโมดูลที่ชื่อ text ของเรามี method ชื่อ capitalize ที่เอาไว้ทำให้ตัวอักษรตัวแรกของข้อความเป็นตัวใหญ่ครับ ทีนี้เวลาเราจะเรียกใช้ความสามารถจากโมดูล text เราก็สามารถทำผ่าน require() แบบนี้ได้เลยครับ// ไฟล์ myModule.js
var text = require('text');
exports.print = function(message){
message = text.capitalize(message);
console.log(message);
};
exports.print = print;น่าเสียดาย! CommonJS ไม่เหมาะกับ Web Browserหากสังเกตุโค้ดของ ​JavaScript Module แบบ CommonJS ดีๆ จะเห็นว่ามันเป็นแบบ synchronous ครับ จากตัวอย่างด้านบน มันจะต้องรอให้โมดูล text โหลดเสร็จก่อน ถึงจะทำ print ได้ พูดง่ายๆ ก็คือการเขียนแบบ CommonJS นั้น มันจะต้องไปโหลดทุกๆ โมดูล ที่อยู่ใน require() มาให้เสร็จตั้งแต่เริ่มเลยครับ ดังนั้น เรามักจะเห็น JavaScript Module แบบนี้กับ JavaScript ที่รันฝั่ง Server อย่าง Node.js ซะเยอะครับ เพราะถ้าเอามาใช้ในฝั่ง Client กับ Web Browser นั้น มันก็คงไม่ค่อยดีในแง่ของ Performance

รู้จักกับ AMD

เพื่อแก้ปัญหาดังกล่าว จึงเกิดรูปแบบการเขียน JavaScript Module แบบใหม่ที่เรียกว่า AMD หรือ Asynchronous Module Definition ขึ้นมาครับ โดยการเขียนรูปแบบนี้จะทำให้ทั้งตัวโมดูลเองและ Dependency สามารถโหลดแบบ asynchronous ได้ เราลองมาดูว่าการเขียนรูปแบบนี้มันมีหน้าตาเป็นอย่างไร// ไฟล์ myModule.js
define(function() {
var myModule = {};
myModule.print = function(message) {
console.log(message);
}
return myModule;
});
ตัวอย่างโค้ดด้านบนจะเป็นโมดูลแบบที่ไม่มี Dependency เลยนะครับ แต่อย่าลืมว่า method print ของเรานั้นมีการเรียกใช้โมดูล text อยู่ด้วย ให้เราใส่ Dependency ที่ต้องการจะใช้ ลงไปก่อนหน้า function ในรูปแบบ array แบบนี้ได้เลยครับ// ไฟล์ myModule.js
define(['text'], function(text) {
var myModule = {};
myModule.print = function(message) {
message = text.capitalize(message);
console.log(message);
}
return myModule;
});
สังเกตว่าผมได้ใส่ text เอาไว้เป็น argument ของ function ด้วยนะครับ เวลาเราจะเรียกใช้ method อะไรจากโมดูล text ก็สามารถทำผ่าน text ตัวนี้ได้เลยและถ้าหากมี Dependency เยอะๆ ก็ใส่ต่อกันไปแบบนี้ได้เลยครับ// ไฟล์ myModule.js
define(['jquery', 'text'], function($, text) {
var myModule = {};
myModule.print = function(target, message) {
message = text.capitalize(message);
$(target).html(message);
}
return myModule;
});
สุดท้าย อย่าลืมเปลี่ยนโมดูล text ที่เดิมเขียนแบบ CommonJS มาเป็นแบบ AMD ด้วยนะครับ ถ้าเราขี้เกียจหรือไม่อยากไปแก้โค้ดเดิม ก็มีวิธีแปลงโมดูล CommonJS ให้กลายเป็นโมดูล ​AMD ง่ายๆ ด้วยการใส่ wrapper แบบนี้เข้าไป// ไฟล์ text.js
define(function(require, exports, module) {
// โค้ดโมดูลแบบ CommonJS เดิม
exports.capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);;
}
});
เพียงเท่านี้โมดูล text ก็จะกลายเป็นโมดูลแบบ AMD แล้วล่ะครับ

โหลดโมดูลแบบ AMD ด้วย RequireJS

บางคนลองทำตามแล้วรันใน web browser ก็อาจจะสงสัยว่า "เฮ้ย! โมดูลทั้งแบบ CommonJS และ AMD ที่เขียนไปมันไม่เห็นจะใช้งานได้เลย นี่เราทำอะไรผิดหรือเปล่า?" คำตอบคือไม่ได้ทำอะไรผิดหรอกครับ เพราะเจ้าโมดูลทั้ง 2 แบบ นั้นมันยังไม่ได้รองรับบน web browser ในตอนนี้ (-_-")การจะใช้โมดูลแบบ AMD ได้นั้น เราจะต้องใช้ JavaScript Loader อย่าง RequireJS เข้ามาช่วยครับ ให้เราทำตามขั้นตอนต่อไปนี้ได้เลย1. ติดตั้ง RequireJSให้เราไป Download RequireJS มาก่อนครับ จากนั้น ให้เราวางโครงสร้างของโฟลเดอร์ตามนี้myProject/
|
|-- js/
| |
| |-- lib/ # เก็บ module ต่างๆ
| | |
| | |-- jquery.js
| | |
| | |-- myModule.js
| | |
| | |-- require.js # ไฟล์ require.js ที่เพิ่งดาวน์โหลดมา
| | |
| | `-- text.js
| |
| |-- config.js # ไฟล์ config ของ RequireJS
| |
| `-- main.js # ไฟล์ script หลักที่เราจะเขียนโค้ด
|
`-- index.html # ไฟล์ html ที่จะโหลด AMD Module ด้วย RequireJS
พวกไฟล์ index.html, config.js และ main.js นี้ ให้เราสร้างไฟล์เปล่าๆ รอไปก่อนนะครับ เดี๋ยวผมจะพูดถึงรายละเอียดในภายหลัง

2. ติด Script tag ที่ index.html

มาเริ่มที่ไฟล์ index.html กันก่อนครับ ให้เราใส่ script tag เพื่อที่จะโหลด require.js เข้ามาใช้ในหน้านี้<head>    
.
.
.
<!-- data-main เอาไว้บอก require.js ว่าให้โหลด js/config.js ต่อ หลังจากที่โหลด require.js เสร็จแล้ว -->
<script src="js/lib/require.js" data-main="js/config"></script>
</head>
จะสังเกตว่าเราจะต้องใส่ data-main เอาไว้ที่ script tag ด้วยนะครับ เพื่อเป็นการบอก RequireJS ว่าให้โหลดไฟล์ JS ตัวไหนต่อ หลังจากที่โหลด require.js เข้ามาแล้ว ซึ่งในตัวอย่างนี้ เราบอกให้ไปอ่านไฟล์ config.js ครับ (ตรงนี้เราไม่ต้องใส่ .js นะครับ)

3. ใส่ Config ไว้ใน config.js

ทีนี้มาดูที่ไฟล์ config.js กันบ้างครับ// ไฟล์ js/config.js
requirejs.config({
// กำหนด baseUrl ให้เป็นโฟลเดอร์ js/lib ครับ จะได้เรียกใช้ Dependency ได้สะดวกๆ
baseUrl: 'js/lib',
});
// เมื่ออ่าน config เสร็จแล้ว ก็ให้ไปอ่าน main.js ต่อ
requirejs(['../main']);

จริงๆ แล้ว ในส่วนของ config ของ RequireJS นั้นมีเยอะกว่านี้มากนะครับ สามารถดูรายละเอียดเพิ่มเติมได้ที่เว็บหลัก
4. เขียนโค้ดที่ main.jsเมื่อทุกอย่างพร้อมแล้ว เราก็มาเริ่มเขียน logic กันที่ main.js ครับ ลองดูตัวอย่างโค้ดด้านล่างนี้require(['jquery', 'myModule'], function($, myModule) {
$(function(){
myModule.print('body', 'siamhtml');
});
});
สังเกตว่าที่ main.js ซึ่งเป็นไฟล์ script หลักของเรา จะใช้ require() แทนการใช้ define() นะครับ เพราะเราต้องการจะสั่งให้มันทำงานทันทีเลย ไม่ใช่แค่ define เฉยๆ เหมือนกับที่ทำในโมดูลแล้ว ส่วน Dependency ผมก็เลือกโหลดโมดูล jquery กับ myModule มาใช้ครับส่วนโค้ดที่ทำใน function ก็จะเป็นการเรียกใช้ method print จากโมดูล myModule ที่เราทำไว้นั่นเองครับ โดย parameter ตัวแรกจะเป็นตำแหน่งที่ต้องการจะให้แสดงข้อความออกมา ส่วนตัวที่สองก็จะเป็นข้อความที่จะให้แสดงออกมา แต่เมื่อลองรันดู ข้อความที่แสดงออกมาจะเป็น Siamhtml แทนที่จะเป็น siamhtml เพราะที่ method print นั้น เราได้สั่งให้เรียกใช้ method capitalize จากโมดูล text ไปแล้วนั่นเองครับโหลดโมดูลแบบ CommonJS ด้วย Browserifyทีนี้ลองมาดูวิธีการใช้โมดูลที่เป็นแบบ CommonJS กันบ้างครับ ให้เราใช้ tool อย่าง Browserify เข้ามาช่วย ลองทำตามขั้นตอนต่อไปนี้ได้เลยครับ

1. ติดตั้ง Browserify

เริ่มด้วยการติดตั้ง browserify ซึ่งเป็น tool ที่จะช่วย build ไฟล์ js ที่มีการเรียกใช้โมดูลแบบ CommonJS ของเรา ให้อยู่ในรูปที่สามารถใช้ได้บน web browser ได้ ให้เราติดตั้งผ่าน npm ได้เลยแบบนี้ครับnpm install browserify --save-dev

2. ติดตั้ง plugin สำหรับรัน Browserify ด้วย gulp.js

การจะใช้ Browserify ในการ build ไฟล์นั้น เราจะต้องทำผ่าน command-line ครับ แต่เพื่อความสะดวกในการเขียนโค้ด ผมแนะนำให้ใช้ task runner อย่าง gulp.js เข้ามาช่วยไปเลยครับ เพื่อที่จะได้สั่งให้มัน build ไฟล์ใหม่ทุกครั้งที่มีการแก้ไขไฟล์ jsเมื่อเราตัดสินใจจะใช้ gulp.js ในการทำ browserify แล้ว plugin สำคัญที่จะต้องลงก็คือ vinyl-transform ครับ เพราะมันจะช่วยให้เราเรียกใช้ browserify แบบตัวจริงได้เลย ไม่ใช่แบบ plugin ของ gulp อย่าง gulp-browserify สาเหตุที่อยากให้ใช้ browserify แท้ๆ ก็เพราะว่ามันยังไม่ค่อยนิ่งครับ สมมติมันมีการเปลี่ยนแปลงอะไร ถ้าเราใช้ gulp-browserify เราก็จะต้องมารอให้คนทำ gulp-browserify อัพเดทตาม browserify อีกnpm install vinyl-transform --save-devนอกนั้นก็จะเป็น plugin ทั่วไปของ gulp อย่าง gulp-uglify และ gulp-concat ครับ สำหรับคนที่ใช้ gulp อยู่ ก็น่าจะคุ้นเคยกันดีอยู่แล้ว

3. เพิ่ม Task สำหรับ Build ลงใน gulpfile.js

ให้เราเพิ่ม task ด้านล่างนี้ลงไปใน gulpfile.js ได้เลยครับgulp.task('browserify', function() {
var browserified = transform(function(filename) {
var b = browserify(filename);
return b.bundle();
});
return gulp.src(['js/main.js'])
.pipe(browserified)
.pipe(concat('built.js'))
.pipe(uglify())
.pipe(gulp.dest('js'));
});

โค้ดด้านบนจะเป็นการสั่งให้ Browserify ทำการ build ไฟล์ main.js ซึ่งเป็นไฟล์ script หลักของเรา ให้ออกมาเป็นไฟล์ built.js พร้อมกับ uglify ให้ด้วยครับ
เพื่อความสะดวกยิ่งขึ้น ให้เราไปเขียน watch ไฟล์ แล้วสั่งให้ทำ task นี้เวลาไฟล์ js มีการแก้ไขไปเลยครับ รายละเอียดผมขอไม่พูดถึงละกันนะครับ สามารถอ่านได้ที่บทความ "สอนวิธีใช้ Gulp.js"4. เตรียมโมดูลแบบ CommonJSต่อมาให้เราเตรียมโมดูลแบบ CommonJS มาให้เรียบร้อยครับ อย่างโมดูล myModule เดิมของเรานั้นจะเป็นแบบ AMD อยู่ เราก็ต้องเปลี่ยนมาเขียนให้อยู่ในรูปของ CommonJS แบบนี้ครับ// ไฟล์ myModule.js
var $ = require('./jquery.js');
var text = require('./text.js');
exports.print = function(target, message) {
message = text.capitalize(message);
$(target).html(message);
};

ส่วนโมดูล text ก็จะเปลี่ยนมาเป็นแบบนี้ครับ
// ไฟล์ text.js
exports.capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};

5. เขียนโค้ดที่ main.js

เมื่อทุกอย่างพร้อมหมดแล้ว สุดท้ายเราก็มาเขียน logic กันที่ไฟล์ main.js ครับ จากตัวอย่างเดิมที่เป็นแบบ AMD นั้น ให้เราเปลี่ยนมาเขียนแบบ CommonJS แทนแบบนี้ครับ// ไฟล์ main.js
var $ = require('./lib/jquery.js');
var myModule = require('./lib/myModule.js');
$(function(){
myModule.print('body', 'siamhtml');
});
6. Build ไฟล์ main.jsข้อเสียอย่างหนึ่งของการใช้ Browserify ก็คือ เวลาที่เราแก้อะไรแล้วมันจะต้อง build ก่อนครับ ถึงจะเห็นผล แต่ถ้าเราได้ใช้ gulp.js เข้ามาช่วยแล้ว ขั้นตอนนี้ก็จะทำโดยอัตโนมัติครับ

7. ติด Script tag

สุดท้ายให้เราติด script tag เพื่อโหลดเอาไฟล์ built.js ซึ่งเป็นผลมาจากการ build ไฟล์ main.js ด้วย Browserify มาใช้ครับ<head>    
.
.
.
<script src="js/built.js"></script>
</head>
เมื่อลองพรีวิวดูก็จะเห็นข้อความ Siamhtml เช่นเดียวกับการใช้ RequireJS เลยครับ

RequireJS vs. Browserify

มาถึงตรงนี้ คาดว่าหลายๆ คนคงจะลังเลว่า "แล้วเราควรจะเขียน JavaScript Module แบบไหนดี?" เพื่อทำให้ตัดสินใจได้ง่ายขึ้น ผมจะสรุปข้อดีของทั้ง 2 แบบ มาให้อ่านกันครับ

ข้อดีของ RequireJS + AMD

  • โหลดโมดูลแบบ asynchronous ได้
  • ไม่ต้อง build
  • ใช้กับโมดูลแบบ CommonJS ก็ได้ เพียงแค่ใส่ wrapper เข้าไป
  • มีฟีเจอร์ที่น่าสนใจมากมาย เช่น การกำหนด fallback ให้กับที่อยู่ของโมดูล สามารถดูรายละเอียดเพิ่มเติมได้ที่เว็บหลัก

ข้อดีของ Browserify + CommonJS

  • โค้ดสั้นกว่า อ่านเข้าใจง่ายกว่า
  • ใช้โมดูลของ Node.js ได้เลย เพราะเป็นแบบ CommonJS อยู่แล้ว เวลาจะใช้ก็แค่ require ชื่อโมดูลเอา เช่น var _ = require('underscore'); เป็นต้น เพราะ Browserify ฉลาดพอที่จะเดาได้ว่าโมดูลนั้นอยู่ที่ไหน
  • เนื่องจากเราสามารถเอาโมดูล CommonJS ที่ตัวเองเขียนขึ้นมา ไป publish ผ่าน npm ได้ นั่นหมายความว่า เราสามารถใช้โมดูลนั้น ทั้งฝั่ง server ผ่าน Node.js และฝั่ง client ผ่าน Browserify ได้โดยที่ไม่ต้องแก้โค้ดอะไรเลย พูดง่ายๆ ก็คือ เราสามารถแชร์โค้ดร่วมกันระหว่าง client กับ server ได้ หรือที่เรียกว่า Isomorphic JavaScript นั่นเอง

แล้ว UMD คืออะไร ?

เล่ามาตั้งนาน ยังไม่ได้พูดถึง UMD เลยสักคำ! เอาล่ะครับ เรามาเข้าเรื่องกันดีกว่า จะเห็นว่าตอนนี้การเขียนโมดูลมันจะแบ่งออกเป็น 2 แบบใหญ่ๆ ด้วยกันครับ ซึ่งมันค่อนข้างจะทำให้ web developer อย่างเราปวดหัวอยู่พอสมควร เพราะต้องมานั่งดูอีกว่าเจ้าโมดูลที่เราจะเอามาใช้เนี่ย มันสามารถใช้ได้กับ tool ที่เราใช้อยู่มั้ย ถ้าไม่ได้ เราก็ต้องเสียเวลาไปแปลงให้มันใช้ได้เสียก่อนเพื่อแก้ปัญหาดังกล่าว จึงเกิดแนวคิดใหม่ที่จะทำให้โมดูลนั้นสามารถใช้ได้ในทุกๆ ที่ ซึ่งมันก็คือโมดูลแบบ UMD หรือ Universal Module Definition นั่นเองครับ พูดง่ายๆ ก็คือ เขียนทีเดียว จะเอาไปใช้กับอะไรก็ได้ ลองมาดูตัวอย่างการเขียนโมดูลแบบ UMD ที่มี Dependency เป็น jquery และ underscore กันครับ(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("underscore"));
} else {
root.myModule = factory(root.$, root._);
}
}(this, function ($, _) {
var myModule = {
// ใส่โค้ดตัวโมดูลจริงๆ ตรงนี้ครับ
};
return myModule;
}));
หากลองไล่โค้ดดู จะเห็นว่าโมดูลแบบ UMD นั้นจะเพิ่มโค้ดของการเช็คว่าเราใช้อะไรอยู่เข้ามาครับ โดยมันจะเริ่มเช็คก่อนว่า tool ที่เราใช้นั้นรองรับ AMD หรือเปล่า ถ้าไม่รองรับก็ไปเช็คต่อว่ารองรับ CommonJS หรือเปล่า และถ้ายังไม่รองรับอีก ก็ให้เรียกใช้โมดูลแบบปกติไปเลยครับ (ซึ่งแน่นอนว่าเราต้องติด script tag เพื่อโหลด jquery และ underscore เข้ามาก่อน)จะเห็นว่าโค้ดของโมดูลแบบ UMD นี้ทำให้มันสามารถเอาไปใช้ได้ในทุกๆ ที่เลยครับ แต่ไม่ต้องกังวลไปนะครับว่า เราจะต้องมานั่งใส่โค้ดพวกนี้ให้กับทุกๆ โมดูลที่เราเขียน เพราะว่าเราสามารถใช้ tool อย่าง gulp-umd ทำให้โดยอัตโนมัติได้เลยครับบทสรุปคงจะพอเห็นภาพกันไม่มากก็น้อยแล้วนะครับว่า JavaScript plugin แบบ UMD ที่กำลังจะมาใน Bootstrap 4 นี้มันคืออะไร ส่วนใครยังตัดสินใจไม่ได้ว่าจะเลือกใช้ RequireJS หรือ Browserify ก็ไม่ต้องเครียดไปครับ เพราะด้วยความที่มันเป็น UMD เลยทำให้เราสามารถใช้ JavaScript plugin ใหม่ๆ เหล่านี้แบบปกติเหมือนที่เคยทำใน Bootstrap 3 ได้อยู่ดีครับ

--

--