文章目录
Vue和Vue CLI
Vue CLI = Vue.js + 一堆插件
创建项目
-
全局安装Vue CLI:
npm install -g @vue/cli
- 查看Vue CLI版本:
vue --version
或vue -V
- 安装Vue CLI时,最好添加版本号,如
npm install -g @vue/cli@4.5.13
- 查看Vue CLI版本:
-
创建一个项目:
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
- Manually select features,按空格选中
-
启动项目
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.js
的rules
中添加'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使用数据绑定
<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
将返回所有电子书。
样式调整
通过设置List
的grid
属性来实现栅格列表,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
,可能的值是development
、production
和test
,具体值取决于应用运行的模式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_ENV
和VUE_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);
})
在请求和响应被then
或catch
处理前拦截它们。
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
-
RequestContextHolder
,可以获取请求信息、session信息
遇到的问题
【问题描述】环绕通知时,切面方法没有返回值,导致响应为空。
【解决方法】切面方法返回结果数据。