7.1 Project Structure
# Projects
When creating web apps, it means that you will be writing many lines of code and often using multiple JavaScript files. So, we need to think about how to organize our code for efficient reuse of the code as well as effectively combining our code with the code from libraries and other developers.
# Namespaces
As already discussed, when you attach multiple script files to the same webpage, they are all able to see each other's variables within the same global scope.
This many not be an issue when you are a single developer or a small team. You can coordinate and watch for name conflicts with your variables and objects.
It becomes much more difficult when you start to create libraries to reuse across projects, or include libraries written by others. The chance of naming conflicts starts to rise.
Best Practice
Try to minimize the number of variables and functions (named objects) that you have in your global namespace.
A common approach to solving this problem is to create namespaces
. You can wrap all your project code or even parts of your project code inside of a single object. Use const
to declare the object and be careful that your object name is unique. Your Object is your namespace
.
A namespace would look like this:
const MYAPP = {
apikey: 'SomeUniqueAPIKey',
today: new Date(),
init: function(){
//an initial function that we will call in our namespace
},
someMethod: function(){
//some method belonging to our namespace
}
}
2
3
4
5
6
7
8
9
10
The namespace
has a couple property values and a couple methods.
If you wanted to access one of the properties or one of the methods inside the namespace then you just have to put the name of the const
in front of the property or method.
MYAPP.apikey // gets the value of the apikey property inside of MYAPP
MYAPP.someMethod(); // call the someMethod function
document.addEventListener('DOMContentLoaded', MYAPP.init);
//call the init function WHEN the DOMContentLoaded event happens
2
3
4
5
A common practice is to wrap all your project code inside one or two namespace objects and then have one listener for the DOMContentLoaded
event that will call some initial method in one of your namespace
objects.
//APP namespace
const APP = {
//this is the primary namespace to control user interaction, events, data loading
init: function(){
APP.addListeners();
},
addListeners: function(){
//function to add event listeners to the app each time the page loads
window.addEventListener('pageshow', NAV.pageSpecific);
document.querySelector('form#searchForm').addEventListener('submit', APP.doSearch)
},
doSearch: function(ev){
//check for keyword and do a search
}
}
//NAV namespace
const NAV = {
//this object deals with history and navigation and form submission
pageSpecific: function(ev){
//check for id on body element and do page specific things
}
}
//start everything
document.addEventListener('DOMContentLoaded', APP.init);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Remember when you use namespaces to include the namespace in front of the method or property name.
# MultiPage Scripts
When you build a web page that contains multiple HTML files, it means that each time the user navigates to a new page, the JavaScript is being reloaded.
When the JavaScript file reloads, it means that it loses all the values that it had in its variables. The DOMContentLoaded
event fires again. Your init
function will be running again. Event listeners are re-added.
If you have code that runs differently depending on which page you are on, you can check for things like an id
attribute in the <body>
tag to see which page you are on. A switch
statement is usually a good way to check for and write the page specific code.
let id = document.body.id
switch(id){
case 'home':
//on the home page
break;
case 'contact':
//on the contact page
break;
case 'profile':
//on the profile page
break;
default:
//on any other page
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# ES Modules
In recent years, a new ability was added to browsers that let us scope each of our namespaces or imported files separately.
With ES Modules
we define a first file to load and then start to import
other scripts into the first file. Each of the files that we import
gets its own scope and gets to define exactly what is allowed to be export
ed from itself. The first file needs to be loaded by a <script>
tag with the type="module"
attribute added. All the other scripts will be loaded via import
statements inside the first JS file.
<script src="js/app.js" type="module"></script>
After we added the type attribute we can start adding import
statements to our app.js
. Alternatively, you can use the .mjs
file extension for files that you want to use as modules.
//app.js
import { someFunc } from './utils.js';
import { otherFunc, someConst } from 'https://example.com/scripts.js';
2
3
We now have the ability to make variables, objects, and functions private. They can be part of our code but we control who is allowed to access them.
# Exporting
As mentioned, to be able to import things into our main JS file, we need to explicitly export
them from the other files. With no export
there will be nothing to import
.
We use the export
keyword to export an object. Inside that object we list the items that will be available through import
. If a function or variable is not listed inside the export
object then you can consider it to be private to
const PI = 3.14;
const name = 'Steve';
function f1() {
console.log('Hello', name);
//we can use the name variable here without exporting it
}
//we are exporting PI and f1
//name is private to this file
export { PI, f1 };
2
3
4
5
6
7
8
9
10
11
# Default Exports
If there is only going to be one thing exported from a file, then we can add the keyword default
to an export statement. In this way, the default export becomes an exported object instead of being wrapped in one. In the import for a default export we can also use any name we want.
//bob.js
export default function bob() {
console.log('this function can be called anything in the import');
}
2
3
4
The import statement for the function bob
would look like this:
//app.js
import frank from './bob.js';
2
Notice how you can use a different name because bob
was the default export
.
It is also possible to have a default export
as well as other exports.
export default function bob() {
//this is the default export
}
function f1() {
//allowed to be exported but is not the default
}
function f2() {
//allowed to be exported but is not the default
}
export { f1, f2 };
2
3
4
5
6
7
8
9
10
11
12
13
# Import variations
There are a number of ways we can import scripts.
import * as thing from './script.js';
//all the things exported from script.js will be wrapped in thing
import { a, b, c } from './script.js';
//importing a, b, and c from script.js
//there could be other things exported that we didn't need.
import { a as x, b as w, c } from './script.js';
//importing a, b, and c from script.js
//a is renamed as x
//b is renamed as w
//c is left with its original name
import bob from './script.js';
//this means that bob was a default export
import bob, { a, b } from './script.js';
//bob would be the default export.
//a and b are non-default exports from the same file.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Dynamic Imports
Another thing that you can do with imports is make the import conditional. We can wait for something to happen - like the user visiting a certain page, or spending a minimum amount of time on the site, or filling out a form, or logging in. Then once that goal is achieved then we can dynamically import another script.
//using async/await with dynamic import
async function getScript() {
const { default: myDefault, foo, bar } = await import('./js/someScript.js');
//now we know that the script is loaded...
//we use destructuring to get the items from the imported script
}
//alternatively
import('./js/someScript.js').then(({ default: defaultObj, obj1, obj2 }) => {
//now we can use defaultObj, obj1, and obj2
});
2
3
4
5
6
7
8
9
10
11
# What to do this week
TODO
Things to do before next week.
- Read all the content from
Modules 7.1, 7.2, and 9.1
. - Continue working on the Hybrid Exercises