Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Time:2021-10-20

Author: LV Yiming

Project code: https://github.com/MarkerHub/…

Project video: https://www.bilibili.com/vide…

Please keep this quote, thank you!

Front and rear end separation items

The article is generally divided into two parts. The Java back-end interface and Vue front-end page are relatively long, because they don’t want to be published separately. They really want you to learn in four hours, ha ha.

First look at the effect:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Not much to say, start typing the code.

Java backend interface development

1. Foreword

To build a project framework from scratch, it is best to choose appropriate and familiar technologies, which are easy to expand in the future and suitable for micro service system, etc. Therefore, we generally take springboot as the basis of our framework, which is inseparable.

Then, in the data layer, we often use mybatis, which is easy to use and easy to maintain. However, it is difficult to operate a single table, especially when adding or reducing fields, so I recommend using mybatis plus here( https://mp.baomidou.com/ ), born to simplify development, crud can be operated quickly with simple configuration, thus saving a lot of time.

As a project skeleton, permissions cannot be ignored. Shiro is simple to configure and use, so Shiro is used as our permissions.

Considering that the project may need to deploy multiple servers, at this time, our session and other information need to be shared. Redis is the mainstream caching middleware and is also suitable for our project.

Then, because the front and back ends are separated, we use JWT as our user identity credentials.

OK, let’s start building our project scaffolding now!

Technology stack:

  • SpringBoot
  • mybatis plus
  • shiro
  • lombok
  • redis
  • hibernate validatior
  • jwt

Map: https://www.markerhub.com/map/131

2. New springboot project

Here, we use idea to develop our project. The new step is relatively simple, so we won’t take a screenshot.

Development tools and environment:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

The newly created project structure is as follows. The latest version of 2.2.6.release is used in springboot

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

The jar package of POM is imported as follows:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  • Devtools: project’s hot load restart plug-in
  • Lombok: a tool to simplify code

3. Integrate mybatis plus

Next, let’s integrate mybatis plus to enable the project to complete the basic addition, deletion, modification and query operations. The steps are simple: you can go to the official website: https://mp.baomidou.com/guide…

Step 1: import the jar package

Import the jar package of mybatis plus into POM. Since code generation will be involved later, we also need to import the page template engine. Here we use freemaker.

<!--mp-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- MP code generator -- >
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.2.0</version>
</dependency>

Step 2: then write the configuration file

# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: admin
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml

In addition to configuring the database information, the above also configures the scanning path of the XML file of the mapper of myabtis plus. Don’t forget this step.
Step 3: start mapper interface scanning and add paging plug-in

Create a new package: specify the package where the interface to become the implementation class is located through the @ maperscan annotation, and then all interfaces under the package will generate corresponding implementation classes after compilation. Paginationinterceptor is a paging plug-in.

  • com.markerhub.config.MybatisPlusConfig
@Configuration
@EnableTransactionManagement
@MapperScan("com.markerhub.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

Step 4: code generation

If you don’t use other plug-ins, you can use mybatis plus now. The official provides us with a code generator. After I write my own parameters, I can directly generate entity, service, mapper and other interfaces and implementation classes according to the database table information.

  • com.markerhub.CodeGenerator

Because the code is relatively long, it won’t be posted. Look at it in the code warehouse!

First, I created a new user table in the database:

CREATE TABLE `m_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `status` int(5) NOT NULL,
  `created` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `m_blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `title` varchar(255) NOT NULL,
  `description` varchar(255) NOT NULL,
  `content` longtext,
  `created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;
INSERT INTO `vueblog`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1', 'markerhub', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

Run the main method of codegenerator and enter the table name: M_ User, the generated results are as follows:
Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Get:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Concise! Convenient! After the above steps, we have basically integrated the mybatis plus framework into the project.

PS: Well, pay attention m_ The code of blog table is also generated.

Write a test in usercontroller:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserService userService;
    @GetMapping("/{id}")
    public Object test(@PathVariable("id") Long id) {
        return userService.getById(id);
    }
}

visit: http://localhost:8080/user/1 The results are as follows: the integration is successful!
Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

3. Unified result encapsulation

Here we use a result class, which is used to encapsulate the results returned asynchronously and uniformly. Generally speaking, there are several necessary elements in the result

  • Success can be indicated by code (for example, 200 indicates success and 400 indicates exception)
  • Result message
  • Result data

Therefore, the package can be obtained as follows:

  • com.markerhub.common.lang.Result
@Data
public class Result implements Serializable {
    private String code;
    private String msg;
    private Object data;
    public static Result succ(Object data) {
        Result m = new Result();
        m.setCode("0");
        m.setData(data);
        m. Setmsg ("operation succeeded");
        return m;
    }
    public static Result succ(String mess, Object data) {
        Result m = new Result();
        m.setCode("0");
        m.setData(data);
        m.setMsg(mess);
        return m;
    }
    public static Result fail(String mess) {
        Result m = new Result();
        m.setCode("-1");
        m.setData(null);
        m.setMsg(mess);
        return m;
    }
    public static Result fail(String mess, Object data) {
        Result m = new Result();
        m.setCode("-1");
        m.setData(data);
        m.setMsg(mess);
        return m;
    }
}

4. Integrate Shiro + JWT and share sessions

Considering that clustering and load balancing may be required later, session sharing is required. For Shiro’s cache and session information, we generally consider using redis to store these data. Therefore, we need to integrate not only Shiro but also redis. In the open source project, we found a starter that can quickly integrate Shiro redis with simple configuration. It is also recommended here.

Because what we need to do is to separate the skeleton of the project from the front and back, we generally use token or JWT as a cross domain authentication solution. Therefore, in the process of integrating Shiro, we need to introduce JWT’s authentication process.

Then we begin to integrate:

We use a jar package of Shiro redis spring boot starter. See the official documentation for the specific tutorial: https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring -boot-starter

Step 1: import Shiro redis starter package, JWT toolkit, and hutool toolkit to simplify development.

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis-spring-boot-starter</artifactId>
    <version>3.2.1</version>
</dependency>
<!--  Hutool tool class -- >
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.3</version>
</dependency>
<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Step 2: write configuration:

ShiroConfig

  • com.markerhub.config.ShiroConfig
/**
 *Shiro enable annotation interception controller
 */
@Configuration
public class ShiroConfig {
    @Autowired
    JwtFilter jwtFilter;
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(redisCacheManager);
        /*
         *Close Shiro's own session. See the document for details
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); //  Permissions are verified mainly by annotation
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

    //Enable annotation agent (it seems to be enabled by default, and can not be used)
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        return creator;
    }
}

Shiroconfig above, we have mainly done several things:

  1. Redissessiondao and rediscachemanager are introduced to solve the problem that Shiro’s permission data and session information can be saved in redis and realize session sharing.
  2. The SessionManager and defaultwebsecuritymanager are rewritten. At the same time, in the defaultwebsecuritymanager, in order to turn off Shiro’s own session mode, we need to set it to false, so that users can no longer log in to Shiro through session mode. You will log in with JWT credentials later.
  3. In shirofilterchaindefinition, we no longer intercept the controller access path in the form of coding, but all routes need to pass through the jwtfilter filter, and then judge whether the request header contains JWT information. If yes, log in and skip. After skipping, the Shiro annotation in the controller is intercepted again, such as @ requireauthentication, so as to control access permissions.

Next, let’s talk about the accountrealm and jwtfilter in shiroconfig.

AccountRealm

Accountrealm is the logic of Shiro’s login or permission verification. It is the core. We need to rewrite three methods, namely

  • Supports: to enable realm to support JWT’s voucher verification
  • Dogetauthorizationinfo: permission verification
  • Dogetauthenticationinfo: login authentication verification

Let’s take a general look at the code of accountrealm, and then analyze it one by one:

  • com.markerhub.shiro.AccountRealm
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserService userService;
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwt = (JwtToken) token;
        log.info("jwt----------------->{}", jwt);
        String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();
        User user = userService.getById(Long.parseLong(userId));
        if(user == null) {
            Throw new unknownaccountexception ("account does not exist!");
        }
        if(user.getStatus() == -1) {
            Throw new lockedaccountexception ("account has been locked!");
        }
        AccountProfile profile = new AccountProfile();
        BeanUtil.copyProperties(user, profile);
        log.info("profile----------------->{}", profile.toString());
        return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
    }
}

In fact, the main method is dogetauthenticationinfo login authentication. You can see that we get the user information through JWT, judge the user’s status, and finally throw the corresponding exception information. Otherwise, it is encapsulated into simpleauthenticationinfo and returned to Shiro.
Next, we will gradually analyze the new classes:

1. Shiro’s default support is usernamepasswordtoken, and now we use JWT, so here we customize a jwttoken to complete Shiro’s support method.

JwtToken

  • com.markerhub.shiro.JwtToken
public class JwtToken implements AuthenticationToken {
    private String token;
    public JwtToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

2. Jwtutils is a tool class for generating and verifying JWT. Some JWT related key information is configured from the project configuration file:

@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {
    private String secret;
    private long expire;
    private String header;
    /**
     *Generate JWT token
     */
    public String generateToken(long userId) {
    ...
    }
    
    //Get JWT's information
    public Claims getClaimByToken(String token) {
    ...
    }
    
    /**
     *Whether the token expires
     *@ return true: expired
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

3. In accountrealm, we also use accountprofile, which is a carrier of user information returned after successful login,

AccountProfile

  • com.markerhub.shiro.AccountProfile
@Data
public class AccountProfile implements Serializable {
    private Long id;
    private String username;
    private String avatar;
}

Step 3: OK. After the basic verification route is completed, we need a small amount of basic information configuration:

shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379
markerhub:
  jwt:
    #Encryption key
    secret: f4e2e52034348f86b67cde581c0f9eb5
    #Valid duration of token, 7 days, in seconds
    expire: 604800
    header: token

Step 4: in addition, if your project uses spring boot devtools, you need to add a configuration file, create a new folder meta-inf in the resources directory, and then create a new file spring devtools.properties, so that no error will be reported during hot restart.

  • resources/META-INF/spring-devtools.properties
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

JwtFilter

Step 5: define JWT filter jwtfilter.

This filter is our focus. Here we inherit Shiro’s built-in authenticatingfilter, which can be built with automatic login method. Some students can inherit basichttpauthenticationfilter.

We need to rewrite several methods:

  1. Createtoken: to realize login, we need to generate our custom supported jwttoken
  2. Onaccessdenied: intercept verification. When there is no authorization in the header, we pass directly without automatic login; When there is, we first verify the validity of JWT. If there is no problem, we directly execute the executelogin method to realize automatic login
  3. Onloginfailure: the method entered when the login exception occurs. We directly encapsulate the exception information and throw it
  4. Prehandle: pre interception of the interceptor, because we are a front-end and back-end analysis project. In addition to cross domain global configuration, we also need to provide cross domain support in the interceptor. In this way, the interceptor will not be restricted before entering the controller.

Let’s look at the overall Code:

  • com.markerhub.shiro.JwtFilter
@Component
public class JwtFilter extends AuthenticatingFilter {
    @Autowired
    JwtUtils jwtUtils;
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //Get token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return null;
        }
        return new JwtToken(jwt);
    }
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader("Authorization");
        if(StringUtils.isEmpty(token)) {
            return true;
        } else {
            //Determine whether it has expired
            Claims claim = jwtUtils.getClaimByToken(token);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                Throw new expiredcredentialsexception ("token has expired, please log in again!");
            }
        }
        //Perform automatic login
        return executeLogin(servletRequest, servletResponse);
    }
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        try {
            //Exception handling login failure
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Result r = Result.fail(throwable.getMessage());
            String json = JSONUtil.toJsonStr(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }
    /**
     *Cross domain support
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //When cross domain, we will first send an options request. Here, we directly return to the normal state for the options request
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

So here, our Shiro has been integrated and used JWT for identity verification.

5. Exception handling

Sometimes it is inevitable that the server will report an error. If the exception handling mechanism is not configured, the 5xx page of Tomcat or nginx will be returned by default. It is not very friendly to ordinary users and users do not know anything. At this time, we programmers need to design and return a friendly and simple format to the front end.

The handling method is as follows: use @ controlleradvice for unified exception handling, and @ exceptionhandler (value = runtimeException. Class) to specify various types of exceptions captured. The handling of this exception is global, and all similar exceptions will be handled in this place.

  • com.markerhub.common.exception.GlobalExceptionHandler

Step 2: define global exception handling, @ controlleradvice means define global controller exception handling, @ exceptionhandler means targeted exception handling, which can be targeted for each exception.

/**
 *Global exception handling
 */
@Slf4j
@RestControllerAdvice
public class GlobalExcepitonHandler {
    //Catch Shiro's exception
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
        return Result.fail(401, e.getMessage(), null);
    }
    /**
     *Exception handling assert
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) throws IOException {
        Log. Error ("assert exception: -------------- > {}", e.getmessage());
        return Result.fail(e.getMessage());
    }
    /**
     *@ validated validation error exception handling
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
        Log. Error ("runtime exception: -------------- >", e);
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) throws IOException {
        Log. Error ("runtime exception: -------------- >", e);
        return Result.fail(e.getMessage());
    }
}

We caught several exceptions above:

  • Shiroexception: exceptions thrown by Shiro, such as no permission and user login exceptions
  • Illegalargumentexception: handles the exception of assert
  • Methodargumentnotvalidexception: handle the exception of entity verification
  • RuntimeException: catch other exceptions

6. Entity verification

When we submit the form data, we can use some JS plug-ins such as jQuery validate to verify the front end, while we can use hibernate validator to verify the back end.

If we use the springboot framework as the basis, hibernate validator has been automatically integrated.

So what does it look like?

Step 1: first, add the corresponding verification rules on the attributes of the entity, such as:

@TableName("m_user")
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Notblank (message = "nickname cannot be empty")
    private String username;
    @Notblank (message = "mailbox cannot be empty")
    @Email (message = "the mailbox format is incorrect")
    private String email;
    
    ...
}

Step 2: Here we use @ validated annotation. If the entity does not meet the requirements, the system will throw an exception, and we will catch methodargumentnotvalidexception in our exception handling.

  • com.markerhub.controller.UserController
/**
 *Test entity verification
 * @param user
 * @return
 */
@PostMapping("/save")
public Object testUser(@Validated @RequestBody User user) {
    return user.toString();
}

7. Cross domain problem

Because it is front-end and back-end analysis, cross domain problems can not be avoided. We directly conduct global cross domain processing in the background:

  • com.markerhub.config.CorsConfig
/**
 *Solve cross domain problems
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

OK, because the interface of our system development is relatively simple, so I don’t integrate swagger2. It’s also relatively simple. Now let’s go directly to our topic and write the login interface.

8. Login interface development

In fact, the logic of login is very simple. You only need to accept the account and password, and then generate JWT from the user’s ID and return it to the previous paragraph. In order to delay the subsequent JWT, we put JWT on the header. The specific codes are as follows:

  • com.markerhub.controller.AccountController
@RestController
public class AccountController {
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserService userService;
    /**
     *Default account password: markerhub / 111111
     *
     */
    @CrossOrigin
    @PostMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {
        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notnull (user, "user does not exist");
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
            Return result.fail ("wrong password!");
        }
        String jwt = jwtUtils.generateToken(user.getId());
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
        //The user can another interface
        return Result.succ(MapUtil.builder()
                .put("id", user.getId())
                .put("username", user.getUsername())
                .put("avatar", user.getAvatar())
                .put("email", user.getEmail())
                .map()
        );
    }
    
    //Exit
    @GetMapping("/logout")
    @RequiresAuthentication
    public Result logout() {
        SecurityUtils.getSubject().logout();
        return Result.succ(null);
    }
}

Interface test:
Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

9. Blog interface development

Our skeleton has been completed. Next, we can add our business interface. Let me take a simple blog list and blog details page as an example:

  • com.markerhub.controller.BlogController
@RestController
public class BlogController {
    @Autowired
    BlogService blogService;
    @GetMapping("/blogs")
    public Result blogs(Integer currentPage) {
        if(currentPage == null || currentPage < 1) currentPage = 1;
        Page page = new Page(currentPage, 5)
        IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
        return Result.succ(pageData);
    }
    @GetMapping("/blog/{id}")
    public Result detail(@PathVariable(name = "id") Long id) {
        Blog blog = blogService.getById(id);
        Assert.notnull (blog, "this blog has been deleted!");
        return Result.succ(blog);
    }
    
    @RequiresAuthentication
@PostMapping("/blog/edit")
public Result edit(@Validated @RequestBody Blog blog) {
    System.out.println(blog.toString());
    Blog temp = null;
    if(blog.getId() != null) {
        temp = blogService.getById(blog.getId());
        Assert.istrue (temp. Getuserid() = = shiruotil. Getprofile(). Getid(), "no permission to edit");
    } else {
        temp = new Blog();
        temp.setUserId(ShiroUtil.getProfile().getId());
        temp.setCreated(LocalDateTime.now());
        temp.setStatus(0);
    }
    BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
    blogService.saveOrUpdate(temp);
    Return result.succ ("operation succeeded", null);
}
}

Note that @ requiresauthentication describes the interfaces that can only be accessed after logging in. Other interfaces that need permission can add Shiro’s related annotations.
The interface is relatively simple. We won’t talk more about it. It’s basically just adding, deleting, modifying and checking. Note that the edit method is a restricted resource that requires login to operate.

Interface test:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

10. Back end summary

Well, it seems a little hurried to finish a basic skeleton in an article, but the basic things are already here. Later, we will develop our front-end interface.

Project code: https://github.com/MarkerHub/…

Project video: https://www.bilibili.com/vide…

Vue front end page development

1. Foreword

Next, let’s complete some functions of the front end of vueblog. The techniques that may be used are as follows:

  • vue
  • element-ui
  • axios
  • mavon-editor
  • markdown-it
  • github-markdown-css

The practice of this project needs a little bit of Vue foundation. I hope you can understand some instructions of Vue, so it will be much easier for us to explain.

2. Project demonstration

Let’s first look at what the project we need to complete looks like. Considering that many students don’t master the style enough, I try to use the style of the native component of element UI to complete the interface of the whole blog. Not much to say, go straight to the figure above:

Online experience: https://markerhub.com:8083

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

3. Environmental preparation

The tall building rises from the ground. Let’s finish it step by step. First, we install Vue. My practice environment is windows 10.

1. First, let’s go to the official website of node.js( https://nodejs.org/zh-cn/ ), download the latest long-term version and run it directly. After the installation is completed, we already have the environment of node and NPM.

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

After installation, check the following version information:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

2. Next, let’s install the environment of Vue

#Install Taobao NPM
npm install -g cnpm --registry=https://registry.npm.taobao.org
#Vue cli installation dependency package
cnpm install --g vue-cli

4. New project

#Open the visual management tool interface of Vue
vue ui

Above, we have installed Taobao NPM and cnpm respectively to improve the speed of our installation. Vue UI is a visual project management tool added to @ Vue / cli3.0, which can run projects, package projects, check and other operations. For beginners, you can remember less commands, ha ha.
3. Create a vueblog Vue project

After running the Vue UI, it will open one for us http://localhost:8080 Page:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Then switch to create. Note that the created directory should be at the same level as the Vue UI you run. This facilitates management and switching. Then click the button [create new item here]

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

In the next step, enter the project name “vueblog Vue” in the project folder. Do not change anything else. Click next, select [manual], and then click next, as shown in the figure. Click the button, check the upper routing router and status management vuex, and remove the verification of JS.

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

Next, select use history mode for router, click create project, and then select create project without saving preset in the pop-up window to enter project creation.

After a few moments, the project is initialized. In the above steps, we created a Vue project and installed router and vuex. So we can use it directly later.

Let’s take a look at the whole vueblog Vue project structure

├── README.md              Project introduction
├── index.html             Entry page
├── build                Build script directory
│    ├── build-server.js           Run the local build server to access the built page
│    ├── build.js              Production environment build script
│    ├── dev-client.js            The development server hot overload script is mainly used to realize the automatic page refresh in the development stage
│    ├── dev-server.js            Run local development server
│    ├── utils.js              Build relevant tools and methods
│    ├── webpack.base.conf.js        Wabpack basic configuration
│    ├── webpack.dev.conf.js         Wabpack development environment configuration
│    └── webpack.prod.conf.js        Wabpack production environment configuration
├── config               Project configuration
│    ├── dev.env.js             Development environment variables
│    ├── index.js              Project profile
│    ├── prod.env.js             Production environment variables
│    └── test.env.js             Test environment variables
├── mock                Mock data directory
│  └── hello.js
├── package.json            NPM package configuration file, which defines the NPM script, dependent package and other information of the project
├── src                 Source directory 
│    ├── main.js               Entry JS file
│    ├── app.vue               Root component
│    ├── components             Common component directory
│  │  └── title.vue
│    ├── assets               Resource directory, where the resources will be built by wabpack
│  │  └── images
│  │    └── logo.png
│    ├── routes               Front end routing
│  │  └── index.js
│    ├── store                Application level data (state) state management
│  │  └── index.js
│    └── views                page directory
│    ├── hello.vue
│    └── notfound.vue
├── static               Pure static resources will not be built by wabpack.
└── test                Test file directory (unit&e2e)
  └── unit                unit testing 
    ├── index.js              Entry script
    ├── karma.conf.js            Karma profile
    └── specs                Single test case directory
      └── Hello.spec.js

5. Installing element UI

Next, we introduce the element UI component( https://element.eleme.cn )In this way, we can get good-looking Vue components and develop good-looking blog interfaces.

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

The command is simple:

#Switch to project root
cd vueblog-vue
#Installing element UI
cnpm install element-ui --save

Then we open main.js in the SRC directory of the project and introduce the element UI dependency.

import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)

In this way, we can happily select components on the official website and copy the code to our project for direct use.

6. Installing Axios

Next, let’s install Axios( http://www.axios-js.com/ ), Axios is an HTTP Library Based on promise, so when we connect the front and back ends, using this tool can improve our development efficiency.

Installation command:

cnpm install axios --save

Then again, we introduce Axios globally in main. JS.

import axios from 'axios'
Vue.prototype.$axios = axios //

In the component, we can initiate our request through this. $Axios. Get().

7. Page routing

Next, we first define the route and page, because we only do a simple blog project with few pages, so we can directly define it first and then develop it slowly, so we can use it directly where links are needed:

We define several pages under the views folder:

  • Blogdetail.vue (blog detail page)
  • Blogedit.vue (edit blog)
  • Blogs.vue (blog list)
  • Login.vue (login page)

Then configure the routing center:

  • routerindex.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import BlogDetail from '../views/BlogDetail.vue'
import BlogEdit from '../views/BlogEdit.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Index',
    redirect: { name: 'Blogs' }
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/blogs',
    name: 'Blogs',
    //Lazy loading
    component: () => import('../views/Blogs.vue')
  },
  {
    Path: '/ blog / add', // note that it should be placed before path: '/ blog /: blogid'
    name: 'BlogAdd',
    meta: {
      requireAuth: true
    },
    component: BlogEdit
  },
  {
    path: '/blog/:blogId',
    name: 'BlogDetail',
    component: BlogDetail
  },
  {
    path: '/blog/:blogId/edit',
    name: 'BlogEdit',
    meta: {
      requireAuth: true
    },
    component: BlogEdit
  }
];
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router

Next, let’s develop our page. Among them, meta: requireauth: true indicates that it is a restricted resource that can be accessed only after the login word. We will use it later when intercepting routing permissions.

8. Login page

Next, let’s make a landing page. We can directly find the form component on the official website of element UI. The landing page has two input boxes and a submit button, which is relatively simple. Then we’d better bring JS verification of the page. Emmm, I posted the code directly~~

  • views/Login.vue
<template>
  <div>
    <el-container>
      <el-header>
        <router-link to="/blogs">
        <img
             style="height: 60%; margin-top: 10px;">
        </router-link>
      </el-header>
      <el-main>
        <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px"
                 class="demo-ruleForm">
          < El form item label = "username" prop = "username" >
            <el-input type="text" maxlength="12" v-model="ruleForm.username"></el-input>
          </el-form-item>
          < El form item label = "password" prop = "password" >
            <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item>
            < El button type = "primary" @ Click = "submitform ('ruleform ')" > login < / El button >
            < El button @ Click = "resetform ('ruleform ')" > Reset < / El button >
          </el-form-item>
        </el-form>
      </el-main>
    </el-container>
  </div>
</template>
<script>
  export default {
    name: 'Login',
    data() {
      var validatePass = (rule, value, callback) => {
        if (value === '') {
          Callback (new error ('Please enter password ');
        } else {
          callback();
        }
      };
      return {
        ruleForm: {
          password: '111111',
          username: 'markerhub'
        },
        rules: {
          password: [
            {validator: validatePass, trigger: 'blur'}
          ],
          username: [
            {required: true, message: 'please enter user name', trigger: 'blur'},
            {Min: 3, Max: 12, message: 'length between 3 and 12 characters', trigger:' blur '}
          ]
        }
      };
    },
    methods: {
      submitForm(formName) {
        const _this = this
        this.$refs[formName].validate((valid) => {
          if (valid) {
            //Submission logic
            this.$axios.post('http://localhost:8081/login', this.ruleForm).then((res)=>{
              const token = res.headers['authorization']
              _this.$store.commit('SET_TOKEN', token)
              _this.$store.commit('SET_USERINFO', res.data.data)
              _this.$router.push("/blogs")
            })
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      },
      resetForm(formName) {
        this.$refs[formName].resetFields();
      }
    },
    mounted() {
      this.$notify({
        Title: 'look here:',
        Message: 'attention to the official account: MarkerHub, reply to vueblog, receive project information and source code',
        duration: 1500
      });
    }
  }
</script>

I can’t find any good way to explain it. Then post the code and explain it again.
In the above code, two main things are actually done

1. Form verification

2. Click login event of login button

The form verification rules are good. It is written in a fixed way. Just check the component of element UI. Let’s analyze the code after initiating login:

const token = res.headers['authorization']
_this.$store.commit('SET_TOKEN', token)
_this.$store.commit('SET_USERINFO', res.data.data)
_this.$router.push("/blogs")

Get the token information from the returned result request header, and then use the store to submit the status of the token and user information. After completing the operation, we adjusted the / blogs route, that is, the blog list page.

State synchronization of token

So in store / index.js, the code is as follows:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    token: '',
    userInfo: JSON.parse(sessionStorage.getItem("userInfo"))
  },
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
      localStorage.setItem("token", token)
    },
    SET_USERINFO: (state, userInfo) => {
      state.userInfo = userInfo
      sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
    },
    REMOVE_INFO: (state) => {
      localStorage.setItem("token", '')
      sessionStorage.setItem("userInfo", JSON.stringify(''))
      state.userInfo = {}
    }
  },
  getters: {
    getUser: state => {
      return state.userInfo
    }
  },
  actions: {},
  modules: {}
})

To store tokens, we use localstorage to store user information, and we use sessionstorage. After all, we don’t need to save the user information for a long time. After saving the token information, we can initialize the user information at any time. Of course, because this project is a relatively simple project. Considering beginners, I didn’t do many relatively complex packages and functions. Of course, after learning this project, I want to continue to go deeper and complete it. I can learn and transform it by myself.

Define global Axios interceptors

Click the login button to initiate the login request, and the data is returned when it is successful. If the password is wrong, should we also pop up a message prompt. In order to make this error pop-up apply to all places, I made a post interceptor for Axios, that is, when returning data, if the code or status of the result is abnormal, I will respond to the pop-up prompt.

Create a file axios.js (the same level as main.js) in the SRC directory to define the interception of Axios:

import axios from 'axios'
import Element from "element-ui";
import store from "./store";
import router from "./router";
axios.defaults.baseURL='http://localhost:8081'
axios.interceptors.request.use(config => {
  Console.log ("pre interception")
  //Request headers can be set uniformly
  return config
})
axios.interceptors.response.use(response => {
    const res = response.data;
    Console.log ("post intercept")
    //When the code of the result is 200
    if (res.code === 200) {
      return response
    } else {
      //Pop up exception information
      Element.Message({
        message: response.data.msg,
        type: 'error',
        duration: 2 * 1000
      })
      //Directly refuse to return the result information below
      return Promise.reject(response.data.msg)
    }
  },
  error => {
    console.log('err' + error)// for debug
    if(error.response.data) {
      error.message = error.response.data.msg
    }
    //Whether to log in or prompt others according to the request status
    if (error.response.status === 401) {
      store.commit('REMOVE_INFO');
      router.push({
        path: '/login'
      });
      Error. Message = 'please login again';
    }
    if (error.response.status === 403) {
      Error. Message = 'insufficient permission, unable to access';
    }
    Element.Message({
      message: error.message,
      type: 'error',
      duration: 3 * 1000
    })
    return Promise.reject(error)
  })

In fact, pre interception can uniformly assemble the token information of the header for all requests requiring permission. In this way, it does not need to be reconfigured before use. My small project is relatively small, so I’d better avoid it~

Then import axios.js into main.js

Import '. / Axios. JS' // request interception

Because the entity returned by the back end is result, the code is 200 in succ and 400 in fail, you can judge whether the result is normal here. In addition, when the permission is insufficient, you can judge whether the result is normal by the status code of the request result. It’s all done here.

The effects of abnormal login are as follows:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

9. Blog list

After logging in, directly enter the blog list page, and then load the data of the blog list to render it. At the same time, we need to display the user information in the page header. Because this module is used in many places, we extract the user information in the page header separately as a component.

Header user information

Then, let’s first complete the user information of the header, which should contain three parts of information: ID, avatar and user name. These information already exists after logging in sessionstorage. Therefore, we can get the user information through the getters of the store.

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

It doesn’t seem very complicated. We post the code:

  • componentsHeader.vue
<template>
  <div class="m-content">
    <h3>Welcome to markerhub's blog</h3>
    <div class="block">
      <el-avatar :size="50" :src="user.avatar"></el-avatar>
      <div>{{ user.username }}</div>
    </div>
    <div class="maction">
      < El link href = "/ blogs" > Home Page < / El link >
      <el-divider direction="vertical"></el-divider>
      <span>
          < El link type = "success" href = "/ blog / add": Disabled = "! Haslogin" > publish an article < / El link >
        </span>
      <el-divider direction="vertical"></el-divider>
      <span v-show="!hasLogin">
          < El link type = "primary" href = "/ login" > log in < / El link >
        </span>
      <span v-show="hasLogin">
          < El link type = "danger" @ Click = "logout" > Exit < / El link >
        </span>
    </div>
  </div>
</template>
<script>
  export default {
    name: "Header",
    data() {
      return {
        hasLogin: false,
        user: {
          Username: 'please login first',
          avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
        },
        blogs: {},
        currentPage: 1,
        total: 0
      }
    },
    methods: {
      logout() {
        const _this = this
        this.$axios.get('http://localhost:8081/logout', {
          headers: {
            "Authorization": localStorage.getItem("token")
          }
        }).then((res) => {
          _this.$store.commit('REMOVE_INFO')
          _this.$router.push('/login')
        });
      }
    },
    created() {
      if(this.$store.getters.getUser.username) {
        this.user.username = this.$store.getters.getUser.username
        this.user.avatar = this.$store.getters.getUser.avatar
        this.hasLogin = true
      }
    }
  }
</script>

The above code created () initializes the user’s information, controls the switching of login and exit buttons through the state of haslogin, and disables the link to publish articles, so that the user’s information can be displayed.
Then there is an exit button. There is a logout () method in methods. The logic is relatively simple. You can directly access / logout. Because we have set the base URL of the Axios request in axios.js before, we no longer need the prefix of the link here. Because it is a restricted resource that can only be accessed after login, authorization is brought in the header. The returned result is clear of the user information and token information in the store, and jump to the login page.

Then, the page that needs header user information only needs a few steps:

import Header from "@/components/Header";
data() {
  components: {Header}
}
Then the components are invoked in the template.
<Header></Header>

Blog paging

Next is the list page, which needs to be paged. The list is directly used in the element UITimelineComponent as our list style, it’s still very nice. And our paging component.

Several pieces of information are required:

  • Paging information
  • Blog list content, including ID, title, summary and creation time
  • viewsBlogs.vue
<template>
  <div class="m-container">
    <Header></Header>
    <div class="block">
      <el-timeline>
        <el-timeline-item v-bind:timestamp="blog.created" placement="top" v-for="blog in blogs">
          <el-card>
            <h4><router-link :to="{name: 'BlogDetail', params: {blogId: blog.id}}">{{blog.title}}</router-link></h4>
            <p>{{blog.description}}</p>
          </el-card>
        </el-timeline-item>
      </el-timeline>
     
    </div>
    <el-pagination class="mpage"
      background
      layout="prev, pager, next"
      :current-page=currentPage
      :page-size=pageSize
      @current-change=page
      :total="total">
    </el-pagination>
  </div>
</template>
<script>
  import Header from "@/components/Header";
  export default {
    name: "Blogs",
    components: {Header},
    data() {
      return {
        blogs: {},
        currentPage: 1,
        total: 0,
        pageSize: 5
      }
    },
    methods: {
      page(currentPage) {
        const _this = this
        this.$axios.get('http://localhost:8081/blogs?currentPage=' + currentPage).then((res) => {
          console.log(res.data.data.records)
          _this.blogs = res.data.data.records
          _this.currentPage = res.data.data.current
          _this.total = res.data.data.total
          _this.pageSize = res.data.data.size
        })
      }
    },
    mounted () {
      this.page(1);
    }
  }
</script>

Data () directly defines the blog list, blogs, and some paging information. The calling interface page (CurrentPage) for paging is defined in methods (). The parameter is the page number CurrentPage to be adjusted. After the result is obtained, it can be assigned directly. Then initialize the first page this.page (1) in the mounted () method. Perfect. Using the element UI component is simple and fast ha ha!
Note the title. Here we add a link, using the < router link > tag.

10. Blog editor (post)

We click the post blog link to adjust to the / blog / add page. Here we need to use a markdown editor. In the Vue component, MAVON editor is easier to use, so we can use it directly. First, install the MAVON editor related components:

Install MAVON editor

Vue based markdown editor MAVON editor

cnpm install mavon-editor --save

Then register globally in main.js:

//Global registration
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)

OK, let’s define our blog form:

<template>
  <div class="m-container">
    <Header></Header>
    <div class="m-content">
      <el-form ref="editForm" status-icon :model="editForm" :rules="rules" label-width="80px">
        < El form item label = "title" prop = "title" >
          <el-input v-model="editForm.title"></el-input>
        </el-form-item>
        < El form item label = "summary" prop = "description" >
          <el-input type="textarea" v-model="editForm.description"></el-input>
        </el-form-item>
        < El form item label = "content" prop = "content" >
          <mavon-editor v-model="editForm.content"/>
        </el-form-item>
        <el-form-item>
          < El button type = "primary" @ Click = "submitform()" > create < / El button >
          < El button > cancel < / El button >
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<script>
  import Header from "@/components/Header";
  export default {
    name: "BlogEdit",
    components: {Header},
    data() {
      return {
        editForm: {
          id: null,
          title: '',
          description: '',
          content: ''
        },
        rules: {
          title: [
            {required: true, message: 'please enter a title', trigger: 'blur'},
            {Min: 3, Max: 50, message: 'length between 3 and 50 characters', trigger:' blur '}
          ],
          description: [
            {required: true, message: 'please enter summary', trigger: 'blur'}
          ]
        }
      }
    },
    created() {
      const blogId = this.$route.params.blogId
      const _this = this
      if(blogId) {
        this.$axios.get('/blog/' + blogId).then((res) => {
          const blog = res.data.data
          _this.editForm.id = blog.id
          _this.editForm.title = blog.title
          _this.editForm.description = blog.description
          _this.editForm.content = blog.content
        });
      }
    },
    methods: {
      submitForm() {
        const _this = this
        this.$refs.editForm.validate((valid) => {
          if (valid) {
            this.$axios.post('/blog/edit', this.editForm, {
              headers: {
                "Authorization": localStorage.getItem("token")
              }
            }).then((res) => {
              _ This. $alert ('operation succeeded ',' prompt '{
                Confirmbuttontext: 'OK',
                callback: action => {
                  _this.$router.push("/blogs")
                }
              });
            });
          } else {
            console.log('error submit!!');
            return false;
          }
        })
      }
    }
  }
</script>

The logic is still simple. Verify the form, then click the button to submit the form, pay attention to the authorization information added to the header, return the result, pop-up prompt that the operation is successful, and then jump to the blog list page. EMM is no different from writing Ajax. Be familiar with some instructions of Vue.
Then, because editing and adding are the same page, there is a create () method, such as obtaining the ID with blogid 7 from the edit connection / blog / 7 / edit. Then echo the blog information. The acquisition method is const blogid = this. $route.params.blogid.

By the way, since MAVON editor has been registered globally, we can directly use the component:

<mavon-editor v-model="editForm.content"/>

The effects are as follows:
Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

11. Blog details

The blog information needs to be echoed in the blog details. Then there is a problem that the blog content transmitted from the back end is in markdown format. We need to render and display it. Here, we use a plug-in markdown it to parse MD documents, and then import github-markdown-c, the so-called MD style.

The method is as follows:

#Used to parse MD documents
cnpm install markdown-it --save
#MD style
cnpm install github-markdown-css

Then you can use it where you need to render:

  • viewsBlogDetail.vue
<template>
  <div class="m-container">
    <Header></Header>
    <div class="mblog">
      <h2>{{ blog.title }}</h2>
      < El link icon = "El icon edit" V-IF = "ownblog" > < router link: to = "{Name: 'blogedit', params: {blogid: blog. ID}}" > Edit < / router link > < / El link >
      <el-divider></el-divider>
      <div class="content markdown-body" v-html="blog.content"></div>
    </div>
  </div>
</template>
<script>
  Import 'GitHub markdown CSS / GitHub markdown. CSS' // then add the style markdown body
  import Header from "@/components/Header";
  export default {
    name: "BlogDetail",
    components: {
      Header
    },
    data() {
      return {
        blog: {
          userId: null,
          title: "",
          description: "",
          content: ""
        },
        ownBlog: false
      }
    },
    methods: {
      getBlog() {
        const blogId = this.$route.params.blogId
        const _this = this
        this.$axios.get('/blog/' + blogId).then((res) => {
          console.log(res)
          console.log(res.data.data)
          _this.blog = res.data.data
          var MarkdownIt = require('markdown-it'),
            md = new MarkdownIt();
          var result = md.render(_this.blog.content);
          _this.blog.content = result
          //Judge whether it is your own article and whether it can be edited
          _this.ownBlog =  (_this.blog.userId === _this.$store.getters.getUser.id)
        });
      }
    },
    created() {
      this.getBlog()
    }
  }
</script>

The specific logic is quite simple. In the initialization create () method, the getBlog () method is called to request the blog details interface, and the returned blog details content is rendered through the markdown-it tool.

Re import styles:

import 'github-markdown.css'

Then add class markdown body to the div of content.
The effects are as follows:

Super detailed! 4 hours to develop a springboot + Vue front and rear end separation blog project!!

In addition, a small edit button is added under the title to judge whether the button is displayed through ownblog (judge whether the blog author is the same as the login user).

12. Routing permission interception

After the page has been developed, let’s control which pages can jump after login. If you don’t log in, you can directly redirect to the login page. Therefore, we define a JS file in the SRC Directory:

  • srcpermission.js
import router from "./router";
//The route determination login is based on the parameters of the route configuration file
router.beforeEach((to, from, next) => {
  If (to. Matched. Some (record = > record. Meta. Requireauth)) {// judge whether the route requires login permission
    const token = localStorage.getItem("token")
    console.log("------------" + token)
    If (token) {// judge whether the current token exists; log in to the saved token
      if (to.path === '/login') {
      } else {
        next()
      }
    } else {
      next({
        path: '/login'
      })
    }
  } else {
    next()
  }
})

By redefining the meta information during page routing, we specify requireauth: true, which can be accessed only after login. Therefore, here we judge the status of the token before each routing (router. Beforeeach) and think whether we need to jump to the login page.

{
  Path: '/ blog / add', // note that it should be placed before path: '/ blog /: blogid'
  name: 'BlogAdd',
  meta: {
    requireAuth: true
  },
  component: BlogEdit
}

Then we import our permission.js in main.js

Import '. / permission. JS' // route interception

13. Front end summary

OK, basically all the pages have been developed. I haven’t posted the CSS style information. You can check it directly on GitHub and clone.

Project summary

Well, the project came here first. It took 3 and a half days to record a corresponding video. Remember to watch it and give it to me for three consecutive days.

Project code: https://github.com/MarkerHub/…

Project video: https://www.bilibili.com/vide…

Recommended Today

SQL exercise 20 – Modeling & Reporting

This blog is used to review and sort out the common topic modeling architecture, analysis oriented architecture and integration topic reports in data warehouse. I have uploaded these reports to GitHub. If you are interested, you can have a lookAddress:https://github.com/nino-laiqiu/TiTanI recorded a relatively complete development process in my hexo blog deployed on GitHub. You can […]