背景
笔者最近是在做活动页可视化编辑平台,某天发现了页面上使用富文本编辑出来的内容,在平台上的编辑效果与预览效果不一致,经过简单排查后得知是因为平台的编辑和预览区分了路由,分别使用了React.lazy+ Import
做了路由懒加载,由于编辑路由引入了 Antd UI库,但预览路由是不会把Antd相关的代码打包进去的,而Antd的global.css
内部重写了多个标签的默认样式,所以引发了问题
那具体Antd的global.css
是在什么时候被引入进来的呢?Antd UI库的构建流程具体是怎样,组件按需加载方案是如何设计的?带着这些疑惑,笔者开始了一场Antd的源码调试,从源码角度抛析Antd的按需加载与构建方案
使用yarn link 调试本地的Antd包
首先第一步是梳理Antd框架整体的构建流程,那如何在本地项目上调试本地的Antd包呢?答案就是Yarn Link
按以下步骤执行
- 本地 git clone antd-design仓库
- 在antd-design根目录执行
yarn install
和yarn run build
先构建出源代码 - antd-design根目录中执行
yarn link
- 在本地项目根目录(指需要调试antd的项目)执行
yarn link antd
完成这些步骤后本地项目执行yarn run dev
启动,查看页面中Antd组件是否正常显示,如无意外浏览器的控制台将看到如下错误
这是因为Antd中的package.json
中peerDependencies
所依赖的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的入口文件配置了main
和module
字段。分别表示在不同环境下进行 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-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命令,可以按如下步骤操作
- 在Antd项目中的npm-scripts加上一条命令,改为使用node命令的方式来启动脚手架
"debug": "node --inspect-brk=9999 ./node_modules/.bin/antd-tools-run compile"
- 配置launch.json
按照图中标记的步骤进行配置,最后点击绿色三角运行,即可在入口文件处自动断点,具体launch.json的配置字段可查看VSCode launch.json配置详解
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框架的构建是怎么实现的,其它的貌似也没有太大的学习意义