[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 LRA(导入生产的配置文件)-->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优化 | 可配置 | ✔ |
进度指示 | ✔ | 无 |
ps:求职 2020 届实习内推.