webpack 从入门到放弃(一)

8,208 阅读14分钟

什么是 webpack,为什么要使用 webpack

什么是 webpack

官网给出的概念是:

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。 根据官网最直观最出名的那个图我们可以知道,webpack 可以打包/脚本/图片/样式/表 从图中我们可以看出左边有依赖关系的模块(MODULES WITH DEPENDENCIES)通过 webpack 打包成了各种静态资源(STATIC ASSETS)

为什么要使用 webpack

通过上面的概念,你是不是已经大概知道了 webpack 是干什么的,那么问题来了,为什么要使用 webpack 呢? 这就说来话长了,那就长话短说,emmmm

为什么使用 webpack,这应该是和前端的发展有关系的,我认为,webpack 是前端发展到一定阶段的必然产物(貌似是一句废话)。 因为计算机网络的飞速发展,导致 web 前端也在迅猛发展。最初的实践方案已经不能满足我们的需求,于是,越来越多的新技术新思想新框架孕育而生,比如:

前端模块化

随着前端项目的复杂度越来越高,相互之前的依赖越来越多,以及为了更好的复用代码,前端也需要用模块化的思想来组织代码。

首先我们要明白模块化解决了前端的哪些痛点:

  • 命名冲突
  • 文件依赖(js 加载顺序)
  • 代码复用

我们这里说的模块和 Java 的 package 的概念是类似的。逻辑上相关的代码放在一个包中,每一个包都是相互独立的,不用担心命名冲突的问题,如果其他人想要用这部分功能,直接 import 导入包就好

所以前端代码模块化的实现,会帮我们解决命名冲突和代码复用的问题,那么文件依赖要怎么处理呢?这就用到了我们的 webpack,稍后再做介绍。

所以有了模块,我们就可以方便的复用他人的代码,那么问题来了,无规矩不成方圆,我们在使用他人代码的时候肯定是要遵循某种规范,所以就出现了 CommonJS、AMD 和 终极模块化方案 —— ES6 模块,这些都是前端模块化的规范。

我们来简单了解一下:

CommonJS

node.js 采用的就是 CommonJS 规范,使用 require 的方法同步加载依赖,一个文件就是一个模块,导入导出格式如下:

// 导入
const moduleA = require('./moduleA');

// 导出
module.exports = moduleA.someFunc;

缺点是加载的模块是同步的,只有加载完才能执行后面的操作。因为 node.js 的模块文件一般存在于本地硬盘,所以一般不会出现这个问题,但是在浏览器环境该规范就不那么适用了。

AMD

原因如上,因为 CommonJS 不适用于浏览器环境,所以出现了 AMD 规范。该规范异步加载依赖,可以再声明的时候指定需要加载的依赖,并且需要当做参数传入,对于依赖的模块提前执行,依赖前置。

写法如下:

define("module", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });

ES6 模块化

ES6 直接在语言层面上实现了模块化。我们现在用的最多的就是 ES6 模块化的实践方式。

写法如下:

// 导入
import { readFile } from 'fs';
import React from 'react';
// 导出
export function hello() {};
export default {
  // ...
};

样式模块化

现在越来越多人也开始使用模块化的思想写样式。比如现在大部分的 CSS 预编译器都支持 @import 的写法。将一些公用样式放在一个文件中,在其他文件中导入。


三大框架的出现,使我们不需要像传统的 JQ 一样操作 DOM,将精力集中在对数据的处理上。

以及 ES6/7/8 和 TS 的使用越来普及,无疑使我们的开发效率提高了很多。

但是出现的问题是:这些新兴的技术并不是在所有的浏览器上都适用,都需要将源代码转化为可以直接在浏览器上运行的代码

所以,webpack 就解决了这个问题。

Gulp/Grunt、Rollup 和 webpack 的比较

Gulp/Grunt

其实,Gulp/Gunt 和 webpack 应该是没有可比性的,但是他们都可以称为前端自动化构建工具(让我们不再做机械重复的事情,解放我们的双手)

但是 Gulp/Gunt 和 webpack 确实干的不是一件事

Gulp 本质是 task runner,Webpack 是 module bundler

我认为 Gulp 正如他的定义一样:基于流的自动化构建工具,定义每一个任务,然后自动将一个个任务执行。

而 webpack 是模块化地组织,模块化地依赖,然后模块化地打包。相对来上,场景局限在前端模块化打包上。

推荐知乎 寸志 大佬的回答:gulp 有哪些功能是 webpack 不能替代的?

Rollup

Rollup 是一个和 Webpack 很类似但专注于 ES6 的模块打包工具。 Rollup 的亮点在于能针对 ES6 源码进行 Tree Shaking 以去除那些已被定义但没被使用的代码,以及 Scope Hoisting 以减小输出文件大小提升运行性能。 然而 Rollup 的这些亮点随后就被 Webpack 模仿和实现。 由于 Rollup 的使用和 Webpack 差不多,这里就不详细介绍如何使用了,而是详细说明它们的差别:

Rollup 是在 Webpack 流行后出现的替代品; Rollup 生态链还不完善,体验不如 Webpack; Rollup 功能不如 Webpack 完善,但其配置和使用更加简单; Rollup 不支持 Code Spliting,但好处是打包出来的代码中没有 Webpack 那段模块的加载、执行和缓存的代码。 Rollup 在用于打包 JavaScript 库时比 Webpack 更加有优势,因为其打包出来的代码更小更快。 但功能不够完善,很多场景都找不到现成的解决方案。

安装与使用

创建 package.json 文件

可以手动的创建,也可以使用命令自动创建

npm init

安装

webpack 可以直接使用 npm 安装,可以安装到全局,也可以安装到项目

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

使用

推荐大家阅读这篇文章: 入门 Webpack,看这篇就够了

我就是跟着这篇文章做的

新建文件

首先新建一个文件夹,然后在终端打开该文件夹并执行来初始化你的项目,在初始过程中,会有一些问题帮助你创建 package.json 文件。

npm init

初始化之后我们还要创建几个文件来存放我们的项目文件。

创建一个 app 文件夹来存放我们打包之前的源文件

创建一个 public 文件夹来存放一个入口文件 index.html 和通过 webpack 打包之后浏览器可直接运行的 js 文件

比如我们在 app 文件夹下创建一个 Greeter.js 文件,里面有一个方法可以再页面显示文字信息 Hi there and greetings!

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

然后我们创建一个 main.js 来引入 Greeter.js 这个文件,

浏览器的入口 index.html 文件内容如下(其中 bundle.js 是打包之后的文件):

<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

此时的文件路径如下

.
├── app
│   ├── Greeter.js
│   └── main.js
├── package.json
├── public
│   ├── bundle.js
│   └── index.html

安装 webpack

初始化项目之后,要安装 webpack

npm install --save-dev webpack

在安装完 webpack 之后,可以再 package.json 文件中看到增加了 webpack 的依赖。

配置 webpack

安装完 webpack 之后,我们就要配置 webpack 了,首先创建配置文件 webpack.config.js 文件内容如下:

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}

然后我们在该项目的终端输入

webpack

就可以看到如下信息:

Hash: 4e6a6b5eb88a83b29e02
Version: webpack 4.12.0
Time: 551ms
Built at: 2018-06-24 14:53:39
    Asset      Size  Chunks             Chunk Names
bundle.js  6.82 KiB       0  [emitted]  main
[3] ./node_modules/css-loader!./app/main.css 190 bytes {0} [built]
[4] ./app/main.css 1.04 KiB {0} [built]
[5] ./app/Greeter.js 143 bytes {0} [built]
[6] ./app/main.js 119 bytes {0} [built]
    + 3 hidden modules

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

看到这样的信息,那么恭喜你,你的第一个 webpack 项目就完成了!

打开 public 文件夹下面的 index.html,你就可以再浏览器上看到如下的效果。

Loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

因为 webpack 本身只能处理 JavaScript,如果要处理其他类型的文件,就需要使用 loader 进行转换,loader 本身就是一个函数,接受源文件为参数,返回转换的结果。

举个 🌰 —— css-loader

例如,我们想在刚刚的页面增加样式,使文字居中显示,那么我在 app 文件夹下面新建一个 main.css 文件。在 webpack 中,所有的文件都是模块,所以要使用这个 css 文件,就必须要先引入。

引入 css 文件

所以我就在 app 文件夹下面的 main.js 中引入该 css 文件

const greeter = require('./Greeter.js');
require ('./main.css')
document.querySelector("#root").appendChild(greeter());

重新打包

然后我重新打包一遍,蓝后,发现居然报错了!

Hash: 179c18498fac6de89a96
Version: webpack 4.12.0
Time: 533ms
Built at: 2018-06-24 15:00:24
 1 asset
[0] ./app/Greeter.js 143 bytes {0} [built]
[1] ./app/main.js 119 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

ERROR in ./app/main.js
Module not found: Error: Can't resolve 'style-loader' in '/Users/cherry/Workspace/webpack-demo'
 @ ./app/main.js 2:0-22

根据报错信息,我们很明显能发现是提示我们项目缺少 style-loader,这是因为 webpack 原生只支持解析 js 文件,要支持非 js 类型的文件,就需要使用 loader

安装 loader

所以我们要安装 style-loadercss-loader

npm i -D style-loader css-loader

修改配置文件

然后修改 webpack 的配置文件 webpack.config.js

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
}

配置文件增加了 module.rules 数组,该数组是一些配置规则,告诉 webpack 符合 test 的文件需要使用 use 后面的 loader 处理。所以该规则就是对所有 .css 结尾的文件使用 style-loadercss-loader 进行处理。

loader 特性

我们来看一下 loader 有哪些特性:

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

使用 webpack 的三种姿势

webpack 中使用 loader 有三种姿势

通过 CLI

命令行中运行

webpack --module-bind jade  --module-bind 'css=style!css'   

jade,style,css后面可省略-loader,他们分别对.jade使用jade-loader,对.css使用style-loader和css-loader

通过require

可以直接在源码中指定使用什么 loader 去处理文件。

require('style-loader!css-loader?minimize!./main.css')

这样就是对 ./main.css 文件先使用 css-loader 再使用 style-loader 进行转换

使用配置文件 webpack.config.js

最常用的方式就是使用本文所使用的配置文件的方式

常见的 loader

常见的 loader

loader name loader des
babel-loader 加载 ES2015+ 代码,然后使用 Babel 转译为 ES5
buble-loader 使用 Bublé 加载 ES2015+ 代码,并且将代码转译为 ES5
cache-loader 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里。
coffee-loader CoffeeScript 转化为 JS
css-loader css-loader 是将 @importurl() 引入的 css 转换为 import/require() 的方式然后在解析他们
exports-loader 通过添加 exports[...] = ... 语句导出文件中的变量。
expose-loader expose-loader 将模块添加到全局对象上
file-loader file-loader 可以解析项目中的url引入(不仅限于css),根据我们的配置,

将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。 gzip-loader | 可以加载 gzip 压缩之后的资源 html-loader | 将 html 输出为字符串,也可以根据配置进行压缩 imports-loader | imports-loader 允许使用依赖于特定全局变量的模块,这对于依赖于像 $ 这样的全局变量的第三方模块非常有用 jshint-loader | 为加载的模块使用 jshint json-loader | 由于 webpack >= v2.0.0 默认支持导入 JSON 文件。如果你使用自定义文件扩展名,你可能仍然需要使用此 loader json5-loader | 将 json5 文件解析成 js 对象 less-loader | 将 less 转化为 css null-loader | 返回一个空模块 postcss-loader | 将 postcss 转化为 css raw-loader | 加载文件原始内容(utf-8格式) sass-loader | 将 SASS/SCSS 转换为 css source-map-loader | 从现有源文件(源代码源URL)中提取源映射,方便调试 style-loader | 将 CSS 放在 <style> 标签中注入到 DOM 中 script-loader | 在全局上下文中执行一次 JavaScript 文件(如在 script 标签),不需要解析 svg-inline-loader | 将 SVG 作为模块嵌入 url-loader | 将文件加载为 Base64 编码的 URL

row 2 col 1 | row 2 col 2

Plugin

插件(plugin)是 webpack 的支柱功能。webpack 自身也是构建于,你在 webpack 配置中用到的相同的插件系统之上! 插件目的在于解决 loader 无法实现的其他事

Plugin 是用来扩展 Webpack 功能的,通过在构建流程里注入钩子实现,它给 Webpack 带来了很大的灵活性。

通过plugin(插件)webpack可以实 loader 所不能完成的复杂功能,使用 plugin 丰富的自定义 API 以及生命周期事件,可以控制 webpack 打包流程的每个环节,实现对 webpack 的自定义功能扩展。

举个 🌰 —— ExtractTextPlugin

webpack4 已经不支持用extract-text-webpack-plugin来优化 css, 需要改成optimize-css-assets-webpack-plugin和mini-css-extract-plugin

在刚刚的例子中,我们查看打包之后的 index.html 文件可以看到我们刚刚写的 css 代码放在了 head 中的 style 标签中,这是 style-loader 帮我们处理的

但是,如果你希望打包之后 css 在单独的文件中,那么你就需要 ExtractTextPlugin 这个 plugin 了。

安装 ExtractTextPlugin

npm i -D ExtractTextPlugin

修改配置文件

我们需要修改配置文件:

const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js",//打包后输出文件的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 转换 .css 需要的 loader
        loaders: ExtractTextPlugin.extract({
          use: ['css-loader'],
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: '[name]-[contenthash:8].css'
    })
  ]
}

然后我们再重新打包,就可以发现在 public 文件夹下面多了一个 main-493a2c3c.css 文件,下面我们要在 index.html 中自动引入这个 css 文件

html-webpack-plugin

html-webpack-plugin 可以根据你设置的模板,在每次运行后生成对应的模板文件,同时所依赖的 CSS/JS 也都会被引入,如果 CSS/JS 中含有 hash 值,则 html-webpack-plugin 生成的模板文件也会引入正确版本的 CSS/JS 文件。

安装 html-webpack-plugin

安装方式我们已经很熟悉了

npm i -D html-webpack-plugin

修改配置文件

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

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js",//打包后输出文件的文件名
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        loaders: ExtractTextPlugin.extract({
          use: ['css-loader'],
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: '[name]-[contenthash:8].css'
    }),
    new HtmlWebpackPlugin(),
  ]
}

蓝后我们删除之前我们在 punlic 文件夹下面 index.html 的内容,在打包一次,就会生成一个 html 模板

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  <link href="main-493a2c3c.css" rel="stylesheet"></head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

new HtmlWebpackPlugin 的时候,我们可以进行一系列的配置

    new HtmlWebpackPlugin({  
      // 生成的HTML模板的title,如果模板中有设置title的名字,则会忽略这里的设置
      title: "This is the webpack config", 
      
      // 生成的模板文件的名字
      filename: "/index.html", 
      
      // 模板来源文件 
      template: "index.html", 
      
      // 引入模块的注入位置;取值有true/false/body/head 
      // true 默认值,script标签位于html文件的 body 底部
      // body script标签位于html文件的 body 底部
      // head script标签位于html文件的 head中
      // false 不插入生成的js文件,这个几乎不会用到的
      inject: "body", 
      
      // 指定页面图标; 
      favicon: "", 
      
      // 是html-webpack-plugin中集成的 html-minifier ,生成模板文件压缩配置 
      minify: {  
          caseSensitive: false, //是否大小写敏感  
          collapseBooleanAttributes: true, // 省略布尔属性的值  
          collapseWhitespace: true //删除空格 
      }, 
      
      // 是否生成hash添加在引入文件地址的末尾,类似于我们常用的时间戳,避免缓存带来的麻烦
      hash: true,
      
      // 是否需要缓存,如果填写true,则文件只有在改变时才会重新生成
      cache: true,  
      
      // 是否将错误信息写在页面里,默认true,出现错误信息则会包裹在一个pre标签内添加到页面上
      
      showErrors: true,  
      
      // 引入的模块,这里指定的是entry中设置多个js时,在这里指定引入的js,如果不设置则默认全部引入
      chunks: "",  
      
      // 引入模块的排序方式
      // 默认四个选项: none auto dependency {function}
      // 'dependency' 文件的依赖关系
      // 'auto' 默认值,插件的内置的排序方式
      // 'none' 无序
      // {function} 自定义
      chunksSortMode: "auto",  
      
      // 排除的模块
      excludeChunks: "",  
      
      // 生成的模板文档中标签是否自动关闭,针对xhtml的语法,会要求标签都关闭,默认false
      xhtml: false  
    
    }),

常见的 plugin

摘自:http://www.css88.com/doc/webpack/plugins/

Name Description
AggressiveSplittingPlugin 将原来的 chunk 分成更小的 chunk
BabiliWebpackPlugin 基于 Babel 的裁剪工具:Babili
BannerPlugin 在每个生成的 chunk 顶部添加 banner
CommonsChunkPlugin 提取 chunks 之间共享的通用模块
ComponentWebpackPlugin 通过 webpack 使用组件
CompressionWebpackPlugin 预先准备的资源压缩版本,使用 Content-Encoding 提供访问服务
ContextReplacementPlugin 重写 require 表达式的推断上下文
DefinePlugin 允许在编译时(compile time)配置的全局常量
DllPlugin 为了极大减少构建时间,进行分离打包
EnvironmentPlugin DefinePlugin 中 process.env 键的简写方式。
ExtractTextWebpackPlugin 从 bundle 中提取文本(CSS)到单独的文件
HotModuleReplacementPlugin 启用模块热替换(Enable Hot Module Replacement - HMR)
HtmlWebpackPlugin 简单创建 HTML 文件,用于服务器访问
I18nWebpackPlugin 为 bundle 增加国际化支持
IgnorePlugin 从 bundle 中排除某些模块
LimitChunkCountPlugin 设置 chunk 的最小/最大限制,以微调和控制 chunk
LoaderOptionsPlugin 用于从 webpack 1 迁移到 webpack 2
MinChunkSizePlugin 确保 chunk 大小超过指定限制
NoEmitOnErrorsPlugin 在输出阶段时,遇到编译错误跳过
NormalModuleReplacementPlugin 替换与正则表达式匹配的资源
NpmInstallWebpackPlugin 在开发时自动安装缺少的依赖
ProvidePlugin 不必通过 import/require 使用模块
SourceMapDevToolPlugin 对 source map 进行更细粒度的控制
UglifyjsWebpackPlugin 可以控制项目中 UglifyJS 的版本
ZopfliWebpackPlugin 通过 node-zopfli 将资源预先压缩的版本

参考文章: