├── .gitignore
├── README.md
├── frontend
├── App.jsx
├── clientEntry.jsx
├── components
│ └── Counter.jsx
├── package-lock.json
├── package.json
└── serverEntry.jsx
├── go.mod
├── go.sum
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SSR React using Go
2 |
3 | 
4 |
5 | ## What this is
6 |
7 | just a poc for ssring react components using Go.
8 |
9 | ## How to run it
10 |
11 | run the command below and go to `http://localhost:3002`.
12 |
13 | ```
14 | go run main.go
15 | ```
--------------------------------------------------------------------------------
/frontend/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Counter from "./components/Counter";
3 |
4 | function App(props) {
5 | console.log("APP rendered", props);
6 | return (
7 |
8 |
タイトル: {props.Name}
9 |
10 |
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/frontend/clientEntry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App'; // Adjust the import path as necessary
4 |
5 | const root = ReactDOM.hydrateRoot(
6 | document.getElementById('app'),
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/frontend/components/Counter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Counter(props) {
4 | const [count, setCount] = React.useState(props.defaultNum);
5 | return (
6 |
7 |
{count}
8 |
9 |
10 | );
11 | }
12 |
13 | export default Counter;
14 |
--------------------------------------------------------------------------------
/frontend/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "frontend",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0"
14 | }
15 | },
16 | "node_modules/js-tokens": {
17 | "version": "4.0.0",
18 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
19 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
20 | },
21 | "node_modules/loose-envify": {
22 | "version": "1.4.0",
23 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
24 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
25 | "dependencies": {
26 | "js-tokens": "^3.0.0 || ^4.0.0"
27 | },
28 | "bin": {
29 | "loose-envify": "cli.js"
30 | }
31 | },
32 | "node_modules/react": {
33 | "version": "18.2.0",
34 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
35 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
36 | "dependencies": {
37 | "loose-envify": "^1.1.0"
38 | },
39 | "engines": {
40 | "node": ">=0.10.0"
41 | }
42 | },
43 | "node_modules/react-dom": {
44 | "version": "18.2.0",
45 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
46 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
47 | "dependencies": {
48 | "loose-envify": "^1.1.0",
49 | "scheduler": "^0.23.0"
50 | },
51 | "peerDependencies": {
52 | "react": "^18.2.0"
53 | }
54 | },
55 | "node_modules/scheduler": {
56 | "version": "0.23.0",
57 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
58 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
59 | "dependencies": {
60 | "loose-envify": "^1.1.0"
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/serverEntry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToString } from "react-dom/server";
3 | import App from './App'; // Adjust the import path as necessary
4 |
5 | function renderApp(props) {
6 | return renderToString();
7 | }
8 |
9 | globalThis.renderApp = renderApp;
10 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module go-render
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/buke/quickjs-go v0.3.0 // indirect
7 | github.com/evanw/esbuild v0.19.11 // indirect
8 | github.com/lithdew/quickjs v0.0.0-20200714182134-aaa42285c9d2 // indirect
9 | github.com/quickjs-go/quickjs-go v0.0.0-20230414054158-b72900cb68c1 // indirect
10 | github.com/robertkrimen/otto v0.3.0 // indirect
11 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
12 | golang.org/x/text v0.4.0 // indirect
13 | gopkg.in/sourcemap.v1 v1.0.5 // indirect
14 | rogchap.com/v8go v0.9.0 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/buke/quickjs-go v0.3.0 h1:3etkdaPlm/mP48yYT+gjwcTbyqYCqzUcroBtLfmlfZ8=
2 | github.com/buke/quickjs-go v0.3.0/go.mod h1:d+CLE8FY8v4gQJkPlwcinM+E9mhREMpYy5Zn7nlgE9s=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/evanw/esbuild v0.19.11 h1:mbPO1VJ/df//jjUd+p/nRLYCpizXxXb2w/zZMShxa2k=
5 | github.com/evanw/esbuild v0.19.11/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
6 | github.com/lithdew/quickjs v0.0.0-20200714182134-aaa42285c9d2 h1:9o8F2Jlv6jetf9FKdseYhgv036iyW87vi9DoFd2O76s=
7 | github.com/lithdew/quickjs v0.0.0-20200714182134-aaa42285c9d2/go.mod h1:zkXUczDT56GViklqUXAzmvSKkGTxV2jrG/NOWqHAbT8=
8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9 | github.com/quickjs-go/quickjs-go v0.0.0-20230414054158-b72900cb68c1 h1:ewpMPEbRRTe0ifOn2zix4nGt8hDtVdvqH+ivifPvccs=
10 | github.com/quickjs-go/quickjs-go v0.0.0-20230414054158-b72900cb68c1/go.mod h1:N0i1BZeGJnNJpvvrRXKRX3gE6Wdtpzmfx6PfoENb41s=
11 | github.com/robertkrimen/otto v0.3.0 h1:5RI+8860NSxvXywDY9ddF5HcPw0puRsd8EgbXV0oqRE=
12 | github.com/robertkrimen/otto v0.3.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
15 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
18 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
20 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
23 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 | rogchap.com/v8go v0.9.0 h1:wYbUCO4h6fjTamziHrzyrPnpFNuzPpjZY+nfmZjNaew=
26 | rogchap.com/v8go v0.9.0/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs=
27 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "html/template"
7 | "log"
8 | "net/http"
9 |
10 | esbuild "github.com/evanw/esbuild/pkg/api"
11 | v8 "rogchap.com/v8go"
12 | )
13 |
14 | // [Yaffle/TextEncoderTextDecoder.js](https://gist.github.com/Yaffle/5458286)
15 | var textEncoderPolyfill = `function TextEncoder(){} TextEncoder.prototype.encode=function(string){var octets=[],length=string.length,i=0;while(i>c)),c-=6;while(c>=0){octets.push(0x80|((codePoint>>c)&0x3F)),c-=6}i+=codePoint>=0x10000?2:1}return octets};function TextDecoder(){} TextDecoder.prototype.decode=function(octets){var string="",i=0;while(i0?function(){for(var k=0;k
21 |
22 |
23 |
24 | React App
25 |
26 |
27 | {{.RenderedContent}}
28 |
31 |
32 |
33 |
34 | `
35 |
36 | type PageData struct {
37 | RenderedContent template.HTML
38 | InitialProps template.JS
39 | JS template.JS
40 | }
41 |
42 | type InitialProps struct {
43 | Name string
44 | InitialNumber int
45 | }
46 |
47 | func buildBackend() string {
48 | result := esbuild.Build(esbuild.BuildOptions{
49 | EntryPoints: []string{"./frontend/serverEntry.jsx"},
50 | Bundle: true,
51 | Write: false,
52 | Outdir: "/",
53 | Format: esbuild.FormatIIFE,
54 | Platform: esbuild.PlatformBrowser,
55 | Target: esbuild.ES2015,
56 | Banner: map[string]string{
57 | "js": textEncoderPolyfill + processPolyfill + consolePolyfill,
58 | },
59 | Loader: map[string]esbuild.Loader{
60 | ".jsx": esbuild.LoaderJSX,
61 | },
62 | })
63 | script := fmt.Sprintf("%s", result.OutputFiles[0].Contents)
64 | return script
65 | }
66 |
67 | func buildClient() string {
68 | clientResult := esbuild.Build(esbuild.BuildOptions{
69 | EntryPoints: []string{"./frontend/clientEntry.jsx"},
70 | Bundle: true,
71 | Write: true,
72 | })
73 | clientBundleString := string(clientResult.OutputFiles[0].Contents)
74 | return clientBundleString
75 | }
76 |
77 | func main() {
78 | // reactをSSRするためのバンドルを作成
79 | backendBundle := buildBackend()
80 | // reactをhydrateするためのバンドルを作成
81 | clientBundle := buildClient()
82 |
83 | ctx := v8.NewContext(nil)
84 | _, err := ctx.RunScript(backendBundle, "bundle.js")
85 | if err != nil {
86 | log.Fatalf("Failed to evaluate bundled script: %v", err)
87 | }
88 | val, err := ctx.RunScript("renderApp()", "render.js")
89 | if err != nil {
90 | log.Fatalf("Failed to render React component: %v", err)
91 | }
92 | renderedHTML := val.String()
93 |
94 | tmpl, err := template.New("webpage").Parse(htmlTemplate)
95 | if err != nil {
96 | log.Fatal("Error parsing template:", err)
97 | }
98 |
99 | // backendから渡すprops
100 | initialProps := InitialProps{
101 | Name: "GoでReactをSSRする",
102 | InitialNumber: 100,
103 | }
104 | jsonProps, err := json.Marshal(initialProps)
105 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
106 | w.Header().Set("Content-Type", "text/html")
107 | data := PageData{
108 | RenderedContent: template.HTML(renderedHTML),
109 | InitialProps: template.JS(jsonProps),
110 | JS: template.JS(clientBundle),
111 | }
112 | err := tmpl.Execute(w, data)
113 | if err != nil {
114 | http.Error(w, err.Error(), http.StatusInternalServerError)
115 | }
116 | })
117 | fmt.Println("Server is running at http://localhost:3002")
118 | log.Fatal(http.ListenAndServe(":3002", nil))
119 | }
120 |
--------------------------------------------------------------------------------