微信小程序自發布到現在已經有半年多的時間了,憑藉微信平臺的強大影響力,愈來愈多企業加入小程序開發。 小程序於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也須要自行處理)微信小程序
筆者團隊最近開發了多個微信小程序,爲了彌補小程序各項不足和延續開發者VUE的開發習慣,團隊在開發初期 就選用了wepy框架,該框架是騰訊內部基於小程序的開發框架,設計思路基本參考VUE,開發模式和編碼風 格上80%以上接近VUE,開發者能夠以很小的成本從VUE開發切換成小程序開發,相比於小程序,主要優勢以下:promise
一、開發模式容易轉換 wepy在原有的小程序的開發模式下進行再次封裝,更貼近於現有MVVM框架開發模式。框架在開發過程當中參考了 一些如今框架的一些特性,而且融入其中,如下是使用wepy先後的代碼對比圖。bash
官方DEMO代碼:
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 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 // 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 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 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 npm install wepy-cli -g
腳手架
1 wepy new myproject
切換至項目目錄
1 cd myproject
實時編譯
1 wepy build --watch
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 · 首先事件監聽須要寫在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-cli:用於把.wpy文件提取分析並編譯成小程序所要求的wxml、wxss、js、json格式 wepy:編譯後js文件中的js框架
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 //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 <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 <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頁,將是一件很是爽的事情。
若是你喜歡咱們的文章,關注咱們的公衆號和咱們互動吧。