dynamics react shims - dev flexibility

Manually loading/unloading solutions is too slow for the edit-compile-debug cycle of development especially compared to what is normally available for web application development. Depending on how you perform application development, you may want to consider a few options for allowing you to hot reload your Web Resource UI solution. The options for loading your Web Resources are:

  • Use the standard Solution based loading approach. Unfortunately, this has the same issues as the approach above and is also too slow to use.

  • Use a proxy server that you run on your workstation. You connect to your 'forward' proxy server from your browser and the proxy server connects to the CRM server. You can ask the proxy server to replace URLs that match a specific pattern with a local file, such as your app.js file that loads your solution. This approach was covered in the article here. Fiddler can be used on a Windows workstation but other OS have other forward proxy solutions. You can also start up copy of a local web server and ask it to proxy for you. Both nginx and apache can do this. A picture of the fiddler configuration from the article mentioned above is show below:

  • Use a local dev server that supports hot load from your web application development environment. It is quite common to have a local web server running on your workstation when developing web applications. You typically run the web server on your workstation and have it server a portion of your site while proxying or providing shims for the other parts of the site you are not working on.

The last option, obviously, is the most convenient and it works anywhere your local dev server works. Fortunately, there is a dev server built into webpack (webpack-dev-server) that allows you server up your project files, with hot reload builtin. You need to add some small config to your webpack.config.js file:

//...
  },
    devServer: {
    https: true
    }
};

Then start the web server using npm start if you have an entry in your package.json like

 "scripts": {
    "start": "webpack-dev-server"
  }

or you can start it from the command line

$ npm run-script start

Either way, you get hot reload capabilities from your workstation. However, you still need to shim up the CRM Web Resource.

By adding a Web Resource shim that you manually load once, you can develop your UI solution using hot reload. The shim merely needs to load your main script, say app.js from https://localhost:8080/apps.js (or whatever your main load file is).

Create a Web Resource with any name, such as contact_form_shim, as an HTML Web Resource in CRM, then add the following shim HTML:

<html>
  <head>
    <script>
var runmain = true;
    </script>
    <script src="https://localhost:8080/app.js"></script>
  </head>
  <body>
    <div id="container"></div>
  </body>
</html>

We had to include our main container div in the shim HTML but you could also have your app.js add the div element to the body on the fly. If you want, your shim could also use CRM's URL parameters to parameterize the script location so you could specify the URL or the app.js as a parameter when you add the Web Resource to your Form. This allows you to use the same shim across different development areas. The shim looks a bit ugly when you do this as you have to construct the script tag programmatically and add the script tag to the document using javascript.

The ugly shim version is much more flexible and allows you to add CRM webresource custom data parameters to control the shim process. You can pass in any string parameters via the Web Resource Properties when editing the form so you could pass in the entire URL and change the port, host, or whatever. You may even choose to pass in a set of resources that should be loaded in the HTML and dynamically load multiple resources, including CSS files, Content Delivery Network (CDN) files as well as multiple javascript files. In this case, you would need to write a simple parser to identify each resource type and load it accordingly or write a shim that loads HTML that contains your script/css loads tags and appends that html to the iframe's document e.g. document.write):

<html>
<head>
<!-- Ensure you can access the global context to get the "base" url for web service calls. -->
<!-- The number of leading .. is dependent on where the shim is located in the fake file tree. -->
<script src="../ClientGlobalContext.js.aspx" type="text/javascript"></script>

<script>
// Parse a parameter from the document location URL that
// was created with query parameters when you add the 
// Web Resource to a form.

function getURLParameter(name, search) {
    return decodeURIComponent((new RegExp('[?|&]' + name + '=' +
                      '([^&;]+?)(&|#|;|$)').exec(search) || [, ""])[1].replace(/\+/g, '%20')) || null;

// CRM sends data in via an encoded "data" query arg                                    
var data = decodeURIComponent(getURLParameter("data", document.location.search));
// If you structure your arg as a query arg, use this
// or parse it whatever way your "data" has been structured
// e.g. maybe its json!
//var targetjs = getURLParameter("targetjs", data) || 'app.js';                                                            
var config = JSON.parse(data);
var targetjs = config.targetjs || 'http://localhost:8080/app.js';

// There are specialized libraries that help you
// load scripts dynamically, but we can keep it simple here.                    
var scriptHtml = document.createElement("script");
scriptHtml.setAttribute("type", "text/javascript");
scriptHtml.setAttribute("src", targetjs + document.location.search);
// some people use document.body.appendChild(scriptHtml)
document.head.appendChild(scriptHtml);
</script>
</head>
<body>
<div id="container">If you are seeing this message, then your shim is not connected to your hot reload http server.</div>
</body>
</html>

This is a fairly simple CRM Web Resource shim. If you were to create a fancier shim that dynamically loads a variety of content, all you need is a shim loaded to get started with Web Resource development and the appropriate "data" setting in the Web Resource Properties in the CRM form. Note that you should be able to pull in the GlobalContext and have it parse the query parameters passed to this page, but I could not get that to work reliable.

The runmain variable allows us to detect when our code is loaded using a shim versus a standard Web Resource bundle that we might deploy to production without a shim.

Then, assuming one of the outputs of webpack is consolidated file named app.js our application will load. In your app.jsx file (in my case the application entry "main" function is a react based file for the Contact form) you need to include an expression to load itself if runmain is true:

// app.jsx

//...
// other code in your javascript file
//...

export function run() {
   React.render(<div>hello world</div>,
     document.getElementById("container"));
}

/**
 * This only runs if runmain is defined as truthy.
 * Define var runmain=true in the HTML shim file that 
 * pulls in this resource in order to fore
 * the bootstrap to happen here. Or bootstrap
 * in HTML by *not* defining runmain and call 
 * run() (or whateve your initialization function/module is) instead.
 */
window.addEventListener('load', function() {
    if(typeof runmain != 'undefined' && runmain) {
    console.log("Running initialization based on shim.");
    run();
    }
});
}

Another variation is to shift the window.addEventListener(...) into the shim so that the shim contract says that the js is always loaded after the window has loaded and our runmain has had a chance to initialize correctly. Then we could just run the if logic without using the event listener callback model. It's possible to use webpack's script-loader to load a small function in the global namespace, however, you would still need to reach into the module that defines ReactContextForm.onLooad or whatever function calls React.render(...,....) so its easier to put the window.addEventListener into a file that is processed by webpack normally.

That's it! Now when you save a file, it will be compiled and reloaded in your browser. The approach is similar to python scripting where python requires you to test if __name__== '__main__': in your script file to see if your script's initialization code should run.

You may run into issues with the load from the localhost domain. If so, you may have to go to https://localhost:8080 in another browser tab and import the certificate or you may need to turn off any CORS related browser plugins that was enabling CORS. In chrome, you can go to chrome://flags/#allow-insecure-localhost. Enabling insecure localhosts tells chrome to not worry about certificates from locally sourced content. You will need to restart chrome for the new setting to take effect.

webpack Friendliness

webpack is a bundler for react programs and is popular. It bundles up your application by traversing a graph of dependencies, given a starting point.

The starting point is your app.jsx file, which above, is turned into a app.js file. Nicely enough, if you set your output to "library", the window.addEventListener is still run when your app.js is loaded. Hence, the window event listener is run and activated.

Because you will want to make your project's dist directory look like the CRM webresources "fake" filesystem, you need to align your local file system file tree and webpack's outputs so that it supports hot reloading during development. You need to set your webpack-dev-server's paths correctly. It is not the case that you will only server up one massive js file. You will want to leverage other assets that are statically served and cached. For example, if you code-split your project and output a vendor.js file, that vendor.js file may be used for different pages in your solution.

Let's assume that your webpack output (as in the output config object) is at dist/publisher_/ui. This means that when you run "build" for your production build, the output will be in the ui directory. However, when you run the dev server, compilations are kept in memory. You'll need to set dev server's location for serving static content, if you have any, and the "bundles."

For example, your output spec may be:

const myoutput = {
  path: path.join(__dirname, "dist/publisher_/ui"),
  filename: "app.js",
  library: "MyApp",
  libraryTarget: "var" // this is the default
}

const finalWebpackConfig = merge(common, {output: myoutput});

When building for production via "npm build" we would get a file at dist/publisher_/ui/app.js that defines a "var MyApp = ...crazy webpack stuff...".

However, the HMR in the dev-server does not use this file for when running.

The dev-server part is:

devServer: {
  contentBase: path.join(__dirname, "/dist/"), // for static assets
  publicPath: "/publisher_/ui/", // webpack output files found at http://localhost:8080/publisher_/ui/app.js for example
  compress: true,
  hot: true,
  https: true
}

This means that your static assets will be pulled from dist and your incrementally compiled webpack output bundles will be served like https://localhost:8080/publisher_/ui/app.js.

If you were to push your dist directory to the CRM server's webresources, then they would be pushed to "publisher_/ui/app.js" which matches perfectly.

The benefit of matching the paths is that your webpack build process that is local and not "hot" can use the same resource pathing as the hot dev server.

The generation of paths from your development sources is a complex, mind-bending exercise because pathing has to be flexible to accomodate funky setups such as HMR as well as production builds.

Azure Web App or AWS Shim Friendliness

The shim concept above can also be used to pull resources from nearly anywhere. One other option that helps with workflows is to put a small server up in the Azure or AWS cloud. Then, you can indicate in your Web Resource properties that you do not want to go to localhost:8080 but the address of your Azure or AWS site. Your local workstation worflow would then push into Azure or AWS, which is also fairly easy to do e.g. a push to github triggers an update or you can automate an ftp push.

Depending on the cloud platform deployment service you use, you can also have a sync between your cloud "drive" and your computer's filesystem them have the cloud drive sync to your web server. Hence, any changes on disk are reflected in the web server app you deploy and then in Web Resource. This model does not support hot reload but you could write your Web Resource stub to perform reload upon rechange.

You have lots of choices.

Stylesheets

Styles in the form of CSS under webpack can also be hotloaded if you use the webpack provided css-loader and style-loader loader. They support HMR (Hot Module Reload). You create your stylesheets as you normally would using .css files and when the stylesheet changes, it is reloaded. You would include your style sheet in webpack's dependency graph by importing it in your main js file import styles from './styles.css'.

I suggest using the "module mode" of css-loader to help reduce namespace pollution--a classic problem when working with styles. Web Resources load in iframes so they are already isolated from the standard CRM styles (which is both god and bad). The "css module syntax is described at https://github.com/css-modules/css-modules.

Start with css-loader and continue to add layers as needed to meet your style authoring needs.

Images

Images (icons, pngs, svgs) can be loaded the same way with webpack and made hot reloadable when webpack can "translate" them into js "code." See this introductory article here.

That article is mostly referencing the github location for image-webpack-loader. Notice that you must then use require(...path to image resources...) because webpack is controlling your resources and may rename them or transform the resource e.g. decrease the pixel density, is at builds the outputs in javascript. You will find that anytime you put a resource under a "builder" control or want the ability to control its loading/emergence in your app, its usually better to wrap its usage in a function that allows you to munge resource names and the resources themselves under the hood. Many of the "CSS in JS" methods do the same thing to control when the "" gets augmented with transformed css classnames and values.

It is a bit more tricky then you might think because for HMR support, you need to ensure that any URL created by webpack has URLs that point back to your local server (or wherever the images are). Hence, the URL generated at the point you perform require('./icon.png') needs to be equivalent to https://localhost:8080/<some dir path locally>/icon.png. But notice that since we used a relative path in the require, the actual possible path is relative to the context directory and could be something like ./components/icon.png. Factoring all of this in, our webpack config for HMR, not for production, looks like:

// the other "module" entries in webpack.config.js
,{
                test: /\.(jpe?g|png|gif|svg)$/i,
                use: [
                    {
                        loader: 'file-loader'
                        ,options: {
                            publicPath: 'https://localhost:8080/',
                            name: "[path][name].[ext]",
                            useRelativePath: true,
                        }
                    },
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            progressive: true,
                            optipng: { optimizationLevel: 7 },
                            mozjpeg: { quality: 65 },
                            gifsicle: { interlaced: true },
                            pngquant: { quality: '65-90', speed: 4 }
                        }
                    }]
            }

and then you would wrap the asset name in your JS like:

<img src=require("./some-image.png") />

Assuming that the component is in ./components relative to your context directory, then the URL generated above is equivalent to:

<img src="https://localhost:8080/./components/some-image.png"/>

which is exactly what we want. If we had used only [name].[ext] then not having [path] means that file-loader sees some-image.png and we would need to set publicPath to https://localhost:8080/components to compensate.

You should also notice that we did not really stick our images into a special assets directory but kept them close to the .jsx file that used it--something more common when using react and webpack these days. For non-shared assets, this type of co-location makes alot sense but is not the dominante pattern.

Some of the images I use, such as next/previous images for controls, I place into the target directory to begin with e.g. a "dist" folder. While many people create the dist folder from "assets" (and have to copy them) and webpack bundling activities, it is easier to create a permanent "dist" folder in your project setup and place them there.

Then to reference them in your css stylesheets (for a background image url) or your jsx code, just create an alias in webpack that points to the dist directory.

For example, if we assume the images are placed into "dist/publisher_/images" then we would create a webpack alias in our baseline configuration file (say common.webpack.js) like:

 resolve: {
        alias: {
            Images: path.resolve(__dirname, "dist/publisher_/styles/images"),
            Styles: path.resolve(__dirname, "dist/publisher_/styles"),
            Js: path.resolve(__dirname, "dist/publisher_/js")
        }
    },

With this, our webpack processed files can just use:

var myimage = require("Images/myimage.png";

when needed. Other CSS not managed by webpack, would just use a URL appropriate to it e.g.

.someclass {
   background: url("./images/background.png");
}

Last updated