webpack学习与总结

初识 Webpack

webpack 是模块打包工具,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码,包括如下内容。

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。

历史上先后出现一系列构建工具,它们各有其优缺点。由于前端工程师很熟悉 JavaScript ,Node.js 又可以胜任所有构建需求,所以大多数构建工具都是用 Node.js 开发的。下面来一一介绍它们。

Npm Script

Npm Script 是一个任务执行者。Npm 是在安装 Node.js 时附带的包管理器,Npm Script 则是 Npm 内置的一个功能,允许在 package.json 文件里面使用 scripts 字段定义任务:

1
2
3
4
5
6
{
"scripts": {
"dev": "node dev.js",
"pub": "node build.js"
}
}

里面的 scripts 字段是一个对象,每个属性对应一段 Shell 脚本,以上代码定义了两个任务 devpub。 其底层实现原理是通过调用 Shell 去运行脚本命令,例如执行 npm run pub 命令等同于执行命令 node build.js

Npm Script的优点是内置,无须安装其他依赖。其缺点是功能太简单,虽然提供了 prepost 两个钩子,但不能方便地管理多个任务之间的依赖。

Grunt

Grunt 和 Npm Script 类似,也是一个任务执行者。Grunt 有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.js 里,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = function(grunt) {
// 所有插件的配置信息
grunt.initConfig({
// uglify 插件的配置信息
uglify: {
app_task: {
files: {
'build/app.min.js': ['lib/index.js', 'lib/test.js']
}
}
},
// watch 插件的配置信息
watch: {
another: {
files: ['lib/*.js'],
}
}
});

// 告诉 grunt 我们将使用这些插件
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');

// 告诉grunt当我们在终端中启动 grunt 时需要执行哪些任务
grunt.registerTask('dev', ['uglify','watch']);
};

在项目根目录下执行命令 grunt dev 就会启动 JavaScript 文件压缩和自动刷新功能。

Grunt的优点是:

  • 灵活,它只负责执行你定义的任务;
  • 大量的可复用插件封装好了常见的构建任务。

Grunt的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。

Grunt 相当于进化版的 Npm Script,它的诞生其实是为了弥补 Npm Script 的不足。

Gulp

Gulp 是一个基于流的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:

  • 通过 gulp.task 注册一个任务;
  • 通过 gulp.run 执行任务;
  • 通过 gulp.watch 监听文件变化;
  • 通过 gulp.src 读取文件;
  • 通过 gulp.dest 写文件。

Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递,大致使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 引入 Gulp
var gulp = require('gulp');
// 引入插件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');

// 编译 SCSS 任务
gulp.task('sass', function() {
// 读取文件通过管道喂给插件
gulp.src('./scss/*.scss')
// SCSS 插件把 scss 文件编译成 CSS 文件
.pipe(sass())
// 输出文件
.pipe(gulp.dest('./css'));
});

// 合并压缩 JS
gulp.task('scripts', function() {
gulp.src('./js/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('./dist'));
});

// 监听文件变化
gulp.task('watch', function(){
// 当 scss 文件被编辑时执行 SCSS 任务
gulp.watch('./scss/*.scss', ['sass']);
gulp.watch('./js/*.js', ['scripts']);
});

Gulp 的优点是好用又不失灵活,既可以单独完成构建也可以和其它工具搭配使用。其缺点是和 Grunt 类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。

可以将Gulp 看作 Grunt 的加强版。相对于 Grunt,Gulp增加了监听文件、读写文件、流式处理的功能。

Webpack

Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。

其官网的首页图很形象的画出了 Webpack 是什么,如下:

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

Webpack 具有很大的灵活性,能配置如何处理文件,大致使用如下:

1
2
3
4
5
6
7
8
module.exports = {
// 所有模块的入口,Webpack 从入口开始递归解析出所有依赖的模块
entry: './app.js',
output: {
// 把入口所依赖的所有模块打包成一个文件 bundle.js 输出
filename: 'bundle.js'
}
}

Webpack的优点是:

  • 专注于处理模块化的项目,能做到开箱即用一步到位;
  • 通过 Plugin 扩展,完整好用又不失灵活;
  • 使用场景不仅限于 Web 开发;
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
  • 良好的开发体验。

Webpack的缺点是只能用于采用模块化开发的项目。

为什么选择 Webpack

上面介绍的构建工具是按照它们诞生的时间排序的,它们是时代的产物,侧面反映出 Web 开发的发展趋势如下:

  1. 在 Npm Script 和 Grunt 时代,Web 开发要做的事情变多,流程复杂,自动化思想被引入,用于简化流程;
  2. 在 Gulp 时代开始出现一些新语言用于提高开发效率,流式处理思想的出现是为了简化文件转换的流程,例如将 ES6 转换成 ES5。
  3. 在 Webpack 时代由于单页应用的流行,一个网页的功能和实现代码变得庞大,Web 开发向模块化改进。

这些构建工具都有各自的定位和专注点,它们之间既可以单独地完成任务,也可以相互搭配起来弥补各自的不足。 在了解这些常见的构建工具后,你需要根据自己的需求去判断应该如何选择和搭配它们才能更好地完成自己的需求。

经过多年的发展, Webpack 已经成为构建工具中的首选,这是有原因的:

  • 大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack 可以为这些新项目提供一站式的解决方案;
  • Webpack 有良好的生态链和维护团队,能提供良好的开发体验和保证质量;
  • Webpack 被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享。

使用Webpack

安装 Webpack 到本项目

在开始给项目加入构建前, 在安装 Webpack 前请确保你的系统安装了5.0.0及以上版本的 Node.js。你还需要先新建一个 Web 项目,进入项目根目录执行 npm init 来初始化最简单的采用了模块化开发的项目;

要安装 Webpack 到本项目,可按照你的需要选择以下任意命令运行:

1
2
3
4
5
6
7
8
9
# npm i -D 是 npm install --save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies
# 安装最新稳定版
npm i -D webpack

# 安装指定版本
npm i -D webpack@<version>

# 安装最新体验版本
npm i -D webpack@beta

安装完后你可以通过这些途径运行安装到本项目的 Webpack:

  • 在项目根目录下对应的命令行里通过 node_modules/.bin/webpack 运行 Webpack 可执行文件。

  • Npm Script 里定义的任务会优先使用本项目下的 Webpack,代码如下:

    1
    2
    3
    "scripts": {
    "start": "webpack --config webpack.config.js"
    }
  • 可以使用npx webpack

    Node 自带 npm 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。

    1
    npm install -g npx

    npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了 webpack 而且全局没有安装 webpack。

    1
    npm install -D webpack

    一般来说,调用 webpack,只能在项目脚本和 package.json 的scripts字段里面, 如果想在命令行下调用,必须像下面这样。

    1
    2
    3
    # 项目的根目录下执行
    cd node_modules\.bin
    webpack --version # 输出 4.41.0

    npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。

    1
    2
    # 项目的根目录下执行
    npx webpack --version

    npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。

    由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

    除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

    1
    $ npx create-react-app my-react-app

    上面代码运行时,npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app

    下载全局模块时,npx 允许指定版本。

    1
    $ npx uglify-js@3.1.0 main.js -o ./dist/main.js

    上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。

    注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

    1
    $ npx http-server

    参考:npx 使用教程

安装 Webpack 到全局

安装到全局后你可以在任何地方共用一个 Webpack 可执行文件,而不用各个项目重复安装,安装方式如下:

1
npm i -g webpack

虽然介绍了以上两种安装方式,但是我们推荐安装到本项目,原因是可防止不同项目依赖不同版本的 Webpack 而导致冲突。

使用 Webpack

默认配置

Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js 文件读取配置,所以你还需要新建它,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');

module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
}
};

由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object 对象。

使用loader

Webpack 把一切文件看作模块,CSS 文件也不例外,Webpack 不原生支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制。Webpack的配置修改使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');

module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
// 用正则去匹配要用该 loader 转换的 CSS 文件
test: /\.css$/,
use: ['style-loader', 'css-loader?minimize'],
}
]
}
};

Loader 可以看作具有文件转换功能的翻译员,配置里的 module.rules 数组配置了一组规则,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。 如上配置告诉 Webpack 在遇到以 .css 结尾的文件时先使用 css-loader 读取 CSS 文件,再交给 style-loader 把 CSS 内容注入到 JavaScript 里。 在配置 Loader 时需要注意的是:

  • use 属性的值需要是一个由 Loader 名称组成的数组,Loader 的执行顺序是由后到前的;
  • 每一个 Loader 都可以通过 URL querystring 的方式传入参数,例如 css-loader?minimize 中的 minimize 告诉 css-loader 要开启 CSS 压缩。

Webpack 构建前要先安装新引入的 Loader:

1
npm i -D style-loader css-loader

给 Loader 传入属性的方式除了有 querystring 外,还可以通过 Object 传入,以上的 Loader 配置可以修改为如下:

1
2
3
4
5
6
7
8
9
use: [
'style-loader',
{
loader:'css-loader',
options:{
minimize:true,
}
}
]

除了在 webpack.config.js 配置文件中配置 Loader 外,还可以在源码中指定用什么 Loader 去处理文件。 以加载 CSS 文件为例,修改上面例子中的 main.js 如下:

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

这样就能指定对 ./main.css 这个文件先采用 css-loader 再采用 style-loader 转换。

使用 Plugin

plugin 可以在 webpack 运行到某一时刻的时候,帮你做一些事情。

htmlWepackPluginnn 会在打包结束后,自动生成一个 html 文件,并把打包的生成的 js 自动引入到这个 html 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const HtmlWebpackPlugin = require("html-webpack-plugin"); //通过 npm 安装
const webpack = require("webpack"); //访问内置的插件
const path = require("path");

const config = {
mode: "production",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: "file-loader"
}
]
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })]
};

module.exports = config;

使用 DevServer

DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack ,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览。

首先需要安装 DevServer:

1
npm i -D webpack-dev-server

安装成功后执行 webpack-dev-server 命令, DevServer 就启动了,这时你会看到控制台有一串日志输出:

1
2
Project is running at http://localhost:8080/
webpack output is served from /

这意味着 DevServer 启动的 HTTP 服务器监听在 http://localhost:8080/ ,DevServer 启动后会一直驻留在后台保持运行,访问这个网址你就能获取项目根目录下的 index.html。 用浏览器打开这个地址你会发现页面空白错误原因是 ./dist/bundle.js 加载404了。 同时你会发现并没有文件输出到 dist 目录,原因是 DevServer 会把 Webpack 构建出的文件保存在内存中,在要访问输出的文件时,必须通过 HTTP 服务访问。 由于 DevServer 不会理会 webpack.config.js 里配置的 output.path 属性,所以要获取 bundle.js的正确 URL 是 http://localhost:8080/bundle.js,对应的 index.html 应该修改为:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 DevServer 输出的 JavaScript 文件-->
<script src="bundle.js"></script>
</body>
</html>

Webpack 可以在启动 Webpack 时通过 webpack --watch 来开启监听模式,实时预览。Webpack 支持生成 Source Map,只需在启动时带上 --devtool source-map 参数。 加上参数重启 DevServer 后刷新页面,再打开 Chrome 浏览器的开发者工具,就可在 Sources 栏中看到可调试的源代码了。

DevServer 还有一种被称作模块热替换的刷新技术。 模块热替换能做到在不重新加载整个网页的情况下,通过将被更新过的模块替换老的模块,再重新执行一次来实现实时预览。 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验。 模块热替换默认是关闭的,要开启模块热替换,你只需在启动 DevServer 时带上 --hot 参数,重启 DevServer 后再去更新文件就能体验到模块热替换的神奇了。

1
2
3
const config=require('./webpack.config.js');
const complier=wepack(config)
// 在node中使用webpack的方法。

核心概念

Webpack 有以下几个核心概念。

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。

webpack的配置

Entry

entry 是配置模块的入口,可抽象成输入, Webpack 执行构建的第 步将从入 口开始,搜寻及递归解析出所有入口依赖的模块。entry 配置是必填的,若不填则将导致 Webpack 报错、退出。

context

Webpack 在寻找相对路径的文件时会以 context 为根目录, context 默认为执行启动
Webpack 时所在的当前工作目录。如果想改变 context 的默认配置,则可以在配置文件里这
样设置它

1
2
3
module.exports = {
context: path.resolve(__dirname,'app')
}

loader

file-loader

就会拷贝到打包文件夹内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.jpg$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]"
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};

url-loader

会变成 base64,在 js 文件中直接加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: "./src/index.js"
},
module: {
rules: [
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "url-loader",
options: {
name: "[name].[ext]",
outputPath: "images/",
limit: 1024
}
}
}
]
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
}
};

css-loader style-loader

Webpack 进阶

Webpack 配置及案例

Webpack 原理及脚手架


webpack学习与总结
https://www.gishai.top/blog/posts/6e7939e6.html
作者
Hai
发布于
2019年9月24日
更新于
2022年6月10日
许可协议