原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/9.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 9(opens new window)
Author
回答者: shfshanyue(opens new window)
新人入职新上手项目,如何把它跑起来,这是所有人都会碰到的问题:所有人都是从新手开始的。
有可能你会脱口而出:npm run dev/npm start
,但实际工作中,处处藏坑,往往没这么简单。
- 查看是否有
CI/CD
,如果有跟着CI/CD
部署的脚本跑命令 - 查看是否有
dockerfile
,如果有跟着dockerfile
跑命令 - 查看 npm scripts 中是否有 dev/start,尝试
npm run dev/npm start
- 查看是否有文档,如果有跟着文档走。为啥要把文档放到最后一个?原因你懂的
但即便是十分谨慎,也有可能遇到以下几个叫苦不迭、浪费了一下午时间的坑:
- 前端有可能在本地环境启动时需要依赖前端构建时所产生的文件,所以有时需要先正常部署一遍,再试着按照本地环境启动 (即需要先
npm run build
一下,再npm run dev/npm start
)。(比如,一次我们的项目 npm run dev 时需要 webpack DllPlugin 构建后的东西) - 别忘了设置环境变量或者配置文件 (.env/consul/k8s-configmap)
因此,设置一个少的 script,可以很好地避免后人踩坑,更重要的是,可以避免后人骂你,
此时可设置 script hooks,如 prepare
、postinstall
自动执行脚本,来完善该项目的基础设施
{
"scripts": {
"start": "npm run dev",
"config": "node assets && node config",
"build": "webpack",
// 设置一个钩子,在 npm install 后自动执行,此处有可能不是必须的
"prepare": "npm run build",
"dev": "webpack-dev-server --inline --progress"
}
}
对于一个纯生成静态页面打包的前端项目而言,它们是没有多少区别的:生产环境的部署只依赖于构建生成的资源,更不依赖 npm scripts。可见 如何部署前端项目(opens new window)。
使用 create-react-app
生成的项目,它的 npm script 中只有 npm start
{
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
使用 vuepress
生成的项目,它的 npm script 中只有 npm run dev
{
"dev": "vuepress dev",
"build": "vuepress build"
}
在一个面向服务端的项目中,如 next
、nuxt
与 nest
。dev 与 start 的区别趋于明显,一个为生产环境,一个为开发环境
- dev: 在开发环境启动项目,一般带有 watch 选项,监听文件变化而重启服务,此时会耗费大量的 CPU 性能,不宜放在生产环境
- start: 在生产环境启动项目
在 nest
项目中进行配置
{
"start": "nest start",
"dev": "nest start --watch"
}
Author
我的意见和楼上相反,应该先大概看一遍文档…… 文档中会描述本地环境的配置方法
查看是否有 CI/CD,如果有跟着 CI/CD 部署的脚本跑命令
查看是否有 dockerfile,如果有跟着 dockerfile 跑命令
查看 npm scripts 中是否有 dev/start,尝试 npm run dev/npm start
大部分公司的开发环境都是本地环境,所以什么 CI/CD、Docker 可以先放到一边
npm run dev/npm start 这个是一般的约定,但不是所有的项目都是这样。所以需要先看 package.json 中的 script 来确定
- npm start 是 npm run start 的别名,支持 prestart 和 poststart 钩子
Author
回答者: linlai163(opens new window)
我的意见和楼上相反,应该先大概看一遍文档…… 文档中会描述本地环境的配置方法
查看是否有 CI/CD,如果有跟着 CI/CD 部署的脚本跑命令 查看是否有 dockerfile,如果有跟着 dockerfile 跑命令 查看 npm scripts 中是否有 dev/start,尝试 npm run dev/npm start
大部分公司的开发环境都是本地环境,所以什么 CI/CD、Docker 可以先放到一边
npm run dev/npm start 这个是一般的约定,但不是所有的项目都是这样。所以需要先看 package.json 中的 script 来确定
- npm start 是 npm run start 的别名,支持 prestart 和 poststart 钩子
你是真没吃过文档的亏。。。管他什么公司,文档都有坑。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/84.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 84(opens new window)
Author
回答者: wjw-gavin(opens new window)
减少 http 请求次数: CSS Sprites, JS、CSS 源码压缩、图片大小适当控制; 网页 Gzip,CDN 托管,data 缓存 ,图片服务器。 尽量减少内联样式 将脚本放在底部 少用全局变量、缓存 DOM 节点查找的结果 图片预加载 按需加载
Author
回答者: shfshanyue(opens new window)
可参考 Google 的文档 https://developers.google.com/web/fundamentals/performance/get-started(opens new window)
Author
回答者: hwb2017(opens new window)
https://developers.google.com/web/fundamentals/performance/get-started
根据谷歌 Web 开发者网站总结的性能优化点:
- 资源加载优化
- 衡量性能指标
- Lab Data, 在规范的特定条件下,对 Web 应用的各项指标进行评估,典型工具如谷歌的 lighthouse
- RUM,基于真实用户的性能指标监控,包括 FCP,FID,CLS 等,参考 https://web.dev/user-centric-performance-metrics/
- 瀑布图,借助 performance API 记录整个站点和各个资源的加载时长
- 优化资源大小(字节数)
- 评估各资源的用途并评估是否可以直接移除
- 通过压缩技术(minimize 和 compress)减少文本类资源(CSS,JavaScript,HTML)大小
- 选择合适的图片格式、裁剪图片、懒加载图片等,通过 picture 标签响应式地返回图片,参考 https://www.jianshu.com/p/607567e488fc
- 预加载和长期缓存字体,参考 https://web.dev/optimize-webfont-loading/
- 减少 HTTP 请求次数
- 合并文本资源,比如使用 webpack 这样的 bundle 技术
- 合并图片资源,比如雪碧图
- 内联内容较小的资源到 html 中,比如 data url
- 善用 HTTP 缓存
- 本地缓存命中顺序,内存缓存 => Service Worker 缓存 => HTTP 缓存(磁盘缓存) => HTTP/2 Push 缓存,参考 https://calendar.perfplanet.com/2016/a-tale-of-four-caches/
- https://web.dev/http-cache/
- 优化 JavaScript
- JavaScript 的处理过程:下载(fetch) => 解压 => 解析(代码转换为 AST) => 编译(AST 转换为字节码) => 执行
- 死代码消除(Tree Shaking),减小总体传输文件大小
- Code Spliting + 基于路由的按需加载,减小首次渲染的传输文件大小
- 优化首次渲染路径
- 渲染路径: DOM 树构建 => CSSOM 树构建 => Render Tree 构建 => 样式计算 => 布局 => 绘制位图 => 合成图层
- 通过媒体查询避免首次渲染时加载不必要的 CSS 文件
- 将对页面结构无影响的 JS 文件标记为 async 和 defer,避免阻塞 html 解析
- 衡量性能指标
- 渲染优化
- 使用 requestAnimationFrame 代替 setTimeout 和 setInterval 来更新视图,减少卡顿
- 将计算密集型的 JavaScript 代码移动到 Web Worker 中执行,避免占用主线程
- 使用复杂度更低、class 风格的 CSS 选择器;减少频繁变动的 CSS 样式的影响元素个数
- 使用性能更高的 flex 布局代替 float 布局
- 避免对 offsetHeight 等 dom 属性的频繁访问,导致重绘和重排操作队列的频繁同步执行
- 在 performance profiling 之后,将频繁变动的动画部分所属的 dom 元素标记为 will-change,独立构成一个图层
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/95.html
更多描述
更短的部署时间,更少的人为干预,更有利于敏捷开发
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 95(opens new window)
Author
回答者: shfshanyue(opens new window)
TODO
Author
回答者: DoubleRayWang(opens new window)
Jenkins+docker
Author
回答者: Carrie999(opens new window)
需要 1 个小时,需要
Author
回答者: shfshanyue(opens new window)
@Carrie999 一个小时!!!?这也太久了吧
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/103.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 103(opens new window)
Author
回答者: wangkailang(opens new window)
- 注册 npm 账号 https://www.npmjs.com/
- 本地通过命令行
npm login
登陆 - 进入到项目目录下(与 package.json 同级),在 package.json 中指定发布文件、文件夹
{
"name": "pkg-xxx",
"version": "0.0.1",
"main": "lib/index.js",
"module": "esm/index.js",
"typings": "types/index.d.ts",
"files": [
"CHANGELOG.md",
"lib",
"esm",
"dist",
"types",
],
...
}
执行 npm publish --registry=https://registry.npmjs.org/
即可发布
还可以配合 GitHub Packages(opens new window) 发布
Author
回答者: Carrie999(opens new window)
我还会发布 vscode 主题呢,https://marketplace.visualstudio.com/items?itemName=carrie999.cyberpunk-2020 ,看下载量 8k 呢
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/138.html
title: "【Q137】js 代码压缩 minify 的原理是什么 | js,前端工程化高频面试题" description: "【Q137】js 代码压缩 minify 的原理是什么 字节跳动面试题、阿里腾讯面试题、美团小米面试题。"
更多描述
我们知道 javascript
代码经压缩 (uglify) 后,可以使体积变得更小,那它代码压缩的原理是什么。
如果你来做这么一个功能的话,你会怎么去压缩一段 js
代码的体积
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 138(opens new window)
Author
回答者: shfshanyue(opens new window)
https://github.com/mishoo/UglifyJS2
Author
回答者: libin1991(opens new window)
@shfshanyue 问的是原理,你贴UglifyJS2的地址干嘛
Author
回答者: everlose(opens new window)
uglify 包里有 ast.js 所以它一定是生成了抽象语法树 接着遍历语法树并作出优化,像是替换语法树中的变量,变成a,b,c那样的看不出意义的变量名。还有把 if/else 合并成三元运算符等。 最后输出代码的时候,全都输出成一行。
Author
回答者: fariellany(opens new window)
uglify 包里有 ast.js 所以它一定是生成了抽象语法树 接着遍历语法树并作出优化,像是替换语法树中的变量,变成a,b,c那样的看不出意义的变量名。还有把 if/else 合并成三元运算符等。 最后输出代码的时候,全都输出成一行。
非常nice
Author
回答者: shfshanyue(opens new window)
通过 AST 分析,根据选项配置一些策略,来生成一颗更小体积的 AST 并生成代码。
目前前端工程化中使用 terser(opens new window) 和 swc(opens new window) 进行 JS 代码压缩,他们拥有相同的 API。
常见用以压缩 AST 的几种方案如下:
// 对两个数求和
function sum (a, b) {
return a + b;
}
此时文件大小是 62 Byte
, 一般来说中文会占用更大的空间。
多余的空白字符会占用大量的体积,如空格,换行符,另外注释也会占用文件体积。当我们把所有的空白符合注释都去掉之后,代码体积会得到减少。
去掉多余字符之后,文件大小已经变为 30 Byte
。 压缩后代码如下:
function sum(a,b){return a+b}
替换掉多余字符后会有什么问题产生呢?
有,比如多行代码压缩到一行时要注意行尾分号。
function sum (first, second) {
return first + second;
}
如以上 first
与 second
在函数的作用域中,在作用域外不会引用它,此时可以让它们的变量名称更短。但是如果这是一个 module
中,sum
这个函数也不会被导出呢?那可以把这个函数名也缩短。
// 压缩: 缩短变量名
function sum (x, y) {
return x + y;
}
// 再压缩: 去除空余字符
function s(x,y){return x+y}
在这个示例中,当完成代码压缩 (compress
) 时,代码的混淆 (mangle
) 也捎带完成。 但此时缩短变量的命名也需要 AST 支持,不至于在作用域中造成命名冲突。
通过分析代码逻辑,可对代码改写为更精简的形式。
合并声明的示例如下:
// 压缩前
const a = 3;
const b = 4;
// 压缩后
const a = 3, b = 4;
布尔值简化的示例如下:
// 压缩前
!b && !c && !d && !e
// 压缩后
!(b||c||d||e)
在编译期进行计算,减少运行时的计算量,如下示例:
// 压缩前
const ONE_YEAR = 365 * 24 * 60 * 60
// 压缩后
const ONE_YAAR = 31536000
以及一个更复杂的例子,简直是杀手锏级别的优化。
// 压缩前
function hello () {
console.log('hello, world')
}
hello()
// 压缩后
console.log('hello, world')
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/154.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 154(opens new window)
Author
RBAC: Role-Based Access Control?
Author
回答者: knockkeykey(opens new window)
当我们通过角色为某一个用户指定到不同的权限之后,那么该用户就会在 项目中体会到不同权限的功能
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/157.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 157(opens new window)
Author
回答者: shfshanyue(opens new window)
圈复杂度(Cyclomatic complexity)描写了代码的复杂度,可以理解为覆盖代码所有场景所需要的最少测试用例数量。CC 越高,代码则越不好维护
Author
回答者: Carrie999(opens new window)
code review
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/190.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 190(opens new window)
Author
window.performance.timing,详细的可以看下这篇文章前端性能优化衡量指标(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/192.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 192(opens new window)
Author
回答者: grace-shi(opens new window)
Open Graph 协议可以让任何一个网页集成到社交图谱中。例如,facebook 就是一种社交图谱(social graph)。 一旦一个网页按照该协议进行集成,这个网页就像是社交图谱的一个节点,例如,你的网页集成了 open graph 协议, 按照协议加入了网页的标题,描述以及图片信息等等,那么你在 facebook 中分享这个网页的时候,facebook 就会按照 你定义的内容来展示这个网页。
这个协议其实很简单,主要是通过在 html 中加入一些元数据(meta)标签来实现,例如 在 head 中加入 meta 标签,property 是以 og(open graph)开头, 后面跟着具体属性,content 里面是属性的值, 下面这段描述的就是一个类型为 video.movie,标题为 The rock,以及 url 和图片信息。这个例子就可以当做是 为 https://www.imdb.com/title/tt0117500/ 实现了 Open Graph 协议、
<html prefix="og: http://ogp.me/ns#">
<head>
<title>The Rock (1996)</title>
<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="http://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="http://ia.media-imdb.cimg/rock.jpg" />
...
</head>
...
</html>
结论: 这个协议主要是 Facebook 提出来的,为了更好的展示用户分享的网页的内容,实现这个协议,有助于 SEO 优化,告诉 google 该网页有哪些内容,以及关键词等。
可以快速实现 Open Graph 协议的工具有: Wordpress 的 SEO plugin 使用 Facebook 的 Facebook Page 功能
Reference:
- The Open Graph Protocol https://ogp.me/
- Open Graph Protocol for Facebook Explained with Examples https://www.optimizesmart.com/how-to-use-open-graph-protocol/
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/193.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 193(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/194.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 194(opens new window)
Author
回答者: CaicoLeung(opens new window)
换成 taobao 源?
Author
可以直接使用淘宝源,使用以下命令切换淘宝源: npm config set registry=https://registry.npm.taobao.org
另外不建议直接使用 cnpm,实际使用中发现会遇到很多奇怪的错误。
Author
回答者: wjw-gavin(opens new window)
可以使用nrm进行 npm 不同源的切换 https://github.com/Pana/nrm
Author
回答者: shfshanyue(opens new window)
- 选择时延低的 registry,需要企业技术基础建设支持
NODE_ENV=production
,只安装生产环境必要的包(如果 dep 与 devDep 没有仔细分割开来,工作量很大,可以放弃)CI=true
,npm 会在此环境变量下自动优化- 结合 CI 的缓存功能,充分利用
npm cache
- 使用
npm ci
代替npm i
,既提升速度又保障应用安全性
Author
回答者: Carrie999(opens new window)
科学上网,镜像,使用 pnpm
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/195.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 195(opens new window)
Author
回答者: fariellany(opens new window)
npm ci (6.0 版本以上) 1。会删除项目中的 node_modules
文件夹; 2. 会依照项目中的package.json
来安装确切版本的依赖项; 3. 不像 npm install, npm ci
不会修改你的 package-lock.json
但是它确实期望你的项目中有一个 - package-lock.json 文件 - 如果你没有这个文件, npm ci 将不起作用,此时必须使用 npm install
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/196.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 196(opens new window)
Author
回答者: shfshanyue(opens new window)
packagelock.json
/yarn.lock
用以锁定版本号,保证开发环境与生产环境的一致性,避免出现不兼容 API 导致生产环境报错
在这个问题之前,需要了解下什么是 semver
: 什么是 semver(opens new window)。
当我们在 npm i
某个依赖时,默认的版本号是最新版本号 ^1.2.3
,以 ^
开头可最大限度地使用新特性,但是某些库不遵循该依赖可能出现问题。
^1.2.3
指 >=1.2.3 <2.0.0,可查看 semver checker(opens new window)
一个问题: 当项目中没有 lock 文件时,生产环境的风险是如何产生的?
演示风险过程如下:
pkg 1.2.3
: 首次在开发环境安装 pkg 库,为此时最新版本1.2.3
,dependencies
依赖中显示^1.2.3
,实际安装版本为1.2.3
pkg 1.19.0
: 在生产环境中上线项目,安装 pkg 库,此时最新版本为1.19.0
,满足dependencies
中依赖^1.2.3
范围,实际安装版本为1.19.0
,但是pkg
未遵从 semver 规范,在此过程中引入了 Breaking Change,如何此时1.19.0
有问题的话,那生产环境中的1.19.0
将会导致 bug,且难以调试
而当有了 lock 文件时,每一个依赖的版本号都被锁死在了 lock 文件,每次依赖安装的版本号都从 lock 文件中进行获取,避免了不可测的依赖风险。
pkg 1.2.3
: 首次在开发环境安装 pkg 库,为此时最新版本1.2.3
,dependencies
依赖中显示^1.2.3
,实际安装版本为1.2.3
,在 lock 中被锁定版本号pkg 1.2.3
: 在生产环境中上线项目,安装 pkg 库,此时 lock 文件中版本号为1.2.3
,符合dependencies
中^1.2.3
的范围,将在生产环境安装1.2.3
,完美上线。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/201.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 201(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/237.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 237(opens new window)
看场景的。请补充场景。再说这个和前端工程化有啥关系?
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/252.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 252(opens new window)
Author
回答者: edisonwd(opens new window)
在 linux 系统中,我通常通过 ps -aux |grep 服务名
查看服务端口
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/257.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 257(opens new window)
Author
回答者: shfshanyue(opens new window)
请求头中的 refer 来判断是否屏蔽图片
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/270.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 270(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/271.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 271(opens new window)
Author
回答者: shfshanyue(opens new window)
CSS (Cross Site Scripting),跨站脚本攻击。可使用以下脚本在指定网站上进行攻击
<script> alert("XSS"); </script>
<img src="https://devtool.tech/notfound.png" onerror="alert('XSS')" />
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/274.html
更多描述
当入职新公司,接手一个新的项目时,如何知道这个项目需要的 node 版本是多少
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 274(opens new window)
Author
回答者: DoubleRayWang(opens new window)
如果项目使用的 yarn 和 typescript,可以查看 yarn.lock 里的@types/node@* 的 version
Author
回答者: shfshanyue(opens new window)
packageJson.engines
,第三方模块都会有,自己的项目中有可能有pm2.app[].interpreter
,如果采用pm2
部署,可以查看 interpreter 选项,但不保证该项存在FROM
,如果采用docker
部署,查看基础镜像Dockerfile
中 node 的版本号- 如果以上方式都不可以,那只有问人了
Author
回答者: shfshanyue(opens new window)
@DoubleRayWang 我试了一下,这种方法应该是不靠谱的
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/278.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 278(opens new window)
Author
回答者: shfshanyue(opens new window)
du
(disk usage) 命令可以查看磁盘的使用情况,从它可以看出来文件及目录的大小
# -d 搜索深度,0 指当前目录
# -h 以可读性的方式显示大小
$ du -hd 0 node_modules
132M node_modules
同理,可以使用以下命令查看 node_modules
下每个目录所占的大小
$ du -hd 1 node_modules
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/294.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 294(opens new window)
Author
回答者: shfshanyue(opens new window)
https://indepth.dev/npm-peer-dependencies/(opens new window)
Author
回答者: micro-kid(opens new window)
避免重复安装
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/295.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 295(opens new window)
Author
回答者: maya1900(opens new window)
语义化版本号。版本格式:主版本号.次版本号.修订号
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/296.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 296(opens new window)
Author
回答者: shfshanyue(opens new window)
当一个包是可依赖可不依赖时,可采用 optionalDependencies
,但需要在代码中做好异常处理。
如 chokidar(opens new window) 对 fsevents
的引入
{
"optionalDependencies": {
"fsevents": "~2.1.2"
}
}
let fsevents;
try {
fsevents = require("fsevents");
} catch (error) {
if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
}
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/298.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 298(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/378.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 378(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/391.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 391(opens new window)
Author
回答者: shfshanyue(opens new window)
生成 DOM 会从远程下载 Byte,并根据相应的编码 (如 utf8
) 转化为字符串,通过 AST 解析为 Token,生成 Node 及最后的 DOM。
AST 解析过程可以查看 https://astexplorer.net/(opens new window)
可以通过 devtools 中查看该过程
当解析 CSS 文件时,最终会生成 CSSOM
DOM 与 CSSOM 会一起生成 Render Tree,只包含渲染网页所需的节点。
计算每一个元素在设备视口内的确切位置和大小
以下图片来自于 关键渲染路径 - 掘金(opens new window)
将渲染树中的每个节点转换成屏幕上的实际像素,这一步通常称为绘制或栅格化
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/412.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 412(opens new window)
Author
回答者: zxhycxq(opens new window)
chrome 自带的灯箱
Author
回答者: shfshanyue(opens new window)
最常见且实用的性能工具有两个:
lighthouse
: 可在 chrome devtools 直接使用,根据个人设备及网络对目标网站进行分析,并提供各种建议webpagetest
: 分布式的性能分析工具,可在全球多个区域的服务器资源为你的网站进行分析,并生成相应的报告
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/422.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 422(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/430.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 430(opens new window)
Author
回答者: shfshanyue(opens new window)
下边这个正则表达式能把 CPU 跑挂的正则表达式就是一个定时炸弹,回溯次数进入了指数爆炸般的增长。
const safe = require("safe-regex");
const re = /(x+x+)+y/;
// 能跑死 CPU 的一个正则
re.test("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
// 使用 safe-regex 判断正则是否安全
safe(re); // false
safe-regex(opens new window) 能够发现哪些不安全的正则表达式。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/435.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 435(opens new window)
Author
回答者: shfshanyue(opens new window)
通过 proxy_pass
与 upstream
即可实现最为简单的负载均衡。如下配置会对流量均匀地导向 172.168.0.1
,172.168.0.2
与 172.168.0.3
三个服务器
http {
upstream backend {
server 172.168.0.1;
server 172.168.0.2;
server 172.168.0.3;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
关于负载均衡的策略大致有以下四种种
- round_robin,轮询
- weighted_round_robin,加权轮询
- ip_hash
- least_conn
轮询,nginx
默认的负载均衡策略就是轮询,假设负载三台服务器节点为 A、B、C,则每次流量的负载结果为 ABCABC
加权轮询,根据关键字 weight 配置权重,如下则平均没来四次请求,会有八次打在 A,会有一次打在 B,一次打在 C
upstream backend {
server 172.168.0.1 weight=8;
server 172.168.0.2 weight=1;
server 172.168.0.3 weight=1;
}
对每次的 IP 地址进行 Hash,进而选择合适的节点,如此,每次用户的流量请求将会打在固定的服务器上,利于缓存,也更利于 AB 测试等。
upstream backend {
server 172.168.0.1;
server 172.168.0.2;
server 172.168.0.3;
ip_hash;
}
选择连接数最少的服务器节点优先负载
upstream backend {
server 172.168.0.1;
server 172.168.0.2;
server 172.168.0.3;
least_conn;
}
说到最后,这些负载均衡策略对于应用开发者至关重要,而基础开发者更看重如何实现这些策略,如这四种负载算法如何实现?请参考以后的文章
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/475.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 475(opens new window)
Author
回答者: shfshanyue(opens new window)
commonjs
是 Node 中的模块规范,通过 require
及 exports
进行导入导出 (进一步延伸的话,module.exports
属于 commonjs2
)
同时,webpack 也对 cjs
模块得以解析,因此 cjs
模块可以运行在 node 环境及 webpack 环境下的,但不能在浏览器中直接使用。但如果你写前端项目在 webpack 中,也可以理解为它在浏览器和 Node 都支持。
比如,著名的全球下载量前十 10 的模块 ms(opens new window) 只支持 commonjs
,但并不影响它在前端项目中使用(通过 webpack),但是你想通过 cdn 的方式直接在浏览器中引入,估计就会出问题了
// sum.js
exports.sum = (x, y) => x + y;
// index.js
const { sum } = require("./sum.js");
由于 cjs
为动态加载,可直接 require
一个变量
require(`./${a}`);
esm
是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。
它使用 import/export
进行模块导入导出.
// sum.js
export const sum = (x, y) => x + y;
// index.js
import { sum } from "./sum";
esm
为静态导入,正因如此,可在编译期进行 Tree Shaking,减少 js 体积。
如果需要动态导入,tc39 为动态加载模块定义了 API: import(module)
。可将以下代码粘贴到控制台执行
const ms = await import("https://cdn.skypack.dev/ms@latest");
ms.default(1000);
esm 是未来的趋势,目前一些 CDN 厂商,前端构建工具均致力于 cjs 模块向 esm 的转化,比如 skypack
、 snowpack
、vite
等。
目前,在浏览器与 node.js 中均原生支持 esm。
- cjs 模块输出的是一个值的拷贝,esm 输出的是值的引用
- cjs 模块是运行时加载,esm 是编译时加载
示例: array-uniq(opens new window)
一种兼容 cjs
与 amd
的模块,既可以在 node/webpack 环境中被 require
引用,也可以在浏览器中直接用 CDN 被 script.src
引入。
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["jquery"], factory);
} else if (typeof exports === "object") {
// CommonJS
module.exports = factory(require("jquery"));
} else {
// 全局变量
root.returnExports = factory(root.jQuery);
}
})(this, function ($) {
// ...
});
示例: react-table(opens new window), antd(opens new window)
这三种模块方案大致如此,部分 npm package 也会同时打包出 commonjs/esm/umd 三种模块化格式,供不同需求的业务使用,比如 antd(opens new window)。
Author
回答者: xiyuanyuan(opens new window)
webpack 打包使用的是 cjs, vite 使用的是 esm,这样理解对吗 rollup 使用的是啥模块化方案?
webpack 也有 tree-shaking 吗和 rollup 的有啥不同
Author
回答者: songcee(opens new window)
已收到你的邮件,谢谢~~~
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/495.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 495(opens new window)
Author
回答者: haotie1990(opens new window)
前端工程化的主要目标就是解放生产力、提高生产效率。通过制定一系列的规范,借助工具和框架解决前端开发以及前后端协作过程中的痛点和难度问题。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/497.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 497(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/507.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 507(opens new window)
Author
回答者: buzuosheng(opens new window)
服务端渲染 SSR:在服务端将请求的所有资源生成 HTML,客户端收到后可以直接渲染。
Author
回答者: shfshanyue(opens new window)
renderToString
hydrate
Author
回答者: haotie1990(opens new window)
服务器渲染 (SSR):将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。这个过程可以成为服务端渲染。
优势:
-
更好的 SEO
-
更快的内容到达时间 (time-to-content)
Vue.js 服务器端渲染指南(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/513.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 513(opens new window)
Author
回答者: shfshanyue(opens new window)
- LCP: Largest Content Paint
- FID: Firtst Input Delay
- CLS: Cumulative Layout Shift
Good | Needs improvement | Poor | |
---|---|---|---|
LCP | <=2.5s | <=4s | >4s |
FID | <=100ms | <=300ms | >300ms |
CLS | <=0.1 | <=0.25 | >0.25 |
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/521.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 521(opens new window)
Author
回答者: shfshanyue(opens new window)
对于业务代码而讲,它俩区别不大
当进行业务开发时,严格区分 dependencies
与 devDependencies
并无必要,实际上,大部分业务对二者也并无严格区别。
当打包时,依靠的是 Webpack/Rollup
对代码进行模块依赖分析,与该模块是否在 dep/devDep
并无关系,只要在 node_modules
上能够找到该 Package 即可。
以至于在 CI 中 npm i --production
可加快包安装速度也无必要,因为在 CI 中仍需要 lint、test、build 等。
对于库 (Package) 开发而言,是有严格区分的
- dependencies: 在生产环境中使用
- devDependencies: 在开发环境中使用,如 webpack/babel/eslint 等
当在项目中安装一个依赖的 Package 时,该依赖的 dependencies
也会安装到项目中,即被下载到 node_modules
目录中。但是 devDependencies
不会
因此当我们开发 Package 时,需要注意到我们所引用的 dependencies
会被我们的使用者一并下载,而 devDependencies
不会。
一些 Package 宣称自己是 zero dependencies
,一般就是指不依赖任何 dependencies
,如 highlight(opens new window)
JavaScript syntax highlighter with language auto-detection and zero dependencies.
Author
生产依赖会随着包一起下载,开发依赖不会,npm i --production 可以只下载生产依赖
Author
回答者: haotie1990(opens new window)
dependencies、devDependencies
dependencies
字段指定了项目运行所依赖的模块,devDependencies
指定项目开发所需要的模块。
当你在软件包目录下执行npm install
命令时,dependencies
、devDependencies
指定的三方软件包均会在node_modules
目录下安装,若执行npm install --production
命令,则不会安装devDependecies
指定的三方软件包。但当软件包作为三方软件包被安装时(npm install $package
),则dependencies
指定的软件包会被安装,devDependencies
指定指定的软件包不会被安装。
了解dependencies
和devDependencies
的作用后,我们在开发软件包时,哪些依赖应该放入dependencies
,哪些依赖应该放入devDependencies
中。
首先我们要明确放入dependencies
中的依赖软件包,是我们的项目在生产环境下运行时必须依赖的软件包,其的部分功能或全部功能通常会被打包到我们工程发布的bundles
中。而放入devDependencies
中软件包是我们的工程在开发时依赖的软件包,通常情况下以下的依赖会被放入devDenpencies
中:
-
格式化代码或错误检查类软件包:
esLint
、prettier
-
打包工具及其插件:
webpack
,gulp
,parceljs
-
babel
及其的插件 -
单元测试类:
enzyme
,jest
Author
回答者: haotie1990(opens new window)
peerDependencies
的目的是提示宿主环境去安装满足插件peerDependencies
所指定依赖的包,然后在插件import
或者require
所依赖的包的时候,永远都是引用宿主环境统一安装的npm
包,最终解决插件与所依赖包不一致的问题。
知道peerDependencies
的作用后,什么样的软件包依赖需要放入?
当我们开发的工程将作为第三方软件包发布的时候,我们就会用到peerDependencies
。当我们发布软件包作为三方依赖运行时,并且我们确认或猜测到依赖我们的软件包的工程也会安装和我们软件包相同的三方依赖,我们就可以将这些依赖放入peerDependencies
中。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/522.html
更多描述
例: 你们项目中是否引用了 npm 库 semver(opens new window)
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 522(opens new window)
Author
回答者: shfshanyue(opens new window)
yarn list | grep xxx
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/523.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 523(opens new window)
Author
package.json 中的 main 对应的文件
Author
回答者: shfshanyue(opens new window)
TODO
Author
回答者: hwb2017(opens new window)
- 如果 npm 包导出的是 ESM 规范的包,使用 module
- 如果 npm 包只在 web 端使用,并且严禁在 server 端使用,使用 browser
- 如果 npm 包只在 server 端使用,使用 main
- 如果 npm 包在 web 端和 server 端都允许使用,使用 browser 和 main
参考 https://www.cnblogs.com/h2zZhou/p/12929472.html
Author
回答者: shfshanyue(opens new window)
@hwb2017 目前 main、module、exports 是用的最多的几项字段,browser 目前用的越来越少了
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/524.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 524(opens new window)
Author
回答者: shfshanyue(opens new window)
多个包难以互相链接
Author
回答者: haotie1990(opens new window)
https://docs.npmjs.com/cli/v7/using-npm/workspaces(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/533.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 533(opens new window)
Author
回答者: shfshanyue(opens new window)
- 我: 老大,我这个项目本地白屏了,今天调了一天都没找到问题,快来看看
- leader: (瞄了一眼) 你的 node 版本号有问题
- 我: 老大,不能怪我跑挂了,我一个新入职的小前端怎么能够知道这个项目所需的 Node 版本号是多少呢
- leader: 怎么不能知道,这说明你水平不到家
指定一个项目所需的 node 最小版本,这属于一个项目的质量工程。
如果对于版本不匹配将会报错(yarn)或警告(npm),那我们需要在 package.json
中的 engines
字段中指定 Node 版本号
更多质量工程问题,见 如何保障项目质量(opens new window)
{
"engines": {
"node": ">=14.0.0"
}
}
一个示例:
我在本地把项目所需要的 node 版本号改成 >=16.0.0
,而本地的 node 版本号为 v10.24.1
此时,npm 将会发生警告,提示你本地的 node 版本与此项目不符。
npm WARN EBADENGINE Unsupported engine { package: 'next-app@1.0.0',
npm WARN EBADENGINE required: { node: '>=16.0.0' },
npm WARN EBADENGINE current: { node: 'v10.24.1', npm: '7.14.0' } }
而 yarn 将会直接报错,提示。
error next-app@1.0.0: The engine "node" is incompatible with this module. Expected version ">=16.0.0". Got "10.24.1"
最为重要的是,项目中某些依赖所需要的 Node 版本号与项目运行时的 Node 版本号不匹配,也会报错(在 yarn 中),此时无法正常运行项目,可避免意外发生。
可看一个示例,engines 示例(opens new window),其中 ansi-regex
该依赖所需的 node 版本号为 12+
,而此时本地的 node 版本号为 10,使用 yarn 安装报错!
// 在 package.json 中,所需 node 版本号需要 >=10
{
"engines": {
"node": ">=10.0.0"
}
}
// 在 package-lock.json 中,所需 node 版本号需要 >=12
{
"node_modules/ansi-regex": {
"version": "6.0.1",
"engines": {
"node": ">=12"
}
}
}
PS: 如果项目的 package.json 中没有
engines
字段,可查看 Dockerfile 中 node 镜像确定项目所需的 node 版本号。
Author
回答者: qiutian00(opens new window)
Great!
Author
回答者: 946629031(opens new window)
nice job~ !!
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/534.html
更多描述
当你 npm install
时,你安装的是哪一种形式
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 534(opens new window)
Author
回答者: shfshanyue(opens new window)
semver
,Semantic Versioning
语义化版本的缩写,文档可见 https://semver.org/(opens new window),它由 [major, minor, patch]
三部分组成,其中
major
: 当你发了一个含有 Breaking Change 的 APIminor
: 当你新增了一个向后兼容的功能时patch
: 当你修复了一个向后兼容的 Bug 时
假设你的版本库中含有一个函数
// 假设原函数
export const sum = (x: number, y: number): number => x + y;
// Patch Version,修复小 Bug
export const sum = (x: number, y: number): number => x + y;
// Minor Version,向后兼容
export const sum = (...rest: number[]): number =>
rest.reduce((s, x) => s + x, 0);
// Marjor Version,出现 Breaking Change
export const sub = () => {};
对于 ~1.2.3
而言,它的版本号范围是 >=1.2.3 <1.3.0
对于 ^1.2.3
而言,它的版本号范围是 >=1.2.3 <2.0.0
当我们 npm i
时,默认的版本号是 ^
,可最大限度地在向后兼容与新特性之间做取舍,但是有些库有可能不遵循该规则,我们在项目时应当使用 yarn.lock
/package-lock.json
锁定版本号。
我们看看 package-lock
的工作流程。
npm i webpack
,此时下载最新 webpack 版本5.58.2
,在package.json
中显示为webpack: ^5.58.2
,版本号范围是>=5.58.2 < 6.0.0
- 在
package-lock.json
中全局搜索webpack
,发现 webpack 的版本是被锁定的,也是说它是确定的webpack: 5.58.2
- 经过一个月后,webpack 最新版本为
5.100.0
,但由于webpack
版本在package-lock.json
中锁死,每次上线时仍然下载5.58.2
版本号 - 经过一年后,webpack 最新版本为
6.0.0
,但由于webpack
版本在package-lock.json
中锁死,且 package.json 中webpack
版本号为^5.58.2
,与package-lock.json
中为一致的版本范围。每次上线时仍然下载5.58.2
版本号 - 支线剧情:经过一年后,webpack 最新版本为
6.0.0
,需要进行升级,此时手动改写package.json
中webpack
版本号为^6.0.0
,与package-lock.json
中不是一致的版本范围。此时npm i
将下载6.0.0
最新版本号,并重写package-lock.json
中锁定的版本号为6.0.0
npm i 某个 package 时会修改 package-lock.json
中的版本号吗?
当 package-lock.json
该 package 锁死的版本号符合 package.json
中的版本号范围时,将以 package-lock.json
锁死版本号为主。
当 package-lock.json
该 package 锁死的版本号不符合 package.json
中的版本号范围时,将会安装该 package 符合 package.json
版本号范围的最新版本号,并重写 package-lock.json
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/535.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 535(opens new window)
Author
回答者: shfshanyue(opens new window)
main
指 npm package 的入口文件,当我们对某个 package 进行导入时,实际上导入的是 main
字段所指向的文件。
main
是 CommonJS 时代的产物,也是最古老且最常用的入口文件。
// package.json 内容
{
name: 'midash',
main: './dist/index.js'
}
// 关于如何引用 package
const midash = require('midash')
// 实际上是通过 main 字段来找到入口文件,等同于该引用
const midash = require('midash/dist/index.js')
随着 ESM 且打包工具的发展,许多 package 会打包 N 份模块化格式进行分发,如 antd
既支持 ES
,也支持 umd
,将会打包两份。
如果使用 import
对该库进行导入,则首次寻找 module
字段引入,否则引入 main
字段。
基于此,许多前端友好的库,都进行了以下分发操作:
- 对代码进行两份格式打包:
commonjs
与es module
module
字段作为es module
入口main
字段作为commonjs
入口
{
name: 'midash',
main: './dist/index.js',
module: './dist/index.mjs'
}
// 以下两者等同
import midash from 'midash'
import midash from 'midash/dist/index.mjs'
如果你的代码只分发一份 es module
模块化方案,则直接置于 main
字段之中。
如果说以上两个是刀剑,那 exports
至少得是瑞士军刀。
exports
可以更容易地控制子目录的访问路径,也被称为 export map
。
假设我们 Package 的目录如下所示:
├── package.json
├── index.js
└── src
└── get.js
不在 exports
字段中的模块,即使直接访问路径,也无法引用!
// package.json
{
name: 'midash',
main: './index.js',
exports: {
'.': './dist/index.js',
'get': './dist/get.js'
}
}
// 正常工作
import get from 'midash/get'
// 无法正常工作,无法引入
import get from 'midash/dist/get'
exports
不仅可根据模块化方案不同选择不同的入口文件,还可以根据环境变量(NODE_ENV
)、运行环境(nodejs
/browser
/electron
) 导入不同的入口文件。
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/536.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 536(opens new window)
Author
回答者: shfshanyue(opens new window)
- prepublishOnly
- prepack
- prepare
- postpack
- publish
- postpublish
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/537.html
更多描述
例如: husky
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 537(opens new window)
Author
回答者: shfshanyue(opens new window)
使用 npm script 生命周期中的 npm prepare
,他将会在发包 (publish) 之前以及装包 (install) 之后自动执行。
如果指向在装包之后自动执行,可使用 npm postinstall
例如:
{
"prepare": "npm run build & node packages/husky/lib/bin.js install"
}
vue-cli(opens new window) 一些著名的仓库会使用 patch-package(opens new window) 自动修复 node_modules 中依赖的问题
{
"postinstall": "patch-package"
}
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/552.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 552(opens new window)
Author
回答者: shfshanyue(opens new window)
- lint
- type
- test
- code review
- git hooks
- CI
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/591.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 591(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/600.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 600(opens new window)
Author
回答者: shfshanyue(opens new window)
关于 http 缓存配置的最佳实践为以下两条:
- 文件路径中带有 hash 值:一年的强缓存。因为该文件的内容发生变化时,会生成一个带有新的 hash 值的 URL。前端将会发起一个新的 URL 的请求。配置响应头
Cache-Control: public,max-age=31536000,immutable
- 文件路径中不带有 hash 值:协商缓存。大部分为
public
下文件。配置响应头Cache-Control: no-cache
与etag/last-modified
但是当处理永久缓存时,切记不可打包为一个大的 bundle.js
,此时一行业务代码的改变,将导致整个项目的永久缓存失效,此时需要按代码更新频率分为多个 chunk 进行打包,可细粒度控制缓存。
webpack-runtime
: 应用中的webpack
的版本比较稳定,分离出来,保证长久的永久缓存react/react-dom
:react
的版本更新频次也较低vendor
: 常用的第三方模块打包在一起,如lodash
,classnames
基本上每个页面都会引用到,但是它们的更新频率会更高一些。另外对低频次使用的第三方模块不要打进来pageA
: A 页面,当 A 页面的组件发生变更后,它的缓存将会失效pageB
: B 页面echarts
: 不常用且过大的第三方模块单独打包mathjax
: 不常用且过大的第三方模块单独打包jspdf
: 不常用且过大的第三方模块单独打包
在 webpack5
中可以使用以下配置:
{
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
}
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/603.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 603(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/613.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 613(opens new window)
Author
回答者: shfshanyue(opens new window)
BFF 全称 Backend For Frontend
,一般指在前端与服务器端搭建一层由前端维护的 Node Server 服务,具有以下好处
- 数据处理。对数据进行校验、清洗及格式化。使得数据更与前端契合
- 数据聚合。后端无需处理大量的表连接工作,第三方接口聚合工作,业务逻辑简化为各个资源的增删改查,由 BFF 层聚合各个资源的数据,后端可集中处理性能问题、监控问题、消息队列等
- 权限前移。在 BFF 层统一认证鉴权,后端无需做权限校验,后端可直接部署在集群内网,无需向外网暴露服务,减少了后端的服务度。
但其中也有一些坏处,如以下
- 引入复杂度,新的 BFF 服务需要一套基础设施的支持,如日志、异常、部署、监控等
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/642.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 642(opens new window)
Author
回答者: shfshanyue(opens new window)
const fetchUser = (id) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetch: ", id);
resolve(id);
}, 5000);
});
};
const cache = {};
const cacheFetchUser = (id) => {
if (cache[id]) {
return cache[id];
}
cache[id] = fetchUser(id);
return cache[id];
};
cacheFetchUser(3).then((id) => console.log(id))
cacheFetchUser(3).then((id) => console.log(id))
cacheFetchUser(3).then((id) => console.log(id))
// Fetch: 3
// 3
// 3
// 3
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/644.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 644(opens new window)
Author
回答者: shfshanyue(opens new window)
- terser(opens new window) 或者 uglify(opens new window),及流行的使用 Rust 编写的
swc
压缩混淆化 JS。 gzip
或者brotli
压缩,在网关处(nginx)开启- 使用
webpack-bundle-analyzer
分析打包体积,替换占用较大体积的库,如moment
->dayjs
- 使用支持 Tree-Shaking 的库,对无引用的库或函数进行删除,如
lodash
->lodash/es
- 对无法 Tree Shaking 的库,进行按需引入模块,如使用
import Button from 'antd/lib/Button'
,此处可手写babel-plugin
自动完成,但不推荐 - 使用 babel (css 为 postcss) 时采用
browserlist
,越先进的浏览器所需要的 polyfill 越少,体积更小 - code spliting,路由懒加载,只加载当前路由的包,按需加载其余的 chunk,首页 JS 体积变小 (PS: 次条不减小总体积,但减小首页体积)
- 使用 webpack 的 splitChunksPlugin,把运行时、被引用多次的库进行分包,在分包时要注意避免某一个库被多次引用多次打包。此时分为多个 chunk,虽不能把总体积变小,但可提高加载性能 (PS: 此条不减小总体积,但可提升加载性能)
Author
回答者: 1689851268(opens new window)
压缩的具体操作
- 去除多余字符,eg:空格,换行、注释
- 压缩变量名,函数名、属性名
- 使用更简单的表达,eg:合并声明、布尔值简化
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/654.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 654(opens new window)
Author
回答者: Mikerui(opens new window)
lodash axios echarts file-saver patch-package qs sortablejs vue-clipboard2 xlsx watermark-dom
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/664.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 664(opens new window)
Author
回答者: shfshanyue(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/688.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 688(opens new window)
Author
回答者: shfshanyue(opens new window)
TODO
Author
回答者: illumi520(opens new window)
- 对于 pv 量比较高的页面,比如 b 站等流量图也比较大的,采用 ssr 采用 ssr 如何优化性能
- 性能瓶颈在于 react-dom render/hydrate 和 server 端的 renderToString
- 尽量减少 dom 结构, 采用流式渲染,jsonString 一个对象,而不是 literal 对象
- server 去获取数据
- 不同情况不同分析,减少主线程阻塞时间
- 减少不必要的应用逻辑在服务端运行
- 减少依赖和包的体积
- 利用 webpack 的 contenthash 缓存
- 重复依赖包处理,可以采用 pnpm
- 采用 code splitting,减少首次请求体积
- 减少第三方依赖的体积
- FP (First Paint) 首次绘制 FCP (First Contentful Paint) 首次内容绘制 LCP (Largest Contentful Paint) 最大内容渲染 DCL (DomContentloaded) FMP(First Meaningful Paint) 首次有效绘制 L (onLoad) TTI (Time to Interactive) 可交互时间 TBT (Total Blocking Time) 页面阻塞总时长 FID (First Input Delay) 首次输入延迟 CLS (Cumulative Layout Shift) 累积布局偏移 SI (Speed Index) 一些性能指标可以监控性能
4.网络 prefetch cdn
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/719.html
更多描述
如在npm script
中有以下命令:
{
"start": "serve"
}
其中 serve
可通过 --port
指定端口号:
$ npm start -- --port 8080
# 而在 yarn 时无需传递参数
$ yarn start --port 8080
那为什么 npm 执行命令传递参数时,为何需要双横线
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 719(opens new window)
Author
npm/npm#5518 npm 脚本执行时会开启一个 shell,执行后面指定的脚本命令或文件, -- 是为了给后面 shell 脚本命令传递参数,类似 node 环境的 process.argv 的吧。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/722.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 722(opens new window)
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/734.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 734(opens new window)
Author
垫片
Author
回答者: shfshanyue(opens new window)
core-js(opens new window) 是关于 ES 标准最出名的 polyfill
,polyfill 意指当浏览器不支持某一最新 API 时,它将帮你实现,中文叫做垫片。你也许每天都与它打交道,但你毫不知情。
有一段时间,当你执行
npm install
并且项目依赖core-js
时,会发现core-js
的作者正借助于npm postinstall
在找工作。
由于垫片的存在,打包后体积便会增加,所需支持的浏览器版本 越高,垫片越少,体积就会越小。
以下代码便是 Array.from
(ES6) 的垫片代码,有了它的存在,在任意浏览器中都可以使用 Array.from
这个 API。
// Production steps of ECMA-262, Edition 6, 22.1.2.1
if (!Array.from) {
Array.from = () => { // 省略若干代码 }
}
而 core-js
的伟大之处是它包含了所有 ES6+
的 polyfill,并集成在 babel
等编译工具之中
试举一例:
你在开发环境使用了 Promise.any(opens new window),而它属于 ES2021
新出的 API,在部分浏览器里尚未实现,同时,你又使用了 ES2020
新出的操作符 ?.
。
为了使代码能够在大部分浏览器里能够实现,你将会使用 babel
或者 swc
将代码编译为 ES5。
但是此时你会发现问题,如果不做任何配置,babel
/swc
只能处理操作符,而无法处理新的 API。以下代码会报错
好消息是,core-js
已集成到了 babel
/swc
之中,你可以使用 @babel/preset-env
或者 @babel/polyfill
进行配置,详见文档 core-js(opens new window)。通过配置,babel
编译代码后将会自动包含所需的 polyfill,如下所示。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/739.html
更多描述
用户反馈白屏了,你怎么处理?
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 739(opens new window)
Author
回答者: akbchris(opens new window)
- 排查兼容性。大部分原因是因为低端机型/浏览器低版本 polyfill 的问题导致报错
- 排查网络。js 是否下载成功 cdn 是否生效
- 做 js 错误上报。分析是否存在代码缺陷
- 做重试逻辑/诱导用户重试
- Error Boundry 避免整页崩溃。限制在组件级别
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/740.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 740(opens new window)
Author
回答者: shfshanyue(opens new window)
在 npm 中,使用 npm scripts
可以组织整个前端工程的工具链。
{
start: 'serve ./dist',
build: 'webpack',
lint: 'eslint'
}
除了可自定义 npm script
外,npm 附带许多内置 scripts,他们无需带 npm run
,可直接通过 npm <script>
执行
$ npm install
$ npm test
$ npm publish
我们在实际工作中会遇到以下几个问题:
- 在某个 npm 库安装结束后,自动执行操作如何处理?
- npm publish 发布 npm 库时将发布打包后文件,如果遗漏了打包过程如何处理,如何在发布前自动打包?
这就要涉及到一个 npm script 的生命周期
当我们执行任意 npm run
脚本时,将自动触发 pre
/post
的生命周期。
当手动执行 npm run abc
时,将在此之前自动执行 npm run preabc
,在此之后自动执行 npm run postabc
。
// 自动执行
npm run preabc
npm run abc
// 自动执行
npm run postabc
patch-package(opens new window) 一般会放到 postinstall
中。
{
postinstall: "patch-package";
}
而发包的生命周期更为复杂,当执行 npm publish
,将自动执行以下脚本。
- prepublishOnly: 最重要的一个生命周期。
- prepack
- prepare
- postpack
- publish
- postpublish
当然你无需完全记住所有的生命周期,如果你需要在发包之前自动做一些事情,如测试、构建等,请在 prepulishOnly
中完成。
{
prepublishOnly: "npm run test && npm run build";
}
prepare
npm install
之后自动执行npm publish
之前自动执行
比如 husky
{
prepare: "husky install";
}
假设某一个第三方库的 npm postinstall
为 rm -rf /
,那岂不是又很大的风险?
{
postinstall: "rm -rf /";
}
实际上,确实有很多 npm package 被攻击后,就是通过 npm postinstall
自动执行一些事,比如挖矿等。
如果 npm 可以限制某些库的某些 hooks 执行,则可以解决这个问题。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/741.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 741(opens new window)
Author
回答者: shfshanyue(opens new window)
git
允许在各种操作之前添加一些 hook
脚本,如未正常运行则 git 操作不通过。最出名的还是以下两个
precommit
prepush
而 hook
脚本置于目录 ~/.git/hooks
中,以可执行文件的形式存在。
$ ls -lah .git/hooks
applypatch-msg.sample pre-merge-commit.sample
commit-msg.sample pre-push.sample
fsmonitor-watchman.sample pre-rebase.sample
post-update.sample pre-receive.sample
pre-applypatch.sample prepare-commit-msg.sample
pre-commit.sample update.sample
另外 git hooks 可使用 core.hooksPath
自定义脚本位置。
# 可通过命令行配置 core.hooksPath
$ git config 'core.hooksPath' .husky
# 也可通过写入文件配置 core.hooksPath
$ cat .git/config
[core]
ignorecase = true
precomposeunicode = true
hooksPath = .husky
在前端工程化中,husky
即通过自定义 core.hooksPath
并将 npm scripts
写入其中的方式来实现此功能。
~/.husky
目录下手动创建 hook 脚本。
# 手动创建 pre-commit hook
$ vim .husky/pre-commit
在 pre-commit
中进行代码风格校验
#!/bin/sh
npm run lint
npm run test
Author
回答者: Carrie999(opens new window)
https://www.jb51.net/article/180357.htm
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/742.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 742(opens new window)
Author
回答者: shfshanyue(opens new window)
如何确保所有 npm install
的依赖都是安全的?
当有一个库偷偷在你的笔记本后台挖矿怎么办?
比如,不久前一个周下载量超过八百万的库被侵入,它在你的笔记本运行时会偷偷挖矿。
Audit
,审计,检测你的所有依赖是否安全。npm audit
/yarn audit
均有效。
通过审计,可看出有风险的 package
、依赖库的依赖链、风险原因及其解决方案。
$ npm audit
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ high │ Regular Expression Denial of Service in trim │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ trim │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=0.0.3 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ @mdx-js/loader │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ @mdx-js/loader > @mdx-js/mdx > remark-mdx > remark-parse > │
│ │ trim │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://www.npmjs.com/advisories/1002775 │
└───────────────┴──────────────────────────────────────────────────────────────┘
76 vulnerabilities found - Packages audited: 1076
Severity: 49 Moderate | 27 High
✨ Done in 4.60s.
你可以在我的笔记本上挖矿,但绝不能在生产环境服务器下挖矿,此时可使用以下两条命令。
$ npm audit production
$ yarn audit dependencies
通过 npm audit fix
可以自动修复该库的风险,原理就是升级依赖库,升级至已修复了风险的版本号。
$ npm audit fix
yarn audit
无法自动修复,需要使用 yarn upgrade
手动更新版本号,不够智能。
synk(opens new window) 是一个高级版的 npm audit
,可自动修复,且支持 CICD 集成与多种语言。
$ npx snyk
$ npx wizard
可通过 CI/gitlab/github 中配置机器人,使他们每天轮询一次检查仓库的依赖中是否有风险。
在 Github 中,可单独设置 dependabot
机器人,在仓库设置中开启小机器人,当它检测到有问题时,会自动向该仓库提交 PR。
而它的解决方案也是升级版本号。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/744.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 744(opens new window)
Author
回答者: shfshanyue(opens new window)
eslint
,对代码不仅有风格的校验,更有可读性、安全性、健壮性的校验。
关于校验分号、冒号等,属于风格校验,与个人风格有关,遵循团队标准即可,可商量可妥协。
// 这属于风格校验
{
semi: ["error", "never"];
}
与 prettier
不同,eslint
更多是关于代码健壮性校验,试举一例。
Array.prototype.forEach
不要求也不推荐回调函数返回值Array.prototype.map
回调函数必须返回一个新的值用以映射
当代码不遵守此两条要求时,通过 eslint
以下规则校验,则会报错。此种校验与代码健壮有关,不可商量不可妥协。
// 这属于代码健壮性校验
{
'array-callback-return': ['error', { checkForEach: true }]
}
在 eslint
中,使用 Rule
最为校验代码最小规则单元。
{
rules: {
semi: ["error", "never"];
quotes: ["error", "single", { avoidEscape: true }];
}
}
在 eslint
自身,内置大量 rules
,比如分号冒号逗号等配置。
校验 typescript
、react
等规则,自然不会由 eslint
官方提供,那这些 Rules 如何维护?
如 react
、typescript
、flow
等,需要自制 Rule
,此类为 Plugin
,他们维护了一系列 Rules
。
在命名时以 eslint-plugin-
开头并发布在 npm
仓库中,而执行的规则以 react/
、flow/
等开头。
{
'react/no-multi-comp': [error, { ignoreStateless: true }]
}
在第三方库、公司业务项目中需要配置各种适应自身的规则、插件等,称为 Config
。
- 作为库发布,在命名时以
elint-config-
开头,并发布在npm
仓库中。 - 为项目服务,在项目中以
.eslintrc
命名或者置于项目 package.json 中的eslintConfig
字段中,推荐第二种方案。
以下是 eslint-config-airbnb
的最外层配置。
module.exports = {
extends: [
"eslint-config-airbnb-base",
"./rules/react",
"./rules/react-a11y",
].map(require.resolve),
rules: {},
};
在我们公司实际项目中,无需重新造轮子,只需要配置文件中的 extends
继承那些优秀的 eslint-config
即可。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/745.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 745(opens new window)
Author
回答者: shfshanyue(opens new window)
npm 的版本号为 semver
规范,由 [major, minor, patch] 三部分组成,其中
- major: 当你发了一个含有 Breaking Change 的 API
- minor: 当你新增了一个向后兼容的功能时
- patch: 当你修复了一个向后兼容的 Bug 时
假设 react
当前版本号为 17.0.1
,我们要升级到 17.0.2
应该如何操作?
- "react": "17.0.1", + "react": "17.0.2",
升级版本号,最不建议的事情就是手动在 package.json 中进行修改。
- "react": "17.0.1", + "react": "17.0.2",
毕竟,你无法手动发现所有需要更新的 package。
此时可借助于 npm outdated
,发现有待更新的 package。
使用 npm outdated
,还可以列出其待更新 package 的文档。
$ npm outdated -l
Package Current Wanted Latest Location Depended by Package Type Homepage
@next/bundle-analyzer 10.2.0 10.2.3 12.0.3 node_modules/@next/bundle-analyzer app dependencies https://github.com/vercel/next.js#readme
使用 npm outdated
虽能发现需要升级版本号的 package,但仍然需要手动在 package.json 更改版本号进行升级。
此时推荐一个功能更强大的工具 npm-check-updates
,比 npm outdated
强大百倍。
npm-check-updates -u
,可自动将 package.json 中待更新版本号进行重写。
升级 [minor] 小版本号,有可能引起 Break Change
,可仅仅升级到最新的 patch 版本。
$ npx npm-check-updates --target patch
- 当一个库的 major 版本号更新后,不要第一时间去更新,容易踩坑,可再度过几个 patch 版本号再更新尝试新功能
- 当遇到 major 版本号更新时,多看文档中的 ChangeLog,多看升级指导并多测试及审计
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/746.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 746(opens new window)
Author
回答者: shfshanyue(opens new window)
以下 mermaid 无法渲染,可移至 https://juejin.cn/post/7030084290989948935(opens new window)
当 require('package-hello')
时,假设 package-hello
是一个 npm 库,我们是如何找到该 package
的?
- 寻找当前目录的
node_modules/package-hello
目录 - 如果未找到,寻找上一级的
../node_modules/package-hello
目录,以此递归查找
在 npmv2
时,node_modules
对于各个 package 的拓扑为嵌套结构。
假设:
- 项目依赖
package-a
与package-b
两个 package package-a
与package-b
均依赖lodash@4.17.4
依赖关系以 Markdown 列表表示:
- package-a
- `lodash@4.17.4`
- package-b
- `lodash@4.17.4`
此时 node_modules
目录结构如下:
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
A ---> C("lodash@4.17.4")
B ---> D("lodash@4.17.4")
此时最大的问题
- 嵌套过深
- 占用空间过大
目前在 npm/yarn 中仍然为平铺结构,但 pnpm 使用了更省空间的方法,以后将会提到
在 npmv3
之后 node_modules
为平铺结构,拓扑结构如下:
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
app ---> C("lodash@4.17.4")
依赖关系以 Markdown 列表表示
- package-a
- `lodash@^4.17.4`
- package-b
- `lodash@^4.16.1`
答: 与上拓扑结构一致,因为二者为 ^
版本号,他们均会下载匹配该版本号范围的最新版本,比如 @4.17.4
,因此二者依赖一致。
此时如果有 lock,会有一点小问题,待稍后讨论
node_modules 目录结构如下图:
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
app ---> C("lodash@4.17.4")
- package-a
- `lodash@4.17.4`
- package-b
- `lodash@4.16.1`
答:package-b 先从自身 node_modules 下寻找 lodash
,找到 lodash@4.16.1
node_modules 目录结构如下图:
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
app ---> C("lodash@4.17.4")
B ---> D("lodash@4.16.1")
- package-a
- `lodash@4.0.0`
- package-b
- `lodash@4.0.0`
- package-c
- `lodash@3.0.0`
- package-d
- `lodash@3.0.0`
答:package-d 只能从自身的 node_modules 下寻找 lodash@3.0.0
,而无法从 package-c 下寻找,此时 lodash@3.0.0 不可避免地会被安装两次
node_modules 目录结构如下图:
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
app ---> C(package-c)
app ---> D(package-d)
app ---> X("lodash@4.0.0")
C ---> Y("lodash@3.0.0")
D ---> Z("lodash@3.0.0")
可参考 npm doppelgangers(opens new window)
- Install Size,安装体积变大,浪费磁盘空间
- Build Size,构建打包体积变大,浪费带宽,网站打开延迟,破坏用户体验 (PS: 支持 Tree Shaking 会好点)
- 破坏单例模式,破坏缓存,如 postcss 的许多插件将 postcss 扔进 dependencies,重复的版本将导致解析 AST 多次
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/747.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 747(opens new window)
Author
回答者: shfshanyue(opens new window)
该观点仅对第三方库的
dependencies
有效
答: 你自己项目中所有依赖都会根据 lockfile 被锁死,但并不会依照你第三方依赖的 lockfile。
试举一例:
- 项目中依赖
react@^17.0.2
- 而
react@17.0.2
依赖object-assign@^4.1.0
在 React 自身的 yarn.lock
中版本锁定依赖如下:
react@17.0.2
└── object-assign@4.1.0 (PS: 请注意该版本号)
而在个人业务项目中 yarn.lock
中版本锁定依赖如下:
Application
└── react@17.0.2
└── object-assign@4.99.99 (PS: 请注意该版本号)
此时个人业务项目中 object-assign@4.99.99
与 React 中 object-assign@4.1.0
不符,将有可能出现问题。
此时,即使第三方库存在 lockfile
,但也有着间接依赖(如此时的 object-assign
,是第三方的依赖,个人业务项目中的依赖的依赖)不可控的问题。
可参考 next.js
的解决方案。
- 将所有依赖中的版本号在
package.json
中锁死。可见 package.json(opens new window) - 将部分依赖直接编译后直接引入,而非通过依赖的方式,如
webpack
、babel
等。可见目录 next/compiled(opens new window)
以下是一部分 package.json
{
"dependencies": {
"@babel/runtime": "7.15.4",
"@hapi/accept": "5.0.2",
"@napi-rs/triples": "1.0.3"
}
}
除了参考 next.js
直接锁死版本号方式外,还可以仍然按照 ^x.x.x
加勤加维护并时时更新 depencencies
lockfile
对于第三方库仍然必不可少。可见 react
、next.js
、webpack
均有 yarn.lock
。(PS: 可见 yarn 的受欢迎程度,另外 vue3 采用了 pnpm)
- 第三方库的
devDependencies
必须在 lockfile 中锁定,这样 Contributor 可根据 lockfile 很容易将项目跑起来。 - 第三方库的
dependencies
虽然有可能存在不可控问题,但是可通过锁死package.json
依赖或者勤加更新的方式来解决。
Author
回答者: xiyuanyuan(opens new window)
对于业务开发者而言第三方库是否锁死自己无法决定吗? 需要库的开发者自觉处理,请问大佬是这样吗
Author
回答者: shfshanyue(opens new window)
@xiyuanyuan 不对,恰好相反。我们是对于间接依赖而言的,在业务方可以锁死,但是库的开发者无法决定他们的依赖在我们业务方的锁死版本号
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/748.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 748(opens new window)
Author
回答者: shfshanyue(opens new window)
CI
,Continuous Integration,持续集成。CD
,Continuous Deployment,持续部署。
CICD
一般合称,无需特意区分二者区别。从开发、测试到上线的过程中,借助于 CICD 进行一些自动化处理,保障项目质量。
CICD
与 git 集成在一起,可理解为服务器端的 git hooks
: 当代码 push 到远程仓库后,借助 WebHooks
对当前代码在构建服务器(即 CI 服务器,也称作 Runner)中进行自动构建、测试及部署等。
它有若干好处:
- 功能分支提交后,通过 CICD 进行自动化测试、语法检查等,如未通过 CICD,则无法 CodeReview,更无法合并到生产环境分支进行上线
- 功能分支提交后,通过 CICD 检查 npm 库的风险、检查构建镜像容器的风险等
- 功能分支提交后,通过 CICD 对当前分支代码构建独立镜像并生成独立的分支环境地址进行测试,如对每一个功能分支生成一个可供测试的地址,一般是
<branch>.dev.shanyue.tech
此种地址 - 功能分支测试通过后,合并到主分支,自动构建镜像并部署到生成环境 (一般生成环境需要手动触发、自动部署)
由于近些年来 CICD 的全面介入,项目开发的工作流就是 CICD 的工作流,请看一个比较完善的 CICD Workflow。
CICD
集成于 CICD 工具及代码托管服务。CICD 有时也可理解为进行 CICD 的构建服务器,而提供 CICD 的服务,如以下产品,将会提供构建服务与 github/gitlab 集成在一起。
jenkins
Travis CI
如果你们公司没有 CICD 基础设置,那么你可以尝试 github 免费的 CICD 服务: github actions(opens new window)。
公司一般以 gitlab CI
作为 CICD 工具,此时需要自建 gitlab Runner
作为构建服务器。
每一家 CICD 产品,都有各自的配置方式,但是总体上用法差不多。以下 CI 脚本指当在 master 有代码变更时,自动部署上线。
deploy:
stage: deploy
only:
- master
script:
- docker build -t harbor.shanyue.tech/fe/devtools-app
- docker push harbor.shanyue.tech/fe/devtools-app
- helm upgrade -install devtools-app-chart .
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/749.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 749(opens new window)
Author
回答者: shfshanyue(opens new window)
使用 docker
部署前端最大的好处是隔离环境,单独管理:
- 前端项目依赖于 Node v16,而宿主机无法满足依赖,使用容器满足需求
- 前端项目依赖于 npm v8,而宿主机无法满足依赖,使用容器满足需求
- 前端项目需要将 8080 端口暴露出来,而容易与宿主机其它服务冲突,使用容器与服务发现满足需求
假设本地跑起一个前端项目,需要以下步骤,并最终可在 localhost:8080
访问服务。
$ npm i
$ npm run build
$ npm start
那在 docker 中部署前端,与在本地将如何将项目跑起来步骤大致一致,一个 Dockerfile 如下
# 指定 node 版本号,满足宿主环境
FROM node:16-alpine
# 指定工作目录,将代码添加至此
WORKDIR /code
ADD . /code
# 如何将项目跑起来
RUN npm install
RUN npm run build
CMD npm start
# 暴露出运行的端口号,可对外接入服务发现
EXPOSE 8080
此时,我们使用 docker build
构建镜像并把它跑起来。
# 构建镜像
$ docker build -t fe-app .
# 运行容器
$ docker run -it --rm fe-app
恭喜你,能够写出以上的 Dockerfile,这说明你对 Docker 已经有了理解。但其中还有若干问题,我们对其进行一波优化
- 使用
node:16
作为基础镜像过于奢侈,占用体积太大,而最终产物 (js/css/html) 无需依赖该镜像。可使用更小的 nginx 镜像做多阶段构建。 - 多个 RUN 命令,不利于 Docker 的镜像分层存储。可合并为一个 RUN 命令
- 每次都需要
npm i
,可合理利用 Docker 缓存,ADD 命令中内容发生改变将会破坏缓存。可将 package.json 提前移至目标目录,只要 package.json/lockfile 不发生变动,将不会重新npm i
优化后 Dockerfile 如下:
FROM node:16-alpine as builder
WORKDIR /code
ADD package.json package-lock.json /code/
RUN npm install
ADD . /code
RUN npm run build
# 选择更小体积的基础镜像
FROM nginx:alpine
# 将构建产物移至 nginx 中
COPY --from=builder code/build/ /usr/share/nginx/html/
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/751.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 751(opens new window)
Author
回答者: shfshanyue(opens new window)
假设我们有一个文件,称为 hello
通过 ln -s
创建一个软链接,通过 ln
可以创建一个硬链接。
$ ln -s hello hello-soft
$ ln hello hello-hard
$ ls -lh
total 768
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello-hard
45463415 lrwxr-xr-x 1 xiange staff 5B 11 19 19:40 hello-soft -> hello
他们的区别有以下几点:
- 软链接可理解为指向源文件的指针,它是单独的一个文件,仅仅只有几个字节,它拥有独立的
inode
- 硬链接与源文件同时指向一个物理地址,它与源文件共享存储数据,它俩拥有相同的
inode
它解决了 npm/yarn 平铺 node_modules 带来的依赖项重复的问题 (doppelgangers)
假设存在依赖依赖:
.
├── package-a
│ └── lodash@4.0.0
├── package-b
│ └── lodash@4.0.0
├── package-c
│ └── lodash@3.0.0
└── package-d
└── lodash@3.0.0
那么不可避免地在 npm 或者 yarn 中,lodash@3.0.0
会被多次安装,无疑造成了空间的浪费与诸多问题。
./node_modules/lodash
./node_modules/package-a
./node_modules/package-b
./node_modules/package-c
./node_modules/package-c/node_mdoules/lodash
./node_modules/package-d
./node_modules/package-d/node_mdoules/lodash
graph
app(node_modules) ---> A(package-a)
app ---> B(package-b)
app ---> C(package-c)
app ---> D(package-d)
app ---> X("lodash@4.0.0")
C ---> Y("lodash@3.0.0")
D ---> Z("lodash@3.0.0")
这里有一个来自 Rush(opens new window) 的图可以很形象的说明问题。
这是一个较为常见的场景,在平时项目中有些库相同版本甚至会安装七八次,如 postcss
、ansi-styles
、ansi-regex
、braces
等,你们可以去你们的 yarn.lock
/package-lock.json
中搜索一下。
而在 pnpm 中,它改变了 npm/yarn 的目录结构,采用软链接的方式,避免了 doppelgangers
问题更加节省空间。
它最终生成的 node_modules
如下所示,从中也可以看出它解决了幽灵依赖的问题。
./node_modules/package-a -> .pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/package-b -> .pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/package-c -> .pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/package-d -> .pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/lodash@3.0.0
./node_modules/.pnpm/lodash@4.0.0
./node_modules/.pnpm/package-a@1.0.0
./node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/.pnpm/package-a@1.0.0/node_modules/lodash -> .pnpm/package-a@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-b@1.0.0
./node_modules/.pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/.pnpm/package-b@1.0.0/node_modules/lodash -> .pnpm/package-b@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-c@1.0.0
./node_modules/.pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/.pnpm/package-c@1.0.0/node_modules/lodash -> .pnpm/package-c@1.0.0/node_modules/lodash@3.0.0
./node_modules/.pnpm/package-d@1.0.0
./node_modules/.pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/package-d@1.0.0/node_modules/lodash -> .pnpm/package-d@1.0.0/node_modules/lodash@3.0.0
如此,依赖软链接的方式,可解决重复依赖安装 (doppelgangers) 的问题,如果一个项目占用 1000 MB,那么使用 pnpm 可能仅占用 800 MB
然而它除此之外,还有一个最大的好处,如果一个项目占用 1000 MB,传统方式十个项目占用 10000 MB,那么使用 pnpm 可能仅占用 3000 MB,而它得益于硬链接。
再借用以上示例,lodash@3.0.0
与 lodash@4.0.0
会生成一个指向全局目录(~/.pnpm-store
)的硬链接,如果新项目依赖二者,则可复用存储空间。
./node_modules/.pnpm/lodash@3.0.0/node_modules/lodash -> hardlink
./node_modules/.pnpm/lodash@4.0.0/node_modules/lodash -> hardlink
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/752.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 752(opens new window)
Author
回答者: shfshanyue(opens new window)
通过 script[type=module]
,可直接在浏览器中使用原生 ESM
。这也使得前端不打包 (Bundless
) 成为可能。
<script type="module"> import lodash from "https://cdn.skypack.dev/lodash"; </script>
由于前端跑在浏览器中,因此它也只能从 URL 中引入 Package
- 绝对路径:
https://cdn.sykpack.dev/lodash
- 相对路径:
./lib.js
现在打开浏览器控制台,把以下代码粘贴在控制台中。由于 http import
的引入,你发现你调试 lodash
此列工具库更加方便了。
> lodash = await import('https://cdn.skypack.dev/lodash')
> lodash.get({ a: 3 }, 'a')
但 Http Import
每次都需要输入完全的 URL,相对以前的裸导入 (bare import specifiers
),很不太方便,如下例:
import lodash from "lodash";
它不同于 Node.JS
可以依赖系统文件系统,层层寻找 node_modules
/home/app/packages/project-a/node_modules/lodash/index.js
/home/app/packages/node_modules/lodash/index.js
/home/app/node_modules/lodash/index.js
/home/node_modules/lodash/index.js
在 ESM 中,可通过 importmap
使得裸导入可正常工作:
<script type="importmap"> {
"imports": {
"lodash": "https://cdn.skypack.dev/lodash",
"ms": "https://cdn.skypack.dev/ms"
}
} </script>
此时可与以前同样的方式进行模块导入
import lodash from 'lodash'
import("lodash").then(_ => ...)
那么通过裸导入如何导入子路径呢?
<script type="importmap"> {
"imports": {
"lodash": "https://cdn.skypack.dev/lodash",
"lodash/": "https://cdn.skypack.dev/lodash/"
}
} </script>
<script type="module"> import get from "lodash/get.js"; </script>
通过 script[type=module]
,不仅可引入 Javascript 资源,甚至可以引入 JSON/CSS,示例如下
<script type="module"> import data from "./data.json" assert { type: "json" };
console.log(data); </script>
Author
回答者: heretic-G(opens new window)
补充三点
1.module 默认是 defer 的加载和执行方式
2.这里会存在单独的 module 的域不会污染到全局
3.直接是 strict
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/753.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 753(opens new window)
Author
回答者: shfshanyue(opens new window)
本篇文章/答案本计划是三四百字,没想到最后越写越多,写了一千字。
由于 Bundless 构建工具的兴起,要求所有的模块都是 ESM 模块化格式。
目前社区有一部分模块同时支持 ESM 与 CommonJS,但仍有许多模块仅支持 CommonJS/UMD,因此将 CommonJS 转化为 ESM 是全部模块 ESM 化的过渡阶段。
在 ESM 中,导入导出有两种方式:
- 具名导出/导入:
Named Import/Export
- 默认导出/导入:
Default Import/Export
代码示例如下:
// Named export/import
export { sum };
import { sum } from "sum";
// Default export/import
export default sum;
import sum from "sum";
而在 CommonJS 中,导入导出的方法只有一种:
module.exports = sum;
而所谓的 exports
仅仅是 module.exports
的引用而已
// 实际上的 exports
exports = module.exports;
// 以下两个是等价的
exports.a = 3;
module.exports.a = 3;
PS: 一道题关于
exports
与module.exports
的区别,以下console.log
输出什么// hello.js exports.a = 3; module.exports.b = 4; // index.js const hello = require("./hello"); console.log(hello);
再来一道题:
// hello.js exports.a = 3; module.exports = { b: 4 }; // index.js const hello = require("./hello"); console.log(hello);
正因为有二者的不同,因此在二者转换的时候有一些兼容问题需要解决。
正因为,二者有所不同,当 exports 转化时,既要转化为 export {}
,又要转化为 export default {}
// Input: index.cjs
exports.a = 3;
// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export const a = 3;
export default { a };
如果仅仅转为 export const a = 3
的具名导出,而不转换 export default { a }
,将会出现什么问题?以下为例:
// Input: CJS
exports.a = 3; // index.cjs
const o = require("."); // foo.cjs
console.log(o.a); // foo.cjs
// Output: ESM
// 这是有问题的错误转换示例:
// 此处 a 应该再 export default { a } 一次
export const a = 3; // index.mjs
import o from "."; // foo.mjs
console.log(o.a); // foo.mjs 这里有问题,这里有问题,这里有问题
对于 module.exports
,我们可以遍历其中的 key (通过 AST),将 key 转化为 Named Export
,将 module.exports
转化为 Default Export
// Input: index.cjs
module.exports = {
a: 3,
b: 4,
};
// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export default {
a: 3,
b: 4,
};
export const a = 3;
export const b = 4;
如果 module.exports
导出的是函数如何处理呢,特别是 exports
与 module.exports
的程序逻辑混合在一起?
以下是一个正确的转换结果:
// Input: index.cjs
module.exports = () => {}
exports.a = 3
exports.b = 4
// Output: index.mjs
const sum = () => {}
sum.a = 3
sum.b = 4
export const a = 3
export const b = 4
export default = sum
也可以这么处理,将 module.exports
与 exports
的代码使用函数包裹起来,此时我们无需关心其中的逻辑细节。
var esm$1 = { exports: {} };
(function (module, exports) {
module.exports = () => {};
exports.a = 3;
exports.b = 4;
})(esm$1, esm$1.exports);
var esm = esm$1.exports;
export { esm as default };
ESM 与 CommonJS 不仅仅是简单的语法上的不同,它们在思维方式上就完全不同,因此还有一些较为复杂的转换,本篇先不做谈论,感兴趣的可以去我的博客上查找相关文章。
- 如何处理
__dirname
- 如何处理
require(dynamicString)
- 如何处理 CommonJS 中的编程逻辑,如下
以下代码涉及到编程逻辑,由于 exports
是一个动态的 Javascript 对象,而它自然可以使用两次,那应该如何正确编译为 ESM 呢?
// input: index.cjs
exports.sum = 0;
Promise.resolve().then(() => {
exports.sum = 100;
});
以下是一种不会出问题的代码转换结果
// output: index.mjs
const _default = {};
let sum = (_default.sum = 0);
Promise.resolve().then(() => {
sum = _default.sum = 100;
});
export default _default;
export { sum };
CommonJS 向 ESM 转化,自然有构建工具的参与,比如
甚至把一些 CommonJS 库转化为 ESM,并且置于 CDN 中,使得我们可以直接使用,而无需构建工具参与
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/754.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 754(opens new window)
Author
回答者: shfshanyue(opens new window)
在发布公共 package 之前,需要在 npm 官网(opens new window)进行注册一个账号。
随后,在本地(需要发包的地方)执行命令 npm login
,进行交互式操作并且登录。
$ npm login
发布一个 npm 包之前,填写 package.json
中以下三项最重要的字段。假设此时包的名称为 @shanyue/just-demo
{
name: '@shanyue/just-demo',
version: '1.0.0',
main: './index.js',
}
之后执行 npm publish
发包即可。
$ npm publish
一旦发布完成,在任意地方通过 npm i
均可依赖该包。
const x = require("@shanyue/just-demo");
console.log(x);
如若该包进行更新后,需要再次发包,可 npm version
控制该版本进行升级,记住需要遵守 Semver 规范(opens new window)
# 增加一个修复版本号: 1.0.1 -> 1.0.2 (自动更改 package.json 中的 version 字段)
$ npm version patch
# 增加一个小的版本号: 1.0.1 -> 1.1.0 (自动更改 package.json 中的 version 字段)
$ npm version minor
# 将更新后的包发布到 npm 中
$ npm publish
在 npm 发包时,实际发包内容为 package.json
中 files
字段,一般只需将构建后资源(如果需要构建)进行发包,源文件可发可不发。
{
files: ["dist"];
}
若需要查看一个 package 的发包内容,可直接在 node_modules/${package}
进行查看,将会发现它和源码有很大不同。也可以在 CDN 中进行查看,以 React 为例
- jsdelivr: https://cdn.jsdelivr.net/npm/react/(opens new window)
- unpkg: https://unpkg.com/browse/react/(opens new window)
npm publish
将自动走过以下生命周期
- prepublishOnly: 如果发包之前需要构建,可以放在这里执行
- prepack
- prepare: 如果发包之前需要构建,可以放在这里执行 (该周期也会在 npm i 后自动执行)
- postpack
- publish
- postpublish
发包实际上是将本地 package 中的所有资源进行打包,并上传到 npm 的一个过程。你可以通过 npm pack
命令查看详情
$ npm pack
npm notice
npm notice 📦 midash@0.2.6
npm notice === Tarball Contents ===
npm notice 1.1kB LICENSE
npm notice 812B README.md
npm notice 5.7kB dist/midash.cjs.development.js
npm notice 13.4kB dist/midash.cjs.development.js.map
npm notice 3.2kB dist/midash.cjs.production.min.js
npm notice 10.5kB dist/midash.cjs.production.min.js.map
npm notice 5.3kB dist/midash.esm.js
npm notice 13.4kB dist/midash.esm.js.map
npm notice 176B dist/omit.d.ts
......
npm notice === Tarball Details ===
npm notice name: midash
npm notice version: 0.2.6
npm notice filename: midash-0.2.6.tgz
npm notice package size: 11.5 kB
npm notice unpacked size: 67.8 kB
npm notice shasum: c89d8c1aa96f78ce8b1dcf8f0f058fa7a6936a6a
npm notice integrity: sha512-lyx8khPVkCHvH[...]kBL6K6VqOG6dQ==
npm notice total files: 46
npm notice
midash-0.2.6.tgz
当你发包成功后,也可以前往 npm devtool(opens new window) 查看各项数据。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/756.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 756(opens new window)
Author
回答者: shfshanyue(opens new window)
AST
是 Abstract Syntax Tree
的简称,是前端工程化绕不过的一个名词。它涉及到工程化诸多环节的应用,比如:
- 如何将 Typescript 转化为 Javascript (typescript)
- 如何将 SASS/LESS 转化为 CSS (sass/less)
- 如何将 ES6+ 转化为 ES5 (babel)
- 如何将 Javascript 代码进行格式化 (eslint/prettier)
- 如何识别 React 项目中的 JSX (babel)
- GraphQL、MDX、Vue SFC 等等
而在语言转换的过程中,实质上就是对其 AST 的操作,核心步骤就是 AST 三步走
- Code -> AST (Parse)
- AST -> AST (Transform)
- AST -> Code (Generate)
以下是一段代码,及其对应的 AST
// Code
const a = 4
// AST
{
"type": "Program",
"start": 0,
"end": 11,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 4,
"raw": "4"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
不同的语言拥有不同的解析器,比如 Javascript 的解析器和 CSS 的解析器就完全不同。
对相同的语言,也存在诸多的解析器,也就会生成多种 AST,如 babel
与 espree
。
在 AST Explorer(opens new window) 中,列举了诸多语言的解析器(Parser),及转化器(Transformer)。
AST 的生成这一步骤被称为解析(Parser),而该步骤也有两个阶段: 词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)
词法分析用以将代码转化为 Token
流,维护一个关于 Token 的数组
// Code
a = 3
// Token
[
{ type: { ... }, value: "a", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "=", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "3", start: 4, end: 5, loc: { ... } },
...
]
词法分析后的 Token 流也有诸多应用,如:
- 代码检查,如 eslint 判断是否以分号结尾,判断是否含有分号的 token
- 语法高亮,如 highlight/prism 使之代码高亮
- 模板语法,如 ejs 等模板也离不开
语法分析将 Token 流转化为结构化的 AST,方便操作
{
"type": "Program",
"start": 0,
"end": 5,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 5,
"expression": {
"type": "AssignmentExpression",
"start": 0,
"end": 5,
"operator": "=",
"left": {
"type": "Identifier",
"start": 0,
"end": 1,
"name": "a"
},
"right": {
"type": "Literal",
"start": 4,
"end": 5,
"value": 3,
"raw": "3"
}
}
}
],
"sourceType": "module"
}
可通过自己写一个解析器,将语言 (DSL) 解析为 AST 进行练手,以下两个示例是不错的选择
- 解析简单的 HTML 为 AST
- 解析 Marktodwn List 为 AST
或可参考一个最简编译器的实现 the super tiny compiler(opens new window)
Author
回答者: wenhui7788(opens new window)
你好,请问下 token 流 怎么理解这个名词?因为通常理解的 token 就是一个唯一的字符串,流,一般想到的是什么文件流什么的。一些什么序列化相关的,而 token 流说是一个数组,那就是说是由很多字符串组成的一个数组吗?为什么不直接说是一个数组反而要说是 token 流,为什么要提到 token 以及流(有点咬文嚼字了:( ),谢谢~~
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/757.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 757(opens new window)
Author
回答者: YaoHuangMark(opens new window)
browserslist 是在不同的前端工具之间共用目标浏览器和 node 版本的配置工具。 相当于给 Babel、PostCSS、ESLint、StyleLint 等这些前端工具预设一个浏览器支持范围,这些工具转换或检查代码时会参考这个范围。
Author
回答者: shfshanyue(opens new window)
browserslist(opens new window) 用特定的语句来查询浏览器列表,如 last 2 Chrome versions
。
$ npx browserslist "last 2 Chrome versions"
chrome 100
chrome 99
细说起来,它是现代前端工程化不可或缺的工具,无论是处理 JS 的 babel
,还是处理 CSS 的 postcss
,凡是与垫片相关的,他们背后都有 browserslist
的身影。
babel
,在@babel/preset-env
中使用core-js
作为垫片postcss
使用autoprefixer
作为垫片
关于前端打包体积与垫片关系,我们有以下几点共识:
- 由于低浏览器版本的存在,垫片是必不可少的
- 垫片越少,则打包体积越小
- 浏览器版本越新,则垫片越少
那在前端工程化实践中,当我们确认了浏览器版本号,那么它的垫片体积就会确认。
假设项目只需要支持最新的两个谷歌浏览器。那么关于 browserslist
的查询,可以写作 last 2 Chrome versions
。
而随着时间的推移,该查询语句将会返回更新的浏览器,垫片体积便会减小。
如使用以上查询语句,一年前可能还需要 Promise.any
的垫片,但目前肯定不需要了。
最终,谈一下 browserslist
的原理: browserslist
根据正则解析查询语句,对浏览器版本数据库 caniuse-lite
进行查询,返回所得的浏览器版本列表。
PS:
caniuse-lite
这个库也由browserslist
团队进行维护,它是基于 caniuse(opens new window) 的数据库进行的数据整合。
因为 browserslist
并不维护数据库,因此它会经常提醒你去更新 caniuse-lite
这个库,由于 lock 文件的存在,因此需要使用以下命令手动更新数据库。
$ npx browserslist@latest --update-db
该命令将会对 caniuse-lite 进行升级,可体现在 lock 文件中。
"caniuse-lite": { - "version": "1.0.30001265", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", - "integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==", + "version": "1.0.30001332", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz", + "integrity": "sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==", "dev": true
},
> 5%
: 在全球用户份额大于5%
的浏览器> 5% in CN
: 在中国用户份额大于5%
的浏览器
last 2 versions
: 所有浏览器的最新两个版本last 2 Chrome versions
: Chrome 浏览器的最新两个版本
dead
: 官方不在维护已过两年,比如IE10
Chrome > 90
: Chrome 大于 90 版本号的浏览器
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/758.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 758(opens new window)
Author
Bundleless 的优势。 1.项目启动快。因为不需要过多的打包,只需要处理修改后的单个文件,所以响应速度是 O(1) 级别,刷新即可即时生效,速度很快。 2.浏览器加载块。利用浏览器自主加载的特性,跳过打包的过程。 3.本地文件更新,重新请求单个文件。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/759.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 759(opens new window)
Author
npm 会把所有下载的包,保存在用户文件夹下面。
默认值:~/.npm 在 Posix 上 或 %AppData%/npm-cache 在 Windows 上。根缓存文件夹。
npm install 之后会计算每个包的 sha1 值(PS:安全散列算法(Secure Hash Algorithm)),然后将包与他的 sha1 值关联保存在 package-lock.json 里面,下次 npm install 时,会根据 package-lock.json 里面保存的 sha1 值去文件夹里面寻找包文件,如果找到就不用从新下载安装了。
npm cache verify
上面这个命令是重新计算,磁盘文件是否与 sha1 值匹配,如果不匹配可能删除。
要对现有缓存内容运行脱机验证,请使用
npm cache verify
。
npm cache clean --force
上面这个命令是删除磁盘所有缓存文件。
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/760.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 760(opens new window)
Author
回答者: shfshanyue(opens new window)
假设 lodash
有一个 Bug,影响线上开发,应该怎么办?
答: 三步走。
- 在 Github 提交 Pull Request,修复 Bug,等待合并
- 合并 PR 后,等待新版本发包
- 升级项目中的 lodash 依赖
很合理很规范的一个流程,但是它一个最大的问题就是,太慢了,三步走完黄花菜都凉了。
此时可直接上手修改 node_modules
中 lodash 代码,并修复问题!
新问题:node_modules
未纳入版本管理,在生产环境并没有用。请看流程
- 本地修改
node_modules/lodash
,本地正常运行 ✅ - 线上
npm i lodash
,lodash 未被修改,线上运行失败 ❌
此时有一个简单的方案,临时将修复文件纳入工作目录,可以解决这个问题
- 本地修改
node_modules/lodash
,本地正常运行 ✅ - 将修改文件复制到
${work_dir}/patchs/lodash
中,纳入版本管理 - 线上
npm i lodash
,并将修改文件再度复制到node_modules/lodash
中,线上正常运行 ✅
但此时并不是很智能,且略有小问题,演示如下:
- 本地修改
node_modules/lodash
,本地正常运行 ✅ - 将修改文件复制到
${work_dir}/patchs/lodash
中,纳入版本管理 ✅ - 线上
npm i lodash
,并将修改文件再度复制到node_modules/lodash
中,线上正常运行 ✅ - 两个月后升级
lodash
,该问题得以解决,而我们代码引用了 lodash 的新特性 - 线上
npm i lodash
,并将修改文件再度复制到node_modules/lodash
中,由于已更新了 lodash,并且依赖于新特性,线上运行失败 ❌
此时有一个万能之策,那就是 patch-package(opens new window)
想要知道 patch-package
如何解决上述问题,请先了解下它的用法,流程如下
# 修改 lodash 的一个小问题
$ vim node_modules/lodash/index.js
# 对 lodash 的修复生成一个 patch 文件,位于 patches/lodash+4.17.21.patch
$ npx patch-package lodash
# 将修复文件提交到版本管理之中
$ git add patches/lodash+4.17.21.patch
$ git commit -m "fix 一点儿小事 in lodash"
# 此后的命令在生产环境或 CI 中执行
# 此后的命令在生产环境或 CI 中执行
# 此后的命令在生产环境或 CI 中执行
# 在生产环境装包
$ npm i
# 为生产环境的 lodash 进行小修复
$ npx patch-package
# 大功告成!
再次看下 patch-package
自动生成 patch 文件的本来面目吧:
它实际上是一个 diff
文件,在生产环境中可自动根据 diff 文件与版本号 (根据 patch 文件名存取) 将修复场景复原!
$ cat patches/lodash+4.17.21.patch
diff --git a/node_modules/lodash/index.js b/node_modules/lodash/index.js
index 5d063e2..fc6fa33 100644
--- a/node_modules/lodash/index.js
+++ b/node_modules/lodash/index.js
@@ -1 +1,3 @@
+console.log('DEBUG SOMETHING')
+
module.exports = require('./lodash');
\ No newline at end of file
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/761.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 761(opens new window)
Author
回答者: shfshanyue(opens new window)
为什么需要进行分包,一个大的 bundle.js
不好吗?
极其不建议,可从两方面进行考虑:
- 一行代码将导致整个
bundle.js
的缓存失效 - 一个页面仅仅需要
bundle.js
中 1/N 的代码,剩下代码属于其它页面,完全没有必要加载
webpack(或其他构建工具) 运行时代码不容易变更,需要单独抽离出来,比如
webpack.runtime.js
。由于其体积小,必要时可注入index.html
中,减少 HTTP 请求数,优化关键请求路径
React(Vue) 运行时代码不容易变更,且每个组件都会依赖它,可单独抽离出来
framework.runtime.js
。请且注意,务必将 React 及其所有依赖(react-dom/object-assign)共同抽离出来,否则有可能造成性能损耗,见下示例
假设仅仅抽离 React 运行时(不包含其依赖)为单独 Chunk,且每个路由页面为单独 Chunk。某页面不依赖任何第三方库,则该页面会加载以下 Chunk
webpack.runtime.js
5KB ✅framework.runtime.js
30KB ✅page-a.chunk.js
50KB ✅vendor.chunk.js
50KB ❌ (因 webpack 依赖其object-assign
,而object-assign
将被打入共同依赖vendor.chunk.js
,因此此时它必回加载,但是该页面并不依赖任何第三方库,完全没有必要全部加载vendor.chunk.js
)
将 React 运行时及其所有依赖,共同打包,修复结果如下,拥有了更完美的打包方案。
webpack.runtime.js
5KB ✅framework.runtime.js
40KB ✅ (+10KB)page-a.chunk.js
50KB ✅
一个模块被 N(2 个以上) 个 Chunk 引用,可称为公共模块,可把公共模块给抽离出来,形成 vendor.js
。
问:那如果一个模块被用了多次 (2 次以上),但是该模块体积过大(1MB),每个页面都会加载它(但是无必要,因为不是每个页面都依赖它),导致性能变差,此时如何分包?
答:如果一个模块虽是公共模块,但是该模块体积过大,可直接 import()
引入,异步加载,单独分包,比如 echarts
等
问:如果公共模块数量多,导致 vendor.js 体积过大(1MB),每个页面都会加载它,导致性能变差,此时如何分包
答:有以下两个思路
- 思路一: 可对 vendor.js 改变策略,比如被引用了十次以上,被当做公共模块抽离成 verdor-A.js,五次的抽离为 vendor-B.js,两次的抽离为 vendor-C.js
- 思路二: 控制 vendor.js 的体积,当大于 100KB 时,再次进行分包,多分几个 vendor-XXX.js,但每个 vendor.js 都不超过 100KB
在 webpack 中可以使用 SplitChunksPlugin(opens new window) 进行分包,它需要满足三个条件:
- minChunks: 一个模块是否最少被 minChunks 个 chunk 所引用
- maxInitialRequests/maxAsyncRequests: 最多只能有 maxInitialRequests/maxAsyncRequests 个 chunk 需要同时加载 (如一个 Chunk 依赖 VendorChunk 才可正常工作,此时同时加载 chunk 数为 2)
- minSize/maxSize: chunk 的体积必须介于 (minSize, maxSize) 之间
以下是 next.js
的默认配置,可视作最佳实践
{
// Keep main and _app chunks unsplitted in webpack 5
// as we don't need a separate vendor chunk from that
// and all other chunk depend on them so there is no
// duplication that need to be pulled out.
chunks: (chunk) =>
!/^(polyfills|main|pages\/_app)$/.test(chunk.name) &&
!MIDDLEWARE_ROUTE.test(chunk.name),
cacheGroups: {
framework: {
chunks: (chunk: webpack.compilation.Chunk) =>
!chunk.name?.match(MIDDLEWARE_ROUTE),
name: 'framework',
test(module) {
const resource =
module.nameForCondition && module.nameForCondition()
if (!resource) {
return false
}
return topLevelFrameworkPaths.some((packagePath) =>
resource.startsWith(packagePath)
)
},
priority: 40,
// Don't let webpack eliminate this chunk (prevents this chunk from
// becoming a part of the commons chunk)
enforce: true,
},
lib: {
test(module: {
size: Function
nameForCondition: Function
}): boolean {
return (
module.size() > 160000 &&
/node_modules[/\\]/.test(module.nameForCondition() || '')
)
},
name(module: {
type: string
libIdent?: Function
updateHash: (hash: crypto.Hash) => void
}): string {
const hash = crypto.createHash('sha1')
if (isModuleCSS(module)) {
module.updateHash(hash)
} else {
if (!module.libIdent) {
throw new Error(
`Encountered unknown module type: ${module.type}. Please open an issue.`
)
}
hash.update(module.libIdent({ context: dir }))
}
return hash.digest('hex').substring(0, 8)
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true,
},
commons: {
name: 'commons',
minChunks: totalPages,
priority: 20,
},
middleware: {
chunks: (chunk: webpack.compilation.Chunk) =>
chunk.name?.match(MIDDLEWARE_ROUTE),
filename: 'server/middleware-chunks/[name].js',
minChunks: 2,
enforce: true,
},
},
maxInitialRequests: 25,
minSize: 20000,
}
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/762.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 762(opens new window)
Author
回答者: AndyTiTi(opens new window)
以下是基于 gitlab 的分支和 tag 进行前端部署的.gitlab-ci.yml 配置
image: node:12-alpine3.14
stages: # 分段
# - install
- build
- deploy
- clear
cache: # 缓存
paths:
- node_modules
job_install:
tags:
- test
stage: build
script:
- npm install -g cnpm --registry=https://registry.npm.taobao.org
- cnpm install
- npm run build
# 只在指定dev分支或者tag以 dev_ 开头的标签执行该job
only:
refs:
- dev
- /^dev_[0-9]+(?:.[0-9]+)+$/ # regular expression
# 打包后的文件可以在gitlab上直接下载
artifacts:
name: "dist"
paths:
- dist
job_deploy:
image: docker
stage: deploy
environment:
name: test
url: http://172.6.6.6:8000
script:
- docker build -t appimages .
- if [ $(docker ps -aq --filter name=app-container) ]; then docker rm -f app-container;fi
- docker run -d -p 8082:80 --name app-container appimages
job_clear:
image: docker
stage: clear
tags:
- test
script:
- if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker stop $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
- if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker rm $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
- if [ $(docker images | grep "none" | awk '{print $3}') ]; then docker rmi $(docker images | grep "none" | awk '{print $3}');fi
原文:https://q.shanyue.tech/fe/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/772.html
Issue
欢迎在 Gtihub Issue 中回答此问题: Issue 772(opens new window)