3、构建工具之Webpack:Webpack核心概念

Webpack 这东西,说实话刚接触时我也觉得头大。但用久了你会发现,它其实就是个「打包机」。你把各种资源扔进去,它给你吐出一个或多个打包后的文件。今天我就带你把这台机器的核心零件拆开看看。

3.1 核心概念:entry / output / loader / plugin

这四个概念,是 Webpack 的骨架。搞懂了它们,你就掌握了 Webpack 的命脉。

entry(入口)

入口就是 Webpack 开始打包的起点。我习惯把入口比作「树根」——从它开始,Webpack 会顺着依赖关系把整棵树捋一遍。

// 单入口配置
module.exports = {
  entry: './src/index.js'
};

// 多入口配置(比如多页面应用)
module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js'
  }
};

嗯,这里要注意:多入口场景下,output 的 filename 要用 [name] 占位符,不然会重名覆盖。

output(输出)

输出就是打包后的文件放哪儿、叫啥名。我在项目中遇到过一个问题:一开始没配 clean: true,每次打包旧文件还在,搞得部署时一堆垃圾文件。后来学乖了。

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    clean: true  // 打包前清空 dist
  }
};

loader(加载器)

Webpack 默认只认识 JS 和 JSON。那 CSS、图片、TypeScript 怎么办?这时候就需要 loader 了。说白了,loader 就是「翻译官」——把 Webpack 不认识的文件转成它能处理的模块。

plugin(插件)

插件能干 loader 干不了的事。比如打包前清空目录、自动生成 HTML、压缩代码等等。我个人的经验是:loader 处理「文件转换」,plugin 处理「流程增强」。

一句话总结:entry 定起点,output 定终点,loader 转格式,plugin 做增强。

3.2 常用 Loader

babel-loader

把 ES6+ 转成 ES5,兼容老浏览器。你想想看,现在谁还用 IE 啊?但项目要求兼容,你就得配。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};

我曾经因为没加 exclude: /node_modules/,打包慢得像蜗牛。后来才发现,第三方库大多已经转译过了,没必要再跑一遍 babel。

css-loader & style-loader

这两个通常是成对出现的。css-loader 解析 CSS 文件中的 @importurl(),style-loader 把 CSS 插入到页面的 <style> 标签里。

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

注意 loader 的执行顺序:从右到左,从下到上。所以 css-loader 要写在右边,style-loader 写在左边。

file-loader

处理图片、字体等静态资源。不过现在 Webpack 5 内置了 Asset Modules,可以替代 file-loader 和 url-loader。

// Webpack 5 推荐用法
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[name].[hash:8][ext]'
        }
      }
    ]
  }
};

3.3 常用 Plugin

HtmlWebpackPlugin

自动生成 HTML 文件,并自动引入打包后的 JS/CSS。我刚开始手动写 HTML 引用路径,每次改文件名都要改 HTML,烦死了。用了这个插件后,彻底解放了。

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: '我的项目',
      inject: true  // 自动注入资源
    })
  ]
};

CleanWebpackPlugin

每次打包前清空 dist 目录。Webpack 5 的 output 配置里可以直接加 clean: true,但如果你用老版本,还是得装这个插件。

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  plugins: [
    new CleanWebpackPlugin()
  ]
};

小提示:Webpack 5 中,直接在 output 里配 clean: true 就行,不用额外装插件。

3.4 开发服务器与热更新

开发时总不能每次改代码都手动打包吧?webpack-dev-server 就是干这个的。它启动一个本地服务器,文件变化时自动刷新页面。

module.exports = {
  devServer: {
    port: 3000,
    hot: true,           // 开启热更新
    open: true,          // 自动打开浏览器
    historyApiFallback: true  // 解决 SPA 路由问题
  }
};

热更新(HMR)比全量刷新快得多。它只替换修改的模块,不刷新整个页面。我在项目中遇到过一个问题:样式热更新没问题,但 JS 状态会丢失。后来发现是没配 module.hot.accept()

if (module.hot) {
  module.hot.accept('./module.js', () => {
    // 模块更新后的回调
    console.log('模块已热更新');
  });
}

3.5 代码分割与懒加载

项目大了,打包文件也大。用户打开页面要下载整个 JS,体验很差。代码分割就是解决这个问题的——把代码拆成多个小块,按需加载。

动态 import

Webpack 遇到 import() 语法,会自动进行代码分割。

// 懒加载示例
button.addEventListener('click', () => {
  import('./module.js').then(module => {
    module.default();
  });
});

SplitChunksPlugin

提取公共代码,比如把 node_modules 里的第三方库单独打包成一个 chunk。

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

注意:代码分割不是越多越好。chunk 太多会导致 HTTP 请求数暴增,反而拖慢加载速度。我一般控制在 4-6 个 chunk 左右。

好了,Webpack 的核心内容就这些。说白了,它就是一套「配置驱动」的构建工具。你给它规则,它帮你干活。下一章我们聊聊 Webpack 的性能优化,那才是真正考验功力的地方。


公众号:蓝海资料掘金营,微信deep3321