js

A 18-post collection

JS: Web Server Testing with "chai-http"

We're going to test a web api using GET, POST using chai-http by actual http request.

chai is not a test runner, it's just a javascript testing framework (aka. asserter). If you are looking for a test runner go see mocha

Note: You should install mocha globally (npm install -g mocha) and install chai locally.

To get started, install chai and chai-http:

npm install --save-dev chai chai-http  

You should have your destination web server in place because I will show only the test part, here it is:

const chai = require('chai')  
const chaiHttp = require('chai-http')  
chai.use(chaiHttp)

describe('web server test', () => {  
    it('test post request with file upload', (done) => {
        chai.request('http://....:...')
            .post('/api/post/test')
            .attach('image_field', img, 'upload.png')
            .end((err, res) => {
                if (err) {
                    console.error(err)
                } else {
                    const body = res.text
                    chai.expect(body).to.be.equal(....)
                    done()
                }
            })
    })
})

To not underestimate my reader's ability, I feel like there is no need to explain it in details. There is so much more to discover, yet beginning from reading body response from the request, I think, is a good start !

อ่านต่อ »

Firebase.off() Recursively for All Listeners

หากท่านเคยใช้ firebase ท่านก็มักจะคุ้นชินกับการใช้ method ref.on('..', callback); ของ firebase ซึ่งเป็นวิธีการหลักในการอ่านค่าจาก database ของ firebase ซึ่งอาศัยวิธีการสร้าง listener นั่นเอง

มันมักจะมีเหตุการณ์เสมอที่เราต้องการจะ "ปิด" listener เหล่านี้ ซึ่ง firebase มีเครื่องมือสำหรับสิ่งนี้ก็คือการเรียก ref.off() สำหรับการปิดทุก ๆ listener ทีอยู่บน ref นี้

ปัญหาก็เกิดขึ้นเมื่อ firebase ไม่มี method สำหรับ "ปิดทุก ๆ listener" นั่นก็คือเราไม่สามารถ เรียก rootRef.offRecursively() อะไรลักษณะนี้ได้

ในทีนี้ผู้เขียนต้องการใช้ฟังก์ชันดังกล่าวอย่างมาก จึงเสนอวิธีการแก้ปัญหาดังกล่าวด้วยการเขียน wrapper function ขึ้นมาดังนี้

ไฟล์: firebaseWrapper.js

let allRefs = [];

// all listener must be wrapped with this function
// for properly allOff function
export function wrap (ref) {  
  allRefs.push(ref);
  console.log('allRefs:', allRefs);
  return ref;
};

export function allOff () {  
  allRefs.forEach((ref) => ref.off());
  allRefs = [];
};

ดังนั้นเมื่อใดก็ต้องที่เราต้องการ ใช้ method พวกที่จะสร้าง listener เช่น .on('...', callback) เราจะต้อง wrap มันด้วย wrapper function ที่เราเพิ่งเขียนขึ้นมาดังนี้

import {wrap, allOff} from './firebaseWrapper';  
import Firebase from 'firebase';

const fireRef = new Firebase('....');

wrap(fireRef).onAuth(....);

const userRef = fireRef.child('users');

wrap(userRef).on('value', callback);

แต่ละครั้งที่เราเรียก wrap มันจะจำว่า มี ref ตัวไหนบ้างที่เราเรียกใช้ไปแล้ว ซึ่งจะทำให้ allOff() สามารถเรียก off() ได้ครบทุกตัวที่เกี่ยวข้อง

อ่านต่อ »

Angular's Directive Wrapper (ES6)

ความเดิมตอนที่แล้วที่ผมแนะนำวิธีการเขียน Angular Directive ด้วย ES6 นั้น ... ผมพบว่าตัวผมเอง ผิดถนัด และตอนนี้ผมได้เตรียมตัวมาแก้ตัว เอาเป็นว่าเป็นความพยายามครั้งที่สองของผมในการทำให้การใช้งาน directive มีเหตุผลมากขึ้น อ่านง่ายขึ้น เขียนง่ายขึ้น จัดการง่ายขึ้นแล้วกันนะครับ

โค้ดหลักอยู่ที่ https://github.com/phizaz/angular-directive-wrapper

วิธีการของผมก็คือผมสร้างไฟล์ directive.js ขึ้นมาเพื่อเป็น Singleton สำหรับจัดการงานเกี่ยวกับ สร้าง directive (โดยเฉพาะ isolated scope ซึ่งเป็นเป้าหมายหลักของการใช้งาน; ใครใช้ shared-scope โปรดเลือกเขียนตามใจชอบ ๕๕๕) เพราะว่าโดยปกติ directive ประกอบไปด้วย controller และ link ซึ่งไม่ได้ทำงานพร้อมกัน และไม่ได้มีเป้ามหายเหมือนกันซะทีเดียว แต่ส่วนตัวแล้วผมเห็นว่ามันเป็นการยากโดยใช่เหตุที่จะต้องแยกสองสิ่งเหล่านี้ออกจากกันโดยสิ้นเชิง และผมยังเห็นอีกว่าเมื่อ directive ของเราใหญ่ขึ้น ๆ การจัดการให้โค้ดต่าง ๆ อยู่ในร่องในรอยก็ทำได้ยากขึ้น จึงเสนอวิธีจัดการโค้ดออกมาจากใน directive.js นี้ด้วย

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

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

ตัวอย่างการใช้งาน

import angular from 'angular';  
//importing directive.js
import Directive from './directive';

angular  
  .module('TestDirectiveModule', [])
  .directive('Test',
    () => {

      return Directive.new({

        controllerAs: 'my',
        template: '<test></test>',

        // this replaces the normal 'scope'
อ่านต่อ »

Angular's Directive ES6 Era

ปกติ directive ที่เขียนอยู่ใน AngularJS official documentation จะแนะนำให้เราเขียนแบบนี้

var app = angular.module('app', [])  
app.directive('directiveName',  
  function () {
    return {
      restrict: 'E',
      // isolate scope
      scope: {},
      controller: function () {
        ...
      },
      controllerAs: 'loginBox',
      link: function ($scope, element, attrs) {
        ...
      },
      template: '<div>template here</div>',
    };
  });

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

import _ from 'lodash';  
import angular from 'angular';

class SomeDirective {  
  constructor() {
    _.extend(this, {
      restrict: 'E',
      // create its isolate scope that will not interfere with
      // the outside world
      // scope is equivalent to `this` in the class
      scope: {},
      // always use bindToController
      // so that the code will work as expected
      bindToController: true,
      // this is var's name to be used in template
      // to talk about controller's `this` or scope
      controllerAs: 'ctrl',
      template: loginBoxTemplate,
    });
  }

  controller() {
    ...
  }

  link($scope, element, attr) {
    ...
  }
}

let app = angular.module('app', []);  
app.directive('directiveName', () => new SomeDirective());  

โดยภาพรวมแล้วโค้ดอันใหม่ทำงานเหมือนโค้ดอันเก่าทุกประการ แต่ว่าตอนนี้โค้ดของ directive ทั้งก้อนรวมเป็นก้อนเดียวกันแล้ว ในรูปที่เป็นระเบียบมากขึ้น คือ class

ที่ต้อง new SomeDirective() ด้านล่างก็เพราะว่า directive คาดหวังว่าจะได้ function ที่จะถูกรันด้วย การ execution ไม่ใช่การ new ดังนั้นก็ต้อง new ให้เสียก่อน

อ่านต่อ »

Adding babel-polyfill with Browserify

ปกติเราใช้ babel เมื่อเราต้องการใช้ฟีเจอร์บางอย่างของ es6 (อ่านเพิ่ม) ซึ่งวิธีการทำงานของ babel ก็คล้าย ๆ กับ coffeescript ซึ่งก็คือแปลงนั้น ๆ กลับมาเป็น javascript ES5 ให้สามารถรันได้ทาง web browser ทั่ว ๆ ไป (ไม่ใช่ทุก browser จะรองรับ es6 ในปริมาณที่เท่ากัน) ซึ่งในเว็บไซต์ documentation ของ babel เองก็จะระบุไว้ว่าแต่ละฟีเจอร์นั้นจะต้อง setup babel ไม่เหมือนกันบางฟีเจอร์อาจจะต้องการการติดตั้งมากกว่าอันอื่นเล็กน้อย บางอันอาจจะต้องอาศัยโปรแกรมอื่นช่วยเช่น คำสั่ง import ที่ต้องอาศัยเครื่องมือตัวกลางมาคอย require ไฟล์ต่าง ๆ ให้เพราะว่า browser babel ไม่ได้ใส่เครื่องมือพวกนี้มาให้

สถานการณ์เป็นแบบนี้ครับ ผมติดตั้ง babel (babelify) ร่วมกับ browserify ที่จริงก็มี watchify (แต่ว่าไม่สำคัญในที่นี้เท่าไหร่) และเพราะการตั้งค่าไม่ครบถ้วนของผมทำให้ babel แปลโค้ดให้ผมไม่ได้เป็นภาษา es5 ที่รองรับทุก ๆ browser และส่งผมให้โค้ดของผมบึ้มในหลาย ๆ browser บึ้มแบบไม่รู้ว่าผิดตรงไหนเลยล่ะ ฮ่ะ ๆ

ผมลองใช้ for .. of loop ซึ่งมาพร้อมกับ es6 มีลักษณะแบบนี้

let schools = [... list of schools ...];  
for (let school of schools) {  
    console.log('school name:', school);
}

แล้ว error ที่ผมเห็นถ้าเปิดด้วย browser เช่น safari จะพบแบบนี้

Error: Can't find variable: Symbol  

ซึ่งตอนแรกผมก็ไม่เข้าใจว่าหมายถึงอะไรแน่ แต่สุดท้ายมันก็คือบอกว่ามันไม่รู้จัก Symbol ซึ่งเป็น class ที่มีใน es6 (for .. of จะถูกแปลงด้วย babel ไปใช้ Symbol.iterate แทน ซึ่งก็ไม่มีใน es5 อยู่ดี) ซึ่งการจะเสริมความสามารถ Symbol ให้ browser ทั่ว ๆ ไปก็คือต้องมีการใช้ polyfill ที่จริ

อ่านต่อ »

Using ES6 Promises in Angular Apps

ที่จริงคอนเซ็ปต์เรื่อง promise ไม่ใช่เรื่องใหม่ (อ่านเพิ่มเติมเรื่อง promise) และ Angular เองก็ได้รับเอาคอนเซ็ปต์นี้เข้ามาเป็นส่วนหนึ่งของ framework ซึ่งเราจะเรียกใช้งานได้จากการ inject $q (วิธีการใช้ $q) โดยผมพบด้วยตัวผมเองว่าการใช้ promise ทำให้ผมสามารถทำงานกับ Angular ได้อย่างมีความสุขขึ้นอย่างมาก โค้ดที่เขียนออกมานั้นอ่านและเข้าใจได้ง่ายกว่าเดิมมาก จากเดิมต้องเขียนด้วยการใช้ callback ซึ่งก็เข้าใจยากด้วยตัวเองอยู่แล้ว ยังยากต่อการอ่านให้รู้เรื่องด้วยเนื่องจากการเขียน callback ไม่ได้ถูกออกแบบมาให้อ่านด้วยมนุษย์ได้ง่าย ๆ

แต่สำหรับคนที่ไม่เคยได้ยินคำว่า promise มาก่อน ผมจะอธิบยไว้ตรงนี้เสียหน่อยว่ามันก็เหมือนกับ "สัญญา" ดังความหมายของมันนั่นแหละ มันคือสิ่งที่จะมี "ค่า" ในอนาคต แม้ว่ายังไม่ใช่ตอนนี้ ลักษณะการใช้งานทั่ว ๆ ไปที่เราจะเห็นได้ก็เช่น

let a = some promise;  
a.then(  
    function afterA(promisedValue) {
        ... do something here ...
    });

จากตัวอย่างนี้ ... ฟังก์ชัน afterA จะถูกเรียกเมื่อ "สัญญา" ได้กลายเป็นความจริงแล้ว (ไม่เกิดขั้นทันที) และจะถูกส่งค่าที่ได้สัญญาเอาไว้ใส่ไปใน promisedValue ด้วย ถ้าดูตรงนี้แค่นี้ก็อาจจะคิดว่ามันก็ไม่ได้ต่างกับ callback หนิ แค่มีตัวอักษร .then ซึ่งก็จริง เพราะว่า .then เองก็ยังต้องใช้ callback เลย แต่เชื่อหรือไม่ว่าแค่มี .then ก็ทำให้อ่านโค้ดได้ง่ายขึ้นเป็นโขแล้ว

อีกหนึ่งตัวอย่างที่แสดงว่า promise มันน่าจะดีกว่า callback แน่ ๆ ก็เช่น

let a = some promise;  
a  
.then(
    function afterA(promisedValue) {
        ... do something here ...
        let b = some promise;
        return b;
    })
.then(
อ่านต่อ »

Poor scrolling performance using bootstrap-select on a large list

Bootstrap select (เว็บไซต์) เป็น plugin ที่ทำให้เราสามารถ "รับประกัน" ประสบการณ์การใช้งาน dropdown list ของผู้ใช้ได้ เนื่องจากโดยปกติแล้ว dropdown list หรือ <select> มีความแตกต่างขึ้นอยู่กับ browser และก็ยากต่อการกำหนดค่าต่าง ๆ เนื่องจากสามารถกำหนดค่า CSS ได้อย่างจำกัดอย่างยิ่ง จึงมีคนคิด bootstrap select ขึ้นมาโดยอาศัยการจำลอง "ปุ่มกด" ประกอบกับเทคนิคเล็กน้อย เพื่อใช้แทน <select>

หากเราท่านติดตั้ง bootstrap select และใช้งานมันอย่างที่ได้เขียนกำกับไว้แล้วนั้น ผลลัพธ์ก็จะงดงามอย่างไม่น่ามีปัญหาอะไร

ทว่าปัญหาจะเกิดขึ้นก็ต่อเมื่อ เราเริ่มใช้ list ที่มีขนาด "ใหญ่" นี่ที่นี้ผมกล่าวถึง ราว ๆ 60-70 ขึ้นไป เช่น จังหวัดในประเทศไทย หรือรายชื่อมหาวิทยาลัยในประเทศไทย ซึ่งผมได้ใช้ bootstrap select กับข้อมูลเหล่านี้ แล้วพบว่าในบางกรณีมีปัญหาการ "scroll หน้าเว็บที่ช้าเอามาก ๆ" และเป็นไปอย่างผิดปกติ กล่าวคือเมื่อเรา คลี่ select ออกมาการ scroll ก็ลื่นปรื้ดตามปกติ แต่เมื่อเรา หุบ select เข้าไปการ scroll กับช้าราวกับเต่าคลาน (framerate ที่ผมพบไม่น่าจะเกิน 5 ต่อวินาที)

เมื่อผมลองดูหน้าต่าง performance ของ google chrome ก็จะได้ภาพดังต่อไปนี้

จะเห็นว่า framerate น้อยมาก ๆ ตอนที่มีการ scroll เกิดขึ้นจะเห็นกราฟสีเขียวดรอปลงในช่วงนี้

ที่แปลกคือปัญหาการ scroll เกิดขึ้นเมื่อ หุบ list เข้าไปแล้วซึ่งหมายความมว่า display: none ซึ่งตามความเข้าใจของผม browser ก็ควรจะทำงานน้อยลง แม้ว่า list จะใหญ่มี items เยอะ แต่มันก็สามารถมองข้ามไปด้วยเพราะว่ามันไม่ต้อง render เลยแม้แต่น้อย (ซึ่งผมอาจจะผิด)

หากมองลึกลงไปหน่อยจะเห็นว่าส่วนที่ช้าที่สุด ที่ทำให้การ

อ่านต่อ »