mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
add new responses, file-server (and mvc)
parent
16708aa8b6
commit
bf7330a4d6
108
File-server.md
108
File-server.md
|
@ -1,108 +0,0 @@
|
|||
Serve static files from a specific directory (system physical or embedded to the application) is done by the `Party.HandleDir` method.
|
||||
|
||||
`HandleDir` registers a handler that serves HTTP requests
|
||||
with the contents of a file system (physical or embedded).
|
||||
|
||||
* First parameter : the route path
|
||||
* Second parameter : the system or the embedded directory that needs to be served
|
||||
* Third parameter : not required, the directory options, set fields is optional.
|
||||
|
||||
Returns the GET *Route.
|
||||
|
||||
```go
|
||||
HandleDir(requestPath, directory string, opts ...DirOptions) (getRoute *Route)
|
||||
```
|
||||
|
||||
The `DirOptions` structure looks like this:
|
||||
|
||||
```go
|
||||
type DirOptions struct {
|
||||
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
|
||||
// then it redirects to **/*(/) which another handler is handling it,
|
||||
// that another handler, called index handler, is auto-registered by the framework
|
||||
// if end developer wasn't managed to handle it manually/by hand.
|
||||
IndexName string
|
||||
// Should files served under gzip compression?
|
||||
Gzip bool
|
||||
|
||||
// List the files inside the current requested directory if `IndexName` not found.
|
||||
ShowList bool
|
||||
// If `ShowList` is true then this function will be used instead
|
||||
// of the default one to show the list of files of a current requested directory(dir).
|
||||
DirList func(ctx iris.Context, dirName string, dir http.File) error
|
||||
|
||||
// When embedded.
|
||||
Asset func(name string) ([]byte, error)
|
||||
AssetInfo func(name string) (os.FileInfo, error)
|
||||
AssetNames func() []string
|
||||
|
||||
// Optional validator that loops through each found requested resource.
|
||||
AssetValidator func(ctx iris.Context, name string) bool
|
||||
}
|
||||
```
|
||||
|
||||
Let's say that you have an `./assets` folder near to your executable and you want the files to be served through `http://localhost:8080/static/**/*` route.
|
||||
|
||||
```go
|
||||
app := iris.New()
|
||||
|
||||
app.HandleDir("/static", "./assets")
|
||||
|
||||
app.Listen(":8080")
|
||||
```
|
||||
|
||||
Now, if you want to embed the static files to be lived inside the executable build in order to not depend on a system directory you can use a tool like [go-bindata](https://github.com/go-bindata/go-bindata) to convert the files into `[]byte` inside your program. Let's take a quick tutorial on this and how Iris helps to serve those data.
|
||||
|
||||
Install go-bindata:
|
||||
|
||||
```sh
|
||||
go get -u github.com/go-bindata/go-bindata/...
|
||||
```
|
||||
|
||||
Navigate to your program directory, that the `./assets` subdirectory exists and execute:
|
||||
|
||||
```sh
|
||||
$ go-bindata ./assets/...
|
||||
```
|
||||
|
||||
The above creates a generated go file which contains three main functions: `Asset`, `AssetInfo` and `AssetNames`. Use them on the `DirOptions`:
|
||||
|
||||
```go
|
||||
// [app := iris.New...]
|
||||
|
||||
app.HandleDir("/static", "./assets", iris.DirOptions {
|
||||
Asset: Asset,
|
||||
AssetInfo: AssetInfo,
|
||||
AssetNames: AssetNames,
|
||||
Gzip: false,
|
||||
})
|
||||
```
|
||||
|
||||
Build your app:
|
||||
|
||||
```sh
|
||||
$ go build
|
||||
```
|
||||
|
||||
The `HandleDir` supports all the standards, including content-range, for both physical and embedded directories.
|
||||
|
||||
However, if you just need a handler to work with, without register a route, you can use the `iris.FileServer` package-level function instead.
|
||||
|
||||
The `FileServer` function returns a Handler which serves files from a specific system, phyisical, directory or an embedded one.
|
||||
|
||||
* First parameter: is the directory.
|
||||
* Second parameter: optional parameter is any optional settings that the caller can use.
|
||||
|
||||
```go
|
||||
iris.FileServer(directory string, options ...DirOptions)
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```go
|
||||
handler := iris.FileServer("./assets", iris.DirOptions {
|
||||
ShowList: true, Gzip: true, IndexName: "index.html",
|
||||
})
|
||||
```
|
||||
|
||||
Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server
|
2
Home.md
2
Home.md
|
@ -7,5 +7,3 @@
|
|||
**Iris** can be used as a web port for [gRPC](https://grpc.io/). REST API for gRPC services.
|
||||
|
||||
The source code of Iris is hosted on [GitHub ](https://github.com/kataras/iris) and licensed under the terms of [BSD 3-clause License](https://opensource.org/licenses/BSD-3-Clause), like the [Go project](https://github.com/golang/go) itself.
|
||||
|
||||
> Documentation refers to the [v12.1.8 stable release](https://github.com/kataras/iris/tree/v12.1.8). Want to learn about the upcoming release? Head over to the [HISTORY.md#next](https://github.com/kataras/iris/blob/master/HISTORY.md#next) instead.
|
||||
|
|
|
@ -5,8 +5,8 @@ Prepare a cup of coffee or tea, whatever pleases you the most, and read some art
|
|||
* [How to write a Go API Part 1: A Webserver With Iris](https://bit.ly/32xmf4Q)
|
||||
* [How to write a Go API Part 2: Database Integration With GORM](https://bit.ly/34PvKxR)
|
||||
* [How to write a Go API Part 3: Testing With Dockertest](https://bit.ly/2NoZziF)
|
||||
* [A URL Shortener Service using Go, Iris and Bolt \(Updated\)](https://bit.ly/2KeP6pE)
|
||||
* [CRUD REST API in Iris \(a framework for golang\)](https://bit.ly/2X9EsXl)
|
||||
* [A URL Shortener Service using Go, Iris and Bolt (Updated)](https://bit.ly/2KeP6pE)
|
||||
* [CRUD REST API in Iris (a framework for golang)](https://bit.ly/2X9EsXl)
|
||||
* [Monitor APIs in realtime using Go and Iris](https://pusher.com/tutorials/monitor-api-go)
|
||||
* [A Todo MVC Application using Iris and Vue.js](https://bit.ly/2yjBvoZ)
|
||||
* [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://bit.ly/2Kfdsjf)
|
||||
|
|
27
_Sidebar.md
27
_Sidebar.md
|
@ -24,7 +24,6 @@
|
|||
* [[Forms]]
|
||||
* [[Model Validation|Model-validation]]
|
||||
* [[Cache]]
|
||||
* [[File Server|File-server]]
|
||||
* [[View]]
|
||||
* [[Cookies]]
|
||||
* [[Sessions]]
|
||||
|
@ -37,6 +36,32 @@
|
|||
* [[Sitemap]]
|
||||
* [[Localization]]
|
||||
* [[Testing]]
|
||||
* 🚀Responses
|
||||
* [Text](responses/text.md)
|
||||
* [HTML](responses/html.md)
|
||||
* [Markdown](responses/markdown.md)
|
||||
* [XML](responses/xml.md)
|
||||
* [YAML](responses/yaml.md)
|
||||
* [Binary](responses/binary.md)
|
||||
* [JSON](responses/json.md)
|
||||
* [JSONP](responses/jsonp.md)
|
||||
* [Problem](responses/problem.md)
|
||||
* [Protocol Buffers](responses/protobuf.md)
|
||||
* [MessagePack](responses/messagepack.md)
|
||||
* [Gzip](responses/gzip.md)
|
||||
* [Content Negotitation](responses/content-negotiation.md)
|
||||
* [Stream](responses/stream.md)
|
||||
* [Server-Sent Events](responses/sse.md)
|
||||
* [HTTP/2 Push](responses/http2_push.md)
|
||||
* [Recorder](responses/recorder.md)
|
||||
* [Outroduction](responses/outroduction.md)
|
||||
* [➲ Examples](https://github.com/kataras/iris/tree/master/_examples/response-writer)
|
||||
* 📁File Server
|
||||
* [Introduction](file-server/introduction.md)
|
||||
* [Listing](file-server/listing.md)
|
||||
* [In-memory Cache](file-server/memory-cache.md)
|
||||
* [Serve files from Context](file-server/context-file-server.md)
|
||||
* [➲ Examples](https://github.com/kataras/iris/tree/master/_examples/file-server)
|
||||
* 🤓Resources
|
||||
* [[Starter Kits|Starter-kits]]
|
||||
* [[Publications]]
|
||||
|
|
BIN
_assets/http2push.png
Normal file
BIN
_assets/http2push.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
96
file-server/context-file-server.md
Normal file
96
file-server/context-file-server.md
Normal file
|
@ -0,0 +1,96 @@
|
|||
# Serve Files from Context
|
||||
|
||||
```go
|
||||
// SendFile sends a file as an attachment, that is downloaded and
|
||||
// saved locally from client.
|
||||
// Note that compression can be registered through `ctx.CompressWriter(true)`
|
||||
// or `app.Use(iris.Compression)`.
|
||||
// Use `ServeFile` if a file should be served as a page asset instead.
|
||||
SendFile(filename string, destinationName string) error
|
||||
// SendFileWithRate same as `SendFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
SendFileWithRate(src, destName string, limit float64, burst int) error
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
Force-Send a file to the client:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
src := "./files/first.zip"
|
||||
ctx.SendFile(src, "client.zip")
|
||||
}
|
||||
```
|
||||
|
||||
Limit download speed to ~50Kb/s with a burst of 100KB:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
src := "./files/big.zip"
|
||||
// optionally, keep it empty to resolve the filename based on the "src".
|
||||
dest := ""
|
||||
|
||||
limit := 50.0 * iris.KB
|
||||
burst := 100 * iris.KB
|
||||
ctx.SendFileWithRate(src, dest, limit, burst)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ServeContent replies to the request using the content in the
|
||||
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
|
||||
// is that it handles Range requests properly, sets the MIME type, and
|
||||
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
|
||||
// and If-Range requests.
|
||||
//
|
||||
// If the response's Content-Type header is not set, ServeContent
|
||||
// first tries to deduce the type from name's file extension.
|
||||
//
|
||||
// The name is otherwise unused; in particular it can be empty and is
|
||||
// never sent in the response.
|
||||
//
|
||||
// If modtime is not the zero time or Unix epoch, ServeContent
|
||||
// includes it in a Last-Modified header in the response. If the
|
||||
// request includes an If-Modified-Since header, ServeContent uses
|
||||
// modtime to decide whether the content needs to be sent at all.
|
||||
//
|
||||
// The content's Seek method must work: ServeContent uses
|
||||
// a seek to the end of the content to determine its size.
|
||||
//
|
||||
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
|
||||
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
|
||||
//
|
||||
// Note that *os.File implements the io.ReadSeeker interface.
|
||||
// Note that compression can be registered through
|
||||
// `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`.
|
||||
ServeContent(content io.ReadSeeker, filename string, modtime time.Time)
|
||||
// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
|
||||
// and though writing the "content" to the client.
|
||||
ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int)
|
||||
// ServeFile replies to the request with the contents of the named
|
||||
// file or directory.
|
||||
//
|
||||
// If the provided file or directory name is a relative path, it is
|
||||
// interpreted relative to the current directory and may ascend to
|
||||
// parent directories. If the provided name is constructed from user
|
||||
// input, it should be sanitized before calling `ServeFile`.
|
||||
//
|
||||
// Use it when you want to serve assets like css and javascript files.
|
||||
// If client should confirm and save the file use the `SendFile` instead.
|
||||
// Note that compression can be registered through
|
||||
// `ctx.CompressWriter(true)`
|
||||
// or `app.Use(iris.Compression)`.
|
||||
ServeFile(filename string) error
|
||||
// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
|
||||
// and though writing the file to the client.
|
||||
ServeFileWithRate(filename string, limit float64, burst int) error
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
ctx.ServeFile("./public/main.js")
|
||||
}
|
||||
```
|
243
file-server/introduction.md
Normal file
243
file-server/introduction.md
Normal file
|
@ -0,0 +1,243 @@
|
|||
Serve static files from a specific directory (system physical or embedded to the application) is done by the `Party.HandleDir` method.
|
||||
|
||||
`HandleDir` registers a handler that serves HTTP requests with the contents of a file system (physical or embedded).
|
||||
|
||||
* First parameter : the route path
|
||||
* Second parameter : the file system that needs to be served
|
||||
* Third parameter : optional, the directory options.
|
||||
|
||||
```go
|
||||
HandleDir(requestPath string, fs http.FileSystem, opts ...DirOptions) ( []*Route)
|
||||
```
|
||||
|
||||
The `DirOptions` structure looks like this:
|
||||
|
||||
```go
|
||||
type DirOptions struct {
|
||||
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
|
||||
// then it redirects to **/*(/) which another handler is handling it,
|
||||
// that another handler, called index handler, is auto-registered by the framework
|
||||
// if end developer does not managed to handle it by hand.
|
||||
IndexName string
|
||||
// PushTargets filenames (map's value) to
|
||||
// be served without additional client's requests (HTTP/2 Push)
|
||||
// when a specific request path (map's key WITHOUT prefix)
|
||||
// is requested and it's not a directory (it's an `IndexFile`).
|
||||
//
|
||||
// Example:
|
||||
// "/": {
|
||||
// "favicon.ico",
|
||||
// "js/main.js",
|
||||
// "css/main.css",
|
||||
// }
|
||||
PushTargets map[string][]string
|
||||
// PushTargetsRegexp like `PushTargets` but accepts regexp which
|
||||
// is compared against all files under a directory (recursively).
|
||||
// The `IndexName` should be set.
|
||||
//
|
||||
// Example:
|
||||
// "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$")
|
||||
// See `iris.MatchCommonAssets` too.
|
||||
PushTargetsRegexp map[string]*regexp.Regexp
|
||||
|
||||
// Cache to enable in-memory cache and pre-compress files.
|
||||
Cache DirCacheOptions
|
||||
// When files should served under compression.
|
||||
Compress bool
|
||||
|
||||
// List the files inside the current requested directory if `IndexName` not found.
|
||||
ShowList bool
|
||||
// If `ShowList` is true then this function will be used instead
|
||||
// of the default one to show the list of files of a current requested directory(dir).
|
||||
// See `DirListRich` package-level function too.
|
||||
DirList DirListFunc
|
||||
|
||||
// Files downloaded and saved locally.
|
||||
Attachments Attachments
|
||||
|
||||
// Optional validator that loops through each requested resource.
|
||||
AssetValidator func(ctx *context.Context, name string) bool
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Let's say that you have an `./assets` folder near to your executable and you want the files to be served through `http://localhost:8080/static/**/*` route.
|
||||
|
||||
```go
|
||||
app := iris.New()
|
||||
|
||||
app.HandleDir("/static", iris.Dir("./assets"))
|
||||
|
||||
app.Listen(":8080")
|
||||
```
|
||||
|
||||
Now, if you want to embed the static files to be lived inside the executable build in order to not depend on a system directory you can use a tool like [go-bindata](https://github.com/go-bindata/go-bindata) to convert the files into `[]byte` inside your program. Let's take a quick tutorial on this and how Iris helps to serve those data.
|
||||
|
||||
Install go-bindata:
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/go-bindata/go-bindata/v3/go-bindata
|
||||
```
|
||||
|
||||
Navigate to your program directory, that the `./assets` subdirectory exists and execute:
|
||||
|
||||
```bash
|
||||
$ go-bindata -fs -prefix "assets" ./assets/...
|
||||
```
|
||||
|
||||
The above creates a generated go file which contains a `AssetFile()` functions that returns a compatible `http.FileSystem` you can give to Iris to serve the files.
|
||||
|
||||
```go
|
||||
// [app := iris.New...]
|
||||
|
||||
app.HandleDir("/static", AssetFile())
|
||||
```
|
||||
|
||||
Run your app:
|
||||
|
||||
```bash
|
||||
$ go run .
|
||||
```
|
||||
|
||||
The `HandleDir` supports all the standards, including content-range, for both physical, embedded and cached directories.
|
||||
|
||||
However, if you just need a handler to work with, without register a route, you can use the `iris.FileServer` package-level function instead.
|
||||
|
||||
The `FileServer` function returns a Handler which serves files from a specific system directory, an embedded one or a memory-cached one.
|
||||
|
||||
```go
|
||||
iris.FileServer(fs http.FileSystem, options DirOptions)
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```go
|
||||
handler := iris.FileServer(iris.Dir("./assets"), iris.DirOptions {
|
||||
ShowList: true, Compress: true, IndexName: "index.html",
|
||||
})
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Let's create an application that users can upload one or more files and list them.
|
||||
|
||||
Copy-paste the contents of **main.go**:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Mkdir("./uploads", 0700)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.RegisterView(iris.HTML("./views", ".html"))
|
||||
// Serve assets (e.g. javascript, css).
|
||||
app.HandleDir("/public", iris.Dir("./public"))
|
||||
|
||||
app.Get("/", index)
|
||||
|
||||
app.Get("/upload", uploadView)
|
||||
app.Post("/upload", upload)
|
||||
|
||||
app.HandleDir("/files", iris.Dir("./uploads"), iris.DirOptions{
|
||||
Compress: true,
|
||||
ShowList: true,
|
||||
})
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func index(ctx iris.Context) {
|
||||
ctx.Redirect("/upload")
|
||||
}
|
||||
|
||||
func uploadView(ctx iris.Context) {
|
||||
now := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(now, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
ctx.View("upload.html", token)
|
||||
}
|
||||
|
||||
const maxSize = 10 * iris.MB
|
||||
|
||||
func upload(ctx iris.Context) {
|
||||
ctx.SetMaxRequestBodySize(maxSize)
|
||||
|
||||
_, err := ctx.UploadFormFiles("./uploads", beforeSave)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusPayloadTooRage, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect("/files")
|
||||
}
|
||||
|
||||
func beforeSave(ctx iris.Context, file *multipart.FileHeader) {
|
||||
ip := ctx.RemoteAddr()
|
||||
ip = strings.ReplaceAll(ip, ".", "_")
|
||||
ip = strings.ReplaceAll(ip, ":", "_")
|
||||
|
||||
file.Filename = ip + "-" + file.Filename
|
||||
}
|
||||
```
|
||||
|
||||
The **./views/upload.html** is a simple html form, looks like that:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Files</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/upload" method="POST">
|
||||
<input type="file" id="upload_files" name="upload_files" multiple />
|
||||
<input type="hidden" name="token" value="{{.}}" />
|
||||
|
||||
<input type="button" value="Upload Using JS" onclick="uploadFiles()" />
|
||||
<input type="submit" value="Upload by submiting the form" />
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
function uploadFiles() {
|
||||
let files = document.getElementById("upload_files").files;
|
||||
let formData = new FormData();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
formData.append("files[]", files[i]);
|
||||
}
|
||||
|
||||
fetch('/upload',
|
||||
{
|
||||
method: "POST",
|
||||
body: formData
|
||||
}).
|
||||
then(data => window.location = "/files").
|
||||
catch(e => window.alert("upload failed: file too large"));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
366
file-server/listing.md
Normal file
366
file-server/listing.md
Normal file
|
@ -0,0 +1,366 @@
|
|||
By default Iris will not list files and sub-directories when client requests a path of a directory (e.g. `http://localhost:8080/files/folder`). To enable file listing you just set `DirOptions.ShowList` to `true`:
|
||||
|
||||
```go
|
||||
options := iris.DirOptions{
|
||||
// [...]
|
||||
ShowList: true,
|
||||
}
|
||||
|
||||
app.HandleDir("/files", iris.Dir("./uploads"), options)
|
||||
```
|
||||
|
||||
By default listing is rendering a simple page of `<a href` html links, like the standard `net/http` package does. To change the default behavior use the `DirOptions.DirList` field, which accepts a function of form:
|
||||
|
||||
```go
|
||||
type DirListFunc func(ctx iris.Context, dirOptions iris.DirOptions, dirName string, dir http.File) error
|
||||
```
|
||||
|
||||
```go
|
||||
options := iris.DirOptions{
|
||||
// [...]
|
||||
ShowList: true,
|
||||
DirList: func(ctx iris.Context, dirOptions iris.DirOptions, dirName string, dir http.File) error {
|
||||
// [...]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `DirListRich` is a function helper for a better look & feel. It's a `DirListFunc`.
|
||||
|
||||
```go
|
||||
func DirListRich(opts ...DirListRichOptions) DirListFunc
|
||||
```
|
||||
|
||||
The `DirListRich` function can optionally accept `DirListRichOptions`:
|
||||
|
||||
```go
|
||||
type DirListRichOptions struct {
|
||||
// If not nil then this template's "dirlist" is used to render the listing page.
|
||||
Tmpl *template.Template // * "html/template"
|
||||
// If not empty then this view file is used to render the listing page.
|
||||
// The view should be registered with `Application.RegisterView`.
|
||||
// E.g. "dirlist.html"
|
||||
TmplName string
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```go
|
||||
options := iris.DirOptions{
|
||||
// [...]
|
||||
ShowList: true,
|
||||
DirList: iris.DirListRich(),
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Here's a full example that users can upload files and server can list them. It contains a customized listing page and delete file feature with basic authentication.
|
||||
|
||||
```text
|
||||
│ main.go # Iris web server.
|
||||
└───views
|
||||
│ upload.html # used to render upload form.
|
||||
│ dirlist.html # used to render listing page.
|
||||
└───uploads
|
||||
│ # uploaded files will be stored here.
|
||||
```
|
||||
|
||||
Create a **main.go** file and copy-paste the following contents:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Mkdir("./uploads", 0700)
|
||||
}
|
||||
|
||||
const (
|
||||
maxSize = 1 * iris.GB
|
||||
uploadDir = "./uploads"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
view := iris.HTML("./views", ".html")
|
||||
view.AddFunc("formatBytes", func(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
})
|
||||
app.RegisterView(view)
|
||||
|
||||
// Serve assets (e.g. javascript, css).
|
||||
// app.HandleDir("/public", "./public")
|
||||
|
||||
app.Get("/", index)
|
||||
|
||||
app.Get("/upload", uploadView)
|
||||
app.Post("/upload", upload)
|
||||
|
||||
filesRouter := app.Party("/files")
|
||||
{
|
||||
filesRouter.HandleDir("/", iris.Dir(uploadDir), iris.DirOptions{
|
||||
Compress: true,
|
||||
ShowList: true,
|
||||
DirList: iris.DirListRich(iris.DirListRichOptions{
|
||||
// Optionally, use a custom template for listing:
|
||||
// Tmpl: dirListRichTemplate,
|
||||
TmplName: "dirlist.html",
|
||||
}),
|
||||
})
|
||||
|
||||
auth := basicauth.New(basicauth.Config{
|
||||
Users: map[string]string{
|
||||
"myusername": "mypassword",
|
||||
},
|
||||
})
|
||||
|
||||
filesRouter.Delete("/{file:path}", auth, deleteFile)
|
||||
}
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func index(ctx iris.Context) {
|
||||
ctx.Redirect("/upload")
|
||||
}
|
||||
|
||||
func uploadView(ctx iris.Context) {
|
||||
now := time.Now().Unix()
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(now, 10))
|
||||
token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
ctx.View("upload.html", token)
|
||||
}
|
||||
|
||||
func upload(ctx iris.Context) {
|
||||
ctx.SetMaxRequestBodySize(maxSize)
|
||||
|
||||
_, err := ctx.UploadFormFiles(uploadDir, beforeSave)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusPayloadTooRage, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect("/files")
|
||||
}
|
||||
|
||||
func beforeSave(ctx iris.Context, file *multipart.FileHeader) {
|
||||
ip := ctx.RemoteAddr()
|
||||
ip = strings.ReplaceAll(ip, ".", "_")
|
||||
ip = strings.ReplaceAll(ip, ":", "_")
|
||||
|
||||
file.Filename = ip + "-" + file.Filename
|
||||
}
|
||||
|
||||
func deleteFile(ctx iris.Context) {
|
||||
// It does not contain the system path,
|
||||
// as we are not exposing it to the user.
|
||||
fileName := ctx.Params().Get("file")
|
||||
|
||||
filePath := path.Join(uploadDir, fileName)
|
||||
|
||||
if err := os.RemoveAll(filePath); err != nil {
|
||||
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect("/files")
|
||||
}
|
||||
```
|
||||
|
||||
The **views/upload.html** should look like that:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Upload Files</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/upload" method="POST">
|
||||
<input type="file" id="upload_files" name="upload_files" multiple />
|
||||
<input type="hidden" name="token" value="{{.}}" />
|
||||
|
||||
<input type="button" value="Upload using JS" onclick="uploadFiles()" />
|
||||
<input type="submit" value="Upload by submiting the form" />
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
function uploadFiles() {
|
||||
let files = document.getElementById("upload_files").files;
|
||||
let formData = new FormData();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
formData.append("files[]", files[i]);
|
||||
}
|
||||
|
||||
fetch('/upload',
|
||||
{
|
||||
method: "POST",
|
||||
body: formData
|
||||
}).
|
||||
then(data => window.location = "/files").
|
||||
catch(e => window.alert("upload failed: file too large"));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
||||
|
||||
And finally the **customized listing page**. Copy-paste the following code to the **views/dirlist.html** file:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
a {
|
||||
padding: 8px 8px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #10a2ff;
|
||||
}
|
||||
|
||||
table {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
empty-cells: show;
|
||||
border: 1px solid #cbcbcb;
|
||||
}
|
||||
|
||||
table caption {
|
||||
color: #000;
|
||||
font: italic 85%/1 arial, sans-serif;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border-left: 1px solid #cbcbcb;
|
||||
border-width: 0 0 0 1px;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: #10a2ff;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
table td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.table-odd td {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.table-bordered td {
|
||||
border-bottom: 1px solid #cbcbcb;
|
||||
}
|
||||
|
||||
.table-bordered tbody>tr:last-child>td {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table class="table-bordered table-odd">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $idx, $file := .Files }}
|
||||
<tr>
|
||||
<td>{{ $idx }}</td>
|
||||
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
|
||||
{{ if $file.Info.IsDir }}
|
||||
<td>Dir</td>
|
||||
{{ else }}
|
||||
<td>{{ formatBytes $file.Info.Size }}</td>
|
||||
{{ end }}
|
||||
|
||||
<td><input type="button" style="background-color:transparent;border:0px;cursor:pointer;" value="❌"
|
||||
onclick="deleteFile({{ $file.RelPath }})" /></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
function deleteFile(filename) {
|
||||
if (!confirm("Are you sure you want to delete " + filename + " ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/files/' + filename,
|
||||
{
|
||||
method: "DELETE",
|
||||
// If you don't want server to prompt for username/password:
|
||||
// credentials:"include",
|
||||
headers: {
|
||||
// "Authorization": "Basic " + btoa("myusername:mypassword")
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
}).
|
||||
then(data => location.reload()).
|
||||
catch(e => alert(e));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
```
|
3
file-server/memory-cache.md
Normal file
3
file-server/memory-cache.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Memory Cache
|
||||
|
||||
TODO
|
138
mvc/mvc-grpc.md
Normal file
138
mvc/mvc-grpc.md
Normal file
|
@ -0,0 +1,138 @@
|
|||
# gRPC
|
||||
|
||||
gRPC[\*](https://grpc.io/) is a modern open source high performance Remote Procedure Call framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.
|
||||
|
||||
Iris and gRPC integration lives inside the [mvc](https://github.com/kataras/iris/tree/master/mvc) package.
|
||||
|
||||
Have you ever have difficulties converting your app or parts of it from HTTP to gGRPC or did you ever wish you had decent HTTP framework support as well for your gRPC services? Now, with Iris you have the best of two worlds. Without change a bit of your existing gRPC services code, you can register them as Iris HTTP routes through a Controller \(your service struct value\).
|
||||
|
||||
![\_assets/grpc-compatible-question.png](../.gitbook/assets/grpc-compatible-question.png)
|
||||
|
||||
> Learn more about our conversation at: [https://github.com/kataras/iris/issues/1449\#issuecomment-623260695](https://github.com/kataras/iris/issues/1449#issuecomment-623260695)
|
||||
|
||||
## Step by step
|
||||
|
||||
We will follow the [official helloworld gRPC example](https://github.com/grpc/grpc-go/tree/master/examples/helloworld). If you had already work with gRPC services you can skip 1-5.
|
||||
|
||||
**1.** Let's write our proto schema for request and response.
|
||||
|
||||
```text
|
||||
syntax = "proto3";
|
||||
|
||||
package helloworld;
|
||||
|
||||
// The greeting service definition.
|
||||
service Greeter {
|
||||
// Sends a greeting
|
||||
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||
}
|
||||
|
||||
// The request message containing the user's name.
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// The response message containing the greetings
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
```
|
||||
|
||||
**2.** Install the protoc Go plugin
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
```
|
||||
|
||||
**3.** Generate Go file from the helloworld.proto file above
|
||||
|
||||
```bash
|
||||
$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
|
||||
```
|
||||
|
||||
**4.** Implement a gRPC service as usually, with or without Iris
|
||||
|
||||
```go
|
||||
import (
|
||||
// [...]
|
||||
pb "myapp/helloworld"
|
||||
"context"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
type Greeter struct { }
|
||||
|
||||
// SayHello implements helloworld.GreeterServer.
|
||||
func (c *Greeter) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
|
||||
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
|
||||
}
|
||||
```
|
||||
|
||||
Iris automatically binds the standard "context" `context.Context` to `iris.Context.Request().Context()` and any other structure that is not mapping to a registered dependency as a payload \(depending on the request\), e.g XML, YAML, Query, Form, JSON, Protobuf.
|
||||
|
||||
**5.** Register your service to the gRPC server
|
||||
|
||||
```go
|
||||
import (
|
||||
// [...]
|
||||
pb "myapp/helloworld"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
grpcServer := grpc.NewServer()
|
||||
|
||||
myService := &Greeter{}
|
||||
pb.RegisterGreeterServer(grpcServer, myService)
|
||||
```
|
||||
|
||||
**6.** Register this `myService` to Iris
|
||||
|
||||
The `mvc.New(party).Handle(ctrl, mvc.GRPC{...})` option allows to register gRPC services per-party \(without the requirement of a full wrapper\) and optionally strict access to gRPC-only clients.
|
||||
|
||||
Register MVC application controller for gRPC services. You can bind as many mvc gRpc services in the same Party or app, as the `ServiceName` differs.
|
||||
|
||||
```go
|
||||
import (
|
||||
// [...]
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
app := iris.New()
|
||||
|
||||
rootApp := mvc.New(app)
|
||||
rootApp.Handle(myService, mvc.GRPC{
|
||||
Server: grpcServer, // Required.
|
||||
ServiceName: "helloworld.Greeter", // Required.
|
||||
Strict: false,
|
||||
})
|
||||
```
|
||||
|
||||
**7.** Generate TLS Keys
|
||||
|
||||
The Iris server **should ran under TLS** \(it's a gRPC requirement\).
|
||||
|
||||
```bash
|
||||
$ openssl genrsa -out server.key 2048
|
||||
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
```
|
||||
|
||||
**8.** Listen and Serve
|
||||
|
||||
```go
|
||||
app.Run(iris.TLS(":443", "server.crt", "server.key"))
|
||||
```
|
||||
|
||||
POST: `https://localhost:443/helloworld.Greeter/SayHello` with request data: `{"name": "John"}` xpected output: `{"message": "Hello John"}`.
|
||||
|
||||
Both HTTP Client and gRPC client will be able to communicate with our Iris+gRPC service.
|
||||
|
||||
### Exercise files
|
||||
|
||||
Full Server, Clients and Testing Code can be found at: [https://github.com/kataras/iris/tree/master/\_examples/mvc/grpc-compatible](https://github.com/kataras/iris/tree/master/_examples/mvc/grpc-compatible).
|
||||
|
379
mvc/mvc-quickstart.md
Normal file
379
mvc/mvc-quickstart.md
Normal file
|
@ -0,0 +1,379 @@
|
|||
# Quick start
|
||||
|
||||
The following guide is just a simple example of usage of some of the **Iris MVC** features. You are not limited to that data structure or code flow.
|
||||
|
||||
Create a folder, let's assume its path is `app`. The structure should look like that:
|
||||
|
||||
```text
|
||||
│ main.go
|
||||
│ go.mod
|
||||
│ go.sum
|
||||
└───environment
|
||||
│ environment.go
|
||||
└───model
|
||||
│ request.go
|
||||
│ response.go
|
||||
└───database
|
||||
│ database.go
|
||||
│ mysql.go
|
||||
│ sqlite.go
|
||||
└───service
|
||||
│ greet_service.go
|
||||
└───controller
|
||||
│ greet_controller.go
|
||||
```
|
||||
|
||||
Navigate to that `app` folder and execute the following command:
|
||||
|
||||
```bash
|
||||
$ go init app
|
||||
$ go get -u github.com/kataras/iris/v12@master
|
||||
# or @latest for the latest official release.
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
Let's start by defining the available environments that our web-application can behave on.
|
||||
|
||||
We'll just work on two available environments, the "development" and the "production", as they define the two most common scenarios. The `ReadEnv` will read from the `Env` type of a system's environment variable (see `main.go` in the end of the page).
|
||||
|
||||
Create a `environment/environment.go` file and put the following contents:
|
||||
|
||||
```go
|
||||
package environment
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PROD Env = "production"
|
||||
DEV Env = "development"
|
||||
)
|
||||
|
||||
type Env string
|
||||
|
||||
func (e Env) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func ReadEnv(key string, def Env) Env {
|
||||
v := Getenv(key, def.String())
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
env := Env(strings.ToLower(v))
|
||||
switch env {
|
||||
case PROD, DEV: // allowed.
|
||||
default:
|
||||
panic("unexpected environment " + v)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func Getenv(key string, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
We will use two database management systems, the `MySQL` and the `SQLite`. The first one for "production" use and the other for "development".
|
||||
|
||||
Create a `database/database.go` file and copy-paste the following:
|
||||
|
||||
```go
|
||||
package database
|
||||
|
||||
import "app/environment"
|
||||
|
||||
type DB interface {
|
||||
Exec(q string) error
|
||||
}
|
||||
|
||||
func NewDB(env environment.Env) DB {
|
||||
switch env {
|
||||
case environment.PROD:
|
||||
return &mysql{}
|
||||
case environment.DEV:
|
||||
return &sqlite{}
|
||||
default:
|
||||
panic("unknown environment")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's simulate our MySQL and SQLite `DB` instances. Create a `database/mysql.go` file which looks like the following one:
|
||||
|
||||
```go
|
||||
package database
|
||||
|
||||
import "fmt"
|
||||
|
||||
type mysql struct{}
|
||||
|
||||
func (db *mysql) Exec(q string) error {
|
||||
return fmt.Errorf("mysql: not implemented <%s>", q)
|
||||
}
|
||||
```
|
||||
|
||||
And a `database/sqlite.go` file.
|
||||
|
||||
```go
|
||||
package database
|
||||
|
||||
type sqlite struct{}
|
||||
|
||||
func (db *sqlite) Exec(q string) error { return nil }
|
||||
```
|
||||
|
||||
The `DB` depends on the \`Environment.
|
||||
|
||||
> A practical and operational database example, including Docker images, can be found at the following guide: [https://github.com/kataras/iris/tree/master/\_examples/database/mysql](https://github.com/kataras/iris/tree/master/_examples/database/mysql)
|
||||
|
||||
## Service
|
||||
|
||||
We'll need a service that will communicate with a database instance in behalf of our Controller\(s\).
|
||||
|
||||
In our case we will only need a single service, the Greet Service.
|
||||
|
||||
For the sake of the example, let's use two implementations of a greet service based on the `Environment`. The `GreetService` interface contains a single method of `Say(input string) (output string, err error)`. Create a `./service/greet_service.go` file and write the following code:
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"app/database"
|
||||
"app/environment"
|
||||
)
|
||||
|
||||
// GreetService example service.
|
||||
type GreetService interface {
|
||||
Say(input string) (string, error)
|
||||
}
|
||||
|
||||
// NewGreetService returns a service backed with a "db" based on "env".
|
||||
func NewGreetService(env environment.Env, db database.DB) GreetService {
|
||||
service := &greeter{db: db, prefix: "Hello"}
|
||||
|
||||
switch env {
|
||||
case environment.PROD:
|
||||
return service
|
||||
case environment.DEV:
|
||||
return &greeterWithLogging{service}
|
||||
default:
|
||||
panic("unknown environment")
|
||||
}
|
||||
}
|
||||
|
||||
type greeter struct {
|
||||
prefix string
|
||||
db database.DB
|
||||
}
|
||||
|
||||
func (s *greeter) Say(input string) (string, error) {
|
||||
if err := s.db.Exec("simulate a query..."); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := s.prefix + " " + input
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type greeterWithLogging struct {
|
||||
*greeter
|
||||
}
|
||||
|
||||
func (s *greeterWithLogging) Say(input string) (string, error) {
|
||||
result, err := s.greeter.Say(input)
|
||||
fmt.Printf("result: %s\nerror: %v\n", result, err)
|
||||
return result, err
|
||||
}
|
||||
```
|
||||
|
||||
The `greeter` will be used on "production" and the `greeterWithLogging` on "development". The `GreetService` depends on the `Environment` and the `DB`.
|
||||
|
||||
## Models
|
||||
|
||||
Continue by creating our HTTP request and response models.
|
||||
|
||||
Create a `model/request.go` file and copy-paste the following code:
|
||||
|
||||
```go
|
||||
package model
|
||||
|
||||
type Request struct {
|
||||
Name string `url:"name"`
|
||||
}
|
||||
```
|
||||
|
||||
Same for the `model/response.go` file.
|
||||
|
||||
```go
|
||||
package model
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"msg"`
|
||||
}
|
||||
```
|
||||
|
||||
The server will accept a URL Query Parameter of `name` \(e.g. `/greet?name=kataras`\) and will reply back with a JSON message.
|
||||
|
||||
## Controller
|
||||
|
||||
MVC Controllers are responsible for controlling the flow of the application execution. When you make a request \(means request a page\) to MVC Application, a controller is responsible for returning the response to that request.
|
||||
|
||||
We will only need the `GreetController` for our mini web-application. Create a file at `controller/greet_controller.go` which looks like that:
|
||||
|
||||
```go
|
||||
package controller
|
||||
|
||||
import (
|
||||
"app/model"
|
||||
"app/service"
|
||||
)
|
||||
|
||||
type GreetController struct {
|
||||
Service service.GreetService
|
||||
// Ctx iris.Context
|
||||
}
|
||||
|
||||
func (c *GreetController) Get(req model.Request) (model.Response, error) {
|
||||
message, err := c.Service.Say(req.Name)
|
||||
if err != nil {
|
||||
return model.Response{}, err
|
||||
}
|
||||
|
||||
return model.Response{Message: message}, nil
|
||||
}
|
||||
```
|
||||
|
||||
The `GreetController` depends on the `GreetService`. It serves the `GET: /greet` index path through its `Get` method. The `Get` method accepts a `model.Request` which contains a single field name of `Name` which will be extracted from the `URL Query Parameter 'name'` \(because it's a `GET` requst and its `url:"name"` struct field\). And it will respond with a `model.Response` (JSON) or an error.
|
||||
|
||||
{% page-ref page="../dependency-injection/inputs.md" %}
|
||||
{% page-ref page="../dependency-injection/outputs.md" %}
|
||||
|
||||
## Wrap up
|
||||
|
||||
```sh
|
||||
+-------------------+
|
||||
| Env (DEV, PROD) |
|
||||
+---------+---------+
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
DEV | | | PROD
|
||||
-------------------+---------------------+ | +----------------------+-------------------
|
||||
| | |
|
||||
| | |
|
||||
+---+-----+ +----------------v------------------+ +----+----+
|
||||
| sqlite | | NewDB(Env) DB | | mysql |
|
||||
+---+-----+ +----------------+---+--------------+ +----+----+
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
+--------------+-----+ +-------------------v---v-----------------+ +----+------+
|
||||
| greeterWithLogging | | NewGreetService(Env, DB) GreetService | | greeter |
|
||||
+--------------+-----+ +---------------------------+-------------+ +----+------+
|
||||
| | |
|
||||
| | |
|
||||
| +-----------------------------------------+ |
|
||||
| | GreetController | | |
|
||||
| | | | |
|
||||
| | - Service GreetService <-- | |
|
||||
| | | |
|
||||
| +-------------------+---------------------+ |
|
||||
| | |
|
||||
| | |
|
||||
| | |
|
||||
| +-----------+-----------+ |
|
||||
| | HTTP Request | |
|
||||
| +-----------------------+ |
|
||||
| | /greet?name=kataras | |
|
||||
| +-----------+-----------+ |
|
||||
| | |
|
||||
+------------------+--------+ +------------+------------+ +-------+------------------+
|
||||
| model.Response (JSON) | | Response (JSON, error) | | Bad Request |
|
||||
+---------------------------+ +-------------------------+ +--------------------------+
|
||||
| { | | mysql: not implemented |
|
||||
| "msg": "Hello kataras" | +--------------------------+
|
||||
| } |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
Now it's the time to wrap all the above into our `main.go` file. Copy-paste the following code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"app/controller"
|
||||
"app/database"
|
||||
"app/environment"
|
||||
"app/service"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.Get("/ping", pong).Describe("healthcheck")
|
||||
|
||||
mvc.Configure(app.Party("/greet"), setup)
|
||||
|
||||
// http://localhost:8080/greet?name=kataras
|
||||
app.Listen(":8080", iris.WithLogLevel("debug"))
|
||||
}
|
||||
|
||||
func pong(ctx iris.Context) {
|
||||
ctx.WriteString("pong")
|
||||
}
|
||||
|
||||
func setup(app *mvc.Application) {
|
||||
// Register Dependencies.
|
||||
app.Register(
|
||||
environment.DEV, // DEV, PROD
|
||||
database.NewDB, // sqlite, mysql
|
||||
service.NewGreetService, // greeterWithLogging, greeter
|
||||
)
|
||||
|
||||
// Register Controllers.
|
||||
app.Handle(new(controller.GreetController))
|
||||
}
|
||||
```
|
||||
|
||||
The `mvc.Application.Register` method registers one more dependencies, dependencies can depend on previously registered dependencies too. Thats the reason we pass, first, the `Environment(DEV)`, then the `NewDB` which depends on that `Environment`, following by the `NewGreetService` function which depends on both the `Environment(DEV)` and the `DB`.
|
||||
|
||||
The `mvc.Application.Handle` registers a new controller, which depends on the `GreetService`, for the targeted sub-router of `Party("/greet")`.
|
||||
|
||||
## Run
|
||||
|
||||
Install [Go](https://golang.org/dl) and run the application with:
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Or Docker
|
||||
|
||||
Download the [Dockerfile](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/Dockerfile) and [docker-compose.yml](https://raw.githubusercontent.com/kataras/iris/9b93c0dbb491dcedf49c91e89ca13bab884d116f/_examples/mvc/overview/docker-compose.yml) files to the `app` folder.
|
||||
|
||||
Install [Docker](https://www.docker.com/) and execute the following command:
|
||||
|
||||
```bash
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
Visit [http://localhost:8080?name=kataras](http://localhost:8080?name=kataras).
|
||||
|
||||
Optionally, replace the `main.go`'s `app.Register(environment.DEV` with `environment.PROD`, restart the application and refresh. You will see that a new database \(`sqlite`\) and another service of \(`greeterWithLogging`\) will be binded to the `GreetController`. With **a single change** you achieve to completety change the result.
|
68
mvc/mvc-sessions.md
Normal file
68
mvc/mvc-sessions.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Session Controller
|
||||
|
||||
Create a `main.go` file and copy-paste the following code snippets:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
"github.com/kataras/iris/v12/sessions"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
// Configure sessions manager as we used to.
|
||||
sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"})
|
||||
app.Use(sess.Handler())
|
||||
|
||||
visitApp := mvc.New(app)
|
||||
visitApp.Register(time.Now())
|
||||
visitApp.Handle(new(VisitController))
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## Controller
|
||||
|
||||
- The `VisitController.Session` is automatically binded to the current `sessions.Session`.
|
||||
- The `VisitController.StartTime` is statically set to the server's start time with `.Register(time.Now())` above.
|
||||
|
||||
```go
|
||||
type VisitController struct {
|
||||
Session *sessions.Session
|
||||
|
||||
StartTime time.Time
|
||||
}
|
||||
|
||||
func (c *VisitController) Get() string {
|
||||
// it increments a "visits" value of integer by one,
|
||||
// if the entry with key 'visits' doesn't exist
|
||||
// it will create it for you.
|
||||
visits := c.Session.Increment("visits", 1)
|
||||
// write the current, updated visits.
|
||||
since := time.Now().Sub(c.StartTime).Seconds()
|
||||
return fmt.Sprintf("%d visit(s) from my current session in %0.1f seconds of server's up-time",
|
||||
visits, since)
|
||||
}
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Open a terminal session and execute:
|
||||
|
||||
```sh
|
||||
$ go run main.go
|
||||
```
|
||||
|
||||
1. Prepare a client, e.g. your browser
|
||||
2. navigate to http://localhost:8080
|
||||
3. refresh the page some times
|
||||
4. close the browser
|
||||
5. re-open the browser (if it wasn't in private mode) and re-play 2.
|
83
mvc/mvc-websockets.md
Normal file
83
mvc/mvc-websockets.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Websocket Controller
|
||||
|
||||
Iris has a trivial way of registering websocket events via a Go structure. The websocket controller is part of the [MVC](mvc.md) features.
|
||||
|
||||
Iris has its own `iris/mvc/Application.HandleWebsocket(v interface{}) *neffos.Struct` to register controllers in existing Iris MVC applications\(offering a fully featured dependency injection container for request values and static services\).
|
||||
|
||||
```go
|
||||
// HandleWebsocket handles a websocket specific controller.
|
||||
// Its exported methods are the events.
|
||||
// If a "Namespace" field or method exists then namespace is set,
|
||||
// otherwise empty namespace will be used for this controller.
|
||||
//
|
||||
// Note that a websocket controller is registered and ran under
|
||||
// a connection connected to a namespace
|
||||
// and it cannot send HTTP responses on that state.
|
||||
// However all static and dynamic dependencies behave as expected.
|
||||
func (*mvc.Application) HandleWebsocket(controller interface{}) *neffos.Struct
|
||||
```
|
||||
|
||||
Let's see a usage example, we want to bind the `OnNamespaceConnected`, `OnNamespaceDisconnect` built-in events and a custom `"OnChat"` event with our controller's methods.
|
||||
|
||||
**1.** We create the controller by declaring a NSConn type field as `stateless` and write the methods we need.
|
||||
|
||||
```go
|
||||
type websocketController struct {
|
||||
*neffos.NSConn `stateless:"true"`
|
||||
Namespace string
|
||||
|
||||
Logger MyLoggerInterface
|
||||
}
|
||||
|
||||
func (c *websocketController) OnNamespaceConnected(msg neffos.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketController) OnNamespaceDisconnect(msg neffos.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketController) OnChat(msg neffos.Message) error {
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Iris is smart enough to catch the `Namespace string` struct field to use it to register the controller's methods as events for that namespace, alternatively you can create a controller method of `Namespace() string { return "default" }` or use the `HandleWebsocket`'s return value to `.SetNamespace("default")`, it's up to you.
|
||||
|
||||
**2.** We inititalize our MVC application targets to a websocket endpoint, as we used to do with regular HTTP Controllers for HTTP routes.
|
||||
|
||||
```go
|
||||
import (
|
||||
// [...]
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
)
|
||||
// [app := iris.New...]
|
||||
|
||||
mvcApp := mvc.New(app.Party("/websocket_endpoint"))
|
||||
```
|
||||
|
||||
**3.** We register our dependencies, if any.
|
||||
|
||||
```go
|
||||
mvcApp.Register(
|
||||
&prefixedLogger{prefix: "DEV"},
|
||||
)
|
||||
```
|
||||
|
||||
**4.** We register one or more websocket controllers, each websocket controller maps to one namespace \(just one is enough, as in most of the cases you don't need more, but that depends on your app's needs and requirements\).
|
||||
|
||||
```go
|
||||
mvcApp.HandleWebsocket(&websocketController{Namespace: "default"})
|
||||
```
|
||||
|
||||
**5.** Next, we continue by mapping the mvc application as a connection handler to a websocket server \(you may use more than one mvc applications per websocket server via `neffos.JoinConnHandlers(mvcApp1, mvcApp2)`\).
|
||||
|
||||
```go
|
||||
websocketServer := neffos.New(websocket.DefaultGorillaUpgrader, mvcApp)
|
||||
```
|
||||
|
||||
**6.** And the last step is to register that server to our endpoint through a normal `.Get` method.
|
||||
|
||||
```go
|
||||
mvcApp.Router.Get("/", websocket.Handler(websocketServer))
|
||||
```
|
293
mvc/mvc.md
Normal file
293
mvc/mvc.md
Normal file
|
@ -0,0 +1,293 @@
|
|||
# MVC
|
||||
|
||||
![\_assets/web\_mvc\_diagram.png](../.gitbook/assets/web_mvc_diagram.png)
|
||||
|
||||
Using Iris MVC for code reuse.
|
||||
|
||||
By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same \(or similar\) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user.
|
||||
|
||||
Iris has **first-class support for the MVC \(Model View Controller\) architectural pattern**, you'll not find these stuff anywhere else in the Go world. You will have to import the [iris/mvc](https://github.com/kataras/iris/tree/master/mvc) subpackage.
|
||||
|
||||
```go
|
||||
import "github.com/kataras/iris/v12/mvc"
|
||||
```
|
||||
|
||||
Iris web framework supports Request data, Models, Persistence Data and Binding with the fastest possible execution.
|
||||
|
||||
If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller).
|
||||
|
||||
**Note:** Read the [Dependency Injection](../dependency-injection/dependency-injection.md) section before continue.
|
||||
|
||||
**Characteristics**
|
||||
|
||||
All HTTP Methods are supported, for example if want to serve `GET` then the controller should have a function named `Get()`, for `POST` verb use the `Post()`, for a parameter use the `By` keyword, e.g. `PostUserBy(id uint64)` which translates to POST: [...]/User/{id}.
|
||||
|
||||
Serve custom controller's struct's methods as handlers with custom paths\(even with regex parametermized path\) via the `BeforeActivation` custom event callback, per-controller. Example:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
mvc.Configure(app.Party("/root"), myMVC)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func myMVC(app *mvc.Application) {
|
||||
// app.Register(...)
|
||||
// app.Router.Use/UseGlobal/Done(...)
|
||||
app.Handle(new(MyController))
|
||||
}
|
||||
|
||||
type MyController struct {}
|
||||
|
||||
func (m *MyController) BeforeActivation(b mvc.BeforeActivation) {
|
||||
// b.Dependencies().Add/Remove
|
||||
// b.Router().Use/UseGlobal/Done
|
||||
// and any standard Router API call you already know
|
||||
|
||||
// 1-> Method
|
||||
// 2-> Path
|
||||
// 3-> The controller's function name to be parsed as handler
|
||||
// 4-> Any handlers that should run before the MyCustomHandler
|
||||
b.Handle("GET", "/something/{id:int64}", "MyCustomHandler", anyMiddleware...)
|
||||
}
|
||||
|
||||
// GET: http://localhost:8080/root
|
||||
func (m *MyController) Get() string {
|
||||
return "Hey"
|
||||
}
|
||||
|
||||
// GET: http://localhost:8080/root/something/{id:int64}
|
||||
func (m *MyController) MyCustomHandler(id int64) string {
|
||||
return "MyCustomHandler says Hey"
|
||||
}
|
||||
```
|
||||
|
||||
Persistence data inside your Controller struct \(share data between requests\) by defining services to the Dependencies or have a `Singleton` controller scope.
|
||||
|
||||
Share the dependencies between controllers or register them on a parent MVC Application, and ability to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, i.e `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`.
|
||||
|
||||
Access to the `Context` as a controller's field\(no manual binding is neede\) i.e `Ctx iris.Context` or via a method's input argument, i.e `func(ctx iris.Context, otherArguments...)`.
|
||||
|
||||
Models inside your Controller struct \(set-ed at the Method function and rendered by the View\). You can return models from a controller's method or set a field in the request lifecycle and return that field to another method, in the same request lifecycle.
|
||||
|
||||
Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. `Controllers` can be registered to any `Party`, including Subdomains, the Party's begin and done handlers work as expected.
|
||||
|
||||
Optional `BeginRequest(ctx)` function to perform any initialization before the method execution, useful to call middlewares or when many methods use the same collection of data.
|
||||
|
||||
Optional `EndRequest(ctx)` function to perform any finalization after any method executed.
|
||||
|
||||
Inheritance, recursively, e.g. our [mvc session-controller example](https://github.com/kataras/iris/tree/master/_examples/mvc/session-controller), has the `Session *sessions.Session` as struct field which is filled by the sessions manager.
|
||||
|
||||
Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods with the `By` word, uppercase is a new sub path. Example:
|
||||
|
||||
If `mvc.New(app.Party("/user")).Handle(new(user.Controller))`
|
||||
|
||||
* `func(*Controller) Get()` - `GET:/user`.
|
||||
* `func(*Controller) Post()` - `POST:/user`.
|
||||
* `func(*Controller) GetLogin()` - `GET:/user/login`
|
||||
* `func(*Controller) PostLogin()` - `POST:/user/login`
|
||||
* `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers`
|
||||
* `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers`
|
||||
* `func(*Controller) GetBy(id int64)` - `GET:/user/{param:int64}`
|
||||
* `func(*Controller) PostBy(id int64)` - `POST:/user/{param:int64}`
|
||||
|
||||
If `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))`
|
||||
|
||||
* `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}`
|
||||
|
||||
If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))`
|
||||
|
||||
* `func(*Controller) GetByWildcard(path string)` - `GET:/assets/{param:path}`
|
||||
|
||||
Supported types for method functions receivers: int, int64, bool and string.
|
||||
|
||||
Optionally, response via output arguments, like we've shown at the [Dependency Injection](../dependency-injection/dependency-injection.md) chapter. E.g.
|
||||
|
||||
```go
|
||||
func(c *ExampleController) Get() string |
|
||||
(string, string) |
|
||||
(string, int) |
|
||||
int |
|
||||
(int, string) |
|
||||
(string, error) |
|
||||
error |
|
||||
(int, error) |
|
||||
(any, bool) |
|
||||
(customStruct, error) |
|
||||
customStruct |
|
||||
(customStruct, int) |
|
||||
(customStruct, string) |
|
||||
mvc.Result or (mvc.Result, error)
|
||||
```
|
||||
|
||||
Where `mvc.Result` is just a type alias of `hero.Result`:
|
||||
|
||||
```go
|
||||
type Result interface {
|
||||
// Dispatch should sends the response to the context's response writer.
|
||||
Dispatch(ctx iris.Context)
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
This example is equivalent to a simple hello world application.
|
||||
|
||||
It seems that additional code you have to write doesn't worth it but remember that, this example does not make use of iris mvc features like the Model, Persistence or the View engine neither the Session, it's very simple for learning purposes, probably you'll never use such as simple controller anywhere in your app.
|
||||
|
||||
The cost we have on this example for using MVC on the "/hello" path which serves JSON is ~2MB per 20MB throughput on my personal laptop, it's tolerated for the majority of the applications but you can choose what suits you best with Iris, low-level handlers: performance or high-level controllers: easier to maintain and smaller codebase on large applications.
|
||||
|
||||
**Read the comments carefully**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/mvc"
|
||||
|
||||
"github.com/kataras/iris/v12/middleware/logger"
|
||||
"github.com/kataras/iris/v12/middleware/recover"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
// Optionally, add two built'n handlers
|
||||
// that can recover from any http-relative panics
|
||||
// and log the requests to the terminal.
|
||||
app.Use(recover.New())
|
||||
app.Use(logger.New())
|
||||
|
||||
// Serve a controller based on the root Router, "/".
|
||||
mvc.New(app).Handle(new(ExampleController))
|
||||
|
||||
// http://localhost:8080
|
||||
// http://localhost:8080/ping
|
||||
// http://localhost:8080/hello
|
||||
// http://localhost:8080/custom_path
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
// ExampleController serves the "/", "/ping" and "/hello".
|
||||
type ExampleController struct{}
|
||||
|
||||
// Get serves
|
||||
// Method: GET
|
||||
// Resource: http://localhost:8080
|
||||
func (c *ExampleController) Get() mvc.Result {
|
||||
return mvc.Response{
|
||||
ContentType: "text/html",
|
||||
Text: "<h1>Welcome</h1>",
|
||||
}
|
||||
}
|
||||
|
||||
// GetPing serves
|
||||
// Method: GET
|
||||
// Resource: http://localhost:8080/ping
|
||||
func (c *ExampleController) GetPing() string {
|
||||
return "pong"
|
||||
}
|
||||
|
||||
// GetHello serves
|
||||
// Method: GET
|
||||
// Resource: http://localhost:8080/hello
|
||||
func (c *ExampleController) GetHello() interface{} {
|
||||
return map[string]string{"message": "Hello Iris!"}
|
||||
}
|
||||
|
||||
// BeforeActivation called once, before the controller adapted to the main application
|
||||
// and of course before the server ran.
|
||||
// After version 9 you can also add custom routes for a specific controller's methods.
|
||||
// Here you can register custom method's handlers
|
||||
// use the standard router with `ca.Router` to
|
||||
// do something that you can do without mvc as well,
|
||||
// and add dependencies that will be binded to
|
||||
// a controller's fields or method function's input arguments.
|
||||
func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) {
|
||||
anyMiddlewareHere := func(ctx iris.Context) {
|
||||
ctx.Application().Logger().Warnf("Inside /custom_path")
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
b.Handle(
|
||||
"GET",
|
||||
"/custom_path",
|
||||
"CustomHandlerWithoutFollowingTheNamingGuide",
|
||||
anyMiddlewareHere,
|
||||
)
|
||||
|
||||
// or even add a global middleware based on this controller's router,
|
||||
// which in this example is the root "/":
|
||||
// b.Router().Use(myMiddleware)
|
||||
}
|
||||
|
||||
// CustomHandlerWithoutFollowingTheNamingGuide serves
|
||||
// Method: GET
|
||||
// Resource: http://localhost:8080/custom_path
|
||||
func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string {
|
||||
return "hello from the custom handler without following the naming guide"
|
||||
}
|
||||
|
||||
// GetUserBy serves
|
||||
// Method: GET
|
||||
// Resource: http://localhost:8080/user/{username:string}
|
||||
// By is a reserved "keyword" to tell the framework that you're going to
|
||||
// bind path parameters in the function's input arguments, and it also
|
||||
// helps to have "Get" and "GetBy" in the same controller.
|
||||
//
|
||||
// func (c *ExampleController) GetUserBy(username string) mvc.Result {
|
||||
// return mvc.View{
|
||||
// Name: "user/username.html",
|
||||
// Data: username,
|
||||
// }
|
||||
// }
|
||||
|
||||
/* Can use more than one, the factory will make sure
|
||||
that the correct http methods are being registered for each route
|
||||
for this controller, uncomment these if you want:
|
||||
|
||||
func (c *ExampleController) Post() {}
|
||||
func (c *ExampleController) Put() {}
|
||||
func (c *ExampleController) Delete() {}
|
||||
func (c *ExampleController) Connect() {}
|
||||
func (c *ExampleController) Head() {}
|
||||
func (c *ExampleController) Patch() {}
|
||||
func (c *ExampleController) Options() {}
|
||||
func (c *ExampleController) Trace() {}
|
||||
*/
|
||||
|
||||
/*
|
||||
func (c *ExampleController) All() {}
|
||||
// OR
|
||||
func (c *ExampleController) Any() {}
|
||||
|
||||
|
||||
|
||||
func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) {
|
||||
// 1 -> the HTTP Method
|
||||
// 2 -> the route's path
|
||||
// 3 -> this controller's method name that should be handler for that route.
|
||||
b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...)
|
||||
}
|
||||
|
||||
// After activation, all dependencies are set-ed - so read only access on them
|
||||
// but still possible to add custom controller or simple standard handlers.
|
||||
func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {}
|
||||
|
||||
*/
|
||||
```
|
||||
|
||||
Every `exported` func prefixed with an HTTP Method\(`Get`, `Post`, `Put`, `Delete`...\) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method.
|
||||
|
||||
An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server \(including the TCP port\): `localhost:8080` and the target URI `/helloworld`.
|
||||
|
||||
The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL.
|
||||
|
||||
Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "[http://localhost:8080/helloworld/welcome/golang/32719](http://localhost:8080/helloworld/welcome/golang/32719)", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead.
|
||||
|
||||
> The [\_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller\_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advandage of the Iris MVC Binder, Iris MVC Models and more...
|
15
responses/binary.md
Normal file
15
responses/binary.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
**Content-Type: "application/octet-stream"**
|
||||
|
||||
A MIME attachment with the content type `"application/octet-stream"` is a **binary** file. Typically, it will be an application or a document that must be opened in an application, such as a spreadsheet or word processor. If the attachment has a filename extension associated with it, you may be able to tell what kind of file it is. A .exe extension, for example, indicates it is a Windows or DOS program (executable), while a file ending in .doc is probably meant to be opened in Microsoft Word.
|
||||
|
||||
No matter what kind of file it is, an application/octet-stream attachment **is rarely viewable** in web client. After downloading an attachment users must then open the attachment in the appropriate application to view its contents.
|
||||
|
||||
The `Context.Binary(data)` is the method which sends binary responses to the client. It accepts `[]byte` value.
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
ctx.Binary([]byte("Some binary data here."))
|
||||
}
|
||||
```
|
||||
|
||||
You may also want to see the following methods for [[file serving |file-server/context-file-server]].
|
230
responses/content-negotiation.md
Normal file
230
responses/content-negotiation.md
Normal file
|
@ -0,0 +1,230 @@
|
|||
**Content-Type: _*_**
|
||||
|
||||
## What is it
|
||||
|
||||
**Sometimes a server application needs to serve different representations of a resource at the same URI**. Of course this can be done by hand, manually checking the `Accept` request header and push the requested form of the content. However, as your app manages more resources and different kind of representations this can be very painful, as you may need to check for `Accept-Charset`, `Accept-Encoding`, put some server-side priorities , handle the errors correctly and e.t.c.
|
||||
|
||||
There are some web frameworks in Go already struggle to implement a feature like this but they don't do it correctly:
|
||||
|
||||
* they don't handle accept-charset at all
|
||||
* they don't handle accept-encoding at all
|
||||
* they don't send error status code (406 not acceptable) as RFC proposes and more...
|
||||
|
||||
But, fortunately for us, **Iris always follows the best practises and the Web standards**.
|
||||
|
||||
Based on:
|
||||
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation)
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept)
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset)
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding)
|
||||
|
||||
Implemented on:
|
||||
|
||||
* [https://github.com/kataras/iris/pull/1316/commits/8ee0de51c593fe0483fbea38117c3c88e065f2ef\#diff-15cce7299aae8810bcab9b0bf9a2fdb1](https://github.com/kataras/iris/pull/1316/commits/8ee0de51c593fe0483fbea38117c3c88e065f2ef#diff-15cce7299aae8810bcab9b0bf9a2fdb1)
|
||||
|
||||
## Introduction
|
||||
|
||||
The Context method which is responsible to render specific mime types based on the client's `Accept` header is the `Negotiate` one.
|
||||
|
||||
Before `Negotiate` method fired, the handler MUST declare what mime types the server supports to render with safety. We can do that using the `Negotiation` priorities builder.
|
||||
|
||||
## The `Negotitation` priorities builder
|
||||
|
||||
The Context method which returns the builder is the `Negotiation` one.
|
||||
|
||||
```go
|
||||
Negotiation() *NegotiationBuilder
|
||||
```
|
||||
|
||||
It returns:
|
||||
|
||||
```go
|
||||
type NegotiationBuilder struct {
|
||||
Accept NegotiationAcceptBuilder
|
||||
// [other unexproted fields]
|
||||
}
|
||||
```
|
||||
|
||||
The `Accept` struct **field** can be used to customize the **client's** Accept header manually, e.g. when the client does not contain an "application/json" mime type on its `Accept` header value.
|
||||
|
||||
Read more about [NegotiationAcceptBuilder](https://godoc.org/github.com/kataras/iris/context#NegotiationAcceptBuilder).
|
||||
|
||||
The `NegotitationBuilder` has the necessary **methods** to help you prioritize mime types, charsets and encoding. [Read the documentation](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder).
|
||||
|
||||
In short, it contains the following methods:
|
||||
|
||||
```go
|
||||
// Declare custom mime and optionally content to render.
|
||||
MIME(mime string, content interface{}) *NegotiationBuilder
|
||||
// MIME method helpers:
|
||||
Text(v ...string) *NegotiationBuilder
|
||||
Markdown(v ...[]byte) *NegotiationBuilder
|
||||
Binary(v ...[]byte) *NegotiationBuilder
|
||||
JSON(v ...interface{}) *NegotiationBuilder
|
||||
Problem(v ...interface{}) *NegotiationBuilder
|
||||
JSONP(v ...interface{}) *NegotiationBuilder
|
||||
XML(v ...interface{}) *NegotiationBuilder
|
||||
YAML(v ...interface{}) *NegotiationBuilder
|
||||
Protobuf(v ...interface{}) *NegotiationBuilder
|
||||
MsgPack(v ...interface{}) *NegotiationBuilder
|
||||
Any(v ...interface{}) *NegotiationBuilder
|
||||
```
|
||||
|
||||
```go
|
||||
// Charset
|
||||
Charset(charset ...string) *NegotiationBuilder
|
||||
```
|
||||
|
||||
```go
|
||||
// Encoding (compression)
|
||||
Encoding(encoding ...string) *NegotiationBuilder
|
||||
EncodingGzip() *NegotiationBuilder
|
||||
```
|
||||
|
||||
```go
|
||||
// Clears all the above, resets the builder
|
||||
// if necessary on another handler.
|
||||
Clear() *NegotiationBuilder
|
||||
```
|
||||
|
||||
The `Build` is called automatically on `Negotiate` method but
|
||||
it it's exported for a custom implementation of negotiation by the end-developer if ever required.
|
||||
|
||||
```go
|
||||
// Build calculates the client's and server's
|
||||
// mime type(s), charset(s) and encoding
|
||||
// and returns the final content type, charset and
|
||||
// encoding that server should render to the client.
|
||||
Build() (contentType, charset, encoding string, content interface{})
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The Negotiation is a context method which sets and returns the negotiation builder, so it can be used inside a middleware too.
|
||||
|
||||
To declare what mime types server can render and match versus the client's `Accept` header you can do that:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
users := app.Party("/users")
|
||||
users.Use(setAcceptTypes)
|
||||
// [...]
|
||||
}
|
||||
|
||||
func setAcceptTypes(ctx iris.Context) {
|
||||
ctx.Negotiation().JSON().XML().HTML().EncodingGzip()
|
||||
ctx.Next()
|
||||
}
|
||||
```
|
||||
|
||||
The above will tell the server that we can accept JSON, XML and HTML mime types, optionally encoding by Gzip if the client supports. So far nothing is rendered to the client, learn about the `Negotiate` method below.
|
||||
|
||||
## The `Negotiate` method
|
||||
|
||||
The `Context.Negotiate` method used for serving different representations of a resource at the same URI. It returns `context.ErrContentNotSupported` when not matched mime types.
|
||||
|
||||
```go
|
||||
Negotiate(v interface{}) (int, error)
|
||||
```
|
||||
|
||||
The `Context.Negotiate` method accepts an `interface{}` which can be any Go value or a [ContentNegotiator](https://godoc.org/github.com/kataras/iris/context#ContentNegotiator) or a [ContentSelector](https://godoc.org/github.com/kataras/iris/context#ContentSelector) such as the [iris.N](https://godoc.org/github.com/kataras/iris/context#N) structure (see below).
|
||||
|
||||
* The "v" can be a single [iris.N](https://godoc.org/github.com/kataras/iris/context#N) struct value.
|
||||
* The "v" can be any value completes the [context.ContentSelector](https://godoc.org/github.com/kataras/iris/context#ContentSelector) interface.
|
||||
* The "v" can be any value completes the [context.ContentNegotiator](https://godoc.org/github.com/kataras/iris/context#ContentNegotiator) interface.
|
||||
* The "v" can be any value of struct (JSON, JSONP, XML, YAML, MsgPack, Protobuf) or
|
||||
|
||||
`string(TEXT, HTML)` or `[]byte(Markdown, Binary)` or `[]byte` with any matched mime type.
|
||||
|
||||
* If the "v" is nil, the `Context.Negotitation()` builder's content will be used instead, otherwise "v" overrides builder's content (server mime types are still retrieved by its registered, supported, mime list)
|
||||
* Set mime type priorities by [Negotiation().MIME.Text.JSON.XML.HTML...]https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.JSON).
|
||||
* Set charset priorities by [Negotiation().Charset(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.Charset).
|
||||
* Set encoding algorithm priorities by [Negotiation().Encoding(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationBuilder.Encoding).
|
||||
* Modify the accepted by [Negotiation().Accept.Override().XML().JSON().Charset(...).Encoding(...)](https://godoc.org/github.com/kataras/iris/context#NegotiationAcceptBuilder.Override).
|
||||
|
||||
The `iris.N` is a struct which can be passed on the `Context.Negotiate` method.
|
||||
It contains fields which should be filled based on the `Context.Negotiation()`
|
||||
server side values. If no matched mime then its "Other" field will be sent,
|
||||
which should be a string or []byte.
|
||||
It completes the `ContentSelector` interface.
|
||||
|
||||
```go
|
||||
type N struct {
|
||||
Text, HTML string
|
||||
Markdown []byte
|
||||
Binary []byte
|
||||
|
||||
JSON interface{}
|
||||
Problem Problem
|
||||
JSONP interface{}
|
||||
XML interface{}
|
||||
YAML interface{}
|
||||
Protobuf interface{}
|
||||
MsgPack interface{}
|
||||
|
||||
Other []byte // custom content types.
|
||||
}
|
||||
```
|
||||
|
||||
If the given `interface{}` value is not a type which implements one the above then the `Negotiate` method will render that based on the request's `Accept` header value matching the declared priorities.
|
||||
|
||||
Note that if the given `v interface{}` is nil then it will uses the contents declared by the `Negotiation` builder itself.
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
// data := [...]
|
||||
ctx.Negotiation().
|
||||
JSON(data).
|
||||
XML(data).
|
||||
HTML("<h1>Test Name</h1><h2>Age 26</h2>").
|
||||
EncodingGzip().
|
||||
Charset("utf-8")
|
||||
|
||||
err := ctx.Negotiate(nil)
|
||||
// [handle err]
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
When the client accepts **JSON** and **XML** and **HTML** responses from a specific server's endpoint and the server can render all of them:
|
||||
|
||||
```go
|
||||
type testdata struct {
|
||||
ID uint64 `json:"id" xml:"ID"`
|
||||
Name string `json:"name" xml:"Name"`
|
||||
Age int `json:"age" xml:"Age"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
users := app.Party("/users")
|
||||
users.Use(setAcceptTypes)
|
||||
|
||||
users.Post("/{id:uint64}", handler)
|
||||
// [...]
|
||||
}
|
||||
|
||||
func setAcceptTypes(ctx iris.Context) {
|
||||
ctx.Negotiation().JSON().XML().HTML().EncodingGzip()
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
data := testdata{
|
||||
ID: ctx.Params().GetUint64Default("id", 0),
|
||||
Name: "Test Name",
|
||||
Age: 26,
|
||||
}
|
||||
|
||||
ctx.Negotiate(iris.N{
|
||||
JSON: data,
|
||||
XML: data,
|
||||
HTML: "<h1>Test Name</h1><h2>Age 26</h2>",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
That's all, read the examples for a comprehensive understanding in practise.
|
43
responses/gzip.md
Normal file
43
responses/gzip.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
**Content-Type: _*_**
|
||||
|
||||
**Gzip** is a method of compressing files (making them smaller) for faster network transfers. It is also a file format. Compression allows your web server to provide smaller file sizes which load faster for your website users. Enabling **gzip compression** is a standard practice. https://en.wikipedia.org/wiki/Gzip
|
||||
|
||||
To enable **compression** on **all** server's **responses** and **requests** use the `iris.Compression` middleware:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.Use(iris.Compression)
|
||||
|
||||
app.Post("/data", handler)
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := iris.Map{"name", "kataras"}
|
||||
ctx.JSON(response)
|
||||
}
|
||||
```
|
||||
|
||||
> If the client does not accept an encoding then it will write the contents as they are, without compression.
|
||||
|
||||
When you want control over it you can use the `Context.CompressWriter(enable bool)` method before sent the response:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
ctx.CompressWriter(true)
|
||||
|
||||
// [...]
|
||||
}
|
||||
```
|
||||
|
||||
There is also the `Context.ClientSupportsEncoding(s ...string) bool` method which reports if the client does accept and support gzip encoding:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
if ctx.ClientSupportsEncoding("gzip") {
|
||||
// client expects and supports gzip replies.
|
||||
} else {
|
||||
// client does not support gzip.
|
||||
}
|
||||
}
|
||||
```
|
18
responses/html.md
Normal file
18
responses/html.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
**Content-Type: "text/html"**
|
||||
|
||||
The `Context.HTML(format, ...args)` is the method which sends HTML responses to the client. It accepts the value (like fmt package works).
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
response := "Hello, <strong>%s!</strong>"
|
||||
ctx.HTML(response, "World")
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```html
|
||||
Hello, <strong>World!</strong>
|
||||
```
|
||||
|
||||
> If you want to learn how to render template files or embedded views please head over to the [View](../view) chapter instead.
|
88
responses/http2_push.md
Normal file
88
responses/http2_push.md
Normal file
|
@ -0,0 +1,88 @@
|
|||
**Content-Type: _*_**
|
||||
|
||||
Server push lets the server preemptively "push" website assets
|
||||
to the client without the user having explicitly asked for them.
|
||||
When used with care, we can send what we know the user is going
|
||||
to need for the page they’re requesting.
|
||||
|
||||
The **target** must either be an absolute path (like "/path") or an absolute
|
||||
URL that contains a valid host and the same scheme as the parent request.
|
||||
If the target is a path, it will inherit the scheme and host of the
|
||||
parent request.
|
||||
|
||||
```go
|
||||
Context.ResponseWriter().Push(target string, opts *http.PushOptions) error
|
||||
```
|
||||
|
||||
> The `Push` method **returns** `iris.ErrPushNotSupported` if the client has disabled push or if push is not supported on the underlying connection.
|
||||
|
||||
## Example
|
||||
|
||||
The `Push` feature works only on **HTTP/2** servers.
|
||||
|
||||
Create the project structure, e.g.
|
||||
|
||||
```text
|
||||
│ main.go
|
||||
└───public
|
||||
│ main.js
|
||||
```
|
||||
|
||||
The `main.js` contains a simple alert function:
|
||||
|
||||
```js
|
||||
window.alert("javascript loaded");
|
||||
```
|
||||
|
||||
Execute the following command to generate sample server keys:
|
||||
|
||||
```sh
|
||||
$ openssl req -new -newkey rsa:4096 -x509 -sha256 \
|
||||
-days 365 -nodes -out mycert.crt -keyout mykey.key
|
||||
```
|
||||
|
||||
Create the `main.go` file and copy-paste the code below:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/kataras/iris/v12"
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.Get("/", pushHandler)
|
||||
app.Get("/main.js", simpleAssetHandler)
|
||||
|
||||
app.Run(iris.TLS("127.0.0.1:443", "mycert.crt", "mykey.key"))
|
||||
}
|
||||
|
||||
func pushHandler(ctx iris.Context) {
|
||||
target := "/main.js"
|
||||
err := ctx.ResponseWriter().Push(target, nil)
|
||||
if err != nil {
|
||||
if err == iris.ErrPushNotSupported {
|
||||
ctx.StopWithText(iris.StatusHTTPVersionNotSupported,
|
||||
"HTTP/2 push not supported.")
|
||||
} else {
|
||||
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(`<html><body><script src="%s"></script></body></html>`, target)
|
||||
}
|
||||
|
||||
func simpleAssetHandler(ctx iris.Context) {
|
||||
ctx.ServeFile("./public/main.js")
|
||||
}
|
||||
```
|
||||
|
||||
Run the server:
|
||||
|
||||
```sh
|
||||
$ go run main.go
|
||||
```
|
||||
|
||||
Now, open your browser's developer tools and click the `Network` tab. Navigate to https://127.0.0.1/, the `main.js` _Initiator_ should be `Push / (index)` as shown below:
|
||||
|
||||
![](../.assets/http2push.png)
|
69
responses/json.md
Normal file
69
responses/json.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
**Content-Type: "application/json"**
|
||||
|
||||
The `Context.JSON(v, ...opts)` is the method which sends JSON responses to the client. It accepts the value and optional settings for rendering. The `JSON` options structure looks like this:
|
||||
|
||||
```go
|
||||
// JSON contains the options for the JSON (Context's) Renderer.
|
||||
type JSON struct {
|
||||
// http-specific
|
||||
StreamingJSON bool
|
||||
// content-specific
|
||||
UnescapeHTML bool
|
||||
Indent string
|
||||
Prefix string
|
||||
ASCII bool
|
||||
// if true then it prepends a "while(1);" when Go slice (to JSON Array) value.
|
||||
Secure bool
|
||||
// proto.Message specific marshal options.
|
||||
Proto ProtoMarshalOptions
|
||||
}
|
||||
```
|
||||
|
||||
> If `Indent` field is empty and the application runs without optimizations, the `Indent` field will be automatically set to `2 spaces`.
|
||||
|
||||
So, if we want to write a JSON with indentation of four spaces and prefixed with `while(1)` we can do something like that:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
response := map[string]interface{}{"message": "ok"}
|
||||
options := iris.JSON{Indent: " ", Secure:true}
|
||||
ctx.JSON(response, options)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
As with all RESTful rich responses, any marshallable (JSON at this case) Go type can be given. If we want to render a Go struct as JSON, the struct's fields we want to render should be **[exported](https://tour.golang.org/basics/3)**, and optionally tagged with the `json` struct tag. Look the exaple below:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
IgnoredField int `json:"-"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := User{
|
||||
Firstname: "makis",
|
||||
Lastname: "maropoulos",
|
||||
IgnoredField:42,
|
||||
}
|
||||
|
||||
ctx.JSON(response)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```json
|
||||
{
|
||||
"firstname": "makis",
|
||||
"lastname": "maropoulos"
|
||||
}
|
||||
```
|
82
responses/jsonp.md
Normal file
82
responses/jsonp.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
**Content-Type: "text/javascript"**
|
||||
|
||||
## What is JSONP, and why was it created?
|
||||
|
||||
Say you're on domain `example.com`, and you want to make a request to domain `example.net`. To do so, you need to cross domain boundaries, a no-no in most of browserland.
|
||||
|
||||
The one item that bypasses this limitation is `<script>` tags. When you use a script tag, the domain limitation is ignored, but under normal circumstances, you can't really do anything with the results, the script just gets evaluated.
|
||||
|
||||
Enter **JSONP**. When you make your request to a server that is JSONP enabled, you pass a special parameter that tells the server a little bit about your page. That way, the server is able to nicely wrap up its response in a way that your page can handle.
|
||||
|
||||
For example, say the server expects a parameter called callback to enable its JSONP capabilities. Then your request would look like:
|
||||
|
||||
```
|
||||
http://www.example.net/sample?callback=mycallback
|
||||
```
|
||||
|
||||
Without JSONP, this might return some basic JavaScript object, like so:
|
||||
```json
|
||||
{ "foo": "bar" }
|
||||
```
|
||||
|
||||
However, with `JSONP`, when the server receives the "callback" parameter, it wraps up the result a little differently, returning something like this:
|
||||
|
||||
```js
|
||||
mycallback({ "foo": "bar" });
|
||||
```
|
||||
|
||||
As you can see, it will now invoke the method you specified. So, in your page, you define the callback function:
|
||||
|
||||
```js
|
||||
mycallback = function(data){
|
||||
alert(data.foo);
|
||||
};
|
||||
```
|
||||
|
||||
And now, when the script is loaded, it'll be evaluated, and your function will be executed. Voila, cross-domain requests!
|
||||
|
||||
It's also worth noting the one major issue with JSONP: you lose a lot of control of the request. For example, there is no "nice" way to get proper failure codes back. As a result, you end up using timers to monitor the request, etc, which is always a bit suspect. The proposition for JSONRequest is a great solution to allowing cross domain scripting, maintaining security, and allowing proper control of the request.
|
||||
|
||||
**However**, CORS[*](https://github.com/kataras/iris/tree/master/_examples/auth/cors) is the recommended approach vs. JSONRequest. JSONP is still useful for older browser support, but given the security implications, unless you have no choice CORS is the better choice.
|
||||
|
||||
> https://stackoverflow.com/a/2067584 _(source)_
|
||||
|
||||
## Send JSONP with Iris
|
||||
|
||||
The `Context.JSONP(v, ...opts)` is the method which sends JSONP responses to the client. It accepts the value and optional settings for rendering. The `JSONP` options structure looks like this:
|
||||
|
||||
```go
|
||||
type JSONP struct {
|
||||
Indent string
|
||||
Callback string
|
||||
}
|
||||
```
|
||||
|
||||
> If `Indent` field is empty and the application runs without optimizations, the `Indent` field will be automatically set to `4 spaces`.
|
||||
|
||||
So, if we want to write a JSONP with indentation of two spaces and a callback extracted from URL Query Parameter of `?callback=mycallback`, we write something like that:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
callback := ctx.URLParamDefault("callback", "defaultCallback")
|
||||
response := map[string]interface{}{"foo": "bar"}
|
||||
options := iris.JSONP{Indent: " ", Callback:true}
|
||||
ctx.JSONP(response, options)
|
||||
}
|
||||
```
|
||||
|
||||
If we want to render a Go struct as JSONP's callback data, the struct's fields we want to render should be **[exported](https://tour.golang.org/basics/3)**, and optionally tagged with the `json` struct tag. Look the exaple below:
|
||||
|
||||
```go
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := Item{
|
||||
Name: "gopher",
|
||||
}
|
||||
|
||||
ctx.JSONP(response, iris.JSONP{Callback: "addToCard"})
|
||||
}
|
||||
```
|
23
responses/markdown.md
Normal file
23
responses/markdown.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
**Content-Type: "text/html"**
|
||||
|
||||
Iris can be used to parse and render markdown contents as HTML to the web client.
|
||||
|
||||
```go
|
||||
Markdown(contents []byte, options ...Markdown) (int, error)
|
||||
```
|
||||
|
||||
It accepts the raw `[]byte` contents and optionally `Markdown` options structure which contains a single `Sanitize bool` field. If `Sanitize` is set to true then it takes a `[]byte` that contains a HTML fragment or document and applies the `UGCPolicy`. The UGCPolicy is a policy aimed at user generated content that is a result of `HTML WYSIWYG` tools and Markdown conversions. This is expected to be a fairly rich document where as much markup as possible should be retained. Markdown permits raw HTML so we are basically providing a policy to sanitise **HTML5 documents safely** but with the
|
||||
least intrusion on the formatting expectations of the user.
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
response := []byte(`# Hello Dynamic Markdown -- Iris`)
|
||||
ctx.Markdown(response)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```html
|
||||
<h1>Hello Dynamic Markdown – Iris</h1>
|
||||
```
|
22
responses/messagepack.md
Normal file
22
responses/messagepack.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
**Content-Type: "application/msgpack"**
|
||||
|
||||
MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller. Small integers are encoded into a single byte, and typical short strings require only one extra byte in addition to the strings themselves. https://msgpack.org
|
||||
|
||||
The `Context.MsgPack(v)` is the method which sends MessagePack responses to the client. It accepts any Go value. As always, if it's a struct value, the fields should be exported.
|
||||
|
||||
```go
|
||||
type Data struct {
|
||||
Message string `msgpack:"message"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := Data{Message: "a message"}
|
||||
ctx.MsgPack(response)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```text
|
||||
\x81\xa7message\xa9a message
|
||||
```
|
40
responses/outroduction.md
Normal file
40
responses/outroduction.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
Iris does offer quite lot amount of helpers to help you out correctly send rich responses to the client. However, you are not limited to those helpers. You can send any data format to the client through `ctx.ContentType("a-mime-type")` and `ctx.Write([]byte)`.
|
||||
|
||||
Available Content Type helpers:
|
||||
|
||||
```go
|
||||
// ContentType sets the response writer's
|
||||
// header "Content-Type" to the 'cType'.
|
||||
ContentType(cType string)
|
||||
// GetContentType returns the response writer's
|
||||
// header value of "Content-Type".
|
||||
GetContentType() string
|
||||
// GetContentType returns the request's
|
||||
// trim-ed(without the charset and priority values)
|
||||
// header value of "Content-Type".
|
||||
GetContentTypeRequested() string
|
||||
|
||||
// GetContentLength returns the request's
|
||||
// header value of "Content-Length".
|
||||
GetContentLength() int64
|
||||
```
|
||||
|
||||
Available raw `[]byte` Write helpers:
|
||||
|
||||
```go
|
||||
Write(body []byte) (int, error)
|
||||
// Writef formats according to a format specifier and writes to the response.
|
||||
Writef(format string, args ...interface{}) (int, error)
|
||||
// WriteString writes a simple string to the response.
|
||||
WriteString(body string) (int, error)
|
||||
// WriteNotModified sends a 304 "Not Modified" status code to the client,
|
||||
// it makes sure that the content type, the content length headers
|
||||
// and any "ETag" are removed before the response sent.
|
||||
WriteNotModified()
|
||||
// WriteWithExpiration works like `Write` but it will
|
||||
// check if a resource is modified,
|
||||
// based on the "modtime" input argument,
|
||||
// otherwise sends a 304 status code in order to
|
||||
// leave the client-side render the cached content.
|
||||
WriteWithExpiration(body []byte, modtime time.Time) (int, error)
|
||||
```
|
107
responses/problem.md
Normal file
107
responses/problem.md
Normal file
|
@ -0,0 +1,107 @@
|
|||
**Content-Type: "application/problem+json" or "application/problem+xml"**
|
||||
|
||||
Iris has builtin support for the [Problem Details for HTTP APIs](https://tools.ietf.org/html/rfc7807).
|
||||
|
||||
The `Context.Problem` method sends a `JSON` or `XML` problem response. Behaves exactly like `Context.JSON` but with default `ProblemOptions.JSON` indent of two spaces (" ") and a response content type of `"application/problem+json"` instead.
|
||||
|
||||
```go
|
||||
Problem(v interface{}, opts ...ProblemOptions) (int, error)
|
||||
```
|
||||
|
||||
The `ProblemOptions` looks like that:
|
||||
|
||||
```go
|
||||
// ProblemOptions the optional settings when server replies with a Problem.
|
||||
// See `Context.Problem` method and `Problem` type for more details.
|
||||
type ProblemOptions struct {
|
||||
// JSON are the optional JSON renderer options.
|
||||
JSON JSON
|
||||
|
||||
// RenderXML set to true if want to render as XML doc.
|
||||
// See `XML` option field too.
|
||||
RenderXML bool
|
||||
// XML are the optional XML renderer options.
|
||||
// Affect only when `RenderXML` field is set to true.
|
||||
XML XML
|
||||
|
||||
// RetryAfter sets the Retry-After response header.
|
||||
// https://tools.ietf.org/html/rfc7231#section-7.1.3
|
||||
// The value can be one of those:
|
||||
// time.Time
|
||||
// time.Duration for seconds
|
||||
// int64, int, float64 for seconds
|
||||
// string for duration string or for datetime string.
|
||||
//
|
||||
// Examples:
|
||||
// time.Now().Add(5 * time.Minute),
|
||||
// 300 * time.Second,
|
||||
// "5m",
|
||||
// 300
|
||||
RetryAfter interface{}
|
||||
// A function that, if specified, can dynamically set
|
||||
// retry-after based on the request. Useful for ProblemOptions reusability.
|
||||
// Should return time.Time, time.Duration, int64, int, float64 or string.
|
||||
//
|
||||
// Overrides the RetryAfter field.
|
||||
RetryAfterFunc func(Context) interface{}
|
||||
}
|
||||
```
|
||||
|
||||
Use the `options.RenderXML` and `XML` fields to change this behavior and send a response of content type `"application/problem+xml"` instead.
|
||||
|
||||
```go
|
||||
func newProductProblem(productName, detail string) iris.Problem {
|
||||
return iris.NewProblem().
|
||||
// The type URI, if relative it automatically convert to absolute.
|
||||
Type("/product-error").
|
||||
// The title, if empty then it gets it from the status code.
|
||||
Title("Product validation problem").
|
||||
// Any optional details.
|
||||
Detail(detail).
|
||||
// The status error code, required.
|
||||
Status(iris.StatusBadRequest).
|
||||
// Any custom key-value pair.
|
||||
Key("productName", productName)
|
||||
// Optional cause of the problem, chain of Problems.
|
||||
// .Cause(other iris.Problem)
|
||||
}
|
||||
|
||||
func fireProblem(ctx iris.Context) {
|
||||
response := newProductProblem("product name", "problem details")
|
||||
options := iris.ProblemOptions{
|
||||
JSON: iris.JSON{
|
||||
Indent: " ",
|
||||
},
|
||||
RetryAfter: 300,
|
||||
}
|
||||
|
||||
ctx.Problem(response, options)
|
||||
}
|
||||
```
|
||||
|
||||
**Outputs** "application/problem+json"
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://host.domain/product-error",
|
||||
"status": 400,
|
||||
"title": "Product validation problem",
|
||||
"detail": "problem error details",
|
||||
"productName": "product name"
|
||||
}
|
||||
```
|
||||
|
||||
When RenderXML is set to true then the response will look be rendered as XML instead.
|
||||
|
||||
**Outputs** "application/problem+xml"
|
||||
|
||||
```xml
|
||||
<Problem>
|
||||
<Type>https://host.domain/product-error</Type>
|
||||
<Status>400</Status>
|
||||
<Title>Product validation problem</Title>
|
||||
<Detail>problem error details</Detail>
|
||||
<ProductName>product name</ProductName>
|
||||
</Problem>
|
||||
```
|
||||
|
200
responses/protobuf.md
Normal file
200
responses/protobuf.md
Normal file
|
@ -0,0 +1,200 @@
|
|||
**Content-Type: "application/x-protobuf"**
|
||||
|
||||
Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. https://developers.google.com/protocol-buffers
|
||||
|
||||
The `Context.Protobuf(proto.Message)` is the method which sends protos to the client. It accepts a [proto.Message](https://godoc.org/google.golang.org/protobuf/proto#Message) value.
|
||||
|
||||
> Note: Iris is using the newest version of the Go protocol buffers implementation. Read more about it at [The Go Blog: A new Go API for Protocol Buffers](https://blog.golang.org/protobuf-apiv2).
|
||||
|
||||
|
||||
The methods you need to know when working with Protocol Buffers are the following:
|
||||
|
||||
```go
|
||||
// Protobuf writes a proto message to the client,
|
||||
// which should be able to read and parse protos too.
|
||||
Protobuf(v proto.Message) (int, error)
|
||||
// ReadProtobuf binds the request body
|
||||
// to the proto message.
|
||||
ReadProtobuf(ptr proto.Message) error
|
||||
```
|
||||
|
||||
```go
|
||||
// JSON renders a proto.Message compatible value as JSON.
|
||||
JSON(v interface{}, options ...JSON) (int, error)
|
||||
// ReadJSONProtobuf reads a JSON body request
|
||||
// into the given "ptr" proto.Message.
|
||||
ReadJSONProtobuf(ptr proto.Message, opts ...protojson.UnmarshalOptions) error
|
||||
```
|
||||
|
||||
```go
|
||||
type JSON struct {
|
||||
// [...other fields]
|
||||
Proto ProtoMarshalOptions
|
||||
}
|
||||
|
||||
type ProtoMarshalOptions struct {
|
||||
// Multiline specifies whether the marshaler
|
||||
// should format the output in
|
||||
// indented-form with every textual element
|
||||
// on a new line.
|
||||
// If Indent is an empty string,
|
||||
// then an arbitrary indent is chosen.
|
||||
Multiline bool
|
||||
|
||||
// Indent specifies the set of indentation
|
||||
// characters to use in a multiline
|
||||
// formatted output such that every entry
|
||||
// is preceded by Indent and
|
||||
// terminated by a newline. If non-empty,
|
||||
// then Multiline is treated as true.
|
||||
// Indent can only be composed of space or tab characters.
|
||||
Indent string
|
||||
|
||||
// AllowPartial allows messages that have
|
||||
// missing required fields to marshal
|
||||
// without returning an error.
|
||||
// If AllowPartial is false (the default),
|
||||
// Marshal will return error if there are
|
||||
// any missing required fields.
|
||||
AllowPartial bool
|
||||
|
||||
// UseProtoNames uses proto field name
|
||||
// instead of lowerCamelCase name in JSON
|
||||
// field names.
|
||||
UseProtoNames bool
|
||||
|
||||
// UseEnumNumbers emits enum values as numbers.
|
||||
UseEnumNumbers bool
|
||||
|
||||
// EmitUnpopulated specifies whether to emit unpopulated fields.
|
||||
// It does not emit unpopulated oneof fields
|
||||
// or unpopulated extension fields.
|
||||
// The JSON value emitted for unpopulated fields are as follows:
|
||||
// ╔═══════╤════════════════════════════
|
||||
// ║ JSON │ Protobuf field
|
||||
// ╠═══════╪════════════════════════════
|
||||
// ║ false │ proto3 boolean fields
|
||||
// ║ 0 │ proto3 numeric fields
|
||||
// ║ "" │ proto3 string/bytes fields
|
||||
// ║ null │ proto2 scalar fields
|
||||
// ║ null │ message fields
|
||||
// ║ [] │ list fields
|
||||
// ║ {} │ map fields
|
||||
// ╚═══════╧════════════════════════════
|
||||
EmitUnpopulated bool
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Let's create a simple application that showcases the use case of all of the above methods.
|
||||
|
||||
```text
|
||||
│ main.go
|
||||
│ go.mod
|
||||
│ go.sum
|
||||
└───protos
|
||||
│ hello.proto
|
||||
│ hello.pb.go
|
||||
```
|
||||
|
||||
Navigate to the project's directory, create your go module: `$ go mod init app`.
|
||||
|
||||
The `protos/hello.proto` contents should look like that:
|
||||
|
||||
```proto
|
||||
syntax = "proto3";
|
||||
|
||||
package protos;
|
||||
|
||||
option go_package = "./protos";
|
||||
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
```
|
||||
|
||||
To generate the `protos/hello.pb.go` file follow the commands below:
|
||||
|
||||
1. Install the protoc-gen-go tool.
|
||||
|
||||
```sh
|
||||
$ go get -u google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
```
|
||||
|
||||
2. Generate proto
|
||||
|
||||
```sh
|
||||
$ protoc -I protos/ protos/hello.proto --go_out=.
|
||||
```
|
||||
|
||||
Now, create the `main.go` file and copy-paste the following code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"app/protos"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
app.Get("/", send)
|
||||
app.Get("/json", sendAsJSON)
|
||||
app.Post("/read", read)
|
||||
app.Post("/read_json", readFromJSON)
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func send(ctx iris.Context) {
|
||||
response := &protos.HelloReply{Message: "Hello, World!"}
|
||||
ctx.Protobuf(response)
|
||||
}
|
||||
|
||||
func sendAsJSON(ctx iris.Context) {
|
||||
response := &protos.HelloReply{Message: "Hello, World!"}
|
||||
options := iris.JSON{
|
||||
Proto: iris.ProtoMarshalOptions{
|
||||
AllowPartial: true,
|
||||
Multiline: true,
|
||||
Indent: " ",
|
||||
},
|
||||
}
|
||||
|
||||
ctx.JSON(response, options)
|
||||
}
|
||||
|
||||
func read(ctx iris.Context) {
|
||||
var request protos.HelloRequest
|
||||
|
||||
err := ctx.ReadProtobuf(&request)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("HelloRequest.Name = %s", request.Name)
|
||||
}
|
||||
|
||||
func readFromJSON(ctx iris.Context) {
|
||||
var request protos.HelloRequest
|
||||
|
||||
err := ctx.ReadJSONProtobuf(&request)
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("HelloRequest.Name = %s", request.Name)
|
||||
}
|
||||
```
|
||||
|
||||
> If you want to learn about Iris gRPC support, please navigate through the [mvc/grpc](../mvc/mvc-grpc.md) section instead.
|
105
responses/recorder.md
Normal file
105
responses/recorder.md
Normal file
|
@ -0,0 +1,105 @@
|
|||
A response recorder is one of the Iris specific `http.ResponseWriter`. It records the response body, status code and headers that you can manipulate at any handler inside a route's handlers chain.
|
||||
|
||||
1. Call `Context.Record()` before send data.
|
||||
2. The `Context.Recorder()` returns a [ResponseRecorder](https://godoc.org/github.com/kataras/iris/context#ResponseRecorder). Its methods can be used to manipulate or retrieve the response.
|
||||
|
||||
The `ResponseRecorder` type contains the standard Iris ResponseWriter methods plus the following methods.
|
||||
|
||||
Body **returns** the body tracked from the writer so far. Do not use this for edit.
|
||||
|
||||
```go
|
||||
Body() []byte
|
||||
```
|
||||
|
||||
Use this to clear the body.
|
||||
|
||||
```go
|
||||
ResetBody()
|
||||
```
|
||||
|
||||
Use `Write/Writef/WriteString` to stream write and `SetBody/SetBodyString` to **set** body instead.
|
||||
|
||||
```go
|
||||
Write(contents []byte) (int, error)
|
||||
|
||||
Writef(format string, a ...interface{}) (n int, err error)
|
||||
|
||||
WriteString(s string) (n int, err error)
|
||||
|
||||
SetBody(b []byte)
|
||||
|
||||
SetBodyString(s string)
|
||||
```
|
||||
|
||||
Reset headers to their original state, before `Context.Record` call.
|
||||
|
||||
```go
|
||||
ResetHeaders()
|
||||
```
|
||||
|
||||
Clear all headers.
|
||||
|
||||
```go
|
||||
ClearHeaders()
|
||||
```
|
||||
|
||||
Reset resets the response body, headers and the status code header.
|
||||
|
||||
```go
|
||||
Reset()
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Record operation log in global Interceptor.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/kataras/iris/v12"
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
// start record.
|
||||
app.Use(func(ctx iris.Context) {
|
||||
ctx.Record()
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
// collect and "log".
|
||||
app.Done(func(ctx iris.Context) {
|
||||
body := ctx.Recorder().Body()
|
||||
|
||||
// Should print success.
|
||||
ctx.Application().Logger().Infof("sent: %s", string(body))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Register the routes...
|
||||
|
||||
```go
|
||||
app.Get("/save", func(ctx iris.Context) {
|
||||
ctx.WriteString("success")
|
||||
ctx.Next() // calls the Done middleware(s).
|
||||
})
|
||||
```
|
||||
|
||||
Or to remove the need of `ctx.Next` in your main handlers, modify the Iris handlers [execution rules](https://github.com/kataras/iris/blob/master/_examples/mvc/middleware/without-ctx-next/main.go) as follows.
|
||||
|
||||
```go
|
||||
// It applies per Party and its children,
|
||||
// therefore, you can create a routes := app.Party("/path")
|
||||
// and set middlewares, their rules and the routes there as well.
|
||||
app.SetExecutionRules(iris.ExecutionRules{
|
||||
Done: iris.ExecutionOptions{Force: true},
|
||||
})
|
||||
|
||||
// [The routes...]
|
||||
app.Get("/data", func(ctx iris.Context) {
|
||||
ctx.JSON(data)
|
||||
})
|
||||
```
|
||||
|
||||
In addition to that, Iris provides a comprehensive API for **Transactions**. Learn more about it by running an [example](https://github.com/kataras/iris/blob/master/_examples/response-writer/transactions/main.go).
|
59
responses/sse.md
Normal file
59
responses/sse.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
**Content-Type: _*_**
|
||||
|
||||
A server-sent event is when a web page automatically gets updates from a server.
|
||||
|
||||
This was also possible before, but the web page would have to ask if any updates were available. With server-sent events, the updates come automatically.
|
||||
|
||||
Examples: Facebook/Twitter updates, stock price updates, news feeds, sport results, etc. https://www.w3schools.com/htmL/html5_serversentevents.asp
|
||||
|
||||
## Receive Server-Sent Event Notifications
|
||||
|
||||
The EventSource object is used to receive server-sent event notifications:
|
||||
|
||||
```js
|
||||
var source = new EventSource("http://localhost:8080/handler");
|
||||
source.onmessage = function(event) {
|
||||
document.getElementById("result").innerHTML += event.data + "<br>";
|
||||
};
|
||||
```
|
||||
|
||||
### Check Server-Sent Events Support
|
||||
|
||||
```js
|
||||
if(typeof(EventSource) !== "undefined") {
|
||||
// Yes! Server-sent events support!
|
||||
// Some code.....
|
||||
} else {
|
||||
// Sorry! No server-sent events support..
|
||||
}
|
||||
```
|
||||
|
||||
## Server-Side Code Example
|
||||
|
||||
The server-side event stream syntax is simple. Set the `"Content-Type"` header to `"text/event-stream"`. Now you can start sending event streams. The response messages starts with `"data: "` and ends with `"\n\n"`.
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
// It's always recommended to first check for `Flusher`
|
||||
// compatibility in Go, you do that with:
|
||||
flusher, ok := ctx.ResponseWriter().Flusher()
|
||||
if !ok {
|
||||
ctx.StopWithText(iris.StatusHTTPVersionNotSupported, "Streaming unsupported!")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ContentType("text/event-stream")
|
||||
ctx.Header("Cache-Control", "no-cache")
|
||||
|
||||
now := time.Now()
|
||||
ctx.Writef("data: The server time is: %s\n\n", now)
|
||||
|
||||
// Flush the data immediately instead of buffering it for later.
|
||||
flusher.Flush()
|
||||
}
|
||||
```
|
||||
|
||||
* Set the `"Content-Type"` header to `"text/event-stream"`
|
||||
* Specify that the page should not cache
|
||||
* Output the data to send (**Always** start with `"data: "`)
|
||||
* `Flush` the output data back to the web page
|
122
responses/stream.md
Normal file
122
responses/stream.md
Normal file
|
@ -0,0 +1,122 @@
|
|||
**Content-Type: _*_**
|
||||
|
||||
**Chunked transfer encoding is a streaming data transfer mechanism** available in HTTP/1.1. In chunked transfer encoding, the data stream is divided into a series of non-overlapping "chunks". The chunks are sent out and received independently of one another. No knowledge of the data stream outside the currently-being-processed chunk is necessary for both the sender and the receiver at any given time.
|
||||
|
||||
The **chunked** keyword in the **Transfer-Encoding header** is used to indicate chunked transfer.
|
||||
|
||||
> HTTP/2 uses DATA frames to carry message payloads. The "chunked" transfer encoding defined in MUST NOT be used in HTTP/2.
|
||||
|
||||
References:
|
||||
- [Wikipedia](https://en.wikipedia.org/wiki/Chunked_transfer_encoding)
|
||||
- [RFC](https://tools.ietf.org/html/rfc7230#section-4.1)
|
||||
|
||||
As we read above, the `Transfer-Encoding: "chunked"` header is required:
|
||||
|
||||
```go
|
||||
ctx.Header("Transfer-Encoding", "chunked")
|
||||
```
|
||||
|
||||
Then, define the response content type (when we use the `ctx.Write` methods), e.g.
|
||||
|
||||
```go
|
||||
ctx.ContentType("text/html")
|
||||
```
|
||||
|
||||
And we write the contents:
|
||||
|
||||
```go
|
||||
ctx.Writef("A message here")
|
||||
```
|
||||
|
||||
Finally, we must flush the contents with:
|
||||
|
||||
```go
|
||||
ctx.ResponseWriter().Flush()
|
||||
```
|
||||
|
||||
And repeat.
|
||||
|
||||
Iris gives you a helper method which sends streaming data until connection close or error returned:
|
||||
|
||||
```go
|
||||
StreamWriter(func(w io.Writer) error) error
|
||||
```
|
||||
|
||||
The second way for streaming data is with `ctx.XXX` response methods (as we've seen previously, e.g. `ctx.JSON`) following by a `ctx.ResponseWriter().Flush()`).
|
||||
|
||||
## Example
|
||||
|
||||
Let's say that you want to send number messages from 1 to 29.
|
||||
|
||||
```go
|
||||
var errDone = errors.New("done")
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
ctx.ContentType("text/html")
|
||||
ctx.Header("Transfer-Encoding", "chunked")
|
||||
i := 0
|
||||
ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29}
|
||||
// Send the response in chunks and
|
||||
// wait for half a second between each chunk,
|
||||
// until connection closed.
|
||||
err := ctx.StreamWriter(func(w io.Writer) error {
|
||||
ctx.Writef("Message number %d<br>", ints[i])
|
||||
time.Sleep(500 * time.Millisecond) // simulate delay.
|
||||
if i == len(ints)-1 {
|
||||
return errDone // ends the loop.
|
||||
}
|
||||
i++
|
||||
return nil // continue write
|
||||
})
|
||||
|
||||
if err != errDone {
|
||||
// Test it by canceling the request before the stream ends:
|
||||
// [ERRO] $DATETIME stream: context canceled.
|
||||
ctx.Application().Logger().Errorf("stream: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Example
|
||||
|
||||
Let's do the same using JSON.
|
||||
|
||||
```go
|
||||
type messageNumber struct {
|
||||
Number int `json:"number"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
ctx.Header("Transfer-Encoding", "chunked")
|
||||
i := 0
|
||||
ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29}
|
||||
// Send the response in chunks and wait
|
||||
// for half a second between each chunk,
|
||||
// until connection close.
|
||||
notifyClose := ctx.Request().Context().Done()
|
||||
for {
|
||||
select {
|
||||
case <-notifyClose:
|
||||
// err := ctx.Request().Context().Err()
|
||||
ctx.Application().Logger().Infof("Connection closed, loop end.")
|
||||
return
|
||||
default:
|
||||
ctx.JSON(messageNumber{Number: ints[i]})
|
||||
ctx.WriteString("\n")
|
||||
time.Sleep(500 * time.Millisecond) // simulate delay.
|
||||
if i == len(ints)-1 {
|
||||
ctx.Application().Logger().Infof("Loop end.")
|
||||
return
|
||||
}
|
||||
i++
|
||||
ctx.ResponseWriter().Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
![](../.gitbook/assets/iris-stream-writer.gif)
|
||||
|
||||
That's all. As you've noticed, the client receives messages while loading. Check the next chapter (Server-Sent Events) to see an alternative way of sending messages to the client with connection-alive and loaded page.
|
19
responses/text.md
Normal file
19
responses/text.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
**Content-Type: "text/plain"**
|
||||
|
||||
The `Context.Text(format, ...args)` is the method which sends plain text responses to the client. It accepts the value (like fmt package works). It's identical to `Context.WriteString` and `Context.Writef`.
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
response := "Hello, %s!"
|
||||
ctx.Text(response, "World")
|
||||
// ctx.Writef(response, "World")
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
That's all.
|
57
responses/xml.md
Normal file
57
responses/xml.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
**Content-Type: "text/xml"**
|
||||
|
||||
Extensible Markup Language (**XML**) is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable.
|
||||
|
||||
The `Context.XML(v, ...opts)` is the method which sends XML responses to the client. It accepts the value and optional settings for rendering. The `XML` options structure looks like this:
|
||||
|
||||
```go
|
||||
type XML struct {
|
||||
Indent string
|
||||
Prefix string
|
||||
}
|
||||
```
|
||||
|
||||
> If `Indent` field is empty and the application runs without optimizations, the `Indent` field will be automatically set to `2 spaces`.
|
||||
|
||||
Render a Go struct as XML response:
|
||||
|
||||
```go
|
||||
type ExampleXML struct {
|
||||
XMLName xml.Name `xml:"example"`
|
||||
One string `xml:"one,attr"`
|
||||
Two string `xml:"two,attr"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := ExampleXML{One: "hello", Two: "xml"}
|
||||
ctx.XML(response)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```xml
|
||||
<example one="hello" two="xml"/>
|
||||
```
|
||||
|
||||
You can **NOT** provide a raw `map` or `iris.Map` on `context.XML` because the value should complete the [xml.Marshaler](https://godoc.org/encoding/xml#Marshaler) interface. Iris has the `iris.XMLMap` which converts a `map` (or `iris.Map`) to xml Marshaler. That helper function accepts the root level name and a map. Example:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
kv := iris.Map{"key": "value"}
|
||||
response := iris.XMLMap("keys", kv)
|
||||
ctx.XML(response, iris.XML{Indent: " "})
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```xml
|
||||
<keys>
|
||||
<key>value</key>
|
||||
</keys>
|
||||
```
|
||||
|
||||
References:
|
||||
- [Specification](https://www.w3.org/TR/REC-xml/)
|
||||
- [SOAP vs REST vs JSON - a 2020 comparison](https://raygun.com/blog/soap-vs-rest-vs-json/)
|
37
responses/yaml.md
Normal file
37
responses/yaml.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
**Content-Type: "application/x-yaml"**
|
||||
|
||||
**YAML** (a recursive acronym for "YAML Ain't Markup Language") is a human-readable data-serialization language. It is commonly used for **configuration** files and in applications where data is being stored or transmitted. YAML targets many of the same communications applications as Extensible Markup Language (XML) but has a minimal syntax which intentionally differs from [SGML](https://en.wikipedia.org/wiki/Standard_Generalized_Markup_Language).
|
||||
|
||||
The `Context.YAML(v)` is the method which sends YAML responses to the client. It accepts a value of any type. You only need the `yaml` struct field and all fields should be exported.
|
||||
|
||||
```go
|
||||
type ExampleYAML struct {
|
||||
Name string `yaml:"name"`
|
||||
ServerAddr string `yaml:"ServerAddr"`
|
||||
}
|
||||
|
||||
func handler(ctx iris.Context) {
|
||||
response := ExampleYAML{Name: "Iris", ServerAddr: "localhost:8080"}
|
||||
ctx.YAML(response)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**
|
||||
|
||||
```yaml
|
||||
name: Iris
|
||||
ServerAddr: localhost:8080
|
||||
```
|
||||
|
||||
The same result can be achieved using `iris.Map` or a standard Go `map`:
|
||||
|
||||
```go
|
||||
func handler(ctx iris.Context) {
|
||||
response := iris.Map{"name": "Iris", "serverAddr": "localhost:8080"}
|
||||
ctx.YAML(response)
|
||||
}
|
||||
```
|
||||
|
||||
References:
|
||||
- [Wikipedia](https://en.wikipedia.org/wiki/YAML)
|
||||
- [The Official YAML Web Site](https://yaml.org/)
|
Loading…
Reference in New Issue
Block a user