I\'m building a template for our team on top of the .NET Core 2.1 + Angular 5 template included in the latest release of core 2.1, we deploy applications into virtual folder
Yes, I know it's an old question, but I hope this help someone more. The answer is about Angular 6 and .NET Core 2.1. Advice: the explanation is a bit large (sorry)
In Core 2.1, we have a tipical Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPage = $"/index.html";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
The important part is the
spa.UseAngularCliServer(npmScript: "start");
this tell to .NET that use the script defined in package.json
{
"name": "client-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --base-href=/ --serve-path=/", //<--this line
"build": "ng build",
...
},
....
}
See that we have two options. --base-href and --serve-path
When we execute the application, we received an index.html like
<!DOCTYPE html>
<html>
<head>
<title>Angular i18n example</title>
<base href="/"
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<app-root>Loading...</app-root>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="styles.js"></script>
<script type="text/javascript" src="vendor.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
We can change the base href change our package.json. But if not change the --serve-path according, our app fails because we are looking for the script according to the base href.
The idea is that the scripts not depending for base href. I want that the src of scripts was in absolute path. That's the scripts becomes like
<script type="text/javascript" src="/<serve-path>/runtime.js"></script>
<script type="text/javascript" src="/<serve-path>/polyfills.js"></script>
<script type="text/javascript" src="/<serve-path>/styles.js"></script>
<script type="text/javascript" src="/<serve-path>/vendor.js"></script>
<script type="text/javascript" src="/<serve-path>/main.js"></script>
So, if we write in our .html some like
<head>
<title>Angular i18n example</title>
<script>
var str = document.location.href;
...
document.write('<base href="' + str+ '" />');
</script>
<!--we remove base href="/"
<base href="/"
-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
Our scripts are looking for from the correct path and the application works. ¿How change the way to add the scripts? Well, we have to make a few changes in two files:
index-html-webpack-plugin.js from node_modules\@angular-devkit\build-angular\src\angular-cli-files\plugins
browser.js from node_modules\@angular-devkit\build-angular\src\angular-cli-files\models\webpack-configs
See first the class IndexHtmlWebpackPlugin in index-html-webpack-plugin.js
class IndexHtmlWebpackPlugin {
constructor(options) {
this._options = Object.assign({ input: 'index.html', output: 'index.html', entrypoints: ['polyfills', 'main'], sri: false }, options);
}
apply(compiler) {
....
const scriptElements = treeAdapter.createDocumentFragment();
for (const script of scripts) {
const attrs = [
{ name: 'type', value: 'text/javascript' },
//change the line
//{ name: 'src', value: (this._options.deployUrl || '') + script },
//by
{ name: 'src', value: (this._options.baseHref)+(this._options.deployUrl || '') + script },
];
if (this._options.sri) {
const content = compilation.assets[script].source();
attrs.push(...this._generateSriAttributes(content));
}
const element = treeAdapter.createElement('script', undefined, attrs);
treeAdapter.appendChild(scriptElements, element);
}
see how we add the this._options.baseHref to the scripts Well, really in constructor we haven't baseHref, it is for we must change the file browser.js in
plugins: extraPlugins.concat([
new index_html_webpack_plugin_1.IndexHtmlWebpackPlugin({
input: path.resolve(root, buildOptions.index),
output: path.basename(buildOptions.index),
baseHref: buildOptions.baseHref,
entrypoints: package_chunk_sort_1.generateEntryPoints(buildOptions),
deployUrl: buildOptions.deployUrl,
//we add this line
servePath: buildOptions.servePath,
sri: buildOptions.subresourceIntegrity,
}),
]),
One more step. As we are writen the base tag using javascript in our index.html, we don't want that angular-cli add this tag. For this, the only thing that we must change is comment the lines in index-html-webpack-plugin.js
// Adjust base href if specified
if (typeof this._options.baseHref == 'string') {
let baseElement;
for (const headChild of headElement.childNodes) {
if (headChild.tagName === 'base') {
baseElement = headChild;
}
}
const baseFragment = treeAdapter.createDocumentFragment();
if (!baseElement) {
baseElement = treeAdapter.createElement('base', undefined, [
{ name: 'href', value: this._options.baseHref },
]);
//coment the two lines below
// treeAdapter.appendChild(baseFragment, baseElement);
// indexSource.insert(headElement.__location.startTag.endOffset + 1, parse5.serialize(baseFragment, { treeAdapter }));
}
And that's all !!
Two more some points of interes
When we create the base tag using javascript we need ajust the base. Else, if we refresh the page, our base href change and, if we are, e.g in page home, our url will be like http:// www.dominio.com/< something>/< something>/home. We want that the base will go on < something>/< something>. take a look to the script and ajust like your want,e.g.
<script>
var items = document.location.href.split('/');
while (items.length>4)
items.pop();
document.write('<base href="' + items.join('/') + '" />');
</script>
The last point if make that only valid url are server as SPA. For that, we modified our startup.cs make that only valid url was server. (else, some url like http://www.dominio.com/patata/patata serve our SPA) Add between app.UseMvc and app.UseSpa a Middleware
app.UseMvc(routes =>
{
...
});
//Add this Middleware
app.Use(async (context, next) =>
{
string path = context.Request.Path.Value.Substring(1);
bool encontrado = path.EndsWith(".js") || path.EndsWith(".css") || path.EndsWith(".map") || path.StartsWith("sockjs");
if (!encontrado)
{
//make here the check for the pages
//"silly" e.g.
encontrado==(path=="es/adminitrator" || path=="it/administrator")
}
if (encontrado)
await next.Invoke();
else
{
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "error404.html"));
}
app.UseSpa(spa=>
{
....
}
});
Update
Use java script to add tab base it's a really ugly work-around. Better use BASE_REF Provider
import {Component, NgModule} from '@angular/core';
import {APP_BASE_HREF} from '@angular/common';
@NgModule({
providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}]
})
class AppModule {}