溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何自定義配置Angular CLI下的Webpack和loader處理

發(fā)布時(shí)間:2021-10-09 11:30:36 來(lái)源:億速云 閱讀:216 作者:柒染 欄目:web開(kāi)發(fā)

今天就跟大家聊聊有關(guān)如何自定義配置Angular CLI下的Webpack和loader處理,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

1 Angular 使用自定義Webpack配置方法

1.1 背景

使用Angular CLI新建工程后,一鍵式的配置已經(jīng)能滿足大部分需求,但針對(duì)個(gè)體述求,可能會(huì)希望給webpack配置一些額外的loader或者plugins?!鞠嚓P(guān)教程推薦:《angular教程》】

1.2 替換Builder實(shí)現(xiàn)外部配置webpack

angular.json 暴露了多種Builder可以替換的接口,如果需要使用自定義webpack配置可以替換一下builder。 @angular-builders/custom-webpackngx-build-plus都提供了對(duì)應(yīng)的builder,查看npm的趨勢(shì)custom-webpack用戶比較多,這里以custom-webpack為例,介紹如何修改angular.json以用上自定義的webpack配置。

1.3 安裝Builder的包

由于@angular-builders/custom-webpack并不是ng官方的包,所以使用前都需要先安裝一下:

npm install @angular-builders/custom-webpack

不同的ng版本需要安裝對(duì)應(yīng)不同的版本的包, ng的大部分庫(kù)目前有一個(gè)約定俗成的好習(xí)慣,就是主版本號(hào)和ng的主版本號(hào)是能夠?qū)ι系?。比如使用的是ng12,那就用custom-webpack@12的版本。那么為什么需要這么多版本,原因是ng在自己的不同版本下的默認(rèn)使用的@angular-devkit/build-angular包的內(nèi)容和結(jié)構(gòu)甚至schema結(jié)構(gòu)和位置可能會(huì)發(fā)生變化。對(duì)于custom-webpack來(lái)說(shuō)更多是是繼承build-angular的schema和代碼,并暴露webpack的修改入口,讓用戶不需要了解整個(gè)webpack配置的情況下局部配置自己想要的功能。

1.4 配置方法

在angular.json文件中,替換@angular-devkit/build-angular@angular-builders/custom-webpack, 主要包括browser、dev-server、karma等幾個(gè)不同環(huán)節(jié)的builder,并增加配置參數(shù)

 "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
               // 以下為新增的配置 customWebpackConfig
               "customWebpackConfig": {
                     "path": "scripts/extra-webpack.config.js"
               },
              ....
           },
          "configurations": ...
 },

path可以按自己的工程來(lái)指定。 該文件可以導(dǎo)出一個(gè)函數(shù)(將會(huì)被調(diào)用)或者一段webpack配置(將會(huì)被Merge Options)。

從使用情況來(lái)說(shuō)函數(shù)靈活性更好,可以直接操作整個(gè)webpack配置。示例文件內(nèi)容

// extra-webpack.config.js
module.exports = (config) => {
    //  do something..
   return config;
};

至此,webpack的擴(kuò)展配置所需要的基礎(chǔ)步驟就完成了。

2 使用自定義Webpack配置案例-loader篇

2.1 案例1:使用PostCSS插件來(lái)處理CSS降級(jí)插值(主題化降級(jí))

背景

組件庫(kù)主題化采用了css-var方案進(jìn)行主題化定制,通過(guò)運(yùn)行時(shí)替換樣式:root里的css自定義屬性的值來(lái)達(dá)到變更主題色的功能。對(duì)于IE來(lái)說(shuō)它不認(rèn)識(shí)也無(wú)法解析帶var的值,那么它會(huì)表現(xiàn)為無(wú)顏色。為了盡量滿足漸進(jìn)增強(qiáng)和優(yōu)雅退化。我們需要做一些兼容,以便IE無(wú)法使用主題化的情況下也能正常顯示顏色。

目標(biāo):

color: var(--devui-brand, #5e7ce0); 
-> 
color: #5e7ce0; color: var(--devui-brand, #5e7ce0);

上下文:

為了規(guī)范顏色的使用,庫(kù)里使用的是scss變量來(lái)約束。如$devui-brand: var(--devui-brand, #5e7ce0), 本身這種寫(xiě)法是能滿足現(xiàn)代瀏覽器的降級(jí)的,當(dāng)找不到--devui-brand的css自定義屬性,會(huì)回落到后面的色值,但是IE不認(rèn)識(shí)var所以無(wú)法讀出色值。組件的樣式文件引用是定義文件然后直接使用$devui-brand作為值,如下

@import '~ng-devui/styles-var/devui-var.scss';
.custom-class {
  color: $devui-brand;
}

默認(rèn)編譯完為:

.custom-class {
  color: var(--devui-brand, #5e7ce0);
}

解決方案

既然已經(jīng)知道目標(biāo)了,那么這件事情就變得簡(jiǎn)單多了,通過(guò)插樁(console.log)查看默認(rèn)NG工程啟動(dòng)的webpack配置,可以看到module里有兩個(gè)rule是負(fù)責(zé)處理SCSS和SASS文件的, 它們都擁有test: /\.scss$|\.sass$/字段,一個(gè)負(fù)責(zé)全局的scss的編譯(通過(guò)include字段指定了配置在angular.json的style的路徑集合),一個(gè)負(fù)責(zé)全局以外的組件內(nèi)引用的scss的處理(通過(guò)exclude字段排除了前面全局已經(jīng)處理過(guò)的scss)。

通常第一個(gè)想法可能是處理sass,遇到$devui-brand的地方前面插入一句它的原始值。但是由于sass變量本身可能被二次賦值,如$my-brand: $devui-brand; color: $my-brand;,這時(shí)候遇到$devui-brand的就插值的顯然不合適,重復(fù)的定義$my-brand只是會(huì)最后一個(gè)值生效。

換個(gè)思路,當(dāng)scss展開(kāi)為css之后,每個(gè)取值的位置就是確定的了,哪怕二次賦值的地方也是同一個(gè)終值了。這時(shí)候就可以采用腳本來(lái)寫(xiě)IE的降級(jí),也就是目標(biāo)所寫(xiě)的內(nèi)容。

那么,我們可以再sass-loader處理完之后增加一個(gè)loader來(lái)處理這段css。對(duì)css的處理使用PostCSS能對(duì)語(yǔ)法結(jié)構(gòu)進(jìn)行走查更嚴(yán)謹(jǐn)。

最后修改代碼如下:

// 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,在對(duì)應(yīng)的use里的loader里找到less-loader/sass-loader的位置,然后在其數(shù)組位置前面增加一個(gè)postcss-loader,loader里使用了自定義的add-origin-varvalue的PostCSS插件。(備注:這里有一塊邏輯是找到sass-loader的位置, 這里有兩個(gè)等式是因?yàn)閚g7,8和ng9用戶的loader寫(xiě)法不一樣了,之前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) });
      }
    });
  }
});

代碼的大致邏輯如下,通過(guò)postcss.plugin定義了一個(gè)插件,該插件遍歷css每一條declarion(聲明),如果不是注釋,且它的值(對(duì)于每一條css聲明來(lái)說(shuō)冒號(hào)左邊稱為property,postcss里為decl.prop;右邊稱為value,postcss里為decl.value)剛好匹配了正則規(guī)則(這里的正則規(guī)則為--devui-開(kāi)頭),則在這條規(guī)則的前面插入該規(guī)則且把值替換為原規(guī)則逗號(hào)后面的值。

最后掛載到extra-webpack-config里

// extra-webpack.config.js
const webpackConfigAddTheme = require('./webpack-config-add-theme');
module.exports = (config) => {
   return webpackConfigAddTheme(config);
};

至此我們達(dá)成了我們的目標(biāo),而且對(duì)插值的范圍做了限定,限定為--devui開(kāi)頭的才需要插值,避免其他不想被處理的var被處理了。

要點(diǎn):

  • 找準(zhǔn)CSS處理的位置, sass存在變量依賴問(wèn)題,更適合在編譯后的css文件里處理

  • 掌握PostCss插件的簡(jiǎn)單寫(xiě)法, sourceMap選項(xiàng)維持不變

  • 注意loader的處理順序,是從use里的最后一個(gè)loader接收原始數(shù)據(jù)不斷往前面的loader傳遞,最前面的loader負(fù)責(zé)了最后內(nèi)容的呈現(xiàn)。

2.2 案例2: 讀取配置上下文處理:SCSS/LESS 處理TS別名路徑同步處理 (別名模塊聯(lián)調(diào))

背景

組件庫(kù)的demo對(duì)組件的引用,我們通過(guò)tsconfig里的alias實(shí)現(xiàn)了ts的別名引用,并在網(wǎng)站生產(chǎn)構(gòu)建階段采用了分開(kāi)構(gòu)建,先構(gòu)建庫(kù),然后配置另外的tsconfig指向了構(gòu)建完的庫(kù)(不再直接指向源碼)。

一方面使得demo看起來(lái)用法和業(yè)務(wù)一致,另一方面分開(kāi)構(gòu)建實(shí)現(xiàn)生產(chǎn)端組件庫(kù)的demo的使用方法和業(yè)務(wù)使用方法完全一致,減少因?yàn)閣ebpack構(gòu)建和ng-packagr構(gòu)建出來(lái)后一些細(xì)微差別導(dǎo)致問(wèn)題沒(méi)有提前暴露出來(lái)。

這些通過(guò)tsconfig和配置build的不同的configuration已經(jīng)可以實(shí)現(xiàn)了,但是僅僅只適用于ts文件,導(dǎo)出的scss文件/less文件就不生效了(由于支持外部主題化變量使用,scss文件和less文件會(huì)導(dǎo)出)。

目標(biāo): sass、less文件實(shí)現(xiàn)ts別名一樣的引用路徑。

上下文:

現(xiàn)有angular.json里配置了兩個(gè)configuration,一個(gè)是使用默認(rèn)的tsconfig.app.json,一個(gè)是分開(kāi)構(gòu)建的tsconfig.app.separate.json。

angular.json如下:

如何自定義配置Angular CLI下的Webpack和loader處理

tsconfig.app.json 繼承了tsconfig.json有如下別名配置

{
   ....
"compilerOptions":{
    "paths": {
      "ng-devui": ["devui/index.ts"],
      "ng-devui/*": ["devui/*"]
    }
}
...
}

如何自定義配置Angular CLI下的Webpack和loader處理

tsconfig.app.separate.json又繼承了tsconfig.app.json并且覆寫(xiě)了path字段,

{
   ....
"compilerOptions":{
    "paths": {
      "ng-devui": ["./publish"],
      "ng-devui/*": ["./publish/*"]
    }
}
...
}

如何自定義配置Angular CLI下的Webpack和loader處理

所以當(dāng)npm run startng serve)的時(shí)候,會(huì)直接從ts目錄讀取文件,直接走webpack構(gòu)建,編譯速度快;

當(dāng)npm run build:prodng build --prod --configuration separate)的時(shí)候,會(huì)從組件構(gòu)建的目錄./publish/下找尋npm包同目錄結(jié)構(gòu)的組件。

以上就是整個(gè)不同環(huán)境采用不同ts配置達(dá)到不同的構(gòu)建,可以看出來(lái)ts別名在這里起到非常大的作用。

然而我們的npm二方庫(kù)的包里面還導(dǎo)出了.scss 和 .less 文件。在demo里我們可以非常簡(jiǎn)單的用ts別名‘ng-devui’引用 ./devui目錄的文件,在生產(chǎn)打包又會(huì)自動(dòng)引用 ./publish目錄下的組件非常方便。

和業(yè)務(wù)側(cè)在代碼里引用node_modules目錄下的文件是一樣的寫(xiě)法,最后構(gòu)建也是一樣的編譯路徑,屏蔽了這一層差異。但是sass和less文件卻不支持再引用包里的變量,原因是,當(dāng)ts文件請(qǐng)求了sass文件,這一層的路徑處理是webpack處理的,但是sass-loader接手之后(less-loader也是同理,這里僅直接說(shuō)sass-loader),sass內(nèi)部對(duì)sass文件引用的處理是sass-loader去啟動(dòng)一個(gè)sass編譯器實(shí)例編譯拿到的sass文件內(nèi)容,該sass編譯器實(shí)例也直接處理了sass文件之間的引用。

解決方案

好在sass-loader其實(shí)提供了一個(gè)importer的配置option,這里可以弄點(diǎn)文章。

importer提供了同步和異步的api,考慮不阻塞我們采用異步的api function(url, prev, done),它可以直接返回內(nèi)容{content: string}或者返回文件的實(shí)際路徑{file: string},而且它規(guī)定了如果返回null則代表這個(gè)importer里找不到,它會(huì)繼續(xù)鏈?zhǔn)秸{(diào)用查找其他importer。

這個(gè)是一個(gè)很關(guān)鍵的點(diǎn)。

通過(guò)走讀sass-loader本身的代碼,我們可以看到傳給sass-loader的importer會(huì)和它內(nèi)置的波浪線(~)的importer合并,見(jiàn)代碼1,代碼2。

也就是說(shuō),我們可以實(shí)現(xiàn)自己的importer的同時(shí),仍然保留sass-loader內(nèi)置的波浪線解析到node_module的語(yǔ)法糖。

和案例1的思路一樣,我們可以通過(guò)webpack配置的module的rules里找到sass-loader,并給它的options的sassOptions傳入一個(gè)importer的數(shù)組,這樣就可以完成importer的插入。

那么下一個(gè)問(wèn)題就是,我們?cè)趺磸倪\(yùn)行時(shí)拿出對(duì)應(yīng)的別名路徑映射過(guò)去?

Angular在編譯的時(shí)候,有一個(gè)AngularCompilerPlugin(require('@ngtools/webpack').AngularCompilerPlugin)會(huì)用于處理angular.json的build不同configuration下對(duì)應(yīng)的tsconfig文件路徑,Angular Compiler CLI內(nèi)又導(dǎo)出了一個(gè)readConfiguration函數(shù)(require('@angular/compiler-cli').readConfiguration)用于解析路徑下的tsconfig下的最后真實(shí)的配置。

tsconfig的描述文件是可以具有擴(kuò)展功能的,可以拓展另一個(gè)tsconfig文件,readConfiguration幫我們解決了擴(kuò)展過(guò)的tsconfig的合并問(wèn)題。這樣就能拿到當(dāng)前運(yùn)行環(huán)境對(duì)應(yīng)的tsconfig里面的path別名配置了。

下一步就是簡(jiǎn)單的取出path數(shù)據(jù)進(jìn)行一個(gè)簡(jiǎn)單的映射,保留波浪線的規(guī)則,我們把~ng-devui在本地開(kāi)發(fā)時(shí)候映射到./devui的,在生成打包時(shí)映射到./publish目錄,在用戶側(cè)時(shí)候的時(shí)候會(huì)引用來(lái)自node_modules的。

最后代碼如下:

//  tsconfig-alias-importer.js
const path = require('path');
const readConfiguration = require('@angular/compiler-cli').readConfiguration;

function pathAlias(tsconfigPath) {
  const {baseUrl, paths} = readConfiguration(path.resolve(tsconfigPath)).options;
  if (!paths) { return []; }
  return Object.keys(paths)
    .filter(alias => alias.endsWith('/*'))
    .map(alias => (
      {alias: alias, paths:paths[alias]}
    ))
    .map(rule => ({
      aliasReg: new RegExp('^~' + rule.alias.replace(/\/\*$/,'/(.*?)')),
      pathPrefixes: rule.paths.map(pathname => path.resolve(baseUrl || '' , pathname.replace(/\*$/,'')))
    }));
}

module.exports = function getTsconfigPathAlias(tsconfigPath = 'tsconfig.json') {
  try {
    const rules = pathAlias(tsconfigPath);
    // 匹配的情況下給出文件
    return function importer(url, prev, done) {
      if (!rules || rules.length === 0) {
        return null;
      }
      for (let rule of rules) {
        if (rule.aliasReg.test(url)) {
          // 暫時(shí)只支持第一個(gè)alias地址,其他的忽略
          const prefix = rule.pathPrefixes[0];
          const filename = path.resolve(prefix, url.replace(rule.aliasReg, (item, match) => match));
          return { file: filename};
        }
      }
      return null; // 沒(méi)有匹配的返回null,以繼續(xù)使用下一個(gè)importer
    };
  } catch (error) {
    console.warn('Sass alias importer might not effected', error);
    return function importer(url, prev, done) {
      return null;
    }
  }
}

代碼的大體邏輯是pathAlias函數(shù)通過(guò)讀tsconfig里的baseUrl和path,過(guò)濾出/*結(jié)尾的(因?yàn)榉?code>/*結(jié)尾的主要是指向index.ts的,不會(huì)代理到樣式),然后通過(guò)整合組裝成一條條正則和正則要替換的內(nèi)容,比如這條規(guī)則

"ng-devui/*": ["./devui/*"]

通過(guò)map轉(zhuǎn)換為

{
    alias:"ng-devui/*",
    paths: ["./devui/"]
}

進(jìn)一步轉(zhuǎn)換為

{
    aliasReg: /^~ng-devui\/(.*?)/,
    pathPrefixes: "D:\\code\\ng-devui\devui" // 真實(shí)路徑,筆者此處用的是windows系統(tǒng)
 }

這里baseUrl最外層tsconfig指向了./, 也就是工程的根目錄,最后pathResolve會(huì)解析為真實(shí)的路徑。

導(dǎo)出的getTsconfigPathAlias這個(gè)sass的importer,假定我們有一個(gè)文件在./devui/styles-var/devui-var.scss這個(gè)路徑,那么demo引用的時(shí)候可以使用~ng-devui/styles-var/devui-var.scss, 函數(shù)將多個(gè)別名進(jìn)行挨個(gè)檢測(cè)匹配到了, 如果有url匹配到正則,比如目前demo這個(gè)引用地址匹配到了 /^~ng-devui\/(.*?)/, 那么importer會(huì)返回{filename: "D:\\code\\ng-devui\\devui\\styles-var\\devui-var.scss"}。這樣就能找到tsconfig別名里面配置的路徑別名,實(shí)現(xiàn)了sass文件引用的別名。

Tsconfig的path別名本身是可以回落到多個(gè)地址的,這里簡(jiǎn)化成只回落到第一個(gè)地址, 如果需要實(shí)現(xiàn)多個(gè)地址, 可能需要塞進(jìn)去多個(gè)importer或者在一個(gè)importer里面檢測(cè)文件是否存在,回落到第二個(gè)地址,第三個(gè)地址。 這時(shí)候再把這個(gè)importer塞到webpack配置的每個(gè)sass-loader里。

// webpack-config-sass-loader-importer.js
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const getTsConfigAlias = require('./get-tsconfig-alias');

function getAngularCompilerTsConfigPath(config) {
  const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop();
  if (angularCompilerPlugin) {
    return angularCompilerPlugin.options.tsConfigPath;
  }
  return undefined;
}
function webpackConfigSassImporterAlias(config) {
  const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json';
  [{
    ruleTest: /\.scss$|\.sass$/,
    loaderName: 'sass-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[insertPosition].options.sassOptions.importer = [
            getTsConfigAlias(tsconfigPath)
          ];
        }

      }
    });
  });
  return config;
}
module.exports = webpackConfigSassImporterAlias;

這段代碼先從webpack的配置里找到AngualrCompilerPlugin插件,然后讀取它此時(shí)的tsconfig路徑。

以上是sass路徑別名的解決,得益于sass本身有一個(gè)importer,但是less上這個(gè)問(wèn)題就沒(méi)有那么好解決了,less只提供includePaths的選項(xiàng), 它會(huì)挨個(gè)遍歷去回落,并且是一視同仁的,即所有文件都會(huì)按這個(gè)includePaths去挨個(gè)嘗試。實(shí)際情況less是不支持波浪線的,但是less-loader卻又是支持波浪線語(yǔ)法的,走讀一下less-loader的代碼看看有沒(méi)有線索。可以看到less-loader用的寫(xiě)了一個(gè)WebpackFIleManagerment的plugin來(lái)做后綴名補(bǔ)充和利用webpack的resolve來(lái)做回落判斷。這個(gè)邏輯相對(duì)來(lái)說(shuō)就比較復(fù)雜了。

我們只能換個(gè)思路來(lái)解決這個(gè)問(wèn)題, less本身是有語(yǔ)法的,也就是我們能從語(yǔ)法中判定哪些是引用外部文件的,在引用之前我們可以處理一下路徑,比如把~ng-devui/styles-var/devui-var.less處理成相對(duì)于less文件的../../styles-var/devui-var.less那么less編譯器就能理解。

這是我們可以借用前面幾個(gè)案例的思路,在less-loader加載前增加一個(gè)loader,提前處理less語(yǔ)法里面的import引用語(yǔ)句,直接把波浪線地址替換成我們真實(shí)開(kāi)發(fā)環(huán)境或者生產(chǎn)打包環(huán)境的地址。

const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const pathAlias = require('./get-path-alias-from-tsconfig');

function getAngularCompilerTsConfigPath(config) {
  const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop();
  if (angularCompilerPlugin) {
    return angularCompilerPlugin.options.tsConfigPath;
  }
  return undefined;
}
function webpackConfigSassImporterAlias(config) {
  const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json';
  [{
    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 + 1, 0, {
            loader: require.resolve('./less-alias-replacer-loader'),
            options: {
              aliasMap: pathAlias(tsconfigPath)
            }
          });
        }

      }
    });
  });
  return config;
}
module.exports = webpackConfigSassImporterAlias;

這是webpack的修改,代碼大致意思是找到less-loader并在后面位置增加一個(gè)自定義的loader,并且把路徑別名從tsconfig的數(shù)據(jù)取出作為options傳給該loader。

pathAlias的寫(xiě)法就和前面sass-loader的是一樣的

// get-path-alias-from-tsconfig.js
const path = require('path');
const readConfiguration = require('@angular/compiler-cli').readConfiguration;

module.exports = function pathAlias(tsconfigPath) {
  const { baseUrl, paths } = readConfiguration(path.resolve(tsconfigPath)).options;
  if (!paths) { return []; }
  return Object.keys(paths)
    .filter(alias => alias.endsWith('/*'))
    .map(alias => (
      { alias: alias, paths: paths[alias] }
    ))
    .map(rule => ({
      aliasReg: new RegExp('^~' + rule.alias.replace(/\/\*$/, '/(.*?)')),
      pathPrefixes: rule.paths.map(pathname => path.resolve(baseUrl || '', pathname.replace(/\*$/, '')))
    }));
}
const path = require('path');
const { getOptions } = require('loader-utils');
const validateOptions = require('schema-utils');
const postcss = require('postcss');
const postcssLessSyntax = require('postcss-less');

const loaderName = 'less-path-alias-replacer-loader';
const trailingSlashAndContent = /[/\\][^/\\]*?$/;
const optionsSchema = {
  type: 'object',
  properties: {
    aliasMap: {
      anyOf: [{
        instanceof: 'Array'
      }, {
        enum: [
          null
        ]
      }]
    },
  },
  additionalProperties: false
}

const defaultOptions = {
}

function getOptionsFromConfig(config) {
  const rawOptions = getOptions(config)
  if (rawOptions) {
    validateOptions(optionsSchema, rawOptions, loaderName);
  }
  return Object.assign({}, defaultOptions, rawOptions);
}

/**
 *
 * @param {*} css less文本內(nèi)容
 * @param {*} aliasMap 別名規(guī)則集合
 */
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); // 去掉頭尾單引號(hào)雙引號(hào) 
          const rule = aliasMap.filter(rule => rule.aliasReg.test(oFilename)).pop();
          if (rule) {
            const prefix = rule.pathPrefixes[0]; // 取第一個(gè)路徑忽略剩余的
            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();

  // 獲取配置文件里的主題數(shù)據(jù)
  const aliasMap = getOptionsFromConfig(this).aliasMap;
  let newSource = source;
  if (aliasMap.length > 0) {
    newSource = lessReplacePathAlias(source, aliasMap, this.resourcePath);
  }
  // 返回結(jié)果
  this.callback(null, newSource, map);
  return newSource;
}

exports.default = process;

這里自定義一個(gè)wepack-loader的寫(xiě)法,實(shí)際上也是可以用postcss-loader搭配自定義replacePathAlias 的plugin 和 postcss-less的syntax進(jìn)行使用。這里演示了wepack-loader的寫(xiě)法。

代碼大意是定義了一個(gè)loader的optionSchema用于校驗(yàn)選項(xiàng),process函數(shù)獲取option里的路徑別名數(shù)據(jù)之后,如果路徑別名有數(shù)據(jù)則用postcss對(duì)代碼進(jìn)行處理。注意在process里this指向webpack的上下文,所以可以從resourcePath里獲取當(dāng)前文件路徑。

代碼核心為中間的postcss插件, 通過(guò)遍歷@開(kāi)頭的規(guī)則,如果是一個(gè)import聲明則讀取文件名去掉頭尾的單雙引號(hào);測(cè)試是否文件名命中了規(guī)則中的任意一條,命中則取第一條,和sass-loader處理的一樣得到了文件的絕對(duì)路徑,然后通過(guò)path.relative重新計(jì)算出和當(dāng)前文件的相對(duì)路徑。然后將這條@import規(guī)則替換成新的文件地址。

實(shí)際上這個(gè)思路同樣適用于sass規(guī)則的處理,只需要把語(yǔ)法syntax換成postcss-sass。

我們可以看到前兩個(gè)大案例都是在處理css類問(wèn)題的,大部分時(shí)候處理都可以用上postcss利器。直接去操作css內(nèi)容容易誤修改內(nèi)容,而經(jīng)過(guò)AST語(yǔ)法樹(shù)拆解后的遍歷會(huì)更穩(wěn)當(dāng)一些。

說(shuō)一下為什么會(huì)同時(shí)使用sass和less。一般工程是不會(huì)同時(shí)使用兩種的。實(shí)際上我們的工程主要也是使用sass。但是對(duì)于一個(gè)打包后的組件庫(kù)來(lái)說(shuō),業(yè)務(wù)使用的時(shí)候是不會(huì)感知它是sass還是less的,甚至也不會(huì)提供sass或者less文件給業(yè)務(wù)引用。這里是為了主題化的能力能夠?qū)ν廨椛?,組件庫(kù)同時(shí)提供了sass和less的版本變量供使用。

后記

這兩個(gè)樣式編譯器loader支持路徑別名的腳本最早寫(xiě)于2020年8月。

Webpack官方在sass-loader/less-loader的使用文檔里面都說(shuō)明了,~語(yǔ)法已經(jīng)廢除,建議刪除。

sass-loader@11.0.0(2021-2-5),less-loader@8.0.0(2021-2-1)分別發(fā)布了對(duì)應(yīng)版本聲明~已經(jīng)標(biāo)記為Deprecated。

作為歷史解決方案,這個(gè)案例依然會(huì)放在這里,提供一些解決思路。

筆者認(rèn)為能~和現(xiàn)在的回落解決方案還是不一樣的,尤其當(dāng)存在同名文件的時(shí)候(目前這種情況會(huì)比較少,少有人使用模塊同名路徑作為相對(duì)目錄路徑),波浪線方案仍然能明顯強(qiáng)調(diào)出文件的第一指向。

Webpack官方在2020年8月底開(kāi)始給less-loader也加上了webpackImporter的選項(xiàng),進(jìn)一步屏蔽less-loader和sass-loader之間的差異。兼容歷史原因,這兩個(gè)loader目前還會(huì)保留波浪線語(yǔ)法。由于項(xiàng)目ng版本滯后于NG官方版本,NG官方版本使用的loader又滯后于webpack官方版本。目前NG9版本仍在用less-loader@5.0.0,sass-loader@8.0.2。

要點(diǎn):

  • 從運(yùn)行時(shí)獲取tsconfig配置項(xiàng)

  • 文件路徑處理(scss、less)

  • 掌握l(shuí)oader的寫(xiě)法,接收參數(shù)。

更多解法補(bǔ)充

resolve.alias

Webpack配置中 config.resolve.alias 也是一個(gè)配置別名的地方, 而且sass-loader/less-loader也支持了webpackImporter的配置,其實(shí)可以直接通過(guò)修改config中的alias就能達(dá)成目的。 比如:

config.resolve.alias = {
      ... config.resolve.alias,
      'ng-devui': path.resolve('./devui/'),
   })
resolve.plugins:TsConfigPathsPlugin

那么如果webpack的resolve alias已經(jīng)支持了,是不是tsconfig就可以不用配置,或者怎么配置成一份?

不幸的是 Angular工程的 webpack 和 tsconfig不是同步的,兩邊需要同時(shí)配置,否則會(huì)出現(xiàn)找不到模塊的構(gòu)建錯(cuò)誤, 如果兩者配置不同,隱患會(huì)更大,因?yàn)閠sconfig的配置是直接影響tsc編譯器的拿不到文件,webpack也會(huì)解讀出一份等。

配置成一份可以通過(guò)寫(xiě)webpack-plugin在運(yùn)行時(shí)拿到當(dāng)前的tsconfig再處理數(shù)據(jù)塞到alias里。因?yàn)槿绻苯有薷腸onfig,那么它是靜態(tài)的,實(shí)際情況還是需要獲取到ng工程當(dāng)前的ts配置是什么文件會(huì)比較好。

Awesome-typescript-loader有個(gè)TsConfigPathsPlugin,使用也非常簡(jiǎn)單。

const { TsConfigPathsPlugin } = require('awesome-typescript-loader');
module.exports = (config) => {
  config.resolve.plugins = [
      ... (config.resolve.plugins || []),
      new TsConfigPathsPlugin()
   ]
};

給resolve.plugins塞一個(gè)TsConfigPathsPlugin就可以把tsconfig里的,不過(guò)這個(gè)插件已經(jīng)存檔了,最后一個(gè)版本是3年前的了。這里我們做了一個(gè)測(cè)試,仍然是不支持動(dòng)態(tài)的tsconfig,它的代碼可以參考用來(lái)寫(xiě)一個(gè)resolve的plugin, 動(dòng)態(tài)的tsconfig的path獲取可以參考我們之前的操作。

最后補(bǔ)充幾個(gè)新的結(jié)論
  • Webpack config的resolve.alias和 tsconfig的alias不是同步的,需要配置兩份或者找到一個(gè)方法同步。

  • Sass-loader 和less-loader的內(nèi)部引用都能走webpack的resolve.alias, 可以說(shuō)resolve.alias的解法會(huì)比 在改sass-loader的importer或者在less-loader前先處理 通用性更好,基本上所有blob都可以嘗試去這樣解決路徑別名問(wèn)題。更多需要注意的仍然是動(dòng)態(tài)的tsconfig的配置獲取問(wèn)題。

2.3 案例3:修改TerserPlugin排除項(xiàng),搖樹(shù)處理問(wèn)題(搖樹(shù)問(wèn)題)

背景

組件庫(kù)的業(yè)務(wù)使用方剛升級(jí)ng7的時(shí)候,打包經(jīng)常出問(wèn)題,經(jīng)常出現(xiàn)文件搖樹(shù)之后,很多自執(zhí)行命令被搖樹(shù)認(rèn)為無(wú)副作用搖樹(shù)掉了。

目標(biāo):不關(guān)掉全局搖樹(shù)的情況下,針對(duì)個(gè)別目錄進(jìn)行搖樹(shù)的問(wèn)題排除。

上下文:

Angular.json里有個(gè)配置,默認(rèn)為打開(kāi),打開(kāi)之后可以對(duì)js類型的代碼進(jìn)行搖樹(shù)優(yōu)化,從而減小打包體積。

Terser是從Uglifyjs這個(gè)庫(kù)fork 出來(lái)的項(xiàng)目用來(lái)支持ES6語(yǔ)法,Angular 編譯階段用它來(lái)壓縮js代碼。

Angular使用TerserPlugin來(lái)進(jìn)行搖樹(shù),由于裝飾器等問(wèn)題導(dǎo)致?lián)u樹(shù)效果不理想的問(wèn)題(相關(guān)討論內(nèi)容見(jiàn)Angular搖樹(shù)如何工作),angular提供了一個(gè)專門(mén)用于標(biāo)記純函數(shù)的注釋的優(yōu)化器,所以對(duì)于默認(rèn)的Angular項(xiàng)目來(lái)說(shuō),搖樹(shù)是一個(gè)兩階段模型。

第一個(gè)階段是Angular在生產(chǎn)構(gòu)建階段(ng build --prod)在解析資源的時(shí)候加入了一個(gè)@angular_devkit/build_optimizer/webpack-loader,用于標(biāo)記js文件里的無(wú)用代碼;angular_devkit/build_optimizer 介紹它的主要功能為標(biāo)記/* PURE */功能。

第二個(gè)階段是Angular在生成打包的時(shí)候給Webpack配置了 optimization.minimizer數(shù)組塞入了TerserPlugin然后把無(wú)副作用的無(wú)引用代碼搖掉。(準(zhǔn)確說(shuō)是塞入了兩個(gè)Plugin,一個(gè)針對(duì)globalScript,一個(gè)排除globalScript。GlobalScript是指在angular.json的build的script字段配置的路徑。)

解決方案

TerserPlugin本身是有一個(gè)include和exclude字段(見(jiàn)API)可以用正則和字符串來(lái)排除。

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工程會(huì)遇到比較多的搖樹(shù)問(wèn)題,有些升級(jí)到ng9之后默認(rèn)配置有點(diǎn)變化之后就沒(méi)有搖樹(shù)問(wèn)題了。包括IVY打包模式下編譯引擎實(shí)際上搖樹(shù)的方式不太一樣,有些文件不會(huì)被搖掉了,這個(gè)問(wèn)題還是要遇到具體問(wèn)題具體分析來(lái)解決。必要時(shí)可以重新new一個(gè)TerserPlugin但是要保持它的原來(lái)的options不變。

要點(diǎn):

  • 搖樹(shù)具體的階段,攔截問(wèn)題

  • 維持原有的Options,不對(duì)其進(jìn)行破壞。

2.4 案例4: 三方ES2015庫(kù)自動(dòng)babel化處理(僅有ES2015的庫(kù)處理)

背景

highlight.js升級(jí)到10.0.0之后,官方就開(kāi)始不再默認(rèn)支持ie11了,導(dǎo)出的包也只有es2015的包。之前的業(yè)務(wù)需要為了兼容IE11,我們需要對(duì)highlight.js進(jìn)行一次babel。更新到ng9之后,其實(shí)ng默認(rèn)會(huì)僅打包es2015然后進(jìn)行差分打包到es5,所以實(shí)際情況在生產(chǎn)打包是沒(méi)有問(wèn)題的,但是在本地開(kāi)發(fā)的ie11調(diào)試環(huán)節(jié)會(huì)出現(xiàn)問(wèn)題,比如class的語(yǔ)法ie不認(rèn)識(shí)等等,導(dǎo)致整個(gè)js無(wú)法加載。

目標(biāo):讓不支持es5的包在開(kāi)發(fā)態(tài)支持es5。

上下文:由于之前組件庫(kù)9的版在一段時(shí)間內(nèi)仍然需要支持ie11,也就經(jīng)常需要開(kāi)發(fā)態(tài)下到ie11下debug,所以開(kāi)發(fā)態(tài)下的highlight.js導(dǎo)致ie無(wú)法訪問(wèn)問(wèn)題需要解決。

解決方案

首先es2015轉(zhuǎn)es5經(jīng)典的做法就是讓babel幫忙處理。然后大部分的ES語(yǔ)法新增的api可以由core-js來(lái)解決,剩下的IE11還有一些瀏覽器端DOM的API的實(shí)現(xiàn)還需要添加一些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;

/**
 * 備注:如果三方庫(kù)只提供es6版本, 則添加到ES6_ONLY_THIRD_PARTY_LIST, 通過(guò)babel轉(zhuǎn)換語(yǔ)法到es5
 * 僅對(duì)target為es5的時(shí)候啟用(比如npm start狀態(tài))
 * 差分打包會(huì)自動(dòng)解決,不需要解決
 */
// es6-only-third-party-list.js
/**
 * 如果三方庫(kù)只提供es6版本, 則添加到ES6_ONLY_THIRD_PARTY_LIST, 通過(guò)babel轉(zhuǎn)換語(yǔ)法到es5
 */
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里面讀,如果目標(biāo)為es5,則塞進(jìn)去babel-loader,然后把對(duì)應(yīng)的三方庫(kù)的路徑放到include里邊。

ES6_ONLY_THIRD_PARTY_LIST 列表示意了highlight.js的路徑應(yīng)該怎么寫(xiě)。如果編譯目標(biāo)為es2015,則這段處理就不需要了不會(huì)被插入,哪怕是差分打包也不會(huì)調(diào)用它,ng-cli會(huì)自行調(diào)用內(nèi)部邏輯。

后記

IE11已經(jīng)慢慢退出了歷史舞臺(tái),各大網(wǎng)站也開(kāi)始聲明不再支持IE11,這些冗余的插件已經(jīng)可以慢慢移除。包括ng12起也不再承諾支持ie,升級(jí)到ng12之后這些插件也沒(méi)有必要了。

要點(diǎn):

  • 分清語(yǔ)法API的降級(jí)和瀏覽器BOM/DOM墊片

  • 提供一個(gè)可維護(hù)的列表

  • 針對(duì)tsconfig的target上下文進(jìn)行編譯。

2.5 案例5: 攔截修改自動(dòng)生成ts內(nèi)容(內(nèi)容自動(dòng)掃描生成)

案例5-1: 自動(dòng)掃描目錄

背景

在可視化拖拽生產(chǎn)力平臺(tái)項(xiàng)目,組件定義目錄會(huì)有一系列重復(fù)雷同的目錄結(jié)構(gòu),最后需要匯總到一個(gè)ts里作為全局信息入口。

目標(biāo):自動(dòng)掃描組件定義目錄,匯總信息到ts里,避免手動(dòng)增加信息維護(hù)。

上下文:

生成的信息匯總內(nèi)容結(jié)構(gòu)為:

src/app/connect/connet.ts

如何自定義配置Angular CLI下的Webpack和loader處理

目錄的結(jié)構(gòu)為:

src/component-lib

如何自定義配置Angular CLI下的Webpack和loader處理

要求不要包含 _目錄的內(nèi)容

解決方案
const fs = require('fs').promises;
const path = require('path');

async function listDir() {
  return fs.readdir(path.resolve('./src/component-lib')).then(dirs => dirs.filter(item => !item.startsWith('_'))); // 過(guò)濾_開(kāi)頭的
}
function genConnectInfo(dirArr) {
  return `export const ConnectInfo = {
  ${dirArr.map(item =>"'"+ item +"': import( /* webpackChunkName: \"component-lib-" + item + "-connect\" */  '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('./src/component-lib')); // 自動(dòng)掃描目錄,但是刪除目錄可能會(huì)引起報(bào)錯(cuò)
  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('./src/app/connect')
      ],
      enforce: 'post',
    });
  
  return config;
};
module.exports = webpackConfigAddScanAndGenerateConnectInfo;

通過(guò)簡(jiǎn)單的一個(gè)loader 將目錄掃描內(nèi)容組裝返回給src/app/connect/connet.ts。

這里有幾個(gè)要點(diǎn):

  • 這里需要使用loader的enforce: 'post'屬性,因?yàn)閠s的編譯最后loader是直接去文件系統(tǒng)讀取內(nèi)容的不是從上一個(gè)loader拿到結(jié)果繼續(xù)往下處理的(不符合loader的規(guī)范,但是性能會(huì)更好),所以這里需要把階段屬性配置為'post',確保最后編譯的內(nèi)容是我們生成的內(nèi)容。

  • 對(duì)loader添加一些依賴可以在目錄結(jié)構(gòu)變化的時(shí)候刷新內(nèi)容,否則就只能等下一次啟動(dòng)的時(shí)候獲取內(nèi)容,即這一行 this.addContextDependency(path.resolve('./src/component-lib'));

  • 由于webpack的tsc編譯是文件分析依賴型的,我們動(dòng)態(tài)生成的文件內(nèi)容,webpack就無(wú)法從中分析依賴了另外一些ts內(nèi)容,導(dǎo)致其他ts內(nèi)容不會(huì)走ts編譯。這時(shí)候可以參考下面,在tsconfing加一下include字段,解決編譯問(wèn)題。

/* tsconfig.app.json*/
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts",
    "src/component-lib/**/*.ts"
  ]
}

要點(diǎn):

  • 自動(dòng)掃描邏輯

  • 添加依賴

  • 添加非直接依賴的編譯入口

案例5-2: 自動(dòng)掃描css內(nèi)容

背景

圖標(biāo)庫(kù)導(dǎo)出了一系列可用字體圖標(biāo),圖標(biāo)通過(guò)不同的css類名來(lái)引用,現(xiàn)在要做一個(gè)圖標(biāo)選擇器,需要把所有圖標(biāo)列出來(lái)。

目標(biāo):分析icon.css,提取所有有效的icon名字。

上下文:

需要把信息自動(dòng)生成到一個(gè)文件叫 src/app/properties-panel/properties-control/icon-picker/icon-library.data.ts

格式為一個(gè)數(shù)組

如何自定義配置Angular CLI下的Webpack和loader處理

圖標(biāo)庫(kù)的文件為./node_modules/@devui-design/icons/icomoon/devui-icon.css

格式為:

如何自定義配置Angular CLI下的Webpack和loader處理

圖片里紅色標(biāo)記的就是要提取出來(lái)的圖標(biāo)名。

解決方案

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('./src/app/properties-panel/properties-control/icon-picker')
      ],
      enforce: 'post',
    });
  
  return config;
};
module.exports = webpackConfigAddGenIconData;

代碼相對(duì)就比較簡(jiǎn)單了。

要點(diǎn):

1、分析圖標(biāo)庫(kù)文件結(jié)構(gòu),排除一些對(duì)齊的干擾項(xiàng)

2.6 補(bǔ)充:自定義Webpack配置loader的調(diào)試方法

  • 配置項(xiàng)調(diào)試:在方法中進(jìn)行打印調(diào)試, 通常在執(zhí)行前就已經(jīng)可以打印出配置項(xiàng)相關(guān)的內(nèi)容。

  • 內(nèi)容調(diào)試:使用簡(jiǎn)單的loader打印前后經(jīng)過(guò)loader的內(nèi)容, 自定義一個(gè)打印內(nèi)容的loader,在執(zhí)行自定義loader前后都打印一下,可以獲得對(duì)比內(nèi)容。

  • 一些經(jīng)驗(yàn): 修改loader的時(shí)候要注意本地開(kāi)發(fā)階段和生產(chǎn)打包階段,不要顧此失彼。


文章介紹了如何在AngularCLI生成的工程里使用自定義的webpack設(shè)置,并且舉了幾個(gè)實(shí)際情況下為了解決問(wèn)題修改的webpack配置,覆蓋css的修改、js的修改以及攔截內(nèi)容自動(dòng)掃描生成。

本文主要為解決問(wèn)題或者功能特性去修改webpack配置,沒(méi)有涉及復(fù)雜的構(gòu)建速度優(yōu)化、性能優(yōu)化等。

有些內(nèi)容是舊版本處理IE11問(wèn)題做兼容的,具體的實(shí)踐不再具備復(fù)制方案就能解決現(xiàn)實(shí)問(wèn)題的意義,更多的是提供一些思路和參考。

Webpack目前幾個(gè)版本始終是從入口文件開(kāi)始,通過(guò)規(guī)則配置對(duì)文件進(jìn)行加載解析,通過(guò)插件攔截整個(gè)構(gòu)建生命周期做一些抽取變換。

大部分時(shí)候Angular的默認(rèn)配置已經(jīng)能滿足需求,當(dāng)和三方庫(kù)集成、自動(dòng)化處理結(jié)合的時(shí)候,采用自定義的配置可以解決問(wèn)題,節(jié)省工作甚至進(jìn)一步優(yōu)化性能等等。

看完上述內(nèi)容,你們對(duì)如何自定義配置Angular CLI下的Webpack和loader處理有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI