├── .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 |
2 |
3 |
Hello from Vue#!!!#
4 | Dashboard
5 | Message
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ClientApp/components/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Dashboard
4 | Hello Bootstrap
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ClientApp/components/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ message.title }}
4 |
{{ message.text }}
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/ClientApp/components/Messages.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 `