npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

megasilver-rwd-callnumber

v1.0.0

Published

keywords: koa2,react,redux,isomorphic app

Downloads

37

Readme

koa2-react-isomorphic-boilerplate

keywords: koa2,react,redux,isomorphic app

Getting Start in dev mode

git clone https://github.com/wssgcg1213/koa2-react-isomorphic-boilerplate
cd koa2-react-isomorphic-boilerplate
npm install # 国内可以使用 cnpm 加速, 教育网可使用 rednpm (https://npm.mirror.cqupt.edu.cn)加速
npm start  # dev mode

open in browser

http://127.0.0.1:3000/

Deploy

npm test # pass unit tests
npm run build # build to dist
npm run production # or pm2 start

目录结构

.
├── app
│   ├── actions
│   ├── common
│   ├── components
│   ├── containers
│   │   └── App.jsx             # React App
│   ├── reducers
│   ├── routes.js               # 路由配置文件
│   └── store
│       └── configureStore.js
├── bin
│   ├── development.js
│   └── production.js
├── package.json
├── platforms
│   ├── browser                 # 浏览器相关
│   │   └── index.js            # 浏览器 APP 入口
│   ├── common
│   │   └── config              # 配置
│   │       ├── default.js
│   │       └── index.js
│   └── server                  # 服务端相关
│       ├── controllers
│       │   ├── indexCtrl.js
│       │   ├── serverRenderCtrl.js
│       │   └── usersCtrl.js
│       ├── index.js            # 服务端入口
│       ├── middlewareRegister.js
│       ├── models
│       ├── routes              # 服务端路由
│       │   ├── api.js
│       │   ├── index.js
│       │   └── render.js
│       ├── services
│       └── templates           # 服务端模板
│           ├── 404.ejs
│           ├── 422.ejs
│           ├── 500.ejs
│           └── index.ejs
├── pm2.json
├── public                      # public
│   ├── favicon.ico
│   └── robots.txt
├── test
│   └── test.js
├── webpack.build.js
└── webpack.development.js

同构的优势

  1. 首屏性能
  2. SEO / 搜索引擎爬虫支持
  3. 无缝的用户体验

实现的细节

目录结构

从 commits 可以看出, 目录的结构中途修改了多次, 对于目前的结构我也觉得有不合理的地方, 欢迎来提 issue 讨论. 由于在同构应用中, 有三种代码: 客户端 only ,服务端 only , 共用代码 , 这使得良好的目录结构显得更加重要. 最后我是参考了 ultimate-hot-reloading-example 这个项目的目录结构, 又做了适当的修改.

ServerSideRender

有人提出 React 是一种架构模式, 无论是内建的 DOM、Native还是React Canvas都是的一种基于React模式的具体实现. 那么 HTML string 其实也是 react 模式的一种实现, 只不过产出的是一堆字符串. React 在前几个版本中就把 render 函数从 react 中单独独立出来(react-dom), 服务端渲染依赖的就是 react-dom/server 中的renderToString 方法.

import { RouterContext } from 'react-router'
import { renderToString } from 'react-dom/server'

const appHtml = renderToString(<App {...renderProps} />)

路由

由于服务端需要做这么几件事情:

  1. react render
  2. api server

这些都需要路由来做分发

在服务端对于 /api 的请求, 全部交给 koa-router 来处理, 对于其他请求则交给 react-router 来处理

  // api server through koa-router
  if (ctx.path.match(/^\/api/)) {
    return await require('./api').routes()(ctx, next)
  }
  // others react-router to render
  await require('./render')(ctx, next)

koa-router 的用法在 platforms/server/routes/api.js 中, react-router 的用法的核心是 match 函数

const { redirectLocation, renderProps } = await _match({ routes: require('../../../app/routes'), location: ctx.url })
if (redirectLocation) {
  ctx.redirect(redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
  await renderCtrl(ctx, next, renderProps)
} else {
  await next()
}

上面的 renderCtrl 是负责服务端渲染的方法. require 的 routes 文件就是 react-router 的配置文件, 该文件在前后端路由中是共用的.

// routes.js
export default (
<Router history={browserHistory}>
  <Route path="/" component={App}>
    <Route path="picture" component={Picture} />
    <Route path="counter" component={Counter} />
  </Route>
</Router>
)

数据层

React 有丰富的数据层框架, 我用的是现在比较流行的 redux.

在服务端前置拉取数据的逻辑参考了 ReactJS 服务端同构实践【QQ音乐web团队】这篇文章的实现方式.

即在 react component 中规定了一个 fetch 的静态方法,在 Server Rendering 时,和前端相比组件没有完整的生命周期,只会走到 componentWillMount, 所以在前端的 componentDidMount 方法中去判断是否在前端做 fetch; 在做服务端渲染的时候遍历需要渲染的 components 去同步调用静态方法得到数据并吐出在页面上作为redux 的 initialState.

  let prefetchTasks = []
  for (let component of renderProps.components) {
    if (component && component.WrappedComponent && component.WrappedComponent.fetch) {
      const _tasks = component.WrappedComponent.fetch(store.getState(), store.dispatch)
      if (Array.isArray(_tasks)) {
        prefetchTasks = prefetchTasks.concat(_tasks)
      } else if (_tasks.then) {
        prefetchTasks.push(_tasks)
      }
    }
  }

  await Promise.all(prefetchTasks)
  await ctx.render('index', {
    title: config.title,
    dev: ctx.app.env === 'development',
    reduxData: store.getState(),
    app: renderToString(<Provider store={store}>
      <RouterContext {...renderProps} />
    </Provider>)
  })  

Component:

class App extends Component {
  static fetch (state, dispatch) {
    const fetchTasks = []
    fetchTasks.push(
      dispatch(fetchStateIfNeeded(state))
    )
    return fetchTasks
  }

  componentDidMount () {
    const { loaded, success } = this.props
    if ( !loaded || (loaded && !success) ) {
      this.constructor.fetch(this.props, this.props.dispatch)
    }
  }
  
  render () {
    const { location: { pathname } } = this.props
    const headerCurrent = pathname === '/' ? 'home' : pathname.slice(1)
    
    return (<div>
      <Header current={headerCurrent}/>
      <Main>{this.props.children}</Main>
      <Footer />
    </div>)
  }
}

模板

模板引擎是用的简单的 ejs

在 server render 的时候把 initial State 获取并吐出在页面中

    <section role="main" class="react-container">
      <div><%- app %></div>
    </section>
    <script>
      try {
        window.__REDUX_STATE__ = JSON.parse('<%- JSON.stringify(reduxData) %>');
      } catch (e) {
        console.warn('error in getting server redux data');
      }
    </script>
    <script src="/build/common.js"></script>
    <script src="/build/main.js"></script>

在前端的入口文件中使用 window.__REDUX_STATE__ 作为初始 state

const store = configureStore(window.__REDUX_STATE__)
ReactDOM.render(
  <Provider store={store}>
    {routes}
  </Provider>,
  document.querySelector('.react-container')
)

前后端兼容

前后端逻辑区分

虽然是同构应用, 大部分逻辑是共用的, 但是在服务端和浏览器端具体的实现肯定是不同的, 比如 superagent / isomorphic-fetch 这些库在服务端是使用的 http.request 方法, 而在前端使用 XHR 来实现. 那么在编写代码中, 前端的 url 可以使用相对路径, 而服务端只能使用完整的 http 请求路径.

const fetchStateUrl = __SERVER__ ? `http://localhost:${require('../../platforms/common/config').port}/api/state` : '/api/state'

可以通过 webpack 的 definePlugin 来做, 但是由于我在 development 模式下是通过 node + babel-register + webpack-dev-middleware 直接运行的应用, 对于服务端运行的代码 webpack 就无能为力了. 我写了一个 babel 的插件 https://github.com/wssgcg1213/babel-plugin-inline-replace-variables 来直接对 js 的 identifier 进行替换.

在打包前端 bundle 的时候得益于 uglifyjs 插件的处理, 不可达代码会被清除, 这样也不需要担心 bundle.js 体积增大.

Nodejs require 静态资源文件的处理

react 的 component 中充满了 import './component.less', import img from './img.png'这样的语法, 但在 node 中是会报错的, 因为 isomorphic-tools 的文档太冗杂, 我没有采取它那一套方案, 而是使用了babel-plugin-transform-require-ignore来忽略 css/less, 转而在 webpack-dev-middleware 中使用 style-loader 打包成 js bundle 输出, 这样也能同时支持 hot module replacement; 对于图片使用asset-require-hook这个包来使 require 直接返回文件路径.

总之, 开发模式下的服务端对 require 进行了魔改使它支持了引入 ES6+React+Stylesheets+Images 的能力.

总结

搞两个流程图来看看

开发模式:

开发模式

线上模式:

开发模式

Example

使用这个模式开发了

https://github.com/CQUPTMirror/mirror-web-isomorphic/

线上地址 https://mirror.cqupt.edu.cn

Contact

issues

@Ling