【微信小程序項目實踐總結】30分鐘從陌生到熟悉 web app 、native app、hybrid app比較 30分鐘ES6從陌生到熟悉 【原創】淺談內存泄露 HTML5 五子棋 - JS/Can

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

前言

咱們以前對小程序作了基本學習:javascript

閱讀本文以前,若是你們想對小程序有更深刻的瞭解,或者一些細節的瞭解能夠先閱讀上述文章,本文後面點須要對着代碼調試閱讀php

對應的github地址是:https://github.com/yexiaochai/wxdemocss

首先咱們來一言以蔽之,什麼是微信小程序?PS:這個問題問得好像有些扯:)html

小程序是一個不須要下載安裝就可以使用的應用,它實現了應用觸手可及的夢想,用戶掃一掃或者搜一下便可打開應用。也體現了用完即走的理念,用戶不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝卸載。從字面上看小程序具備相似Web應用的熱部署能力,在功能上又接近於原生APP。前端

因此說,其實微信小程序是一套超級Hybrid的解決方案,如今看來,小程序應該是應用場景最廣,也最爲複雜的解決方案了vue

不少公司都會有本身的Hybrid平臺,我這裏瞭解到比較不錯的是攜程的Hybrid平臺、阿里的Weex、百度的糯米,可是從應用場景來講都沒有微信來得豐富,這裏根本的區別是:html5

微信小程序是給各個公司開發者接入的,其餘公司平臺可能是給本身業務團隊使用,這一根本區別,就造就了咱們看到的不少小程序不同的特性:java

① 小程序定義了本身的標籤語言WXMLnode

② 小程序定義了本身的樣式語言WXSSreact

③ 小程序提供了一套前端框架包括對應Native API

④ 禁用瀏覽器Dom API(這個區別,會影響咱們的代碼方式)

只要瞭解到這些區別就會知道爲何小程序會這麼設計:

由於小程序是給各個公司的開發作的,其餘公司的Hybrid方案是給公司業務團隊用的,通常擁有Hybrid平臺的公司實力都不錯
可是開發小程序的公司實力參差不齊,因此小程序要作絕對的限制,最大程度的保證框架層(小程序團隊)對程序的控制
由於畢竟程序運行在微信這種體量的APP中

以前我也有一個疑惑爲何微信小程序會設計本身的標籤語言,也在知乎看到各類各樣的回答,可是若是出於設計層面以及應用層面考慮的話:這樣會有更好的控制,並且我後面發現微信小程序事實上依舊使用的是webview作渲染(這個與我以前認爲微信是NativeUI是向左的),可是若是咱們使用的微信限制下面的標籤,這個是有限的標籤,後期想要換成NativeUI會變得更加輕易:

另外一方面,通過以前的學習,我這邊明確能夠得出一個感覺:

① 小程序的頁面核心是標籤,標籤是不可控制的(我暫時沒用到js操做元素的方法),只能按照微信給的玩法玩,標籤控制顯示是咱們的view

② 標籤的展現只與data有關聯,和js是隔離的,沒有辦法在標籤中調用js的方法

③ 而咱們的js的惟一工做即是根據業務改變data,從新引起頁面渲染,之後別想操做DOM,別想操做Window對象了,改變開發方式,改變開發方式,改變開發方式!

1 this.setData({'wxml': `
2   <my-component>
3   <view>動態插入的節點</view> 
4   </my-component>
5 `});

 

而後能夠看到這個是一個MVC模型

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

複製代碼
 1 project
 2 ├── pages
 3 |   ├── index
 4 |   |   ├── index.json  index 頁面配置
 5 |   |   ├── index.js    index 頁面邏輯
 6 |   |   ├── index.wxml  index 頁面結構
 7 |   |   └── index.wxss  index 頁面樣式表
 8 |   └── log
 9 |       ├── log.json    log 頁面配置
10 |       ├── log.wxml    log 頁面邏輯
11 |       ├── log.js      log 頁面結構
12 |       └── log.wxss    log 頁面樣式表
13 ├── app.js              小程序邏輯
14 ├── app.json            小程序公共設置
15 └── app.wxss            小程序公共樣式表
複製代碼

每一個組件的目錄也大概是這個樣子的,大同小異,可是入口是Page層。

小程序打包後的結構(這裏就真的不懂了,引用:小程序底層框架實現原理解析):

全部的小程序基本都最後都被打成上面的結構

一、WAService.js  框架JS庫,提供邏輯層基礎的API能力

二、WAWebview.js 框架JS庫,提供視圖層基礎的API能力

三、WAConsole.js 框架JS庫,控制檯

四、app-config.js 小程序完整的配置,包含咱們經過app.json裏的全部配置,綜合了默認配置型

五、app-service.js 咱們本身的JS代碼,所有打包到這個文件

六、page-frame.html 小程序視圖的模板文件,全部的頁面都使用此加載渲染,且全部的WXML都拆解爲JS實現打包到這裏

七、pages 全部的頁面,這個不是咱們以前的wxml文件了,主要是處理WXSS轉換,使用js插入到header區域

從設計的角度上說,小程序採用的組件化開發的方案,除了頁面級別的標籤,後面所有是組件,而組件中的標籤view、data、js的關係應該是與page是一致的,這個也是咱們平時建議的開發方式,將一根頁面拆分紅一個個小的業務組件或者UI組件:

從我寫業務代碼過程當中,以爲總體來講仍是比較順暢的,小程序是有本身一套完整的前端框架的,而且釋放給業務代碼的主要就是page,而page只能使用標籤和組件,因此說框架的對業務的控制力度很好。

最後咱們從工程角度來看微信小程序的架構就更加完美了,小程序從三個方面考慮了業務者的感覺:

① 開發工具+調試工具

② 開發基本模型(開發基本標準WXML、WXSS、JS、JSON)

③ 完善的構建(對業務方透明)

④ 自動化上傳離線包(對業務費透明離線包邏輯)

⑤ 監控統計邏輯

因此,微信小程序從架構上和使用場景來講是很使人驚豔的,至少驚豔了我......因此咱們接下來在開發層面對他進行更加深刻的剖析,咱們這邊最近一直在作基礎服務,這一切都是爲了完善技術體系,這裏對於前端來講即是咱們須要作一個Hybrid體系,若是作App,React Native也是不錯的選擇,可是必定要有完善的分層:

① 底層框架解決開發效率,將複雜的部分作成一個黑匣子,給頁面開發展現的只是固定的三板斧,固定的模式下開發便可

② 工程部門爲業務開發者封裝最小化開發環境,最優爲瀏覽器,確實不行便爲其提供一個相似瀏覽器的調試環境

如此一來,業務便能快速迭代,由於業務開發者寫的代碼大同小異,因此底層框架配合工程團隊(通常是同一個團隊),即可以在底層作掉不少效率性能問題。

稍微大點的公司,稍微寬裕的團隊,還會同步作不少後續的性能監控、錯誤日誌工做,如此造成一套文檔->開發->調試->構建->發佈->監控、分析 爲一套完善的技術體系

若是造成了這麼一套體系,那麼後續就算是內部框架更改、技術革新,也是在這個體系上改造,這塊微信小程序是作的很是好的。但很惋惜,不少其餘公司團隊只會在這個路徑上作一部分,後面因爲種種緣由不在深刻,有多是感受沒價值,而最恐怖的行爲是,本身的體系沒造成就貿然的換基礎框架,戒之慎之啊!好了閒話少說,咱們繼續接下來的學習。

我對小程序的理解有限,由於沒有源碼只能靠經驗猜想,若是文中有誤,請各位多多提點

文章更多面對初中級選手,若是對各位有用,麻煩點贊喲

微信小程序的執行流程

微信小程序爲了對業務方有更強的控制,App層作的工做頗有限,我後面寫demo的時候根本沒有用到app.js,因此我這裏認爲app.js只是完成了一個路由以及初始化相關的工做,這個是咱們看獲得的,咱們看不到的是底層框架會根據app.json的配置將全部頁面js都準備好。

我這裏要表達的是,咱們這裏配置了咱們全部的路由:

"pages":[
  "pages/index/index",
  "pages/list/list",
  "pages/logs/logs"
],

微信小程序一旦載入,會開3個webview,裝載3個頁面的邏輯,完成基本的實例化工做,只顯示首頁!這個是小程序爲了優化頁面打開速度所作的工做,也勢必會浪費一些資源,因此究竟是所有打開或者預加載幾個,詳細底層Native會根據實際狀況動態變化,咱們也能夠看到,從業務層面來講,要了解小程序的執行流程,其實只要能瞭解Page的流程就行了,關於Page生命週期,除了釋放出來的API:onLoad -> onShow -> onReady -> onHide等,官方還出了一張圖進行說明:

Native層在載入小程序時候,起了兩個線程一個的view Thread一個是AppService Thread,我這邊理解下來應該就是程序邏輯執行與頁面渲染分離,小程序的視圖層目前使用 WebView 做爲渲染載體,而邏輯層是由獨立的 JavascriptCore 做爲運行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模塊,並不具有數據直接共享的通道。當前,視圖層和邏輯層的數據傳輸,實際上經過兩邊提供的 evaluateJavascript 所實現。即用戶傳輸的數據,須要將其轉換爲字符串形式傳遞,同時把轉換後的數據內容拼接成一份 JS 腳本,再經過執行 JS 腳本的形式傳遞到兩邊獨立環境。而 evaluateJavascript 的執行會受不少方面的影響,數據到達視圖層並非實時的。

由於以前我認爲頁面是使用NativeUI作渲染跟Webview沒撒關係,便以爲這個圖有問題,可是後面實際代碼看到了熟悉的shadow-dom以及Android能夠看到哪部分是Web的,其實小程序主體仍是使用的瀏覽器渲染的方式,仍是webview裝載HTML和CSS的邏輯,最後我發現這張圖是沒有問題的,有問題的是個人理解,哈哈,這裏咱們從新解析這張圖:

WXML先會被編譯成JS文件,引入數據後在WebView中渲染,這裏能夠認爲微信載入小程序時同時初始化了兩個線程,分別執行彼此邏輯:

① WXML&CSS編譯造成的JS View實例化結束,準備結束時向業務線程發送通知

② 業務線程中的JS Page部分同步完成實例化結束,這個時候接收到View線程部分的等待數據通知,將初始化data數據發送給View

③ View線程接到數據,開始渲染頁面,渲染結束執行通知Page觸發onReady事件

這裏翻開源碼,能夠看到,應該是全局控制器完成的Page實例化,完成後便會執行onLoad事件,可是在執行前會往頁面發通知:

複製代碼
1 __appServiceSDK__.invokeWebviewMethod({
2     name: "appDataChange",
3     args: o({}, e, {
4         complete: n
5     }),
6     webviewIds: [t]
7 })
複製代碼

真實的邏輯是這樣的,全局控制器會完成頁面實例化,這個是根據app.json中來的,所有完成實例化存儲起來而後選擇第一個page實例執行一些邏輯,而後通知view線程,即將執行onLoad事件,由於view線程和業務線程是兩個線程,因此不會形成阻塞,view線程根據初始數據完成渲染,而業務線程繼續後續邏輯,執行onLoad,若是onLoad中有setData,那麼會進入隊列繼續通知view線程更新。

因此我我的感受微信官網那張圖不太清晰,我這裏從新畫了一個圖:

引用一張其餘地方的圖

模擬實現

都這個時候了,不來個簡單的小程序框架實現好像有點不對,咱們作小程序實現的主要緣由是想作到一端代碼三端運行:web、小程序、Hybrid甚至Servce端

咱們這裏沒有可能實現太複雜的功能,這裏想的是就實現一個基本的頁面展現帶一個最基本的標籤便可,只作Page一塊的簡單實現,讓你們能瞭解到小程序可能的實現,以及如何將小程序直接轉爲H5的可能走法

1 <view>
2   <!-- 如下是對一個自定義組件的引用 -->
3   <my-component inner-text="組件數據"></my-component>
4   <view>{{pageData}}</view>
5 </view>
複製代碼
1 Page({
2   data: {
3     pageData: '頁面數據'
4   },
5   onLoad: function () {
6     console.log('onLoad')
7   },
8 })
複製代碼
1 <!-- 這是自定義組件的內部WXML結構 -->
2 <view class="inner">
3   {{innerText}}
4 </view>
5 <slot></slot>
複製代碼
 1 Component({
 2   properties: {
 3     // 這裏定義了innerText屬性,屬性值能夠在組件使用時指定
 4     innerText: {
 5       type: String,
 6       value: 'default value',
 7     }
 8   },
 9   data: {
10     // 這裏是一些組件內部數據
11     someData: {}
12   },
13   methods: {
14     // 這裏是一個自定義方法
15     customMethod: function () { }
16   }
17 })
複製代碼

咱們直接將小程序這些代碼拷貝一份到咱們的目錄:

咱們須要作的就是讓這段代碼運行起來,而這裏的目錄是咱們最終看見的目錄,真實運行的時候可能不是這個樣,運行以前項目會經過咱們的工程構建,變成能夠直接運行的代碼,而我這裏思考的能夠運行的代碼事實上是一個模塊,因此咱們這裏從最終結果反推、分拆到開發結構目錄,咱們首先將全部代碼放到index.html,多是這樣的:

複製代碼
  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4   <meta charset="UTF-8">
  5   <title>Title</title>
  6 </head>
  7 <body>
  8 
  9 <script type="text/javascript" src="libs/zepto.js" ></script>
 10 <script type="text/javascript">
 11 
 12   class View {
 13     constructor(opts) {
 14       this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';
 15 
 16       //由控制器page傳入的初始數據或者setData產生的數據
 17       this.data = {
 18         pageShow: 'pageshow',
 19         pageData: 'pageData',
 20         pageShow1: 'pageShow1'
 21       };
 22 
 23       this.labelMap = {
 24         'view': 'div',
 25         '#text': 'span'
 26       };
 27 
 28       this.nodes = {};
 29       this.nodeInfo = {};
 30     }
 31 
 32     /*
 33       傳入一個節點,解析出一個節點,而且將節點中的數據以初始化數據改變
 34       而且將其中包含{{}}標誌的節點信息記錄下來
 35     */
 36     _handlerNode (node) {
 37 
 38       let reg = /\{\{([\s\S]+?)\}\}/;
 39       let result, name, value, n, map = {};
 40       let attrs , i, len, attr;
 41 
 42       name = node.nodeName;
 43       attrs = node.attributes;
 44       value = node.nodeValue;
 45       n = document.createElement(this.labelMap[name.toLowerCase()] || name);
 46 
 47       //說明是文本,須要記錄下來了
 48       if(node.nodeType === 3) {
 49         n.innerText =  this.data[value] || '';
 50 
 51         result =  reg.exec(value);
 52         if(result) {
 53           n.innerText =  this.data[result[1]] || '';
 54 
 55           if(!map[result[1]]) map[result[1]] = [];
 56           map[result[1]].push({
 57             type: 'text',
 58             node: n
 59           });
 60         }
 61       }
 62 
 63       if(attrs) {
 64         //這裏暫時只處理屬性和值兩種狀況,多了就複雜10倍了
 65         for (i = 0, len = attrs.length; i < len; i++) {
 66           attr = attrs[i];
 67           result = reg.exec(attr.value);
 68 
 69           n.setAttribute(attr.name, attr.value);
 70           //若是有node須要處理則須要存下來標誌
 71           if (result) {
 72             n.setAttribute(attr.name, this.data[result[1]] || '');
 73 
 74             //存儲全部會用到的節點,以便後面動態更新
 75             if (!map[result[1]]) map[result[1]] = [];
 76             map[result[1]].push({
 77               type: 'attr',
 78               name: attr.name,
 79               node: n
 80             });
 81 
 82           }
 83         }
 84       }
 85 
 86       return {
 87         node: n,
 88         map: map
 89       }
 90 
 91     }
 92 
 93     //遍歷一個節點的全部子節點,若是有子節點繼續遍歷到沒有爲止
 94     _runAllNode(node, map, root) {
 95 
 96       let nodeInfo = this._handlerNode(node);
 97       let _map = nodeInfo.map;
 98       let n = nodeInfo.node;
 99       let k, i, len, children = node.childNodes;
100 
101       //先將該根節點插入到上一個節點中
102       root.appendChild(n);
103 
104       //處理map數據,這裏的map是根對象,最初的map
105       for(k in _map) {
106         if(map[k]) {
107           map[k].push(_map[k]);
108         } else {
109           map[k] = _map[k];
110         }
111       }
112 
113       for(i = 0, len = children.length; i < len; i++) {
114         this._runAllNode(children[i], map, n);
115       }
116 
117     }
118 
119     //處理每一個節點,翻譯爲頁面識別的節點,而且將須要操做的節點記錄
120     splitTemplate () {
121       let nodes = $(this.template);
122       let map = {}, root = document.createElement('div');
123       let i, len;
124 
125       for(i = 0, len = nodes.length; i < len; i++) {
126         this._runAllNode(nodes[i], map, root);
127       }
128 
129       window.map = map;
130       return root
131     }
132 
133       //拆分目標造成node,這個方法過長,真實項目須要拆分
134     splitTemplate1 () {
135       let template = this.template;
136       let node = $(this.template)[0];
137       let map = {}, n, name, root = document.createElement('div');
138       let isEnd = false, index = 0, result;
139 
140       let attrs, i, len, attr;
141       let reg = /\{\{([\s\S]+?)\}\}/;
142 
143       window.map = map;
144 
145       //開始遍歷節點,處理
146       while (!isEnd) {
147         name = node.localName;
148         attrs = node.attributes;
149         value = node.nodeValue;
150         n = document.createElement(this.labelMap[name] || name);
151 
152         //說明是文本,須要記錄下來了
153         if(node.nodeType === 3) {
154           n.innerText =  this.data[value] || '';
155 
156           result =  reg.exec(value);
157           if(result) {
158             n.innerText =  this.data[value] || '';
159 
160             if(!map[value]) map[value] = [];
161             map[value].push({
162               type: 'text',
163               node: n
164             });
165           }
166         }
167 
168         //這裏暫時只處理屬性和值兩種狀況,多了就複雜10倍了
169         for(i = 0, len = attrs.length; i < len; i++) {
170           attr = attrs[i];
171           result =  reg.exec(attr.value);
172 
173           n.setAttribute(attr.name, attr.value);
174           //若是有node須要處理則須要存下來標誌
175           if(result) {
176             n.setAttribute(attr.name, this.data[result[1]] || '');
177 
178             //存儲全部會用到的節點,以便後面動態更新
179             if(!map[result[1]]) map[result[1]] = [];
180             map[result[1]].push({
181               type: 'attr',
182               name: attr.name,
183               node: n
184             });
185 
186           }
187         }
188 
189 debugger
190 
191         if(index === 0) root.appendChild(n);
192         isEnd = true;
193         index++;
194 
195       }
196 
197       return root;
198 
199 
200       console.log(node)
201     }
202 
203   }
204 
205   let view = new View();
206 
207   document.body.appendChild(window.node)
208 
209 </script>
210 </body>
211 </html>
複製代碼

這段代碼,很是簡單:

① 設置了一段模板,甚至,咱們這裏根本不關係其格式化狀態,直接寫成一行方便處理

this.template = '<view>{{pageShow}}</view><view class="ddd" is-show="{{pageShow}}" >{{pageShow}}<view class="c1">{{pageData}}</view></view>';

② 而後咱們將這段模板轉爲node節點(這裏能夠不用zepto,可是模擬實現怎麼簡單怎麼來吧),而後遍歷處理全部節點,咱們就能夠處理咱們的數據了,最終造成了這個html:

1 <div><div><span>ffsd</span></div><div class="ddd" is-show="pageshow"><span>pageshow</span><div class="c1"><span>pageData</span></div></div></div>

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

這個對象是全部setData會影響到node的一個映射表,後面調用setData的時候,即可以直接操做對應的數據了,這裏咱們分拆咱們代碼,造成了幾個關鍵部分,首先是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%,實際工做中便只須要完善細節便可,我這裏便沒有再增強,一來是時間不足,二來是純粹業務代碼只會讓學習的代碼變得複雜,沒什麼太大的必要,但願對初學者有必定幫助:

 

 

以前作討論的時候,提出了這麼一個問題,互聯網產品的載體有多種,好比native app,web app,微信公衆號,小程序等,那麼這些不一樣形式的載體有什麼區別點呢。當時只有一個特別簡單的理解,後來又去查了一下,本文就先分析一下 web app 與 native app之間的區別點。

本文的結構主要分爲如下部分:
1.app的分類
2.每類app的定義,明確各種app具體是什麼
3.各種app的優缺點
4.具體開發過程當中,到底該採用哪一種類型的app

1.app的分類
大體能夠分爲這3種:

  • native app(原生app)
  • web app
  • hybrid app(混合app)
 
 

2.三類app的定義
**2.1 native app **
中文名稱爲「原生app」
來看一下百度百科的定義:基於智能手機本地操做系統如iOS、Android、WP並使用原生程式編寫運行的第三方應用程序,通常開發的語言爲Java、C++等。在使用上的具體表現就是,手機桌面上的圖標點進去基本就是native app了。

2.2 web app
仍然看一下百度百科的定義:基於web的系統和應用,運行於網絡和瀏覽器之上,目前多采用h5標準開發。在使用上的具體表現是,手機瀏覽器點擊進入,會有一些應用的小圖標,這些小圖標在點擊後,在瀏覽器里加載的頁面 跟你直接下載一個app後打開的頁面是相同的,這些小圖標表明的就是web app。

2.3 hybrid app
中文名稱是「混合app」
顧名思義,就是 native app 與 web app的混合。在native app裏內置瀏覽器,合適的功能頁面採用網頁的形式呈現。好比京東的某些營銷頁面,今日頭條的某些新聞頁面、微信的騰訊新聞的內容頁面等。

3.各種app的優缺點
3.1native app
優勢:

  • 提供最佳用戶體驗,最優質的用戶界面,流暢的交互
  • 能夠訪問本地資源
  • 能夠調用移動硬件設備,好比攝像頭、麥克風等

缺點:

  • 開發成本高。每種移動操做系統都須要獨立的開發項目,針對不一樣平臺提供不一樣體驗;
  • 發佈新版本慢。下載是用戶控制的,不少用戶不肯意下載更新(好比說,版本發佈到了3.0,但仍是有不少1.0的用戶,你可能就得繼續維護1.0版本的API)
  • 應用商店發佈審覈週期長。安卓平臺大概要1~3天,而iOS平臺須要的時間更長

3.2 web app
優勢:

  • 不須要安裝包,節約手機空間
  • 總體量級輕,開發成本低
  • 不須要用戶進行手動更新,由應用開發者直接在後臺更新,推送到用戶面前的都是全新版本,更便於業務的開展
  • 基於瀏覽器,能夠跨平臺使用

缺點:

  • 頁面跳轉費力,不穩定感更強。在網速受到限制時,不少時候出現卡頓或者卡死現象,交互效果受到限制
  • 安全性相對較低,數據容易泄露或者被劫持

3.3 Hybrid app
這類app集合了上面兩種app各自的優點:
(下面優點點 參考 點擊此處

  • 在實現更多功能的前提下,使得app安裝包不至於過大
  • 在應用內部打開web網頁,省去了跳轉瀏覽器的麻煩
  • 主要功能區相對穩定下,增長的功能區採用web 形式,使得迭代更加方便
  • web頁面在用戶設置不一樣的網絡制式時會以不一樣的形式呈現(以微信朋友圈爲例,在數據流量下,設置APNS爲WAP時,微信訂閱號內容將屏蔽圖片和視頻。這樣就能爲用戶省去一部分流量,整個頁面閱讀就不那麼友好了)

另外,爲何有些原生app還會作web app呢?
如下圖爲例,這是個人手機瀏覽器自帶的幾個web app的圖標

 
web app示例

有這麼幾點緣由:

  • 數據能夠被搜索引擎的爬蟲抓到,並進行索引。若是產品只有一個app,那麼它的入口獨立,但同時數據也是封閉的。若是用戶從搜索引擎查找的話,是找不到相關信息的。因此作成web app,能夠被搜索引擎找到
  • 用戶碎片時間使用,例如一些黏性不高的應用,好比 移動搜索、網址導航等

4.具體開發過程當中,到底該採用哪一種類型的app
參考 pmcaff上的 你們公司的app是用原生作的仍是h5呢?
本文將作一下整理:
不一樣的頁面狀況選擇不一樣的開發方式

  • 4.1 若是app中出現了大段文字(如新聞、攻略等),而且格式比較豐富(如加粗、字體多樣等),採用H5較好。緣由:原生開發對解析json字符串格式不是很友好
  • 4.2 若是講究app反應速度(含頁面切換流暢性),採用原生開發。緣由:H5本質上是網頁,換網頁的時候,基本要加載整個頁面,就像一個瀏覽器打開一個新的網頁同樣,比較慢,而原生系統只須要加載變化的部分
  • 4.3 若是app對有無網絡、網絡優劣敏感(譬若有離線操做、在線操做),則採用原生開發。雖然H5能夠作到,可是比較敏感
  • 4.4 若是app要頻繁地調用硬件設備(好比攝像頭、麥克風等),則採用原生開發,這樣支持硬件更多,調用速度更快,H5可望不可即
  • 4.5 若是app用戶常見頁面頻換(如淘寶首頁的各類營銷活動),採用H5,維護起來更容易
  • 4.6 若是預算有限(H5開發一套可在安卓、iOS、黑莓等跨平臺使用)、不在意用戶體驗、不在意加載速度,確定是H5

另:
短時間活動,專題營銷類的頁面居多的,能夠選擇原生app搭建框架,詳細頁面採用H5,便於活動的隨時修改和管理
主要業務流程方面,選擇原生app開發,有更好的用戶體驗,也能夠更方便的拓展其餘功能

參考閱讀:

  1. 你們公司的app是用原生作的仍是h5的呢?
  2. H5將來真的會取代APP嗎?
  3. 爲何各大公司一直熱衷於研究使用web app替換原生app?
  4. 科普:Native App、Web App與Hybrid App
  5. Web App 和 Native App,哪一個是趨勢?
  6. 關於H5的介紹


做者:產品新人學習路
連接:https://www.jianshu.com/p/24bf070a4dcb
來源:簡書
簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
 
 

30分鐘ES6從陌生到熟悉

前言

ECMAScript 6.0(如下簡稱 ES6)是 JavaScript 語言的下一代標準,已經在 2015 年 6 月正式發佈了。它的目標,是使得 JavaScript 語言能夠用來編寫複雜的大型應用程序,成爲企業級開發語言。

這句話基本涵蓋了爲何會產生ES6此次更新的緣由——編寫複雜的大型應用程序。

回顧近兩年的前端開發,複雜度確實在快速增長,近期不論從系統複雜度仍是到前端開發人員數量應該達到了一個飽和值,換個方式說,沒有ES6咱們的前端代碼依舊能夠寫不少複雜的應用,而ES6的提出更好的幫咱們解決了不少歷史遺留問題,另外一個角度ES6讓JS更適合開發大型應用,而不用引用太多的庫了。

本文,簡單介紹幾個ES6核心概念,我的感受只要掌握如下新特性便能愉快的開始使用ES6作代碼了!

這裏的文章,請配合着阮老師這裏的教程,一些細節阮老師那邊講的好得多:http://es6.ruanyifeng.com/#docs/class-extends

除了阮老師的文章還參考:http://www.infoq.com/cn/articles/es6-in-depth-arrow-functions

PS:文中只是我的感悟,有誤請在評論提出

模塊Module的引入

都說了複雜的大型應用了,因此咱們第一個要討論的重要特性就是模塊概念,咱們作一個複雜的項目一定須要兩步走:

① 分得開,而且須要分開

② 合得起來

咱們廣泛認爲沒有複雜的應用,只有分不開的應用,再複雜的應用,一旦能夠使用組件化、模塊化的方式分紅不一樣的小單元,那麼其難度便會大大下降,模塊化是大型、複雜項目的主要攔路虎。爲了解決這個問題,社區制定了一些模塊加載方案,對於瀏覽器開發來講,咱們用的最多的是AMD規範,也就是你們熟知的requireJS,而ES6中在語音標準層面實現了模塊功能,用以取代服務端通訊的CommonJS和AMD規範,成爲了通用的規範,多說無益,咱們這裏上一段代碼說明:

複製代碼
 1 /*
 2 validate.js 多用於表單驗證
 3 */
 4 export function isEmail (text) {
 5     var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 6     return reg.test(text);
 7 }
 8 
 9 export function  isPassword (text) {
10     var reg = /^[a-zA-Z0-9]{6,20}$/;
11     return reg.test(text);
12 }
複製代碼

那麼咱們如今想在頁面裏面使用這個工具類該怎麼作呢:

複製代碼
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8 <!-- 請注意這裏type=module才能運行 -->
 9 <script type="module">
10     import {isEmail} from './validate.js';
11     var e1 = 'dddd';
12     var e2 = 'yexiaochai@qq.com'
13     console.log(isEmail(e1))
14     console.log(isEmail(e2))
15 </script>
16 </body>
17 </html>
複製代碼

ES6中的Module提出,在我這裏看來是想在官方完成以前requireJS乾的工做,這裏也有一些本質上的不同:

① requireJS是使用加載script標籤的方式載入js,沒有什麼限制

② import命令會被js引擎靜態分析,先於模塊其餘語句執行

以上特性會直接給咱們帶來一些困擾,好比原來咱們項目控制器會有這麼一段代碼:

1 var viewId = ''; //由瀏覽器獲取試圖id,url可能爲?viewId=booking|list|...
2 //若是不存在則須要構建,記住構建時須要使用viewdata繼承源view
3 requirejs(viewId, function(View) {
4     //執行根據url參數動態加載view邏輯
5 })

前面說過了,import命令會被js引擎靜態分析,先於模塊其餘語句執行,因此咱們在根本不能將import執行滯後,或者動態化,作不到的,這種寫法也是報錯的:

if (viewId) {
  import view from './' + viewId;
}

這種設計會有利於提升編譯器效率,可是以前的動態業務邏輯就不知道如何繼續了?而ES6若是提供import的方法,咱們變能夠執行邏輯:

1 import(viewId, function() {
2     //渲染頁面
3 })

事實上他也提供了:

如今看起來,JS中的模塊便十分完美了,至於其中一些細節,即可以用到的時候再說了

ES6中的類Class

咱們對咱們的定位一直是很是清晰的,咱們就是要幹大項目的,咱們是要幹複雜的項目,除了模塊概念,類的概念也很是重要,咱們以前用的這種方式實現一個類,咱們來溫故而知新。

當一個函數被建立時,Function構造函數產生的函數會隱式的被賦予一個prototype屬性,prototype包含一個constructor對象

而constructor即是該新函數對象(constructor意義不大,可是能夠幫咱們找到繼承關係)

每一個函數都會有一個prototype屬性,該屬性指向另外一對象,這個對象包含能夠由特定類型的全部實例共享的屬性和方法

每次實例化後,實例內部都會包含一個[[prototype]](__proto__)的內部屬性,這個屬性指向prototype

① 咱們經過isPrototypeOf來肯定某個對象是否是個人原型
② hasOwnPrototype 能夠檢測一個屬性是存在實例中仍是原型中,該屬性不是原型屬性才返回true
複製代碼
var Person = function (name, age) {
    this.name = name;
    this.age = age;
};
Person.prototype.getName = function () {
    return this.name;
};
var y = new Person('葉小釵', 30);
複製代碼

爲了方便,使用,咱們作了更爲複雜的封裝:

複製代碼
 1 var arr = [];
 2 var slice = arr.slice;
 3 
 4 function create() {
 5   if (arguments.length == 0 || arguments.length > 2) throw '參數錯誤';
 6 
 7   var parent = null;
 8   //將參數轉換爲數組
 9   var properties = slice.call(arguments);
10 
11   //若是第一個參數爲類(function),那麼就將之取出
12   if (typeof properties[0] === 'function')
13     parent = properties.shift();
14   properties = properties[0];
15 
16   function klass() {
17     this.initialize.apply(this, arguments);
18   }
19 
20   klass.superclass = parent;
21   klass.subclasses = [];
22 
23   if (parent) {
24     var subclass = function () { };
25     subclass.prototype = parent.prototype;
26     klass.prototype = new subclass;
27     parent.subclasses.push(klass);
28   }
29 
30   var ancestor = klass.superclass && klass.superclass.prototype;
31   for (var k in properties) {
32     var value = properties[k];
33 
34     //知足條件就重寫
35     if (ancestor && typeof value == 'function') {
36       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
37       //只有在第一個參數爲$super狀況下才須要處理(是否具備重複方法須要用戶本身決定)
38       if (argslist[0] === '$super' && ancestor[k]) {
39         value = (function (methodName, fn) {
40           return function () {
41             var scope = this;
42             var args = [function () {
43               return ancestor[methodName].apply(scope, arguments);
44             } ];
45             return fn.apply(this, args.concat(slice.call(arguments)));
46           };
47         })(k, value);
48       }
49     }
50 
51     klass.prototype[k] = value;
52   }
53 
54   if (!klass.prototype.initialize)
55     klass.prototype.initialize = function () { };
56 
57   klass.prototype.constructor = klass;
58 
59   return klass;
60 }
複製代碼

這裏寫一個demo:

複製代碼
 1 var AbstractView = create({
 2   initialize: function (opts) {
 3     opts = opts || {};
 4     this.wrapper = opts.wrapper || $('body');
 5 
 6     //事件集合
 7     this.events = {};
 8 
 9     this.isCreate = false;
10 
11   },
12   on: function (type, fn) {
13     if (!this.events[type]) this.events[type] = [];
14     this.events[type].push(fn);
15   },
16   trigger: function (type) {
17     if (!this.events[type]) return;
18     for (var i = 0, len = this.events[type].length; i < len; i++) {
19       this.events[type][i].call(this)
20     }
21   },
22   createHtml: function () {
23     throw '必須重寫';
24   },
25   create: function () {
26     this.root = $(this.createHtml());
27     this.wrapper.append(this.root);
28     this.trigger('onCreate');
29     this.isCreate = true;
30   },
31   show: function () {
32     if (!this.isCreate) this.create();
33     this.root.show();
34     this.trigger('onShow');
35   },
36   hide: function () {
37     this.root.hide();
38   }
39 });
40 
41 var Alert = create(AbstractView, {
42 
43   createHtml: function () {
44     return '<div class="alert">這裏是alert框</div>';
45   }
46 });
47 
48 var AlertTitle = create(Alert, {
49   initialize: function ($super) {
50     this.title = '';
51     $super();
52 
53   },
54   createHtml: function () {
55     return '<div class="alert"><h2>' + this.title + '</h2>這裏是帶標題alert框</div>';
56   },
57 
58   setTitle: function (title) {
59     this.title = title;
60     this.root.find('h2').html(title)
61   }
62 
63 });
64 
65 var AlertTitleButton = create(AlertTitle, {
66   initialize: function ($super) {
67     this.title = '';
68     $super();
69 
70     this.on('onShow', function () {
71       var bt = $('<input type="button" value="點擊我" />');
72       bt.click($.proxy(function () {
73         alert(this.title);
74       }, this));
75       this.root.append(bt)
76     });
77   }
78 });
79 
80 var v1 = new Alert();
81 v1.show();
82 
83 var v2 = new AlertTitle();
84 v2.show();
85 v2.setTitle('我是標題');
86 
87 var v3 = new AlertTitleButton();
88 v3.show();
89 v3.setTitle('我是標題和按鈕的alert');
複製代碼

ES6中直接從標準層面解決了咱們的問題,他提出了Class關鍵詞讓咱們能夠更好的定義類,咱們這裏用咱們ES6的模塊語法從新實現一次:

複製代碼
 1 export class AbstractView {
 2     constructor(opts) {
 3         opts = opts || {};
 4         this.wrapper = opts.wrapper || $('body');
 5         //事件集合
 6         this.events = {};
 7         this.isCreate = false;
 8     }
 9     on(type, fn) {
10         if (!this.events[type]) this.events[type] = [];
11         this.events[type].push(fn);
12     }
13     trigger(type) {
14         if (!this.events[type]) return;
15         for (var i = 0, len = this.events[type].length; i < len; i++) {
16             this.events[type][i].call(this)
17         }
18     }
19     createHtml() {
20         throw '必須重寫';
21     }
22     create() {
23         this.root = $(this.createHtml());
24         this.wrapper.append(this.root);
25         this.trigger('onCreate');
26         this.isCreate = true;
27     }
28     show() {
29         if (!this.isCreate) this.create();
30         this.root.show();
31         this.trigger('onShow');
32     }
33     hide() {
34         this.root.hide();
35     }
36 }
37 export class Alert extends AbstractView {
38     createHtml() {
39         return '<div class="alert">這裏是alert框</div>';
40     }
41 }
42 export class AlertTitle extends Alert {
43     constructor(opts) {
44         super(opts);
45         this.title = '';
46     }
47     createHtml() {
48         return '<div class="alert"><h2>' + this.title + '</h2>這裏是帶標題alert框</div>';
49     }
50     setTitle(title) {
51         this.title = title;
52         this.root.find('h2').html(title)
53     }
54 }
55 export class  AlertTitleButton extends AlertTitle {
56     constructor(opts) {
57         super(opts);
58         this.on('onShow', function () {
59             var bt = $('<input type="button" value="點擊我" />');
60             bt.click($.proxy(function () {
61                 alert(this.title);
62             }, this));
63             this.root.append(bt)
64         });
65     }
66 }
複製代碼
複製代碼
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8 <script type="text/javascript" src="zepto.js"></script>
 9 
10 <!-- 請注意這裏type=module才能運行 -->
11 <script type="module">
12 import {Alert, AlertTitle, AlertTitleButton} from './es6class.js';
13 var v1 = new Alert();
14 v1.show();
15 var v2 = new AlertTitle();
16 v2.show();
17 v2.setTitle('我是標題');
18 var v3 = new AlertTitleButton();
19 v3.show();
20 v3.setTitle('我是標題和按鈕的alert');
21 </script>
22 </body>
23 </html>
複製代碼

這裏的代碼完成了與上面同樣的功能,而代碼更加的清爽了。

ES6中的函數

咱們這裏學習ES6,由大到小,首先討論模塊,其次討論類,這個時候理所固然到了咱們的函數了,ES6中函數也多了不少新特性或者說語法糖吧,首先咱們來講一下這裏的箭頭函數

箭頭函數

複製代碼
//ES5
$('#bt').click(function (e) {
    //doing something
})
//ES6
$('#bt').click(e => {
    //doing something
})
複製代碼

有點語法糖的感受,有一個很大不一樣的是,箭頭函數不具備this屬性,箭頭函數直接使用的是外部的this的做用域,這個想不想用看我的習慣吧。

參數新特性

ES6能夠爲參數提供默認屬性 

複製代碼
1 function log(x, y = 'World') {
2   console.log(x, y);
3 }
4 
5 log('Hello') // Hello World
6 log('Hello', 'China') // Hello China
7 log('Hello', '') // Hello
複製代碼

至於不定參數撒的,我這裏沒有多過多的使用,等項目遇到再說吧,若是研究的太細碎,反而不適合咱們開展工做。

let、const和var

以前的js世界裏,咱們定義變量都是使用的var,別說還真挺好用的,雖有會有一些問題,可是對於熟悉js特性的小夥伴都能很好的解決,通常記住:變量提高會解決絕大多數問題。

就能解決不少問題,並且真實項目中,咱們會會避免出現變量出現重名的狀況因此有時候你們面試題中看到的場景在實際工做中不多發生,只要不刻意臆想、製造一些難以判斷的場景,其實並不會出現多少BUG,不能由於想考察人家對語言特性的瞭解,就作一些容易容易忘掉的陷阱題。

不管如何,var 聲明的變量受到了必定詬病,事實上在強類型語言看來也確實是設計BUG,可是徹底廢棄var的使用顯然不是js該作的事情,這種狀況下出現了let關鍵詞。

let與var一致用以聲明變量,而且一切用var的地方均可以使用let替換,新的標準也建議你們不要再使用var了,let具備更好的做用域規則,也許這個規則是邊界更加清晰了:

複製代碼
{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1
複製代碼

這裏是一個經典的閉包問題:

複製代碼
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
複製代碼

由於i在全局範圍有效,共享同一個做用域,因此i就只有10了,爲了解決這個問題,咱們以前會引入閉包,產生新的做用域空間(好像學名是變量對象,我給忘了),可是那裏的i跟這裏的i已經不是一個東西了,但若是將var改爲let,上面的答案是符合預期的。能夠簡單理解爲每一次「{}」,let定義的變量都會產生新的做用域空間,這裏產生了循環,因此每一次都不同,這裏與閉包有點相似是開闢了不一樣的空間。

複製代碼
for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc
複製代碼

這裏由於內部從新聲明瞭i,事實上產生了3個做用域,這裏一共有4個做用域指向,let最大的做用就是js中塊級做用域的存在,而且內部的變量不會被外部所訪問,因此以前爲了防止變量侮辱的當即執行函數,彷佛變得不是那麼必要了。

以前咱們定義一個常量會採用所有大寫的方式:

var NUM = 10;

爲了解決這個問題,ES6引入了const命令,讓咱們定義只讀常量,這裏不對細節作過多研究,直接後續項目實踐吧,項目出真知。

生成器Generators

ES6中提出了生成器Generators的概念,這是一種異步編程的解決方案,能夠將其理解爲一種狀態機,封裝了多個內部狀態,這裏來個demo:

複製代碼
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
複製代碼

這個yield(產出)相似於以前的return,直觀的理解就是一個函數能夠返回屢次了,或者說函數具備「順序狀態」,yield提供了暫停功能。這裏我想寫個代碼來驗證下期中的做用域狀態:

複製代碼
function* test(){
    let i = 0;
    setTimeout(function() {
        i++;
    }, 1000);
    yield i;
    yield i++;
    return i
}

let t = test();
console.log(t.next());

setTimeout(function() {
    console.log(t.next());
}, 2000);
console.log(t.next());

//{value: 0, done: false}
//{value: 0, done: false}
//{value: 2, done: true}
複製代碼

以前咱們寫一個城市級聯的代碼,可能會有些使人蛋疼:

複製代碼
 1 $.get('getCity', {id: 0}, function(province) {
 2     let pid = province[0];
 3     //根據省id獲取城市數據
 4     $.get('getCity', {id: pid}, function(city) {
 5         let cityId = city[0];
 6         //根據市級id獲取縣
 7         $.get('getCity', {id: cityId}, function(city) {
 8             //do smt.
 9         });
10     });
11 });
複製代碼

這個代碼你們應當比較熟悉了,用promise能從語法層面解決一些問題,這裏簡單介紹下promise。

Promise

Promise是一種異步解決方案,有些同事認爲其出現就是爲了咱們代碼變得更好看,解決回調地獄的語法糖,ES6將其寫入了語音標準,提供了原生Promise對象。Promise爲一容器,裏面保存異步事件的結果,他是一個對象具備三個狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗),這裏仍是來個簡單代碼說明:

複製代碼
1 function timeout(ms) {
2   return new Promise((resolve, reject) => {
3     setTimeout(resolve, ms, 'done');
4   });
5 }
6 
7 timeout(100).then((value) => {
8   console.log(value);
9 });
複製代碼

實例化Promise時,第一個回調必須提供,是進行轉爲成功時候會執行,第二個也是一個函數失敗時候調用,非必須,這裏來個demo:

複製代碼
 1 let  timeout = function (ms) {
 2     return new Promise(function (resolve) {
 3         setTimeout(resolve, ms);
 4     });
 5 };
 6 
 7 timeout(1000).then(function () {
 8     return timeout(1000).then(function () {
 9         let s = '你們';
10         console.log(s)
11         return s;
12     })
13 
14 }).then(function (data) {
15     return timeout(1000).then(function () {
16         let s = data + '好,';
17         console.log(s)
18         return s;
19     })
20 }).then(function(data) {
21     return timeout(1000).then(function () {
22         let s = data + '我是葉小釵';
23         console.log(s)
24         return s;
25     });
26 }).then(function(data) {
27     console.log(data)
28 });
複製代碼

若是咱們請求有依賴的話,第一個請求依賴於第二個請求,代碼就能夠這樣寫:

複製代碼
 1 let getData = function(url, param) {
 2     return new Promise(function (resolve) {
 3         $.get(url, param, resolve );
 4     });
 5 }
 6 getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data) {
 7     console.log('我獲取了省數據,咱們立刻根據省數據申請市數據', data);
 8     return getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) {
 9         console.log(data1);
10         return '我是市數據';
11     })
12 
13 }).then(function(data) {
14     //前面的參數傳過來了
15     console.log(data);
16     console.log('我獲取了市數據,咱們立刻根據市數據申請縣數據');
17     getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) {
18         console.log(data1);
19     });
20 })
複製代碼

如此即可以免多層嵌套了,關於Promise的知識點還不少,咱們遇到複雜的工做場景再拿出來講吧,我對他的定位就是一個語法糖,將異步的方式變成同步的寫法,骨子裏仍是異步,上面咱們用Promise解決回調地獄問題,可是回調地獄問題遇到的很少,卻發現Promise一堆then看見就有點煩,咱們的Generator函數彷佛可讓這個狀況獲得緩解。

可是暫時在實際工做中我沒有找到更好的使用場景,這裏暫時到這裏,後面工做遇到再詳述,對這塊不是很熟悉也不妨礙咱們使用ES6寫代碼。

代理

代理,其實就是你要作什麼我幫你作了就好了,通常代理的緣由都是,我須要作點手腳,或者多點操做,或者作點「賦能」,如咱們經常包裝setTimeout通常:

1 let timeout = function (ms, callback) {
2     setTimeout(callback, ms);
3 }

咱們包裝setTimeout每每是爲了clearTimeout的時候能所有清理掉,其實就是攔截下,ES6提供了Proxy關鍵詞用於設置代理器:

複製代碼
 1 var obj = new Proxy({}, {
 2   get: function (target, key, receiver) {
 3     console.log(`getting ${key}!`);
 4     return Reflect.get(target, key, receiver);
 5   },
 6   set: function (target, key, value, receiver) {
 7     console.log(`setting ${key}!`);
 8     return Reflect.set(target, key, value, receiver);
 9   }
10 });
11 obj.count = 1
12 //  setting count!
13 ++obj.count
14 //  getting count!
15 //  setting count!
16 //  2
複製代碼
//target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定製攔截行爲
var proxy = new Proxy(target, handler);

咱們這裏繼續寫一個簡單的demo:

複製代碼
 1 let person = {
 2     constructor: function(name, age = 20) {
 3         this.name = name;
 4         this.age = age
 5     },
 6     addAge: function() {
 7         this.age++;
 8     },
 9     getAge: function() {
10         return this.age;
11     }
12 }
13 
14 var proxy = new Proxy(person, {
15     get: function(target, property) {
16         console.log(arguments);
17         return target[property];
18     },
19     set: function(target, property) {
20         console.log(arguments);
21     }
22 });
23 
24 person.constructor('葉小釵', 30);
25 console.log(person.age)
26 console.log(proxy.age)
複製代碼

可是暫時我沒有發現比較好的業務場景,好比說,我如今重寫了一個實例的get方法,便能在一個全局容器中記錄這個被執行了多少次,這裏一個業務場景是:我一次個頁面連續發出了不少次請求,可是我單頁應用作頁面跳轉時候,我須要將全部的請求句柄移除,這個彷佛也不是代理完成的工做,因而要使用ES6寫代碼,彷佛能夠暫時忽略代理。

結語

有了以上知識,基本從程序層面能夠使用ES6寫代碼了,可是工程層面還須要引入webpack等工具,這些咱們下次介紹吧。

 

 

【原創】淺談內存泄露

 

前言

這個話題已是老生常談了,之因此又被我拎出來,是由於博主隔壁的一個童鞋最近寫了一篇叫作《ThreadLocal內存泄露》的文章,我就不上連接了,由於寫的實在是。。(省略一萬字)
重點是寫完後,還被我問懵了。出於人道主義關懷,博主很不要臉的再寫一篇。

正文

定義

首先,咱們要先談一下定義,由於一堆人搞不懂內存溢出和內存泄露的區別。
內存溢出(OutOfMemory):你只有十塊錢,我卻找你要了一百塊。對不起啊,我沒有這麼多錢。(給不起)
內存泄露(MemoryLeak):你有十塊錢,我找你要一塊。可是無恥的博主,不把錢還你了。(沒退還)
關係:屢次的內存泄露,會致使內存溢出。(博主不要臉的找你多要幾回錢,你就沒錢了,就是這個道理。)

危害

ok,你們在項目中有沒遇到過java程序愈來愈卡的狀況。
由於內存泄露,會致使頻繁的Full GC,而Full GC 又會形成程序停頓,最後Crash了。所以,你會感受到你的程序愈來愈卡,愈來愈卡,而後你就被產品經理鄙視了。順便提一下,咱們之因此JVM調優,就是爲了減小Full GC的出現。
我記得,我曾經有一次,就遇到項目剛上線的時候好好的。結果隨着時間的堆積,報了OutOfMemoryError: PermGen space
說到這個PermGen space,忽然間,一陣洪荒之力,從博主體內噴涌而出,必定要介紹一下這個方法區,不過點到爲止,畢竟這不是在講《jvm從入門到放棄》。
方法區:出自java虛擬機規範, 可供各條線程共享運行時內存區域。它存儲了每個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。
上面講的是規範,在不一樣虛擬機裏頭實現是不同的,最典型的就是永久代(PermGen space)元空間(Metaspace)

jdk1.8之前:實現方法區的叫永久代。由於在好久遠之前,java以爲類幾乎是靜態的,而且不多被卸載和回收,因此給了一個永久代的雅稱。所以,若是你在項目中,發現堆和永久代一直在不斷增加,沒有降低趨勢,回收的速度根本趕不上增加的速度,不用說了,這種狀況基本能夠肯定是內存泄露。

jdk1.8之後:實現方法區的叫元空間。Java以爲對永久代進行調優是很困難的。永久代中的元數據可能會隨着每一次Full GC發生而進行移動。而且爲永久代設置空間大小也是很難肯定的。所以,java決定將類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。這樣,咱們就避開了設置永久代大小的問題。可是,這種狀況下,一旦發生內存泄露,會佔用你的大量本地內存。若是你發現,你的項目中本地內存佔用率異常高。嗯,這就是內存泄露了。

如何排查

(1)經過jps查找java進程id。
(2)經過top -p [pid]發現內存佔用達到了最大值
(3)jstat -gccause pid 20000 每隔20秒輸出Full GC結果
(4)發現Full GC次數太多,基本就是內存泄露了。生成dump文件,藉助工具分析是哪一個對象太多了。基本能定位到問題在哪。

實例

在stackoverflow上,有一個問題,以下所示

I just had an interview, and I was asked to create a memory leak with Java. Needless to say I felt pretty dumb having no clue on how to even start creating one.

大體就是,由於面試須要手寫一段內存泄露的程序,而後提問的人忽然懵逼了,因而不少大佬紛紛給出回答。
案例一
此例子出自《算法》(第四版)一書,我簡化了一下

class stack{ Object data[1000]; int top = 0; public void push(Object o){ data[top++] = o; } public Object pop(Object o){ return data[--top]; } }

當數據從棧裏面彈出來以後,data數組還一直保留着指向元素的指針。那麼就算你把棧pop空了,這些元素佔的內存也不會被回收的。
解決方案就是

public Object pop(Object o){ Object result = data[--top]; data[top] = null; return result; }

案例二
這個實際上是一堆例子,這些例子形成內存泄露的緣由都是相似的,就是不關閉流,具體的,能夠是文件流,socket流,數據庫鏈接流,等等
具體以下,沒關文件流

try { BufferedReader br = new BufferedReader(new FileReader(inputFile)); ... ... } catch (Exception e) { e.printStacktrace(); }

再好比,沒關閉鏈接

try { Connection conn = ConnectionFactory.getConnection(); ... ... } catch (Exception e) { e.printStacktrace(); }

解決方案就是。。。嗯,你們應該都會。。你敢說你不會調close()方法。
案例三
講這個例子前,你們對ThreadLocalTomcat中引發內存泄露有了解麼。不過,我要說一下,這個泄露問題,和ThreadLocal自己關係不大,我看了一下官網給的例子,基本都是屬於使用不當引發的。
在Tomcat的官網上,記錄了這個問題。地址是:https://wiki.apache.org/tomcat/MemoryLeakProtection
不過,官網的這個例子,可能很差理解,咱們略做改動。

public class HelloServlet extends HttpServlet{ private static final long serialVersionUID = 1L; static class LocalVariable { private Long[] a = new Long[1024 * 1024 * 100]; } final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { localVariable.set(new LocalVariable()); } }

再來看下conf下sever.xml配置

 <!--The connectors can use a shared executor, you can define one or more named thread pools--> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="150" minSpareThreads="4"/>

線程池最大線程爲150個,最小線程爲4個
Tomcat中Connector組件負責接受並處理請求,每來一個請求,就會去線程池中取一個線程。
在訪問該servlet時,ThreadLocal變量裏面被添加了new LocalVariable()實例,可是沒有被remove,這樣該變量就隨着線程回到了線程池中。另外屢次訪問該servlet可能用的不是工做線程池裏面的同一個線程,這會致使工做線程池裏面多個線程都會存在內存泄露。

另外,servletdoGet方法裏面建立new LocalVariable()的時候使用的是webappclassloader
那麼
LocalVariable對象沒有釋放 -> LocalVariable.class沒有釋放 -> webappclassloader沒有釋放 -> webappclassloader加載的全部類也沒有被釋放,也形成了內存泄露。

除此以外,你在eclipse中,作一個reload操做,工做線程池裏面的線程仍是一直存在的,而且線程裏面的threadLocal變量並無被清理。而reload的時候,又會新構建一個webappclassloader,重複上述步驟。多reload幾回,就內存溢出。
不過Tomcat7.0之後,你每作一次reload,會清理工做線程池中線程的threadLocals變量。所以,這個問題在tomcat7.0後,不會存在。

ps:ThreadLocal的使用在Tomcat的服務環境下要注意,並不是每次web請求時候程序運行的ThreadLocal都是惟一的。ThreadLocal的什麼生命週期不等於一次Request的生命週期。ThreadLocal與線程對象緊密綁定的,因爲Tomcat使用了線程池,線程是可能存在複用狀況。

 

Delegate, invoke,BeginInvoke Task,Task<t>, aysnc, await, Thread, Monitor,Mutex,

 

 

 

C#.NET使用Task,await,async,異步執行控件耗時事件(event),不阻塞UI線程和不跨線程執行UI更新,以及其餘方式比較

C#.NET使用Task,await,async,異步執行控件耗時事件(event),不阻塞UI線程和不跨線程執行UI更新,以及其餘方式比較

 
使用Task,await,async,異步執行事件(event),不阻塞UI線程和不跨線程執行UI更新

  使用Task,await,async 的異步模式 去執行事件(event) 解決不阻塞UI線程和不誇跨線程執行UI更新報錯的最佳實踐,附加幾種其餘方式比較

因爲是Winform代碼和其餘緣由,本文章只作代碼截圖演示,不作界面UI展現,固然全部代碼都會在截圖展現。

 

1:封裝異步按鈕(爲了比較放了3個按鈕)和進度條的控件,包含基本文件演示截圖

1.1 演示工程截圖 1.2按鈕和進度條控件演示 

 

2:定義異步委託和事件和幾種演示封裝

2.1 定義相關事件
解析:最前面的是普通的事件定義,後面2行是異步定義。

 

2.2 按鈕名稱[Task]執行普通異步Task

解析調用過程:當用戶點擊按鈕時會加載全部用戶註冊的事件進行多線程分發,單獨每個委託進行執行,最後單獨使用線程進行等待,這樣不阻塞UI線程。

可是用戶註冊的事件方法若是有更新UI會報錯,須要額外的Invoke進行處理。

 

 

2.3 按鈕名稱[BeginInvoke]執行普通異步

解析調用過程:這個調用過程和Task同樣,可是簡單,這個也能夠寫成多事件註冊,多多領會異步編程模型的好處(原理:異步執行,內部等待信號通知結束)。

 

 

2.4 (推薦)按鈕名稱[Task await]執行方便的異步耗時操做和簡單的UI

解析調用過程:推薦的方式附加調用流程

 這個全是優勢啊:代碼精簡,異步執行方法能夠像同步的方式來調用,用戶註冊的事件方法能夠隨意更新UI,無需invoke,稍微改造一下就能多事件註冊。

 

3:其餘用戶調用封裝好的異步按鈕執行耗時操做

 

 

總結

 

你們有時間的能夠本身根據截圖去敲打代碼試試,總結以下:

1.按鈕名稱[Task]   : 能夠實現多個事件註冊,可是代碼比較多,須要額外的線程等待來結束進度條,並且用戶註冊的事件的方法更新UI時會報錯,提示跨線程操做UI,須要invoke方法調用到UI線程執行。

2.按鈕名稱[BeginInvoke] :  簡單方便的異步編程模型,不須要額外的線程等待結束來結束進度條,缺點和按鈕名稱[Task]同樣,用戶註冊的事件的方法更新UI時會報錯,提示跨線程操做UI,須要invoke方法調用到UI線程執行.

3.按鈕名稱[Task await] : 稍微有一點點繞,可是簡單呀,不須要額外的線程等待UI更新進度條,像同步方法放在await後面便可,並且用戶註冊的事件方法 更新UI時不須要invoke方法回到UI線程執行。

 

Web前端,HTML5開發,前端資源,前端網址,前端博客,前端框架整理 - 轉改

 

 

 

Web前端,HTML5開發,前端資源,前端網址,前端博客,前端框架整理 - 轉改

 
Web前端/H5開發,前端資源,前端網址,前端博客,前端框架整理

 

  1. 綜合類

  2. 入門類

  3. 效果類

  4. 工具類

  5. 慕課專題

  6. 週刊類

API

 

1. 總目錄

  1. 開發中心

  2. 綜合搜索

  3. 綜合API

2. jQuery

3. Ecmascript

4. Js template

5. 彈出層

6. CSS

7. Angularjs

8. React

9. 移動端API

  1. API
  2. 框架

10. avalon

11. Requriejs

12. Seajs

13. Less,sass

14. Markdown

15. D3

16. 兼容性

17. UI相關

18. HTTP

19. 其它API

20. 圖表類

21. vue

21. 正則

22. ionic

23. 其它

面試題

 

字體庫
前端導航和網站
經常使用CDN
簡歷模版
  1. 生成

  2. 模板

 
 
 
 

HTML5 五子棋 - JS/Canvas 遊戲

 
背景介紹

  由於以前用c#的winform中的gdi+,java圖形包作過五子棋,因此作這個邏輯思路也就得心應手,然而最近想溫故html5的canvas繪圖功能(公司通常不用這些),因此作了個五子棋,固然沒參考以前的客戶端代碼,只用使用以前計算輸贏判斷算法和電腦AI(網絡借取)的算法,固然如今html5作的五子棋百度一下很是多,可是本身實現一邊總歸是好事情,好了廢話很少說了進入正題。^_^

界面功能介紹與後續可增長功能

 目前界面功能:

  主界面包含

    1:人人、人機對戰選項 2:棋子外觀選擇 3:棋盤背景選擇 4:棋盤線條顏色選擇

  遊戲界面包含

    1:玩家名稱 2:玩家棋子 3:當前由誰下棋背景定位 4:玩家比分 5:功能菜單區域(從新開始和無限悔棋) 6:棋盤區域 7.勝利後連環棋子鏈接 8.最後下棋位置閃爍顯示 9.光標定位

  遊戲結束界面

    1:勝利背景圖 2:勝利玩家姓名 3:繼續下一把按鈕

 可增長功能

  1.返回主界面 2.保存棋局和相關數據 3.讀取棋局和相關數據 4.交換角色 5.網絡對戰(2臺機器)6.雙方思考總時間記錄

演示地址

http://sandbox.runjs.cn/show/pl3fyuy4   (注意:沒有加棋子圖片下載提示,若是使用仿真棋子,出現下棋爲空,請等待棋子圖片下載完畢)

 

界面截圖賞析(沒花時間美化,比較醜)

 相關設置選擇    

  

總體設計和主代碼介紹

總體設計

  下棋流程:玩家or電腦AI下棋 ---> 繪製棋子 ---> 設定棋子二維座標值 ----> logic(邏輯判斷) ----> (玩家)一方五子連環 ---> 獲勝界面
      ↑                                          |
      |                                          ↓
      <--------------------------------------------------------------------------------------------沒有五子

 

  悔棋流程(人人對戰):一方玩家悔棋 ----> 彈出下棋記錄堆棧並設定爲它是最後一枚棋 ---> 清除最後一枚棋子圖像 ---> 清除棋子二維座標值---> 從新定位顯示最後下棋位置並閃爍

  悔棋流程(人機對戰):玩方悔棋 ----> 彈出下棋記錄堆棧2次,設定上一次電腦爲最後一枚棋 ---> 清除彈出的2次記錄圖像 ---> 清除棋子2個棋子二維座標值---> 從新定位顯示最後下棋位置並閃爍

 

主代碼介紹

   主代碼分爲二塊: 1.界面邏輯塊  2.遊戲主體塊 (界面與遊戲代碼分離,邏輯清晰,分工明確)

模擬事件通知:遊戲主體邏輯塊,每次結果都會通知到界面層來進行交互(相似於c#或者java的委託或事件)

界面邏輯代碼

複製代碼
  1 <script type="text/javascript">
  2         var gb = null;
  3         var infoboj = document.getElementsByClassName("info")[0];
  4         var pl1obj = document.getElementById("pl1");
  5         var pl2obj = document.getElementById("pl2");
  6         var plname1obj = document.getElementById("plname1");
  7         var plname2obj = document.getElementById("plname2");
  8         var chesstypeobj = document.getElementsByName("chesstype");
  9         var chesscolorobj = document.getElementsByName("chesscolor");
 10         var chessbgObj = document.getElementsByName("chessbg");
 11         var winerpnl = document.getElementById("winer");
 12         document.getElementById("startgame").addEventListener("click", function() {
 13             
 14             function initParams() {
 15                 var chessTypeValue = 1;
 16                 if (chesstypeobj.length > 0) {
 17                     for (var i = 0; i < chesstypeobj.length; i++) {
 18                         if (chesstypeobj[i].checked) {
 19                             chessTypeValue = chesstypeobj[i].value;
 20                             break;
 21                         }
 22                     }
 23                 }
 24                 var linevalue = "";
 25                 if (chesscolorobj.length > 0) {
 26                     for (var i = 0; i < chesscolorobj.length; i++) {
 27                         if (chesscolorobj[i].checked) {
 28                             linevalue = chesscolorobj[i].value;
 29                             break;
 30                         }
 31                     }
 32                 }
 33                 var bcorimgvalue = "";
 34                 if (chessbgObj.length > 0) {
 35                     for (var i = 0; i < chessbgObj.length; i++) {
 36                         if (chessbgObj[i].checked) {
 37                             bcorimgvalue = chessbgObj[i].value;
 38                             break;
 39                         }
 40                     }
 41                 }
 42                 return {
 43                     lineColor: linevalue,
 44                     chessType: chessTypeValue, //1 色彩棋子 2 仿真棋子
 45                     playAName: plname1Input.value,
 46                     playBName: plname2Input.value,
 47                     backColorORImg: bcorimgvalue,
 48                     playAImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playA.png",
 49                     playBImg: "http://sandbox.runjs.cn/uploads/rs/62/nbqodq5i/playB.png",
 50                     playerBIsComputer:openComputer.checked
 51                 };
 52             }
 53             document.getElementById("cc").style.display = "block";
 54             gb = new gobang(initParams());
 55             /**
 56              * 設置一些界面信息
 57              * @param {Object} opt
 58              */
 59             gb.info = function(opt) {
 60                     infoboj.style.visibility = "visible";
 61                     document.getElementsByClassName("startpnl")[0].style.visibility = "hidden";
 62                     plname1obj.innerHTML = opt.playAName;
 63                     plname2obj.innerHTML = opt.playBName;
 64                     if (opt.chessType == 1) {
 65                         var span1 = document.createElement("span");
 66                         pl1obj.insertBefore(span1, plname1obj);
 67                         var span2 = document.createElement("span");
 68                         pl2obj.insertBefore(span2, plname2obj);
 69                     } else {
 70                         var img1 = document.createElement("img");
 71                         img1.src = opt.playAImg;
 72                         pl1obj.insertBefore(img1, plname1obj);
 73                         var img2 = document.createElement("img");
 74                         img2.src = opt.playBImg;
 75                         pl2obj.insertBefore(img2, plname2obj);
 76                     }
 77                 }
 78                 /**
 79                  * 每次下棋後觸發事件 
 80                  * @param {Object} c2d
 81                  */
 82             gb.operate = function(opt, c2d) {
 83                 if (!c2d.winer || c2d.winer <= 0) {
 84                     pl1obj.removeAttribute("class", "curr");
 85                     pl2obj.removeAttribute("class", "curr");
 86                     if (c2d.player == 1) {
 87                         pl2obj.setAttribute("class", "curr");
 88                     } else {
 89                         pl1obj.setAttribute("class", "curr");
 90                     }
 91                     document.getElementById("backChessman").innerHTML="悔棋("+c2d.canBackTimes+")";
 92                 } else {
 93                     var winname = c2d.winer == 1 ? opt.playAName : opt.playBName;
 94                     var str = "恭喜,【" + winname + "】贏了!"
 95                     alert(str);
 96                     winerpnl.style.display = "block";
 97                     document.getElementById("winerName").innerHTML = "恭喜,【" + winname + "】贏了!";
 98                     document.getElementById("pl" + c2d.winer).style.backgroundColor = "pink";
 99                     document.getElementById("scoreA").innerHTML = c2d.playScoreA;
100                     document.getElementById("scoreB").innerHTML = c2d.playScoreB;
101                 }
102             }
103             gb.start();
104         });
105         
106         document.getElementById("openComputer").addEventListener("change", function() {
107             if (this.checked) {
108                 plname2Input.value = "電腦";
109                 plname2Input.disabled = "disabled";
110             } else {
111                 plname2Input.value = "玩家二";
112                 plname2Input.disabled = "";
113             }
114         });
115         
116         //document.getElementById("openComputer").checked="checked";
117         
118         //從新開始
119         function restartgui() {
120             if (gb) {
121                 winerpnl.style.display = "none";
122                 pl1obj.removeAttribute("class", "curr");
123                 pl2obj.removeAttribute("class", "curr");
124                 document.getElementById("pl1").style.backgroundColor = "";
125                 document.getElementById("pl2").style.backgroundColor = "";
126                 gb.restart();
127             }
128         };
129     </script>
複製代碼

 

遊戲主體代碼塊(只包含函數聲明代碼)

 

複製代碼
//  ========== 
//  =name:gobang 遊戲 
//  =anthor:jasnature
//  =last modify date:2016-04-13
//  ========== 
(function(win) {

    var gb = function(option) {

        var self = this,
            canObj = document.getElementById("cc"),
            can = canObj.getContext("2d");
        self.contextObj = canObj;
        self.context = can;

        if (!self.context) {
            alert("瀏覽器不支持html5");
            return;
        };

        self.Opt = {
            lineColor: "green",
            chessType: 1, //1 色彩棋子 2 仿真棋子
            playAName: "play1",
            playBName: "play2",
            playAColor: "red",
            playBColor: "blue",
            playAImg: "img/playA.png",
            playBImg: "img/playB.png",
            backColorORImg: "default",
            playerBIsComputer: false
        };

        self.operate;

        //合併屬性
        for (var a in option) {
            //console.log(opt[a]);
            self.Opt[a] = option[a];
        };

        //私有變量
        var my = {};
        my.enableCalcWeightNum = false; //顯示AI分數
        my.gameover = false;
        //棋盤相關
        my.baseWidth = 30;
        my.lastFocusPoint = {}; //鼠標最後移動到的座標點,計算後的
        my.cw = self.contextObj.offsetWidth; //棋盤寬
        my.ch = self.contextObj.offsetHeight; //高
        my.xlen = Math.ceil(my.cw / my.baseWidth); //行數
        my.ylen = Math.ceil(my.ch / my.baseWidth); //列
        my.chessRadius = 14; //棋子半徑
        my.playerBIsComputer = false; //棋手B是不是電腦
        my.ComputerThinking = false; //電腦是否在下棋
        my.goBackC2dIsComputer = false; //最後下棋是否爲電腦

        my.switcher = 1; //由誰下棋了 1-a 2-b or computer
        my.winer = -1; //贏家,值參考my.switcher
        my.playScoreA = 0;
        my.playScoreB = 0;
        //x,y 正方形數量(20*20)
        my.rectNum = my.xlen;
        //存儲已下的點
        my.rectMap = [];
        my.NO_CHESS = -1; //沒有棋子標識
        my.goBackC2d = {}; //最後下的數組轉換座標
        my.downChessmanStackC2d = []; // 記錄已下棋子的順序和位置,堆棧

        my.focusFlashInterval = null; //焦點閃爍線程
        my.focusChangeColors = ["red", "fuchsia", "#ADFF2F", "yellow", "purple", "blue"];
        my.eventBinded = false;
        my.currChessBackImg = null;
        my.currChessAImg = null;
        my.currChessBImg = null;
        my.currDrawChessImg = null;
        my.ChessDownNum = 0; //2個玩家 下棋總數

        /**
         * 開始遊戲
         */
        self.start = function() {
            
        };

        /**
         * 從新開始遊戲
         */
        self.restart = function() {

            
        };

        /**
         * 悔棋一步 ,清棋子,並返回上一次參數
         */
        self.back = function() {

            
        }

        /**
         * 初始化一些數據
         */
        function init() {

            
        }

        //        self.paint = function() {
        //
        //            //window.requestAnimationFrame(drawChessboard);
        //        };

        /**
         * 遊戲邏輯
         */
        function logic(loc, iscomputer) {
            

        };

        /**
         * 判斷是否有玩家勝出
         * @param {Object} c2d
         */
        function isWin(c2d) {
            

            return false;
        }

        /**
         * 鏈接贏家棋子線
         * @param {Object} points
         */
        function joinWinLine(points) {

            

        }

        /**
         * 畫棋盤
         */
        function drawChessboard() {
            
        };

        /**
         * 畫棋子
         * @param {Object} loc 鼠標點擊位置
         */
        function drawChessman(c2d) {

            
        }

        function drawRect(lastRecord, defColor) {
            
        }

        /**
         * 閃爍最後下棋點
         */
        function flashFocusChessman() {

            
        }

        /**
         * 清棋子
         * @param {Object} c2d
         */
        function clearChessman() {

            
        }

        /**
         * @param {Object} loc
         * @return {Object} I 二維數組橫點(),J二維數組縱點,IX 橫點起始座標,JY縱點起始座標,player 最後下棋玩, winer 贏家
         */
        function calc2dPoint(loc) {
            var txp = Math.floor(loc.x / my.baseWidth),
                typ = Math.floor(loc.y / my.baseWidth)
            dxp = txp * my.baseWidth, dyp = typ * my.baseWidth;

            loc.I = txp;
            loc.J = typ;
            loc.IX = dxp;
            loc.JY = dyp;

            return loc;
        }

        my.isChangeDraw = true;

        /**
         * 位置移動光標
         * @param {Object} loc
         */
        function moveFocus(loc) {
            
        }

        /**
         * 綁定事件
         */
        function bindEvent() {
            if (!my.eventBinded) {
                self.contextObj.addEventListener("touchstart", function(event) {
                    //console.log(event);
                    var touchObj = event.touches[0];
                    eventHandle({
                        s: "touch",
                        x: touchObj.clientX - this.offsetLeft,
                        y: touchObj.clientY - this.offsetTop
                    })
                });
                self.contextObj.addEventListener("click", function(event) {
                    //console.log("click event");
                    eventHandle({
                        s: "click",
                        x: event.offsetX,
                        y: event.offsetY
                    })
                });

                self.contextObj.addEventListener("mousemove", function(event) {
                    //console.log("mousemove event");
                    moveFocus({
                        x: event.offsetX,
                        y: event.offsetY
                    });

                });

                my.eventBinded = true;
            }

            function eventHandle(ps) {
                if (!my.gameover && !my.ComputerThinking) {
                    logic(ps);

                    if (my.playerBIsComputer && my.switcher == 2) {
                        my.ComputerThinking = true;
                        var pp = AI.analysis(my.goBackC2d.I, my.goBackC2d.J);
                        logic({
                            I: pp.x,
                            J: pp.y
                        }, true);
                        my.ComputerThinking = false;
                    }
                }
                event.preventDefault();
                event.stopPropagation();
                return false;
            }

        }


    };

    win.gobang = gb;

})(window);
複製代碼

 

主要算法介紹

玩家OR電腦勝出算法

複製代碼
/**
         * 判斷是否有玩家勝出
         * @param {Object} c2d
         */
        function isWin(c2d) {
            //四個放心計數 豎 橫 左斜 右斜
            var hcount = 0,
                vcount = 0,
                lbhcount = 0,
                rbhcount = 0,
                temp = 0;

            var countArray = [];

            //左-1
            for (var i = c2d.I; i >= 0; i--) {
                temp = my.rectMap[i][c2d.J];
                if (temp < 0 || temp !== c2d.player) {
                    break;
                }
                hcount++;
                countArray.push({
                    I: i,
                    J: c2d.J
                });
            }
            //右-1
            for (var i = c2d.I + 1; i < my.rectMap.length; i++) {
                temp = my.rectMap[i][c2d.J];
                if (temp < 0 || temp !== c2d.player) {
                    break;
                }
                hcount++;
                countArray.push({
                    I: i,
                    J: c2d.J
                });
            }

            if (countArray.length < 5) {
                countArray = [];
                //上-2
                for (var j = c2d.J; j >= 0; j--) {
                    temp = my.rectMap[c2d.I][j];
                    if (temp < 0 || temp !== c2d.player) {
                        break;
                    }
                    vcount++;

                    countArray.push({
                        I: c2d.I,
                        J: j
                    });
                }
                //下-2
                for (var j = c2d.J + 1; j < my.rectMap[c2d.I].length; j++) {
                    temp = my.rectMap[c2d.I][j];
                    if (temp < 0 || temp !== c2d.player) {
                        break;
                    }
                    vcount++;
                    countArray.push({
                        I: c2d.I,
                        J: j
                    });
                }
            }

            if (countArray.length < 5) {
                countArray = [];
                //左上
                for (var i = c2d.I, j = c2d.J; i >= 0, j >= 0; i--, j--) {
                    if (i < 0 || j < 0) break;
                    temp = my.rectMap[i][j];
                    if (temp < 0 || temp !== c2d.player) {
                        break;
                    }
                    lbhcount++;
                    countArray.push({
                        I: i,
                        J: j
                    });
                }
                //右下
                if (c2d.I < my.rectMap.length - 1 && c2d.I < my.rectMap[0].length - 1) {
                    for (var i = c2d.I + 1, j = c2d.J + 1; i < my.rectMap.length, j < my.rectMap[0].length; i++, j++) {
                        if (i >= my.rectMap.length || j >= my.rectMap.length) break;
                        temp = my.rectMap[i][j];
                        if (temp < 0 || temp !== c2d.player) {
                            break;
                        }
                        lbhcount++;
                        countArray.push({
                            I: i,
                            J: j
                        });
                    }
                }
            }
            if (countArray.length < 5) {
                countArray = [];
                //右上
                for (var i = c2d.I, j = c2d.J; i < my.rectMap.length, j >= 0; i++, j--) {
                    if (i >= my.rectMap.length || j < 0) break;
                    temp = my.rectMap[i][j];
                    if (temp < 0 || temp !== c2d.player) {
                        break;
                    }
                    rbhcount++;
                    countArray.push({
                        I: i,
                        J: j
                    });
                }
                //左下
                if (c2d.I >= 1 && c2d.J < my.rectMap[0].length - 1) {
                    for (var i = c2d.I - 1, j = c2d.J + 1; i > 0, j < my.rectMap[0].length; i--, j++) {
                        if (j >= my.rectMap.length || i < 0) break;
                        temp = my.rectMap[i][j];
                        if (temp < 0 || temp !== c2d.player) {
                            break;
                        }
                        rbhcount++;
                        countArray.push({
                            I: i,
                            J: j
                        });
                    }
                }
            }

            if (hcount >= 5 || vcount >= 5 || lbhcount >= 5 || rbhcount >= 5) {
                my.winer = c2d.player;
                my.gameover = true;

                joinWinLine(countArray);

                return true;
            }

            return false;
        }
複製代碼

算法簡介:主要思路是搜索最後落下棋子的位置(二維座標)計算 米  字形線座標,看是否有連續5個或以上棋子出現。

 

鏈接贏家棋子線

複製代碼
/**
         * 鏈接贏家棋子線
         * @param {Object} points
         */
        function joinWinLine(points) {

            points.sort(function(left, right) {
                return (left.I + left.J) > (right.I + right.J);
            });

            var startP = points.shift();
            var endP = points.pop();
            var poffset = my.baseWidth / 2;
            can.strokeStyle = "#FF0000";
            can.lineWidth = 2;
            can.beginPath();
            var spx = startP.I * my.baseWidth + poffset,
                spy = startP.J * my.baseWidth + poffset;
            can.arc(spx, spy, my.baseWidth / 4, 0, 2 * Math.PI, false);
            can.moveTo(spx, spy);
            var epx = endP.I * my.baseWidth + poffset,
                epy = endP.J * my.baseWidth + poffset;
            can.lineTo(epx, epy);
            can.moveTo(epx + my.baseWidth / 4, epy);
            can.arc(epx, epy, my.baseWidth / 4, 0, 2 * Math.PI, false);

            can.closePath();
            can.stroke();

        }
複製代碼

算法簡介:根據贏家返回的連子位置集合,作座標大小位置排序,直接使用lineto 鏈接 第一個棋子和最後一個

 

座標換算

複製代碼
/**
         * 座標換算
         * @param {Object} loc
         * @return {Object} I 二維數組橫點(),J二維數組縱點,IX 橫點起始座標,JY縱點起始座標,player 最後下棋玩, winer 贏家
         */
        function calc2dPoint(loc) {
            var txp = Math.floor(loc.x / my.baseWidth),
                typ = Math.floor(loc.y / my.baseWidth)
            dxp = txp * my.baseWidth, dyp = typ * my.baseWidth;

            loc.I = txp;
            loc.J = typ;
            loc.IX = dxp;
            loc.JY = dyp;

            return loc;
        }
複製代碼

算法簡介:這個比較簡單,根據每一個格子的寬度計算出實際座標

 

電腦AI主要代碼(修改來源於網絡)

複製代碼
/**
         * AI棋型分析 
         */
        AI.analysis = function(x, y) {
            //若是爲第一步則,在玩家棋周圍一格隨機下棋,保證每一局棋第一步都不同
            if (my.ChessDownNum == 1) {
                return this.getFirstPoint(x, y);
            }
            var maxX = 0,
                maxY = 0,
                maxWeight = 0,
                i, j, tem;

            for (i = BOARD_SIZE - 1; i >= 0; i--) {
                for (j = BOARD_SIZE - 1; j >= 0; j--) {
                    if (my.rectMap[i][j] !== -1) {
                        continue;
                    }
                    tem = this.computerWeight(i, j, 2);
                    if (tem > maxWeight) {
                        maxWeight = tem;
                        maxX = i;
                        maxY = j;

                    }
                    if (my.enableCalcWeightNum) {
                        can.clearRect(i * 30 + 2, j * 30 + 2, 24, 24);
                        can.fillText(maxWeight, i * 30 + 5, j * 30 + 15, 30);
                    }
                }
            }
            return new Point(maxX, maxY);
        };
        //下子到i,j X方向 結果: 多少連子 兩邊是否截斷
        AI.putDirectX = function(i, j, chessColor) {
            var m, n,
                nums = 1,
                side1 = false, //兩邊是否被截斷
                side2 = false;
            for (m = j - 1; m >= 0; m--) {
                if (my.rectMap[i][m] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[i][m] === my.NO_CHESS) {
                        side1 = true; //若是爲空子,則沒有截斷
                    }
                    break;
                }
            }
            for (m = j + 1; m < BOARD_SIZE; m++) {
                if (my.rectMap[i][m] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[i][m] === my.NO_CHESS) {
                        side2 = true;
                    }
                    break;
                }
            }
            return {
                "nums": nums,
                "side1": side1,
                "side2": side2
            };
        };
        //下子到i,j Y方向 結果
        AI.putDirectY = function(i, j, chessColor) {
            var m, n,
                nums = 1,
                side1 = false,
                side2 = false;
            for (m = i - 1; m >= 0; m--) {
                if (my.rectMap[m][j] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][j] === my.NO_CHESS) {
                        side1 = true;
                    }
                    break;
                }
            }
            for (m = i + 1; m < BOARD_SIZE; m++) {
                if (my.rectMap[m][j] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][j] === my.NO_CHESS) {
                        side2 = true;
                    }
                    break;
                }
            }
            return {
                "nums": nums,
                "side1": side1,
                "side2": side2
            };
        };
        //下子到i,j XY方向 結果
        AI.putDirectXY = function(i, j, chessColor) {
            var m, n,
                nums = 1,
                side1 = false,
                side2 = false;
            for (m = i - 1, n = j - 1; m >= 0 && n >= 0; m--, n--) {
                if (my.rectMap[m][n] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][n] === my.NO_CHESS) {
                        side1 = true;
                    }
                    break;
                }
            }
            for (m = i + 1, n = j + 1; m < BOARD_SIZE && n < BOARD_SIZE; m++, n++) {
                if (my.rectMap[m][n] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][n] === my.NO_CHESS) {
                        side2 = true;
                    }
                    break;
                }
            }
            return {
                "nums": nums,
                "side1": side1,
                "side2": side2
            };
        };
        AI.putDirectYX = function(i, j, chessColor) {
            var m, n,
                nums = 1,
                side1 = false,
                side2 = false;
            for (m = i - 1, n = j + 1; m >= 0 && n < BOARD_SIZE; m--, n++) {
                if (my.rectMap[m][n] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][n] === my.NO_CHESS) {
                        side1 = true;
                    }
                    break;
                }
            }
            for (m = i + 1, n = j - 1; m < BOARD_SIZE && n >= 0; m++, n--) {
                if (my.rectMap[m][n] === chessColor) {
                    nums++;
                } else {
                    if (my.rectMap[m][n] === my.NO_CHESS) {
                        side2 = true;
                    }
                    break;
                }
            }
            return {
                "nums": nums,
                "side1": side1,
                "side2": side2
            };
        };

        /**
         * 計算AI下棋權重 
         * chessColor 玩家1爲玩家2爲AI
         */
        AI.computerWeight = function(i, j, chessColor) {
            //基於棋盤位置權重(越靠近棋盤中心權重越大)
            var weight = 19 - (Math.abs(i - 19 / 2) + Math.abs(j - 19 / 2)),
                pointInfo = {}; //某點下子後連子信息

            //x方向
            pointInfo = this.putDirectX(i, j, chessColor);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重
            pointInfo = this.putDirectX(i, j, chessColor - 1);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重
            //y方向
            pointInfo = this.putDirectY(i, j, chessColor);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重
            pointInfo = this.putDirectY(i, j, chessColor - 1);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重
            //左斜方向
            pointInfo = this.putDirectXY(i, j, chessColor);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重
            pointInfo = this.putDirectXY(i, j, chessColor - 1);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重
            //右斜方向
            pointInfo = this.putDirectYX(i, j, chessColor);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, true); //AI下子權重
            pointInfo = this.putDirectYX(i, j, chessColor - 1);
            weight += this.weightStatus(pointInfo.nums, pointInfo.side1, pointInfo.side2, false); //player下子權重
            return weight;
        };
        //權重方案   活:兩邊爲空可下子,死:一邊爲空
        //其實還有不少種方案,這種是最簡單的
        AI.weightStatus = function(nums, side1, side2, isAI) {
            var weight = 0;
            switch (nums) {
                case 1:
                    if (side1 && side2) {
                        weight = isAI ? 15 : 10; //一
                    }
                    break;
                case 2:
                    if (side1 && side2) {
                        weight = isAI ? 100 : 50; //活二
                    } else if (side1 || side2) {
                        weight = isAI ? 10 : 5; //死二
                    }
                    break;
                case 3:
                    if (side1 && side2) {
                        weight = isAI ? 500 : 200; //活三
                    } else if (side1 || side2) {
                        weight = isAI ? 30 : 20; //死三
                    }
                    break;
                case 4:
                    if (side1 && side2) {
                        weight = isAI ? 5000 : 2000; //活四
                    } else if (side1 || side2) {
                        weight = isAI ? 400 : 100; //死四
                    }
                    break;
                case 5:
                    weight = isAI ? 100000 : 10000; //五
                    break;
                default:
                    weight = isAI ? 500000 : 250000;
                    break;
            }
            return weight;
        };
複製代碼

AI分析:這個只是最簡單的算法,其實很簡單,計算每一個沒有下棋座標的分數,也是按照  字形 計算,計算格子8個方向出現的 一個棋子 二個棋子 三個棋子 四個棋子,其中還分爲是否被截斷,其實就是邊緣是否被堵死。

 

其實這個AI算法後續還有不少能夠優化,好比 斷跳 二活 其實就是2個交叉的 活二  , 由於是斷掉的因此沒有歸入算法權重計算,若是加入這個算法,估計很難下贏電腦了。

如符號圖:

*        *
*      *
空位         
下這裏

由於不是連續的,全部沒有歸入。

 

開源與下載

 http://jasnature.github.io/gobang_html5/

有興趣的能夠下載修改並提交代碼進來^_^

 
 

meta 詳解,html5 meta 標籤平常設置

 
複製代碼
<!DOCTYPE html> <!-- 使用 HTML5 doctype,不區分大小寫 -->
<html lang="zh-cmn-Hans"> <!-- 更加標準的 lang 屬性寫法 http://zhi.hu/XyIa -->
<head>
    <!-- 聲明文檔使用的字符編碼 -->
    <meta charset='utf-8'>
    <!-- 優先使用 IE 最新版本和 Chrome -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <!-- 頁面描述 -->
    <meta name="description" content="不超過150個字符"/>
    <!-- 頁面關鍵詞 -->
    <meta name="keywords" content=""/>
    <!-- 網頁做者 -->
    <meta name="author" content="name, email@gmail.com"/>
    <!-- 搜索引擎抓取 -->
    <meta name="robots" content="index,follow"/>
    <!-- 爲移動設備添加 viewport -->
    <meta name="viewport" content="initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no">
    <!-- `width=device-width` 會致使 iPhone 5 添加到主屏後以 WebApp 全屏模式打開頁面時出現黑邊 http://bigc.at/ios-webapp-viewport-meta.orz -->
 
    <!-- iOS 設備 begin -->
    <meta name="apple-mobile-web-app-title" content="標題">
    <!-- 添加到主屏後的標題(iOS 6 新增) -->
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <!-- 是否啓用 WebApp 全屏模式,刪除蘋果默認的工具欄和菜單欄 -->
 
    <meta name="apple-itunes-app" content="app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL">
    <!-- 添加智能 App 廣告條 Smart App Banner(iOS 6+ Safari) -->
    <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
    <!-- 設置蘋果工具欄顏色 -->
    <meta name="format-detection" content="telphone=no, email=no"/>
    <!-- 忽略頁面中的數字識別爲電話,忽略email識別 -->
    <!-- 啓用360瀏覽器的極速模式(webkit) -->
    <meta name="renderer" content="webkit">
    <!-- 避免IE使用兼容模式 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 針對手持設備優化,主要是針對一些老的不識別viewport的瀏覽器,好比黑莓 -->
    <meta name="HandheldFriendly" content="true">
    <!-- 微軟的老式瀏覽器 -->
    <meta name="MobileOptimized" content="320">
    <!-- uc強制豎屏 -->
    <meta name="screen-orientation" content="portrait">
    <!-- QQ強制豎屏 -->
    <meta name="x5-orientation" content="portrait">
    <!-- UC強制全屏 -->
    <meta name="full-screen" content="yes">
    <!-- QQ強制全屏 -->
    <meta name="x5-fullscreen" content="true">
    <!-- UC應用模式 -->
    <meta name="browsermode" content="application">
    <!-- QQ應用模式 -->
    <meta name="x5-page-mode" content="app">
    <!-- windows phone 點擊無高光 -->
    <meta name="msapplication-tap-highlight" content="no">
    <!-- iOS 圖標 begin -->
    <link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-57x57-precomposed.png"/>
    <!-- iPhone 和 iTouch,默認 57x57 像素,必須有 -->
    <link rel="apple-touch-icon-precomposed" sizes="114x114" href="/apple-touch-icon-114x114-precomposed.png"/>
    <!-- Retina iPhone 和 Retina iTouch,114x114 像素,能夠沒有,但推薦有 -->
    <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144x144-precomposed.png"/>
    <!-- Retina iPad,144x144 像素,能夠沒有,但推薦有 -->
    <!-- iOS 圖標 end -->
 
    <!-- iOS 啓動畫面 begin -->
    <link rel="apple-touch-startup-image" sizes="768x1004" href="/splash-screen-768x1004.png"/>
    <!-- iPad 豎屏 768 x 1004(標準分辨率) -->
    <link rel="apple-touch-startup-image" sizes="1536x2008" href="/splash-screen-1536x2008.png"/>
    <!-- iPad 豎屏 1536x2008(Retina) -->
    <link rel="apple-touch-startup-image" sizes="1024x748" href="/Default-Portrait-1024x748.png"/>
    <!-- iPad 橫屏 1024x748(標準分辨率) -->
    <link rel="apple-touch-startup-image" sizes="2048x1496" href="/splash-screen-2048x1496.png"/>
    <!-- iPad 橫屏 2048x1496(Retina) -->
 
    <link rel="apple-touch-startup-image" href="/splash-screen-320x480.png"/>
    <!-- iPhone/iPod Touch 豎屏 320x480 (標準分辨率) -->
    <link rel="apple-touch-startup-image" sizes="640x960" href="/splash-screen-640x960.png"/>
    <!-- iPhone/iPod Touch 豎屏 640x960 (Retina) -->
    <link rel="apple-touch-startup-image" sizes="640x1136" href="/splash-screen-640x1136.png"/>
    <!-- iPhone 5/iPod Touch 5 豎屏 640x1136 (Retina) -->
    <!-- iOS 啓動畫面 end -->
 
    <!-- iOS 設備 end -->
    <meta name="msapplication-TileColor" content="#000"/>
    <!-- Windows 8 磁貼顏色 -->
    <meta name="msapplication-TileImage" content="icon.png"/>
    <!-- Windows 8 磁貼圖標 -->
 
    <link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml"/>
    <!-- 添加 RSS 訂閱 -->
    <link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
    <!-- 添加 favicon icon -->
 
    <title>標題</title>
</head>
複製代碼

 另外,建議X-UA這樣寫

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />






C#中回滾TransactionScope的使用方法和原理

TransactionScope只要一個操做失敗,它會自動回滾,Complete表示事務完成

 

實事上,一個錯誤的理解就是Complete()方法是提交事務的,這是錯誤的,事實上,它的做用的表示本事務完成,它通常放在try{}的結尾處,不用判斷前臺操做是否成功,若是不成功,它會本身回滾。

 

 

在.net 1.1的時代,尚未TransactionScope類,所以不少關於事務的處理,都交給了SqlTransaction和SqlConnection,每一個Transaction是基於每一個Connection的。這種設計對於跨越多個程序集或者多個方法的事務行爲來講,不是很是好,須要把事務和數據庫鏈接做爲參數傳入。

在.net 2.0後,TransactionScope類的出現,大大的簡化了事務的設計。示例代碼以下:

  1.  
    static void Main(string[] args)
  2.  
    {
  3.  
    using (TransactionScope ts = new TransactionScope())
  4.  
    {
  5.  
    userBLL u =  new userBLL();
  6.  
    TeacherBLL t =  new TeacherBLL();
  7.  
    u.ADD();
  8.  
    t.ADD();
  9.  
    ts.Complete();
  10.  
    }
  11.  
    }

只須要把須要事務包裹的邏輯塊寫在using (TransactionScope ts = new TransactionScope())中就能夠了。從這種寫法能夠看出,TransactionScope實現了IDispose接口。除非顯示調用ts.Complete()方法。不然,系統不會自動提交這個事務。若是在代碼運行退出這個block後,還未調用Complete(),那麼事務自動回滾了。在這個事務塊中,u.ADD()方法和t.ADD()方法內部都沒有用到任何事務類。

TransactionScope是基於當前線程的,在當前線程中,調用Transaction.Current方法能夠看到當前事務的信息。具體關於TransactionScope的使用方法,已經它的成員方法和屬性,能夠查看 MSDN 。

TransactionScope類是能夠嵌套使用,若是要嵌套使用,須要在嵌套事務塊中指定TransactionScopeOption參數。默認的這個參數爲Required。

該參數的具體含義能夠參考http://msdn.microsoft.com/zh-cn/library/system.transactions.transactionscopeoption(v=vs.80).aspx

好比下面代碼:

  1.  
    static void Main(string[] args)
  2.  
    {
  3.  
    using (TransactionScope ts = new TransactionScope())
  4.  
    {
  5.  
    Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
  6.  
    userBLL u =  new userBLL();
  7.  
    TeacherBLL t =  new TeacherBLL();
  8.  
    u.ADD();
  9.  
    using (TransactionScope ts2 = new TransactionScope(TransactionScopeOption.Required))
  10.  
    {
  11.  
    Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
  12.  
    t.ADD();
  13.  
    ts2.Complete();
  14.  
    }
  15.  
    ts.Complete();
  16.  
    }
  17.  
    }

當嵌套類的TransactionScope的TransactionScopeOption爲Required的時候,則能夠看到以下結果,他們的事務的ID都是同一個。而且,只有當2個TransactionScope都complete的時候才能算真正成功。



ED8FDA3E241D48B0A90F30A5AC8A9A59

若是把TransactionScopeOption設爲RequiresNew,則嵌套的事務塊和外層的事務塊各自獨立,互不影響。

  1.  
    static void Main(string[] args)
  2.  
    {
  3.  
    using (TransactionScope ts = new TransactionScope())
  4.  
    {
  5.  
    Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
  6.  
    userBLL u =  new userBLL();
  7.  
    TeacherBLL t =  new TeacherBLL();
  8.  
    u.ADD();
  9.  
    using (TransactionScope ts2 = new TransactionScope(TransactionScopeOption.RequiresNew))
  10.  
    {
  11.  
    Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
  12.  
    t.ADD();
  13.  
    ts2.Complete();
  14.  
    }
  15.  
    ts.Complete();
  16.  
    }
  17.  
    }

22D29B0134CD4993B629F085EEBF63D9

能夠看到,他們的事務id是不同的。

 

 

TransactionScopeOption的屬性值:

 

對於多個不一樣服務器之間的數據庫操做,TransactionScope依賴DTC(Distributed Transaction Coordinator)服務完成事務一致性。

可是對於單一服務器數據,TransactionScope的機制則比較複雜。主要用的的是線程靜態特性。線程靜態特性ThreadStaticAttribute讓CLR知道,它標記的靜態字段的存取是依賴當前線程,而獨立於其餘線程的。既然存儲在線程靜態字段中的數據只對存儲該數據的同一線程中所運行的代碼可見,那麼,可以使用此類字段將其餘數據從一個方法傳遞到該第一個方法所調用的其餘方法,並且徹底不用擔憂其餘線程會破壞它的工做。TransactionScope 會將當前的 Transaction 存儲到線程靜態字段中。當稍後實例化 SqlCommand 時(在此 TransactionScope 從線程局部存儲中刪除以前),該 SqlCommand 會檢查線程靜態字段以查找現有 Transaction,若是存在則列入該 Transaction 中。經過這種方式,TransactionScope 和 SqlCommand 可以協同工做,從而開發人員沒必要將 Transaction 顯示傳遞給 SqlCommand 對象。實際上,TransactionScope 和 SqlCommand 所使用的機制很是複雜。

相關文章
相關標籤/搜索