.. title: A Simple, Understandable Production Ready Frontend Project Setup .. slug: a-simple-understandable-production-ready-frontend-project-setup .. date: 2019-07-14 11:08:47 UTC .. tags: .. category: .. link: .. description: .. type: text I have low tolerance for complexity, I also like to understand the tools I use. That's why over the years I've developed project setups myself after seeing towers of complex "boilerplate" fell apart once I tried to do some customization, or worse, for no apparent reason other tan bitrot or setting up the project on another machine. Before the setup I will show you here I used to use `requirejs `_ as module system and the lowest common denominator of js features for development, the release step involved just minifying the module hierarchy. With browsers keeping themselves up to date automatically and many "advanced" js features being available on them I started new projects by using some advanced features during development that are supported by the browsers I use to develop (Firefox and Chrome) and then only for the release I transpile those features to ES5. I see a point in the near future where if we can target Chrome-based Edge and evergreen Firefox, Safari and Chrome, I can skip the tranpilation step and ship ES6 directly. Here's the setup I use for my projects as an attempt to show a simpler alternative to current practices, or maybe to show how all the parts fit together by making them ourselves. Initial Setup ------------- Let's start by creating the folder for our project: .. code-block:: sh mkdir myapp cd myapp Let's create the basic structure: .. code-block:: sh mkdir js css lib img Folder usage: js Our Javascript code css CSS files lib Libraries we use img Images, you can call it assets and put fonts and other things there too Let's create our `index.html` with the following content: .. code-block:: html My App
The file already gives you some ideas of the next steps, you may have noticed that the script tag has `type="module"` in it, that's because `(new Date()).getFullYear() >= 2019` and we are going to use `Javascript Modules `_ supported `by all evergreen browsers `_ and easily translatable to old javascript syntax with a single command. Let's create a file at `js/app.js` with the following content: .. code-block:: js function main() { document.getElementById('app').innerHTML = 'Hello World'; } // call main when the page completed loading window.addEventListener('load', main); Now we can try that it works by starting a server that will serve the files, I use python's builtin HTTP server when I'm starting, you can use others: If you have python 2.x installed: .. code-block:: sh python -m SimpleHTTPServer If you have python 3.x installed: .. code-block:: sh python3 -m http.server Now open your browser at http://localhost:8000/ Let's see how to use modules, let's create a module at `js/util.js` and use it from `js/app.js`, write the following in `js/util.js`. .. code-block:: js function byId(id) { return document.getElementById(id); } function setNodeText(node, text) { node.innerText = text; } export {byId, setNodeText}; And change `js/app.js` to use our new util module: .. code-block:: js import {byId, setNodeText} from './util.js'; function main() { let node = byId('app'); setNodeText(node, 'Hello World!'); } window.addEventListener('load', main); You can read more about `import` and `export` syntax on the `MDN page about import `_ and the `MDN page about export `_. Now let's add bootstrap to it: .. code-block:: sh wget https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css -O css/bootstrap.css Yes, I just downloaded it to the css folder, I don't change my deps so often, when I do I want to do it manually and check that everything works, I also want my workflow to work when I don't have an internet connection and be fully versioned at every point so I can checkout any commit and dependencies will be at the right version. Building a Release ------------------ To build a release we need to bundle all our modules together and minify it, to bundle them we are going to use `rollup `_ from the command line, first we need to install it if we don't have it: .. code-block:: sh npm install -g rollup Then to use it: .. code-block:: sh # remove dist if it existed rm -rf dist # create it again mkdir dist # bundle app.js and all it's dependencies into dist/bundle.js rollup js/app.js --file dist/bundle.js --format iife -n app .. note:: This post assumes you have `node.js `_ and `npm `_ installed. You can open `dist/bundle.js` and confirm that all our code is there as a single vanilla js file. Now we want to minify it, for that we will use `uglify-es`, first we need to install it: .. code-block:: sh npm install -g uglify-es Then we can use it: .. code-block:: sh uglifyjs dist/bundle.js -m -o dist/bundle.min.js You can open `dist/bundle.min.js` and confirm that it's our code but minified. What if we want to use new js features? One option is to just use the features available on our minimum common denominator browser (which if we target Edge and evergreen Firefox and Chrome it's a lot!) and our build step stays as is (es modules -> rollup -> uglify-es), the alternative if we are targeting older browsers (I hope it's just IE 11) is to transpile our new javascript into an older version like ES5, we can achieve that with babel, first we install it: .. code-block:: sh npm install babel-cli babel-preset-es2015 We put a file at the root of our project called `.babelrc` to tell babel what options we want to use: .. code-block:: js {"presets": ["babel-preset-es2015"]} And then use it: .. code-block:: sh babel dist/bundle.js --minified -o dist/bundle.babel.js Let's modify our code a little to see the transpiling in action, change the `main` function in `js/app.js` to look like this: .. code-block:: js function main() { let node = byId('app'), items = [1, 2, 3, 4]; setNodeText(node, items.map(v => '#' + v)); } We are using arrow functions in `items.map`, now let's do the build: .. code-block:: sh rm -rf dist mkdir dist rollup js/app.js --file dist/bundle.js --format iife -n app babel dist/bundle.js --minified -o dist/bundle.babel.js # notice that we are now minifying the output of babel uglifyjs dist/bundle.babel.js -m -o dist/bundle.min.js If you check `dist/bundle.babel.js` you will notice that the `let` turned into `var` and the arrow function turned into a anonymous function. You will also notice that the output from babel is already sort of minified, we can let it as is or pass it through uglify, that in my experience makes the output even smaller. In my case the outputs here are 312 bytes for babel and 231 for uglify. There's a final step in the build process, if you remember our script tag said `type="module"` and our build is no longer using modules, so we need to get rid of it, let's do that: .. code-block:: bash sed 's/type="module" //g' index.html > dist/index.html Done, we just remove `type="module"` in `index.html` and put the result in `dist/index.html` So, now we have a production ready build process in 5 commands, let's work with dependencies. Let's try using react, it will be similar with vue (even simpler), but I want to show how to use react without fancy things: .. code-block:: bash wget https://unpkg.com/react@16.8.6/umd/react.production.min.js -O lib/react.js wget https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js -O lib/react-dom.js Yes, I just downloaded the minified versions from `cdnjs `_. Now let's bundle all our dependencies: .. code-block:: bash cat lib/react.js lib/react-dom.js > lib/deps.js If you check index.html is already loading `lib/deps.js` before our `app.js` so it will just work. Our build needs an extra step, that is: copying the css, libs and images into the build directory: .. code-block:: bash mkdir -p dist/lib dist/css cp lib/deps.js dist/lib/ cp css/bootstrap.css dist/css/ cp -r img dist/ Let's use react without a build step (aka no JSX), change `js/app.js` to look like this: .. code-block:: js import {byId} from './util.js'; import {render, div, span, button} from './dom.js'; // global state const STATE = {counter: 0}; function counterClicked(state) { state.counter += 1; doRender(); } function renderRoot(state) { return div( {}, span({}, 'Counter: ', state.counter), button({onClick: _e => counterClicked(state)}, 'Increment') ); } let rootNode; function doRender() { render(renderRoot(STATE), rootNode); } function main() { rootNode = byId('app'); doRender(); } window.addEventListener('load', main); Here I'm doing a simplification with `doRender` calls, event handling and state management to avoid introducing any other library and make the example more complex, what you use for state management is up to you. The important part is the `renderRoot`, it's all vanilla js calls, I don't have to learn a new syntax or mix two different syntaxes, I don't need a compiler running, I can use everything I know about javascript for my render code. Let's see the "magic" behind the `dom.js` module: .. code-block:: js /*globals React, ReactDOM*/ const c = React.createFactory.bind(React), div = c('div'), span = c('span'), button = c('button'), render = ReactDOM.render; export {render, div, span, button}; Yep, that's all, of course in my real `dom.js` module I have all the HTML and SVG tags instead of just 3. Now you may want to automate this instead of copying and pasting the commands, I have a `Makefile` that does the job for me, you can put it in shell scripts or whatever works for you, here's a `Makefile` for this project: .. code-block:: makefile setup: npm install rollup uglify-es babel-cli babel-preset-es2015 mkdir -p js lib css img wget https://unpkg.com/react@16.8.6/umd/react.production.min.js -O lib/react.js wget https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js -O lib/react-dom.js cat lib/react.js lib/react-dom.js > lib/deps.js wget https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css -O css/bootstrap.css build: rm -rf dist mkdir -p dist/lib dist/css dist/js sed 's/type="module" //g' index.html > dist/index.html rollup js/app.js --file dist/bundle.js --format iife -n app babel dist/bundle.js --minified -o dist/bundle.babel.js uglifyjs dist/bundle.babel.js -m -o dist/bundle.min.js cp dist/bundle.min.js dist/js/app.js cp lib/deps.js dist/lib/ cp css/bootstrap.css dist/css/ cp -r img dist/ serve: python3 -m http.server The "workflow" is, run `make setup` only once, change and reload as needed, when you want to ship, run `make build`, you can do `cd dist; make serve` and opening `http://localhost:8000/` to check that it works. To deploy remove `dist/bundle*.js` and copy the `dist` folder to its destination. You may think that I'm lying and telling you something I don't use, well, this is the same workflow I use to build 4 apps at `instadeq `_. The main differences are: * I have a target in the makefile called `setup-dev` that fetches the non minified versions of the libraries to diagnose errors easily and be able to step into library code. * I have some extra dependencies That's all, well... I also use `eslint `_ and `prettier `_, but that's in my editor config and not in the project. But.. live reloading? I used to have it, basically by storing the current state in the window object, reloading the script tag and restoring the state if it was in the window object at startup (it was less than 30 lines of code), now I develop in a notebook style so I create an example of the thing I want to implement and just reload, the reload is fast and the example is in the initial state I want, no need to reimplement live reloading yet. So there it is, a simple, modern, understandable and production ready setup for your frontends.