Giter Site home page Giter Site logo

universal-webpack's Introduction

universal-webpack

NPM Version Test Coverage

For beginners: consider trying Next.js first: it's user-friendly and is supposed to be a good start for people not wanting to deal with configuring Webpack manually. On the other hand, if you're an experienced Webpack user then setting up universal-webpack shouldn't be too difficult.

This library generates client-side and server-side configuration for Webpack therefore enabling seamless client-side/server-side Webpack builds. Requires some initial set up and some prior knowledge of Webpack.

Small Advertisement: ๐Ÿ“ž if you're looking for a React phone number component check out react-phone-number-input

Installation

npm install universal-webpack --save

Example project

You may refer to this sample project as a reference example of using this library (see webpack directory, package.json and client/rendering-service/main.js).

Usage

Suppose you have a typical webpack.config.js file. Create two new files called webpack.config.client.babel.js and webpack.config.server.babel.js with the following contents:

webpack.config.client.babel.js

import { client } from 'universal-webpack/config'
import settings from './universal-webpack-settings'
import configuration from './webpack.config'

export default client(configuration, settings)

webpack.config.server.babel.js

import { server } from 'universal-webpack/config'
import settings from './universal-webpack-settings'
import configuration from './webpack.config'

export default server(configuration, settings)

universal-webpack-settings.json

{
	"server":
	{
		"input": "./source/server.js",
		"output": "./build/server/server.js"
	}
}

Use webpack.config.client.babel.js instead of the old webpack.config.js for client side Webpack builds.

The server() configuration function takes the client-side Webpack configuration and tunes it a bit for server-side usage (target: "node").

The server-side bundle (settings.server.output file) is generated from settings.server.input file by Webpack when it's run with the webpack.config.server.babel.js configuration. An example of settings.server.input file may look like this (it must export a function):

source/server.js

// express.js
import path from 'path'
import http from 'http'
import express from 'express'
import http_proxy from 'http-proxy'

// react-router
import routes from '../client/routes.js'

// Redux
import store from '../client/store.js'

// The server code must export a function
// (`parameters` may contain some miscellaneous library-specific stuff)
export default function(parameters)
{
	// Create HTTP server
	const app = new express()
	const server = new http.Server(app)

	// Serve static files
	app.use(express.static(path.join(__dirname, '..', 'build/assets')))

	// Proxy API calls to API server
	const proxy = http_proxy.createProxyServer({ target: 'http://localhost:xxxx' })
	app.use('/api', (req, res) => proxy.web(req, res))

	// React application rendering
	app.use((req, res) =>
	{
		// Match current URL to the corresponding React page
		// (can use `react-router`, `redux-router`, `react-router-redux`, etc)
		react_router_match_url(routes, req.originalUrl).then((error, result) =>
		{
			if (error)
			{
				res.status(500)
				return res.send('Server error')
			}

			// Render React page

			const page = redux.provide(result, store)

			res.status(200)
			res.send('<!doctype html>' + '\n' + ReactDOM.renderToString(<Html>{page}</Html>))
		})
	})

	// Start the HTTP server
	server.listen()
}

The last thing to do is to create a startup file for the server side. This is the file you're gonna run with Node.js, not the file provided above.

source/start-server.js

var startServer = require('universal-webpack/server')
var settings = require('../universal-webpack-settings')
// `configuration.context` and `configuration.output.path` are used
var configuration = require('../webpack.config')

startServer(configuration, settings)

Calling source/start-server.js will basically call the function exported from source/server.js built with Webpack.

In the end you run all the above things like this (in parallel):

webpack-dev-server --hot --inline --config "./webpack.config.client.babel.js" --port XXXX --colors --display-error-details
webpack --watch --config "./webpack.config.server.babel.js" --colors --display-error-details
nodemon "./source/start-server" --watch "./build/server"

The above three commands are for development mode. For production mode the same command sequence would be:

webpack --config "./webpack.config.client.babel.js" --colors --display-error-details
webpack --config "./webpack.config.server.babel.js" --colors --display-error-details
node "./source/start-server"

Chunks

This library will pass the chunks() function parameter (inside the parameters argument of the server-side function) which returns webpack-compiled chunks filename info:

build/webpack-chunks.json

{
	javascript:
	{
		main: `/assets/main.785f110e7775ec8322cf.js`
	},

	styles:
	{
		main: `/assets/main.785f110e7775ec8322cf.css`
	}
}

These filenames are required for <script src=.../> and <link rel="style" href=.../> tags in case of isomorphic (universal) rendering on the server-side.

Gotchas

  • It emits no assets on the server side so make sure you include all assets on the client side (e.g. "favicon").
  • resolve.root won't work out-of-the-box while resolve.aliases do. For those using resolve.root I recommend switching to resolve.alias. By default no "modules" are bundled in a server-side bundle except for resolve.aliases and excludeFromExternals matches (see below).

Using extract-text-webpack-plugin or mini-css-extract-plugin

The third argument โ€“ options object โ€“ may be passed to client() configuration function. If options.development is set to false, then it will apply extract-text-webpack-plugin to CSS styles automatically, i.e. it will extract all CSS styles into one big bundle file: this is considered the "best practice" for production deployment and using this option is more convenient then adding extract-text-webpack-plugin to production webpack configuration manually. If upgrading a project from Webpack <= 3 to Webpack >= 4 (or starting fresh with Webpack >= 4) then extract-text-webpack-plugin should be replaced with mini-css-extract-plugin. In this case also pass options.useMiniCssExtractPlugin option set to true.

Advanced configuration

{
	// By default, all `require()`d packages
	// (e.g. everything from `node_modules`, `resolve.modules`),
	// except for `resolve.alias`ed ones,
	// are marked as `external` for server-side Webpack build
	// which means they won't be processed and bundled by Webpack,
	// instead being processed and `require()`d at runtime by Node.js.
	//
	// With this setting one can explicitly define which modules
	// aren't gonna be marked as `external` dependencies.
	// (and therefore are gonna be compiled and bundled by Webpack)
	//
	// Can be used, for example, for ES6-only `node_modules`.
	// ( a more intelligent solution would be accepted
	//   https://github.com/catamphetamine/universal-webpack/issues/10 )
	//
	excludeFromExternals:
	[
		'lodash-es',
		/^some-other-es6-only-module(\/.*)?$/
	],

	// As stated above, all files inside `node_modules`, when `require()`d,
	// would be resolved as "externals" which means Webpack wouldn't use
	// loaders to process them, and therefore `require()`ing them
	// would result in an error when running the server-side bundle.
	//
	// E.g. for CSS files Node.js would just throw `SyntaxError: Unexpected token .`
	// because these CSS files need to be compiled by Webpack's `css-loader` first.
	//
	// Hence the "exclude from externals" file extensions list
	// which by default is initialized with some common asset types:
	//
	loadExternalModuleFileExtensions:
	[
		'css',
		'png',
		'jpg',
		'svg',
		'xml'
	],

	// Enable `silent` flag to prevent client side webpack build
	// from outputting chunk stats to the console.
	silent: true,

	// By default, chunk_info_filename is `webpack-chunks.json`
	chunk_info_filename: 'submodule-webpack-chunks.json'
}

Source maps

I managed to get source maps working in my Node.js server-side code using source-map-support module.

source/start-server.js

// Enables proper source map support in Node.js
require('source-map-support/register')

// The rest is the same as in the above example

var startServer = require('universal-webpack/server')
var settings = require('../universal-webpack-settings')
var configuration = require('../webpack.config')

startServer(configuration, settings)

Without source-map-support enabled it would give me No element indexed by XXX error (which means that by default Node.js thinks there are references to other source maps and tries to load them but there are no such source maps).

devtool is set to source-map for server-side builds.

Nodemon

I recommend using nodemon for running server-side Webpack bundle. Nodemon has a --watch <directory> command line parameter which restarts Node.js process each time the <directory> is updated (e.g. each time any file in that directory is modified).

In other words, Nodemon will relaunch the server every time the code is rebuilt with Webpack.

There's one little gotcha though: for the --watch feature to work the watched folder needs to exist by the time Nodemon is launched. That means that the server must be started only after the settings.server.output path folder has been created.

To accomplish that this library provides a command line tool: universal-webpack. No need to install in globally: it is supposed to work locally through npm scripts. Usage example:

package.json

...
  "scripts": {
    "start": "npm-run-all prepare-server-build start-development-workflow",
    "start-development-workflow": "npm-run-all --parallel development-webpack-build-for-client development-webpack-build-for-server development-start-server",
    "prepare-server-build": "universal-webpack --settings ./universal-webpack-settings.json prepare",
    ...

The prepare command creates settings.server.output path folder, or clears it if it already exists.

Note: In a big React project server restart times can reach ~10 seconds.

Flash of unstyled content

(this is an "advanced" section which can be safely skipped)

A "flash of unstyled content" is a well-known thing. One can observe it when refreshing the page in development mode: because Webpack's style-loader adds styles to the page dynamically there's a short period (a second maybe) when there are no CSS styles applied to the webpage (in production mode extract-text-webpack-plugin or mini-css-extract-plugin is used instead of style-loader so there's no "flash of unstyled content").

It's not really a bug, many projects live with it and it doesn't really affect the development process that much, so one can safely skip reading this section. It's just if you're a perfectionist then it can get a little itchy.

I came up with a solution which seems to be working good enough. To enable the anti-unstyled-flash feature one needs to pass the third parameter to the client-side webpack configuration creation function - an options object with:

  • development key set to true indicating that it's a development build configuration
  • cssBundle key set to true

If both development and cssBundle options are set to true, then universal-webpack will enhance the client side Webpack configuration to also output all styles into a single CSS bundle (while retaining style-loader) which is later added to the webpage's <head/> as a <link rel="stylesheet"/> tag on the server side, therefore making that "flash of unstyled content" disappear.

There's a gotcha though. Because the whole CSS bundle gets inserted as a <link rel="stylesheet"/> tag in the <head/> it also means that the styles defined in that CSS bundle are static, not dynamic, and they aren't gonna "hot reload" themselves or something. So, my proposed solution is to have that <link rel="stylesheet"/> tag sit in the <head/> for a while (say, a couple of seconds) and then remove it from there. The styling of the webpage isn't gonna disappear at that moment because by that time the dynamic styles of style-loader have already kicked in. See an example of how this can be done.

import { client } from 'universal-webpack/config'
import settings from './universal-webpack-settings'
import configuration from './webpack.config'

export default client(configuration, settings, { development: true, cssBundle: true })

resolve.moduleDirectories

If you were using resolve.moduleDirectories for global paths instead of relative paths in your code then consider using resolve.alias instead

resolve:
{
  alias:
  {
    components: path.resolve(__dirname, '../src/components'),
    ...
  }
}

universal-webpack vs webpack-isomorphic-tools

Note: If you never heard of webpack-isomorphic-tools then you shouldn't read this section.

webpack-isomorphic-tools runs on the server-side and hooks into Node.js require() function with the help of require-hacker and does what needs to be done.

universal-webpack doesn't hook into require() function - it's just a helper for transforming client-side Webpack configuration to a server-side Webpack configuration. It doesn't run on the server-side or something. It's just a Webpack configuration generator - turned out that Webpack has a target: "node" parameter which makes it output code that runs on Node.js without any issues.

I wrote webpack-isomorphic-tools before universal-webpack, so universal-webpack is the recommended tool. However many people still use webpack-isomorphic-tools (including me) and find it somewhat less complicated for beginners.

License

MIT

universal-webpack's People

Contributors

alexicum avatar catamphetamine avatar gustavo-gimenez avatar indeyets avatar lixiaoyan avatar lonelyclick avatar lukesheard avatar papigers avatar purecatamphetamine avatar rogeres avatar springuper avatar

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.