阿里云主机折上折
  • 微信号
Current Site:Index > Design pattern considerations in server-side rendering (SSR)

Design pattern considerations in server-side rendering (SSR)

Author:Chuan Chen 阅读数:27839人阅读 分类: JavaScript

Design Pattern Considerations in Server-Side Rendering (SSR)

Server-side rendering plays a crucial role in modern web development, directly impacting first-screen performance, SEO, and user experience. When implementing SSR, the judicious application of design patterns can effectively address core issues such as data flow management, component reuse, and state synchronization.

Application of Factory Pattern in Component Creation

Server-side rendering environments require the creation of numerous component instances. The factory pattern can unify component creation logic, particularly excelling when distinguishing between client-side and server-side components based on the runtime environment.

class ComponentFactory {
  static createComponent(type, props) {
    if (typeof window === 'undefined') {
      return new ServerComponent(props);
    } else {
      return new ClientComponent(props);
    }
  }
}

// Usage example
const component = ComponentFactory.createComponent('button', {
  text: 'Click me'
});

This implementation ensures different component implementations are used on the server and client while maintaining a consistent interface. For isomorphic components, the factory method can be further extended:

class UniversalComponentFactory {
  static createComponent(type) {
    const components = {
      header: UniversalHeader,
      footer: UniversalFooter
    };
    
    if (!components[type]) {
      throw new Error(`Unknown component type: ${type}`);
    }
    
    return components[type];
  }
}

Singleton Pattern for Managing Global State

In SSR applications, it is essential to ensure the server and client share the same application state. The singleton pattern prevents duplicate state initialization and maintains data consistency.

class AppState {
  constructor() {
    if (AppState.instance) {
      return AppState.instance;
    }
    
    this.user = null;
    this.theme = 'light';
    AppState.instance = this;
  }
  
  static getInstance() {
    if (!AppState.instance) {
      AppState.instance = new AppState();
    }
    return AppState.instance;
  }
}

// Server-side usage
const serverState = new AppState();
serverState.user = { id: 1, name: 'John' };

// Client-side retrieval of the same instance
const clientState = AppState.getInstance();
console.log(clientState.user); // { id: 1, name: 'John' }

For more complex scenarios, consider combining serialization techniques:

class SerializableState {
  // ...similar implementation as above...
  
  serialize() {
    return JSON.stringify(this);
  }
  
  static deserialize(json) {
    const data = JSON.parse(json);
    const instance = new SerializableState();
    Object.assign(instance, data);
    return instance;
  }
}

// Server-side
const state = new SerializableState();
const serialized = state.serialize();

// Embed serialized data in HTML
// Client-side
const restoredState = SerializableState.deserialize(window.__INITIAL_STATE__);

Strategy Pattern for Handling Rendering Differences

Different pages may require different rendering strategies. The strategy pattern allows dynamic switching of rendering logic without modifying the main process code.

class RenderStrategy {
  render() {
    throw new Error('Method not implemented');
  }
}

class SSGStrategy extends RenderStrategy {
  render(component) {
    // Static generation logic
    return generateStaticHTML(component);
  }
}

class SSRStrategy extends RenderStrategy {
  render(component) {
    // Server-side rendering logic
    return renderToString(component);
  }
}

class RenderContext {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  executeRender(component) {
    return this.strategy.render(component);
  }
}

// Usage example
const context = new RenderContext(new SSRStrategy());
const html = context.executeRender(App);

// Switch to static generation
context.setStrategy(new SSGStrategy());

Observer Pattern for Data Prefetching

SSR requires prefetching data before rendering. The observer pattern elegantly handles the dependency between data preparation and rendering.

class DataObserver {
  constructor() {
    this.observers = [];
    this.data = null;
  }
  
  subscribe(fn) {
    this.observers.push(fn);
  }
  
  unsubscribe(fn) {
    this.observers = this.observers.filter(subscriber => subscriber !== fn);
  }
  
  async fetchData() {
    this.data = await fetch('/api/data');
    this.notify();
  }
  
  notify() {
    this.observers.forEach(observer => observer(this.data));
  }
}

// Component usage
const dataObserver = new DataObserver();

class MyComponent {
  constructor() {
    dataObserver.subscribe(this.update.bind(this));
  }
  
  update(data) {
    this.render(data);
  }
  
  render(data) {
    // Render using data
  }
}

// Initiate data fetching
dataObserver.fetchData();

Proxy Pattern for API Requests

In SSR environments, API request methods may differ between server and client. The proxy pattern can unify the interface.

class ApiClient {
  request(endpoint) {
    // Base request implementation
  }
}

class ServerApiProxy extends ApiClient {
  request(endpoint) {
    // Server-specific logic, such as adding headers
    return super.request(endpoint);
  }
}

class ClientApiProxy extends ApiClient {
  request(endpoint) {
    // Client-specific logic, such as handling credentials
    return fetch(endpoint);
  }
}

// Create proxy based on environment
function createApiClient() {
  if (typeof window === 'undefined') {
    return new ServerApiProxy();
  } else {
    return new ClientApiProxy();
  }
}

const api = createApiClient();
api.request('/data');

Decorator Pattern for Enhancing Component Functionality

In SSR scenarios, components may require additional server-side capabilities. The decorator pattern can dynamically add these features.

function withServerData(WrappedComponent) {
  return class extends React.Component {
    static async getInitialProps(ctx) {
      const data = await fetchServerData();
      return { ...data, ...(WrappedComponent.getInitialProps ? await WrappedComponent.getInitialProps(ctx) : {}) };
    }
    
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

// Using the decorator
class MyComponent extends React.Component {
  // Component implementation
}

export default withServerData(MyComponent);

For more complex decoration scenarios, multiple decorators can be combined:

function withLogger(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log('Component mounted', WrappedComponent.name);
    }
    
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

// Combining decorators
const EnhancedComponent = withLogger(withServerData(MyComponent));

Template Method Pattern for Unified Rendering Flow

SSR typically follows a fixed processing flow. The template method pattern ensures step consistency.

abstract class RenderPipeline {
  async render() {
    await this.prepareData();
    const content = await this.renderContent();
    const html = this.wrapDocument(content);
    return html;
  }
  
  abstract prepareData();
  abstract renderContent();
  
  wrapDocument(content) {
    return `
      <!DOCTYPE html>
      <html>
        <head>
          <title>My App</title>
        </head>
        <body>
          <div id="app">${content}</div>
          <script src="/client.js"></script>
        </body>
      </html>
    `;
  }
}

class ProductPageRenderer extends RenderPipeline {
  async prepareData() {
    this.data = await fetchProductData();
  }
  
  async renderContent() {
    return renderToString(<ProductPage data={this.data} />);
  }
}

// Usage
const renderer = new ProductPageRenderer();
const html = await renderer.render();

State Pattern for Managing Rendering Lifecycle

The SSR process involves multiple state transitions. The state pattern can clearly manage these states.

class RenderState {
  constructor(context) {
    this.context = context;
  }
  
  start() {
    throw new Error('Method not implemented');
  }
  
  complete() {
    throw new Error('Method not implemented');
  }
  
  error() {
    throw new Error('Method not implemented');
  }
}

class InitialState extends RenderState {
  start() {
    console.log('Starting render process');
    this.context.setState(new DataFetchingState(this.context));
  }
}

class DataFetchingState extends RenderState {
  async start() {
    try {
      this.context.data = await fetchData();
      this.context.setState(new RenderingState(this.context));
      this.context.currentState.start();
    } catch (err) {
      this.context.setState(new ErrorState(this.context, err));
    }
  }
}

class RenderingState extends RenderState {
  start() {
    this.context.html = renderToString(this.context.app);
    this.context.setState(new CompletedState(this.context));
  }
}

class RenderContext {
  constructor() {
    this.setState(new InitialState(this));
    this.data = null;
    this.html = null;
  }
  
  setState(state) {
    this.currentState = state;
  }
  
  async render() {
    await this.currentState.start();
  }
}

// Usage
const context = new RenderContext();
await context.render();

Composite Pattern for Building Page Structure

Complex pages typically consist of multiple parts. The composite pattern can uniformly handle the relationship between the whole and its parts.

class PageComponent {
  constructor(name) {
    this.name = name;
    this.children = [];
  }
  
  add(component) {
    this.children.push(component);
  }
  
  async render() {
    const childrenHtml = await Promise.all(
      this.children.map(child => child.render())
    );
    return `
      <section class="${this.name}">
        ${childrenHtml.join('')}
      </section>
    `;
  }
}

class HeaderComponent {
  async render() {
    return '<header>Header Content</header>';
  }
}

class FooterComponent {
  async render() {
    return '<footer>Footer Content</footer>';
  }
}

// Building the page
const page = new PageComponent('app');
page.add(new HeaderComponent());
page.add(new PageComponent('main-content'));
page.add(new FooterComponent());

const html = await page.render();

Adapter Pattern for Integrating Different SSR Frameworks

When integrating multiple SSR solutions, the adapter pattern can provide a unified interface.

class NextJSAdapter {
  constructor(nextApp) {
    this.nextApp = nextApp;
  }
  
  async render(req, res) {
    return this.nextApp.render(req, res);
  }
}

class NuxtJSAdapter {
  constructor(nuxt) {
    this.nuxt = nuxt;
  }
  
  async render(req, res) {
    return this.nuxt.render(req, res);
  }
}

class SSRGateway {
  constructor(adapter) {
    this.adapter = adapter;
  }
  
  setAdapter(adapter) {
    this.adapter = adapter;
  }
  
  handleRequest(req, res) {
    return this.adapter.render(req, res);
  }
}

// Usage
const nextApp = require('next')(...);
const adapter = new NextJSAdapter(nextApp);
const gateway = new SSRGateway(adapter);

// Handling requests
server.use((req, res) => gateway.handleRequest(req, res));

Memento Pattern for Rendering Snapshots

During SSR, it may be necessary to save and restore rendering states. The memento pattern can manage these state snapshots.

class RendererMemento {
  constructor(state) {
    this.state = JSON.parse(JSON.stringify(state));
  }
  
  getState() {
    return this.state;
  }
}

class SSRRenderer {
  constructor() {
    this.state = {
      data: null,
      html: '',
      error: null
    };
  }
  
  createMemento() {
    return new RendererMemento(this.state);
  }
  
  restoreMemento(memento) {
    this.state = memento.getState();
  }
  
  async render() {
    try {
      const memento = this.createMemento();
      
      this.state.data = await fetchData();
      this.state.html = renderToString(<App data={this.state.data} />);
      
      return this.state;
    } catch (error) {
      this.state.error = error;
      throw error;
    }
  }
}

// Usage
const renderer = new SSRRenderer();

// Save state before rendering
const beforeRender = renderer.createMemento();

try {
  await renderer.render();
} catch (error) {
  // Restore state on error
  renderer.restoreMemento(beforeRender);
}

Chain of Responsibility Pattern for Rendering Middleware

The SSR process typically requires multiple processing steps. The chain of responsibility pattern can flexibly organize these steps.

class RenderMiddleware {
  constructor() {
    this.nextMiddleware = null;
  }
  
  setNext(middleware) {
    this.nextMiddleware = middleware;
    return middleware;
  }
  
  async process(request, response) {
    if (this.nextMiddleware) {
      return await this.nextMiddleware.process(request, response);
    }
    return null;
  }
}

class DataFetchingMiddleware extends RenderMiddleware {
  async process(request, response) {
    request.data = await fetchData(request.url);
    return super.process(request, response);
  }
}

class RenderingMiddleware extends RenderMiddleware {
  async process(request, response) {
    request.html = renderToString(<App data={request.data} />);
    return super.process(request, response);
  }
}

class ResponseMiddleware extends RenderMiddleware {
  async process(request, response) {
    response.send(request.html);
    return true;
  }
}

// Building the chain of responsibility
const dataMiddleware = new DataFetchingMiddleware();
const renderMiddleware = new RenderingMiddleware();
const responseMiddleware = new ResponseMiddleware();

dataMiddleware.setNext(renderMiddleware).setNext(responseMiddleware);

// Handling requests
server.use(async (req, res) => {
  await dataMiddleware.process(req, res);
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.