赞
踩
无论是作为专业的开发者还是接触互联网的普通人,其实都能深刻的感知到Web前端的发展是非常快速的
对于开发者来说我们会更加深有体会;
从后端渲染的JSP、PHP,到前端原生JavaScript,再到jQuery开发,再到目前的三大框架Vue、React、Angular;
开发方式也从原来的JavaScript的ES5语法,到ES6、7、8、9、10,到TypeScript,包括编写CSS的预处理器less、scss等;
前端开发的复杂化
前端开发目前我们面临哪些复杂的问题呢?
目前前端流行的三大框架:Vue、React、Angular
webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序;
webpack is a static module bundler for modern JavaScript applications.
我们来对上面的解释进行拆解:
打包bundler:webpack可以将帮助我们进行打包,所以它是一个打包工具
静态的static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
模块化module:webpack默认支持各种模块化开发,ES Module、CommonJS、AMD等;
现代的modern:我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;
Webpack 是一个前端资源加载和打包工具。所谓的模块就是在平时的前端开发中,用到一些静态资源,如JavaScript、CSS、图片等文件,webpack就将这些静态资源文件称之为模块。 webpack支持AMD和CommonJS,以及其他的一些模块系统,并且兼容多种JS书写规范,可以处理模块间的依赖关系,所以具有更强大的JS模块化的功能,它能对静态资源进行统一的管理以及打包发布。
作为一款 Grunt和Gulp的替代产品,Webpack受到大多数开发者的喜爱,因为它能够编译打包CSS,做CSS预处理,对JS的方言进行编译,打包图片,代码压缩等等。
与其他的构建工具相比,Webpack具有如下的一些优势:
在Webpack的世界里有两个最核心的概念:
一切皆模块
正如js文件可以是一个“模块(module)”一样,其他的(如css、image或html)文件也可视作模 块。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。这意味着我们可以将事物(业务)分割成更小的易于管理的片段,从而达到重复利用等的目的。
按需加载
传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。但是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会导致应用一直处于加载中状态。因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载。
Webpack官网图片
工作中的webpack
我们来提一个问题:webpack会被vite取代吗?
但是目前vite取代webpack还有很长的路要走
vite的核心思想并不是首创
webpack的更新迭代
关于vite的思考
我的个人观点:
学习任何的东西,重要的是学习核心思想:
任何工具的出现,都是更好的服务于我们开发:
无论是vite的出现,还是以后新的工具的出现,不要有任何排斥的思想;
我们要深刻的明白,工具都是为了更好的给我们提供服务;
不可能出现了某个工具,让我们的开发效率变得更低,而这个工具却可以变得非常流行,这是不存在的;
webpack的官方文档是https://webpack.js.org/
点击DOCUMENTATION来到文档页:
Webpack的运行是依赖Node环境的,所以我们电脑上必须有Node环境
Webpack的安装
webpack的安装目前分为两个:webpack、webpack-cli
那么它们是什么关系呢?
npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
传统开发存在的问题
我们的代码存在什么问题呢?某些语法浏览器是不认识的(尤其在低版本浏览器上)
显然,上面存在的问题,让我们在发布静态资源时,是不能直接发布的,因为运行在用户浏览器必然会存在各种各样的兼容性问题。
我们可以通过webpack进行打包,之后运行打包之后的代码
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
在通常情况下,webpack需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的
我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path');
module.exports = {
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist")
}
}
继续执行webpack命令,依然可以正常打包
指定配置文件
但是如果我们的配置文件并不是webpack.config.js的名字,而是其他的名字呢?
比如我们将webpack.config.js修改成了 wk.config.js;
这个时候我们可以通过 --config 来指定对应的配置文件;
webpack --config wk.config.js
但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在package.json中增加一个新的脚本:
{
"scripts": {
"build": webpack --config wk.config.js
}
}
之后我们执行 npm run build来打包即可。
webpack到底是如何对我们的项目进行打包的呢?
事实上webpack在处理应用程序时,它会根据命令或者配置文件找到入口文件;
从入口开始,会生成一个 依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如.js文件、css文件、图片、字体等);
然后遍历图结构,打包一个个模块(根据文件的不同使用不同的loader来解析);
webpack默认打包
我们可以通过webpack进行打包,之后运行打包之后的代码
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
这个文件中的代码被压缩和丑化了;
我们暂时不关心他是如何做到的,后续我讲webpack实现模块化原理时会再次讲到;
另外我们发现代码中依然存在ES6的语法,比如箭头函数、const等,这是因为默认情况下webpack并不清楚我们打包后的文件是否需要转成ES5之前的语法,后续我们需要通过babel来进行转换和设置;
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
事实上,当我们运行webpack时,webpack会查找当前目录下的 src/index.js作为入口;
所以,如果当前项目中没有存在src/index.js文件,那么会报错;
当然,我们也可以通过配置来指定入口和出口
npx webpack --entry ./src/main.js --output-path ./build
Webpack配置文件
在通常情况下,webpack需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的
我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path');
module.exports = {
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist")
}
}
继续执行webpack命令,依然可以正常打包
output里面的path 不能是相对路径,否则会报错
指定配置文件
但是如果我们的配置文件并不是webpack.config.js的名字,而是其他的名字呢?
比如我们将webpack.config.js修改成了 wk.config.js;
这个时候我们可以通过 --config 来指定对应的配置文件;
webpack --config wk.config.js
但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在package.json中增加一个新的脚本:
"scripts": {
"build": "webpack --config wk.config.js"
},
之后我们执行 npm run build来打包即可。
Webpack依赖图
webpack到底是如何对我们的项目进行打包的呢?
事实上webpack在处理应用程序时,它会根据命令或者配置文件找到入口文件;
从入口开始,会生成一个 依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如.js文件、css文件、图片、字体等);
然后遍历图结构,打包一个个模块(根据文件的不同使用不同的loader来解析);
注:没有被引入的模块、资源,不会被打包(npm install 的node_modules下的资源也一样)
编写案例代码
我们创建一个component.js
// component.js
import "../css/index.css";
import "../css/component.less";
function component() {
const element = document.createElement("div");
element.innerHTML = ["Hello", "Webpack"].join(" ");
element.className = "content";
return element;
}
document.body.appendChild(component());
// index.css
@import "./test.css";
.demo {
color: red;
}
继续编译命令npm run build
css-loader
css-loader的使用
上面的错误信息告诉我们需要一个loader来加载这个css文件,但是loader是什么呢?
loader 可以用于对模块的源代码进行转换;
我们可以将css文件也看成是一个模块,我们是通过import来加载这个模块的;
在加载这个模块时,webpack其实并不知道如何对其进行加载,我们必须制定对应的loader来完成这个功能;
那么我们需要一个什么样的loader呢?
对于加载css文件来说,我们需要一个可以读取css文件的loader;
这个loader最常用的是css-loader;
css-loader的安装:
npm install css-loader -D
css-loader的使用方案
如何使用这个loader来加载css文件呢?有三种方式:
**内联方式:**内联方式使用较少,因为不方便管理;
import "css-loader!../css/style.css"
CLI方式
在webpack5的文档中已经没有了–module-bind;
实际应用中也比较少使用,因为不方便管理;
loader配置方式
配置方式表示的意思是在我们的webpack.config.js文件中写明配置信息:
module.rules中允许我们配置多个loader(因为我们也会继续使用其他的loader,来完成其他文件的加载);
这种方式可以更好的表示loader的配置,也方便后期的维护,同时也让你对各个Loader有一个全局的概览;
module.rules的配置如下:
rules属性对应的值是一个数组:[Rule]
数组中存放的是一个个的Rule,Rule是一个对象,对象中可以设置多个属性:
test属性:用于对 resource(资源)进行匹配的,通常会设置成正则表达式;
use属性:对应的值时一个数组:[UseEntry]
loader属性: Rule.use: [ { loader } ] 的简写。
Loader的配置代码
const path = require('path'); module.exports = { entry: "./src/main.js", output: { filename: "bundle.js", // 必须是一个绝对路径 path: path.resolve(__dirname, "./build") }, module: { rules: [ { // 规则使用正则表达式 test: /\.css$/, // 匹配资源 use: [ // { loader: "css-loader" }, // 注意: 编写顺序(从下往上, 从右往做, 从后往前) "style-loader", { loader: "css-loader", options: { importLoaders: 1 } }, "postcss-loader" ], // loader: "css-loader" }, { test: /\.less$/, use: [ "style-loader", { loader: "css-loader", options: { importLoaders: 2 } }, "postcss-loader", "less-loader" ] } ] } }
style-loader
我们已经可以通过css-loader来加载css文件了
这是为什么呢?
因为css-loader只是负责将.css文件进行解析,并不会将解析之后的css插入到页面中;
如果我们希望再完成插入style的操作,那么我们还需要另外一个loader,就是style-loader;
安装style-loader:
npm install style-loader -D
配置style-loader
那么我们应该如何使用style-loader:
在配置文件中,添加style-loader;
注意:因为loader的执行顺序是从右向左(或者说从下到上,或者说从后到前的),所以我们需要将style-loader写到css-loader的前面;
use: [
// 注意: style-loader在css-loader之前
{ loader: "style-loader" },
{ loader: "css-loader"}
]
重新执行编译npm run build,可以发现打包后的css已经生效了:
当前目前我们的css是通过页内样式的方式添加进来的;
后续我们也会讲如何将css抽取到单独的文件中,并且进行压缩等操作;
less-loader
如何处理less文件?
在我们开发中,我们可能会使用less、sass、stylus的预处理器来编写css样式,效率会更高。
那么,如何可以让我们的环境支持这些预处理器呢?
比如我们编写如下的less样式:
@fontSize: 50px;
@fontWeight: 700;
.content {
font-size: @fontSize;
font-weight: @fontWeight;
}
Less工具处理
我们可以使用less工具来完成它的编译转换:
npm install less -D
执行如下命令:
npx less ./src/css/title.less > title.css
less-loader处理
但是在项目中我们会编写大量的css,它们如何可以自动转换呢?
npm install less less-loader -D
配置webpack.config.js
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' },
]
}
执行npm run build
less就可以自动转换成css,并且页面也会生效了
我们来思考一个问题:开发中,浏览器的兼容性问题,我们应该如何去解决和处理?
当然这个问题很笼统,这里我说的兼容性问题不是指屏幕大小的变化适配;
我这里指的兼容性是针对不同的浏览器支持的特性:比如css特性、js语法,之间的兼容性;
我们知道市面上有大量的浏览器:
有Chrome、Safari、IE、Edge、Chrome for Android、UC Browser、QQ Browser等等;
它们的市场占率是多少?我们要不要兼容它们呢?
其实在很多的脚手架配置中,都能看到类似于这样的配置信息:
> 1%
last 2 versions
not dead
浏览器市场占有率
但是在哪里可以查询到浏览器的市场占有率呢?
这个最好用的网站,也是我们工具通常会查询的一个网站就是caniuse;
https://caniuse.com/usage-table
browserslist
认识browserslist工具
但是有一个问题,我们如何可以在css兼容性和js兼容性下共享我们配置的兼容性条件呢?
就是当我们设置了一个条件: > 1%;
我们表达的意思是css要兼容市场占有率大于1%的浏览器,js也要兼容市场占有率大于1%的浏览器;
如果我们是通过工具来达到这种兼容性的,比如后面我们会讲到的postcss-prest-env、babel、autoprefixer等
如何可以让他们共享我们的配置呢?
Browserslist是什么?Browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置:
浏览器查询过程
我们可以编写类似于这样的配置:
> 1%
last 2 versions
not dead
那么之后,这些工具会根据我们的配置来获取相关的浏览器信息,以方便决定是否需要进行兼容性的支持:
Browserslist编写规则
那么在开发中,我们可以编写的条件都有哪些呢?(加粗部分是最常用的)
defaults:Browserslist的默认浏览器(> 0.5%, last 2 versions, Firefox ESR, not dead)。
5%:通过全局使用情况统计信息选择的浏览器版本。 >=,<和<=工作过。
dead:24个月内没有官方支持或更新的浏览器。现在是IE 10,IE_Mob 11,BlackBerry 10,BlackBerry 7, Samsung 4和OperaMobile 12.1。
last 2 versions:每个浏览器的最后2个版本。
last 2 Chrome versions:最近2个版本的Chrome浏览器。
last 2 major versions或last 2 iOS major versions:最近2个主要版本的所有次要/补丁版本。
node 10和node 10.4:选择最新的Node.js10.x.x 或10.4.x版本。
iOS 7:直接使用iOS浏览器版本7。
Firefox > 20:Firefox的版本高于20 >=,<并且<=也可以使用。它也可以与Node.js一起使用。
ie 6-8:选择一个包含范围的版本。
Firefox ESR:最新的[Firefox ESR]版本。
PhantomJS 2.1和PhantomJS 1.9:选择类似于PhantomJS运行时的Safari版本。
extends browserslist-config-mycompany:从browserslist-config-mycompanynpm包中查询 。
supports es6-module:支持特定功能的浏览器。 es6-module这是“我可以使用” 页面feat的URL上的参数。有关所有可用功能的列表,请参见 。caniuse-lite/data/features
browserslist config:在Browserslist配置中定义的浏览器。在差异服务中很有用,可用于修改用户的配置,例如 browserslist config and supports es6-module。
since 2015或last 2 years:自2015年以来发布的所有版本(since 2015-03以及since 2015-03-10)。
unreleased versions或unreleased Chrome versions:Alpha和Beta版本。
not ie <= 8:排除先前查询选择的浏览器。
命令行使用browserslist
我们可以直接通过命令来查询某些条件所匹配到的浏览器:
npx browserslist ">1%, last 2 version, not dead"
配置browserslist
我们如何可以配置browserslist呢?两种方案:
方案一:在package.json中配置;
方案二:单独的一个配置文件.browserslistrc文件;
方案一:package.json配置:
"browsweslist": [
"last 2 version",
"not dead",
"> 0.2%"
]
方案二:.browserslistrc文件
>1%
last 2 version
not dead
默认配置和条件关系
如果没有配置,那么也会有一个默认配置:
// Default browers query
browserslist.defaults = [
'> 0.5%',
'last 2 versions',
'Firefox ESR',
'not dead'
]
我们编写了多个条件之后,多个条件之间是什么关系呢?
认识PostCSS工具
什么是PostCSS呢?
PostCSS是一个通过JavaScript来转换样式的工具;
这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置;
但是实现这些工具,我们需要借助于PostCSS对应的插件;
如何使用PostCSS呢?主要就是两个步骤:
第一步:查找PostCSS在构建工具中的扩展,比如webpack中的postcss-loader;
第二步:选择可以添加你需要的PostCSS相关的插件;
命令行使用postcss
当然,我们能不能也直接在终端使用PostCSS呢?
我们可以安装一下它们:postcss、postcss-cli
npm install postcss postcss-cli -D
我们编写一个需要添加前缀的css:
https://autoprefixer.github.io/
我们可以在上面的网站中查询一些添加css属性的样式;
:fullscreen {
color: red;
}
.content {
user-select: none;
}
插件autoprefixer
因为我们需要添加前缀,所以要安装autoprefixer:
npm install autoprefixer -D
直接使用使用postcss工具,并且制定使用autoprefixer
npx postcss --use autoprefixer -o end.css ./src/css/style.css
转化之后的css样式如下:
:ms-fullscreen {
}
:fullscreen {
}
.content {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
单独的postcss配置文件
当然,我们也可以将这些配置信息放到一个单独的文件中进行管理:
module.exports = {
plugins: [
'postcss-preset-env'
]
}
postcss-preset-env
事实上,在配置postcss-loader时,我们配置插件并不需要使用autoprefixer。
我们可以使用另外一个插件:postcss-preset-env
postcss-preset-env也是一个postcss的插件;
它可以帮助我们将一些现代的CSS特性,转成大多数浏览器认识的CSS,并且会根据目标浏览器或者运行时环境添加所需的polyfill;
也包括会自动帮助我们添加autoprefixer(所以相当于已经内置了autoprefixer);
首先,我们需要安装postcss-preset-env:
npm install postcss-preset-env -D
之后,我们直接修改掉之前的autoprefixer即可:
plugins: [
require("postcss-preset-env")
]
注意:我们在使用某些postcss插件时,也可以直接传入字符串
module.exports = {
plugins: [
'postcss-preset-env'
]
}
举个例子
我们举一个例子:
我们这里在使用十六进制的颜色时设置了8位;
但是某些浏览器可能不认识这种语法,我们最好可以转成RGBA的形式;
但是autoprefixer是不会帮助我们转换的;
而postcss-preset-env就可以完成这样的功能;
.content {
color: $12345678;
}
为了演示我们项目中可以加载图片,我们需要在项目中使用图片,比较常见的使用图片的方式是两种:
img元素,设置src属性;
其他元素(比如div),设置background-image的css属性;
// import "css-loader!../css/index.css"; import "../css/index.css"; import "../css/component.less"; import zznhImage from "../img/zznh.png"; function component() { const element = document.createElement("div"); element.innerHTML = ["Hello", "Webpack"].join(" "); element.className = "content"; // 创建一个img元素,设置src属性 const imgEl = new Image(); // imgEl.src = require("../img/zznh.png").default; imgEl.src = zznhImage; element.appendChild(imgEl); // 创建一个div, 设置背景图片 const bgDivEl = document.createElement('div'); bgDivEl.style.width = 200 + 'px'; bgDivEl.style.height = 200 + 'px'; bgDivEl.className = 'bg-image'; bgDivEl.style.backgroundColor = "red"; element.appendChild(bgDivEl); // 创建一个i元素, 设置一个字体 const iEl = document.createElement("i"); iEl.className = "iconfont icon-ashbin why_icon"; element.appendChild(iEl); return element; } document.body.appendChild(component());
@import "./test.css"; @import "../font/iconfont.css"; .demo { color: red; } .bg-image { display: inline-block; background-image: url('../img/nhlt.jpg'); background-size: contain; } .why_icon { display: inline-block; font-size: 50px; color: red; }
这个时候,打包会报错
要处理jpg、png等格式的图片,我们也需要有对应的loader:file-loader
file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的文件夹中;
当然我们待会儿可以学习如何修改它的名字和所在文件夹;
安装file-loader:
npm install file-loader -D
配置处理图片的Rule:
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "file-loader"
}
}
文件的名称规则
有时候我们处理后的文件名称按照一定的规则进行显示:
这个时候我们可以使用PlaceHolders来完成,webpack给我们提供了大量的PlaceHolders来显示不同的内容:
https://webpack.js.org/loaders/file-loader/#placeholders
我们可以在文档中查阅自己需要的placeholder;
我们这里介绍几个最常用的placeholder:
[ext]: 处理文件的扩展名;
**[name]:**处理文件的名称;
**[hash]:**文件的内容,使用MD4的散列函数处理,生成的一个128位的hash值(32个十六进制);
**[contentHash]:**在file-loader中和[hash]结果是一致的(在webpack的一些其他地方不一样,后面会讲到);
**[hash:<length>
]:**截图hash的长度,默认32个字符太长了;
**[path]:**文件相对于webpack配置文件的路径;
设置文件名称
那么我们可以按照如下的格式编写:
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "file-loader",
options: {
name: "img/[name].[hash:8].[ext]"
}
}
}
设置文件的存放路径
当然,我们刚才通过 img/ 已经设置了文件夹,这个也是vue、react脚手架中常见的设置方式:
其实按照这种设置方式就可以了;
当然我们也可以通过outputPath来设置输出的文件夹;
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "file-loader",
options: {
name: "img/[name].[hash:8].[ext]",
outputPath: "img"
}
}
}
url-loader和file-loader的工作方式是相似的,但是可以将较小的文件,转成base64的URI。
安装url-loader:
npm install url-loader -D
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "url-loader",
options: {
name: "img/[name].[hash:8].[ext]",
outputPath: "img"
}
}
}
显示结果是一样的,并且图片可以正常显示;
但是在dist文件夹中,我们会看不到图片文件:
url-loader的limit
但是开发中我们往往是小的图片需要转换,但是大的图片直接使用图片即可
这是因为小的图片转换base64之后可以和页面一起被请求,减少不必要的请求过程;
而大的图片也进行转换,反而会影响页面的请求速度;
那么,我们如何可以限制哪些大小的图片转换和不转换呢?
url-loader有一个options属性limit,可以用于设置转换的限制;
下面的代码38kb的图片会进行base64编码,而295kb的不会;
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "url-loader",
options: {
limit: 100 * 1024,
name: "img/[name].[hash:8].[ext]",
outputPath: "img"
}
}
}
asset module type的介绍
我们当前使用的webpack版本是webpack5:
在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader 、url-loader、file-loader;
在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代上面的这些loader;
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现;
asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现;
asset/source 导出资源的源代码。之前通过使用 raw-loader 实现;
asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现;
Asset module type的使用
比如加载图片,我们可以使用下面的方式:
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset/resource"
}
但是,如何可以自定义文件的输出路径和文件名呢?
**方式一:**修改output,添加assetModuleFilename属性;
**方式二:**在Rule中,添加一个generator属性,并且设置filename;
output: {
filename: "bundle.js",
// 必须是一个绝对路径
path: path.resolve(__dirname, "./build"),
assetModuleFilename: "img/[name].[hash:6][ext]"
},
{
test: /\.(png|jpe?g|gif|svg)$/,
// type: "asset/resource", file-loader的效果
// type: "asset/inline", url-loader
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
},
url-loader的limit效果
我们需要两个步骤来实现:
**步骤一:**将type修改为asset;
**步骤二:**添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性;
{
test: /\.(png|jpe?g|gif|svg)$/,
// type: "asset/resource", file-loader的效果
// type: "asset/inline", url-loader
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
},
加载字体文件
如果我们需要使用某些特殊的字体或者字体图标,那么我们会引入很多字体相关的文件,这些文件的处理也是一样的。
首先,我从阿里图标库中下载了几个字体图标:
在component中引入,并且添加一个i元素用于显示字体图标:
// 创建一个i元素, 设置一个字体
const iEl = document.createElement("i");
iEl.className = "iconfont icon-ashbin why_icon";
element.appendChild(iEl);
字体的打包
这个时候打包会报错,因为无法正确的处理eot、ttf、woff等文件:
{
test: /\.ttf|eot|woff2?$/i,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:6][ext]"
}
}
认识Plugin
Webpack的另一个核心是Plugin,官方有这样一段对Plugin的描述:
上面表达的含义翻译过来就是:
Loader是用于特定的模块类型进行转换;
Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等;
CleanWebpackPlugin
前面我们演示的过程中,每次修改了一些配置,重新打包时,都需要手动删除dist文件夹:
首先,我们先安装这个插件:
npm install clean-webpack-plugin -D
之后在插件中配置:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(),
]
}
HtmlWebpackPlugin
另外还有一个不太规范的地方:
我们的HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的。
在进行项目部署的时,必然也是需要有对应的入口文件index.html;
所以我们也需要对index.html进行打包处理;
对HTML进行打包处理我们可以使用另外一个插件:HtmlWebpackPlugin;
npm install html-webpack-plugin -D
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "coderwhy webpack",
}),
]
}
生成的index.html分析
我们会发现,现在自动在dist文件夹中,生成了一个index.html的文件:
<!doctype html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="icon" href="./favicon.ico"> <title>coderwhy webpack</title> </head> <body> <script src="bundle.js"></script> </body> </html>
这个文件是如何生成的呢?
默认情况下是根据ejs的一个模板来生成的;
在html-webpack-plugin的源码中,有一个default_index.ejs模块;
自定义HTML模板
如果我们想在自己的模块中加入一些比较特别的内容:
比如添加一个noscript标签,在用户的JavaScript被关闭时,给予响应的提示;
比如在开发vue或者react项目时,我们需要一个可以挂载后续组件的根标签 <div id="app"></div>
;
这个我们需要一个属于自己的index.html模块:
<!DOCTYPE html> <html lang=""> <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"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
自定义模板数据填充
上面的代码中,会有一些类似这样的语法<% 变量 %>,这个是EJS模块填充数据的方式。
在配置HtmlWebpackPlugin时,我们可以添加如下配置:
**template:**指定我们要使用的模块所在的路径;
**title:**在进行htmlWebpackPlugin.options.title读取时,就会读到该信息;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "coderwhy webpack",
template: "./public/index.html"
}),
]
}
DefinePlugin的介绍
但是,这个时候编译还是会报错,因为在我们的模块中还使用到一个BASE_URL的常量:
这是因为在编译template模块时,有一个BASE_URL:
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
;
但是我们并没有设置过这个常量值,所以会出现没有定义的错误;
这个时候我们可以使用DefinePlugin插件;
DefinePlugin的使用
DefinePlugin允许在编译时创建配置的全局常量,是一个webpack内置的插件(不需要单独安装):
const { DefinePlugin } = require('webpack');
module.exports = {
plugins: [
new DefinePlugin({
BASE_URL: '"./"'
}),
]
}
这个时候,编译template就可以正确的编译了,会读取到BASE_URL的值;
CopyWebpackPlugin
在vue的打包过程中,如果我们将一些文件放到public的目录下,那么这个目录会被复制到dist文件夹中。
安装CopyWebpackPlugin插件:
npm install copy-webpack-plugin -D
接下来配置CopyWebpackPlugin即可:
复制的规则在patterns中设置;
**from:**设置从哪一个源中开始复制;
**to:**复制到的位置,可以省略,会默认复制到打包的目录下;
**globOptions:**设置一些额外的选项,其中可以编写需要忽略的文件:
new CopyWebpackPlugin({
patterns: [
{
from: "public",
globOptions: {
ignore: [
"**/index.html",
"**/.DS_Store",
"**/abc.txt"
]
}
}
]
})
Mode配置
前面我们一直没有讲mode,但是在这里我们要简单讲一下,后面还会提到它的其他用法。
Mode配置选项,可以告知webpack使用响应模式的内置优化:
默认值是production(什么都不设置的情况下);
可选值有:‘none’ | ‘development’ | ‘production’;
这几个选项有什么样的区别呢?
Mode配置代表更多
Webpack打包的代码,允许我们使用各种各样的模块化,但是最常用的是CommonJS、ES Module。
我们来研究一下它的原理,包括如下原理:
CommonJS模块化实现原理;
ES Module实现原理;
CommonJS加载ES Module的原理;
ES Module加载CommonJS的原理;
// 01_CommonJS的实现原理 // 定义了一个对象 // 模块的路径(key): 函数(value) var __webpack_modules__ = { "./src/js/format.js": (function (module) { const dateFormat = (date) => { return "2020-12-12"; } const priceFormat = (price) => { return "100.00"; } // 将我们要导出的变量, 放入到module对象中的exports对象 module.exports = { dateFormat, priceFormat } }) } // 定义一个对象, 作为加载模块的缓存 var __webpack_module_cache__ = {}; // 是一个函数, 当我们加载一个模块时, 都会通过这个函数来加载 function __webpack_require__(moduleId) { // 1.判断缓存中是否已经加载过 if (__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } // 2.给module变量和__webpack_module_cache__[moduleId]赋值了同一个对象 var module = __webpack_module_cache__[moduleId] = { exports: {} }; // 3.加载执行模块 __webpack_modules__[moduleId](module, module.exports, __webpack_require__); // 4.导出module.exports {dateFormat: function, priceForamt: function} return module.exports; } // 具体开始执行代码逻辑 !function () { // 1.加载./src/js/format.js const { dateFormat, priceFormat } = __webpack_require__("./src/js/format.js"); console.log(dateFormat("abc")); console.log(priceFormat("abc")); }();
// 02_ESModule的实现原理 // 1.定义了一个对象, 对象里面放的是我们的模块映射 var __webpack_modules__ = { "./src/es_index.js": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) { // 调用r的目的是记录时一个__esModule -> true __webpack_require__.r(__webpack_exports__); // _js_math__WEBPACK_IMPORTED_MODULE_0__ == exports var _js_math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/js/math.js"); console.log(_js_math__WEBPACK_IMPORTED_MODULE_0__.mul(20, 30)); console.log(_js_math__WEBPACK_IMPORTED_MODULE_0__.sum(20, 30)); }), "./src/js/math.js": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); // 调用了d函数: 给exports设置了一个代理definition // exports对象中本身是没有对应的函数 __webpack_require__.d(__webpack_exports__, { "sum": function () { return sum; }, "mul": function () { return mul; } }); const sum = (num1, num2) => { return num1 + num2; } const mul = (num1, num2) => { return num1 * num2; } }) }; // 2.模块的缓存 var __webpack_module_cache__ = {}; // 3.require函数的实现(加载模块) function __webpack_require__(moduleId) { if (__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } !function () { // __webpack_require__这个函数对象添加了一个属性: d -> 值function __webpack_require__.d = function (exports, definition) { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); } } }; }(); !function () { // __webpack_require__这个函数对象添加了一个属性: o -> 值function __webpack_require__.o = function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } }(); !function () { // __webpack_require__这个函数对象添加了一个属性: r -> 值function __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; }(); __webpack_require__("./src/es_index.js");
// 03_CommonJS和ESModule相互导入 var __webpack_modules__ = ({ "./src/index.js": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _js_format__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/js/format.js"); var _js_format__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_js_format__WEBPACK_IMPORTED_MODULE_0__); // es module导出内容, CommonJS导入内容 const math = __webpack_require__("./src/js/math.js"); // CommonJS导出内容, es module导入内容 console.log(math.sum(20, 30)); console.log(math.mul(20, 30)); console.log(_js_format__WEBPACK_IMPORTED_MODULE_0___default().dateFormat("aaa")); console.log(_js_format__WEBPACK_IMPORTED_MODULE_0___default().priceFormat("bbb")); }), "./src/js/format.js": (function (module) { const dateFormat = (date) => { return "2020-12-12"; } const priceFormat = (price) => { return "100.00"; } module.exports = { dateFormat, priceFormat } }), "./src/js/math.js": (function (__unused_webpack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "sum": function () { return sum; }, "mul": function () { return mul; } }); const sum = (num1, num2) => { return num1 + num2; } const mul = (num1, num2) => { return num1 * num2; } }) }); var __webpack_module_cache__ = {}; // The require function function __webpack_require__(moduleId) { // Check if module is in cache if (__webpack_module_cache__[moduleId]) { return __webpack_module_cache__[moduleId].exports; } // Create a new module (and put it into the cache) var module = __webpack_module_cache__[moduleId] = { // no module.id needed // no module.loaded needed exports: {} }; // Execute the module function __webpack_modules__[moduleId](module, module.exports, __webpack_require__); // Return the exports of the module return module.exports; } !function () { // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function () { return module['default']; } : function () { return module; }; __webpack_require__.d(getter, { a: getter }); return getter; }; }(); /* webpack/runtime/define property getters */ !function () { // define getter functions for harmony exports __webpack_require__.d = function (exports, definition) { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); } } }; }(); /* webpack/runtime/hasOwnProperty shorthand */ !function () { __webpack_require__.o = function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } }(); /* webpack/runtime/make namespace object */ !function () { // define __esModule on exports __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; }(); __webpack_require__("./src/index.js");
认识source-map
我们的代码通常运行在浏览器上时,是通过打包压缩的:
也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的;
比如ES6的代码可能被转换成ES5;
比如对应的代码行号、列号在经过编译后肯定会不一致;
比如代码进行丑化压缩时,会将编码名称等修改;
比如我们使用了TypeScript等方式编写的代码,最终转换成JavaScript;
但是,当代码报错需要调试时(debug),调试转换后的代码是很困难的
但是我们能保证代码不出错吗?不可能。
那么如何可以调试这种转换后不一致的代码呢?答案就是source-map
source-map是从已转换的代码,映射到原始的源文件;
使浏览器可以重构原始源并在调试器中显示重建的原始源;
如何使用source-map
如何可以使用source-map呢?两个步骤:
第一步:根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map;
第二步:在转换后的代码,最后添加一个注释,它指向sourcemap;
//# sourceMappingURL=common.bundle.js.map
浏览器会根据我们的注释,查找响应的source-map,并且根据source-map还原我们的代码,方便进行调试。
在Chrome中,我们可以按照如下的方式打开source-map:
分析source-map
最初source-map生成的文件带下是原始文件的10倍,第二版减少了约50%,第三版又减少了50%,所以目前一个133kb的文件,最终的source-map的大小大概在300kb。
目前的source-map长什么样子呢?
version:当前使用的版本,也就是最新的第三版;
sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件);
names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名称);
mappings:source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ(veriable-length quantity可变长度值)编码;
file:打包后的文件(浏览器加载的文件);
sourceContent:转换前的具体代码信息(和sources是对应的关系);
sourceRoot:所有的sources相对的根目录;
source-map文件
参考文档(MDN):https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/SourceMap.jsm
生成source-map
如何在使用webpack打包的时候,生成对应的source-map呢?
webpack为我们提供了非常多的选项(目前是26个),来处理source-map;
https://webpack.docschina.org/configuration/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:
bundle文件中有如下的注释:
//# sourceMappingURL=bundle.js.map
eval-source-map值
eval-source-map:会生成sourcemap,但是source-map是以DataUrl添加到eval函数的后面
inline-source-map值
inline-source-map:会生成sourcemap,但是source-map是以DataUrl添加到bundle文件的后面
cheap-source-map
cheap-source-map:
会生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)
因为在开发中,我们只需要行信息通常就可以定位到错误了
cheap-module-source-map值
cheap-module-source-map:
**这里有一个很模糊的概念:**对源自loader的sourcemap处理会更好,官方也没有给出很好的解释
如果我这里使用了babel-loader(注意:目前还没有讲babel)
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
]
},
cheap-source-map和cheap-module-source-map
cheap-source-map和cheap-module-source-map的区别:
hidden-source-map值
hidden-source-map:
会生成sourcemap,但是不会对source-map文件进行引用;
相当于删除了打包文件中对sourcemap的引用注释;
// 被删除掉的
//# sourceMappingURL=bundle.js.map
如果我们手动添加进来,那么sourcemap就会生效了
nosources-source-map值
nosources-source-map:
正确的错误提示:
点击错误提示,无法查看源码:
多个值的组合
事实上,webpack提供给我们的26个值,是可以进行多组合的。
组合的规则如下:
**inline-|hidden-|eval:**三个值时三选一;
**nosources:**可选值;
cheap可选值,并且可以跟随module的值;
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
那么在开发中,最佳的实践是什么呢?
在进行前端开发设置谷歌浏览器跨域时遇到了问题
总结三种方法:
一、49版本以前的设置:
在桌面chrome快捷方式的属性中的目标输入框添加 --disable-web-security 添加部分与前面字符之间有空格(有文章说目标引号结尾的加 --args --disable-web-security,反正我试过,没有49版本之前的)
二、49版本以后的设置:
1.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
2.在属性页面中的目标输入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,–user-data-dir的值就是刚才新建的目录(参考上面截图)
(我用此方法能成功设置)
三、如果以上两种方法失败,用以下方法(此方法为网上copy)
1.通过桌面快捷方式打开文件位置,找到谷歌浏览器安装位置,看到有个chrome.exe文件
2.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
3.打开cmd命令行,进入快捷方式位置 例如
cd C:\Program Files (x86)\Google\Chrome\Application
4.通过命令行启动谷歌浏览器
C:\Program Files (x86)\Google\Chrome\Application>chrome.exe --disable-web-security --user-data-dir=C:\MyChromeDevUserData
各位根据自己情况做相应更改即可
<script type=‘module‘>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./src/index.js" type="module"></script>
</body>
</html>
通过默认浏览器运行时报错,加载js被阻止。出现了以下错误
解决方法如下:
配置webpack打包主入口和出口
npx webpack --entry .\src\main.js --output-path .\build // 报上面的错误
错误原因:
无法解析Windows的.\src\main.js
路径。需要换成./src/main.js
也就是,改成下面这样就不会报这个错了
npx webpack --entry ./src/main.js --output-path ./build
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
// type: "asset/resource", // file-loader效果
// type: "asset/inline", // url-loader
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
},
const imgEl = new Image();
imgEl.src = require('../img/zznh.png').default
element.appendChild(imgEl)
通过以上方式打包之后的,加载不出zznh.png这张图片
如果换成下面这种引入方式,则可以加载出来
import zznhImage from "../img/zznh.png";
const imgEl = new Image();
// imgEl.src = require('../img/zznh.png').default
imgEl.src = zznhImage
element.appendChild(imgEl)
或者不用assetModuleType。重新使用file-loader那种方式
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: "img/[name].[hash:6].[ext]",
// outputPath: 'img'
}
}
]
}
这样子打包也可以正常引入zznh这张图片
一开始,我以为是因为淘宝镜像引起的问题,因为一开始我通过npm安装依赖报错了,就直接用cnpm 安装依赖了
结果npm安装依赖之后的报错更加简便,只有一个
Unexpected identifier
百度也找不到答案,一开始我以为是postcss-cli的版本问题。我一开始的版本是9.1.0,这个时候执行上面那条命令,就只会爆出这个错误。后面我就重新安装postcss-cli
npm uninstall postcss-cli
npm install postcss-cli@8.3.1
之后再执行上述命令行语句之后,这时候的报错才显得正常了起来
ExperimentalWarning: The fs.promises API is experimental
原来是node版本过旧,我原先的node版本为10.16.0,这个版本太老了,重新安装node到14.17.6就一切都正常了
[webpack-cli] HookWebpackError: Not supported
at xxxxxxxxxxx\node_modules\copy-webpack-plugin\dist\index.js:485:13
.....
-- inner error --
版本问题导致
CopyWebpackPlugin报错版本:10.0.0
解决办法:降低版本
npm i -D copy-webpack-plugin@9.*
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。