Bundling ToDoMVC with WebPack¶
We now have the frontend moved out of the back end, served by a static web server at a different URL. Great! Let’s use Webpack to bundle our JavaScript and run under its development server, giving us hands-free browser reloading.
Webpack gives us browser-side module loading, so we’ll switch app.js
and
todo.js
to use CommonJS modules.
Steps¶
From the previous step, make a virtual env if necessary, then install if needed the npm and Python dependencies.
Hook up
ESLint
in preferences if necessary.Install our new dependencies:
npm install --save-dev webpack webpack-dev-server
Update our HTML file, replacing
<script>
nodes forjquery.js
,app.js
, andtodo.js
with a singlebundle.js
:<!DOCTYPE html> <html> <head> <title>Flask ToDoMVC</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css"/> <link rel="stylesheet" href="app.css"/> <link rel="icon" type="image/x-icon" href="favicon.ico"/> </head> <body class="container"> <div class="col-md-6"> <h1>ToDos</h1> <p><input id="newName" placeholder="Add todo..." class="form-control"/></p> <div id="todoList"> <ul></ul> </div> </div> <script src="bundle.js"></script> <script type="text/html" id="list_todos"> <ul class="list-group"> <% for ( var i = 0; i < todos.length; i++ ) { %> <li class="list-group-item" id="<%= todos[i].id %>"> <span><%= todos[i].name %></span> <input class="form-control input-sm" title="Edit title" value="<%= todos[i].name %>"/> <div class="btn-group pull-right" role="group"> <button class="btn btn-xs btn-default edit">Edit</button> <button class="btn btn-xs btn-default delete">Delete</button> </div> </li> <% } %> </ul> </script> </body> </html>
tmpl.js
simply moves the template code out ofapp.js
, into an export:// John Resig jQuery Microtemplating /*eslint-disable */ module.exports = function tmpl (str, data) { // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var cache = {}; var fn = !/\W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + "');}return p.join('');"); // Provide some basic currying to the user return data ? fn(data) : fn; }; /*eslint-enable */
app.js
gets rid of the IIFE, imports jQuery, and importstodo.js
:var $ = require('jquery'), ToDos = require('./todo'); $(document).ready(function () { // All REST requests should send content type, and log failures $.ajaxSetup({contentType: 'application/json'}); $(document).ajaxError(function (event, jqxhr, settings, thrownError) { console.error('Ajax call failed:', settings.type, settings.url, thrownError); }); });
todo.js
also gets rid of the IIFE. It does not domodule.exports
, which seems kind of weird. But that’s because it does its work on jQuery$(document).ready
and we don’t want to refactor too much in this step. It does, though, importjQuery
andtmpl.js
at the top:var $ = require('jquery'), tmpl = require('./tmpl'); $(document).ready(function () { var newName = $('#newName'), todoList = $('#todoList'), template = tmpl('list_todos'); var todos; function refreshToDos () { /* Fetch the list of todos and re-draw the listing */ $.get('http://localhost:5000/api/todo', function (data) { todos = data['objects']; todoList.find('ul') .replaceWith(template({todos: todos})); }); } // Create a new to do newName.change(function () { var payload = JSON.stringify({name: newName.val()}); $.post('http://localhost:5000/api/todo', payload, function () { refreshToDos(); newName.val(''); }) }); // Edit a to do todoList.on('click', '.edit', function () { // Toggle the <input> for this todo todoList.find('li').removeAttr('editing'); var li = $(this).closest('li').first().attr('editing', '1'); }); todoList.on('change', 'input', function () { // When the revealed-input changes, update using PATCH var todoId = $(this).closest('li')[0].id, data = JSON.stringify({name: $(this).val()}); $.ajax({url: 'http://localhost:5000/api/todo/' + todoId, type: 'PATCH', data: data}) .done(function () { refreshToDos(); }); }); // Delete an existing to do todoList.on('click', '.delete', function () { var todoId = $(this).closest('li')[0].id; $.ajax({url: 'http://localhost:5000/api/todo/' + todoId, type: 'DELETE'}) .done(function () { refreshToDos(); }); }); // On startup, go fetch the list of todos and re-draw refreshToDos(); });
Let’s add a line to
package.json
‘s section forscripts
, to start the dev server, based inapp/
:{ "name": "flask_todo", "version": "1.0.0", "description": "", "main": "index.js", "directories": { "doc": "doc" }, "scripts": { "lint": "eslint todo/static/*.js", "start": "webpack-dev-server --content-base app/" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "jquery": "^2.2.0" }, "devDependencies": { "eslint": "^1.10.3", "webpack": "^1.12.12", "webpack-dev-server": "^1.14.1" } }
We need a (simple)
webpack.config.js
to drive the dev server:module.exports = { context: __dirname + '/app', entry: './app.js', devtool: 'source-map' };
Start Flask. Right-click on
run_server.py
and run it.Run dev server. Use PyCharm’s
npm run
too window to runstart
, then visithttp://localhost:5000/
.
And now we’re in business! To emphasize the workflow, let’s resize PyCharm and Chrome so they can be side-by-side. As you type, you will see the updates automatically.
- Previous topic: Moving the Frontend Out of the Backend
- Next topic: ES6 Imports for ToDoMVC