ASP.NET Core 2.0 with Isomorphic Vue.js and TypeScript

ASP.NET Core 2.0 with Isomorphic Vue.js and TypeScript

NOTE: This article assumes intermediate knowledge of ASP.NET Core, Webpack, TypeScript, and Yarn/NPM.

ASP.NET Core 2.0 was recently released and it has some exciting features and improvements. Combined with client-side javascript framework Vue.js and the static/typed javascript-superset language TypeScript, you can create some powerful web applications. Putting all of these frameworks together can seem daunting, especially since Vue.js is the new kid on the trendy block, but is easier than it looks. We won’t get into the details of how to use any of the specific frameworks. There are tutorials all over for that. We will stick with how to set up and configure the project so you can get to building your next project in no time. This project was created in Linux but everything we do will be cross-platform and available on OS X and Windows.

This article is based on several fantastic blog articles and projects:

 


I don’t care! Just show me the code!

https://github.com/AustinWinstanley/asp-net-core-2-vue-typescript

For those like me who want to get straight to it, the repository can be cloned and easily run yourself. This project includes settings for VS Code but can easily be used in your preferred editor.

 


The repository/code structure and breakdown.

The layout of the code is simple and includes the basic files needed for an ASP.NET 2.0 project, as well as editor/source control files and configuration scripts. For beginners, it may look intimidating (why do I need so many files?!) but A) you don’t really, several of these are optional and that will be pointed out and B) it is simpler than it looks at first glance. Stay with me a little longer and we’ll walk through each.

 

| asp-net-core-2-vue-typescript                  // OPTIONAL. The project folder.
  | .vscode                                      // OPTIONAL. VS Code editor folder.
    - launch.json                                // OPTIONAL. VS Code debug/launch configuration.
    - settings.json                              // OPTIONAL. VS Code editor settings configuration.
    - tasks.json                                 // OPTIONAL. VS Code build task configuration.
  | Client                                       // Main client-side application folder.
    | components                                 // Vue.js components to be included in the application.
      - App.vue                                  // Main Vue.js component to be loaded.
    - app.ts                                     // Vue.js application file to load your client-side code.
    - vue-shims.d.ts                             // Tells Vue.js where to find your .vue files.
  | Controllers                                  // ASP.NET Core controllers that map page requests to views.
    - HomeController.cs                          // Controller for the initial page view.
  | Views                                        // ASP.NET Core views folder where the page view code resides.
    | Home                                       // Home view folder.
      - Index.cshtml                             // Home view file to be shown on application load where the Vue.js app will be loaded.
  | wwwroot                                      // Public assets directory to which the Vue.js application is compiled.
    - dist                                       // COMPILED. Distribution directory.
      - app.js                                   // COMPILED. Compiled Vue.js application.
  - .babelrc                                     // Configuration for Babel javascript compilation.
  - .editorconfig                                // OPTIONAL. Editor-agnostic configuration file.
  - .gitignore                                   // OPTIONAL. Source control (Git) ignore settings.
  - appsettings.Development.json                 // OPTIONAL. Development application environment settings.
  - appsettings.json                             // OPTIONAL. Application environment settings.
  - AspNetCore2VueTypeScript.csproj              // ASP.NET Core project file.
  - package.json                                 // Yarn/NPM configuration file.
  - Program.cs                                   // ASP.NET Core entrypoint file.
  - Startup.cs                                   // ASP.NET Core startup file.
  - tsconfig.json                                // TypeScript configuration settings.
  - tslint.json                                  // TypeScript lint/style settings.
  - webpack.config.js                            // Webpack configuration settings.


Let’s get started!

For the purposes of this post, to focus on the project setup rather than the technologies, I’m assuming you have already installed .Net Core 2.0 and Yarn or NPM. This will also be a brief overview, rather than getting into the details of every file. Refer to the code repository to see how everything works in practice.

The first thing we’ll do is create a new project in the terminal.

# Create the project.
dotnet new web --name FooProject --output foo-project
# Change directories into our new project folder.
cd foo-project
# Open VS Code or your preferred editor.
code .

Now that we’ve created our project, we need to add Microsoft.AspNetCore.SpaServices.Webpack which is the ASP.NET Core library that will compile and serve our application server-side (isomorphic). Open your FooProject.csproj file and add the dependency under <ItemGroup>. Once complete, it will look something like this:

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
         <TargetFramework>netcoreapp2.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <FolderInclude="wwwroot\"/>
    </ItemGroup>
    <ItemGroup>
        <PackageReferenceInclude="Microsoft.AspNetCore.All"Version="2.0.0"/>
        <PackageReferenceInclude="Microsoft.AspNetCore.SpaServices"Version="2.0.0"/>
    </ItemGroup>
</Project>
Restore your packages in the dotnet-cli to install the new dependency and let’s kick off our first build.
# Install the new dependency.
dotnet restore
# Build the project.
dotnet build

Which, if all goes well, will give you a successful result:

austin@AUSTIN-HOME:~/projects/foo-project$ dotnet restore
 Restoring packages for /home/austin/projects/foo-project/FooProject.csproj...
 Restore completed in 2.52 sec for /home/austin/projects/foo-project/FooProject.csproj.


austin@AUSTIN-HOME:~/projects/foo-project$ dotnet build
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

FooProject -> /home/austin/projects/test/foo-project/bin/Debug/netcoreapp2.0/FooProject.dll

Build succeeded.
 0 Warning(s)
 0 Error(s)

Time Elapsed 00:00:05.36

Build succeeded! Woo hoo!

Open your Startup.cs file and add support for MVC and WebPack hot module replacement, which will allow us to build our Vue.js project automatically on page load in the development environment. We’ll also add support for environment configuration while we’re at it.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
        {
            HotModuleReplacement = true
        });
    } else {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
    });
}

We are telling our application to user WebPack middleware and allow hot module replacement to reload our Vue.js application on change.

Now we need to set up our TypeScript, WebPack, and Vue.js files.

First create a tsconfig.json file in the root of the project to set our TypeScript settings and add our settings:

{
    "compilerOptions": {
        // Redirect output structure to the directory.
        "outDir": "./wwwroot/dist/",
        // Generates corresponding .map file.
        "sourceMap": true,
        // Enable all strict type checking options.
        // --noImplicitAny, --noImplicitThis, --alwaysStrict and --strictNullChecks
        "strict": true,
        // Specify module code generation.
        "module": "es2015",
        // Determine how modules get resolved.
        "moduleResolution": "node",
        // Specify ECMAScript target version.
        "target": "es5",
        // Emit design-type metadata for decorated declarations in source.
        "emitDecoratorMetadata": true,
        // Do not transpile each file as a separate module.
        "isolatedModules": false,
        // Enable experimental support for class decorators.
        "experimentalDecorators": true,
        // Remove all comments except copy-right header comments beginning with /*!.
        "removeComments": true,
        // Suppress --noImplicitAny errors for indexing objects lacking index signatures.
        "suppressImplicitAnyIndexErrors": true,
        // Allow default imports from modules with no default export.
        "allowSyntheticDefaultImports": true,
        // List of library files to be included in the compilation.
        "lib": [
          "dom",
          "es5",
          "es2015",
          "es2015.promise"
        ]
    },
    // Files/Folders to include in compilation.
    "include": [
        "./Client/**/*"
    ],
    // Files/Folders to exclude in compilation.
    "exclude":[
      "node_modules"
    ]
}

And now the tslint.json to define how our TypeScript code should look. Feel free to alter these settings to work the way you work.

{
    // The severity level used when a rule specifies a default warning level.
    "defaultSeverity": "error",
    // The name of a built-in configuration preset.
    "extends": [
        "tslint:recommended"
    ],
    // These rules are applied to .js and .jsx files.
    "jsRules": {},
    // A map of rule names to their configuration settings.
    "rules": {
        "quotemark": [
            true,
            "single"
        ],
        "indent": [
            true
        ],
        "interface-name": [
            false
        ],
        "arrow-parens": false,
        // Pending fix for shorthand property names.
        "object-literal-sort-keys": false
    },
    // A path to a directory or an array of paths to directories of custom rules.
    "rulesDirectory": []
}

Finally, we create the webpack.config.js file to configure the WebPack build:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  // The entry point for the bundle.
  entry: {
    'client': './Client/app.ts'
  },
  // Options affecting the output of the compilation.
  output: {
    // The output directory as an absolute path (required).
    path: path.resolve(__dirname, './wwwroot/dist'),
    // The publicPath specifies the public URL address of the output files when referenced in a browser.
    publicPath: '/dist/',
    // Specifies the name of each output file on disk.
    filename: 'app.js'
  },
  // Options affecting the normal modules.
  module: {
    // An array of Rules which are matched to requests when modules are created.
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
            // the "scss" and "sass" values for the lang attribute to the right configs here.
            // other preprocessors should work out of the box, no loader config like this necessary.
            'scss': 'vue-style-loader!css-loader!sass-loader',
            'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
          }
          // Other vue-loader options here.
        }
      },
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          appendTsSuffixTo: [/\.vue$/],
        }
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[ext]?[hash]'
        }
      }
    ]
  },
  // Options affecting the resolving of modules.
  resolve: {
    // An array of extensions that should be used to resolve modules.
    extensions: ['.ts', '.js', '.vue', '.json'],
    // Replace modules with other modules or paths.
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  // Can be used to configure the behaviour of webpack-dev-server when the webpack config is passed to webpack-dev-server CLI.
  devServer: {
    // Access dev server from arbitrary url.
    historyApiFallback: true,
    // With noInfo enabled, messages like the webpack bundle information that is shown when starting up and after each save, will be hidden. Errors and warnings will still be shown.
    noInfo: true
  },
  // These options allows you to control how webpack notifies you of assets and entrypoints that exceed a specific file limit.
  performance: {
    // Turns hints on/off. In addition, tells webpack to throw either an error or a warning when hints are found.
    hints: false
  },
  // Choose a developer tool to enhance debugging.
  devtool: '#eval-source-map'
}

// Rules to add/alter when used in a production environment.
if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'

  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

Now that we have the build configuration set up, we can move on to implementing our client and pages.

Open /Views/Home/Index.cshtml
and add your app container and compiled Vue.js app reference:

@{
ViewData["Title"] = "Home Page";
}

<!-- Define the App container. -->
<div id="app"></div>

<!-- Include the compiled Vue.js. -->
<script src="~/dist/app.js" asp-append-version="true"></script>

Finally, to the fun part! Let’s add our client code in the /Client/ folder of the project root.

Create an app.ts file:

import Vue from 'vue'
import App from './components/App.vue'

// Mount the Vue.js application.
new Vue({
  el: '#app',
  render: h => h(App, {
    props: {
      propMessage: 'World'
    }
  })
})

Now create a shim file in vue-shim.d.ts:

declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}

Create a /Client/components/ folder to hold our Vue.js components and add our main App component in a file called App.vue:

<!-- Client/components/App.vue -->
<template>
  <div>
    <input v-model="msg">
    <p>prop: {{propMessage}}</p>
    <p>msg: {{msg}}</p>
    <p>helloMsg: {{helloMsg}}</p>
    <p>computed msg: {{computedMsg}}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script lang="ts">
// Import the core Vue.js dependency.
import Vue from 'vue'
// Import the Vue.js class component.
import Component from 'vue-class-component'

@Component({
  props: {
    propMessage: String
  }
})
export default class App extends Vue {
  // Define properties.
  propMessage: string

  // Inital data.
  msg: number = 123

  // Use prop values for initial data.
  helloMsg: string = 'Hello, ' + this.propMessage

  // Lifecycle hook.
  mounted () {
    this.greet()
  }

  // Computed message function.
  get computedMsg () {
    return 'computed ' + this.msg
  }

  // Greet message function.
  greet () {
    alert('greeting: ' + this.msg)
  }
}
</script>

Run webpack now from your root and it will build the Vue.js app and when you run the project, you should have a working, server-side rendered Vue.js application.

I know a lot has been introduced quickly here, and there are a lot of moving parts, but review the code in the repository and everything will make sense when put together.

And that’s all there is to it! Enjoy!

Leave a Reply

%d bloggers like this: