Coder Social home page Coder Social logo

metnew / tiny-universal-skeleton Goto Github PK

View Code? Open in Web Editor NEW
10.0 4.0 1.0 1.05 MB

Alternative approach to HMR for both server with SSR and client in development.[EXPERIMENTAL EXAMPLE]

Home Page: https://github.com/Metnew/suicrux

License: Apache License 2.0

JavaScript 100.00%
webpack webpack-dev-server server-middleware universal ssr devserver development webpack-configuration webpack-boilerplate

tiny-universal-skeleton's Introduction

Tiny-universal-skeleton

If you want advanced starter with this feature -> take a look at SUIcrux!

Imagine that you have server, development server and client app:

  • Server - your server middlewares, api endpoints and SSR.
  • Development server - express server with webpack-hot-middleware and webpack-dev-middleware.
  • Client - your frontend. (e.g. React app).

Main problem: sync server middlewares with client and don't lose power of webpack-(dev|hot)-middleware.

Typical use case

Your server has middleware that checks is a user logged in and use this info later for SSR.

There are other solutions like universal-webpack.
I'm not sure what's going on inside universal-webpack and can it solve the described above problem. But it looks complicated.

In case of a bug inside a complicated software with low community support you'll be the one person who cares about this bug.

This solution is very-very simple. But it's not the best, of course.

99.9% of universal starters/frameworks run 2 process on 2 ports with 2 different FS. We run only one process on one port with the same FS.

TL;DR:

  1. Server entry should export function that decorates server with all middlewares, api endpoints, ssr. Except dev middlewares.
  2. Compile both server and client with Webpack.
  3. Get compiled decorator for server and decorate your dev server. DevServer = express server with development middlewares.
  4. ????????????
  5. Every time your code changes, webpack recompiles your code and decorates your server with newly compiled code.

Server Entry (src/server/index.js)

import express from 'express'
import chalk from 'chalk'

const app = express()
const PORT = 3000
const isProduction = process.env.NODE_ENV === 'production'
const pathToServerDecorator = isProduction
    ? './decorator'
    : '../../webpack_config/devServer'
// NOTE: Such dynamic imports is a bad practice!
// It's used here to show that our `serverDecorator` is a dynamic thing.
const serverDecorator = require(pathToServerDecorator).default
serverDecorator(app)

app.listen(PORT, () => {
    console.log(chalk.green(`SERVER IS LISTENING ON ${PORT}`))
})

Did you notice serverDecorator() function? Let's figure out what's the hack is it:

Main server decorator (src/server/decorator.js)

This function decorates express server instance with your middlewares, API and SSR stuff. (e.g. makes your server yours) We require this function to decorate our server in production.

import addMiddlewares from './middlewares'
import API from './api'
import SSR from './ssr'

/**
 * Mount API, SSR and middlewares to app.
 * @param  {Object} app - Express server instance
 * @return {Object}     - Decorated server instance
 */
export default function (app) {
    // Add global middlewares
    addMiddlewares(app)
    // Add API
    app.use('/api/v1', API)
    // Add SSR
    app.use(SSR)
    return app
}

Server decorator in development (webpack_config/devServer.js)

TL;DR: after every compilation we remove already applied middlewares from Express app (from middlewares stack) and apply new ones.

In development our server decorator looks like:

// NOTE: Every time we apply our compiled code to development server
// We add new middlewares from new code, but don't remove old middlewares from old code
// Number of middlewares that our app should have
let prevSize = null
/**
 * @desc Adds dev middlewares + your code to an express server instance
 * @param {ExpressServer} app - Express dev server to which compiled code will be applied
 */
export default function (app) {
    /**
     * @desc Function that executes after your server-side code compiles
     * @param  {Function}  serverSideCode - compiled server-side code
     */
    const done = serverSideCode => {
        // Get current stack of the app (e.g. applied to Express server middlewares)
        const {stack} = app._router
        // get current lenght of stack
        const {length} = stack
        // When we run server first time we don't have any applied middlewares from compiled code
        prevSize = prevSize || length
        if (length > prevSize) {
            // TL;DR: Remove already compiled code
            // That means that we already applied our code to devServer
            // And we can remove already applied middlewares from the last compilation
            app._router.stack = stack.slice(0, prevSize)
        }
        // Apply newly compiled code to our app
        serverSideCode(app)
    }

    // webpack Compiler for server
    const serverCompiler = compiler.compilers.find(
        compiler => compiler.name === 'server'
    )
    // webpack Compiler for client
    const clientCompiler = compiler.compilers.find(
        compiler => compiler.name === 'client'
    )
    // Add webpack-dev-middleware
    app.use(devMiddleWare)
    // Add webpack-hot-middleware
    app.use(
        webpackHotMiddleware(clientCompiler, {
            log: console.log
        })
    )
    // Run `done` function after serverCompiler emits `done` event with a newly compiled code as argument
    webpackGetCodeOnDone(serverCompiler, done)
}

Bundle server with Webpack

I don't want to argue about "Is it ok to bundle server-side code with Webpack?" Shortly, it's a great idea. Main features:

  • tree-shaking,
  • code optimizations,
  • high configuration possibilities with webpack.DefinePlugin().

Main cons: it's hard to work with dirs, because webpack supports __dirname not as you expected. But it's easy to solve this problem with webpack alias. Read more in webpack docs.

Webpack.config

Your webpack.config.js for server may looks like:

import path from 'path'
import fs from 'fs'
import webpack from 'webpack'

const isProduction = process.env.NODE_ENV === 'production'
const distPath = 'my/dist/path'
// NOTE: Notice these lines also:
const entry = isProduction
    ? path.join(config.srcPath, './server')
    : path.join(config.srcPath, './server/server')

// Read more about Webpack for server, if you don't know what this lines do.
let nodeModules = {}
fs
    .readdirSync('node_modules')
    .filter(x => {
        return ['.bin'].indexOf(x) === -1
    })
    .forEach(mod => {
        nodeModules[mod] = 'commonjs ' + mod
    })

const baseWebpackConfig = {
    name: 'server',
    entry,
    target: 'node',
    output: {
        path: path.join(distPath, './server'),
        filename: 'index.js',
    // NOTE: You should add this line:
        libraryTarget: 'commonjs2'
    // If you didn't add info about "libraryTarget", setup will not work.
    // You should add `libraryTarget` property in dev mode
    // it's not a must to have `libraryTarget` in production
    },
    externals: nodeModules,
    node: {
        __dirname: true,
        global: true
    }
}

export default baseWebpackConfig

There are 2 strange things inside our webpack config:

"libraryTarget"

"libraryTarget" must be "commonjs2". (Please, be sure that you didn't forget to set libraryTarget in development!)

Dynamic "entry"

As you already know: in dev we use server decorator and decorate our development(!) server. for production build we use server decorator to decorate our server.

Summary

You can easily make your universal app hot-reloaded in few lines of code.
Performance is good, there are no benchmarks and comparisons, but it works well even on huge projects.

License

Apache 2.0 License

Author

Vladimir Metnew [email protected]

tiny-universal-skeleton's People

Contributors

metnew avatar mrmabo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

mrmabo

tiny-universal-skeleton's Issues

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because we are using your CI build statuses to figure out when to notify you about breaking changes.

Since we did not receive a CI status on the greenkeeper/initial branch, we assume that you still need to configure it.

If you have already set up a CI for this repository, you might need to check your configuration. Make sure it will run on all new branches. If you don’t want it to run on every branch, you can whitelist branches starting with greenkeeper/.

We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

Once you have installed CI on this repository, you’ll need to re-trigger Greenkeeper’s initial Pull Request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper integration’s white list on Github. You'll find this list on your repo or organiszation’s settings page, under Installed GitHub Apps.

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.