Skip to content
当前页导航

webpack

vite 和 webpack 的区别

  1. 开发模式下的构建速度
  • vite 利用原生 ES 模块加载能力,可以直接启动项目,然后再按需编译(访问哪个页面再编译对应页面),这就导致比 webpack 启动速度快很多。
  • webpack 则是会打包整个项目,然后在启动。
  1. 热更新处理
  • Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块,这大大减少了热更新的时间。
  • 在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。
  1. 生产构建的区别
  • Vite 使用 Rollup 进行打包,不会将所有代码打包成一个 bundle,它保留了第三方依赖的单独引用,以实现更快的加载速度。
  • Webpack 在生产环境下,它将所有代码打包成一个或多个 bundle。
  1. 兼容性
  • vite 兼容性较差,依赖于 ES 模块和浏览器原生支持。
  • webpack 兼容性较好,支持更多浏览器,和一些旧版本浏览器,通过 polyfills 和其他手段来确保兼容性。
  1. 配置及插件生态
  • vite 基本开箱就用,需要配置的东西很少,它目前的生态没有 webpack 好。
  • webpack 配置较为复杂,对新手不太友好,它的生态比较丰富。

说说 webpack proxy 工作原理?为什么能解决跨域?

是利用 webpack-dev-server 在本地开启了一个服务器,利用服务器之间没有跨域的行为,通过访问本地服务器,本地服务器再去访问接口,从而实现的解决跨域问题。

webpack 分割代码

一个单页应用,如果不进行设置,大部分文件都会被打包到同一个 js 文件中,这就会导致文件过于庞大,加载慢。

利用 optimization.splitChunks 则可以根据具体规则进行拆分,提高首屏加载速度。

js
const path = require('path')
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // all 对同步和异步代码都进行分割
      cacheGroups: {
        // libs: { // 最好是不要把 node_modules 里面的包都放在一起,会很大
        //   name: 'libs', // 文件导出名称
        //   test(module) {
        //     // 返回一个布尔值
        //     // `module.resource` 包含磁盘上文件的绝对路径。
        //     // 请注意使用 `path.sep` 而不是 / 或 \,以实现跨平台兼容性。
        //     return (
        //       module.resource &&
        //       module.resource.includes(`${path.sep}node_modules`)
        //     )
        //   },
        //   priority: -10, // 该规则的优先级,数字越大,当符合多个规则的时候,优先级越高,尽量用负数,因为默认是0
        //   minSize: 1, // 当文件大于此大小的时候,进行分割 ,实际开发中一般为 5 * 1024 ,即 5KB
        //   minChunks: 1,  // 最少被引用的次数,满足此条件就会进行分割
        // },
        echarts: {
          name: 'echarts-lib',
          test(module) {
            return (
              module.resource &&
              module.resource.includes(
                `${path.sep}node_modules${path.sep}echarts`
              )
            )
          },
          priority: -8,
          minSize: 1,
          minChunks: 1,
        },
      },
    },
  },
}

vite 分割代码

js
// 方法1  推荐
build: {
  // rollup 配置
  rollupOptions: {
    output: {
      // key自定义 value[] 插件同步package.json名称 或 src/相对路径下的指定文件 (自己可以看manualChunks ts类型)
      manualChunks: {
        // vue vue-router合并打包
        vue: ['vue', 'vue-router'],
        echarts: ['echarts'],
        lodash: ['lodash'],
        // 两个文件合并成一个helloWorld文件
        helloWorld: ['src/components/HelloWorld.vue','src/components/HelloWorld1.vue'],
      }
    }
  }
}

// 方法二
build: {
  // rollup 配置
  rollupOptions: {
    output: {
      manualChunks(id: any): string {
        if (id.includes("style.css")) {
          // 需要单独分割那些资源 就写判断逻辑就行
          return 'src/style.css';
        }
        if (id.includes("HelloWorld.vue")) {
          // 单独分割hello world.vue文件
          return 'src/components/HelloWorld.vue';
        }
        if(id.includes('node_modules/echarts')){
          return 'echarts'
        }
        // 最小化拆分包
        if (id.includes("node_modules")) {
          return id.toString().split("node_modules/")[1].split("/")[0].toString();
        }
      }
    }
  }
}

Loader 和 Plugin 的不同

  1. loader:意思为"加载器"。它的作用是让 webpack 拥有加载和解析非 js 文件的功能。因为 webpack 将一切文件都视为模块,且它本身只提供了解析 js 文件的能力。

webpack 要求 loader 导出一个函数,函数有个参数,为接收的资源文件,并且函数最终返回一个 js 字符串。

js
// markerdown-loader.js  实现一个把md文件转成html的loader

const marked = require('marked')
module.exports = (source) => {
  // source 就是文件内容
  const html = marked(source)
  return `export default ${JSON.stringify(html)}`
}
js
//webpack.config.js
const path = require('path')
module.exports = {
  entry: './src/main.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'index.js',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /.md$/,
        use: {
          loader: './markerdown-loader.js',
        },
      },
    ],
  },
}
  1. plugin:意思为"插件"。它的作用是扩展 webpack 的功能。 webpack 在运行的时候,生命周期会广播出很多事件, plugin 则可以去监听这些事件,改变 webpack 的输出结果。

webpack 要求插件是一个函数或者一个包含 apply 方法的对象。webpack 在启动过后会广播出很多钩子,我们可以通过钩子,添加自己的方法进行处理,下面列举一些常见的钩子函数:

完整的钩子官网:https://www.webpackjs.com/api/compiler-hooks/

生命周期钩子函数,是由   compiler   暴露,可以通过如下方式访问 compiler.hooks.<hooks-name>.tap(...)

js
// sky-plugin.js  清除打包后js文件中的 /******/ 注释
class SkyPlugin {
  // apply 方法在 webpack 启动时会被调用
  apply(compiler) {
    // compiler 包含了此次构建的所有配置信息

    // 监听 emit hooks(生成资源到 output 目录之前),对文件进行处理
    compiler.hooks.emit.tap('SkyPlugin', (compilation) => {
      // compilation 可以理解为此次打包的上下文

      // compilation.assets  打包后生成的所有资源文件
      for (const name in compilation.assets) {
        // name  文件名
        // compilation.assets[name].source() 文件内容
        if (name.endsWith('.js')) {
          // 获取文件内容
          const contents = compilation.assets[name].source() // 对文件内容进行处理,清除文件中的 /******/ 注释
          const afterContents = contents.replace(/\/\*\*+\*\//g, '') // 把处理后的文件内容,覆盖返回的结果
          compilation.assets[name] = {
            source: () => afterContents,
            size: () => afterContents.length,
          }
        }
      }
    })
  }
}

module.exports = SkyPlugin

常用 loader

名称作用
style-loader将 css 添加到 DOM 的内联样式标签 style 里
css-loader允许将 css 文件通过 require 的方式引入,并返回 css 代码
less-loader处理 less
postcss-loader用 postcss 来处理 CSS
autoprefixer-loader处理 CSS3 属性前缀,已被弃用,建议直接使用 postcss
file-loader分发文件到 output 目录并返回相对路径
url-loader和 file-loader 类似,但是当文件小于设定的 limit 时可以返回一个 Data Url
html-minify-loader压缩 HTML
babel-loader用 babel 来转换 ES6 代码

常用 plugin

名称作用
html-webpack-plugin在打包结束后,⾃动生成⼀个 html ⽂文件,并把打包生成的 js 模块引⼊到该 html 中
clean-webpack-plugin删除(清理)构建目录
mini-css-extract-plugin提取 CSS 到一个单独的文件中
DefinePlugin允许在编译时创建配置的全局对象,是一个 webpack 内置的插件,不需要安装
copy-webpack-plugin复制文件或目录到执行区域,如 vue 的打包过程中,如果我们将一些文件放到 public 的目录下,那么这个目录会被复制到 dist 文件夹中
js
// webpack.config.js

const SkyPlugin = require('./sky-plugin')
const path = require('path')

module.exports = {
  entry: './src/main.js',
  mode: 'development',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'app.js',
  },
  module: {
    rules: [
      {
        //文件后缀
        test: /\.js$/,
        use: 'babel-loader', //不需要编译的文件
        exclude: /(node_modules)/,
      },
    ],
  },
  plugins: [new SkyPlugin()],
}

各种 source-map 的区别

sou

开发模式推荐配置:eval-cheap-module-source-map

  • 本地开发首次打包慢点没关系,因为 eval 缓存的原因,rebuild 会很快
  • 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
  • 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module

生产模式不设置,不暴露源码。

webpack 文件的三种 hash

例如我们在基础配置中用到的:filename: "[name][hash:8][ext]"

这里里面 [] 包起来的,就叫占位符,它们都是什么意思呢?请看下面这个表

占位符含义
ext文件后缀名
name文件名
path文件相对路径
folder文件所在文件夹
hash每次构建生成的唯一 hash 值
chunkhash根据 chunk 生成 hash 值
contenthash根据文件内容生成 hash 值
  • hash :任何一个文件改动,整个项目的构建 hash 值都会改变。
  • chunkhash:文件的改动只会影响其所在 chunk 的 hash 值,chunk 是指该文件所有的引用关系。
  • contenthash:每个文件都有单独的 hash 值,文件的改动只会影响自身的 hash 值。 js、css 文件建议使用这个类型

webpack 优化构建速度

  • 使用 resolve.alias 给一些常用的路径起一个别名,来减少 webpack 查找过程,不然通过 ../../形式会查找的慢一点
js
module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
}
  • 配置 thread-loader 开启多线程。小型项目不需要,不然有反效果。而且不需要所有 loader 都开启多线程,仅在耗时的 loader 中开启。
js
// vue.config.js
module.exports = {
  configureWebpack: {
    // ...省略
    module: {
      rules: [
        {
          test: /\.js$/,
          include: path.join(__dirname, 'src'),
          exclude: /node_modules/,
          use: [
            {
              loader: 'thread-loader',
              options: {
                // 指定 worker 线程的数量
                workers: 2,
              },
            },
            'babel-loader',
          ],
        },
      ],
    },
  },
}
  • 开启缓存。vue-cli 已经内置了 cache-loader 和 babel-loader 的缓存方式,不需要再配置这两种,这里我们使用 webpack 5 的 hard-source-webpack-plugin
js
// 介绍 babel-loader 开启缓存
const config = {
  module: {
    rules: [
      {
        test: /\.js$/i,
        include: path.join(__dirname, 'src'),
        exclude: /node_modules/,
        use: [
          // ...
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 启用缓存
            },
          },
        ],
      },
    ],
  },
}

// 介绍 cache-loader 开启缓存,需要先安装 cache-loader
const config = {
  module: {
    rules: [
      {
        test: /\.(s[ac]|c)ss$/i,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'cache-loader', // 获取前面 loader 转换的结果
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
    ],
  },
}
js
// 使用 hard-source-webpack-plugin 首次打包会更慢,后续才会更快
module.exports = {
  configureWebpack: {
    // ...省略
    plugins: [
      new HardSourceWebpackPlugin({
        root: process.cwd(),
        directories: [],
        environmentHash: {
          root: process.cwd(),
          directories: [],
          files: ['package.json', 'yarn.lock'],
        },
      }),
    ],
  },
}

说说 webpack 的构建流程

  • 初始化流程:从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数
  • 编译构建流程:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出流程:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

1

webpack 的热更新原理

  • 通过 webpack-dev-server 创建两个服务器:提供静态资源的服务(express)和 Socket 服务
  • express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • socket server 是一个 websocket 的长连接,双方可以通信
  • 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest 文件)和.js 文件(update chunk)
  • 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新