Dev. To /open-wc/sto…

Dev. To/Dakmor

Published: November 30, 2019 · 13 minutes to read

Building a Web application is a fairly large and challenging task. As with many large tasks, it makes sense to break them down into smaller pieces. For applications, this usually means splitting your application into separate components.

Once you start doing this, you’ll notice that you have a lot of individual parts in your hand, and it’s hard to get an overview of all those moving parts.

To solve this problem, we’ve been recommending storybooks for quite some time.

Its support for Web components has always been good (via @storybook/ Polymer), and the recent addition of @Storybook/Web-Components makes it even better.

However, some parts of the Storybook are not fine-tuned (open-WC) for developing Web components.

Let’s take a look at some of these points and how they can be improved.

You can read along on the attached Github Repo

After a typical storybook setup, it would look something like this

$ start-storybookInfo@storybook /web-components v5.3.0-alpha.40 info info => Loading presets info => Loading presets info => Loading custom manager config. info => Using default Webpack setup. webpack built b6c5b0bf4e5f02d4df8c in 7853ms ╭ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │ Storybook 5.3.0 - alpha. 40 started │ │ 8.99 s for manager and 8.53 s for preview │ │ Local: http://localhost:52796/ │ On your network: http://192.168.1.5:52796/ │ │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯# browser opens
Copy the code

When we compare this to starting the project with NPM init. open-wc

$ npm run start
es-dev-server started on http://localhost:8000
  Serving files from '/my-demo'.
  Opening browser on '/my-demo/'
  Using history API fallback, redirecting non-file requests to '/my-demo/index.html'
# browser opens
Copy the code

The most obvious difference is that in one case we had 2 builds of about 8 seconds and in the other case we had no builds at all.

So why are there 2 builds?

To understand why we need to do this, we first need to understand some of the requirements of a general-purpose presentation system like StoryBook.

Tour of the universal demonstration system

Let’s say we’re a startup and we’re creating a new application. Our technology of choice is vue.js. We happily started building our application and soon saw the need for a demo system to show and work on all these individual components. They said, go ahead, we built a demo system for Vue.

It might look something like this

Just some sample code — don’t think of it as good vue code 😅.

<template> <div class="hello"> <h1>{{ msg }}</h1> <ul> <li v-for="demo in demos" v-on:click="showDemo(demo.name)">{{demo.name}}</li> </ul> <div v-html="demo"></div> </div> </template> <script> export default { name: 'HelloWorld', props: { msg: { type: String, default: 'My Demo System', }, demos: { type: Array, default: () => [ { name: 'Demo One', content: '<h1>Hey there from demo one</h1>' }, { name: 'Demo Two', content: '<h1>I am demo two</h1>' }, ], }, }, methods: { showDemo: function(name) { this.demoIndex = this.demos.findIndex(el => el.name === name); }, }, data() { return { demoIndex: -1, }; }, computed: { demo() { if (this.demoIndex >= 0) { return this.demos[this.demoIndex].content; } return '<h1>Please select a demo by clicking in the menu</h1>'; ,}}}; </script>Copy the code

The code here displays only the most relevant information

For demos and more details, check out the vue-Demo-system folder.

You can start it by using NPM I && NPM Run Serve.

Everything was working and everyone was happy — life was good.

Fast forward 12 months and we have a new CIO. A new wind blows, and with it comes a thriving opportunity to develop a second app. However, the wind demands Angular this time. No problem — we’re professionals, so we started working on new apps.

Very early on, we saw a similar pattern — components were everywhere, and we needed a way to work and demonstrate them individually.

Ah, we think it’s easy, we already have a system 😬.

We did our best — but Angular components just don’t want to work well with vue demos 😭.

What can we do? Do we really need to recreate the Angular demo system now?

It seems our problem is that having the demo UI and the component demo on the same page has the unnecessary side effect of only using the UI system in the demo.

Not really. That’s 😅

Can we separate the UI from the demo?

How about using iframes and communicating via postMessage only? Does that mean that each window can do whatever it wants? 🤞

Let’s do a simple POC, using

  • A UL/LI list serves as a menu
  • Displays the demo iframe

What do we need?

  1. We start with an empty menu
  2. We listened to the postings for the demo
  3. The iframe will be loaded and the demo inside will send a post message.
  4. We then create menu items for each demo
  5. When we click on the menu item, we change the url of the iframe.
  6. If iframe gets a demo to display, it updates the HTML.

The following is a index. HTML

<ul id="menu"></ul>
<iframe id="iframe" src="./iframe.html"></iframe>

<script>
  window.addEventListener('message'.ev= > {
    const li = document.createElement('li');
    li.addEventListener('click'.ev= > {
      iframe.src = `./iframe.html? slug=${slug}`;
    });
    menu.appendChild(li);
  });
</script>
Copy the code

Here is the iframe HTML

<body>
  <h1>Please select a demo by clicking in the menu</h1>
</body>

<script>
  // Demo One
  if (window.location.href.indexOf('demo-one')! = = -1) {
    document.body.innerHTML = '<h1>Hey there from demo two</h1>';
  }
  // Demo Two
  if (window.location.href.indexOf('demo-two')! = = -1) {
    document.body.innerHTML = '<h1>I am demo two</h1>';
  }

  // register demos when not currently showing a demo
  if (window.location.href.indexOf('slug') = = = -1) {
    parent.postMessage({ name: 'Demo One'.slug: 'demo-one' });
    parent.postMessage({ name: 'Demo Two'.slug: 'demo-two' });
  }
</script>
Copy the code

The code here displays only the most relevant information

For demonstrations and more details, see the postMessage folder.

You can start it by NPM I && NPM run start.

Now imagine that the UI is much more than just a UL/LI list, and the demo follows a certain demo format?

Will this be a system where the UI and Demo can be written in completely different technologies?

The answer is YES 💪

The only way to communicate was through postMessages.

Therefore, preview only needs to know which postMessage format to use.

Also, postMessage is a native function, so every framework or system can use them.

Two builds (to continue)

The concept above is what storybook uses — which means there are actually two applications running. One is the Storybook UI (called the manager) and one is your actual demo (called the preview). Knowing this, it makes sense to have two separate builds.

But why is there a build step? Why does storybook have this setup?

Let’s take a look at what it takes to make some code run and work in multiple browsers.

Browser-based apps publish Tours

Let’s take a small example where we use a private class field.

This feature is currently in phase 3 and is only available in Chrome.

// index.js
import { MyClass } from './MyClass.js';

const inst = new MyClass();
inst.publicMethod();

// MyClass.js
export class MyClass {
  #privateField = 'My Class with a private field';

  publicMethod() {
    document.body.innerHTML = this.#privateField;
    debugger; }}Copy the code

We purposely put a debugger breakpoint inside to see the actual code being executed by the browser.

Let’s take a look at how WebPack works with some Babel plug-ins. (See complete configuration)

__webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MyClass", function() { return MyClass; }); function _classCallCheck(instance, Constructor) { if (! (instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { // ... more helper functions var MyClass = /*#__PURE__*/ function () { function MyClass() { _classCallCheck(this, MyClass); _privateField.set(this, { writable: true, value: 'My Class with a private field' }); } _createClass(MyClass, [{ key: "publicMethod", value: function publicMethod() { document.body.innerHTML = _classPrivateFieldGet(this, _privateField); debugger; } }]); return MyClass; } (); var _privateField = new WeakMap();Copy the code

Wow, that’s quite a lot of code 🙈, it doesn’t really look like the code to write 😱.

Note: In most cases, you won’t see this because of the source map

In a typical WebPack and Babel setup, your code is compiled to ES5 so it can run on older browsers like IE11.

However, you may ask, how often do I run my applications on older browsers?

A typical developer should probably develop about 90 percent on modern browsers and about 10 percent on older browsers to make sure everything still works. At least we hope you have such a good workflow 🤗.

The question is, why compile, ship, debug, and use “weird” code 100% of the time if you only need 10% of it? Can we do better?

Let’s see how es-Dev-Server handles this by opening the same file on Chrome.

export class MyClass {
  #privateField = 'My Class with a private field';

  publicMethod() {
    document.body.innerHTML = this.#privateField;
    debugger; }}Copy the code

It looks exactly like the original code — because it is. The as-is code runs perfectly in Chrome without any adjustments. That’s what happened. The source code is intact.

However, we are using private class fields, a feature that is not supported, for example on Firefox. What happens if we open it there?

It failed 😭

Syntax error: Private fields are not currently supported.

Well, that’s our fault, because we used phase 3 functionality, and now we’re not compiling anything.

Let’s try using es-dev-server — Babel, which will use.babelrc just like Webpack.

The following code will be generated.

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = privateMap.get(receiver);
  if(! descriptor) {throw new TypeError('attempted to get private field on non-instance');
  }
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

export class MyClass {
  constructor() {
    _privateField.set(this, {
      writable: true.value: 'My Class with a private field'}); }publicMethod() {
    document.body.innerHTML = _classPrivateFieldGet(this, _privateField);
    debugger; }}var _privateField = new WeakMap(a);Copy the code

What it does is 💪 it compiles only private fields, not all fields 👌.

However, if you go back to Chrome now, you’ll see that it compiles there now, too, because once you start going through Babel, it does it according to @babel/reset-env, and Babel is always conservative. The reason is that once you start going through Babel, it does its thing according to @babel/reset-env, and Babel tends to be conservative.

The real magic ✨ happens when you open it on an older browser like IE11. Because then it will compile to SystemJS, a polyfill for the ES module.

It’s going to look something like this

System.register([], function(_export, _context)) {
  "use strict";

  var MyClass, _privateField;

  function _classCallback(instance, Constructor) {
// ...
Copy the code

It behaves exactly like the real ES module, so your code will work on browsers that don’t support them 💪.

If you’re worried about speed, it’s best to just use phase 4 features and not use Babel at all. If you really need to, you can use two startup commands

"start": "es-dev-server --open"."start:babel"."es-dev-server --babel --open".Copy the code

So, what the es-Dev-server automatic mode implements is that you don’t need to think about it. On modern browsers, it will be instant, and it will work even when you need to test on older browsers.

To summarize, in order to be able to work and debug code in all the browsers we want to support, we basically have two options.

  1. Compile to the lowest denominator
  2. Service code based on browser functionality

As always, please don’t go crazy with new features. Use the currently stable features on your developer browser. You will have the best experience when you do not use a custom Babel configuration.

The code here displays only the most relevant information

For a demo and more details, see the esDevServer-vs-WebPackDevServer folder. You can start it with NPM Run Start, NPM Run Start: Babel, and NPM run webpack.

The source maps

Fortunately, in most cases, you will see the source code even when using compiled code. How is that possible? All thanks to Sourcemaps.

They’re a way of mapping raw code to compiled code, and browsers are smart enough to link them together and show you only what you’re interested in. Just check the “enable JavaScript source map” option in your development tool.

It’s really good. It just works. However, it is another moving part that may break or you need to know it at least.

The opportunity to

So we see a window of opportunity in modern code compilation and distribution. We want to have the functionality of Storybook, but we also want to not rely on the ease of use of WebPack.

In a nutshell, the idea is to combine the Storybook UI with es-Dev-Server.

Let’s get started 💪

Here is the master plan

  1. Pre-built storybook UI (so we don’t have to use Webpack)
  2. Replace webpack magic such as require.context
  3. Copy and preview the way you communicate with your manager.
  4. Use rollup to build the static version of the storybook.

Storybook advanced

Prefabricated storybook

To get a storybook preview of the ES module version, go through Webpack & Rollup. Yes, it’s a little dark magic, but it’s the only way it works. It seems that Storybook has not been optimized to have a completely separate manager/preview. But hey hey, it works and we will work with Storybook to make this better 💪.

You can find the source code on Github, and the output is published on NPM as @open-wc/storybook-prebuilt.

Prebuilt has the following benefits.

  • Speed is fast
  • Preview can be independent of the storybook build setup

Prefabrication has the following disadvantages.

  • You can’t change pre-made add-ons.
  • However, you can create your own prefabricated ones

Replaced webpack magic

In the current storybook, preview.js uses require.context to define which stories are loaded. However, this is a feature that is only available in WebPack, which basically means it’s a lock on a particular build tool. We want to be free to choose whatever we want, so that needs to be replaced.

We select a command line argument.

In short, you now don’t need to define where to find stories in your JS, but through the command line

start-storybook --stories 'path/to/stories/*.stories.{js,mdx}'
Copy the code

Doing so exposes this value to various tools, such as Koa-middlewares and rollup.

Mimic the way previews communicate with the manager.

Now that we can “include/use” the separate Storybook UI (manager), it’s time to rotate es-Dev-server.

For the manager, we create an index.html, which boils down to a single import.

<script src="path/to/node_modules/@open-wc/storybook-prebuilt/dist/manager.js"></script>
Copy the code

We do some special caching to ensure that your browser loads the storybook manager only once.

For preview, we need to load/register all individual stories, as shown in the postMessage example. We’ll get the story list from the command line argument.

An important part of what the browser ends up using is dynamically importing all the story files and then calling StoryBooks Configure to trigger postMessage.

import { configure } from './node_modules/@open-wc/demoing-storybook/index.js';

Promise.all([
  import('/stories/demo-wc-card.stories.mdx'),
  // here an import to every story file will created
]).then(stories= > {
  configure(() = > stories, {});
});
Copy the code

Additional MDX support

The upcoming storybook 5.3.x (currently in beta) will introduce document mode. This is a special mode that allows markdowns and stories to be written in one file and displayed on one page. You can think of it as Markdown, but three-dimensional 😬.

This format, called MDX, allows you to write Markdown, as well as import javascript and write JSX.

We recommend it as the primary way to document components.

To support such functionality, ES-Dev-Server needs to know how to handle MDX files.

To do this, we added a KOA middleware that converts requests for *.mdx files into A CSF (Component Story Format).

This basically means that when you ask for http://localhost:8001/stories/demo-wc-card.stories.mdx, the file system file looks like this.

###### Header <Story name="Custom Header"> {html` <demo-wc-card header="Harry Potter">A character that is part of a book  series... </demo-wc-card> `} </Story>Copy the code

It will send this information to your browser

// ...
mdx('h6'.null.`Header`);
// ...
export const customHeader = () = > html`
  <demo-wc-card header="Harry Potter">A character that is part of a book series...</demo-wc-card>
`;
customHeader.story = {};
customHeader.story.name = 'Custom Header';
customHeader.story.parameters = {
  mdxSource:
    'html`\n 
      
       A character that is part of a book series... 
      \n `'};Copy the code

You can open your Web panel and see the response 💪

Use rollup to create a static storybook

In most cases, you’ll also want to publish your storybook somewhere on a static server. To do this, we pre-set up a scroll configuration that does all of the above and outputs two versions.

  1. For modern browsers that support the ES module and
  2. For all other browsers, we provide an ES5 version that includes all polyfills.

For more details on how different versions are shipped from static servers, see the Open-WC rollup recommendation.

The code here shows only the most relevant information

See the storybookOnSteroids folder for a complete demonstration. You can start it by NPM I && NPM run storybook. See @open-wc/demoing-storybook for the actual source code.

The judgment

We did 💪

A fully functional demo system can

  • It’s not built into modern browsers
  • Lightning start
  • There is a prefabricated user interface
  • Provide preview code based on browser functionality
  • usees-dev-serverSo you can use all its functions.

Most importantly, it’s great to see a completely separate server powering storybooks. The storybook setup is really worth 👍.

  • You can check it out in open-WC repo.
  • Please in open-wc.org/demoing-sto… View a field example.
  • And read the documentation

PS: It’s not all roses and rainbows, but with this step, we now know it’s possible — further improvements, such as a smaller preview pack or separate MDX conversion pack, will happen at some point 🤗.

In the future

We hope to use this as a starting point to make Storybook directly available to other framework servers 👍. Even non-javascript servers will work -Ruby, PHP are you ready? 🤗

If you are interested in supporting your framework server and you need help/guidance, please be sure to let us know.

thanks

Please follow us on Twitter, or follow me on my personal Twitter. Be sure to check out our other tools and recommendations at open-wc.org.

Thanks to Benny and Lars for their feedback and for helping me turn my doodle into focus AB

Thanks to Benny and Lars for their feedback and for helping me turn my doodle into a story to follow.

Cover photo courtesy of Nong Vang on Unsplash.


Translation via www.DeepL.com/Translator (free version)