Webpack
这是我参与第五届青训营伴学笔记创作活动的第 7 天,记录了 Webpack 零基础入门的一些知识。
一、为什么要学习Webpack
- 理解前端“工程化”概念、工具、目标
- 一个团队总要有那么几个人熟悉Webpack,某种程度上可以成为个人的核心竞争力
- 高阶前端必经之路
二、什么是Webpack
首先我们来了解Webpack出现的原因:前端项目资源(JS、CSS、图片等等)的种类杂、数量多且耦合性高
我们当然可以手动管理这些文件,但是问题也很明显
- 依赖手工,比如有n多个 JS 文件……一个个去导入过于繁琐
- 当代码文件之间有依赖的时候,就得严格按依赖顺序书写,耦合性高
- 开发与生产环境一致,难以接入 TS 或 JS 新特性
- 比较难接入Less、Sass等工具
- JS、图片、CSS资源管理模型不一致
这些缺陷极大影响开发效率,我们看也令人头痛
为了解决这些问题,就涌现出了Webpack等一系列工程化工具
那么什么是Webpack呢?
Webpack本质上是一种前端资源编译、打包工具
编译主要是将浏览器不认识的语法编译成浏览器认识的语法以及资源的压缩优化。less编译成css,ES6语法转换成ES5。
- 将多份资源文件打包成一个Bundle
- 支持 Babel、Eslink、TS、CoffeScript、Less、Sass
- 支持模块化处理 css、图片 等资源文件
- 支持 HMR + 开发服务器
- 支持持续监听、持续构建
- 支持代码分离
- 支持 Tree-shaking
- 支持 Sourcemap
- ……
三、使用Webpack——流程
基本流程
- 安装webpack:npm i -D webpack webpack-cli
- 编辑配置文件:webpack.config.js
- 执行编译命令:npx webpack
执行完这三步后我们就得到编译打包好的 js 文件啦!
核心流程
- 入口处理(Get Start):从
entry
文件开始,启动编译流程 - 依赖解析(Dependency Lookup):从
entry
文件开始,根据require
或import
等语句找到依赖资源 - 资源解析(Transform):根据
module
配置,调用资源转移器,将 png、css 等非标准 JS 资源转译为 JS 内容 - 资源合并打包(Combine Assets):将转译后的资源内容合并打包为可直接在浏览器运行的 JS 文件
2和3步骤是递归执行的,因为很有可能出现一个文件嵌套其他文件的情况,递归直到处理完全部文件进入4步骤打包合并
功能:模块化 + 一致性
- 多个文件资源合并成一个,减少 http 请求数
- 支持模块化开发
- 支持高级 JS 特性
- 支持 TypeScript 、 CoffeeScript 方言
- 统一图片、CSS、字体 等其他资源的处理模型
- ……
配置文件——流程类和工具类
关于Webpack的使用方法,基本都围绕“配置”展开,而这些配置大致可划分为两类:
- 流程类:作用于流程中某个 or 若干个环节,直接影响打包效果的配置项
- 工具类:主流程之外,提供更多工程化能力的配置项
配置总览
按使用频率:
- entry/output
- module/plugins
- mode
- watch/devServer/devtool
最简单使用:只声明入口 entry 和出口 output
假设目录结构如下:
D:.
│ webpack.config.js
│
└─src
index.js
- 声明入口
entry
: - 声明产物出口:
output
:
const path = require("path");
module.exports = {
entry: "./src/index",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
}
};
- 运行
npx webpack
即可看到出现了一个dist目录,里面出现了打包好的main.js
对非js文件的打包——使用loader
webpack默认只能处理 .js 后缀的文件,一旦使用默认的webpack处理非 js 文件就会报错 Unexpected token
那么怎么处理这种情况呢——使用loader就可以解决了。以处理css为例
假设文件目录如下:
D:.
│ webpack.config.js
│
└─src
index.css
index.js
- 安装Loader:npm add -D css-loader style-loader
- 添加 module 处理 css 文件
- 运行
npx webpack
const path = require("path");
module.exports = {
entry: "./src/index",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
module: {
// css 处理器
rules: [{
// test处理不同后缀文件,当然可以用.less等等文件
test: /\.css/i,
// 使用什么loader来处理,多个loader执行顺序从后向前
use: [
"style-loader",
"css-loader"
]
}]
}
};
js不兼容——接入Babel
我们知道有很多 js 新特性是不能够直接被浏览器运行的,要转为更早版本的 js 代码才可以,常规是使用Babel进行转换,比如将ES6转化为ES5语法。
webpack就可以通过接入Babel的方式来对 js 进行转换
- 安装依赖:npm i -D @babel/preset-env babel-loader
- 声明产物出口
output
- 运行
npx webpack
const path = require("path");
module.exports = {
entry: "./src/index",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
module: {
rules: [{
// 以 js 结尾的文件
test: /\.js?$/,
use: [{
loader: 'babel-loader',
// loader的配置项
options: {
presets: [
[
'@babel/preset-env'
]
]
}
},]
}]
}
};
生成 HTML
可以用 webpack 自动生成 HTML 文件,使用html-webpack-plugin插件
- 安装依赖:npm i -D html-webpack-plugin
- 声明产物出口
output
- 执行
npx webpack
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
plugins: [new HtmlWebpackPlugin()]
};
这样我们会在dist目录下得到一个index.html,这个html也自动导入了main.js,但是没有什么实质内容,有兴趣小伙伴可以自行去学习这个插件。
四、使用Webpack——工具
模块热替换——HMR(Hot Module Replacement)
可能没听过,但是我们都潜在的用过,就是代码保存后立刻就运行了,浏览器自动做出了一系列响应,那么webpack怎么使用呢?
- 开启 HMR
module.exports = {
// ...
devServer: {
hot: true
}
};
- 启动Webpack:npx webpack serve
Tree-Shaking——摇树优化
什么是Tree-Shaking
呢?它的用途就是删掉代码中的Dead Code,就是那些没有什么用的代码
- 代码没有被用到,不可到达
- 代码的执行结果不会被用到
- 代码只读不写
- ……
那么怎么使用呢?
开启 tree-shaking:添加两种代码
-
mode: "production",
-
optimization: {
useExports: true,
}
例:
module.exports = {
entry: "./src/index",
output: {
filename: "[name].js",
path: path.join(__dirname, "./dist"),
},
mode: "production",
optimization: {
usedExports: true
}
};
这种方法对工具类库有奇效,因为工具类库一般非常全面,我们只用到一部分,所以这种方法会帮我们节省非常大的空间
五、Loader
理解链式调用
我们刚刚说过Loader的用途:为了解决Webpack只认识标准 JS 的问题,用于将资源翻译为标准 JS
但是我们刚刚也举了例子,对一个css的处理用到了css-loader和style-loader,那么这两个loader有什么关系吗?
答案是,loader间会发生链式调用,一个loader用另一个loader的产物。我们以一个 .less文件为例
先有一个pitch阶段,按照从前往后的定义顺序执行,pitch 阶段一般不返回值,一旦 pitch 阶段有 loader 返回值,则从这里开始进入从后往前执行的 normal 阶段。
然后执行阶段多个loader执行顺序从后到前执行
如何编写 Loader
需要导出一个默认函数,一般会有三个参数:
source
:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果sourceMap
: 可选参数,代码的 sourcemap 结构data
: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象
也就是下面这样
module.exports = function(source, sourceMap?, data?) {
// source 为 loader 的输入
// 可能是文件内容,也可能是上一个 loader 处理结果
return source;
}
常用的loader
六、理解插件
首先,插件不是webpack独有,我们熟悉的VS Code、Vue、Babel等等工具为了提高扩展性,都设计了所谓的“插件架构”。
我们用webpack编译的流程图来引入插件的话题:
这是一个特别复杂的过程:
- 新人需要了解整个流程细节,上手成本高
- 功能迭代成本高,牵一发而动全身
- 功能僵化,作为开源项目而言缺乏成长性
- 最最关键,上手成本高 -> 可维护性低 -> 生命力弱
为了解决这些问题,出现了插件架构
插件架构精髓:对扩展开放,对修改封闭
只做最核心的部分,但开放一系列修改方式供开发者开发。甚至Webpack本身的很多功能也是通过插件实现的
使用插件
以 webpack-dashboard 插件为例,dashboard 的作用是将webpack的编译结果用一个好看的表格形式展现出来
- 安装插件:npm -i -D webpack-dashboard
- 导入插件:
- 创建实例
// npm -i -D webpack-dashboard
// 导入插件
const DashboardPlugin = require("webpack-dashboard/plugin");
// 创建实例
module.exports = {
// ...
plugins: [new DashboardPlugin()];
// ...
}
写插件——过难
插件用起来十分简单,但是写起来嘛……
首先:插件围绕钩子
展开,这里的钩子可以理解成就是webpack的一系列的事件
钩子的核心信息:
- 时机:编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情
- 上下文:通过 tapable 提供的回调机制,以参数方式传递上下文信息
- 交互:在上下文参数对象中附带了很多存在 side effect 的交互接口,插件可以通过这些接口改变状态
例:EntryPlugin插件
- 时机:complier.hooks.compilation
- 参数:compilation等
- 交互:dependencyFactories.set
由于插件很复杂,故不推荐新手上手,还需去官方文档学习
七、参考
- 字节录播课 - 《Webpack定义解析》
- 字节录播课 - 《Webpack使用方法》
- 字节录播课 - 《理解Loader》
- 字节录播课 - 《理解插件》