9 months ago

練習使用 spring-boot-starter-thymeleaf 跟 vuejs 來做個簡易管理後台

需要的組件

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.yysport.pms</groupId>
    <artifactId>pms</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>yysport-pmsAdmin</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-data-rest</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

在組態部分定義了錯誤頁面,注意這邊頁面會用類似 forward 效果吐回 src\main\resources\static\404.html 的內容

ServiceConfig.java
@Configuration
public class ServiceConfig {
    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/401.html");
                ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html");
                ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html");
                container.addErrorPages(error401Page, error404Page, error500Page);
            }
        };
    }
}

接下來寫模板控制

IndexController.java
@Controller
public class IndexController {

    @RequestMapping("index")
    public String index(@RequestParam(value="name", required=false, defaultValue="Sam") String name, Model model) {
        model.addAttribute("name", name);
        return "index";
    }

    @RequestMapping("")
    public String root(@RequestParam(value="name", required=false, defaultValue="Sam") String name, Model model) {
        model.addAttribute("name", name);
        return "index";
    }
}

這樣的寫法會轉到 src\main\resources\templates\index.html 由 thymeleaf 來渲染頁面

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:inline="text">Hello, [[${name}]]!</p>
</body>
</html>

目前 SpringBoot 配置的版本是 thymeleaf 2.1.5 語法文件請參考
thymeleaf tutorials 2.1

但是 thymeleaf 預設的 html5 格式檢查相當嚴謹,像剛剛上面的 html 會造成解析錯誤如下

org.xml.sax.SAXParseException: 元素類型 "meta" 必須由配對的結束標記 "</meta>" 終止。
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:203) ~[na:1.8.0_112]
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(ErrorHandlerWrapper.java:177) ~[na:1.8.0_112]
    at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:400) ~[na:1.8.0_112]
    at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:327) ~[na:1.8.0_112]
    at com.sun.org.apache.xerces.internal.impl.XMLScanner.reportFatalError(XMLScanner.java:1472) ~[na:1.8.0_112]

除了太嚴謹會有點麻煩,還有另一個原因是之後加上 vue 後你還是會發生解析錯誤,所以我們來放寬一下限制

application.yml 增加

application.yml
spring.

  thymeleaf:

    mode: LEGACYHTML5

還要加上依賴

pom.xml
<dependency>
    <groupId>net.sourceforge.nekohtml</groupId>
    <artifactId>nekohtml</artifactId>
    <version>1.9.22</version>
</dependency>

重新啟動時就不會再解析錯誤了

現在我們來加上 vue 來開發前端,先在 pom.xml 中加上 webjars 的 vue

pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>vue</artifactId>
    <version>2.1.3</version>
</dependency>

再把我們原來的 index.html 搭配 veu 的範例 Handling-User-Input 改造一下結果如下

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <script src="https://unpkg.com/vue/dist/vue.js" th:src="@{/webjars/vue/2.1.3/vue.js}"></script>
</head>
<body>

<div id="app-6">
    <p>{{ message }}</p>
    <input v-model="message">
</div>

<script th:inline="javascript">
    var username = /*[[${name}]]*/ 'No Body';
    var app6 = new Vue({
        el: '#app-6',
        data: {
            message: 'Hello ' + username + ' !'
        }
    })
</script>

</body>
</html>

thymeleaf 最大一點好處就是沒有後端應用來渲染頁面可以正常執行,但是會稍微比以前多花點心思來寫,但好處是你們家前後分離開發的話,你就可以讓前端來獨立作業,比如說像上面這個範例

在 Server 上執行

前端工程師直接開啟index.html

javascript 都沒有崩壞~~!! 灑花 。:.゚ヽ(*´∀`)ノ゚.:。

這樣你就可以脫離前端開發的工作,專心開發後端部分
ps.如果你們沒有前端工程師的話,就忘了這段吧,你自己一個人寫方便就好

再使用 js 來操作 ajax 取得資料
官方原本有開發一個 ajax 套件 vue-resource,但是之後不會再維護了,為什麼不維護請參考下面

为何放弃vue-resource

尤大的原话:

最近团队讨论了一下,Ajax 本身跟 Vue 并没有什么需要特别整合的地方,使用 fetch polyfill 或是 axios、superagent 等等都可以起到同等的效果,vue-resource 提供的价值和其维护成本相比并不划算,所以决定在不久以后取消对 vue-resource 的官方推荐。已有的用户可以继续使用,但以后不再把 vue-resource 作为官方的 ajax 方案。
知乎链接:https://www.zhihu.com/question/52418455/answer/130535375

官方網頁沒有找到特別推薦的 ajax 方案,但是有 axios 的範例,有些部落格是說官方推薦的是 axios ,寫起來是差不多就一起練一下

Spring 中增加一個回覆時間的資料

TimeController.java
@RestController
public class TimeController {

    @GetMapping(path = "time")
    public Map getName() {
        Map map = new HashMap();
        map.put("name", "Sam");
        map.put("time", new Date());
        return map;
    }
}

指定時間轉換的格式

application.yml
spring:

  jackson:

    date-format: com.fasterxml.jackson.databind.util.ISO8601DateFormat

    time-zone: UTC

使用 webjar 加入前端 js

pom.xml
<dependency>
    <groupId>org.webjars.bower</groupId>
    <artifactId>vue-resource</artifactId>
    <version>1.0.3</version>
</dependency>
<dependency>
    <groupId>org.webjars.bower</groupId>
    <artifactId>axios</artifactId>
    <version>0.15.2</version>
</dependency>

前端網頁

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
    <script src="https://unpkg.com/vue/dist/vue.js" th:src="@{/webjars/vue/2.1.3/vue.js}"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js" th:src="@{/webjars/axios/0.15.2/dist/axios.js}"></script>
    <script src="https://cdn.jsdelivr.net/vue.resource/1.0.3/vue-resource.min.js"
            th:src="@{/webjars/vue-resource/1.0.3/dist/vue-resource.js}"></script>
</head>
<body>

<div id="app-6">
    <p>{{ message }}</p>
    <input v-model="message">
</div>

<script th:inline="javascript">
    var username = /*[[${name}]]*/ 'No Body';
    var app6 = new Vue({
        el: '#app-6',
        data: {
            message: 'Hello ' + username + ' !',
            apiUrl: '/time'
        },
        mounted: function () {
                // 只是定時器

                setInterval(this.gettime, 1000);
            //this.cycle();

        },
        methods: {
            // 只是定時器

            cycle: function () {
                this.gettime();
                //setTimeout(this.cycle, 1000);

            },
            gettime: function () {
                // use axios

                var self = this;
                axios.get(this.apiUrl)
                    .then(function (response) {
                        self.message = response.data.name + ', Time is ' + response.data.time;
                    })
                    .catch(function (error) {
                        console.log(error);
                    });

                // use vue-resource

//                this.$http.get(this.apiUrl, {}, {

//                    headers: {}

//                }).then(function (response) {

//                    // 這裡是處理正確的回調

//                    this.message = response.data.name + ', Time is ' + response.data.time

//                    // this.articles = response.data["subjects"] 也可以

//                }, function (response) {

//                    // 這裡是處理錯誤的回調

//                    console.log(response)

//                });

            }
        }
    })
</script>

</body>
</html>

說明一下改了什麼

js引入
<script src="https://unpkg.com/axios/dist/axios.min.js" th:src="@{/webjars/axios/0.15.2/dist/axios.js}"></script>
<script src="https://cdn.jsdelivr.net/vue.resource/1.0.3/vue-resource.min.js"
            th:src="@{/webjars/vue-resource/1.0.3/dist/vue-resource.js}"></script>

vue 初始化

vue綁定初始化觸發
mounted: function () {
    this.cycle();
}
使用axios
// use axios

var self = this;
axios.get(this.apiUrl)
    .then(function (response) {
        self.message = response.data.name + ', Time is ' + response.data.time;
    })
    .catch(function (error) {
        console.log(error);
});
使用vue-resource
this.$http.get(this.apiUrl, {}, {
        headers: {}
    }).then(function (response) {
        // 這裡是處理正確的回調

        this.message = response.data.name + ', Time is ' + response.data.time
        // this.articles = response.data["subjects"] 也可以

    }, function (response) {
        // 這裡是處理錯誤的回調

        console.log(response)
});

其實都算好寫啦,差別是 vue-resource 裡面的 this 可以很輕易指到 vue 物件裡面的 data,而 axios 裡面的 this 應該是指到 axios 自己吧,不過這一點點沒什麼影響開發。

完成圖

這樣一個簡單的後台就可以取得很多資訊了

參考
Vue.js学习系列
Vue.js 官方簡中手冊

← SpringBoot use liquibase 關於注入的方式 →
 
comments powered by Disqus