【前端工程化】我写了万字,带你快速上手webpack

大家好,我是一碗周,一个不想被喝(内卷)的前端。如果写的文章有幸可以得到你的青睐,万分有幸~

写在前面

webpack是一个现代的打包模块化的JavaScript工具,在webpack中一切文件皆模块,在内部通过Loader转换文件,通过Plugin注入钩子,最后输出由多个模块组合成的文件。

image.png

上图是webpack官网的截图,可以看出,对于webpack来说,JavaScript、CSS、SCSS、图片等所有文件,全部作为模块来使用,这些模块经过webpack处理后,最终会输出为浏览器可以使用的资源。

webpack的优点如下:

  • 社区庞大且活跃,在前端发展速度如此快的今天,可以很快找到最新的Plugin;
  • 使用场景不限于Web开发;
  • 具有一个良好的开发体验;

webpack初体验

快速上手

webpack属于一个npm工具模块,所以在安装之前,需要初始化npm,然后安装webpackwebpack-cli,命令如下:

npm init -y
npm i webpack webpack-cli -D
复制代码

webpack自4.0以后,0配置也可以进行使用直接使用webpack,这里我们的项目目录结构如下:

.
├── src
│   ├── module.js
│   └── index.js
├── index.html
└── package.json
复制代码

目录中的代码如下:

src/module.js

export default () => {
  // 创建一个DOM元素
  const element = document.createElement('h2')

  element.textContent = 'Hello 一碗周'
  element.addEventListener('click', () => {
    alert('Hello webpack')
  })

  return element
}

复制代码

src/index.js

import createTitle from './module.js'

const title = createTitle()

document.body.append(title)

复制代码

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>webpack快速上手</title>
</head>

<body>
  <script type="module"
          src="./src/index.js"></script>
</body>

</html>
复制代码

然后执行如下命令:

npx webpack
复制代码

最终可以看到根目录下出现了一个dist目录,目录下有个main.js文件,文件内容如下:

(()=>{"use strict";const e=(()=>{const e=document.createElement("h2");return e.textContent="Hello 一碗周",e.addEventListener("click",(()=>{alert("Hello webpack")})),e})();document.body.append(e)})();
复制代码

现在我们可以先改一下index.html中的<script>标签的引用地址,代码如下:

<script type="module"
        src="./dist/main.js"></script>
复制代码

然后可以看一下效果,与之前是否一致。

配置文件

自从webpack4开始,webpack支持0配置的方式,它的默认入口文件是src/index.js,默认出口文件是dist/main.js,如果我们想要自定义这些内容的话,我们需要在根目录下创建一个webpack.config.js文件,来自定义这些配置。

webpack.condfig.js是运行在node下的一个js文件,所以说我们要按照CommonJS的方式来编写这个文件,该文件导出一个对象,所有的配置项作为该对象的属性出现。

示例代码如下:

const path = require('path')
module.exports = {
  // 入口文件
  entry: './src/main.js',
  // 出口配置
  output: {
    // 配置出口文件名
    filename: 'index.js',
    // 文件输出目录,必须是绝对目录
    path: path.join(__dirname, 'output'),
  },
}

复制代码

现在运行webpack,就可以按照我们的配置进行打包。

工作模式

webpack中的工作模式有三种,设置工作模式有两种方式,第一种是通过配置文件中提供的mode选项,另一种在命令行中通过—mode选项,具体的值分别是'none' | 'development' | 'production',三种模式的区别如下:

  • none:不会进行代码的优化操作,可读性最高,打包速度最快
  • production:默认值,该模式会对代码进行压缩
  • development:自动优化打包速度,添加一些调试过程的辅助

资源模块

除了js文件之外,我们还可以通过Loader加载任意类型的的资源模块,这里我们以css资源为例,首先安装这两个Loader,命令如下:

npm i style-loader css-loader -D
复制代码

然后我们在src目录下创建两个CSS文件,并将原本内容进行修改,代码如下:

src/module.js

import './module.css'
export default () => {
  // 创建一个DOM元素
  const element = document.createElement('h2')

  element.textContent = 'Hello 一碗周'
  element.classList.add('title')
  element.addEventListener('click', () => {
    alert('Hello webpack')
  })

  return element
}

复制代码

src/main.js

import createTitle from './module.js'
import './main.css'
const title = createTitle()

document.body.append(title)

复制代码

src/module.css

.title {
  background-color: silver;
  color: #444;
}

复制代码

src/main.css

body {
  margin: 0 auto;
  width: 1200px;
  background-color: antiquewhite;
}

复制代码

然后我们的webpack.config.js配置文件的信息如下:

const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'index.js',
    path: path.join(__dirname, 'dist'),
    // publicPath 用于表示资源的打包后的资源前缀,默认为空
  },
  module: {
    rules: [
      {
        // 模块的匹配规则
        test: /.css$/,
        // 从后往前执行,每一项都是一个loader
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
}

复制代码

现在运行webpack即可将src下的4个资源打包为一个js文件。

Loader是webpack的实现整个前端模块化的核心,通过加载不同的loader,从而可以实现任意类型资源的加载。

url-loader

url-loader可以让指定文件在低于指定大小时,可以让其返回一个DataURL,否则通过file-loader处理文件。

DataURL即前缀为data:协议的URL,其允许内容创建者向文档中嵌入小文件。如下图所示:

DataURL.png

首先我们安装一下必要的npm包,命令如下:

npm i url-loader file-loader -D
复制代码

url-loader一般配合file-loader一起使用。

现在我们通过js的方式引入一个图片,然后添加到页面中,代码如下:

src/main.js

import createTitle from './module.js'
import './main.css'
import logo from './logo.png'
const title = createTitle()
document.body.append(title)
// 在body中插入图片
const img = new Image()
img.src = logo
document.body.append(img)

复制代码

然后我们在配置一下webpack.config.js,代码如下:

const path = require('path')
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'index.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        // 模块的匹配规则
        test: /.css$/,
        // 从后往前执行,每一项都是一个loader
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            // 小于这个值的通过url-loader,转换为DataURL,否则使用file-loader
            limit: 10 * 1024,
          },
        },
      },
    ],
  },
}

复制代码

然后在命令行执行如下命令:

npx webpack
复制代码

即可输出所有内容,然后去运行我们的index.html,即可获得如下内容:

image_1.png

以上就是url-loader的使用方式。

何时使用DataURL,何时单独存在

  • 对于小文件,我们可以使用DataURLs,从而减少请求次数
  • 对于大文件我们可以单独进行存在,提高加载速度

处理ES6+

webpack仅仅是完成打包作用 ,并不会对ES6+的新特性的转换,需要我们通过一些Loader进行转换,其中最常见的就是babel-loader。如果使用babel-loader的话,还需要依赖相关的babel核心模块,安装命令如下:

npm i babel-loader @babel/core @babel/preset-env -D
复制代码

然后webpack.config.js配置信息如下:

module: {
  rules: [
    {
      test: /.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
        },
      },
    },
    {/* more code */},
  ],
},
复制代码

现在我们打包后的代码就是ES5的写法。

webpack加载资源的方式

webpack中除了支持ES Module规范引入资源,他也支持使用CommonJS规范和AMD规范,虽然它支持多种规范,但是除非必要情况,尽量不要混合使用,如何混合使用可能会导致大大降低项目的可维护性。

除了JavaScript提供的这三种方式以外,还有一些独立的Loader提供的加载也会触发资源加载,例如css-loader中,我们使用@import指令和url()函数,都会触发资源加载;还有就是html-loader<img>标签的src属性,也会触发资源加载,除了<img>标签的src属性还有很多属性都是默认支持的。

开发一个Loader

Loader作为webpack的核心机制,实现原理呢也比较简单,现在我们就来开发一款属于自己的Loader。

我们开发的这个Loader,用于将Markdown语法转换为HTML代码,开发步骤如下所示:

  1. 导出一个工作函数,该函数接受一个参数,参数为当前模块的输入;
  2. 安装Markdown解析模块,这里使用marked,安装名npm i marked -D
  3. 最后将处理后的内容进行return,返回的结果最终必须是一段JavaScript代码。 如果我们返回的内容并不是一段JavaScript代码,我们这里有两种方式进行处理,方式一就是自己编写一个将我们的结果处理为JavaScript代码的Loader,另一种就是借助现成的Loader进行处理。

核心代码如下:

markdown-loader.js

const marked = require('marked')

// source 输入的内容,这里为 Markdown 语法
module.exports = source => {
  // 通过 marked 插件进行处理,然后将 HTML 字符串交给下一个 loader 处理
  return marked.parse(source)
}

复制代码

webpack配置文件如下:

module: {
  rules: [
    {
      test: /.md$/,
      // 除了使用文件名,也可以使用路径的方式引入 loader
      use: ['html-loader', './markdown-loader'],
    },
  ],
},
复制代码

配置完成后我们执行npx webpack命令,即可完成打包。

上面的执行流程,流程图如下:

markdown-loader.png

Loader就像一个管道,将输入的内容处理后输出。

webpack插件

插件机制是webpack中除了Loader外的另一个核心特性,它的目的就是增强webpack在项目自动化方面的能力。前面我们了解到Loader就是实现各种资源的加载,从而实现模块化打包;而Plugin就是解决除了资源加载之外的其他一些的自动化的工作;就比如Plugin就可以帮助我们在打包之前清空dist目录。

Loader+Plugin可以帮助我们实现大多前端工程化中的工作,这就给了我们一个webpack就是前端工程化的一个错觉,实则不然。

webpack中常用的插件

首先我们介绍一个webpack中常用的一些插件,并在项目中使用这些插件。

下面我们看一下在webpack.config.js如何使用这三个插件,代码如下:

const path = require('path')
// 引入对应插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'index.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [/* more code */],
  },
  plugins: [
    // 用于清空 dist 目录
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      // 添加 title 属性,在 HTML 文件中通过 htmlWebpackPlugin.options.title 获取,采用 Lodash 模板语法书写
      title: 'Webpack Plugin Sample',
      // 添加一个 meta 元素,name 为 viewport content 为 width=device-width
      meta: {
        viewport: 'width=device-width',
      },
      // 使用哪个 HTML 文件作为模板
      template: 'index.html',
    }),
    // 生成多个页面 创建多个 HtmlWebpackPlugin 实例,用于生成 about.html
    new HtmlWebpackPlugin({
      // filename 默认为 index.html
      filename: 'about.html',
    }),
    // 拷贝不需要打包的内容
    new CopyWebpackPlugin([
      // 'public/**'
      // 直接拷贝 public 下所有内容至输出目录
      'public',
    ]),
  ],
}


复制代码

这里用到的模板如下:

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>Webpack</title>
</head>

<body>
  <div class="container">
    <h1>
      <!-- 使用 Lodash 模板语法可以直接使用 webpack 中的数据 -->
      <!-- htmlWebpackPlugin.options.title 是 htmlWebpackPlugin 插件提供的 -->
      <%= htmlWebpackPlugin.options.title
          %>
    </h1>
  </div>
</body>

</html>
复制代码

这里的的模板语法采用的是Lodash提供的。

执行webpack打包后的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>Webpack</title>
<meta name="viewport" content="width=device-width"></head>

<body>
  <div class="container">
    <h1>
      <!-- 使用 Lodash 模板语法可以直接使用 webpack 中的数据 -->
      <!-- htmlWebpackPlugin.options.title 是 htmlWebpackPlugin 插件提供的 -->
      Webpack Plugin Sample
    </h1>
  </div>
<script type="text/javascript" src="index.js"></script></body>

</html>
复制代码

除了上面介绍的插件外,webpack中还提供了成百上千的插件,我们不需要全部理解,如果有需要我们直接去Github上进行搜索即可。

开发一款插件

相比于Loader,Plugin的拥有更宽的能力范围,其插件机制其实就是通过钩子机制实现的。webpack在工作之中会有很多的环节,在每个环节webpack都埋下了一些钩子,方便我们利用这些钩子去执行某些任务,从而开发一款插件,如下图所示:

webpack-plugin.png

具体有哪些预先定义好的钩子我们可以通过webpack官方文档进行查看,跳转链接点我

webpack要求插件必须是一个函数,或者一个包含apply方法的一个对象。一般情况下,我们会定义一个类,这个类中包含一个apply的方法,然后实例化这个类进行使用。

我们现在编写一个用于清空打包后的.js文件中的/**/那些没有必要的注释的插件,实现代码如下:

myWebpackPlugin.js

module.exports = class myWebpackPlugin {
  apply(compiler) {
    console.log('我的webpack插件开始执行')

    compiler.hooks.emit.tap('myWebpackPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      // 可以通过 compilation.assets 获取所有文件的对象
      for (const name in compilation.assets) {
        // 通过 for...in 语句遍历到所有的文件名
        // name // 当前文件名
        // compilation.assets[name].source() 获取该文件下所有的内容
        if (name.endsWith('.js')) {
          // 如果以 .js 文件结尾,获取内容并删除对应的注释
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          // 重新将内容包装一个对象
          compilation.assets[name] = {
            source: () => withoutComments,
            // 约定包含一个 size 属性
            size: () => withoutComments.length,
          }
        }
      }
    })
  }
}

复制代码

在webpack中引用,代码如下:

webpack.config.js

const path = require('path')
// 引入对应插件
/* more code */
const myWebpackPlugin = require('./myWebpackPlugin')
module.exports = { /* more code */ },
  module: { /* more code */ },
  plugins: [
    /* more code */
    new myWebpackPlugin(),
  ],
}

复制代码

执行webpack打包后就会发现打包好的.js代码中的无用注释就被清空了。

通过上面的那个demo我们了解到,插件就是通过往webpack生命周期中的钩子挂载函数实现的。

开发体验优化

经过上面的学习,我们已经对webpack有了一个初步的认识,但是如果以目前的情况是无法满足我们的开发环境的。一个舒服的开发环境肯定满足以下内容:

  1. 使用HTTP服务运行我们的代码(更加接近生产环境)
  2. 自动编译+自动刷新
  3. 提供SourceMap支持(快速定位错误信息)

自动编译

webpack的自动编译只需要启动Watch模式即可,开始该模式后,webpack就会监听文件的变化,当它们发生修改后,webpack会重新打包编译。

开始Watch模式有两种方式:

  1. 运行webpack时添加一个--watch的参数,示例如下:

    npx webapck --watch
    复制代码
  2. webpack.config.js中添加watch: true的配置,配置如下:

    module.exports = {
      //...
      watch: true,
    };
    Ti
    复制代码

自动刷新浏览器

自动刷新浏览器可以通过browser-sync来完成,该工具使用也比较简单,只需要监听文件改变即可。使用步骤如下:

  1. 首先安装该模块

    npm install -g browser-sync
    复制代码
  2. 启动该命令,并监听对应文件

    browser-sync dist --files "**/*"
    复制代码

使用这种方式的缺点如下:

  • 在操作上更加繁琐,因为我们需要同时启动两个命令;
  • 在执行效率上有所降低,这是因为webpack打包后需要写磁盘,写完之后监听到对应文件变化后,browser-sync需要读磁盘。

由于种种问题,webpack提供了webpack-dev-server工具,接下来我们学习一下该工具。

webpack-dev-server

该工具将自动编译自动刷新浏览器等功能集成在一起,使用也比较简单,安装完成之后会提供一个cli命令,首先我们先需要安装一下这个工具,命令如下:

npm i webpack-dev-server -D
复制代码

安装完成之后直接使用如下命令即可创建一个HTTP服务,并监听文件变化自动打包和刷新浏览器,运行命令如下:

npx webpack-dev-server # 或者 npx webpack server
复制代码

启动之后,默认监听的是8080端口,访问http://127.0.0.1:8080即可访问我们打包后的程序。

值得注意的是,使用webpack-dev-server并不会将我们的打包结果放入磁盘中,它只是临时将打包结果放入我们的内存中,从而加快构建效率,提升我们的开发体验。

关于webpack-dev-server的配置项这里介绍两个:

静态资源访问配置项

首先我们介绍一下关于静态资源访问的配置,该配置项在webpack4和webpack5中有所不同,具体如下:

  • webpack4*中配置项为contentBase,它的参数可以是一个字符串也可以是数组,表示静态文件目录;
  • webpack5*中配置项为static,它的默认目录就是public,可以给他传递一个false,可以将其设置为禁用。

如下代码展示了webpack4*中如何处理静态资源:

webpack.config.js

const path = require('path')
// 引入对应插件

module.exports = {
  /* more code */
  devServer: {
    contentBase: path.join(__dirname, 'public'),
  },
  /* more code */
  plugins: [
    /* more code */
    // 开发环境不需要每次都拷贝静态资源,只有在打包编译上线前才会编译打包
    // new CopyWebpackPlugin([
    //   // 'public/**'
    //   // 直接拷贝 public 下所有内容至输出目录
    //   'public',
    // ]),
  ],
}

复制代码

代理API配置项

目前大多数情况下,开发过程中的前端应用和后端接口是不同源的(什么是同源策略),导致跨域请求失败,出现这种问题的解决方法之一就是服务端接口支持CORS(跨域资源共享),但是并不是所有的API都支持CORS;如果我们前后端同源部署的情况下,后端接口也没有必要开始CORS。

除了上面介绍的那种,解决这个问题最好的办法就是配置代理服务器,将API请求为代理服务器(开发服务器),然后代理服务器在去请求真正的API服务器,如下图所示:

webpack-dev-server支持直接通过配置实现代理服务器,主要是通过devServer.proxy配置项实现。

demo:我们这里将GitHubAPI代理到本地代理服务器,配置信息如下:

const path = require('path')
module.exports = {
  /* more code */
  devServer: {
    contentBase: path.join(__dirname, 'public'),
    proxy: {
      // 凡是以 /api 开头的接口,都会被代理
      '/api': {
        // 例如我们请求 http://localhost:8080/api/users 会被代理到 https://api.github.com/api/users
        target: 'https://api.github.com',
        // 实际上我们的真正请求地址为 https://api.github.com/users 所有我们需要通过 pathRewrite 配置项进行重写路径
        pathRewrite: { '^/api': '' },
        // 默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为。
        changeOrigin: true,
      },
    },
  },
  /* more code */
}

复制代码

现在我们执行如下命令启动项目

npx webpack server
复制代码

访问http://localhost:8080/api/users即可访问到https://api.github.com/users

Source Map

概述

目前生产环境的代码都是经过打包编译之后的代码,和源代码大不相同,但是这样就会出现一个问题,那就是无法快速定位bug,影响开发效率。

Source Map可以帮助我们解决这个问题,简答的说,Source Map就是一个信息文件,里面存储的源代码和编译之后的代码的对应信息,通过Source Map可以快速帮助我们定位。

Source Map的使用比较简单,只需要在转换后的代码尾部添加一行即可

//@ sourceMappingURL=map文件路径
复制代码

打开Source map文件,它大概是这个样子:

{  
 version : 3,  
 file: "test.js",  
 sourceRoot : "",  
 sources: ["testB.js", "testB.js"],  
 names: ["print", "maps", "list", "func"],  
 mappings: "AAgBC,SAAQ,CAAEA"  
}
复制代码

整个文件就是一个JavaScript对象,可以被解释器读取。它主要有以下几个属性:

  • version:Source Map的版本。
  • file:转换后的文件名。
  • sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
  • sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。
  • names:转换前的所有变量名和属性名。
  • mappings:记录位置信息的字符串,它是Base64-VLQ编码的字符串。

webpack配置Source Map

在webpack中配置Source Map比较容易,可以通过devtool配置项开启,示例代码如下:

module.exports = {
  // ...
  devtool: none // 不生成 source map
}
复制代码

webpack中提供的Source Map模式比较多,具体可以参考文档,点我进入

虽然webpack为我们提供了这么多种Source Map模式,但实际开发过程中主要使用以下几个:

  • cheap-module-eval-source-map:适用于开发环境;
  • none:适用于生产环境;
  • hidden-source-map:通常开发模块使用较多;
  • nosources-source-map:适用于生产环境,该模式可以定位报错信息至具体一行,但是不会暴露源代码信息。

HMR

概述

虽然自动刷新浏览器帮助我们解决了一些问题,但是这样的话也存在着一个问题,它不会保存当前页面的状态,webpack提供的HMR就可以解决这个问题,HMR全称是Hot Module Replacement翻译过来就是模块热替换,所谓的模块热替换就是在程序运行的过程中实时的替换某个模块,而应用的运行状态不受影响。

开始HMR

HMR已经集成在webpack-dev-server模块中,也就是说我们使用HMR就无需单独安装了,开启HMR有两种方式。

  1. 通过命令行启动:

    npx webpack-dev-server --hot
    复制代码
  2. 通过配置文件开启

    // 2. 引用 webpack 提供的 HMR插件
    const { HotModuleReplacementPlugin } = require('webpack')
    
    module.exports = {
      /* more code */
      devServer: {
        // 1. 将 devServer.hot 设置为 true
        hot: true,
        proxy: {
          /* more code */
        },
      },
      module: {
        /* more code */
      },
      plugins: [
        /* more code */
        // 3. 使用 HMR 插件
        new HotModuleReplacementPlugin(),
      ],
    }
    
    复制代码

现在我们就可以在项目中体验HMR了,但是webpack需要我们手动的通过HMR API去编写模块热替换逻辑,关于样式文件的热替换,在style-loader中已经帮助我们编写完成,无需我们去编写。

值得注意的是手动HMR API主要用于框架和工具作者。作为最终用户,HMR可能已经在特定于框架为你处理过了。

生产环境优化

webpack提供的HMR、Source Map等功能虽然优化了开发体验,但是如果这些代码用作为生产环境的代码的话,肯定是存在问题的,多了很多冗余代码。

在webpack4中推出了mode的概念,前面也有所介绍,不同的模式会有一些不同的体验,现在我们来学习一下如何为不同的环境创建不同的配置。

创建不同配置

webpack为不同的环境创建不同的配置主要有两种方式,

  • 配置文件根据环境导出不同配置
  • 一个环境对应一个配置文件

接下来我们就看一下这两种方式如何使用。

配置文件根据环境导出不同配置

首先我们来看一下根据环境来导出不同配置如何实现,webpack允许我们导出一个函数,该函数return一个配置对象,这个函数接受两个参数:

  • 第一个表示当前模式
  • 第二个表示命令行中的参数

现在我们就根据介绍的信息来实现一下配置文件,代码如下:

const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = (env, argv) => {
  // env 表示当前模式,可以通过命令行中的--env参数指定
  // argv 命令行中的其他参数
  const config = {
    // 开发环境下的配置
    /* mroe code... */
  }

  if (env === 'production') {
    // 生产环境下的配置,对一些配置进行重写,或者增加些配置
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public']),
    ]
  }
  return config
}

复制代码

此时我们可以通过--env来传递具体参数,比如生产模式,打包如下如下:

npx webpack --env production
复制代码

这种方式比较适合中小型项目。

一个环境对应一个配置文件

使用这种方式通常是创建多个webpack配置文件,并通过--config参数指定具体的配置文件。

一般来说我们创建3个webpack配置文件,分别是:

webpack.common.js  # 公共配置
webpack.dev.js     # 开发配置
webpack.prod.js    # 生产配置
复制代码

根据分类分别进行配置,然后使用webpck-merge模块提供的merge进行配置的合并,安装该模块的命令如下:

npm i webpack-merge -D
复制代码

如下代码展示了生产环境下如何配置:

const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
// 通过 merge 进行配置的合并
module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin(), new CopyWebpackPlugin(['public'])],
})

复制代码

然后使用该配置文件的命令如下:

npx webpack --config webpack.prod.js
复制代码

DefinePlugin

DefinePlugin允许创建一个在编译时可以配置的全局常量。这个插件是webpack本身提供的,它可以帮助我们区别开发环境还是生产环境,用法如下:

const webpack = require('webpack')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  plugins: [
    new webpack.DefinePlugin({
      /**
       * 如果这个值是一个字符串,它会被当作一个代码片段来使用。
       * 如果这个值不是字符串,它会被转化为字符串(包括函数)。
       * 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
       * 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。
       */
      API_BASE_URL: JSON.stringify('https://api.example.com'),
    }),
  ],
}


复制代码

使用如下代码所示:

console.log(API_BASE_URL)
复制代码

Tree-shaking

Tree-shaking是一个专业术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于ES6的模块化。需要注意的是Tree-shaking并不是webpack中的某一个配置选项,是webpack中一组配置选项的搭配起来的效果,这组功能会在production模式下自动开启。

接下来我们看一下如何手动开启Tree-shaking,代码如下:

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中,这个功能称为 Scope Hoisting 这个概念是在 webpack3 中提出的
    concatenateModules: true,
    // 压缩输出结果,删除没有被使用的成员
    minimize: true,
  },
}

复制代码

sideEffects

sideEffects是webpack4中增加的,它允许我们通过配置的方式来标识我们的文件是否具有副作用,所谓的副作用就是指模块执行时除了导出成员之外所做的事情。

该功能也会在production模式下自动开启,或者通过optimization.sideEffects配置项启动,将该配置项设置为true即可。

在webpack中开启后还需要在package.json文件中标记我们的代码时没有副作用的,代码如下:

{
  "sideEffects": false
}
复制代码

如果你的代码确实有一些副作用,那么可以改为提供一个数组,数组的就是我们有副作用的代码路径(支持相对路径和绝对路径):

{
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}
复制代码

代码分包

所谓的代码分包,就是将不同的代码逻辑分别打包到不同的文件。例如我们有一个Vue多页应用,每一页中除了一些公共的组件其余都是没有任何关系的,这里我们就可以将每一个页面打包为一个文件,减少单个文件的大小,避免一个文件过于庞大。

目前我们主要有两种方式实现代码分包,具体如下:

  • 多入口打包
  • 动态导入

接下来我们来具体看一下。

多入口打包

配置多个打包入口也比较简单,这里只需要将entry配置项配置为一个对象,对象中的每一个属性表示一个入口,属性名称则为文件名称。示例代码如下所示:

/* more code ... */
module.exports = {
  /* more code ... */
  entry: {
    // 对象中的每一个属性表示一个入口,属性名表示文件名称
    index: './src/index.js',
    album: './src/album.js',
  },
  output: {
    // [name] 表示 entry 中的属性名称,这里的输出结果最终为两个文件
    filename: '[name].bundle.js',
  },
  /* more code ... */
}


复制代码

这里需要注意的是,默认会在打包的HTML中引入所有的js文件,如果需要制定具体的文件,可以通过如下代码:

new HtmlWebpackPlugin({
  title: 'Multi Entry',
  template: './src/index.html',
  filename: 'index.html',
  // 值为 entry 中的属性名称
  chunks: ['index'],
}),
复制代码

还有就是我们想要抽象出公共模块,可以通过如下配置进行实现:

optimization: {
  splitChunks: {
    // 自动提取所有公共模块到单独 bundle
    chunks: 'all',
  },
},
复制代码

因为客户端是存在缓存机制的,如果我们重新发布了一个版本,但是由于客户端有缓存,就导致我们的代码没有及时更新;为了解决这个问题,我们在可以在为我们的文件名添加Hash,webpack提供了三种Hash,分别是:

  • hash:项目级别的Hash
  • chunkhash:块级别的Hash
  • contenthash:文件级别的Hash,推荐使用

示例代码如下:

/* more code... */
module.exports = {
  /* more code... */
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },
 /* more code... */
}

复制代码

动态导入

动态导入的实现方式也比较简单,只需要在import()方法即可。动态导入的模块会自动分包,这种方式的优点是用到某个模块,在加载模块模块,极大节省了我们的带宽和流量。

我们还可以通过“魔法注释”来实现分包的模块的使用指定的名称,例如

import(/* webpackChunkName: 'components1' */ './posts/posts').then(
({ default: posts }) => {
  mainElement.appendChild(posts())
},
import(/* webpackChunkName: 'components2' */ './album/album').then(
  ({ default: album }) => {
    mainElement.appendChild(album())
  },
)

复制代码

这里就会出现两个打包入口,如果“魔法注释”中的名称一样,则会被打包进同一个页面。

提取CSS到单个文件

最后我们来介绍一个如何将CSS提供到单个文件,这里我们webpack推荐使用mini-css-extract-plugin插件,该插件可以直接提取CSS到单个文件。

如果最终的CSS不会大于150kb,该CSS还是存储在HTML的<style>标签中是比较友好的。

写在最后

随着前端的日益复杂, webpack目前已经成为所有前端开发者必备的一项技能了,在不学一学就真的out了。

猜你喜欢

转载自juejin.im/post/7049160267124441102