@artsy/stitch

中文文档
6.5.0发布于 2022/04/13MITTypeScriptCommonJS近 30 天下载 8189

Helps your Component and Template dependencies peacefully coexist

原始使用文档
文档目录8 节
  1. Overview
  2. Installation
  3. Example Scenarios / Who This Library is Aimed At
  4. Usage
  5. Full API
  6. Troubleshooting
  7. Development
  8. Commits and Deployments

@artsy/stitch

Build Status

Helps your Component and Template dependencies peacefully coexist

Overview

At Artsy we have a number of Node.js applications that have been in production for a while. Force, for example, was built when CoffeeScript and Jade made a lot of sense; CoffeeScript fixed many of the problems with JavaScript pre-ES6, and Jade made working with HTML much more elegant. As time progressed and new technologies became a thing these solutions starting feeling more burdensome to continue building features with and many of our developers longed to start using next-generation tools like React.js. But how? Short of a rebuild, which resource constraints often prohibit, merging old and new tech can be difficult, particularly on the view layer.

This library is an attempt to help those making incremental transitions away from templating solutions like Jade, EJS and numerous others, towards React (or other React-like libs). What it does is provide a flexible set of conventions for dealing with layout code typically seen in Express, but will work just about anywhere that raw HTML is sent down the wire to the client.

If you're using a templating library supported by consolidate.js, this library could be useful to you.

Installation

yarn install @artsy/stitch

Example Scenarios / Who This Library is Aimed At

  • Your UI is built in Jade/Pug or other layout based-engines, and you use "block" functionality. How to inject React components into each while maintaining backwards compatibility?
  • Alternatively, you use a templating library that relies on server-side includes. How to do the same?
  • The bulk of your app is built in Backbone, with each view backed by a Handlebars template. You'd still like to use your existing views / UI while incrementally migrating sections towards React. How to "render" each Backbone view inside of your React components -- or vice versa?
  • You've got a server-side-rendered, EJS-based app and you'd like to start taking advantage of React's isomorphic "universal" rendering. How to go about rendering React components on the server and then rehydrating them on the client?
  • You've got a new app built entirely in React but for one reason or another need to keep portions of the layout isolated from one another.
  • ...

Out of the box, Stitch aims for flexibility.

Usage

Table of Contents

(If you want to jump right in, see the full example project.)

Basic Example

In its most basic form, this library is a single function that accepts a path to your layout, some data, and returns a stitched-together string of rendered, raw html that can be sent to the client:

<div>
  {{title}}
</div>
const html = await stitch({
  layout: "templates/layout.handlebars",
  data: {
    title: "Hello!",
  },
})

console.log(html)

// => Outputs:
/*
<div>
  Hello!
</div>
*/

By adding "blocks" you can begin assembling more complex layouts. Blocks represent either a path to a template or a function / React class that returns a string:

// templates/layout.handlebars

<html>
  <head>
    <title>
      {{title}}
    </title>
  </head>
  <body
    {{{body}}}
  </body>
</html>
// index.js

const html = await stitch({
  layout: "templates/layout.handlebars",
  data: {
    title: "Hello World!",
  },
  blocks: {
    body: props => {
      return <h1>{props.title}</h1>
    },
  },
})

console.log(html)

// => Outputs:
/*
<html>
  <head>
    <title>Hello World!</title>
  </head>
  <body>
    <h1>
      Hello World!
    </h1>
  </body>
</html>
*/

You can add as many blocks as you need, which are accessible by key. Each block is evaluated and compiled, and then injected into the layout. Keep in mind that any field that accepts a path to a template can also accept a component (as either a Class or a stateless functional component), and template formats can be mixed and matched. This allows for a great amount of flexibility within seemingly incompatible view systems.

Express.js and Pug

NOTE! Each of the following use async / await, which is available by default in Node >= 7.6.0. If your environment doesn't yet support async / await see the Troubleshooting section below for an example that uses ES6 Promises.

// index.js

import express from "express"
import { stitch } from "@artsy/stitch"

import Body from "./components/Body"
import Footer from "./components/Footer"

const app = express()

app.get("/", async (req, res, next) => {
  try {
    const html = await stitch({
      layout: "templates/mainLayout.pug",
      data: {
        title: "Hello World!",
        description:
          "An example showing how to take a view structure based in .pug and interpolate with React",
      },
      blocks: {
        head: "templates/head.pug",
        body: Body,
        footer: Footer,
      },
    })

    res.send(html)
  } catch (error) {
    next(error)
  }
})

app.listen(3000, () => {
  console.log("Listening on port 3000.")
})
//- templates/mainLayout.pug

html
  head
    != head
  body
    != body

    footer
      != footer
//- templates/head.pug

title= title
meta( property='og:title', content= title )
meta( name='description', content= description )
// components/Body.js

export default function Body({ title, description }) {
  return (
    <div>
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  )
}
// components/Footer.js

export default function Footer() {
  return <h4>Hello footer!</h4>
}

Layouts and other complex UI configurations

Where this system really begins to shine is when there is a base layout that is extended by sub-layouts. For example, say you have an Express app that mounts a number of sub-apps, and each sub-app has its own layout that "extends" the main layout using blocks (or includes, etc):

//- templates/mainLayout.pug

html
  head
    include partials/head
    block head

  body
    include partials/body
    block body
//- apps/home/templates/homeLayout.pug

extends ../../../templates/mainLayout.pug

block head
  != head

block body
  if locals.isAuthenticated
    div
      | Logged-in

  != body
// apps/home/routes.js

...

const html = await stitch({
  basePath: __dirname,
  layout: 'templates/homeLayout.pug',
  data: {
    title: 'Hello how are you?',
    description: 'Views extending views extending views...'
  },

  // Can also define locals, which are passed down to components under
  // the `props.locals` key
  locals: {
    isAuthenticated: req.locals.userIsAuthenticated
  },
  blocks: {
    head: 'templates/head.pug',
    body: Body
  }
})

res.send(html)

Isomorpic (or "Universal") rendering

This is covered in more depth in the isomorphic-react-pug-webpack example but in short, since the data object is injected into the template before rendering takes place, making it available on the client is as easy as JSON.stringifying it:

// - templates/layout.pug

html
  body
    script.
      var __BOOTSTRAP__ = {JSON.stringify(data)}

    != app
const html = await stitch({
  layout: "templates/layout.pug",
  blocks: {
    app: App,
  },
  data: {
    name: "Z",
  },
})

res.send(html)
export default class App extends Component {
  componentDidMount () {
    console.log(`Hello ${this.props.name}, <App /> is mounted on the client.`)
  },

  render () {
    return (
      <div>
        Hello {this.props.name}
      </div>
    )
  }

}
// client.js

React.render(<App {...window.__BOOTSTRAP__} />)

Precompiling Templates

What to do if you have a bunch of old-school Backbone views that you don't want to throw out while moving over to React, but would still like to inject their markup into your React app for mounting later on the client? This library makes that easy:

// apps/login/index.js

...

const html = await stitch({
  layout: 'templates/loginLayout.pug',
  blocks: {
    app: App
  },
  templates: {
    login: 'templates/login.pug'
  }
})
// components/Body.js

import React from "react"
import Login from "./Login"

export default function App(props) {
  const {
    templates: { login },
  } = props

  return (
    <div>
      <Login template={login} />
    </div>
  )
}
// components/Login.js

import React, { Component } from 'react'
import LoginView from '../views/LoginView'

export default class Login extends Component {
  componentDidMount () {
    this.loginView = new LoginView()
    this.loginView.render()
  }

  componentWillUnmount () {
    this.loginView.remove()
  }

  render () {
    return (
      <div>
        <div dangerouslySetInnerHtml={{
          __html: this.props.template
        }}>
      </div>
    )
  }
}
// templates/login.handlebars

<div id="login">
  <button>
    Login
  </button>
</div>
// views/LoginView.js

var LoginView = Backbone.View.extend({
  el: '#login',

  events: {
    'click button': 'handleLoginClick'
  },

  handleLoginClick: function () {
    ...
  },

  render: function() {
    ...
  }
})

module.exports = LoginView

The template is precompiled, and the html is available from within your React components to mount and unmount as necessary under the props.templates key.

Custom Renderers

If you would prefer to use a rendering engine other than React, no problem -- just pass in a custom render function that returns a string:

import renderToString from "preact-render-to-string"

const html = await stitch({
  layout: "templates/loginLayout.pug",
  config: {
    componentRenderer: renderToString,
  },
  blocks: {
    body: App,
  },
})

Additionally, if you would like to override any default template engines (e.g., what is returned by require('handlebars')) you can do so by updating config.engines:

const html = await stitch({
  layout: "templates/loginLayout.pug",
  config: {
    engines: {
      handlebars: (filePath, locals) => {
        return customHandlebarsRenderer(filePath, locals)
      },
      pug: (filePath, locals) => {
        return customPugRenderer(filePath, locals)
      },
    },
  },
  blocks: {
    body: App,
  },
})

Styled Components Support

If your React app uses styled-components, ensure you've installed babel-plugin-styled-components and enable server-side rendering via config:

import styled from "styled-components"

const html = await stitch({
  layout: "templates/layout.pug",
  config: {
    styledComponents: true,
  },
  blocks: {
    body: props => {
      const Layout = styled.div`
        background: purple;
        border: 1px solid black;
        color: white;
        width: 100%;
      `

      return <Layout>Hello Styled Components!</Layout>
    },
  },
})

Lastly, make sure to mount your styles in your layout template:

html
  head
    != css

  body
    #react-root
      != body

Full API

const html = await stitch({

  /**
   * Sets a base path from which to fetch templates.
   *
   * @type {String}
   */
  basePath: process.cwd(),

  /**
   * Path to layout template file / component.
   *
   * @type {String|Component}
   */
  layout: <REQUIRED>

  /**
   * Block sections to pass to template / components. A "block" typically
   * represents a discreet section (header, body, footer, etc) but can be
   * anything.
   */
  blocks: {
    /**
     * @type {String|Component}
     */
  },

  /**
   * Locals represent Express.js locals as they flow through the `req` / `res`
   * cycle. These values could be passed to the following section, `data`, but
   * for the sake of clarity and isolation they're typically passed along here.
   *
   * Accessible from within components via `props.locals` and from within a
   * template via `locals`
   */
  locals: {
    /**
     * @type {*}
     */
  },

  /**
   * Data that is passed to templates and components. Embedded directly
   * within template code, but also accessible via `data` (useful for
   * JSON.stringify'ing and passing down the wire for rehydration). In a
   * component, it represents `props` and is accessible as such.
   */
  data: {
    /**
     * @type {*}
     */
  },

  /**
   * Templates / Components that are precompiled and passed along as rendered
   * html strings. From within your component, template html is accessible via
   * `props.templates`
   */
  templates: {
    /**
     * @type {String|Component}
     */
  },

  config: {

    /**
     * Configuration for layout renderer. Right now components are rendered via
     * ReactDOM, but any kind of engine can be passed in and accommodated
     * assuming it returns a string of rendered markup.
     *
     * @type {Function}
     */
    componentRenderer: ReactDOM.renderToString,

    /**
     * If you would like to override any default template engines pass in a
     * key matching the extension and a function, e.g.,
     *
     * engines: {
     *   pug: (filePath, locals) => string
     * }
     */
    engines: {
      /**
       * @type {Function}
       */
    },

    /**
     * If your project uses <StyledComponents /> and you would like to extract
     * styles from your component during server-side renders, set this to true.
     *
     * See https://www.styled-components.com/docs/advanced#server-side-rendering
     * for more information.
     *
     * @type {Boolean}
     */
    styledComponents: false
  }
})

Troubleshooting

Help! My view doesn't render and there's an Unexpected token ( in my console.

All of the examples assume that you've enabled async / await, which comes by default in versions of Node >= 7.6.0. If you're getting this error, it's likely you're using an older version that doesn't yet support this feature. But no fear - async / await is just a wrapper around Promises:

stitch({ layout: "layout.ejs" })
  .then(html => {
    res.send(html)
  })
  .catch(error => {
    next(error)
  })

Async / await is supported in my version of Node, but I'm not seeing anything in the browser and no errors in the console!

Did you remember to use the await keyword before stitch? It's easy to forget :)

Development

Commits and Deployments

This project uses auto-release to automatically release on every PR. Every PR should have a label that matches one of the following

  • Version: Trivial
  • Version: Patch
  • Version: Minor
  • Version: Major

No release will happen on a Trivial update.

版本与发布记录

@artsy/stitch 共发布 58 个版本,首次发布于 2017/08/26;过去 12 个月发布了 0 个版本。

版本发布日期距今
6.5.0latest2022/04/134 年前
6.4.22021/12/114 年前
6.4.12021/10/205 年前
6.4.02021/09/215 年前
6.3.02021/06/235 年前
6.2.02020/10/316 年前

其他发布通道:canary → 6.1.7-canary.103.1452.0。

关于 @artsy/stitch 的常见问题

@artsy/stitch 是什么?

Helps your Component and Template dependencies peacefully coexist

如何安装 @artsy/stitch?

在项目目录运行 npm install @artsy/stitch,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^16.5.0)、react-dom(^16.5.0)、styled-components(4.0.3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@artsy/stitch 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@artsy/stitch 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@artsy/stitch 可以商用吗?

@artsy/stitch 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@artsy/stitch 的下载量有多少?

根据 npm 官方下载统计,@artsy/stitch 在 2026/10/07 – 2026/11/05 这 30 天内被下载 8,189 次,日均约 273 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@artsy/stitch 还在维护吗?

最新版本 6.5.0 发布于 2022/04/13(4 年前);至今共发布 58 个版本,其中过去 12 个月发布了 0 个;首次发布于 2017/08/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。