Написать плагин для Babel с нуля

внешний интерфейс Babel

Создание рабочей среды веб-пакета

Плагин babel, который мы написали, относится к babel-loader, а babel-loader в основном работает со средой webpack, поэтому, чтобы определить, работает ли плагин babel, мы должны создать среду webpack.

Структура каталогов

|-- babel-plugin-wyimport
    |-- .editorconfig
    |-- .gitignore
    |-- package.json
    |-- README.md
    |-- build
    |   |-- app.be45e566.js
    |   |-- index.html
    |-- config
    |   |-- paths.js
    |   |-- webpack.dev.config.js
    |   |-- webpack.prod.config.js
    |-- scripts
    |   |-- build.js
    |   |-- start.js
    |-- src
        |-- index.js

webpack.prod.config.js

Файл конфигурации не сжимает и не обфусцирует код, в основном для удобства сравнения содержимого файлов до и после компиляции

'use strict'

process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

const path = require('path');
const paths = require("./paths");
const fs = require('fs');
const webpack = require("webpack");
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');

module.exports = {
    output: {
        path: paths.build,
        filename: '[name].[chunkhash:8].js',
        chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
        publicPath: "/"
    },
    entry: {
        "app":path.resolve(paths.src, "index.js")
    },
    resolve:{
        extensions:[".js", ".json"],
        modules: ["node_modules", paths.src]
    },
    module: {
        rules: [
            {
                test:/\.css$/,
                include:paths.src,
                loader: ExtractTextPlugin.extract({
                    use: [
                        {
                            options:{
                                root: path.resolve(paths.src, "images")
                            },
                            loader: require.resolve('css-loader')
                        }
                    ]
                })
            },
            {
                test:/\.less$/,
                include:paths.src,
                use:[
                    require.resolve('style-loader'),
                    {
                        loader:require.resolve('css-loader'),
                        options:{
                            root: path.resolve(paths.src, "images")
                        }
                    },
                    {
                        loader: require.resolve('less-loader')
                    }
                ]
            },
            {
                test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
                loader: require.resolve('url-loader'),
                options: {
                  limit: 1000,
                  name: 'static/images/[name].[hash:8].[ext]',
                },
            },
            {
                test:/\.(js|jsx)$/,
                include:paths.src,
                loader: require.resolve("babel-loader"),
                options:{
                    presets:["react-app"],
                    plugins:[
                        //["wyimport", {libraryName:"lodash"}]
                    ],
                    compact: true
                    //cacheDirectory: true
                }
            },
            {
                exclude: [
                  /\.html$/,
                  /\.(js|jsx)$/,
                  /\.css$/,
                  /\.less$/,
                  /\.json$/,
                  /\.bmp$/,
                  /\.gif$/,
                  /\.jpe?g$/,
                  /\.png$/,
                  /\.svg$/
                ],
                loader: require.resolve('file-loader'),
                options: {
                  name: 'static/[name].[hash:8].[ext]',
                },
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin('[name].[chunkhash:8].css'),
        new WebPlugin({
            //输出的html文件名称
            filename: 'index.html',
            //这个html依赖的`entry`
            requires:["app"]
        }),
    ]
}

build.js

Запустите файл, в основном рассчитайте размер содержимого файла до и после компиляции.

const webpack = require('webpack');
const path = require('path');
const config = require('../config/webpack.prod.config');
const chalk = require('chalk');
const paths = require('../config/paths');
const fs = require("fs");

// 获取目录大小
const getDirSize = (rootPath, unit ="k") => {
	if (!fs.existsSync(rootPath)) {
		return 0;
	}
	let buildSize = 0;
	const dirSize = (dirPath) => {
		let files = fs.readdirSync(dirPath, "utf-8")
		files.forEach((files) => {
			let filePath = path.resolve(dirPath, files);
			let stat = fs.statSync(filePath) || [];
			if (stat.isDirectory()){
				dirSize(filePath)
			} else {
				buildSize += stat.size
			}
		})
	}
	dirSize(rootPath)
	let map = new Map([["k",(buildSize/1024).toFixed(2)+"k"], ["M",buildSize/1024/1024+"M"]])
	return map.get(unit);
}
// 清空目录文件
const rmDir = (path, isDeleteDir) => {
	if(fs.existsSync(path)) {
        files = fs.readdirSync(path);
        files.forEach(function(file, index) {
            var curPath = path + "/" + file;
            if(fs.statSync(curPath).isDirectory()) { // recurse
                rmDir(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
}

const measureFileBeforeBuild = () => {
	console.log(`打包之前build文件夹的大小: ${chalk.green(getDirSize(paths.build))}\n`)
	rmDir(paths.build)  //删除build文件夹
	return build().then((stats) => {
		console.log(chalk.green(`打包完成\n`))
		console.log(`打包之后文件夹大小:${chalk.green(getDirSize(paths.build))}\t花费时间: ${chalk.green((stats.endTime-stats.startTime)/1000)}s`)
	}, err => {
		console.log(chalk.red('Failed to compile.\n'));
      	console.log((err.message || err) + '\n');
      	process.exit(1);
	})
}

const build = () => {
	const compiler = webpack(config)
	return new Promise((resolve, reject) => {
		compiler.run((err, stats) => {
			console.log(chalk.green("开始打包..."))
			if (err) {
				return reject(err);
			}
			const message = stats.toJson({}, true)
			if (message.errors.length) {
				return reject(message.errors);
			}
			return resolve(stats)
		})
	})
}
measureFileBeforeBuild()

Мелкий измельчитель

мы вsrc/index.jsввод в файл

 import { uniq } from "lodash"

потомnpm run build

размер531k, очевидно, что lodash был введен во всех, поэтому он введен такlodashВнимание одноклассникам библиотеки! Обычно мы должны написать это для загрузки по требованию.

 //import { uniq } from "lodash"
 import uniq from "lodash/uniq"

потомnpm run build

Если файл импортируетlodashмногие методы, такие как

import uniq   from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
...

Этот способ написания довольно раздут, так что вы можете написать это так?import {uniq, extend, flatten, cloneDeep } from "lodash"А также реализовать загрузку по требованию?Это очень просто, просто скомпилируй и выведи как

import uniq   from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";

просто хорошо

Подготовка знаний

записыватьpluginПрежде всего, нам нужно понять следующие два момента

  1. pluginКогда это работает?
  2. pluginкак это работает

принцип компиляции webpack

babel-loaderтак какwebpackодин изloaderСначала разберемся с процессом компиляции webpack иloaderсуществуетwebpackроль вздесьЕсть статья, которая говорит, что это очень хорошо, все сначала прочитали, а потом дочитали.

Основные понятия вавилона

Зная, что есть статья, которая проясняет ситуацию, студенты, которые не очень хорошо разбираются в Babel, сначалавходитьЧитайте дальше, когда поймете!

Здесь я прежде всего хочу подчеркнутьbabelконфигурация параметров, если я пишуfiveoneизbabelПлагин, я его так настраиваю в параметрах

    {
        presets:["react-app", "es2015"],
        plugins:[
            ["fiveone", {libraryName:"lodash"}],
            ["transform-runtime", {}]
        ],
    }
    起作用的顺序为fiveone->transform-runtime->es2015->react-app

Порядок компиляции первыйpluginsтогда слева направоpresetsсправа налево

Принцип компиляции Babel

Два вышеприведенных раздела объясняютpluginКогда это работает, объясните нижеpluginКак это работает?

  1. babylonИнтерпретатор преобразует строку кода вASTдерево, напримерimport {uniq, extend, flatten, cloneDeep } from "lodash"превратиться вASTДерево
  2. babel-traverseправильноASTПроизводится разбор дерева и обход всего дерева.
  3. плагин преобразует новыйASTДерево.
  4. вывести новую строку кодаЛитературный адрес

Плагин, который мы хотим написать, находится на третьем шаге, конвертируйте новый через путьASTДеревья? Давайте начнем, как перейти к третьему шагу!

запустить Babel-плагин

Сначала нам нужно установить два инструментаbabel-coreа такжеbabel-types;

npm install --save babel-core babel-types;

  1. babel-coreпоставкаtransformметод преобразует строку кода вASTДерево
  2. babel-typesОбеспечить различные операцииASTБиблиотека инструментов Node

мы вsrc/index.jsввод в

var babel = require('babel-core');
var t = require('babel-types');
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
const visitor = {
    Identifier(path){
        console.log(path.node.name)
    }
}
const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
})

бегатьnode src index.js

visitor

вавилонская параASTДерево пройдено, и процесс обхода предоставит метод, называемый объектом посетителя, для доступа к определенному этапу, такому как выше

    Identifier(path){
        console.log(path.node.name)
    }

посетить узел идентификатора,ASTДерево расширяется следующим образом

Почему выводит дваuniq, так как этот метод вызывается для каждого входа и выхода узла. Будет два обхода, один вход как нижний обход, а другой выход как верхний обход. мы будемsrc/index.jsсерединаIdentifierметод изменен на

Identifier:{
    enter(path) {
        console.log("我是进入的:",path.node.name)
    },
    exit(path) {
        console.log("我是进入的:",path.node.name)
    }
}

бегатьnode src index.js

遍历流程: 向下遍历-进入uniq->退出uniq->向上遍历-进入uniq->退出uniq

path

путь представляет собой соединение между двумя узлами.Через этот объект мы можем получить доступ к текущему узлу, дочернему узлу, родительскому узлу, а также добавить, удалить, изменить, заменить и другие операции над узлами. Следующая демонстрация будетuniqзаменять_uniqкод показывает, как показано ниже:

var babel = require('babel-core');
var t = require('babel-types');
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;

const visitor = {
    Identifier(path){
        if (path.node.name == "uniq") {
            var newIdentifier = t.identifier('_uniq')  //创建一个名叫_uniq的新identifier节点
            path.replaceWith(newIdentifier)            //把当前节点替换成新节点
        }
    }
}

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
})
console.log(result.code) //import { _uniq, extend, flatten, cloneDeep } from "lodash";

Начинать

С учетом приведенных выше концепций мы теперь помещаем строку кодаimport {uniq, extend, flatten, cloneDeep } from "lodash"Конвертировано в

import uniq   from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";

код показывает, как показано ниже

var babel = require('babel-core');
var t = require('babel-types');
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;

const visitor = {
    ImportDeclaration(path, _ref = {opts:{}}){
        const specifiers = path.node.specifiers;
        const source = path.node.source;
        if (!t.isImportDefaultSpecifier(specifiers[0]) ) {
            var declarations = specifiers.map((specifier, i) => {         //遍历  uniq extend flatten cloneDeep
                return t.ImportDeclaration(                               //创建importImportDeclaration节点
                    [t.importDefaultSpecifier(specifier.local)],
                    t.StringLiteral(`${source.value}/${specifier.local.name}`)
                )
            })
            path.replaceWithMultiple(declarations)

        }
    }
}

const result = babel.transform(code, {
    plugins: [{
        visitor: visitor
    }]
})
console.log(result.code)

потомnode src/index.js

KO, Некоторые люди спросят, Сяобянь, откуда ты знаешь, как это писать? очень просто вASTначальство
将1变换成2就可以了

Настроить на node_modules

После того, как код написан, его нужно настроить, если он работает.Мы назвали этот плагин Fiveone, поэтому создайте новую папку с именем babel-plugin-fiveone в node_modules

существуетbabel-plugin-fiveone/index.jsввод в

var babel = require('babel-core');
var t = require('babel-types');
const visitor = {
    // 对import转码
    ImportDeclaration(path, _ref = {opts:{}}){
        const specifiers = path.node.specifiers;
        const source = path.node.source;
        if (!t.isImportDefaultSpecifier(specifiers[0]) ) {
            var declarations = specifiers.map((specifier) => {      //遍历  uniq extend flatten cloneDeep
                return t.ImportDeclaration(                         //创建importImportDeclaration节点
                    [t.importDefaultSpecifier(specifier.local)],
                    t.StringLiteral(`${source.value}/${specifier.local.name}`)
                )
            })
            path.replaceWithMultiple(declarations)

        }
    }

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

затем изменитьwebpack.prod.config.jsсерединаbabel-loaderэлемент конфигурации

options:{
    presets:["react-app"],
    plugins:[
        ["fiveone", {}]
    ],
}

потомsrc/index.jsввод в

import {uniq, extend, flatten, cloneDeep } from "lodash"

npm run build

ясно достигнутонагрузка по требованию

Однако ввести такое перекодирование для всех библиотек невозможно, поэтому вbabel-loaderПлагин добавляет библиотеку

options:{
    presets:["react-app"],
    plugins:[
        ["fiveone", {libraryName:"lodash"}]
    ],
}

существуетbabel-plugin-fiveone/index.jsИзменить


var babel = require('babel-core');
var t = require('babel-types');
const visitor = {
    // 对import转码
    ImportDeclaration(path, _ref = {opts:{}}){
        const specifiers = path.node.specifiers;
        const source = path.node.source;
        // 只有libraryName满足才会转码
        if (_ref.opts.libraryName == source.value && (!t.isImportDefaultSpecifier(specifiers[0])) ) { //_ref.opts是传进来的参数
            var declarations = specifiers.map((specifier) => {      //遍历  uniq extend flatten cloneDeep
                return t.ImportDeclaration(                         //创建importImportDeclaration节点
                    [t.importDefaultSpecifier(specifier.local)],
                    t.StringLiteral(`${source.value}/${specifier.local.name}`)
                )
            })
            path.replaceWithMultiple(declarations)

        }
    }

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

конец

Пожалуйста, поправьте меня, если что-то не так в статье, большое спасибо! адрес гитхаба:GitHub.com/Aman's Su/Put…Если вы что-то получаете, вы можете дать этоstarВеликолепно!

Ссылка на ссылку