【转】Webpack 快速上手(上)

嫌啰嗦想直接看最终的配置请戳这里 webpack-workbench (https://github.com/onlymisaky/webpack-workbench)

由于文章篇幅较长,为了更好的阅读体验,本文分为上、中、下三篇:

  • 上篇介绍了什么是 webpack,为什么需要 webpack,webpack 的文件输入和输出
  • 中篇介绍了 webpack 在输入和输出这段中间所做的事情,也就是 loader 和 plugins
  • 下篇介绍了 webpack 的优化,以及在开发环境和生产环境的不同用法

用两个自问自答来当作序吧:

  • Q:为什么要写这篇文章?

  • A:因为我在将自己的一个项目 AngularJS-ES6 (https://github.com/onlymisaky/AngularJS-ES6) 从 webpack3.x 升级到 4.x 的时候发现,作为一个熟练的 GitHub 搬运工,改起来还是很费力,主要是因为对其没有一个更完整的认知,因此有必要写一篇文章强化认知。

  • Q:既然是写给自己看的,那对于其他人有帮助吗?

  • A:如果你对 webpack 有少许的了解(至少知道webpack是干什么用的),那这篇文章应该还是有帮助的。

Why webpack?

一个工具的诞生,必然有其诞生的原因,也许是为了简化工作,也许是为了解决某些痛点,也可能是今年的kpi压力很大...

今天的主角 webpack 的诞生就是为了解决前端开发长久以来的痛点:模块化 ,这是也它的前辈 gruntgulp 所不具备的功能。

回想一下那个前端还被称作切图仔的时代,我们是怎么组织多个 .js 文件的:

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

且不说这样写有多low,就单从代码维护角度来说,b.js 可能使用了 a.js 中的某个方法; c.js 同样如此,可能还用到了 b.js 中的某些方法。单看每个文件,是根本不知道这些方法是哪来的,也不清楚这三个文件之间的依赖关系的。

为了解决的这个问题,requirejs 诞生了,这是一套 AMD 的模块化实现方案。而此时 node 已经出现有些时日,其遵循的是 CommonJS ,同样是 JavaScript 模块化,却有两套实现方案,语法也不一样。于是又出现了 CMD 和其实现 seajs ,它是为了让服务端模块化和浏览器端模块化的差异能够最小化。

以上这些都是前辈们对 JavaScript 模块化的探索,虽然不是标准,但却推动了标准的发展,于是在 ES6 中,终于有了标准的、原生的模块化方案了,然鹅...

浏览器厂商:标准是标准,至于什么时候实现,fucked say (日后再议)。虽然现在大部分浏览器内核都实现了原生的模块化,但是我们不能确保用户都已将浏览器更新至最新了。

所以,在所有浏览器都实现模块化标准之前,我们还是不能够愉快的使用 importexport ,于是 webpack 来了,给乡亲们带了希望,让乡亲们再也不用看浏览器脸色,从此过上了没羞没臊幸福的生活了。

简单介绍

为了有更好更清晰的认识,建议读者跟着文章一起做一遍,可以先创建一个新的文件夹 learn-webpack ,在该目录中打开命令行,输入 npm init 命令初始化 package.json 文件。

安装

npm i webpack webpack-cli -D

可以全局安装,也可以本地安装,建议本地安装,因为 webpack 不同的版本之间还是有一定的差异,为了避免这个问题,我们选择本地。

在上面安装命令中,除了安装了 webpack 外,还安装了 webpack-cli 。那么这个工具是干什么用的?在 webpack4.x 之后,webpack 把命令行单独提取出来了,也就是说,我们想在命令行中执行 webpack xxx 等命令时,就需要先安装 webpack-cli 。 所以如你的使用的4.x版本的 webpack ,还需要额外安装一下 webpack-cli 。

使用

webpack 的使用还是比较简单的,并且提供了三种使用方法:

  1. 不使用配置文件
webpack <entry> <output>

entry:要打包的文件,可以是一个文件,也可以是一组文件。

output:打包后生成的文件。

例如将 ./src/index.js 打包到 dist/app.js

webpack ./src/index.js dist/app.js
  1. 使用配置文件

不使用配置文件的方式显然不够灵活多变,所以通常都是先编写 webpack 配置文件,然后根据配置文件内容进行打包。在根目录下创建 webpack.config.js 文件,然后在命令行中输入 webpack ,webpack 会自动读取 webpack.config.js 中的配置内容,然后进行打包,下文将会着重介绍如何编写配置文件。

  1. 在node中启动
const webpack = require('webpack');
webpack({
  /* webpack配置内容 */
}, (err, stats) => {
  /* 打包后回调 */
});

npm script

在使用第二种方式的时候,我们也可以将一些配置内容以参数的形式添加在命令后面,比如我们想设置环境为 production ,可以在 webpack.config.js 中将 mode 设置为 production ,也可以在命令后面添加 --mode production

webpack --mode production

如果还需要其他的配置参数,可有继续在后面添加。这样做的好处是可以将一些多变的参数从配置文件中抽离出来,使用起来很灵活。

但是如果参数太多,每次使用的时候又要敲好多命令,可能还会敲错,为了方便管理我们可以将这些命令全部保存在 package.jsonscripts 属性中:

{
  "scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
  }
}

这样就可以通过 npm run build 命令进行打包了。

核心概念

经常看到有人抱怨 webpack 太难太复杂了,“我们万事俱备,就差一个 webpack 配置工程师了”。确实如此,相比于 gulp 简洁的 api ,webpack 确实复杂了许多。

其实仔细的梳理一下,webpack 最重要也就4个核心概念:

  1. entry 入口
  2. output 出口
  3. loader 模块转换器
  4. plugins 插件

除了这四个核心的概念,剩下的那些都是为了优化代码、让我们能有更好的开发体验而设计的。

mode

开头先讲一个 webpack4 中新增的选项:mode。可能是受 parcel 的刺激,webpack4 终于也可以零配置打包了,主要原因是 webpack 终于明白了一个道理:约定大于配置。

model 的值有三种:productiondevelopmentnone ,分别表示不同模式。

在 production 模式下,会默认启用下面这些插件:

  • process.env.NODE_ENV 的值设为 production
  • FlagDependencyUsagePlugin:删除无用代码
  • FlagIncludedChunksPlugin:删除无用代码
  • ModuleConcatenationPlugin:作用域提升
  • NoEmitOnErrorsPlugin:编译出现错误,跳过输出阶段
  • OccurrenceOrderPlugin
  • SideEffectsFlagPlugin
  • UglifyJsPlugin:js代码压缩

在 development 模式下,会默认启用下面这些插件:

  • process.env.NODE_ENV 的值设为 development
  • devtool 设置为 evel
  • NamedChunksPlugin
  • NamedModulesPlugin

entry

既然是模块化开发,就需要有一个入口文件,相关的模块就可以根据这个入口文件形成一个树形的依赖关系。

模块依赖关系

当然 webpack 还没有智能到可以自动识别出你的模块依赖关系,所以需要咱们来告诉它,如果你不告诉它,则会默认把 src/index.js(webpack4.x+) 当做入口文件。

入口文件可以是一个文件(string):

// webpack.config.js
module.exports = {
  entry: 'src/main.js'
}

也可以是多个文件(array):

// webpack.config.js
module.exports = {
  entry: ['src/login.js', 'src/logout.js']
}

甚至也可以是一个对象(object):

// webpack.config.js
module.exports = {
  entry: {
    login: 'src/login.js',
    logout: 'src/logout.js',
  }
}

这三种写法的区别是:

  1. 传入一个文件(string)的时候,会把所有具有依赖关系的模块打包生成一个文件;
  2. 传入多个文件(array)的时候,还是会打包生成一个文件,webpack 会把这些文件合并在一起,但是执行的时候会按照数组内文件的顺序依次执行;
  3. 传入对象的时候,则会根据对象key的个数,打包出对应数量的文件;

很显然,传入对象的方式更复杂,但也更利于扩展,同时也适合用来打包多页应用。

output

有进必有出,webpack 也需要我们指定打包后的文件存放位置,也叫做出口文件,和 entry 一样,output 也有默认值 dist/main.js(webpack4.x+) 。

下面是 output 常见的配置项:

// webpack.config.js
module.exports = {
  output: {
    path: __dirname + '/dist',
    filename: '[name].bundle.js',
    publicPath: '/assets/'
  }
}
  • path

指定打包后的文件存放位置,注意这是一个 绝对路径 !上面的例子中用了 node 内置的常量 __dirname ,该常量表示当前执行文件所在的目录,所以我们打包出的文件就存放在和 webpack 配置文件同级的 dist 目录下面。

  • filename

打包后的文件名称,该选项有5个可配置项:

配置项 作用
[name] 模块名称,对应 entry 中的 key 值,如果 entry 传入的是 string 或 array 默认为 main
[id] 模块id,由 webpack 生成
[hash] 模块的 hash 值,当有文件修改时,这个值就会重新计算并改变
[chunkhash] 这也是一个 hash 值,webpack中每打包生成一个文件,就叫一个chunk ,它是 chunk 本身的 hash ,通常用它来做文件缓存

补充一个小知识,如果 entry 中传入的是对象,且对象的 key 值像这种形式 "a/b" ,并且在 output.filename 中设置了 [name] 那么打包出的文件会存放在 a 文件夹下的 b.js 中(a/b.js)。

  • publicPath

关于这个配置,笔者曾经纠结了好久,知道它的作用,却总是无法理解,在网上看了很多关于 publicPath 的介绍,包括 webpack 的官网,但一直没有豁然开朗的感觉,直到后来在自己的项目中遇到了一些问题,才算是明白了为什么会有这个选项。

如果不想看下面这些内容,可以直接查看 总结 ,建议第一次阅读的时候跳过下面这一小段,等到了 devServer.publicPath 再回过来看一遍。

这里我们可以反向的分析一下。首先,在设置了 path 和 filename 这两个属性之后,便可以确定打包出的文件在本机存放的具体路径了。然后需要明确一点,打包出的代码需要上传到 Web 服务器上,这些文件中可能有 .css .js .png 等等,它们最终都要以 .html 为载体,假设这个文件是 index.html 就像这样:

注意:index.html 是通过 html-webpack-plugin 插件生成的,下文会介绍到。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack</title>

  <link href="index.css" rel="stylesheet">

</head>

<body>
  <img src="hello.png" />

  <script type="text/javascript" src="index.bundle.js"></script>
</body>

</html>

从这段简单的 html 中我们可以得到一个信息,那就是 index.csshello.pngindex.bundle.js 这个三个文件都放在相对于 index.html 的 同一级别下面,就像这样:

dist
├── index.html
├── hello.png
├── index.css
└── index.bundle.js

如果我们的 webpack 设置是下面这样的:

// webpack.config.js
module.exports = {
  entry: {
    app: './src/main.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].bundle.js'
  }
}

那么打包出来的文件结构应该是和上面的一模一样的,我们按照这个结构上传到 Web 服务器,不需要修改什么就可以直接访问了。

一般公司都有专门的 cdn 服务器,那么你可以把 index.csshello.pngindex.bundle.js 这些资源传到 cdn 服务器上,假设你 cdn 地址是 https://mycdn.com 那么你可以通过 https://mycdn.com/index.css 的方式来访问相应的资源。这时候为了使我们的网站不报错,我们就需要将 index.html 中的资源引用方式改为 :

<link href="https://mycdn.com/index.css" rel="stylesheet">

<img src="https://mycdn.com/hello.png" />

<script type="text/javascript" src="https://mycdn.com/index.bundle.js"></script>

很明显,这种打包完成后还需要手动修改的方式很智障,而如果我们不想做这样修改的话,只需要将 output.publicPath 设置为 https://mycdn.com/ ,便会在打包出来的 index.html 文件内自动加上 output.publicPath 设置的值。

还有一种情况,就是笔者所遇到的情况了。假设你的项目还是传统的开发方式,并没有采用前后端分离,用的还是后端模板的方式。而你作为一个前端开发,你想要模块化开发所以引入了 webpack 来打包,后端的哥们跟你说,你把你打包出的文件放在咱们项目的 static 文件夹下面就行了。一开始都没什么问题,但是在做某一个功能的时候,你发现打包出来的文件体积有点大,而且有些代码可以通过按需加载的方式拆分一下,这时候你想到了用 import() 来动态加载,于是除了打包出了 index.js ,还有一些需要动态加载的 .js 文件,你把它们都放进了 static 下面。但是在调试的时候却发现,那些需要按需加载的资源无法加载了,全都是 404 ,咦?怎么回事小老弟!打开控制台看一下,所有 404 的资源地址都是 https://test.com/assets/xxx.js 。干!说好的 static 怎么变成了 /assets/ 了?后端的哥们跟你说,这是后端框架的原因,虽然你是放在 static 下面,但是请求的时候请求的是 相对于当前页面的 /assets/ 这个路径 ,总之 后端没法改,需要前端想办法解决。这个时候,我们只要把 output.publicPath 设置为 /assets/ 就可以解决这个问题了。


总结:

这个选项默认是 '' ,一般情况是不需要修改的。但是在有些情况下,打包出的资源部署上线后,可能会出现 404 访问不到的情况。这个时候就需要配置一下这个选项来解决这个问题了。

如果你将打包后的资源上传到 cdn 上面,那么需要将它设置为可以通过 cdn 方式访问的地址,比如 publicPath: 'https://mycdn.com/assets/'

如果你的项目在服务器上面目录结构和你打包出的文件结构不一样,比如你打包出来的 .html 和 .js 是平级的,但是在服务器上却把 .js 文件都放在 /assets 下面,那你需要设置为 publicPath: /assets/'

所以这个值并不会影响你打包出的文件路径,它只是用来设置在线上运行的时候,所请求的资源相对于 服务 /html页面 的路径

简单的说,在线上运行的时候,所请求的资源具体路径是 https://你的域名/publicPath/资源 或者 https://你设置的cdn地址/资源

output 的常用配置项就这三个,如果你想用 webpack 把你的代码打包成类库,你还需要配置一下 output.libraryoutput.libraryTarget 等,不过笔者建议直接使用 rollup 打包类库。

所以如果有下面这样一份 webpack 配置文件:

// webpack.config.js
module.exports = {
  entry: {
    app: './src/main.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].bundle.js'
  }
}

会打包出如下这些文件:

project
├── src // 源代码文件夹
│   ├── main.js
│   ├── login.js
│   └── logout.js
├── dist // 打包后生成的文件夹
│   └── app.bundle.js
└── webpack.config.js  // webpack 位置文件

总结:本篇介绍了前端模块化的发展历史和如何使用 webpack 打包实现模块化,那么在打包过程中 webpack 是如何多不同的文件进行解析、编译、处理的呢?在写一篇中将会详细的介绍。

posted @ 2019-09-10 17:00  Jingge  阅读(240)  评论(0编辑  收藏  举报