定制擴展端

2020-05-14 14:20 更新

基于 chameleon-tool@1.0.3

新框架太多?學不動啦?有這一套跨端標準,今后再也不用學習新框架了。

如今前端比較流行的 React Native、Weex、Flutter 等跨平臺開發(fā)框架,對于開發(fā)來說屬于技術方案的選擇,比如,我們會考慮用這個技術開發(fā),性能會不會超過 h5,開發(fā)效率會不會超過原生開發(fā)等等。

但是從 2017 年微信推出小程序,到至今各大廠商都推出自己的小程序,跨端開發(fā)就不僅僅是技術的問題了。已經變成了必爭的流量入口?,F在的小程序大戰(zhàn)像極了當前的瀏覽器大戰(zhàn)。大戰(zhàn)中受苦的是我們一線開發(fā)者,同樣的應用要開發(fā) N 次,面對了前所未有的挑戰(zhàn),所以跨端框架的產生是大趨勢下的必然產物。

MVVM+ 協(xié)議

chameleon 基于對跨端工作的積累, 規(guī)范了一套跨端標準,稱之為 MVVM+ 協(xié)議;開發(fā)者只需要按照標準擴展流程,即可快速擴展任意 MVVM 架構模式的終端。并讓已有項目無縫運行新端。所以如果你希望讓 CML 快速支持淘寶小程序、React Native?只需按標準實現即可擴展。

最終讓開發(fā)者只需要用 CML 開發(fā),就可以在任意端運行,再也不用學習新平臺框架啦。

新端接入情況

滴滴、芒果 TV、阿里的同學合作,按照跨端協(xié)議流程,目前已完成字節(jié)跳動小程序的共建開發(fā)

  • 分工排期如下:https://github.com/didi/chameleon/issues/157
  • 倉庫地址: https://github.com/chameleon-team/cml-tt-sets

快應用官方研發(fā)團隊目前也接入完成

  • 分工排期如下:https://github.com/didi/chameleon/issues/185
  • 倉庫地址: https://github.com/quickappcn/cml-extplatform-quickapp

跨端原理

跨端框架最核心的工作是統(tǒng)一,chameleon 定義了標準的跨端協(xié)議,通過編譯時+運行時的手段去實現各端的代碼和功能,其實現原理如下圖所示。

其中運行時和基礎庫部分利用多態(tài)協(xié)議實現各端的獨立性與框架的統(tǒng)一性。chameleon 目前支持的端都是采用這種方式,我們定義了擴展一個新端所需要實現的所有標準,用戶只需要按照這些標準實現即可完成一個新端的擴展。

跨端標準協(xié)議

我們再來看一張 CML 的設計圖,能夠實現標準化的擴展新端,得益于多態(tài)協(xié)議中對各層代碼進行了接口的定義,各端代碼按照接口定義進行實現,向用戶代碼提供統(tǒng)一調用,同時還提供”多態(tài)協(xié)議“讓用戶代碼保障可維護性的前提下,直接觸達各端原生能力的方式。

  • API 接口協(xié)議:定義基礎接口能力標準。
  • 內置組件協(xié)議:定義基礎 UI 組件標準。
  • 框架協(xié)議:定義生命周期、路由等框架標準。
  • DSL 協(xié)議:定義視圖和邏輯層的語法標準。
  • 多態(tài)實現協(xié)議:定義允許用戶使用差異化能力標準。

如何擴展新端?

簡單來說只需要實現 6 個 npm 包。

實現 API 接口協(xié)議

chameleon-api提供了網絡請求,數據存儲,獲取系統(tǒng)信息,交互反饋等方法,用戶需要創(chuàng)建一個 npm 包,結構參考cml-demo-api。將 chameleon-api 中提供的每個方法利用多態(tài)接口擴展語法擴展新端的實現。 以擴展一個alert方法為例,chameleon-api中alert方法的接口定義文件為chameleon-api/src/interfaces/alert.interface,其中的接口定義內容如下:

<script cml-type="interface">
type alertOpt = {
  message: String,
  confirmTitle: String
}
type successCallBack = (result: String) => void;
type failCallBack = (result: String) => void;
interface uiInterface {
  alert(opt: alertOpt, successCallBack: successCallBack, failCallBack: failCallBack): void,
}
</script>

用戶實現的interface文件中采用<include></include>語法引入chameleon-api中alert方法的 interface 文件, 實現uiInterface。

// 引入官方標準interface文件
<include src="chameleon-api/src/interfaces/alert/index.interface"></include>
// 擴展實現新端(以頭條小程序為例,假設端擴展標識為:tt)
<script cml-type="tt">
class Method implements uiInterface {
  alert(opt, successCallBack, failCallBack) {
    // 根據頭條小程序實現alert彈窗
    let { message, confirmTitle} = opt;
    tt.showModal({
      content: message,
      confirmText: confirmTitle,
      ......
    });
  }
}
export default new Method();
</script>

實現內置組件協(xié)議

組件分為內置組件 chameleon-ui-builtin 和擴展組件 cml-ui。所以用戶需要創(chuàng)建兩個 npm 包分別實現這兩個組件庫,結構參考cml-demo-ui-builtincml-demo-ui。利用多態(tài)組件擴展語法,對原有組件庫中的每一個組件進行新端的實現。

原有組件庫中的組件也分為兩種,一種為各端都有分別實現的多態(tài)組件,例如chameleon-ui-builtin中的button組件。實現起來新端基本上也是要單獨實現。另一種例如chameleon-ui-builtin中的radio組件,各端的實現都是用的chameleon-ui-builtin/components/radio/radio.cml。所以新端基本也可以復用這個實現,(還需要測試情況確實是否可以復用)。

新端獨立實現

例如:

編寫 my-ui-builtin/components/button/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/button/button.interface" />

編寫 my-ui-builtin/components/button/button.demo.cml

<template>
  <origin-button c-bind:tap="onclick" open-type="{{openType}}"> </origin-button>
</template>
<script>
// js實現部分
</script>
<style scoped>
// 樣式部分
</style>
<script cml-type="json">
// json配置
</script>

獨立實現的 my-ui-builtin/components/button/button.demo.cml 文件屬于多態(tài)組件的灰度層,可以調用各端底層組件和 api,具體例子參見 button 和 scroller 的實現。

新端復用現有組件

編寫 my-ui-builtin/components/radio/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/radio/radio.interface"></include>
// 復用官方的實現
<script cml-type="demo" src="chameleon-ui-builtin/components/radio/radio.cml"></script>

實現 DSL 協(xié)議(編譯時插件)

chameleon 內部會將整個項目文件編譯為如下編譯圖結構,節(jié)點中的內容經過了標準編譯,比如script節(jié)點的babel處理,style節(jié)點的less與stylus處理等等。 

節(jié)點的數據結構如下:

class CMLNode {
  constructor(options = {}) {
    this.realPath; // 文件物理地址  會帶參數
    this.moduleType; // template/style/script/json/asset
    this.dependencies = []; // 該節(jié)點的直接依賴       app.cml依賴pages.cml pages.cml依賴components.cml js依賴js
    this.childrens = []; // 子模塊 cml文件才有子模塊
    this.source; // 模塊源代碼
    this.output; // 模塊輸出  各種過程操作該字段
    ......
  }
}

用戶只需要實現一個編譯插件類,利用鉤子方法實現對節(jié)點的編譯,所有節(jié)點編譯完后再進行文件的組織。編譯類如下:

module.exports = class DemoPlugin {
  constructor(options) {
    ......
  }
  /**
   * @description 注冊插件
   * @param {compiler} 編譯對象
   * */
  register(compiler) {
    // 編譯script節(jié)點,比如做模塊化
    compiler.hook('compile-script', function(currentNode, parentNodeType) {
    })
    // 編譯template節(jié)點 語法轉義
    compiler.hook('compile-template', function(currentNode, parentNodeType) {
    })
    // 編譯style節(jié)點  比如尺寸單位轉義
    compiler.hook('compile-style', function(currentNode, parentNodeType) {
    })
    // 編譯結束進入打包階段
    compiler.hook('pack', function(projectGraph) {
      // 遍歷編譯圖的節(jié)點,進行各項目的拼接
      // 調用writeFile方法寫入文件
      // compiler.writeFile()
    })
    ......
  }
}

實現框架協(xié)議

運行時主要是對 cml 文件的邏輯對象進行適配,chameleon 內部將 cml 文件的邏輯對象分為三類 App、Page、Component。對應會調用用戶運行時 npm 包的createApp、createPage、createComponent方法,所以對外只需要實現這三個方法。

例如一個 Page 的邏輯對象如下:

class PageIndex {
  data = {
    name: 'chameleon'
  }
  computed = {
    sayName () {
      return 'Hello' + this.name;
    }
  }
  mounted() {
  }
}
export default new PageIndex();

編譯時就會自動插入用戶的運行時方法處理邏輯對象,例如cml-demo-runtime:

class PageIndex {
  ......
}
export default new PageIndex();

// 編譯時自動插入用戶配置的運行時方法
import {createPage} from 'cml-demo-runtime';
createPage(exports.default);

createApp、createPage、createComponent方法,參考 cml-demo-runtime 的結構進行實現,需要 include 中相應的接口進行實現,才能夠實現對 chameleon-runtime 的擴展。用戶的工作量主要在于對邏輯對象的處理,可以參考 chameleon-runtime 中的實現方式,一般需要如下方面的適配工作。

從宏觀來看,運行時處理可分為:

  • 輸入 Options 對象的適配
  • 跨端運行時能力注入

從微觀來看,有以下處理:

例如: createPage 方法的實現

<include src="chameleon-runtime/src/interfaces/createPage/index.interface"></include>
<script cml-type="demo">
  class Method implements createPageInterface {
    createPage(options) {
      // 各端自行實現adapter
      adapter(options);
      //例如調用小程序原生頁面構造函數
      Page(options);
      return {};
    }
  }

  export default new Method();
</script>

實現框架數據管理

chameleon-store 提供了類似 Vuex 的數據管理解決方案同樣利用多態(tài)協(xié)議實現其功能。

更多

  • 擴展新端 Demo 示例倉庫: https://github.com/chameleon-team/cml-extplatform-demo。 實現了微信端的基本擴展,用戶可以以此為模板進行開發(fā)。

期待更多人的加入開源。

擴展新端

開發(fā)示例體驗

擴展新端 Demo 示例倉庫: https://github.com/chameleon-team/cml-extplatform-demo。 實現了微信端的基本擴展。

運行項目

  • 首先全局安裝支持擴展新端的命令行npm i chameleon-tool@1.0.3 -g。
  • 全局安裝lerna 對本項目進行管理 npm i lerna -g。
  • 在本倉庫根目錄執(zhí)行l(wèi)erna bootstrap,這一步是安裝外部依賴與建立本倉庫 npm 包之間的依賴。
  • 在cml-demo-project目錄執(zhí)行cml demo dev, 用微信開發(fā)者工具打開cml-demo-project/dist/demo目錄。

開發(fā)流程

采用 lerna 對開發(fā)的 npm 包進行管理,解決本地開發(fā)時多個 npm 包之間相互依賴的問題。lerna init 即可創(chuàng)建一個 lerna 項目,建議直接用示例倉庫進行修改。

確定名稱,創(chuàng)建 npm 包

  • 端標識,擴展一個新端首先要確定這個端的標識名稱,例如微信小程序端為wx,百度小程序端為baidu,這個標識決定了構建命令的名稱、多態(tài)協(xié)議中的 cmlType, 配置對象中的 cmlType 等。 示例中確定為demo。
  • 運行時 npm 包名稱, 建議以cml-${端標識名稱}-runtime格式命名,示例中為cml-demo-runtime。
  • api 庫 npm 包名稱, 建議以cml-${端標識名稱}-api格式命名,示例中為cml-demo-api。
  • 內置組件 npm 包名稱, 建議以cml-${端標識名稱}-ui-builtin格式命名,示例中為cml-demo-ui-builtin。
  • 擴展組件 npm 包名稱, 建議以cml-${端標識名稱}-ui格式命名,示例中為cml-demo-ui。
  • 數據管理 npm 包名稱, 建議以cml-${端標識名稱}-store格式命名,示例中為cml-demo-store。
  • 編譯插件 npm 包名稱, 建議以cml-${端標識名稱}-plugin格式命名,示例中為cml-demo-plugin。

在packages目錄創(chuàng)建上述的 6 個 npm 包。

創(chuàng)建開發(fā) CML 項目

在packages目錄創(chuàng)建一個 CML 的項目作為測試項目,開發(fā)過程中可以進行調試代碼。示例中為cml-demo-project。

1 cml-demo-project的chameleon.config.js需要 添加 extPlatform,babelPath,builtinNpmName 字段的配置,配置如下:


cml.config.merge({
  builtinNpmName: 'cml-demo-ui-builtin',
  extPlatform: {
    demo: 'cml-demo-plugin',
  },
  babelPath: [
    path.join(__dirname,'node_modules/cml-demo-ui-builtin'),
    path.join(__dirname,'node_modules/cml-demo-runtime'),
    path.join(__dirname,'node_modules/cml-demo-api'),
  ]
})

你的項目中注意把示例 npm 包名稱改成你命名的 npm 包名稱。

  • builtinNpmName 字段是你定義的內置 npm 包名稱
  • extPlatform 是配置擴展新端的編譯插件,key 值為端標識,value 為編譯插件 npm 包名稱。
  • babelPath 配置運行時相關的三個 npm 包需要過 babel

2 cml-demo-project的package.json的dependencies中添加這幾個開發(fā) npm 包。

"cml-demo-api": "1.0.0",
"cml-demo-plugin": "1.0.0",
"cml-demo-runtime": "1.0.0",
"cml-demo-ui-builtin": "1.0.0"

3 在倉庫的根目錄執(zhí)行l(wèi)erna bootstrap安裝依賴,建立關聯(lián),這樣cml-demo-project的node_modules下的這幾個 npm 包會符號鏈接到packages下的同名 npm 包。

如何進行開發(fā)分工

  • api 的工作是獨立的 不依賴其他 npm 包的開發(fā)
  • runtime 運行時的工作也是獨立的 不依賴其他 npm 包
  • plugin 中的模板編譯 在事件綁定的代理函數中依賴 runtime 其他的工作不依賴
  • ui-builtin 和 ui 依賴 runtime 和 plugin 的完成 才能正確執(zhí)行。 cml-tt-store 的工作也是獨立的,基本上小程序端實現是相同的。

開發(fā)編譯插件

參考cml-demo-plugin/index.js文件 實現編譯類。對項目中的每一個文件或者部分進行編譯處理。處理節(jié)點的source字段,編譯后結果放入output字段。

模板編譯

監(jiān)聽compile-template事件 參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理.cml 文件的模板部分,如果模板是類 vue 語法,內部已經將其轉為標準的 cml 語法,這個階段用于對模板語法進行編譯,生成目標代碼。

compiler.hook('compile-template', function(currentNode, parentNodeType) {
    currentNode.output = templateParser(currentNode.source)
})

mvvm-template-parser這個 npm 包提供了模板編譯的方法。 const {cmlparse, generator, types: t, traverse} = require('mvvm-template-parser');

  • cmlparse 將字符串轉為 ast 語法樹
  • traverse 對語法樹進行遍歷
  • types 語法樹節(jié)點類型判斷
  • generator 語法樹生成字符串

例如模板編譯方法如下:

const {cmlparse, generator, types: t, traverse} = require('mvvm-template-parser');

module.exports = function(content) {
  let ast = cmlparse(content);
  traverse(ast, {
    enter(path) {
      let node = path.node;
      if (t.isJSXElement(node)) {
        let attributes = node.openingElement.attributes;
        attributes.forEach(attr=>{
          if(t.isJSXIdentifier(attr.name) && attr.name.name === 'c-for') {
            attr.name.name = 'wx:for'
          }
          if(t.isJSXIdentifier(attr.name) && attr.name.name === 'c-if') {
            attr.name.name = 'wx:if'
          }
        })
        let tagName = node.openingElement.name.name;
        if(/^origin\-/.test(tagName)) {
          let newtagName = tagName.replace(/^origin\-/,'');
          node.openingElement.name.name = newtagName;
          node.closingElement.name.name = newtagName;
        }
      }
    }
  });
  return generator(ast).code;
}

上面的方法就可以將 模板

<view>
  <view c-for="{{array}}">
  </view>
  <view c-if="{{condition}}"> True </view>
  <origin-button></origin-button>
</view>

編譯為

<view>
  <view wx:for="{{array}}">
  </view>
  <view wx:if="{{condition}}"> True </view>
  <button></button>
</view>

對模板 ast 的編譯本質上是對目標節(jié)點的增刪改,通過類型判斷確定目標節(jié)點. 可以使用網站https://astexplorer.net/ 方便我們確定節(jié)點類型。該網站是將 ast 中的節(jié)點圖形化展示出來,注意選擇javascript,babylon7 jsx。

增刪改的 api 參考 babel 插件編寫文檔https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-replacing-a-node-with-multiple-nodes。

我們沒有直接讓用戶采用 babel 系列 是因為對generator和parser內部都有做針對 CML 的改造。

原生組件的處理

chameleon 中規(guī)定,在模板中使用origin-組件名稱作為組件名稱,代表使用各端原生組件。例如

<template>
  <origin-button></origin-button>
</tempalte>

代表使用原生的<button></button>組件,CML 對于模板的標準編譯沒有處理origin-組件名稱這種標簽名稱,原因是能夠讓用戶根據組件的名稱區(qū)別組件是否是原生組件而做不同的處理,例如原生組件的事件不做代理。所以最后用戶的模板編譯中應該對origin-組件名稱這種組件名稱進行替換。替換方法如下:

traverse(ast, {
    enter(path) {
      let node = path.node;
      if (t.isJSXElement(node)) {
        let tagName = node.openingElement.name.name;
        if(/^origin\-/.test(tagName)) {
          let newtagName = tagName.replace(/^origin\-/,'');
          node.openingElement.name.name = newtagName;
          node.closingElement.name.name = newtagName;
        }
      }
    }
  });

模板編譯總體上是要將所有的CML 的語法編譯成目標端的語法,可以參考 chameleon-template-parse 中的編譯實現。 比如微信端包括如下幾方面的實現:

  • 標簽的替換,比如slider-item 標簽替換成swiper-item。
  • 屬性的替換,c-if c-else c-else-if c-show c-for c-text c-key。
  • 動態(tài)組件,component is 動態(tài)組件的支持。
  • style 與 class 的編譯處理
  • c-model 的實現 配合運行時 mixins 代理事件與處理事件對象
  • c-animation 的處理
  • c-bind 事件綁定 配合運行時 mixins 代理事件與處理事件對象

樣式編譯

監(jiān)聽compile-style事件 參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理所有的 style 節(jié)點,內部已經對 less stylus 等語法進行編譯處理,這里得到的已經是標準的 css 格式,可以轉成對應端的樣式,比如對尺寸單位 cpx 的轉換,將 css 轉成對象形式等。

compiler.hook('compile-style', function(currentNode, parentNodeType) {
  currentNode.output = styleParser(currentNode.source);
})

推薦用戶使用postcss或者rework進行編譯,參考cml-demo-plugin/styleParser.js的實現。利用了chameleon-css-loader中的 postcss 插件進行編譯。例如:

const postcss = require('postcss');
const cpx = require('chameleon-css-loader/postcss/cpx.js')
const weexPlus = require('chameleon-css-loader/postcss/weex-plus.js')

module.exports = function(source) {
  let options = {
    cpxType: 'rpx'
  }
  return postcss([cpx(options), weexPlus()]).process(source).css;
}

上面方法可以將 內容

.test {
  font-size: 24cpx;
  lines: 1;
}

編譯為:

.test {
  font-size: 24rpx;
  lines: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
}

script 編譯

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理script節(jié)點,內部已經對 js 文件進行了 babel 處理,這個階段用于做模塊的包裝,compiler.amd對象提供了 amd 模塊的包裝方法,模塊 id 使用節(jié)點的modId字段。例如:

compiler.hook('compile-script', function(currentNode, parentNodeType) {
  currentNode.output = compiler.amd.amdWrapModule(currentNode.source, currentNode.modId);
})

例如一個節(jié)點 source 如下,modId 為 './component/test.js':

module.exports = function() {
  return 'test';
}

經過compiler.amd.amdWrapModule(currentNode.source, currentNode.modId)處理后成為

cmldefine('./component/test.js', function(require, exports, module) {
  module.exports = function() {
    return 'test';
  }
})

asset 編譯

靜態(tài)資源的編譯和script節(jié)點的編譯相同,因為 cml 內部已經將靜態(tài)資源節(jié)點變成了資源的 publicPath 字符串。 例如src/assets/img/chameleon.png這個節(jié)點的 source 為

 module.exports = 'http://168.1.1.1:8000/static/img/chameleon.png'

經過compiler.amd.amdWrapModule(currentNode.source, currentNode.modId)處理后成為

cmldefine('./component/test.js', function(require, exports, module) {
  module.exports = 'http://168.1.1.1:8000/static/img/chameleon.png';
})

編譯打包與文件輸出

參考cml-demo-plugin/index.js中對compiler.hook('pack', function(projectGraph) {}) pack事件的實現,編譯編譯圖,拼接目標文件,調用compiler.writeFile輸出文件。

開發(fā) api 庫

用戶需要創(chuàng)建一個 npm 包,結構參考cml-demo-api。將chameleon-api中提供的每個方法利用多態(tài)接口擴展語法擴展新端的實現。

** 擴展新端 API(以頭條小程序為例,假設端擴展標識為:tt)**

// 引入官方標準interface文件
<include src="chameleon-api/src/interfaces/alert/index.interface"></include>

// 擴展實現新端(以頭條小程序為例,假設端擴展標識為:tt)
<script cml-type="tt">
class Method implements uiInterface {
  alert(opt, successCallBack, failCallBack) {
    // 根據頭條小程序實現alert彈窗
    let { message, confirmTitle} = opt;
    tt.showModal({
      showCancel: false,
      title: '',
      content: message,
      confirmText: confirmTitle,
      success() {
        successCallBack(confirmTitle);
      },
      fail() {
        failCallBack(confirmTitle);
      }
    });
  }
}

export default new Method();
</script>

// 想覆寫某已有端的方法實現(以微信小程序為例)
<script cml-type="wx">
class Method implements uiInterface {
  alert(opt, successCallBack, failCallBack) {
    // 按你的想法重新實現
  }
}
export default new Method();
</script>

需要注意的是,為了方便結合異步流程控制如 async、await 等進行操作,chameleon 官方提供的 api 接口均以 promise 形式進行返回。所以你也需要在外層使用 js 文件進行包裝,將 interface 實現進行 promise 化或進行其他操作(如傳入默認值)。

import ui from './index.interface';

export default function alert(opt) {
  let { message = '操作成功', confirmTitle = '我知道了' } = opt;
  return new Promise((resolve, reject) => {
    ui.alert({ message, confirmTitle }, resolve, reject);
  });
}

開發(fā)運行時

運行時主要是對 cml 文件的邏輯對象進行適配,chameleon 內部將 cml 文件的邏輯對象分為三類 App、Page、Component。對應會調用用戶運行時 npm 包的createApp、createPage、createComponent方法,所以對外只需要實現這三個方法。

例如一個 Page 的邏輯對象如下:

class PageIndex {
  data = {
    name: 'chameleon'
  }
  computed = {
    name2 () {
      return 'Hello' + this.name;
    }
  }
  beforeCreated() {
  }
  mounted() {
  }
}

export default new PageIndex();

在編譯插件的構造函數中添加上運行時 npm 包名稱,cml-demo-runtime。

  constructor(options) {
    this.runtimeNpmName = 'cml-demo-runtime';
  }

編譯時就會自動插入cml-demo-runtime處理邏輯對象的方法:

class PageIndex {
  data = {
    name: 'chameleon'
  }
  computed = {
    name2 () {
      return 'Hello' + this.name;
    }
  }
  beforeCreated() {
  }
  mounted() {
  }
}
export default new PageIndex();

// 編譯時自動插入用戶配置的運行時方法
import {createPage} from 'cml-demo-runtime';
createPage(exports.default);

createApp、createPage、createComponent 方法,參考 cml-demo-runtime 的結構進行實現,需要 include chameleon-runtime 中相應的接口進行實現,才能夠實現對 chameleon-runtime 的擴展。用戶的工作量主要在于對邏輯對象的處理,可以參考 chameleon-runtime 中的實現方式,一般需要如下方面的適配工作。

從宏觀來看,運行時處理可分為:

  • 輸入 Options 對象的適配
  • 跨端運行時能力注入

從微觀來看,有以下處理:

例如: createPage 方法的實現

<include src="chameleon-runtime/src/interfaces/createPage/index.interface"></include>
<script cml-type="demo">
  class Method implements createPageInterface {
    createPage(options) {
      // 各端自行實現PageRuntime
      let pageRuntime = new PageRuntime(options);

      // 處理props
      pageRuntime.initProps();

      // 處理data
      pageRuntime.initData();

      // 處理computed
      pageRuntime.initComputed();

      // 處理methods
      pageRuntime.initMethods();

      // 處理生命周期映射
      pageRuntime.initLifeCycle();

      //調用小程序原生頁面構造函數
      Page(options);
      return {};
    }
  }

  export default new Method();
</script>

開發(fā)組件庫

組件分為內置組件 chameleon-ui-builtin 和擴展組件 cml-ui。所以用戶需要創(chuàng)建兩個 npm 包分別實現這兩個組件庫,結構參考cml-demo-ui-builtincml-demo-ui。利用多態(tài)組件擴展語法,對原有組件庫中的每一個組件進行新端的實現。

原有組件庫中的組件也分為兩種,一種為各端都有分別實現的多態(tài)組件,例如chameleon-ui-builtin中的button組件。實現起來新端基本上也是要單獨實現。另一種例如chameleon-ui-builtin中的radio組件,各端的實現都是用的chameleon-ui-builtin/components/radio/radio.cml。所以新端基本也可以復用這個實現,(還需要測試情況確實是否可以復用)。

新端獨立實現

例如:

編寫 my-ui-builtin/components/button/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/button/button.interface"></include>

編寫 my-ui-builtin/components/button/button.demo.cml

<template>
  <origin-button
    class="cml-btn"
    c-bind:tap="onclick"
    style="{{mrBtnStyle}}"
    open-type="{{openType}}"
    lang="{{lang}}"
    session-from="{{sessionFrom}}"
    send-message-title="{{sendMessageTitle}}"
    send-message-path="{{sendMessagePath}}"
    send-message-img="{{sendMessageImg}}"
    show-message-card="{{showMessageCard}}"
    app-parameter="{{appParameter}}"
    c-bind:getuserinfo="getuserinfo"
    c-bind:contact="contact"
    c-bind:getphonenumber="getphonenumber"
    c-bind:error="error"
    c-bind:opensetting="opensetting"
  >
    <text class="btn-text" style="{{mrTextStyle}}">{{ text }}</text>
  </origin-button>
</template>
<script>
// js實現部分
</script>
<style scoped>
// 樣式部分
</style>
<script cml-type="json">
// json配置
</script>

#新端復用現有組件

編寫 my-ui-builtin/components/radio/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/radio/radio.interface"></include>
// 復用官方的實現
<script cml-type="demo" src="chameleon-ui-builtin/components/radio/radio.cml"></script>

開發(fā)數據管理

標準實現

邏輯對象協(xié)議

邏輯層負責反饋用戶對界面操作的處理中心。

而 邏輯對象 是邏輯層規(guī)范的輸入口,是運行時方法(ceateApp、createComponent、createPage)的輸入,包括

字段名類型說明
propsObject聲明當前組件可接收數據屬性
props = { type, default }
type為數據類型,default為數據默認值
dataObjectCML模板可直接使用的響應數據,是連接視圖層的樞紐
methodsObject處理業(yè)務邏輯與交互邏輯的方法
watchObject偵聽屬性,監(jiān)聽數據的變化,觸發(fā)相應操作
computedObjectCML模板可直接使用的計算屬性數據,也是連接視圖層的樞紐
beforeCreateFunction例初始化之后,數據和方法掛在到實例之前 一個頁面只會返回一次
createdFunction數據及方法掛載完成
beforeMountFunction開始掛載已經編譯完成的cml到對應的節(jié)點時
mountedFunctioncml模板編譯完成,且渲染到dom中完成
beforeDestroyFunction實例銷毀之前
destroyedFunction實例銷毀后

理解了每個輸入字段代表的含義后,可以擴展(ceateApp、createComponent、createPage)處理邏輯對象,將邏輯對象轉換成當前平臺可接收的格式。

運行時能力注入

映射,并且在各生命周期中,注入新端到 cml 框架的運行時能力

生命周期 hook 映射

每個 CML 實例(App、Page、Component)在被創(chuàng)建時都要經過一系列的初始化過程 ————

例如,需要設置數據監(jiān)聽、編譯模板、將實例掛載到 CML 節(jié)點并在數據變化時更新 CML 節(jié)點等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,這給開發(fā)者在不同階段添加自己的代碼的機會。

CML 為 App、Page、Component 提供了一系列生命周期事件,保障應用有序執(zhí)行。

另外,你還需要實現生命周期多態(tài)。

methods

應用程序運行過程中,提供給上下文this可調用的方法

數據驅動能力

當做數據修改的時候,只需要在邏輯層修改數據,視圖層就會做相應的更新。

注入的核心是:賦予生命周期 hook mixins擴展能力,并且在特定 hook 中,賦予this響應數據變化的能力

data 數據屬性

watch 監(jiān)聽屬性

computed 計算屬性

組件化標準

MVVM 標準中將.cml文件分為三類,src/app/app.cml為 app,router.config.json中配置的路由對應的文件為 page,其他的.cml文件為 component。

組件引用

在.cml文件的<script cml-type="json"></script>json 部分,usingComponents字段中聲明組件的引用。 例如:

{
  "base":{
    "usingComponents": {
      "navi": "/components/navi/navi",
      "c-cell": "../components/c-cell/c-cell",
      "navi-npm": "cml-test-ui/navi/navi"
    }
  }
}

usingComponents對象中,key 為組件名稱,組件名稱為小寫字母、中劃線和下劃線組成。value 為組件路徑,組件路徑的規(guī)則如下:

  • 相對當前文件的相對路徑
  • src 下的絕對路徑
  • node_modules 下的組件直接從 npm 的包名稱開始寫例如 cml-test-ui/navi/navi
  • 組件的路徑禁止包含后綴擴展名,查找的優(yōu)先級為1.interface 文件指向的多態(tài)組件2 .cml 文件3 用戶通過 hook: find-component 找到的組件

組件使用

組件在 CML 模板中使用,組件名為 usingComponents 中的 key 值,組件使用形式為閉合標簽,標簽名為組件名。例如:

<template>
  <c-cell><c-cell>
</template>
<script cml-type="json">
{
  "base":{
    "usingComponents": {
      "c-cell": "../components/c-cell/c-cell"
    }
  }
}
</script>

事件系統(tǒng)標準

Chameleon 支持一些基礎的原生事件和自定義事件,保障各端效果一致運行。

  • 事件是視圖層到邏輯層的通訊方式。
  • 事件可以將用戶的行為反饋到邏輯層進行處理。
  • 事件可以綁定在組件上,當達到觸發(fā)事件,就會執(zhí)行邏輯層中對應的事件處理函數。

原生事件標準

當用戶點擊該組件的時候會在該組件邏輯 VM 對象的 methods 中尋找相應的處理函數,該處理函數會收到一個事件對象。

原生基礎事件的類型
類型觸發(fā)條件
tap手指觸摸后馬上離開
touchstart手指觸摸動作開始
touchmove手指觸摸后移動
touchend手指觸摸動作結束
原生事件對象

它有以下屬性:

名稱類型說明
typeString事件類型
timeStampNumber頁面打開到觸發(fā)事件所經過的毫秒數
targetObject觸發(fā)事件的目標元素 且 target = { id, dataset }
currentTargetObject綁定事件的目標元素 且 currentTarget = { id, dataset }
touchesArray觸摸事件中的屬性,當前停留在屏幕中的觸摸點信息的數組 且 touches = [{ identifier, pageX, pageY, clientX, clientY }]
changedTouchesArray觸摸事件中的屬性,當前變化的觸摸點信息的數組 且 changedTouches = [{ identifier, pageX, pageY, clientX, clientY }]
detailObject自定義事件所攜帶的數據。 通過`$cmlEmit`方法觸發(fā)自定義事件,可以傳遞自定義數據即detail。具體下面`自定義事件`。
_originEventObjectCML 對各平臺的事件對象進行統(tǒng)一,會把原始的事件對象放到_originEvent屬性中,當需要特殊處理的可以進行訪問。

target && currentTarget 事件屬性

屬性類型說明
idString事件源組件的id
datasetObject事件源組件上由`data-`開頭的自定義屬性組成的集合

touches && changedTouches 事件屬性

數組中的對象具有如下屬性:

屬性類型說明
identifierNumber觸摸點的標識符
pageX, pageYNumber距離文檔左上角的距離,文檔的左上角為原點 ,橫向為X軸,縱向為Y軸
clientX, clientYNumber距離頁面可顯示區(qū)域(屏幕除去導航條)左上角距離,橫向為X軸,縱向為Y軸

自定義事件標準

自定義事件用于父子組件之間的通信,父組件給子組件綁定自定義事件,子組件內部觸發(fā)該事件。綁定事件的方法是以bind+事件名稱="事件處理函數的形式給組件添加屬性,規(guī)定事件名稱不能存在大寫字母觸發(fā)事件的方法是調用this.$cmlEmit(事件名稱,detail對象)。

狀態(tài)管理

數據管理 createStore

創(chuàng)建返回數據 store 實例

store 實例方法

Store.commit(type: string, payload?: any)

Store.dispatch(type: string, payload?: any)

Store.mapState(map:Array\<string\>|Object\<string\>): Object
Store.mapGetters(map:Array\<string\>|Object\<string\>): Object
Store.mapMutations(map:Array\<string\>|Object\<string\>): Object

Store.mapActions(map:Array\<string\>|Object\<string\>): Object

Store.registerModule(path: String, module: Module)

多態(tài)擴展

多態(tài)接口擴展

(chameleon-tool@0.4.0 以上開始支持)

多態(tài)接口一節(jié)講解了多態(tài)接口的使用,這一節(jié)講解多態(tài)接口的一些擴展語法,更適用于擴展新端使用。

擴展語法

include標簽

當前 interface 文件用<include>標簽可以引入其他.interface文件,代表繼承該文件的接口定義和實現。然后在當前文件中去實現引入的接口定義,可以覆寫引入的某一端實現,也可以去擴展新端的實現。 語法是<include src="${引用路徑}.interface"></include>,有如下規(guī)則:

  • src 屬性指向的文件路徑不支持 webpack 別名,只能是相對路徑或者 npm 包名開始。
#例如擴展新端 API(以頭條小程序為例,假設端擴展標識為:toutiao):
// 引入官方標準interface文件
<include src="chameleon-api/src/interfaces/alert/index.interface"></include>

// 擴展實現新端(以頭條小程序為例,假設端擴展標識為:toutiao)
<script cml-type="toutiao">
class Method implements uiInterface {
  alert(opt, successCallBack, failCallBack) {
    // 根據頭條小程序實現alert彈窗
    let { message, confirmTitle} = opt;
    tt.showModal({
      showCancel: false,
      title: '',
      content: message,
      confirmText: confirmTitle,
      success() {
        successCallBack(confirmTitle);
      },
      fail() {
        failCallBack(confirmTitle);
      }
    });
  }
}

export default new Method();
</script>

// 想覆寫某已有端的方法實現(以微信小程序為例)
<script cml-type="wx">
class Method implements uiInterface {
  alert(opt, successCallBack, failCallBack) {
    // 按你的想法重新實現
  }
}
export default new Method();
</script>

script src 屬性

<script></script>標簽可以通過指定src的方式引用外部 js 文件為某一平臺或某幾個的實現或者接口定義。 有如下規(guī)則:

  • src 屬性指向的文件路徑不支持 webpack 別名,只能是相對路徑或者 npm 包名開始。
  • cml-type 屬性為字符串,如果多個端可以用英文逗號 , 進行分割,例如 cml-type="web,wx".

例如:

<script cml-type="interface">
interface FirstInterface {
  getMsg(msg: String): String;
}

</script>

<script cml-type="web" src="./web.js"></script>

<script cml-type="wx,alipay" src="./miniapp.js"></script>

web.js文件內容如下:

class Method implements FirstInterface {
  getMsg(msg) {
    return 'web:' + msg;
  }
}

export default new Method();

多態(tài) API 查找優(yōu)先級

沒有擴展語法之前,每個端的實現都在同一個文件中,沒有優(yōu)先級的問題,但是有了擴展語法之后,就會有優(yōu)先級問題。查找優(yōu)先級如下:

  • 文件內部的定義, 包括采用 src 的形式指定其他文件
  • <include>src屬性指向的 interface 文件中繼續(xù)查找該部分

例如有如下兩個 interface 文件:

├── first
│   └── first.interface
└── second
    └── second.interface

first.interface 包括接口定義,web 和 Weex 端的實現,內容如下:

<script cml-type="interface">
interface FirstInterface {
  getMsg(msg: String): String;
}
</script>
<script cml-type="web">
class Method implements FirstInterface {
  getMsg(msg) {
    return 'first web:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="weex">
class Method implements FirstInterface {
  getMsg(msg) {
    return 'first weex:' + msg;
  }
}
export default new Method();
</script>

second.interface 包括對first.interface的include Weex 端和 wx 端的實現,內容如下:

<include src="../first/first.interface"></include>
<script cml-type="weex">
class Method implements FirstInterface {
  getMsg(msg) {
    return 'second weex:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="wx">
class Method implements FirstInterface {
  getMsg(msg) {
    return 'second wx:' + msg;
  }
}
export default new Method();
</script>

當外部引用second.interface文件并調用 getMsg 方法時 各端編譯獲取方法如下:

  • Web 端,因為second.interface中沒有 Web 端實現 所以查找到first.interface中 Web 端 getMsg 方法
  • Weex 端,因為second.interface中有 Weex 端實現 所以使用second.interface中 Weex 端 getMsg 方法
  • wx 端,因為second.interface中有 wx 端實現 所以使用second.interface中 wx 端 getMsg 方法
注意 cml-type interface 部分必須是唯一的

例 1:

// 引入chameleon-api interface文件
<include src="chameleon-api/src/interfaces/alert/index.interface"></include>

// 錯誤實現interface部分
<script cml-type="interface">
......
</script>

// 擴展實現新端
<script cml-type="demo">
......
</script>

<include>的chameleon-api/src/interfaces/alert/index.interface文件中已經有cml-type="interface"的定義,所以當前文件中<script cml-type="interface"></script>定義部分是錯誤的。

例 2:

// 引入cml-tt-api interface文件
<include src="cml-tt-api/src/interfaces/alert/index.interface"></include>

// 引入cml-quickapp-api interface文件
<include src="cml-quickapp-api/src/interfaces/alert/index.interface"></include>

// 擴展實現新端
<script cml-type="demo">
......
</script>

文件中引入了兩個interface文件,但是他們內部找到的<script cml-type="interface"></script>定義部分都在chameleon-api/src/interfaces/alert/index.interface中,是同一文件。所以不認為是錯誤的。

多態(tài)組件擴展

(chameleon-tool@0.4.0 以上開始支持)

多態(tài)組件一節(jié)講解了多態(tài)組件的使用,這一節(jié)講解多態(tài)組件的一些擴展語法,更適用于擴展新端使用。

擴展語法

include標簽

多態(tài)組件的.interface文件內部可以用<include>標簽引入其他多態(tài)組件的.interface文件,代表采用該文件的接口定義和對應的各端實現。 語法是<include src="${引用路徑}.interface"></include>,有如下規(guī)則:

  • src 屬性指向的文件路徑不支持 webpack 別名,只能是相對路徑或者 npm 包名開始。
  • cml-type interface 部分必須是唯一的
例如擴展新端組件(以頭條小程序為例,假設端擴展標識為:toutiao):

編寫 my-ui-builtin/button/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/button/button.interface"></include>

再編寫 my-ui-builtin/button/button.toutiao.cml

//
擴展實現新端(以頭條小程序為例,假設端擴展標識為:toutiao),具體代碼可參考在其他端的實現如:chameleon-ui-builtin/components/button/button.wx.cml
<template>
  <button
    class="cml-btn"
    c-bind:tap="onclick"
    style="{{mrBtnStyle}}"
    open-type="{{openType}}"
    lang="{{lang}}"
    session-from="{{sessionFrom}}"
    send-message-title="{{sendMessageTitle}}"
    send-message-path="{{sendMessagePath}}"
    send-message-img="{{sendMessageImg}}"
    show-message-card="{{showMessageCard}}"
    app-parameter="{{appParameter}}"
    c-bind:getuserinfo="getuserinfo"
    c-bind:contact="contact"
    c-bind:getphonenumber="getphonenumber"
    c-bind:error="error"
    c-bind:opensetting="opensetting"
  >
    <text class="btn-text" style="{{mrTextStyle}}">{{ text }}</text>
  </button>
</template>
<script>
// js實現部分
</script>
<style scoped>
// 樣式部分
</style>
<script cml-type="json">
// json配置
</script>

這樣在引用my-ui-builtin/button/button這個多態(tài)組件時,就會有chameleon-ui-builtin中 button 支持的端和自己擴展的端。

script src 屬性

<script></script>標簽可以通過指定src的方式引用 cml 文件作為某一平臺或某幾個平臺的實現或者接口定義。 有如下規(guī)則:

  • src 屬性指向的文件路徑不支持 webpack 別名,只能是相對路徑或者 npm 包名開始。
  • cml-type 屬性為字符串,如果多個端可以用英文逗號 , 進行分割,例如 cml-type="web,wx"。

例如上面擴展 button 的例子可以寫成 下面的形式 編寫 my-ui-builtin/button/button.interface

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/button/button.interface"></include>
<script cml-type="toutiao" src="./mybutton.cml"></script>

再編寫 my-ui-builtin/button/mybutton.cml

//
擴展實現新端(以頭條小程序為例,假設端擴展標識為:toutiao),具體代碼可參考在其他端的實現如:chameleon-ui-builtin/components/button/button.wx.cml
<template>
  <button
    class="cml-btn"
    c-bind:tap="onclick"
    style="{{mrBtnStyle}}"
    open-type="{{openType}}"
    lang="{{lang}}"
    session-from="{{sessionFrom}}"
    send-message-title="{{sendMessageTitle}}"
    send-message-path="{{sendMessagePath}}"
    send-message-img="{{sendMessageImg}}"
    show-message-card="{{showMessageCard}}"
    app-parameter="{{appParameter}}"
    c-bind:getuserinfo="getuserinfo"
    c-bind:contact="contact"
    c-bind:getphonenumber="getphonenumber"
    c-bind:error="error"
    c-bind:opensetting="opensetting"
  >
    <text class="btn-text" style="{{mrTextStyle}}">{{ text }}</text>
  </button>
</template>
<script>
// js實現部分
</script>
<style scoped>
// 樣式部分
</style>
<script cml-type="json">
// json配置
</script>

多態(tài)組件查找優(yōu)先級

沒有擴展語法之前,采用的是各端的實現和.interface文件保持同名的方式,各端有各自的端標識后綴,有了擴展語法之后這種約定同名的方式被打破。 多態(tài)組件的查找從 interface 作為入口進行查找,多態(tài)組件的查找,包含在組件的查找內,所以這里直接講組件的查找優(yōu)先級。 以一個例子進行說明 組件引用

{
  "usingComponents": {
    "demo-com": "/componnets/button/button"
  }
}

文件結構

├── components
  ├── button
    └── button.interface
    └── custombutton.cml
    └── button.toutiao.cml
    └── button.cml

button.interface內容如下:

// 引入官方標準interface文件
<include src="chameleon-ui-builtin/components/button/button.interface"></include>
<script cml-type="toutiao" src="./custombutton.cml"></script>

當前端標識為toutiao查找優(yōu)先級如下:

  • 1 找/componnets/button/button.interface文件,如果里面<script>的 cmltype 等于當前端標識 toutiao,則找到<script>的 src 屬性指向的 cml 文件。本例子中是custombutton.cml
  • 2 找/componnets/button/button.interface文件 對應同名的帶有當前端標識的 cml 文件,本例子中是button.toutiao.cml
  • 3 找/componnets/button/button.interface文件,里面的<include></include>標簽的 src 指向的.interface文件,以該文件進行遞歸執(zhí)行 1,2 步的查找。
  • 3 找非多態(tài)組件/componnets/button/button.cml文件
  • 4 找各平臺的原生組件例如/componnets/button/button.vue,/componnets/button/button.wxml ,還有擴展新端中用戶可以自定義查找
注意 cml-type interface 部分必須是唯一的

例 1:

// 引入cml-ui interface文件
<include src="cml-ui/src/components/c-tab/c-tab.interface"></include>

// 錯誤實現interface部分
<script cml-type="interface"></script>

// 擴展實現新端
<script cml-type="demo" src="......"></script>

<include>的cml-ui/src/components/c-tab/c-tab.interface文件中已經有cml-type="interface"的定義,所以當前文件中定義<script cml-type="interface"></script>部分是錯誤的。

例 2:

// 引入cml-tt-ui interface文件
<include src="cml-tt-ui/src/components/c-tab/c-tab.interface"></include>

// 引入cml-quickapp-ui interface文件
<include src="cml-quickapp-ui/src/components/c-tab/c-tab.interface"></include>

// 擴展實現新端
<script cml-type="demo" src="......"></script>

文件中引入了兩個interface文件,但是他們內部找到的<script cml-type="interface"></script>定義部分都在cml-ui/src/components/c-tab/c-tab.interface中,是同一文件。所不認為是錯誤的。

新端編譯插件擴展

命令行

安裝chameleon-tool@1.0.3 進行擴展新端的開發(fā)。

擴展新端總體編譯流程

擴展新端 首先要了解擴展新端總體的編譯流程,理解用戶擴展新端的工作處于編譯的什么階段。

  • 黃色部分表示 cml 命令行及 webpack 編譯
  • 藍色部分表示擴展的 mvvm+編譯
  • 綠色部分表示用戶要實現的部分,整個項目的文件會用一張編譯圖表示,用戶提供針對每一個節(jié)點的編譯處理,全部編譯完成后,進行文件的拼接打包。

編譯圖與編譯節(jié)點

在總體編譯流程中,webpack 編譯完成后,會將 webpack 編譯的結果轉成標準的 mvvm 編譯圖 projectGraph,這個圖由 CMLNode 節(jié)點構成,先理解編譯圖的組織形式和節(jié)點的數據結構,對用戶寫編譯插件有很大幫助。

編譯節(jié)點

編譯節(jié)點是 CMLNode 類的實例,CMLNode 定義如下:

class CMLNode {
  constructor(options = {}) {
    this.ext;
    this.realPath; // 文件物理地址  會帶參數
    this.nodeType; // app/page/component/module // 節(jié)點類型     app/page/component  其他的為module  cml文件中的每一個部分也是一個Node節(jié)點
    this.moduleType; // template/style/script/json/asset
    this.dependencies = []; // 該節(jié)點的直接依賴       app.cml依賴pages.cml pages.cml依賴components.cml js依賴js
    this.childrens = []; // 子模塊 cml文件才有子模塊
    this.parent; // 父模塊 cml文件中的子模塊才有
    this.source; // 模塊源代碼
    this.convert; // 源代碼的格式化形式
    this.output; // 模塊輸出  各種過程操作該字段
    this.identifier; // 節(jié)點唯一標識
    this.modId; // 模塊化的id requirejs
    this.extra; // 節(jié)點的額外信息
    Object.keys(options).forEach(key => {
      this[key] = options[key];
    })
  }
}

具體字段含義如下:

字段含義
nodeType節(jié)點類型,分為app/page/component/module,其中只有src/app/app.cml類型為app, router.config.json中配置的cml文件為page,其他的cml文件為component。非cml文件為Module
moduleType模塊類型,當節(jié)點的nodeType為app/page/component時,其moduleType為undefined。cml文件中四個部分的moduleType分別為template、script、style、json。其他節(jié)點的nodeType為module時,根據文件后綴判斷moduleType。
后綴moduleType值
/\.css|\.less|\.stylus|\.styls$/style
/\.js|\.interface$/script
/\.json$/json
/\.(png|jpe?g|gif|svg|mp4|webm|ogg|mp3|wav|flac|aac|woff|woff2?|eot|ttf|otf)(\?.*)?$asset
其他后綴other
dependencies節(jié)點的依賴節(jié)點,app依賴page page依賴component script節(jié)點中依賴require的節(jié)點
childrens節(jié)點的子節(jié)點,只有cml文件才會有子節(jié)點,子節(jié)點為cml文件的四個部分,分別為四個節(jié)點
parent節(jié)點的父節(jié)點,只有cml文件節(jié)點的子節(jié)點才有父節(jié)點
originSource節(jié)點編譯前源代碼(目前只有script節(jié)點有該字段)
source經過mvvm標準編譯之后節(jié)點的代碼
convertsource的轉換格式,source均為字符串,convert可能裝成AST或者JSON對象
output節(jié)點的輸出內容,建議用戶編譯可以將編譯結果放在output字段用于輸出
identifier節(jié)點的唯一標識,是webpack module中的request字段,保證了唯一性
modId節(jié)點的模塊id,用于js的模塊化id標識
extra節(jié)點的額外信息,例如template節(jié)點就會添加上模板使用的原生組件和內置組件信息
ext文件后綴,例如.js 注意如果引用資源后有參數也會帶著,例如 .png?__inline
realPath節(jié)點對應的文件路徑,注意如果引用資源后有參數也會帶著,例如 /user/didi/yyl/project/chameleon.png?__inline

編譯圖組織結構

編譯圖由上節(jié)介紹的編譯節(jié)點組成,以 nodeType 為 app 的節(jié)點開始形成編譯圖,內部會遞歸編譯節(jié)點,根據節(jié)點的類型觸發(fā)相應的用戶編譯。

如何編寫用戶插件

擴展內置組件庫和內置 API 庫是獨立的兩個 NPM 包,其他編譯相關的工作都放在用戶插件中。

確定端標識名稱

擴展一個新端首先要確定這個端的標識名稱,例如微信小程序端為wx,百度小程序端為baidu,這個標識決定了構建命令的名稱、多態(tài)協(xié)議中的 cmlType, 配置對象中的 cmlType 等。

配置插件

在項目的 chameleon.config.js 中配置構建目標端命令時要執(zhí)行的插件,這里配置的只是插件的名稱,后面會講解插件的寫法。配置的字段為extPlatform Object 類型,key 值為上一步確定的端標識名稱,value 為要實現的插件的 npm 包名稱, 例如要擴展頭條小程序,確定標識為toutiao。

cml.config.merge({
  extPlatform: {
    toutiao: 'cml-toutiao-plugin'
  }
})

當執(zhí)行cml 端標識名稱 dev|build時將走用戶插件進行編譯。

擴展新端插件

上一步講解了如何配置端標識命令對應的用戶插件,這里講一下插件該如何編寫。插件是一個類,要求是 npm 包的入口。下面展示出這個類的屬性和方法。


module.exports = class ToutiaoPlugin {
    constructor(options) {
      let { cmlType, media} = options;
      this.webpackRules = []; // webpack的rules設置  用于當前端特殊文件處理
      this.moduleRules = []; // 文件后綴對應的節(jié)點moduleType
      this.logLevel = 3;
      this.originComponentExtList = ['.wxml']; // 用于擴展原生組件的文件后綴查找
      this.runtimeNpmName = 'cml-demo-runtime'; // 指定當前端的運行時庫
      this.builtinUINpmName = 'cml-demo-ui-builtin'; // 指定當前端的內置組件庫
      this.cmlType = cmlType;
      this.media = media;
      this.miniappExt = {  // 小程序原生組件處理
        rule: /\.wxml$/,
        mapping: {
          'template': '.wxml',
          'style': '.wxss',
          'script': '.js',
          'json': '.json'
        }
      }
      // 需要壓縮文件的后綴
      this.minimizeExt = {
        js: ['.js'],
        css: ['.css','.wxss']
      }
    }
  /**
   * @description 注冊插件
   * @param {compiler} 編譯對象
   * */
  register(compiler) {
      /**
       * cml節(jié)點編譯前
       * currentNode 當前節(jié)點
       * nodeType 節(jié)點的nodeType
       */
      compiler.hook('compile-preCML', function(currentNode, nodeType) {

      })
      /**
       * cml節(jié)點編譯后
       * currentNode 當前節(jié)點
       * nodeType 節(jié)點的nodeType
       */
      compiler.hook('compile-postCML', function(currentNode, nodeType) {

      })

      /**
       * 編譯script節(jié)點,比如做模塊化
       * currentNode 當前節(jié)點
       * parentNodeType 父節(jié)點的nodeType
       */
      compiler.hook('compile-script', function(currentNode, parentNodeType) {

      })

      /**
       * 編譯template節(jié)點 語法轉義
       * currentNode 當前節(jié)點
       * parentNodeType 父節(jié)點的nodeType
       */
      compiler.hook('compile-template', function(currentNode, parentNodeType) {

      })

      /**
       * 編譯style節(jié)點  比如尺寸單位轉義
       * currentNode 當前節(jié)點
       * parentNodeType 父節(jié)點的nodeType
       */
      compiler.hook('compile-style', function(currentNode, parentNodeType) {

      })

      /**
       * 編譯json節(jié)點
       * currentNode 當前節(jié)點
       * parentNodeType 父節(jié)點的nodeType
       */
      compiler.hook('compile-json', function(currentNode, parentNodeType) {

      })

      /**
       * 編譯other類型節(jié)點
       * currentNode 當前節(jié)點
       */
      compiler.hook('compile-other', function(currentNode) {

      })


      /**
       * 編譯結束進入打包階段
       */
      compiler.hook('pack', function(projectGraph) {
        // 遍歷編譯圖的節(jié)點,進行各項目的拼接
        //調用writeFile方法寫入文件
        // compiler.writeFile()
      })
  }
}

下面對插件類中的每一個屬性和方法的使用進行介紹。

插件構造函數參數

  constructor(options) {
    let { cmlType, media} = options;
  }

用戶插件的構造函數會接受options參數,cmlType是當前端標識名稱,例如web|wx|weex, media是構建的模式,dev|build。

this.logLevel

類型:Number 日志的等級, 可取值 0,1,2,3,默認值為 2,值越大顯示日志越詳細。

this.originComponentExtList

類型:Array 用于設置原生組件的文件后綴,適用于多態(tài)組件的查找。 例如 usingComponents 中對于組件的引用是沒有后綴的,用戶可以對其進行擴展,再進行組件查找時會嘗試用戶設置的后綴,一般用于多態(tài)組件調用底層原生組件。 例如微信小程序中:

this.originComponentExtList = ['.wxml']; // 用于擴展原生組件的文件后綴查找
this.runtimeNpmName

類型:String 用于設置當前端運行時 npm 包名稱。

this.builtinUINpmName

類型:String 用于設置當前端內置組件 npm 包名稱。

this.minimizeExt

類型:Object

  {
    js: Array,
    css: Array
  }

內置了兩種代碼壓縮,一種是 js 一直是 css,用戶指定輸出文件后綴對應的壓縮類型。例如微信小程序中:

  this.minimizeExt = {
    js: ['.js'],
    css: ['.css','.wxss']
  }
this.miniappExt

類型:Object chameleon 內置了針對小程序類的原生組件的處理方法,只需要用戶進行文件后綴的配置。例如微信小程序:

this.miniappExt = {  // 小程序原生組件處理
  rule: /\.wxml$/,
  mapping: {
    'template': '.wxml',
    'style': '.wxss',
    'script': '.js',
    'json': '.json'
  }
}

rule 的正則匹配文件后綴和this.originComponentExtList中的設置的文件后綴保持一致。 mapping 中的四個部分配置小程序對應的文件后綴。

this.webpackRules

類型:Array 當用戶有其他文件類型的原生組件要處理,可以通過配置 weback 的 module.rules 字段,用于擴展目標端特殊文件類型的處理,用戶可以擴展 webpack 編譯過程的 loader。例如:

this.webpackRules = [{
  test: /\.vue$/,
  use: [{
    loader:'vue-loader',
    options: {}
  }]
}]

在 loader 中可以設置 this._module 中的一些字段控制生成的CMLNode的內容。

  • this._module._cmlSource 設置CMLNode的 source 字段
  • this._module._nodeType 設置CMLNode的 nodeType 字段
  • this._module._moduleType 設置CMLNode的 moduleType 字段
  • this._module._cmlExtra 設置CMLNode的 extra 字段
this.moduleRules

類型:Array 設置文件類型對應的 moduleType,可以配合 webpackRules 使用,內置對應關系如下:

 [ // 文件后綴對應module信息
  {
    test: /\.css|\.less|\.stylus|\.styls$/,
    moduleType: 'style'
  },
  {
    test: /\.js|\.interface$/,
    moduleType: 'script'
  },
  {
    test: /\.json$/,
    moduleType: 'json'
  },
  {
    test: /\.(png|jpe?g|gif|svg|mp4|webm|ogg|mp3|wav|flac|aac|woff|woff2?|eot|ttf|otf)(\?.*)?$/,
    moduleType: 'asset'
  }
]

例如用戶可以擴展.vue類型的 moduleType 為vue。

this.webpackRules = [{
  test: /\.vue$/,
  moduleType: 'vue'
}]

在遞歸觸發(fā)用戶編譯階段的鉤子名稱,也是根據節(jié)點的moduleType決定,所以用戶擴展了節(jié)點的moduleType,相應這個節(jié)點觸發(fā)的編譯鉤子也為compile-${moduleType}, 上面的例子中觸發(fā)compile-vue。

register 方法

register 方法中接受compiler對象,該對象是編譯的核心對象,用戶通過該對象注冊編譯流程。

compiler.hook 方法

使用該方法可以注冊編譯流程,第一個參數是鉤子名稱,第二個參數是處理函數,處理函數中會接收編譯流程對應的參數,下面說明每一個鉤子的作用和參數。

compiler.hook(鉤子名稱, function(參數) {

})

compile-preCML

參數列表:(currentNode,nodeType)

  • currentNode 當前處理的節(jié)點
  • nodeType 當前節(jié)點的 nodeType,app/page/component

說明:

這個鉤子是編譯 cml 文件節(jié)點之前觸發(fā),并且傳遞 cml 文件節(jié)點,可以通過該鉤子去處理 cml 文件節(jié)點的template、json、style、script四個子節(jié)點之前需要的聯(lián)系。

compile-postCML

參數列表:(currentNode,nodeType)

  • currentNode 當前處理的節(jié)點
  • nodeType 當前節(jié)點的 nodeType,app/page/component

說明:

這個鉤子是編譯完 cml 文件節(jié)點的依賴和子節(jié)點后觸發(fā),傳遞 cml 文件節(jié)點,可以通過該鉤子去處理 cml 文件節(jié)點編譯之后的處理。

compile-script

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理nodeType='module',moduleType='script'的節(jié)點,內部已經對 js 文件進行了 babel 處理,這個階段用于做模塊的包裝,compiler.amd對象提供了 amd 模塊的包裝方法,模塊 id 使用節(jié)點的modId字段。例如:

compiler.hook('compile-script', function(currentNode, parentNodeType) {
  currentNode.output = compiler.amd.amdWrapModule(currentNode.source, currentNode.modId);
})

compile-template

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理nodeType='module',moduleType='template'的節(jié)點,如果模板是類 vue 語法,內部已經將其轉為標準的 cml 語法,這個階段用于對模板語法進行編譯,生成目標代碼,轉義可以采用mvvm-template-parsernpm 包提供的方法,可以將模板字符串轉為 ast 語法樹進行操作。例如:

const {cmlparse,generator,types,traverse} = require('mvvm-template-parser');
compiler.hook('compile-template', function(currentNode, parentNodeType) {
    let ast = cmlparse(currentNode.source);
    traverse(ast, {
      enter(path) {
        //進行轉義
      }
    });
    currentNode.output =  generate(ast).code;
})

compile-style

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理nodeType='module',moduleType='style'的節(jié)點,內部已經對 less stylus 等語法進行編譯處理,這里得到的已經是標準的 css 格式,可以轉成對應端的樣式,比如對尺寸單位 cpx 的轉換,將 css 轉成對象形式等。例如:

compiler.hook('compile-style', function(currentNode, parentNodeType) {
   //利用編寫postcss插件的形式進行轉義
    let output = postcss([cpx()]).process(currentNode.source).css;
    currentNode.output =  output;
})

compile-json

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理nodeType='module',moduleType='json'的節(jié)點。例如:

compiler.hook('compile-json', function(currentNode, parentNodeType) {
  let jsonObj = currentNode.convert;
  jsonObj.name = "用戶自定義操作"
  currentNode.output = JSON.stringify(jsonObj);
})

compile-other

參數列表:(currentNode)

  • currentNode 當前處理的節(jié)點

說明: 這個鉤子用于處理nodeType='module',moduleType='other'的節(jié)點。對于不是 cml 識別的模塊類型進行編譯。

compile-asset

參數列表:(currentNode,parentNodeType)

  • currentNode 當前處理的節(jié)點
  • parentNodeType 父節(jié)點的 nodeType,如果是 app/page/component 節(jié)點的子節(jié)點會有值,否則為 undefined

說明: 這個鉤子用于處理nodeType='module',moduleType='asset'的節(jié)點,資源節(jié)點內部已經將其 source 轉為 js 語法,返回資源的publichPath,所以將其等同于script節(jié)點進行處理。例如:

compiler.hook('compile-script', function(currentNode, parentNodeType) {
  currentNode.output = compiler.amd.amdWrapModule(currentNode.source, currentNode.modId);
})

pack

參數列表:(projectGraph)

  • projectGraph 編譯圖根節(jié)點

說明: 所有編譯結束之后觸發(fā)這個鉤子,在這個鉤子中編譯圖,拼接目標端的文件內容,調用compiler.writeFile()方法寫入要生成的文件路徑及內容。

compiler.hook('pack', function(projectGraph) {
  // 遍歷編譯圖的節(jié)點,進行各項目的拼接
  //調用writeFile方法寫入生成文件
  compiler.writeFile('/app.json',projectGraph.output)
})
compiler.amd

compiler.amd 對象提供了 js 語言的 AMD 模塊化方案供開發(fā)者使用,

ompiler.amd.amdWrapModule

參數列表 ({content, modId})

  • content js 模塊的內容
  • modId 該模塊的 Id

說明: 將 js 模塊包裝成 amd 模塊。 例如 modId 為src/pages/index/index.cml,content 為如下的模塊:

class Index  {
  data = {
    title: "chameleon",
    chameleonSrc: require('../../images/images/chameleon.png')
  }
}

export default new Index();

調用 compiler.amd.amdWrapModule 后返回的結果為:

cmldefine('src/pages/index/index.cml', function(require, exports, module) {
  class Index  {
    data = {
      title: "chameleon",
      chameleonSrc: require('../../images/images/chameleon.png')
    }
  }

  export default new Index();
})

compiler.amd.getGlobalBootstrap

參數列表 (globalName)

  • globalName 環(huán)境的全局變量

說明: 該方法返回 js amd 模塊方案的啟動腳本,這個腳本是將 cmldefine 和 cmlrequire 都放到用戶傳遞的全局變量上。返回代碼如下:


(function(cmlglobal) {
  cmlglobal = cmlglobal || {};
  cmlglobal.cmlrequire;

  var factoryMap = {};

  var modulesMap = {};
  cmlglobal.cmldefine = function(id, factory) {
    factoryMap[id] = factory;
  };

  cmlglobal.cmlrequire = function(id) {
    var mod = modulesMap[id];
    if (mod) {
      return mod.exports;
    }

    var factory = factoryMap[id];
    if (!factory) {
      throw new Error('[ModJS] Cannot find module `' + id + '`');
    }

    mod = modulesMap[id] = {
      exports: {}
    };

    var ret = (typeof factory == 'function')
      ? factory.apply(mod, [require, mod.exports, mod])
      : factory;

    if (ret) {
      mod.exports = ret;
    }
    return mod.exports;
  };

})($GLOBAL); // 全局變量

compiler.amd.getModuleBootstrap

無參數。

說明: 某些平臺沒有提供全局變量,所有 amd 的啟動腳本也提供了模塊化的方案。返回如下代碼:


/**
 * 模塊型
 */
(function() {

  var factoryMap = {};

  var modulesMap = {};
  var cmldefine = function(id, factory) {
    factoryMap[id] = factory;
  };

  var cmlrequire = function(id) {
    var mod = modulesMap[id];
    if (mod) {
      return mod.exports;
    }

    var factory = factoryMap[id];
    if (!factory) {
      throw new Error('[ModJS] Cannot find module `' + id + '`');
    }

    mod = modulesMap[id] = {
      exports: {}
    };

    var ret = (typeof factory == 'function')
      ? factory.apply(mod, [require, mod.exports, mod])
      : factory;

    if (ret) {
      mod.exports = ret;
    }
    return mod.exports;
  };

  module.exports = {
    cmldefine,
    cmlrequire
  }
})();

compiler.writeFile

參數列表 (filePath, content)

  • filePath 文件路徑相對路徑,會在項目根目錄dist/${端標識}下拼接上 filePath。
  • content 輸出的文件內容 String or Buffer

說明: 在pack鉤子中通知用戶所有節(jié)點已經編譯完成,用戶可以遍歷projectGraph編譯圖,進行目標文件的拼接,調用compiler.writeFile方法進行寫出,pack鉤子執(zhí)行完畢后,內部編譯將輸出文件。例如:

let appJson = {
  window: {
    "navigationBarTitleText": "Chameleon"
  }
}
compiler.writeFile('/app.json', JSON.stringify(appJson))
compiler.getRouterConfig

參數列表: 無參數 返回值結構:

{
  projectRouter,
  subProjectRouter
}

說明:projectRouter 為當前項目的 router.config.json 的對象形式。 subProjectRouter 為包含所有子項目的 router.config.json 對象,例如如下配置子項目:

cml.config.merge({
  subProject: ['cml-subproject']
})

compiler.getRouterConfig() 返回值結構如下:

{
  projectRouter: {
    "mode": "history",
    "domain": "https://www.chameleon.com",
    "routes":[
      {
        "url": "/cml/h5/index",
        "path": "/pages/page1/page1",
        "name": "主項目",
        "mock": "index.php"
      }
    ]
  },
  subProjectRouter: {
    // 子項目npm包名稱為key,value為子項目的router.config.json
    "cml-subproject": {
      "mode": "history",
      "domain": "https://www.chameleon.com",
      "routes":[
        {
          "url": "/cml/h5/index",
          "path": "/pages/page1/page1",
          "name": "主項目",
          "mock": "index.php"
        }
      ]
    }
  }

}

利用這個方法可以獲取路由配置,用戶可以根據這些配置進行路由實現,同時 app 節(jié)點的dependencies字段中的節(jié)點都是頁面節(jié)點。

組件化相關信息

1 cml 文件的 extra 字段 (0.4.0 以上開始生效) CMLNode 中的 extra 字段用于存放節(jié)點的額外信息,cml 文件對應的節(jié)點,extra 中的 componentFiles 字段記錄 cml 文件引用的組件信息,結構如下:

{
  componentFiles: {
    demo-com: "/user/cml/demo-project/src/components/demo-com/demo-com.cml"
  }
}

key 為組件名稱,value 為組件的絕對路徑。

2 cml 文件節(jié)點的 dependencies 字段 cml 文件節(jié)點的 dependencies 字段記錄的就是這個 cml 文件引用的組件節(jié)點,其中如果 cml 文件是 app 節(jié)點 則dependencies中還包含頁面節(jié)點。通過componentFiles字段中的組件絕對路徑匹配dependencies中節(jié)點的realPath字段,就能找到組件名對應的節(jié)點。

3 cml 節(jié)點的 json 子節(jié)點 cml 節(jié)點的 children 字段存放 cml 文件的四個子節(jié)點,其中 moduleType 為 json 的節(jié)點 convert 字段為編譯后的 json 對象。

擴展新端 Demo 倉庫: https://github.com/chameleon-team/cml-extplatform-demo

CML 模板語言標準

CML 支持兩種語法,在模板 template 標簽中聲明 lang 屬性即可

聲明模板中用 CML 語法

<template lang="cml"> </template>

聲明模板中用類 vue 的語法

<template lang="vue"> </template>

如果不聲明的話默認就是 cml 語法

CML 語法協(xié)議

數據綁定

模板中的數據要使用 Mustache{{}}將變量包起來,可以作用于

標簽內容
<view>{{message}}</view>
class Index {
  data = {
    message: 'helloCML,
  };
}
組件屬性

簡單屬性

<view id="item-{{id}}"></view>
class Index {
  data = {
    id: 0,
  };
}

控制屬性

<view c-if="{{condition}}"></view>
運算:可以在 {{}}中進行簡單的運算,支持如下幾種形式

三元運算

<view class="{{true ? 'cls1':'cls2'}}"></view>

邏輯判斷

<view c-if="{{length > 5}}"> </view>

字符串運算

<view>{{'hello' + name}} </view>
class Index {
  data = {
    name: 'chameleon',
  };
}

數據路徑運算

<view>{{object.key}} {{array[0]}}</view>
class Index {
  data = {
    object: { key: 'Hello' },
    array: ['Chameleon'],
  };
}
組合

可以在 {{}}中直接寫數組;

<view c-for="{{[{name:'apple'},{name:'orange'}]}}">
  {{item.name}}
</view>

列表渲染

默認數組的當前項的下標變量名為 index,數組當前項的變量名為item

<view c-for="{{array}}">
{{index}}:{{item.message}}
</view>
class Index {
  data = {
    array: [
      {
        message: 'foo',
      },
      {
        message: 'bar',
      },
    ],
  };
}

使用 c-for-item可以用來指定數組當前元素的變量名

使用c-for-index可以指定數組當前下標的變量名

<view c-for="{{array}}" c-for-item="itemName" c-for-index="idx">
{{idx}}:{{itemName.message}}
</view>

如果列表中項目的位置會動態(tài)改變或者有新的項目添加到列表中,并且希望列表中的項目保持自己的特征和狀態(tài)

c-key 的值以兩種形式提供

字符串,代表在 for 循環(huán)的 array 中 item 的某個 property,該 property 的值需要是列表中唯一的字符串或數字,且不能動態(tài)改變。 保留關鍵字 *this 代表在 for 循環(huán)中的 item 本身,這種表示需要 item 本身是一個唯一的字符串或者數字,如: 當數據改變觸發(fā)渲染層重新渲染的時候,會校正帶有 key 的組件,框架會確保他們被重新排序,而不是重新創(chuàng)建,以確保使組件保持自身的狀態(tài),并且提高列表渲染時的效率。

c-key等于 item 項中某個 property

<view c-for="{{array}}" c-key="id">
</view>
class Index {
  data = {
    array: [
      {
        id: 'foo',
      },
      {
        id: 'bar',
      },
    ],
  };
}

c-key等于關鍵字 *this

<view c-for="{{array}}" c-key="*this">
</view>
class Index {
  data = {
    array: [1, 2, 3],
  };
}

條件渲染

<view c-if="{{condition}}"></view>

結合c-else-if c-else

<view c-if="{{condition}}"></view>
<view c-else-if="{{condition}}"></view>
<view c-else></view>

事件綁定

c-bind表示可以冒泡的事件

<view c-bind:click="handleClick"></view>
class Index {
  methods = {
    handleClick(e) {
      console.log(e); //默認傳遞一個事件對象參數
    },
  };
}

c-catch表示阻止冒泡的事件

<view c-catch:click="handleClick"></view>

內聯(lián)事件

內聯(lián)事件可以直接傳遞參數,特殊的參數 $event代表事件對象參數

<view c-for="{{array}}">
  <view c-bind:click="handleClick1(1,'string',item,index)"></view>
  <view c-bind:click="handleClick2(1,'string',item,index,$event)"></view>
</view>
class Index{
  data = {
    array:[{name:'apple'},{name:'orange'}]
  }
  methods = {
    handleClick1(...args){
      console.log(...args)
    }
    handleClick1(...args){
      console.log(...args)
    }
  }
}

類 Vue 語法協(xié)議

數據綁定

標簽內容

模板中的標簽內容中的變量要使用 Mustache{{}}包起來

<view>{{message}}</view>
class Index {
  data = {
    message: 'helloCML,
  };
}

標簽中的內容支持簡單的運算

字符串運算

<view>{{'hello' + name}} </view>
class Index {
  data = {
    name: 'chameleon',
  };
}

數據路徑運算

<view>{{object.key}} {{array[0]}}</view>
class Index {
  data = {
    object: { key: 'Hello' },
    array: ['Chameleon'],
  };
}
組件屬性

簡單屬性

模板中組件屬性中的變量要通過 :id="value"或者 v-bind:id="value"這種形式去使用。

<view :id="'item-' + id"></view>
class Index {
  data = {
    id: 0,
  };
}

控制屬性

<view v-if="condition"></view>

列表渲染

v-for指令根據一組數組的選項列表進行渲染。v-for 指令需要使用 (item,index) in items 形式的特殊語法,items 是源數據數組并且 item是數組元素迭代的別名,index是數組元素的下標

<view v-for="(item, index) in array">
{{index}}:{{item.message}}
</view>
class Index {
  data = {
    array: [
      {
        message: 'foo',
      },
      {
        message: 'bar',
      },
    ],
  };
}

如果列表中項目的位置會動態(tài)改變或者有新的項目添加到列表中,并且希望列表中的項目保持自己的特征和狀態(tài)

:key 的值以兩種形式提供

<view v-for="(item, index) in array" :key="item.id">
</view>
class Index {
  data = {
    array: [
      {
        id: 'foo',
      },
      {
        id: 'bar',
      },
    ],
  };
}

:key等于 數組元素

<view v-for="(item, index) in array" :key="item">
</view>
class Index {
  data = {
    array: [1, 2, 3],
  };
}

條件渲染

<view v-if="condition"></view>

結合v-else-if v-else

<view v-if="length < 5"></view>
<view v-else-if="length > 5"></view>
<view v-else></view>
class Index {
  data = {
    length: 5,
  };
}

事件綁定

@eventName或者 v-on:eventName 表示可以冒泡的事件

<view @click="handleClick"></view>
class Index {
  methods = {
    handleClick(e) {
      console.log(e); //默認傳遞一個事件對象參數
    },
  };
}

@eventName.stop或者v-on:eventName.stop表示阻止冒泡的事件

<view @click.stop="handleClick"></view>

內聯(lián)事件

內聯(lián)事件可以直接傳遞參數,特殊的參數 $event代表事件對象參數

<view v-for="(item, index) in array">
  <view @click="handleClick1(1,'string',item,index)"></view>
  <view @click.stop="handleClick2(1,'string',item,index,$event)"></view>
</view>
class Index{
  data = {
    array:[{name:'apple'},{name:'orange'}]
  }
  methods = {
    handleClick1(...args){
      console.log(...args)
    }
    handleClick1(...args){
      console.log(...args)
    }
  }
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號