升级至 Angular8 及实现自定义 webpack 配置的方案

1 升级至 Angular8

如果你使用的是 angular/cli 构建方案,执行 ng update 即可,该过程主要是更新 package.json 中的依赖。为了了解到底改变了哪些内容,个人更喜欢手动方式升级,方法参考如下:

  • 首先全局安装 @angular/clinpm i -g @angular/cli
  • 然后创建一个新的项目 ng new ng8-demo
  • 对比测试项目 ng8-demo 目录中的 package.json 文件,更新旧项目 pacakge.json 文件中的依赖;
  • 对比 angular.jsontslint.jsontsconfig.json 等配置文件,根据需要进行修改;
  • 执行 ng lint,排查报错情况进行全局文件的修改适配

2 实现自定义 webpack 配置的几种方法

2.1 编写脚本注入到 @angular-devkit 构建流程

在 Angular7 以前,我是采用该方法自定义一个注入脚本实现,相关记录可参考该文: 一种自定义 Angular-cli 6.x/7.x 默认 webpack 配置的方法。在升级到 Angular8 的过程中,排查各种问题时发现了一个不错的第三方案 angular-builders,在测试效果后采用了它(后文会详细介绍)。这里仅提供 Angular8 自定义注入的方案供参考:

2.1.1 新建注入代码的脚本

新建注入代码的脚本 scripts/insert-to-cli-webpack.js,内容参考:

const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const rootDir = path.resolve(__dirname, '../');
const webpackCliPath = path.resolve(rootDir, 'config/webpack-cli-inject.js').replace(/\\/g, '\\\\');
const indexTransformPath = path.resolve(rootDir, 'scripts/index-html.transform.js').replace(/\\/g, '\\\\');
const buildNgSrcPathList = {
  common: {
    file: path.resolve(rootDir, 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/common.js'),
    findStr: 'exports.getCommonConfig = getCommonConfig;',
    replaceStr: `exports.getCommonConfig = require('${webpackCliPath}')(getCommonConfig);`,
  },
  /**
   * Set's directTemplateLoading: false to allow custom pug template loader to work
   * @see https://github.com/angular/angular-cli/issues/14534
   */
  typescript: {
    file: path.resolve(rootDir, 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/typescript.js'),
    findStr: 'directTemplateLoading: true,',
    replaceStr: 'directTemplateLoading: false,',
  },
  /** 实现自定义 index.html 处理 */
  augmentIndexHtml: {
    file: path.resolve(rootDir, 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/utilities/index-file/augment-index-html.js'),
    findStr: 'async function augmentIndexHtml(params) {',
    replaceStr: `async function augmentIndexHtml(params) {
      params.inputContent = await require('${indexTransformPath}')(params);
      `,
  },
};

try {
  Object.keys(buildNgSrcPathList).forEach(type => {
    const config = buildNgSrcPathList[type];
    const filePath = config.file;
    const filePathShort = filePath.replace(rootDir, '');
    const configText = fs.readFileSync(filePath, 'utf-8');

    if (configText.includes(config.replaceStr)) return;
    if (!configText.includes(config.findStr)) {
      console.log(chalk.red.bold(`文件 ${chalk.yellow.bold(filePathShort)} 中未发现可替换的字符串: ${config.findStr}`));
      return;
    }

    console.log(chalk.yellow.bold(` Inserting to: `), chalk.yellow(filePathShort));
    const output = configText.replace(config.findStr, config.replaceStr);
    fs.writeFileSync(filePath, output);
  });
} catch (err) {
  console.log(err);
}

2.1.2 新建自定义的 webpack 配置文件

文件名为 config/webpack.cli-inject.js,内容参考:

const webpackMerge = require('webpack-merge');
// const pkg = require('../package.json');
const getCustomConfig = (commonConfig, wco) => {
  // console.log(commonConfig, wco.buildOptions);
  const customCfg = {
    module: {
      rules: [
        {
          test: /\.pug$/,
          use: [
            'raw-loader',
            {
              loader: 'pug-html-loader',
              options: {
                doctype: 'html',
              },
            },
          ],
        },
        // {
        //   test: /\.html$/,
        //   loader: 'raw-loader',
        //   exclude: [helpers.root('src/index.html')],
        // },
      ],
    },

    plugins: [],
  };
  const mergedCfg = webpackMerge([commonConfig, customCfg]);

  return mergedCfg;
};

module.exports = getCommonConfig => {
  return wco => {
    const commonConfig = getCommonConfig(wco);
    return getCustomConfig(commonConfig, wco);
  };
};

2.1.3 新建自定义处理 index.html 逻辑的文件

文件名 scripts/index-html.transform.js,内容参考:

/**
 * angular8 以后 index.html 不再由 webpack 处理,相关插件也不再有效
 * @angular-devkit/build-angular 提供了 indexTransform 接口,但只能通过 custom-webpack 方式定义
 * 这里通过在 node_modules/@angular-devkit/build-angular/src/angular-cli-files/utilities/index-file/augment-index-html.js
 * 文件中注入方法实现构建时修改 index.html 的内容
 */
const pkg = require('../package.json');
const template = require('lodash').template;

module.exports = function indexTransform(params) {
  // console.log('indexTransform params\n\n', params);
  const isProd = ['production', 'prod'].includes(process.env.NODE_ENV);
  const htmlWebpackPlugin = {
    options: {
      title: pkg.title,
      appVersion: pkg.appVersion,
      pkg: pkg,
      nodeEnv: isProd ? 'prod' : 'dev',
      deployUrl: params.deployUrl || '/',
      gitCommit: process.env.GIT_COMMIT || '',
    },
  };
  const content = template(params.inputContent)({ htmlWebpackPlugin });
  // console.log('indexTransformed: \n\n', content);

  return content;
};

2.1.4 设置 npm 钩子脚本

package.jsonscripts 中添加如下内容:

{
  "scripts": {
    "postinstall": "node scripts/insert-to-cli-webpack.js"
  }
}

至此,当执行 npm install 后,则会自动执行该注入脚本。

2.2 使用第三方构建方案 ngx-build-plus

这是一个使用比较多的第三方构建方案。简单的 webpack 自定义通过 --extra-webpack-config 参数指定 webpack 配置文件即可。对于复杂的自定义需求,可以通过 --plugin 指定插件配置文件路径来实现。具体可参考项目 github 主页说明文档:

https://github.com/manfredsteyer/ngx-build-plus

2.3 使用第三方构建方案 @angular-builders/custom-webpack

这是我在升级 Angular8 后当前采用的方案。具体方法与步骤如下。

2.3.1 安装与配置 @angular-builders/custom-webpack

安装依赖:

yarn add -D @angular-builders/custom-webpack
# or
npm i -D @angular-builders/custom-webpack

修改 angular.json 中的配置。主要是将 builder 相关的项由 @angular-devkit/build-angular 改为 @angular-builders/custom-webpack 方案。搜索关键字并对照修改即可:

"builder": "@angular-builders/custom-webpack:[browser|server|karma|dev-server]"

2.3.2 实现自定义 webpack 配置

angular.json 中增加 customWebpackConfig 配置项,参考:

{
  "architect": {
    "build": {
      "builder": "@angular-builders/custom-webpack:browser",
      "options": {
        "customWebpackConfig": {
          "path": "./webpack.config.js"
        }
      }
    }
  }
}

新建配置文件 webpack.config.ts 文件,内容参考如下:

const webpack = require('webpack');
const pkg = require('./package.json');
const path = require('path');
const angularCompilerPlugin = require('@ngtools/webpack');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = (config, options) => {
  config.plugins.push(
    new webpack.DefinePlugin({
      APP_VERSION: JSON.stringify(pkg.version),
    })
    // new BundleAnalyzerPlugin()
  );

  config.module.rules.unshift(
    {
      test: /\.pug$/,
      use: [
        'raw-loader',
        {
          loader: 'pug-html-loader',
          options: {
            doctype: 'html',
          },
        },
      ],
    },
    {
      test: /\.html$/,
      loader: 'raw-loader',
      exclude: [path.resolve(__dirname, 'src/index.html')],
    }
  );

  // 设置 directTemplateLoading = fale
  // @link https://github.com/just-jeb/angular-builders/issues/465
  const index = config.plugins.findIndex(p => p instanceof angularCompilerPlugin.AngularCompilerPlugin);
  const oldOptions = config.plugins[index]._options;
  oldOptions.directTemplateLoading = false;
  config.plugins.splice(index);
  config.plugins.push(new angularCompilerPlugin.AngularCompilerPlugin(oldOptions));

  // console.log(config.module.rules, options);
  return config;
};

2.3.3 实现自定义 index.html

angular.json 文件的 architect.build.options 下增加配置项 indexTransform

{
  "indexTransform": "scripts/index-html-transform.js"
}

然后新建文件 scripts/index-html-transform.js,内容参考:

const pkg = require('../package.json');
const template = require('lodash').template;
const angularCfg = require('../angular.json');
const projectName = angularCfg.defaultProject || pkg.name;
const { configurations } = angularCfg.projects[projectName].architect.build;

// (options: TargetOptions, indexHtmlContent: string) => string|Promise<string>;
module.exports = (targetOptions, indexHtml) => {
  console.log('indexTransform params\n\n', targetOptions);
  const isProd = ['local', 'production', 'prod'].includes(targetOptions.configuration);
  const deployUrl = configurations[targetOptions.configuration] ? configurations[targetOptions.configuration].deployUrl : '/';

  const htmlWebpackPlugin = {
    options: {
      isProd,
      title: pkg.title,
      appVersion: pkg.appVersion,
      pkg: pkg,
      now: helpers.utils.dateFormat('yyyy-MM-dd hh:mm:ss', helpers.utils.getTimeByTimeZone(8)),
      nodeEnv: isProd ? 'prod' : 'dev',
      deployUrl: deployUrl || '/',
      gitCommit: process.env.GIT_COMMIT || '',
    },
  };
  const content = template(indexHtml)({ htmlWebpackPlugin });

  // console.log(htmlWebpackPlugin);
  // console.log('indexTransformed: \n\n', content);
  return content;
};

2.3.4 更多参考

关于 angular-builders 更多的信息请参考其仓库主页相关文档介绍:

https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack
https://github.com/just-jeb/angular-builders/blob/master/MIGRATION.MD

3 Angular8 升级相关问题及注意事项

3.1 报错 An unhandled exception occurred: Unexpected token: keyword (default)

原因: 代码压缩存在问题,主要是在生成 es5 的代码并压缩时出错。

以下几种方法可以临时解决该报错问题:

  • 方法一 暂时禁用代码压缩优化选项。将 angular.json 中的 optimization 设置为 flase
  • 方法二tsconfig.json 中的 target 值设置为 es5

3.2 无法自定义 index.html 的编译

angualr/cli 修改了默认首页的构建与编译方式,不再由 webpack 去编译输出。

angular-builders 提供了一个配置项 indexTransform,通过定义该配置项可以实现自行格式化处理 index.html 的输出。具体可参考:

https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack#index-transform

3.3 自定义 webpack 配置,html 类型文件的 loader 失效

关于该问题的讨论可参考 https://github.com/just-jeb/angular-builders/issues/465

可以修改 angularCompilerPlugin 插件的配置项 directTemplateLoading = fale 以解决。以使用 “ 自定义 webpack 配置为例:


module.exports = (config, options) => { // 设置 directTemplateLoading = fale // @link https://github.com/just-jeb/angular-builders/issues/465 const index = config.plugins.findIndex(p => p instanceof angularCompilerPlugin.AngularCompilerPlugin); const oldOptions = config.plugins[index]._options; oldOptions.directTemplateLoading = false; config.plugins.splice(index); config.plugins.push(new angularCompilerPlugin.AngularCompilerPlugin(oldOptions)); return config; };

3.4 Angular8 中关于 @ViewChild() 第二个参数的问题

Angular8 中对于 Viewchild 等需要传递第二个参数。

该参数应当如何设置?这里需要注意,简单来说,一般设置为 {static: true} 即可。但是如果组件或组件父级使用了 ngIf,则应当设置为 {static: false}

<div *ngIf="isShow">
  <lzwme-test #lzwmeTest></lzwme-test>
</div>
@ViewChild('lzwmeTest', {static: false}) lzwmeTest;

3.5 Angular8 中的路由懒加载写法

在 ng8 中可以用 import 方式书写路由懒加载,但需要在 tsconfig.json 中配置编译模式为 "module": "esNext"。示例:

loadChildren: () => import('app/routes/lzwme/test.module').then(mod => mod.TestModule)

3.6 使用了 ng-zorro-antd 框架,自定义 svg 图标的简单方案

ng-zorro-antd 组件库官方推荐的引入自定义 svg 图标方法为使用 iconfont 平台。但这种方式会使得增删时无法自动化处理。

参考其自带 svg 图标的方案,我们可以写一个脚本,读取自定义 svg 目录并生成与其自带图标方案一致的文件,由此实现自动化处理。具体方法参考如下:

添加依赖库 svgo: yarn add -D svgo

新建文件 script/generate-svg-for-icons-angular.js。内容参考:

const path = require('path');
const fs = require('fs');
const utils = require('./lib/utils');
const SVGO = require('svgo');
const svgo = new SVGO({
  plugins: [{ removeAttrs: { attrs: ['fill', 'class', 'xmlns'] } }, { removeXMLNS: true }, { removeStyleElement: true }, { removeUselessDefs: true }],
});
cosnt ROOTDIR = path.resolve(__dirname, '..');
const toCamelCase = s => {
  return String(s || '').replace(/\_(\w)/g, x => x.slice(1).toUpperCase());
}
const svgDir = path.resolve(ROOTDIR, 'src/svg-icon');
const svgTsDir = path.resolve(ROOTDIR, 'src/svg-ts');
const svgPrefix = 'lzwme';

async function build() {
  const rdir = fs.readdirSync(svgDir).filter(v => v.endsWith('.svg'));
  /** 暂存 index.ts 的内容 */
  const svgTsIndexList = [];
  let newCount = 0;

  if (!fs.existsSync(svgTsDir)) fs.mkdirSync(svgTsDir);

  for (let svgName of rdir) {
    svgTsIndexList.push(`export * from './${svgName.replace('.svg', '')}';`);

    // 文件已存在则不继续处理
    const svgTsFilePath = path.resolve(svgTsDir, svgName.replace('.svg', '.ts'));
    if (fs.existsSync(svgTsFilePath)) continue;

    // 读取 svg 内容并压缩处理
    const svgFilePath = path.resolve(svgDir, svgName);
    const svgContent = fs.readFileSync(svgFilePath, { encoding: 'utf8' });
    const svgContentOp = await svgo.optimize(svgContent);
    // console.log(svgContent);

    const svgTsExportName = toCamelCase(svgPrefix + '_' + svgName.replace('-', '_').replace('.svg', ''));
    const svgTsContent = `// This icon file is generated by scripts/generate-svg-for-icons-angular.js
// tslint:disable

import { IconDefinition } from '@ant-design/icons-angular';

export const ${svgTsExportName}: IconDefinition = {
  name: 'gtp-${svgName.replace('.svg', '')}',
  theme: 'outline',
  icon: '${svgContentOp.data}'
};
`;
    fs.writeFileSync(svgTsFilePath, svgTsContent);
    newCount++;
  }

  // console.log(svgTsIndexList.join('\n'));
  if (newCount) fs.writeFileSync(path.resolve(svgTsDir, 'index.ts'), svgTsIndexList.join('\n'), { encoding: 'utf8' });
  console.log(`处理完成! 当前自定义 svg 图标总数 ${svgTsIndexList.length} 个。本次新增 ${newCount} 个\n`);
}

build();

package.json 中增加执行该脚本的命令:

{
  "scripts": {
    "genSvgTs": "node scripts/generate-svg-for-icons-angular.js"
  }
}

将所有自定义的 svg 图标都放到 src/svg-icon 目录下,执行 npm run genSvgTs,则会生成相关文件至 src/svg-ts 目录。
根据 ng-zorro-antd 官方文档中静态引入的方案进行引入即可。部分代码参考:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { NgZorroAntdModule, NZ_ICONS, NZ_DATE_CONFIG } from 'ng-zorro-antd';
import { IconDefinition } from '@ant-design/icons-angular';
import * as LzwmeIcons from 'svg-ts';

const icons: IconDefinition[] = Object.keys(LzwmeIcons).map(key => LzwmeIcons[key]);

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule,
    NgZorroAntdModule,
  ],
  providers: [
    // { provide: NZ_ICON_DEFAULT_TWOTONE_COLOR, useValue: '#00ff00' }, // 不提供的话,即为 Ant Design 的主题色
    {
      provide: NZ_DATE_CONFIG,
      useValue: { firstDayOfWeek: 0, },
    },
    { provide: NZ_ICONS, useValue: icons },
  ],
  declarations: [],
  exports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule,
    NgZorroAntdModule,
  ],
})
export class SharedModule {}
点赞 (5)
  1. 小鱼上岸说道:
    Google Chrome 83.0.4103.61 Google Chrome 83.0.4103.61 Windows 10 x64 Edition Windows 10 x64 Edition

    :biggrin: 受益匪浅

  2. 夏日博客说道:
    Google Chrome 63.0.3239.132 Google Chrome 63.0.3239.132 Windows 7 x64 Edition Windows 7 x64 Edition

    只看懂了一个icon。 :smile:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code