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
,内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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 */ 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
,内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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
,内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** * 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
中添加如下内容:
1 2 3 4 5 | { "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
安装依赖:
1 2 3 | 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
配置项,参考:
1 2 3 4 5 6 7 8 9 10 11 12 | { "architect" : { "build" : { "builder" : "@angular-builders/custom-webpack:browser" , "options" : { "customWebpackConfig" : { "path" : "./webpack.config.js" } } } } } |
新建配置文件 webpack.config.ts
文件,内容参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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 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
:
1 2 3 | { "indexTransform" : "scripts/index-html-transform.js" } |
然后新建文件 scripts/index-html-transform.js
,内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 配置为例:
1 2 3 4 5 6 7 8 9 10 11 | <br>module.exports = (config, options) => { // 设置 directTemplateLoading = fale 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}
。
1 2 3 | < div * ngIf = "isShow" > < lzwme-test #lzwmeTest></ lzwme-test > </ div > |
1 | @ViewChild( 'lzwmeTest' , { static : false }) lzwmeTest; |
3.5 Angular8 中的路由懒加载写法
在 ng8 中可以用 import
方式书写路由懒加载,但需要在 tsconfig.json
中配置编译模式为 "module": "esNext"
。示例:
1 | 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
。内容参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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
中增加执行该脚本的命令:
1 2 3 4 5 | { "scripts" : { "genSvgTs" : "node scripts/generate-svg-for-icons-angular.js" } } |
将所有自定义的 svg 图标都放到 src/svg-icon
目录下,执行 npm run genSvgTs
,则会生成相关文件至 src/svg-ts
目录。
根据 ng-zorro-antd
官方文档中静态引入的方案进行引入即可。部分代码参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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。