Vue框架作为当前主流的前端框架之一,作者尤雨溪更是国内开发者心中男神,作为开发工程师你是否知道Vue框架名字的由来?你是否知道Vue框架作者尤大大创作Vue过程中的几个节点?今天就来带领大家深入Vue背后的故事
其实尤大大最初专业为室内艺术,并非计算机,但是尤大大在后面到帕森斯设计学院进修艺术研究硕士学位,学习的是科技与设计项目,它涵盖了编程、设计和新媒体艺术,也正是在这个时候尤大大自学了JavaScript,开始用JavaScript进行创作,毕业后就进入了谷歌,尤大大为谷歌浏览器开发了一些实验性的互动场景,把它们放到了作品集中,尤大大在采访中提到曾在学校期间开发了一个克隆版的Clear应用,它是一个有着很新颖的手势操作的待办事项应用程序,它应该算是“滑动来完成”手势操作的开拓者了,所以就在网页上重置了他的大部分功能,它在黑客新闻(Hacker News)上火了起来,被很多网站报道了,这件事让谷歌的招聘官注意到了尤大大,那时候谷歌有很多为谷歌浏览器开发的实验性场景,包括一些3D动效、2D的物理效果和可视化的处理等,都是尤大大他们在用的浏览器上实现的,这在当时是前所未有的,它真的体现了JavaScript的强大,不知什么缘故尤大大就接到了谷歌招聘官的电话她说,你有兴趣加入我们的创新实验室吗,作为我们招收的五人之一,尤大大当时真的不敢相信,都没给他们发我的简历就直接有人给打电话让加入谷歌,那肯定是要去的,尤大大非常高兴,自己不用也搬家,工作场地就在纽约;
尤大大聊出这段经历:“工作两年多,做一些有意思和实验性的项目,稀奇古怪的点子,比如说,十年后的搜索界面是什么样子的,或者如果整面墙都是可以互动屏幕的会怎样?如果谷歌能够听懂你说的每一句话?讽刺的是这件事现在几乎已经成为现实了,我们要做出一些关于这方面的概念视频和这种交互操作的原型,在2012年。在很长的一段时间内,我都在用原生的JavaScript来手写这些原型,因为这些原型不会使用传统的UI界面,他们需要很多特殊的处理,这些处理手段在如今普通的应用中很难见到,我尝试着去用一些现有的框架进行开发,但是很多时候我发现他们不能解决我的问题,我使用的第一个框架是Backbone,Backbone更像是一个为应用搭建的结构,他不会帮助你控制视图,尽管Angular提供了数据绑定,能将视图和数据状态同步起来,但它太约束你写代码的方式,所以它也不能很好的处理我正在进行的工作项目,因为这些项目很注重交互而不是如何成为一个完整的应用程序,我开始思考如何才能让我的工作变得更高效,就在那时我开始想或许我可以自己开发一个我自己的框架,最初我的目标就是开发一个非常简单专一的工具,仅仅就是同步DOM和一些JavaScript对象,这是最初的想法,在这个初始项目有了一点人气之后,我们开始扩大开发范围,一点点加入一些新功能到这个生态环境中去,最终使他变成了一个框架。”
尤大大对Vue.js进行的首次提交是在2013年的6月份,它刚开始的名字是Seed.js,据尤大大说当时想不到一个更好的名字,但是当尤大大要在NPM上发布的时候,发现Seed.js在NPM上已经被占用了,所以只能再想一个,尤大大表述:“我觉得,这是一个视图(View)的框架,但是就叫他View的话有点太直接了,所以我就把“View”放到谷歌翻译里面,然后我找到了它法语的翻译——vue,只有三个字母,看起来很好,也没有在NPM上被占用,所以我就用了这个名字。”
尤大大自述:“我基本上就是为我自己创建的这个框架,所以我的期望是,我希望,开发出一个我自己喜欢的框架,这也是我为什么要做这个框架的原因,我当然还在谷歌创意实验室工作,所以我把它发布成了我的私人项目,应该有几百名用户,我觉的有几百个GitHub的星星,给了我很多初始用户。”
你有没有想过你会通过这个赚到钱?尤大大:“完全没想到,当我出版的时候,并不是真的在说我想将其发布为一个可持续的项目,就像一个音乐爱好者为了乐趣而做一个专辑,你知道那种感觉不是为了赚钱,你发布这个音乐,只是因为你享受创作它的过程,并且你希望其他人能听到它,所以开发Vue.js的过程就像是一个途径像是我在做一个人们在真实世界中真的会用到的东西,相比我在谷歌创意实验室的工作来说,我们把做的东西给经理们看,但谁知道这个东西什么时候会变成现实呢(这里主要的意思是在谷歌中想法的出现到真正的落地的周期都是不确定的,甚至都不知道能否落地)所以我有一种冲动,要开发一个人们真正会用到的东西,我当时并没有想那么多,但有一个热门的开源项目会帮助找到下一个工作,这就是我加入Meteor的方式,他们基本上很倾佩我在Vue上做出的工作,所以他们直接跳过了面试的过程,当场就给了我工作,所以我很高兴,这才是你进行开源项目工作获得的真正的好处之一;
尤大大在Meteor工作的时候,仍然在持续的开发Vue.js,因为尤大大觉得它在那时就像自己的孩子一样,我们可以看到上面尤大大的提交记录,在Github上看到一个非常绿的图表,因为尤大大在每个周末都会完成很多提交,当时进行更多的工作也越来越有必要了,因为Vue的用户越来越多了,据尤大大表述Vue一直在成长所以会出现更多的问题,更多的Bug要被修复。
就在那段时间,Taylor发了一条推特提到了Vue.js,那是2014年,Taylor Otwell 是Laravel的作者,Laravel是一个非常流行的PHP框架,大家都知道在2014年那段时间PHP火的一塌糊涂。
Taylor那天在推特上说:“目前自己在学习React,但是状态很迷惑,React很难,我正在学习Vue.js,因为它看起来很简单”,虽然看起来这不像是一个正式的对Vue的认可,他只是在说我正在学习Vue.js",然后很多人开始问他:你觉得Vue怎么样,它好用吗?基本上在每一条回复中,他都说,没错,它很棒,这开始看起来像是一个对Vue公开的认可,然后所有的Laravel用户就会想,哇,如果Taylor喜欢Vue,那它肯定很好呀,所以我们也应该试一试,所以Vue就获得了很多来之Laravel社区的用户"
Taylor表述:“我当时在网上查找一个JavaScript框架来开发这个叫Laravel Spark的产品,它是一个快速开发脚手架,帮助你开发你自己的软件和服务体系,帮助人们启动他们自己的生意,就像我一样,我感觉到不知所措,我看了其他的框架,他们有复杂的编译流程,需要很多工具才能起步,我尝试了Vue.js发现你可以用Windows笔记本就能写出一个简单的网页应用你不需要其他的编译工具,仅仅需要添加一个CDN链接,写几句JavaScript,你的网页就跑起来了,对于我这个不是很熟悉JavaScript的人,至少可以说这么简单就可以实现那么多功能,真的让人耳目一新而且还不需要学习很多其他的工具,我记得我发布了一条推特说《我发现了这个很棒的新玩意叫Vue.js,像我一样的新手,都可以写出JavaScript应用程序,真的很令人惊叹》因此我们为Laravel Spark采用了Vue.js,最后产生了,你知道的,为Laravel带来了超过一百万美金收入,成千上万个用户搭建了他们自己的生意,所以这真是一件对Vue.js和Laravel都很棒的事情,帮助简化了不知道多少,使用Spark搭建的业务,都同时运用了Laravel和Vue.js”
上面说到的这些都发生在尤大大决定全职开发Vue.js之前,在那之前Vue.js还处于只是在前端社区中比较有名的阶段,尤大大还没想过要将它作为自己的全职工作,甚至是从中赚钱,直到尤大大学习到Taylor的做法之后,才知道Laravel是那么庞大,意识到或许自己真的能干点东西出来,但开始有人发起帖子和讨论说:“我到底该用哪个?我应该用Vue.js吗?”,你能看到的很多的回复都是:“不要用,因为它还是起步阶段”、“它不稳定,它仅仅由一个人维护着,天知道它什么时候就消失了”,尤大大想证明这些人是错误的,要让Vue.js变成一个V1.0版本的可生产的框架,所以尤大大花费了整个2015年的假期,用3个星期的时间把所有东西都开发好,更新文档,然后在2015年10月,尤大大发布了1.0版本;
当时Vue.js刚刚发布了1.0版本,还不存在像今天这样的核心团队,只有大概三个人,他们都忙着弄好文档和修复Bug,在1.0发布之后,当时的论坛就像是一个荒地;LinusBorg 就到论坛里去回答很多的问题,在论坛里非常的活跃,LinusBorg 了解到很多。
LinusBorg自述:“ 人们在他们项目中遇到的问题,我都在我空闲时间里干这件事,持续了三到四个月之后,Evan了解到了这个情况,他就给我发了一个Slack邀请,之前我们从未交流过,我也没开启任何问题讨论,没提出合并请求,我没在代码上做出任何贡献,但我从一开始就注意到Vue.js框架不仅是正确的编码,不仅是一个库,还需要一个描述它工作原理的文档,帮助你解决问题,它应该是我们需要的能想到的最好的文档,我们必须要扎身到社区中去,我们需要额外的工具和支持,这就是我被邀请到Vue团队的原因,因为我慢慢成长为社区领袖,我也确实加入到了Vue的核心团队,即便在没有对代码和内容作出贡献的情况下,不过之后我为了我所谓的自尊心,我学习了源码,研究了一些代码库,看了一些问题,然后开始四处作出一些贡献”。
尤大大自述:“我认为在1.0版本之后,我开始尝试着进行全职工作,或者这个东西还真能给我一些收入,让我能够把它当作一个全职工作,这时我开始觉得我在Meteor和我在Vue.js上的工作存在一些差异,在Meteor我只是一个普通的开发者,我没有做决策的权利,虽然我可以提出建议但我还远没到那种,说我们应该怎么做的那种地位,这让我对Meteor的发展有一些悲观,相比之下,Vue.js一直在快速成长。我注意到用Meteor的人比较少,而用Vue.js的人越来越多,所以那时我就觉得或许把Vue.js作为我的全职工作是值得的”
LevelUp教程的Scott Tolinski在Vue火了后,人们一直要求他做一个Vue.js的教程,在这个系列的教程中,会为大家讲到所有关于Vue.js的知识。
Scott Tolinski表示:“Vue.js出现的正是时候,因为很多人从Angular一代出现或更早之前就开始用Angular,人们都觉得Angular一代很简单,但当Angular二代推出的时候它几乎是一个完全不同的版本了,那个时候React也正在被推出,所以很多开发者开始用React,他们觉得React是个能够登上前端宝座的新东西,但这些开发者就会失去使用Angular一代的那种简单,轻松的感觉,就会感觉到有一种差距,这个由Angular一代的缺失而导致的大洞,Vue.js正好在这个时候出现了,它填补了一个空白,因为人们想要更低的入门门槛,还想要React的这种强大和灵活性,而这就是Vue.js的强项了,它吸取了多个框架和平台最强的部分”。
尤大大自从有了全职开发Vue.js的想法,就在探索能让自己经济上独立的办法,尤大大做的第一件事就是设立一个Patreon(众筹平台)账号,结果发现还真的不错,在建立那个账号不久之后,就能拿到2000美元一个月,尤大大的一位朋友郭达峰,他是“上线了(Strikingly)”的CTO,一个YC投资的公司,他的公司有个小基金用来支持开源项目,纯粹是出于做好事的想法,如果能拿到这个支持的话,尤大大就完全可以辞掉工作,并开始全职开发Vue.js了,这就足以让尤大大迈出这一步,至少可以去尝试一下。
Serah Drasner在CSS-tricks工作,是一个写手,Serah Drasner问Chris Coyier:“我觉的我要写一篇关于Vue.js的文章”,他说听起来不错,所以Serah Drasner开始写关于Vue.js的一篇文章,感觉要写挺长的,Serah Drasner问Chris Coyier:“什么情况下一篇文章需要分为一个系列来写”他说,什么时候你觉的你需要写一个目录的时候,那个时候Serah Drasner就该分开写了,Serah Drasner自述:这个时候我写的谷歌文档就已经有25页长了,然后我写完了最好一章,动画,最后就有了一个共有五篇文章的一个系列,我接着不断的写关于Vue.js的东西,下个月我写了更多的文章,然后做了一些Demo和一些开源项目,我觉得从那个时候开始,我就对这个框架感到非常激动,我想要投资更多的时间到其中去,所以Evan和Chris Fritz就邀请我和他们一起制作文档,同时进行一个新的项目叫cookbook,然后我就开始领导cookbook项目并且加入了Vue.js核心团队,现在我帮助运营Vue文档的见面会,Vue团队的见面会我真的很喜欢和这个团队一起工作。
“在家里工作绝对是非常自由的,你可以自己定制时间表,除去不需要通勤的第二大好处可能就是你不需要穿的像Tom Dale一样。”
尤大大:“想象一个在中国的人想要学习一个新的框架,他们去官网发现只有英文的文档,如果这个框架本身就非常难懂的话就更糟了,他们就会很难学会这个框架,但当他们到Vue.js官网的时候发现这里有中文版的文档,他们读了一下发现文档是由母语是汉语的人写的,一些技术性的内容直接用汉语表达的话会有些奇怪,因为你必须对技术名词想出一些很好的译名,比如说绑定(binding),引用(reference)或视图模型(view model),有些东西你甚至都翻译不了,因为汉语是我的母语然后英语版本的文档也是我自己写的,所以我能够改写一些术语,让它的汉语翻译更自然,这种语言上的熟悉让他们能够读懂你的框架,比他们用自己不熟悉的第二语言来学习要快的多,制作这样的中文文档,绝对帮助提高了Vue.js在中国的使用度”;
Jinjiang Zhao:“在中国,很少有人在科技界很知名,所以在中国,人们把Evan看作是一个英雄,一个中国开发者开发的人人都喜欢的框架”;
Gu Yiliang:“这在中国真的很不寻常,我们看不到有人能在开源项目上对世界作出这么大的影响,不仅仅是在中国内部,Evan,他是世界顶尖的,而且他还是中国人,所以我觉得这就是他有这么多中国粉丝的原因”;
Taylor:“他是一个中国人,当中国人看到这一点的时候,他们觉得自己是这其中的一部分,觉得自己是Vue社区的一部分,这是一种很真实的感觉因为他们与Vue的创始人是联系在一起的,Evan为中国社区的作为创造了一种强大的现象,它在中国的开发者之间建立了一种强大的联系,每个人都希望成为这个群体的一员,它让你感觉很棒,当你是这个群体中的一员时,Evan能融入这个群体,这种力量是非常强大的,这是他能在中国成功的巨大原因,它能够融入到这个群体中真的很惊人”;
Jinjiang Zhao:“那个时候移动网络非常的复杂并且不稳定,尤其是在中国,很多人还在用GPRS数据,所以我们尝试搭建一些新的,小体积高性能框架,帮助我们开发成千上万个网页,来支持千万级别的流量,这个量是非常非常大的,我在Github发现了Vue,所以我们的故事就开始了”;
尤大大被到阿里巴巴园区,做一个小的科技讲座,介绍Vue背后的基本想法、是怎么开始做它的、用Vue的好处到底是什么、相比一些手写的jQuery。
他们花费了很长时间,一年或一年半,一些开发者终于接受了Vue,并开始使用它,现在越来越多的人都在用Vue,在那之后,阿里巴巴也越来越多的用Vue了,人们也开始对Vue.js有更多的了解,因为尤大大在知乎上的存在,同时也因为Vue.js在中国以外也获得了更多的关注,这些消息也反向流回中国,人们才发现有一个新的框架叫Vue.js,事实上很多人都是以一个开源项目的身份认识到Vue.js,然后他们才意识到原来这是一个中国人写出来的,Gu Yiliang采访中提到:“我们主要是在我们的企业管理应用上使用Vue.js,像是谷歌的AdWords,我想为什么不参与到Vue.js其中呢?它在发展,它是潮流,中国开发者不仅仅把他当作一个开源项目开发者,而是一个开源项目领导者。”
Leo Deng:“我很熟悉Ember.js,而Vue.js看起来和它很像,所以我开始用Vue.js写一些东西,确实起作用了,我感觉它像是我已经学过的东西,Vue.js有非常高质量的中文文档,帮助了很多入门级的工程师进行开发,我们用它来开发生态系统,社区,我觉得这就是它这么流行的原因,在中国我们有很多的软件工程师,但是我们没有一个像他那样的巨星。”
Serah Drasner:“在软件工程领域,不光是Vue.js,在所有软件开发领域,我们都有一个概念叫做“终身仁慈独裁者,描述有些人是项目行动策划的关键大脑,来推动项目的前进,它并不意味着,有些人把它搞混了,认为这意味着没有其他人为这个项目而工作,这完全是错误的,你可以看到有非常多的人在为Vue.js和Vue核心而共同工作,它确实意味着它背后有一种推进力量,我觉得当存在一个像Vue.js这样不是产自大公司的项目,是有好处的因为它的内容是非常真实的,我们得到的好处就是,我们不是被某一个公司推动的",这意味着不是大公司在指定规则,是人们。”
Taylor:“Evan有一个总体的巨大的图景,Angular和React是由大公司搭建起来的,从一开始就有很多人参与其中,它们像是被一个委员会设计的,但对与Vue.js和Evan来说,在他大脑中有一个单一的图景,他希望创造这个整体的框架,我觉得这对于创造一个非常好的产品是非常重要的,这也是Vue.js如此成功的原因。”
LinusBorg:“前端市场被脸书的React统治,Angular是谷歌的,然后就是我们,像是一堆书呆子做他们自己的框架,完全是因为他们想做。”
Scott Tolinski:“它给人的感觉不像是一个公司,这样的话会让人更激动,更多像是民间的,光是这种想法就足以让人们感到兴奋,让人们想去使用它。”
LinusBorg:“Vue.js仍然在成长,它不是在推翻前端领域成为统治者,这不是它的目标,我们只是很高兴能为这个项目而工作,我们很高兴看到它的成长,也对它未来的发展感到很激动,我也不是很确定。”
尤大大:“我很自豪当初迈出了那一步,离开了早9晚5的工作,并开始投身于我真正热爱的事业上去,有时候我会看着那些星星,我们有了那么多用户,那么多的下载,但是什么给了我最大的成就感和满足感呢,是每次我看到这些人们",尤其是在结束一场集会之后,人们会走过通常握着我的手说谢谢你,Evan,谢谢你让我的生活变得更轻松,就是这些场景让我觉得,这就是我开发Vue.js的原因,我创造了这个东西,我把他分享给人们,希望它能让人们的生活变得更轻松,人们向我走过来,感谢我做出了这个东西,这也算完成这个循环了"
如今Vue框架火爆IT行业,已成为前端工程师的必备技能,其实也是博主我现在饭碗依靠的技术,Vue现在的Star数近20w,使用者更是数不胜数,正如尤大大表示自己很自豪的迈出那一步,其实作为受益者的我们更是感恩尤大大当时所做的决定;在CSDN的《新程序员004》中也曾攥入“与Vue.js作者尤雨溪畅谈他的程序人生”的篇幅,有兴趣的朋友可以阅读一下尤雨溪Vue登榜GitHub之路看似不难,加油每一位前端人。博主作为尤大大的忠实粉丝且作为Vue技术的热爱者并在CSDN构建了Vue技能树,欢迎大家打卡学习哦 https://edu.csdn.net/skill/vue
uni-app 是一个使用 Vue.js (opens new window)开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。DCloud公司拥有900万开发者、数百万应用、12亿手机端月活用户、数千款uni-app插件、70+微信/qq群。阿里小程序工具官方内置uni-app(详见 (opens new window)),腾讯课堂官方为uni-app录制培训课程(详见 (opens new window)),开发者可以放心选择。uni-app在手,做啥都不愁。即使不跨端,uni-app也是更好的小程序开发框架(详见 (opens new window))、更好的App跨平台框架、更方便的H5开发框架。不管领导安排什么样的项目,你都可以快速交付,不需要转换开发思维、不需要更改开发习惯。
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
目录
一、前言介绍:
随着现在网络的快速发展,网上管理系统也逐渐快速发展起来,网上管理模式很快融入到了许多商家的之中,随之就产生了“网上图书购物系统”,这样就让网上图书购物系统更加方便简单。
对于本网上图书购物系统的设计来说,系统开发主要是采用java语言技术,在整个系统的设计中应用MySQL数据库来完成数据存储,具体根据网上图书购物系统的现状来进行开发的,具体根据现实的需求来实现网上图书购物系统网络化的管理,各类信息有序地进行存储,进入网上图书购物系统页面之后,方可开始操作主控界面,主要功能包括管理员:首页、个人中心、图书分类管理、回收类别管理、新书榜管理、特价区管理、旧书回收管理、用户管理、订单评价管理、回收预约管理、图书回收管理、管理员管理、系统管理、订单管理。前台使用:首页、新书榜、特价区、旧书回收、公告资讯、个人中心、后台管理、购物车、客服。用户:首页、个人中心、订单评价管理、回收预约管理、图书回收管理、我的收藏管理、订单管理等功能。
本系统主要讲述了网上图书购物系统开发背景,该系统它主要是对需求分析和功能需求做了介绍,并且对系统做了详细的测试和总结。具体从业务流程、数据库设计和系统结构等多方面的问题。望能利用先进的计算机技术和网络技术来改变目前的网上图书购物系统状况,提高管理效率。
二、系统分析:
可行性分析主要是针对这个项目开发是否有意义和价值观来进行的全面分析,在分析的过程当中发现这个系统所存在的不足之处。就拿这次网上图书购物系统的设计与实现来说主要是针对一些用户在发布网上图书购物系统信息时遇到不方便的操作和问题来进行解决问题的,最后能够让网上图书购物系统开发得到最大的用处。而且对于用户方面我们可以提供给一个简单方便操作的网上图书购物系统。所以我们要计算开发这个系统它能否有效的解决好这个系统经济问题,在开发完成以后所带来的利益是否大于开发过成当中的成本。所以可行性的研究与分析是这个系统在开发和设计上是必不可缺少的一部分。从该系统文章的全部来看,我们要从以下几个方面进行分析:
技术可行性:在技术方面我们要从现有自己掌握的技术能否设计出我们当初所预定的目标。
经济可行性:在这次系统开发和设计过程当中所用的经费是否大于以后给社会带来的价值观。
操作可行性:系统在用户使用过程当中是否方便、简单,能否达到大部分的用户会使用。
技术的可行性分析主要是针对开发该系统所用到技术进行分析,对于网上图书购物系统的设计,可以在任何一个地方都进行使用和管理。通过当前我们所学的程序开发和语言介绍利用以上的技术开发该系统是比较合适的。而且我们在使用的数据库也是要保证这个系统的完整性、数据安全性好的条件。
经济可行性主要是决定这个系统是否具有价值存在,是否具有开发意义,如果开发的项目不能够节约物品和资源,反而使用的大量的人力、财力和物力不成正比甚至小于投资成本,那么该项目是不具备开发意义和价值的。在开发本项目的初期,节约成本是最基本的,设计和开发都是由本人一人完成的,并且在开发中使我学习到了很多的知识,也开拓了自己的眼界,在通过可行性分析之后,该项目的利大于弊,所以该项目是具有开发意义和价值的。
网上图书购物系统的开发登录界面它是我们最常见的一种登录窗口来完成的,用户可以使电脑来进行登录并简单的访问不需要做任何的操作。对于此次的系统开发它主要是基于B/S结构和java技术及MySQL数据库来完成,让系统开发更加完美和完善,所以我们开出的系统界面更加人性化,用户使用也更加方便。而且系统在使用过程当中也拥有方便操作、易管理等特点。
经过以上的叙述,所以开发此系统在经济上、技术上是满足开发条件的。
1.如果我们想要对前后台处理的层次分明那么我们就要采用B/S模式来进行系统的开发这样就可以方便用户的使用。
2.对于系统的开发和设计我们就要采用大家日常所需要的要求,这样一来可以提高系统的适用性也能保证系统利用价值。对于一个系统来说一个好的框架是很重要的,因为一个好的框架它可以提高系统的稳定和高效性。
3.在系统界面上也要设计一个方便快捷的登录界面,这样就可以提高用户对系统操作性和适用性。
4.在系统模块设计当中我们要对系统各个模块进行合理简化和设计,这样就能提高系统使用性。
5.对于一个完整的系统来说对于它的测评和测试是比较重要的,所以我们在软件设计程序中要保持软件占用的时间和速度快的特点。
6.对于这个系统来说我们首先要考虑所设计出的系统它具有那些突破和体现,所以我们尽力去改进这个系统去适应用户。
对于一个新的网站来说开发新网站我们就要做出这个系统的任务需求分析,因为对系统分析的质量好坏它可以决定这个网站开发的意义,俗话说得好一个好的开头是成功的一半,对于开发这个网上图书购物系统来说前期的分析是比较重要的,所以任务分析它可以决定这系统的开展和设计,这样就可以保证用户满意性。
任务的需求它能决定这个系统开发过程当中一个重要环节,所以我们在系统开发过程所用质量是比较重要的,因为我们在系统应用过程当中不一定那会出现问题,所以我们在进行对系统分析是比较重要的,因为它可以决定这个系统功能和需求。
本课题要求实现一套网上图书购物系统的开发与实现,主要实现功能包括管理员:首页、个人中心、图书分类管理、回收类别管理、新书榜管理、特价区管理、旧书回收管理、用户管理、订单评价管理、回收预约管理、图书回收管理、管理员管理、系统管理、订单管理,
前台使用:首页、新书榜、特价区、旧书回收、公告资讯、个人中心、后台管理、购物车、客服,
用户:首页、个人中心、订单评价管理、回收预约管理、图书回收管理、我的收藏管理、订单管理 网上图书购物系统。
2.3.3用户登录注册时序图
前端用户发送登录请求-验证输入的账号虚线-执行数据查询-返回查询结果-判断用户是否存在-前端根据结果集执行不同的操作。
通过系统需求分析,本网上图书购物系统主要实现功能包括;管理员:首页、个人中心、图书分类管理、回收类别管理、新书榜管理、特价区管理、旧书回收管理、用户管理、订单评价管理、回收预约管理、图书回收管理、管理员管理、系统管理、订单管理。用户:首页、个人中心、订单评价管理、回收预约管理、图书回收管理、我的收藏管理、订单管理等功能。其功能结构图如下图所示。
在该系统的信息中,由于数据库的支持,我们可以对数据库进行收集、整理、更新和加工等操作。由于数据库的存储功能强大,所以数据库已经成为了计算机必不可少的,一个数据库的好坏直接影响该系统的质量和效率。一个系统中的数据库是必不可少的,并且起着决定性因素。通过之前的系统分析,可以规划出本系统中使用的主要等,下面设计出这几个关键实体的实体关系图。
订单管理实体E-R图如图4-2所示
三、功能截图:
通过填写用户名、密码、角色等信息,输入完成后选择登录即可进入网上图书购物系统。
网上图书购物系统,在系统首页可以查看首页、新书榜、特价区、旧书回收、公告资讯、个人中心、后台管理、购物车、客服等内容。
新书榜,在新书榜页面可以填写图书名称、分类、标签、图片、作者、出版社、发行日期、价格等内容进行立即购买。
图书页面,可以收藏,加入购物车,查看详情以及评论和购买等操作。
购物车详情,可以添加数量和删除。点击购买进行模拟支付结账。
选择收货地址后点击支付下单:
支付成功后查看物流状态信息:
查看回收详情,点击回收预约填写相关信息,完成后管理员进行审核。
个人中心可以查看修改个人信息,查看订单得各种状态,以及对收货地址进行查看修改和收藏进行查看。也可以收货地址页面可以填写联系人、手机号码、地址、默认等内容进行添加地址
普通用户后台管理:
可以对客服进行留言聊天。
管理员对新书榜管理查看图书名称、分类、标签、图片、作者、出版社、发行日期、价格等信息进行详情、删除、修改、查看等操作。
四、代码实现:
-
-
/**
-
* 新书榜
-
* 后端接口
-
* @author
-
* @email
-
* @date 2022-01-18 07:36:34
-
*/
-
@RestController
-
@RequestMapping("/xinshubang")
-
public class XinshubangController {
-
@Autowired
-
private XinshubangService xinshubangService;
-
-
/**
-
* 后端列表
-
*/
-
@RequestMapping("/page")
-
public R page(@RequestParam Map<String, Object> params,XinshubangEntity xinshubang, HttpServletRequest request){
-
-
EntityWrapper<XinshubangEntity> ew = new EntityWrapper<XinshubangEntity>();
-
PageUtils page = xinshubangService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.likeOrEq(ew, xinshubang), params), params));
-
return R.ok().put("data", page);
-
}
-
-
/**
-
* 查询
-
*/
-
@RequestMapping("/query")
-
public R query(XinshubangEntity xinshubang){
-
EntityWrapper< XinshubangEntity> ew = new EntityWrapper< XinshubangEntity>();
-
ew.allEq(MPUtil.allEQMapPre( xinshubang, "xinshubang"));
-
XinshubangView xinshubangView = xinshubangService.selectView(ew);
-
return R.ok("查询新书榜成功").put("data", xinshubangView);
-
}
-
-
/**
-
* 后端详情
-
*/
-
@RequestMapping("/info/{id}")
-
public R info(@PathVariable("id") Long id){
-
XinshubangEntity xinshubang = xinshubangService.selectById(id);
-
xinshubang.setClicknum(xinshubang.getClicknum()+1);
-
xinshubang.setClicktime(new Date());
-
xinshubangService.updateById(xinshubang);
-
return R.ok().put("data", xinshubang);
-
}
-
-
-
/**
-
* 前端保存
-
*/
-
@RequestMapping("/add")
-
public R add(@RequestBody XinshubangEntity xinshubang, HttpServletRequest request){
-
xinshubang.setId(new Date().getTime()+new Double(Math.floor(Math.random()*1000)).longValue());
-
//ValidatorUtils.validateEntity(xinshubang);
-
-
xinshubangService.insert(xinshubang);
-
return R.ok();
-
}
-
-
-
/**
-
* 删除
-
*/
-
@RequestMapping("/delete")
-
public R delete(@RequestBody Long[] ids){
-
xinshubangService.deleteBatchIds(Arrays.asList(ids));
-
return R.ok();
-
}
-
-
/**
-
* 前端智能排序
-
*/
-
@IgnoreAuth
-
@RequestMapping("/autoSort")
-
public R autoSort(@RequestParam Map<String, Object> params,XinshubangEntity xinshubang, HttpServletRequest request,String pre){
-
EntityWrapper<XinshubangEntity> ew = new EntityWrapper<XinshubangEntity>();
-
Map<String, Object> newMap = new HashMap<String, Object>();
-
Map<String, Object> param = new HashMap<String, Object>();
-
Iterator<Map.Entry<String, Object>> it = param.entrySet().iterator();
-
while (it.hasNext()) {
-
Map.Entry<String, Object> entry = it.next();
-
String key = entry.getKey();
-
String newKey = entry.getKey();
-
if (pre.endsWith(".")) {
-
newMap.put(pre + newKey, entry.getValue());
-
} else if (StringUtils.isEmpty(pre)) {
-
newMap.put(newKey, entry.getValue());
-
} else {
-
newMap.put(pre + "." + newKey, entry.getValue());
-
}
-
}
-
params.put("sort", "clicknum");
-
-
params.put("order", "desc");
-
PageUtils page = xinshubangService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.likeOrEq(ew, xinshubang), params), params));
-
return R.ok().put("data", page);
-
}
-
-
-
}
-
<!DOCTYPE html>
-
<html>
-
<head lang="en">
-
<meta charset="utf-8">
-
<title>新书榜</title>
-
<meta name="keywords" content="" />
-
<meta name="description" content="" />
-
<meta name="renderer" content="webkit">
-
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-
<link rel="stylesheet" href="../../layui/css/layui.css">
-
<link rel="stylesheet" href="../../xznstatic/css/common.css"/>
-
<link rel="stylesheet" href="../../xznstatic/css/style.css"/>
-
<script type="text/javascript" src="../../xznstatic/js/jquery-1.11.3.min.js"></script>
-
<script type="text/javascript" src="../../xznstatic/js/jquery.SuperSlide.2.1.1.js"></script>
-
</head> </div>
-
<div class="pager" id="pager" :style="{textAlign:1==1?'left':1==2?'center':'right'}"></div>
-
</div>
-
</div
-
</div>
-
<script src="../../layui/layui.js"></script>
-
<script src="../../js/vue.js"></script>
-
<script src="../../js/config.js"></script>
-
<script src="../../modules/config.js"></script>
-
<script src="../../js/utils.js"></script>
-
<script>
-
// 获取轮播图 数据
-
http.request('config/list', 'get', {
-
page: 1,
-
limit: 5
-
}, function(res) {
-
if (res.data.list.length > 0) {
-
let swiperList = [];
-
res.data.list.forEach(element => {
-
if (element.value != null) {
-
swiperList.push({
-
img: element.value
-
});
-
}
-
});
-
vue.swiperList = swiperList;
-
-
vue.$nextTick(() => {
-
carousel.render({
-
elem: '#test1',
-
width: '100%',
-
height: '430px',
-
arrow: 'hover',
-
anim: 'default',
-
autoplay: 'true',
-
interval: '3000',
-
indicator: 'inside'
-
});
-
});
-
-
//类型查询
-
categoryList();
-
//类型搜索
-
$(document).on("click", ".category-search", function(e){
-
vue.swiperIndex = $(this).attr('index')
-
pageList(e.target.innerText);
-
});
-
// 获取列表数据
-
http.request('xinshubang/list', 'get', param, function(res) {
-
vue.dataList = res.data.list
-
// 分页
-
laypage.render({
-
elem: 'pager',
-
count: res.data.total,
-
limit: limit,
-
groups: 3,
-
layout: ["prev","page","next"],
-
theme: '#065279',
-
jump: function(obj, first) {
-
param.page = obj.curr;
-
//首次不执行
-
if (!first) {
-
http.request('xinshubang/list', 'get', param, function(res) {
-
vue.dataList = res.data.list
-
})
-
}
-
});
-
window.xznSlide = function() {
-
jQuery(".banner").slide({mainCell:".bd ul",autoPlay:true,interTime:5000});
-
jQuery("#ifocus").slide({ titCell:"#ifocus_btn li", mainCell:"#ifocus_piclist ul",effect:"leftLoop", delayTime:200, autoPlay:true,triggerTime:0});
-
jQuery("#ifocus").slide({ titCell:"#ifocus_btn li", mainCell:"#ifocus_tx ul",delayTime:0, autoPlay:true});
-
jQuery(".product_list").slide({mainCell:".bd ul",autoPage:true,effect:"leftLoop",autoPlay:true,vis:5,trigger:"click",interTime:4000});
-
};
-
</script>
-
</body>
-
</html>
五、文档目录:
六、项目总结:
对于本次的系统开发来看,它主要是把我以前所学的知识进行了一次综合的应用。经过这次毕业设计的制作它主要是把我以前所学的理论知识应用到社会实践当中。通过这一次的网上图书购物系统的设计与实现它能够有效把计算机知识与实际问题相互应用,通过计算机网络技术来解决用户生活当中的实际问题,从而提高我的编程能力。虽然在这次毕业设计当中我遇到了很多的问题和困难,但是通过不断的调试和老师的帮助让我圆满的完成了这次毕业设计。通过这次毕业设计的制作让我对计算机实际应用得到了很强的锻炼,同时也大大的提高了我的动手动脑能力,让我也感受到了其中的乐趣和喜悦。
通过这次项目设计的撰写把我在大学期间所学到的东西都应用上了,但是我觉得还是微不足道的,因为在这次毕业设计当中让我深深的了解到对于软件开发和学习理论知识它是两个完全不同的概念。但是通过这次软件的开发让我在以后的工作当中打下了良好的基础。
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
作为后端开放人员,最烦的事就是自己写接口文档和别人没有写接口文档,不管是前端还是后端开发,多多少少都会被接口文档所折磨,前端会抱怨后端没有及时更新接口文档,而后端又会觉得编写接口文档太过麻烦。Swagger 可以较好的接口接口文档的交互问题,以一套标准的规范定义接口以及相关的信息,就能做到生成各种格式的接口文档,生成多种语言和客户端和服务端的代码,以及在线接口调试页面等等。只需要更新 Swagger 描述文件,就能自动生成接口文档,做到前端、后端联调接口文档的及时性和便利性。
Swagger 是一个规范且完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
Swagger 的目标是对 REST API 定义一个标准且和语言无关的接口,可以让人和计算机拥有无须访问源码、文档或网络流量监测就可以发现和理解服务的能力。当通过 Swagger 进行正确定义,用户可以理解远程服务并使用最少实现逻辑与远程服务进行交互。与为底层编程所实现的接口类似,Swagger 消除了调用服务时可能会有的猜测。
Swagger 的优势
通过在项目中引入 Springfox,可以扫描相关的代码,生成该描述文件,进而生成与代码一致的接口文档和客户端代码。
<!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-spring-web</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>![]()
在配置文件 config 目录下,添加 swagger 的配置文件 SwaggerConfig.java
@Configuration // 配置类 @EnableSwagger2 // 开启 swagger2 的自动配置 public class SwaggerConfig { }
这个时候 Swagger 已经算是整合到项目之中了,可以启动下服务,输入:http://localhost:8080/swagger-ui.html# (这里我的项目端口是 8868 ,项目路径是 /mike,所以我打开的文档地址为 http://localhost:8868/mike/swagger-ui.html# )即可查看 swagger 文档。

可以看到 Swagger 文档中大概有这四类信息
Swagger 有自己的实例 Docket,如果我们想要自定义基本信息,可以使用 docket 来配置 swagger 的基本信息,基本信息的设置在 ApiInfo 这个对象中。
Swagger 默认的基本信息展示
ApiInfo 中默认的基本设置
SwaggerConfig.java 配置文件添加以下内容:
@Bean public Docket docket() { // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) // 配置基本信息 .apiInfo(apiInfo()) ; } // 基本信息设置 private ApiInfo apiInfo() { Contact contact = new Contact( "米大傻", // 作者姓名 "https://blog.csdn.net/xhmico?type=blog", // 作者网址 "777777777@163.com"); // 作者邮箱 return new ApiInfoBuilder() .title("多加辣-接口文档") // 标题 .description("众里寻他千百度,慕然回首那人却在灯火阑珊处") // 描述 .termsOfServiceUrl("https://www.baidu.com") // 跳转连接 .version("1.0") // 版本 .license("Swagger-的使用(详细教程)") .licenseUrl("https://blog.csdn.net/xhmico/article/details/125353535") .contact(contact) .build(); }![]()
重启服务,打开 Swagger 文档,基本信息改变如下所示:
默认情况下,Swagger 是会展示所有的接口信息的,包括最基础的 basic-error 相关的接口
有时候我们希望不要展示 basic-error-controller 相关的接口,或者是说这想要显示某些接口,比如说:user-controller 下的接口,由该怎么去实现呢?这个时候就需要设置 扫描接口
@Bean public Docket docket() { // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) // 配置接口信息 .select() // 设置扫描接口 // 配置如何扫描接口 .apis(RequestHandlerSelectors //.any() // 扫描全部的接口,默认 //.none() // 全部不扫描 .basePackage("com.duojiala.mikeboot.controller") // 扫描指定包下的接口,最为常用 //.withClassAnnotation(RestController.class) // 扫描带有指定注解的类下所有接口 //.withMethodAnnotation(PostMapping.class) // 扫描带有只当注解的方法接口 ) .paths(PathSelectors .any() // 满足条件的路径,该断言总为true //.none() // 不满足条件的路径,该断言总为false(可用于生成环境屏蔽 swagger) //.ant("/user/**") // 满足字符串表达式路径 //.regex("") // 符合正则的路径 ) .build(); }![]()
可根据自己的需求去设置对应的配置,这里我就不再一一赘述了,以上是我所设置的配置,重启服务,打开 Swagger 文档,接口信息改变如下所示:
可以看到之前 basic-error-controller 相关的接口已经没有了
Swagger 默认只有一个 default 分组选项,如果没有设置,所有的接口都会显示在 default `分组下,如果功能模块和接口数量一多,就会显得比较凌乱,不方便查找和使用。
swagger 文档中组名默认是 default,可通过 .groupName(String )
@Bean public Docket docket() { // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改组名为 "mike" ; }
修改后:

如果需要配置多个组的话,就需要配置多个 docket() 方法,这里我就简单写两组,代码如下:
/** * 展示 controller 包下所有的接口 */ @Bean public Docket docket1() { // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改组名为 "mike" // 配置接口信息 .select() // 设置扫描接口 // 配置如何扫描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 扫描指定包下的接口,最为常用 ) .paths(PathSelectors .any() // 满足条件的路径,该断言总为true ) .build(); } /** * 展示路径为 /error 的所有接口(基础接口) */ @Bean public Docket docket2() { // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) .groupName("yank") // 修改组名为 "yank" // 配置接口信息 .select() // 设置扫描接口 // 配置如何扫描接口 .apis(RequestHandlerSelectors .any() // 扫描全部的接口,默认 ) .paths(PathSelectors .ant("/error") // 满足字符串表达式路径 ) .build(); }![]()
重启服务,打开 Swagger 文档,接口信息改变如下所示:
组名为 mike 的文档中只有 user-controller 相关的接口信息
组名为 yank 的文档中只有 basic-error-controller 相关的接口信息
在开发或者测试环境下,我们开启 swagger 会方便前端和后端的交互,但是如果在生产环境下也开启 swagger 的话,是会将接口暴露出去的,有极大风险,如何让 swagger 根据不同的环境来决定是否开启?
这里我准备了四个项目的配置文件,dev、test、pro 三个环境的配置文件仅是端口上的不同
application.yml 内容如下,用于指定选择的环境:
spring: profiles: active: dev
可以通过代码判断此时是在什么环境:dev、test、pro,如果是在 pro 生产环境,则关闭 swagger。
/** * swagger 配置 * @param environment 环境 */ @Bean public Docket docket(Environment environment) { // 设置环境范围 Profiles profiles = Profiles.of("dev","test"); // 如果在该环境返回内则返回:true,反之返回 false boolean flag = environment.acceptsProfiles(profiles); // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) .enable(flag) // 是否开启 swagger:true -> 开启,false -> 关闭 ; }![]()
在 application.yml 全局配置文件中环境指向 dev 时,是可以打开 swagger 的

如果我将 application.yml 全局配置文件中环境指向 pro 时,就不能打开 swagger 了,提示 Could not render e, see the console
之前有说 Swagger 会将接口请求或者相应的实体类信息展示在 Models 下的,比如我 UserController.java 下有一个接口如下所示:
@PostMapping(value = "/query-user-info") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }
它的请求体是 IdReq,响应是 ResponseBean,Models 展示这两个实体类信息如下:

前端可通过看这个 Models 知道后端定义实体类的信息。
该注解是作用于类上面的,是用来描述类的一些基本信息的。
相关属性:
value:提供类的一个备用名,如果不设置,默认情况下将使用 class 类的名称
譬如:这个为给 IdReq 这个类添加该注解
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id请求体") public class IdReq { private String id; }

可以看到这里的名字从 IdReq 变成 Id请求体 了
它的作用是添加和操作属性模块的数据。
该注解的使用详情可参见博客:@ApiModelProperty注解的用法
这里我还是以 IdReq 类为例,为该类的属性添加说明
@Data @NoArgsConstructor @AllArgsConstructor @ApiModel(value = "Id请求体") public class IdReq { @ApiModelProperty("主键id") private String id; }

可以看到这里对该字段有一个备注说明。
该注解用来对某个方法/接口进行描述
该注解的使用详情可参见博客:Swagger @ApiOperation 注解详解
这里我以 UserController 下的接口为例:
@PostMapping(value = "/query-user-info") @ApiOperation(value = "根据id查询用户详情") public ResponseBean queryUserInfo(@RequestBody @Validated IdReq req) { return ResponseBean.success(userService.queryUserInfo(req)); }

可以看见该接口就多了对其的描述信息。
该注解使用在方法上或者参数上,字段说明,表示对参数的添加元数据(说明或者是否必填等)
相关属性:
这里我以 UserController 下的接口为例:
@PostMapping(value = "/query-user-infos") @ApiOperation(value = "条件查询用户信息") public ResponseBean queryUserInfos( // name 用户名称 不必填 @ApiParam(value = "用户名称", required = false) @RequestParam(required = false) String name, // gender 用户性别 必填 @ApiParam(value = "用户性别", required = true) @RequestParam(required = true) GenderEnum gender ) { return ResponseBean.success(userService.queryUserInfos(name,gender)); }

这里会展示请求参数的备注信息,以及是否必填等。
使用 swagger 除了让前后端交互变得方便,也让接口的请求变得简单,只需要填写好请求所需要的参数信息,便可直接发起请求。
比如说接口 /user/query-user-info
点击 Try it out

设置好请求所需的参数,点击 Execute 执行
就能看到接口响应的结果了
接口 /user/query-user-infos 也差不多

有时候我们的接口是需要获取请求头信息的,这样的话就还需要在 swagger 配置中添加请求头的配置。
@Bean public Docket docket() { // 设置请求头 List<Parameter> parameters = new ArrayList<>(); parameters.add(new ParameterBuilder() .name("token") // 字段名 .description("token") // 描述 .modelRef(new ModelRef("string")) // 数据类型 .parameterType("header") // 参数类型 .defaultValue("default value") // 默认值:可自己设置 .hidden(true) // 是否隐藏 .required(false) // 是否必须 .build()); // 创建一个 swagger 的 bean 实例 return new Docket(DocumentationType.SWAGGER_2) .groupName("mike") // 修改组名为 "mike" // 配置接口信息 .select() // 设置扫描接口 // 配置如何扫描接口 .apis(RequestHandlerSelectors .basePackage("com.duojiala.mikeboot.controller") // 扫描指定包下的接口,最为常用 ) .paths(PathSelectors .any() // 满足条件的路径,该断言总为true ) .build() // 添加请求头参数 .globalOperationParameters(parameters); }![]()
比如接口:
@GetMapping(value = "/get-token") @ApiOperation(value = "获取请求头中的token信息") public void getToken( @RequestHeader(value = "token",required = false) String token ) { // 直接获取 token 信息 System.out.println("token = " + token); // 通过代码获取 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (servletRequestAttributes != null) { HttpServletRequest request = servletRequestAttributes.getRequest(); String header = request.getHeader("token"); System.err.println("header = " + header); } }![]()

可以看到这个接口已经可以去设置请求头了,调用接口

后端也能获取到。
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
| 项目 | 描述 |
|---|---|
| 开发语言 | HTML、JavaScript、CSS |
| 库 | dyCalendarJS、vanilla-tilt |
| Edge | 108.0.1462.54 (正式版本) (64 位) |
该项目中需要使用到的库有:
如果你在观看本篇文章前并没有对这两个库进行了解,欢迎移步至我的另外两篇文章进行学习:
该项目文件中我已对代码进行了注释。如遇不懂的地方,请尝试查看相关注释。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>毛玻璃-倾斜-日历</title> <!-- 导入自定义 CSS 文件 --> <link rel="stylesheet" href="./index.css"> <!-- 导入 dycalendar.css --> <link rel="stylesheet" href="../dycalendar.min.css"> </head> <body> <div id="calendar" class="dycalendar-container"></div> <!-- 导入 dycalendar.js --> <script src="../dycalendar.min.js"></script> <!-- 导入 vanilla-tilt.js --> <script src="../vanilla-tilt.js"></script> <script> // 绘制日历 dycalendar.draw({ target: '#calendar', // 指定用于创建日历的 HTML 容器 type: 'month', // 设置日历的类型 prevnextbutton: 'show', // 显示 "<" 及 ">" 按钮 highlighttoday: true // 高亮显示当前日期 }) // 为目标元素添加倾斜效果 VanillaTilt.init(document.querySelector('#calendar'), { target: '#calendar', // 指定需要添加倾斜效果的目标元素 scale: 0.8, // 鼠标悬停于目标元素上时,目标元素的放缩倍数 glare: true, // 是否设置反光效果 'max-glare': 0.6 // 设置反光效果的强度 }) </script> </body> </html>![]()
*{ /* 去除元素默认的内外边距 */ margin: 0px; padding: 0px; /* 设置边框时将压缩内容区域,而不会向外扩张。 也就是说,为某个元素设置边框并不会改变其宽高。 */ box-sizing: border-box; } body{ /* 显示区域的最小高度为显示窗口的高度 */ min-height: 100vh; /* 设置该元素内部元素居中显示 */ display: flex; justify-content: center; align-items: center; /* 设置该元素的背景颜色 */ background-color: #161623; } body::before{ /* 若需要正常使用伪元素,必须为其设置 content 属性 */ content: ''; width: 400px; height: 400px; /* 设置颜色渐变效果 */ background: linear-gradient(#ffc107,#e91e63); /* 设置边框圆角,当该属性的值为 50% 时元素边框将显示为一个圆 */ border-radius: 50%; /* 为该元素设置绝对定位,阻止该元素遮挡日历 (定位元素可以设置 z-index 来调节显示顺序, z-index 的值越高,显示优先级越大)。 */ position: absolute; top: 10%; left: 20%; z-index: -1; } body::after{ content: ''; width: 300px; height: 300px; position: absolute; background: linear-gradient(#2196f3,#31ff38); border-radius: 50%; top: 45%; left: 55%; z-index: -1; } #calendar{ /* 设置日历的宽高 */ width: 400px; height: 400px; color: #fff; /* 设置日历的背景元素,为产生毛玻璃效果,这里将背景颜色设置为白色, 将透明度设置为 0.1(透明度的取值范围为 0~1,取值越接近 1 ,颜色 越不透明)。 */ background-color: rgb(255, 255, 255, 0.1); /* 设置 blur 过滤器,该过滤器可以将背景模糊化,参数中的 像素值设定越高,显示得越是模糊。 */ backdrop-filter: blur(50px); /* 分别设置日历的四条边框,使日历显示得更为立体 */ border-top: 1px solid rgb(255, 255, 255, 0.5); border-left: 1px solid rgb(255, 255, 255, 0.5); border-right: 1px solid rgb(255, 255, 255, 0.2); border-bottom: 1px solid rgb(255, 255, 255, 0.2); border-radius: 5px; /* 设置日历的内边距 */ padding: 0px 20px; /* 设置日历周边的阴影效果,box-shadow 接收的值(如下)分别为 阴影的 X 偏移量、阴影的 Y 偏移量、扩散半径、阴影颜色。 */ box-shadow: 5px 10px 10px rgb(0, 0, 0, 0.1); } /* 这里存在许多在 HTML 文件中没有看到的类名,这是因为这些标签 是 dyCalendarJS 通过 JavaScript 动态创建的元素,如果有需要对 日历中的某些元素的样式进行改变,可以通过浏览器的 检查 功能来查看 JavaScript 创建的元素并对其样式进行适当的修改。 */ /* 有些元素需要通过修改传递给 dycalendar.draw() 的配置对象中的 部分属性才能够被发现。 */ /* 设置日历的头部部分的样式 */ #calendar .dycalendar-header{ margin-top: 60px; font-size: 20px; } /* 设置日历 "<" 及 ">" 按钮的样式,应用该样式时请将 传递给 dycalendar.draw() 的配置对象中的 prevnextbutton 属性的值设置为 true 。 */ #calendar .dycalendar-header .prev-btn, #calendar .dycalendar-header .next-btn{ width: 40px; height: 30px; background-color: rgb(255, 255, 255, 0.15); /* 设置文本对其方式及行高以使 ">" 及 "<" 居中显示 */ text-align: center; line-height: 30px; /* 设置上下方向的外边距为 0px,设置左右方向的外边距为 5px */ margin: 0px 5px; } #calendar .dycalendar-body table{ width: 100%; height: 100%; margin-top: 50px; } /* tr:nth-child(1) 选择 table 标签中的第一个 tr 元素 */ /* 设置日历中星期(星期几)标识的样式 */ #calendar .dycalendar-body table tr:nth-child(1) td{ background-color: rgb(255, 255, 255, 0.15); margin-bottom: 20px; } #calendar .dycalendar-body table td{ border-radius: 3px; /* 设置鼠标悬停时的指针样式 */ cursor: pointer; } /* :hover 伪类选择器用于设置鼠标悬停在指定元素时, 某个元素的样式 */ #calendar .dycalendar-today-date, #calendar .dycalendar-body table td:hover{ color: #000; /* 使用 !important 提升该属性在多个设置了该属性的选择器 中的权重 */ background-color: #fff !important; }分享此文一切功德,皆悉回向给文章原作者及众读者. 免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
最近也是临近期末了,各种的期末大作业,后台管理也是很多地方需要用到的,为了方便大家能快速上手,快速搭建一个简单的后台管理,我花了两天时间整理了一下
我会从0开始介绍,从数据库的设计到前端页面的引入最后到后端代码的编写,你只需要会一点前端的基础和ssm的基础就能快速上手搭建一个简单的后台管理
本次案例分两篇文章教学:
(第一篇):数据表设计,前端框架引入和编写前端页面,搭建基本的springboot项目,引入前端到springboot项目中,在浏览器显示
(第二篇):后端代码的设计,这部分逻辑涉及的比较多,所以单独放一篇出来讲,代码从0手敲讲解,保证你能学会,完成增删改查的功能
目录
前言和环境介绍
无论是做app,网站,还是小程序,都少不了后台管理
那么对于前端不是很会,后端也是只会一些的人来说,如何快速搭建一个简单的后台管理系统呢,哎别急,今天就来教大家简单快速搭建一个后台管理系统
首先,简单介绍一下我的开发环境
工具 用处 H+ 前端框架,直观的教程文档,非常实用 SpringBoot 后端框架,简单上手,搭建快 MySQL 数据库 IDEA 非常强大的编译器 Ajax 异步请求,前端向后端发送请求 thymeleaf 模板引擎,实时渲染页面,基于HTML HBuild X 前端编译器,用其他的也可以,看自己 好了,环境介绍完毕,我们先从前端界面做起
数据库
一个后台管理,肯定少不了数据,不然怎么叫后台管理呢
这里我是用的是MySQL数据库,当然你使用其他的也行,不过后面在SpringBoot中要做不同的配置
在MySQL中新建一个user数据库,新建一个t_user表,字段如下,id,用户名,昵称,密码(id记得设置为自增长模式)
![]()
H+前端框架
基本介绍
官网地址:H+ 后台主题UI框架 - 主页
![]()
H+是一个非常强大的前端开源框架,开箱即用,不需要过多的配置,里面有非常多组件,具体就不一一介绍了,有兴趣的自己去看看
![]()
我们要做的是后台管理,所以我们直接找到表格,可以看到有很多样式选择,我们选择一个简单点
这里为了方便快速搭建我选择基本表格,当时你们可以根据自己喜欢来选择
![]()
H+框架引入
打开HBuild X编译器(你用其他的也可以,没影响)
在左侧空白处新建一个项目(快捷键ctrl+n),选择基于HTML普通项目,添加项目名称,选好路径,点击创建
![]()
一个基本的前端项目就创建好了
![]()
接着,将H+框架的css,js,font等静态资源全部复制项目下
![]()
编写后台表格页面
找到H+框架中基本表格的源码,复制代码到index.htm下
再对其进行一点修改,去掉右上角的工具栏,加上增删改查的按钮
![]()
修改之后的页面如下,用到了H+框中的表格、表单、按钮、字体图标库,就不详细介绍了,有兴趣可以自己看看前端代码,我们着重讲解js和后端的搭建
![]()
同时,我们还需要一个弹出框,当点击添加和修改的时候会弹出一个表单框
我们在H+前端框架的表单中找到弹出框示例,复制代码做点修改
![]()
修改后如下:
![]()
![]()
modal弹出框原理
HTML页面代码比较多,就不放上来了,底部Gitee仓库完整的项目,主要讲解一下上面这个modal弹出框怎么实现的就行
首先,每个modal弹出框都有唯一的标识ID属性,这里我们有两个,一个添加用户,一个修改用户(里面的表单代码我没放出来,比较多,文章底部Gitee仓库我上传了完整开源项目)
<!-- 添加用户的弹出框 --> <div id="modal-form-add" class="modal fade" aria-hidden="true"> </div>
<!-- 修改用户的弹出框 --> <div id="modal-form-update" class="modal fade" aria-hidden="true"> </div>我们为每个修改和删除按钮都添加一个类名标识(这里为什么不用ID,是因为ID只能唯一标识,添加按钮可以用ID,但是修改和删除不能用ID只能用class,因为有多个修改和删除按钮,添加按钮只有一个)
![]()
我们在js文件夹下面新建一个myJS文件夹,存放自己编写的js代码,新建一个index.js,添加以下代码
// 监听添加按钮事件(通过id属性监听) $('#addUserBtn').click(function() { // 添加按钮被点击之后,展示modal框 $('#modal-form-add').modal('show'); }) // 监听修改按钮事件(通过class属性监听) $('.updateUserBtn').click(function() { // 修改按钮被点击之后,展示modal框 $('#modal-form-update').modal('show'); })然后再index.html底部引入index.js即可
![]()
就可以实现点击添加和修改按钮会弹出modal表单框,简单前端管理页面就搭建完毕了,接下来是后端
搭建后端
基本介绍
SpringBoot是目前非常主流的后端框架,简化新spring应用的初始搭建以及开发过程,搭建快,省去了编写大量配置文件的过程
创建SpringBoot项目
打开IDEA,选择spring Initializr,点击下一步
![]()
添加主包名称,java version版本选择8,点击下一步
![]()
在左侧找到这五个依赖,勾选上,点击下一步
![]()
添加项目名称(默认为之前填写的主包名称),项目路径自己选择,点击完成
![]()
右下角处选择自动导入pom.xml依赖(没有的话直接跳过)
![]()
一个基本的SpringBoot项目就创建完毕了
![]()
基本配置
在搭建项目之前,先做一些基本的配置,如数据源(连接MySQL)、static静态资源映射(避免被拦截)、设置端口号(避免冲突)等
删除原有的application.properties文件,新建一个application.yml文件(两种都可以,yml文件方便一点),添加以下配置信息
![]()
spring: # 数据源,连接MySQL数据库 datasource: url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true username: 数据库用户名 password: 数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver # JPA配置,打印sql语句 jpa: show-sql: true properties: hibernate: format_sql: true # mvc配置,映射html页面 mvc: static-path-pattern: /** view: prefix: / suffix: .html # thymeleaf模板引擎配置,设置编码,false取消缓存 thymeleaf: encoding: UTF-8 cache: false server: # 修改启动端口号 port: 8081 # 静态资源映射路径 web: resources: static-locations: classpath:/static/在项目目录下新建一个config包(存在基本配置类),新建一个MyWebMVCConfig类,代码如下
@Configuration public class MyWebMVCConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { System.out.println("==========静态资源拦截!============"); //将所有/static/** 访问都映射到classpath:/static/ 目录下 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
引入前端
把刚刚在HBuildX写好的前端页面引入到IDEA中,首先复制除了html的其他所有文件到static文件夹下
![]()
接着复制index.html到templates文件夹下
![]()
然后打开index.html,在所有访问静态资源的链接前面加上/static/
![]()
测试项目(测试一下浏览器是否能显示页面)
想项目目录下新建一个controller包,新建一个IndexCtroller类,添加以下代码
![]()
@RestController public class IndexController { @RequestMapping(value = "/index") // 访问路径 public ModelAndView toIndex() { // 返回templates目录下index.html ModelAndView view = new ModelAndView("index"); return view; } }点击启动项目,选哪个都可以,我一般选第二个debug模式启动,方式debug调试
![]()
等待启动完成之后,打开浏览器,输入 localhost:8081/index,页面成功显示
![]()
第一篇文章到这里介绍完毕,第二篇正在火速撰写中······
Gitee开源项目地址(本次项目源码)
SpringBoot项目教学合集: CSDN中的所有SpringBoot项目开源,持续更新新项目、新教学文章
各大技术基础教学、实战项目开发教学
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
目录
把函数当作一个参数传到另外一个函数中,当需要用这个函数是,再回调运行()这个函数.
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。(作为参数传递到另外一个函数中,这个作为参数的函数就是回调函数)
理解:函数可以作为一个参数传递到另外一个函数中。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
function print(num) {
-
console.log(num);
-
}
-
-
add(1, 2, print); //3
-
</script>
分析:add(1, 2, print);中,函数print作为一个参数传入到add函数中,但并不是马上起作用,而是var sum = num1 + num2;运行完之后需要打印输出sum的时候才会调用这个函数。(这个作为参数传递到另外一个函数中,这个作为参数的函数就是回调函数.
匿名回调函数:
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
callback(sum);
-
}
-
-
add(1, 2, function (sum) {
-
console.log(sum); //=>3
-
});
-
</script>
1.不会立即执行
回调函数作为参数传递给一个函数的时候,传递的只是函数的定义并不会立即执行。和普通的函数一样,回调函数在调用函数数中也要通过()运算符调用才会执行。
2.回调函数是一个闭包
回调函数是一个闭包,也就是说它能访问到其外层定义的变量。
3.执行前类型判断
在执行回调函数前最好确认其是一个函数。
-
<script>
-
function add(num1, num2, callback) {
-
var sum = num1 + num2;
-
//判定callback接收到的数据是一个函数
-
if (typeof callback === 'function') {
-
//callback是一个函数,才能当回调函数使用
-
callback(sum);
-
}
-
}
-
</script>
注意在回调函数调用时this的执行上下文并不是回调函数定义时的那个上下文,而是调用它的函数所在的上下文。
举例:
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData(function(n){
-
console.log(this,1111); //window 1111
-
})
-
}
-
}
-
obj.tool();
-
</script>
分析:this指向是 离它最近的或者嵌套级别的 function/方法的调用者,这里离它最近的function是
function(n),会回到上面的callback()中,这时候调用者就不是obj而是window。
解决回调函数this指向的方法1:箭头函数
回调函数(若回调函数是普通函数时)当参数传入另外的函数时,若不知道这个函数内部怎么调用回调函数,就会出现回调函数中的this指向不明确的问题(就比如上面例子中this指向的不是obj而是window)。所以 把箭头函数当回调函数,然后作为参数传入另外的函数中就不会出现this指向不明的问题。
-
<script>
-
function createData(callback){
-
callback();
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
createData((n)=>{
-
this.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
分析:回调函数用箭头函数写之后,this指向很明确,就是 离它最近的或者嵌套级别的 function/方法的调用者,所以这里是 obj 。
解决回调函数this指向的方法2:var self = this;
-
<script>
-
function createData(callback){
-
callback(999);
-
}
-
var obj ={
-
data:100,
-
tool:function(){
-
var self = this; //这里的this指向obj,然后当一个变量取用
-
createData(function(n){
-
self.data = n;
-
})
-
}
-
}
-
obj.tool();
-
console.log(obj.data);
-
</script>
有一个非常重要的原因 —— JavaScript 是事件驱动的语言。这意味着,JavaScript 不会因为要等待一个响应而停止当前运行,而是在监听其他事件时继续执行。来看一个基本的例子:
-
<script>
-
function first() {
-
console.log(1);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:正如你所料,first 函数首先被执行,随后 second 被执行 —— 控制台输出:1 2
但如果函数 first 包含某种不能立即执行的代码会如何呢?例如我们必须发送请求然后等待响应的 API 请求?为了模拟这种状况,我们将使用 setTimeout,它是一个在一段时间之后调用函数的 JavaScript 函数。我们将函数延迟 500 毫秒来模拟一个 API 请求,新代码长这样:
-
<script>
-
function first() {
-
// 模拟代码延迟
-
setTimeout(function () { //所以function(){console.log(1)}是回调函数
-
console.log(1);
-
}, 500);
-
}
-
-
function second() {
-
console.log(2);
-
}
-
-
first();
-
second();
-
</script>
分析:这里 function(){console.log(1)}函数当作一个参数传入setTimeout函数中,因为setTimeout是官方提供得一个函数,里面有很多复杂的业务程序,所以函数 function(){console.log(1)}传入后,不一定马上运行,要setTimeout里面要运行到function(){console.log(1)}时才会运行该函数参数,那是不是整个程序就一直等setTimeout运行?不是的!!!
整个程序运行结果为: 2 1 ,并不是原先的1 2 .即使我们首先调用了 first() 函数,我们记录的输出结果却在 second() 函数之后。
这不是 JavaScript 没有按照我们想要的顺序执行函数的问题,而是 JavaScript 在继续向下执行 second() 之前没有等待 first() 响应的问题。回调正是确保一段代码执行完毕之后再执行另一段代码的方式。
定义:回调函数被认为是一种高级函数,一种被作为参数传递给另一个函数的高级函数。回调函数的本质是一种模式(一种解决常见问题的模式),因此回调函数也被称为回调模式。
简而言之:一个函数在另一个函数中被调用。而且可以当参数传给其他函数。
所以: 回调函数和异步操作的关系是没有关系!!!
那为什么很多的异步操作都有回填函数啊??
问:你所知道的异步操作,是回调的作用么??? 并不是。
回调:更多的可以理解为一种业务逻辑把 异步编程:JS代码的执行顺序
简单理解:callback 顾名思义 打电话回来的意思
eg1:你点外卖,刚好你要吃的食物没有了,于是你在店老板那里留下了你的电话,过了几天店里有了,店员就打了你的电话,然后你接到电话后就跑到店里买了。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
eg2:再比如,你发送一个axios 请求,请求成功之后,触发成功的回调函数,请求失败触发失败的回调函数。这里面的回调函数更像是一个工具,后台通过这个工具告诉你,你成功了抑或是失败了。这里面的所有异步操作都和回调没关系,真正的异步是then方法。
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
概述:基于WebGL的三维引擎,目前是国内资料最多、使用最广泛的三维引擎,可以制作一些3D可视化项目
目前随着元宇宙概念的爆火,THREE技术已经深入到了物联网、VR、游戏、数据可视化等多个平台,今天我们主要基于THREE实现一个三维的VR看房小项目
Three.js一般分为三个部分:场景、相机、渲染器,这三个主要的分支就构成了THREE.JS的主要功能区,这三大部分还有许多细小的分支,这些留到我们后续抽出一些章节专门讲解一下。
工作流程:场景——相机——渲染器
从实际生活中拍照角度立方体网格模型和光照组成了一个虚拟的三维场景,相机对象就像你生活中使用的相机一样可以拍照,只不过一个是拍摄真实的景物,一个是拍摄虚拟的景物。拍摄一个物体的时候相机的位置和角度需要设置,虚拟的相机还需要设置投影方式,当你创建好一个三维场景,相机也设置好,就差一个动作“咔”,通过渲染器就可以执行拍照动作。
概述:场景主要由网络模型与光照组成,网络模型分为几何体与材质
几何体就像我们小时候学我们就知道点线面体四种概念,点动成线,线动成面,面动成体,而材质就像是是几何体上面的涂鸦,有不同的颜色、图案…
例子如下:
//打造酷炫三角形 for (let i = 0; i < 50; i++) { const geometry = new THREE.BufferGeometry(); const arr = new Float32Array(9); for (let j = 0; j < 9; j++) { arr[j] = Math.random() * 5; } geometry.setAttribute('position', new THREE.BufferAttribute(arr, 3)); let randomColor = new THREE.Color(Math.random(), Math.random(), Math.random()); const material = new THREE.MeshBasicMaterial({ color: randomColor, transparent: true, opacity:0.5, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UlBSgxKr-1666681292595)(https://gitee.com/riskbaby/picgo/raw/master/blog/202209211037215.png#pic_center)]
const geometry = new THREE.BoxGeometry(100, 100, 100); const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const cube = new THREE.Mesh(geometry, material); scene.add(cube);
const geometry = new THREE.ConeGeometry(5, 15, 32);//底面半径 高 侧边三角分段 const material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); const clone = new THREE.Mesh(geometry, material); scene.add(clone);
概念:光照对three.js的物体全表面进行光照测试,有可能会发生光照融合
//环境光 const ambient = new THREE.AmbientLight(0x404040); scene.add(ambient);
概念:向特定方向发射的光,太阳光也视作平行的一种,和上面比较,物体变亮了
//平行光 颜色 强度 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(100, 100, 100);//光源位置 directionalLight.target = cube;//光源目标 默认 0 0 0 scene.add(directionalLight);
概念:由中间向四周发射光、强度比平行光小
// 颜色 强度 距离 衰退量(默认1) const pointLight = new THREE.PointLight(0xff0000, 1, 100, 1); pointLight.position.set(50, 50, 50); scene.add(pointLight);
概念:家里面的节能灯泡,强度较好
//聚光灯 const spotLigth = new THREE.PointLight(0xffffff); spotLigth.position.set(50, 50, 50); spotLigth.target = cube; spotLigth.angle = Math.PI / 6; scene.add(spotLigth);
概念:光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色
//半球光 const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);//天空 场景 scene.add(light);
| 参数(属性) | 含义 |
|---|---|
| left | 渲染空间的左边界 |
| right | 渲染空间的右边界 |
| top | 渲染空间的上边界 |
| bottom | 渲染空间的下边界 |
| near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1 |
| far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.OrthographicCamera(width / - 2, width / 2, height / 2, height / - 2, 1, 1000); scene.add(camera); camera.position.set(100, 200, 100);
| 参数 | 含义 | 默认值 |
|---|---|---|
| fov | fov表示视场,所谓视场就是能够看到的角度范围,人的眼睛大约能够看到180度的视场,视角大小设置要根据具体应用,一般游戏会设置60~90度 | 45 |
| aspect | aspect表示渲染窗口的长宽比,如果一个网页上只有一个全屏的canvas画布且画布上只有一个窗口,那么aspect的值就是网页窗口客户区的宽高比 | window.innerWidth/window.innerHeight |
| near | near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 | 0.1 |
| far | far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小,会有部分场景看不到 | 1000 |
let width = window.innerWidth; let height = window.innerHeight; const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000); camera.position.set(150, 100, 300); camera.lookAt(scene.position);
概述:从WEBGL的角度来看,three就是对它的进一步封装,想要进一步了解渲染器这方面的知识点还需要了解一下WEBGL,这里我们就不做过多介绍了。
概述:这部分对于我们是否能够给别人呈现一个真实的渲染场景来说,很重要,比如下面一个普普通通的正方体,我们只要一加上贴图,立马不一样了。
以前
之后
概述:目前有许许多多的贴图,比如基础、透明、环境、法线、金属、粗糙、置换等等,今天我们呢主要讲解一下环境和一点 HDR处理
在THREE的世界里面,坐标抽x、y、z的位置关系图如下所示:
红、绿、蓝分别代表x、z、y,我们的贴图就是在px nx py ny pz nz这六个方向防止一张图片,其中p就代表坐标轴的正方向
CubeTextureLoader:加载CubeTexture的一个类。 内部使用ImageLoader来加载文件。
//场景贴图 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //场景添加背景 scene.background = envTexture; //场景的物体添加环境贴图(无默认情况使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//设置粗糙程度 metalness: 1,//金属度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere);
gif图片有点大上传不了,我就截了几张图
概述:高动态范围图像,相比普通的图像,能够提供更多的动态范围和图像细节,一般被运用于电视显示产品以及图片视频拍摄制作当中。
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader; const rgbeLoader = new RGBELoader().setPath('./textures/course/hdr/'); //异步加载 rgbeLoader.loadAsync('002.hdr').then((texture) => { //设置加载方式 等距圆柱投影的环境贴图 texture.mapping = THREE.EquirectangularReflectionMapping; scene.background = texture; })
概述:坐标轴能够更好的反馈物体的位置信息,红、绿、蓝分别代表x、z、y
const axesHelper = new THREE.AxesHelper(20);//里面的数字代表坐标抽长度 scene.add(axesHelper);
概述:通过鼠标控制物体和相机的移动、旋转、缩放
导包
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
应用
const controls = new OrbitControls(camera, renderer.domElement)
自旋转
controls.autoRotate = true
必须在render函数调用update实时更新才奏效
概述:根据屏幕大小自适应场景
//自适应屏幕 window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) renderer.setPixelRatio(window.devicePixelRatio) })
设置相机的宽高比、重新更新渲染相机、渲染器的渲染大小、设备的像素比
概述:双击进入全屏,再次双击/ESC退出全屏
window.addEventListener('dblclick', () => { let isFullScreen = document.fullscreenElement if (!isFullScreen) { renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } })
概述;通过操作面板完成界面的移动物体的相关应用
链接:https://www.npmjs.com/package/dat.gui
//安装npm npm install --save dat.gui //如果出现...标记错误,安装到开发依赖就可以了 npm i --save-dev @types/dat.gui
//界面操作 const gui = new dat.GUI(); //操作物体位置 gui .add(cube.position, 'x') .min(0) .max(10) .step(0.1) .name('X轴移动') .onChange((value) => { console.log('修改的值为' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物体颜色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物体颜色 cube.material.color.set(value); });
概述:检测帧率
导包
import Stats from 'three/addons/libs/stats.module.js';
应用
const stats = new Stats(); document.body.appendChild(stats.dom);
自变化
stats.update()
必须在render函数调用update实时更新才奏效
概述:底部二维平面的网格化,帮助我们更好的创建场景
const gridHelper = new THREE.GridHelper(10, 20)//网格大小、细分次数 scene.add(gridHelper)
//导入包 import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import * as dat from 'dat.gui'; import Stats from 'three/addons/libs/stats.module.js'; let scene,camera,renderer //场景 scene = new THREE.Scene(); //坐标抽 const axesHelper = new THREE.AxesHelper(20); scene.add(axesHelper); //场景贴图 const sphereTexture = new THREE.CubeTextureLoader().setPath('./textures/course/environmentMaps/0/'); const envTexture= sphereTexture.load([ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ]); //场景添加背景 scene.background = envTexture; //场景的物体添加环境贴图(无默认情况使用) scene.environment = envTexture; const sphereGeometry = new THREE.SphereGeometry(5, 30, 30); const sphereMaterial = new THREE.MeshStandardMaterial({ roughness: 0,//设置粗糙程度 metalness: 1,//金属度 envMap:envTexture, }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere); //光照 const ambient = new THREE.AmbientLight(0xffffff); scene.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.05); directionalLight.position.set(10,10,10); directionalLight.lookAt(scene.position); scene.add( directionalLight ); //相机 camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 2000, ); camera.position.set(10,10,20); camera.lookAt(scene.position); scene.add(camera); //渲染器 renderer = new THREE.WebGLRenderer({ //防止锯齿 antialias: true, }); renderer.setSize(window.innerWidth, window.innerHeight); // renderer.setClearColor(0xb9d3ff, 1); document.body.appendChild(renderer.domElement); //鼠标控制器 const controls = new OrbitControls(camera, renderer.domElement); //阻尼 必须在 render函数调用 controls.update(); controls.dampingFactor = true; controls.autoRotate=true const stats=new Stats() document.body.appendChild(stats.dom); function render () { renderer.render(scene, camera); requestAnimationFrame(render); controls.update();//调用 stats.update() } render(); //全屏操作 window.addEventListener('dblclick', () => { //查询是否全屏 let isFullScene = document.fullscreenElement; console.log(isFullScene); if (!isFullScene) { renderer.domElement.requestFullscreen(); } else { document.exitFullscreen(); } }) //自适应 window.addEventListener('resize', () => { //宽高比 camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio);//设置像素比 }) //界面操作 const gui = new dat.GUI(); //操作物体位置 gui .add(sphere.position, 'x') .min(0) .max(10) .step(0.1) .name('X轴移动') .onChange((value) => { console.log('修改的值为' + value); }) .onFinishChange((value) => { console.log('完全停止' + value); }); //操作物体颜色 const colors = { color: '#0000ff', }; gui .addColor(colors, 'color') .onChange((value) => { //修改物体颜色 sphere.material.color.set(value); });
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
目录
相同点:
- 都是基于tcp的,都是可靠性传输协议
- 都是应用层协议
不同点:
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
- HTTP是单向的
- WebSocket是需要浏览器和服务器握手进行建立连接的
- 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
联系:
- WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的
总结(总体过程):
- 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
- 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
从例子上来看有个问题:
- 假如有好多人一起在快递站等快递,那么这个地方是否足够大,(抽象解释:需要有很高的并发,同时有很多请求等待在这里)
推送延迟。服务端数据发生变更后,长轮询结束,立刻返回响应给客户端。
服务端压力。长轮询的间隔期一般很长,例如 30s、60s,并且服务端 hold 住连接不会消耗太多服务端资源。
从例子上来看有两个问题:
- 假如说,张三打电话的时间间隔为10分钟,当他收到快递前最后一次打电话,快递员说没到,他刚挂掉电话,快递入库了(就是到了),那么等下一次时间到了,张三打电话知道快递到了,那么这样的通讯算不算实时通讯?很显然,不算,中间有十分钟的时间差,还不算给快递员打电话的等待时间(抽象的解释:每次request的请求时间间隔等同于十分钟,请求解析相当于等待)
- 假如说张三所在的小区每天要收很多快递,每个人都采取主动给快递员打电话的方式,那么快递员需要以多快的速度接到,其他人打电话占线也是问题(抽象解释:请求过多,服务端响应也会变慢)
推送延迟。
服务端压力。配置一般不会发生变化,频繁的轮询会给服务端造成很大的压力。
推送延迟和服务端压力无法中和。降低轮询的间隔,延迟降低,压力增加;增加轮询的间隔,压力降低,延迟增高
一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实现了“真·长链接”,实时性优势明显。
WebSocket有以下特点:
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
目前主流的 Web 开发模式有两种,分别是:
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:
优点:
缺点:
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
优点:
缺点:
不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式。
身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
身份认证的目的,是为了确认当前所声称为某种身份的用户,确实是所声称的用户。例如,你去找快递员取快递,你要怎么证明这份快递是你的。
在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将“马云的存款数额”显示到“马化腾的账户”上。
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
注意:现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie
Cookie的几大特性:
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
注意:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。
3.6、提高身份认证的安全性
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。
这种“会员卡 + 刷卡认证”的设计理念,就是 Session 认证机制的精髓。
在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证:
npm install express-session
express-session 中间件安装成功后,需要通过 app.use() 来注册 session 中间件,示例代码如下:
-
// 导入 session 中间件
-
const session = require('express-session')
-
-
// 配置 session 中间件
-
app.use(
-
session({
-
secret: 'itheima', // secret 属性的值可以为任意字符串
-
resave: false, // 固定写法
-
saveUninitialized: true, // 固定写法
-
})
-
)
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:
-
// 登录的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判断用户提交的登录信息是否正确
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登录失败' })
-
}
-
-
// TODO_02:请将登录成功后的用户信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
-
req.session.user = req.body // 用户的信息
-
console.log(req.body)
-
req.session.islogin = true // 用户的登录状态
-
-
res.send({ status: 0, msg: '登录成功' })
-
})
可以直接从 req.session 对象上获取之前存储的数据,示例代码如下:
-
// 获取用户姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:请从 Session 中获取用户的名称,响应给客户端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息。
-
// 退出登录的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登录成功',
-
})
-
})
index.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>后台主页</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<h1>首页</h1>
-
-
<button id="btnLogout">退出登录</button>
-
-
<script>
-
$(function () {
-
-
// 页面加载完成后,自动发起请求,获取用户姓名
-
$.get('/api/username', function (res) {
-
// status 为 0 表示获取用户名称成功;否则表示获取用户名称失败!
-
if (res.status !== 0) {
-
alert('您尚未登录,请登录后再执行此操作!')
-
location.href = './login.html'
-
} else {
-
alert('欢迎您:' + res.username)
-
}
-
})
-
-
// 点击按钮退出登录
-
$('#btnLogout').on('click', function () {
-
// 发起 POST 请求,退出登录
-
$.post('/api/logout', function (res) {
-
if (res.status === 0) {
-
// 如果 status 为 0,则表示退出成功,重新跳转到登录页面
-
location.href = './login.html'
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
login.html
-
<!DOCTYPE html>
-
<html lang="en">
-
-
<head>
-
<meta charset="UTF-8">
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
<title>登录页面</title>
-
<script src="./jquery.js"></script>
-
</head>
-
-
<body>
-
<!-- 登录表单 -->
-
<form id="form1">
-
<div>账号:<input type="text" name="username" autocomplete="off" /></div>
-
<div>密码:<input type="password" name="password" /></div>
-
<button>登录</button>
-
</form>
-
-
<script>
-
$(function () {
-
// 监听表单的提交事件
-
$('#form1').on('submit', function (e) {
-
// 阻止默认提交行为
-
e.preventDefault()
-
// 发起 POST 登录请求
-
$.post('/api/login', $(this).serialize(), function (res) {
-
// status 为 0 表示登录成功;否则表示登录失败!
-
if (res.status === 0) {
-
location.href = './index.html'
-
} else {
-
alert('登录失败!')
-
}
-
})
-
})
-
})
-
</script>
-
</body>
-
-
</html>
app.js
-
// 导入 express 模块
-
const express = require('express')
-
// 创建 express 的服务器实例
-
const app = express()
-
-
// TODO_01:请配置 Session 中间件
-
const session = require('express-session')
-
app.use(
-
session({
-
secret: 'itheima',
-
resave: false,
-
saveUninitialized: true,
-
})
-
)
-
-
// 托管静态页面
-
app.use(express.static('./pages'))
-
// 解析 POST 提交过来的表单数据
-
app.use(express.urlencoded({ extended: false }))
-
-
// 登录的 API 接口
-
app.post('/api/login', (req, res) => {
-
// 判断用户提交的登录信息是否正确
-
if (req.body.username !== 'admin' || req.body.password !== '000000') {
-
return res.send({ status: 1, msg: '登录失败' })
-
}
-
-
// TODO_02:请将登录成功后的用户信息,保存到 Session 中
-
// 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
-
req.session.user = req.body // 用户的信息
-
console.log(req.body)
-
req.session.islogin = true // 用户的登录状态
-
-
res.send({ status: 0, msg: '登录成功' })
-
})
-
-
// 获取用户姓名的接口
-
app.get('/api/username', (req, res) => {
-
// TODO_03:请从 Session 中获取用户的名称,响应给客户端
-
if (!req.session.islogin) {
-
return res.send({ status: 1, msg: 'fail' })
-
}
-
res.send({
-
status: 0,
-
msg: 'success',
-
username: req.session.user.username,
-
})
-
})
-
-
// 退出登录的接口
-
app.post('/api/logout', (req, res) => {
-
// TODO_04:清空 Session 信息
-
req.session.destroy()
-
res.send({
-
status: 0,
-
msg: '退出登录成功',
-
})
-
})
-
-
// 调用 app.listen 方法,指定端口号并启动web服务器
-
app.listen(80, function () {
-
console.log('Express server running at http://127.0.0.1:80')
-
})
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意:
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。
总结:用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。
三者之间使用英文的“.”分隔,格式如下:
下面是 JWT 字符串的示例:
JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,格式如下:
运行如下命令,安装如下两个 JWT 相关的包:
npm install jsonwebtoken express-jwt
其中:
使用 require() 函数,分别导入 JWT 相关的两个包:
-
// 安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
-
const jwt = require('jsonwebtoken')
-
const expressJWT = require('express-jwt')
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
-
// 定义 secret 密钥,建议将密钥命名为 secretKey,本质上就是一个字符串
-
const secretKey = 'itheima No1 ^_^'
调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端:
-
// 登录接口
-
app.post('/api/login', function (req, res) {
-
// 将 req.body 请求体中的数据,转存为 userinfo 常量
-
const userinfo = req.body
-
// 登录失败
-
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
-
return res.send({
-
status: 400,
-
message: '登录失败!',
-
})
-
}
-
// 登录成功
-
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
-
// 参数1:用户的信息对象
-
// 参数2:加密的秘钥
-
// 参数3:配置对象,可以配置当前 token 的有效期
-
// 记住:千万不要把密码加密到 token 字符中
-
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
-
res.send({
-
status: 200,
-
message: '登录成功!',
-
token: tokenStr, // 要发送给客户端的 token 字符串
-
})
-
})
-
// 使用 app.use() 来注册中间件
-
// expressJWT({ secret: secretKey }) 就是用来解析 Token 的中间件
-
// .unless({ path: [/^\/api\//] }) 用来指定哪些接口不需要访问权限
-
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
6.6、使用 req.user 获取用户信息
当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:
-
// 这是一个有权限的 API 接口
-
app.get('/admin/getinfo', function (req, res) {
-
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
-
console.log(req.user)
-
res.send({
-
status: 200,
-
message: '获取用户信息成功!',
-
data: req.user, // 要发送给客户端的用户信息
-
})
-
})
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
jsoup 是一款基于 Java 的HTML解析器,它提供了一套非常省力的API,不但能直接解析某个URL地址、HTML文本内容,而且还能通过类似于DOM、CSS或者jQuery的方法来操作数据,所以 jsoup 也可以被当做爬虫工具使用。
Document :文档对象。每份HTML页面都是一个文档对象,Document 是 jsoup 体系中最顶层的结构。
Element:元素对象。一个 Document 中可以着包含着多个 Element 对象,可以使用 Element 对象来遍历节点提取数据或者直接操作HTML。
Elements:元素对象集合,类似于List<Element>。
Node:节点对象。标签名称、属性等都是节点对象,节点对象用来存储数据。
获得文档对象 Document 一共有4种方法,分别对应不同的获取方式。
正式开始之前,我们需要导入有关 jar 包。
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.1</version> </dependency>
–
使用 Jsoup.connect(String url).get()方法获取(只支持 http 和 https 协议):
Document doc = Jsoup.connect("http://csdn.com/").get(); String title = doc.title(); System.out.println(title);
connect(String url)方法创建一个新的 Connection并通过.get()或者.post()方法获得数据。如果从该URL获取HTML时发生错误,便会抛出 IOException,应适当处理。
Connection 接口还提供一个方法链来解决特殊请求,我们可以在发送请求时带上请求的头部参数,具体如下:
Document doc = Jsoup.connect("http://csdn.com") .data("query", "Java") .userAgent("Mozilla") .cookie("auth", "token") .timeout(8000) .post(); System.out.println(doc);
想获得完整的响应对象和响应码?我们可以使用execute()方法:
// 获得响应对象 Connection.Response response = Jsoup.connect("http://csdn.com").execute(); int code = response.statusCode(); // 输出状态码:200 System.out.println(code);
–
可以使用静态的Jsoup.parse(File in, String charsetName) 方法从文件中加载文档。其中in表示路径,charsetName表示编码方式,示例代码:
File input = new File("/tmp/input.html"); Document doc = Jsoup.parse(input, "UTF-8"); System.out.println(doc);
–
使用静态的Jsoup.parse(String html) 方法可以从字符串文本中获得文档对象 Document ,示例代码:
String html = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>"; Document doc = Jsoup.parse(html); System.out.println(doc);
–
使用Jsoup.parseBodyFragment(String html)方法.
String html = "<p>Lorem ipsum.</p>"; Document doc = Jsoup.parseBodyFragment(html); // doc 此时为:<body> <p>Lorem ipsum.</p></body> Element body = doc.body(); System.out.println(body);
parseBodyFragment 方法创建一个新的文档,并插入解析过的HTML到body元素中。假如你使用正常的 Jsoup.parse(String html) 方法,通常也能得到相同的结果,但是明确将用户输入作为 body 片段处理是个更好的方式。
Document.body() 方法能够取得文档body元素的所有子元素,与 doc.getElementsByTag("body")相同。
解析文档对象并获取数据一共有 2 种方式,分别为 DOM方式、CSS选择器方式,我们可以选择一种自己喜欢的方式去获取数据,效果一样。
将HTML解析成一个Document之后,就可以使用类似于DOM的方法进行操作。
// 获取csdn首页所有的链接 Document doc = Jsoup.connect("http://csdn.com").get(); Elements elements = doc.getElementsByTag("body"); Elements contents = elements.first().getElementsByTag("a"); for (Element content : contents) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText+"\t"); System.out.println(linkHref); }
说明
Elements这个对象提供了一系列类似于DOM的方法来查找元素,抽取并处理其中的数据。具体如下:
4.1.1)查找元素
getElementById(String id):通过id来查找元素
getElementsByTag(String tag):通过标签来查找元素
getElementsByClass(String className):通过类选择器来查找元素
getElementsByAttribute(String key) :通过属性名称来查找元素,例如查找带有href元素的标签。
siblingElements():获取兄弟元素。如果元素没有兄弟元素,则返回一个空列表。
firstElementSibling():获取第一个兄弟元素。
lastElementSibling():获取最后一个兄弟元素。
nextElementSibling():获取下一个兄弟元素。
previousElementSibling():获取上一个兄弟元素。
parent():获取此节点的父节点。
children():获取此节点的所有子节点。
child(int index):获取此节点的指定子节点。
4.1.2)获取元素数据
attr(String key):获取单个属性值
attributes():获取所有属性值
attr(String key, String value):设置属性值
text():获取文本内容
text(String value):设置文本内容
html():获取元素内的HTML内容
html(String value):设置元素内的HTML内容
outerHtml():获取元素外HTML内容
data():获取数据内容(例如:script和style标签)
id():获得id值(例:<p id="goods">衣服</p>)
className():获得第一个类选择器值
classNames():获得所有的类选择器值
tag():获取元素标签
tagName():获取元素标签名(如:<p>、<div>等)
4.1.3)操作HTML文本
append(String html):在末尾追加HTML文本
prepend(String html):在开头追加HTML文本
html(String value):在匹配元素内部添加HTML文本。
–
可以使用类似于CSS选择器的语法来查找和操作元素,常用的方法为select(String selector)。
Document doc = Jsoup.connect("http://csdn.com").get(); // 获取带有 href 属性的 a 元素 Elements elements = doc.select("a[href]"); for (Element content : elements) { String linkHref = content.attr("href"); String linkText = content.text(); System.out.print(linkText + "\t"); System.out.println(linkHref); }
4.2.1)说明
select()方法在Document、Element或Elements对象中都可以使用,而且是上下文相关的,因此可实现指定元素的过滤,或者采用链式访问。
select() 方法将返回一个Elements集合,并提供一组方法来抽取和处理结果。
4.2.2)select(String selector)方法参数简介
tagname: 通过标签查找元素,例如通过"a"来查找<a>标签。
#id: 通过ID查找元素,比如通过#logo查找<p id="logo">。
.class: 通过class名称查找元素,比如通过.titile查找<p class="titile">。
ns|tag: 通过标签在命名空间查找元素,比如使用 fb|name 来查找 <fb:name> 。
[attribute]: 利用属性查找元素,比如通过[href]查找<a href="...">。
[^attribute]: 利用属性名前缀来查找元素,比如:可以用[^data-] 来查找带有HTML5 dataset属性的元素。
[attribute=value]: 利用属性值来查找元素,比如:[width=500]。
[attribute^=value], [attribute$=value], [attribute*=value]: 利用匹配属性值开头、结尾或包含属性值来查找元素,比如通过[href*=/path/]来查找<a href="a/path/c.html">。
[attribute~=regex]: 利用属性值匹配正则表达式来查找元素,比如通过 img[src~=(?i)\.(png|jpe?g)]来匹配所有的png或者jpg、jpeg格式的图片。
*: 通配符,匹配所有元素。
4.2.3)参数属性组合使用
el#id: 元素+ID,比如: div#logo
el.class: 元素+class,比如: div.masthead
el[attr]: 元素+class,比如 a[href]匹配所有带有 href 属性的 a 元素。
a[href].highlight匹配所有带有 href 属性且class="highlight"的 a 元素。
ancestor child: 查找某个元素下子元素,比如:可以用.body p 查找在"body"元素下的所有 p元素
parent > child: 查找某个父元素下的直接子元素,比如:可以用div.content > p 查找 p 元素,也可以用body > * 查找body标签下所有直接子元素
siblingA + siblingB: 查找在A元素之前第一个同级元素B,比如:div.head + div
siblingA ~ siblingX: 查找A元素之前的同级X元素,比如:h1 ~ p
el, el, el:多个选择器组合,查找匹配任一选择器的唯一元素,例如:div.masthead, div.logo
4.2.4)特殊参数:伪选择器
:lt(n): 查找哪些元素的同级索引值(它的位置在DOM树中是相对于它的父节点)小于n,比如:td:lt(3) 表示小于三列的元素
:gt(n):查找哪些元素的同级索引值大于n``,比如: div p:gt(2)表示哪些div中有包含2个以上的p元素
:eq(n): 查找哪些元素的同级索引值与n相等,比如:form input:eq(1)表示包含一个input标签的Form元素
:has(seletor): 查找匹配选择器包含元素的元素,比如:div:has(p)表示哪些div包含了p元素
:not(selector): 查找与选择器不匹配的元素,比如: div:not(.logo) 表示不包含 class=logo 元素的所有 div 列表
:contains(text): 查找包含给定文本的元素,搜索不区分大不写,比如: p:contains(jsoup)
:containsOwn(text): 查找直接包含给定文本的元素
:matches(regex): 查找哪些元素的文本匹配指定的正则表达式,比如:div:matches((?i)login)
:matchesOwn(regex): 查找自身包含文本匹配指定正则表达式的元素
在获得文档对象并且指定查找元素后,我们就可以获取元素中的数据。
这些访问器方法都有相应的setter方法来更改数据。
.attr(String key) :获得属性的值。
.text():获得元素中的文本。
.html():获得元素或属性内部的HTML内容(不包括本身)。
.outerHtml():获得元素或属性完整的HTML内容。
.id():获得元素id属性值。
className():获得元素类选择器值。
.tagName():获得元素标签命名。
.hasClass(String className):检查这个元素是否含有一个类选择器(不区分大小写)。
String html = "<p><a href='http://csdn.com/'><b>example</b></a> link.</p>"; Document doc = Jsoup.parse(html); // 查找第一个<a>元素 Element link = doc.select("a").first(); // 输出:example String text = link.text(); // 输出:http://csdn.com/ String href = link.attr("href"); // 输出:<b>example</b> String aHtml = link.outerHtml(); // 输出:<a href='http://csdn.com/'><b>example</b></a> String aOuterHtml = link.outerHtml();
–
在解析了一个Document对象之后,你可能想修改其中的某些属性值,并把它输出到前台页面或保存到其他地方,jsoup对此提供了一套非常简便的接口(支持链式写法)。
当以下方法针对Element对象操作时,只有一个元素会受到影响。当针对Elements对象进行操作时,可能会影响到多个元素。
.attr(String key, String value):设置标签的属性值。
.addClass(String className):增加类选择器选项
.removeClass(String className):删除对应的类选择器
Document doc = Jsoup.connect("http://csdn.com").get(); // 复数,Elements Elements elements = doc.getElementsByClass("text"); // 单数,Element Element element = elements.first(); // 复数对象,所有 class="text" 的元素都将受到影响 elements.attr("name","goods"); // 单数对象,只有一个元素会受到影响(链式写法) element.attr("name","shop") .addClass("red");
可以使用Element中的HTML设置方法具体如下:
.html(String value):这个方法将先清除元素中的HTML内容,然后用传入的HTML代替。
.prepend(String value):在元素前添加html内容。
.append(String value):在元素后添加html内容。
.wrap(String value):对元素包裹一个外部HTML内容,将元素置于新增的内容中间。
Document doc = Jsoup.connect("http://csdn.com").get(); Element div = doc.select("div").first(); div.html("<p>csdn</p>"); div.prepend("<p>a</p>"); div.append("<p>good</p>"); // 输出:<div"> <p>a</p> <p>csdn</p> <p>good</p> </div> Element span = doc.select("span").first(); span.wrap("<li><a href='...'></a></li>"); // 输出: <li><a href="..."> <span>csdn</span> </a></li>
对于传入的文本,如果含有像 <, > 等这样的字符,将以文本处理,而非HTML。
.text(String text) :清除元素内部的HTML内容,然后用提供的文本代替。
.prepend(String first):在元素后添加文本节点。
Element.append(String last):在元素前添加文本节点。
// <div></div> Element div = doc.select("div").first(); div.text(" one "); div.prepend(" two "); div.append(" three "); // 输出: <div> two one three </div>
问题描述:
你有一个包含相对URLs路径的HTML文档,现在需要将这些相对路径转换成绝对路径的URLs。
解决方式:
base URI路径。
abs: 属性前缀来取得包含base URI的绝对路径。代码如下:
Document doc = Jsoup.connect("http://www.open-open.com").get(); Element link = doc.select("a").first(); // 输出:/ String relHref = link.attr("href"); // 输出:http://www.open-open.com/ String absHref = link.attr("abs:href");
说明:
在HTML元素中,URLs经常写成相对于文档位置的相对路径,如:<a href="/download">...</a>。当你使用 .attr(String key) 方法来取得a元素的href属性时,它将直接返回在HTML源码中指定的值。
假如你需要取得一个绝对路径,需要在属性名前加 abs: 前缀,这样就可以返回包含根路径的URL地址attr("abs:href")。因此在解析HTML文档时,定义base URI非常重要。
如果你不想使用abs: 前缀,还有一个方法能够实现同样的功能 .absUrl(String key)。
–
问题描述:
在某些网站中经常会提供用户评论的功能,但是有些不怀好意的用户,会搞一些脚本到评论内容中,而这些脚本可能会破坏整个页面的行为,更严重的是获取一些机要信息,此时需要清理该HTML,以避免跨站脚本攻击(XSS)。
解决方式:
使用clean()方法清除恶意代码,但需要指定一个配置的 Safelist(旧版本中是Whitelist),通常使用Safelist.basic()即可。Safelist的工作原理是将输入的 HTML 内容单独隔离解析,然后遍历解析树,只允许已知的安全标签和属性输出。
String unsafe = "<p><a href='http://csdn.com/' οnclick='attack()'>Link</a></p>"; // 输出: <p><a href="http://csdn.com/" >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
说明:
Safelist不仅能够在服务器端对用户输入的HTML进行过滤,只输出一些安全的标签和属性,还可以限制用户可以输入的标签范围。
问题描述:
在某些网站中经常会提供用户评论的功能,但是有些不怀好意的用户,会搞一些脚本到评论内容中,而这些脚本可能会破坏整个页面的行为,更严重的是获取一些机要信息,此时需要清理该HTML,以避免跨站脚本攻击(XSS)。
解决方式:
使用clean()方法清除恶意代码,但需要指定一个配置的 Safelist(旧版本中是Whitelist),通常使用Safelist.basic()即可。Safelist的工作原理是将输入的 HTML 内容单独隔离解析,然后遍历解析树,只允许已知的安全标签和属性输出。
String unsafe = "<p><a href='http://csdn.com/' οnclick='attack()'>Link</a></p>"; // 输出: <p><a href="http://csdn.com/" >Link</a></p> String safe = Jsoup.clean(unsafe, Safelist.basic()); System.out.println(safe);
说明:
jsoup的Safelist不仅能够在服务器端对用户输入的HTML进行过滤,只输出一些安全的标签和属性,还可以限制用户可以输入的标签范围。
–
Connection.Response execute = Jsoup.connect("http://csdn.net/") .proxy("12.12.12.12", 1080) // 使用代理 .execute();
如果你读完觉得有收获,不妨点个赞~
分享此文一切功德,皆悉回向给文章原作者及众读者.
免责声明:蓝蓝设计尊重原作者,文章的版权归原作者。如涉及版权问题,请及时与我们取得联系,我们立即更正或删除。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务、UI设计公司、界面设计公司、UI设计服务公司、数据可视化设计公司、UI交互设计公司、高端网站设计公司、UI咨询、用户体验公司、软件界面设计公司
蓝蓝设计的小编 http://www.lanlanwork.com