回 RWD響應式網頁設計班 課程時間表

使用React.js庫建立高效能前端網頁

上課日期:107年8月19日

目錄

現代前端網站開發

React.js開發四劍客,ES6、Webpack、React.js、Babel(由左至右)

現代化網站由於在強調與使用者互動,功能多,其開發複雜度跟過去比起來提升非常多,網站前端(Web Front End)和後端(Web Back End)的開發工具、程式語言和程式框架的功能和種類都非常多,加上要跨瀏覽器、跨平台相容的要求下還有技術變化的快速,對網站開發工程師的挑戰也越來越多,其開發方式也和過去有非常大的不同,除了需要各自分工之外,熟悉工具的使用才能大大減少開發、維護上的成本。

過去這幾年,網站開發不管是前端或是後端,就像是工業革命一樣,各種框架、套件如雨後春筍般出現,隨然隨著技術與工具的進步,可以大大的協助工程師開發,但由於在網站複雜的大大提升,所以其實開發工作並不會比較輕鬆,例如在前端部分,一開始只需要懂HTML、CSS和Javascript就已經足夠,到後來需要再多掌握幾個函式庫,例如jQuery、ExtJS等等,到現在的Angular、Bootstrap、Vue、React、Babel等等的框架或函式庫,都要有一定程度的了解,否則在開發複雜應用的網站上,會出現一定的瓶頸。

在現代網站開發技術或框架上,React.js可以說是代表,它是Facebook在2013年開放出來的一個Javascipt函式庫,它讓工程師可以更模組化的操作網頁元素,尤其Virtual DOM可以讓網頁在描繪時更有效率,在Virtual DOM的出現之前,只要網頁上某個元素被插入、刪除或是內容更新,都會整個重新描繪,而這也是整個網頁最占效能的部分,如果該網站需要大量操作網頁元素,將造成非常大的效能支出,而Virtual DOM的架構與DOM非常接近,但它可以將對網頁的變化,先集中起來,再一次更新到網頁,這樣便可以大大提升網頁的重繪效率。

一般開發網站時,我們會強調將HTML(資料)、CSS(樣式)和Javascript(邏輯)這三個部分分開,讓程式碼較乾淨且易於維護,而React開發方式則打破了這項傳統,將這三個部分全部在Javascript裡面進行操作。

除了React之外,其它重要的技術還有:

JavaScript

前端網站的開發基礎,也是唯一一個所有瀏覽器都可以執行的程式語言,並且它是前端開發的支柱,其標準是EMCAScript 5,在學習其他框架或函式庫之前,必須先熟練它。

ES6

EMCAScript 6的簡稱,Javascript語言的擴展,各大瀏覽器正在實現對ES6的支援,但到現在為止,尚未完全支援,因此許多語法上無法直接在瀏覽器上使用。但透過轉譯器的使用,你現在就可以開始使用ES6來編寫程式,轉譯器,如Babel,可以將ES6程式碼轉譯為ES5程式碼,雖然ES6有許多新語法更好用,但因為尚未定案,仍然有可能被修改,目前ES6支援的狀況可以參考:https://kangax.github.io/compat-table/es6/

Babel

目前最流行的ES6到ES5轉譯器之一,Facebook的React推薦使用它來做轉譯,連官網的教學範例都是使用它,此外,它不僅僅是ES6轉譯器,還是JSX轉譯器。

JSX

JSX(Javascript with XML)並不是一種程式語言,也不是框架或函式庫,而是一種語法糖(Syntatic Sugar),一種語法類似XML的ECMAScript語法擴充。在JSX裡,HTML元素和程式碼有緊密的關係,這和過去強調將HTML、JavaScript分離的觀念非常不同,一般在開發React理會頻繁使用JSX,當然也可以選擇不要使用,而使用純Javascript來開發React,但使用JSX開發React元件(Component)時,將會覺得有JSX的美好。

Node.js

Node.js是一個伺服器端平台,讓工程是可以使用JavaScript語言來開發網站後端。雖然node.js是用在後端,但是它內建的許多工具都被使用在前端開發上,所以即使是前端網站開發,也有很大機會使用到node.js。

npm

npm(Node Package Manager)是node.js下的套件管理工具。在npm尚有需多前人開發好的套件,許多網站開發用到的功能,都已經有人開發好了,讓你可以直接使用,不需要重新開發,在這裏,我們可以透過npm來安裝React套件和Babel套件。

Grunt或Gulp

這兩個是在Node.js平台上最為流行的工作執行工具,用來讓你自動化許多前端工作,如用來檢查程式碼和最佳化程式碼的Lint、合併、縮減、部署以及其它功能。

Bower

用於前端函式庫的一個套件管理工具,例如要增加jQuery到你的專案中透過它將會非常容易。

WebPack或Browserify

這兩個都是最流行的模組打包工具,它們可以直接取得Javascript原始碼,找出正確的依賴關系,將網站需要用的資源(如圖片、聲音等等),自動執行轉譯Javascript,將建構一個網站的動作都自動化後建立整個可以發佈的網站。

Flux或Redux

Flux實現了單向流的應用程式資料架構,也是由Facebook開發,和React專注於View的部份,以互補的方式相輔相成。而由Dan Abramov所開發的Redux被 React開發社群認為是Flux-like更優雅的一套函式庫,也是目前主流用來搭配React的狀態(State)管理工具。讓我們在開發更複雜的網頁應用程式時可以更方便管理狀態(state)。

Immutable.js

Immutable.js這個函式庫用來建立不可變得資料結構,用來解決在建立React開發上的某些時候的性能問題,也就是不可變(immutable)的資料結構讓狀態可預測性更高,因此提升程式的效能。

Angular 2

Angular 2也是JavaScript中最流行的MVC框架之一的下一個版本,常用來和React.js比較。這版被完全重新設計過,其學習曲線較為陡峭,需要花較多時間,但也是一個非常優的現代化網站開發框架。

TypeScript

TypeScript是一種由微軟開發的自由和開放原始碼的程式開發語言。它是JavaScript的一個嚴格超集合,並添加了可選的靜態型別和類別基礎的物件導向程式設計的特性。其設計目標是用來開發大型應用程式,然後轉譯成JavaScript。由於TypeScript是JavaScript的嚴格超集,任何現有的JavaScript程式都是合法的TypeScript程式。

安裝React

本課程使用React.js開發的網站架構

CDN和離線開發兩種方式

第一種方式為使用CDN開發,較為方便,不需要安裝,只要在HTML中引入React-CDN和Babel-CDN就可以開發了,但缺點就是需要透過網路即時做轉譯,效能會較差,且需要有網路才能開發,範例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        
    <!-- Don't use this in production: -->
    <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/babel">

      ReactDOM.render(
        <h1>Hello, world!</h1>,
        document.getElementById('root')
      );

    </script>
  </body>
</html>

第二種方式則是將React.js和Babel安裝在本機端,可以離線轉譯及開發。

安裝步驟:

我們需要npm這個套件管理工具來安裝需要的React套件和Babel套件,而npm是附屬在node.js底下,它原本就是node.js內的一個套件管理工具,因此必須先安裝node.js。

  1. 建立專案目錄並切換到該目錄下:
    md HelloReact
    cd HelloReact
    
    注意:接下來的操作,都會在這個目錄下進行。
  2. https://nodejs.org/en/下載v8.11.3 LTS

    選擇v8.11.3 LTS下載即可
  3. B.確認安裝路徑

    確認要安裝的路徑
  4. C.確認環境變數

    安裝程式會自動設定環境變數
  5. D.執行Node.js command prompt

    執行Node.js command prompt來啟動命令列模式
  6. E.執行npm -v確認有無安裝成功

    在命令列模式下透過npm -v顯示npm版本指令來確定有無安裝成功。
  7. 初始化npm設定

    npm init
    npm初始化
  8. 安裝React:
    npm install --save react react-dom
    初始化後會在目錄下產生package.json和package-lock.json兩個檔案

    React安裝官網參考:https://facebook.github.io/react/docs/installation.html(add react to existing project)。

備註:

  • LTS版為Long-term support,亦即官方保證會支援該版本bug修正、維護的時間較長,適合用在正式環境,其保證時間可以參考:https://github.com/nodejs/LTS#lts-schedule1
  • 使用npm install命令來進行安裝時,有個參數叫做--global-g,其目的為將該模組安裝在系統目錄,而非目前的目錄。

    例如:

    npm install --global --save react react-dom
  • 一般當我們在安裝一個node.js模組時,如果該模組需要依賴其他的模組,正常情況下必須先安裝依賴的模組,然後再將該模組的版本號碼受始動增加到設定檔package.json中的dependencies或devdependencies裡,而-save可以自動幫我們完成這些動作。-save參數會自動將依賴的模組和版本號碼增加到dependencies,而-save-dev則自動把模組和版本號碼新增到devdependencies。

Babel的安裝與使用

  1. 安裝babel:
    npm install --save-dev babel-cli babel-preset-react

    補充:

    Babel安裝官網參考:https://babeljs.io/docs/plugins/preset-react/
  2. 新增.babelrc檔案,將Babel的preset設定為react模式:
    { 
        "presets": ["react"] 
    }
    
  3. 進行React.js開發(可參考下一小節Hello, React小節)。

  4. Babel轉譯指令:
    node_modules\.bin\babel main.jsx -o main.js
    • 將內涵JSX語法或ES6語法的main.jsx檔案轉譯為純Javascript檔並輸出為main.js
    • -o--output後面為要輸出的檔名。

  5. 每次修改了JSX檔都要重新轉譯較為麻煩,可加上--watch讓Babel監聽原始碼只要有修改,就自動執行轉譯的動作,例如:
    node_modules\.bin\babel main.jsx -o main.js --watch

Hello, React

建立一個使用JSX語法的檔案如下:

ReactDOM.render(
  <h1>Hello, React.js!</h1>,
  document.getElementById('root')
);

透過Babel指令轉譯成Javascript結果

ReactDOM.render(React.createElement(
  'h1',
  null,
  'Hello, React.'
), document.getElementById('root'));

建立index.html內容如下:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width initial-scale=1" />
    <script src="node_modules/react/umd/react.development.js"></script>
    <script src="node_modules/react-dom/umd/react-dom.development.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="main.js"></script>
  </body>
</html>
使用瀏覽器看到的執行結果

React特性

幾個React.js的特性為:
  • Virtual DOM
  • 基於元件(Component-Based)
  • 單向資料流(Unidirectional Data Flow)
  • 透過JSX的宣告式(Declarative)設計

Virtual DOM

在網頁上跟使用者做互動時,常常需取得DOM,並做一些資料更新、插入、刪除的動作,例如:動態增加一個元素或欄位、更新由AJAX傳回來的訊息等等,而這些動作是在前端的所有動作中最耗效能的,因為每次操作DOM的時候,整個網頁都必須要重繪一次,使用者才能看到最新的網頁效果,有可能僅僅是在頁面上增加了一個p元素,或只是改變一下元素的文字大小。

如果只是少量DOM操作,其影響的效能,使用者並感覺不出來,但如果是一個複雜的網頁,如:Facebook,當有大量的DOM被操作的時候,這時效能的差距就會以等比級數下降,因為瀏覽器會不停的重繪整個網頁,使用者會發現網頁的反應越來越慢,當元素的層級越多,就越明顯。

Javascript中提供了一個DocumentFragment的機制就是想要解決這個問題,其原理為操作DOM的時候,不直接對著網頁上的DOM做處理,而是透過建立DocumentFragment物件,將要操作的元素都先加入DocumentFragment,等到最後再一次從DocumentFragment更新到網頁上,僅僅是這樣做,效能變可以快好幾倍。

React的Virtual DOM其原理就像DocumentFragment一樣,在實際的網頁跟程式碼之間多了一層虛擬的DOM,來將要操作的DOM都收集好之後,再一次更新網頁,Virtual DOM是以Javascript模擬出來的,雖然不是真正的DOM,但保留了DOM之間的層級關係和一些屬性,而且其運作會比DocumentFragment優秀很多,一來許多需要自己建立和加入DocumentFragment的動作,在Virtual DOM都會自動完成,二來其實做了一種稱為DOM diff的演算法,用來計算Virtual DOM的變化跟差異,而僅將Virtual DOM差異的部分輸出到網頁上,整個開發商的方便性跟效能都比原本的DocumentFragment還要好。

因此有了Virtual DOM之後,工程師只需要跟原本操作DOM一樣進行開發,其餘的事情都交給Virtual DOM處理就可以了。


基於元件(Component Based)

在React的世界中,每一個元素都是一個元件(Component),它也是用來構成一個網頁最基本的單元,每個元件還可以包含一個以上的子元件,子元件在包含子元件,就像元素一樣,並組裝成一個組合式(Composable)元件,因此擁有:重複使用 (Reuse) 、封裝(encapsulation)、關注點分離 (Separation of Concerns)、 、組合 (Compose) 等等的特性。

當網頁元素元件化之後,再透過把一個個的元件組合成一個網頁,每個元件都會有render方法,當元件的狀態(state)被改變了,就會透過呼叫元件的render方法去重繪整個元件的外觀,而這個重繪的動作,都是指重繪到Virtual DOM中,其它網頁的重繪時間點都交由Virtual DOM來處理。

元件化的優點非常多,例如:可重複使用、易於維護與更容易建立大型應用網頁,但在剛開始接觸時,可能需要多花點時間來習慣與適應,畢竟和過去的開發方式大大不同。


inline CSS寫法

在React中,會使用CSS Inline Style寫法,全都直接寫在JavaScript 當中,例如:

const divStyle = 
{
  backgroundImage: 'url(' + imgUrl + ')',
  color: 'yellow',
  font-size: '2em'
};

ReactDOM.render(<div style={divStyle}>Hello World!</div>, document.getElementById('app'));
注意:
  • 元件名稱的第一個字母一定是大寫,否則無法正常運作。
  • 元件render的最外層元素只能有一個,如果有多個元素,必須用一個外層元素包起來。

JSX基礎

註解

由於JSX最終也是編譯成JavaScript程式,所已註解的方式也非常相似,使用//來做單航註解,而使用/**/來做多行註解,但在子元件中要使用註解時,必須在註解外面以一對大括號包住{}

// 單行註解

/*
多行註解
*/

var content = 
(
   <SubComponent
     name='abc'
     /* 
        多行註解1
        多行註解2
        多行註解3
     */

    // 單行註解
    >
    {/* 若是在子元件註解要加大括號{}  */}
  </SubComponent>
);

屬性

在HTML中,我們可以透過修改元素上的class屬性來改變標籤外觀樣式,在JSX中則可以透過修改className屬性來達成,列如:

class HelloTitle extends React.Component 
{
  render() 
  {
    return (
      <div className="title">
      <p>Hello React!</p>
      </div>
    );
  }
}
因為classfor為JavaScript的保留關鍵字,因此在JSX中必須以classNamehtmlFor來代替。

Boolean屬性

在JSX中如果只有屬性名稱但有設定值的話代表其值為true,例如下面兩行JSX程式碼是一樣的:

<input type="button" disabled />;
<input type="button" disabled={true} />;
而如果沒有設定該屬性,則預設值為false,下面兩行意思是一樣:
<input type="button" />;
<input type="button" disabled={false} />;

自定義屬性

如果想要自行定義元件的屬性,須以data-做開頭,例如:

<HelloComponent data-attr="cusome attr" />

顯示HTML

使用_html來顯示HTML,例如:

<div>{{_html: '<h1>Hello React!</h1>'}}</div>

直接操作CSS樣式

在JSX中使用CSS樣式如下:

<HelloCss style={{ background-color: 'yellow', fontSize: '2em'}} />

事件處理

在JSX中透過inline事件的綁定來監聽並處理事件:

<HelloEvent onClick={this.onBtnClick} />

與Bootstrap整合

React.js用來操作網頁上元素的,而Bootstrap用來呈現網頁的設計,實現響應式,兩者功能互不衝突,因此常常搭配一起使用。

下面為使用這兩個套件的HTML網頁head裡可能的定義:
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width initial-scale=1" />
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <script src="node_modules/react/umd/react.development.js"></script>
    <script src="node_modules/react-dom/umd/react-dom.development.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="main.js"></script>
  </head>
  <body>
    ...網頁內容...
  </body>
</html>

上課範例-一個簡單的塗鴉牆

學習重點:
  • React與Bootstrap的合作。
  • React Component的使用。
  • 透過React props的資料傳遞。
  • 動態新增塗鴉訊息(網頁元素)。

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />  
    <meta name="viewport" content="width=device-width initial-scale=1" />
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <script src="node_modules/react/umd/react.development.js"></script>
    <script src="node_modules/react-dom/umd/react-dom.development.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="main.js"></script>
</head>
<body>
    <!-- Bootstrap必須將所有元素包圍在container或container-fluid裡面 -->
    <div class="container-fluid">
        <div id="header"></div>
        <div id="panel"></div>
        <div id="input"></div>
        <div id="buttons"></div>
        <br/>

        <!-- Modal -->
        <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                        <h4 class="modal-title" id="myModalLabel">新增滴滴姑姑</h4>
                    </div>
                    <div class="modal-body">
                        文字欄位不可為空
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-primary" data-dismiss="modal">關閉</button>
                    </div>
                </div>
            </div>
        </div>

        <!-- 刪除時的YES、NO對話盒 -->
        <div class="modal fade" id="myYesNo" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                        <h4 class="modal-title" id="myModalLabel">新增滴滴姑姑</h4>
                    </div>
                    <div class="modal-body">
                        確定要刪除嗎?
                    </div>
                    <div class="modal-footer">
                        <button id="yes" type="button" class="btn btn-primary" data-dismiss="modal">確定</button>
                        <button id="no" type="button" class="btn btn-primary" data-dismiss="modal">取消</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

main.jsx

document.addEventListener("DOMContentLoaded", init);

function init()
{
    // 輸出網頁大標題(JSX)
    ReactDOM.render(
        <div class="page-header">
        <h1>Aaron的滴滴咕咕</h1>
        <blockquote>
        生活中的不滿,需要發洩
        </blockquote>
        </div>
    , document.getElementById('header'));

    // 新增輸入文字區域,建立InputArea
    class InputArea extends React.Component
    {
        render()
        {
            return(<textarea id="input-data" className="form-control" rows="3"></textarea>);
        }
    }

    // 輸出testarea網頁上
    ReactDOM.render(<InputArea />, document.getElementById('input'));

    // 建立按鈕元件
    class AddButton extends React.Component
    {
        // render涵式為React需要用來輸出HTML元素到畫面上的方法,必須實作它
        render()
        {
            // 注意: 元件輸出的元素,最外層只能有一個元素存在
            return(
            <div>
            <br/>
            <button onClick={this.onAdd} type="button" className="btn btn-primary btn-lg">
            新增滴滴咕咕
            </button>
            </div>);
        }

        // 按鈕元件的事件涵式
        onAdd()
        {
            // 取得textarea內容
            var content = document.getElementById('input-data').value;

            // 判斷textarea內文字長度是否為0
            if(content.length == 0)
            {
                // 顯示對話盒
                $('#myModal').modal('show');
            }
            else
            {
                var timeNow = new Date();
                var yyyy = timeNow.getFullYear();
                var MM = (timeNow.getMonth() + 1 < 10 ? '0' : '') + (timeNow.getMonth() + 1);
                var dd = (timeNow.getDate() < 10 ? '0' : '') + timeNow.getDate();
                var h = (timeNow.getHours() < 10 ? '0' : '') + timeNow.getHours();
                var m = (timeNow.getMinutes() < 10 ? '0' : '') + timeNow.getMinutes();
                var s = (timeNow.getSeconds() < 10 ? '0' : '') + timeNow.getSeconds();

                var ok = yyyy + "年" + MM + "月" + dd + "日 " + h + ":" + m + ":" + s;

                // 將新資料插入陣列
                messageItems.splice(0, 0, {datenow: ok, text: content});

                // 更新網頁
                ReactDOM.render(<MessagePanel />, document.getElementById('panel'));
            }
        }
    }

    // 輸出按鈕到網頁上
    ReactDOM.render(<AddButton />, document.getElementById('buttons'));

    // 存放所有使者滴滴咕咕的陣列
    var messageItems = 
    [
        {datenow: "2017年07月22日 09:00:00", text: "今天也太熱了吧"}
    ];

    // 訊息面板元件(所有訊息)
    class MessagePanel extends React.Component
    {
        render()
        {
            return(
            <div>
            {
                messageItems.map((item) => (<MessageItem datenow={item.datenow} text={item.text}/>))
            }
            </div>);
        }
    }

    // 訊息本體元件
    class MessageItem extends React.Component
    {
        render()
        {
            return(
                <div className="panel panel-primary">
                <MessageHead datenow={this.props.datenow} />
                <MessageBody text={this.props.text} />
                </div>);
        }
    }

    // 標題元件
    class MessageHead extends React.Component
    {
        render()
        {
            return(
                <div className="panel-heading">
                {this.props.datenow}
                <button onClick={() => this.onDel(this.props.datenow)} type="button" className="pull-right btn btn-default btn-xs">
                <span className="glyphicon glyphicon glyphicon-remove" aria-hidden="true"></span>
                </button>
                </div>
            );
        }

        onDel(datenow) // 刪除被點擊的留言
        {
            $('#myYesNo').modal('show');

            document.getElementById('yes').onclick = function()
            {
                for(var i = 0 ; i < messageItems.length ; i++)
                {
                    if(datenow === messageItems[i].datenow)
                    {
                        messageItems.splice(i, 1);
                        break;
                    }
                }

                // 更新網頁
                ReactDOM.render(<MessagePanel />, document.getElementById('panel'));
            }
        }
    }

    // 內容元件
    class MessageBody extends React.Component
    {
        render()
        {
            return(<div className="panel-body">{this.props.text}</div>);
        }
    }

    ReactDOM.render(<MessagePanel />, document.getElementById('panel'));
}
注意:
  • 必須安裝「Bootstrap」、「React.js」和「Babel」才能正常執行。
  • 請記得必須使用Babel將.jsx檔轉譯成.js檔才能正常執行。