├── .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 | ![](https://storage.googleapis.com/zenn-user-upload/4f86b0635c6b-20240121.gif) 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 | --------------------------------------------------------------------------------