从0开始教你搭建前后端分离的微服务开发环境

提示:需要的一定的maven,Spring Boot基础

这里使用的Spring Cloud版本是2.2.0.RELEASE

代码参考:https://github.com/phone15082037343/demo


一、Sping Cloud与微服务概述

1.单体应用

简单的理解为:将所有的功能全部堆积在一起,形成的一个jar或者war、随着业务的发展,功能的增多,这个单体应用会变得越来越臃肿,代码变得不易维护。

2.什么是微服务

微服务是一种架构风格,即将单体应用划分为小型的服务单元,微服务之间使用http的API进行资源访问与操作 。

微服务的优势和劣势

臃肿的系统、复杂的代码、超长的启动时间带给开发人员的只有无限的抱怨,丝毫没有那种很舒服、很流畅的写代码的感觉。你会发现,解决问题的大部分时间都用在了平台的启动上。

  1. 优势
  • 服务独立部署
  • 服务的快速启动
  • 更加适合敏捷开发
  • 职责专一
  • 服务动态扩容
  • 代码复用
  1. 劣势

微服务其实是一把双刃剑,有利必然有弊。

  • 分布式部署,调用的复杂性高
  • 独立的数据库,分布式事务的挑战
  • 测试难度的提升
  • 运维难度的提升

3.什么是Spring Cloud

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便捷性,巧妙地简化了分布式基础设施的开发。

Spring Cloud的模块介绍

  • Eureka:微服务注册中心,用于服务管理
  • Ribbon:基于客户端的负载均衡组件
  • Hystrix:容错框架,能够防止服务的雪崩效应
  • Feign:web服务的客户端,能够简化Http接口的调用。
  • Zuul:API网关,提供路由转发,请求过滤等功能
  • Config:分布式配置管理
  • Sleuth:服务跟踪

Spring Cloud还提供了很多其他模块,这里就不一一列举了。

4.项目模块以及说明

开发、测试、产品(正式)环境
通常情况下,在windows上开发,在Linux上部署。这里演示的是windows上开发,Linux单机环境测试,Linux多台机器部署正式环境。

环境机器名备注
开发(windows10)开发以及打包都用windows来操作
测试server(centos8)单机环境用来测试,所有的application都在这台机器上启动
正式master、worker1、worker2 (centos8)eureka注册中心集群部署,其他application视情况而定

产品环境模块部署如下

机器名app
mastereureka、dashboard、gateway
worker1eureka、provider
worker2eureka、client

由于机器资源有限,只能这样部署。

模块说明

模块截图如下:

demo作为父模块,子模块所有的依赖都从父模块继承,方便统一管理。

  • demo-common:存放一些公共的代码
  • demo-eureka:注册中心
  • demo-provider:服务提供者,负责数据库访问
  • demo-client:服务调用者,为客户端调用提供访问接口
  • demo-dashboard:提供一个建议界面,可以看到demo-client调用demo-provider接口的情况,成功或者失败
  • demo-gateway:统一路由,客户端直接访问这个路由,再有路由转发请求到demo-client,客户端代码也放在这里面

二、Eureka注册中心

1. Eureka

主要负责实现微服务架构中的服务治理功能。Spring Cloud Eureka是一个基于REST的服务,并且提供了Java客户端组件,能够非常方便地将服务注册到Spring Cloud Eureka中进行统一管理。
这里有几种角色,注册中心、服务提供者与服务消费者。注册中心负责服务的注册于管理,提供提供者提供服务给服务消费者消费。

2. Eureka注册中心

设置版本号

<properties>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>

    <spring-cloud.version>2.2.0.RELEASE</spring-cloud.version>
    <spring-boot.version>2.2.1.RELEASE</spring-boot.version>
    <spring.version>5.2.1.RELEASE</spring.version>
    <gson.version>2.8.6</gson.version>
    <jackson.version>2.10.0</jackson.version>
    <lombok.version>1.18.12</lombok.version>
    <druid.version>1.1.20</druid.version>
    <postgresql.version>42.2.11</postgresql.version>
    <servlet-api.version>2.5</servlet-api.version>
</properties>
  • 添加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

spring-cloud-starter-netflix-eureka-server:注册中心
gson:会报Class找不到的错
spring-boot-starter-security:通常情况下,后台管理界面不允许直接访问,需要登录认证

  • application启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@SpringBootApplication
@EnableEurekaServer
@EnableWebSecurity
public class EurekaApplication extends WebSecurityConfigurerAdapter {

	public static void main(String[] args) {
		SpringApplication.run(EurekaApplication.class, args);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();
		super.configure(http);
	}

}

@EnableEurekaServer:启动eureka注册中心
@EnableWebSecurity:启动spring security认证

  • application.yml
server:
  port: 8761

spring:
  application:
    name: demo-eureka
  profiles:
    active: dev
  security:
    user:
      name: admin
      password: admin

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

spring.application.name:指定应用名称,很重要
spring.profiles.active:指定环境,值为 application-*.yml
eureka.client.register-with-eureka:不向注册中心注册自己,因为自己就是注册中心
eureka.client.fetch-registry:不需要检索服务
spring.security.user.name:用户名
spring.security.user.password:密码

  • application-dev.yml
eureka:
  server:
    enable-self-preservation: false # 关闭保护机制
  • application-test.yml

  • application-master.yml

eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@worker1:8761/eureka/,http://admin:admin@worker2:8761/eureka/

注意:这里设置了用户名和密码

  • application-worker1.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@master:8761/eureka/,http://admin:admin@worker2:8761/eureka/
  • application-worker2.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@master:8761/eureka/,http://admin:admin@worker1:8761/eureka/

打包后,java -jar执行会出现以下错误,是因为jackson的jar找不到

添加依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
</dependency>

浏览器访问http://127.0.0.1:8761,需要登录

3. 服务提供者(demo-provider)

这里提供接口使用jdbc访问数据库,接口提供给服务消费者使用,需要注册到注册中心。

  • 添加依赖
<dependency>
    <groupId>com.demo</groupId>
    <artifactId>demo-common</artifactId>
    <version>${project.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

demo-common:里面存放了一些公有代码
spring-cloud-starter-netflix-eureka-server:eureka客户端
spring-boot-starter-data-jpa:jpa框架访问数据库
lombok:需要idea安装lombok插件
druid:alibaba数据源
postgresql:pg连接,因为pg在Linux上安装比较方便,所以这里用pg

  • 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProviderApplication.class, args);
	}

}

@EnableDiscoveryClient:标识这是一个eureka的客户端

  • application.yml
server:
  port: 9000

spring:
  profiles:
    active: dev
  application:
    name: demo-provider
  • application-dev.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@127.0.0.1:8761/eureka/

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:postgresql://127.0.0.1:5432/platform
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: 123456
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false

eureka.client.service-url.defaultZone:eureka注册中心的地址
spring.datasource.*:数据源连接

  • application-test.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@server:8761/eureka/

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:postgresql://server:5432/platform
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: postgres
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
  • application-pro.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@master:8761/eureka/,http://admin:admin@worker1:8761/eureka/,http://admin:admin@worker2:8761/eureka/

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:postgresql://master:5432/platform
    driver-class-name: org.postgresql.Driver
    username: postgres
    password: postgres
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: false
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false

这里注意一下注册到eureka集群的方式

  • controller
import com.demo.common.dto.req.UserRequest;
import com.demo.common.dto.res.UserResponse;
import com.demo.common.utils.page.PageModel;
import com.demo.provider.entity.User;
import com.demo.provider.repository.UserRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserRepository userRepository;

	@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
	public UserResponse save(@RequestBody UserRequest userRequest) {
		User user = new User();
		BeanUtils.copyProperties(userRequest, user);

		userRepository.save(user);
		UserResponse userResponse = new UserResponse();
		BeanUtils.copyProperties(user, userResponse);
		return userResponse;
	}

	@DeleteMapping("/{id}")
	public String deleteById(@PathVariable(name = "id") String id) {
		userRepository.deleteById(id);
		return id;
	}

	@GetMapping("/{id}")
	public UserResponse findById(@PathVariable(name = "id") String id) {
		User user = userRepository.findById(id).orElse(null);
		if (user != null) {
			UserResponse userResponse = new UserResponse();
			BeanUtils.copyProperties(user, userResponse);
			return userResponse;
		}
		return null;
	}

	@GetMapping
	public PageModel<UserResponse> find(@RequestParam(name = "pageNo", defaultValue = "1") int pageNo,
	                                    @RequestParam(name = "size", defaultValue = "10") int size) {
		Page<User> page = userRepository.findAll(PageRequest.of(pageNo - 1, size));
		PageModel<UserResponse> pm = new PageModel<>();
		pm.setPageNo(pageNo);
		pm.setSize(size);
		pm.setTotalCount((int) page.getTotalElements());
		pm.setTotalPage(page.getTotalPages());
		List<UserResponse> list = page.stream().map(user -> {
			UserResponse userResponse = new UserResponse();
			BeanUtils.copyProperties(user, userResponse);
			return userResponse;
		}).collect(Collectors.toList());
		pm.setList(list);
		return pm;
	}

}

这里只是简答的做了一下增删改查

  • 实体对象
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@Table(name = "user_info")
public class User {

	@Id
	@Column(name = "user_id", length = 32)
	@GenericGenerator(name = "uuidGenerator", strategy = "uuid")
	@GeneratedValue(generator = "uuidGenerator")
	private String userId;

	@Column(name = "name", length = 64)
	private String name;

	@Column(name = "sex")
	private int sex;

	@Column(name = "age")
	private int age;

	@Column(name = "birthday")
	private Date birthday;

}
  • dto:也是这几个字段
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

@Data
public class UserRequest {

	private String userId;
	private String name;
	private int sex;
	private int age;
	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
	private Date birthday;

}
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

@Data
public class UserResponse {

	private String userId;
	private String name;
	private int sex;
	private int age;
	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
	private Date birthday;

}
  • repository
import com.demo.provider.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {



}

4. 服务消费者

见【三、Feign客户端

三、Feign客户端(demo-client)

java调用http接口,httpclient、okhttp、httpurlconnection、resttemplate都不是很方便,使用Feign,简化了调用http接口的过程。
添加依赖

<dependency>
    <groupId>com.demo</groupId>
    <artifactId>demo-common</artifactId>
    <version>${project.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
</dependency>
<dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
</dependency>

spring-cloud-starter-netflix-eureka-server:
spring-cloud-starter-openfeign:Feign的依赖
spring-cloud-starter-netflix-hystrix:监控与容错

  • 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
public class ClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ClientApplication.class, args);
	}

}

@EnableDiscoveryClient:作为eureka的客户端
@EnableFeignClients:启用feign
@EnableHystrix:启用监控与容错

  • application.yml
server:
  port: 8000

spring:
  profiles:
    active: dev
  application:
    name: demo-client

# 启动feign
feign:
  hystrix:
    enabled: true

# 需要加上
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

  • application-dev.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@127.0.0.1:8761/eureka/
  • application-test.yml

  • application-pro.yml

  • FeignConfiguration

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfiguration {

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

}
  • UserController
import com.demo.client.api.UserClient;
import com.demo.common.dto.req.UserRequest;
import com.demo.common.utils.ajax.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserClient userClient;

	@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
	public Result save(@RequestBody UserRequest userRequest) {
		Result result = new Result();
		result.setData(userClient.save(userRequest));
		result.setSuccess(true);
		result.setMessage("用户保存成功");
		return result;
	}

	@DeleteMapping("/{id}")
	public Result deleteById(@PathVariable(name = "id") String id) {
		Result result = new Result();
		result.setData(userClient.deleteById(id));
		result.setSuccess(true);
		result.setMessage("用户删除成功");
		return result;
	}

	@GetMapping("/{id}")
	public Result findById(@PathVariable(name = "id") String id) {
		Result result = new Result();
		result.setData(userClient.findById(id));
		result.setSuccess(true);
		return result;
	}

	@GetMapping
	public Result find(@RequestParam(name = "pageNo", defaultValue = "1") int pageNo,
	                                    @RequestParam(name = "size", defaultValue = "10") int size) {
		Result result = new Result();
		result.setData(userClient.find(pageNo, size));
		result.setSuccess(true);
		return result;
	}

}

  • UserClient
import com.demo.client.conf.FeignConfiguration;
import com.demo.client.fallback.UserClientFallbackFactory;
import com.demo.common.dto.req.UserRequest;
import com.demo.common.dto.res.UserResponse;
import com.demo.common.utils.page.PageModel;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "demo-provider", path = "/user", configuration = FeignConfiguration.class, fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

	@PostMapping
	UserResponse save(@RequestBody UserRequest userRequest);

	@DeleteMapping("/{id}")
	public String deleteById(@PathVariable(name = "id") String id);

	@GetMapping("/{id}")
	public UserResponse findById(@PathVariable(name = "id") String id);

	@GetMapping
	PageModel<UserResponse> find(@RequestParam(name = "pageNo", defaultValue = "1") int pageNo,
	                             @RequestParam(name = "size", defaultValue = "10") int size);

}

@FeignClient():value设置服务提供者,path设置统一访问前缀,fallbackFactory设置http请求失败之后默认返回值

  • Fallback
import com.demo.client.api.UserClient;
import com.demo.common.dto.req.UserRequest;
import com.demo.common.dto.res.UserResponse;
import com.demo.common.utils.page.PageModel;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

	@Override
	public UserClient create(Throwable throwable) {
		return new UserClient() {
			@Override
			public UserResponse save(UserRequest userRequest) {
				return null;
			}

			@Override
			public String deleteById(String id) {
				return id;
			}

			@Override
			public UserResponse findById(String id) {
				return null;
			}

			@Override
			public PageModel<UserResponse> find(int pageNo, int size) {
				return null;
			}
		};
	}

}

四、Dashboard监控(demo-dashboard)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
  • 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.turbine.EnableTurbine;

@SpringBootApplication
@EnableHystrixDashboard
@EnableDiscoveryClient
@EnableTurbine
public class DashboardApplication {

	public static void main(String[] args) {
		SpringApplication.run(DashboardApplication.class, args);
	}

}

@EnableHystrixDashboard:启用dashboard
@EnableTurbine:启用turbine

  • application.yml
server:
  port: 9210

spring:
  application:
    name: demo-dashboard
  profiles:
    active: dev

turbine:
  app-config: demo-client
  aggregator:
    cluster-config: default
  cluster-name-expression: new String("default")
  • application-dev.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@127.0.0.1:8761/eureka/
  • application-test.yml
  • application-pro.yml

浏览器访问http://127.0.0.1:9210/hystrix


五、zuul路由(demo-gateway)

提供客户端接口访问统一地址

  • 依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
</dependency>
  • 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulApplication.class, args);
	}

}

@EnableZuulProxy

  • application.yml
server:
  port: 8080

spring:
  application:
    name: demo-gateway
  profiles:
    active: dev
  resources:
    static-locations: classpath:/static/, classpath:/templates/

# 注意这里的配置,不过多解释
zuul:
  routes:
    demo-client:
      path: /client/**
  sensitive-headers:   # 转换的时候带上cookie
  • application-dev.yml
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin@127.0.0.1:8761/eureka/
  • application-test.yml
  • application-pro.yml

六、vue+element ui客户端

获取客户端模板源码,是由vue提供的,百度搜索关键字“vue-admin-template”

把客户端源码放在路由里面,原因后面再解释

开发环境下,客户端是由nodejs启动,所有需要配置代理

  • vue.config.js

    注意:这两个文件.env.development、.env.production

proxy: {
      [process.env.VUE_APP_BASE_API]: {
        target: 'http://127.0.0.1:8080',
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }

启动客户端之前需要先用一下命令安装nodejs依赖

npm install --registry https://registry.npm.taobao.org
  • 客户端请求,src/utils/request.js
response => {
    const result = response.data;
    if (!result.success) {
      // 错误提示
      Message({
        message: result.message || 'Error',
        type: 'error',
        duration: 3 * 1000
      });
    } else {
      const { message } = result;
      // 成功提示
      if (message != null && message.length > 0) {
        Message({
          message: message,
          type: 'success',
          duration: 3 * 1000
        });
      }
    }
    return response;
  },

统一使用这个工具类来发送请求

  • 用户登录,src/api/user.js

这里有三个接口,需要服务端根据对应的情况来弄,这里就不做更多的演示,提供简单的接口就行。

import com.demo.client.api.UserClient;
import com.demo.common.dto.req.UserRequest;
import com.demo.common.utils.ajax.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserClient userClient;

	@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
	public Result save(@RequestBody UserRequest userRequest) {
		Result result = new Result();
		result.setData(userClient.save(userRequest));
		result.setSuccess(true);
		result.setMessage("用户保存成功");
		return result;
	}

	@DeleteMapping("/{id}")
	public Result deleteById(@PathVariable(name = "id") String id) {
		Result result = new Result();
		result.setData(userClient.deleteById(id));
		result.setSuccess(true);
		result.setMessage("用户删除成功");
		return result;
	}

	@GetMapping("/{id}")
	public Result findById(@PathVariable(name = "id") String id) {
		Result result = new Result();
		result.setData(userClient.findById(id));
		result.setSuccess(true);
		return result;
	}

	@GetMapping
	public Result find(@RequestParam(name = "pageNo", defaultValue = "1") int pageNo,
	                                    @RequestParam(name = "size", defaultValue = "10") int size) {
		Result result = new Result();
		result.setData(userClient.find(pageNo, size));
		result.setSuccess(true);
		return result;
	}

}
  • vue路由配置,src/router/index.js
{
    path: '/user',
    component: Layout,
    children: [
      {
        path: 'manage',
        name: 'Manage',
        component: () => import('@/views/user/manage/index'),
        meta: {
          title: 'UserManage',
          icon: 'form'
        }
      },
      {
        path: 'add',
        name: 'UserAdd',
        component: () => import('@/views/user/add/index'),
        meta: {
          title: 'UserAdd',
          icon: 'form'
        },
        hidden: true
      },
      {
        path: 'update',
        name: 'UserUpdate',
        component: () => import('@/views/user/update/index'),
        meta: {
          title: 'UserUpdate',
          icon: 'form'
        },
        hidden: true
      }
    ]
  },
  • 管理,src/views/user/manage/index.vue
<template>
  <div class="app-container">
    <div class="btns">
      <el-tooltip class="item" effect="dark" content="添加" placement="top">
        <el-button type="primary" size="medium" icon="el-icon-plus" @click="toAddPage" />
      </el-tooltip>

    </div>
    <el-table :data="tableData" stripe border style="width: 100%">
      <el-table-column type="selection" width="55" prop="userId" align="center" />
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="sex" label="性别">
        <template slot-scope="scope">
          <span v-if="scope.row.sex == 0"></span>
          <span v-else-if="scope.row.sex == 1"></span>
        </template>
      </el-table-column>
      <el-table-column prop="age" label="年龄" />
      <el-table-column prop="birthday" label="生日" />
      <el-table-column label="操作">
        <template slot-scope="scope">
          <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
          <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <div class="block">
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="page.currentPage"
        :page-sizes="[10, 20, 50, 100]" :page-size="page.size" layout="total, sizes, prev, pager, next, jumper" :total="page.total" />
    </div>
  </div>
</template>

<script>
  import request from '@/utils/request'
  export default {
    data() {
      return {
        tableData: [],
        page: {
          currentPage: 1,
          size: 10,
          total: 101
        }
      }
    },
    methods: {
      handleSizeChange(val) {
        this.page.size = val;
        this.page.currentPage = 1;
        this.loadTableData();
      },
      handleCurrentChange(val) {
        this.page.currentPage = val;
        this.loadTableData();
      },
      loadTableData() {
        request({
          url: "/client/user",
          method: "get",
          params: {
            pageNo: this.page.currentPage,
            size: this.page.size
          }
        }).then(response => {
          const result = response.data;
          const {
            data
          } = result;
          this.tableData = data.list;
          this.page.total = data.totalCount;
        });
      },
      handleEdit(index, row) {
        // 跳转到更新页面
        this.$router.push({
          name: 'UserUpdate',
          params: {
            userId: row.userId
          }
        });
      },
      handleDelete(index, row) {
        request({
          url: "/client/user/" + row.userId,
          method: "delete"
        }).then(response => {
          var result = response.data;
          if (result.success) {
            this.loadTableData();
          }
        });
      },
      toAddPage() {
        this.$router.push({
          path: '/user/add'
        });
      }
    },
    created() {
      this.loadTableData();
    }
  }
</script>
<style scoped>
  .block {
    text-align: right;
    margin-top: 10px;
  }

  .btns {
    margin-bottom: 10px;
  }
</style>

  • 添加,src/views/user/add/index.vue
<template>
  <div class="app-container">
    <el-form ref="form" :model="form" label-width="80px">
      <el-form-item label="姓名">
        <el-input v-model="form.name" clearable placeholder="在这里输入性名" />
      </el-form-item>
      <el-form-item label="性别">
        <el-radio-group v-model="form.sex">
          <el-radio label="男" />
          <el-radio label="女" />
        </el-radio-group>
      </el-form-item>
      <el-form-item label="年龄">
        <el-input v-model="form.age" type="number" min="0" clearable placeholder="在这里输入年龄" />
      </el-form-item>
      <el-form-item label="生日">
        <el-date-picker v-model="form.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">保存</el-button>
        <el-button @click="cancel">取消</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
  import request from '@/utils/request'
  export default {
    data() {
      return {
        form: {
          name: '',
          sex: '男',
          age: 18,
          birthday: ''
        }
      }
    },
    methods: {
      onSubmit() {
        var data = {
          name: this.form.name,
          age: this.form.age,
          birthday: this.form.birthday
        }
        const { sex } = this.form;
        if (sex == '男') {
          data.sex = 0;
        } else if (sex == '女') {
          data.sex = 1;
        }

        request({
          url: "/client/user",
          method: "post",
          data
        }).then(response => {
          var result = response.data;
          if (result.success) {
            this.$router.push({
              path: '/user/manage'
            });
          }
        });

      },
      cancel() {
        this.$router.push({
          path: '/user/manage'
        });
      }
    }
  }
</script>

<style scoped>

</style>

  • 更新,src/views/user/update/index.vue
<template>
  <div class="app-container">
    <el-form ref="form" :model="form" label-width="80px">
      <el-form-item label="姓名">
        <el-input v-model="form.name" clearable placeholder="在这里输入性名" />
      </el-form-item>
      <el-form-item label="性别">
        <el-radio-group v-model="form.sex">
          <el-radio label="男" />
          <el-radio label="女" />
        </el-radio-group>
      </el-form-item>
      <el-form-item label="年龄">
        <el-input v-model="form.age" type="number" min="0" clearable placeholder="在这里输入年龄" />
      </el-form-item>
      <el-form-item label="生日">
        <el-date-picker v-model="form.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">保存</el-button>
        <el-button @click="cancel">取消</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
  import request from '@/utils/request'
  export default {
    data() {
      return {
        form: {
          userId: '',
          name: '',
          sex: '男',
          age: 18,
          birthday: ''
        }
      }
    },
    methods: {
      onSubmit() {
        var data = {
          userId: this.form.userId,
          name: this.form.name,
          age: this.form.age,
          birthday: this.form.birthday
        }
        const {
          sex
        } = this.form;
        if (sex == '男') {
          data.sex = 0;
        } else if (sex == '女') {
          data.sex = 1;
        }

        request({
          url: "/client/user",
          method: "post",
          data
        }).then(response => {
          var result = response.data;
          if (result.success) {
            this.$router.push({
              path: '/user/manage'
            });
          }
        });

      },
      cancel() {
        this.$router.push({
          path: '/user/manage'
        });
      }
    },
    created() {
      const userId = this.$route.params.userId;
      request({
        url: '/client/user/' + userId,
        method: 'get'
      }).then(response => {
        const result = response.data;
        const { data } = result;
        this.form.name = data.name;
        this.form.age = data.age;
        this.form.birthday = data.birthday;
        if (data.sex == 0) {
          this.form.sex = '男';
        } else if (data.sex == 1) {
          this.form.sex = '女';
        }
        this.form.userId = data.userId;
      });
    }
  }
</script>

<style scoped>

</style>

打包

这是重点,客户端需要打包,打包结果如下

这些是可以直接放到nginx下面去跑的,但是会出现跨域的问题,所有考虑将这个客户端的打包结果放到路由里面去,这样就不会出现跨域的问题了。
如果需要客户端先打包,在把打包结果放到路由里面,这样就有点不方便了,所以使用maven插件,一次性就能把包打出来。

<build>
    <finalName>demo-gateway</finalName>
    <plugins>
        <!-- spring boot 打包插件 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <fork>true</fork>
                <addResources>true</addResources>
                <mainClass>com.demo.gateway.ZuulApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>exec-npm-install</id>
                    <phase>generate-resources</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>install</argument>
                            <argument>--registry</argument>
                            <argument>https://registry.npm.taobao.org</argument>
                        </arguments>
                        <workingDirectory>${project.basedir}/src/main/front-end</workingDirectory>
                    </configuration>
                </execution>
                <execution>
                    <id>exec-npm-run-build</id>
                    <phase>generate-resources</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>run</argument>
                            <argument>build:prod</argument>
                        </arguments>
                        <workingDirectory>${project.basedir}/src/main/front-end</workingDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <configuration>
                <encoding>${project.build.sourceEncoding}</encoding>
            </configuration>
            <executions>
                <execution>
                    <id>copy-spring-boot-resources</id>
                    <!-- here the phase you need -->
                    <phase>generate-resources</phase>
                    <goals>
                        <goal>copy-resources</goal>
                    </goals>
                    <configuration>
                        <encoding>utf-8</encoding>
                        <outputDirectory>${project.basedir}/src/main/resources/templates</outputDirectory>
                        <overwrite>true</overwrite>
                        <resources>
                            <resource>
                                <directory>${basedir}/src/main/front-end/dist</directory>
                            </resource>
                        </resources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

结果演示

eureka集群部署

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流年ln

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值