顾问:JJ Foley
项目期限: 2019年1月〜2019年5月
- 尼尔自动机-战斗是故意乏味的吗? (剧透)
- IVP超增长播客:首席执行官Ilkka Paananen的6则关于超增长过程中扩展Supercell的经验教训
- 一动不动的旅程(Shemue I&II)
- 银朱手表:Moorgate Accord珍藏版决赛
- 战斗学院2:东方阵线APK Android
Github存储库到我的项目
更新:应用程序已部署 在这里 !
欢迎来到我的高级项目! 我叫Faith,我是史密斯学院的一名高级计算机科学学生。 我还是游戏实验室的实验室助理,该实验室是一个开放空间,学生可以在那里玩电子游戏。 目前,我们的库存中有70多种游戏,这是实验室助理的工作,它可以帮助学生找到他们想要玩的游戏。
尽管空间如此凉爽,但仍然存在一个迫在眉睫的问题:我们目前没有用户友好和可访问的游戏清单,供学生在游戏实验室之外浏览。 因此,我的专项研究将专注于开发搜索引擎,让学生可以查找游戏实验室清单中可用的游戏。

此搜索引擎的目的
学生们往往会问我:“你有这个XYZ游戏吗?”
目前,我们使用Google文档来跟踪所有可用的游戏,但是学生往往对游戏的描述含糊不清,而忘记了确切的标题。 我最后不得不说:“对不起,似乎我们现在还没有在实验室中这款游戏。”但是两个小时之后,我意识到我们确实有该款游戏。
有时是因为我们的大多数访问者都是初学者,他们仍然在学习各种游戏类型。 他们很难在实验室中浏览哪些游戏,而我们当前的Google文档除了游戏名称外没有做过多的说明。 这些学生在寻找游戏时需要花费10-15分钟的时间,最终他们感到害怕在自己不喜欢的地方玩游戏。 希望通过搜索引擎,用户可以更轻松地搜索游戏,并希望学生可以在自己的舒适区域之外探索游戏。
第一周:设计功能
我将开发一个Web应用程序,其中将React用于前端,将SpringBoot用于后端,并将H2DB用于我们的数据库连接。

该网络应用将实现的主要功能是:
- 搜索栏 :用户可以按标题,流派和可玩游戏机搜索游戏
- 游戏表 :此表将显示所有可用的游戏,包括有关标题,类型和可玩游戏机的信息; 每行是一个游戏条目,并且每个条目都可以单击以获取游戏的简要说明
- 游戏结帐系统:授权用户可以使用此Web应用程序结帐游戏(授权用户是出于学术目的使用游戏的教授以及游戏实验室的工作人员); 这些游戏被签出时,将为时间戳设置一个单独的数据库表
- 添加/编辑/删除游戏条目: 实验助手可以修改游戏条目
作为后端开发人员,我不知道设计网页布局将有多大挑战。 我还意识到,在进入项目的技术部分之前,我们需要细微的细节(例如,如果用户输入错误的密码,输入无效的字符串等)。
该Web应用程序在美学上可能不会令人满意,但功能更多。 一旦我们开发了Web应用程序的核心,然后我们将继续使其变得漂亮!
我们还将实施一个安全系统,以检查谁是授权用户,谁不是。 进入第2周,我将建立与SpringBoot的H2DB连接,并尝试创建游戏数据库。
第2–3周:从后端开始
如第1周所述,我将对后端使用SpringBoot,对数据库连接使用H2DB。
使我的数据库连接正常工作花费了令人尴尬的时间(仅仅是因为我在Entity类中使用了不正确的SpringBoot批注),但是当我在屏幕上看到以下内容时,我感到非常兴奋:

创建表的基本结构以及如何查询特定数据的方式如下:
- 设置Entity类:为了创建具有指定字段(又称表的列)的表,我使用Entity批注让SpringBoot为我创建表。
- 使用CrudRepository为每个Entity类设置DAO :我将使用DAO类(我称它们为 Repository)进行基本查询(按ID查找行,查找所有条目,保存等),然后我也将在DAO类中创建特定的SQL查询
- 为每个DAO类设置一个DataService:为查询特定数据,我将创建一个称为“数据服务”类的中介类,在该类中它将对DAO存储库进行函数调用以返回指定的数据。 数据服务类将由控制器使用
- 设置控制器: Controller类将用于实际接收来自Web的请求+将请求的数据返回到Web。 为了查询特定数据,可以在控制器类中访问数据服务类。
当前,我有一个Game实体类(Spring将在数据库中创建一个游戏表),GameRepository,GameService和GameController。 我正在辩论是否应该将GameController设置为主控制器类,或者是否应该划分控制器类的功能。 例如,也许GameController类将仅负责查询数据并将其返回(GetMapping批注)给用户。 也许我会创建一个不同的控制器类来处理数据库中数据的添加和删除(我很好奇这是否有助于组织如何为添加/删除功能设置安全性。只有授权用户才能访问这些功能)。 我将更多地使用Controller类,以了解我的顾问会做出什么样的决定。
这周,我主要集中于试验我能够轻松进行的查询以及难以查询的查询。 例如,我最初计划为Game对象创建字段,以具有流派,控制台类型,等级和可玩模式的列表(它们全部被编写为枚举)。

这些枚举都是以类似的方式编写的(它们都实现相似的字段和方法,并且如果它们是类,我将拥有这些枚举来从父类继承方法)。
使用枚举的棘手部分是我希望Game对象具有类型列表和可玩模式列表。 事实证明,存储枚举列表并从数据库中查询是困难的。 我需要为每个枚举创建一个AttributeConverter,最终导致重复和混乱的代码。
我与我的顾问讨论了这个问题,他建议不要在Java级别创建这些值,而应该在数据库级别创建它们。 原始的枚举类型将存储为字符串,并且我们将有一个专用于每种枚举类型的表。 例如,我们将创建一个控制台表,该表具有一个ID和定义特定控制台类型(即“ PS4”,“ PS3”等)的相应字符串。
其次,我们将创建另一个具有游戏ID和控制台ID的表(均为与各自表相对应的外键)。 这些ID将用于查询游戏所属的游戏机。 这样,我可以消除游戏表中的console列,并对我目前拥有的所有其他枚举值采用相同的结构。
下周,我将致力于创建这些表,并尝试创建脚本以在SpringBoot中导入和导出csv文件。
第4-6周:整理后端+单元测试
第三周快要结束时,我致力于整合后端功能和查询。 现在有添加和保存游戏,控制台,类型和可玩模式对象的功能。 每个游戏还具有游戏+控制台,游戏+类型,游戏+可玩模式之间的映射,所有这些都由外键游戏的唯一ID连接。
我为后端创建了两个rest控制器,一个简单地称为GameController(以及我正在使用的原始控制器),另一个则称为AdminController。 GameController现在仅处理查询游戏对象,游戏机,类型等。另一方面,AdminController处理“管理任务”,例如将对象添加到数据库,删除对象和修改对象。
尝试创建一个集中式查询以根据不同的参数(例如标题关键字,体裁,可玩模式等)查找游戏仍是一项工作。 我要使其成为可选参数,以便在用户不输入有关一个字段的信息的情况下,我希望我的SQL查询忽略对该字段的搜索,并在其他非空字段中查找关键字。 当前很难找到可行的查询,尤其是因为我要将大约六个不同的表连接到游戏表,然后查询游戏对象。 我在AdminController的“编辑游戏”功能上也遇到了类似的困难。 我将与顾问联系以解决此问题。
为了将真实数据放入我们的数据库中,我的顾问编写了一个很棒的Python脚本,该脚本将使用csv文件添加数据(比手动插入80个游戏更容易)。 我遇到的问题之一是游戏对象中的description字段。 SQL抱怨说,描述超出了允许的字符数,无论我对每个游戏的描述减少了多少。 我将与我的顾问联系以解决此问题。
我已经开始对数据服务进行单元测试,这些数据服务直接与我的DAO类联系。 通常可以理解,在大量处理DAO类的类文件上进行单元测试不是一种好习惯,但是就我而言,我想确保正在进行特定的函数调用。 因此,我的顾问建议我使用Mockito模拟我的DAO类函数调用的行为。 最终,我还将对控制器进行单元测试。 单元测试绝对是一项耗时且繁琐的任务。 我花了一个小时对每个数据服务进行单元测试。
在大多数情况下,后端几乎完成了一些错误修复,需要进行一些修复。 我希望我能解决在此博客条目中提到的问题,以便我们可以过渡到在项目的前端工作。
第七周:创建主游戏桌
在过渡到项目的前端组件之前,建议我创建一个主数据库表,其中包含游戏的所有相关字段,例如类型,控制台和可玩模式,这些字段通过游戏ID映射到特定游戏。 该表将被称为“主游戏”表。 主游戏桌的目的是要有一个集中的单元,我们可以在其中通过各种字段查询游戏。 如果我们只是要从常规游戏桌中查询,它将无法在一个集中式单元中检索有关类型,可玩模式和游戏机的信息。 Master Game表可以帮助您解决这一问题。
在表的设计中,我有两个选择:
- 选项1:创建一个全部由游戏ID +映射ID映射的从属表
- 选项2:不要通过映射ID来映射它,而是将所有映射都转换为字符串以存储在数据库中(即,不是逐行存储流派→将所有流派连接在一起以映射到一个游戏行)
选项1:我可以按游戏ID映射所有列,并以这种方式引入所有相关映射。 该表将取决于游戏表和映射表。 每行将是游戏ID,也将映射到控制台映射,模式映射和类型映射ID。 因为每个可玩模式和类型都是单独映射的(而不是每个游戏行都有模式/类型的列表),所以主游戏表将具有同一游戏的多行,但具有唯一的模式和类型映射。 这导致数据具有重复性信息,并使组织和显示前端数据变得更加困难,这给React组织信息带来了更多负担。
优点:使用此设计更易于访问,删除和添加新映射(从后端的角度来看)
缺点:增加前端整理游戏数据的负担,导致重复信息
选项2:让后端手柄组织主游戏桌,而不是前端。 我将创建一个AttributeConverter,以便例如通过游戏ID查询所有类型,并将它们连接在一起成为一个字符串,以便主游戏表中的一个游戏行将很好地显示这些字符串。 这样,当React通过特定的游戏ID查询类型时,它不需要做额外的工作来组织游戏数据。 这种设计的问题在于,它会使创建的其他表(游戏,地图,控制台表等)变得无用。 当我们可以轻松地创建具有所有这些字段的游戏表时,为什么还要创建其他表?
优点:在数据库方面更有条理,重复信息较少,数据更干净,仍然易于访问信息
缺点:难以按特定字段删除和添加游戏,在删除映射依赖项时无法实现创建游戏表+映射表的目的,并且后端在组织数据/信息方面承担更多负担
最后,我最终选择了选项1,并要求前端在组织数据方面做更多的工作。
第8-12周:使用React创建前端
博客文章这一部分的更新时间很长,但是我希望我能记住与前端的大部分挣扎。
我在React方面的经验非常有限。 我第一次被介绍给React是在我去年夏天的实习期间(2018),当时我们不得不将其用于我们的黑客马拉松项目。 我上一次与React合作是当我构建一个古怪的Grocery Calculator来获取该框架的句柄时,两年来我一直没有在JavaScript上投入过多的工作。 这个项目的目标也是学习一种新技术,在这种情况下是React。
在开发过程中,我有两台服务器分别为后端和前端服务(分别连接到:8080和:3000)。 我首先通过为搜索引擎创建组件来开始构建该项目。 我有一个称为SearchEngine的父组件,而我有一个诸如控制台,可玩模式,签出等字段的子组件。这些字段将用于根据选定的条件过滤特定的游戏。
前端部分的最大挑战是组织主游戏数据并进行渲染,这是我在选项1中所解释的。因为我有太多重复的数据,所以我不得不创建一种方法,将所有数据按游戏ID,并且仅呈现数据中的唯一元素。 此过程需要进行大量测试,因为并非所有元素都能正确对齐并显示所有流派和可播放模式。
我还必须改变创建Spring的方法,以便按照给定的标准查找游戏。 最初,我设想将这些字段作为RequestParams引入,然后将这些字段引入游戏服务。 但是,我意识到的是,只有当我知道所有字段都将被输入时(这意味着用户选择为每个字段输入信息,例如控制台,可播放模式,等级等),此方法才可行,理想情况下知道固定要考虑的参数数量。 但是,由于我允许用户输入任意数量的标准,因此我需要使参数的数量为可选/灵活。 RequestParam确实提供了required = false参数,但是我在React端的fetch方法的链接中进行了硬编码,因此我需要前端灵活处理需要处理的不同类型的参数。
我对此的解决方案是创建一个将包含JSON正文并具有所有条件字段的帖子映射。 我没有在Spring端将所有内容映射到RequestParams,而是使用@RequestBody并接受了一个将输入所有字段的对象。 这个对象称为Query,我创建的一个类具有诸如key(用于查找与标题,描述和流派匹配的关键字),游戏,控制台,流派和可播放模式等字段。 这些字段可以保留为空(null)或填充,并且我们的SQL查询将在查询特定游戏对象时考虑到这一点。
总体而言,在对Web应用程序的SearchEngine组件进行优先级排序的过程中,我将所有内容呈现在一个页面上,而不是将它们路由到其他页面。 这是网站早期的样子:




我在React中创建的类和组件越多,我就越了解组件的工作方式。 最初,我以为一个组件对应一个网页,但实际上我意识到组件谈论的是网站中最小的功能单元。 例如,搜索栏可以是其自己的组件,具有自己的编程行为(即,如果我们单击搜索栏上的“提交”按钮会发生什么)。 我还了解到JS文件没有与每个JS文件相对应的单独的.html文件。 我们有一个index.html文件,可根据事件触发器呈现不同的JS文件。
反应路由
最终,我们逐步使用React Router将每个功能映射到不同的页面。 我们路由了以下页面:
- 主页: Web应用程序的启动,欢迎人们访问该网站。
- 搜索 :搜索页面将具有用户可用于过滤特定游戏的不同字段/条件。
- 关于 :向Smith社区成员介绍什么是游戏实验室及其在Smith校园中的运作方式
- 管理员 :管理员页面,其中也有多个页面路由到搜索页面,但可以选择编辑游戏,编辑游戏页面,添加新游戏页面以及编辑类型,游戏机和可玩模式的页面
- 登录 :如果尝试访问管理员功能,则用户必须通过登录页面登录
因为在我实现React Router的时候,我不知道我们有一个index.html文件可以在不同的触发器上渲染不同的JS文件,所以我对于为什么需要路由器感到非常困惑。 我意识到React系统要复杂得多(或愚蠢),并且如果没有解决方法(即使用React Router),就无法自然地映射到其他路径。 感觉就像我在伪造这些映射,但嘿,至少它是事后看来。
安全
开发过程中另一个棘手的部分是为受管理员保护的功能配置安全性。 我希望用户每次尝试访问“管理”选项卡时都触发Spring的安全性。
我映射了后端的“ / admin / **”以明确要求登录,并允许映射“ / game / **”访问Web应用程序的所有其他部分。 首先,后端的映射工作正常; 但是,我不知道如何将其集成到前端中。 我的顾问在我的React Admin组件上创建了一个名为adminFetch()的方法,以便每次调用该方法时,它将从“ / admin / **”获取一些映射并触发登录。 我创建了一个类似的方法,但用于映射所需的JSON主体。
具有安全功能的最终障碍是由于CSRF。 最初,我不想禁用CSRF来防御此类攻击。 但是,由于我们处于时间紧缩状态,因此我们决定暂时将其禁用。
在最终版本中,当用户尝试访问“管理”选项卡时,弹出窗口将显示一个登录屏幕,并且在提交登录信息后,React还将呈现其自己的登录页面,并要求用户再次输入相同的密码。 这实际上是Web应用程序的意外行为,但是我觉得花时间修复它不够重要。
开发/本地环境中的最终产品
正确解析所有主游戏数据,配置安全性,并将React将不同的组件路由到不同的页面,这是Web应用程序的最终美学效果:







将React项目集成到Spring的服务器是我们必须攀登的另一座大山,但是总的来说,我们使用此博客文章来完成它! 我仍然不太了解React如何成功地集成到Spring应用程序中,但是我想称其为“ Maven”和“文件路径”魔术。 经过如此激烈的谷歌搜索之后,这似乎是奇迹。
当mvn打包整个构建时,某些映射不可用。 在index.html中,所有构建都将映射到/ static / **,网络无法在其中找到这样的路径。 一旦删除了“ / static / **”并保持所有路径相同,React就可以成功集成到Spring的服务器中(这很糟糕,每次创建新版本时,我都必须对其进行重新编辑)。
第12-13周:部署Web应用程序
这是整个项目中最艰苦的过程。 我没想到部署Web应用程序会如此困难。 我使用Heroku作为部署平台(这是在学期开始时由我的顾问决定的)。
我面临的第一个问题是我不知道部署过程如何工作。 我按照Heroku开发中心页面上的说明进行了操作,但是我不确定Heroku在部署我的应用程序时需要什么。 因此,Heroku在我尝试的前20次部署失败,我意识到我遇到了R10,H10和H20错误,这表明我的应用程序崩溃非常严重,并且在需要的75秒内没有启动。
我一直在通过我的github仓库部署到Heroku分支。 我认为将所有内容包含在jar文件中并让Heroku在部署时启动它会更容易,所以我的主要重点是将jar文件推送到Heroku分支上。 我创建了一个Procfile,其中包含启动jar文件的命令。
不幸的是,没有任何数据传输到远程服务器。 我了解到H2DB并不用于生产环境,并且所有在H2DB上提供的数据实际上都存储在我的本地本地驱动器上,这就是为什么没有任何数据传输到远程服务器的原因。 我还了解到,Heroku实际上并不明确支持将H2DB作为数据库使用,而应该使用PostgreSQL。
但是,我相信仍然可以使用H2,因为H2的服务器应该在启动Spring的服务器时启动,所以我尝试创建与H2的TCP连接,甚至尝试创建一个静态脚本来创建表和插入数据在应用启动时进入他们。 我意识到这不是一个明智的方法,但是我绝望地尝试了一切。 最终,我放弃了H2DB,转而永久使用PostgreSQL(特别是因为在使用H2DB进行构建时,实际上所有我的依赖项和所需的构建都在mvn软件包中被擦除并不得不回滚)。
我反对使用PostgreSQL仅仅是因为我的查询变得非常慢(特别是在尝试加载我的所有游戏数据时)。 切换到PostgreSQL时,我的JPA查询都不起作用。 但是,我发现PostgreSQL这么慢的原因是因为我的主游戏数据中的description字段是LOB。 我将描述从String / LOB转换为bytea,并创建了一个字节数组,而不是在Spring中将其存储为String。 这大大提高了查询性能。 使用字节数组而不是字符串的缺点是我无法再通过关键字查询描述列; 因此,搜索栏现在仅搜索标题和类型,而不再搜索描述。 这是一个令人失望的缺点,但比等待23秒让游戏桌显示要好得多,而这比现在需要3–6秒要好(是的,我确实为它计时)。
在搜索游戏时,我还创建了多个JPA查询,而不是一个庞大的查询,因为针对PostgreSQL的JPA查询不再接受null /可选参数(将根据从网络中首先发送的输入内容读取参数,而不是根据顺序输入参数在函数内部列出的参数)。
一旦我弄清楚了数据库并创建了一个HomeController,该控制器将React Router的网络路径映射到index.html文件,一切就顺利进行了。
结论
总的来说,我为能够从事Web应用程序的全栈开发并从该项目中学到许多新技能而感到自豪。 我现在知道如何熟练地使用Spring和React(是的)! 这个项目最难的部分是缺乏关于使用React + Spring Boot的社区/论坛(大多数开发人员选择使用NodeJS Express作为后端(如果使用React的话)),所以下一次我想我真的很想JavaScript后端框架让我手忙脚乱。
我了解到,创建和设计前端部分根本不容易(创建这些按钮并使它们漂亮却很痛苦)! 事后看来,写这篇博客时,我的一切看起来都那么顺利和轻松,但是我总是觉得自己在摸索,或者在不确定的黑暗中徘徊。 我只是不知道我99%的时间在做什么,并花了所有时间在Google搜索中寻找答案。
我确实违反了许多安全性,软件开发规则/规范/流程(我的意思是,当您启动该应用程序时,它已经告诉您它不安全),但是该项目的目的仅仅是“使其正常工作”,例如,只是玩弄工具并做点什么。 在整个过程中学习和犯错真是太有趣了,当我尝试部署该应用程序时,我感到非常绝望,就像我根本就不会完成一样,但是我真的努力了,向上帝祈祷,并且不懈地向我的顾问寻求帮助(上帝保佑您,JJ)。 这是我第一次使用完整堆栈,从头到尾创建一个完全充实的(尽管很混乱)Web应用程序。 遗憾的是,我无法创建签入/签出系统,并且确实想向游戏桌中的每个游戏添加视觉效果/图像; 但是,这将是一个不同学期的项目。
该Web应用程序绝对不是完美的,也不是最用户友好的网站。 但是我必须通过从事这个项目来学习更多关于什么是用户友好的Web应用程序的知识。 能够有机会参与一个高级项目,我感到非常高兴和感激。毕业后进入高科技行业,我将为此感到自豪。