阅读 4348

从零开始编写一个babel插件

构建webpack生产环境

我们编写的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的编译过程和loaderwebpack中作用 这里有一篇文章说很好,大家先去阅读理解之后再往下看

babel的基本概念

知乎有一篇文章讲得比较清楚,对babel不是很清楚的同学先进去了解之后再往下看!

在这里,我主要想强调一下babel参数的配置,如果我写了一个名叫fiveonebabel插件,我在参数中这么配置

    {
        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-traverseAST树进行解析遍历出整个树的path.
  3. plugin转换出新的AST树.
  4. 输出新的代码字符串 文献地址

我们要编写的plugin在第三步.通过path来转换出新的AST树?下面我们就开始如何进行第三步!

开始babel-plugin

首先我们需要安装二个工具babel-corebabel-types;

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

  1. babel-core提供transform方法将代码字符串转换为AST
  2. babel-types提供各种操作AST节点的工具库

我们在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

babel对AST树进行遍历,遍历的过程会提供一个叫visitor对象的方法对某个阶段访问, 例如上面的

    Identifier(path){
        console.log(path.node.name)
    }
复制代码

就是访问了Identifier节点,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

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所以在node_modules里面新建一个名叫babel-plugin-fiveone的文件夹

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.jsbabel-loader的配置项

options:{
    presets:["react-app"],
    plugins:[
        ["fiveone", {}]
    ],
}
复制代码

然后src/index.js中输入

import {uniq, extend, flatten, cloneDeep } from "lodash"
复制代码

npm run build

很明显实现了按需加载

然而不能对所有的库都进入这么转码所以在babel-loader的plugin增加lib

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地址:github.com/Amandesu/ba… 如果大家有所收获,可以随手给个star不胜感激!

参考链接

关注下面的标签,发现更多相似文章
评论