從零開始編寫一個babel插件

構建webpack生產環境

咱們編寫的babel插件是所屬於babel-loader,而babel-loader基本運行與webpack環境.因此爲了檢測babel插件的是否起做用,咱們必須構建webpack環境.css

目錄結構

|-- 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

配置文件,沒有對代碼進行壓縮和混淆,主要爲了方便對比編譯先後的文件內容html

'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

啓動文件,主要計算編譯先後的文件內容大小node

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文件裏面輸入react

import { uniq } from "lodash"
複製代碼

而後 npm run build webpack

大小是 531k,很明顯lodash被所有引入了進來了,因此這樣引入 lodash庫的同窗注意咯! 正常咱們應該這樣寫來按需載入

//import { uniq } from "lodash"
 import uniq from "lodash/uniq"
複製代碼

而後 npm run build git

若是一個文件引入lodash不少方法如github

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"而且也實現按需載入呢? 很簡單,只要將它編譯輸出成web

import uniq   from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
複製代碼

就能夠了npm

知識準備

編寫plugin以前首先咱們要清楚如下二點json

  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不勝感激!

參考連接

相關文章
相關標籤/搜索