webpack 笔记 指南篇(一)

webpack 用于编译 JavaScript 模块。一旦完成安装,你就可以通过 webpack 的 CLI 或 API 与其配合交互。

一、安装

要安装最新版本或特定版本,请运行以下命令之一:

$ npm install --save-dev webpack
$ npm install --save-dev webpack@

如果你使用 webpack 4+ 版本,你还需要安装 CLI。

$ npm install --save-dev webpack-cli

全局安装
以下的 NPM 安装方式,将使 webpack 在全局环境下可用:

$ npm install --global webpack

不推荐全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中,可能会导致构建失败。

二、起步

1)创建一个本地页面
1、首先我们创建一个目录,初始化 npm,然后 在本地安装 webpack,接着安装 webpack-cli(此工具用于在命令行中运行 webpack):

$ mkdir webpack-demo && cd webpack-demo      //创建目录
$ npm init -y                                //创建package.json文件
$ npm install webpack webpack-cli --save-dev //安装 webpack-cli

2、现在我们将创建以下目录结构、文件和内容:

  webpack-demo
  |- package.json
+ |- /dist
+   |- index.html
+ |- /src
+   |- index.js
  • “源”代码(/src)是用于书写和编辑的代码。
  • “分发”代码(/dist)是构建后代码最小化和优化后的“输出”目录
  • 要在 index.js 中打包 lodash 依赖,我们需要在本地安装 library:
$ npm install --save lodash
  • src/index.js
import _ from 'lodash';
function component() {
  var element = document.createElement('div');
  // lodash 是由当前 script 脚本 import 导入进来的
 element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}
document.body.appendChild(component());
  • index.html


  
    起步
  
  
 
  

3、执行

$ npx webpack

4、在浏览器中打开 index.html,如果一切访问都正常,你应该能看到以下文本:'Hello webpack'。

2)模块

  • ES2015 中的 import 和 export 语句已经被标准化。虽然大多数浏览器还无法支持它们,但是 webpack 却能够提供开箱即用般的支持。
  • webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 的转译器。

3)使用一个配置文件
webpack.config.js

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

4)NPM 脚本(NPM Scripts)
在 package.json 添加一个 npm 脚本(npm script):

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --mode development",
    "build": "webpack --mode production"
}

现在,可以使用 npm run dev 命令,来替代我们之前使用的 npx 命令。
5)结论
现在,你已经实现了一个基本的构建过程,此刻你的项目应该和如下类似:

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
  |- bundle.js
  |- index.html
|- /src
  |- index.js
|- /node_modules

如果你使用的是 npm 5,你可能还会在目录中看到一个 package-lock.json 文件。

三、管理资源

webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。
1)加载 CSS
为了从 JavaScript 模块中 import 一个 CSS 文件,你需要在 module 配置中 安装并添加 style-loader 和 css-loader:

$ npm install --save-dev style-loader css-loader

webpack.config.js中添加模块

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

webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。

我们尝试一下,通过在项目中添加一个新的 style.css 文件,并将其导入到我们的 index.js 中:

  |- /src
    |- style.css
    |- index.js

src/style.css

.hello {
  color: red;
}

src/index.js

import _ from 'lodash';
import './style.css';                  //引入css
function component() {
  var element = document.createElement('div');
  // lodash 是由当前 script 脚本 import 导入进来的
 element.innerHTML = _.join(['Hello', 'webpack'], ' ');
 element.classList.add('hello');        //添加节点
  return element;
}
document.body.appendChild(component());

现在运行构建命令:

$ npm run build

浏览器中打开 index.html,你应该看到 Hello webpack 现在的样式是红色。
查看页面的 head 标签。它包含了我们在 index.js 中导入的 style 块元素。

2)加载图片
背景和图标这些图片,使用 file-loader,我们可以轻松地将这些内容混合到 CSS 中:

$ npm install --save-dev file-loader

webpack.config.js中添加

 module: {
       {
         test: /\.(png|svg|jpg|gif)$/,
         use: [ 'file-loader']
      }
}

我们向项目添加一个图像,然后看它是如何工作的,你可以使用任何你喜欢的图像:

  |- /src
    |- icon.png
    |- style.css
    |- index.js

src/index.js

import _ from 'lodash';
import './style.css';              
import Icon from './icon.png';           //引入图片
function component() {
  var element = document.createElement('div');
  // lodash 是由当前 script 脚本 import 导入进来的
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.classList.add('hello');   
        
  var myIcon = new Image();              //将图像添加到我们现有的 div
  myIcon.src = Icon;
  element.appendChild(myIcon);
  return element;
}
document.body.appendChild(component());

src/style.css

  .hello {
    color: red;
    background: url('./icon.png');
  }

现在运行构建命令:

$ npm run build

如果一切顺利,在Hello webpack 文本旁边将显示 img 元素。

想要压缩和优化你的图像。查看 image-webpack-loader 和 url-loader,以了解更多关于如果增强加载处理图片功能。

3)加载字体
像字体这样的其他资源如何处理呢?file-loader 和 url-loader 可以接收并加载任何文件,这就是说,我们可以将它们用于任何类型的文件,包括字体。让我们更新 webpack.config.js 来处理字体文件:
webpack.config.js中添加:

module: {
     {
         test: /\.(woff|woff2|eot|ttf|otf)$/,
         use: [ 'file-loader' ]
       }
 }

在项目中添加一些字体文件:

  |- /src
    |- my-font.woff
    |- my-font.woff2
    |- icon.png
    |- style.css
    |- index.js

通过一个 @font-face 声明引入。本地的 url(...) 指令会被 webpack 获取处理,就像它处理图片资源一样:
src/style.css

 @font-face {
   font-family: 'MyFont';
   src:  url('./my-font.woff2') format('woff2'),
         url('./my-font.woff') format('woff');
   font-weight: 600;
  font-style: normal;
 }
  .hello {
    color: red;
    font-family: 'MyFont';
    background: url('./icon.png');
  }

现在运行构建命令:

$ npm run build

重新打开 index.html 看看我们的 Hello webpack 文本显示是否换上了新的字体。如果一切顺利,你应该能看到变化。

4)加载数据
此外,可以加载的有用资源还有数据JSON 支持实际上是内置的,也就是说 import Data from './data.json' 默认将正常运行。

四、管理输出

随着引入所有资源的增长,手动地对 index.html 文件进行管理,一切就会变得困难起来。然而,可以通过一些插件,会使这个过程更容易操控。
1)预先准备
首先,让我们调整一下我们的项目:

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
    |- print.js
  |- /node_modules

我们在 src/print.js 文件中添加一些逻辑:
src/print.js

export default function printMe() {
  console.log('I get called from print.js!');
}

并且在 src/index.js 文件中使用这个函数:
src/index.js

  import _ from 'lodash';
  import printMe from './print.js';
  function component() {
    var element = document.createElement('div');
    var btn = document.createElement('button');
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;
    element.appendChild(btn);
    return element;
  }
  document.body.appendChild(component());

我们还要更新 dist/index.html 文件,来为 webpack 分离入口做好准备:
dist/index.html

  
  
    
     Output Management
     
    
    
     
    
  

现在调整配置。我们将在 entry 添加 src/print.js 作为新的入口起点(print),然后修改 output,以便根据入口起点名称动态生成 bundle 名称:
webpack.config.js

  const path = require('path');

  module.exports = {
   entry: {
     app: './src/index.js',
     print: './src/print.js'
   },
    output: {
     filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

现在运行构建命令:

$ npm run build

我们可以看到,webpack 生成 print.bundle.js 和 app.bundle.js 文件,这也和我们在 index.html 文件中指定的文件名称相对应。如果你在浏览器中打开 index.html,就可以看到在点击按钮时会发生什么。

2)清理 /dist 文件夹
在每次构建前清理 /dist 文件夹,是比较推荐的做法,因此只会生成用到的文件。clean-webpack-plugin 是一个比较普及的管理插件,让我们安装和配置下。

$ npm install clean-webpack-plugin --save-dev

webpack.config.js

  const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
 const { CleanWebpackPlugin } = require("clean-webpack-plugin");  //新增

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
    plugins: [
     new CleanWebpackPlugin(),        //新增
      new HtmlWebpackPlugin({
        title: 'Output Management'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

现在执行 npm run build,再检查 /dist 文件夹。如果一切顺利,你现在应该不会再看到旧的文件,只有构建后生成的文件!

五、开发

1)使用 source map
当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。
为了更容易地追踪错误和警告,JavaScript 提供了 [source map]功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。
我们使用 inline-source-map 选项,这有助于解释说明我们的目的。
webpack.config.js

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
   devtool: 'inline-source-map',      //使用 inline-source-map 选项
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Development'
      })
    ],

现在,让我们来做一些调试,在 print.js 文件中生成一个错误:
src/print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   cosnole.error('I get called from print.js!');
  }

运行 npm run build
现在在浏览器打开最终生成的 index.html 文件,点击按钮,并且在控制台查看显示的错误。错误应该如下:

 Uncaught ReferenceError: cosnole is not defined
    at HTMLButtonElement.printMe (print.js:2)

我们可以看到,此错误包含有发生错误的文件(print.js)和行号(2)的引用。这是非常有帮助的,因为现在我们知道了,所要解决的问题的确切位置。

2)选择一个开发工具
每次要编译代码时,手动运行 npm run build 就会变得很麻烦。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:

  • webpack's Watch Mode
  • webpack-dev-server
  • webpack-dev-middleware

多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以上的所有选项。

3)观察模式
我们添加一个用于启动 webpack 的观察模式的 npm script 脚本:

package.json

    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "watch": "webpack --watch",
      "build": "webpack"
    },

现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。
现在,修改保存文件并检查终端窗口。应该可以看到 webpack 自动重新编译修改后的模块!

4)使用 webpack-dev-server
webpack-dev-server 提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。让我们设置以下:

npm install --save-dev webpack-dev-server

修改配置文件,告诉开发服务器(dev server),在哪里查找文件:
webpack.config.js

    devtool: 'inline-source-map',
+   devServer: {
+     contentBase: './dist'
+   },

以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。
让我们添加一个 script 脚本,可以直接运行开发服务器(dev server):
package.json

   "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "watch": "webpack --watch",
+     "start": "webpack-dev-server --open",
      "build": "webpack"
    },

现在,我们可以在命令行中运行 npm start,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码

5)使用 webpack-dev-middleware
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。
同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
首先,安装 express 和 webpack-dev-middleware:

npm install --save-dev express webpack-dev-middleware

webpack.config.js

    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
+     publicPath: '/'
    }

下一步就是设置我们自定义的 express 服务:
project

  webpack-demo
  |- package.json
  |- webpack.config.js
+ |- server.js
  |- /dist
  |- /src
    |- index.js
    |- print.js
  |- /node_modules

server.js

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

//告诉express使用webpack-dev-middleware并使用webpack.config.js
//以配置文件为基础。
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

//服务监听3000端口。
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

现在,添加一个 npm script,以使我们更方便地运行服务:
package.json

"scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "watch": "webpack --watch",
     "start": "webpack-dev-server --open",
+     "server": "node server.js",
     "build": "webpack"
   },

现在,在你的终端执行 npm run server
打开浏览器,跳转到 http://localhost:3000,你应该看到你的webpack 应用程序已经运行!

六、模块热替换

模块热替换功能会在应用程序运行过程中替换、添加或删除模块而无需重新加载整个页面。
它允许在运行时更新各种模块,而无需进行完全刷新。

1)启用 HMR
启用此功能实际上相当简单。而我们要做的,就是更新 webpack-dev-server的配置,和使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js 的入口起点,因为它现在正被 index.js 模块使用。
webpack.config.js

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

  module.exports = {
    entry: {
-      app: './src/index.js',
-      print: './src/print.js'
+      app: './src/index.js'
    },
    devtool: 'inline-source-map',
    devServer: {
      contentBase: './dist',
+     hot: true
    },
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Hot Module Replacement'
      }),
+     new webpack.NamedModulesPlugin(),
+     new webpack.HotModuleReplacementPlugin()
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

package.json

"scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "watch": "webpack --watch",
-      "start": "webpack-dev-server --open",
+      "start": "webpack-dev-server --hotOnly",
       "build": "webpack"
    },

在起步阶段,我们将通过在命令行中运行 npm start 来启动并运行 dev server。

修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。
index.js

  document.body.appendChild(component());
+
+ if (module.hot) {
+   module.hot.accept('./print.js', function() {
+     console.log('Accepting the updated printMe module!');
+     printMe();
+   })
+ }

更改 print.js 中 console.log 的输出内容。
print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...')
  }

你将会在浏览器中看到如下的输出。
console

[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR]  - 20
+ main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.

2)通过 Node.js API
当使用 webpack dev server 和 Node.js API 时,不要将 dev server 选项放在 webpack 配置对象(webpack config object)中。而是,在创建选项时,将其作为第二个参数传递。例如:
new WebpackDevServer(compiler, options)
想要启用 HMR,还需要修改 webpack 配置对象,使其包含 HMR 入口起点。webpack-dev-server package 中具有一个叫做 addDevServerEntrypoints 的方法,你可以通过使用这个方法来实现。这是关于如何使用的一个小例子:
dev-server.js

const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');

const config = require('./webpack.config.js');
const options = {
  contentBase: './dist',
  hot: true,
  host: 'localhost'
};

webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);

server.listen(5000, 'localhost', () => {
  console.log('dev server listening on port 5000');
});

3)问题
模块热替换可能比较难掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮,你会发现控制台仍在打印这旧的 printMe 功能。

这是因为按钮的 onclick 事件仍然绑定在旧的 printMe 函数上。

为了让它与 HMR 正常工作,我们需要使用 module.hot.accept 更新绑定到新的 printMe 函数上:
index.js

- document.body.appendChild(component());
+ let element = component(); // 当 print.js 改变导致页面重新渲染时,重新获取渲染的元素
+ document.body.appendChild(element);

  if (module.hot) {
    module.hot.accept('./print.js', function() {
      console.log('Accepting the updated printMe module!');
-     printMe();
+     document.body.removeChild(element);
+     element = component(); // 重新渲染页面后,component 更新 click 事件处理
+     document.body.appendChild(element);
    })
  }

4)HMR 修改样式表
借助于 style-loader 的帮助,CSS 的模块热替换实际上是相当简单的。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch)