Goal
Our goal is (in theory) to write cross-platform (Node + browser) code, otherwise known as universal JavaScript. In practice we will write CommonJS modules (or ES6 modules, if you want), that anyone can use on NodeJS platform. We will use Webpack to create bundles to be loaded in the browser. I like creating an extra bundle for each project that is intended to run in the browser, because I like creating demo pages with each project. Thus my typical project distributed via NPM has the following structure
1 | ./ |
I build the file dist/project-name.js
using Webpack, and I show how it should be used
in the demo page dist/index.html
.
Any other NPM dependent project should require the main file index.js
or whatever is
listed as the main file inside package.json
.
Here are my typical commands
Start an NPM package
npm init
works great. If you want to keep the project private and not accidentally publish it
to the NPM registry, set "private": true
in the package.json
right away
Decide what is the main source file for the package, for example ./index.js
. Keep writing
the code the same way as you would for Node - using CommonJS exports and require
function.
Add and configure webpack (library case)
npm install --save-dev webpack
then create a webpack.config.js
file in the project's
root folder. Here is sample contents for a project named "foo"
1 | module.exports = { |
This will bundle everything reachable from the file "./index.js" into a file "dist/foo.js". The output file will have universal module definition boilerplate (UMD) code and can be used from other modules using 'require(...)', or as a stand alone bundle under the name 'foo'.
You can configure other library targets and export the code directly as a property of window
for example
1 | // will generate window.foo = ... |
Set up the NPM script to run weback build step, for example
1 | "scripts": { |
This should generate dist/foo.js
file every time you run npm run build
. You can include
the bundle from the dist/index.html
and use the library using window.foo
(the name "foo",
is the value of the "library" property)
1 | <script src="foo.js"></script> |
Example project tiny-toast
Note
Do not forget to include the "dist" folder in the list of files published to NPM
Bundle JavaScript application
Sometimes you are writing a library to be reused by others, and in other cases you are writing an application. Webpack supports both types; bundling an application is even simpler than bundling a library - there is no UMD boilerplate. Instead specify an "entry" file
1 | module.exports = { |
This will create "dist/app.js" bundle that will include all the code reachable from "src/app.js". The "src/app.js" should have all the logic to start the application - this is what will run when the browser loads the page (if you include "dist/app.js" from the page, of course)
Example project instant-vdom-todo
Bundle application CSS
If your project includes CSS files, you can bundle them up using WebPack and even compile SaSS / Less / Stylus / etc into CSS using loaders. This applies more to the application bundles, not the library bundles.
To create a simple CSS bundle next to the corresponding JavaScript bundle in the "dist" folder, install the following webpack plugins
npm install --save-dev extract-text-webpack-plugin css-loader style-loader
Tell webpack to load every ".css" file using the style-loader and create a separate bundle
1 | var ExtractTextPlugin = require('extract-text-webpack-plugin') |
In "src/app.js" use "require" to load CSS files to let webpack know which files to bundle to create "dist/app.css"
1 |
|
Example project instant-vdom-todo
Bundle library and application
You can have multiple parallel configurations inside a single config file. This is useful if you want to build unrelated bundles. For example a project might build a library and a demo application. Just export an array of configuraton objects
1 | module.exports = [{ |
The above example is taken from fake-todos build.
It builds a universal library 'dist/fake-todos.js' and a complete stand alone demo application
dist/demo-app.js
Split code across several bundles
If you want to split a large code base across multiple bundles, with possible code sharing, for example, read the multiple entrypoints section.
As an example, consider a small Vue.js application. The only vendor libraries
it imports are 'vue' and 'vue-router'. Thus we can mark these imports as
"vendor" and they will be placed into separate bundle by the
webpack.optimize.CommonsChunkPlugin
.
1 | var path = require('path') |
This will produce in DEV mode files public/bundle.js
and
public/vendor.bundle.js
, which is exactly what we need.
Add object spread
Often in the code examples we see features like Object spread. For example, if we want to quickly extend an object with additional properties to create a new object to use:
1 | const p = { |
The object result
will have all properties of the object p
plus additional
property job
. Without ...foo
we could have used ES6 Object.assign
to achieve the same but it looks clunky.
1 | const p = { |
To allow us to use ...object
notation we need to transpile code using
Babel (or some other transpiler) plus add a special plugin to support
"object spread and rest" feature.
In our "loaders" object we should specify Babel as the loader for ".js" files.
1 | { |
In the babel options file .babelrc
add this transform to the list
1 | { |
Do not forget to actually add the transform to the dev dependencies
1 | npm i -D babel-plugin-transform-object-rest-spread |
Enjoy the short object spread notation.
Inject environment variables during build
We might want to inject environment-specific variables when building the bundle. The easiest way to do this is by using "EnvironmentPlugin" which is included with the webpack.
Let us put all settings into file "dev-config.js". We will right away use
environment variables in this file, for example process.env.USER
1 | module.exports = { |
Elsewhere in our code, we will access this file NOT by its filename, but by
an alias, for example require('config')
to dynamically select it. We could
for example inject completely different settings file when building in
production environment for example. In simple example, let us just print
the loaded username from index.js
1 | const config = require('config') |
We should configure the webpack.config.js
like this
1 | const webpack = require('webpack') |
Note the following points
- We tell webpack to inject the current value (at build time) of the
environment variable
USER
, thus it will literally replace every reference to stringprocess.env.USER
in our code. - We alias every request to "config" module to point at file "./dev-config.js". This alias can be very flexible and depend on the target environment for example.
The produced bundle shows the result of the build-time substitution
1 | /******/ ([ |
This produces the desired effect.
Futher reading
What I have shown is just the beginning. To learn more, read the following tutorials