博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
webpack学习之对比 umi 和 create-react-app的打包流程和优化配置
阅读量:6094 次
发布时间:2019-06-20

本文共 23301 字,大约阅读时间需要 77 分钟。

[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 届实习内推.

转载地址:http://xnqwa.baihongyu.com/

你可能感兴趣的文章
activity-alias的使用
查看>>
第36周日
查看>>
SQL Server 无法打开物理文件的 2 种解决办法
查看>>
推荐一款好用的文件/文件夹对比工具 —— Beyond Compare
查看>>
java设计模式--结构型模式--桥接模式
查看>>
JS window.open()属性
查看>>
手机管理中的应用【6】——电源管理篇
查看>>
【Android工具】DES终结者加密时报——AES加密演算法
查看>>
效果收集-点击显示大图
查看>>
Android 开机过程PMS分析
查看>>
找不到com.apple.Boot.plist
查看>>
使用openssl创建自签名证书及部署到IIS教程
查看>>
入门视频采集与处理(学会分析YUV数据)
查看>>
java keytool详解
查看>>
记一次Redis被攻击的事件
查看>>
Debian 的 preinst, postinst, prerm, 和 postrm 脚本
查看>>
socket编程的select模型
查看>>
IDEA和Eclipse经常使用快捷键(Win Mac)
查看>>
ubutntu apt 源
查看>>
PHP 文件处理
查看>>