微信小程序自發(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包的支持迫筑。如下圖:
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頁,將是一件非常爽的事情不狮。