When I first started playing with web development, I was building toy websites with Microsoft FrontPage and ogling over the cool features on Dynamic Drive. It seemed like files were written by hand in text editors and published directly by copying them to FTP sites.
(Later I found out about PHP and server-rendered sites.)
The Web Platform was really hard to work with - particularly Javascript:
- Nearly everything was globally scoped
- Inter-module dependencies were not explicitly stated
- Modules were in danger of overwriting each others' variables
- Script files had to be explicitly listed in index.html
- If one file depended on each other, you had to list them in the correct order or else the page wouldn't work
- It didn't support classes (easily). If you wanted to use inherit tance, you had to figure out how to connect function prototypes together.
var
had terrible and surprising semantics, hoisting variables to the top of functions and being function-scoped// Create 10 buttons on the page for (var i = 1; i <= 10; i++) { var btn = document.createElement('button'); btn.onClick = function() { alert('clicked button ' + i); } document.body.appendChild(btn); } // Every single button generates "clicked button 10" because every callback uses the same `i` closure
We developed tools to cope with these problems: CoffeeScript and other transpilers smoothed out many of the language's rough edges, and bundlers like Webpack recycled NodeJS's module system to work on the web.
But Javascript hasn't remained stagnant; many of its original problems have been fixed:
- ECMAScript Modules can
import
each other, and their variable scopes are private by default. const
andlet
supplantedvar
- Classes are now supported (but then React adds function components + hooks so I rarely even use classes!)
- "Fat-arrow" lambda functions are more compact and don't use
- Microsoft has killed Internet Explorer and ships rebranded Chrome instead, so Lowest Common Denominator users are now using evergreen browsers supporting reasonably modern features.
With all that in place, do we still need all of our tools? They raise the barrier to entry: New developers are tasked with installing NodeJS, learning to use the command line to add dependencies, and learning how to use Webpack. Starter projects skip over a few steps, but they use built-in Webpack configurations that make it hard to add new Loaders, and they may contain outdated transitive dependencies that may conflict with that shiny new library that will help you deliver your feature on time.
I started out with Notepad (quickly moving to Notepad++ on Windows and Geany on Linux), and all I needed to do to test my code was open index.html
in Firefox. Shouldn't it be possible to do web development the same way again?
Why not actually use ES Modules directly in the browser now? Webpack taught us to export
functions from one module and import
them into another. If you declare your script as type="module"
, you don't need Webpack:
<!-- index.html -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello world!</title>
</head>
<body>
<h1>Hello world!</h1>
<button id="my-button">Click me!</button>
</body>
<script type="module">
import { sayHello } from './sayHello.mjs'; // !!! Important: Include the file extension! Webpack made this optional
document.querySelector('#my-button').onClick = () => sayHello();
</script>
</html>
// sayHello.mjs
export function sayHello() {
alert("Hello world!");
}
How about third party dependencies? It's still probably a good idea to cache them locally for production projects for the sake of reliability, but for small side projects where you can use a CDN, ESM.sh automatically turns many NPM packages into ES Modules.
Do we still need transpilers like Babel? If you're first starting out, you don't need your site to work on ES5-only browsers, so you can just use ES6 features directly.
What about JSX? Browsers don't support React's superset of Javascript. But do we really need it? You can just call createElement
directly:
// This requires a transpiler:
return (
<div>
<p><label>Username: <input name="Username" onChange={setUsername} value={username} /></label></p>
<p><label>Password: <input name="Password" onChange={setPassword} type="password" value={password} /></label></p>
<button className="btn btn-primary">Submit</button>
</div>
);
// This can run directly in a browser:
import { createElement as _ } from 'https://esm.sh/react'; // Use underscore to minimize visual clutter
return (
_('div', {},
_('p', {}, _('label', {}, 'Username: ', _('input', { name: 'Username', onChange: setUsername, value: username }))),
_('p', {}, _('label', {}, 'Password: ', _('input', { name: 'Password', onChange: setPassword, type: 'password', value: password }))),
_('button', { className: 'btn btn-primary' }, 'Submit'),
)
);
I've actually used this in a side project, and it isn't terrible to work with. The syntax is different, but you can get used to it. (Also, relevant XKCD)
This technique might even have an advantage over JSX for beginners. JSX is an entirely different syntax that obeys some of HTML's rules (like open/closing tags), but not all of them (quotes and ending tags are required). It suggests a mental model where you're creating "real HTML" inline in Javascript, when you're really building Javascript objects for React to reconcile later.
JSX doesn't always make it clear when you're operating under its special syntactical rules or Javascript's.
I mean, look at <div style={{ fontSize: '2em' }} />
. Why are there double {{
? The outer {}
"escape"
into Javascript and the inner {}
define an object literal.
Finally, types. I'm an avid fan of Typescript as a framework for designing the structure of my application and for its ability to catch serious issues before I even run code. If I could run Typescript directly in the browser, I'd do so.
For the time being, I'm settling for running Typescript in the background in checkJs: true
mode,
and adding JSDoc type annotations everywhere. JSDoc types are less flexible and than Typescript's
when it comes to templating, and they're more cumbersome for documenting interfaces.
To the second end, I'm still putting many of my interface declarations in separate .ts
files.
The browser doesn't need to load them, the Typescript language service can still find them,
and it's a much easier way to describe an interface.
Would I do this for work? No -- at least not yet.
Ideally, code files should be small and take care of a single concern, but users' browsers shouldn't be import
ing
a hundred tiny files just to load my employers' web app. Bundling all the files together makes the site load faster.
I'd also rather write "real" Typescript and JSX, the former for my own benefit and the latter for my teammates'.
But if someone asked me how websites worked, I might consider tryng to use this to teach them the fundamentals without getting lost in setting up a proper toolchain.