不会化妆的写手
不是好程序员


  • Home

  • Categories

  • Archives

  • Tags

  • About

2022 年,如何在 CRA 的项目中做浏览器兼容

Posted on 2022-07-12 | In 框架/库/工具 , Babel

CRA 项目如何兼容低版本浏览器

对于兼容低版本浏览器的需求,CRA 官方文档写得很清楚。只要引入 react-app-polyfill 就可以解决绝大部分情况的问题。
但实际使用时,我们发现,至少在今天(2022.7.12),对于超低版本浏览器(安卓 5.1.1,Chrome 42),react-app-polyfill 并没有提供对实例方法 arr.includes,以及浏览器 API URLSearchParams 的支持。因此,不得已自己使用 babel 作出支持。

CRA 项目引入 babel

原本的 @babel/polyfill 已经在 babel 7.4.0 版本中被弃用,如今完整的 polyfill 已经转移到了 core-js 中。实际上,直接 import core-js 就可以解决所有问题了。但意味着引入了所有的 polyfill,不管你想支持的浏览器版本,不管你实际使用了哪些方法。这势必会造成包的臃肿,因而需要对 babel 进行配置。
贴心的 babel 提供了两种按需加载方式,@babel/preset-env 的 useBuiltIns 可配置为 entry 或 usage。
如果配置为 entry,babel 则会通过对 browserlists 的配置,按需引入 core-js 的代码,如官方示例:

你的代码:

1
import "core-js";

转换后:

1
2
import "core-js/modules/es.string.pad-start";
import "core-js/modules/es.string.pad-end";

而 usage 参数,则代表着更精确的按需加载。无需在文件开头 import 'core-js',babel 在处理你的代码的时候,会直接解析你的代码实际使用了哪些语法,然后有选择性地引入。

该参数是在 babel 配置文件中配置的:

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.23" // usage 才需要该配置
}
]
]
}

然而,现实是,我花了不少时间尝试,分别在 package.json 和 babel.config.json 中写了配置,似乎都并不生效。无论怎么改配置,打出来的 js 文件夹始终是 1.2M,并没有按需加载的效果。这让我感到很迷茫。

于是查了下CRA 源码,发现不仅是 webpack 配置,CRA 创建的项目默认连 babel 配置都不支持。那就只能用 react-app-rewired + customize-cra 来改了~看文档就会用,很方便很直观。

值得一提的是,如果 customize-cra 没有提供对应的方法,可以用以下方法去补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const {
override,
useBabelRc,
addWebpackPlugin,
} = require("customize-cra")
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

// 自定义方法
const setConcatenateModules = cm => (
config => {
config.optimization.concatenateModules = cm
return config
}
)

module.exports = override(
useBabelRc(),
setConcatenateModules(false),
addWebpackPlugin(new BundleAnalyzerPlugin()),
)

即自己写一个方法来变更配置。

至此,我终于成功配置了 babel。

成功配置 babel 后

在配置完 babel 后,打包推到测试服,5.1.1 的手机仍旧白屏。debug 发现,是 URLSearchParams 仍旧不支持。这个很好理解,useBuildIns: usage 是在我们使用某方法后才会将对应 polyfill 引入的,而 URLSearchParams 实际上是 react-router-dom@6 自行使用的。而在 CRA 项目的 webpack 配置中,babel-loader 只会处理 src 路径下的文件,当然不包括 node_modules。

因此,在文件开头手动引入对应 polyfill import 'core-js/web/url-search-params'。

至此,项目的兼容性问题就完全解决了。

一个未解之谜

我的项目中有多个打包命令,用于区分是否生成 sourcemap,请求的接口是测试环境还是正式环境等。
yarn build:prod 打包,请求的接口是正式环境,而 yarn build 请求的是测试环境。
此前,在 yarn build:prod 的情况下,配置已经没有什么问题了。但是改成 yarn build 打算提测后,控制台忽然就出现了 Object.assign is not a function. 的报错,且页面白屏。
考虑到两个环境的差异就只有请求接口的不同,我第一反应就是接口环境不同导致重定向的页面不同,造成访问的页面不同。所以正式环境没有触发 bug,测试环境触发了。
以这个思路查了半天,发现并不是这个问题。甚至发现客户端其实会拦截所有前端请求更改为正式环境(历史 Charles 抓包也显示确实从未请求过测试环境),所以两个包的运行情况应该是一模一样的才对。
虽然不知道两个运行情况相同的包为什么会有不同的表现,但我还是打算先猜测解决方案。首先从 browserlists 入手,利用这个地址,我查到,我手头这个 5.1.1 的机器,webview 内核版本为 Chrome 42,而我的配置的浏览器支持似乎高于这个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
# @babel/preset-env debug 模式下的控制台输出
Using targets:
{
"android": "4.4.3",
"chrome": "79",
"edge": "97",
"firefox": "96",
"ie": "11",
"ios": "12.2",
"opera": "82",
"safari": "13.1",
"samsung": "16"
}

虽然支持的安卓版本低于 5.1.1,但是 chrome 版本是高于 42 的。我怀疑是这个原因导致的,就在 browserlists 中加入了 chrome 30。

然后问题就解决了。

但这还是不能解释一个问题,就是为什么正式环境的包就没有同样的问题。因此,我将 chrome 30 的配置移除了,想再次观察情况。然后自此开始,Object.assign 这个 bug 就再也没有出现了。

我怀疑是缓存的问题,删除了 node_modules/.cache,不复现。我干脆直接让其他同事拉代码部署了一下,同样不再复现。

而因为发现 debug 的用处太晚,我甚至没有及时地监控到之前问题复现的时候,object.assign 的 polyfill 是否被正确引入了。这件事就暂时成为了未解之谜…………

总之还是记录一下,debug 真的非常好用,不仅可以告诉你 target 的系统/浏览器内核版本,还会在 usage 的时候告诉你因什么文件而引入了什么 polyfill,非常非常好用了。

1
2
3
4
5
6
# @babel/preset-env debug 模式下的控制台输出
[/Users/wangyi/Documents/edg/zhixue/src/bridge/index.ts]
The corejs3 polyfill added the following polyfills:
es.regexp.exec { "android":"4.4.3", "chrome":"79", "edge":"97", "firefox":"96", "ie":"11", "ios":"12.2", "opera":"82", "safari":"13.1", "samsung":"16" }
es.regexp.test { "android":"4.4.3", "chrome":"79", "edge":"97", "firefox":"96", "ie":"11", "ios":"12.2", "opera":"82", "safari":"13.1", "samsung":"16" }
es.object.assign { "android":"4.4.3", "chrome":"79", "edge":"97", "firefox":"96", "ie":"11", "ios":"12.2", "opera":"82", "safari":"13.1", "samsung":"16" }

Error: 你居然咸到文档都不看了!!!

Posted on 2022-03-08 | In 框架/库/工具 , React.js

必须承认,当我决定写 Next 项目时,我并没有先认真学习原理,而是简单看了遍大略文档就直接上了。
但遇到问题发现文档里就有,还是很尴尬的……

遇到的问题

在 Next 项目中,通过屏幕宽度确定是否是移动端布局。而在 Server 端生成 HTML 的时候,是无法取到屏幕宽度的。于是,我写了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// utils.ts
// 是否是移动端布局
const ua = process.browser ? window.navigator.userAgent : ''
export const isMLayout = process.browser && document.documentElement.clientWidth <= 1199

// 页面组件
const Success = () => {
// Web 端下载部分
const renderWebDownload = () => (
<section className={s.downloadWebContainer}>
// ...
</section>
)

// Wap 端下载部分
const renderWapDownload = () => {
return (
<>
<section className={s.downloadAppContainer}>
// ...
</section>

<section className={s.downloadPcContainer}>Please go to the Web terminal to view and download.</section>
</>
)
}

return (
<article>
// ...
{isMLayout ? renderWapDownload() : renderWebDownload()}
// ...
</article>
)
}

export default Success

这段代码产生了问题。在 Client 端渲染后,renderWapDownload 生成的代码块,最外层的包裹却是 <section className={s.downloadWebContainer}></section>,也就是在渲染时复用了一部分 web 端的 DOM。

仔细想想,这也是有迹可循的。用移动端访问该页面时,Server 端吐出的 html 会是 web 端的情况,而在 Client 端收到之后,吐出的 html 会是 wap 端的情况。Server 端生成的 html 和 Client 端生成的 html 会是不同的。这应该是导致 Client 端渲染 wap 样式时,复用了一部分 web DOM 的原因。

有趣的是,我专门写了一个 state,用来促使页面重新渲染,竟然也不会把这个渲染错误给修正。除非我直接把这个 state 作为错误 DOM 的 key,强制要求其重新渲染。

我下意识认为是 DOM diff 的问题。

直到我看了 React 的文档。

解决方案

用 isMLayout 初始化一个 state,然后再进行分析。

TypeScript 想看的问题

Posted on 2021-12-24

interface 和 type 的区别是什么?
我记得之前看过一个 typescript 的新语法,好像也是类似 type 的。说是区分一些混淆,这个语法是什么来着?把那篇掘金文章找出来。

Next.js 本地开发配置 https

Posted on 2021-12-20 | In 框架/库/工具 , Next.js

一搜其实不少博客在说,比如 Using HTTPS on Next.js local development server 这篇文章写的就是一个很常用的解法。
为了防止链接失效,见到说下这篇文章的做法。先通过 mkcert 生成 https 证书,将生成的证书拷贝到项目中,然后自己写一个 server.js,引用该证书,且仅在本地开发时启动。

mkcert 使用方法见:mkcert 文档

重写 server.js 见:Next.js 自定义 Server

Read more »

Next 学习过程中想知道的问题

Posted on 2021-12-07 | In 框架/库/工具 , Next.js

先做个笔记。等需求完成之后再细看。

如何做到从预渲染页面到可交互页面的?

每个生成 HTML 与该页所需的最小 JavaScript 代码相关联。当一个页面被浏览器加载时,它的 JavaScript 代码就会运行并使页面完全交互。(此过程称为hydration.)

这个是如何做到的呢?在编译过程中就把所有可交互内容(如绑定事件 onClick 等)收集起来,JS 执行之后组装到预渲染的 html 内容上?

需要看看原理。

getStaticProps

在预渲染阶段是请求到数据了。在实际客户端运行的过程中呢?就不重复取这个数据了吗?

面试以来遇到的广度问题

Posted on 2021-11-22

工程化

webpack

有没有写过 webpack loader 或者插件?

其他

除了 webpack,还用过什么别的?比如 vite 等。

性能优化

CRA 重写 webpack 配置:react-app-rewired
重写原因:https://medium.com/@timarney/but-i-dont-wanna-eject-3e3da5826e39#.x81bb4kji

code splitting 具体文档还可以再看一下。值得注意的是,import 返回 Promise,打包后的实现原理是动态引入 script 标签。

webpack-bundle-analyzer 分析打包体积

配置 babel

import corejs, package.json babel config usage, 1.2M
无 corejs 1M
delete .cache, delete babel config, import corejs, 1.2M

babel config is not working.

try babel config useBuildIns entry, not working.
try babel.config.json (not in package.json), not working.
cra forbid babel config

新技术新方向

性能优化实验效果

Posted on 2021-11-19

构建

loader cache

dll

hard-source-webpack-plugin

文件大小

Gzip

以 mocktest 为例。

vendor.bundle.js 357kb
main.js 603kb

初步可尝试:gzip、按页面动态懒加载。

动态懒加载的作用可能有限,因为该项目大多是同一页面读大量题目。

先看 gzip。

不需要前端打包 .gz 文件,线上已经是 gz 文件的大小。(以 vendor.bundle.js 为例,原大小 1.6M,通过 compression-webpack-plugin 打包后大小 356k,刚好是线上访问的大小。

  1. 看 referral 的打包大小、f2e 测试服大小(响应头没有显示接受 gzip)和线上大小。
    f2e js 大小 1.1M,本地同样大小。线上 cdn 同一个文件大小 364kb,果然是自动做了压缩的。唯一值得一提的是,本地用 compression-webpack-plugin 压缩的大小为 308kb,压缩效果好像更好一些。
  2. 问运维他们如何自动做了 gzip。
  3. 问运维项目中配置的 dockerfile 和 nginx config 是如何生效的。

代码压缩

js 压缩

css 压缩

图片压缩

代码性能

CSS 动画代替 js 动画

避免重排重绘

懒加载

SPA 项目懒加载

import(),webpack 实现方式是在执行时临时创建 script 标签引入文件。

备忘

到第一个字节的时间(TTFB) 性能优化指标?

虚拟 DOM 与 DOM Diff

Posted on 2021-10-19

本文为一个简单的笔记。详情可参见参考资料里的两篇文章,互相填补,结合起来看很不错。

为什么需要虚拟 DOM

  • 前端性能优化的一个秘诀就是尽可能少地操作 DOM,因为操作 DOM 相对较慢,还会触发浏览器的重排和重绘。因此需要一层抽象,尽可能将所有更改一次性更新到 DOM 上,保证 DOM 不会出现性能很差的情况。
  • 方便通过数据驱动 DOM,不需要手动做 DOM 操作。
  • 可以跨平台。比如 SSR,服务端不存在真实 DOM,在服务端执行过程中都是操作虚拟 DOM,最后一步不是 commit 到页面上,而是变成字符串输出。SSR 脉络梳理

from 你对虚拟DOM原理的理解?

对比时,Vue 从左右两端开始,一直到中间有剩余。Old 无剩余 New 有剩余,则新增。Old 有剩余 New 无剩余,则删除。
Old New 都有剩余,则对比。Old 建立 map,key 为节点,value 为 Old 节点下标。遍历 New,如果 New 在 Old 中无,则标记新增。如果 Old 在 new 中无,则标记删除。
(React 因为 fiber tree 是单向链表,只能从左向右原地复用。)
剩下需要移动的,Vue 和 React 不同。
Vue 会做一个最长子序列的计算,然后保持最长子序列不动,移动其他节点。
React 则是仅右移,比较新旧位置,无视新节点需要左移的情况,仅处理新节点需要右移。

参考资料

  • 精读《DOM diff 原理详解》
  • 图文结合讲解React17、Vue2.0、Vue next Diff算法原理及实现

零碎笔记

Posted on 2021-09-09
  • useEffect,异步执行。也就是在浏览器渲染全部完成之后,浏览器通知 React 自己处于空闲阶段。此时 React 开始执行 useEffect 产生的函数。
  • useLayoutEffect,同步执行。执行时机和 componentDidMount componentDidUpdate 一样。此时,内存中的真实 DOM 已经变化,但还没有渲染在屏幕上。会同步执行,阻塞渲染。但是,如果要对 DOM 进行操作,在这个阶段执行,然后把更改一起渲染在屏幕,可以减少一次重绘和回流。

关于我写了这么多年 React,居然还能忽然懵掉这件事

Posted on 2021-09-07 | In 框架/库/工具 , React.js

起因

需求是实现一个伪对话框,也就是实现一个“两人互发消息”的效果。
第一反应,当然是把需要展示出来的消息存入数组,作为 React 组件的 state。这样,在需要发出消息的时候,就把消息推入这个数组,造成重新渲染即可。

Read more »

12…6
泉先

泉先

今天我有变得更厉害一点吗!>v<

51 posts
14 categories
23 tags
© 2017 - 2022 泉先
Powered by Hexo
Theme - NexT.Gemini