简体   繁体   English

使用React在Golang中进行服务器端渲染

[英]Using React for server-side rendering with Golang

Say we want to use a Node.js process pool, to render some HTML using React. 假设我们要使用Node.js进程池,以使用React渲染一些HTML。 (I am not saying this is a good idea, just assume that this is the case, lulz). (我并不是说这是一个好主意,只是假设情况确实如此,lulz)。

Is there a way to pass a reference to the request/response streams from Golang to a Node.js process? 有没有一种方法可以将对请求/响应流的引用从Golang传递到Node.js进程? I think the cluster module for Node.js uses this technique, by passing a file descriptor or something like that. 我认为Node.js的集群模块通过传递文件描述符或类似的东西来使用这种技术。 Note that the Node.js process pool (maybe 3 processes or so), would be children of the Golang process. 请注意,Node.js进程池(大约3个进程)将是Golang进程的子进程。

The following is a really rough draft, that uses a channel to implement a process pool, and shows how Go's io.Reader and io.Writer interfaces can be used to plug process and HTTP streams together. 以下是一个非常粗略的草稿,它使用通道来实现进程池,并显示了如何使用Go的io.Reader和io.Writer接口将进程流和HTTP流插入在一起。 The code is also on the playground , for easy copy-paste. 该代码也位于操场上 ,以便于复制粘贴。

Note that I've written this in a hurry, just to show the general idea. 请注意,我急着写了这个,只是为了展示总体思路。 Don't use this in production. 不要在生产中使用它。 I'm sure there are bugs, especially related to incomplete reads or writes. 我确定存在错误,尤其是与不完整的读取或写入有关的错误。 Processes exiting while idle are also not handled. 空闲时退出的进程也不会被处理。

package main

import (
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "os"
        "os/exec"
)

exec.Cmd.Stdin and exec.Cmd.Stdout are of type io.Reader and io.Writer respectively. exec.Cmd.Stdin和exec.Cmd.Stdout的类型分别为io.Reader和io.Writer。 However, it is more convenient for us to treat them the other way around. 但是,对于我们来说,反过来对待它们更方便。 The StdinPipe and StdoutPipe methods facilitate exactly that, but they must be called only once and only before the process starts. StdinPipe和StdoutPipe方法可以简化此操作,但是必须仅在过程开始之前调用它们一次。 So we store the pipes together with the command itself in a simple wrapper. 因此,我们将管道与命令本身一起存储在一个简单的包装器中。 This allows us to call nodeWrapper.Write([]byte) to send data to node, and nodeWrapper.Read() to read from its stdout. 这使我们可以调用nodeWrapper.Write([] byte)将数据发送到node,并调用nodeWrapper.Read()从其stdout中读取。 This is what I meant when I said in a comment you usually pass Readers and Writers around. 这就是我在评论中说的意思,通常,您会绕过读者和作家。

type nodeWrapper struct {
        *exec.Cmd
        io.Writer // stdin
        io.Reader // stdout
}

// mustSpawnNode returns a started nodejs process that executes render.js
func mustSpawnNode() nodeWrapper {
        cmd := exec.Command("node", "render.js")
        cmd.Stderr = os.Stderr

        stdin, err := cmd.StdinPipe()
        if err != nil {
                panic(err)
        }

        stdout, err := cmd.StdoutPipe()
        if err != nil {
                panic(err)
        }

        if err := cmd.Start(); err != nil {
                panic(err)
        }

        return nodeWrapper{cmd, stdin, stdout}
}

We use a simple channel-based ring buffer here to implement a process pool. 我们在这里使用基于通道的简单环形缓冲区来实现进程池。

The handler parses an HTTP request and extracts information that is required to render a page. 处理程序解析HTTP请求并提取呈现页面所需的信息。 In this example we simply pass the request Path to node. 在此示例中,我们仅将请求路径传递给节点。 We then wait for a free node process and call render. 然后,我们等待一个自由节点进程并调用render。 render will write directly the ResponseWriter. render将直接写入ResponseWriter。

func main() {
        pool := make(chan nodeWrapper, 4) // acts as a ring buffer
        for i := 0; i < cap(pool); i++ {
                pool <- mustSpawnNode()
        }

        log.Println("listening on :3000")
        log.Fatal(http.ListenAndServe(":3000", handler(pool)))
}

func handler(pool chan nodeWrapper) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                var renderArgs struct {
                        Path string
                }
                renderArgs.Path = r.URL.Path

                node := <-pool

                err := render(w, node, renderArgs)
                if err != nil {
                        // Assume the node process has failed and replace it
                        // with a new one.
                        node.Process.Kill()
                        pool <- mustSpawnNode()
                        http.Error(w, err.Error(), 500)
                } else {
                        pool <- node
                }
        }
}

For the rendering we a) want to pass some data to the already running node process, and b) read from node's stdout and, more importantly, have to know when to stop reading. 对于渲染,我们a)希望将一些数据传递到已经运行的节点进程,b)从节点的标准输出读取,更重要的是,必须知道何时停止读取。

Usually we would set Stdout to our desired writer and simply run the process to completion. 通常,我们会将Stdout设置为所需的编写器,然后简单地运行该过程即可完成。 But in this case the process will not exit once it has finished rendering, so it will also not close stdout, and we need a replacement for the usual EOF signal. 但是在这种情况下,该过程一旦完成渲染就不会退出,因此也不会关闭stdout,因此我们需要替换通常的EOF信号。

This is where we have to get creative and a find a solution that works well for you. 这是我们必须发挥创造力并找到适合您的解决方案的地方。 I decided on the following protocol: We write a single line of JSON encoded data to node's stdin and then decode a single JSON encoded string from node's stdout. 我决定采用以下协议:我们将单行JSON编码的数据写入节点的stdin,然后从节点的stdout解码单个JSON编码的字符串。 Ideally we wouldn't buffer the whole HTML document in memory but put it directly on the wire (by writing to w in real-time). 理想情况下,我们不会将整个HTML文档缓存在内存中,而是将其直接放在网上(通过实时写入w)。 But this keeps both the Go code and render.js real simple. 但这使Go代码和render.js都变得非常简单。

func render(w io.Writer, node nodeWrapper, args interface{}) error {
        stdinErr := make(chan error, 1)
        go func() {
                stdinErr <- json.NewEncoder(node).Encode(args)
        }()

        var html string
        if err := json.NewDecoder(node).Decode(&html); err != nil {
                return err
        }
        if _, err := fmt.Fprint(w, html); err != nil {
                return err
        }

        return <-stdinErr
}

And finally, the contents of render.js: 最后,render.js的内容:

let lineReader = require('readline').createInterface({input: process.stdin})

lineReader.on('line', (line) => {
    let data = JSON.parse(line);

    let html = "";
    html += "<h1>Path: " + data.Path + "</h1>\n";
    html += "<small>PID: " + process.pid + "</small>\n";

    process.stdout.write(JSON.stringify(html)+"\n")
})

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM