Skip to content

工程化知识小结

工具知识篇

npm与package

  1. package指拥有package.json文件的一个文件夹,package的属性就是这个文件的内容
  2. 由于包的name唯一,所以我们可以通过固定的URL访问其仓库页面 https://www.npmjs.com/package/package-name 还可以通过docs命令快速打开其对应文档: Npm docs lodash
  3. 由于发布后的包通常都是经过构建后的,如果想了解一个包的真实目录可以通过访问其提供的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架构

json
{
  "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

  1. 当我们使用某个包导入文件时候,实际我们找到的就是这些字段 main/module/exports
  2. main是作为commonjs时代的入口字段,最常用的入口文件字段
  3. module则是随着ESM规范以及打包的兴起使用的字段,如果是import导入 则优先使用这个字段寻找,当你的包只有esm的打包文件,则直接使用main字段
  4. exports更灵活,你可以根据运行环境,打包规范,甚至是目录的方式导入,只要再包属性中定义好,且它的优先级更高
json
{
  "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 如果存在多个锁文件,则通过以下方法对比:

  1. 比较锁文件的修改时间
  2. 是否有CI CD, 跟着CI CD的包管理器工具
  3. 是否有DockerFile, 存在则以DockerFile为准
  4. 是否有文档,以文档为准
  5. 找同事或者领导确认

什么是 npx/pnpx?原理?

用于执行项目中的命令,不存在则安装它,它们是一样的,在yarn下则是 yarn dlx command 它的原理是:

  1. 直接执行 node_modules/.bin 下的可执行命令,如果不存在,则递归往上寻找,最后则在全局目录下寻找,不存在则进行下载。
  2. 对于npx,如果包不存在则下载到全局的 .npm/._npx 目录下,而不会下载全局目录下污染 软链接的Path变量,因此npx serve后,全局执行 serve仍然报错,pnpx与npx不同的是,pnpx不会在全局目录下去寻找包

npm link原理? 如何更好的去调试编译后的包?

  1. 以rollup为例, 在rollup目录中,执行 npm link,会根据package.json中的name创建一个软链接, 然后再我们需要进行调试的项目中 执行 npm link rollup, 将会替换 node_modules/rollup 将其软链接到 rollup源码中(如果是yarn link则会软链接到全局下的 yarn/link/rollup. Pnpm npm则不会多这一步操作)
  2. 经过编译后的代码一般都会被压缩混淆难以阅读,且sourceMap也没有,此时我们可以通过 npm link 软链接的形式进行替换再进行调试

目前npm的node_modules拓扑结构?有什么问题?

  1. 平铺结构
  2. 重复依赖,如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 为何节省资源

  1. 与平铺结构不同,它使用软链接的方式解决了重复依赖的问题,且会把项目中的依赖如 lodash3通过硬链接的方式链接到全局目录下(windows下则是根据项目所在的位置提升到磁盘根目录)的.pnpm-store
  2. 其他项目如果也使用lodash3这个包则可以直接复用同一个存储空间

pnpm 是如何解决幽灵依赖以及重复依赖安装的问题

pnpm将直接依赖置于 node_modules中,将间接依赖置于 node_modules/.pnpm中, 通过软链接 解决 重复依赖的问题,同时也解决了幽灵依赖的问题, 不在package.json中声明的包,是不会置于node_modules中,不会在项目中被引入

pnpm是如何解决eslint中幽灵依赖的问题?

javascript
module.exports = {
  extends: [
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@next/next/recommended',
  ]
}
  1. eslint中 由于其约定的命名规则配置,许多脚手架都会进行独立发包, 如 @vue/eslint-config-typescript, 执行 eslint . 去校验规则时, 它自身的 eslint配置内又会依赖其他包, 并由于eslint的命名规则 就会导致幽灵依赖.
  2. public-hoist-pattern是pnpm中用来忽略某些依赖特定的幽灵依赖, 默认是 eslint和prettier。 它会取消其幽灵依赖,让相关的依赖生效。而不是被作为幽灵依赖在安装包时被忽略

.npmrn的作用

配置包中的一些规范

registry=https://registry.npmmirror.com  源
save-prefix='~' 安装包时的范围
engine-strict=true 检验engines字段,不符合node版本不安装
fetch-retries=2  拉取包失败时的重试次数

如何修改仓库的源

  1. 修改npmrc文件
  2. npm config set registry cnpm源

npm ci的作用?什么情况下会失败?如何判断CI环境?

  1. 相比于npm i ,更安全,更快速,针对CI环境做了优化。
  2. 只修改了package.json的版本,但并未在开发环境npm i 重新安装,此时生产环境下/CI环境 npm ci 会直接失败
  3. 使用包:ci-info

exports 与 module.exports 有何区别

exports是 CJS的规范,而在Node和Webpack中对于CJS模块的代码 都会使用一个包裹函数处理 实际上如果同时使用这两个导出,最终导出会以 module.exports为准

javascript
(function(exports, require, module, __filename, __dirname) {
  // 以下一行是传参时确认的,但写在这里方便理解
  exports = module.exports
  // Module code actually lives in here
});

npm update/dedupe

  1. Npm update会将 package.json/package-lock.json 中的依赖升级到 符合范围内的最新版本。 但可能还是会存在重复依赖的问题
  2. 此时可以使用 npm dedupe,它会将符合版本范围的包升级为一个共同版本的包(pnpm install 自带pnpm dedupe的过程)

性能优化篇

TerserJS

一个用于压缩混淆JS代码的库,相比于uglify 对ES6有更好的支持。 而由于JS压缩效率不理想,因此出现了由RUST重写的 库swc,与terser拥有一样的 API

我们可以通过什么策略对 JavaScript 代码进行压缩

  1. 去除空格 换行 注释
  2. 压缩变量名 函数名 属性名
  3. 合并变量声明,简化逻辑布尔值
  4. 预计算编译,如常量和值, 函数输出简化

Terser options

https://www.webpackjs.com/configuration/optimization/#optimizationminimize

javascript
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标签,可以降级处理

html
<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存储服务

  1. 无域名图片支持,域外图片不受编译控制
  2. 无宽高优化,原图1000,浏览器仅渲染100
  3. 无响应式图片

什么是 DataURL

将小图片转为DataURL,可以减少HTTP请求。DataURL由四个部分组成,前缀、MIME类型、标记、数据, 它不仅是图片,也可以代表其他类型,只不过图片音频等二进制数据则必须由base64表示

小图片优化策略是什么

转为DataURL,减少图片内联,即减少HTTP请求达到优化的效果

在Webpack中处理小图片:

javascript
{
 module: {
    rules: [
      {
        test: /\.(bmp|jpe?g|png|gif)/,
        type: 'asset',
        parser: {
          // 对小于 4kb 的图片进行 DataURL 处理
          dataUrlCondition: {
            maxSize: 4 * 1024
          }
        }
      }
    ]
  },
};

svg

对于svg则需要借助另外一个包,不然直接压缩会导致原本的体积增大,因为svg不同于二进制图片,属于文本图片,文本不需要base64处理

javascript
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对其进行更进一步的压缩

javascript
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);
          }
        }
      }
    ]
  },
};