Webpack 快速入门指南(二)

开发

本指南继续沿用管理输出指南中的代码示例。

如果你一直跟随之前的指南,应该对一些 webpack 基础知识有着很扎实的理解。在我们继续之前,先来看看如何建立一个开发环境,使我们的开发变得更容易一些。

本指南中的工具仅用于开发环境,请不要在生产环境中使用它们!

使用 source map

当 webpack 打包源代码时,可能很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js。这通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。

source map 有很多不同的选项可用,请务必仔细阅读它们,以便可以根据需要进行配置。

对于本指南,我们使用 inline-source-map 选项,这有助于解释说明我们的目的(仅解释说明,不要用于生产环境):

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'
    },
    devtool: 'inline-source-map',
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            title: 'Output Management'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

现在,让我们来做一些调试,在 src/print.js 文件中,故意写一段错误的代码:

export default function printMe() {
    cosnole.error('this line has a error');
}

运行 npm run dev,在终端并看不出异常。

现在,在浏览器打开最终生成的 index.html 文件,点击页面中的 按钮,就可以在浏览器的控制台查看显示的错误。错误应该如下:

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

此错误信息具体指明了发生错误的文件(print.js)和行号(2)。这是非常有帮助的。

选择一个开发工具

某些文本编辑器具有安全写入功能,可能会干扰以下某些工具。

每次要编译(构建)代码时,都需要重新手动执行 npm run dev,显得很麻烦。

webpack 中有三种不同的工具,可以帮助你在代码发生变化后自动编译代码:

  • webpack's Watch Mode(观察模式)
  • webpack-dev-server
  • webpack-dev-middleware

多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下这三种工具。

webpack's Watch Mode(观察模式)

你可以指示 webpack "watch" 依赖图中的所有文件,如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行构建。

通过修改 package.json 文件,添加一个用于启动 webpack 的观察模式的 npm script 脚本:

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo test ok !",
    "watch": "webpack --watch --mode development",
    "dev": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.18",
    "css-loader": "^0.28.10",
    "csv-loader": "^2.1.1",
    "file-loader": "^1.1.9",
    "html-webpack-plugin": "github:jantimon/html-webpack-plugin",
    "style-loader": "^0.20.2",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.9",
    "xml-loader": "^1.2.1"
  },
  "dependencies": {
    "lodash": "^4.17.5"
  }
}

现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。

在 webpack 观察文件的同时,我们先移除之前引入的错误:

src/print.js

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

现在,保存文件并检查终端窗口。可以看到 webpack 自动重新编译修改后的模块!

唯一的缺点是,为了看到修改后的实际效果,你需要刷新浏览器。

如果能够自动刷新浏览器就更好了,可以尝试使用 webpack-dev-server,恰好可以实现我们想要的功能。

webpack-dev-server

webpack-dev-server 为你提供了一个简单的 web 服务器,并且能够实时重新加载。

安装 webpack-dev-server :

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

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

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'
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist'
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            title: 'Output Management'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。

添加 npm script 脚本,以便可以通过 npm script 直接运行开发服务器(dev server)。也就是修改项目目录中的 package.json 文件,在 scripts 键指向的值中加入如下代码:

"start": "webpack-dev-server --open",

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

说明: webpack-dev-server 是通过 node.js 创建的 web 服务器来和 web 进行交互的。node.js 创建的 web 服务器和其他主流 web 服务器(Apache、Nginx、IIS)的功能差不多。webpack-dev-server 有很多配置选项,具体可查询 webpack 官网。

webpack-dev-middleware

webpack-dev-middleware 是一个容器,它可以把 webpack 处理后的文件传递给一个服务器。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置。

接下来是一个 webpack-dev-middleware 配合 express server 的示例。

首先,安装 express 和 webpack-dev-middleware 。

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

接下来我们需要对 webpack 的配置文件做一些调整,以确保中间件(middleware)功能能够正确启用。主要是调整 webpack.config.js 文件中的 output 的 publicPath 属性。

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'
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist'
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            title: 'Output Management'
        })
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: '/'
    }
};

publicPath 也会在服务器脚本用到,以确保文件资源能够在 http://localhost:3000 下正确访问。下一步就是设置我们自定义的 express 服务。

在项目根目录中,添加 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);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

然后,在 package.json 文件中添加 npm script 脚本,以使我们更方便地运行服务。即在该文件的 scripts 中添加下面的代码:

"server": "node server.js",

现在,在你的终端(命令行)执行 npm run server 。

打开浏览器,访问 http://localhost:3000,你应该看到你的 webpack 应用程序已经运行!

调整文本编辑器

使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。

要在一些常见的编辑器中禁用此功能,请查看以下列表:

  • Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: "false"。
  • IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。
  • Vim - 在设置(settings)中增加 :set backupcopy=yes。
  • WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消 Use "safe write"。

结论

现在,你已经学会了如何自动编译代码,并运行一个简单的开发服务器。接下来,进入 模块热替换(Hot Module Replacement)。

模块热替换

模块热替换(Hot Module Replacement)简称 HMR, 是 webpack 提供的最有用的功能之一。

它允许在运行时更新各种模块,而无需进行完全刷新。HMR 不适用于生产环境,应当只在开发环境使用。

启用 HMR

启用 HMR 比较简单。使用 webpack 内置的 HMR 插件可以实现。

要删掉 print.js 的入口起点,因为它现在正被 index.js 模式使用。

【 如果你使用了 webpack-dev-middleware 而没有使用 webpack-dev-server,请使用 webpack-hot-middleware package 包,以在你的自定义服务或应用程序上启用 HMR。】

修改 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'
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist',
        hot: true
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            title: '模块热替换'
        }),
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

【 也可通过命令来修改 webpack-dev-server 的配置:webpack-dev-server --hotOnly。】

我们还添加了 NamedModulesPlugin,以便更容易查看要修补(patch)的依赖。

在命令行中运行 npm start 来启动 webpack-dev-server。

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

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!333';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
}

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 的内容。

export default function printMe() {
    console.log('Updating print.js...');
}

你就会在浏览器(http://localhost:8080/) 的控制台(Console),看到如下内容。

[HMR] Waiting for update signal from WDS...
client:77 [WDS] Hot Module Replacement enabled.
[WDS] App hot update...
log.js:24 [HMR] Checking for updates on the server...
index.js:22 Accepting the updated printMe module!
print.js:2 Updating print.js...
log.js:24 [HMR] Updated modules:
log.js:24 [HMR]  - ./src/print.js
log.js:24 [HMR] App is up to date.

如此,就实现了 HMR(模块热替换)。还可以通过 Node.js API 来实现HMR(这里不讲解)。

问题

在刚才的示例中,如果你继续点击示例页面上的按钮,可以发现问题:控制台打印的是旧的内容。

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

为了让它与 HRM 正常工作,我们需要调整 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());
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);
    })
}

这只是一个例子,还有很多其他地方容易让人犯错。幸运的是,存在很多 loader(其中一些在下面提到),使得模块热替换的过程变得更容易。

HMR 修改样式表

借助 style-loader 的帮助,CSS 的模块热替换非常简单。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch)