JavaScript 运动框架 Step by step(转)

1,运动原理

Js运动,本质来说,就是让 web 上 DOM 元素动起来。而想要 DOM 动起来,改变其自身的位置属性,比如高宽,左边距,上边距,透明度等。动画的原理就是把不同状态的物体,串成连续的样子,就像一本书,画了几个小人,然后一翻书,就看见小人在动。js动画也一样。不同状态的DOM,用定时器控制,就能得到动画效果。

  1. window.onload = function(){
  2. var oBtn = document.getElementById('btn');
  3. oBtn.onclick = function(){
  4. var oDiv = document.getElementById('div1');
  5. //设置定时器
  6. setInterval(function(){
  7. //改变物体位置
  8. oDiv.style.left = oDiv.offsetLeft + 10 + 'px';
  9. },30)
  10. }
  11. }

上述代码,点击btn之后,就能是物体向左运动。可是会一直向右动,不会停止。因此需要创立一个停止的条件。在条件符合的情况下,清楚定时器。其中对于目标点的判断,尤为重要。

  1. window.onload = function(){
  2. var oBtn = document.getElementById('btn');
  3. oBtn.onclick = function(){
  4. var oDiv = document.getElementById('div1');
  5. //设置定时器
  6. var timer = setInterval(function(){
  7. //判断停止条件
  8. if(oDiv.offsetLeft > 300){
  9. clearInterval(timer);
  10. }else{
  11. //改变物体位置
  12. oDiv.style.left = oDiv.offsetLeft + 10 + 'px';
  13. document.title = oDiv.offsetLeft;
  14. }
  15. },30);
  16. }
  17. }

上述代码中,但物体的位置大于300的时候,将停止运动。但是上述代码还有个问题,就是连续点击按钮,物体会运动越来越快。因为每点击一次,就开了一个定时器,累加的定时器。造成运动混乱。

2,运动框架 (滑入滑出,淡入淡出)

为了解决上述问题,则必须在开启定时器之前,先清除定时器,因此需要一个全局变量 timer保存定时器。如下面代码。

  1. window.onload = function(){
  2. var oBtn = document.getElementById('btn');
  3. oBtn.onclick = function(){
  4. startMove();
  5. }
  6. }
  7. var timer = null;
  8. function startMove(){
  9. var oDiv = document.getElementById('div1');
  10. clearInterval(timer);
  11. //设置定时器
  12. timer = setInterval(function(){
  13. //判断停止条件
  14. if(oDiv.offsetLeft > 300){
  15. clearInterval(timer);
  16. }else{
  17. //改变物体位置
  18. oDiv.style.left = oDiv.offsetLeft + 10 + 'px';
  19. document.title = oDiv.offsetLeft;
  20. }
  21. },30);
  22. }

此外,在改变物体位置的时候,那个 “10”则是更改的数量,其实也就是速度。如果更改速度,运动的快慢就能确定。因此,运动框架的原理,基本步骤为。

  • 先清除定时器

  • 开启定时器,计算速度

  • 判断停止条件,执行运动

  1. var timer = null;
  2. function startMove(){
  3. var oDiv = document.getElementById('div1');
  4. clearInterval(timer);
  5. //计算速度
  6. var iSpeed = 10;
  7. //设置定时器
  8. timer = setInterval(function(){
  9. //判断停止条件
  10. if(oDiv.offsetLeft > 300){
  11. clearInterval(timer);
  12. }else{
  13. //改变物体位置
  14. oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px';
  15. document.title = oDiv.offsetLeft;
  16. }
  17. },30);
  18. }

对于停止条件,写死在里面了,所以需分离出参数。下面是一个分享到的例子。主要是根据目标判断速度的正负。从而在鼠标滑入画出时候进行运动/恢复的效果。

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. oDiv.onmouseover = function(){
  4. startMove(0);
  5. }
  6. oDiv.onmouseout = function(){
  7. startMove(-100);
  8. }
  9. }
  10. var timer = null;
  11. var iSpeed;
  12. function startMove(iTatget){
  13. var oDiv = document.getElementById('div1');
  14. clearInterval(timer);
  15. timer = setInterval(function(){
  16. //计算速度
  17. if(iTatget -oDiv.offsetLeft > 0){
  18. iSpeed = 10;
  19. }else{
  20. iSpeed = -10;
  21. }
  22. if(oDiv.offsetLeft == iTatget){
  23. clearInterval(timer);
  24. }else{
  25. oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px';
  26. }
  27. document.title = oDiv.offsetLeft;
  28. },30)
  29. }

另外一个小例子,淡入淡出,即改变物体的透明度,由于没有像原生的位置属性那样的offsetLset. 需要一个变量来保存透明度的值,用来和速度加减,最后付给元素的透明度样式。从而实现淡入淡出效果。

  1. window.onload = function(){
  2. var oImg = document.getElementById('img1');
  3. oImg.onmouseover = function(){
  4. startMove(100);
  5. }
  6. oImg.onmouseout = function(){
  7. startMove(30);
  8. }
  9. }
  10. var timer = null;
  11. //保存透明度的数字值
  12. var alpha = 30;
  13. function startMove(iTarget){
  14. var oDiv = document.getElementById('img1');
  15. clearInterval(timer);
  16. timer = setInterval(function(){
  17. var iSpeed = 0;
  18. if(alpha > iTarget){
  19. iSpeed = -1;
  20. }else{
  21. iSpeed = 1;
  22. }
  23. if(alpha == iTarget){
  24. clearInterval(timer);
  25. }else{
  26. //改变透明度速度值
  27. alpha += iSpeed;
  28. oDiv.style.filter = 'alpha(opacity:'+ alpha+')';
  29. oDiv.style.opacity = alpha/100;
  30. document.title = alpha;
  31. }
  32. },30)
  33. }

3,缓冲运动

缓冲运动原理就是,改变速度的值。每次累加的速度值变小,就是会是整个物体看起来越来越慢,以至于最后停掉。相当于改变使物体具有一个加速度。这个加速度,可以由物体当前位置和目标位置之间的距离得到,因为两者之间的距离一直在变小,所以速度也一直在变小。如下:

  1. window.onload = function(){
  2. var btn = document.getElementsByTagName('input')[0];
  3. btn.onclick = function(){
  4. startMove(300);
  5. }
  6. }
  7. var timer = null;
  8. function startMove(iTarget){
  9. var oDiv = document.getElementById('div1');
  10. clearInterval(timer);
  11. timer = setInterval(function(){
  12. //求出带有变化的速度
  13. var iSpeed = (iTarget - oDiv.offsetLeft) / 8;
  14. if(oDiv.offsetLeft == iTarget){
  15. clearInterval(timer);
  16. }else{
  17. oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px';
  18. }
  19. document.title = oDiv.offsetLeft + '...' + iSpeed;
  20. },30);
  21. }

上述方法可以得到缓冲运动,但是实际运行效果,物体并没有在 300的位置停掉,而是在 293的位置就停掉了。究其原因。因为当物体的速度小于1 的时候。物体位置为293。此时计算的速度是 0.875.通过 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 物体的位置为 293.875.可是计算机不能识别小数,将小数省略了。此时的 位置offsetLeft仍然是 293.再计算一次,还是同样的结果。定时器没有关掉,但是物体的位置却再也无法改变,故停留在了 293的位置。解决方案,就是将速度进行向上取整。但是,像上述运动,速度是正的,可是,当速度是负的时候,就同样会有相同的结果,因此需要在速度为负的时候,向下取整。

  1. function startMove(iTarget){
  2. var oDiv = document.getElementById('div1');
  3. clearInterval(timer);
  4. timer = setInterval(function(){
  5. var iSpeed = (iTarget - oDiv.offsetLeft) / 8;
  6. //对正的速度向上取整,负的速度向下取整
  7. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  8. if(oDiv.offsetLeft == iTarget){
  9. clearInterval(timer);
  10. }else{
  11. oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px';
  12. }
  13. document.title = oDiv.offsetLeft + '...' + iSpeed;
  14. },30);
  15. }

4.多物体运动

下一步,就是处理多物体运动,运动函数里面每次都要选取一个元素加事件。如果需要对多个物体进行同样的运动, 需要将运动对象作为参数传进来。

  1. window.onload = function(){
  2. var aDiv = document.getElementsByTagName('div');
  3. for(var i=0;i<aDiv.length;i++){
  4. aDiv[i].onmouseover = function(){
  5. startMove(this,300);
  6. }
  7. aDiv[i].onmouseout = function(){
  8. startMove(this,100);
  9. }
  10. }
  11. }
  12. var timer = null;
  13. function startMove(obj,iTarget){
  14. clearInterval(timer);
  15. timer = setInterval(function(){
  16. var iSpeed = (iTarget - obj.offsetWidth) / 8;
  17. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  18. if(obj.offsetWidth == iTarget){
  19. clearInterval(timer);
  20. }else{
  21. obj.style.width = obj.offsetWidth + iSpeed + 'px';
  22. }
  23. },30)
  24. }

通过循环物体,将物体的 this传给运动函数,使得多物体可以运动。但是这样有一个弊端,即当滑入第一个运动的时候,开启了定时器。如果此时,滑入另外一个物体,将会清理上一个定时器。这就造成了,上一次运动,很有可能还没完成结束,定时器就没关闭了。解决的方法,每个运动的物体,都能开了一个属于自己的定时器。因此,把定时器当成物体的属性。清理的时候也就是清理自己的定时器。

  1. window.onload = function(){
  2. var aDiv = document.getElementsByTagName('div');
  3. for(var i=0;i<aDiv.length;i++){
  4. aDiv[i].onmouseover = function(){
  5. startMove(this,300);
  6. }
  7. aDiv[i].onmouseout = function(){
  8. startMove(this,100);
  9. }
  10. }
  11. }
  12. function startMove(obj,iTarget){
  13. //将定时器,变成物体的属性,类似给物体添加索引
  14. clearInterval(obj.timer);
  15. obj.timer = setInterval(function(){
  16. var iSpeed = (iTarget - obj.offsetWidth) / 8;
  17. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  18. if(obj.offsetWidth == iTarget){
  19. clearInterval(obj.timer);
  20. }else{
  21. obj.style.width = obj.offsetWidth + iSpeed + 'px';
  22. }
  23. },30)
  24. }

多物体的淡入淡出的时候,也有类似的问题。因为修改透明度的时候,是先用一个变量保存透明度,必须针对每个物体设立透明度值属性。

  1. window.onload = function(){
  2. var aDiv = document.getElementsByTagName('div');
  3. for(var i=0;i<aDiv.length;i++){
  4. //将透明度值当初属性
  5. aDiv[i].alpha = 30;
  6. aDiv[i].onmouseover = function(){
  7. startMove(this,100);
  8. }
  9. aDiv[i].onmouseout = function(){
  10. startMove(this,30);
  11. }
  12. }
  13. }
  14. function startMove(obj,iTarget){
  15. clearInterval(obj.timer);
  16. obj.timer = setInterval(function(){
  17. var iSpeed = (iTarget - obj.alpha) / 8;
  18. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  19. if(obj.alpha == iTarget){
  20. clearInterval(obj.timer);
  21. }else{
  22. obj.alpha += iSpeed;
  23. obj.style.filter = 'alpha(opacity:'+obj.alpha+')';
  24. obj.style.opacity = obj.alpha / 100;
  25. }
  26. document.title = obj.alpha;
  27. },30);
  28. }

4.1  位置属性的bug

offsetWidth 或者 offsetHeight 等位置属性,一旦给他们加上 border。则会有诡异的现象出现。

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. setInterval(function(){
  4. oDiv.style.width = oDiv.offsetWidth - 1 + "px";
  5. },30)
  6. }

例如 oDiv.style.width = oDiv.offsetWidth - 1 + 'px'; 如果给 oDiv 的width 为一百,border 为 1.则这个物体的 width是100px;offsetWidth 为102px;带入公式之后,即减一之后。100 = 102 - 1 ,反而等于101;即 物体本来要减小,事实却增大了。解决的方案就是,加减的时候,必须使用物体的内联样式。但是 火狐 和 IE 又有兼容模式。解决方案如下:

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. setInterval(function(){
  4. oDiv.style.width = parseInt(getStyle(oDiv,'width')) - 1 + 'px';
  5. },30)
  6. }
  7. function getStyle(obj,attr){
  8. if(obj.currentStyle){
  9. return obj.currentStyle[attr];
  10. }else{
  11. return getComputedStyle(obj,false)[attr];
  12. }
  13. }

其中,getStyle函数,传入一个元素对象,和其 css 属性,获取的是元素的样式,即 witdh 100px;因此需要parseInt转换

5.任意值运动

通过 getStyle 函数,可以获取元素的样式,还可也通过 attr 制定需要修改的 css属性。这样就能是物体有不同的运动形式。

  1. window.onload = function(){
  2. var aDiv = document.getElementsByTagName('div');
  3. aDiv[0].onmouseover = function(){
  4. startMove(this,'width',300);
  5. }
  6. aDiv[0].onmouseout = function(){
  7. startMove(this,'width',100);
  8. }
  9. aDiv[1].onmouseover = function(){
  10. startMove(this,'height',100);
  11. }
  12. aDiv[1].onmouseout = function(){
  13. startMove(this,'height',50);
  14. }
  15. }
  16. function getStyle(obj,attr){
  17. if(obj.currentStyle){
  18. return obj.currentStyle(attr);
  19. }else{
  20. return getComputedStyle(obj,false)[attr];
  21. }
  22. }
  23. function startMove(obj,attr,iTarget){
  24. clearInterval(obj.timer);
  25. obj.timer = setInterval(function(){
  26. var iCur = parseInt(getStyle(obj,attr));
  27. var iSpeed = (iTarget - iCur) / 8;
  28. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  29. if(iCur == iTarget){
  30. clearInterval(obj.timer);
  31. }else{
  32. obj.style[attr] = iCur + iSpeed + 'px';
  33. }
  34. },30)
  35. }

5.1 任意值完美版

上述版本,还不能处理透明度的任意值,因此需要增加额外的兼容hack。

  1. window.onload = function(){
  2. var aDiv = document.getElementsByTagName('div');
  3. aDiv[0].onmouseover = function(){
  4. startMove(this,'opacity',100);
  5. }
  6. aDiv[0].onmouseout = function(){
  7. startMove(this,'opacity',30);
  8. }
  9. }
  10. function getStyle(obj,attr){
  11. if(obj.currentStyle){
  12. return obj.currentStyleattr[attr];
  13. }else{
  14. return getComputedStyle(obj, false)[attr];
  15. }
  16. }
  17. function getStyle(obj, attr){
  18. if(obj.currentStyle)    {
  19. return obj.currentStyle[attr];
  20. }else{
  21. return getComputedStyle(obj, false)[attr];
  22. }
  23. }
  24. function startMove(obj,attr,iTarget){
  25. clearInterval(obj.timer);
  26. obj.timer = setInterval(function(){
  27. var iCur = 0;
  28. if(attr == 'opacity'){
  29. iCur = parseInt(parseFloat(getStyle(obj, attr))*100);
  30. }else{
  31. iCur = parseInt(getStyle(obj,attr));
  32. }
  33. var iSpeed = (iTarget - iCur) / 8;
  34. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  35. if(iCur == iTarget){
  36. clearInterval(obj.timer);
  37. }else{
  38. if(attr=='opacity'){
  39. iCur += iSpeed
  40. obj.style.filter='alpha(opacity:' + iCur + ')';
  41. obj.style.opacity=iCur / 100;
  42. }
  43. else{
  44. obj.style[attr]=iCur+iSpeed+'px';
  45. }
  46. document.title = obj.style[attr];
  47. }
  48. },30)
  49. }

6.链式运动

我们的运动框架到目前为止,基本功能都能实现了。现在拓展。所谓链式运动,即运动接着运动。当运动停止的时候,如果回调一个函数。回调一个运动函数,就能出现这样的效果。因此传入一个函数作为回调函数。

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. oDiv.onclick = function(){
  4. startMove(this,'width',300,function(){
  5. startMove(oDiv,'height',300,function(){
  6. startMove(oDiv,'opacity',100)
  7. })
  8. })
  9. }
  10. }
  11. function getStyle(obj,attr){
  12. if(obj.currentStyle){
  13. return obj.currentStyleattr[attr];
  14. }else{
  15. return getComputedStyle(obj, false)[attr];
  16. }
  17. }
  18. function startMove(obj,attr,iTarget,fn){
  19. clearInterval(obj.timer);
  20. obj.timer = setInterval(function(){
  21. var iCur = 0;
  22. if(attr == 'opacity'){
  23. iCur = parseInt(parseFloat(getStyle(obj, attr))*100);
  24. }else{
  25. iCur = parseInt(getStyle(obj,attr));
  26. }
  27. var iSpeed = (iTarget - iCur) / 8;
  28. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  29. if(iCur == iTarget){
  30. clearInterval(obj.timer);
  31. //回调函数
  32. if(fn) fn();
  33. }else{
  34. if(attr=='opacity'){
  35. iCur += iSpeed
  36. obj.style.filter='alpha(opacity:' + iCur + ')';
  37. obj.style.opacity=iCur / 100;
  38. }
  39. else{
  40. obj.style[attr]=iCur+iSpeed+'px';
  41. }
  42. document.title = obj.style[attr];
  43. }
  44. },30)
  45. }

7.同时运动

目前为止,我们的运动框架还有个小缺点,就是不能同时该两个属性进行运动,比如同时更改宽和高。这个要求传入的属性是不同的几个值。则考虑传入一个 json用来保存需要更改的属性。

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. oDiv.onclick = function(){
  4. startMove(this,{'width':300,'height':400});
  5. }
  6. }
  7. function getStyle(obj, attr){
  8. if(obj.currentStyle)    {
  9. return obj.currentStyle[attr];
  10. }else{
  11. return getComputedStyle(obj, false)[attr];
  12. }
  13. }
  14. function startMove(obj,json,fn){
  15. clearInterval(obj.timer);
  16. obj.timer = setInterval(function(){
  17. // 循环json
  18. for(var attr in json){
  19. var iCur = 0;
  20. if(attr == 'opacity'){
  21. iCur = parseInt(parseFloat(getStyle(obj, attr))*100);
  22. }else{
  23. iCur = parseInt(getStyle(obj,attr));
  24. }
  25. var iSpeed = (json[attr] - iCur) / 8;
  26. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  27. if(iCur == json[attr]){
  28. clearInterval(obj.timer);
  29. if(fn) fn();
  30. }else{
  31. if(attr=='opacity'){
  32. iCur += iSpeed
  33. obj.style.filter='alpha(opacity:' + iCur + ')';
  34. obj.style.opacity=iCur / 100;
  35. }
  36. else{
  37. obj.style[attr]=iCur+iSpeed+'px';
  38. }
  39. document.title = obj.style[attr];
  40. }
  41. }
  42. },30)

上述代码,可以解决了同时运动的问题。但是还是有一个bug。比如,同时运动的某个属性,如果变化很小,马上就停止了,即关掉了定时器。那么会造成其他属性的变化也停止。因为这些属性都共用了一个定时器。因此需要判断,假设有三个人要来,然后一起去爬山。三个人有的先来,有的后来,只要三个人都到齐了,才出发。也就是只有三个属性都到了目标值,才关定时器。一开始,设立一个检查量,为真。假设所有人都到了,然后循环,只有有一个人没有到,检查就为假。直到所有的都到了,检测为真。则停止定时器。

  1. window.onload = function(){
  2. var oDiv = document.getElementById('div1');
  3. oDiv.onclick = function(){
  4. startMove(this,{'width':102,'height':400,'opacity':100});
  5. }
  6. }
  7. function getStyle(obj, attr){
  8. if(obj.currentStyle)    {
  9. return obj.currentStyle[attr];
  10. }else{
  11. return getComputedStyle(obj, false)[attr];
  12. }
  13. }
  14. function startMove(obj,json,fn){
  15. clearInterval(obj.timer);
  16. obj.timer = setInterval(function(){
  17. var bStop = true;
  18. for(var attr in json){
  19. //取当前值
  20. var iCur = 0;
  21. if(attr == 'opacity'){
  22. iCur = parseInt(parseFloat(getStyle(obj, attr))*100);
  23. }else{
  24. iCur = parseInt(getStyle(obj,attr));
  25. }
  26. //计算速度
  27. var iSpeed = (json[attr] - iCur) / 8;
  28. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  29. //检测停止
  30. if(iCur != json[attr]){
  31. bStop = false;
  32. }
  33. if(attr=='opacity'){
  34. iCur += iSpeed
  35. obj.style.filter='alpha(opacity:' + iCur + ')';
  36. obj.style.opacity=iCur / 100;
  37. }
  38. else{
  39. obj.style[attr]=iCur+iSpeed+'px';
  40. }
  41. }
  42. if(bStop){
  43. clearInterval(obj.timer);
  44. if(fn) fn();
  45. }
  46. },30)
  47. }

再循环外定义一个 标志变量 bStop = true。用来表示所有属性到达目标值。等循环结束了,如果这个值是真的,则停止定时器。因为,每次运行定时器,都会初始化这个值。循环的过程中,只要有一个没有到,bStop就被设定为 false。如果某个到了,此时 iCur != json[attr],表示速度为0 后面执行的结果,也不会有变化。只有所有的都达到目标值。循环则不再改变 bStop的值。此时,只要下一次运行定时器。就是初始化 bStop为真。而循环因为都到了,所以速度为0 也就再也没有变化。循环结束,sBstop还是真,表示所有都到了。因此此时结束定时器。

最后附上完美运动框架,封装成 move.js 就可以调用了。

  1. /**
  2. * @author rsj217
  3. * getStyle 获取样式
  4. * startMove 运动主程序
  5. */
  6. function getStyle(obj, attr){
  7. if(obj.currentStyle)    {
  8. return obj.currentStyle[attr];
  9. }else{
  10. return getComputedStyle(obj, false)[attr];
  11. }
  12. }
  13. function Move(obj,json,fn){
  14. //停止上一次定时器
  15. clearInterval(obj.timer);
  16. //保存每一个物体运动的定时器
  17. obj.timer = setInterval(function(){
  18. //判断同时运动标志
  19. var bStop = true;
  20. for(var attr in json){
  21. //取当前值
  22. var iCur = 0;
  23. if(attr == 'opacity'){
  24. iCur = parseInt(parseFloat(getStyle(obj, attr))*100);
  25. }else{
  26. iCur = parseInt(getStyle(obj,attr));
  27. }
  28. //计算速度
  29. var iSpeed = (json[attr] - iCur) / 8;
  30. iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
  31. //检测同时到达标志
  32. if(iCur != json[attr]){
  33. bStop = false;
  34. }
  35. //更改属性,获取动画效果
  36. if(attr=='opacity'){
  37. iCur += iSpeed
  38. obj.style.filter='alpha(opacity:' + iCur + ')';
  39. obj.style.opacity=iCur / 100;
  40. }
  41. else{
  42. obj.style[attr]=iCur+iSpeed+'px';
  43. }
  44. }
  45. //检测停止
  46. if(bStop){
  47. clearInterval(obj.timer);
  48. if(fn) fn();
  49. }
  50. },30)
  51. }
上一篇:玩转spring boot——结合JPA事务


下一篇:玩转spring boot——结合JPA入门