组件库万字长文解析版

可参考项目:https://github.com/wjywy/miniAntd;笔者两年前参加字节青训营编写的项目,刚学前端几个月,稍显丑陋

项目描述

  1. 实现组件库的打包和发布,适应npmpnpmyarn等多个平台,支持ESMUMD等多种模块导出方式
  2. 支持样式与逻辑的按需引入,结合 ESM 的 TreeShaking 特性,优化引入组件库时的代码体积与加载性能
  3. 实现灵活的样式管理机制,提供组件库主题切换局部样式按需覆盖方式,提升使用者的开发体验
  4. 实现 Message 组件的函数式调用方式,支持动态创建/销毁实例,并自动计算垂直偏移量解决消息堆叠问题
  5. 使用 Vitest 结合 TDD 模式开发组件,并使用 VitePress 搭建文档站点,提供实时预览和 API 示例,并自动部署至 Vercel
  6. 负责整体仓库的搭建,集成 ESLint+Prettier 保障代码风格的统一,集成 Husky 规范化 commit 提交信息

pnpm 相关问题

既然简历上说到了支持 pnpm、npm、yarn 等平台的发布,那么准备下 pnpm 相关的问题肯定是重中之重!!

pnpm 和 npm 以及 yarn 的区别

主要区别有两点:

  1. pnpm 支持全局共享存储,将依赖包安装到全局磁盘当中,使用的时候通过硬链接到达各个项目中去,允许不同项目共享相同的依赖,即使某一个依赖的版本不同,也只是去下载有 diff 的文件,不会全量下载。而 npm 和 yarn 在创建每个项目时,都会将所有项目的依赖放在当前目录下的 node_modules,造成存储空间的浪费
  2. pnpm 解决了 npm 和 yarn 中存在幽灵依赖的问题
    1. 幽灵依赖指的是在软件项目中存在但并未在package.json中声明的依赖项
    2. 产生原因:使用 npm、yarn 下载某些依赖时会下载上依赖的其他依赖项,但这些间接依赖项并未在package.json中显示声明
    3. pnpm 怎么解决的呢:pnpm 通过非扁平化的 node_modules 来解决的,也就是 node_modules 目录下只有.pnpm和直接依赖项,没有其他次级依赖包

那直接依赖项的依赖怎么办呢,同时怎么在下载的时候超过 npm 和 yarn 的速度呢

这个问题的答案就涉及到 pnpm 的三层寻址机制了,在 pnpm 中,每个包的寻找都要经过三层结构:

node_modules/package-a > 软链接**********/ > 硬链接 ~/.pnpm-store/v3/files/

也就是说,每个项目 node_modules 下安装的包以软链接方式将内容指向 node_modules/.pnpm 中的包;.pnpm目录以扁平化结构管理每个版本包的源码内容,以硬链接方式指向 pnpm-store 中的文件地址

软链接和硬链接的不同

详情可以看看我编写的这一篇文章,但是这里挑一些重点专门总结一下:现代包管理工具方案

硬链接
  • 具有相同inode节点号的多个文件互为硬链接文件;
  • 删除硬链接文件或者删除源文件任意之一,文件实体并未被删除;
  • 只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除;
  • 硬链接文件是文件的另一个入口;
软链接
  • 软链接类似windows系统的快捷方式;
  • 软链接里面存放的是源文件的路径,指向源文件;
  • 删除源文件,软链接依然存在,但无法访问源文件内容;

pnpm 的 hoist 机制

也就是依赖提升机制,指将依赖包的某些文件或目录提升到项目的顶层 node_moudles 目录中

npm 和 yarn 的区别

为什么会说到这个问题呢,这也是展示一个知识广度的点,在前面,我们精讲了 pnpm,但是对于 npm 和 yarn 的区别却没有详细展开来讲,这一点也是被很多同学所忽略的:即 npm 和 yarn 的不同之处到底在哪呢?这个问题其实有挺多可以详细说的点的,让我们详细来看看

npm v2

在最早期的npm版本(npm v2),npm的设计可以说是非常的简单,在安装依赖的时候会将依赖放到node_modules文件中; 随着项目的不断增大,依赖逐渐变成一个巨大的依赖树,不同依赖之间重复的依赖包也会重复安装,既占用我们电脑内存,也在安装/删除的过程中变得极为缓慢, 形成嵌套地狱

比如你安装一个 express,那么你会在 node_modules 下面只找到一个 express 的文件夹。而 express 依赖的项目都放在其文件夹下。

- app/
  - package.json
  - node_modules/
    - express/
      - index.js
      - package.json
      - node_modules/
        - connect/
        - path-to-regexp/
          - index.js
          - package.json
          - node_modules/
            - ...
        - ...

这个带来的问题或许 windows 用户深谙其痛,因为在这种安装环境下,会导致目录的层级特别高,而对于 windows 来说,最大的路径长度限制在 248 个字符,再加上 node_modules 这个单词又特别长,

npm v3

为了解决这些问题,(npm v3)重新考虑了 node_modules 结构并提出了扁平化。可以说很好的解决了嵌套层级过深以及实例不共享的问题。 所有的依赖都被拍平到node_modules目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。

- app/
- node_modules/
  - express/
  - connect/
  - path-to-regexp/
  - ...

如果出现了不同版本的依赖,比如说 package-a 依赖 [**********](mailto:**********)的版本,而package-b依赖**********` 版本,那么解决方案还是像之前的那种嵌套模式一样

- app/
- node_modules/
  - package-a/
  - package-c/
    - // 0.x.x
  - package-b/
    - node_modules/
      - package-c/
        - // 1.x.x

但是, 扁平化的方式依然存在诸多问题...

yarn v1

随着 node 社区壮大, 为了解决 npm 的几个问题

  • 无法保证两次安装的版本是完全相同的。(主要是没有锁版本文件, 默认安装为了处理一些小问题, 会默认使用^ 方式安装, 安装最新的小版本, 比如 2.XXX)
  • 安装速度慢。
  • npm 是不支持离线模式,导致内网使用比较困难

所以,此时 yarn 诞生了,为的就是解决上面几个问题。

  • 引入 yarn.lock 文件来管理依赖版本问题,保证每次安装都是一致的。
  • 缓存加并行下载保证了安装速度
npm v5

可能是受yarn的影响, npm v5 引入了 package-lock.json 来锁定版本, 且自动添加. 并且提升了安装速度, 但依然没有 yarn 快

npm v6

加入了缓存, 进一步提升了速度

yarn v2

2020 年发布的 yarn v2 可谓是 yarn 的一个巨大的改变 增加Plug'n'Play能力,作为重头戏,现在已经直接内置在 2.0 版本里面了。官方说可以用 node-modules 插件切换,但看起来并不能……

在使用 yarn 2.x 安装以后,node_modules 不会再出现,代替它的是.yarn 目录,里面有 cache 和 unplugged 两个目录,以及外面一个.pnp.js

  • .yarn/cache 里面放所有需要的依赖的压缩包,zip 格式
  • .yarn/unplugged 是你需要手动去修改的依赖,使用 yarn unplugin lodash 可以把 lodash 解压到这个目录下,之后想修改什么的随意
  • .pnp.js是 PNP 功能的核心,所有的依赖定位都需要通过它来
为什么会有package-lock.jsonyarn.lock 的出现

package.json中, 我们新增一个包都是使用 ^ 或者 ~ 的方式来安装依赖.:

  1. 如果在不同的时间安装这些软件包,则可能会导致下载这些软件包的不同版本。
  2. 在删除了 node_modules 文件夹后, 不实用 lock 文件安装的情况下, 每次安装的依赖都不一样
  3. 在项目的依赖中, 某个依赖由于使用 ^ 方式, 将会安装最新的小版本, 这将可能影响到项目, 比如 [**********](mailto:**********)与之后的版本的Transfer 穿梭框` 组件样式就完全不一样等

因此,有package-lock.jsonyarn.lock 的出现,将项目依赖每次新增以及修改的过程中, 将关系记录固定,就解决了这些问题。 但是, 千万不要混用 npmyarn, 这将导致安装依赖修改的 lock 文件会记录在不同的地方, 相当于各自记录一半. 建议您提交而不删除这些文件,除非您打算根据 package.json 规范更新软件包,并且准备进行彻底的测试或快速修复生产中发现的所有错误

node_modules 的扁平化结构问题

模块可以访问它们不依赖的包(幽灵依赖): 对于一个安装包内 package.json 中未申明的包,npm/yarn 由于扁平化的原因, 可以借助其安装的包内使用的依赖直接使用,这对于维护来说极度不安全, 当其依赖去除或升级, 将不可控。 例如,一个加载的包中使用的 moment.js, 随着后面可能的升级, 假如将其替换为day.js,将导致代码报错

总结
  1. 在 npm v3 版本之前,npm 采用的是树状结构管理依赖,容易造成重复依赖下载以及安装/卸载极为缓慢的问题
  2. 从 npm v3 版本开始,开始采用扁平化结构存储依赖,解决了上述的一些问题
  3. yarn v1 版本发布,一方面引入 yarn.lock 文件来管理依赖版本问题,保证每次安装都是一致的;另一方面采用缓存加并行下载保证了安装速度
  4. npm v5 版本发布,也开始引入package-locak.json文件来管理依赖版本问题,但是安装速度依旧没有 yarn 快
  5. yarn v2 版本发布,使用.yarn 目录来代替node_modules目录,里面有 cache

unplugged 两个目录,以及外面一个.pnp.js

  1. 除此之外,yarn 还有 yarn workspace,可以用来管理 monerepo 项目,但是 npm 并不可以

所以在回答 npm 和 yarn 的区别的时候,是一个可以展示你知识广度的一个很好的方式,因为没有限定 npm 和 yarn 的版本,所以你可以讲的具体再具体一点

ESM 和 UMD 模块的区别是什么,怎么去支持多种模块的导出的呢

在用户角度来说,ESM 就是可以形如import Button from 'antd'的形式来导出一个组件,可以实现 TreeShaking,进行按需引用;而 UMD 就是通过 <script src = 'xxxx'></script> 进行全局引用,不能够实现按需引用

TreeShaking 是什么,它的原理又是什么呢

简单总结下:

1. tree shaking 是一种死代码剔除技术
2. tree shaking 是根据模块间的信息来完成死代码的剔除的

原理:

Tree shaking 利用的是模块间的信息,进行的死代码删除。死代码的删除有很多方式,比如代码压缩时候的 compress,但它不是 tree shaking,只是简单的消除冗余、简化表达式

Tree shaking 的原理在于需要利用 ESM 之间的引用信息,即它需要先去构建初代码的依赖关系图谱,然后去标记出未被使用的代码并确定模块之间的依赖关系,最后再消除未使用过的代码。

但是,受限于 JS 的动态性与模块的复杂性(比如各种意料之外的副作用的产生), 目前并没有哪个 bundler 可以对 tree shaking 做到极致,我们在编写代码时仍需要有意识的去优化代码结构(比如避免无意义的赋值)

延伸:

但是,在某些场景下,CommonJS 之间也能分析出模块间的引用关系,也能实现 tree shaking

针对于非 JS 文件, Bundler(打包器) 的老大哥 Webpack(Vite )不支持噢) 支持 JSON 资源的 tree shaking(你的 JSON 文件中某个字段没用到,直接帮你删了,比如 <font style="color:rgb(51, 51, 51);background-color:rgb(244, 244, 244);">package.json</font> 打包之后就留个 name 和 version 字段)。至于 CSS 文件,则更多的借助外部的库,比如 purgecss、cssnano,当然,这是后话,后续会出一篇文章详细的来讲 css 代码体积的优化

最重要的一点,tree shaking 就是一个概念,并没有具体实现的规范。所以各家 bundler 可以天马行空的实现

逻辑的按需加载分别是怎么做的

回答按需加载的时候这里大家首先想到的多半是回答逻辑的按需加载:

所以就先说说逻辑的按需加载

  • 按需加载:分别使用 <font style="color:rgb(51, 51, 51);">export</font> 导出每个组件,且组件库使用 ESMoudle 形式导出,让构建工具可以进行 tree haking

但其实最关键的是支持 CSS 进行按需引入,因为由于 ESM 的存在,逻辑的按需引入已经很成熟了,下面我将详细说一说 CSS 的按需引入的方案

CSS 的按需引入是怎么做的

大致分为三种方案,如下:(Vue 项目中 CSS 的按需引入略有不同)

样式和逻辑分离:

过程

在打包后,每个组件的输出文件都包括单独的 JS 文件和 CSS 文件,不过在 JS 中并不引入 CSS 文件,所以使用者在不加任何处理的情况下,需要分别添加每个组件的逻辑和样式代码,如下:

import { Button } form "My-Ui/Button/Button.js"
import "My-Ui/Button/Button.css"

这样肯定会为使用者带来较大的心理负担,所以市面上针对于此也开发了插件:[babel-plugin-import](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fumijs%2Fbabel-plugin-import),可以在编译阶段辅助生成引入样式的 import 语句

优点
    1. <font style="color:rgb(51, 51, 51);">不限制组件库的技术栈,因为 CSS 文件是完全单独独立的,同一份样式文件可以作用于多个框架的组件库中</font>
    2. <font style="color:rgb(51, 51, 51);">可以对外部直接提供</font>`<font style="color:rgb(51, 51, 51);">less</font>`<font style="color:rgb(51, 51, 51);">等样式文件,便于组件库样式一键切换</font>
缺点
    1. <font style="color:rgb(51, 51, 51);">需要使用者手动引入 CSS 文件</font>

样式和逻辑结合:

过程

天然支持样式的按需引入,使用时只需要引入对应的逻辑文件即可,目前有两种方案:

        1. <font style="color:rgb(51, 51, 51);">CSS In JS:例如 Style-Component 等 CSS in JS 方案,天然与逻辑文件进行绑定</font>
        2. <font style="color:rgb(51, 51, 51);">将 CSS 打包到 JS 中:在组件库开发时使用 </font>`<font style="color:rgb(51, 51, 51);">import</font>`<font style="color:rgb(51, 51, 51);">引入样式文件,在 Webpack 中可通过</font>`<font style="color:rgb(51, 51, 51);">style-loader</font>`<font style="color:rgb(51, 51, 51);">将 CSS 代码打包进逻辑文件中</font>
优点
    1. <font style="color:rgb(51, 51, 51);">便于使用者操作,只需要</font>`<font style="color:rgb(51, 51, 51);">import</font>`<font style="color:rgb(51, 51, 51);">组件即可使用</font>
    2. <font style="color:rgb(51, 51, 51);">天然支持按需加载,因为样式与逻辑都在同一个文件之中</font>
缺点
    1. <font style="color:rgb(51, 51, 51);">无论是 CSS in JS 还是 CSS 打包进 JS,所引用的插件都会有额外的运行时,会略微影响组件库的性能</font>

样式和逻辑关联:

过程

在组件库开发时与 CSS 打包至 JS 中相似,依旧使用 <font style="color:rgb(51, 51, 51);">import</font> 引入样式文件,不过打包后会生成独立的逻辑与样式文件,但是打包会在逻辑文件中保留对应的 <font style="color:rgb(51, 51, 51);">import</font>语句

优点
    1. <font style="color:rgb(51, 51, 51);">使用者只需要 </font>`<font style="color:rgb(51, 51, 51);">import</font>`<font style="color:rgb(51, 51, 51);">组件即可使用</font>
    2. <font style="color:rgb(51, 51, 51);">支持按需加载</font>
缺点
    1. <font style="color:rgb(51, 51, 51);">如果使用了 CSS 预处理语言,打包编译的流程会更加复杂,因为需要让逻辑文件通过</font>`<font style="color:rgb(51, 51, 51);">import</font>`<font style="color:rgb(51, 51, 51);">关联正确的 CSS 文件</font>

总结

样式与逻辑分析 样式与逻辑结合 样式和逻辑关联
开发打包流程 中等 简单 复杂
输出文件 样式与逻辑文件 逻辑文件 样式与逻辑文件
使用方法 分别引入 JS 和 CSS 文件 只引入 JS 只引入 JS
按需加载 需要插件,如:<font style="color:rgb(51, 51, 51);">babel-plugin-import</font> 支持 支持
性能影响 带额外 runtime

其实关于这几种方案的区别还可以从 SSR性能优化的影响两方面去说,从而去引出 SSR 的知识以及性能优化的知识,不过大部分同学的组件库项目都不涉及这两方面,所以这里就不进行赘述,想要详细了解的可以参考文章:React组件库CSS样式方案分析

ESM 和 CJS 的区别(模块化方案有哪些)

  1. 用法不同:
    • ES module 是原生支持的 Javascript 模块系统,使用 import/export 关键字实现模块的导入和导出。
    • 而 CommonJs 是 Node 最早引入的模块化方案,采用 require 和 module.exports 实现模块的导入和导出
  2. 加载方式不同:
    • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
    • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  3. 导入和导出特性不同:
    • ES module支持异步导入,动态导入和命名导入等特性,可以根据需要动态导入导出,模块里面的变量绑定其所在的模块。
    • 而 CommonJs 只支持同步导入导出。
  4. 循环依赖处理方式不同:
    • ES module 采用在编译阶段解决并处理:ES Module 通过使用一张模块间的依赖地图来解决死循环问题,标记进入过的模块为“获取中”,所以循环引用时不会再次进入
    • 而 cjs 则通过在第一次被<font style="color:#000000;">require</font>时就会执行并缓存其<font style="color:#000000;">exports</font>对象。这样在循环引用中,CommonJS 就****会提供一个“部分导出对象”(partial exports),从而打破无限循环,如下,若 a 文件中引用了 b,b 文件中引用了 a:
main.js
  └──> a.js
         └──> b.js
                   └──> a.js (cached partial exports)

  1. 兼容性不同
    • ESmodule 需要在支持 ES6 的浏览器或者 Nodejs 的版本才能使用,
    • 而CommonJS 的兼容性会更好
  2. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
    • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
    • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

Vitest 和 Jest 选型对比

  1. vitest 对比 jest 一个显著的优势是 vitest 原生支持 TypeScript、ESM,而 jest v29 版本现在对于 ESM 的支持还是实验性,此外如果要支持 TypeScript 还需要配置 babel,通过 babel 来转译支持 TypeScript。而 vitest 这些特性都是开箱即用的
  2. vitest 的 HMR(热更新)特性比 vitest 快

下面是一个详细的对比,大家在回答时可以挑几个重点的点来回答:

特性 Jest vitest
模块支持
浏览器 UI
TS 类型测试
benchmark
源码内测试
浏览器模式
多浏览器支持
增强的错误匹配
项目级配置
快照测试
交互式快照测试
代码覆盖率
并发测试
分片支持
Mocking

综上,vitest 有更多的现代特性,所以如果是一个使用现代特性的新项目,那么 vitest 肯定是首选!!

组件库中的样式覆盖怎么做的呢

  1. className 透传
<Button className="bg-red-500 hover:bg-red-600" />
- 优点:最灵活,特别适合 tailwind
- 注意:需要注意 className 后应用,避免被覆盖

2. style 透传

<Button style={{ backgroundColor: 'orange' }} />
- 缺点:优先级高,样式不可继承,支持有限

3. css 变量

组件内部:

.button {
  background-color: var(--button-bg, blue);
  color: var(--button-text, white);
}

使用者:

/* 页面或容器局部覆盖 */
:root {
  --button-bg: green;
  --button-text: white;
}
  1. style-component/emotion 方式(CSS in JS)
import styled from 'styled-components';
import { Button } from './my-ui-lib';

const CustomButton = styled(Button)`
  background-color: red;
  &:hover {
    background-color: darkred;
  }
`;

<CustomButton />;

  • 注意:适合于 CSS in JS 的项目;如果组件库是原生 CSS/Less,就不一定能用了

详细讲一讲 Message 组件的实现

正在 writing

自动计算垂直偏移量是怎么计算的呢

正在 writing

SSG、SSR、CSR 这三种渲染方式的区别,以及 SPA 应用的优缺点

为什么会准备这个问题呢,因为在回答 vitePress 的时候可以多说一句:VitePress 其实就是静态文档站点,其原理就是 SSG

  1. SSR(Server-Side Rendering):
  • 优点
    1. 更好的首次加载性能:服务器将首次渲染的 HTML 发送到浏览器,使用户能够更快地看到页面内容。
    2. 更好的 SEO:搜索引擎可以更容易地索引服务器渲染的内容。
    3. 更好的性能:对于一些性能较低的设备,由服务器渲染的页面可能会更快加载。
  • 缺点
    1. 较高的服务器负载:每个请求都需要服务器进行渲染,可能导致服务器负载较高。
    2. 较复杂的部署:需要服务器端的渲染支持,配置较为复杂。
  1. SSG(Static Site Generation):
  • 优点
    1. 静态文件部署:生成静态文件,可以使用 CDN 进行分发,提高访问速度。
    2. 低服务器负载:由于页面在构建时就已经生成,不需要在每个请求时进行渲染。
    3. SEO 优化:生成的静态文件对搜索引擎友好。
  • 缺点
    1. 不适用于动态内容:对于频繁更新的动态内容,SSG 可能不够灵活。
    2. 构建时间较长:对于大型网站,构建时间可能较长。
  1. CSR(Client-Side Rendering):
  • 优点
    1. 更快的页面切换:一旦初始页面加载完成,后续的页面切换通常更快,因为只需要加载数据而不需要重新渲染整个页面。
    2. 更好的交互性:支持丰富的交互,用户体验更流畅。
  • 缺点
    1. 首次加载性能较差:用户需要等待 JavaScript 加载和执行,才能看到页面内容。
    2. SEO 不佳:搜索引擎在渲染页面时可能无法执行 JavaScript,因此初始 HTML 中可能没有完整的内容被索引。
    3. 对于低性能设备,可能加载较慢。
  1. SPA(Single Page Application)应用的优缺点:
  • 优点
    1. 更流畅的用户体验:页面不需要重新加载,通过 AJAX 更新内容,用户体验更接近原生应用。
    2. 前后端分离:前端和后端可以独立开发、部署,提高开发效率。
  • 缺点
    1. SEO 难度:由于初始 HTML 中通常包含较少的内容,对搜索引擎的索引不友好,需要通过特定的技术手段(如预渲染)来解决。
    2. 首次加载性能:对于大型单页面应用,首次加载可能较慢,用户需要等待 JavaScript 和其他资源加载完成。
    3. 对于一些低性能设备,可能导致较差的性能。

编写单测的步骤

这里就可以开始自由发挥了,其实并没有固定的答案捏,笔者编写单测的步骤是:

  1. 先准备前置环境以及必须的配置
  2. 模拟用户或者系统操作
  3. 断言操作后的值与预期值是否一致

TDD:测试驱动开发

TDD 也就是测试驱动开发,是一种软件开发方法,其核心思想是:先写测试,再写代码,以测试来驱动代码的设计与开发。

通常我们分为下面三个步骤进行开发:

  • **编写失败的测试:**写一个针对尚未实现功能的单元测试,测试一定会失败(因为功能还没实现)
  • **编写刚好通过测试的代码:**编写最简单的代码,只为通过刚才的测试。
  • **重构:**优化代码结构,不改变行为,保持测试通过

除了单测,你还了解过其他测试吗

了解过的,现在的测试按维度主要分为两个方向:

  • 单元测试,主要用来验证最小逻辑单元是否正确,比如单个函数、组件等
  • 集成测试:用来验证组件之间的协作是否正常
  • 端对端测试,也称之为 e2e 测试,用来验证整个系统的用户流程、页面交互等是否符合预期。主要使用的框架为playwright

还了解其它模式吗,比如说 DDD 模式

这个问题就简单回答一下就 OK 了,也不会细问的

DDD( Domain-Driven Design ) 模式,领域驱动设计。即围绕“业务概念”来设计代码结构,让软件模型和现实世界的业务保持一致

那你了解过设计模式吗?—— 观察者模式,策略模式、依赖注入模式

这里很多同学可能会回答观察订阅者模式,但是严格来说发布订阅并不在设计模式之中,它只是观察者模式的变种而已,但是这里也会将发布订阅详细介绍一下,再挑几个前端在日常开发中使用最高的设计模式详细讲讲:

  1. 观察者模式:观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新
  2. 发布订阅模式:发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在
  3. 策略模式:
    1. 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句
    2. 策略模式提供了开放-封闭原则,使代码更容易理解和扩展
    3. 策略模式中的代码可以复用
  4. 依赖注入模式:通过将依赖(即一个类对另一个类的依赖)从硬编码的引用中移除,并将其替换为通过构造函数参数,方法参数,或者属性来传递,以达到松耦合的目的。它是一种控制反转的实现方式,使得代码之间的依赖关系被第三方进行管理和控制。比如现在很火的 node 框架:nest 运用的就是这种设计模式

观察者模式和发布订阅模式的区别是什么

通常我们会这么回答:

  1. 在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  2. 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
  3. 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)

但是这么回答显得干巴巴的,所以一般我们还会实际举一个例子来具体展示观察者模式和发布订阅模式到底不同在哪:

比如有一家报纸社每天发送报纸,有多个用户想订阅报纸。

- 观察者模式就是用户直接在报纸社等级姓名和地址,报纸社有一份用户列表(观察者列表),那么每当报纸出版的时候,报纸社都回去主动通知所有在名单上的用户,比如送上门或者打电话通知
- 发布订阅模式就是有一个类似于邮局的中间人,报纸社将报纸发给邮局,不关心是谁订阅了;用户也去邮局等级对哪份报纸感兴趣,到报社发布新报纸,邮局负责转发给所有订阅该报纸的用户

那你一般怎么去封装一个组件

具体细节可以参考我的另一篇专题文章:组件设计原则

在组件设计前,通常关注如下几点:

明确组件职责

  1. 组件的目标是什么?
    • 明确它是展示型(UI组件)还是功能型(逻辑组件)
  2. 是否具备复用价值?
    • 是否在多个场景中出现、是否可以抽象公共逻辑

在组件开发中,需要关注以下几点:

组件名称

根据组件的类型和关键特征进行命名

组件 props

确定组件的入参,一般从这几方面进行考虑:

  • 数据型属性(必传,用于渲染 UI,如 dataSource
  • 配置型属性(可选,修饰功能或样式,如 disabledtype
  • 事件型属性
    • 命名以 on 开头,如 onChange, onSearch
    • 遵循“自上而下”设计,避免子组件控制父组件
  • **插槽型属性: **用于需要传 JSX、灵活渲染子元素的场景,如 childrenappend

组件方法

组件方法是指组件通过 useImperativeHandle 暴露的父组件可以调用的方法。

能用属性和事件的实现组件尽量用属性和事件,实在不行的再用方法。因为 React 更偏向于使用声明式编程,而组件方法使用命令式编程,应当少用

可以先举一个需要用方法的例子,比如咱们后台常用的抽屉组件,在抽屉里有个表单组件,而“保存”按钮通常在抽屉组件里,而表单内容通常作为一个整体封装成一个组件,表单内的各种操作逻辑都封装在组件内实现了高内聚,但是这样有个小问题,因为“保存”按钮通常在抽屉组件里,点击“保存”的事件表单无法知晓,抽屉又拿不到表单内的数据,就无法完成保存功能,这个时候很难使用属性和事件来实现,这就需要使用方法了,点击“保存”时通过调用表单组件的方法拿到表单数据进行保存

组件 State

  • 注重并且只保留组件内部的状态
  • 可由 props 或其他 state 推导出的,**使用 ****useMemo**包裹一下

组件样式

  1. 不要使用 css module,因为使用 module,就无法知晓 css 类名,进行局部样式微调时比较困难
  2. 组件最外层的元素样式名称与组件名称保持一致,比如 Input 组件的最外层元素的样式名称是 input。组件库通常会提供一个前缀,比如 ant-design 的 Input 组件样式名称是 ant-input

你一般在项目中怎么去践行前端工程化

首先我们需要明确一下前端工程化的目的,总结一下,其目标是**提升开发效率、代码质量、协作能力与可维护性。**所以我们可以拆解一下整个项目的开发流程,从以下几个方面去回答:

┌────────────┬──────────────────────────┐
│ 阶段       │ 工程化手段               │
├────────────┼──────────────────────────┤
│ 初始化     │ 脚手架、TS、Husky、规范提交 │
│ 代码规范   │ ESLint、Prettier、Stylelint │
│ 模块化     │ 组件抽象、hooks、目录分层   │
│ 自动化开发 │ Mock、接口工具、环境变量   │
│ 状态路由   │ Zustand/Redux、Router 管理 │
│ 测试体系   │ Jest、playwright        │
│ 持续集成   │ GitHub Actions、Docker    │
│ 协作与文档 │ Storybook、dumi        │
└────────────┴──────────────────────────

分割线,以上的问题都是根据简历上的描述去进行提问,那么接下来也需要掌握一下常规组件库的考点

组件库是怎么进行联调的呢

这里其实就不单独仅涉及组件库了,还需要有一个用到此组件库的项目,通常在开发阶段,使用 npm link 的方式来进行联调。

先介绍一下,npm link 是一种把包链接到包文件夹的方式,即:可以在不发布 npm 模块的情况下,调试该模块,并且修改模块后会实时生效,不需要通过npm install进行安装

使用的话分为两种情况:

1. 模块和项目在同一目录下:直接`npm link ../module`
2. 模块和项目不在同一目录下: 
    1. 先去到模块目录,把它 link 到全局:`cd ../npm-link-test``npm link`
    2. 再去项目目录通过报名来 link:`cd ../my-project-link``npm link test-npm-link`

css样式隔离的方案

这里就比较简单的,大概方法分为以下几种:

  1. BEM 命名方法:通过命名规则避免样式冲突
  2. css-module:通过编译生成不冲突的选择器类名。打包工具会将类名编译成带哈希的字符串
  3. 预处理器——less 或 sass,不嵌套的话同 BEM
  4. css in js:天然支持隔离

#前端##面试##实习##秋招#
全部评论
厉害了
点赞 回复 分享
发布于 昨天 13:46 广西
需要的朋友直接找我要仓库链接,在语雀上,自发开源
点赞 回复 分享
发布于 05-13 12:49 重庆

相关推荐

评论
4
10
分享

创作者周榜

更多
牛客网
牛客企业服务