一、服务端功能开发

同样的,Go 语言的服务端帖子模块相关代码我们分为以下三部分:

  • 结构体:topic_model
  • 业务服务:topic_service
  • 控制器:topic_controller

结构体(topic_model)

新建文件server/topic_model.go,在该文件中定义帖子结构体,完整代码如下:

package main

import "time"

type Topic struct {
    Id         int64     `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"`  // 编号
    UserId     int64     `gorm:"not null" json:"userId"`                // 作者编号
    Title      string    `gorm:"size:128;not null" json:"title"`        // 标题
    Content    string    `gorm:"type:longtext;not null" json:"content"` // 内容
    CreateTime time.Time `gorm:"not null" json:"createTime"`            // 创建时间
}

帖子结构体定义完成之后不要忘记将Topic结构体放入gormAutoMigrate中,这样启动服务的时候gorm会自动创建 topic 表,打开server/main.go,修改gorm配置如下:

db.AutoMigrate(&User{}, &UserToken{}, &Topic{})

业务服务(topic_service)

新建文件server/topic_service.go,在该文件中我们去实现帖子相关的业务逻辑,并进行数据库的读写操作,完整代码如下:

package main

import (
    "time"
)

var TopicService = &topicService{}

type topicService struct {
}

// 创建帖子
func (topicService) Create(userId int64, title, content string) (*Topic, error) {
    topic := &Topic{
        UserId:     userId,
        Title:      title,
        Content:    content,
        CreateTime: time.Now(),
    }
    err := db.Create(topic).Error
    if err != nil {
        return nil, err
    }
    return topic, nil
}

// 根据编号获取帖子
func (topicService) Get(id int64) *Topic {
    ret := &Topic{}
    if err := db.First(ret, "id = ?", id).Error; err != nil {
        return nil
    }
    return ret
}

// 查询帖子列表
func (topicService) List(page int) (topics []Topic, totalCount int) {
    if page <= 0 {
        page = 1
    }
    offset := 20 * (page - 1)
    db.Order("id desc").Offset(offset).Limit(20).Find(&topics) // 查列表
    db.Model(&Topic{}).Count(&totalCount)     // 查计数
    return
}

控制器(topic_controller)

新建文件server/topic_controller.go,该文件中定义帖子相关接口,完整代码如下:

package main

import (
    "github.com/kataras/iris/context"
    "github.com/mlogclub/simple"
)

type TopicController struct {
    Ctx context.Context
}

// 创建帖子
func (this *TopicController) PostAdd() *simple.JsonResult {

    // 获取当前登录用户,发帖必须要求用户已经登录了
    user := UserService.GetCurrent(this.Ctx)
    if user == nil {
        return simple.JsonErrorMsg("请先登录")
    }

    var (
        title   = this.Ctx.FormValue("title")
        content = this.Ctx.FormValue("content")
    )

    topic, err := TopicService.Create(user.Id, title, content)
    if err != nil {
        return simple.JsonErrorMsg(err.Error())
    }

    return simple.JsonData(topic)
}

// 根据id获取
func (this *TopicController) GetBy(id int64) *simple.JsonResult {
    topic := TopicService.Get(id)
    if topic == nil {
        return simple.JsonErrorMsg("帖子不存在")
    }
    user := UserService.Get(topic.UserId)
    return simple.NewRspBuilder(topic).Put("user", user).JsonResult()
}

// 帖子列表
func (this *TopicController) GetListBy(page int) *simple.JsonResult {
    topics, totalCount := TopicService.List(page)
    return simple.NewEmptyRspBuilder().Put("topics", topics).Put("totalCount", totalCount).JsonResult()
}

完成topic_controller功能开发之后,需要将topic_controller添加到iris路由中,打开server/main.go文件,修改代码如下:

mvc.Configure(app.Party("/api/topic"), func(mvcApp *mvc.Application) {
    mvcApp.Handle(new(TopicController))
})

启动接口服务

综上我们已经完成帖子模块服务端相关接口开发,下面我们来启动服务,命令如下:

➜  server git:(master) ✗ go run *.go
Now listening on: http://localhost:8081
Application started. Press CMD+C to shut down.

二、Nuxt.js 页面功能开发

前面我们完成了帖子模块服务端接口开发,并且成功启动了接口服务,接下来我们使用Nuxt.js来完成页面开发。

发表帖子页面

新建文件site/pages/topic/create.vue,完整代码如下:

<template>
  <section>
    <my-nav />
    <section class="section">
      <div class="container">
        <div class="field">
          <label class="label">标题</label>
          <div class="control">
            <input
              v-model="form.title"
              class="input"
              type="text"
              placeholder="请输入帖子标题"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">内容</label>
          <div class="control">
            <textarea
              v-model="form.content"
              class="textarea has-fixed-size"
              rows="10"
              placeholder="请输入帖子内容"
            ></textarea>
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button class="button is-link" @click="createTopic">发表</button>
          </div>
        </div>
      </div>
    </section>
    <my-footer />
  </section>
</template>

<script>
  import MyNav from '~/components/MyNav';
  import MyFooter from '~/components/MyFooter';

  export default {
    components: {
      MyNav,
      MyFooter,
    },
    data() {
      return {
        form: {
          title: '',
          content: '',
        },
      };
    },
    methods: {
      async createTopic() {
        try {
          const resp = await this.$axios.post('/api/topic/add', this.form);
          console.log('发表成功', resp);
          this.$router.push('/topic/' + resp.id); // 发表成功后跳转到帖子详情页
        } catch (err) {
          alert(err.message || err);
        }
      },
    },
  };
</script>

启动前端页面服务之后,访问路径/topic/create就可以看到页面效果,如下图:

接下来我们就可以发表一篇帖子啦,当然发表之前你需要先登录。

帖子详情页面

新建文件site/pages/topic/_id.vue,完整代码如下:

<template>
  <section>
    <my-nav />
    <section class="section">
      <div class="container">
        <article>
          <div class="title">
            {{ topic.title }}
            <span class="meta"
              >By {{topic.user.nickname}} @ {{topic.createTime}}</span
            >
          </div>
          <pre class="content">{{ topic.content }}</pre>
        </article>
      </div>
    </section>
    <my-footer />
  </section>
</template>

<script>
  import MyNav from '~/components/MyNav';
  import MyFooter from '~/components/MyFooter';

  export default {
    components: {
      MyNav,
      MyFooter,
    },
    async asyncData({ params, $axios }) {
      try {
        const topicId = params.id; // 从动态路由参数中获取帖子id
        const topic = await $axios.get('/api/topic/' + topicId);
        return {
          topic: topic,
        };
      } catch (err) {
        console.log(err);
      }
    },
  };
</script>

<style scoped>
  article .title {
    font-weight: bold;
    font-size: 15px;
    border-bottom: 2px #f7f8fb dashed;
    padding-bottom: 10px;
  }

  article .title .meta {
    font-weight: normal;
    font-size: 12px;
    color: #3b8070;
  }

  article .content {
    margin-top: 10px;
  }
</style>

启动前端页面服务之后,访问路径/topic/1就可以看到页面效果(路径中的1是你刚刚发表的帖子编号,请按实际情况填写),页面效果如下图:

帖子列表页面

新建文件site/pages/topics/_page.vue,完整代码如下:

<template>
  <section>
    <my-nav />
    <section class="section">
      <div class="container">
        <ul class="topics">
          <li class="topic" v-for="topic in topics" :key="topic.id">
            <a :href="'/topic/' + topic.id"
              >{{topic.title}}<span class="meta">@{{topic.createTime}}</span></a
            >
          </li>
        </ul>

        <nav class="pagination" role="navigation" aria-label="pagination">
          <a
            class="pagination-previous"
            :href="page > 1 ? '/topics/' + (page - 1) : 'javascript:void(0)'"
            >上一页</a
          >
          <a
            class="pagination-next"
            :href="page < maxPage ? '/topics/' + (page + 1) : 'javascript:void(0)'"
            >下一页</a
          >
        </nav>
      </div>
    </section>
    <my-footer />
  </section>
</template>

<script>
  import MyNav from '~/components/MyNav';
  import MyFooter from '~/components/MyFooter';

  export default {
    components: {
      MyNav,
      MyFooter,
    },
    async asyncData({ params, $axios }) {
      try {
        const page = params.page || 1; // 从动态路由参数中获取页码,如果没获取到默认认为是第一页
        const resp = await $axios.get('/api/topic/list/' + page);

        // 计算帖子的最大页码
        const maxPage =
          resp.totalCount % 20 > 0
            ? parseInt(resp.totalCount / 20) + 1
            : resp.totalCount / 20;
        return {
          topics: resp.topics,
          page: parseInt(page), // 当前页码
          maxPage: maxPage, // 最大页码
        };
      } catch (err) {
        console.log(err);
      }
    },
  };
</script>

<style scoped>
  .topics .topic {
    padding: 10px 0px;
    border-bottom: 1px solid #f7f8fb;
  }

  .topics .topic .meta {
    font-weight: normal;
    font-size: 12px;
    color: #3b8070;
    float: right;
  }

  .pagination {
    margin-top: 20px;
  }
</style>

启动前端页面服务之后,访问路径/topics/1就可以看到页面效果,如下图:

列表页是有分页功能的哦,每页 20 条数据,多添加点数据之后可以看到分页效果。

三、总结

本实例中我们完成了整个帖子模块的功能。下面我们看下本实例完整源码的目录结构:

.
├── server
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── topic_controller.go
│   ├── topic_model.go
│   ├── topic_service.go
│   ├── user_controller.go
│   ├── user_model.go
│   └── user_service.go
└── site
    ├── README.md
    ├── assets
    │   └── README.md
    ├── components
    │   ├── Logo.vue
    │   ├── MyFooter.vue
    │   ├── MyNav.vue
    │   └── README.md
    ├── jsconfig.json
    ├── layouts
    │   ├── README.md
    │   └── default.vue
    ├── middleware
    │   └── README.md
    ├── nuxt.config.js
    ├── package-lock.json
    ├── package.json
    ├── pages
    │   ├── README.md
    │   ├── index.vue
    │   ├── topic
    │   │   ├── _id.vue
    │   │   └── create.vue
    │   ├── topics
    │   │   └── _page.vue
    │   └── user
    │       ├── login.vue
    │       └── reg.vue
    ├── plugins
    │   ├── README.md
    │   └── axios.js
    ├── static
    │   ├── README.md
    │   └── favicon.ico
    └── store
        └── README.md