前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[Element-UI表格组件行点击事件处...]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
转载文章
...案开启的种类和使用的处理程序等更多资讯。 Hex2dec 十六进位数字和十进位数字相互转换。 Junction 建立 Win2K NTFS 符号连结。 LDMDump 倾印逻辑磁碟管理员的磁碟上之资料库内容,其中描述 Windows 2000 动态磁碟分割。 ListDLLs 列出所有目前载入的 DLL,包括载入位置和他们的版本编号。2.0 版列印载入模组的完整路径名称。 LiveKd 使用 Microsoft 核心侦错工具检视即时系统。 LoadOrder 检视在您 WinNT/2K 系统上载入装置的顺序。 LogonSessions 列出系统上的作用中登入工作阶段。 MoveFile 允许您对下一次开机进行移动和删除命令的排程。 NTFSInfo 使用 NTFSInfo 检视详细的 NTFS 磁碟区资讯,包括主档案表格 (MFT) 和 MFT 区的大小和位置,还有 NTFS 中继资料档案的大小。 PageDefrag 将您的分页档和登录 Hive 进行磁碟重组。 PendMoves 列举档案重新命名的清单,删除下次开机将会执行的命令。 Portmon 使用这个进阶的监视工具进行监视序列和平行连接埠活动。它不仅掌握所有标准的序列和平行 IOCTL,甚至会显示传送和接收的资料部份。Version 3.x 具有强大的新 UI 增强功能和进阶的筛选功能。 Process Monitor 即时监控档案系统、登录、程序、执行绪和 DLL 活动。 procexp 任务管理器,这个管理器比windows自带的管理器要强大方便的很多,建议替换自带的任务管理器(本人一直用这个管理器,很不错)。此工具也有汉化版,fans可以自己搜索下载 ProcFeatures 这个小应用程式会描述「实体位址扩充」的处理器和 Windows 支援,而没「没有执行」缓冲区溢位保护。 PsExec 以有限的使用者权限执行处理程序。 PsFile 检视远端开启档案有哪些。 PsGetSid 显示电脑或使用者的 SID。 PsInfo 取得有关系统的资讯。 PsKill 终止本机或远端处理程序。 PsList 显示处理程序和执行绪的相关资讯。 PsLoggedOn 显示使用者登录至一个系统。 PsLogList 倾印事件记录档的记录。 PsPasswd 变更帐户密码。 PsService 检视及控制服务。 PsShutdown 关机及选择重新启动电脑。 PsSuspend 暂停及继续处理程序。 PsTools PsTools 产品系列包括命令列公用程式,其功能有列出在本机或远端电脑上执行的处理程序、远端执行的处理程序、重新开机的电脑和倾印事件记录等等。 RegDelNull 扫描并删除登录机码,这些登录机码包括了标准登录编辑工具无法删除的内嵌式 Null 字元。 RegHide 建立名为 "HKEY_LOCAL_MACHINE\Software\Sysinternals\Can't touch me!\0" 并使用原生 API 的金钥,而且会在此金钥内建立一个值。 Regjump 跳至您在 Regedit 中指定的登录路径。 Regmon 这个监视工具让您即时看到全部的登录活动。 RootkitRevealer 扫描您系统上 Rootkit 为基础的恶意程式码。 SDelete 以安全的方法覆写您的机密档案,并且清除因先前使用这个 DoD 相容安全删除程式所删除档案後而释放的可用空间。包括完整的原始程式码。 ShareEnum 扫描网路上档案共用并检视其安全性设定,来关闭安全性漏洞。 Sigcheck 倾印档案版本资讯和验证系统上的影像皆已完成数位签章。 Strings 搜寻 binaryimages 中的 ANSI 和 UNICODE 字串。 Sync 将快取的资料清除至磁碟。 TCPView 作用中的通讯端命令列检视器。 VolumeId 设定 FAT 或 NTFS 磁碟区 ID。 Whois 看看谁拥有一个网际网路位址。 Winobj 最完整的物件管理员命名空间检视器在此。 ZoomIt 供萤幕上缩放和绘图的简报公用程式。 转自:http://www.360doc.com/content/15/0323/06/20545288_457293504.shtml 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_33515088/article/details/80721846。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2024-01-22 15:44:41
102
转载
Docker
...ner的Web界面,点击“Add Container”,然后填写几个基本信息即可: yaml image: mysql:5.7 name: my-mysql ports: - "3306:3306" volumes: - /data/mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: rootpassword 这段YAML配置文件描述了一个MySQL容器的基本参数。Portainer会自动帮你解析并生成对应的Docker命令。是不是超方便? 另外,Portainer还有一个特别棒的功能——实时监控。你打开页面就能看到每个“小房子”(就是容器)里用掉的CPU和内存情况,而且还能像穿越空间一样,去访问别的机器上跑着的那些“小房子”(Docker实例)。这种功能对于运维人员来说简直是福音! --- 4. Rancher 企业级的容器编排利器 如果你是一个团队协作的开发者,或者正在运营一个大规模的服务集群,那么Rancher可能是你的最佳选择。它不仅仅是一个Docker管理工具,更是一个完整的容器编排平台。 Rancher的核心优势在于它的“多集群管理”能力。想象一下,你的公司有好几台服务器,分别放在地球上的不同角落,有的在美国,有的在欧洲,还有的在中国。每台服务器上都跑着各种各样的服务,比如网站、数据库啥的。这时候,Rancher就派上用场了!它就像一个超级贴心的小管家,让你不用到处切换界面,在一个地方就能轻松搞定所有服务器和服务的管理工作,省时又省力! 举个例子,如果你想在Rancher中添加一个新的节点,只需要几步操作即可完成: 1. 登录Rancher控制台。 2. 点击“Add Cluster”按钮。 3. 输入目标节点的信息(IP地址、SSH密钥等)。 4. 等待几分钟,Rancher会自动为你安装必要的组件。 一旦节点加入成功,你就可以直接在这个界面上部署应用了。比如,用Kubernetes部署一个Redis集群: bash kubectl create deployment redis --image=redis:alpine kubectl expose deployment redis --type=LoadBalancer --port=6379 虽然这条命令看起来很简单,但它背后实际上涉及到了复杂的调度逻辑和网络配置。而Rancher把这些复杂的事情封装得很好,让我们可以专注于业务本身。 --- 5. Traefik 反向代理与负载均衡的最佳拍档 最后要介绍的是Traefik,这是一个轻量级的反向代理工具,专门用来处理HTTP请求的转发和负载均衡。它最厉害的地方啊,就是能跟Docker完美地融为一体,还能根据容器上的标签,自动调整路由规则呢! 比如说,你有两个服务分别监听在8080和8081端口,现在想通过一个域名访问它们。只需要给这两个容器加上相应的标签: yaml labels: - "traefik.enable=true" - "traefik.http.routers.service1.rule=Host(service1.example.com)" - "traefik.http.services.service1.loadbalancer.server.port=8080" - "traefik.http.routers.service2.rule=Host(service2.example.com)" - "traefik.http.services.service2.loadbalancer.server.port=8081" 这样一来,当用户访问service1.example.com时,Traefik会自动将请求转发到监听8080端口的容器;而访问service2.example.com则会指向8081端口。这种方式不仅高效,还极大地减少了配置的工作量。 --- 6. 总结 找到最适合自己的工具 好了,到这里咱们已经聊了不少关于服务器管理工具的话题。从Docker到Portainer,再到Rancher和Traefik,每一种工具都有其独特的优势和适用场景。 我的建议是,先根据自己的需求确定重点。要是你只想弄个小玩意儿,图个省事儿快点搞起来,那用Docker配个Portainer就完全够用了。但要是你们团队一起干活儿,或者要做大范围的部署,那Rancher这种专业的“老司机工具”就得安排上啦! 当然啦,技术的世界永远没有绝对的答案。其实啊,很多时候你会发现,最适合你的工具不一定是最火的那个,而是那个最合你心意、用起来最顺手的。就像穿鞋一样,别人觉得好看的根本不合脚,而那双不起眼的小众款却让你走得又稳又舒服!所以啊,在用这些工具的时候,别光顾着看,得多动手试试,边用边记下自己的感受和想法,这样你才能真的搞懂它们到底有啥门道! 好了,今天的分享就到这里啦!如果你还有什么问题或者想法,欢迎随时留言交流哦~咱们下次再见啦!
2025-04-16 16:05:13
97
月影清风_
转载文章
...ent.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.position = "absolute"; //相对于背景绝对定位temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function refresh(){ //重绘画板,每次鼠标点击后刷新for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){squareSet[i][j].style.backgroundImage = "url(./pic/" + squareSet[i][j].num + ".png)"squareSet[i][j].style.left = squareSet[i][j].col squareWidth + "px"; // 别忘了加"px"squareSet[i][j].style.bottom = squareSet[i][j].row squareWidth + "px";} }}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面for(var i = 0 ; i < boardWidth; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 效果 第二阶段:鼠标选中后,闪烁 只有JavaScript需要修改 var table; //游戏桌面var squareWidth = 50; //方块宽高var boardWidth = 10; //行列数var squareSet = []; //方块信息集合(二维数组)每个元素保存该方块的全部信息var baseScore = 5; //第一块的分数var stepScore = 10; //每多一块的累加分数var totalScore = 0; //当前总分var targetScore = 1500; //目标分var choose = []; //选中的连通小方块var timer = null; //闪烁定时器var flag = true; //锁,防止点击事件中响应其他点击或移入时间var tempSquare = null; //临时方块function refresh(){for (var i = 0; i < squareSet.length; i++) {for (var j = 0; j < squareSet[i].length; j++) {squareSet[i][j].style.background="url(pic/"+squareSet[i][j].num+".png)"squareSet[i][j].style.left=squareSet[i][j].colsquareWidth+"px";squareSet[i][j].style.bottom=squareSet[i][j].rowsquareWidth+"px";} }}function createSquare(value,row,col){ //创建小方块,传入参数为颜色、行、列,初始化时使用。var temp = document.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.position = "absolute"; //相对于背景绝对定位temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function goBack(){ //还原样式if(timer != null){ //清空计时器clearInterval(timer);}for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){squareSet[i][j].style.border = "0px solid white";squareSet[i][j].style.transform = "scale(0.95)";} }}function checkLinked(square , arr){ // 递归连通图算法arr.push(square); // 将当前方块放入选中数组中// check leftif( square.col > 0 && //未到边界squareSet[square.row][square.col - 1].num == square.num && //颜色相同arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循环判断checkLinked(squareSet[square.row][square.col - 1] , arr);}// check rightif( square.col < boardWidth - 1 &&squareSet[square.row][square.col + 1].num == square.num &&arr.indexOf(squareSet[square.row][square.col + 1]) == -1) {checkLinked(squareSet[square.row][square.col + 1] , arr);}// check upif( square.row < boardWidth - 1 &&squareSet[square.row + 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row + 1][square.col]) == -1) {checkLinked(squareSet[square.row + 1][square.col] , arr);}// check downif( square.row > 0 &&squareSet[square.row - 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row - 1][square.col]) == -1) {checkLinked(squareSet[square.row - 1][square.col] , arr);} }function flicker(arr){ // 选中连通的小方块可以闪烁var num = 0;timer = setInterval(function(){for(var i = 0 ; i < arr.length ; i ++){arr[i].style.border = "3px solid BFEFFF";//有个框arr[i].style.transform = "scale(" + (0.9 + (0.05 Math.pow(-1 , num))) + ")";//一闪一闪}num ++; // 注意这里所采用的数学技巧,仍然使用transform:scale(val)来进行缩放。},300);//闪烁的时间}function mouseOver(obj){ //鼠标移入区域响应// 还原所有样式goBack();// 检查相邻choose = [];checkLinked(obj , choose);// 闪烁flicker(choose);// 显示分数selectScore();}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //显示目标分数用innerHTML// 循环初始化星星区域for(var i = 0 ; i < boardWidth ; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);// 鼠标移入事件square.onmouseover = function(){mouseOver(this);}squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 效果2.1 加入这段代码,便会计算闪烁方块得分 function selectScore(){ //可以显示当前选中小方块的得分var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points";document.getElementById('selectScore').style.opacity = 1;// 设置时间间隔1秒后显示消失的过渡动画setTimeout(function(){document.getElementById('selectScore').style.opacity = 0;document.getElementById('selectScore').style.transition = "opacity 1s";},1000);} 完整代码为: var table; //游戏桌面var squareWidth = 50; //方块宽高var boardWidth = 10; //行列数var squareSet = []; //方块信息集合(二维数组)每个元素保存该方块的全部信息var baseScore = 5; //第一块的分数var stepScore = 10; //每多一块的累加分数var totalScore = 0; //当前总分var targetScore = 1500; //目标分var choose = []; //选中的连通小方块var timer = null; //闪烁定时器var flag = true; //锁,防止点击事件中响应其他点击或移入时间var tempSquare = null; //临时方块function refresh(){for (var i = 0; i < squareSet.length; i++) {for (var j = 0; j < squareSet[i].length; j++) {squareSet[i][j].style.background="url(pic/"+squareSet[i][j].num+".png)"squareSet[i][j].style.left=squareSet[i][j].colsquareWidth+"px";squareSet[i][j].style.bottom=squareSet[i][j].rowsquareWidth+"px";} }}function createSquare(value,row,col){ //创建小方块,传入参数为颜色、行、列,初始化时使用。var temp = document.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.position = "absolute"; //相对于背景绝对定位temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function goBack(){ //还原样式if(timer != null){ //清空计时器clearInterval(timer);}for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){squareSet[i][j].style.border = "0px solid white";squareSet[i][j].style.transform = "scale(0.95)";} }}function checkLinked(square , arr){ // 递归连通图算法arr.push(square); // 将当前方块放入选中数组中// check leftif( square.col > 0 && //未到边界squareSet[square.row][square.col - 1].num == square.num && //颜色相同arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循环判断checkLinked(squareSet[square.row][square.col - 1] , arr);}// check rightif( square.col < boardWidth - 1 &&squareSet[square.row][square.col + 1].num == square.num &&arr.indexOf(squareSet[square.row][square.col + 1]) == -1) {checkLinked(squareSet[square.row][square.col + 1] , arr);}// check upif( square.row < boardWidth - 1 &&squareSet[square.row + 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row + 1][square.col]) == -1) {checkLinked(squareSet[square.row + 1][square.col] , arr);}// check downif( square.row > 0 &&squareSet[square.row - 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row - 1][square.col]) == -1) {checkLinked(squareSet[square.row - 1][square.col] , arr);} }function flicker(arr){ // 选中连通的小方块可以闪烁var num = 0;timer = setInterval(function(){for(var i = 0 ; i < arr.length ; i ++){arr[i].style.border = "3px solid BFEFFF";//有个框arr[i].style.transform = "scale(" + (0.9 + (0.05 Math.pow(-1 , num))) + ")";//一闪一闪}num ++; // 注意这里所采用的数学技巧,仍然使用transform:scale(val)来进行缩放。},300);//闪烁的时间}function selectScore(){ //可以显示当前选中小方块的得分var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points";document.getElementById('selectScore').style.opacity = 1;// 设置时间间隔1秒后显示消失的过渡动画setTimeout(function(){document.getElementById('selectScore').style.opacity = 0;document.getElementById('selectScore').style.transition = "opacity 1s";},1000);}function mouseOver(obj){ //鼠标移入区域响应// 还原所有样式goBack();// 检查相邻choose = [];checkLinked(obj , choose);// 闪烁flicker(choose);// 显示分数selectScore();}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //显示目标分数用innerHTML// 循环初始化星星区域for(var i = 0 ; i < boardWidth ; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);// 鼠标移入事件square.onmouseover = function(){mouseOver(this);}squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 效果2.2 第三阶段:消灭星星(只消灭一次) 只消除选中的星星,但是不会掉下来。 在function init(){}里面添加以下代码: // 鼠标点击事件square.onclick = function(){//为移除增加一个延迟动画,为了防止闭包,这里采用立即执行函数for(var i = 0 ; i < choose.length ; i ++){(function(i){setTimeout(function(){squareSet[choose[i].row][choose[i].col] = null; //为状态数组置空table.removeChild(choose[i]); //将其从桌面上移除} , i 100);})(i);} } 效果 使得星星移动(原作者这里出现错误) function move(){//纵向下落,采用快慢指针算法for(var i = 0 ; i < boardWidth ; i ++){var pointer = 0; //慢指针for(var j = 0 ; j < boardWidth ; j ++){if(squareSet[j][i] != null){ //按行遍历if(pointer != j){ //快慢指针不同步说明中间有空元素squareSet[pointer][i] = squareSet[j][i]; //慢指针设成快指针元素squareSet[j][i].row = pointer;squareSet[j][i] = null; //快指针处置空}pointer ++; //该行非空时慢指针增加} }} 完整代码如下: var table; //游戏桌面var squareWidth = 50; //方块宽高var boardWidth = 10; //行列数var squareSet = []; //方块信息集合(二维数组)每个元素保存该方块的全部信息var baseScore = 5; //第一块的分数var stepScore = 10; //每多一块的累加分数var totalScore = 0; //当前总分var targetScore = 1500; //目标分var choose = []; //选中的连通小方块var timer = null; //闪烁定时器var flag = true; //锁,防止点击事件中响应其他点击或移入时间var tempSquare = null; //临时方块function refresh(){for (var i = 0; i < squareSet.length; i++) {for (var j = 0; j < squareSet[i].length; j++) {squareSet[i][j].style.background="url(pic/"+squareSet[i][j].num+".png)"squareSet[i][j].style.left=squareSet[i][j].colsquareWidth+"px";squareSet[i][j].style.bottom=squareSet[i][j].rowsquareWidth+"px";} }}function createSquare(value,row,col){ //创建小方块,传入参数为颜色、行、列,初始化时使用。var temp = document.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.position = "absolute"; //相对于背景绝对定位temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function goBack(){ //还原样式if(timer != null){ //清空计时器clearInterval(timer);}for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){squareSet[i][j].style.border = "0px solid white";squareSet[i][j].style.transform = "scale(0.95)";} }}function checkLinked(square , arr){ // 递归连通图算法arr.push(square); // 将当前方块放入选中数组中// check leftif( square.col > 0 && //未到边界squareSet[square.row][square.col - 1].num == square.num && //颜色相同arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循环判断checkLinked(squareSet[square.row][square.col - 1] , arr);}// check rightif( square.col < boardWidth - 1 &&squareSet[square.row][square.col + 1].num == square.num &&arr.indexOf(squareSet[square.row][square.col + 1]) == -1) {checkLinked(squareSet[square.row][square.col + 1] , arr);}// check upif( square.row < boardWidth - 1 &&squareSet[square.row + 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row + 1][square.col]) == -1) {checkLinked(squareSet[square.row + 1][square.col] , arr);}// check downif( square.row > 0 &&squareSet[square.row - 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row - 1][square.col]) == -1) {checkLinked(squareSet[square.row - 1][square.col] , arr);} }function flicker(arr){ // 选中连通的小方块可以闪烁var num = 0;timer = setInterval(function(){for(var i = 0 ; i < arr.length ; i ++){arr[i].style.border = "3px solid BFEFFF";//有个框arr[i].style.transform = "scale(" + (0.9 + (0.05 Math.pow(-1 , num))) + ")";//一闪一闪}num ++; // 注意这里所采用的数学技巧,仍然使用transform:scale(val)来进行缩放。},300);//闪烁的时间}function selectScore(){ //可以显示当前选中小方块的得分var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points";document.getElementById('selectScore').style.opacity = 1;// 设置时间间隔1秒后显示消失的过渡动画setTimeout(function(){document.getElementById('selectScore').style.opacity = 0;document.getElementById('selectScore').style.transition = "opacity 1s";},1000);}function mouseOver(obj){ //鼠标移入区域响应// 还原所有样式goBack();// 检查相邻choose = [];checkLinked(obj , choose);// 闪烁flicker(choose);// 显示分数selectScore();}function move(){//纵向下落,采用快慢指针算法for(var i = 0 ; i < boardWidth ; i ++){var pointer = 0; //慢指针for(var j = 0 ; j < boardWidth ; j ++){if(squareSet[j][i] != null){ //按行遍历if(pointer != j){ //快慢指针不同步说明中间有空元素squareSet[pointer][i] = squareSet[j][i]; //慢指针设成快指针元素squareSet[j][i].row = pointer;squareSet[j][i] = null; //快指针处置空}pointer ++; //该行非空时慢指针增加} }}// 横向移动(当出现一列为空时)for(var i = 0 ; i < squareSet[0].length ;){ //必须注意循环结束条件的判断if(squareSet[0][i] == null){ //逻辑:只需判断最低层为空,该行则全为空for(var j = 0 ; j < boardWidth ; j ++){squareSet[j].splice(i , 1); //splice删除数组squareSet[j]中从i开始的1个元素}continue;//注意移动后i不应改变了}i ++;}refresh();}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //显示目标分数用innerHTML// 循环初始化星星区域for(var i = 0 ; i < boardWidth ; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);// 鼠标移入事件square.onmouseover = function(){mouseOver(this);}// 鼠标点击事件square.onclick = function(){//对锁进行控制if(!flag || choose.length == null){return;}flag = false;tempSquare = null;//更新分数var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}totalScore += score;document.getElementById('nowScore').innerHTML = "Current Score : " + totalScore;//为移除增加一个延迟动画,为了防止闭包,这里采用立即执行函数for(var i = 0 ; i < choose.length ; i ++){(function(i){setTimeout(function(){squareSet[choose[i].row][choose[i].col] = null; //为状态数组置空table.removeChild(choose[i]); //将其从桌面上移除} , i 100);})(i);}//需要等星星消除完毕后再移动,故需增加一个延迟setTimeout(function(){move(); //调用移动函数},choose.length 100);}squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 效果(下降成功,但是有点小bug只有部分下降了) 解决方案:只需要在function refresh(){}的双循环里面增加以下代码: if(squareSet[i][j] == null) continue; 完整代码如下: var table; //游戏桌面var squareWidth = 50; //方块宽高var boardWidth = 10; //行列数var squareSet = []; //方块信息集合(二维数组)每个元素保存该方块的全部信息var baseScore = 5; //第一块的分数var stepScore = 10; //每多一块的累加分数var totalScore = 0; //当前总分var targetScore = 1500; //目标分var choose = []; //选中的连通小方块var timer = null; //闪烁定时器var flag = true; //锁,防止点击事件中响应其他点击或移入时间var tempSquare = null; //临时方块function refresh(){for (var i = 0; i < squareSet.length; i++) {for (var j = 0; j < squareSet[i].length; j++) {if(squareSet[i][j] == null) continue; // 点击后数组中可能有空值需要跳过squareSet[i][j].style.background="url(pic/"+squareSet[i][j].num+".png)"squareSet[i][j].style.left=squareSet[i][j].colsquareWidth+"px";squareSet[i][j].style.bottom=squareSet[i][j].rowsquareWidth+"px";} }}function createSquare(value,row,col){ //创建小方块,传入参数为颜色、行、列,初始化时使用。var temp = document.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.position = "absolute"; //相对于背景绝对定位temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function goBack(){ //还原样式if(timer != null){ //清空计时器clearInterval(timer);}for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){squareSet[i][j].style.border = "0px solid white";squareSet[i][j].style.transform = "scale(0.95)";} }}function checkLinked(square , arr){ // 递归连通图算法arr.push(square); // 将当前方块放入选中数组中// check leftif( square.col > 0 && //未到边界squareSet[square.row][square.col - 1].num == square.num && //颜色相同arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循环判断checkLinked(squareSet[square.row][square.col - 1] , arr);}// check rightif( square.col < boardWidth - 1 &&squareSet[square.row][square.col + 1].num == square.num &&arr.indexOf(squareSet[square.row][square.col + 1]) == -1) {checkLinked(squareSet[square.row][square.col + 1] , arr);}// check upif( square.row < boardWidth - 1 &&squareSet[square.row + 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row + 1][square.col]) == -1) {checkLinked(squareSet[square.row + 1][square.col] , arr);}// check downif( square.row > 0 &&squareSet[square.row - 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row - 1][square.col]) == -1) {checkLinked(squareSet[square.row - 1][square.col] , arr);} }function flicker(arr){ // 选中连通的小方块可以闪烁var num = 0;timer = setInterval(function(){for(var i = 0 ; i < arr.length ; i ++){arr[i].style.border = "3px solid BFEFFF";//有个框arr[i].style.transform = "scale(" + (0.9 + (0.05 Math.pow(-1 , num))) + ")";//一闪一闪}num ++; // 注意这里所采用的数学技巧,仍然使用transform:scale(val)来进行缩放。},300);//闪烁的时间}function selectScore(){ //可以显示当前选中小方块的得分var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points";document.getElementById('selectScore').style.opacity = 1;// 设置时间间隔1秒后显示消失的过渡动画setTimeout(function(){document.getElementById('selectScore').style.opacity = 0;document.getElementById('selectScore').style.transition = "opacity 1s";},1000);}function mouseOver(obj){ //鼠标移入区域响应// 还原所有样式goBack();// 检查相邻choose = [];checkLinked(obj , choose);// 闪烁flicker(choose);// 显示分数selectScore();}function move(){//纵向下落,采用快慢指针算法for(var i = 0 ; i < boardWidth ; i ++){var pointer = 0; //慢指针for(var j = 0 ; j < boardWidth ; j ++){if(squareSet[j][i] != null){ //按行遍历if(pointer != j){ //快慢指针不同步说明中间有空元素squareSet[pointer][i] = squareSet[j][i]; //慢指针设成快指针元素squareSet[j][i].row = pointer;squareSet[j][i] = null; //快指针处置空}pointer ++; //该行非空时慢指针增加} }}// 横向移动(当出现一列为空时)for(var i = 0 ; i < squareSet[0].length ;){ //必须注意循环结束条件的判断if(squareSet[0][i] == null){ //逻辑:只需判断最低层为空,该行则全为空for(var j = 0 ; j < boardWidth ; j ++){squareSet[j].splice(i , 1); //splice删除数组squareSet[j]中从i开始的1个元素}continue;//注意移动后i不应改变了}i ++;}refresh();}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //显示目标分数用innerHTML// 循环初始化星星区域for(var i = 0 ; i < boardWidth ; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);// 鼠标移入事件square.onmouseover = function(){mouseOver(this);}// 鼠标点击事件square.onclick = function(){//对锁进行控制if(!flag || choose.length == null){return;}flag = false;tempSquare = null;//更新分数var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}totalScore += score;document.getElementById('nowScore').innerHTML = "Current Score : " + totalScore;//为移除增加一个延迟动画,为了防止闭包,这里采用立即执行函数for(var i = 0 ; i < choose.length ; i ++){(function(i){setTimeout(function(){squareSet[choose[i].row][choose[i].col] = null; //为状态数组置空table.removeChild(choose[i]); //将其从桌面上移除} , i 100);})(i);}//需要等星星消除完毕后再移动,故需增加一个延迟setTimeout(function(){move(); //调用移动函数},choose.length 100);}squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 第四阶段:消灭全部星星,返回结果 最终完整版代码如下: var table; //游戏桌面var squareWidth = 50; //方块宽高var boardWidth = 10; //行列数var squareSet = []; //方块信息集合(二维数组)每个元素保存该方块的全部信息var baseScore = 5; //第一块的分数var stepScore = 10; //每多一块的累加分数var totalScore = 0; //当前总分var targetScore = 1500; //目标分var choose = []; //选中的连通小方块var timer = null; //闪烁定时器var flag = true; //锁,防止点击事件中响应其他点击或移入时间var tempSquare = null; //临时方块function refresh(){ //重绘画板,每次鼠标点击后刷新for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){if(squareSet[i][j] == null) continue; // 点击后数组中可能有空值需要跳过squareSet[i][j].row = i; //更新当前的行列数squareSet[i][j].col = j;squareSet[i][j].style.backgroundImage = "url(./pic/" + squareSet[i][j].num + ".png)"squareSet[i][j].style.backgroundSize = "cover"; //占满范围squareSet[i][j].style.transform = "scale(0.95)"; //美观效果让不同星星之间留出空隙(缩小至0.95倍大小)squareSet[i][j].style.left = squareSet[i][j].col squareWidth + "px"; // 别忘了加"px"squareSet[i][j].style.bottom = squareSet[i][j].row squareWidth + "px";squareSet[i][j].style.transition = "left 0.3s, bottom 0.3s";} }}function createSquare(value,row,col){ //创建小方块,传入参数为颜色、行、列,初始化时使用。var temp = document.createElement('div'); //创建div dom对象temp.style.height = squareWidth + "px";temp.style.width = squareWidth + "px";temp.style.display = "inline-block"; //需要让对象元素能排列一排temp.style.position = "absolute"; //相对于背景绝对定位temp.style.boxSizing = "border-box"; //重要:不会使增加的边框溢出覆盖到旁边的元素temp.style.borderRadius = "12px";temp.num = value;temp.col = col;temp.row = row;return temp; //返回这个创建出来的对象}function goBack(){ //还原样式if(timer != null){ //清空计时器clearInterval(timer);}for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){if(squareSet[i][j] == null) continue;squareSet[i][j].style.border = "0px solid white";squareSet[i][j].style.transform = "scale(0.95)";} }}function checkLinked(square , arr){ // 递归连通图算法if(square == null) return; // 递归边界arr.push(square); // 将当前方块放入选中数组中// check leftif( square.col > 0 && //未到边界squareSet[square.row][square.col - 1] && //左侧有块squareSet[square.row][square.col - 1].num == square.num && //颜色相同arr.indexOf(squareSet[square.row][square.col - 1]) == -1) { //不在choose中,避免循环判断checkLinked(squareSet[square.row][square.col - 1] , arr);}// check rightif( square.col < boardWidth - 1 &&squareSet[square.row][square.col + 1] &&squareSet[square.row][square.col + 1].num == square.num &&arr.indexOf(squareSet[square.row][square.col + 1]) == -1) {checkLinked(squareSet[square.row][square.col + 1] , arr);}// check upif( square.row < boardWidth - 1 &&squareSet[square.row + 1][square.col] &&squareSet[square.row + 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row + 1][square.col]) == -1) {checkLinked(squareSet[square.row + 1][square.col] , arr);}// check downif( square.row > 0 &&squareSet[square.row - 1][square.col] &&squareSet[square.row - 1][square.col].num == square.num &&arr.indexOf(squareSet[square.row - 1][square.col]) == -1) {checkLinked(squareSet[square.row - 1][square.col] , arr);} }function flicker(arr){ // 选中连通的小方块可以闪烁var num = 0;timer = setInterval(function(){for(var i = 0 ; i < arr.length ; i ++){arr[i].style.border = "3px solid BFEFFF";arr[i].style.transform = "scale(" + (0.9 + (0.05 Math.pow(-1 , num))) + ")";}num ++; // 注意这里所采用的数学技巧,仍然使用transform:scale(val)来进行缩放。},300);}function selectScore(){ //可以显示当前选中小方块的得分var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}if(score == 0) return;document.getElementById('selectScore').innerHTML = choose.length + " blocks " + score + " points";document.getElementById('selectScore').style.opacity = 1;document.getElementById('selectScore').style.transition = null;// 设置时间间隔1秒后显示消失的过渡动画setTimeout(function(){document.getElementById('selectScore').style.opacity = 0;document.getElementById('selectScore').style.transition = "opacity 1s";},1000);}function mouseOver(obj){ //鼠标移入区域响应// 加锁,点击事件过程中不允许其他点击事件与移入事件if(!flag){tempSquare = obj;return;}// 还原所有样式goBack();// 检查相邻choose = [];checkLinked(obj , choose);if(choose.length <= 1){choose = [];return;}// 闪烁flicker(choose);// 显示分数selectScore();}function move(){ //下落移动控制//纵向下落,采用快慢指针算法for(var i = 0 ; i < boardWidth ; i ++){var pointer = 0; //慢指针for(var j = 0 ; j < boardWidth ; j ++){if(squareSet[j][i] != null){ //按行遍历if(pointer != j){ //快慢指针不同步说明中间有空元素squareSet[pointer][i] = squareSet[j][i]; //慢指针设成快指针元素squareSet[j][i].row = pointer;squareSet[j][i] = null; //快指针处置空}pointer ++; //该行非空时慢指针增加} }}// 横向移动(当出现一列为空时)for(var i = 0 ; i < squareSet[0].length ;){ // 注意循环终止条件的判断!!!因为数组长度会更新if(squareSet[0][i] == null){ //逻辑:只需判断最低层为空,该行则全为空for(var j = 0 ; j < boardWidth ; j ++){squareSet[j].splice(i , 1); //splice删除数组squareSet[j]中从i开始的1个元素}continue;//注意移动后i不应改变了}i ++;}refresh();}function isFinish(){ //判断游戏结束flag = true; //重要:需要先解锁,保证后续鼠标事件可以被响应for(var i = 0 ; i < squareSet.length ; i ++){for(var j = 0 ; j < squareSet[i].length ; j ++){if(squareSet[i][j] == null) continue; //遍历每一元素判断连通var temp = [];checkLinked(squareSet[i][j] , temp);if(temp.length > 1) return false; //若有某一元素仍有多块连通,则游戏未结束} }return flag;}function init(){ // JS调用入口table = document.getElementById('pop_star'); // 获取到最外层的父元素作为桌面document.getElementById('targetScore').innerHTML = "Target Score : " + targetScore; //显示目标分数用innerHTML// 循环初始化星星区域for(var i = 0 ; i < boardWidth ; i ++){squareSet[i] = new Array(); //二维数组的创建,对每一个元素new Array()创建新数组for(var j = 0 ; j < boardWidth ; j ++){var square = createSquare(Math.floor(Math.random() 5) , i , j);// 鼠标移入事件square.onmouseover = function(){mouseOver(this);}// 鼠标点击事件square.onclick = function(){//对锁进行控制if(!flag || choose.length == null){return;}flag = false;tempSquare = null;//更新分数var score = 0;for(var i = 0 ; i < choose.length ; i ++){score += (baseScore + i stepScore);}totalScore += score;document.getElementById('nowScore').innerHTML = "Current Score : " + totalScore;//为移除增加一个延迟动画,为了防止闭包,这里采用立即执行函数for(var i = 0 ; i < choose.length ; i ++){(function(i){setTimeout(function(){squareSet[choose[i].row][choose[i].col] = null; //为状态数组置空table.removeChild(choose[i]); //将其从桌面上移除} , i 50);})(i);}//需要等星星消除完毕后再移动,故需增加一个延迟setTimeout(function(){move(); //调用移动函数setTimeout(function(){var judge = isFinish();if(judge){ //游戏达到结束条件if(totalScore > targetScore){alert('Congratulations! You win!');}else{alert('Mission Failed!');} }else{flag = true;choose = [];mouseOver(tempSquare); //处理可能存在的冲突} },300 + choose.length 75); //需要一个判断延迟},choose.length 50);}squareSet[i][j] = square; //必须将新创建的方块放回到数组中table.appendChild(square); //需要将创建的新元素添加到桌面上} }refresh(); //每次页面内容发生变化需要重绘页面}window.onload = function(){init();} // window.onload 保证了在页面全部加载完毕后再执行JS代码 效果 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_56471396/article/details/128681321。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-06-08 15:26:34
516
转载
转载文章
...现缺陷和与其他代码、组件的集成问题) 随时可部署 (高频率的集成可以尽可能地保证随时部署上线,缩短开发复杂软件的市场交付时间) 极大程度避免低级错误 (减少大量内容合并到主干分支的请看看,避免代码合并冲突和无法预料的行为) 低级错误:编译错误,安装问题,接口问题,性能问题等 难点 迁移遗留代码到现有CI系统,需要的投入通常爱预料之外 在文化和组织上如果没有采用敏捷原则或DecOps的工作方式,那么很可能没有持续不断的提交,那么CI的存在意义不大 随着业务增长、工具的更替、技术的演进。CI系统也必然随之改动,往往会导致阶段性的不稳定和人力物力的耗费 如果CI的基本设定不到位,开发流程将会增加特别的开销 注意点 CI流程的触发方式 跟踪触发式:在每次提交到源码版本管理系统时触发 计划任务:预配置好的计划 手动:无论是通过CI服务器的管理界面还是脚本,用户可以手工执行CI工作流 代码审核 可在持续集成服务器里使用代码分析工具(例如Sonar)来执行自动代码审查 自动代码审查通过后,可发起一个人工代码审查,揪出那些自动审查无法找出的问题,即验证业务需求,架构问题,代码是否可读,以及是否易于扩展。 可灵活配置代码审核策略,例如:如果某些人没有审查代码便阻止对主干分支的任何提交。 最常用的工具是Gerrit 持续交付 简述 持续交付简称CD或CDE,是一种能够使得软件在较短的循环中可靠的发布的软件工程方法 与持续集成相比,持续交付的重点在于 交付,其核心对象不在于代码,而在于可交付的产物。 由于持续集成仅仅针对于新旧代码的集成过程执行来了一定的测试,其变动到持续交付后还需要一些额外的流程 持续交付可以看作为是持续集成的下一步,它强调的是,不敢怎么更新,软件是随时随快可以交付的 有图可看出,持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实的运行环境的[类生产环境]中 目的 持续交付永爱确保让代码能够快速、安全的部署到产品环境中,它通过将每一次改动都会提交到一个模拟产品环境中,使用严格的自动化测试,确保业务应用和服务能符合预期 好处 持续交付和持续集成的好处非常相似: 快速发布。能够应对业务需求,并更快地实现软件价值 编码→测试→上线→交付的频繁迭代周期缩短,同时获得迅速反馈 高质量的软件发布标准。整个交付过程标准化、可重复、可靠 整个交付过程进度可视化,方便团队人员了解项目完成度 更先进的团队协作方式。从需求分析、产品的用户体验到交互、设计、开发、测试、运维等角色密切协作,相比于传统的瀑布式软件团队,更少浪费 持续部署 简述 持续部署 意味着:通过自动化部署的手段将软件功能频繁的进行交付 持续部署是持续交付的下一步,指的是代码通过审批以后,自动化部署到生产环境。 持续部署是持续交付的最高阶段,这意味着,所有通过了一系列的自动化测试的改动都将自动部署到生产环境。它也可以被称为“Continuous Release” 持续化部署的目标是:代码在任何时候都是可部署的,可以进入生产阶段。 持续部署的前提是能自动化完成测试、构建、部署等步骤 注:持续交付不等于持续集成 与持续交付以及持续集成相比,持续部署强调了通过 automated deployment 的手段,对新的软件功能进行集成 目标 持续部署的目标是:代码在任何时刻都是可部署的,可以进入生产阶段 有很多的业务场景里,一种业务需要等待另外的功能特征出现才能上线,这是的持续部署成为不可能。虽然使用功能切换能解决很多这样的情况,但并不是没每次都会这样。所以,持续部署是否适合你的公司是基于你们的业务需求——而不是技术限制 优点 持续部署主要的好处是:可以相对独立地部署新的功能,并能快速地收集真实用户的反馈 敏捷开发 简述 敏捷开发就是一种以人为核心、迭代循环渐进的开发方式。 在敏捷开发中,软件仙姑的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。 简单的说就是把一个大的项目分为多个相互联系,但也可以独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态 注意事项 敏捷开的就是一种面临迅速变化的需求快速开发的能力,要注意一下几点: 敏捷开发不仅仅是一个项目快速完成,而是对整个产品领域需求的高效管理 敏捷开发不仅仅是简单的快,而是短周期的不断改进、提高和调整 敏捷开发不仅仅是一个版本只做几个功能,而是突出重点、果断放弃当前的非重要点 敏捷开发不仅仅是随时增加需求,而是每个迭代周期对需求的重新审核和排序 如何进行敏捷开发 1、组织建设 也就是团队建设,建立以产品经理为主导,包含产品、设计、前后台开发和测试的team,快速进行产品迭代开发;扁平化的团队管理,大家都有共同目标,更有成就感; 2、敏捷制度 要找准适合自身的敏捷开发方式,主要是制定一个完善的效率高的设计、开发、测试、上线流程,制定固定的迭代周期,让用户更有期待; 3、需求收集 这个任何方式下都需要有,需求一定要有交互稿,评审通过后,一定要确定功能需求列表、责任人、工作量、责任人等; 4、工具建设 是指能够快速完成某项事情的辅助工具,比如开发环境的一键安装,各种底层的日志、监控等平台,发布、打包工具等; 5、系统架构 略为超前架构设计:支持良好的扩容性和可维护性;组件化基础功能模块:代码耦合度低,模块间的依赖性小;插件化业务模块:降低营销活动与业务耦合度,自升级、自维护;客户端预埋逻辑;技术预研等等; 6、数据运营与灰度发布 点击率分析、用户路径分析、渠道选择、渠道升级控制等等 原则、特点和优势 敏捷开发技术的12个原则: 1.我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。 2.即使到了开发的后期,也欢迎改变需求。 3.经常性地交付可以工作的软件,交付的间隔可以从几周到几个月,交付的时间间隔越短越好。 4.在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。 5.围绕被激励起来的个人来构建项目。 6.在团队内部,最具有效果并且富有效率的传递信息的方法,就是面对面的交谈。 7.工作的软件是首要的进度度量标准。 8.敏捷过程提倡可持续的开发速度。 9.不断地关注优秀的技能和好的设计会增强敏捷能力。 10.简单使未完成的工作最大化。 11.最好的构架、需求和设计出自于自组织的团队。 12.每隔一定时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。 特点: 个体和交互胜过过程和工具 可以工作的软件胜过面面俱到的文档 客户合作胜过合同谈判 响应变化胜过遵循计划 优势总结: 敏捷开发确实是项目进入实质开发迭代阶段,用户很快可以看到一个基线架构班的产品。敏捷注重市场快速反应能力,也即具体应对能力,客户前期满意度高 适用范围: 项目团队的人不能太多 项目经常发生变更 高风险的项目实施 开发人员可以参与决策 劣势总结: 敏捷开发注重人员的沟通 忽略文档的重要性 若项目人员流动太大,维护的时候很难 项目存在新手的比较多的时候,老员工会比较累 需要项目中存在经验较强的人,要不然大项目中容易遇到瓶颈问题 Open-falcon 简述 open-falcon是小米的监控系统,是一款企业级、高可用、可扩展的开源监控解决方案 公司用open-falcon来监控调度系统各种信息,便于监控各个节点的调度信息。在服务器安装了falcon-agent自动采集各项指标,主动上报 特点 强大灵活的数据采集 (自动发现,支持falcon-agent、snmp、支持用户主动push、用户自定义插件支持、opentsdb data model like(timestamp、endpoint、metric、key-value tags) ) 水平扩展能力 (支持每个周期上亿次的数据采集、告警判定、历史数据存储和查询 ) 高效率的告警策略管理 (高效的portal、支持策略模板、模板继承和覆盖、多种告警方式、支持callback调用 ) 人性化的告警设置 (最大告警次数、告警级别、告警恢复通知、告警暂停、不同时段不同阈值、支持维护周期 ) 高效率的graph组件 (单机支撑200万metric的上报、归档、存储(周期为1分钟) ) 高效的历史数据query组件 (采用rrdtool的数据归档策略,秒级返回上百个metric一年的历史数据 ) dashboard(面向用户的查询界面,可以看到push到graph中的所有数据,并查看数据发展趋势 ) (对维度的数据展示,用户自定义Screen) 高可用 (整个系统无核心单点,易运维,易部署,可水平扩展) 开发语言 (整个系统的后端,全部golang编写,portal和dashboard使用python编写。 ) 监控范围 Open-Falcon支持系统基础监控,第三方服务监控,JVM监控,业务应用监控 基础监控指的是Linux系统的指标监控,包括CPU、load、内存、磁盘、IO、网络等, 这些指标由Openfalcon的agent节点直接支持,无需插件 第三方服务监控指的是一些常见的服务监控,包括Mysql、Redis、Nginx等 OpenFalcon官网提供了很多第三方服务的监控插件,也可以自己实现插件,定义采集指标。而采集到的指标,也是通过插件先发送给agent,再由agent发送到OpenFalcon。 JVM监控主要通过插件完成,插件通过JVM开放的JMX通信端口,获取到JVM参数指标,并推送到agent节点,再由agent发送到OpenFalcon。 业务应用监控就是监控企业自主开发的应用服务 主要通过插件完成,插件通过JVM开放的JMX通信端口,获取到JVM参数指标,并推送到agent节点,再由agent发送到OpenFalcon。 数据流向 常见的OpenFalcon包含transfer、hbs、agent、judge、graph、API几个进程 以下是各个节点的数据流向图,主数据流向是agent -> transfer -> judge/graph: SNMP 简述 SNMP:简单网络管理协议,是TCP/IP协议簇 的一个应用层协议,由于SNMP的简单性,在Internet时代得到了蓬勃的发展 ,1992年发布了SNMPv2版本,以增强SNMPv1的安全性和功能。现在,已经有了SNMPv3版本(它对网络管理最大的贡献在于其安全性。增加了对认证和密文传输的支持 )。 一套完整的SNMP系统主要包括:管理信息库(MIB)、管理信息结构(SMI)和 SNMP报文协议 为什么要用SNMP 作为运维人员,我们很大一部分的工作就是为了保证我们的网络能够正常、稳定的运行。因此监控,控制,管理各种网络设备成了我们日常的工作 优点和好处 优点: 简单易懂,部署的开销成本也小 ,正因为它足够简单,所以被广泛的接受,事实上它已经成为了主要的网络管理标准。在一个网络设备上实现SNMP的管理比绝大部分其他管理方式都简单直接。 好处: 标准化的协议:SNMP是TCP/IP网络的标准网络管理协议。 广泛认可:所有主流供应商都支持SNMP。 可移植性:SNMP独立于操作系统和编程语言。 轻量级:SNMP增强对设备的管理能力的同时不会对设备的操作方式或性能产生冲击。 可扩展性:在所有SNMP管理的设备上都会支持相同的一套核心操作集。 广泛部署:SNMP是最流行的管理协议,最为受设备供应商关注,被广泛部署在各种各样的设备上。 MIB、SMI和SNMP报文 MIB 管理信息库MIB:任何一个被管理的资源都表示成一个对象,称为被管理的对象。 MIB是被管理对象的集合。 它定义了被管理对象的一系列属性:对象的名称、对象的访问权限和对象的数据类型等。 每个SNMP设备(Agent)都有自己的MIB。 MIB也可以看作是NMS(网管系统)和Agent之间的沟通桥梁。 MIB文件中的变量使用的名字取自ISO和ITU管理的对象表示符命名空间,他是一个分级数的结构 SMI SMI定义了SNNMP框架多用信息的组织、组成和标识,它还未描述MIB对象和表述协议怎么交换信息奠定了基础 SMI定义的数据类型: 简单类型(simple): Integer:整型是-2,147,483,648~2,147,483,647的有符号整数 octet string: 字符串是0~65535个字节的有序序列 OBJECT IDENTIFIER: 来自按照ASN.1规则分配的对象标识符集 简单结构类型(simple-constructed ): SEQUENCE 用于列表。这一数据类型与大多数程序设计语言中的“structure”类似。一个SEQUENCE包括0个或更多元素,每一个元素又是另一个ASN.1数据类型 SEQUENCE OF type 用于表格。这一数据类型与大多数程序设计语言中的“array”类似。一个表格包括0个或更多元素,每一个元素又是另一个ASN.1数据类型。 应用类型(application-wide): IpAddress: 以网络序表示的IP地址。因为它是一个32位的值,所以定义为4个字节; counter:计数器是一个非负的整数,它递增至最大值,而后回零。在SNMPv1中定义的计数器是32位的,即最大值为4,294,967,295; Gauge :也是一个非负整数,它可以递增或递减,但达到最大值时保持在最大值,最大值为232-1; time ticks:是一个时间单位,表示以0.01秒为单位计算的时间; SNMP报文 SNMP规定了5种协议数据单元PDU(也就是SNMP报文),用来在管理进程和代理之间的交换。 get-request操作:从代理进程处提取一个或多个参数值。 get-next-request操作:从代理进程处提取紧跟当前参数值的下一个参数值。 set-request操作:设置代理进程的一个或多个参数值。 get-response操作:返回的一个或多个参数值。这个操作是由代理进程发出的,它是前面三种操作的响应操作。 trap操作:代理进程主动发出的报文,通知管理进程有某些事情发生。 操作命令 SNMP协议之所以易于使用,这是因为它对外提供了三种用于控制MIB对象的基本操作命令。它们是:Get、Set 和 Trap。 Get:管理站读取代理者处对象的值 Set:管理站设置代理者处对象的值 Trap: 代理者主动向管理站通报重要事件 SLA 简述 SLA(服务等级协议):是关于网络服务供应商和客户之间的一份合同,其中定义了服务类型、服务质量和客户付款等术语 一个完整的SLA同时也是一个合法的文档,包括所涉及的当事人、协定条款(包含应用程序和支持的服务)、违约的处罚、费用和仲裁机构、政策、修改条款、报告形式和双方的义务等。同样服务提供商可以对用户在工作负荷和资源使用方面进行规定。 KPI 简述 KPI(关键绩效指标):是通过对组织内部流程的输入端、输出端的关键参数进行设置、取样、计算、分析,衡量流程绩效的一种目标式量化管理指标,是把企业的战略目标分解为可操作的工作目标的工具,是企业绩效管理的基础。 KPI可以是部门主管明确部门的主要责任,并以此为基础,明确部门人员的业绩衡量指标,建立明确的切实可行的KPI体系,是做好绩效管理的关键。 KPI(关键绩效指标)是用于衡量工作人员工作绩效表现的量化指标,是绩效计划的重要组成部分 转载于:https://www.cnblogs.com/woshinideyugegea/p/11242034.html 本篇文章为转载内容。原文链接:https://blog.csdn.net/anqiongsha8211/article/details/101592137。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-03-19 16:00:05
45
转载
转载文章
...入理解C中的委托和事件机制后,我们发现它们是.NET框架中实现灵活、解耦编程的核心工具。最近,随着.NET 5的发布以及对异步编程模式的持续优化,委托和事件在现代应用程序开发中的重要性更为凸显。例如,在构建大规模分布式系统或微服务架构时,通过事件驱动的方式进行组件间通信已成为一种最佳实践。 在实际应用中,.NET Core 3.0引入了源生成器(Source Generators),这一特性使得开发者能够更高效地处理事件和委托,进一步提升代码质量和可维护性。通过自定义源生成器,可以动态创建委托实例并自动绑定相关事件,从而减少手动编写重复代码的工作量。 此外,委托还在并发和多线程编程场景下发挥关键作用,如Task类和async/await关键字背后就依赖于委托来实现异步方法的调用和状态管理。微软在.NET生态系统中提倡采用异步编程模型,利用C的事件和委托机制,能够简化异步操作的处理流程,提高程序性能和响应速度。 对于设计模式层面的理解,委托与观察者模式(Observer Pattern)紧密相连,它允许对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。结合最新的.NET技术趋势,诸如Reactive Extensions (Rx.NET)等库更是将这种模式发扬光大,借助LINQ风格的查询操作符和事件流处理,让委托在实时数据流处理领域展现出了强大的功能。 总之,深入掌握C中的委托和事件不仅有助于日常开发工作的效率提升,更能紧跟现代软件工程的发展潮流,充分利用最新的技术和框架优势,构建出高性能、高可维护性的应用程序。而不断跟进官方文档、社区讨论和技术博客,则是深化此类主题理解和实践运用的有效途径。
2023-10-05 16:02:19
80
转载
转载文章
...park提供的实时流处理组件,它允许对大规模数据流进行实时计算和分析。在本文中,电商广告点击日志数据被连续不断地收集并存储至Kafka消息队列中,Spark Streaming应用程序从Kafka中拉取这些实时数据,并通过一系列操作(如窗口、聚合、状态更新等)进行实时统计和分析,从而实现用户行为监控、流量趋势分析、热门广告排行等功能。 Kafka , Kafka是一种高吞吐量的分布式发布订阅消息系统,常用于构建实时数据管道和流应用平台。在该案例中,Kafka作为中间件承载了广告点击日志的实时传输功能,生产者将模拟或实际的广告点击事件发送至Kafka主题中,而Spark Streaming消费者则能够从Kafka中消费这些实时数据进行进一步处理与分析。 updateStateByKey , updateStateByKey是Spark Streaming提供的一种基于键值的状态管理API,它允许开发者维护每个key的最新状态,并在每次接收到新的数据时更新对应key的状态值。在广告点击综合案例中,可以利用updateStateByKey来实时过滤黑名单用户、跟踪用户的点击次数以及按地区统计广告点击排名等复杂状态信息,以满足业务需求中的动态过滤有效用户点击行为、在线计算广告点击流量和区域分布分析等功能。 Structured Streaming , 虽然文章未直接提到Structured Streaming,但在Spark 2.x及更高版本中,Structured Streaming是Spark Streaming框架的一个重要演进方向,提供了更高级别的抽象和SQL-like API,使得实时流处理更加简单和直观。若在类似场景下采用Structured Streaming,可以通过定义DataFrame/Dataset查询语句的方式处理广告点击流数据,实现诸如黑名单过滤、流量统计、热点广告分析等任务。 MySQL , MySQL是一个开源的关系型数据库管理系统,在此案例中扮演着持久化存储系统的角色。经过Spark Streaming实时处理后的结果数据,如广告点击流量统计结果、热门广告排行榜等,会被写入到MySQL中以便于后续查询展示和报表生成,同时也便于其他系统和服务实时获取最新的广告效果数据。
2023-02-14 19:16:35
297
转载
转载文章
...tity has built-in support for Microsoft, Facebook, and Twitter accounts as well. Table 15-1 summarizes this chapter. 本章将完成对ASP.NET Identity的描述,向你展示它所提供的一些高级特性。我将演示,你可以扩展ASP.NET Identity的数据库架构,其办法是在用户类上定义一些自定义属性。也会演示如何使用数据库迁移,这样可以运用自定义属性,而不必删除ASP.NET Identity数据库中的数据。还会解释ASP.NET Identity如何支持声明(Claims)概念,并演示如何将它们灵活地用来对动作方法进行授权访问。最后向你展示ASP.NET Identity很容易通过第三方部件来认证用户,以此结束本章以及本书。将要演示的是使用Google账号认证,但ASP.NET Identity对于Microsoft、Facebook以及Twitter账号,都有内建的支持。表15-1是本章概要。 Table 15-1. Chapter Summary 表15-1. 本章概要 Problem 问题 Solution 解决方案 Listing 清单号 Store additional information about users. 存储用户的附加信息 Define custom user properties. 定义自定义用户属性 1–3, 8–11 Update the database schema without deleting user data. 更新数据库架构而不删除用户数据 Perform a database migration. 执行数据库迁移 4–7 Perform fine-grained authorization. 执行细粒度授权 Use claims. 使用声明(Claims) 12–14 Add claims about a user. 添加用户的声明(Claims) Use the ClaimsIdentity.AddClaims method. 使用ClaimsIdentity.AddClaims方法 15–19 Authorize access based on claim values. 基于声明(Claims)值授权访问 Create a custom authorization filter attribute. 创建一个自定义的授权过滤器注解属性 20–21 Authenticate through a third party. 通过第三方认证 Install the NuGet package for the authentication provider, redirect requests to that provider, and specify a callback URL that creates the user account. 安装认证提供器的NuGet包,将请求重定向到该提供器,并指定一个创建用户账号的回调URL。 22–25 15.1 Preparing the Example Project 15.1 准备示例项目 In this chapter, I am going to continue working on the Users project I created in Chapter 13 and enhanced in Chapter 14. No changes to the application are required, but start the application and make sure that there are users in the database. Figure 15-1 shows the state of my database, which contains the users Admin, Alice, Bob, and Joe from the previous chapter. To check the users, start the application and request the /Admin/Index URL and authenticate as the Admin user. 本章打算继续使用第13章创建并在第14章增强的Users项目。对应用程序无需做什么改变,但需要启动应用程序,并确保数据库中有一些用户。图15-1显示了数据库的状态,它含有上一章的用户Admin、Alice、Bob以及Joe。为了检查用户,请启动应用程序,请求/Admin/Index URL,并以Admin用户进行认证。 Figure 15-1. The initial users in the Identity database 图15-1. Identity数据库中的最初用户 I also need some roles for this chapter. I used the RoleAdmin controller to create roles called Users and Employees and assigned the users to those roles, as described in Table 15-2. 本章还需要一些角色。我用RoleAdmin控制器创建了角色Users和Employees,并为这些角色指定了一些用户,如表15-2所示。 Table 15-2. The Types of Web Forms Code Nuggets 表15-2. 角色及成员(作者将此表的标题写错了——译者注) Role 角色 Members 成员 Users Alice, Joe Employees Alice, Bob Figure 15-2 shows the required role configuration displayed by the RoleAdmin controller. 图15-2显示了由RoleAdmin控制器所显示出来的必要的角色配置。 Figure 15-2. Configuring the roles required for this chapter 图15-2. 配置本章所需的角色 15.2 Adding Custom User Properties 15.2 添加自定义用户属性 When I created the AppUser class to represent users in Chapter 13, I noted that the base class defined a basic set of properties to describe the user, such as e-mail address and telephone number. Most applications need to store more information about users, including persistent application preferences and details such as addresses—in short, any data that is useful to running the application and that should last between sessions. In ASP.NET Membership, this was handled through the user profile system, but ASP.NET Identity takes a different approach. 我在第13章创建AppUser类来表示用户时曾做过说明,基类定义了一组描述用户的基本属性,如E-mail地址、电话号码等。大多数应用程序还需要存储用户的更多信息,包括持久化应用程序爱好以及地址等细节——简言之,需要存储对运行应用程序有用并且在各次会话之间应当保持的任何数据。在ASP.NET Membership中,这是通过用户资料(User Profile)系统来处理的,但ASP.NET Identity采取了一种不同的办法。 Because the ASP.NET Identity system uses Entity Framework to store its data by default, defining additional user information is just a matter of adding properties to the user class and letting the Code First feature create the database schema required to store them. Table 15-3 puts custom user properties in context. 因为ASP.NET Identity默认是使用Entity Framework来存储其数据的,定义附加的用户信息只不过是给用户类添加属性的事情,然后让Code First特性去创建需要存储它们的数据库架构即可。表15-3描述了自定义用户属性的情形。 Table 15-3. Putting Cusotm User Properties in Context 表15-3. 自定义用户属性的情形 Question 问题 Answer 回答 What is it? 什么是自定义用户属性? Custom user properties allow you to store additional information about your users, including their preferences and settings. 自定义用户属性让你能够存储附加的用户信息,包括他们的爱好和设置。 Why should I care? 为何要关心它? A persistent store of settings means that the user doesn’t have to provide the same information each time they log in to the application. 设置的持久化存储意味着,用户不必每次登录到应用程序时都提供同样的信息。 How is it used by the MVC framework? 在MVC框架中如何使用它? This feature isn’t used directly by the MVC framework, but it is available for use in action methods. 此特性不是由MVC框架直接使用的,但它在动作方法中使用是有效的。 15.2.1 Defining Custom Properties 15.2.1 定义自定义属性 Listing 15-1 shows how I added a simple property to the AppUser class to represent the city in which the user lives. 清单15-1演示了如何给AppUser类添加一个简单的属性,用以表示用户生活的城市。 Listing 15-1. Adding a Property in the AppUser.cs File 清单15-1. 在AppUser.cs文件中添加属性 using System;using Microsoft.AspNet.Identity.EntityFramework;namespace Users.Models { public enum Cities {LONDON, PARIS, CHICAGO}public class AppUser : IdentityUser {public Cities City { get; set; } }} I have defined an enumeration called Cities that defines values for some large cities and added a property called City to the AppUser class. To allow the user to view and edit their City property, I added actions to the Home controller, as shown in Listing 15-2. 这里定义了一个枚举,名称为Cities,它定义了一些大城市的值,另外给AppUser类添加了一个名称为City的属性。为了让用户能够查看和编辑City属性,给Home控制器添加了几个动作方法,如清单15-2所示。 Listing 15-2. Adding Support for Custom User Properties in the HomeController.cs File 清单15-2. 在HomeController.cs文件中添加对自定义属性的支持 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models;namespace Users.Controllers {public class HomeController : Controller {[Authorize]public ActionResult Index() {return View(GetData("Index"));}[Authorize(Roles = "Users")]public ActionResult OtherAction() {return View("Index", GetData("OtherAction"));}private Dictionary<string, object> GetData(string actionName) {Dictionary<string, object> dict= new Dictionary<string, object>();dict.Add("Action", actionName);dict.Add("User", HttpContext.User.Identity.Name);dict.Add("Authenticated", HttpContext.User.Identity.IsAuthenticated);dict.Add("Auth Type", HttpContext.User.Identity.AuthenticationType);dict.Add("In Users Role", HttpContext.User.IsInRole("Users"));return dict;} [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;await UserManager.UpdateAsync(user);return View(user);}private AppUser CurrentUser {get {return UserManager.FindByName(HttpContext.User.Identity.Name);} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } I added a CurrentUser property that uses the AppUserManager class to retrieve an AppUser instance to represent the current user. I pass the AppUser object as the view model object in the GET version of the UserProps action method, and the POST method uses it to update the value of the new City property. Listing 15-3 shows the UserProps.cshtml view, which displays the City property value and contains a form to change it. 我添加了一个CurrentUser属性,它使用AppUserManager类接收了表示当前用户的AppUser实例。在GET版本的UserProps动作方法中,传递了这个AppUser对象作为视图模型。而在POST版的方法中用它更新了City属性的值。清单15-3显示了UserProps.cshtml视图,它显示了City属性的值,并包含一个修改它的表单。 Listing 15-3. The Contents of the UserProps.cshtml File in the Views/Home Folder 清单15-3. Views/Home文件夹中UserProps.cshtml文件的内容 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";}<div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr></table></div> @using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Caution Don’t start the application when you have created the view. In the sections that follow, I demonstrate how to preserve the contents of the database, and if you start the application now, the ASP.NET Identity users will be deleted. 警告:创建了视图之后不要启动应用程序。在以下小节中,将演示如何保留数据库的内容,如果现在启动应用程序,将会删除ASP.NET Identity的用户。 15.2.2 Preparing for Database Migration 15.2.2 准备数据库迁移 The default behavior for the Entity Framework Code First feature is to drop the tables in the database and re-create them whenever classes that drive the schema have changed. You saw this in Chapter 14 when I added support for roles: When the application was started, the database was reset, and the user accounts were lost. Entity Framework Code First特性的默认行为是,一旦修改了派生数据库架构的类,便会删除数据库中的数据表,并重新创建它们。在第14章可以看到这种情况,在我添加角色支持时:当重启应用程序后,数据库被重置,用户账号也丢失。 Don’t start the application yet, but if you were to do so, you would see a similar effect. Deleting data during development is usually not a problem, but doing so in a production setting is usually disastrous because it deletes all of the real user accounts and causes a panic while the backups are restored. In this section, I am going to demonstrate how to use the database migration feature, which updates a Code First schema in a less brutal manner and preserves the existing data it contains. 不要启动应用程序,但如果你这么做了,会看到类似的效果。在开发期间删除数据没什么问题,但如果在产品设置中这么做了,通常是灾难性的,因为它会删除所有真实的用户账号,而备份恢复是很痛苦的事。在本小节中,我打算演示如何使用数据库迁移特性,它能以比较温和的方式更新Code First的架构,并保留架构中的已有数据。 The first step is to issue the following command in the Visual Studio Package Manager Console: 第一个步骤是在Visual Studio的“Package Manager Console(包管理器控制台)”中发布以下命令: Enable-Migrations –EnableAutomaticMigrations This enables the database migration support and creates a Migrations folder in the Solution Explorer that contains a Configuration.cs class file, the contents of which are shown in Listing 15-4. 它启用了数据库的迁移支持,并在“Solution Explorer(解决方案资源管理器)”创建一个Migrations文件夹,其中含有一个Configuration.cs类文件,内容如清单15-4所示。 Listing 15-4. The Contents of the Configuration.cs File 清单15-4. Configuration.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity;using System.Data.Entity.Migrations;using System.Linq;internal sealed class Configuration: DbMigrationsConfiguration<Users.Infrastructure.AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(Users.Infrastructure.AppIdentityDbContext context) {// This method will be called after migrating to the latest version.// 此方法将在迁移到最新版本时调用// You can use the DbSet<T>.AddOrUpdate() helper extension method// to avoid creating duplicate seed data. E.g.// 例如,你可以使用DbSet<T>.AddOrUpdate()辅助器方法来避免创建重复的种子数据//// context.People.AddOrUpdate(// p => p.FullName,// new Person { FullName = "Andrew Peters" },// new Person { FullName = "Brice Lambson" },// new Person { FullName = "Rowan Miller" }// );//} }} Tip You might be wondering why you are entering a database migration command into the console used to manage NuGet packages. The answer is that the Package Manager Console is really PowerShell, which is a general-purpose tool that is mislabeled by Visual Studio. You can use the console to issue a wide range of helpful commands. See http://go.microsoft.com/fwlink/?LinkID=108518 for details. 提示:你可能会觉得奇怪,为什么要在管理NuGet包的控制台中输入数据库迁移的命令?答案是“Package Manager Console(包管理控制台)”是真正的PowerShell,这是Visual studio冒用的一个通用工具。你可以使用此控制台发送大量的有用命令,详见http://go.microsoft.com/fwlink/?LinkID=108518。 The class will be used to migrate existing content in the database to the new schema, and the Seed method will be called to provide an opportunity to update the existing database records. In Listing 15-5, you can see how I have used the Seed method to set a default value for the new City property I added to the AppUser class. (I have also updated the class file to reflect my usual coding style.) 这个类将用于把数据库中的现有内容迁移到新的数据库架构,Seed方法的调用为更新现有数据库记录提供了机会。在清单15-5中可以看到,我如何用Seed方法为新的City属性设置默认值,City是添加到AppUser类中自定义属性。(为了体现我一贯的编码风格,我对这个类文件也进行了更新。) Listing 15-5. Managing Existing Content in the Configuration.cs File 清单15-5. 在Configuration.cs文件中管理已有内容 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models;namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);}foreach (AppUser dbUser in userMgr.Users) {dbUser.City = Cities.PARIS;}context.SaveChanges();} }} You will notice that much of the code that I added to the Seed method is taken from the IdentityDbInit class, which I used to seed the database with an administration user in Chapter 14. This is because the new Configuration class added to support database migrations will replace the seeding function of the IdentityDbInit class, which I’ll update shortly. Aside from ensuring that there is an admin user, the statements in the Seed method that are important are the ones that set the initial value for the City property I added to the AppUser class, as follows: 你可能会注意到,添加到Seed方法中的许多代码取自于IdentityDbInit类,在第14章中我用这个类将管理用户植入了数据库。这是因为这个新添加的、用以支持数据库迁移的Configuration类,将代替IdentityDbInit类的种植功能,我很快便会更新这个类。除了要确保有admin用户之外,在Seed方法中的重要语句是那些为AppUser类的City属性设置初值的语句,如下所示: ...foreach (AppUser dbUser in userMgr.Users) { dbUser.City = Cities.PARIS;}context.SaveChanges();... You don’t have to set a default value for new properties—I just wanted to demonstrate that the Seed method in the Configuration class can be used to update the existing user records in the database. 你不一定要为新属性设置默认值——这里只是想演示Configuration类中的Seed方法,可以用它更新数据库中的已有用户记录。 Caution Be careful when setting values for properties in the Seed method for real projects because the values will be applied every time you change the schema, overriding any values that the user has set since the last schema update was performed. I set the value of the City property just to demonstrate that it can be done. 警告:在用于真实项目的Seed方法中为属性设置值时要小心,因为你每一次修改架构时,都会运用这些值,这会将自执行上一次架构更新之后,用户设置的任何数据覆盖掉。这里设置City属性的值只是为了演示它能够这么做。 Changing the Database Context Class 修改数据库上下文类 The reason that I added the seeding code to the Configuration class is that I need to change the IdentityDbInit class. At present, the IdentityDbInit class is derived from the descriptively named DropCreateDatabaseIfModelChanges<AppIdentityDbContext> class, which, as you might imagine, drops the entire database when the Code First classes change. Listing 15-6 shows the changes I made to the IdentityDbInit class to prevent it from affecting the database. 在Configuration类中添加种植代码的原因是我需要修改IdentityDbInit类。此时,IdentityDbInit类派生于描述性命名的DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 类,和你相像的一样,它会在Code First类改变时删除整个数据库。清单15-6显示了我对IdentityDbInit类所做的修改,以防止它影响数据库。 Listing 15-6. Preventing Database Schema Changes in the AppIdentityDbContext.cs File 清单15-6. 在AppIdentityDbContext.cs文件是阻止数据库架构变化 using System.Data.Entity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Models;using Microsoft.AspNet.Identity; namespace Users.Infrastructure {public class AppIdentityDbContext : IdentityDbContext<AppUser> {public AppIdentityDbContext() : base("IdentityDb") { }static AppIdentityDbContext() {Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());}public static AppIdentityDbContext Create() {return new AppIdentityDbContext();} } public class IdentityDbInit : NullDatabaseInitializer<AppIdentityDbContext> {} } I have removed the methods defined by the class and changed its base to NullDatabaseInitializer<AppIdentityDbContext> , which prevents the schema from being altered. 我删除了这个类中所定义的方法,并将它的基类改为NullDatabaseInitializer<AppIdentityDbContext> ,它可以防止架构修改。 15.2.3 Performing the Migration 15.2.3 执行迁移 All that remains is to generate and apply the migration. First, run the following command in the Package Manager Console: 剩下的事情只是生成并运用迁移了。首先,在“Package Manager Console(包管理器控制台)”中执行以下命令: Add-Migration CityProperty This creates a new migration called CityProperty (I like my migration names to reflect the changes I made). A class new file will be added to the Migrations folder, and its name reflects the time at which the command was run and the name of the migration. My file is called 201402262244036_CityProperty.cs, for example. The contents of this file contain the details of how Entity Framework will change the database during the migration, as shown in Listing 15-7. 这创建了一个名称为CityProperty的新迁移(我比较喜欢让迁移的名称反映出我所做的修改)。这会在文件夹中添加一个新的类文件,而且其命名会反映出该命令执行的时间以及迁移名称,例如,我的这个文件名称为201402262244036_CityProperty.cs。该文件的内容含有迁移期间Entity Framework修改数据库的细节,如清单15-7所示。 Listing 15-7. The Contents of the 201402262244036_CityProperty.cs File 清单15-7. 201402262244036_CityProperty.cs文件的内容 namespace Users.Migrations {using System;using System.Data.Entity.Migrations; public partial class Init : DbMigration {public override void Up() {AddColumn("dbo.AspNetUsers", "City", c => c.Int(nullable: false));}public override void Down() {DropColumn("dbo.AspNetUsers", "City");} }} The Up method describes the changes that have to be made to the schema when the database is upgraded, which in this case means adding a City column to the AspNetUsers table, which is the one that is used to store user records in the ASP.NET Identity database. Up方法描述了在数据库升级时,需要对架构所做的修改,在这个例子中,意味着要在AspNetUsers数据表中添加City数据列,该数据表是ASP.NET Identity数据库用来存储用户记录的。 The final step is to perform the migration. Without starting the application, run the following command in the Package Manager Console: 最后一步是执行迁移。无需启动应用程序,只需在“Package Manager Console(包管理器控制台)”中运行以下命令即可: Update-Database –TargetMigration CityProperty The database schema will be modified, and the code in the Configuration.Seed method will be executed. The existing user accounts will have been preserved and enhanced with a City property (which I set to Paris in the Seed method). 这会修改数据库架构,并执行Configuration.Seed方法中的代码。已有用户账号会被保留,且增强了City属性(我在Seed方法中已将其设置为“Paris”)。 15.2.4 Testing the Migration 15.2.4 测试迁移 To test the effect of the migration, start the application, navigate to the /Home/UserProps URL, and authenticate as one of the Identity users (for example, as Alice with the password MySecret). Once authenticated, you will see the current value of the City property for the user and have the opportunity to change it, as shown in Figure 15-3. 为了测试迁移的效果,启动应用程序,导航到/Home/UserProps URL,并以Identity中的用户(例如Alice,口令MySecret)进行认证。一旦已被认证,便会看到该用户City属性的当前值,并可以对其进行修改,如图15-3所示。 Figure 15-3. Displaying and changing a custom user property 图15-3. 显示和个性自定义用户属性 15.2.5 Defining an Additional Property 15.2.5 定义附加属性 Now that database migrations are set up, I am going to define a further property just to demonstrate how subsequent changes are handled and to show a more useful (and less dangerous) example of using the Configuration.Seed method. Listing 15-8 shows how I added a Country property to the AppUser class. 现在,已经建立了数据库迁移,我打算再定义一个属性,这恰恰演示了如何处理持续不断的修改,也为了演示Configuration.Seed方法更有用(至少无害)的示例。清单15-8显示了我在AppUser类上添加了一个Country属性。 Listing 15-8. Adding Another Property in the AppUserModels.cs File 清单15-8. 在AppUserModels.cs文件中添加另一个属性 using System;using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models {public enum Cities {LONDON, PARIS, CHICAGO} public enum Countries {NONE, UK, FRANCE, USA}public class AppUser : IdentityUser {public Cities City { get; set; }public Countries Country { get; set; }public void SetCountryFromCity(Cities city) {switch (city) {case Cities.LONDON:Country = Countries.UK;break;case Cities.PARIS:Country = Countries.FRANCE;break;case Cities.CHICAGO:Country = Countries.USA;break;default:Country = Countries.NONE;break;} }} } I have added an enumeration to define the country names and a helper method that selects a country value based on the City property. Listing 15-9 shows the change I made to the Configuration class so that the Seed method sets the Country property based on the City, but only if the value of Country is NONE (which it will be for all users when the database is migrated because the Entity Framework sets enumeration columns to the first value). 我已经添加了一个枚举,它定义了国家名称。还添加了一个辅助器方法,它可以根据City属性选择一个国家。清单15-9显示了对Configuration类所做的修改,以使Seed方法根据City设置Country属性,但只当Country为NONE时才进行设置(在迁移数据库时,所有用户都是NONE,因为Entity Framework会将枚举列设置为枚举的第一个值)。 Listing 15-9. Modifying the Database Seed in the Configuration.cs File 清单15-9. 在Configuration.cs文件中修改数据库种子 using System.Data.Entity.Migrations;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Users.Infrastructure;using Users.Models; namespace Users.Migrations {internal sealed class Configuration: DbMigrationsConfiguration<AppIdentityDbContext> {public Configuration() {AutomaticMigrationsEnabled = true;ContextKey = "Users.Infrastructure.AppIdentityDbContext";}protected override void Seed(AppIdentityDbContext context) {AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context));AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); string roleName = "Administrators";string userName = "Admin";string password = "MySecret";string email = "admin@example.com";if (!roleMgr.RoleExists(roleName)) {roleMgr.Create(new AppRole(roleName));}AppUser user = userMgr.FindByName(userName);if (user == null) {userMgr.Create(new AppUser { UserName = userName, Email = email },password);user = userMgr.FindByName(userName);}if (!userMgr.IsInRole(user.Id, roleName)) {userMgr.AddToRole(user.Id, roleName);} foreach (AppUser dbUser in userMgr.Users) {if (dbUser.Country == Countries.NONE) {dbUser.SetCountryFromCity(dbUser.City);} }context.SaveChanges();} }} This kind of seeding is more useful in a real project because it will set a value for the Country property only if one has not already been set—subsequent migrations won’t be affected, and user selections won’t be lost. 这种种植在实际项目中会更有用,因为它只会在Country属性未设置时,才会设置Country属性的值——后继的迁移不会受到影响,因此不会失去用户的选择。 1. Adding Application Support 1. 添加应用程序支持 There is no point defining additional user properties if they are not available in the application, so Listing 15-10 shows the change I made to the Views/Home/UserProps.cshtml file to display the value of the Country property. 应用程序中如果没有定义附加属性的地方,则附加属性就无法使用了,因此,清单15-10显示了我对Views/Home/UserProps.cshtml文件的修改,以显示Country属性的值。 Listing 15-10. Displaying an Additional Property in the UserProps.cshtml File 清单15-10. 在UserProps.cshtml文件中显示附加属性 @using Users.Models@model AppUser@{ ViewBag.Title = "UserProps";} <div class="panel panel-primary"><div class="panel-heading">Custom User Properties</div><table class="table table-striped"><tr><th>City</th><td>@Model.City</td></tr> <tr><th>Country</th><td>@Model.Country</td></tr></table></div>@using (Html.BeginForm()) {<div class="form-group"><label>City</label>@Html.DropDownListFor(x => x.City, new SelectList(Enum.GetNames(typeof(Cities))))</div><button class="btn btn-primary" type="submit">Save</button>} Listing 15-11 shows the corresponding change I made to the Home controller to update the Country property when the City value changes. 为了在City值变化时能够更新Country属性,清单15-11显示了我对Home控制器所做的相应修改。 Listing 15-11. Setting Custom Properties in the HomeController.cs File 清单15-11. 在HomeController.cs文件中设置自定义属性 using System.Web.Mvc;using System.Collections.Generic;using System.Web;using System.Security.Principal;using System.Threading.Tasks;using Users.Infrastructure;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Models; namespace Users.Controllers {public class HomeController : Controller {// ...other action methods omitted for brevity...// ...出于简化,这里忽略了其他动作方法... [Authorize]public ActionResult UserProps() {return View(CurrentUser);}[Authorize][HttpPost]public async Task<ActionResult> UserProps(Cities city) {AppUser user = CurrentUser;user.City = city;user.SetCountryFromCity(city);await UserManager.UpdateAsync(user);return View(user);}// ...properties omitted for brevity...// ...出于简化,这里忽略了一些属性...} } 2. Performing the Migration 2. 准备迁移 All that remains is to create and apply a new migration. Enter the following command into the Package Manager Console: 剩下的事情就是创建和运用新的迁移了。在“Package Manager Console(包管理器控制台)”中输入以下命令: Add-Migration CountryProperty This will generate another file in the Migrations folder that contains the instruction to add the Country column. To apply the migration, execute the following command: 这将在Migrations文件夹中生成另一个文件,它含有添加Country数据表列的指令。为了运用迁移,可执行以下命令: Update-Database –TargetMigration CountryProperty The migration will be performed, and the value of the Country property will be set based on the value of the existing City property for each user. You can check the new user property by starting the application and authenticating and navigating to the /Home/UserProps URL, as shown in Figure 15-4. 这将执行迁移,Country属性的值将根据每个用户当前的City属性进行设置。通过启动应用程序,认证并导航到/Home/UserProps URL,便可以查看新的用户属性,如图15-4所示。 Figure 15-4. Creating an additional user property 图15-4. 创建附加用户属性 Tip Although I am focused on the process of upgrading the database, you can also migrate back to a previous version by specifying an earlier migration. Use the –Force argument make changes that cause data loss, such as removing a column. 提示:虽然我们关注了升级数据库的过程,但你也可以回退到以前的版本,只需指定一个早期的迁移即可。使用-Force参数进行修改,会引起数据丢失,例如删除数据表列。 15.3 Working with Claims 15.3 使用声明(Claims) In older user-management systems, such as ASP.NET Membership, the application was assumed to be the authoritative source of all information about the user, essentially treating the application as a closed world and trusting the data that is contained within it. 在旧的用户管理系统中,例如ASP.NET Membership,应用程序被假设成是用户所有信息的权威来源,本质上将应用程序视为是一个封闭的世界,并且只信任其中所包含的数据。 This is such an ingrained approach to software development that it can be hard to recognize that’s what is happening, but you saw an example of the closed-world technique in Chapter 14 when I authenticated users against the credentials stored in the database and granted access based on the roles associated with those credentials. I did the same thing again in this chapter when I added properties to the user class. Every piece of information that I needed to manage user authentication and authorization came from within my application—and that is a perfectly satisfactory approach for many web applications, which is why I demonstrated these techniques in such depth. 这是软件开发的一种根深蒂固的方法,使人很难认识到这到底意味着什么,第14章你已看到了这种封闭世界技术的例子,根据存储在数据库中的凭据来认证用户,并根据与凭据关联在一起的角色来授权访问。本章前述在用户类上添加属性,也做了同样的事情。我管理用户认证与授权所需的每一个数据片段都来自于我的应用程序——而且这是许多Web应用程序都相当满意的一种方法,这也是我如此深入地演示这些技术的原因。 ASP.NET Identity also supports an alternative approach for dealing with users, which works well when the MVC framework application isn’t the sole source of information about users and which can be used to authorize users in more flexible and fluid ways than traditional roles allow. ASP.NET Identity还支持另一种处理用户的办法,当MVC框架的应用程序不是有关用户的唯一信息源时,这种办法会工作得很好,而且能够比传统的角色授权更为灵活且流畅的方式进行授权。 This alternative approach uses claims, and in this section I’ll describe how ASP.NET Identity supports claims-based authorization. Table 15-4 puts claims in context. 这种可选的办法使用了“Claims(声明)”,因此在本小节中,我将描述ASP.NET Identity如何支持“Claims-Based Authorization(基于声明的授权)”。表15-4描述了声明(Claims)的情形。 提示:“Claim”在英文字典中不完全是“声明”的意思,根据本文的描述,感觉把它说成“声明”也不一定合适,所以在之后的译文中基本都写成中英文并用的形式,即“声明(Claims)”。根据表15-4中的声明(Claims)的定义:声明(Claims)是关于用户的一些信息片段。一个用户的信息片段当然有很多,每一个信息片段就是一项声明(Claim),用户的所有信息片段合起来就是该用户的声明(Claims)。请读者注意该单词的单复数形式——译者注 Table 15-4. Putting Claims in Context 表15-4. 声明(Claims)的情形 Question 问题 Answer 答案 What is it? 什么是声明(Claims)? Claims are pieces of information about users that you can use to make authorization decisions. Claims can be obtained from external systems as well as from the local Identity database. 声明(Claims)是关于用户的一些信息片段,可以用它们做出授权决定。声明(Claims)可以从外部系统获取,也可以从本地的Identity数据库获取。 Why should I care? 为何要关心它? Claims can be used to flexibly authorize access to action methods. Unlike conventional roles, claims allow access to be driven by the information that describes the user. 声明(Claims)可以用来对动作方法进行灵活的授权访问。与传统的角色不同,声明(Claims)让访问能够由描述用户的信息进行驱动。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework, but it is integrated into the standard authorization features, such as the Authorize attribute. 这不是直接由MVC框架使用的特性,但它集成到了标准的授权特性之中,例如Authorize注解属性。 Tip you don’t have to use claims in your applications, and as Chapter 14 showed, ASP.NET Identity is perfectly happy providing an application with the authentication and authorization services without any need to understand claims at all. 提示:你在应用程序中不一定要使用声明(Claims),正如第14章所展示的那样,ASP.NET Identity能够为应用程序提供充分的认证与授权服务,而根本不需要理解声明(Claims)。 15.3.1 Understanding Claims 15.3.1 理解声明(Claims) A claim is a piece of information about the user, along with some information about where the information came from. The easiest way to unpack claims is through some practical demonstrations, without which any discussion becomes too abstract to be truly useful. To get started, I added a Claims controller to the example project, the definition of which you can see in Listing 15-12. 一项声明(Claim)是关于用户的一个信息片段(请注意这个英文单词的单复数形式——译者注),并伴有该片段出自何处的某种信息。揭开声明(Claims)含义最容易的方式是做一些实际演示,任何讨论都会过于抽象根本没有真正的用处。为此,我在示例项目中添加了一个Claims控制器,其定义如清单15-12所示。 Listing 15-12. The Contents of the ClaimsController.cs File 清单15-12. ClaimsController.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} }} } Tip You may feel a little lost as I define the code for this example. Don’t worry about the details for the moment—just stick with it until you see the output from the action method and view that I define. More than anything else, that will help put claims into perspective. 提示:你或许会对我为此例定义的代码感到有点失望。此刻对此细节不必着急——只要稍事忍耐,当看到该动作方法和视图的输出便会明白。尤为重要的是,这有助于洞察声明(Claims)。 You can get the claims associated with a user in different ways. One approach is to use the Claims property defined by the user class, but in this example, I have used the HttpContext.User.Identity property to demonstrate the way that ASP.NET Identity is integrated with the rest of the ASP.NET platform. As I explained in Chapter 13, the HttpContext.User.Identity property returns an implementation of the IIdentity interface, which is a ClaimsIdentity object when working using ASP.NET Identity. The ClaimsIdentity class is defined in the System.Security.Claims namespace, and Table 15-5 shows the members it defines that are relevant to this chapter. 可以通过不同的方式获得与用户相关联的声明(Claims)。方法之一就是使用由用户类定义的Claims属性,但在这个例子中,我使用了HttpContext.User.Identity属性,目的是演示ASP.NET Identity与ASP.NET平台集成的方式(请注意这句话所表示的含义:用户类的Claims属性属于ASP.NET Identity,而HttpContext.User.Identity属性则属于ASP.NET平台。由此可见,ASP.NET Identity已经融合到了ASP.NET平台之中——译者注)。正如第13章所解释的那样,HttpContext.User.Identity属性返回IIdentity的接口实现,当使用ASP.NET Identity时,该实现是一个ClaimsIdentity对象。ClaimsIdentity类是在System.Security.Claims命名空间中定义的,表15-5显示了它所定义的与本章有关的成员。 Table 15-5. The Members Defined by the ClaimsIdentity Class 表15-5. ClaimsIdentity类所定义的成员 Name 名称 Description 描述 Claims Returns an enumeration of Claim objects representing the claims for the user. 返回表示用户声明(Claims)的Claim对象枚举 AddClaim(claim) Adds a claim to the user identity. 给用户添加一个声明(Claim) AddClaims(claims) Adds an enumeration of Claim objects to the user identity. 给用户添加Claim对象的枚举。 HasClaim(predicate) Returns true if the user identity contains a claim that matches the specified predicate. See the “Applying Claims” section for an example predicate. 如果用户含有与指定谓词匹配的声明(Claim)时,返回true。参见“运用声明(Claims)”中的示例谓词 RemoveClaim(claim) Removes a claim from the user identity. 删除用户的声明(Claim)。 Other members are available, but the ones in the table are those that are used most often in web applications, for reason that will become obvious as I demonstrate how claims fit into the wider ASP.NET platform. 还有一些可用的其它成员,但表中的这些是在Web应用程序中最常用的,随着我演示如何将声明(Claims)融入更宽泛的ASP.NET平台,它们为什么最常用就很显然了。 In Listing 15-12, I cast the IIdentity implementation to the ClaimsIdentity type and pass the enumeration of Claim objects returned by the ClaimsIdentity.Claims property to the View method. A Claim object represents a single piece of data about the user, and the Claim class defines the properties shown in Table 15-6. 在清单15-12中,我将IIdentity实现转换成了ClaimsIdentity类型,并且给View方法传递了ClaimsIdentity.Claims属性所返回的Claim对象的枚举。Claim对象所示表示的是关于用户的一个单一的数据片段,Claim类定义的属性如表15-6所示。 Table 15-6. The Properties Defined by the Claim Class 表15-6. Claim类定义的属性 Name 名称 Description 描述 Issuer Returns the name of the system that provided the claim 返回提供声明(Claim)的系统名称 Subject Returns the ClaimsIdentity object for the user who the claim refers to 返回声明(Claim)所指用户的ClaimsIdentity对象 Type Returns the type of information that the claim represents 返回声明(Claim)所表示的信息类型 Value Returns the piece of information that the claim represents 返回声明(Claim)所表示的信息片段 Listing 15-13 shows the contents of the Index.cshtml file that I created in the Views/Claims folder and that is rendered by the Index action of the Claims controller. The view adds a row to a table for each claim about the user. 清单15-13显示了我在Views/Claims文件夹中创建的Index.cshtml文件的内容,它由Claims控制器中的Index动作方法进行渲染。该视图为用户的每项声明(Claim)添加了一个表格行。 Listing 15-13. The Contents of the Index.cshtml File in the Views/Claims Folder 清单15-13. Views/Claims文件夹中Index.cshtml文件的内容 @using System.Security.Claims@using Users.Infrastructure@model IEnumerable<Claim>@{ ViewBag.Title = "Claims"; }<div class="panel panel-primary"><div class="panel-heading">Claims</div><table class="table table-striped"><tr><th>Subject</th><th>Issuer</th><th>Type</th><th>Value</th></tr>@foreach (Claim claim in Model.OrderBy(x => x.Type)) {<tr><td>@claim.Subject.Name</td><td>@claim.Issuer</td><td>@Html.ClaimType(claim.Type)</td><td>@claim.Value</td></tr>}</table></div> The value of the Claim.Type property is a URI for a Microsoft schema, which isn’t especially useful. The popular schemas are used as the values for fields in the System.Security.Claims.ClaimTypes class, so to make the output from the Index.cshtml view easier to read, I added an HTML helper to the IdentityHelpers.cs file, as shown in Listing 15-14. It is this helper that I use in the Index.cshtml file to format the value of the Claim.Type property. Claim.Type属性的值是一个微软模式(Microsoft Schema)的URI(统一资源标识符),这是特别有用的。System.Security.Claims.ClaimTypes类中字段的值使用的是流行模式(Popular Schema),因此为了使Index.cshtml视图的输出更易于阅读,我在IdentityHelpers.cs文件中添加了一个HTML辅助器,如清单15-14所示。Index.cshtml文件正是使用这个辅助器格式化了Claim.Type属性的值。 Listing 15-14. Adding a Helper to the IdentityHelpers.cs File 清单15-14. 在IdentityHelpers.cs文件中添加辅助器 using System.Web;using System.Web.Mvc;using Microsoft.AspNet.Identity.Owin;using System;using System.Linq;using System.Reflection;using System.Security.Claims;namespace Users.Infrastructure {public static class IdentityHelpers {public static MvcHtmlString GetUserName(this HtmlHelper html, string id) {AppUserManager mgr= HttpContext.Current.GetOwinContext().GetUserManager<AppUserManager>();return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName);} public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType) {FieldInfo[] fields = typeof(ClaimTypes).GetFields();foreach (FieldInfo field in fields) {if (field.GetValue(null).ToString() == claimType) {return new MvcHtmlString(field.Name);} }return new MvcHtmlString(string.Format("{0}",claimType.Split('/', '.').Last()));} }} Note The helper method isn’t at all efficient because it reflects on the fields of the ClaimType class for each claim that is displayed, but it is sufficient for my purposes in this chapter. You won’t often need to display the claim type in real applications. 注:该辅助器并非十分有效,因为它只是针对每个要显示的声明(Claim)映射出ClaimType类的字段,但对我要的目的已经足够了。在实际项目中不会经常需要显示声明(Claim)的类型。 To see why I have created a controller that uses claims without really explaining what they are, start the application, authenticate as the user Alice (with the password MySecret), and request the /Claims/Index URL. Figure 15-5 shows the content that is generated. 为了弄明白我为何要先创建一个使用声明(Claims)的控制器,而没有真正解释声明(Claims)是什么的原因,可以启动应用程序,以用户Alice进行认证(其口令是MySecret),并请求/Claims/Index URL。图15-5显示了生成的内容。 Figure 15-5. The output from the Index action of the Claims controller 图15-5. Claims控制器中Index动作的输出 It can be hard to make out the detail in the figure, so I have reproduced the content in Table 15-7. 这可能还难以认识到此图的细节,为此我在表15-7中重列了其内容。 Table 15-7. The Data Shown in Figure 15-5 表15-7. 图15-5中显示的数据 Subject(科目) Issuer(发行者) Type(类型) Value(值) Alice LOCAL AUTHORITY SecurityStamp Unique ID Alice LOCAL AUTHORITY IdentityProvider ASP.NET Identity Alice LOCAL AUTHORITY Role Employees Alice LOCAL AUTHORITY Role Users Alice LOCAL AUTHORITY Name Alice Alice LOCAL AUTHORITY NameIdentifier Alice’s user ID The table shows the most important aspect of claims, which is that I have already been using them when I implemented the traditional authentication and authorization features in Chapter 14. You can see that some of the claims relate to user identity (the Name claim is Alice, and the NameIdentifier claim is Alice’s unique user ID in my ASP.NET Identity database). 此表展示了声明(Claims)最重要的方面,这些是我在第14章中实现传统的认证和授权特性时,一直在使用的信息。可以看出,有些声明(Claims)与用户标识有关(Name声明是Alice,NameIdentifier声明是Alice在ASP.NET Identity数据库中的唯一用户ID号)。 Other claims show membership of roles—there are two Role claims in the table, reflecting the fact that Alice is assigned to both the Users and Employees roles. There is also a claim about how Alice has been authenticated: The IdentityProvider is set to ASP.NET Identity. 其他声明(Claims)显示了角色成员——表中有两个Role声明(Claim),体现出Alice被赋予了Users和Employees两个角色这一事实。还有一个是Alice已被认证的声明(Claim):IdentityProvider被设置到了ASP.NET Identity。 The difference when this information is expressed as a set of claims is that you can determine where the data came from. The Issuer property for all the claims shown in the table is set to LOCAL AUTHORITY, which indicates that the user’s identity has been established by the application. 当这种信息被表示成一组声明(Claims)时的差别是,你能够确定这些数据是从哪里来的。表中所显示的所有声明的Issuer属性(发布者)都被设置到了LOACL AUTHORITY(本地授权),这说明该用户的标识是由应用程序建立的。 So, now that you have seen some example claims, I can more easily describe what a claim is. A claim is any piece of information about a user that is available to the application, including the user’s identity and role memberships. And, as you have seen, the information I have been defining about my users in earlier chapters is automatically made available as claims by ASP.NET Identity. 因此,现在你已经看到了一些声明(Claims)示例,我可以更容易地描述声明(Claim)是什么了。一项声明(Claim)是可用于应用程序中的有关用户的一个信息片段,包括用户的标识以及角色成员等。而且,正如你所看到的,我在前几章定义的关于用户的信息,被ASP.NET Identity自动地作为声明(Claims)了。 15.3.2 Creating and Using Claims 15.3.2 创建和使用声明(Claims) Claims are interesting for two reasons. The first reason is that an application can obtain claims from multiple sources, rather than just relying on a local database for information about the user. You will see a real example of this when I show you how to authenticate users through a third-party system in the “Using Third-Party Authentication” section, but for the moment I am going to add a class to the example project that simulates a system that provides claims information. Listing 15-15 shows the contents of the LocationClaimsProvider.cs file that I added to the Infrastructure folder. 声明(Claims)比较有意思的原因有两个。第一个原因是应用程序可以从多个来源获取声明(Claims),而不是只能依靠本地数据库关于用户的信息。你将会看到一个实际的示例,在“使用第三方认证”小节中,将演示如何通过第三方系统来认证用户。不过,此刻我只打算在示例项目中添加一个类,用以模拟一个提供声明(Claims)信息的系统。清单15-15显示了我添加到Infrastructure文件夹中LocationClaimsProvider.cs文件的内容。 Listing 15-15. The Contents of the LocationClaimsProvider.cs File 清单15-15. LocationClaimsProvider.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public static class LocationClaimsProvider {public static IEnumerable<Claim> GetClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.Name.ToLower() == "alice") {claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC"));} else {claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036"));claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY"));}return claims;}private static Claim CreateClaim(string type, string value) {return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims");} }} The GetClaims method takes a ClaimsIdentity argument and uses the Name property to create claims about the user’s ZIP code and state. This class allows me to simulate a system such as a central HR database, which would be the authoritative source of location information about staff, for example. GetClaims方法以ClaimsIdentity为参数,并使用Name属性创建了关于用户ZIP码(邮政编码)和州府的声明(Claims)。上述这个类使我能够模拟一个诸如中心化的HR数据库(人力资源数据库)之类的系统,它可能会成为全体职员的地点信息的权威数据源。 Claims are associated with the user’s identity during the authentication process, and Listing 15-16 shows the changes I made to the Login action method of the Account controller to call the LocationClaimsProvider class. 在认证过程期间,声明(Claims)是与用户标识关联在一起的,清单15-16显示了我对Account控制器中Login动作方法所做的修改,以便调用LocationClaimsProvider类。 Listing 15-16. Associating Claims with a User in the AccountController.cs File 清单15-16. AccountController.cs文件中用户用声明的关联 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... You can see the effect of the location claims by starting the application, authenticating as a user, and requesting the /Claim/Index URL. Figure 15-6 shows the claims for Alice. You may have to sign out and sign back in again to see the change. 为了看看这个地点声明(Claims)的效果,可以启动应用程序,以一个用户进行认证,并请求/Claim/Index URL。图15-6显示了Alice的声明(Claims)。你可能需要退出,然后再次登录才会看到发生的变化。 Figure 15-6. Defining additional claims for users 图15-6. 定义用户的附加声明 Obtaining claims from multiple locations means that the application doesn’t have to duplicate data that is held elsewhere and allows integration of data from external parties. The Claim.Issuer property tells you where a claim originated from, which helps you judge how accurate the data is likely to be and how much weight you should give the data in your application. Location data obtained from a central HR database is likely to be more accurate and trustworthy than data obtained from an external mailing list provider, for example. 从多个地点获取声明(Claims)意味着应用程序不必复制其他地方保持的数据,并且能够与外部的数据集成。Claim.Issuer属性(图15-6中的Issuer数据列——译者注)能够告诉你一个声明(Claim)的发源地,这有助于让你判断数据的精确程度,也有助于让你决定这类数据在应用程序中的权重。例如,从中心化的HR数据库获取的地点数据可能要比外部邮件列表提供器获取的数据更为精确和可信。 1. Applying Claims 1. 运用声明(Claims) The second reason that claims are interesting is that you can use them to manage user access to your application more flexibly than with standard roles. The problem with roles is that they are static, and once a user has been assigned to a role, the user remains a member until explicitly removed. This is, for example, how long-term employees of big corporations end up with incredible access to internal systems: They are assigned the roles they require for each new job they get, but the old roles are rarely removed. (The unexpectedly broad systems access sometimes becomes apparent during the investigation into how someone was able to ship the contents of the warehouse to their home address—true story.) 声明(Claims)有意思的第二个原因是,你可以用它们来管理用户对应用程序的访问,这要比标准的角色管理更为灵活。角色的问题在于它们是静态的,而且一旦用户已经被赋予了一个角色,该用户便是一个成员,直到明确地删除为止。例如,这意味着大公司的长期雇员,对内部系统的访问会十分惊人:他们每次在获得新工作时,都会赋予所需的角色,但旧角色很少被删除。(在调查某人为何能够将仓库里的东西发往他的家庭地址过程中发现,有时会出现异常宽泛的系统访问——真实的故事) Claims can be used to authorize users based directly on the information that is known about them, which ensures that the authorization changes when the data changes. The simplest way to do this is to generate Role claims based on user data that are then used by controllers to restrict access to action methods. Listing 15-17 shows the contents of the ClaimsRoles.cs file that I added to the Infrastructure. 声明(Claims)可以直接根据用户已知的信息对用户进行授权,这能够保证当数据发生变化时,授权也随之而变。此事最简单的做法是根据用户数据来生成Role声明(Claim),然后由控制器用来限制对动作方法的访问。清单15-17显示了我添加到Infrastructure中的ClaimsRoles.cs文件的内容。 Listing 15-17. The Contents of the ClaimsRoles.cs File 清单15-17. ClaimsRoles.cs文件的内容 using System.Collections.Generic;using System.Security.Claims; namespace Users.Infrastructure {public class ClaimsRoles {public static IEnumerable<Claim> CreateRolesFromClaims(ClaimsIdentity user) {List<Claim> claims = new List<Claim>();if (user.HasClaim(x => x.Type == ClaimTypes.StateOrProvince&& x.Issuer == "RemoteClaims" && x.Value == "DC")&& user.HasClaim(x => x.Type == ClaimTypes.Role&& x.Value == "Employees")) {claims.Add(new Claim(ClaimTypes.Role, "DCStaff"));}return claims;} }} The gnarly looking CreateRolesFromClaims method uses lambda expressions to determine whether the user has a StateOrProvince claim from the RemoteClaims issuer with a value of DC and a Role claim with a value of Employees. If the user has both claims, then a Role claim is returned for the DCStaff role. Listing 15-18 shows how I call the CreateRolesFromClaims method from the Login action in the Account controller. CreateRolesFromClaims是一个粗糙的考察方法,它使用了Lambda表达式,以检查用户是否具有StateOrProvince声明(Claim),该声明来自于RemoteClaims发行者(Issuer),值为DC。也检查用户是否具有Role声明(Claim),其值为Employees。如果用户这两个声明都有,那么便返回一个DCStaff角色的Role声明。清单15-18显示了如何在Account控制器中的Login动作中调用CreateRolesFromClaims方法。 Listing 15-18. Generating Roles Based on Claims in the AccountController.cs File 清单15-18. 在AccountController.cs中根据声明生成角色 ...[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(LocationClaimsProvider.GetClaims(ident)); ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);}... I can then restrict access to an action method based on membership of the DCStaff role. Listing 15-19 shows a new action method I added to the Claims controller to which I have applied the Authorize attribute. 然后我可以根据DCStaff角色的成员,来限制对一个动作方法的访问。清单15-19显示了在Claims控制器中添加的一个新的动作方法,在该方法上已经运用了Authorize注解属性。 Listing 15-19. Adding a New Action Method to the ClaimsController.cs File 清单15-19. 在ClaimsController.cs文件中添加一个新的动作方法 using System.Security.Claims;using System.Web;using System.Web.Mvc;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [Authorize(Roles="DCStaff")]public string OtherAction() {return "This is the protected action";} }} Users will be able to access OtherAction only if their claims grant them membership to the DCStaff role. Membership of this role is generated dynamically, so a change to the user’s employment status or location information will change their authorization level. 只要用户的声明(Claims)承认他们是DCStaff角色的成员,那么他们便能访问OtherAction动作。该角色的成员是动态生成的,因此,若是用户的雇用状态或地点信息发生变化,也会改变他们的授权等级。 提示:请读者从这个例子中吸取其中的思想精髓。对于读物的理解程度,仁者见仁,智者见智,能领悟多少,全凭各人,译者感觉这里的思想有无数的可能。举例说明:(1)可以根据用户的身份进行授权,比如学生在校时是“学生”,毕业后便是“校友”;(2)可以根据用户所处的部门进行授权,人事部用户属于人事团队,销售部用户属于销售团队,各团队有其自己的应用;(3)下一小节的示例是根据用户的地点授权。简言之:一方面用户的各种声明(Claim)都可以用来进行授权;另一方面用户的声明(Claim)又是可以自定义的。于是可能的运用就无法估计了。总之一句话,这种基于声明的授权(Claims-Based Authorization)有无限可能!要是没有我这里的提示,是否所有读者在此处都会有所体会?——译者注 15.3.3 Authorizing Access Using Claims 15.3.3 使用声明(Claims)授权访问 The previous example is an effective demonstration of how claims can be used to keep authorizations fresh and accurate, but it is a little indirect because I generate roles based on claims data and then enforce my authorization policy based on the membership of that role. A more direct and flexible approach is to enforce authorization directly by creating a custom authorization filter attribute. Listing 15-20 shows the contents of the ClaimsAccessAttribute.cs file, which I added to the Infrastructure folder and used to create such a filter. 前面的示例有效地演示了如何用声明(Claims)来保持新鲜和准确的授权,但有点不太直接,因为我要根据声明(Claims)数据来生成了角色,然后强制我的授权策略基于角色成员。一个更直接且灵活的办法是直接强制授权,其做法是创建一个自定义的授权过滤器注解属性。清单15-20演示了ClaimsAccessAttribute.cs文件的内容,我将它添加在Infrastructure文件夹中,并用它创建了这种过滤器。 Listing 15-20. The Contents of the ClaimsAccessAttribute.cs File 清单15-20. ClaimsAccessAttribute.cs文件的内容 using System.Security.Claims;using System.Web;using System.Web.Mvc; namespace Users.Infrastructure {public class ClaimsAccessAttribute : AuthorizeAttribute {public string Issuer { get; set; }public string ClaimType { get; set; }public string Value { get; set; }protected override bool AuthorizeCore(HttpContextBase context) {return context.User.Identity.IsAuthenticated&& context.User.Identity is ClaimsIdentity&& ((ClaimsIdentity)context.User.Identity).HasClaim(x =>x.Issuer == Issuer && x.Type == ClaimType && x.Value == Value);} }} The attribute I have defined is derived from the AuthorizeAttribute class, which makes it easy to create custom authorization policies in MVC framework applications by overriding the AuthorizeCore method. My implementation grants access if the user is authenticated, the IIdentity implementation is an instance of ClaimsIdentity, and the user has a claim with the issuer, type, and value matching the class properties. Listing 15-21 shows how I applied the attribute to the Claims controller to authorize access to the OtherAction method based on one of the location claims created by the LocationClaimsProvider class. 我所定义的这个注解属性派生于AuthorizeAttribute类,通过重写AuthorizeCore方法,很容易在MVC框架应用程序中创建自定义的授权策略。在这个实现中,若用户是已认证的、其IIdentity实现是一个ClaimsIdentity实例,而且该用户有一个带有issuer、type以及value的声明(Claim),它们与这个类的属性是匹配的,则该用户便是允许访问的。清单15-21显示了如何将这个注解属性运用于Claims控制器,以便根据LocationClaimsProvider类创建的地点声明(Claim),对OtherAction方法进行授权访问。 Listing 15-21. Performing Authorization on Claims in the ClaimsController.cs File 清单15-21. 在ClaimsController.cs文件中执行基于声明的授权 using System.Security.Claims;using System.Web;using System.Web.Mvc;using Users.Infrastructure;namespace Users.Controllers {public class ClaimsController : Controller {[Authorize]public ActionResult Index() {ClaimsIdentity ident = HttpContext.User.Identity as ClaimsIdentity;if (ident == null) {return View("Error", new string[] { "No claims available" });} else {return View(ident.Claims);} } [ClaimsAccess(Issuer="RemoteClaims", ClaimType=ClaimTypes.PostalCode,Value="DC 20500")]public string OtherAction() {return "This is the protected action";} }} My authorization filter ensures that only users whose location claims specify a ZIP code of DC 20500 can invoke the OtherAction method. 这个授权过滤器能够确保只有地点声明(Claim)的邮编为DC 20500的用户才能请求OtherAction方法。 15.4 Using Third-Party Authentication 15.4 使用第三方认证 One of the benefits of a claims-based system such as ASP.NET Identity is that any of the claims can come from an external system, even those that identify the user to the application. This means that other systems can authenticate users on behalf of the application, and ASP.NET Identity builds on this idea to make it simple and easy to add support for authenticating users through third parties such as Microsoft, Google, Facebook, and Twitter. 基于声明的系统,如ASP.NET Identity,的好处之一是任何声明都可以来自于外部系统,即使是将用户标识到应用程序的那些声明。这意味着其他系统可以代表应用程序来认证用户,而ASP.NET Identity就建立在这样的思想之上,使之能够简单而方便地添加第三方认证用户的支持,如微软、Google、Facebook、Twitter等。 There are some substantial benefits of using third-party authentication: Many users will already have an account, users can elect to use two-factor authentication, and you don’t have to manage user credentials in the application. In the sections that follow, I’ll show you how to set up and use third-party authentication for Google users, which Table 15-8 puts into context. 使用第三方认证有一些实际的好处:许多用户已经有了账号、用户可以选择使用双因子认证、你不必在应用程序中管理用户凭据等等。在以下小节中,我将演示如何为Google用户建立并使用第三方认证,表15-8描述了事情的情形。 Table 15-8. Putting Third-Party Authentication in Context 表15-8. 第三方认证情形 Question 问题 Answer 回答 What is it? 什么是第三方认证? Authenticating with third parties lets you take advantage of the popularity of companies such as Google and Facebook. 第三方认证使你能够利用流行公司,如Google和Facebook,的优势。 Why should I care? 为何要关心它? Users don’t like having to remember passwords for many different sites. Using a provider with large-scale adoption can make your application more appealing to users of the provider’s services. 用户不喜欢记住许多不同网站的口令。使用大范围适应的提供器可使你的应用程序更吸引有提供器服务的用户。 How is it used by the MVC framework? 如何在MVC框架中使用它? This feature isn’t used directly by the MVC framework. 这不是一个直接由MVC框架使用的特性。 Note The reason I have chosen to demonstrate Google authentication is that it is the only option that doesn’t require me to register my application with the authentication service. You can get details of the registration processes required at http://bit.ly/1cqLTrE. 提示:我选择演示Google认证的原因是,它是唯一不需要在其认证服务中注册我应用程序的公司。有关认证服务注册过程的细节,请参阅http://bit.ly/1cqLTrE。 15.4.1 Enabling Google Authentication 15.4.1 启用Google认证 ASP.NET Identity comes with built-in support for authenticating users through their Microsoft, Google, Facebook, and Twitter accounts as well more general support for any authentication service that supports OAuth. The first step is to add the NuGet package that includes the Google-specific additions for ASP.NET Identity. Enter the following command into the Package Manager Console: ASP.NET Identity带有通过Microsoft、Google、Facebook以及Twitter账号认证用户的内建支持,并且对于支持OAuth的认证服务具有更普遍的支持。第一个步骤是添加NuGet包,包中含有用于ASP.NET Identity的Google专用附件。请在“Package Manager Console(包管理器控制台)”中输入以下命令: Install-Package Microsoft.Owin.Security.Google -version 2.0.2 There are NuGet packages for each of the services that ASP.NET Identity supports, as described in Table 15-9. 对于ASP.NET Identity支持的每一种服务都有相应的NuGet包,如表15-9所示。 Table 15-9. The NuGet Authenticaton Packages 表15-9. NuGet认证包 Name 名称 Description 描述 Microsoft.Owin.Security.Google Authenticates users with Google accounts 用Google账号认证用户 Microsoft.Owin.Security.Facebook Authenticates users with Facebook accounts 用Facebook账号认证用户 Microsoft.Owin.Security.Twitter Authenticates users with Twitter accounts 用Twitter账号认证用户 Microsoft.Owin.Security.MicrosoftAccount Authenticates users with Microsoft accounts 用Microsoft账号认证用户 Microsoft.Owin.Security.OAuth Authenticates users against any OAuth 2.0 service 根据任一OAuth 2.0服务认证用户 Once the package is installed, I enable support for the authentication service in the OWIN startup class, which is defined in the App_Start/IdentityConfig.cs file in the example project. Listing 15-22 shows the change that I have made. 一旦安装了这个包,便可以在OWIN启动类中启用此项认证服务的支持,启动类的定义在示例项目的App_Start/IdentityConfig.cs文件中。清单15-22显示了所做的修改。 Listing 15-22. Enabling Google Authentication in the IdentityConfig.cs File 清单15-22. 在IdentityConfig.cs文件中启用Google认证 using Microsoft.AspNet.Identity;using Microsoft.Owin;using Microsoft.Owin.Security.Cookies;using Owin;using Users.Infrastructure;using Microsoft.Owin.Security.Google;namespace Users {public class IdentityConfig {public void Configuration(IAppBuilder app) {app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create); app.UseCookieAuthentication(new CookieAuthenticationOptions {AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,LoginPath = new PathString("/Account/Login"),}); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);app.UseGoogleAuthentication();} }} Each of the packages that I listed in Table 15-9 contains an extension method that enables the corresponding service. The extension method for the Google service is called UseGoogleAuthentication, and it is called on the IAppBuilder implementation that is passed to the Configuration method. 表15-9所列的每个包都含有启用相应服务的扩展方法。用于Google服务的扩展方法名称为UseGoogleAuthentication,它通过传递给Configuration方法的IAppBuilder实现进行调用。 Next I added a button to the Views/Account/Login.cshtml file, which allows users to log in via Google. You can see the change in Listing 15-23. 下一步骤是在Views/Account/Login.cshtml文件中添加一个按钮,让用户能够通过Google进行登录。所做的修改如清单15-23所示。 Listing 15-23. Adding a Google Login Button to the Login.cshtml File 清单15-23. 在Login.cshtml文件中添加Google登录按钮 @model Users.Models.LoginModel@{ ViewBag.Title = "Login";}<h2>Log In</h2> @Html.ValidationSummary()@using (Html.BeginForm()) {@Html.AntiForgeryToken();<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><div class="form-group"><label>Name</label>@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })</div><div class="form-group"><label>Password</label>@Html.PasswordFor(x => x.Password, new { @class = "form-control" })</div><button class="btn btn-primary" type="submit">Log In</button>}@using (Html.BeginForm("GoogleLogin", "Account")) {<input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" /><button class="btn btn-primary" type="submit">Log In via Google</button>} The new button submits a form that targets the GoogleLogin action on the Account controller. You can see this method—and the other changes I made the controller—in Listing 15-24. 新按钮递交一个表单,目标是Account控制器中的GoogleLogin动作。可从清单15-24中看到该方法,以及在控制器中所做的其他修改。 Listing 15-24. Adding Support for Google Authentication to the AccountController.cs File 清单15-24. 在AccountController.cs文件中添加Google认证支持 using System.Threading.Tasks;using System.Web.Mvc;using Users.Models;using Microsoft.Owin.Security;using System.Security.Claims;using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.Owin;using Users.Infrastructure;using System.Web; namespace Users.Controllers {[Authorize]public class AccountController : Controller {[AllowAnonymous]public ActionResult Login(string returnUrl) {if (HttpContext.User.Identity.IsAuthenticated) {return View("Error", new string[] { "Access Denied" });}ViewBag.returnUrl = returnUrl;return View();}[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<ActionResult> Login(LoginModel details, string returnUrl) {if (ModelState.IsValid) {AppUser user = await UserManager.FindAsync(details.Name,details.Password);if (user == null) {ModelState.AddModelError("", "Invalid name or password.");} else {ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie); ident.AddClaims(LocationClaimsProvider.GetClaims(ident));ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident)); AuthManager.SignOut();AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, ident);return Redirect(returnUrl);} }ViewBag.returnUrl = returnUrl;return View(details);} [HttpPost][AllowAnonymous]public ActionResult GoogleLogin(string returnUrl) {var properties = new AuthenticationProperties {RedirectUri = Url.Action("GoogleLoginCallback",new { returnUrl = returnUrl})};HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();}[AllowAnonymous]public async Task<ActionResult> GoogleLoginCallback(string returnUrl) {ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();AppUser user = await UserManager.FindAsync(loginInfo.Login);if (user == null) {user = new AppUser {Email = loginInfo.Email,UserName = loginInfo.DefaultUserName,City = Cities.LONDON, Country = Countries.UK};IdentityResult result = await UserManager.CreateAsync(user);if (!result.Succeeded) {return View("Error", result.Errors);} else {result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);if (!result.Succeeded) {return View("Error", result.Errors);} }}ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false }, ident);return Redirect(returnUrl ?? "/");}[Authorize]public ActionResult Logout() {AuthManager.SignOut();return RedirectToAction("Index", "Home");}private IAuthenticationManager AuthManager {get {return HttpContext.GetOwinContext().Authentication;} }private AppUserManager UserManager {get {return HttpContext.GetOwinContext().GetUserManager<AppUserManager>();} }} } The GoogleLogin method creates an instance of the AuthenticationProperties class and sets the RedirectUri property to a URL that targets the GoogleLoginCallback action in the same controller. The next part is a magic phrase that causes ASP.NET Identity to respond to an unauthorized error by redirecting the user to the Google authentication page, rather than the one defined by the application: GoogleLogin方法创建了AuthenticationProperties类的一个实例,并为RedirectUri属性设置了一个URL,其目标为同一控制器中的GoogleLoginCallback动作。下一个部分是一个神奇阶段,通过将用户重定向到Google认证页面,而不是应用程序所定义的认证页面,让ASP.NET Identity对未授权的错误进行响应: ...HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");return new HttpUnauthorizedResult();... This means that when the user clicks the Log In via Google button, their browser is redirected to the Google authentication service and then redirected back to the GoogleLoginCallback action method once they are authenticated. 这意味着,当用户通过点击Google按钮进行登录时,浏览器被重定向到Google的认证服务,一旦在那里认证之后,便被重定向回GoogleLoginCallback动作方法。 I get details of the external login by calling the GetExternalLoginInfoAsync of the IAuthenticationManager implementation, like this: 我通过调用IAuthenticationManager实现的GetExternalLoginInfoAsync方法,我获得了外部登录的细节,如下所示: ...ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();... The ExternalLoginInfo class defines the properties shown in Table 15-10. ExternalLoginInfo类定义的属性如表15-10所示: Table 15-10. The Properties Defined by the ExternalLoginInfo Class 表15-10. ExternalLoginInfo类所定义的属性 Name 名称 Description 描述 DefaultUserName Returns the username 返回用户名 Email Returns the e-mail address 返回E-mail地址 ExternalIdentity Returns a ClaimsIdentity that identities the user 返回标识该用户的ClaimsIdentity Login Returns a UserLoginInfo that describes the external login 返回描述外部登录的UserLoginInfo I use the FindAsync method defined by the user manager class to locate the user based on the value of the ExternalLoginInfo.Login property, which returns an AppUser object if the user has been authenticated with the application before: 我使用了由用户管理器类所定义的FindAsync方法,以便根据ExternalLoginInfo.Login属性的值对用户进行定位,如果用户之前在应用程序中已经认证,该属性会返回一个AppUser对象: ...AppUser user = await UserManager.FindAsync(loginInfo.Login);... If the FindAsync method doesn’t return an AppUser object, then I know that this is the first time that this user has logged into the application, so I create a new AppUser object, populate it with values, and save it to the database. I also save details of how the user logged in so that I can find them next time: 如果FindAsync方法返回的不是AppUser对象,那么我便知道这是用户首次登录应用程序,于是便创建了一个新的AppUser对象,填充该对象的值,并将其保存到数据库。我还保存了用户如何登录的细节,以便下次能够找到他们: ...result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);... All that remains is to generate an identity the user, copy the claims provided by Google, and create an authentication cookie so that the application knows the user has been authenticated: 剩下的事情只是生成该用户的标识了,拷贝Google提供的声明(Claims),并创建一个认证Cookie,以使应用程序知道此用户已认证: ...ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);ident.AddClaims(loginInfo.ExternalIdentity.Claims);AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);... 15.4.2 Testing Google Authentication 15.4.2 测试Google认证 There is one further change that I need to make before I can test Google authentication: I need to change the account verification I set up in Chapter 13 because it prevents accounts from being created with e-mail addresses that are not within the example.com domain. Listing 15-25 shows how I removed the verification from the AppUserManager class. 在测试Google认证之前还需要一处修改:需要修改第13章所建立的账号验证,因为它不允许example.com域之外的E-mail地址创建账号。清单15-25显示了如何在AppUserManager类中删除这种验证。 Listing 15-25. Disabling Account Validation in the AppUserManager.cs File 清单15-25. 在AppUserManager.cs文件中取消账号验证 using Microsoft.AspNet.Identity;using Microsoft.AspNet.Identity.EntityFramework;using Microsoft.AspNet.Identity.Owin;using Microsoft.Owin;using Users.Models; namespace Users.Infrastructure {public class AppUserManager : UserManager<AppUser> {public AppUserManager(IUserStore<AppUser> store): base(store) {}public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options,IOwinContext context) {AppIdentityDbContext db = context.Get<AppIdentityDbContext>();AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db)); manager.PasswordValidator = new CustomPasswordValidator {RequiredLength = 6,RequireNonLetterOrDigit = false,RequireDigit = false,RequireLowercase = true,RequireUppercase = true}; //manager.UserValidator = new CustomUserValidator(manager) {// AllowOnlyAlphanumericUserNames = true,// RequireUniqueEmail = true//};return manager;} }} Tip you can use validation for externally authenticated accounts, but I am just going to disable the feature for simplicity. 提示:也可以使用外部已认证账号的验证,但这里出于简化,取消了这一特性。 To test authentication, start the application, click the Log In via Google button, and provide the credentials for a valid Google account. When you have completed the authentication process, your browser will be redirected back to the application. If you navigate to the /Claims/Index URL, you will be able to see how claims from the Google system have been added to the user’s identity, as shown in Figure 15-7. 为了测试认证,启动应用程序,通过点击“Log In via Google(通过Google登录)”按钮,并提供有效的Google账号凭据。当你完成了认证过程时,浏览器将被重定向回应用程序。如果导航到/Claims/Index URL,便能够看到来自Google系统的声明(Claims),已被添加到用户的标识中了,如图15-7所示。 Figure 15-7. Claims from Google 图15-7. 来自Google的声明(Claims) 15.5 Summary 15.5 小结 In this chapter, I showed you some of the advanced features that ASP.NET Identity supports. I demonstrated the use of custom user properties and how to use database migrations to preserve data when you upgrade the schema to support them. I explained how claims work and how they can be used to create more flexible ways of authorizing users. I finished the chapter by showing you how to authenticate users via Google, which builds on the ideas behind the use of claims. 本章向你演示了ASP.NET Identity所支持的一些高级特性。演示了自定义用户属性的使用,还演示了在升级数据架构时,如何使用数据库迁移保护数据。我解释了声明(Claims)的工作机制,以及如何将它们用于创建更灵活的用户授权方式。最后演示了如何通过Google进行认证结束了本章,这是建立在使用声明(Claims)的思想基础之上的。 本篇文章为转载内容。原文链接:https://blog.csdn.net/gz19871113/article/details/108591802。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-28 08:49:21
283
转载
转载文章
... 多平台游戏引擎 多处理器环境下的游戏编程 工作管道及游戏资产数据库 作者/译者简介 作者介绍:Jason Gregory在1994年开始任职专业软件工程师,自1999年3月开始在游戏产业中任职软件工程师。在圣迭哥Midway Home Entertainment公司开始游戏编程的他,为《疯狂飞行员(Freaky Flyers)》及《Crank the Weasel》开发PlayStation 2/Xbox上的动画系统。在2003年,他转到洛杉矶艺电,为《荣誉勋章:血战太平洋(Medal of Honor: Pacific Assault)》开发游戏引擎及游戏性技术,并在《荣誉勋章:空降神兵(Medal of Honor: Airborne)》中担任首席工程师。他现时是顽皮狗公司的通才程序员,为《神秘海域:德雷克船长的宝藏(Uncharted: Drake's Fortune)》及《神秘海域:纵横四海(Uncharted: Among Thieves)》开发引擎及游戏性软件。他也在南加州大学教授游戏技术的课程。 译者简介:叶劲峰(Milo Yip)从小自习编程,并爱好计算机图形学。上中学时兼职开发策略RPG《王子传奇》,该游戏在1995年于台湾发行。其后他获取了香港大学认知科学学士、香港中文大学系统工程及工程管理哲学硕士。毕业后在香港理工大学设计学院从事游戏引擎及相关技术的研发,职至项目主任。除发表学术文章外,也曾合著《DirectX9游戏编程实务》。2008年往上海育碧担任引擎工程师开发《美食从天而降(Cloudy with a Chance of Meatballs)》Xbox360/PS3/Wii/PC,2009年起于麻辣马开发《爱丽丝:疯狂回归(Alice: Madness Returns)》Xbox360/PS3/PC,2011年加入腾讯互动娱乐引擎技术中心担任专家工程师,所研发的技术已用于《斗战神》、《天涯明月刀》、《众神争霸》等项目中。 推荐序1 最初拿到《Game Engine Architecture》一书的英文版,是编辑侠少邮寄给我的打印版。他建议我接下翻译此书的合同。当时我正在杭州带领一个团队开发3D游戏引擎,我和我的同事都对这本书的内容颇有兴趣,两大本打印的英文书立刻在同事间传开。可惜那段时间个人精力顾及不来,把近千页的英文读物精读而后翻译成中文对个人的业余时间是个极大的挑战,不能担此翻译任务颇为遗憾。 不久以后听说Milo Yip(叶劲峰)已开始着手翻译,甚为欣喜。翻译此巨著,他一定是比我更合适的人选。我和Milo虽未曾蒙面,但神交已久。在网络上读过一些他的成长经历,和我颇为相似,心有戚戚。他对游戏3D实时渲染技术研究精深为我所不及,我们曾通过Google Talk讨论过许多技术问题,他都有独到的见解。翻译工作开始后,Milo是香港人,英文技术术语在香港的中文译法和大陆的有许多不同。但此书由大陆出版社出版,考虑到面对的读者主要是大陆程序员,Milo希望能更符合大陆程序员的用词习惯,所以在翻译一开始就通过Google Docs创建了协作页面,邀请大家共同探讨书中技术名词的中译名。从中我们可以一窥他作为译者的慎重。 三年之后,有幸在出版之前就拿到了完整的译本。这是一本用LaTeX精心排版的800页的电子书,我只花了一周时间,几乎是一口气读完。流畅的阅读享受,绝对不仅仅是因为原著精彩的内容,精美的版面和翔实的译注也加了不少分。 在阅读本书的过程中,我不只一次地获得共鸣。例如在第5章的内存管理系统的介绍中,作者介绍的几种游戏特有的内存管理方法我都曾在项目中用过,而这是第一次有书籍专门将这些方法详尽记录;又如第11章动画系统的介绍,我们也同样在3D引擎开发过程中改进原有动画片段混合方法的经历。虽然书中介绍的每个技术点,都可能可以在某篇论文,某本其他的书的章节,某篇网络blog上见过,但之前却无一本书可以把这些东西放在一起相互参照。对于从事游戏引擎开发的程序员来说,了解各种引擎在处理每个具体问题时的方案是相当重要的。而每种方案又各有利弊,即使不做引擎开发工作而是在某一特定游戏引擎上做游戏开发,从中也可以理解引擎的局限性以及可能的改进方法。尤其是第14章介绍的对游戏性相关系统的设计,各个开发人员几乎都是凭经验设计,很少见有书籍对这些做总结。对于基于渲染引擎做开发的游戏程序员,这是必须面对的工作,这一章会有很大的借鉴意义。 本书作者是业内资深的游戏引擎开发人,他所参于的《神秘海域》和《最后生还者》都是我的个人最爱。在玩游戏的过程中,作为游戏程序员的天性,自然会不断地猜想各个技术点是如何实现的,背后需要怎样的工具支持。能在书中一一得到印证是件特别开心的事情。作者反复强调代码实践的重要性,在书中遍布着C++代码。我不认为这些代码有直接取来使用的价值,但它们极大地帮助了读者理解书中的技术点。书中列出的顽皮狗工作室用lisp方言作为游戏配置脚本的范例也给我很大的启发,有了这些具体的代码示例以及作者本身的一线工程师背景,也让我确信书中那些关于主机游戏开发相关等,我所没有接触过的内容都也绝非泛泛而谈。 国内的游戏开发社区的壮大,主要是随最近十年的MMO风潮而生。而就在大型网络游戏在中国有些畸形发展,让这类游戏偏离电子游戏游戏性的趋势时,我们有幸迎来了为移动设备开发游戏的大潮。游戏开发的重心重新回到游戏性本身。我们更需要去借鉴单机游戏是如何为玩家带来更纯粹的游戏体验,我相信书中记录的各种技术点会变的更有帮助。 资深游戏开发及创业者 云风 @简悦云风 推荐序2 在我认识的许多游戏业开发同仁中,只有少数香港同胞,Milo Yip(叶劲峰)却正是这样一位给我印象非常深刻的优秀香港游戏开发者。我俩认识,是在Milo加入腾讯互动娱乐研发部引擎技术中心后,说来到现在也只是两年多时间。其间,他为人的谦逊务实,对待技术问题的严谨求真态度,对算法设计和性能优化的娴熟技术,都为人所称道。Milo一丝不苟的工作风格,甚至表现在对待技术文档排版这类事情上(Milo常执著地用LaTeX将技术文档排到完美),我想这一定是他在香港读大学、硕士及在香港理工大学的多媒体创新中心从事研究员,一贯沿袭至今的好作风。 我很高兴腾讯游戏有实力吸引到这样优秀的技术专家;即使在其已从上海迁回香港家中,依然选择到深圳腾讯互动娱乐总部工作。叶兄从此工作日每天早晚过关,来往香港和深圳两地,虽有舟车劳顿,但是兼顾了对家庭的照顾和在游戏引擎方面的专业研究,希望这样的状况是令他满意的。 认识叶兄当时,我便知道他在进行Jason Gregory所著《游戏引擎架构》一书的中译工作。因为自己从前也有业余翻译游戏开发有关书籍的经历,所以我能理解其中的辛苦和责任重大,对叶兄也更多一分钦佩。我以为,本书以及本书的中文读者最大的幸运便是,遇到叶兄这位对游戏有着如同对家对国般强烈责任感,犹如“游戏科学工作者”般的专业译者! 现在(2013年年末)无疑是游戏史上对独立游戏制作者最友好的年代。开发设备方便获得(相对过往仅由主机厂商授权才能获得专利开发设备,现在有一台智能手机和一台个人电脑就可以开发)、技术工具友好、调试过程简单方便,且互联网上有丰富的例程和开源代码参考,也有网上社区便于交流。很多爱好者能够很快地制作出可运行的游戏原型,其中一些也能发布到应用商店。 但是不全面掌握各方面知识,尤其是游戏引擎架构知识,往往只能停留在勉强修改、凑合重用别人提供的资源的应用程度上,难以做极限的性能改进,更妄谈革命式的架构创新。这样的程度是很难在成千上万的游戏中脱颖而出的。我们所认可的真正的游戏大作,必定是在某方面大幅超越用户期待的产品。为了打造这样的产品,游戏内容创作者(策划、美术等)需要“戴着镣铐跳舞”(在当前的机能下争取更多的创作自由度),而引擎架构合理的游戏可以经得起──也值得进行──反复优化,最终可以提供更多的自由度,这是大作出现的技术前提。 书的作者、译者、出版社的编者,加上读者,大家是因书而结缘的有缘人。因叶兄这本《游戏引擎架构》译著而在线上线下相识的读者们,你们是不是因“了解游戏引擎架构,从而制作/优化好游戏”这样的理想而结了缘呢? 亲爱的读者,愿你的游戏有一天因谜题巧妙绝伦、趣味超凡、虚拟世界气势磅礴、视觉效果逼真精美等专业因素取得业界褒奖,并得到玩家真诚的赞美。希望届时曾读叶兄这本《游戏引擎架构》译作的你,也可以回馈社会,回馈游戏开发的学习社区,帮助新人。希望你也可以建立微信公众号、博客等,或翻译游戏开发书籍,造福外语不好的读者,所以如果你的外语(英语、日语、韩语之于游戏行业比较重要)水平仍需精进,现在也可以同步加油了! 腾讯《天天爱消除》游戏团队Leader 沙鹰 @也是沙鹰 译序 数千年以来,艺术家们通过文学、绘画、雕塑、建筑、音乐、舞蹈、戏剧等传统艺术形式充实人类的精神层面。自20世纪中叶,计算机的普及派生出另一种艺术形式──电子游戏。游戏结合了上述传统艺术以及近代科技派生的其他艺术(如摄影、电影、动画),并且完全脱离了艺术欣赏这种单向传递的方式──游戏必然是互动的,“玩家”并不是“读者”、“观众”或“听众”,而是进入游戏世界、感知并对世界做出反应的参与者。 基于游戏的互动本质,游戏的制作通常比其他大众艺术复杂。商业游戏的制作通常需要各种人才的参与,而他们则需要依赖各种工具及科技。游戏引擎便是专门为游戏而设计的工具及科技集成。之所以称为引擎,如同交通工具中的引擎,提供了最核心的技术部分。因为复杂,研发成本高,人们不希望制作每款游戏(或车款)时都重新设计引擎,重用性是游戏引擎的一个重要设计目标。 然而,各游戏本身的性质以及平台的差异,使研发完全通用的游戏引擎变得极困难,甚至不可能。市面上出售的游戏引擎,有一些虽然已经达到很高的技术水平,但在商业应用中,很多时候还是需要因应个别游戏项目对引擎改造、整合、扩展及优化。因此,即使能使用市面上最好的商用引擎或自研引擎,我们仍需要理解当中的架构、各种机制和技术,并且分析及解决在制作中遇到的问题。这些也是译者曾任于上海两家工作室时的主要工作范畴。 选择翻译此著作,主要原因是在阅读中得到共鸣,并且能知悉一些知名游戏作品实际上所采用的方案。有感坊间大部分游戏开发书籍并不是由业内人士执笔,内容只足够应付一些最简单的游戏开发,欠缺宏观比较各种方案,技术与当今实际情况也有很大差距。而一些Gems类丛书虽然偶有好文章,但受形式所限欠缺系统性、全面性。难得本书原作者身为世界一流游戏工作室的资深游戏开发者(注1),在繁重的游戏开发工作外,还在大学教授游戏开发课程以至编写本著作。此外,从与内地同事的交流中,了解到许多从业者不愿意阅读外文书籍。为了普及知识及反馈业界社会,希望能尽绵力。 或许有些人以为本著作是针对单机/游戏机游戏的,并不适合国内以网游为主的环境。但译者认为这是一种误解,许多游戏本身所涉及的技术是具通用性的。例如游戏性相关的游戏性系统、场景管理、人工智能、物理模拟等部分,许多时候也会同时用于网游的前台和后台。现时,一些动作为主、非MMO的国内端游甚至会直接在后台运行传统意义上的游戏引擎。至于前台相关的技术,单机和端游的区别更少。此外,随着近年移动终端的兴起,其硬件性能已超越传统掌上游戏机,开发手游所需的技术与传统掌上游戏机并无太大差异。还可预料,现时单机/游戏机的一些较高级的架构及技术,将在不远的未来着陆移动终端平台。 译者认为,本书涵括游戏开发技术的方方面面,同时适合入门及经验丰富的游戏程序员。书名中的架构二字,并不单是给出一个系统结构图,而是描述每个子系统的需求、相关技术及与其他子系统的关系。对译者本人而言,本书的第11章(动画系统)及第14章(运行时游戏性基础系统)是本书特別精彩之处,含有许多少见于其他书籍的内容。而第10章(渲染引擎)由于是游戏引擎中的一个极大的部分,有限的篇幅可能未能覆盖广度及深度,推荐读者参考[1](注2),人工智能方面也需参考其他专著。 本译作采用LaTeX排版(注3),以Inkscape编译矢量图片。为了令阅读更流畅,内文中的网址都统一改以脚注标示。另外,由于现时游戏开发相关的文献以英文为主,而且游戏开发涉及的知识面很广,本译作尽量以括号形式保留英文术语。为了方便读者查找内容,在附录中增设中英文双向索引(索引条目与原著的不同)。 本人在香港成长学习及工作,至2008年才赴内地游戏工作室工作,不黯内地的中文写作及用字习惯,翻译中曾遇到不少困难。有幸得到出版社人员以及良师益友的帮助,才能完成本译作。特别感谢周筠老师支持本作的提案,并耐心地给予协助及鼓励。编辑张春雨老师和卢鸫翔老师,以及好友余晟给予了大量翻译上的知识及指导。也感谢游戏业界专家云风、大宝和Dave给予了许多宝贵意见。此书的翻译及排版工作比预期更花时间,感谢妻子及儿女们的体谅。此次翻译工作历时三年半,因工作及家庭事宜导致严重延误,唯有在翻译及排版工作上更尽心尽力,希望求得等待此译作的读者们谅解。无论是批评或建议,诚希阁下通过电邮miloyip@gmail.com、新浪微博、豆瓣等渠道不吝赐教。 叶劲峰(Milo Yip) 2013年10月 原作者是顽皮狗(Naughty Dog)《神秘海域(Uncharted)》系列的通才程序员、《最后生还者(The Last of Us)》的首席程序员,之前还曾在EA和Midway工作。 中括号表示引用附录中的参考文献。一些参考条目加入了其中译本的信息。 具体是使用CTEX套装,它是在MiKTeX的基础上增加中文的支持。 前言 最早的电子游戏完全由硬件构成,但微处理器(microprocessor)的高速发展完全改变了游戏的面貌。现在的游戏是在多用途的PC和专门的电子游戏主机(video game console)上玩的,凭借软件带来绝妙的游戏体验。从最初的游戏诞生至今已有半个世纪,但很多人仍然认为游戏是一个未成熟的产业。即使游戏可能是个年轻的产业,若仔细观察,也会发现它正在高速发展。 现时游戏已成为一个上百亿美元的产业,覆盖不同年龄、性别的广泛受众。 千变万化的游戏,可以分为从纸牌游戏到大型多人在线游戏(massively multiplayer online game,MMOG)等多个种类(category)和“类型(genre)”(注1),也可以运行在任何装有微芯片(microchip)的设备上 。你现在可以在PC、手机及多种特别为游戏而设计的手持/电视游戏主机上玩游戏。家用电视游戏通常代表最尖端的游戏科技,又由于它们是周期性地推出新版本,因此有游戏机“世代”(generation)的说法。最新一代(注2)的游戏机包括微软的Xbox 360和索尼的PlayStation 3,但一定不可忽视长盛不衰的PC,以及最近非常流行的任天堂Wii。 最近,剧增的下载式休闲游戏,使这个多样化的商业游戏世界变得更复杂。虽然如此,大型游戏仍然是一门大生意。今天的游戏平台非常复杂,有难以置信的运算能力,这使软件的复杂度得以进一步提升。所有这些先进的软件都需要由人创造出来,这导致团队人数增加,开发成本上涨。随着产业变得成熟,开发团队要寻求更好、更高效的方式去制作产品,可复用软件(reusable software)和中间件(middleware)便应运而生,以补偿软件复杂度的提升。 由于有这么多风格迥异的游戏及多种游戏平台,因此不可能存在单一理想的软件方案。然而,业界已经发展出一些模式 ,也有大量的潜在方案可供选择。现今的问题是如何找到一个合适的方案去迎合某个项目的需要。再进一步,开发团队必须考虑项目的方方面面,以及如何把各方面集成。对于一个崭新的游戏设计,鲜有可能找到一个完美搭配游戏设计各方面的软件包。 现时业界内的老手,入行时都是“开荒牛”。我们这代人很少是计算机科学专业出身(Matt的专业是航空工程、Jason的专业是系统设计工程),但现时很多学院已设有游戏开发的课程和学位。时至今日,为了获取有用的游戏开发信息,学生和开发者必须找到好的途径。对于高端的图形技术,从研究到实践都有大量高质量的信息。可是,这些信息经常不能直接应用到游戏的生产环境,或者没有一个生产级质量的实现。对于图形以外的游戏开发技术,市面上有一些所谓的入门书籍,没提及参考文献就描述很多内容细节,像自己发明的一样。这种做法根本没有用处,甚至经常带有不准确的内容。另一方面,市场上有一些高端的专门领域书籍,例如物理、碰撞、人工智能等。可是,这类书或者啰嗦到让你难以忍受,或者高深到让部分读者无法理解,又或者内容过于零散而难于融会贯通。有一些甚至会直接和某项技术挂钩,软硬件一旦改动,其内容就会迅速过时。 此外,互联网也是收集相关知识的绝佳工具。可是,除非你确实知道要找些什么,否则断链、不准确的资料、质量差的内容也会成为学习障碍。 好在,我们有Jason Gregory,他是一位拥有在顽皮狗(Naughty Dog)工作经验的业界老手,而顽皮狗是全球高度瞩目的游戏工作室之一。Jason在南加州大学教授游戏编程课程时,找不到概括游戏架构的教科书。值得庆幸的是,他承担了这个任务,填补了这个空白。 Jason把应用到实际发行游戏的生产级别知识,以及整个游戏开发的大局编集于本书。他凭经验,不仅融汇了游戏开发的概念和技巧,还用实际的代码示例及实现例子去说明怎样贯通知识来制作游戏。本书的引用及参考文献可以让读者更深入探索游戏开发过程的各方面。虽然例子经常是基于某些技术的,但是概念和技巧是用来实际创作游戏的,它们可以超越个别引擎或API的束缚。 本书是一本我们入行做游戏时想要的书。我们认为本书能让入门者增长知识,也能为有经验者开拓更大的视野。 Jeff Lander(注3) Matthew Whiting(注4) 译注:Genre一词在文学中为体裁。电影和游戏里通常译作类型。不同的游戏类型可见1.2节。 译注:按一般说法,2005年至今属于第7个游戏机世代。这3款游戏机的发行年份为Xbox 360(2005)、PlayStation 3(2006)、Wii(2006)。有关游戏机世代可参考维基百科。 译注:Jeff Lander现时为Darwin 3D公司的首席技术总监、Game Tech公司创始人,曾为艺电首席程序员、Luxoflux公司游戏性及动画技术程序员。 译注:Matthew Whiting现时为Wholesale Algorithms公司程序员,曾为Luxoflux公司首席软件工程师、Insomniac Games公司程序员。 序言 欢迎来到《游戏引擎架构》世界。本书旨在全面探讨典型商业游戏引擎的主要组件。游戏编程是一个庞大的主题,有许多内容需要讨论。不过相信你会发现,我们讨论的深度将足以使你充分理解本书所涵盖的工程理论及常用实践的方方面面。话虽如此,令人着迷的漫长游戏编程之旅其实才刚刚启程。与此相关的每项技术都包含丰富内容,本书将为你打下基础,并引领你进入更广阔的学习空间。 本书焦点在于游戏引擎的技术及架构。我们会探讨商业游戏引擎中,各个子系统的相关理论,以及实现这些理论所需要的典型数据结构、算法和软件接口。游戏引擎与游戏的界限颇为模糊。我们将把注意力集中在引擎本身,包括多个低阶基础系统(low-level foundation system)、渲染引擎(rendering engine)、碰撞系统(collision system)、物理模拟(physics simulation)、人物动画(character animation),及一个我称为游戏性基础层(gameplay foundation layer)的深入讨论。此层包括游戏对象模型(game object model)、世界编辑器(world editor)、事件系统(event system)及脚本系统(scripting system)。我们也将会接触游戏性编程(gameplay programming)的多个方面,包括玩家机制(player mechanics)、摄像机(camera)及人工智能(artificial intelligence,AI)。然而,这类讨论会被限制在游戏性系统和引擎接口范围。 本书可以作为大学中等级游戏程序设计中两到三门课程的教材。当然,本书也适合软件工程师、业余爱好者、自学的游戏程序员,以及游戏行业从业人员。通过阅读本书,资历较浅的游戏程序员可以巩固他们所学的游戏数学、引擎架构及游戏科技方面的知识。专注某一领域的资深程序员也能从本书更为全面的介绍中获益。 为了更好地学习本书内容,你需要掌握基本的面向对象编程概念并至少拥有一些C++编程经验。尽管游戏行业已经开始尝试使用一些新的、令人兴奋的编程语言,然而工业级的3D游戏引擎仍然是用C或C++编写的,任何认真的游戏程序员都应该掌握C++。我们将在第3章重温一些面向对象编程的基本原则,毫无疑问,你还会从本书学到一些C++的小技巧,不过C++的基础最好还是通过阅读[39]、[31]及[32]来获得。如果你对C++已经有点生疏,建议你在阅读本书的同时,最好能重温这几本或者类似书籍。如果你完全没有C++经验,在看本书之前,可以考虑先阅读[39]的前几章,或者尝试学习一些C++的在线教程。 学习编程技能最好的方法就是写代码。在阅读本书时,强烈建议你选择一些特别感兴趣的主题付诸实践。举例来说,如果你觉得人物动画很有趣,那么可以首先安装OGRE,并测试一下它的蒙皮动画示范。接着还可以尝试用OGRE实现本书谈及的一些动画混合技巧。下一步你可能会打算用游戏手柄控制人物在平面上行走。等你能玩转一些简单的东西了,就应该以此为基础,继续前进!之后可以转移到另一个游戏技术范畴,周而复始。这些项目是什么并不重要,重要的是你在实践游戏编程的艺术,而不是纸上谈兵。 游戏科技是一个活生生、会呼吸的家伙 ,永远不可能将之束缚于书本之上 。因此,附加的资源、勘误、更新、示例代码、项目构思等已经发到本书的网站。 目录 推荐序1 iii推荐序2 v译序 vii序言 xvii前言 xix致谢 xxi第一部分 基础 1第1章 导论 31.1 典型游戏团队的结构 41.2 游戏是什么 71.3 游戏引擎是什么 101.4 不同游戏类型中的引擎差异 111.5 游戏引擎概观 221.6 运行时引擎架构 271.7 工具及资产管道 46第2章 专业工具 532.1 版本控制 532.2 微软Visual Studio 612.3 剖析工具 782.4 内存泄漏和损坏检测 792.5 其他工具 80第3章 游戏软件工程基础 833.1 重温C++及最佳实践 833.2 C/C++的数据、代码及内存 903.3 捕捉及处理错误 118第4章 游戏所需的三维数学 1254.1 在二维中解决三维问题 1254.2 点和矢量 1254.3 矩阵 1394.4 四元数 1564.5 比较各种旋转表达方式 1644.6 其他数学对象 1684.7 硬件加速的SIMD运算 1734.8 产生随机数 180第二部分 低阶引擎系统 183第5章 游戏支持系统 1855.1 子系统的启动和终止 1855.2 内存管理 1935.3 容器 2085.4 字符串 2255.5 引擎配置 234第6章 资源及文件系统 2416.1 文件系统 2416.2 资源管理器 251第7章 游戏循环及实时模拟 2777.1 渲染循环 2777.2 游戏循环 2787.3 游戏循环的架构风格 2807.4 抽象时间线 2837.5 测量及处理时间 2857.6 多处理器的游戏循环 2967.7 网络多人游戏循环 304第8章 人体学接口设备(HID) 3098.1 各种人体学接口设备 3098.2 人体学接口设备的接口技术 3118.3 输入类型 3128.4 输出类型 3168.5 游戏引擎的人体学接口设备系统 3188.6 人体学接口设备使用实践 332第9章 调试及开发工具 3339.1 日志及跟踪 3339.2 调试用的绘图功能 3379.3 游戏内置菜单 3449.4 游戏内置主控台 3479.5 调试用摄像机和游戏暂停 3489.6 作弊 3489.7 屏幕截图及录像 3499.8 游戏内置性能剖析 3499.9 游戏内置的内存统计和泄漏检测 356第三部分 图形及动画 359第10章 渲染引擎 36110.1 采用深度缓冲的三角形光栅化基础 36110.2 渲染管道 40410.3 高级光照及全局光照 42610.4 视觉效果和覆盖层 43810.5 延伸阅读 446第11章 动画系统 44711.1 角色动画的类型 44711.2 骨骼 45211.3 姿势 45411.4 动画片段 45911.5 蒙皮及生成矩阵调色板 47111.6 动画混合 47611.7 后期处理 49311.8 压缩技术 49611.9 动画系统架构 50111.10 动画管道 50211.11 动作状态机 51511.12 动画控制器 535第12章 碰撞及刚体动力学 53712.1 你想在游戏中加入物理吗 53712.2 碰撞/物理中间件 54212.3 碰撞检测系统 54412.4 刚体动力学 56912.5 整合物理引擎至游戏 60112.6 展望:高级物理功能 616第四部分 游戏性 617第13章 游戏性系统简介 61913.1 剖析游戏世界 61913.2 实现动态元素:游戏对象 62313.3 数据驱动游戏引擎 62613.4 游戏世界编辑器 627第14章 运行时游戏性基础系统 63714.1 游戏性基础系统的组件 63714.2 各种运行时对象模型架构 64014.3 世界组块的数据格式 65714.4 游戏世界的加载和串流 66314.5 对象引用与世界查询 67014.6 实时更新游戏对象 67614.7 事件与消息泵 69014.8 脚本 70714.9 高层次的游戏流程 726第五部分 总结 727第15章 还有更多内容吗 72915.1 一些未谈及的引擎系统 72915.2 游戏性系统 730参考文献 733中文索引 737英文索引 755 参考文献 Tomas Akenine-Moller, Eric Haines, and Naty Hoffman. Real-Time Rendering (3rd Edition). Wellesley, MA: A K Peters, 2008. 中译本:《实时计算机图形学(第2版)》,普建涛译,北京大学出版社,2004. Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. Resding, MA: Addison-Wesley, 2001. 中译本:《C++设计新思维:泛型编程与设计模式之应用》,侯捷/於春景译,华中科技大学出版社,2003. Grenville Armitage, Mark Claypool and Philip Branch. Networking and Online Games: Understanding and Engineering Multiplayer Internet Games. New York, NY: John Wiley and Sons, 2006. James Arvo (editor). Graphics Gems II. San Diego, CA: Academic Press, 1991. Grady Booch, Robert A. Maksimchuk, Michael W. Engel, Bobbi J. Young, Jim Conallen, and Kelli A. Houston. Object-Oriented Analysis and Design with Applications (3rd Edition). Reading, MA: Addison-Wesley, 2007. 中译本:《面向对象分析与设计(第3版)》,王海鹏/潘加宇译,电子工业出版社,2012. Mark DeLoura (editor). Game Programming Gems. Hingham, MA: Charles River Media, 2000. 中译本:《游戏编程精粹 1》, 王淑礼译,人民邮电出版社,2004. Mark DeLoura (editor). Game Programming Gems 2. Hingham, MA: Charles River Media, 2001. 中译本:《游戏编程精粹 2》,袁国忠译,人民邮电出版社,2003. Philip Dutré, Kavita Bala and Philippe Bekaert. Advanced Global Illumination (2nd Edition). Wellesley, MA: A K Peters, 2006. David H. Eberly. 3D Game Engine Design: A Practical Approach to Real-Time Computer Graphics. San Francisco, CA: Morgan Kaufmann, 2001. 国内英文版:《3D游戏引擎设计:实时计算机图形学的应用方法(第2版)》,人民邮电出版社,2009. David H. Eberly. 3D Game Engine Architecture: Engineering Real-Time Applications with Wild Magic. San Francisco, CA: Morgan Kaufmann, 2005. David H. Eberly. Game Physics. San Francisco, CA: Morgan Kaufmann, 2003. Christer Ericson. Real-Time Collision Detection. San Francisco, CA: Morgan Kaufmann, 2005. 中译本:《实时碰撞检测算法技术》,刘天慧译,清华大学出版社,2010. Randima Fernando (editor). GPU Gems: Programming Techniques, Tips and Tricks for Real-Time Graphics. Reading, MA: Addison-Wesley, 2004. 中译本:《GPU精粹:实时图形编程的技术、技巧和技艺》,姚勇译,人民邮电出版社,2006. James D. Foley, Andries van Dam, Steven K. Feiner, and John F. Hughes. Computer Graphics: Principles and Practice in C (2nd Edition). Reading, MA: Addison-Wesley, 1995. 中译本:《计算机图形学原理及实践──C语言描述》,唐泽圣/董士海/李华/吴恩华/汪国平译,机械工业出版社,2004. Grant R. Fowles and George L. Cassiday. Analytical Mechanics (7th Edition). Pacific Grove, CA: Brooks Cole, 2005. John David Funge. AI for Games and Animation: A Cognitive Modeling Approach. Wellesley, MA: A K Peters, 1999. Erich Gamma, Richard Helm, Ralph Johnson, and John M. Vlissiddes. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1994. 中译本:《设计模式:可复用面向对象软件的基础》,李英军/马晓星/蔡敏/刘建中译,机械工业出版社,2005. Andrew S. Glassner (editor). Graphics Gems I. San Francisco, CA: Morgan Kaufmann, 1990. Paul S. Heckbert (editor). Graphics Gems IV. San Diego, CA: Academic Press, 1994. Maurice Herlihy, Nir Shavit. The Art of Multiprocessor Programming. San Francisco, CA: Morgan Kaufmann, 2008. 中译本:《多处理器编程的艺术》,金海/胡侃译,机械工业出版社,2009. Roberto Ierusalimschy, Luiz Henrique de Figueiredo and Waldemar Celes. Lua 5.1 Reference Manual. Lua.org, 2006. Roberto Ierusalimschy. Programming in Lua, 2nd Edition. Lua.org, 2006. 中译本:《Lua程序设计(第2版)》,周惟迪译,电子工业出版社,2008. Isaac Victor Kerlow. The Art of 3-D Computer Animation and Imaging (2nd Edition). New York, NY: John Wiley and Sons, 2000. David Kirk (editor). Graphics Gems III. San Francisco, CA: Morgan Kaufmann, 1994. Danny Kodicek. Mathematics and Physics for Game Programmers. Hingham, MA: Charles River Media, 2005. Raph Koster. A Theory of Fun for Game Design. Phoenix, AZ: Paraglyph, 2004. 中译本:《快乐之道:游戏设计的黄金法则》,姜文斌等译,百家出版社,2005. John Lakos. Large-Scale C++ Software Design. Reading, MA: Addison-Wesley, 1995. 中译本:《大规模C++程序设计》,李师贤/明仲/曾新红/刘显明译,中国电力出版社,2003. Eric Lengyel. Mathematics for 3D Game Programming and Computer Graphics (2nd Edition). Hingham, MA: Charles River Media, 2003. Tuoc V. Luong, James S. H. Lok, David J. Taylor and Kevin Driscoll. Internationalization: Developing Software for Global Markets. New York, NY: John Wiley & Sons, 1995. Steve Maguire. Writing Solid Code: Microsoft's Techniques for Developing Bug Free C Programs. Bellevue, WA: Microsoft Press, 1993. 国内英文版:《编程精粹:编写高质量C语言代码》,人民邮电出版社,2009. Scott Meyers. Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition). Reading, MA: Addison-Wesley, 2005. 中译本:《Effective C++:改善程序与设计的55个具体做法(第3版)》,侯捷译,电子工业出版社,2011. Scott Meyers. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Reading, MA: Addison-Wesley, 1996. 中译本:《More Effective C++:35个改善编程与设计的有效方法(中文版)》,侯捷译,电子工业出版社,2011. Scott Meyers. Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library. Reading, MA: Addison-Wesley, 2001. 中译本:《Effective STL:50条有效使用STL的经验》,潘爱民/陈铭/邹开红译,电子工业出版社,2013. Ian Millington. Game Physics Engine Development. San Francisco, CA: Morgan Kaufmann, 2007. Hubert Nguyen (editor). GPU Gems 3. Reading, MA: Addison-Wesley, 2007. 中译本:《GPU精粹3》,杨柏林/陈根浪/王聪译,清华大学出版社,2010. Alan W. Paeth (editor). Graphics Gems V. San Francisco, CA: Morgan Kaufmann, 1995. C. Michael Pilato, Ben Collins-Sussman, and Brian W. Fitzpatrick. Version Control with Subversion (2nd Edition). Sebastopol , CA: O'Reilly Media, 2008. (常被称作“The Subversion Book”,线上版本.) 国内英文版:《使用Subversion进行版本控制》,开明出版社,2009. Matt Pharr (editor). GPU Gems 2: Programming Techniques for High-Performance Graphics and General-Purpose Computation. Reading, MA: Addison-Wesley, 2005. 中译本:《GPU精粹2:高性能图形芯片和通用计算编程技巧》,龚敏敏译,清华大学出版社,2007. Bjarne Stroustrup. The C++ Programming Language, Special Edition (3rd Edition). Reading, MA: Addison-Wesley, 2000. 中译本《C++程序设计语言(特别版)》,裘宗燕译,机械工业出版社,2010. Dante Treglia (editor). Game Programming Gems 3. Hingham, MA: Charles River Media, 2002. 中译本:《游戏编程精粹3》,张磊译,人民邮电出版社,2003. Gino van den Bergen. Collision Detection in Interactive 3D Environments. San Francisco, CA: Morgan Kaufmann, 2003. Alan Watt. 3D Computer Graphics (3rd Edition). Reading, MA: Addison Wesley, 1999. James Whitehead II, Bryan McLemore and Matthew Orlando. World of Warcraft Programming: A Guide and Reference for Creating WoW Addons. New York, NY: John Wiley & Sons, 2008. 中译本:《魔兽世界编程宝典:World of Warcraft Addons完全参考手册》,杨柏林/张卫星/王聪译,清华大学出版社,2010. Richard Williams. The Animator's Survival Kit. London, England: Faber & Faber, 2002. 中译本:《原动画基础教程:动画人的生存手册》,邓晓娥译,中国青年出版社,2006. 勘误 第1次印册(2014年2月) P.xviii: 译注中 Wholesale Algoithms -> Wholesale Algorithms P.10: 最后一段第一行 微软的媒体播放器 -> 微软的Windows Media Player (多谢读者OpenGPU来函指正) P.15: 1.4.3节第三点 按妞 -> 按钮 (多谢读者一个小小凡人来函指正) P.40: 正文最后一行 按扭 -> 按钮 P.50: 1.7.8节第二节第一行 同是 -> 同时 (多谢读者czfdd来函指正) P.98: 代码 writeExampleStruct(Example& ex, Stream& ex) 中 Stream& ex -> Stream& stream (多谢读者Snow来函指正) P.106: 第一段中有六处 BBS -> BSS,最后一段代码的注释也有同样错误 (多谢读者trout来函指正) P.119: 译注中 软体工程 -> 软件工程 (多谢读者Snow来函指正) P.214: 正文第一段有两处 虚内存 -> 虚拟内存 (多谢读者Snow来函指正) P.216: 脚注24应标明为译注 (多谢读者Snow来函指正) P.221: 第一段代码的第二个断言应为 ASSERT(link.m_pPrev != NULL); (多谢读者Snow来函指正) P.230: 5.4.4.1节 第二段 软体 -> 软件 P.286: 脚注4应标明为译注 (多谢读者Snow来函指正) P.322: 第二段 按扭事件字 -> 按钮事件 P.349: 9.8节第二段第二行两处 部析器 -> 剖析器 (多谢读者Snow来函指正) P.738-572: 双数页页眉 参考文献 -> 中文索引 P.755-772: 双数页页眉 参考文献 -> 英文索引 P.755: kd tree项应归入K而不是Symbols 以上的错误已于第2次印册中修正。 第2次印册及之前 P.11: 第四行 细致程度 -> 层次细节 (这是level-of-detail/LOD的内地通译,多谢读者OpenGPU来函指正) P.12: 正文第一段及图1.2标题 使命之唤 -> 使命召唤 (多谢读者OpenGPU来函指正) P.12: 正文第一段 战栗时空 -> 半条命 (多谢读者OpenGPU来函指正) P.16: 第一点 表面下散射 -> 次表面散射 (多谢读者OpenGPU来函指正) P.17: 1.4.4节第五行 次文化 -> 亚文化 (此译法在内地更常用。多谢读者OpenGPU来函提示) P.22: 战栗时空 -> 半条命 P.24: 战栗时空2 -> 半条命2 P.34: 1.6.8.2节第一行 提呈 -> 提交 (这术语在本书其他地方都写作提交。多谢读者OpenGPU来函提示) P.35: 第七行 提呈 -> 提交 (这术语在本书其他地方都写作提交。多谢读者OpenGPU来函提示) P.50: 战栗时空2 -> 半条命2 P.365: 第四段第二行: 细致程度 -> 层次细节 P.441: 10.4.3.2节第三行 细致程度 -> 层次细节 P.494: sinusiod -> sinusoid (多谢读者OpenGPU来函指正) P.511: 11.10.4节第一行 谈入 -> 淡入 (多谢读者Snow来函指正) P.541: 战栗时空2 -> 半条命2 P.627: 战栗时空2 -> 半条命2 P.654: 第二行 建康值 -> 血量 (原来是改正错别字,但译者发现应改作前后统一使用的“血量”。多谢读者Snow来函指正) P.692: 第二行 内部分式 -> 内部方式 (多谢读者Snow来函指正) P.696: 14.7.6节第四行 不设实际 -> 不切实际 (多谢读者Snow来函指正) 以上的错误已于第3次印册中修正。 其他意见 P.220: 正文第一段 m_root.m_pElement 和 P.218 第一段代码中的 m_pElem 不统一。原文有此问题,但因为它们是不同的struct,暂不列作错误。 (多谢读者Snow来函提示) P.331: 8.5.8节第二段中 “反覆”较常见的写法为“反复”,但前者也是正确的,暂不列作错误。 (多谢读者Snow来函提示) P.390: 10.1.3.3节静态光照第二段中“取而代之,我们会使用一张光照纹理贴到所有受光源影响范围内的物体上。这样做能令动态物体经过光源时得到正确的光照。” 后面的一句与前句好像难以一起理解。译者认为,作者应该是指,使用同一静态光源去为静态物件生成光照纹理,以及用于动态对象的光照,能使两者的效果维持一致性。译者会考虑对译文作出改善或加入译注解译。(多谢读者店残来函查询) P.689: 第五行 并行处理世代 -> 并行处理时代 是对era较准确的翻译。 (多谢读者Snow来函提示) 本篇文章为转载内容。原文链接:https://blog.csdn.net/mypongo/article/details/38388381。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-02-12 23:04:05
327
转载
转载文章
...展库 filterbuilder.jar htmllexer.jar htmlparser.jar jsoup-1.9.2.jar junit.jar sax2.jar thumbelina.jar 2、执行以下方法 package com.zgs.look;import java.io.BufferedReader;import java.io.File;import java.io.FileInputStream;import java.io.InputStreamReader;import java.util.HashMap;import java.util.Map;import java.util.regex.Matcher;import java.util.regex.Pattern;import org.htmlparser.Node;import org.htmlparser.NodeFilter;import org.htmlparser.Parser;import org.htmlparser.filters.NodeClassFilter;import org.htmlparser.filters.OrFilter;import org.htmlparser.tags.LinkTag;import org.htmlparser.tags.TableTag;import org.htmlparser.util.NodeList;import org.jsoup.Jsoup;import org.jsoup.nodes.Document;import org.jsoup.nodes.Element;import org.jsoup.select.Elements;public class HtmlLook {private static String ENCODE = "UTF-8";public static void main(String[] args) {String szContent = openFile( "d:/index.html");try {Document doc = Jsoup.parse(szContent);Elements elList=doc.getElementsByAttributeValue("id","vulDataTable");szContent=elList.outerHtml();Parser parser = Parser.createParser(szContent, ENCODE);NodeFilter[] filters = new NodeFilter[2];filters[0] = new NodeClassFilter(TableTag.class); filters[1] = new NodeClassFilter(LinkTag.class);NodeFilter filter =new OrFilter (filters);NodeList list = parser.extractAllNodesThatMatch(filter);String ldName="";String ldJianjie="";for (int i = 0; i < list.size(); i++) { Node node = list.elementAt(i); if(node instanceof LinkTag){String nodeHtml=node.toHtml();if(nodeHtml.contains("onclick")&&nodeHtml.contains("vul-")){if(!"".equals(ldName)&&!"".equals(ldJianjie)){//提交数据System.out.println("---commit---漏洞名称-------"+ldName);System.out.println("---commit---漏洞简介-------"+ldJianjie);ldName="";ldJianjie="";}String level="";if(nodeHtml.contains("vul-vh")){level="高危漏洞";}else if(nodeHtml.contains("vul-vm")){level="中危漏洞";}else if(nodeHtml.contains("vul-vl")){level="低危漏洞";}ldName=getLinkTagContent(nodeHtml)+"-----"+level+"------";// System.out.println("---漏洞名称-----"+getLinkTagContent(nodeHtml)+"-----"+level+"------");} }else{ldJianjie=getTableTagContent(node.toHtml());} } } catch (Exception e) {e.printStackTrace();} }/ 提取文件里面的文本信息 @param szFileName @return/public static String openFile(String szFileName) {try {BufferedReader bis = new BufferedReader(new InputStreamReader(new FileInputStream(new File(szFileName)), ENCODE));String szContent = "";String szTemp;while ((szTemp = bis.readLine()) != null) {szContent += szTemp + "\n";}bis.close();return szContent;} catch (Exception e) {return "";} }/ 提取标签<a>a</a>内的内容 return a;/public static String getLinkTagContent(String link){String content="";Pattern pattern = Pattern.compile("<a[^>]>(.?)</a>");Matcher matcher = pattern.matcher(link);if(matcher.find()){content=matcher.group(1);}return content;}/ 解析Table标签内的东西 @param table/public static String getTableTagContent(String table){Map<String,String> conMap=new HashMap<String,String>();String content="";Document doc = Jsoup.parse(table);Elements elList=doc.getElementsByAttributeValue("class","cmn_table plumb");Element el=elList.first();Elements trLists = el.select("tr");for (int i = 0; i < trLists.size(); i++) {Elements tds = trLists.get(i).select("td");String key="";String val="";for (int j = 0; j < tds.size(); j++) {String text = tds.get(j).text();if(j==0){key=text; }else{val=text; } }conMap.put(key, val);content+="|"+key+"-"+val;// System.out.println(key+"-"+val);}return content;} } 本篇文章为转载内容。原文链接:https://blog.csdn.net/zhaoguoshuai91/article/details/51802116。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-07-19 10:42:16
295
转载
转载文章
... of three elements. </p> @since 1.0 @author Daniel Fernández/public final class Triplet<A,B,C> extends Tupleimplements IValue0<A>,IValue1<B>,IValue2<C> {private static final long serialVersionUID = -1877265551599483740L;private static final int SIZE = 3;private final A val0;private final B val1;private final C val2;public static <A,B,C> Triplet<A,B,C> with(final A value0, final B value1, final C value2) {return new Triplet<A,B,C>(value0,value1,value2);} 我们一般调用静态方法with,传入元组数据,创建一个元组。当然了,也可以通过有参构造、数组Array、集合Collection、迭代器Iterator来创建一个元组,直接调用相应方法即可。 但是,我们可能记不住各元组对象的名称(Unit、Pair、Triplet、Quartet、Quintet、Sextet、Septet、Octet、Ennead、Decade),还要背下单词…因此,我们可以自定义一个工具类,提供公共方法,根据传入的参数个数,返回不同的元组对象。 2.2.2 自定义工具类 package com.superchen.demo.utils;import org.javatuples.Decade;import org.javatuples.Ennead;import org.javatuples.Octet;import org.javatuples.Pair;import org.javatuples.Quartet;import org.javatuples.Quintet;import org.javatuples.Septet;import org.javatuples.Sextet;import org.javatuples.Triplet;import org.javatuples.Unit;/ ClassName: TupleUtils Function: <p> Tuple helper to create numerous items of tuple. the maximum is 10. if you want to create tuple which elements count more than 10, a new class would be a better choice. if you don't want to new a class, just extends the class {@link org.javatuples.Tuple} and do your own implemention. </p> date: 2019/9/2 16:16 @version 1.0.0 @author Chavaer @since JDK 1.8/public class TupleUtils{/ <p>Create a tuple of one element.</p> @param value0 @param <A> @return a tuple of one element/public static <A> Unit<A> with(final A value0) {return Unit.with(value0);}/ <p>Create a tuple of two elements.</p> @param value0 @param value1 @param <A> @param <B> @return a tuple of two elements/public static <A, B> Pair<A, B> with(final A value0, final B value1) {return Pair.with(value0, value1);}/ <p>Create a tuple of three elements.</p> @param value0 @param value1 @param value2 @param <A> @param <B> @param <C> @return a tuple of three elements/public static <A, B, C> Triplet<A, B, C> with(final A value0, final B value1, final C value2) {return Triplet.with(value0, value1, value2);} } 以上的TupleUtils中提供了with的重载方法,调用时根据传入的参数值个数,返回对应的元组对象。 2.2.3 示例代码 若有需求: 现有pojo类Student、Teacher、Programmer,需要存储pojo类的字节码文件、对应数据库表的主键名称、对应数据库表的毕业院校字段名称,传到后层用于组装sql。 可以再定义一个对象类,但是如果还要再添加条件字段的话,又得重新定义…所以我们这里直接使用元组Tuple实现。 public class TupleTest {public static void main(String[] args) {List<Triplet<Class, String, String>> roleList = new ArrayList<Triplet<Class, String, String>>();/三元组,存储数据:对应实体类字节码文件、数据表主键名称、数据表毕业院校字段名称/Triplet<Class, String, String> studentTriplet = TupleUtils.with(Student.class, "sid", "graduate");Triplet<Class, String, String> teacherTriplet = TupleUtils.with(Teacher.class, "tid", "graduate");Triplet<Class, String, String> programmerTriplet = TupleUtils.with(Programmer.class, "id", "graduate");roleList.add(studentTriplet);roleList.add(teacherTriplet);roleList.add(programmerTriplet);for (Triplet<Class, String, String> triplet : roleList) {System.out.println(triplet);} }} 存储数据结构如下: 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_35006663/article/details/100301416。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-09-17 17:43:51
257
转载
转载文章
...,主要是感觉它支持的事件非常丰富,文件选取后的事件,文件上传中的事件(可获得文件的上传进度),文件上传成功的事件,文件上传失败的事件,等等 我的例子主要是上传一个单个文件,并显示上传的进度条(使用jQuery的一个进度条插件) 下面的例子主要是为文件上传交给 UploadCoursePackage.ashx 来处理 /ProgressBar/ var progressBar = $("loading").progressbar({ width: '500px', color: 'B3240E', border: '1px solid 000000' }); /Plupload/ //实例化一个plupload上传对象 var uploader = new plupload.Uploader({ browse_button: 'browse', //触发文件选择对话框的按钮,为那个元素id runtimes: 'html5,flash,silverlight,html4',//兼容的上传方式 url: "Handlers/UploadCoursePackage.ashx", //后端交互处理地址 max_retries: 3, //允许重试次数 chunk_size: '10mb', //分块大小 rename: true, //重命名 dragdrop: false, //允许拖拽文件进行上传 unique_names: true, //文件名称唯一性 filters: { //过滤器 max_file_size: '999999999mb', //文件最大尺寸 mime_types: [ //允许上传的文件类型 { title: "Zip", extensions: "zip" }, { title: "PE", extensions: "pe" } ] }, //自定义参数 (键值对形式) 此处可以定义参数 multipart_params: { type: "misoft" }, // FLASH的配置 flash_swf_url: "../Scripts/plupload/Moxie.swf", // Silverligh的配置 silverlight_xap_url: "../Scripts/plupload/Moxie.xap", multi_selection: false //true:ctrl多文件上传, false 单文件上传 }); //在实例对象上调用init()方法进行初始化 uploader.init(); uploader.bind('FilesAdded', function (uploader, files) { $("<%=fileSource.ClientID %>").val(files[0].name); $.ajax( { type: 'post', url: 'HardDiskSpace.aspx/GetHardDiskFreeSpace', data: {}, dataType: 'json', contentType: 'application/json;charset=utf-8', success: function (result) { //选择文件以后检测服务器剩余磁盘空间是否够用 if (files.length > 0) { if (parseInt(files[0].size) > parseInt(result.d)) { $('error-msg').text("文件容量大于剩余磁盘空间,请联系管理员!"); } else { $('error-msg').text(""); } } }, error: function (xhr, err, obj) { $('error-msg').text("检测服务器剩余磁盘空间失败"); } }); }); uploader.bind('UploadProgress', function (uploader, file) { var percent = file.percent; progressBar.progress(percent); }); uploader.bind('FileUploaded', function (up, file, callBack) { var data = $.parseJSON(callBack.response); if (data.statusCode === "1") { $("<%=hfPackagePath.ClientID %>").val(data.filePath); var id = $("<%=hfCourseID.ClientID %>").val(); __doPostBack("save", id); } else { hideLoading(); $('error-msg').text(data.message); } }); uploader.bind('Error', function (up, err) { alert("文件上传失败,错误信息: " + err.message); }); /Plupload/ 后台 UploadCoursePackage.ashx 的代码也重要,主要是文件分片跟不分片的处理方式不一样 using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; namespace WebUI.Handlers { /// <summary> /// UploadCoursePackage 的摘要说明 /// </summary> public class UploadCoursePackage : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; int statuscode = 1; string message = string.Empty; string filepath = string.Empty; if (context.Request.Files.Count > 0) { try { string resourceDirectoryName = System.Configuration.ConfigurationManager.AppSettings["resourceDirectory"]; string path = context.Server.MapPath("~/" + resourceDirectoryName); if (!Directory.Exists(path)) Directory.CreateDirectory(path); int chunk = context.Request.Params["chunk"] != null ? int.Parse(context.Request.Params["chunk"]) : 0; //获取当前的块ID,如果不是分块上传的。chunk则为0 string fileName = context.Request.Params["name"]; //这里写的比较潦草。判断文件名是否为空。 string type = context.Request.Params["type"]; //在前面JS中不是定义了自定义参数multipart_params的值么。其中有个值是type:"misoft",此处就可以获取到这个值了。获取到的type="misoft"; string ext = Path.GetExtension(fileName); //fileName = string.Format("{0}{1}", Guid.NewGuid().ToString(), ext); filepath = resourceDirectoryName + "/" + fileName; fileName = Path.Combine(path, fileName); //对文件流进行存储 需要注意的是 files目录必须存在(此处可以做个判断) 根据上面的chunk来判断是块上传还是普通上传 上传方式不一样 ,导致的保存方式也会不一样 FileStream fs = new FileStream(fileName, chunk == 0 ? FileMode.OpenOrCreate : FileMode.Append); //write our input stream to a buffer Byte[] buffer = null; if (context.Request.ContentType == "application/octet-stream" && context.Request.ContentLength > 0) { buffer = new Byte[context.Request.InputStream.Length]; context.Request.InputStream.Read(buffer, 0, buffer.Length); } else if (context.Request.ContentType.Contains("multipart/form-data") && context.Request.Files.Count > 0 && context.Request.Files[0].ContentLength > 0) { buffer = new Byte[context.Request.Files[0].InputStream.Length]; context.Request.Files[0].InputStream.Read(buffer, 0, buffer.Length); } //write the buffer to a file. if (buffer != null) fs.Write(buffer, 0, buffer.Length); fs.Close(); statuscode = 1; message = "上传成功"; } catch (Exception ex) { statuscode = -1001; message = "保存时发生错误,请确保文件有效且格式正确"; Util.LogHelper logger = new Util.LogHelper(); string path = context.Server.MapPath("~/Logs"); logger.WriteLog(ex.Message, path); } } else { statuscode = -404; message = "上传失败,未接收到资源文件"; } string msg = "{\"statusCode\":\"" + statuscode + "\",\"message\":\"" + message + "\",\"filePath\":\"" + filepath + "\"}"; context.Response.Write(msg); } public bool IsReusable { get { return false; } } } } 再附送一个检测服务器端硬盘剩余空间的功能吧 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.Script.Services; using System.Web.Services; using System.Web.UI; using System.Web.UI.WebControls; namespace WebUI { public partial class CheckHardDiskFreeSpace : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } /// <summary> /// 获取磁盘剩余容量 /// </summary> /// <returns></returns> [WebMethod] public static string GetHardDiskFreeSpace() { const string strHardDiskName = @"F:\"; var freeSpace = string.Empty; var drives = DriveInfo.GetDrives(); var myDrive = (from drive in drives where drive.Name == strHardDiskName select drive).FirstOrDefault(); if (myDrive != null) { freeSpace = myDrive.TotalFreeSpace+""; } return freeSpace; } } } 效果展示: 详细配置信息可以参考这篇文章:http://blog.ncmem.com/wordpress/2019/08/12/plupload%e4%b8%8a%e4%bc%a0%e6%95%b4%e4%b8%aa%e6%96%87%e4%bb%b6%e5%a4%b9-2/ 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_45525177/article/details/100654639。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-19 09:43:46
127
转载
转载文章
...图形界面开发简介 GUI Designer Basics scrcpy ScrcpyController Services 创建一个Gui Form文件(类+form文件) 创建一个Gui Form 此时生成两个类, 是自动关联的 鼠标可以拖入控件,每拖入一个组件,在TestForm.java类中会自动生成对应的属性名. 界面显示 显示在ToolWindow的右侧 我们拿ScrcpyController举例,显示如下的界面 需要用到如下三个文件 其中ScrcpyController显示的就是界面, ScrcpyToolWindowFactory是把界面放入的的工厂类. 声明这个工厂类 <extensions defaultExtensionNs="com.intellij"><toolWindowanchor="right"canCloseContents="false"factoryClass="com.codertainment.scrcpy.controller.ui.ScrcpyToolWindowFactory" icon="/toolWindowIcon.svg"id="scrcpy"/>...</extensions> 设置界面 需要如下三个类 ScrcpyControllerSettingsComponent是界面, ScrcpyControllerconfigurable是绑定的工厂类. 在plugin.xml中声明 <extensions defaultExtensionNs="com.intellij"><applicationConfigurabledisplayName="Scrcpy Controller" id="com.codertainment.scrcpy.controller.ui.ScrcpyControllerConfigurable"instance="com.codertainment.scrcpy.controller.ui.ScrcpyControllerConfigurable"parentId="tools"/></extensions> 存储服务 本篇文章为转载内容。原文链接:https://blog.csdn.net/AdrianAndroid/article/details/127860988。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-05-01 10:38:51
437
转载
转载文章
...一下他的链接。 通过点击每个分类,发现不同的分类下,地址栏显示为域名后面拼接这对应分类的拼音,但在分类为王者荣耀之后的拼接的确是“s/分类拼音”。这样我们可以创建一个枚举类,将所有分类集中管理。在common包下创建一个Kind枚举类: package com.asahi.common;/ 分类的枚举/public enum Kind {RILI("rili"), DONGMAN("dongman"), FENGJING("fengjing"), MEINV("meinv"), YOUXI("youxi"), YINGSHI("yingshi"),DONGTAI("dongtai"), WEIMEI("weimei"), SHEJI("sheji"), KEAI("keai"), QICHE("qiche"), HUAHUI("huahui"),DONGWU("dongwu"), JIERI("jieri"), RENWU("renwu"), MEISHI("meishi"), SHUIGUO("shuiguo"), JIANZHU("jianzhu"),TIYU("tiyu"), JUNSHI("junshi"), FEIZHULIU("feizhuliu"), QITA("qita"), WANGZHERONGYAO("s/wangzherongyao"), HUYAN("s/huyan"), LOL("s/lol");String kind;Kind(String kind) {this.kind = kind;}public static boolean contains(String test) {for (Kind c : Kind.values()) {if (c.kind.equals(test)) {return true;} }return false;} } 这里我添加了一个比较的方法供之后判断输入的分类名是否包含在这些分类里面。 接下来我们在分析分类面的展示情况,以美女分类页面为例(●´∀`●),最下边有分页,如果只获取这个页面的图片并不能获取所有美女图,我们还需要点击每一个分页,从分页中获取所有的图片。通过分析发现,第一页的链接是在原有链接基础上拼接“/index.htm”,从第二页之后拼接的是“/index_页号.htm”。 这样我们只需要获取总页数在依次遍历拼接就可以了,现在的问题是如何获取总页数,我一开始的想法是获取分页中“共167页”这个标签后再只保留数字就可以个,但发现运行后获取不到该元素节点,经过排查了解到这个标签是通过js生成的,于是我转换了思路,通过获取最后一个页号来得到一共分了多少页 Document root_doc = Jsoup.connect("http://www.netbian.com/" + kind + "/").get();Elements els = root_doc.select("main .page a");//这里els.eq(els.size() - 2的原因是后边确定按钮用的是a标签要去掉,再去掉一个“下一页”标签Integer page = Integer.parseInt(els.eq(els.size() - 2).text()); 分类页中图片所在的标签结构为: 分类页面下的图片不是我们想要的,我们想要的是点击进去详细页的高清大图,所以需要获取a标签的链接,再从这个链接中获取真正想要的图片。 详细页中图片所在的标签结构为: 二、代码实现 到这里分类页分析的差不多了,我们通过代码来进行获取图片。首先导入Jsoup的jar包:jsoup-1.12.1.jar,如果采用Maven请导入下边的依赖。 <dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.12.1</version></dependency> 在utils创建JsoupPic类,并添加getPic方法,代码如下: public static void getPic(String kind) throws Exception {//get请求方式进行请求Document root_doc = Jsoup.connect("http://www.netbian.com/" + kind + "/").get();//获取分页标签,用于获取总页数Elements els = root_doc.select("main .page a");Integer page = Integer.parseInt(els.eq(els.size() - 2).text());for (int i = 1; i < page; i++) {Document document = null;//这里判断的是当前页号是否为1,如果为1就不拼页号,否则拼上对应的页号if (i == 1) {document = Jsoup.connect("http://www.netbian.com/" + kind + "/index.htm").get();} else {document = Jsoup.connect("http://www.netbian.com/" + kind + "/index_" + i + ".htm").get();}//获取每个分页链接里面a标签的链接,进入链接页面获取当前图拼的大尺寸图片Elements elements = document.select("main .list li a");for (Element element : elements) {String href = element.attr("href");String picUrl = "http://www.netbian.com" + href;Document document1 = Jsoup.connect(picUrl).get();Elements elements1 = document1.select(".endpage .pic p a img");//获取所有图片的链接System.out.println(elements1);} }} 在分类页中有一个隐藏的问题图片: 正常的图片链接都是以“/”开头,以“.htm”结尾,而每个分类下的第三张图片的链接都是“http://pic.netbian.com/”,如果不过滤的话会报如下错误: 所以这里必须要判断一下: Elements elements = document.select("main .list li a");for (Element element : elements) {String href = element.attr("href");//判断是否是以“/”开头if (href.startsWith("/")) {String picUrl = "http://www.netbian.com" + href;Document document1 = Jsoup.connect(picUrl).get();Elements elements1 = document1.select(".endpage .pic p a img");System.out.println(elements1);} } 到这里,页面就已经分析好了,问题基本上已经解决了,接下来我们需要将图片存到我们的系统里,这里我将图片保存到我的电脑桌面上,并按照分类来存储图片。 首先是要获取桌面路径,在utils包下创建Download类,添加getDesktop方法,代码如下: public static File getDesktop(){FileSystemView fsv = FileSystemView.getFileSystemView();File path=fsv.getHomeDirectory(); return path;} 接着我们再该类中添加下载图片的方法: //urlPath为网络图片的路径,savePath为要保存的本地路径(这里指定为桌面下的images文件夹)public static void download(String urlPath,String savePath) throws Exception {// 构造URLURL url = new URL(urlPath);// 打开连接URLConnection con = url.openConnection();//设置请求超时为5scon.setConnectTimeout(51000);// 输入流InputStream is = con.getInputStream();// 1K的数据缓冲byte[] bs = new byte[1024];// 读取到的数据长度int len;// 输出的文件流File sf=new File(savePath);int randomNo=(int)(Math.random()1000000);String filename=urlPath.substring(urlPath.lastIndexOf("/")+1,urlPath.length());//获取服务器上图片的名称filename=new java.text.SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date())+randomNo+filename;//时间+随机数防止重复OutputStream os = new FileOutputStream(sf.getPath()+"\\"+filename);// 开始读取while ((len = is.read(bs)) != -1) {os.write(bs, 0, len);}// 完毕,关闭所有链接os.close();is.close();} 写好后,我们再完善一下JsouPic中的getPic方法。 public static void getPic(String kind) throws Exception {//get请求方式进行请求Document root_doc = Jsoup.connect("http://www.netbian.com/" + kind + "/").get();//获取分页标签,用于获取总页数Elements els = root_doc.select("main .page a");Integer page = Integer.parseInt(els.eq(els.size() - 2).text());for (int i = 1; i < page; i++) {Document document = null;//这里判断的是当前页号是否为1,如果为1就不拼页号,否则拼上对应的页号if (i == 1) {document = Jsoup.connect("http://www.netbian.com/" + kind + "/index.htm").get();} else {document = Jsoup.connect("http://www.netbian.com/" + kind + "/index_" + i + ".htm").get();}File desktop = Download.getDesktop();Download.checkPath(desktop.getPath() + "\\images\\" + kind);//获取每个分页链接里面a标签的链接,进入链接页面获取当前图拼的大尺寸图片Elements elements = document.select("main .list li a");for (Element element : elements) {String href = element.attr("href");if (href.startsWith("/")) {String picUrl = "http://www.netbian.com" + href;Document document1 = Jsoup.connect(picUrl).get();Elements elements1 = document1.select(".endpage .pic p a img");Download.download(elements1.attr("src"), desktop.getPath() + "\\images\\" + kind);} }} } 在Download类中,我添加了checkPath方法,用于判断目录是否存在,不存在就创建一个。 public static void checkPath(String savePath) throws Exception {File file = new File(savePath);if (!file.exists()){file.mkdirs();} } 最后在mainapp包内创建PullPic类,并添加主方法。 package com.asahi.mainapp;import com.asahi.common.Kind;import com.asahi.common.PrintLog;import com.asahi.utils.JsoupPic;import java.util.Scanner;public class PullPic {public static void main(String[] args) throws Exception {new PullPic().downloadPic();}public void downloadPic() throws Exception {System.out.println("启动程序>>\n请输入所爬取的分类:");Scanner scanner = new Scanner(System.in);String kind = scanner.next();while(!Kind.contains(kind)){System.out.println("分类不存在,请重新输入:");kind = scanner.next();}System.out.println("分类输入正确!");System.out.println("开始下载>>");JsoupPic.getPic(kind);} } 三、成果展示 最终的运行结果如下: 最终的代码已上传到我的github中,点击“我的github”进行查看。 在学习Java爬虫的过程中,我收获了很多,一开始做的时候确实遇到了很多困难,这次写的获取图片也是最基础的,还可以继续深入。本来我想写一个通过多线程来获取图片来着,也尝试着去写了一下,越写越跑偏,暂时先放着不处理吧,等以后有时间再来弄,我想问题应该不大,只是考虑的东西有很多。希望大家多多指点不足,有哪些需要改进的地方,我也好多学习学习๑乛◡乛๑。 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_39693281/article/details/108463868。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-06-12 10:26:04
130
转载
CSS
...、Material-UI)已内置了针对不同设备尺寸的响应式模态框组件,可以根据屏幕大小自动调整位置、尺寸及内容布局。 综上所述,模态框这一常见的交互组件在现代网页设计中的运用正不断丰富和完善,不仅局限于基础的显示与隐藏控制,更注重于流畅的动效、良好的无障碍访问支持以及跨设备的兼容性优化,这些都将为网站整体用户体验的提升带来重要影响。
2023-09-25 10:35:23
468
数据库专家
CSS
...页面上添加一些常见的组件,如导航栏、表格等等。采用样式表框架可以让构建者节省时间和精力,加快网页页面的构建进程。 那么样式表框架能否远程采用呢?答案是肯定的。样式表框架的本质是一系列的CSS样式表,只要这些样式表被正确地引入到HTML文件中,就可以在远程服务器上采用它们。构建者可以将样式表框架托管在云端,然后通过相应的链接引入到自己的HTML文件中。只要有网络连接,就可以方便地采用样式表框架。 当然,远程采用样式表框架也有一些注意事项。首先,确保样式表框架的链接可用,需要保证服务器链接的稳定性和可靠性。其次,下载和采用样式表框架时,需要注意不要侵犯版权,最好采用开源的样式表框架。此外,需要注意CSS的版本兼容性问题,在采用样式表框架时,也需要考虑不同浏览器、不同设备之间的兼容性问题。 总的来说,样式表框架可以远程采用,这为构建者提供了一个方便的工具。在采用样式表框架时需要注意一些细节问题,以免因错误采用而导致网站出现问题。因此,构建者应该根据实际情况慎重选择样式表框架,并遵守相关规定,以确保网页页面的正常构建与运行。
2023-04-02 22:13:16
509
电脑达人
Material UI
...了Material-UI Stepper组件的自定义步骤指示器后,我们还可以关注更多关于前端组件个性化定制和用户体验优化的前沿动态与深度解析。近期,Google Material Design团队发布了一系列关于增强无障碍设计的新指南,强调在构建用户界面时,应确保Stepper等交互组件对各类用户群体(包括视觉障碍者)友好,建议开发者在自定义步骤指示器时充分考虑颜色对比度、键盘导航以及屏幕阅读器支持等因素。 此外,随着Web Components技术的发展,React社区也在积极讨论如何更好地整合原生Web组件以提升性能和灵活性。例如,有开发者分享了如何将Material-UI Stepper与Stencil.js或LitElement等库结合使用,创建出可复用且高度自定义的步骤进度条组件,并在实际项目中取得了良好的效果。 另外,在Material-UI官方文档及GitHub仓库中,不断有新的自定义案例和最佳实践涌现,如通过CSS-in-JS技术styled-components进一步细化Stepper组件样式,或是利用Material-UI Lab中的实验性功能扩展其功能边界。这些实时更新的资源对于寻求更深层次个性化定制的开发者来说,是不可多得的学习参考资料。 总之,前端开发的世界日新月异,紧跟行业发展趋势,了解并掌握如何针对不同场景高效地定制化Material-UI组件,不仅能提升应用的用户体验,也是每一位追求卓越的前端工程师必备技能之一。
2024-02-10 10:53:38
258
昨夜星辰昨夜风
Element-UI
ElementUI的动画效果不流畅或缺失:原因、解决策略与实践示例 1. 引言 在日常前端开发中,ElementUI作为一款深受开发者喜爱的Vue.js组件库,以其丰富的功能和优雅的设计赢得了广泛的认可。然而,在实际操作的时候,咱们偶尔会发现,这玩意儿内置的动画效果并不像咱期望的那样顺滑流畅,甚至还会出现动画突然消失、不给力的情况。本文将围绕这个话题进行深入探讨,通过实例分析问题产生的原因,并提供优化解决方案。 2. 动画效果不流畅的原因探析 - CSS3动画性能限制:ElementUI中的动画基于CSS3实现,而浏览器对CSS3动画的渲染有一定的性能瓶颈,特别是在低配设备上,可能导致动画卡顿。 - 过度绘制与重排重绘:频繁的DOM操作和样式更改可能会引发页面过度绘制以及不必要的重排重绘,影响动画流畅度。 - 组件内部状态更新:当ElementUI组件的状态发生变化时,如果其内部没有恰当地处理动画过渡,就可能出现动画效果缺失或者不连贯的问题。 3. 代码示例及问题展现 html 在上述示例中,我们使用了ElementUI提供的el-collapse-transition组件来为内容区域添加折叠动画。当你遇到特定情况,比如手机正在疯狂加载大量数据时,那个动画可能就会变得有点儿卡卡的,或者会有那么一丢丢延迟,就像小短腿突然跟不上趟了那样。 4. 解决策略与实践 - 优化CSS动画性能:我们可以尝试优化CSS动画的关键帧(@keyframes),减少动画属性变化的复杂性,同时利用will-change属性提前告知浏览器元素可能的变化,提升渲染性能。 css .el-collapse-item__content { will-change: height, opacity; transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } - 合理管理组件状态变更:确保在触发组件状态变更时,能正确地触发并完成动画过渡。比如说,在Vue里头,我们可以巧妙地使用这个小玩意儿,再配上v-show指令,就能代替那个v-if啦。这么一来,既能保留住节点不被删除,又能有效防止频繁的DOM操作捣乱咱们的动画效果,是不是很机智的做法呀? html - 分批次加载数据:对于大数据量导致动画卡顿的情况,可以通过懒加载、分页加载等策略,减轻单次渲染的数据压力,从而改善动画流畅度。 5. 总结与思考 面对ElementUI动画效果不流畅或缺失的问题,我们需要从多个维度去审视和解决问题,包括但不限于优化CSS动画性能、合理管理组件状态变更以及根据实际情况采取相应的数据加载策略。在完成这个任务时,我们可不能光说不练,得实实在在地去钻研底层技术的来龙去脉,同时更要紧贴用户的真实感受。这就像是烹饪一道菜,不仅要知道食材的属性,还要了解食客的口味,才能不断试炼和改良。我们要让ElementUI的动画效果像调味料一样,恰到好处地融入到我们的产品设计中,这样一来,就能大大提升用户体验,让他们感觉像品尝美食一样享受咱们的产品。 让我们一起拥抱挑战,享受解决问题带来的乐趣,用更流畅、自然的动画效果赋予界面生命,提升用户的交互体验吧!
2023-03-20 20:53:01
463
林中小径
转载文章
...c:typedef uint8_t CardValue;// All code generators assume that the size of a card table entry is one byte.// They need to be updated to reflect any change to this.// This code can typically be found by searching for the byte_map_base() method.STATIC_ASSERT(sizeof(CardValue) == 1);protected:// The declaration order of these const fields is important; see the// constructor before changing.const MemRegion _whole_heap; // the region covered by the card tableconst size_t _page_size; // page size used when mapping _byte_mapsize_t _byte_map_size; // in bytesCardValue _byte_map; // the card marking arrayCardValue _byte_map_base;// Some barrier sets create tables whose elements correspond to parts of// the heap; the CardTableBarrierSet is an example. Such barrier sets will// normally reserve space for such tables, and commit parts of the table// "covering" parts of the heap that are committed. At most one covered// region per generation is needed.static constexpr int max_covered_regions = 2;// The covered regions should be in address order.MemRegion _covered[max_covered_regions];// The last card is a guard card; never committed.MemRegion _guard_region;inline size_t compute_byte_map_size(size_t num_bytes);enum CardValues {clean_card = (CardValue)-1,dirty_card = 0,CT_MR_BS_last_reserved = 1};// a word's worth (row) of clean card valuesstatic const intptr_t clean_card_row = (intptr_t)(-1);// CardTable entry sizestatic uint _card_shift;static uint _card_size;static uint _card_size_in_words;size_t last_valid_index() const {return cards_required(_whole_heap.word_size()) - 1;}private:void initialize_covered_region(void region0_start, void region1_start);MemRegion committed_for(const MemRegion mr) const;public:CardTable(MemRegion whole_heap);virtual ~CardTable() = default;void initialize(void region0_start, void region1_start);// Barrier set functions.// Initialization utilities; covered_words is the size of the covered region// in, um, words.inline size_t cards_required(size_t covered_words) const {assert(is_aligned(covered_words, _card_size_in_words), "precondition");return covered_words / _card_size_in_words;}// Dirty the bytes corresponding to "mr" (not all of which must be// covered.)void dirty_MemRegion(MemRegion mr);// Clear (to clean_card) the bytes entirely contained within "mr" (not// all of which must be covered.)void clear_MemRegion(MemRegion mr);// Return true if "p" is at the start of a card.bool is_card_aligned(HeapWord p) {CardValue pcard = byte_for(p);return (addr_for(pcard) == p);}// Mapping from address to card marking array entryCardValue byte_for(const void p) const {assert(_whole_heap.contains(p),"Attempt to access p = " PTR_FORMAT " out of bounds of "" card marking array's _whole_heap = [" PTR_FORMAT "," PTR_FORMAT ")",p2i(p), p2i(_whole_heap.start()), p2i(_whole_heap.end()));CardValue result = &_byte_map_base[uintptr_t(p) >> _card_shift];assert(result >= _byte_map && result < _byte_map + _byte_map_size,"out of bounds accessor for card marking array");return result;}// The card table byte one after the card marking array// entry for argument address. Typically used for higher bounds// for loops iterating through the card table.CardValue byte_after(const void p) const {return byte_for(p) + 1;}void invalidate(MemRegion mr);// Provide read-only access to the card table array.const CardValue byte_for_const(const void p) const {return byte_for(p);}const CardValue byte_after_const(const void p) const {return byte_after(p);}// Mapping from card marking array entry to address of first wordHeapWord addr_for(const CardValue p) const {assert(p >= _byte_map && p < _byte_map + _byte_map_size,"out of bounds access to card marking array. p: " PTR_FORMAT" _byte_map: " PTR_FORMAT " _byte_map + _byte_map_size: " PTR_FORMAT,p2i(p), p2i(_byte_map), p2i(_byte_map + _byte_map_size));// As _byte_map_base may be "negative" (the card table has been allocated before// the heap in memory), do not use pointer_delta() to avoid the assertion failure.size_t delta = p - _byte_map_base;HeapWord result = (HeapWord) (delta << _card_shift);assert(_whole_heap.contains(result),"Returning result = " PTR_FORMAT " out of bounds of "" card marking array's _whole_heap = [" PTR_FORMAT "," PTR_FORMAT ")",p2i(result), p2i(_whole_heap.start()), p2i(_whole_heap.end()));return result;}// Mapping from address to card marking array index.size_t index_for(void p) {assert(_whole_heap.contains(p),"Attempt to access p = " PTR_FORMAT " out of bounds of "" card marking array's _whole_heap = [" PTR_FORMAT "," PTR_FORMAT ")",p2i(p), p2i(_whole_heap.start()), p2i(_whole_heap.end()));return byte_for(p) - _byte_map;}CardValue byte_for_index(const size_t card_index) const {return _byte_map + card_index;}// Resize one of the regions covered by the remembered set.void resize_covered_region(MemRegion new_region);// Card-table-RemSet-specific things.static uintx ct_max_alignment_constraint();static uint card_shift() {return _card_shift;}static uint card_size() {return _card_size;}static uint card_size_in_words() {return _card_size_in_words;}static constexpr CardValue clean_card_val() { return clean_card; }static constexpr CardValue dirty_card_val() { return dirty_card; }static intptr_t clean_card_row_val() { return clean_card_row; }// Initialize card sizestatic void initialize_card_size();// Card marking array base (adjusted for heap low boundary)// This would be the 0th element of _byte_map, if the heap started at 0x0.// But since the heap starts at some higher address, this points to somewhere// before the beginning of the actual _byte_map.CardValue byte_map_base() const { return _byte_map_base; }virtual bool is_in_young(const void p) const = 0;}; class G1CardTable : public CardTable {friend class VMStructs;friend class G1CardTableChangedListener;G1CardTableChangedListener _listener;public:enum G1CardValues {g1_young_gen = CT_MR_BS_last_reserved << 1,// During evacuation we use the card table to consolidate the cards we need to// scan for roots onto the card table from the various sources. Further it is// used to record already completely scanned cards to avoid re-scanning them// when incrementally evacuating the old gen regions of a collection set.// This means that already scanned cards should be preserved.//// The merge at the start of each evacuation round simply sets cards to dirty// that are clean; scanned cards are set to 0x1.//// This means that the LSB determines what to do with the card during evacuation// given the following possible values://// 11111111 - clean, do not scan// 00000001 - already scanned, do not scan// 00000000 - dirty, needs to be scanned.//g1_card_already_scanned = 0x1};static const size_t WordAllClean = SIZE_MAX;static const size_t WordAllDirty = 0;STATIC_ASSERT(BitsPerByte == 8);static const size_t WordAlreadyScanned = (SIZE_MAX / 255) g1_card_already_scanned;G1CardTable(MemRegion whole_heap): CardTable(whole_heap), _listener() {_listener.set_card_table(this);}static CardValue g1_young_card_val() { return g1_young_gen; }static CardValue g1_scanned_card_val() { return g1_card_already_scanned; }void verify_g1_young_region(MemRegion mr) PRODUCT_RETURN;void g1_mark_as_young(const MemRegion& mr);size_t index_for_cardvalue(CardValue const p) const {return pointer_delta(p, _byte_map, sizeof(CardValue));}// Mark the given card as Dirty if it is Clean. Returns whether the card was// Clean before this operation. This result may be inaccurate as it does not// perform the dirtying atomically.inline bool mark_clean_as_dirty(CardValue card);// Change Clean cards in a (large) area on the card table as Dirty, preserving// already scanned cards. Assumes that most cards in that area are Clean.inline void mark_range_dirty(size_t start_card_index, size_t num_cards);// Change the given range of dirty cards to "which". All of these cards must be Dirty.inline void change_dirty_cards_to(CardValue start_card, CardValue end_card, CardValue which);inline uint region_idx_for(CardValue p);static size_t compute_size(size_t mem_region_size_in_words) {size_t number_of_slots = (mem_region_size_in_words / _card_size_in_words);return ReservedSpace::allocation_align_size_up(number_of_slots);}// Returns how many bytes of the heap a single byte of the Card Table corresponds to.static size_t heap_map_factor() { return _card_size; }void initialize(G1RegionToSpaceMapper mapper);bool is_in_young(const void p) const override;}; 以位为粒度的位图能准确描述每一个字的引用关系,但是一个位通常包含的信息太少,只能描述2个状态:引用还是未引用。实际应用中JVM在垃圾回收的时候需要更多的状态,如果增加至一个字节来描述状态,则位图需要256KB的空间,这个数字太大,开销占了25%。所以一个可能的做法位图不再描述一个字,而是一个区域,JVM选择512字节为单位,即用一个字节描述512字节的引用关系。选择一个区域除了空间利用率的问题之外,实际上还有现实的意义。我们知道Java对象实际上不是一个字能描述的(有一个参数可以控制对象最小对齐的大小,默认是8字节,实际上Java在JVM中还有一些附加信息,所以对齐后最小的Java对象是16字节),很多Java对象可能是几十个字节或者几百个字节,所以用一个字节描述一个区域是有意义的。但是我没有找到512的来源,为什么512效果最好?没有相应的数据来支持这个数字,而且这个值不可以配置,不能修改,但是有理由相信512字节的区域是为了节约内存额外开销。按照这个值,1MB的内存只需要2KB的额外空间就能描述引用关系。这又带来另一个问题,就是512字节里面的内存可能被引用多次,所以这是一个粗略的关系描述,那么在使用的时候需要遍历这512字节。 再举一个例子,假设有两个对象B、C都在这512字节的区域内。为了方便处理,记录对象引用关系的时候,都使用对象的起始位置,然后用这个地址和512对齐,因此B和C对象的卡表指针都指向这一个卡表的位置。那么对于引用处理也有可有两种处理方法:·处理的时候会以堆分区为处理单位,遍历整个堆分区,在遍历的时候,每次都会以对象大小为步长,结合卡表,如果该卡表中对应的位置被设置,则说明对象和其他分区的对象发生了引用。具体内容在后文中介绍Refine的时候还会详细介绍。·处理的时候借助于额外的数据结构,找到真正对象的位置,而不需要从头开始遍历。在后文的并发标记处理时就使用了这种方法,用于找到第一个对象的起始位置。在G1除了512字节粒度的卡表之外,还有bitMap,例如使用bitMap可以描述一个分区对另外一个分区的引用情况。在JVM中bitMap使用非常多,例如还可以描述内存的分配情况。 在G1除了512字节粒度的卡表之外,还有bitMap,例如使用bitMap可以描述一个分区对另外一个分区的引用情况。在JVM中bitMap使用非常多,例如还可以描述内存的分配情况。G1在混合收集算法中用到了并发标记。在并发标记的时候使用了bitMap来描述对象的分配情况。例如1MB的分区可以用16KB(16KB×ObjectAlignmentInBytes×8=1MB)来描述,即16KB额外的空间。其中ObjectAlignmentInBytes是8字节,指的是对象对齐,第二个8是指一个字节有8位。即每一个位可以描述64位。例如一个对象长度对齐之后为24字节,理论上它占用3个位来描述这个24字节已被使用了,实际上并不需要,在标记的时候只需要标记这3个位中的第一个位,再结合堆分区对象的大小信息就能准确找出。其最主要的目的是为了效率,标记一个位和标记3个位相比能节约不少时间,如果对象很大,则更划算。这些都是源码的实现细节,大家在阅读源码时需要细细斟酌。 本篇文章为转载内容。原文链接:https://blog.csdn.net/qq_16500963/article/details/132133125。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-16 20:37:50
246
转载
JQuery插件下载
...计的jQuery扩展组件。该插件充分利用了Bootstrap强大的下拉菜单(Dropdown)功能,能够方便快捷地在网页元素上生成自定义的右键上下文菜单。开发者无需从零开始构建复杂的交互逻辑,只需进行简单的配置,即可实现当用户在指定元素上执行鼠标右键操作时弹出丰富的定制化菜单选项。jquery-contextify以其轻量级和易用性著称,通过与Bootstrap框架的良好整合,确保生成的右键菜单不仅具备响应式布局,适应不同屏幕尺寸,还支持多层次子菜单,极大地丰富了用户的交互体验。无论是Web应用还是网站开发,它都能帮助开发者高效提升用户体验,降低在处理右键事件及关联功能时的工作复杂度。 点我下载 文件大小:82.94 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2023-10-08 20:07:37
114
本站
JQuery插件下载
...范围内通过鼠标滚轮或点击按钮进行翻页的功能。NaiveScroll插件通过对鼠标事件的智能覆盖处理,确保了在用户操作时不会产生冗余的事件触发,从而提升了用户体验。此外,它还提供了一系列便捷的API接口,使得开发者可以根据需求手动控制上下翻页的动作,增强了页面内容展示的灵活性与交互性。简而言之,NaiveScroll是一个高效实用的工具,它将简洁、流畅的全屏滚动体验融入到网页设计中,特别适合用于产品介绍、故事叙述、长图文浏览等场景,帮助构建出引人入胜且操作顺畅的一体化全屏页面浏览效果。 点我下载 文件大小:41.00 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2023-04-12 18:00:07
35
本站
JQuery插件下载
...款名为“jQueryUISlider内容滑块分页效果”的浏览器插件,是基于强大的jQueryUI库开发的一款创新分页组件。它巧妙地将jQueryUI的Slider控件应用在内容展示与分页交互上,为用户提供流畅且直观的浏览体验。该插件具有两种不同的控制模式:第一种模式通过导航箭头实现翻页功能,用户只需点击箭头即可轻松切换前后内容页面;第二种模式则利用了Slider的核心特性,用户可以通过拖拽滑块来精确选择和跳转到相应的内容页面,实现了动态、连续且可视化的分页控制。这个插件适用于多种场景,尤其适合那些需要平滑过渡和高效空间利用的内容展示场合,如产品列表、图像画廊或滚动新闻等。开发者可以方便地定制滑动范围、步长以及动画效果,以满足不同项目的个性化需求。此外,凭借jQueryUI的良好兼容性和可扩展性,此内容滑块分页插件能够在各种现代浏览器中稳定运行,并确保界面与交互的一致性与专业性。 点我下载 文件大小:68.49 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2023-01-05 16:35:30
414
本站
JQuery插件下载
...r可以灵活地与不同的UI组件进行搭配,无论是点击文本框弹出颜色面板,还是通过按钮触发颜色选择功能,都能无缝衔接,大大提升了开发效率和用户体验。这款插件是移动优先时代的理想选择,尤其适合对性能和空间占用有较高要求的项目场景。 点我下载 文件大小:123.52 KB 您将下载一个JQuery插件资源包,该资源包内部文件的目录结构如下: 本网站提供JQuery插件下载功能,旨在帮助广大用户在工作学习中提升效率、节约时间。 本网站的下载内容来自于互联网。如您发现任何侵犯您权益的内容,请立即告知我们,我们将迅速响应并删除相关内容。 免责声明:站内所有资源仅供个人学习研究及参考之用,严禁将这些资源应用于商业场景。 若擅自商用导致的一切后果,由使用者承担责任。
2024-04-07 22:10:28
313
本站
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
zip -r archive.zip dir
- 将目录压缩为ZIP格式。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"