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.