Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Building Applications with Spring 5 and Vue.js 2

You're reading from   Building Applications with Spring 5 and Vue.js 2 Build a modern, full-stack web application using Spring Boot and Vuex

Arrow left icon
Product type Paperback
Published in Oct 2018
Publisher Packt
ISBN-13 9781788836968
Length 590 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
James J. Ye James J. Ye
Author Profile Icon James J. Ye
James J. Ye
Arrow right icon
View More author details
Toc

Table of Contents (17) Chapters Close

Preface 1. Modern Web Application Development - This Is a New Era FREE CHAPTER 2. Vue.js 2 - It Works in the Way You Expected 3. Spring 5 - The Right Stack for the Job at Hand 4. TaskAgile - A Trello-like Task Management Tool 5. Data Modeling - Designing the Foundation of the Application 6. Code Design - Designing for Stability and Extensibility 7. RESTful API Design - Building Language Between Frontend and Backend 8. Creating the Application Scaffold - Taking off Like a Rocket 9. Forms and Validation - Starting with the Register Page 10. Spring Security - Making Our Application Secure 11. State Management and i18n - Building a Home Page 12. Flexbox Layout and Real-Time Updates with WebSocket - Creating Boards 13. File Processing and Scalability - Playing with Cards 14. Health Checking, System Monitoring - Getting Ready for Production 15. Deploying to the Cloud with Jenkins - Ship It Continuously 16. Other Books You May Enjoy

ES6 basics

ES6 (short for ECMAScript 2015), is the sixth version of ECMAScript, which is a general-purpose, cross-platform, and vendor-neutral programming language. ECMAScript is defined in ECMA Standard (ECMA-262) by Ecma International. Most of the time, ECMAScript is more commonly known by the name JavaScript.

Understanding ES6 is the key to writing web applications using modern JavaScript. Owing to the scope of this book, we will only cover the basics of new features introduced in ES6 here as you will see them in the rest of the book.

Block scoping, let, and const

As mentioned earlier, in ES6, you can use let to define variables or use const to define constants, and they will have block-level scope. And in the same scope, you can not redefine a variable using let. Also, you cannot access a variable or a constant that is defined with let or const before its declaration, since there is no variable hoisting with let or const.

Let's see the following workout example:

1.  function workout() {
2. let gym = 'Gym A';
3.
4. const gymStatuses = {'Gym A': 'open', 'Gym B': 'closed'};
5. for (let gym in gymStatuses) {
6. console.log(gym + ' is ' + gymStatuses[gym]);
7. }
8.
9. {
10. const gym = 'Gym B';
11. console.log('Workout in ' + gym);
12. // The following will throw TypeError
13. // gym = 'Gym C';
14. }
15.
16. console.log('Workout in ' + gym);
17.
18. {
19. function gym () {
20. console.log('Workout in a separate gym');
21. }
22. gym();
23. }
24.
25. if (gymStatuses[gym] == 'open') {
26. let exercises = ['Treadmill', 'Pushup', 'Spinning'];
27. }
28. // exercises are no longer accessible here
29. // console.log(exercises);
30.
31. try {
32. let gym = 'Gym C';
33. console.log('Workout in ' + gym);
34. throw new Error('Gym is closed');
35. } catch (err) {
36. console.log(err);
37. let gym = 'Gym D';
38. console.log('Workout in ' + gym);
39. }
40. }
41. workout();

In line 2, we declare the gym variable, and it is visible in the workout() function body. In line 5, we declare the gym variable within the for loop block. It shadows the gym variable declared in line 2 and is only accessible within that for loop block.

In lines 9 to 14, we declare a new scope using a block statement. The gym constant declared in line 10 is only accessible within that scope. And as you can see in line 13, assigning a value to a constant will cause TypeError.

In line 16, the gym variable is back to the one declared in line 2. In lines 18 to 23, we declare the gym function and it is only accessible within that block.

In line 26, we define the exercises variable within the if block. And as you can see from line 29, it is no longer accessible outside the if block.

In lines 31 to 39, we declare a try-catch block. As you can see in lines 32 and 37, the try block and catch block are in different scopes.

To wrap up, using let and const, we can archive block-level scope with for loop blocks, if blocks, try-catch blocks, and block statements, as well as switch blocks.

Classes

ES2015 introduces classes, which is primarily a syntactical sugar over prototype-based inheritance. With the class syntax, you can create constructors, extends from a superclass, and create static methods, as well as getters and setters.

Let's see the following example that uses the class syntax to implement User, and TeamMember:

1.  class User {
2. constructor(name, interests) {
3. this.name = name;
4. this.interests = interests;
5. }
6. greeting () {
7. console.log('Hi, I\'m ' + this.name + '.');
8. }
9. get interestsCount () {
10. return this.interests ? this.interests.length : 0;
11. }
12. }

In lines 1 to 12, we define class Userwhich accepts two arguments via its constructor. It has a greeting() method and an interestsCount getter:

13. class TeamMember extends User {
14. constructor(name, interests) {
15. super(name, interests);
16. this._tasks = [];
17. this._welcomeText = 'Welcome to the team!';
18. }
19. greeting () {
20. console.log('I\' m ' + this.name + '. ' + this._welcomeText);
21. }
22. work () {
23. console.log('I\' m working on ' + this._tasks.length + '
tasks.')
24. }
25. set tasks (tasks) {
26. let acceptedTasks = [];
27. if (tasks.length > TeamMember.maxTasksCapacity()) {
28. acceptedTasks = tasks.slice(0,
TeamMember.maxTasksCapacity());
29. console.log('It\'s over max capacity. Can only take two.');
30. } else {
31. acceptedTasks = tasks;
32. }
33. this._tasks = this._tasks.concat(acceptedTasks);
34. }
35. static maxTasksCapacity () {
36. return 2;
37. }
38. }

In lines 13 to 38, we create a TeamMember class to extend from User. In its constructor, it calls the constructor of the User with super to instantiate the properties of name and interests. We also define two additional properties, _tasks and _welcomeText. The preceding underscore suggests that they are regarded as private properties and should not be changed directly from outside. However, nothing is private in JavaScript. You can still access these properties, for example, member._tasks, and member._welcomeText.

We override the greeting() method of user in line 20 and add a new work() method in line 22. In lines 25 to 34, we define a setter tasks, inside which we access the maxTasksCapacity() static method of TeamMember:

39. let member = new TeamMember('Sunny', ['Traveling']);
40. member.greeting(); // I' m Sunny. Welcome to the team!
41. member.tasks = ['Buy three tickets', 'Book a hotel', 'Rent a car'];
// It's over max capacity. Can only take two.
42. member.work(); // I' m working on 2 tasks.
43. console.log(member.interestsCount); // 1
44. member.interestsCount = 2; // This won’t save the change
45. console.log(member.interestsCount); // 1
46. console.log(member.tasks); // undefined

As you can see, in lines 39 to 43, the member object has all the features of the User class and TeamMember, working as expected. In lines 44 to 45, we try to make changes to member.interestsCount, but it won't work because there is no setter defined. And line 46 shows that accessing member.tasks results in undefined because we didn't define a getter for it.

You cannot use member.constructor to access the constructor of the TeamMember defined in line 14. It is for accessing the member object’s constructor function, in this case, TeamMember.

And now let's see how to add a new method, eat(), to the User class:

User.prototype.eat = function () {
console.log('What will I have for lunch?');
};
member.eat(); // What will I have for lunch?

You still need to add it to the prototype object of User. If you add it directly to User as follows, you will get TypeError:

User.sleep = function () {
console.log('Go to sleep');
};
member.sleep(); // Uncaught TypeError: member.sleep is not a function
User.sleep(); // Go to sleep

It is because as a result of writing in this way that you add sleep as a property of the User class itself or, more precisely, the User constructor function itself. And you might have already noticed that sleep becomes a static method of the User class. When using the class syntax, when you define a method, behind the scene, JavaScript adds it to its prototype object, and when you define a static method, JavaScript adds it to the constructor function:

console.log(User.prototype.hasOwnProperty('eat'));  // true
console.log(User.hasOwnProperty('sleep')); // true

Enhanced object literals

In ES6, object literals support setting prototypes, shorthand assignments, defining methods, making super calls, and computing properties with expressions.

Let's see the following example, which creates an advisor object with a TeamMember object as its prototype:

1.  const advice = 'Stay hungry. Stay foolish.';
2.
3. let advisor = {
4. __proto__: new TeamMember('Adam', ['Consulting']),
5. advice,
6. greeting () {
7. super.greeting();
8. console.log(this.advice);
9. },
10. [advice.split('.')[0]]: 'Always learn more'
11. };

Line 4, assigning the object of TeamMember to the advisor object's __proto__ property makes advisor an instance of TeamMember:

console.log(TeamMember.prototype.isPrototypeOf(advisor));  // true
console.log(advisor instanceof TeamMember); // true

Line 5 is a shorthand assignment of advice:advice. Line 7 is creating the greeting() method of TeamMember, inside which it will invoke the greeting method of TeamMember:

advisor.greeting();   // I' m Adam. Welcome to the team!
// Stay hungry. Stay foolish.

In line 10, the Stay hungry property is calculated with bracket notation. And to access this property, in this case, because the property name contains a space, you need to use bracket notation, like this—advisor['Stay hungry'].

Arrow functions

ES6 introduces arrow functions as a function shorthand, using => syntax. Arrow functions support statement block bodies as well as expression bodies. When using an expression body, the expression's result is the value that the function returns.

An arrow syntax begins with function arguments, then the arrow =>, and then the function body. Let's look at the following different variations of arrow functions. The examples are written in both ES5 syntax and ES6 arrow function syntax:

const fruits = [{name: 'apple', price: 100}, {name: 'orange', price: 80}, {name: 'banana', price: 120}];

// Variation 1
// When no arguments, an empty set of parentheses is required
var countFruits = () => fruits.length;
// equivalent to ES5
var countFruits = function () {
return fruits.length;
};

// Variation 2
// When there is one argument, parentheses can be omitted.
// The expression value is the return value of the function.
fruits.filter(fruit => fruit.price > 100);
// equivalent to ES5
fruits.filter(function(fruit) {
return fruit.price > 100;
});

// Variation 3
// The function returns an object literal, it needs to be wrapped
// by parentheses.
var inventory = fruits.map(fruit => ({name: fruit.name, storage: 1}));
// equivalent to ES5
var inventory = fruits.map(function (fruit) {
return {name: fruit.name, storage: 1};
});

// Variation 4
// When the function has statements body and it needs to return a
// result, the return statement is required
var inventory = fruits.map(fruit => {
console.log('Checking ' + fruit.name + ' storage');
return {name: fruit.name, storage: 1};
});
// equivalent to ES5
var inventory = fruits.map(function (fruit) {
console.log('Checking ' + fruit.name + ' storage');
return {name: fruit.name, storage: 1};
});

There is an additional note regarding variation 3. When an arrow function uses curly brackets, its function body needs to be a statement or statements:

var sum = (a, b) => { return a + b };
sum(1, 2); // 3

The sum function won't work as expected when it is written like this:

var sum = (a, b) => { a + b };
sum(1, 2); // undefined
// Using expression will work
var sum = (a, b) => a + b;
sum(1, 2); // 3

Arrow functions have a shorter syntax and also many other important differences compared with ES5 function. Let's go through some of these differences one by one.

No lexical this

An arrow function does not have its own this. Unlike an ES5 function, that will create a separate execution context of its own, an arrow function uses surrounding execution context. Let's see the following shopping cart example:

1.  var shoppingCart = {
2. items: ['Apple', 'Orange'],
3. inventory: {Apple: 1, Orange: 0},
4. checkout () {
5. this.items.forEach(item => {
6. if (!this.inventory[item]) {
7. console.log('Item ' + item + ' has sold out.');
8. }
9. })
10. }
11. }
12. shoppingCart.checkout();
13.
14. // equivalent to ES5
15. var shoppingCart = {
16. items: ['Apple', 'Orange'],
17. inventory: {Apple: 1, Orange: 0},
18. checkout: function () {
19. // Reassign context and use closure to make it
20. // visible to the callback passed to forEach
21. var that = this
22. this.items.forEach(function(item){
23. if (!that.inventory[item]) {
24. console.log('Item ' + item + ' has sold out.');
25. }
26. })
27. }
28. }
29. shoppingCart.checkout();

In line 6, this refers to the shoppingCart object itself, even it is inside the callback of the Array.prototype.forEach() method. As you can see in line 21, with the ES5 version, you need to use closure to keep the execution context available to the callback function.

And because an arrow function does not have a separate execution context, when it is invoked with Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind() method, the execution context that passed in as the first argument will be ignored. Let's take a look at an example:

1. var name = 'Unknown';
2. var greeting = () => {
3. console.log('Hi, I\'m ' + this.name);
4. };
5. greeting.call({name: 'Sunny'}); // I'm Unknown
6. greeting.apply({name: 'Tod'}); // I'm Unknown
7. var newGreeting = greeting.bind({name: 'James'});
8. newGreeting(); // I'm Unknown

As you can see from line 3, in an arrow function, this always resolves to its surrounding execution context. The call(), apply(), or bind() method has no effect on its execution context.

Unlike ES5 functions, arrow functions do not have their own arguments object. The arguments object is a reference to the surrounding function's arguments object.

Because arrow functions use its surrounding execution context, they are not suitable for defining methods of objects. 

Let's see the following shopping cart example, which uses an arrow function for the checkout:

1.  var shoppingCart = {
2. items: ['Apple', 'Orange'],
3. inventory: {Apple: 1, Orange: 0},
4. checkout: () => {
5. this.items.forEach(item => {
6. if (!this.inventory[item]) {
7. console.log('Item ' + item + ' has sold out.');
8. }
9. })
10. }
11. }
12. shoppingCart.checkout();

In line 4, we change checkout to an arrow function. And because an arrow function uses its surrounding execution context, this in line 5 no longer references the shoppingCart object and it will throw Uncaught TypeError: Cannot read property 'forEach' of undefined.

The preceding shopping cart example is written with object literals. Arrow functions do not work well when defining object methods using a prototype object either. Let's see the following example:

1.  class User {
2. constructor(name) {
3. this.name = name;
4. }
5. }
6. User.prototype.swim = () => {
7. console.log(this.name + ' is swimming');
8. };
9. var user = new User();
10. console.log(user.swim()); // is swimming

As you can see from the output, in line 7this does not reference the user object. In this example, it references the global context.

No prototype object

Arrow functions do not have prototype objects, hence, they are not constructor functions. And they cannot be invoked with a new operator. An error will be thrown if you try that. Here's an example:

const WorkoutPlan = () => {};
// Uncaught TypeError: WorkoutPlan is not a constructor
let workoutPlan = new WorkoutPlan();
console.log(WorkoutPlan.prototype); // undefined

Default parameter value

In ES6, you can define the default values of a function's parameters. This is quite a useful improvement because the equivalent implementation in ES5 is not only tedious but also decreases the readability of the code.

Let's see an example here:

const shoppingCart = [];
function addToCart(item, size = 1) {
shoppingCart.push({item: item, count: size});
}
addToCart('Apple'); // size is 1
addToCart('Orange', 2); // size is 2

In this example, we give the parameter size a default value, 1. And let's see how we can archive the same thing in ES5. Here is an equivalent of the addToCart function in ES5:

function addToCart(item, size) {
size = (typeof size !== 'undefined') ? size : 1;
shoppingCart.push({item: item, count: size});
}

As you can see, using the ES6 default parameter can improve the readability of the code and make the code easier to maintain.

Rest parameters

In ES5, inside a function body, you can use the arguments object to iterate the parameters of the function. In ES6, you can use rest parameters syntax to define an indefinite number of arguments as an array.

Let's see the following example:

1.  // Using arguments in ES5
2. function workout(exercise1) {
3. var todos = Array.prototype.slice.call(arguments,
workout.length);
4. console.log('Start from ' + exercise1);
5. console.log(todos.length + ' more to do');
6. }
7. // equivalent to rest parameters in ES6
8. function workout(exercise1, ...todos) {
9. console.log('Start from ' + exercise1); // Start from
//Treadmill
10. console.log(todos.length + ' more to do'); // 2 more to do
11. console.log('Args length: ' + workout.length); // Args length: 1
11. }
12. workout('Treadmill', 'Pushup', 'Spinning');

In line 8, we define a rest parameter todos. It is prefixed with three dots and is the last named parameter of the workout() function. To archive this in ES5, as you can see in line 3, we need to slice the arguments object. And in line 11, you can see that the rest parameter todos does not affect the length of the argument in the workout () function.

Spread syntax

In ES6, when the three dot notation (...) is used in a function declaration, it defines a rest parameter; when it is used with an array, it spreads the array's elements. You can pass each element of the array to a function in this way. You can also use it in array literals.

Let's see the following example:

1. let urgentTasks = ['Buy three tickets'];
2. let normalTasks = ['Book a hotel', 'Rent a car'];
3. let allTasks = [...urgentTasks, ...normalTasks];
4.
5. ((first, second) => {
6. console.log('Working on ' + first + ' and ' + second)
7. })(...allTasks);

In line 3, we use spread syntax to expand the urgentTasks array and the normalTasks array. And in line 7, we use spread syntax to expand the allTasks array and pass each element of it as arguments of the function. And the first argument has the value Buy three ticketswhile the second argument has the value Book a hotel.

Destructuring assignment

In ES6, you can use the destructuring assignment to unpack elements in an array, characters in a string, or properties in an object and assign them to distinct variables using syntax similar to array literals and object literals. You can do this when declaring variables, assigning variables, or assigning function parameters.

Object destructuring

First of all, let's see an example of object destructuring:

1. let user = {name:'Sunny', interests:['Traveling', 'Swimming']};
2. let {name, interests, tasks} = user;
3. console.log(name); // Sunny
4. console.log(interests); // ["Traveling", "Swimming"]
5. console.log(tasks); // undefined

As you can see, the name and interests variables defined in line 2 pick up the values of the properties with the same name in the user object. And the tasks variable doesn't have a matching property in the user object. Its value remains as undefined. You can avoid this by giving it a default value, like this:

let {name, interests, tasks=[]} = user;
console.log(tasks) // []

Another thing you can do with object destructuring is that you can choose a different variable name. In the following example, we pick the value of the name property of the user object and assign it to the firstName variable:

let {name: firstName} = user;
console.log(firstName) // Sunny

Array destructuring

Array destructuring is similar to object destructuring. Instead of using curly brackets, array destructuring uses brackets to do the destructuring. Here is an example of array destructuring:

let [first, second] = ['Traveling', 'Swimming', 'Shopping'];
console.log(first); // Traveling
console.log(second); // Swimming

You can also skip variables and only pick the one that you need, like the following:

let [,,third, fourth] = ['Traveling', 'Swimming', 'Shopping'];
console.log(third); // Shopping
console.log(fourth); // undefined

As you can see, we skip the first two variables and only require the third and the fourth. However, in our case, the fourth variable doesn't match any elements in the array and its value remains as undefined. Also, you can give it a default value, like this:

let [,,third, fourth = ''] = ['Traveling', 'Swimming', 'Shopping'];
console.log(fourth); // an empty string

Nested destructuring

Similar to using object literals and array literals to create complex nested data structures with a terse syntax, you can use a destructuring assignment to pick up variables in a deeply nested data structure.

Let's see the following example, in which we only need the user's second interest:

1. let user = {name:'Sunny', interests:['Traveling', 'Swimming']};
2. let {interests:[,second]} = user;
3. console.log(second); // Swimming
4. console.log(interests); // ReferenceError

In line 2, even though we put interests in the destructuring assignment, JavaScript doesn't really declare it. As you can see in line 4, accessing it will raise ReferenceError. What happens here is that JavaScript uses the part on left side of the colon (:), in this case, interests, to extract the value of the property of the same name, and uses the part on the right side to do further destructuring assignments. If you want to extract the interests property, as demonstrated previously, you need to write it in like this: let {interests} = user;.

Here is another example in which the name property of the second element in an array is destructured:

const fruits = [{name:'Apple', price:100},{name:'Orange', price:80}];
let [,{name:secondFruitName}] = fruits;
console.log(secondFruitName); // Orange

Rest elements

You can use the same syntax of the rest parameters in the destructuring assignment to put the remainder of the elements of an array into another array. Here is an example:

let [first, ...others] = ['Traveling', 'Swimming', 'Shopping'];
console.log(others); // ["Swimming", "Shopping"]

As you can see, the second and third items of the array have been copied into the others variable. We can use this syntax to copy an array. However, this is only a shallow clone. When the elements of the array are objects, changes to an object's property of the copied array will be seen in the original array because essentially, the elements of both arrays reference the same object. Here is an example:

1. const fruits = [{name:'Apple', price:100},{name:'Orange', price:80}];
2. let [...myFruits] = fruits;
3. console.log(myFruits[0].name); // Apple
4. myFruits.push({name:'Banana', price:90});
5. console.log(myFruits.length); // 3
6. console.log(fruits.length); // 2
7. myFruits[0].price = 110;
8. console.log(fruits[0].price); // 110

As you can see in line 2, we use the destructuring assignment syntax to copy the fruits array into the myFruits array. And adding a new item to the copied array doesn't affect the original array, as you can see in lines 4 to 6. However, changing the value of the price property from the copied array will be also seen in the original array.

Function parameters destructuring

You can apply a destructuring assignment to function parameters as well. Let's see the following example:

1. function workout({gym}, times) {
2. console.log('Workout in ' + gym + ' for ' + times + ' times');
3. }
4. let thisWeek = {gym: 'Gym A'};
5. workout(thisWeek, 2); // Workout in Gym A for 2 times

As you can see, in line 1, we use object destructuring syntax to extract the gym variable from the first argument of the workout() function. In this way, the argument passed to the workout() function cannot be null or undefined. Otherwise, TypeError will be thrown. You can pass a number, a string, an array, or a function to the workout() function and JavaScript won't complain about it, although you will get undefined as a value for the gym variable.

Let's look at another example, in which we will perform a further destructuring of a destructured variable:

1. function workout({gym, todos}) {
2. let [first] = todos;
3. console.log('Start ' + first + ' in ' + gym);
4. }
5. let today = {gym: 'Gym A', todos: ['Treadmill']};
6. workout(today); // Start Treadmill in Gym A
7. workout({gym: 'Gym B'}) // throw TypeError

In line 1, we do a parameter destructuring of the first argument, and in line 2 we do a further destructuring of the todos variable. In this way, the argument passed to the workout() function must have a todos property and its value is an array. Otherwise, TypeError will be thrown, as you can see in line 7. This is because, in line 2, JavaScript cannot do destructuring on undefined or null. We can improve this by giving todos a default value, as follows:

1. function workout({gym, todos=['Treadmill']}) {
2. let [first] = todos;
3. console.log('Start ' + first + ' in ' + gym);
4. }
5. workout({gym: 'Gym A'}); // Start Treadmill in Gym A
6. workout(); // throw TypeError

As you can see, in line 1, we only give todos a default value and we have to call the workout() function with a parameter. Calling it without any parameters, as in line 6, will still throw an error. It is because JavaScript still cannot do destructuring on undefined to get a value for the gym variable. And if you try to assign a default value to gym itself, such as workout({gym='', ...), it won't work. You need to assign the entire parameter destructuring a default value, like this:

function workout({gym='', todos=['Treadmill']} = {}) {
...
}

Template literals

Template literals provide the ability to embed expressions in string literals and support multiple lines. The syntax is to use the back-tick (`) character to enclose the string instead of single quotes or double quotes. Here is an example:

let user = {
name: 'Ted',
greeting () {
console.log(`Hello, I'm ${this.name}.`);
}
};
user.greeting(); // Hello, I'm Ted.

As you can see, inside the template literals, you can access the execution context via this by using the syntax ${...}. Here is another example with multiple lines:

let greeting = `Hello, I'm ${user.name}.
Welcome to the team!`;
console.log(greeting); // Hello, I'm Ted.
// Welcome to the team!

One caveat is that all the whitespaces inside the back-tick characters are part of the output. So, if you indent the second line as follows, the output wouldn't look good:

let greeting = `Hello, I'm ${user.name}.
Welcome to the team!`;
console.log(greeting); // Hello, I'm Ted.
// Welcome to the team!

Modules

In ES6, JavaScript provides language-level support for modules. It uses export and import to organize modules and create a static module structure. That means you can determine imports and exports at compile time. Another important feature of ES6's module is that imports and exports must be at the top level. You cannot nest them inside blocks such as if and try/catch.

Besides static declarations of imports and exports, there is a proposal to use the import() operator to programmatically load modules. The proposal is, at the time of writing, at stage 3 of the TC39 process. You can checkout the details at https://github.com/tc39/proposal-dynamic-import.

To create a module, all you need to do is to put your JavaScript code into a .js file. You can choose to use tools such as Babel (http://babeljs.io) to compile ES6 code into ES5, together with tools such as webpack (https://webpack.js.org) to bundle the code together. Or, another way to use the module files is to use <script type="module"> to load them into browsers. 

Export

Inside a module, you can choose to not export anything. Or, you can export primitive values, functions, classes, and objects. There are two types of exports—named exports and default exports. You can have multiple named exports in the same module but only a single default export in that module.

In the following examples, we will create a user.js module that exports the User class, a tasks.js module that tracks the count of total completed tasks, and a roles.js module that exports role constants.

Let's have a look at user.js file:

1. export default class User {
2. constructor (name, role) {
3. this.name = name;
4. this.role = role;
5. }
6. };

In this module, we export the User class inline as the default export by placing the keywords export and default in front of it. Instead of declaring an export inline, you can declare the User class first and then export it at the bottom, or anywhere that is at the top level in the module, even before the User class.

Let's have a look at roles.js file:

1. const DEFAULT_ROLE = 'User';
2. const ADMIN = 'Admin';
3. export {DEFAULT_ROLE as USER, ADMIN};

In this module, we create two constants and then export them using named exports in a list by wrapping them in curly brackets. Yes, in curly brackets. Don't think of them as exporting an object. And as you can see in line 3, we can rename things during export. We can also do the rename with import too. We will cover that shortly.

Let's have a look at tasks.js file:

1. console.log('Inside tasks module');
2. export default function completeTask(user) {
2. console.log(`${user.name} completed a task`);
3. completedCount++;
4. }
5. // Keep track of the count of completed task
6. export let completedCount = 0;

In this module, in line 2, we have a default export of the completeTask function and a named export of a completedCount variable in line 6.

Import

Now, let's create a module app.js to import the modules we just created.

Let's have a look at app.js file:

1.  import User from './user.js';    
2. import * as Roles from './roles.js';
3. import completeTask from './tasks.js';
4. import {completedCount} from './tasks.js';
5.
6. let user = new User('Ted', Roles.USER);
7. completeTask(user);
8. console.log(`Total completed ${completedCount}`);
9. // completedCount++;
10. // Only to show that you can change imported object.
11. // NOT a good practice to do it though.
12. User.prototype.walk = function () {
13. console.log(`${this.name} walks`);
14. };
15. user.walk();

In line 1, we use default import to import the User class from the user.js module. You can use a different name other than User here, for example, import AppUser from './user.js'. default import doesn't have to match the name used in the default export.

In line 2, we use namespace import to import the roles.js module and named it Roles. And as you can see from line 6, we access the named exports of the roles.js module using the dot notation.

In line 3, we use default import to import the completeTask function from the tasks.js module. And in line 4, we use named import to import completedCount from the same module again. Because ES6 modules are singletons, even if we import it twice here, the code of the tasks.js module is only evaluated once. You will see only one Inside tasks module in the output when we run it. You can put default import and named import together. The following is equivalent to the preceding lines 3 and 4:

import completeTask, {completedCount} from './tasks.js';

You can rename a named import in case it collides with other local names in your module. For example, you can rename completedCount to totalCompletedTasks like this:

import {completedCount as totalCompletedTasks} from './tasks.js';

Just like function declarations, imports are hoisted. So, if we put line 1 after line 6 like this, it still works. However, it is not a recommended way to organize your imports. It is better to put all your imports at the top of the module so that you can see the dependencies at a glance:

let user = new User('Ted', Roles.USER);        
import User from './user.js';

Continue with the app.js module. In line 7, we invoke the completeTask() function and it increases the completedCount inside the tasks.js module. Since it is exported, you can see the updated value of completedCount in another module, as shown in line 8.

Line 9 is commented out. We were trying to change the completedCount directly, which didn't work. And if you uncomment it, when we run the example later, you will see TypeError, saying that you cannot modify a constant. Wait. completedCount is defined with let inside the tasks.js module; it is not a constant. So what happened here?

Import declarations have two purposes. One, which is obvious, is to tell the JavaScript engine what modules need to be imported. The second is to tell JavaScript what names those exports of other modules should be. And JavaScript will create constants with those names, meaning you cannot reassign them.

However, it doesn't mean that you cannot change things that are imported. As you can see from lines 12 to 15, we add the walk() method to the User class prototype. And you can see from the output, which will be shown later, that the user object created in line 6 has the walk() method right away.

Now, let's load the app.js module in an HTML page and run it inside Chrome.

Here is the index.html file:

1. <!DOCTYPE html>
2. <html>
3. <body>
4. <script type="module" src="./app.js"></script>
5. <script>console.log('A embedded script');</script>
6. </body>
7. </html>

In line 4, we load app.js as a module into the browser with <script type="module">, which is specified in HTML and has the defer attribute by default, meaning the browser will execute the module after it finishes parsing the DOM. You will see in the output that line 5, which is script code, will be executed before the code inside app.js.

Here are all the files in this example:

/app.js
/index.html
/roles.js
/tasks.js
/user.js

You need to run it from an HTTP server such as NGINX. Opening index.html directly as a file in Chrome won't work because of the CORS (short for Cross-Origin Resource Sharing) policy, which we will talk about in another chapter.

If you need to spin up a simple HTTP server real quick, you can use http-serverwhich requires zero configuration and can be started with a single command. First of all, you need to have Node.js installed and then run npm install http-server -g. Once the installation completes, switch to the folder that contains the example code, run http-server -p 3000and then open http://localhost:3000 in Chrome.

You will need to go to Chrome's Developer Tools to see the output, which will be similar to the following:

A embedded script
Inside tasks module
Ted completed a task
Total completed 1
Ted walks

As you can see from the output, the browser defers the execution of the module's code, while the script code is executed immediately, and the tasks.js module is only evaluated once.

Starting from ES6, there are two types in JavaScript—scripts and modules. Unlike scripts code, where you need to put 'use strict'; at the top of a file to render the code in strict mode, modules code is automatically in strict mode. And top-level variables of a module are local to that module unless you use export to make them available to the outside. And, at the top level of a module, this refers to undefined. In browsers, you can still access a window object inside a module.

Promises

Promises are another option in addition to callbacks, events for asynchronous programming in JavaScript. Before ES6, libraries such as bluebird (http://bluebirdjs.com) provided promises compatible with the Promises/A+ spec. 

A promise represents the eventual result of an asynchronous operation, as described in the Promises/A+ spec. The result would be a successful completion or a failure. And it provides methods such as .then(), and .catch() for chaining multiple asynchronous operations together that would make the code similar to synchronous code that is easy to follow.

The features of ES6 promises are a subset of those provided by libraries such as bluebird. In this book, the promises we use are those defined in the ES6 language spec unless otherwise specified.

Let's look at an example in which we will get a list of projects from the server and then get tasks of those projects from the server in a separate API call. And then we will render it. The implementation here is a simplified version for demonstrating the differences between using callbacks and promises. We use setTimeout to stimulate an asynchronous operation.

First of all, let's see the version that uses callbacks:

1.  function getProjects(callback) {
2. // Use setTimeout to stimulate calling server API
3. setTimeout(() => {
4. callback([{id:1, name:'Project A'},{id:2, name:'Project B'}]);
5. }, 100);
6. }
7. function getTasks(projects, callback) {
8. // Use setTimeout to stimulate calling server API
9. setTimeout(() => {
10. // Return tasks of specified projects
11. callback([{id: 1, projectId: 1, title: 'Task A'},
12. {id: 2, projectId: 2, title: 'Task B'}]);
13. }, 100);
14. }
15. function render({projects, tasks}) {
16. console.log(`Render ${projects.length} projects and
${tasks.length} tasks`);
17. }
18. getProjects((projects) => {
19. getTasks(projects, (tasks) => {
20. render({projects, tasks});
21. });
22. });

As you can see in lines 18 to 22, we use callbacks to organize asynchronous calls. And even though the code here is greatly simplified, you can still see that the getProjects(), getTasks(), and render() methods are nested, creating a pyramid of doom or callback hell.

Now, let's see the version that uses promises:

1.  function getProjects() {
2. return new Promise((resolve, reject) => {
3. setTimeout(() => {
4. resolve([{id:1, name:'Project A'},{id:2, name:'Project B'}]);
5. }, 100);
6. });
7. }
8. function getTasks(projects) {
9. return new Promise((resolve, reject) => {
10. setTimeout(() => {
11. resolve({projects,
12. tasks:['Buy three tickets', 'Book a hotel']});
13. }, 100);
14. });
15. }
16. function render({projects, tasks}) {
17. console.log(`Render ${projects.length} projects and ${tasks.length} tasks`);
18. }
19. getProjects()
20. .then(getTasks)
21. .then(render)
22. .catch((error) => {
23. // handle error
24. });

In lines 1 to 15, in the getProjects() and getTasks() method, we wrap up asynchronous operations inside a Promise object that is returned immediately. The Promise constructor function takes a function as its parameter. This function is called an executor function, which is executed immediately with two arguments, a resolve function and a reject function. These two functions are provided by the Promise implementation. When the asynchronous operation completes, you call the resolve function with the result of the operation or no result at all. And when the operation fails, you can use the reject function to reject the promise. Inside the executor function, if any error is thrown, the promise will be rejected too.

A promise is in one of these three states:

  • Pending: The initial state of a promise
  • Fulfilled: The state when the operation has completed successfully
  • Rejected: The state when the operation didn't complete successfully due to an error or any other reason

You cannot get the state of a promise programmatically. Instead, you can use the .then() method of the promise to take action when the state changes to fulfilled, and use the .catch() method to react when the state changed to rejected or an error are thrown during the operation.

The .then() method of a promise object takes two functions as its parameters. The first function in the argument is called when the promise is fulfilled. So it is usually referenced as onFulfilledand the second one is called when the promise is rejected, and it is usually referenced as onRejected. The .then() method will also return a promise object. As you can see in lines 19 to 21, we can use .then() to chain all the operations. The .catch() method in line 22 is actually a syntax sugar of .then(undefined, onRejected). Here, we put it as the last one in the chain to catch all the rejects and errors. You can also add .then() after .catch() to perform further operations.

The ES6 Promise also provides the .all(iterable) method to aggregate the results of multiple promises and the .race(iterable) method to return a promise that fulfills or rejects as soon as one of the promises in the iterable fulfills or rejects.

Another two methods that ES6 Promise provides are the .resolve(value) method and the .reject(reason) method. The .resolve(value) method returns a Promise object. When the value is a promise, the returned promise will adopt its eventual state. That is when you call the .then() method of the returned promise; the onFulfilled handler will get the result of the value promise. When the value is not a promise, the returned promise is in a fulfilled state and its result is a value. The .reject(reason) method returns a promise that is in a rejected state with the reason passed in to indicate why it is rejected.

As you might have noticed, promises do not help you write less code, but they do help you to improve your code's readability by providing a better way of organizing code flow.

You have been reading a chapter from
Building Applications with Spring 5 and Vue.js 2
Published in: Oct 2018
Publisher: Packt
ISBN-13: 9781788836968
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image