I had to work on a MVP where there were specific instructions to use Vue directly from a CDN. The Vue build available in this way is also called UMD (Universal Module Definition) build since you can use Vue from anywhere and the project does not need specific setup to build and package your code. Here’s a demo of how a simple Vue setup from CDN can be used for quick demo projects.
But, why use Vue from CDN? There may be a few cases where such an arrangement can help-
- demo simple functionality (and do not want to spend time on setup)
- create quick MVPs that can be demonstrated using simple HTML and JS files
- make everything portable; enable others to easily change stuff without the full Vue setup
Rather than building a simple “hello world”, we will incorporate two components and router, and axios to call external services - and get all this working within a single HTML page and a couple of JS files.
Setup
There is nothing to setup when using Vue from CDN. Create a new index.html
page and include Vue (and Vue router).
1
2
3
4
5
6
7
8
9
10
|
<html>
<head>
<title>The Coolest Vue - Axios Demo</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script src="https://unpkg.com/vue-router@2.0.0"></script>
</head>
<body>
Sample page
</body>
</html>
|
You are all set to start coding.
Add Bootstrap for future styling - we will use it for nav pane, and restrict ourselves to simple styles elsewhere.
Also introduce a footer while we are at it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
<html>
<head>
<title>The Coolest Vue - Axios Demo</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script src="https://unpkg.com/vue-router@2.0.0"></script>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
/>
<style>
.section {
margin-top: 2em;
}
</style>
</head>
<body>
<div class="wrapper">
<footer
style="
background-color: rgb(236, 236, 236);
position: fixed;
width: 100%;
font-size: 80%;
margin-top: 5em;
padding-top: 5px;
padding-bottom: 5px;
bottom: 0;
"
>
<div class="container">
<i>© 2020.</i>
</div>
</footer>
</div>
</body>
</html>
|
Add Script in index.html
Initiate vue and use a couple of routes using router. Include axios from CDN.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
Vue.component("Navigation", Navigation);
const routes = [
{
path: "/",
component: Access,
},
{
path: "/content",
component: Content,
},
];
const router = new VueRouter({
routes: routes,
mode: "history",
base: "/",
});
var app = new Vue({
el: "#app",
router: router,
data() {
return {
authstatus: false,
apiurls: {
authget: "http://localhost:9000/auth",
authpost: "http://localhost:9000/auth",
postget: "http://localhost:9000/posts",
},
};
},
methods: {
setAuthStatus(val) {
console.log("Setting authstatus through event to: ", val);
this.authstatus = val;
console.log("New value: ", app.authstatus);
},
},
});
</script>
|
As you can see, we created a couple of variables and a method. These will be used by more than one components that we will create next.
Also note that we used mode: "history"
. This will necessitate using a server to serve the index.html
file. If you don’t want that, use mode: "hash"
.
Create Components
We will not quite use .vue
files - although it can be done by using vue-http-loader
package when using Vue UMD build. Instead, we will create “pseudo-components” by just defining the entire Vue template structure in an object.
Let us create three components -
- Get access details and authenticate
- Show content
- Navigation: this can be included in the main HTML page or in both of the above components.
Navigation
Create a new file and call it Navigation.vue.js
.
- Create the Vue template as an object (use Bootstrap for styling)
- Show access and content links
- Display
Content
links only if user has “logged in”
data
(and computed
, methods
etc.) will be objects as well, and used where relevant (almost represents real Vue single file components)
The value to check whether user has logged in comes from the root.
Although I am not a fan of accessing variables like this, it serves its purpose in a simple setup. Use Vuex or pass values through props/events if it becomes more complex.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
const Navigation = {
template: `
<nav class="navbar navbar-expand-md" style="background-color: #ffffff;">
<div class="nav-brand font-weight-bold">Vue-Axios Demo</div>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto mt-2 mt-lg-0 font-weight-bold">
<li>
<router-link class="nav-link" to="/">
Access
</router-link>
</li>
<li v-if="authstatus">
<router-link class="nav-link" to="content">
Content
</router-link>
</li>
</ul>
</div>
</nav>
`,
computed: {
authstatus() {
// used to show/hide `content` link
return this.$root.authstatus;
},
},
};
|
Access
Create a new file access.vue.js
.
- Collect email
- Check whether email has access through an Axios call to our dummy service
- Disable form on successful authentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
const Access = {
template: `
<div>
<Navigation/>
<h1>Access</h1>
<div class="error" v-if="validationErrors.length > 0" style = "color:red; margin-top: 2em; margin-bottom: 1em;">
{{ validationErrors }}
</div>
<div v-else style="margin-top: 2em; margin-bottom: 1em;"> </div>
<form @submit.prevent="validateAndSubmit" id="formLogin">
<div class="field section" style="margin-top:3em">
<label for="email" >Email:</label>
<input type="text" name="email" v-model="email" @input="validate" :disabled="authstatus"/>
<button style="margin-left:1em" type="submit" :disabled="authstatus">Access</button>
</div>
<div class="section">
<i v-if="authstatus">You have access! </i>
<i v-else>Verify email to gain access.</i>
</div>
</form>
</div>`,
data() {
return {
email: "",
validationErrors: [],
};
},
computed: {
authstatus() {
// this way of sharing values is ok for small projects.
// large projects must use Vuex
return this.$root.authstatus;
},
urls() {
// fetch URLs from root
return this.$root.apiurls;
},
},
mounted() {
this.fetchAuthStatus(); // check auth on mount
},
methods: {
async fetchAuthStatus() {
// triggered at load. Relevant only when you have a session.
// for JWT and similar: check against token, and pass refresh token if expired
console.log("Fetching auth status..");
const res = await axios.get(this.urls.authget, {
crossdomain: true,
});
const authData = res.data;
console.log(`GET auth done!`, authData);
if (authData["auth-status"]) {
console.log("Auth approved!");
} else {
// this is silent error since user will ..
// .. most likely not have access the first time the screen loads
console.log("Auth denied..! Re-login.");
}
this.$emit("auth-verified", authData["auth-status"]);
},
async login() {
try {
// main login flow
const res = await axios.post(this.urls.authpost, { email: this.email });
const authData = res.data;
console.log(`POST auth done!`, authData);
if (!authData["auth-status"]) {
this.validationErrors.push(
"Could not verify email. Validate and resubmit."
);
}
this.$emit("auth-verified", authData["auth-status"]);
} catch (e) {
this.validationErrors.push[e.message];
console.error(e);
}
},
validateAndSubmit() {
// .. validate before sending the request off to server.
this.validationErrors = [];
if (this.validate()) {
this.login();
}
},
validate() {
const errors = [];
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!pattern.test(this.email)) errors.push("Invalid email.");
if (errors) this.validationErrors = errors;
else this.validationErrors = [];
return !this.validationErrors.length;
},
},
};
|
Content
Create a new file content.vue.js
.
Include -
- template, data and methods
- logic to call an external service to get posts
Like earlier, we will fetch variables from the root by using this.$root
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
const Content = {
template: `
<div>
<Navigation/>
<h1>Content</h1>
<div class="error" v-if="validationErrors.length > 0" style = "color:red; margin-top: 2em; margin-bottom: 1em;">
{{ validationErrors }}
</div>
<div v-else style="margin-top: 2em; margin-bottom: 1em;"> </div>
<div class="field section" style="margin-top:3em">
<label for="keywords" >Keywords:</label>
<input type="text" name="keywords" v-model="keywords"/>
<button style="margin-left:1em" @click="">Search</button>
</div>
<div class="field section">
<label for="posts"><b>Posts</b></label>
</div>
<div class="posts" style="margin-left:30%; text-align: left!important">
<div v-for="(post,index) in posts" :key="index" >
{{post.id}}. {{post.description}}
</div>
<i v-if="!posts || posts.length == 0">No content found.</i>
</div>
<div class="section" style="margin-top:1em">
</div>
</div>`,
data() {
return {
keywords: "",
posts: [],
validationErrors: [],
currentPage: 0,
totalPages: 0,
};
},
computed: {
authstatus() {
// large projects must used Vuex. This will work for small components and data sets.
return this.$root.authstatus;
},
urls() {
// a simple way to standardize URLs from root
return this.$root.apiurls;
},
},
mounted() {
this.fetchPosts();
},
methods: {
async fetchPosts() {
if (!this.authstatus)
// authstatus check here is not quite required, but is just a fall-back
this.validationErrors.push(
"Not logged in. You have to login to fetch posts."
);
else {
console.log("Fetching posts..");
// keywords passed as query - as-is.
// verify whether server requires any specific format
const res = await axios.get(
`${this.urls.postget}?query=${this.keywords}&page=${
this.currentPage + 1
}`,
{
crossdomain: true,
}
);
const postData = res.data;
console.log(`GET posts done!`, postData);
if (!postData["auth-status"]) {
// auth-status is returned by post requests too!
console.log("Auth denied..! Re-login.");
this.validationErrors.push("Access is denied.");
}
this.posts = postData["posts"] ? postData["posts"] : [];
this.currentPage = postData["currentpage"]
? postData["currentpage"]
: 0;
this.totalPages = postData["totalpages"] ? postData["totalpages"] : 0;
// pagination logic yet to be implemented to navigate to subsequent pages
console.log(".. posts fetched!");
}
},
},
validateAndSubmit() {
// there are no validations at this time.
this.validationErrors = [];
this.fetchPosts();
},
};
|
Index.html
We will change index.html to include -
- the newly developed components
- common variables used across components
Change the script
section with following code -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
<script src="./navigation.vue.js"></script>
<script src="./access.vue.js"></script>
<script src="./content.vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
Vue.component("Navigation", Navigation);
Vue.component("Content", Content);
Vue.component("Access", Access);
const routes = [
{
path: "/",
component: Access,
},
{
path: "/content",
component: Content,
},
];
const router = new VueRouter({
routes: routes,
mode: "history",
base: "/",
});
var app = new Vue({
el: "#app",
router: router,
data() {
return {
authstatus: false,
apiurls: {
authget: "http://localhost:9000/auth",
authpost: "http://localhost:9000/auth",
postget: "http://localhost:9000/posts",
},
};
},
methods: {
setAuthStatus(val) {
console.log("Setting authstatus through event to: ", val);
this.authstatus = val;
console.log("New value: ", app.authstatus);
},
},
});
</script>
|
And ta da.. your application is ready. Just serve the directory that has index.html
. For e.g. we can use a quick http server using http-server
:
npm i -g http-server
http-server
If you use mode: "hash"
, you can simply open index.html
to see the magic.
How it works?
- Parent HTML page instantiates Vue and mounts in on a
div
.
- Three JS files that define Vue templates, methods, et al. as objects.
- Component objects defined in the external JS files are imported and used in parent HTML page.
Not quite beautiful or modular, but it works!
See this code on Github
Conclusion
Using Vue from CDN can certainly play a role in super simple projects when there are people in the ecosystem who are not quite onboard with Vue, and want to understand how things work by seeing a simple project.
But, I find it cleaner to just use Vue CLI and its single file components. While Vue CLI does bring in complexity in build processes, the overall structure is easier to understand and demonstrate to a technical audience.