全面掌握webpack

75 阅读9分钟

我们对webpack的掌握依次要做到,能够读懂,能够为一个项目配置webpack,并针对项目实际问题做webpack的配置(使用webpack进行项目优化) 知道webpack的能力,在实际开发中根据能力方向去做实际化的配置

什么是webpack(带来的优势)

webpack本质上是一个打包工具,除了打包代码这个核心功能以外可以通过插件拓展其功能,包括编译兼容,按需加载,代码压缩等来提升开发人员的开发体验。

webpack构成

Entry

确定打包的入口文件,就是作为构建其内部依赖图的开始

Output

打包后文件的输出路径,默认值为 ./dist

loader

webpack本身可以处理的资源有限,需要通过loader对css,html等文件处理做配置,Loader 就是将 Webpack 不认识的内容转化为认识的内容

Plugin

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

dev-serve

Nginx类似,webpack-dev-server也是通过url正则匹配的方式进行url代理配置 可以进行mock操作

webpack的执行流程(设计思路)

webpack的执行流程简单来讲就是根据配置获取入口文件之后,根据相应的依赖关系生成依赖图然后以此为依据进行打包的一个过程。在这过程中,由于浏览器并不认识除 html、js、css 以外的文件格式,所以我们还需要对源文件进行转换 —— Loader 系统。webpack在编译代码的过程中,会触发一系列的钩子事件,插件的行为就是找到相应的钩子,往上面挂载相应的行为,这样在webpack编译代码时注册的事件就会随着钩子的触发执行了 —— Plugin 系统。 以下结合代码了解webpack的设计思路

  1. 搭建结构,读取配置参数
  2. 用配置参数对象初始化 Compiler 对象
  3. 挂载配置文件中的插件
  4. 执行 Compiler 对象的 run 方法开始执行编译
  5. 根据配置文件中的 entry配置项找到所有的入口
  6. 从入口文件出发,调用配置的 loader规则,对各模块进行编译
  7. 找出此模块所依赖的模块,再对依赖模块进行编译
  8. 等所有模块都编译完成后,根据模块之间的依赖关系,组装代码块 chunk
  9. 把各个代码块 chunk 转换成一个一个文件加入到输出列表
  10. 确定好输出内容之后,根据配置的输出路径和文件名,将文件内容写入到文件系统
Compiler基础代码
//Compiler其实是一个类,它是整个编译过程的大管家,而且是单例模式
class Compiler {
//Compiler构造函数
 constructor(webpackOptions) {
  this.options = webpackOptions; //存储配置信息
 //它内部提供了很多钩子分别在打包的各个过程中执行
  this.hooks = {
   run: new SyncHook(), //会在编译刚开始的时候触发此run钩子
   done: new SyncHook(), //会在编译结束的时候触发此done钩子
  };
 }
// run方法
 run(callback) {
  this.hooks.run.call(); //在编译前触发run钩子执行,表示开始启动编译了
  const onCompiled = () => {
    this.hooks.done.call(); //当编译成功后会触发done这个钩子执行   
   };
  this.compile(onCompiled); //开始编译,成功之后调用onCompiled
  }
// compile方法
compile(callback) {
//虽然webpack只有一个Compiler,但是每次编译都会产出一个新的Compilation,
//这里主要是为了考虑到watch模式,它会在启动时先编译一次,然后监听文件变化,如果发生变化会重新开始编译
//每次编译都会产出一个新的Compilation,代表每次的编译结果
 let compilation = new Compilation(this.options);
  compilation.build(callback); //执行compilation的build方法进行编译,编译成功之后执行回调
  }
}
###### pligins基础代码
```js
class WebpackRunPlugin {
 apply(compiler) {
   compiler.hooks.run.tap("WebpackRunPlugin", () => {// 可以控制是同步执行还是异步执行
   console.log("开始编译");
  });
 }
}
//Webpack Plugin 其实就是一个普通的函数,在该函数中需要我们定制一个 apply 方法。当 Webpack 内部进行插件挂载时会执行apply 函数。我们可以在 apply 方法中订阅各种生命周期钩子,当到达对应的时间点时就会执行。
执行流程

以下将代码中的核心模块拆出,整理执行的过程

//第一步:搭建结构,读取配置参数
function webpack(webpackOptions) {
//第二步:用配置参数对象初始化 `Compiler` 对象
const compiler = new Compiler(webpackOptions)
//第三步:挂载配置文件中的插件
  const { plugins } = webpackOptions;
  for (let plugin of plugins) {
    plugin.apply(compiler);
  }
 return compiler;
}
//第四步:执行`Compiler`对象的`run`方法开始执行编译
 run(callback) {
  this.hooks.run.call(); //在编译前触发run钩子执行,表示开始启动编译了
  const onCompiled = () => {
    this.hooks.done.call(); //当编译成功后会触发done这个钩子执行   
   };
  this.compile(onCompiled); //开始编译,成功之后调用onCompiled
  }
//第五步:根据配置文件中的`entry`配置项找到所有的入口(Compilation中的build方法)
 build(callback) {
    let entry = {}
    if (typeof this.options.entry === 'string') {
      entry.main = this.options.entry //如果是单入口,将entry:"xx"变成{main:"xx"},这里需要做兼容
    } else {
      entry = this.options.entry
    }
    //第六步:从入口文件出发,调用配置的 `loader` 规则,对各模块进行编译
    ......// 持续补充
    //编译成功执行callback
    callback()
  }

关于webpack的优化

思路一:针对处理不同类型文件(所有资源),如js文件的编译过程,css文件的压缩过程 思路二:因为项目大小不同,所以有些方面需要根据项目实际情况进行配置,如通过开启多进行加速代码编译速度,这就是自定义配置的意义。 思路三:打包的所有过程都会产生优化的空间 生产模式:需要更小的包体积——需要代码压缩、gzip图片压缩、合理的sourceMap、分割代码分析等优化的配置,大大降低最终项目打包体积 开发模式:需要更快的构建速度——热更新,需要打印debug信息,完整的sourceMap方便定位问题,不要压缩代码

解决兼容性问题

postcss-loader:自动添加 CSS3 部分属性的浏览器前缀做到样式兼容 babel-loader:在开发中我们想使用最新的 Js 特性,但是有些新特性的浏览器支持并不是很好,所以 Js 也需要做兼容处理,常见的就是将 ES6 语法转化为 ES5。

提升开发体验
  • devtool: 'cheap-module-eval-source-map',是一项将编译、打包、压缩后的代码映射回源代码的技术
  • HotModuleReplacementPlugin(+、开发环境):热模块替换(修改某个模块的代码,就只有这个模块进行重新打包编译,其他模块不变提升打包速度——vue-loder会自动实现热模块替换功能)
// webpack.dev.js
//引入webpack
const webpack = require('webpack');
//使用webpack提供的热更新插件
   plugins: [
   new webpack.HotModuleReplacementPlugin()
    ],
    //最后需要在我们的devserver中配置
     devServer: {
      hot: true
    },
提升打包速度
  • one of (+) :module中做loder配置是可以使用one of进行处理使得每种文件只能被一种规则进行处理,无需遍历后续的规则提升打包速度
  • exclude:处理js文件时排除node_modules中间的文件,因为modules本身就是处理过的
  • babel-loader、cache-loader、thread-loader:利用缓存节省打包时间
      // js 规则module中rules配置:
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          'cache-loader', //缓存资源,提高二次构建的速度,使用方法是将`cache-loader`放在比较费时间的loader之前,比如`babel-loader`
          'thread-loader', //多进程打包,可以大大提高构建的速度,使用方法是将`thread-loader`放在比较费时间的loader之前,比如`babel-loader`
          'babel-loader'
        }
      }
  • TerserPlugin(生产模式、插件):开启多进程编译代码,项目大时需要,因为开启进程本身是需要时间的
  • 告诉 webpack 优先 src 目录下查找需要解析的文件,会大大节省查找时间
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

const config = {
  //...
  resolve: {
     modules: [resolve('src'), 'node_modules'],
  },
};
减少代码体积
  • tree-shaking:简单说作用就是:只打包用到的代码,没用到的代码不打包,而webpack5默认开启tree-shaking,当打包的modeproduction时,自动开启tree-shaking进行优化
  • code split(+):分割文件+按需加载;将打包生成的文件分割成不同的部分,需要哪个文件加载哪个文件 防止模块被重复打包,拆分过大的js文件,合并零散的js文件。最终的目的就是减少请求资源的大小和请求次数。

补充

利用了浏览器对于ESM的支持,本质上实现了动态加载,而Webpack 叫做 bundler,是根据依赖将资源打包成一个文件 使用Esbuild对资源进行打包,打包速度更快 vite特点

  • 快速的冷启动: No Bundle + esbuild 预构建
  • 即时的模块热更新: 基于ESMHMR,同时利用浏览器缓存策略提升速度
  • 真正的按需加载: 利用浏览器ESM支持,实现真正的按需加载
  1. 当声明一个 script标签类型为 module 时,如
  <script type="module" src="/src/main.js"></script>
  1. 当浏览器解析资源时,会往当前域名发起一个GET请求main.js文件
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
  1. 请求到了main.js文件,会检测到内部含有import引入的包,又会import 引用发起HTTP请求获取模块的内容文件,如App.vuevue文件

Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,Vite启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多!