├── .babelrc ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .whitesource ├── ClientApp ├── app.js ├── client.js ├── components │ ├── App.vue │ ├── Dashboard.vue │ ├── Message.vue │ └── Messages.vue ├── renderOnServer.js ├── router │ └── index.js ├── server.js └── vuex │ ├── actions.js │ └── store.js ├── Controllers └── HomeController.cs ├── LICENSE ├── Models ├── ClientState.cs ├── FakeMessageStore.cs └── Message.cs ├── Program.cs ├── README.md ├── Startup.cs ├── Views ├── Home │ └── Index.cshtml └── _ViewImports.cshtml ├── appsettings.Development.json ├── appsettings.json ├── package.before_client_side_routing.json ├── package.before_hot_reload.json ├── package.before_loading_indicator.json ├── package.before_server_side_rendering.json ├── package.json ├── vdn.csproj └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", "stage-2" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: axios 11 | versions: 12 | - "> 0.19.2, < 0.20" 13 | - dependency-name: axios 14 | versions: 15 | - "> 0.20.0, < 1" 16 | - dependency-name: webpack-cli 17 | versions: 18 | - 4.3.1 19 | - 4.4.0 20 | - 4.5.0 21 | - dependency-name: webpack 22 | versions: 23 | - 5.24.2 24 | - dependency-name: css-loader 25 | versions: 26 | - 5.0.1 27 | - 5.1.1 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | #js dependencies 7 | wwwroot/dist/ 8 | wwwroot/dist/*.* 9 | ##already included 10 | #node_modules/ 11 | *lock.json 12 | *.[Dd]evelopment.json 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | 35 | # Visual Studio 2015 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # Benchmark Results 54 | BenchmarkDotNet.Artifacts/ 55 | 56 | # .NET Core 57 | project.lock.json 58 | project.fragment.lock.json 59 | artifacts/ 60 | **/Properties/launchSettings.json 61 | 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.pch 69 | *.pdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # TFS 2012 Local Workspace 108 | $tf/ 109 | 110 | # Guidance Automation Toolkit 111 | *.gpState 112 | 113 | # ReSharper is a .NET coding add-in 114 | _ReSharper*/ 115 | *.[Rr]e[Ss]harper 116 | *.DotSettings.user 117 | 118 | # JustCode is a .NET coding add-in 119 | .JustCode 120 | 121 | # TeamCity is a build add-in 122 | _TeamCity* 123 | 124 | # DotCover is a Code Coverage Tool 125 | *.dotCover 126 | 127 | # AxoCover is a Code Coverage Tool 128 | .axoCover/* 129 | !.axoCover/settings.json 130 | 131 | # Visual Studio code coverage results 132 | *.coverage 133 | *.coveragexml 134 | 135 | # NCrunch 136 | _NCrunch_* 137 | .*crunch*.local.xml 138 | nCrunchTemp_* 139 | 140 | # MightyMoose 141 | *.mm.* 142 | AutoTest.Net/ 143 | 144 | # Web workbench (sass) 145 | .sass-cache/ 146 | 147 | # Installshield output folder 148 | [Ee]xpress/ 149 | 150 | # DocProject is a documentation generator add-in 151 | DocProject/buildhelp/ 152 | DocProject/Help/*.HxT 153 | DocProject/Help/*.HxC 154 | DocProject/Help/*.hhc 155 | DocProject/Help/*.hhk 156 | DocProject/Help/*.hhp 157 | DocProject/Help/Html2 158 | DocProject/Help/html 159 | 160 | # Click-Once directory 161 | publish/ 162 | 163 | # Publish Web Output 164 | *.[Pp]ublish.xml 165 | *.azurePubxml 166 | # Note: Comment the next line if you want to checkin your web deploy settings, 167 | # but database connection strings (with potential passwords) will be unencrypted 168 | *.pubxml 169 | *.publishproj 170 | 171 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 172 | # checkin your Azure Web App publish settings, but sensitive information contained 173 | # in these scripts will be unencrypted 174 | PublishScripts/ 175 | 176 | # NuGet Packages 177 | *.nupkg 178 | # The packages folder can be ignored because of Package Restore 179 | **/packages/* 180 | # except build/, which is used as an MSBuild target. 181 | !**/packages/build/ 182 | # Uncomment if necessary however generally it will be regenerated when needed 183 | #!**/packages/repositories.config 184 | # NuGet v3's project.json files produces more ignorable files 185 | *.nuget.props 186 | *.nuget.targets 187 | 188 | # Microsoft Azure Build Output 189 | csx/ 190 | *.build.csdef 191 | 192 | # Microsoft Azure Emulator 193 | ecf/ 194 | rcf/ 195 | 196 | # Windows Store app package directories and files 197 | AppPackages/ 198 | BundleArtifacts/ 199 | Package.StoreAssociation.xml 200 | _pkginfo.txt 201 | *.appx 202 | 203 | # Visual Studio cache files 204 | # files ending in .cache can be ignored 205 | *.[Cc]ache 206 | # but keep track of directories ending in .cache 207 | !*.[Cc]ache/ 208 | 209 | # Others 210 | ClientBin/ 211 | ~$* 212 | *~ 213 | *.dbmdl 214 | *.dbproj.schemaview 215 | *.jfm 216 | *.pfx 217 | *.publishsettings 218 | orleans.codegen.cs 219 | 220 | # Since there are multiple workflows, uncomment next line to ignore bower_components 221 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 222 | #bower_components/ 223 | 224 | # RIA/Silverlight projects 225 | Generated_Code/ 226 | 227 | # Backup & report files from converting an old project file 228 | # to a newer Visual Studio version. Backup files are not needed, 229 | # because we have git ;-) 230 | _UpgradeReport_Files/ 231 | Backup*/ 232 | UpgradeLog*.XML 233 | UpgradeLog*.htm 234 | 235 | # SQL Server files 236 | *.mdf 237 | *.ldf 238 | *.ndf 239 | 240 | # Business Intelligence projects 241 | *.rdl.data 242 | *.bim.layout 243 | *.bim_*.settings 244 | 245 | # Microsoft Fakes 246 | FakesAssemblies/ 247 | 248 | # GhostDoc plugin setting file 249 | *.GhostDoc.xml 250 | 251 | # Node.js Tools for Visual Studio 252 | .ntvs_analysis.dat 253 | node_modules/ 254 | 255 | # Typescript v1 declaration files 256 | typings/ 257 | 258 | # Visual Studio 6 build log 259 | *.plg 260 | 261 | # Visual Studio 6 workspace options file 262 | *.opt 263 | 264 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 265 | *.vbw 266 | 267 | # Visual Studio LightSwitch build output 268 | **/*.HTMLClient/GeneratedArtifacts 269 | **/*.DesktopClient/GeneratedArtifacts 270 | **/*.DesktopClient/ModelManifest.xml 271 | **/*.Server/GeneratedArtifacts 272 | **/*.Server/ModelManifest.xml 273 | _Pvt_Extensions 274 | 275 | # Paket dependency manager 276 | .paket/paket.exe 277 | paket-files/ 278 | 279 | # FAKE - F# Make 280 | .fake/ 281 | 282 | # JetBrains Rider 283 | .idea/ 284 | *.sln.iml 285 | 286 | # CodeRush 287 | .cr/ 288 | 289 | # Python Tools for Visual Studio (PTVS) 290 | __pycache__/ 291 | *.pyc 292 | 293 | # Cake - Uncomment if you are using it 294 | # tools/** 295 | # !tools/packages.config 296 | 297 | # Tabs Studio 298 | *.tss 299 | 300 | # Telerik's JustMock configuration file 301 | *.jmconfig 302 | 303 | # BizTalk build output 304 | *.btp.cs 305 | *.btm.cs 306 | *.odx.cs 307 | *.xsd.cs 308 | .vscode/set_dev.ps1 309 | vuejs-aspnetcore-ssr1.sln 310 | vuejs-aspnetcore-ssr.sln 311 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/vdn.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "taskName": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/vdn.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "generalSettings": { 3 | "shouldScanRepo": true 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure" 7 | } 8 | } -------------------------------------------------------------------------------- /ClientApp/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './components/App.vue' 3 | import store from './vuex/store.js' 4 | import router from './router' 5 | //import BootstrapVue from 'bootstrap-vue' 6 | 7 | //Vue.use(BootstrapVue) 8 | 9 | const app = new Vue({ 10 | router, 11 | store, 12 | ...App // ... is spread operator if App is Array; is rest(remaining) properties if App is Object 13 | }) 14 | 15 | export { 16 | app, 17 | router, 18 | store 19 | } 20 | -------------------------------------------------------------------------------- /ClientApp/client.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | 4 | import 'bootstrap/dist/css/bootstrap.css' 5 | import 'bootstrap-vue/dist/bootstrap-vue.css' 6 | 7 | import { 8 | app, 9 | router, 10 | store 11 | } from './app' 12 | import Vue from 'vue' 13 | import BootstrapVue from 'bootstrap-vue' 14 | Vue.use(BootstrapVue) 15 | store.replaceState(__INITIAL_STATE__) 16 | 17 | router.onReady(() => { 18 | router.beforeResolve((to, from, next) => { 19 | const matched = router.getMatchedComponents(to) 20 | const prevMatched = router.getMatchedComponents(from) 21 | 22 | // compare two list of components from previous route and current route 23 | let diffed = false 24 | const activated = matched.filter((c, i) => { 25 | return diffed || (diffed = (prevMatched[i] !== c)) 26 | }) 27 | 28 | // if no new components loaded, do nothing 29 | if (!activated.length) { 30 | return next() 31 | } 32 | 33 | NProgress.start() 34 | 35 | // for each newly loaded components, asynchorously load data to them 36 | Promise.all(activated.map(c => { 37 | if (c.asyncData) { 38 | return c.asyncData({store, route: to}) 39 | } 40 | })).then(() => { 41 | NProgress.done() 42 | next() 43 | }).catch(next) 44 | }) 45 | app.$mount('#app') 46 | }) 47 | -------------------------------------------------------------------------------- /ClientApp/components/App.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ClientApp/components/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /ClientApp/components/Message.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /ClientApp/components/Messages.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /ClientApp/renderOnServer.js: -------------------------------------------------------------------------------- 1 | process.env.VUE_ENV = 'server' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const filePath = path.join(__dirname, '../wwwroot/dist/main-server.js') 7 | const code = fs.readFileSync(filePath, 'utf8') 8 | 9 | const vue_renderer = require('vue-server-renderer').createBundleRenderer(code) 10 | 11 | //prevent XSS attack when initialize state 12 | var serialize = require('serialize-javascript') 13 | var prerendering = require('aspnet-prerendering') 14 | module.exports = prerendering.createServerRenderer(function (params) { 15 | return new Promise( 16 | function (resolve, reject) { 17 | // console.log('#prerendering#',params) 18 | const context = { 19 | url: params.url, 20 | absoluteUrl: params.absoluteUrl, 21 | baseUrl: params.baseUrl, 22 | data: params.data, 23 | domainTasks: params.domainTasks, 24 | location: params.location, 25 | origin: params.origin, 26 | xss: serialize("") 27 | } 28 | const serverVueAppHtml = vue_renderer.renderToString(context, (err, _html) => { 29 | if(err) { reject(err.message) } 30 | resolve({ 31 | globals: { 32 | html: _html, 33 | __INITIAL_STATE__: context.state 34 | } 35 | }) 36 | }) 37 | }) 38 | }); 39 | -------------------------------------------------------------------------------- /ClientApp/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import Dashboard from '../components/Dashboard.vue' 5 | import Messages from '../components/Messages.vue' 6 | 7 | Vue.use(VueRouter) 8 | 9 | const router = new VueRouter({ 10 | mode: 'history', 11 | routes: [ 12 | { path: '/', component: Dashboard }, 13 | { path: '/messages', component: Messages } 14 | ] 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /ClientApp/server.js: -------------------------------------------------------------------------------- 1 | import { app, router, store } from './app' 2 | 3 | export default context => { 4 | return new Promise((resolve, reject) => { 5 | router.push(context.url) 6 | 7 | router.onReady(() => { 8 | const matchedComponents = router.getMatchedComponents() 9 | if (!matchedComponents.length) { 10 | return reject(new Error({ code: 404 })) 11 | } 12 | Promise.all(matchedComponents.map(Component => { 13 | if (Component.asyncData) { 14 | return Component.asyncData({ store, context }) 15 | } 16 | })) 17 | .then(() => { 18 | context.state = store.state 19 | resolve(app) 20 | }) 21 | .catch(reject) 22 | }, reject) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /ClientApp/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const fetchInitialMessages = ({commit}, origin) => { 4 | // this one will run on server so it need FQDN or server won't able to resolve the API address 5 | return axios.get(`${origin}/initialMessages`).then(response => { 6 | commit('INITIAL_MESSAGES', response.data) 7 | }).catch(err => { 8 | console.log(err) 9 | }) 10 | } 11 | 12 | export const fetchMessages = ({commit}, lastFetchedMessageDate) => { 13 | axios.post('/fetchMessages',{ 14 | lastMessageDate : lastFetchedMessageDate 15 | }) 16 | .then(response => { 17 | commit('FETCH_MESSAGES', response.data) 18 | }).catch(err => { 19 | console.log(err) 20 | }) 21 | } -------------------------------------------------------------------------------- /ClientApp/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { fetchInitialMessages, fetchMessages } from './actions' 4 | import minBy from 'lodash/minBy' 5 | 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | state: { messages: [], lastFetchedMessageDate: -1}, 10 | 11 | mutations: { 12 | INITIAL_MESSAGES: (state, payload) => { 13 | state.messages = payload.messages 14 | state.lastFetchedMessageDate = payload.lastFetchedMessageDate 15 | }, 16 | FETCH_MESSAGES: (state, payload) => { 17 | state.messages = state.messages.concat(payload.messages) 18 | state.lastFetchedMessageDate = minBy(state.messages, 'date').date 19 | } 20 | }, 21 | actions: { 22 | fetchInitialMessages, 23 | fetchMessages 24 | }, 25 | getters: { 26 | messages: state => state.messages, 27 | lastFetchedMessageDate: state => state.lastFetchedMessageDate 28 | } 29 | }) 30 | 31 | export default store 32 | -------------------------------------------------------------------------------- /Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Threading; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace vdn.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | public IActionResult Index() 14 | { 15 | return View(); 16 | } 17 | 18 | [Route("initialMessages")] 19 | public JsonResult initialMessages(){ 20 | var initialMessages = FakeMessageStore.FakeMessages 21 | .OrderByDescending(m => m.Date) 22 | .Take(2); 23 | 24 | var initialValues = new ClientState(){ 25 | Messages = initialMessages, 26 | LastFetchedMessageDate = initialMessages.Last().Date 27 | }; 28 | 29 | return Json(initialValues); 30 | } 31 | 32 | [HttpPost] 33 | [Route("fetchMessages")] 34 | public JsonResult FetchMessages([FromBody]FetchMessageRequest request){ 35 | var messages = FakeMessageStore.FakeMessages 36 | .OrderByDescending(m => m.Date) 37 | .Where(p => p.Date < request.lastMessageDate); 38 | 39 | if(messages.Any()){ 40 | var newMessages = messages.Take(2); 41 | return Json(new ClientState() 42 | { 43 | Messages = newMessages, 44 | LastFetchedMessageDate = newMessages.Last().Date 45 | }); 46 | } 47 | return Json(null); 48 | } 49 | } 50 | public class FetchMessageRequest { 51 | public DateTime lastMessageDate {get; set;} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ferry To 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Models/ClientState.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | public class ClientState { 5 | [JsonProperty(PropertyName = "messages")] 6 | public IEnumerable Messages {get; set;} 7 | 8 | [JsonProperty(PropertyName = "lastFetchedMessageDate")] 9 | public DateTime LastFetchedMessageDate {get; set;} 10 | } -------------------------------------------------------------------------------- /Models/FakeMessageStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System; 3 | 4 | public class FakeMessageStore { 5 | private static DateTime startDateTime = DateTime.Now; 6 | 7 | public static readonly List FakeMessages = new List(){ 8 | Message.CreateMessage("First message title", "First message text", startDateTime), 9 | Message.CreateMessage("2nd msg title", "2nd msg txt", startDateTime.AddDays(1)), 10 | Message.CreateMessage("3rd msg title", "3rd msg txt", startDateTime.AddDays(2)), 11 | Message.CreateMessage("4th msg title", "4th msg txt", startDateTime.AddDays(3)), 12 | Message.CreateMessage("5th msg title", "5th msg txt", startDateTime.AddDays(4)), 13 | Message.CreateMessage("6th msg title", "6th msg txt", startDateTime.AddDays(5)) 14 | }; 15 | } -------------------------------------------------------------------------------- /Models/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | public class Message{ 4 | [JsonProperty(PropertyName="date")] 5 | public DateTime Date {get; set;} 6 | 7 | [JsonProperty(PropertyName="title")] 8 | public string Title {get; set;} 9 | 10 | [JsonProperty(PropertyName="text")] 11 | public string Text {get; set;} 12 | 13 | private Message() {} 14 | 15 | public static Message CreateMessage(string title, string text, DateTime date){ 16 | return new Message(){ 17 | Title = title, 18 | Text = text, 19 | Date = date 20 | }; 21 | } 22 | } -------------------------------------------------------------------------------- /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 vdn 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | Update 2022/05/01: I was trying to figure out how to do SSR in .NET6 for Vue3 app, 4 | however it seems Microsoft has ditched the SPA / JS Service in favor of Blazor since .NETCore 3. 5 | Therefore it seems not possible to do SSR in ASP.NET for Vue anymore... -_- 6 | 7 | [[Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices](https://github.com/dotnet/AspNetCore/issues/12890) 8 | 9 | This repository was built from scratch following the steps described in the original blog post *[Server rendering Vue.js applications with ASP.NET Core](https://stu.ratcliffe.io/2017/07/20/vuejs-serverside-rendering-with-aspnet-core)* from Stu Ratcliffe. The aim of this repository is to adding more steps and comments from my experience following the steps that make it works as a supplement of the original blog post. I personally did not have any prior knowledge in VueJS nor modern web development frameworks. I were a C# developer and learn VueJS by my own from VueJS documentation and Stu Ratcliffe blog post. Hope this helps if you also read the same blog post and got stuck some way. :pray: :grinning: 10 | 11 | You can get the complete code repo made by **Stu Ratcliffe** from [[Here]](https://github.com/sturatcliffe/VueDotnetSSR) 12 | 13 | Once you get used to this repository, you may want to know how to do the same thing in Typescript. 14 | **@Kukks** has forked this repository and use Typescript instead. 15 | You can find his repository [[Here]](https://github.com/Kukks/vuejs-aspnetcore-ssr/tree/feature/langspecific) 16 | 17 | ## Install and run: 18 | dotnet restore 19 | npm install 20 | dotnet run 21 | 22 | if you see the following lines, than you are looking great! 23 | 24 | Now listening on: http://localhost:5000 25 | Application started. Press Ctrl+C to shut down. 26 | info: Microsoft.AspNetCore.NodeServices[0] 27 | webpack built c91dc3e2f186f013c53c in 3212ms 28 | 29 | ## Packages used: *The description may not accurate, just my understanding* 30 | - vue <- VueJS 31 | - lodash <- similiar to numpy in Python, utilies library for manipulating array/object. 32 | - axios <- Promise based HTTP client for the browser and Node.js, think of $.ajax() if you come from jQuery world 33 | - vuex <- Vue variant of Flux implementation, just like Redux in React 34 | - nprogress <- loading indiciator 35 | - vue-router <- enable client side "page" routing 36 | - vue-server-renderer <- enable Server Side Rendering 37 | - aspnet-prerendering <- enable ASP.NET to trigger Node for SSR 38 | 39 | - webpack <- pack your JavaScript files into bundles for faster loading, remove duplicate imports, reduce final code size 40 | - webpack-cli <- since webpack version 4 the command line tool placed in this package instead, install only if you want to pack the JavaScript code manually 41 | - webpack-merge <- merge webpack config so common configuration attributes can be shared among configurations 42 | - webpack-hot-middleware <- enable hot reload of code changes 43 | - aspnet-webpack <- enable ASP.NET to execute webpack on demand during runtime. 44 | - vue-loader <- required for packing vuejs code 45 | - css-loader <- required if you have css file to pack 46 | - style-loader <- required if you use template with style 47 | - json-loader <- requiredd if you need to pack json file 48 | - vue-template-compiler <- required for packing if you use template in vue components 49 | - babel-* <- transpile code syntax used in Vue into browser understandable version before packing 50 | 51 | ### Working with data: 52 | - .Net part 53 | 1. Message.cs 54 | 2. FakeMessageStore.cs 55 | 3. ClientState.cs 56 | - Vue part 57 | 0. (create ClientApp/vuex folder) 58 | 1. ClientApp/vuex/action.js 59 | 2. ClientApp/vuex/store.js 60 | 61 | ### Client side routing: 62 | 0. Move to ClientApp/components 63 | 1. Dashboard.vue 64 | 2. Messages.vue 65 | 3. Modify App.vue 66 | 4. create ClientApp/router folder 67 | 5. Move to ClientApp/router 68 | 6. index.js 69 | 7. Move back to ClientApp folder 70 | 8. Modify app.js 71 | 9. Move back to . 72 | 10. Modify Startup.cs 73 | 74 | ### Loading indicator: 75 | Here we try to modify the implementation order different from the original post. 76 | We are going to add the loading indicator before implementing the Server Side Rendering. 77 | To simulate timely API call form remote server, we add the following line in HomeController.cs: 78 | 79 | public JsonResult initialMessages(){ 80 | //Added to simulate initial loading from remote server 81 | Thread.Sleep(2000); 82 | ... 83 | } 84 | 85 | 1. Add nprogess in package.json dependency: 86 | 87 | "dependencies": { 88 | "vue": "^2.5.8", 89 | "vuex": "^3.0.1", 90 | "vue-router": "^3.0.1", 91 | "lodash": "^4.17.4", 92 | "axios": "^0.17.1", 93 | "nprogress": "^0.2.0" 94 | } 95 | 96 | 2. Add style-loader and css-loader to webpack.config: 97 | 98 | { 99 | test: /\.css$/, 100 | loader: "style-loader!css-loader" 101 | } 102 | 103 | 3. Modify `ClientApp/vuex/actions.js`, add `NProgress.start()` and `NProgress.done()` before and after axios remote call. 104 | 105 | ### Server Side Renderering (SSR) 106 | 107 | 1. Add the following dependencies to package.json: 108 | 109 | - devDependencies: 110 | - aspnet-webpack [Replaced by Microsoft.AspNetCore.SpaServices.Extensions] 111 | - webpack-merge 112 | - dependencies:x 113 | - vue-server-renderer 114 | - aspnet-prerenderer 115 | 116 | 3. Split the code into two part: 117 | 118 | 1. `server.js` <- this will load by renderOnServer.js, which aspnet-prerendering will trigger Node to execute and return pre-rendered result back to renderOnServer.js and thus send to browser as initial state of app. 119 | 2. `client.js` <- once initial app state rendered and injected in the resulting index.cshtml, the script tag will load client.js and mount it to pre-rendered app tag. 120 | 121 | 4. Create Node server code for ASP.NET Core to trigger the Node hosting service to execute 122 | 123 | - `rendererOnServer.js` <- responsible for loading the webpacked server.js for Node to render the initial app state. 124 | 125 | 5. ASP.NET Core part 126 | 127 | - Edit `Views/Home/index.cshtml` app tag to use `aspnet-prerendering` attributes 128 | 129 | 6. Webpack Configuration 130 | 131 | - Edit `webpack.config.js`, make use of `webpack-merge` to split the original configuration into two sets. 132 | 133 | ### Thoughts: 134 | SSR was by far the most difficult part of my VueJS journey, it takes more than half of the time of my VueJS learning. Whether to use Server Side Rendering or not is highly optional, you don't need it to write a cool SPA. The performance and user experience gain is arguablely worth the complexity and develop time involved. 135 | 136 | ### BootstrapVue: 137 | Bootstrap is a very popular library for beautiful and simple UI components and styles. 138 | Using Bootstrap in VueJS application is easy with BootstrapVue (it seems bootstrap is not required in package.json to use bootstrap-vue, installing bootstrap-vue install bootstrap as well): 139 | 140 | - Install: `npm i bootstrap-vue` 141 | - Import into app.js: `import BootstrapVue from 'bootstrap-vue' 142 | - Import the css files: (tricky here, for this repo I need to add the imports at client.js instead of app.js) 143 | import 'bootstrap/dist/css/bootstrap.css' 144 | import 'boostrap-vue/dist/bootstrap-vue.css' 145 | - Add the Bootstrap components (e.g. I added a badge at Dashboard.vue template.) 146 | 147 | ### Prevent XSS Attack: 148 | During the journey in solving the asp-prerendering v3.0.0+ dependency issue, I found an article talking about Cross-site scripting attack in JavaScript applications: *[The Most Common XSS Vulnerability in React.js Applications](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0)* And turns out rednerOnServer.js also has such vulnerability. 149 | 150 | module.exports = prerendering.createServerRenderer(function (params) { 151 | return new Promise( 152 | function (resolve, reject) { 153 | const context = { 154 | url: params.url, 155 | xss:"" 156 | } 157 | resolve({ 158 | globals: { 159 | __INITIAL_STATE__: context 160 | } 161 | }) 162 | }) 163 | }); 164 | 165 | If we modify the renderOnServer.js as above, an alert will be shown when we load the page from browser. This will potentially enable attacker to execute arbitary code. To fix this vulnerability, we can make use of `serialize-javascript` package from Yahoo engineers and cleanse all initial state assignment from user input: 166 | 167 | npm install --save serialize-javascript 168 | 169 | and serialize the initial state like this: 170 | 171 | //prevent XSS attack when initialize state 172 | var serialize = require('serialize-javascript') 173 | 174 | module.exports = prerendering.createServerRenderer(function (params) { 175 | return new Promise( 176 | function (resolve, reject) { 177 | const context = { 178 | url: params.url, 179 | xss: serialize("") 180 | } 181 | resolve({ 182 | globals: { 183 | __INITIAL_STATE__: context 184 | } 185 | }) 186 | }) 187 | }); 188 | 189 | and when you inspect the HTML from browser you will see the tags are escaped: 190 | 191 | window.__INITIAL_STATE__ = {"url":"/","xss":"\"\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('Possible XSS vulnerability from user input!')\\u003C\\u002Fscript\\u003E\""}; 192 | 193 | Cheers. :smirk: 194 | 195 | ### Reference 196 | *[Building Single Page Applications on ASP.NET Core with JavaScriptServices](https://blogs.msdn.microsoft.com/webdev/2017/02/14/building-single-page-applications-on-asp-net-core-with-javascriptservices/)* 197 | *[Use JavaScriptServices to Create Single Page Applications in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/client-side/spa-services)* 198 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.AspNetCore.SpaServices.Webpack; 10 | using Microsoft.Extensions.Logging; 11 | namespace vdn 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddMvc(); 26 | // services.AddNodeServices(); 27 | } 28 | 29 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 30 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory) 31 | { 32 | // factory.AddConsole(); 33 | 34 | if (env.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | app.UseWebpackDevMiddleware( new WebpackDevMiddlewareOptions { 38 | HotModuleReplacement = true 39 | }); 40 | } 41 | else 42 | { 43 | app.UseExceptionHandler("/Home/Error"); 44 | } 45 | 46 | app.UseStaticFiles(); 47 | 48 | app.UseMvc(routes => 49 | { 50 | routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index"}); 51 | 52 | routes.MapRoute( 53 | name: "default", 54 | template: "{controller=Home}/{action=Index}/{id?}"); 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 
2 | -------------------------------------------------------------------------------- /Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" 2 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices" -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.before_client_side_routing.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ferry To", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": { 13 | "vue": "^2.5.8", 14 | "vuex": "^3.0.1", 15 | "lodash": "^4.17.4", 16 | "axios": "^0.17.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.26.0", 20 | "babel-loader": "^7.1.2", 21 | "babel-preset-es2015": "^6.24.1", 22 | "babel-preset-stage-2": "^6.24.1", 23 | "vue-loader": "^13.5.0", 24 | "vue-template-compiler": "^2.5.8", 25 | "css-loader": "^0.28.7", 26 | "aspnet-webpack": "^2.0.1", 27 | "webpack": "^3.8.1", 28 | "webpack-hot-middleware": "^2.20.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.before_hot_reload.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ferry To", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": { 13 | "vue": "^2.5.8" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.26.0", 17 | "babel-loader": "^7.1.2", 18 | "babel-preset-es2015": "^6.24.1", 19 | "babel-preset-stage-2": "^6.24.1", 20 | "vue-loader": "^13.5.0", 21 | "vue-template-compiler": "^2.5.8", 22 | "css-loader": "^0.28.7", 23 | "webpack": "^3.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.before_loading_indicator.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ferry To", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": { 13 | "vue": "^2.5.8", 14 | "vuex": "^3.0.1", 15 | "vue-router": "^3.0.1", 16 | "lodash": "^4.17.4", 17 | "axios": "^0.17.1" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.26.0", 21 | "babel-loader": "^7.1.2", 22 | "babel-preset-es2015": "^6.24.1", 23 | "babel-preset-stage-2": "^6.24.1", 24 | "vue-loader": "^13.5.0", 25 | "vue-template-compiler": "^2.5.8", 26 | "css-loader": "^0.28.7", 27 | "aspnet-webpack": "^2.0.1", 28 | "webpack": "^3.8.1", 29 | "webpack-hot-middleware": "^2.20.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.before_server_side_rendering.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ferry To", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": { 13 | "vue": "^2.5.8", 14 | "vuex": "^3.0.1", 15 | "vue-router": "^3.0.1", 16 | "lodash": "^4.17.4", 17 | "axios": "^0.17.1", 18 | "nprogress": "^0.2.0" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.26.0", 22 | "babel-loader": "^7.1.2", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-stage-2": "^6.24.1", 25 | "vue-loader": "^13.5.0", 26 | "vue-template-compiler": "^2.5.8", 27 | "css-loader": "^0.28.7", 28 | "style-loader": "^0.19.0", 29 | "aspnet-webpack": "^2.0.1", 30 | "webpack": "^3.8.1", 31 | "webpack-hot-middleware": "^2.20.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Ferry To", 10 | "license": "MIT", 11 | "private": true, 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "aspnet-prerendering": "3.0.1", 15 | "aspnet-webpack": "^3.0.0", 16 | "axios": "^1.3.4", 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^9.1.2", 19 | "babel-preset-es2015": "^6.24.1", 20 | "babel-preset-stage-2": "^6.24.1", 21 | "bootstrap-vue": "^2.0.0-rc.11", 22 | "css-loader": "^7.1.2", 23 | "json-loader": "^0.5.7", 24 | "lodash": "^4.17.11", 25 | "nprogress": "^0.2.0", 26 | "serialize-javascript": "^6.0.0", 27 | "style-loader": "^4.0.0", 28 | "vue": "^2.6.10", 29 | "vue-loader": "^17.0.0", 30 | "vue-router": "^4.0.14", 31 | "vue-server-renderer": "^2.6.10", 32 | "vue-template-compiler": "^2.6.10", 33 | "vuex": "^4.0.2", 34 | "webpack": "^5.47.1", 35 | "webpack-cli": "^5.1.4", 36 | "webpack-dev-middleware": "^7.3.0", 37 | "webpack-hot-middleware": "^2.24.4", 38 | "webpack-merge": "^6.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /vdn.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | // using webpack-merge so we don't have to repeat common configuration attributes twice 5 | const merge = require('webpack-merge') 6 | 7 | module.exports = (env) => { 8 | const sharedConfig = () => ({ 9 | mode: "development", 10 | stats: { modules: false }, 11 | resolve: { extensions: ['.js', '.vue'] }, 12 | output: { 13 | filename: '[name].js', 14 | publicPath: '/dist/' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.vue$/, 20 | loader: 'vue-loader' 21 | }, 22 | { 23 | test: /\.js$/, 24 | loader: 'babel-loader', 25 | include: __dirname, 26 | exclude: file => ( 27 | /node_modules/.test(file) && 28 | !/\.vue\.js/.test(file) 29 | ) 30 | }, 31 | { 32 | test: /\.css$/, 33 | oneOf: [ 34 | // this matches `