这是我参与「第四届青训营 」笔记创作活动的的第10天
Webpack知识体系
什么是Weboack
为了解决前端开发时遇到的一系列问题,如依赖手工,难以接入其他Less,Sass工具等,出现了很多工程化工具。
某种程度上,正是这些工具的出现,才有了“前端工程”这一概念
Webpack本质上是一种前端资源编译、打包工具
使用Webpack
示例
1.安装依赖
npm i -D webpack webpack-cli
2.编辑配置文件
在webpack.config.js中配置文件
webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件, 从而基于给定的配置,对项目进行打包。 也就是说,我们所有打包操作都依赖于这个配置文件。
module.exports={
//声明入口:要打包的文件
entry:'index.js',
//声明产物出口
output:{
filename:"[name].js",
//项目打包后的存放位置
path:path.join(__diename,"./dist"),
},
module:{
rules:[{
test:/.less$/i,
use:['style-loader','css-loader','less-loader']
}]
}
}
我们观察上面的例子,我们先不考虑里面一些文件的内容,可以把整个export出去的东西看成一个对象。这里面有三个属性分别是,entry,output和module,我们先一一来解释一下这三个属性。
-
entry这个属性填的值是整个配置文件的入口文件,也就是我们首先从哪个文件开始打包编译。
-
output这个属性值是最后我们打包压缩完的文件名,也就是说我们填main.js最后就会打包成一个叫main.js的文件
-
module这个属性决定了如何处理项目中的不同类型的模块,其中最重要的个属性是rules,这个属性用来存放我们设置的处理规则。比如:在例子中,test意义是让我们过滤出.less后缀的文件以use中的loader来处理这些文件。
3.执行编译命令
npx webpack
打包后的文件
webpack打包的流程
这个过程是webpack的打包的核心流程。 大概是这样:首先会取得entry的文件来处理,然后扫描里面的import和require了哪些模块和代码,对应去寻找所在的文件,并用module模块定义的规则来处理找寻到的文件,如果处理过程在又发现import或者require的文件,则接着递归处理下去,直到完全处理完打包生成output的文件。
webpack实际上主要做了两件事情——模块化 + 一致性
-
多个文件资源合并成一个,减少http请求数
-
支持模块化开发(
import、export对不同类型资源的管理) -
支持高级js特性
-
支持Typescript、CoffeeScript方言
-
统一图片、css、字体等其他资源的处理模型
-
Etc···
webpack属性的区分
关于Webpack的使用方法,基本都围绕配置"展开,而这些配置大致可划分为两类:
- 流程类:作用于流程中某个or若干个环节,直接影响打包效果的配置项
- 工具类:主流程之外,提供更多工程化能力的配置项
流程类配置
配置总览
按使用频率
-
'entry/output'
-
'module/plugins'
-
'mode'
-
'watch/devServer/devtool'
处理CSS
首先我们需要安装loader npm add -D css-loader style-loader 在这里我们只需要重点关注module中的操作即可。
module: {
rules: [
{ test: /.css$/, use: ['style-loader', 'css-loader'] },
]
}
其中,test 表示匹配的文件类型,用正则表达式过滤出为所以后缀为.css的文件。 use 表示对应要调用的 loader,要注意的是
- use 数组中指定的 loader 顺序是固定的
- 多个 loader 的调用顺序是:从后往前调用
思考题
1.Loader有什么作用?为什么这里需要用到css-loader、style-loader?
答:loader这里是用来转译过滤出来的文件的,在转译过程是需要调用对应的加载资源库, css-loader、style-loader就是在转译时所需要的库。
2.与旧时代一在HTML文件中维护css相比,这种方式会有什么优劣处?
答:显然在此时我们已经不需要再手动去维护HTML文件中的css代码了,这些多出来的冗余操作可以丢掉了。
3.有没有接触过Less、Sass、 Stylus 这一类CSS预编译框架?如何在Webpack接 入这些工具?
处理JS
接入Babel webpack 只能打包处理一部分高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法,需要借助于 babel-loader 进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码 :
// 1.定义了名为info的装饰器
function info(target) {
// 2.为目标添加静态属性info
target.info = 'Person info
}
/1 3.为Person 类应用info 装饰器
@info
class Person {}
// 4.打印Person 的静态属性info
console. log(Person. info)
复制代码
安装bable
npm i babel-loader @babel/core @babel/preset-env -D
module: {
rules: [
// 使用 babel-loader 处理高级的 JS 语法
// 在配置 babel-loader 的时候,程序员只需要把自己的代码进行转换即可;一定要排除 node_modules 目录中的 JS 文件,因为第三方包中的 JS 兼容性,不需要程序员关心
{ test: /.js$/, use: 'babel-loader', options: { presets: [ '@babel/preset-env' ] } }
]
},
思考题
1.Babel具体有什么功能?
答:Babel 是一个 JavaScript 编译器,我们很多类型的代码文件单独拎出来其实是不能跑的。比如react中的JSX,react也是在脚手架中直接引入babel编译器,把JSX代码解析成JS代码。所以Babel就是把一些其他类型的代码编译成JS。
2.Babel与Webpack分别解决了什么问题?为何两者能协作到一起了?
答:Babel是来解析代码变成JS代码,webpack是用来打包压缩整合代码的工具。在webpack中有个东西叫loader,loader需要解析到很多代码文件,两者有同样的应用场景,因此用到出现babel-loader其实并不奇怪。
参考资料
生成 HTML
webpack 中的 HTML 插件(类似于一个模板引擎插件), 可以通过此插件自定制 index.html 页面的内容 。html-webpack-plugin 是 webpack 中的 HTML 插件,可以通过此插件自定制 index.html 页面的内容。 例如:通过 html-webpack-plugin 插件,将 src 目录下的 index.html 首页,复制到项目根目录中一份!
安装包 npm install html-webpack-plugin -D
module.exports = {
// 插件的数组,将来 webpack 在运行时,会加载并调用这些插件
plugins: [htmlPlugin, new CleanWebpackPlugin()],
}
plugins这个属性是用来挂载插件的。
通过 HTML 插件复制到项目根目录中的 index.html 页面,也被放到了内存中。 HTML 插件在生成的 index.html 页面,自动注入了打包的 bundle.js 文件
Question:
1.相比于手工维护HTML内容,这种自动生成的方式有什么优缺点?
答:在传统项目中,每次对项目打包后我们都需要手动在index.html添加js的链接,当打包后的js入口文件名更改时,我们又需要再一次修改index.html中的js。 但是用自动生成的方式即可避免这种问题。
工具线
工具类一般有这些,如下图:
1.HMR(模块热替换)
HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验。 使用HRM Webpack 生态下,只需要经过简单的配置即可启动 HMR 功能,大致上分两步:
// webpack.config.js
module.exports = {
// ...
devServer: {
// 必须设置 devServer.hot = true,启动 HMR 功能
hot: true
}
};
之后,还需要调用 module.hot.accept 接口,声明如何将模块安全地替换为最新代码,如:
import component from "./component";
let demoComponent = component();
document.body.appendChild(demoComponent);
// HMR interface
if (module.hot) {
// Capture hot update
module.hot.accept("./component", () => {
const nextComponent = component();
// Replace old content with the hot loaded one
document.body.replaceChild(nextComponent, demoComponent);
demoComponent = nextComponent;
});
}
模块代码的替换逻辑可能非常复杂,幸运的是我们通常不太需要对此过多关注,因为业界许多 Webpack Loader 已经提供了针对不同资源的 HMR 功能,例如:
-
style-loader内置 Css 模块热更 -
vue-loader内置 Vue 模块热更 -
react-hot-reload内置 React 模块热更接口
因此,站在使用的角度,只需要针对不同资源配置对应支持 HMR 的 Loader 即可,很容易上手。
参考资料:
2.Tree-Shaking
Tree-shaking(树摇)
用来删除Dead Code(没有被用到的代码)使用Tree-shaking
开启tree-shaking :
-
'mode: "production" '
-
'optimization.useExports: true'
PS:对工具类库如Lodash有奇效
这里解释一下mode结点内容: mode属性:mode 节点的可选值有两个,分别是: development 和 production.
-
development为开发环境 不会对打包生成的文件进行代码压缩和性能优化 ,打包速度快,适合在开发阶段使用。 -
production为 生产环境, 会对打包生成的文件进行代码压缩和性能优化, 打包速度很慢,仅适合在项目发布阶段使用 。
其他工具:
-
缓存
-
Sourcemap
-
性能监控
-
日志
-
代码压缩
-
分包
-
......
理解 loader
作用:
-
为了处理非标准JS资源,设计出资源翻译模块
-
用于将非js资源翻译为标准的js资源
使用上,可以为某种资源文件配置多个 Loader,Loader 之间按照配置的顺序从前到后(pitch),再从后到前依次执行,从而形成一套内容转译工作流。
认识:下面这3个loader 主要干了些什么
以链式调用的方式加载——前一个loader的输出作为后一个loader的输入
-
less-loader: 实现了 less ~> css 的转换
-
css-loader: 将 CSS 包装成类似 module.exports = "${css}" 的内容,包装后的内容符合 JavaScript 语法
-
style-loader: 将 CSS 模块包进 require 语句,并在运行时调用 injectStyle 等函数将内容注入到页面的 style 标签
其他特性:
-
链式执行
-
支持异步执行
-
分 normal、pitch 两种模式
参考资料:如何编写loader
实现eslint-loader:
import getOptions from './ get0ptions ' ;
import Linter from './Linter' ;
import cacheLoader from './cacheLoader ';
export default function loader( content, map){
const options = getOptions ( this );
//创建lint实例
const linter = new Linter( this, options ) ;
this.cacheable( );
//return early if cached
if(options.cache){
cacheLoader( linter, content,map );
return;
}
//检查输入代码,并返回结果cn见tent
linter.printOutput(linter.lint( content ));
this.callback( null, content,map );
}
常见Loader
站在使用角度,建议掌握这些常见loader的功能、配置方法
理解插件
什么是插件?为什么需要这么设计?
很多的知名工具都设计了所谓“插件”架构的 如:
-
VScode、Webstorm、Chrome、Firefox
-
Babel、Webpack、Rollup、Eslint
-
Vue、Redux、Quill、Axios
如果没有使用“插件”架构,对于一个项目而言是不好的,缺点如下:
-
新人需要了解整个流程细节,上手成本高
-
功能迭代成本高,牵一发动全身
-
功能僵化,作为开源项目而言缺乏成长性
-
Blaba
总结:
-
心智成本高
-
可维护性低
-
缺少生命力
插件构架精髓:是对扩展开放,对修改封闭的一种思维
使用插件
使用html-webpack-plugin
module.exports={
...
plugins:[
new HTMLWebpackPlugin()
]
...
}
使用html-webpack-plugin + DefinePlugin
const webpack = require('webpack')
const HtmlwebpackPlugin = require( 'html-webpack-plugin ')
module.exports = {
entry: "./ src/index" ,
output: {
filename: " [name].js ",
path: path.join( __dirname, " ./dist" )
},
plugins:[
new HtmlwebpackPlugin( ),new webpack. DefinePlugin({
PRODUCTION:JSON.stringify( true ),
VERSION: JSON.stringify( ' 5fa3b9'),
})
]
};
如何写插件
写一个插件最需要关注以三个参数:
-
时机:
compier.hooks.compilation——钩子在何时触发 -
参数:
compilation等 -
交互:
dependencyFactories.set——在钩子回调中如何和webpack其他上下文内容进行交互
插件围绕钩子展开
钩子的核心信息:
-
时机: 编译过程的特定节点,Webpack 会以钩子形式通知插件此刻正在发生什么事情
-
上下文: 通过 tapable 提供的回调机制,以参数方式传递上下文信息
-
交互: 在上下文参数对象中附带了很多存在 side effect 的交互接口,插件可以通过这些接口改变
参考资料:
小结
-
Webpack 的作用
-
理解 Webpack 配置结构,学习关键配置项
-
Loader 的作用和常用Loader
-
插件基本形态与作用
学习方法
知识点