1 升级至 Angular8
如果你使用的是 angular/cli
构建方案,执行 ng update
即可,该过程主要是更新 package.json
中的依赖。为了了解到底改变了哪些内容,个人更喜欢手动方式升级,方法参考如下:
- 首先全局安装
@angular/cli
:npm i -g @angular/cli
; - 然后创建一个新的项目
ng new ng8-demo
; - 对比测试项目
ng8-demo
目录中的package.json
文件,更新旧项目pacakge.json
文件中的依赖; - 对比
angular.json
、tslint.json
、tsconfig.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.json
的 scripts
中添加如下内容:
{
"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 {}
受益匪浅
只看懂了一个icon。