Webpack คืออะไร ? + สอนวิธีใช้ร่วมกับ React

Suranart Niamcome
SiamHTML
Published in
8 min readJul 26, 2015
webpack

บทความนี้ผมจะพาเพื่อนๆ ไปทำความรู้จักกับ Webpack ซึ่งเป็น tool ที่กำลังได้รับความนิยมมากอีกตัวหนึ่งในการทำเว็บสมัยนี้ครับ ถึงแม้ว่าหน้าที่หลักๆ ของ Webpack จะเป็นการรวมโมดูลของ JavaScript ต่างๆ ให้อยู่ในรูปที่สามารถใช้กับ web browser ได้ ซึ่งจะคล้ายๆ กับการใช้ Grunt หรือ Gulp ร่วมกับ Browserify แต่รับรองว่าฟีเจอร์ที่ Webpack ให้มานั้นน่าสนใจสุดๆ จนหลายคนถึงกับเปลี่ยนมาใช้เจ้านี่แทนเลยล่ะครับ เรามาดูกันซิว่า Webpack นี่มันมีดีอะไร ?

Webpack คืออะไร ?

อย่างที่ผมได้เกริ่นไปก่อนหน้านี้แล้วนะครับว่า Webpack นั้นเป็น tool ที่เอาไว้แปลงโมดูลของ JavaScript ให้อยู่ในรูปที่เว็บสามารถนำไปใช้ได้ หรือพูดง่ายๆ ก็คือ มันเอาไว้แปลง JavaScript Module แบบต่างๆ ไม่ว่าจะเป็น CommonJS, AMD หรือแม้แต่ ES6 Module ให้กลายเป็นโค้ด JavaScript ธรรมดาๆ ที่สามารถรันบน web browser ทั่วไปได้นั่นเอง คุ้นๆ มั้ยครับ ? นี่มันฟีเจอร์ของ Browserify ชัดๆ

จุดเด่นของ Webpack

ในเมื่อสิ่งที่ Webpack ทำนั้น Browserify ก็ทำได้ แล้วเราจะเปลี่ยนมาใช้ Webpack ทำไม ? เหตุผลง่ายๆ คืออย่างนี้ครับ

  • เร็ว Webpack อ้างว่าเค้าใช้ async I/O บวกกับการทำ cache หลายชั้น ทำให้ขั้นตอนในการ compile นั้นเร็วสุดๆ ครับ ซึ่งหลังจากที่ผมได้ลองใช้ดูแล้ว ก็ต้องยอมรับว่ามันเร็วจริงๆ
  • ครบ ผมว่าคนทำ Webpack นี่เค้ารู้ใจ web developer อย่างเราจริงๆ ครับ ฟีเจอร์อะไรที่ควรจะมีอยู่ใน workflow นั้น พี่แกเตรียมมาให้หมดเลย สิ่งที่เราต้องทำก็แค่ config นิดๆ หน่อยๆ เท่านั้นเองครับ

แล้ว Webpack ทำอะไรได้บ้าง ?

ทีนี้มาดูกันครับว่า ไอ้ที่ว่าเตรียมมาให้หมดนั้น มันมีอะไรบ้าง ?

  • Loaders สมัยนี้เราคงไม่ได้เขียนโมดูลด้วย JavaScript แบบเดิมๆ กันแล้วถูกมั้ยครับ บางคนอาจจะเขียนด้วย ECMAScript 6, JSX หรือ TypeScript แต่สุดท้ายแล้ว เราก็ต้องแปลงโค้ดเหล่านี้ให้กลับมาเป็น JavaScript อยู่ดี ซึ่ง Webpack เค้าก็เตรียมช่องทางมาให้แล้ว ผ่านสิ่งที่เรียกว่า Loader ครับ สมมติว่าเราเขียนโมดูลด้วย ECMAScript 6 เราก็จะต้องกำหนด Loader ให้เป็น Babel ซึ่งเป็น tool ที่มีความสามารถในการแปลง ECMAScript 6 ให้กลายเป็น JavaScript ได้ อะไรทำนองนี้ครับ หรือสรุปสั้นๆ ก็คือ Loader มันเหมือนกับ transform ของ Browserify นั่นแหละครับ
  • Dev Server เราสามารถใช้ Webpack เป็น web server สำหรับ dev ได้ด้วยนะครับ แล้วเจ้า dev server ที่ว่านี่ความสามารถมันก็ไม่ใช่เล่นๆ เลย เราสามารถกำหนดให้มันช่วยรีเฟรชหน้าเว็บโดยอัตโนมัติได้เวลาที่มีไฟล์ไหนถูกแก้ไข และหากไฟล์นั้นเป็น css มันก็จะใช้วิธี inject สไตล์เข้าไปให้เลย ทำให้เราไม่ต้องมาเสียเวลาโหลดหน้าเว็บใหม่ทั้งหน้าครับ เรียกว่าเหมือนกับการใช้ BrowserSync ยังไงยังงั้นเลย ส่วนใครที่เขียน React อยู่นี่ ยิ่งพลาดไม่ได้เลยครับ เพราะเวลาที่เรามีการแก้โค้ดอะไร dev server ของ Webpack มันสามารถสั่งให้ render เฉพาะ component ที่ถูกอัพเดทได้ด้วย แถมมันยังรักษา state เดิมของ component ในขณะนั้นๆ เอาไว้ให้อีก ฟีเจอร์นี้ผมว่ามีประโยชน์สุดๆ ครับ
  • Code Splitting นี่ถือเป็นฟีเจอร์ที่ผมชอบมากที่สุดเลยครับ คือ Webpack มันจะสามารถแบ่งโค้ดของเราออกเป็นส่วนย่อยๆ ได้ด้วย ซึ่งจะช่วยให้เว็บเราไม่จำเป็นต้องมาโหลดไฟล์ JavaScript ใหญ่ๆ ไฟล์เดียวอีกแล้ว คำถามที่ตามมาแน่ๆ ก็คือ แล้ว Webpack มันใช้เกณฑ์อะไรในการแบ่ง ? คำตอบคือเราเป็นคนกำหนดเองครับ เราสามารถกำหนดได้ว่า โค้ดส่วนนี้เป็นโค้ดหลักนะ ให้โหลดมาตั้งแต่แรกเลย ส่วนโค้ดตรงนี้เป็นโค้ดที่นานๆ จะใช้ทีนะ ให้โหลดแบบ asynchronous มาเฉพาะตอนที่จะต้องใช้งานดีกว่า ฟีเจอร์นี้ผมมองว่าคล้ายๆ กับการใช้ RequireJS ครับ

คาดว่าเพื่อนๆ คงจะอยากเห็นตัวอย่างโค้ดของการเรียกใช้ Webpack แล้ว งั้นเรามาดูวิธีการใช้งานกันเลยครับ

ตัวอย่างการใช้ Webpack แบบง่ายๆ

1. ติดตั้ง Webpack

เริ่มด้วยการติดตั้ง webpack แบบ global ครับ เราจะได้ใช้ webpack ผ่าน command-line ได้

npm install webpack -g

ต่อด้วยการติดตั้ง webpack ที่ตัวโปรเจคของเราครับ

npm install webpack --save-dev

2. สร้างไฟล์ Config

จากนั้นเราก็จะต้องสร้างไฟล์ config ขึ้นมาก่อนฮะ ให้เราตั้งชื่อไฟล์ว่า webpack.config.js แล้ววางไว้ที่ root ของเว็บแอปเรา จากนั้นก็ใส่โค้ดด้านล่างนี้ลงไป

// ไฟล์ webpack.config.jsmodule.exports = {
entry: './app.js', // กำหนด entry point เป็น app.js
output: {
filename: 'bundle.js' // ตั้งชื่อไฟล์ output เป็น bundle.js
}
};
นี่เป็น config ที่ simple สุดๆ แล้ว ที่จะทำให้ Webpack ทำงานได้ครับ สิ่งที่เราทำไปก็คือกำหนด entry point ของแอป จากนั้นเราก็ระบุว่าจะให้รวมร่างเป็นไฟล์ชื่อว่าอะไรก็เท่านั้นเอง
3. รัน Webpackทีนี้ก็มาถึงการรัน Webpack ที่เราจะทำผ่าน CLI ครับ ให้เราพิมพ์ command ด้านล่างนี้ลงไปwebpackเราจะใช้ท่านี้ในการรัน Webpack ตาม config ที่ได้กำหนดเอาไว้ในไฟล์ webpack.config.js ครับ ส่วน option ที่เราใช้บ่อยๆ จะมีอยู่ 4 อัน ด้วยกัน
  • -d เพิ่มฟีเจอร์สำหรับ debug และ source maps เข้ามาด้วย เรามักจะใช้ option นี้ ตอนที่กำลัง dev อยู่ครับ
  • -p ช่วยบีบอัดไฟล์ให้เล็กลงด้วย อันนี้เหมาะสำหรับตอนจะเอาขึ้น production ครับ
  • — watch คอย watch ไฟล์ให้ด้วย หากมีการแก้ไขใดๆ ก็จะรัน Webpack ใหม่ให้โดยอัตโนมัติ
มาถึงตรงนี้ให้เราลองรันแต่ละ option ดูเลยครับ จะได้เห็นภาพว่าแต่ละ option นั้นจะใช้ในสถานการณ์ไหนบ้างแต่อย่างที่บอกไปแล้วนะครับว่า ในการเขียนโค้ด JavaScript ทุกวันนี้ แทบจะไม่มีใครเขียน JavaScript เพียวๆ กันแล้ว ปัญหาก็คือ เจ้า Webpack นี่มันรองรับแต่ JavaScript เท่านั้นน่ะสิครับ แล้วเราจะแก้ปัญหานี้ยังไง ?รู้จักกับ Loader ของ Webpackวิธีที่ Webpack ใช้ก็คือ มันจะพ่วงฟีเจอร์ที่เรียกว่า Loader เข้ามาให้ด้วยครับ โดยเจ้า Loader ที่ว่านี้ มันจะสามารถแปลงโค้ดที่ยังไม่ได้อยู่ในรูปของ JavaScript ให้กลายเป็น JavaScript ได้ หน้าที่ของเราก็คือ ดูว่าโค้ดที่เราเขียนนั้น มีการใช้อะไรที่ไม่ใช่ JavaScript แบบปกติบ้าง หากมีเราก็จะต้องไปบอก Webpack ก่อนว่าเราจะใช้ Loader ตัวไหนครับ

1. แปลง ES6/JSX

สมมติว่าเว็บแอปเราเขียนด้วย React ซึ่งมักจะมีการใช้ JSX และ ECMAScript 6 อยู่ด้วย เราจะต้องไปเพิ่ม config ในส่วนของ Loader ตามนี้ครับ// ไฟล์ webpack.config.jsvar path = require('path');module.exports = {

entry: path.resolve(__dirname, 'src/js/app.js'),
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.js'
},
module: {

// ส่วนนี้เอาไว้ระบุ Loader ที่จะใช้
loaders: [
{
test: /\.jsx?$/, // ถ้าเจอไฟล์นามสกุล js หรือ jsx
loader: 'babel-loader' // ให้ load ไฟล์นั้นด้วย babel นะ
}
]
}
};
จะเห็นว่าการเรียกใช้ Loader นั้นง่ายเอามากๆ เลยใช่มั้ยล่ะครับ แต่พอ config เสร็จแล้ว อย่าเพิ่งรันนะครับ เพราะมันจะยังหา Loader ที่ชื่อ babel ไม่เจอ ให้เราติดตั้ง babel-loader ซึ่งเป็น babel เวอร์ชั่นที่เป็น Loader ของ Webpack เข้าไปก่อน แบบนี้ครับ
npm install babel-loader --save-devก่อนหน้านี้พอติดตั้ง Babel เสร็จแล้ว มันจะพร้อมใช้งานได้ทันทีฮะ แต่ตอนนี้ไม่ได้แล้วครับ เพราะเราจะต้องบอก Babel ก่อนว่าอยากจะให้ Babel ช่วยแปลงอะไรบ้าง สมมติว่าโปรเจคเรามีการใช้ JSX กับ ES6 ก็ให้เราติดตั้ง preset ของ Babel ตามนี้ฮะnpm install babel-preset-es2015 babel-preset-react --save-devจากนั้นให้เราไปสร้างไฟล์ .babelrc ที่จะเอาไว้กำหนด option ต่างๆ ให้กับ Babel แล้วใส่ preset ที่เราโหลดมาเมื่อกี้ลงไป{
"presets": ["es2015", "react"]
}
เพียงเท่านี้ เราก็จะสามารถใช้ Webpack กับโค้ดที่เขียนด้วย JSX และ ES6 ได้แล้วล่ะครับ ให้เราลองรัน webpack ดูได้เลย

2. แปลง CSS/Sass

ทีนี้เรามาดูความสามารถของ Webpack อีกอันที่น่าสนใจ นั่นก็คือ ความสามารถในการโหลด css ครับ โดยปกติแล้ว เราอาจจะโหลด css โดยการใช้ <link /> แบบนี้<link rel="stylesheet" type="text/css" href="style.css" />แต่ถ้าเราใช้ Webpack เราจะมีทางเลือกใหม่เพิ่มเข้ามาครับ สมมติเราใช้ React เราสามารถที่จะนำไฟล์ css ไปผูกไว้กับ app หรือ component ได้ด้วย เพื่อให้เห็นภาพมากขึ้น เรามาดูตัวอย่างโค้ดกันเลยครับ

1. ผูก CSS ไว้กับ App

วิธีแรกจะเป็นการรวมสไตล์ชีททั้งหมดให้เป็นไฟล์เดียว แล้วเอาไปผูกไว้กับไฟล์ที่เป็น entry point เลยครับ// ไฟล์ src/js/app.js// โหลด style.css มาใช้กับทั้งแอป
import '../css/style.css';
var React = require('react');
var ReactDOM = require('react-dom');
var MyComponent = require('./components/MyComponent.jsx');
ReactDOM.render(<MyComponent />, document.getElementById('app'));
จากโค้ดด้านบน สิ่งที่ Webpack ทำก็คือ ไปดูดสไตล์จาก style.css มาแปลงเป็น JavaScript แล้วนำมาฝังไว้ใน bundle.js ครับ แล้วเวลารันบน web browser พวกสไตล์ชีทต่างๆ ก็จะถูก bundle.js พ่นเข้าไปในหน้านั้นๆ โดยอัตโนมัติผ่าน <style> เพียงเท่านี้ เราก็ไม่ต้องไปโหลด css โดยใช้ <link /> ให้เปลือง HTTP request อีกแล้วล่ะครับ
2. ผูก CSS ไว้กับ Componentส่วนอีกวิธีนึงก็จะคล้ายๆ กับวิธีแรกนั่นแหละครับ เพียงแต่จะเปลี่ยนมาผูก css ไว้ที่ระดับ component แทน// ไฟล์ MyComponent.jsx// โหลด MyComponent.css มาใช้กับ component นี้
import '../../css/MyComponent.css';
var React = require('react');var MyComponent = React.createClass({
render: function () {
...
}
});
module.exports = MyComponent;
คำถามที่ตามมาก็คือ ข้อดีของการผูก css เอาไว้กับ component นั้นคืออะไร ? ผมขออธิบายอย่างนี้ครับ สมมติแอปเรามีขนาดใหญ่ๆ หน่อย เราอาจจะออกแบบให้มันมีหลาย entry point แล้วแต่ละ entry point ก็อาจจะไม่ได้ใช้ครบทุก component ที่สร้างเอาไว้ครับ ดังนั้น การผูก css เอาไว้กับ component จึงช่วยให้เรามั่นใจว่าโค้ด css ต่างๆ ที่อยู่ใน bundle ของ entry point นั้นๆ จะเป็นโค้ดที่แอปเราจำเป็นต้องใช้อย่างแน่นอนครับ
บางคนอาจจะสงสัยว่า แล้วเราสามารถใช้ 2 วิธี นี้ร่วมกันได้มั้ย ตอบเลยว่าได้ครับ เราอาจจะใช้วิธีโหลดสไตล์ที่ใช้ร่วมกันเอาไว้ที่ตัวแอปไปเลยก็ได้ แล้วพวกสไตล์เฉพาะทางของ component ต่างๆ ก็ให้ผูกไว้กับตัว component แทน เวลา Webpack มันพ่น style ออกมา มันจะรู้เองว่าต้องพ่นสไตล์ของ component ระดับที่ลึกกว่าเอาไว้ทีหลัง เพื่อที่จะสามารถ override สไตล์ของ component ที่อยู่ระดับตื้นกว่าได้นั่นเองครับ3. เพิ่ม Loaderอย่าลืมนะครับว่า Webpack รู้จักแต่ JavaScript เท่านั้น การที่จะโหลด css ได้นั้น ยังไงก็ต้องอาศัย Loader ครับ ให้เราติดตั้ง Loader เพิ่มเข้าไปอีก 2 ตัวnpm install css-loader style-loader --save-devจากนั้นก็ไปเพิ่ม Loader ทั้ง 2 ตัวนี้เข้าไปในไฟล์ webpack.config.js เหมือนเดิมฮะ// ไฟล์ webpack.config.jsmodule.exports = {

...

module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader'
},
{
test: /\.css$/, // ถ้าเจอไฟล์ .css
loader: 'style-loader!css-loader' // ให้ load ไฟล์นั้นด้วย style-loader และ css-loader
}
]
}
};
สาเหตุที่เราจะต้องใช้ Loader ถึง 2 ตัวในการโหลด css ก็เพราะว่า Loader 2 ตัวนี้ มันจะต้องทำงานร่วมกันครับ แต่ถ้าใครเขียน css ด้วย Sass ก็จะต้องไปโหลด Loader มาเพิ่มอีก 2 ตัว
npm install sass-loader node-sass --save-devจากนั้นก็เพิ่ม sass-loader เข้าไปแบบนี้// ไฟล์ webpack.config.js

module.exports = {

...

module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader'
},
{
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.scss$/, // ถ้าเจอไฟล์ .scss
loaders: ["style", "css?sourceMap", "sass?sourceMap"] // ให้ load ไฟล์นั้นด้วย style-loader, css-loader และ sass-loader
}
]
}
};
เรียบร้อยแล้วครับ ให้เราลอง require ไฟล์ css ตาม component ต่างๆ ดู แล้วรัน Webpack ใหม่ จากนั้นก็ลองพรีวิวเว็บดูได้เลย เว็บเราจะต้องหน้าตาสวยงามเหมือนเดิมครับ เพียงแต่โค้ด css ต่างๆ จะย้ายมาฝังอยู่ใน bundle.js หมดแล้วแต่ถ้าเว็บแอปของเรามีความซับซ้อนเอามากๆ ขนาดของ css ก็จะใหญ่ตามไปด้วย user บางคนเข้ามาดูเพียงไม่กี่หน้า แต่ก็ต้องมาโหลดไฟล์ css ที่รวมเอาสไตล์ของทุกหน้าเข้าไปด้วย การเอา css ไปฝังไว้ใน bundle.js แบบนี้มันจะเวิร์คหรอ ? อย่าเพิ่งด่าผมนะครับ เพราะปัญหานี้สามารถแก้ได้ด้วยการทำ Code Splitting เดี๋ยวเราจะมาว่าเรื่องนี้กันอีกทีในหัวข้อ Optimize โค้ดครับใช้ Webpack สร้าง Dev Serverมาถึงตรงนี้ เราก็พอจะเห็นภาพแล้วนะครับว่า Loader นั้นมีวิธีใช้งานอย่างไร ทีนี้เรามาดูอีกหนึ่งฟีเจอร์ของ Webpack ที่น่าสนใจมากๆ เลย นั่นก็คือ มันสามารถทำตัวเป็น web server สำหรับ dev ได้ด้วยครับ ส่วนมันจะมีอะไรดีกว่าการใช้ web server ตัวอื่นๆ นั้น เรามาดูกันเลยฮะ

1. สร้าง Web Server ขึ้นมาก่อน

เริ่มด้วยการติดตั้ง webpack-dev-server เข้ามาอีกตัวครับnpm install webpack-dev-server --save-devจากนั้นให้เราเข้าไปใส่ command สำหรับสร้าง web server เอาไว้ที่ package.json เลยครับ จะได้รันได้สะดวกๆ// ไฟล์ package.json{
...
"scripts": {
"dev": "webpack-dev-server --content-base build/ --hot"
},
...
}
command ด้านบนจะเอาไว้สร้าง dev server ของ Webpack โดยอิงตาม content ที่ path build/ ครับ จากนี้ไปเราก็จะสามารถสร้าง dev server ด้วย Webpack ได้ง่ายๆ แบบนี้เลยฮะ
npm run devเพียงเท่านี้ เราก็จะได้ web server ที่ port 8080 มาใช้งานแล้วล่ะครับ ให้เราลองเข้าไปดูหน้าเว็บได้ที่ http://localhost:8080

2. ทำ Browser ให้รีเฟรชอัตโนมัติได้

ต่อด้วยการทำให้ browser มันอัพเดทตัวเองโดยอัตโนมัติหากมีการแก้ไขโค้ดครับ ขั้นแรกให้เราใส่ script ตัวนึงเข้าไปที่ไฟล์ index.html ของเราแบบนี้ฮะ<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Tutorial by SiamHTML</title>
</head>
<body>
<div id="app"></div>
<script src="http://localhost:8080/webpack-dev-server.js"></script>
<script src="bundle.js"></script>
</body>
</html>
จากนั้นเราจะต้องไปแก้ config ในส่วนของ entry point นิดนึงครับ ให้เราเพิ่ม webpack/hot/dev-server ลงไปแบบนี้// ไฟล์ webpack.config.jsvar path = require('path');module.exports = {

// เพิ่ม 'webpack/hot/dev-server' เข้าไปข้างหน้า
entry: ['webpack/hot/dev-server', path.resolve(__dirname, 'src/js/app.js')],
...};
เสร็จแล้วครับ ให้เราลองรัน web server ดูอีกที แล้วลองแก้ไขโค้ดอะไรก็ได้ดูเลย ผลที่ได้ก็คือ browser มันจะรีเฟรชหน้าเว็บให้เองหากโค้ดที่เราแก้นั้นไปทำให้ bundle.js มีการเปลี่ยนแปลงครับ แล้วที่สำคัญก็คือ หากโค้ดที่เราแก้นั้นเป็น css มันก็จะเปลี่ยนมาใช้วิธี inject สไตล์เข้าไปให้แทน เราจะได้ไม่ต้องมาเสียเวลาโหลดใหม่ทั้งหน้าครับ
3. เปิดใช้ React Hot Loaderแต่สำหรับคนเขียน React แล้ว สิ่งที่เราแก้กันก็มักจะเป็น JavaScript ถูกมั้ยครับ ปัญหาที่เจอแน่ๆ ก็คือ เวลาแก้โค้ดแล้วมันจะรีเฟรชใหม่ทั้งหน้า ไม่เหมือนกับ css แล้วหากเขียน React แล้วก็คงจะรู้ดีว่าพอรีเฟรชแล้ว state ในตอนนั้นๆ ก็จะหายไปหมด ซึ่งทำให้การ debug ของเราไม่ค่อยจะราบรื่นซักเท่าไรครับ มีวิธีไหนมั้ย ที่จะสามารถทำให้มัน inject เฉพาะส่วนที่เราแก้ได้ เหมือนกับตอนที่เราแก้ css ไงไม่น่าเชื่อว่ามีครับ โดยการใช้ tool ที่มีชื่อว่า React Hot Loader ให้เราติดตั้งมันเพิ่มเข้าไปอีกตัวnpm install react-hot-loader --save-devจากนั้นเราจะต้องไปแก้ตรง Loader นิดนึงครับ ให้เราเปลี่ยนจากการใช้ babel-loader เพียงอย่างเดียว มาใช้ react-hot-loader ควบคู่กันไปด้วย// ไฟล์ webpack.config.jsvar path = require('path');
var webpack = require('webpack');

module.exports = {

...
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel'] // เพิ่ม react-hot-loader
}
]
},
// เพิ่ม plugin ที่ต้องใช้ร่วมกับ react-hot-loader
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
};
สุดท้ายให้เราไปที่ไฟล์ที่เป็น entry point ของเรา แล้วเพิ่มโค้ดตามด้านล่างนี้ลงไปฮะ
// ไฟล์ src/js/app.jsvar React = require('react');
var MyComponent = require('./MyComponent.jsx');
// assign ค่าให้ rootInstance
var rootInstance = React.render(<MyComponent />, document.getElementById('app'));
// เพิ่มโค้ดสำหรับเรียกใช้ react-hot-loader
if (module.hot) {
require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({
getRootInstances: function() {
return [rootInstance];
}
});
}
เรียบร้อยแล้วครับ จากนี้ไปเวลาเราแก้โค้ดอะไร component ที่ได้รับผลจากการแก้นั้นก็จะถูก render ใหม่ในทันที แล้ว state ของ component นั้น ก็จะยังคงมีค่าเหมือนเดิมอีกด้วยครับ
Optimize โค้ดด้วย Webpackตอนนี้เราก็คงจะพอเห็นภาพแล้วนะครับว่า web server ที่ Webpack ให้มานั้น ช่วยให้การ dev ของเราสะดวกขึ้นมากแค่ไหน ทีนี้เรามาดูขั้นตอนหลังจากที่ dev เสร็จแล้วอย่างการ optimize โค้ดกันบ้างฮะ ลองนึกดูเล่นๆ นะครับว่า React เปล่าๆ แบบ minified นี่ก็ปาเข้าไป 100KB แล้ว ไหนจะโค้ด vendor ตัวอื่นๆ อีก ไหนจะโค้ดของตัวแอปเราเองอีก สุดท้ายแล้ว bundle.js ของเราจะใหญ่ขนาดไหน ? ถ้าเราไม่ optimize เลยนี่ ผมถือว่าบาป!

0. สร้างไฟล์ Config สำหรับ Production

ก่อนอื่นให้เราแยกไฟล์ config สำหรับ production ออกมาต่างหากเลยครับ เราจะใช้วิธี copy ไฟล์ webpack.config.json มาก็ได้ฮะ แล้วตั้งชื่อไฟล์ใหม่เป็น webpack.production.config.js จากนั้นก็เพิ่ม script สำหรับ deploy เข้าไปในไฟล์ package.json แบบนี้// ไฟล์ package.json

{
...

"scripts": {
"dev": "webpack-dev-server --content-base build/ --hot",
"deploy": "webpack -p --config webpack.production.config.js"
},

...
}
จากโค้ดจะเห็นว่าเราสั่งให้รัน Webpack ด้วย option ที่ให้มันช่วยทำโค้ดให้เล็กลงครับ นอกจากนั้นเรายังบอกให้ Webpack อ่าน config จากไฟล์ webpack.production.config.json ซึ่งเป็นไฟล์ที่เราเพิ่งสร้างขึ้นมานั่นเองฮะ

1. แยกโค้ดแอปออกเป็นหลายๆ Entry Point

สมมติว่าเว็บแอปที่เราทำอยู่มีระบบสมาชิกด้วย เราก็อาจจะแบ่งโค้ดออกเป็น 2 entry point ก็ได้นะฮะ ให้อันนึงเป็น main.js เอาไว้จัดการกับหน้าเว็บทั่วไป ส่วนอีกอันเป็น member.js เอาไว้จัดการกับหน้าที่ต้อง login ก่อน เวลาโหลดก็ให้ทุกหน้าโหลด main.js มาเสมอ ส่วน member.js นี่ให้โหลดเฉพาะหน้าที่จำเป็นต้องใช้เท่านั้นครับ เรามาดูตัวอย่างโค้ดกันเลย// ไฟล์ webpack.production.config.jsvar path = require('path');module.exports = {

// แบ่ง entry point ออกเป็น 2 จุด
entry: {
main: path.resolve(__dirname, 'src/js/main.js'),
member: path.resolve(__dirname, 'src/js/member.js')
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // ใช้ [name] แทนการระบุชื่อไฟล์ตรงๆ
},
module: {

loaders: [

...
]
}
};
จะเห็นว่าเราได้เพิ่ม entry point เป็น 2 จุด แล้วนะครับ นั่นก็คือ main และ member แล้วตรง filename ของ output เราก็จะต้องเปลี่ยนมากำหนดให้เป็น [name] แทน ซึ่งจะเป็นการบอกให้ใช้ชื่อ filename ตามชื่อของ entry point นั่นเองครับ
2. แยกโค้ดของ Vendors ออกมาจากตัวแอปแต่หลังจากที่แยกออกเป็น 2 entry point แล้ว ผลลัพธ์อาจไม่เหมือนที่เราคิดนะครับ เพราะขนาดไฟล์ของ main.js และ member.js รวมกันนั้นกลับใหญ่กว่าตอนที่มีแค่ไฟล์เดียวซะงั้น ที่เป็นเช่นนี้ก็เพราะว่าแต่ละไฟล์มันมีโค้ดที่ซ้ำซ้อนกันอยู่เยอะเลยครับ ที่เห็นชัดๆ เลยก็คือ ทั้ง main.js และ member.js ต่างก็มี React เป็นของตัวเอง อย่างนี้ไม่เวิร์คแน่ๆ ครับวิธีแก้ก็คือ เราจะต้องดึงพวกโค้ด vendors ทั้งหลาย ออกมาจากทั้งสองไฟล์ครับ สมมติเราจะดึง React ออกมา เราก็จะต้องเขียนโค้ดแบบนี้// ไฟล์ webpack.production.config.jsvar path = require('path');
var webpack = require('webpack');

module.exports = {

entry: {
main: path.resolve(__dirname, 'src/js/main.js'),
member: path.resolve(__dirname, 'src/js/member.js'),
vendors: ['react'] // แยก react ออกมาเป็น vendors
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},

module: {

loaders: [

...
]
},

plugins: [

// เซฟ vendors ออกมาเป็นไฟล์ vendors.js
new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js')
]
};
พอรันดู เราก็จะได้มา 3 ไฟล์ ครับ ให้เรากำหนด vendors.js ให้ติดแคชนานๆ ไปได้เลย ส่วนขนาดไฟล์ของ main.js และ member.js ก็คงจะลดลงมากจนเหลือไม่กี่ KB แล้วล่ะครับ
3. แยกโค้ดที่ใช้ร่วมกันออกมาต่างหากนอกจากเราจะสามารถแยกโค้ด vendors ต่างๆ ออกมาจาก bundle ได้แล้ว เรายังสามารถใช้ Webpack แยกโค้ดที่ entry point ต่างๆ ใช้ร่วมกันออกมาเป็นไฟล์ใหม่ได้ด้วยนะครับ ลองดูตัวอย่างโค้ดด้านล่างนี้// ไฟล์ webpack.production.config.jsvar path = require('path');
var webpack = require('webpack');
module.exports = {

entry: {
main: path.resolve(__dirname, 'src/js/main.js'),
member: path.resolve(__dirname, 'src/js/member.js')
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},

module: {

loaders: [

...
]
},

plugins: [

// แยกโค้ดที่ entry point ต่างๆ ใช้ร่วมกันออกมาเป็นไฟล์ commons.js
new webpack.optimize.CommonsChunkPlugin("commons.js")
]
};
เมื่อลองรันดู เราก็จะได้ไฟล์ commons.js เพิ่มเข้ามาครับ แต่ไฟล์ commons.js นั้นจะมีขนาดค่อนข้างใหญ่ เพราะมันจะเก็บโค้ดที่ main.js และ member.js ใช้ร่วมกันเอาไว้ทั้งหมดเลย
4. โหลดเฉพาะโค้ดที่จำเป็นต้องใช้แบบ Asynchronousและที่สุดยอดมากเลยก็คือ Webpack มันสามารถแบ่งโค้ดแอปเราออกเป็นส่วนย่อยๆ แล้วค่อยๆ ทยอยโหลดโค้ดเหล่านั้นมาให้เมื่อถึงเวลาที่จะต้องใช้โดยอัตโนมัติได้ด้วยนะครับ หรือพูดง่ายๆ ก็คือ แทนที่เราจะต้องโหลดไฟล์ js ใหญ่ๆ มาไฟล์เดียว เราก็จะเปลี่ยนมาโหลดไฟล์ js เล็กๆ ไฟล์นึงที่สามารถทำให้แอปมันพอรันได้ก่อน แล้วจึงค่อยๆ โหลดไฟล์อื่นๆ มาทีหลังเมื่อโค้ดในไฟล์นั้นกำลังจะถูกใช้งาน ซึ่งขั้นตอนที่ว่ามานี่ Webpack เป็นคนจัดการให้ทั้งหมดครับ สิ่งเดียวที่เราต้องทำเองก็คือ เราจะต้องเป็นคนกำหนดว่าจะให้ Webpack หั่นโค้ดตรงไหนออกไปเป็นไฟล์ย่อยบ้าง สมมติเรามองว่าโมดูลนี้มันยังไม่จำเป็นต้องใช้หรอก พอจะใช้จริงๆ ค่อยโหลดมาทีหลังก็ได้ เราก็จะต้องปรับโค้ดในส่วนของ require ให้เป็นแบบนี้ครับ// เขียน require แบบปกติ
// require('../utils/Log').print('SiamHTML');
// เขียน require แบบให้แยกโมดูลที่ require ออกมาเป็นไฟล์ย่อย
// แล้วค่อยโหลดไฟล์นั้น เมื่อถึงเวลาต้องใช้
require.ensure([], function() {
require('../utils/Log').print('SiamHTML');
});
แค่นี้เองครับ ให้ลองรัน Webpack ดูอีกครั้งได้เลย เราก็จะได้ไฟล์ใหม่เพิ่มมาอีกอัน ซึ่งเป็นไฟล์ที่เราเพิ่งหั่นออกมานั่นเองครับ เพียงเท่านี้ ไฟล์ js หลักของเราก็จะเล็กลง การโหลดแอปครั้งแรกก็จะใช้เวลาไม่นานเหมือนแต่ก่อนแล้วล่ะครับ
วิธีใช้ Webpack ร่วมกับ Gulpแต่ถ้าเราใช้ task runner อย่าง Gulp อยู่แล้ว เราอาจจะมอง Webpack เป็นแค่ task นึง ของ Gulp ไปเลยก็ได้นะครับ เรามาดูตัวอย่างการเขียน task สำหรับสร้าง dev server ของ Webpack ด้วย Gulp กันเลยครับ// ไฟล์ gulpfile.jsvar gulp = require("gulp");
var gutil = require("gulp-util");
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");
// สร้าง task ใหม่ ชื่อ webpack-dev-server
gulp.task("webpack-dev-server", function(callback) {

// ปรับ config เพิ่มเติมตรงนี้
var myConfig = Object.create(webpackConfig);
myConfig.devtool = "eval";
myConfig.debug = true;
// รัน webpack-dev-server ตาม config ที่กำหนดไว้
new WebpackDevServer(webpack(myConfig), {
contentBase: './build',
hot: true,
stats: {
colors: true
}
}).listen(8080, "localhost", function(err) {
if (err) throw new gutil.PluginError("webpack-dev-server", err);
gutil.log("[webpack-dev-server]", "http://localhost:8080/webpack-dev-server/index.html");
});
});
// ตั้งให้เป็น default task
gulp.task("default", ["webpack-dev-server"]);
เพียงเท่านี้ เราก็จะสามารถผูก Webpack เข้ากับ Gulp ได้แล้วล่ะครับ จริงๆ แล้วยังมีตัวอย่างการเขียน task สำหรับ build ด้วยนะครับ เพื่อนๆ สามารถเข้าไปดูตัวอย่าง gulpfile.js แบบเต็มๆ ได้ที่นี่ฮะ
ความรู้สึกหลังลองใช้ Webpackส่วนตัวผมชอบมันมากๆ เลยนะครับ ถึงแม้ว่าฟีเจอร์หลายๆ อย่าง เราจะสามารถหาได้จาก tool ตัวอื่นๆ แต่ในแง่ของความเร็วบวกกับฟีเจอร์อย่าง Code Splitting และ React Hot Loader นี่ผมว่ามันตอบโจทย์คนเขียน React ได้ดีจริงๆ ฮะ ก่อนหน้านี้ผมเขียนโค้ดไป หงุดหงิดไปเพราะกว่าจะ build เสร็จ นี่ ต้องรอนานหลายวิเลย ตอนนี้รอไม่ถึงวิแล้วครับ เรียกว่าพอกดเซฟทีนี่ Webpack อัพเดทผลลัพธ์ให้เห็นทันทีเลย ตรงนี้ผมมองว่ามันส่งผลโดยตรงต่อความสุขในการเขียนโค้ดเลยล่ะครับขอบอกก่อนนะครับว่าที่ผมเล่ามาทั้งหมดนี้ มันเป็นแค่ส่วนหนึ่งที่ Webpack ทำได้เท่านั้นนะครับ จริงๆ แล้ว มันยังมีอะไรให้เล่นมากกว่านี้อีกเยอะเลย ผมอยากให้เพื่อนๆ ลองเข้าไปอ่านรายละเอียดเพิ่มเติมที่เว็บหลัก เพราะในนั้นจะมีตัวอย่างโค้ดให้ดูเยอะเลย หรือถ้าอยากอ่านเวอร์ชั่นเข้าใจง่ายๆ หน่อยก็ลองดู Cookbook ตัวนี้ก็ได้ฮะ ก็ลองนำมาปรับใช้กับงานของเราดูนะครับ ผมมั่นใจว่าถ้าเราปรับแต่งดีๆ Webpack จะช่วยทำให้ productivity ของเราสูงขึ้นมากกว่าเดิมอย่างแน่นอน

--

--