In the previous lesson you built your first working panel: containers made with
ref(), mustaches in the template, a button wired up to @click and a class that follows the state of the mission. The panel came alive - but it came alive a little magically. You typed the code and the picture simply appeared on the screen. You still do not know who put it there, or at which moment.On a research station, "simply" does not pass inspection. The engineer on duty has to know which switch fires the system up, in what order the modules wake, and where to look for the fault when the panel screen stays black. Today, @name, we are going to take the ignition sequence of a Vue application apart into single steps - and after this lesson not one of them will be magic any more.
Open the project the wizard generated for you back in the lesson about Vite. There are three files in it that you have not touched even once:
index.html in the root directory, src/main.js and src/App.vue. None of them displays anything on its own. Only together do the three of them form the chain that turns your code into a picture on a Mission Control monitor.One housekeeping note before we set off. If you agreed to TypeScript in the wizard, the startup file is called
main.ts, and without TypeScript it is called main.js. It is the same file, the same role and the same contents; only the extension differs. In this lesson I write about main.js, because that is the name it carries in the exercises you get right after it.Let us start at the beginning of the chain, with the thing the browser receives first. A browser cannot open a
.vue file - that is not a format it knows. It can do one thing: load an HTML page. That is why every Vue project has exactly one HTML file, and that file is the first thing to reach the screen.There are four things worth seeing inside it. The
<meta charset="UTF-8"> tag sets the character encoding, so that accented letters and symbols do not turn into rubbish. The <meta name="viewport"> tag tells mobile devices not to pretend they have a wide screen - without that line, the panel on a tablet in the habitat module will show up as a shrunken version of the desktop page. The <title> tag is the caption on the browser tab. The last two lines matter most, though: an empty <div> with an id attribute set to app, and a <script> tag with a type="module" attribute.That empty
<div> is the landing platform. Vue will not glue the interface down just anywhere - it needs one clearly indicated element that it is allowed to rule. The type="module" attribute, in turn, means that the file being pointed at is an ES module, that is, that import statements are allowed inside it. Without that attribute the browser would stop on the very first line of main.js and report a syntax error.1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>NOVA LAB - Mission Control</title>
7 </head>
8 <body>
9 <div id="app"></div>
10 <script type="module" src="/src/main.js"></script>
11 </body>
12</html>Read that file once more and notice what is not in it. There is not a single Vue tag, not one pair of mustaches, not one component name. This is plain HTML, exactly the kind you wrote in the first world - Vue changes nothing here. The key
<div> is empty, and empty it will stay on disk; content will appear inside it only in the browser, at the moment the application starts.The value
app in the id attribute is not magic either. It is an ordinary name that you choose yourself, and it could just as well read control-room or nova. It does have to match what you are about to write in main.js - and that is one of the most common sources of a black screen, which we will come back to at the end of this lesson.The browser reached the
<script> tag and asked for the /src/main.js file. In development mode Vite handles that request: it finds the file, runs it through the compiler if that is needed, and sends back a finished module. This is the first piece of your own code that runs at all - everything you see on the screen afterwards begins in these few lines.The whole startup file is usually four or five lines long. We will follow them one at a time, because each of them does exactly one thing.
The first two lines bring the tools on board. An
import statement always has the same shape and is made of four parts standing in a fixed order: the word import, the list of names to bring in, the word from, and at the end the source written in quotes. Remember that foursome, because you will write it in every Vue file you ever create.The difference between the two lines sits in the braces. The
createApp function is one of many tools offered by the vue package, so we take its name in braces - this is what is called a named export, and the braces say "out of this parcel I pick exactly this one". A .vue file is different: it offers the outside world one single thing, its component, and it does so as a default export. A default export you bring in without braces, and you give it a name yourself - App is only an accepted convention, nothing forces that particular name.Take a look at the source itself as well. The form
'vue' without a slash is a package name, which Vite will look for in the node_modules directory installed by npm install. The form './App.vue' begins with a dot and a slash, so it is a relative path: look next to me, in the same directory. The .vue extension is spelled out here on purpose, because otherwise the tooling will not guess which file you mean.1import { createApp } from 'vue'
2import App from './App.vue'After these two lines, absolutely nothing changed on the screen, and nothing could have. An import is no more than carrying tools aboard: you now have the
createApp function and the App component within reach, but you have not used either of them yet. The App.vue file itself also stayed untouched - you do not change it by importing it.With the tools at hand you can call the application into being. The
createApp function does that, and its only job is to return a Vue application instance - one object that is the command center of the whole system. It is the one that knows which component stands at the top, it is the one that will take on extra modules, and it is the one you will ask to start.In the parentheses you give the root component, the one the entire hierarchy begins with. For us that is
App. Every other component of the station will hang below it later on, but createApp is interested in that first one only.Before you see the code, let us settle what
createApp does not do - because this function gets confused with three completely different ones. It does not create a database: the shared memory of the mission is the business of Pinia and its own createPinia function, which you will meet later in this lesson. It does not compile CSS: styles are described in the <style> section of a component, and Vite is what turns them into a finished stylesheet while the project is being built. Nor does it create routing: navigation between the screens of the station is run by Vue Router with the createRouter function, which we will come back to in the Navigation Center. All three are separate tools, and not one of them is hiding inside createApp.1const app = createApp(App)The shape of that line is identical to the
ref() line from the previous lesson: the word const, a name, an equals sign, a function call with an argument. The app variable now holds the application instance.And here comes the most important observation of the whole lesson: there is still nothing on the screen. The instance exists in the memory of the browser and knows its root component, but it does not have the faintest idea where on the page it is supposed to appear. The
index.html file has not changed, and its <div> is still empty. The application has been built, but it has not launched yet.One move is missing: pointing at the platform. That is done by the
mount method, called on the application instance. You hand it a CSS selector - the same kind of notation you used in stylesheets. The # sign means "the element with this identifier", so '#app' reads as "the element whose id equals app", which is exactly that empty <div> from the HTML file.From that call onwards, Vue takes over the element you pointed at: it puts the result of the root component's template inside it, and from then on looks after its contents itself. This is the moment the system starts.
Remember what
mount does not do as well, because the mistakes here always run in the same direction. It does not create any HTML file - the index.html file has to exist beforehand, and you are the one who writes it. It does not install packages, because npm install from the Vite lesson is there for that. Nor does it start the development server: the server is started by the npm run dev command and has been running long before your code executes at all.1import { createApp } from 'vue'
2import App from './App.vue'
3
4const app = createApp(App)
5
6app.mount('#app')That is the complete startup file of the simplest Vue application - four statements and not one more. Run the project and the browser will show you what you have in the
App.vue template. Notice that nothing changed apart from that one file: index.html looks the same, App.vue looks the same, and the system moved all the same.The instance can also be mounted without being saved into a variable, as the chain
createApp(App).mount('#app'). It works identically, and you will meet that form fairly often in documentation examples, but at NOVA LAB I recommend the two-step version with the app variable. The reason is practical: in a moment you will be adding station modules to that instance, and for that you need a handle - which the chain simply throws away.Since the selector is ordinary text, nothing stops you from typing anything at all into it - and that is where the classic first-day failure comes from. The application compiles, the console does not complain about syntax, and the page is blank.
1// index.html contains <div id="app"></div>
2// but the selector below points to something else
3app.mount('#control-room')Vue looks for the
#control-room element, does not find it, and has nowhere to put the interface, so a warning about a missing target element appears in the browser console. Nothing changed in your component in the meantime - App.vue is perfectly correct, and yet you will not see a single character of it. So when the panel screen stays black, @name, start by comparing two places: the id value in index.html and the text inside the mount call.One panel is not a station yet. Sooner or later, a mission system needs shared memory that all the modules draw on at once, and navigation that switches the screens of Mission Control. Things like that cannot sensibly be bolted onto a single component - they have to apply across the whole application.
Vue calls them plugins, and in our terminology they are station modules, added to the instance before launch. You connect them with the
use method called on the application instance: you write the name of the variable, a dot, use, and in the parentheses you pass the ready module. Let us meet two of them right away, the ones the wizard from the Vite lesson wrote into your project for you.The first is Pinia, the central database of the mission. The
createPinia function creates a fresh instance of it - just as createApp creates an application instance - and the result of that call travels straight into use. The second is the router, brought in from the ./router directory that the wizard generated. That one is a finished object already, so you pass it along without any call.1import { createApp } from 'vue'
2import { createPinia } from 'pinia'
3import App from './App.vue'
4import router from './router'
5
6const app = createApp(App)
7
8app.use(createPinia())
9app.use(router)
10
11app.mount('#app')Look at what did not change here: the
createApp line and the mount line are exactly the same as in the minimal version, and App.vue still knows nothing about any of it. Only two lines in the middle were added - and that is the whole idea behind plugins. You extend the system without rebuilding it.Order matters here, and there is only one correct one: every
use call has to stand before mount. Mounting is the launch, and modules are not added to a rocket after the engines have fired. If you swapped those lines around, the first render would happen without the plugins - components would find neither the shared memory nor the navigation elements, and errors about unrecognized names would land in the console. The use method does return the same instance, by the way, so technically the calls could be strung into a chain, but I recommend writing them one under another: in an emergency you can see the list of connected modules at a glance.If your project has no
router directory, or you did not install Pinia, simply delete the matching import along with its use line - the application will work without them, exactly like the minimal version. Both modules get locations of their own anyway: the Navigation Center deals with the router, and the Mission Database with Pinia.The last of the three files is left.
App.vue is the root component, the only one you mount directly; all the others will hang inside it later. Its contents follow from that role: we keep here only what is common to the whole station and does not disappear when the screens are switched. In practice that means the top bar with the name and the navigation, the main area for content, and a footer.To build a frame like that, the tags you know from the first world are enough:
<header>, <nav>, <main> and <footer>. Only one thing is new - the way the links are wired up. In the previous lesson you learned that @click takes a function name without parentheses. That holds true for as long as the function needs no argument. When you want to pass it something, you write a normal call with parentheses and a value inside them, for example @click="changeSection('Systems')". Vue treats that as an expression and wraps it by itself into a function called on click - which is why it does not run the moment the page is displayed.1<template>
2 <div class="nova-lab">
3 <header>
4 <h1>NOVA LAB</h1>
5 <nav>
6 <a href="#" @click="changeSection('Dashboard')">Dashboard</a>
7 <a href="#" @click="changeSection('Systems')">Systems</a>
8 <a href="#" @click="changeSection('Crew')">Crew</a>
9 </nav>
10 </header>
11
12 <main class="content">
13 <h2>{{ currentSection }}</h2>
14 <p>Section content will be loaded here.</p>
15 </main>
16
17 <footer>
18 <p>NOVA LAB Mission Control 2087</p>
19 </footer>
20 </div>
21</template>The whole frame is ordinary, semantic HTML - nothing changed in it apart from three at signs and one pair of mustaches. The header, the navigation, the main area and the footer would look identical on a page with no framework at all. Vue added just two things here: a reaction to a click, and one place into which it drops the current value.
Now for the second section of the file. We need one container holding the name of the current section, and a function that swaps that name. The function takes an argument - the very value you passed in the template - and writes it into the container through
.value, following the rule from the previous lesson: in the script always .value, in the template never.1<script setup>
2import { ref } from 'vue'
3
4const currentSection = ref('Dashboard')
5
6function changeSection(name) {
7 currentSection.value = name
8}
9</script>Click any of the links and the heading in the main area changes to the name of the section you chose. Notice what stays in place while that happens: the top bar, the navigation and the footer do not flicker for a moment, because Vue refreshes only the fragment that uses the container that changed. That is exactly why the root component is a frame - the things that are shared are not supposed to reload.
I left the
<style scoped> section out here on purpose, because we talked about styles in the lesson on .vue modules and it would add nothing new. In the exercise you are about to get, the styles are already prepared for you.For now we only switch a caption, because that is all it takes to understand the role of the root component. In the Navigation Center this same frame will turn into real navigation: the
<a> tags will be replaced by the RouterLink component, and in place of the heading inside <main> a RouterView will step in, swapping whole station screens instead of a single line of text. The currentSection container will stop being needed then.You have all the pieces now, so let us put them together into one checklist. This is what the start of the NOVA LAB system looks like, step by step, in the same order every single time:
index.html - the only HTML file in the project.<script type="module"> tag and asks for the startup file, that is main.js or main.ts in a TypeScript project, and Vite hands that file over.import statements bring in createApp from the vue package and the App component.createApp(App) call creates the application instance - there is still nothing to see on the screen.app.use() calls add the modules, if the project uses them at all.app.mount('#app') call connects the application to the empty <div> in index.html.App component inside that element, and the system is ready for duty.That order is not a matter of convention - it follows from dependencies. You will not mount an application you have not called into being, and you will not call it into being before you have brought in
createApp. The only step that can disappear in a simple project is number five - it drops out when you connect no modules at all. Everything else always looks the same, in every Vue project you will ever open.Remember this, @name:
index.html is the landing platform, main.js is the ignition sequence, App.vue is the station frame - and app.mount('#app') is the one call in which the system really moves.