Babel Coder

[Day #1] แนะนำ Webpack2 และการใช้งานร่วมกับ React

beginner

 บทความนี้เป็นส่วนหนึ่งของชุดบทความ [ชุดบทความ] สอนสร้าง Isomorphic Application ด้วย React.js และ Redux ใน 5 วัน

โลกของ Front-end ปัจจุบัน React ถือเป็นหนึ่งในเฟรมเวิร์คที่มาแรงมากถึงมากที่สุดขณะนี้ ผมเชื่อว่าหลายคนคงรู้จัก React บางคนอาจเคยใช้งานแล้ว แต่มันคงผิดธรรมเนียมเป็นแน่ ถ้าผมละเลยที่จะสรรเสริญสรรพคุณต่างๆของ React การใช้งานด้วย Redux และ module bundler คู่หูอย่าง Webpack

สารบัญ

นิทาน React

กาลครั้งหนึ่งเมื่อ Angular1 เป็นที่นิยม หลายโปรเจคเริ่มใช้ Angular เพื่่อทำ Single Page Application (SPA) ด้วยความง่ายของ Angular ที่สนับสนุนโครงสร้างแบบ MV* จึงง่ายต่อความเข้าใจ ทั้งช่วยจัดการโปรเจคขนาดใหญ่ได้ดีกว่าการใช้ jQuery พื้นฐาน ในช่วงนั้น Ember.js เป็นอีกหนึ่งเฟรมเวิร์คที่ได้รับความนิยม โดยเฉพาะนักพัฒนาจากฝั่ง Ruby on Rails

ทั้ง Angular1 และ Ember1 สนับสนุนการทำ 2-ways data binding หมายความว่าคุณสามารถผูกก้อน model เข้ากับ element ได้ เมื่อใดที่ model อัพเดท element ก็จะอัพเดทตาม ในทางกลับกันเมื่อ element อัพเดท model จะอัพเดทด้วย เช่นเราผูกอีเมล์ไว้กับ input element เมื่อเราพิมพ์อีเมล์ลงไปยังช่องนั้น model ที่เราระบุจะอัพเดทอีเมล์อัตโนมัติโดยเราไม่ต้องเขียนโค๊ดจัดการกับอีเวนต์

ทุกอย่างไม่ได้สวยงามทั้งหมด เมื่อ Google bot ไม่รัก JavaScript ปัญหาการทำ SEO จึงบังเกิด เมื่อเราใช้ Angular/Ember ทำ SPA โดยอาศัยการโหลดเนื้อหามาใส่ในหน้าเพจผ่าน AJAX ทำให้ตอนแรกสุดหลังเราเข้าเว็บจะได้ไฟล์ที่ไม่มีเนื้อหา มีแต่โค๊ด AJAX ที่ไปโหลดเนื้อหามาแปะอีกทีนึง ดังนั้นเมื่อ Google bot ไต่มาที่เว็บคุณ มันจึงได้แค่ HTML ที่ไม่มีเนื้อหาอะไรกลับไป ครั้นจะให้มันรอเนื้อหาของเพจด้วย AJAX ก็ไม่ได้เช่นกัน เพราะบอทไม่ได้เรียนเขียน JavaScript มาหนะซิ เมื่อเป็นเช่นนี้ เราจึงต้องหวังพึ่งเครื่องมือที่ช่วยแปลงเพจของเราให้เป็น HTML แบบมีเนื้อหาพร้อม เมื่อบอทมาเยือนก็ส่งเอกสารนี้ให้ แต่หากเป็นผู้ใช้งานระบบก็เอา HTML ฉบับโหลดเนื้อหาด้วย AJAX ส่งกลับมา เทคนิดนี้เรียก prerender

ย้อนให้เก่าไปอีกนิด เมื่อก่อนเราสร้าง HTML ที่มีเนื้อหาครบถ้วนจากฝั่ง server แล้วส่งมาที่บราวเซอร์ของผู้ใช้งานเลย แน่นอนว่าเร็ว เพราะเมื่อบราวเซอร์ได้รับข้อมูลแล้ว แทบไม่ต้องทำอะไรต่อ ผิดกับการใช้ Angular/Ember เมื่อคุณร้องขอเพจ คุณได้เพจกลับมาแน่นอน สิ่งที่คุณคาดหวังคืออยากได้เพจที่มีเนื้อหาให้อ่าน แต่สิ่งที่คุณได้กลับมาจริง เป็นเพียงเพจที่อุดมไปด้วย JavaScript เมื่อบราวเซอร์ได้รับเพจแล้ว จึงประมวลผล JavaScript เพื่อใช้ AJAX ไปโหลดเนื้อหาอีกที ฉะนั้น request แรกจึงช้าเพราะต้องรอโหลด JavaScript ทั้งหมดก่อนจึงได้ฤกษ์มงคลต่อการโหลดเนื้อหา

ที่พิมพ์มาทั้งหมดนี่ไม่ได้หมายความว่า Ember หรือ Angular ไม่ดีนะครับ แต่บทความนี้กล่าวถึง React จึงจำเป็นต้องทำการขายของและยกขึ้นหิ้งไว้ก่อน เรื่องของเฟรมเวิร์กหรือไลบรารี่เป็นเรื่องนานาจิตตังครับ ใครใคร่ใช้ตัวไหน ถนัดตัวไหน ตัวไหนใช้แล้วงานเสร็จไวกว่า ก็จัดตัวนั้นไปเลยครับ

การปรากฎตัวของ React.js ในเวลาถัดมา ทำให้โลกสั่นคลอนไปถึงชั้นแมกม่า เหตุเพราะ React มาพร้อมกับสิ่งเหล่านี้…

  • ลาก่อย 2-ways data-binding React สนับสนุนให้ทำ one-way data flow แทน เนื่องจากเข้าใจได้ง่ายกว่า ตรงไปตรงมาและได้ประสิทธิภาพที่สูงกว่า

  • Virtual Dom ทุกครั้งที่เราสร้าง element ของ HTML ขึ้นมาใหม่หรือเปลี่ยนแปลงค่าจะมีการอัพเดท DOM ซึ่งใช้เวลาในการดำเนินการสูง ยิ่งเรามี node เยอะก็ยิ่งเสียเวลาในการอัพเดท DOM ด้วยเหตุนี้ React จึงไม่จัดการกับ DOM โดยตรง แต่ทำผ่าน Virtual DOM กล่าวคือ React จะสร้าง DOM เสมือนไว้ในหน่วยความจำ เมื่อโปรแกรมทำงาน มีการอัพเดทข้อมูลของคอมโพแนนท์เป็นเหตุให้ต้องอัพเดท DOM ตาม มันจะไปเช็คกับ Virtual DOM ก่อนว่าข้อมูลไหนที่จำเป็นต้องอัพเดท DOM ของจริงบ้าง ค่าไหนที่ไม่เปลี่ยนแปลงก็จะไม่ทำอะไร

Virtual Dom

  • Server-side rendering เมื่อ Request แรกมันช้าเพราะต้องรอโหลด JavaScript ก่อนถึงจะโหลดเนื้อหา เราก็ทำให้มันเร็วด้วยการส่ง Response กลับมาจาก server พร้อมเนื้อหาไปเลยซิ นั่นคือ Server-side rendering

  • Universal Application JavaScript รู้ภาษาเดียวทำได้ทุกส่วน React ก็เช่นกันรู้ตัวเดียวเที่ยวได้รอบโลก คุณสามารถใช้ React สร้าง Mobile Application ด้วย React Native และใช้ React DOM สร้าง Web Application ได้ ที่เป็นเช่นนี้เพราะว่า React แยกส่วนที่ขึ้นตรงกับ platform แยกเป็นอีก package ตัว React เองจึงเป็นไลบรารี่ที่ใช้ได้ทุกที่

  • JavaScript-centric React ใช้ฟอร์แมต JSX ที่อนุญาตให้คุณสร้างคอมโพแนนท์ด้วย JavaScript คุณจึงไม่ต้องเรียนรู้อะไรใหม่ แค่รู้ JavaScript พอ ผิดกับ Angular คุณต้องเรียนรู้ไวยากรณ์ใหม่ๆ เช่น <button (click)="onClickMe()">Click me!</button> ใช้ (event_name)=“callback” สำหรับผูกอีเวนต์ หรือคุณต้องเรียนรู้ไวยากรณ์ของ Handlebar เมื่อใช้ Ember.js นั่นเป็นเพราะทั้งสองตัวนี้เป็น HTML-centric หรือจัดการทุกอย่างด้วยการขยายความสามารถของ HTML tag นั่นเอง

หลังจาก React ออกมาไม่นาน ทั้ง Angular และ Ember ต่างทำการบ้านเป็น version 2 ที่นำความสามารถต่างๆของ React มาใส่ในเฟรมเวิร์คตัวเอง ตอนนี้ทั้ง Angular2/Ember2 ใช้เทคนิค Virtual DOM ทั้งคู่ สนับสนุนแนวคิด one-way data flow และทำ Server-side rendering ได้แล้ว (Ember ทำผ่านโปรเจค Fastboot ที่ยังไม่ค่อยสมบูรณ์มากนัก)

เริ่มสร้างโปรเจค React ด้วย Webpack2

ลองจินตนาการถึงการจัดการโครงสร้างโปรเจคขนาดใหญ่ แน่นอนว่าไม่ได้เขียนทั้งหมดลงไฟล์เดียวแน่ อย่างน้อยก็ต้องแบ่ง JavaScript, CSS แยกเป็นคนละไฟล์ นอกจากนี้แต่ละไฟล์หรือคอมโพแนนท์ยังสามารถเรียกกันและกันได้ หน้าเพจ Dashboard และ AboutUs มีการเรียก Header ที่เป็นส่วนหัวด้านบนของทุกๆเพจ คำถามคือ เราจะจัดการการเรียกไฟล์แบบโยงไยไปมาอย่างไร? ในแต่ละคอมโพแนนท์เช่น AboutUS ก็จะมี Stylesheet (CSS) เป็นของตนเองที่ไม่เกี่ยวกับเพจอื่น เราจะแยก CSS นี้ให้อิสระจากเพจอื่นอย่างไร? จะทำอย่างไรเวลาเข้าเว็บแล้วไม่โหลด JavaScript/CSS ทั้งหมดมาในครั้งเดียว แต่โหลดเฉพาะที่ใช้ในเพจนั้น เมื่อเข้าหน้าอื่นค่อยโหลดที่ต้องการใช้จริงๆมา? แต่เดี๋ยวนะนี่เราจะเขียน React ด้วย ES2015 หนิ มันจะใช้บนเว็บได้จริงหรอ บราวเซอร์ก็ยังไม่สนับสนุนทุกฟังก์ชันหนิ? ทั้งหมดนี้คือปัญหาด้านการจัดการสิ่งที่เราเรียกว่า “module” เราจึงต้องหาเครื่องมือมาจัดการกับ module ของเรา และเครื่องมือที่เป็นพระเอกของเราก็คือ… แท่นแท๊นน Webpack

Webpack

เริ่มกำราบ module ที่ซับซ้อนด้วย Webpack

Webpack เป็นเครื่องมือจัดการกับ module ที่ไม่ได้ตรัสรู้อะไรด้วยตนเอง เราต้องบอกมันว่าต้องการให้ทำอะไร ให้ผลลัพธ์เป็นอะไร ในหัวข้อนี้เราจะเริ่มดูปัญหาต่างๆที่สามารถจัดการด้วย Webpack ได้จากจุดที่เล็กที่สุดไปถึงระดับการใช้งานจริง

เนื่องจากชุดบทความนี้ใช้ Webpack และ React ในการเดินเรื่อง จึงจำเป็นต้องทำการติดตั้งก่อน เริ่มจากสร้างไฟล์ package.json โดยมีส่วนประกอบดังนี้

{
  "name": "babelcoder-wiki",
  "version": "1.0.0",
  "description": "BabelCoder Wiki!",
  "main": "index.js",
  "scripts": {
    "start": "webpack --watch"
  },
  "author": "Nuttavut Thongjor",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^2.1.0-beta.7"
  },
  "dependencies": {
    "react": "^15.0.2",
    "react-dom": "^15.0.2"
  },
  "peerDependencies": {
    "react": "^15.0.2",
    "react-dom": "^15.0.2"
  },
  "engines": {
    "node": "6.0.0",
    "npm": "3.8.6"
  }
}

รวมไฟล์เป็นหนึ่งเดียว

เราเขียนโค๊ดโดยแยกแต่ละคอมโพแนนท์ออกจากกันคนละไฟล์ เช่น Dashboard.js สำหรับคอมโพแนนท์ Dashboard และ Article.js สำหรับคอมโพแนนท์ Article แต่สุดท้ายเราต้องการ JavaScript ไฟล์เดียวเพื่อสะดวกต่อการเรียกใช้งาน พิจารณา HTML ต่อไปนี้

<!doctype html>
<html>
  <head>
    <meta charset='utf-8'>
    <title>Babel Coder Wiki</title>
  </head>
  <body>
    <div id='app'></div>
    <script src='/static/bundle.js'></script>
  </body>
</html>

จะมีกี่ไฟล์ กี่คอมโพแนนท์เราไม่สนใจ แต่เราสนแค่ว่าต้องการผลลัพธ์สุดท้ายคือไฟล์ /static/bundle.js เพื่อเรียกใช้งานใน index.html เอาหละถึงตรงนี้เราต้องสร้างไฟล์ config ของ Webpack ขึ้นมาก่อน ผมตั้งชื่อไฟล์ว่า webpack.config.js พร้อมกำหนดค่าต่างๆตามนี้

const webpack = require('webpack');
const path = require('path');

module.exports = {
  // เปิดใช้งาน sourcemap ด้วยโหมด eval
  devtool: 'eval',
  
  // ตรงจุดนี้สำคัญครับ! จุดเริ่มต้นของโปรแกรมเราคือ index.js
  // Dashboard.js หรือ Article.js จะเข้าถึงได้ก็ต้องผ่านไฟล์นี้
  // เราจึงบอกว่า index.js เป็น "entry" หรือทางเข้าถึงของโมดูลอื่น
  entry: './index.js',
  output: {
    publicPath: '/static/',
    path: path.join(__dirname, 'static'),
    
    // หลังจากรวมร่างทุกไฟล์เข้าเป็นไฟล์เดียวแล้ว ให้ไฟล์เดียวนั้นชื่ออะไร
    filename: 'bundle.js'
  }
};

จากการตั้งค่าข้างบนจะได้ว่า เรามีไฟล์ index.js เป็นต้นทาง หรือ entry สำหรับเข้าถึงไฟล์อื่นๆ module หรือไฟล์ที่ index.js สามารถเรียกได้ขอให้รวมกันเป็นก้อนเดียวชื่อไฟล์ bundle.js แล้วนำไฟล์นี้ไปวางไว้ที่ static

เอาหละถึงเวลาสร้างไฟล์ index.js แล้ว ขึ้นชื่อว่าสอนศาสตร์การเขียนโปรแกรม ไม่มีที่ไหนไม่เริ่มต้นด้วยการไหว้ครู มาพูด Hello World ฉบับ React กันเถอะ สำหรับตรงนี้ใครไม่เข้าใจไม่เป็นไรนะครับ ผมจะอธิบายเรื่องนี้อีกครั้งเมื่อกล่าวถึง React

// index.js
import React, { Component } from 'react'
import { render } from 'react-dom'

export default class HelloWorld extends Component {
  render() {
    return (
      <h1>Hello World</h1>
    )
  }
}

render(<HelloWorld />, document.getElementById('app'))

คราวนี้เราจะให้ webpack ช่วยสร้างไฟล์ให้พร้อมใช้งานบนบราวเซอร์ให้ที ผลลัพธ์สุดท้ายควรได้ /static/bundle.js ออกมาตามที่ได้ตั้งค่านะครับ เราเพิ่มคำสั่งนี้ลงไปใน package.json จะได้ง่ายต่อการรัน

{
  ...
  "main": "index.js",
  "scripts": {
    // เมื่อออกคำสั่ง npm start จะไปเรียก webpack ให้ทำตามสิ่งที่เราระบุใน webpack.config.js
    "start": "webpack"
  },
  "author": "Nuttavut Thongjor",
  ...
}

จากนั้นจึงสั่งรัน npm start… เป็นไงครับ ไม่ได้หรอ?? ได้ error แบบข้างล่างนี้ซะงั้น

Need Loader

ทำความรู้จัก Loader

มีสองสิ่งเกิดขึ้น อย่างแรกคือเราใช้ฟอร์แมต JSX สร้าง React JSX เป็น syntax ของ React ที่อนุญาตให้คุณเขียน JavaScript ผสม HTML tag ได้ แต่น่าเสียดาย Webpack ไม่เข้าใจ JSX คืออะไร! ด้วยเหตุนี้เราจึงต้องหาคนกลางมาจัดการอะไรซักอย่างก่อน ในที่นี่คือหาคนกลางมาแปลง JSX ก่อนให้ Webpack ทำงานต่อไป และนั่นคือหน้าที่ของสิ่งที่เรียกว่า Loader

ปัญหาถัดมาคือ เราต้องการใช้ ES2015 รวมถึงบางส่วนของ ES7 ด้วย จึงต้องมี Loader มาแปลงสิ่งเหล่านี้เป็น ES5 ที่บราวเซอร์ทั่วไปเข้าใจ เราจึงใช้ babel-loader แก้ปัญหาที่กล่าวมา เพิ่มบรรทัดต่อไปนี้ลงใน package.json แล้วสั่ง npm install ได้เลย

{
  ...
  "devDependencies": {
    "babel-core": "^6.8.0",
    "babel-loader": "^6.2.4",
    "babel-plugin-transform-runtime": "^6.8.0",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "babel-preset-react-hmre": "^1.1.1",
    "babel-preset-stage-0": "^6.5.0",
    "webpack": "^2.1.0-beta.7"
  },
  "dependencies": {
    "babel-runtime": "^6.6.1",
    "react": "^15.0.2",
    "react-dom": "^15.0.2"
  },
  ...
}

เนื่องจาก babel-loader จะอ่าน config จากไฟล์ .babelrc เราจึงต้องสร้างไฟล์ดังกล่าวขึ้นมา พร้อมใส่ค่าดังนี้

// เป็นการบอกว่าใช้ es2015, ใช้โหมด stage-0 ของ babel และให้แปลง react
{
  "presets": ["es2015", "stage-0", "react"]
}

ใส่ Loader เข้าไปใน webpack.config.js ดังนี้

module.exports = {
  devtool: 'eval',
  entry: './index.js',
  output: {
    publicPath: '/static/',
    path: path.join(__dirname, 'static'),
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        // ใช้ Regular Expression ทดสอบ ถ้าไฟล์ไหนลงท้ายด้วย js หรือ jsx
        // ให้ใช้ babel-loader
        test: /\.(js|jsx)$/,
        
        // ไม่รวม node_modules เนื่องจากเป็นของที่คนอื่นเขียน
        // เราไม่ต้องใส่ใจ
        exclude: /node_modules/,
        loaders: [
          'babel-loader'
        ]
      }
    ],
  }
};

เมื่อเสร็จแล้ว จะรออะไรอยู่เล่า สั่งรัน npm start ได้เลยครับ ถึงตรงนี้คุณควรได้ไฟล์ bundle.js ออกมาแล้ว

ใช้งาน Webpack Dev Server

แม้เราจะได้ไฟล์ bundle.js ออกมาสมใจ แต่จะเกิดประโยชน์อะไรถ้าไม่มี server ไว้ส่งไฟล์ Webpack เข้าใจจุดนี้ จึงมี Webpack Dev Server ไว้สร้าง server สำหรับใช้งานใน development ครับ (อย่าเอาไปใช้ใน production นะ) เริ่มติดตั้งด้วยการออกคำสั่งต่อไปนี้

npm i --save-dev [email protected]

ต่อจากนี้เราให้ Webpack Dev Server จัดการงานต่างๆให้เราแทน เปลี่ยนคำสั่ง start ใน package.json ดังนี้

{
  ..
  "scripts": {
    "start": "webpack-dev-server --hot --inline"
  }
  ..
}

Hot Module Replacement

hot และ inline ที่เราใส่เข้าไปใน webpack-dev-server คืออะไร?

Webpack มี feature หนึ่งที่เราเรียกว่า Hot Module Replacement (HMR) ที่จะคอยตรวจสอบว่าโมดูลไหนมีการแก้ไข เมื่อพบว่าโมดูลไหนเปลี่ยนแปลง Webpack จะส่งการเปลี่ยนแปลงนั้นไปอัพเดทในหน้าเว็บให้อัตโนมัติ ทั้งนี้การอัพเดทที่ว่าไม่ใช่การ refresh หน้าเพจนะครับ แต่เป็นการอัพเดทเฉพาะโมดูลโดยไม่โหลดเพจใหม่ทั้งหมด ตัวอย่างเช่น เรามีไฟล์ CSS ที่แสดงปุ่มเป็นสีแดง ต่อมาเราแก้ไข CSS ให้ปุ่มเป็นสีเขียว Webpack จะนำส่วนต่างไปแทนที่เฉพาะปุ่มหรือโมดูลนั้น โดยไม่กระทบส่วนอื่นและไม่มีการโหลดเพจใหม่ทั้งหมด เห็นไหมครับว่าชีวิตโหมด development ของเราสะดวกขึ้นเยอะ และนั่นคือการอธิบายว่า --hot คืออะไร

Hot Module Replacement ฟังดูสตรองมากก็จริงครับ แต่ถ้า Loader ไม่สนับสนุนก็จบ กรณีการแก้ไข CSS โชคดีที่เราใช้ style-loader ที่สนับสนุน HMR อยู่แล้วจึงไม่เกิดปัญหา แต่ถ้า Loader ไม่สนับสนุนหละ? มันก็ไม่เกิดอะไรขึ้นไงครับ ฉะนั้นแล้วเราจึงใส่ --inline เข้าไป เพื่อบอกว่า เห้ยๆในเมื่อแกไร้น้ำยาจะ HMR อย่างน้อยก็ช่วย reload หน้าเพจให้ก็ยังดี

ProTips! คุณสามารถใช้ react-hot-loader เพื่อให้ React Component สามารถ Hot Reload ได้ ก่อนหน้านี้เจ้าของโปรเจคแนะให้ใช้ react-transform-hmr แทน แต่ปัจจุบันขณะที่เขียนบทความนี้ react-hot-loader ออกเวอร์ชัน 3.0.0 beta-1 แล้ว ทั้งนี้เจ้าของโปรเจคแนะนำให้กลับมาใช้ตัวนี้แทนครับ

สร้างสีสันให้ชีวิต อย่างมีสไตล์

ถ้าเว็บของเรามีแต่ขาวๆดำๆคงจะจืดชืดน่าดูครับ สิ่งที่ปรากฎในหัวข้อนี้ใช้ในการอธิบายนะครับ ในทางปฏิบัติของการกำหนดสไตล์ให้ element จะนำเสนอในบทความต่อๆไป คุณสามารถอ่านเพิ่มเติมเรื่อง ตั้งชื่อคลาสใน CSS อย่างไรดี? จาก Global CSS สู่ BEM และ Local CSS

เริ่มจากสร้างไฟล์ชื่อ styles.scss ขึ้นมาก่อนครับ สังเกตนะครับในที่นี้ผมจะเขียนสไตล์ด้วย SCSS เรามาลองดูกันว่าจะทำให้ Webpack รู้จักมันได้อย่างไร

h1 {
  color: red;
}

จากนั้นเรียกมันเข้ามาใช้งานในคอมโพแนนท์ของเรา แก้ไขไฟล์ index.js ตามนี้ครับ

import React, { Component } from 'react'
import { render } from 'react-dom'
import './styles.scss'

export default class HelloWorld extends Component {
  render() {
    return (
      <div>
        <h1>Hello World</h1>
      </div>
    )
  }
}

render(<HelloWorld />, document.getElementById('app'))

Webpack ก็จะบอกเราว่า You may need an appropriate loader to handle this file type. เห้ยๆ ฉันไม่รู้จักไอ้เจ้า SCSS นี่นะ เราจึงต้องเพิ่ม Loader ให้มันดังนี้

...
loaders: [
  {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    loaders: [
      'babel-loader'
    ]
  },
  {
    // สำหรับไฟล์นามสกุล css ให้ใช้ Loader สองตัวคือ css-loader และ style-loader
    test: /\.css$/,
    loaders: [
      'style-loader',
      'css-loader'
    ]
  }, {
    // ใช้ Loader สามตัวสำหรับ scss
    test: /\.scss$/,
    exclude: /node_modules/,
    loaders: [
      'style-loader',
      {
        loader: 'css-loader',
        query: {
          sourceMap: true
        }
      },
      {
        loader: 'sass-loader',
        query: {
          outputStyle: 'expanded',
          sourceMap: true
        }
      }
    ]
  }
]
...

อย่าลืมติดตั้ง Loader ต่างๆผ่าน NPM ดังนี้

npm i --save-dev css-loader style-loader sass-loader node-sass

พิจารณา test: /\.scss$/ นะครับ จะพบว่ามีการใช้ Loader ถึงสามตัวคือ style-loader, css-loader และ sass-loader โดย sass-loader มีการตั้งค่าเพิ่มเติมผ่าน query เช่นให้รวม sourcemap ด้วย คำถามที่หลายคนอาจสงสัยคือ เราสลับที่ Loader ได้ไหม คือตัว c ต้องมาก่อนตัว s ซิ ขอเอา css-loader ขึ้นก่อน style-loader แล้วกัน

คำตอบคือ ไม่ได้ครับ! เพราะการทำงานของ Loader นั้นต่อเนื่องโดยจะเริ่มการทำงานที่ Loader ตัวล่างสุดก่อนเสมอ ฉะนั้นแล้วสำหรับ .scss sass-loader จะทำหน้าที่แปลงไฟล์ scss ให้เป็น css ปกติธรรมดาก่อน จากนั้น css-loader จะรับช่วงต่อไปทำงานเพื่อให้ Webpack เข้าใจว่า CSS คืออะไรด้วยการแปลงเป็นก้อน JSON สุดท้าย style-loader ถึงมารับช่วงต่อ

จุดสำคัญที่อยากอธิบายเพิ่มอยู่ตรง style-loader ครับ เพราะมันจะรับก้อน JSON จาก css-loader มาแปะลงใน style tag ดังรูปข้างล่าง

Style Loader

เมื่อเราเข้าหน้า index.html แม้เราไม่ได้แปะ <link rel='stylesheet' href='styles.css'> CSS ก็ยังทำงาน นั่นเป็นเพราะ styles.scss เป็น dependency ของ index.js เพียงแค่เรียก index.js ก็ได้ของแถมมาพร้อมกัน และนี่คือความสามารถของ Webpack ที่เป็น Module Bundler

คราวนี้เราลองใส่ animation ให้กับข้อความเราบ้าง โดยให้ข้อความของเราค่อยๆเปลี่ยนสีจากเหลืองไปแดง ดังนี้

h1 {
  animation: text-animation 2s infinite;
}

@keyframes text-animation {
  0% {
    color: yellow;
  }
  100% {
    color: red;
  }
}

ข้อความ Hello World ของเราดูดีขึ้นทันทีใช่ไหมครับ แต่ถ้าเราสังเกตดีๆจะพบว่า animation ของเราอาจไม่สามารถทำงานได้บนทุกๆบราวเซอร์เพราะเราไม่ได้ใส่ vendor prefix เข้าไป เช่น -webkit-animation ครั้นเราจะใส่ prefix เข้าไปในโค๊ด CSS ของเราก็ดูยุ่งยากเกินไป ดังนั้นเราจะใช้ autoprefixer ช่วยใส่ prefix ให้เราอัตโนมัติครับ ก่อนอื่นเราต้องติดตั้งมันก่อน ดังนี้

npm i --save-dev postcss-loader autoprefixer

จากนั้นเข้าไปตั้งค่าให้ใช้ autoprefixer ใน webpack.config.js ดังนี้

const webpack = require('webpack');
const path = require('path');
const autoprefixer = require('autoprefixer');

module.exports = {
  devtool: 'eval',
  entry: './index.js',
  output: {
    publicPath: '/static/',
    path: path.join(__dirname, 'static'),
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: [
          'babel-loader'
        ]
      },
      {
        test: /\.css$/,
        loaders: [
          'style-loader',
          'css-loader'
        ]
      }, {
        test: /\.scss$/,
        exclude: /node_modules/,
        loaders: [
          'style-loader',
          {
            loader: 'css-loader',
            query: {
              sourceMap: true
            }
          },
          {
            loader: 'sass-loader',
            query: {
              outputStyle: 'expanded',
              sourceMap: true
            }
          },
          'postcss-loader' // เพิ่ม postcss
        ]
      }
    ]
  },
  postcss: function () {
    return [autoprefixer]; // สั่งให้ autoprefix ให้เรา
  }
};

จากนั้นสั่ง npm start แล้วรอดูผลลัพธ์บนบราวเซอร์ได้เลยครับ จะพบว่า Webpack ใส่ prefix ให้เราเป็น

h1 {
  -webkit-animation: text-animation 2s infinite;
          animation: text-animation 2s infinite;
}

Local CSS

จากบทความ ตั้งชื่อคลาสใน CSS อย่างไรดี? จาก Global CSS สู่ BEM และ Local CSS ทำให้เราทราบว่าปัญหาการตั้งชื่อคลาสของ CSS นั้นเป็นของยาก ในโลกความเป็นจริงเช่นกัน คงไม่มีใครกำหนดสไตล์ให้ h1 สำหรับข้อความ สวัสดีชาวโลก แบบที่เราทำกันอยู่ เพราะมันกระทบทั้งหมด ทุกๆหน้าที่มี h1 จะโดนไปหมด เราลองมาใช้ Webpack จำกัดเขตให้สไตล์ของเรามีผลเฉพาะคอมโพแนนท์กันเถอะ

เอาหละ เริ่มจากบอก css-loader ว่าเราจะใช้โหมด local-css ด้วยการใส่ query ว่า module: true

// webpack.config.js

{
  loader: 'css-loader',
  query: {
    sourceMap: true,
    module: true,
    localIdentName: '[local]___[hash:base64:5]'
  }
}

ขอให้สังเกตตรง localIdentName นะครับ เราเพิ่มสิ่งนี้เข้าไปเพื่อบอก css-loader ว่า แต่ละคอมโพแนนท์ที่เรียกใช้สไตล์ ให้ css-loader ช่วยเปลี่ยนชื่อคลาสเป็น [CLASS_NAME]___[HASH 5 ตัวอักษร] ให้ที เช่นเปลี่ยน .greeting เป็น greeting___2v8wf ที่ต้องทำเช่นนี้ เพื่อให้แต่ละคอมโพแนนท์มี hash ไม่เหมือนกัน ชื่อคลาสจะได้ไม่ชนกัน เมื่อชื่อคลาสไม่ชนกัน CSS ของเราก็จะมีผลเฉพาะจุดไม่ไปปะปนกับ .greeting ในคอมโพแนนท์อื่น

จากนั้นไปเพิ่มคลาสให้ข้อความสวัสดีชาวโลกของเราซะหน่อย อย่ากำหนดสไตล์ให้กับ h1 อีกต่อไปเลย มันบาป!

// styles.scss

.greeting {
  animation: text-animation 2s infinite;
}

เนื่องจากเราต้องการให้สไตล์มีผลเฉพาะจุด เราจึงต้อง import มันเข้ามาในชื่อของ styles แล้วเรียก selector greeting ของเราผ่าน styles อีกทีดังนี้

// index.js

import React, { Component } from 'react'
import { render } from 'react-dom'

// import สไตล์เข้ามาในชื่อ styles
import styles from './styles.scss'

export default class HelloWorld extends Component {
  render() {
    return (
      <div>
        {/* ให้ข้อความของเราประยุกต์สไตล์ของคลาส greeting แบบเฉพาะจุด */}
        <h1 className={styles.greeting}>Hello World</h1>
      </div>
    )
  }
}

render(<HelloWorld />, document.getElementById('app'))

กลับมาดูผลลัพธ์ของเราในบราวเซอร์กัน พบว่าชื่อคลาสของเราเปลี่ยนไป ดังนี้

<h1 class='greeting___2v8wf'>Hello World</h1>

และ Stylesheet ของเราก็เปลี่ยนชื่อคลาสเช่นกัน

.greeting___2v8wf {
  -webkit-animation: text-animation 2s infinite;
          animation: text-animation 2s infinite;
}

บทความในวันนี้ขอเสนอเท่านี้ครับ ในบทความต่อๆไปของชุดนี้เรายังต้องยุ่งกับ Webpack อีกหลายส่วน ไม่ว่าจะเป็นการใช้งาน Plugins การทำ Code Spliting หรือ Webpack สำหรับ production build ขอให้ระลึกเสมอว่าสิ่งที่ปรากฎในบทความนี้เป็นเพียงการสร้างเพื่ออธิบาย ในความเป็นจริงไม่มีใครสร้างคอมโพแนนท์ใน index.js หรือยัดสีแดงเข้าไปใน h1 ตรงๆครับ

บทความหน้าเราจะเริ่มพูดถึงการใช้งาน React รวมไปถึงการจำลอง server ขึ้นมาเพื่อให้ React Application ของเราได้ดึงข้อมูลไปใช้งาน ฝากติดตามด้วยนะครับ (ยิ้มแฉ่ง)

คุณสามารถดูโค๊ดทั้งหมดที่เสนอไปในวันนี้ได้ที่ Github ครับ

มีอะไรใหม่บ้างใน Webpack2

ในชุดบทความนี้เราใช้ Webpack เวอร์ชัน2 ในการทำงานร่วมกับ React แม้จะมีสถานะเป็นเวอร์ชันเบต้าในขณะที่เขียนบทความนี้ก็ตาม สำหรับท่านใดที่เคยใช้ Webpack เวอร์ชัน1มาก่อน ลองมาดูกันครับว่ามีอะไรเปลี่ยนแปลงไปบ้าง

ES2015 Module

Webpack2 ตอนนี้เข้าใจ import/export แล้วโดยไม่ต้องผ่านการแปลงเป็น CommonJS ก่อน

Tree-shaking

Webpack1 ไม่สนับสนุน ES2015 Module ทำให้ทุกครั้งที่รวมไฟล์ (bundle) ต้องแปลงประโยค import/export เป็น require ใน CommonJS ก่อน ข้อเสียของวิธีนี้คือถ้าเรา export โมดูลหลายตัวแต่ไม่ได้ใช้งานมันเลย โมดูลเหล่านั้นก็ยังคงหลอกหลอนเราในไฟล์ผลลัพธ์ที่ได้จากการ bundle ด้วย

ไม่เป็นเช่นนั้นสำหรับ Webpack2 เนื่องจาก Webpack2 สนับสนุน ES2015 Module ในตัวเองเลย ทำให้มันเข้าใจประโยค import/export โดยไม่ต้องแปลงเป็น CommonJS จังหวะที่มันรวมไฟล์เป็นหนึ่ง มันจึงสามารถใช้คุณสมบัติของ import/export ได้คือการกำจัดโมดูลส่วนเกินที่ไม่ใช้งาน เราเรียกอัลกอริทึมในการกำจัด dead code นี้ว่า Tree-shaking ดังนี้

// myLib.js
export function util1() {}
export function util2() {}

// index.js
import { util1 } from './myLib.js'
util1()

จากโค๊ดข้างบนพบว่าเรา export util1 และ util2 จากโมดูล myLib แต่กลับ import แค่เพียง util1 เท่านั้น ทำให้ util2 เป็นของที่ไม่ได้ใช้งาน ด้วยความสามารถของ ES2015 Module มันจึงกำจัดส่วนเกินออกในไฟล์ bundle เหลือเพียงดังนี้

function util1() {}
util1()

เมื่อไม่ export ทุกสรรพสิ่งรอบจักรวาลแบบที่เป็นใน CommonJS ดังนั้น Webpack2 จึงช่วยทำให้ไฟล์ bundle ของคุณผอมเพรียว เล็กกะจิดริดมากขึ้น

เปลี่ยนวิธีการตั้งค่า Loader

จากเดิมเราสามารถเรียก Loader แบบต่อเนื่องได้ด้วยการใส่ ! เข้าไประหว่าง Loader และใช้ ? เพื่อใส่เงื่อนไขเข้าไปในแต่ละ Loader ดังนี้

loaders: [
  {
    test: /\.scss$/,
    loader: 'style!css!sass?outputStyle=expanded&sourceMap',
    exclude: /node_modules/
  }
]

แต่สำหรับ Webpack2 เราอาศัยโครงสร้างแบบซ้อนกันแทน ! และใช้ query แทนการใช้ ? ดังนี้

loaders: [
  'style-loader',
  {
    loader: 'css-loader',
    query: {
      sourceMap: true
    }
  }
  {
    loader: 'sass-loader',
    query: {
      outputStyle: 'expanded',
      sourceMap: true
    }
  }
]

Code Spliting

Webpack1 เราใช้ require.ensure เพื่อโหลดโมดูลเมื่อต้องการใช้จริง (Dynamically loaded at runtime) ดังนี้

function onClick() {
  require.ensure('./module1', function(require) {
    var a = require('module1);
  });
}

คำถามคือ แล้วถ้าเราโหลดโมดูลไม่สำเร็จหละ จะจัดการอย่างไร? สำหรับ Webpack2 มาพร้อม System.import ที่จะทำให้ชีวิตคุณง่ายขึ้น ตอนนี้เราจัดการกับข้อผิดพลาดในการโหลดโมดูลได้แล้ว

function onClick() {
  System.import('./module1').then(module => {
    module.default;
  }).catch(err => {
    console.err('Loading failed!');
  })
}

อื่นๆ

ยังมีการเปลี่ยนแปลงที่สำคัญอีกหลายส่วน เพื่อนๆสามารถเข้าไปอ่านเพิ่มเติมได้ที่ What’s new in webpack 2

เอกสารอ้างอิง

Axel Rauschmayer. Static Module Structure. Retrieved May, 9, 2016, from http://exploringjs.com/es6/ch_modules.html#static-module-structure

seek. The End of Global CSS. Retrieved May, 9, 2016, from https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284#.w6aru9del

Axel Rauschmayer. Tree-shaking with webpack 2 and Babel 6. Retrieved May, 9, 2016, from http://www.2ality.com/2015/12/webpack-tree-shaking.html

Grgur Grisogono. Webpack 2 Tree Shaking Configuration. Retrieved May, 9, 2016, from https://medium.com/modus-create-front-end-development/webpack-2-tree-shaking-configuration-9f1de90f3233#.mqqwnquo2

sokra. What’s new in webpack 2. Retrieved May, 9, 2016, from https://gist.github.com/sokra/27b24881210b56bbaff7


แสดงความคิดเห็นของคุณ


Danglebz Highbreed10 เดือนที่ผ่านมา

ผมลองดูแล้วนะครับ PostCss มีปันหาตรง

postcss: function () { return [autoprefixer]; }

พอใส่เข้าไปใน webpack.config.js มัน Error ครับ

ขอบคุณครับ ปล. มือใหม่ครับ

ข้อความตอบกลับ
ไม่ระบุตัวตน8 เดือนที่ผ่านมา

ลองนี่แทนครับ
plugins: [ new webpack.LoaderOptionsPlugin({ options: { postcss: [ autoprefixer ] } }) ],


Nuttawut Singhabutปีที่แล้ว

เป็น tutorial ที่อ่านสนุกดีครับ


Koft Nattapolปีที่แล้ว

เพิ่มเติมหน่อยครับ พอดีผมใช้ทุกอย่างเวอร์ชั่นใหม่สุด (ยกเว้น webpack ใช้ stable 1.3 กว่าๆ) แล้วเกิด error หาไฟล์ไม่เจอ ในส่วน scss เลยต้องปรับแก้ในไฟล์ webpack.config.js เป็นดังนี้ครับ

{ // SCSS // ใช้ Loader สามตัวสำหรับ scss test: /.scss$/, exclude: /node_modules/, loaders: [“style”, “css”, “sass”] }

และถ้าต้องการโหลด query อื่นๆ ใช้แบบนี้ครับ

{ // SCSS // ใช้ Loader สามตัวสำหรับ scss test: /.scss$/, exclude: /node_modules/, loaders: [“style”, “css?modules&localIdentName=[local]_[hash:base64:5][path][name]”, “sass”, ‘postcss’] }

พิมพ์ต่อกันยาวๆ เหมือนเราจะใช้ method GET เวลาทำส่งข้อมูลในเว็บเลยครับ (อันที่ไม่ใช่ POST)


sirawat ggปีที่แล้ว

เจ๋งมากฮะะ