webpack 入门到精通 (下)

未命名文件.png 本章讲解的部分是 wabpack 的高级进阶部分,本章节适合有wepack基础知识和一定的vue、react开发的人群,可以先去webpack入门到精通(上) 看完再过来看本章节,方便你更好的理解webpack高级进阶部分。

1、vue create vue-project

通过脚手架快速构建项目,将 vue 中的 webpack 部分暴露出来,以方便我们再做一下构建上面的性能优化,同时也可以让我们知道在 vue 中 webpack 是如何构建项目的,理清构建项目分析的各个步骤。各部分的相关的源码注释在gitee上有。

  • 通过 vue inspect --mode=development > webpack.dev.js 可以从 vue 中将开发模块的 development 抛出来,以方便在开发环境的时候只需要关注 webpack.dev.js 里面的代码即可。
  • 通过 vue inspect --mode=production > webpack.prod.js 可以从 vue 中将生产模块的 production 抛出来,以方便在生产环境的时候只需要关注 webpack.prod.js里面的代码即可。 通过研究 vue 中的 webpack.prod.js 和 webpack.dev.js 两者可以看出,本质上两者并没有什么太大的区别,唯一的不同点就是 webpack.prod.js 里面将 js 、css、html 代码进行了压缩和加入了thread-loader进行了多线程打包,其余的都是一样的。

2、create-react-app react-project

通过脚手架快速构建项目,将 react 中的 webpack 部分暴露出来,以方便我们再做一下构建上面的性能优化,同时也可以让我们知道在 react 中 webpack 是如何构建项目的,理清构建项目分析的各个步骤。各部分的相关的源码注释在gitee上有。

  • 通过 npm run ejct 将 webpack 的配置文件暴露出来 其中script文件夹中有 build.js、start.js.和 test.js,在这里我们只需要关注 build.js 和 start.js 即可,其中有一个 paths.js 文件主要来配置路径。

  • webpack.config.js(主要内容为对loader和plugin的配置,将来自己修改的时候可以直接在这个文件夹里面进行loader和plugin的修改)(重点)

  • build.js:生产环境对应的文件,与开发环境对应的文件差不多。

  • start.js:开发环境对应的文件,与生产环境文件的差不多

react 中在生产模式中没有用到多进程打包,也没有对 less 等其他样式进行配置,vue 中里面的loader兼容很多的样式,并且实现了多线程打包。在配置 webpack的时候应该各取所长,配套出一套符合自己实际要求的方案即可。

3、自定义loader

首先要明白 loader 是什么,有什么作用,webpack 入门到精通(上)可以知道 loader 作用就是用来让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只能够识别 JavaScript),比如 css、less 等,在这里可以理解为 loader 就是一个"翻译官"。loader 本质其实就是一个函数

loader函数

  1. loader 在 use 数组中的执行顺序是从右到左、从下到上的。
  2. loader 里面有一个 pitch 方法,pitch 方法在 use 数组中的执行顺序是从左到右、上到下的。如果我们想先执行某些功能,可以写在 pitch 方法中。
module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(111)
    return content
}
module.exports.pitch = function() {
    // 先执行
    console.log(111)
}
复制代码
  1. loader有三个参数: 分别是content、map、meta,分别表示进入 loader 的文件内容、文件的映射信息、文件的元信息。

同步 loader

// 第一种写法
module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(111)
    return content
}
复制代码
// 第二种写法
module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(111)
    this.callback(null, content, map, meta)
}
复制代码

异步 loader

// 异步 loader
module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(222)
    const callback = this.async()
    setTimeout(() => {
        callback(null, content)
    }, 1000);
}
复制代码

在 webpack 中主要推荐使用异步 loader,因为在异步中还可以处理其他的任务,性能会有稍微的提升。

获取 options 配置参数

module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(111)
    // 获取options中的参数
    console.log(this.getOptions())
    this.callback(null, content, map, meta)
}
复制代码

校验 options 参数

创建并配置schema.json   "additionalProperties": true可追加属性
复制代码
{
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "名称"
        }
    },
    "additionalProperties": true
}
复制代码
const { validate } = require('schema-utils')
// 引入校验schema
const schema = require('./schema.json')
module.exports = function (content, map, meta) {
    //content 文件的内容   map 文件的映射信息   meta 文件的元信息
    console.log(111)
    // 获取options中的参数
    console.log(this.getOptions())
    // 检验options是否合法 
    // 第一个参数 校验规则 
    // 第二个参数 校验对象
    // 第三个是一个对象 检验的loader 报错的时候会提示哪个loader出错
    validate(schema, this.getOptions(),{
        name: 'loader1'
    })
    this.callback(null, content, map, meta)
}
复制代码

自定义 babel-loader

{
    "type": "object",
    "properties": {
        "presets": {
            "type": "array"
        }
    },
    "additionalProperties": true
}
复制代码
const babelSchema = require('./babelSchema.json')
const { validate } = require('schema-utils')
// 生成一个promise对象
const utils = require('util')
const babel = require('@babel/core')
// babel.transform 专门用来编译代码的方法
// 返回一个普通异步方法
//我们这里使用一个promsie异步处理 所以引入utils包下下面的promisify方法
const transform = utils.promisify(babel.transform)
module.exports = function (content, map, meta) {
    // 判断是否传入options
    const options = this.getOptions() || {}
    // 检验参数
    validate(babelSchema, options, {
        name: 'Babel Loader'
    })
    // 使用异步
    const callback = this.async()
    // 使用transform编译代码
    transform(content, options)
        // code 为处理后的代码
        .then(({ code, map }) => callback(null, code, map, meta))
        .catch((e) => callback(e))
}
复制代码

4、自定义plugin

在先写 plugin 之前,我们首先要理解 compiler 当中的一些 hooks 都有哪些用途,compiler 是 webpack 的主要引擎,它通过 cli 或者 node api 传递的所有选项创建出一个 compilation 实例,它扩展自 Tapable。这里介绍 tapable里面的几个hooks(钩子)。分别是 SyncHook、SyncBailHook、AsyncParallelHook、AsyncServicesHook。

SyncHook

SyncHook 属于 tapable hooks中的同步hooks,以下有相关代码演示

const { SyncHook } = require('tapable')
class Lesson{
    constructor(){
        // 初始化 hooks 容器
        this.hooks = {
        // 创建相应的钩子容器 参数为address
        // 同步hooks:意味着里面的任务是异步执行的
        go: new SyncHook(['address'])
        }
    }
    tap() {
        // 往hooks容器中注册时间/添加回调函数
        this.hooks.go.tap('class0318', (address) => {
            console.log('class0318', address);
        })
        this.hooks.go.tap('class0418', (address) => {
            console.log('class0418', address);
        })
    }
    start() {
        // 触发hooks钩子函数
        this.hooks.go.call('c318');
    }
}
const l = new Lesson()
// 注册事件
l.tap()
// 触发注册的事件
l.start()
复制代码

输出结果:
class0318 c318
class0418 c318

SyncBailHook

SyncBailHook 属于 tapable hooks中的同步hooks,但是添加的回调函数中含有 return 语句的时候会停止往下执行。 以下有相关代码演示

const { SyncBailHook } = require('tapable')
class Lesson{
    constructor(){
        // 初始化 hooks 容器
        this.hooks = {
        // 创建相应的钩子容器 参数为address
        // 同步hooks:意味着里面的任务是异步执行的
        go: new SyncBailHook(['address'])
        }
    }
    tap() {
        // 往hooks容器中注册时间/添加回调函数
        this.hooks.go.tap('class0318', (address) => {
            console.log('class0318', address);
            return 111
        })
        this.hooks.go.tap('class0418', (address) => {
            console.log('class0418', address);
        })
    }
    start() {
        // 触发hooks钩子函数
        this.hooks.go.call('c318');
    }
}
const l = new Lesson()
// 注册事件
l.tap()
// 触发注册的事件
l.start()
复制代码

输出结果:
class0318 c318

以上两个均为 tapable 中的同步hooks,也意味着里面的任务是依次执行的。

AsyncParallelHook

AsyncParallelHook 属于 tapable hooks中的异步hooks。在执行任务的时候是异步并行执行的。以下相关代码演示

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable')
class Lesson{
    constructor(){
        // 初始化 hooks 容器
        this.hooks = {
        // 创建相应的钩子容器 参数为address
        // 同步hooks:意味着里面的任务是异步执行的
        // go: new SyncHook(['address'])
        go: new SyncBailHook(['address']) ,//遇到返回值return会停止往下执行
        // 异步hooks 
        //  AsyncParallelHook:异步并行
        leave: new AsyncParallelHook(['name', 'age'])
        }
    }
    tap() {
        // 往hooks容器中注册时间/添加回调函数
        this.hooks.go.tap('class0318', (address) => {
            console.log('class0318', address);
            return 111
        })
        this.hooks.go.tap('class0418', (address) => {
            console.log('class0418', address);
        })
        // 触发异步hooks 第一种写法
        this.hooks.leave.tapAsync('class0518', (name, age, cb) => {
            setTimeout(() => {
                console.log('class0518', name, age);
                cb()
            }, 2000);
        })
        // 第二种写法
        this.hooks.leave.tapPromise('class0518', (name, age) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log('class0518', name, age);
                    resolve()
                }, 1000);
            })
        })
    }
    start() {
        // 触发hooks钩子函数
        this.hooks.go.call('c318');
        this.hooks.leave.callAsync('alex', 18, () => {
            // 回调函数 代表所有leave容器中的回调函数触发完了,才触发
            console.log('end')
        })
    }
}
const l = new Lesson()
// 注册事件
l.tap()
// 触发注册的事件
l.start()
复制代码

输出结果:
class0318 c318
class0518 alex 18
class0618 alex 18
end
这里的 tapPrimose 会在 1s 后执行输出 class0518 alex 18 ,而 tapAsync 也会在 tapPromise 输出的 1s 后输出 class0618 alex 18 ,这也验证了 AsyncParalleHook 是异步并行执行。

AsyncSeriesHook

AsyncSeriesHook 属于 tapable hooks中的异步hooks。在执行任务的时候是异步串行执行的。以下相关代码演

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable')
class Lesson{
    constructor(){
        // 初始化 hooks 容器
        this.hooks = {
        // 创建相应的钩子容器 参数为address
        // 同步hooks:意味着里面的任务是异步执行的
        // go: new SyncHook(['address'])
        go: new SyncBailHook(['address']) ,//遇到返回值return会停止往下执行
        // 异步hooks 
        //  AsyncParallelHook:异步并行
        // leave: new AsyncParallelHook(['name', 'age'])
        leave: new AsyncSeriesHook(['name', 'age'])
        }
    }
    tap() {
        // 往hooks容器中注册时间/添加回调函数
        this.hooks.go.tap('class0318', (address) => {
            console.log('class0318', address);
            return 111
        })
        this.hooks.go.tap('class0418', (address) => {
            console.log('class0418', address);
        })
        // 触发异步hooks 第一种写法
        this.hooks.leave.tapAsync('class0518', (name, age, cb) => {
            setTimeout(() => {
                console.log('class0618', name, age);
                cb()
            }, 2000);
        })
        // 第二种写法
        this.hooks.leave.tapPromise('class0518', (name, age) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log('class0518', name, age);
                    resolve()
                }, 1000);
            })
        })
    }
    start() {
        // 触发hooks钩子函数
        this.hooks.go.call('c318');
        this.hooks.leave.callAsync('alex', 18, () => {
            // 回调函数 代表所有leave容器中的回调函数触发完了,才触发
            console.log('end')
        })
    }
}
const l = new Lesson()
// 注册事件
l.tap()
// 触发注册的事件
l.start()
复制代码

输出结果:
class0318 c318
class0618 alex 18
class0518 alex 18
end 先等 2s 后输出class0618 alex 18 ,然后再等 1s 输出class0518 alex 18,说明了是异步串行执行任务。

理解了以上的 hooks 之后,可以让我们更加的容易去理解 compiler 里面的 hooks 以及相关 hooks 的使用。

compiler 的使用

plugin 就是在某些 hooks 函数中调用,调用的时候去修改相关的资源或者添加相关的资源,从而使 webpack 在构建打包的时候使资源发生变化或者输出更多的资源。而 compiler 是 plugin 在运行的时候触发 compiler 上面的各个 hooks,从而实现资源发生变化。
compiler的钩子函数太多,这里就介绍两个thisCompilation hooks 和 emit hooks,就不一一列举了,详细请去webpack compiler钩子官网查看

emit

输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器。同时这个钩子属于 tapable 中的 AsyncSeriesHook 异步串行钩子。

class Plugin1{
    // 每个插件都会自动的执行这个apply函数 相当于 类中的constructor
    apply(compiler){
        // 添加回调函数
        // 输出 asset 到 output 目录之前执行
        compiler.hooks.emit.tap('Plugin1',(compilation) => {
            console.log('emit tap');
        })
        compiler.hooks.emit.tapAsync('Plugin1',(compilation, cb) => {
            setTimeout(() => {
                console.log('emit tapAsync tap');
                cb()
            }, 1000);
        })
        compiler.hooks.emit.tapPromise('Plugin1',(compilation) => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log('emit tapPromise tap');
                    resolve()
                }, 1000);
            })
        })
        compiler.hooks.afterEmit.tap('Plugin1',(compilation) => {
            console.log('afterEmit tap');
        })
        compiler.hooks.done.tap('Plugin1',(compilation) => {
            console.log('done tap');
        })
    }
}
module.exports  = Plugin1;
复制代码

输出结果:
emit tap
emit tapAsync tap
emit tapPromise tap
afterEmit tap
done tap

thisCompilation

初始化 compilation 时调用,在触发 compilation 事件之前调用,也就是说会生成一个 compilation 。这个钩子 不会 被复制到子编译器。同时接收的参数为 compilation,compilationParams。 这个钩子函数属于 tapable hooks 中的 SyncHook 同步钩子。

compilation 的使用

上面通过 compiler 的 thisCompilation 钩子生成了一个 compilation,而 compilation 也跟 compiler 一样都有生命钩子。同时 compilation 也是一个对象。里面就包含对我们打包后的资源进行各种操作,比如添加资源、修改资源都可以去 compilation 对象里面进行相应的操作。

为了在 node 环境下更好的进行 webapck 调试,在 package.json 中我们进行以下修改配置

"scripts": {
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  },
复制代码
class Plugin2 {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('Plugin', (compilation) => {
            debugger
            console.log(compilation)
        })
    }
}


module.exports = Plugin2
复制代码

在打断之后,执行 npm start,按 F12 可以找到 node 图标进入到调试工具中,在右边的watch中添加 compilation即可有相关的方法出现包括 hooks 。

image.png

下面,我们实现的需求就是在输出资源添加新的资源输出。

additionalAssets

为 compilation 创建额外 asset,实现添加输出资源。 这个钩子可以用来下载图像,这个钩子函数属于 tapable hooks 中的 AsyncSeriesHook 异步串行钩子。

输出新的资源
复制代码

class Plugin2 {
    apply(compiler) {
        // 初始化 compilation
        compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
            // debugger
            // console.log(compilation)
            //添加资源
            compilation.hooks.additionalAssets.tapAsync('Plugin2', (cb) => {
                // debugger
                // console.log(compilation)
                const content = 'hello plugin'
                // 往输出资源中添加a.txt输出
                compilation.assets['a.txt'] = {
                    // 文件的大小
                    size() {
                        return content.length
                    },
                    // 文件的内容
                    source() {
                        return content
                    }
                }
                cb()
            })
        })
    }
}


module.exports = Plugin2
复制代码

执行 npx webpack,dist 文件夹中输出了 a.txt 文件。

以一个作为为模板输出
复制代码
const fs = require('fs')
const util = require('util')
const path = require('path')
// 将fs.readFile方法变成基于promise的异步方法
const readFile = util.promisify(fs.readFile)
const webpack = require('webpack')
const { RawSource } = webpack.sources
class Plugin2 {
    apply(compiler) {
        // 初始化 compilation
        compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
            // debugger
            // console.log(compilation)
            //添加资源
            compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
                // debugger
                // console.log(compilation)
                const content = 'hello plugin'
                // 往输出资源中添加a.txt输出
                compilation.assets['a.txt'] = {
                    // 文件的大小
                    size() {
                        return content.length
                    },
                    // 文件的内容
                    source() {
                        return content
                    }
                }
                const data =  await readFile(path.resolve(__dirname, 'b.txt'))
                // 生成 webpack 资源文件并输出
                // compilation.assets['b.txt'] = new RawSource(data)
                // 跟上面等价
                compilation.emitAsset('b.txt', new RawSource(data))
                cb()
            })
        })
    }
}


module.exports = Plugin2
复制代码

实现一个 CopyWebpackPlugin

const { validate } = require('schema-utils')
const path = require('path')
const fs = require('fs')
const { promisify } = require('util')
const readFile = promisify(fs.readFile)
const schema = require('./schema.json')
const webpack = require('webpack')
const { RawSource } = webpack.sources
// 专门用来匹配文件列表 忽略以下文件  返回一个promise对象
const globby = require('globby')
class CopyWebpackPlugin {
    constructor(options){
        // 验证参数
        validate(options, schema, {
            name: 'CopyWebpackPlugin'
        })
        // 获取参数
        this.options = options
    }

    apply(compiler){
        // 初始化 compilation
        compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
            // 添加资源的 hooks
            compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
                const { from, ignore} = this.options
                // 给个默认值
                const to = this.options.to || '.'
                // 获取上下文路径 就是webpack的配置
                const context = compiler.options.context;
                // 将输入的路径变成绝对路径
                const absolutePath = path.isAbsolute(from) ? from : path.resolve(context, from)
                // globby(要处理的文件夹(要绝对路径),options) 
                // globby不识别/ 要将/ 替换成\
                const absoluteFrom = absolutePath.replace(/\\/g,'\/')
                // 1、读取from中的所有资源、过滤忽略的文件
                const paths = await globby(absoluteFrom, { ignore })
                // 2、读取所有要加载的文件路径
                const files =  await Promise.all(
                    paths.map(async (absolutePath) => {
                        // 因为 map 方法遇到 ayncs 函数并不会等待 所以这里要使用 promise.all 方法
                        const data =  await readFile(absolutePath)
                        // 以路径中的的最后一个名字 作为输出的名称
                        const relativePath = path.basename(absolutePath)
                        //结合 to 属性
                        // 没有 to reset.cs
                        // 有to 若to为css 则输出为 css/reset.css
                        const fileName = path.join(to, relativePath)
                        // map返回一个数组  async 返回一个 promise 对象
                        return {
                            // 文件数据
                            data,
                            // 文件名称
                            fileName
                        }
                    })
                )
                // 3、生成 webpack 资源文件
                    const assets = files.map((file) => {
                        const source = new RawSource(file.data)
                        return {
                            source,
                            filename: file.fileName
                        }
                    })
                // 4、添加到 compilation中 输出出去
                assets.forEach(item=>{
                    // 第一个参数 文件名称 第二个 文件数据
                    compilation.emitAsset(item.filename,item.source)
                })
                cb()
            })
        })
    }
}
module.exports = CopyWebpackPlugin
复制代码

5、手写 Webpack

在手写 webpack 之前我们先了解 webpack 的打包的执行流程是怎样的,这样更加方便、更加容易让我们去手写和理解其中的代码含义。

webpack 的执行流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:上布得到的参数初始化 Compiler 对象,加载所有配置的插件,执行 Compiler 的 run 方法开始编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过本步骤的处理(递归自直到所有模块被加载进来)。
  5. 完毕模块编译:经过第 4 步使用 Loader 翻译完所有模块后,得到每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,Webpack 会在特定的时间点有相关的 hooks,插件可以在相关的hooks中进行执行的相关逻辑,从而改变 Webpack 的运行结果。

定义 Compiler 类

class Compiler {
    constructor(options = {}) {
        // weback配置对象
        this.options = options
        // 所有依赖容器
        this.modules = []
    }
    // 启动 webpack 打包
    run() {}

    //开始构建
    build(filePath) {}
    
    //构建资源输出
    gennerate(depsGraphs) {}
}
module.exports = Compiler
复制代码

解析 AST 抽象语法树

使用 babel 提供的 @babel/parser 将代码转换成 AST 抽象语法树。目的是让我们可以更好的去分析模块之间的相关依赖。

const fs = require('fs')
// 解析AST语法树
const babelParser = require('@babel/parser')
const parser = {
    // 获取AST抽象语法树 
    getAst(filePath) {
        const file = fs.readFileSync(filePath, 'utf-8')
        // 2 将其解析 AST 抽象语法树 目的是去分析依赖
        const ast = babelParser.parse(file, {
            sourceType: 'module' // 解析的模块为 ES Module
        })
        return ast
    },
}
复制代码

获取所有依赖的模块

这是使用 babel 提供的 @babel/traverse,在 traverse 中其中有个 ImportDeclaration 方法可以让我们快速的在 AST 中找到哪些是 import 依赖模块。

const fs = require('fs')
const path = require('path')
// 收集依赖
const traverse = require('@babel/traverse').default
// 解析AST语法树
const babelParser = require('@babel/parser')
const parser = {
    // 获取AST抽象语法树 
    getAst(filePath) {
        const file = fs.readFileSync(filePath, 'utf-8')
        // 2 将其解析 AST 抽象语法树 目的是去分析依赖
        const ast = babelParser.parse(file, {
            sourceType: 'module' // 解析的模块为 ES Module
        })
        return ast
    },
    // 获取依赖
    getDeps(ast,filePath) {
        // 获取入口文件夹路径 绝对路径
        const dirname = path.dirname(filePath)

        //存储依赖的容器
        const deps = {}
        // 收集依赖
        traverse(ast, {
            // 内部会遍历ast中的prpgram.body 判断里面语句类型
            // 如果 type:ImportDeclaration 就会触发当前函数
            ImportDeclaration({ node }) {
                // 文件的相对路径
                const relativePath = node.source.value
                // 绝对路径 基于入口文件
                const absolutePath = path.resolve(dirname, relativePath)
                // 添加依赖
                deps[relativePath] = absolutePath.replace(/\\/g,'\/')
            }
        })
        return deps
    },
}
复制代码

将 AST 解析成 code

这是使用 babel 提供的 @babel/core 和 @babel/preset-env,从 @babel/core 中解构出 transformFromASt 方法,将代码中的浏览器不能识别的语法进行编译。

const fs = require('fs')
const path = require('path')
// 解析AST语法树
const babelParser = require('@babel/parser')
// 收集依赖
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const parser = {
    // 获取AST抽象语法树 
    getAst(filePath) {
        const file = fs.readFileSync(filePath, 'utf-8')
        // 2 将其解析 AST 抽象语法树 目的是去分析依赖
        const ast = babelParser.parse(file, {
            sourceType: 'module' // 解析的模块为 ES Module
        })
        return ast
    },
    // 获取依赖
    getDeps(ast,filePath) {
        // 获取入口文件夹路径 绝对路径
        const dirname = path.dirname(filePath)
        //存储依赖的容器
        const deps = {}
        // 收集依赖
        traverse(ast, {
            // 内部会遍历ast中的prpgram.body 判断里面语句类型
            // 如果 type:ImportDeclaration 就会触发当前函数
            ImportDeclaration({ node }) {
                // 文件的相对路径
                const relativePath = node.source.value
                // 绝对路径 基于入口文件
                const absolutePath = path.resolve(dirname, relativePath)
                // 添加依赖
                deps[relativePath] = absolutePath.replace(/\\/g,'\/')
            }
        })
        return deps
    },
    //将ast解析成code
    getCode (ast){
        // 编译代码:将代码中浏览器不能识别的语法进行编译
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        })
        return code
    }
}

module.exports = parser
复制代码

递归遍历收集所有的的依赖

const { getAst, getDeps, getCode } = require('./parser')
class Compiler {
    constructor(options = {}) {
        // weback配置对象
        this.options = options
        // 所有依赖容器
        this.modules = []
    }
    // 启动 webpack 打包
    run() {
        // 1 读取入口文件内容
        const filePath = this.options.entry
        // 第一次构建 获取入口文件信息
        const fileInfo = this.build(filePath)
        // 将文件信息放到依赖容器中
        this.modules.push(fileInfo)
        //递归遍历
        for (let i = 0; i < this.modules.length; i++) {
            if (this.modules[i].deps) {
                // 遍历
                for (const relativePath in this.modules[i].deps) {
                    // 取出依赖文件的绝对路径
                    const absolutePath = this.modules[i].deps[relativePath]
                    // 将依赖的文件进行ast deps code处理
                    const fileInfo = this.build(absolutePath)
                    // 将处理后的结果添加到modules中
                    this.modules.push(fileInfo) // 这里可以边遍历边向数组中添加元素,遍历的长度会随着元素的变化而发生变化
                }
            }
        }
    }
    //开始构建
    build(filePath) {
        // 将文件解析Ast语法树
        const ast = getAst(filePath)
        // 获取ast中的所有依赖
        const deps = getDeps(ast, filePath)
        // 将ast解析成浏览器识别的代码 编译代码
        const code = getCode(ast)
        return {
            // 当前模块的文件路径
            filePath,
            // 当前文件的所有依赖
            deps,
            // 当前文件解析后的所有代码
            code
        }
    }

    //构建资源输出
    gennerate(depsGraphs) {}
}
module.exports = Compiler
复制代码

整理依赖关系图

const { getAst, getDeps, getCode } = require('./parser')
class Compiler {
    constructor(options = {}) {
        // weback配置对象
        this.options = options
        // 所有依赖容器
        this.modules = []
    }
    // 启动 webpack 打包
    run() {
        // 1 读取入口文件内容
        const filePath = this.options.entry
        // 第一次构建 获取入口文件信息
        const fileInfo = this.build(filePath)
        // 将文件信息放到依赖容器中
        this.modules.push(fileInfo)
        //递归遍历
        for (let i = 0; i < this.modules.length; i++) {
            if (this.modules[i].deps) {
                // 遍历
                for (const relativePath in this.modules[i].deps) {
                    // 取出依赖文件的绝对路径
                    const absolutePath = this.modules[i].deps[relativePath]
                    // 将依赖的文件进行ast deps code处理
                    const fileInfo = this.build(absolutePath)
                    // 将处理后的结果添加到modules中
                    this.modules.push(fileInfo)
                }
            }
        }
        // 将依赖整理好更好的依赖关系图
        /*
            整理成这种形式
            {
                'index.js': {
                    code: 'xxx'
                    deps: { 'add.js': "xxx"}
                },
                'add.js': {
                    code: 'xxx'
                    deps: { }
                }
            }
        */
        const depsGraphs = this.modules.reduce((graph, module) => {
            return {
                ...graph,
                [module.filePath]:{
                    code: module.code,
                    deps: module.deps
                }
            }
        }, {})
    }
    //开始构建
    build(filePath) {
        // 将文件解析Ast语法树
        const ast = getAst(filePath)
        // 获取ast中的所有依赖
        const deps = getDeps(ast, filePath)
        // 将ast解析成浏览器识别的代码 编译代码
        const code = getCode(ast)
        return {
            // 当前模块的文件路劲
            filePath,
            // 当前文件的所有依赖
            deps,
            // 当前文件解析后的所有代码
            code
        }
    }
    //构建资源输出
    gennerate(depsGraphs) {}
}
module.exports = Compiler
复制代码

构建资源输出 bundle

const fs = require('fs')
const path = require('path')
const { getAst, getDeps, getCode } = require('./parser')
class Compiler {
    constructor(options = {}) {
        // weback配置对象
        this.options = options
        // 所有依赖容器
        this.modules = []
    }
    // 启动 webpack 打包
    run() {
        // 1 读取入口文件内容
        const filePath = this.options.entry
        // 第一次构建 获取入口文件信息
        const fileInfo = this.build(filePath)
        // 将文件信息放到依赖容器中
        this.modules.push(fileInfo)
        //递归遍历
        for (let i = 0; i < this.modules.length; i++) {
            if (this.modules[i].deps) {
                // 遍历
                for (const relativePath in this.modules[i].deps) {
                    // 取出依赖文件的绝对路径
                    const absolutePath = this.modules[i].deps[relativePath]
                    // 将依赖的文件进行ast deps code处理
                    const fileInfo = this.build(absolutePath)
                    // 将处理后的结果添加到modules中
                    this.modules.push(fileInfo)
                }
            }
        }
        // 将依赖整理好更好的依赖关系图
        /*
            整理成这种形式
            {
                'index.js': {
                    code: 'xxx'
                    deps: { 'add.js': "xxx"}
                },
                'add.js': {
                    code: 'xxx'
                    deps: { }
                }
            }
        */
        const depsGraphs = this.modules.reduce((graph, module) => {
            return {
                ...graph,
                [module.filePath]: {
                    code: module.code,
                    deps: module.deps
                }
            }
        }, {})
        this.generate(depsGraphs)
    }

    //开始构建
    build(filePath) {
        // 将文件解析Ast语法树
        const ast = getAst(filePath)
        // 获取ast中的所有依赖
        const deps = getDeps(ast, filePath)
        // 将ast解析成浏览器识别的代码 编译代码
        const code = getCode(ast)
        return {
            // 当前模块的文件路劲
            filePath,
            // 当前文件的所有依赖
            deps,
            // 当前文件解析后的所有代码
            code
        }
    }

    //构建资源输出
    generate(depsGraphs) {
        const bundle = `
            (function (depsGraphs) {
                // require目的加载入口文件 
                function require(module){
                    // 定义模块内部的 require函数 内部的 require 会在这个函数里面执行
                   function localRequire(relativePath){
                       // 为了找到当前模块的绝对路径 再通过require函数加载进来
                       return require(depsGraphs[module].deps[relativePath])
                   }
                   // 定义暴露的对象(将来模块暴露的内容)
                   var exports = {}

                   (function (require, exports, code){
                       // 这里执行eval里面的代码会自动执行require 根据作用域会找到localRequire
                        eval(code)
                   })(localRequire, exports, depsGraphs[module].code)
                   // 作为require函数返回值返回出去
                   // 后面的require函数能够得到暴露的内容
                   return exports
                }
                // 加载入口文件
                require('${this.options.entry}')
            })(${JSON.stringify(depsGraphs)})
        `
        // 生成输出文件的绝对路径
        const filePath = path.resolve(this.options.output.path, this.options.output.filename)
        //自动创建文件夹
        try {
            // 检查是否存在dist目录
            fs.statSync(path.resolve(__dirname,'../../dist'))
        } catch (e) {
            // 不存在 则创建
            fs.mkdirSync(path.resolve(__dirname,'../../dist'))
        }
        // 写入文件
        fs.writeFileSync(filePath, bundle, 'utf-8');
    }
}
module.exports = Compiler
复制代码

以上就是 webpack 的全部内容,希望对你们学习webpack能够有所帮助。需要相关代码的可以去gitee获取。戳我获取源码

分类:
前端
标签: