Third Party Libraries
This should be easy! We have JavaScript code that we're going to write, but we want to use some existing libraries to help us do that.
In the World Before WebPack™, you'd often add the library's CDN-hosted URL to a <script>
tag and be on your way.
That would work because the library would dump itself into the global namespace. That's not what we want to do for a few reasons.
First, this means that every library everywhere has to agree on a unique set of names so as to not squash each other. That is difficult.
Secondly, it means that we have no control over when or how these libraries are loaded, which becomes important for writing tests.
Thirdly, this isn't how third-party JavaScript libraries are built - they mostly assume you are using a module bundler like Webpack.
Point is, dumping into global namespace bad. I promise not to get on my soapbox about this again, so let's get to it. Let's add a library to our project!
But wait, we don't really have a project, yet. Let's make one real quick.
A Real Project: Markdown Previewing
Despite how awesome our console.log
'ing supersystem is, we should work on something more interesting. The simplest thing I
could think of is a markdown previewer. There's a markdown module we can use, so that will be our third-party library!
First, we'll add it to our package.json
file using yarn add
:
> yarn add markdown
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info This module is OPTIONAL, you can safely ignore this error
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
└─ markdown@0.5.0
info All dependencies
├─ markdown@0.5.0
└─ nopt@2.1.2
Done in 2.97s.
warning Error running install script for optional dependency: "/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments:
Directory: /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@3.8.0
gyp info using node@12.3.0 | darwin | x64
gyp info spawn /usr/bin/python
gyp info spawn args [
gyp info spawn args '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args 'binding.gyp',
gyp info spawn args '-f',
gyp info spawn args 'make',
gyp info spawn args '-I',
gyp info spawn args '/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents/build/config.gypi',
gyp info spawn args '-I',
gyp info spawn args '/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args '-I',
gyp info spawn args '/Users/davec/.node-gyp/12.3.0/include/node/common.gypi',
gyp info spawn args '-Dlibrary=shared_library',
gyp info spawn args '-Dvisibility=default',
gyp info spawn args '-Dnode_root_dir=/Users/davec/.node-gyp/12.3.0',
gyp info spawn args '-Dnode_gyp_dir=/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args '-Dnode_lib_file=/Users/davec/.node-gyp/12.3.0/<(target_arch)/node.lib',
gyp info spawn args '-Dmodule_root_dir=/Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents',
gyp info spawn args '-Dnode_engine=v8',
gyp info spawn args '--depth=.',
gyp info spawn args '--no-parallel',
gyp info spawn args '--generator-output',
gyp info spawn args 'build',
gyp info spawn args '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
SOLINK_MODULE(target) Release/.node
CXX(target) Release/obj.target/fse/fsevents.o
In file included from ../fsevents.cc:6:
In file included from ../../nan/nan.h:54:
/Users/davec/.node-gyp/12.3.0/include/node/node.h:107:12: fatal error: 'util-inl.h' file not found
# include <util-inl.h>
^~~~~~~~~~~~
1 error generated.
make: *** [Release/obj.target/fse/fsevents.o] Error 1
gyp ERR! build error
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack at ChildProcess.onExit (/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack at ChildProcess.emit (events.js:200:13)
gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Darwin 18.7.0
gyp ERR! command \"/Users/davec/.asdf/installs/nodejs/12.3.0/bin/node\" \"/Users/davec/.asdf/installs/nodejs/12.3.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/davec/Projects/Personal/wtf-is-a-webpack/work/work/node_modules/fsevents
gyp ERR! node -v v12.3.0
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok"
This will also run yarn install
which will download the markdown package to node_modules
.
Let's create our HTML first by replacing dist/index.html
with the following:
<!DOCTYPE html>
<html>
<head>
<script src="bundle.js"></script>
</head>
<body>
<h1>Markdown Preview-o-tron 7000!</h1>
<form id="editor">
<textarea id="source" rows="10" cols="80"></textarea>
<br>
<input type="submit" value="Preview!">
</form>
<hr>
<section id="preview">
</section>
</body>
</html>
The app will work by listening to a submit event on that form and, when the button is pressed, render a preview of the markdown. Rather than put this all in line, we'll make our own module that constructs this function.
The function will use the markdown
library to do the actual rendering, so the module we need to write should take some
inputs—where to find the source and where to render the preview, and produce a side-effect, namely the formatted markdown gets
written to the part of the DOM where the preview should go.
Our entry point is js/index.js
, so let's create that like so:
import markdownPreviewer from "./markdownPreviewer";
window.onload = function() {
document.getElementById("editor").addEventListener(
"submit",
markdownPreviewer.attachPreviewer(
document, // pass in document
"source", // id of source textarea
"preview")); // id of preview DOM element
};
markdownPreviewer
is a file we'll create in a moment. Hopefully you know enough JavaScript to see what's going on (we're
passing in document
so that our code doesn't have to depend on global state, which will help write tests later).
Now, let's create js/markdownPreviewer.js
, which does all the work.
We need to import our markdown library, however it's in node_modules
, so how does that work?
Actually Importing Third-party Libraries
The answer is: finding code in node_modules
Just Works™. This is one of the scant defaults Webpack provides.
Now, before we write import markdown from "markdown";
we need to learn a bit more about how import
works.
The markdown package exports a structure like so;
{
"markdown": {
"toHTML": function() { ... }
}
}
Meaning, if we use import
the way we've seen before, we'd need to write markdown.markdown.toHTML
. Yuck. Fortunately, import
allows you to pluck symbols out of the exported object by using curly braces:
import { markdown } from "markdown";
This allows us to write markdown.toHTML
, which is better.
With that said, here's how js/markdownPreviewer.js
should look:
import { markdown } from "markdown";
var attachPreviewer = function($document,sourceId,previewId) {
return function(event) {
var text = $document.getElementById(sourceId).value,
preview = $document.getElementById(previewId);
preview.innerHTML = markdown.toHTML(text);
event.preventDefault();
};
}
export default {
attachPreviewer: attachPreviewer
}
What's up with export default
? Writing import attachPreviewer from "./markdownPreviewer";
is asking Webpack (or whoever) to import the default exported thing. Using export default
sets that thing, which is handy when you are just exporting one function (though, let's be honest, it's not handy, because it's another special form to have to understand and remember to do, all to save a few scant keystrokes and obscure intent).
Let's package everything up and see if it works.
> yarn webpack
yarn run v1.21.1
$ webpack --config webpack.config.js --display-error-details
Hash: 8f740e9980f30a8938d5
Version: webpack 4.41.5
Time: 173ms
Built at: 01/21/2020 5:45:34 PM
Asset Size Chunks Chunk Names
bundle.js 80.3 KiB 0 [emitted] main
Entrypoint main = bundle.js
[0] ./js/index.js 334 bytes {0} [built]
[1] ./js/markdownPreviewer.js 381 bytes {0} [built]
+ 6 hidden modules
Done in 0.62s.
If we open up dist/index.html
, we should see our UI:
And, if you type in some markdown and hit the submit button, voila it's rendered inline by our markdown library:
Who would've thought it takes 1,000 words to talk about using third party libraries, but this is JavaScript and we should be thankful Webpack exists to make up for the language's deficiencies.
We mentioned earlier that we passed document
into our attachPreviewer
function to facilitate testing. We
should probably look into testing at this point because a) I get nervous writing untested code and b) things we'll
learn later, like CSS and ES6, will definitely break things and we want to make sure we can continue to run tests
as we use more sophisticated features of Webpack.