超詳細!4小時開發一個SpringBoot+vue前後端分離部落格專案!!

MarkerHub發表於2020-05-29
作者:呂一明

專案程式碼:https://github.com/MarkerHub/...

專案視訊:https://www.bilibili.com/vide...

轉載請保留此引用,感謝!

前後端分離專案

文章總體分為2大部分,Java後端介面和vue前端頁面,比較長,因為不想分開發布,真正想你4小時學會,哈哈。

先看效果:

圖片

圖片

不多說,開始敲程式碼。

Java後端介面開發

1、前言

從零開始搭建一個專案骨架,最好選擇合適,熟悉的技術,並且在未來易擴充,適合微服務化體系等。所以一般以Springboot作為我們的框架基礎,這是離不開的了。

然後資料層,我們常用的是Mybatis,易上手,方便維護。但是單表操作比較困難,特別是新增欄位或減少欄位的時候,比較繁瑣,所以這裡我推薦使用Mybatis Plus(https://mp.baomidou.com/),為簡化開發而生,只需簡單配置,即可快速進行 CRUD 操作,從而節省大量時間。

作為一個專案骨架,許可權也是我們不能忽略的,Shiro配置簡單,使用也簡單,所以使用Shiro作為我們的的許可權。

考慮到專案可能需要部署多臺,這時候我們的會話等資訊需要共享,Redis是現在主流的快取中介軟體,也適合我們的專案。

然後因為前後端分離,所以我們使用jwt作為我們使用者身份憑證。

ok,我們現在就開始搭建我們的專案腳手架!

技術棧:

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

導圖:https://www.markerhub.com/map/131

2、新建Springboot專案

這裡,我們使用IDEA來開發我們專案,新建步驟比較簡單,我們就不截圖了。

開發工具與環境:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

新建好的專案結構如下,SpringBoot版本使用的目前最新的2.2.6.RELEASE版本

圖片

pom的jar包匯入如下:

<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:專案的熱載入重啟外掛
  • lombok:簡化程式碼的工具

3、整合mybatis plus

接下來,我們來整合mybatis plus,讓專案能完成基本的增刪改查操作。步驟很簡單:可以去官網看看:https://mp.baomidou.com/guide...

第一步:匯入jar包

pom中匯入mybatis plus的jar包,因為後面會涉及到程式碼生成,所以我們還需要匯入頁面模板引擎,這裡我們用的是freemarker。

<!--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程式碼生成器-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.2.0</version>
</dependency>

第二步:然後去寫配置檔案

# 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

上面除了配置資料庫的資訊,還配置了myabtis plus的mapper的xml檔案的掃描路徑,這一步不要忘記了。
第三步:開啟mapper介面掃描,新增分頁外掛

新建一個包:通過@mapperScan註解指定要變成實現類的介面所在的包,然後包下面的所有介面在編譯之後都會生成相應的實現類。PaginationInterceptor是一個分頁外掛。

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

第四步:程式碼生成

如果你沒再用其他外掛,那麼現在就已經可以使用mybatis plus了,官方給我們提供了一個程式碼生成器,然後我寫上自己的引數之後,就可以直接根據資料庫表資訊生成entity、service、mapper等介面和實現類。

  • com.markerhub.CodeGenerator

因為程式碼比較長,就不貼出來了,在程式碼倉庫上看哈!

首先我在資料庫中新建了一個user表:

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);

執行CodeGenerator的main方法,輸入表名:m_user,生成結果如下:
圖片

得到:

圖片

簡潔!方便!經過上面的步驟,基本上我們已經把mybatis plus框架整合到專案中了。

ps:額,注意一下m_blog表的程式碼也生成一下哈。

在UserController中寫個測試:

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

訪問:http://localhost:8080/user/1 獲得結果如下,整合成功!
圖片

3、統一結果封裝

這裡我們用到了一個Result的類,這個用於我們的非同步統一返回的結果封裝。一般來說,結果裡面有幾個要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示異常)
  • 結果訊息
  • 結果資料

所以可得到封裝如下:

  • 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("操作成功");
        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、整合shiro+jwt,並會話共享

考慮到後面可能需要做叢集、負載均衡等,所以就需要會話共享,而shiro的快取和會話資訊,我們一般考慮使用redis來儲存這些資料,所以,我們不僅僅需要整合shiro,同時也需要整合redis。在開源的專案中,我們找到了一個starter可以快速整合shiro-redis,配置簡單,這裡也推薦大家使用。

而因為我們需要做的是前後端分離專案的骨架,所以一般我們會採用token或者jwt作為跨域身份驗證解決方案。所以整合shiro的過程中,我們需要引入jwt的身份驗證過程。

那麼我們就開始整合:

我們使用一個shiro-redis-spring-boot-starter的jar包,具體教程可以看官方文件:https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter

第一步:匯入shiro-redis的starter包:還有jwt的工具包,以及為了簡化開發,我引入了hutool工具包。

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis-spring-boot-starter</artifactId>
    <version>3.2.1</version>
</dependency>
<!-- hutool工具類-->
<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>

第二步:編寫配置:

ShiroConfig

  • com.markerhub.config.ShiroConfig
/**
 * shiro啟用註解攔截控制器
 */
@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);
        /*
         * 關閉shiro自帶的session,詳情見文件
         */
        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"); // 主要通過註解方式校驗許可權
        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;
    }

    // 開啟註解代理(預設好像已經開啟,可以不要)
    @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,我們主要做了幾件事情:

  1. 引入RedisSessionDAO和RedisCacheManager,為了解決shiro的許可權資料和會話資訊能儲存到redis中,實現會話共享。
  2. 重寫了SessionManager和DefaultWebSecurityManager,同時在DefaultWebSecurityManager中為了關閉shiro自帶的session方式,我們需要設定為false,這樣使用者就不再能通過session方式登入shiro。後面將採用jwt憑證登入。
  3. 在ShiroFilterChainDefinition中,我們不再通過編碼形式攔截Controller訪問路徑,而是所有的路由都需要經過JwtFilter這個過濾器,然後判斷請求頭中是否含有jwt的資訊,有就登入,沒有就跳過。跳過之後,有Controller中的shiro註解進行再次攔截,比如@RequiresAuthentication,這樣控制許可權訪問。

那麼,接下來,我們聊聊ShiroConfig中出現的AccountRealm,還有JwtFilter。

AccountRealm

AccountRealm是shiro進行登入或者許可權校驗的邏輯所在,算是核心了,我們需要重寫3個方法,分別是

  • supports:為了讓realm支援jwt的憑證校驗
  • doGetAuthorizationInfo:許可權校驗
  • doGetAuthenticationInfo:登入認證校驗

我們先來總體看看AccountRealm的程式碼,然後逐個分析:

  • 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("賬戶不存在!");
        }
        if(user.getStatus() == -1) {
            throw new LockedAccountException("賬戶已被鎖定!");
        }
        AccountProfile profile = new AccountProfile();
        BeanUtil.copyProperties(user, profile);
        log.info("profile----------------->{}", profile.toString());
        return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
    }
}

其實主要就是doGetAuthenticationInfo登入認證這個方法,可以看到我們通過jwt獲取到使用者資訊,判斷使用者的狀態,最後異常就丟擲對應的異常資訊,否者封裝成SimpleAuthenticationInfo返回給shiro。
接下來我們逐步分析裡面出現的新類:

1、shiro預設supports的是UsernamePasswordToken,而我們現在採用了jwt的方式,所以這裡我們自定義一個JwtToken,來完成shiro的supports方法。

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是個生成和校驗jwt的工具類,其中有些jwt相關的金鑰資訊是從專案配置檔案中配置的:

@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {
    private String secret;
    private long expire;
    private String header;
    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
    ...
    }
    
    // 獲取jwt的資訊
    public Claims getClaimByToken(String token) {
    ...
    }
    
    /**
     * token是否過期
     * @return  true:過期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

3、而在AccountRealm我們還用到了AccountProfile,這是為了登入成功之後返回的一個使用者資訊的載體,

AccountProfile

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

第三步,ok,基本的校驗的路線完成之後,我們需要少量的基本資訊配置:

shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379
markerhub:
  jwt:
    # 加密祕鑰
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效時長,7天,單位秒
    expire: 604800
    header: token

第四步:另外,如果你專案有使用spring-boot-devtools,需要新增一個配置檔案,在resources目錄下新建資料夾META-INF,然後新建檔案spring-devtools.properties,這樣熱重啟時候才不會報錯。

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

圖片

JwtFilter

第五步:定義jwt的過濾器JwtFilter。

這個過濾器是我們的重點,這裡我們繼承的是Shiro內建的AuthenticatingFilter,一個可以內建了可以自動登入方法的的過濾器,有些同學繼承BasicHttpAuthenticationFilter也是可以的。

我們需要重寫幾個方法:

  1. createToken:實現登入,我們需要生成我們自定義支援的JwtToken
  2. onAccessDenied:攔截校驗,當頭部沒有Authorization時候,我們直接通過,不需要自動登入;當帶有的時候,首先我們校驗jwt的有效性,沒問題我們就直接執行executeLogin方法實現自動登入
  3. onLoginFailure:登入異常時候進入的方法,我們直接把異常資訊封裝然後丟擲
  4. preHandle:攔截器的前置攔截,因為我們是前後端分析專案,專案中除了需要跨域全域性配置之外,我們再攔截器中也需要提供跨域支援。這樣,攔截器才不會在進入Controller之前就被限制了。

下面我們看看總體的程式碼:

  • com.markerhub.shiro.JwtFilter
@Component
public class JwtFilter extends AuthenticatingFilter {
    @Autowired
    JwtUtils jwtUtils;
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 獲取 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 {
            // 判斷是否已過期
            Claims claim = jwtUtils.getClaimByToken(token);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                throw new ExpiredCredentialsException("token已失效,請重新登入!");
            }
        }
        // 執行自動登入
        return executeLogin(servletRequest, servletResponse);
    }
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        try {
            //處理登入失敗的異常
            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;
    }
    /**
     * 對跨域提供支援
     */
    @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"));
        // 跨域時會首先傳送一個OPTIONS請求,這裡我們給OPTIONS請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

那麼到這裡,我們的shiro就已經完成整合進來了,並且使用了jwt進行身份校驗。

5、異常處理

有時候不可避免伺服器報錯的情況,如果不配置異常處理機制,就會預設返回tomcat或者nginx的5XX頁面,對普通使用者來說,不太友好,使用者也不懂什麼情況。這時候需要我們程式設計師設計返回一個友好簡單的格式給前端。

處理辦法如下:通過使用@ControllerAdvice來進行統一異常處理,@ExceptionHandler(value = RuntimeException.class)來指定捕獲的Exception各個型別異常 ,這個異常的處理,是全域性的,所有類似的異常,都會跑到這個地方處理。

  • com.markerhub.common.exception.GlobalExceptionHandler

步驟二、定義全域性異常處理,@ControllerAdvice表示定義全域性控制器異常處理,@ExceptionHandler表示針對性異常處理,可對每種異常針對性處理。

/**
 * 全域性異常處理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExcepitonHandler {
    // 捕捉shiro的異常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
        return Result.fail(401, e.getMessage(), null);
    }
    /**
     * 處理Assert的異常
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) throws IOException {
        log.error("Assert異常:-------------->{}",e.getMessage());
        return Result.fail(e.getMessage());
    }
    /**
     * @Validated 校驗錯誤異常處理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
        log.error("執行時異常:-------------->",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("執行時異常:-------------->",e);
        return Result.fail(e.getMessage());
    }
}

上面我們捕捉了幾個異常:

  • ShiroException:shiro丟擲的異常,比如沒有許可權,使用者登入異常
  • IllegalArgumentException:處理Assert的異常
  • MethodArgumentNotValidException:處理實體校驗的異常
  • RuntimeException:捕捉其他異常

6、實體校驗

當我們表單資料提交的時候,前端的校驗我們可以使用一些類似於jQuery Validate等js外掛實現,而後端我們可以使用Hibernate validatior來做校驗。

我們使用springboot框架作為基礎,那麼就已經自動整合了Hibernate validatior。

那麼用起來啥樣子的呢?

第一步:首先在實體的屬性上新增對應的校驗規則,比如:

@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 = "暱稱不能為空")
    private String username;
    @NotBlank(message = "郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    private String email;
    
    ...
}

第二步 :這裡我們使用@Validated註解方式,如果實體不符合要求,系統會丟擲異常,那麼我們的異常處理中就捕獲到MethodArgumentNotValidException。

  • com.markerhub.controller.UserController
/**
 * 測試實體校驗
 * @param user
 * @return
 */
@PostMapping("/save")
public Object testUser(@Validated @RequestBody User user) {
    return user.toString();
}

7、跨域問題

因為是前後端分析,所以跨域問題是避免不了的,我們直接在後臺進行全域性跨域處理:

  • com.markerhub.config.CorsConfig
/**
 * 解決跨域問題
 */
@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,因為我們系統開發的介面比較簡單,所以我就不整合swagger2啦,也比較簡單而已。下面我們就直接進入我們的正題,進行編寫登入介面。

8、登入介面開發

登入的邏輯其實很簡答,只需要接受賬號密碼,然後把使用者的id生成jwt,返回給前段,為了後續的jwt的延期,所以我們把jwt放在header上。具體程式碼如下:

  • com.markerhub.controller.AccountController
@RestController
public class AccountController {
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserService userService;
    /**
     * 預設賬號密碼: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, "使用者不存在");
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
            return Result.fail("密碼錯誤!");
        }
        String jwt = jwtUtils.generateToken(user.getId());
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
        // 使用者可以另一個介面
        return Result.succ(MapUtil.builder()
                .put("id", user.getId())
                .put("username", user.getUsername())
                .put("avatar", user.getAvatar())
                .put("email", user.getEmail())
                .map()
        );
    }
    
    // 退出
    @GetMapping("/logout")
    @RequiresAuthentication
    public Result logout() {
        SecurityUtils.getSubject().logout();
        return Result.succ(null);
    }
}

介面測試:
圖片

9、部落格介面開發

我們的骨架已經完成,接下來,我們就可以新增我們的業務介面了,下面我以一個簡單的部落格列表、部落格詳情頁為例子開發:

  • 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, "該部落格已刪除!");
        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() == ShiroUtil.getProfile().getId(), "沒有許可權編輯");
    } 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("操作成功", null);
}
}

注意@RequiresAuthentication說明需要登入之後才能訪問的介面,其他需要許可權的介面可以新增shiro的相關注解。
介面比較簡單,我們就不多說了,基本增刪改查而已。注意的是edit方法是需要登入才能操作的受限資源。

介面測試:

圖片

10、後端總結

好了,一篇文章搞定一個基本骨架,好像有點趕,但是基本的東西這裡已經有了。後面我們就要去開發我們的前端介面了。

專案程式碼:https://github.com/MarkerHub/...

專案視訊:https://www.bilibili.com/vide...

Vue前端頁面開發

1、前言

接下來,我們來完成vueblog前端的部分功能。可能會使用的到技術如下:

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

本專案實踐需要一點點vue的基礎,希望你對vue的一些指令有所瞭解,這樣我們講解起來就簡單多了哈。

2、專案演示

我們先來看下我們需要完成的專案長什麼樣子,考慮到很多同學的樣式的掌握程度不夠,所以我儘量使用了element-ui的原生元件的樣式來完成整個部落格的介面。不多說,直接上圖:

線上體驗:https://markerhub.com:8083

圖片

圖片

圖片

3、環境準備

萬丈高樓平地起,我們下面一步一步來完成,首先我們安裝vue的環境,我實踐的環境是windows 10哈。

1、首先我們上node.js官網(https://nodejs.org/zh-cn/),下載最新的長期版本,直接執行安裝完成之後,我們就已經具備了node和npm的環境啦。

圖片

安裝完成之後檢查下版本資訊:

圖片

2、接下來,我們安裝vue的環境

# 安裝淘寶npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue-cli 安裝依賴包
cnpm install --g vue-cli

4、新建專案

# 開啟vue的視覺化管理工具介面
vue ui

上面我們分別安裝了淘寶npm,cnpm是為了提高我們安裝依賴的速度。vue ui是@vue/cli3.0增加一個視覺化專案管理工具,可以執行專案、打包專案,檢查等操作。對於初學者來說,可以少記一些命令,哈哈。
3、建立vueblog-vue專案

執行vue ui之後,會為我們開啟一個http://localhost:8080 的頁面:

圖片

然後切換到【建立】,注意建立的目錄最好是和你執行vue ui同一級。這樣方便管理和切換。然後點選按鈕【在此建立新羨慕】

圖片

下一步中,專案資料夾中輸入專案名稱“vueblog-vue”,其他不用改,點選下一步,選擇【手動】,再點選下一步,如圖點選按鈕,勾選上路由Router、狀態管理Vuex,去掉js的校驗。

圖片

下一步中,也選上【Use history mode for router】,點選建立專案,然後彈窗中選擇按鈕【建立專案,不儲存預設】,就進入專案建立啦。

稍等片刻之後,專案就初始化完成了。上面的步驟中,我們建立了一個vue專案,並且安裝了Router、Vuex。這樣我們後面就可以直接使用。

我們來看下整個vueblog-vue的專案結構

├── README.md            專案介紹
├── index.html           入口頁面
├── build              構建指令碼目錄
│  ├── build-server.js         執行本地構建伺服器,可以訪問構建後的頁面
│  ├── build.js            生產環境構建指令碼
│  ├── dev-client.js          開發伺服器熱過載指令碼,主要用來實現開發階段的頁面自動重新整理
│  ├── dev-server.js          執行本地開發伺服器
│  ├── utils.js            構建相關工具方法
│  ├── webpack.base.conf.js      wabpack基礎配置
│  ├── webpack.dev.conf.js       wabpack開發環境配置
│  └── webpack.prod.conf.js      wabpack生產環境配置
├── config             專案配置
│  ├── dev.env.js           開發環境變數
│  ├── index.js            專案配置檔案
│  ├── prod.env.js           生產環境變數
│  └── test.env.js           測試環境變數
├── mock              mock資料目錄
│  └── hello.js
├── package.json          npm包配置檔案,裡面定義了專案的npm指令碼,依賴包等資訊
├── src               原始碼目錄 
│  ├── main.js             入口js檔案
│  ├── app.vue             根元件
│  ├── components           公共元件目錄
│  │  └── title.vue
│  ├── assets             資源目錄,這裡的資源會被wabpack構建
│  │  └── images
│  │    └── logo.png
│  ├── routes             前端路由
│  │  └── index.js
│  ├── store              應用級資料(state)狀態管理
│  │  └── index.js
│  └── views              頁面目錄
│    ├── hello.vue
│    └── notfound.vue
├── static             純靜態資源,不會被wabpack構建。
└── test              測試檔案目錄(unit&e2e)
  └── unit              單元測試
    ├── index.js            入口指令碼
    ├── karma.conf.js          karma配置檔案
    └── specs              單測case目錄
      └── Hello.spec.js

5、安裝element-ui

接下來我們引入element-ui元件(https://element.eleme.cn),這樣我們就可以獲得好看的vue元件,開發好看的部落格介面。

圖片

命令很簡單:

# 切換到專案根目錄
cd vueblog-vue
# 安裝element-ui
cnpm install element-ui --save

然後我們開啟專案src目錄下的main.js,引入element-ui依賴。

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

這樣我們就可以愉快得在官網上選擇元件複製程式碼到我們專案中直接使用啦。

6、安裝axios

接下來,我們來安裝axios(http://www.axios-js.com/),axios是一個基於 promise 的 HTTP 庫,這樣我們進行前後端對接的時候,使用這個工具可以提高我們的開發效率。

安裝命令:

cnpm install axios --save

然後同樣我們在main.js中全域性引入axios。

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

元件中,我們就可以通過this.$axios.get()來發起我們的請求了哈。

7、頁面路由

接下來,我們先定義好路由和頁面,因為我們只是做一個簡單的部落格專案,頁面比較少,所以我們可以直接先定義好,然後在慢慢開發,這樣需要用到連結的地方我們就可以直接可以使用:

我們在views資料夾下定義幾個頁面:

  • BlogDetail.vue(部落格詳情頁)
  • BlogEdit.vue(編輯部落格)
  • Blogs.vue(部落格列表)
  • Login.vue(登入頁面)

然後再路由中心配置:

  • 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',
    // 懶載入
    component: () => import('../views/Blogs.vue')
  },
  {
    path: '/blog/add', // 注意放在 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

接下來我們去開發我們的頁面。其中,帶有meta:requireAuth: true說明是需要登入字後才能訪問的受限資源,後面我們路由許可權攔截時候會用到。

8、登入頁面

接下來,我們來搞一個登陸頁面,表單元件我們直接在element-ui的官網上找就行了,登陸頁面就兩個輸入框和一個提交按鈕,相對簡單,然後我們最好帶頁面的js校驗。emmm,我直接貼程式碼了~~

  • views/Login.vue
<template>
  <div>
    <el-container>
      <el-header>
        <router-link to="/blogs">
        <img src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png"
             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="使用者名稱" prop="username">
            <el-input type="text" maxlength="12" v-model="ruleForm.username"></el-input>
          </el-form-item>
          <el-form-item label="密碼" 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')">登入</el-button>
            <el-button @click="resetForm('ruleForm')">重置</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('請輸入密碼'));
        } else {
          callback();
        }
      };
      return {
        ruleForm: {
          password: '111111',
          username: 'markerhub'
        },
        rules: {
          password: [
            {validator: validatePass, trigger: 'blur'}
          ],
          username: [
            {required: true, message: '請輸入使用者名稱', trigger: 'blur'},
            {min: 3, max: 12, message: '長度在 3 到 12 個字元', trigger: 'blur'}
          ]
        }
      };
    },
    methods: {
      submitForm(formName) {
        const _this = this
        this.$refs[formName].validate((valid) => {
          if (valid) {
            // 提交邏輯
            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: '看這裡:',
        message: '關注公眾號:MarkerHub,回覆【vueblog】,領取專案資料與原始碼',
        duration: 1500
      });
    }
  }
</script>

找不到啥好的方式講解了,之後先貼程式碼,然後再講解。
上面程式碼中,其實主要做了兩件事情

1、表單校驗

2、登入按鈕的點選登入事件

表單校驗規則還好,比較固定寫法,查一下element-ui的元件就知道了,我們來分析一下發起登入之後的程式碼:

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

從返回的結果請求頭中獲取到token的資訊,然後使用store提交token和使用者資訊的狀態。完成操作之後,我們調整到了/blogs路由,即部落格列表頁面。

token的狀態同步

所以在store/index.js中,程式碼是這樣的:

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: {}
})

儲存token,我們用的是localStorage,儲存使用者資訊,我們用的是sessionStorage。畢竟使用者資訊我們不需要長久儲存,儲存了token資訊,我們隨時都可以初始化使用者資訊。當然了因為本專案是個比較簡單的專案,考慮到初學者,所以很多相對複雜的封裝和功能我沒有做,當然了,學了這個專案之後,自己想再繼續深入,完成可以自行學習和改造哈。

定義全域性axios攔截器

點選登入按鈕發起登入請求,成功時候返回了資料,如果是密碼錯誤,我們是不是也應該彈窗訊息提示。為了讓這個錯誤彈窗能運用到所有的地方,所以我對axios做了個後置攔截器,就是返回資料時候,如果結果的code或者status不正常,那麼我對應彈窗提示。

在src目錄下建立一個檔案axios.js(與main.js同級),定義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("前置攔截")
  // 可以統一設定請求頭
  return config
})
axios.interceptors.response.use(response => {
    const res = response.data;
    console.log("後置攔截")
    // 當結果的code是否為200的情況
    if (res.code === 200) {
      return response
    } else {
      // 彈窗異常資訊
      Element.Message({
        message: response.data.msg,
        type: 'error',
        duration: 2 * 1000
      })
      // 直接拒絕往下面返回結果資訊
      return Promise.reject(response.data.msg)
    }
  },
  error => {
    console.log('err' + error)// for debug
    if(error.response.data) {
      error.message = error.response.data.msg
    }
    // 根據請求狀態覺得是否登入或者提示其他
    if (error.response.status === 401) {
      store.commit('REMOVE_INFO');
      router.push({
        path: '/login'
      });
      error.message = '請重新登入';
    }
    if (error.response.status === 403) {
      error.message = '許可權不足,無法訪問';
    }
    Element.Message({
      message: error.message,
      type: 'error',
      duration: 3 * 1000
    })
    return Promise.reject(error)
  })

前置攔截,其實可以統一為所有需要許可權的請求裝配上header的token資訊,這樣不需要在使用是再配置,我的小專案比較小,所以,還是免了吧~

然後再main.js中匯入axios.js

import './axios.js' // 請求攔截

後端因為返回的實體是Result,succ時候code為200,fail時候返回的是400,所以可以根據這裡判斷結果是否是正常的。另外許可權不足時候可以通過請求結果的狀態碼來判斷結果是否正常。這裡都做了簡單的處理。

登入異常時候的效果如下:

圖片

9、部落格列表

登入完成之後直接進入部落格列表頁面,然後載入部落格列表的資料渲染出來。同時頁面頭部我們需要把使用者的資訊展示出來,因為很多地方都用到這個模組,所以我們把頁面頭部的使用者資訊單獨抽取出來作為一個元件。

頭部使用者資訊

那麼,我們先來完成頭部的使用者資訊,應該包含三部分資訊:id,頭像、使用者名稱,而這些資訊我們是在登入之後就已經存在了sessionStorage。因此,我們可以通過store的getters獲取到使用者資訊。

圖片

看起來不是很複雜,我們貼出程式碼:

  • componentsHeader.vue
<template>
  <div class="m-content">
    <h3>歡迎來到MarkerHub的部落格</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">主頁</el-link>
      <el-divider direction="vertical"></el-divider>
      <span>
          <el-link type="success" href="/blog/add" :disabled="!hasLogin">發表文章</el-link>
        </span>
      <el-divider direction="vertical"></el-divider>
      <span v-show="!hasLogin">
          <el-link type="primary" href="/login">登陸</el-link>
        </span>
      <span v-show="hasLogin">
          <el-link type="danger" @click="logout">退出</el-link>
        </span>
    </div>
  </div>
</template>
<script>
  export default {
    name: "Header",
    data() {
      return {
        hasLogin: false,
        user: {
          username: '請先登入',
          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>

上面程式碼created()中初始化使用者的資訊,通過hasLogin的狀態來控制登入和退出按鈕的切換,以及發表文章連結的disabled,這樣使用者的資訊就能展示出來了。
然後這裡有個退出按鈕,在methods中有個logout()方法,邏輯比較簡單,直接訪問/logout,因為之前axios.js中我們已經設定axios請求的baseURL,所以這裡我們不再需要連結的字首了哈。因為是登入之後才能訪問的受限資源,所以在header中帶上了Authorization。返回結果清楚store中的使用者資訊和token資訊,跳轉到登入頁面。

然後需要頭部使用者資訊的頁面只需要幾個步驟:

import Header from "@/components/Header";
data() {
  components: {Header}
}
# 然後模板中呼叫元件
<Header></Header>

部落格分頁

接下來就是列表頁面,需要做分頁,列表我們在element-ui中直接使用時間線元件來作為我們的列表樣式,還是挺好看的。還有我們的分頁元件。

需要幾部分資訊:

  • 分頁資訊
  • 部落格列表內容,包括id、標題、摘要、建立時間
  • 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()中直接定義部落格列表blogs、以及一些分頁資訊。methods()中定義分頁的呼叫介面page(currentPage),引數是需要調整的頁碼currentPage,得到結果之後直接賦值即可。然後初始化時候,直接在mounted()方法中呼叫第一頁this.page(1)。完美。使用element-ui元件就是簡單快捷哈哈!
注意標題這裡我們新增了連結,使用的是<router-link>標籤。

10、部落格編輯(發表)

我們點選發表部落格連結調整到/blog/add頁面,這裡我們需要用到一個markdown編輯器,在vue元件中,比較好用的是mavon-editor,那麼我們直接使用哈。先來安裝mavon-editor相關元件:

安裝mavon-editor

基於Vue的markdown編輯器mavon-editor

cnpm install mavon-editor --save

然後在main.js中全域性註冊:

// 全域性註冊
import Vue from 'vue'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)

ok,那麼我們去定義我們的部落格表單:

<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="標題" prop="title">
          <el-input v-model="editForm.title"></el-input>
        </el-form-item>
        <el-form-item label="摘要" prop="description">
          <el-input type="textarea" v-model="editForm.description"></el-input>
        </el-form-item>
        <el-form-item label="內容" prop="content">
          <mavon-editor v-model="editForm.content"/>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitForm()">立即建立</el-button>
          <el-button>取消</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: '請輸入標題', trigger: 'blur'},
            {min: 3, max: 50, message: '長度在 3 到 50 個字元', trigger: 'blur'}
          ],
          description: [
            {required: true, message: '請輸入摘要', 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('操作成功', '提示', {
                confirmButtonText: '確定',
                callback: action => {
                  _this.$router.push("/blogs")
                }
              });
            });
          } else {
            console.log('error submit!!');
            return false;
          }
        })
      }
    }
  }
</script>

邏輯依然簡單,校驗表單,然後點選按鈕提交表單,注意頭部加上Authorization資訊,返回結果彈窗提示操作成功,然後跳轉到部落格列表頁面。emm,和寫ajax沒啥區別。熟悉一下vue的一些指令使用即可。
然後因為編輯和新增是同一個頁面,所以有了create()方法,比如從編輯連線/blog/7/edit中獲取blogId為7的這個id。然後回顯部落格資訊。獲取方式是const blogId = this.$route.params.blogId。

對了,mavon-editor因為已經全域性註冊,所以我們直接使用元件即可:

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

效果如下:
圖片

11、部落格詳情

部落格詳情中需要回顯部落格資訊,然後有個問題就是,後端傳過來的是部落格內容是markdown格式的內容,我們需要進行渲染然後顯示出來,這裡我們使用一個外掛markdown-it,用於解析md文件,然後匯入github-markdown-c,所謂md的樣式。

方法如下:

# 用於解析md文件
cnpm install markdown-it --save
# md樣式
cnpm install github-markdown-css

然後就可以在需要渲染的地方使用:

  • 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}}">編輯</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' // 然後新增樣式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
          // 判斷是否是自己的文章,能否編輯
          _this.ownBlog =  (_this.blog.userId === _this.$store.getters.getUser.id)
        });
      }
    },
    created() {
      this.getBlog()
    }
  }
</script>

具體邏輯還是挺簡單,初始化create()方法中呼叫getBlog()方法,請求部落格詳情介面,返回的部落格詳情content通過markdown-it工具進行渲染。

再匯入樣式:

import 'github-markdown.css'

然後在content的div中新增class為markdown-body即可哈。
效果如下:

圖片

另外標題下新增了個小小的編輯按鈕,通過ownBlog (判斷博文作者與登入使用者是否同一人)來判斷按鈕是否顯示出來。

12、路由許可權攔截

頁面已經開發完畢之後,我們來控制一下哪些頁面是需要登入之後才能跳轉的,如果未登入訪問就直接重定向到登入頁面,因此我們在src目錄下定義一個js檔案:

  • srcpermission.js
import router from "./router";
// 路由判斷登入 根據路由配置檔案的引數
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) { // 判斷該路由是否需要登入許可權
    const token = localStorage.getItem("token")
    console.log("------------" + token)
    if (token) { // 判斷當前的token是否存在 ; 登入存入的token
      if (to.path === '/login') {
      } else {
        next()
      }
    } else {
      next({
        path: '/login'
      })
    }
  } else {
    next()
  }
})

通過之前我們再定義頁面路由時候的的meta資訊,指定requireAuth: true,需要登入才能訪問,因此這裡我們在每次路由之前(router.beforeEach)判斷token的狀態,覺得是否需要跳轉到登入頁面。

{
  path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
  name: 'BlogAdd',
  meta: {
    requireAuth: true
  },
  component: BlogEdit
}

然後我們再main.js中import我們的permission.js

import './permission.js' // 路由攔截

13、前端總結

ok,基本所有頁面就已經開發完畢啦,css樣式資訊我未貼出來,大家直接上github上clone下來檢視。

專案大總結

好啦,專案先到這裡,花了3天半錄製了一套對應的視訊,記得去看,給我三連哇。

專案程式碼:https://github.com/MarkerHub/...

專案視訊:https://www.bilibili.com/vide...

相關文章