/dev/random

Using Vue.js without webpack

Have you ever wondered if you can use Javascript frameworks like Vue.js by accessing them directly in your webpage, without having to transpile or bundle them before deploy? I have, and here is the result of my first test.

The problem

Most of today's Javascript frameworks need some sort of bundling via Webpack or other similar technology, and the step from there to transpiling the latest alpha version of Ecmascript, preprocessing CSS via SASS/SCSS/LESS/..., transpiling new languages like JSX and finally forcing you to adopt Node.js because "isomorphism", is quite short.

I believe this trend is getting out of hands, and hinders the adoption of other, better suited, server-side technologies, like PHP, Go, Rust or others. It also forces you to configure bundlers, then configure a "watch dev" to develop, and keep it in line with the "prod bundle", and remember to build before deploy, etc. etc.

Sometimes it can be necessary, but, in my opinion, having an "old style" quick and easy solution to develop is a better way to go.

The solution

Recently I (and my colleagues) am experimenting with Vue.js to build a backend admin panel.

Vue.js is available as an npm module, but also as a standalone, CDN served or downloadable, normal javascript file.

In our case, we also chose Bootstrap and FontAwesome, as an easy way to style the interface. We are not using the js part of Bootstrap, only the CSS.

And we are also using Axios for the AJAX calls, but this is a different story.

In the past I used require.js extensively to create a Backbone+Marionette based backend, so I was curious to see if I could adopt the same solution with Vue, and it turns out it works.

The basic structure

You are free to structure your application however you want. There are few conventions to abide to, mostly with require.js, to simplify modules loading.

This is a basic structure I will be using for this post:

.
|-- css
|   |-- bootstrap.css
|   `-- font-awesome.css
|-- fonts
|   |-- FontAwesome.otf
|   |-- fontawesome-webfont.eot
|   |-- fontawesome-webfont.svg
|   |-- fontawesome-webfont.ttf
|   |-- fontawesome-webfont.woff
|   `-- fontawesome-webfont.woff2
|-- js
|   |-- components
|   |   |-- greeting.js
|   |   `-- target.js
|   |-- main.js
|   |-- require.js
|   `-- vue.js
`-- index.html

The index.html is pretty straightforward:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>My wonderful site</title>

    <link rel="stylesheet" href="css/bootstrap.css">
    <link rel="stylesheet" href="css/font-awesome.css">
    <link rel="stylesheet" href="css/style.css">

    <!-- <script src="js/vue.js"></script> -->
    <script data-main="js/main" src="js/require.js"></script>
</head>
<body>
    <div id="main"></div>
</body>
</html>

You can choose to load Vue.js globally by uncommenting the <script> part in this HTML template, or decide to load it via require.js in every component that needs it.

Any library distributed in AMD or UMD format can be included via require.js directly, so for the the sake of showing how to do it (and for explicit dependency injection), in this post I will use require.js.

Whet this code does is setup a base, empty web page, downloading require.js synchronously, and then executing it, as normal.

Require.js will look into its own <script> tag and find the data-main attribute, and use it as the core script to load. It will automatically append .js at the end, so in this case it will load js/main.js.

The main entry point

There are two ways in which you can use require.js: with require or with define.

An example of the first syntax is the following:

require(['components/component1'], function(comp1) {
  // use comp1
});

But this syntax tends to become very verbose when you load 6 or 7 components.

The syntax I prefer is via define, and this is the way my main.js is defined:

define(function (require) {
    let Vue = require('vue');
    require('components/greeting');

    new Vue({
        el: '#main',
        data: {
            msg: 'Hello',
        },
        template: `
            <div>
                <header>
                    <h1>Site title</h1>
                </header>
                <main>
                    <greeting :greet="msg" to="World"></greeting>
                </main>
            </div>
        `,
    });
});

A little explanation: define called in this way allows to pass a function that accepts a single argument, that is the require function itself, and which can then be used inside the code to include other scripts.

We use it immediately to load Vue and assign it to the Vue variable, the standard name used in the Vue examples and as the global name when imported directly. You can remove this if you decided to import Vue globally in the HTML.

We use it again to load the first component, greeting, which is inside the components directory. We won't use this component directly, so we don't need to assign it to a variable, but it could be done for more advanced uses of Vue.js.

Paths are relative to the directory containing the file declared in the data-main attribute of the require.js include. You can configure the basePath in require.js to change this setting, but for out purpouse it's ok.

The application gets connected to the element with id main in the HTML (remember the <div id="main"></div> earlier).

This also shows how to define the model attached to the app (via the data attribute). In this case it contains only one variable: msg.

The template attribute defines the HTML code to use for the main app, and we can easily use the component we just imported. We are using the ES5 literal templating syntax here, that should be well supported by most modern browsers, but you may want to go back to normal concatenated strings for maximum compatibility. The code in this post works perfectly without any shim or polyfill on Firefox.

In the component line we used two different ways to pass informations to the component.

The first is by binding a local data to a component attribute. The :msg="msg" attribute means: bind (:) the component's greet attribute to my local msg variable.

The second is by just passing a string. The to="world" means: pass to the component's to attribute the value World.

More informations about the require.js configuration can be found on the Require.js API docs.

The first component

The definition of a component (or of any other included file that's not the main entry point) is almost identical, but the component must be declared for Vue with a different syntax:

define(function (require) {
    let Vue = require('vue');
    require('components/target');

    let template = `<div>
        {{ greet }}
        <target :destination="to" :style="{color: color}"></target>
    </div>`;

    Vue.component('greeting', {
        props: {
            greet: {
                default: 'Hi'
            },
            to: {
                default: 'Mars',
            }
        },
        data: () => ({
            color: 'red'
        }),
        template: template,
    });
});

This, again, loads Vue and then requires a second component (note the path, relative to the directory containing main.js, and not relative to the directory containing this file).

In this case we defined the template outside the component, just to show how it can be done.

This template uses both "model variables" we are going to define in the component, in different ways: the first is via textual interpolation: {{ variable_name }} which will print whatever value the variable will contain when the component gets rendered (or re-rendered) by Vue, and the second is, as before, by binding the secondo component's attribute (destination) to a local variable (to)

In a Vue component the props attribute defines a list of attributes the component accepts when called from a parent container. In our case we define a greet prop with a default value of Hi and a to with a default of Mars.

Data models must be defined as a function in components, so here we define a short-syntax function that will return an object with all the models. In our case just a color variable containing the value red.

The second component

This is the simplest one:

define(function (require) {
    let Vue = require('vue');

    let template = '<span>{{ destination }}</span>';

    Vue.component('target', {
        props: ['destination'],
        template: template,
    });
});

Nothing fancy compared to the previous ones, just a different way to declare props.

What's gained

Simplicity

It's a lot easier and faster to start a project in this way. You don't need to setup anything, just download require.js, vue.js and possibly some other library, save them in a project, include them and start working.

The project doesn't need compiling. When you change a file you just go back to the browser and refresh the page.

Better modularisation

You can load only the required components in every page, so you can organise your site as a single page application with a client-side router, but you can also have separate pages and don't need to generate multiple bundles (and find a way to simulate a SPA while you develop with watch-compilation).

Fewer dependencies

You save a lot of space. A basic installation of all the npm modules required to transpile and pack the project can take over 300 MB.

You have fewer dependencies to keep up-to-date, but on the other side you must update them by hand.

You can use your preferred language on the server and don't need a full node.js installation to compile or to run the project.

What's lost

.vue modules

The biggest downside of this approach is that vue components cannot be defined with the .vue technique, as they cannot be parsed (for now?) on the client, and therefore css styles cannot be easily scoped to a component.

On the other side, templates can be separated completely from components, by using, for example, the Requirejs Text loader plugin.

No transpiling

There could be difficulties in loading Vue plugins that are not distributed in AMD or UMD format, or if they use ES6 syntax not supported by browsers.

If you want to use ES6 syntax, you need to be careful with browser support, as there is no transpiling phase.

You can't use CSS preprocessors if they don't provide a client-side compiler.

No hot-reloading of parts of the page, but in my experience this never works well for long.

Performance

I have no benchmark to test if this solution is faster or slower compared to a bundled version.

It is probably much slower over HTTP1 if no ES5/ES6 shims/polyfills get included in the bundled version.

It could be faster when using HTTP2, as there would be no handshake for multiple files, and they could be downloaded and compiled in parallel.

Conclusions

Using Vue.js without a packaging system is possible and quite easy.

Obviously everyone has different requirements, so this solution is not a silver bullet, but it allows to start quickly and extend the project as it grows.