动态加载外部 webpack 捆绑的 ngModule 作为路由处理程序

Posted

技术标签:

【中文标题】动态加载外部 webpack 捆绑的 ngModule 作为路由处理程序【英文标题】:Dynamically loading an external webpack bundled ngModule as a route handler 【发布时间】:2018-04-11 15:09:35 【问题描述】:

我们希望将大型前端项目划分为多个单独部署的项目,这些项目更易于使用。我正在尝试包含一个捆绑的 ngModule 来处理来自另一个应用程序的路由。应用程序必须不知道彼此的配置。捆绑包将通过全局共享一些大型依赖项(如 Angular)。我们不需要在捆绑包之间摇摆不定,我们可能只需要接受一些重复的依赖项。

根路由器抱怨

Error: No NgModule metadata found for 'TestsetModule'.

这让我相信子模块没有在加载时进行角度编译,或者由于某种原因没有注册它的模块。我认为可能需要手动编译模块,但我不确定如何使用这个https://angular.io/api/core/Compiler#compileModuleAndAllComponentsAsync

根应用通过路由加载子应用:

import  ModuleWithProviders  from '@angular/core';
import  Routes, RouterModule  from '@angular/router';
const load = require("little-loader");


const routes: Routes = [
   path: ``, loadChildren: () => new Promise(function (resolve) 
      load('http://localhost:3100/testset-module-bundle.js',(err: any) => 
        console.log('global loaded bundle is: ', (<any>global).TestsetModule )
        resolve((<any>global).TestsetModule)
      
    )
  )
];

export const HostRouting: ModuleWithProviders = RouterModule.forRoot(routes);

我也尝试使用 Angular 路由器的字符串解析语法,而不是你看到的这种奇怪的全局东西,但我遇到了类似的问题。

这是正在加载的模块,除了全局导出之外非常标准:

import  NgModule  from '@angular/core';
import  CommonModule  from '@angular/common';
import  HttpModule  from '@angular/http';
//import  MaterialModule  from '@angular/material';
import  FlexLayoutModule  from '@angular/flex-layout';
import  FormsModule    from '@angular/forms';
import  LoggerModule, Level  from '@churro/ngx-log';

import  FeatureLoggerConfig  from './features/logger/services/feature-logger-config';


import  TestsetComponent  from './features/testset/testset.component';
import  TestsetRouting  from './testset.routing';

@NgModule(
    imports: [
        CommonModule,
        //MaterialModule,
        FlexLayoutModule,
        HttpModule,
        FormsModule,
        LoggerModule.forChild(
          moduleName: 'Testset',
          minLevel: Level.INFO
        ),
        TestsetRouting,
    ],
    declarations: [TestsetComponent],
    providers: [
      /* TODO: Providers go here */
    ]
)
class TestsetModule  
(<any>global).TestsetModule = TestsetModule

export TestsetModule as default, TestsetModule;

这里是根包的 webpack 配置。请注意通过名称不佳的“ProvidePlugin”进行的全局导出。

const webpack = require('webpack');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
const path = require('path');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const IgnorePlugin = require('webpack/lib/IgnorePlugin');
const PolyfillsPlugin = require('webpack-polyfills-plugin');
const WebpackSystemRegister = require('webpack-system-register');


module.exports = (envOptions) => 
    envOptions = envOptions || ;
    const config = 

        entry: 
          'bundle': './root.ts'
        ,
        output: 

          libraryTarget: 'umd',
          filename: '[name].js',//"bundle.[hash].js",
          chunkFilename: '[name]-chunk.js',
          path: __dirname
          ,
          externals: 

          ,
        resolve: 
            extensions: ['.ts', '.js', '.html'],
        ,
        module: 
            rules: [
                 test: /\.html$/, loader: 'raw-loader' ,
                 test: /\.css$/, loader: 'raw-loader' ,

            ]
        ,
        devtool: '#source-map',
        plugins: [
          new webpack.ProvidePlugin(
            'angular': '@angular/core',
            'ngrouter': '@angular/router',
            'ngxlog':'@churro/ngx-log'
          )

        ]
    ;
    config.module.rules.push(
       test: /\.ts$/, loaders: [
        'awesome-typescript-loader', 
        'angular-router-loader',
        'angular2-template-loader', 
        'source-map-loader'
      ]  
    );
  



    return config;
;

这是子包的 webpack 配置。请注意将角度视为全局的“外部”。

module.exports = (envOptions) => 
    envOptions = envOptions || ;
    const config = 
        entry: 
          'testset-module-bundle': './src/index.ts'
        ,
        output: 
          //library: 'TestsetModule',
          libraryTarget: 'umd',
          filename: '[name].js',//"bundle.[hash].js",
          chunkFilename: '[name]-chunk.js',
          path: path.resolve(__dirname, "dist")
          ,
          externals: 
            //expect these to come from the app that imported us
            // name to be required : name from global
             'angular': '@angular/core',
             'ngrouter': '@angular/router',
             'ngxlog': '@churro/ngx-log'       
          ,
        resolve: 
            extensions: ['.ts', '.js', '.html'],
        ,
        module: 
            rules: [
                 test: /\.html$/, loader: 'raw-loader' ,
                 test: /\.css$/, loader: 'raw-loader' ,

            ]
        ,
        devtool: '#source-map',
        plugins: [

        ]
    ;

    config.module.rules.push(
       test: /\.ts$/, loaders: [
        'awesome-typescript-loader', 
        'angular-router-loader',
        'angular2-template-loader', 
        'source-map-loader'
      ] 
    );
  



    return config;
;

这里是我的 tsconfig 文件,'awesome-typescript-loader' 读取。


  "compilerOptions": 
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "app",
    "paths": 
      "@capone/*": [
        "*"
      ],
      "@angular/*": [
        "node_modules/@angular/*"
      ],
      "rxjs/*": [
        "node_modules/rxjs/*"
      ]
    
  ,

  "exclude": ["node_modules", "src/node_modules", "compiled", "src/dev_wrapper_app"],
  "angularCompilerOptions": 
    "genDir": "./compiled",
    "skipMetadataEmit": true
  

如果你还在阅读,那太棒了。当两个包都是同一个 webpack 配置的一部分并且子模块只是一个块时,我能够让它工作。 Angular 就是为此而设计的。但是我们的用例是让孩子和父母在运行之前相互不了解。

【问题讨论】:

您是否尝试过使用webpack-angular-externals? github.com/mattlewis92/webpack-angular-externals#usage另请阅读medium.com/@trekhleb/… 可能会将其捆绑到一个 npm 模块中,您可以将其安装到您的其他项目中。 我将采取引导主应用程序的方法,创建一个子模块,然后使用主应用程序路由器模块延迟加载子模块。 Ionic 在它创建的每个页面上都有这个实现,结果是更快的应用程序引导。一个很好的参考来分离主模块和子模块angular-2-training-book.rangle.io/handout/modules/… 我想知道你是否已经解决了这个问题?我正在寻找类似用例的解决方案。我也尝试过使用 rollup 或 umd 包注入脚本标签,但我总是遇到错误。 【参考方案1】:

正如你所说的

应用必须不知道彼此的配置。

我在 Angular2 中遇到了类似的问题。我通过创建一个子应用程序解决了这个问题。一个单独的 sub-main.browser.ts 和 index.html 文件。它有自己的依赖关系,共享相同的节点模块。两个主要模块都引导不同的应用程序组件。我们在没有 angular-cli 的情况下使用 Angular。

在 webpack 配置中,我添加了

entry: 

  'polyfills': './src/polyfills.browser.ts',
  'main' .   :     './src/main.browser.aot.ts',
  'sub-main' : '/src/sub-main.browser.ts'

,

还有一个更详细的 HtmlWebpackPlugin。在块中,我们仅加载将在两个应用程序中使用的模块。如果我们看到 polyfills 很常见。

   new HtmlWebpackPlugin(
    template: 'src/index.html',
    title: METADATA.title,
    chunksSortMode: 'dependency',
    metadata: METADATA,
    inject: 'head',
    chunks: ['polyfills','main']
  ),

  new HtmlWebpackPlugin(
    template: 'src/index2.html',
    title: 'Sub app',
    chunksSortMode: 'dependency',
    metadata: METADATA,
     inject: 'head',
    filename: './sub-app.html',
    chunks: ['polyfills','sub-main']
  ),

下一个任务是为开发环境的两个子应用创建单独的端点。

devServer: 
      port: METADATA.port,
      host: METADATA.host,
      historyApiFallback: true,
      watchOptions: 
        aggregateTimeout: 300,
        poll: 1000
      ,
      proxy: 
   "/sub-app": 
    target: "http://localhost:3009",
    bypass: function(req, res, proxyOptions) 
        return "/index2.html";
    
  

    ,

现在,当我构建项目时,会生成两个不同的 HTML 文件。每个都有自己的 javascript 包依赖项和公共资产。它们也可以部署在不同的服务器上。

经过多次反复试验,我能够完成我的 POC。我的建议是看起来比角度高出一步。查看 webpack 如何部署您当前的项目。如果您可以对其进行配置以达到您的目的。

【讨论】:

不过,在这一点上,基础应用程序知道 sub-main,对吗? 这很好。在我看来,当子主更改时,您的主应用程序需要重建。我应该更清楚:这就是我们试图避免的。 @JaimeTorres webpack 作为 root 和 mains 作为孩子。如果您正在考虑将主应用程序作为基础应用程序。它不会知道 sub-main,反之亦然。 @light24bulbs 是的,它确实重建了整个应用程序。但我相信我们可以配置 webpack 只重建更新的子应用程序。

以上是关于动态加载外部 webpack 捆绑的 ngModule 作为路由处理程序的主要内容,如果未能解决你的问题,请参考以下文章

Webpack:如何使用动态捆绑组合两个完全独立的捆绑包

Angular Cli Webpack,如何添加或捆绑外部 js 文件?

使用 webpack [request] 魔术注释将动态模块捆绑在一起

webpack加载AMD模块而不捆绑

如何预加载 webpack4+babel 捆绑的 CSS @font-face 字体?

允许加载外部捆绑文件?