Skip to content

S09-02 Webpack5-跨域、source-map、Babel、devServer

[TOC]

跨域

概述

同源策略: 是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

同源: 如果两个 URL 的 protocolport(如果有指定的话) 和 host都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是“元组”。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同(http:// 默认端口是 80)
http://news.company.com/dir/other.html失败主机不同

跨域: 事实上跨域的产生和前端分离的发展有很大的关系:

  • 早期的服务器端渲染的时候,是没有跨域的问题的;

  • 但是随着前后端的分离,目前前端开发的代码和服务器开发的API接口往往是分离的,甚至部署在不同的服务器上的;

这个时候我们就会发现,访问 静态资源服务器 和 API接口服务器 很有可能不是同一个服务器或者不是同一个端口。

  • 浏览器发现静态资源和API接口(XHR、Fetch)请求不是来自同一个地方时(同源策略),就产生了跨域

所以,在静态资源服务器和API服务器(其他资源类同)是同一台服务器时,是没有跨域问题的。

前端我们学习了很多服务器开发的知识,接下来,我们就可以演示一下跨域产生和不产生的项目部署区别了。

跨域演练

搭建服务器

1、生成package.json文件

sh
pnpm init

2、安装 koa@koa/router

sh
pnpm i koa @koa/router

3、创建src/index.js文件

image-20240612171035465

4、添加脚本

image-20240612170951839

5、运行服务器

sh
pnpm run start

添加接口

1、添加/list接口

image-20240612171249995

2、在浏览器中发送请求,获取数据

image-20240612171327207

前端代码

实际开发中,并不是直接在浏览器中通过发送url地址直接获取接口数据的。而是在前端代码中发起请求,获取的数据

1、编写前端代码

image-20240612171631917

2、在前端代码中通过xhrfetchaxios发起请求,获取接口数据

  • xhr请求

    image-20240612172539967

  • fetch请求

    image-20240612171951853

3、如果通过live server打开index.html,会开启一个服务器http://localhost:5500运行文件。此时就会出现在http://localhost:5500服务器上向http://localhost:8000的服务器中发送请求。由于它们的端口不同,所以会形成跨域请求。

image-20240612172703924

注意: 在Chrome125版本中居然没有出现跨域问题!其他浏览器依然有跨域问题

解决: 这是由于在chrome上安装了 Allow CORS: Access-Control-Allow-Origin 插件

image-20240614110853845

同服务器部署

如果想解决以上跨域问题,可以将前端静态资源和接口代码部署到同一个服务器上,可以通过 koa-static实现该想法。

1、安装 koa-static

sh
pnpm i koa-static

2、在接口服务器中部署前端静态资源

image-20240612173344086

3、启动服务器

sh
pnpm run start

4、通过http://localhost:8000访问静态资源,此时静态资源和接口就都在http://localhost:8000服务器上,不存在跨域问题

image-20240612173613640

解决方案

那么跨域问题如何解决呢?

  • 所以跨域的解决方案几乎都和服务器有关系,单独的前端基本解决不了跨域(虽然网上也能看到各种方案,都是实际开发基本不会使用)。

  • 你说:老师,不对丫,我明明和配置前端的webpack就可以解决跨域问题了。

  • webpack配置的本质也是在webpack-server的服务器中配置了代理。

跨域常见的解决方案:

  • 方案一:同服务器部署,静态资源和API服务器部署在同一个服务器中;

  • 方案二:CORS, 即是指跨域资源共享;

  • 方案三:Node代理服务器(webpack中就是它);

  • 方案四:Nginx反向代理

不常见的方案:

  • jsonp:现在很少使用了(曾经流行过一段时间);

  • postMessage:有兴趣了解一下吧;

  • websocket:为了解决跨域,所有的接口都变成socket通信?

  • …..

方案-同服务器部署

见:同服务器部署

方案-CORS

跨源资源共享(Cross-Origin Resource Sharing,CORS): 它是一种基于http header的机制。该机制通过允许服务器标示除了它自己以外的其它源(域、协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。

浏览器将 CORS 请求分成两类:

  • 简单请求

  • 非简单请求。

简单请求: 只要同时满足以下两大条件,就属于简单请求(不满足就属于非简单请求)(了解即可)。

  • 请求方法是以下是三种方法之一:

  • HEAD

    • GET

    • POST

  • HTTP 的头信息不超出以下几种字段:

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID

    • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

简单请求设置CORS

image-20240612180529329

非简单请求设置CORS

image-20240819181308598

方案-Node代理服务器(开发)

Node代理服务器是平时开发中前端配置最多的一种方案。

方案1: 同服务器部署Node代理服务器

1、创建Node代理服务器(9000端口)

image-20240613141158319

2、修改前端服务器请求的地址为Node代理服务器的地址

image-20240613142012216

3、安装 http-proxy-middleware

sh
pnpm i http-proxy-middleware

4、在Node代理服务器中,通过插件转发请求地址到目标资源所在的服务器

image-20240613142116498

5、将Node代理服务器和前端服务器部署到同一个服务器中

image-20240613142543902

6、在浏览器通过locahost:9000打开前端项目

image-20240613142708676

注意: 在webpack中也是使用的 http-proxy-middleware 插件,配置如下:

js
module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        pathRewrite: { '^/api': '' }
      }
    }
  }
}

方案2: Node代理服务器开启CORS

前4步同【方案1: 同服务器部署Node代理服务器

5、在Node代理服务器中设置CORS

image-20240613163939551

5、通过live server打开前端代码,发起跨域请求

image-20240613164039691

方案-Nginx反向代理(部署)

1、安装Nginx

下载Nginx地址:https://nginx.org/en/download.html

2、配置Nginx反向代理

image-20240820095736678

3、重启Nginx

sh
systemctl restart nginx

nginx

4、修改前端请求代码

image-20240613145545692

source-map

概述

source-map: 是一种描述原始源代码与转换后代码之间位置对应关系的映射关系表。通过使用 source-map,开发者可以在浏览器的开发者工具中直接查看源代码,并且能够正确地调试源代码,而不需要去查看压缩后的代码。

我们的代码通常运行在浏览器上时,是通过打包压缩的。也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的;

  • 比如 ES6 的代码可能被转换成 ES5;

  • 比如对应的代码行号、列号在经过编译后肯定会不一致;

  • 比如代码进行丑化压缩时,会将变量名称等修改;

  • 比如我们使用了 TypeScript 等方式编写的代码,最终转换成 JavaScript;

但是,当代码报错需要调试时(debug),调试转换后的代码是很困难的

但是我们能保证代码不出错吗?不可能。

那么如何可以调试这种转换后不一致的代码呢?答案就是source-map

开启 source-map

生成 source-map:

1、配置 webpack 生成 source-map:devtool: 'source-map'

根据源文件,生成 source-map 文件,webpack 在打包时,可以通过配置生成 source-map

image-20240222162653138

image-20240222162648569

2、在转换后的代码,最后添加一个注释,它指向 source-map

浏览器会根据我们的注释,查找相应的 source-map,并且根据 source-map 还原我们的代码,方便进行调试。

js
//# sourceMappingURL=bundle.js.map

打开浏览器 source-map:

在 Chrome 中,我们可以按照如下的方式开启 source-map:

image-20240221141613001

source-map 文件

最初 source-map 生成的文件大小是原始文件的 10 倍,第二版减少了约 50%,第三版又减少了 50%,所以目前一个 133kb 的文件,最终的 source-map 的大小大概在 300kb。

目前的 source-map 长什么样子呢?

  • version:当前使用的版本,也就是最新的第三版;
  • sources:从哪些文件转换过来的 source-map 和打包的代码(最初始的文件);
  • names:转换前的变量和属性名称(因为我目前使用的是 development 模式,所以不需要保留转换前的名称);
  • mappings:source-map 用来和源文件映射的信息(比如位置信息等),一串 base64 VLQ(variable-length quantity 可变长度值)编码;
  • file:打包后的文件(浏览器加载的文件);
  • sourcesContent:转换前的具体代码信息(和 sources 是对应的关系);
  • sourceRoot:所有的 sources 相对的根目录;

image-20240222164930699

image-20240222165831648

devtool

作用: 控制是否生成,以及如何生成 source-map

文档: https://webpack.docschina.org/configuration/devtool

常见 devtool 选项:

不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

  • 不生成 source-map 文件:

  • false:``,不使用 source-map,也就是没有任何和 source-map 相关的内容。

  • none:``,

  • eval:``,

  • development 环境:

  • eval:``,(development 模式下的默认值),不生成 source-map 文件。但是可以映射出 source-map

    • 但是它会在 eval 执行的代码中,添加//# sourceURL=
    • 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码。但不是很准确
  • cheap-source-map: ``,会生成 sourcemap,但是会更加高效一些(cheap 低开销),因为它没有生成列映射

  • cheap-module-source-map: ``,会生成 sourcemap,类似于 cheap-source-map,但是对源自 loader(如 babel-loader)的 sourcemap 处理会更好。

  • production 环境:

  • none:``,(production 模式下的默认值),什么值都不要写,不生成 source-map。

  • source-map: ``,生成独立的 source-map 文件,最完整的映射关系,但会增加打包后文件的大小,不适合生产环境使用。主要用于打包上线后发现错误时临时调试用

  • eval-source-map: ``,会生成 source-map,但是 source-map 是以 DataUrl 添加到 eval 函数的后面

  • inline-source-map: ``,会生成 source-map,但是 source-map 是以 DataUrl 添加到 bundle 文件的后面

  • hidden-source-map: ,会生成 source-map,但是不会对 source-map 文件进行引用。

    • 相当于删除了打包文件中对 source-map 的引用注释。
    • 需要手动添加://# sourceMappingURL=bundle.js.map
  • nosources-source-map: ``,会生成 source-map,但是生成的 source-map 只有错误信息的提示,不会生成源代码文件

eval

image-20240221141712251

image-20240221141720161

cheap-source-map

cheap-source-map

  • 会生成 sourcemap,但是会更加高效一些(cheap 低开销),因为它没有生成列映射(Column Mapping)

  • 因为在开发中,我们只需要行信息通常就可以定位到错误了

image-20240221141816652

cheap-module-source-map

cheap-module-source-map

  • 会生成 sourcemap,类似于 cheap-source-map,但是对源自 loader 的 sourcemap 处理会更好。

**这里有一个很模糊的概念:**对源自 loader 的 sourcemap 处理会更好,官方也没有给出很好的解释

  • 其实是如果 loader 对我们的源码进行了特殊的处理,比如 babel;

如果我这里使用了 babel-loader(注意:目前还没有详细讲 babel)

  • 可以先按照我的 babel 配置演练;

image-20240221141828629

cheap-source-mapcheap-module-source-map的区别:

cheap-module-source-map 对经过 loader 处理后的 source-map 处理的更好,不会额外删除一些空白行

image-20240221141841824

source-map

source-map

  • 生成一个独立的 source-map 文件,并且在 bundle 文件中有一个注释,指向 source-map 文件;

bundle 文件中有如下的注释:

  • 开发工具会根据这个注释找到 source-map 文件,并且解析;
js
//# sourceMappingURL=bundle.js.map

image-20240221141735235

eval-source-map

eval-source-map:会生成 sourcemap,但是 source-map 是以 DataUrl 添加到 eval 函数的后面

image-20240221141749998

inline-source-map

inline-source-map:会生成 sourcemap,但是 source-map 是以 DataUrl 添加到 bundle 文件的后面

image-20240221141803902

hidden-source-map

hidden-source-map

  • 会生成 sourcemap,但是不会对 source-map 文件进行引用;

  • 相当于删除了打包文件中对 sourcemap 的引用注释;

js
// 被删除掉的
//# sourceMappingURL=bundle.js.map

如果我们手动添加进来,那么 sourcemap 就会生效了

nosources-source-map

nosources-source-map

  • 会生成 sourcemap,但是生成的 sourcemap 只有错误信息的提示,不会生成源代码文件;

正确的错误提示:

image-20240221141908121

点击错误提示,无法查看源码:

image-20240221141917973

多个值的组合

事实上,webpack 提供给我们的26个值,是可以进行多组合的。

组合的规则如下:

  • *inline-|hidden-|eval:*三个值时三选一;

  • *nosources:*可选值;

  • cheap可选值,并且可以跟随module的值;

js
[inline-|hidden-|eval-] [nosources-] [cheap-[module-]] source-map

那么在开发中,最佳的实践是什么呢?

  • 开发阶段:推荐使用 source-map或者cheap-module-source-map

    • 这分别是 vue 和 react 使用的值,可以获取调试信息,方便快速开发;
  • 测试阶段:推荐使用 source-map或者cheap-module-source-map

    • 测试阶段我们也希望在浏览器下看到正确的错误提示;
  • 发布阶段:false、缺省值(不写)

Babel7

概述

事实上,在开发中我们很少直接去接触 babel,但是 babel 对于前端开发来说,目前是不可缺少的一部分:

  • 开发中,我们想要使用 ES6+ 的语法,想要使用 TypeScript,开发 React 项目,它们都是离不开 Babel 的;

  • 所以,学习 Babel 对于我们理解代码从编写到线上的转变过程至关重要;

Babel: 是一个工具链,主要用于旧浏览器或者环境中将 ECMAScript 2015+代码转换为向后兼容版本的 JavaScript,包括:语法转换、源代码转换、Polyfill 实现目标环境缺少的功能

image-20240229134727911

命令行执行

命令行语法

babel 本身可以作为一个独立的工具(和 postcss 一样),不和 webpack 等构建工具配合来单独使用。

依赖包:

  • @babel/core:babel 的核心代码,必须安装
    • 安装:npm i @babel/core -D
  • @babel/cli:可以让我们在命令行使用 babel
    • 安装:npm i @babel/cli -D

语法:

sh
npx babel <src> --out-dir <dist> --plugins?=<插件> --presets?=@babel/preset-env
# 注意:此处使用 pnpx 不行

命令参数:

  • --out-dir:指定要输出的文件夹 <dist>
  • --plugins?=:添加插件,多个插件用,分隔。和插件之间使用=连接
  • --presets?=:添加预设插件 @babel/preset-env

注意: 默认情况下,babel 不会转化代码,需要配置相关插件

plugins

依赖插件:

  • @babel/plugin-transform-arrow-functions:(已废弃)转化箭头函数

    • 安装:npm install @babel/plugin-transform-arrow-functions -D
  • @babel/plugin-transform-block-scoping:(已废弃)转化变量声明

    • 安装:npm install @babel/plugin-transform-block-scoping -D

1、比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:

sh
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions

2、查看转换后的结果:我们会发现 const 并没有转成 var

  • 这是因为 plugin-transform-arrow-functions,并没有提供这样的功能;

  • 我们需要使用 plugin-transform-block-scoping 来完成这样的功能;

sh
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

image-20240229153928928

presets

但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(presets):后面我们再具体来讲预设代表的含义;

注意: 预设是根据浏览器环境browerlist来设置配置的

依赖包:

  • @babel/preset-env:可以根据浏览器环境或运行时环境来编译 JS 代码,从而更方便地使用最新的 JS 特性。
    • 安装:npm i @babel/preset-env -D
    • 功能:
      • 自动插件选择:根据目标环境自动选择需要的 Babel 插件,无需手动配置每个插件。
      • 目标环境自定义:可以通过 browserslist 配置文件或直接在 Babel 配置中指定目标环境。
      • Polyfill 管理:可以与 @babel/polyfillcore-js 等工具配合使用,根据需要自动引入 polyfill。
      • 模块转换:支持按需转换 ES 模块为 CommonJS、AMD 等模块格式。

执行如下命令:

sh
npx babel src --out-dir dist --presets=@babel/preset-env

底层原理

Babel 的底层原理

babel 是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?

  • 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?

  • 就是编译器,事实上我们可以将 babel 看成就是一个编译器。

  • Babel 编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码

Babel 也拥有编译器的工作流程:

  • 解析阶段Parsing

  • 转换阶段Transformation

  • 生成阶段Code Generation

手写编译器: https://github.com/jamiebuilds/

babel 编译器执行原理

Babel 的执行阶段

image-20240229134902687

当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:

image-20240229134911899

babel-loader

在实际开发中,我们通常会在构建工具中通过配置 babel 来对其进行使用的,比如在 webpack 中。

依赖包:

  • babel-loader
    • 安装:npm i babel-loader -D

配置: 在加载 js 文件时,使用我们的 babel

image-20240229134926987

指定使用的插件:

我们必须指定使用的插件才会生效

image-20240821110241661

babel-preset

如果我们一个个去安装使用插件,那么需要手动来管理大量的 babel 插件,我们可以直接给 webpack 提供一个 preset,webpack 会根据我们的预设来加载对应的插件列表,并且将其传递给 babel。

比如常见的预设有三个:

  • @babel/preset-env

  • @babel/preset-react

  • @babel/preset-TypeScript

安装 preset-env:

sh
npm install @babel/preset-env

image-20240229134947381

注意: presets 还有另外一种写法。二者的不同在于:使用数组时,可以添加其他的配置选项(如 target,...)

image-20240301092524872

浏览器兼容

浏览器兼容性

我们来思考一个问题:开发中,浏览器的兼容性问题,我们应该如何去解决和处理?

  • 当然这个问题很笼统,这里我说的兼容性问题不是指屏幕大小的变化适配

  • 我这里指的兼容性是针对不同的浏览器支持的特性:比如 css 特性js 语法之间的兼容性;

我们知道市面上有大量的浏览器:

  • 有 Chrome、Safari、IE、Edge、Chrome for Android、UC Browser、QQ Browser 等等;

  • 它们的市场占有率是多少?我们要不要兼容它们呢?

其实在很多的脚手架配置中,都能看到类似于这样的配置信息:

  • 这里的百分之一,就是指市场占有率
js
> 1%
last 2 versions
not dead

浏览器市场占有率:

但是在哪里可以查询到浏览器的市场占有率呢?

image-20240229135028785

设置目标浏览器方法:

我们最终打包的 JavaScript 代码,是需要跑在目标浏览器上的,那么如何告知 babel 我们的目标浏览器呢?

  • browserslist 工具

  • targets 属性

  • Stage-X

browserslist

概述

但是有一个问题,我们如何可以在 css 兼容性和 js 兼容性下共享我们配置的兼容性条件呢?

  • 就是当我们设置了一个条件: > 1%;

  • 我们表达的意思是 css 要兼容市场占有率大于 1%的浏览器,js 也要兼容市场占有率大于 1%的浏览器;

  • 如果我们是通过工具来达到这种兼容性的,比如我们讲到的 postcss-preset-env、babel、autoprefixer 等

如何可以让他们共享我们的配置呢?

  • 这个问题的答案就是browserslist

browserslist: 是一个在不同的前端工具之间,共享目标浏览器和 Node.js 版本的配置:

浏览器查询过程

我们可以编写类似于这样的配置:

js
> 1%
last 2 versions
not dead

那么之后,这些工具会根据我们的配置来获取相关的浏览器信息,以方便决定是否需要进行兼容性的支持:

条件查询使用的是 caniuse-lite 的工具,这个工具的数据来自于 caniuse 的网站上;

image-20240229135117302

编写规则

那么在开发中,我们可以编写的条件都有哪些呢?(加红部分是最常用的)

  • 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 versionslast 2 iOS major versions:最近 2 个主要版本的所有次要/补丁版本。
  • node 10node 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.1PhantomJS 1.9:选择类似于 PhantomJS 运行时的 Safari 版本。

  • extends browserslist-config-mycompany:从 browserslist-config-mycompany npm 包中查询 。

  • supports es6-module:支持特定功能的浏览器。

    • es6-module 这是“我可以使用” 页面 feat 的 URL 上的参数。有关所有可用功能的列表,请参见 。caniuse-lite/data/features
  • browserslist config:在 Browserslist 配置中定义的浏览器。在差异服务中很有用,可用于修改用户的配置,例如 browserslist config and supports es6-module。

  • since 2015last 2 years:自 2015 年以来发布的所有版本(since 2015-03 以及 since 2015-03-10)。

  • unreleased versionsunreleased Chrome versions:Alpha 和 Beta 版本。

  • not ie <= 8:排除先前查询选择的浏览器。

命令行使用

我们可以直接通过命令来查询某些条件所匹配到的浏览器:

sh
npx browserslist ">1%, last 2 versions, not dead"

image-20240229135232912

配置文件使用

配置方案:

我们如何可以配置 browserslist 呢?两种方案:

  • 方案一:在 package.json 中配置;

  • 方案二:单独的一个配置文件.browserslistrc 文件;

方案一: package.json 配置:

image-20240229135248290

方案二: .browserslistrc 文件

image-20240229135253294

默认配置:

如果没有配置,那么也会有一个默认配置:

image-20240229135301898

条件关系:

我们编写了多个条件之后,多个条件之间是什么关系呢?

image-20240229135314650

targets(弃)

之前我们已经使用了 browserslist 工具,我们可以对比一下不同的配置,打包的区别:

image-20240229135329365

配置方法:

我们也可以通过 targets 来进行配置:

image-20240229135337938

那么,如果两个同时配置了,哪一个会生效呢?

  • 配置的 targets 属性会覆盖 browserslist

  • 但是在开发中,更推荐通过 browserslist 来配置,因为 browserslist 可以在多个前端工具(postcss、babel...)之间共享浏览器兼容性配置

Stage-X(弃)

要了解 Stage-X,我们需要先了解一下 TC39 的组织:

  • TC39 是指技术委员会(Technical Committee)第 39 号

  • 它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构;

  • ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展;

TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage

  • stage-0: strawman(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人";

  • stage-1: proposal(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响;

  • stage-2: draft(草稿),Stage 2 的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否合理;

  • stage-3: candidate(候补),Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3 的提案不会有太大的改变,在对外发布之前只是修正一些问题;

  • stage-4: finished(完成),进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中;

配置方法:

在 babel7 之前(比如 babel6 中),我们会经常看到这种设置方式:

  • 它表达的含义是使用对应的 babel-preset-stage-x 预设

  • 但是从 babel7 开始,已经不建议使用了,建议使用 preset-env 来设置;

image-20240229135413708

配置文件

像之前一样,我们可以将 babel 的配置信息放到一个独立的文件中,babel 给我们提供了两种配置文件的编写:

  • babel.config.json(或者.js,.cjs,.mjs)文件,(推荐);

  • .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;

它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel 本身、element-plus、umi 等);

  • .babelrc.json:早期使用较多的配置方式,但是对于配置 Monorepo 项目是比较麻烦的;

  • babel.config.json(babel7):(推荐),可以直接作用于 Monorepo 项目的子包;

js 写法:

image-20240229135421500

json 写法:

image-20240301110233351

polyfill

概述

Polyfill 是什么呢?

  • 翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适;

  • 理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用 JavaScript;

什么时候会用到 polyfill 呢?

  • 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol 等以及实例方法例如 Array.prototype.includes 等)
  • 但是某些浏览器压根不认识这些特性,必然会报错;我们可以使用 polyfill 来填充或者说打一个补丁,那么就会包含该特性了;

使用 polyfill

1、依赖包:

  • babel7.4.0 之前,可以使用 @babel/polyfill 的包,但是该包现在已经不推荐使用了:

    sh
    npm i @babel/polyfill

    image-20240229135443621

  • babel7.4.0 之后,可以通过单独引入 core-jsregenerator-runtime 来完成 polyfill 的使用:

    sh
    npm i core-js regenerator-runtime

2、配置 babel.config.js:

我们需要在 babel.config.js文件中进行配置,给 preset-env 配置一些属性:

  • useBuiltIns:设置以什么样的方式来使用 polyfill;

    • false:(默认),不自动导入任何 polyfill,此时无需设置 corejs 属性。
    • usage:(推荐),会根据源代码中出现的语言特性,自动检测所需要的 polyfill。
    • entry:引入所有需要的 Polyfill,但只在代码中指定的位置(入口文件)进行一次性引入。适用于当使用的第三方库依赖于 babel 的情况。
  • corejs:设置 corejs 的版本,目前使用较多的是 3.x 的版本,比如我使用的是 3.26.1 的版本;

  • 2:使用 2.x 版本的 corejs。

  • 3:(推荐),使用 3.x 版本的 corejs。

  • 注意: 另外 corejs 可以设置是否对提议阶段的特性进行支持;设置 proposals 属性为 true 即可;

useBuiltIns 属性有三个常见的值

第一个值:false

  • 打包后的文件不使用 polyfill 来进行适配;

  • 并且这个时候是不需要设置 corejs 属性的;

image-20240301121237499

第二个值:usage

  • 会根据源代码中出现的语言特性,自动检测所需要的 polyfill;

  • 这样可以确保最终包里的 polyfill 数量的最小化,打包的包相对会小一些;

  • 可以设置 corejs 属性来确定使用的 corejs 的版本;

image-20240229135514043

第三个值:entry

  • 如果我们依赖的某一个库本身使用了某些 polyfill 的特性,但是因为我们使用的是 usage,所以之后用户浏览器可能会报错;

  • 所以,如果你担心出现这种情况,可以使用 entry;

  • 并且需要在入口文件中添加 import 'core-js/stable' import 'regenerator-runtime/runtime';

  • 这样做会根据 browserslist 目标导入所有的 polyfill,但是对应的包也会变大;

1、设置useBuiltIns: "entry"

image-20240229135527994

2、在入口文件index.js中引入以下包

image-20240229135534282

注意: 可以通过设置exclude,告诉babel不要处理/node_modules/下的代码

image-20240229135455920

编译 JSX

在我们编写 react 代码时,react 使用的语法是 jsx,jsx 是可以直接使用 babel 来转换的。

对 react jsx 代码进行处理需要如下的插件:

但是开发中,我们并不需要一个个去安装这些插件,我们依然可以使用 presets 来配置 @babel/preset-react

依赖包:

  • babel-loader
    • 安装:npm i babel-loader -D
  • @babel/preset-react
    • 安装:npm install @babel/preset-react -D

配置:

webpack.config.js中如下配置:

image-20240301145105732

打包过程:

1、安装 reactreact-dom依赖包

sh
pnpm i react react-dom

2、编写 jsx 代码

image-20240621213606816

3、在index.js中挂载 App 组件

image-20240621213858753

4、编写模板文件index.html

image-20240821161117726

5、通过插件 html-webpack-plugin 使用模板

image-20240621213734744

6、为了让 babel 认识 jsx 语法,需要配置 babel 的 preset

image-20240301145105732

7、效果

image-20240621214853404

编译 TS

在项目开发中,我们会使用 TypeScript 来开发,那么 TypeScript 代码是需要转换成 JavaScript 代码。

我们可以通过以下三种方式编译 TypeScript:

  • 命令行编译
  • typescript
  • webpack 中编译
  • ts-loader:使用的是tsc作为内核,缺点:不包含 polyfill
  • babel-loader:(推荐),缺点:不检测类型错误
typescript

可以通过 TypeScript 的 compiler typescript 来转换成 JavaScript:

依赖包:

  • typescript
    • 安装:npm i typescript -D

配置:

1、生成 TypeScript 的编译配置信息的配置文件tsconfig.json

sh
npx tsc --init

生成配置文件如下:

image-20240229135617296

2、运行 npx tsc 来编译自己的 ts 代码

sh
npx tsc
ts-loader

如果我们希望在 webpack 中使用 TypeScript,那么我们可以使用 ts-loader 来处理 ts 文件:

依赖包:

  • ts-loader
    • 安装:npm i ts-loader -D

配置:

1、在webpack.config.js中添加ts-loader配置

image-20240229135630144

2、生成 TypeScript 的编译配置信息的配置文件tsconfig.json

sh
tsc --init

生成配置文件如下:

image-20240229135617296

3、我们通过 npm run build 打包即可。

补充: tsconfig.json 配置

babel-loader

除了可以使用 TypeScript Compiler 来编译 TypeScript 之外,我们也可以使用 Babel:

  • Babel 是有对 TypeScript 进行支持;

  • 我们可以使用插件: @babel/tranform-typescript;

  • 但是更推荐直接使用 preset:@babel/preset-typescript

依赖包:

  • babel-loader
    • 安装:npm i babel-loader -D
  • @babel/preset-typescript
    • 安装:npm i @babel/preset-typescript -D

配置:

1、配置babel-loader

image-20240229135656799

2、配置babel-loader的预设@babel/preset-typescript和 polyfill

image-20240301152947713

ts-loader、babel-loader 选择

那么我们在开发中应该选择 ts-loader 还是 babel-loader 呢?

  • 使用 ts-loader(TypeScript Compiler)

    • 来直接编译 TypeScript,那么只能将 ts 转换成 js;

      • 如果我们还希望在这个过程中添加对应的 polyfill,那么 ts-loader 是无能为力的;

      • 我们需要借助于 babel 来完成 polyfill 的填充功能;

  • 使用 babel-loader(Babel)

    • 来直接编译 TypeScript,也可以将 ts 转换成 js,并且可以实现 polyfill 的功能;

    • 但是 babel-loader 在编译的过程中,不会对类型错误进行检测

那么在开发中,我们如何可以同时保证两个情况都没有问题呢?

编译 TS 最佳实践: 使用 Babel 来完成代码的转换,使用 tsc 来进行类型的检查

事实上 TypeScript 官方文档有对其进行说明:

image-20240229135713964

也就是说我们使用 Babel 来完成代码的转换使用 tsc 来进行类型的检查

但是,如何可以使用 tsc 来进行类型的检查呢?

  • 在这里,我在 scripts 中添加了两个脚本,用于类型检查;

  • 我们执行 npm run type-check 可以对 ts 代码的类型进行检测

  • (推荐),我们执行 npm run type-check-watch 可以实时的检测类型错误

image-20240301154028158

webpack 服务器

概述

目前我们开发的代码,为了运行需要有两个操作:

  • 操作一:npm run build,编译相关的代码;

  • 操作二:通过 live server 或者直接通过浏览器,打开 index.html 代码,查看效果;

这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示

为了完成自动编译,webpack 提供了几种可选的方式:

  • webpack watch mode

  • webpack-dev-server(常用)

  • webpack-dev-middleware

webpack-dev-server

上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:

  • 当然,目前我们可以在 VSCode 中使用 live-server 来完成这样的功能;

  • 但是,我们希望在不适用 live-server 的情况下,可以具备 live reloading(实时重新加载)的功能;

依赖包:

  • webpack-dev-server
    • 安装:npm install webpack-dev-server -D

开启本地服务器:

1、设置 scripts 脚本

sh
"serve": "webpack serve --config wk.config.js"

2、运行命令

sh
npm run serve

注意:

  • webpack-dev-server 在编译之后不会写入到任何输出文件,而是将 bundle 文件保留在内存中
  • 事实上 webpack-dev-server 使用了一个库叫 memfs(memory-fs webpack 自己写的)

缺点:

  • 局部修改后,整个页面都会被重新刷新一遍,浪费性能
  • 可以通过 HMR 解决该问题

devServer 配置

修改配置文件,启动时加上 serve 参数:

image-20240229140441933

  • staticboolean | string | array,(默认:'public'),允许配置从目录提供静态文件的选项。将其设置为 false 以禁用

  • proxy{'/api': {target, pathRewrite, changeOrigin}},设置代理来解决跨域访问的问题

    • targetstring,实际储存资源的服务器地址
    • pathRewrite{'^/api': ''},去除原始路径中的/api
    • changeOriginboolean,代理时是否保留主机头的来源
  • historyApiFallbackboolean | object,解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误

    • false:(默认),
    • true:在刷新时如果返回 404 错误,会自动返回 index.html 的内容
    • object{rewrites: [{from, to},...]},可以配置rewrites属性。通过配置 from 来匹配路径,决定要跳转到哪一个页面
  • hottrue | 'only',启用 webpack 的 模块热替换 特性

    • true:(默认),启用 webpack 的模块热替换特性
    • 'only':启用模块热替换功能,在构建失败时不刷新页面作为回退
  • host:``,设置主机地址

    • 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 地址是可以访问的

      • 如果希望其他地方也可以访问,可以设置为 0.0.0.0

  • port:``,(默认:8080),设置监听的端口

  • open:``,(默认:false),是否打开浏览器。也可以设置为类似于 Google Chrome 等值

    • false:(默认),不打开
    • true:打开默认浏览器
    • ['/my-page', '/another-page']:在浏览器中打开多个指定页面,也可以打开一个页面
    • app{name: 'google-chrome'}:打开指定的浏览器,chrome 在不同平台名称也不同,此处为 linux 平台名
  • compress:``,(默认:false),是否为打包文件开启 gzip compression 压缩

image-20240624170215248

image-20240229140538507

static

staticboolean | string | array,(默认:'public'),允许配置从目录提供静态文件的选项。将其设置为 false 以禁用

devServer 中 static 对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:

  • 比如在 index.html 中,我们需要依赖一个 abc.js 文件,这个文件我们存放在 public 文件 中;

  • 在 index.html 中,我们应该如何去引入这个文件呢?

    • 比如代码是这样的:<script src="./public/abc.js"></script>

    • 但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;

    • 所以代码是这样的:<script src="/abc.js"></script>;

    • 但是我们如何让它去查找到这个文件的存在呢? 设置 static 即可;

image-20240229140518135

image-20240301165445563

proxy

proxy{'/api': {target, pathRewrite, changeOrigin}},设置代理来解决跨域访问的问题

  • targetstring,实际储存资源的服务器地址
  • pathRewrite{'^/api': ''},去除原始路径中的/api
  • changeOriginboolean,代理时是否保留主机头的来源

proxy 是我们开发中非常常用的一个配置选项,它的目的是设置代理来解决跨域访问的问题:

  • 比如我们的一个 api 请求是 http://localhost:9000,但是本地启动服务器的域名是 http://localhost:8888,这个时候发送网络请求就会出现跨域的问题;

  • 那么我们可以将请求先发送到一个代理服务器,代理服务器和 API 服务器没有跨域的问题,就可以解决我们的跨域问题了;

示例:

API 资源服务器: http://localhost:9000

本地请求服务器: http://localhost:8888

1、本地 webpack.config.js 文件配置

image-20240301173551717

2、本地请求代码

image-20240301174024071

3、API 服务器代码

image-20240301174207541

4、请求结果

image-20240301173754975

changeOrigin

这个 changeOrigin 官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的 headers 中的 host 属性:

  • 因为我们真实的请求,其实是需要通过 http://localhost:9000 来请求的;

  • 但是因为使用了代理,默认情况下它的值时 http://localhost:8888

  • 如果我们需要修改,那么可以将 changeOrigin 设置为 true 即可;

changeOrigin 设为默认值false时 API 服务器的 ctx.headers:

image-20240624172432893

修改 changeOrigin 为 true 后API 服务器的 ctx.headers:

image-20240301181243783

内部源码:

image-20240229140618630

historyApiFallback

historyApiFallbackboolean | object,解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误

  • false:(默认),进行页面刷新时,会返回 404 的错误
  • true:(推荐),在刷新时如果返回 404 错误,会自动返回 index.html 的内容
  • object{rewrites: [{from, to},...]},可以配置rewrites属性。通过配置 from 来匹配路径,决定要跳转到哪一个页面

historyApiFallback 是开发中一个非常常见的属性,它主要的作用是解决 SPA 页面在路由跳转之后,进行页面刷新时,返回 404 的错误。

boolean 值:默认是 false

  • 如果设置为 true,那么在刷新时,返回 404 错误时,会自动返回 index.html 的内容;

image-20240301164556414

object 类型的值,可以配置 rewrites 属性:

  • 可以配置 from 来匹配路径,决定要跳转到哪一个页面;

image-20240301164534403

事实上 devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库的