webpack由浅入深——(ast、loader和plugin)

6,562 阅读3分钟

webpack系列文章

  1. webpack由浅入深——(webpack基础配置)
  2. webpack由浅入深——(webpack优化配置)
  3. webpack由浅入深——(tapable)
  4. webpack由浅入深——(webapck简易版)
  5. webpack由浅入深——(ast、loader和plugin)

ast

AST定义了代码的结构,通过操纵这颗语法树,可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作,主要有以下用途:

  1. 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等,例如:JSLint、JSHint等
  2. 代码混淆压缩,例如:UglifyJS2等
  3. 优化变更代码,改变代码结构使达到想要的结构,例如:CoffeeScript、TypeScript、JSX等转化为原生Javascript

JavaScript Parser

JavaScriptParser是把js源码转化为抽象语法树的解析器。常用的JavaScript Parser:

  • esprima
  • traceur
  • acorn
  • shift

其中webpack就是使用的acorn将源代码解析成AST进行操作。

loader

什么是loader

loader是webpack用来处理加载不同资源文件的插件,它只在webpack对资源文件进行加载阶段使用。

loader的格式

从前面的文章webpack由浅入深——(webapck简易版)可以知道,loader的本质是一个函数。

getSource(modulePath) {
        let source = fs.readFileSync(modulePath, 'utf8');
        
        //获取webpack.config.js中的rules
        let rules = that.options.module.rules;

        //遍历rules调用loader
        for (let i = 0; i < rules.length; i++) {
            let rule = rules[i];
            // 用rule的test中正则匹配文件的类型是否需要使用laoder
            if (rule.test.test(modulePath)) {
                //获取rule中的loaders,例如['style-laoder','css-loader']
                let loaders = rule.use;
                let length = loaders.length;    //loader的数量 
                let loaderIndex = length - 1;   // 往右向左执行
                
                // loader遍历器
                function iterateLoader() {
                    let loaderName = loaders[loaderIndex--];
                    //loader只是一个包名,需要用require引入
                    let loader = require(join(that.root, 'node_modules', loaderName));
                    //使用loader,可以看出loader的本质是一个函数
                    source = loader(source);
                    if (loaderIndex >= 0) {
                        iterateLoader();
                    }
                }
                //遍历执行loader
                iterateLoader();
                break;
            }
        }
        return source; 
    }

所以loader的结构一般为:

module.exports = function (source) {
    //TODO需要执行的逻辑
}

实现按需加载的loader

  • 不按需加载:
import { flatten,concat } from "lodash"
console.log(flatten([1,2],[3,4,[5,6]]));
console.log(contcat([1,2],[3,4]));

优化前

  • 按需加载:
import flatten from "lodash/flatten"
import concat from "lodash/concat"
console.log(flatten([1,2],[3,4,[5,6]]));
console.log(contcat([1,2],[3,4]));

优化后

  • 利用ast实现babel-loader的插件
  1. 安装需要的插件
npm install babel-core babel-types -D
  1. babeljs.io/en/repl.htm…
    normal
    default
  2. 编写babel-loader插件
//mode_modules/babel-plugin-babel-import
let babel = require('babel-core');
let types = require('babel-types');
const visitor = {
    ImportDeclaration:{
        enter(path,state={opts:{}}){
            const specifiers = path.node.specifiers;
            const source = path.node.source;
            //加载的是lodash并且通过{xxx,xxx}的形式加载
            if(state.opts.library == source.value && !types.isImportDefaultSpecifier(specifiers[0])){
                const declarations = specifiers.map((specifier,index)=>{
                    return types.ImportDeclaration(
                        [types.importDefaultSpecifier(specifier.local)],
                        types.stringLiteral(`${source.value}/${specifier.local.name}`)
                    )
                });
                //替换原来的节点
                path.replaceWithMultiple(declarations);
            }
        }
    }
}
module.exports = function(babel){
    return {
        visitor
    }
}
  1. 修改配置文件
const path = require("path");
const fs = require("fs");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            plugins: [["babel-import", { library: "lodash" }]]
          }
        }
      }
    ]
  },
  resolve: {},
  plugins: [],
  devServer: {}
};

plugin

什么是插件

插件向第三方开发者提供了webpack引擎中完整的能力。使用阶段式的构建回调,开发者可以引入自定义插件到webpack构建流程中,几乎能够任意更改webpack编译结果。

对象 钩子
Compiler run,compile,compilation,make,emit,done
Compilation buildModule,normalModuleLoader,succeedModule,finishModule,seal,optimize,after-seal
Module Factory beforeResolver,afterResolver,module,parser
Parser program,statement,call,expression
Template Factory hash,bootstrap,localVars,render

插件的格式

从前面的文章webpack由浅入深——(webapck简易版)可以知道,其实插件是往钩子中注册回调的函数。

//../lib/Compiler
class Compiler {
    constructor(options){
        this.options = options;
         this.hooks = {
            entryOption: new SyncHook(),
            afterPlugins: new SyncHook(),
            run: new SyncHook(),
            beforeCompile: new SyncHook(),
            afterCompile: new SyncHook(),
            emit: new SyncHook(),
            afterEmit: new SyncHook(),
            done: new SyncHook(),
        }
    }
    .....
}
#! /usr/bin/env node    
const path = require('path');
const fs = require('fs');
const root = process.cwd();
const Compiler = require('../lib/Compiler');
let options = require(path.resolve('webpack.config.js'));
let compiler = new Compiler(options); 
compiler.hooks.entryOption.call();     //触发entryOptions
let {plugins} = options;        //获取webpack.config.js中的plugns进行注册
plugins.forEach(plugin => {
    plugin.apply(compiler)
});
compiler.hooks.afterPlugins.call(),     //触发afterPlugins
compiler.run();

所以简单插件的格式一般为:

class xxxxPlugin{
    //new xxxxPlugin(options)
    constructor(options) {
        this.options=options;
    }
    apply(compiler) {
        //往钩子上注册回调
        compiler.hooks.xxxx.tap('xxxxPlugin', ()=> {
            //TODO执行的逻辑
        });
    }
}
module.exports=xxxxPlugin;

实现AutoExternalPlugin

前篇webpack由浅入深——(webpack优化配置)中提到了external来cdn引用第三方库从而减小文件体积,但是存在一个问题,必须手动在模板的html文件中预先写好script标签引入第三方的cdn,AutoExternalPlugin实现自动插入script。

  1. 修改配置文件
const path = require("path");
const fs = require("fs");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const AutoExternalPlugin = require("./plugin/AutoExternalPlugin");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: []
  },
  resolve: {},
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html"
    }),
    new AutoExternalPlugin({
      jquery: {
        varName: "jQuery",
        url: "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"
      }
    })
  ],
  devServer: {
    contentBase: path.resolve(__dirname, "dist"),
    host: "localhost",
    port: 3000
  }
};
  1. 创建插件
/*
1. 分析import xxxx语句是否引用了特定的模块 
2. 自动往html中插入一个script标签,src就等于cdn地址
3. 生成模块的时候,如果是插件配置的模块生成一个外部模块返回
*/
const ExternalModule = require("webpack/lib/ExternalModule");
class AutoExternalPlugin {
    constructor(options) {
        this.options = options;
        //记录外部模块
        this.externalModules = {};
    }
    apply(compiler) {
        //normalModuleFactory普通模块工厂,
        compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
            normalModuleFactory.hooks.parser
                .for('javascript/auto')
                .tap('AutoExternalPlugin', parser => {
                    //当语法拿到会遍历语法树,当遍历到import节点的时候会
                    //statement就是import $ from 'jquery'语句 
                    //source'jquery'的文件路径 ;
                    parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
                        //jquery模块要变成外部模块
                        if (this.options[source]) {
                            this.externalModules[source] = true;
                        }
                    });
                })
            //factory是一个工厂,完成创建模块的工作
            normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => {
                const dependency = data.dependencies[0];
                let value = dependency.request;//jquery 
                //需要转成外部模块,执行这里的逻辑
                if (this.externalModules[value]) {
                    //let $ = window.jQuery;
                    callback(null, new ExternalModule(this.options[value].varName, 'window'));
                //否则执行正常的工厂方法,默认创建一个普通的模块    
                } else {
                    factory(data, callback);
                }
            });
        });
        compiler.hooks.compilation.tap('InlinePlugin', (compilation) => {
            compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('InlinePlugin', (htmlData, callback) => {
                Object.keys(this.externalModules).forEach(key => {
                    htmlData.body.unshift({
                        tagName: 'script',
                        closeTag: true,
                        attributes: { type: 'text/javascript', src: this.options[key].url }
                    });
                });
                callback(null, htmlData);
            });
        });
    }
}
module.exports = AutoExternalPlugin;

结语:

webpack系列文章已经完结,后面会持续增加和修改内容。