Skip to content
This repository was archived by the owner on May 8, 2018. It is now read-only.

Repository files navigation

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)

importexpressfrom'express'importchalkfrom'chalk'constapp=express()constPORT=3000constisProduction=process.env.NODE_ENV==='production'constpathToServerDecorator=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.constserverDecorator=require(pathToServerDecorator).defaultserverDecorator(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.

importaddMiddlewaresfrom'./middlewares'importAPIfrom'./api'importSSRfrom'./ssr'/** * Mount API, SSR and middlewares to app. * @param {Object} app - Express server instance * @return {Object} - Decorated server instance */exportdefaultfunction(app){// Add global middlewaresaddMiddlewares(app)// Add APIapp.use('/api/v1',API)// Add SSRapp.use(SSR)returnapp}

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 haveletprevSize=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 */exportdefaultfunction(app){/** * @desc Function that executes after your server-side code compiles * @param {Function} serverSideCode - compiled server-side code */constdone=serverSideCode=>{// Get current stack of the app (e.g. applied to Express server middlewares)const{stack}=app._router// get current lenght of stackconst{length}=stack// When we run server first time we don't have any applied middlewares from compiled codeprevSize=prevSize||lengthif(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 compilationapp._router.stack=stack.slice(0,prevSize)}// Apply newly compiled code to our appserverSideCode(app)}// webpack Compiler for serverconstserverCompiler=compiler.compilers.find(compiler=>compiler.name==='server')// webpack Compiler for clientconstclientCompiler=compiler.compilers.find(compiler=>compiler.name==='client')// Add webpack-dev-middlewareapp.use(devMiddleWare)// Add webpack-hot-middlewareapp.use(webpackHotMiddleware(clientCompiler,{log: console.log}))// Run `done` function after serverCompiler emits `done` event with a newly compiled code as argumentwebpackGetCodeOnDone(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:

importpathfrom'path'importfsfrom'fs'importwebpackfrom'webpack'constisProduction=process.env.NODE_ENV==='production'constdistPath='my/dist/path'// NOTE: Notice these lines also:constentry=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.letnodeModules={}fs.readdirSync('node_modules').filter(x=>{return['.bin'].indexOf(x)===-1}).forEach(mod=>{nodeModules[mod]='commonjs '+mod})constbaseWebpackConfig={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}}exportdefaultbaseWebpackConfig

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 vladimirmetnew@gmail.com

About

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

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages