前言
咱们以前对小程序作了基本学习: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 所使用的机制很是复杂。