寫一個babel外掛實現按需打包的功能

Macchiato發表於2018-03-28

你要是沒有抽象語法樹基礎的話,建議先看一下上一篇文章抽象語法樹 Abstract syntax tree這樣更有助於對下文的理解!

背景:當我們同時引入一個包中的兩個方法,有兩種形式

第一種形式

import {flatten,join} from 'lodash';
複製程式碼

第二種形式

import flatten from 'lodash/flatten';
import join from 'lodash/join';
複製程式碼

對比兩種形式,我們可以看出:

第一種方式的引入會把整個lodash包引進來 第二種方式是指引入整個包中的兩個方法

顯然我們要用第二種

但是一般的專案中 大部分都是以import解構的形式,所以在這裡我們就寫一個外掛,當我們寫成第一種形式引入的時候,利用外掛轉化成第二種形式

這是我們要寫的外掛的功能

第一步:初始化一個webpack的專案

 npm i webpack webpack-cli babel-core babel-loader babel-preset-env babel-preset-stage-0 -D
複製程式碼

在Webpack中,提供了mode變數,用於配置執行環境,mode的值可以為development,表示的是開發模式,或者是production,表示的是生產模式。

在package.json中寫入編譯的命令

"scripts":{
  "build":"webpack --mode production/development"
}
複製程式碼

第二步:建立一個專案結構

webpack-plugin // 專案名稱
    dist // 打包輸出目錄
        bundle.js // 打包輸出的檔案
    src // 主要邏輯
        index.js // 專案的入口檔案
    ./babelrc // 語法解析配置
    package.json
    webpack.config.js

複製程式碼

第三步:寫webpack的配置檔案

const path = require('path');
module.exports = {
    entry: './src/index.js',// 入口檔案
    output: {
        path: path.join(__dirname, 'dist'), // 輸出路徑
        filename: 'bundle.js' // 輸出的檔名稱
    },
    // 配置
    module: {
        // 配置載入器
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader'
            }
        ]
    }
}

複製程式碼

第四步:配置.babelrc

{
  "presets": [
    "env",
    "stage-0"
  ],
  "plugins": [
    [
      // "demand-loading", // 注:這個是我們自己寫的外掛名 顯示先不放,等我們寫好外掛後再加上
      {
         "library": "lodash", // 我們在引用哪個庫的時候使用我們寫的這個外掛,這裡的意思是當我們引用lodash庫的時候使用我們寫的這個外掛
      },
      "syntax-decorators"
    ]
  ]
}
複製程式碼

第五步:index.js中寫入對比指令碼

 // import {flatten,join} from 'lodash'
import flatten from 'lodash/flatten'
import join from 'lodash/join'
複製程式碼

先引入後面的這兩句,然後打包一下

Hash: fcb0bd5d9734b5f56676
Version: webpack 4.2.0
Time: 346ms
Built at: 2018-3-27 21:24:33
    Asset      Size  Chunks             Chunk Names
bundle.js  21.3 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 823 bytes {main} [built]
[./src/index.js] 286 bytes {main} [built]
    + 15 hidden modules


複製程式碼

看到這樣打包後的程式碼,我們發現這種方式引入 打包後的大小是。21.3k

然後註釋掉後兩行,只引入第一行

    Hash: aa8b689e1072463fc1cd
Version: webpack 4.2.0
Time: 3277ms
Built at: 2018-3-27 21:30:22
    Asset     Size  Chunks             Chunk Names
bundle.js  483 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./node_modules/webpack/buildin/amd-options.js] (webpack)/buildin/amd-options.js 82 bytes {main} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 823 bytes {main} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 521 bytes {main} [built]
[./src/index.js] 47 bytes {main} [built]
    + 1 hidden module

複製程式碼

這次打包後的大小是 483k

通過對比 證實了我們所說的兩種引入方式的區別

第六步:那我們就來寫寫這個外掛吧!

先對比一下兩種引入方式的抽象語法樹的差別

寫一個babel外掛實現按需打包的功能

通過對比我們發現,只是ImportDeclaration不相同


	const babel = require('babel-core');
	const types = require('babel-types');
	
	let visitor = {
	    // 這裡的ref是ImportDeclaration的第二個引數,這裡的值是.babelrc中的 {
	       // "library": "lodash"
	    //}, 這裡是指定 我們在引用哪個庫的時候使用這個外掛
	    ImportDeclaration(path, ref={options:{}}) {
	        let node = path.node;
	        let specifiers = node.secifiers
	        if (options.library == node.soure.value && !types.isImportDeclaration(specifiers[0])) {
	            let newImport = specifiers.map((specifier) => (
	                types.importDeclaration([types.ImportDefaultSpecifier(specifier.local)], types.stringLiteral(`${node.soure.value}/${specifier.local.name}`))
	            ));
	            path.replaceWithMultiple(newImport)
	        }
	    }     
	}
	
	const code = "import {flatten, join} from 'lodash';";
	
	let r = babel.transform(code, {
	    plugins: [
	        {visitor}
	    ]
	})

複製程式碼

在建立替換邏輯的時候,types上的方法 用github上的這個網址,哪個不會搜哪個,媽媽再也不用擔心我的學習。嘻嘻

第七步:將上面的程式碼整理一下放到node_modules檔案中

新建一個資料夾 babel-plugin-demand-loading 放到node_modules中,再新建一個index.js檔案,將下面的程式碼放進去,再然後進入這個資料夾 npm init -y初始化一個package.json檔案,裡面的入口檔案寫成index.js

需要注意的事項:

第一: babel外掛的資料夾命名,必須以 babel-plugin-xxx(你寫的外掛名)命名,否則引入不成功 第二: babel外掛返回的是一個物件,裡面有一個訪問者模式的visitor物件,裡面是我們的轉化程式碼


	const babel = require('babel-core');
	const types = require('babel-types');
	
	module.exports = {
	    visitor:  {
	        // 這裡的ref是ImportDeclaration的第二個引數,這裡的值是.babelrc中的 {
	        // "library": "lodash"
	        //}, 這裡是指定 我們在引用哪個庫的時候使用這個外掛
	        ImportDeclaration(path, ref={}) {
	            let { opts } = ref
	            let node = path.node;
	            let specifiers = node.specifiers
	            if (opts.library == node.source.value && !types.isImportDeclaration(specifiers[0])) {
	                let newImport = specifiers.map((specifier) => (
	                    types.importDeclaration([types.ImportDefaultSpecifier(specifier.local)], types.stringLiteral(`${node.source.value}/${specifier.local.name}`))
	                ));
	                path.replaceWithMultiple(newImport)
	            }
	        }
	    }
	}


複製程式碼

最後 npm run build 編譯後,發現打包後的大小是。20多k說明我們的外掛起作用了。

到這裡你是不是也會了呢?嘻嘻!

不要厭煩熟悉的事物,每天都進步一點;不要畏懼陌生的事物,每天都學習一點;

相關文章