連接: https://pan.baidu.com/s/16j8WCMv2JrRK2OzwvMwZ1w 提取碼: 34mp javascript
咱們以前對小程序作了基本學習:php
閱讀本文以前,若是你們想對小程序有更深刻的瞭解,或者一些細節的瞭解能夠先閱讀上述文章,本文後面點須要對着代碼調試閱讀css
對應的github地址是:https://github.com/yexiaochai/wxdemohtml
首先咱們來一言以蔽之,什麼是微信小程序?PS:這個問題問得好像有些扯:)前端
小程序是一個不須要下載安裝就可以使用的應用,它實現了應用觸手可及的夢想,用戶掃一掃或者搜一下便可打開應用。也體現了用完即走的理念,用戶不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝卸載。從字面上看小程序具備相似Web應用的熱部署能力,在功能上又接近於原生APP。java
因此說,其實微信小程序是一套超級Hybrid的解決方案,如今看來,小程序應該是應用場景最廣,也最爲複雜的解決方案了。node
不少公司都會有本身的Hybrid平臺,我這裏瞭解到比較不錯的是攜程的Hybrid平臺、阿里的Weex、百度的糯米,可是從應用場景來講都沒有微信來得豐富,這裏根本的區別是:ios
微信小程序是給各個公司開發者接入的,其餘公司平臺可能是給本身業務團隊使用,這一根本區別,就造就了咱們看到的不少小程序不同的特性:git
① 小程序定義了本身的標籤語言WXMLgithub
② 小程序定義了本身的樣式語言WXSS
③ 小程序提供了一套前端框架包括對應Native API
④ 禁用瀏覽器Dom API(這個區別,會影響咱們的代碼方式)
只要瞭解到這些區別就會知道爲何小程序會這麼設計:
由於小程序是給各個公司的開發作的,其餘公司的Hybrid方案是給公司業務團隊用的,通常擁有Hybrid平臺的公司實力都不錯 可是開發小程序的公司實力參差不齊,因此小程序要作絕對的限制,最大程度的保證框架層(小程序團隊)對程序的控制 由於畢竟程序運行在微信這種體量的APP中
以前我也有一個疑惑爲何微信小程序會設計本身的標籤語言,也在知乎看到各類各樣的回答,可是若是出於設計層面以及應用層面考慮的話:這樣會有更好的控制,並且我後面發現微信小程序事實上依舊使用的是webview作渲染(這個與我以前認爲微信是NativeUI是向左的),可是若是咱們使用的微信限制下面的標籤,這個是有限的標籤,後期想要換成NativeUI會變得更加輕易:
另外一方面,通過以前的學習,我這邊明確能夠得出一個感覺:
① 小程序的頁面核心是標籤,標籤是不可控制的(我暫時沒用到js操做元素的方法),只能按照微信給的玩法玩,標籤控制顯示是咱們的view
② 標籤的展現只與data有關聯,和js是隔離的,沒有辦法在標籤中調用js的方法
③ 而咱們的js的惟一工做即是根據業務改變data,從新引起頁面渲染,之後別想操做DOM,別想操做Window對象了,改變開發方式,改變開發方式,改變開發方式!
1 this.setData({'wxml': ` 2 <my-component> 3 <view>動態插入的節點</view> 4 </my-component> 5 `});
而後能夠看到這個是一個MVC模型
每一個頁面的目錄是這個樣子的:
1 project 2 ├── pages 3 | ├── index 4 | | ├── index.json index 頁面配置 5 | | ├── index.js index 頁面邏輯 6 | | ├── index.wxml index 頁面結構 7 | | └── index.wxss index 頁面樣式表 8 | └── log 9 | ├── log.json log 頁面配置 10 | ├── log.wxml log 頁面邏輯 11 | ├── log.js log 頁面結構 12 | └── log.wxss log 頁面樣式表 13 ├── app.js 小程序邏輯 14 ├── app.json 小程序公共設置 15 └── app.wxss 小程序公共樣式表
每一個組件的目錄也大概是這個樣子的,大同小異,可是入口是Page層。
小程序打包後的結構(這裏就真的不懂了,引用:小程序底層框架實現原理解析):
全部的小程序基本都最後都被打成上面的結構
一、WAService.js 框架JS庫,提供邏輯層基礎的API能力
二、WAWebview.js 框架JS庫,提供視圖層基礎的API能力
三、WAConsole.js 框架JS庫,控制檯
四、app-config.js 小程序完整的配置,包含咱們經過app.json裏的全部配置,綜合了默認配置型
五、app-service.js 咱們本身的JS代碼,所有打包到這個文件
六、page-frame.html 小程序視圖的模板文件,全部的頁面都使用此加載渲染,且全部的WXML都拆解爲JS實現打包到這裏
七、pages 全部的頁面,這個不是咱們以前的wxml文件了,主要是處理WXSS轉換,使用js插入到header區域
從設計的角度上說,小程序採用的組件化開發的方案,除了頁面級別的標籤,後面所有是組件,而組件中的標籤view、data、js的關係應該是與page是一致的,這個也是咱們平時建議的開發方式,將一根頁面拆分紅一個個小的業務組件或者UI組件:
從我寫業務代碼過程當中,以爲總體來講仍是比較順暢的,小程序是有本身一套完整的前端框架的,而且釋放給業務代碼的主要就是page,而page只能使用標籤和組件,因此說框架的對業務的控制力度很好。
最後咱們從工程角度來看微信小程序的架構就更加完美了,小程序從三個方面考慮了業務者的感覺:
① 開發工具+調試工具
② 開發基本模型(開發基本標準WXML、WXSS、JS、JSON)
③ 完善的構建(對業務方透明)
④ 自動化上傳離線包(對業務費透明離線包邏輯)
⑤ 監控統計邏輯
因此,微信小程序從架構上和使用場景來講是很使人驚豔的,至少驚豔了我......因此咱們接下來在開發層面對他進行更加深刻的剖析,咱們這邊最近一直在作基礎服務,這一切都是爲了完善技術體系,這裏對於前端來講即是咱們須要作一個Hybrid體系,若是作App,React Native也是不錯的選擇,可是必定要有完善的分層:
① 底層框架解決開發效率,將複雜的部分作成一個黑匣子,給頁面開發展現的只是固定的三板斧,固定的模式下開發便可
② 工程部門爲業務開發者封裝最小化開發環境,最優爲瀏覽器,確實不行便爲其提供一個相似瀏覽器的調試環境
如此一來,業務便能快速迭代,由於業務開發者寫的代碼大同小異,因此底層框架配合工程團隊(通常是同一個團隊),即可以在底層作掉不少效率性能問題。
稍微大點的公司,稍微寬裕的團隊,還會同步作不少後續的性能監控、錯誤日誌工做,如此造成一套文檔->開發->調試->構建->發佈->監控、分析 爲一套完善的技術體系
若是造成了這麼一套體系,那麼後續就算是內部框架更改、技術革新,也是在這個體系上改造,這塊微信小程序是作的很是好的。但很惋惜,不少其餘公司團隊只會在這個路徑上作一部分,後面因爲種種緣由不在深刻,有多是感受沒價值,而最恐怖的行爲是,本身的體系沒造成就貿然的換基礎框架,戒之慎之啊!好了閒話少說,咱們繼續接下來的學習。
我對小程序的理解有限,由於沒有源碼只能靠驚豔猜想,若是文中有誤,請各位多多提點
文章更多面對初中級選手,若是對各位有用,麻煩點贊喲
微信小程序爲了對業務方有更強的控制,App層作的工做頗有限,我後面寫demo的時候根本沒有用到app.js,因此我這裏認爲app.js只是完成了一個路由以及初始化相關的工做,這個是咱們看獲得的,咱們看不到的是底層框架會根據app.json的配置將全部頁面js都準備好。
我這裏要表達的是,咱們這裏配置了咱們全部的路由:
"pages":[ "pages/index/index", "pages/list/list", "pages/logs/logs" ],
微信小程序一旦載入,會開3個webview,裝載3個頁面的邏輯,完成基本的實例化工做,只顯示首頁!這個是小程序爲了優化頁面打開速度所作的工做,也勢必會浪費一些資源,因此究竟是所有打開或者預加載幾個,詳細底層Native會根據實際狀況動態變化,咱們也能夠看到,從業務層面來講,要了解小程序的執行流程,其實只要能瞭解Page的流程就行了,關於Page生命週期,除了釋放出來的API:onLoad -> onShow -> onReady -> onHide等,官方還出了一張圖進行說明:
Native層在載入小程序時候,起了兩個線程一個的view Thread一個是AppService Thread,我這邊理解下來應該就是程序邏輯執行與頁面渲染分離,小程序的視圖層目前使用 WebView 做爲渲染載體,而邏輯層是由獨立的 JavascriptCore 做爲運行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模塊,並不具有數據直接共享的通道。當前,視圖層和邏輯層的數據傳輸,實際上經過兩邊提供的 evaluateJavascript
所實現。即用戶傳輸的數據,須要將其轉換爲字符串形式傳遞,同時把轉換後的數據內容拼接成一份 JS 腳本,再經過執行 JS 腳本的形式傳遞到兩邊獨立環境。而 evaluateJavascript
的執行會受不少方面的影響,數據到達視圖層並非實時的。
由於以前我認爲頁面是使用NativeUI作渲染跟Webview沒撒關係,便以爲這個圖有問題,可是後面實際代碼看到了熟悉的shadow-dom以及Android能夠看到哪部分是Web的,其實小程序主體仍是使用的瀏覽器渲染的方式,仍是webview裝載HTML和CSS的邏輯,最後我發現這張圖是沒有問題的,有問題的是個人理解,哈哈,這裏咱們從新解析這張圖:
WXML先會被編譯成JS文件,引入數據後在WebView中渲染,這裏能夠認爲微信載入小程序時同時初始化了兩個線程,分別執行彼此邏輯:
① WXML&CSS編譯造成的JS View實例化結束,準備結束時向業務線程發送通知
② 業務線程中的JS Page部分同步完成實例化結束,這個時候接收到View線程部分的等待數據通知,將初始化data數據發送給View
③ View線程接到數據,開始渲染頁面,渲染結束執行通知Page觸發onReady事件
這裏翻開源碼,能夠看到,應該是全局控制器完成的Page實例化,完成後便會執行onLoad事件,可是在執行前會往頁面發通知:
1 __appServiceSDK__.invokeWebviewMethod({ 2 name: "appDataChange", 3 args: o({}, e, { 4 complete: n 5 }), 6 webviewIds: [t] 7 })
真實的邏輯是這樣的,全局控制器會完成頁面實例化,這個是根據app.json中來的,所有完成實例化存儲起來而後選擇第一個page實例執行一些邏輯,而後通知view線程,即將執行onLoad事件,由於view線程和業務線程是兩個線程,因此不會形成阻塞,view線程根據初始數據完成渲染,而業務線程繼續後續邏輯,執行onLoad,若是onLoad中有setData,那麼會進入隊列繼續通知view線程更新。
因此我我的感受微信官網那張圖不太清晰,我這裏從新畫了一個圖:
都這個時候了,不來個簡單的小程序框架實現好像有點不對,咱們作小程序實現的主要緣由是想作到一端代碼三端運行:web、小程序、Hybrid甚至Servce端
咱們這裏沒有可能實現太複雜的功能,這裏想的是就實現一個基本的頁面展現帶一個最基本的標籤便可,只作Page一塊的簡單實現,讓你們能瞭解到小程序可能的實現,以及如何將小程序直接轉爲H5的可能走法
1 <view> 2 <!-- 如下是對一個自定義組件的引用 --> 3 <my-component inner-text="組件數據"></my-component> 4 <view>{{pageData}}</view> 5 </view>
1 Page({ 2 data: { 3 pageData: '頁面數據' 4 }, 5 onLoad: function () { 6 console.log('onLoad') 7 }, 8 })
1 <!-- 這是自定義組件的內部WXML結構 --> 2 <view class="inner"> 3 {{innerText}} 4 </view> 5 <slot></slot>
1 Component({ 2 properties: { 3 // 這裏定義了innerText屬性,屬性值能夠在組件使用時指定 4 innerText: { 5 type: String, 6 value: 'default value', 7 } 8 }, 9 data: { 10 // 這裏是一些組件內部數據 11 someData: {} 12 }, 13 methods: { 14 // 這裏是一個自定義方法 15 customMethod: function () { } 16 } 17 })
咱們直接將小程序這些代碼拷貝一份到咱們的目錄:
咱們須要作的就是讓這段代碼運行起來,而這裏的目錄是咱們最終看見的目錄,真實運行的時候可能不是這個樣,運行以前項目會經過咱們的工程構建,變成能夠直接運行的代碼,而我這裏思考的能夠運行的代碼事實上是一個模塊,因此咱們這裏從最終結果反推、分拆到開發結構目錄,咱們首先將全部代碼放到index.html,多是這樣的:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 9 <script type="text/javascript" src="libs/zepto.js" ></script> 10 <script type="text/javascript"> 11 12 class View { 13 constructor(opts) { 14 this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>'; 15 16 //由控制器page傳入的初始數據或者setData產生的數據 17 this.data = { 18 pageShow: 'pageshow', 19 pageData: 'pageData', 20 pageShow1: 'pageShow1' 21 }; 22 23 this.labelMap = { 24 'view': 'div', 25 '#text': 'span' 26 }; 27 28 this.nodes = {}; 29 this.nodeInfo = {}; 30 } 31 32 /* 33 傳入一個節點,解析出一個節點,而且將節點中的數據以初始化數據改變 34 而且將其中包含{{}}標誌的節點信息記錄下來 35 */ 36 _handlerNode (node) { 37 38 let reg = /\{\{([\s\S]+?)\}\}/; 39 let result, name, value, n, map = {}; 40 let attrs , i, len, attr; 41 42 name = node.nodeName; 43 attrs = node.attributes; 44 value = node.nodeValue; 45 n = document.createElement(this.labelMap[name.toLowerCase()] || name); 46 47 //說明是文本,須要記錄下來了 48 if(node.nodeType === 3) { 49 n.innerText = this.data[value] || ''; 50 51 result = reg.exec(value); 52 if(result) { 53 n.innerText = this.data[result[1]] || ''; 54 55 if(!map[result[1]]) map[result[1]] = []; 56 map[result[1]].push({ 57 type: 'text', 58 node: n 59 }); 60 } 61 } 62 63 if(attrs) { 64 //這裏暫時只處理屬性和值兩種狀況,多了就複雜10倍了 65 for (i = 0, len = attrs.length; i < len; i++) { 66 attr = attrs[i]; 67 result = reg.exec(attr.value); 68 69 n.setAttribute(attr.name, attr.value); 70 //若是有node須要處理則須要存下來標誌 71 if (result) { 72 n.setAttribute(attr.name, this.data[result[1]] || ''); 73 74 //存儲全部會用到的節點,以便後面動態更新 75 if (!map[result[1]]) map[result[1]] = []; 76 map[result[1]].push({ 77 type: 'attr', 78 name: attr.name, 79 node: n 80 }); 81 82 } 83 } 84 } 85 86 return { 87 node: n, 88 map: map 89 } 90 91 } 92 93 //遍歷一個節點的全部子節點,若是有子節點繼續遍歷到沒有爲止 94 _runAllNode(node, map, root) { 95 96 let nodeInfo = this._handlerNode(node); 97 let _map = nodeInfo.map; 98 let n = nodeInfo.node; 99 let k, i, len, children = node.childNodes; 100 101 //先將該根節點插入到上一個節點中 102 root.appendChild(n); 103 104 //處理map數據,這裏的map是根對象,最初的map 105 for(k in _map) { 106 if(map[k]) { 107 map[k].push(_map[k]); 108 } else { 109 map[k] = _map[k]; 110 } 111 } 112 113 for(i = 0, len = children.length; i < len; i++) { 114 this._runAllNode(children[i], map, n); 115 } 116 117 } 118 119 //處理每一個節點,翻譯爲頁面識別的節點,而且將須要操做的節點記錄 120 splitTemplate () { 121 let nodes = $(this.template); 122 let map = {}, root = document.createElement('div'); 123 let i, len; 124 125 for(i = 0, len = nodes.length; i < len; i++) { 126 this._runAllNode(nodes[i], map, root); 127 } 128 129 window.map = map; 130 return root 131 } 132 133 //拆分目標造成node,這個方法過長,真實項目須要拆分 134 splitTemplate1 () { 135 let template = this.template; 136 let node = $(this.template)[0]; 137 let map = {}, n, name, root = document.createElement('div'); 138 let isEnd = false, index = 0, result; 139 140 let attrs, i, len, attr; 141 let reg = /\{\{([\s\S]+?)\}\}/; 142 143 window.map = map; 144 145 //開始遍歷節點,處理 146 while (!isEnd) { 147 name = node.localName; 148 attrs = node.attributes; 149 value = node.nodeValue; 150 n = document.createElement(this.labelMap[name] || name); 151 152 //說明是文本,須要記錄下來了 153 if(node.nodeType === 3) { 154 n.innerText = this.data[value] || ''; 155 156 result = reg.exec(value); 157 if(result) { 158 n.innerText = this.data[value] || ''; 159 160 if(!map[value]) map[value] = []; 161 map[value].push({ 162 type: 'text', 163 node: n 164 }); 165 } 166 } 167 168 //這裏暫時只處理屬性和值兩種狀況,多了就複雜10倍了 169 for(i = 0, len = attrs.length; i < len; i++) { 170 attr = attrs[i]; 171 result = reg.exec(attr.value); 172 173 n.setAttribute(attr.name, attr.value); 174 //若是有node須要處理則須要存下來標誌 175 if(result) { 176 n.setAttribute(attr.name, this.data[result[1]] || ''); 177 178 //存儲全部會用到的節點,以便後面動態更新 179 if(!map[result[1]]) map[result[1]] = []; 180 map[result[1]].push({ 181 type: 'attr', 182 name: attr.name, 183 node: n 184 }); 185 186 } 187 } 188 189 debugger 190 191 if(index === 0) root.appendChild(n); 192 isEnd = true; 193 index++; 194 195 } 196 197 return root; 198 199 200 console.log(node) 201 } 202 203 } 204 205 let view = new View(); 206 207 document.body.appendChild(window.node) 208 209 </script> 210 </body> 211 </html>
模擬核心代碼
這段代碼,很是簡單:
① 設置了一段模板,甚至,咱們這裏根本不關係其格式化狀態,直接寫成一行方便處理
this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';
② 而後咱們將這段模板轉爲node節點(這裏能夠不用zepto,可是模擬實現怎麼簡單怎麼來吧),而後遍歷處理全部節點,咱們就能夠處理咱們的數據了,最終造成了這個html:
1 <div><div><span>ffsd</span></div><div class="ddd" is-show="pageshow"><span>pageshow</span><div class="c1"><span>pageData</span></div></div></div>
③ 與此同時,咱們存儲了一個對象,這個對象包含全部與之相關的節點:
這個對象是全部setData會影響到node的一個映射表,後面調用setData的時候,即可以直接操做對應的數據了,介於此咱們簡單的模擬便先到此結束,這裏結束的比較倉促有一些緣由:
① 這段代碼能夠是最終打包構建造成的代碼,可是我這裏的完成度只有百分之一,後續須要大量的構建相關介入
② 這篇文章目的仍是接受開發基礎,而本章模擬實現太過複雜,若是篇幅大了會主旨不清
③ 這個是最重要的點,我一時也寫不出來啊!!!,因此各位等下個長篇,小程序前端框架模擬實現吧
因此咱們繼續下章吧......
小程序的Page類是這樣寫的:
1 Page({ 2 data: { 3 pageData: '頁面數據' 4 }, 5 onLoad: function () { 6 console.log('onLoad') 7 }, 8 })
傳入的是一個對象,顯然,咱們爲了更好的拆分頁面邏輯,前面咱們介紹了小程序是採用組件化開發的方式,這裏的說法能夠更進一步,小程序是採用標籤化的方式開發,而標籤對應的控制器js只會改變數據影響標籤顯示,因此某種程度小程序開發的特色是:先標籤後js,咱們構建一個頁面,首先就應該思考這個頁面有哪些標籤,哪些標籤是公共的標籤,而後設計好標籤再作實現。
好比咱們一個頁面中有比較複雜的日曆相關模塊,事實上這個日曆模塊也就是在操做日曆標籤的數據以及設置點擊回調,那麼咱們就須要將頁面分開
好比這裏的業務日曆模塊僅僅是index的一部分(其餘頁面也可能用獲得),因此咱們實現了一個頁面共用的記錄,便與咱們更好的分拆頁面:
1 class Page { 2 constructor(opts) { 3 //用於基礎page存儲各類默認ui屬性 4 this.isLoadingShow = 'none'; 5 this.isToastShow = 'none'; 6 this.isMessageShow = 'none'; 7 8 this.toastMessage = 'toast提示'; 9 10 this.alertTitle = ''; 11 this.alertMessage = 'alertMessage'; 12 this.alertBtn = []; 13 14 //通用方法列表配置,暫時約定用於點擊 15 this.methodSet = [ 16 'onToastHide', 17 'showToast', 18 'hideToast', 19 'showLoading', 20 'hideLoading', 21 'onAlertBtnTap', 22 'showMessage', 23 'hideMessage' 24 ]; 25 26 //當前page對象 27 this.page = null; 28 } 29 //產出頁面組件須要的參數 30 getPageData() { 31 return { 32 isMessageShow: this.isMessageShow, 33 alertTitle: this.alertTitle, 34 alertMessage: this.alertMessage, 35 alertBtn: this.alertBtn, 36 37 isLoadingShow: this.isLoadingShow, 38 isToastShow: this.isToastShow, 39 toastMessage: this.toastMessage 40 41 } 42 } 43 44 //pageData爲頁面級別數據,mod爲模塊數據,要求必定不能重複 45 initPage(pageData, mod) { 46 //debugger; 47 let _pageData = {}; 48 let key, value, k, v; 49 50 //爲頁面動態添加操做組件的方法 51 Object.assign(_pageData, this.getPageFuncs(), pageData); 52 53 //生成真實的頁面數據 54 _pageData.data = {}; 55 Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); 56 57 for( key in mod) { 58 value = mod[key]; 59 for(k in value) { 60 v = value[k]; 61 if(k === 'data') { 62 Object.assign(_pageData.data, v); 63 } else { 64 _pageData[k] = v; 65 } 66 } 67 } 68 69 console.log(_pageData); 70 return _pageData; 71 } 72 onAlertBtnTap(e) { 73 let type = e.detail.target.dataset.type; 74 if (type === 'default') { 75 this.hideMessage(); 76 } else if (type === 'ok') { 77 if (this.alertOkCallback) this.alertOkCallback.call(this); 78 } else if (type == 'cancel') { 79 if (this.alertCancelCallback) this.alertCancelCallback.call(this); 80 } 81 } 82 showMessage(msg) { 83 let alertBtn = [{ 84 type: 'default', 85 name: '知道了' 86 }]; 87 let message = msg; 88 this.alertOkCallback = null; 89 this.alertCancelCallback = null; 90 91 if (typeof msg === 'object') { 92 message = msg.message; 93 alertBtn = []; 94 msg.cancel.type = 'cancel'; 95 msg.ok.type = 'ok'; 96 97 alertBtn.push(msg.cancel); 98 alertBtn.push(msg.ok); 99 this.alertOkCallback = msg.ok.callback; 100 this.alertCancelCallback = msg.cancel.callback; 101 } 102 103 this.setData({ 104 alertBtn: alertBtn, 105 isMessageShow: '', 106 alertMessage: message 107 }); 108 } 109 hideMessage() { 110 this.setData({ 111 isMessageShow: 'none', 112 }); 113 } 114 //當關閉toast時觸發的事件 115 onToastHide(e) { 116 this.hideToast(); 117 } 118 //設置頁面可能使用的方法 119 getPageFuncs() { 120 let funcs = {}; 121 for (let i = 0, len = this.methodSet.length; i < len; i++) { 122 funcs[this.methodSet[i]] = this[this.methodSet[i]]; 123 } 124 return funcs; 125 } 126 127 showToast(message, callback) { 128 this.toastHideCallback = null; 129 if (callback) this.toastHideCallback = callback; 130 let scope = this; 131 this.setData({ 132 isToastShow: '', 133 toastMessage: message 134 }); 135 136 // 3秒後關閉loading 137 setTimeout(function() { 138 scope.hideToast(); 139 }, 3000); 140 } 141 hideToast() { 142 this.setData({ 143 isToastShow: 'none' 144 }); 145 if (this.toastHideCallback) this.toastHideCallback.call(this); 146 } 147 //須要傳入page實例 148 showLoading() { 149 this.setData({ 150 isLoadingShow: '' 151 }); 152 } 153 //關閉loading 154 hideLoading() { 155 this.setData({ 156 isLoadingShow: 'none' 157 }); 158 } 159 } 160 //直接返回一個UI工具了類的實例 161 module.exports = new Page
全部page頁面基類
其中頁面會用到的一塊核心就是:
1 //pageData爲頁面級別數據,mod爲模塊數據,要求必定不能重複 2 initPage(pageData, mod) { 3 //debugger; 4 let _pageData = {}; 5 let key, value, k, v; 6 7 //爲頁面動態添加操做組件的方法 8 Object.assign(_pageData, this.getPageFuncs(), pageData); 9 10 //生成真實的頁面數據 11 _pageData.data = {}; 12 Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); 13 14 for( key in mod) { 15 value = mod[key]; 16 for(k in value) { 17 v = value[k]; 18 if(k === 'data') { 19 Object.assign(_pageData.data, v); 20 } else { 21 _pageData[k] = v; 22 } 23 } 24 } 25 26 console.log(_pageData); 27 return _pageData; 28 }
調用方式是:
1 Page(_page.initPage({ 2 data: { 3 sss: 'sss' 4 }, 5 // methods: uiUtil.getPageMethods(), 6 methods: { 7 }, 8 goList: function () { 9 if(!this.data.cityStartId) { 10 this.showToast('請選擇出發城市'); 11 return; 12 } 13 if(!this.data.cityArriveId) { 14 this.showToast('請選擇到達城市'); 15 return; 16 } 17 18 wx.navigateTo({ 19 }) 20 21 } 22 }, { 23 modCalendar: modCalendar, 24 modCity: modCity 25 }))
能夠看到,其餘組件,如這裏的日曆模塊只是一個對象而已:
1 module.exports = { 2 showCalendar: function () { 3 this.setData({ 4 isCalendarShow: '' 5 }); 6 }, 7 hideCalendar: function () { 8 this.setData({ 9 isCalendarShow: 'none' 10 }); 11 }, 12 preMonth: function () { 13 14 this.setData({ 15 calendarDisplayTime: util.dateUtil.preMonth(this.data.calendarDisplayTime).toString() 16 }); 17 }, 18 nextMonth: function () { 19 this.setData({ 20 calendarDisplayTime: util.dateUtil.nextMonth(this.data.calendarDisplayTime).toString() 21 }); 22 }, 23 onCalendarDayTap: function (e) { 24 let data = e.detail; 25 var date = new Date(data.year, data.month, data.day); 26 console.log(date) 27 28 //留下一個鉤子函數 29 if(this.calendarHook) this.calendarHook(date); 30 this.setData({ 31 isCalendarShow: 'none', 32 calendarSelectedDate: date.toString(), 33 calendarSelectedDateStr: util.dateUtil.format(date, 'Y年M月D日') 34 }); 35 }, 36 onContainerHide: function () { 37 this.hideCalendar(); 38 }, 39 40 data: { 41 isCalendarShow: 'none', 42 calendarDisplayMonthNum: 1, 43 calendarDisplayTime: selectedDate, 44 calendarSelectedDate: selectedDate, 45 calendarSelectedDateStr: util.dateUtil.format(new Date(selectedDate), 'Y年M月D日') 46 } 47 }
可是在代碼層面卻幫咱們作到了更好的封裝,這個基類裏面還包括咱們自定義的經常使用組件,loading、toast等等:
page是最值得封裝的部分,這裏是基本page的封裝,事實上,列表頁是經常使用的一種業務頁面,雖然各類列表頁的篩選條件不同,可是主體功能無非都是:
① 列表渲染
② 滾動加載
③ 條件篩選、從新渲染
因此說咱們其實能夠將其作成一個頁面基類,跟abstract-page一個意思,這裏留待咱們下次來處理吧
請你們對着github中的代碼調試閱讀這裏
前面已經說了,小程序的開發重點是一個個的標籤的實現,咱們這裏將業務組件設置成了一個個mod,UI組件設置成了真正的標籤,好比咱們頁面會有不少非業務類的UI組件:
① alert類彈出層
② loading類彈出層
③ 日曆組件
④ toast&message類提示彈出組件
⑤ 容器類組件
⑥ ......
這些均可以咱們本身去實現,可是微信其實提供給咱們了系統級別的組件:
這裏要不要用就看實際業務需求了,通常來講仍是建議用的,咱們這裏爲了幫助各位更好的瞭解小程序組件,特別實現了一個較爲複雜,而小程序又沒有提供的組件日曆組件,首先咱們這裏先創建一個日曆組件目錄:
其次咱們這裏先作最簡單實現:
1 let View = require('behavior-view'); 2 const util = require('../utils/util.js'); 3 4 // const dateUtil = util.dateUtil; 5 6 Component({ 7 behaviors: [ 8 View 9 ], 10 properties: { 11 12 }, 13 data: { 14 weekDayArr: ['日', '一', '二', '三', '四', '五', '六'], 15 displayMonthNum: 1, 16 17 //當前顯示的時間 18 displayTime: null, 19 //能夠選擇的最先時間 20 startTime: null, 21 //最晚時間 22 endTime: null, 23 24 //當前時間,有時候是讀取服務器端 25 curTime: new Date() 26 27 }, 28 29 attached: function () { 30 //console.log(this) 31 }, 32 methods: { 33 34 } 35 })
ui-calendar
1 <wxs module="dateUtil"> 2 var isDate = function(date) { 3 return date && date.getMonth; 4 }; 5 6 var isLeapYear = function(year) { 7 //傳入爲時間格式須要處理 8 if (isDate(year)) year = year.getFullYear() 9 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 10 return false; 11 }; 12 13 var getDaysOfMonth = function(date) { 14 var month = date.getMonth(); //注意此處月份要加1,因此咱們要減一 15 var year = date.getFullYear(); 16 return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 17 } 18 19 var getBeginDayOfMouth = function(date) { 20 var month = date.getMonth(); 21 var year = date.getFullYear(); 22 var d = getDate(year, month, 1); 23 return d.getDay(); 24 } 25 26 var getDisplayInfo = function(date) { 27 if (!isDate(date)) { 28 date = getDate(date) 29 } 30 var year = date.getFullYear(); 31 32 var month = date.getMonth(); 33 var d = getDate(year, month); 34 35 //這個月一共多少天 36 var days = getDaysOfMonth(d); 37 38 //這個月是星期幾開始的 39 var beginWeek = getBeginDayOfMouth(d); 40 41 /* 42 console.log('info',JSON.stringify( { 43 year: year, 44 month: month, 45 days: days, 46 beginWeek: beginWeek 47 })); 48 */ 49 50 return { 51 year: year, 52 month: month, 53 days: days, 54 beginWeek: beginWeek 55 } 56 } 57 58 module.exports = { 59 getDipalyInfo: getDisplayInfo 60 } 61 </wxs> 62 63 64 <view class="cm-calendar"> 65 <view class="cm-calendar-hd "> 66 <block wx:for="{{weekDayArr}}"> 67 <view class="item">{{item}}</view> 68 </block> 69 </view> 70 <view class="cm-calendar-bd "> 71 <view class="cm-month "> 72 </view> 73 <view class="cm-day-list"> 74 75 <block wx:for="{{dateUtil.getDipalyInfo(curTime).days + dateUtil.getDipalyInfo(curTime).beginWeek}}" wx:for-index="index"> 76 77 <view wx:if="{{index < dateUtil.getDipalyInfo(curTime).beginWeek }}" class="item active"></view> 78 <view wx:else class="item">{{index + 1 - dateUtil.getDipalyInfo(curTime).beginWeek}}</view> 79 80 </block> 81 82 <view class=" active cm-item--disabled " data-cndate="" data-date=""> 83 84 </view> 85 </view> 86 </view> 87 </view>
日曆結構部分代碼
這個是很是簡陋的日曆雛形,在代碼過程當中有如下幾點比較痛苦:
① WXML與js間應該只有數據傳遞,根本不能傳遞方法,應該是兩個webview的通訊,而日曆組件這裏在WXML層由不得不寫一點邏輯
② 原本在WXML中寫邏輯已經很是費勁了,而咱們引入的WXS,使用與HTML中的js片斷也有很大的不一樣,主要體如今日期操做
這些問題,一度讓代碼變得複雜,而能夠看到一個簡單的組件,尚未複雜功能,涉及到的文件都太多了,這裏頁面調用層引入標籤後:
<ui-calendar is-show="" ></ui-calendar>
日曆的基本頁面就出來了:
這個日曆組件應該是在小程序中寫的最複雜的組件了,尤爲是不少邏輯判斷的代碼都放在了WXML裏面,根據以前的瞭解,小程序渲染在一個webview中,js邏輯在一個webview中,他這樣作的目的多是想讓性能更好,這種UI組件使用的方式通常是直接使用,可是若是涉及到了頁面業務,便須要獨立出一個mod小模塊去操做對應組件的數據,如圖咱們這裏的日曆組件通常
<import src="./mod.searchbox.wxml" /> <view> <template is="searchbox" /> </view> <include src="./mod/calendar.wxml"/> <include src="../../utils/abstract-page.wxml"/>
1 /* 2 事實上一個mod就只是一個對象,只不過爲了方便拆分,將對象分拆成一個個的mod 3 一個mod對應一個wxml,可是共享外部的css,暫時如此設計 4 全部日曆模塊的需求所有再此實現 5 */ 6 module.exports = { 7 q: 1, 8 ddd: function(){}, 9 10 data: { 11 isCalendarShow: '', 12 CalendarDisplayMonthNum: 2, 13 CalendarDisplayTime: new Date(), 14 CalendarSelectedDate: null 15 } 16 }
因而代碼便很是好拆分了,這裏請各位對比着github中的代碼閱讀,最終使用效果:
小程序使用這個接口請求數據,這裏須要設置域名白名單:
wx.request(OBJECT)
能夠看到數據請求已經回來了,可是咱們通常來講一個接口不止會用於一個地方,每次從新寫好像有些費事,加之我這裏想將重複的請求緩存起來,因此咱們這裏封裝一套數據訪問層出來
以前在瀏覽器中,咱們通常使用localstorage存儲一些不太更改的數據,微信裏面提供了接口處理這一切:
wx.setStorage(OBJECT)
咱們這裏須要對其進行簡單封裝,便與後面更好的使用,通常來講有緩存就必定要有過時,因此咱們動態給每一個緩存對象增長一個過時時間:
1 class Store { 2 constructor(opts) { 3 if(typeof opts === 'string') this.key = opts; 4 else Object.assign(this, opts); 5 6 //若是沒有傳過時時間,則默認30分鐘 7 if(!this.lifeTime) this.lifeTime = 1; 8 9 //本地緩存用以存放全部localstorage鍵值與過時日期的映射 10 this._keyCache = 'SYSTEM_KEY_TIMEOUT_MAP'; 11 12 } 13 //獲取過時時間,單位爲毫秒 14 _getDeadline() { 15 return this.lifeTime * 60 * 1000; 16 } 17 18 //獲取一個數據緩存對象,存能夠異步,獲取我同步便可 19 get(sign){ 20 let key = this.key; 21 let now = new Date().getTime(); 22 var data = wx.getStorageSync(key); 23 if(!data) return null; 24 data = JSON.parse(data); 25 //數據過時 26 if (data.deadLine < now) { 27 this.removeOverdueCache(); 28 return null; 29 } 30 31 if(data.sign) { 32 if(sign === data.sign) return data.data; 33 else return null; 34 } 35 return null; 36 } 37 38 /*產出頁面組件須要的參數 39 sign 爲格式化後的請求參數,用於同一請求不一樣參數時候返回新數據,好比列表爲北京的城市,後切換爲上海,會判斷tag不一樣而更新緩存數據,tag至關於簽名 40 每一鍵值只會緩存一條信息 41 */ 42 set(data, sign) { 43 let timeout = new Date(); 44 let time = timeout.setTime(timeout.getTime() + this._getDeadline()); 45 this._saveData(data, time, sign); 46 } 47 _saveData(data, time, sign) { 48 let key = this.key; 49 let entity = { 50 deadLine: time, 51 data: data, 52 sign: sign 53 }; 54 let scope = this; 55 56 wx.setStorage({ 57 key: key, 58 data: JSON.stringify(entity), 59 success: function () { 60 //每次真實存入前,須要往系統中存儲一個清單 61 scope._saveSysList(key, entity.deadLine); 62 } 63 }); 64 } 65 _saveSysList(key, timeout) { 66 if (!key || !timeout || timeout < new Date().getTime()) return; 67 let keyCache = this._keyCache; 68 wx.getStorage({ 69 key: keyCache, 70 complete: function (data) { 71 let oldData = {}; 72 if(data.data) oldData = JSON.parse(data.data); 73 oldData[key] = timeout; 74 wx.setStorage({ 75 key: keyCache, 76 data: JSON.stringify(oldData) 77 }); 78 } 79 }); 80 } 81 //刪除過時緩存 82 removeOverdueCache() { 83 let now = new Date().getTime(); 84 let keyCache = this._keyCache; 85 wx.getStorage({ 86 key: keyCache, 87 success: function (data) { 88 if(data && data.data) data = JSON.parse(data.data); 89 for(let k in data) { 90 if(data[k] < now) { 91 delete data[k]; 92 wx.removeStorage({key: k, success: function(){}}); 93 } 94 } 95 wx.setStorage({ 96 key: keyCache, 97 data: JSON.stringify(data) 98 }); 99 } 100 }); 101 } 102 103 } 104 105 module.exports = Store
緩存層核心代碼
這個類的使用也很是簡單,這裏舉個例子:
1 sss = new global.Store({key: 'qqq', lifeTime: 1}) 2 sss.set({a: 1}, 2) 3 sss.get()//由於沒有祕鑰會是null 4 sss.get(2)//sss.get(2)
這個時候咱們開始寫咱們數據請求的類:
首先仍是實現了一個抽象類和一個業務基類,而後開始在業務層請求數據:
1 class Model { 2 constructor() { 3 this.url = ''; 4 this.param = {}; 5 this.validates = []; 6 } 7 pushValidates(handler) { 8 if (typeof handler === 'function') { 9 this.validates.push(handler); 10 } 11 } 12 setParam(key, val) { 13 if (typeof key === 'object') { 14 Object.assign(this.param, key); 15 } else { 16 this.param[key] = val; 17 } 18 } 19 //@override 20 buildurl() { 21 return this.url; 22 } 23 onDataSuccess() { 24 } 25 //執行數據請求邏輯 26 execute(onComplete) { 27 let scope = this; 28 let _success = function(data) { 29 let _data = data; 30 if (typeof data == 'string') _data = JSON.parse(data); 31 32 // @description 開發者能夠傳入一組驗證方法進行驗證 33 for (let i = 0, len = scope.validates.length; i < len; i++) { 34 if (!scope.validates[i](data)) { 35 // @description 若是一個驗證不經過就返回 36 if (typeof onError === 'function') { 37 return onError.call(scope || this, _data, data); 38 } else { 39 return false; 40 } 41 } 42 } 43 44 // @description 對獲取的數據作字段映射 45 let datamodel = typeof scope.dataformat === 'function' ? scope.dataformat(_data) : _data; 46 47 if (scope.onDataSuccess) scope.onDataSuccess.call(scope, datamodel, data); 48 if (typeof onComplete === 'function') { 49 onComplete.call(scope, datamodel, data); 50 } 51 }; 52 this._sendRequest(_success); 53 } 54 55 //刪除過時緩存 56 _sendRequest(callback) { 57 let url = this.buildurl(); 58 wx.request({ 59 url: this.buildurl(), 60 data: this.param, 61 success: function success(data) { 62 callback && callback(data); 63 } 64 }); 65 } 66 } 67 module.exports = Model
數據請求核心類
這裏是業務基類的使用辦法:
1 let Model = require('./abstract-model.js'); 2 3 class DemoModel extends Model { 4 constructor() { 5 super(); 6 let scope = this; 7 this.domain = 'https://apikuai.baidu.com'; 8 this.param = { 9 head: { 10 version: '1.0.1', 11 ct: 'ios' 12 } 13 }; 14 15 //若是須要緩存,能夠在此設置緩存對象 16 this.cacheData = null; 17 18 this.pushValidates(function(data) { 19 return scope._baseDataValidate(data); 20 }); 21 } 22 23 //首輪處理返回數據,檢查錯誤碼作統一驗證處理 24 _baseDataValidate(data) { 25 if (typeof data === 'string') data = JSON.parse(data); 26 if (data.data) data = data.data; 27 if (data.errno === 0) return true; 28 return false; 29 } 30 31 dataformat(data) { 32 if (typeof data === 'string') data = JSON.parse(data); 33 if (data.data) data = data.data; 34 if (data.data) data = data.data; 35 return data; 36 } 37 38 buildurl() { 39 return this.domain + this.url; 40 } 41 42 getSign() { 43 let param = this.getParam() || {}; 44 return JSON.stringify(param); 45 } 46 onDataSuccess(fdata, data) { 47 if (this.cacheData && this.cacheData.set) 48 this.cacheData.set(fdata, this.getSign()); 49 } 50 51 //若是有緩存直接讀取緩存,沒有才請求 52 execute(onComplete, ajaxOnly) { 53 let data = null; 54 if (!ajaxOnly && this.cacheData && this.cacheData.get) { 55 data = this.cacheData.get(this.getSign()); 56 if (data) { 57 onComplete(data); 58 return; 59 } 60 } 61 super.execute(onComplete); 62 } 63 64 } 65 66 class CityModel extends DemoModel { 67 constructor() { 68 super(); 69 this.url = '/city/getstartcitys'; 70 } 71 } 72 73 module.exports = { 74 cityModel: new CityModel 75 76 }
業務請求基類
接下來是實際調用代碼:
1 let model = models.cityModel; 2 model.setParam({ 3 type: 1 4 }); 5 model.execute(function(data) { 6 console.log(data); 7 debugger; 8 });
數據便請求結束了,有了這個類咱們能夠作很是多的工做,好比:
① 前端設置統一的錯誤碼處理邏輯
② 前端打點,統計全部的接口響應狀態
③ 每次請求相同參數作數據緩存
④ 這個對於錯誤處理很關鍵,通常來講前端出錯很大可能都是後端數據接口字段有變化,而這種錯誤是比較難尋找的,若是我這裏作一個統一的收口,每次數據返回記錄全部的返回字段的標誌上報呢,就以這個城市數據爲例,咱們能夠這樣作:
1 class CityModel extends DemoModel { 2 constructor() { 3 super(); 4 this.url = '/city/getstartcitys'; 5 } 6 //每次數據訪問成功,錯誤碼爲0時皆會執行這個回調 7 onDataSuccess(fdata, data) { 8 super.onDataSuccess(fdata, data); 9 //開始執行自我邏輯 10 let o = { 11 _indate: new Date().getTime() 12 }; 13 for(let k in fdata) { 14 o[k] = typeof fdata[k]; 15 } 16 //執行數據上報邏輯 17 console.log(JSON.stringify(o)); 18 } 19 }
這裏就會輸出如下信息:
{"_indate":1533436847778,"cities":"object","hots":"object","total":"number","page":"string"}
若是對數據要求很是嚴苛,對某些接口作到字段層面的驗證,那麼加一個Validates驗證便可,這樣對接口的控制會最大化,就算哪次出問題,也能很好從數據分析系統之中能夠查看到問題所在,若是我如今想要一個更爲具體的功能呢?我想要首次請求一個接口時便將其數據記錄下來,第二次便再也不請求呢,這個時候咱們以前設計的數據持久層便派上了用處:
1 let Store = require('./abstract-store.js'); 2 3 class CityStore extends Store { 4 constructor() { 5 super(); 6 this.key = 'DEMO_CITYLIST'; 7 //30分鐘過時時間 8 this.lifeTime = 30; 9 } 10 } 11 12 module.exports = { 13 cityStore: new CityStore 14 }
1 class CityModel extends DemoModel { 2 constructor() { 3 super(); 4 this.url = '/city/getstartcitys'; 5 this.cacheData = Stores.cityStore; 6 } 7 //每次數據訪問成功,錯誤碼爲0時皆會執行這個回調 8 onDataSuccess(fdata, data) { 9 super.onDataSuccess(fdata, data); 10 //開始執行自我邏輯 11 let o = { 12 _indate: new Date().getTime() 13 }; 14 for(let k in fdata) { 15 o[k] = typeof fdata[k]; 16 } 17 //執行數據上報邏輯 18 console.log(JSON.stringify(o)); 19 } 20 }
這個時候第二次請求時候便會直接讀取緩存了
若是讀到這裏,我相信你們應該清楚了,30分鐘固然是騙人的啦。。。。。。別說三十分鐘了,三個小時這些東西都讀不完,對於初學者的同窗建議把代碼下載下來一邊調試一邊對着這裏的文章作思考,這樣3天左右即可以吸取不少微信小程序的知識
寫這篇文章說實話還比較辛苦,近期小釵這邊工做繁忙,有幾段都是在和老闆開會的時候偷偷寫的......,因此各位若是以爲文章還行麻煩幫忙點個贊
總結起來基本仍是那句話,微信小程序從架構工程層面十分值得學習,而我這邊不出意外時間容許會深刻的探索前端框架的實現,爭取實現一套能兼容小程序和web同時運行的代碼
咱們實際工做中會直接使用上面的代碼,也會使用一些比較成熟的框架好比:https://tencent.github.io/wepy/,用什麼,怎麼作單看本身團隊項目的需求
咱們在學校過程當中作了一個實際的項目,完成度有60%,實際工做中便只須要完善細節便可,我這裏便沒有再增強,一來是時間不足,二來是純粹業務代碼只會讓學習的代碼變得複雜,沒什麼太大的必要,但願對初學者有必定幫助: