├── ReactCoreTemplate ├── ClientApp │ ├── src │ │ ├── decorators │ │ │ ├── index.ts │ │ │ └── debounce.ts │ │ ├── store │ │ │ ├── index.ts │ │ │ └── ApplicationState.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── connect.ts │ │ │ └── createServerRenderer.ts │ │ ├── services │ │ │ ├── httpclient │ │ │ │ ├── index.ts │ │ │ │ ├── formatRequestQuery.ts │ │ │ │ └── HttpClient.ts │ │ │ ├── index.ts │ │ │ ├── globals.ts │ │ │ ├── AppInfoSink.ts │ │ │ └── NavigationSink.ts │ │ ├── react-app-env.d.ts │ │ ├── components │ │ │ ├── WeatherForecast │ │ │ │ ├── weatherForecast.module.scss.d.ts │ │ │ │ ├── weatherForecast.module.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── WeatherForecastSink.ts │ │ │ │ └── WeatherForecast.tsx │ │ │ ├── index.ts │ │ │ ├── Counter │ │ │ │ ├── index.tsx │ │ │ │ ├── Counter.tsx │ │ │ │ └── CounterService.ts │ │ │ ├── Home.tsx │ │ │ └── Routes.tsx │ │ ├── index.tsx │ │ └── server.tsx │ ├── .vscode │ │ ├── settings.json │ │ └── launch.json │ ├── public │ │ ├── favicon.ico │ │ ├── media │ │ │ └── logo.png │ │ ├── manifest.json │ │ └── index.html │ ├── .babelrc │ ├── tslint.json │ ├── .gitignore │ ├── types │ │ ├── global.d.ts │ │ └── react-redux.d.ts │ ├── tspaths.json │ ├── postbuild.js │ ├── tsconfig.json │ ├── config-overrides.js │ └── package.json ├── appsettings.json ├── Pages │ ├── _ViewImports.cshtml │ ├── Error.cshtml.cs │ └── Error.cshtml ├── appsettings.Development.json ├── Program.cs ├── Services │ ├── SpaPrerenderingService.cs │ └── ServiceLocator.cs ├── Controllers │ └── SampleDataController.cs ├── Startup.cs ├── ReactCoreTemplate.csproj └── .gitignore ├── ReactCoreTemplate.sln ├── README.md ├── .gitattributes └── .gitignore /ReactCoreTemplate/ClientApp/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debounce'; -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApplicationState'; -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { connect } from './connect'; -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/httpclient/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HttpClient'; -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/WeatherForecast/weatherForecast.module.scss.d.ts: -------------------------------------------------------------------------------- 1 | export const container: string; 2 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/WeatherForecast/weatherForecast.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | background: #EFEEFE; 3 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiarongGu/ReactCoreTemplate/HEAD/ReactCoreTemplate/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-syntax-dynamic-import"] 4 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/public/media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiarongGu/ReactCoreTemplate/HEAD/ReactCoreTemplate/ClientApp/public/media/logo.png -------------------------------------------------------------------------------- /ReactCoreTemplate/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /ReactCoreTemplate/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using ReactCoreTemplate 2 | @namespace ReactCoreTemplate.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppInfoSink'; 2 | export * from './globals'; 3 | export * from './httpclient'; 4 | export * from './NavigationSink'; -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/store/ApplicationState.ts: -------------------------------------------------------------------------------- 1 | import { AppInfoState } from '../services'; 2 | 3 | export interface ApplicationState { 4 | appInfo: AppInfoState; 5 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Counter } from './Counter'; 2 | export { WeatherForecast } from './WeatherForecast'; 3 | export { Home } from './Home'; 4 | export { Routes } from './Routes'; -------------------------------------------------------------------------------- /ReactCoreTemplate/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ "tslint:react" ], 4 | "jsRules": {}, 5 | "rules": { "quotemark": [true, "single", "jsx-double"], "arrow-parens": false }, 6 | "rulesDirectory": [] 7 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/Counter/index.tsx: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | import * as React from 'react'; 3 | 4 | export const Counter = Loadable({ 5 | loader: () => import('./Counter'), 6 | loading: () => (
loading...
), 7 | }); -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/WeatherForecast/index.tsx: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | import * as React from 'react'; 3 | 4 | export const WeatherForecast = Loadable({ 5 | loader: () => import('./WeatherForecast'), 6 | loading: () => (
loading...
), 7 | }); -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/decorators/debounce.ts: -------------------------------------------------------------------------------- 1 | import _debounced from 'lodash/debounce'; 2 | 3 | export function debounce(wait: number, option?: any) { 4 | return function (target: any, name: string, descriptor: PropertyDescriptor) { 5 | descriptor.value = _debounced(descriptor.value, wait, option); 6 | } 7 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/globals.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | 3 | class ConfigurationService { 4 | private _axiosConfig?: AxiosRequestConfig; 5 | 6 | get axiosConfig() { 7 | return this._axiosConfig 8 | } 9 | 10 | set axiosConfig(config) { 11 | this._axiosConfig = config 12 | } 13 | } 14 | 15 | export const globals = new ConfigurationService(); -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ReactCoreTemplate", 3 | "name": "ReactCoreTemplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const Home = props => ( 5 |
6 |

Hello, world!

7 |

Welcome to your new single-page application, built with: .net core

8 |

Go Counter

9 |

Go Fetch Data

10 |
11 | ); -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /** Global definitions for development **/ 2 | 3 | // for style loader 4 | declare module '*.css' { 5 | const styles: any; 6 | export = styles; 7 | } 8 | 9 | // Omit type https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-377567046 10 | type Omit = Pick> 11 | type PartialPick = Partial & Pick; 12 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/AppInfoSink.ts: -------------------------------------------------------------------------------- 1 | import { sink, state } from 'redux-sink'; 2 | 3 | 4 | export class AppInfoState { 5 | logoUrl: string; 6 | isClient: boolean; 7 | 8 | constructor(isClient = true) { 9 | this.logoUrl = '/media/logo.png'; 10 | this.isClient = isClient; 11 | } 12 | } 13 | 14 | @sink('appInfoService') 15 | export class AppInfoService { 16 | @state 17 | state = new AppInfoState(); 18 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/Routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route, Switch } from "react-router"; 3 | import { Home, Counter, WeatherForecast } from "@components"; 4 | 5 | export const Routes = () => ( 6 | 7 | 8 | 9 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/httpclient/formatRequestQuery.ts: -------------------------------------------------------------------------------- 1 | export function formatRequestQuery(model: TModel) { 2 | const parameters: String[] = []; 3 | 4 | Object.keys(model).forEach(key => { 5 | if(model[key]) { 6 | if(Array.isArray(model[key])) { 7 | parameters.push(model[key].map(x => `${key}=${x}`)); 8 | } else { 9 | parameters.push(`${key}=${model[key]}`); 10 | } 11 | } 12 | }); 13 | 14 | return parameters.join("&"); 15 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/tspaths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@components*": ["./src/components*"], 6 | "@components": ["./src/components"], 7 | "@services": ["./src/services"], 8 | "@services*": ["./src/services*"], 9 | "@store*": ["./src/store*"], 10 | "@store": ["./src/store"], 11 | "@utils*": ["./src/utils*"], 12 | "@utils": ["./src/utils"], 13 | "@decorators*": ["./src/decorators*"], 14 | "@decorators": ["./src/decorators"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/NavigationSink.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createBrowserHistory, History, Location } from 'history'; 3 | import { sink, SinkFactory, state } from 'redux-sink'; 4 | 5 | 6 | @sink('navigation') 7 | export class NavigationSink { 8 | @state public history!: History; 9 | @state public location!: Location; 10 | } 11 | 12 | export const createNavigationHistory = () => { 13 | const history = createBrowserHistory(); 14 | const navigation = SinkFactory.getSink(NavigationSink); 15 | 16 | history.listen((location) => navigation.location = location); 17 | navigation.history = history; 18 | navigation.location = history.location; 19 | 20 | return history; 21 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ReactCoreTemplate 12 | 13 | 14 | 15 | 16 | 17 | 20 |
21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /ReactCoreTemplate/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace ReactCoreTemplate.Pages 10 | { 11 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 12 | public class ErrorModel : PageModel 13 | { 14 | public string RequestId { get; set; } 15 | 16 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 17 | 18 | public void OnGet() 19 | { 20 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/utils/connect.ts: -------------------------------------------------------------------------------- 1 | import { Options, MapStateToPropsParam, connect as originalConnect, MergeProps, MapDispatchToPropsParam } from "react-redux"; 2 | 3 | export function connect( 4 | mapStateToProps: MapStateToPropsParam, 5 | mapDispatchToProps?: MapDispatchToPropsParam, 6 | mergeProps?: MergeProps, 7 | options?: Options) 8 | { 9 | return function(target: any) 10 | { 11 | return originalConnect(mapStateToProps, mapDispatchToProps as any, mergeProps as any, options as any)(target) as any; 12 | } 13 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace ReactCoreTemplate 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseUrls("http://localhost:5000/;", "https://localhost:5001/") 23 | .UseStartup(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/postbuild.js: -------------------------------------------------------------------------------- 1 | const fse = require('fs-extra'); 2 | 3 | const fromArg = process.argv.find(x => x.startsWith('--from')); 4 | const toArg = process.argv.find(x => x.startsWith('--to')); 5 | const cleanArg = process.argv.find(x => x.startsWith('--clean')); 6 | 7 | if (!fromArg && !toArg && !cleanArg) 8 | return; 9 | 10 | const from = fromArg && fromArg.substring(7).trim(); 11 | const to = toArg && toArg.substring(5).trim(); 12 | const clean = cleanArg ? cleanArg.substring(8).trim() : to; 13 | 14 | if (from && to) 15 | { 16 | console.log(`Post build cleaning ${clean} ...`); 17 | fse.remove(clean).then(() => { 18 | console.log(`Post build file copying ${from} to ${to} ...`); 19 | fse.copy(from, to).then(() => { 20 | console.log('Copy completed') 21 | }); 22 | }) 23 | } else { 24 | console.log(`Post build cleaning ${clean} ...`); 25 | fse.remove(clean).then(() => { 26 | console.log('Clean completed'); 27 | }) 28 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to Development environment will display more detailed information about the error that occurred. 20 |

21 |

22 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 23 |

24 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/types/react-redux.d.ts: -------------------------------------------------------------------------------- 1 | import 'react-redux'; 2 | import "reflect-metadata"; 3 | 4 | declare module 'react-redux' { 5 | // Add removed inferrable type to support connect as decorator 6 | // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/16652 7 | export interface InferableComponentDecorator { 8 | (component: T): T; 9 | } 10 | 11 | // overload connect interface to return built-in ClassDecorator 12 | // https://github.com/reactjs/react-redux/pull/541#issuecomment-269197189 13 | export interface Connect { 14 | ( 15 | mapStateToProps: MapStateToPropsParam, 16 | mapDispatchToProps?: MapDispatchToPropsParam, 17 | mergeProps?: MergeProps, 18 | options?: Options 19 | ): InferableComponentDecorator; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "module": "esnext", 5 | "target": "es6", 6 | "lib": [ 7 | "es6", 8 | "es7", 9 | "dom" 10 | ], 11 | "sourceMap": true, 12 | "allowJs": true, 13 | "jsx": "preserve", 14 | "moduleResolution": "node", 15 | "downlevelIteration": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitReturns": false, 18 | "noImplicitThis": false, 19 | "noImplicitAny": false, 20 | "noUnusedLocals": false, 21 | "importHelpers": true, 22 | "strictNullChecks": true, 23 | "suppressImplicitAnyIndexErrors": true, 24 | "experimentalDecorators": true, 25 | "skipLibCheck": true, 26 | "esModuleInterop": true, 27 | "allowSyntheticDefaultImports": true, 28 | "strict": true, 29 | "resolveJsonModule": true, 30 | "isolatedModules": true, 31 | "noEmit": true 32 | }, 33 | "exclude": [ 34 | "build", 35 | "node_modules", 36 | "scripts", 37 | "dist" 38 | ], 39 | "include": [ 40 | "src" 41 | ], 42 | "extends": "./tspaths.json" 43 | } 44 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/Counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useSink } from 'redux-sink'; 3 | import { CounterService } from './CounterService'; 4 | import { AppInfoService } from '@services'; 5 | 6 | const Counter = () => { 7 | const counterService: CounterService = useSink(CounterService)!; 8 | const appInfoService: AppInfoService = useSink(AppInfoService)!; 9 | 10 | return ( 11 |
12 |

Counter - {appInfoService.state.isClient ? 'client' : 'server'}

13 |

Current Increment: {counterService.incrementCount}

14 |

Current Decrement: {counterService.decrementCount}

15 |

Current Total: {counterService.total}

16 |

Current Action Calls: {counterService.actions}

17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default Counter; -------------------------------------------------------------------------------- /ReactCoreTemplate.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactCoreTemplate", "ReactCoreTemplate\ReactCoreTemplate.csproj", "{972AEB52-B3DF-4FCD-817C-FDA472D4D1F9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {972AEB52-B3DF-4FCD-817C-FDA472D4D1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {972AEB52-B3DF-4FCD-817C-FDA472D4D1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {972AEB52-B3DF-4FCD-817C-FDA472D4D1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {972AEB52-B3DF-4FCD-817C-FDA472D4D1F9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {89A672D0-33FD-49E5-86E8-98CF259479E0} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ReactCoreTemplate/Services/SpaPrerenderingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace ReactCoreTemplate.Services 8 | { 9 | public static class SpaPrerenderingServiceLocator 10 | { 11 | public static Action> GetProcessor(IApplicationBuilder app) { 12 | return (HttpContext httpContext, IDictionary supplyData) => 13 | { 14 | var service = app.ApplicationServices.CreateScope().ServiceProvider.GetService(); 15 | service.Process(httpContext, supplyData); 16 | }; 17 | } 18 | } 19 | 20 | public interface ISpaPrerenderingService { 21 | void Process(HttpContext httpContext, IDictionary supplyData); 22 | } 23 | 24 | public class SpaPrerenderingService: ISpaPrerenderingService 25 | { 26 | public void Process(HttpContext httpContext, IDictionary supplyData) 27 | { 28 | supplyData["host"] = $"{httpContext.Request.Scheme}://{httpContext.Request.Host.ToString()}"; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ReactCoreTemplate/Services/ServiceLocator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace ReactCoreTemplate.Services 8 | { 9 | public class ServiceLocator 10 | { 11 | private ServiceProvider _currentServiceProvider; 12 | private static ServiceProvider _serviceProvider; 13 | 14 | public ServiceLocator(ServiceProvider currentServiceProvider) 15 | { 16 | _currentServiceProvider = currentServiceProvider; 17 | } 18 | 19 | public static ServiceLocator Current 20 | { 21 | get 22 | { 23 | return new ServiceLocator(_serviceProvider); 24 | } 25 | } 26 | 27 | public static void SetLocatorProvider(ServiceProvider serviceProvider) 28 | { 29 | _serviceProvider = serviceProvider; 30 | } 31 | 32 | public object GetInstance(Type serviceType) 33 | { 34 | return _currentServiceProvider.GetService(serviceType); 35 | } 36 | 37 | public TService GetInstance() 38 | { 39 | return _currentServiceProvider.GetService(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/Counter/CounterService.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { state, sink, effect, trigger } from 'redux-sink'; 4 | import { AppInfoService } from '@services'; 5 | 6 | @sink('counter') 7 | export class CounterService { 8 | @state 9 | incrementCount = 0; 10 | 11 | @state 12 | decrementCount = 0; 13 | 14 | @state 15 | total = 0; 16 | 17 | @state 18 | actions = 0; 19 | 20 | offset = 0 21 | appInfo = new AppInfoService(); 22 | 23 | @effect 24 | increment(value: number) { 25 | const increase = value + this.offset; 26 | this.offset++; 27 | this.incrementCount = this.incrementCount + increase; 28 | this.total = this.total + increase; 29 | } 30 | 31 | @effect 32 | decrement(value: number) { 33 | const decrease = value + this.offset; 34 | this.offset--; 35 | this.incrementCount = this.incrementCount - decrease; 36 | this.total = this.total - decrease; 37 | } 38 | 39 | @effect 40 | incrementall(values: Array) { 41 | this.total = this.total + values.reduce((a, b) => a + b, 0); 42 | } 43 | 44 | @effect 45 | updateAll(increment: number, decrement: number) { 46 | this.decrement(decrement); 47 | this.increment(increment); 48 | } 49 | 50 | @trigger('counter/decrement') 51 | @trigger('counter/increment') 52 | actionCounter() { 53 | this.actions ++; 54 | } 55 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { paths } = require('react-app-rewired'); 3 | const { addWebpackAlias } = require('customize-cra'); 4 | const webpack = require('webpack'); 5 | 6 | const isServer = process.argv.indexOf('--server') > 0; 7 | 8 | module.exports = { 9 | webpack: function (config, env) { 10 | config = addWebpackAlias({ 11 | '@store': path.resolve(__dirname, `${paths.appSrc}/store/`), 12 | '@components': path.resolve(__dirname, `${paths.appSrc}/components`), 13 | '@services': path.resolve(__dirname, `${paths.appSrc}/services`), 14 | '@utils': path.resolve(__dirname, `${paths.appSrc}/utils`), 15 | '@decorators': path.resolve(__dirname, `${paths.appSrc}/decorators`) 16 | })(config); 17 | 18 | // used for server-side bundle 19 | if (isServer) 20 | config = getServerConfig(config); 21 | return config; 22 | }, 23 | } 24 | 25 | function getServerConfig(config) { 26 | config = { 27 | ...config, 28 | target: 'node', 29 | entry: [`${paths.appSrc}/server.tsx`], 30 | devtool: false, 31 | output: { 32 | ...config.output, 33 | filename: 'bundle.js', 34 | chunkFilename: 'bundle.[chunkhash:8].chunk.js', 35 | libraryTarget: 'commonjs' 36 | }, 37 | optimization: undefined, 38 | plugins: [ 39 | ...config.plugins, 40 | new webpack.optimize.LimitChunkCountPlugin({ 41 | maxChunks: 1, 42 | }) 43 | ] 44 | } 45 | return config; 46 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { hot } from 'react-hot-loader'; 4 | import { Provider } from 'react-redux'; 5 | import { Routes } from './components'; 6 | import { composeWithDevTools } from 'redux-devtools-extension'; 7 | import { SinkFactory } from 'redux-sink'; 8 | import { Router } from 'react-router'; 9 | import { createNavigationHistory } from '@services'; 10 | 11 | declare global { 12 | interface Window { __PRELOADED_STATE__: any; } 13 | } 14 | 15 | // Grab the state from a global variable injected into the server-generated HTML 16 | const preloadedState = window.__PRELOADED_STATE__; 17 | 18 | // Allow the passed state to be garbage-collected 19 | delete window.__PRELOADED_STATE__; 20 | 21 | // prepare store 22 | const history = createNavigationHistory(); 23 | const store = SinkFactory.createStore({ 24 | preloadedState, 25 | devToolOptions: { devToolCompose: composeWithDevTools } 26 | }); 27 | 28 | // const locationChange = (location) => store.dispatch({ type: 'location_change', payload: location }); 29 | // history.listen(locationChange); 30 | 31 | // if (!preloadedState) 32 | // locationChange(history.location); 33 | 34 | // hot app module 35 | export const App = hot(module)(() => ( 36 | 37 | )); 38 | 39 | // initalize default state with requests, then render dom 40 | ReactDOM.hydrate( 41 | 42 | 43 | 44 | 45 | , 46 | document.getElementById('root') 47 | ); -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/WeatherForecast/WeatherForecastSink.ts: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router'; 2 | import { HttpClient } from '../../services'; 3 | import { sink, state, trigger, effect } from 'redux-sink'; 4 | import { globals } from '@services'; 5 | import { Location } from 'history'; 6 | 7 | @sink('weatherForecast') 8 | export class WeatherForecastSink { 9 | @state public forecasts: any[] = []; 10 | @state public loading: boolean = false; 11 | @state public index: number = 0; 12 | @state public error?: Error; 13 | 14 | async loadingPipe(action: Promise) { 15 | this.loading = true; 16 | this.error = undefined; 17 | try { 18 | return await action 19 | } catch (e) { 20 | this.error = e; 21 | } 22 | finally { 23 | this.loading = false; 24 | } 25 | } 26 | 27 | @trigger('navigation/location') 28 | async loadOnWeatherUrl(location: Location) { 29 | const matches = matchPath<{ index?: string }>(location.pathname, '/weather-forecast/:index?'); 30 | if (!matches) return; 31 | 32 | const index = parseInt((matches.params && matches.params.index) || '') || 0; 33 | this.index = index; 34 | return this.loadWeather(index); 35 | } 36 | 37 | @effect 38 | async loadWeather(index: number) { 39 | const httpClient = new HttpClient(globals.axiosConfig); 40 | const forecasts = await this.loadingPipe( 41 | httpClient.get(`/api/SampleData/WeatherForecasts?startDateIndex=${index}`) 42 | ); 43 | this.forecasts = forecasts && forecasts.data; 44 | return forecasts; 45 | } 46 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/Controllers/SampleDataController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace ReactCoreTemplate.Controllers 8 | { 9 | [Route("[controller]")] 10 | [ApiController] 11 | public class SampleDataController : ControllerBase 12 | { 13 | private static string[] Summaries = new[] 14 | { 15 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 16 | }; 17 | 18 | [HttpGet("[action]")] 19 | public IEnumerable WeatherForecasts([FromRoute]int startDateIndex) 20 | { 21 | var rng = new Random(); 22 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 23 | { 24 | DateFormatted = DateTime.Now.AddDays(index + startDateIndex).ToString("d"), 25 | TemperatureC = rng.Next(-20, 55), 26 | Summary = Summaries[rng.Next(Summaries.Length)] 27 | }); 28 | } 29 | 30 | public class WeatherForecast 31 | { 32 | public string DateFormatted { get; set; } 33 | public int TemperatureC { get; set; } 34 | public string Summary { get; set; } 35 | 36 | public int TemperatureF 37 | { 38 | get 39 | { 40 | return 32 + (int)(TemperatureC / 0.5556); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/components/WeatherForecast/WeatherForecast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { sinking } from 'redux-sink'; 4 | import { WeatherForecastSink } from './WeatherForecastSink'; 5 | 6 | @sinking(WeatherForecastSink) 7 | export default class WeatherForecast extends React.PureComponent { 8 | render() { 9 | const weatherForecast = this.props.weatherForecast as WeatherForecastSink; 10 | return ( 11 |
12 |

Weather forecast

13 |

This component demonstrates fetching data from the server and working with URL parameters.

14 | {weatherForecast.loading &&

loading forecasts...

} 15 | {weatherForecast.error &&

{weatherForecast.error.message}

} 16 | {weatherForecast.forecasts && 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {weatherForecast.forecasts.map(forecast => 28 | 29 | 30 | 31 | 32 | 33 | 34 | )} 35 | 36 |
DateTemp. (C)Temp. (F)Summary
{forecast.dateFormatted}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
37 | } 38 |

Previous

39 |

Next

40 |
41 | ); 42 | } 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactCoreTemplate 2 | Example for .Net Core + React + Redux + Typescript + Server Rendering + Code Split + Scss 3 | Using creat-react-app (react-script) with react-app-rewired 4 | 5 | hope this can help someone who is seeking for .net core + react 6 | 7 | ## Getting Started 8 | This project is using .net core 2.2, please ensure you have the right sdk. 9 | https://dotnet.microsoft.com/download/dotnet-core/2.2 10 | 11 | Run the complie in ClientApp before you start the .net core webapp 12 | ``` 13 | npm run build:all 14 | ``` 15 | The proxy for api to .net core using https port 5001 16 | 17 | ## Rewire Overrides 18 | [react-app-rewired](https://github.com/timarney/react-app-rewired]) for override react-script default config. 19 | currently used for: 20 | - apply typescript alias 21 | - override config to generate server bundle 22 | 23 | ## SCSS Typing 24 | css-module typing is a marjor problem for using typescript, so I used a helper library to do it. 25 | [typed-scss-modules](https://github.com/skovy/typed-scss-modules) for generate scss.d.ts files to use for css module. 26 | generate typing files use: 27 | ``` 28 | npm run scss 29 | ``` 30 | watch scss file changes auto-generate use: 31 | ``` 32 | npm run scss:watch 33 | ``` 34 | 35 | ## Server Side Rendering 36 | because this project used .net core to follow that we need to create a separated bundle for server, which current react-script does not support, so I did some hack 37 | 38 | build in server mode when pass ``--server`` in ``react-app-rewire build``, the bundle will be built in server mode. 39 | 40 | postbuild scripts to move the files from ``build`` to ``dist`` to avoid file clean up by second run of react-script. 41 | 42 | ``server.tsx`` uses ``aspnet-prerendering`` as interface wrapper for .net core use 43 | 44 | ## Redux Creator 45 | [redux-creator](https://github.com/JiarongGu/banbrick-redux-creator) is a library I created for redux code-spliting, and also used for ssr. It will gives a simpler use for redux, also ``processLocationTasks`` and ``getEffectTasks`` to ensure the data is completely loaded when rendering server html. 46 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-core-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-app-rewired start", 7 | "build": "react-app-rewired build", 8 | "postbuild": "node postbuild.js --from=build --to=dist", 9 | "build:ssr": "react-app-rewired build --server", 10 | "postbuild:ssr": "node postbuild.js --from=build/bundle.js --to=dist/server/bundle.js --clean=dist/server", 11 | "build:all": "npm run scss & npm run build & npm run build:ssr", 12 | "postbuild:all": "node postbuild.js --clean=build", 13 | "scss": "node ./node_modules/typed-scss-modules/dist/lib/cli.js src/**/*.module.scss", 14 | "scss:watch": "node ./node_modules/typed-scss-modules/dist/lib/cli.js src/**/*.module.scss --watch", 15 | "eject": "react-app-rewired eject", 16 | "format": "prettier --write \"src/**/*.{ts,tsx,css}\"" 17 | }, 18 | "browserslist": [ 19 | ">0.2%", 20 | "not dead", 21 | "not ie <= 11", 22 | "not op_mini all" 23 | ], 24 | "proxy": "https://localhost:5001", 25 | "devDependencies": { 26 | "@types/classnames": "2.2.6", 27 | "@types/history": "4.7.0", 28 | "@types/node": "10.5.7", 29 | "@types/react": "16.8.3", 30 | "@types/react-dom": "16.8.1", 31 | "@types/react-helmet": "^5.0.7", 32 | "@types/react-redux": "7.1.1", 33 | "@types/react-router": "5.0.1", 34 | "@types/react-router-dom": "^4.3.0", 35 | "customize-cra": "^0.2.12", 36 | "fs-extra": "^7.0.1", 37 | "node-sass": "^4.11.0", 38 | "prettier": "^1.14.2", 39 | "react-app-rewired": "^2.1.0", 40 | "react-hot-loader": "^4.6.5", 41 | "react-scripts": "^3.0.1", 42 | "redux-devtools-extension": "^2.13.8", 43 | "reflect-metadata": "^0.1.12", 44 | "tslint": "^5.12.1", 45 | "typed-scss-modules": "0.0.6", 46 | "typescript": "^3.3.3" 47 | }, 48 | "dependencies": { 49 | "axios": "^0.19.0", 50 | "classnames": "^2.2.6", 51 | "lodash": "^4.17.13", 52 | "react": "^16.8.2", 53 | "react-dom": "^16.8.2", 54 | "react-helmet": "^5.2.1", 55 | "react-loadable": "^5.5.0", 56 | "react-redux": "^7.1.0", 57 | "react-router": "^5.0.1", 58 | "react-router-dom": "^5.0.1", 59 | "redux-sink": "^0.12.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/services/httpclient/HttpClient.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosRequestConfig, AxiosInstance, AxiosPromise, AxiosInterceptorManager, AxiosResponse, CancelTokenSource } from 'axios'; 2 | import { formatRequestQuery } from './formatRequestQuery'; 3 | 4 | export class HttpClient { 5 | _config?: AxiosRequestConfig; 6 | _axios: AxiosInstance; 7 | 8 | constructor(config?: AxiosRequestConfig) { 9 | // initalize config if does not supply 10 | this.cancelTokenSource = Axios.CancelToken.source(); 11 | const defualtConfig = { cancelToken: this.cancelTokenSource.token }; 12 | const axiosConfig = config ? Object.assign({}, config, defualtConfig) : defualtConfig; 13 | 14 | this._config = axiosConfig; 15 | this._axios = Axios.create(this._config); 16 | this.interceptors = { 17 | request: this._axios.interceptors.request, 18 | response: this._axios.interceptors.response 19 | } 20 | } 21 | 22 | cancelTokenSource: CancelTokenSource; 23 | 24 | interceptors: { 25 | request: AxiosInterceptorManager; 26 | response: AxiosInterceptorManager>; 27 | }; 28 | 29 | request(config: AxiosRequestConfig): AxiosPromise { 30 | return this._axios.request(config); 31 | } 32 | 33 | delete(url: string, config?: AxiosRequestConfig): AxiosPromise { 34 | return this._axios.delete(url, config); 35 | } 36 | 37 | head(url: string, config?: AxiosRequestConfig): AxiosPromise { 38 | return this._axios.head(url, config); 39 | } 40 | 41 | get(url: string, data?: TRequest, config?: AxiosRequestConfig): AxiosPromise { 42 | if (data) 43 | return this._axios.get(`${url}?${formatRequestQuery(data)}`, config); 44 | return this._axios.get(url, config); 45 | } 46 | 47 | post(url: string, data?: TRequest, config?: AxiosRequestConfig): AxiosPromise { 48 | return this._axios.post(url, data, config); 49 | } 50 | 51 | put(url: string, data?: TRequest, config?: AxiosRequestConfig): AxiosPromise { 52 | return this._axios.put(url, data, config); 53 | } 54 | 55 | patch(url: string, data?: TRequest, config?: AxiosRequestConfig): AxiosPromise { 56 | return this._axios.patch(url, data, config); 57 | } 58 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using ReactCoreTemplate.Services; 7 | using System.Threading.Tasks; 8 | 9 | namespace ReactCoreTemplate 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 24 | 25 | services.AddScoped(); 26 | 27 | // In production, the React files will be served from this directory 28 | services.AddSpaStaticFiles(configuration => 29 | { 30 | configuration.RootPath = "ClientApp/dist"; 31 | }); 32 | 33 | ServiceLocator.SetLocatorProvider(services.BuildServiceProvider()); 34 | } 35 | 36 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 37 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 38 | { 39 | if (env.IsDevelopment()) 40 | { 41 | app.UseDeveloperExceptionPage(); 42 | } 43 | else 44 | { 45 | app.UseExceptionHandler("/Error"); 46 | app.UseHsts(); 47 | } 48 | 49 | app.UseHttpsRedirection(); 50 | 51 | app.Map("/api", apiApp => { 52 | apiApp.UseMvc(routes => routes.MapRoute("default", "{controller}/{action=Index}/{id?}")); 53 | }); 54 | 55 | app.UseStaticFiles(); 56 | app.UseSpaStaticFiles(); 57 | 58 | app.UseSpa(spa => 59 | { 60 | spa.Options.SourcePath = "ClientApp"; 61 | spa.UseSpaPrerendering(options => 62 | { 63 | options.BootModulePath = $"{spa.Options.SourcePath}/dist/server/bundle.js"; 64 | options.SupplyData = SpaPrerenderingServiceLocator.GetProcessor(app); 65 | }); 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ReactCoreTemplate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | %(DistFiles.Identity) 54 | PreserveNewest 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as https from "https"; 3 | import Loadable from "react-loadable"; 4 | import Helmet from "react-helmet"; 5 | import { Provider } from "react-redux"; 6 | import { renderToString } from "react-dom/server"; 7 | import { StaticRouter } from "react-router-dom"; 8 | import { StaticRouterContext } from "react-router"; 9 | import { SinkFactory } from "redux-sink"; 10 | import { AppInfoState, globals, NavigationSink } from "./services"; 11 | import { Routes } from "./components"; 12 | import { 13 | createServerRenderer, 14 | BootFuncParams, 15 | RenderResult 16 | } from "@utils/createServerRenderer"; 17 | 18 | export default createServerRenderer( 19 | async (params: BootFuncParams): Promise => { 20 | // Prepare Redux store with in-memory history, and dispatch a navigation event 21 | // corresponding to the incoming URL 22 | const basename = params.baseUrl.substring(0, params.baseUrl.length - 1); // Remove trailing slash 23 | const urlAfterBasename = params.url.substring(basename.length); 24 | 25 | // Server supplyData 26 | const host = params.data.host; 27 | const originalHtml = params.data.originalHtml; 28 | 29 | // Prepare store 30 | const httpsAgent = new https.Agent({ rejectUnauthorized: false }); 31 | const preloadedState: any = { appInfoService: new AppInfoState(false) }; 32 | 33 | const store = SinkFactory.createStore({ 34 | preloadedState, 35 | effectTrace: true 36 | }); 37 | 38 | // dispatch new http config 39 | globals.axiosConfig = { baseURL: host, httpsAgent }; 40 | 41 | // load all chunk components 42 | await Loadable.preloadAll(); 43 | 44 | // Prepare an instance of the application and perform an initial render that will 45 | const routerContext: StaticRouterContext = { url: undefined }; 46 | 47 | const navigation = SinkFactory.getSink(NavigationSink); 48 | navigation.location = { pathname: urlAfterBasename } as any; 49 | 50 | const app = ( 51 | 52 | 57 | 58 | 59 | 60 | ); 61 | 62 | 63 | // ensure all effect task completed 64 | await Promise.all(SinkFactory.getTasks()); 65 | 66 | // If there's a redirection, just send this information back to the host application 67 | if (routerContext.url) { 68 | return { redirectUrl: routerContext.url }; 69 | } 70 | 71 | // render headers 72 | const header = Helmet.renderStatic(); 73 | const headerTags = 74 | `${header.title.toString()}\n` + 75 | `${header.meta.toString()}\n` + 76 | `${header.link.toString()}\n` + 77 | `${header.script.toString()}\n` + 78 | `${header.noscript.toString()}`; 79 | const state = store.getState(); 80 | 81 | return { 82 | html: originalHtml 83 | .replace(holderTag("body"), renderToString(app)) 84 | .replace(holderTag("header"), headerTags) 85 | .replace( 86 | holderTag("store"), 87 | `` 90 | ) 91 | }; 92 | } 93 | ); 94 | 95 | function holderTag(holder: string) { 96 | return ``; 97 | } 98 | -------------------------------------------------------------------------------- /ReactCoreTemplate/ClientApp/src/utils/createServerRenderer.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | 3 | export interface BootModuleInfo { 4 | moduleName: string; 5 | exportName?: string; 6 | webpackConfig?: string; 7 | } 8 | 9 | export interface RenderToStringFunc { 10 | ( 11 | callback: RenderToStringCallback, 12 | applicationBasePath: string, 13 | bootModule: BootModuleInfo, 14 | absoluteRequestUrl: string, 15 | requestPathAndQuery: string, 16 | customDataParameter: any, 17 | overrideTimeoutMilliseconds: number, 18 | requestPathBase: string 19 | ): void; 20 | } 21 | 22 | export interface RenderToStringCallback { 23 | (error: any, result?: RenderResult): void; 24 | } 25 | 26 | export interface RenderToStringResult { 27 | html: string; 28 | statusCode?: number; 29 | globals?: { 30 | [key: string]: any; 31 | }; 32 | } 33 | 34 | export interface RedirectResult { 35 | redirectUrl: string; 36 | } 37 | 38 | export declare type RenderResult = RenderToStringResult | RedirectResult; 39 | 40 | export interface BootFuncParams { 41 | location: any; 42 | origin: string; 43 | url: string; 44 | baseUrl: string; 45 | absoluteUrl: string; 46 | data: any; 47 | } 48 | 49 | export interface BootFunc { 50 | (params: BootFuncParams): Promise; 51 | } 52 | 53 | const defaultTimeoutMilliseconds = 30 * 1000; 54 | 55 | const getTimeoutError = (timeoutMilliseconds, moduleName) => 56 | `Prerendering timed out after ${timeoutMilliseconds}ms because the boot function in '${moduleName}' ` 57 | + 'returned a promise that did not resolve or reject. Make sure that your boot function always resolves or rejects its promise.'; 58 | 59 | export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc { 60 | const resultFunc = (callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number, requestPathBase: string) => { 61 | // Prepare a promise that will represent the completion of all domain tasks in this execution context. 62 | // The boot code will wait for this before performing its final render. 63 | 64 | const parsedAbsoluteRequestUrl = url.parse(absoluteRequestUrl); 65 | const params: BootFuncParams = { 66 | // It's helpful for boot funcs to receive the query as a key-value object, so parse it here 67 | // e.g., react-redux-router requires location.query to be a key-value object for consistency with client-side behaviour 68 | location: url.parse(requestPathAndQuery, /* parseQueryString */ true), 69 | origin: parsedAbsoluteRequestUrl.protocol + '//' + parsedAbsoluteRequestUrl.host, 70 | url: requestPathAndQuery, 71 | baseUrl: (requestPathBase || '') + '/', 72 | absoluteUrl: absoluteRequestUrl, 73 | data: customDataParameter 74 | }; 75 | 76 | const bootFuncPromise = bootFunc(params); 77 | 78 | if (!bootFuncPromise || typeof bootFuncPromise.then !== 'function') { 79 | callback(`Prerendering failed because the boot function in ${bootModule.moduleName} did not return a promise.`, undefined); 80 | return; 81 | } 82 | 83 | const timeoutMilliseconds = overrideTimeoutMilliseconds || defaultTimeoutMilliseconds; // e.g., pass -1 to override as 'never time out' 84 | const bootFuncPromiseWithTimeout = wrapWithTimeout(bootFuncPromise, timeoutMilliseconds, getTimeoutError(timeoutMilliseconds, bootModule.moduleName)); 85 | 86 | // Actually perform the rendering 87 | bootFuncPromiseWithTimeout.then(successResult => { 88 | callback(null, successResult); 89 | }, error => { 90 | callback(error, undefined); 91 | }); 92 | }; 93 | 94 | // Indicate to the prerendering code bundled into Microsoft.AspNetCore.SpaServices that this is a serverside rendering 95 | // function, so it can be invoked directly. This flag exists only so that, in its absence, we can run some different 96 | // backward-compatibility logic. 97 | resultFunc['isServerRenderer'] = true; 98 | 99 | return resultFunc; 100 | } 101 | 102 | function wrapWithTimeout(promise: Promise, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise { 103 | return new Promise((resolve, reject) => { 104 | const timeoutTimer = setTimeout(() => { 105 | reject(timeoutRejectionValue); 106 | }, timeoutMilliseconds); 107 | 108 | promise.then( 109 | resolvedValue => { 110 | clearTimeout(timeoutTimer); 111 | resolve(resolvedValue); 112 | }, 113 | rejectedValue => { 114 | clearTimeout(timeoutTimer); 115 | reject(rejectedValue); 116 | } 117 | ) 118 | }); 119 | } -------------------------------------------------------------------------------- /ReactCoreTemplate/.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | dist/ 24 | bld/ 25 | bin/ 26 | Bin/ 27 | obj/ 28 | Obj/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | /wwwroot/dist/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | orleans.codegen.cs 185 | 186 | /node_modules 187 | 188 | # RIA/Silverlight projects 189 | Generated_Code/ 190 | 191 | # Backup & report files from converting an old project file 192 | # to a newer Visual Studio version. Backup files are not needed, 193 | # because we have git ;-) 194 | _UpgradeReport_Files/ 195 | Backup*/ 196 | UpgradeLog*.XML 197 | UpgradeLog*.htm 198 | 199 | # SQL Server files 200 | *.mdf 201 | *.ldf 202 | 203 | # Business Intelligence projects 204 | *.rdl.data 205 | *.bim.layout 206 | *.bim_*.settings 207 | 208 | # Microsoft Fakes 209 | FakesAssemblies/ 210 | 211 | # GhostDoc plugin setting file 212 | *.GhostDoc.xml 213 | 214 | # Node.js Tools for Visual Studio 215 | .ntvs_analysis.dat 216 | 217 | # Visual Studio 6 build log 218 | *.plg 219 | 220 | # Visual Studio 6 workspace options file 221 | *.opt 222 | 223 | # Visual Studio LightSwitch build output 224 | **/*.HTMLClient/GeneratedArtifacts 225 | **/*.DesktopClient/GeneratedArtifacts 226 | **/*.DesktopClient/ModelManifest.xml 227 | **/*.Server/GeneratedArtifacts 228 | **/*.Server/ModelManifest.xml 229 | _Pvt_Extensions 230 | 231 | # Paket dependency manager 232 | .paket/paket.exe 233 | 234 | # FAKE - F# Make 235 | .fake/ 236 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc --------------------------------------------------------------------------------