像VUE同樣寫微信小程序-深刻研究wepy框架

像VUE同樣寫微信小程序-深刻研究wepy框架

 

微信小程序自發布到現在已經有半年多的時間了,憑藉微信平臺的強大影響力,愈來愈多企業加入小程序開發。 小程序於M頁比相比,有如下優點: html

一、小程序擁有更多的能力,包括定位、錄音、文件、媒體、各類硬件能力等,想象空間更大 node

二、運行在微信內部,體驗更接近APPgit

三、在過分競爭的互聯網行業中,獲取一個有效APP用戶的成本已經很是高了,小程序相比APP更加輕量、即用即走, 更容易獲取用戶github

 

開發對比

從開發角度來說,小程序官方封裝了不少經常使用組件給開發帶來不少便利性,但同時也帶來不少不便: npm

一、小程序從新定義了DOM結構,沒有window、document、div、span等,小程序只有view、text、image等 封裝好的組件,頁面佈局只能經過這些基礎組件來實現,對開發人員來說須要必定的習慣轉換成本 json

二、小程序不推薦直接操做DOM(僅僅從2017年7月開始才能夠獲取DOM和部分屬性),若是不熟悉MVVM模式的開發者, 須要很高的學習成本小程序

三、小程序沒有cookie,只能經過storage來模擬各項cookie操做(包括http中的setCookie也須要自行處理)微信小程序

wepy

筆者團隊最近開發了多個微信小程序,爲了彌補小程序各項不足和延續開發者VUE的開發習慣,團隊在開發初期 就選用了wepy框架,該框架是騰訊內部基於小程序的開發框架,設計思路基本參考VUE,開發模式和編碼風 格上80%以上接近VUE,開發者能夠以很小的成本從VUE開發切換成小程序開發,相比於小程序,主要優勢以下:promise

一、開發模式容易轉換 wepy在原有的小程序的開發模式下進行再次封裝,更貼近於現有MVVM框架開發模式。框架在開發過程當中參考了 一些如今框架的一些特性,而且融入其中,如下是使用wepy先後的代碼對比圖。bash

官方DEMO代碼:

  1.  1 /index.js
     2 
     3 //獲取應用實例
     4 
     5 var app = getApp()
     6 
     7 Page({
     8 
     9    data: {
    10 
    11        motto: 'Hello World',
    12 
    13        userInfo: {}
    14 
    15    },
    16 
    17    //事件處理函數
    18 
    19    bindViewTap: function() {
    20 
    21        console.log('button clicked')
    22 
    23    },
    24 
    25    onLoad: function () {
    26 
    27        console.log('onLoad')
    28 
    29    }
    30 
    31 })

     

基於wepy的實現:

  1.  1 import wepy from 'wepy';
     2 
     3  
     4 
     5 export default class Index extends wepy.page {
     6 
     7  
     8 
     9    data = {
    10 
    11        motto: 'Hello World',
    12 
    13        userInfo: {}
    14 
    15    };
    16 
    17    methods = {
    18 
    19        bindViewTap () {
    20 
    21            console.log('button clicked');
    22 
    23        }
    24 
    25    };
    26 
    27    onLoad() {
    28 
    29        console.log('onLoad');
    30 
    31    };
    32 
    33 }

     

2.真正的組件化開發 小程序雖然有標籤能夠實現組件複用,但僅限於模板片斷層面的複用,業務代碼與交互事件 仍需在頁面處理。沒法實現組件化的鬆耦合與複用的效果。

wepy組件示例

  1.  1 // index.wpy
     2 
     3 <template>
     4 
     5    <view>
     6 
     7        <panel>
     8 
     9            <h1 slot="title"></h1>
    10 
    11        </panel>
    12 
    13        <counter1 :num="myNum"></counter1>
    14 
    15        <counter2 :num.sync="syncNum"></counter2>
    16 
    17        <list :item="items"></list>
    18 
    19    </view>
    20 
    21 </template>
    22 
    23 <script>
    24 
    25 import wepy from 'wepy';
    26 
    27 import List from '../components/list';
    28 
    29 import Panel from '../components/panel';
    30 
    31 import Counter from '../components/counter';
    32 
    33  
    34 
    35 export default class Index extends wepy.page {
    36 
    37  
    38 
    39    config = {
    40 
    41        "navigationBarTitleText": "test"
    42 
    43    };
    44 
    45    components = {
    46 
    47        panel: Panel,
    48 
    49        counter1: Counter,
    50 
    51        counter2: Counter,
    52 
    53        list: List
    54 
    55    };
    56 
    57    data = {
    58 
    59        myNum: 50,
    60 
    61        syncNum: 100,
    62 
    63        items: [1, 2, 3, 4]
    64 
    65    }
    66 
    67 }
    68 
    69 </script>

     

3.支持加載外部NPM包 小程序較大的缺陷是不支持NPM包,致使沒法直接使用大量優秀的開源內容,wepy在編譯過程中,會遞歸 遍歷代碼中的require而後將對應依賴文件從node_modules當中拷貝出來,而且修改require爲相對路徑, 從而實現對外部NPM包的支持。以下圖:

4.單文件模式,使得目錄結構更加清晰 小程序官方目錄結構要求app必須有三個文件app.json,app.js,app.wxss,頁面有4個文件 index.json,index.js,index.wxml,index.wxss。並且文 件必須同名。 因此使用wepy開發先後開發目錄對好比下:

官方DEMO:

  1.  1 project
     2 
     3 ├── pages
     4 
     5 |   ├── index
     6 
     7 |   |   ├── index.json  index 頁面配置
     8 
     9 |   |   ├── index.js    index 頁面邏輯
    10 
    11 |   |   ├── index.wxml  index 頁面結構
    12 
    13 |   |   └── index.wxss  index 頁面樣式表
    14 
    15 |   └── log
    16 
    17 |       ├── log.json    log 頁面配置
    18 
    19 |       ├── log.wxml    log 頁面邏輯
    20 
    21 |       ├── log.js      log 頁面結構
    22 
    23 |       └── log.wxss    log 頁面樣式表
    24 
    25 ├── app.js              小程序邏輯
    26 
    27 ├── app.json            小程序公共設置
    28 
    29 └── app.wxss            小程序公共樣式表

     

使用wepy框架後目錄結構:

  1.  1 project
     2 
     3 └── src
     4 
     5    ├── pages
     6 
     7    |   ├── index.wpy    index 頁面配置、結構、樣式、邏輯
     8 
     9    |   └── log.wpy      log 頁面配置、結構、樣式、邏輯
    10 
    11    └──app.wpy           小程序配置項(全局樣式配置、聲明鉤子等)

     

5.默認使用babel編譯,支持ES6/7的一些新特性。

6.wepy支持使用less

默認開啓使用了一些新的特性如promise,async/await等等

如何開發

快速起步

安裝

  1. 1 npm install wepy-cli -g

腳手架

  1. 1 wepy new myproject

切換至項目目錄

  1. 1 cd myproject

實時編譯

  1. 1 wepy build --watch

     

目錄結構

  1.  1 ├── dist                   微信開發者工具指定的目錄
     2 
     3 ├── node_modules
     4 
     5 ├── src                    代碼編寫的目錄
     6 
     7 |   ├── components         組件文件夾(非完整頁面)
     8 
     9 |   |   ├── com_a.wpy      可複用組件 a
    10 
    11 |   |   └── com_b.wpy      可複用組件 b
    12 
    13 |   ├── pages              頁面文件夾(完整頁面)
    14 
    15 |   |   ├── index.wpy      頁面 index
    16 
    17 |   |   └── page.wpy       頁面 page
    18 
    19 |   └── app.wpy            小程序配置項(全局樣式配置、聲明鉤子等)
    20 
    21 └── package.json           package 配置

     

wepy和VUE在編碼風格上面很是類似,VUE開發者基本能夠無縫切換,所以這裏僅介紹二者的主要區別:

1.兩者均支持props、data、computed、components、methods、watch(wepy中是watcher), 但wepy中的methods僅可用於頁面事件綁定,其餘自定義方法都要放在外層,而VUE中全部方法均放在 methods下

2.wepy中props傳遞須要加上.sync修飾符(相似VUE1.x)才能實現props動態更新,而且父組件再 變動傳遞給子組件props後要執行this.$apply()方法才能更新

3.wepy支持數據雙向綁定,子組件在定義props時加上twoway:true屬性值便可實現子組件修改父組 件數據

4.VUE2.x推薦使用eventBus方式進行組件通訊,而在wepy中是經過$broadcast,$emit,$invoke 三種方法實現通訊

  1.  1 · 首先事件監聽須要寫在events屬性下:
     2 
     3 ``` bash
     4 
     5 import wepy from 'wepy';
     6 
     7 export default class Com extends wepy.component {
     8 
     9    components = {};
    10 
    11    data = {};
    12 
    13    methods = {};
    14 
    15    events = {
    16 
    17        'some-event': (p1, p2, p3, $event) => {
    18 
    19              console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`);
    20 
    21        }
    22 
    23    };
    24 
    25    // Other properties
    26 
    27 }
    28 
    29 ```
    30 
    31 · $broadcast:父組件觸發全部子組件事件
    32 
    33  
    34 
    35 · $emit:子組件觸發父組件事件
    36 
    37  
    38 
    39 · $invoke:子組件觸發子組件事件

     

5.VUE的生命週期包括created、mounted等,wepy僅支持小程序的生命週期:onLoad、onReady等

6.wepy不支持過濾器、keep-alive、ref、transition、全局插件、路由管理、服務端渲染等VUE特性技術

wepy原理研究

雖然wepy提高了小程序開發體驗,但畢竟最終要運行在小程序環境中,歸根結底wepy仍是須要編譯成小程序 須要的格式,所以wepy的核心在於代碼解析與編譯。

wepy項目文件主要有兩個: wepy-cli:用於把.wpy文件提取分析並編譯成小程序所要求的wxml、wxss、js、json格式 wepy:編譯後js文件中的js框架

wepy編譯過程

拆解過程核心代碼

  1.   1 //wepy自定義屬性替換成小程序標準屬性過程
      2 
      3 return content.replace(/<([\w-]+)\s*[\s\S]*?(\/|<\/[\w-]+)>/ig, (tag, tagName) => {
      4 
      5    tagName = tagName.toLowerCase();
      6 
      7    return tag.replace(/\s+:([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { // replace :param.sync => v-bind:param.sync
      8 
      9        if (type === '.once' || type === '.sync') {
     10 
     11        }
     12 
     13        else
     14 
     15            type = '.once';
     16 
     17        return ` v-bind:${name}${type}=`;
     18 
     19    }).replace(/\s+\@([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { // replace @change => v-on:change
     20 
     21        const prefix = type !== '.user' ? (type === '.stop' ? 'catch' : 'bind') : 'v-on:';
     22 
     23        return ` ${prefix}${name}=`;
     24 
     25    });
     26 
     27 });
     28 
     29  
     30 
     31 ...
     32 
     33 //按xml格式解析wepy文件
     34 
     35 xml = this.createParser().parseFromString(content);
     36 
     37 const moduleId = util.genId(filepath);
     38 
     39 //提取後的格式
     40 
     41 let rst = {
     42 
     43    moduleId: moduleId,
     44 
     45    style: [],
     46 
     47    template: {
     48 
     49        code: '',
     50 
     51        src: '',
     52 
     53        type: ''
     54 
     55    },
     56 
     57    script: {
     58 
     59        code: '',
     60 
     61        src: '',
     62 
     63        type: ''
     64 
     65    }
     66 
     67 };
     68 
     69 //循環拆解提取過程
     70 
     71 [].slice.call(xml.childNodes || []).forEach((child) => {
     72 
     73    const nodeName = child.nodeName;
     74 
     75    if (nodeName === 'style' || nodeName === 'template' || nodeName === 'script') {
     76 
     77        let rstTypeObj;
     78 
     79  
     80 
     81        if (nodeName === 'style') {
     82 
     83            rstTypeObj = {code: ''};
     84 
     85            rst[nodeName].push(rstTypeObj);
     86 
     87        } else {
     88 
     89            rstTypeObj = rst[nodeName];
     90 
     91        }
     92 
     93  
     94 
     95        rstTypeObj.src = child.getAttribute('src');
     96 
     97        rstTypeObj.type = child.getAttribute('lang') || child.getAttribute('type');
     98 
     99        if (nodeName === 'style') {
    100 
    101            // 針對於 style 增長是否包含 scoped 屬性
    102 
    103            rstTypeObj.scoped = child.getAttribute('scoped') ? true : false;
    104 
    105        }
    106 
    107  
    108 
    109        if (rstTypeObj.src) {
    110 
    111            rstTypeObj.src = path.resolve(opath.dir, rstTypeObj.src);
    112 
    113        }
    114 
    115  
    116 
    117        if (rstTypeObj.src && util.isFile(rstTypeObj.src)) {
    118 
    119            const fileCode = util.readFile(rstTypeObj.src, 'utf-8');
    120 
    121            if (fileCode === null) {
    122 
    123                throw '打開文件失敗: ' + rstTypeObj.src;
    124 
    125            } else {
    126 
    127                rstTypeObj.code += fileCode;
    128 
    129            }
    130 
    131        } else {
    132 
    133            [].slice.call(child.childNodes || []).forEach((c) => {
    134 
    135                rstTypeObj.code += util.decode(c.toString());
    136 
    137            });
    138 
    139        }
    140 
    141  
    142 
    143        if (!rstTypeObj.src)
    144 
    145            rstTypeObj.src = path.join(opath.dir, opath.name + opath.ext);
    146 
    147    }
    148 
    149 });
    150 
    151 ...
    152 
    153 // 拆解提取wxml過程
    154 
    155 (() => {
    156 
    157    if (rst.template.type !== 'wxml' && rst.template.type !== 'xml') {
    158 
    159        let compiler = loader.loadCompiler(rst.template.type);
    160 
    161        if (compiler && compiler.sync) {
    162 
    163            if (rst.template.type === 'pug') { // fix indent for pug, https://github.com/wepyjs/wepy/issues/211
    164 
    165                let indent = util.getIndent(rst.template.code);
    166 
    167                if (indent.firstLineIndent) {
    168 
    169                    rst.template.code = util.fixIndent(rst.template.code, indent.firstLineIndent * -1, indent.char);
    170 
    171                }
    172 
    173            }
    174 
    175            //調用wxml解析模塊
    176 
    177            let compilerConfig = config.compilers[rst.template.type];
    178 
    179  
    180 
    181            // xmldom replaceNode have some issues when parsing pug minify html, so if it's not set, then default to un-minify html.
    182 
    183            if (compilerConfig.pretty === undefined) {
    184 
    185                compilerConfig.pretty = true;
    186 
    187            }
    188 
    189            rst.template.code = compiler.sync(rst.template.code, config.compilers[rst.template.type] || {});
    190 
    191            rst.template.type = 'wxml';
    192 
    193        }
    194 
    195    }
    196 
    197    if (rst.template.code)
    198 
    199        rst.template.node = this.createParser().parseFromString(util.attrReplace(rst.template.code));
    200 
    201 })();
    202 
    203  
    204 
    205 // 提取import資源文件過程
    206 
    207 (() => {
    208 
    209    let coms = {};
    210 
    211    rst.script.code.replace(/import\s*([\w\-\_]*)\s*from\s*['"]([\w\-\_\.\/]*)['"]/ig, (match, com, path) => {
    212 
    213        coms[com] = path;
    214 
    215    });
    216 
    217  
    218 
    219    let match = rst.script.code.match(/[\s\r\n]components\s*=[\s\r\n]*/);
    220 
    221    match = match ? match[0] : undefined;
    222 
    223    let components = match ? this.grabConfigFromScript(rst.script.code, rst.script.code.indexOf(match) + match.length) : false;
    224 
    225    let vars = Object.keys(coms).map((com, i) => `var ${com} = "${coms[com]}";`).join('\r\n');
    226 
    227    try {
    228 
    229        if (components) {
    230 
    231            rst.template.components = new Function(`${vars}\r\nreturn ${components}`)();
    232 
    233        } else {
    234 
    235            rst.template.components = {};
    236 
    237        }
    238 
    239    } catch (e) {
    240 
    241        util.output('錯誤', path.join(opath.dir, opath.base));
    242 
    243        util.error(`解析components出錯,報錯信息:${e}\r\n${vars}\r\nreturn ${components}`);
    244 
    245    }
    246 
    247 })();
    248 
    249 ...

     

wepy中有專門的script、style、template、config解析模塊 以template模塊舉例:

  1.   1 //compile-template.js
      2 
      3 ...
      4 
      5 //將拆解處理好的wxml結構寫入文件
      6 
      7 getTemplate (content) {
      8 
      9    content = `<template>${content}</template>`;
     10 
     11    let doc = new DOMImplementation().createDocument();
     12 
     13    let node = new DOMParser().parseFromString(content);
     14 
     15    let template = [].slice.call(node.childNodes || []).filter((n) => n.nodeName === 'template');
     16 
     17  
     18 
     19    [].slice.call(template[0].childNodes || []).forEach((n) => {
     20 
     21        doc.appendChild(n);
     22 
     23    });
     24 
     25    ...
     26 
     27    return doc;
     28 
     29 },
     30 
     31 //處理成微信小程序所需的wxml格式
     32 
     33 compileXML (node, template, prefix, childNodes, comAppendAttribute = {}, propsMapping = {}) {
     34 
     35    //處理slot
     36 
     37    this.updateSlot(node, childNodes);
     38 
     39    //處理數據綁定bind方法
     40 
     41    this.updateBind(node, prefix, {}, propsMapping);
     42 
     43    //處理className
     44 
     45    if (node && node.documentElement) {
     46 
     47        Object.keys(comAppendAttribute).forEach((key) => {
     48 
     49            if (key === 'class') {
     50 
     51                let classNames = node.documentElement.getAttribute('class').split(' ').concat(comAppendAttribute[key].split(' ')).join(' ');
     52 
     53                node.documentElement.setAttribute('class', classNames);
     54 
     55            } else {
     56 
     57                node.documentElement.setAttribute(key, comAppendAttribute[key]);
     58 
     59            }
     60 
     61        });
     62 
     63    }
     64 
     65    //處理repeat標籤
     66 
     67    let repeats = util.elemToArray(node.getElementsByTagName('repeat'));
     68 
     69    ...
     70 
     71  
     72 
     73    //處理組件
     74 
     75    let componentElements = util.elemToArray(node.getElementsByTagName('component'));
     76 
     77    ...
     78 
     79    return node;
     80 
     81 },
     82 
     83  
     84 
     85 //template文件編譯模塊
     86 
     87 compile (wpy){
     88 
     89    ...
     90 
     91    //將編譯好的內容寫入到文件
     92 
     93    let plg = new loader.PluginHelper(config.plugins, {
     94 
     95        type: 'wxml',
     96 
     97        code: util.decode(node.toString()),
     98 
     99        file: target,
    100 
    101        output (p) {
    102 
    103            util.output(p.action, p.file);
    104 
    105        },
    106 
    107        done (rst) {
    108 
    109            //寫入操做
    110 
    111            util.output('寫入', rst.file);
    112 
    113            rst.code = self.replaceBooleanAttr(rst.code);
    114 
    115            util.writeFile(target, rst.code);
    116 
    117        }
    118 
    119    });
    120 
    121 }

     

編譯先後文件對比

wepy編譯前的文件:

  1.  1 <scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
     2 
     3    <!-- 商品列表組件 -->
     4 
     5    <view class="goods-list">
     6 
     7      <GoodsList :goodsList.sync="goodsList" :clickItemHandler="clickHandler" :redirect="redirect" :pageUrl="pageUrl"></GoodsList>
     8 
     9    </view>
    10 
    11 </scroll-view>

     

wepy編譯後的文件:

  1.  1 <scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
     2 
     3  <view class="goods-list">
     4 
     5    <view  wx:for="{{$GoodsList$goodsList}}" wx:for-item="item" wx:for-index="index" wx:key="{{item.infoId}}" bindtap="$GoodsList$clickHandler" data-index="{{index}}" class="item-list-container{{index%2==0 ? ' left-item' : ''}}">
     6 
     7      <view class="item-img-list"><image src="{{item.pic}}" class="item-img" mode="aspectFill"/></view>
     8 
     9      <view class="item-desc">
    10 
    11        <view class="item-list-title">
    12 
    13          <text class="item-title">{{item.title}}</text>
    14 
    15        </view>
    16 
    17        <view class="item-list-price">
    18 
    19          <view wx:if="{{item.price && item.price>0}}" class="item-nowPrice"><i></i>{{item.price}}</view>
    20 
    21          <view wx:if="{{item.originalPrice && item.originalPrice>0}}" class="item-oriPrice">¥{{item.originalPrice}}</view>
    22 
    23        </view>
    24 
    25        <view class="item-list-local"><view>{{item.cityName}}{{item.cityName&&item.businessName?' | ':''}}{{item.businessName}}    </view>
    26 
    27      </view>
    28 
    29      </view>
    30 
    31        <form class="form" bindsubmit="$GoodsList$sendFromId" report-submit="true" data-index="{{index}}">
    32 
    33          <button class="submit-button" form-type="submit"/>
    34 
    35        </form>
    36 
    37      </view>
    38 
    39    </view>
    40 
    41  </view>
    42 
    43 </scroll-view>

     

能夠看到wepy將頁面中全部引入的組件都直接寫入頁面當中,而且按照微信小程序的格式來輸出 固然也從一個側面看出,使用wepy框架後,代碼風格要比原生的更加簡潔優雅

以上是wepy實現原理的簡要分析,有興趣的朋友能夠去閱讀源碼(https://github.com/wepyjs/wepy)。 綜合來說,wepy的核心在於編譯環節,可以將優雅簡潔的相似VUE風格的代碼,編譯成微信小程序所須要的繁雜代碼。

wepy做爲一款優秀的微信小程序框架,能夠幫咱們大幅提升開發效率,在爲數很少的小程序框架中一枝獨秀,但願有更多的團隊選擇wepy。

PS:wepy也在實現小程序和VUE代碼同構,但目前還處在開發階段,若是將來能實現一次開發,同時產出小程序和M頁,將是一件很是爽的事情。

 

若是你喜歡咱們的文章,關注咱們的公衆號和咱們互動吧。

相關文章
相關標籤/搜索