什么是按需引入?

当我们在引用第三方模块时,如果不注意,往往会把第三方模块全部打包进我们的项目,但是我们实际上不会用用到全部的代码,甚至可以说只会用到很少一部分,所以这是很不友好的。
这时我们就需要用到按需引入,只打包用到的部分代码。
一般我们使用 babel-plugin-import 插件实现。
我们用一个案例来解释。

  1. /** ./src/index.js **/
  2. import { flatten, concat } from 'lodash';
  3. console.log(flatten, concat);

webpack.config.js 不做任何配置,我进行打包。
结果如下:
image.png
我们发现打包后的文件非常大,这是因为他把所有的 lodash 文件都打包进来了,没有用到的也打包进来了,而我们要把没用的代码去除。

使用限制

只适用于特定的项目,如 lodash,antd,antd-mobile,lodash,material-ui 等符合特定结构的项目。
必须满足如下的项目结构:

  • 每个功能必须按文件分开(如果你的功能都写在一个文件中的,那就无法实现按需引入)。

image.pngimage.png

如何使用?

安装依赖

我们需要安装 babel-loader 和 babel-plugin-import

// 安装 babel-loader 和 babel-plugin-import
yarn add babel-loader babel-plugin-import --dev

配置 webpack.config.js

/** ./webpack.config.js **/
const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules:[
      {
        test:/\.js$/,
        use:{
          loader:'babel-loader',
          options: {
            plugins:[['import', {
              // 指定要按需加载的模块
              'libraryName': 'lodash',
              // 按需加载的目录,默认是 lib
              'libraryDirectory':'',
              'camel2DashComponentName':false
            }]]
          }
        }
      }
    ]
  }
}

我们再来看一下打包后的结果
image.png
发现只引用了用到的部分代码,体积从原来的 554KB 变成了 21.5KB,大大的优化了打包效率。

原理

其实这个插件的原理很简单,看一下 index.js 文件,使用了插件后原本的代码可以理解为如下:

//import { flatten, concat } from 'lodash';

// 实现按需加载相当于以下代码
import flatten from 'lodash/flatten';
import concat from 'lodash/concat';

console.log(flatten, concat);

打包的结果也是这样的。
image.png

实现 babel-plugin-import

最后我们不用他的插件,自己实现一个。
首先我们在根目录创建一个 import.js,这个就是我们插件文件,后面介绍。

如何引用?

/** ./webpack.config.js **/
const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules:[
      {
        test:/\.js$/,
        use:{
          loader:'babel-loader',
          options: {
            // 把原来引入的包改成本地路径
            plugins:[[path.resolve(__dirname, 'import'), {
              'libraryName': 'lodash',
              'libraryDirectory':'',
              'camel2DashComponentName':false
            }]]
          }
        }
      }
    ]
  }
}

目标

首先我们得知道我们要干嘛,观察下面两张图,我们要做的就是把上面的的语法树转换成下面的语法树。
image.png
image.png

实现

现在我们来写这个 import.js

const types = require('@babel/types');

const visitor = {
  ImportDeclaration(path, state){
    const { node } = path; //获取节点
    const { specifiers } = node; //获取批量导入声明数组
    const { libraryName } = state.opts; //获取选项中的支持的库的名称

    // 如果当前的节点的模块名称是我们需要的库的名称并且不是默认导入才会进来
    if(node.source.value === libraryName && !types.isImportDefaultSpecifier(specifiers[0])){
      // 遍历批量导入声明数组
      const declarations = specifiers.map(specifier => {
        // 返回一个 importDeclaration 节点
        return types.importDeclaration(
          // 导入声明 importDefaultSpecifier flatten
          [types.importDefaultSpecifier(specifier.local)],
          // 导入模块 source lodash/flatten
          types.stringLiteral(`${libraryName}/${specifier.imported.name}`)
        )
      });
      path.replaceWithMultiple(declarations);// 替换当前节点
    }
  }
}

module.exports = function() {
  return {
    visitor
  }
}