Webpack从入门到进阶(一)—附沿路学习案例代码
一、Webpack简介
1、前端发展的几个阶段
无论是作为专业的开发者还是接触互联网的普通人,其实都能深刻的感知到Web前端的发展是非常快速的
-
对于开发者来说我们会更加深有体会;
-
从后端渲染的JSP、PHP,到前端原生JavaScript,再到jQuery开发,再到目前的三大框架Vue、React、Angular;
-
开发方式也从原来的JavaScript的ES5语法,到ES6、7、8、9、10,到TypeScript,包括编写CSS的预处理器less、scss等;
前端开发的复杂化
前端开发目前我们面临哪些复杂的问题呢?
- 比如开发过程中我们需要通过模块化的方式来开发;
- 比如也会使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码;
- 比如开发过程中,我们还希望试试的监听文件的变化来并且反映到浏览器上,提高开发的效率;
- 比如开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化;
- 等等
2、前端三个框架的脚手架
目前前端流行的三大框架:Vue、React、Angular
- 但是事实上,这三大框架的创建过程我们都是借助于脚手架(CLI)的;
- 事实上Vue-CLI、create-react-app、Angular-CLI都是基于webpack来帮助我们支持模块化、less、TypeScript、打包优化等的;
3、Webpack是什么?
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具有如下的一些优势:
- 对 CommonJS 、 AMD 、ES6 的语法做了兼容;
- 对 js、css、图片等资源文件都支持打包;
- 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6的支持;
- 有独立的配置文件 webpack.config.js;
- 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化时间;
- 支持 SourceUrls 和 SourceMaps,易于调试;
- 具有强大的 Plugin 接口,大多是内部插件,使用起来比较灵活;
- webpack 使用异步 IO 并具有多级缓存。这使得 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
4、webpack和vite
我们来提一个问题:webpack会被vite取代吗?
- vite推出后确实引起了很多的反响,也有很多人看好vite的发展(包括我);
但是目前vite取代webpack还有很长的路要走
- 目前vue的项目支持使用vite,也支持使用webpack;
- React、Angular的脚手架目前没有支持,暂时也没有转向vite的打算;
- vite最终打包的过程,依然需要借助于rollup来完成;
vite的核心思想并不是首创
- 事实上,vite的很多思想和之前的snowpack是重合的,而且相对目前来说snowpack会更加成熟;
- 当然,后续发展来看vite可能会超越snowpack;
webpack的更新迭代
- webpack在发展的过程中,也会不断改进自己,借鉴其他工具的一些优势和思想;
- 在这么多年的发展中,无论是自身的优势还是生态都是非常强大的;
关于vite的思考
我的个人观点:
学习任何的东西,重要的是学习核心思想:
- 我们不能学会了JavaScript,有一天学习TypeScript、Java或者其他语言,所有的内容都是从零开始的;
- 我们在掌握了react之后,再去学习vue、Angular框架,也应该是可以快速上手的;
任何工具的出现,都是更好的服务于我们开发:
-
无论是vite的出现,还是以后新的工具的出现,不要有任何排斥的思想;
-
我们要深刻的明白,工具都是为了更好的给我们提供服务;
-
不可能出现了某个工具,让我们的开发效率变得更低,而这个工具却可以变得非常流行,这是不存在的;
5、vue-cli-service运行过程
6、Webpack的官方文档
webpack的官方文档是https://webpack.js.org/
- webpack的中文官方文档是https://webpack.docschina.org/
- DOCUMENTATION:文档详情,也是我们最关注的
点击DOCUMENTATION来到文档页:
- API:API,提供相关的接口,可以自定义编译的过程(比如自定义loader和Plugin可以参考该位置的API)
- BLOG:博客,等同于上一个tab的BLOG,里面有一些博客文章;
- CONCEPTS:概念,主要是介绍一些webpack的核心概念,比如入口、出口、Loaders、Plugins等等,但是这里并没有一些对它们解析的详细API;
- CONFIGURATION:配置,webpack详细的配置选项,都可以在这里查询到,更多的时候是作为查询手册;
- GUIDES:指南,更像是webpack提供给我们的教程,我们可以按照这个教程一步步去学习webpack的使用过程;
- LOADERS:loaders,webpack的核心之一,常见的loader都可以在这里查询到用法,比如css-loader、babel-loader、less-loader等等;
- PLUGINS:plugins,webpack的核心之一,常见的plugin都可以在这里查询到用法,比如BannerPlugin、CleanWebpackPlugin、MiniCssExtractPlugin等等;
- MIGRATE:迁移,可以通过这里的教程将webpack4迁移到webpack5等;
7、Webpack的依赖和安装
Webpack的运行是依赖Node环境的,所以我们电脑上必须有Node环境
- 所以我们需要先安装Node.js,并且同时会安装npm;
- Node官方网站:https://nodejs.org/
Webpack的安装
webpack的安装目前分为两个:webpack、webpack-cli
那么它们是什么关系呢?
- 执行webpack命令,会执行node_modules下的.bin目录下的webpack;
- webpack在执行时是依赖webpack-cli的,如果没有安装就会报错;
- 而webpack-cli中代码执行时,才是真正利用webpack进行编译和打包的过程;
- 所以在安装webpack时,我们需要同时安装webpack-cli(第三方的脚手架事实上是没有使用webpack-cli的,而是类似于自己的vue-service-cli的东西)
npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
传统开发存在的问题
我们的代码存在什么问题呢?某些语法浏览器是不认识的(尤其在低版本浏览器上)
- 1.使用了ES6的语法,比如const、箭头函数等语法;
- 2.使用了ES6中的模块化语法;
- 3.使用CommonJS的模块化语法;
- 4.在通过script标签引入时,必须添加上 type=“module” 属性;
显然,上面存在的问题,让我们在发布静态资源时,是不能直接发布的,因为运行在用户浏览器必然会存在各种各样的兼容性问题。
- 我们需要通过某个工具对其进行打包,让其转换成浏览器可以直接识别的语法;
8、webpack默认打包
我们可以通过webpack进行打包,之后运行打包之后的代码
- 在目录下直接执行 webpack 命令
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
- 这个文件中的代码被压缩和丑化了;
- 我们暂时不关心他是如何做到的,后续我讲webpack实现模块化原理时会再次讲到;
- 另外我们发现代码中依然存在ES6的语法,比如箭头函数、const等,这是因为默认情况下webpack并不清楚我们打包后的文件是否需要转成ES5之前的语法,后续我们需要通过babel来进行转换和设置;
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
- 事实上,当我们运行webpack时,webpack会查找当前目录下的 src/index.js作为入口;
- 所以,如果当前项目中没有存在src/index.js文件,那么会报错;
9、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来打包即可。
10、Webpack依赖图
webpack到底是如何对我们的项目进行打包的呢?
-
事实上webpack在处理应用程序时,它会根据命令或者配置文件找到入口文件;
-
从入口开始,会生成一个 依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如.js文件、css文件、图片、字体等);
-
然后遍历图结构,打包一个个模块(根据文件的不同使用不同的loader来解析);
二、Webpack知识点
一、Webpack配置和css处理
1、webpack打包和配置
webpack默认打包
我们可以通过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
- 通过JavaScript创建了一个元素,并且希望给它设置一些样式;
// 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
2、CSS处理
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文件呢?有三种方式:
- 内联方式;
- CLI方式(webpack5中不再使用);
- 配置方式;
**内联方式:**内联方式使用较少,因为不方便管理;
- 在引入的样式前加上使用的loader,并且使用!分割;
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]
- UseEntry是一个对象,可以通过对象的属性来设置一些其他属性
- loader:必须有一个 loader属性,对应的值是一个字符串;
- options:可选的属性,值是一个字符串或者对象,值会被传入到loader中;
- query:目前已经使用options来替代;
- 传递字符串(如:use: [ ‘style-loader’ ])是 loader 属性的简写方式(如:use: [ { loader: ‘style-loader’} ]);
- 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在我们的代码中并没有生效(页面没有效果)。
这是为什么呢?
-
因为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、sass等编写的css需要通过工具转换成普通的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,它们如何可以自动转换呢?
- 这个时候我们就可以使用less-loader,来自动使用less工具转换less到css;
- less-loader需要less工具进行less文件的处理
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,并且页面也会生效了
3、浏览器兼容性
我们来思考一个问题:开发中,浏览器的兼容性问题,我们应该如何去解决和处理?
-
当然这个问题很笼统,这里我说的兼容性问题不是指屏幕大小的变化适配;
-
我这里指的兼容性是针对不同的浏览器支持的特性:比如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是什么?Browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置:
- Autoprefixer
- Babel
- postcss-preset-env
- eslint-plugin-compat
- stylelint-no-unsupported-browser-features
- postcss-normalize
- obsolete-webpack-plugin
浏览器查询过程
我们可以编写类似于这样的配置:
> 1%
last 2 versions
not dead
那么之后,这些工具会根据我们的配置来获取相关的浏览器信息,以方便决定是否需要进行兼容性的支持:
- 条件查询使用的是caniuse-lite的工具,这个工具的数据来自于caniuse的网站上;
Browserslist编写规则
那么在开发中,我们可以编写的条件都有哪些呢?(加粗部分是最常用的)
defaults:Browserslist的默认浏览器(> 0.5%, last 2 versions, Firefox ESR, not dead)。
5%:通过全局使用情况统计信息选择的浏览器版本。 >=,<和<=工作过。
- 5% in US:使用美国使用情况统计信息。它接受两个字母的国家/地区代码。
- > 5% in alt-AS:使用亚洲地区使用情况统计信息。有关所有区域代码的列表,请参见caniuse-lite/data/regions
- > 5% in my stats:使用自定义用法数据。
- > 5% in browserslist-config-mycompany stats:使用 来自的自定义使用情况数据browserslist-config-mycompany/browserslist-stats.json。
- cover 99.5%:提供覆盖率的最受欢迎的浏览器。
- cover 99.5% in US:与上述相同,但国家/地区代码由两个字母组成。
- cover 99.5% in my stats:使用自定义用法数据。
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版本。
- current node:Browserslist现在使用的Node.js版本。
- maintained node versions:所有Node.js版本,仍由 Node.js Foundation维护。
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'
]
我们编写了多个条件之后,多个条件之间是什么关系呢?
4、PostCSS
认识PostCSS工具
什么是PostCSS呢?
-
PostCSS是一个通过JavaScript来转换样式的工具;
-
这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置;
-
但是实现这些工具,我们需要借助于PostCSS对应的插件;
如何使用PostCSS呢?主要就是两个步骤:
-
第一步:查找PostCSS在构建工具中的扩展,比如webpack中的postcss-loader;
-
第二步:选择可以添加你需要的PostCSS相关的插件;
命令行使用postcss
当然,我们能不能也直接在终端使用PostCSS呢?
- 也是可以的,但是我们需要单独安装一个工具postcss-cli;
我们可以安装一下它们: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配置文件
当然,我们也可以将这些配置信息放到一个单独的文件中进行管理:
- 在根目录下创建postcss.config.js
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;
}
二、Webpack加载和处理其他资源
1、案例准备
为了演示我们项目中可以加载图片,我们需要在项目中使用图片,比较常见的使用图片的方式是两种:
-
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;
}
这个时候,打包会报错
2、file-loader
要处理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"
}
}
文件的名称规则
有时候我们处理后的文件名称按照一定的规则进行显示:
- 比如保留原来的文件名、扩展名,同时为了防止重复,包含一个hash值等;
这个时候我们可以使用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配置文件的路径;
设置文件名称
那么我们可以按照如下的格式编写:
- 这个也是vue的写法;
{
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"
}
}
}
3、url-loader
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文件夹中,我们会看不到图片文件:
- 这是因为我的两张图片的大小分别是38kb和295kb;
- 默认情况下url-loader会将所有的图片文件转成base64编码
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"
}
}
}
4、asset module type
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等文件:
- 我们可以选择使用file-loader来处理,也可以选择直接使用webpack5的资源模块类型来处理;
{
test: /\.ttf|eot|woff2?$/i,
type: "asset/resource",
generator: {
filename: "font/[name].[hash:6][ext]"
}
}
4、Plugin
认识Plugin
Webpack的另一个核心是Plugin,官方有这样一段对Plugin的描述:
- While loaders are used to transform certain types of modules, plugins can be leveraged to perform a wider range of tasks like bundle optimization, asset management and injection of environment variables.
上面表达的含义翻译过来就是:
-
Loader是用于特定的模块类型进行转换;
-
Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等;
CleanWebpackPlugin
前面我们演示的过程中,每次修改了一些配置,重新打包时,都需要手动删除dist文件夹:
- 我们可以借助于一个插件来帮助我们完成,这个插件就是CleanWebpackPlugin;
首先,我们先安装这个插件:
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的文件:
- 该文件中也自动添加了我们打包的bundle.js文件;
<!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来完成;
安装CopyWebpackPlugin插件:
npm install copy-webpack-plugin -D
接下来配置CopyWebpackPlugin即可:
-
复制的规则在patterns中设置;
-
**from:**设置从哪一个源中开始复制;
-
**to:**复制到的位置,可以省略,会默认复制到打包的目录下;
-
**globOptions:**设置一些额外的选项,其中可以编写需要忽略的文件:
- .DS_Store:mac目录下回自动生成的一个文件;
- index.html:也不需要复制,因为我们已经通过HtmlWebpackPlugin完成了index.html的生成;
new CopyWebpackPlugin({
patterns: [
{
from: "public",
globOptions: {
ignore: [
"**/index.html",
"**/.DS_Store",
"**/abc.txt"
]
}
}
]
})
三、模块化原理和source-map
1、Mode
Mode配置
前面我们一直没有讲mode,但是在这里我们要简单讲一下,后面还会提到它的其他用法。
Mode配置选项,可以告知webpack使用响应模式的内置优化:
-
默认值是production(什么都不设置的情况下);
-
可选值有:‘none’ | ‘development’ | ‘production’;
这几个选项有什么样的区别呢?
Mode配置代表更多
2、Webpack的模块化
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");
3、source-map
认识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:
- 生成一个独立的source-map文件,并且在bundle文件中有一个注释,指向source-map文件;
bundle文件中有如下的注释:
- 开发工具会根据这个注释找到source-map文件,并且解析;
//# 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:
- 会生成sourcemap,类似于cheap-source-map,但是对源自loader的sourcemap处理会更好。
**这里有一个很模糊的概念:**对源自loader的sourcemap处理会更好,官方也没有给出很好的解释
- 其实是如果loader对我们的源码进行了特殊的处理,比如babel;
如果我这里使用了babel-loader(注意:目前还没有讲babel)
- 可以先按照我的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:
- 会生成sourcemap,但是生成的sourcemap只有错误信息的提示,不会生成源代码文件;
正确的错误提示:
点击错误提示,无法查看源码:
多个值的组合
事实上,webpack提供给我们的26个值,是可以进行多组合的。
组合的规则如下:
-
**inline-|hidden-|eval:**三个值时三选一;
-
**nosources:**可选值;
-
cheap可选值,并且可以跟随module的值;
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
那么在开发中,最佳的实践是什么呢?
- 开发阶段:推荐使用 source-map或者cheap-module-source-map
- 这分别是vue和react使用的值,可以获取调试信息,方便快速开发;
- 测试阶段:推荐使用 source-map或者cheap-module-source-map
- 测试阶段我们也希望在浏览器下看到正确的错误提示;
- 发布阶段:false、缺省值(不写)
三、知识扩展
1、谷歌浏览器设置跨域
在进行前端开发设置谷歌浏览器跨域时遇到了问题
总结三种方法:
一、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
各位根据自己情况做相应更改即可
2、使用vscode模块化开发加载js类型为module所出现的问题<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被阻止。出现了以下错误
解决方法如下:
- 下载安装 live server
- 选择需要运行的文件,通过open with live server打开
- 运行成功
3、Module not found: Error: Can’t resolve ‘.\src\main.js’ in XXXX
配置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
4、assetModuleType 方式打包 require的图片加载不出来
{
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这张图片
5、npx postcss unexpected identifier
一开始,我以为是因为淘宝镜像引起的问题,因为一开始我通过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就一切都正常了
6、CopyWebpackPlugin出现HookWebpackError: Not supported
[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.*