📜 ⬆️ ⬇️

Setting up a Webpack 3 + Angular 4 development environment: from complex to simple

Hello!

A modern Angular front-end application should include the following features:


There are many assembly options that solve these problems (angular cli, A2 seed, etc.). Usually they have a complex structure, poorly configured / expanded, and are a monolith that cannot be changed.
')
In the article I will tell you how to combine Angular 2+ with a webpack and deal with all the stages of assembly / development.

You will be surprised how simple it is.
The final application .

I will try to highlight the most subtle moments. So let's go.

1) Create a project


Create a folder with the project, so that no one guesses, let's call it angular-project .
(I use Webstorm , however you can do the same in your editor)



2) Environment


Install node.js ( npm bundled by default).

We create package.json , of course, the number of modules connected to the project potentially tends to infinity, but I will leave only the necessary, in my opinion, for full development. There are many modules, I will try to justify why they are needed.

package.json
{ "name": "angular-project", "version": "1.0.0", "description": "angular scaffolding", "author": "maxim1006", "license": "MIT", "dependencies": { //   Angular  "@angular/animations": "^4.3.6", "@angular/common": "^4.3.6", "@angular/compiler": "^4.3.6", "@angular/compiler-cli": "^4.3.6", "@angular/core": "^4.3.6", "@angular/forms": "^4.3.6", "@angular/http": "^4.3.6", "@angular/platform-browser": "^4.3.6", "@angular/platform-browser-dynamic": "^4.3.6", "@angular/router": "^4.3.6", //  hmr "@angularclass/hmr": "^2.1.1", "@angularclass/hmr-loader": "^3.0.2", //polyfills  es5 "core-js": "^2.5.0", //      "reflect-metadata": "^0.1.8", //      "rxjs": "^5.4.3", //  .   js "typescript": "2.3.4", //  js,  ,   "zone.js": "^0.8.17" }, "devDependencies": { //  AoT (Ahead-of-Time Compilation) angular "@ngtools/webpack": "^1.6.2", // ,    typescript "@types/es6-shim": "^0.31.35", "@types/jasmine": "^2.5.54", "@types/node": "^7.0.43", //routing   "angular-router-loader": "^0.6.0", //         "angular2-template-loader": "^0.6.2", //     css "autoprefixer": "^6.3.7", //   typescript  webpack "awesome-typescript-loader": "^3.2.3", //    / "copy-webpack-plugin": "^4.0.1", //   css "css-loader": "^0.28.5", "css-to-string-loader": "^0.1.2", //es6 polyfills "es6-shim": "^0.35.1", //   "hammerjs": "^2.0.8", // webpack   html "html-webpack-plugin": "^2.29.0", //       "less": "^2.7.2", "less-loader": "^4.0.3", //      "on-build-webpack": "^0.1.0", //   webpack   "raw-loader": "^0.5.1", //    "postcss-loader": "^1.3.3", "style-loader": "^0.17.0", // "tslint": "^5.7.0", //  -  "rimraf": "^2.6.1", //    css   base64 "url-loader": "^0.5.8", //webpack "webpack": "^3.5.5", //   express  "webpack-dev-server": "^2.7.1" }, //        npm run __command__ ( npm run serve)  ) "scripts": { // .              ,     .    .    -   .      ,     (   . .),  ( --profile);  ,      webpack   ,  (--watch);       ,    ( –-progress). "serve": "webpack-dev-server --config ./webpack.config.js --profile --watch --progress", // ,   serve,     "hmr": "webpack-dev-server --config ./webpack.config.js --profile --watch --progress", // prod     "prod": "npm run aot", //      prod,    "prodServer": "webpack-dev-server --config ./webpack.config.js --open", // ./dist    "clean": "rimraf ./dist", //,   webpack.js ,    aot.   ,    . "aot": "webpack", //   "test": "karma start" } } 


3) Installation of modules


Through the terminal, go to the folder where package.json is located, and enter the npm i command.


4) Installing global modules


Since we use the rimraf, webpack and webpack-dev-server commands in the terminal, we will have to explain them to your PC using the npm i rimraf webpack command webpack-dev-server -g

After these manipulations, our project was replenished with the node_modules folder .




5) README.md


Create a README.md , where, in addition to the link to this article, you can add features for the development of your project.


6) Linter


Create a tslint.json , I will not stop here, since there is no silver bullet.

tslint.json
 { "rules": { "no-unused-variable": true, "curly": true, "no-console": [ true, "log", "error", "debug", "info" ], "no-debugger": true, "no-duplicate-variable": true, "no-eval": true, "no-invalid-this": true, "no-shadowed-variable": true, "no-unsafe-finally": true, "no-var-keyword": true, "triple-equals": [ true, "allow-null-check", "allow-undefined-check" ], "semicolon": [ true, "always", "ignore-interfaces" ], "variable-name": [ true, "ban-keywords", "check-format", "allow-leading-underscore" ] } } 


7) PostCss


Create postcss.config.js to not write prefixes to styles.

postcss.config.js
 module.exports = { plugins: [ require('autoprefixer')({ browsers: [ 'last 2 versions' ], cascade: true }) ] }; 


A little more complicated manipulations will follow, please focus.


8) Configure Typescript tsconfig.json


Since the development of A2 +, in my opinion, is impossible without typescript , it must be configured. The settings are normal, but if you have questions, ask in the comments.

tsconfig.json
 { //  typescript "compilerOptions": { "target": "es5", "module": "es2015", "declaration": false, "moduleResolution": "node", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "removeComments": false, "noImplicitAny": false, "suppressImplicitAnyIndexErrors": true, "skipLibCheck": true, "lib": ["es6", "dom"], "outDir": "./dist/", "typeRoots": [ "./node_modules/@types/" ] }, "compileOnSave": false, "buildOnSave": false, //      ./src "include": [ "./src/**/*" ], // typescript   : "exclude": [ "node_modules/*", "dist/*", "dist-serve/*", "node/*", "**/*.spec.ts" ], // loader  webpack "awesomeTypescriptLoaderOptions": { "forkChecker": true, "useWebpackText": true, "useCache": true }, //  AoT "angularCompilerOptions": { "genDir": ".", "skipMetadataEmit" : true } } 


9) Configure Webpack


The most difficult thing is to let the webpack know what we want from it. To do this, create a webpack.conf.js , without panic, I will try to explain everything

webpack.conf.js
 "use strict"; // node   webpack ,      const path = require('path'); const fs = require('fs'); const webpack = require('webpack'); const WebpackOnBuildPlugin = require('on-build-webpack'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const AotPlugin = require('@ngtools/webpack').AotPlugin; //,  package.json   serve, hmr, prod  . .?  ,   (,    npm run serve,     'serve')      : const ENV = process.env.npm_lifecycle_event ? process.env.npm_lifecycle_event : ''; const isStatic = ENV === 'serve'; const isHmr = ENV === 'hmr'; const isProd = ENV === 'prod'; const isTest = ENV === 'test'; const isAot = ENV.includes('aot'); const isProdServer = ENV.includes('prodServer'); //   ,    webpack   //  webpack.conf.js  ,     module.exports = function makeWebpackConfig() { console.log(`You are in ${ENV} mode`); //    let config = {}; //    //  -   npm run prodServer,    npm run prod,   if (isProdServer) { if (!fs.existsSync('./dist')) { throw "Can't find ./dist, please use 'npm run prod' to get it."; } } // sourcemaps if (isHmr || isStatic) { config.devtool = 'inline-source-map'; } else { config.devtool = 'source-map'; } //  ,    webpack.     index.html   “./ng-app.js” config.entry = { 'ng-app': './src/app/ng-main.ts' }; //  AoT        ,  … if (isAot) { config.entry['ng-app'] = './src/app/ng-main-aot.ts'; } //  ,   webpack  'ng-app',    filename: '[name].js',    prod ,      './dist',     path: root('./dist') config.output = isTest ? {} : { path: root('./dist'), //root –   ,      ,    webpack.config.js filename: '[name].js' }; //  entry   webpack    - ,   ,    prodServer       prod .       ,     ,  ,  .    ,   webpack.conf.js,    webpack-prod-server.js,       ,    ,       . if (isProdServer) { config.entry = { 'server': './webpack-prod-server.js' }; config.output = {}; } //  ,   webpack   config.resolve = { extensions: ['.ts', '.js', '.json', '.html', '.less', '.svg'] }; //   loaders:     ,   .  ,      ts  js, html   js , less   css    js ,   10   base64    js . config.module = { rules: [ { test: /\.ts$/, use: isAot ? [{loader: '@ngtools/webpack'}] : [ { loader: 'awesome-typescript-loader?' }, { loader: 'angular2-template-loader' }, { loader: 'angular-router-loader' } ].concat(isHmr ? '@angularclass/hmr-loader?pretty=' + !isProd + '&prod=' + isProd : []), exclude: [/\.(spec|e2e|d)\.ts$/] }, { test: /\.html$/, loader: 'raw-loader', exclude: [/node_modules\/(?!(ng2-.+))/, root('src/index.html')] }, { test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?name=[name].[ext]&limit=10000&useRelativePath=true" }, { test: /\.less$/, use: [ {loader: "css-to-string-loader"}, {loader: "css-loader"}, {loader: "postcss-loader"}, {loader: "less-loader"} ] } ] }; //     ,   webpack  if (!isTest) { config.plugins = [ //  webpack warcher   new webpack.NoEmitOnErrorsPlugin(), //     .ts ,     .ts     new webpack.DefinePlugin({ 'process.env': { 'STATIC': isStatic, 'HMR': isHmr, 'PROD': isProd, 'AOT': isAot } }), // -    new WebpackOnBuildPlugin((stats) => { console.log('build is done'); }) ] //    hmr,     hmr .concat(isHmr ? new webpack.HotModuleReplacementPlugin() : []); } //    'npm run prod',     prod   AoT if (isAot) { config.plugins = [ //  AoT  new AotPlugin({ tsConfigPath: './tsconfig.json', entryModule: root('src/app/app.module.ts#AppModule') }), //   new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, screw_ie8: true, conditionals: true, unused: true, comparisons: true, sequences: true, dead_code: true, evaluate: true, if_return: true, join_vars: true }, output: { comments: false }, sourceMap: true }), //     ./dist  (js     webpack,    ,    ) new CopyWebpackPlugin([ {from: 'index.html', context: './src'}, {from: 'assets/themes/base/fonts/**/*', context: './src'}, {from: 'assets/themes/base/images/other-images/**/*', context: './src'}, ]), new WebpackOnBuildPlugin((stats) => { console.log('build in aot is done'); }) ]; } //     webpack-dev-server config.devServer = { contentBase: isProdServer ? "./dist" : "./src",//  ,  prod   ./dist,     ./src headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" }, //   rest  historyApiFallback: true, // HTML5 history api,   1  compress: true,// gzip quiet: false, //        inline: isHmr || isStatic || isProdServer, //inline mode hot: isHmr, // hmr,   hmr  stats: "minimal", port: 9000, //      Webpack overlay: { errors: true }, //  webpack warcher watchOptions: { aggregateTimeout: 50, ignored: /node_modules/ } }; return config; }; //      function root(__path = '.') { return path.join(__dirname, __path); } 


10) src structure


Now our project looks like this, except for the src folder



Create a structure in the src folder:



A couple of comments: in the app folder will be our angular application, in the assets folder, auxiliary files, index.html just put in src. In assets, we will support the theme and split folders for convenient work with fonts, pictures, styles.

In our company, we use the BEM methodology, which is slightly more refined and more optimal, in our opinion. base.less - aggregate .less file for the base theme:

base.less
 // Common @import "themes/base/styles/common/normalize"; @import "themes/base/styles/common/colors"; @import "themes/base/styles/common/common"; @import "themes/base/styles/common/fonts"; @import "themes/base/styles/common/vars"; // Blocks // (please, add new blocks in alphabetical order) @import "themes/base/styles/blocks/app-component"; 


Note that, in our opinion, the functional and style parts of the application should be distributed: this solves a number of problems of both assembly and project support. If you use the BEM and the paradigm of one block - one less file, then the approach does not show any problems. However, there are a bunch of alternatives. In more detail to rummage in assets it is possible in the appendix, to this post. Ask questions in the comments to the article.

11) index.hml


index.html - has become incredibly simple in A2 + applications

index.html
 <!DOCTYPE html> <html> <head> <base href="/"> //  A2+ routing <meta charset="utf-8"> <title>Landing</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="/img/favicon.ico"> </head> <body> <app-component>Loading...</app-component> <script type="text/javascript" src="./ng-app.js"></script> </body> </html> 


12) Angular app


Let's take a deep breath, we have already done everything complicated, now the framework itself remains)

Create a structure in the app folder:



At first glance - rebus.

However, if you have completed at least the Angular 2+ Tutorial , then all this is already familiar to you. For the rest, brief comments: the whole application is divided into modules, the framework even provides such an entity - module. There is a main module - app.module.ts, there are additional modules that extend the functionality of the application. Most applications will have home, lazy and shared modules. The names of the modules, of course, are optional, but if you follow the rules for naming, you will not have problems with the extensibility of the application.

We won't talk a lot about the framework itself, there is excellent documentation . Better focus on the subtle points:

ng-main.ts


It begins with him

ng-main.ts
 import './ng-polyfills'; //   ie 9+ import … //  webpack   ,    if (process.env.STATIC) { //console.log("******************You are in Dev mode******************"); platformBrowserDynamic().bootstrapModule(AppModule).then(():any => {}); } else if (process.env.HMR) { //  hmr  Angular //console.log("******************You are in HMR mode******************"); bootloader(main); } export function main() { return platformBrowserDynamic() .bootstrapModule(AppModule) } 


ng-main-aot.ts for AoT


For the AoT (Ahead-of-Time Compilation) mode, create another main ng-main-aot.ts file, so you need ...

ng-main-aot.ts
 import … console.log("******************You are in prod mode******************"); enableProdMode(); platformBrowser() .bootstrapModuleFactory(<any>AppModuleNgFactory) .catch(error=>console.log(error)); 


HMR, styles, hammerjs


HMR, the styles of our application (just in case, left an example of connecting images) and the hammerjs settings for mobile development are connected to app.module.ts in this way:

app.module.ts
 require("style-loader!../assets/base.less"); //    webpack import … // hammer.js export class MyHammerConfig extends HammerGestureConfig { overrides = <any>{ 'swipe': {velocity: 0.4, threshold: 20} } } @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, HomeModule, NgRoutingModule ], providers: [ ], bootstrap: [ AppComponent ] }) export class AppModule { constructor(public appRef: ApplicationRef) {} hmrOnInit(store) { if (!store || !store.state) return; if ('restoreInputValues' in store) { store.restoreInputValues(); } this.appRef.tick(); delete store.state; delete store.restoreInputValues; } hmrOnDestroy(store) { let cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement); store.disposeOldHosts = createNewHosts(cmpLocation); store.state = {data: 'yolo'}; store.restoreInputValues = createInputTransfer(); removeNgStyles(); } hmrAfterDestroy(store) { store.disposeOldHosts(); delete store.disposeOldHosts; } } 


Lazy loading


Lazy loading of modules we connect in ng-routing.module.ts

ng-routing.module.ts
 import … const routes: Routes = [ {path: '', redirectTo: '/home', pathMatch: 'full'}, {path: 'home', component: HomeComponent}, //  lazy ,  .js  webpack     {path: 'lazy', loadChildren: './modules/lazy/lazy.module#LazyModule'}, {path: '**', component: PageNotFoundComponent}, ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class NgRoutingModule { } 


After connecting the lazy module in the router, you need to do the following in the module that we want to load deferred (using the example of lazy.module.ts):

lazy.module.ts
 importconst routes: Routes = [ {path: '', component: LazyComponent}, ]; @NgModule({ imports: [SharedModule, RouterModule.forChild(routes)], exports: [LazyComponent], declarations: [LazyComponent] }) export class LazyModule {} 


Hmm ... well, that's all. You can dig in the app folder in the attachment to this post.

To develop a page reload for every code change in the editor, we write in the terminal, being in the folder with package.json: npm run serve
The same, but without page reload: npm run hmr
Making a prod build with AoT: npm run prod
Run a static server to view the prod: npm run prodServer
Clean the ./dist folder: npm run clean

Just a few steps and we work: webpack build with Angular 4, AoT, HMR, Lazy loading . Everything, including patterns and styles, is neatly put into a bundle and optimized.
Of course, this configuration can be expanded, improved, changed, but in my opinion, it is quite enough to boldly begin to develop with Angular 2+.

PS

AoT small advertisement: a great boost to the performance of your SPA application on Angular.



Thanks for attention.

Source: https://habr.com/ru/post/337540/


All Articles