前言
这是我参与「第五届青训营」伴学笔记创作活动的第 13天,前几篇我们介绍了React用的jsx语法,而浏览器是识别不了jsx语法,得通过webpack等编译打包工具。那么我们就来看一下webpack具体是什么?
为什么要学习Webpack?
- 理解前端“工程化”概念、工具、目标
- 一个团队总要有那么几个人熟悉
Webpack某种程度上可以成为个人的核心竞争力 - 高阶前端必经之路
主要内容
- 什么是 Webpack
- Webpack 打包核心流程
- 示例
- Entry => Dependencies Lookup => Transform => Bundle => Output
- 关键配置项介绍
Loader组件Plugin组件- 如何学习
Webpack- 入门级: 学会灵活应用
- 进阶: 学会扩展 Webpack
- 大师: 源码级理解 Webpack 打包编译原理
课程目标
- 理解
Webpack的基本用法 - 通过介绍
Webpack功能、Loader与Plugin组件设计,建立一个知识体系 - X 不会事无巨细,介绍
Webpack所有 - X 也不是深入源码,讲解底层实现原理
webpack认知,关于webpack的用法,webpack的扩展性
什么是 Webpack
- 资源前端项目由什么构成?
- PNG
- css
- JPG
- Less
- GIF
- Vue
- WEBP
- JSX
- js
- Sass
- TS
- ...
资源管理方式
以前是通过以下方式去管理这些资源:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div>
<img src="./img.jpg" alt="" >
</div>
<script src="./main.js"></script>
<script src="./foo.js"></script>
<script src="./bar.js"></script>
</body>
</html>
- 依赖手工,比如有50个
JS文件...操作,过程繁琐 - 当代码文件之间有依赖的时候,就得严格按依赖顺序书写
- 开发与生产环境一致,难以接入
TS或JS新特性 - 比较难接入
Less、Sass等工具 JS、图片、CSS资源管理模型不一致
这些都是旧时代非常突出的问题,对开发效率影响非常大,直到webpack 等构建打包工具的出现
工程化工具
出现了很多工程化工具,某种程度上正是这些工具的出现,才有了“前端工程”这一概念
本质上是一种前端资源编译、打包工具
- 多份资源文件打包成一个
Bundle - 支持
Babel、Eslint、TS、CoffeScript、Less、Sass - 支持模块化处理 css、图片 等资源文件
- 支持
HMR+ 开发服务器 - 支持持续监听、持续构建
- 支持代码分离
- 支持
Tree-shaking - 支持
Sourcemap - ...
使用 Webpack
使用 Webpack -- 示例
- 安装
npm i -D webpack webpack-cli
- 编辑配置文件
module.exports = {
entry: 'main.js',
output: {
filename:"[name].js"
path: path.join(__dirname,"./dist"),
},
module: {
rules: [{
test: /\.less$/i,
use: ['style-loader','css-loader','less-loader']
}]
}
}
- 执行编译命令
npx webpack
核心流程 - 极度简化版
- Get Start
entry- 1.入口处理; 从 entry’文件开始,启动编译流程
- Dependencies Lookup
requireimport- 2. 依赖解析; 从
entry文件开始,根据requireorimport等语句找到依赖资源
- 2. 依赖解析; 从
- Transfrom
module- 3. 资源解析; 根据
module配置,调用资源转移器,将png、css等非标准JS资源转译为JS内容
- 3. 资源解析; 根据
- Combine Assets
output- 4. 资源合并打包; 将转译后的资源内容合并打包为可直接在浏览器运行的
JS文件
- 4. 资源合并打包; 将转译后的资源内容合并打包为可直接在浏览器运行的
递归调用2、3,直到所有资源处理完毕
模块化 + 一致性
- 多个文件资源合并成一个,减少
http请求数 - 支持模块化开发
- 支持高级
JS特性 - 支持
Typescript、CoffeeScript方言 - 统一图片、
CSS、字体 等其它资源的处理模型 - Etc..
那么,怎么使用 Webpack?
关于 Webpack 的使用方法,基本都围绕“配置展开,而这些配置大致可划分为两类:
- 流程类: 作用于流程中某个 or 若干个环节直接影响打包效果的配置项可
- 工具类: 主流程之外,提供更多工程化能力 的配置项
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { HotModuleReplacePlugin } = require('webpack')
module.exports = {
entry: './src/index',
mode: 'devlopment',
devtool: false,
output: {
filename: '[name].js',
path: path.join(__dirname,'./dist'),
clean: true,
},
devServer:{
hot: true,
}
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'Hot Module Replacement',
}),
]
};
使用 Webpack -- 流程类配置
- Get Start
entry-
- 输入
entry,context
- 输入
-
- Dependencies Lookup
requireimport- 2. 模块解析
resolve,externals
- 2. 模块解析
- Transfrom
module- 3. 模块转译
module
- 3. 模块转译
- Combine Assets
output-
4. 后处理:
optimization,mode,target
-
使用 Webpack --配置总览
按使用频率:
entry/outputmodule/pluginsmodewatch/devServer/devtool
文件结构
|-- src
| - index.js
|- webpack.config.js
- 声明入口
entry
module.exports = {
entry: './src/index'
}
- 声明产物入口
output
// webpack.config.js
const path = require('path')
module.exports = {
entry:'src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
}
}
- 运行
npx webpack
下载
webpck、webpack-cli,然后配置文件,文件内容至少配置entry和output模块;通过npx webpack命令运行
使用 Webpack -- 处理 CSS
|-- src
| - index.js
| - index.css
|- webpack.config.js
const styles = require('./index.css');
// or
import styles from './index.css'
- 安装 Loader
npm add -D css-loader style-loader
- 添加
module处理css 文件
// webpack.config.js
const path = require('path')
module.exports = {
entry:'./src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
},
module:{
// css 处理器
rules:[{
test: /\.css/i,
use:[
'style-loader',
'css-loader'
]
}]
}
}
通过
module和rules字段配置相应的规则,通过test字段,过滤文件名和文件后缀,通过use字段配置相应的loader序列;loader是在webpack用来处理不同的资源的组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div>
<img src="./img.jpg" alt="" >
</div>
<script src="./main.js"></script>
<script src="./foo.js"></script>
<script src="./bar.js"></script>
</body>
</html>
问题:
Loader有什么作用? 为什么这里需要用到css-loader、style-loader- 与旧时代 -- 在
HTML文件中维护css相比,这种方式会有什么优劣处? - 有没有接触过
Less、Sass、Stylus这一类 CSS预编译框架? 如何在Webpack接入这些工具?
loader用于处理不同资源文件的配置组件;- 因为
webpack是用css-loader的css,后通过push到一个数组,将css 模块转化为 js 模块,然后将运用style-loader将这些css-loader转化为js的css,继续转换会css,再加上<style>标签上;- 优点
是减少很多繁琐重复的工作,提供了这样的模板写法,更清晰的看懂自己做了什么操作,只需关注单个文件,不需要在各个css文件进行穿梭和跳转;
缺点
是对新手不是那么的友好,需要记住许多的概念和配置,每深入一个概念就会有更多概念,让人难以琢磨- 接触过
less,也是需要配置相应的loader,只不过是在原先style-loader和css-loader上 多添加了less-loader
参考资料:
Css-loader
Webpack 原理系列七: 如何编写Loader
Style-loader
使用 Webpack -- 接入 Babel
文件结构
|-- src
| - index.js
|- webpack.config.js
// index.js
class Person {
constructor() {
this.name = 'Tecvan'
}
}
console.log((new Person()).name)
const say = () => {}
- 安装 依赖
npm i -D @babel/core @babel/preset-env babel-loader
- 声明产物出口
output
// webpack.config.js
const path = require('path')
module.exports = {
entry:'./src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
},
module:{
// css 处理器
rules:[{
test: /\.js?$/,
use:[{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env'
]
]
}
}
]
}]
}
}
- 执行
npx webpack
思考题:
Babel 具体有什么功能?
Babel 与 Webpack 分别解决了什么问题? 为何两者能协作到一起了?
babel可以对js代码转译;- 在那个浏览器不支持ES6的
js新语法,所以那时候babel就 发挥出了它的作用;可以将高版本的js代码转译成低版本的js代 码,因为webpack集合各种loader对scass,less等预处理 器进行编译和转译,还有一些plugin可以进一步提高开发效率; 通过webpack去管理这些loader,plugin,babel;可以理解为webpack负责管理这些工具函数和编译和打包,它们这些工具函数只需要负责自己的职责就好了
参考资料:
Babel-loader
Babel 官网
@babel/preset-env
@babel/preset-react
@babel/preset-typescript
使用 Webpack -- 生成 HTML
文件结构
.
|-- src
| - index.js
|- webpack.config.js
- 安装依赖
npm i -D html-webpack-plugin
- 声明产物出口
output
// webpack.config.js
const path = require('path')
module.exports = {
entry:'./src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
},
plugins:[new HtmlWebpackPlugin()]
}
- 执行
npx webpack
<!- index.html -!>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="main.js"></script>
</head>
<body>
</body>
</html>
思考题:
相比于手工维护 HTML 内容,这种自动生成的方式有什么优缺点?
可以自动生成
html和js文件,假如要生成多个html和js文件时会非常的方便,但是假如你只需要一两个文件时,就不需要去配置webpack.config.js文件了,直接去书写就好了;
参考资料:
html-webpack-plugin
使用 Webpack -- 工具线
使用 Webpack -- HMR
- 开启
HMR
// webpack.config.js
module.exports = {
// ...
devServer: {
hot: true
}
}
- 启动
Webpack
npx webpack serve
可以利用
webpack.config.js中的devServer:{hot:true}和watch构建开发的服务器
参考: 《Webpack 原理系列十: HMR 原理全解析》
使用 Webpack- Tree-Shaking
Tree-Shaking - 树摇,用于删除 Dead Code
开启 tree-shaking:
mode:“production”optimization.usedExports: true
PS: 对工具类库如 Lodash 有奇效
tree-shaking在生产环境、预编译时没有使用的全局变量进行清理
其它工具:
- 缓存
- Sourcemap
- 性能监控
- 日志
- 代码压缩
- 分包
- ...
思考题
- 除上面提到的内容,还有哪些配置可划分为“流程类”配置?
- 工具类配置具体有什么作用? 包括
devtool/cache/stat等
resolve,performace等devtool就是可以用于开发工具,最常见是sourcemap可以开启打包前映射一份源文件,方便进行代码调试;cache可以启用缓存提升构建速度;stat可以进行 控制台输出日志控制
参考资料:
Awesome-webpack-4plus
《深入浅出 Webpack》
《Survivejs - Webpack Book》
进阶篇: 理解 Loader
// webpack.config.js
const path = require('path')
module.exports = {
entry:'./src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
}
}
// index.js
import './index.css'
使用 Loader
.
|-- src
| - a.less
| - b.less
| - index.js
|- webpack.config.js
// index.js
import styles from './a.less';
- 安装 Loader
npm add -D css-loader style-loader less-loader
module处理 css 文件
// webpack.config.js
const path = require('path')
module.exports = {
module:{
rules:[{
test: /\.less$/i,
use:[
'style-loader',
'css-loader',
'less-loader',
}
}
]
}]
}
}
认识 Loader: 链式调用
less-loader: 实现less=>css的转换css-loader: 将CSS包装成类似module.exports="${css}"的内容,包装后的内容符合JavaScript语法style-loader: 将css模块包进require语句,并在运行时调用iniectStyle等函数将内容注入到页面的style标签
认识 Loader: 其它特性
特点:
- 链式执行
- 支持异步执行
- 分
normal、pitch两种模式
如何编写 Loader
// eslint-loader/index.js
import getOptions from './getOptions';
import Linter from './Linter';
importcacheLoader from './cacheLoader';
export default function loader(content, map) {
const options = getOptions(this);
const linter = new Linter(this, options);
this.cacheable();
// return early if cached
if (options.cache) {
cacheLoader(linter, content, map);
return;
}
linter.printOutput(linter.lint(content));
this.callback(null, content, map);
}
module.exports = function (source,sourceMap?,data?) {
// source 为 loader 的输入
// 可能是文件内容,也可能是上一个 loader 处理结果
return source;
}
常见 Loader
站在使用角度,建议掌握这些常见Loader 的功能、配置方法
理解插件
思考题:
Loader输入是什么? 要求的输出是什么?Loader的链式调用是什么意思? 如何串联多个Loader?Loader中如何处理异步场景? 参考:- 《 Webpack 原理系列七: 如何编写loader 》
进阶篇:理解插件
插件是什么? 为什么这么设计?
很多知名工具,如:
VS Code、Web Storm、Chrome、FirefoxBabel、Webpack、Rollup、EslintVue、Redux、Quill、Axios
等等,都设计了所谓插件架构,为什么?
因为需要整合插件的分类方便进行管理,各司其职
这是一个特别复杂的过程,那么:
- 新人需要了解整个流程细节,上手成本高
- 功能迭代成本高,牵一发动全身
- 功能僵化,作为开源项目而言缺乏成长性
- Blabla
心智成本高 => 可维护性低 => 生命力弱
插件架构精髓:对扩展开放,对修改封闭
甚至,Webpack 本身的很多功能也是基于插件实现的
理解插件
// webpack.config.js
// npm i -D webpack-dashboard
// Import the plugin:
const DashboardPlugin = require( "webpack-dashboard/plugin");
// Add it ta your webpack configuration plugins.
module.exports = {
// ...
plugins: [new DashboardPlugin( )];
// ...
};
使用 html-webpack-plugin
/* webpack.config.js */
module.exports = {
/* ... */
plugins: [
new HtmlWebpackPlugin()
];
/* ... */
};
使用 html-webpack-plugin + DefinePlugin
// webpack.config.js
const path = require('webpack')
module.exports = {
entry:'./src/index',
output: {
filename:'[name].js',
path: paht.join(__dirname,'./dist')
},
plugins:{
new HtmlWebpackPlugin(),
new Webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9')
})
}
}
用起来很简单,但写起来...
首先:插件围绕 钩子 展开
// somePlugin.js
class somePlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
}
}
}
钩子的核心信息
- 时机:编译过程的特定节点,
Webpack会以钩子形式通知插件此刻正在发生什么事情 - 上下文: 通过
tapable提供的回调机制,以参数方式传递上下文信息 - 交互:在上下文参数对象中附带了很多存在
side effect的交互接口,插件可以通过这些接口改变
- 时机:
compier.hooks.compilation - 参数:
compilation等 - 交互:
dependencyFactories.set
class EntryPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("EntryPlugin", (compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
)
}
)
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compilation.addEntry(context, dep, options, (err) => {
callback(err);
});
})
}
}
思考题:
- Loader 与插件有什么区同点?
- “钩子”有什么作用?如何监听钩子函数?
loader让webpack拥有了加载和解析非JavaScript文件的能力,loader可以将文件从不同的语言(如TypeScript)转换为JavaScript,或者将内联图像转换为data URL。比如说:CSS-Loader,Style-Loader等。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的 API 改变输出结果。hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。它允许在某个特定时刻调用一些代码。用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。
为了监听钩子函数,你需要连接到钩子,并定义需要在钩子被触发时执行的代码。
参考:
《Webpack 插件架构深度讲解》
《[万字总结] 一文吃透 Webpack 核心原理》
小结
我们前面学了
Webpack的作用- 理解
Webpack配置结构,学习关键配置项 Loader的作用与常用Loader- 插件基本形态与作用
学习方法
- 入门应用
- 理解打包流程
- 熟练掌握常用配置项、Loader、插件的使用方法,能够灵活搭建集成
Vue、React、Babel、Eslint、Less、Sass、图片处理等工具的Webpack环境 - 掌握常见脚手架工具的用法,例如:
Vue-cli、create-react-app、@angular/cli
- 进阶
- 理解
Loader、Plugin机制,能够自行开发Webpack组件 - 理解常见性能优化手段,并能用于解决实际问题
- 理解前端工程化概念与生态现状
- 大师级
- 阅读源码,理解
Webpack编译、打包原理,甚至能够参与共建