├── 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 |
18 | You need to enable JavaScript to run this app.
19 |
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 |
counterService.increment(1)}>Increment
18 |
counterService.decrement(1)}>Decrement
19 |
counterService.updateAll(2, 2)}>Update All
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 | Date
21 | Temp. (C)
22 | Temp. (F)
23 | Summary
24 |
25 |
26 |
27 | {weatherForecast.forecasts.map(forecast =>
28 |
29 | {forecast.dateFormatted}
30 | {forecast.temperatureC}
31 | {forecast.temperatureF}
32 | {forecast.summary}
33 |
34 | )}
35 |
36 |
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
--------------------------------------------------------------------------------