Webpack
Webpack初识
认识Webpack
事实上随着前端的快速发展,目前前端的开发已经变的越来越复杂了:
- 比如开发过程中我们需要通过模块化的方式来开发;
- 比如也会使用一些高级的特性来加快我们的开发效率或者安全性,比如通过 ES6+、TypeScript 开发脚本逻辑,通过sass、less 等方式来编写 css 样式代码;
- 比如开发过程中,我们还希望实时的监听文件的变化来并且反映到浏览器上,提高开发的效率;
- 比如开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化;
- 等等….
但是对于很多的前端开发者来说,并不需要思考这些问题,日常的开发中根本就没有面临这些问题:
- 这是因为目前前端开发我们通常都会直接使用三大框架来开发:Vue、React、Angular;
- 但是事实上,这三大框架的创建过程我们都是借助于脚手架(CLI)的;
- 事实上 Vue-CLI、create-react-app、Angular-CLI 都是基于 webpack 来帮助我们支持模块化、less、 TypeScript、打包优化等的;
常见框架的脚手架
- 事实上我们上面提到的所有脚手架都是依赖于 webpack 的:
Webpack的作用
- 官方的解释:webpack is a static module bundler for modern JavaScript applications.
- webpack 是一个静态的模块化打包工具,为现代的 JavaScript 应用程序;
- 我们来对上面的解释进行拆解:
- 打包 bundler:webpack 可以将帮助我们进行打包,所以它是一个打包工具
- 静态的 static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
- 模块化 module:webpack 默认支持各种模块化开发,ES Module、CommonJS、AMD等;
- 现代的 modern:我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了 webpack 的出现和发展;
Vue项目的打包内容
JavaScript 的打包:
将 ES6 转换成 ES5 的语法;
TypeScript 的处理,将其转换成 JavaScript;
CSS 的处理:
- CSS文件模块的加载、提取;
- Less、Sass等预处理器的处理;
资源文件 img、font:
- 图片 img 文件的加载;
- 字体 font 文件的加载;
HTML 资源的处理:
- 打包 HTML 资源文件;
处理 vue 项目的 SFC 文件 .vue 文件;
webpack的使用前提
webpack 的官方文档是 : https://webpack.js.org/
webpack 的中文官方文档是https://webpack.docschina.org/
- DOCUMENTATION:文档详情,也是我们最关注的
Webpack 的运行是依赖 Node 环境的,所以我们电脑上必须有 Node 环境
- 所以我们需要先安装 Node.js,安装 node 的同时也会安装 npm;
- 查看 node 是否安装成功以及查看 node 版本,终端中输入指令 : node –version , 出现 node 版本即为安装成功
- 查看 npm 版本 : npm –version
- 我当前电脑上的 node 版本是 v14.15.5,npm 版本是 6.14.11(你也可以使用 nvm 来管理Node版本);
- 所以我们需要先安装 Node.js,安装 node 的同时也会安装 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 的东西
1 | npm install webpack webpack-cli –g # 全局安装 |
Webpack的默认打包
Webpack的默认打包
./src/js/math.js
1 | // ES Module |
./src/js/format.js
1 | // CommomJS |
./src/index.js
1 | import { sum } from './js/math' |
- 01 我们可以通过 webpack 进行打包,之后运行打包之后的代码
- 在目录下直接执行命令: webpack ,就会执行默认打包,使用的是全局的 webpack
- 02 执行上面的命令后,生成一个 dist 文件夹,里面存放一个 main.js 的文件,就是我们打包之后的文件:
- 这个文件中的代码被压缩和丑化了;
- 在项目中使用 ./index.html 文件,在 body 结束标签之前,引入该 main.js 即可在浏览器中执行
- 另外我们发现代码中依然存在 ES6 的语法,比如箭头函数、const 、let 等,这是因为默认情况下 webpack 并不清楚我们打包后的文件是否需要转成 ES5 之前的语法,后续我们需要通过 babel 来进行转换和设置;
- 03 我们发现是可以正常进行打包的,但是有一个问题,webpack 是如何确定我们的入口的呢?
- 事实上,当我们运行 webpack 命令时,webpack 会查找当前目录下的 ./src/index.js 作为入口;
- 所以,如果当前项目中没有存在 ./src/index.js 文件,那么会报错;
- 当然,我们也可以通过配置来指定入口和出口
- 指定入口文件为 ./src/main.js , 打包输入目录为 ./build
1 | npx webpack --entry ./src/main.js --output-path ./build |
创建局部Webpack
- 前面我们直接执行webpack命令使用的是全局的webpack,如果希望使用局部的可以按照下面的步骤来操作。
- 第一步:执行命令 npm init 或者 npm init -y 创建 package.json 文件,用于管理项目的信息、库依赖等
- 第二步:安装局部的 webpack
1 | npm install webpack webpack-cli -D |
- 第三步:使用局部的 webpack 执行打包, 命令 : npx webpack , npx 命令是有 npm 提供的,无需另外安装
- 第四步:在 package.json 中创建以下 scripts 脚本,执行脚本命令 npm run build 打包即可
1 | // package.json |
Webpack的配置文件
- 在通常情况下,webpack 需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的。
- 我们可以在根目录下创建一个 webpack.config.js 文件,来作为 webpack 的配置文件:
- 配置文件的文件名规定默认就是叫 webpack.config.js , 如果配置文件是其他名称, 需要另外配置命令专门指定
1 | const path = require('path') // 导入node的内置path模块 |
- 继续执行 npm run build 命令,依然可以正常打包
指定配置文件
- 但是如果我们的配置文件并不是 webpack.config.js 的名字,而是其他的名字呢?
- 比如我们将
webpack.config.js
修改成了wk.config.js
; - 这个时候我们可以通过
--config
来指定对应的配置文件;- 在执行打包的同时指定配置文件, 命令 : webpack –config wk.config.js
- 比如我们将
- 但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在 package.json 文件中增加一个新的脚本:
1 | "scripts": { |
- 之后我们执行 npm run build 来打包即可。
Webpack的依赖图
- webpack 到底是如何对我们的项目进行打包的呢?
- 01 事实上 webpack 在处理应用程序时,它会根据命令或者配置文件找到入口文件;
- 02 从入口开始,会生成一个依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如 .js 文件、css 文件、图片、字体等);
- 03 然后遍历图结构,打包一个个模块(根据文件的不同使用不同的 loader 来解析);
webpack打包案例
- 我们创建一个 element.js
- 通过 JavaScript 创建了一个元素,并且希望给它设置一些样式;
./src/js/element.js
1 | import "../css/style.css" |
./src/css/style.css
1 | .title { |
./src/index.js
1 | import "./js/element" |
执行打包命令: npm run build , 此时会报错模块解析失败,需要 loader 解析模块, 解决方法见 CSS-loader 章节
CSS与Less的打包
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 中不再使用);
- 配置方式 (推荐使用);
01 内联方式:内联方式使用较少,因为不方便管理;
- 在引入的样式前加上使用的 loader,并且使用
!
分割;
- 在引入的样式前加上使用的 loader,并且使用
1 | import "css-loader!../css/style.css" |
02 CLI 方式
- 在 webpack5 的文档中已经没有了
--module-bind
; - 实际应用中也比较少使用,因为不方便管理;
- 在 webpack5 的文档中已经没有了
03 配置方式
- 表示的意思是在我们的
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 } ] 的简写。
- webpack.config.js :
- 表示的意思是在我们的
1 | const path = require('path') // 导入node的内置path模块 |
- 配置好 CSS-loader 后,即可执行 npm run build 打包 .css 文件,但是此时 CSS 样式依然还是未生效,解决方法参考 style-loader 章节
style-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 的前面;
1 | const path = require('path') // 导入node的内置path模块 |
- 重新执行编译 npm run build,可以发现打包后的 css 已经生效了:
- 当前我们的 css 是通过页内样式的方式添加进来的;
- 后续我们也会讲如何将 css 抽取到单独的文件中,并且进行压缩等操作;
less-loader
处理less工具
- 在我们开发中,我们可能会使用less、sass、stylus的预处理器来编写css样式,效率会更高。
- 那么,如何可以让我们的环境支持这些预处理器呢?
- 首先我们需要确定,less、sass 等编写的 css 需要通过工具转换成普通的 css;
- 比如我们编写如下的 less 样式:
./src/css/title.less
1 | @bgColor: pink; |
./src/js/element.js , 将 less 文件添加到依赖关系图
1 | import "../css/style.css" |
- 我们可以使用 less 工具来完成它的编译转换:
- less 工具是一个独立的工具,跟 webpack 没有关系 , lessc 是安装 less 后,由 less 提供的
- 安装 less : npm install less -D
- 执行命令: npx lessc ./src/css/title.less title.css , 将 title.less 编译成 title.css
使用less-loader
但是在项目中我们会编写大量的 css,它们如何可以自动转换呢?
- 这个时候我们就可以使用 less-loader,来自动使用 less 工具转换 less 到 css;
- less-loader 的原理很简单,就是调用 less 库提供的方法,转译 less 语法后输出
01 安装 less-loader : npm install less less-loader -D
- 使用 less-loader,也需要安装 less
02 在 webpack.config.js 中配置 less-loader:
1 | const path = require('path') // 导入node的内置path模块 |
- 03 执行 npm run build 执行打包, less 就可以自动转换成 css,并且页面也会生效了
postcss-loader
认识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 属性的样式,并将该 css 文件添加到关系依赖图(即在某个文件中导入)
1 | .title { |
插件autoprefixer
因为我们需要自动添加前缀,所以要安装 autoprefixer:npm install autoprefixer -D
直接使用使用 postcss 工具,并且制定使用 autoprefixer :
- npx postcss –use autoprefixer -o ./src/css/end.css ./src/css/style.css
- 通过 postcss ,使用 autoprefixer 插件,给 ./src/css/style.css 添加前缀,输出到 ./src/css/end.css
- 处理结果如下:
1 | .title { |
postcss-loader
真实开发中我们必然不会直接使用命令行工具来对 css 进行处理,而是可以借助于构建工具:
- 在 webpack 中使用 postcss 就是使用 postcss-loader 来处理的;
我们来安装 postcss-loader:npm install postcss-loader -D
我们修改加载 css 的 loader:(配置文件已经过多,给出一部分了)
- 注意:因为 postcss 需要有对应的插件才会起效果,所以我们需要配置它的 plugin;
1 | const path = require('path') // 导入node的内置path模块 |
单独的postcss配置
- 当然,我们也可以将这些配置信息放到一个单独的文件中进行管理:
- 在根目录下创建 postcss.config.js
- 打包时会先去 webpack.config.js 中查看配置,有就使用,没有再去 postcss.config.js 中查看配置
- 在根目录下创建 postcss.config.js
postcss.config.js
1 | module.exports = { |
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
- 之后,我们直接修改掉之前 postcss.config.js 文件中的 autoprefixer 即可:
- 注意:我们在使用某些 postcss 插件时,也可以直接传入字符串
- 该插件也可以将十六进制的颜色值,转换为 rgb 值,测试时数值只能是 6 位或者 8 位 , 其他位数编辑器会报错,浏览器报警告属性值无效
postcss.config.js
1 | module.exports = { |
Webpack打包静态资源
打包图片资源
- 为了演示我们项目中可以加载图片,我们需要在项目中使用图片,比较常见的使用图片的方式是两种:
- img 元素,设置 src 属性;
- 其他元素(比如 div ),设置 background-image 的 css 属性;
1 | // element.js |
1 | /** image.css */ |
- 这个时候,打包会报错
- (测试时直接就打包过去了,并没有报错,也可以正常显示在页面中,并没有配置 file-loader ,与视频中的操作不同)
file-loader
file-loader的作用
- 要处理 jpg、png 等格式的图片,我们也需要有对应的 loader:file-loader
- file-loader 的作用就是帮助我们处理 import/require() 方式引入的一个文件资源,并且会将它放到我们输出的文件夹中;
- 如果通过原生JS给img添加图片,需要将图片作为一个模块导入,不能直接赋值图片的url地址
- 当然我们待会儿可以学习如何修改它的名字和所在文件夹;
file-loader的安装
- 命令 : npm install file-loader -D
file-loader的配置
- 配置处理图片的 Rule:
1 | const path = require('path') // 导入node的内置path模块 |
- 注意点: 如果通过原生 JS 给 img 添加图片,需要将图片作为一个模块导入,不能直接赋值图片的 url 地址,否则图片可以打包但不能显示在页面中
1 | // element.js |
文件的命名规则
有时候我们处理后的文件名称按照一定的规则进行显示:
- 比如保留原来的文件名、扩展名,同时为了防止重复,包含一个 hash 值等;
这个时候我们可以使用 PlaceHolders 来完成,webpack 给我们提供了大量的 PlaceHolders 来显示不同的内容:
- 我们可以在文档中查阅自己需要的 placeholder;
我们这里介绍几个最常用的 placeholder:
- [ext]:处理文件的扩展名;
- [name]:处理文件的名称;
- [hash]:文件的内容,使用 MD4 的散列函数处理,生成的一个 128 位的 hash 值(32 个十六进制);
- [contentHash]:在 file-loader 中和 [hash] 结果是一致的(在 webpack 的一些其他地方不一样,后面会讲到);
- [hash:length]:截图 hash 的长度,默认 32 个字符太长了;
- [path]:文件相对于 webpack 配置文件的路径;
设置文件的名称
- 那么我们可以按照如下的格式编写:
- 这个也是 vue 的写法;
1 | //匹配图片资源 |
文件的存放路径
- 当然,我们刚才通过 img/ 已经设置了文件夹,这个也是 vue、react 脚手架中常见的设置方式:
- 其实按照这种设置方式就可以了;
- 当然我们也可以通过 outputPath 来设置输出的文件夹;
1 | // 模块 |
url-loader
- url-loade 和 file-loader 的工作方式是相似的,但是可以将较小的文件,转成 base64 的 URI。
- 就是会将体积较小的文件,会以 base64 编码打包压缩到出口文件 ./dist/bundle.js 文件中
url-loader的安装
- 安装 url-loader:npm install url-loader -D
url-loader的配置
1 | // 模块 |
显示结果是一样的,并且图片可以正常显示;
但是在 dist 文件夹中,我们会看不到图片文件:
这是因为我的两张图片的大小分别是 38kb 和 295kb;
默认情况下 url-loader 会将所有的图片文件转成 base64 编码
url-loader的limit
- 但是开发中我们往往是小的图片需要转换,但是大的图片直接使用图片即可
- 这是因为小的图片转换 base64 之后可以和页面一起被请求,减少不必要的请求过程;
- 而大的图片也进行转换,反而会影响页面的请求速度;
- 那么,我们如何可以限制哪些大小的图片转换和不转换呢?
- url-loader 有一个 options 属性 limit,可以用于设置转换的限制;
- 下面的代码 38kb 的图片会进行 base64 编码,而 295kb 的不会;
1 | // 模块 |
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
- 比如加载图片,我们可以使用下面的方式:
1 | // 模块 |
- 但是,如何可以自定义文件的输出路径和文件名呢?
- 方式一:修改 output,添加 assetModuleFilename 属性;
- 方式二:在 Rule 中,添加一个 generator 属性,并且设置 filename;
1 | // 方式一: |
1 | // 方式二: |
limit
- 我们需要两个步骤来实现:
- 步骤一:将 type 修改为 asset;
- 骤二:添加一个 parser 属性,并且制定 dataUrl 的条件,添加 maxSize 属性;
1 | // 模块 |
打包字体文件
加载字体文件
- 如果我们需要使用某些特殊的字体或者字体图标,那么我们会引入很多字体相关的文件,这些文件的处理也是一样的。
- 首先,我从阿里图标库中下载了几个字体图标:
- 在 component 中引入,并且添加一个 i 元素用于显示字体图标:
1 | // element.js |
字体文件的打包
- 这个时候打包会报错,因为无法正确的处理 eot、ttf、woff 等文件:
- (测试时并没有报错,而是直接打包过去了,也可以正常显示在页面中)
- 被打包到了 img 文件夹中,img 图片使用的是 asset 模块打包
- 我们可以选择使用 file-loader 来处理,也可以选择直接使用 webpack5 的资源模块类型来处理;
- (测试时并没有报错,而是直接打包过去了,也可以正常显示在页面中)
- 使用 file-loader 来处理:
- 能正常打包,但是也同时打包到了 img 文件夹中,img 图片使用的是 asset 模块打包
1 | // 模块 |
- 使用 webpack5 的资源模块类型来处理:
- 正常打包,图片打包到 img 文件夹, 字体打包到 font 文件夹
1 | // 模块 |
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
- 之后在配置文件中配置插件:
- 第一步: 导入 CleanWebpackPlugin
- 第二步: 在 plugins 中创建 CleanWebpackPlugin 类的实例对象
1 | const path = require('path') // 导入node的内置path模块 |
HtmlWebpackPlugin
- 另外还有一个不太规范的地方:
- 我们的HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的。
- 在进行项目部署的时,必然也是需要有对应的入口文件index.html;
- 所以我们也需要对index.html进行打包处理;
- 对 HTML 进行打包处理我们可以使用另外一个插件:HtmlWebpackPlugin;
- 安装插件 : npm install html-webpack-plugin -D
- 配置插件:
1 | const path = require('path') // 导入node的内置path模块 |
自定义HTML模板
自定义HTML模板
- 我们会发现,现在自动在 dist 文件夹中,生成了一个 index.html 的文件:
- 该文件中也自动添加了我们打包的 bundle.js 文件;
- 这个文件是如何生成的呢?
- 默认情况下是根据 ejs 的一个模板来生成的;
- 在 html-webpack-plugin 的源码中,有一个 default_index.ejs 模块;
- 如果我们想在自己的模块中加入一些比较特别的内容:
- 比如添加一个 noscript 标签,在用户的 JavaScript 被关闭时,给予响应的提示;
- 比如在开发 vue 或者 react 项目时,我们需要一个可以挂载后续组件的根标签
<div id="app"></div>
;
- 这个我们需要一个属于自己的 index.html 模块:
./public/index.html
1 |
|
- 上面的代码中,会有一些类似这样的语法
<% 变量 %>
,这个是 EJS 模块填充数据的方式。 - 在配置 HtmlWebpackPlugin 时,我们可以添加如下配置:
- template:指定我们要使用的模块所在的路径;
- title:在进行 htmlWebpackPlugin.options.title 读取时,就会读到该信息;
1 | const path = require('path') // 导入node的内置path模块 |
- 但是,这个时候编译还是会报错,因为在我们的模块中还使用到一个 BASE_URL 的常量:
DefinePlugin的介绍
- 这是因为在编译template模块时,有一个BASE_URL:
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
;- 但是我们并没有设置过这个常量值,所以会出现没有定义的错误;
- 这个时候我们可以使用 DefinePlugin 插件;
DefinePlugin的使用
- DefinePlugin 允许在编译时创建配置的全局常量,是一个 webpack 内置的插件(不需要单独安装):
1 | const path = require('path') // 导入node的内置path模块 |
- 这个时候,编译 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 的生成;
- 第一步: 导入 CopyWebpackPlugin
- 第二步: 创建 CopyWebpackPlugin 的实例对象
- 复制的规则在 patterns 中设置;
1 | const path = require('path') // 导入node的内置path模块 |
Mode与source-map
Mode
- 前面我们一直没有讲 mode。
- Mode 配置选项,可以告知 webpack 使用响应模式的内置优化:
- 默认值是 production(什么都不设置的情况下);
- 可选值有:**’none’ | ‘development’ | ‘production’**;
- 这几个选项有什么样的区别呢?
1 | module.exports = { |
source-map
- source-map 的作用 : 建立 js 映射文件,方便调试代码和错误
1 | module.exports = { |
babel
babel的作用
- 事实上,在开发中我们很少直接去接触 babel,但是 babel 对于前端开发来说,目前是不可缺少的一部分:
- 开发中,我们想要使用 ES6+ 的语法,想要使用T ypeScript,开发 React 项目,它们都是离不开 Babel 的;
- 所以,学习 Babel 对于我们理解代码从编写到线上的转变过程至关重要;
- 那么,Babel 到底是什么呢?
- Babel 是一个工具链,主要用于旧浏览器或者环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript;
- 包括:语法转换、源代码转换等;
1 | const message = 'Hello World' |
Babel命令行使用
babel 本身可以作为一个独立的工具(和 postcss 一样),不和 webpack 等构建工具配置来单独使用。
如果我们希望在命令行尝试使用 babel,需要安装如下库:
- @babel/core:babel 的核心代码,必须安装;
- @babel/cli:可以让我们在命令行使用 babel;
安装babel : npm install @babel/cli @babel/core -D
使用 babel 来处理我们的源代码:npx babel ./src/js/demo.js –out-dir dist
- 将指定文件通过 babel 转换后,输出到 dist 文件夹 ;
- 如果要输出成指定文件 : npx babel ./src/js/demo.js –out-file test.js
插件的使用
比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:
安装插件 : npm install @babel/plugin-transform-arrow-functions -D
使用插件编译文件: npx babel demo.js –out-dir dist --plugins=@babel/plugin-transform-arrow-functions
查看转换后的结果:我们会发现 const 并没有转成 var
- 这是因为 plugin-transform-arrow-functions,并没有提供这样的功能;
- 我们需要使用 plugin-transform-block-scoping 来完成这样的功能;
- 安装插件 : npm install @babel/plugin-transform-block-scoping -D
使用插件编译文件: npx babel demo.js –out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
Babel的预设preset
- 但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset):
- 后面我们再具体来讲预设代表的含义;
- 安装 @babel/preset-env 预设:
- npm install @babel/preset-env -D
- 执行如下命令编译文件:
- npx babel src –out-dir dist --presets=@babel/preset-env
- npx babel demo.js –out-dir dist --presets=@babel/preset-env
- 编译后的文件默认开启严格模式
Babel的底层原理
babel 是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?
- 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
- 就是编译器,事实上我们可以将 babel 看成就是一个编译器。
- 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
Babel 编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码;
Babel也拥有编译器的工作流程:
- 解析阶段(Parsing)
- 转换阶段(Transformation)
- 生成阶段(Code Generation)
Babel的编译流程
- Babel的执行阶段
- 当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:
- 详细的编译流程:
Babel-loader
- 在实际开发中,我们通常会在构建工具中通过配置 babel 来对其进行使用的,比如在 webpack 中。
- 那么我们就需要去安装相关的依赖:
- 如果之前已经安装了 @babel/core,那么这里不需要再次安装;
- npm install babel-loader @babel/core -D
- 如果之前已经安装了 @babel/core,那么这里不需要再次安装;
- 我们可以设置一个规则,在加载 js 文件时,使用我们的 babel:我们必须指定使用的插件才会生效
1 | // 匹配 js 文件 |
babel-preset
如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。
比如常见的预设有三个:
- env
- preact
- TypeScript
安装 preset-env:npm install @babel/preset-env
1 | // 匹配 js 文件 |
Babel的配置文件
- 像之前一样,我们可以将 babel 的配置信息放到一个独立的文件中,babel 给我们提供了两种配置文件的编写:
- babel.config.json(或者.js,.cjs,.mjs)文件;
- babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
- 它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式( babel 本身、element-plus、umi等);
- babelrc.json:早期使用较多的配置方式,但是对于配置 Monorepos 项目是比较麻烦的;
- babel.config.json(babel7):可以直接作用于 Monorepos 项目的子包,更加推荐;
babel.config.js
1 | module.exports = { |
webpack.config.js
1 | // 匹配 js 文件 |
对Vue文件的打包
对 vue 代码的打包
- 安装 vue3: npm install vue@next
element.js
1 | import { createApp } from 'vue' |
- 界面上是没有效果的:
- 并且我们查看运行的控制台,会发现有警告信息;
对 vue 不同版本的解析
- vue(.runtime).global(.prod).js:
- 通过浏览器中的
<script src="...">
直接使用; - 我们之前通过 CDN 引入和下载的Vue版本就是这个版本;
- 会暴露一个全局的 Vue 来使用;
- 通过浏览器中的
- vue(.runtime).esm-browser(.prod).js:
- 用于通过原生 ES 模块导入使用 (在浏览器中通过
<script type="module">
来使用)。
- 用于通过原生 ES 模块导入使用 (在浏览器中通过
- vue(.runtime).esm-bundler.js:
- 用于 webpack,rollup 和 parcel 等构建工具;
- 构建工具中默认是 vue.runtime.esm-bundler.js;
- 如果我们需要解析模板 template,那么需要手动指定 vue.esm-bundler.js;
- vue.cjs(.prod).js:
- 服务器端渲染使用;
- 通过 require() 在 Node.js 中使用;
对 template 模板的解析
在 Vue 的开发过程中我们有三种方式来编写 DOM 元素:
- 方式一:template 模板的方式(之前经常使用的方式);
- 方式二:render 函数的方式,使用h函数来编写渲染的内容;
- 方式三:通过.vue 文件中的 template 来编写模板;
它们的模板分别是如何处理的呢?
- 方式二中的 h 函数可以直接返回一个虚拟节点,也就是 Vnode 节点;
- 方式一和方式三的 template 都需要有特定的代码来对其进行解析:
- 方式三 .vue 文件中的template可以通过在 vue-loader 对其进行编译和处理;
- 方式一中的 template 我们必须要通过源码中一部分代码来进行编译;
所以,Vue 在让我们选择版本的时候分为 运行时+编译器 vs 仅运行时
- 运行时+编译器包含了对 template 模板的编译代码,更加完整,但是也更大一些;
- 仅运行时没有包含对 template 版本的编译代码,相对更小一些;
我们会发现控制台还有另外的一个警告:
在 GitHub 上的文档中我们可以找到说明:
- 这是两个特性的标识,一个是使用 Vue 的 Options,一个是 Production 模式下是否支持 devtools 工具;
- 虽然他们都有默认值,但是强烈建议我们手动对他们进行配置;
1 | plugins: [ |
对vue单文件组件的打包
App.vue
1 | <template> |
- 我们对代码打包会报错:我们需要合适的 Loader 来处理文件。
- 这个时候我们需要使用 vue-loader:
- 安装 vue-loader : npm install vue-loader -D
- 在 webpack 的模板规则中进行配置:
1 | // 匹配 .vue 文件 |
打包依然会报错,这是因为我们必须添加 @vue/compiler-sfc 来对 template 进行解析:
安装插件 : npm install @vue/compiler-sfc -D
另外我们还需要配置对应的 Vue 插件:
1 | const { VueLoaderPlugin } = require('vue-loader/dist/index') |
重新打包即可支持 App.vue 的写法, 另外,我们也可以编写其他的 .vue 文件来编写自己的组件;
搭建本地服务
搭建本地服务器
- 目前我们开发的代码,为了运行需要有两个操作:
- 操作一:npm run build,编译相关的代码;
- 操作二:通过 live server 或者直接通过浏览器,打开 index.html 代码,查看效果;
- 这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示;
- 为了完成自动编译,webpack 提供了几种可选的方式:
- webpack watch mode;
- webpack-dev-server(常用);
- webpack-dev-middleware;
实时自动编译
- webpack 给我们提供了 watch 模式:
- 在该模式下,webpack 依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译;
- 我们不需要手动去运行 npm run build 指令了;
- 如何开启 watch 呢?两种方式:
- 方式一:在导出的 webpack.config.js 配置中,添加 watch: true
- 方式二:在启动 webpack 的命令中,添加 –watch 的标识;
- 这里我们选择方式二,在 package.json 的 scripts 中添加一个 watch 脚本
1 | "scripts": { |
实时刷新页面
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:
- 当然,目前我们可以在 VSCode 中使用 live-server 来完成这样的功能;
- 但是,我们希望在不适用 live-server 的情况下,可以具备 live reloading(实时重新加载)的功能;
安装 webpack-dev-server,提供本地服务,实现实时刷新网页
安装插件 : npm install webpack-dev-server -D
在 package.json 中,添加 serve 指令:
package.json
1 | "scripts": { |
- webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中:
- 事实上 webpack-dev-server 使用了一个库叫 memfs(memory-fs webpack 自己写的)
热替换HMR
认识HMR
- 什么是 HMR 呢?
- HMR 的全称是 Hot Module Replacement,翻译为模块热替换;
- 模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面;
- HMR通过如下几种方式,来提高开发的速度:
- 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失;
- 只更新需要变化的内容,节省开发的时间;
- 修改了 css、js 源代码,会立即在浏览器更新,相当于直接在浏览器的 devtools 中直接修改样式;
- 如何使用 HMR 呢?
- 默认情况下,webpack-dev-server 已经支持 HMR,我们只需要开启即可;
- 在不开启 HMR 的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是 live reloading;
开启HMR
- 修改 webpack.config.js 的配置:
1 | // web 环境 |
浏览器可以看到如下效果:
但是你会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:
- 这是因为我们需要去指定哪些模块发生更新时,进行 HMR;
index.js
1 | import './js/element' |
框架的HMR
- 有一个问题:在开发其他项目时,我们是否需要经常手动去写入 module.hot.accpet相关的API呢?
- 比如开发 Vue、React 项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
- 事实上社区已经针对这些有很成熟的解决方案了:
- 比如 vue 开发中,我们使用 vue-loader,此 loader 支持 vue 组件的 HMR,提供开箱即用的体验;
- 比如 react 开发中,有 React Hot Loader,实时调整 react 组件(目前 React 官方已经弃用了,改成使用 react-refresh);
- 接下来我们来演示一下 Vue 实现一下 HMR 功能。
HMR的原理
- 那么 HMR 的原理是什么呢?如何可以做到只更新一个模块中的内容呢?
- webpack-dev-server 会创建两个服务:提供静态资源的服务(express)和 Socket 服务(net.Socket);
- express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
- HMR Socket Server,是一个 socket 的长连接:
- 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
- 当服务器监听到对应的模块发生变化时,会生成两个文件 .json(manifest文件)和 .js 文件(update chunk);
- 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
- 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新;
主机/端口号/压缩格式
host 设置主机地址:
- 默认值是 localhost;
- 如果希望其他地方也可以访问,可以设置为 0.0.0.0;
localhost 和 0.0.0.0 的区别:
- localhost:本质上是一个域名,通常情况下会被解析成 127.0.0.1
- 127.0.0.1:回环地址 (Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
- 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
- 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
- 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的
- 0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序;
- 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的;
port 设置监听的端口,默认情况下是 8080
open 是否打开浏览器:
- 默认值是 false,设置为 true 会打开浏览器;
- 也可以设置为类似于 Google Chrome等值;
compress 是否为静态文件开启 gzip compression:
- 默认值是 false,可以设置为 true;
webpack.config.js
1 | // 本地服务 |
Proxy
Proxy代理
- proxy 是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:
- 比如我们的一个 api 请求是 http://localhost:8888,但是本地启动服务器的域名是 http://localhost:8000,这个时候发送网络请求就会出现跨域的问题;
- 那么我们可以将请求先发送到一个代理服务器,代理服务器和 API 服务器没有跨域的问题,就可以解决我们的跨域问题了;
- 我们可以进行如下的设置:
- ptarget:表示的是代理到的目标地址,比如 /api-hy/moment 会被代理到 http://localhost:8888/api-hy/moment;
- pathRewrite:默认情况下,我们的 /api-hy 也会被写入到 URL 中,如果希望删除,可以使用 pathRewrite;
- secure:默认情况下不接收转发到 https 的服务器上,如果希望支持,可以设置为 false;
- changeOrigin:它表示是否更新代理后请求的 headers 中 host 地址;
1 |
changeOrigin的解析
- 这个 changeOrigin 官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的headers中的host属性:
- 因为我们真实的请求,其实是需要通过 http://localhost:8888来请求的;
- 但是因为使用了代码,默认情况下它的值时 http://localhost:8000;
- 如果我们需要修改,那么可以将 changeOrigin 设置为 true 即可;
1 |
historyApiFallback
- historyApiFallback 是开发中一个非常常见的属性,它主要的作用是解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误。
- boolean值:默认是 false
- 如果设置为 true,那么在刷新时,返回 404 错误时,会自动返回 index.html 的内容;
- object 类型的值,可以配置 rewrites 属性(了解):
- 可以配置 from 来匹配路径,决定要跳转到哪一个页面;
- 事实上 devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库的:
- 可以查看 connect-history-api-fallback 文档
1 |
resolve模块
resolve模块解析
resolve用于设置模块如何被解析:
- 在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;
- resolve可以帮助webpack从每个 require/import 语句中,找到需要引入到合适的模块代码;
- webpack 使用 enhanced-resolve 来解析文件路径;
webpack能解析三种文件路径:
绝对路径
- 由于已经获得文件的绝对路径,因此不需要再做进一步解析。
相对路径
- 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
- 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
模块路径
- 在 resolve.modules 中指定的所有目录检索模块;
- 默认值是 [‘node_modules’],所以默认会从 node_modules 中查找文件;
- 我们可以通过设置别名的方式来替换初识模块路径,具体后面讲解 alias 的配置;
- 在 resolve.modules 中指定的所有目录检索模块;
如果是一个文件:
- 如果文件具有扩展名,则直接打包文件;
- 否则,将使用 resolve.extensions 选项作为文件扩展名解析;
如果是一个文件夹:
- 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找;
- resolve.mainFiles 的默认值是 [‘index’];
- 再根据 resolve.extensions 来解析扩展名;
- 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找;
extensions和alias配置
- extensions 是解析到文件时自动添加扩展名:
- 默认值是 [‘.wasm’, ‘.mjs’, ‘.js’, ‘.json’];
- 所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;
- 另一个非常好用的功能是配置别名 alias:
- 特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 ../../../这种路径片段;
- 我们可以给某些常见的路径起一个别名;
1 | resolve: { |
开发与生产环境的分离
区分开发环境
- 目前我们所有的 webpack 配置信息都是放到一个配置文件中的:webpack.config.js
- 当配置越来越多时,这个文件会变得越来越不容易维护;
- 并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的;
- 所以,我们最好对配置进行划分,方便我们维护和管理;
- 那么,在启动时如何可以区分不同的配置呢?
- 方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
- 方式二:使用相同的一个入口配置文件,通过设置参数来区分它们;
1 |
入口文件解析
- 我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,我们是否应该变成 ../src/index.js呢?
- 如果我们这样编写,会发现是报错的,依然要写成 ./src/index.js;
- 这是因为入口文件其实是和另一个属性时有关的 context;
- context 的作用是用于解析入口(entry point)和加载器(loader):
- 官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)
- 另外推荐在配置中传入一个值;
1 |
开发与生产环境的分离
- 安装插件: npm install webpack-merge -D , 用于合并 webpack 配置
webpack.comm.config.js
1 | const path = require('path') // 导入node的内置path模块 |
webpack.dev.config.js
1 | const { merge } = require('webpack-merge') |
webpack.prod.config.js
1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') |
- 感谢你赐予我前进的力量