跳到主要内容

Webpack 概念

Webpack 是基于模块化的打包工具,他把一切文件都视为模块。 Webpack 通过开发时态的入口文件开始,通过模块化导入分析出所有依赖模块,经过一系列的处理过程,最终将开发时态的打包为运行时态的代码。 特点:

  1. 为前端工程化而生:解决工程化开发的问题,让开发者注重开发业务代码,而工程化的问题交给 Webpack 处理就行
  2. 简单易用:支持零配置,可以不用写一行配置代码,就能用 webpack。webpack 默认打包的入口文件是 src/index.js
  3. 强大的生态:Webpack 非常灵活、可扩展,其本身的功能并不能,但其强大的扩展(插件)机制,使我们可以用第三方库来增强 Webpack 的能力
  4. 基于 Nodejs:Webpack 打包需要读写文件
  5. 基于模块化:Webpack 在打包过程通过模块化导入语句来分析依赖关系的,它支持各种模块化规范,包含 Commonjs、ES6 Module、UMD等

Webpack 使用需要强制安装两个包:

  • webpack:核心包,包含 webpack 构建过程需要的所有 API
  • webpack-cli:webpack 的 cli 工具,通过调用 webpack 核心包的 API,来完成构建过程

安装:全局安装、本地安装(推荐) 简单的打包流程 Alt text

Webpack 多个模块化混合使用

Webpack 是同时支持多个模块化同时使用的,常用的是两个:Commonjs、ES6 Module。不管是哪个模块化,最终模块导出的是一个对象,模块导入的也是当前模块导出的对象。

相同模块化​

Alt text

Alt text

模块化混用​

Alt text

Alt text

Webpack 打包结果分析

  1. 先通过主模块分析出所有依赖模块,封装成 key 为模块路径,value 为模块代码的对象,传到 IIFE modules 中
  2. 定义模块缓存对象,所以相同的模块只会执行一次
  3. 定义 __webpack_require__函数,用于运行返回和缓存模块导出内容
  4. 从执行入口模块开始,执行所有运行时代码
(function (modules) { // 2.modules 所有模块的集合
// 3.缓存模块
var installedModules = {};

// ===> 运行 & 缓存模块,最后返回模块的导出结果
function __webpack_require__(moduleId) {

// 5.判断是否有缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}

// 6.创建模块对象,并缓存对象的引用
var module = installedModules[moduleId] = {
exports: {}
};

// 7.执行模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 8.返回模块导出结果 module.exports
return module.exports;
}
// 4.执行入口模块
return __webpack_require__("./src/index.js");
})(
// 1.所有模块的集合
{
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync.js");
console.log('sync', _sync__WEBPACK_IMPORTED_MODULE_0__["default"]);
}),

"./src/sync.js": (function (module, __webpack_exports__, __webpack_require__) {
const data = 'sync'
// __webpack_exports__ === module.exports
__webpack_exports__["default"] = (data);
})
}
);

Webpack 编译过程(粗粒度)

初始化​

通过 cli、配置文件、默认配置生成 webpack 最终的配置

编译​

创建 chunk​

根据 webpack 配置的入口文件创建一个 chunk(多个入口会创建多个 chunk)

chunk 有两个属性: name:chunk 的名称,默认为 main id:开发环境是 name,生产环境是数字

构建依赖​

Alt text

通过入口开始分析依赖(AST 分析),并生成对应的模块记录,模块记录包含模块 id 和转换后的代码

生成 chunk 资源列表​

通过配置的 chunk 生成最终的 chunk 文件名和文件内容,并创建文件内容的 hash(chunkhash)。

Alt text

chunk 合并​

将所有 chunk 资源合并为一个总的 assets 资源,并生成一个 hash 值

Alt text

输出​

通过 node 的 fs 模块,将总的 assets 资源列表生成相应的文件到 output 目录下。

Webpack 基本配置

devtool​

汉译为开发工具,就是方便我们开发调试需要的工具。在 Webpack 指的是 source-map(源码地图)。

我们运行的代码是打包后的代码,打包后的代码不是人看的,代码出错了,并不能定位到源代码哪个位置出错了,所以需要一个工具来帮助我们在运行时定位到源码(开发时代码)的位置,这个工具就是 Source map。Source map 实际上就是一个配置文件,给浏览器看的,它包含我们的原始代码,并记录了运行时代码和源代码之间的对应关系

Alt text

最佳实践:

  • Source Map 最好在开发环境使用,可友好的调试代码

Alt text

  • Source Map 尽量不要再生产环境使用,因为他会暴露我们的源代码,并且体积也很大,还会带来额外的请求。如果要是用的话,需要去规避这些问题

webpack 中使用:

module.exports = {
mode: "development",
devtool: "eval",
};

几种常见 devtool:

  • eval:映射的是 loader 转换后的源代码

Alt text

Alt text

  • eval-source-map 对生成的 source-map 文件 base64 加密,并且添加到 eval 中,这种方式可以看到源代码和运行时代码的真是映射

Alt text

Alt text

  • cheap-eval-source-map 类似 eval-source-map ,低开销的 source-map,显示转义后的代码,并且只能定位行,不能定位到列

entry & output​

用于配置 入口文件和输出文件

const path = require("path");

module.exports = {
mode: "development",
entry: { // 多个入口会生成多个 chunk
main: "./src/index.js",
// 将多个入口打包到一个 chunk 中,入口加载时从前往后执行
test: ["./src/a.js", "./src/b.js"],
},
output: {
// chunkhash:5 原始 hash 很长,我们只需要截取前几位就行,5 就是截取前 5 位
filename: "[name].[chunkhash:5].js",
path: path.resolve(__dirname, "build"),
},
};

filename 占位符规则:

  • name:chunkname,entry 中配置的 key
  • id: 使用 chunkid 当 name,不推荐,生产环境 chunkid 是数字
  • hash:总资源的 hash,多个 chunk 资源文件的 hash 是一样的,如果一个文件内容改变,所有 chunk 资源文件的 hash 都会变

Alt text

  • chunkhash:使用 chunk hash,每个 chunk 资源文件的 hash 不同,修改的文件和其他 chunk 没有依赖关系,就不会改变 chunk 资源的 hash

Alt text

loader​

一个函数,参数是代码字符串,返回值是一个新的(修改后的)代码字符串。在文件读取后,AST 处理之前执行

Alt text

使用场景:webpack ast 语法分析只支持 js 语法,所以需要 loader 来将不是 js 语法的代码文件,转为 js 语法的代码。比如处理 css、图片、字体图标等文件,在进行语法分析前都需要 loader 来处理;还可用来压缩源代码,反正基于源文件内容的修改都可用 loader 来实现。

loader 的使用​

自定义一个 loader:替换匹配的字符串为 Hello,匹配项可自定义

loader-utils 可以处理配置项 options 的参数

const loaderUtils = require("loader-utils");

// 替换 replaceText 属性值为 Hello
module.exports = function (sourceCode) {
const options = loaderUtils.getOptions(this);
return sourceCode.replace(new RegExp(options.replaceText, "g"), "Hello");
};

module.exports = {
mode: "development",
module: { // 针对模块处理的配置
rules: [// 定义模块匹配规则,模块匹配指的是模块 ID(文件的相对路径) 的匹配
{
test: /index\.js$/, // 正则匹配模块 ID
use: [// 匹配的文件使用 loader 处理
{
loader: "./lib/loaders/custom-loader.js",
options: {
replaceText: "你好",
},
},
],
},
{
test: /\.js$/,
// 等效 上面 loader 的写法
use: ["./lib/loaders/custom-loader.js?replaceText=你好"],
},
],
},
};

loader 的处理流程​

当前模块匹配的 loader 会形成一个数组,loader 依次自右向左执行 loader 的配置上的处理顺序:从上到下匹配,从下往上、从右到左依次执行

Alt text

loader 小练习​

loader 一个重要的作用是将模块转为 js 语法,所以可以尝试自己实现一下

处理 CSS 文件​

方法:将 css 代码用 js 动态插入到 head 标签内

module.exports = function (sourceCode) {
return `
var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.getElementsByTagName("head")[0].appendChild(style);
modules.exports = \`${sourceCode}\`;
`;
};

Alt text

处理图片​

方法:必须拿到源文件的 buffer (二进制文件),然后小文件用 base64 ,大文件生成到打包目录下

const loaderUtils = require("loader-utils");

function imageLoader(buffer) {
const { limit = 1000, name = "[contenthash:5].[ext]" } =
loaderUtils.getOptions(this);

let content = "";
if (buffer.byteLength >= limit) {
content = getFilePath.call(this, buffer, name);
} else {
content = getBase64(buffer);
}

return `module.exports = \`${content}\``;
}

// 将 buffer 转为 base64 格式
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}

// 将图片输出到总资源 assets,并返回文件路径
function getFilePath(buffer, name) {
// 按照规则生成文件名字
const filename = loaderUtils.interpolateName(this, name, {
content: buffer,
});
// 可以将文件输出的打包目录下
this.emitFile(filename, buffer);
return filename;
}

imageLoader.raw = true; // 处理原始 buffer
module.exports = imageLoader;

Alt text

plugin​

loader 主要负责的是代码转换,在文件被读取后执行。plugin 负责整个编译过程,loader 发生在文件打包之前,所以 plugin 使用面更广。 plugin 的本质是一个带有 apply 方法的对象

const plugin = {
apply: function(compiler){}
};

但通常会写成 类的形式,更方便增强插件的功能

class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
//事件处理函数
})
}
}

插件的 apply 放在是在初始化 Complier 后执行的,整个编译完成打包工作是 compiler 内部创建的 compilation 完成的,每一次编译都会重新生成一个 compilattion,而 compiler 只会在初始化时创建,插件的 apply 方法也是只执行一次。 apply 执行的目的是,将我们写的事件函数注册到 webpack 的钩子上

Alt text

一个简单地 plugin​

功能:生成打包清单

将文件生成到打包目录下,只需要在总资源列表添加一个文件对象,必须包含属性 source 和 size 方法

module.exports = class FileListPlugin {
// 初始化生成的文件名
constructor(filename) {
this.filename = filename;
}

apply(compiler) {
// 生成资源到 ouput 目录之前
compiler.hooks.emit.tap("FileListPlugin", (compilation) => {
const assets = compilation.assets; // 所有生成的资源文件的信息
let fileContent = [];
Object.keys(assets).forEach((key) => {
fileContent.push(
`【文件】${key}\n【大小】${assets[key].size() / 1000} KB`
);
});
const str = fileContent.join("\n\n");

// 往最终生成的资源中,添加一个文件
// 格式也必须是这样
compilation.assets[this.filename || "filelist.txt"] = {
source() { // 文件内容
return str;
},
size() { // 文件大小
return str.length;
},
};
});
}
};

注册

const FileListPlugin = require("./lib/plugins/FileListPlugin");
module.exports = {
mode: "development",
plugins: [new FileListPlugin("filelist.md")],
};

Alt text

env​

我们需要区分开发环境和生产环境的 webpack 配置,这时可以通过命令行参数 env 来传 env 的定义方式不同,对应输出的 结果也会不同,通常用于判断环境可以用 第一种方式,若需要定义其他变量可以使用第二种

  • webpack --env development:{ development: true }
  • webpack --env development=true:{ development: "true" }
  • webpack --env.development``webpack --env.development=true webpack 5 不支持这两种写法

webpack 的配置也需要该问函数的形式,参数是 env,返回值是 webpack 的配置对象

const baseConfig = require("./webpack.base"); // 公共配置
const devConfig = require("./webpack.dev"); // 开发环境配置
const proConfig = require("./webpack.pro"); // 生产环境的配置

module.exports = function (env) {
if (env.prod) {
return {
...baseConfig,
...proConfig
}
} else {
return {
...baseConfig,
...devConfig
}
}
}

publicPath​

是一个字符串,拼接在打包后文件路径的前面publicPath + 文件路径,其实 publicPath 并没有什么实际意义,用不用的到是 plugin 和 loader 自己控制的。

console.log(__webpack_public_path__);

// 打包后
console.log(__webpack_require__.p);

设置可以在,output 中直接设置,但如果有的扩展用到 publicPath,可能会出现文件找不到的情况,所以需要注意下

扩展​

webpack 的扩展其实就是 loader 和 plugin,他们可以增强 webpack 功能

【plugin】 clean-webpack-plugin​

清空资源输出目录,不需要我们手动的去删除 原理:compiler emit 钩子注册一个方法,该方法会用 fs 模块来删除上一次生成的文件目录

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
// ....
plugins: [new CleanWebpackPlugin()],
};

【plugin】 html-webpack-plugin​

生成页面模板,模板可以自定义。如果站点是多入口的,可以 new 多个实例来生成多个 html 模板,每个模板插入的 chunk 可以自定义。 原理:compiler emit 钩子下,注册一个方法,该方法会在最终需要输出的资源下,添加一个 html 文件,并将 js 文件,通过 script 标签,添加到模板中

const HTMWebpackPlugin = require("html-webpack-plugin");

module.exports = {
mode: "development",
entry: {
main: "./src/index.js",
test: "./src/test.js",
},
plugins: [
new HTMWebpackPlugin({
template: "./public/index.html", // 模板文件的路径
filename: "main.html", // 重命名模板
chunks: ["main"], // chunkname 的数组
}),
new HTMWebpackPlugin({
template: "./public/index.html",
filename: "test.html",
chunks: ["test"],
}),
],
};

Alt text

【plugin】 copy-webpack-plugin​

拷贝文件,将开发时代码不需要编译的文件,拷贝到输出目录

  • from 文件源文件(文件夹)
  • to 目标目录或文件,文件路径相对与 output 目录
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [{ from: "./public/images", to: "./images" }],
}),
],
};

webpack-dev-server​

开发服务器,既不是 loader 也不是 plugin,是 webpack 官方提供的,方便开发。打包命令和 webpack-cli 类似 原理:

  • 处理传的命令参数,处理后将参数交给 webpack
  • 在配置中打开 watch
  • 打包不生成最终文件,注册 compiler.emit 钩子,将最终 assets 文件列表文件缓存,并将 assets 资源列表置为空对象,保证打包后文件不输出到 output 目录下
  • 启动一个 express 服务器,来处理资源的访问
module.exports = {
devServer: {
port: 8080,
open: true,
openPage: "main.html",
proxy: {
"/api": {
target: "https://xxx.com",
changeOrigin: true, // 更改请求头的 host 和 origin
},
},
},
};

【loader】file-loader、url-loader​

共同点

  • 文件经过 loader 转换后,默认都是使用 ES6 Module 导出的
  • 可以自定义 name,但 name 的 hash 是 contenthash 不是总的资源列表的 hash,因为这时总资源列表还没生成

区别:

  • file-loader:返回打包后文件的路径
  • url-loader:可通过 limit 判断,文件大小小于 limit 时将文件 base64 编码,大于 limit 就使用 file-loader 将文件打包到输出目录,返回文件的路径
{
test: /\.(jpg|png|jpeg)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[hash:5].[ext]",
},
},
],
},
{
test: /\.(jpg|png|jpeg)$/,
use: [
{
loader: "url-loader",
options: {
limit: 1000,
name: "[name].[hash:5].[ext]", // 大于 limit 才会生效
},
},
],
},

内置插件​

样式模块化

webpack 本身并没有处理 css 样式文件的能力,所以需要使用 loader 对样式代码进行转换

css-loader​

作用:将 css 代码转为 js 代码 原理:将 css 转为字符串的导出 配置

{
test: /\.css$/,
use: ["css-loader"],
}

无依赖​

css-loader 处理后的代码,执行会返回一个 Module,调用 toString 方法可以获取到转换后的代码

body {
background-color: aquamarine;
}

const css = require("./index.css");
console.log(css);

Alt text

处理流程

Alt text

有依赖​

如果 css 文件里面依赖了其他文件资源时,会递归处理依赖的文件

@import "./reset.css";
body {
background-color: aquamarine;
background-image: url(./logo.png);
}

处理流程

Alt text

css-loader 只做了 css 代码字符串的转换,并没有将样式添加到页面上,所以需要 style-loader 做进一步处理

style-loader​

将 css-loader 处理好的代码增加添加到页面 style 标签的能力,其实就是在代码字符串上增加了一些额外的代码 就上面的例子,在经过 style-loader 处理后的代码字符串

Alt text

CSS Module​

主要目的是解决样式类名的冲突,可以通过 css-loader 开启

  • css-loader 会对我们的类名进行 hash 编码,hash 编码是对「文件 id、类名」进行 hash 编码,这种方式只能保证不同文件的类名不冲突,同文件我们也不会写类名冲突的样式,所以可以很好的解决类名冲突的问题
  • style-loader 除了将样式放到 style 标签,还可以格式 css-loader 的内容,只返回真实类名和 hash 编码类名的映射关系,我们在写代码只需要引用真实类名就好

配置

{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
// modules: true, // 开启 css module
modules: { // 配置了 modules 默认开启了 css module
localIdentName: "[name]-[hash:5]", // 自定义类名规则
},
},
},
],
}

一个简单的例子

Alt text

使用

import css from "./index.css"; // 引入的 css 是一个 类名与 hash 的对象
/*
{
[真实类名]: [hash 编码后的类名]
}
*/
console.log(css.c1); // _1TyUMnubgEBLCDM5y3ayYM

如果某些全局样式,不希望被 hash 编码,可以使用 :global(.类名){} 区分开,css module 默认是 local 模式,所以一般省略了

预编译语言 Less​

受限于 css 语言本身,开发中常会有一些问题

  1. 重复的样式代码段
  2. 重复的样式值
  3. 重复的嵌套

css 标准也迟迟没有给出好的解决方法,于是一些第三方机构给出了一些解决办法。 预编译语言就是第三方机构的产物,常见的有 less 和 sass,基本解决的问题都差不多,没必要学习两个,了解一个后,其他的看看文档就可直接上手。

浏览器是不识别这类语法的,需要每个预编译语言,使用对应的编译器,将当前语言处理为 css

Alt text

less 配置​

css-loader 和 style-loader 不会对预编译语言进行处理,需要使用对应的 loader 进行处理。less 使用 less-loader,作用是调用 less 编译器的能力,将 less 语法转为 css 语法

安装:pnpm add less less-loader -D

{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
}

使用​

变量​

可提取样式值

@width: 10px;
@height: @width + 10px;

#header {
width: @width;
height: @height;
}

编译后

#header {
width: 10px;
height: 20px;
}

混合​

解决代码样式代码段的重复

  • 不带 (),会被当做普通样式,输出到目标文件中
  • 带 (),是 less 语法,会被编译器删掉

Alt text

嵌套​

解决了类名的重复写,重复嵌套 右边代码更优雅

Alt text

运算​

变量值是支持运算的

@base: 5%;
@filler: @base * 2; // 结果是 10%
@other: @base + @filler; // 结果是 15%

// calc 特例
@var: 50vh/2;
@width: calc(50% + (@var - 20px)); // 结果是 calc(50% + (25vh - 20px))

转义​

提取一些代码,赋给变量,达到简写、方便配置的功能

Alt text

函数​

参考 less 函数

映射​

3.5 版本后的功能,类似一个对象,使用 # 定义

Alt text

作用域​

块作用域

Alt text

注释​

less 增加了一个行注释 // 注释内容

import​

引入依赖样式

  • 如果是 less 文件可省略后缀,并将 less 文件处理后的样式,输出到目标 css 文件
  • 如果是 css 文件,原样输出到目标 css 文件

Alt text

PostCSS​

css 问题这么多,为什么不提出来统一处理呢?postcss 的出现就是解决这个问题的。 post 类似一个编译器,将样式源码编译为最终的 CSS 代码。看上去和 Less Sass 一样,但思路不同。postcss 只是做了一些代码解析的事情,代码转换交给插件来处理 Alt text

安装​

pnpm add postcss postsee-cli -D

  • postcss:node 编写的库,对外提供了很多 API
  • postsee-cli:通过调用 postcss 的 API 来完成编译

配置​

postcss 和 webpack 一样,有自己的配置文件 postcss.config.js

module.exports = {
map: false, //关闭source-map
}

插件​

postcss 本身做不了多少事情,需要借助插件来丰富编译功能,postcss的插件市场:https://www.postcss.parts/

postcss-preset-env​

和 babel 预设类似,就是将常用的插件整合在一起,让使用者只需要安装这一个插件相当于安装了一些常用的插件

module.exports = {
plugins: {
"postcss-preset-env": {} // {} 中可以填写插件的配置
}
}

然后就可以使用了,功能有很多

自动补厂商前缀​

使用 autoprefixer 来完成的,postcss-preset-env 已经内置了

Alt text

可以配置浏览器兼容范围,样式前缀也会有所不同 三种配置方式:推荐第二种

  1. postcss-config.css 中
module.exports = {
plugins: {
"postcss-preset-env": {
browsers: [
"last 2 version",
"> 1%"
]
}
}
}
  1. 单独的配置文件 .browserslistrc
last 2 version
> 1%
  1. package.json中配置
"browserslist": [
"last 2 version",
"> 1%"
]
  • last 2 version: 浏览器的兼容最近期的两个版本
  • 1% in CN: 匹配中国大于1%的人使用的浏览器, in CN可省略

  • not ie <= 8: 排除掉版本号小于等于8的IE浏览器
未来的语法​

css 标准制定中时,还没有正式称为标准,如果我们想使用这些语法,需要使用 cssnext库(已被弃用,使用 postcss-preset-env即可),而 postcss-preset-env已经内置了该库,所以可以直接配置 postcss 使用。

通过 stage来配置,值表示需要兼容到 css 标准制定的那个阶段,默认值是 2

module.exports = {
plugins: {
"postcss-preset-env": {
stage: 0
}
}
}

可配置的值有五种

  • Stage 0: Aspirational - 只是一个早期草案,极其不稳定
  • Stage 1: Experimental - 仍然极其不稳定,但是提议已被W3C公认
  • Stage 2: Allowable - 虽然还是不稳定,但已经可以使用了
  • Stage 3: Embraced - 比较稳定,可能将来会发生一些小的变化,它即将成为最终的标准
  • Stage 4: Standardized - 所有主流浏览器都应该支持的W3C标准
  1. 变量

原语法依然会被编译到最终的 css 环境中,如果想去掉,可以配置** **postcss-preset-env.preserve=false,会去掉源语法,只生成兼容的 css 代码

Alt text

配置 preserve 后

Alt text

  1. 自定义选择器

@custom-selector [名称] [选择器集合],stage 为 0 时才会生效

Alt text

  1. 嵌套

Alt text

postcss-apply​

支持定义一些属性集,后续可以直接使用 @apply [属性集名称]来使用

Alt text

该插件不支持最新 8.X 版本的 postcss 编译不通过,需要降版本

Alt text

postcss-color-function​

使用一些颜色函数

  • color(#aabbcc a(90%)):透明度
  • color(#aabbcc red(90%)):红色部分设为 90%
  • color(#aabbcc tint(90%)):调亮 90%
  • color(#aabbcc shade(90%)):调暗 90%
  • .........

Alt text

stylelint​

验证样式书写规范,我们可以去配置适合自己的规范,官网地址:https://stylelint.io/

pnpm add stylelint stylelint-config-standard -D

stylelint-config-standard提供了比较好的验证,然后在配置文件里应用

{
"extends": "stylelint-config-standard",
"rules": {
"indentation": null // 禁用一些规则,把值置为 null
}
}

webpack 使用​

pnpm add postcss postcss-preset-env stylelint stylelint-config-standard postcss-loader -D

module.exports = {
module: {
rules: [
{
test: /\.pcss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[name]-[hash:5]",
},
},
},
"postcss-loader",
],
},
],
},
};
module.exports = {
map: false,
plugins: {
"postcss-preset-env": {
preserve: false,
stage: 0,
},
},
};

last 3 version
> 1%
not ie <= 8

样式抽离​

之前的 css-loader 只是将 css 语法转为 js 语法,并将样式字符串导出。style-loader 在 css-loader 的基础上增加了将 样式字符串添加到页面 style 标签的能力,如果 css-loader 启用了 CSS Module 还会导出样式类名的映射,否则导出空对象。 如果我们希望将 css 单独提取出来,就需要 loader 和 plugin 配合才能很好的做到,这种库有很多,比如下面说道的 mini-css-extract-plugin mini-css-extract-plugin 提供了 loader 和 plugin

  • loader:不提供将样式通过 style 插入到页面上,只是把最终样式内容保存起来,同时导出 CSS Module 样式类的映射关系
  • plugin:生成样式文件,输出到 output 目录下

配置

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
mode: "development",
devtool: "source-map",
entry: {
index: "./src/index.js",
test: "./src/test.js",
},
output: {
filename: "scripts/[name].[chunkhash:5].js",
path: path.resolve(__dirname, "build"),
publicPath: "/",
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[name]-[hash:5]",
},
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "styles/[name].[contenthash:5].css",
}),
],
};

如果有多入口,可以自定义文件 name,根据不同的入口,为当前入口生成单独的样式文件

  • name:入口 chunk name
  • contenthash:css 文件内容的 hash 值

注意:filename 如果配置为路径的形式,根路径是相对与 output 配置的输出目录,开始不要写 ./,不然其他文件加载该资源时,路径会匹配为 [publicPath]./xxx/xxx.css

Alt text

js 兼容处理(Babel)

主要就是兼容性,很多浏览器是不兼容 js 的一些语法,所以需要工具做降级处理,让我们开发时可以用 js 的最新语法和 API 特性,编译后可以生成兼容性更好的 js 代码。 通常会用 babel 来处理 js 文件代码,babel 不是 webpack 独有的工具,是独立的,我们可以单独的使用它

babel 介绍​

简介​

babel 最开始叫 6to5,顾名思义是将 es6 转 es5,但是后来随着 es 标准的推进,有了 es7、es8 等, 6to5 的名字已经不合适了,所以改名为了 babel。

babel一词来自于希伯来语,直译为巴别塔。

当时人类联合起来兴建希望能通往天堂的高塔,为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此事件,为世上出现不同语言和种族提供解释。这座塔就是巴别塔。

和上面的典故一样,我们 js 也在面领着这样一个问题。每个浏览器支持的 js 标准语法和 API 都会存在一定的差异,因此为了所有浏览器都支持我们开发写的不同标准的代码,需要一个工具来将我们的代码转为各个浏览器都能识别的代码。babel 就在这个背景下产生了。

babel 和 webpack 差不多,本身提供的的功能并不多,需要一些插件来增强

Alt text

安装​

使用 babel 需要安装两个基础包

  1. @babel/core:核心包,提供编译所需要的 API
  2. @babel/cli:调用 babel 核心包的能力,来编译模块。这个包只有在单独使用 babel 才会使用,webpack 中是使用 babel-loader来调用核心包的能力的

pnpm add @babel/core @babel/cli -D

使用​

可以处理单个文件或单个目录

babel 源文件 -o 目标文件 babel 源目录 -d 目标目录

示例

babel src/index.js -o dist/index.min.js babel src -d dist

但这样处理后的代码,和编译前的代码没什么区别

Alt text

所以可以配置一些插件来增强编译的功能,通常可以建一个 .babelrc 的配置文件,配置有两项

  • presets预设,多个插件的杂合体,一般一个预设插件里包含多个插件,可以解决一系列的兼容问题
  • plugins配置单个插件
{
"presets": [],
"plugins": []
}

presets预设是对那些已经形成标准语法 js 进行转换,对于处于早期阶段、还没有确定的语法不做转换,只是就需要使用 plugins 插件来处理了

预设(presets)​

语法和 API 的区别

  • 语法就是一些写法的区别,比如 const、async...await
  • API 是函数或者对象
  1. 语法转换

最常用的预设是 @babel/preset-env,可以让我们使用新的 js 语法,而无需去配置一些单独的插件做额外的处理

pnpm add @babel/preset-env -D

{
"presets": [
"@babel/preset-env"
],
"plugins": []
}

在运行 babel src/index.js -o dist/index.js

Alt text

可以看到,const 语法被转为了 var。 @babel/preset-env 还提供了一个功能,来控制浏览器的兼容范围。可已通过配置文件 .browserslistrc 来处理

last 3 version
> 1%
not ie <= 8
  1. API 转换

@babel/preset-env 默认只是对语法进行转换,并不会对新的 API 进行转换,比如 Promise

Alt text

所以那些不支持 Promise 运行编译后的代码就会报错。因此我们可以对预设添加配置项,来解决这个问题

{
"presets": [
"@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}
],
"plugins": []
}

useBuiltIns可以往代码注入新 API ES5 的实现,默认值为 false,表示不注入任何 API,如果低版本浏览器不支持新 API,就会报错。我们可以将 useBuiltIns 值改为 usage,表示注入新 API 的兼容实现,并且是按需引入。corejs和 useBuiltIns搭配使用,corejs主要就是对新 API ES5 的实现。

@babel/polyfill 之前用的比较多,但已过时,目前被 core-js 和 generator-runtime 所取代

Alt text

插件(plugins)​

预设和插件都可以对代码进行转换,执行顺序:plugin(前 -> 后) --> presets(后 -> 前) 一些好用的插件

  1. @babel/plugin-proposal-class-properties

在类中写初始化字段,ES2022 已经支持,在新版本的 @babel/preset-env已经注册了,不需另做配置

class A {
a = 1;
constructor() {
this.b = 3;
}
}

  1. @babel/plugin-proposal-function-bind

好用的 this 绑定,但 vscode 不识别,用的时候会导致标红

const box = {
weight: 2,
getWeight() {
return this.weight;
},
};

const { getWeight } = box;

function add(val) {
return this + val;
}

console.log(bigBox::getWeight()::add(5)); // prints '15'
  1. babel-plugin-transform-remove-console

去除 console.log()

Alt text

webpack 配置​

webpack 不需要安装 @babel/cli,需要安装 babel-loader,babel-loader会调用 babel 核心包来对代码进行转换

module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader",
},
],
},
};

{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage", // usage 需要安装 core-js@3
"corejs": 3 // 三版本的 core-js
}]
],
"plugins": []
}