npm and Advanced Modules
Advanced modules and library usage
One of the wonderful things about the modern JavaScript ecosystem is that there are literally thousands of libraries for us to use, solving any number of complex problems we might encounter in the realm of web development. Learning how to install, manage, and import these third-party dependencies is a key part of being an effective web developer.
More Modules
You'll recall that we've been working with our own library of components through import-ing and export-ing default chunks of JavaScript. But there are a few variations on this these syntaxes that will help us keep our code organized as our applications begin to pull in third-party dependencies. Here's a quick overview:
// import the default export from a module
import toolbox from 'toolbox';
// import a single export (of many) from a module
import { tool } from 'toolbox';
// import many exports at once from a module
import * from 'toolbox';
// import many exports as a named Object from a module
import * as tools from 'toolbox';
// export a single, default chunk from a module
export default 'toolbox';
// export a single chunk (of many) from a module
export 'tool';Let's see how these import/export variations might help us organize our large state and content trees in our portfolio projects.
Portfolio Project 1
Modular State and Content
When last we left our state tree, it was beginning to look something like this:
This is pretty ungainly. Let's see if we can clean this up by extracting all of these disparate states into their own modules.
To start, let's create a directory called
storethat will contain all of ourstates. Inside thatstoredirectory, create a singleindex.jsfile. This file will act similarly to the way theindex.htmldoes for HTML documents: our bundler will, by default, look for thatindexif no explicit file path is provided.Inside of
store, let's create a separate JavaScript file for each part of our state tree, That means newHome,Blog,Contact, andProjectsfiles. Each of these files should have a singledefaultexport of a single Object. That Object should represent that single piece of the original state tree. So, for example, theBlogmodule would look like this:Once every state module has been created, we can re-export each of those modules with a name in
store/index.js. The syntax for one of those re-exports would beexport { default as SomeName } from './some-location';. See if you can re-export all of these pieces of our state tree with this syntax! HINT:Now that this base module has been set up we should be able to
importour states through the module system. In this case, we would like toimportall of the states in one big glob using the* as namesyntax. Try the following in your root-levelindex.jsfile, and see if you can figure out what the data type ofstateswill be:We should now be able to access any piece of our
statestree, just as before! Except that this time, all of the complexity of our application state is hidden away behind our module system (which is a good thing).Now that we've ironed out navigation, let's see if we can finally get our content to change in response to our user input! You should still have HTML files that represent content for your
Blog,Content, andProjectslanding pages that we haven't yet incorporated into our new Single-Page architecture. Let's convert those pieces of HTML into their owncomponents. To start, let's create aPagesdirectory inside ourcomponentsdirectory. This is a common pattern when dealing with a group of similar components. In this case, we're grouping all of the different page-level (or content-level) templates.Let's repeat the export pattern from our
states. That means creating anindex.jsfile next to aBlog.js,Contact.js,Home.js, andProjects.jsfile inside ofcomponents/Pages. Each Page component shouldexportsome HTML as a template literal, e.g.:...and re-
exportthosedefaults with a name fromcomponents/Pages/index.js, e.g.:Now each
statecomponent can point to a component from thecomponents/Pagesdirectory. Let's use a String to connect that content to each state component as abodyproperty. For example, ourBlogstate might become:Once you've associated some content with each piece of the state tree, it's time to change our
Contentcomponent to allow for variation in thebodyproperty of astateparameter. That means ourContentcomponent becomes:
Now you should have a Single-Page application that behaves almost exactly like our old HTML page-based site, but with a lot more flexibility and some real performance wins for our users.
Dependencies
We've already worked with "dependencies" during our time with CSS. A dependency is any piece of code (regardless of language) provided by a third-party upon which our project depends. Previous dependencies were included through <link> tags in our HTML files, and were retrieved from a Content Delivery Network (e.g. CDNJS). These dependencies included things like CSS reset libraries (normalize.css), fonts, and icons.
In the realm of JavaScript, the number of possible dependencies is much larger than we've encountered in CSS-land. While we could include many third-party libraries from a CDN using a <script> tag, there are a couple of good reasons not to:
It's easier to update third-party dependencies through a dedicated dependency management system instead of keeping track of those dependencies by hand. JavaScript's dependency management program is called
npm.As your application gets more complex, it's not uncommon to have more than 10 JavaScript dependencies, many of which will need to be loaded in a certain order to work correctly. That's a lot of
<script>tags to keep track of! Instead, we caninstalldependencies withnpmandimportthose dependencies into a single bundle.Unlike with CSS, it's rare to use every piece of a JavaScript library. With our module bundler, we can use a process called tree-shaking to get rid of un-unsed parts of our dependencies. This means that our users don't need to download nearly as many bytes of data as they would if the entire dependency were included.
Let's go through two practical examples of using third-party dependencies to improve navigation for our users!
lodash
lodashlodash is a library of useful utility functions that make working with collections of data much easier. On top of the collections helpers, though, there are a number of functions that make working with Strings just a bit easier.
Portfolio Project 2
lowercased routes
Install
lodashby typing the following into your terminal:And just like that, we should be able to
importindividual helper functions in any of our own modules. Try adding this to the top ofNavigation.js:lowerCaseis a function that turns any String into its lower-cased variant. Since lower-case routes are much more common than the upper-case routes that we've been using so far, let's turn ourbuildLinksfunction into something that looks like this instead:Along with the changes to
buildLinks, we'll also need to capitalize our component names inhandleNavigationin ourindex.jsfile. That's as simple as addingimport { capitalize } from 'lodash';to the top of the file before modifyinghandleNavigationto be:
Not a bad way to offload some of that complexity to a library maintainer, right?
Routing and navigo
navigoWhile our SPA has some nice performance benefits for users (once our JavaScript has loaded) and some nice features during development (from automatic re-bundling and hot-module reloading), there's a major drawback for those visiting our application for the first time: as written, there's no way to navigate directly to any "page" other than the landing page/Home component. That's because our JavaScript application no longer differentiates between any URL paths; localhost:1234 is treated the same as localhost:1234/blog and localhost:1234/whatever. To capture these URLs and treat them correctly, we need a client-side router that listens for changes to the URL and responds with our startApp function called with the correct state.
It's very possible (and a fun bonus exercise) to use window.location.pathname to create a very basic client-side router, but we can make things easier on ourselves by leveraging the work of others. For this project, we'll use navigo.
Portfolio Project 3
Routing with navigo
navigoLet's start by installing the
navigolibrary withnpm install --save navigoWe should now be able to import the default
Navigoconstructor at the top of ourindex.jsfile with:Navigois a special type of function that can be used to create a new Object (more on these later). To create therouterObject that we'll use to route requests, create aroutervariable like so:routerworks by chaining a number of different functions together (more on this idea of chaining functions later, too). The two that we'll use areonandresolve.onuses a callback structure: whenever a URL matches the pattern given toonas its first argument, the function provided as the second argument is called. We useresolveat the end of the chain to kick off the client-side routing process. Try this onindex.js:You should see
hello home page!whenever you visit the landing page of your application.onalso allows us to capture routes as something called params (short for parameters) using a special syntax (but one that's common among routers of all types). Try experimenting with this setup:Now we have the ability to execute JavaScript in response to the URL. But what is it we actually want to do? Previously, we were using
handleNavigationto navigate around through click events targets. Now, we can handle routes a bit more fluidly. Let's refactorhandleNavigationinto ahandleRoutefunction that looks something like this:...which we can apply to the
:pagesroute as a callback, like so:We should now be able to use the URL to navigate between application states... pretty cool! But we're not done yet. What if we want to be able to navigate to different URLs using the links in our
Navigationcomponent? Right now, we're hijacking that process with custom event listeners, so our URL isn't changing at all. Luckily,navigogives us a helper function to let us handle those anchor tags without reloading the page (and without the custom event listeners). Let's start by refactoring yourstartAppfunction to something a bit more simple:Now we can add a special attribute to our generated links called
data-navigoto allow ourrouterto "hijack" these anchor tags. That would mean that ourbuildLinksfunction inNavigationis as simple as:Now most of our links should be navigable using client-side routing!
You may have noticed that "most" from above. What's the bug? You'll notice that we're currently handling our landing page (the
/route) with aconsole.logstatement. What we'd really like to do is start our application with theHomebranch of thestatetree. So let's modify our routing to account for users navigating to our landing page:Almost done now! The last bug: you'll notice that any
Homelink actually links to/homeon click. Instead of doing that, let's route allHomelinks to/, which is our actual home page. We'll do that over inNavigation.js'sbuildLinksfunction by doing a quick refactor to:
And there you have it! Now you have a fully-operational SPA, ready for your users with lightning-fast navigation.
Last updated