dynamics react shims - dev flexibility
Last updated
Was this helpful?
Last updated
Was this helpful?
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 manual load processing by opening up the Web Resource and pushing the "Load File" button. This is actually quite slow as you must first select the file, press Save to upload it then publish.
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:
Then start the web server using npm start
if you have an entry in your package.json like
or you can start it from the command line
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:
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):
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:
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 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:
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:
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.
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.
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 (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:
and then you would wrap the asset name in your JS like:
Assuming that the component is in ./components
relative to your context directory, then the URL generated above is equivalent to:
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:
With this, our webpack processed files can just use:
when needed. Other CSS not managed by webpack, would just use a URL appropriate to it e.g.