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-middlewareandwebpack-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.
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 insideuniversal-webpackand 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:
- Server entry should export function that decorates server with all middlewares, api endpoints, ssr. Except dev middlewares.
- Compile both server and client with Webpack.
- Get compiled decorator for server and decorate your dev server. DevServer = express server with development middlewares.
- ????????????
- Every time your code changes, webpack recompiles your code and decorates your server with newly compiled code.
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:
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}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)}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.
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}}exportdefaultbaseWebpackConfigThere are 2 strange things inside our webpack config:
"libraryTarget" must be "commonjs2". (Please, be sure that you didn't forget to set libraryTarget in development!)
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.
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.
Apache 2.0 License
Vladimir Metnew vladimirmetnew@gmail.com
