2017年1月19日 星期四

使用webpack打包React.js 專案

在上一篇我們終於在server 上把React.js 跑起來之後,就能繼續寫下去,結果很快的我們就遇到另一個問題,那就是怎麼所有code 都擠在一塊了(yay,
我們上一篇解法的問題,在於我們template render 這個網頁之後,相關的檔案全塞在用<script>引入的javascript 檔案裡,然後這個文件就引不入其他文件了,而React 提倡的模組化該是能把各元件分別歸檔才是。
幸好,我們還是有解法的,利用webpack 讓我們把檔案分開,然後用webpack 打包成單一的 javascript 檔案,據說這種寫法就是<入坑>,因為一用了webpack 就脫不了身,之後都會綁死在webpack 上面。
網路上有些相關的文件,就來一步步跟著做:
https://rhadow.github.io/2015/04/02/webpack-workflow/

首先先準備我們的front-end/main.jsx,因為使用到react跟react-dom,因此在檔案開頭用require 引入,完成的main.jsx 像這樣:
'use strict'

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

var hello = React.createClass({
  render: function() {
    return (<h2>Hello World!</h2>);
  }
});

ReactDOM.render(
  React.createElement(hello, null),
  document.getElementById('content')
);
接著使用npm 安裝webpack :
npm install --save-dev webpack

並編輯webpack 的設定檔webpack.config.js:
var path = require('path');

var config = {
  entry: [path.resolve(__dirname, './front-end/main.jsx')],
  output: {
    path: path.resolve(__dirname, './static'),
    filename: 'bundle.js'
  },
};

module.exports = config;
這時候使用webpack 來打包,會出現錯誤訊息:
ERROR in ./front-end/main.jsx
Module parse failed: /home/yodalee/website/message-downloader/front-end/main.jsx Unexpected token (8:12)
You may need an appropriate loader to handle this file type.

明顯它看不懂react 的jsx 語法,我們需要設定webpack.config.js 遇到 jsx 檔案就使用babel-loader,還要排除本地的node_modules 裡面的檔案,與output 同層加上:
module: {
  loaders: [{
    test: /\.jsx?$/,
    loader: 'babel-loader',
    exclude: path.join(__dirname, 'node_modules')
  }]
},
並編輯babel的設定檔 .babelrc:
{
  "presets": ["react"],
}
並且使用npm 安裝babel-loader和preset react:
npm install --save-dev babel-loader babel-core babel-preset-react react react-dom

這樣就能用webpack 幫忙打包所有檔案了,有了這個我們就能…嗯…把bower 給刪掉啦(・∀・)。

現在bundle.js 產生在static 裡面,我們可以把template/view.html 清得乾乾淨淨只剩下這樣就會動了:
<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="UTF-8">
    <title>View Test</title>
  </head>
  <body>
    <div id="content"></div>
    <script src="/static/bundle.js"></script>
  </body>
</html>
有沒有覺得超級神奇,我也是這麼覺得的。
老實說你要說我一知半解我也認了,前端真的是各種古怪離奇,我看網路上那堆stack overflow的答案怎麼每個都不太一樣,然後試著做不能動的居多(yay,這篇要不是強者我同學人生勝利組一哥冠霖大神的指導,根本要試超久才試得出來。

下面整理一些相關的錯誤訊息:
Unexpected token
well 它看不懂 jsx 的檔案,所以需要用babel-loader 來處理 jsx 檔

Module not found: Error: Cannot resolve module 'react'
表示它在node_modules 裡面找不到require 的module,使用npm 安裝即可

ERROR in ./front-end/main.jsx
Module build failed: Error: Couldn't find preset "react" relative to directory "/home/yodalee/website/message-downloader"
這個是指 babel-loader 沒有相關的……套件?像上面裝了babel-preset-react 就解掉了。

本文感謝強者我同學人生勝利組一哥冠霖大神的指導

2017年1月14日 星期六

使用bower安裝react 前端環境

最近寫message-viewer ,想在bottle.py 執行的server 上面跑React.js,於是就小找了一下,基本上排除了使用 bottle-react 這種懶人套件,我想要的就是能直接寫,同時react jsx 也能在我的管控之下的設定。
後來找到這篇文章,照著它的步驟、跟留言的回覆做就成功了,在這邊整理一下:
https://realpython.com/blog/python/the-ultimate-flask-front-end/

這裡就不介紹React.js 的運作原理了,筆者到目前也還在學,總之我們就跟裡面的一樣,先寫個 view.html,裡面沒什麼,就是用React 寫一個Hello World,直接使用cdnjs 提供的library:https://cdnjs.com/libraries/react/
<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="UTF-8">
    <title>View Test</title>
  </head>
  <body>
    <div id="content"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.38/browser.min.js"></script>

    <script type="text/babel">
var hello = React.createClass({
  render: function() {
    return (<h2>Hello World!</h2>);
  }
});

ReactDOM.render(
  React.createElement(hello, null),
  document.getElementById('content')
);
    </script>
  </body>
</html>

接著我們使用任何一種server 的template engine ,我這裡用的是Jinja2就能把網頁跑起來,因為script 來自CDN,所以不必特別設定就能直接使用,打開來應該會出現h1 的Hello World。
app.route('/view', 'GET', MessageViewHandler)
@route('/view')
def MessageViewHandler():
    template = JINJA_ENVIRONMENT.get_template('view.html')
    return template.render()

下一步我們要在自己的電腦上面裝上React,我們使用的是管理前端的管理程式bower,像是bootstrap, React 什麼的都可以裝,

因為我是用archlinux ,本身就提供了bower 套件,所以可以用 pacman 裝bower;非archlinux 的發行版就要用npm 裝bower:
$ npm install -g bower
使用-g 是設定global ,因為在其他project 中八成也會用到bower,但另外,為了服務載了你的project 卻沒有bower 的使用者,我們也需要設定一個npm 的文件:
$ npm init
$ npm install --save-dev bower

有了bower之後,在project 中初始一個bower:
$ bower init
設定直接接受預設設定即可,它會產生一個 bower.json 檔案,我們另外要指定bower 安裝檔案的路徑為static,這要編輯 .bowerrc 並加入下列內容:
{
  "directory": "./static/bower_components"
}
並使用bower 安裝套件,可以用 bower install <package_name> --save 或是在bower.json 中加入套件名之後,再呼叫 bower install;我們這裡用第二種方法,在bower.json 的dependency 下面加上:
"dependencies": {
  "bootstrap": "^3.3.6",
  "react": "^15.1.0",
  "babel": "^5.8.38"
}
並執行 bower install,就能安裝好所需的套件,這時project 中的檔案應該差不多是這樣:
.bowerrc
.gitignore
bower.json
package.json
static/bower_components
template/

現在可以把上面的cdnjs 換成本地資料夾的static link:
<script src="/static/bower_components/react/react.min.js"></script>
<script src="/static/bower_components/react/react-dom.min.js"></script>
<script src="/static/bower_components/babel/browser.min.js"></script>
同時在server 要加上static handler來處理所有對static 的連結:
@route('/static/<path:path>')
def callback(path):
    return static_file(path, root='static')

這樣應該就能在python server 上面寫react.js 的網頁前端了,把剛剛的view.html 打開來跑跑看吧。

2017年1月7日 星期六

多益 900+

去年12/18 ,參加了去年最後一場多益聽讀考試,1/6 公佈成績結果是930,聽力490閱讀440。
之所以要考這場多益,原因是五月退伍後有小小求職過,發現手邊已經沒有適合的英文能力證照,2011 年暑假考的GRE 在今年過期了,當時的成績也不太好,V 60 % / Q 90 % / W 3.5,而且GRE 本身就不是一個廣泛使用的求職英文標準,寫出來大家都不太鳥。
為了及時填補英文能力證明的火力空隙,暑假做了練習題,評估過自己英文實力後,決定報考12/18的多益測驗。

自己的背景當然比較優一點,在學校已經習慣念英文課本跟paper ,聽英文演講也沒有太大問題,口說跟寫作當然…嗯…沒事,就加加減減過得去啦
報考之後下半年就展開練習的生活,大概從8月底持續到12月,除了中間有事出差或太忙,每週末就到圖書館作一回練習題,看看都錯什麼題目。這裡要感謝之前實驗室的地下室長(X 莘予大大借我練習題庫:<New TOEIC 新多益閱讀題庫解析>跟<New TOEIC 新多益聽力題庫解析>,兩本題庫總共2000題,這套據說難度跟真實考試比起來稍難一些,我自己是覺得差不多,全部刷完對我的幫助不小。
另外也有在手機上下載背單字用的App AnkiDroid,單字卡是選用GRE的7500 張單字卡,平時零碎的時間就我把手機拿出來記點單字,我想多少有點幫助(雖然到現在還是沒記完)。
平常練習的時候聽力跟閱讀都是錯10題左右,也就是各450 分,那時候才會定下 >900這個目標,雖然只要超過860就有金色證書,但身為一個整數控,當然是要取個整數900,幸好這次有順利達成目標不用二試。
// 不過我要是有強者我同學:呂神郝神的 -3 dB 大概就 990 了吧QAQ

分科來看,老實說聽力的結果比我想像的好很多,平時練習聽力最怕多益的英、澳口音,跟聽習慣的美、加口音不同,英、澳的口音比較糊成一團,特別聽力 Part 2聽短句問題然後最適當的回答,問題短短的糊成一團就過去了,根本沒聽懂在問什麼,有些 5W1H 的發音還不太一樣,直接聽錯在問什麼;這次測驗事後論壇也有不少人評論有個男生講話特別的糊,好像含著魯蛋在講話,能考到490我也覺得運氣真的很好。
閱讀則是有點偏難(? ,平常練習都可以寫完還剩20分鐘左右,考試可能是太緊張或者太謹慎,剛進到最後一題雙閱讀題,竟然就宣布剩10分鐘,當下有些緊張,最後一題也沒好好讀,成績果然沒有太好看。

小碎念一下,我一直很疑惑多益為什麼不把滿分定在1000 分,拿個990讓人很想進位呀OwO,而且1000 四位數亮出來就是硬比三位數來得有氣勢呀;另外因為身旁實在太多人都990了,害我以為930其實不是個很好的成績 lol不過真要說起來,多益還是比較偏門,像強者我學長丞淵大神在TOEFL 大殺四方,那才是聽說讀寫俱佳的真強者owo。

曾經聽人說過,<這些檢定什麼的都是外顯的,並不能決定你>,仔細想想,要是真有些實際功績,例如在國外念過書或者在外商工作大殺四方,甚或如柏任學長直接在會議會場擔任口譯,丞淵學長在英文辯論場上如戰群雄,他們也許根本不需要這些證書,也能證明自己的英文能力,會考這張證書,也許正是沒有實戰能力的表徵吧?
Anyway,多益順利考過了,評估起來如果再練一兩個月,把閱讀速度跟單字量再提高一些,再考一次閱讀應該能拿到更好的成績,不過目前既然有過900就不打算再考了,接下來就要專心準備七月二戰JLPT N1 了(你看…又是沒實戰能力所以要考證書…QAQ)。

2016年12月31日 星期六

如何在Pull request 被merge/revert 後再送 pull request

自從換了智慧型手機,又搭配1.5G的網路之後,用手機上網的機會大增,在用Firefox 刷Facebook 的時候,很容易跑出廣告來,於是發想把「在Yahoo 台灣大殺四方驚動萬教的人生溫拿勝利組強者我同學 qcl 大神」所寫的神專案QClean 移植到Firefox Mobile 上面。

這部分後來還在努力中,目標是把QClean 的Add-On 移植到Mobile上,研究時,Mozilla 的網頁都跑出一直跑出提醒:Add-on 寫的 plugin 很快就會被淘汰,請改用 Web extension 來寫,想說就順便,在寫mobile version 前,先把本來用 add-on 寫的QClean for Firefox 搬移到 Web-extension 上面。
總體來說是沒什麼難處,把原本用 Web-extension 寫的QClean for Google Chrome,複製一份,然後把裡面web extension 的API 從 chrome 取代為 Mozilla 所用的 browser ,結果就差不多會動了,真的是超級狂,整體來說沒什麼工作。

到時再送出Pull Request 之後,發現寫的Makefile 裡面有typo,雖然緊急在Pull Request 下面留言<先不要merge>,但在作者看到之前已經被merge 了。
接下來出現一個很有趣的狀況,直接幫我把 typo 修好並commit 之後,對上游的master branch送出新的 Pull request,github 卻顯示這個 Pull Request 有衝突,沒辦法像之前一樣直接合併。

後來想了一下才發現問題在哪裡,目前repository 的狀況大概像這個樣子。


問題在於送出新的 Pull Request 的時候,github 會比較兩個 branch 最近的共同祖先,從那個 commit 開始算pull request,在上圖中會是那個”Add features 2” 的commit。
這時我們從”fix some typo” 送了PR,github 會比較這個新的commit ,跟revert 過後的commit 作比較,因為revert 這個commit 修改的檔案(等同我們Pull Request的修改只是剛好反向),兩者因此有衝突;同時,之前的 commit “Add features 1, 2” 都不會算在這個Pull Request 裡面。

要修正這個問題,我們必須讓 upstream 跟現在這個 develop branch完全沒有共同祖先,我所知的有兩個解法:
一個是從開始的master branch開一個新的branch newDev,然後用cherry-pick 把develop 上面的 commit 都搬到這個branch 上,再重送 Pull Request;就如這篇文中所示:
http://stackoverflow.com/questions/25484945/pull-request-merged-closed-then-reverted-now-cant-pull-the-branch-again

$ git checkout master
$ git checkout -b newDev
For every commit in develop branch do
$ git cherry-pick commit-hash

另外一個方法比較方便,直接在 develop branch 上面切換到 newDev branch,然後使用 interactive rebase ,然後把所有的commit 都重新commit 一篇,newDev 就會變成一個全新的分枝了:
$ git checkout develop
$ git checkout -b newDev
$ git rebase -i master
Mark every commits as reword (r) in interactive setting

修正完之後的 repository 大概會長這樣


這時候就能由 newDev branch 對上游的 master 送出Pull Request,把這次的修改都收進去了。

題外話:
寫這篇時發現了這個工具,滿好用的,可以輕鬆畫各種git 的圖,雖然說試用一下也發現不少bugs XD,不過這篇文中的圖都是用這個工具畫的:
https://github.com/nicoespeon/gitgraph.js

2016年12月30日 星期五

使用git hook在commit 前進行unittest

使用 git 做為版本控制系統已經有一段時間了,最近在寫Facebook message viewer 的時候,就想到能不能在本地端建一個 CI,每次 git commit 的時候都會時自動執行寫好的測試?
查了一下就查到這一個網頁,裡面有相關的教學:
https://www.atlassian.com/continuous-delivery/git-hooks-continuous-integration
在這裡記錄一下相關的設定:

Git hook 可以想成git 的plugin system,在某些特定的指令像是commit, merge的時候觸發一個script。
Hook 可分為 client side 和server side,又有 pre-hook 跟 post-hook 的分別,pre-hook 在指令執行前觸發,並可取消行為;post-hook 得是在指令結束後執行,它無法取消行為只能做其他自動化的設定。

網頁中有給一些使用範例
Client-side + Pre-hook: 檢查coding style
Client-side + Post-hook: 檢查repository 的狀態
Server-side + Pre-hook: 保護master
Server-side + Post-hook: broadcast 訊息

所有的hook 會放在.git/hook 資料夾裡面,在開啟專案時就有預設的一些範例,只要拿掉檔名的 .sample就能執行,它們是從 /usr/share/git-core/templates/hooks/ 複製而來。
望檔名生義,這些檔名大多直接了當的指名它們的功用;有些script 如 pre-commit 在回傳非零值時,能阻止git 接受這次commit;詳細的使用方法跟觸發時機請見參考資料。

這裡我想做的是用 Client-side + Pre-hook ,在每次commit 前都先跑過一次測試,如果測試不過就拒絕此次commit,所以我們用到的是 pre-commit。
首先先在project 裡面加上一個test.py,用來統整所有的test script,這樣就可以用 python test.py 跑過所有測試,可以先在command line 測一下:
python test.py || echo $?

pre-commit script 就很簡單:
#!/bin/sh

python test.py || exit 1
這個|| exit 1 是要確保pre-commit script 一定以錯誤結束,如果這個測試之後就沒有其它測試就不需要這段,因為script 會回傳最後一行command 的回傳值。
另外,如果不想在打下commit 的時候出現 unittest 的輸出,可以改寫成:
#!/bin/sh

python test.py 2>/dev/null
if [[ $? -ne 0 ]]; then
  echo "> Unit tests DID NOT pass !"
  exit 1
fi
注意後面的判斷跟echo 是必要的,否則python 輸出導向null之後,使用者打git commit 會變成完全沒有反應,這顯然不是個好狀況;完成之後,試著下git commit,就會發現test 不過,而不像正常流程一樣跳出commit message編輯器:
Garbage@GarbageLaptop $ git commit
.F
======================================================================
FAIL: test_zh_tw (__main__.REdictParseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 17, in test
assert(ans == parse)
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.007s

FAILED (failures=1)

我的建議是在 test case 有一定規摸的時候,足以進行TDD 的時候才引入此流程,或者要先把test 裡都加上expected failure ,否則有test 不過就會讓git 根本commit 進不去,如果又因為過不了就每次都下git commit --no-verify,就失去TDD 的意義了
另外,請記得所有要跑的 script 一定要有執行權限,之前試了一段時間,每次commit 還是都commit 進去,後來發現是 pre-commit 沒有執行權限lol;hook 內的東西clone 的時候也不會複製到 client side,要把它們包到repository 裡面,再由使用者自己把script 加到.git/hook 裡。

這裡大致介紹 git hook 的簡單用法,網路上能輕易找到許多亂七八糟的script 來做各種事,例如用autopep8, cppcheck 檢查語法是否合標準,自動跑npm test 等等。

參考資料:
git hook in gitbook
https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
stackoverflow about pre-commit and unittest
http://stackoverflow.com/questions/2087216/commit-in-git-only-if-tests-pass