|
|
# 32|通用模块(下):用户模块开发
|
|
|
|
|
|
你好,我是轩脉刃。
|
|
|
|
|
|
上一节课我们设计好用户模块的需求后,开始了后端开发。在后端开发中我们明确了开发流程的四个步骤,先将接口swaggger化,再定义用户服务协议,接着开发模块接口,最后实现用户服务协议。而且上节课已经完成了接口swagger化,以及用户服务协议设计的模型部分。
|
|
|
|
|
|
这节课,我们就继续完成用户服务协议的定义,再开发模块接口和实现用户服务协议。
|
|
|
|
|
|
## 用户服务协议
|
|
|
|
|
|
前面我们设计好了一个模型User了,“接口优于实现”,来设计这个服务的接口,看看要提供哪些能力。
|
|
|
|
|
|
首先用户服务一定要提供的是预注册能力,所以提供了一个Register方法。预注册之后,我们还要提供发送邮件的能力,再提供一个发送邮件的接口SendRegisterMail。当然最后要提供一个确认注册用户的接口VerfityRegister。
|
|
|
|
|
|
在登录这块,用户服务一定要提供登录、登出的接口Login和Logout。同时由于所有业务请求,比如创建问题等逻辑,我们需要使用token来获取用户信息,所以我们也要提供验证登录的接口VerifyLogin。
|
|
|
|
|
|
于是整体的接口设计如下,详细信息都写在注释中了:
|
|
|
|
|
|
```go
|
|
|
// Service 用户相关的服务
|
|
|
type Service interface {
|
|
|
|
|
|
// Register 注册用户,注意这里只是将用户注册, 并没有激活, 需要调用
|
|
|
// 参数:user必填,username,password, email
|
|
|
// 返回值: user 带上token
|
|
|
Register(ctx context.Context, user *User) (*User, error)
|
|
|
// SendRegisterMail 发送注册的邮件
|
|
|
// 参数:user必填: username, password, email, token
|
|
|
SendRegisterMail(ctx context.Context, user *User) error
|
|
|
// VerifyRegister 注册用户,验证注册信息, 返回验证是否成功
|
|
|
VerifyRegister(ctx context.Context, token string) (bool, error)
|
|
|
|
|
|
// Login 登录相关,使用用户名密码登录,获取完成User信息
|
|
|
Login(ctx context.Context, user *User) (*User, error)
|
|
|
// Logout 登出
|
|
|
Logout(ctx context.Context, user *User) error
|
|
|
// VerifyLogin 登录验证
|
|
|
VerifyLogin(ctx context.Context, token string) (*User, error)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
这里也说明一下,要抽象设计出一个服务模块的协议确实不是一件很简单的事情,也不一定能一次性设计好。
|
|
|
|
|
|
我们说过,**从“服务需要提供哪些对外能力”的角度来思考,会比较完善**。比如这里为什么要设计一个VerfityLogin能力呢?系统对外提供的接口并没有这个服务,但是在内部,我们每次验证token的时候,会需要用token来验证和换取user的。所以这个接口的设计是有“需要”的。
|
|
|
|
|
|
服务协议的设计从需求出发,当遇到新的需求,不断迭代就可以了。
|
|
|
|
|
|
## 用户模块接口实现
|
|
|
|
|
|
设计了用户服务的协议,下一步我们也不是急于实现它,需要先验证下这些服务协议是否能满足我们的“需求”。如何验证呢?可以直接开发用户接口,确认是否有未满足的需求。
|
|
|
|
|
|
上一节课梳理了,要实现四个接口:
|
|
|
|
|
|
* app/http/module/user/api\_register.go
|
|
|
|
|
|
* app/http/module/user/api\_verify.go
|
|
|
|
|
|
* app/http/module/user/api\_login.go
|
|
|
|
|
|
* app/http/module/user/api\_logout.go
|
|
|
|
|
|
|
|
|
我们还是拿其中比较复杂的注册接口api\_register.go做一下说明,其他接口的实现没什么难点,你可以参考GitHub上的代码。
|
|
|
|
|
|
注册接口我们要做几个事情?**首先验证接口参数,其次要进行预注册,然后发送预注册的验证邮件,最后返回成功状态**。
|
|
|
|
|
|
验证接口参数之前讲过,使用定义好的 registerParam结构和Gin带有的binding逻辑就可以做参数的获取和验证了。预注册的逻辑,既然已经定义好了用户服务的预注册接口,这里可以直接调用这个接口Register。同样,发送验证邮件的接口我们也已经在用户服务中定义好了,直接调用SendRegisterMail即可。最终,返回成功状态,我们使用hade框架对Gin扩展的IStatusOk。
|
|
|
|
|
|
```go
|
|
|
func (api *UserApi) Register(c *gin.Context) {
|
|
|
// 验证参数
|
|
|
userService := c.MustMake(provider.UserKey).(provider.Service)
|
|
|
logger := c.MustMake(contract.LogKey).(contract.Log)
|
|
|
|
|
|
param := ®isterParam{}
|
|
|
if err := c.ShouldBind(param); err != nil {
|
|
|
c.ISetStatus(400).IText("参数错误"); return
|
|
|
}
|
|
|
|
|
|
// 注册对象
|
|
|
model := &provider.User{
|
|
|
UserName: param.UserName,
|
|
|
Password: param.Password,
|
|
|
Email: param.Email,
|
|
|
CreatedAt: time.Now(),
|
|
|
}
|
|
|
// 注册
|
|
|
userWithToken, err := userService.Register(c, model)
|
|
|
if err != nil {
|
|
|
logger.Error(c, err.Error(), map[string]interface{}{
|
|
|
"stack": fmt.Sprintf("%+v", err),
|
|
|
})
|
|
|
c.ISetStatus(500).IText(err.Error()); return
|
|
|
}
|
|
|
if userWithToken == nil {
|
|
|
c.ISetStatus(500).IText("注册失败"); return
|
|
|
}
|
|
|
|
|
|
if err := userService.SendRegisterMail(c, userWithToken); err != nil {
|
|
|
|
|
|
c.ISetStatus(500).IText("发送电子邮件失败"); return
|
|
|
}
|
|
|
|
|
|
c.ISetOkStatus().IText("注册成功,请前往邮箱查看邮件"); return
|
|
|
}
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
这里使用了之前我们对Gin框架扩展定义的Response结构中的链式方法:
|
|
|
|
|
|
```go
|
|
|
c.ISetOkStatus().IText("注册成功,请前往邮箱查看邮件");
|
|
|
|
|
|
```
|
|
|
|
|
|
很明显,这种方法确实比Gin框架自带的Response方法更为优雅轻便了。
|
|
|
|
|
|
实现好Register接口,我们基本确认了之前设计的用户服务中注册部分是满足需求的。再一个个接口 Verify/Login/Logout 都实现一下,基本能确定之前用户服务的设计是可以的。
|
|
|
|
|
|
## 开发模块接口
|
|
|
|
|
|
既然我们已经确定了用户服务设计可行,进入最后一步,实现这些用户服务定义的协议方法。在实现中,你能看到很多之前定义的各种服务的具体使用。用户注册的三个相关协议接口的实现,我们详细说一下,其他登录相关的接口协议,你可以参考GitHub上的代码。
|
|
|
|
|
|
### 预注册协议接口
|
|
|
|
|
|
```go
|
|
|
// Register 注册用户,注意这里只是将用户注册, 并没有激活, 需要调用
|
|
|
// 参数:user必填,username,password, email
|
|
|
// 返回值: user 带上token
|
|
|
Register(ctx context.Context, user *User) (*User, error)
|
|
|
|
|
|
```
|
|
|
|
|
|
预注册协议接口的具体实现要做几个事情:
|
|
|
|
|
|
1. 去数据库判断邮箱是否已经注册用户了,如果邮箱已经注册,那么这个预注册操作是不能执行的;
|
|
|
2. 去数据库判断用户名是否已经被注册了,如果用户名已经被注册了,那么预注册操作也是不能执行的;
|
|
|
3. 生成预注册的验证token;
|
|
|
4. 将要注册的用户存储在缓存中,存储1天,待用户注册验证。
|
|
|
|
|
|
我们注意到四步操作里**前面两步是去数据库的查询操作,所以可以使用hade框架的ORM服务**,先从容器中获取ORM服务,使用GetDB获取gorm.DB,接着就可以使用gorm的Where、First 等方法了。由于之前已经定义好了User结构作为数据库模型,所以我们直接使用这个模型:
|
|
|
|
|
|
```go
|
|
|
// 判断邮箱是否已经注册了
|
|
|
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
|
|
|
db, err := ormService.GetDB()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
userDB := &User{}
|
|
|
if db.Where(&User{Email: user.Email}).First(userDB).Error != gorm.ErrRecordNotFound {
|
|
|
return nil, errors.New("邮箱已注册用户,不能重复注册")
|
|
|
}
|
|
|
if db.Where(&User{UserName: user.UserName}).First(userDB).Error != gorm.ErrRecordNotFound {
|
|
|
return nil, errors.New("用户名已经被注册,请换一个用户名")
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
而第三步生成token,就使用一个简单的随机生成token的算法,直接去一排字符串中随机获取下标来生成token。
|
|
|
|
|
|
```go
|
|
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
|
|
func genToken(n int) string {
|
|
|
b := make([]byte, n)
|
|
|
for i := range b {
|
|
|
// 这里是随机获取的
|
|
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
|
|
}
|
|
|
return string(b)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
最后一步,需要将User对象存储到缓存中。我们使用key为"user:register:\[token\]"来存储User对象。
|
|
|
|
|
|
```go
|
|
|
// 将请求注册进入redis,保存一天
|
|
|
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
|
|
|
|
|
|
key := fmt.Sprintf("user:register:%v", user.Token)
|
|
|
if err := cacheService.SetObj(ctx, key, user, 24*time.Hour); err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
return user, nil
|
|
|
|
|
|
```
|
|
|
|
|
|
但是你还记得吗,**在hade的Cache服务中,如果直接使用SetObj和GetObj操作对象,那么这个对象必须实现BinaryMarshaler和BinaryUnMarshaler**。所以我们再给User对象实现这两个接口。在app/provider/user/service.go中:
|
|
|
|
|
|
```go
|
|
|
// MarshalBinary 实现BinaryMarshaler 接口
|
|
|
func (b *User) MarshalBinary() ([]byte, error) {
|
|
|
return json.Marshal(b)
|
|
|
}
|
|
|
|
|
|
// UnmarshalBinary 实现 BinaryUnMarshaler 接口
|
|
|
func (b *User) UnmarshalBinary(bt []byte) error {
|
|
|
return json.Unmarshal(bt, b)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
于是,用户服务的Register方法实现就写好了。在这个小小的方法中,我们已经演示了之前定义的ORM服务、Cache服务,所有的这些服务在服务容器中都可以得到。你可以具体感受一下,容器加服务协议在具体的业务代码中带来的便利。
|
|
|
|
|
|
### 发送邮件协议接口
|
|
|
|
|
|
```go
|
|
|
// SendRegisterMail 发送注册的邮件
|
|
|
// 参数:user必填: username, password, email, token
|
|
|
SendRegisterMail(ctx context.Context, user *User) error
|
|
|
|
|
|
```
|
|
|
|
|
|
发送邮件协议接口要实现的就是一个邮件发送的功能。在Golang中邮件发送功能也是有现成库的,[gomail](https://github.com/go-gomail/gomail)。这个库目前已经有3.4k star了,基于限制比较少的MIT协议。
|
|
|
|
|
|
发送电子邮件的方式其实有很多种,但是我们最好使用SMTP的方式来发送邮件,因为SMTP的服务提供方,基本上都是在互联网上已经认证的服务提供商,比如Gmail、126等。通过这些邮件服务提供商注册的SMTP账号发送邮件,基本上不会进入对方邮箱的“垃圾箱”中。
|
|
|
|
|
|
不过所有邮件服务提供商的SMTP账号都需要单独申请,但是基本都是免费的。这里是我使用126注册的邮箱申请了一个126的SMTP发送账号。
|
|
|
|
|
|
我们把SMTP的账号信息存储在配置文件config/development/app.yaml中:
|
|
|
|
|
|
```yaml
|
|
|
domain: "http://hadecast.funaio.cn"
|
|
|
|
|
|
smtp:
|
|
|
host: "smtp.126.com"
|
|
|
port: 25
|
|
|
from: "jianfengye110@126.com"
|
|
|
username: "jianfengye110"
|
|
|
password: "123456"
|
|
|
|
|
|
```
|
|
|
|
|
|
下面来演示如何使用gomail来通过SMTP账号发送邮件,我们直接看app/provider/user/service.go 的具体实现:
|
|
|
|
|
|
```go
|
|
|
func (u *UserService) SendRegisterMail(ctx context.Context, user *User) error {
|
|
|
logger := u.container.MustMake(contract.LogKey).(contract.Log)
|
|
|
configer := u.container.MustMake(contract.ConfigKey).(contract.Config)
|
|
|
|
|
|
// 配置服务中获取发送邮件需要的参数
|
|
|
host := configer.GetString("app.smtp.host")
|
|
|
port := configer.GetInt("app.smtp.port")
|
|
|
username := configer.GetString("app.smtp.username")
|
|
|
password := configer.GetString("app.smtp.password")
|
|
|
from := configer.GetString("app.smtp.from")
|
|
|
domain := configer.GetString("app.domain")
|
|
|
|
|
|
// 实例化gomail
|
|
|
d := gomail.NewDialer(host, port, username, password)
|
|
|
|
|
|
// 组装message
|
|
|
m := gomail.NewMessage()
|
|
|
m.SetHeader("From", from)
|
|
|
m.SetAddressHeader("To", user.Email, user.UserName)
|
|
|
m.SetHeader("Subject", "感谢您注册我们的hadecast")
|
|
|
link := fmt.Sprintf("%v/user/register/verify?token=%v", domain, user.Token)
|
|
|
m.SetBody("text/html", fmt.Sprintf("请点击下面的链接完成注册:%s", link))
|
|
|
|
|
|
// 发送电子邮件
|
|
|
if err := d.DialAndSend(m); err != nil {
|
|
|
logger.Error(ctx, "send email error", map[string]interface{}{
|
|
|
"err": err,
|
|
|
"message": m,
|
|
|
})
|
|
|
return err
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
首先通过hade的配置服务来获取SMTP的所有配置,使用这些配置实例化一个gomail.Dialer对象;然后创建邮件的内容,内容的From和To分别代表发送方和接收方,接收方自然就是我们的预注册用户填写的邮箱。将链接放在邮件的Body里面。组装好邮件内容之后,我们使用DailAndSend 就可以直接发送一个邮件到预注册的用户的邮箱了。
|
|
|
|
|
|
在这个过程中,**我们会希望如果发送邮箱失败的话,使用日志记录一下发送失败的原因和内容**。这个是很有必要的,因为后续如果希望有一些脚本能补发邮件,这个日志就很有帮助了。所以使用hade定义的日志服务,这里使用日志服务的Error方法来记录发送邮件错误信息。
|
|
|
|
|
|
不管配置服务还是日志服务,都是从服务容器中可以获取到。按照上述逻辑,发送邮件的接口就完成了。
|
|
|
|
|
|
### 注册验证协议接口
|
|
|
|
|
|
注册相关的最后一个协议接口是验证注册。
|
|
|
|
|
|
```go
|
|
|
// VerifyRegister 注册用户,验证注册信息, 返回验证是否成功
|
|
|
VerifyRegister(ctx context.Context, token string) (bool, error)
|
|
|
|
|
|
```
|
|
|
|
|
|
这个协议接口逻辑会复杂一些了。
|
|
|
|
|
|
它的参数为一个token,我们首先要拿着这个token去缓存中,获取到这个token对应的预注册用户。由于之前已经将User实现了BinaryUnmarshaler接口,这里就使用缓存服务的GetObj方法:
|
|
|
|
|
|
```go
|
|
|
//验证token
|
|
|
cacheService := u.container.MustMake(contract.CacheKey).(contract.CacheService)
|
|
|
key := fmt.Sprintf("user:register:%v", token)
|
|
|
user := &User{}
|
|
|
if err := cacheService.GetObj(ctx, key, user); err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
if user.Token != token {
|
|
|
return false, nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
然后下一步,**由于预注册和注册验证过程是异步的,中间数据库是有可能发生变化的**,所以我们需要再次验证一下这个用户在数据库中是否已经存在了,他的用户名和邮箱是否是唯一的。
|
|
|
|
|
|
```go
|
|
|
//验证邮箱,用户名的唯一
|
|
|
ormService := u.container.MustMake(contract.ORMKey).(contract.ORMService)
|
|
|
db, err := ormService.GetDB()
|
|
|
if err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
userDB := &User{}
|
|
|
if db.Where(&User{Email: user.Email}).First(userDB).Error != gorm.ErrRecordNotFound {
|
|
|
return false, errors.New("邮箱已注册用户,不能重复注册")
|
|
|
}
|
|
|
if db.Where(&User{UserName: user.UserName}).First(userDB).Error != gorm.ErrRecordNotFound {
|
|
|
return false, errors.New("用户名已经被注册,请换一个用户名")
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
最后准备将这个缓存中的用户存储进入数据库users表。这里我们知道,缓存中预注册用户的密码是用户填写的真实密码。但是将真实密码直接存储进入数据库,是一个非常不安全的做法。如果我们的数据库被黑客攻击拖库了,这对我们的网站用户是个非常大的影响。所以这里我们**有必要对用户的密码做一次加密操作**。
|
|
|
|
|
|
Golang的 [golang.org/x/crypto/bcrypt](https://pkg.go.dev/golang.org/x/crypto/bcrypt) 库提供了对密码进行加密的标准方法。还记得这种golang.org/x/ 开头的库么,可以说是Golang标准库的预备库,我们可以直接放心使用。在这个库中,提供了加密密码的方法 GenerateFromPassword 和验证密码的方法 CompareHashAndPassword 。
|
|
|
|
|
|
在这个函数中,我们就使用到加密密码的方法:
|
|
|
|
|
|
```go
|
|
|
// 验证成功将密码存储数据库之前需要加密,不能原文存储进入数据库
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.MinCost)
|
|
|
if err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
GenerateFromPassword 的第二个参数cost,是表示加密密码的复杂度,最小必须为MinCost。
|
|
|
|
|
|
最后一步,就是将用户存储到数据库中了。同样使用的是Gorm,其中有个Create方法,能将对象保存进入数据库:
|
|
|
|
|
|
```go
|
|
|
user.Password = string(hash)
|
|
|
|
|
|
// 具体在数据库创建用户
|
|
|
if err := db.Create(user).Error; err != nil {
|
|
|
return false, err
|
|
|
}
|
|
|
return true, nil
|
|
|
|
|
|
```
|
|
|
|
|
|
以上,就完成了注册实现的具体方法了。
|
|
|
|
|
|
## 后端调试
|
|
|
|
|
|
现在,汇总两节课的成果,我们完成了用户服务、用户模块接口以及swagger的搭建。之后就可以很方便地使用swagger来调试用户模块和用户服务了。
|
|
|
|
|
|
记得开启hade特有的调试模式: `./bbs dev backend`
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/85/c6/8560931d5c87e56860fa87ca1e1b28c6.png?wh=1084x336)
|
|
|
|
|
|
打开浏览器 [http://localhost:8070/swagger/index.html](http://localhost:8070/swagger/index.html) 看到swagger-UI界面。点击要调试接口的 “try it out” 按钮进入接口调用,填写要调用的接口参数,点击“Execute” 调用接口,并且获取接口返回值。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/76/aa/765346a7f820dbfc8fa61408faffceaa.png?wh=1920x1111)
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/2c/25/2cf2bb0168135e98f02fb84016590a25.png?wh=1920x1068)
|
|
|
|
|
|
如果接口调用错误,我们要修改接口,只需要直接在IDE上修改代码,并且直接保存,hade就会检测到文件更新,并且重新编译重启服务,立刻生效。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/9f/b1/9fa0d12a5548781869fd52041e080ab1.png?wh=1078x674)
|
|
|
|
|
|
## 前端开发
|
|
|
|
|
|
关于用户接口的前端开发部分,由于并不是我们课程的重点,就简要描述一下关键实现点。
|
|
|
|
|
|
前端一共就两个页面,注册页面和登录页面,所以我们在src/views/中创建两个文件夹,register和login,分别存储这两个页面。这里我主要也描述一下注册页面的具体实现。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/26/08/2669c8cdbbfd66df207e8b97f06ba508.png?wh=788x848)
|
|
|
|
|
|
在注册页面上,实际上是搭建了一个表单,在element-UI中我们可以使用el-form来方便搭建一个漂亮的表单, 这个表单包含用户名、邮箱、密码等信息,并且这些信息都对应script中的form数据。在表单的按钮,按钮点击行为我们设置成触发submitFrom方法。在src/views/register/index.vue中:
|
|
|
|
|
|
```plain
|
|
|
<el-form v-model="form" class="register-form">
|
|
|
<el-form-item >
|
|
|
<el-input v-model="form.username" placeholder="用户名" ></el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item >
|
|
|
<el-input v-model="form.email" placeholder="邮箱"></el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item >
|
|
|
<el-input
|
|
|
placeholder="密码"
|
|
|
type="password"
|
|
|
v-model="form.password"
|
|
|
></el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item >
|
|
|
<el-input
|
|
|
placeholder="确认密码"
|
|
|
type="password"
|
|
|
v-model="form.repassword"
|
|
|
></el-input>
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button
|
|
|
:loading="loading"
|
|
|
class="login-button"
|
|
|
type="primary"
|
|
|
native-type="submit"
|
|
|
@click="submitForm"
|
|
|
block
|
|
|
>注册</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
```
|
|
|
|
|
|
```plain
|
|
|
<script>
|
|
|
export default {
|
|
|
name: "register",
|
|
|
data() {
|
|
|
return {
|
|
|
form: {
|
|
|
username: '', // 用户名
|
|
|
password: '', // 密码
|
|
|
email: '', // 邮箱
|
|
|
repassword: '' // 重复输入密码
|
|
|
},
|
|
|
loading: false,
|
|
|
};
|
|
|
},
|
|
|
|
|
|
```
|
|
|
|
|
|
对应的submitFrom方法,就调用第30节课介绍的封装了axios库的request.js。我们使用request,并且传递上面输入的form对象数据给后端,如果请求返回成功,就返回返回体中的成功信息。
|
|
|
|
|
|
```plain
|
|
|
methods: {
|
|
|
submitForm: function(e) {
|
|
|
if (this.form.repassword !== this.form.password) {
|
|
|
this.$message.error("两次输入密码不一致");
|
|
|
return;
|
|
|
}
|
|
|
const that = this;
|
|
|
request({
|
|
|
url: '/user/register',
|
|
|
method: 'post',
|
|
|
data: this.form
|
|
|
}).then(function (response) {
|
|
|
const msg = response.data
|
|
|
that.$message.success(msg);
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
注册界面的开发就完成了,虽然逻辑比较简单,但也是使用了前面介绍的几个前端组件vue、element-Ui、axios,所以如果你看源码,对这几个组件的使用有一些疑惑的话,还是要研究一下每一个前端组件。
|
|
|
|
|
|
写完前端之后,别忘记我们的hade模块的强大调试功能之一:可以前后端同时调试。
|
|
|
|
|
|
使用命令 `./hade dev all` 开启前后端同时调试模式:
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/94/08/9422ea8fc5a9fc0f33d2348cfc340308.png?wh=1015x733)
|
|
|
|
|
|
控制台可以看到前端和后端都已经编译运行了。然后我们通过 [http://localhost:8070/#/register](http://localhost:8070/#/register) 直接看到前端页面:
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/c1/b4/c1d21d6f0eb943f48a062d294820e4b4.png?wh=536x484)
|
|
|
|
|
|
如果我们发现接口或者页面有需要修改的地方,直接修改前后端的代码即可重新编译,直接调试:
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/bd/6a/bd0148ac02be4e707c9086fb6313036a.png?wh=471x244)
|
|
|
|
|
|
这节课我们实现了用户模块的前后端的开发,代码改动量较大,已经提交到 [geekbang/32](https://github.com/gohade/bbs/tree/geekbang/32) 分支了。欢迎比对查看。
|
|
|
|
|
|
## 小结
|
|
|
|
|
|
这节课我们就完完整整做好了用户模块的开发。还是再啰嗦强调一下,后端开发的四个步骤:先将接口swaggger化、再定义用户服务协议、接着开发模块接口、最后实现用户服务协议。**服务模块的协议设计不一定能一次性抽象好,可以从服务需要提供哪些对外能力”的角度来思考,从需求出发,遇到新的需求,不断迭代你的设计就可以**。
|
|
|
|
|
|
同时关于前端开发,我们重点讲了一下如何使用element-UI来构建页面,以及如何使用axios来向后端发送请求。要掌握前后端都开发完成之后的调试方式,使用dev all 的调试模式来同时调试前后端,这个能让你的开发速度提高不少。
|
|
|
|
|
|
### 思考题
|
|
|
|
|
|
对于用户服务来说,我们定义了一个VerifyLogin的接口,根据token来获取对应的user信息。这个你觉得应该在哪里使用?怎么使用呢?
|
|
|
|
|
|
欢迎在留言区分享你的学习笔记。感谢你的收听,如果觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。下节课我们实战继续。
|
|
|
|