【微信小程序项目实践总结】30分钟从陌生到熟悉
前言
我们之前对小程序做了基本学习:
- 1. 微信小程序开发07-列表页面怎么做
- 2. 微信小程序开发06-一个业务页面的完成
- 3. 微信小程序开发05-日历组件的实现
- 4. 微信小程序开发04-打造自己的UI库
- 5. 微信小程序开发03-这是一个组件
- 6. 微信小程序开发02-小程序基本介绍
- 7. 微信小程序开发01-小程序的执行流程是怎么样的?
阅读本文之前,如果大家想对小程序有更深入的了解,或者一些细节的了解可以先阅读上述文章,本文后面点需要对着代码调试阅读
对应的github地址是:https://github.com/yexiaochai/wxdemo
首先我们来一言以蔽之,什么是微信小程序?PS:这个问题问得好像有些扯:)
小程序是一个不需要下载安装就可使用的应用,它实现了应用触手可及的梦想,用户扫一扫或者搜一下即可打开应用。也体现了用完即走的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载。从字面上看小程序具有类似Web应用的热部署能力,在功能上又接近于原生APP。
所以说,其实微信小程序是一套超级Hybrid的解决方案,现在看来,小程序应该是应用场景最广,也最为复杂的解决方案了。
很多公司都会有自己的Hybrid平台,我这里了解到比较不错的是携程的Hybrid平台、阿里的Weex、百度的糯米,但是从应用场景来说都没有微信来得丰富,这里根本的区别是:
微信小程序是给各个公司开发者接入的,其他公司平台多是给自己业务团队使用,这一根本区别,就造就了我们看到的很多小程序不一样的特性:
① 小程序定义了自己的标签语言WXML
② 小程序定义了自己的样式语言WXSS
③ 小程序提供了一套前端框架包括对应Native API
④ 禁用浏览器Dom API(这个区别,会影响我们的代码方式)
只要了解到这些区别就会知道为什么小程序会这么设计:
因为小程序是给各个公司的开发做的,其他公司的Hybrid方案是给公司业务团队用的,一般拥有Hybrid平台的公司实力都不错但是开发小程序的公司实力良莠不齐,所以小程序要做绝对的限制,最大程度的保证框架层(小程序团队)对程序的控制因为毕竟程序运行在微信这种体量的APP中
之前我也有一个疑惑为什么微信小程序会设计自己的标签语言,也在知乎看到各种各样的回答,但是如果出于设计层面以及应用层面考虑的话:这样会有更好的控制,而且我后面发现微信小程序事实上依旧使用的是webview做渲染(这个与我之前认为微信是NativeUI是向左的),但是如果我们使用的微信限制下面的标签,这个是有限的标签,后期想要换成NativeUI会变得更加轻易:
另一方面,经过之前的学习,我这边明确可以得出一个感受:
① 小程序的页面核心是标签,标签是不可控制的(我暂时没用到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层。
小程序打包后的结构(这里就真的不懂了,引用:小程序底层框架实现原理解析):
所有的小程序基本都最后都被打成上面的结构
1、WAService.js 框架JS库,提供逻辑层基础的API能力
2、WAWebview.js 框架JS库,提供视图层基础的API能力
3、WAConsole.js 框架JS库,控制台
4、app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型
5、app-service.js 我们自己的JS代码,全部打包到这个文件
6、page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里
7、pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域
从设计的角度上说,小程序采用的组件化开发的方案,除了页面级别的标签,后面全部是组件,而组件中的标签view、data、js的关系应该是与page是一致的,这个也是我们平时建议的开发方式,将一根页面拆分成一个个小的业务组件或者UI组件:
从我写业务代码过程中,觉得整体来说还是比较顺畅的,小程序是有自己一套完整的前端框架的,并且释放给业务代码的主要就是page,而page只能使用标签和组件,所以说框架的对业务的控制力度很好。
最后我们从工程角度来看微信小程序的架构就更加完美了,小程序从三个方面考虑了业务者的感受:
① 开发工具+调试工具
② 开发基本模型(开发基本标准WXML、WXSS、JS、JSON)
③ 完善的构建(对业务方透明)
④ 自动化上传离线包(对业务费透明离线包逻辑)
⑤ 监控统计逻辑
所以,微信小程序从架构上和使用场景来说是很令人惊艳的,至少惊艳了我......所以我们接下来在开发层面对他进行更加深入的剖析,我们这边最近一直在做基础服务,这一切都是为了完善技术体系,这里对于前端来说便是我们需要做一个Hybrid体系,如果做App,React Native也是不错的选择,但是一定要有完善的分层:
① 底层框架解决开发效率,将复杂的部分做成一个黑匣子,给页面开发展示的只是固定的三板斧,固定的模式下开发即可
② 工程部门为业务开发者封装最小化开发环境,最优为浏览器,确实不行便为其提供一个类似浏览器的调试环境
如此一来,业务便能快速迭代,因为业务开发者写的代码大同小异,所以底层框架配合工程团队(一般是同一个团队),便可以在底层做掉很多效率性能问题。
稍微大点的公司,稍微宽裕的团队,还会同步做很多后续的性能监控、错误日志工作,如此形成一套文档->开发->调试->构建->发布->监控、分析 为一套完善的技术体系
如果形成了这么一套体系,那么后续就算是内部框架更改、技术革新,也是在这个体系上改造,这块微信小程序是做的非常好的。但很可惜,很多其他公司团队只会在这个路径上做一部分,后面由于种种原因不在深入,有可能是感觉没价值,而最恐怖的行为是,自己的体系没形成就贸然的换基础框架,戒之慎之啊!好了闲话少说,我们继续接下来的学习。
我对小程序的理解有限,因为没有源码只能靠经验猜测,如果文中有误,请各位多多提点
文章更多面对初中级选手,如果对各位有用,麻烦点赞哟
微信小程序的执行流程
微信小程序为了对业务方有更强的控制,App层做的工作很有限,我后面写demo的时候根本没有用到app.js,所以我这里认为app.js只是完成了一个路由以及初始化相关的工作,这个是我们看得到的,我们看不到的是底层框架会根据app.json的配置将所有页面js都准备好。
我这里要表达的是,我们这里配置了我们所有的路由:
"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
文件,借助工具分析是哪个对象太多了。基本能定位到问题在哪。
实例
在*上,有一个问题,如下所示
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 所使用的机制非常复杂。