Integrate Angular and Webpack in an ASP.NET MVC Ap

2020-02-26 00:10发布

问题:


Want to improve this question? Update the question so it's on-topic for Stack Overflow.

Closed 2 years ago.

I am looking for a write up with steps i can follow to add Angular to an existing MVC application that lives in an area of my solution. I have found a write up that shows me how to add angular using a gulp.js file that pulls the necessary node_modules and transpiles my ts files into js files. I would like to do the same exact thing using webpack instead.

Currently my package.json looks like this.

{
    "version": "1.0.0",
    "name": "aspnet",
    "private": true,
    "scripts": {},
    "dependencies": {
        "@angular/animations": "4.3.5",
        "@angular/common": "4.3.5",
        "@angular/compiler": "4.3.5",
        "@angular/compiler-cli": "4.3.5",
        "@angular/core": "4.3.5",
        "@angular/forms": "4.3.5",
        "@angular/http": "4.3.5",
        "@angular/platform-browser": "4.3.5",
        "@angular/platform-browser-dynamic": "4.3.5",
        "@angular/platform-server": "4.3.5",
        "@angular/router": "4.3.5",
        "@angular/upgrade": "4.3.5",
        "angular-in-memory-web-api": "0.3.2",
        "bootstrap": "3.3.7",
        "core-js": "2.5.0",
        "ie-shim": "0.1.0",
        "rxjs": "5.4.3",
        "zone.js": "0.8.16",
        "systemjs": "^0.20.18"
    },
    "devDependencies": {
        "gulp": "^3.9.1",
        "gulp-clean": "^0.3.2",
        "gulp-concat": "^2.6.1",
        "gulp-tsc": "~1.3.2",
        "gulp-typescript": "^3.2.2",
        "path": "^0.12.7",
        "typescript": "^2.4.2"
    }
}

This is the write up I followed that uses gulp and this works.

But i would much rather prefer using webpack instead of a gulp task.

回答1:

I hope that can help you find the idea because our projects also need to integrate webpack and ASP.NET MVC together. Noted that it is my own proposal so that there might be a better way to do it. Below is what we do.

Check the source code on Github

1. Structure the project

We separated our project into two folders: Client and Server. Those will be located in mvc5-angular-webpack folder and this folder will be committed to the repository

mvc5-angular-webpack/
├── Server/
│   ├── WebApplication/
│   │     ├── Controllers/
│   │     ├── Scripts/
│   │     ├── Web.config
│   │     ├── Many more folder and file...
│   │        
│   └── Web-Core.sln
│   
├── Client/
    ├── modules
    │     ├── angularModule-1/
    │     │      ├── main.ts
    │     │      ├── app.modules.ts
    │     │      ├── app.component.ts
    │     │      ├── Many more file...
    │     │
    │     ├── angularModule-2/
    │     │      ├── main.ts
    │     │      ├── app.modules.ts
    │     │      ├── app.component.ts
    │     │      ├── Many more file...
    │     ├── polyfill.ts
    │     ├── vendor.ts
    │
    └── build.bat
    └── npm-shrinkwrap.json
    └── package.json
    └── tsconfig.json
    └── tslint.json
    └── webpack.config.js
  • In the Server folder, we added the MVC solution named Web-Core.sln and all the common library project is written in C#.
  • In the Client folder only contains front-end related stuff. To build the front project, simply call the build.bat. I will talk about this file content later. Inside modules folder, our project will create each subfolder for each module.

Our website has some module still using server-side rendering with pure Razor. And there is some module written in client-side code with AngularJS and Angular.

2. Configure webpack

Assume that you configured all the typescript and npm already. Let see what I have inside webpack.config.js

const webpack = require('webpack')
const path = require('path')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const entryPath = path.resolve(__dirname, 'modules')
const corePath = path.resolve(__dirname, '../Server/WebApplication/Scripts/ng2')
const module1 = `${entryPath}/angularModule-1`
const module2 = `${entryPath}/angularModule-2`

module.exports = (envOptions) => {
    envOptions = envOptions || {};

    const config = {
        entry: {
            'polyfills': `${entryPath}/polyfill.ts`,
            'vendors': `${entryPath}/vendor.ts`,
            'module1': `${module1}/main.ts`,
            'module2': `${module2}/main.ts`            
        },
        output: {
            path: corePath,
            filename: '[name].js',
            sourceMapFilename: "[name].js.map"            
        },
        resolve: {
            extensions: ['.ts', '.js', '.html']
        },
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    loaders: ['awesome-typescript-loader', 'angular2-template-loader']
                },
                {
                    test: /\.html$/,
                    loader: 'raw-loader'
                },
                {
                    test: /\.css$/,
                    loader: 'raw-loader'
                }
            ]
        },
        devtool: 'source-map',
        plugins: [
            new webpack.NoEmitOnErrorsPlugin(),
            new webpack.optimize.CommonsChunkPlugin({
                name: ['vendors', 'polyfills']
            })
        ]
    }

    if (envOptions.MODE === 'prod') {
        config.plugins.push(
            new UglifyJsPlugin()
        )
    }

    return config;
}

So basically it will try to resolve the directory in an upper level and put all the compiled files to Scripts/ng2 inside Server folder.

3. Configure npm script

After configured webpack, we will basically add some more script to run during the build process. Add the following code to package.json file

"scripts": {
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "dev": "webpack-dev-server --https --open",
    "watch": "webpack --config webpack.config.js --watch",
    "build": "webpack --config webpack.config.js",
    "build:html": "webpack --config webpack-html-plugin.config.js --env.MODE=prod",
    "build:prod": "webpack --config webpack.config.js --env.MODE=prod"    
}

4. Configure build.bat

At the beginning of Angular 2 integration, we have created an empty web application project for front-end purpose and added this project as a dependency of our WebApplication. But our backend team later complained about how slow the front-end process take. Because they don't need the front-end project to build every time the WebApplication is being built.

The idea of the build.bat file is to manually run it to get the latest version of front-end code on their machine. Not every single time they run the project.

call npm install --production  --loglevel verbose
echo "Build Angular projects"
npm run build:prod

The call is to continue because some of the commands abort the command line. Refer here

The script here is very simple. First, we run the npm install to restore all the necessary dependency. Then call build:prod as we defined on package.json before. Webpack will take care of bundle our Typescript code into three big JavaScript files as vendors.js, polyfills.js and module1.js.

Our team used Jenkins for deployment, so our dev ops just need to include to run the build.bat and we are all set. If you want to run it everytime your project was built, you can set it inside the pre-build or post-build event.

Image source

5. Reference compiled JavaScript file in the view.

Normally we will return only one view in an area as below. The my-angular-app is what we defined in app.component.ts

Index.cshtml

<script src="~/Scripts/ng2/polyfills.js"></script>
<script src="~/Scripts/ng2/vendors.js"></script>
<script src="~/Scripts/ng2/module1.js"></script>

<my-angular-app>
    Loading...
</my-angular-app>

HomeController.cs

public ActionResult Module1()
{
    return View();
}

That is a bit drawback If we deploy to production. Because after the compiled JavaScript was updated, the browser sometimes still keep the old version of the file because of caching. We should have a mechanism to provide a unique name for the files after deployment. There are 3 options for us to do so.

i. html-webpack-plugin

If we work with pure front-end project, webpack provides html-webpack-plugin to take care of it as below. Technically, It will automatically inject the JavaScript file into our view with the unique id.

webpack-html-webpack-plugin.config.js

...
const HtmlWebpackPlugin = require('html-webpack-plugin');
const viewPath = path.resolve(
    __dirname,
    "../Server/WebApplication/Views/Home"
  );
...
entry: {
    polyfills: `${entryPath}/polyfill.ts`,
    vendors: `${entryPath}/vendor.ts`,
    module1: `${module1}/main.ts`      
},
output: {
    path: corePath,
    filename: "[name].[hash].js",
    sourceMapFilename: "[name].[hash].js.map"
}
....
plugins: [,
    ...,
    new HtmlWebpackPlugin({
        template: viewPath + "/loader.cshtml",
        filename: viewPath + "/Module1.cshtml",
        inject: false
    })
]

And in the same folder with the designated view, we created a cshtml file named loader.cshtml

loader.cshtml

<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>  
    <script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>  
<% } %> 

<my-angular-app>
    Loading...
</my-angular-app>

And run the npm run build:html as defined on package.json. If it runs successfully, the result will look like that.

@{
    ViewBag.Title = "Module 1";
}
<script src="../../Scripts/ng2/vendors.1470de344a0f2260b700.js"></script>
<script src="../../Scripts/ng2/vendors.1470de344a0f2260b700.js"></script>
<script src="../../Scripts/ng2/module1.1470de344a0f2260b700.js"></script>

<my-angular-app>Loading....</my-angular-app>

ii. Defined your own JavaScript version

In our ASP.NET MVC project is a bit different because we used more than one Angular app. So that we defined a version in the class and append it at the end of the file when loading the JS. By doing it, we will make sure the latest will be loaded into the browser. But it is out of this question context so I will not go further. Basically, It is gonna look like below code.

<script src="@string.Format("{0}?v={1}", "~/Scripts/ng2/polyfills.js", VersionHelper.CurrentVersion())</script>
<script src="@string.Format("{0}?v={1}", "~/Scripts/ng2/vendors.js", VersionHelper.CurrentVersion())</script>
<script src="@string.Format("{0}?v={1}", "~/Scripts/ng2/module1.js", VersionHelper.CurrentVersion())</script>

When serving it on browser, it will look like

<script src="~/Scripts/ng2/polyfills.js?v=1.1.0"></script>
<script src="~/Scripts/ng2/vendors.js?v=1.1.0"></script>
<script src="~/Scripts/ng2/module1.js?v=1.1.0"></script>

iii. ASP.NET Bundling

At Zyllem, our team didn't use it because of our JavaScript files is configure inside the model and render it later to the view.

You can just open App_Start\BundleConfig.cs in your project and config a bundle. Let say the name is module1

bundles.Add(new ScriptBundle("~/bundles/module1").Include(
                "~/Scripts/ng2/polyfills.js",
                "~/Scripts/ng2/vendors.js"
                "~/Scripts/ng2/module1.js"));

And render inside the view by doing it.

Index.cshtml

@Scripts.Render("~/bundles/module1")

So that when serving on the browser, it will have the unique token at the end and it is different If you make any changes in the script bundle.

<script src="/bundles/module1?v=2PbUw0z_gh_Ocp_Vz13rdmmBSG6utLJAXm2lThxYAow1"></script>

If everything works fine, you will see the below screenshots.


Update

Add the repository on Github https://github.com/trungk18/mvc5-angular-webpack