前后端交互

Vue和Vue CLI

Vue CLI = Vue.js + 一堆插件

创建项目
  • 全局安装Vue CLI:npm install -g @vue/cli

    • 查看Vue CLI版本:vue --versionvue -V
    • 安装Vue CLI时,最好添加版本号,如npm install -g @vue/cli@4.5.13
  • 创建一个项目:vue create web

    • Manually select features,按空格选中
      • TypeScript
      • Router
      • Vuex
      • Linter/Formatter
    • Choose a version of Vue,3.x
    • Use class-style component syntax? N
    • Use Babel alongside TypeScript? n
    • Use history mode for router? Y
      • history:xxx/user
      • hash:xxx/#/user
    • Pick a linter/formatter config
      • ESLint with error prevention only
    • Pick additional lint feature
      • Lint on save
    • Where do you prefer palcing config for Babel,ESLint,etc.?
      • In dedicated config files
    • Save this as a preset for future projects?N
  • 启动项目

    • cd web
    • npm run serve
  • 浏览器访问:http://localhost:8080/
    在这里插入图片描述

项目结构
  • main.ts|App.vue|index.html
    Vue CLI 初始执行main.ts,将内容页App.vue渲染到index.html,完成页面显示。
  • package.json
    完整的版本号定义:<主版本号><次版本号><修订版本号>
    • "vue": "^3.0.0" 表示会自动安装3.0.0及以上版本,注意哈,只是改变修订版本号,不会改变主版本号
      • 比如,3.0.1发布了,下载该源码后,npm install时会下载3.0.1,而不是3.0.0
      • 比如,4.0.0发布了,下载该源码后,npm install时会下载最新的3.x.x,但不会下载4.0.0
    • "typescript": "~4.1.5",表示会自动安装4.1.x的最新版本,注意哈,只改变修订版本号,不会改变次版本号
      • 比如,4.1.6发布了,npm install时会下载4.1.6
      • 比如,4.2.0发布了,npm install时会下载最新的4.1.x,但不会下载4.2.0
  • package-lock.json
    package.json中指定最新版本的主版本号,package-lock.json则用来确定修订版本号。
    比如,package.json中指定"@vue/cli-service": "~4.5.0"package-lock.json中则确定了修订版本号:"version": "4.5.13"
集成ant-design-vue
安装ant-design-vue
  • npm install ant-design-vue --save
    下载的是ant-design-vue@1.7.5,但ant-design-vue@1.x不支持Vue3。
  • npm install ant-design-vue@next --save
    表示安装ant-design-vue最新的未发布的版本,此时下载的是ant-design-vue@2.1.4
使用ant-design-vue
  • main.ts中引入antdv
    在这里插入图片描述
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

createApp(App).use(store).use(router).use(Antd).mount('#app')
  • Home.vue中使用Button组件
    在这里插入图片描述
<template>
  <div class="home">
    <a-button type="danger">Danger</a-button>
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

export default defineComponent({
  name: 'Home',
  components: {
    HelloWorld,
  },
});
</script>
改造项目

页面布局是上、中、下,即header、content、footer。
所以,content的内容是随路由动态变化的,因此进行如下改造:
1、App.vue中使用<router-view/>代替content
2、具体的content(content里进行了左、右布局,左是<a-layout-sider>,右是<a-layout-content>)放在Home.vue

  • 修改App.vue
<template>
  <a-layout>
    <a-layout-header class="header">
      <div class="logo" />
      <a-menu
          theme="dark"
          mode="horizontal"
          v-model:selectedKeys="selectedKeys1"
          :style="{ lineHeight: '64px' }"
      >
        <a-menu-item key="1">nav 1</a-menu-item>
        <a-menu-item key="2">nav 2</a-menu-item>
        <a-menu-item key="3">nav 3</a-menu-item>
      </a-menu>
    </a-layout-header>
    <router-view/>
    <a-layout-footer style="text-align: center">
      Test for 电子书
    </a-layout-footer>
  </a-layout>
</template>

<style>
.logo {
  float: left;
  width: 120px;
  height: 31px;
  margin: 16px 24px 16px 0;
  background: rgba(255, 255, 255, 0.3);
}
</style>
  • 修改Home.vue
<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      Content
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src

export default defineComponent({
  name: 'Home',
  components: {
    HelloWorld,
  },
});
</script>
  • npm run serve
    执行npm run serve后报错:The “HelloWorld” component has been registered but not used vue/no-unused-components,这是ESLint进行代码检查的结果。
    对此,有两种解决方法:
    • 第一种:删除组件HelloWorld
    • 第二种:在.eslintrc.jsrules中添加'vue/no-unused-components':'off',即关闭对该项的检查
自定义组件
  • the-header.vue
<template>
  <a-layout-header class="header">
    <div class="logo" />
    <a-menu
        theme="dark"
        mode="horizontal"
        v-model:selectedKeys="selectedKeys1"
        :style="{ lineHeight: '64px' }"
    >
      <a-menu-item key="1">nav 1</a-menu-item>
      <a-menu-item key="2">nav 2</a-menu-item>
      <a-menu-item key="3">nav 3</a-menu-item>
    </a-menu>
  </a-layout-header>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'the-header',
});
</script>

<style>
  .logo {
    float: left;
    width: 120px;
    height: 31px;
    margin: 16px 24px 16px 0;
    background: rgba(255, 255, 255, 0.3);
  }
</style>
  • the-footer.vue
<template>
  <a-layout-footer style="text-align: center">
    Test for 电子书
  </a-layout-footer>
</template>

<script>
import {defineComponent} from "vue";
export default defineComponent({
  name:"the-footer"
})
</script>
  • App.vue
<template>
  <a-layout>
    <the-header/>
    <router-view/>
    <the-footer/>
  </a-layout>
</template>

<script>
import TheHeader from "@/components/the-header";
import TheFooter from "@/components/the-footer";
export default {
  components: {
    TheHeader,
    TheFooter
  }
}
</script>
完整源码

在这里插入图片描述

  • main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

createApp(App).use(store).use(router).use(Antd).mount('#app')
  • App.vue
<template>
  <a-layout>
    <the-header/>
    <router-view/>
    <the-footer/>
  </a-layout>
</template>

<script>
import TheHeader from "@/components/the-header";
import TheFooter from "@/components/the-footer";
export default {
  components: {
    TheHeader,
    TheFooter
  }
}
</script>
  • components/the-header.vue
<template>
  <a-layout-header class="header">
    <div class="logo" />
    <a-menu
        theme="dark"
        mode="horizontal"
        v-model:selectedKeys="selectedKeys1"
        :style="{ lineHeight: '64px' }"
    >
      <a-menu-item key="1">nav 1</a-menu-item>
      <a-menu-item key="2">nav 2</a-menu-item>
      <a-menu-item key="3">nav 3</a-menu-item>
    </a-menu>
  </a-layout-header>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'the-header',
});
</script>

<style>
  .logo {
    float: left;
    width: 120px;
    height: 31px;
    margin: 16px 24px 16px 0;
    background: rgba(255, 255, 255, 0.3);
  }
</style>
  • components/the-footer.vue
<template>
  <a-layout-footer style="text-align: center">
    Test for 电子书
  </a-layout-footer>
</template>

<script>
import {defineComponent} from "vue";
export default defineComponent({
  name:"the-footer"
})
</script>
  • router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
  • views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>
  • views/Home.vue
<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      Content
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Home'
});
</script>
  • store/index.ts
import { createStore } from 'vuex'

export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})
集成axios
安装axios
npm install axios --save
使用axios
  • Home.vue中使用axios调用后台接口,获取电子书列表
<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      Content
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
export default defineComponent({
  name: 'Home',
  setup(){
    axios.get("http://127.0.0.1:8088/ebook/list").then(resp => {
      console.log(resp);
    })
  }
});
</script>
设置TestApplication应用的启动端口

application.properties中添加server.port=8088,这样TestApplication应用就会在8088端口启动。

启动应用

运行TestApplication,启动web,遇到跨域问题:Access to XMLHttpRequest at ‘http://127.0.0.1:8088/ebook/list’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
PS:跨域访问限制是针对浏览器的,服务器与服务器之间没有跨域限制。

在这里插入图片描述

解决跨域问题

Spring Frame的接口org.springframework.web.servlet.config.annotation.WebMvcConfigurer里有很多方法,其中addCorsMapping()可以设置全局跨域。
在这里插入图片描述
在这里插入图片描述
在src目录下新建包config,在config下新建类CorsConfig,如下所示,
在这里插入图片描述

package com.jepcc.test.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
                .maxAge(3600);
    }
}

重新启动应用。
在这里插入图片描述
在这里插入图片描述

组件

关于Vue组件注册,可以移步Vue官网

全局组件
  • main.ts
    在这里插入图片描述
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import * as Icons from "@ant-design/icons-vue";

const app = createApp(App);
app.use(store).use(router).use(Antd).mount('#app');

const icons: any = Icons;
for(const i in icons){
    app.component(i,icons[i]);
}
  • Home.vue
    在这里插入图片描述
<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="listData">
        <template #footer>
          <div>
            <b>ant design vue</b>
            footer part
          </div>
        </template>
        <template #renderItem="{ item }">
          <a-list-item key="item.title">
            <template #actions>
          <span v-for="{ type, text } in actions" :key="type">
            <component v-bind:is="type" style="margin-right: 8px" />
            {{ text }}
          </span>
            </template>
            <template #extra>
              <img
                  width="272"
                  alt="logo"
                  src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
              />
            </template>
            <a-list-item-meta :description="item.description">
              <template #title>
                <a :href="item.href">{{ item.title }}</a>
              </template>
              <template #avatar><a-avatar :src="item.avatar" /></template>
            </a-list-item-meta>
            {{ item.content }}
          </a-list-item>
        </template>
      </a-list>
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';

const listData: Record<string, string>[] = [];

for (let i = 0; i < 23; i++) {
  listData.push({
    href: 'https://www.antdv.com/',
    title: `ant design vue part ${i}`,
    avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
    description:
        'Ant Design, a design language for background applications, is refined by Ant UED Team.',
    content:
        'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
  });
}

export default defineComponent({
  name: 'Home',
  setup(){
    axios.get("http://127.0.0.1:8088/ebook/list?name=Spring").then(resp => {
      console.log(resp);
    });
    const pagination = {
      onChange: (page: number) => {
        console.log(page);
      },
      pageSize: 3,
    };
    const actions: Record<string, string>[] = [
      { type: 'StarOutlined', text: '156' },
      { type: 'LikeOutlined', text: '156' },
      { type: 'MessageOutlined', text: '2' },
    ];
    return {
      listData,
      pagination,
      actions,
    };
  }
});
</script>

局部组件
  • main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

createApp(App).use(store).use(router).use(Antd).mount('#app')
  • Home.vue
    在这里插入图片描述
<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="listData">
        <template #footer>
          <div>
            <b>ant design vue</b>
            footer part
          </div>
        </template>
        <template #renderItem="{ item }">
          <a-list-item key="item.title">
            <template #actions>
          <span v-for="{ type, text } in actions" :key="type">
            <component v-bind:is="type" style="margin-right: 8px" />
            {{ text }}
          </span>
            </template>
            <template #extra>
              <img
                  width="272"
                  alt="logo"
                  src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
              />
            </template>
            <a-list-item-meta :description="item.description">
              <template #title>
                <a :href="item.href">{{ item.title }}</a>
              </template>
              <template #avatar><a-avatar :src="item.avatar" /></template>
            </a-list-item-meta>
            {{ item.content }}
          </a-list-item>
        </template>
      </a-list>
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
import { StarOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons-vue';

const listData: Record<string, string>[] = [];

for (let i = 0; i < 23; i++) {
  listData.push({
    href: 'https://www.antdv.com/',
    title: `ant design vue part ${i}`,
    avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
    description:
        'Ant Design, a design language for background applications, is refined by Ant UED Team.',
    content:
        'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
  });
}

export default defineComponent({
  name: 'Home',
  components: {
    StarOutlined,
    LikeOutlined,
    MessageOutlined
  },
  setup(){
    axios.get("http://127.0.0.1:8088/ebook/list?name=Spring").then(resp => {
      console.log(resp);
    });
    const pagination = {
      onChange: (page: number) => {
        console.log(page);
      },
      pageSize: 3,
    };
    const actions: Record<string, string>[] = [
      { type: 'StarOutlined', text: '156' },
      { type: 'LikeOutlined', text: '156' },
      { type: 'MessageOutlined', text: '2' },
    ];
    return {
      listData,
      pagination,
      actions,
    };
  }
});
</script>

当前页面效果如下:
在这里插入图片描述

数据绑定
使用ref实现数据绑定

ref()用来定义响应式数据。

<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <a-list item-layout="vertical" size="large" :data-source="ebooks">
        <template #renderItem="{ item }">
          <a-list-item key="item.title">
            <template #actions>
          <span v-for="{ type, text } in actions" :key="type">
            <component v-bind:is="type" style="margin-right: 8px" />
            {{ text }}
          </span>
            </template>
            <a-list-item-meta :description="item.description">
              <template #title>
                <a :href="item.href">{{ item.name }}</a>
              </template>
              <template #avatar><a-avatar :src="item.cover" /></template>
            </a-list-item-meta>
          </a-list-item>
        </template>
      </a-list>
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent,onMounted,ref } from 'vue';
import axios from 'axios';
import { StarOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons-vue';

export default defineComponent({
  name: 'Home',
  components: {
    StarOutlined,
    LikeOutlined,
    MessageOutlined
  },
  setup(){
    console.log("setup");

    const actions: Record<string, string>[] = [
      { type: 'StarOutlined', text: '156' },
      { type: 'LikeOutlined', text: '156' },
      { type: 'MessageOutlined', text: '2' },
    ];
    const ebooks = ref();

    onMounted(() => {
      console.log("onMounted");
      axios.get("http://127.0.0.1:8088/ebook/list?name=Spring").then(resp => {
        ebooks.value = resp.data.content;
      });
    })

    console.log("begin to return");
    return {
      ebooks,
      actions
    };
  }
});
</script>
使用reactive使用数据绑定

更多可以访问reactivetoRef

<template>
  <a-layout>
    <a-layout-sider width="200" style="background: #fff">
      <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
      >
        <a-sub-menu key="sub1">
          <template #title>
              <span>
                <user-outlined />
                subnav 1
              </span>
          </template>
          <a-menu-item key="1">option1</a-menu-item>
          <a-menu-item key="2">option2</a-menu-item>
          <a-menu-item key="3">option3</a-menu-item>
          <a-menu-item key="4">option4</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #title>
              <span>
                <laptop-outlined />
                subnav 2
              </span>
          </template>
          <a-menu-item key="5">option5</a-menu-item>
          <a-menu-item key="6">option6</a-menu-item>
          <a-menu-item key="7">option7</a-menu-item>
          <a-menu-item key="8">option8</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub3">
          <template #title>
              <span>
                <notification-outlined />
                subnav 3
              </span>
          </template>
          <a-menu-item key="9">option9</a-menu-item>
          <a-menu-item key="10">option10</a-menu-item>
          <a-menu-item key="11">option11</a-menu-item>
          <a-menu-item key="12">option12</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout-content
        :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <a-list item-layout="vertical" size="large" :data-source="ebooks">
        <template #renderItem="{ item }">
          <a-list-item key="item.title">
            <template #actions>
          <span v-for="{ type, text } in actions" :key="type">
            <component v-bind:is="type" style="margin-right: 8px" />
            {{ text }}
          </span>
            </template>
            <a-list-item-meta :description="item.description">
              <template #title>
                <a :href="item.href">{{ item.name }}</a>
              </template>
              <template #avatar><a-avatar :src="item.cover" /></template>
            </a-list-item-meta>
          </a-list-item>
        </template>
      </a-list>
    </a-layout-content>

  </a-layout>
</template>

<script lang="ts">
import { defineComponent,onMounted,reactive,toRef } from 'vue';
import axios from 'axios';
import { StarOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons-vue';

export default defineComponent({
  name: 'Home',
  components: {
    StarOutlined,
    LikeOutlined,
    MessageOutlined
  },
  setup(){
    console.log("setup");

    const actions: Record<string, string>[] = [
      { type: 'StarOutlined', text: '156' },
      { type: 'LikeOutlined', text: '156' },
      { type: 'MessageOutlined', text: '2' },
    ];
    const temp = reactive({ebooks:[]});

    onMounted(() => {
      console.log("onMounted");
      axios.get("http://127.0.0.1:8088/ebook/list?name=Spring").then(resp => {
        temp.ebooks = resp.data.content;
      });
    })

    console.log("begin to return");
    return {
      "ebooks":toRef(temp,"ebooks"),
      actions
    };
  }
});
</script>

当前页面效果如下:在这里插入图片描述

动态sql

目前,http://127.0.0.1:8088/ebook/list时,返回如下数据:

{
	"success": true,
	"message": null,
	"content": []
}

http://127.0.0.1:8088/ebook/lis?name=Spring,返回如下数据:

{
	"success": true,
	"message": null,
	"content": [
		{
			"id": 1,
			"name": "SpringBoot入门教程",
			"category1Id": null,
			"category2Id": null,
			"description": "零基础入门Java,企业级应用开发最佳首选框架",
			"cover": null,
			"docCount": null,
			"viewCount": null,
			"voteCount": null
		}
	]
}

现在希望实现动态sql,即如果url有查询字符串(比如?name=Spring),则sql中添加过滤条件,最后返回符合条件的数据;如果没有查询字符串,则返回所有数据。
所以,对com.jepcc.test.service.EbookService做如下修改:

package com.jepcc.test.service;

import com.jepcc.test.mapper.EbookMapper;
import com.jepcc.test.model.Ebook;
import com.jepcc.test.model.EbookExample;
import com.jepcc.test.req.EbookReq;
import com.jepcc.test.resp.EbookResp;
import com.jepcc.test.util.CopyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.List;


@Service
public class EbookService {
    @Autowired
    private EbookMapper ebookMapper;
    public List<EbookResp> list(EbookReq req){
        EbookExample ebookExample = new EbookExample();
        EbookExample.Criteria criteria = ebookExample.or();
        if(!ObjectUtils.isEmpty(req.getName())){
            criteria.andNameLike("%"+req.getName()+"%");
        }

        List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);

        return CopyUtil.copyList(ebookList,EbookResp.class);
    }
}

在这里插入图片描述
所以,现在调用接口http://127.0.0.1/ebook/list将返回所有电子书。

在这里插入图片描述

样式调整

通过设置Listgrid属性来实现栅格列表,column可以设置期望显示的列数。

<a-list item-layout="vertical" size="large" :data-source="ebooks" :grid="{ gutter: 16, column: 3 }">
</a-list>

在这里插入图片描述
调整电子书图标(封面),在Home.vue中添加如下样式,

<style scoped>
.ant-avatar{
  width: 50px;
  height: 50px;
  border-radius: 8%;
  margin: 5px 0;
}
</style>

在这里插入图片描述

Vue CLI多环境配置
注意事项
  • 环境文件的命名,.env.[mode]
    这里的mode值必须与npm scripts中的mode值一致,否则无法读取到环境文件。
    在这里插入图片描述
    在这里插入图片描述
  • 环境文件中的自定义变量要以VUE_APP_为前缀。
    只有以VUE_APP_开头的变量才会保留下来。
    除了VUE_APP_*变量外,process.env这个对象中始终还有另外两个特殊变量。
    • NODE_ENV,可能的值是developmentproductiontest,具体值取决于应用运行的模式
    • BASE_URL,应用部署的基础路径
新增环境文件和环境变量

在web目录下新增环境文件:.env.dev.env.prod,环境文件中包含环境变量的“键=值”对。
注意哈,虽然本例中使用了.env.dev.env.prod,但仍建议使用.env.development.env.production来命名环境文件,因为如果npm scripts中vue-cli-service serve没有显式指定mode参数,则默认是development模式。

  • .env.dev
NODE_ENV=development
VUE_APP_SERVER=http://127.0.0.1:8088

  • .env.prod
NODE_DEV=production
VUE_APP_SERVER=http://127.0.0.1:8088
  • 修改package.json
  "scripts": {
    "serve-dev": "vue-cli-service serve --mode dev --port 8080",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  }

--mode dev:指定模式为dev,即开发模式;
--port 8080:指定端口为8080

  • 前端使用环境变量
//main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

createApp(App).use(store).use(router).use(Antd).mount('#app');

console.log("NODE_ENV:",process.env.NODE_ENV);
console.log("VUE_APP_SERVER:",process.env.VUE_APP_SERVER);

main.ts中通过process.env访问环境变量:NODE_ENVVUE_APP_SERVER
在这里插入图片描述
举一个实用的例子,在main.ts用环境变量设置全局axios默认值。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import axios from 'axios';
axios.defaults.baseURL = process.env.VUE_APP_SERVER;

createApp(App).use(store).use(router).use(Antd).mount('#app');

axios.defaults.baseURL = process.env.VUE_APP_SERVER,有了这句,Home.vue中的axios请求就不用像下面这样写,

 axios.get("http://127.0.0.1:8088/ebook/list").then(resp => {
   temp.ebooks = resp.data.content;
 });

而应该这样,且所有的axios请求其url都不需要添加服务器地址了。

axios.get("/ebook/list").then(resp => {
  temp.ebooks = resp.data.content;
});
axios拦截器:打印请求参数和返回参数

可以使用axios拦截器来打印请求参数和返回参数。

axios.interceptors.request.use(function(config){
    console.log("请求参数:",config);
    return config;
},function(error){
    console.log("返回错误:",error);
    return Promise.reject(error);
})

axios.interceptors.response.use(function(response){
    console.log("返回参数:",response);
    return response;
},function(error){
    console.log("返回错误:",error);
    return Promise.reject(error);
})

在请求和响应被thencatch处理前拦截它们。
在这里插入图片描述

SpringBoot过滤器:打印接口耗时

Filter依赖于Servlet容器,属于Servlet规范的一部分,Filter的执行由Servlet容器回调完成,Filter的生命周期由Servlet容器管理。
在这里插入图片描述
现在使用Filter来打印接口耗时。
com.jepcc.test下新建包filter,并在该包下新建类LogFilter来记录接口耗时。

package com.jepcc.test.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class LogFilter implements Filter {
    private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        LOG.info("----------LogFilter start----------");
        LOG.info("请求地址:{} {}",request.getRequestURI(),request.getMethod());
        LOG.info("远程地址:{}",request.getRemoteAddr());

        long startTime = System.currentTimeMillis();
        filterChain.doFilter(servletRequest,servletResponse);
        LOG.info("----------LogFilter end,接口耗时:{} ms----------",(System.currentTimeMillis()-startTime));
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

在项目test目录下新建目录http,并新建文件test.http,该文件内容给如下,

GET http://localhost:8088/ebook/list
###

来调试下看看效果,
在这里插入图片描述
一张图描述容器、Servlet、Filter之间的关系。
在这里插入图片描述
我们知道,Servlet是用来处理请求和响应的。客户端发出请求,但请求不是直接交给Servlet本身,而是交给部署了该Servlet的容器,由容器提供HTTP请求对象、HTTP响应对象,然后到Servlet并调用Servlet的服务方法,如doPost()doGet()。本例中加了Filter,那么来自客户端的请求就需要多经历一道,即从容器->Servlet变成了容器->Filter->Servlet

Spring拦截器:打印接口耗时

Spring拦截器是Spring框架特有的,常用于登录校验、权限校验、请求日志打印等。
在这里插入图片描述
要区分Spring过滤器和Spring拦截器,可以借用上一张图。
在这里插入图片描述
拦截器是在请求达到DispatcherServlet后,在DispatcherServlet调用某个Controller类时执行的。
使用Spring拦截器来打印接口耗时。
com.jepcc.test新建包interceptor,并在该包下新建类LogInterceptor,其内容如下,

package com.jepcc.test.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LogInterceptor implements HandlerInterceptor {
    private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOG.info("---------- LogInterceptor start----------");
        LOG.info("请求地址:{} {}",request.getRequestURI(),request.getMethod());
        LOG.info("远程地址:{}",request.getRemoteAddr());
        long startTime = System.currentTimeMillis();
        request.setAttribute("requestStartTime",startTime);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (long) request.getAttribute("requestStartTime");
        LOG.info("----------LogInterceptor---------- 请求耗时:{} ms",System.currentTimeMillis()-startTime);
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

使用拦截器需要增加一个全局配置类,在com.jepcc.test.config下新增配置类SpringMvcConfig,该类内容给如下,

package com.jepcc.test.config;

import com.jepcc.test.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Resource
    LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor)
                .addPathPatterns("/**");
    }
}

WebMvcConfigurer是不是很眼熟,因为我们在解决跨域问题时也用到了它。
WebMvcConfigurer这个接口类是Spring内部的一种配置方式,我们可以创建一个类并实现WebMvcConfigurer接口来自定义Handler、Interceptor、ViewResolver、MessageConverter等。这种配置方式与传统的xml配置文件不同,而是采用JavaBean的方式来进行个性化定制。
好了,最后我们来测试下。
在这里插入图片描述

SpringBoot AOP:打印请求参数、返回参数、接口耗时

为了更快地下载依赖包,我这边改用淘宝镜像。

D:\JavaProjects\test>npm get registry
https://registry.npmjs.org/

D:\JavaProjects\test>npm set registry https://registry.npm.taobao.org

D:\JavaProjects\test>npm get registry
https://registry.npm.taobao.org/

本例中,为了打印日志方便,需要将Java对象转换为JSON字符串,所以会用到fastjson这个jar包,因此在pom.xml里添加fastjson。

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.70</version>
</dependency>

好了,我们再说回AOP。
要用SpringBoot AOP,就要引入相应的依赖,SpringBoot提供了spring-boot-starter-aop这个内置模块,所以在pom.xml中添加这个依赖,如下所示,

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

接下里,就是定义切面类:在com.jepcc.test下新建包aspect,并在该包下新建类LogAspect,如下,

package com.jepcc.test.aspect;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class LogAspect {
    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    @Pointcut("execution(public * com.jepcc.*.controller..*Controller.*(..))")
    public void controllerPointcut(){}

    @Before("controllerPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable{
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        LOG.info("----------LogAspect start----------");
        LOG.info("请求地址:{} {}",request.getRequestURI(),request.getMethod());
        LOG.info("远程地址:{}",request.getRemoteAddr());

        Object[] args = joinPoint.getArgs();
        Object[] arguments = new Object[args.length];
        for(int i=0;i<args.length;i++){
            if(args[i] instanceof ServletRequest
                || args[i] instanceof ServletResponse
                || args[i] instanceof MultipartFile){
                continue;
            }
            arguments[i] = args[i];
        }

        LOG.info("请求参数:{}", JSONObject.toJSONString(arguments));

    }

    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();

        LOG.info("返回参数:{}",JSONObject.toJSONString(result));
        LOG.info("----------LogAspect end---------- 接口耗时:{} ms",System.currentTimeMillis()-startTime);
        return result;
    }
}

在这里插入图片描述

AOP相关概念
  • 连接点
    一个类中的所有方法都可以是连接点。
  • 切点
    一个类中的所有方法都可以是连接点,你可以将它们全部定义为切点,也可以将其中的一部分定义为切点。
    使用@Pointcut注解来定义切点。
    @Pointcut("execution(public * com.jepcc.*.controller..*Controller.*(..))")的意思是,将本应用下的controller包下的所有Controller类的所有方法定义为切点。
    • execution,表达式的主体
    • 第一个*,代表任意的返回值
    • com.jepcc.*.controller,代表com.jepcc下的任意包下的controller子包
    • ..,代表当前包名
    • *Controller,代表任意XXXController类
    • .*(..),代表任何方法,括号代表参数,..表示任意参数

在这里插入图片描述

  • 通知
    本例中我们希望通过AOP来打印请求参数、返回参数和接口耗时,这些我们自己的业务逻辑其实就是通知。

    • @Before,前置通知,在方法开始执行前执行
    • @After,后置通知,在方法执行后执行
    • @Around,环绕通知,在方法执行前、执行后都会执行
  • 切面
    在类上使用@Component注解,可将该类交给Spring容器管理。
    在类上使用@Aspect注解,使之成为切面类。
    切面里面就是切点通知,如果说通知定义了切面的动作和执行时机,那么切点就定义了动作的执行地点。

  • JoinPoint

    • getArgs(),获取方法参数
    • 更多信息访问这里那里
  • RequestContextHolder,可以获取请求信息、session信息

遇到的问题

【问题描述】环绕通知时,切面方法没有返回值,导致响应为空。
在这里插入图片描述
【解决方法】切面方法返回结果数据。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值