CHAPTER 7
Chapter 7 ■ Modularization and WebpaCk
116
The contents of the new issue.js file are shown in Listing 7-1.
Listing 7-1. issue.js: New File for the Issue Model 'use strict';
const validIssueStatus = { New: true,
Open: true, Assigned: true, Fixed: true, Verified: true, Closed: true, };
const issueFieldType = { status: 'required', owner: 'required', effort: 'optional', created: 'required', completionDate: 'optional', title: 'required',
};
function validateIssue(issue) {
for (const field in issueFieldType) { const type = issueFieldType[field];
if (!type) {
delete issue[field];
} else if (type === 'required' && !issue[field]) { return `${field} is required.`;
} }
if (!validIssueStatus[issue.status])
return `${issue.status} is not a valid status.`;
return null;
}
module.exports = {
validateIssue: validateIssue };
■Note the first line, 'use strict';, is important. Without it, the behavior of const and let are different in node.js version 4.5. ensure that you have not missed this line.
Chapter 7 ■ Modularization and WebpaCk To use this new module in server.js, we must include the module that we just created, using the (by now familiar) require statement. When you refer to your own modules rather than modules installed via npm, you need to tell Node.js the path of the module’s file rather than just the name. So, in this case, we must use './issue.js' rather than a plain 'issue'. The changes are shown in Listing 7-2.
Listing 7-2. server.js: Changes to Using issue.js Module ...
const MongoClient = require('mongodb').MongoClient;
const Issue = require('./issue.js');
...
app.post('/api/issues', (req, res) => { ...
const err = Issue.validateIssue(newIssue);
...
We could have chosen to assign module.exports directly to validateIssue (like module.exports = validateIssue), in which case, the return value of the require statement in server.js would have been the function itself, and we could use it as is (like const validateIssue = require('./issue.js');). But we chose to use an object to enclose the exported function. The expectation is that we may export more things related to the Issue object later, and they can all be inside this object.
Finally, to let nodemon watch all the files in the new server directory, we need to modify the start command in package.json. Now that we have a directory for all of the server-side code, the -w argument can take the directory server as the value instead of a single file. Further, the starting point is server/server.js rather than simply server.js.
This change is listed in Listing 7-3.
Listing 7-3. package.json: Changes to Start Script ...
"start": "nodemon -w server.js server.js", "start": "nodemon -w server server/server.js", ...
If you restart the server (by pressing Ctrl-C in the console where it was started, and running npm start again), you should be able to test your changes. You need to do this because package.json itself changed, and it needed to be reloaded. The application should function as before, because we have only refactored the code. We have not introduced any functional changes.
Introduction to Webpack
Traditionally, one would split client-side JavaScript code into multiple files, and include them all (or whichever are required) using <script> tags in the main HTML file. This is less than ideal because the dependency management is done by the developer, by maintaining a certain order of files in the HTML file. Further, when the number of files becomes large, this becomes unmanageable.
Chapter 7 ■ Modularization and WebpaCk
118
Tools such as webpack and browserify provide alternatives that let you define dependencies as you would in a Node.js application using require or equivalent statements. They automatically figure out not just your own dependent modules, but also third-party libraries. Then, they put together these individual files into one or more bundles of pure JavaScript that has all the required code that can be included in the HTML file.
The only downside is that this requires a build step. But then, we already have a build step to transform JSX and ES2015 into plain JavaScript. It’s not much of a change in habit to let the build step also create a bundle based on multiple files. Both webpack and browserify are good tools and can be used to achieve the goals. But I chose webpack, because it is simpler to get all that we want done, which includes separate bundles for third-party libraries and our own modules. It has a single pipeline to transform, bundle, and watch for changes and generate new bundles as fast as possible.
If you choose Browserify instead, you will need other task runners such as gulp or grunt to automate watching and include multiple transforms. This is because Browserify does only one thing: bundle. In order to combine bundle and transform (using babel) and watch for file changes, you need something that puts all of them together, and gulp is one such utility. In comparison, webpack (with a little help from loaders, which we’ll explore soon) can not only bundle, but can also do many more things such as transforms and watching for changes to files. You don’t need additional task runners to use webpack.
Note that webpack can also handle other static assets such as CSS files. It can even split the bundles such that they can be loaded asynchronously. We will not be exercising those aspects of webpack; instead, we’ll focus on the goal of being able to modularize the client-side code, which is mainly JavaScript at this point in time.
Using Webpack Manually
To get used to what webpack really does, we’ll use webpack from the command-line just like we did for the JSX transform using babel command line. Let’s first install webpack:
$ npm install --save-dev webpack
We installed webpack locally because, just like with babel, we’ll eventually move all commands into commands defined in package.json, so that they can be run using npm run. We don’t need webpack globally. Let’s see what webpack does. You can run it on the client-side JavaScript file App.js and produce a bundle called app.bundle.js like this:
$ node_modules/.bin/webpack static/App.js static/app.bundle.js
Chapter 7 ■ Modularization and WebpaCk You can see that webpack creates app.bundle.js, which is not very different from App.js itself. Note also that we didn’t run it against the React file App.jsx, because webpack cannot handle JSX natively. What webpack did in this bundling is hardly interesting. We did it just to make sure we’ve installed it correctly and are able to run it.
To start the modularization exercise, let’s split App.jsx into two files by separating out one component, IssueAdd. Let’s create a new file called IssueAdd.jsx under the src directory, and move the entire IssueAdd class to this file. To include the new file, we could use the require style imports and exports as in the server-side modules. But ES2015 supports a new style with import statements, which are easier to read than require statements. We could not take advantage of this style for the server side-code because Node.js does not support this style natively as of the latest version at the time of writing this book.
Using the new ES2015 style to export a class is as simple as prefixing export before the class definition. Further, you can add the keyword default if you are exporting a single class, and you want it to be the result of an import statement directly (or a top-level export).
Listing 7-4 shows the changes to the class definition. The rest of the new file is the original contents of the class unchanged.
Listing 7-4. IssueAdd.jsx: New File, Class Contents Moved Here from App.jsx ...
export default class IssueAdd extends React.Component { ...
}
To import this class in App.jsx, we need to use an import statement right at the beginning of the file. This, as well as the removal of the IssueAdd class, is shown in Listing 7-5
Listing 7-5. App.jsx: Move IssueAdd Class and Import It ...
import IssueAdd from ‘./IssueAdd.js’;
...
class IssueAdd extends React.Component { constructor() {
super();
this.handleSubmit = this.handleSubmit.bind(this);
} ...
} ...
Chapter 7 ■ Modularization and WebpaCk
120
Let’s run the JSX transformation using npm run compile (or let the babel watch automatically recompile upon detecting changes). This will transform both files, since we’d specified the entire src directory as input source to babel. You’ll see that two files, App.js and IssueAdd.js, are generated. Now, when you run webpack with the same command as before, you will notice that it automatically includes IssueAdd.js in the bundle, without you ever telling it do so. Here is a sample result of a webpack run:
Time: 88ms
Asset Size Chunks Chunk Names app.bundle.js 11.7 kB 0 [emitted] main [0] ./static/App.js 7.06 kB {0} [built]
[1] ./static/IssueAdd.js 2.9 kB {0} [built]
You could create many more such files, and webpack does not need to be told about any of them. That’s because it looks at the import statements within each of the JavaScript files and figures out the dependency tree. Finally, we need to change the script file referenced in index.html to the new app.bundle.js instead of App.js. This change is shown in Listing 7-6.
Listing 7-6. index.html: Replace App.js Script with app.bundle.js ...
<script src="/app.bundle.js"></script>
...
At this point, you should test the application to see if it works as before. There will be some residual temporary files, App.js and IssueAdd.js, in the static directory, which you will not require any longer, so they can be removed.
Transform and Bundle
In the previous step, we had to manually transform JSX files, and then use webpack to bundle them together. The good news is that webpack is capable of combining these two steps, obviating the need for intermediate files. But it can’t do that on its own; it needs some helpers called loaders. All transforms and file types other than pure JavaScript require loaders in webpack. These are separate modules.
In this case, we need the babel loader to handle JSX transforms, so let’s install it:
$ npm install --save-dev babel-loader
Using loaders in the command line is cumbersome, especially if you want to pass parameters to the loaders (babel, in this case, needs presets react and es2015). You can instead use a configuration file to give these parameters to webpack. The default configuration file that webpack looks for is called webpack.config.js, so let’s create that file.
Chapter 7 ■ Modularization and WebpaCk Webpack loads this configuration file as a module and gets its parameters from this module. Everything has to be under one exported object. Since we won’t be transforming this file, let’s use the module.exports syntax rather than the ES2015 export syntax.
Inside the exported object, webpack looks for various properties. We’ll use the properties entry and output to replace what we did with the command line parameters until now.
The entry point is now App.jsx rather than App.js. The output property takes path and filename as two subproperties.
To add the babel loader, we need to define the configuration property
module.loaders and supply it with an array of loaders. We will have only one element, the babel loader in this array. A loader specification includes a test (regular expression) to match files, the loader to apply (in this case, babel-loader), and finally a set of options to the loader specified by the property query.
The final contents of the configuration file are shown in Listing 7-7.
Listing 7-7. webpack.config.js: Configuration File for webpack module.exports = {
entry: './src/App.jsx', output: {
path: './static',
filename: 'app.bundle.js' },
module: { loaders: [ {
test: /\.jsx$/,
loader: 'babel-loader', query: {
presets: ['react','es2015']
} }, ] } };
The option for babel loader is an array of presets, very similar to the babel command line:
...
query: {
presets: ['react','es2015']
} ...
The import statement in App.jsx referred to IssueAdd.js, but this file will no longer be created because webpack will transform IssueAdd.jsx to IssueAdd.js on the fly. So, for the dependency tree to be built correctly, we will have to import the pretransformed file with the jsx extension, as shown in Listing 7-8.
Chapter 7 ■ Modularization and WebpaCk
122
Listing 7-8. App.jsx: Import the jsx File Instead of js ...
import IssueAdd from './IssueAdd.jsx';
...
Now, webpack can be run on the command line without any command line parameters, like this:
$ node_modules/.bin/webpack
You will notice that it takes quite a while to complete. You don’t want to wait this long every time you modify a source file, do you? Nor do you want to run this command over and over again. Fortunately, just like babel, webpack has a watch mode, which looks for changes and processes only the changed files. So, run this command instead:
$ node_modules/.bin/webpack --watch
Modify some text (create a syntax error, for instance) and ensure that it rebuilds the bundle on every change, and note how long it takes to do this. Watch how errors are reported, too. Now is a good time to modify the npm script commands for building and watching for changed files. Let’s replace the compile and watch script specifications in package.json, as shown in Listing 7-9.
Listing 7-9. package.json: New Compile and Watch Scripts ...
"compile": "webpack", "watch": "webpack --watch", ...
Now that you know what webpack is capable of, let’s organize your files into one file per class: IssueList, IssueAdd, and IssueFilter. Let the stateless components IssueTable and IssueRow remain with IssueList in the same file. The entry file, App.jsx, will only import the other classes and mount the main component. This will create a two-level hierarchy of imports: App.jsx will import IssueList.jsx, which in turn will import IssueAdd.jsx and IssueFilter.jsx.
To start with, let’s move the placeholder class IssueFilter to its own file. This is shown in in Listing 7-10.
Listing 7-10. IssueFilter.jsx: Move Class IssueFilter Here and Add Export export default class IssueFilter extends React.Component { render() {
return (
<div>This is a placeholder for the Issue Filter.</div>
) } }
Chapter 7 ■ Modularization and WebpaCk Similarly, let’s take the class IssueAdd from the file App.jsx and create a new file called IssueAdd.jsx. Listing 7-11 shows the partial contents of the new file: the class as such is unchanged, except that in the class declaration you have the keywords export default.
Listing 7-11. IssueAdd.jsx: New File, Move Class IssueAdd Here, and Add Export export default class IssueAdd extends React.Component {
constructor() { super();
this.handleSubmit = this.handleSubmit.bind(this);
} ...
}
Listing 7-12 shows another change, but here we extract three classes (IssueRow, IssueTable, and IssueList) from App.jsx. The new file created is called IssueList.jsx.
Since this file needs the other extracted classes, IssueAdd and IssueFilter, we also need import statements for them in addition to the classes that are moved into this file. Only the class IssueList is exported, so we need to add the keywords export default only to that class declaration. The other two classes are for internal use only, so they are not exported.
Listing 7-12. IssueList.jsx: Move Classes IssueList, IssueTable, and Issue Row Here, and Add Import and Export
import IssueAdd from './IssueAdd.jsx';
import IssueFilter from './IssueFilter.jsx';
const IssueRow = (props) => ( <tr>
...
</tr>
)
function IssueTable(props) {
const issueRows = props.issues.map(issue => <IssueRow key={issue._id} issue={issue} />)
return ( ...
);
}
Chapter 7 ■ Modularization and WebpaCk
124
export default class IssueList extends React.Component { constructor() {
super();
this.state = { issues: [] };
this.createIssue = this.createIssue.bind(this);
} ...
}
Now that most of the contents of App.jsx have been moved out to their individual files, we’re left with just the rendering of the component. The contents of this file are shown in Listing 7-13.
Listing 7-13. App.jsx: Complete Contents After Moving All Classes Out import IssueList from './IssueList.jsx';
const contentNode = document.getElementById('contents');
ReactDOM.render(<IssueList />, contentNode); // Render the component inside the content Node
If you run npm run watch, you will find that all the compilations are done and the bundle is ready for you to test the application. If not, just start the npm start and npm run watch commands in different consoles, and then test the application to ensure that it behaves the same as before.