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