# 按需加载

# webpack插件分析

直接看webpack-transform-modules-plugin的代码,核心代码如下

function runGetter(compiler, callback) {
  var transformModules = that._collectTransformModules(compiler);
  if (Object.keys(transformModules).length) {
    var rules = compiler.options.module.rules;
    rules && rules.forEach(iterRule);
  }
  callback();

  function interRule() {}
  function doRule() {}
  function updateLoaderConf() {}
  function updateRule() {}
  function updateVueLoaderOptions() {}
}

compiler.hooks.beforeRun.tapAsync('TransformModulesPlugin', runGetter);
compiler.hooks.watchRun.tapAsync('TransformModulesPlugin', runGetter);

_collectTransformModules方法的核心是获取transformModules的配置,通过查找查找项目的package.json配置,找到对应的transformModules配置并添加后返回,再递归的查找项目的dependencies的配置,找出所有的transformModules配置后并返回。

接着获取到当前webpack的所有module.rules配置,并循环执行interRule方法

function iterRule(rule, i) {
  if (doRule(rule, 'loader')) {
  } else if (doRule(rule, 'use')) {
  } else if (rule.oneOf) {
    if (!Array.isArray(rule.oneOf)) {
      rule.oneOf = [rule.oneOf];
    }
    rule.oneOf.forEach(iterRule);
  }
}
function doRule(rule, key) {
  var conf = rule[key];
  if (conf) {
    if (Array.isArray(conf)) {
      conf.forEach(function (loaderConf, i) {
        if (typeof loaderConf === 'string') {
          loaderConf = conf[i] = {
            loader: loaderConf,
          };
        }
        updateLoaderConf(loaderConf);
      });
    } else {
      if (key === 'use') {
        if (typeof conf === 'string') {
          conf = rule.use = {
            loader: conf,
          };
        }
        updateLoaderConf(conf);
      } else {
        updateLoaderConf(rule);
      }
    }
    return true;
  }
  return false;
}
function updateLoaderConf(rule) {
  if (util.loaderNameMatches(rule, 'babel-loader')) {
    updateRule(rule);
  } else if (util.loaderNameMatches(rule, 'vue-loader')) {
    updateVueLoaderOptions(rule);
  }
}

因为loader的有多种配置方法,iterRule就是根据不同的配置方式调用doRule这个方法。doRule方法核心是调用updateLoaderConf方法,根据配置的不同处理不同的入参.updateLoaderConf这个方法,根据rule来判断是vue-loader还是babel-loader来调用不同的处理方法

function updateRule(conf) {
  if (!conf.options) {
    conf.options = {};
  }
  if (!conf.options.plugins) {
    conf.options.plugins = [];
  }
  if (!Array.isArray(conf.options.plugins)) {
    conf.options.plugins = [conf.options.plugins];
  }
  var name = 'babel-plugin-transform-modules';
  var plugins = conf.options.plugins;
  var added = plugins.some(function (plugin) {
    return plugin === name || plugin[0] === name;
  });
  !added && plugins.push([name, transformModules]);
}
function updateVueLoaderOptions(conf) {
  if (!conf.options) {
    conf.options = {};
  }
  if (!conf.options.loaders) {
    conf.options.loaders = {};
  }
  var jsConf = conf.options.loaders.js;
  if (!jsConf) {
    jsConf = conf.options.loaders.js = [];
  } else if (typeof jsConf === 'string') {
    if (jsConf === 'babel-loader') {
      // reset
      jsConf = conf.options.loaders.js = [];
    } else {
      jsConf = conf.options.loaders.js = [
        {
          loader: jsConf,
        },
      ];
    }
  }
  if (typeof jsConf === 'object' && jsConf.loader === 'babel-loader') {
    jsConf.plugins = jsConf.plugins || [];
    jsConf.plugins.push(['babel-plugin-transform-modules', transformModules]);
  } else {
    jsConf.push({
      loader: 'babel-loader',
      options: {
        plugins: [['babel-plugin-transform-modules', transformModules]],
      },
    });
  }
}

updateRule方法用来处理babel-loader核心就是给babel-loader的参数添加上babel-plugin-transform-modules这个 plugin。类型如下

//处理前
{ loader:'babel-loader' }
//处理后
{
    loader:'babel-loader',
    options:{
        plugins:[['babel-plugin-transform-modules',{ /* transformModule */ }]]
    }
}

updateVueLoaderOptions方法用来处理vue-loader的参数,主要是添加babel-loader配置

//处理前
{ loader:'vue-loader' }
//处理后
{
    loader:'vue-loader',
    options:{
        loaders:{
            js:[
                {
                    loader:'babel-loader',
                    options:{
                        plugins:[ 'babel-plugin-transform-modules',{ /* transformModule */ } ]
                    }
                }
            ]
        }
    }
}

所以,webpack-transform-modules-plugin核心就是给webpackbabel-loader vue-loader添加额外的参数。其实这部分我们在自己项目的webpack中就可以添加,唯一实现的增强就是递归去查找所有依赖中的transformModule配置

# babel插件分析

babel-plugin-transform-modules是一个babel插件,核心是处理import语句的。以一个例子来说明

// 假设上面的transformModule配置如下
var transformModules = {
  'element-ui': {
    kebabCase: true,
    transform: 'element-ui/src/components/${member}',
  },
};
// 导入方式如下
import ElementUI, { Message } from 'element-ui';

上面的代码,带入到babel-plugin-transform-modules这个插件的源码中进行理解

module.exports = function (babel) {
  var types = babel.types;
  return {
    visitor: {
      ImportDeclaration: function (path, state) {
        var source = path.node.source.value; //导入的源 例子中的 ‘element-ui’
        // state.opts plugin的参数 就是 transformModules 这个对象
        // 这段是处理相对路径的导入 不作分析
        if (!(source in state.opts) && source.match(/^\.{0,2}\//)) {
          source = pathLib.resolve(
            pathLib.join(
              source[0] === '/'
                ? ''
                : pathLib.dirname(state.file.opts.filename),
              source,
            ),
          );
        }
        if (source in state.opts) {
          var opts = state.opts[source];

          var transforms = [];
          var styleTransforms = [];

          // 全量导入的节点 例子中的 ElementUI
          var fullImports = path.node.specifiers.filter(function (specifier) {
            return specifier.type !== 'ImportSpecifier';
          });
          // 成员导入的节点 例子中的 Message
          var memberImports = path.node.specifiers.filter(function (specifier) {
            return specifier.type === 'ImportSpecifier';
          });

          if (fullImports.length > 0) {
            if (memberImports.length > 0) {
              // 如果一个导入语句中既有全局导入也有成员导入 那么往 transform 中插入一条全量导入
              // Exp: import ElementUI from 'element-ui'
              transforms.push(
                types.importDeclaration(
                  fullImports,
                  types.stringLiteral(source),
                ),
              );
            }
            // 处理样式导入
            handleStyleImport(opts, styleTransforms, types);
          }
          var hasFullStyleImports = styleTransforms.length > 0;
          memberImports.forEach(function () {
            // 根据配置处理name
            var importName = memberImport.imported.name;
            if (opts.camelCase) importName = camel(importName);
            if (opts.kebabCase) importName = kebab(importName);
            if (opts.snakeCase) importName = snake(importName);

            // 生成替换节点source字符串
            // 'element-ui/src/components/message'
            var replace = transform(opts.transform, importName);

            // 是否保存默认语法
            // 不保存则转成 default语法 import message
            var newImportSpecifier = opts.skipDefaultConversion
              ? memberImport
              : types.importDefaultSpecifier(
                  types.identifier(memberImport.local.name),
                );

            // 最后生成完整的导入语句节点
            // import message from 'element-ui/src/components/message'
            transforms.push(
              types.importDeclaration(
                [newImportSpecifier],
                types.stringLiteral(replace),
              ),
            );

            // 判断是否有全量导入样式的节点
            // 如果没有则处理成员的样式节点
            !hasFullStyleImports &&
              handleStyleImport(opts, styleTransforms, types, importName);
          });

          // 最后替换或插入语句,本例中最后的语句为
          // import ElementUI from 'element-ui'
          // import message from 'element-ui/src/components/message'
          if (transforms.length > 0) {
            path.replaceWithMultiple(transforms.concat(styleTransforms));
          } else if (styleTransforms.length) {
            path.insertAfter(styleTransforms);
          }
        }
      },
    },
  };
};

可见得,babel-plugin-transform-modules这个插件主要就是对import的语句做了处理,再看下handleStyleImport的处理

function handleStyleImport(opts, styleTransforms, types, importName) {
  if (opts.style) {
    // parseStyleOption 返回 { name:'styleName',ignore:[] } 这样标准格式的对象
    var styleOption = parseStyleOption(opts.style);
    var styleName = styleOption.name;
    var ignore = styleOption.ignore;
    var hasImportName = !!importName;
    if (!importName) {
      importName = styleName;
    }
    if (ignore.indexOf(importName) >= 0) {
      return;
    }
    // 这里返回的结果与是否是全量导入有关
    // 全量导入 ‘element-ui/src/components/style.css’
    // 成员导入 ‘element-ui/src/components/input/style.css’
    var replace = transform(
      opts.transform,
      importName,
      styleName,
      hasImportName,
    );
    // 非标记符的导入方式 exp: import ‘element-ui/src/components/style.css’
    styleTransforms.push(
      types.importDeclaration([], types.stringLiteral(replace)),
    );
  }
}

# 总结

  • webpack-transform-modules-plugin作为一个webpack插件,主要作用是动态的修改module.rules里面的loader配置,给babel-loader加上babel-plugin-transform-modules这个插件,给vue-loaderoptions.loader.js添加babel-loader。且会递归的的查看依赖的package.json找到transformModules配置,作为插件的参数
  • babel-plugin-transform-modules插件作为一个babel插件,主要是分析import语句。再根据传入的参数修改添加新的import语句,实现开发过程中简化导入语法的目的