前言
咱們以前對小程序作了基本學習:javascript
- 1. 微信小程序開發07-列表頁面怎麼作
- 2. 微信小程序開發06-一個業務頁面的完成
- 3. 微信小程序開發05-日曆組件的實現
- 4. 微信小程序開發04-打造本身的UI庫
- 5. 微信小程序開發03-這是一個組件
- 6. 微信小程序開發02-小程序基本介紹
- 7. 微信小程序開發01-小程序的執行流程是怎麼樣的?
閱讀本文以前,若是你們想對小程序有更深刻的瞭解,或者一些細節的瞭解能夠先閱讀上述文章,本文後面點須要對着代碼調試閱讀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的圖標
有這麼幾點緣由:
- 數據能夠被搜索引擎的爬蟲抓到,並進行索引。若是產品只有一個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開發,有更好的用戶體驗,也能夠更方便的拓展其餘功能
參考閱讀:
做者:產品新人學習路
連接: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()
方法。
案例三
講這個例子前,你們對ThreadLocal
在Tomcat
中引發內存泄露有了解麼。不過,我要說一下,這個泄露問題,和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
可能用的不是工做線程池裏面的同一個線程,這會致使工做線程池裏面多個線程都會存在內存泄露。
另外,servlet
的doGet
方法裏面建立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更新報錯的最佳實踐,附加幾種其餘方式比較
因爲是Winform代碼和其餘緣由,本文章只作代碼截圖演示,不作界面UI展現,固然全部代碼都會在截圖展現。
1.1 演示工程截圖 1.2按鈕和進度條控件演示
2.1 定義相關事件
解析:最前面的是普通的事件定義,後面2行是異步定義。
2.2 按鈕名稱[Task]執行普通異步Task
解析調用過程:當用戶點擊按鈕時會加載全部用戶註冊的事件進行多線程分發,單獨每個委託進行執行,最後單獨使用線程進行等待,這樣不阻塞UI線程。
可是用戶註冊的事件方法若是有更新UI會報錯,須要額外的Invoke進行處理。
2.3 按鈕名稱[BeginInvoke]執行普通異步
解析調用過程:這個調用過程和Task同樣,可是簡單,這個也能夠寫成多事件註冊,多多領會異步編程模型的好處(原理:異步執行,內部等待信號通知結束)。
2.4 (推薦)按鈕名稱[Task await]執行方便的異步耗時操做和簡單的UI
解析調用過程:推薦的方式附加調用流程
這個全是優勢啊:代碼精簡,異步執行方法能夠像同步的方式來調用,用戶註冊的事件方法能夠隨意更新UI,無需invoke,稍微改造一下就能多事件註冊。
你們有時間的能夠本身根據截圖去敲打代碼試試,總結以下:
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前端開發大系概覽
- Web前端開發大系概覽-中文版
- Web Front-end Stack v2.2
- 免費的編程中文書籍索引
- 前端書籍
- 前端免費書籍大全
- 前端知識體系
- 免費的編程中文書籍索引
- 智能社 - 精通JavaScript開發
- 從新介紹 JavaScript(JS 教程)
- 麻省理工學院公開課:計算機科學及編程導論
- JavaScript中的this陷阱的最全收集--沒有之一
- JS函數式編程指南
- JavaScript Promise迷你書(中文版)
- 騰訊移動Web前端知識庫
- Front-End-Develop-Guide 前端開發指南
- 前端開發筆記本
- 大前端工具集 - 聶微東
- 前端開發者手冊
-
入門類
-
效果類
-
工具類
-
慕課專題
-
週刊類
1. 總目錄
-
開發中心
- mozilla js參考
- chrome開發中心(chrome的內核已轉向blink)
- safari開發中心
- http://msdn.microsoft.com/zh-cn/library/d1et7k7c(v=vs.94).aspx">microsoft js參考
- js祕密花園
- js祕密花園
- w3help 綜合Bug集合網站
-
綜合搜索
-
綜合API
- runoob.com-包含各類API集合
- 開源中國在線API文檔合集
- devdocs 英文綜合API網站
2. jQuery
3. Ecmascript
- Understanding ECMAScript 6 - Nicholas C. Zakas
- exploring-es6
- exploring-es6翻譯
- exploring-es6翻譯後預覽
- 阮一峯 es6
- 阮一峯 Javascript
- ECMA-262,第 5 版
- es5
4. Js template
- template-chooser
- artTemplate
- tomdjs
- 淘寶模板juicer模板
- Fxtpl v1.0 繁星前端模板引擎
- laytpl
- mozilla - nunjucks
- Juicer
- dustjs
- etpl
- twitter-tpl
5. 彈出層
6. CSS
- CSS 語法參考
- CSS3動畫手冊
- 騰訊css3動畫製做工具
- 志爺css小工具集合
- css3 js 移動大雜燴
- bouncejs 觸摸庫
- css3 按鈕動畫
- animate.css
- 全局CSS的終結(狗帶) [譯]
7. Angularjs
- Angular.js 的一些學習資源
- angularjs中文社區
- Angular Style Guide
- Angularjs源碼學習
- Angularjs源碼學習
- angular對bootstrap的封裝
- angularjs + nodejs
- 呂大豹 Angularjs
- AngularJS 最佳實踐
- Angular的一些擴展指令
- Angular數據綁定原理
- 一些擴展Angular UI組件
- Ember和AngularJS的性能測試
- 帶你走近AngularJS - 基本功能介紹
- Angularjs開發指南
- Angularjs學習
- 不要帶着jQuery的思惟去學習AngularJS
- angularjs 學習筆記
- angularjs 開發指南
- angularjs 英文資料
- angular bootstrap
- angular jq mobile
- angular ui
- 整合jQuery Mobile+AngularJS經驗談
- 有jQuery背景,該如何用AngularJS編程思想
- AngularJS在線教程
- angular學習筆記
8. React
- react海量資源
- react.js 中文論壇
- react.js 官方網址
- react.js 官方文檔
- react.js material UI
- react.js TouchstoneJS UI
- react.js amazeui UI
- React 入門實例教程 - 阮一峯
- React Native 中文版
- Webpack 和 React 小書 - 前端亂燉
- Webpack 和 React 小書 - gitbook
- React原創實戰視頻教程
- React 入門教程
- react-webpack-starter
- 基於react組件化開發
9. 移動端API
- API
- 框架
10. avalon
11. Requriejs
- Javascript模塊化編程(一):模塊的寫法
- Javascript模塊化編程(二):AMD規範
- Javascript模塊化編程(三):require.js的用法
- RequireJS入門(一)
- RequireJS入門(二)
- RequireJS進階(三)
- requrie源碼學習
- requrie 入門指南
- requrieJS 學習筆記
- requriejs 其一
- require backbone結合
12. Seajs
13. Less,sass
14. Markdown
- Markdown 語法說明 (簡體中文版)
- markdown入門參考
- gitbook 國外的在線markdown可編輯成書
- mdeditor 一款國內的在線markdown編輯器
- stackedit 國外的在線markdown編輯器,功能強大,同步雲盤
- mditor 一款輕量級的markdown編輯器
- lepture-editor
- markdown-editor
15. D3
16. 兼容性
- esma 兼容列表
- W3C CSS驗證服務
- caniuse
- csscreator
- http://msdn.microsoft.com/zh-cn/library/cc351024(v=vs.85).aspx">microsoft
- 在線測兼容-移動端
- emulators
17. UI相關
- bootcss
- MetroUICSS
- semantic
- Buttons
- kitecss
- pintuer
- amazeui
- worldhello
- linuxtoy
- gitmagic
- rogerdudler
- gitref
- book
- gogojimmy
18. HTTP
19. 其它API
- javascript流行庫彙總
- 驗證api
- underscore 中文手冊
- underscore源碼分析
- underscore源碼分析-亞里士朱德的博客
- underscrejs en api
- lodash - underscore的代替品
- ext4api
- backbone 中文手冊
- qwrap手冊
- 緩動函數
- svg 中文參考
- svg mdn參考
- svg 導出 canvas
- svg 導出 png
- ai-to-svg
- localStorage 庫
20. 圖表類
21. vue
21. 正則
22. ionic
23. 其它
- 那幾個月在找工做(百度,網易遊戲)
- 2014最新面試題
- 名企筆試大全
- 阿里前端面試題
- 2016校招內推 -- 阿里巴巴前端 -- 三面面試經歷
- 騰訊面試題
- 年後跳槽那點事:樂視+金山+360面試之行
- 阿里前端面試題上線
- 拉勾網js面試題
- 前端面試
- Web開發筆試面試題 大全
- 前端開發面試題
- 2014最新前端面試題
- 百度面試
- 面試題
- 前端工做面試問題
- 前端開發面試題
- 5個經典的前端面試問題
- 最全前端面試問題及答案總結
- 如何面試一名前端開發工程師?
- 史上最全 前端開發面試問題及答案整理
- 前端實習生面試總結
- 史上最全 前端開發面試問題及答案整理
- BAT及各大互聯網公司2014前端筆試面試題:JavaScript篇
- 前端開發面試題大收集
- 收集的前端面試題和答案
- 如何面試前端工程師
- 前端開發面試題
- 牛客網-筆試面經
- 新浪CDN
- 百度靜態資源公共庫
- 360網站衛士經常使用前端公共庫CDN服務
- Bootstrap中文網開源項目免費 CDN 服務
- 開放靜態文件 CDN - 七牛
- CDN加速 - jq22
- jQuery CDN
- Google jQuery CDN
- 微軟CDN
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類的出現,大大的簡化了事務的設計。示例代碼以下:
-
static void Main(string[] args)
-
{
-
using (TransactionScope ts = new TransactionScope())
-
{
-
userBLL u = new userBLL();
-
TeacherBLL t = new TeacherBLL();
-
u.ADD();
-
t.ADD();
-
ts.Complete();
-
}
-
}
只須要把須要事務包裹的邏輯塊寫在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
好比下面代碼:
-
static void Main(string[] args)
-
{
-
using (TransactionScope ts = new TransactionScope())
-
{
-
Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
-
userBLL u = new userBLL();
-
TeacherBLL t = new TeacherBLL();
-
u.ADD();
-
using (TransactionScope ts2 = new TransactionScope(TransactionScopeOption.Required))
-
{
-
Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
-
t.ADD();
-
ts2.Complete();
-
}
-
ts.Complete();
-
}
-
}
當嵌套類的TransactionScope的TransactionScopeOption爲Required的時候,則能夠看到以下結果,他們的事務的ID都是同一個。而且,只有當2個TransactionScope都complete的時候才能算真正成功。
若是把TransactionScopeOption設爲RequiresNew,則嵌套的事務塊和外層的事務塊各自獨立,互不影響。
-
static void Main(string[] args)
-
{
-
using (TransactionScope ts = new TransactionScope())
-
{
-
Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
-
userBLL u = new userBLL();
-
TeacherBLL t = new TeacherBLL();
-
u.ADD();
-
using (TransactionScope ts2 = new TransactionScope(TransactionScopeOption.RequiresNew))
-
{
-
Console.WriteLine(Transaction.Current.TransactionInformation.LocalIdentifier);
-
t.ADD();
-
ts2.Complete();
-
}
-
ts.Complete();
-
}
-
}
能夠看到,他們的事務id是不同的。
TransactionScopeOption的屬性值:
對於多個不一樣服務器之間的數據庫操做,TransactionScope依賴DTC(Distributed Transaction Coordinator)服務完成事務一致性。
可是對於單一服務器數據,TransactionScope的機制則比較複雜。主要用的的是線程靜態特性。線程靜態特性ThreadStaticAttribute讓CLR知道,它標記的靜態字段的存取是依賴當前線程,而獨立於其餘線程的。既然存儲在線程靜態字段中的數據只對存儲該數據的同一線程中所運行的代碼可見,那麼,可以使用此類字段將其餘數據從一個方法傳遞到該第一個方法所調用的其餘方法,並且徹底不用擔憂其餘線程會破壞它的工做。TransactionScope 會將當前的 Transaction 存儲到線程靜態字段中。當稍後實例化 SqlCommand 時(在此 TransactionScope 從線程局部存儲中刪除以前),該 SqlCommand 會檢查線程靜態字段以查找現有 Transaction,若是存在則列入該 Transaction 中。經過這種方式,TransactionScope 和 SqlCommand 可以協同工做,從而開發人員沒必要將 Transaction 顯示傳遞給 SqlCommand 對象。實際上,TransactionScope 和 SqlCommand 所使用的機制很是複雜。