|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <!doctype html><!-- This is a valid HTML5 document. -->
- <!-- Screen readers, SEO, extensions and so on. -->
- <html lang="fr">
- <!-- Has to be within the first 1024 bytes, hence before the `title` element
- See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
- <meta charset="utf-8">
- <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
- <!-- The viewport meta is quite crowded and we are responsible for that.
- See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <!-- Required to make a valid HTML5 document. -->
- <title>Maintaining JavaScript applications in the long term (archive) — David Larlet</title>
- <meta name="description" content="Publication mise en cache pour en conserver une trace.">
- <!-- That good ol' feed, subscribe :). -->
- <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
- <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
- <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
- <link rel="manifest" href="/static/david/icons2/site.webmanifest">
- <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
- <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
- <meta name="msapplication-TileColor" content="#f7f7f7">
- <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
- <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
- <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
- <!-- Documented, feel free to shoot an email. -->
- <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
- <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <script>
- function toggleTheme(themeName) {
- document.documentElement.classList.toggle(
- 'forced-dark',
- themeName === 'dark'
- )
- document.documentElement.classList.toggle(
- 'forced-light',
- themeName === 'light'
- )
- }
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme !== 'undefined') {
- toggleTheme(selectedTheme)
- }
- </script>
-
- <meta name="robots" content="noindex, nofollow">
- <meta content="origin-when-cross-origin" name="referrer">
- <!-- Canonical URL for SEO purposes -->
- <link rel="canonical" href="https://9elements.com/blog/maintaining-javascript-applications-in-the-long-term/">
-
- <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
-
-
- <article>
- <header>
- <h1>Maintaining JavaScript applications in the long term</h1>
- </header>
- <nav>
- <p class="center">
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="https://9elements.com/blog/maintaining-javascript-applications-in-the-long-term/" title="Lien vers le contenu original">Source originale</a>
- </p>
- </nav>
- <hr>
- <p>In 2019, I wrote an article on <a href="https://9elements.com/blog/maintaining-large-javascript-projects/">maintaining large JavaScript applications</a>. As a follow-up, I’d like to describe a client project we are maintaining since 2014.</p>
- <h2 id="the-oecd-data-portal">The OECD Data Portal</h2>
- <figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://9elements.com/blog/content/images/2021/01/oecd-data-portal1.jpg" class="kg-image"><figcaption>Start page of the Data Portal</figcaption></figure>
- <p>The Organisation for Economic Co-operation and Development (OECD) is an intergovernmental body that collects data and publishes studies on behalf of its member states. The fields of work, amongst others, include economy, environmental issues, well-being or education.</p>
- <p>The <a href="https://data.oecd.org"><strong>OECD Data Portal</strong></a> is the central hub for statistical data. It helps researchers, journalists and policymakers to find meaningful data and to visualize it quickly with different charts. It connects with the <a href="https://www.oecd-ilibrary.org/">OECD iLibrary</a>, hosting the publications, and <a href="https://stats.oecd.org/">OECD.Stat</a>, storing the full data.</p>
- <p>The OECD is funded by its member states, so eventually by taxpayers like you and me. Using cost-effective, sustainable technologies was one of the requirements so the code can be maintained in the long term.</p>
- <p>The Data Portal is a joint work by OECD staff as well as external developers and designers. The initial design and prototyping came from <a href="https://truth-and-beauty.net">Moritz Stefaner</a> and <a href="https://raureif.net/">Raureif</a>. 9elements developed the production front-end code and still maintains it.</p>
- <h2 id="complex-javascript-codebase">Complex JavaScript codebase</h2>
- <p>The most complex part of the front-end is the JavaScript charting engine. It features ten main chart types with numerous configuration options. Using powerful interfaces, users can query the database and create charts for embedding or sharing.</p>
- <p>We started working on the Data Portal in 2014. Since then, there was no “big rewrite”, only new features, gradual improvements and refactoring. Recently, for the <a href="https://www.oecd.org/economic-outlook/">OECD Economic Outlook in December 2020</a>, we added several features, including four new chart types. Also we refactored the codebase significantly.</p>
- <p>In this article I’m going to describe how we maintained the code for so long and how we improved the code step by step. Also I will point out things that did not work well.</p>
- <h2 id="boring-mainstream-technology">Boring mainstream technology</h2>
- <p>When the project started in 2014, we chose plain HTML, XSLT templates, Sass for stylesheets and CoffeeScript as compile-to-JavaScript language. As JavaScript libraries, we chose jQuery, D3, D3.chart as well as Backbone.</p>
- <p>Back in 2014, these technologies were the safest, most compatible available. Only CoffeeScript was kind of a venture. We chose CoffeeScript because it made us more productive and helped us to write reliable code. But we were aware that it poses a liability.</p>
- <p>From 2015 on, 9elements has been using React for most JavaScript web applications. We considered to migrate the Data Portal chart engine to React, but we could not find the right moment. And in the end, it was probably a good decision to stick with the original stack.</p>
- <p>From today, the described JavaScript stack might seem outdated. But in fact the codebase stood the test of time. One reason is that the technologies we chose have aged well.</p>
- <h2 id="the-ravages-of-time">The ravages of time</h2>
- <p>While plenty of JavaScript libraries appeared and vanished, jQuery remains the most popular JavaScript library. It is robust, well-maintained and widely deployed. According to the <a href="https://almanac.httparchive.org/en/2020/javascript#libraries">Web Almanac 2020</a>, jQuery is used on 83% of all web sites. (For comparison, React was detected on 4% of all web sites.)</p>
- <p>Without doubt, jQuery has lost its dominance for non-trivial DOM scripting tasks. As mentioned, we would choose React or Preact for a project like the Data Portal today.</p>
- <p>The second library, <a href="https://d3js.org/">D3</a>, remains the industry standard when it comes to data visualization in the browser. It exists since 2010 and is still leading. While several major releases changed the structure and the API significantly, it is still an outstanding work of engineering.</p>
- <p>The <a href="https://backbonejs.org/">Backbone</a> library is not as popular, but has other qualities. Backbone is a relatively simple library. You can read the source code in one morning and could re-implement the core parts yourself in one afternoon. Backbone is still maintained, but more importantly it is feature-complete.</p>
- <p>From today’s perspective, only CoffeeScript poses a significant technical debt. CoffeeScript was developed because of blatant deficits in ECMAScript 5. Later, many ideas from CoffeeScript were incorporated into the ECMAScript 6 (2015) and ECMAScript 7 (2016) standards. Since then, there is no compelling reason to use CoffeeScript any longer.</p>
- <p>We picked CoffeeScript in 2014 because its philosophy is “it’s just JavaScript”. In contrast to other languages that compile to JavaScript, CoffeeScript is a straight-forward abstraction. CoffeeScript code compiles to clean JavaScript code without surprises.</p>
- <p>Today, most companies have migrated their CoffeeScript codebases to modern JavaScript. That’s what we did as well.</p>
- <h2 id="from-coffeescript-to-typescript">From CoffeeScript to TypeScript</h2>
- <p>Using the <a href="https://github.com/decaffeinate/decaffeinate">decaffeinate</a> tool, we converted the CoffeeScript code to ECMAScript 6 (2015). We still wanted to support the same browsers, so we now use the Babel compiler to produce backwards-compatible ECMAScript 5.</p>
- <p>All in all, this migration went smoothly. But we did not want to stop there.</p>
- <p>In new projects, 9elements is using TypeScript. In my opinion, TypeScript is best thing that happened in the JavaScript world in the last couple of years. As I mentioned in my previous article, <a href="https://9elements.com/blog/maintaining-large-javascript-projects/#avoid-creating-untyped-objects">TypeScript forces you to think about your types</a> and name them properly.</p>
- <p>For the Data Portal, we wanted to have the development benefits of TypeScript without converting the codebase to fully typed TypeScript.</p>
- <p>TypeScript is a superset of JavaScript. The compiler understands .js files pretty well. So we gradually added type annotations with a 20-year-old technology: <a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">JSDOC</a>. In addition, we wrote few typings in .ts files in order to reference them in the JSDOC annotations.</p>
- <p>This way, the developing experience in Visual Studio Code improved greatly with little effort. While there is no strict type checking, code editing feels as good as in an average TypeScript project.</p>
- <p>By combining a rather boring but rock-solid technology with the latest TypeScript compiler, we could add new features and refactor the code safely and easily.</p>
- <p>On the surface, coding is a conversation between you and the computer: You tell the computer what it should do.</p>
- <p>But more importantly, coding is a conversation between you and the reader of the code. It is a well-known fact that code is written once but read again and again. First and foremost, the reader is your future self.</p>
- <p>The Data Portal codebase contains many comments and almost all proved valuable during the last six years. Obviously, code should be structured to help human readers understanding it. But I do not believe in “self-descriptive” or “self-documenting” code.</p>
- <p>Before we switched to JSDOC, we had human-readable type annotations, documented function parameters and return values. Also we documented the main data types, complex nested object structures.</p>
- <p>These human-readable comments proved to be really helpful six years later. We translated them into machine-readable JSDOC and type declarations for the TypeScript compiler.</p>
- <h2 id="things-will-break-have-a-test-suite">Things will break – have a test suite</h2>
- <p>The project has only a few automated unit tests, but more than 50 test pages that demonstrate all Data Portal pages, components, data query interfaces, chart types and chart configuration options. They test against live or staging data, but also against fabricated data.</p>
- <p>These test pages serve the same purpose as automated tests: If we fix a bug, we add the scenario to the corresponding test page first. If we develop a new feature, we create a comprehensive test page simultaneously.</p>
- <figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://9elements.com/blog/content/images/2021/01/oecd-data-portal4.jpg" class="kg-image"><figcaption>Test page for the line chart responsiveness</figcaption></figure>
- <p>Before a release, we check all test pages manually and compare them to the last release – both visually and functionally. This is time-consuming, but it lets us find regressions quickly.</p>
- <p>I don’t think an automated test suite would serve us better. It is almost impossible to test interactive data visualizations in the browser in an automated way. Visual regression testing is a valuable tool in general, but would produce too many false positives in our case.</p>
- <h2 id="backward-and-forward-compatibility">Backward and forward compatibility</h2>
- <p>In 2014, the Data Portal had to work with Internet Explorer 9. Today, Internet Explorer has no importance when you develop a dynamic, in-browser charting engine.</p>
- <p>We decided to keep the compatibility with old browsers. The Data Portal is an international platform, so users visit from all over the world. They do not have the latest hardware and newest browsers.</p>
- <figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://9elements.com/blog/content/images/2021/01/oecd-data-portal5-ie9.jpg" class="kg-image"><figcaption>The Data Portal in Internet Explorer 9</figcaption></figure>
- <p>We accomplished to maintain the browser support baseline by using boring standard technologies. We do use several modern web features. But we apply <a href="https://en.wikipedia.org/wiki/Progressive_enhancement">Progressive Enhancement</a> to activate them only if the browser supports them. Also we use Babel and polyfills to make the modern JavaScript features work in old browsers.</p>
- <h2 id="your-abstractions-will-bite-you">Your abstractions will bite you</h2>
- <p>The technology stack was not the limit we faced in this project over the years. It was rather the abstractions we created our own that got in our way.</p>
- <p>We divided the user interface into views and created a base class similar to Backbone.View. (Today, all big JavaScript libraries use the term “component” instead of “view” for parts of the UI.) For holding the data and the state, we used Backbone.Model. This worked quite well, but we should have stuck to our own best practices.</p>
- <p>The idea of Backbone’s model-view separation is to have the model as the single source of truth. The DOM should merely reflect the model data. All changes should originate from the model. Modern frameworks like React, Vue and Angular enforce the convention that the UI is “a function of the state”, meaning the UI is derived from the state deterministically.</p>
- <p>We violated this principle and sometimes made the DOM the source of truth. This led to confusion with code that treated the model as authoritative.</p>
- <h2 id="object-oriented-charts">Object-oriented charts</h2>
- <p>For the charts, we chose yet another approach. We created chart classes not based on the view class described above.</p>
- <p>D3 itself is functional. A chart is typically created and updated with a huge <code>render</code> function that calls other functions. The chart data is the input for this large function. More state is held in specific objects.</p>
- <p>This makes D3 tremendously expressive and flexible. But D3 code is hard to read since there are little conventions on the structure of a chart.</p>
- <p>Folks at Bocoup, Irene Ros and Mike Pennisi, invented <a>d3.chart</a>, a small library on top of D3 that introduced class-based OOP. Its main goal was to structure and reuse charting code. These charts are made of layers. A layer renders and updates a specific part of the DOM using D3. Charts can have other charts attached.</p>
- <p>A general rule of OOP is “favor composition over inheritance”. Unfortunately, we used a weird mix of composition <em>and</em> inheritance to mix chart behavior.</p>
- <p>We should have used functions or simple classes instead of complex class hierarchies. People still wrap D3 in class-based OOP today, but no class-based solution has prevailed against D3’s functional structure. </p>
- <h2 id="summary">Summary</h2>
- <p>Since we designed the Data Portal front-end architecture in 2014, powerful patterns for JavaScript-driven web interfaces have emerged.</p>
- <p>Instead of rendering string-based HTML templates and updating the DOM manually, UI components are declarative nowadays. You simply update the state and the framework updates the DOM accordingly. This unidirectional data flow eliminates a whole class of bugs.</p>
- <p>The technologies we picked in 2014 either stood the test of time or offered a clear migration path. You could say we were lucky, but together with the client, we also chose long-lasting technologies deliberately.</p>
- <p>At 9elements, we take pride in using cutting-edge technologies. This includes assessing experimental front-end technologies that probably are not maintained any longer in three to four years from now. Unfortunately, many open source JavaScript projects are technically groundbreaking yet prove to be unsustainable.</p>
- <p>For each client project, we seek the right balance between well-established, zero-risk technologies as well as innovative technologies that help us to deliver an outstanding product in time.</p>
- <p><a href="https://9elements.com/contact"><strong>We're open for business so feel free to contact us for your next project.</strong></a></p>
- <h2 id="acknowledgments">Acknowledgments</h2>
- <figure class="kg-card kg-image-card"><img src="https://9elements.com/blog/content/images/2020/12/maintaining-large-javascript-projects.svg" class="kg-image"></figure>
- <p>Thanks to <a href="https://dribbble.com/LittleSue">Susanne Nähler</a>, designer at 9elements, for creating the teaser illustration.</p>
- <p>Thanks to the kind folks at OECD for the great collaboration over the course of the last six years.</p>
- </article>
-
-
- <hr>
-
- <footer>
- <p>
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
- </svg> Suivre</a> •
- <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
- </svg> Pro</a> •
- <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
- </svg> Email</a> •
- <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
- </svg> Légal</abbr>
- </p>
- <template id="theme-selector">
- <form>
- <fieldset>
- <legend><svg class="icon icon-brightness-contrast">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
- </svg> Thème</legend>
- <label>
- <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
- </label>
- <label>
- <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
- </label>
- <label>
- <input type="radio" value="light" name="chosen-color-scheme"> Clair
- </label>
- </fieldset>
- </form>
- </template>
- </footer>
- <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
- <script>
- function loadThemeForm(templateName) {
- const themeSelectorTemplate = document.querySelector(templateName)
- const form = themeSelectorTemplate.content.firstElementChild
- themeSelectorTemplate.replaceWith(form)
-
- form.addEventListener('change', (e) => {
- const chosenColorScheme = e.target.value
- localStorage.setItem('theme', chosenColorScheme)
- toggleTheme(chosenColorScheme)
- })
-
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme && selectedTheme !== 'undefined') {
- form.querySelector(`[value="${selectedTheme}"]`).checked = true
- }
- }
-
- const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
- window.addEventListener('load', () => {
- let hasDarkRules = false
- for (const styleSheet of Array.from(document.styleSheets)) {
- let mediaRules = []
- for (const cssRule of styleSheet.cssRules) {
- if (cssRule.type !== CSSRule.MEDIA_RULE) {
- continue
- }
- // WARNING: Safari does not have/supports `conditionText`.
- if (cssRule.conditionText) {
- if (cssRule.conditionText !== prefersColorSchemeDark) {
- continue
- }
- } else {
- if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
- continue
- }
- }
- mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
- }
-
- // WARNING: do not try to insert a Rule to a styleSheet you are
- // currently iterating on, otherwise the browser will be stuck
- // in a infinite loop…
- for (const mediaRule of mediaRules) {
- styleSheet.insertRule(mediaRule.cssText)
- hasDarkRules = true
- }
- }
- if (hasDarkRules) {
- loadThemeForm('#theme-selector')
- }
- })
- </script>
- </body>
- </html>
|