阅读 319

webpack 源码流程

webpack 是一个静态模块打包工具。当webpack处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每一个模块,然后将这些模块打包成一个或多个bundle。

核心的几个options:

  • entry 入口
  • output
  • loader
  • plugin插件
  • mode 打包模式

webpack执行流程参考

整体流程

整合option(config + shell config) --> addEntry --> addModuleChain & addModuleDependcies --> buildModule --> compilation.seal处理每一个chunk,合并、拆分、生成hash --> createAssets & outputAssets --> end

关键事件节点

  • entryOption 在entry 配置项处理过之后,执行
  • watchRun 监听模式下,一个新的编译(compilation)触发之后,执行一个插件
  • build-module ,主要是调用loader处理源文件,生成AST,遍历AST,遇到require\import等依赖,就创建依赖加入到依赖数组中

  • after-compile 这个在seal阶段,对每个module和chunk进行整理,生成编译后的源码,合并,拆分,生成hash。
  • emit事件 生成资源到 output 目录之前。
  • after-emit 生成资源到 output 目录之后。

这里有几个概念需要清晰一下:

  • compiler与compilation对象的区别
    • compiler代表整个webpack从启动到关闭的生命周期
    • compilation对象代表一次新的编译 modules变成chunks的编译过程。(modules 记录了所有解析后的模块; chunks 记录了所有chunk; assets记录了所有要生成的文件)

compilation流程视图:

  • Tapable 类

主要是控制钩子函数的发布与订阅(观察者模式),控制着 webpack 的插件系统,compiler & compilation 都继承自Tapaple 。 Tapable库暴露了很多各种类型的hook

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
复制代码

Hook类的几个关键方法:

  • tap(name, fn):注册回调函数fn,只需当前fn的代码执行完,Hook就会执行下一个fn;
  • tapAsync(name, fn, callback):注册含有异步操作的回调函数,通过调用入参callback()告知Hook当前fn已经执行完毕,可以执行下一个fn;
  • call(...args):按照Hook的类型以对应的规则去执行所有的回调函数;
  • Async:表示在回调函数中支持异步操作,并不是指回调函数的执行规则;
  • Series:表示回调函数的执行规则为串行,同理Parallel表示并行;

所以AsyncSeriesHook,表示在回调函数中支持异步操作,并且所有注册的回调函数按照串行的方式来执行;

compiler 整个过程的事件钩子都是基于tapable 的hook的

class Compiler extends Tapable {
	consturctor(context){
    	super()
        this.hooks = {
            beforeCompile: new AsyncSeriesHook(),
            compile: new SyncHook(['params']),
            afterCompile: xxx,
            enterOption: xxx,
            // ... 定义了很多类型的钩子
        }
    }
}
复制代码

以tapable 的syncHook 简易展示,这里就是一个典型的观察者模式

class SyncHook {
    consturctor(){
    	this.hooks = []
    }
    // 订阅
    tap(name, fn){
    	this.hooks.push(fn)
    }
    // 发布
    call(){
    	this.hooks.forEach(hook => hook(...arguments))
    }
}

use
const hooks1 = new SyncHook()
hooks1.tap('hook1', ()=>{})
hooks1.tap('hook2', ()=>{})

//同步执行
hooks1.call()
复制代码

再参照插件写法,就可以明白compiler.hooks.enterOption.tab了。

不同模式下帮我们,引入的插件

常见问题:

1、打包流程和插件是怎么关联起来的? 流程+tapable。

简述流程+tapable

2、打包优化?

知道了,流程再谈打包优化,就更清晰了

  • 1、缩小文件搜索范围

利用好rules下的exclude,去除不需要babel-loader处理的文件;

{
    rules: [{
        test: /\.js$/,
        use: {
            loader: 'babel-loader'
        },
        // exclude: /node_modules/,
        include: [path.resolve(__dirname, 'src')]
    }]
}
复制代码

对引用的第三方库(jq,chartjs),不进行解析和转换.

  module: {
      //noParse: /jquery|lodash|chartjs/,
      noParse: function(content){
          return /jquery|lodash|chartjs/.test(content)
      }
  }

复制代码

起别名alias ,像 @ @utils

resolve.modules 告诉webpack 查找引用, 减少业务侧的../../xx的使用。比如:path.resolve(__dirname, "src")

利用extensions字段告诉webpack,导入模块,自动带入后缀尝试去匹配对应的文件,减小遍历。 最好在代码中导入文件的时候,要尽量把后缀名带上,避免查找。

  resolve: {
      extensions: ['.js', '.json']
  }
复制代码
  • 2、减少打包文件

1、提前公共代码 webpack 3用的CommonsChunkPlugin,webpack 4用的SplitChunksPlugin

2、利用好tree-shaking,去除无用代码。写法上都用import xx from ,修改babel的preset(默认会将任何模块类型编译成Commonjs)

3、第三方库导成cdn 像导成vue、common、业务js。

4、利用第三方库也找那种ES模块版本的,利于tree-shaking

  • 3、缓存

cache-loader 进行loader的缓存

 {
    test: /\.js/,
    use: [
      {
        loader: 'cache-loader'
      },
      {
        loader: "babel-loader",
      },
    ],
  }
  
{
  test: /\.js/,
  use: [
    {
      loader: "babel-loader",
      options: {
        cacheDirectory: true
      }
    },
  ],
}

复制代码

HardSourceWebpackPlugin也可以为模块提供缓存功能。

plugins: [
  new HardSourceWebpackPlugin()
]

复制代码
  • 4、多进程

thread-loader happypack 实现多进程处理,实际效果不明显。

3、webpack5新增部分?

  • 1、持久缓存、长期缓存,提高构建性能。
  • 2、更好tree-shaking,减小体积。
  • 3、不再为 Node.js 模块 自动引用 Polyfills,减少体积。

4、如何写一个plugin 插件

class Plugin{
	apply(compiler){
    }
}
复制代码
  • webpack 的生命周期节点

5、说的webpack 就会想到vite

vite 原理

  • 当声明一个script标签类型为module时
<script type="module" src="/src/main.js"></script>
复制代码
  • 浏览器就会像服务器发起一个GET
http://localhost:3000/src/main.js请求main.js文件:
// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
复制代码
  • 浏览器请求到了main.js 文件,检测到内部含有import引入的包,又会对其内部import引用发起HTTP请求获取模块的内容文件

GET http://localhost:3000/@modules/vue.js GET http://localhost:3000/src/App.vue

  • vite的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多!

vite 缺点:

  • 生态不如webapck; webpack的loader和plugin非常丰富
  • prod环境的构建,目前采用的rollup; 原因在于esbuild对于css和代码分割不是很友好
  • 还没大规模使用,很多问题或者诉求没有真正暴露出来

vite与webpack的比较

学习链接:

webpack

tapable

mini-pack简化示例

webpack-5-release

文章分类
前端
文章标签