基于 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),所以跨端框架的產生是大趨勢下的必然產物。
chameleon 基于對跨端工作的積累, 規(guī)范了一套跨端標準,稱之為 MVVM+ 協(xié)議;開發(fā)者只需要按照標準擴展流程,即可快速擴展任意 MVVM 架構模式的終端。并讓已有項目無縫運行新端。所以如果你希望讓 CML 快速支持淘寶小程序、React Native?只需按標準實現即可擴展。
最終讓開發(fā)者只需要用 CML 開發(fā),就可以在任意端運行,再也不用學習新平臺框架啦。
滴滴、芒果 TV、阿里的同學合作,按照跨端協(xié)議流程,目前已完成字節(jié)跳動小程序的共建開發(fā)
快應用官方研發(fā)團隊目前也接入完成
跨端框架最核心的工作是統(tǒng)一,chameleon 定義了標準的跨端協(xié)議,通過編譯時+運行時的手段去實現各端的代碼和功能,其實現原理如下圖所示。
其中運行時和基礎庫部分利用多態(tài)協(xié)議實現各端的獨立性與框架的統(tǒng)一性。chameleon 目前支持的端都是采用這種方式,我們定義了擴展一個新端所需要實現的所有標準,用戶只需要按照這些標準實現即可完成一個新端的擴展。
我們再來看一張 CML 的設計圖,能夠實現標準化的擴展新端,得益于多態(tài)協(xié)議中對各層代碼進行了接口的定義,各端代碼按照接口定義進行實現,向用戶代碼提供統(tǒng)一調用,同時還提供”多態(tài)協(xié)議“讓用戶代碼保障可維護性的前提下,直接觸達各端原生能力的方式。
簡單來說只需要實現 6 個 npm 包。
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>
組件分為內置組件 chameleon-ui-builtin 和擴展組件 cml-ui。所以用戶需要創(chuàng)建兩個 npm 包分別實現這兩個組件庫,結構參考cml-demo-ui-builtin和cml-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>
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()
})
......
}
}
運行時主要是對 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 中的實現方式,一般需要如下方面的適配工作。
從宏觀來看,運行時處理可分為:
從微觀來看,有以下處理:
例如: 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。 實現了微信端的基本擴展。
采用 lerna 對開發(fā)的 npm 包進行管理,解決本地開發(fā)時多個 npm 包之間相互依賴的問題。lerna init 即可創(chuàng)建一個 lerna 項目,建議直接用示例倉庫進行修改。
在packages目錄創(chuàng)建上述的 6 個 npm 包。
在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 包名稱。
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 包。
參考cml-demo-plugin/index.js文件 實現編譯類。對項目中的每一個文件或者部分進行編譯處理。處理節(jié)點的source字段,編譯后結果放入output字段。
監(jiān)聽compile-template事件 參數列表:(currentNode,parentNodeType)
說明: 這個鉤子用于處理.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');
例如模板編譯方法如下:
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 中的編譯實現。 比如微信端包括如下幾方面的實現:
監(jiān)聽compile-style事件 參數列表:(currentNode,parentNodeType)
說明: 這個鉤子用于處理所有的 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;
}
參數列表:(currentNode,parentNodeType)
說明: 這個鉤子用于處理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';
}
})
靜態(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輸出文件。
用戶需要創(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);
});
}
運行時主要是對 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 中的實現方式,一般需要如下方面的適配工作。
從宏觀來看,運行時處理可分為:
從微觀來看,有以下處理:
例如: 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>
組件分為內置組件 chameleon-ui-builtin 和擴展組件 cml-ui。所以用戶需要創(chuàng)建兩個 npm 包分別實現這兩個組件庫,結構參考cml-demo-ui-builtin和cml-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>
邏輯層負責反饋用戶對界面操作的處理中心。
而 邏輯對象 是邏輯層規(guī)范的輸入口,是運行時方法(ceateApp、createComponent、createPage)的輸入,包括
字段名 | 類型 | 說明 |
---|---|---|
props | Object | 聲明當前組件可接收數據屬性 props = { type, default } type為數據類型,default為數據默認值 |
data | Object | CML模板可直接使用的響應數據,是連接視圖層的樞紐 |
methods | Object | 處理業(yè)務邏輯與交互邏輯的方法 |
watch | Object | 偵聽屬性,監(jiān)聽數據的變化,觸發(fā)相應操作 |
computed | Object | CML模板可直接使用的計算屬性數據,也是連接視圖層的樞紐 |
beforeCreate | Function | 例初始化之后,數據和方法掛在到實例之前 一個頁面只會返回一次 |
created | Function | 數據及方法掛載完成 |
beforeMount | Function | 開始掛載已經編譯完成的cml到對應的節(jié)點時 |
mounted | Function | cml模板編譯完成,且渲染到dom中完成 |
beforeDestroy | Function | 實例銷毀之前 |
destroyed | Function | 實例銷毀后 |
理解了每個輸入字段代表的含義后,可以擴展(ceateApp、createComponent、createPage)處理邏輯對象,將邏輯對象轉換成當前平臺可接收的格式。
映射,并且在各生命周期中,注入新端到 cml 框架的運行時能力
每個 CML 實例(App、Page、Component)在被創(chuàng)建時都要經過一系列的初始化過程 ————
例如,需要設置數據監(jiān)聽、編譯模板、將實例掛載到 CML 節(jié)點并在數據變化時更新 CML 節(jié)點等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,這給開發(fā)者在不同階段添加自己的代碼的機會。
CML 為 App、Page、Component 提供了一系列生命周期事件,保障應用有序執(zhí)行。
另外,你還需要實現生命周期多態(tài)。
應用程序運行過程中,提供給上下文this可調用的方法
當做數據修改的時候,只需要在邏輯層修改數據,視圖層就會做相應的更新。
注入的核心是:賦予生命周期 hook mixins擴展能力,并且在特定 hook 中,賦予this響應數據變化的能力
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ī)則如下:
組件在 CML 模板中使用,組件名為 usingComponents 中的 key 值,組件使用形式為閉合標簽,標簽名為組件名。例如:
<template>
<c-cell><c-cell>
</template>
<script cml-type="json">
{
"base":{
"usingComponents": {
"c-cell": "../components/c-cell/c-cell"
}
}
}
</script>
Chameleon 支持一些基礎的原生事件和自定義事件,保障各端效果一致運行。
當用戶點擊該組件的時候會在該組件邏輯 VM 對象的 methods 中尋找相應的處理函數,該處理函數會收到一個事件對象。
類型 | 觸發(fā)條件 |
---|---|
tap | 手指觸摸后馬上離開 |
touchstart | 手指觸摸動作開始 |
touchmove | 手指觸摸后移動 |
touchend | 手指觸摸動作結束 |
它有以下屬性:
名稱 | 類型 | 說明 |
---|---|---|
type | String | 事件類型 |
timeStamp | Number | 頁面打開到觸發(fā)事件所經過的毫秒數 |
target | Object | 觸發(fā)事件的目標元素 且 target = { id, dataset } |
currentTarget | Object | 綁定事件的目標元素 且 currentTarget = { id, dataset } |
touches | Array | 觸摸事件中的屬性,當前停留在屏幕中的觸摸點信息的數組 且 touches = [{ identifier, pageX, pageY, clientX, clientY }] |
changedTouches | Array | 觸摸事件中的屬性,當前變化的觸摸點信息的數組 且 changedTouches = [{ identifier, pageX, pageY, clientX, clientY }] |
detail | Object | 自定義事件所攜帶的數據。 通過`$cmlEmit`方法觸發(fā)自定義事件,可以傳遞自定義數據即detail。具體下面`自定義事件`。 |
_originEvent | Object | CML 對各平臺的事件對象進行統(tǒng)一,會把原始的事件對象放到_originEvent屬性中,當需要特殊處理的可以進行訪問。 |
target && currentTarget 事件屬性
屬性 | 類型 | 說明 |
---|---|---|
id | String | 事件源組件的id |
dataset | Object | 事件源組件上由`data-`開頭的自定義屬性組成的集合 |
touches && changedTouches 事件屬性
數組中的對象具有如下屬性:
屬性 | 類型 | 說明 |
---|---|---|
identifier | Number | 觸摸點的標識符 |
pageX, pageY | Number | 距離文檔左上角的距離,文檔的左上角為原點 ,橫向為X軸,縱向為Y軸 |
clientX, clientY | Number | 距離頁面可顯示區(qū)域(屏幕除去導航條)左上角距離,橫向為X軸,縱向為Y軸 |
自定義事件用于父子組件之間的通信,父組件給子組件綁定自定義事件,子組件內部觸發(fā)該事件。綁定事件的方法是以bind+事件名稱="事件處理函數的形式給組件添加屬性,規(guī)定事件名稱不能存在大寫字母觸發(fā)事件的方法是調用this.$cmlEmit(事件名稱,detail對象)。
創(chuàng)建返回數據 store 實例
Store.dispatch(type: string, payload?: any)
Store.mapActions(map:Array\<string\>|Object\<string\>): Object
Store.registerModule(path: String, module: Module)
多態(tài)擴展
(chameleon-tool@0.4.0 以上開始支持)
多態(tài)接口一節(jié)講解了多態(tài)接口的使用,這一節(jié)講解多態(tài)接口的一些擴展語法,更適用于擴展新端使用。
當前 interface 文件用<include>標簽可以引入其他.interface文件,代表繼承該文件的接口定義和實現。然后在當前文件中去實現引入的接口定義,可以覆寫引入的某一端實現,也可以去擴展新端的實現。 語法是<include src="${引用路徑}.interface"></include>,有如下規(guī)則:
// 引入官方標準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></script>標簽可以通過指定src的方式引用外部 js 文件為某一平臺或某幾個的實現或者接口定義。 有如下規(guī)則:
例如:
<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();
沒有擴展語法之前,每個端的實現都在同一個文件中,沒有優(yōu)先級的問題,但是有了擴展語法之后,就會有優(yōu)先級問題。查找優(yōu)先級如下:
例如有如下兩個 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 方法時 各端編譯獲取方法如下:
例 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中,是同一文件。所以不認為是錯誤的。
(chameleon-tool@0.4.0 以上開始支持)
多態(tài)組件一節(jié)講解了多態(tài)組件的使用,這一節(jié)講解多態(tài)組件的一些擴展語法,更適用于擴展新端使用。
多態(tài)組件的.interface文件內部可以用<include>標簽引入其他多態(tài)組件的.interface文件,代表采用該文件的接口定義和對應的各端實現。 語法是<include src="${引用路徑}.interface"></include>,有如下規(guī)則:
編寫 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></script>標簽可以通過指定src的方式引用 cml 文件作為某一平臺或某幾個平臺的實現或者接口定義。 有如下規(guī)則:
例如上面擴展 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>
沒有擴展語法之前,采用的是各端的實現和.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:
// 引入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ā)。
擴展新端 首先要了解擴展新端總體的編譯流程,理解用戶擴展新端的工作處于編譯的什么階段。
在總體編譯流程中,webpack 編譯完成后,會將 webpack 編譯的結果轉成標準的 mvvm 編譯圖 projectGraph,這個圖由 CMLNode 節(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。
| ||||||||||||
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é)點的代碼 | ||||||||||||
convert | source的轉換格式,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。
類型:Number 日志的等級, 可取值 0,1,2,3,默認值為 2,值越大顯示日志越詳細。
類型:Array 用于設置原生組件的文件后綴,適用于多態(tài)組件的查找。 例如 usingComponents 中對于組件的引用是沒有后綴的,用戶可以對其進行擴展,再進行組件查找時會嘗試用戶設置的后綴,一般用于多態(tài)組件調用底層原生組件。 例如微信小程序中:
this.originComponentExtList = ['.wxml']; // 用于擴展原生組件的文件后綴查找
類型:String 用于設置當前端運行時 npm 包名稱。
類型:String 用于設置當前端內置組件 npm 包名稱。
類型:Object
{
js: Array,
css: Array
}
內置了兩種代碼壓縮,一種是 js 一直是 css,用戶指定輸出文件后綴對應的壓縮類型。例如微信小程序中:
this.minimizeExt = {
js: ['.js'],
css: ['.css','.wxss']
}
類型:Object chameleon 內置了針對小程序類的原生組件的處理方法,只需要用戶進行文件后綴的配置。例如微信小程序:
this.miniappExt = { // 小程序原生組件處理
rule: /\.wxml$/,
mapping: {
'template': '.wxml',
'style': '.wxss',
'script': '.js',
'json': '.json'
}
}
rule 的正則匹配文件后綴和this.originComponentExtList中的設置的文件后綴保持一致。 mapping 中的四個部分配置小程序對應的文件后綴。
類型:Array 當用戶有其他文件類型的原生組件要處理,可以通過配置 weback 的 module.rules 字段,用于擴展目標端特殊文件類型的處理,用戶可以擴展 webpack 編譯過程的 loader。例如:
this.webpackRules = [{
test: /\.vue$/,
use: [{
loader:'vue-loader',
options: {}
}]
}]
在 loader 中可以設置 this._module 中的一些字段控制生成的CMLNode的內容。
類型: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 方法中接受compiler對象,該對象是編譯的核心對象,用戶通過該對象注冊編譯流程。
使用該方法可以注冊編譯流程,第一個參數是鉤子名稱,第二個參數是處理函數,處理函數中會接收編譯流程對應的參數,下面說明每一個鉤子的作用和參數。
compiler.hook(鉤子名稱, function(參數) {
})
compile-preCML
參數列表:(currentNode,nodeType)
說明:
這個鉤子是編譯 cml 文件節(jié)點之前觸發(fā),并且傳遞 cml 文件節(jié)點,可以通過該鉤子去處理 cml 文件節(jié)點的template、json、style、script四個子節(jié)點之前需要的聯(lián)系。
compile-postCML
參數列表:(currentNode,nodeType)
說明:
這個鉤子是編譯完 cml 文件節(jié)點的依賴和子節(jié)點后觸發(fā),傳遞 cml 文件節(jié)點,可以通過該鉤子去處理 cml 文件節(jié)點編譯之后的處理。
compile-script
參數列表:(currentNode,parentNodeType)
說明: 這個鉤子用于處理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)
說明: 這個鉤子用于處理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)
說明: 這個鉤子用于處理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)
說明: 這個鉤子用于處理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)
說明: 這個鉤子用于處理nodeType='module',moduleType='other'的節(jié)點。對于不是 cml 識別的模塊類型進行編譯。
compile-asset
參數列表:(currentNode,parentNodeType)
說明: 這個鉤子用于處理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)
說明: 所有編譯結束之后觸發(fā)這個鉤子,在這個鉤子中編譯圖,拼接目標端的文件內容,調用compiler.writeFile()方法寫入要生成的文件路徑及內容。
compiler.hook('pack', function(projectGraph) {
// 遍歷編譯圖的節(jié)點,進行各項目的拼接
//調用writeFile方法寫入生成文件
compiler.writeFile('/app.json',projectGraph.output)
})
compiler.amd 對象提供了 js 語言的 AMD 模塊化方案供開發(fā)者使用,
ompiler.amd.amdWrapModule
參數列表 ({content, modId})
說明: 將 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)
說明: 該方法返回 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
}
})();
參數列表 (filePath, content)
說明: 在pack鉤子中通知用戶所有節(jié)點已經編譯完成,用戶可以遍歷projectGraph編譯圖,進行目標文件的拼接,調用compiler.writeFile方法進行寫出,pack鉤子執(zhí)行完畢后,內部編譯將輸出文件。例如:
let appJson = {
window: {
"navigationBarTitleText": "Chameleon"
}
}
compiler.writeFile('/app.json', JSON.stringify(appJson))
參數列表: 無參數 返回值結構:
{
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 支持兩種語法,在模板 template 標簽中聲明 lang 屬性即可
聲明模板中用 CML 語法
<template lang="cml"> </template>
聲明模板中用類 vue 的語法
<template lang="vue"> </template>
如果不聲明的話默認就是 cml 語法
模板中的數據要使用 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)
}
}
}
模板中的標簽內容中的變量要使用 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)
}
}
}
更多建議: