前言
我們之前對小程序做了基本學習:
- 1. 微信小程序開發07-列表頁面怎么做
- 2. 微信小程序開發06-一個業務頁面的完成
- 3. 微信小程序開發05-日歷組件的實現
- 4. 微信小程序開發04-打造自己的UI庫
- 5. 微信小程序開發03-這是一個組件
- 6. 微信小程序開發02-小程序基本介紹
- 7. 微信小程序開發01-小程序的執行流程是怎么樣的?
閱讀本文之前,如果大家想對小程序有更深入的了解,或者一些細節的了解可以先閱讀上述文章,本文后面點需要對着代碼調試閱讀
對應的github地址是:https://github.com/yexiaochai/wxdemo
首先我們來一言以蔽之,什么是微信小程序?PS:這個問題問得好像有些扯:)
小程序是一個不需要下載安裝就可使用的應用,它實現了應用觸手可及的夢想,用戶掃一掃或者搜一下即可打開應用。也體現了用完即走的理念,用戶不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝卸載。從字面上看小程序具有類似Web應用的熱部署能力,在功能上又接近於原生APP。
所以說,其實微信小程序是一套超級Hybrid的解決方案,現在看來,小程序應該是應用場景最廣,也最為復雜的解決方案了。
很多公司都會有自己的Hybrid平台,我這里了解到比較不錯的是攜程的Hybrid平台、阿里的Weex、百度的糯米,但是從應用場景來說都沒有微信來得豐富,這里根本的區別是:
微信小程序是給各個公司開發者接入的,其他公司平台多是給自己業務團隊使用,這一根本區別,就造就了我們看到的很多小程序不一樣的特性:
① 小程序定義了自己的標簽語言WXML
② 小程序定義了自己的樣式語言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層。
小程序打包后的結構(這里就真的不懂了,引用:小程序底層框架實現原理解析):
所有的小程序基本都最后都被打成上面的結構
1、WAService.js 框架JS庫,提供邏輯層基礎的API能力
2、WAWebview.js 框架JS庫,提供視圖層基礎的API能力
3、WAConsole.js 框架JS庫,控制台
4、app-config.js 小程序完整的配置,包含我們通過app.json里的所有配置,綜合了默認配置型
5、app-service.js 我們自己的JS代碼,全部打包到這個文件
6、page-frame.html 小程序視圖的模板文件,所有的頁面都使用此加載渲染,且所有的WXML都拆解為JS實現打包到這里
7、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的時候,便可以直接操作對應的數據了,這里我們分拆我們代碼,形成了幾個關鍵部分,首先是View類,這個對應我們的模板,是核心類:

1 //View為模塊的實現,主要用於解析目標生產node 2 class View { 3 constructor(template) { 4 this.template = template; 5 6 //由控制器page傳入的初始數據或者setData產生的數據 7 this.data = {}; 8 9 this.labelMap = { 10 'view': 'div', 11 '#text': 'span' 12 }; 13 14 this.nodes = {}; 15 this.root = {}; 16 } 17 18 setInitData(data) { 19 this.data = data; 20 } 21 22 //數據便會引起的重新渲染 23 reRender(data, allData) { 24 this.data = allData; 25 let k, v, i, len, j, len2, v2; 26 27 //開始重新渲染邏輯,尋找所有保存了的node 28 for(k in data) { 29 if(!this.nodes[k]) continue; 30 for(i = 0, len = this.nodes[k].length; i < len; i++) { 31 for(j = 0; j < this.nodes[k][i].length; j++) { 32 v = this.nodes[k][i][j]; 33 if(v.type === 'text') { 34 v.node.innerText = data[k]; 35 } else if(v.type === 'attr') { 36 v.node.setAttribute(v.name, data[k]); 37 } 38 } 39 } 40 } 41 } 42 /* 43 傳入一個節點,解析出一個節點,並且將節點中的數據以初始化數據改變 44 並且將其中包含{{}}標志的節點信息記錄下來 45 */ 46 _handlerNode (node) { 47 48 let reg = /\{\{([\s\S]+?)\}\}/; 49 let result, name, value, n, map = {}; 50 let attrs , i, len, attr; 51 52 name = node.nodeName; 53 attrs = node.attributes; 54 value = node.nodeValue; 55 n = document.createElement(this.labelMap[name.toLowerCase()] || name); 56 57 //說明是文本,需要記錄下來了 58 if(node.nodeType === 3) { 59 n.innerText = this.data[value] || ''; 60 61 result = reg.exec(value); 62 if(result) { 63 n.innerText = this.data[result[1]] || ''; 64 65 if(!map[result[1]]) map[result[1]] = []; 66 map[result[1]].push({ 67 type: 'text', 68 node: n 69 }); 70 } 71 } 72 73 if(attrs) { 74 //這里暫時只處理屬性和值兩種情況,多了就復雜10倍了 75 for (i = 0, len = attrs.length; i < len; i++) { 76 attr = attrs[i]; 77 result = reg.exec(attr.value); 78 79 n.setAttribute(attr.name, attr.value); 80 //如果有node需要處理則需要存下來標志 81 if (result) { 82 n.setAttribute(attr.name, this.data[result[1]] || ''); 83 84 //存儲所有會用到的節點,以便后面動態更新 85 if (!map[result[1]]) map[result[1]] = []; 86 map[result[1]].push({ 87 type: 'attr', 88 name: attr.name, 89 node: n 90 }); 91 92 } 93 } 94 } 95 96 return { 97 node: n, 98 map: map 99 } 100 101 } 102 103 //遍歷一個節點的所有子節點,如果有子節點繼續遍歷到沒有為止 104 _runAllNode(node, map, root) { 105 106 let nodeInfo = this._handlerNode(node); 107 let _map = nodeInfo.map; 108 let n = nodeInfo.node; 109 let k, i, len, children = node.childNodes; 110 111 //先將該根節點插入到上一個節點中 112 root.appendChild(n); 113 114 //處理map數據,這里的map是根對象,最初的map 115 for(k in _map) { 116 if(!map[k]) map[k] = []; 117 map[k].push(_map[k]); 118 } 119 120 for(i = 0, len = children.length; i < len; i++) { 121 this._runAllNode(children[i], map, n); 122 } 123 124 } 125 126 //處理每個節點,翻譯為頁面識別的節點,並且將需要操作的節點記錄 127 splitTemplate () { 128 let nodes = $(this.template); 129 let map = {}, root = document.createElement('div'); 130 let i, len; 131 132 for(i = 0, len = nodes.length; i < len; i++) { 133 this._runAllNode(nodes[i], map, root); 134 } 135 136 this.nodes = map; 137 this.root = root; 138 } 139 140 render() { 141 let i, len; 142 this.splitTemplate(); 143 for(i = 0, len = this.root.childNodes.length; i< len; i++) 144 document.body.appendChild(this.root.childNodes[0]); 145 } 146 147 }
這個類主要完成的工作是:
① 接受傳入的template字符串(直接由index.wxml讀出)
② 解析template模板,生成字符串和兼職與node映射表,方便后期setData導致的改變
③ 渲染和再次渲染工作
然后就是我們的Page類的實現了,這里反而比較簡單(當然這里的實現是不完善的):
1 //這個為js羅傑部分實現,后續會釋放工廠方法 2 class PageClass { 3 //構造函數,傳入對象 4 constructor(opts) { 5 6 //必須擁有的參數 7 this.data = {}; 8 Object.assign(this, opts); 9 } 10 11 //核心方法,每個Page對象需要一個模板實例 12 setView(view) { 13 this.view = view; 14 } 15 16 //核心方法,設置數據后會引發頁面刷新 17 setData(data) { 18 Object.assign(this.data, data); 19 20 //只影響改變的數據 21 this.view.reRender(data, this.data) 22 } 23 24 render() { 25 this.view.setInitData(this.data); 26 this.view.render(); 27 28 if(this.onLoad) this.onLoad(); 29 } 30 31 }
現在輪着我們實際調用方,Page方法出場了:
function Page (data) { let page = new PageClass(data); return page; }
基本上什么都沒有干的感覺,調用層代碼這樣寫:
1 function main() { 2 let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>'); 3 let page = Page({ 4 data: { 5 pageShow: 'pageshow', 6 pageData: 'pageData', 7 pageShow1: 'pageShow1' 8 }, 9 onLoad: function () { 10 this.setData({ 11 pageShow: '我是pageShow啊' 12 }); 13 } 14 }); 15 16 page.setView(view); 17 page.render(); 18 } 19 20 main();
於是,我們可以看到頁面的變化,由開始的初始化頁面到執行onLoad時候的變化:
這里是最終完整的代碼:

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 //這個為js羅傑部分實現,后續會釋放工廠方法 13 class PageClass { 14 //構造函數,傳入對象 15 constructor(opts) { 16 17 //必須擁有的參數 18 this.data = {}; 19 Object.assign(this, opts); 20 } 21 22 //核心方法,每個Page對象需要一個模板實例 23 setView(view) { 24 this.view = view; 25 } 26 27 //核心方法,設置數據后會引發頁面刷新 28 setData(data) { 29 Object.assign(this.data, data); 30 31 //只影響改變的數據 32 this.view.reRender(data, this.data) 33 } 34 35 render() { 36 this.view.setInitData(this.data); 37 this.view.render(); 38 39 if(this.onLoad) this.onLoad(); 40 } 41 42 } 43 44 //View為模塊的實現,主要用於解析目標生產node 45 class View { 46 constructor(template) { 47 this.template = template; 48 49 //由控制器page傳入的初始數據或者setData產生的數據 50 this.data = {}; 51 52 this.labelMap = { 53 'view': 'div', 54 '#text': 'span' 55 }; 56 57 this.nodes = {}; 58 this.root = {}; 59 } 60 61 setInitData(data) { 62 this.data = data; 63 } 64 65 //數據便會引起的重新渲染 66 reRender(data, allData) { 67 this.data = allData; 68 let k, v, i, len, j, len2, v2; 69 70 //開始重新渲染邏輯,尋找所有保存了的node 71 for(k in data) { 72 if(!this.nodes[k]) continue; 73 for(i = 0, len = this.nodes[k].length; i < len; i++) { 74 for(j = 0; j < this.nodes[k][i].length; j++) { 75 v = this.nodes[k][i][j]; 76 if(v.type === 'text') { 77 v.node.innerText = data[k]; 78 } else if(v.type === 'attr') { 79 v.node.setAttribute(v.name, data[k]); 80 } 81 } 82 } 83 } 84 } 85 /* 86 傳入一個節點,解析出一個節點,並且將節點中的數據以初始化數據改變 87 並且將其中包含{{}}標志的節點信息記錄下來 88 */ 89 _handlerNode (node) { 90 91 let reg = /\{\{([\s\S]+?)\}\}/; 92 let result, name, value, n, map = {}; 93 let attrs , i, len, attr; 94 95 name = node.nodeName; 96 attrs = node.attributes; 97 value = node.nodeValue; 98 n = document.createElement(this.labelMap[name.toLowerCase()] || name); 99 100 //說明是文本,需要記錄下來了 101 if(node.nodeType === 3) { 102 n.innerText = this.data[value] || ''; 103 104 result = reg.exec(value); 105 if(result) { 106 n.innerText = this.data[result[1]] || ''; 107 108 if(!map[result[1]]) map[result[1]] = []; 109 map[result[1]].push({ 110 type: 'text', 111 node: n 112 }); 113 } 114 } 115 116 if(attrs) { 117 //這里暫時只處理屬性和值兩種情況,多了就復雜10倍了 118 for (i = 0, len = attrs.length; i < len; i++) { 119 attr = attrs[i]; 120 result = reg.exec(attr.value); 121 122 n.setAttribute(attr.name, attr.value); 123 //如果有node需要處理則需要存下來標志 124 if (result) { 125 n.setAttribute(attr.name, this.data[result[1]] || ''); 126 127 //存儲所有會用到的節點,以便后面動態更新 128 if (!map[result[1]]) map[result[1]] = []; 129 map[result[1]].push({ 130 type: 'attr', 131 name: attr.name, 132 node: n 133 }); 134 135 } 136 } 137 } 138 139 return { 140 node: n, 141 map: map 142 } 143 144 } 145 146 //遍歷一個節點的所有子節點,如果有子節點繼續遍歷到沒有為止 147 _runAllNode(node, map, root) { 148 149 let nodeInfo = this._handlerNode(node); 150 let _map = nodeInfo.map; 151 let n = nodeInfo.node; 152 let k, i, len, children = node.childNodes; 153 154 //先將該根節點插入到上一個節點中 155 root.appendChild(n); 156 157 //處理map數據,這里的map是根對象,最初的map 158 for(k in _map) { 159 if(!map[k]) map[k] = []; 160 map[k].push(_map[k]); 161 } 162 163 for(i = 0, len = children.length; i < len; i++) { 164 this._runAllNode(children[i], map, n); 165 } 166 167 } 168 169 //處理每個節點,翻譯為頁面識別的節點,並且將需要操作的節點記錄 170 splitTemplate () { 171 let nodes = $(this.template); 172 let map = {}, root = document.createElement('div'); 173 let i, len; 174 175 for(i = 0, len = nodes.length; i < len; i++) { 176 this._runAllNode(nodes[i], map, root); 177 } 178 179 this.nodes = map; 180 this.root = root; 181 } 182 183 render() { 184 let i, len; 185 this.splitTemplate(); 186 for(i = 0, len = this.root.childNodes.length; i< len; i++) 187 document.body.appendChild(this.root.childNodes[0]); 188 } 189 190 } 191 192 function Page (data) { 193 let page = new PageClass(data); 194 return page; 195 } 196 197 function main() { 198 let view = new View('<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>'); 199 let page = Page({ 200 data: { 201 pageShow: 'pageshow', 202 pageData: 'pageData', 203 pageShow1: 'pageShow1' 204 }, 205 onLoad: function () { 206 this.setData({ 207 pageShow: '我是pageShow啊' 208 }); 209 } 210 }); 211 212 page.setView(view); 213 page.render(); 214 } 215 216 main(); 217 218 </script> 219 </body> 220 </html>
我們簡單的模擬便先到此結束,這里結束的比較倉促有一些原因:
① 這段代碼可以是最終打包構建形成的代碼,但是我這里的完成度只有百分之一,后續需要大量的構建相關介入
② 這篇文章目的還是接受開發基礎,而本章模擬實現太過復雜,如果篇幅大了會主旨不清
③ 這個是最重要的點,我一時也寫不出來啊!!!,所以各位等下個長篇,小程序前端框架模擬實現吧
④ 如果繼續實現,這里馬上要遇到組件處理、事件模型、分文件構建等高端知識,時間會拉得很長
所以我們繼續下章吧......
小程序中的Page的封裝
小程序的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
其中頁面會用到的一塊核心就是:
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 })

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%,實際工作中便只需要完善細節即可,我這里便沒有再加強,一來是時間不足,二來是純粹業務代碼只會讓學習的代碼變得復雜,沒什么太大的必要,希望對初學者有一定幫助: