GithubHelp home page GithubHelp logo

spatie / server-side-rendering Goto Github PK

View Code? Open in Web Editor NEW
602.0 602.0 34.0 68 KB

Server side rendering JavaScript in a PHP application

Home Page: https://sebastiandedeyne.com/posts/2018/server-side-rendering-javascript-from-php

License: MIT License

PHP 98.75% JavaScript 1.25%
javascript php server-side-rendering ssr

server-side-rendering's Introduction

Server side rendering JavaScript in your PHP application

Latest Version on Packagist Build Status Total Downloads

use Spatie\Ssr\Renderer;
use Spatie\Ssr\Engines\V8;

$engine = new V8();

$renderer = new Renderer($engine);

echo $renderer
    ->entry(__DIR__.'/../../public/js/app-server.js')
    ->render();

// <div>My server rendered app!</div>
  • Works with any JavaScript framework that allows for server side rendering
  • Runs with or without the V8Js PHP extension
  • Requires minimal configuration

If you're building a Laravel app, check out the laravel-server-side-rendering package instead.

This readme assumes you already have some know-how about building server rendered JavaScript apps.

Support us

We invest a lot of resources into creating best in class open source packages. You can support us by buying one of our paid products.

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on our contact page. We publish all received postcards on our virtual postcard wall.

Who's this package for?

Server side rendering (SSR) can be hard, and non-trivial to enable in your JavaScript application. Before using this library, make sure you know what you're getting in to. Alex Grigoryan has a pretty concise article on the benefits and caveats of SSR. Anthony Gore also has a great article on server side rendering a Vue application in Laravel, which inspired this library.

In case you're in need of a refresher...

  • SSR reduces the time until the first meaningful paint, providing a better experience for your users
  • SSR is recommended if you need to tailor your app for crawlers that can't execute JavaScript (SEO)
  • SSR adds a meaningful amount of complexity to your application
  • SSR can increase response times and the overall load on your server

When you've got an answer to the "Do I need SSR?" question, ask yourself if you need SSR in a PHP application. Benefits of rendering your app in a PHP runtime are:

  • Access to your application's session & state, which you normally don't if your SPA is consuming a stateless api
  • Reduced infrastructure complexity because you don't need to maintain a node server

If you're building a SPA that connects to an external API, and the PHP runtime doesn't provide any extra value, you're probably better off using a battle tested solution like Next.js or Nuxt.js.

As a final disclaimer, judging by the amount—well, lack—of people blogging about rendering JavaScript applications in PHP, this whole setup is uncharted territory. There may be more unknown caveats lurking around the corner.

If you're still sure you want to keep going, please continue!

Installation

You can install the package via composer:

composer require spatie/server-side-rendering

Usage

Your JavaScript app's architecture

This guide assumes you already know how to build a server-rendered application. If you're looking for reading material on the subject, Vue.js has a very comprehensive guide on SSR. It's Vue-specific, but the concepts also translate to other frameworks like React.

Engines

An engine executes a JS script on the server. This library ships with two engines: a V8 engine which wraps some V8Js calls, so you'll need to install a PHP extension for this one, and a Node engine which builds a node script at runtime and executes it in a new process. An engine can run a script, or an array of multiple scripts.

The V8 engine is a lightweight wrapper around the V8Js class. You'll need to install the v8js extension to use this engine.

The Node engine writes a temporary file with the necessary scripts to render your app, and executes it in a node.js process. You'll need to have node.js installed to use this engine.

Rendering options

You can chain any amount of options before rendering the app to control how everything's going to be displayed.

echo $renderer
    ->disabled($disabled)
    ->context('user', $user)
    ->entry(__DIR__.'/../../public/js/app-server.js')
    ->render();

enabled(bool $enabled = true): $this

Enables or disables server side rendering. When disabled, the client script and the fallback html will be rendered instead.

debug(bool $debug = true): $this

When debug is enabled, JavaScript errors will cause a php exception to throw. Without debug mode, the client script and the fallback html will be rendered instead so the app can be rendered from a clean slate.

entry(string $entry): $this

The path to your server script. The contents of this script will be run in the engine.

context($context, $value = null): $this

Context is passed to the server script in the context variable. This is useful for hydrating your application's state. Context can contain anything that json-serializable.

echo $renderer
    ->entry(__DIR__.'/../../public/js/app-server.js')
    ->context('user', ['name' => 'Sebastian'])
    ->render();
// app-server.js

store.user = context.user // { name: 'Sebastian' }

// Render the app...

Context can be passed as key & value parameters, or as an array.

$renderer->context('user', ['name' => 'Sebastian']);
$renderer->context(['user' => ['name' => 'Sebastian']]);

env($env, $value = null): $this

Env variables are placed in process.env when the server script is executed. Env variables must be primitive values like numbers, strings or booleans.

$renderer->env('NODE_ENV', 'production');
$renderer->env(['NODE_ENV' => 'production']);

fallback(string $fallback): $this

Sets the fallback html for when server side rendering fails or is disabled. You can use this to render a container for the client script to render the fresh app in.

$renderer->fallback('<div id="app"></div>');

resolveEntryWith(callable $resolver): $this

Add a callback to transform the entry when it gets resolved. It's useful to do this when creating the renderer so you don't have to deal with complex paths in your views.

echo $renderer
    ->resolveEntryWith(function (string $entry): string {
        return __DIR__."/../../public/js/{$entry}-server.js";
    })
    ->entry('app')
    ->render();

Testing

composer test

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you've found a bug regarding security please mail [email protected] instead of using the issue tracker.

Postcardware

You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.

Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.

We publish all received postcards on our company website.

Credits

License

The MIT License (MIT). Please see License File for more information.

server-side-rendering's People

Contributors

adrianmrn avatar alexvanderbist avatar edbizarro avatar freekmurze avatar jdreesen avatar kal-aster avatar nielsvanpach avatar patinthehat avatar reasno avatar robinvdvleuten avatar sebastiandedeyne avatar thecaliskan avatar timrspratt avatar vmitchell85 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

server-side-rendering's Issues

Not working

I followed your all instructions, but still not able to run example.

  1. App.vue
<template>
  <div>SSR</div>
</template>`
  1. Welcome.blade.php
<html>
    <head>
        <title>My server side rendered app</title>
        <script defer src="{{ mix('js/app-client.js') }}"></script>
    </head>
    <body>
       <div id="app">
         {!! ssr('js/app-server.js')->render() !!}
        </div>
    </body>
</html>
  1. webpack.mix.js
mix
    .js('resources/js/app-client.js', 'public/js')
    .js('resources/js/app-server.js', 'public/js')
    .vue()
    .sass('resources/sass/app.scss', 'public/css');
  1. app-client.js
import app from './app';

app.$mount('#app');
  1. app-server.js
import app from './app';
import renderVueComponentToString from 'vue-server-renderer/basic';

renderVueComponentToString(app, (err, html) => {
    if (err) {
        throw new Error(err);
    }
    dispatch(html);
});

6.app.js

import Vue from 'vue';
import App from './components/App';


export default new Vue({
    render: h => h(App)
});

let me know what else needed.

View source result:
Screenshot from 2023-07-20 19-52-39

On browser component showing correctly.

Issue with Node as an engine

I know the readme says that knowledge of SSR is assumed, and so far I have only really worked with SSR in a Ruby on Rails & React context, which worked pretty much out of the box. So my knowledge is shallow and I hope I'm not asking an unnecessary question here...

I've chosen Node as an engine, and if I call the renderer from a Twig template there is no error. But there is also nothing being rendered.

When I dump the result of $result = $this->engine->run($serverScript); it's string(0) "".

I tried putting several different contents into my JS file, also working with npm packages that explicitly support SSR, but the result is always the same. So I'm wondering if something should be rendered in my case, or if there are additional steps necessary that I'm missing.

Data hydration between node process and client

First of all, great job! I would like to ask is there any way to hydrate data between Vue App and PHP environment ?
Now we can pass context and environment data to application, so we can get data from PHP to VueJS app and this is ok.
What in case when I'm getting data from serverPrefetch (ssr hook) and I want to pass them to the client - in that case we should share our context from node process to PHP. When it comes to node environment only there is no issue, but is it possible to do this together with PHP ?

EDIT:
I have done something like this. It does not look like much, but it works...

Is there better way ?

if (typeof dispatch !== 'undefined') {
    renderToString(app, (err, html) => {
        if (err) {
            throw new Error(err)
        }
        html = `
        <script type="text/javascript">
        var __CONTEXT__ = ${JSON.stringify(app.$store.state)};
        </script>
        ${html}`
        // eslint-disable-next-line no-undef
        dispatch(html)
    })
}

Performances question

I'm trying to improve performances by this library and v8js, there isn't any section to explain which engine is better?

Also I'm wonder should I use V8js snapshot? is there any plan to add it to this library?

Error when "this" is not defined

PR #49 seems to have introduced a bug when "this" is not defined.

Also for reference, I am compiling my code with Vite instead of Webpack.

Here is the error:

Error Output:
================
file://storage/app/ssr/816a2e2ab307342d248eac819f1c3211.js:3
};(function () { if (this.process != null) { return; } this.process = { env: {}, argv: [] }; }).call(null);process.env.NODE_ENV = "production";process.env.VUE_ENV = "server";var context = {"url":"\/"};function ih(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Wc={exports:{}},xo={},Qc={exports:{}},I={};/**
                          ^

TypeError: Cannot read properties of null (reading 'process')
    at file://storage/app/ssr/816a2e2ab307342d248eac819f1c3211.js:3:27
    at file://storage/app/ssr/816a2e2ab307342d248eac819f1c3211.js:3:97
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:323:24)
    at async loadESM (node:internal/process/esm_loader:28:7)
    at async handleMainPromise (node:internal/modules/run_main:120:12)

Node.js v21.6.0.

I was able to fix this error by making the following change to /src/Renderer.php on Line 191:

'(function () { if (!this || this.process != null) { return; } this.process = { env: {}, argv: [] }; }).call(null)',

Basically, return early if !this

The command "node /var/www/tmp/c4e446ba25160cc5639395f94fb99626.js" failed. Exit Code: 1(General error)

I faced an issue after upgrading from Ubuntu 16 to 22 and upgrading the Node version from v14 to v20. The issue persists despite keeping other things the same on the old machine running Ubuntu 16. I had PHP 7.0 installed, and I also tried using PHP 7.2, but both versions gave the same error.

Also, the Vue files were generated and stored in the /var/www/tmp directory with permissions set to 744 for www-data:www-data.

The configuration of my old system was as follows:
OS: Ubuntu 16
PHP: 7.0
Node: 14

The current system configuration is:
OS: Ubuntu 16
PHP: 7.0 (tried with PHP 7.2, got the same problem)
Node: 20 (tried with 14 as well, got the same problem)

I have tried upgrading the package spatie/server-side-rendering 0f 0.2.6 to 0.3.2. But still got the same error.

My Error is:

The command "node /var/www/tmp/c4e446ba25160cc5639395f94fb99626.js" failed. Exit Code: 1(General error) Working directory: /var/www/v3 Output: ================ "

**** Here load My HTML part without CSS *****
****** After that given JS error******

" Error Output: ================ /var/www/tmp/c4e446ba25160cc5639395f94fb99626.js:3 };var process = process || { env: {} };process.env.NODE_ENV = "production";process.env.VUE_ENV = "server";var context = {"showAllRequisitions":true,"createRequisition":true,"url":"/requisitions","navOptions":[{"text":"Home","url":"/dashboard","icon":"mdi-view-dashboard-outline"}, ...................

My goal is to upgrade the OS and keep the existing system running as it is. Without changing the code, I will upgrade the package version as per the dependencies if required.

V8js "No module loader" and return Array

Hi,

V8js return V8Js::compileString():615: No module loader, i am change file

public function __construct(V8Js $v8)

// V8.php 

public function __construct(V8Js $v8)
{
      $this->v8 = $v8;
      
      $this->v8->setModuleLoader(function ($module) {
          switch ($module) {
              case '@vue/compiler-core':
                  return File::get(base_path('node_modules/@vue/compiler-core/dist/compiler-core.cjs.prod.js'));

              case '@vue/runtime-core':
                  return File::get(base_path('node_modules/@vue/runtime-core/dist/runtime-core.cjs.prod.js'));

              case '@vue/reactivity':
                  return File::get(base_path('node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js'));

              case '@vue/compiler-ssr':
                  return File::get(base_path('node_modules/@vue/compiler-ssr/dist/compiler-ssr.cjs.js'));

              case '@vue/shared':
                  return File::get(base_path('node_modules/@vue/shared/dist/shared.cjs.prod.js'));

              case '@vue/compiler-dom':
                  return File::get(base_path('node_modules/@vue/compiler-dom/dist/compiler-dom.cjs.prod.js'));

              case '@vue/runtime-dom':
                  return File::get(base_path('node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.prod.js'));

              case 'vue':
                  return File::get(base_path('node_modules/vue/dist/vue.cjs.prod.js'));

              case 'vue-router':
                  return File::get(base_path('node_modules/vue-router/dist/vue-router.cjs.prod.js'));

              case '@vue/server-renderer':
                  return File::get(base_path('node_modules/@vue/server-renderer/dist/server-renderer.cjs.prod.js'));

          }
      });
}

now ssr()->render() return array

ErrorException
Array to string conversion

i am change to ssr()->render()[0] it work, but is ok?

Why is this happening, is this the expected result?

Usage with a Webpack Dev Server

Hi,

Has anyone managed to get this package working using Webpack Encore? I'm having issues with the dev-server as the file isn't technically created, but directly served from memory at the given URL instead. For example http://localhost:8080/build/server.js. In production it would work because you generate the files so they have a physical path on the server.

My question is has anyone got this working using a webpack dev server?

Thanks

Not working in laravel5.6

I followed your all instructions and its giving me error Uncaught SyntaxError: Unexpected token '<' (at app-client.js:1:1) .If I comment all of the code its still giving me error. Another query is how should look like my app.blade.php file ? Where to put main id like app id ?

react with nextjs for async api calls

Has anybody used next.js to render react application to string? Current approach does not work for me, because react renderToString method ignores all "fetch" requests to external apis and nextjs handles it properly. Though nextjs only works with expressjs and it does not have proper documentation for advanced usage.

[Question] First byte drastic performance decrease

In e-commerce site that heavily uses Vue as a javascript framework we've faced the problem of generating full source for SEO purposes.

We have decided to try and go with SSR.

Unfortunately after enabling SSR performance of website drastically decreased.
Without SSR we have ~400ms response time (first byte) for a website that has to analyze ~250k offers, filters, and other stuff. Once we enable SSR first byte jumps to around ~1.2s. Which unfortunately is not acceptable for the SEO purposes.

We use Node engine, which basically means that on each request new node instance is started.

We can of course try to apply caching, and not remove generated .js file for some time and solve this issue for uses, but this won't solve the problem for robots. Because if a page is not cached, then first byte will still be around ~1.2s

Any ideas what might be done to improve performance of SSR?

SyntaxError: Unexpected identifier

use Spatie\Ssr\Renderer;
use Spatie\Ssr\Engines\V8;

$renderer = new Renderer(new V8(new V8Js));

echo $renderer->debug()->entry($_SERVER['DOCUMENT_ROOT'] . '/../resources/js/catering/app.js')->render();

But got error on library code of method dispatchScript:
vendor\spatie\server-side-rendering\src\Engines\V8.php(25): V8Js->executeString('var dispatch = ...')

It doesn't even reach my script...

VDOM mismatch

Thanks for the package... it is working and I get server rendered content, but I am having trouble with white space. I am rendering a vue.js application server side. The server side render works well and prior to hydration, my content is looking good... but.... I hit random issues

The following forces a full client re-render of the site when trying to mount client side...

          <div class="mini-promo-text">
            <span class="title">COMMERCIAL</span>
            <hr class="my-0">
            <span class="subject">Built tough</span>
          </div>

But if I remove the <hr> tag, then the $mount completes with no problem...

          <div class="mini-promo-text">
            <span class="title">COMMERCIAL</span>
            <span class="subject">Built tough</span>
          </div>

image

See screenshot above from browser console where it indentifies mismatch.

I think my workflow is ok, because I have this working on other pages on my site with no problem and I get full hydration client side....

My webpack config is the following:

const bfa_server = {
    target: 'node',
    entry: './src/js/bfa-server.js',
    output: {
        filename: './js/bfa-server.min.js',
        libraryTarget: 'commonjs2',
        path: path.resolve(__dirname, 'dist'),
        publicPath: 'design/themes/bfa/dist/',
    },
    module: {
        rules: [{
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.m?js$/,
            exclude: /node_modules/,
            use: 'babel-loader'
        }, {
            test: /\.(gif|png|jpg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/'
                }
            }]
        }, {
            test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'fonts/'
                }
            }]
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                use: ['css-loader']
            })
        }, {
            test: /\.scss$/,
            use: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: ['css-loader', 'postcss-loader', 'sass-loader']
            })
        }]
    },
    plugins: [
        new ExtractTextPlugin('./css/bfa.min.css'),
        new VueLoaderPlugin({
            compilerOptions: {
                whitespace: 'condense'
            },
            optimizeSSR: true
        })
    ],
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js' // Use the full build
        }
    }
};

const bfa_client = {
    entry: './src/js/bfa-client.js',
    output: {
        filename: './js/bfa-client.min.js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: 'design/themes/bfa/dist/',
    },
    module: {
        rules: [{
            test: /\.vue$/,
            loader: 'vue-loader'
        }, {
            test: /\.m?js$/,
            exclude: /node_modules/,
            use: 'babel-loader'
        }, {
            test: /\.(gif|png|jpg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'images/'
                }
            }]
        }, {
            test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [{
                loader: 'file-loader',
                options: {
                    name: '[name].[ext]',
                    outputPath: 'fonts/'
                }
            }]
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                use: ['css-loader']
            })
        }, {
            test: /\.scss$/,
            use: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: ['css-loader', 'postcss-loader', 'sass-loader']
            })
        }]
    },
    plugins: [
        new ExtractTextPlugin('./css/bfa.min.css'),
        new VueLoaderPlugin({
            compilerOptions: {
                whitespace: 'condense'
            },
            optimizeSSR: false
        }),
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ],
    resolve: {
        alias: {
            jquery: "jquery/src/jquery",
            'vue$': 'vue/dist/vue.esm.js' // Use the full build
        }
    }
};

module.exports = [bfa_client, bfa_server];

Issue with access to global process variable when using Webpack

Hello,

I've stumbled upon an issue with when using Node engine with server-side bundle generated by Webpack 4. In the javascript generation process in the Renderer class, there's following line added to the file:

var process = process || { env: {} };

In my case, the process global resolves to undefined, which causes my script to malfunction - axios won't recognize Node environment. Axios can be thus used to reproduce this issue as it uses this condition to determine Node env typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]'.

To fix this, I've replaced the global with require('process') which solved the issue for my instance.

I wanted to ask if this is by design and/or this is some issue with the webpack bundle or it is, indeed, a bug.

Node versions: 12.4.0 / 6.16.0 - both same result
Webpack version: 4.34.0 - target set to 'node', transpilation via babel

Thanks for feedback.
Daniel

What's the point of the library?

Sorry if my question is stupid but I couldn't think of any real use-case for the library.

If your SPA is already decoupled (or already designed as SSR friendly from the beginning) to have the app-server.js (or server-entry.js), why don't you directly use NodeJS to act as the SSR server than using a PHP wrapper. It's just introducing another layer of PHP code to maintain.

Otherwise, the frontend developer still needs quite a lot of effort to decouple the SPA to make it SSR friendly, then my previous "if" statement should be true => use NodeJS as SSR server will be better in performance & complexity.

Please enlighten me.
Thanks

Allow context to be sent back up with `dispatch`

The magic dispatch function should accept a second argument to send up context.

Implementation idea: dispatch returns a JSON-serialized object, with the html in one key, and context in another.

{
  "html": "....",
  "context": {
    "meta": "<meta ...>"
  }
}

This is a breaking change, and would change the return of Engine::run() from a string to an array or custom object.

See spatie/laravel-server-side-rendering#18 for a real-world use case.

Process 132 exit code.

Any idea why always get 132 exit code following coding? This work fine on window platform, but not on Linux platform.

$script = 'var d = 1;console.log(d);';
file_put_contents($tempFilePath, $script);
$process = new \Symfony\Component\Process\Process([$this->nodePath, $tempFilePath ], null, null, null, null);
$process->mustRun()->getOutput();

Root element does not get id attribute

When I have a successful ssr response:

<div data-server-rendered="true">

You still find a console error complaining about not finding #app.

Isn't vue-server-renderer supposed to add this on the root element as well?

<div id="app" data-server-rendered="true">

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.