天天看點

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

前言

我們之前對小程式做了基本學習:

  • 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會變得更加輕易:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

另一方面,經過之前的學習,我這邊明确可以得出一個感受:

① 小程式的頁面核心是标簽,标簽是不可控制的(我暫時沒用到js操作元素的方法),隻能按照微信給的玩法玩,标簽控制顯示是我們的view

② 标簽的展示隻與data有關聯,和js是隔離的,沒有辦法在标簽中調用js的方法

③ 而我們的js的唯一工作便是根據業務改變data,重新引發頁面渲染,以後别想操作DOM,别想操作Window對象了,改變開發方式,改變開發方式,改變開發方式!

1 this.setData({\'wxml\': `
2   <my-component>
3   <view>動态插入的節點</view> 
4   </my-component>
5 `});      
【微信小程式項目實踐總結】30分鐘從陌生到熟悉

然後可以看到這個是一個MVC模型

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

每個頁面的目錄是這個樣子的:

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元件:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

從我寫業務代碼過程中,覺得整體來說還是比較順暢的,小程式是有自己一套完整的前端架構的,并且釋放給業務代碼的主要就是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等,官方還出了一張圖進行說明:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

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 })      
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉

真實的邏輯是這樣的,全局控制器會完成頁面執行個體化,這個是根據app.json中來的,全部完成執行個體化存儲起來然後選擇第一個page執行個體執行一些邏輯,然後通知view線程,即将執行onLoad事件,因為view線程和業務線程是兩個線程,是以不會造成阻塞,view線程根據初始資料完成渲染,而業務線程繼續後續邏輯,執行onLoad,如果onLoad中有setData,那麼會進入隊列繼續通知view線程更新。

是以我個人感覺微信官網那張圖不太清晰,我這裡重新畫了一個圖:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

再引用一張其他地方的圖:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

模拟實作

都這個時候了,不來個簡單的小程式架構實作好像有點不對,我們做小程式實作的主要原因是想做到一端代碼三端運作:web、小程式、Hybrid甚至Servce端

我們這裡沒有可能實作太複雜的功能,這裡想的是就實作一個基本的頁面展示帶一個最基本的标簽即可,隻做Page一塊的簡單實作,讓大家能了解到小程式可能的實作,以及如何将小程式直接轉為H5的可能走法

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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 })      

我們直接将小程式這些代碼拷貝一份到我們的目錄:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

我們需要做的就是讓這段代碼運作起來,而這裡的目錄是我們最終看見的目錄,真實運作的時候可能不是這個樣,運作之前項目會通過我們的工程建構,變成可以直接運作的代碼,而我這裡思考的可以運作的代碼事實上是一個子產品,是以我們這裡從最終結果反推、分拆到開發結構目錄,我們首先将所有代碼放到index.html,可能是這樣的:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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>      

③ 與此同時,我們存儲了一個對象,這個對象包含所有與之相關的節點:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

這個對象是所有setData會影響到node的一個映射表,後面調用setData的時候,便可以直接操作對應的資料了,這裡我們分拆我們代碼,形成了幾個關鍵部分,首先是View類,這個對應我們的模闆,是核心類:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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 }      

核心模闆處理類View

這個類主要完成的工作是:

① 接受傳入的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時候的變化:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉

這裡是最終完整的代碼:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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>      

View Code

我們簡單的模拟便先到此結束,這裡結束的比較倉促有一些原因:

① 這段代碼可以是最終打包建構形成的代碼,但是我這裡的完成度隻有百分之一,後續需要大量的建構相關介入

② 這篇文章目的還是接受開發基礎,而本章模拟實作太過複雜,如果篇幅大了會主旨不清

③ 這個是最重要的點,我一時也寫不出來啊!!!,是以各位等下個長篇,小程式前端架構模拟實作吧

④ 如果繼續實作,這裡馬上要遇到元件處理、事件模型、分檔案建構等高端知識,時間會拉得很長

是以我們繼續下章吧......

小程式中的Page的封裝

小程式的Page類是這樣寫的:

1 Page({
2   data: {
3     pageData: \'頁面資料\'
4   },
5   onLoad: function () {
6     console.log(\'onLoad\')
7   },
8 })      

傳入的是一個對象,顯然,我們為了更好的拆分頁面邏輯,前面我們介紹了小程式是采用元件化開發的方式,這裡的說法可以更進一步,小程式是采用标簽化的方式開發,而标簽對應的控制器js隻會改變資料影響标簽顯示,是以某種程度小程式開發的特點是:先标簽後js,我們建構一個頁面,首先就應該思考這個頁面有哪些标簽,哪些标簽是公共的标簽,然後設計好标簽再做實作。

比如我們一個頁面中有比較複雜的月曆相關子產品,事實上這個月曆子產品也就是在操作月曆标簽的資料以及設定點選回調,那麼我們就需要将頁面分開

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

比如這裡的業務月曆子產品僅僅是index的一部分(其他頁面也可能用得到),是以我們實作了一個頁面共用的記錄,便與我們更好的分拆頁面:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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等等:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

page是最值得封裝的部分,這裡是基本page的封裝,事實上,清單頁是常用的一種業務頁面,雖然各種清單頁的篩選條件不一樣,但是主體功能無非都是:

① 清單渲染

② 滾動加載

③ 條件篩選、重新渲染

是以說我們其實可以将其做成一個頁面基類,跟abstract-page一個意思,這裡留待我們下次來處理吧

小程式中的元件

請大家對着github中的代碼調試閱讀這裡

前面已經說了,小程式的開發重點是一個個的标簽的實作,我們這裡将業務元件設定成了一個個mod,UI元件設定成了真正的标簽,比如我們頁面會有很多非業務類的UI元件:

① alert類彈出層

② loading類彈出層

③ 月曆元件

④ toast&message類提示彈出元件

⑤ 容器類元件

⑥ ......

這些都可以我們自己去實作,但是微信其實提供給我們了系統級别的元件:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

這裡要不要用就看實際業務需求了,一般來說還是建議用的,我們這裡為了幫助各位更好的了解小程式元件,特别實作了一個較為複雜,而小程式又沒有提供的元件月曆元件,首先我們這裡先建立一個月曆元件目錄:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

其次我們這裡先做最簡單實作:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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>      

月曆的基本頁面就出來了:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

這個月曆元件應該是在小程式中寫的最複雜的元件了,尤其是很多邏輯判斷的代碼都放在了WXML裡面,根據之前的了解,小程式渲染在一個webview中,js邏輯在一個webview中,他這樣做的目的可能是想讓性能更好,這種UI元件使用的方式一般是直接使用,但是如果涉及到了頁面業務,便需要獨立出一個mod小子產品去操作對應元件的資料,如圖我們這裡的月曆元件一般

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
<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中的代碼閱讀,最終使用效果:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉

小程式中的資料請求與緩存

小程式使用這個接口請求資料,這裡需要設定域名白名單:

wx.request(OBJECT)      
【微信小程式項目實踐總結】30分鐘從陌生到熟悉

可以看到資料請求已經回來了,但是我們一般來說一個接口不止會用于一個地方,每次重新寫好像有些費事,加之我這裡想将重複的請求緩存起來,是以我們這裡封裝一套資料通路層出來

之前在浏覽器中,我們一般使用localstorage存儲一些不太更改的資料,微信裡面提供了接口處理這一切:

wx.setStorage(OBJECT)      

我們這裡需要對其進行簡單封裝,便與後面更好的使用,一般來說有緩存就一定要有過期,是以我們動态給每個緩存對象增加一個過期時間:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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)      

這個時候我們開始寫我們資料請求的類:

首先還是實作了一個抽象類和一個業務基類,然後開始在業務層請求資料:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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      

資料請求核心類

這裡是業務基類的使用辦法:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
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分鐘從陌生到熟悉

結語

如果讀到這裡,我相信大家應該清楚了,30分鐘當然是騙人的啦。。。。。。别說三十分鐘了,三個小時這些東西都讀不完,對于初學者的同學建議把代碼下載下傳下來一邊調試一邊對着這裡的文章做思考,這樣3天左右便可以吸收很多微信小程式的知識

寫這篇文章說實話還比較辛苦,近期小钗這邊工作繁忙,有幾段都是在和老闆開會的時候偷偷寫的......,是以各位如果覺得文章還行麻煩幫忙點個贊

總結起來基本還是那句話,微信小程式從架構工程層面十分值得學習,而我這邊不出意外時間允許會深入的探索前端架構的實作,争取實作一套能相容小程式和web同時運作的代碼

我們實際工作中會直接使用上面的代碼,也會使用一些比較成熟的架構比如:https://tencent.github.io/wepy/,用什麼,怎麼做單看自己團隊項目的需求

我們在學習過程中做了一個實際的項目,完成度有60%,實際工作中便隻需要完善細節即可,我這裡便沒有再加強,一來是時間不足,二來是純粹業務代碼隻會讓學習的代碼變得複雜,沒什麼太大的必要,希望對初學者有一定幫助:

【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉
【微信小程式項目實踐總結】30分鐘從陌生到熟悉