工程化知识小结
工具知识篇
npm与package
- package指拥有package.json文件的一个文件夹,package的属性就是这个文件的内容
- 由于包的name唯一,所以我们可以通过固定的URL访问其仓库页面 https://www.npmjs.com/package/package-name 还可以通过docs命令快速打开其对应文档: Npm docs lodash
- 由于发布后的包通常都是经过构建后的,如果想了解一个包的真实目录可以通过访问其提供的Github仓库地址,或者是在一些CDN上进行定位:unpkg 、 jsdelivr
找到某个包的文档?
Npm docs lodash
找到某个包的Github地址?
Npm repo lodash
什么是semver
语义化版本规范,由major、minor、patch组成 ,对应的是破坏性更新、向后兼容的新功能更新、Bug修复的版本,如Vue中:
2.7 到 3.0 就是破坏性更新 一些大型包中,还会使用 prerelease版本号 npm view vue versions 可以看Vue历史的发布版本
版本比较
~ 锁定的是minor版本, 故1.2.3为基时,安装的是 <1.3.0的版本 ^锁定的是major版本, 故1.2.3为基时,安装的是 <2.0.0的版本
Npm i 时,默认是 按照 ^ 来安装
如果项目中需要使用到完全不同版本的vue,应如何处理
使用别名安装
Npm install vue3@npm:vue@3 Npm install vue2@npm:vue@2
什么是install size
指安装一个包时候的真正体积,它的计算时按照包自身以及包自身的依赖和间接依赖的总体积
什么是零依赖npm包?
即除了自身代码,没有使用其他依赖的包,即dependencies 字段下是空的
optionaldependencies
意味着这个包是可选的,在包属性中,一般会和os字段使用,代表包只在特定环境下生效,cpu字段指特定的cpu架构
{
"optionalDependencies": {
"fsevents": "~2.1.2"
}
}
Peerdependencies的安装策略
对等依赖,项目中不存在时,进行安装,如果存在但版本对不上则会提示警告
什么是 npm scripts?
项目中用于定义脚本的方法,通过npm run command执行,它可以帮我们通过命令自动化执行常见的任务,也可以快速开发构建部署项目。 通过package.json中 script字段定义. 默认的scripts有: install 依赖安装, start启动服务, test 测试项目
如何配置环境变量
在脚本命令中 通过 NODE_ENV=production command或者NODE_ENV=development command读取环境变量。windwos下需要借助cross-env这个包,此时命令要变为 cross-env NODE_ENV=production command
pre/post scripts注意点
Npm 与 yarn 无异,但在pnpm中pre post钩子会失效,这是pnpm出于安全考虑,防止一些注入或者黑客操作
Npm pack 做了什么事情
将当前包打包为tarball,它就是实际上传到npm的内容,npm pack时 会自动计算integrity信息,用于npm i时信息校验
Npm prepare在哪个阶段执行
Npm publish之前执行 , Npm install之后执行,有时会用 prepare代替postinstall
Npm publish做了什么事情
执行npm pack,然后将打包后的包 上传到 npm仓库。它有一些相关的钩子,重点就是 prepublishOnly和prepare
如果项目中没有 lockfile,将会出现什么问题
比如第一次我们安装的是1.2.0的一个包,过了一段时间之后,此时包升级到了1.15.0, 但由于没有遵守规范增加了破坏性更新,此时如果我们再另外一个环境去install时,就会安装这个1.15的包,由于破坏性更新导致项目运行出错
项目中拥有 lockfile,与在 package.json 中直接写死版本号有何区别
锁文件为每个依赖及其间接依赖也指定了版本、位置、完整性哈希这是重点。以及如果我们删除了node_modules重新install速度也会更快,且安装位置不变
sideeffects字段作用
指定包是否具有副作用,副作用指 import时会执行一些副作用操作,比如修改全局变量。 当为false时,会触发打包器的 tree shaking优化,将一些无用模块安全的删除
main/exports/module
- 当我们使用某个包导入文件时候,实际我们找到的就是这些字段 main/module/exports
- main是作为commonjs时代的入口字段,最常用的入口文件字段
- module则是随着ESM规范以及打包的兴起使用的字段,如果是import导入 则优先使用这个字段寻找,当你的包只有esm的打包文件,则直接使用main字段
- exports更灵活,你可以根据运行环境,打包规范,甚至是目录的方式导入,只要再包属性中定义好,且它的优先级更高
{
"type": "module",
"exports": {
"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",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
}
}
npm i -g 原理
将包安装到 全局目录下(用户目录),通过库中bin字段,将对应的命令通过符号链接 挂载到PATH路径,对应的脚本文件添加 可执行权限
resolve算法
引入模块时,会在当前路径的node_modules 寻找对应的package,如果找不到则递归上级目录的node_modules,直到根路径,如 当前node_modules >项目 node_modules > 全局node_modules
如何使用 corepack
从node16.9后的版本 会以实验性工具内置 corepack,并全局支持命令执行,我们可以通过corepack命令去使用它,如
corepack enable 激活 yarn pnpm 无需安装 corepack pnpm --version 查看pnpm版本
如何确定使用的是什么包管理器
通过锁文件确定,不同包管理器锁文件不一样: pnpm-lock.yaml、yarn.lock、package-lock.json 如果存在多个锁文件,则通过以下方法对比:
- 比较锁文件的修改时间
- 是否有CI CD, 跟着CI CD的包管理器工具
- 是否有DockerFile, 存在则以DockerFile为准
- 是否有文档,以文档为准
- 找同事或者领导确认
什么是 npx/pnpx?原理?
用于执行项目中的命令,不存在则安装它,它们是一样的,在yarn下则是 yarn dlx command 它的原理是:
- 直接执行 node_modules/.bin 下的可执行命令,如果不存在,则递归往上寻找,最后则在全局目录下寻找,不存在则进行下载。
- 对于npx,如果包不存在则下载到全局的 .npm/._npx 目录下,而不会下载全局目录下污染 软链接的Path变量,因此npx serve后,全局执行 serve仍然报错,pnpx与npx不同的是,pnpx不会在全局目录下去寻找包
npm link原理? 如何更好的去调试编译后的包?
- 以rollup为例, 在rollup目录中,执行 npm link,会根据package.json中的name创建一个软链接, 然后再我们需要进行调试的项目中 执行 npm link rollup, 将会替换 node_modules/rollup 将其软链接到 rollup源码中(如果是yarn link则会软链接到全局下的 yarn/link/rollup. Pnpm npm则不会多这一步操作)
- 经过编译后的代码一般都会被压缩混淆难以阅读,且sourceMap也没有,此时我们可以通过 npm link 软链接的形式进行替换再进行调试
目前npm的node_modules拓扑结构?有什么问题?
- 平铺结构
- 重复依赖,如node_modules中假设存在 a包和b包, 如果它们分别依赖了确定版本或者版本major存在差异的 lodash3.2,lodash3.1, 此时安装时,先扫描a包,然后平铺结构会将 lodash3.2 提升,然后再遇到b包时,lodash3.1则不会被提升,并被安装到 b包的node_modules
什么是幽灵依赖?
假设一个A包 使用了 B包,但在下一次版本更新后 自身实现了B包的功能,然后移除了B包的依赖, 而我们的项目又因为依赖A包项目中的 B包, 则会导致报错
软链接和硬链接
软链接:指向源文件的的指针,是一个单独文件,只有几个字节,拥有独立的inode 硬链接:与源文件指向同一个物理地址,与源文件共享数据,和源文件拥有相同的inode
pnpm 为何节省资源
- 与平铺结构不同,它使用软链接的方式解决了重复依赖的问题,且会把项目中的依赖如 lodash3通过硬链接的方式链接到全局目录下(windows下则是根据项目所在的位置提升到磁盘根目录)的.pnpm-store
- 其他项目如果也使用lodash3这个包则可以直接复用同一个存储空间
pnpm 是如何解决幽灵依赖以及重复依赖安装的问题
pnpm将直接依赖置于 node_modules中,将间接依赖置于 node_modules/.pnpm中, 通过软链接 解决 重复依赖的问题,同时也解决了幽灵依赖的问题, 不在package.json中声明的包,是不会置于node_modules中,不会在项目中被引入
pnpm是如何解决eslint中幽灵依赖的问题?
module.exports = {
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@next/next/recommended',
]
}
- eslint中 由于其约定的命名规则配置,许多脚手架都会进行独立发包, 如 @vue/eslint-config-typescript, 执行 eslint . 去校验规则时, 它自身的 eslint配置内又会依赖其他包, 并由于eslint的命名规则 就会导致幽灵依赖.
- public-hoist-pattern是pnpm中用来忽略某些依赖特定的幽灵依赖, 默认是 eslint和prettier。 它会取消其幽灵依赖,让相关的依赖生效。而不是被作为幽灵依赖在安装包时被忽略
.npmrn的作用
配置包中的一些规范
registry=https://registry.npmmirror.com 源
save-prefix='~' 安装包时的范围
engine-strict=true 检验engines字段,不符合node版本不安装
fetch-retries=2 拉取包失败时的重试次数
如何修改仓库的源
- 修改npmrc文件
- npm config set registry cnpm源
npm ci的作用?什么情况下会失败?如何判断CI环境?
- 相比于
npm i
,更安全,更快速,针对CI环境做了优化。 - 只修改了package.json的版本,但并未在开发环境npm i 重新安装,此时生产环境下/CI环境 npm ci 会直接失败
- 使用包:ci-info
exports 与 module.exports 有何区别
exports是 CJS的规范,而在Node和Webpack中对于CJS模块的代码 都会使用一个包裹函数处理 实际上如果同时使用这两个导出,最终导出会以 module.exports为准
(function(exports, require, module, __filename, __dirname) {
// 以下一行是传参时确认的,但写在这里方便理解
exports = module.exports
// Module code actually lives in here
});
npm update/dedupe
- Npm update会将 package.json/package-lock.json 中的依赖升级到 符合范围内的最新版本。 但可能还是会存在重复依赖的问题
- 此时可以使用 npm dedupe,它会将符合版本范围的包升级为一个共同版本的包(pnpm install 自带pnpm dedupe的过程)
性能优化篇
TerserJS
一个用于压缩混淆JS代码的库,相比于uglify 对ES6有更好的支持。 而由于JS压缩效率不理想,因此出现了由RUST重写的 库swc,与terser拥有一样的 API
我们可以通过什么策略对 JavaScript 代码进行压缩
- 去除空格 换行 注释
- 压缩变量名 函数名 属性名
- 合并变量声明,简化逻辑布尔值
- 预计算编译,如常量和值, 函数输出简化
Terser options
https://www.webpackjs.com/configuration/optimization/#optimizationminimize
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {//...
optimization: {
minimize: false,
minimizer:[
new TerserPlugin({
parallel:true,
terserOptions:{
ecma: undefined,
parse: {},
compress: {},
mangle: true, // Note `mangle.properties` is `false` by default.
module: false,
// Deprecated
output: null,
format: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
safari10: false,
}
})
]
},
};
什么是垫片?Corejs?
包含了所有ES6+的polyfill,用于兼容浏览器不支持的API, 并且集成在了主流的包中如@bable/preset-env
browserslist 与垫片体积
像babel/preset-env 背后的corejs垫片,会受browserlist字段 通过 数据查询出来的 支持率 决定最终打包大小,新的浏览器所需的垫片则更少,打包体积更小 browserslist会根据caniuse-lite 这个库查询 查询覆盖率:https://browsersl.ist/
Tree Shaking 的原理是什么
不同于Require动态引入,基于 ES module的静态引入 是可以被分析的,通过AST将用不到的代码进行移除,减小打包体积
有哪几种图片文件格式,其体积如何
相同质量图片体积:jpeg 》 png 》webp 》 avif(但支持率不高)
当你们网站中使用 avif 优化图片时,如果浏览器不支持 avif 如何处理
使用picture标签,可以降级处理
<picture>
<source srcset="shanyue-hello.avif" type="image/avif">
<source srcset="shanyue-hello.webp" type="image/webp">
<img src="shanyue-hello.jpeg">
</picture>
在前端项目中,如何自动化处理图片
编译时,webpack中,借助 imagemin-webpack-plugin 、 imagemin-webp-webpack-plugin 去处理, 借助url-loader 优化小图片为base64等
仅使用 canvas 优化图片,有哪些局限
仅能对 图片的宽高做裁剪,且当指定格式为jpeg或者webp的情况下,使用toDataURL第二个参数对图片质量进行压缩
编译时与运行时优化图片的区别在哪里
这里的运行时优化主要指借助图片处理服务器 ,即OSS存储服务
- 无域名图片支持,域外图片不受编译控制
- 无宽高优化,原图1000,浏览器仅渲染100
- 无响应式图片
什么是 DataURL
将小图片转为DataURL,可以减少HTTP请求。DataURL由四个部分组成,前缀、MIME类型、标记、数据, 它不仅是图片,也可以代表其他类型,只不过图片音频等二进制数据则必须由base64表示
小图片优化策略是什么
转为DataURL,减少图片内联,即减少HTTP请求达到优化的效果
在Webpack中处理小图片:
{
module: {
rules: [
{
test: /\.(bmp|jpe?g|png|gif)/,
type: 'asset',
parser: {
// 对小于 4kb 的图片进行 DataURL 处理
dataUrlCondition: {
maxSize: 4 * 1024
}
}
}
]
},
};
svg
对于svg则需要借助另外一个包,不然直接压缩会导致原本的体积增大,因为svg不同于二进制图片,属于文本图片,文本不需要base64处理
const path = require('path');
const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
return svgToMiniDataURI(content);
}
}
}
]
},
};
svgo
在之前的基础上, 增加svgo对其进行更进一步的压缩
const path = require('path');
const svgToMiniDataURI = require('mini-svg-data-uri');
const { optimize } = require('svgo');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
// svgo 的核心 API,进行 svg 压缩
const result = optimize(content);
return svgToMiniDataURI(result.data);
}
}
}
]
},
};