블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

06-25 03:49

Category

Recent Comment

2019. 4. 10. 16:35 Angular/Architecture

두번째 Multi Application에 대한 Plugin 방식을 구성 테스트해 보자.

 

 

Library기반 Plugin 파일 만들기

플랫폼에 있는 플러그인이 아니라 플랫폼과 별개의 서비스에서 운영되는 플로그인을 플랫폼상에 렌더링하기 위해서는 UMD(Universal Module Definition) 방식으로 플러그인이 번들링 되어야 한다. UMD에 대해서는 본 블로그의 글을 참조한다.

$ ng g library plugin2 --publishable

 

plugin2 로 별도 번들링이 가능한 라이브러리 파일을 생성한다. 최종 npm scope를 jm으로 주었기 때문에 npm으로 설치했을 때 node_modules최종 명칭은 @jm/plugin2가 된다. libs/plugins/package.json의 설정 참조. build가 되는지 실행해 본다.

$ ng build plugin2

dist/libs/plugin2에 npm repository로 publish할 수 있는 형태의 다양한 포멧으로 번들링이 되었다. 해당 번들링은 ng-packagr를 기본으로 한 Angular Package Format에 따른다.

plugin2 라이브러리에 plugin2라는 이름의 컴포넌트를 생성하고 여기에 ngx-echart를 사용하는 샘플을 넣어보자.

// 컴포넌트 생성
$ ng g component plugin2 --project=plugin2

// echart 관련 라이브러리 및 echart angular wrapper 설치
$ npm i -S echarts ngx-echarts
$ npm i -D @types/echarts

plugin2/plugin2.component.html과 .ts에 ngx-echart의 예제 내용을 첨부한다. Plugin2Module에 bootstrap에 Plugin2Component를 설정한다.

// plugin2.component.html
<div echarts [options]="options" class="demo-chart"></div>

// plugin2.component.ts 
options 내용 설정: https://xieziyu.github.io/ngx-echarts/#/usage/basic 참조

// plugin2.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgxEchartsModule } from 'ngx-echarts';

import { Plugin2Component } from './plugin2/plugin2.component';

@NgModule({
  declarations: [Plugin2Component],
  imports: [
    CommonModule,
    NgxEchartsModule
  ],
  exports: [
    Plugin2Component
  ],
  bootstrap: [
    Plugin2Component
  ]
})
export class Plugin2Module {}

plugin2 컴포넌트를 사용하는 별도 애플리케이션을 생성해 보자.

 

 

별도 애플리케이션 생성하여 Plugin2 테스트 하기

Multi Application을 가정하여 Plugin2를 사용하는 App2 애플리케이션을 생성하여 Plugin2가 잘 나오는지 테스트 한다.

$ ng g application app2

app2 가 실행할 때 4300 port 를 사용토록 angular.json에 포트설정을 추가한다.

"serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "app2:build",
            "port": 4300
          },
 ... 중략 ...

 $ ng s app2 

apps/app2/src/app/app.module.ts 와 app.component.html을 수정한다. import 할 때"@jm/plugin2"로 사용함을 주의한다.

// app.component.html
<jm-plugin2></jm-plugin2>

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { Plugin2Module } from '@jm/plugin2';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    Plugin2Module
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

"ng s app2" 실행하고 http://localhost:4300 으로 호출하면 샘플 echart가 보인다.

 

 

Plugin2 번들링 하기

app2 애플케이션에 plugin2 컴포넌트가 잘 나오는 것을 확인 하였다. plugin2 번들링을 위해 한가지 설정 옵션을 변경하여 다시 번들링한다. build시에 에러가 발생하면 ng-packagr v5.0.1 이상을 설치한다.

//libs/plugin2/src/tsconfig.lib.json 에서 skipTemplateCodegen을 false로 변경한다
  "angularCompilerOptions": {
    "annotateForClosureCompiler": true,
    "skipTemplateCodegen": false,

// ng-packagr 관련 업데이트 
$ npm update ng-packagr
$ npm i -D tsickle

//다시 번들링한다. 
$ ng build plugin2
Building Angular Package
Building entry point '@jm/plugin2'
Compiling TypeScript sources through ngc
Bundling to FESM2015
Bundling to FESM5
Bundling to UMD
No name was provided for external module 'ngx-echarts' in output.globals – guessing 'ngxEcharts'
Minifying UMD bundle
Copying declaration files
Writing package metadata
Removing scripts section in package.json as it's considered a potential security vulnerability.
Built @jm/plugin2
Built Angular Package!
 - from: /Users/dowonyun/prototyping/jamong/libs/plugin2

skipTemplateCodegen를 false로 하면 ngfactory파일까지 생성되고 해당 파일은 Angular메타정보를 해석해 놓은 파일로 동적으로 파일을 로딩할 때 사용할 것이다. 다음으로 bundle 된파일을 다시 하나의 파일로 합치기 위해 package.json에 script를 등록한다.

// package.json
"scripts": {
   ... 중략 ...
   "build:plugin2": "rollup dist/libs/plugin2/esm2015/lib/plugin2.module.ngfactory.js --file dist/apps/api/plugin2.js --format umd --name plugin2"
}

// bundling 
$ npm run build:plugin2

dist/apps/api/plugin2.js 파일이 생성된다. 또는 ROOT에 rollup.config.js 파일을 생성하여 관리할 수도 있다.

//rollup.config.js
export default [
  {
    input: 'dist/libs/plugin2/esm2015/lib/plugin2.module.ngfactory.js',
    output: [
      {
        name: 'plugin2',
        file: 'dist/apps/api/plugin2.js',
        format: 'umd'
      }
    ]
  }
];

//build 명령
$ rollup -c

두가지의 명령을 수행한다.

  • ng build plugin2
  • npm run build:plugin2  또는 rollup.config.js를 설정하였다면 rollup -c

 

Plugin2 파일 API 서비스 추가

plugin2 파일을 동적으로 로딩하기 위해 다음과 같이 역할을 추가한다.

  • Dev Server는 Platform 서버 역할
  • API Server는 별개 애플리케이션 서버로 보고 Plugin-2 파일을 서비스하는 역할

이제 API Server에서 plugin2.js 파일을 읽어서 파일 내용을 전달하는 API를 추가한다. NetsJS는 NodeJS위에 구동되는 서버프레임워크로 Angular와 유사한 Syntax를 통해 MVC 패턴으로 개발한다.

  • GET으로 plugin2를 호출하면 파일을 읽는다.
  • 파일 내용을 return하면 끝!
// apps/api/src/app/app.controller.ts

import { Controller, Get } from "@nestjs/common";

import { Message } from "@jm/api-interface";
import { AppService } from "./app.service";

const path = require('path');
const fs = require('fs-extra');

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get("hello")
  getData(): Message {
    return this.appService.getData();
  }

  @Get("plugin2")
  getPlugin2() {
    const fileName = path.join(process.cwd(), 'dist/apps/api/plugin2.js');
    const plugin2 = fs.readFileSync(fileName, 'utf8');
    return plugin2;
  }
}

테스트를 해보자. API Server기본 context는 api로 http://localhost:3333/api/plugin2 로 호출한다. 번들링 파일 내용이 응답됨을 확인했다.

// API Server 기동
$ ng s api

 

 

원격 Plugin2을 Lazy Loading 하기

plugin2.js 파일을 다운로드 받아 로딩하기 위해 apps/jamong/src/app/app.component.html과 .ts에 로딩 구문을 추가한다.

// app.component.html
<button (click)="loadPlugin()">Load plugin</button>
<lazy-af *ngIf="plugin1Path" [moduleName]="plugin1Path"></lazy-af>

<p></p>

<button (click)="loadPlugin2()">Load plugin2</button>
<ng-template #Plugin></ng-template>

app.component.ts

  • plugin2.js 파일을 요청하기 위해 HttpClient를 사용
  • Plugin2ModuleNgFactory안의 Angular MetaData를 통해 관련 컴포넌트들 JIT 컴파일 수행
  • @herodevs/dynamic-af 패키지이 DynamicAFService 서비스를 injector받는다.
  • 이때 plugin2.js 소스내의 require하는 모듈을 위해 import * as from ''; 하여 아래 소스처럼 설정한다.
import { Component, ViewChild, ViewContainerRef, NgModuleFactory, Injector } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { DynamicAFService } from '@herodevs/dynamic-af';

import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as core from '@angular/core';
import * as router from '@angular/router';
import * as rxjs from 'rxjs';
import * as rxjsOperators from 'rxjs/operators';
import * as ngxEcharts from 'ngx-echarts';

@Component({
  selector: 'jm-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  @ViewChild('Plugin', {read: ViewContainerRef}) pluginVcr: ViewContainerRef;
  plugin1Path: string;
  loadedPlugins: any = {};

  constructor(
    private http: HttpClient,
    private injector: Injector,
    private lazyService: DynamicAFService
  ) { }

  loadPlugin() {
    this.plugin1Path = 'apps/jamong/src/app/plugin1/plugin1.module#Plugin1Module';
  }

  loadPlugin2() {
    const moduleFactory = this.loadedPlugins['api/plugin2'];
    if (!moduleFactory) {
      this.loadRemoteComponent();
    } else {
      this.lazyService.createAndAttachModuleAsync(moduleFactory, this.injector, { vcr: this.pluginVcr });
    }
  }

  private loadRemoteComponent() {
    let moduleFactory: NgModuleFactory<any>;
    this.http.get('api/plugin2', { responseType: 'text' })
      .pipe(
        catchError(this.handleError)
      ).subscribe((compiledSource: any) => {
        const exports = {};
        const modules = {
          '@angular/core': core,
          '@angular/common': common,
          '@angular/common/http': commonHttp,
          '@angular/router': router,
          'rxjs': rxjs,
          'rxjs/operators': rxjsOperators,
          'ngx-echarts': ngxEcharts
        };
        const require: any = (module) => modules[module];
        // tslint:disable-next-line: no-eval
        eval(compiledSource);
        moduleFactory = exports['Plugin2ModuleNgFactory'];
        this.loadedPlugins['api/plugin2'] = moduleFactory;
        this.lazyService.createAndAttachModuleAsync(moduleFactory, this.injector, { vcr: this.pluginVcr});
      });
  }

  private handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(`Backend returned code ${error.status}, body was: ${error.error}`);
    }
    return throwError(error);
  }
}

Load plugin2를 호출하였을 때 원격 처리 결과

위의 모든 소스는 GitHub에...

 

 

<참조>

- ngx-echarts

 

ngx-echarts demo

 

xieziyu.github.io

- Another Angular Plugin Example

 

iwnow/angular-plugin-example

Angular plugin with AOT and separate build. Contribute to iwnow/angular-plugin-example development by creating an account on GitHub.

github.com

- Rollup Configuration

 

Bundling Your JavaScript Library with Rollup

A step-by-step tutorial on how to bundle your JavaScript library using Rollup. You’ll also learn how to publish those bundles to NPM.

bagja.net

 

posted by peter yun 윤영식
2017. 9. 4. 15:51

Angular 최신 버전이 v5 beta-6를 향해 가고 있다. 9월에는 v5 release가 될 것으로 보인다. Angular에서 툴 기능으로 @angular/cli를 제공하고 있는데 오늘은 이것의 내부구조에 대해 연구해 본다. 



@angular/cli 설치 및 프로젝트 생성

설치는 npm 또는 yarn을 이용한다.

$> npm i -g @angular/cli


or 


$> yarn add global @angular/cli


Angular 기반 프로젝트를 생성해 보자.

$> ng new jamong


실행해 보자

$> ng serve




폴더 구조

angular/cli는 Automation Tool 과 Module loader로 webpack을 사용하고 있다. 웹팩의 환경파일을 루트 폴더에 생성한다. 
$> ng eject
=====================================================
Ejection was successful.

To run your builds, you now need to do the following commands:
   - "npm run build" to build.
   - "npm test" to run unit tests.
   - "npm start" to serve the app using webpack-dev-server.
   - "npm run e2e" to run protractor.

Running the equivalent CLI commands will result in an error.

=====================================================
Some packages were added. Please run "npm install".


설명에 따라 추가 명령을 수행한다. 

$> npm run build && npm start


// package.json의 build 와 start 스크립트 설정내역

  "scripts": {

    "ng": "ng",

    "start": "webpack-dev-server --port=4200",

    "build": "webpack",

    "test": "karma start ./karma.conf.js",

    "lint": "ng lint",

    "e2e": "protractor ./protractor.conf.js",

    "pree2e": "webdriver-manager update --standalone false --gecko false --quiet"

  },



webpack 오류가 난다면 webpack이  global설치가 않되어 있기때문이다. webpack을 설치한다. 

$> yarn add webpack --dev

또는

$> yarn add global webpack 

또는 

$> npm install -g webpack





Webpack 환경파일

webpack의 기본은 초기 참조할 entry파일과 결과 번들파일을 지정하는 것이다. 

$> webpack <entry file path> <output bundle file path>


예) webpack ./entry.js bunlde.js


간단한 번들링은 CLI를 사용해도 무방하지만 복잡한 옵션이 적용되어야 한다면 환경파일을 사용한다. 예) webpack.config.js 아래 예에서 ouput의 filename에 [name]은 entry의 'app' 키값이다. 

module.exports = {

  context: __dirname + '/app',

  entry: {

    app: './app.js'

  },

  output: {

    path: __dirname + '/dist',

    filename: '[name].bundle.js'

  }

}


환경설정 자세한 옵션은 공식문서를 참조한다. 여기서 주의할 것은 entry는 String, Object, Array로 설정가능하다. 

- String: 하나만 설정

- Array: output 번들링 파일에 순서적으로 합쳐진다.

- Object: SPA처럼 index.html에 적용되는 것이 아니라, index1.html 또는 index2.html 등 output이 각각의 html에 포함될 때 사용한다. 





Webpack Loader

Webpack에는 자바스크립트가 아닌 확장형식의 파일을 자바스크립트에서 동작할 수 있도록 해주는 로더(Loader)가 있다. 즉, 모든(CSS, Images, HTML...)을 모듈로 취급하게 해주는 핵심역할을 로더가 수행한다. 말 그대로 다양한 파일 확장자의 Module Loader의 줄임말이라 보면된다. (전체 목록 참조) 만일 설정중에 module, rules를 사용하지않고 loaders를 쓰거나, options대신 query를 쓰면 webpack 1에 대한 설명이다.


- 스타일: style, css

- 변환: typescript, coffeescript, ES2015


환경파일안에 module 밑으로 설정한다. 


module.exports = {

  entry: ...

  output: ...

  module: {

    rules: [

      {

        test: /\.css$/,

        use: [ 'style-loader', 'css-loader']

      }

    ]

    ...

  }


}


특히 Angular 개발시에는 Typescript를 사용하므로 sourcemap을 남기려면 'source-map-loader'를 module 프로퍼티에 설정한다. (sourcemap에 대해 webpack3에서는 plugin으로 설정도 가능하다.) enforce 설정값을 "pre"로 하면 자바스크립트 변환전에 sourcemap 을 만든다.

{

        "enforce": "pre",

        "test": /\.js$/,

        "loader": "source-map-loader",

        "exclude": [

          /(\\|\/)node_modules(\\|\/)/

        ]

}


개발시에는 Node.js기반의 webpack-dev-server를 통해 개발 페이지를 테스트할 수 있다. 옵션은 --inline (전체 페이지 리로딩) --hot (변경 컴포넌트만 리로딩)한다. 

// 전체 및 부분 리로딩 옵션 설정

$> webpack-dev-server --inline --hot




Webpack Plugins

플러그인은 Output 번들의 Chunk 또는 Compilation 레벨 파일에 대한 추가 작업을 수행한다. 예로 ulgifyJSPlugin은 번들 파일 사이즈를 줄이고, 코드를 못 알아보게 만들어 준다. 또는 extract-text-webpack-plugin의 경우는 css-loader, style-loader의 결과를 하나의 별도 외부 파일로 만들어 준다. (플러그인 목록 참조)


@angular/cli에서  ng eject로 나온 webpack.config.js안의 plugins설정 내역

"plugins": [

    new NoEmitOnErrorsPlugin(),

    new GlobCopyWebpackPlugin({

      "patterns": [

        "assets",

        "favicon.ico"

      ],

      "globOptions": {

        "cwd": path.join(process.cwd(), "src"),

        "dot": true,

        "ignore": "**/.gitkeep"

      }

    }),

    new ProgressPlugin(),

    new CircularDependencyPlugin({

      "exclude": /(\\|\/)node_modules(\\|\/)/,

      "failOnError": false

    }),

    new NamedLazyChunksWebpackPlugin(),

    new HtmlWebpackPlugin({

      "template": "./src/index.html",

      "filename": "./index.html",

      "hash": false,

      "inject": true,

      "compile": true,

      "favicon": false,

      "minify": false,

      "cache": true,

      "showErrors": true,

      "chunks": "all",

      "excludeChunks": [],

      "title": "Webpack App",

      "xhtml": true,

      "chunksSortMode": function sort(left, right) {

        let leftIndex = entryPoints.indexOf(left.names[0]);

        let rightindex = entryPoints.indexOf(right.names[0]);

        if (leftIndex > rightindex) {

          return 1;

        } else if (leftIndex < rightindex) {

          return -1;

        } else {

          return 0;

        }

      }

    }),

    new BaseHrefWebpackPlugin({}),

    new CommonsChunkPlugin({

      "name": [

        "inline"

      ],

      "minChunks": null

    }),

    new CommonsChunkPlugin({

      "name": [

        "vendor"

      ],

      "minChunks": (module) => {

        return module.resource &&

          (module.resource.startsWith(nodeModules) ||

            module.resource.startsWith(genDirNodeModules) ||

            module.resource.startsWith(realNodeModules));

      },

      "chunks": [

        "main"

      ]

    }),

    new SourceMapDevToolPlugin({

      "filename": "[file].map[query]",

      "moduleFilenameTemplate": "[resource-path]",

      "fallbackModuleFilenameTemplate": "[resource-path]?[hash]",

      "sourceRoot": "webpack:///"

    }),

    new CommonsChunkPlugin({

      "name": [

        "main"

      ],

      "minChunks": 2,

      "async": "common"

    }),

    new NamedModulesPlugin({}),

    new AotPlugin({

      "mainPath": "main.ts",

      "replaceExport": false,

      "hostReplacementPaths": {

        "environments/environment.ts": "environments/environment.ts"

      },

      "exclude": [],

      "tsConfigPath": "src/tsconfig.app.json",

      "skipCodeGeneration": true

    })

]


JHipster로 자동생된 webpack.config.dev.js의 plugins 설정 내역

plugins: [

        new BrowserSyncPlugin({

            host: 'localhost',

            port: 9000,

            proxy: {

                target: 'http://localhost:9060',

                ws: true

            }

        }, {

            reload: false

        }),

        new webpack.NoEmitOnErrorsPlugin(),

        new webpack.NamedModulesPlugin(),

        new writeFilePlugin(),

        new webpack.WatchIgnorePlugin([

            utils.root('src/test'),

        ]),

        new WebpackNotifierPlugin({

            title: 'JHipster',

            contentImage: path.join(__dirname, 'logo-jhipster.png')

        })

 ]




Production vs Development

개발과 운영 시점에 webpack 환경을 달리 적용하기 위해 보통  webpack.config.dev.js 와 webpack.config.prod.js를  따로 만들고 package.json의 script에 적용해 사용한다. package.json에 적용하고 싶지 않다며 gulp를 사용해도 된다. 


@angular/cli의 script 내역

 "scripts": {

    "ng": "ng",

    "start": "webpack-dev-server --port=4200",

    "build": "webpack",

    "test": "karma start ./karma.conf.js",

    "lint": "ng lint",

    "e2e": "protractor ./protractor.conf.js",

    "pree2e": "webdriver-manager update --standalone false --gecko false --quiet"

}


JHipster script 내역

"scripts": {

    "lint": "tslint --type-check --project './tsconfig.json' -e 'node_modules/**'",

    "lint:fix": "yarn run lint -- --fix",

    "ngc": "ngc -p tsconfig-aot.json",

    "cleanup": "rimraf target/{aot,www}",

    "clean-www": "rimraf target//www/app/{src,target/}",

    "start": "yarn run webpack:dev",

    "serve": "yarn run start",

    "build": "yarn run webpack:prod",

    "test": "karma start src/test/javascript/karma.conf.js",

    "test:watch": "yarn test -- --watch",

    "webpack:dev": "yarn run webpack-dev-server -- --config webpack/webpack.dev.js --progress --inline --hot --profile --port=9060",

    "webpack:build:main": "yarn run webpack -- --config webpack/webpack.dev.js --progress --profile",

    "webpack:build": "yarn run cleanup && yarn run webpack:build:main",

    "webpack:prod:main": "yarn run webpack -- --config webpack/webpack.prod.js --progress --profile",

    "webpack:prod": "yarn run cleanup && yarn run webpack:prod:main && yarn run clean-www",

    "webpack:test": "yarn run test",

    "webpack-dev-server": "node --max_old_space_size=4096 node_modules/webpack-dev-server/bin/webpack-dev-server.js",

    "webpack": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js",

    "e2e": "protractor src/test/javascript/protractor.conf.js",

    "postinstall": "webdriver-manager update && node node_modules/phantomjs-prebuilt/install.js"

}


to be continued...



<참고>

- Webpack core concept

- 네이버 webpack 소개

- Webpack의 혼란스러운 사항들

- Webpack3에서 주의할 점


'' 카테고리의 다른 글

[CLI] @angular/cli속의 Webpack 이해하기  (0) 2017.09.04
posted by peter yun 윤영식
prev 1 next