一、服务端功能开发

服务端开发我们会分为model/service/controller,这样让项目结构清晰明了。这三个模块作用分别如下:

  • model:用来定义结构体,结构体和数据库表结构一一对应
  • service:用来编写业务逻辑,读写数据库
  • controller:对外提供数据接口

main.go

首先我们来看server/main.go中的代码,他是我们整个程序的入口,在main.go中我们将初始化数据库,配置iris路由,他的完整代码如下:

package main

import (
    "github.com/jinzhu/gorm"
    "github.com/kataras/iris"
    "github.com/kataras/iris/mvc"

    "github.com/iris-contrib/middleware/cors"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var db *gorm.DB

func main() {
    initDB()

    app := iris.New()

    // 跨域配置
    app.Use(cors.New(cors.Options{
        AllowedOrigins:   []string{"*"}, // allows everything, use that to change the hosts.
        AllowCredentials: true,
        MaxAge:           600,
        AllowedMethods:   []string{iris.MethodGet, iris.MethodPost, iris.MethodOptions, iris.MethodHead, iris.MethodDelete, iris.MethodPut},
        AllowedHeaders:   []string{"*"},
    }))
    app.AllowMethods(iris.MethodOptions)

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

    _ = app.Run(iris.Addr(":8081"), iris.WithoutServerError(iris.ErrServerClosed))
}

// 初始化数据库链接
func initDB() {
    var err error
    db, err = gorm.Open("mysql", "root@tcp(localhost:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    db.LogMode(true)
}

结构体(user_model)

新建/server/user_model.go文件,在该文件中定义我们用户结构体。用户相关的有两个结构体,分别如下:

  • 用户(user):用于保存用户资料
  • 用户授权令牌(user_token):用户的登录标识,通过令牌能获取到当前登录用户

/server/user_model.go完整代码如下:

package main

import (
    "time"
)

const (
    UserRoleAdmin  = "管理员"
    UserRoleNormal = "普通用户"
)

// 用户表
type User struct {
    Id         int64     `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"`    // 编号
    Username   string    `gorm:"size:32;not null;unique" json:"username"` // 用户名,添加唯一标识
    Password   string    `gorm:"size:128;not null" json:"password"`       // 密码
    Nickname   string    `gorm:"size:32;not null" json:"nickname"`        // 昵称
    Role       string    `gorm:"size:32;not null" json:"role"`            // 用户角色,管理员、普通用户
    CreateTime time.Time `gorm:"not null" json:"createTime"`              // 创建时间
}

// 用户授权令牌,用户的登录标识
type UserToken struct {
    Id         int64     `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"`              // 编号
    UserId     int64     `gorm:"not null" json:"userId"`                            // 用户编号
    Token      string    `gorm:"size:32;unique;not null" json:"token" form:"token"` // 令牌
    ExpiredAt  int64     `gorm:"not null" json:"expiredAt" form:"expiredAt"`        // 过期时间戳
    CreateTime time.Time `gorm:"not null" json:"createTime"`                        // 创建时间
}

用户相关的结构体定义完成之后,需要将他们放入gormAutoMigrate,这样在启动服务的时候gorm会自动创建用户相关的表,打开server/main.go,修改gorm配置如下:

err = db.AutoMigrate(&User{}, &UserToken{}).Error
if err != nil {
  panic(err)
}

业务服务(user_service)

UserService是我们的业务代码,我们将在这里完成用户相关的核心逻辑、并操作数据库完成数据的读写操作。主要实现功能如下:

  • 用户注册
  • 用户登录
  • 获取当前登录用户

接下来我们新增文件server/user_service.go,该文件完整代码如下:

package main

import (
    "time"

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

var UserService = &userService{}

type userService struct {
}

// 创建用户
func (userService) Create(username, password, nickname, role string) error {
    return db.Create(&User{
        Username:   username,
        Password:   password,
        Nickname:   nickname,
        CreateTime: time.Now(),
    }).Error
}

// 根据id查询用户
func (userService) Get(id int64) *User {
    ret := &User{}
    if err := db.First(ret, "id = ?", id).Error; err != nil {
        return nil
    }
    return ret
}

// 根据用户名查找
func (userService) GetByUsername(username string) *User {
    ret := &User{}
    if err := db.Take(ret, "username = ?", username).Error; err != nil {
        return nil
    }
    return ret
}

// 用户登录
func (userService) Login(username, password string) (*User, string) {
    // 查找用户
    user := UserService.GetByUsername(username)
    if user == nil {
        return nil, ""
    }

    // 验证密码
    passwordValidated := simple.ValidatePassword(user.Password, password)
    if !passwordValidated {
        return nil, ""
    }

    // 生成授权令牌
    token := simple.Uuid()
    expiredAt := time.Now().Add(time.Hour * 24 * 7) // 7天后过期
    db.Create(&UserToken{
        UserId:     user.Id,
        Token:      token,
        ExpiredAt:  simple.Timestamp(expiredAt),
        CreateTime: time.Now(),
    })
    return user, token
}

// 获取当前登录用户
func (u userService) GetCurrent(ctx context.Context) *User {
    token := u.GetUserToken(ctx)
    if len(token) == 0 {
        return nil
    }
    userToken := &UserToken{}
    if err := db.Take(userToken, "token = ?", token).Error; err != nil {
        return nil
    }
    return u.Get(userToken.UserId)
}

// 从请求体中获取UserToken
func (userService) GetUserToken(ctx context.Context) string {
    userToken := ctx.FormValue("userToken")
    if len(userToken) > 0 {
        return userToken
    }
    return ctx.GetHeader("X-User-Token")
}

控制器(user_controller)

controller中我们对外提供数据接口,我们所有的接口都返回JsonResult对象,该对象最终会被序列化成JSON返回,下面通过 JsonResult 的代码+注释来了解下JsonResult中每个字段的含义。

type JsonResult struct {
    ErrorCode int         `json:"errorCode"`  // 错误码,当接口发生错误的时候可以指定错误码
    Message   string      `json:"message"`    // 错误消息,当接口发生错误的时候返回的错误消息
    Data      interface{} `json:"data"`       // 业务数据
    Success   bool        `json:"success"`    // 接口调用是否成功
}

接下来我们定义UserController,创建文件server/user_controller.go代码如下:

package main

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

// controller
type UserController struct {
    Ctx context.Context
}

// 用户注册
func (this *UserController) PostAdd() *simple.JsonResult {
    var (
        username   = this.Ctx.FormValue("username")
        password   = this.Ctx.FormValue("password")
        rePassword = this.Ctx.FormValue("rePassword")
        nickname   = this.Ctx.FormValue("nickname")
    )

    // 数据校验
    if len(username) == 0 || len(password) == 0 || len(nickname) == 0 {
        return simple.JsonErrorMsg("请认真填写用户名、密码、昵称")
    }
    if password != rePassword {
        return simple.JsonErrorMsg("两次填写密码不同,请检查后重新填写")
    }

    // 密码加密
    password = simple.EncodePassword(password)

    // 判断用户名是否存在
    tmp := UserService.GetByUsername(username)
    if tmp != nil {
        return simple.JsonErrorMsg("用户名【" + username + "】已经存在")
    }

    // 执行注册操作
    err := UserService.Create(username, password, nickname, UserRoleNormal)
    if err != nil {
        return simple.JsonErrorMsg(err.Error())
    }
    return simple.JsonSuccess()
}

// 用户登录
func (this *UserController) PostLogin() *simple.JsonResult {
    var (
        username = this.Ctx.FormValue("username")
        password = this.Ctx.FormValue("password")
    )

    user, token := UserService.Login(username, password)
    if user == nil {
        return simple.JsonErrorMsg("用户名密码错误")
    }
    // 登录成功返回用户信息和授权令牌
    return simple.NewRspBuilder(user).Put("token", token).JsonResult()
}

// 获取当前登录用户
func (this *UserController) GetCurrent() *simple.JsonResult {
    user := UserService.GetCurrent(this.Ctx)
    if user != nil {
        return simple.JsonData(user)
    }
    return simple.JsonSuccess()
}

UserCotroller中我们定义了三个接口,分别为:用户注册、用户登录、获取当前登录用户。接下来我们将UserController配置到iris路由中,打开server/main.go新增如下代码:

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

如果不明白如何使用iris的同学,请认真温习前面实验中关于iris使用方法的讲解

至此我们就完成了用户模块的服务端开发。然后在server目录下执行以下命令来启动接口服务:

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

二、Nuxt.js 页面功能开发

之前的章节中已经讲解了如何使用Nuxt.js,如果你认真阅读了之前的章节,那么你就能流畅地使用Nuxt.js完成日常开发工作。接下来就让我们开始实战吧。

Axios 插件

在创建Nuxt.js项目的时候我们安装了Axios插件,这里我们需要对Axios插件功能做一个简单的封装。封装主要为了实现以下两个功能:

  • Axios每次请求的时候自动对 JsonObject 参数进行编码。
  • 处理统一的返回状态码和返回结果。

这里需要添加一个第三方依赖qs,我们执行以下命令来添加qs依赖:

npm install qs --save

然后我们创建文件site/plugins/axios.js,完整内容如下:

import qs from 'qs';

export default function ({ $axios, $toast, app }) {
  $axios.onRequest((config) => {
    config.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
    config.transformRequest = [
      function (data) {
        if (process.client && data instanceof FormData) {
          // 如果是FormData就不转换
          return data;
        }
        data = qs.stringify(data);
        return data;
      },
    ];
  });

  $axios.onResponse((response) => {
    if (response.status !== 200) {
      return Promise.reject(response);
    }
    const jsonResult = response.data;
    if (jsonResult.success) {
      return Promise.resolve(jsonResult.data);
    } else {
      return Promise.reject(jsonResult);
    }
  });
}

接下来修改nuxt.config.js文件,修改该文件中的plugins,如下:

  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/axios'
  ],

用户注册页面

新增文件site/pages/user/reg.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.username"
              class="input"
              type="text"
              placeholder="请输入用户名"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">密码</label>
          <div class="control">
            <input
              v-model="form.password"
              class="input"
              type="password"
              placeholder="请输入密码"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">重复密码</label>
          <div class="control">
            <input
              v-model="form.rePassword"
              class="input"
              type="password"
              placeholder="请再次输入密码"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">昵称</label>
          <div class="control">
            <input
              v-model="form.nickname"
              class="input"
              type="text"
              placeholder="请输入昵称"
            />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button class="button is-link" @click="postAdd">注册</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,
    },
    head() {
      return {
        title: '用户注册',
      };
    },
    data() {
      return {
        form: {
          username: '',
          password: '',
          rePassword: '',
          nickname: '',
        },
      };
    },
    methods: {
      // 提交注册
      async postAdd() {
        try {
          const resp = await this.$axios.post('/api/user/add', this.form);
          console.log(resp);
          this.$router.push('/user/login'); // 注册成功跳转到登录页
        } catch (err) {
          alert(err.message || err);
        }
      },
    },
  };
</script>

<style>
  .container {
    min-height: 300px;
  }
</style>

然后我们在site目录下执行命令npm run dev来启动前端页面服务,服务启动成功之后,访问路径/user/reg来查看页面效果,效果图如下:

注册一个用户示例

用户登录页面

完成用户注册,接下来我们来完善用户登录功能。新建文件site/pages/user/login.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.username"
              class="input"
              type="text"
              placeholder="请输入用户名"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">密码</label>
          <div class="control">
            <input
              v-model="form.password"
              class="input"
              type="password"
              placeholder="请输入密码"
            />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button class="button is-link" @click="login">登录</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,
    },
    head() {
      return {
        title: '用户登录',
      };
    },
    data() {
      return {
        form: {
          username: '',
          password: '',
        },
      };
    },
    methods: {
      async login() {
        try {
          const resp = await this.$axios.post('/api/user/login', this.form);
          console.log('登录成功', resp);
        } catch (e) {
          alert(e.message || e);
        }
      },
    },
  };
</script>

<style>
  .container {
    min-height: 300px;
  }
</style>

然后我们访问页面/user/login就能够看到页面效果,输入我们刚刚注册的用户名、密码,点击登录按钮,然后我们打开浏览器控制台,会看到登录的用户信息,如下图:

如何记录登录状态

由于我们是前后端分离,Go 语言的服务端和 Nuxt.js 的页面服务无法共享 session,所以登录状态我们没法由 session 来存储,所以我们引入了token机制。在用户登录成功之后,同时为该用户生成一个token,它就相当于sessionId,通过它能够找到对应的用户,上面的登录功能在登录成功后服务端给我们返回了用户信息和token,所以登录成功之后需要由浏览器记录下token,并且每次Nuxt.js请求接口的时候都需要带上该token,这样接口服务在收到带token的请求之后就能够知道该请求是哪个用户发起的。总结流程如下:

  1. 调用登录接口,验证用户名密码,验证成功后接口返回授权令牌(userToken);
  2. 前端网页收到授权令牌(userToken)后,将他们存储到 cookie 中;
  3. 前端网页在每次请求后台接口的时候检查 cookie 中是否有userToken,如果有就带上;
  4. 服务端在收到网页中的接口请求时,检查请求中是否有合法的userToken,没有就返回错误要求网页进行登录;

所以我们需要借助 cookie 来存储token,并且在每次Nuxt.js请求接口的时候都需要带上 cookie 中的token

利用 cookie 存储 token

Nuxt.js 有 cookie 插件:cookie-universal-nuxt ,接下来我们来使用该插件。

安装cookie-universal-nuxt

npm i --save cookie-universal-nuxt

安装成功之后打开文件nuxt.config.js配置cookie-universal-nuxt,在 modules 中添加如下配置:

modules: [
  // Doc: https://github.com/nuxt-community/modules/tree/master/packages/bulma
  '@nuxtjs/bulma',
  // Doc: https://axios.nuxtjs.org/usage
  '@nuxtjs/axios',
  ['cookie-universal-nuxt', { alias: 'cookies' }]
]

接下来我们修改site/pages/user/login.vue文件,在登录成功之后将token保存到 cookie 中,修改之后的完整代码如下:

<template>
  <section>
    <my-nav />
    <section class="section">
      <div class="container">
        <div class="field">
          <label class="label">用户名</label>
          <div class="control">
            <input
              v-model="form.username"
              class="input"
              type="text"
              placeholder="请输入用户名"
            />
          </div>
        </div>
        <div class="field">
          <label class="label">密码</label>
          <div class="control">
            <input
              v-model="form.password"
              class="input"
              type="password"
              placeholder="请输入密码"
            />
          </div>
        </div>
        <div class="field is-grouped">
          <div class="control">
            <button class="button is-link" @click="login">登录</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,
    },
    head() {
      return {
        title: '用户登录',
      };
    },
    data() {
      return {
        form: {
          username: '',
          password: '',
        },
      };
    },
    methods: {
      async login() {
        try {
          const resp = await this.$axios.post('/api/user/login', this.form);
          console.log('登录成功', resp);
          this.$cookies.set('userToken', resp.token, {
            maxAge: 86400 * 7,
            path: '/',
          });
          this.$router.push('/');
        } catch (e) {
          alert(e.message || e);
        }
      },
    },
  };
</script>

<style>
  .container {
    min-height: 300px;
  }
</style>

Nuxt.js 的请求带上 token

前面我们已经对axios做了一个简单的封装,接下来我们继续修改该封装,让它在每次请求的时候都检查 cookie 中是否有 token,如果有就自动带上。修改文件site/plugins/axios.js,修改之后的完整内容如下:

import qs from 'qs';

export default function ({ $axios, $toast, app }) {
  $axios.onRequest((config) => {
    config.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
    const userToken = app.$cookies.get('userToken'); // 从cookie中获取token
    if (userToken) {
      // 如果找到了token,那么将token放到请求头中
      config.headers.common['X-User-Token'] = userToken;
    }
    config.transformRequest = [
      function (data) {
        if (process.client && data instanceof FormData) {
          // 如果是FormData就不转换
          return data;
        }
        data = qs.stringify(data);
        return data;
      },
    ];
  });

  $axios.onResponse((response) => {
    if (response.status !== 200) {
      return Promise.reject(response);
    }
    const jsonResult = response.data;
    if (jsonResult.success) {
      return Promise.resolve(jsonResult.data);
    } else {
      return Promise.reject(jsonResult);
    }
  });
}

这样我们在每次使用axios请求的 Go 语言接口时就都会检查并带上 cookie 中保存的token了。

总结

本实例中我们完成了整个用户的登录和注册模块,通过学习本章内容相信你已经具备开发一个独立模块的能力了。下面我们看下本实例完整源码的目录结构:

.
├── server
│   ├── go.mod
│   ├── go.sum
│   ├── main.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
    │   └── user
    │       ├── login.vue
    │       └── reg.vue
    ├── plugins
    │   ├── README.md
    │   └── axios.js
    ├── static
    │   ├── README.md
    │   └── favicon.ico
    └── store
        └── README.md