本篇文章通过案例介绍一下Angular CLI下的自定义Webpack配置方法和自定义loader处理的方法,希望对大家有所帮助!
1 Angular 使用自定义Webpack配置方法1.1 背景使用Angular CLI新建工程后,一键式的配置已经能满足大部分需求,但针对个体述求,可能会希望给webpack配置一些额外的loader或者plugins。【相关教程推荐:《angular教程》】
1.2 替换Builder实现外部配置webpackangular.json 暴露了多种Builder可以替换的接口,如果需要使用自定义webpack配置可以替换一下builder。 @angular-builders/custom-webpack
和 ngx-build-plus
都提供了对应的builder,查看npm的趋势custom-webpack用户比较多,这里以custom-webpack为例,介绍如何修改angular.json以用上自定义的webpack配置。
由于@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配置的情况下局部配置自己想要的功能。
在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负责了最后内容的呈现。
组件库的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/*": ["devui/*"] } } ... }
tsconfig.app.separate.json又继承了tsconfig.app.json并且覆写了path字段,
{ .... "compilerOptions":{ "paths": { "ng-devui": ["./publish"], "ng-devui/*": ["./publish/*"] } } ... }
所以当npm run start
(ng serve
)的时候,会直接从ts目录读取文件,直接走webpack构建,编译速度快;
当npm run build:prod
(ng build --prod --configuration separate
)的时候,会从组件构建的目录./publish/
下找寻npm包同目录结构的组件。