webpack4配置到优化到原理(下)

1,317 阅读8分钟

五. webpack原理

  • webpack就是基于事件流的编程范例,一系列的插件运行

webpck整体流程

一句话说:从入口文件开始,递归地构建一个依赖关系图,包含了应用程序需要的每个模块,将所有这些模块打包成一个或多个文件

简单说

  1. 初始化参数:确定行为,
  2. 加载插件:便于后续的监听,针对特定的事件执行特定的逻辑
  3. 编译模块:从入口文件开始递归编译所有依赖的模块,利用相应的loader对其进行转换,
  4. 生成文件:根据入口和模块间的依赖关系,生成chunk
  5. 输出文件:根据output及其他插件确定输出路径和文件名,输出到对应的文件系统中

详细说

webpack构建流程图

webpack 的事件节点,较多,这里只说了一些关键的事件节点和做的事情

  1. 初始化参数:读取shell语句和package配置文件的参数

    • 对参数的具体分析靠webpack-cli,命令调用webpack做相应的编译构建 【1】对参数使用错误进行报错 【2】对非构建命令执行相应的操作(如npm inpm init等)
  2. 实例化Compiler

    • 利用参数初始化Compiler,来广播和监听事件
  3. 加载插件

    • 调用complierapply方法,相当于注册各种插件的回调方法
    • 内部插件挂载到 compiler,便于后续的监听,针对特定的事件执行特定的逻辑
  4. options参数,转化成插件

    • 主要利用webpackOptionsApply,将webpack&package配置文件和命令行里的中的options参数,转化成插件
    • 例如: externals -> ExtrenalsPlugin
  5. 对entry的单多入口处理

    • itemToPlugin方法判断单入口还是多入口文件,进而使用MultiEntryPluginSingleEntryPlugin
  6. 开始编译(run)

    • 调用compiler 的run方法开始,触发compile,创建compilation对象
  7. 模块构建(make)

    • 执行addEntry方法,将entry加入到构建列表中,从 entry开始,对依赖模块进行build
    • build-module,构建 某个模块,使用对应的 Loader 去转换一个模块
    • 一个模块转换完后,使用acorn解析, 生成对应的抽象语法树(AST),便于后面对代码的分析
  8. chunk生成(seal阶段)

    • 先将entry对应的module都生成一个新的chunk
    • 遍历module的依赖列表,依赖的module也加入到chunk
    • 如果依赖的module是动态引入的模块,那么根据module创建一个新的chunk,据徐遍历依赖,
    • 重复直到得到所有的chunks
  9. 输出 emit

    • 获取输出的内容,将输出的内容输出到对应的磁盘中去

重点说

tapable

  • 核心对象 compilercompilation 都继承于 tapable
  • tapable 类似node里 EventEmitter发布订阅模块, 控制钩子函数的发布与订阅
  • 众多插件监听 compiler 和compilation 上关键的事件节点

compile

  • 继承于tapable对象, 包含了webpack环境的所偶的配置信息,
  • 在webpack启东时被实例化,全局唯一
  • 可以广播和监听webpack事件

compilation

  • 继承于tapable对象,包含了当前的模块资源、编译生成资源、变化的文件
  • 每当文件变化,新的compilation被创建,

AST

  • 抽象语法树
  • 以树状的形式表现编程语言的语法结构
  1. 用处
    • 比如在vsCode中的代码风格,语法的检查,错误提示,格式化,自动补全
    • 代码压缩
    • babel,TS,JSX等的转译
    • 模版引擎

举例

const html = '<div><span>tom</span></div>'
const ast = {
  tag: 'div',
  children: [
    {
      tag: 'span'
    },
  ],
}

具体看在线demo

模块化

随着前端发展,越来越复杂,模块化的必要性大大增加,

  1. <script>标签方式是存在问题的

    • 全局作用域下容易造成变量冲突
    • 文件只能按照<script>的书写顺序进行加载
  2. CommonJs(CJS)

    • 使用require关键字,
    • 可动态导入
    • NodeJS使用
    • 同步的加载方式不适合使用在浏览器异步资源中
  3. AMD

    • 借鉴了commonjs,浏览器使用
    • define 定义模块,require使用模块
  4. ESModule(ESM)

    • 静态导入,编译时就可以确定模块的依赖关系,便于静态分析,
    • treeShaking就是在此基础上实现的
    • 浏览器支持不够友好

webpack简单实现

流程图

webpack简单实现流程图
简单实现流程

  1. 通过babylon对代码转换成AST,
  2. 获取文件的依赖,
  3. 通过babel-core将AST重新生成源码
  4. 以上三者记录在模块列表modules
  5. 深度遍历依赖,都push进模块列表中,
  6. 遍历模块列表,利用字符串拼接写入指定文件目录

webpack打包文件代码大致格式

// 一个立即执行函数
(function(modules) {
...
})({
 modules
})

// modules
{
  "./src/index.js": (function (require, module, exports) {
   ...
  }),
  "./src/tools.js": (function (require, module, exports) {
    ...
  }),
  "./src/two.js": (function (require, module, exports) {
   ...
  })
}

核心代码

// ./lib/index.js webpack入口执行文件
const Compiler = require('./compiler')
const options = require('../webpack.config.js')
new Compiler(options).run();

// ./lib/compiler.js 核心compiler类,负责编译和输出
const { getAST, getDepencies, transform } = require('./parser.js')
const path = require('path')
const fs = require('fs');
module.exports = class Compiler {
    constructor(options) {
        const { entry, output } = options
        this.entry = entry;
        this.output = output;
        this.modules = []; // 生成的模块列表 
    }
    run() { // 开始构建
        const entryModule = this.buildModule(this.entry, true);
        this.modules.push(entryModule)
        this.modules.forEach((_module) => {// 遍历处理依赖 
            _module.dependencies.forEach((dependency)=> {
                this.modules.push(this.buildModule(dependency)); //依赖也执行模块构建
            })
        })
        this.emitFiles();
    }
    buildModule(filename, isEntry) { //模块构建
        let ast;
        if(isEntry) { // 文件是入口模块
            ast = getAST(filename); // 源代码转换成AST树
        } else {
            // 将相对路径转化为绝对路径
            const absolutePath = path.join(process.cwd(), './src', filename)
            ast = getAST(absolutePath);
        }
        return {
            filename, // 文件名
            dependencies: getDepencies(ast), // 文件依赖
            source: transform(ast), // 重新生成ES5源码
        }

    }
    emitFiles() { // 输出
        const outputPath = path.join(this.output.path, this.output.filename)
        let modules = '';
        this.modules.forEach((_module)=>{
            modules += `'${_module.filename}': function(require, module, exports) {${_module.source}},`
        })
        // 实现类似webpack require函数来解析依赖,每个模块进行包裹
        const bundle = `(function(modules) {
            function require(filename) {
                var fn = modules[filename]
                var module = {exports : {}}
                fn(require, module, module.exports)
                return module.exports;
                
            }
            require('${this.entry}')
        })({${modules}})`;
        fs.writeFileSync(outputPath, bundle, 'utf-8')
    }
}

//  ./lib/parser.js 解析器,分析依赖,ast的获取与转化方法
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('babel-core');
module.exports = {
    getAST: (path) => { // 通过babylon对代码转换成AST,获取AST树
        const source = fs.readFileSync(path, 'utf-8')
        return babylon.parse(source, {
            sourceType: 'module'
        })
    },
    getDepencies: (ast) => { // 分析依赖,
        const depencies = [];
        traverse(ast, {
            ImportDeclaration: ({node}) => {
                depencies.push(node.source.value)
            }
        })
        return depencies
    },

    transform: (ast) => { // 通过babel-core将AST重新生成源码
        const { code } = transformFromAst(ast, null, {
            presets: ['env']
        } );
        return code
    }
}

六. loader

1. 简介

  • loader是一个导出为函数的javascript的模块,它就像一个纯函数,输入不变输出就不变, 举例如下:
module.exports = function (source){
    return source;
}
  • 多个loader的执行: 串行执行, 从右往左

举例

      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader',     
          'postcss-loader',
        ],
      },

2. 运行环境

  • loader有单独的运行环境loader-runner,允许在不安装webpack情况下运行loaders loader-runner作用
  • 作为webpack的依赖,在webpack中使用他执行loader
  • 进行loader的开发和调试

举例

import { runnerLoaders } from "loader-runner"
runner({
    resource: '/path/index.js?query', // 要解析的文件
    loaders: ['/path/loader.js?query',] // 解析使用的loader
    context: { // 上下文参数,
        minimize: true // 压缩参数
        } 
    readResource: fs.readFile.bind(fs) // 查询resource的方式
}, function(err, res) { // err 错误信息,和执行结果

})

3. 参数获取

  • 通过 loader-utils插件 的geOptions方法获取

举例

const loaderUtils = require('loader-utils')
    module.exports = function (source) {
    const { name } = loaderUtils.getOptions(this)
}

4. 异常处理

  1. loader 通过 throw 抛出
  2. 通过 this.callback 传递错误(同步loader)

举例

module.exports = function (source) {
    throw new Error('Error')
    // 一般情况下使用this.callback 返回,第一个参数是error对象
    this.callback(new Error('Error'), source) 
    // 回传多个值,也可使用this.callback返回
    this.callback(new Error('Error'), source, 2, 3, 4) 
}

5. 异步处理

  • 通过 this.async() 来返回一个异步函数
  • 第一个参数是Error, 第二个参数是处理的结果

举例

module.exports = function (source) {
    const callback = this.async();
    fs.readFile(// 异步读取文件
        path.join(__dirname, './async.txt'), 
        'utf-8',  
        (err, res) => {
            callback(null, res);
    }));
    
}

6. 缓存的使用

  • webpack 默认开启缓存

  • 可使用 this.cacheable(false) 关掉缓存

  • 缓存条件:

    【1】loader的结果在相同的输入下有确定的输出

    【2】有依赖的loader 无法使用缓存

7. 文件输出

  • 利用this.emitFile进行文件写入(它也是file-loader实现的关键)
  • loaderUtils.interpolateName 用占位符或一个正则表达式转换成一个文件名

举例

const loaderUtils = require('loader-utils')
module.exports = function (source) {
    const url = loaderUtils.interpolateName(
        this, // 上下文执行环境,
        "[hash].[ext]", // 占位符
        {
            source
        }
    )
    this.emitFile(url, source)
    return source
}

七. 插件(plugin)

1. 简介

  • loader不能做的事就要plugin来做啦,
  • 我们知道在webpack配置plugins数组里 new Plugin() 来使用插件,
  • 所以我们编写一个插件就是一个Class ,实例化它,
  • 插件都会有apply 方法接收compiler对象,plugin可以监听compiler的hooks,在特定的时间去执行特定的逻辑

举例

module.exports = class SpecialPlugin {
    apply(compiler) {
            // 在某个hooks阶段
            compiler.hooks.done.tap('SpecialPlugin',(compilation, callback) => {
            console.log('hello ,world');
        })
    }
}

2. 运行环境

  • 插件没有像loader那样独立运行环境
  • 只能在webpack里面运行
  • 如果编写一个插件测试,只能依赖 webapck, webpack-cli,webpack.config.js 把插件放入plugin中测试

举例

const path = require('path')
const SpecialPlugin = require('./plugins/specialPlugin')
module.exports = {
    entry: './src/index.js'
    output: {
        path: path.join(__dirname, './dist')
        filename: 'main.js'
    }
    plugins: [
        new SpecialPlugin({
            name: 'SpecialPlugin'
        });
    ]
}

3. 获取参数

  • 直接使用构造函数 获取参数,

举例

module.exports = class MyPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        console.log('options', this.options);
    }
}

4. 异常处理

  1. throw 抛出
  2. 通过compilation 对象的warnings 和 errors接收

举例

module.exports = class SpecialPlugin {
    apply(compiler) {
            compiler.hooks.done.tap('SpecialPlugin',
            (compilation, callback) => {
                throw new Error('Error)
                compilation.warnings.push("warning")
                compilation.errors.push('error')
        })
    }
}

5. 文件写入

  • 监听compileremit文件生成阶段, 获取compileation对象,把内容赋给assets对象
  • 在最终emit生成的时候,会读取assets对象,输出到设定好的文件位置
  • 生成文件时也需要webpack-sources的配合,比如下面的输出一段代码到文件中去,就可以使用RawSource

举例

// 简单的插件,输出一段代码到文件中去
const RawSource = require('webpack-sources').RawSource;
module.exports = class SpecialPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        const { name } = this.options;
        // 监听compiler的emit的hooks
            compiler.hooks.emit.tap('SpecialPlugin', (compilation, callback) => {
                // 将想要设置的内容,赋值给assets
            compilation.assets[name] = new RawSource('content')
        })
    }
}

总结

webpack基本告一段落

  1. 核心概念
  2. 其他常用配置
  3. 优化手段
  4. 配置总结
  5. webpack整体流程和简单实现
  6. loader相关
  7. 插件相关

相关链接

webpack4配置到优化到原理(上)

参考

  1. 玩转webpack

  2. webpack原理

  3. webpack原理