MVC 电影项目示例
MVC 电影项目示例
Iris 有一个非常强大和炽热 快速 的 MVC 支持,你可以从一个方法函数里返回你想要的任何类型的值,并且这个值会发送到你预期的客户端。
- 如果是
string
类型 那么它就是响应的 body。 - 如果
string
是第二个输出参数,那么它就是内容类型 - 如果是
int
,那么它就是返回的状态码。 - 如果
error
并且不为 nil 那么(任何类型)响应将被省略,并且将呈现带有 400 错误请求的错误文本。 - 如果
(int,error)
并且 error 不是nil,那么响应结果将是错误的文本,状态代码是上面提到的int
的状态码。 - 如果是
custom struct
或者interface{}
或者是slice
或者是map
,那么返回的响应除非跟随string
内容类型,否则它将呈现为json。 - 如果是
mvc.Result
,然后执行它的Dispatch
函数,这样就可以使用好的设计模式在需要的地方拆分模型的逻辑。
没有什么能阻止你使用自己喜欢的文件夹结构。 Iris 是一个低阶 Web 框架,它有一流的MVC 支持,但它不限制你的文件夹结构,这是你的选择。
结构取决于你自己的需求。 我们无法告诉你如何设计自己的应用程序,但你可以仔细查看下面的一个用例示例;
数据模型层
接下来开始写数据模型层 Movie
。
// 文件: datamodels/movie.go
package datamodels
// Movie 是基本数据的的结构体.
// 请注意公共标签(适用于我们的 web 应用)
// 应保存在 「web / viewmodels / movie.go」等其他文件中
// 它可以通过嵌入数据模型进行换行。
//电影或声明新的字段,但我们将使用此数据模型作为我们的应用程序
//中唯一的一个电影模型,为了简单起见。
type Movie struct {
ID int64 `json:"id"`
Name string `json:"name"`
Year int `json:"year"`
Genre string `json:"genre"`
Poster string `json:"poster"`
}
数据源/数据存储层
之后,我们继续为我们的 Movies
创建一个简单的内存存储。
// 文件: datasource/movies.go
package datasource
import "github.com/kataras/iris/_examples/mvc/overview/datamodels"
// 电影是我们想象中的数据源。
var Movies = map[int64]datamodels.Movie{
1: {
ID: 1,
Name: "Casablanca",
Year: 1942,
Genre: "Romance",
Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg",
},
2: {
ID: 2,
Name: "Gone with the Wind",
Year: 1939,
Genre: "Romance",
Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg",
},
3: {
ID: 3,
Name: "Citizen Kane",
Year: 1941,
Genre: "Mystery",
Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg",
},
4: {
ID: 4,
Name: "The Wizard of Oz",
Year: 1939,
Genre: "Fantasy",
Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg",
},
5: {
ID: 5,
Name: "North by Northwest",
Year: 1959,
Genre: "Thriller",
Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg",
},
}
库
可以直接访问“数据源”并可以直接操作数据的层。
这是可选的 (因为你也可以在 Service
内部使用),不过在展示这个例子的时候我们需要创建一个 Repository
,一个处理“低级”数据,可以直接访问 Movies
数据源的库。保留一个 Repository
,它是一个 interface
(接口) 因为可能会不同,取决于您开发的应用程序的状态,也就是说在production
开发状态下它会使用某些真正的SQL查询数据或者其他一些您用于查询的数据。
// 文件: repositories/movie_repository.go
package repositories
import (
"errors"
"sync"
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
)
// Query代表一种“访客”和它的查询动作。
type Query func(datamodels.Movie) bool
// MovieRepository会处理一些关于movie实例的基本的操作 。
// 这是一个以测试为目的的接口,即是一个内存中的movie库
// 或是一个连接到数据库的实例。
type MovieRepository interface {
Exec(query Query, action Query, limit int, mode int) (ok bool)
Select(query Query) (movie datamodels.Movie, found bool)
SelectMany(query Query, limit int) (results []datamodels.Movie)
InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error)
Delete(query Query, limit int) (deleted bool)
}
// NewMovieRepository返回一个新的基于内存的movie库。
// 库的类型在我们的例子中是唯一的。
func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository {
return &movieMemoryRepository{source: source}
}
// movieMemoryRepository就是一个"MovieRepository"
// 它负责存储于内存中的实例数据(map)
type movieMemoryRepository struct {
source map[int64]datamodels.Movie
mu sync.RWMutex
}
const (
// ReadOnlyMode will RLock(read) the data .
ReadOnlyMode = iota
// ReadWriteMode will Lock(read/write) the data.
ReadWriteMode
)
func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) {
loops := 0
if mode == ReadOnlyMode {
r.mu.RLock()
defer r.mu.RUnlock()
} else {
r.mu.Lock()
defer r.mu.Unlock()
}
for _, movie := range r.source {
ok = query(movie)
if ok {
if action(movie) {
loops++
if actionLimit >= loops {
break // break
}
}
}
}
return
}
// Select方法会收到一个查询方法
// 这个方法给出一个单独的movie实例
// 直到这个功能返回为true时停止迭代。
//
// 它返回最后一次查询成功所找到的结果的值
// 和最后的movie模型
// 以减少caller之间的通信
//
// 这是一个很简单但很聪明的雏形方法
// 我基本在所有会用到的地方使用自从我想到了它
// 也希望你们觉得好用
func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) {
found = r.Exec(query, func(m datamodels.Movie) bool {
movie = m
return true
}, 1, ReadOnlyMode)
// set an empty datamodels.Movie if not found at all.
if !found {
movie = datamodels.Movie{}
}
return
}
// SelectMany作用相同于Select但是它返回一个切片
// 切片包含一个或多个实例
// 如果传入的参数limit<=0则返回所有
func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) {
r.Exec(query, func(m datamodels.Movie) bool {
results = append(results, m)
return true
}, limit, ReadOnlyMode)
return
}
// InsertOrUpdate添加或者更新一个movie实例到(内存)储存中。
//
// 返回最新操作成功的实例或抛出错误。
func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) {
id := movie.ID
if id == 0 { // 创建一个新的操作
var lastID int64
// 找到最大的ID,避免重复。
// 在实际使用时您可以使用第三方库去生成
// 一个string类型的UUID
r.mu.RLock()
for _, item := range r.source {
if item.ID > lastID {
lastID = item.ID
}
}
r.mu.RUnlock()
id = lastID + 1
movie.ID = id
// map-specific thing
r.mu.Lock()
r.source[id] = movie
r.mu.Unlock()
return movie, nil
}
// 更新操作是基于movie.ID的,
// 在例子中我们允许了对poster和genre的更新(如果它们非空)。
// 当然我们可以只是做单纯的数据替换操作:
// r.source[id] = movie
// 并注释掉下面的代码;
current, exists := r.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
if !exists { // 当ID不存在时抛出一个error
return datamodels.Movie{}, errors.New("failed to update a nonexistent movie")
}
// 或者注释下面这段然后用 r.source[id] = m 做单纯替换
if movie.Poster != "" {
current.Poster = movie.Poster
}
if movie.Genre != "" {
current.Genre = movie.Genre
}
// map-specific thing
r.mu.Lock()
r.source[id] = current
r.mu.Unlock()
return movie, nil
}
func (r *movieMemoryRepository) Delete(query Query, limit int) bool {
return r.Exec(query, func(m datamodels.Movie) bool {
delete(r.source, m.ID)
return true
}, limit, ReadWriteMode)
}
服务层 Service
Service
可以访问 存储库
和 数据模型层
(如果是简单的应用,也可以访问 数据模型层
)的函数的层。它应该包含大部分的逻辑。
我们需要一个服务与存储库
在high-level
和 存储/检索 Movies
中进行通信,这将在下面的 Web控制器
上使用。
// file: services/movie_service.go
package services
import (
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/_examples/mvc/overview/repositories"
)
// `MovieService` 会处理一些 `movie` 数据模型层的 CRUID 操作
// 这取决于 `movie` 存储库 的一些行为.
//这里将数据源和高级组件进行解耦
// 所以,我们可以在不做任何修改的情况下,轻松的切换使用不同的储库类型
// 这个是一个通用的接口
//因为我们可能需要在不的地方修改和尝试不同的逻辑
type MovieService interface {
GetAll() []datamodels.Movie
GetByID(id int64) (datamodels.Movie, bool)
DeleteByID(id int64) bool
UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error)
}
// NewMovieService 返回默认的 movie 服务层.
func NewMovieService(repo repositories.MovieRepository) MovieService {
return &movieService{
repo: repo,
}
}
type movieService struct {
repo repositories.MovieRepository
}
// GetAll 返回所有的 movies.
func (s *movieService) GetAll() []datamodels.Movie {
return s.repo.SelectMany(func(_ datamodels.Movie) bool {
return true
}, -1)
}
// GetByID 根据 id 返回一个 movie .
func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) {
return s.repo.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
}
// UpdatePosterAndGenreByID 更新 一个 movie 的 poster 和 genre 字段.
func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) {
// update the movie and return it.
return s.repo.InsertOrUpdate(datamodels.Movie{
ID: id,
Poster: poster,
Genre: genre,
})
}
// DeleteByID 根据 id 删除一个 movie
//
// Returns true if deleted otherwise false.
func (s *movieService) DeleteByID(id int64) bool {
return s.repo.Delete(func(m datamodels.Movie) bool {
return m.ID == id
}, 1)
}
视图模型
视图模型,用于在客户端展现的结构。
例子:
import (
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/context"
)
type Movie struct {
datamodels.Movie
}
func (m Movie) IsValid() bool {
/* do some checks and return true if it's valid... */
return m.ID > 0
}
Iris 可以将任意的自定义数据结构转换成 HTTP 响应, 因此理论上, 下面这样的写法也是被允许的。
// 快速的完成 `kataras/iris/mvc#Result` 接口。
// 发送一个 `Movie`作为一个受限的HTTP响应。
//如果它的 ID 小于等于 0 ,会返回未找到的 404 错误,
// 否则会返回一个正常响应的 JSON 数据响应,
//(控制器默认情况下的返回数据类型)。
//
// 应用程序的逻辑不应该写在这里。
// 这只是响应前的一个验证步骤,
// 这里可以添加一些简单的检查。
//
//这只是一个案例,
// 当设计更大的应用程序时候,想象一下这个特性的潜力。
//
// 从这个控制器的方法函数返回值是 `Movie` 的类型。
// 例如 `controllers/movie_controller.go#GetBy`。
func (m Movie) Dispatch(ctx context.Context) {
if !m.IsValid() {
ctx.NotFound()
return
}
ctx.JSON(m, context.JSON{Indent: " "})
}
然而,我们将使用 "数据模型" 作为唯一的模型包 ,因为 Movie 的结构不包含任何的敏感数据。客户端可以看到时它所有的字段,并且不需要任何额外的功能或者验证。
控制器
处理 WEB 请求,是服务端和客户端之间的关联所在。
最重要的是 , Controller
是 Iris 框架的入口,它与 MovieService
之间进行通信。 我们通常将所有的与 HTTP 相关的内容存放在 web
名字的文件夹中,所以所有控制器都可以放在 web/controllers
目录下。当然也可以根据你个人的喜好 ,使用其他的设计模式。
// file: web/controllers/movie_controller.go
package controllers
import (
"errors"
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/_examples/mvc/overview/services"
"github.com/kataras/iris"
)
// MovieController is our /movies controller.
type MovieController struct {
// Our MovieService, it's an interface which
// is binded from the main application.
Service services.MovieService
}
// Get 返回 movies 的列表
// 演示:
// curl -i http://localhost:8080/movies
//
// The correct way if you have sensitive data:
// func (c *MovieController) Get() (results []viewmodels.Movie) {
// data := c.Service.GetAll()
//
// for _, movie := range data {
// results = append(results, viewmodels.Movie{movie})
// }
// return
// }
// 否则,只返回数据模型。
func (c *MovieController) Get() (results []datamodels.Movie) {
return c.Service.GetAll()
}
// GetBy 返回一个 movie
// 演示:
// curl -i http://localhost:8080/movies/1
func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) {
return c.Service.GetByID(id) // it will throw 404 if not found.
}
// PutBy 更新一个movie
// 演示:
// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1
func (c *MovieController) PutBy(ctx iris.Context, id int64) (datamodels.Movie, error) {
// 获取请求内的 poster 和 genre 字段数据
file, info, err := ctx.FormFile("poster")
if err != nil {
return datamodels.Movie{}, errors.New("failed due form file 'poster' missing")
}
// 关闭
file.Close()
// 假设这是一个上传文件的 url ...
poster := info.Filename
genre := ctx.FormValue("genre")
return c.Service.UpdatePosterAndGenreByID(id, poster, genre)
}
// DeleteBy 删除一个 movie
// 演示:
// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1
func (c *MovieController) DeleteBy(id int64) interface{} {
wasDel := c.Service.DeleteByID(id)
if wasDel {
// 返回被删除的 movie 的 id
return iris.Map{"deleted": id}
}
//在这里,我们可以看到一个方法函数可以返回两种类型中的任何一种(map 或者 int),
// 我们不用指定特定的返回类型。
return iris.StatusBadRequest
}
web/middleware
里面的最常用的一个中间件例子。
// file: web/middleware/basicauth.go
package middleware
import "github.com/kataras/iris/middleware/basicauth"
// BasicAuth middleware sample.
var BasicAuth = basicauth.New(basicauth.Config{
Users: map[string]string{
"admin": "password",
},
})
最后到 main.go
.
// file: main.go
package main
import (
"github.com/kataras/iris/_examples/mvc/overview/datasource"
"github.com/kataras/iris/_examples/mvc/overview/repositories"
"github.com/kataras/iris/_examples/mvc/overview/services"
"github.com/kataras/iris/_examples/mvc/overview/web/controllers"
"github.com/kataras/iris/_examples/mvc/overview/web/middleware"
"github.com/kataras/iris"
"github.com/kataras/iris/mvc"
)
func main() {
app := iris.New()
app.Logger().SetLevel("debug")
// 加载视图模板地址
app.RegisterView(iris.HTML("./web/views", ".html"))
// 注册控制器
// mvc.New(app.Party("/movies")).Handle(new(controllers.MovieController))
//你也可以使用 `mvc.Configure` 方法拆分编写 MVC 应用程序的配置。
// 如下所示:
mvc.Configure(app.Party("/movies"), movies)
// http://localhost:8080/movies
// http://localhost:8080/movies/1
app.Run(
// Start the web server at localhost:8080
iris.Addr("localhost:8080"),
// skip err server closed when CTRL/CMD+C pressed:
iris.WithoutServerError(iris.ErrServerClosed),
// enables faster json serialization and more:
iris.WithOptimizations,
)
}
// 注意 mvc.Application, 不是 iris.Application.
func movies(app *mvc.Application) {
// Add the basic authentication(admin:password) middleware
// for the /movies based requests.
app.Router.Use(middleware.BasicAuth)
// 使用数据源中的一些(内存)数据创建 movie 的数据库。
repo := repositories.NewMovieRepository(datasource.Movies)
// 创建 movie 的服务,我们将它绑定到 movie 应用程序。
movieService := services.NewMovieService(repo)
app.Register(movieService)
//初始化控制器
// 注意,你可以初始化多个控制器
// 你也可以 使用 `movies.Party(relativePath)` 或者 `movies.Clone(app.Party(...))` 创建子应用。
app.Handle(new(controllers.MovieController))
}
点击 这里 去查看 MVC loves Websockets
相关内容。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: