3
开发工具诞生的目的永远是加速开发. 程序员应该不断追求更快更好的开发工具.

前文一步步学Webpack4(0)-- 实战起步已经完成了Webpack环境的搭建以及实现了一句命令自动打包项目,这一次我们继续使用之前的项目webpack-stepbystep来尝试搭建适合对开发者友好的项目开发环境.

本章按照以下步骤进行:

  1. 开发需求总结:跟随官方文档 Development ,总结前端开发者对于调试与开发的需求;
  2. 练习1:Webpack开发工具认识与选择;
  3. 练习2:Webpack热模块更新(HMR);
  4. 练习3:认识loader并完成基础配置;

写Webpack文章不写版本都是耍流氓,这篇文章基于当下最新的 webpack v4.22.0 以及 webpack-cli v3.1.2 编写.

1. 开发需求探索

Eating your own dog food

尝试深入探索学习Webpack的人大概都有一颗想给自己写个顺手的手脚架的心吧,吃自己的狗粮这件事对开发者肯定是好事,但是前提是自己真正懂得自己的需求.

对于一个普通前端开发者来说,一个简单项目的手脚架必须具备一定的能力,总结一下一些必不可少的需求吧:

  1. 方便的调试信息追溯;
  2. 代码修改之后自动打包;
  3. 代码修改之后自动更新页面内容;

接下来我们就来借助Webpack的能力,一个个实现这些需求~

2. 练习1:Webpack开发工具认识与选择

2.1 source map 实现调试信息追溯

文章跟随 一步步学Webpack4(0)-- 实战起步 继续开发.

项目已经能够使用Webpack打包了,我们现在使用这个项目来随便写点会发生错误的代码,例如在方法第一行加入 console.abg('generate component')

index.js

import _ from 'lodash';
function component () {
    console.abg('generate component');
    let element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'Webpack'], ' ');
    return element;
}
document.body.appendChild(component());

然后在终端中运行 webpack 完成打包,运行结果如下:

Error1

发现错误是被指向了编译后的文件 main.js ,这并不是我们想要的. 发生错误的时候浏览器如果不能追溯到源代码发生错误的位置,这将增大调试的难度,幸好Webpack已经提供解决这个问题的方法--source map,我们只需要简单地修改一下配置文件:

webpack.config.js

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

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    devtool: 'inline-source-map',
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            inject: false,
            template: 'index.html',
            filename: 'index.html'
        })
    ]
};

上面代码中加入了一句 devtool: 'inline-source-map' , 重新在终端中运行 webpack 完成打包,运行结果如下:

Error2

成功了,借助source map的力量, 错误发生时浏览器从 main.js 追溯到了源代码 index.js 中. 至此我们已经成功实现了第一个需求“方便的错误信息追溯”. 另外要特别注意的是,source map 只能在开发环境中使用以方便调试,千万不能用于生产环境,简单原因看看添加了 source map之后的main.js文件大小就知道了(逃

当然 source map 还有许多配置可以选择, 不过与本章的学习关系不大, 先继续往下学习吧~

2.2 试用开发工具

刀耕火种时期每次保存完代码都要F5,在项目中应用了Webpack之后每次保存完代码居然需要先Webpack打包再F5,这么愚蠢的事情程序员怎么可能允许呢,于是开发工具们开始诞生了:

2.2.1 Webpack观察者模式(webpack's Watch Mode)

严格来说这不算是一种额外的开发工具,这只是Webpack的一种运行模式,可以在终端输入 webpack --watch 开始持续监听文件变化,只要修改代码并保存,webpack将会自动帮你打包项目,听起来还不错能够自动打包,但是这种模式并不能帮助开发者更新页面内容也就是说, 你还是需要自己按F5刷新..., 感觉还是有点惨啊.

算了 =。= Next one

2.2.2 webpack-dev-server(推荐)

这是一个官方推荐的新手友好的开发工具,webpack-dev-server提供了一个具备实时重载功能的简单服务器,只需要下载到工程中,并对配置文件进行简单配置即可使用,安装命令如下:

npm i -D webpack-dev-server

此处基本使用 webpack-dev-server 的默认配置,唯一修改的地方是配置开启服务器的位置即 contentBase: './dist',整份配置如下所示:

webpack.config.js

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

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist'
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            inject: false,
            template: 'index.html',
            filename: 'index.html'
        })
    ]
};

接下来打开 package.json 文件,在"scripts"中添加一行运行脚本 "start": "webpakc-dev-server --open",完整package.json文件如下:

package.json

{
  "name": "webpack-stepbystep",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "start": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.19",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.22.0",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  },
  "dependencies": {
    "lodash": "^4.17.11"
  }
}

然后打开终端,运行命令 npm start,稍后就能看到浏览器自动打开了 localhost:8080 界面,如果对代码进行保存修改,web服务器就会自动重新打包代码并将更新应用到浏览器网页上~至此前端程序员的对于手脚架的几个需求已经完全得到了满足,现在已经可以舒舒服服地开始前端开发了~

至此项目提交为 feat(project): add source map & devtools .

P.S. 官方文档中还有一个开发工具 webpack-dev-middleware, 与Node.js结合能够进行更多的自定义配置,不过暂时我们不需要用到它.

3. 练习2:Webpack热模块更新(HMR)

3.1 修改错误代码测试

完成上一小节的配置之后,我们可以开始尝试在当前项目中编写代码了,首先我们当然是先来改正第一小节的错误,将 index.js 中的console.abg('generate component'); 改为 console.log('generate component'); ,文件完整代码如下所示:

index.js

import _ from 'lodash';
function component () {
    console.log('generate component');
    let element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'Webpack'], ' ');
    return element;
}
document.body.appendChild(component());

保存之后,很快就能看到页面上出现了熟悉的 Hello Webpack~ 修改代码之后只需要保存,剩下的事情Webpack都会帮你自动搞定,自动更新的效果不错嘛~

然而,这只是一个小小的项目。设想一下,你现在正在调试一个规模比较大的项目,在最后一步按下"提交button"之前,你突然想起"提交button"绑定错了触发的事件. 如果此时修改代码并保存,应用页面将会被刷新,也就是说你刚刚选择的许多选项的状态会被重置回初始值。你的粗心让你需要把之前的选择流程走一遍,如果之后又发现了另一个小错误那么又要再走一遍流程...此时的你多么希望有一个工具能让你保持着页面当前的状态,并偷偷地帮你更新修改好的绑定关系, 你只需要在完成更新后从容按下"提交button"就完事. 没错,这就是这一小节的重点 模块热更新 Hot Module Replacement(HMR)!

3.2 实战:简单模块热更新(HMR)

说了那么多,不如show me your code. 好,现在马上通过实战来见识一下 HMR 的厉害.

3.2.1 实战准备

我们新建一个模块称为printMe, 负责打印一段文字, 在index.js中引用该模块并为编写一个button来触发它,完整代码如下所示:

print.js

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

index.js

import _ from 'lodash';
import printMe from './print';
function component () {
    let element = document.createElement('div');
    let 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());

3.2.2 实时刷新测试

修改 print.js 的打印内容并保存,当前效果是:整个页面直接通过刷新来更新界面.

3.2.3 应用模块热更新

  • 步骤一:首先更新webpack的配置文件. 在配置头部加入对webpack的引用,然后在devServer对象中配置 hot: true 来开启 HMR ,最后在plugins对象中配置 HotModuleReplacementPlugin 插件以替换模块,完整代码如下所示:

    webpack.config.js

    const path = require('path');
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const webpack = require('webpack');
    
    module.exports = {
        entry: './src/index.js',
        output: {
            filename: 'main.js',
            path: path.resolve(__dirname, 'dist')
        },
        devtool: 'inline-source-map',
        devServer: {
            contentBase: './dist',
            hot: true
        },
        plugins: [
            new CleanWebpackPlugin(['dist']),
            new HtmlWebpackPlugin({
                inject: false,
                template: 'index.html',
                filename: 'index.html'
            }),
            new webpack.HotModuleReplacementPlugin()
        ]
    };
  • 步骤二:修改 index.js 文件,在底部加入 HMR 相关代码,令其在 printMe 模块发生改变时可以接受更新的模块,完整代码如下所示:

    index.js

    import _ from 'lodash';
    import printMe from './print';
    function component () {
        let element = document.createElement('div');
        let 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());
    
    if (module.hot) {
        module.hot.accept('./print.js', function () {
            console.log('Accepting the updated printMe module!');
            printMe();
        });
    }
  • 步骤三:修改 print.js 的打印文字并保存,通过观察控制打印结果,发现页面完成了修改并且没有产生刷新.
  • 步骤四:你以为就这样结束了?其实并没有,HMR 手撸的话还是比较坑的. 点击button你会发现控制台中打印的东西一直都是最初始的打印值,这是因为button的事件依然绑定在旧的函数上,为了解决这个问题,我们将通过 index.js 底部 HMR 代码更新button的事件绑定,具体完整代码如下所示:

    index.js

    import _ from 'lodash';
    import printMe from './print';
    function component () {
        let element = document.createElement('div');
        let 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 ele = component();
    document.body.appendChild(ele);
    
    if (module.hot) {
        module.hot.accept('./print.js', function () {
            console.log('Accepting the updated printMe module!');
            // printMe();
            document.body.removeChild(ele);
            ele = component();
            document.body.appendChild(ele);
        });
    }

现在再修改 print.js 的打印文字并保存,通过观察控制打印结果,发现页面完成了修改并且没有产生刷新,并且点击之后控制台会出现新修改的文字. HMR 配置成功~

现在觉得 HMR 开发很难?Webpack的开发者自然考虑到了这一点,Webpack 的 loader将会帮你把这一个过程变得简单.

3.3 HRM 修改样式表

只需下载loader并完成配置,之后的同类型改动需要更新时,loader会自动在幕后通过 module.hot.accept 完成对于内容的修补.

这次实战我们先安装并配置 styleloader & cssloader, 然后借助loader的力量帮助我们实现页面样式的模块热更新,体验loader带来的便利.

  • 步骤一、安装loader到项目
npm i -D style-loader css-loader
  • 步骤二、加入样式对应loader配置, 完整配置代码如下所示:

webpack.config.js

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

module.exports = {
    entry: {
        app: './src/index.js'
    },
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist',
        hot: true
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        new HtmlWebpackPlugin({
            inject: false,
            template: 'index.html',
            filename: 'index.html'
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
};
  • 步骤三、在项目的src文件夹下添加文件 styles.css 并在 index.js 引用:

styles.css

body {
    background-color: blue;
}

index.js

import _ from 'lodash';
import printMe from './print';
import './styles.css';
function component () {
    let element = document.createElement('div');
    let 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 ele = component();
document.body.appendChild(ele);

if (module.hot) {
    module.hot.accept('./print.js', function () {
        console.log('Accepting the updated printMe module!');
        // printMe();
        document.body.removeChild(ele);
        ele = component();
        document.body.appendChild(ele);
    });
}
  • 步骤四、在终端输入命令 npm start, 确认应用开启完毕后,修改 styles.css, 保存后观察控制台的打印:
body {
    background-color: #fff;
}

发现页面在没刷新的情况下完成了背景颜色的变化. 借助loader的力量成功实现了 HMR 的效果.

3.4 HMR小结

Hot Module Replacement(HMR)是Webpack最棒的特性之一,当代码修完并保存之后,Webpack将重新打包项目,并将新的模块发送到浏览器端,浏览器更新对应的模块,以此达到更新应用页面的目的.

不同于实时刷新的开发工具库,HMR 在更新之后依旧能够保持原有的应用状态,提高了开发者的开发效率.

至此项目提交为 feat(project): finish dev-server & HMR config .

4. 练习3:认识loader并完成基础配置

在Webpack出现之前,前端工程师们使用的打包工具通常是 grunt 或者 gulp, 这些工具处理图片等资源的方式通常是复制,也就是将文件复制一份到打包目录下.

但是Webpack不同,它对于js和资源文件一视同仁,也就是将资源也看作模块,使用到这些模块的地方需要显示调用资源,然后由Webpack动态构建依赖图完成统一打包, Webpack通过资源间的强依赖关系,完美避开了隐式引用和无效引用造成的错误和浪费.

为了完成对任何类型资源的引用,社区出现了各种格式的loader来帮助Webpack完成这个任务. 比较通用的loader有:

  1. 样式loader: style-loader、css-loader等
  2. 图片loader: file-loader
    Tip:进阶可以学习使用 pimage-webpack-loader](https://github.com/tcoopman/i... 或者 url-loader
  3. 字体loader: file-loader
  4. 数据loader

以上资源可以直接放在一个控件目录下,并通过显式声明依赖建立起该控件的依赖关系图. 这样的控件更具备可移植性.

具体使用操作可以跟随官方文档的Asset Management章节跑一波,目标是认识常用loader并跟随文档完成当前项目配置即可.

至此项目提交为feat(project): finish loaders study

5. 项目地址

6. 总结

本章以开发需求探索开始, 根据总结的需求提出解决方案并选择新手友好的开发工具 webpack-dev-server , 接着进一步了解方便开发者调试修改应用特性 HMR, 最后再学习并使用loader完成项目的基础配置. 简单的开发环境搭建已经完成了,现在可以使用这个环境试试愉快的代码编写吧~

To be continued...

系列文章


Nodreame
155 声望32 粉丝

伪全栈|前端|前软粉