Integrate Webpack and ASP.NET MVC/WebAPI
Quick tour of some tricks I’m using in my Podcasts-Angular2 demo project.
While that project is using Angular2, everything discussed here applies to React or Angular 1.x or any other FE framework you might be using, as long as it builds with webpack.
In fact, not too much here is even webpack specific. It could work easily
with any FE framework or tooling that provides some sort of “serve” command
that compiles and hosts assets during development time, such as ember serve
or the http-server
npm package.
Note: this approach makes sense for projects where you want to mix server side
razor rendering and javascript rendering. For example, perhaps some of the
shell of your site is rendered serverside, but the SPA framework only controls a
big ol’ div
in the center of the page. If the entire app is driven from the client
side, you can use webpack-dev-server’s proxy feature to proxy to the API endpoints
Using webpack-dev-server for development
In development, I want to link the FE assets to come from webpack-dev-server
. This
allows for FE auto-reload, or hot module replacement, recompilation, etc.
In my layout Razor view, I use a configuration setting to switch between
local assets and assets served from webpack-dev-server
.
I like to use a settings class to wrap ConfigurationManager
.
public static class Settings
{
public static bool UseWebpackDevServer => "true".Equals(ConfigurationManager.AppSettings["UseWebpackDevServer"], StringComparison.InvariantCultureIgnoreCase);
public static string WebpackDevServerRoot => ConfigurationManager.AppSettings["WebpackDevServerRoot"];
}
The layout looks something like this. When UseWebpackDevServer
is
true
, assets like CSS and javascript are linked to the webpack-dev-server
URL. Now when I save a FE file, it picks up the change,
recompiles the bundles, and automatically reloads the page. React
projects can similarly benefit from hot-module reloading.
@if (Settings.UseWebpackDevServer)
{
<link href="@(Settings.WebpackDevServerRoot)/vendor.css" rel="stylesheet">
<link href="@(Settings.WebpackDevServerRoot)/app.css" rel="stylesheet">
}
else
{
<!-- local assets, discussed below -->
}
Then the web.config looks something like this. The UseWebpackDevServer
key can be set to false in production, or omitted entirely. The
WebpackDevServerRoot
key is not needed in production either.
<appSettings>
<add key="UseWebpackDevServer" value="true"/>
<add key="WebpackDevServerRoot" value="http://localhost:8080"/>
</appSettings>
Now I can run my project from visual studio to provide the REST API and to serve the application shell markup.
Using local assets for production
In production, I don’t want to use webpack-dev-server
, I want the compiled
and minified assets on disk so they can be served by IIS.
I just omit the UseWebpackDevServer
configuration value from my web.config
in production (or set it to "false"
).
@if (Settings.UseWebpackDevServer)
{
<script type="text/javascript" src="@(Settings.WebpackDevServerRoot)/polyfills.js"></script>
<script type="text/javascript" src="@(Settings.WebpackDevServerRoot)/vendor.js"></script>
<script type="text/javascript" src="@(Settings.WebpackDevServerRoot)/app.js"></script>
}
else
{
<script type="text/javascript" src="/static/polyfills.min.js"></script>
<script type="text/javascript" src="/static/vendor.min.js"></script>
<script type="text/javascript" src="/static/app.min.js"></script>
}
Now that Settings.UseWebpackDevServer
is false, the script tags all
point to the /static/
folder instead.
But how to get the compiled files there?
My package.json
for the FE build defines a command
to copy the compiled assets to a folder in the web root of the project.
"scripts": {
"start": "webpack-dev-server --inline --progress --port 8080",
"build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail && ncp dist ../Podcasts.Web/static"
}
Scroll right
I’m using the ncp
package to copy files from webpack’s dist
output
folder over to the static
folder within the server side project. I
could also have had webpack output right to that folder.
Since these are output files compiled by the webpack build process, I don’t
want them stored in source control. Thus I add
the dist
folder and the static
folder to .gitignore
.
dist/
Podcasts.Web/static/
Deployment
msdeploy doesn’t send files not included in the project file.
To get the assets to deploy, I needed to add the
files to the .csproj
in Visual Studio (Right click -> Include in
Project) or modify the project file to automatically include them.
Integrating with client side routing
Since I’m using client side routing in HTML5 mode (no hash based URLs) I need to have IIS just serve the shell HTML for any request. That way if users bookmark a URL or send a link to a friend, the server will not 404, but send the shell markup and let the client side router set up the page and load data from the APIs.
I just use a simple HomeController
that renders a Razor view for the shell, and
the URL Rewrite module configured in web.config to use it:
<system.webServer>
<rewrite>
<rules>
<rule name="AngularJS Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
</system.webServer>
Thanks to Yagiz Ozturk on StackOverflow for that bit of magic.
This tells IIS to match all routes, except any URL that is a static file
on disk, or is a directory, or starts with /api
and instead pretend it
was really a request for just /
. This hits the HomeController.Index
action and renders the application shell markup. Then the client side
router spins up, reads the URL from the address bar and performs the
correct actions.
As long as the APIs live under /api
everything works just fine.
Conclusion
I’ve used this pattern successfully for a few side projects, both React and angular 2.
I hope someone finds it useful.