Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

tmns
tmns

Posted on

     

Making Ember Addons Fastboot Compatible

Intro

Hello to the 30 people out there who still useEmber 👋🐿

Just kidding - I know the number is higher than 30, but in a world dominated by React, Angular, and Vue, it seems like we who develop with Ember (either by choice [really?!?] or by career happenstance), are pretty alone - especially in terms of useful and helpful material.

That's part of the reason why when faced with the task of addingFastboot (Ember's version of server-side rendering) to a project riddled with jQuery, along with the demand that all the jQuery remain functional, it took me quite a heavy amount of blood, sweat, and tears to get things working.

As such, I'll share here a nice little trick I learned along the way in case any other poor soul finds themself in the dark shadows that is Ember Fastboot development.

What's the issue?

When adding Fastboot to an Ember project that makes heavy use of addons that in turn utilize third-party client-side JS libraries (typically jQuery), you will quickly find out that your project will have a hard time rendering on the server if you don't make some drastic changes. This is simply due to the project being unable to build and render client-side JS within the server (ie node) environment.

This leaves us with a few options. First, we can simply gut all the incompatible client-side JS logic and / or use node-compatible equivalents. A common example of this is usingember-fetch instead ofjQuery.ajax. Second, we can hope that the maintainer(s) of the addon in question has taken notice of the Fastboot issue and made their library Fastboot compatible.

Unfortunately, there are inherent problems with both of these options. First, often a node-compatible equivalent simply doesn't exist. Second, often the maintainer of a library's idea of making their library Fastboot compatible looks something like this:

if(process.env.EMBER_CLI_FASTBOOT){return;}
Enter fullscreen modeExit fullscreen mode

...which, aside from being broken (this test always fails, asEMBER_CLI_FASTBOOT does not exist inprocess.env as far as I can tell), essentially only does one thing - which is to simply not import the library into the application. This means that when the app finally makes it to the browser, the library will not be there 😑

We want the best of both worlds. We want the offending addon to be loaded into Fastboot but its client-side code not evaluated until it reaches the browser.

What's the solution?

The most streamlined and bulletproof solution I've found so far is acting as if you yourself are the maintainer of the library. In essence, you must become one with the maintainer and realign the inner zen of the library - also known as making some changes to the library'sindex.js 😁

As noted in theFastboot Addon Author Guide, if your addon includes third-party code that is incompatible with node / Fastboot, you can add a guard to yourindex.js that ensures it is only included in the browser build. This is achieved by creating separate build tree specifically for the browser.

Unfortunately, the Fastboot guide falls short in its given example of actually implementing such a guard. So we will give a more thorough and real-world example here.

Being Slick(er)

Let's say we want to use the addonember-cli-slick, which is essentially an Ember port of theSlick Slider plugin. The addon'sindex.js looks like this:

'use strict';constpath=require('path');module.exports={name:require('./package').name,blueprintsPath:function(){returnpath.join(__dirname,'blueprints');},included:function(app){this._super.included(app);app.import('node_modules/slick-carousel/slick/slick.js');app.import('node_modules/slick-carousel/slick/slick.css');app.import('node_modules/slick-carousel/slick/slick-theme.css');app.import('node_modules/slick-carousel/slick/fonts/slick.ttf',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.svg',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.eot',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/fonts/slick.woff',{destDir:'assets/fonts'});app.import('node_modules/slick-carousel/slick/ajax-loader.gif',{destDir:'assets'});}};
Enter fullscreen modeExit fullscreen mode

If you look closely, you will see that the first import being made isslick.js. This is awful for Fastboot and will cause it to blow up server-side. So how do we make slick a little more slicker with its imports?

The first step is getting rid of theblueprintsPath and creating a separate import tree for our offending code, which we will term asvendor code. Let's write out the function and import our necessary objects:

module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');},included:function(app){[...]
Enter fullscreen modeExit fullscreen mode

Now, let's use theFunnel object to specify the code we want to separate:

module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})},included:function(app){[...]
Enter fullscreen modeExit fullscreen mode

Next, we define theguard that is mentioned in the Fastboot documentation, which essentially states to only include our code if theFastBoot object isundefined, which is guaranteed to betrue when we are in the browser:

module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);},included:function(app){[...]
Enter fullscreen modeExit fullscreen mode

Then, to wrap up the separation, we return a merge of both thedefaultTree and our browser / vendor tree:

module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);returnnewmergeTrees([defaultTree,browserVendorLib]);},included:function(app){[...]
Enter fullscreen modeExit fullscreen mode

But wait!! This also has the potential to fail - as it is actually possible fordefaulTree to beundefined! So, we must guard against this by only including it if it exists:

module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);letnodes=[browserVendorLib];if(defaultTree){nodes.unshift(defaultTree);}returnnewmergeTrees(nodes);},included:function(app){[...]
Enter fullscreen modeExit fullscreen mode

The next step is correcting the app import statement inincluded. We want to change the import statement to point at our newvendor/slick/ directory. In our case this looks like:

[...]included:function(app){this._super.included(app);app.import("node_modules/slick-carousel/slick/slick.css");app.import("node_modules/slick-carousel/slick/slick-theme.css");app.import("node_modules/slick-carousel/slick/fonts/slick.ttf",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.svg",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.eot",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.woff",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/ajax-loader.gif",{destDir:"assets"});app.import("vendor/slick/slick.js");}};
Enter fullscreen modeExit fullscreen mode

And finally, the obligatory code snippet of everything put together:

'use strict';module.exports={name:'ember-cli-slicker',treeForVendor(defaultTree){constmap=require("broccoli-stew").map;constFunnel=require("broccoli-funnel");constmergeTrees=require('broccoli-merge-trees');letbrowserVendorLib=newFunnel('node_modules/slick-carousel/slick/',{destDir:'slick',files:['slick.js']})browserVendorLib=map(browserVendorLib,(content)=>`if (typeof FastBoot === 'undefined') {${content} }`);letnodes=[browserVendorLib];if(defaultTree){nodes.unshift(defaultTree);}returnnewmergeTrees(nodes);},included:function(app){this._super.included(app);app.import("node_modules/slick-carousel/slick/slick.css");app.import("node_modules/slick-carousel/slick/slick-theme.css");app.import("node_modules/slick-carousel/slick/fonts/slick.ttf",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.svg",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.eot",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/fonts/slick.woff",{destDir:"assets/fonts"});app.import("node_modules/slick-carousel/slick/ajax-loader.gif",{destDir:"assets"});app.import("vendor/slick/slick.js");}};
Enter fullscreen modeExit fullscreen mode

And that's it! We now can successfully includeember-slick into our server-side rendered Ember project, deferring its evaluation until it reaches the browser and in turn avoiding any fatal errors during the process - which is quite a feat for anyone that's dealt with Ember Fastboot and fancy browser JS addons 🥳

Conclusion

While it's quite a cold, dark world out there for Ember developers nowadays, there are still some glints of light and hope here and there. One such glint is the realization that including client-side JS heavy addons into a Fastboot project is indeed possible and can be achieved by editing the addon'sindex.js.

I hope this helps the 29 others out there that may be facing similar issues 😉

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

pentester -> humanitarian volunteer -> developer
  • Location
    上海
  • Education
    B.S. in Computer Science, UC Santa Barbara
  • Joined

More fromtmns

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp