博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[性能优化] 7个DEMO教你写Babel Import按需加载
阅读量:5756 次
发布时间:2019-06-18

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

前言

这并不是一篇深入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的插件开发可以参考

先上一个最简单的demo

根据STEP 1的思路

// babel.jsvar 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官网 ,不过需要注意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.jsconst 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看下效果:

发现log输出了import homePage from "./views/homePage";
说明我们的alias生效了

STEP 4 | 乞丐乞丐版BabelPluginImport is coming

问题:

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

我在 中介绍过BalbelPluginImport,其实这个插件的一个功能是把 import { Button } from 'antd' 转换为 import { Button } from 'antd/lib/button';

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

// babel.jsvar babel = require('@babel/core');var types = require('babel-types');// Babel helper functions for inserting module loadsvar 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);复制代码

输出结果:

可以发现:
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.jsimport React from 'react';import Button from 'firefly-ui';function App() {	return (		
);}export default App;// Button.jsimport React, { Component } from 'react';class Button extends Component{ render(){ return
我是button啊
}}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。如下图:

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

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

转载于:https://juejin.im/post/5cec92bef265da1b7b3169ff

你可能感兴趣的文章
html中table的画法及table和div的区别
查看>>
配置dcom时,在此计算机运行应用程序不可选
查看>>
多个精美的导航样式web2.0源码
查看>>
Dom4j操作xml
查看>>
我是如何学习NodeJs
查看>>
JDK1.6新特性,网络增强(Networking features and enhancements)
查看>>
silverlight 双击事件
查看>>
[转]anchorPoint 锚点解析
查看>>
从程序员到项目经理(十九):想改变任何人都是徒劳的
查看>>
[7] 金字塔(Pyramid)图形的生成算法
查看>>
常用代码之五:RequireJS, 一个Define需要且只能有一个返回值/对象,一个JS文件里只能放一个Define....
查看>>
Linux常用命令大全
查看>>
[WPF疑难]Hide me! not close
查看>>
percona-toolkit 之 【pt-heartbeat】说明
查看>>
struts2:遍历自定义字符串数组,遍历Action实例所引用对象中的数组
查看>>
Python List 中 Append 和 Extent 方法不返回值。
查看>>
linux ssh 无密码登陆
查看>>
jquery中美元符号命名冲突问题解决
查看>>
WebSocket 学习笔记--IE,IOS,Android等设备的兼容性问题与代码实现
查看>>
grep时排除指定的文件和目录
查看>>