likes
comments
collection
share

Webpack从0开始配置开发环境

作者站长头像
站长
· 阅读数 79

前言

这是一篇关于webpack5从0开始配置的入门文章。

通过本文你可以获得:

  • 了解webpack具体的作用
  • 了解如何使用webpack做打包资源
  • 了解如何利用webpack提升开发体验
  • 能看懂官方文档的配置项
  • 能获得一些优化的知识

本文阅读时长大概在15分钟左右,如果你能根据例子来配置的话,可能花费的时间更长一些。

我会尽可能用白话跟你解释所有配置项的由来,能解决什么问题。

同时也希望读者能亲自配一下,加深自己的印象。

话不多说,我们开始。

一、基本概念

1.1 Webpack是什么

Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把前端的各种资源文件(js、css、jpg、png等等)作为各个模块按照特定的规则和顺序组织在一起。

它根据模块的依赖关系进行静态分析,最终打包生成对应的静态资源。

这个过程就叫作模块打包。

官网上的构建图示能很好地说明Webpack的作用

Webpack从0开始配置开发环境

1.2 JS中的模块

所谓模块,就是将特定功能的代码分拆成多个代码片段,每个片段实现一种目的,最终通过接口将它们组合在一起,各个模块协同工作,保证程序的正常运转。

在很长一段时间,JavaScript不像其他程序语言一般,能够使用模块化进行开发。因为这门语言诞生时,仅仅作为轻量级的脚本语言,为用户提供上传表单时的校验功能。

随着业务越来越复杂以及前端技术的发展,引入多个js文件到页面中已经逐渐成为常态,此时也暴露出一些问题:

  • 需要手动维护JavaScript的加载顺序。页面中多个script之间通常存在依赖关系,但由于这种依赖关系是隐性的,当js文件过多时就容易出现问题。
  • 每个script标签都意味着请求一次静态资源,过多的请求会拖慢页面的渲染速度。
  • 每个script标签中,顶级作用域都是全局作用域,如果没有经过处理直接在代码中进行变量或者函数声明,会造成全局作用域的污染。

模块化则一一解决了上述问题:

  • 通过导入和导出语句来分析模块间的依赖关系
  • 使用工具将全部js文件打包成一个或多个文件,减少网络开销
  • 多个模块间的作用域相互隔离,彼此之间不会存在命名冲突

对于模块化,社区提出了AMD、CMD、CommonJS等方案,ES6模块标准则将模块化提升到语言层面。但由于以下原因,ES6标准模块还不能用于实际应用:

  1. 无法使用code splitting和tree shaking
  2. 大部分npm模块采用ComminJS的规则,浏览器不支持其语法
  3. 浏览器兼容性问题等

于是,我们需要使用模块打包工具来帮助我们完成一系列工作。

模块打包工具的任务就是解决模块间的依赖,使其打包后的结果可以运行在浏览器上。

1.3 打包工具做了什么

使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等。

打包工具做以下内容:

  1. 从一个打算放在 HTML 中的 <script type="module"> “主”模块开始。(Webpack默认从src/index.js开始)
  2. 分析它的依赖:它的导入,以及它的导入的导入等。
  3. 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。
  4. 在处理过程中,可能会应用其他转换和优化:
    • 删除无法访问的代码。
    • 删除未使用的导出(“tree-shaking”)。
    • 删除特定于开发的像 consoledebugger 这样的语句。
    • 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
    • 压缩生成的文件(删除空格,用短的名字替换变量等)。

如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>

<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 -->
<script src="bundle.js"></script>

1.4 Webpack的优势

  1. 默认支持多种模块标准,包括AMD、CommonJS、ES6模块等。它会帮我们处理好模块间的依赖关系。

  2. 完备的代码分割(code splitting)解决方案。它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能动态加载。这有助于有效减少资源体积,提升首页渲染速度。

  3. Webpack可以处理各种类型的资源,js、图片、css等等。

  4. Webpack有庞大的社区支持,插件齐全。

  5. webpack可以提高前端开发的体验,webpack-dev-server具备整套的协同开发功能,提高前端开发、调试的效率

1.5 Webpack五个核心概念

  • entry:入口。指定打包工具从哪个文件开始构建内部依赖图,并以此为起点打包

  • output:输出。指定打包好后的bundles资源最终输出到哪个地方,输出名字是什么

  • loader:加载器。让webpack能够处理非js文件的翻译、打包工作。(例如less、image等静态资源)

  • plugin:插件。让webpack能够处理打包优化、压缩、生成模板等功能性任务。

  • mode:模式。development模式、production模式、none。能够设置process.env.NODE_ENV的值,并且根据环境不同自动开启一些插件。

1.6 小结

通过上面的介绍,我们能够大概知道webpack的概念、作用。

下面我们进入实战环节,在实战环节,我们需要做一些准备工作。

二、准备工作

2.1 初始化项目

mkdir webpack-demo-1
cd webpack-demo-1
// 初始化
yarn init -y
// 安装webpack和cli工具
yarn add webpack webpack-cli --dev
// 查看版本
npx webpack -v

为了测试,我们首先在根目录下创建src目录,并创建三个文件(index.html在根目录下)

index.html
src
├── index.css
└── index.js

分别在里面添加内容:

index.css

.div {
  color: red;
  font-size: 16px;
}

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>first webpack app</title>
  </head>
  <body></body>
  <script src="./dist/bundle.js"></script>
</html>

index.js

function createElement() {
  const div = document.createElement('div');
  div.innerHTML = 'hello world';
  div.classList.add('div');
  document.body.append(div);
}
createElement();

2.2 打包第一个js文件

初始化好后,我们可以看到index.html上的<script src="./dist/bundle.js"></script>,目前我们的项目并没有创建dist这个目录,bundle.js也是不存在的。

我们期望能够用webpack打包index.js的内容来生成dist目录和bundle.js文件

此时在命令行上输入:

npx webpack --entry=./src/index.js --output-filename=bundle.js --mode=development

npx webpack是在本地用npx启动webpack的意思

--entry 参数是寻找当前目录下的src/index.js文件

--output-filename 是指定输出的文件名

--mode 指的是开发模式

此时,根目录下应该会多了dist目录,下面有一个bundle.js的文件,它就是打包后的index.js文件。

如果此时用http-server或者vscodelive server插件打开本地项目,你会发现chrome浏览器屏幕前输出现hello world字样,代表打包成功了。

由于目前是从0开始配置,您需要自行下载http-server或者vscode的live-server插件来查看打包后的项目页面。

简单总结,我们刚才的操作是:

Webpackentry指定的入口文件src/index.js为入口点查找模块依赖,此时没有其他依赖。于是通过output输出成bundle.js

最后的参数mode指的是打包模式,一共有三种:development、production、none三种模式。它会自动添加适合于当前模式的一系列配置,减少了人为的工作量。

2.3 配置script

由于使用cli的方式会增加很多指令参数,不容易维护,所以我们需要在package.json中添加脚本,这样就不需要输入那么长的指令了。

package.json中添加命令:

  ...
  "scripts": {
    "build": "webpack --output-filename=bundle.js --mode=development"
  },
  ...

上面的脚本省略掉了entry的配置。

这是因为webpack默认是从工程根目录的src目录下的index.js作为入口文件,打包好后的文件自动放在dist目录。所以我们可以按照默认目录配置来简化我们的命令行。

此时通过yarn build也可以打包。

2.4 配置文件

Webpack提供大量的命令行参数,可以帮助我们满足各种场景的需求。

上面的例子我们已经看到了,我们可以定制入口文件和输出的文件名和指定模式等。

这些命令行参数可以使用下面的命令获取

npx webpack –h

命令中添加更多的参数仅适用于配置较少的项目,如果配置比较多,我们就需要专门的配置文件。

Webpack每次打包时都会读取该配置文件,这样就不必在命令行中添加太多参数了,方便后期修改维护。

默认的配置文件为webpack.config.js,也可以通过命令行参数--config指定配置文件。

  "scripts": {
    "build": "webpack --config build.config.js",
  },

这里就按照之前的命令行参数,在根目录下创建webpack.config.js,并且加入配置项:

// webpack.config.js
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js' },
  mode: 'development',
};

output.filename还是跟先前一样,但是如果要配置output.path——最终资源输出路径则需要绝对路径

默认的配置输出路径相当于

  output: {  path: path.join(__dirname, 'dist') } 

由于是默认配置,所以webpack.config.js中省略了output.path的配置

写好配置项后,我们就可以去除package.json中配置的打包参数了。

  "scripts": {
    "build": "webpack"
  },

执行yarn build后,webpack会预读webpack.config.js中的配置,再进行打包。

当构建后,请使用编辑器打开并在chrome上查看结果。

2.5 小结

准备工作主要是熟悉webpack-cli的使用以及webpack脚本、webpack配置文件的默认项等。

我们还用webpack打包了一个js文件,这甚至不需要任何配置,因为webpack自身就具备打包js的能力。

但是它默认不具备打包其他静态资源(css、sass、file文件等)的能力,所以我们需要给它配置loader,让它可以”翻译“这些静态资源。

在翻译的同时,我们还能够使用plugin让webpack帮助我们做一些额外的工作,例如生成模板文件、压缩、优化等等。

下面进入配置loaderplugin的环节。

三、利用loader、plugin打包资源

3.1 打包css

我们在准备工作中虽然写了css,但是实际打包后并没有css的效果,这是因为webpack在分析依赖时,并没有找到css的引入语句。

我们可以在index.js上引入css

import './index.css';

打包除js文件外的资源需要用到loader,我们先安装两个loader:

yarn add style-loader css-loader --dev

style-loader用于将<style>标签插入到html中,css-loader是用于识别import './index.css'语句并打包css文件。

根据目前的官方网站,配置如下:

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],//注意顺序,webpack从右到左读取loader
      },
    ],
  },
};

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这个示例中,所有以 .css 结尾的文件,都将被提供给 style-loadercss-loader

模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。

上面的顺序是先执行css-loader再执行style-loader

配置完后,请用yarn build构建并在chrome上查看结果,以下不再提示。

3.2 打包less

我们先在src目录下增加一个style.less的文件,内容如下:

@width: 100px;
@height: 100px;
div {
  background-color: aqua;
  width: @width;
  height: @height;
  border: 1px solid red;
  user-select: none;
}

然后在index.js中引入less

import './style.less';

接着安装less和less-loader

yarn add less less-loader --dev

配置:

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

3.3 postcss

postcss是利用JavaScript转换样式的工具。我们可以用它配合autoprefixer来给css添加更多兼容性的前缀代码以支持更多浏览器平台。

首先我们在当前的index.css上加一句css代码:

// index.css
.div {
  color: red;
  font-size: 16px;
  /* 为了查看postcss-loader有没有效果 */
+ user-select: none;
}
yarn add postcss autoprefixer postcss-loader --dev

然后在webpack中配置规则:

  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [require('autoprefixer')],
              },
            },
          },
        ],
      },
    ],
  },

经过打包后,查看打包后的页面,会发现已经被添加了兼容性的前缀代码

Webpack从0开始配置开发环境

postcss中还有很多有关于css加载所需要的插件,都集成到postcss-preset-env的插件中了,比如能够让浏览器支持#12345678这样的八位数颜色以及autoprefixer支持的功能。

我们也可以直接使用这个预设的插件,这样就相当于用了很多个类似autoprefixer这样的小loader

使用方法:

yarn add postcss-preset-env --dev

直接配置在options.postcssOptions.plugins中即可,这里就替换掉上面的require(autoprefixer),因为postcss-preset-env已经拥有它的功能了。

              postcssOptions: {
                plugins: ['postcss-preset-env'],
              }

专用的postcss配置

我们可以使用less、css等来书写css,而postcss则需要体现到所有css上,因此我们需要给所有css预编译工具配置postcss-loader,但这就会增加大量重复的配置代码。

为了解决这个问题,我们可以使用默认的postcss.config.js来给postcss做共同的配置。

在根目录下创建postcss.config.js,内容如下:

// postcss.config.js
module.exports = {
  plugins: ['postcss-preset-env'],
};

然后同步修改webpack.config.js配置

// webpack.config.js
module.exports = {
...
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
      {
        test: /\.less$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'],
      },
    ],
  },
};

设置好后,webpack打包代码时,会从use的最后一项开始(比如less-loader)往前执行loader,当执行到postcss-loader时,会读取postcss.config.js的配置,最后执行css-loaderstyle-loader

3.4 importLoader

css-loader能够支持类似于@import xxx.css之类的css引入。

例如我们在index.css中用这种方法引入的test.css

@import './test.css';
.div {
  color: red;
  font-size: 16px;
  /* 为了查看postcss-loader有没有效果 */
  user-select: none;
}

test.css内容如下:

body {
  background-color: antiquewhite;
  min-height: 100vh;
  user-select: none;
}

但是根据目前配置,当css-loader识别到这个代码时,postcss-loader已经加载过了,就会导致test.css无法获得postcss的支持,所以需要修改css-loader的配置。

  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1, // 期望往回加载的位数,1代表往回1位-也就是postcss-loader加载
            },
          },
          'postcss-loader',
        ], //注意顺序,webpack从右到左读取loader
      },
      {
        test: /\.less$/i,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'],
      },
    ],
  },

css-loader读取到新的css时(此时可能这段新的css没有被postcss-loader处理过),配置了options.importLoaders属性后,会重新往回找options.importLoaders位,再依次往后重新loader一遍。

上面的例子是往回1位找postcss-loader。如果此时postcss-loader前还有other-loader,我们又希望它能够加载,那么可以填2。

3.5 file-loader打包图片

实际开发中,我们会将图片等资源放到一个叫assets的目录下。现在我们在src目录下创建assets目录,然后随便放一张图片进去。

随后在index.js中加入以下代码

import animal from './assets/animal.jpg';
// 或 const animal= require('./assets/animal.jpg')

function createImg() {
  const img = new Image();
  img.src = animal;
  document.body.append(img);
}
createImg();

然后下载

yarn add file-loader --dev

配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1, // 期望往回加载的位数,1代表往回1位-也就是postcss-loader加载
              esModule: false, // 这里需要关闭esModule
            },
          },
          'postcss-loader',
        ], //注意顺序,webpack从右到左读取loader
      },
    // ...省略其他loader
      {
        test: /\.(png|jpe?g|gif)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              esModule: false, //不转为 esModule
              name: '[name].[hash:6].[ext]',//按照name+6位hash+扩展名的规则来命名
              outputPath:'image' //输出目录
            },
          },
        ],
      },
    ],
  },
};

如果不配置options.esModule,则需要使用require().default或者仅使用import xx from 'xxx' 语句。

除此之外,还需要在css-loader处关闭esModule,这是因为类似于下面的代码,会被替换成require语法,替换成require语法后,需要用.default才能正常访问,这是不符合正常开发习惯的。

// index.css
.div {
  background: url('./assets/animal.jpg');/*会被替换成require('./assets/animal.jpg') */
}

请在index.css上加入上面的代码,然后删除css-loader处的esModule:false代码测试一下。

3.6 url-loader打包图片

url-loader包含了file-loader的功能,此外它还可以将图片等资源打包成base64的形式,这样打包后的dist目录下就不会有对应的静态资源了,资源会转化成base64代码储存在打包后的bundle.js中。

好处是减少了静态资源的请求,坏处是静态资源越大,页面显示出来所需要的时间就越长。

我们开发时,一般都将体积小于10kb或者20kb的转成base64。

请在assets目录下分别放置一张20kb以上和一张20kb以下的图片,然后在index.js中引入比较大的图片,在index.css中引入比较小的图片

// index.js
import dp from './assets/dp.png';// 这是比较大的图片
function createImg() {
  const img = new Image();
  img.src = dp;
  document.body.append(img);
}
// index.css
.div {
  background: url('./assets/animal.jpg');/* 这是比较小的图片 */
}

然后下载

yarn add url-loader --dev

将原来的file-loader配置修改成以下:

      {
        test: /\.(png|jpe?g|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              esModule: false, //不转为 esModule
              name: '[name].[hash:6].[ext]', //按照name+6位hash+扩展名的规则来命名
              outputPath: 'image', //输出目录
              limit: 20 * 1024, // 限制20kb以下才打包成base64
            },
          },
        ],
      },

请删除原来的dist目录,然后重新打包,你会看到新生成的dist目录只有一个大的图片被打包了

Webpack从0开始配置开发环境

这是因为我们的配置是将20kb以下的图片转成base64的代码,而20kb以上的依然打包到image目录下。

3.7 asset模块打包静态资源

webpack5内置了asset模块,它包含了file-loaderurl-loader这两个旧模块的功能。

如果我们希望asset模块将所有静态资源以相同的命名规则打包到相同的目录下,则可以在output.assetModuleFilename中配置。

  output: {
    ...
    assetModuleFilename: 'asset/[name].[hash:6][ext]', //asset模块全局配置
  },

由于静态资源的种类较多,包含图片、文件、字体等,所以一般不用全局配置。

如果我们希望有file-loader的功能,可以使用asset/resource

  module: {
    rules: [
    ...
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'assets/[name].[hash:6][ext]',
        },
      },
    ],
  }

如果希望有url-loader的功能,可以使用asset/inline

  module: {
    rules: [
    ...
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset/inline',
      },
    ],
  }

如果希望混用,则直接使用asset

  module: {
    rules: [
    ...
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        generator: {
          filename: 'assets/[name].[hash:6][ext]', //输出规则
        },
        parser: {
          dataUrlCondition: {
            maxSize: 20 * 1024, // 小于20kb则解析成dataUrl
          },
        },
      },
    ],
  }

我在这里直接使用asset混用模式的配置来替换掉上面的url-loader模块。

请删除原来的dist目录再重新打包试一试。由于目前没配plugin,所以只能手动清除原有的dist目录。

3.8 使用plugin在打包前删除dist

注意:webpack5内置了此功能。

在webpack.config.js中设置成output.clean为true即可

 module.exports = {
   ...
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
+    clean: true,
   },
 };

以下为旧方法:

——分割线——

webpack在打包时并不会删除原有的dist目录,而是在其基础上替换内容,所以我们需要手动删除dist目录,下面介绍打包时自动帮我们先删除dist目录的plugin

yarn add clean-webpack-plugin --dev

使用:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
  ...
  },
  mode: 'development',
  module: {
  ...
  },
  plugins: [new CleanWebpackPlugin()],//插件会被当成类来用
};

这里有一点需要注意:我们需要手动设置上output.path

clean-webpack-plugin在执行时,会读取webpack.config.js中的output.path,如果没加上的话,会报错:

clean-webpack-plugin: options.output.path not defined. Plugin disabled...

3.9 打包字体

assets/resource还可以用来打包字体,配置如下:

  module: {
    rules: [
    ...
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash:3][ext]', //输出规则
        },
      },
    ],
  }

3.10 html-webpack-plugin

这个插件用于生成index.html

用它可以指定一个html模板,每次打包后,webpack会用该模板新生成一个index.html,并且自动引入打包好的资源。

插件内置.ejs文件作为模板,我们也可以手动指定自己写好的模板,下面是使用自己创建的模板的示例:

在根目录的public目录下,创建了一个template.html的模板,内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="<%=BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

下载

yarn add html-webpack-plugin --dev

插件配置:

const { DefinePlugin } = require('webpack');//webpack自带的初始化插件
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
  	...
  },
  mode: 'development',
  module: {
    rules: [
    ...
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'my APP',
      template: './public/template.html', //指定index.html模板而不用插件内置的ejs模板
    }),
    new DefinePlugin({
      BASE_URL: '"./"', // 可以替换模板中的<%=BASE_URL %> 为”./“
    }),
  ],
};

执行yarn build后,请用chrome打开dist/index.html,会看到这个模板已经将打包好的资源都引入过来了。

这样就节省了我们手动引入资源到index.html下的繁琐环节了。

到这为止,我们就可以将根目录下的index.html删除掉了。

3.11 babel的使用

为了让浏览器平台直接使用JSXTSes6等代码,在打包前,可以用babel插件转换一下语法。

下载

yarn add @babel/core --dev // babel核心,需要跟各种插件配合
yarn add @babel/preset-env --dev // babel预设的插件集合
yarn add babel-loader --dev // babel的webpack loader

在项目src目录下,创建es6.js,内容如下:

export const a = () => {
  const arr = [1, 2, 3];
  const arr2 = [...arr];
  const [a, ...rest] = arr2;
  console.log(a);
  console.log(rest);
};

然后在index.js中,引入并执行:

import { a } from './es6';
a();

跟postcss一样,babel中包含大量的插件,比较好的实践是我们单独在根目录下创建babel.config.js

// babel.config.js
module.exports = {
  presets: ['@babel/preset-env'],
};

webpack.config.js中配置:

// webpack.config.js 
  module: {
    rules: [
      {
        test: /\.js$/i,
        use: ['babel-loader'],
      },
    ],
  }

构建后打开bundle.js,仔细翻一下,可以看到我们的代码被转译了:

Webpack从0开始配置开发环境

3.12 copy-webpack-plugin

实际项目中,有时候我们并不希望资源被打包,而仅仅是拷贝到dist目录下就行。

比如,开发时会把要打包的静态资源放到src/assets中,一些直接拿来就可以用的静态资源则会放到public目录下,然后build时在public目录下的资源我们希望它们可以不经过loader处理,直接被拷贝至dist目录,这样发布线上后可以直接使用。

这样的需求下,我们可以使用copy-webpack-plugin来帮我们做这件事。

安装:

yarn add copy-webpack-plugin --dev

在根目录下的public内放置一张图片或者其他静态资源,目前我的public目录结构是这样的:

.
├── dp.png
├── template.html

其中dp.png是我需要直接拷贝到dist目录内的静态资源,template.html是模板。

接着在index.js中把原来的createImage函数修改成这样:

function createImg() {
  const img = new Image();
  // 注意,这里不再使用require或者import语句,因为不需要经过打包,直接用拷贝到dist内的资源
  img.src = './dp.png'; 
  document.body.append(img);
}

我们在引用该静态资源时,已经预先知道它会被拷贝到dist目录下了,所以可以直接用./而不是requireimport

实际开发中,我们有时候也会这样用静态资源,很多框架就提供了这样的功能,一些特别大的静态资源(例如地图)等,我们并不需要它们再被打包工具“翻译”一遍,而是直接拷贝到dist目录,在这种情况下,我们就能够直接使用./的形式引入这些静态资源。

接着补充配置

  plugins: [
  ...
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/template.html'], // 忽略public下的template.html
          },
        },
      ],
    }),
  ],

当前public下的template.html会被html-webpack-plugin拷贝到dist目录下并被读写成index.html模板,所以我们在这里就忽略它。

毫无疑问,如果你的项目中不需要被打包的静态资源特别多,那使用这种拷贝的方式能够大大提高上线前构建的速度。

3.13 编译TS

支持TS语法需要安装typescript和执行tsc --init

yarn add typescript @types/react --dev
yarn add ts-loader --dev
tsc --init  // 初始化`tsconfig.json`文件

tsconfig.json文件中开启sourceMap以及开启jsx语法

"compilerOptions": {
  "jsx": "react",
  "sourceMap": true,
  ...
}

最后配置

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.tsx?$/i,
        use: ['ts-loader'],
      },
    ],
  },
};

这里有个小插曲,我们已经在项目中使用了babel,根据官方文档的一篇介绍,我觉得我们可以使用另一个loader

Note that if you're already using babel-loader to transpile your code, you can use @babel/preset-typescript and let Babel handle both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary to ts-loader, the underlying @babel/plugin-transform-typescript plugin does not perform any type checking.

注意如果你已经使用了babel-loader去转译你的代码,你可以使用@babel/preset-typesctiptbabel去处理你的jvascript以及typesctipt代码来替代其他额外的loader。请记住,与 ts-loader 不同,底层的 @babel/plugin-transform-typescript 插件不执行任何类型检查。

这种情况下需要下载@babel/preset-typescript

yarn add @babel/preset-typescript --dev

修改配置:

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.tsx?$/i,
-       use: ['ts-loader'],
+       use: ['babel-loader'],
      },
    ],
  },
};

添加babel的预设

// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
+    '@babel/preset-typescript',
  ],
  plugins: [['react-refresh/babel']],
};

3.14 打包CSV、TSV 和 XML

webpack内置支持json,但如果项目要导入csv、tsv、xml等资源,需要另外安装loader

yarn add csv-loader xml-loader --dev
// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.(csv|tsv)$/i,
        use: ['csv-loader'],
      },
      {
        test: /\.xml$/i,
        use: ['xml-loader'],
      },
    ],
  },
};

3.15 区分生产、打包环境

如何区分生产、打包环境其实官网上已经有详细的介绍了,简单来说,就是将webpack.config.js分成三个文件

  webpack-demo
  |- package.json
  |- package-lock.json
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js

其中dev.js是存放开发环境要用的配置,prod.js存放生产环境要用的配置,common.js则存放两者都要用的配置。

比如,common.js会放置入口、出口、必须的插件、必须的loader等

webpack.common.js

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

 module.exports = {
   entry: {
     app: './src/index.js',
   },
   plugins: [
     new HtmlWebpackPlugin({
       title: 'Production',
     }),
   ],
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
     clean: true,
   },
 };

dev.js则存放开发时要用的devServer之类的配置,mode是一定要写的(也可以通过脚本命令传递模式)

 const { merge } = require('webpack-merge');
 const common = require('./webpack.common.js');

 module.exports = merge(common, {
   mode: 'development',
   devtool: 'inline-source-map',
   devServer: {
     static: './dist',
   },
 });

prod.js则存放生产环境要用的插件之类的

 const { merge } = require('webpack-merge');
 const common = require('./webpack.common.js');

 module.exports = merge(common, {
   mode: 'production',
 });

其中dev.jsprod.js是通过webpack提供的merge函数对common.js的配置进行合并。

最后在package.json中设置脚本命令对应不同的config.js即可

  "scripts": {
    "build": "webpack --config ./webpack.prod.js",
    "dev": "webpack serve --config ./webpack.dev.js"
  },

区分生产、开发环境的难点在于需要根据环境理清每个配置项、插件、loader。

3.16 小结

在这个环节,我们利用webpack替我们打包了一些资源,由于篇幅原因,这里没办法具体讲到所有静态资源。

落实到实际开发也是如此。我们没办法考虑到所有因素,只能在遇到实际问题时再对症下药,去寻找网上提供的方案,再配置webpack帮助我们解决这些问题。

好在很多优秀的模板,例如next.jsumi.js等,得益于这些集成型前端框架的作者,或由于其高超的技术水平,或脱胎于大量实际场景,我们获得了配套的、成熟的webpack配置,开箱即用。

我们可以参考它们的配置来做细小的优化或者更多的扩展。

webpack除了拥有几乎所有类型的静态资源的打包能力,对前端开发来说,更重要的是它带来的不一样的开发体验。这对于从远古时期一路走来的前端er来说,是革命性的创举,同时也真正让前端拥有模块化开发的感受。

下面介绍webpack配套的开发工具。

四、开发工具

4.1 webpack-dev-server

实际开发中,我们都会用到webpack-dev-server这样的静态服务器协助开发,它的好处是能够自动监控文件的修改,而且不用打包就能够直接预览效果。

当前我们的方案是每次修改后执行yarn build打包然后用live-server预览打包后的项目页面。

它有以下缺点:

  • 每次修改后都需要重新将所有的源码编译打包一次
  • 每次编译成功后都需要进行文件读写,性能开销大
  • 不能实现局部刷新

而webpack的webpack-dev-server能够提供热更新、无需打包即可预览的功能,完美解决上述的问题。

首先我们在package.json的script里创建脚本命令:

  "scripts": {
    ...
    "dev": "webpack serve"
  },

然后安装

yarn add -D webpack-dev-server

修改配置文件,告知 dev server,从什么位置查找文件:

 module.exports = {
  ...
+  devServer: {
+    static: './dist',
+  },
 ...
 };

以上配置告知 webpack-dev-server,将 dist 目录下的文件 serve 到 localhost:8080 下。

serve,将资源作为 server 的可访问文件

接着使用命令

yarn dev

webpack-dev-server就启动好了

<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.199.229:8080/

接着我们进入http://localhost:8080/就可以预览效果。

此时可以删除dist目录,并且任意修改各个地方的源代码,修改后保存,webpack-dev-server会监听到文件有修改,于是会实时重新加载。

Webpack-dev-server默认会查找webpack.config.js

如果你的webpack配置文件不是默认的webpack.config.js,假设这里叫wb.config.js那么package.json需要这么配

"scripts": {
 "build": "webpack --config wb.config.js",
 "dev": "webpack serve --config wb.config.js"
},

webpack-dev-server的热更新功能主要是将数据保存在缓存当中,每次启动后,都去缓存中更新数据,这样的好处是提高开发效率,减少文件读写,提升静态服务器的性能。

想要获得热更新功能,我们需要手动开启HMR。

4.2 HMR功能

HMR是hot module replacement的简写,翻译过来就是模块热替换。

它允许在运行时更新所有类型的模块,而无需完全刷新。

当前的开发模式是组件化开发,当我们修改其中一个组件时,启动HMR功能则会让浏览器只对局部发生源代码改变的组件进行更新展示,不需要全部刷新一遍。

webpack5已经内置了这样的功能,我们只需要开启它就行

 module.exports = {
  ...
  devServer: {
    static: './dist',
+   hot: true,
  },
 ...
 };

然后我们需要在入口文件(我这里是src/index.js)内写上需要热更新模块的代码

// src/index.js

/* ...省略其他代码...*/
+if (module.hot) {
+  module.hot.accept(['./es6.js'], function () {
+    console.log('Accepting the updated printMe module!');
+  });
+}

在上面的代码中,数组的每个项是需要热更新文件的路径。当这些文件的源码修改后,就会触发局部更新,然后执行callback

当前我的项目中es6.js文件修改了任何代码都只会局部刷新,并且热替换完成后会打印Accepting the updated printMe module!

4.3 打包React组件jsx

打包jsx的代码依然需要用到babel,我们需要安装以下插件(其中章节【babel的使用】已经用了前三个 ):

yarn add @babel/core --dev // babel核心,需要跟各种插件配合
yarn add @babel/preset-env --dev // babel预设的插件集合
yarn add babel-loader --dev // babel的webpack loader
yarn add @babel/preset-react --dev

接着安装react

yarn add react react-dom

然后在index.js中添加如下代码

import App from './App.jsx';
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';

const rootElement = document.getElementById('root');
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);

接着新建App.jsx,内容如下:

import React from 'react';

const App = () => {
  return <div>hello world</div>;
};

export default App;

下面修改配置:

// webpack.config.js 
  module: {
    rules: [
    ...
-      {
-        test: /\.js$/i,
-        use: ['babel-loader'],
-      },
+      {
+        test: /\.jsx?$/i,
+        use: ['babel-loader'],
+      },
    ],
  }

最后在babel.config.js中添加@babel/preset-react即可

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

4.4 React组件热更新

下面实现React组件热更新的功能

安装

yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh --dev

webpack.config.js中添加插件

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
  	...
  },
  mode: 'development',
  module: {
    rules: [
    ...
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'my APP',
      template: './public/template.html', //指定index.html模板而不用插件内置的ejs模板
    }),
    new DefinePlugin({
      BASE_URL: '"./"', // 可以替换模板中的<%=BASE_URL %> 为”./“
    }),
+   new ReactRefreshWebpackPlugin(),
  ],
};

当前是启动babel插件来loaderReact组件的,我们还需要到babel.config.js中添加插件

// babel.config.js
module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
+ plugins: [['react-refresh/babel']],
};

下面测试一下:

src目录下新建一个Hello.jsx,内容如下:

import React from 'react';

const Hello = () => {
  return (
    <section>
      <input type='text' />
    </section>
  );
};

export default Hello;

然后到App.jsx中使用这个组件

import React from 'react';
+ import Hello from './Hello.jsx';

const App = () => {
  return (
    <div>
      hello231
+     <Hello />
    </div>
  );
};

export default App;

打开localhost:8080,然后在输入框中输入任意内容:

Webpack从0开始配置开发环境

为了测试组件是否支持热更新,我们在App组件中任意修改内容,这里就把hello231修改成hello i am qiuyanxi

按照我们的设想,App组件的修改不会影响到Hello组件,所以原来在输入框中输入的内容依然会存在。

保存后,查看一下页面,成功了

Webpack从0开始配置开发环境

查看chrome控制台提示

[HMR] Updated modules:
[HMR]  - ./src/App.jsx
[HMR] App is up to date.

4.5 Vue组件热更新

Vue组件需要用Vue-loader加载

yarn add vue-loader --dev

相对于React来说,Vue的配置简单很多。Vue组件默认支持HMR功能,因此我们不需要额外配置。

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports={
		...
  module: {
    rules: [
    ...
+      {
+        test: /\.vue$/i,
+        use: ['vue-loader'],
+      },
    ],
  }
  plugins: [
		...
+   new VueLoaderPlugin(),
  ],  
}

4.6 output.publicPath

在打包时,output.publicPath属性影响打包后的index.html内部的引用路径。

当不设置或者设置成空字符串时,打包后的资源会通过origin+/+output.filename来获取资源

举个例子:

当前我的设置如下:

module.exports={
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: '',// 也可以不设置,默认为空字符串
  },
}

那么打包后的脚本被这样引用

<script defer src="bundle.js"></script>

当我们开启http-server去访问本地http://127.0.0.0时,会自动访问到http://127.0.0.0/bundle.js

浏览器会自动帮我们加上/

为了保险起见,我们将output.publicPath设置成/

module.exports={
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
+    publicPath: '/',
  },
}

这样打包后的脚本引用方式就变成这样

<script defer src="/bundle.js"></script>

相当于我们手动加上了/

原来版本不知道是webpack的bug还是浏览器的bug,加上/后可能导致build后的资源无法在本地被访问,在笔者写这篇博客时,已经没有这个问题了。——本地静态服务器用的是http-server

4.7 devServer常用配置

devServer.hot:开启HMR时,我们将其设置成了true,但是更好的做法是设置成only,因为在构建失败时不刷新页面作为回退。举个例子,当你在某个组件上写错代码时,设置成only不会自动重新刷新全部的组件。

devServer.open:设置成true后,启动server服务时,自动打开浏览器

devServer.port:设置端口号

devServer.historyApiFallback:使用 HTML5 History API 时,可能必须提供 index.html 页面来代替任何 404 响应。

  devServer: {
    static: './dist',
    hot: 'only', // 构建失败时不刷新页面作为回退
    open: true, // 自动打开浏览器
    port: 8888, // 端口号
    compress: true, // 自动压缩
    historyApiFallback: true,
  },

比较值得说的是devServer.historyApiFallback,下面我们创建目录src/components,再在里面新建几个两个React组件,如下:

components
├── About.jsx
└── Home.jsx

内容如下:

// Home.jsx
import React from 'react';

const Home = () => {
  return <div>Home</div>;
};

export default Home;
// About.jsx
import React from 'react';

const About = () => {
  return <div>about</div>;
};

export default About;

再修改App.jsx的源代码

import React from 'react';
import Hello from './Hello.jsx';
+ import Home from './components/Home.jsx';
+ import About from './components/About.jsx';
+ import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

const App = () => {
  return (
    <div>
      hello i am qiuyanxi
      <Hello />
+      <BrowserRouter>
+        <Link to='/home'>Home</Link>
+        <br />
+        <Link to='/about'>About</Link>
+        <Routes>
+          <Route path='/home' element={<Home />} />
+          <Route path='about' element={<About />} />
+        </Routes>
+      </BrowserRouter>
    </div>
  );
};

export default App;

现在我们进入页面http://localhost:8888/about,刷新一下。

如果没有配置historyApiFallback,则会提示404。因为此时类似于向后端请求接口,而/about是前端路由,所以报404了。

如果配置了historyApiFallback,则会出现正确的页面。

4.8 proxy设置

在我们开发过程中,请求后端接口时,经常遇到跨域问题。

此时就需要使用代理转发一下请求。

配置如下:

  devServer: {
  	...
+    proxy: {
+      '/api': {
+        target: 'https://api.github.com',
+        pathRewrite: { '^/api': '' }, // 把/api重写为空
+        changeOrigin: true, // 修改主机来源,一般情况下不需要设置
+      },
+    },
  },

举个例子,现在有个现成的github接口:

https://api.github.com/users

我在本地通过设置好上面的代理后,再通过axios访问

axios.get('/api/users').then(res => {
  console.log('res.data:', res.data);
});

如果没有写pathRewrite,就将请求转发到了https://api.github.com/api/users上。

如果不希望传递/api,则需要通过pathRewrite重写路径。重写后转发到https://api.github.com/users

默认情况下,代理时会保留主机头的来源。

https://api.github.com/users这个接口通过判断来源来响应数据,我们可以通过设置changeOrigin: true来绕过github的判断。一般开发时并不需要这样设置

4.9 resolve解析规则

webpack内部有自己的一套解析规则,我们也可以通过resolve设置来修改它。

常见的有

  1. alias:设置别名

        alias: {
          '@': path.resolve(__dirname, 'src'),
        },
    

    设置以上的别名后,可以通过import xxx from '@/xx'来引入根目录下src目录的xx文件。

  2. enforceExtension:是否允许导入时有扩展名。

  3. extensions:尝试按顺序解析后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀。

    举个例子,当前配置如下:

      resolve: {
        extensions: ['.js', '.jsx', '.tsx'],
      },
    

    当我修改如下代码后:

    - import Home from './components/Home.jsx
    // 修改成
    + import Home from './components/Home
    

    Webpack会按照extensions属性自动加上后缀名。如果加上后缀名后依然没有找到文件,就会报错。

  4. mainFiles:解析目录时要使用的文件名。

    假设当前有个components目录,我在某个地方这样import

    import xx from './components'
    

    如果此时resolve设置为mainFiles: ['index'],则会引入components目录下的index文件。

4.10 source map

顾名思义,source map是源代码映射。

如果没有配置source map,那浏览器的报错信息显示将会是打包后的对应位置

比如我在index.js内写这样一段代码

console.log(abc);

在没有声明的情况下,浏览器会报错

Webpack从0开始配置开发环境

此时定位的代码错误显示是在打包后的bundle.js中。这对于程序员来说基本无用。

程序员希望得到的信息是:在源代码中的哪一个文件哪一行出问题了。

于是source map出场了,它可以定位到源代码中的信息。

webpack.config.js中添加配置:

module.exports = {
 ...
  mode: 'development',
+  devtool: 'source-map',
}

此时yarn dev后就可以看到源代码中的错误信息了

Webpack从0开始配置开发环境

它的原理是,在打包后,生成一份map映射文件,它能够体现bundle.js和源代码的映射关系。

你可以通过yarn build后看到它。

五、关于优化

webpack是用来编译、打包的,所以能够优化的无非两种:更快、更小。

如何更快——优化打包速度

打包速度影响到的是开发过程中的热更新速度以及上线前的构建速度。

通过以下方式我们可以优化打包速度:

mode参数

webpack内部对production或者development有做优化,所以针对开发和生产环境我们需要配置不同的mode

resolve配置

通过resolve解析规则,我们可以手动控制webpack的查找规则,除了对开发友好外,相当于显式告诉webpack利用resolve中的配置规则查找文件,合理的配置会提高webpack查找文件的效率。

  • alias设置别名

    通过alisa设置别名可以让webpack通过规则项从上到下查找文件,而不是递归查找。

        alias: {
          '@': path.resolve(process.cwd(), 'src'),
        },
    

    通过上面的别名设置,除了让我们开发时可以通过import xx from '@/xxx' 引用src目录下的内容以外,还对webpack的查找规则非常友好——webpack知道可以src目录从上到下查找文件,而不是通过相对路径递归向上查找文件。

  • extensions高频扩展名前置

    通过设置extensions可以在引入时不写扩展名。

      resolve: {
        extensions: ['.js', '.jsx', '.tsx'],
      },
    

    webpack会从前到后遍历extensions属性来匹配是否有对应扩展名的文件,一些高频的后缀放在前面可以提高webpack搜索的速度

  • modules告诉webpack 解析模块时应该搜索的目录

    const path = require('path');
    
    module.exports = {
      //...
      resolve: {
        modules: [path.resolve(__dirname, 'src'), 'node_modules'],
      },
    };
    

    上面的代码将告诉webpack搜索src目录和node_modules目录,src目录优先搜索。

    这样有助于加快搜索时间

cache属性

module.exports = {
  //...
  cache: {
    type: 'filesystem',
  },
};

通过设置cache属性为文件系统缓存生成的 webpack 模块和 chunk,来改善构建速度。

thread-loader

在耗时的操作中使用此loader可以生成独立的worker池。每个 worker 都是一个独立的 node.js 进程。

相当于开启了多进程来处理耗时慢的loader,这样就达到了多loader同时处理的效果。

下面是官方文档的示例:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          "thread-loader",
          // 耗时的 loader (例如 babel-loader)
        ],
      },
    ],
  },
};

如何更小——缩小打包体积

缩小打包体积的思路有利用一些plugin来缩小代码量,或者利用webpack的Tree-shaking功能来删除没用过的代码。

压缩css

Optimize CSS Assets Webpack Plugin

使用方式参照官方文档

var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  module: {
  ...
  },
  plugins: [
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.optimize\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
  ]
};

压缩bundle

通过webpack内置的optimization属性开启压缩功能。

module.exports = {
  //...
  optimization: {
    minimize: true,
  },
};

Tree Shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

webpack内置这个功能,只需要通过mode:"production"来开启就行。

最后

webpack生态太庞大了,而且变更非常快。

不单单是生态、插件、配置项的变化,包括优化方案变化都非常快。

本篇博客作为入门,只能提供一个构思,尽可能结合实际开发来解释webpack对我们的作用。

如果你详细看完了本篇博客,就可以去啃一啃webpack官方文档的配置说明了。

最后建议大家⭐️使用vite⭐️,手动狗头

新年快乐,我们下期再见👋🏻👋🏻

六、参考来源

webpack-guides

Webpack实战:入门、进阶与调优

推广一下

最后推广一下本人长期维护的 github 博客

1.从学习到总结,记录前端重要知识点,涉及 Javascript 深入、HTTP 协议、数据结构和算法、浏览器原理、ES6、前端技巧等内容。

2.在 README 中有目录可对应查找相关知识点。

如果对您有帮助,欢迎star、follow。

转载自:https://juejin.cn/post/7058244124159246366
评论
请登录