Make templates work with nonempty baseUrls (e.g., IIS virtual directories)

This commit is contained in:
Steve Sanderson 2017-07-11 23:45:25 +01:00
Родитель bb0727c34c
Коммит e65ecebac6
31 изменённых файлов: 65 добавлений и 44 удалений

Просмотреть файл

@ -10,12 +10,12 @@ import { AppComponent } from './components/app/app.component';
AppModuleShared
],
providers: [
{ provide: 'ORIGIN_URL', useFactory: getOriginUrl }
{ provide: 'BASE_URL', useFactory: getBaseUrl }
]
})
export class AppModule {
}
export function getOriginUrl() {
return location.origin;
export function getBaseUrl() {
return document.getElementsByTagName('base')[0].href;
}

Просмотреть файл

@ -8,8 +8,8 @@ import { Http } from '@angular/http';
export class FetchDataComponent {
public forecasts: WeatherForecast[];
constructor(http: Http, @Inject('ORIGIN_URL') originUrl: string) {
http.get(originUrl + '/api/SampleData/WeatherForecasts').subscribe(result => {
constructor(http: Http, @Inject('BASE_URL') baseUrl: string) {
http.get(baseUrl + 'api/SampleData/WeatherForecasts').subscribe(result => {
this.forecasts = result.json() as WeatherForecast[];
}, error => console.error(error));
}

Просмотреть файл

@ -1,6 +1,7 @@
import 'reflect-metadata';
import 'zone.js';
import 'rxjs/add/operator/first';
import { APP_BASE_HREF } from '@angular/common';
import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
@ -11,7 +12,8 @@ enableProdMode();
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {

Просмотреть файл

@ -13,7 +13,7 @@ module.exports = (env) => {
resolve: { extensions: [ '.js', '.ts' ] },
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
module: {
rules: [

Просмотреть файл

@ -36,7 +36,7 @@ module.exports = (env) => {
]
},
output: {
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]'
},

Просмотреть файл

@ -6,7 +6,7 @@ export class Fetchdata {
public forecasts: WeatherForecast[];
constructor(http: HttpClient) {
http.fetch('/api/SampleData/WeatherForecasts')
http.fetch('api/SampleData/WeatherForecasts')
.then(result => result.json() as Promise<WeatherForecast[]>)
.then(data => {
this.forecasts = data;

Просмотреть файл

@ -1,5 +1,6 @@
import 'isomorphic-fetch';
import { Aurelia, PLATFORM } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap';
declare const IS_DEV_BUILD: boolean; // The value is supplied by Webpack during the build
@ -11,5 +12,10 @@ export function configure(aurelia: Aurelia) {
aurelia.use.developmentLogging();
}
new HttpClient().configure(config => {
const baseUrl = document.getElementsByTagName('base')[0].href;
config.withBaseUrl(baseUrl);
});
aurelia.start().then(() => aurelia.setRoot(PLATFORM.moduleName('app/components/app/app')));
}

Просмотреть файл

@ -14,7 +14,7 @@ module.exports = (env) => {
},
output: {
path: path.resolve(bundleOutputDir),
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js'
},
module: {

Просмотреть файл

@ -38,7 +38,7 @@ module.exports = ({ prod } = {}) => {
},
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]',
},

Просмотреть файл

@ -4,12 +4,14 @@ import * as ko from 'knockout';
import './webpack-component-loader';
import AppRootComponent from './components/app-root/app-root';
const createHistory = require('history').createBrowserHistory;
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
const basename = baseUrl.substring(0, baseUrl.length - 1); // History component needs no trailing slash
// Load and register the <app-root> component
ko.components.register('app-root', AppRootComponent);
// Tell Knockout to start up an instance of your application
ko.applyBindings({ history: createHistory() });
ko.applyBindings({ history: createHistory({ basename }), basename });
// Basic hot reloading support. Automatically reloads and restarts the Knockout app each time
// you modify source files. This will not preserve any application state other than the URL.

Просмотреть файл

@ -1,7 +1,7 @@
<div class='container-fluid'>
<div class='row'>
<div class='col-sm-3'>
<nav-menu params='route: route'></nav-menu>
<nav-menu params='router: router'></nav-menu>
</div>
<div class='col-sm-9' data-bind='component: { name: route().page, params: route }'></div>
</div>

Просмотреть файл

@ -12,12 +12,12 @@ const routes: Route[] = [
class AppRootViewModel {
public route: KnockoutObservable<Route>;
private _router: Router;
public router: Router;
constructor(params: { history: History.History }) {
constructor(params: { history: History.History, basename: string }) {
// Activate the client-side router
this._router = new Router(params.history, routes)
this.route = this._router.currentRoute;
this.router = new Router(params.history, routes, params.basename);
this.route = this.router.currentRoute;
// Load and register all the KO components needed to handle the routes
// The optional 'bundle-loader?lazy!' prefix is a Webpack feature that causes the referenced modules
@ -32,7 +32,7 @@ class AppRootViewModel {
// To support hot module replacement, this method unregisters the router and KO components.
// In production scenarios where hot module replacement is disabled, this would not be invoked.
public dispose() {
this._router.dispose();
this.router.dispose();
// TODO: Need a better API for this
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {

Просмотреть файл

@ -12,7 +12,7 @@ class FetchDataViewModel {
public forecasts = ko.observableArray<WeatherForecast>();
constructor() {
fetch('/api/SampleData/WeatherForecasts')
fetch('api/SampleData/WeatherForecasts')
.then(response => response.json() as Promise<WeatherForecast[]>)
.then(data => {
this.forecasts(data);

Просмотреть файл

@ -13,17 +13,17 @@
<div class='navbar-collapse collapse'>
<ul class='nav navbar-nav'>
<li>
<a href='/' data-bind='css: { active: route().page === "home-page" }'>
<a data-bind='attr: { href: router.link("/") }, css: { active: route().page === "home-page" }'>
<span class='glyphicon glyphicon-home'></span> Home
</a>
</li>
<li>
<a href='/counter' data-bind='css: { active: route().page === "counter-example" }'>
<a data-bind='attr: { href: router.link("/counter") }, css: { active: route().page === "counter-example" }'>
<span class='glyphicon glyphicon-education'></span> Counter
</a>
</li>
<li>
<a href='/fetch-data' data-bind='css: { active: route().page === "fetch-data" }'>
<a data-bind='attr: { href: router.link("/fetch-data") }, css: { active: route().page === "fetch-data" }'>
<span class='glyphicon glyphicon-th-list'></span> Fetch data
</a>
</li>

Просмотреть файл

@ -1,18 +1,20 @@
import * as ko from 'knockout';
import { Route } from '../../router';
import { Route, Router } from '../../router';
interface NavMenuParams {
route: KnockoutObservable<Route>;
router: Router;
}
class NavMenuViewModel {
public router: Router;
public route: KnockoutObservable<Route>;
constructor(params: NavMenuParams) {
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
// You could remove this viewmodel entirely, and define 'nav-menu' as a template-only component.
// But in most apps, you'll want some viewmodel logic to determine what navigation options appear.
this.route = params.route;
this.router = params.router;
this.route = this.router.currentRoute;
}
}

Просмотреть файл

@ -16,7 +16,7 @@ export class Router {
private disposeHistory: () => void;
private clickEventListener: EventListener;
constructor(history: History.History, routes: Route[]) {
constructor(private history: History.History, routes: Route[], basename: string) {
// Reset and configure Crossroads so it matches routes and updates this.currentRoute
crossroads.removeAllRoutes();
crossroads.resetState();
@ -33,8 +33,9 @@ export class Router {
let target: any = evt.currentTarget;
if (target && target.tagName === 'A') {
let href = target.getAttribute('href');
if (href && href.charAt(0) == '/') {
history.push(href);
if (href && href.indexOf(basename + '/') === 0) {
const hrefAfterBasename = href.substring(basename.length);
history.push(hrefAfterBasename);
evt.preventDefault();
}
}
@ -46,6 +47,10 @@ export class Router {
crossroads.parse((history as any).location.pathname);
}
public link(url: string): string {
return this.history.createHref({ pathname: url });
}
public dispose() {
this.disposeHistory();
$(document).off('click', 'a', this.clickEventListener);

Просмотреть файл

@ -2,7 +2,7 @@
ViewData["Title"] = "Home Page";
}
<app-root params="history: history"></app-root>
<app-root params="history: history, basename: basename"></app-root>
@section scripts {
<script src="~/dist/main.js" asp-append-version="true"></script>

Просмотреть файл

@ -13,7 +13,7 @@ module.exports = (env) => {
output: {
path: path.join(__dirname, bundleOutputDir),
filename: '[name].js',
publicPath: '/dist/'
publicPath: 'dist/'
},
module: {
rules: [

Просмотреть файл

@ -21,7 +21,7 @@ module.exports = (env) => {
},
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]',
},

Просмотреть файл

@ -12,7 +12,8 @@ import * as RoutesModule from './routes';
let routes = RoutesModule.routes;
// Create browser history to use in the Redux store
const history = createBrowserHistory();
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
const history = createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
const initialState = (window as any).initialReduxState as ApplicationState;

Просмотреть файл

@ -12,15 +12,17 @@ export default createServerRenderer(params => {
return new Promise<RenderResult>((resolve, reject) => {
// Prepare Redux store with in-memory history, and dispatch a navigation event
// corresponding to the incoming URL
const basename = params.baseUrl.substring(0, params.baseUrl.length - 1); // Remove trailing slash
const urlAfterBasename = params.url.substring(basename.length);
const store = configureStore(createMemoryHistory());
store.dispatch(replace(params.location));
store.dispatch(replace(urlAfterBasename));
// Prepare an instance of the application and perform an inital render that will
// cause any async tasks (e.g., data access) to begin
const routerContext: any = {};
const app = (
<Provider store={ store }>
<StaticRouter context={ routerContext } location={ params.location.path } children={ routes } />
<StaticRouter basename={ basename } context={ routerContext } location={ params.location.path } children={ routes } />
</Provider>
);
renderToString(app);

Просмотреть файл

@ -45,7 +45,7 @@ export const actionCreators = {
requestWeatherForecasts: (startDateIndex: number): AppThunkAction<KnownAction> => (dispatch, getState) => {
// Only load data if it's something we don't already have (and are not already loading)
if (startDateIndex !== getState().weatherForecasts.startDateIndex) {
let fetchTask = fetch(`/api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`)
let fetchTask = fetch(`api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`)
.then(response => response.json() as Promise<WeatherForecast[]>)
.then(data => {
dispatch({ type: 'RECEIVE_WEATHER_FORECASTS', startDateIndex: startDateIndex, forecasts: data });

Просмотреть файл

@ -13,7 +13,7 @@ module.exports = (env) => {
resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
module: {
rules: [

Просмотреть файл

@ -33,7 +33,7 @@ module.exports = (env) => {
],
},
output: {
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]',
},

Просмотреть файл

@ -10,9 +10,10 @@ let routes = RoutesModule.routes;
function renderApp() {
// This code starts up the React app when it runs in a browser. It sets up the routing
// configuration and injects the app into a DOM element.
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
ReactDOM.render(
<AppContainer>
<BrowserRouter children={ routes } />
<BrowserRouter children={ routes } basename={ baseUrl } />
</AppContainer>,
document.getElementById('react-app')
);

Просмотреть файл

@ -11,7 +11,7 @@ export class FetchData extends React.Component<{}, FetchDataExampleState> {
super();
this.state = { forecasts: [], loading: true };
fetch('/api/SampleData/WeatherForecasts')
fetch('api/SampleData/WeatherForecasts')
.then(response => response.json() as Promise<WeatherForecast[]>)
.then(data => {
this.setState({ forecasts: data, loading: false });

Просмотреть файл

@ -13,7 +13,7 @@ module.exports = (env) => {
output: {
path: path.join(__dirname, bundleOutputDir),
filename: '[name].js',
publicPath: '/dist/'
publicPath: 'dist/'
},
module: {
rules: [

Просмотреть файл

@ -21,7 +21,7 @@ module.exports = (env) => {
},
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]',
},

Просмотреть файл

@ -13,7 +13,7 @@ export default class FetchDataComponent extends Vue {
forecasts: WeatherForecast[] = [];
mounted() {
fetch('/api/SampleData/WeatherForecasts')
fetch('api/SampleData/WeatherForecasts')
.then(response => response.json() as Promise<WeatherForecast[]>)
.then(data => {
this.forecasts = data;

Просмотреть файл

@ -23,7 +23,7 @@ module.exports = (env) => {
output: {
path: path.join(__dirname, bundleOutputDir),
filename: '[name].js',
publicPath: '/dist/'
publicPath: 'dist/'
},
plugins: [
new CheckerPlugin(),

Просмотреть файл

@ -28,7 +28,7 @@ module.exports = (env) => {
},
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
publicPath: '/dist/',
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]'
},