由Antd的global.css引发的一场源码调试

2,331 阅读12分钟

背景

笔者最近是在做活动页可视化编辑平台,某天发现了页面上使用富文本编辑出来的内容,在平台上的编辑效果与预览效果不一致,经过简单排查后得知是因为平台的编辑和预览区分了路由,分别使用了React.lazy+ Import做了路由懒加载,由于编辑路由引入了 Antd UI库,但预览路由是不会把Antd相关的代码打包进去的,而Antd的global.css内部重写了多个标签的默认样式,所以引发了问题

那具体Antd的global.css是在什么时候被引入进来的呢?Antd UI库的构建流程具体是怎样,组件按需加载方案是如何设计的?带着这些疑惑,笔者开始了一场Antd的源码调试,从源码角度抛析Antd的按需加载与构建方案

使用yarn link 调试本地的Antd包

首先第一步是梳理Antd框架整体的构建流程,那如何在本地项目上调试本地的Antd包呢?答案就是Yarn Link

按以下步骤执行

  1. 本地 git clone antd-design仓库
  2. 在antd-design根目录执行yarn installyarn run build先构建出源代码
  3. antd-design根目录中执行yarn link
  4. 在本地项目根目录(指需要调试antd的项目)执行yarn link antd

完成这些步骤后本地项目执行yarn run dev启动,查看页面中Antd组件是否正常显示,如无意外浏览器的控制台将看到如下错误

image.png

这是因为Antd中的package.jsonpeerDependencies所依赖的react与本地项目所依赖的react版本不对等,解决方案就是本地项目中的react依赖Antd项目下node_module中react包版本即可


# 先进入antd下的react目录执行link
cd  /workspace/antd/node_modules/react
yarn link 

# 再切换至本地项目执行 link react
cd /workspace/xxx
yarn link react

完成以上步骤后尝试修改Antd根目录下es或lib目下的源代码,此时本地项目的webpack-dev-server能够监听到Antd源码变化并reload页面,说明调试流程已走通。

Antd具体的构建工作是交给了antd-tools脚手架,所以修改components下的组件代码后执行yarn run compile即可重新编译

{
  "scripts": {
     compile:antd-tools run compile
   }
}

分析Antd 构建后的产物

笔者这里下载的是Antd 4.15.3,从其根目录的构建后产物可以看出antd是构建了es,lib以及dist三个目录,用于支持组件库的按需引入与全量引入

  • es和lib

    es与lib分别对应esModule和cjs版本

  • dist

    dist是Antd的全量版本,如antd.js ,antd.css 等

入口文件分析

从Antd的package.json的部分配置,可以看到Antd的入口文件配置了mainmodule字段。分别表示在不同环境下进行 import {button} from 'antd'时Antd包对应的入口文件

{
  "name": "antd",
  "main": "lib/index.js",
  "module": "es/index.js",
  "scripts": {
    "build": "npm run compile && npm run dist",
    "compile": "npm run clean && antd-tools run compile",
    "compile:less": "antd-tools run compile:less",
    "dist": "antd-tools run dist",
    "dist:esbuild": "ESBUILD=true npm run dist",
    "clean": "antd-tools run clean && rm -rf es lib",
  }
}
  • main : 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用
  • module : 定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用
  • browser : 定义 npm 包在 browser 环境下的入口文件

在webpack的配置中,可以使用 resolve.mainFields 指定优先使用npm包的哪个入口字段

Antd的按需加载方案

从打包后的es和lib目录可以看出,针对Antd组件库的按需加载有两种方案,一是基于构建工具进行Tree-Shaking ,二是使用 babel-plugin-import进行独立引入组件样式,因为Antd已经为每个组件都独立在lib和es目录下都单独打包了css与js,外部直接按需引入即可

下面是打包后lib目录下button组件目录结构,比如使用button组件,只需要引入这两个入口文件就即可达到组件按需加载目的

button
├── LoadingIcon.d.ts
├── LoadingIcon.js
├── button-group.d.ts
├── button-group.js
├── button.d.ts
├── button.js 
├── index.d.ts
├── index.js   //组件入口文件
└── style
    ├── index.css
    ├── index.d.ts
    ├── index.js   //样式入口文件
    ├── index.less 
    ├── mixin.less
    └── rtl.less
  • Tree Shaking

    如今webpack,rollup等构建工具都具备了摇树功能(Tree Shaking), 其原理是利用ESM模块的import语法的特性,通过AST语法树进行分析然后去除未使用到的代码。而要开启此功能必须确保引入的Antd es版本,在webpack上可通过resolve.mainFields来指定npm包的入口文件优先级,但tree shaking方式也只是处理了js部分,对于组件的css加载还需要手动引入,通常由有两种方式,一是在入口文件加上import 'antd/dist/antd.css'进行全量加载,二是手动按需去引入组件样式

  • babel-plugin-import

    babel-plugin-import是babel的插件,它可以在babel-loader编译阶段对源代码中的import语法进行转化,通过编译后能直接引入到组件对应的css与js文件实现按需加载

  //源代码
  import {Button} from 'antd'
  //使用babel-plugin-import后,在babel-loader编译阶段会被改为
  var _button = require('antd/lib/button');
  require('antd/lib/button/style.js');

antd/lib/index.js 文件中也可以看到Antd对其全量引入组件方式进行检测并抛出提示建议使用babel-plugin-import进行按需加载。

关于babel-plugin-import的扩展阅读【babel-plugin-import源码解析+代码实现】

  if (ENV !== 'production' && ENV !== 'test' && typeof console !== 'undefined' && console.warn && 
      typeof window !== 'undefined') {
      // eslint-disable-next-line no-console
      console.warn('You are using a whole package of antd, ' + 'please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.');
  }

antd的global.css

antd的global.css是何时被引入的呢?笔者以button组件为例在本地对Antd源码进行了调试,在查阅到button/style/index.tsx中的代码时,问题找到了答案。

由于在button/style/index.tsx这个文件中引入了style/core/global.less文件,经过gulp打包编译。最终在lib和es目录下生成了button/style/index.js文件。项目中平常写的import { button } from 'antd',经过babel-plugin-import转换代码后,就会包含require('antd/lib/button/style.js')这行代码

事实上Antd的每个组件的样式入口文件中都引入了style/core/global.less文件,所以这也是为什么引入antd的任何组件全局样式都能被加载进来

button源码组件目录

   button    
   ├── LoadingIcon.tsx
   ├── button-group.tsx
   ├── button.tsx
   ├── index.en-US.md
   ├── index.tsx        //模块入口文件,引入了button.tsx,button-group等
   ├── index.zh-CN.md
   └── style
       ├── index.less   //组件样式源码
       ├── index.tsx    //样式入口文件, 引入了index.less,以及全局样式在这里被进行了引入
       ├── mixin.less  
       └── rtl.less

  //components/style(antd放置通用样式的目录)

   style       
   ├── color
   │   ├── colors.less
   ├── compact.less
   ├── core
   │   ├── base.less
   │   ├── global.less  //全局样式
   │   ├── index.less
   │   ├── motion
   │   │   └── zoom.less
   │   └── motion.less
   ├── dark.less
   ├── index.less
   ├── index.tsx
   ├── mixins
   │   ├── box.less
   └── themes
       └── index.less

lib目录下的button组件结构

button
├── LoadingIcon.d.ts
├── LoadingIcon.js
├── button-group.d.ts
├── button-group.js
├── button.d.ts
├── button.js
├── index.d.ts
├── index.js
└── style
    ├── index.css
    ├── index.d.ts
    ├── index.js
    ├── index.less
    ├── mixin.less
    └── rtl.less

其实关于Antd的全局样式问题,网上也有很多人已经尝试了多种解决方案,这里不再讨论

【如何优雅地彻底解决 antd 全局样式问题】

Antd的构建方案

antd-tools脚手架

上述部分只是从构建产物进行分析Antd是如何支持按需加载的,接下来笔者从package.json中的npm-scripts中的一系列构建指令,分析下具体的构建实现

{
  "name": "antd",
  "main": "lib/index.js",
  "module": "es/index.js",
  "scripts": {
    "build": "npm run compile && npm run dist",
    "compile": "npm run clean && antd-tools run compile",
    "compile:less": "antd-tools run compile:less",
    "dist": "antd-tools run dist",
    "dist:esbuild": "ESBUILD=true npm run dist",
    "clean": "antd-tools run clean && rm -rf es lib",
  }
}

可以看到npm-script声明了build和coppile两条命令,执行build命令会打包出lib,es目录与dist三个目录,执行compile命令只会打包lib,es这两个目录

而antd-tools是Antd团队独立封装的脚手架工程,专门于用包揽构建发布相关的累活,如处理build,lint,publish等操作,封装gulp任务,启动webpack编译,jest,eslint,babel等

对node脚手架命令进行断点调试

断点调试在分析源码的过程中是经常会使用到的一种技巧,那如何在vscode中对node脚手架项目进行调试呢?其实所谓的脚手架命令就是执行npm-script定义的命令,那在vscode中调试npm-script命令,可以按如下步骤操作

  1. 在Antd项目中的npm-scripts加上一条命令,改为使用node命令的方式来启动脚手架
  "debug": "node --inspect-brk=9999 ./node_modules/.bin/antd-tools-run compile"
  1. 配置launch.json

debug.png

按照图中标记的步骤进行配置,最后点击绿色三角运行,即可在入口文件处自动断点,具体launch.json的配置字段可查看VSCode launch.json配置详解

image.png

antd-tools源码分析

脚手架的指令注册

在命令行终端执行antd-tools时,实际上是会找到tools的package.json中的bin字段中对应路径文件执行,下面是antd-tools的package.json的bin字段配置

"name":"@ant-design/tools",
 "bin": {
   "antd-tools": "bin/antd-tools.js",
   "antd-tools-run": "bin/antd-tools-run.js",
 },
 

antd/toolsbin/antd-tools.js最后会执行tools/lib/cli/run.js,在run.js中从process.argv中拿到命令行参数执行对应的gulp任务

graph TD
tools/bin/antd-tools.js --> tools/lib/cli/indx.js  --> 通过commander的子命令执行run命令 --> tools/lib/cli/run.js启动gulp --> 执行gulpFile.js中定义的任务 

antd/tools/lib/cli/run.js中,引入了gulp,调用runTask方法执行gulp任务

#!/usr/bin/env node
require('colorful').colorful();
const gulp = require('gulp');
const program = require('commander');

program.parse(process.argv);

//toRun表示命令行中输入的任务名,如compile,dist等
function runTask(toRun) {
  const metadata = { task: toRun };
  // Gulp >= 4.0.0 (doesn't support events)
  const taskInstance = gulp.task(toRun);
  if (taskInstance === undefined) {
    gulp.emit('task_not_found', metadata);
    return;
  }
  const start = process.hrtime();
  gulp.emit('task_start', metadata);
  try {
    taskInstance.apply(gulp);
    metadata.hrDuration = process.hrtime(start);
    gulp.emit('task_stop', metadata);
    gulp.emit('stop');
  } catch (err) {
    err.hrDuration = process.hrtime(start);
    err.task = metadata.task;
    gulp.emit('task_err', err);
  }
}

const task = program.args[0];

if (!task) {
  program.help();
} else {
  require('../gulpfile');
  //接受命令行参数,执行gulp任务
  runTask(task); 
}

postinstall钩子重写npmScripts

在antd-tools的package.json的scripts中声明了npm postinstall钩子,它的作用时在执行yarn install后,为Antd的package.json注入npm script具体的打包命令

"name":"@ant-design/tools",
 "bin": {
   "antd-tools": "bin/antd-tools.js",
   "antd-tools-run": "bin/antd-tools-run.js",
 },
 "scripts":{
     "postinstall": "node lib/init.js", //执行yarnn install时会执行该脚本
 }
 

lib/init.js

    function addConfigHooks(cfg, projectDir) {
    if (!cfg.scripts) {
        cfg.scripts = {};
    }
    if (cfg.scripts.pub) {
        return false;
    }
    cfg.scripts = Object.assign(cfg.scripts, {
        dist: 'antd-tools run dist',
        compile: 'antd-tools run compile',
        clean: 'antd-tools run clean',
        start: 'antd-tools run start',
        site: 'antd-tools run site',
        deploy: 'antd-tools run update-self && antd-tools run deploy',
        'just-deploy': 'antd-tools run just-deploy',
        pub: 'antd-tools run update-self && antd-tools run pub',
    });

    if (cfg.scripts.prepublish) {
        cfg.scripts['pre-publish'] = cfg.scripts.prepublish;
    }

    cfg.scripts.prepublish = 'antd-tools run guard';

    writeFile(pathJoin(projectDir, 'package.json'), JSON.stringify(cfg, null, 2));

    return true;
    }

compile指令

compile指令是用于打包所有组件代码构建lib和es目录的,具体是在gulpFile.js中对compile任务进行了定义

  gulp.task(
     'compile',
      gulp.series(
      gulp.parallel('compile-with-es', 'compile-with-lib'),'compile-finalize')
   );
   gulp.task('compile-with-es', done => {
     console.log('[Parallel] Compile to es...');
     compile(false).on('finish', done);
   });
   gulp.task('compile-with-lib', done => {
     console.log('[Parallel] Compile to js...');
     compile().on('finish', done);
   });
   

代码中可以看到compile任务,是分别执行了三个任务。而compile-with-es和compile-with-lib任务都是调用的compile函数,而compile-finalize是调用的finalize函数,这里一一分析一下compile函数和finalize函数

  • compile函数
    
 // modules表示是否构建esm版本
function compile(modules) {
  const { compile: { transformTSFile, transformFile } = {} } = getConfig();
  rimraf.sync(modules !== false ? libDir : esDir);

  // =============================== LESS ===============================
  const less = gulp
    .src(['components/**/*.less'])
    .pipe(
      through2.obj(function (file, encoding, next) {
        // Replace content
        const cloneFile = file.clone();
        const content = file.contents.toString().replace(/^\uFEFF/, '');

        cloneFile.contents = Buffer.from(content);

        // Clone for css here since `this.push` will modify file.path
        const cloneCssFile = cloneFile.clone();

        this.push(cloneFile);

        // Transform less file
        if (
          file.path.match(/(\/|\\)style(\/|\\)index\.less$/) ||
          file.path.match(/(\/|\\)style(\/|\\)v2-compatible-reset\.less$/)
        ) {
          transformLess(cloneCssFile.contents.toString(), cloneCssFile.path)
            .then(css => {
              cloneCssFile.contents = Buffer.from(css);
              cloneCssFile.path = cloneCssFile.path.replace(/\.less$/, '.css');
              this.push(cloneCssFile);
              next();
            })
            .catch(e => {
              console.error(e);
            });
        } else {
          next();
        }
      })
    )
    .pipe(gulp.dest(modules === false ? esDir : libDir));
    
  const assets = gulp
    .src(['components/**/*.@(png|svg)'])
    .pipe(gulp.dest(modules === false ? esDir : libDir));
  let error = 0;

  // =============================== FILE ===============================
  let transformFileStream;

  if (transformFile) {
    transformFileStream = gulp
      .src(['components/**/*.tsx'])
      .pipe(
        through2.obj(function (file, encoding, next) {
          let nextFile = transformFile(file) || file;
          nextFile = Array.isArray(nextFile) ? nextFile : [nextFile];
          nextFile.forEach(f => this.push(f));
          next();
        })
      )
      .pipe(gulp.dest(modules === false ? esDir : libDir));
  }

  // ================================ TS ================================
  const source = [
    'components/**/*.tsx',
    'components/**/*.ts',
    'typings/**/*.d.ts',
    '!components/**/__tests__/**',
  ];
  // allow jsx file in components/xxx/
  if (tsConfig.allowJs) {
    source.unshift('components/**/*.jsx');
  }

  // Strip content if needed
  let sourceStream = gulp.src(source);
  if (modules === false) {
    sourceStream = sourceStream.pipe(
      stripCode({
        start_comment: '@remove-on-es-build-begin',
        end_comment: '@remove-on-es-build-end',
      })
    );
  }

  if (transformTSFile) {
    sourceStream = sourceStream.pipe(
      through2.obj(function (file, encoding, next) {
        let nextFile = transformTSFile(file) || file;
        nextFile = Array.isArray(nextFile) ? nextFile : [nextFile];
        nextFile.forEach(f => this.push(f));
        next();
      })
    );
  }

  const tsResult = sourceStream.pipe(
    ts(tsConfig, {
      error(e) {
        tsDefaultReporter.error(e);
        error = 1;
      },
      finish: tsDefaultReporter.finish,
    })
  );

  function check() {
    if (error && !argv['ignore-error']) {
      process.exit(1);
    }
  }

  tsResult.on('finish', check);
  tsResult.on('end', check);
  
  //通过gulp-babel对代码进行转换
  const tsFilesStream = babelify(tsResult.js, modules);
  const tsd = tsResult.dts.pipe(gulp.dest(modules === false ? esDir : libDir));
  
  return merge2([less,tsFilesStream, tsd, assets,transformFileStream].filter(s => s));
  
}



function babelify(js, modules) {
  const babelConfig = getBabelCommonConfig(modules);
  delete babelConfig.cacheDirectory;
  if (modules === false) {
    babelConfig.plugins.push(replaceLib);
  }
  const stream = js.pipe(babel(babelConfig)).pipe(
    through2.obj(function z(file, encoding, next) {
      //先复制一份文件,避免后续被修改了原文件
      this.push(file.clone());
      
      //匹配文件路径是/style/index.js时进入
      if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
        const content = file.contents.toString(encoding);
        if (content.indexOf("'react-native'") !== -1) {
          // actually in antd-mobile@2.0, this case will never run,
          // since we both split style/index.mative.js style/index.js
          // but let us keep this check at here
          // in case some of our developer made a file name mistake ==
          next();
          return;
        }
        
    
        file.contents = Buffer.from(cssInjection(content));
        
        //  将/style/index.js 替换为 css.js
        file.path = file.path.replace(/index\.js/, 'css.js');
        this.push(file);
        next();
      } else {
        next();
      }

    })
  );
  return stream.pipe(gulp.dest(modules === false ? esDir : libDir));
}

//替换一些css代码
function cssInjection(content) {
  return content
    .replace(/\/style\/?'/g, "/style/css'")
    .replace(/\/style\/?"/g, '/style/css"')
    .replace(/\.less/g, '.css');
}

  • compile-finalize compile-finalize任务很简单,它只是执行了finalize函数,而finalize的作用只是在lib目录下方的style目录下生成一个components.less文件。而antd-tools.config.js是antd-tools脚手架的配置文件,在antd的根目录下
gulp.task('compile-finalize', done => {
  // Additional process of compile finalize
  const { compile: { finalize } = {} } = getConfig();
  if (finalize) {
    console.log('[Compile] Finalization...');
    finalize();
  }
  done();
});

//读取antd根目录下的antd-tools.config.js
function getConfig() {
  const configPath = getProjectPath('.antd-tools.config.js');
  if (fs.existsSync(configPath)) {
    return require(configPath);
  }

  return {};
}

//antd-tools.config.js中的finalizeCompile

function finalizeCompile() {
  if (fs.existsSync(path.join(__dirname, './lib'))) {
    // Build a entry less file to dist/antd.less
    const componentsPath = path.join(process.cwd(), 'components');
    let componentsLessContent = '';
    // Build components in one file: lib/style/components.less
    fs.readdir(componentsPath, (err, files) => {
      files.forEach(file => {
        if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) {
          componentsLessContent += `@import "../${path.join(file, 'style', 'index.less')}";\n`;
        }
      });
      fs.writeFileSync(
        path.join(process.cwd(), 'lib', 'style', 'components.less'),
        componentsLessContent,
      );
    });
  }
}

dist指令

dist指令是用于打包出全量antd.js,该指令是借用gulp启动了webpack进行编译

graph TD
gulp执行dist任务  --> 调用getProjectPath方法获取Antd目录下webpackfconfig.js --> 调用getWebpackConfig获取通用打包配置  -->  导出多份webpackConfig配置 --> 以antd根目录的index.js为入口启动webpack进行构建

  • dist 任务

    dist任务本身除了启动webpack进行编译外,并没有做其他的工作,而webpack.config的配置放在了antd的根目录下

onst webpack = require('webpack');
const babel = require('gulp-babel');
const rimraf = require('rimraf');
const { getProjectPath, injectRequire, getConfig } = require('./utils/projectHelper');


gulp.task(
  'dist',
  gulp.series(done => {
    dist(done);
  })
);

function dist(done) {
  rimraf.sync(getProjectPath('dist'));
  process.env.RUN_ENV = 'PRODUCTION';
  
  //获取antd根目录下的webapck.cponfig配置
  
  const webpackConfig = require(getProjectPath('webpack.config.js'));
  webpack(webpackConfig, (err, stats) => {
    if (err) {
      console.error(err.stack || err);
      if (err.details) {
        console.error(err.details);
      }
      return;
    }

    const info = stats.toJson();
    const { dist: { finalize } = {}, bail } = getConfig();

    if (stats.hasErrors()) {
      (info.errors || []).forEach(error => {
        console.error(error);
      });
      // https://github.com/ant-design/ant-design/pull/31662
      if (bail) {
        process.exit(1);
      }
    }

    if (stats.hasWarnings()) {
      console.warn(info.warnings);
    }

    const buildInfo = stats.toString({
      colors: true,
      children: true,
      chunks: false,
      modules: false,
      chunkModules: false,
      hash: false,
      version: false,
    });
    console.log(buildInfo);

    //就是compile-finalize任务中的finalize函数
    if (finalize) {
      console.log('[Dist] Finalization...');
      finalize();
    }
    done(0);
  });
}

  • antd下的webpack.config.js

    调用getWebpackConfig函数获取基础配置,导出了多份webpack配置,为Antd打包不同的主题色css样式

/* eslint no-param-reassign: 0 */
// This config is for building dist files
const getWebpackConfig = require('@ant-design/tools/lib/getWebpackConfig');
const IgnoreEmitPlugin = require('ignore-emit-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { ESBuildPlugin, ESBuildMinifyPlugin } = require('esbuild-loader');
const darkVars = require('./scripts/dark-vars');
const compactVars = require('./scripts/compact-vars');

const { webpack } = getWebpackConfig;

// noParse still leave `require('./locale' + name)` in dist files
// ignore is better: http://stackoverflow.com/q/25384360
function ignoreMomentLocale(webpackConfig) {
  delete webpackConfig.module.noParse;
  webpackConfig.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
}

function addLocales(webpackConfig) {
  let packageName = 'antd-with-locales';
  if (webpackConfig.entry['antd.min']) {
    packageName += '.min';
  }
  webpackConfig.entry[packageName] = './index-with-locales.js';
  webpackConfig.output.filename = '[name].js';
}

function externalMoment(config) {
  config.externals.moment = {
    root: 'moment',
    commonjs2: 'moment',
    commonjs: 'moment',
    amd: 'moment',
  };
}

function injectWarningCondition(config) {
  config.module.rules.forEach(rule => {
    // Remove devWarning if needed
    if (rule.test.test('test.tsx')) {
      rule.use = [
        ...rule.use,
        {
          loader: 'string-replace-loader',
          options: {
            search: 'devWarning(',
            replace: "if (process.env.NODE_ENV !== 'production') devWarning(",
          },
        },
      ];
    }
  });
}

function processWebpackThemeConfig(themeConfig, theme, vars) {
  themeConfig.forEach(config => {
    ignoreMomentLocale(config);
    externalMoment(config);

    // rename default entry to ${theme} entry
    Object.keys(config.entry).forEach(entryName => {
      config.entry[entryName.replace('antd', `antd.${theme}`)] = config.entry[entryName];
      delete config.entry[entryName];
    });

    // apply ${theme} less variables
    config.module.rules.forEach(rule => {
      // filter less rule
      if (rule.test instanceof RegExp && rule.test.test('.less')) {
        const lessRule = rule.use[rule.use.length - 1];
        if (lessRule.options.lessOptions) {
          lessRule.options.lessOptions.modifyVars = vars;
        } else {
          lessRule.options.modifyVars = vars;
        }
      }
    });

    const themeReg = new RegExp(`${theme}(.min)?\\.js(\\.map)?$`);
    // ignore emit ${theme} entry js & js.map file
    config.plugins.push(new IgnoreEmitPlugin(themeReg));
  });
}

const webpackConfig = getWebpackConfig(false);
const webpackDarkConfig = getWebpackConfig(false);
const webpackCompactConfig = getWebpackConfig(false);

webpackConfig.forEach(config => {
  injectWarningCondition(config);
});

if (process.env.RUN_ENV === 'PRODUCTION') {
  webpackConfig.forEach(config => {
    ignoreMomentLocale(config);
    externalMoment(config);
    addLocales(config);
    // Reduce non-minified dist files size
    config.optimization.usedExports = true;
    // use esbuild
    if (process.env.ESBUILD || process.env.CSB_REPO) {
      config.plugins.push(new ESBuildPlugin());
      config.optimization.minimizer[0] = new ESBuildMinifyPlugin({
        target: 'es2015',
      });
    }

    config.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false,
        reportFilename: '../report.html',
      }),
    );
  });

  processWebpackThemeConfig(webpackDarkConfig, 'dark', darkVars);
  processWebpackThemeConfig(webpackCompactConfig, 'compact', compactVars);
}

module.exports = [...webpackConfig, ...webpackDarkConfig, ...webpackCompactConfig];

  • getWebpackConfig.js

webapck的基础配置封装

const { getProjectPath, resolve, injectRequire } = require('./utils/projectHelper'); 

injectRequire();

// Show warning for webpack
process.traceDeprecation = true;

// Normal requirement
const path = require('path');
const webpack = require('webpack');
const WebpackBar = require('webpackbar');
const webpackMerge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');
const CleanUpStatsPlugin = require('./utils/CleanUpStatsPlugin');

const svgRegex = /\.svg(\?v=\d+\.\d+\.\d+)?$/;
const svgOptions = {
  limit: 10000,
  minetype: 'image/svg+xml',
};

const imageOptions = {
  limit: 10000,
};


//modules为是否是es6模块

function getWebpackConfig(modules) {
  const pkg = require(getProjectPath('package.json'));
  const babelConfig = require('./getBabelCommonConfig')(modules || false);


  if (modules === false) {
    babelConfig.plugins.push(require.resolve('./replaceLib'));
  }

  const config = {
    devtool: 'source-map',
    output: {
      path: getProjectPath('./dist/'),
      filename: '[name].js',
    },
    resolve: {
      modules: ['node_modules', path.join(__dirname, '../node_modules')],
      extensions: [
        '.web.tsx',
        '.web.ts',
        '.web.jsx',
        '.web.js',
        '.ts',
        '.tsx',
        '.js',
        '.jsx',
        '.json',
      ],
      alias: {
        [pkg.name]: process.cwd(),
      },
    },
    node: [
      'child_process',
      'cluster',
      'dgram',
      'dns',
      'fs',
      'module',
      'net',
      'readline',
      'repl',
      'tls',
    ].reduce(
      (acc, name) => ({
        ...acc,
        [name]: 'empty',
      }),
      {}
    ),

    module: {
      noParse: [/moment.js/],
      rules: [
        {
          test: /\.jsx?$/,
          exclude: /node_modules/,
          loader: resolve('babel-loader'),
          options: babelConfig,
        },
        {
          test: /\.tsx?$/,
          use: [
            {
              loader: resolve('babel-loader'),
              options: babelConfig,
            },
            {
              loader: resolve('ts-loader'),
              options: {
                transpileOnly: true,
              },
            },
          ],
        },
        {
          test: /\.css$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: resolve('css-loader'),
              options: {
                sourceMap: true,
              },
            },
            {
              loader: resolve('postcss-loader'),
              options: {
                postcssOptions: {
                  plugins: ['autoprefixer'],
                },
                sourceMap: true,
              },
            },
          ],
        },
        {
          test: /\.less$/,
          use: [
            MiniCssExtractPlugin.loader,
            {
              loader: resolve('css-loader'),
              options: {
                sourceMap: true,
              },
            },
            {
              loader: resolve('postcss-loader'),
              options: {
                postcssOptions: {
                  plugins: ['autoprefixer'],
                },
                sourceMap: true,
              },
            },
            {
              loader: resolve('less-loader'),
              options: {
                lessOptions: {
                  javascriptEnabled: true,
                },
                sourceMap: true,
              },
            },
          ],
        },

        // Images
        {
          test: svgRegex,
          loader: resolve('url-loader'),
          options: svgOptions,
        },
        {
          test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i,
          loader: resolve('url-loader'),
          options: imageOptions,
        },
      ],
    },

    plugins: [
      new CaseSensitivePathsPlugin(),
      new webpack.BannerPlugin(`
${pkg.name} v${pkg.version}

Copyright 2015-present, Alipay, Inc.
All rights reserved.
      `),
      new WebpackBar({
        name: '🚚  Ant Design Tools',
        color: '#2f54eb',
      }),
      new CleanUpStatsPlugin(),
      new FilterWarningsPlugin({
        // suppress conflicting order warnings from mini-css-extract-plugin.
        // ref: https://github.com/ant-design/ant-design/issues/14895
        // see https://github.com/webpack-contrib/mini-css-extract-plugin/issues/250
        exclude: /mini-css-extract-plugin[^]*Conflicting order between:/,
      }),
    ],

    performance: {
      hints: false,
    },
  };

  if (process.env.RUN_ENV === 'PRODUCTION') {
    const entry = ['./index'];

    // Common config
    config.externals = {
      react: {
        root: 'React',
        commonjs2: 'react',
        commonjs: 'react',
        amd: 'react',
      },
      'react-dom': {
        root: 'ReactDOM',
        commonjs2: 'react-dom',
        commonjs: 'react-dom',
        amd: 'react-dom',
      },
    };
    config.output.library = pkg.name;
    config.output.libraryTarget = 'umd';
    config.optimization = {
      minimizer: [
        new UglifyJsPlugin({
          cache: true,
          parallel: true,
          sourceMap: true,
          uglifyOptions: {
            warnings: false,
          },
        }),
      ],
    };

    // Development
    const uncompressedConfig = webpackMerge({}, config, {
      entry: {
        [pkg.name]: entry,
      },
      mode: 'development',
      plugins: [
        new MiniCssExtractPlugin({
          filename: '[name].css',
        }),
      ],
    });

    // Production
    const prodConfig = webpackMerge({}, config, {
      entry: {
        [`${pkg.name}.min`]: entry,
      },
      mode: 'production',
      plugins: [
        new webpack.optimize.ModuleConcatenationPlugin(),
        new webpack.LoaderOptionsPlugin({
          minimize: true,
        }),
        new MiniCssExtractPlugin({
          filename: '[name].css',
        }),
      ],
      optimization: {
        minimize: true,
        minimizer: [new CssMinimizerPlugin({})],
      },
    });

    return [prodConfig, uncompressedConfig];
  }

  return [config];
}
getWebpackConfig.webpack = webpack;
getWebpackConfig.svgRegex = svgRegex;
getWebpackConfig.svgOptions = svgOptions;
getWebpackConfig.imageOptions = imageOptions;

module.exports = getWebpackConfig;

  • Antd Webpack配置的入口文件
/* eslint no-console:0 */
function pascalCase(name) {
  return name.charAt(0).toUpperCase() + name.slice(1).replace(/-(\w)/g, (m, n) => n.toUpperCase());
}

// Just import style for https://github.com/ant-design/ant-design/issues/3745
const req = require.context('./components', true, /^\.\/[^_][\w-]+\/style\/index\.tsx?$/);

req.keys().forEach(mod => {
  let v = req(mod);
  if (v && v.default) {
    v = v.default;
  }
  const match = mod.match(/^\.\/([^_][\w-]+)\/index\.tsx?$/);
  console.warn('match', match)
  if (match && match[1]) {
    if (match[1] === 'message' || match[1] === 'notification') {
      // message & notification should not be capitalized
      exports[match[1]] = v;
    } else {
      exports[pascalCase(match[1])] = v;
    }
  }
});

module.exports = require('./components');

结语

其实antd-tools还有很多其他的指令,比如pub,ts-lint,ts-lint-fix,watch-tsc等,笔者只分析了较为关键的compile,dist这两个指令,看这些也只是因为比较好奇Antd框架的构建是怎么实现的,其它的貌似也没有太大的学习意义