使用babel-plugin-import 实现自动按需引入方式
作者:swx980
babel-plugin-import 实现自动按需引入
Vant 支持一次性导入所有组件,引入所有组件会增加代码包体积,因此不推荐这种做法
babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。
1、下载
npm i babel-plugin-import -D
2、
(1)在.babelrc 中添加配置
注意:webpack 1 无需设置 libraryDirectory
{ "plugins": [ ["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }] ] }
(2)对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = { plugins: [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true }, 'vant'] ] };
3、接着你可以在代码中直接引入 Vant 组件,插件会自动将代码转化为方式二中的按需引入形式
import { Button } from 'vant';
babel-plugin-import 的组件按需加载原理
对比webpack懒加载
webpack 懒加载是将源码中的 import、require 引入的文件编译之后再根据动态加载语法配置(通常以页面路由为基本单位)将较大的代码拆分并构建出较小的 chunk 包,运行时执行到相应业务逻辑时才去加载执行对应 chunk 代码。
webpack 懒加载主要发生在 JS 拆分出不同的 Chunk 这一过程中。
babel-plugin-import 按需加载是以组件为基本单位产出 js、css、less 文件,借助插件或者部分引入的写法,使得项目代码或 babel 编译后的代码中只包含使用到的组件的 js、css、less 等。
首先是执行时机不同,babel-plugin-import 按需加载是在源码编写阶段或者 babel 编译 js 阶段,而 webpack 懒加载则是在构建生成打包产物阶段。
其次是原理不同,babel-plugin-import 按需加载是在源码阶段就去掉了无关代码,而 webpack 懒加载则是将经过 tree-shaking 优化过后的大文件包进行拆分在适当的运行时进行按需加载。两者并不冲突,可以一前一后共同作用。
实现原理
babel-plugin-import 按需加载目的是减少项目构建打包产物的大小,提高项目线上首屏渲染速度,减少白屏时间,减少流量消耗。
若是采用手动引入需要使用到的组件以及其对应的样式文件,那么在 webpack 构件时组件库中其他未被引入的文件不会被打包。
import Button from 'lib/button'; import 'lib/lib/button/style';
若是自动引入:
npm i babel-plugin-import -D module.exports = { plugins: [ ['import', { libraryName, libraryDirectory: 'es', style: true }, libraryName] ] }; import { Button } from libraryName;
组件其实就是对一堆 js、css 以及 less 等文件的总称,自动引入的本质是将引入组件的写法通过插件来转换成手动引入组件对应的代码以及样式文件的写法。核心原理依然是对源码的 import 导入写法进行转换——词法语法分析,AST转换,代码生成。
该插件主要参数:
"libraryName": "", // 组件库名称,对应 import 语法中的包名 "libraryDirectory": "lib", // 编译之后各个组件单元所在文件夹名称 "style": true, // 是否引入组件对应样式文件,也可以传入 less 来引入 less 文件 "styleLibraryDirectory": "", // 编译之后引入的组件样式文件所在文件夹名称 "camel2DashComponentName": false, // 是否将驼峰命名的导入变量转换为对应的横线连接命名的文件名 "customName": (name, file) => { return `/lib/${name}` }, // 自定义编译之后引入的组件名 "customStyleName": (name, file) => { return `/lib/css/${name}` }, // 自定义编译之后引入样式文件的名称
插件中使用到的钩子函数有:
const methods = [ 'ImportDeclaration', // import 导入声明 'CallExpression', // 函数调用 'MemberExpression', 'Property', 'VariableDeclarator', 'ArrayExpression', 'LogicalExpression', 'ConditionalExpression', 'IfStatement', 'ExpressionStatement', 'ReturnStatement', 'ExportDefaultDeclaration', 'BinaryExpression', 'NewExpression', 'ClassDeclaration', 'SwitchStatement', 'SwitchCase', ];
Visitor 对象上还配置了 Program 钩子,该钩子是在 babel 处理一个独立文件(或者叫做模块更合适,node 规范定义一个文件就是一个模块)时执行,其中若不按此方式具体指定则默认为 enter 钩子。
const Program = { // 进入钩子 enter(path, options) { // 1. 根据插件接受到的配置参数初始化插件 Plugin 数组 // 2. 遍历插件 Plugin 数组,依次执行各个插件的初始化方法 ProgramEnter }, // 退出钩子 exit() { // ... } }
转换 import 语法需要识别 ES6 模块规范的默认导入、部分导入以及整体导入等语法,主要逻辑包括鉴别是否是部分导入,只有部分导入才表示导入具体组件,转换导入变量名等。
// 1. 部分导入 import { Button } from ''; console.log(Button); // 2. 默认导入 import default from ''; console.log(default.Button); // 3. 全部导入 import * as D from ''; console.log(D.Button);
整体处理逻辑如下:
- ImportDeclaration 钩子中将部分导入、默认导入和整体导入的语句记录到插件全局状态对象上,同时将节点的 path 对象记录至插件全局状态对象上;
- 插件全局状态对象上存储的 path 对象会在 Program 退出时遍历执行 remove 方法,从而移除了所有原始的导入语句;
- 在 MemberExpression、CallExpression、buildExpressionHandler、buildDeclaratorHandler等钩子函数中执行 importMethod 函数;
- importMethod 函数会根据插件的配置参数计算出真实文件导入路径、是否导入样式文件、样式文件名、是否转换默认导入等配置,从而使用 @babel/helper-module-imports 提供的 addSideEffect 方法添加对应的部分导入语句。
- 重命名导入模块的变量描述符 Identifier。
以钩子函数为入口,根据不同的节点类型取找到不同节点与变量相关的属性;
校验变量的 name 是否存在于插件全局状态的 specfied 中,即变量是否是导入组件指向的变量;
通过 path.scope.hasBinding、path.scope.getBinding 排除掉其他作用域的变量;
借助 importMethod 方法计算转换后模块对应的变量名然后修改节点对应的变量名。
变量描述符 Identifier可能指向的钩子有:
Property VariableDeclarator ArrayExpression LogicalExpression ConditionalExpression IfStatement ExpressionStatement ReturnStatement ExportDefaultDeclaration BinaryExpression NewExpression SwitchStatement SwitchCase ClassDeclaration
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。