[性能優化] 7個DEMO教你寫Babel Import按需加載

前言

這並非一篇深刻babel的文章,相反這是一篇適合初學babel的demos;本demos不會介紹一大堆babel各類牛逼特性(ps:由於這我也不會,有待深刻研究),相反這裏提供一大堆demos來解釋如何從零開啓babel plugin之路,而後開發一個乞丐乞丐版BabelPluginImport,並接入webpack中應用node

五分鐘閱讀,五分鐘Demo Coding你能學會什麼?

  • 編寫你的第一個babel plugin
  • 使用babel plugin實現webpack resolve alias功能
  • 實現乞丐乞丐版BabelPluginImport
  • 把本身的插件接入webpack

STEP 1 | 冥想

先來試想下babel的實現,大概分幾個步驟:react

  1. js文件應該是做爲字符串傳遞給babel
  2. babel對字符串進行解析,出AST
  3. AST應該大概是個json,這時候啥es6轉es5啊都發生了,叫作轉換
  4. 轉換完的AST還得輸出爲String,這叫生成

STEP 2 | 小試牛刀

編寫你的第一個babel plugin

babel的插件開發能夠參考 Babel插件手冊webpack

先上一個最簡單的demo

根據STEP 1的思路git

// 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,鉤子函數怎麼定義,如何定義,什麼規範?

問題解答es6

  • 問題1
    這個babel plugin定義要求如此,咱們不糾結
  • 問題2
    所謂鉤子函數固然是跟生命週期之類的有關了,這裏的鉤子函數實際上是babel的在解析過程當中的鉤子函數,好比Identifier,當解析到標識符時就會進這個鉤子函數
  • 問題3
    鉤子函數的定義能夠參考babel官網 @babel/types[API],不過須要注意Api的首字母大寫,否則會提示你沒有此鉤子函數

ok,對這個簡單的demo沒有問題以後來執行下這個demo:node babel.js,輸出以下path AST:github

// 由於光是一個"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,這一節小試牛刀的目的就達到了web

STEP 3 | 實現resolve alias

前言

通過小試牛刀的階段,而後本身熟悉下@babel/types的api,熟悉幾個api以後就能夠進行簡單的開發了,這一節要講的是ImportDeclarationnpm

使用babel plugin實現webpack resolve alias功能

先思考下要實現resolve alias的步驟:json

  1. 造數據_code="import homePage from '@/views/homePage';";
  2. 造數據const alias = {'@': './'};
  3. 把'@/views/homePage'變成'./views/homePage'輸出

總結好咱們要實現的功能,下面用demo來實現一遍api

// 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看下效果:

發現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);
複製代碼

輸出結果:

能夠發現:
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。以下圖:

好的,按照個人demo完整實現以後,發現import和jsx裏所有被轉換了。而且程序正常運行。以下圖:

到這裏差很少就結束了,認真的同窗可能還會發現有不少問題沒有給出解答,後面有時間再繼續寫babel,由於感受這篇文章的知識點對於初學者來講已經挺多了,若是環境搭建有問題,或者本身沒法寫出plugin示例的效果,能夠看個人 babel-demo源碼,有問題能夠諮詢我

相關文章
相關標籤/搜索