【Amaple教程】4. 元件

JOU_amjs發表於2018-01-24

在Amaple單頁應用中,一個頁面其實存在兩種模組化單位,分別是

  1. 模組 am.Module類),它是以web單頁應用跳轉更新為最小單位所拆分的獨立塊;
  2. 元件 am.Component類),它的定位是擁有特定功能的封裝塊,就像由一堆程式碼封裝成的具有特定功能的函式一樣,一個元件也有獨立的檢視、狀態資料物件、元件行為以及生命週期。常用的元件有DialogBubbleNavigatorMenubar等。

在模組中定義並使用一個簡單的元件衍生類

使用am.class類構造器繼承am.Component類定義一個元件,而繼承am.Component建立的類被稱為 元件衍生類 ,你可以這樣定義一個元件衍生類:

// 在am.class函式的引數中指定該元件衍生類的類名,它返回指定名稱的元件衍生類
// 類名須遵循首字母大寫的駝峰式命名規範,如"BubbleDemo",否則將會報錯。但接收變數名沒有限制
var BubbleDemo = am.class ( "BubbleDemo" ).extends ( am.Component ) ( {

    // 在init函式返回該元件的狀態資料物件
    init : function () {
        return {
            bubbleText: "this is a component bubble"
        };
    },

    // 元件中必須定義render函式,在該函式中指定元件的template模板和樣式
    render : function () {
        this.template ( "<span>{{ bubbleText }}</span>" )
        .style ( {
            span: { background: "red", fontSize: 20, padding: "10px 16px" }
            // !注意:當元素選擇器為符合變數命名規則時可不用引號,如上面選擇span元素時。當選擇器不符合變數名規則時需使用引號,如:
            // ".class-name": { fontSize: 15 }
            // "span #id": { margin-top: 24 }
        } );
        // this.template ( templateHTML )函式中傳入html字串來定義該元件的檢視
        // this.style ( styleObj )函式為該元件的檢視定義樣式,這些樣式也只作用於元件檢視
        // 需注意的是該函式傳入一個物件,物件屬性名為css選擇器語法,值為css樣式物件,樣式名也是使用駝峰式表示,樣式值為量值時可直接寫為數字
    }
} );

在一個模組中使用 元件衍生類 渲染元件檢視也是非常簡單的,首先在am.startRouter函式中配置元件載入的baseURL

am.startRouter ( {
    baseURL : {
        // ...

        // 為元件檔案設定base路徑,所有的元件檔案請求路徑都將基於“/component”目錄,不設定時預設“/”
        component: "/component"
    },
    // ...
} );

然後在需要使用的模組或元件中通過import函式引入,並在<template>中通過自動以標籤名來使用元件

<template>
    <!-- 自定義標籤名為該元件衍生類的類名以全部小寫的中劃線式規範轉換而來,而不是接收的變數名的轉換 -->
    <bubble-demo></bubble-demo>
</template>
<script>

    // 當你將上面的元件衍生類單獨編寫在src/component檔案裡時,你需要使用“import ( componentFilePath )”來引入此元件,
    // 這樣在template模板中的<bubble-demo>元素就會解析為元件模板“<span>this is a component bubble</span>”了。
    // 引入時可省略“.js”檔案字尾
    var BubbleDemo = import ( "BubbleDemo" );

    // 當然你也可以直接在模組中編寫使用一個元件,像這樣:
    // var BubbleDemo = am.class ( "BubbleDemo" ).extends ( am.Component ) ( ... );
    new am.Module ( {
        init : function () { ... }
    } );
</script>

元件生命週期

與模組生命週期階段數一樣,一個元件從建立到解除安裝也分為5個階段的生命週期,具體如下:

  • init:元件初始化時觸發,它返回元件<template>模板解析與掛載所使用的狀態資料。init函式內可呼叫this.propsType函式進行props引數驗證。

props相關知識將在本章節的後面部分介紹

  • render:渲染元件檢視時觸發,該生命週期函式內可分別呼叫this.template函式定義檢視標籤的字串,和this.style函式為元件檢視新增樣式
  • mounted:解析並掛載狀態資料到元件檢視後觸發,你可以在此函式中處理一些檢視解析完成後的操作,如為此元件請求網路資料並更新到模板等
  • updated:當元件在頁面中的位置改變時觸發,在元件上使用:for指令時將會渲染多個元件,此時改變:for指令所繫結的狀態陣列時將可能改變元件的位置
  • unmount:元件解除安裝時觸發,有兩種情況將會解除安裝元件:
    ①. 通過:for指令渲染多個元件檢視後,呼叫繫結的狀態陣列的變異函式都可能在頁面上解除安裝一個或多個元件;
    ②. 當一個模組或元件被解除安裝時,該模組或元件中的元件也將會解除安裝。

元件行為

我們已經知道元件是一個擁有特定功能的封裝塊,所以它會有自己特定的 元件行為 ,如Dialog元件有開啟和關閉行為,輪播圖元件有翻頁行為等。你可以這樣定義 元件行為

var Dialog = am.class ( "Dialog" ).extends ( am.Component ) ( {
    init : function () {
        return { open: false, text: "" };
    },
    render : function () {
        this.template ( [
            `<div :if="open">`,
                `<span>{{ text }}</span>`,
            `</div>`
        ].join ( "" ) );
    },

    // 新增action成員函式,該函式返回元件行為的函式集合物件,該物件被稱為元件行為物件
    // action函式的this指標也是指向該元件物件本身
    action : function () {
        var _this = this;
        return {

            // 元件行為函式的this指標不會指向任何值
            // 通過state.open來控制Dialog檢視的隱藏與顯示
            open: function ( text ) {
                _this.state.text = text;
                _this.state.open = true;
            },
            close: function () {
                _this.state.open = false;
            }
        };
    }
} );

# 元件行為的兩種使用方法

  • [1]在元件的生命週期函式mountedupdateunmount中可通過this.action使用元件行為物件;
  • [2]在元件元素上使用:ref指令,呼叫module.refs函式獲取元件引用時將返回該元件的元件行為物件。

巢狀元件

元件與元件之間配合使用可以發揮更強大的元件能力,在一個元件的<template>模板中可以巢狀其他元件,你可以這樣寫:

// ComponentB元件依賴ComponentA元件
// ComponentA元件的編寫與普通元件編寫相同,這裡省略
var CompoenntB = am.class ( "CompoenntB" ).extends ( am.Component ) ( {

    // 在建構函式中通過this.depComponents來指定該元件的依賴元件陣列
    constructor : function () {
    
        // 和ES6的class關鍵字定義類一樣,在建構函式中需首先呼叫super()函式,否則將會丟擲錯誤
        this.__super ();
        this.depComponents = [ ComponentA ];
    },
    init : function () { ... },
    render : function () {
        this.template ( "<component-a></component-a>" );
    }
} );

ComponentAComponentB元件都編寫在單獨的檔案中時,你需要在模組中同時引入 元件 巢狀元件 ,像這樣:

<template>...</template>
<script>

    // 在ComponentB元件中只需通過this.depComponents = [ ComponentA ]指定它所依賴的元件即可,然後在使用的模組中統一引入這些元件檔案
    // 因為ComponentB元件依賴ComponentA元件,所以需在ComponentB之前引入ComponentA
    // 此時ComponentA元件就可以被ComponentB所獲取到
    var ComponentA = import ( "component/ComponentA" );
    var ComponentB = import ( "component/ComponentB" );

    new am.Module ( ... );
</script>

元件與元件、元件與模組之間的通訊

元件作為一個單獨的封裝塊,它必須與其他元件或模組進行通訊,你可以在模組中分發資料到不同元件,也可以在元件中分發資料到巢狀元件中。在元件中可以使用props進行資料的通訊,使用subElements進行html模板塊分發。

# 使用props傳遞靜態值

<template>
    <!-- 在元件元素上定義任何非指令屬性(屬性名不為“:”開頭),它都會被當做props屬性傳入元件中 -->
    <dialog text="this is a external message!"></dialog>
</template>
<script>
    am.class ( "Dialog" ).extends ( am.Component ) ( {
        init : function () {

            // 在元件中使用this.props接收外部傳入的資料
            // this.props.text的值為"this is a external message!",即外部傳入的字串
            return { text: this.props.text };
        },
        // ...
    } );

    new am.Module ( ... );
</script>

# 使用props傳遞動態值

props還支援使用插值表示式的方式傳遞狀態資料,這被稱為 動態props 。動態props將建立一個對外部狀態資料的代理屬性,當在元件內更改了此代理屬性時,外部對應的狀態資料也將同步更新。如下:

  • 在使用Dialog元件的檢視中,將狀態屬性text傳入元件後,元件的this.props.text即為該狀態屬性的代理屬性。
<template>
    <dialog text="{{ text }}"></dialog>
</template>
<script>
    new am.Module ( {
        init : function () {
            text: "this is a external message!"
        },
        // ...
   } );
</script>
  • Dialog元件的程式碼中,可通過this.props.text獲取外部傳遞的text狀態屬性。
am.class ( "Dialog" ).extends ( am.Component ) ( {
    init : function () {
        return {

            // 使用text1接收並使用this.props.text的值
            text1: this.props.text,

            // 如果你希望更新外部的text屬性後,元件檢視中掛載了this.props.text資料的地方也同步更新,
            // 你可以在元件中建立一個計算屬性作為this.props.text的代理,如下建立的text2計算屬性:
            computed: {
                var _this = this;
                text2: {
                    get: function () {
                        return _this.props.text;
                    },
                    set: function ( newVal ) {
                        _this.props.text = newVal;
                    }
                }
                // 因為元件內對this.props.text的值更新後,外部的text狀態屬性也會同步更新,反之也成立
                // 這樣在元件檢視中掛載text2就等於掛載props.text
                // 此時需注意的是,更改text2的值也將同步更改外部text屬性的值
            }
        };
    },
    // ...
} );

# props驗證

當你希望開放你所編寫的元件給其他開發者使用時,你不確定其他開發者傳入的props引數是否符合元件內的處理要求,此時你可以為你的元件設定props資料驗證,你可以在元件的init函式內呼叫this.propsType函式進行驗證:

am.class ( "Dialog" ).extends ( am.Component ) ( {
    init : function () {

        // 每項值的驗證都可以設定validate、require和default屬性
        this.propsType ( {
            text: {
                validate: String,  // 表示當傳入text值時它必須為字串
                require: true,  // 表示text引數為必須傳入的引數,預設為false
                default: "Have no message"   // 表示不傳入text引數時的預設值,預設值不會參與props驗證,不指定default時無預設值
                // validate的值可以有四種型別的引數,分別為:
                // ①. 基礎資料建構函式,分別有String、Number、Boolean三種基本資料型別建構函式,Function、Object、Array三種引用型別建構函式,
                //     以及undefined和null,它表示允許傳入的資料型別
                // ②. 正規表示式,如/^1d{10}$/表示只允許傳入一個手機號碼
                // ③. 函式,它接收此props引數值,必須返回true或false表示是否通過驗證,如:
                // function ( text ) { return text.length > 5 }
                // ④. 陣列,陣列內是以上三種值的組合,通過陣列內任意一項驗證都可以通過,相當於“||”運算子

            }

            // 當text屬性驗證只要設定validate屬性時,可直接如下縮寫:
            // text: String
        } );

        return { text: this.props.text };
    },
    // ...
} );

使用subElements分發html片段

如果你想開發一個更加通用的Dialog元件,你應該不希望Dialog的檢視佈局是固定不變的,而是可以根據不同的需求自定義Dialog檢視,因為這樣才顯得更加靈活多變,元件的subElements就是用來解決這個問題的,它可以使你從元件外部傳入html片段與元件檢視混合:

<dialog>
    <!-- <span>的內容會被作為html片段傳入元件內 -->
    <span>this is external HTML template</span>
</dialog>

然後在元件內通過subElements屬性獲取外部傳遞的檢視,並插入到元件檢視中的任意位置。subElement接收的檢視可分為 預設subElements subElements的單數分塊 subElements的不定數分塊 三種形式。

# 預設subElements

在元件元素中傳入html片段時,元件內將會建立一個預設的subElements區域性變數,你可以在元件內的模板中通過{{ subElements.default }}插入此html片段:

am.class ( "Dialog" ).extends ( am.Component ) ( {
    init : function () { ... },
    render : function () {

        // {{ subElements.default }}將會輸出外部傳入的“<span>this is external HTML template</span>”
        this.template ( "<div>{{ subElements.default }}</div>" );
    }
    // ...
} );

Dialog元件將會被渲染成:

<div>
    <span>this is external HTML template</span>
</div>

【注意】分發的html片段也可以使用模板指令與元件,此html片段解析時掛載的資料來源是元件外部的狀態資料,如下:

<template>
     <dialog>
          <!-- {{ text }}的資料來源是此模組的狀態 -->
          <!-- 它就像JavaScript中傳入某個函式內的回撥函式,該回撥函式可對外部資料訪問而不是函式內 -->
          <span>{{ text }}</span>
     </dialog>
</template>
<script>
     new am.Module ( {
          init : function () {
               return { text: "this is external HTML template" };
          },
          // ...
     } );
</script>

# subElements的單數分塊

如果你希望開發的Dialog分為頭部和內容兩部分檢視,再混合到元件模板的不同位置,subElements也允許你這樣編寫html片段:

<dialog>
    <header>
        <span>this is a title</span>
    </header>
    <content>
        <span>this is external HTML template</span>
    </content>
</dialog>

<header><content>將分發的程式碼塊分為了兩部分,你可以在元件檢視中分別將它們插入到不同的位置,只需在元件中分別定義HeaderContent兩個 元件子元素

am.class ( "Dialog" ).extends ( am.Component ) ( {
    init : function () { ... },
    render : function () {

        // 指定分塊的元件子元素名
        // 元件子元素名也需遵循首字母大寫的駝峰式規則,在元件元素內使用時也是全部小寫的中劃線式規範
        this.subElements ( "Header", "Content" )
        .template ( [
            "<div>",
                "<div>{{ subElements.Header }}</div>",
                "<div>{{ subElements.Content }}</div>",
            "</div>"
        ].join ( "" ) );
        // <header>、<content>兩個子元素只能作為<dialog>子元素使用
        // 元件模板中分別通過subElements.Header和subElements.Content輸出對應的html分塊片段
    }
    // ...
} );

此時Dialog元件將會被渲染成:

<div>
    <div><span>this is a title</span></div>
    <div><span>this is external HTML template</span></div>
</div>

【注意】①. 如沒有在this.subElements函式中定義相應的元件子元素時,Amaple只會將它們作為普通dom元素對待。
②. 除<header><content>外的其他html片段會自動包含在subElements.default中。

# subElements的不定數分塊

subElements的分塊分發可能會讓你想到很多原生的元素,如<ul><li><select><option><table><tr><td>等,他們都屬於包含與被包含的關係,但你會發現其實<ul>中可以定義一個或多個,在subElements中你也可以定義一個 元件子元素 的不定數分塊,如Grid元件(網格)可包含不定個數的GridItem

<grid>
    <grid-item>a</grid-item>
    <grid-item>b</grid-item>
    <grid-item>c</grid-item>
</grid>

在元件中這樣定義不定數分塊的subElements

am.class ( "Grid" ).extends ( am.Component ) ( {
    init : function () { ... },
    render : function () {
        this.subElements ( { elem: "GridItem", multiple: true } )
        .template ( [
            "<ul>",
                "<li :for=`item in subElements.GridItem`>{{ item }}</li>",
            "</ul>"
        ].join ( "" ) )
        .style ( ... );
        // 此時區域性變數subElements.GridItem為一個包含所有GridItem分塊片段的陣列,在元件內使用:for指令迴圈輸出,
        // 也可以使用陣列索引如subElements.GridItem [ 0 ]
        
        // 其實上面定義單數分塊的Header的全寫是{ elem: "Header", multiple: false },但它可縮寫為"Header"
    }
    // ...
} );

繼續學習下一節:【AmapleJS教程】5. 外掛
也可回顧上一節:【AmapleJS教程】3. 模板指令與狀態資料(state)

相關文章