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 支持,但它不限制你的文件夹结构,这是你的选择。

结构取决于你自己的需求。 我们无法告诉你如何设计自己的应用程序,但你可以仔细查看下面的一个用例示例;

file

数据模型层

接下来开始写数据模型层  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 相关内容。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/iris-go/10/mvc_...

译文地址:https://learnku.com/docs/iris-go/10/mvc_...

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~