Siam HTML http://www.siamhtml.com Thu, 23 Jun 2016 05:23:10 +0000 en-US hourly 1 http://wordpress.org/?v=4.0 React Redux Isomorphic Cookbookhttp://www.siamhtml.com/react-redux-isomorphic-cookbook/ http://www.siamhtml.com/react-redux-isomorphic-cookbook/#comments Sun, 07 Feb 2016 07:23:36 +0000 http://www.siamhtml.com/?p=10168 ES2015

Let, Const

{
  let count = 0;
  console.log(count);  // 0

  const API_KEY = 'KEY';
  API_KEY = 'NEW_KEY'; // ERROR! "API_KEY" is read-only
}

console.log(count);  // count is not defined

Arrow Function

function(arg) {
  // do something
}

// Use arrow function
(arg) => {
  // do something
}

// share the same this as surrounding code.
var bob = {
  _name: "Bob",
  _friends: [],
  printFriends() {
    this._friends.forEach(function(friend) {
      console.log(this._name + " knows " + friend);
    });
  },
  printFriendsWithArrowFunction() {
    this._friends.forEach((friend) => {
      console.log(this._name + " knows " + friend);
    });
  }
};
 
bob._friends.push('Tae');

bob.printFriends(); // Cannot read property '_name' of undefined
bob.printFriendsWithArrowFunction(); // "Bob knows Tae"

Destructuring

const person = {
  name: 'suranart',
  gender: 'male',
  nationality: 'thailand'
};

// use destructuring
const { name, gender } = person;

// equivalent to
// const name = person.name;
// const gender = person.gender;

console.log(name); // 'suranart'
console.log(gender); // 'male'

Template Strings

const name = 'Suranart';

const message = `Hello! "${name}".`;

// equivalent to
// const message = "Hello! \"" + name + "\".";

console.log(message);

Default Parameters

function getSomething(limit = 10) {
  // ...
}

getSomething();

// equivalent to
// getSomething(10);

Modules

// myModules.js
export function doSomething1() {
  // do something
}
export function doSomething2() {
  // do something
}

// app.js
import * as myModules from './myModules.js';
myModules.doSomething1();
myModules.doSomething2();

// or use destructuring
// import { doSomething2 }  from './myModules.js';
// doSomething2();

Classes

import parentClass from './path/to/parentClass.js';

class Hello extends parentClass {
  constructor(prefix = 'Miss') {
    this.prefix = prefix;
  }
  static print(message) {
    console.log(`Hello! ${message}.`);
  }
  printWithPrefix(message) {
    console.log(`Hello! ${this.prefix} ${message}.`);
  }
}

Hello.print('Tae'); // Hello! Tae.

const hello = new Hello('Mr.');
hello.printWithPrefix('Tae'); // Hello! Mr. Tae.

Spread

function calculateArea(width, height) {
  console.log(`Area is ${width * height}.`);
}

const dimension = [20, 30];
calculateArea(...dimension);

Rest

function findLeaderAndFollowers(leader, ...followers) {
  console.log(`${leader} is the leader of ${followers.length} persons.`);
}

findLeaderAndFollowers('A', 'B', 'C');

Promises

function doSomethingAsync(input) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (input < 0) {
            reject('Input must greater or equal 0');
          }
          resolve(input + 1);
        }, 1000);
    })
}

doSomethingAsync(0)
  .then(result => {
      console.log(result);
      return doSomethingAsync(result)
  })
  .then(result => {
      console.log(result);
  })
  .catch(err => {
      console.log(err);
  });

React

Create a function component

import React from 'react';

const MyComponent = () => {
  return (
    <div>This is a function component.</div>
  );
};

export default MyComponent;

Render component with ReactDOM

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>React Cookbook</title>
</head>
<body>
  <div id="app"></div>
  <script src="./path/to/bundle.js"></script>
</body>
</html>

import React from 'react';
import ReactDOM from 'react-dom';

import MyComponent from './path/to/MyComponent.js';

ReactDOM.render(<MyComponent />, document.getElementById('app'));

Nested components

import React from 'react';

import Header from './Header';
import Main from './Main';
import Sidebar from './Sidebar';
import Footer from './Footer';

const App = () => {
  return (
    <div>
      <Header />
      <div>
        <Main />
        <Sidebar />
      </div>
      <Footer />
    </div>
  );
}

export default App;

Iteration

import React from 'react';

const Main = () => {
	
  const articles = [
    {
      id: 1, 
      title: 'Title 1',
      description: 'Description 1'
    }, {
      id: 2, 
      title: 'Title 2',
      description: 'Description 2'
    }, {
      id: 3, 
      title: 'Title 3',
      description: 'Description 3'
    }
  ];

  return (
    <div>
      {articles.map((data, index) => {
        return (
          <article key={ index }>
            <h2>{ data.title }</h2>
            <p>{ data.description }</p>
          </article>
        );
      })}
    </div>
  );
}

export default Main;

Props

// Main.js

import Article from './Article';

const Main = () => {
	
  const articles = [
    // ...
  ];

  return (  
    <div>
      {articles.map((data, index) => {
        return (
          <Article key={ index } article={data} />
        );
      })}
    </div>
  );
}

// Article.js

const Article = (props) => {
  const { article } = props;
  return (  
    <article>
      <h2>{ article.title }</h2>
      <p>{ article.description }</p>
    </article>
  );
}

Styling

<head>

  ...

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>

const App = () => {

  return (
    <div className="container">
      <Header />
      <div>
        <Main />
        <Sidebar />
      </div>
      <Footer />
    </div>
  );
}

Create a class component

import React, { Component } from 'react';

class MyComponent extends Component {
	
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>This is a Class component</div>
    );
  }
}

export default MyComponent;

Set state

class MyComponent extends Component {
	
  constructor(props) {
    super(props);
    this.state = { 
      property1: value1, 
      property2: value2
    };
  }

  // ...
}

Update State

this.setState({ someProperty: newValue });

Fetch  data from API

npm install --save isomorphic-fetch es6-promise

require('es6-promise').polyfill();
require('isomorphic-fetch');

fetch(`http://www.myapi.com/path/to/something/`)
  .then(response => {
    if (response.status >= 400) {
      throw new Error("Bad response from server");
    }
    return response.json();
  })
  .then(data => {
    console.log(data);
  });

Event handling

export default class Search extends Component {

  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();
    this.props.getResultsFromKeyword(this.refs.keyword.value);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input ref="keyword" type="text" placeholder="Search..." />
        <button>Search</button>
      </form>
    );
  }
}

Refs

<input ref="myInput" />

var input = this.refs.myInput;
var inputValue = input.value;

React Router

npm install react-router --save

Just render <Router /> instead

import { Router, Route, IndexRoute, browserHistory }  from 'react-router';

ReactDOM.render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        {/* relative => match /inbox/messages/123 */}
        <Route path="messages/:id" component={Message} /> 
        {/* absolute => match /messages/123 */}    
        <Route path="/messages/:id" component={Message} />    
      </Route>
    </Route>
  </Router>
), document.getElementById('app'));

URLComponents
/App -> Dashboard
/aboutApp -> About
/inboxApp -> Inbox
/inbox/messages/123App -> Inbox -> Message
/messages/123App -> Inbox -> Message

Choose Target to Render Child Component

const App = (props) => {
  return (
    <div className="container">
      <Header />
      <div>
        { props.children }
      </div>
      <Footer />
    </div>
  );
}

Add some links

<Link to={`/messages/${id}`}>

 Get parameters from URL

fetch(`${API_URL}messages/${this.props.params.id}`);

Component Lifecycle

Mounting (Home => Article A)

  • componentWillMount()
  • componentDidMount()

// Article.js

componentWillMount() {
  if (this.state.activeUser.id !== this.props.params.id) {
    this.getUser(this.props.params.id);
  }
}

Updating (Article A => Article B)

  • componentWillReceiveProps(nextProps)
  • componentDidUpdate(prevProps)

// Article.js

componentWillReceiveProps(nextProps) {
  if (this.props.params.id !== nextProps.params.id) {
    this.getUser(nextProps.params.id);
  }
}

Unmounting (Article B => Home)

  • componentWillUnmount()

// Article.js

componentWillUnmount() {
  this.ignoreLastFetch = true;
}

getUser(id) {
  // ...
  fetch(url, (err, data) => {
    if (!this.ignoreLastFetch)
      this.setState({ activeUser: data});
  });
}

Improve Performance

Apply shouldComponentUpdate() method

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.id !== this.props.id;
}

Use immutable data

// How ?

// use Object.assign() for object
Object.assign({}, state, myObject);

// use .concat() for array
state.concat(myArray);

// or use spread
[...state, myObject];
{...state, somePropety:newValue};

// -----------------------------------------------------------

// Example for Object
const currentState = {a: 1, b: 2};

// DO NOT mutate state like this
const newState = currentState;
newState.a = 100;
console.log(currentState);  // {"a":100,"b":2}
console.log(newState);      // {"a":100,"b":2}

// use Object.assign() instead
const newState = Object.assign({}, currentState, {a: 100});

// or use spread
// const newState = {...currentState, a: 100};  

console.log(currentState);      // {"a":1,"b":2}
console.log(newState);          // {"a":100,"b":2}

// -----------------------------------------------------------

// Example for arrays
const currentState = [];

// DO NOT use .push()
// let newState = currentState;
// newState.push({a: 1});

// use .concat() instead
let newState = currentState.concat([{a: 1}]);

// or use spread
// let newState = [...currentState, {a: 1}];

Redux

Create an action creator

// ArticleActions.js

export function getArticles(limit) {
  return {
    type: 'GET_ARTICLES',
    limit: limit
  }
}

export function getArticleById(id) {
  return {
    type: 'GET_ARTICLE_BY_ID',
    id: id
  }
}

Create a reducer

// ArticleListReducer.js

export default function(state = [], action) {
  switch(action.type) {
    case 'GET_ARTICLES':
      // Receive data from API limit by action.limit
      return action.data;
    default:
      return state;
  }
}

// ActiveArticleReducer.js

export default function(state = {}, action) {
  switch(action.type) {
    case 'GET_ARTICLE_BY_ID':
      // Receive data from API query by action.id
      return action.data;
    default:
      return state;
  }
}

Create the root reducer

import { combineReducers } from 'redux';

import ArticleListReducer from './ArticleListReducer';
import ActiveArticleReducer from './ActiveArticleReducer';

const rootReducer = combineReducers({
  articles: ArticleListReducer,
  activeArticle: ActiveArticleReducer
});

export default rootReducer;

Create the store

import { createStore } from 'redux';
import rootReducer from './rootReducer';

const store = createStore(rootReducer);

Wrap the root component with Provider

import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <Router />
  </Provider>, document.getElementById('app')
);

Create a container

import React, { Component } from 'react';

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import * as ArticleActions from './ArticleActions';

class ArticleList extends Component {

  componentDidMount() {
    this.props.getArticles(10);
  }

  render() {
    return (
      <div>
        <ul>
          {this.props.articles.map((article) => {
            return (
              <li key={article.id}>{ article.title }</li>
            );
          })}
        </ul>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return { 
    articles: state.articles 
  };
}

export default connect(mapStateToProps, ArticleActions)(ArticleList);

Apply Middleware

// promiseMiddleware.js

export default function promiseMiddleware() {
  return next => action => {
    const { promise, type, ...rest } = action;
   
    if (!promise) return next(action);
   
    const SUCCESS = type;
    const REQUEST = type + '_REQUEST';
    const FAILURE = type + '_FAILURE';
    next({ ...rest, type: REQUEST });
    
    return promise
      .then(function(response) {
        if (response.status >= 400) {
          next({ ...rest, error: response.status, type: FAILURE });
          console.log(response.status);
          return false;
        }
        return response.json();
      })
      .then(data => {
        next({ ...rest, data, type: SUCCESS });
        return true;
      });
   };
}

import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from './promiseMiddleware';

const store = createStore(rootReducer, applyMiddleware(promiseMiddleware));

// ArticleActions.js

require('es6-promise').polyfill();
require('isomorphic-fetch');

const API_URL = 'http://localhost:3004';
 
export function getArticles() {
  return {
    type: 'GET_ARTICLES',
    promise: fetch(`${API_URL}/articles`)
  }
}

export function getArticleById(id) {
  return {
    type: 'GET_ARTICLE_BY_ID',
    promise: fetch(`${API_URL}/articles/${id}`)
  }
}

// ArticleListReducer.js
 
export default function(state = [], action) {
  switch(action.type) {
    case 'GET_ARTICLES':
      return action.data;
    default:
      return state;
  }
}

Isomorphic

Use Middleware Instead

// server.js

import express from 'express';
import serverRendering from './serverRendering';

const app = express();

// use serverRendering middleware
app.use(serverRendering);

// ...

Match the URL with Routes to detect the Status

// serverRendering.js

import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './routes'

export default function((req, res) {
  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).end(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (!renderProps) {
      res.status(404).end('Not found');
    }

    res.status(200).end(renderToString(<RouterContext {...renderProps} />));

  })
})

Create a function to return the complete HTML

res.status(200).end(renderHTML());

// serverRendering.js

function renderHTML() {

  const renderedComponent = renderToString(
    <Provider store={store}>
      <RouterContext {...renderProps} />
    </Provider>
  );

  const HTML = `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Isomorphic React Redux Tutorial</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
      </head>
      <body>
        <div id="app">${renderedComponent}</div>
        <script src="/assets/bundle.js"></script>
      </body>
    </html>    
  `;

  return HTML;
}

Prefetch Data

// prefetchComponentData.js

export default function prefetchComponentData(dispatch, components, params) {

  const needs = components.reduce((prev, current) => {
    return (current.prefetchData || []).concat(prev);
  }, []);

  const promises = needs.map(need => dispatch(need(params)));
  return Promise.all(promises);
}

// serverRendering.js

// Wait until data has been fetched
prefetchComponentData(store.dispatch, renderProps.components, renderProps.params)
  .then(() => res.status(200).end(renderHTML()))
  .catch(err => res.end(err.message));

Pass the current state to client

// serverRendering.js

// ...

const initialState = store.getState();
      
// ...

<script type="application/javascript">
   window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>

Load the state into the store of client

// client.js

// ...

const initialState = window.__INITIAL_STATE__;
const store = createStoreWithMiddleware(rootReducer, initialState);

More Tools

React Helmet

// serverRendering.js

import Helmet from 'react-helmet';

const renderedComponent = ReactDOM.renderToString(<Handler />);
let head = Helmet.rewind();

const html = `
    <!doctype html>
    <html>
        <head>
            ${head.title.toString()}
            ${head.meta.toString()}
            ${head.link.toString()}
        </head>
        <body>
            <div id="content">
                ${renderedComponent}
            </div>
        </body>
    </html>
`;

render() {
  return (
    <article>
      <Helmet
        title={ this.props.article.title }
        meta={[
          {"name": "description", "content": "add description here..."}
        ]}
        link={[
          {"rel": "apple-touch-icon", "href": "http://mysite.com/img/apple-touch-icon-57x57.png"},
          {"rel": "apple-touch-icon", "sizes": "72x72", "href": "http://mysite.com/img/apple-touch-icon-72x72.png"}
        ]}
      />

      {/* ... */}

    </article>
  );
}

Scroll Behavior

import useScroll from 'scroll-behavior/lib/useStandardScroll';

import createBrowserHistory from 'history/lib/createBrowserHistory'
const history = useScroll(createBrowserHistory)();

 

]]>
http://www.siamhtml.com/react-redux-isomorphic-cookbook/feed/ 0
18 เรื่องที่ Front-end Developer ควรรู้ในปี 2016http://www.siamhtml.com/front-end-development-skills-in-2016/ http://www.siamhtml.com/front-end-development-skills-in-2016/#comments Sun, 03 Jan 2016 07:14:32 +0000 http://www.siamhtml.com/?p=10040 ในปี 2015 ที่ผ่านมา ชาว front-end developer ทั้งหลาย คงจะเหนื่อยกันไม่น้อยเลยใช่มั้ยละครับ ก็พวกเทคนิคการทำเว็บต่างๆ มันช่างไปไวซะเหลือเกิน เรียกว่าถ้าหยุดเล่นไปเดือนนึงนี่อาจจะรู้สึกว่าตัวเองโง่ไปเลยทีเดียว

บทความนี้ ผมเลยจะมาสรุปความรู้ต่างๆ ที่ตัวผมคิดว่า front-end developer ควรจะมีติดตัวไว้ฮะ หากใครมีตามนี้หมด หรือเกือบทั้งหมด ผมว่าเท้าคุณคงจะมีแสง รับรองว่าบริษัทดังๆ จะยอมจ่ายค่าตัวคุณเหยียบแสนอย่างแน่นอนฮะ ส่วนใครมีเกินครึ่ง ผมว่าก็โอเคแล้วนะ พยายามเก็บในส่วนที่เหลือ เราก็โหดใช่เล่นเหมือนกัน แต่ถ้าใครมีไม่ถึงครึ่ง ก็ไม่ต้องตกใจไปนะฮะ ให้เราค่อยๆ ศึกษาไล่ตามไป ผมเชื่อว่า front-end ไม่ใช่เรื่องของความเก่งหรือฉลาด แต่เป็นเรื่องของความใฝ่รู้ล้วนๆ ฮะ ^0^

พื้นฐานที่ทุกคนต้องรู้

ผมจะขอเริ่มจากเรื่องที่ front-end developer ทุกคนจะต้องรู้ก่อนเลยฮะ เรื่องต่อไปนี้ ผมถือว่าถ้าเรารู้ไม่ได้แปลว่าเราเก่งนะ คือเราไม่สามารถเอามาบอกคนอื่นๆ ได้ว่าเรารู้ไอ้นี่นะ เพราะมันเป็นสิ่งที่ทุกคนควรจะรู้อยู่แล้ว เรามาดูกันเลยฮะ ว่าเรื่องพื้นฐานเหล่านี้มันมีอะไรบ้าง

  • Version Control อย่างน้อยก็ควรจะใช้ Git ให้เป็นไว้ซักตัวฮะ ผมจำได้ว่าเมื่อประมาณ 5 ปีก่อน ผมไม่ชอบ Git เลย เพราะคิดว่าถ้าเรามีวินัย จัดระเบียบดีๆ ก็น่าจะเอาอยู่แล้ว แต่พอได้ลองใช้ดูก็พบว่ามันโคตรจะดีอะครับ ทุกวันนี้ผมบอกเลยว่าชีวิตผมขาด Git ไม่ได้ซะแล้ว
  • Front-end Framework เลือกสักตัวครับ จะเป็น Bootstrap หรือ Foundation ก็ได้ หรือเป็นทั้ง 2 ตัว เลยยิ่งดีฮะ ที่ผมแนะนำให้เป็นไว้บ้าง เพราะอยากให้ลองเข้าไปที่ repository ของเค้าแล้วลองแกะโค้ดดูฮะ ดูว่าเค้ามีวิธีการออกแบบ framework พวกนี้อย่างไร ดูการเขียน Sass เขียน JavaScript ของเค้า แล้วสกิลเราจะอัพเกรดขึ้นเยอะเลยฮะ ดีไม่ดีเราอาจจะพัฒนา framework ของเราขึ้นมาใช้เองเลยก็เป็นได้ครับ
  • CSS Preprocessor ทุกวันนี้มีให้ใช้กันหลายตัวฮะ แต่ถ้าจะให้ผมเลือกก็คงจะเป็น Sass ละกัน หรือใครจะดูเป็น PostCSS ไปเลยก็ได้ครับ
  • JavaScript เป็นเรื่องน่าตลกที่ job description สมัยนี้ยังมีเขียนเอาไว้ด้วยว่า front-end developer จะต้องเขียน JavaScript ได้ด้วย ทั้งๆ ที่มันควรจะสิงสถิตอยู่ใน front-end developer ทุกคนอยู่แล้ว และที่น่าตลกกว่านั้นก็คือบางบริษัทเขียนเอาไว้ด้วยว่าจะต้องเขียน jQuery เป็นด้วย! โอว สมัยนี้เค้ามีแต่จะลดการใช้ jQuery ให้น้อยลงกันนะคร้าบ
  • Responsive Web Design เมื่อ 5 ปี ก่อน RWD อาจจะเป็นเรื่องน่าตื่นตาตื่นใจครับ แต่สมัยนี้ไม่แล้ว front-end developer ทุกคนจะต้องมีสกิลนี้ติดตัวฮะ
  • Node.js / Automation การพัฒนา front-end สมัยนี้ ยังไงก็ต้องใช้ automation ครับ ความรู้เรื่อง Node.js นี่เรียกว่ายังไงก็ต้องมีติดตัวเอาไว้บ้าง อาจจะไม่ถึงขั้นเขียนแอพขึ้นมาใช้เอง เอาแค่สามารถติดตั้งโมดูลที่ต้องการจะใช้ให้เป็นก็ถือว่าโอเคในระดับหนึ่งแล้วล่ะครับ

หากเพื่อนๆ คนไหนยังมีสกิลที่ว่านี้ไม่ครบ ไม่ได้แปลว่าเราอ่อนนะครับ เพียงแต่ workflow ที่เราใช้นั้นอาจจะยังเป็นแบบเก่าอยู่ ผมอยากให้ลองศึกษาตามหัวข้อข้างต้นให้ครบ แล้วจะพบว่างานของเราจะมีคุณภาพและเสร็จไวมากขึ้นอีกเยอะเลยล่ะฮะ

เรื่องที่ควรเรียนรู้ในปี 2016

หากใครมีสกิลพื้นฐานในหัวข้อที่ผ่านมาครบหมดเลย ก็แปลว่าคุณสามารถเป็น front-end developer ได้สบายๆ แล้วล่ะครับ ทีนี้เรามาดูเรื่องอื่นๆ ที่เราควรจะเรียนรู้เพิ่มเติมในปี 2016 นี้กันบ้าง ลองเช็คหัวข้อต่อไปนี้ดูสิว่า เราเป็นเรื่องไหนแล้วบ้าง ?

  • React ขาดไม่ได้จริงๆ ครับ สำหรับ React เรียกว่าฮิตกันทั่วบ้านทั่วเมืองทั่วโลกกันเลยในขณะนี้ ถึงแม้ว่า learning curve จะค่อนข้างสูง แต่ React ก็ได้รับเสียงวิจารณ์ในแง่ดีมากๆ พูดง่ายๆ คือคุ้มอะครับที่จะลงทุนกับมัน (จริงๆ learning curve ของตัว React เอง ไม่ได้สูงมากมายอะไร แต่คนที่มาสาย React จะต้องเรียนรู้บรรดาผองเพื่อนที่จะต้องใช้ร่วมกับมันด้วย ซึ่งแต่ละตัวนี่ก็เอาเรื่องอยู่เหมือนกันฮะ T__T)
  • Angular ส่วน Angular นี่ก็ห้ามพลาดเช่นกันครับ เพราะในเวอร์ชั่นใหม่นี้ได้มีการปรับปรุงในเรื่องของ performance ให้ดีขึ้นเยอะเลย แถมยังทำ server-side rendering ได้แล้วด้วย ข้อดีของ Angular อย่างนึงก็คือมันจะเตรียมของมาให้หมดเลย ทำให้ doc นั้นอยู่รวมกัน ไม่กระจัดกระจายเหมือนกับ React ครับ
  • ECMAScript 2015 / TypeScript 2 ภาษานี้ เราควรจะเขียนเป็นครับ อย่างน้อยเอา ECMAScript 2015 ไว้ซักตัวก็ยังดี ไม่งั้นผมรับรองเลยว่าเราจะคุยกับเค้าไม่รู้เรื่อง!
  • Functional Programming functional programming น่าจะมาแรงในปีนี้ฮะ คือเราจะเปลี่ยนจากการเขียนแบบ OOP มาใช้ pure function แทน เน้นการใช้มันซ้ำๆ แล้วก็ไม่ไป mutate state อะไรทำนองนี้ครับ
  • Testing อีกสกิลที่ front-end developer มักจะละเลยกันก็คือเรื่องของการเขียน test ครับ ไม่ว่าจะเป็น unit test, end-to-end test รวมไปถึง css regression test พวกนี้อย่าให้ขาดฮะ ลองนึกดูว่าทุกวันนี้ เราเสียเวลาไปกับ manual test เยอะแค่ไหน ถ้า test เหล่านั้น มันสามารถทำโดยอัตโนมัติได้ ชีวิตเราจะดีขึ้นมั้ย ?
  • Web Performance เรื่องนี้ผมคิดว่าทุกคนรู้ดีครับว่าควรทำ แต่จะทำหรือเปล่าอีกเรื่องนะฮะ ในปีนี้ผมมองว่าเว็บน่าจะไม่ได้แข่งกันที่ฟีเจอร์เท่าไร แต่จะวัดกันที่ performance ซะมากกว่า โดยเฉพาะเว็บ e-commerce นี่เห็นชัดเลย ถึงระบบจะดีแค่ไหนแต่ถ้าเว็บมันโหลดช้า ผมว่าคนหายไปเยอะครับ
  • Progressive Web Appsเป็นเทรนด์ที่น่าจะมาเหมือนกันครับในปีนี้ คือมันเป็นแนวคิดที่จะเปลี่ยน UX ของเว็บแอพให้ใกล้เคียงกับโมบายล์แอพมากขึ้น มี push notifications, offline อยู่ก็ยังทำงานได้, สร้างไอคอนเอาไว้เข้าเว็บนั้นโดยตรงได้ อะไรทำนองนี้ฮะ

เรื่องพวกนี้ผมมองว่าเราน่าจะได้ใช้กันค่อนข้างแน่ครับในปีนี้ ให้เราเตรียมตัวกันให้พร้อมเลย จะเห็นว่าบางเรื่องนั้นมีมานานมากแล้ว แต่ที่ผมเอามาใส่ด้วยก็เพราะคิดว่าในปีนี้เราน่าจะได้ใช้มันอย่างเต็มที่เลยนั่นเองฮะ

สกิลที่ควรจะมีไว้ประดับบารมี

ส่วนเรื่องต่อไปนี้ เราจะรู้หรือไม่รู้ก็ได้นะฮะ ไม่ถือว่าผิดจรรยาบรรณของการเป็น front-end developer แต่อย่างใด เพียงแต่ว่าถ้าเรารู้เรื่องพวกนี้ด้วย มันจะช่วยทำให้เราดูโดดเด่นกว่าชาวบ้านเค้าเท่านั้นเองฮะ

  • DevOps หมดยุคของการอัพโหลดโค้ดผ่าน FTP ตอนจะขึ้นงานแล้วนะครับ ใครยังทำแบบนี้อยู่แล้วรู้สึกว่าชีวิตลำบากมาก ผมแนะนำให้ลองศึกษาเรื่อง DevOps ดูนะฮะ ผมมองว่ามันเป็นเรื่องที่น่าลงทุนมากๆ เพราะเมื่อเราได้ระบบมาแล้ว การพัฒนาอะไรๆ หลังจากนี้ก็จะสบายขึ้นเยอะแล้วล่ะครับ สิ่งที่ยากกว่าคือการเปลี่ยนแนวคิดฮะ ผมว่าถ้าทีมเห็นความสำคัญของ DevOps อะไรๆ ก็จะง่ายขึ้นเยอะเลย
  • Mobile Apps ส่วนโมบายล์แอพนี่ รู้ไว้บ้างก็ดีครับ จะ iOS หรือ Android ก็แล้วแต่เลย หรือได้ทั้งคู่ยิ่งดีฮะ ผมอยากให้เรารู้แนวคิดเฉยๆ ว่าพวกที่เขียนโมบายล์แอพเนี่ยเค้ามีวิธีการทำยังไง บางทีเว็บที่เราดูแลอยู่อาจจะมีเวอร์ชั่นโมบายล์แอพด้วย เวลาคุยงานกันจะได้เข้าใจฮะ หรือใครจะต่อยอดไปถึงขนาดที่ทำโมบายล์แอพเองได้เลยด้วยก็ดีนะครับ สมัยนี้ไม่ได้ยากเหมือนแต่ก่อนแล้ว หรือใครจะใช้ตัวช่วยอย่าง React Native หรือ Ionic นี่ก็ยิ่งสบายเข้าไปใหญ่ครับ
  • Desktop Apps ที่แนะนำให้เพิ่มสกิลนี้เข้าไปด้วยก็เพราะสมัยนี้มันทำได้ง่ายขึ้นเยอะครับ ให้เราเขียนแอพขึ้นมาด้วยภาษาเว็บที่เราถนัดนี่แหละ แล้ว Electron มันจะช่วยแปลงให้เป็นแอพที่สามารถรันบน desktop ให้เอง ตัวอย่างแอพดังๆ ที่สร้างด้วย Electron ก็มีไม่น้อยเลยนะครับ ไม่ว่าจะเป็น editor สำหรับสาย geek อย่าง Atom และ Visual Studio Code, แอพสำหรับทำงานร่วมกันในทีมอย่าง Slack, แอพสำหรับช่วยงาน front-end อย่าง Avocode หรือแม้แต่ Kitematic ที่เป็น GUI ของ Docker นี่ก็ใช้ Electron ทำเหมือนกันครับ
  • Digital Marketing หัวข้อนี้ออกจะแหวกแนวหน่อยนะฮะ แต่ผมบอกเลยว่ามันจะช่วยเพิ่มมูลค่าให้กับ front-end developer มากๆ เลยล่ะครับ เพราะในปัจจุบันการแข่งขันทางด้านนี้นั้นสูงเอามากๆ และ front-end developer ก็ถือว่าเป็น developer ที่ใกล้ชิดกับ digital marketing ที่สุดแล้ว ถ้าเรามีความรู้ทางด้านนี้ไว้บ้าง การคุยงานอะไรก็จะราบรื่นขึ้นอย่างแน่นอนครับ แล้วยิ่งถ้าใครเป็นเจ้าของธุรกิจเองด้วย สกิลนี้ถือว่าห้ามพลาดโดยเด็ดขาดเลยล่ะฮะ
  • User Experience เคยมั้ยครับ? เวลาเราทำ interface ตามที่ web designer เค้าออกแบบมาแล้วรู้สึกว่ามันไม่ได้เรื่องเอาซะเลย ผมแนะนำให้ศึกษาเรื่อง user experience เอาไว้อีกตัวฮะ แล้วเอาความรู้ตรงนี้ไปลองเสนอเค้าดู designer บางคนนี่เอาแต่สวยอย่างเดียว เรื่อง UX นี่ไม่ได้สนเลย ส่วนตัวผมมองว่า front-end developer ที่พ่วงสกิล UX ติดมาด้วยนี่โคตรหล่อเลย ก็ลองไปศึกษากันดูนะฮะ รับรองว่ายังไงก็ได้ใช้แน่ๆ ครับ

จะเห็นว่าเรื่องพวกนี้ ไม่ได้เกี่ยวกับโลกของ front-end โดยตรงนะฮะ แต่หากเรามีความรู้ทางด้านเหล่านี้ด้วย มันจะช่วยส่งเสริมหรือต่อยอดสกิลของเราให้ดูดีขึ้น เพื่อนๆ คนไหน ที่อัพสกิลหลักเต็มหมดแล้ว ผมขอแนะนำให้อัพสกิลเหล่านี้ต่อเลยฮะ (ถ้ายังมี point เหลืออะนะ)

โลก Front-end นั้นช่างสวยงาม…(กูประชด!)

ผมลองนึกย้อนไปเมื่อประมาณ 2-3 ปี ก่อนนะฮะ ตอนนั้นจำได้ว่าเรื่อง responsive web design นี่ยังใหม่มากๆ สำหรับหลายๆ คนเลย ลองดูตอนนี้สิครับ มันกลายเป็นสิ่งที่ทุกคนต้องรู้ไปซะแล้ว แต่กรณีของ responsive web design นี่ยังดีนะฮะ เพราะในปัจจุบันแนวคิดนี้ยังคงมีการใช้กันอย่างแพร่หลายอยู่เหมือนเดิม

ที่เลวร้ายหน่อยก็จะเป็นกรณีของ AngularJS ที่ตอนออกมาแรกๆ คนก็หันไปใช้กันเยอะครับ two-way data binding มันช่างเทพซะเหลือเกิน แต่พอใช้ไปซักพักคนก็จะเริ่มเจอปัญหา สิ่งที่เคยคิดว่าดี มันกลับกลายเป็นไม่ดีไปซะแล้ว พอ Angular 2 ออกมาแก้ พี่แกก็เล่นเปลี่ยนแนวคิดและ syntax ใหม่ซะหมดเลย หลายๆ คนที่เคยเขียนจนคล่องแล้ว ก็ต้องมาเริ่มต้นเรียนรู้กันใหม่หมด อย่าง React ที่กำลังเป็นกระแสกันอยู่ในตอนนี้ ใครจะรู้ว่าอีกไม่กี่เดือนข้างหน้านี้อาจจะไม่มีคนใช้แล้วก็ได้นะครับ ทำเป็นเล่นไป ในโลกของ front-end ผมว่าอะไรก็เกิดขึ้นได้

จะเห็นว่าโลกของ front-end นี่มันไปเร็วมากเลยนะฮะ สิ่งที่เคยดี วันนี้กลับไม่ดี สิ่งที่เคยคิดว่าแย่ วันนี้เรากลับต้องใช้มัน จริงๆ แล้ว ไอ้เรื่องไปเร็วนี่ผมว่ายังไม่เท่าไร แต่มันดันซับซ้อนมากขึ้นทุกปีด้วยนี่สิ ผมเชื่อว่าหลายๆ คนคงเคยถูกมองว่า “มึงทำเว็บงั้นหรอ ? ใครๆ ก็ทำเป็น ไม่เห็นจะยากเลย” มาวันนี้ไปบอกไอ้เพื่อนคนนั้นเลยนะครับ บอกให้มันกลับมาลองทำดู จะได้รู้ว่าทุกวันนี้การทำเว็บมันโหดขนาดไหน

แต่ในความโหดร้ายก็ยังมีเรื่องโชคดีเล็กๆ อยู่นะครับ คือบางทีไอ้สิ่งที่เข้ามาใหม่ มันดันไปล้างสิ่งเก่าที่มีอยู่เดิมออกด้วยฮะ อย่างก่อนหน้านี้ เราอาจจะมีทางเลือกในการเขียน JavaScript เป็น CoffeeScript, TypeScript แล้วก็ Dart ถูกมั้ยครับ ? แต่มาวันนี้ ผมมองว่าเรารู้แค่ ECMAScript 2015 ที่เป็นมาตรฐาน บวกกับ TypeScript อีกซักตัวก็น่าจะพอแล้วล่ะฮะ

วิธีเอาตัวรอด

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

ผมเคยคิดนะครับว่า การเป็นเป็ดมันไม่ได้แย่อะไรนะ กลับดีซะอีก ทำเป็นหมดทุกอย่าง ใครๆ ก็อยากได้ตัว ความคิดนี้ไม่ได้ผิดอะไรครับ แต่เชื่อเถอะครับว่า ด้วยขีดจำกัดของมนุษย์แล้ว เราไม่สามารถเก่งอะไรได้หลายๆ อย่างพร้อมกันหรอก ต่อให้มองแค่ในโลกของ front-end ผมเองยังคิดเลยว่าจะมีซักกี่คน ที่เทพทั้ง html, css และ js ในคนๆ เดียวกัน บางคนอาจจะบอกว่า “ก็ผมนี่ไง! ได้ทั้ง 3 ภาษาเลย” เชื่อผมเถอะครับว่าคุณอาจจะยังไม่รู้ว่ามีบางสิ่งที่คุณยังไม่รู้ก็เป็นได้

สิ่งสุดท้ายที่อยากจะฝากไว้ก็คือ พยายามหา domain ตัวเองให้เจอครับ เลือกซักทางใน front-end แล้วพยายามไปให้สุด เล่นให้หนักจนถึงจุดที่สามารถเปิดสอนคนอื่นได้ แล้วสกิลด้านอื่นๆ เราค่อยอาศัยอ่านหรือฟังจากคนอื่นเอา ยอมเป็นคนโง่ในหลายๆ ด้าน เพื่อที่จะขอเป็นเทพกับเค้าซักด้าน ผมว่าคุ้มนะครับ…

]]>
http://www.siamhtml.com/front-end-development-skills-in-2016/feed/ 21
ขอเชิญทุกท่านร่วมงาน BarCamp Bangkhen 2015http://www.siamhtml.com/barcamp-bangkhen-2015/ http://www.siamhtml.com/barcamp-bangkhen-2015/#comments Tue, 17 Nov 2015 15:09:09 +0000 http://www.siamhtml.com/?p=9953 ขอเชิญผู้สนใจทุกท่านร่วมงาน BarCamp Bangkhen 2015 ซึ่งจัดขึ้นในวันอาทิตย์ที่ 22 พฤศจิกายนนี้ ณ อาคารนานาชาติ 17 (IUP) คณะวิศวกรรมศาสตร์ มหาวิทยาลัยเกษตรศาสตร์

บาร์แคมป์คืออะไร?

บาร์แคมป์เป็นงานชุมนุมรวมกลุ่มสำหรับผู้ที่สนใจแบ่งปันความรู้ ประสบการณ์ หรือเรื่องราวต่างๆให้ผู้ร่วมงานคนอื่นๆ ได้ฟังกัน งานนี้ไม่มีการกำหนดหัวข้อเอาไว้ล่วงหน้า ผู้ร่วมงานทุกคนจะเป็นผู้กำหนดหัวข้อในงาน

ใครคือผู้ร่วมงาน

คุณไงละ! งานของเราจะขาดคุณซึ่งเป็นผู้แบ่งปันเรื่องราวดีๆไปไม่ได้

ทำไมต้องมาด้วยล่ะ?

เราไม่จำกัดหัวข้อในการนำเสนอ ถ้าคุณมีเรื่องราวที่สนใจอยากพูดคุย คุณสามารถเสนอหัวข้อที่ต้องการได้เลย แต่หากคุณยังคิดไม่ออก คุณก็สามารถเลือกหัวข้อที่คนอื่นเสนอ และเข้าไปร่วมพูดคุยได้ หัวข้อในงานของเรามีความหลากหลายแตกต่างกันไป หัวข้อที่ถูกเลือกอาจจะเกี่ยวกับเทคโนโลยี, ทริปท่องเที่ยว, สอนธุรกิจ, รีวิวเกมส์, เอาตัวรอดในดินแดนซอมบี้ และอื่นๆอีกมากมาย

สามารถลงทะเบียนร่วมงานได้ที่ www.barcampbangkhen.org และอย่าลืมชวนเพื่อนๆคุณมาร่วมงานด้วยละ สำหรับกำหนดการ แผนที่ และรายละเอียดต่างๆสามารถติดตามผ่านทางเว็บไซต์

Website: www.barcampbangkhen.org
Facebook: http://facebook.com/barcampbangkhen
Twitter: @barcampbangkhen

]]>
http://www.siamhtml.com/barcamp-bangkhen-2015/feed/ 0
Big Data คืออะไร ? + วิธีใช้ Hadoop/Spark บน Cloud Dataprochttp://www.siamhtml.com/getting-started-with-big-data-and-hadoop-spark-on-cloud-dataproc/ http://www.siamhtml.com/getting-started-with-big-data-and-hadoop-spark-on-cloud-dataproc/#comments Sat, 07 Nov 2015 17:15:48 +0000 http://www.siamhtml.com/?p=9673 ช่วงนี้ไปไหนมาไหนก็มีแต่คนพูดถึง Big Data จนหลายๆ คนก็คงจะพอรู้กันแล้วว่ามันก็คือการนำข้อมูลปริมาณมหาศาลมาใช้ให้เกิดประโยชน์นั่นเองฮะ ส่วนจะใช้ tool ตัวไหน มีวิธีการทำอย่างไรนั้น ผมจะขอใช้บทความนี้เล่าให้เพื่อนๆ ฟังคร่าวๆ แล้วกันนะครับ

โลกทุกวันนี้ ไม่แน่จริง อยู่ไม่ได้ !

อย่างที่เรารู้กันดีนะครับว่า ธุรกิจสมัยนี้มีการแข่งขันกันสูงเอามากๆ เลย ถ้าเราหยุดอยู่กับที่ คนอื่นๆ ก็พร้อมที่จะแซงเราได้ตลอดเวลาฮะ ดังนั้นธุรกิจต่างๆ จึงต้องคอยพัฒนาสินค้าและบริการของตนเองอยู่อย่างต่อเนื่อง แล้วไอ้การพัฒนาที่ว่านี้มันจะง่ายขึ้นเยอะเลยล่ะครับ ถ้าเราสามารถ “รู้อนาคต” ได้ก่อนคนอื่น

รู้ก่อน มีชัยไปกว่าครึ่ง

ถามว่าเราจะสามารถรู้ล่วงหน้าได้ยังไงว่าอะไรจะเกิดขึ้น ? คำตอบคือเราจะต้องใช้ความรู้ทางด้านคณิตศาสตร์เข้ามาช่วยครับ หากเรามีข้อมูลที่เกิดขึ้นในอดีตและปัจจุบัน เราก็พอจะมองออกว่าข้อมูลที่กำลังจะเกิดขึ้นในอนาคตมันจะมีหน้าตาเป็นอย่างไร ซึ่งเราสามารถนำข้อมูลตรงนี้ไปปรับปรุงสินค้าหรือบริการของเราให้ตรงกับความต้องการของลูกค้ามากขึ้นได้ แต่ขั้นตอนที่ว่านี้มันจะไม่ค่อยแม่นยำเท่าไรเลยนะฮะ ถ้าข้อมูลที่นำมาวิเคราะห์นั้นมีปริมาณไม่มากนัก

เราอยู่ในยุคที่เต็มไปด้วยข้อมูล

แต่ในปัจจุบัน เราไม่ต้องกังวลเรื่องปริมาณของข้อมูลเท่าไรแล้วนะครับ เพราะจากการสำรวจล่าสุดพบว่าในแต่ละวัน ผู้บริโภคนั้นผลิตข้อมูลออกมามากขึ้นเรื่อยๆ ไม่ว่าจะเป็นการเข้าเว็บเพื่ออ่านข่าว  การค้นหาข้อมูล การซื้อขายของออนไลน์ การเล่น Social Network พฤติกรรมเหล่านี้ล้วนก่อให้เกิดข้อมูลใหม่ๆ ที่สามารถนำมาวิเคราะห์ได้ฮะ อยู่ที่เจ้าของธุรกิจแล้วล่ะครับว่า จะสามารถนำข้อมูลปริมาณมหาศาลหรือที่นิยมเรียกกันว่า “Big Data” เหล่านี้ มาใช้ให้เกิดประโยชน์อะไรได้บ้าง ?

Big Data คืออะไร ?

คำถามที่ตามมาก็คือ แล้วข้อมูลมันต้องมากแค่ไหนเราถึงจะเรียกมันว่า Big Data ? เค้าว่ากันว่าจะเรียก Big Data ได้ ข้อมูลจะต้องมีลักษณะตามนี้ฮะ

  • Volumn อย่างแรกเลยคือข้อมูลจะต้องเยอะครับ เช่น 1000TB ขึ้นไป
  • Velocity นอกจากเยอะแล้ว ข้อมูลจะต้องโตเร็วด้วยนะครับ อาจจะมีขนาดใหญ่ขึ้น 1TB ต่อวัน เลยทีเดียว
  • Variety อีกเรื่องคือข้อมูลนั้นจะต้องมีความหลากหลายครับ คืออาจจะมีหลาย format แล้วแต่ละ format ต้องใช้วิธีวิเคราะห์ที่แตกต่างกันด้วย อะไรทำนองนี้ฮะ

ผมเคยสงสัยว่า แล้วทำไมเราต้องมาแยกแยะด้วยว่าข้อมูลของเราเป็น Big Data แล้วหรือยัง ? คำตอบคือ เพราะ tool สำหรับ Big Data นั้น มันไม่เหมาะกับข้อมูลน้อยๆ อะครับ หากข้อมูลของเรายังไม่ถึงขั้นเรียกว่า Big Data แล้ว ผมแนะนำให้ใช้ tool ทั่วไปจะเหมาะสมกว่าฮะ พูดง่ายๆ ก็คือ Big Data มันคือข้อมูลที่มีปริมาณมากและมีความซับซ้อนสูงจนไม่เหมาะที่จะนำมาวิเคราะห์ด้วย tool ทั่วไปนั่นเองครับ

ความลำบากในการวิเคราะห์ Big Data

เรามาดูกันครับว่า จากลักษณะของ Big Data ทั้ง 3 ข้อ ข้างต้น ถ้าจะใช้วิธีวิเคราะห์ข้อมูลแบบเดิมๆ เราจะต้องเจอกับปัญหาอะไรบ้าง ?

  • ที่เก็บข้อมูล ลองคิดดูเล่นๆ นะครับว่าถ้าเราจะต้องหาที่มาเก็บข้อมูลที่มันมาวันละ 1TB เราจะต้องเตรียม HDD เอาไว้เยอะแค่ไหน ?
  • ประสิทธิภาพ แล้วต่อให้เราไปซื้อ HDD มาเพิ่ม เพื่อที่จะเก็บข้อมูลให้ได้ทั้งหมด เราก็จะเจอปัญหาเกี่ยวกับ access time ของ HDD ที่ค่อนข้างสูง ทำให้การประมวลผลใช้เวลานานมากเลยกว่าจะได้ผลลัพธ์ออกมา แล้วบางทีเราก็อยากวิเคราะห์ข้อมูลแบบ real-time ซะด้วยสิ
  • เครื่องคอมพิวเตอร์ สมมติว่าเรามีเงินมากพอที่จะใช้ SSD แทน HDD แล้วเตรียมความจุไว้มากพอที่จะเก็บข้อมูล Big Data ได้  ปัญหาต่อมาที่จะต้องเจอก็คือเราจะต้องเตรียมเครื่องคอมพิวเตอร์ที่จะเอาไว้ใช้ในการวิเคราะห์ข้อมูลเหล่านั้นครับ หากใช้แค่เครื่องเดียวมาคำนวณข้อมูลปริมาณมหาศาล กว่าจะเสร็จก็คงเป็นวันๆ ฮะ แล้วผลลัพธ์ที่เราได้มาก็จะกลายเป็นข้อมูลในอดีตไปซะแล้ว ดังนั้นเราจะต้องเตรียมเครื่องคอมพิวเตอร์เอาไว้เยอะพอสมควร เพื่อที่จะทำให้เราได้ผลลัพธ์เร็วเท่าที่เราต้องการครับ
  • ต้นทุน สุดท้ายแล้ว เราก็จะเห็นว่าเม็ดเงินที่จะต้องลงไปกับ Big Data นั้นไม่ใช่น้อยๆ เลยล่ะครับ

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

  • ความปลอดภัยของข้อมูล  อย่าลืมนะครับว่าเราจะต้องคิดเผื่อเหตุการณ์ที่ข้อมูลใน SSD มันเสียด้วย การเก็บข้อมูลเอาไว้ที่เดียวนั้น ถือว่ามีความเสี่ยงสูงครับ ดังนั้นเราจะต้องมีระบบสำรองข้อมูลอัตโนมัติ แล้วการประเมินพื้นที่จัดเก็บข้อมูลนั้นก็จะต้องคิดเผื่อพื้นที่ที่จะต้องใช้ในการสำรองข้อมูลเอาไว้ด้วยนะครับ T__T
  • ประสิทธิภาพ ถึงแม้ว่าเราจะมีพื้นที่จัดเก็บข้อมูลที่ใหญ่และเร็ว และมีเครื่องคอมพิวเตอร์แรงๆ แต่เชื่อเถอะครับว่ามันไม่พอที่จะใช้วิเคราะห์ข้อมูล Big Data หรอก ถ้าเรายังใช้วิธีวิเคราะห์แบบเดิมๆ ที่เราเคยทำกับ RDBMS อยู่ หรือต่อให้ทำได้ ก็คงจะต้องใช้ต้นทุนที่สูงมากๆ เลยล่ะครับ

ถึงแม้ว่าปัญหาที่ผมว่ามาอาจจะดูเยอะ แต่จริงๆ แล้วมันมีตัวช่วยอยู่ครับ ซึ่ง tool ที่เค้านิยมใช้กันมากที่สุดตัวหนึ่งก็คือ Hadoop เรามาดูกันครับว่า เจ้า Hadoop ที่ว่านี้ มันช่วยอะไรเราได้บ้าง ?

รู้จักกับ Apache Hadoop

Apache Hadoop เป็นซอฟต์แวร์ open-source ที่สร้างขึ้นมาเพื่อเป็น framework ในการทำ distributed processing สำหรับข้อมูลขนาดใหญ่ครับ จุดเด่นข้อนึงของ Hadoop ก็คือ เค้าออกแบบมาให้ใช้กับเครื่องคอมพิวเตอร์แบบไม่ต้องแรงมากได้ด้วย การจะขยาย scale ในอนาคต ก็สามารถเพิ่มเครื่องเข้าไปได้ง่ายๆ เลย แถมยังมีระบบสำรองข้อมูลให้โดยอัตโนมัติอีก เรียกว่าถ้าใครกำลังมองหาตัวช่วยในการจัดการกับ Big Data แล้วล่ะก็ Hadoop นี่ถือเป็นทางเลือกที่น่าสนใจเอามากๆ เลยล่ะครับ

hadoop

Hadoop เป็น framework สำหรับการประมวลผลข้อมูลขนาดใหญ่

องค์ประกอบของ Hadoop

ด้วยความซับซ้อนของ Big Data จึงทำให้ Hadoop แบ่งออกเป็นโมดูลย่อยๆ ครับ ในบทความนี้ ผมจะขอพูดถึงแต่โมดูลหลักๆ แล้วกันนะฮะ

  • Hadoop Distributed File System (HDFS) โมดูลนี้จะเอาไว้ใช้จัดเก็บข้อมูลที่จะนำมาวิเคราะห์ให้อยู่ในรูปที่สามารถเข้าถึงได้อย่างรวดเร็ว รวมไปถึงการสำรองข้อมูลดังกล่าวให้โดยอัตโนมัติครับ
  • MapReduce ส่วนโมดูลนี้จะเอาไว้ใช้เกี่ยวกับการประมวลผลข้อมูลปริมาณมหาศาลที่เราได้เก็บเอาไว้ครับ

พูดง่ายๆ ก็คือ Hadoop มันจะแบ่งออกเป็น 2 ส่วน นั่นก็คือ ส่วนที่เอาไว้เก็บกับส่วนที่เอาไว้คิดนั่นเองฮะ เพื่อให้เห็นภาพมากขึ้น ผมจะขอลงรายละเอียดเพิ่มเติมอีกนิดนึงแล้วกันนะครับ

เก็บข้อมูล Big Data ไว้ใน HDFS

สมมติว่าเรามี Cluster ที่ได้ติดตั้ง Hadoop เอาไว้จนพร้อมใช้งาน แล้วเราต้องการจะใช้ Hadoop ประมวลผลข้อมูลที่อยู่ในไฟล์ CSV ไฟล์นึง เราจะมีวิธีการอย่างไร?

ปล่อยให้ HDFS จัดการเรื่องไฟล์

วิธีการก็คือ ให้เราอัพโหลดไฟล์ CSV นี้ เข้าไปเก็บไว้ใน HDFS ก่อน แล้วปล่อยให้เจ้า HDFS ทำหน้าที่ของมัน 2 อย่างนี้ฮะ

  • แบ่งไฟล์ออกเป็น Block ย่อยๆ สมมติว่าไฟล์ CSV นั้น มีขนาด 1TB การประมวลผลไฟล์ใหญ่ขนาดนี้ รับรองว่าช้าแน่ๆ ฮะ HDFS เลยจะแบ่งไฟล์นี้ออกเป็นไฟล์ย่อยๆ ที่เรียกว่า “Block” แล้วนำไปเก็บกระจายตาม Node ต่างๆ ใน Cluster ของเรา เพียงเท่านี้ Node ต่างๆ ก็จะสามารถช่วยกันประมวลผลไฟล์ CSV นี้แบบขนานกันได้แล้วล่ะครับ
  • สำรอง Block เหล่านั้น เอาไว้ที่ Node อื่นๆ นอกจากนั้น HDFS ยังช่วย replicate แต่ละ Block เอาไว้ที่ Node อื่นๆ ด้วยนะครับ(default คือ replicate ไป 3 Node) คือสมมติ Block A ของ Node 1 พัง เรามั่นใจได้เลยว่า Block A จะยังมีสำรองอยู่ใน Node อื่นๆ อย่างแน่นอนฮะ

รู้จักกับ Node ใน Hadoop

Node นั้นหมายถึงเครื่องคอมพิวเตอร์ที่ประกอบไปด้วย CPU, RAM แล้วก็ Disk ครับ ซึ่ง Node ต่างๆ ใน Hadoop จะแบ่งออกเป็น 2 แบบด้วยกัน

  • Data Node เป็น Node ที่ทำหน้าที่เก็บ Block ของไฟล์เอาไว้ และรับผิดชอบในการประมวลผล Block นั้นๆ ครับ แต่ตัว Data Node เอง มันจะไม่รู้นะครับว่า Block ที่ตัวเองเก็บอยู่นั้น เป็นของไฟล์ไหน
  • Name Node เป็น Node ที่ทำหน้าที่รวบรวมผลของการประมวลผล Block ต่างๆ จาก Data Node ทั้งหลายครับ ซึ่งแน่นอนว่าเจ้า Name Node นี้ มันจะต้องรู้ทุกอย่างเกี่ยวกับไฟล์ต้นฉบับ ไม่ว่าจะเป็นชื่อไฟล์, ขนาด รวมไปถึงที่อยู่ของแต่ละ Block ที่ถูกกระจายออกไปตาม Data Node ต่างๆ หรือพูดง่ายๆ Name Node มันก็คือ Master ส่วน Data Node ก็คือ Slave นั่นเองครับ

และอย่างที่บอกไปนะครับว่า ค่า defalut ของการ replicate แต่ละ Block นั้นจะอยู่ที่ 3 Node นั่นหมายความว่า การจะใช้ Hadoop ได้อย่างมีประสิทธิภาพนั้น เราอาจจะมีอย่างน้อย 5 เครื่อง ครับ คือ Data Node 3 เครื่อง Name Node 1 เครื่อง แล้วก็ Name Node ตัว secondary อีก 1 เครื่อง ส่วน spec นั้น ให้อัด Disk ให้ Data Node เยอะๆ ครับ แล้วก็อัด RAM ให้ Name Node เยอะๆ เพราะข้อมูลที่ Name Node ดูแลนั้น ส่วนใหญ่จะเก็บอยู่ใน RAM เพื่อป้องกันไม่ให้เกิดปัญหา bottle neck นั่นเองฮะ

ประมวลผลข้อมูลด้วย MapReduce

เมื่อเอาข้อมูลมาเก็บลงใน HDFS เรียบร้อยแล้ว ทีนี้เรามาดูวิธีการนำข้อมูลเหล่านั้นมาประมวลผลฮะ สมติว่าข้อมูลในไฟล์ CSV นั้น เป็นข้อมูลของคนไทยทุกคน แล้วเราจะหาว่าคนที่อายุเยอะที่สุดในไทย มีอายุเป็นเท่าไร เราจะออกแบบการคำนวณอย่างไรดี ? Hadoop เค้าแนะนำให้ทำเป็นขั้นเป็นตอนตามนี้ฮะ

  • Map เนื่องจาก record มันมีขนาดหลายล้าน อยู่ดีๆ เราไปหาเลยว่าคนไหนอายุเยอะสุดคงไม่ได้ฮะ วิธีที่ดีกว่าคือให้เราแบ่งข้อมูลออกเป็นโซนย่อยๆ ก่อน แล้วจึงหาคำตอบที่ต้องการภายในโซนนั้นๆ ไล่ไปเรื่อยๆ ทีละโซนๆ ฮะ อย่างในที่นี้ เราอาจจะหาก่อนว่าใครอายุเยอะที่สุดในแต่ละเขต/อำเภอก็ได้ครับ
  • Shuffle พอเรารู้แล้วว่าใครอายุเยอะสุดในแต่ละอำเภอ ก็ให้เรารวบรวมข้อมูลเหล่านั้นมา group by จังหวัด แล้วเรียงลำดับอายุจากมากไปน้อยฮะ ทีนี้เราก็พอจะรู้คร่าวๆ แล้วว่าจังหวัดไหนมีคนอายุเยอะๆ อยู่บ้าง
  • Combine หลังจากที่เราเรียงลำดับข้อมูลจากมากไปน้อยแล้ว ให้เราตัดข้อมูลที่ไม่เข้าข่ายออกให้หมดฮะ สมมติว่าข้อมูลของคนอายุสูงสุดของแต่ละเขตในกรุงเทพฯ เรียงลำดับจากมากไปน้อยเป็น 100, 95, 93, 87, … , 80 ให้เราเหลือไว้แต่คนที่อายุสูงสุดซึ่งก็คือ 100 ครับ หรือพูดง่ายๆ ก็คือ ให้เราหาผู้ชนะของแต่ละจังหวัดนั่นเองครับ
  • Reduce มาถึงตรงนี้ เราจะได้แชมป์ของแต่ละจังหวัดมาแล้ว ถูกมั้ยครับ ? ขั้นตอนสุดท้ายก็คือการหาว่าในบรรดาแชมป์เหล่านั้น ใครกันที่อายุเยอะที่สุด จะเห็นว่าขั้นตอนนี้ไม่ค่อยยากแล้ว เพราะเราได้ปรับแต่งข้อมูลให้อยู่ในรูปที่คำนวณง่ายเอามากๆ แล้วนั่นเองฮะ

ผมมั่นใจว่าคำถามในหัวของทุกคนในตอนนี้ก็คือ แล้วไอ้ 4 ขั้นตอนที่ว่ามานี้ เราจะต้องไปทำที่ไหน อย่างไร คำตอบคือเราจะต้องเขียนโปรแกรมขึ้นมาฮะ ^0^ ซึ่งภาษาที่เค้าใช้กันก็จะเป็นพวก Java, C++, Python หรือ Scala แต่ในบทความนี้ ผมจะขอยังไม่ลงรายละเอียดนะครับ ให้เราไปอ่านกันเอาเองที่เว็บหลักในหัวข้อ MapReduce Tutorial แต่ถ้าจะให้สรุปแบบสั้นๆ เลยก็คือ สมมติเราเลือกเขียน MapReduce ด้วย Java พอเขียนเสร็จ เราก็แค่อัพโหลดไฟล์ JAR ของเราขึ้น HDFS แล้วก็รันโปรแกรมที่เราเขียน โดยระบุ input ให้เป็นไฟล์ CSV ของเรา แล้วก็ระบุ output เป็นที่ๆ เราต้องการจะให้ Hadoop เก็บผลลัพธ์จากการรันเอาไว้ อะไรทำนองนี้ครับ

Tool อื่นๆ ที่ควรรู้จัก

ตอนนี้ผมว่าเราคงพอจะเห็นภาพกันคร่าวๆ แล้วล่ะครับว่า Hadoop มันช่วยอะไรเราได้บ้าง ทีนี้เรามาดู tool ตัวอื่นๆ ที่เค้านิยมใช้ร่วมกับ Hadoop กันครับว่ามันมีอะไรบ้าง

Apache Pig

หากเราไม่ถนัดทางด้านการเขียนโปรแกรมเท่าไร หรือบางทีเราแค่อยากจะวิเคราะห์อะไรง่ายๆ ไม่อยากมานั่งเสียเวลาเขียนโปรแกรม Apache Pig ช่วยเราได้ฮะ คือมันจะคล้ายๆ กับการทำ MapReduce เลย เพียงแต่ Pig มันจะมีฟีเจอร์ที่เรียกว่า Pig Latin ที่จะทำให้เราสามารถ query ข้อมูลด้วยคำสั่งง่ายๆ ได้ด้วย อย่างการวิเคราะห์หาคนอายุเยอะสุดก่อนหน้านี้ เราก็จะใช้คำสั่ง LOAD ข้อมูลจากไฟล์ CSV มาก่อน จากนั้นก็ GROUP คนที่อายุสูงสุดตามจังหวัด แล้วก็ FOREACH จังหวัดทั้งหมด เพื่อหาคำตอบว่าคนไหนที่อายุเยอะที่สุด สุดท้ายก็ STORE ผลลัพธ์ออกมาเก็บเป็นไฟล์ อะไรทำนองนี้ฮะ ลองดูตัวอย่างคำสั่งของ Pig ตามด้านล่างนี้

A = LOAD ‘myfile.txt’ USING PigStorage() AS (t, u, v);
B = GROUP A BY t;
C = FOREACH B GENERATE group, COUNT(A.t) as mycount;
D = ORDER C BY mycount;
STORE D INTO ‘mysortedcount’ USING PigStorage();

จะเห็นว่าโค้ด Pig Latin นี่มันอ่านรู้เรื่องเลยนะครับ ส่วนวิธีรันก็ง่ายๆ ฮะ ให้เราเซฟคำสั่งเหล่านี้เป็นไฟล์ .pig แล้วอัพโหลดขึ้น HDFS จากนั้นก็รันผ่าน command-line ได้เลย

Apache Hive

ส่วน Apache Hive นี่ก็จะคล้ายๆ กันเลยฮะ เพียงแต่เราจะเปลี่ยนจากการเขียน Pig Latin มาเป็นการใช้ HiveQL ซึ่งมี syntax ใกล้เคียงกับ SQL ที่เราถนัดแทน ลองดูตัวอย่าง HiveQL ด้านล่างนี้ฮะ

SELECT age FROM people
  WHERE province = 'bangkok'
  GROUP BY city
  ORDER BY age DESC

รู้สึกว่าชีวิตสบายขึ้นเยอะเลยใช่มั้ยล่ะครับ ผมว่ามันเหมาะมากเลย หากเราอยากจะลอง query อะไรง่ายๆ ไว้เราไปเจอเคสที่การคำนวณมันซับซ้อนเอามากๆ เราค่อยเขียน MapReduce เอาก็ได้ครับ

Apache Spark

Apache Spark เป็น tool สำหรับทำ data processing ที่สร้างบน Hadoop อีกทีครับ จุดขายของ Spark นั้นจะอยู่ที่ความเร็วในการประมวลผล ซึ่งเค้าเคลมว่าสามารถเร็วกว่า MapReduce ของ Hadoop ได้ถึง 10-100 เท่าเลยทีเดียว แถมยังมาพร้อมกับ Spark SQL ที่จะช่วยให้เราสามารถเขียน query ได้ง่ายๆ เหมือนกับการใช้ HiveQL อีกด้วยฮะ

apache spark

Spark เคลมว่าเร็วกว่า MapReduce 10-100 เท่า

ลองใช้งานจริงด้วย Cloud Dataproc

มาถึงตรงนี้ เราจะเห็นว่า Big Data มันไม่ได้เข้าใจยากอย่างที่คิดเลยใช่มั้ยละครับ แต่ถ้าจะให้เห็นภาพมากขึ้น ผมว่าเราจะต้องลองเล่นของจริงดูเลยฮะ แล้วในปัจจุบัน การหา Hadoop มาเล่นก็ไม่ได้ลำบากเหมือนแต่ก่อนแล้ว เพราะเมื่อไม่นานมานี้ Google เค้าได้เปิดตัว Cloud Dataproc ซึ่งเป็นบริการที่นำ Hadoop และ Spark มาติดตั้งบนระบบ Cloud ของ Google แถมยังปรับแต่งระบบมาให้เราใช้งานได้ง่ายๆ แล้วค่าบริการก็ถือว่าไม่แพงเท่าไรด้วยครับ

หากข้อมูลที่ต้องการจะวิเคราะห์อยู่บนระบบ Cloud ของ Google อยู่แล้ว การใช้ Cloud Dataproc ก็ยิ่งสะดวกเข้าไปใหญ่ครับ ให้เราเข้า Google Cloud Platform แล้วไปที่ Big Data แล้วเลือก Cloud Dataproc เราก็จะเจอเมนูย่อย 2 อัน สำหรับสร้าง Cluster และ Job ที่เราอยากจะให้รันบน Cluster ของเราครับ ให้เราเลือก Clusters แล้ว Create a cluster ได้เลย

cloud dataproc - create cluster

สร้าง Cluster ที่จะใช้กับ Cloud Dataproc

setup a cluster

ตั้งชื่อ Cluster พร้อมระบุโซน และจำนวน Node ที่ต้องการจะใช้

เมื่อได้ Cluster มาแล้ว เราก็จะมาสร้าง Job กันต่อฮะ ข่าวดีก็คือ Cloud Dataproc รองรับทั้ง Pig, Hive และ Spark SQL เลย หรือใครอยากจะเขียนโปรแกรมเองก็สามารถระบุไฟล์ที่ต้องการจะรันได้เช่นกันครับ

cloud dataproc job type

Cloud Dataproc รองรับทั้ง MapReduce ของ Hadoop เอง รวมไปถึง Pig, Hive และ Spark SQL

hive query

สมมติเราเลือกใช้ Hive เราก็สามารถใส่ Query ที่ต้องการจะรันลงไปได้ทันที

หนทางยังอีกยาวไกล…

ขอย้ำอีกทีนะครับว่า บทความนี้มันเป็นแค่การพูดถึงภาพรวมเท่านั้น เพราะผมตั้งใจจะให้เพื่อนๆ เข้าใจ Big Data ได้ง่ายๆ และใช้เวลาน้อยที่สุดครับ จริงๆ แล้ว หลายๆ หัวข้อของบทความนี้ สามารถแยกออกมาเป็นอีกบทความ หรืออาจเขียนเป็นหนังสือเลยก็ยังได้ฮะ เพราะเนื้อหามันยาวเอามากๆ แต่ผมก็ไม่รู้ว่าจะไปอ่านหนังสือเหล่านั้น แล้วเอามาแปลลงบทความนี้ไปทำไม สู้ให้เพื่อนๆ ตัดสินใจเอาเองดีกว่า ว่าสนใจเรื่องอะไร จะได้ไปตามอ่านจากเว็บหลักเอาเองได้ถูกครับ ผมก็หวังว่าบทความนี้จะมีประโยชน์กับเพื่อนๆ ที่กำลังจะเริ่มศึกษาเรื่อง Big Data ไม่มากก็น้อยนะฮะ แล้วพบกันใหม่บทความหน้าครับ ^__^

]]>
http://www.siamhtml.com/getting-started-with-big-data-and-hadoop-spark-on-cloud-dataproc/feed/ 11
ถ้าย้อนเวลาได้ ผมอยากเข้าค่าย YWChttp://www.siamhtml.com/unfortunate-that-i-never-participate-in-ywc/ http://www.siamhtml.com/unfortunate-that-i-never-participate-in-ywc/#comments Thu, 29 Oct 2015 17:02:06 +0000 http://www.siamhtml.com/?p=9804 เมื่อปีก่อน ผมได้มีโอกาสไปร่วมงาน YWC ครั้งที่ 12 ครับ (แต่เป็นในฐานะสปอนเซอร์จาก Sanook นะครับ เพราะอายุเกินไปมากแล้ว) ก่อนที่จะไปถึงงาน ผมก็คิดเอาไว้แล้วแหละว่า ค่ายนี้มันจะต้องไม่ธรรมดาแน่นอน เพราะคนที่รู้จักแต่ละคนที่เคยผ่านค่ายนี้มานี่เก่งๆ กันทั้งนั้นเลย แล้วพอได้ไปสัมผัสกับค่ายนี้จริงๆ แล้ว ผมนี่เสียดายเลย ที่ไม่ได้เข้าร่วมค่ายนี้เมื่อตอนที่ยังมีโอกาส ทำไมน่ะเหรอ ?

ความรู้จากกูรูชื่อดัง

อย่างแรกเลยก็คือเสียดายความรู้ดีๆ จากกูรูชื่อดังในวงการเว็บไซต์ครับ โดยในค่ายนี้จะแบ่งความรู้ออกเป็น 4 สาขา ให้เราเลือกได้ นั่นก็คือ Content, Design, Marketing แล้วก็ Programming ฮะ แน่นอนว่าวันนั้นผมเลือกเข้าไปฟังสาขา Programming ซึ่งมี คุณศิระ สัจจินานนท์ แห่ง jitta.com มาให้ความรู้แก่น้องๆ ในค่าย บอกเลยว่าขนาดผมที่ทำงานมาหลายปีแล้ว เข้าไปฟังแค่วันเดียวยังได้อะไรไปเยอะเลย แล้วน้องๆ ที่อยู่จนจบค่ายจะได้ความรู้ไปเยอะขนาดไหน !

ประสบการณ์ที่หาไม่ได้จาก Google

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

สังคมนี่สำคัญสุดๆ

และที่ขาดไม่ได้เลยก็คือสังคมของคน YWC ด้วยกันฮะ สมัยนี้ทำอะไรตัวคนเดียว อาจจะไปได้ไว แต่รับรองว่าไปได้ไม่ไกลหรอกฮะ เราต้องมีทีม ค่ายนี้ช่วยเราได้เต็มๆ ครับ น้องๆ จะได้รู้จักพี่ๆ รุ่นก่อนๆ ที่จะมาคอยเทคแคร์ แล้วหลายๆ คนก็เป็นคนที่มีชื่อเสียงซะด้วย เรียกว่าไปเอาแค่ connection อย่างเดียวยังคุ้มเลยฮะ

งานนี้มันฟรี !

และที่เสียดายสุดๆ ก็คือ เพราะงานนี้มันฟรีฮะ ลองนึกตามนะครับว่า น้องๆ จะได้รับทั้งความรู้ ประสบการณ์ และ connection โดยที่ไม่ต้องเสียตังเลยแม้แต่บาทเดียว ความรู้เราอาจจะค้นหาจาก Google ได้ แต่เราคงจะเสียเวลา หากไม่มีคนคอยแนะแนวทาง ประสบการณ์เราอาจจะสร้างขึ้นมาเองได้ แต่มันต้องใช้เวลา connection เราอาจจะมีเยอะแล้ว แต่คงไม่ใช่กับพี่ๆ ที่นี่ แล้วเจอกันที่ค่ายนะครับ ผมจะรออยู่ที่ห้อง Programming ^__^

น้องๆ คนไหนสนใจก็สมัครกันได้เลยนะครับ ที่ www.ywc.in.th

]]>
http://www.siamhtml.com/unfortunate-that-i-never-participate-in-ywc/feed/ 0
AMP HTML คืออะไร ? + สอนวิธีใช้http://www.siamhtml.com/getting-started-with-amp-html/ http://www.siamhtml.com/getting-started-with-amp-html/#comments Sun, 18 Oct 2015 13:51:08 +0000 http://www.siamhtml.com/?p=9727 บทความนี้ ทาง SiamHTML จะมาพูดถึงวิธีรับมือกับโปรเจคใหม่ของ Google ที่เพิ่งเปิดตัวไปเมื่อสัปดาห์ก่อนอย่าง Accelerated Mobile Pages ครับ เรามาดูกันซิว่ามันส่งผลกระทบอะไรกับ front-end developer อย่างเราบ้าง ?

เพราะโมบายเว็บทุกวันนี้มันโหลดช้า !

ต้องยอมรับนะครับว่าทุกวันนี้ เทคโนโลยีของการทำเว็บมันเปลี่ยนแปลงไปไวมากๆ การพัฒนาเว็บก็เลยทำได้หลายวิธี บางคนมีท่าที่ตัวเองถนัด ก็เลยเลือกใช้ท่านั้นมาตลอด จนอาจไม่รู้ว่ามันมีวิธีที่ดีกว่านั้นแล้ว บางคนไม่ถนัดเลยซักท่า search เจอท่าไหนใน stackoverflow ก็ใช้ท่านั้นเลย เอามายำๆ กันจนงานเสร็จ สุดท้ายแล้วเว็บมันก็ช้าสิครับ เพราะมีโค้ดอะไรก็ไม่รู้ใส่เข้ามาเต็มไปหมด จะมี developer ซักกี่คนที่มีความเข้าใจในเรื่องของ web performance อย่างลึกซึ้ง และใส่ใจในทุกรายละเอียด ?

Google คิดค้น Accelerated Mobile Pages

Google มองว่านี่คือปัญหาครับ ทุกวันนี้เรามักจะเห็นหลายๆ เว็บเลย ที่โหลดช้าเอามากๆ โดยเฉพาะในโมบาย Google เลยพยายามหาว่า มันพอจะมีวิธีไหนมั้ย ที่ทำให้เว็บต่างๆ โหลดได้เร็วเหมือนๆ กันหมด ? วิธีที่ Google คิดได้ก็คือการ “จำกัด” HTML ครับ

Google มองว่าตัว developer เองนั่นแหละครับ ที่ไปทำให้เว็บมันช้าด้วยการเขียนโค้ดที่ไม่คำนึงถึง performance ก็เลยปิ๊งไอเดียสร้าง HTML แบบใหม่ขึ้นมาที่มีชื่อเรียกว่า AMP HTML โดยเจ้า HTML แบบใหม่ที่ว่านี้ มันก็คือ HTML แบบปกติที่เราเขียนกันอยู่ทุกวันนั่นแหละครับ เพียงแต่มันจะมี tag ใหม่ๆ บางตัวเพิ่มเข้ามาให้เราได้ใช้กัน ซึ่ง tag พวกนี้เอง ที่ Google จะเอาไว้ใช้ “ล้อมกรอบ” ไม่ให้โค้ดของเราออกนอกลู่นอกทาง หรือพูดง่ายๆ ก็คือ AMP HTML เป็น subset ของ HTML ที่เพิ่ม tag ใหม่บางตัวเข้ามา เพื่อที่จะเอาไว้คุม performance ของหน้าเว็บไม่ให้ตกลงไปนั่นเองครับ

ส่วนประกอบของ AMP HTML

ก่อนจะไปดูหน้าตาของ AMP HTML ผมขอเล่าก่อนนะครับว่า AMP HTML มันประกอบไปด้วย 3 ส่วนนี้

  • HTML Components คือ tag ใหม่ ที่ Google บอกให้เราเปลี่ยนมาใช้แทน tag แบบเดิม
  • JS Library เป็น JavaScript ที่เอาไว้อ่าน tag ใหม่ที่เพิ่มเข้ามา
  • Validator เอาไว้ validate ว่าหน้าเว็บนี้เขียนโค้ดถูกต้องตามหลัก AMP HTML แล้วหรือยัง

ตัวอย่างโค้ดของ AMP HTML

ก่อนที่เราจะไปดูหน้าตาของ tag ใหม่ที่เพิ่มเข้ามาใน AMP HTML ผมอยากให้เรารู้ข้อตกลงเบื้องต้นของ AMP HTML ก่อนนะครับว่า หน้าเว็บที่จะนับว่าเป็น AMP HTML ได้ จะต้องมีลักษณะอย่างไร ลองดูตัวอย่างโค้ด AMP HTML ด้านล่างนี้ฮะ

<!doctype html>
<html amp>
  <head>
    <meta charset="utf-8">
    <link rel="canonical" href="http://www.siamhtml.com/getting-started-with-amp-html/">
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <style>body {opacity: 0}</style><noscript><style>body {opacity: 1}</style></noscript>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
  </head>
  <body>Hello World!</body>
</html>

หากเราไล่โค้ดดู เราก็จะรู้เลยว่าสาเหตุที่ Google ต้องตั้งข้อตกลงขึ้นมาก็เพื่อที่จะสามารถการันตีในเรื่องของ performance ได้นั่นเองครับ อารมณ์ประมาณว่า ถ้าคุณอยากให้เว็บของคุณโหลดเร็วๆ คุณก็จะต้องวางโครงสร้าง HTML ตามนี้นะ หากคุณทำตามที่ฉันบอกทั้งหมด รับรองว่าหน้าเว็บของคุณโหลดไวแน่ๆ ซึ่งข้อตกลงที่ว่า สามารถสรุปสั้นๆ ได้ประมาณนี้ฮะ

1. ประกาศก่อนว่าหน้าเว็บนี้ใช้ AMP HTML

เริ่มด้วยการใส่ attribute amp เข้าไปที่ html แบบนี้ฮะ

<html amp>

2. ทำ Canonical กลับไปที่หน้าหลัก

จากนั้นก็กำหนด canonical ให้เป็น url ของหน้าเว็บแบบปกติของเราครับ

<link rel="canonical" href="http://www.siamhtml.com/getting-started-with-amp-html/" />

3. กำหนด Viewport Meta Tag

ส่วนที่ meta viewport ให้เรากำหนดเหมือนกับการทำ responsive web ทั่วไปได้เลยฮะ

<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">

4. Inline Critical CSS

สำหรับ css นี่ ให้เราใส่เฉพาะโค้ด critical css เข้าไปครับ ส่วนโค้ด css ที่เหลือ เราค่อยโหลดตามมาทีหลังด้วยวิธี asynchronous

<style>body {opacity: 0}</style>
<noscript>
    <style>body {opacity: 1}</style>
</noscript>

5. โหลด JavaScript แบบ Async

สุดท้าย ให้เราโหลด AMP JS library มาใช้ครับ และแน่นอนว่าเราต้องโหลดมันมาแบบ async

<script async src="https://cdn.ampproject.org/v0.js"></script>

จริงๆ แล้ว ข้อตกลงมันมีเยอะกว่านี้นะครับ เราสามารถเข้าไปดูทั้งหมดได้ที่ Required markup

HTML Tags ใน AMP HTML

เมื่อวางโครงเสร็จเรียบร้อยแล้ว เรามาดู tag ของ AMP HTML กันเลยครับ ว่ามันมีแบบไหนบ้าง

  • Tag ที่ห้ามใช้มีหลายๆ tag เลยนะครับ ที่ไม่สามารถใช้ได้แล้วใน AMP HTML แต่ส่วนใหญ่ก็จะเป็น tag ที่เราไม่ค่อยได้ใช้กันอยู่แล้ว อย่างพวก frame, frameset รวมไปถึง tag อย่าง object, param, embed ฮะ ที่ต้องระวังหน่อยก็จะมี tag ในกลุ่มของ form และ input ต่างๆ รวมไปถึง textarea และ select ที่จะใช้ไม่ได้แล้วเหมือนกันครับ
  • Tag ที่ใช้ได้ภายใต้เงื่อนไขที่กำหนดเช่น script ที่จะให้ใช้ได้เฉพาะโหลด AMP JS library รวมไปถึง library อื่นๆ ที่ AMP HTML เป็นคนจัดหาไว้ให้เท่านั้นครับ นอกจากนั้นก็จะมี style และ link ที่ให้ใส่ได้เฉพาะตามข้อตกลงที่ AMP HTML ระบุไว้เท่านั้นเหมือนกัน ทั้งนี้ก็เพื่อที่จะคุมคุณภาพของโค้ดไม่ให้มีโค้ดที่ไม่จำเป็นนั่นเองครับ
  • Tag ที่หน้าตาเปลี่ยนไปtag อย่าง img, video, audio และ iframe จะมีการเพิ่ม prefix amp- เข้าไปข้างหน้าเป็น amp-img, amp-video, amp-audio และ amp-iframe ครับ จะสังเกตว่า tag พวกนี้ล้วนมีการโหลดของข้างนอกมาใช้ Google เค้าเลยต้องให้เราเปลี่ยนมาใช้ tag ใหม่ เพื่อที่จะสามารถจัดการเรื่องการโหลดทรัพยากรตรงนี้ได้นั่นเองครับ

เพื่อให้เห็นภาพมากขึ้น ลองดูตัวอย่างโค้ด AMP HTML ด้านล่างนี้ฮะ

<!doctype html>
<html amp>
  <head>
    <meta charset="utf-8">
    <title>Sample document</title>
    <link rel="canonical" href="http://www.siamhtml.com/getting-started-with-amp-html/">
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <style amp-custom>
      h1 {color: red}
    </style>
    <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "NewsArticle",
      "headline": "Article headline",
      "image": [
        "thumbnail1.jpg"
      ],
      "datePublished": "2015-02-05T08:00:00+08:00"
    }
    </script>
    <script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>
    <style>body {opacity: 0}</style><noscript><style>body {opacity: 1}</style></noscript>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
  </head>
  <body>
    <h1>Sample document</h1>
    <p>
      Some text
      <amp-img src=sample.jpg width=300 height=300></amp-img>
    </p>
    <amp-ad width=300 height=250
        type="a9"
        data-aax_size="300x250"
        data-aax_pubname="test123"
        data-aax_src="302">
    </amp-ad>
  </body>
</html>

จากโค้ดด้านบน ผมขออธิบายเพิ่มเติมนิดนึงฮะ

  • เราสามารถใช้ style เพื่อ inline critical css ได้นะครับ แต่ต้องใส่ attribute amp-custom เข้าไปด้วย
  • เรายังสามารถใช้ script ได้อยู่นะครับ เพียงแต่ type จะต้องกำหนดให้เป็น application/ld+json เท่านั้น เพราะ type ที่ว่านี้จะไม่มีการ execute ใดๆ จึงไม่มีผลต่อ performance ฮะ
  • พวก script ที่เอาไว้ทำ ui ต่างๆ อย่างเช่น carousel นั้น เรายังสามารถใช้ได้อยู่นะครับ แต่จะต้องเปลี่ยนมาใช้ของที่ Google เค้าเตรียมไว้ให้เท่านั้นฮะ
  • AMP HTML รองรับการใส่ Ad ด้วยนะครับ ผ่านทาง amp-ad อย่าลืมนะครับว่าคนทำนั้นคือ Google :p

ยังมี tag อีกมากเลยนะครับ ที่ผมยังไม่ได้พูดถึง เนื้อหาในส่วนนี้ผมอยากให้ไปอ่านเพิ่มเติมที่หัวข้อ HTML Tags ของ AMP HTML ครับ เพราะมันยังมีการอัพเดทอยู่เรื่อยๆ เลย

AMP HTML มีผลกับ Google Search !

คำถามที่ทุกคนจะต้องสงสัยแน่นอนเลยก็คือ หลังจากที่เราทำหน้าเว็บเวอร์ชั่นที่เป็น AMP HTML เสร็จแล้ว เจ้าหน้าพวกนี้มันจะไปแสดงตรงไหน ? คำตอบคือใน Google Search ของโมบายครับ Google จะดูว่าในบรรดาผลลัพธ์ที่เจอนั้น มีหน้าไหนที่รองรับ AMP HTML แล้วบ้าง ถ้ามีก็จะเลือกเวอร์ชั่นที่เป็น AMP HTML มาแสดงครับ

ส่วนคำถามที่ว่า แล้ว Google จะรู้ได้อย่างไรว่าหน้าเว็บนี้มีเวอร์ชั่น AMP HTML ด้วย คำตอบคือ เพราะเราเป็นคนบอก Google เอง ด้วยโค้ดนี้ฮะ

<link rel="amphtml" href="http://www.siamhtml.com/amp/getting-started-with-amp-html/">

ให้เราใส่โค้ดด้านบนไว้ที่หน้าเว็บเวอร์ชั่นปกติของเรา เพียงเท่านี้ Google ก็จะรู้แล้วล่ะครับว่า หน้าเว็บเวอร์ชั่นที่เป็น AMP HTML นั้นอยู่ที่ไหน แต่ล่าสุด Google ก็ยังไม่ได้บอกตรงๆ นะครับว่า AMP HTML นั้น มีผลกับการจัดอันดับของ Google ด้วยหรือเปล่า ถ้ามีผลเมื่อไรก็งานเข้าเมื่อนั้นล่ะครับ T__T

อนาคตของ AMP HTML

ก็ต้องคอยติดตามข่าวสารจาก Google อย่างใกล้ชิดเลยนะครับว่า อนาคตของ AMP HTML นั้นจะเป็นอย่างไร ถึงแม้ว่าจะมีเว็บยักษ์ใหญ่หลายราย ที่เริ่มทำเวอร์ชั่น AMP HTML ไปบ้างแล้ว แต่ก็มีบางกระแสที่ไม่ค่อยเห็นด้วยกับไอเดียนี้เท่าไร เพราะเค้ามองว่ามันเป็นการจำกัดกันเกินไป จนลดความเป็น web ลงนั่นเองครับ

แต่ส่วนตัวผมอยากให้เราศึกษาเอาไว้บ้างนะครับ อาจยังไม่ถึงกับต้องรีบปรับตามทันที แต่อย่างน้อย ขอให้เราเข้าใจหลักการของมันสักนิดนึงก็ยังดี พอถึงเวลาที่ต้องทำจริงๆ เราจะได้ไม่เสียเวลามากฮะ แล้วอีกอย่างก็คือ ถึงแม้ว่าเราจะไม่ได้ใช้ AMP HTML ในตอนนี้ แต่เราก็สามารถเอาเทคนิคของมันมาปรับใช้กับเว็บของเราได้นะครับ ผมว่าถ้าปรับจูนดีๆ เราก็สามารถทำเว็บของเราให้โหลดได้เร็วไม่แพ้เว็บที่ใช้ AMP HTML เลยล่ะครับ

อัพเดท! มันมาแน่ กุมภาพันธ์นี้…

ล่าสุด Blog ของ AMP ออกมาประกาศถึงความคืบหน้าของโปรเจคแล้วนะครับว่าจะเริ่มแสดงหน้าที่เป็น AMP HTML ในหน้าผลการค้นหาในเดือนกุมภาพันธ์ที่จะถึงนี้ พร้อมกับเสริมด้วยว่าบรรดายักษ์ใหญ่ต่างๆ อย่าง Twitter, LINE และ Pinterest ต่างก็ให้ร่วมมือในการแสดงเนื้อหาจากหน้าที่เป็น AMP HTML ด้วย เรียกว่ามันมาแน่ๆ ครับ แบบนี้ แต่มาถึงตรงนี้ Google ก็ยังไม่ได้บอกตรงๆ อยู่ดีว่า AMP HTML จะมีผลกับการจัดอันดับของ Google ด้วยหรือเปล่า ก็ต้องชั่งใจกันเอาเองละฮะ หากเราจะเอาด้วย ก็เหลือเวลาอีกประมาณ 2 เดือน ครับ ขอให้เพื่อนๆ ทุกคนโชคดีแล้วกันนะฮะ T__T

]]>
http://www.siamhtml.com/getting-started-with-amp-html/feed/ 10
Docker คืออะไร + สอนวิธีใช้สไตล์ Web Developerhttp://www.siamhtml.com/getting-started-with-docker/ http://www.siamhtml.com/getting-started-with-docker/#comments Mon, 31 Aug 2015 03:33:08 +0000 http://www.siamhtml.com/?p=8939 บทความนี้ เราจะมาลองเล่น tool อีกตัวหนึ่ง ที่กำลังถูกพูดถึงมากที่สุดในตอนนี้อย่าง Docker กันครับ แต่เนื่องจากความสามารถของ Docker นั้น ค่อนข้างจะกว้าง หากให้เล่าทั้งหมดก็คงจะใช้เวลาเยอะเลยทีเดียว บทความนี้ผมเลยจะขอเน้นไปที่การใช้งาน Docker ในแง่ของการพัฒนาเว็บไซต์เป็นหลักแล้วกันนะครับ

ปัญหาที่เราเจอกันบ่อยๆ

1. เสียเวลาติดตั้ง Application

ปัญหายอดฮิตของ web developer ทุกคนเลยก็คือ การเสียเวลาไปกับการติดตั้งพวก application ต่างๆ ที่โปรเจคนั้นๆ จำเป็นต้องใช้ ลองคิดกันเล่นๆ ว่าถ้าเราจะทำเว็บด้วย WordPress เราจะต้องลงอะไรบ้าง ?

ที่ต้องลงแน่ๆ เลยก็จะเป็น web server อย่าง Apache หรือ nginx ถูกมั้ยครับ จากนั้นเราก็จะต้องลง PHP แล้วก็ database อย่าง MySQL รวมไปถึง tool อย่าง phpMyAdmin ด้วย และที่ขาดไม่ได้เลยก็คือตัว WordPress เองครับ ส่วนใครอยากจะให้เว็บโหลดเร็วๆ หน่อย ก็อาจจะต้องลง Memcached เพิ่มเข้าไปอีก

จะเห็นว่าขั้นตอนพวกนี้มันกินเวลาไปไม่น้อยเลยล่ะครับ แล้วถ้าโปรเจคนั้นต้องการใช้ application อะไรที่การติดตั้งมันมีความซับซ้อนมากขึ้นกว่านี้ หรือเป็น application ที่เราไม่เคยติดตั้งมาก่อน อย่าง Jenkins, Elasticsearch หรือ Cassandra เราก็จะยิ่งเสียเวลามากขึ้นไปอีกครับ

2. เครื่อง Dev กับ Production ไม่เหมือนกัน

อีกปัญหาที่ผมคิดว่าเราน่าจะเจอกันทุกคนเลยก็คือ สภาพแวดล้อมของการ dev กับ production มันไม่เหมือนกันครับ สมมติเราได้งานฟรีแลนซ์มา 2 งาน งานนึง server ลูกค้าเค้าใช้ PHP 7 อีกงาน server เค้าใช้ HHVM แล้วเราดัน dev บนเครื่องที่ใช้ PHP 5 ปรากฏว่าพอเอางานขึ้นไปแล้วมีปัญหาครับ เนื่องจากเราไม่เคยเทสกับสภาพแวดล้อมของ production มาก่อนเลยนั่นเอง แล้วการที่จะทำให้สภาพแวดล้อมของเครื่อง dev เหมือนกับ production เป๊ะๆ  นี่ก็ไม่ใช่เรื่องง่ายเลย

ปัญหาหมดไป เมื่อมี Docker

ปัญหาดังกล่าวจะไม่เกิดขึ้นเลยครับ ถ้าเราใช้ tool ที่มีชื่อว่า Docker เพราะเจ้าDocker ที่ว่านี้ มันมีความสามารถในการ “ห่อ” เจ้า application ต่างๆ ให้อยู่ในรูปของ “container” ที่เราสามารถนำไปติดตั้งที่เครื่องไหนก็ได้ที่มี Docker รันอยู่ ด้วยความสามารถนี้เอง ที่ทำให้ web developer อย่างเราสบายขึ้น ไม่ว่าจะในฐานะ…

คนนำ Container ไปใช้

อย่างที่เล่าไปก่อนหน้านี้ครับว่า บาง application นั้นมีขั้นตอนการติดตั้งที่ยุ่งยากซับซ้อน หากเราใช้ Docker เราก็แค่ไปโหลด container ที่ชาวบ้านเค้าทำไว้ดีแล้วมาใช้ได้เลย เราอาจจะโหลด container ของ Apache, PHP, MySQL มาก่อน แล้วค่อยลง WordPress เพิ่มเข้าไปเอง หรือจะเลือกโหลด container ของ WordPress แบบพร้อมใช้งานมาเลยก็ได้ สมมติเราได้งานที่ server เค้าลง PHP 5.3 ไว้ เราก็ไปโหลด container ของ PHP 5.3 มารัน หากดึกคืนนั้นมีลูกค้าเก่ามาขอให้ช่วยแก้งานที่ใช้ PHP 5.6 เราก็สลับไปรัน container ของ PHP 5.6 แทน อะไรทำนองนี้ครับ

คนสร้าง Container เอง

สมมติว่าเราได้งาน Node.js ชิ้นใหญ่มา แล้วเราก็ติดตั้ง application ต่างๆ ที่จำเป็นต้องใช้เรียบร้อยแล้ว บังเอิญเราได้เพื่อนมาช่วยเขียนโค้ดอีกคน แล้วเราอยากให้เครื่องของเพื่อนมีสภาพแวดล้อมเหมือนเราเป๊ะๆ เลย สิ่งที่เราต้องทำก็แค่ห่อ application ต่างๆ ที่เราได้ลงไว้ ให้กลายเป็น container ของ Docker ครับ จากนั้นเราก็บอกให้เพื่อนลง Docker ซะ แล้วเราก็ส่ง container ของเรา ไปให้เพื่อนรัน เพียงเท่านี้เพื่อนของเราก็จะสามารถเริ่มงานได้ทันทีแล้วล่ะครับ

รู้จัก Docker ให้มากขึ้น

มาถึงตรงนี้ ผมว่าเพื่อนๆ คงพอจะเห็นภาพแล้วว่า Docker มันช่วยอะไรเราได้บ้าง แต่ก่อนที่เราจะลองเล่นมัน ผมอยากจะให้เราเข้าใจหลักการทำงานของมันสักนิดนึงก่อนครับ

1. Docker ต้องใช้กับ Linux

Docker นั้นใช้ฟีเจอร์บางอย่างของ Linux ครับ นั่นหมายความว่าเครื่องของเราจะต้องติดตั้ง Linux ก่อน ถึงจะรัน Docker ได้ แต่หากใครใช้ Windows หรือ Mac OS X อยู่ แล้วไม่อยากจะเปลี่ยนไปใช้ Linux เราอาจจะใช้ตัวช่วยอย่าง Vagrant หรือ Boot2Docker ในการจำลอง Linux ขึ้นมาก็ได้เช่นกันครับ

2. Container ของ Docker เร็วกว่า VM

บางคนอาจจะบอกว่าสิ่งที่ Docker ทำได้นั้น เราก็สามารถใช้ VM ทำได้เหมือนกันนะ ถูกครับ เพียงแต่ว่ามันจะช้ากว่า Docker เยอะเลยอะครับ ที่เป็นเช่นนี้ก็เพราะว่าในแต่ละ container ของ Docker นั้น จะแชร์ตัว OS รวมไปถึง CPU และ memory ร่วมกันครับ เลยทำให้แต่ละ container นั้นเบามากๆ ไม่เหมือนกับ VM ที่แต่ละตัวจะต้องมี OS เป็นของตัวเอง และเรายังต้องคอยจัดสรรทรัพยากรต่างๆ ให้อีกด้วยครับ บางตัวรันอยู่แต่ไม่ได้ใช้งานเลย ก็เท่ากับว่าเราเสียทรัพยากรไปฟรีๆ

3. Docker มีระบบ Registry

คำถามที่น่าจะสงสัยกันก็คือ แล้วเราจะหา container ที่เราต้องการจะใช้ได้จากที่ไหน คำตอบคือ Docker เค้าจะมีระบบ registry ที่มีชื่อว่า Docker Hub ที่จะเปิดให้คนทั่วไปสามารถเอา container ที่ตัวเองสร้างขึ้นมาเก็บไว้ที่ส่วนกลางเพื่อที่จะได้แบ่งปันให้คนอื่นได้ใช้ด้วยครับ อารมณ์เดียวกับ Node.js ที่มี npm นั่นแหละ สิ่งที่เราต้องทำก็แค่เข้าเว็บ Docker Hub แล้วก็ search หา container ที่ต้องการจะใช้ container ไหนสร้างโดย official เองก็จะมีคนโหลดเยอะหน่อยครับ

4. วิธีสร้าง Container ของ Docker

container ของ Docker นั้นถูกสร้างมาจาก Docker image อีกทีครับ สมมติเราอยากจะสร้าง container ของ Apache เอาไว้ใช้งานเอง เราก็จะต้องสร้าง Docker image ที่บรรจุ Apache ไว้ข้างในขึ้นมาซะก่อน แล้วจึงสั่งให้ Docker รัน image ตัวนี้

ส่วนวิธีสร้างตัว Docker image นั้นก็ไม่ยากเลยครับ ให้เราสร้างไฟล์เปล่าๆ ที่เรียกกันว่า Dockerfile ขึ้นมา แล้วก็ใส่ command ต่างๆ ของ Linux ที่เอาไว้ติดตั้ง Apache ลงไป จากนั้นก็สั่งให้ Docker ทำการ build เจ้าไฟล์นี้ให้กลายเป็น Docker image ครับ แล้วถ้าเราอยากจะแชร์ image นี้ให้คนอื่นได้ใช้ผ่าน Docker Hub ด้วย เราก็สามารถ push ขึ้นไปผ่าน command ของ Docker ได้เลย

สรุปอีกที! ก่อนใช้งาน Docker

เพื่อให้แน่ใจว่าทุกคนเข้าใจ Docker เป็นอย่างดีแล้ว ผมจะขอสรุปภาพรวมให้ฟังอีกที โดยจะแบ่งออกเป็นส่วนๆ ดังนี้ครับ

  • Application คือ สิ่งที่เราต้องการจะใช้งาน เช่น Apache, PHP, MySQL, WordPress, Laravel เป็นต้น
  • Docker image คือ ไฟล์ image ที่บรรจุ application ข้างต้นเอาไว้ โดยไฟล์นี้ได้มาจากการ build ไฟล์ Dockerfile ที่ใส่ command ของ Linux สำหรับติดตั้ง application ดังกล่าวเอาไว้
  • Docker container คือ กล่องที่บรรจุ application ที่พร้อมทำงาน ซึ่งเป็นผลมาจากการรัน Docker image
  • Docker Hub คือ  ที่ๆ เราสามารถอัพโหลด image ที่ตัวเองสร้างขึ้นไปเพื่อแบ่งปันให้คนอื่นได้ใช้ด้วย หรือเราจะดาวน์โหลด image ที่คนอื่นทำไว้ดีแล้วมาใช้งานเลยก็ได้

มาถึงตรงนี้ พื้นฐานเกี่ยวกับ Docker ของทุกคนคงจะแน่นปึ๊กแล้ว ผมว่าเรามาเริ่มใช้ Docker กันเลยดีกว่าครับ

Workshop

ก่อนที่เราจะมาเริ่มใช้งาน Docker ผมอยากให้เราดู OS ของเครื่องที่เราใช้อยู่ก่อนนะครับว่ามันเป็นอะไร

  • Windows / Mac OS X ต้องติดตั้ง Docker Machine เพื่อจำลอง Linux ขึ้นมาก่อน แล้วค่อยติดตั้ง Docker Engine อีกที
  • Linux สามารถติดตั้ง Docker Engine ได้ทันที

สำหรับบทความนี้ ผมจะขอถือว่าเราใช้ Mac OS X แล้วกันนะครับ หากใครใช้ OS อื่น ก็ไม่เปนไรฮะ เพราะวิธีการใช้งานนั้น ไม่ได้แตกต่างกันมาก

1. ติดตั้ง Docker Toolbox

ขั้นตอนแรกให้เราไปดาวน์โหลด Docker Toolbox มาก่อนฮะ พอโหลดเสร็จก็ติดตั้งได้เลย แล้วเราก็จะเจอหน้าจอถามว่าเราต้องการจะติดตั้งอะไรบ้าง

สิ่งที่ Docker Toolbox เตรียมมาให้

สิ่งที่ Docker Toolbox เตรียมมาให้

  • Docker Client เป็นตัว Docker จริงๆ
  • Docker Machine ตัวจำลอง Linux เพื่อที่จะสามารถใช้งาน Docker Client ได้
  • Docker Compose tool อำนวยความสะดวกในการรัน container ของ Docker
  • Docker Quickstart Terminal app terminal ที่เอาไว้ใช้รัน Docker
  • Kitematic Docker เวอร์ชั่นที่เป็น GUI
  • Oracle VM VirtualBox VM ที่เอาไว้ใช้ร่วมกับ Docker Machine ในการจำลอง Linux

ผมแนะนำให้เราลงทั้งหมดเลยฮะ สำหรับใครที่เคยลง VirtualBox มาแล้ว ผมขอแนะนำให้ใช้ตัวที่มากับ Docker Toolbox นะครับ เพราะจะเป็นเวอร์ชั่นที่ใหม่กว่า

2. ลองรัน Docker

พอลงเสร็จแล้ว ให้เราเทสการทำงานของ Docker ด้วยการเปิด Docker CLI ขึ้นมาครับ สิ่งที่เกิดขึ้นก็คือมันจะไปรัน Docker Machine เพื่อที่จะสร้าง VM ของ Linux ขึ้นมา ให้เรารอจนมันสร้างเสร็จครับ

3. ดู IP ของ Docker Machine

พอได้ VM มาแล้ว ให้เราดู IP address ของ VM ด้วยคำสั่งนี้ฮะ

docker-machine ls

สมมติว่าเราได้ IP มาเป็น 192.168.99.100 เราก็จะสามารถพรีวิวเว็บที่เราทำผ่าน http://192.168.99.100/ ครับ ให้เราลองเปิด url นี้ดูเลย เราก็จะพบว่ามันยังเข้าไม่ได้ เพราะว่าเรายังไม่ได้ลง web server เลยนั่นเองครับ

4. SSH เข้าไปที่ Docker Machine

ก่อนที่จะติดตั้ง web server เราจะต้องเข้าใจก่อนนะครับว่า ตอนนี้เรายังอยู่บน Mac OS X อยู่เลย แล้วสิ่งที่เราต้องการจะทำก็คือการลง web server ที่ตัว Docker Machine ครับ เพราะฉะนั้นเราจะต้อง SSH เข้าไปที่เครื่อง Docker Machine ก่อน ด้วยคำสั่งนี้ครับ

docker-machine ssh default

เพียงเท่านี้ เราก็จะเข้ามาอยู่ในตัว Docker Machine แล้วล่ะครับ command อะไรที่เราจะรันในนี้ ก็จะมีผลกับตัว VM เท่านั้น ไม่ใช่กับ Mac OS X แล้ว

5. โหลด Docker image

ตอนนี้เราพร้อมที่จะติดตั้ง web server แล้วครับ ให้เราเข้าเว็บ Docker Hub แล้ว search หา Apache ได้เลย แต่ถ้างานของเราจะต้องใช้ PHP อยู่แล้ว เราจะ search หา PHP ไปเลยก็ได้นะครับ เพราะ Docker image ของ PHP นั้น จะมีแบบพ่วง Apache มาให้ด้วย

Docker Hub

ค้นหา PHP จาก Docker Hub

เมื่อเจอ Docker image ที่ต้องการแล้ว เราก็สามารถโหลด Docker image นั้นมาใช้ด้วย command ด้านล่างนี้ครับ

docker run --name myContainer -d -p 9999:9999 image:tag

command อาจจะดูยาวๆ นะครับ มาดูว่าแต่ละ option มันเอาไว้ทำอะไรบ้าง

  • run คือ การสั่งให้ Docker สร้าง container ขึ้นมาจาก Docker image
  • –name myContainer คือ การตั้งชื่อให้กับ container ที่จะถูกสร้าง แนะนำให้ตั้งชื่อให้สื่อความหมายหน่อยนะครับ เพราะเราจะต้องใช้ชื่อนี้ในการ start/stop container ด้วย
  • -d เป็นการสั่งให้รันแบบ background
  • -p 9999:9999 เอาไว้จับคู่ port ของ Docker Machine กับ port ของ container ครับ อย่าง container ของ Apache นี่เค้าจะใช้ port 80 หากเราต้องการจะให้ Docker Machine ใช้ port 80 เหมือนกัน เราก็กำหนดให้เป็น -p 80:80
  • image:tag ชื่อของ Docker image ที่เราต้องการจะโหลดมาใช้ (ดูชื่อได้จากหน้า image ที่ Docker Hub) ตรงนี้เราสามารถเลือกเวอร์ชั่นย่อยของ image ได้ด้วยการระบุ tag ลงไปนะครับ หากไม่ระบุ ก็จะถือว่าเราเลือกเวอร์ชั่นล่าสุด

ว่าแล้วก็ลองรัน command สำหรับโหลด PHP ดูเลยฮะ

docker run --name php5.6 -d -p 80:80 php:5.6-apache

หลังจากรัน command ด้านบน สิ่งที่เกิดขึ้นก็คือ

  • Docker ตรวจสอบว่าเครื่องเรามีการโหลด image ชื่อ php ที่มี tag เป็น 5.6-apache แล้วหรือยัง
  • ถ้ายัง Docker ก็จะไปโหลด image จาก Docker Hub มาให้
  • พอได้ image มาแล้ว Docker ก็จะรัน image นั้นให้กลายเป็น container พร้อมกับตั้งชื่อว่า php5.6 ตามที่เราได้ระบุไว้
  • จับคู่ port 80 ของ Docker Machine เข้ากับ port 80 ของ container

ขั้นตอนนี้อาจจะต้องรอโหลดนานนิดนึงนะครับ เพราะ image จะมีขนาดประมาณ 100 MB พอ Docker รันเสร็จแล้ว ก็ลองเข้า http://192.168.99.100/ ดูอีกทีครับ คราวนี้เราจะเจอข้อความ Forbidden แทน เพราะตอนนี้ยังไม่มีไฟล์อะไรอยู่ใน document root ของ Apache เลยนั่นเองฮะ

6. Map folder

เพื่อให้ Apache ใน container มองเห็นงานที่เราเคยทำไว้ เราจะต้องเชื่อมประตูมิติครับ แต่การจะเชื่อมนั้น เราจะต้องทำอยู่ 2 จุดด้วยกัน

1. ระหว่าง Mac OS X กับ Docker Machine

ในส่วนนี้ ผมขออธิบายเป็นขั้นตอนตามนี้ครับ

  • ปิด Docker CLI แล้วไปที่ VirtualBox
  • ไปที่ VM ชื่อ default แล้วเลือก Settings
  • ไปที่ Shared Folders
  • เลือก Folder Path ให้ตรงกับ folder ที่เก็บงานของเราไว้ เช่น “/Users”
  • ระบุ Folder Name ที่จะให้ mount ไปยัง VM เช่น “Users”
  • ปิดเครื่อง VM นั้น แล้วเข้า Docker CLI ใหม่อีกครั้ง
เข้าไปกำหนด Shared Folders ของ VirtualBox

เข้าไปกำหนด Shared Folders ของ VirtualBox

จากนั้นให้เราเข้าไปที่เครื่อง Docker Machine อีกครั้ง เพื่อดูว่ามันเห็นไฟล์งานของเราแล้วหรือยัง ให้เรารัน command ด้านล่างนี้ ตามลำดับครับ

docker-machine ssh default
cd /Users
ls

ถ้าไม่มีอะไรผิดพลาด เราจะพบว่างานของเราที่เคยอยู่ใน /Users บน Mac OS X มันเข้ามาอยู่ใน Docker Machine แล้วครับ การเชื่อมประตูมิติจุดแรกได้เสร็จลงแล้ว

2. ระหว่าง Docker Machine กับ container

อีกจุดที่เราต้องทำก็คือการทำให้ Apache ใน container มองเห็นงานของเราบน Docker Machine ครับ ให้เราเพิ่ม option ของการ map folder ระหว่าง Docker Machine กับ container เข้าไปใน command แบบนี้

docker run --name php5.6 -d -p 80:80 -v /Users/siamhtml/Sites/:/var/www/html/ php:5.6-apache

command ด้านบน จะเป็นการเชื่อม /Users/siamhtml/Sites/ ซึ่งเป็นที่เก็บงานของผม เข้ากับ document root ของ Apache ครับ หากเรารันแล้ว Error ก็ไม่ต้องตกใจไปนะครับ เพราะสาเหตุของ Error นั้นมีอยู่ไม่กี่อย่างฮะ

2.1 Port ถูกใช้ไปแล้ว

ถ้าเจอ Error แบบนี้ แปลว่ามี container อื่น ใช้ port 80 ไปแล้วครับ ให้เราลิส container ทั้งหมด ที่กำลังรันอยู่มาดูด้วย command

docker ps

เราก็จะเห็นรายการ container ทั้งหมด ที่กำลังรันอยู่ครับ ให้เรามองหาคอลัมน์ PORTS แล้วดูว่า container ไหน ที่มันใช้ port 80 อยู่ เมื่อเจอแล้วก็ดูที่คอลัมน์ NAMES แล้วจำชื่อ container นั้นไว้ครับ เพราะเราจะต้องเอามาใช้ในการ stop มันด้วย สมมติว่า container ที่เอา port 80 ไป คือ container เดิมที่เราเคยรันไว้ก่อนหน้านี้นี่แหละ ให้เรา stop มันด้วย command นี้ครับ

docker stop php5.6

จากนั้นก็ลองรัน command ที่พ่วง option สำหรับ map folder ดูอีกทีฮะ คราวนี้น่าจะผ่านแล้ว

2.2 ชื่อ Container ซ้ำ

แต่ถ้ายังเจอ Error นี้อีก ก็แปลว่ามี container อื่น ใช้ชื่อนี้ไปแล้วฮะ ซึ่งในกรณีนี้ก็คือ container เดิม ที่เราเคยสร้างไว้แต่ยังไม่ได้ map folder นั่นเอง เพื่อความชัวร์ เราสามารถดูรายการ container ทั้งหมดที่มีอยู่ในเครื่องได้ด้วย command นี้ครับ

docker ps -a

รับรองว่าเราจะต้องเจอ container ที่มีชื่อว่า php5.6 อย่างแน่นอนครับ ให้เราลบ container นั้นออกก่อนด้วย command นี้

docker rm php5.6

เสร็จแล้วก็ลองรัน command ที่พ่วง option สำหรับ map folder ดูอีกทีฮะ หากไม่มี Error อะไรแล้ว ให้เราเทสการทำงานด้วยการสร้างไฟล์ชื่อ phpinfo.php ขึ้นมา แล้วใส่โค้ดนี้ลงไป

<?php
    phpinfo();
?>

จากนั้นก็เอาไปใส่ไว้ใน document root ของ Mac OS X ได้เลยครับ แล้วลองเข้าไปที่ http://192.168.99.100/phpinfo.php หากสามารถรันได้ก็หมายความว่าการเชื่อมประตูมิติทั้งสองจุดนั้นได้เสร็จเรียบร้อยแล้วครับ

7. Start Container ครั้งต่อๆ ไป

ทุกๆ ครั้งที่เราสร้าง containerใหม่ขึ้นมาด้วยการรันคำสั่ง docker run พวก option ต่างๆ ที่เราใส่ไว้นั้น มันจะผูกติดกับตัว container เลยนะครับ เวลาจะรันครั้งต่อไป เราก็สามารถใช้ command สั้นๆ แบบนี้ได้เลย

docker start php5.6

จะเห็นว่าเราไม่ต้องมาพิมพ์ command ยาวๆ อีกแล้วนะครับ ลองนึกดูเล่นๆ ว่าถ้าเราโหลด PHP มาหลายๆ เวอร์ชั่น การจะสลับเวอร์ชั่นนั้นก็แค่ stop เวอร์ชั่นที่รันอยู่ก่อน แล้วค่อย start เวอร์ชั่นที่ต้องการจะใช้เท่านั้นเอง

8. สร้าง Docker image เอง

หากเราอยากจะสร้าง Docker image ขึ้นมาใช้เอง ก็ไม่ยากครับ ให้เรา SSH เข้า Docker Machine ก่อน แล้วก็สร้างไฟล์ขึ้นมาอันนึงโดยใช้ชื่อว่า Dockerfile จากนั้นก็ใส่ command ของ Linux ที่ต้องการจะรันลงไปในไฟล์นี้ เมื่อเรียบร้อยแล้วให้เราเข้าไปที่ folder ที่เก็บ Dockerfile อยู่ แล้วรันคำสั่ง build ตามนี้ครับ

docker build -t myImage .

เพียงเท่านี้ เราก็จะได้ Docker image ที่ชื่อ myImage มาใช้งานแล้วล่ะครับ รายละเอียดเพิ่มเติมเพื่อนๆ สามารถเข้าไปดูได้ที่เว็บหลักครับ

เราสามารถดู Dockerfile ของ Image บน Docker Hub ได้ด้วย

เราสามารถดู Dockerfile ของ Image บน Docker Hub ได้ด้วย

9. เอา Docker image ขึ้น Docker Hub

สุดท้ายแล้ว เราสามารถแชร์ Docker image ที่เราสร้างไปให้คนอื่นๆ ใช้ได้ด้วยนะครับ ให้เราเข้าไปสมัครเป็นสมาชิกของ Docker Hub ก่อน จากนั้นก็สร้าง repository ขึ้นมา เมื่อ Docker image พร้อมแล้ว เราก็สามารถรัน command สำหรับ push ของ Docker ได้ทันที เพียงเท่านี้ เพื่อนๆ ทั่วโลกก็จะเห็น Docker image ที่เราสร้างไว้แล้วล่ะครับ รายละเอียดในส่วนนี้ สามารถเข้าไปดูได้ที่เว็บหลักเช่นกันครับ

ความรู้สึกหลังใช้ Docker

ก็น่าจะเห็นกันแล้วนะครับว่า Docker มันช่วยอะไรเราได้บ้าง อย่าลืมนะครับว่าตัวอย่างที่ผมยกมามันเป็นแค่การลง PHP ซึ่งถือว่าไม่ได้มีอะไรมาก ซึ่งในการใช้งานจริงๆ แล้ว เราอาจจะไปโหลด Laravel แบบ container เดียว พร้อมใช้งานได้ทันที มีทั้ง MongoDB และ Redis มาให้พร้อมเลยก็เป็นได้ เราอาจจะเล่นเว็บ แล้วพอดีไปเห็นคนเค้าใช้ Elasticsearch แล้วอยากลองเล่นมั้ง เราก็สามารถให้ Docker ช่วยติดตั้ง Elasticsearch ได้ด้วยเวลาเพียงไม่กี่นาที ก็ลองไปเล่นกันดูนะครับ แล้วเราจะไม่ได้ยินคำว่า “กว่าจะ setup ระบบเสร็จ ก็หมดไปวันนึงแล้ว”

]]>
http://www.siamhtml.com/getting-started-with-docker/feed/ 21
Webpack คืออะไร ? + สอนวิธีใช้ร่วมกับ Reacthttp://www.siamhtml.com/getting-started-with-webpack-and-react/ http://www.siamhtml.com/getting-started-with-webpack-and-react/#comments Sun, 26 Jul 2015 16:08:09 +0000 http://www.siamhtml.com/?p=9135 webpack
บทความนี้ผมจะพาเพื่อนๆ ไปทำความรู้จักกับ Webpack ซึ่งเป็น tool ที่กำลังได้รับความนิยมมากอีกตัวหนึ่งในการทำเว็บสมัยนี้ครับ ถึงแม้ว่าหน้าที่หลักๆ ของ Webpack จะเป็นการรวมโมดูลของ JavaScript ต่างๆ ให้อยู่ในรูปที่สามารถใช้กับ web browser ได้ ซึ่งจะคล้ายๆ กับการใช้ Grunt หรือ Gulp ร่วมกับ Browserify แต่รับรองว่าฟีเจอร์ที่ Webpack ให้มานั้นน่าสนใจสุดๆ จนหลายคนถึงกับเปลี่ยนมาใช้เจ้านี่แทนเลยล่ะครับ เรามาดูกันซิว่า Webpack นี่มันมีดีอะไร ?

Webpack คืออะไร ?

อย่างที่ผมได้เกริ่นไปก่อนหน้านี้แล้วนะครับว่า Webpack นั้นเป็น tool ที่เอาไว้แปลงโมดูลของ JavaScript ให้อยู่ในรูปที่เว็บสามารถนำไปใช้ได้ หรือพูดง่ายๆ ก็คือ มันเอาไว้แปลง JavaScript Module แบบต่างๆ ไม่ว่าจะเป็น CommonJSAMD หรือแม้แต่ 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.js

module.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.js

var 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.js

module.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.js

var 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.js

var 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.js

var 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.js

var 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.js

var 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.js

var 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.js

var 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 ของเราสูงขึ้นมากกว่าเดิมอย่างแน่นอน

]]>
http://www.siamhtml.com/getting-started-with-webpack-and-react/feed/ 4
Flux คืออะไร ? + สอนวิธีนำไปใช้กับ Reacthttp://www.siamhtml.com/getting-started-with-flux/ http://www.siamhtml.com/getting-started-with-flux/#comments Sat, 20 Jun 2015 09:13:57 +0000 http://www.siamhtml.com/?p=9118 วันนี้ SiamHTML จะขอพูดถึงแนวทางการเขียน JavaScript แบบใหม่ที่มีชื่อเรียกว่า Flux ครับ แต่อย่าเพิ่งตกใจนะครับว่า Flux นั้นเป็น JavaScript framework ตัวใหม่ที่เราจะต้องมาเรียนรู้กันอีก เพราะจริงๆ แล้วมันก็เป็นแค่ “pattern” หรือ “architecture” ของการเขียนโค้ดเท่านั้นเอง เรียกว่าคล้ายๆ กับการที่เราเขียนโค้ดโดยยึดหลัก MVC นั่นแหละครับ เรามาดูกันว่า Flux นั้นจะช่วยให้การเขียนโค้ดของเราดีขึ้นได้มากแค่ไหน ?

ปกติเราเขียน React กันอย่างไร ?

จากบทความก่อนหน้านี้ ที่ SiamHTML ได้เล่าถึงการทำเว็บแบบ Isomorphic ด้วย React นั้น หากสังเกตดีๆ จะเห็นว่ามันเป็นแค่แอปแบบง่ายๆ เท่านั้นเอง ถูกมั้ยครับ ? เรียกว่าแทบจะไม่มี logic อะไรเลย ทีนี้ถ้าเราจะทำแอปกันจริงๆ มี logic ซับซ้อนยิ่งขึ้น มีการดึงข้อมูลมาจากฐานข้อมูล ถามว่าโค้ด logic พวกนี้ เราควรจะเขียนไว้ที่ไหน ? ภายใน component งั้นหรอ ? ไม่ดีแน่ๆ ครับ

รู้จักกับ Flux

ลองนึกเล่นๆ นะครับว่า ถ้าเราเอา application logic ไปกองไว้ตาม component ต่างๆ เราจะลำบากแค่ไหน debug ก็ยาก เขียน test ก็ยาก แถมยัง reuse ไม่ได้อีก เพื่อเป็นการแก้ปัญหาเหล่านี้ วิศวกรของ facebook เค้าจึงคิดค้น Flux ขึ้นมาครับ

อย่างที่บอกนะครับว่า Flux นั้นเป็นแค่ pattern ของการเขียนโค้ดเท่านั้นเอง ซึ่งหมายความว่าตัวมันเองไม่ได้มีไฟล์อะไรเลย มันเป็นแค่ “แนวคิด” ที่ทีม facebook เค้าใช้ในการเขียน React เพื่อช่วยให้การทำงานเป็นไปอย่างราบรื่นเท่านั้นเอง

ผมขอทวนก่อนนะครับว่า React นั้นมี data flow แบบ Unidirectional ซึ่งก็คือระบบที่ข้อมูลจะไหลไปในทิศทางเดียว Flux นั้นก็เหมือนกันครับ เพียงแต่มันจะมีการกำหนดการ flow ของข้อมูลให้มีระบบมากขึ้น เราลองมาดูกันคร่าวๆ นะครับว่า data flow ของ Flux นั้นมีหน้าตาเป็นอย่างไร

flux simple diagram

เห็นมั้ยครับว่าการ flow ของข้อมูลนั้นจะเป็นแบบทางเดียว คือจะเริ่มจากการมี Action ขึ้นมาก่อน จากนั้นก็จะส่งข้อมูลต่อไปยัง Dispatcher และ Store ตามลำดับ และสุดท้ายข้อมูลก็จะถูกส่งไปถึง View ครับ

แต่ในความเป็นจริงแล้ว เว็บแอปส่วนใหญ่มักจะมี interaction กับ user ถูกมั้ยครับ ดังนั้น ตัว View เอง ก็สามารถทำให้เกิด Action ขึ้นมาได้เช่นกันครับ data flow จริงๆ จึงจะเปลี่ยนเป็นแบบนี้

flux diagram

ก่อนจะไปต่อ ผมขอแนะนำให้จำ diagram นี้ให้ขึ้นใจเลยนะครับ แต่ตอนนี้เราอาจจะยังงงๆ อยู่ว่า แล้วแต่ละตัวมันคืออะไร ? เดี๋ยวผมจะไล่อธิบายให้ฟังทีละตัวเลยครับ

เจาะลึก Data Flow ของ Flux

เอาล่ะครับ เรามาดูกันว่า data flow ของ Flux นั้นประกอบไปด้วยอะไรบ้าง

Action

Action ก็คือสิ่งที่เกิดขึ้นเมื่อมี user มาทำอะไรบางอย่างกับ View ครับ (หรือเราจะสั่งให้เกิด Action ขึ้นมาเองเลยก็ได้) สมมติหน้าเว็บเรามีกล่องค้นหาข้อมูลอยู่อันหนึ่ง เมื่อใดก็ตามที่ user กดปุ่ม submit ฟอร์มค้นหา เมื่อนั้นจะมี Action เกิดขึ้นมาครับ แน่นอนว่าข้อมูลที่เราอยากจะผูกไปพร้อมกับ Action นี้ก็คือ keyword ที่ user เค้ากรอกมา ถูกมั้ยครับ คำถามคือ แล้วเราจะส่ง keyword ที่ว่านี้ ไปยังโค้ดส่วนที่จะทำหน้าที่ query ข้อมูลจากฐานข้อมูลได้อย่างไร ?

Dispatcher

วิธีที่ Flux ใช้ก็คือการนำสิ่งที่เรียกว่า Dispatcher มาเป็นตัวกลางครับ โดย Action ดังกล่าวจะต้องไปบอก Dispatcher ว่า 1) ตัวเองคือ Action ประเภทไหน และ 2) มีข้อมูลอะไรที่จะพ่วงมากับ Action นี้บ้าง จากนั้น Dispatcher จะรับหน้าที่ดูว่า Action ดังกล่าว ควรจะวิ่งไปทางไหนต่อครับ

Store

Store คือ ที่ๆ เราจะเก็บข้อมูลต่างๆ ของแอป รวมไปถึง state เอาไว้ครับ นอกจากข้อมูลต่างๆ แล้ว ภายใน Store ก็จะมีการเก็บ method ต่างๆ ที่จะใช้ในการจัดการกับข้อมูลภายใน Store เอาไว้ด้วย หรือพูดง่ายๆ ก็คือมันเป็นที่ๆ เราจะเขียน application logic ต่างๆ นั่นเองครับ อย่างในกรณีนี้ Store จะต้องรับหน้าที่นำ keyword ที่ user กรอกมา ไป query ข้อมูลในฐานข้อมูล แล้วนำผลลัพธ์ที่ได้ ส่งต่อไปยัง View เพื่อแสดงผล แต่แอปเราจะรู้ได้อย่างไรว่า method ไหนใน Store ที่จะต้องจับคู่กับ Action ที่เข้ามาหา ?

คำตอบคือเราจะต้องนำ method ต่างๆ ใน Store ไป register ไว้กับ Dispatcher ก่อนครับ พูดง่ายๆ ก็คือ เราจะต้องจับคู่นั่นเองว่า ถ้ามี Action แบบนี้เข้ามา จะให้วิ่งไปใช้ method ตัวไหน สมมติว่ามี Action ที่ต้องการจะ query ข้อมูลจากฐานข้อมูลเข้ามา เราก็จะต้องจับคู่ Action นี้ เข้ากับ method ที่จะทำหน้าที่เชื่อมต่อกับฐานข้อมูลอะไรทำนองนี้ครับ เมื่อได้ข้อมูลที่ต้องการมาแล้ว Store ก็จะอัพเดทข้อมูลต่างๆ รวมไปถึง state ที่ตัวเองดูแลอยู่ แล้วก็แจ้งกลับไปยัง View เพื่อร้องขอให้มีการ render ใหม่

View

เมื่อ View ได้รับคำร้องขอจาก Store แล้ว View ก็จะไปดึงข้อมูลที่จะต้องใช้ในการ render ใหม่ มาจาก Store จากนั้น View ก็จะ render ตัวเอง และ component ลูกๆ ที่อยู่ภายใน View นั้นๆ ครับ

Workshop – ทำแอป Discussion

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

สมมติว่าโจทย์ของเราคือการสร้างระบบ Discussion แบบง่ายๆ คือเราจะมีฟอร์มอันนึง ให้คนเข้ามา comment อะไรก็ได้ ลองดูซิว่า requirement แบบนี้ เราจะต้องเขียนโค้ดอย่างไร ?

1. เตรียมไฟล์และจัดโครงสร้าง

เริ่มด้วยการสร้างไฟล์และโฟลเดอร์ตามด้านล่างนี้ก่อนเลยครับ

myApp/
|
|-- js/                        
|   |
|   |-- actions/         # เก็บ Action ต่างๆ             
|   |-- components/      # เก็บ component ต่างๆ (.jsx)             
|   |-- constants/       # เก็บชื่อประเภทของ Action ต่างๆ               
|   |-- dispatcher/      # เก็บ Dispatcher
|   |-- stores/          # เก็บ Store ต่างๆ
|   |
|   |-- app.js           # ไฟล์สำหรับ render component ของ React
|   `-- bundle.js        # ไฟล์รวม script ที่ได้จากการรัน browserify        
|
|-- gulpfile.js          # ไฟล์สำหรับทำ automated task ด้วย Gulp
|
`-- index.html           # ไฟล์ html ของหน้าเว็บ

ก็ลองดูคร่าวๆ กันก่อนนะครับว่าแต่ละไฟล์แต่ละโฟลเดอร์นั้นมีหน้าที่เก็บอะไร จากนั้นก็ให้เราไล่ใส่โค้ดตามด้านล่างนี้ต่อได้เลยครับ

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Flux Tutorial by SiamHTML</title>
</head>
<body>
    <div id="app"></div>
    <script src="js/bundle.js"></script>
</body>
</html>

สังเกตนะครับว่าใน body ของเราจะต้องมี #app อยู่ด้วย เพราะเราจะต้องหาที่เอาไว้ render แอปของเราครับ ส่วนก่อนปิด body เราก็จะต้องใส่ script เข้ามาตัวนึง ซึ่งมันก็คือไฟล์ที่ได้จากการรวม script ทั้งหมดของแอปเรานั่นเองครับ

gulpfile.js

'use strict';
 
var gulp = require('gulp');
var streamify = require('gulp-streamify')
var uglify = require('gulp-uglify');
var browserify = require('browserify');
var reactify = require('reactify');                 
var source = require('vinyl-source-stream');
 
gulp.task('js', function() {
    browserify('./js/app.js')         // ให้ app.js เป็น entry point
        .transform(reactify)                
        .bundle()                           
        .pipe(source('bundle.js'))    // ตั้งชื่อไฟล์ output เป็น bundle.js     
        .pipe(streamify(uglify()))          
        .pipe(gulp.dest('./js/')); 
});
 
gulp.task('default', ['js'], function() {
    gulp.watch(['./js/**/*', '!./js/bundle.js'], ['js']);
});

ในบทความนี้ ผมจะขอใช้ browserify ในการทำ bundle นะครับ ให้เราเลือก entry point เป็น app.js ได้เลย แล้วตั้งชื่อไฟล์ output ให้เป็น bundle.js ครับ

2. สร้าง Component ต่างๆ แบบ Static

ทีนี้เราจะมาเริ่มสร้าง component กันเลยครับ โดยผมจะขอเริ่มจาก component ที่อยู่นอกสุดก่อนละกัน

DiscussionApp.jsx

ให้เราสร้างไฟล์ใหม่ขึ้นมาอันนึง แล้วตั้งชื่อว่า DiscussionApp.jsx ครับ จากนั้นก็ใส่โค้ดเริ่มต้นตามด้านล่างนี้ลงไป

var React = require('react');

// โหลด component ลูก มาใช้
var DiscussionForm = require('./DiscussionForm.jsx');
var DiscussionList = require('./DiscussionList.jsx');

// สร้าง component แม่
var DiscussionApp = React.createClass({
  	
    render: function() {
        return (
            <div>
                <DiscussionForm />
                <DiscussionList />
            </div>
        );
    }
});

module.exports = DiscussionApp;

ผมมองว่าแอป Discussion นี้ ประกอบไปด้วย 2 ส่วนหลักๆ ก็คือ 1) ส่วนของฟอร์มที่จะเอาไว้พิมพ์ comment และ 2) ส่วนที่จะเอาไว้แสดง comment ผมเลยจะขอแยก 2 ส่วนนี้ ออกมาเป็น component ลูก ของ DiscussionApp ครับ

DiscussionForm.jsx

ทีนี้มาดูที่ component ลูก อย่าง DiscussionForm กันบ้างครับ ให้เราใส่โค้ดสำหรับ render ฟอร์มธรรมดาๆ ตามด้านล่างนี้ลงไป

var React = require('react');

var DiscussionForm = React.createClass({
    
    // render ออกมาเป็น form แบบง่ายๆ
    render: function() {
        return (
            <form>
                <input type="text" 
                    placeholder="Enter message here..." 
                />
                <button>Comment</button>
            </form>
        );
    }
});

module.exports = DiscussionForm;

DiscussionList.jsx

มาต่อกันที่ component ลูกอีกตัวอย่าง DiscussionList ที่เราจะเอาไว้แสดง comment ครับ ให้เราใส่โค้ดตามนี้ไปได้เลย

var React = require('react');

// สร้าง component ที่จะใช้แสดงตัว comment
var DiscussionComment = React.createClass({
    render: function() {
        
        // รับข้อมูล comment ที่จะแสดงผ่านทาง props
        var comment = this.props.comment;
        return (
            <li>{comment.title}</li>
        );
    }
});

// สร้าง component ที่จะเอาไว้ทำ iteration
var DiscussionList = React.createClass({
    render: function() {
        
        // วนลูป array ของ comments ที่ได้มาจาก props 
        // แล้วส่งต่อให้ DiscussionComment นำไปแสดงผล
        var DiscussionComments = this.props.comments.map(function(data, index) {
            return (
                <DiscussionComment key={data.ID} comment={data} />
            );
        });
        return (
            <ul>
                {DiscussionComments}
            </ul>
        );
    }
});

module.exports = DiscussionList;

จากโค้ดจะเห็นว่า DiscussionList จะรับ props ที่ชื่อ comments มาวนลูปเพื่อแสดงข้อมูล comment โดยใช้ component ลูกอีกตัว ที่ชื่อ DiscussionComment ครับ คือขอแค่ส่ง props ที่ชื่อ comments มาเหอะ แล้ว DiscussionList จะแสดงข้อมูลออกมาให้เอง

app.js

สุดท้ายเรามาดูที่ไฟล์ entry point ของเรา อย่าง app.js กันบ้างครับ ให้เราใส่โค้ดสำหรับ render DiscussionApp เข้าไปใน #app แบบนี้

var React = require('react');
var ReactDOM = require('react-dom');

// โหลด component หลักมา
var DiscussionApp = require('./components/DiscussionApp.jsx');

// แสดงผล component หลัก ที่ #app
ReactDOM.render(<DiscussionApp /> , document.getElementById('app'));

มาถึงตรงนี้ โครงของแอปเราเริ่มจะโอเคแล้วล่ะครับ ที่เหลือก็แค่ทำฟอร์มให้สามารถรับข้อมูล comment ได้จริง แล้วก็นำข้อมูลมา assign ให้ DiscussionList เท่านั้นเอง

3. ใส่ Interaction ด้วย State

ทีนี้เราจะมาทำแอปของเราให้ใช้งานได้จริงครับ จาก requirement เราจะเห็นว่าข้อมูลที่จะ flow ไปตามส่วนต่างๆ ของแอปนี้ก็คือ comment ถูกมั้ยครับ ? ซึ่ง comment เหล่านี้จะถูกสร้างขึ้นมาจาก DiscussionForm แล้วจึงถูกนำไปแสดงที่ DiscussionList อีกที หากเป็นแบบนี้ เราจะต้องเก็บ comment เอาไว้ใน state ของ component ตัวแม่ ซึ่งก็คือ DiscussionApp ครับ เพราะมันจะทำให้ DiscussionList สามารถนำ comment ที่สร้างโดย DiscussionForm ไปใช้ได้ด้วย เพื่อให้เห็นภาพมากขึ้นเรามาเริ่มเขียนโค้ดกันเลยดีกว่าครับ

DiscussionApp.jsx

เริ่มกันที่ component ตัวแม่ก่อนเลยครับ ให้เราเพิ่ม method getInitialState() และ _addComment() เข้าไปแบบนี้

var DiscussionApp = React.createClass({
   
    // กำหนด allMessages ให้เป็น state ที่มีค่าเริ่มต้นเป็น array เปล่าๆ
    getInitialState: function() {
        return {
            allMessages: []
        };
    },

    // สร้าง method สำหรับเซฟ comment
    _addComment: function(message) {
        
        // สร้าง unique id ขึ้นมา
        var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
        
        // เตรียม object ของ comment ที่จะเซฟ
        var newMessage = [{
            'ID': id,
            'title': message
        }];

        // รวม comment ใหม่ เข้ากับ comment ที่มีอยู่เดิม
        this.setState({
            allMessages: newMessage.concat(this.state.allMessages)
        });
    },
    
    // ส่ง method สำหรับเซฟ comment ไปให้ DiscussionForm ผ่าน props
    // assign ข้อมูล comment ทั้งหมด ให้ DiscussionList นำไปแสดงผล
    render: function() {
        return (
            <div>
                <DiscussionForm handleSubmit={this._addComment}  />
                <DiscussionList comments={this.state.allMessages} />
            </div>
        );
    }
});

ไฮไลท์นั้นจะอยู่ที่การส่ง method _addComment() ไปให้ DiscussionForm ใช้ ผ่าน props ที่ชื่อ handleSubmit ครับ ที่เราต้องใช้ท่านี้ในการเซฟ comment ก็เพราะว่าหากเราให้ DiscussionForm เป็นคนเซฟ comment จากการ submit ลง state ซะเอง เราจะไม่สามารถนำ state นั้น ออกมาใช้ข้างนอก DiscussionForm เพื่อส่งต่อไปยัง DiscussionList ได้นั่นเอง

DiscussionForm.jsx

เอาล่ะครับ เมื่อได้รับ method _addComment() มาจาก component แม่แล้ว มาดูว่าเราจะเอาไปใช้ใน DiscussionForm อย่างไร ?

var DiscussionForm = React.createClass({
 
    // กำหนด message ให้เป็น state ที่มีค่าเริ่มต้นเป็นค่าว่าง
    getInitialState: function() {
        return {
            message: ''
        };
    },

    // เมื่อ user พิมพ์ comment ให้อัพเดท message 
    // ให้เป็นค่าเดียวกับที่ user พิมพ์มา
    _onChange: function(event) {
        this.setState({
            message: event.target.value
        });
    },

    // เมื่อฟอร์มถูก submit ให้เซฟค่า message ที่อยู่ใน state 
    // โดยใช้ method ที่ได้รับมาจาก props ที่ชื่อ handleSubmit
    _onSubmit: function(event) {
        event.preventDefault();
        this.props.handleSubmit(this.state.message);

        // จากนั้นก็ reset ค่า message ให้เป็นค่าว่างเหมือนเดิม
        this.setState({
            message: ''
        });
    },
 
    // ผูก event ต่างๆ เข้ากับ element
    // พร้อมกับกำหนด value ของช่องกรอก comment ให้มีค่าตาม message
    render: function() {
        return (
            <form onSubmit={this._onSubmit}>
                <input type="text" 
                    placeholder="Enter message here..." 
                    onChange={this._onChange}
                    value={this.state.message}
                />
                <button onClick={this._onSubmit}>Comment</button>
            </form>
        );
    }
});

เมื่อไล่โค้ดดู ก็จะเห็นว่าเราเรียกใช้ method _addComment() ผ่าน props ที่ชื่อ handleSubmit ครับ เพียงเท่านี้ เราก็จะสามารถเซฟ message ที่เกิดขึ้นภายใน DiscussionForm ไปเก็บไว้เป็น state ของ DiscussionApp ได้แล้วล่ะครับ

มาถึงตอนนี้ก็ลองรันดูเลยครับ ให้เราลองพิมพ์ comment อะไรก็ได้ แล้วลอง submit ดู เราก็จะเห็นว่าแอปของเรานั้นทำงานได้แล้ว แต่ช้าก่อน… นี่ยังไม่ใช่วิธีที่ถูกต้อง !

ถึงแม้ว่าแอปของเราจะใช้งานได้แล้วก็จริง แต่การกอง application logic รวมกันไว้ใน component นั้นไม่ดีแน่นอนครับ ไม่ว่าจะเป็นเรื่องของการ debug โค้ด การ reuse โค้ด รวมไปถึงการ test โค้ด แล้วที่สำคัญก็คือ สิ่งที่เก็บไว้ใน state นั้น ควรจะเป็นแค่ข้อมูลเล็กๆ ที่จะเอาไว้บรรยายถึง “สภาพ” ของ component ในขณะนั้นว่าเป็นอย่างไร ไม่ใช่เอา state ไปเก็บข้อมูลที่จะนำมาแสดงผลแบบที่เราทำอยู่ในตอนนี้

4. เปลี่ยนมาใช้ Flux

เพื่อเป็นการแก้ปัญหาดังกล่าว เราจะลองเปลี่ยนมาเขียนตามแนวคิดของ Flux ดูบ้างครับ

4.1 สร้าง Dispatcher ขึ้นมาก่อน

สำหรับ data flow แบบ Flux แล้ว Dispatcher นั้นถือเป็นศูนย์กลางของข้อมูลเลยนะครับ เพราะมันจะต้องคอยดูว่าถ้ามี Action เข้ามาแบบนี้ จะต้องส่งต่อไปยัง method ไหนใน Store สำหรับโค้ดในส่วนของ Dispatcher นั้น เราไม่ต้องไปเขียนเองนะครับ เพราะ Flux เค้าได้เตรียมมาให้เรียบร้อยแล้ว สิ่งที่เราต้องทำก็แค่ติดตั้ง Flux ผ่าน npm แบบนี้ครับ

npm install flux --save

จากนั้นก็ให้เราสร้างไฟล์ชื่อ AppDispatcher.js ขึ้นมา แล้วใส่โค้ดเรียกใช้ Dispatcher จาก Flux ลงไป

// โหลด Dispatcher จาก Flux มาใช้งาน
var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();

เพียงเท่านี้ เราก็จะได้ความสามารถของ Dispatcher จาก Flux มาใช้แล้วล่ะครับ ให้เราเอาไฟล์นี้ไปวางไว้ในโฟลเดอร์ dispatcher ได้เลย

4.2 ดูว่ามี Action ประเภทไหนบ้าง ?

จากนั้นให้เราลิสต์มาให้หมดครับว่า แอปของเรามีประเภทของ Action หรือ actionType อะไรบ้าง จากตัวอย่างก่อนหน้านี้ เราจะมีเพียงแค่ Action เดียว ซึ่งก็คือ Action ที่เอาไว้เซฟ comment ถูกมั้ยครับ ให้เราสร้างไฟล์ DiscussionConstants.js ขึ้นมา แล้วใส่โค้ดด้านล่างนี้ลงไป

var keyMirror = require('keymirror');

module.exports = keyMirror({
    DISCUSSION_CREATE: null

    // หากอนาคตจะเพิ่มฟีเจอร์ให้สามารถแก้ไขและลบ comment ได้ด้วย
    // ก็ให้เพิ่ม actionType ตรงนี้ได้เลย
    // DISCUSSION_EDIT: null,
    // DISCUSSION_DELETE: null
});

โค้ดด้านบนจะเป็นการบอกว่าแอปนี้ มี actionType ที่ชื่อ DISCUSSION_CREATE เพียงแค่อันเดียวครับ หากเราทำแอปที่ซับซ้อนกว่านี้ ก็ให้เราเพิ่ม actionType ต่อท้ายเข้าไปได้เลย ส่วนสาเหตุที่เราต้องเก็บประเภทของ Action เอาไว้เป็น constant แบบนี้ ก็เพราะว่าเราจะใช้มันในการจับคู่ Action เข้ากับ method ของ Store นั่นเองครับ

4.3 สร้าง Action ขึ้นมา

จากนั้นเราจะมาสร้างตัว Action จริงๆ กันครับ โดยเราจะอาศัย Dispatcher ในการส่ง parameter ที่จำเป็นเข้าไปให้ Store และอาศัย actionType เพื่อเอาไว้บอก Dispatcher ว่า Action นี้ ควรจะจับคู่กับ Store ตัวไหน

// โหลด Dispatcher มาใช้
var AppDispatcher = require('../dispatcher/AppDispatcher');

// โหลด actionType ต่างๆ มาใช้
var DiscussionConstants = require('../constants/DiscussionConstants');

// รวม Action ต่างๆ ในแอป
var DiscussionActions = {

    // เมื่อเกิด Action นี้ให้ทำอะไร ?
    addComment: function(comment) {
        
        // บอกว่า Action นี้ เป็นแบบ DISCUSSION_CREATE นะ
        // พร้อมกับส่งค่า comment พ่วงไปด้วย
        AppDispatcher.dispatch({
            actionType: DiscussionConstants.DISCUSSION_CREATE,
            comment: comment,
        });
    }

};

module.exports = DiscussionActions;

4.4 ย้าย Logic มาไว้ใน Store + ผูก Method ของ Store เข้ากับ Action

ทีนี้มาดูที่ Store กันบ้างครับ ให้เราย้าย application logic มาเขียนเป็น method ต่างๆ ของ Store แทน จากนั้นก็ใส่โค้ดสำหรับจับคู่ method เหล่านั้นเข้ากับ actionType ประเภทต่างๆ ครับ

var AppDispatcher = require('../dispatcher/AppDispatcher');
var DiscussionConstants = require('../constants/DiscussionConstants');

// setup Store (copy ได้เลย เพราะมันเป็น pattern)
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var CHANGE_EVENT = 'change';

// กำหนดค่าเริ่มต้นให้กับ state และข้อมูล comment
var _state = {};
var _comments = [];

// private method ของ Store สำหรับเซฟ comment 
// (ย้ายมาจาก DiscussionApp)
function addData(comment) {
    var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
    var newMessage = [{
        'ID': id,
        'title': comment
    }];
    _comments = newMessage.concat(_comments);
}

// public method ต่างๆ ของ Store
var DiscussionStore = assign({}, EventEmitter.prototype, {

    // ดึงข้อมูล state ในตอนนั้นๆ
    getState: function() {
        return _state;
    },

    // ดึง comment ทั้งหมดในตอนนั้นๆ
    getAll: function() {
        return _comments;
    },

    // 3 methods ด้านล่างนี้เป็น pattern
    // ใช้สำหรับแจ้ง component หลัก ว่าถึงเวลา render ใหม่แล้ว
    // เนื่องจากข้อมูลมีการอย่างเปลี่ยนแปลง
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }

});


// จับคู่ method ของ Store เข้ากับ actionType ต่างๆ
AppDispatcher.register(function(action) {

    // เมื่อ Dispatcher ได้รับ actionType แบบนี้ ให้ใช้ method ตัวไหน
    switch (action.actionType) {
        case DiscussionConstants.DISCUSSION_CREATE:
            addData(action.comment);
            break;
        default:
            // no op
    }
    
    // พอทำ callback เสร็จแล้ว ก็แจ้งกลับไปยัง component หลัก 
    // ว่าเกิดการเปลี่ยนแปลงแล้วนะ
    DiscussionStore.emitChange();
});

module.exports = DiscussionStore;

4.5 ลบ Logic เดิมออก แล้วเปลี่ยนมาใช้ Action แทน

จากนั้นกลับมาดูที่ DiscussionForm ในส่วนของ method _onSubmit() นิดนึงครับ ให้เราเปลี่ยนจากการเซฟ comment ด้วย this.props.handleSubmit() มาเป็นการใช้ Action ที่เราสร้างขึ้นมาแทน

// โหลด Action ที่สร้างไว้มาใช้
var DiscussionActions = require('../actions/DiscussionActions');

var DiscussionForm = React.createClass({
 
    ...

    _onSubmit: function(event) {
        event.preventDefault();
        
        // เปลี่ยนมาใช้ Action แทนการใช้ method จาก component แม่
        DiscussionActions.addComment(this.state.message);
        
        this.setState({
            message: ''
        });
    },
 
    ...

});

4.6 เชื่อม Component หลัก เข้ากับ Store

สุดท้ายแล้ว เราจะต้องเปิดการเชื่อมต่อระหว่าง DiscussionApp กับ Store ครับ ให้เราแก้ไฟล์ DiscussionApp.js ตามโค้ดด้านล่างนี้ได้เลย

// โหลด Store มาใช้
var DiscussionStore = require('../stores/DiscussionStore');

// โหลด Action มาใช้
var DiscussionActions = require('../actions/DiscussionActions');

var DiscussionApp = React.createClass({
    
    // 4 methods ด้านล่างนี้เป็น pattern
    getInitialState: function() {
        // ดึง state ผ่านทาง Store แทน
        return DiscussionStore.getState();
    },
    componentDidMount: function() {
        // รอฟังว่า Store จะแจ้งว่ามีการเปลี่ยนแปลงเมื่อไร
        DiscussionStore.addChangeListener(this._onChange);
    },
    componentWillUnmount: function() {
        // พอ component จะถูกถอดออก ก็เลิกรอ
        DiscussionStore.removeChangeListener(this._onChange);
    },
    _onChange: function() {
        // เมื่อ Store แจ้งว่ามีการเปลี่ยนแปลง ก็สั่งให้อัพเดท state ซะ
        // โค้ดตรงนี้แหละ ที่ทำให้ component ถูก render ใหม่
        this.setState(DiscussionStore.getState());
    },

    render: function() {
        
        // ดึง comment จาก Store แทนการดึงจาก state
        var comments = DiscussionStore.getAll();
        return (
            <div>
                <DiscussionForm />
                <DiscussionList comments={comments} />
            </div>
        );
    }
});

จากนั้นก็ลองรันดูอีกทีเลยครับ จะเห็นว่าแอปเรายังคงสามารถทำงานได้ตามเดิม เพียงแต่โค้ดมันจะเป็นระเบียบมากขึ้นเยอะครับ คือ component ก็จะมีแต่ template อย่างเดียว ไม่มี application logic มาแทรกอยู่ด้วยแล้ว

5. ดึงข้อมูลจาก API

ถึงแม้ว่าแอปเราจะทำงานได้แล้วก็จริง แต่มันก็ยังไม่ค่อยจะสมบูรณ์เท่าไรนะครับ เพราะเวลารีเฟรช พวก comment ต่างๆ ที่เคยพิมพ์ไว้มันจะหายไปหมดเลย เราจะแก้ปัญหานี้ด้วยการนำฐานข้อมูลมาใช้ครับ ทุกครั้งที่มีการ comment เราจะต้องเซฟข้อมูล comment นั้นๆ ลงไปในฐานข้อมูลด้วยเสมอ แล้วตอนที่โหลดแอปมาครั้งแรก ก็ให้เราไปดึง comment ทั้งหมดจากฐานข้อมูลมาแสดง ส่วนวิธีการติดต่อกับฐานข้อมูลนั้น ผมแนะนำให้ทำผ่าน API ครับ

คำถามต่อมาก็คือ แล้วเราจะเอาโค้ดส่วนที่เรียกใช้ API ไปไว้ที่ไหน ? ประเด็นนี้ยังคงเป็นที่ถกเถียงกันอยู่พอสมควรเลยนะครับ แต่ส่วนตัวผมขอแนะนำให้เอาไว้ที่ Action แล้วกัน เพราะรู้สึกว่ามันจะไล่โค้ดได้ง่ายกว่าการเอาไว้ใน Store ครับ เพื่อให้เห็นภาพมากขึ้น เรามาลองเขียนโค้ดต่อยอดจากแอปเดิมกันเลย

5.1  เพิ่ม actionType

เริ่มด้วยการเพิ่ม actionType ให้กับ Action สำหรับการดึงข้อมูลจากฐานข้อมูลมาแสดงก่อนเลยครับ ให้เราเข้าไปเพิ่มได้ที่ DiscussionConstants.js

var keyMirror = require('keymirror');
 
module.exports = keyMirror({
    DISCUSSION_LOADING: null, // สำหรับบอกว่าแอปกำลังโหลดข้อมูลอยู่
    DISCUSSION_CREATE: null,
    DISCUSSION_REQUEST: null  // สำหรับดึงข้อมูลจาก API มาแสดง
});

แต่จะเห็นว่านอกจาก DISCUSSION_REQUEST แล้ว ผมยังเพิ่ม DISCUSSION_LOADING เข้ามาด้วยนะครับ เราจะใช้ actionType นี้แหละ ในการบอกว่าแอปกำลังอยู่ในสถานะโหลดข้อมูลอยู่

5.2 เพิ่ม Action สำหรับดึงข้อมูลจาก API

เมื่อได้ actionType มาแล้ว เรามาสร้างตัว Action จริงๆ กันเลยครับ ให้เราเพิ่ม method getComments() เข้ามา แล้วก็ปรับ method addComment() เล็กน้อย ตามนี้ครับ

// โหลด superagent มาใช้เรียก API
var request = require('superagent');

var DiscussionActions = {

    // เพิ่ม method สำหรับดึง comment จาก API
    // โดยส่งค่าเลขหน้าไปให้
    getComments: function(page) {
        
        // บอก dispatcher ว่าเริ่มโหลดข้อมูลแล้วนะ
        AppDispatcher.dispatch({
            actionType: DiscussionConstants.DISCUSSION_LOADING
        });

        // ใช้ superagent ดึงข้อมูลจาก API
        request
            .get('http://www.myapp.com/api/comment?limit=10&page=' + page)
            .end(function(err, res){
                
                // เมื่อได้ข้อมูลมาก็บอก dispatcher ว่าได้ข้อมูลมาแล้ว 
                // พร้อมกับพ่วงข้อมูล comment เข้าไปด้วย
                AppDispatcher.dispatch({
                    actionType: DiscussionConstants.DISCUSSION_REQUEST,
                    comments: res.body
                });
            });
    },

    addComment: function(comment) {
        AppDispatcher.dispatch({
            actionType: DiscussionConstants.DISCUSSION_CREATE,
            comment: comment,
        });

        // ใช้ superagent เซฟข้อมูล comment ผ่าน API
        request
            .post('http://www.myapp.com/api/comment')
            .send({ comment: comment })
            .end(function(err, res){
                console.log(res);
            });
    }
 
};

ไฮไลท์นั้นอยู่ที่ method getComments() ครับ จะเห็นว่ามาถึงเราจะ dispatch DISCUSSION_LOADING ไปก่อนเลย จากนั้นเราจะเริ่มส่ง ajax request ไปยัง API เพื่อขอข้อมูล comment เมื่อได้รับข้อมูลมาแล้ว เราถึงจะ dispatch DISCUSSION_REQUEST เพื่อส่งข้อมูล comment ที่ได้มาจาก API ไปให้ Store ครับ

ส่วน method addComment() ก็จะคล้ายๆ กันครับ เพียงแต่ตอนส่ง ajax request นั้น เราไม่จำเป็นต้องใส่ callback function อะไรเลยก็ได้ เพราะเราจะใช้วิธี optimistic อยู่แล้ว (คือจะแสดงข้อมูลออกมาทางหน้าจอทันที โดยที่ไม่สนว่าข้อมูลนั้นจะถูกเซฟลงฐานข้อมูลเสร็จแล้วหรือยัง)

5.3 สร้าง Method แล้วจับคู่เข้ากับ actionType

จากนั้นเรามาเพิ่ม method ที่ Store กันครับว่าจะให้ทำอะไรกับข้อมูลที่ได้มา

...

// เพิ่ม loading และ page เริ่มต้นเข้าไปใน state
var _state = {
    loading: false,
    page: 1
};

...

// private method สำหรับเพิ่ม comment ใหม่เข้าไปต่อท้าย comment เดิม
// พร้อมกับอัพเดท page ให้มีค่าเป็นเลขหน้าถัดไป
function parseData(comments) {
    _comments = _comments.concat(comments);
    _state.page = _state.page + 1;
}

AppDispatcher.register(function(action) {
    
    // เพิ่มการจับคู่ actionType ใหม่ เข้ากับ method ของ Store
    switch (action.actionType) {
        
        // เปลี่ยน loading ให้เป็น true
        case DiscussionConstants.DISCUSSION_LOADING:
            _state.loading = true;
            break;

        // เรียกใช้ parseData() พร้อมกับเปลี่ยน loading ให้เป็น false เหมือนเดิม
        case DiscussionConstants.DISCUSSION_REQUEST:
            parseData(action.comments);
            _state.loading = false;
            break;
       
        ...
    }
    DiscussionStore.emitChange();
});

ก่อนอื่นให้เราเปลี่ยนค่าเริ่มต้นของ _state ให้มีการเก็บสถานะ loading ก่อน เราจะได้รู้ว่าตอนนี้แอปกำลังโหลดข้อมูลอยู่หรือเปล่า ส่วนอีกค่านึงที่ต้องเก็บลง _state ก็คือ page ซึ่งเราจะเอาไว้บอกแอปว่าจะต้องไปดึง comment หน้าที่เท่าไรมา

เสร็จแล้วเราจะต้องเพิ่ม private method ที่ชื่อ parseData() เข้ามาครับ เราจะใช้ method นี้ล่ะ ในการรวมร่าง comment ที่โหลดมาก่อนหน้าเข้ากับ comment ที่เพิ่งโหลดมาใหม่ รวมไปถึงการอัพเดทค่า page ที่อยู่ใน _state ด้วย เพื่อเป็นการบอกแอปให้คราวหน้าไปดึงข้อมูลของหน้าถัดไปนั่นเองครับ

เมื่อ method พร้อมแล้ว ทีนี้เราต้องมาจับคู่ให้มันครับ ให้เราจับคู่ DISCUSSION_REQUEST เข้ากับ parseData() ได้เลย จะเห็นว่าผมต้องไปกำหนด loading ให้กลับมาเป็น false เหมือนเดิมด้วยนะครับ เพราะ DISCUSSION_LOADING ที่ถูก dispatch มาก่อนจะเซท loading เป็น true ไว้

5.4 เรียกใช้ Action ที่ View

เนื่องจากผมอยากให้มาถึงก็โหลด comment ล่าสุดมาจากฐานข้อมูลเลย ผมเลยจะขอให้มันไปเรียก getComments() ในตอนที่ componentDidMount เลยละกัน จากนั้นผมก็จะขอเพิ่ม button เข้ามาอันนึง เพื่อเอาไว้กดดู comment เพิ่มเติม เวลามี comment เยอะๆ ครับ จะเห็นว่าปุ่มนี้มันก็ไปเรียก getComments() เหมือนตอนที่ componentDidMount นั่นแหละ เพียงแต่ comment ที่ได้มานั้นจะเป็นของหน้าถัดไปแล้ว เพราะเราจะมีการอัพเดท page ใหม่ทุกครั้งที่โหลด comment เข้ามานั่นเองครับ

var DiscussionApp = React.createClass({
    
    ...
   
    // มาถึงก็ให้ดึง comment จาก API มาแสดงเลย
    componentDidMount: function() {
        
        ...

        DiscussionActions.getComments(this.state.page);
    },
   
    // เพิ่ม method สำหรับดึง comment เพิ่มเติม
    _onViewMoreComments: function() {
        DiscussionActions.getComments(this.state.page);
    },

    ...

    // เมื่ออยู่ในสถานะ loading ให้แสดงข้อความ Loading... ด้วย
    // เพิ่มปุ่มสำหรับโหลด comment เพิ่มเติม
    render: function() {
        var comments = DiscussionStore.getAll();
        return (
            <div>
                <DiscussionForm />
                <DiscussionList comments={comments} />
                {this.state.loading ? <p>Loading...</p> : null}
                <button onClick={this._onViewMoreComments}>View more comments</button>
            </div>
        );
    }
});

สังเกตนิดนึงนะครับว่าผมจะมีการเช็คค่า loading ที่อยู่ใน state ด้วย เพราะถ้าแอปกำลังโหลดข้อมูลอยู่ ผมอยากจะให้มันแสดง feedback อะไรบางอย่างให้ user ได้รับรู้

จากนั้นก็รันดูเลยครับ ให้เราลอง comment ดูเยอะๆ แล้วลองเทสปุ่มโหลด comment เพิ่มเติมดู comment จะต้องแสดงต่อเนื่องกันไปอย่างถูกต้องครับ แล้วที่สำคัญก็คือ เมื่อรีเฟรชแล้ว comment จะต้องไม่หายไปหมดเหมือนในตอนแรก เสร็จแล้วครับ! แอป Discussion ของเรา

ความรู้สึกหลังใช้ Flux

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

การเขียนแอปโดยใช้ Flux นั้น ทำให้โค้ดมีระเบียบ แยกออกจากกันอย่างชัดเจนมากขึ้นครับ ประโยชน์ที่เห็นได้ชัดๆ เลยก็คือ มันจะช่วยทำให้การไล่โค้ดง่ายขึ้นกว่าเดิมมาก แล้วยิ่งแอปเรามีคนเขียนร่วมกันหลายคน การใช้ Flux นั้นจะช่วยให้เราสามารถแบ่งงานกันได้ง่ายขึ้นด้วยนะครับ เราอาจแบ่งให้คนนึงรับผิดชอบในส่วนของ component และ action ไปเลย ส่วนอีกคนก็อาจจะดูในส่วนของ store ไป อะไรทำนองนี้ครับ

ผมเคยลองแก้แอปที่เดิมเขียนด้วย jQuery ธรรมดา มาเป็น React + Flux นะครับ ความรู้สึกหลังเขียนเสร็จก็คือ การใช้ React + Flux มันเขียนสนุกกว่าอะครับ มาถึงเราก็แค่จัดเตรียม ui ที่เราอยากจะให้ render ณ state ต่างๆ เอาไว้ ที่เหลือเราก็แค่เล่นกับข้อมูลเท่านั้นเองครับ เราไม่ต้องไปสนว่ามันจะเกิดอะไรขึ้นเมื่อข้อมูลมีการเปลี่ยนแปลง ขอแค่ข้อมูลมันถูก แอปของเราก็จะแสดงผลได้อย่างถูกต้องโดยอัตโนมัติครับ

แนวทางศึกษาต่อ

ก่อนจากกัน ผมอยากจะบอกว่า มันอาจจะไม่จบแค่ Flux นะครับ เพราะในปัจจุบัน มีหลายๆ เจ้า นำไอเดียของ Flux ไปต่อยอดให้มันดีขึ้น มีฟีเจอร์เยอะขึ้น ลดความยุ่งยากซับซ้อนลง โดยตัวที่ผมแนะนำให้ลองไปเล่นดูก็จะมี Alt และ Redux ครับ Alt นี่จะได้ในเรื่องของ docs ที่โอเค และมีคนใช้งานจริงใน production แล้ว ส่วน Redux นั้นน่าสนใจตรงที่มันเพิ่งเกิดใหม่ได้ไม่นาน ทำให้เห็นแนวคิดของ library อื่นๆ หมดแล้วว่าแต่ละเจ้านั้นมีข้อดีข้อเสียยังไง คิดว่าน่าจะใช้เวลาเรียนรู้ไม่นานมากครับ เพราะวิธีคิดนั้นไม่ได้ต่างจาก Flux มากมายอะไร

หากมองดีๆ เราจะเห็นนะครับว่า จริงๆ แล้ว Flux มันไม่ได้ยึดติดอยู่กับ tool หรือ framework ตัวไหนเลย เพราะมันเป็นก็เพียงแค่แนวคิดเท่านั้นเอง นั่นหมายความว่าในอนาคต เราอาจจะเห็นการใช้ Flux ร่วมกับ Angular ก็เป็นได้ ก็คิดซะว่าบทความนี้เป็นการปูพื้นฐานเกี่ยวกับ Flux แล้วกันนะครับ แล้วพบกันใหม่บทความหน้าครับ

]]>
http://www.siamhtml.com/getting-started-with-flux/feed/ 8
Isomorphic คืออะไร ? + สอนวิธีทำด้วย Reacthttp://www.siamhtml.com/build-isomorphic-apps-with-react/ http://www.siamhtml.com/build-isomorphic-apps-with-react/#comments Sun, 24 May 2015 17:10:30 +0000 http://www.siamhtml.com/?p=7306 หากใครตามข่าวในวงการเว็บหน่อยก็คงจะเคยได้ยินคำว่า Isomorphic JavaScript หรือที่สมัยนี้เปลี่ยนมาเรียกว่า Universal JavaScript กันมาบ้างใช่มั้ยล่ะครับ หรือถ้าเพิ่งเคยได้ยิน ก็ขอบอกเลยว่ามันกำลังมาแรงมากๆ ในตอนนี้ฮะ วันนี้ SiamHTML เลยจะมาเล่าสู่กันฟังซะหน่อยว่าเจ้า Isomorphic นี้มันคืออะไร รับรองเลยว่ามันเจ๋งสุดๆ ครับ

ทุกวันนี้เราทำเว็บกันอย่างไร ?

ก่อนอื่นเรามาดูกันว่าตั้งแต่อดีตจนถึงปัจจุบัน เราทำเว็บกันอย่างไร ?

  • Server เป็นคน Render ในยุคแรกๆ เราจะเอา business logic, route และ template ของหน้าต่างๆ ฝากไว้ที่ฝั่ง server ทั้งหมดครับ คือ server จะทำหน้าที่ render หน้าเว็บทั้งหน้าขึ้นมา แล้วถึงจะส่งหน้าเว็บที่สมบูรณ์แล้ว กลับไปยัง client ทั้งนี้ก็เพราะว่าเครื่อง client สมัยก่อนยังไม่ค่อยแรงเท่าไร ภาระส่วนใหญ่จึงตกอยู่ที่เครื่อง server ครับ
  • Client เป็นคน Render ในปัจจุบัน เครื่อง client เร็วขึ้นมากครับ เลยเกิดเทคนิคการทำเว็บรูปแบบใหม่ที่เรียกว่า Single-page application หรือ SPA ขึ้นมา โดยการทำเว็บแบบ SPA นี้ business logic, route และ template ของหน้าต่างๆ จะย้ายมาอยู่ฝั่ง client แทนครับ การ render หน้าเว็บก็จะทำที่ฝั่ง client เลย ไม่ต้องเสียเวลามาโหลดหน้าเว็บใหม่ทั้งหน้าเหมือนแต่ก่อน ส่วน server ก็จะเหลือหน้าที่เพียงอย่างเดียวนั่นก็คือการดึงข้อมูลจาก database มาให้ หรือพูดง่ายๆ ก็คือเอา server มาทำ API นั่นเองครับ
  • Server/Client ช่วยกัน Render สุดท้ายมาดูเทคนิคใหม่ล่าสุดที่รวมเอาข้อดีของ 2 วิธีแรก มารวมกันครับ คือวิธีนี้ตอนโหลดมาทีแรกจะให้ server เป็นคน render หน้าเว็บทั้งหน้ามาให้ก่อน แล้วหลังจากนั้น client จะเป็นคนรับหน้าที่ render ทั้งหมดแทนครับ ซึ่งการจะทำแบบนี้ได้ เราจะต้องอาศัยสิ่งที่เรียกว่า Isomorphic JavaScript เข้ามาช่วย

Isomorphic JavaScript คืออะไร ?

เวลาเราจะเขียนโค้ดสำหรับดึงค่า cookie มาใช้งาน สมมติว่าที่ฝั่ง server เราใช้ PHP แล้วกัน เราก็จะเขียนโค้ดแบบนึง ส่วนฝั่ง client เราใช้ JavaScript เราก็จะต้องเขียนโค้ดอีกแบบนึงถูกมั้ยครับ เพราะมันคนละภาษากัน syntax ก็ย่อมไม่เหมือนกันอยู่แล้ว ทีนี้ปัญหาคืออะไรครับ ? เราจะต้องมาทำงาน 2 ครั้ง ถูกมั้ยครับ หรืออย่างน้อยเราก็จะต้องเรียนรู้ 2 ภาษา ที่มีวิธีการเขียนที่ไม่เหมือนกัน ปัญหาเหล่านี้จะหมดไปหากเราใช้ Isomorphic JavaScript ครับ

คำว่า Isomorphic JavaScript นั้นมาจากคำว่า Isomorphic ที่แปลว่า “รูปแบบเดียวกัน” ครับ ซึ่งถ้าจะแปลในบริบทของการทำเว็บแล้ว Isomorphic JavaScript ก็จะหมายถึงรูปแบบการเขียนโค้ด JavaScript ที่เหมือนกันไม่ว่าจะเขียนที่ฝั่ง server หรือ ฝั่ง client ก็ตามครับ

เอ๊ะ! JavaScript มันสามารถเขียนที่ฝั่ง server ได้ด้วยหรอ ? ได้สิครับ ด้วยการใช้ Node.js นั่นเอง หากพูดให้เข้าใจง่ายๆ Isomorphic JavaScript ก็คือการใช้ Node.js ร่วมกับ template engine ที่สามารถ render ได้ทั้งที่ฝั่ง server และฝั่ง client ครับ วิธีนี้จะช่วยให้เราเขียน business logic, route รวมไปถึง template เพียงแค่ครั้งเดียวเท่านั้น ตอนโหลดครั้งแรก เราจะให้ server ทำหน้าที่ render หน้าเว็บให้ก่อน แล้วหลังจากนั้น พอ user กดดูข้อมูลอะไร เราก็จะส่งไม้ต่อให้ client ทำหน้าที่ render ข้อมูลส่วนนั้นครับ

ข้อดีของเว็บแบบ Isomorphic

ผมขอออกตัวนิดนึงนะครับว่าผมนี่เป็นแฟนของ Isomorhic เลย เพราะข้อดีของมันนี่เรียกว่าตอบโจทย์เว็บสมัยนี้ได้ดีจริงๆ

  • ใช้โค้ดร่วมกันทั้ง Server และ Client อย่างที่บอกครับว่าสิ่งที่เราทำก็แค่เขียน JavaScript ที่เราคุ้นเคยเป็นอย่างดี ไม่ต้องไปแตะภาษาอื่นๆ ที่เราไม่ค่อยคุ้นเคยเลย (ขอบอกว่า Front-end Developer ทุกคนสามารถทำได้แน่นอนครับ)
  • โหลดครั้งแรกไม่กระพริบ ใครทำ SPA บ่อยๆ จะรู้ดีครับว่า ตอนโหลดหน้าเว็บครั้งแรกมันจะชอบกระพริบๆ เพราะมันไม่ได้ render มาตั้งแต่ฝั่ง server ซึ่งอาการนี้จะไม่พบในเว็บแบบ Isomorphic แน่นอนครับ
  • SEO ประเด็นนี้สำคัญสุดๆ ครับ เพราะว่าเว็บแบบ SPA นั้น ไม่ค่อยดีต่อ SEO เท่าไร (จริงๆ ก็ทำให้ดีได้ แต่ค่อนข้างจะลำบาก) แต่ Isomorphic นั้นได้ SEO ไปเต็มๆ ครับ เพราะ server จะ render หน้าที่มีข้อมูลสมบูรณ์มาให้ตั้งแต่ตอนที่โหลดมาครั้งแรกแล้ว

มาถึงตรงนี้คาดว่าเพื่อนๆ คงอยากจะรู้แล้วใช่มั้ยครับว่าขั้นตอนการทำเว็บแบบ Isomophic  นั้น มันยากง่ายแค่ไหน ส่วนตัวผมมองว่ามันไม่ได้ยากมากนะครับ เพียงแต่ว่าการจะเขียน Isomorphic ได้นั้น เราจำเป็นจะต้องมีความรู้พื้นฐานเยอะซักนิดนึง

พื้นฐานที่ควรรู้

มาดูกันครับว่าก่อนที่จะเริ่มลงมือทำเว็บแบบ Isomorphic เราควรจะมีพื้นฐานอะไรมาก่อนบ้าง

  • Node.js อันนี้ขาดไม่ได้เลยครับ เพราะเราจะต้องอาศัยมันในการใช้ JavaScript เป็นภาษาของฝั่ง server
  • Express + Jade / Handlebars ต่อมาเป็น framework ที่จะใช้กับ Node.js ครับ ซึ่งจริงๆ แล้วจะใช้หรือไม่ใช้ก็ได้ สำหรับบทความนี้ ผมจะขอใช้ Express แล้วกัน ส่วน template engine ที่จะใช้กับ Express นั้น จะใช้หรือไม่ใช้ก็แล้วแต่เลยครับ
  • Browserify / Webpack สิ่งสำคัญที่ขาดไม่ได้เลยก็คือ tool ที่จะใช้แปลง JavaScript Module แบบ CommonJS ให้สามารถรันบน web browser ได้ครับ สำหรับบทความนี้ผมจะขอใช้เป็น Webpack แล้วกันนะฮะ
  • React มาถึงพระเอกของงานนี้ครับ นั่นก็คือ template engine ที่สามารถ render ได้ทั้งฝั่ง server และฝั่ง client นั่นเอง ซึ่งจริงๆ แล้วในปัจจุบันมีให้เลือกใช้หลายท่าเลย แต่ในบทความนี้ ผมจะขอเลือกใช้เป็น React แล้วกันนะฮะ (จริงๆ แล้ว React นั้นไม่ใช่ template engine นะครับ เพียงแต่เราจะอาศัยความสามารถส่วนหนึ่งของมันมาเป็น template engine เท่านั้นเอง)
  • ECMAScript 2015 อันนี้ถึงแม้ว่าจะไม่จำเป็น แต่ผมแนะนำให้เรียนรู้ไว้ครับ เพราะโค้ดของ project ต่างๆ ที่เกี่ยวกับ Isomorphic ส่วนใหญ่จะเขียนด้วย ECMAScript 2015 หมดเลยฮะ

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

ตกลงกันก่อน !

มาถึงตรงนี้ หากใครยังไม่เคลียร์ในประเด็นไหน ผมขอแนะนำให้ย้อนกลับไปอ่านอีกทีก่อนนะครับ เพราะหลังจากนี้ เราจะเริ่มลงมือเขียนโค้ดกันจริงๆ แล้ว ซึ่งตอนนั้นผมจะถือว่าพื้นฐานของเราแน่นแล้ว รายละเอียดเล็กๆ น้อยๆ ผมอาจจะไม่ได้อธิบาย เพราะไม่อยากให้บทความนี้ยาวเกินไปครับ (นี่ขนาดไม่อยากแล้วนะ -_-“)

เอาล่ะครับ เราจะมาเริ่มลงมือเขียนเว็บแบบ Isomorphic กัน ก่อนอื่นผมจะขอพูดถึงพระเอกของงานนี้อย่าง React ซักนิดนึง เพราะมันถือเป็นหัวใจสำคัญของ Isomorphic เลยทีเดียว

React คืออะไร ?

ในเว็บของ React เขียนเอาไว้ว่า React เป็น JavaScript Library ที่เอาไว้สร้าง User Interface ครับ ฟังดูแล้วเหมือน scope มันจะเล็กๆ เนอะ ว่ามั้ย คือเอาไว้ทำแค่พวก component บนหน้าเว็บอะไรทำนองนี้อะหรอ ? คำตอบคือใช่เลยครับ บางคนอาจจะนึกถึง Web Components หรือ Custom Directive ของ AngularJS แต่ขอบอกเลยว่า React มันเทพกว่านั้นเยอะ

จุดเด่นของ React

มาดูกันครับว่าสิ่งที่ทำให้ React ดูโดดเด่นกว่าชาวบ้านนั้นมีอะไรบ้าง

  • สร้างโดย Facebook ก่อนอื่นต้องขอบอกก่อนนะครับว่าคนทำ React นั้นไม่ใช่ใครที่ไหน Facebook นั่นเองครับ แล้วทุกวันนี้ React ก็ยังคงถูกใช้อยู่ทั้งใน Facebook และ Instagram ก็ลองคิดกันเอาเองนะครับว่ามันจะสุดยอดขนาดไหน
  • Render ได้ทั้ง 2 ฝั่ง ข้อนี้เอง ที่ทำให้ React ดูดีกว่า AngularJS ในแง่ของ SEO ครับ เนื่องจาก AngularJS นั้นจะทำกับ DOM ได้เพียงอย่างเดียว crawler จึงไม่สามารถ index เนื้อหาที่โหลดเข้ามาทีหลังได้ครับ
  • Virtual DOM ในส่วนของการ render ที่ฝั่ง client นั้น React ก็ยังดูดีกว่า AngularJS ตรงที่ React จะไม่ทำอะไรกับ DOM โดยตรง แต่ React จะสร้างสิ่งที่เรียกว่า Virtual DOM ขึ้นมา แล้วดูว่า DOM ที่มีอยู่เดิมกับ DOM ที่กำลังจะเปลี่ยนไปนั้นแตกต่างกันอย่างไร เมื่อหาความแตกต่างได้แล้ว React จึงค่อยไปแก้ DOM จริงๆ ซึ่งขั้นตอนที่ว่านี้อยู่ใน memory ทั้งหมดครับ เรียกได้ว่าการใช้ React ทำ DOM Manipulation นั้น เร็วกว่าการใช้ JavaScript แบบเพียวๆ ซะอีก
  • Unidirectional ในปัจจุบัน เรามักจะพบว่า JavaScript framework ส่วนใหญ่จะเป็นแบบ Two-way Data Binding ซึ่งมันก็คือการที่ data ใน model และ view จะเปลี่ยนแปลงตามกันและกัน แต่ React นั้นตรงกันข้ามครับ คือจะเป็นแบบ Unidirectional หรือแบบทางเดียวนั่นเอง เพราะ React มองว่า Two-way นั้นอาจทำให้เกิดการอัพเดทข้อมูลที่ไม่จำเป็นขึ้นมาได้ครับ

มาถึงตรงนี้ ผมว่าจะต้องมีคนมองว่าผมอวย React แน่ๆ เลย หรือมองว่าผมไม่ชอบ AngularJS หรือเปล่า ? ตอบเลยว่าไม่ใช่ครับ ทั้ง React และ AngularJS ต่างก็เป็น tool ที่ดีด้วยกันทั้งคู่ และไม่ได้เกิดมาเพื่อทดแทนกันครับ หากสงสัยว่า React นั้นมีข้อเสียอะไรบ้าง ? ขอให้อดใจรอนิดนึง เพราะเดี๋ยวเราจะได้เจอกับตัวตอนที่เขียนโค้ดในบทความนี้แหละครับ

Syntax ของ React

เล่ามาตั้งนาน มาดูวิธีการเขียนโค้ดของ React กันดีกว่าครับ ก่อนอื่นเราจะต้องเข้าใจก่อนว่า แนวคิดของ React นั้นก็คือ การมองทุกสิ่งทุกอย่างบนหน้าเว็บเป็น component หากมองดีๆ จะเห็นว่าหน้าเว็บนั้นก็จะมี root element อันนึง ภายในก็จะประกอบไปด้วยกล่องต่างๆ แล้วภายในกล่องต่างๆ นั้นก็อาจจะมีกล่องลูกๆ ซ้อนเข้าไปข้างในอีกที เจ้าพวกกล่องเหล่านี้แหละ ที่เราจะเรียกมันว่า component

Top-Level API

ก่อนจะเริ่มสร้าง component กัน เรามาดู api ที่เราจะต้องใช้กันแน่ๆ ครับ ว่ามันมีอะไรบ้าง

  • React.createClass เอาไว้สร้าง component ครับ เพราะจริงๆ แล้ว component มันก็คือ class ที่ประกอบไปด้วย method ต่างๆ นั่นเอง
  • React.createElement เอาไว้สร้าง ReactElement ขึ้นมาจาก component ที่เราสร้างครับ
  • ReactDOM.render เอาไว้ render ReactElement เข้าไปใน DOM ครับ เราจะใช้ method นี้ ในการ render ทางฝั่ง client
  • ReactDOMServer.renderToString เอาไว้ render ReactElement ออกมาเป็น string ครับ เราจะใช้ method นี้ ในการ render ทางฝั่ง server

api พวกนี้ เรายังไม่ต้องไปท่องจำนะครับว่าอันไหนเอาไว้ใช้ทำอะไรบ้าง ผมอยากให้เราเห็นภาพรวมคร่าวๆ ว่าเราจะต้องสร้าง component ของ react ขึ้นมาก่อน แล้วค่อยบอกให้ react มัน render component เหล่านั้น เข้าไปใน DOM ก็เท่านั้นเองฮะ

วิธีสร้าง Component แบบง่ายๆ

สมมติว่าเรามี container เปล่าๆ อันนึง ชื่อ div#content หากเราต้องการจะใส่ component ง่ายๆ อย่าง p.greet เข้าไปแบบนี้ เราจะต้องเขียนโค้ดอย่างไร ?

<body>
    <div id="content">
      <p class="greet">Hello, SiamHTML!</p>
    </div>
</body>

จากโจทย์ด้านบน เราสามารถเขียนโค้ดด้วย React ได้แบบนี้ครับ

// โหลด React มาสร้าง component
var React = require('react');

// โหลด ReactDOM มา render component ใส่ DOM
var ReactDOM = require('react-dom');

//  สร้าง component ชื่อ Greet
var Greet = React.createClass({
    // component นี้ แสดงผลอย่างไร ?
    render: function() {
        return (
            // เป็น p.greet ที่มี text เป็น "Hello, SiamHTML!"
            React.createElement(
                'p', 
                {className: "greet"}, 
                "Hello, SiamHTML!"
            )
        );
    }
});

// แสดงผล component ชื่อ Greet ข้างใน #content
ReactDOM.render(
    React.createElement(Greet, null), 
    document.getElementById('content')
);

จะเห็นว่าเราใช้ React.createClass ในการสร้าง component ขึ้นมาครับ ซึ่งภายในก็จะเป็น object ที่จะเอาไว้ใช้ในการอธิบายว่า component นี้มีลักษณะอย่างไร สังเกตนะครับว่าผมจำเป็นต้องใส่ method render เอาไว้ด้วย ไม่งั้น React มันจะไม่รู้ว่าจะแสดงผล component นี้ออกมาอย่างไร

แม่เจ้า! โจทย์ง่ายๆ แค่นี้ React ล่อไปกี่บรรทัดเนี่ย ใช้ jQuery เขียนบรรทัดเดียวจบเลยนะ! อย่าเพิ่งบ่นไปครับ เพราะคนทำ React เค้าเตรียมวิธีแก้ปัญหานี้มาแล้ว ด้วยสิ่งที่เรียกว่า JSX

รู้จักกับ JSX Syntax

จากโจทย์เดิมครับ เราจะเปลี่ยนมาสร้าง component โดยใช้ syntax แบบใหม่ที่เรียกว่า JSX แทน ลองดูโค้ดด้านล่างนี้ฮะ

var Greet = React.createClass({
    render: function() {
        return (
            // เขียนแบบ JSX 
            <p className="greet">Hello, SiamHTML!</p>
        );
    }
});

// เขียนแบบ JSX 
ReactDOM.render(<Greet />, document.getElementById('content'));

เห็นมั้ยครับว่าโค้ดของเราจะสั้นลง และอ่านง่ายขึ้นเยอะเลย จะเห็นว่าการเขียนด้วย JavaScript ด้วย JSX นั้น คล้ายกับการเขียน XML มากเลยครับ จะต่างเล็กน้อยก็ตรงที่การระบุ class นั้น เราจะต้องเปลี่ยนมาใช้ className แทนเท่านั้นเอง

ทีนี้ลองมาดูตัวอย่าง component ที่ซับซ้อนขึ้นอีกนิดนึงครับ สมมติเราจะสร้าง component ใหม่ที่ชื่อ <Header /> แล้วข้างในมี component ลูก อยู่ 2 ตัว คือ <Menu /> และ <Search /> เราก็จะเขียนโค้ดแบบนี้ครับ

var Menu = React.createClass({
    render: function() {
        return (
            <ul>
                <li>Menu 1</li>
                <li>Menu 2</li>
                <li>Menu 3</li>
            </ul>
        );
    }
});

var Search = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" value="Search" />
                <input type="submit" value="Submit" />
            </form>
        );
    }
});

var Header = React.createClass({
    render: function() {
        return (
            <header>
                <Menu />
                <Search />
            </header>
        );
    }
});

ReactDOM.render(<Header />, document.getElementById('content'));

JSX ช่วยให้อะไรๆ ง่ายขึ้นเยอะเลยใช่มั้ยล่ะครับ มาถึงตรงนี้ เราคงพอจะเห็นภาพแล้วว่า การใช้ React สร้าง UI ของหน้าเว็บ ก็คือการแบ่งของในหน้าเว็บออกเป็น component ย่อยๆ ก่อน แล้วค่อยจัดกลุ่ม component ย่อยๆ เหล่านั้นให้กลายเป็น component ที่ใหญ่ขึ้น เมื่อ component ใหญ่ขึ้นเรื่อยๆ สุดท้ายแล้ว มันก็จะกลายเป็นหน้าเว็บที่สมบูรณ์ครับ

ส่งต่อข้อมูลไปยัง Component อื่นด้วย Props

ในการทำเว็บจริงๆ คงไม่มีใครใส่ข้อมูลลงไปใน template ตั้งแต่แรกถูกมั้ยครับ เรามักจะใช้วิธี assign ข้อมูลเหล่านั้น เข้าไปใน template แทน การใช้ React ก็เหมือนกันครับ สมมติเราต้องการจะส่งค่าอะไรให้กับ component เราสามารถเขียนแบบนี้ได้เลยฮะ

ReactDOM.render(<Greet name="SiamHTML" />, document.getElementById('content'));

แบบนี้จะเป็นการส่ง props ชื่อ name ที่มีค่าเป็น “SiamHTML” ไปให้กับ component ที่ชื่อ Greet ครับ ทีนี้ หากเราอยากจะดึงข้อมูลที่ได้รับ มาแสดง ให้เราใช้ this.props แล้วตามด้วยชื่อ props แบบนี้ได้เลยครับ

var Greet = React.createClass({
    render: function() {
        return (
            // ดึงค่าของ property ชื่อ name มาใช้
            <p className="greet">Hello, {this.props.name}!</p>
        );
    }
});

ผลลัพธ์ที่ได้ก็จะออกมาเป็น “Hello, SiamHTML!” ครับ

แชร์ข้อมูลภายใน Component ด้วย State

แต่บางที แค่ property อย่างเดียวอาจจะทำอะไรได้ไม่มากนะครับ เพราะพอเรา assign ค่าอะไรให้ component แล้ว พอมัน render เสร็จ มันก็จะอยู่นิ่งๆ แบบนั้นไปตลอด หากเราอยากจะทำให้ component มี interaction ใดๆ เราจะต้องอาศัยสิ่งที่เรียกว่า state เข้ามาช่วยครับ

สำหรับผมแล้ว state นี่ถือเป็นจุดขายของ React เลยนะครับ หน้าที่ของมันก็คือการเก็บ “สภาพ” ของ component ในขณะนั้นๆ เอาไว้ เมื่อไรก็ตามที่ state มีค่าเปลี่ยนไป เมื่อนั้น method render จะถูกสั่งให้ทำงานทันทีครับ พูดง่ายๆ ก็คือ หากเราต้องการจะอัพเดทหน้าตาของ component ไหน ก็ให้เราไปเปลี่ยน state ของมันนั่นแหละครับ เพื่อให้เห็นภาพมากขึ้น ลองมาดูโค้ดสร้าง component ที่เป็นปุ่ม Like กันครับ

var LikeButton = React.createClass({
    // method นี้ เอาไว้กำหนด state เริ่มต้น
    getInitialState: function() {
        return {liked: false};
    },
    // เพิ่ม custom method ชื่อ handleClick
    handleClick: function(event) {
        // เปลี่ยน state "liked" ให้มีค่าตรงข้ามกับค่า liked เดิม
        this.setState({liked: !this.state.liked});
    },
    render: function() {
        var text = this.state.liked ? 'Unike' : 'Like';
        return (
            // เมื่อถูก click ให้รัน method handleClick
            <button onClick={this.handleClick}>
                {text}
            </button>
        );
    }
});

จะเห็นว่าที่ handleClick นั้น เราไม่ต้องไปสั่งให้มัน render ใหม่นะครับ เพราะเมื่อไรที่ state เปลี่ยน component ก็จะถูก render ใหม่โดยอัตโนมัติอยู่แล้วครับ

Lifecycle Methods ของ Component

สุดท้ายเรามาดู Lifecycle ของ component กันหน่อยครับว่า ตั้งแต่ component ถูกสร้างขึ้นมา จนถึงตอนที่มันถูกถอดออกจากหน้าเว็บนั้น มันเกิดอะไรขึ้นบ้าง

  • componentWillMount เกิดขึ้นก่อนที่ component จะถูก render ขึ้นมา
  • componentDidMount เกิดขึ้นหลังจากที่ component ได้ถูก render แล้ว
  • componentWillReceiveProps เกิดขึ้นหลังจากที่ component ได้รับ props ค่าใหม่
  • componentWillUpdate เกิดขึ้นก่อนที่ component จะถูก render ใหม่ หลังจากที่ได้รับ props ใหม่
  • componentDidUpdate เกิดขึ้นหลังที่จาก component ถูก render ใหม่
  • componentWillUnmount เกิดขึ้นก่อนที่ component จะถูกเอาออกจาก DOM

สาเหตุที่เราต้องรู้ Lifecycle ของ component ก็เพราะว่าเราสามารถนำ Lifecycle เหล่านี้มาใส่เป็น method ให้กับ component ได้ครับ ลองดูตัวอย่างโค้ดด้านล่างนี้

var Timer = React.createClass({
    // กำหนด state ชื่อ secondsElapsed ให้มีค่าเป็น 0
    getInitialState: function() {
        return {secondsElapsed: 0};
    },
    // custom method สำหรับอัพเดท state ชื่อ secondsElapsed
    tick: function() {
        this.setState({secondsElapsed: this.state.secondsElapsed + 1});
    },
    // หลังจาก render แล้ว ให้ทำอะไร ?
    componentDidMount: function() {
        this.interval = setInterval(this.tick, 1000);
    },
    // ก่อนจะลบออกจาก DOM ให้ทำอะไร ?
    componentWillUnmount: function() {
        clearInterval(this.interval);
    },
    // แสดงผลออกมาอย่างไร ?
    render: function() {
        return (
          <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
        );
    }
});

ก็น่าจะพอเห็นประโยชน์ของ Lifecycle methods กันบ้างแล้วใช่มั้ยครับ ทีนี้เราลองมาดูวิธีการนำ React ไปใช้งานจริงกันดีกว่าครับ

วิธีนำ React มาใช้งาน

สำหรับการใช้งาน React นั้นจะแบ่งออกเป็น 2 วิธีด้วยกัน ดังนี้ครับ

1. ใช้ที่ฝั่ง Client

แบบแรกจะเป็นการเอา React มาใช้สร้าง UI ที่ฝั่ง client ครับ วิธีใช้ก็ง่ายๆ ให้เราไปดาวน์โหลด React มาก่อน หรือจะใช้จากเว็บของ facebook ไปก่อนก็ได้ฮะ จากนั้นก็ติด script ของ React เข้าไปแบบนี้ได้เลย

<!DOCTYPE html>
<html>
    <head>
        <script src="https://fb.me/react-0.14.3.min.js"></script>
        <script src="https://fb.me/react-dom-0.14.3.min.js"></script>
    </head>
    <body>
        <div id="content"></div>
    </body>
</html>

สังเกตนะครับว่าเราจะต้องติด script ของ React ถึง 2 ตัว ด้วยกัน คือตัวแรกจะเป็น core ของ React เลย ส่วนตัวที่สองจะเป็น script ที่ดูแลในส่วนของการ render ใส่ DOM ของหน้าเว็บครับ แล้วถ้าเราเขียน React โดยใช้ JSX ด้วย เราก็จะต้องเพิ่ม script ที่ชื่อ Babel ที่มีความสามารถในการแปลง JSX เข้ามาอีกตัวครับ ไม่งั้นโค้ดที่เราเขียนจะพังเพราะ web browser มันยังไม่รู้จัก JSX นั่นเอง

<head>
    <script src="https://fb.me/react-0.14.3.min.js"></script>
    <script src="https://fb.me/react-dom-0.14.3.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
</head>

ส่วนโค้ดหลักของเรา ก็แยกไปเขียนอีกไฟล์ก็ได้ครับ แล้วค่อยใส่เข้ามาต่อท้าย container หลักของเราแบบนี้

<body>
    <div id="content"></div>
    <script type="text/babel" src="siamhtml.js"></script>
</body>

สังเกตนะครับว่า เราจะต้องใส่ type ของ script ให้เป็น text/babel ด้วย เพราะ Babel จะแปลงให้เฉพาะ script ที่เป็น type นี้เท่านั้นครับ เพียงเท่านี้ เราก็พร้อมที่จะรันโค้ดที่เขียนด้วย React แล้วล่ะครับ

2. ใช้ที่ฝั่ง Server

ส่วนแบบที่สองจะเป็นการเอา React มาใช้สร้าง UI ด้วยภาษาฝั่ง server อย่าง Node.js ครับ วิธีนี้จะค่อนข้างซับซ้อนกว่าวิธีแรกนิดนึง เพราะเราจะต้องเปลี่ยนมาใช้ React แบบที่เป็นโมดูล CommonJS แทน

// ไฟล์ SiamHTML.jsx

// โหลด React ที่เป็นโมดูลแบบ CommonJS มาใช้งาน
var React = require('react');

// ใช้ React สร้าง component
var SiamHTML = React.createClass({
    render: function() {
        return (
            <p>SiamHTML</p>
        );
    }
});

// export component นี้ ออกเป็น module
module.exports = SiamHTML;

ส่วนฝั่ง server ก็ให้ใช้ ReactDOMServer ในการ render component ออกมาเป็น string ครับ

// ไฟล์ app.js

// โค้ดสำหรับสร้าง HTTP server
...

// โหลด React มาใช้งาน
var React =  require('react');

// โหลด ReactDOMServer มา render component ให้เป็น String
var ReactDOMServer = require('react-dom/server');

// โหลด component ที่ชื่อ SiamHTML มาใช้
var SiamHTML = require('./components/SiamHTML.jsx');

// โค้ดสำหรับ render component ที่ใช้กับฝั่ง server
app.get('*', function(req, res) {
    res.send(ReactDOMServer.renderToString(React.createElement(SiamHTML)));
});

มาถึงตรงนี้ บางคนอาจจะสงสัยว่า ถ้าจะทำ Isomorphic มันก็ต้อง render ได้ทั้ง 2 ฝั่งเลยสิ แล้วเราจะใช้วิธีไหนล่ะ ?

Workshop – Setup Isomorphic

เอาล่ะครับ เมื่อเรารู้พื้นฐานต่างๆ ครบแล้ว เรามาเริ่มเขียน Isomorphic กันเลยดีกว่า

*** เนื่องจากวิธีการทำเว็บแบบ Isomorphic นั้นยังไม่นิ่ง หลายๆ dependencies ที่เราจำเป็นต้องใช้ ก็ยังมีการพัฒนาอยู่เรื่อยๆ บางทีถึงขั้นเปลี่ยน syntax ไปเลยก็มีเยอะฮะ ผมเลยต้องขออภัยล่วงหน้าก่อนเลย หากตัวอย่างโค้ดในบทความนี้ เพื่อนๆ นำไปรันแล้วมันไม่ผ่าน เพราะบางทีอัพเดทโค้ดตามไม่ทันจริงๆ ครับ แต่โดยรวมๆ แล้ว หลักการในบทความนี้ยังคงใช้งานได้เหมือนเดิมนะฮะ เพียงแต่เราจะต้องไปปรับ syntax ให้มันอัพเดทหน่อยเท่านั้นเอง ระหว่างนี้ เจอ error อะไร ก็ขอให้เพื่อนๆ ไปไล่อ่าน doc ของตัวที่มันเกิด error ที่เว็บหลักของมันไปก่อนแล้วกันนะครับ ยังไงผมจะมาไล่อัพเดทเนื้อหาในบทความนี้อยู่เรื่อยๆ ฮะ T__T

เตรียมไฟล์และโฟลเดอร์

เริ่มด้วยการจัดโครงสร้างของเว็บแอปเราครับ ให้เราสร้างไฟล์และโฟลเดอร์ขึ้นมาตามนี้ได้เลย

myApp/
|
|-- public/                   
|   |
|   |-- images/              # เก็บรูปต่างๆ ที่จะใช้ในเว็บ
|   |
|   `-- js/
|       | 
|       `-- bundle.js        # ไฟล์ bundle ที่ได้จากการรัน webpack
|
|-- src/                        
|   |
|   |-- js/                      
|   |   |
|   |   |-- components/      # เก็บ component ทั้งหมด (.jsx)
|   |   |
|   |   |-- client.js        # โค้ดสำหรับ render ฝั่ง client (เป็น entry point ของ webpack)
|   |   |
|   |   |-- router.jsx       # middleware สำหรับทำ routing
|   |   |
|   |   |-- routes.jsx       # เก็บ routes ทั้งหมดของแอป
|   |   |
|   |   `-- server.js        # โค้ดสำหรับ render ฝั่ง server
|   |
|   `-- templates/           # เก็บ template ต่างๆ ของแอป
|
|-- .babelrc                 # ไฟล์กำหนด option ของ Babel
|
|-- app.js                   # ไฟล์ entry point ของฝั่ง server (รันด้วย Node.js)
|
|-- package.json             # ระบุ dependency ที่จะใช้
|
`-- webpack.config.js        # ไฟล์ config ของ webpack

ก่อนไปต่อผมอยากให้ทำความเข้าใจกับโครงสร้างนี้ให้ดีๆ นะครับ เพราะถ้าเราเข้าใจมันแล้ว ขั้นตอนต่อๆ ไปก็จะง่ายขึ้นเยอะเลยล่ะครับ

ติดตั้ง Babel

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

npm install babel babel-register --save-dev

ก่อนหน้านี้พอติดตั้ง Babel เสร็จแล้ว มันจะพร้อมใช้งานได้ทันทีฮะ แต่ตอนนี้ไม่ได้แล้วครับ เพราะเราจะต้องบอก Babel ก่อนว่าอยากจะให้ Babel ช่วยแปลงอะไรบ้าง สำหรับโปรเจคนี้ เราจะมีการใช้ JSX และ ECMAScript 2015 ครับ ให้เราติดตั้ง preset ของ Babel ตามนี้ได้เลย

npm install babel-preset-es2015 babel-preset-react --save-dev

จากนั้นให้เราไปสร้างไฟล์ .babelrc ที่จะเอาไว้กำหนด option ต่างๆ ให้กับ Babel แล้วใส่ preset ที่เราโหลดมาเมื่อกี้ลงไป

{
  "presets": ["es2015", "react"]
}

เพียงเท่านี้ Babel ก็พร้อมใช้งานแล้วล่ะครับ ^0^

สร้าง Entry point ของฝั่ง Server

เริ่มแรกให้เราเข้าไปดูที่ไฟล์ app.js ซึ่งจะเป็นไฟล์ที่เราเอาไว้ start app Node.js ของเราฮะ ให้เราใส่โค้ดด้านล่างนี้ลงไป

// ให้ Babel ช่วยแปลง ECMAScript 2015 และ JSX
require('babel-register');

// โหลดไฟล์ที่เอาไว้ใช้ start server
require('./src/js/server.js');

จะเห็นว่าไฟล์นี้ เราแค่จะเอาไว้เรียก Babel มาใช้งานเท่านั้นเองฮะ ทีนี้เราก็จะสามารถเขียนโค้ดด้วย JSX และ ECMAScript 2015 ได้แล้วล่ะครับ

ใส่โค้ดสำหรับ Render ฝั่ง Server

ต่อมาไปดูที่ไฟล์ server.js ครับ ไฟล์นี้เราจะเอาไว้สร้าง http server ขึ้นมาเพื่อรับ request จาก client ให้เราใส่โค้ดสำหรับสร้าง http server ขึ้นมาตามปกติ โดยบทความนี้ ผมจะขอใช้ Express เป็น framework สำหรับ Node.js และใช้ Handlebars เป็น template engine ครับ ให้เราติดตั้ง dependency ที่จำเป็นตามด้านล่างนี้

npm install express express-handlebars serve-favicon --save-dev

จากนั้นก็ใส่โค้ดสำหรับ create server ด้วย Express ลงไป

import path from 'path';
import express from 'express';
import exphbs from 'express-handlebars';
import favicon from 'serve-favicon';

var app = express();
app.use(favicon(path.join(__dirname, '../../public/images/favicon.ico')));
app.use(express.static('public'));

app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, '../templates'));
 
var server = app.listen(3000, function() {
    var host = server.address().address;
    var port = server.address().port;
    console.log('listening at http://%s:%s', host, port);
});

app.get('*', function(req, res){
    res.render("index.handlebars", {
        markup: 'Hello, SiamHTML!'
    });
});

จะเห็นว่าเป็นโค้ดธรรมดาๆ ที่เราน่าจะคุ้นเคยกันดีครับ ทีนี้มาต่อกันที่ไฟล์ index.handlebars ที่เราจะใช้เป็น template หลักกันบ้างครับ ให้เราใส่โค้ดด้านล่างนี้ลงไป

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Isomorphic with React Tutorial by SiamHTML</title>
</head>
<body>
    <div id="content">{{{markup}}}</div>
</body>
</html>

เสร็จแล้วก็ลองรันดูเลยครับ เราก็จะได้หน้าเว็บที่มีคำว่า “Hello, SiamHTML!” แสดงออกมา

ทีนี้เราจะเปลี่ยนการ assign ค่า markup จากการยัด string ลงไปตรงๆ มาเป็นการ render ด้วย component ของ React กันบ้างครับ ก่อนอื่นให้เราสร้าง component ที่ง่ายสุดๆ ขึ้นมาสักอัน แล้วตั้งชื่อไฟล์ว่า App.jsx ครับ

var React = require('react');

var App = React.createClass({
    render: function () {
        return (
            <div>
                Hello, SiamHTML!
            </div>
        );
    }
});

module.exports = App;

แต่อย่างที่บอกครับ สมัยนี้เค้านิยมเขียน ECMSScript 2015 กัน งั้นเราก็ตามเทรนด์ไปหน่อยละกันฮะ เราก็จะได้โค้ดสำหรับสร้าง <App /> ที่มีหน้าตาแบบนี้

import React from 'react';
 
class App extends React.Component {

    render() {
        return (
            <div>
                Hello, SiamHTML!
            </div>
        );
   }
}

export default App;

จะเห็นว่ามันแก้แค่นิดเดียวเองฮะ เอาเป็นว่าต่อจากนี้ไป ผมจะเขียน ECMAScript 2015 ตลอดเลยละกันนะครับ ใครยังไม่คุ้นกับ syntax ก็ลองไปอ่านทบทวนกันดูได้ฮะ

พอเราจะเปลี่ยนมาใช้ React เราก็จะต้องแก้โค้ดในส่วนของการ render ที่ไฟล์ server.js นิดนึงครับ แต่อย่าลืมนะครับว่าการ render ที่ฝั่ง server นั้น เราจะต้องอาศัย ReactDOMServer ด้วย ให้เราติดตั้ง dependency เพิ่ม ตามด้านล่างนี้ฮะ

npm install react react-dom --save-dev

จากนั้นก็เข้าไปแก้ไฟล์ server.js ตรงส่วนของ routing นิดนึงครับ

// โค้ดสำหรับสร้าง HTTP server

...

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './components/App.jsx';

app.get('*', function(req, res){
    res.render("index.handlebars", {
        markup: ReactDOMServer.renderToString(React.createElement(App))
    });
});

เมื่อลองรันดูอีกที ก็จะพบว่าผลลัพธ์ยังคงเหมือนเดิมเลยครับ เพียงแต่ตอนนี้เราเปลี่ยนมา render ด้วย React แล้ว

ทำ Routing

เมื่อ render ด้วย server ได้แล้ว ต่อมาก็จะเป็นการทำ routing ครับ ซึ่งในการทำ Isomorphic นั้น เราจะไม่ใช้ routing ของ Express นะครับ เพราะว่ามันไม่สามารถแชร์โค้ดกับฝั่ง client ได้ วิธีแก้ก็คือการใช้โมดูลที่มีชื่อว่า react-router ครับ

npm install react-router --save-dev

จากนั้นให้เราสร้างไฟล์ src/js/routes.jsx แล้วใส่โค้ดด้านล่างนี้ลงไป

import React from 'react';

// โหลดความสามารถของ react-router มาใช้งาน
import { Route, IndexRoute } from 'react-router';

// โหลด component ต่างๆ
import App from './components/App.jsx';
import Home from './components/Home.jsx';
import About from './components/About.jsx';

// ระบุว่า path นี้จะถูก handle ด้วย component ไหน
export default (
    <Route path="/" component={App}>
        <IndexRoute component={Home}/>
        <Route path="/about" component={About}/>
    </Route>
);

หากไล่โค้ดดู ก็คงจะเข้าใจได้ไม่ยากครับ ข้อสังเกตก็คือ โค้ด JSX ในส่วนของการกำหนด route นั้นสามารถซ้อนต่อกันไปได้เรื่อยๆ เลยนะครับ หาก path ของ <Route /> ไหน match สำเร็จ component ของ <Route /> เหล่านั้น ก็จะถูกนำมา render ตรงตามระดับที่เราซ้อนกันไว้ครับ จากโค้ดด้านบน หาก path เป็น /about สิ่งที่ render ออกมาก็จะเป็น <App /> ที่มี <About /> อยู่ข้างในครับ

ต่อมาให้เราเตรียม component ของแต่ละ route มาให้พร้อมเลยครับ ไฮไลท์นั้นอยู่ที่ไฟล์ App.jsx ครับ เพราะมันเป็น component ระดับสูงสุดของเรา ให้เราลบโค้ดเดิมใน App.jsx ออกให้หมด แล้วเปลี่ยนมาใช้โค้ดด้านล่างนี้แทนครับ

import React from 'react';

// โหลดความสามารถของ react-router มาใช้งาน
import { Link } from 'react-router';
 
class App extends React.Component {

    // ใส่ link ไปยังหน้า Home และ About
    render() {
        return (
            <div>
                <header>
                    <ul>
                        <li>
                            <a href="/">Home</a>
                        </li>
                        <li>
                            <a href="/about">About</a>
                        </li>
                    </ul>
                </header>
                {this.props.children}
            </div>
        );
   }
}

export default App;

สังเกตนะครับว่าที่ <App /> จะมีการใส่ props ที่ชื่อ children เข้ามาด้วย ซึ่งเจ้า props นี้เอง ที่เราจะเอาไว้ render component ลูก ที่ match กับ route ที่เราได้กำหนดไว้ฮะ อย่างที่ได้บอกไปว่าถ้า path เป็น /about react มันก็จะ render <About /> ลงไปยังตำแหน่งที่ this.props.children วางอยู่นั่นเองฮะ

มาดูที่ component ของหน้า Home กันบ้างครับ ให้เราสร้างเป็น component ง่ายๆ ไปก่อนแบบนี้ฮะ

import React from 'react';

class Home extends React.Component {
    render() {
        return (
            <div>
                Hello, SiamHTML!
            </div>
        );
    }
}

export default Home;

ส่วน component ขอหน้า About ก็เหมือนกันครับ ให้เราสร้างขึ้นมาแบบง่ายๆ เอาแค่กำหนดข้อความที่จะแสดงให้ตรงกับชื่อ component ก็ได้ครับ

เมื่อสร้าง route และ component ต่างๆ เสร็จทั้งหมดแล้ว ให้เราสร้างไฟล์ src/js/router.jsx ขึ้นมาเพื่อเป็น middleware สำหรับทำ routing ฝั่ง server ครับ ให้เราใส่โค้ดด้านล่างนี้ลงไป

import React from 'react';
import ReactDOM from 'react-dom/server';
import { RoutingContext, match } from 'react-router'

import routes from './routes.jsx';

export default function(req, res) {
    match({ routes, location:req.url }, (error, redirectLocation, renderProps) => {
        res.render("index.handlebars", {
            markup: ReactDOM.renderToString(<RoutingContext {...renderProps} />)
        });
    });
}

จากนั้นให้เรากลับไปที่ไฟล์ server.js เพื่อถอดโค้ด routing เดิม ที่ทำด้วย Express ออก แล้วเพิ่มโค้ดสำหรับเรียกใช้ middleware ที่เราเอาไว้คุยกับ react-router ลงไปแทน

// โค้ดสำหรับสร้าง HTTP server
 
...

import router from './router.jsx';

app.use(router);

เสร็จแล้วก็ลองรันดูเลยครับ ให้เราลองกดลิ้งค์ต่างๆ ดู ก็จะพบว่าหน้าเว็บสามารถ render ได้ตรงกับที่เราได้กำหนดไว้ใน routes.jsx แล้วล่ะครับ แถมพอกด view source ดูแล้วก็ยังพบว่าเนื้อหาของแต่ละ component นั้น ถูก render ออกมาด้วย ซึ่งเหมาะกับการทำ SEO สุดๆ ขั้นตอนต่อไป เราจะมาแปลงเว็บแอปของเราให้ได้ feel แบบ SPA ครับ

ใส่โค้ดสำหรับ Render ฝั่ง Client

ทีนี้เราจะมาทำให้เว็บแอปของเราให้สามารถ render ที่ฝั่ง client ได้ด้วยครับ พูดง่ายๆ ก็คือ เวลาเรากดลิ้งค์ไปหน้าอื่นแล้ว มันจะไม่โหลดใหม่ทั้งหน้าแล้ว แต่จะโหลดมาเฉพาะเนื้อหาส่วนที่เปลี่ยนไปเท่านั้น วิธีทำก็ไม่ยากเลยครับ ให้เราสร้างไฟล์ src/js/client.js แล้วใส่โค้ดด้านล่างนี้ลงไป

// โหลด React มาใช้งาน
import React from 'react'
import ReactDOM from 'react-dom';

// โหลดความสามารถของ react-router มาใช้งาน
import { Router } from 'react-router';
import createBrowserHistory from 'history/lib/createBrowserHistory';

// โหลด route ต่างๆ ที่เราได้กำหนดไว้
import routes from './routes.jsx';

// render ลงไปใน DOM ที่ #content
ReactDOM.render(
    <Router routes={routes} history={createBrowserHistory()} />, 
    document.getElementById('content')
);

หากลองไล่โค้ดดูจะเห็นว่าหลักการจะคล้ายๆ กับการใช้ react-router ที่ทางฝั่ง server เลยครับ แต่สังเกตนะครับว่าเราจะใช้ ReactDOM.render() แทน ไม่ได้ใช้ ReactDOMServer.renderToString เหมือนกับทางฝั่ง server แล้ว เนื่องจากครั้งนี้ เราจะ render เข้าไปใน DOM เลยครับ ส่วนสาเหตุที่ต้องมีการโหลด createBrowserHistory มาใช้งานด้วยก็เพราะเราต้องการจะลบ # ออกจาก url เวลามีการเปลี่ยนหน้านั่นเองฮะ

จากนั้น ให้เราเข้าไปแก้ไฟล์ src/js/components/App.jsx นิดนึงครับ ในส่วนของ <header> ให้เราแก้เป็นแบบนี้

<ul>
    <li><Link to='/'>Home</Link></li>
    <li><Link to='/about'>About</Link></li>
</ul>

จะเห็นว่าเราต้องแก้ลิ้งค์จากเดิมที่ใช้ <a /> มาเป็น <Link /> แทนครับ เพื่อเป็นการบอกว่าลิ้งค์นี้ เป็นลิ้งค์ที่เราต้องการจะให้ render ด้วยฝั่ง client นะ ส่วนจะลิ้งค์ไปไหนนั้น เราจะกำหนดด้วย props ที่ชื่อว่า to ซึ่งค่าที่กำหนดเข้าไปนั้นก็คือ path ของ Route ที่เราได้กำหนดเอาไว้ในไฟล์ src/js/routes.jsx นั่นเองฮะ

รวมไฟล์ JavaScript ด้วย Webpack

มาถึงตรงนี้ จะเห็นว่าการ render ของทั้ง 2 ฝั่ง นั้น มีการแชร์โค้ดกันถูกมั้ยครับ ทั้งส่วนของ route และส่วนของ component เลย ซึ่งการจะแชร์กันได้นั้น เราจะต้องเขียน JavaScript เป็นโมดูลแบบ CommonJS ครับ สำหรับฝั่ง server นั้นสบาย เพราะ Node.js รองรับโมดูลแบบนี้อยู่แล้ว แต่ฝั่ง client มันไม่รู้จักหรอกครับ เราเลยจะต้องแปลงโค้ดทั้งหมดที่เราเขียนให้สามารถรันบน browser ได้ซะก่อน ซึ่ง tool ที่ผมชอบใช้นั้นจะเป็น webpack ครับ ให้เราติดตั้งโมดูลเพิ่มตามด้านล่างนี้

npm install webpack -g
npm install webpack babel-loader --save-dev

จากนั้นให้เราสร้างไฟล์ webpack.config.js ขึ้นมา แล้วใส่โค้ดด้านล่างนี้ลงไป

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

module.exports = {

    // ให้ webpack เริ่มรวมโค้ดที่ไฟล์ client.js
    entry: path.resolve(__dirname, 'src/js/client.js'),

    // แล้วตั้งชื่อไฟล์ output ว่า bundle.js
    output: {
        path: path.resolve(__dirname, 'public/js'),
        filename: 'bundle.js'
    },

    // อ่านไฟล์นามสกุล .js, .jsx ด้วย Babel
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            }
        ]
    }
};

สิ่งสำคัญก็คือการกำหนด entry point ครับ ให้เราเลือกไปที่ไฟล์ src/js/client.js เพราะไฟล์นี้ถือเป็นจุดเริ่มต้นของโค้ดสำหรับ render ฝั่ง client (เราจะกำหนด entry point ไปที่ไฟล์เดียว เนื่องจากไฟล์นั้นมันจะไป import โมดูลอื่นๆ ต่อกันไปเป็นทอดๆ อยู่แล้ว)

จากนั้นก็เข้าไปเพิ่มโค้ดสำหรับโหลด bundle.js มาใช้ ที่ src/templates/index.handlebars ครับ

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Isomorphic with React Tutorial by SiamHTML</title>
</head>
<body>
    <div id="content">{{{markup}}}</div>
    <script src="/js/bundle.js"></script>
</body>
</html>

จากนั้นก็รัน webpack ได้เลยฮะ

webpack

เสร็จแล้วก็ลองรันหน้าเว็บดูอีกทีครับ เราจะพบว่าเวลากดลิ้งค์มันจะไม่โหลดใหม่ทั้งหน้าแล้ว ในที่สุดเราก็ทำสำเร็จครับ!

แนวทางการเขียน React

จาก workshop เมื่อกี้นี้ สิ่งที่เราทำไปมันก็แค่โครงสร้างของเว็บแอปแบบ Isomorphic เท่านั้นเองนะครับ T__T ซึ่งขั้นตอนหลังจากนี้ ถึงจะเป็นการเริ่มลงมือสร้าง component ที่เราต้องการจะใช้จริงๆ ขึ้นมาด้วย React ครับ แต่ก่อนจะเริ่มเขียน React เรามาดูวิธีคิดแบบใหม่ ที่เราจะต้องทำความเข้าใจให้ถ่องแท้กันดีกว่า

1. แบ่งหน้าเว็บออกเป็น Component ย่อยๆ ก่อน

ขั้นตอนแรก ให้เราพยายามมองหน้าเว็บออกเป็น component ย่อยๆ ให้ได้ก่อนครับ ส่วนจะใช้เกณฑ์อะไรในการดูว่าส่วนไหนเป็น component บ้าง ผมแนะนำให้แบ่งย่อยเข้าไปถึงระดับที่สิ่งนั้นสามารถอยู่ได้โดยอิสระ โดยที่ไม่ได้ขึ้นอยู่กับ component ใดๆ แล้วครับ

หากเขียน React มาถึงจุดๆ หนึ่ง เราจะมองออกครับว่าจริงๆ แล้ว เจ้า component มันก็คือ Class ดีๆ นี่เอง ตามหลักของ Single Responsibility Principle แล้ว Class ควรจะมีหน้าที่เพียงหนึ่งเดียวเท่านั้นครับ ดังนั้น ให้เราซอย component ให้เล็กลงเรื่อยๆ จนแต่ละ component เหลือเพียงหน้าที่เดียวเท่านั้น

2. ประกอบ Component เหล่านั้น ให้แสดงผลออกมาได้

พอได้ component มาทั้งหมดแล้ว ให้เราเขียนโค้ด React เพื่อประกอบ component เหล่านั้นเข้าด้วยกันครับ ในขั้นตอนนี้ เราจะใช้แต่ props เท่านั้นในการส่งข้อมูลให้กับ component ที่เป็น parent แล้วก็ให้มันส่งต่อไปยัง component ลูกไปเรื่อยๆ ส่วนข้อมูลที่จะส่งนั้น ขอให้ mock ขึ้นมาเองได้เลยครับ พอทำเสร็จแล้ว เราจะได้เว็บหน้าตาเหมือนจริงเลย เพียงแต่มันจะยังเป็นแค่ static version คือจะยังไม่มีการโต้ตอบใดๆ ครับ

3. ดูว่าควรเก็บอะไรไว้ใน State บ้าง

มาถึงขั้นตอนที่น่าจะยากที่สุดครับ นั่นก็คือการมองให้ออกว่าข้อมูลอะไรควรเก็บไว้ใน state บ้าง มีหลักง่ายๆ ก็คือ ถ้าข้อมูลมีลักษณะแบบนี้ เราจะไม่เก็บไว้ใน state ครับ

  • ถ้า parent สามารถส่งมาให้ได้
  • ถ้าข้อมูลนั้นไม่ได้เปลี่ยนแปลงไปตามกาลเวลา
  • ถ้าข้อมูลนั้นสามารถคำนวณได้จาก state หรือ props อื่นๆ

บางคนอาจสงสัยว่าทำไมเราต้องมาคิดด้วยว่าอะไรไม่ควรอยู่ใน state เก็บๆ ไปเลยไม่ได้หรอ ? คำตอบคือมันมีผลต่อ performance อะครับ เราควรจะเก็บเฉพาะข้อมูลที่มีขนาดเล็กมากๆ เท่านั้น เพราะหน้าที่จริงๆ ของ state ก็คือการเอาไว้ switch UI ตาม event ต่างๆ ที่เกิดขึ้นเท่านั้นเองครับ

4. ดูว่าควรใส่ State ให้กับ Component ไหน

ตอนนี้เรารู้แล้วว่าข้อมูลอะไร ที่ต้องเก็บไว้ใน state บ้าง ต่อมาเราจะต้องมองให้ออกครับว่าเราควรจะกำหนด state ให้กับ component ไหน ขั้นตอนนี้ก็ยากมากเหมือนกันครับหากเราเริ่มเขียน React ใหม่ๆ แต่เราก็มีหลักง่ายๆ ดังนี้ครับ

  • ดูว่ามี component ไหนบ้าง ที่การ render ของมันขึ้นอยู่กับ state
  • หา component ที่เป็น parent ของ component เหล่านั้น แล้วใส่ state ให้มัน
  • หากไม่มี component ที่เป็น parent ของ component เหล่านั้นเลย ให้สร้าง component ใหม่มาครอบ component เหล่านั้น แล้วค่อยใส่ state ให้มัน

พูดง่ายๆ ก็คือ ในการเขียน React นั้น เราจะใช้วิธีใส่ state เอาไว้ที่ตัวแม่ แล้วให้มันส่งข้อมูลที่จำเป็นไปยังลูกๆ ผ่าน props ครับ ลองนึกดูว่า หากเราไปใส่ state ที่ลูก แล้วลูกตัวอื่นๆ ที่ต้องการจะใช้ข้อมูลจาก state จะรู้ได้อย่างไรว่า state ได้เปลี่ยนไปแล้ว ? อย่าลืมว่า state นั้นจะอยู่แค่ภายใน component นั้นๆ เท่านั้นนะครับ

แนวทางศึกษาเพิ่มเติม

มาถึงตรงนี้ ผมคิดว่าพื้นฐานของเราโอเคในระดับหนึ่งแล้วล่ะครับ ที่เหลือก็แค่ฝึกซ้อมจนเขียน React ได้คล่องเท่านั้นเอง เวลาเห็น design มางี้แล้วต้องรู้เลยว่าจะต้องซอยอะไรออกมาเป็น component บ้าง ต้องใส่ state ไว้ตรงไหน ตรงนี้ต้องมีชั่วโมงบินสูงๆ ถึงจะทำได้ครับ หรือถ้าใครอยากเป็นเร็ว ผมขอแนะนำให้ลองติดตั้ง React Developer Tools ดูครับ แล้วเราจะรู้เลยว่าแต่ละเว็บที่ใช้ React ทำนั้น เค้ามีการออกแบบ component อย่างไร ใส่ state อะไร เอาไว้ที่ ไหนบ้าง

ผมขอสารภาพให้เจ็บใจเล่นๆ นิดนึงนะครับว่า หากจะเอา Isomorphic ไปใช้กับ production จริงๆ แล้ว เราจะไม่เขียนโค้ดในรูปแบบที่เราเขียนกันในบทความนี้หรอกครับ เพราะมันเป็นแค่โค้ดพื้นฐานสุดๆ ที่ทำให้ Isomorphic ทำงานได้เท่านั้นเอง (อารมณ์เหมือนเขียน PHP เพียวๆ ในช่วงหัดเขียนแรกๆ แต่สุดท้ายก็เปลี่ยนไปใช้ framework อยู่ดี) ซึ่งถ้าผมเอารูปแบบการเขียนโค้ดที่เค้าใช้กันจริงๆ มาอธิบายด้วย บทความนี้ก็คงจะยาวกว่านี้อีกเยอะเลยล่ะครับ ก็ถือซะว่าเนื้อหาในบทความนี้เป็นก้าวแรกในการศึกษาเรื่อง Isomorphic แล้วกันนะครับ ^0^

หากใครจะมาเอาดีทางด้าน Isomorphic หรือจะลองนำไปใช้กับ production ดู ผมขอแนะนำให้ไปศึกษาเรื่องพวกนี้ต่อเลยครับ

  • ECMAScript 2015 project ที่เกี่ยวกับ Isomorphic ต่างๆ บน GitHub มักจะเขียนด้วย ECMAScript 2015 ครับ ถ้าเราไม่เป็นเลย รับรองว่าไล่โค้ดคนอื่นลำบากแน่ๆ
  • Flux แล้วส่วนใหญ่เค้าก็จะใช้ Flux เป็น pattern ในการเขียน React กันครับ ลองไปหัดเล่นดูนะครับ แล้วจะพบว่ามันทำให้การเขียน React นั้นเข้าใจง่ายขึ้นเยอะเลย
  • Redux เมื่อเข้าใจหลักการของ Flux แล้ว ผมแนะนำให้ลองเล่น Redux ดูครับ เพราะมันเป็น library ที่นำ Flux ไปต่อยอดให้ดียิ่งขึ้น ซึ่งจริงๆ library ที่นำ Flux ไปต่อยอดนั้น มีเยอะมากๆ เลยนะครับ แต่ที่ผมแนะนำ Redux ก็เพราะว่า docs ของมันค่อนข้างจะโอเคแล้ว แล้วคนที่ใช้ก็ถือว่าเยอะพอสมควรเลยครับ ในชั่วโมงนี้ถือว่าเป็นตัวที่น่าเล่นที่สุดแล้ว

ความรู้สึกหลังลองทำเว็บแบบ Isomorphic

ผมมองว่า Isomorphic คืออนาคตของเว็บนะครับ ข้อดีผมพูดไปหลายครั้งแล้ว ขอพูดถึงข้อเสียมั่งละกัน หลักๆ เลยก็คือ learning curve ที่แอบสูงนิดๆ ครับ อาจจะเป็นเพราะมันยังใหม่อยู่ด้วยมั้ง เราเลยยังไม่ค่อยเจอบทความไหนที่อ่านแล้วเข้าใจง่ายๆ เท่าไร

ส่วนข้อเสียอีกเรื่องซึ่งผมเพิ่งเจอมาหมาดๆ เลยก็คือ แต่ละส่วนในการทำ Isomorphic มันพัฒนาแยกกันฮะ คือวันดีคืนดี หากเราอัพเดท dependency ที เราอาจจะพบว่าโค้ดเดิมของเราใช้งานไม่ได้ซะละ หรือบางทีเราพบว่าบางโมดูลมี bug แล้วการจะแก้ bug นี้ได้ ต้องอัพเดทเวอร์ชั่นใหม่ ที่ syntax มันเปลี่ยนยกเครื่องเลย นี่เป็นสิ่งที่เราพบเจอได้บ่อยๆ ในการทำ Isomorphic ณ วันนี้ฮะ ก็ชั่งใจเอาเองนะครับว่างานของเรามันเหมาะกับ tool ตัวไหน จำเป็นต้องแคร์เรื่อง SEO มั้ย ? ส่วนตัวผมก็ใช้เกณฑ์นี้แหละ เป็นตัวตัดสินใจว่าจะใช้ AngularJS หรือ React ดี

แต่อย่าเพิ่งคิดว่าโลกของเว็บหลังจากนี้ React จะผูกขาดนะครับ เพราะกระแสของ Angular 2 นั้นเริ่มจะมาแล้ว แถมในเวอร์ชั่นใหม่นี้ยังสามารถทำ server-rendering ได้แล้วด้วย ประเด็นเรื่อง SEO จึงตัดออกไปได้ฮะ อีกตัวที่ทำได้(นานแล้ว) ก็จะเป็น Meteor ครับ ก็ลองไปศึกษากันต่อดูนะครับ ว่า tool ตัวไหนมันเหมาะกับงานของเรา

ก็ต้องทำใจกันหน่อยนะครับ สำหรับชาว Front-end ทั้งหลาย ที่ต้องคอยเรียนรู้อะไรใหม่ๆ อยู่ตลอดเวลา บ่อยครั้งที่เรารู้สึกว่าพอใช้ตัวนี้จนเก่งแล้ว ตัวใหม่ที่ดีกว่าดันออกมาอีกละ เอาเป็นว่าผมจะคอยช่วยกรองมาให้ชั้นนึงก่อนแล้วกันนะครับว่า tool ตัวไหนดูดี มีอนาคต SiamHTML จะรีบนำมารีวิวให้ได้อ่านกันก่อนใครแน่นอน แล้วพบกันใหม่บทความหน้าครับผม

]]>
http://www.siamhtml.com/build-isomorphic-apps-with-react/feed/ 15