Giter Club home page Giter Club logo

coren's Introduction

Coren

npm Version Build Status

React Pluggable Serverside Render

Is serverside render a big headache for your Single Page App?

say you need head title, description, jsonld, og...

perhaps fetch data from db, then render redux preloadedState

so many things need to be rendered in HTML

How about we use more flexible way to solve it?

What if we let component define what they need in static method?

What if we could fetch database in component?

Coren provide you pluggable, flexible way to render your html

Table Of Content

Features

Installation

$ npm install coren --save

Simple Example

Here's some simple example components using collector

Head

we'll insert what component want as <title> to HTML

React

@collector()
export default class User extends Component {

  // Put `user ${props.userId}` title tag to HTML
  static defineHead(props) {
    return {
      title: `user ${props.userId}`
    }
  }

  render() {
    return <div>
      ...
    </div>;
  }
}

How HeadCollector insert head

In MultiRoutesRenderer, HeadCollector insert head using appendToHead

class HeadCollector {
  constructor() {
    this.heads = [];
  }

  // ...

  componentDidConstruct(id, component, props) {
    this.heads.push(component.defineHead(props));
  }

  getFirstHead() {
    return this.heads[0] || {};
  }

  // ...

  appendToHead($head) {
    const {title, description} = this.getFirstHead();
    $head.append(`<title>${title}</title>`);
    $head.append(`<meta name="description" content="${description}">`);
  }
}

Serverside

const app = new App({
  path: path.resolve(__dirname, 'path/to/app')
});

// HeadCollector get data from `defineHead()`
app.registerCollector("head", new HeadCollector());

// ssr
const ssr = new MultiRoutesRenderer({app});
ssr.renderToString()
.then(result => {
  console.log(result);
  // [{route: "/", html: "<html><head>user 1</head>...</html>"}]
})
.catch(err => console.log(err));

Redux preloaded state

How Coren render __PRELOADED_STATE__

React

@collector()
export default class Product extends Component {

  // Fetch data first
  // then, during serverside render, put `window.__PRELOADED_STATE__=${state}` to HTML
  static definePreloadedState({db}) {
    return db.fetch('products').exec()
    .then(data => ({products: data}));
  }

  render() {
    return <div>
      ...
    </div>;
  }
}

Collector

In ReduxCollector, we push promise we got from definePreloadedState

then, we wait all promises done at appWillRender

Last, we wrap your app with react-redux provider, and get state from store.getState(), append the state to head

class ReduxCollector {
  // ...
  componentDidImport(id, component) {
    const promise = component.definePreloadedState(this.componentProps);
    this.queries.push(promise);
  }

  appWillRender() {
    return Promise.map(this.queries,
      state => Object.assign(this.initialState, state));
  }

  wrapElement(appElement) {
    const store = createStore(this.reducers, this.initialState);
    const wrapedElements = react.createElement(Provider, {store}, appElement);
    this.state = store.getState();
    return wrapedElements;
  }

  appendToHead($head) {
    $head.append(`<script>
      window.__PRELOADED_STATE__ = ${JSON.stringify(this.state)}
      </script>`);
  }
}

Serverside

const app = new App({
  path: path.resolve(__dirname, 'path/to/app')
});

// ReduxCollector get initialState from `definePreloadedState()`
app.registerCollector("redux", new ReduxCollector({
  // componentProps will be passed to 
  componentProps: {
    db
  },
  // reducer of your app
  reducers: reducer
}));

// ssr
const ssr = new MultiRoutesRenderer({app});
ssr.renderToString()
.then(result => {
  console.log(result);
  // [{route: "/", html: "<html><body>window.__PRELOADED_STATE__={...}</body>></html>"}]
})
.catch(err => console.log(err));

Concepts

Define

Coren render html base on data gotten from Component.

so, where do Component write what they could provide for Serverside render?

Component should use @collector decorator outside, and use static method, prefixed with define. In this case, @collector could return data back to server during right lifecycle.

Lifecycle Hook

We metioned lifecycle above. How does this work?

let us take a look at collector decorator

export default function() {
  return WrappedComponent => {
    const uniqId = shortid.generate();
    /*
      trigger componentDidImport lifecycle here
      notify collectors
    */
    hook.componentDidImport(uniqId, WrappedComponent);
    class Hoc extends React.Component {
      constructor(props) {
        super(props);
        /*
          trigger componentDidConstruct lifecycle here
          pass props to collectors 
        */
        hook.componentDidConstruct(uniqId, WrappedComponent, props);
      }

      render() {
        return <WrappedComponent {...this.props} />;
      }
    }
    return hoistStatic(Hoc, WrappedComponent);
  };
}

During serverside render, two lifecycle will be triggered

  • componentDidImport(id, component): called when component imported
  • componentDidConstruct(id, component, props): called when component constructed

Why these two methods?

In React-router, only component matched with route will be rendered. So, component rendered will trigger both methods, on the other hand, component not rendered will trigger only componentDidImport. It will help you put right data in your HTML.

For Example, we should only put the head tags return from first constructed component. Components that didn't trigger componentDidConstruct should not be considered.

Collector

What is a Collector?

Collector collect data from define methods, collector can choose which lifecycle it want to call define method.

For example, we take a look at HeadCollector, HeadCollector call defineHead(props) in componentDidConstruct, it get {title, description}, then push to heads array.

when serverside renderer call appendToHead, HeadCollector push the first head it got from component to $head

class HeadCollector {
  constructor() {
    this.heads = [];
  }

  // ...

  componentDidConstruct(id, component, props) {
    this.heads.push(component.defineHead(props));
  }

  getFirstHead() {
    return this.heads[0] || {};
  }

  // ...

  appendToHead($head) {
    const {title, description} = this.getFirstHead();
    $head.append(`<title>${title}</title>`);
    $head.append(`<meta name="description" content="${description}">`);
  }
}

App

App represent your react application. developer use App to register collector

// create App with path to your React App entry file
const app = new App({
  path: path.resolve(__dirname, 'path/to/app')
});

// register collector
app.registerCollector("head", new HeadCollector());

App controlls lifecycle of all registered collectors.

Serverside renderer will call App's lifecycle method at certain time, to get the desired result it want.

Serverside Renderer

The main purpose of Serverside Renderer is to create HTML. By calling App to controll lifecycle of collectors, make sure collectors get the result they want.

Collector Lifecycle

In MultiRoutesRenderer, every collector will go through same phases:

  1. componentDidImport(id, component): when component imported
  2. appWillRender: do some async work here if you want to make some api call before render
  3. routeWillRender: when rendering multiple routes, appWillRender will be called every time the route match with your component and trigger render, so is every method below
  4. wrapElement: you can wrap your app reactElement if you need a provider outside
  5. (app renderToString) => ssrRenderer will call ReactDom.renderToString
  6. componentDidConstruct(id, component, props): called when component was constructed
  7. appendToHead($cheerio('head')): append any html to head
  8. appendToBody($cheerio('body')): append any html to body

API

App

constructor({path: String})

  • path: path to your React app entry file
const app = new App({
  path: path.resolve(__dirname, 'path/to/app')
});

registerCollector(key: String, collector: Collector)

  • key: you can directly access to collector by key
app.getCollector("head")
// return headCollector
  • collector: the collector you want to register
app.registerCollector("head", new HeadCollector());

Collector

ifEnter(component): Boolean

app will use ifEnter to determine whether call this collector or not

componentDidImport(id, component): void

called when component imported, when component imported, a unique id attached to it, so you'll know where this component appeared before or not in componentDidConstruct.

componentDidConstruct(id: String, component: ReactComponent, props: Object): void

called when component was constructed

appWillRender(): Promise

Because we react wont wait for your async code during import. So a better way to use async related task is to push your promise to an array, wait for them in appWillRender.

Take reduxCollector for example:

// /src/reduxCollector
componentDidImport(id, component) {
  const promise = component.definePreloadedState(this.componentProps);
  this.queries.push(promise);
}

appWillRender() {
  return Promise.map(this.queries,
    state => Object.assign(this.initialState, state));
}

routeWillRender(): void

In MultiRoutesRenderer, you'll have multiple routes to be rendered, so you need a hook to tell your collector when a route is going to be rendered. You can do some reset variable things here.

Take HeadCollector for example, we make sure we collect fresh head from component constructed.

componentDidConstruct(id, component, props) {
  this.heads.push(component.defineHead(props));
}

routeWillRender() {
  // empty heads
  this.heads = [];
}

wrapElement(ReactElement): ReactElement

Some module require developer wrap ReactElement with provider in serverside render.

Take reduxCollector for example, we wrap ReactElement with react-redux provider.

wrapElement(appElement) {
  const store = createStore(this.reducers, this.initialState);
  const wrapedElements = react.createElement(Provider, {store}, appElement);
  this.state = store.getState();
  return wrapedElements;
}

appendToHead($head: cheerio)

append any html to head

appendToBody($body: cheerio)

append any html to body

Usage

Getting Started

  1. npm install coren --save
  2. use @collector in your component
import collector from 'coren/lib/client/collectorHoc';

@collector()
export default class UserList extends Component {
  // ...
  render() {
    ...
  }
}
  1. write define method.
@collector()
export default class UserList extends Component {
  static defineHead() {
    return {
      title: "user list",
      description: "user list"
    };
  }

  static defineRoutes({Url}) {
    return new Url('/users');
  }

  static definePreloadedState({db}) {
    return db.users.find().execAsync()
    .then(list => ({
      users: {
        list,
        fetched: true,
        isFetching: false,
        error: false
      }
    }));
  }
}
  1. serverside render serverside render with app and multiRoutesRenderer
const db = mongodb;
const app = new App({
  path: path.resolve(__dirname, 'path/to/app')
});

// register collectors
app.registerCollector("head", new HeadCollector());
app.registerCollector("routes", new RoutesCollector({
  componentProps: {
    db
  }
}));
app.registerCollector("redux", new ImmutableReduxCollector({
  componentProps: {
    db
  },
  reducers: reducer
}));

// ssr
const ssr = new MultiRoutesRenderer({
  app,
  // bundle path will be append to html body
  js: ["/bundle.js"]
});

// get the array of html result
ssr.renderToString()
.then(results => {
  return Promise.all(results.map(result => {
    // throw HTML to anywhere you want
    // cached to web server, cache server
    // write to s3, cdn
  }));
})
.catch(err => console.log(err));

How to create own Collector

Write your own class, implement methods in Collector.

Take a look at built-in collector for reference.

https://github.com/Canner/coren/tree/master/server/collectors

Example

Here's a example repo using this module. https://github.com/Canner/coren-example

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.