- Create a
dist
folder at the root of your project, and add the followingindex.html
file to it:
<!doctype html>
<html>
<head>
</head>
<body>
<div class="app"></div>
<script src="client-bundle.js"></script>
</body>
</html>
In your src
folder, create the following subfolders: server
, shared
, client
, and move your current index.js
into server
, and dog.js
into shared
. Create app.js
in client
.
We are not going to do any Node back-end yet, but this separation will help you see more clearly where things belong. You'll need to change the import Dog from './dog';
in server/index.js
to import Dog from '../shared/dog';
though, or ESLint will detect errors for unresolved modules.
Write this in client/app.js
:
import Dog from '../shared/dog';
const browserToby = new Dog('Browser Toby');
document.querySelector('.app').innerText = browserToby.bark();
Add the following to your package.json
, under eslintConfig
:
"env": {
"browser": true
}
This way we can use variables such as window
or document
which are always accessible in the browser without ESLint complaining about undeclared variables.
If you want to use some of the most recent ES features in your client code, like Promise
s, you need to include the Babel Polyfill in your client code.
- Run
yarn add babel-polyfill
And before anything else in app.js
, add this import:
import 'babel-polyfill';
Including the polyfill adds some weight to your bundle, so add it only if you use the features it covers. In order to provide some solid boilerplate code with this tutorial, I am including it and it will appear in code samples in the next chapters.
In a Node environment, you can freely import
different files and Node will resolve these files using your filesystem. In a browser, there is no filesystem, and therefore your import
s point to nowhere. In order for our entry point file app.js
to retrieve the tree of imports it needs, we are going to "bundle" that entire tree of dependencies into one file. Webpack is a tool that does this.
Webpack uses a config file, just like Gulp, called webpack.config.js
. It is possible to use ES6 imports and exports in it, in the exact same way that we made Gulp rely on Babel to do so: by naming this file webpack.config.babel.js
.
-
Create an empty
webpack.config.babel.js
file -
While you're at it, add
webpack.config.babel.js
to your Gulplint
task, and a few morepaths
constants:
const paths = {
allSrcJs: 'src/**/*.js',
gulpFile: 'gulpfile.babel.js',
webpackFile: 'webpack.config.babel.js',
libDir: 'lib',
distDir: 'dist',
};
// [...]
gulp.task('lint', () =>
gulp.src([
paths.allSrcJs,
paths.gulpFile,
paths.webpackFile,
])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError())
);
We need to teach Webpack how to process ES6 files via Babel (just like we taught Gulp how to process ES6 files with gulp-babel
). In Webpack, when you need to process files that are not plain old JavaScript, you use loaders. So let's install the Babel loader for Webpack:
-
Run
yarn add --dev babel-loader
-
Write the following to your
webpack.config.babel.js
file:
export default {
output: {
filename: 'client-bundle.js',
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: [/node_modules/],
},
],
},
resolve: {
extensions: ['', '.js', '.jsx'],
},
};
Let's analyze this a bit:
We need this file to export
stuff for Webpack to read. output.filename
is the name of the bundle we want to generate. devtool: 'source-map'
will enable source maps for a better debugging experience in your browser. In module.loaders
, we have a test
, which is the JavaScript regex that will be used to test which files should be processed by the babel-loader
. Since we will use both .js
files and .jsx
files (for React) in the next chapters, we have the following regex: /\.jsx?$/
. The node_modules
folder is excluded because there is no transpilation to do there. This way, when your code import
s packages located in node_modules
, Babel doesn't bother processing those files, which reduces build time. The resolve
part is to tell Webpack what kind of file we want to be able to import
in our code using extension-less paths like import Foo from './foo'
where foo
could be foo.js
or foo.jsx
for instance.
Okay so now we have Webpack set up, but we still need a way to run it.
Webpack can do a lot of things. It can actually replace Gulp entirely if your project is mostly client-side. Gulp being a more general tool, it is better suited for things like linting, tests, and back-end tasks though. It is also simpler to understand for newcomers than a complex Webpack config. We have a pretty solid Gulp setup and workflow here, so integrating Webpack to our Gulp build is going to be easy peasy.
Let's create the Gulp task to run Webpack. Open your gulpfile.babel.js
.
We don't need the main
task to execute node lib/
anymore, since we will open index.html
to run our app.
- Remove
import { exec } from 'child_process'
.
Similarly to Gulp plugins, the webpack-stream
package lets us integrate Webpack into Gulp very easily.
-
Install the package with:
yarn add --dev webpack-stream
-
Add the following
import
s:
import webpack from 'webpack-stream';
import webpackConfig from './webpack.config.babel';
The second line just grabs our config file.
Like I said earlier, in the next chapter we are going to use .jsx
files (on the client, and even on the server later on), so let's set that up right now to have a bit of a head start.
- Change the constants to the following:
const paths = {
allSrcJs: 'src/**/*.js?(x)',
serverSrcJs: 'src/server/**/*.js?(x)',
sharedSrcJs: 'src/shared/**/*.js?(x)',
clientEntryPoint: 'src/client/app.js',
gulpFile: 'gulpfile.babel.js',
webpackFile: 'webpack.config.babel.js',
libDir: 'lib',
distDir: 'dist',
};
The .js?(x)
is just a pattern to match .js
or .jsx
files.
We now have constants for the different parts of our application, and an entry point file.
- Modify the
main
task like so:
gulp.task('main', ['lint', 'clean'], () =>
gulp.src(paths.clientEntryPoint)
.pipe(webpack(webpackConfig))
.pipe(gulp.dest(paths.distDir))
);
Note: Our build
task currently transpiles ES6 code to ES5 for every .js
file located under src
. Now that we've split our code into server
, shared
, and client
code, we could make this task only compile server
and shared
(since Webpack takes care of client
). However, in the Testing chapter, we are going to need Gulp to also compile the client
code to test it outside of Webpack. So until you reach that chapter, there is a bit of useless duplicated build being done. I'm sure we can all agree that it's fine for now. We actually aren't even going to be using the build
task and lib
folder anymore until that chapter, since all we care about right now is the client bundle.
- Run
yarn start
, you should now see Webpack building yourclient-bundle.js
file. Openingindex.html
in your browser should display "Wah wah, I am Browser Toby".
One last thing: unlike our lib
folder, the dist/client-bundle.js
and dist/client-bundle.js.map
files are not being cleaned up by our clean
task before each build.
- Add
clientBundle: 'dist/client-bundle.js?(.map)'
to ourpaths
configuration, and tweak theclean
task like so:
gulp.task('clean', () => del([
paths.libDir,
paths.clientBundle,
]));
- Add
/dist/client-bundle.js*
to your.gitignore
file:
Next section: 8 - React
Back to the previous section or the table of contents.