# 微信小程序組件化開發框架WePY官方文檔
## 快速入門指南
### WePY項目的創建與使用
WePY的安裝或更新都通過`npm`進行。
**全局安裝或更新WePY命令行工具**
```bash
npm install wepy-cli -g
```
**在開發目錄中生成Demo開發項目**
```bash
wepy new myproject
```
**切換至項目目錄**
```bash
cd myproject
```
**開啟實時編譯**
```bash
wepy build --watch
```
### WePY項目的目錄結構
```
├── dist ? ? ? ? ? ? ? ? ? 微信開發者工具指定的目錄(該目錄由WePY的build指令自動編譯生成,請不要直接修改該目錄下的文件)
├── node_modules ? ? ? ? ?
├── src ? ? ? ? ? ? ? ? ? 代碼編寫的目錄(該目錄為使用WePY后的開發目錄)
| ? ├── components ? ? ? ? WePY組件目錄(組件不屬于完整頁面,僅供完整頁面或其他組件引用)
| ? | ? ├── com_a.wpy ? ? 可復用的WePY組件a
| ? | ? └── com_b.wpy ? ? 可復用的WePY組件b
| ? ├── pages ? ? ? ? ? ? WePY頁面目錄(屬于完整頁面)
| ? | ? ├── index.wpy ? ? index頁面(經build后,會在dist目錄下的pages目錄生成index.js、index.json、index.wxml和index.wxss文件)
| ? | ? └── other.wpy ? ? ?other頁面(經build后,會在dist目錄下的pages目錄生成other.js、other.json、other.wxml和other.wxss文件)
| ? └── app.wpy ? ? ? ? ? 小程序配置項(全局數據、樣式、聲明鉤子等;經build后,會在dist目錄下生成app.js、app.json和app.wxss文件)
└── package.json ? ? ? ? ? 項目的package配置
```
### 參考建議
1. WePY借鑒了Vue.js(后文簡稱Vue)的語法風格和功能特性,如果你之前從未接觸過Vue,建議先閱讀Vue的[官方文檔](https://cn.vuejs.org/v2/guide/),以熟悉相關概念,否則在閱讀WePY文檔以及使用WePY進行開發的過程中,將會遇到比較多的障礙。
2. 開發建議使用第三方成熟IDE或編輯器(具體請參看后文的`代碼高亮`部分),`微信開發者工具`僅用于實時預覽和調試。
### 重要提醒
1. 使用`微信開發者工具`-->`添加項目`,`項目目錄`請選擇`dist`目錄。
2. `微信開發者工具`-->`項目`-->`關閉ES6轉ES5`。 <font style="color:red">重要:漏掉此項會運行報錯。</font>
3. `微信開發者工具`-->`項目`-->`關閉上傳代碼時樣式自動補全`。 <font style="color:red">重要:某些情況下漏掉此項也會運行報錯。</font>
4. `微信開發者工具`-->`項目`-->`關閉代碼壓縮上傳`。 <font style="color:red">重要:開啟后,會導致真機computed, props.sync 等等屬性失效。</font>(注:壓縮功能可使用WePY提供的build指令代替,詳見后文相關介紹以及Demo項目根目錄中的`wepy.config.js`和`package.json`文件。)
5. 本地項目根目錄運行`wepy build --watch`,開啟實時編譯。(注:如果同時在`微信開發者工具`-->`設置`-->`編輯器`中勾選了`文件保存時自動編譯小程序`,將可以實時預覽,非常方便。)
### 代碼高亮
文件后綴為`.wpy`,可共用`Vue`的高亮規則,但需要手動設置。下面提供一些常見IDE或編輯器中實現代碼高亮的相關設置步驟以供參考(也可通過更改文件后綴名的方式來實現高亮,詳見后文相關介紹)。
- **Sublime**
  1. 打開`Sublime->Preferences->Browse Packages..`進入用戶包文件夾。
  2. 在此文件夾下打開cmd,運行`git clone git@github.com:vuejs/vue-syntax-highlight.git`,無GIT用戶可以直接下載[zip包](https://github.com/vuejs/vue-syntax-highlight/archive/master.zip)解壓至當前文件夾。
  3. 關閉`.wpy`文件重新打開即可高亮。
- **WebStorm/PhpStorm**
  1. 打開`Settings`,搜索`Plugins`,搜索`Vue.js`插件并安裝。
  2. 打開`Settings`,搜索`File Types`,找到`Vue.js Template`,在`Registered Patterns`添加`*.wpy`,即可高亮。
- **Atom**
  1. 在Atom里先安裝Vue的語法高亮 - `language-vue`,如果裝過了就忽略這一步。
  2. 打開`Atom -> Config`菜單。在`core`鍵下添加:
```javascript
customFileTypes:
"text.html.vue": [
"wpy"
]
```
- **VS Code**
  1. 在 Code 里先安裝 Vue 的語法高亮插件 `Vetur`。
  2. 打開任意 `.wpy` 文件。
  3. 點擊右下角的選擇語言模式,默認為`純文本`。
  4. 在彈出的窗口中選擇 `.wpy 的配置文件關聯...`。
  5. 在`選擇要與 .wpy 關聯的語言模式` 中選擇 `Vue`。
- **VIM**
  1. 安裝 `Vue` 的 VIM 高亮插件,例如 [posva/vim-vue](https://github.com/posva/vim-vue)。
  2. 配置 `.wpy` 后綴名的文件使用 `Vue` 語法高亮。
```vim
au BufRead,BufNewFile *.wpy setlocal filetype=vue.html.javascript.css
```
### 代碼規范
1. 變量與方法盡量使用駝峰式命名,并且注意避免使用`$`開頭。
? 以`$`開頭的標識符為WePY框架的內建屬性和方法,可在JavaScript腳本中以`this.`的方式直接使用,具體請[參考API文檔](#api)。
2. 小程序入口、頁面、組件文件名的后綴為`.wpy`;外鏈的文件可以是其它后綴。
? 具體請參考[wpy文件說明](#wpy文件說明)。
?
3. 使用ES6語法開發。
框架在ES6(ECMAScript 6)下開發,因此也需要使用ES6開發小程序,ES6中有大量的語法糖可以讓我們的代碼更加簡潔高效。
4. 使用Promise。
? 框架默認對小程序提供的API全都進行了 Promise 處理,甚至可以直接使用`async/await`等新特性進行開發(注意:WePY 1.4.1以后的版本默認不支持async/await語法,因為可能導致iOS 10.0.1崩潰,如果不在意該問題可手動開啟,具體可參看[這里](https://github.com/wepyjs/wepy/wiki/wepy%E9%A1%B9%E7%9B%AE%E4%B8%AD%E4%BD%BF%E7%94%A8async-await))。
5. 事件綁定語法使用優化語法代替。
原`bindtap="click"`替換為`@tap="click"`,原`catchtap="click"`替換為`@tap.stop="click"`。更多`@`符用法,參見[組件自定義事件](https://github.com/wepyjs/wepy#組件自定義事件)。
6. 事件傳參使用優化后語法代替。
原`bindtap="click" data-index={{index}}`替換為`@tap="click({{index}})"`。
7. 自定義組件命名應避開微信原生組件名稱以及功能標簽`<repeat>`。
? 不可以使用`input、button、view、repeat`等微信小程序原生組件名稱命名自定義組件;另外也不要使用WePY框架定義的輔助標簽`repeat`命名。有關`repeat`的詳細信息,請參見[循環列表組件引用](https://github.com/wepyjs/wepy#循環列表組件引用)。
## 主要功能特性
### 開發模式轉換
WePY框架在開發過程中參考了Vue等現有框架的一些語法風格和功能特性,對原生小程序的開發模式進行了再次封裝,更貼近于MVVM架構模式。以下是使用WePY前后的代碼對比。
原生代碼:
```javascript
//index.js
//獲取應用實例
var app = getApp()
//通過Page構造函數創建頁面邏輯
Page({
? //可用于頁面模板綁定的數據
? ?data: {
motto: 'Hello World',
userInfo: {}
},
? ?//事件處理函數
bindViewTap: function() {
console.log('button clicked')
},
? ?//頁面的生命周期函數
? ?onLoad: function () {
console.log('onLoad')
}
})
```
基于WePY的代碼:
```javascript
//index.wpy中的<script>部分
import wepy from 'wepy';
//通過繼承自wepy.page的類創建頁面邏輯
export default class Index extends wepy.page {
? //可用于頁面模板綁定的數據
data = {
motto: 'Hello World',
userInfo: {}
};
? ?//事件處理函數(集中保存在methods對象中)
methods = {
bindViewTap () {
console.log('button clicked');
}
};
? ?//頁面的生命周期函數
onLoad() {
console.log('onLoad');
};
}
```
### 支持組件化開發
參見章節:[組件](#組件)
示例代碼:
```html
// index.wpy
<template>
<view>
<panel>
<h1 slot="title"></h1>
</panel>
<counter1 :num="myNum"></counter1>
<counter2 :num.sync="syncNum"></counter2>
<list :item="items"></list>
</view>
</template>
<script>
import wepy from 'wepy';
//引入List、Panel和Counter組件
import List from '../components/list';
import Panel from '../components/panel';
import Counter from '../components/counter';
export default class Index extends wepy.page {
? //頁面配置
? config = {
"navigationBarTitleText": "test"
};
? //聲明頁面中將要使用到的組件
? components = {
panel: Panel,
counter1: Counter,
counter2: Counter,
list: List
};
? //可用于頁面模板中綁定的數據
? data = {
myNum: 50,
syncNum: 100,
items: [1, 2, 3, 4]
}
}
</script>
```
### 支持加載外部NPM包
在編譯過程當中,會遞歸遍歷代碼中的`require`然后將對應依賴文件從node_modules當中拷貝出來,并且修改`require`為相對路徑,從而實現對外部NPM包的支持。如下圖:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554645/482b0f64-b198-11e6-8d4e-70c92326004f.png">
</p>
### 單文件模式,目錄結構更清晰,開發更方便
原生小程序要求app實例必須有3個文件:`app.js`、`app.json`、`app.wxss`,而page頁面則一般有4個文件:`page.js`、`page.json`、`page.wxml`、`page.wxss`,并且還要求app實例的3個文件以及page頁面的4個文件除后綴名外必須同名,具體可參看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/framework/structure.html?t=20161107" target="_blank">官方目錄結構</a>。
而在WePY中則使用了單文件模式,將原生小程序app實例的3個文件統一為`app.wpy`,page頁面的4個文件統一為`page.wpy`。使用WePY開發前后的開發目錄結構對比如下:
原生小程序的目錄結構:
```
project
├── pages
| ├── index
| | ├── index.js index 頁面邏輯
| | ├── index.json index 頁面配置
| | ├── index.wxml index 頁面結構
| | └── index.wxss index 頁面樣式
| └── log
| ├── log.js log 頁面邏輯
| ├── log.json log 頁面配置
| ├── log.wxml log 頁面結構
| └── log.wxss log 頁面樣式
├── app.js 小程序邏輯
├── app.json ? ? ? ? ? 小程序公共配置
└── app.wxss ? ? ? ? ? 小程序公共樣式
```
使用WePY框架后的開發目錄結構(主要為src目錄的結構,dist目錄除外):
```
注:dist目錄為WePY通過build指令生成的目錄,除額外增加的npm目錄外,其目錄結構與原生小程序的目錄結構類似。
```
```
project
└── src
├── pages
| ├── index.wpy index 頁面邏輯、配置、結構、樣式
| └── log.wpy log 頁面邏輯、配置、結構、樣式
? ?└──app.wpy ? ? ? ? ? 小程序邏輯、公共配置、公共樣式
```
### 默認使用babel編譯,支持ES6/7的一些新特性
用戶可以通過修改`wepy.config.js`(老版本使用`.wepyrc`)配置文件,配置自己熟悉的babel環境進行開發。默認開啟使用了一些新的特性如`promise`、`async/await`(自WePY 1.4.1開始必須手動開啟,原因參見前文`代碼規范`一節中的介紹)等等。
示例代碼:
```javascript
import wepy from 'wepy';
export default class Index extends wepy.page {
getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({data: 123});
}, 3000);
});
};
async onLoad() {
let data = await this.getData();
console.log(data.data);
};
}
```
### 針對原生API進行優化
對小程序原生API進行promise處理,同時修復了一些原生API的缺陷,比如:wx.request的并發問題等。
原生代碼:
```javascript
onLoad = function () {
var self = this;
wx.login({
success: function (data) {
wx.getUserInfo({
success: function (userinfo) {
self.setData({userInfo: userinfo});
}
});
}
});
}
```
基于WePY的代碼:
```javascript
import wepy from 'wepy';
async onLoad() {
await wepy.login();
this.userInfo = await wepy.getUserInfo();
}
```
在同時并發10個request請求測試時:
不使用WePY:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554651/5185f740-b198-11e6-88f8-45e359090dc3.png" alt="2 small">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554886/c30e802a-b199-11e6-927d-08cd4e5ed0b0.png" alt="2 small">
</p>
使用WePY后:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554663/65704c2e-b198-11e6-8277-abb77e0c7b3e.png">
</p>
## 進階介紹
### wepy.config.js配置文件說明
執行`wepy new demo`后,會生成類似下面這樣的配置文件。
```javascript
let prod = process.env.NODE_ENV === 'production';
module.exports = {
'output': 'dist',
'source': 'src',
'wpyExt': '.wpy',
'compilers': {
less: {
'compress': true
},
/*sass: {
'outputStyle': 'compressed'
},*/
babel: {
'presets': [
'es2015',
'stage-1'
],
'plugins': [
'transform-export-extensions',
'syntax-export-extensions',
'transform-runtime'
]
}
},
'plugins': {
}
};
if (prod) {
// 壓縮sass
module.exports.compilers['sass'] = {'outputStyle': 'compressed'};
// 壓縮less
module.exports.compilers['less'] = {'compress': true};
// 壓縮js
module.exports.plugins = {
'uglifyjs': {
filter: /\.js$/,
config: {
}
},
'imagemin': {
filter: /\.(jpg|png|jpeg)$/,
config: {
'jpg': {
quality: 80
},
'png': {
quality: 80
}
}
}
};
}
```
**wpyExt:** 缺省值為'.wpy',IDE默認情況下不會對此文件類型進行高亮處理,這種情況下,除了按照前文`代碼高亮`部分的介紹進行設置之外,還可以直接將相關文件的后綴名由`.wpy`修改為`.vue`(因為與Vue的高亮規則一樣),然后將此選項修改為`.vue`,也能解決部分IDE中代碼高亮的問題。
**compilers:** compilers為`1.3.1`版本之后的功能,如果需要使用其它語法,請先配置`compilers`,然后再安裝相應的compilers。目前支持`wepy-compiler-less`,`wepy-compiler-sass`、`wepy-compiler-babel`、`wepy-compiler-pug`,其他compiler持續開發中......
對應各compiler請參考各自文檔:
> **sass:** sass編譯配置,參見<a href="https://github.com/sass/node-sass" target="_blank">這里</a>。
> **less:** less編譯配置,參見<a href="http://lesscss.org/#using-less-usage-in-code" target="_blank">這里</a>。
> **stylus:** stylus編譯配置,參見<a href="http://www.zhangxinxu.com/jq/stylus/js.php" target="_blank">這里</a>。
> **babel:** babel編譯配置,參見<a href="http://babeljs.io/docs/usage/options/" target="_blank">這里</a>。
> **typescript:** typescript編譯配置,參見<a href="https://www.tslang.cn/docs/home.html" target="_blank">這里</a>。
**plugins:** plugins為`1.1.6`版本之后的功能,目前支持js壓縮`wepy-plugin-ugliyjs`、圖片壓縮`wepy-plugin-imagemin`,其他plugin持續開發中......
### 關于compilers和plugins
1.3.1版本新功能,文檔建設中...
### .wpy文件說明
`.wpy`文件的編譯過程過下:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/22774706/422375b0-eee3-11e6-9046-04d9cd3aa429.png" alt="5 small">
</p>
一個`.wpy`文件可分為三大部分,各自對應于一個標簽:
1. 腳本部分,即`<script></script>`標簽中的內容,又可分為兩個部分:
    邏輯部分,除了config對象之外的部分,對應于原生的`.js`文件;
    配置部分,即config對象,對應于原生的`.json`文件。
2. 結構部分,即`<template></template>`模板部分,對應于原生的`.wxml`文件。
3. 樣式部分,即`<style></style>`樣式部分,對應于原生的`.wxss`文件。
其中,小程序入口文件`app.wpy`不需要`template`,所以編譯時會被忽略。`.wpy`文件中的`script`、`template`、`style`這三個標簽都支持`lang`和`src`屬性,`lang`決定了其代碼編譯過程,`src`決定是否外聯代碼,存在`src`屬性且有效時,會忽略內聯代碼。
示例如下:
```Html
<style lang="less" src="page1.less"></style>
<template lang="wxml" src="page1.wxml"></template>
<script>
// some code
</script>
```
各標簽對應的`lang`值如下表所示:
| 標簽 | lang默認值 | lang支持值 |
| -------- | ------- | ---------------------------- |
| style ? | `css` ? | `css`、`less`、`scss`、`stylus` |
| template | `wxml` | `wxml`、`xml`、`pug(原jade)` ? |
| script ? | `babel` | `babel`、`TypeScript` ? ? ? ? |
### 腳本部分介紹
#### 小程序入口app.wpy
```html
<script>
import wepy from 'wepy';
export default class extends wepy.app {
config = {
"pages":[
"pages/index/index"
],
"window":{
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
}
};
onLaunch() {
console.log(this);
}
}
</script>
<style lang="less">
/** less **/
</style>
```
入口文件`app.wpy`中所聲明的小程序實例繼承自`wepy.app`類,包含一個`config`屬性和其它全局屬性、方法、事件。其中`config`屬性對應原生的`app.json`文件,build編譯時會根據`config`屬性自動生成`app.json`文件,如果需要修改`config`中的內容,請使用微信提供的相關API。
#### 頁面page.wpy
```html
<script>
import wepy from 'wepy';
import Counter from '../components/counter';
export default class Page extends wepy.page {
config = {};
components = {counter1: Counter};
data = {};
methods = {};
events = {};
onLoad() {};
// Other properties
}
</script>
<template lang="wxml">
<view>
</view>
<counter1></counter1>
</template>
<style lang="less">
/** less **/
</style>
```
頁面文件`page.wpy`中所聲明的頁面實例繼承自`wepy.page`類,該類的主要屬性介紹如下:
| 屬性 | 說明 |
| ---------- | ---------------------------------------- |
| config ? ? | 頁面配置對象,對應于原生的`page.json`文件,類似于`app.wpy`中的config |
| components | 頁面組件列表對象,聲明頁面所引入的組件列表? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
| data ? ? ? | 頁面渲染數據對象,存放可用于頁面模板綁定的渲染數據 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
| methods ? | wxml事件處理函數對象,存放響應wxml中所捕獲到的事件的函數,如`bindtap`、`bindchange` ? ? ? |
| events ? ? | WePY組件事件處理函數對象,存放響應組件之間通過`$broadcast`、`$emit`、`$invoke`所傳遞的事件的函數 ? ? ? ? ? |
| 其它 ? ? ? ? | 小程序頁面生命周期函數,如`onLoad`、`onReady`等,以及其它自定義的方法與屬性 ? |
#### 組件com.wpy
```html
<template lang="wxml">
<view> </view>
</template>
<script>
import wepy from 'wepy';
export default class Com extends wepy.component {
components = {};
data = {};
methods = {};
events = {};
// Other properties
}
</script>
<style lang="less">
/** less **/
</style>
```
組件文件`com.wpy`中所聲明的組件實例繼承自`wepy.component`類,除了不需要`config`配置以及頁面特有的一些生命周期函數之外,其屬性與頁面屬性大致相同。
### 實例
通過前文的介紹可知,在 WePY 中,小程序被分為三個實例:小程序實例`App`、頁面實例`Page`、組件實例`Component`。其中`Page`實例繼承自`Component`。各自的聲明方式如下:
```javascript
import wepy from 'wepy';
// 聲明一個App小程序實例
export default class MyAPP extends wepy.app {
}
// 聲明一個Page頁面實例
export default class IndexPage extends wepy.page {
}
// 聲明一個Component組件實例
export default class MyComponent extends wepy.component {
}
```
#### App小程序實例
App小程序實例中主要包含小程序生命周期函數、config配置對象、globalData全局數據對象,以及其他自定義方法與屬性。
```javascript
import wepy from 'wepy';
export default class MyAPP extends wepy.app {
customData = {};
customFunction () { }
onLaunch () {}
onShow () {}
config = {} // 對應 app.json 文件
? ?globalData = {}
}
```
在Page頁面實例中,可以通過`this.$parent`來訪問App實例。
#### Page頁面實例和Component組件實例
由于Page頁面實際上繼承自Component組件,即Page也是組件。除擴展了頁面所特有的`config`配置以及特有的頁面生命周期函數之外,其它屬性和方法與Component一致,因此這里以Page頁面為例進行介紹。
```javascript
import wepy from 'wepy';
// export default class MyPage extends wepy.page {
export default class MyComponent extends wepy.component {
? ?customData = {} ?// 自定義數據
? ?customFunction () {} ?//自定義方法
onLoad () {} // 在Page和Component共用的生命周期函數
? ?onShow () {} // 只在Page中存在的頁面生命周期函數
?config = {}; // 只在Page實例中存在的配置數據,對應于原生的page.json文件
? ?data = {}; // 頁面所需數據均需在這里聲明,可用于模板數據綁定
? ?components = {}; // 聲明頁面中所引用的組件,或聲明組件中所引用的子組件
mixins = []; // 聲明頁面所引用的Mixin實例
? ?computed = {}; // 聲明計算屬性(詳見后文介紹)
? ?watch = {}; // 聲明數據watcher(詳見后文介紹)
? ?methods = {}; // 聲明頁面wxml中標簽的事件處理函數。注意,此處只用于聲明頁面wxml中標簽的bind、catch事件,自定義方法需以自定義方法的方式聲明
? ?events = {}; // 聲明組件之間的事件處理函數
}
```
注意,對于WePY中的methods屬性,因為與Vue中的使用習慣不一致,非常容易造成誤解,這里需要特別強調一下:WePY中的methods屬性只能聲明頁面wxml標簽的bind、catch事件,不能聲明自定義方法,這與Vue中的用法是不一致的。示例如下:
```javascript
// 錯誤示例
import wepy from 'wepy';
export default class MyComponent extends wepy.component {
methods = {
bindtap () {
let rst = this.commonFunc();
// doSomething
},
bindinput () {
let rst = this.commonFunc();
// doSomething
},
? ? ? ?//錯誤:普通自定義方法不能放在methods對象中
? ? ? ?customFunction () {
return 'sth.';
}
};
}
// 正確示例
import wepy from 'wepy';
export default class MyComponent extends wepy.component {
methods = {
bindtap () {
let rst = this.commonFunc();
// doSomething
},
bindinput () {
let rst = this.commonFunc();
// doSomething
},
}
? ?//正確:普通自定義方法在methods對象外聲明,與methods平級
? ?customFunction () {
return 'sth.';
}
}
```
### 組件
原生小程序支持js<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/framework/app-service/module.html?t=20161107" target="_blank">模塊化</a>,但彼此獨立,業務代碼與交互事件仍需在頁面處理。無法實現組件化的松耦合與復用的效果。
例如模板A中綁定一個`bindtap="myclick"`,模板B中同樣綁定一樣`bindtap="myclick"`,那么就會影響同一個頁面事件。對于數據同樣如此。因此,只有通過改變變量或者事件方法,或者給其加不同前綴才能實現綁定不同事件或者不同數據。當頁面復雜之后就十分不利于開發維護。
因此,在WePY中實現了小程序的組件化開發,組件的所有業務與功能在組件本身實現,組件與組件之間彼此隔離,上述例子在WePY的組件化開發過程中,A組件只會影響到A所綁定的`myclick`,B也如此。
WePY編譯組件的過程如下:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/22774767/8f090dd6-eee3-11e6-942b-1591a6379ad3.png">
</p>
#### 普通組件引用
當頁面需要引入組件或組件需要引入子組件時,必須在`.wpy`文件的`<script>`腳本部分先import組件文件,然后在`components`對象中給組件聲明唯一的組件ID,接著在`<template>`模板部分中添加以`components`對象中所聲明的組件ID進行命名的自定義標簽以插入組件。如:
```html
/**
project
└── src
├── components
| └── child.wpy
├── pages
| ├── index.wpy index 頁面配置、結構、樣式、邏輯
| └── log.wpy log 頁面配置、結構、樣式、邏輯
? └──app.wpy ? ? ? ? ? 小程序配置項(全局公共配置、公共樣式、聲明鉤子等)
**/
// index.wpy
<template>
<!-- 以`<script>`腳本部分中所聲明的組件ID為名命名自定義標簽,從而在`<template>`模板部分中插入組件 -->
? ?<child></child>
</template>
<script>
import wepy from 'wepy';
? ?//引入組件文件
? import Child from '../components/child';
export default class Index extends wepy.component {
? ? ? //聲明組件,分配組件id為child
? ? ? components = {
child: Child
};
}
</script>
```
需要注意的是,WePY中的組件都是靜態組件,是以組件ID作為唯一標識的,每一個ID都對應一個組件實例,當頁面引入兩個相同ID的組件時,這兩個組件共用同一個實例與數據,當其中一個組件數據變化時,另外一個也會一起變化。
如果需要避免這個問題,則需要分配多個組件ID和實例。代碼如下:
```html
<template>
<view class="child1">
<child></child>
</view>
<view class="child2">
<anotherchild></anotherchild>
</view>
</template>
<script>
import wepy from 'wepy';
import Child from '../components/child';
export default class Index extends wepy.component {
components = {
? ? ? ? ? //為兩個相同組件的不同實例分配不同的組件ID,從而避免數據同步變化的問題
? ? ? ? ? child: Child,
anotherchild: Child
};
}
</script>
```
*注意*:WePY中,在父組件`template`模板部分插入駝峰式命名的子組件標簽時,不能將駝峰式命名轉換成短橫桿式命名(比如將`childCom`轉換成`child-com`),這與Vue中的習慣是不一致。
#### 組件的循環渲染
*1.4.6新增*
當需要循環渲染WePY組件時(類似于通過`wx:for`循環渲染原生的wxml標簽),必須使用WePY定義的輔助標簽`<repeat>`,代碼如下:
```Html
/**
project
└── src
? ├── components
| └── child.wpy
├── pages
| ├── index.wpy index 頁面配置、結構、樣式、邏輯
| └── log.wpy log 頁面配置、結構、樣式、邏輯
└──app.wpy 小程序配置項(全局樣式配置、聲明鉤子等)
**/
// index.wpy
<template>
<!-- 注意,使用for屬性,而不是使用wx:for屬性 -->
? ?<repeat for="{{list}}" key="index" index="index" item="item">
<!-- 插入<script>腳本部分所聲明的child組件,同時傳入item -->
? ? ? ?<child :item="item"></child>
</repeat>
</template>
<script>
import wepy from 'wepy';
? // 引入child組件文件
? import Child from '../components/child';
export default class Index extends wepy.component {
components = {
? ? ? ? ? // 聲明頁面中要使用到的Child組件的ID為child
? ? ? ? ? child: Child
}
data = {
list: [{id: 1, title: 'title1'}, {id: 2, title: 'title2'}]
}
}
</script>
```
頁面可以引入組件,而組件還可以引入子組件。一個頁面引入若干組件后,組件結構如下圖:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554681/796da1ae-b198-11e6-91ab-e90f485c594d.png">
</p>
如上圖所示,Page_Index頁面引入了ComA、ComB、ComC三個組件,同時ComA組件和ComB組件又有自己的子組件ComD、ComE、ComF、ComG、ComH。
#### computed 計算屬性
* **類型**: `{ [key: string]: Function }`
* **詳細**:
`computed`計算屬性,是一個有返回值的函數,可直接被當作綁定數據來使用。因此類似于`data`屬性,代碼中可通過`this.計算屬性名`來引用,模板中也可通過`{{ 計算屬性名 }}`來綁定數據。
需要注意的是,只要是組件中有任何數據發生了改變,那么所有計算屬性就都會被重新計算。
* **示例**:
```javascript
data = {
a: 1
}
// 計算屬性aPlus,在腳本中可通過this.aPlus來引用,在模板中可通過{{ aPlus }}來插值
computed = {
aPlus () {
return this.a + 1
}
}
```
#### watcher 監聽器
* **類型**: `{ [key: string]: Function }`
* **詳細**:
通過監聽器`watcher`能夠監聽到任何屬性的更新。監聽器在`watch`對象中聲明,類型為函數,函數名與需要被監聽的`data`對象中的屬性同名,每當被監聽的屬性改變一次,監聽器函數就會被自動調用執行一次。
監聽器適用于當屬性改變時需要進行某些額外處理的情形。
* **示例**:
```javascript
data = {
num: 1
}
// 監聽器函數名必須跟需要被監聽的data對象中的屬性num同名,
// 其參數中的newValue為屬性改變后的新值,oldValue為改變前的舊值
watch = {
num (newValue, oldValue) {
console.log(`num value: ${oldValue} -> ${newValue}`)
}
}
// 每當被監聽的屬性num改變一次,對應的同名監聽器函數num()就被自動調用執行一次
onLoad () {
setInterval(() => {
this.num++;
this.$apply();
}, 1000)
}
```
#### props 傳值
props傳值在WePY中屬于父子組件之間傳值的一種機制,包括靜態傳值與動態傳值。
在props對象中聲明需要傳遞的值,靜態傳值與動態傳值的聲明略有不同,具體可參看下面的示例代碼。
**靜態傳值**
靜態傳值為父組件向子組件傳遞常量數據,因此只能傳遞String字符串類型。
在父組件`template`模板部分的組件標簽中,使用子組件props對象中所聲明的屬性名作為其屬性名來接收父組件傳遞的值。
```Javascript
<child title="mytitle"></child>
// child.wpy
props = {
title: String
};
onLoad () {
console.log(this.title); // mytitle
}
```
**動態傳值**
動態傳值是指父組件向子組件傳遞動態數據內容,父子組件數據完全獨立互不干擾。但可以通過使用`.sync`修飾符來達到父組件數據綁定至子組件的效果,也可以通過設置子組件props的`twoWay: true`來達到子組件數據綁定至父組件的效果。那如果既使用`.sync`修飾符,同時子組件`props`中添加的`twoWay: true`時,就可以實現數據的雙向綁定了。
*注意*:下文示例中的`twoWay`為`true`時,表示子組件向父組件單向動態傳值,而`twoWay`為`false`(默認值,可不寫)時,則表示子組件不向父組件傳值。這是與Vue不一致的地方,而這里之所以仍然使用`twoWay`,只是為了盡可能保持與Vue在標識符命名上的一致性。
在父組件`template`模板部分所插入的子組件標簽中,使用`:prop`屬性(等價于Vue中的`v-bind:prop`屬性)來進行動態傳值。
```Javascript
// parent.wpy
<child :title="parentTitle" :syncTitle.sync="parentTitle" :twoWayTitle="parentTitle"></child>
data = {
parentTitle: 'p-title'
};
// child.wpy
props = {
? ?// 靜態傳值
? ?title: String,
? ?// 父向子單向動態傳值
? ?syncTitle: {
type: String,
default: 'null'
},
? ?
? ?twoWayTitle: {
type: Number,
default: 'nothing',
twoWay: true
}
};
onLoad () {
console.log(this.title); // p-title
console.log(this.syncTitle); // p-title
console.log(this.twoWayTitle); // p-title
this.title = 'c-title';
console.log(this.$parent.parentTitle); // p-title.
this.twoWayTitle = 'two-way-title';
this.$apply();
? ?console.log(this.$parent.parentTitle); // two-way-title. --- twoWay為true時,子組件props中的屬性值改變時,會同時改變父組件對應的值
this.$parent.parentTitle = 'p-title-changed';
this.$parent.$apply();
? ?console.log(this.title); // 'c-title';
? ?console.log(this.syncTitle); // 'p-title-changed' --- 有.sync修飾符的props屬性值,當在父組件中改變時,會同時改變子組件對應的值。
}
```
#### 組件通信與交互
`wepy.component`基類提供`$broadcast`、`$emit`、`$invoke`三個方法用于組件之間的通信和交互,如:
```javascript
this.$emit('some-event', 1, 2, 3, 4);
```
用于監聽組件之間的通信與交互事件的事件處理函數需要寫在組件和頁面的`events`對象中,如:
```javascript
import wepy from 'wepy'
export default class Com extends wepy.component {
components = {};
data = {};
methods = {};
? ?// events對象中所聲明的函數為用于監聽組件之間的通信與交互事件的事件處理函數
events = {
'some-event': (p1, p2, p3, $event) => {
console.log(`${this.$name} receive ${$event.name} from ${$event.source.$name}`);
}
};
// Other properties
}
```
**$broadcast**
`$broadcast`事件是由父組件發起,所有子組件都會收到此廣播事件,除非事件被手動取消。事件廣播的順序為廣度優先搜索順序,如上圖,如果頁面`Page_Index`發起一個`$broadcast`事件,那么按先后順序依次接收到該事件的組件為:ComA、ComB、ComC、ComD、ComE、ComF、ComG、ComH。如下圖:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554688/800089e6-b198-11e6-84c5-352d2d0e2f7e.png">
</p>
**$emit**
`$emit`與`$broadcast`正好相反,事件發起組件的所有祖先組件會依次接收到`$emit`事件。如果組件ComE發起一個`$emit`事件,那么接收到事件的先后順序為:組件ComA、頁面Page_Index。如下圖:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554704/9997932c-b198-11e6-9840-3edae2194f47.png">
</p>
**$invoke**
`$invoke`是一個頁面或組件對另一個組件中的方法的直接調用,通過傳入組件路徑找到相應的組件,然后再調用其方法。
比如,想在頁面`Page_Index`中調用組件ComA的某個方法:
```Javascript
this.$invoke('ComA', 'someMethod', 'someArgs');
```
如果想在組件ComA中調用組件ComG的某個方法:
```Javascript
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs');
```
#### 組件自定義事件處理函數
*1.4.8新增*
可以通過使用`.user`修飾符為自定義組件綁定事件,如:`@customEvent.user="myFn"`
其中,`@`表示事件修飾符,`customEvent` 表示事件名稱,`.user`表示事件后綴。
目前總共有三種事件后綴:
- `.default`: 綁定小程序冒泡型事件,如`bindtap`,`.default`后綴可省略不寫;
- `.stop`: 綁定小程序捕獲型事,如`catchtap`;
- `.user`: 綁定用戶自定義組件事件,通過`$emit`觸發。**注意,如果用了自定義事件,則events中對應的監聽函數不會再執行。**
示例如下:
```Html
// index.wpy
<template>
<child @childFn.user="parentFn"></child>
</template>
<script>
import wepy from 'wepy'
import Child from '../components/child'
export default class Index extends wepy.page {
components = {
child: Child
}
methods = {
parentFn (num, evt) {
console.log('parent received emit event, number is: ' + num)
}
}
}
</script>
// child.wpy
<template>
<view @tap="tap">Click me</view>
</template>
<script>
import wepy from 'wepy'
export default class Child extends wepy.component {
methods = {
tap () {
console.log('child is clicked')
this.$emit('childFn', 100)
}
}
}
</script>
```
#### slot 組件內容分發插槽
WePY中的`slot`插槽作為內容分發標簽的空間占位標簽,便于在父組件中通過對相當于擴展板卡的內容分發標簽的“插拔”,更為靈活、方便地對子組件進行內容分發。
具體使用方法是,首先在子組件`template`模板部分中聲明`slot`標簽作為內容插槽,同時必須在其`name`屬性中指定插槽名稱,還可設置默認的標簽內容;然后在引入了該帶有插槽的子組件的父組件`template`模板部分中聲明用于“插拔”的內容分發標簽。
注意,這些父組件中的內容分發標簽必須具有`slot`屬性,并且其值為子組件中對應的插槽名稱,這樣父組件內容分發標簽中的內容會覆蓋掉子組件對應插槽中的默認內容。
另外,要特別注意的是,父組件中一旦聲明了對應于子組件插槽的內容分發標簽,即便沒有內容,子組件插槽中的默認內容也不會顯示出來,只有刪除了父組件中對應的內容分發標簽,才能顯示出來。
示例:
在`Panel`組件中有以下模板:
```html
<view class="panel">
<slot name="title">默認標題</slot>
<slot name="content">默認內容</slot>
</view>
```
在父組件中使用`Pannel`子組件時,可以這樣使用:
```html
<panel>
<view slot="title">新的標題</view>
<view slot="content">
? ? ? ?<text>新的內容</text>
</view>
</panel>
```
### 第三方組件
WePY允許使用基于WePY開發的第三方組件,開發第三方組件規范請參考<a href="https://github.com/wepyjs/wepy-com-toast" target="_blank">wepy-com-toast</a>。
### 混合
混合可以將組之間的可復用部分抽離,從而在組件中使用混合時,可以將混合的數據,事件以及方法注入到組件之中。混合分分為兩種:
* 默認式混合
* 兼容式混合
#### 默認式混合
對于組件`data`數據,`components`組件,`events`事件以及其它自定義方法采用**默認式混合**,即如果組件未聲明該數據,組件,事件,自定義方法等,那么將混合對象中的選項將注入組件這中。對于組件已聲明的選項將不受影響。
```Javascript
// mixins/test.js
import wepy from 'wepy';
export default class TestMixin extends wepy.mixin {
data = {
foo: 'foo defined by page',
bar: 'bar defined by testMix'
};
methods: {
tap () {
console.log('mix tap');
}
}
}
// pages/index.wpy
import wepy from 'wepy';
import TestMixin from './mixins/test';
export default class Index extends wepy.page {
data = {
foo: 'foo defined by index'
};
mixins = [TestMixin ];
onShow() {
console.log(this.foo); // foo defined by index
console.log(this.bar); // bar defined by testMix
}
}
```
#### 兼容式混合
對于組件`methods`響應事件,以及小程序頁面事件將采用**兼容式混合**,即先響應組件本身響應事件,然后再響應混合對象中響應事件。**注意,這里事件的執行順序跟Vue中相反,[Vue中是先執行mixin中的函數, 再執行組件本身的函數](https://vuejs.org/v2/guide/mixins.html#Option-Merging)。**
```Javascript
// mixins/test.js
import wepy from 'wepy';
export default class TestMixin extends wepy.mixin {
methods = {
tap () {
console.log('mix tap');
}
};
onShow() {
console.log('mix onshow');
}
}
// pages/index.wpy
import wepy from 'wepy';
import TestMixin from './mixins/test';
export default class Index extends wepy.page {
mixins = [TestMixin];
methods = {
tap () {
console.log('index tap');
}
};
onShow() {
console.log('index onshow');
}
}
// index onshow
// mix onshow
// ----- when tap
// index tap
// mix tap
```
### 攔截器
可以使用全域攔截器配置API的config、fail、success、complete方法,參考示例:
```javascript
import wepy from 'wepy';
export default class extends wepy.app {
constructor () {
this.intercept('request', {
config (p) {
p.timestamp = +new Date();
return p;
},
success (p) {
console.log('request success');
return p;
},
fail (p) {
console.log('request error');
return p;
}
});
}
}
```
### 數據綁定
#### 小程序數據綁定方式
小程序通過`Page`提供的`setData`方法去綁定數據,如:
```Javascript
this.setData({title: 'this is title'});
```
因為小程序架構本身原因,頁面渲染層和JS邏輯層分開的,setData操作實際就是JS邏輯層與頁面渲染層之間的通信,那么如果在同一次運行周期內多次執行`setData`操作時,那么通信的次數是一次還是多次呢?這個取決于API本身的設計。
#### WePY數據綁定方式
WePY使用臟數據檢查對setData進行封裝,在函數運行周期結束時執行臟數據檢查,一來可以不用關心頁面多次setData是否會有性能上的問題,二來可以更加簡潔去修改數據實現綁定,不用重復去寫setData方法。代碼如下:
```javascript
this.title = 'this is title';
```
但需注意,在函數運行周期之外的函數里去修改數據需要手動調用`$apply`方法。如:
```javascript
setTimeout(() => {
this.title = 'this is title';
this.$apply();
}, 3000);
```
#### WePY臟數據檢查流程
在執行臟數據檢查時,會通過`this.$$phase`標識當前檢查狀態,并且會保證在并發的流程當中,只會有一個臟數據檢查流程在運行,以下是執行臟數據檢查的流程圖:
<p align="center">
<img src="https://cloud.githubusercontent.com/assets/2182004/20554709/a0d8b1e8-b198-11e6-9034-0997b33bdf95.png">
</p>
### 其它優化細節
#### 1. wx.request 接收參數修改
點這里查看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-request.html?t=20161122" target="_blank">官方文檔</a>
```javascript
// 官方
wx.request({
url: 'xxx',
success: function (data) {
console.log(data);
}
});
// WePY 使用方式
wepy.request('xxxx').then((d) => console.log(d));
```
#### 2. 優化事件參數傳遞
點這里查看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/event.html?t=20161122" target="_blank">官方文檔</a>
```javascript
// 官方
<view data-id="{{index}}" data-title="wepy" data-other="otherparams" bindtap="tapName"> Click me! </view>
Page({
tapName: function(event) {
console.log(event.currentTarget.dataset.id)// output: 1
console.log(event.currentTarget.dataset.title)// output: wepy
console.log(event.currentTarget.dataset.other)// output: otherparams
}
});
// WePY 1.1.8以后的版本,只允許傳string。
<view bindtap="tapName({{index}}, 'wepy', 'otherparams')"> Click me! </view>
methods: {
tapName (id, title, other, event) {
console.log(id, title, other)// output: 1, wepy, otherparams
}
}
```
#### 3. 改變數據綁定方式
保留setData方法,但不建議使用setData執行綁定,修復傳入`undefined`的bug,并且修改入參支持:
`this.setData(target, value)`
`this.setData(object)`
點這里查看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/template.html?t=20161122" target="_blank">官方文檔</a>
```html
// 官方
<view> {{ message }} </view>
onLoad: function () {
this.setData({message: 'hello world'});
}
// WePY
<view> {{ message }} </view>
onLoad () {
this.message = 'hello world';
}
```
#### 4. 組件代替模板和模塊
點這里查看<a href="https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/data.html?t=20161122" target="_blank">官方文檔</a>
```html
// 官方
<!-- item.wxml -->
<template name="item">
<text>{{text}}</text>
</template>
<!-- index.wxml -->
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
<!-- index.js -->
var item = require('item.js')
// WePY
<!-- /components/item.wpy -->
<text>{{text}}</text>
<!-- index.wpy -->
<template>
<component id="item"></component>
</template>
<script>
import wepy from 'wepy';
import Item from '../components/item';
export default class Index extends wepy.page {
components = { Item }
}
</script>
```