ทำไมถึงมาทำ Infra?

วันนี้มีน้องถามว่า “ทำไมถึงมาทำ Infra?”

ภาษาแรกที่เขียนจริงจังคือ PHP ซึ่งสมัยนั้น shared hosting เป็นที่นิยมมาก จนกระทั่งประมาณปี 2008 ที่ Ruby on Rails กำลังเริ่มมามากๆ ก็เลยตัดสินใจเรียน Python (ตึ่งโป๊ะ)

เหตุผลที่เลือกเรียน Python ตอนนั้นคือ

  1. PHP Framework ยังไม่มีตัวไหนถูกใจ (ตอนนั้นตัวที่นิยมน่าจะมี Yii กับ CakePHP) ก็ต้องรออีกหลายปีก่อนที่ Laravel จะมา
  2. ตอนนั้นเขียนโค้ดห่วยมากเลยคิดว่าการเขียน Python จะบังคับให้เราหัด coding style ให้เรียบร้อย

ตอนนั้นก็เริ่มเห็นแล้วว่าเราจะเอา Python ไปลง shared hosting ไม่ได้ ก็คงต้อง manage server เองได้แล้ว โชคดีว่าที่บ้านสนับสนุนให้มี home server ด้วย ก็เลยมีเครื่องให้ใช้ เพราะยุคนั้นยังไม่มี VPS ราคาถูกแบบ DigitalOcean ในปัจจุบัน

เชื่อว่าเหตุผลนี้ก็ทำให้หลายคนเข้ามาสนใจ infra เพราะว่าทำของเสร็จแล้วจะต้อง deploy ก็เลยต้องมาหัด

Unlock your design

การที่เราทำ infra เป็นนี่มันมี benefit คือมันปลดล็อคข้อจำกัดในการ design ระบบของเรา

สมัยนั้นเราอาจจะเคยได้ยินเทคโนโลยีล้ำๆ แต่มันรู้สึกเป็นของขึ้นหิ้งจับต้องไม่ได้เลย เช่น PostgreSQL, Memcache, RabbitMQ, Riak, Sphinx เป็นต้น พวกนี้เคยได้ยินชื่อมาแล้วก็ได้แต่มองไม่มีโอกาสได้ใช้

พอเรา deploy เองเป็นแล้วมันก็ช่วยปลดล็อคข้อจำกัดออกไป​ ถ้างานที่ใช้มันจำเป็นจะต้องมี Memcache เราก็ลง Memcache เองเลย ไม่ใช่ว่าต้อง workaround เช่นไปทำ filesystem cache แทน ซึ่งทั้งช้ากว่า, เสียเวลาเขียนและไม่ scale

Story

อีกอย่างหนึ่งที่ชอบในวงการ infra คือ product หลายตัวมักจะมี story เหมือนอาหารญี่ปุ่นที่ชอบมี story นั่นแหละ 555

ยกตัวอย่างเช่น Kubernetes เกิดมาจากประสบการณ์ manage container มาเป็นสิบปีของ Google เช่น design ของ Kubernetes ที่เอา pod ไว้ไหนก็ได้นั้นมาจากเคล็ดลับความสำเร็จของ Google ที่ทุกเครื่องทำหน้าที่เหมือนกัน ทดแทนกันได้หมด ทำให้สามารถ utilize เครื่องได้เต็มประสิทธิภาพมากขึ้น

หรือแม้แต่โปรแกรม ls ถ้าเราไปอ่านดูว่าทำไม GNU ถึงสร้าง ls ทำไมก็จะมี story เรื่องการต่อสู้ของ Stallman

ถ้าเทียบกับในโปรแกรมมิ่งแล้วเรามักจะสนใจมากกว่าว่าภาษานี้ ไลบรารี่นี้ทำอะไรได้ ตอนนี้ยังไม่รู้เลยว่าทำไม Guido สร้าง Python ขึ้นมา

คิดว่าเหตุผลที่มันเป็นแบบนี้เพราะว่า

  1. ของใน Infra หลายๆ ครั้งมักจะเกิดจากการ open source ของภายในองค์กรมา เช่น Hadoop, Cassandra, Redis, Memcache, Kubernetes มันเลยเกิดมาเพื่อ solve ปัญหาใหญ่ขององค์กรนั้นๆ
  2. เราจำเป็นต้องศึกษา story ของ tool นั้นๆ ด้วย เพราะ tool แต่ละตัวมันมีปัญหาที่เค้าอยากแก้จึงสร้าง tool นั้นขึ้นมา และด้วยความเป็น tool เอง use case ของมันก็มักจะจำกัดไม่นอกเหนือไปจากนั้นมาก ต่างกับ library ที่มันมักจะสามารถใช้งานได้อย่างกว้างขวาง

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

Boredom

สุดท้ายเรารู้สึกว่าจริงๆ ธรรมชาติเราเป็น backend dev อย่างที่บอกตั้งแต่ต้นว่าที่มาสนใจ infra เพราะความจำเป็นที่จะปลดล็อคความสามารถในการเอา tool ที่มีอยู่แล้วมาใช้งาน

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

Emotion CSS SSR ทำงานอย่างไร?

ผมเป็นแฟน Emotion CSS มาสักพักแล้ว รู้สึกว่ามัน clean กว่าตัวเลือกอื่นๆ มาก โดยเฉพาะว่ามันไม่ผูกกับ React เลยรู้สึกเหมือนเขียน style ก่อน แล้วเอาไปใส่ component ไม่ใช่ component ที่ถูก style มาแล้ว (พูดง่ายๆ คือมันรู้สึกเหมือนเขียน old school CSS มากกว่า )

แต่ Emotion กลับชอบเปลี่ยน core บ่อยมาก

  • Emotion 5 ใช้ CSS parser ที่ดึงมาจาก styled component แล้ว insertStyle เอง
  • Emotion 6 ใช้ glam
  • Emotion 8 ใช้ stylis.js

วันนี้อยากจะพูดถึงสักหน่อยว่า Emotion มันทำ server side rendering อย่างไร เนื่องจากว่าก่อนหน้านี้เพิ่งส่ง pull request เข้าไปแก้บั๊กเกี่ยวกับ SSR

Emotion 9

ใน Emotion 9 ฟังค์ชั่น css จะ return string cache_key-name ทำให้เราสามารถใช้ className={...} ได้ ซึ่งปกติ cache.key ก็จะตั้งเป็น css ทำให้เราจะเห็นเว็บที่ใช้ emotion ใช้ class ประมาณว่า css-1fe12ej

จากนั้น เวลาเราเรียก renderStylesToString ใน server side rendering มันจะใช้ regex <|${cssKey}-([a-zA-Z0-9-_]+) match เข้าไปยัง HTML ที่ได้จาก React renderToString

จะเห็นว่า Regex นี้ match ได้สองแบบคือ

  1. ตัว < (open tag)
  2. cache_key-name ตามรูปแบบที่ css สร้างไว้

เมื่อพบข้อความที่คล้าย class name แล้ว มันจะทำ list ไว้จนกระทั่งพบตัว < ถัดไป พอพบแล้ว ที่ตัว < ก่อนหน้าจะถูกแทรก <style> tag ที่มี definition ของ class เหล่านั้นลงไป เราจึงจะเห็นว่าในเว็บที่ใช้ emotion จะมี <style> tag อยู่จำนวนมาก แต่จะแทรกอยู่หน้าครั้งแรกที่ใช้ครั้งเดียว ไม่ใช่เป็นก้อนใหญ่ๆ ไว้ในหัวเว็บ

วิธีนี้แน่นอนว่าไม่ใช่ foolproof เพราะผมเองก็เคยเจอบั๊กที่ว่าเว็บมี <a href="...-css-programmer"> แล้ว emotion เลย lookup id programmer ทำให้เกิด <style>undefined</style> ซึ่งก็แก้ไปใน pull request ที่ส่งเข้าไปแล้ว

หรืออีกวิธีหนึ่งที่เทสได้คือลองพิมพ์ css-... ที่ตรงกับ class ที่มีอยู่จริงลงในหน้าเว็บ ก็จะปรากฏว่ามันจะแทรก css ชื่อนั้นลงไปหน้า tag นั้นด้วย ตรงนี้คิดว่าไม่อันตรายถึงขั้น security (เพราะแค่แทรก style tag จาก style ที่มีอยู่จริงในเว็บ) แต่ก็ทำให้เว็บมันรกๆ ได้

<style data-emotion-css="1fjv9nj">.css-1fjv9nj{color:#36629e;}</style><li>ugc textcss-1fjv9nj</li>

ความเจ๋งของเทคนิคทั้งหมดนี้คือ Emotion จึงไม่ได้ผูกกับ React หรือ library ใดเลย เพราะจะสังเกตว่าทุกอย่างทำงานอยู่บน string ทั้งหมด จะใช้กับ HTML อย่างเดียวก็ได้

Emotion 10

Emotion 10 มาพร้อมกับฟีเจอร์ใหม่ Zero configuration serverless ซึ่งตอนนี้น่าจะเป็นเจ้าเดียวที่มี มันทำได้ยังไง?

Emotion 10 เปลี่ยน API ของ css ออกไป โดยจะ return เป็น object แทน จากนั้นจะเห็นว่า Emotion 10 บังคับให้ลง babel plugin หรือ เขียน /** @jsx jsx */ ไว้ที่หัวไฟล์

สิ่งที่ directive นี้ทำคือเปลี่ยนวิธีที่ Babel compile React component ใหม่ จากเดิมคือ

<div css="example">body</div>

React.createElement(
    "div"
    {css: "example"},
    "body"
)

จะเปลี่ยนเป็น

jsx("div", {css: "example"}, "body")

ซึ่งจะเห็นว่าเหมือนกับของเดิม แค่เปลี่ยน function เป็น jsx เท่านั้น แล้วในโค้ดเราจะต้อง import {jsx} from "@emotion/core" มาด้วย

เมื่อลองไล่โค้ด jsx ดูก็จะเห็นว่ามันครอบ React.createElement จริงไว้อีกทีหนึ่ง โดยถ้า component มี prop ชื่อ css มันจะแอบสลับ component ให้เป็น <Emotion __EMOTION_TYPE_PLEASE_DO_NOT_USE__="div" css="example">body</Emotion>

ท่านี้คนเขียน React อาจจะคุ้นเคยถ้าเรียกมันว่า Higher order component ซึ่งอาจจะเขียนได้แบบนี้

function jsx(Element){
    return (...props) => {
        if(!props.css){
            return <Element {...props} />;
        }

        return <Emotion __EMOTION_TYPE_PLEASE_DO_NOT_USE__={Element} {...props} />
    }
}

แล้วเอา HOC นี้ไปครอบทุก element ในหน้า รวมถึง primitive element ด้วย Emotion จึงเลือกใช้การเปลี่ยน React.createElement แทนที่จะใช้ HOC ปกติเพื่อความสะดวก

ทีนี้ <Emotion /> ทำงานยังไง?

ซอร์สโค้ดของ component นี้อยู่ในไฟล์เดียวกัน ก็มีส่วนซับซ้อนมากมาย แต่ส่วนที่เราสนใจคือส่วนที่มันทำให้ zero config server side rendering ได้

วิธีการที่มันทำก็น่าสนใจมาก โดยใช้ React Fragment ที่เพิ่งมาใหม่ (แต่ก็เก่าแล้ว)

return (
  <React.Fragment>
    <style
      {...{
        [`data-emotion-${cache.key}`]: serializedNames,
        dangerouslySetInnerHTML: { __html: rules },
        nonce: cache.sheet.nonce
      }}
    />
    {ele}
  </React.Fragment>
)

พูดง่ายๆ ก็คือ <Emotion /> นั้นก็เป็น HOC อีกชั้นหนึ่งที่จะครอบ component จริงไว้คู่กับ <style> tag ใน React Fragment (และ inject className เข้าไป) หรือเป็นโค้ด HOC อาจจะแบบนี้

function Emotion({css, __EMOTION_TYPE_PLEASE_DO_NOT_USE__, ...props}){
    let rules = getCssString(css);
    let className = getCssClassName(css);
    let Element = __EMOTION_TYPE_PLEASE_DO_NOT_USE__;
    return (
        <>
            <style dangerouslySetInnerHTML: { __html: rules } />
            <Element {...props} className={className} />
        </>
    )
}

ด้วยการใช้ Fragment ทำให้ Emotion 10 ทำ server side rendering ได้ โดยไม่ต้องแก้โค้ดฝั่ง server ใดๆ

Tradeoff

ก็เสียดายว่าท่าพวกนี้นั้นกลับกลายเป็นว่า Emotion 10 ผูกติดกับ React แน่นมาก

ใน Docs ของ Emotion เองแนะนำให้ migrate ไปใช้ API ใหม่นี้ทั้งหมด แต่ก็เขียนไว้ว่าเฉพาะถ้าคุณใช้ React นะ ก็ยังไม่รู้ว่าในอนาคต API เดิมจะยังมีการพัฒนาต่อหรือไม่ ตอนนี้ใน GitHub เองก็ยังมีทั้ง package ของ API เก่าและใหม่อยู่ด้วยกัน