Book Image

Building Applications with Spring 5 and Vue.js 2

By : James J. Ye
Book Image

Building Applications with Spring 5 and Vue.js 2

By: James J. Ye

Overview of this book

Building Applications with Spring 5 and Vue.js 2, with its practical approach, helps you become a full-stack web developer. As well as knowing how to write frontend and backend code, a developer has to tackle all problems encountered in the application development life cycle – starting from the simple idea of an application, to the UI and technical designs, and all the way to implementation, testing, production deployment, and monitoring. With the help of this book, you'll get to grips with Spring 5 and Vue.js 2 as you learn how to develop a web application. From the initial structuring to full deployment, you’ll be guided at every step of developing a web application from scratch with Vue.js 2 and Spring 5. You’ll learn how to create different components of your application as you progress through each chapter, followed by exploring different tools in these frameworks to expedite your development cycle. By the end of this book, you’ll have gained a complete understanding of the key design patterns and best practices that underpin professional full-stack web development.
Table of Contents (23 chapters)
Title Page
Copyright and Credits
Dedication
About Packt
Contributors
Preface
Index

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 featuresintroduced 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 useletto define variables or useconstto define constants, and they will have block-level scope. And in the same scope, you can not redefine a variable usinglet. Also, you cannot access a variable or a constant that is defined withletorconstbefore its declaration, since there is no variable hoisting withletorconst.

Let's see the followingworkout 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 thegym variable, and it is visible in theworkout() function body. In line 5, we declare thegymvariable within the for loop block. It shadows thegymvariable 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 gymconstant 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 causeTypeError.

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

In line 26, we define the exercisesvariable within theifblock. And as you can see from line 29, it is no longer accessible outside theifblock.

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, usingletandconst, 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 implementUser, andTeamMember:

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 aninterestsCount 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 newwork() 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 tomember.interestsCount, but it won't work because there is no setter defined. And line 46 shows that accessingmember.tasks results in undefined because we didn't define a getter for it.

Note

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

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

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 ofUser. If you add it directly toUseras follows, you will getTypeError:

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 addsleepas a property of theUserclass itself or, more precisely, the Userconstructor function itself. And you might have already noticed thatsleepbecomes a static method of theUserclass. 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 anadvisorobject with a TeamMemberobject 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 theadvisorobject's__proto__property makesadvisoran instance ofTeamMember:

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

Line 5 is a shorthand assignment ofadvice:advice. Line 7 is creating thegreeting()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, theStay hungryproperty 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 ownthis. 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 theshoppingCartobject itself, even it is inside the callback of theArray.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(), orFunction.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,thisalways resolves to its surrounding execution context. Thecall(),apply(), orbind() method has no effect on its execution context.

Note

Unlike ES5 functions, arrow functions do not have their ownargumentsobject. Thearguments object is a reference to the surrounding function'sargumentsobject.

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 changecheckoutto an arrow function. And because an arrow function uses its surrounding execution context,thisin line 5 no longer references the shoppingCartobject and it will throwUncaught 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 theuserobject. 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 anewoperator. 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 addToCartfunction 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 theargumentsobject 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 parametertodos. It is prefixed with three dots and is the last named parameter of theworkout()function. To archive this in ES5, as you can see in line 3, we need to slice theargumentsobject. And in line 11, you can see that the rest parametertodos 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 theurgentTasksarray and thenormalTasksarray. And in line 7, we use spread syntax to expand theallTasksarray and pass each element of it as arguments of the function. And the firstargument has the valueBuy three ticketswhile thesecondargument has the valueBook 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, thenameandinterestsvariables defined in line 2 pick up the values of the properties with the same name in theuserobject. And thetasksvariable doesn't have a matching property in theuserobject. 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 theuserobject and assign it to thefirstName 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, thefourth variable doesn't match any elements in the array and its value remains asundefined. 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 putinterestsin the destructuring assignment, JavaScript doesn't really declare it. As you can see in line 4, accessing it will raiseReferenceError. 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 fruitsarray 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 undefinedornull. 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 usesexportandimportto 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 andtry/catch.

Note

Besides static declarations of imports and exports, there is a proposal to use theimport()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 auser.jsmodule that exports theUserclass, atasks.jsmodule that tracks the count of total completed tasks, and aroles.jsmodule that exports role constants.

Let's have a look atuser.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 theUserclass inline as the default export by placing the keywordsexportanddefaultin front of it. Instead of declaring an export inline, you can declare theUser class first and then export it at the bottom, or anywhere that is at the top level in the module, even before theUserclass.

Let's have a look atroles.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 withimport 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 thecompleteTaskfunction and a named export of acompletedCount variable in line 6.

Import

Now, let's create a moduleapp.jsto 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 theUserclass from theuser.jsmodule. 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 theroles.jsmodule and named itRoles. And as you can see from line 6, we access the named exports of the roles.jsmodule using the dot notation.

In line 3, we use default import to import thecompleteTask function from thetasks.jsmodule. And in line 4, we use named import to importcompletedCountfrom the same module again. Because ES6 modules are singletons, even if we import it twice here, the code of thetasks.jsmodule is only evaluated once. You will see only oneInside tasks modulein 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 renamecompletedCounttototalCompletedTaskslike 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 thecompleteTask()function and it increases thecompletedCountinside thetasks.jsmodule. Since it is exported, you can see the updated value ofcompletedCountin another module, as shown in line 8.

Line 9 is commented out. We were trying to change thecompletedCountdirectly, which didn't work. And if you uncomment it, when we run the example later, you will seeTypeError, saying that you cannot modify a constant. Wait. completedCountis defined withletinside thetasks.jsmodule; 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 thewalk()method to theUserclass prototype. And you can see from the output, which will be shown later, that theuserobject created in line 6 has thewalk()method right away.

Now, let's load theapp.jsmodule in an HTML page and run it inside Chrome.

Here is theindex.htmlfile:

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 loadapp.jsas a module into the browser with<script type="module">, which is specified in HTML and has thedeferattribute 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 insideapp.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. Openingindex.htmldirectly 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.

Note

If you need to spin up a simple HTTP server real quick, you can use http-server, which 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 3000, and 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 thetasks.jsmodule 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 useexportto make them available to the outside. And, at the top level of a module,thisrefers toundefined. In browsers, you can still access a windowobject 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.

Note

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 usesetTimeoutto 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(), andrender() 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 thegetProjects()andgetTasks()method, we wrap up asynchronous operations inside aPromiseobject that is returned immediately. ThePromiseconstructor function takes a function as its parameter. This function is called an executor function, which is executed immediately with two arguments, aresolvefunction and arejectfunction. These two functions are provided by thePromiseimplementation. When the asynchronous operation completes, you call theresolvefunction with the result of the operation or no result at all. And when the operation fails, you can use the rejectfunction 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 asonFulfilledand the second one is called when the promise is rejected, and it is usually referenced asonRejected. 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 Promisealso 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 aPromise object. When thevalueis a promise, the returned promise will adopt its eventual state. That is when you call the.then()method of the returned promise; theonFulfilledhandler will get the result of thevaluepromise. When thevalueis 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 thereasonpassed 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.