webpack是什么?
- webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序
webpack的依赖
webpack的运行是依赖node环境的
webpack的安装
- webpack的安装目前分为两个:webpack,webpack-cli
- npm install webpack webpack-cli (-g)
webpack和webpack-cli的关系
- 执行webpack命令,会执行node_modules下的.bin目录下的webpack
- webpack在执行时是依赖webpack-cli的,如果没有安装就会报错
- 而webpack-cli中代码执行时,才是真正利用webpack进行编译和打包的过程
- 所以在安装webpack时,我们需要同时安装webpack-cli(第三方的脚手架事实上是没有使用webpack-cli的,而是类似于自己的vue-service-cli的东西)
传统开发存在的问题
我们的代码存在什么问题呢?某些语法浏览器是不认识的
- 使用了ES6的语法,比如const,箭头函数等语法
- 使用了ES6的模块化语法
- 使用了commonjs的模块化语法
- 在通过script标签引入时,必须添加上type=‘module’属性
webpack默认打包
-
我们可以通过webpack进行打包,运行打包之后的代码
- 在目录下直接执行webpack命令 webpack
-
生成一个dist文件夹,里面存放了一个main.js的文件,就是我们打包之后的文件:
- 这个文件的代码被压缩和丑化了
- 这个文件的代码依然存在ES6的语法
-
webpack是如何确定我们的入口呢?
- 事实上,当我们运行webpack时,webpack会查找当前目录下src/index.js作为入口
- 所以,如果当前项目中没有存在src/index.js文件,那么会报错
webpack配置文件
- 我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path')
mudule.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist') // 这里的路径要是绝对路径
}
}
- 继续执行webpack命令,依然可以正常打包
webpack指定配置文件
-
webpack默认会去读取当前目录下webpack.config.js文件
-
但是如果我们的配置文件并不是命名为webpack.config.js,而是其他名字呢?
-
比如我们将webpack.config.js 修改成了wk.config.js
-
这个时候我们可以通过 --config 来指定对应的配置文件
webpack --config wk.config.js
-
-
但是每次这样执行命令来对源码进行编译会非常繁琐,所以我们可以在package.json中添加一个新的脚本
- { "scripts": { "buile": "webpack --config wk.config.js" } }
webpack的依赖图
-
webpack到底是如何对我们的项目进行打包的呢?
- 事实上webpack在处理应用程序是,它会根据命令或者配置文件找到入口文件
- 从入口文件开始,会生成一个依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块
- 然后遍历这个依赖关系图,打包一个一个模块
css-loader的使用
-
当我们的项目中有css文件时,用webpack对项目进行打包时,会报错
-
webpack的错误信息告诉我们需要一个loader来加载这个css文件
loader是什么?
- loader可以用于对模块的源代码进行转换
- 我们可以将css文件也看成一个模块,我们是通过import来加载这个模块的
- 在加载这个模块时,webpack其实并不知道如何对其进行加载,我们必须制定对应的loader来完成这个功能
css-loader的安装
npm install css-loader -D
css-loader的使用方案
-
如何使用这个loader来加载css文件?有三种
- 内联方式
- CLI方式(webpack中不再使用)
- 配置方式
-
内联方式:内联方式使用较少,因为不方便管理
-
在引入的样式前加上使用的loader,并且用 ! 分割
import 'css-loader!../css/style.css'
-
loader配置方式
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.css$/, // 设置要用loader的文件
// loader: 'css-loader' 写法一,适用于只用一个loader,且没有配置选项
// use: ['css-loader'] 写法二:适用于使用多个loader,且没有配置选项
use: [
{ loader: 'css-loader' } // 写法三:适用于需要配置选项的loader
]
}
]
}
}
认识style-loader
- 通过css-loader来加载css文件,但是会发现这个css在我们的代码中并没有生效(页面没有效果)
- css-loader只是负责将.css文件进行解析,并不会将解析后的css插入页面
- 如果我们希望再完成插入style的操作,那么我们还需要另一个loader,style-loader
npm install style-loader -D
配置style-loader
- 注意loader的执行顺序是从右往左(或者说从上往下,从后往前) ,所以我们需要将style-loader写到css-loader的前面
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
}
处理less文件
- 在开发中,可能会使用less,sass, stylus的预处理器来编写css样式,效率会更高
- 首先需要确定,less,sass等编写的css需要通过工具转化成普通的css
less-loader处理less
处理less需要安装less,但是less-loader依赖了less工具,所以直接安装less-loader即可
npm install less-loader -D
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
]
}
}
浏览器兼容性
-
开发中,浏览器的兼容性问题,我们该如何去解决和处理?
- 这里的兼容性指不同的浏览器支持的特性,比如css特性,js语法,之间的兼容性
-
在很多的脚手架配置中,都能看到类似于这样的配置信息
-
这里的百分之一,就是指市场占用率
> 1% // 市场占有率超过 1% last 2 versions // 最后两个版本 not dead // 在24个月内有进行维护
-
浏览器市场占有率
-
我们可以在哪些可以查询到浏览器的市场占有率
- 这个好用的网站,也就是我们工具通常会查询的一个网站就是caniuse
认识browserslist工具
-
如何可以在css兼容性和js兼容性下共享我们配置的兼容性条件
- browserslist
-
browserslist是什么?
- browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置
browserslist编写规则
- defaults:browserslist的默认浏览器
- not ie <= 8:排除前先查询选择的浏览器
- 5% :通过全局使用情况统计信息选择的浏览器版本。
- dead:24个月内没有官方支持或更新的浏览器。
命令行使用browserslist
npx browserslist ">1%, last 2 version, not dead"
配置browserslist
-
我们可以通过两种方案配置browserslist
- 方案一:在package.json文件中配置
- 方案二:单独的一个配置文件 .browserslistrc文件
-
方案一:package.json配置
{ "browserslist": [ "last 2 version", "not dead", "> 0.2.%" ] } -
方案二:.browserslistrc文件
> 0.5% last 2 version not dead
如果没有配置,那么也会有一个默认配置
browserslist.defaults = [
'> 0.5%',
'last 2 version',
'Firefox ESR',
'not dead'
]
认识PostCSS工具
什么是PostCSS?
- PostCSS是一个通过JavaScript来转换样式的工具
- 这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀,css样式的重置
在webpack使用PostCSS工具
-
安装postcss-loader npm install postcss-loader -D
-
安装postcss的插件,postcss-preset-env npm install postcss-preset-env -D
-
配置
module.exports = { // ... 省略 module: { rules: [ { test: /.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1 // 配置在css-loader之前有多少个加载器 } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ require('postcss-preset-env') ] } } } ] } ] } }
单独的postcss配置文件
-
我们可以将postcss的配置信息放到一个单独的文件
-
在根目录下创建postcss.config.js
module.exports = { plugins: [ require('postcss-preset-env') ] }
加载和处理其他资源
加载图片资源
-
在项目中引入图片,有两种常见的方式
- img元素:设置src属性
- 其他元素:设置background-image的css属性
import zznh from './img/zznh.png'
const zznhImg = new Image()
zznhImg.src = zznh
element.appendChild(zznhImg)
- 不管是通过css还是img标签引入的图片在进行打包时都会报错
file-loader
-
要处理jpg,png等格式的图片,我们也需要有对应的loader:file-loader
- file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的文件夹中
-
安装file-loader : npm install file-loader -D
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.(png|jpe?g|gif|svg)$/i,
use: {
loader: 'file-loader'
}
}
]
}
}
- 文件的名称规则
-
-
有时候我们处理后的文件名称需要按照一定的规则进行显示:
- 比如保留原来的文件名,扩展名,同时为了防止重复,包含一个hash值等
-
这个时候我们可以使用PlaceHolders(占位符) 来完成
-
常见的placeHolder
- [ext] : 处理文件的扩展名
- [name] : 处理文件的名称
- [hash] : 文件的内容,使用MD4的散列函数处理,生成一个128位的hash值(32个16进制)
- [contentHash] : 在file-loader中和[hash]结果是一致的(在其他的地方会不一样)
- [hash: < length >] : 截取hash的长度,默认32个字符太长了
- [path] : 文件相对于webpack配置文件的路径
-
- 设置文件名称
module.exports = {
...
module: {
rules: [
{
test: /.(jpe?g|png|svg|gif)$/i,
use: {
loader: 'file-loader',
options: {
// 这样配置后打包的图片资源会放在img文件夹下
name: 'img/[name].[hash:8].[ext]'
// 下面的写法也可以让打包后的文件保存在img文件夹下
outputPath: 'img', // 设置打包后存放的文件夹
name: '[name].[hash:8].[ext]'
}
}
}
]
}
}
url-loader
- url-loader和file-loader的工作方式是相似的,但是可以将较小的文件,转成base64的URI
- 安装url-loader npm install url-loader -D
- 配置url-loader (这样配置打包后显示结果是一样的,但是我们会看不到打包后的图片文件,这是因为默认情况下,url-loader会将所有的图片文件转成base64编码)
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build')
},
module: {
rules: [
{
test: /.(jpg|svg|gif|png)$/i,
use: {
loader: 'url-loader',
options: {
name: '[name].[hash:8].[ext]',
outputPath: 'img'
}
}
}
]
}
}
-
url-loader的limit
-
开发中我们往往是小的图片需要转化,但是大的图片直接使用即可
- 这是因为小的图片转换base64之后可以和页面一起请求,减少不必要的请求过程
- 而大的图片也进行转换,反而会影响页面的请求速度
-
-
通过limit属性,可以设置转换限制
module.exports = {
// ... 省略
module: {
rules: [
test: /.(png|jpg|svg|gif)$/,
use: {
loader: 'url-loader',
options: {
outputPath: 'img',
name: '[name].[hash:8].[ext]',
limit: 100 * 1024 // 小于100kb的会被转换为base64,大于则不会
}
}
]
}
}
asset module type的介绍
-
当前我们使用的webpack版本是webpack5
- 在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader,url-loader
- 在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代loader
-
资源模块类型,通过添加4种新的模块类型,来替代所有的这些loader
- asset/resource:发送一个单独的文件并导出URL。之前通过使用file-loader实现
- asset/inline:导出一个资源的dataURI。之前通过使用url-loader实现
- asset/soruce:导出资源的源代码。之前通过raw-loader实现
- asset: 在导出一个data URI和发送一个单独的文件之间自动选择。之前通过url-loader,并且配置资源体积限制实现
asset module type的使用
- 加载图片,我们可以使用下面的方式
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.(jpe?g|svg|png|gif)$/,
type: 'asset/resource'
}
]
}
}
-
如何可以自定义文件的输出路径和文件名呢?
- 方式一:修改output,添加assetModuleFilename属性
- 方式二:在rule中,添加一个generator属性,并且设置filename
// 方式一
module.exports = {
output: {
filename: 'js/bundle.js',
path: path.resolve(__dirname, './dist'),
assetModuleFilename: 'img/[name].[hash:6][ext]' // 这里的[ext]包含了 . 所以不用加.
}
}
module.exports = {
// ... 省略
module: {
rules: [
{
test: /.(jpe?g|svg|png|gif)$/,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:6][ext]'
}
}
]
}
}
url-loader的limit效果
- 将type改为 "asset"或者"asset/inline"
- 添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性
// ... 省略
rules: [
{
test: /.(jpe?g|svg|png|gif)$/,
type: 'asset',
generator: {
filename: 'img/[name].[hash:6][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
}
]
认识plugin
-
webpack的另一个核心是plugin
-
loader和plugin有什么区别:
- loader是用于特定模块类型进行转换
- plugin可以用于执行更加广泛的任务,比如打包优化,资源管理,环境变量注入等等
CleanWebpackPlugin
- 在前面我们每修改配置重新打包时,都需要手动删除dist文件夹
- CleanWebpackPlugin这个插件可以帮助我们完成这个操作
安装CleanWebpackPlugin npm install clean-webpack-plugin -D
- 配置插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
// ... 省略
plugins: [
new CleanWebpackPlugin()
]
}
HtmlWebpackPlugin
- 打包后的文件夹是没有html文件的
- 在项目部署时,必然也是需要有对应的入口文件index.html
- 所以我们也需要对index.html进行打包处理
- 对html进行打包处理我们可以使用另外一个插件HtmlWebpackPlugin
安装HtmlWebpackPlugin npm install html-webpack-plugin -D
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ... 省略
plugins: [
new HtmlWebpackPlugin({
title: 'webpack案例',
template: './public/index.html'
})
]
}
DefinePlugin
- DefinePlugin允许在编译时创建配置的全局常量,是一个webpack内置的插件(不需要单独安装)
const { DefinePlugin } = require('webapck')
module.exports = {
// ... 省略
plugins: [
new DefinePlugin({
BASE_URL: "'./'" // 这里如果只写一个双引号,会将双引号里的内容当成一个变量去寻找
})
]
}
CopyWebpackPlugin
-
在vue打包过程中,如果我们将一些文件放到public的目录下,那么这个文件夹会被复制到dist文件夹中
- 这个复制的功能,我们可以使用CopyWebpackPlugin来完成
-
安装CopyWebpackPlugin插件
- npm install copy-webpack-plugin -D
-
接下来配置CopyWebpackPlugin即可:
- 复制的规则在patterns中设置
- from:设置从哪一个源中开始复制
- to:复制到的位置,可以省略,会默认复制到打包的目录下
- globOptions:设置一些额外的选项,其中可以编写需要忽略的文件
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
// ... 省略
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'public',
globOptions: {
ignore: [
'**/index.html'
]
}
}
]
})
]
}
source-map
mode配置
-
mode配置选项,可以告知webpack使用响应式模块的内置优化
- 默认值:production
- 可选值: 'none' | 'development' | 'production'
-
设置完mode选项后,webpack会默认帮助我们配置很多选项、
认识source-map
-
我们的代码运行在浏览器上,是通过打包压缩的
- 也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的
- 但是,当代码报错需要调试时,调试转换后的代码是很困难的
-
如何可以调试这种转换后不一致的代码呢?答案就是source-map
- source-map是从已转换的代码,映射到原始的源文件
- 使浏览器可以重构原始源并在调试器中显示重建的原始源
如何使用source-map
-
根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map
-
在转换后的代码,最后添加一个注释,它指向source-map
//# sourceMappingURL=common.bundle.js.map
-
浏览器会根据我们的注释,查找响应的source-map,并且根据source-map还原我们的代码
分析source-map
目前的source-map文件大概是原始文件的两倍
目前的source-map长什么样子?
- version:当前使用的版本
- sources: 从哪些文件转换过来的source-map和打包的代码(最初始的文件)
- names:转换前的变量和属性名称(如果使用的是development模式,则不需要保留转换前的名称)
- mappings:source-map用来和源文件映射的信息
- file:打包后的文件(浏览器加载的文件)
- sourceContent:转换前的具体代码信息(和sources是对应关系)
- sourceRoot:所有sources相对的根目录
生成source-map
-
通过devtool:设置生成source-map
-
不生成source-map的值:
-
false:不使用source-map,也就是没有任何source-map相关的内容
-
none:production模式下的默认值,不生成source-map
-
eval:development模式下的默认值,不生成source-map
- 但是它会在eval执行的代码中,添加 // sourceURL=;
- eval的效果图如下
-
生成source-map值:
-
source-map:
- 生成一个独立的source-map文件,并且在打包后的js文件中有一个注释,指向source-map文件
-
eval-source-map:
- 会生成source-map,但是source-map是以DataURL添加到eval函数的后面
-
inline-source-map:
- 会生成source-map,但是source-map是以DataUrl添加到打包后的js文件中
-
cheap-source-map:
- 和设置source-map效果差不多,会生成source-map,但是会更加高效一些,因为它没有生成列映射
- 在开发中,我们只需要行信息通常就可以定位到错误了
-
cheap-module-source-map:
- 会生成source-map,类似于cheap-source-map,但是对源自loader的source-map处理会更好
- 如果loader对我们源码进行了特殊的处理,比如babel
-
hidden-source-map
- 会生成source-map,但是不会对source-map文件进行引用
- 相当于删除了打包后文件中对source-map的引用注释
- 如果我们手动添加进来,那么source-map就会生效
-
nosources-source-map
- 会生成source-map,但是生成的source-map只有错误信息的提示,不会生成源代码文件
多个值的组合
-
事实上,webpack提供给我们26个值,是可以进行多组合的
-
组合规则如下:
- inline-|hidden-|eval:三个值选一
- nosources:可选值
- cheap可选值,并且可以跟随module的值
在开发中,最佳的实践是什么?
-
开发阶段:推荐使用source-map或cheap-module-source-map
- 这分别是vue和react使用的值
-
测试阶段:和开发阶段一致
- 因为测试阶段我们也希望在浏览器下看到正确的错误提示
-
发布阶段:false,不写
Babel
为什么需要babel?
- 在开发中我们想要使用ES6+语法,想要使用Typescript,想要编写jsx,都是离不开babel的
babel是什么?
- babel是一个工具链,主要用于将一些浏览器不认识的语法转换为浏览器能够运行的js代码
- 包括:语法转换,代码转换,polyfill:将浏览器没有的新语法进行打补丁
babel命令行使用
-
babel本身可以作为一个独立的工具(和postcss一样),可以单独使用
-
如果我们希望在命令行尝试使用babel,需要安装如下库:
- @babel/core :babel的核心代码,必须安装
- @babel/cli:可以让我们在命令行中使用(如果只需要在webpack中使用可以不安装)
- npm install @babel/cli @babel/core
-
使用babel来处理我们的源代码:
- src:源文件的目录
- --out-dir:指定要输出的文件夹dist
- npx babel src --out-dir dist (--plugins=“插件名”)(--presets=@babel/preset-env)
babel的底层原理
-
babel是如何将我们的一段代码转换成另外一段代码的呢?
- 从一种源代码转换成另一种源代码(目标语言),这是编译器的工作
- 事实上我们可以将babel看成是一个编译器
-
babel也拥有编译器的工作流程
- 解析阶段(parsing)
- 转换阶段 (transformation)
- 生成阶段 (code generator)
babel-loader
-
在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用,比如在webpack中
-
需要安装相关依赖
- npm install babel-loader @babel/core -D
// 省略...
module: {
rules: [
{
test: /.m?js$/,
use: {
loader: 'babel-loader'
}
}
]
}
指定babel使用插件
- 如果没有使用插件,babel是不会对我们的代码进行转换的
// 省略...
module: {
rules: [
{
test: /.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-block-scoping', // 转换块级作用域的
'@babel/plugin-transform-arrow-functions' // 转换箭头函数的
]
}
}
}
]
}
babel-preset
-
在开发中我们要转换的代码,如果需要一个一个去安装插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并将其传递给babel
-
常见的预设
- env
- react
- Typescript
-
安装preset-env
npm install @babel/preset-env -D
- 配置
// 省略 ...
{
test: /.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
设置目标浏览器
// 省略 ...
{
test: /.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: ['last 2 version', 'not dead']
}]
]
}
}
}
- 在前面中我们已经使用了browserslist工具,我们可以对比一下不同的配置,打包的区别
- 如果我们同时设置了targets和browserslist,targets属性会覆盖browserslist
- 在开发中,更推荐通过browserslist来配置,因为类似于postcss工具,与会使用browserslist,进行统一的浏览器适配
babel的配置文件
-
我们可以将babel的配置文件放到一个独立的文件中,babel给我们提供了两种配置文件的编写
- babel.config.json(或者.js, .cjs, .mjs)文件
- .babelrc.json(或者.js, .cjs, .mjs)文件
-
它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);
- babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
- babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐;
认识polyfill
-
polyfill是什么?
- 像是填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript
-
为什么需要polyfill?
- 比如在开发中我们用到了一些新特性,比如Promise,Symbol等
- 但是浏览器根本不认识这些新特性,所以直接运行必然会报错
- 这个时候我们就可以使用polyfill来填充或者说打一个补丁,那么浏览器就能运行该代码了
如何使用polyfill?
- 在babel7.4.0之前(目前7.15.0),可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了
- babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill
npm install core-js regenerator-runtime
{
test: /.m?js$/,
exclude: /node_modules/, // 使用babel时,一般不会包含node_modules下的文件
use: 'babel-loader'
}
使用polyfill,babel.config.js的配置
-
我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性
-
useBuiltIns:设置以什么方式来使用polyfill
-
false
- 打包后的文件不使用polyfill来进行适配
- 并且这个时候是不需要设置corejs属性的
-
usage
- 会根据源代码出现的语言特性,自动检测所需要的的polyfill
- 这样可以确保最终包里的polyfill数量最小化,打包的包相对会小一些
- 可以设置corejs属性来确定使用的corejs版本
-
// babel.config.js module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3.17 }] ] }
-
entry
- 如果我们依赖的某一个库使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错
- 所以如果担心发生这种情况,可以使用entry
- 并且需要在入口文件添加import 'core-js/stable'; import 'regenerator-runtime/runtime'
- 这样做会根据browserslist目标导入所有的polyfill,但是对应的包也会变大
-
// babel.config.js module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'entry', corejs: 3.17 }] ] } // 入口文件 import 'core-js/stable' import 'regenerator-runtime/runtime'
-
-
corejs:设置corejs的版本,目前使用的较多的是3.x版本,目前安装的默认版本是3.17.3
- 另外corejs可以设置是否对提议阶段的特性进行支持
- 设置proposals属性为true即可
认识plugin-transform-runtime
-
前面我们使用的polyfill,默认情况时添加的所有属性都是全局的
- 如果我们正在编写一个工具库,这个工具库需要使用polyfill
- 别人在使用我们的工具是,工具库通过polyfill添加的特性,可能会污染它们的代码
- 所以,当编写工具库时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime来完成polyfill的功能
-
使用plugin-transform-runtime
- npm install @babel/plugin-transform-runtime -D
- npm install @babel/runtime-corejs3
module.exports = {
presets: [
['@babel/preset-env']
],
plugins: [
['@babel/plugin-transform-runtime', {
}]
]
}
打包react代码
-
在编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的
-
对react jsx代码进行处理需要如下插件
- @babel/plugin-syntax-jsx
- @babel/plugin-transform-react-jsx
- @babel/plugin-transform-react-display-name
-
但是开发中,我们不需要一个个安装这些插件,我们依然可以使用preset来配置:
- npm install @babel/preset-react -D
module.exports = { presets: [ ['@babel/preset-react'] ] }
TypeScript的编译
-
可以通过typescript的compiler来转换成JavaScript
- npm install typescript -D
- 另外typescript的编译配置信息,我们通常会写一个tsconfig.json文件 tsc --init
- 通过运行 npx tsc来编译自己的ts代码
ts-loader
- 如果希望在webpack中使用typescript,那么我们可以使用ts-loader来处理ts文件
-
{ test: /.ts$/, exclude: /node_modules/, use: [ 'ts-loader' ] }
使用babel-loader编译typescript代码
- 安装@babel/preset-typescript npm install @babel/preset-typescript -D
{
test: /.ts$/,
exclude: /node_modules/,
use: 'babel-loader'
}
// babel.config.js
module.exports = {
presets: [
['@babel/preset-typescript']
]
}
ts-loader 和 babel-loader选择
-
在开发中我们应该选择ts-loader还是babel-loader
-
使用ts-loader
- 只能将ts转换成js
- 如果我们还希望在这个过程中添加对应的polyfill,ts-loader是无法完成的
-
使用babel-loader
- 可以将ts转为js,并且也能实现polyfill的功能
- 但是babel-loader在编译过程中,不会对类型错误进行检测
-
也就是说,我们可以使用babel来完成代码转换,使用tsc来进行类型检测
-
在package.json文件中的scripts中添加两个脚本,用于类型检测
-
-
我们执行 npm run type-check可以对ts代码的类型进行检测
-
执行npm run type-check-watch可以实时的检测类型错误
-
但是在vscode中,但我们写ts代码时,编译器会自动帮助我们进行代码检测
-
加载vue2文件(加载vue3文件看vue3笔记)
-
安装相关依赖
- npm install vue-loader -D
- npm install vue-template-compiler -D
-
配置webpack
// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
// 省略...
module: {
rules: [
{
test: /.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
DevServer和HMR
为什么要搭建本地服务器
-
目前开发的代码,为了能够运行需要两个操作
- npm run build,编译相关的代码
- 通过live server或者浏览器打开index.html代码,查看效果
-
这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成编译和展示
-
为了完成自动编译,webpack提供了几种可选的方式
- webpack watch mode
- webpack-dev-server
- webpack-dev-middleware
webpack watch
-
webpack提供了watch模式
- 在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译
- 我们不需要手动去运行npm run build指令了
-
如何开启watch?两种方式
- 直接在webpack.config.js 文件中,添加 watch: true
- 在启动webpack的命令中,添加--watch的标识
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}
webpack-dev-server
-
使用watch的方式可以监听到文件的变化,但是事实上它本身没有自动刷新浏览器的功能
- 在vscode中使用live-server可以实现浏览器自动刷新的功能
- 但是,我们希望在不使用live-server的情况下,可以具备live reloading(实时重新加载)的功能
-
安装webpack-dev-server npm install webpack-dev-server -D
-
添加一个新的scripts脚本
"serve": "webpack serve"
-
webpack-dev-server 在编译后不会写入到任何输出文件,而是将bundle文件保存到内存中
- 事实上webpack-dev-server使用了一个库叫memfs(memory-fs webpack自己写的)
webpack-dev-middleware
-
默认情况下,webpack-dev-server已经帮助我们做好了一切
- 比如通过express开启一个服务,比如HMR(热模块替换)
- 如果我们想要有更好的自由度,可以使用webpack-dev-middleware
-
什么是webpack-dev-middleware
- webpack-dev-middleware是一个封装器,可以把wepack处理过的文件发送到一个server
- webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义设置
-
安装express和webpack-dev-middleware npm install webapck-dev-middle express -D
-
创建一个js文件(例如:server.js)
const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
const app = express()
// 记载配置信息
const config = require('你的webpack配置文件的路径')
// 将配置信息传递给webpack进行编译
const compiler = webpack(config)
// 将编译后的结果传递给webpackDevMiddleware这个中间件处理
app.use(webpackDevMiddleware(compiler))
app.listen(8000, () =>{
console.log('服务器已经在8000端口启动')
})
- 通过node server.js 启动服务器
认识模块热替换(HMR)
-
什么是HMR?
- HMR的全称是Hot Module Replacement,翻译为热模块替换
- 热模块替换是指在应用程序运行过程中,替换,添加,删除模块,而无需重新刷新整个页面
-
HMR通过如下几种方式,来提高开发的速度
- 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
- 只更新需要变化的内容,节省开发的时间
- 修改了css,js源码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式
-
如何使用HMR呢?
- 默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可
- 在不开启HMR的情况下,当我们修改了源代码之后,整个页面会重新自动刷新,使用的是 live reloading(实时重新加载)
-
如何开启HMR?
// 在webpack.config.js文件中修改
module.exports = {
mode: '',
entry: '',
// ...
devServer: {
hot: true // 开启HMR
}
}
-
但是会发现,当我们修改了某一个模块的代码时,依然刷新的整个页面
- 这是因为我们需要去指定哪些模块发生更新时,进行HMR
// 在webpack打包的入口文件中,在其他js文件中这样写是不能开启HMR的
if (module.hot) {
module.hot.accept('你想要进行HMR的文件', () => {
console.log('文件更新了')
})
}
框架的HMR
-
开发vue或者react项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
- 事实上vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验
- 比如react开发中,有React Hot Loader,实时调整react组件(目前官方已经弃用了,改成react-refresh)
react的HMR
- react是借助react-refresh来实现的
- 安装依赖 npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D
- 修改webpack.config.js文件和babel.config.js文件
// webpack.config.js文件
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
// ...其他配置
plugins: [
new ReactRefreshWebpackPlugin() // 执行npm run build时,不能有该插件,否则会报错
]
}
// babel.config.js 文件
module.exports = {
plugins: [
['react-refresh/babel']
]
}
vue的HMR
- vue的加载我们需要vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR处理
- 安装加载vue所需要的的依赖 npm install vue-loader vue-template-compiler -D
- 配置webpack.config.js
// webpack.config.js文件
// 有两种方法导入VueLoaderPlugin,本质上都是一样的
// const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
// ...
module: {
rules: [
{
test: /.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
HMR的原理
-
HMR的原理是什么呢?如何可以做到只更新一个模块中的内容呢?
- webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket)
- express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
-
HMR Socket Server,是一个socket的长连接
- 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端)
- 当服务器监听到对应的模块发生变化时,会生成两个文件 .json(manifest文件) 和 .js文件(update chunk)
- 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器)
- 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新
output的publicPath和devServer的publicPath
-
output中的publicPath属性,该属性是指定index.html文件打包引用的一个基本路径
- 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是bundle.js
- 在开发中,我们也将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名加路径去请求对应的资源
- 如果我们希望在本地直接打开html文件来运行,会将其设置为 ./ ,路径为 ./bundle.js,可以根据相对路径去寻找资源
-
devServer中的publicPath,该属性时指定本地服务所在的文件夹
- 它的默认值是 / ,也就是我们直接访问端口即可访问其中的资源
- 如果我们将其设置为 /abc,那么我们需要通过**http://locallhost:8080/abc**才能访问到对应的打包后的资源
- 并且这个时候,我们其中的bundle.js通过**http://locallhost:8080/bundle.js是无法访问的
- 所以建议将output中的publicPath和devServer中的publicPath设置为相同
devServer常见的配置
contentBase
- 这个属性对于打包后的资源是没有什么作用的,它的作用是在devServer创建一个本地服务时,如果我们打包后的资源又依赖其他的资源,那么就需要指定从哪里来查找这个内容
- 例如在给html-webpack-plugin的模板HTML中有引用其他资源
- 通过npm run serve 后会发现这个资源加载不到,默认情况下会在启动项目的根目录进行查找
- 而根目录下并没有aaa.js文件,该文件是被我放在src目录下,所以加载不了该文件
- 如果我们想要能加载aaa.js这个文件,这个时候就可以通过设置contentBase来实现
module.exports = {
// ...
devServer: {
contentBase: './src'
}
}
- 重新执行npm run serve
- 这个时候查找aaa.js文件时,会去根目录下的src文件夹查找,而aaa.js文件就在src文件夹下,所以能加载到aaa.js文件
watchContentBase
- 默认情况下,webpack-dev-server是不会监听上面aaa.js文件的修改,如果想要能监听这个文件的修改,就可以将watchContentbase设置为true,
- 设置完后,当修改了aaa.js文件后,webpack-dev-server会自动帮助我们刷新浏览器
hotOnly,host配置
-
hotOnly是当代码编译失败时,是否刷新整个页面
- 默认情况下当代码编译失败修复后,我们会重新刷新整个页面
- 如果不希望重新刷新整个页面,可以设置hotOnly为true
-
host设置主机地址
- 默认值是localhost
- 如果希望其他地方也可以访问,可以设置0.0.0.0
module.exports = {
// ...
devServer: {
hotOnly: true,
host: '0.0.0.0'
}
}
port,open,compress
-
port设置监听的端口号,默认情况是8080
-
open是否打开浏览器
- 默认值是false,设置为true时会自动打开浏览器
- 也可以设置为类似于Google Chrome等值
-
compress是否为静态文件开启gzip compression
- 默认值是false,可以设置为true
module.exports = {
// ...
devServer: {
open: true,
port: 8000,
compress: true
}
}
proxy代理
-
proxy是我们开发中非常常用的一个配置选项,它的目的是设置代理来解决跨域访问的问题。(浏览器同源策略:当两个url的协议,主机,端口号都相同时,则这两个url为同源,可以互相发送网络请求。当三个其中一个不同时,则会产生跨域问题)
- 我们可以将请求先发送到一个代理服务器,代理服务器和API服务器没有跨域问题,就可以解决我们的跨域问题了
module.exports = {
// ...
devServer: {
proxy: {
'/api': {
target: '你要请求的baseUrl',
pathRewrite: { // 路径重写
'^/api': ''
},
secure: false, // 为false时可以代理没有证书的https
changeOrigin: true, // 设置为true时,客户端发出的请求头端口会和服务器一致,如果为false端口就是客户端的端口,有的服务器会检查端口,如果端口不一致,不返回数据
}
}
}
}
- 发送请求时url变为: "api/参数"
historyApiFallback
-
historyApiFallback是开发中一个非常常见的属性,它的主要作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误
-
boolean值:默认是false
- 如果设置为true,那么在刷新时,返回404错误时,会自动返回index.html的内容
-
object类型的值,可以配置rewrites属性
- 可以配置from来配置路径,决定要跳到哪一个页面
module.exports = {
// ...
devServer: {
// historyApiFallback: true,
historyApiFallback: {
rewrites: [
{ from: /^/$/, to: '/views/landing.html' },
{ from: /^/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' },
],
}
}
}
resolve模块解析
-
webpack能解析三种文件路径:
-
绝对路径:
- 由于已经获的文件的绝对路径,因此不需要再做进一步的解析
-
相对路径
- 在这种情况下,使用import或require的资源文件夹所处的目录,被认为是上下文目录
- 在imort/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径
-
模块路径:
-
在resolve.modules中指定的所有目录检索模块
- 默认值是['node_modules'] ,所以默认会从node_modules中查找文件
-
可以通过设置别名的方式来替换初始模块路径
-
-
webpack查找路径时,确定是文件还是文件夹
-
如果是一个文件:
- 如果文件具有扩展名,则直接打包文件
- 否则,将使用resolve.extensions选项作为文件扩展名解析
-
如果是一个文件夹
-
会在文件夹中根据resolve.mainFiles配置选项中指定的文件顺序查找;
- resolve.mainFiles的默认值是['index'],这也就是为什么我们路径是一个文件夹时,会指定去查找该文件夹下的index类型文件
- 在根据resolve.extensions来解析扩展名
-
extensions和alias配置
-
extensions是解析到文件时自动添加扩展名
- 默认值是: ['.wasm', '.mjs', '.js', 'json']
- 所以如果我们的代码中想要添加加载 .vue,或者.jsx或者.ts等文件时,我们必须自己写上扩展名
-
另一个非常好用的功能是配置别名alias
- 当我们的项目目录结构比较深时,获取一个文件的路径可能需要 ../../../ 这种路径片段
- 所以我们可以给某些常见的路径起一个别名
const path = require('path')
module.exports = {
// ...
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.ts', '.vue'],
alias: {
"@": path.resolve(__dirname, './src')
}
}
}