[toc]
对外配置文件对比
umi
.umirc.js
// ref: https://umijs.org/config/
export default {
treeShaking: true,
plugins: [
// ref: https://umijs.org/plugin/umi-plugin-react.html
['umi-plugin-react', {
antd: false,
dva: false,
dynamicImport: false,
title: 'umilearn',
dll: false,
routes: {
exclude: [
/components\//,
],
},
}],
],
}
可以看到umi的配置文件和webpack的标准配置文件明显不同.对于大多数的构建配置做到配置大于约定。后面我们会来看仔细看。
create-react-app
//使用npm run eject 输出配置文件
npm run eject
webpack.config.js
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
const publicPath = isEnvProduction
? paths.servedPath
: isEnvDevelopment && '/';
const shouldUseRelativeAssetPaths = publicPath === './';
const publicUrl = isEnvProduction
? publicPath.slice(0, -1)
: isEnvDevelopment && '';
const env = getClientEnvironment(publicUrl);
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: Object.assign({}, shouldUseRelativeAssetPaths
? {
publicPath: '../../'
}
: undefined)
}, {
loader: require.resolve('css-loader'),
options: cssOptions
}, {
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'), require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
],
sourceMap: isEnvProduction && shouldUseSourceMap
}
}
].filter(Boolean);
if (preProcessor) {
loaders.push({
loader: require.resolve(preProcessor),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap
}
});
}
return loaders;
};
return {
mode: isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false : isEnvDevelopment && 'cheap-module-source-map',
entry: [
isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs
].filter(Boolean),
output: {
path: isEnvProduction
? paths.appBuild
: undefined,
pathinfo: isEnvDevelopment,
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
publicPath: publicPath,
devtoolModuleFilenameTemplate: isEnvProduction
? info => path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment && (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'))
},
optimization: {
minimize: isEnvProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2
},
mangle: {
safari10: true
},
output: {
ecma: 5,
comments: false,
ascii_only: true
}
},
parallel: true,
cache: true,
sourceMap: shouldUseSourceMap
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
inline: false,
annotation: true
}
: false
}
})
],
splitChunks: {
chunks: 'all',
name: false
},
runtimeChunk: true
},
resolve: {
modules: ['node_modules'].concat(process.env.NODE_PATH.split(path.delimiter).filter(Boolean)),
extensions: paths
.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
'react-native': 'react-native-web'
},
plugins: [
PnpWebpackPlugin,
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson])
]
},
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)]
},
module: {
strictExportPresence: true,
rules: [
{
parser: {
requireEnsure: false
}
}, {
test: /\.(js|mjs|jsx)$/,
enforce: 'pre',
use: [
{
options: {
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint')
},
loader: require.resolve('eslint-loader')
}
],
include: paths.appSrc
}, {
oneOf: [
{
test: [
/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/
],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]'
}
}, {
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'), {
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+ref![path]'
}
}
}
]
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
compact: isEnvProduction
}
}, {
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'), {
helpers: true
}
]
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
sourceMaps: false
}
}, {
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap
}),
sideEffects: true
}, {
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent
})
}, {
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap
}, 'sass-loader'),
sideEffects: true
}, {
test: sassModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent
}, 'sass-loader')
}, {
loader: require.resolve('file-loader'),
exclude: [
/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/
],
options: {
name: 'static/media/[name].[hash:8].[ext]'
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin(Object.assign({}, {
inject: true,
template: paths.appHtml
}, isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}
: undefined)),
isEnvProduction && shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
new ModuleNotFoundPlugin(paths.appPath),
new webpack.DefinePlugin(env.stringified),
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvDevelopment && new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction && new MiniCssExtractPlugin({filename: 'static/css/[name].[contenthash:8].css', chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'}),
new ManifestPlugin({fileName: 'asset-manifest.json', publicPath: publicPath}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
isEnvProduction && new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [
/\.map$/, /asset-manifest\.json$/
],
importWorkboxFrom: 'cdn',
navigateFallback: publicUrl + '/index.html',
navigateFallbackBlacklist: [new RegExp('^/_'), new RegExp('/[^/]+\\.[^/]+$')]
}),
useTypeScript && new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {basedir: paths.appNodeModules}),
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
tsconfig: paths.appTsConfig,
reportFiles: [
'**',
'!**/*.json',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*'
],
watch: paths.appSrc,
silent: true,
formatter: isEnvProduction
? typescriptFormatter
: undefined
})
].filter(Boolean),
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
performance: false
};
};
可以看出create-react-app 提供出了一份接近标准的webpack配置文件。entry和output都明显列出。
构建流程
create-react-app
scripts/build.js
const config = configFactory('production');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
fs.emptyDirSync(paths.appBuild);
copyPublicFolder();
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
//.....
});
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
//....
});
});
}
从build的脚本中可以明确的看出来构建流程为
graph LR
A(导入生产的配置文件)-->B(检查文件目录)
B-->C(计算机上次输出文件大小,并清空)
C-->D(把公共文件复制到输出目录)
D-->E(交给webpack打包)
构建流程非常简单,对于只想做简单的单页应用的开发者来说应该来说是比较合适的.
umi的构建过程
脚本方法调用
umi/src/cli.js --> umi/src/scripts/builds->umi-build-dev/lib/service->run()
umi那么如何将将一个简单的可扩展的配置文件转换成一个标准的webpack配置文件呢.
umi 的整个生命周期都是插件化的,甚至其内部实现就是由大量插件组成,比如 pwa、按需加载、一键切换 preact、一键兼容 ie9 等等,都是由插件实现
umi的插件机制设计的尤为巧妙。我们来以打包方式为入口来分析一下
umi-build-dev/src/Service.js
class Service{
//插件注册的命令
this.commands = {};
//所有应用的插件钩子
this.pluginHooks = {};
//所有内置的插件方法
this.pluginMethods = {};
//初始插件
initPlugin(plugin) {
const { id, apply, opts } = plugin;
//通过代理PlugApi使得 插件可以通过api.[theapiname] 访问到预置的pluginMethods 并注入钩子,
//总感觉这里设计的有点不合理.
const api = new Proxy(new PluginAPI(id, this), {
get: (target, prop) => {
if (this.pluginMethods[prop]) {
return this.pluginMethods[prop];
}
});
apply(api, opts);
plugin._api = api;
} catch (e) {
if (process.env.UMI_TEST) {
throw new Error(e);
} else {
signale.error(
`
Plugin ${chalk.cyan.underline(id)} initialize failed
${getCodeFrame(e, { cwd: this.cwd })}
`.trim(),
);
debug(e);
process.exit(1);
}
}
}
//应用插件方法
applyPlugins(){
//通过reduce方法对于不同的插件注册的同一方法 进行有序调用,并记录调用结果。
return (this.pluginHooks[key] || []).reduce((memo, { fn }) => {
try {
return fn({
memo,
args: opts.args,
});
} catch (e) {
console.error(chalk.red(`Plugin apply failed: ${e.message}`));
throw e;
}
}, opts.initialValue);
}
runCommand(rawName, rawArgs) {
const { fn, opts } = command;
if (opts.webpack) {
// webpack config
this.webpackConfig = require('./getWebpackConfig').default(this);
}
return fn(args);
}
}
下面我们来build插件也是作为umi生命周期的一部分是如何来实现的.
umi-build-dev/src/plugins/commands/build
export default function(api) {
const { service, debug, config, log } = api;
const { cwd, paths } = service;
api.registerCommand(
'build',
{
webpack: true,
description: 'building for production',
},
() => {
notify.onBuildStart({ name: 'umi', version: 2 });
const RoutesManager = getRouteManager(service);
RoutesManager.fetchRoutes();
return new Promise((resolve, reject) => {
process.env.NODE_ENV = 'production';
service.applyPlugins('onStart');
service._applyPluginsAsync('onStartAsync').then(() => {
const filesGenerator = getFilesGenerator(service, {
RoutesManager,
mountElementId: config.mountElementId,
});
filesGenerator.generate();
if (process.env.HTML !== 'none') {
const HtmlGeneratorPlugin = require('../getHtmlGeneratorPlugin').default(
service,
);
service.webpackConfig.plugins.unshift(new HtmlGeneratorPlugin());
}
require('af-webpack/build').default({
cwd,
webpackConfig: service.webpackConfig,
onSuccess({ stats }) {
if (process.env.RM_TMPDIR !== 'none') {
debug(`Clean tmp dir ${service.paths.tmpDirPath}`);
rimraf.sync(paths.absTmpDirPath);
}
.,...
},
onFail({ err, stats }) {
});
},
});
});
});
},
);
}
可以看出 build.js中 注册了命令build 获取service中的webpackconfig 传入af-webpack 进一步处理.
当然这里最重要的还是去生成路由等相关文件,再交由af-webpack 去生成.
再build成功后也会及时清除这些文件,并且调用预留hooks(onBuildSuccess,onBuildSuccessAsync).可以通过这里去实现一些前端部署的方法。
那么webpackconfig到底有哪些约定的配置呢。可以看到
umi-build-dev/src/plugins/afwebpack-config
export default function(api) {
const { debug, cwd, config, paths } = api;
// 把 af-webpack 的配置插件转化为 umi-build-dev 的
api._registerConfig(() => {
return plugins
.filter(p => !excludes.includes(p.name))
.map(({ name, validate = noop }) => {
return api => ({
name,
validate,
onChange(newConfig) {
try {
debug(
`Config ${name} changed to ${JSON.stringify(newConfig[name])}`,
);
} catch (e) {}
if (name === 'proxy') {
global.g_umi_reloadProxy(newConfig[name]);
} else {
api.service.restart(`${name} changed`);
}
},
});
});
});
const reactDir = compatDirname(
'react/package.json',
cwd,
dirname(require.resolve('react/package.json')),
);
const reactDOMDir = compatDirname(
'react-dom/package.json',
cwd,
dirname(require.resolve('react-dom/package.json')),
);
const reactRouterDir = compatDirname(
'react-router/package.json',
cwd,
dirname(require.resolve('react-router/package.json')),
);
const reactRouterDOMDir = compatDirname(
'react-router-dom/package.json',
cwd,
dirname(require.resolve('react-router-dom/package.json')),
);
const reactRouterConfigDir = compatDirname(
'react-router-config/package.json',
cwd,
dirname(require.resolve('react-router-config/package.json')),
);
api.chainWebpackConfig(webpackConfig => {
webpackConfig.resolve.alias
.set('react', reactDir)
.set('react-dom', reactDOMDir)
.set('react-router', reactRouterDir)
.set('react-router-dom', reactRouterDOMDir)
.set('react-router-config', reactRouterConfigDir)
.set(
'history',
compatDirname(
'umi-history/package.json',
cwd,
dirname(require.resolve('umi-history/package.json')),
),
)
.set('@', paths.absSrcPath)
.set('@tmp', paths.absTmpDirPath)
.set('umi/link', join(process.env.UMI_DIR, 'lib/link.js'))
.set('umi/dynamic', join(process.env.UMI_DIR, 'lib/dynamic.js'))
.set('umi/navlink', join(process.env.UMI_DIR, 'lib/navlink.js'))
.set('umi/redirect', join(process.env.UMI_DIR, 'lib/redirect.js'))
.set('umi/prompt', join(process.env.UMI_DIR, 'lib/prompt.js'))
.set('umi/router', join(process.env.UMI_DIR, 'lib/router.js'))
.set('umi/withRouter', join(process.env.UMI_DIR, 'lib/withRouter.js'))
.set(
'umi/_renderRoutes',
join(process.env.UMI_DIR, 'lib/renderRoutes.js'),
)
.set(
'umi/_createHistory',
join(process.env.UMI_DIR, 'lib/createHistory.js'),
)
.set(
'umi/_runtimePlugin',
join(process.env.UMI_DIR, 'lib/runtimePlugin.js'),
);
});
api.addVersionInfo([
`react@${require(join(reactDir, 'package.json')).version} (${reactDir})`,
`react-dom@${
require(join(reactDOMDir, 'package.json')).version
} (${reactDOMDir})`,
`react-router@${
require(join(reactRouterDir, 'package.json')).version
} (${reactRouterDir})`,
`react-router-dom@${
require(join(reactRouterDOMDir, 'package.json')).version
} (${reactRouterDOMDir})`,
`react-router-config@${
require(join(reactRouterConfigDir, 'package.json')).version
} (${reactRouterConfigDir})`,
]);
api.modifyAFWebpackOpts(memo => {
const isDev = process.env.NODE_ENV === 'development';
const entryScript = join(cwd, `./${paths.tmpDirPath}/umi.js`);
const setPublicPathFile = join(
__dirname,
'../../../template/setPublicPath.js',
);
const setPublicPath =
config.runtimePublicPath ||
(config.exportStatic && config.exportStatic.dynamicRoot);
const entry = isDev
? {
umi: [
...(process.env.HMR === 'none' ? [] : [webpackHotDevClientPath]),
...(setPublicPath ? [setPublicPathFile] : []),
entryScript,
],
}
: {
umi: [...(setPublicPath ? [setPublicPathFile] : []), entryScript],
};
const targets = {
chrome: 49,
firefox: 64,
safari: 10,
edge: 13,
ios: 10,
...(config.targets || {}),
};
// Transform targets to browserslist for autoprefixer
const browserslist =
config.browserslist ||
targets.browsers ||
Object.keys(targets)
.filter(key => {
return !['node', 'esmodules'].includes(key);
})
.map(key => {
return `${key} >= ${targets[key]}`;
});
return {
...memo,
...config,
cwd,
browserslist,
entry,
absNodeModulesPath: paths.absNodeModulesPath,
outputPath: paths.absOutputPath,
disableDynamicImport: true,
babel: config.babel || {
presets: [
[
require.resolve('babel-preset-umi'),
{
targets,
env: {
useBuiltIns: 'entry',
...(config.treeShaking ? { modules: false } : {}),
},
},
],
],
plugins: [require.resolve('./lockCoreJSVersionPlugin')],
},
define: {
'process.env.BASE_URL': config.base || '/',
__UMI_BIGFISH_COMPAT: process.env.BIGFISH_COMPAT,
__UMI_HTML_SUFFIX: !!(
config.exportStatic &&
typeof config.exportStatic === 'object' &&
config.exportStatic.htmlSuffix
),
...(config.define || {}),
},
publicPath: isDev
? '/'
: config.publicPath != null
? config.publicPath
: '/',
};
});
}
umi在这里先配置了主要的几个重要配置包括
entry,publicPath,babel等重要参数
又利用webpack-chain 设置resolve中的alias确保 umi的相关api被准确引用到.ih
但是仅靠这里的配置是不够的.看到getWebPackConfig.js
import getConfig from 'af-webpack/getConfig';
import assert from 'assert';
export default function(service) {
const { config } = service;
const afWebpackOpts = service.applyPlugins('modifyAFWebpackOpts', {
initialValue: {
cwd: service.cwd,
},
});
assert(
!('chainConfig' in afWebpackOpts),
`chainConfig should not supplied in modifyAFWebpackOpts`,
);
afWebpackOpts.chainConfig = webpackConfig => {
service.applyPlugins('chainWebpackConfig', {
args: webpackConfig,
});
if (config.chainWebpack) {
config.chainWebpack(webpackConfig, {
webpack: require('af-webpack/webpack'),
});
}
};
return service.applyPlugins('modifyWebpackConfig', {
initialValue: getConfig(afWebpackOpts),
});
}
可以得知 最初的配置还是要从af-webpack中获取的.
看到af-webpack/src/getConfig/index.js
太长,不沾了。简单总结一下
配置入口,输入,配置相关resolve
添加babel css url svg 等相关处理loader
增加必要插件如 从public中粘贴文件进入输出文件夹
最终返回一个标准的webpack配置。
最终看到af-webpack/src/build
export default function build(opts = {}) {
const { webpackConfig, cwd = process.cwd(), onSuccess, onFail } = opts;
assert(webpackConfig, 'webpackConfig should be supplied.');
assert(isPlainObject(webpackConfig), 'webpackConfig should be plain object.');
debug(
`Clean output path ${webpackConfig.output.path.replace(`${cwd}/`, '')}`,
);
rimraf.sync(webpackConfig.output.path);
debug('build start');
webpack(webpackConfig, (err, stats) => {
debug('build done');
if (err || stats.hasErrors()) {
if (onFail) {
onFail({ err, stats });
}
if (!process.env.UMI_TEST) {
process.exit(1);
}
}
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
{
root: webpackConfig.output.path,
sizes: {},
},
webpackConfig.output.path,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE,
);
console.log();
if (onSuccess) {
onSuccess({ stats });
}
});
}
将配置信息传入webpack处理.
总结
不管是umi和create-react-app,最终都是将核心打包工作交给webpack实现。我们通过打印配置信息来对比一下两个脚手架在webpack配置项中的重要几点优化的处理.
对比项 | umi | create-react-app |
---|---|---|
搜索范围优化(resolve) | ✔ | ✔ |
js & css压缩处理 | ✔ | ✔ |
sass&&less&svg处理 | ✔ | ✔ |
动态导入 | ✔ | ✖ |
插件缓存使用 | ✔ | ✔ |
ts 支持 | ✔ | ✔ |
moment locale优化 | 可配置 | ✔ |
进度指示 | ✔ | 无 |