2022年11月前端最新流行打包&编译器调研
一、背景&现状
1.当前牛客的业务打包、热更新工具基于webpack5,虽然经过一次统一优化,但是和目前行业最先进打包工具的构建、热更新时间还有差距。
HMR(项目名称就不公开了) | 优化前 | 优化后 | 行业前沿 |
project 1 | 2~3分钟 | ~3秒(+按路由拆分运行) | |
project 2 | 1分钟 | ~5秒 | |
project 3 | 30秒 | ~4秒 |
Build | 优化之前 | 优化之后 | 行业前沿 |
project 1 | 9分钟6秒 | 3分钟23秒 | |
project 2 | 9分钟3秒 | 1分钟30秒 | |
project 3 | 3分钟41秒 | 1分钟34秒 |
2.VUE3已经发布两年+,属于前端前沿的技术,牛客后续也需要考虑升级与行业前沿对接使用VUE3。与此同时,webpack已被vue3抛弃,不再是官方推荐的打包构建工具。
3.webpack与其构建插件(loader、plguin),绝大多数基于NodeJS编写,受限于NodeJS单线程以及其他的执行效率方面天然的不足,其已不是web开发的最优选择。
二、未来畅想
通过引进使用最先进的打包工具,前端热更新可以实现类似静态页面级别(1S内)的等待时间,打包时间极大优化(10S内)基本无感。在整个开发流程中不再是阻塞点,对于一线开发者来说不存在影响开发效率和体验的问题,CI/CD全流程时间缩短到2min内,实现前端代码的快速上线部署、验证。
三、行业实践
打包工具和编译工具的区别
本次调研包含两个部分,不仅局限于打包工具,还调研了一些编译工具。首先我们要知道打包工具和编译工具有什么区别?我在这方面没有查到严格的官方定义,但是他们确实是不同意义的存在,在调研的时候我们不能搞混,下面由我给大家提供一些关键字和官方文档的只言片语来帮助大家理解。
- 打包工具,关键词:bundle、pack
将项目中所需的每一个模块组合成一个或多个 bundles,它们(指bundles)均为静态资源,用于展示你的内容。打包工具中使用编译工具编译各种资源。
- 编译工具,关键词:complier
一般为打包工具的一部分,编译某种浏览器不能直接识别语言为浏览器可识别语言,像babel-loader(ES6+ -> ES5)、sass-loader(sass -> css)、less-loader(less -> css)、vue-loader(js/vue/style -> js/html/css)等,都是编译工具。编译工具是打包工具的一部分,编译一种或多种资源。
接来下也会对两种工具做区分介绍。
编译工具
SWC
全称 Speedy Web Complier,目前是一个替代babel的complier。
npm包的Description
它基于Rust实现,得益于Rust语言的高效,SWC的transform效率最高可以达到Babel的70倍。
SWC官方给出以下几种包:
- @swc/cli:swc 的命令行工具,可以通过命令行直接对文件进行转译。
- @swc/core:swc 的 js 库,可以在 node 环境中执行,其中包含swc的核心API,打包工具的高级使用者需要关注。
- @swc/wasm-web:swc 的 wasm 版,可以在浏览器环境中执行。
- @swc/jest:服务 Jest 框架,提高jest的执行速度。
- swc-loader:用在webpack中替换babel-loader的转译工具。
- 支持转译 JavaScript、TypeScript、J(T)SX、值得注意的是,它还支持转译 React 17 版本的新 JSX,也能支持「转译到 ES5 语法」。
- 支持 ESM 或 CJS 等各种模块标准。
- 支持 Minification。
- 支持 SourceMap。
- 支持插件。
- 目前用户量还不够大,深入开发使用的时候难免踩坑。
- 生态不够完善,短期内想要替代 Babel 还有些困难。
- Rust 学习困难。
- 支持转译的内容类型有:JS、JSX、TS、TSX、JSON、CSS、二进制、Text、Base64,不同类型的内容需要使用不同的 loader (这里指 esbuild 内置的 loader)。
- 支持压缩。
- 支持 SourceMap。
- 支持指定 Target:转译成 js 或 css 时可指定目标语法版本,默认 esnext,即使用最新的特性。
- 支持 Tree shaking:主要针对 declaration-level。
- 支持 Bundle:默认不启用 Bundle。
- 支持 Watch:监听文件变动,重新构建。
- 支持 DevServer。
- 支持 Code Splitting。
- 支持自定义JS plugin:社区已经有不少 plugin 了 https://github.com/esbuild/community-plugins。
- (重要)作为替换babel-loader的complier,其对转译为ES5编码的能力并不友好,目前来看这是阻塞前端业务构建使用的一个重要的问题。
- 没有提供 AST 级别的 API,用户无法干涉 Transform 过程,加上 Transform 不能完全支持转译到 ES5 语法,如果代码需要运行到低版本浏览器或者项目有依赖 Babel Plugin 的话,就不要用 esbuild 了。
- 对 CSS 的支持较为单一,仅支持纯 CSS,CSS Modules 在规划中了,对于 Less,PostCSS 等预处理语言则需要用 Plugin 来处理。
- Code Splitting 的功能尚未完善,目前只有当产物是 ESM 的时候才能使用这个特性,而且还有一些 import 顺序导致的问题。
- 对 TS 的支持也不够完全,且对 React 17 新的 JSX 处理也还不支持。
- 虽然有 Plugin 机制,但是提供的钩子数量不多,功能也不够强大,并且 JS Plugin 会在一定程度上拖慢效率。
- 支持 JS/TS/JSX/TSX,Parcel 2.0 开始使用了 Rust 实现的 JS Transformer,能更高效地进行转译,同时也支持转译到 ES5,对于 React17 新的 JSX 也能支持。另外 Minification,Tree Shaking 等也是支持的。
- 支持 CSS,功能基本上对齐 CSS Loader,还支持各种 CSS 预处理语言,支持 Tree Shaking,Minification 等。另外支持以文本形式引入 CSS 资源,方便用户手动将 css 放入 Style Tag 中,值得一提的是,Parcel2.0 还用 Rust 实现了 CSS 的 Transformer。
- 支持 HTML。
- 支持 Vue,完全支持 Vue3 语法。
- 支持图片,丰富的图片文件处理,支持图片类型的转换以及裁剪。
- 支持 Code Splitting,不过和 esbuild 一样只能支持比较有限的分割逻辑,被多个入口引用的共用模块或者使用 import() 动态引入的模块会被分割成单独的 Chunk。
- 支持 Tree Shaking。
- 支持 Scope Hoist。
- 支持 Minification。
- 支持 Compression,可生成 Gzip 和 Brotli 两种压缩格式的产物。
- 支持内联 Bundle,即可以以文本或者其他格式引入转译后的资源,例如上面提到的以文本格式引入编译后的 CSS 文件,亦可以直接以 dataURL 的格式引入二进制文件等。
- 支持开发阶段的 DevServer,HMR 等。
- 支持浏览器缓存,产物文件名默认带上文件内容 hash。
- 支持差异化构建,默认会同时构建出 ESM 的产物以及非 ESM 的产物。
- 零配置,告别繁琐的配置功能,能满足大多数场景。
- 默认使用了worker进程,提升了构建效率
- 使用了基于Rust的swc作为JS/CSS的complier,进一步提升了代码的转译效率。
- 因为出现较晚,且产物只支持ESM规范(ESM规范默认支持,commonjs规范产物需要单独引入@rollup/plugin-commonjs插件进行支持),Rollup的打包产物非常非常干净,没有webpack产物中的那种看起来非常混乱的各种polyfill代码。
- Rollup 提供了从 读取参数 到 构建 到 输出产物共计 25 种 Hook,足以满足绝大多数场景。
- 目前社区里的插件数量也非常多,几乎该有的都有
- JavaScript (.js, .mjs)
- TypeScript (.ts, .tsx)
- JSON (.json)
- JSX (.jsx, .tsx),默认使用 ESBuild 来转译,虽然 ESBuild 已经有办法处理新的 JSX 语法了,但 snowpack 似乎没有兼容上,需要降级到 babel 来处理。
- CSS (.css):对于预处理语言似乎仅支持 Sass,对于代码里 import 进来的 css 文件,snowpack 会把它处理成 .proxy.js 后缀的 js 文件,且在 js 文件里的逻辑就是创建 style 标签把 css 内容填进去。
- CSS Modules (.module.css)
- Images & Assets (.svg, .jpg, .png, etc.)
- WASM (.wasm)
- load: 这个 hook 会在加载特定后缀文件的时候触发,通常用于将浏览器无法处理的文件类型转化成浏览器能运行的文件,除了可以更改文件内容外,也可以更改最终输出的文件类型。例如插件@snowpack/plugin-vue 对 .vue 文件的处理就是使用这个 hook 来做的。
- transform: 在所有文件都过完 load 之后,会来到 transform hook,这里可以对文件内容进行更改。
- optimize:snowpack 本身是不做打包的,但前面说到它也可以支持生产环境的打包,这里就是依赖插件来做的打包,而插件则是利用的 optimize 这个 hook,在这个 hook 里可以用户指定打包工具例如 webpack, rollup, parcel 等进行代码的 bundle。
- JavaScript
- TypeScript
- Vue (all,需要引入不同的插件)
- JSX、TSX ,默认使用 ESBuild 来转译,开箱即用。
- CSS (@import 内联和变基、PostCSS、CSS Modules、CSS 预处理器):导入 .css 文件将会把内容插入到 <style> 标签中,同时也带有 HMR 支持。也能够以字符串的形式检索处理后的、作为其模块默认导出的 CSS。
- 静态资源处理。
- JSON:可以被直接导入 —— 同样支持具名导入
- Glob 导入:Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块。
- WASM (.wasm):预编译的 .wasm 文件可以通过 ?init 来导入。默认导出一个初始化函数,返回值为所导出 wasm 实例对象的 Promise
- 动态导入:和 glob 导入 类似,Vite 也支持带变量的动态导入。
- Web Workers:支持通过构造器导入和带有查询后缀的导入。
- 在开发过程中,Vite 是一个开发服务器,根据浏览器的要求编译源文件。无需捆绑,编译后真正做到按需使用。未修改的文件会返回304,所以浏览器根本就不会要求。这就是它启动快、保持快的原因。
- Vite 支持热模块替换,这和 "简单的重载页面 "有本质的区别,在DX方面是天壤之别。Vue组件和CSS HMR是开箱即用的支持,第三方框架可以利用HMR API。
- Vite支持一些webpack启发的功能,比如从js中导入'.css'文件(a la css-loader),基于fs的相对路径引用资产(a la file-loader m在构建过程中只需指定'-base'就能自动处理最终的公共部署路径)。
- Vite通过esbuild支持.(t|j)sx?文件,开箱即用,速度快得惊人,所以即使是TS转码,HMR也是字面上的即时性。
- Vite使用Rollup进行生产构建,内部配置与开发服务器功能一致。生产构建的输出是一个传统的静态资产目录,可以部署在任何地方(并且可以被polyfilled以支持旧的borwsers)。
- Vite的核心也是可扩展的(配置/插件格式待定)--你可以通过添加Koa中间件(用于开发)+Rollup aplugin(用于构建)来添加对自定义文件转换的支持。
- 较慢的首屏加载
- 懒加载性能不好
- JavaScript、TypeScript:TurboPack使用SWC对这两种语言进行编译,所以SWC支持什么,TurboPack就支持什么!
- React:JSX&TSX
- Next:TurboPack实际上是为Next而建,所以对于Next支持将是第一目标,所有Next需要的,TurboPack都将会支持
- Vue&Svelte:目前TurboPack对这两个框架的支持还不是开箱即用的,但是因为这两个框架在全世界非常流行,所以在将来的版本中也会对这两个框架进行支持。
- Turbopack 的架构吸取了 Turborepo 和 Google 的 Bazel 等工具的经验教训,两个工具都专注于使用缓存,永远不会重复相同的工作。
- Rust 的速度加持
- 天然支持 React Server Component
- Webpack 用户还可以期望未来可以增量迁移到 Rust-based 的 Turbopack 特性。
- 目前还是alpha版本
- 目前对于Vue的支持还不是他们的第一要务
Transform能力
SWC 也有自己的插件系统,并且同时开放了Rust侧和JS侧的AST级别的API,所以目前来说 Rust 实际上可以做到任何 Babel 能做的事情。但是目前用户量还不够大,可能会存在一些 bug,生态也还不够完善。但是从它开放了 Rust 侧的 API 这点来说还是很诱人的,使用 Rust 开发的插件在运行效率上比 JS 必然会高出不少。
比起 ESBuild, SWC 是更细粒度的一个工具,可定制化程度也更大,因此目前市面上许多工具譬如 Next.js、Parcel、Deno、Vite、Taro等 都选择基于 SWC 来做代码的转译。
Pack能力
也在发展打包能力,只是还在建设中,不够成熟,所以暂时还不建议在生产环境中用它打包。
优点
除了快以外,关键 SWC 还开放了 Rust 侧 AST 级别的 API,在考虑拓展性的同时还把转译效率上限提高了,可谓是杀手锏了。
缺点
NPM仓库统计
总结
作为 Transformer,SWC 的潜力很大,难怪众多工具都押宝 SWC。但是目前来说 SWC 还处于比较早期,会有一些坑要踩,并且如果单纯使用 JS 来开发插件会是转译效率大打折扣,因为涉及到不同语言之间 AST 的转换,具体可以看这里 https://github.com/swc-project/swc/issues/2175,因此要发挥最好的效果势必要学习 Rust,这个学习曲线可能比较陡峭。
ESBuild
另一个抛弃Node,使用性能更好的其他语言Go做编译工具的一个实现,在编译性能有较大提升的同时,编译结果也经过了众多项目的考验,一些问题也经过了充分的暴露。通过结合项目需求和ESBuild能力,他可能是我们替换babel的不二选择。
ESBuild主打的也是快
大家可以看到,ESBuild下面的描述说是现有的速度比他应有的速度慢10-100倍,这是一种很有意思的表达方式,虽然这段描述给人留下了ESBuild比其他插件快100倍的印象,但他却并没有直言说ESBuild比其他工具快100倍。这么说的原因是,真实的速度虽然提升明显但是确实没有这么夸张。
在另一个ESBuild作为complier使用文档的地方,针对这个问题,esbuild-loader也专门做了说明。
另外,ESBuild也提供了打包能力,但是在前端业务打包领域,ESBuild暴露的底层接口不够丰富,插件生态也不够成熟,所以现在可能还不能真正的到生产环境上去使用打包能力,一些library倒是可以用一下。虽然有这些问题,但是ESBuild也在不断发展,他的成长值得我们关注。
Transform能力
Pack能力
优点
确实快,提供编译能力的同时也提供了压缩能力,我们的nc.webpack中已经使用了ESBuild来进行压缩。无论是压缩时间上还是treeshaking,都比terser做的要好。
缺点
NPM仓库统计
总结
目前在业务项目里单独拿 esbuild 做构建或者转译其实都有不少场景是无法支持到的,不过 esbuild 也在不断完善,我们需要扬之长避之短,现阶段在 library 打包场景还是可以用上 ESBuild 的,或者业务项目里如果没有依赖太多的 Babel 插件的情况下倒是可以利用一下 esbuild 的 Transform 能力,比如像 Vite 那样。
目前前端社区也有使用 esbuild 结合 Webpack 的实践,也正是使用 esbuild 的 Transform 能力作为JS/TS/JSX/TSX 的 loader。
打包工具
Parcel
Parcel的主打并不是快,而是开箱即用。
打包工具的开箱即用这个概念曾经在18-19年很火,当时随着MVVM的框架开始完全的替代JQuery这样的前端开发工具库,gulp、grunt、webpack这样的打包工具也就随之出现,随着webpack在打包工具领域的压倒性胜利,webpack的复杂的配置也就成了摆在前端开发者前面的一座山,零配置的打包工具也就被前端圈子殷切期待。
虽然主打的是开箱即用,但是Parcel默认使用 Worker 进程充分发挥多核 cpu 优势来提升构建速度,因此在打包效率上也还是不错的,而且 Parcel 2.0 在 SWC 基础上用 Rust 改写了 JS/CSS Transformer,进一步提升了构建效率。
Parcel 代码实现得非常「模块化」,有非常多内置的插件来完成各种各样的工作,用户可以针对自己的需求来使用不同的内置插件,只要在 .parcelrc 文件里配置即可,parcel 会自动读取这个配置文件,不过要注意 .parcelrc 是 JSON5 格式的文件。
关于源文件
与 Webpack 不同的是,在 Parcel 中,所有文件都是一等公民,一视同仁,因此不需要用户去针对不同类型的文件配置各种 Loader,Parcel 会帮你做好不同类型文件的处理。
构建特性
优点
缺点
成也零配置,败也零配置,Parcel的扩展性不强,几乎没有类似 Webpack 的那种开放性插件特性,因此如果遇到 Parcel 现阶段无法实现或有 Bug 的东西,用户无能为力,只能等 Parcel 去补齐。
NPM仓库统计
总结
目前 Parcel 最大的卖点就在于无需配置,使用体验也确实不错,性能方面在使用 Rust 改造后相信未来也能得到更大的提升,开箱即用可以满足许多场景,但是封装性好带来的副作用就是扩展性差,因此对于有大量定制化构建需求的大型项目来说 Parcel 现阶段或许不算是一个很好的选择。
Rollup
先说个有意思的事,我通过Google找到rollup官网的时候,发现是英文界面。我以为rollup不提供中文网站,这也没什么,很正常。但是我突然发现网站的路径里包含语言选项的缩写。
尝试输入了https://rollupjs.org/guide/zh/,真的打开了rollup中文网。
但是重要的是在英文主页里面竟然没有找到语言切换的功能!难道Rollup使用过程中也有很多这种没有直接告诉你,需要你自己去探索发现的功能么?!
言归正传。
Rollup 是当前流行的库打包器,它比 Webpack 晚2年出现,也是在 ESM 之后出现的,主打的特点是能够支持并且提倡开发者使用 ESM 模块语法进行开发。
关于源文件
几乎只支持 JS,其他类型的文件均需要使用插件来处理。
优点
缺点
作为业务代码打包工具的话,需要用户使用的浏览器支持ESM规范(Chrome 63+)。
NPM仓库统计
总结
Rollup 总体而言是非常优秀的打包工具,产物精简,符合 ESM 标准,丰富的插件系统,社区生态也很不错,是个很现代化 Web Bundler。不过相应的,他需要支持 ESM 标准的浏览器,因此对于低版本浏览器也实在没办法。
因此对于打包 Web App,使用 Webpack 还是主流,干啥都行,哪儿都能跑。
打包库,推荐使用 Rollup,反正产物最终也是当成依赖引入,浏览器兼容性的事情交给引入方去解决了。
Snowpack(原pika/web)
Snowpack 主打的是 Unbundle,极速的开发体验,在生产环境也同样能依赖 Rollup 打包出产物。
他主要的做法就是利用了浏览器对 ESModule 的支持,而对于项目用到的依赖,为了防止依赖没采用 ESM 模块规范,Snowpack 会把从依赖入口开始把依赖打包成一个文件,并确保产物是符合 ESM 标准且可以运行在浏览器中的,而这里主要是依赖了 esinstall 库,esinstall 又是通过 rollup 来做这个事情的。
关于源文件
插件系统
Snowpack 的插件系统也是利用 snowpack 运行的生命周期中提供的 hooks。且这套是沿袭了 Rollup 的那套插件系统。
优点
Unbundle 可以提供很快速的开发体验,另外插件接口设计不错,开发者可以借此扩展许多应用场景。
缺点
官方文档不是特别的完善,对于一些配置项没有很清楚的解释,而且项目维护者没什么精力去维护这个项目,导致 Snowpack 发展比较缓慢。
另外插件部分也有一些不足,主要表现为社区活跃度不够,生态不是很完善,可能缺少处理某些场景的插件,甚至一些现存的插件在实现上也不是很完善。使用体验不够好。
NPM仓库统计
总结
由于是采用 Unbundle 的,Snowpack 本身做的东西就不如 Bundle 方案的那些工具多,实际上它主要要做的事情就是帮我们处理好项目依赖,让那些项目依赖能跑在浏览器上就行了。
该项目已经不维护了。团队目前正在开发 Astro,一个由 Vite 驱动的静态站点构建工具。
Vite
Vite 是新一代前端开发构建工具。Vue 官方 Vue 3.0 工具链全面默认推荐 Vite。作为一名VUEer,这个必须重点讲讲。
Sean Thomas Larkin是webpack的核心开发者之一。他会一些汉语,以上是他和尤雨溪在twitter上的互动。
与 snowpack 类似,他开发阶段采用 unbundle 模式,并且使用 esbuild 做依赖预构建(snowpack 是用的 rollup),生产阶段利用 rollup 做构建。可谓集众家之长了。另外,作为与Vue3绑定的打包工具,其核心开发团队和社区投入的精力、发现问题的能力,也是可以与当前最流行的webpack相媲美。
关于源文件
充分的API暴露
Vite 插件扩展了设计出色的 Rollup 接口,带有一些 Vite 独有的配置项。因此,你只需要编写一个 Vite 插件,就可以同时为开发环境和生产环境工作。
Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API。
Vite 的 JavaScript API 是完全类型化的,我们推荐使用 TypeScript 或者在 VS Code 中启用 JS 类型检查来利用智能提示和类型校验。
优点
缺点
Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
因为Vite依赖于浏览器对于ESM的支持,部分代码是Unbundle直接给浏览器运行的,且Vite提供给浏览器运行的代码是一个Lazy的过程,只有在浏览器需要某一部分的时候,那部分代码才会被Vite处理。所以Vite在开发模式下的首屏性能要比Webpack差。
和首屏一样,由于 unbundle 机制,动态加载的文件,需要做 resolve、load、transform、parse 操作,并且还有大量的 http 请求,导致懒加载性能也受到影响。
此外,如果懒加载过程中,发生了二次预构建,页面会 reload,对开发体验也有一定程度的影响。
NPM仓库统计
总结
尽管在首屏、懒加载性能方面存在一些不足,但瑕不掩瑜,作为目前最 🔥 的构建工具,Vite 可以说是实至名归。而且这些问题并非不可解决,比如我们可以通过 prefetch、持久化缓存等手段做优化,相信 Vite 未来也会做出对应的改进。
TurboPack
没有最快,只有更快。作为最新开放的一款基于Rust语言开发的打包工具,TurboPack又以【快】这个所有前端er都喜闻乐见的打包特点进入了人们的视野。也因为尤大的质疑而赚足了眼球,成了人们茶余饭后的谈资。
另外,TurboPack由 Webpack 的创始人 Tobias Koppers 牵头创立,有这么一位重量级大佬的加持,TurboPack的未来发展我们必须要关注。
关于源文件
优点
缺点
NPM仓库统计
总结
Turpack目前还处于alpha阶段,插件还未支持,能够学习或者了解的东西还很少。但能够看出来,野心很大,言语之间都是想将vite给比下去。从而成为真正的W下一代Web打包工具。
四、结论
很高兴,有这么多替代Webpack的打包工具供我们选择,充分说明了目前的前端开发社区是非常有活力的!
但是一款好的打包工具,对业务系统来讲,在某个时间范围内,快不是第一要务,构建产物的质量可靠,运行稳定,插件生态丰富才是。如果说构建产物同样稳定,运行质量同样可靠,插件生态丰富同样,那么快,一定是前端开发中优先考虑的!天下苦Webpack久矣!
基于以上,同时结合我们目前业务系统中使用的框架Vue来看,Vite作为新一代的打包工具,由Vue的作者尤雨溪开发,其对于Vue的支持无疑是最好的,同时它的速度方面通过结合SWC、ESBuild、Unbundle模式等,也是在所有的打包工具数一数二的,另外强大的vue社区生态无疑也是对Vite的一个强大支持。如果在一年内(调研有效时间)我们要对我们的打包工具进行升级的话,Vite将会是我们的第一选择。
同时我们也必须意识到,类似于TurboPack这样的后起之秀的崛起,其不断为打包模式带来新理念,Vite也远远不是打包工具的终点。如果我们要与互联网最先进前端开发模式接轨,我们必须对打包工具社区保持实时的关注,为我们的前端研发带来更优的开发体验!
参考资料
https://mp.weixin.qq.com/s/xszC8_k7OMih7x7T4rVu8g
https://juejin.cn/post/7034316603890237477
https://github.com/privatenumber/esbuild-loader
https://juejin.cn/post/7054752322269741064
https://zhuanlan.zhihu.com/p/149351900
https://kalacloud.com/blog/vite-vs-vue-cli/?spm=a2c6h.12873639.article-detail.6.5b6e7d0eHEptyX
https://mp.weixin.qq.com/s/bQvjoTxoXshqM9dBwVQx9A