[性能优化] 7个DEMO教你写Babel Import按需加载

栏目: JavaScript · 发布时间: 6年前

内容简介:这并不是一篇深入babel的文章,相反这是一篇适合初学babel的demos;本demos不会介绍一大堆babel各种牛逼特性(ps:因为这我也不会,有待深入研究),相反这里提供一大堆demos来解释如何从零开启babel plugin之路,然后开发一个乞丐乞丐版BabelPluginImport,并接入webpack中应用先来试想下babel的实现,大概分几个步骤:babel的插件开发可以参考

这并不是一篇深入babel的文章,相反这是一篇适合初学babel的demos;本demos不会介绍一大堆babel各种牛逼特性(ps:因为这我也不会,有待深入研究),相反这里提供一大堆demos来解释如何从零开启babel plugin之路,然后开发一个乞丐乞丐版BabelPluginImport,并接入webpack中应用

五分钟阅读,五分钟Demo Coding你能学会什么?

  • 编写你的第一个babel plugin
  • 使用babel plugin实现webpack resolve alias功能
  • 实现乞丐乞丐版BabelPluginImport
  • 把自己的插件接入webpack

STEP 1 | 冥想

先来试想下babel的实现,大概分几个步骤:

  1. js文件应该是作为字符串传递给babel
  2. babel对字符串进行解析,出AST
  3. AST应该大概是个json,这时候啥es6转es5啊都发生了,叫做转换
  4. 转换完的AST还得输出为String,这叫生成

STEP 2 | 小试牛刀

编写你的第一个babel plugin

babel的插件开发可以参考 Babel插件手册

先上一个最简单的demo

根据STEP 1的思路

// babel.js

var babel = require('babel-core');

const _code = `a`;

const visitor = {
    Identifier(path){
        console.log(path);
        console.log(path.node.name);
    }
};

babel.transform(_code, {
	plugins: [{
		visitor: visitor
	}]
});

复制代码

看完这个demo是不是有几个问题?

  • 问题1. plugins传入[{ visitor: {} }]格式
  • 问题2. 钩子函数为啥叫Identifier,而不叫Id?name?
  • 问题3. 其实类似问题2,钩子函数怎么定义,如何定义,什么规范?

问题解答

  • 问题1
    这个babel plugin定义要求如此,我们不纠结
  • 问题2
    所谓钩子函数当然是跟生命周期之类的有关了,这里的钩子函数其实是babel的在解析过程中的钩子函数,比如Identifier,当解析到标识符时就会进这个钩子函数
  • 问题3
    钩子函数的定义可以参考babel官网@babel/types[API],不过需要注意Api的首字母大写,不然会提示你没有此钩子函数

ok,对这个简单的demo没有问题之后来执行下这个demo:node babel.js,输出如下path AST:

// 因为光是一个"a",AST文件也长达284行,所以就不全部放出来了。只放出AST对象下的表示当前Identifier节点数据信息的node来看下

node: Node {
	type: 'Identifier',
	start: 0,
	end: 1,
	loc: SourceLocation {
		start: [Position],
		end: [Position],
		identifierName: 'a'
	},
	name: 'a'
},
复制代码

从这个AST node,对AST有个初步的认识,node节点会存储当前的loc信息,还有标识符的name,这一节小试牛刀的目的就达到了

STEP 3 | 实现resolve alias

前言

经过小试牛刀的阶段,然后自己熟悉下@babel/types的api,熟悉几个api之后就可以进行简单的开发了,这一节要讲的是ImportDeclaration

使用babel plugin实现webpack resolve alias功能

先思考下要实现resolve alias的步骤:

  1. 造数据_code="import homePage from '@/views/homePage';";
  2. 造数据const alias = {'@': './'};
  3. 把'@/views/homePage'变成'./views/homePage'输出

总结好我们要实现的功能,下面用demo来实现一遍

// babel.js

const babel = require('babel-core');
const _code = `import homePage from '@/views/homePage';`;
const alias = {
    '@': './'
};

const visitor = {
    ImportDeclaration(path){
        for(let prop in alias){
            if(alias.hasOwnProperty(prop)){
                let reg = new RegExp(`${prop}/`);
                path.node.source.value = path.node.source.value.replace(reg, alias[prop]);
            }
        }
    }
};

const result = babel.transform(_code, {
	plugins: [{
		visitor: visitor
	}]
});

console.log(result.code);
复制代码

这个demo的主要作用是当进入到ImportDeclaration钩子函数时把path.node.source.value里面的@替换成了./,来node babel.js看下效果:

[性能优化] 7个DEMO教你写Babel Import按需加载

发现log输出了import homePage from "./views/homePage";

说明我们的alias生效了

STEP 4 | 乞丐乞丐版BabelPluginImport is coming

问题:

还是一样的步骤,先试想下实现一个BabelPluginImport的难点在哪?
复制代码

我在 React性能优化之代码分割 中介绍过BalbelPluginImport,其实这个插件的一个功能是把 import { Button } from 'antd' 转换为 import { Button } from 'antd/lib/button';

-> 我们这个乞丐版BabelPluginImport就简单实现下这个功能

// babel.js

var babel = require('@babel/core');
var types = require('babel-types');
// Babel helper functions for inserting module loads
var healperImport = require("@babel/helper-module-imports");

const _code = `import { Button } from 'antd';`;

const ImportPlugin = {
    // 库名
    libraryName: 'antd',
    // 库所在文件夹
    libraryDirectory: 'lib',
    // 这个队列其实是为了存储待helperModuleImports addNamed的组件的队列,不过remove和import都在ImportDeclaration完成,所以这个队列在这个demo无意义
    toImportQueue: {},
    // 使用helperModuleImports addNamed导入正确路径的组件
    import: function(file){
        for(let prop in this.toImportQueue){
            if(this.toImportQueue.hasOwnProperty(prop)){
                return healperImport.addNamed(file.path, prop, `./main/${this.libraryDirectory}/index.js`);
            }
        }
    }
};

const visitor = {
    ImportDeclaration(path, state) {
        const { node, hub: { file } } = path;
        if (!node) return;
        const { value } = node.source;
        // 判断当前解析到的import source是否是antd,是的话进行替换
        if (value === ImportPlugin.libraryName) {
            node.specifiers.forEach(spec => {
                if (types.isImportSpecifier(spec)) {
                    ImportPlugin.toImportQueue[spec.local.name] = spec.imported.name;
                }
            });
            // path.remove是移除import { Button } from 'antd';
            path.remove();
            // import是往代码中加入import _index from './main/lib/index.js';
            ImportPlugin.import(file);
        }
    }
};

const result = babel.transform(_code, {
	plugins: [
        {
		    visitor: visitor
        },
        // 这里除了自定义的visitor,还加入了第三方的transform-es2015-modules-commonjs来把import转化为require
        "transform-es2015-modules-commonjs"
    ]
});

console.log(result.code);
复制代码

输出结果:

[性能优化] 7个DEMO教你写Babel Import按需加载

可以发现:

import { Button } from 'antd';

->

"use strict"; var _index = require("./main/lib/index.js");

原代码被转换成了下面的代码

STEP 5 | Demo Coding高光时刻

高光时刻来了,说了这么久理论知识,可以来上手自己写一个了。

5.1 create-react-app先来搭起一个项目

npx create-react-app babel-demo
复制代码

5.2 简单的开发下项目,一个入口组件App.js,一个Button组件

目录结构是:
    src
        - App.js
        - firefly-ui文件夹
            - lib文件夹
                - Button.js
代码很简单,如下:

// App.js
import React from 'react';
import Button from 'firefly-ui';
function App() {
	return (
		<div className="App">
			<Button />
		</div>
	);
}
export default App;

// Button.js
import React, { Component } from 'react';
class Button extends Component{
    render(){
        return <div>我是button啊</div>
    }
}
export default Button;
复制代码

ok,代码写完了,一运行,崩了

这没问题,没崩就奇怪了,因为你没装firefly-ui啊,可是firefly-ui是个啥?

有这个疑问说明你跟上节奏了,我可以告诉你,firefly-ui就是你src目录的firefly-ui目录,那么下面我们就要写一个babel plugin来解决这个问题,大致思路如下:

  • 当解析到import { Button } from 'firefly-ui'时对这个import进行转换
  • 当解析到jsx中Button时用上面转换后的import

那下面从这两个入手写babel import

5.3 npm run eject来eject出webpack配置

好的,为啥要eject出配置,因为你要配置babel-loader的plugins啊大佬。   
ok,来配置一把

// 找到webpack.config.js -> 找到babel-loader -> 找到plugins

// 注意点:
// 在plugins里面加入咱们的import插件
// tips:import插件放在src的兄弟文件夹babel-plugins的import.js
// 所以这里的路径是../babel-plugins/import,因为默认是从node_modules开始

//还有个timestamp,这是因为webpackDevServer的缓存,为了重启清缓存加了时间戳

[
	require.resolve('../babel-plugins/import'),
	{
		libName: 'firefly-ui',
		libDir: 'lib',
		timestamp: +new Date
	},
]
以上是balbel-loader的plugins配置,请看下注意点,其他的没什么难点
复制代码

5.4 import plugin开发

所有配置都完成了,那么还差实现../babel-plugins/import.js

const healperImport = require("@babel/helper-module-imports");

let ImportPlugin = {
    // 从webpack配置进Program钩子函数读取libName和libDir
    libName: '',
    libDir: '',
    // helper-module-imports待引入的组件都放在这里
    toImportQueue: [],
    // helper-module-imports引入过的组件都放在这里
    importedQueue: {},
    // helper-module-imports替换原始import
    import: function(path, file){
        for(let prop in this.toImportQueue){
            if(this.toImportQueue.hasOwnProperty(prop)){
                // return healperImport.addNamed(file.path, prop, `./${this.libName}/${this.libDir}/${prop}.js`);
                let imported = healperImport.addDefault(file.path, `./${this.libName}/${this.libDir}/${prop}.js`);
                this.importedQueue[prop] = imported;
                return imported;
            }
        }
    }
};

module.exports = function ({ types }) {
    return {
        visitor: {
            // Program钩子函数主要接收webpack的配置
            Program: {
                enter(path, { opts = {} }) {
                    ImportPlugin.libName = opts.libName;
                    ImportPlugin.libDir = opts.libDir;
                }
            },
            // ImportDeclaration钩子函数主要处理import之类的源码
            ImportDeclaration: {
                enter(path, state){
                    const { node, hub: { file } } = path;
                    if (!node) return;
                    const { value } = node.source;
            
                    if (value === ImportPlugin.libName) {
                        node.specifiers.forEach(spec => {
                            ImportPlugin.toImportQueue[spec.local.name] = spec.local.name;
                        });
                        path.remove();
                        ImportPlugin.import(path, file);
                    }
                }
            },
            // Identifier主要是为了解析jsx里面的Button,并转换为helper-module-imports引入的新节点
            Identifier(path){
                if(ImportPlugin.importedQueue[path.node.name]){
                    path.replaceWith(ImportPlugin.importedQueue[path.node.name]);
                }
            }
        }
    }
}
复制代码

这个plugin的实现,我探索了几个小时才实现的。 如果只是实现ImportDeclaration钩子函数,而不实现Identifier钩子函数的话,可以发现import的Button已被转换,而jsx里面还是Button。所以会提示Button is not defined。如下图:

[性能优化] 7个DEMO教你写Babel Import按需加载

好的,按照我的demo完整实现之后,发现import和jsx里全部被转换了。并且程序正常运行。如下图:

[性能优化] 7个DEMO教你写Babel Import按需加载

到这里差不多就结束了,认真的同学可能还会发现有很多问题没有给出解答,后面有时间再继续写babel,因为感觉这篇文章的知识点对于初学者来说已经挺多了,如果环境搭建有问题,或者自己无法写出plugin示例的效果,可以看我的 babel-demo源码 ,有问题可以咨询我


以上所述就是小编给大家介绍的《[性能优化] 7个DEMO教你写Babel Import按需加载》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Intersectional Internet

The Intersectional Internet

Safiya Umoja Noble、Brendesha M. Tynes / Peter Lang Publishing / 2016

From race, sex, class, and culture, the multidisciplinary field of Internet studies needs theoretical and methodological approaches that allow us to question the organization of social relations that ......一起来看看 《The Intersectional Internet》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具