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

微信小程序自發(fā)布到如今已經有半年多的時間了,憑借微信平臺的強大影響力兄春,越來越多企業(yè)加入小程序開發(fā)醇滥。
小程序于M頁比相比怔球,有以下優(yōu)勢:
1.小程序擁有更多的能力咽白,包括定位、錄音盒延、文件咙崎、媒體桩盲、各種硬件能力等,想象空間更大
2.運行在微信內部湃番,體驗更接近APP
3.在過度競爭的互聯網行業(yè)中夭织,獲取一個有效APP用戶的成本已經非常高了,小程序相比APP更加輕量牵辣、即用即走摔癣,
更容易獲取用戶

開發(fā)對比

從開發(fā)角度來講奴饮,小程序官方封裝了很多常用組件給開發(fā)帶來很多便利性纬向,但同時也帶來很多不便:
1、小程序重新定義了DOM結構戴卜,沒有window逾条、document、div投剥、span等师脂,小程序只有view、text江锨、image等
封裝好的組件吃警,頁面布局只能通過這些基礎組件來實現,對開發(fā)人員來講需要一定的習慣轉換成本
2啄育、小程序不推薦直接操作DOM(僅僅從2017年7月開始才可以獲取DOM和部分屬性)酌心,如果不熟悉MVVM模式的開發(fā)者,
需要很高的學習成本
3挑豌、小程序沒有cookie安券,只能通過storage來模擬各項cookie操作(包括http中的setCookie也需要自行處理)

wepy

筆者團隊最近開發(fā)了多個微信小程序,為了彌補小程序各項不足和延續(xù)開發(fā)者VUE的開發(fā)習慣氓英,團隊在開發(fā)初期
就選用了wepy框架侯勉,該框架是騰訊內部基于小程序的開發(fā)框架,設計思路基本參考VUE铝阐,開發(fā)模式和編碼風
格上80%以上接近VUE址貌,開發(fā)者可以以很小的成本從VUE開發(fā)切換成小程序開發(fā),相比于小程序徘键,主要優(yōu)點如下:

1.開發(fā)模式容易轉換
wepy在原有的小程序的開發(fā)模式下進行再次封裝练对,更貼近于現有MVVM框架開發(fā)模式“⊙迹框架在開發(fā)過程中參考了
一些現在框架的一些特性锹淌,并且融入其中,以下是使用wepy前后的代碼對比圖赠制。

官方DEMO代碼:

/index.js
//獲取應用實例
var app = getApp()
Page({
    data: {
        motto: 'Hello World',
        userInfo: {}
    },
    //事件處理函數
    bindViewTap: function() {
        console.log('button clicked')
    },
    onLoad: function () {
        console.log('onLoad')
    }
})

基于wepy的實現:

import wepy from 'wepy';

export default class Index extends wepy.page {

    data = {
        motto: 'Hello World',
        userInfo: {}
    };
    methods = {
        bindViewTap () {
            console.log('button clicked');
        }
    };
    onLoad() {
        console.log('onLoad');
    };
}

2.真正的組件化開發(fā)
小程序雖然有<templete>標簽可以實現組件復用赂摆,但僅限于模板片段層面的復用挟憔,業(yè)務代碼與交互事件
仍需在頁面處理。無法實現組件化的松耦合與復用的效果烟号。

wepy組件示例

// 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';
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>

3.支持加載外部NPM包
小程序較大的缺陷是不支持NPM包绊谭,導致無法直接使用大量優(yōu)秀的開源內容,wepy在編譯過程當中汪拥,會遞歸
遍歷代碼中的require然后將對應依賴文件從node_modules當中拷貝出來达传,并且修改require為相對路徑,
從而實現對外部NPM包的支持迫筑。如下圖:

wepy對NPM包的處理

4.單文件模式宪赶,使得目錄結構更加清晰
小程序官方目錄結構要求app必須有三個文件app.json,app.js脯燃,app.wxss搂妻,頁面有4個文件 index.json,index.js辕棚,index.wxml欲主,index.wxss。而且文
件必須同名逝嚎。 所以使用wepy開發(fā)前后開發(fā)目錄對比如下:

官方DEMO:

project
├── pages
|   ├── index
|   |   ├── index.json  index 頁面配置
|   |   ├── index.js    index 頁面邏輯
|   |   ├── index.wxml  index 頁面結構
|   |   └── index.wxss  index 頁面樣式表
|   └── log
|       ├── log.json    log 頁面配置
|       ├── log.wxml    log 頁面邏輯
|       ├── log.js      log 頁面結構
|       └── log.wxss    log 頁面樣式表
├── app.js              小程序邏輯
├── app.json            小程序公共設置
└── app.wxss            小程序公共樣式表

使用wepy框架后目錄結構:

project
└── src
    ├── pages
    |   ├── index.wpy    index 頁面配置扁瓢、結構、樣式补君、邏輯
    |   └── log.wpy      log 頁面配置引几、結構、樣式赚哗、邏輯
    └──app.wpy           小程序配置項(全局樣式配置她紫、聲明鉤子等)

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

6.wepy支持使用less

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

如何開發(fā)

快速起步

安裝

npm install wepy-cli -g

腳手架

wepy new myproject

切換至項目目錄

cd myproject

實時編譯

wepy build --watch

目錄結構

├── dist                   微信開發(fā)者工具指定的目錄
├── node_modules
├── src                    代碼編寫的目錄
|   ├── components         組件文件夾(非完整頁面)
|   |   ├── com_a.wpy      可復用組件 a
|   |   └── com_b.wpy      可復用組件 b
|   ├── pages              頁面文件夾(完整頁面)
|   |   ├── index.wpy      頁面 index
|   |   └── page.wpy       頁面 page
|   └── app.wpy            小程序配置項(全局樣式配置、聲明鉤子等)
└── package.json           package 配置

wepy和VUE在編碼風格上面非常相似够掠,VUE開發(fā)者基本可以無縫切換民褂,因此這里僅介紹兩者的主要區(qū)別:

1.二者均支持props、data疯潭、computed赊堪、components、methods竖哩、watch(wepy中是watcher)哭廉,
但wepy中的methods僅可用于頁面事件綁定,其他自定義方法都要放在外層相叁,而VUE中所有方法均放在
methods下

2.wepy中props傳遞需要加上.sync修飾符(類似VUE1.x)才能實現props動態(tài)更新遵绰,并且父組件再
變更傳遞給子組件props后要執(zhí)行this.$apply()方法才能更新

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

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

· 首先事件監(jiān)聽需要寫在events屬性下:
``` bash
import wepy from 'wepy';
export default class Com extends wepy.component {
    components = {};
    data = {};
    methods = {};
    events = {
        'some-event': (p1, p2, p3, $event) => {
               console.log(`${this.name} receive ${$event.name} from ${$event.source.name}`);
        }
    };
    // Other properties
}
```
· $broadcast:父組件觸發(fā)所有子組件事件

· $emit:子組件觸發(fā)父組件事件

· $invoke:子組件觸發(fā)子組件事件

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

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

wepy原理研究

雖然wepy提升了小程序開發(fā)體驗,但畢竟最終要運行在小程序環(huán)境中个盆,歸根結底wepy還是需要編譯成小程序
需要的格式,因此wepy的核心在于代碼解析與編譯朵栖。

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

wepy編譯過程

編譯過程

拆解過程核心代碼

//wepy自定義屬性替換成小程序標準屬性過程
return content.replace(/<([\w-]+)\s*[\s\S]*?(\/|<\/[\w-]+)>/ig, (tag, tagName) => {
    tagName = tagName.toLowerCase();
    return tag.replace(/\s+:([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { // replace :param.sync => v-bind:param.sync
        if (type === '.once' || type === '.sync') {
        }
        else
            type = '.once';
        return ` v-bind:${name}${type}=`;
    }).replace(/\s+\@([\w-_]*)([\.\w]*)\s*=/ig, (attr, name, type) => { // replace @change => v-on:change
        const prefix = type !== '.user' ? (type === '.stop' ? 'catch' : 'bind') : 'v-on:';
        return ` ${prefix}${name}=`;
    });
});

...
//按xml格式解析wepy文件
xml = this.createParser().parseFromString(content);
const moduleId = util.genId(filepath);
//提取后的格式
let rst = {
    moduleId: moduleId,
    style: [],
    template: {
        code: '',
        src: '',
        type: ''
    },
    script: {
        code: '',
        src: '',
        type: ''
    }
};
//循環(huán)拆解提取過程
[].slice.call(xml.childNodes || []).forEach((child) => {
    const nodeName = child.nodeName;
    if (nodeName === 'style' || nodeName === 'template' || nodeName === 'script') {
        let rstTypeObj;

        if (nodeName === 'style') {
            rstTypeObj = {code: ''};
            rst[nodeName].push(rstTypeObj);
        } else {
            rstTypeObj = rst[nodeName];
        }

        rstTypeObj.src = child.getAttribute('src');
        rstTypeObj.type = child.getAttribute('lang') || child.getAttribute('type');
        if (nodeName === 'style') {
            // 針對于 style 增加是否包含 scoped 屬性
            rstTypeObj.scoped = child.getAttribute('scoped') ? true : false;
        }

        if (rstTypeObj.src) {
            rstTypeObj.src = path.resolve(opath.dir, rstTypeObj.src);
        }

        if (rstTypeObj.src && util.isFile(rstTypeObj.src)) {
            const fileCode = util.readFile(rstTypeObj.src, 'utf-8');
            if (fileCode === null) {
                throw '打開文件失敗: ' + rstTypeObj.src;
            } else {
                rstTypeObj.code += fileCode;
            }
        } else {
            [].slice.call(child.childNodes || []).forEach((c) => {
                rstTypeObj.code += util.decode(c.toString());
            });
        }

        if (!rstTypeObj.src)
            rstTypeObj.src = path.join(opath.dir, opath.name + opath.ext);
    }
});
...
// 拆解提取wxml過程
(() => {
    if (rst.template.type !== 'wxml' && rst.template.type !== 'xml') {
        let compiler = loader.loadCompiler(rst.template.type);
        if (compiler && compiler.sync) {
            if (rst.template.type === 'pug') { // fix indent for pug, https://github.com/wepyjs/wepy/issues/211
                let indent = util.getIndent(rst.template.code);
                if (indent.firstLineIndent) {
                    rst.template.code = util.fixIndent(rst.template.code, indent.firstLineIndent * -1, indent.char);
                }
            }
            //調用wxml解析模塊
            let compilerConfig = config.compilers[rst.template.type];

            // xmldom replaceNode have some issues when parsing pug minify html, so if it's not set, then default to un-minify html.
            if (compilerConfig.pretty === undefined) {
                compilerConfig.pretty = true;
            }
            rst.template.code = compiler.sync(rst.template.code, config.compilers[rst.template.type] || {});
            rst.template.type = 'wxml';
        }
    }
    if (rst.template.code)
        rst.template.node = this.createParser().parseFromString(util.attrReplace(rst.template.code));
})();

// 提取import資源文件過程
(() => {
    let coms = {};
    rst.script.code.replace(/import\s*([\w\-\_]*)\s*from\s*['"]([\w\-\_\.\/]*)['"]/ig, (match, com, path) => {
        coms[com] = path;
    });

    let match = rst.script.code.match(/[\s\r\n]components\s*=[\s\r\n]*/);
    match = match ? match[0] : undefined;
    let components = match ? this.grabConfigFromScript(rst.script.code, rst.script.code.indexOf(match) + match.length) : false;
    let vars = Object.keys(coms).map((com, i) => `var ${com} = "${coms[com]}";`).join('\r\n');
    try {
        if (components) {
            rst.template.components = new Function(`${vars}\r\nreturn ${components}`)();
        } else {
            rst.template.components = {};
        }
    } catch (e) {
        util.output('錯誤', path.join(opath.dir, opath.base));
        util.error(`解析components出錯终惑,報錯信息:${e}\r\n${vars}\r\nreturn ${components}`);
    }
})();
...

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

//compile-template.js
...
//將拆解處理好的wxml結構寫入文件
getTemplate (content) {
    content = `<template>${content}</template>`;
    let doc = new DOMImplementation().createDocument();
    let node = new DOMParser().parseFromString(content);
    let template = [].slice.call(node.childNodes || []).filter((n) => n.nodeName === 'template');

    [].slice.call(template[0].childNodes || []).forEach((n) => {
        doc.appendChild(n);
    });
    ...
    return doc;
},
//處理成微信小程序所需的wxml格式
compileXML (node, template, prefix, childNodes, comAppendAttribute = {}, propsMapping = {}) {
    //處理slot
    this.updateSlot(node, childNodes);
    //處理數據綁定bind方法
    this.updateBind(node, prefix, {}, propsMapping);
    //處理className
    if (node && node.documentElement) {
        Object.keys(comAppendAttribute).forEach((key) => {
            if (key === 'class') {
                let classNames = node.documentElement.getAttribute('class').split(' ').concat(comAppendAttribute[key].split(' ')).join(' ');
                node.documentElement.setAttribute('class', classNames);
            } else {
                node.documentElement.setAttribute(key, comAppendAttribute[key]);
            }
        });
    }
    //處理repeat標簽
    let repeats = util.elemToArray(node.getElementsByTagName('repeat'));
    ...

    //處理組件
    let componentElements = util.elemToArray(node.getElementsByTagName('component'));
    ...
    return node;
},

//template文件編譯模塊
compile (wpy){
    ...
    //將編譯好的內容寫入到文件
    let plg = new loader.PluginHelper(config.plugins, {
        type: 'wxml',
        code: util.decode(node.toString()),
        file: target,
        output (p) {
            util.output(p.action, p.file);
        },
        done (rst) {
            //寫入操作
            util.output('寫入', rst.file);
            rst.code = self.replaceBooleanAttr(rst.code);
            util.writeFile(target, rst.code);
        }
    });
}

編譯前后文件對比

wepy編譯前的文件:

<scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
    <!-- 商品列表組件 -->
    <view class="goods-list">
      <GoodsList :goodsList.sync="goodsList" :clickItemHandler="clickHandler" :redirect="redirect" :pageUrl="pageUrl"></GoodsList>
    </view>
</scroll-view>

wepy編譯后的文件:

<scroll-view scroll-y="true" class="list-page" scroll-top="{{scrollTop}}" bindscrolltolower="loadMore">
  <view class="goods-list">
    <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' : ''}}">
      <view class="item-img-list"><image src="{{item.pic}}" class="item-img" mode="aspectFill"/></view>
      <view class="item-desc">
        <view class="item-list-title">
          <text class="item-title">{{item.title}}</text>
        </view>
        <view class="item-list-price">
          <view wx:if="{{item.price && item.price>0}}" class="item-nowPrice"><i>¥</i>{{item.price}}</view>
          <view wx:if="{{item.originalPrice && item.originalPrice>0}}" class="item-oriPrice">¥{{item.originalPrice}}</view>
        </view>
        <view class="item-list-local"><view>{{item.cityName}}{{item.cityName&&item.businessName?' | ':''}}{{item.businessName}}    </view>
      </view>
      </view>
        <form class="form" bindsubmit="$GoodsList$sendFromId" report-submit="true" data-index="{{index}}">
          <button class="submit-button" form-type="submit"/>
        </form>
      </view>
    </view>
  </view>
</scroll-view>

可以看到wepy將頁面中所有引入的組件都直接寫入頁面當中,并且按照微信小程序的格式來輸出
當然也從一個側面看出臼寄,使用wepy框架后霸奕,代碼風格要比原生的更加簡潔優(yōu)雅

以上是wepy實現原理的簡要分析,有興趣的朋友可以去閱讀源碼(https://github.com/wepyjs/wepy)吉拳。
綜合來講质帅,wepy的核心在于編譯環(huán)節(jié),能夠將優(yōu)雅簡潔的類似VUE風格的代碼留攒,編譯成微信小程序所需要的繁雜代碼煤惩。

wepy作為一款優(yōu)秀的微信小程序框架,可以幫我們大幅提高開發(fā)效率炼邀,在為數不多的小程序框架中一枝獨秀魄揉,希望有更多的團隊選擇wepy。

PS:wepy也在實現小程序和VUE代碼同構拭宁,但目前還處在開發(fā)階段洛退,如果未來能實現一次開發(fā)票彪,同時產出小程序和M頁,將是一件非常爽的事情不狮。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末降铸,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子摇零,更是在濱河造成了極大的恐慌推掸,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驻仅,死亡現場離奇詭異谅畅,居然都是意外死亡,警方通過查閱死者的電腦和手機噪服,發(fā)現死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門毡泻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人粘优,你說我怎么就攤上這事仇味。” “怎么了雹顺?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵丹墨,是天一觀的道長。 經常有香客問我嬉愧,道長贩挣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任没酣,我火速辦了婚禮王财,結果婚禮上,老公的妹妹穿的比我還像新娘裕便。我一直安慰自己绒净,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布闪金。 她就那樣靜靜地躺著疯溺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哎垦。 梳的紋絲不亂的頭發(fā)上囱嫩,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音漏设,去河邊找鬼墨闲。 笑死,一個胖子當著我的面吹牛郑口,可吹牛的內容都是我干的鸳碧。 我是一名探鬼主播盾鳞,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瞻离!你這毒婦竟也來了腾仅?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤套利,失蹤者是張志新(化名)和其女友劉穎推励,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體肉迫,經...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡验辞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了喊衫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跌造。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖族购,靈堂內的尸體忽然破棺而出壳贪,到底是詐尸還是另有隱情,我是刑警寧澤联四,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布撑碴,位于F島的核電站,受9級特大地震影響朝墩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜伟姐,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一收苏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愤兵,春花似錦鹿霸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至屹堰,卻和暖如春肛冶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扯键。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工睦袖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荣刑。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓馅笙,卻偏偏與公主長得像伦乔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子董习,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內容

  • 前段時間看到這樣一段話:不要去巴結一個人皿淋,用暫時沒有朋友的時間去提升自己的能力招刹,待到時機成熟時,就會有一眾的朋友和...
    潘多拉魔咒閱讀 1,133評論 5 1
  • 背唐詩的時候年齡小沥匈,朗朗上口的多蔗喂,解其深意的少。背宋詞的時候高帖,瘋狂的愛宋詞缰儿,現在不喜歡,豪放派還好散址,太憂傷的心里也...
    吳雙小姐閱讀 894評論 11 20
  • 驚悚懸疑小說 分水林 第二卷 疑案魅影 第一章 洞穴枯老 黑暗,還是一片黑暗.黑暗像魔爪一般,統治著這個世界. 小...
    笑君殺手閱讀 280評論 0 0
  • 高樓窄巷殘花落葉厚雪酒家推杯換盞淚下晚霞西下怎能斷離別話
    授之以魚閱讀 514評論 1 2