共计 12013 个字符,预计需要花费 31 分钟才能阅读完成。
提醒:本文最后更新于2020-12-30 14:09,文中所关联的信息可能已发生改变,请知悉!
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 {} |