如何自定义配置Angular CLI下的Webpack和loader处理
今天就跟大家聊聊有关如何自定义配置Angular CLI下的Webpack和loader处理,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
1 Angular 使用自定义Webpack配置方法
1.1 背景
使用Angular CLI新建工程后,一键式的配置已经能满足大部分需求,但针对个体述求,可能会希望给webpack配置一些额外的loader或者plugins。【相关教程推荐:《angular教程》】
1.2 替换Builder实现外部配置webpack
angular.json 暴露了多种Builder可以替换的接口,如果需要使用自定义webpack配置可以替换一下builder。 @angular-builders/custom-webpack
和 ngx-build-plus
都提供了对应的builder,查看npm的趋势custom-webpack用户比较多,这里以custom-webpack为例,介绍如何修改angular.json以用上自定义的webpack配置。
1.3 安装Builder的包
由于@angular-builders/custom-webpack
并不是ng官方的包,所以使用前都需要先安装一下:
npm install @angular-builders/custom-webpack
不同的ng版本需要安装对应不同的版本的包, ng的大部分库目前有一个约定俗成的好习惯,就是主版本号和ng的主版本号是能够对上的。比如使用的是ng12,那就用custom-webpack@12的版本。那么为什么需要这么多版本,原因是ng在自己的不同版本下的默认使用的@angular-devkit/build-angular
包的内容和结构甚至schema结构和位置可能会发生变化。对于custom-webpack来说更多是是继承build-angular的schema和代码,并暴露webpack的修改入口,让用户不需要了解整个webpack配置的情况下局部配置自己想要的功能。
1.4 配置方法
在angular.json文件中,替换@angular-devkit/build-angular
为@angular-builders/custom-webpack
, 主要包括browser、dev-server、karma等几个不同环节的builder,并增加配置参数
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
// 以下为新增的配置 customWebpackConfig
"customWebpackConfig": {
"path": "scripts/extra-webpack.config.js"
},
....
},
"configurations": ...
},
path可以按自己的工程来指定。 该文件可以导出一个函数(将会被调用)或者一段webpack配置(将会被Merge Options)。
从使用情况来说函数灵活性更好,可以直接操作整个webpack配置。示例文件内容
// extra-webpack.config.js
module.exports = (config) => {
// do something..
return config;
};
至此,webpack的扩展配置所需要的基础步骤就完成了。
2 使用自定义Webpack配置案例-loader篇
2.1 案例1:使用PostCSS插件来处理CSS降级插值(主题化降级)
背景
组件库主题化采用了css-var方案进行主题化定制,通过运行时替换样式:root里的css自定义属性的值来达到变更主题色的功能。对于IE来说它不认识也无法解析带var的值,那么它会表现为无颜色。为了尽量满足渐进增强和优雅退化。我们需要做一些兼容,以便IE无法使用主题化的情况下也能正常显示颜色。
目标:
color: var(--devui-brand, #5e7ce0);
->
color: #5e7ce0; color: var(--devui-brand, #5e7ce0);
上下文:
为了规范颜色的使用,库里使用的是scss变量来约束。如$devui-brand: var(--devui-brand, #5e7ce0)
, 本身这种写法是能满足现代浏览器的降级的,当找不到--devui-brand的css自定义属性,会回落到后面的色值,但是IE不认识var所以无法读出色值。组件的样式文件引用是定义文件然后直接使用$devui-brand
作为值,如下
@import '~ng-devui/styles-var/devui-var.scss';
.custom-class {
color: $devui-brand;
}
默认编译完为:
.custom-class {
color: var(--devui-brand, #5e7ce0);
}
解决方案
既然已经知道目标了,那么这件事情就变得简单多了,通过插桩(console.log)查看默认NG工程启动的webpack配置,可以看到module里有两个rule是负责处理SCSS和SASS文件的,
它们都拥有test: /\.scss$|\.sass$/
字段,一个负责全局的scss的编译(通过include字段指定了配置在angular.json的style的路径集合),一个负责全局以外的组件内引用的scss的处理(通过exclude字段排除了前面全局已经处理过的scss)。
通常第一个想法可能是处理sass,遇到$devui-brand
的地方前面插入一句它的原始值。但是由于sass变量本身可能被二次赋值,如$my-brand: $devui-brand; color: $my-brand;
,这时候遇到$devui-brand
的就插值的显然不合适,重复的定义$my-brand只是会最后一个值生效。
换个思路,当scss展开为css之后,每个取值的位置就是确定的了,哪怕二次赋值的地方也是同一个终值了。这时候就可以采用脚本来写IE的降级,也就是目标所写的内容。
那么,我们可以再sass-loader处理完之后增加一个loader来处理这段css。对css的处理使用PostCSS能对语法结构进行走查更严谨。
最后修改代码如下:
// webpack-config-add-theme.js
function webpackConfigAddThemeSupportForIE(config) {
[{
ruleTest: /\.scss$|\.sass$/,
loaderName: 'sass-loader'
}, {
ruleTest: /\.less$/,
loaderName: 'less-loader'
}].forEach(({ruleTest, loaderName}) => {
config.module.rules.filter(rule => rule.test + '' === ruleTest + '').forEach((styleRule) => {
if (styleRule) {
var insertPosition = styleRule.use.findIndex(loaderUse => loaderUse.loader === loaderName
|| loaderUse.loader === require.resolve(loaderName));
if (insertPosition > -1) {
styleRule.use.splice(insertPosition, 0, {
loader: 'postcss-loader',
options: {
sourceMap: styleRule.use[insertPosition].options.sourceMap,
plugins: () => {
return [
require('./add-origin-varvalue'),
];
}
}
});
}
}
});
});
return config;
};
module.exports = webpackConfigAddThemeSupportForIE;
代码大致逻辑为寻找test为less/sass正则的rule,在对应的use里的loader里找到less-loader/sass-loader的位置,然后在其数组位置前面增加一个postcss-loader,loader里使用了自定义的add-origin-varvalue的PostCSS插件。(备注:这里有一块逻辑是找到sass-loader的位置, 这里有两个等式是因为ng7,8和ng9用户的loader写法不一样了,之前ng7用字符串,后面ng9用的是文件路径)
PostCSS插件如下:
var postcss = require('postcss');
var varStringJoinSeparator = 'devui-(?:.*?)';
var cssVarReg = new RegExp('var\\(\\-\\-(?:' + varStringJoinSeparator + '),(.*?)\\)', 'g');
module.exports = postcss.plugin('postcss-plugin-add-origin-varvalue', () => {
return (root) => {
root.walkDecls(decl => {
if (decl.type !== 'comment' && decl.value && decl.value.match(cssVarReg)) {
decl.cloneBefore({value: decl.value.replace(cssVarReg, (match, item) => item) });
}
});
}
});
代码的大致逻辑如下,通过postcss.plugin定义了一个插件,该插件遍历css每一条declarion(声明),如果不是注释,且它的值(对于每一条css声明来说冒号左边称为property,postcss里为decl.prop;右边称为value,postcss里为decl.value)刚好匹配了正则规则(这里的正则规则为--devui-开头),则在这条规则的前面插入该规则且把值替换为原规则逗号后面的值。
最后挂载到extra-webpack-config里
// extra-webpack.config.js
const webpackConfigAddTheme = require('./webpack-config-add-theme');
module.exports = (config) => {
return webpackConfigAddTheme(config);
};
至此我们达成了我们的目标,而且对插值的范围做了限定,限定为--devui
开头的才需要插值,避免其他不想被处理的var被处理了。
要点:
找准CSS处理的位置, sass存在变量依赖问题,更适合在编译后的css文件里处理
掌握PostCss插件的简单写法, sourceMap选项维持不变
注意loader的处理顺序,是从use里的最后一个loader接收原始数据不断往前面的loader传递,最前面的loader负责了最后内容的呈现。
2.2 案例2: 读取配置上下文处理:SCSS/LESS 处理TS别名路径同步处理 (别名模块联调)
背景
组件库的demo对组件的引用,我们通过tsconfig里的alias实现了ts的别名引用,并在网站生产构建阶段采用了分开构建,先构建库,然后配置另外的tsconfig指向了构建完的库(不再直接指向源码)。
一方面使得demo看起来用法和业务一致,另一方面分开构建实现生产端组件库的demo的使用方法和业务使用方法完全一致,减少因为webpack构建和ng-packagr构建出来后一些细微差别导致问题没有提前暴露出来。
这些通过tsconfig和配置build的不同的configuration已经可以实现了,但是仅仅只适用于ts文件,导出的scss文件/less文件就不生效了(由于支持外部主题化变量使用,scss文件和less文件会导出)。
目标: sass、less文件实现ts别名一样的引用路径。
上下文:
现有angular.json里配置了两个configuration,一个是使用默认的tsconfig.app.json,一个是分开构建的tsconfig.app.separate.json。
angular.json如下:
tsconfig.app.json 继承了tsconfig.json有如下别名配置
{
....
"compilerOptions":{
"paths": {
"ng-devui": ["devui/index.ts"],
"ng-devui
function lessReplacePathAlias(css, aliasMap, sourcePath) {
const replacePathAlias = postcss.plugin('postcss-plugin-replace-path-alias', () => {
return (root) => {
root.walkAtRules(atRule => {
if (atRule.import && atRule.filename) {
const oFilename = atRule.filename.substring(1, atRule.filename.length - 1); // 去掉头尾单引号双引号
const rule = aliasMap.filter(rule => rule.aliasReg.test(oFilename)).pop();
if (rule) {
const prefix = rule.pathPrefixes[0]; // 取第一个路径忽略剩余的
const filename = path.resolve(prefix, oFilename.replace(rule.aliasReg, (item, match) => match));
const relativePath = path.relative(sourcePath.replace(trailingSlashAndContent, ""), filename).split(path.sep).join('/');
var realPathAtRule = atRule.clone({ params: (atRule.options || '' ) + " '" + relativePath + "'", filename: "'" + relativePath + "'"});
atRule.replaceWith(realPathAtRule);
}
}
});
}
});
return postcss([replacePathAlias]).process(css, { syntax: postcssLessSyntax }).css;
}
function process(source, map) {
this.cacheable && this.cacheable();
// 获取配置文件里的主题数据
const aliasMap = getOptionsFromConfig(this).aliasMap;
let newSource = source;
if (aliasMap.length > 0) {
newSource = lessReplacePathAlias(source, aliasMap, this.resourcePath);
}
// 返回结果
this.callback(null, newSource, map);
return newSource;
}
exports.default = process;
这里自定义一个wepack-loader的写法,实际上也是可以用postcss-loader搭配自定义replacePathAlias 的plugin 和 postcss-less的syntax进行使用。这里演示了wepack-loader的写法。
代码大意是定义了一个loader的optionSchema用于校验选项,process函数获取option里的路径别名数据之后,如果路径别名有数据则用postcss对代码进行处理。注意在process里this指向webpack的上下文,所以可以从resourcePath里获取当前文件路径。
代码核心为中间的postcss插件, 通过遍历@开头的规则,如果是一个import声明则读取文件名去掉头尾的单双引号;测试是否文件名命中了规则中的任意一条,命中则取第一条,和sass-loader处理的一样得到了文件的绝对路径,然后通过path.relative重新计算出和当前文件的相对路径。然后将这条@import规则替换成新的文件地址。
实际上这个思路同样适用于sass规则的处理,只需要把语法syntax换成postcss-sass。
我们可以看到前两个大案例都是在处理css类问题的,大部分时候处理都可以用上postcss利器。直接去操作css内容容易误修改内容,而经过AST语法树拆解后的遍历会更稳当一些。
说一下为什么会同时使用sass和less。一般工程是不会同时使用两种的。实际上我们的工程主要也是使用sass。但是对于一个打包后的组件库来说,业务使用的时候是不会感知它是sass还是less的,甚至也不会提供sass或者less文件给业务引用。这里是为了主题化的能力能够对外辐射,组件库同时提供了sass和less的版本变量供使用。
后记
这两个样式编译器loader支持路径别名的脚本最早写于2020年8月。
Webpack官方在sass-loader/less-loader的使用文档里面都说明了,~语法已经废除,建议删除。
sass-loader@11.0.0(2021-2-5),less-loader@8.0.0(2021-2-1)分别发布了对应版本声明~已经标记为Deprecated。
作为历史解决方案,这个案例依然会放在这里,提供一些解决思路。
笔者认为能~和现在的回落解决方案还是不一样的,尤其当存在同名文件的时候(目前这种情况会比较少,少有人使用模块同名路径作为相对目录路径),波浪线方案仍然能明显强调出文件的第一指向。
Webpack官方在2020年8月底开始给less-loader也加上了webpackImporter的选项,进一步屏蔽less-loader和sass-loader之间的差异。兼容历史原因,这两个loader目前还会保留波浪线语法。由于项目ng版本滞后于NG官方版本,NG官方版本使用的loader又滞后于webpack官方版本。目前NG9版本仍在用less-loader@5.0.0,sass-loader@8.0.2。
要点:
从运行时获取tsconfig配置项
文件路径处理(scss、less)
掌握loader的写法,接收参数。
更多解法补充
resolve.alias
Webpack配置中 config.resolve.alias 也是一个配置别名的地方, 而且sass-loader/less-loader也支持了webpackImporter的配置,其实可以直接通过修改config中的alias就能达成目的。 比如:
config.resolve.alias = {
... config.resolve.alias,
'ng-devui': path.resolve('./devui/'),
})
resolve.plugins:TsConfigPathsPlugin
那么如果webpack的resolve alias已经支持了,是不是tsconfig就可以不用配置,或者怎么配置成一份?
不幸的是 Angular工程的 webpack 和 tsconfig不是同步的,两边需要同时配置,否则会出现找不到模块的构建错误, 如果两者配置不同,隐患会更大,因为tsconfig的配置是直接影响tsc编译器的拿不到文件,webpack也会解读出一份等。
配置成一份可以通过写webpack-plugin在运行时拿到当前的tsconfig再处理数据塞到alias里。因为如果直接修改config,那么它是静态的,实际情况还是需要获取到ng工程当前的ts配置是什么文件会比较好。
Awesome-typescript-loader有个TsConfigPathsPlugin,使用也非常简单。
const { TsConfigPathsPlugin } = require('awesome-typescript-loader');
module.exports = (config) => {
config.resolve.plugins = [
... (config.resolve.plugins || []),
new TsConfigPathsPlugin()
]
};
给resolve.plugins塞一个TsConfigPathsPlugin就可以把tsconfig里的,不过这个插件已经存档了,最后一个版本是3年前的了。这里我们做了一个测试,仍然是不支持动态的tsconfig,它的代码可以参考用来写一个resolve的plugin, 动态的tsconfig的path获取可以参考我们之前的操作。
最后补充几个新的结论
Webpack config的resolve.alias和 tsconfig的alias不是同步的,需要配置两份或者找到一个方法同步。
Sass-loader 和less-loader的内部引用都能走webpack的resolve.alias, 可以说resolve.alias的解法会比 在改sass-loader的importer或者在less-loader前先处理 通用性更好,基本上所有blob都可以尝试去这样解决路径别名问题。更多需要注意的仍然是动态的tsconfig的配置获取问题。
2.3 案例3:修改TerserPlugin排除项,摇树处理问题(摇树问题)
背景
组件库的业务使用方刚升级ng7的时候,打包经常出问题,经常出现文件摇树之后,很多自执行命令被摇树认为无副作用摇树掉了。
目标:不关掉全局摇树的情况下,针对个别目录进行摇树的问题排除。
上下文:
Angular.json里有个配置,默认为打开,打开之后可以对js类型的代码进行摇树优化,从而减小打包体积。
Terser是从Uglifyjs这个库fork 出来的项目用来支持ES6语法,Angular 编译阶段用它来压缩js代码。
Angular使用TerserPlugin来进行摇树,由于装饰器等问题导致摇树效果不理想的问题(相关讨论内容见Angular摇树如何工作),angular提供了一个专门用于标记纯函数的注释的优化器,所以对于默认的Angular项目来说,摇树是一个两阶段模型。
第一个阶段是Angular在生产构建阶段(ng build --prod
)在解析资源的时候加入了一个@angular_devkit/build_optimizer/webpack-loader
,用于标记js文件里的无用代码;angular_devkit/build_optimizer 介绍它的主要功能为标记功能。
第二个阶段是Angular在生成打包的时候给Webpack配置了 optimization.minimizer数组塞入了TerserPlugin然后把无副作用的无引用代码摇掉。(准确说是塞入了两个Plugin,一个针对globalScript,一个排除globalScript。GlobalScript是指在angular.json的build的script字段配置的路径。)
解决方案
TerserPlugin本身是有一个include和exclude字段(见API)可以用正则和字符串来排除。
function terserOptionsWebpackConfig(config) {
let excludeList = [
// 此处可以填自己要排除的目录
]
let minimizerArr = config.optimization.minimizer;
let terserPlugins = minimizerArr
.filter(plugin => plugin.options && plugin.options.terserOptions)
terserPlugins.forEach(terserPlugin => {
if (terserPlugin.plugin.exclude) {
const isArray = Array.isArray(terserPlugin.plugin.exclude)
if (isArray) {
terserPlugin.plugin.exclude = [
...terserPlugin.plugin.exclude,
...excludeList
];
} else {
terserPlugin.plugin.exclude = [
terserPlugin.plugin.exclude,
...excludeList
];
}
} else {
terserPlugin.plugin.exclude = excludeList;
}
});
return config;
};
module.exports = terserOptionsWebpackConfig;
之前在ng7工程会遇到比较多的摇树问题,有些升级到ng9之后默认配置有点变化之后就没有摇树问题了。包括IVY打包模式下编译引擎实际上摇树的方式不太一样,有些文件不会被摇掉了,这个问题还是要遇到具体问题具体分析来解决。必要时可以重新new一个TerserPlugin但是要保持它的原来的options不变。
要点:
摇树具体的阶段,拦截问题
维持原有的Options,不对其进行破坏。
2.4 案例4: 三方ES2015库自动babel化处理(仅有ES2015的库处理)
背景
highlight.js升级到10.0.0之后,官方就开始不再默认支持ie11了,导出的包也只有es2015的包。之前的业务需要为了兼容IE11,我们需要对highlight.js进行一次babel。更新到ng9之后,其实ng默认会仅打包es2015然后进行差分打包到es5,所以实际情况在生产打包是没有问题的,但是在本地开发的ie11调试环节会出现问题,比如class的语法ie不认识等等,导致整个js无法加载。
目标:让不支持es5的包在开发态支持es5。
上下文:由于之前组件库9的版在一段时间内仍然需要支持ie11,也就经常需要开发态下到ie11下debug,所以开发态下的highlight.js导致ie无法访问问题需要解决。
解决方案
首先es2015转es5经典的做法就是让babel帮忙处理。然后大部分的ES语法新增的api可以由core-js来解决,剩下的IE11还有一些浏览器端DOM的API的实现还需要添加一些polyfill才行。Polyfill可以用到了什么api就加什么api,具体可以从这里参考,本文不再累述。
// babel-loader-wepack-config.js
const path = require('path');
const ts = require('typescript');
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const readConfiguration = require('@angular/compiler-cli').readConfiguration;
const ES6_ONLY_THIRD_PARTY_LIST = require('./es6-only-third-party-list');
function getAngularCompilerTsConfigPath(config) {
const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop();
if (angularCompilerPlugin) {
return angularCompilerPlugin.options.tsConfigPath;
}
return undefined;
}
function getTsconfigCompileTarget(tsconfigPath) {
const {target} = readConfiguration(path.resolve(tsconfigPath)).options;
return target;
}
function webpackConfigAddBabel2ES5(config, list = []) {
const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json';
const target = getTsconfigCompileTarget(tsconfigPath);
if (target === ts.ScriptTarget.ES5) {
config.module.rules.push({
test: /\.js$/,
use: [{
loader: 'babel-loader'
}],
include: [
...ES6_ONLY_THIRD_PARTY_LIST,
...list
]
});
}
return config;
};
module.exports = webpackConfigAddBabel2ES5;
// es6-only-third-party-list.js
const path = require('path');
const ES6_ONLY_THIRD_PARTY_LIST = [
path.resolve('./node_modules/highlight.js') // ^10.0.0 no longer support ie 11
];
module.exports = ES6_ONLY_THIRD_PARTY_LIST;
两段代码大概思路就是从tsconfig里面读,如果目标为es5,则塞进去babel-loader,然后把对应的三方库的路径放到include里边。
ES6_ONLY_THIRD_PARTY_LIST 列表示意了highlight.js的路径应该怎么写。如果编译目标为es2015,则这段处理就不需要了不会被插入,哪怕是差分打包也不会调用它,ng-cli会自行调用内部逻辑。
后记
IE11已经慢慢退出了历史舞台,各大网站也开始声明不再支持IE11,这些冗余的插件已经可以慢慢移除。包括ng12起也不再承诺支持ie,升级到ng12之后这些插件也没有必要了。
要点:
分清语法API的降级和浏览器BOM/DOM垫片
提供一个可维护的列表
针对tsconfig的target上下文进行编译。
2.5 案例5: 拦截修改自动生成ts内容(内容自动扫描生成)
案例5-1: 自动扫描目录
背景
在可视化拖拽生产力平台项目,组件定义目录会有一系列重复雷同的目录结构,最后需要汇总到一个ts里作为全局信息入口。
目标:自动扫描组件定义目录,汇总信息到ts里,避免手动增加信息维护。
上下文:
生成的信息汇总内容结构为:
class="lazy" data-src/app/connect/connet.ts
目录的结构为:
class="lazy" data-src/component-lib
要求不要包含 _目录的内容
解决方案
const fs = require('fs').promises;
const path = require('path');
async function listDir() {
return fs.readdir(path.resolve('./class="lazy" data-src/component-lib')).then(dirs => dirs.filter(item => !item.startsWith('_'))); // 过滤_开头的
}
function genConnectInfo(dirArr) {
return `export const ConnectInfo = {
${dirArr.map(item =>"'"+ item +"': import( 'class="lazy" data-src/component-lib/" + item + "/connect')").join(`,
`)}
};`;
}
async function process() {
var list = await listDir();
return genConnectInfo(list);
}
module.exports = function(content, map, meta) {
var callback = this.async();
this.addContextDependency(path.resolve('./class="lazy" data-src/component-lib')); // 自动扫描目录,但是删除目录可能会引起报错
process().then((result)=> {
callback(null, result, map, meta);
}, err => {
if (err) return callback(err);
});
};
const path = require('path');
function webpackConfigAddScanAndGenerateConnectInfo(config) {
config.module.rules.push({
test: /connect\.ts$/,
use: [{
loader: require.resolve('./scan-n-gen-connect-webpack-loader')
}],
include: [
path.resolve('./class="lazy" data-src/app/connect')
],
enforce: 'post',
});
return config;
};
module.exports = webpackConfigAddScanAndGenerateConnectInfo;
通过简单的一个loader 将目录扫描内容组装返回给class="lazy" data-src/app/connect/connet.ts。
这里有几个要点:
这里需要使用loader的
enforce: 'post'
属性,因为ts的编译最后loader是直接去文件系统读取内容的不是从上一个loader拿到结果继续往下处理的(不符合loader的规范,但是性能会更好),所以这里需要把阶段属性配置为'post',确保最后编译的内容是我们生成的内容。对loader添加一些依赖可以在目录结构变化的时候刷新内容,否则就只能等下一次启动的时候获取内容,即这一行
this.addContextDependency(path.resolve('./class="lazy" data-src/component-lib'));
由于webpack的tsc编译是文件分析依赖型的,我们动态生成的文件内容,webpack就无法从中分析依赖了另外一些ts内容,导致其他ts内容不会走ts编译。这时候可以参考下面,在tsconfing加一下include字段,解决编译问题。
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"class="lazy" data-src/main.ts",
"class="lazy" data-src/polyfills.ts"
],
"include": [
"class="lazy" data-src*.d.ts",
"class="lazy" data-src/component-lib*.ts"
]
}
要点:
自动扫描逻辑
添加依赖
添加非直接依赖的编译入口
案例5-2: 自动扫描css内容
背景
图标库导出了一系列可用字体图标,图标通过不同的css类名来引用,现在要做一个图标选择器,需要把所有图标列出来。
目标:分析icon.css,提取所有有效的icon名字。
上下文:
需要把信息自动生成到一个文件叫 class="lazy" data-src/app/properties-panel/properties-control/icon-picker/icon-library.data.ts
格式为一个数组
图标库的文件为./node_modules/@devui-design/icons/icomoon/devui-icon.css
格式为:
图片里红色标记的就是要提取出来的图标名。
解决方案
loader的定义
// auto-gen-icon-data-webpack-loader.js
const fs = require('fs').promises;
const path = require('path');
function genIconData(fileContent) {
const iconNames = [...fileContent.matchAll(/\.icon-(.*?):before/g)].map(item => item[1]);
return `export const ICON_DATA = [
${iconNames.map(item => "'" + item + "'").join(",")}
];
`;
}
async function process(file) {
const content = await fs.readFile(`${path.resolve(file)}`, 'utf8');
return genIconData(content);
}
module.exports = function(content, map, meta) {
const file = './node_modules/@devui-design/icons/icomoon/devui-icon.css';
var callback = this.async();
this.addDependency(path.resolve(file));
process(file).then((result)=> {
callback(null, result, map, meta);
}, err => {
if (err) return callback(err);
});
};
webpack config里塞入loader
const path = require('path');
function webpackConfigAddGenIconData(config) {
config.module.rules.push({
test: /icon-library.data\.ts$/,
use: [{
loader: require.resolve('./auto-gen-icon-data-webpack-loader')
}],
include: [
path.resolve('./class="lazy" data-src/app/properties-panel/properties-control/icon-picker')
],
enforce: 'post',
});
return config;
};
module.exports = webpackConfigAddGenIconData;
代码相对就比较简单了。
要点:
1、分析图标库文件结构,排除一些对齐的干扰项
2.6 补充:自定义Webpack配置loader的调试方法
配置项调试:在方法中进行打印调试, 通常在执行前就已经可以打印出配置项相关的内容。
内容调试:使用简单的loader打印前后经过loader的内容, 自定义一个打印内容的loader,在执行自定义loader前后都打印一下,可以获得对比内容。
一些经验: 修改loader的时候要注意本地开发阶段和生产打包阶段,不要顾此失彼。
文章介绍了如何在AngularCLI生成的工程里使用自定义的webpack设置,并且举了几个实际情况下为了解决问题修改的webpack配置,覆盖css的修改、js的修改以及拦截内容自动扫描生成。
本文主要为解决问题或者功能特性去修改webpack配置,没有涉及复杂的构建速度优化、性能优化等。
有些内容是旧版本处理IE11问题做兼容的,具体的实践不再具备复制方案就能解决现实问题的意义,更多的是提供一些思路和参考。
Webpack目前几个版本始终是从入口文件开始,通过规则配置对文件进行加载解析,通过插件拦截整个构建生命周期做一些抽取变换。
大部分时候Angular的默认配置已经能满足需求,当和三方库集成、自动化处理结合的时候,采用自定义的配置可以解决问题,节省工作甚至进一步优化性能等等。
看完上述内容,你们对如何自定义配置Angular CLI下的Webpack和loader处理有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注编程网行业资讯频道,感谢大家的支持。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341