Skip to main content

5 posts tagged with "前端工程化"

View All Tags

· 24 min read

最近在深入探究 GLSL,每次想写一段 shader 看下效果,都要新开一个 HTML、写 <script> 标签、配 <canvas>、引入threejs包、起本地服务、改完刷新看效果 —— 即便有 AI 加持,这一套流程的心智负担还是太重了。我其实只是想验证一些变换公式,查看顶点变换或颜色变化等效果,而不是搭一个项目

试了一圈现成的在线 GLSL playground,要么功能停在能跑就行,要么稍微改点代码就出 bug,再要么就是必须登录或者收费的复杂产品。于是决定自己撸一个 playground —— ShaderPad

目标

  • 「打开浏览器 30 秒内跑起一个 shader」:零登录、零配置、零下载,面向 Web 着色器学习和调试的极简 playground
  • 支持分享和快速复现3d场景
  • 提供可快速插入 mdx 文本里的 npm 包

在线体验 ,对应的 github 仓库

整体效果

shaderpad首页

整体页面结构跟大部分在线编辑器类似,左边是代码编辑区,提供了顶点着色器和片元着色器的编辑功能,会高亮 glsl 语法。右边是基于 threejs 的3d场景,实时预览当前编辑的代码的效果,并且提供了悬浮的控制台面板,方便查看一些报错或log

为了方便观察,3d场景内置了辅助网格和辅助坐标轴,并且引入了 OrbitControls,用户可以通过鼠标旋转场景,查看不同的角度

技术栈

维度选型理由
前端框架Astro 4.15 + React Island群岛架构 + 首屏友好
编辑器Monaco Editor 0.50VSCode 同款,TS 智能提示、GLSL 语法高亮
3d渲染Three.js 0.170(WebGLRenderer + RawShaderMaterial)
状态管理nanostores 0.11极小(<1KB),React 集成通过 @nanostores/react
包管理pnpm 8.11 workspace硬链接节省空间,monorepo 友好
部署轻量服务器 + Nginx Proxy Manager + GitHub Actions

为什么选 Astro?

做 ShaderPad 之前我其实没怎么用过 Astro。这次之所以选它,是被它的「默认零 JS」设计打动了。它并非基于 React 构建,而是允许你通过 integrations 把 React / Vue / Svelte 等当作「岛屿」嵌入到静态 HTML 中。

对比 Gatsby、Docusaurus 或 Next.js 这些默认把整个 React 运行时推到浏览器的重型方案,Astro 显得非常克制。在「孤岛架构」下,它把整个页面当成一片静态海洋,里面散落着几个交互小岛。对 ShaderPad 来说,文档内容就是静态海面,只有 Playground 这个组件才是真正的交互孤岛。

底层原理上的优势:

  1. 极致的编译时剥离:Astro 的渲染模式是 SSG。在 build 时它会把 .astro 组件全跑一遍,暴力剥离掉所有不需要在客户端执行的 JS 逻辑,只输出纯 HTML。
  2. 精准的按需水合:因为 Playground 强依赖 WebGL(完全没法在 Node 端执行),我给它加了 client:only="react" 指令。Astro 遇到它时,只会在 HTML 里留个带有 astro-island 标签的占位 DOM,并注入极轻量的调度器(几 KB)。等页面加载完,调度器才会去拉取 React 运行时和组件代码,在客户端完成「水合」。

落到实际产物上:构建出来的文档站 index 页面只有 6.84 kB(Gzip 后 2.73 kB),大头全在按需加载的 Playground 孤岛里(约 500+ kB)。这种「主静态、副交互」的颗粒度控制,完美契合了重客户端工具的需求,彻底甩掉了全站 React 渲染的性能包袱。这也是为什么 Astro 在「文档站 + 工具型网站」场景下越来越受欢迎的原因——它把"该省的省到极致"这件事做得很彻底。

架构设计

Monorepo 结构

monorepo 算是现在多仓管理的标配。它不光能在一个仓库里管多个 package,更像是在逼我面对一个问题:"如果这个项目要嵌进别人的网页里,边界该画在哪?"。所以从一开始我就带着 SDK 视角在搭:核心引擎、UI 组件、样式层各管一摊,公共部分一律上提到 packages/,主站只负责壳子和体验。

  • @shaderpad/runtime 抽离出 LanguageAdapter 接口(GLSL 轻量语法预检),未来扩展到 Node / Tauri 桌面端可直接复用。
  • @lucascv/shaderpad-playground 把 Playground 抽成独立 npm 包,独立发版、可嵌入到任何 React 文档站。
  • apps/web 主站保持轻量,作为包的消费者,部署产物也更加干净。
shaderPad/
├── apps/
│ └── web/ # 主站(Astro)
│ ├── src/
│ │ ├── pages/ # 路由(index / play / learn/*)
│ │ ├── components/ # React 组件
│ │ ├── lib/
│ │ │ ├── runtime/ # 浏览器侧渲染引擎
│ │ │ └── share/ # URL/localStorage 持久化
│ │ └── shaders/examples.ts # 内置示例库
│ └── astro.config.mjs
├── packages/
│ ├── shader-runtime/ # 跨端共享核心(未来扩展桌面端)
│ │ └── src/languages/ # GLSL / TSL / WGSL adapter
│ └── shader-playground/ # 可独立发版的 npm 包
│ ├── src/
│ │ ├── runtime/three-engine.ts
│ │ ├── ui/ # ShaderPlayground / CodeEditor / PreviewCanvas
│ │ └── styles/playground.css
│ └── tsup.config.ts
└── .github/workflows/
├── deploy-web.yml # 主站部署
└── release.yml # npm 自动发版(OIDC)

数据流

[Monaco Editor]  --change-->  Playground state (codeRef)
|
|--auto save (1s debounce)--> localStorage
|
'--compileAndRun()--> [ShaderEngine]
|
+---------+---------+
| |
(vertex/fragment) (uniforms)
| |
v v
Three.js RawShaderMaterial <-- OrbitControls / Grid / Axes

核心渲染模块

ShaderEngine(运行时核心)

要让代码在网页上跑起来,必须有一套稳定、高效的渲染器。我封装了 ShaderEngine 这个核心类来处理 Three.js 的脏活累活。

它不仅是对 WebGLRenderer 的简单包装,更重要的是它接管了渲染的生命周期与错误捕获,对外只暴露最极简的 API:

class ShaderEngine {
init() // 创建 WebGLRenderer + 透视相机 + 辅助坐标系
applyShader(source, mode) // 将用户的源码注入 RawShaderMaterial
forceCompile() // 绕过 Three.js 顶层,直接调用 WebGL API 预编译并捕获行号
setGeometry(type) // 无缝切换几何体(复用 Material,不闪烁)
start() / stop() / dispose() // 挂载 RAF 动画循环,并确保销毁时不漏内存
}

内置模块

提供了以下几种 threejs 常见的几何体:

  • PlaneGeometry 平面
  • BoxGeometry 立方体
  • SphereGeometry 球体

默认是 PlaneGeometry,用户可以自行切换。针对不同几何体提供了几种不同的常见 shader 示例,比如时间渐变、鼠标跟随、噪声效果等,选中后就可以查看效果,用户可以根据需要选择。

另外比较关键的是,我还内置了一些开发中常见的 uniform 变量,如下:

uniform float u_time;       // 自启动以来的秒数,每帧递增
uniform vec2 u_resolution; // 画布宽高(像素)
uniform vec2 u_mouse; // 鼠标位置,归一化到 [0,1](Y 已翻转)
uniform float u_random; // applyShader 时的随机数 [0,1)

这样就可以在 shader 中直接使用这些变量来做一些动态效果。

当然目前没法完全自定义 uniform,暂时是逐步加入一些常见变量,有需求的可以评论或者追加 github issue

为什么用 RawShaderMaterial?

在实现 ShaderEngine 时,我面临一个取舍:用 ShaderMaterial 还是 RawShaderMaterial

ShaderMaterial 很方便,它会自动帮你注入一堆 Three.js 内置的 uniforms 和 attributes(比如 cameraPositionmodelViewMatrix 等)。但在「教学和调试」场景下,这反而成了致命缺点——用户会很困惑:「我明明没声明这个变量,为什么它能跑?」

为了做到「所见即所得」,我最终选择了 RawShaderMaterial。它是一张白纸,不注入任何隐藏代码,用户写的 source 就是最终跑在 GPU 里的 GLSL。 这也意味着报错行号能做到 1:1 绝对对应,不会出现「明明只有 10 行代码,控制台却报第 150 行错误」的灵异事件。代价是用户必须在代码开头显式声明所需的内置矩阵:

attribute vec3 position;
attribute vec2 uv;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

但这换来的是运行机制的完全透明,对于一个学习工具来说,这个权衡是非常值得的。

编译错误的精确定位

Three.js 编译失败的报错信息默认是「WebGL: ERROR: 0:5: 'foo' : undeclared identifier」这种字符串,没法结构化处理。Playground 在 ShaderEngine 里直接绕过 Three.js 的封装,调底层 gl.getShaderInfoLog + gl.getShaderSource 自己解析,把行号 / 列号 / 错误消息拆成结构体再浮条展示:

{ line: 5, column: 12, message: "'foo' : undeclared identifier" }

这样写 GLSL 时,看到的都是真实可定位的错误,而不是"WebGL: ERROR: 0:5"这种天书。

模块沉淀:从单一工具到通用的 npm 包

做完主站后我意识到——「实时编辑 + 实时预览」这套交互本身非常有价值,它不应该只局限在 ShaderPad 自己的网站里。如果能在任何 MDX 文档或技术博客里直接嵌入一个能跑的 Shader,阅读体验会呈指数级上升。

比如现在你可以直接修改下面的代码,实时查看效果(试试把 cos 改成 sin,或者调一下 vec3(0, 2, 4) 的颜色偏移):

Neon Plasma (可编辑)
Loading...

于是我把 Playground 抽成了一个独立的 npm 包:@lucascv/shaderpad-playground5 行代码就能嵌进任何 React 文档站。

pnpm add @lucascv/shaderpad-playground react three monaco-editor @monaco-editor/react
import { ShaderPlayground } from "@lucascv/shaderpad-playground";
import "@lucascv/shaderpad-playground/styles";

<ShaderPlayground
code="void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }"
storageKey="my-article/hello"
/>;

Live Demo:shaderpad.lucaslib.net/embed-test | npm:@lucascv/shaderpad-playground

封装过程踩到几个值得记一笔的点:

为什么必须是 MDX(前提中的前提)

这事能成立,关键在 MDX 这个东西本身。普通 Markdown 只能写文字 + 代码块,碰到 <ShaderPlayground /> 这种 JSX 直接懵——<div> 都识别不了,更别说塞交互组件。

MDX(Markdown + JSX)就是为这个问题生的:它扩展了 Markdown 语法,允许在文档里直接写 React 组件。原理不复杂——编译期用 @mdx-js/mdx 这类工具把 .mdx 文件解析成 React 树:Markdown 部分走 remark 管线出 React 元素,JSX 部分原样透传,最后合成一棵完整的组件树渲染。

而且目前主流文档框架几乎都原生支持 MDX:Docusaurus(你正在看的这个博客就是)、Astro、Nextra、VitePress 都是开箱即用,不用额外搭脚手架。如果你的博客 / 文档站已经在用这些技术栈,零迁移成本就能接入——这点很关键,意味着 Playground 的受众不是只有 React 重度用户,而是几乎所有写技术文档的人。

这套机制让「散文 + 代码块 + 实时 demo」在同一个文件里无缝混排。其他路线都做不到这种程度:

  • iframe 嵌外部 playground:样式割裂、跨域通信麻烦、宿主主题融不进去
  • 截图 + 跳 CodePen / ShaderToy:读者被迫跳出当前阅读流
  • 录视频:完全丧失可编辑性,等于把核心价值阉了

MDX 路线能让交互组件和正文真正长在一起

持久化代码

用户编辑过的代码要保留下来(下次打开还是他改过的版本),但如果作者改了文章的示例代码,旧草稿就会"幽灵生效"——看起来加载了,但内容是上一版的。

包里的做法是把源文件内容用 djb2 算一个短 hash,写进 localStorage key 里

// v2 key 格式:embed-test/pair-box:a3f9b1c2
function buildKey(storageKey, source) {
return `${storageKey}:${shortHash(source)}`;
}

源文件一变 hash 就变,自动生成新 key,旧草稿自然绕过。这套机制上线后,文档示例库再迭代也没出过"代码不匹配"的玄学问题。

单 / 双着色器配置

最简的用法是只传一个 code 字段,跑单 stage。但有些场景(顶点动画、varying 传递)必须 vertex + fragment 联动才能跑起来,所以包内同时提供了 pair 配置:

<ShaderPlayground
pair={{
vertex: `void main() { gl_Position = vec4(position, 1.0); }`,
fragment: `void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }`,
}}
storageKey="docs/glsl/coords"
/>

pair 时画布会自动切到 Vertex / Fragment 双 tab,编辑器用单一实例、两边各自一份代码,互不打架。

组件包打包

为什么选 tsup?ESM/CJS 格式怎么控?

在抽离 @lucascv/shaderpad-playground 这个独立的 npm 包时,我需要一个打包工具。之所以选 tsup,是因为它底层基于 esbuild,打包速度极快,而且开箱即用支持 .d.ts 类型生成。对于这种不用配复杂 Webpack loader 的纯 TS/React UI 组件库来说,体验简直是降维打击。

但我在这里踩了一个经典的 ESM/CJS 双格式导出的坑。

一开始打包后,Astro 主站引入组件时报了页面水合(Hydration)失败的 SyntaxError。排查后发现,是因为包的 package.json 声明了 "type": "module",导致宿主在解析依赖时,拿到了格式不匹配的产物。

为了同时完美支持现代框架(需要 ESM 以支持 Tree-shaking)和类似 Docusaurus 2.x 这种可能依赖旧版 Webpack 设定的工具(需要 CJS),必须在 tsup.config.ts 里手动接管输出扩展名:

export default defineConfig({
format: ["esm", "cjs"],
outExtension({ format }) {
// 强制把 ESM 产物后缀设为 .js(因为 type: module),CJS 产物后缀设为 .cjs
return { js: format === "cjs" ? ".cjs" : ".js" };
},
});

同时,package.json 里的 exports 字段必须严丝合缝地对齐

"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js", // ESM 消费者走这里
"require": "./dist/index.cjs" // CJS 消费者走这里
}
}

只有当这两边绝对对齐,各种宿主框架在根据自身环境 importrequire 时,才能精准命中正确的模块,彻底消灭 Hydration 报错。

几个工程取舍

  • CSS 变量全用 spg- 前缀:颜色 / 边框 / 强调色全部走 CSS 变量,主题跟随 documentElement[data-theme]和宿主站主题自然融合,不会突兀地"白底黑字"。
  • 响应式断点 720px:宽屏左右分屏(编辑器 + 画布),窄屏自动堆叠成上下结构,手机也能直接看效果
  • React 17 / 18 / 19 全兼容react / react-dom / three / monaco-editor 全是 peerDependencies不打包进 dist,包体核心 ~66KB(gzip),按需由消费方装。本网站基于 Docusaurus 2.4 + React 17 这个老古董环境,也都支持集成。

URL 分享功能

作为一个在线调试工具,如果不做后端数据库,怎么分享代码?

这里的方案是:把整个 Shader 源码通过 LZString 压缩 + Base64 编码后,直接塞进 URL 的 hash 路由参数里。可以在 ShaderPad 右上角点击 share,然后新打开标签页粘贴体验

为什么是这套组合

https://shaderpad.lucaslib.net/?a=1&b=2#/playground?code=xxx
└── query ──┘ └────── hash ──────┘

整段 URL 只有 hash 留在客户端,query 和 path 都会被发到服务器——意味着要后端配合、还要防日志和 CDN 污染。改 hash 不触发 HTTP 请求,对「无后端 + 静态部署」是天然选择。

  • LZString 压缩。 GLSL 天然高冗余,关键字和模板片段反复出现。LZString 是为「短字符串 + URL」场景设计的,输出本身就是 string,压缩比通常 3~5x。
  • Base64 兜底「URL 安全」。 压缩后的字节流是二进制,里面可能混着控制字符。Base64 把任意字节映射到 64 个 URL-safe 字符,~33% 的体积代价被上一层的压缩比覆盖。

浏览器对 URL 长度有隐性上限(实测 Chrome 大概在 8KB~32KB 之间),代码长了会被截断。所以分享出去的链接天然适合"短小精悍的示例"——这其实和调试场景挺契合的,单文件 shader 本来就不该太长。

这样任何人拿到链接,打开就能直接还原当前的编辑状态,完全不需要后端的介入,真正做到了「无状态」的极简分享。

部署

刚好最近换了台新服务器,ShaderPad 就作为第一个部署的应用上线了。关于新服务器配置环境,还专门写了一篇文章 《linux个人云服务器开荒指南》

顺便吐槽一句某某云:旧那台 1 核 2G 的云服务器续费依旧贵得离谱,反而新买一台 2 核 4G 首年还有大折扣,算下来差不多。旧机器上也没跑几个应用,迁移成本不高,索性换台配置高一点的。

部署的核心组件是 Nginx Proxy Manager(下文简称 NPM,注意和 Node 的 npm 不是一回事)。

Nginx Proxy Manager

NPM 是一个基于 Nginx 的可视化反向代理管理工具,包装成 Docker 镜像后一行命令就能起,很香。

  • 提供 Web 管理界面,不用手写 nginx.conf、不用 nginx -s reload
  • SSL 证书申请 + 部署一条龙(Let's Encrypt 自动化),告别以前去某某云控制台手动申请再 vim nginx.conf 的繁琐

端口规划(默认会占三个,记得在某某云防火墙里放行规则):

端口用途暴露建议
80HTTP公开
443HTTPS公开
81管理界面只对可信 IP 开放

自动化部署

走 GitHub Actions + rsync:

  • push 到 main → 触发 .github/workflows/deploy-web.yml
  • pnpm install + pnpm --filter web build,产物在 apps/web/dist/
  • rsync-deployments action 把 dist/ 推到服务器的 /var/www/shaderpad/dist/(与 NPM 静态资源目录保持一致)

踩过的坑

Nginx 与 CI 环境的琐碎坑点

  • Nginx 500 错误:启动 NPM 容器的时候没正确挂载宿主机的静态资源目录(如 /home),导致 Nginx 找不到文件,修改 docker-compose.yml 补上 volumes 映射即可。
  • pnpm@10 lockfile 冲突:GitHub Actions CI 环境默认拉了最新的 pnpm v10,解析项目 v8 的 pnpm-lock.yaml 时报 ERR_PNPM_LOCKFILE_BREAKING_CHANGE 错误。解决方案是在 CI 步骤里明确指定安装 pnpm@8.11.0 版本。

总结与思考

以前我一直想做个在线工具,但总觉得市面上轮子已经够多了,加上开发和部署成本,迟迟没有动手。这次在 AI 的加持下(核心代码大量借助了 Minimax-M3 等大模型),极大地压缩了「从想法到上线」的周期。

ShaderPad 不仅让我调试和学习 glsl 代码更方便,也让我跑通了从「单体应用开发」到「通用组件抽离」,再到「自动化发版部署」的完整工程化闭环。

第一版先保持极简,后续如果大家觉得好用,会考虑扩展对 TSL 和 WGSL 的支持。欢迎来玩!

在线体验地址:ShaderPad ,对应 github 仓库

· 8 min read

这阵子把组内最大的前端项目做了下 webpack 的升级,构建效率直线上升,主要得益于 webpack5 的缓存策略

webpack5 带来了什么?

  • 持久化缓存。webpack4 需要通过 cache-loader/hardSourcePlugin 来实现中间缓存,webpack5 相当于内置了这部分功能
  • 更好的 hash 算法。hash-->fullhash,比如 webpack 4 如果添加空白、注释或修改变量名是会影响 contenthash 值的计算,webpack5 则不会影响,从而能继续使用缓存,这个方式降低了缓存的失效率,间接加快了应用 rebuild 的速度
  • Asset Modules。指的是图片和字体等这一类型文件模块,它们无须使用额外的预处理插件
  • 模块联邦。实现应用级的模块复用,这个是现在蛮多新兴微前端框架的基础
  • tree-shaking 改进。据说可以减少约 30%的 bundle-size,不过实际项目中并没有体会到这个变化
  • 更严格的代码检查。这个也是造成很多 webpack4 项目在升级后突然在运行时报错的原因,会导致页面 crash
  • 确定的 moduleId/chunkId
  • Node Polyfill 脚本被移除
  • 原生 worker 支持。可以参考 react 项目中使用 web worker 里面有提及 webpack5 下如何使用 web worker

开始升级

这个过程也是遇到了一些问题,总结如下:

因为我们的项目是基于 CRA4 搭建的,并且基于 react-app-rewired 扩展 webpack 配置,所以首先需要升级 react-scripts、react-app-rewired、customize-cra

额外引入的 loader、plugin 都要升级到支持 webpack5 的版本。没有支持 webpack5,那就去 github issue 里面看看啥时候支持?

首先是 addLessLoader 不适配,需要更换插件如下:

// config-overrides.js
const addLessLoader = require("customize-cra-less-loader");
//...
addLessLoader({
lessOptions: {
javascriptEnabled: true,
sourceMap: false,
},
}),

可以借助 npm-check-updates 这个插件检查所有依赖版本

去除一些废弃的插件,比如以下的资源插件

  • url-loader 将文件作为 dataURI 内联到 bundle 中
  • file-loader 将文件发送到输出目录
  • raw-loader 将文件导入为字符串

webpack5 通过以下配置就可以完成对资源文件的解析

  • asset/resource 发送一个单独的文件并导出 URL(file-loader)
  • asset/inline 导出一个资源的 dataURI(url-loader)
  • asset 在导出一个 dataURI 和发送一个单独的文件之间自动选择。webpack4 需要通过使用 url-loader 并且配置资源体积限制来实现
  • asset/source 导出资源的源代码

配置方式参考:

// ...
rules: [
{
test: /\.png$/i,
use: 'file-loader'
},
{
test: /\.(jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
},
},
],
}
],
// 改成
// ...
rules: [
{
test: /\.png$/i,
use: 'asset/resource'
},
{
test: /\.(jpg|gif)$/i,
type: 'asset',
parser: {
dataurlCondition: {
maxSize: 1024 // 单位是B
}
}
}
],

这里可以参考下我给项目做的资源配置

// config-overrides.js
// ...
addWebpackModuleRule({
test: /\.(png|jpe?g|gif|webp|svg|bmp|ttf|eot|woff|woff2)$/i,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 1024 * 1024,
},
},
generator: {
filename: "img/[name].[hash:4][ext]",
publicPath: process.env.PUBLIC_URL,
},
}),
addWebpackModuleRule({
test: /\.(objs?|mtl)$/i,
type: "asset/resource",
generator: {
filename: "model/[name].[hash:4][ext]",
publicPath: process.env.PUBLIC_URL,
},
}),
// ...

去除 hard-source-webpack-plugin,取而代之的是 cache

cache: {
// memory(内存)|filesystem(持久化缓存)
type: "filesystem",
buildDependencies: {
config: [__filename],
},
version: '1.0'
}

缓存文件会保存在 node_modules/.cache。参考 https://github.com/webpack/webpack/issues/6527

如果配置 filesystem 做持久化储存,webpack5 还是会同时使用 memory,用于 watch 模式

如果是 eject 导出完整 webpack 配置的项目,可能还会遇到以下问题:

部分插件的引入方式需要调整,比如 webpack-merge 改成解构的方式引入

const webpackMerge = require("webpack-merge");
const ManifestPlugin = require("webpack-manifest-plugin");
// 改成
const { merge: WebpackMerge } = require("webpack-merge");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");

如果有单独引用 dev-sever 也要升级,启动命令调整如下:

// package.json
{
"scripts": {
"serve": "webpack-dev-server --config webpack.config.dev.js"
}
}
// 改成
{
"scripts": {
"serve": "webpack server --config webpack.config.dev.js"
}
}

通过 require 引入的图片会无法正常加载,需要在 url-loader 配置中添加 esModule: false

sourcemap 的名称可能也需要调整

devtool: "cheap-eval-module-source-map";
// 改成
devtool: "eval-cheap-module-source-map";

开启 hot: true 热更新无效,需要添加配置如下:

{
target: process.env.NODE_ENV === "development" ? "web" : "browserslist",
}

这像是一个 bug,参考 https://github.com/webpack/webpack-dev-server/issues/2758

除去编译问题后,接下来主要在于解决一些运行时的报错,基本上按照提示一个个去解就行了

JSON 模块只能使用默认引入,调整如下

import { name } from "package.json";
// 改为
import pkgInfo from "package.json";
const { name } = pkgInfo;

其他的问题比如重复的函数或变量、undefined 等,在 webpack5 更严格的检查下会暴露,逐个修复就行了

进一步优化

CRA5 在 webpeck 的配置上已经做了很充分的优化工作,参考源码 https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/config/webpack.config.js

进一步优化的空间其实不多,可以做下 code spilting,配置参考:

// config-overrides.js
module.exports = {
webpack: override(
// ...
isProduction &&
setWebpackOptimizationSplitChunks({
chunks: "all",
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)/,
name: "react",
priority: 1,
},
three: {
test: /[\\/]node_modules[\\/](three)/,
name: "three",
priority: 1,
},
antd: {
test: /[\\/]node_modules[\\/](antd|@ant-design)/,
name: "antd",
priority: 1,
},
echarts: {
test: /[\\/]node_modules[\\/](echarts|zrender)/,
name: "echarts",
priority: -1,
},
antv: {
test: /[\\/]node_modules[\\/](@antv)/,
name: "antv",
priority: -1,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
priority: -5,
},
},
})
),
};
// ...

如果是多页或者多路由页面的应用,还可以结合 React.lazy 进行页面级别的分包,按需加载页面及其资源

总结

实际升级下来,其实问题不多,花个一天左右就填完坑了,可能是 react-scripts@5.0 已经做了大部分工作了,然后疑难问题基本上都可以找到解决方法。升级效果也很明显,主要体现在 rebuild 的速度上。我觉得还没升级的话,可以大胆升级一波,升级完之后就可以尝试 webpack5 的各种新特性啦 ~

· 8 min read

前端工程化其实是软件工程在前端方面的应用,是指将系统化的、规范化的、可度量的方法用于前端应用的开发、运行和维护的过程。其主要的作用有:

  • 提升生产效率
  • 降低前端开发成本,解放生产力
  • 提高前端应用质量(质量保证)
  • 降低企业成本

简单说就是研究如何 降本提效和更好地支撑业务

前端工程化的思维导图参考下图: image.png

一个简单的研发流程如下: image.png

以上其实只是我个人现阶段对前端工程化的理解,还会持续更新。文章也主要基于以上这个流程来阐述下前端工程化

脚手架

大厂基本上都会自研脚手架,通过 node-cli 去选择和拉取需要的脚手架到本地进行开发,除了统一的项目结构,此时脚手架里面还包含了这次项目所需的依赖和一些基础配置文件,包括 eslintprettier等代码规范的配置文件

开发

这个阶段是对编码规范、git 规范、项目规范的实践

编码规范

可以参考 Google 的代码风格指南

Javascript 风格指南

TypeScript 风格指南

Git 规范

分支管理规范

  • master、release、develop、feature
  • 基于 develop 创建新的 feature 分支进行开发
  • 如果是多人协作,则需要先额外约定一个合并分支,再基于该分支创建新的 feature 分支

commit 规范

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
  • type
feat: 新功能、新特性
fix: 修改 bug
perf: 更改代码,以提高性能
refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改)
docs: 文档修改
style: 代码格式修改, 注意不是 css 修改(例如分号修改)
test: 测试用例新增、修改
build: 影响项目构建或依赖项修改
revert: 恢复上一次提交
ci: 持续集成相关文件修改
chore: 其他修改(不在上述类型中的修改)
release: 发布新版本
workflow: 工作流相关文件修改
  • scope: 这次 commit 影响的范围, 可以是文件夹或者某个文件
  • subject: commit 的概述
  • body: commit 具体修改内容, 可以分为多行
  • footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接

借助 husky 验证 commit 规范,主要通过 git 的 pre-commit 钩子函数来进行

npm i husky -D

然后在你项目根目录下新建一个文件夹 script,并在下面新建一个文件 commit-check.js,输入以下代码:

const msgPath = process.env.HUSKY_GIT_PARAMS;
const msg = require("fs").readFileSync(msgPath, "utf-8").trim();

const commitRE =
/^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/;

if (!commitRE.test(msg)) {
console.error(`commit 消息不规范`);
process.exit(1);
}

最后在 package.json 加上下面的代码

"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "node script/commit-check.js",
"pre-push": "npm test"
}
}
  1. commit 之前检查代码格式
  2. 检查 commit message
  3. 将代码推送到远程仓库前,执行 npm test 进行测试,测试失败则不会推送

项目规范

项目结构

文件名称统一使用小写,名称过长用 - 隔开

├─assets (静态资源)
├─components (公共组件)
├─containers (页面)
├─styles (公共样式)
├─routes (路由)
├─store (数据管理)
├─services (接口函数)
├─helper (工具文件夹)
├─request (请求函数)
└─utils (工具函数)

其他

  • 权限配置
  • 路由配置
  • 菜单
  • ...

ui 规范

ui 规范需要由前端、UI/UE、产品商量和制定,建议使用统一的组件库

统一 ui 标准,可以减少前端在开发中由于 ui 设计额外增加的工作量

测试

前端其实很少会做比较规范的测试,毕竟这事吃力不讨好。比较多的应该是用 Jest 做单元测试,主要针对工具函数和公共组件,确保这部分高复用代码的质量

构建

构建这一步主要看使用的构建工具吧,现在主流的还是 webpack,所以针对 webpack 构建会创建好最基础的配置,然后可以做一些编译优化来提升构建速度。以及做持续构建和集成,这一步具体做啥我还不清楚

部署

简单的持续部署可以利用 jenkinswebhook 钩子函数来实现,webhook 会监听某个事件,当触发某个事件(比如 push 事件)时,通过钩子函数通知 jenkins 并执行预先设置好的脚本去部署应用

部署时也要讲究一定的策略,减少上线过程造成的异常。可以参考 大公司里怎样开发和部署前端代码?,里面有讲到一些蛮不错的方法

监控

主要分为 性能监控异常监控。作用是监视应用情况、预警和定位问题,然后可以根据应用情况来做一些有效的性能优化。可以通过买 sentry 的服务或者自研的方式实现一个监控系统

性能监控

一般借助 window.performance 来采集网页的性能指标

image.png

初探 performance – 监控网页与程序性能

如何进行 web 性能监控

异常监控

异常监控其实就是收集应用的报错信息,主要收集以下三种报错信息:

  • 资源加载错误
  • js 执行报错
  • 异步错误,比如 promise。

通过报错收集,可以了解到网站发生错误的类型和数量,从而可以做相应的预防措施来减少网站异常的问题

沉淀了 3 年的自研前端错误监控系统,打通你的脉络

埋点

  • 性能数据上报,由性能监控消费
  • 异常数据上报,由异常监控消费
  • 用户数据收集和分析(navigator、UV、PV、跳转来源、页面停留时间...)
  • 业务数据收集...

最后

以上只是我现阶段对前端工程化的理解和概括,后面我会对工程化的各个环节做更深的探索和实践,进一步夯实前端工程化能力

· 11 min read

本文主要是记录 Git 在实际开发中的常见用法和技巧

环境配置(Mac)

1. 检查环境变量

在终端输入 git,如果出现下面的提示,则需要先初始化 xcode

Agreeing to the Xcode/iOS license requires admin privileges, please run “sudo xcodebuild -license” and then retry this command.

打开 Xcode 软件进行初始化即可,之后再次输入'git',出现下面的提示则说明,环境变量配置成功

usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
<command> [<args>]

2. 配置用户基本信息

每一条提交都会产生一条记录,这条记录标记了提交人的姓名与邮箱,以便他人查看与联系。

git config --global user.name "JacksonZhou"
git config --global user.email "1359926897@qq.com"
// 查看全局配置
git config --list

3. 配置 ssh

配置了 SSH 到指定服务器上,即说明让服务器信任你的笔记本,每次从 gitlab 或 github 拉代码和上传代码时不用输入用户名和密码

生成 ssh key

ssh-keygen -t rsa -C "1359926897@qq.com"

检查是否生成成功

cat ~/.ssh/id_rsa.pub
// ssh key 生成成功的话,就会看到如下输出
// ssh-rsa ......

复制 ssh-rsa 后面的内容,粘贴到你服务器网站的 SSH Key 配置项里面即完成配置,接下来就可以拉取代码了。

Git 基本操作

先看下 Git 的基本工作流 image.png 下面是一些实际开发过程常见的操作,更详细的内容建议查看 Git 中文手册

工作区 <--> 远程仓库

克隆远程仓库到本地

  • git clone
git clone origin <remote_repo>
git clone -b <branch> <remote_repo> // 从指定分支拉取代码

连接远程仓库

  • git remote
git remote add origin <remote_repo>     // 链接远程仓库
git remote set-url origin <remote_repo> // 修改远程仓库
git remote rm origin // 删除远程仓库
git remote -v // 查看远程仓库列表
git remote show origin // 查看具体仓库细节

upstream

在 Github 上,我们可以 fork 任意开源项目到个人 ID 下,当我们把个人 ID 下的仓库 clone 到本地时,除了默认的 origin 仓库外,我们还需要配置一个 upstream 仓库,指向原始的开源项目地址,保证可以定期同步源仓库的更新。(用的比较少)

git remote add upstream <git_repository_url> // 建立 upstream 仓库
git remote remove upstream // 删除远程 upstream 仓库

实际开发过程中,如果没有先确定分支的追踪关系,当我们使用 git pull 或 git push 时就需要指定从远程的哪个分支拉取合并和推送到远程的哪个分支。

git branch -u <> <>    // 指定本地分支和远程分支的追踪关系
git push -u <> <> // 确定追踪关系并提交代码

与远程仓库同步

  • git pull
git pull origin <branch> // git fetch + git merge,从指定分支拉取代码并更新
git pull --rebase // git fetch + git rebase,不会生成 merge 记录,建议是用于个人分支

工作区 <--> 暂存区

提交

  • git add
git add .
git add -i // 筛选想要提交的修改并提交

撤销

  • git restore

暂存区 <--> 本地仓库

提交

  • git commit
git commit -m "" // 提交代码到本地仓库

重新提交

如果发现已经提交了改动,但是还有修改也是需要合到那一部分改动中的,可以这样操作

git add .
git commit --amend -m ""
// amend 也可以单纯用来修改 commit 信息

撤销

  • git reset - 适用于本地记录,即还没提交到远程仓库的记录
git reset HEAD^1 --hard   // 重置为上一个 commit,并清空工作区域的代码改动
git reset HEAD^1 --soft // 重置为上一个 commit,不过会保留工作区域的代码改动
  • git revert - 适用于已经提交到远程仓库的记录的撤销

commit message 规范

# 主 type
feat: 增加新功能
fix: 修复bug

# 其他 type
docs: 只改动了文档相关的内容
style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
build: 构造工具的或者外部依赖的改动,例如webpack,npm
refactor: 代码重构时使用
revert: 执行git revert打印的message
test: 添加测试或者修改现有测试
perf: 提高性能的改动
ci:CI(持续集成服务)有关的改动
chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动

本地仓库 <--> 远程仓库

同步

  • git fetch
  1. 从远程仓库下载本地仓库中缺失的提交记录
  2. 更新远程分支指针

提交

  • git push

    在多人协作的分支上尽量不用 git push -f、git rebase、reset 的操作

git push -f.gif

撤销

HEAD 总是指向当前分支上最近一次提交记录。

git checkout <commit_hash> // 让 HEAD 指向指定的 commit
HEAD^ // 向前移动一个提交记录
HEAD^2 // TODO 移动到前面一排记录的第二个提交记录
HEAD~3 // 向前提交三个记录

通过 cat .git/head 可以查看 HEAD 指向

分支

  • git branch - 创建本地分支
git branch '' // 创建分支
git switch '' // 切换分支
git checkout -b '' // 创建分支并切换到该分支
git branch -f <branch> <commit_hash/branch> // 将指定 branch 的 HEAD 指向某个提交记录或者某个分支的 HEAD

stash

git stash 可以将当前工作状态(WIP,work in progress)临时存放在 stash 列表中,待 pull / merge 操作完成后,再从 stash 中重新应用这些修改。

git stash list
git stash save 'message' // 存储一个自定义 message 的 WIP 到 stash 栈中
git stash -u save '' // -u 参数表明新增的文件也一起 stash
git stash pop // 恢复上一次的 WIP 状态,并从 stash 栈中移除
git stash pop stash@{num} // 恢复指定编号的 WIP,并从 stash 栈中移除
git stash apply stash@{num} // 恢复指定编号的 WIP,但不从 stash 栈中移除

如果 git stash 的文件刚好被其他提交修改过了,那么在 pop 时会自动将修改过的文件标记为 conflict 状态

合并代码

merge

该命令主要按以下两种策略合并代码:

Fast-forword(--ff)

git_merge_ff.gif

no-fast-forword(--no-ff)

git_merge_noff.gif

rebase

Rebase 实际上就是取出一系列的提交记录,将他们复制到其他地方。相比 merge,rebase 能提供更加线性的提交历史。这个操作比较危险,会改变提交历史,因此建议不要对已经处于远端的多人共用分支做 rebase 操作。

reword:修改提交信息;
edit:修改此提交;
squash:将提交融合到前一个提交中;
fixup:将提交融合到前一个提交中,不保留该提交的日志消息;
exec:在每个提交上运行我们想要 rebase 的命令;
drop:移除该提交。

git_rebase.gif

复制记录

  • cherry-pick - 可以复制指定的 commit

查看日志

  • git config
// 1. git 配置 alias
git config --global alias.ls 'log --name-status --oneline --graph'
git ls
git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"
git lg
// 2. 用 zsh 配置 alias

git show <commit_id> // 查看具体某次commit记录
git log -p -n // n是代表最近几次的提交,可以查看最近几次commit记录

开发实践

合并多个 commit

  • git rebase -i HEAD~3 // 编辑最近三个记录
p HEAD1
s HEAD2
s HEAD3

如何修改之前的某个提交(已 push)?

  1. git rebase 先调整待修改的提交到最新
  2. 修改后提交
  3. git rebase 再调整回来

如何修改之前的某个 commit?

  • git commit --amend -- 修改 commit message 或追加文件改动建议用这个
  • git reset HEAD^1 --soft

如何复制其他分支的文件夹到当前分支?

git checkout <branch> -- [file_name]

参考

· 13 min read

不论是 react 还是 vue 脚手架,都依赖 webpack 进行构建和打包。这一篇主要是我之前在入门 webpack 4 的时候记的一点小笔记,详细内容还得参见官方文档

简单打个包

初始化项目,会生成一个 package.json

npm init
// 快速 init
// 1、默认都 yes
npm init -y
// 2、设置 init 模板
npm config set init.author.name "JacksonZhou"
npm init -y // 或者 -f

安装 webpack 和 webpack-cli

npm install webpack -D
npm install webpack-cli -D

我们随意创建一个 src 目录下的 index.js 文件,然后在 package.json 修改 scripts

"build": "webapck --mode production"
npm run build

这时会生成一个 dist 文件夹,这个里面就是 webpack 打包好的文件

创建配置文件

创建一个 webpack.config.js,执行 webpack 命令时会去执行这个配置文件。若是自定义配置文件的名字,则应该使用命令 webpack config 文件名

entry

入口,webpack 会从这里开始解析依赖。默认会按 entry 来划分 chunk

entry: './src/index.js'

output

webpack 最终构建出来的静态文件,可配置输出结果的文件名、路径等

const path = require('path') // 本质上webpack配置文件就是nodejs脚本,所以可以使用nodejs的模块
output: {
publicPath: '', // 输出资源的网址前缀
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // 原文件名
}

关于 filename 的 hash 命名中 hash、chunkhash、contenthash 的区别

  • hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值,但是 chunkhash 下,引用其他文件的文件一旦修改,也会改变其他文件的 hash 值,即使其他文件没动过。
  • contenthash保证文件内容必须要修改后,hash 值才会变化

loader

通过使用不同的 loader 来解析处理不同类型的文件。webpack 默认支持打包 js 文件,其他格式的文件需要借助对应的 loader 进行转换

换句话说,当 webpack 遇到其他文件的时候,会去配置文件的 module 选项中去找相应的规则。比如在配置文件中我们可以规定当 webpack 遇到图片文件的时候,就使用 file-loader 来帮我们进行打包文件

常见 loader

  1. babel-loader 将 es6/es7 转换成 es5(待确认)
npm i babel-loader @babel/core @babel/preset-env @babel/polyfill -D
npx babel src --out-dir lib // 测试 babel
module: {
rules: [
{
// 命中 js 文件
test: '/\.js$/',
// 使用 babel-loader 来解析 js 文件
loader: 'babel-loader',
// 只命中 src 目录下的 js 文件,加快 webpack 的编译速度
include: path.resolve(__dirname, 'src'),
// 排除 node_modules,加快 webpack 的编译速度
exclude: /node_modules/,
}
]
},

这样还不能发挥 Babel 的作用。在项目根目录下创建一个 .babelrc 文件,添加代码:

{
"presets": [
"@babel/preset-env"
]
}
  1. 构建 CSS
npm i css-loader style-loader -D
module: {
rules: [
{
test: /\.css/,
include: [
path.resolve(__dirname, 'public/style'),
],
use: ['style-loader','css-loader']
// loader 的执行顺序是从下到上,从右到左
}
]
}
// 在index.js里引入
import '../public/style/index.css'

css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,帮我们分析出几个 css 文件之间的关系,并最终把这些 css 文件合并成一段 css,例如解析 @import 和 url() 等引用外部文件的声明;

style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

经由上述两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js 一起打包了。

  1. 处理预处理语言
npm i less-loader -D
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
'less-loader',
],
}),
},
],
},
  1. 处理图片

file-loader 可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回

url-loader会将引入的图片编码,生成 dataURl 并将其打包到文件中。但是如果图片较大,编码会消耗性能。因此 url-loader 提供了一个 limit 参数,小于 limit 字节的文件会被转为 DataURl,大于 limit 的还会使用 file-loader 进行 copy

image-webpack-loader 可用于压缩图片

{
test: '/\.(png|gif|jpe?g)$/i',
use: [
{
loader: 'url-loader',
options: {
limit: 8192 // 单位是 Byte,一般当文件小于 8KB 时作为 DataURL 处理
}
}
]
},
{
test: '/.*\.(gif|png|jpe?g|svg|webp)$/i',
use: [
{
loader: 'file-loader',
options: {}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: { // 压缩 jpeg 的配置
progressive: true,
quality: 65
},
optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
enabled: false,
},
pngquant: { // 使用 imagemin-pngquant 压缩 png
quality: '65-90',
speed: 4
},
gifsicle: { // 压缩 gif 的配置
interlaced: false,
},
webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
quality: 75
},
}
},
]
}

使用html-withimg-loader进行处理 img 标签引入的图片

npm i html-withimg-loader --save-dev
{
test:/\.(html|htm)$/,
use:'html-withimg-loader'
}
  1. 打包图标字体

其他常用 loader

  • px2rem-loader。移动端 css px 自动转换为 rem 的 loader
  • postcss-loader + autoprefixer。在 css 属性上加上相应的浏览器的厂商前缀,如-webkit、-ms、-moz 等
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
// webpack.config.js
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
'postcss-loader',
]
}
  • ts-loader。转换 ts 代码
  • thread-loader。多进程打包资源
  • raw-loader。将文件以字符串的形式导入
  • ...

plugin

在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。比如有以下这些常见的使用场景

清除 dist 文件

npm i clean-webpack-plugin -D
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

plugins: [new CleanWebpackPlugin()];

关联 HTML

  1. webpack 从入口开始构建 JS ,而通常一个 spa 项目都是从一个 html 页面出发的,这时候需要使用 script 标签引用构建好的 JS 文件。
  2. 但是问题来了,这些构建好的 JS 文件名可能会变,比如用了 hash 值作为文件名,所以我们需要通过插件来自动关联 html 文件
npm i -D html-webpack-plugin
// webpack.config.js
...
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 输出文件
template: './public/index.html' // 模板文件
})
]

之后打包npm run build,dist 目录就有了 index.html 了

其他 plugin

  • ProvidePlugin:设置全局变量
  • mini-css-extract-plugin:将 css 从 bundle 文件中提取成一个独立的 css 文件
  • terser-webpack-plugin:压缩 js 的插件,支持压缩 es6 代码
  • copy-webpack-plugin:将文件或者文件夹拷贝到构建的输出目录
  • ...

mode

开启不同的环境,webpack 4 会根据这个参数做不同的处理和优化,具体处理和优化可以参见 https://webpack.js.org/configuration/mode/#mode-development

resolve

该配置用于扩展模块的解析路径,我们可以通过它配置一些 alias

module.exports = {
...
resolve: {
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
'src': path.resolve(__dirname, '../src'),
},
},
};

optimization

在构建后期负责压缩和优化代码,常见的代码分割现在是通过这个配置来控制。optimization.splitChunks 默认是不用设置的。如果 modeproduction,那 Webpack 4 就会开启 Code Splitting,不过只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks'all'

Webpack 4 的 Code Splitting 最大的特点就是配置简单,和基于内置规则自动拆分。内置的代码切分的规则是这样的:

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

一般用默认的配置就行了。如果有特殊的需求,也可以通过下面的 optimization.splitChunks API 定制。

splitChunks

用法示例

chunks: 'all',
name: true,
cacheGroups: {
// 缓存组,会继承和覆盖splitChunks的配置
default: {
// 模块缓存规则,设置为false,默认缓存组将禁用
minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
priority: -20, // 优先级
reuseExistingChunk: true, // 默认使用已有的模块
},
vendors: {
test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
priority: -10,
},
antd: {
name: 'antd', // 单独将 antd 拆包
priority: 15, // 权重需大于其它缓存组
test: /[\/]node_modules[\/](antd|@ant-design)[\/]/,
},
bizcharts: {
name: 'bizcharts', // 单独将 bizcharts 拆包
priority: 10,
test: /[\/]node_modules[\/]bizcharts[\/]/,
},
antv: {
name: 'antv', // 单独将 antv 拆包
priority: 12,
test: /[\/]node_modules[\/]@antv[\/]/,
},
},

sourceMap

sourceMap 其实就是源码和打包文件的映射关系,可以帮助我们快速定位一些代码问题。webpack 4 默认会开启,关键配置为 devtool,下面是它的一些配置对比:

webpack-sourcemap-type.png

  • inline。直接会将 .map 文件直接打包到对应的 js 中去,从而加快相应的速度。使用这个我们会发现,打包出来的文件没有 .map 文件了,而是以 base64 的形式放入了打包的文件中了。
  • cheap。map 文件只会帮你定为到具体的 某一行,并不会把代码定位到 具体的 某一行 某一列,从而加快速度;cheap 还有一个作用,就是这个选项只使针对业务代码,也就是说只能定位到业务代码里面的错误,并不能定位到我们引用的第三方文件(比如说 loader,第三方模块)的错误。
  • module。不仅会帮我们定位 自己的业务代码中的错误,还会同时帮我们定位第三方模块的错误。
  • eval。使用 eval 包裹模块代码,并且存在 //@sourceURL,这个是打包速度最快,性能最好的的一种方式,但是有的时候,对于代码比较复杂的情况,它提示出来的错误可能不够全面。

建议配置

开发环境:cheap-module-eval-source-map

生产环境不建议开启,如果需要定位错误,可以尝试用cheap-module-source-map