颜海镜

颜海镜 查看完整档案

北京编辑华北电力大学(保定)  |  软件工程 编辑美团点评  |  前端 编辑 yanhaijing.com 编辑
编辑

知名技术博主,《React状态管理与同构实战》作者,http://yanhaijing.com

个人动态

颜海镜 赞了文章 · 4月2日

如何快速构建React组件库

前言

俗话说:“麻雀虽小,五脏俱全”,搭建一个组件库,知之非难,行之不易,涉及到的技术方方面面,犹如海面风平浪静,实则暗礁险滩,处处惊险~

目前团队内已经有较为成熟的 Vue 技术栈的 NutUI 组件库[1] 和 React 技术栈的 yep-react 组件库[2]。然而这些组件库大都从零开始搭建,包括 Webpack 的繁杂配置,Markdown 文件转 Vue 文件功能的开发,单元测试功能的开发、按需加载的 Babel 插件开发等等,完成整个组件库项目实属不易,也是一个浩大的工程。如果我们想快速搭建一个组件库,大可不必如此耗费精力,可以借助业内专业的相关库,经过拼装调试,快速实现一个组件库。
本篇文章就来给大家介绍一下使用 create-react-app 脚手架、docz 文档生成器、node-sass、结合 Netlify 部署项目的整个开发组件库的流程,本着包教包会,不会没有退费的原则,来一场手摸手式教学,话不多说,让我们进入正题:

首先看一下组件库的最终效果:

组件库界面

本文将从以下步骤介绍如何搭建一个 React 组件库:

文章结构

一、构建本地开发环境

开发一个组件库的首要步骤就是调试本地 React 环境,我们直接使用 React 官方脚手架 create-react-app,可以省去从底层配置 Webpack+TypeScript+React 的摧残:

1、使用 create-react-app 初始化脚手架,并且安装 TypeScript

npx create-react-app myapp --typescript

注意使用 node 为较高版本 >10.15.0

2、配置 eslint 进行格式化

由于安装最新的 create-react-app 结合 VScode 编辑器即可支持 eslit,但是需要在项目根目录中要添加 .env 这个配置文件,设置 EXTEND_ESLINT=true 这样才会启用 eslint 检测,注意要 重启 vscode

3、组件库系统文件结构

新建 styles 文件夹,包含了基本样式文件,结构如下:

|-styles
| |-variables.scss // 各种变量以及可配置设置
| |-mixins.scss    // 全局 mixins
| |-index.scss    // 引入全部的 scss 文件,向外抛出样式入口
|-components
| |-Button
|   |-button.scss // 组件的单独样式
|   |-button.mdx // 组件的文档
|   |-button.tsx // 组件的核心代码
|   |-button.test.tsx // 组件的单元测试文件
|  |-index.tsx  // 组件对外入口

4、安装 node-sass 处理器

安装 node-sass 用来编译 SCSS 样式文件:npm i node-sass -D

这样最基本的 react 开发环境就完成了,可以开心的开发组件了。

二、组件库打包编译

本地调试完组件库之后,需要打包压缩编译代码,供其他用户使用,这里我们用的 TypeScript 编写的代码,所以使用 Typescript 来编译项目:
首先在每个组件中新建 index.tsx 文件:

import Button from './button'
export default Button 

修改 index.tsx 文件,导入导出各个模块

export { default as Button } from './components/Button'

在根目录新建 tsconfig.build.json,对 .tsx 文件进行编译:

{
  "compilerOptions": {
    "outDir": "dist",// 生成目录
    "module": "esnext",// 格式
    "target": "es5",// 版本
    "declaration": true,// 为每一个 ts 文件生成 .d.ts 文件
    "jsx": "react",
    "moduleResolution":"Node",// 规定寻找引入文件的路径为 node 标准
    "allowSyntheticDefaultImports": true,
  },
  "include": [// 要编译哪些文件
    "src"
  ],
  "exclude": [// 排除不需要编译的文件
    "src/**/*.test.tsx",
    "src/**/*.stories.tsx",
    "src/setupTests.ts",
  ]
}

对于样式文件,使用 node-sass 编译 SCSS,抽取所有 SCSS 文件生成 CSS 文件:

"script":{
    "build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
}

并且修改 build 命令:

"script":{
    "clean": "rimraf ./dist",// 跨平台的兼容
    "build": "npm run clean && npm run build-ts && npm run build-css",
}

这样,执行 npm run build 之后,就可以生成对应的组件 JS 和 CSS 文件,为后面使用者按需加载和部署到 npm 上提供准备。

三、本地调试组件库

本地完成组件库的开发之后,在发布到 npm 前,需要先在本地调试,避免带着问题上传到 npm 上。这时就需要使用 npm link 出马了。

什么是 npm link

在本地开发 npm 模块的时候,我们可以使用 npm link 命令,将 npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试。

使用方法

假设组件库是 reactui 文件夹,要在本地的 demo 项目中使用组件。则在组件库中(要被 link 的地方)执行 npm link,则生成从本机的 node_modules/reactui 组件库的路径 / reactui 中的映射关系。
然后在要使用组件库的文件夹 demo 中执行 npm link reactui 则生成以下对应链条:

在要使用组件的文件夹 demo 中 -[映射到]—> 本机的 node_modules/reactui —[映射到]-> 开发组件库 reactui 的文件夹 /reactui

需要修改组件库的 package.json 文件来设置入口:

{
  "name": "reactui",
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
}

然后在要使用组件的 demo 项目的依赖中添加:

"dependencies":{
  "reactui":"0.0.1"
}

注意,此时并不用安装依赖,之所以写上该依赖,是为了方便在项目中使用的时候可以有代码提示功能。
然后在 demo 项目中使用:

import { Button } from 'reactui'

在 index.tsx 中引入 CSS 文件

import 'reactui/build/index.css'

正当以为大功告成的时候,下面这个报错犹如一盆冷水从天而降:

错误提示

经过各种问题排查,在 react 官方网站[3] 上查到以下说法:

🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.

说的很明白:

原因 1: React 和 React DOM 的版本不一样的问题
原因 2: 可能打破了 Hooks 的规则
原因 3: 在同一个项目中使用了多个版本的 React

官网很贴心,给出了解决方法:

This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder. Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib. This should make the library use the application’s React copy.

核心思想在组件库中使用 npm link 方式,引到 demo 项目中的 react; 所以在组件库中执行: npm link ../demo/node_modules/react

具体步骤如下:

  1. 在代码库 reactui 中执行 npm link
  2. 在代码库 reactui 中执行 npm link ../../demo/node_modules/react
  3. 在项目 demo 中执行 npm link reactui

如此可以解决上面 react 冲突问题;于是可以在本地一边快乐的调试组件库,一边快乐的在使用组件的项目中看到最终效果了。

四、组件库发布到 npm

该过程一定要注意使用的是 npm 源!![非常重要]

首先确定自己是否已经登录了 npm:

npm adduser
// 填入用户名;密码;email
npm whoami // 查看当前登录名

修改组件库的 package.json ,注意 files 配置;以及 dependencies 文件的化简:
react 依赖原本是要放在 dependencies 中的,但是可能会和用户安装的 react 版本冲突,所以放在了 devDependencies 中,但是这样话用户如果没有安装 react 则无法使用组件库,所以要在 peerDependencies 中定义前置依赖 peerDependencies,告诉用户 react 和 react-dom 是必要的前置依赖:

"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [ // 把哪些文件上传到 npm
  "dist"
],
"dependencies": {  // 执行 npm i 的时候会安装这些依赖到 node_modules 中
  "axios": "^0.19.1",// 发送请求
  "classnames": "^2.2.6",//
  "react-transition-group": "^4.3.0"
},
"peerDependencies": { // 重要!!,提醒使用者,组件库的核心依赖,必须先安装这些依赖才能使用
  "react": ">=16.8.0",  // 在 16.8 之后 才引入了 hooks
  "react-dom": ">=16.8.0"
}

好了,整个组件库经过上述过程,基本上各个功能已经有了,提及一句:由于组件库使用的是 create-react-app 脚手架,最新的版本已经集成了单元测试功能。还有配置 husky 等规范代码提交,在这里不在做赘述,读者可以自行配置。

五、生成说明文档

目前生成说明文档较好的工具有 storybook[4]、docz[5] 等工具,两者都是很优秀的文档生成工具,但是尺有所短,寸有所长,经过认真调研比较,最终选择了 docz。

工具名称区别一区别二
storybook使用特有的API开发文档说明,可以引入markdown文件生成文档的界面带有storybook的痕迹较多一些
docz完美的结合了react和markdown语法开发文档生成的文档界面是常规的文档界面

1、确定选型

1)storybook 的常用编译文档规范相对 docz 而言,略有繁琐

storybook 的编译文档规范如下所示:

//省略 import 引入的代码
storiesOf('Buttons', module)
.addDecorator(storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>)
.add('with text', () => (
<Button onClick={action('clicked')}>Hello Button111</Button>
),{
notes:{markdown}   // 将会渲染 markdown 内容
}) 

对比 docz 的开发文档:

# Button 组件

使用方式如下所示:
import { Playground, Props } from 'docz';
import Button from './index.tsx';

## 按钮组件

<Playground>
    <Button btnWidth="100">我是按钮</Button>
</Playground>

** 基本属性 **

| 属性名称 | 说明 | 默认值 |
|--|--|--|
|btnType | 按钮类型 |--|

众所周知,Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。团队成员在开发文档时,熟练使用 markdown 语法,开发 docz 文档的 mdx 文件,结合了 Markdown 和 React 语法,相比 storybook 要使用很多的 API 来编写文档的方式,无疑减少了很多的学习 storybook 语法的成本。

2)docz 生成的文档样式更加符合个人审美

storybook 生成的文档样式,带有 storybook 的痕迹更为严重一些, 其生成文档界面如下所示:

storybook生成界面

docz 生成的文档图如下所示:

docz生成界面

由上图对比可以看出,docz 生成的界面更加简介,较为常规。
综上,结合默认文档开发习惯和界面风格,我选择了docz,当然仁者见仁、智者见智,读者也可以使用同为优秀的 storybook 尝试,这都不是事儿~

2、使用 docz 开发

确定了 docz 进行开发后,根据官网介绍,在 create-react-app 生成的组件库中进行了安装配置:

npm install docz

安装成功后,就会向 package.json 文件中添加如下配置

{
  "scripts": {
    "docz:dev": "docz dev",
    "docz:build": "docz build",
    "docz:serve": "docz build && docz serve"
  }
}

这时还需要在项目的根目录下新建 doczrc.js 文件,对 docz 进行配置:

export default {
  files: ['./src/components/**/*.mdx','./src/*.mdx'], 
  dest: 'docsite', // 打包 docz 文档到哪个文件夹下
  title: '组件库左上角标题',  // 设置文档的标题
  typescript: true, // 支持 typescript 语法
  themesDir: 'theme', // 主题样式放在哪个文件夹下,后面会讲
  menu: ['快速上手', '业务组件'] // 生成文档的左侧菜单分类
}

其中 files 规定了 docz 去对哪些文件进行编译生成文档,如果不做限制,会搜索项目中所有的 md、mdx 为后缀的文件生成文档,因此我在该文件中做了范围限制,避免一些 README.md 文件也被生成到文档中。

此外还需要注意到两点:

1、menu: ['快速上手', '业务组件'] 对应着组件库左侧的菜单栏分类,比如在 mdx 文档中在最上面设置组件所属的菜单 menu: 业务组件 , 则 Button 组件属于 "业务组件" 的分类:

---
name: Button
route: /button
menu: 业务组件
---

在 src 中新建欢迎页,路由为跟路径,所属菜单为“快速上手”;

---
name: 快速上手
route: /
---

执行 npm run docz:dev,就可以打开

路由配置

介绍到这里,估计有小伙伴会有疑问了,这样生成的网站千篇一律,能否随心所欲的自定义网站的样式和功能呢?当初我也有这种疑问,经过多次尝试,皇天不负苦心人,终于摸索出如下方法:

1、修改 docz 文档本身的样式

根据 docz 官方文档中增加 logo 的方法[6],可以通过自定义组件覆盖原有组件的形式:

Example: If you're using our gatsby-theme-docz which has a Header component located at src/components/Header/index.js you can override the component by creating src/gatsby-theme-docz/components/Header/index.js. Cool right?

所以根据 docz 源代码主题部分代码: https://github.com/doczjs/docz/tree/master/core/gatsby-theme-docz/src,找到对应的文档组件的代码结构,在组件库项目根目录新建同名称的文件夹:

|-theme
|  |-gatsby-theme-docz
|     |-components
|     |-Header
|       |-index.js // 在这里修改自定义的文档组件
|       |-styles.js // 在这里修改生成的样式文件

这样在执行 npm run docz:dev 的时候,就会把自定义的代码覆盖原有样式,实现文档的多样化。

2、修改 markdown 文档样式

事情到这里就结束了吗?不!我们的目标不仅如此,因为我发现自动生成的 markdown 格式,并不符合我的审美,比如生成的表格文字居左对齐,并且整个表格样式单一,但是这里属于 markdown 样式的范畴,修改上述文档组件中并不包括这里的代码,那么如何修改 markdown 生成文档的样式呢?

docz默认生成表格样式

经过我灵机一动又一动,发现既然在上面修改文档组件样式的时候,重写了 component/Header/styles.js 文件,是否可以在该文件中引入自定义的样式呢?文件结构如下:

|-theme
|  |-gatsby-theme-docz
|     |-components
|     |-Header
|       |-index.js // 在这里修改自定义的文档组件
|       |-styles.js // 在这里修改生成的样式文件
|       |-base.css  // 这里修改 markdown 生成文档的样式

这样修改后的表格样式如下:

修改docz表格样式

接下来各位小主可以根据自己的审美或者视觉设计的要求自定义文档的样式了。

六、部署文档到服务器

生成的组件库文档只在本地显示是没有意义的,所以需要部署到服务器上,于是第一时间想到的是放在 github 进行托管,打开 github 中的 setting 设置选项,GitHub Pages 设置配置的分支:

设置分支

这时默认打开的首页路径为:

https://plusui.github.io/plusReact/

但实际上页面有效的访问地址是带有文件夹 docsite 路径的:

https://plusui.github.io/plusReact/docsite/button/index.html

此外,页面引入的其他资源路径,都是绝对路径,如下图资源路径所示:

引入资源路径

所以直接把打包后的资源放在 github 上是无法访问各种资源的。
这时我们只好把网站部署到云服务器上了,考虑到服务器配置的繁琐,这里给大家提供一个简便的部署网站:Netlify[7]

Netlify 是一个提供静态网站托管的服务,提供 CI 服务,能够将托管 GitHub,GitLab 等网站上的 Jekyll,Hexo,Hugo 等静态网站。

部署项目的过程也很简单,傻瓜式的点击选择 github 网站中代码路径,以及配置文件夹跟路径,如下图所示:

配置文件夹路径

然后就可以点击生成的网站 url,访问到部署的网站了:

部署网站

而且很方便的是,一旦完成部署之后,之后再次向代码库中提交代码,Netlify 会自动更新网站。
此外,如果想自定义 url,那么就只能去申请域名了,在自己的云服务器上,解析域名即可。下面简单说一下配置步骤:

1)首先在 Netlify 网站上,选择组件库对应的 Domain settings 下 Custom domains,增加自己的域名:

配置域名

2)然后打开云服务器中的域名解析中的解析设置,将该域名指向 Netlify:

云服务器上增加域名

3)最后打开设置的网址,就可以访问到组件库了:

最终效果

七、组件按需加载

好了,经过上面的流程,可以在 demo 项目中使用组件库了,但是在 demo 项目中,执行 npm run build ,就会发现生成的静态资源中即使只使用了一个组件,也会把 reactui 组件库中所有的组件打包进来。

所以如何进行按需加载呢?

按需加载首先映入脑海的是使用 babel-plugin-import 插件, 该插件可以在 Babel 配置中针对组件库进行按需加载.

用户需要安装 babel-plugin-impor 插件,然后在 plugins 中加入配置:

"plugins": [
  [
    "import",
    {
      "libraryName": "reactui", // 转换组件库的名字
      "libraryDirectory": "dist/components", // 转换的路径
      "camel2DashComponentName":false,  // 设置为 false 来阻止组件名称的转换
      "style":true
    }
  ]
]

这样在 demo 项目中使用如下方式:

import { Button } from 'reactui';

就会在 babel 中编译成:

import { Button } from 'reactui/dist/components/Button';
require('reactui/dist/components/Button/style');

但是这样还有些弊端:

1、 用户在使用组件库的时候还需要安装 babel-plugin-import, 并做相关 plugins 配置;

2、 开发组件库的时候组件对应的样式文件还需要放在 style 文件夹下;

那有没有更为简单的方法呢?在 ant-design 中寻找答案,发现这样一句话 “antd 的 JS 代码默认支持基于 ES modules 的 tree shaking”。 对呀!还可以使用 webpack 的新技术“tree shaking”。

什么是 tree shaking? AST 对 JS 代码进行语法分析后得出的语法树 (Abstract Syntax Tree)。AST 语法树可以把一段 JS 代码的每一个语句都转化为树中的一个节点。DCE Dead Code Elimination,在保持代码运行结果不变的前提下,去除无用的代码。

webpack 4x 中已经使用了 tree shaking 技术,我们只需要在 package.json 文件中配置参数 "sideEffects": false,来告诉 webpack 打包的时候可以大胆的去掉没有用到的模块即可。这时用户在 demo 项目中使用组件库的时候不需要做任何处理,就可以按需引用 JS 资源了。
不知道大家在看到这里时,是否发现这样配置还是有问题的:即 sideEffects 配置成 false 是有问题的。
因为按照上述配置,就会发现组件的样式不见了!!

样式无效

经过排查,原因是引入 CSS 样式的代码:import './button.scss',可以看到相当于只是引入了样式,并不像其他 JS 模块后面做了调用,在 tree shaking 的时候,会把 css 样式去掉。所以在配置 sideEffects 就要把 CSS 文件排除掉:

"sideEffects": [
  "*.scss"
]

通过上述 tree shaking 的方法,可以实现组件库的按需加载功能,打包的文件去除了没有用到的组件代码,同时省去了用户的配置。

八、样式按需加载

通常来说,组件库的 JS 是按需加载的,但是样式文件一般只输出一个文件,即把组件库中的所有文件打包编译成一个 index.css 文件,用户在项目中引入即可;但是如果就是想做按需加载组件的样式文件,该如何去做呢?

这里我提供一种思路,由于 .tsx 文件是由 TS 编译器打包编译的,并没有处理 SCSS,所以我使用了 node-sass 来编译 SCSS 文件,如果需要按需加载 SCSS 文件,则每个组件的 index.tsx 文件中就需要引入对应的 SCSS 文件:

import Button from './button';
import './button.scss';
export default Button;

生成的 SCSS 文件也需要打包到每个组件中,而不是生成到一个文件中:

所以使用了 node-sass 中的 sass.render 函数,抽取每个文件中的样式文件,并打包编译到对应的文件中,代码如下所示:

//省略 import 引入,核心代码如下
function createCss(name){
    const lowerName = name.toLowerCase();
    sass.render({ // 调用 node-sass 函数方法,编译指定的 scss 文件到指定的路径下
        file: currPath(`../src/components/${name}/${lowerName}.scss`),
        outputStyle: 'compressed', // 进行压缩
        sourceMap: true,
    },(err,result)=>{
        if(err){
            console.log(err);
        }
        const stylePath = `../dist/components/${name}/`;
        fs.writeFile(currPath(stylePath+`/${lowerName}.scss`), result.css, function(err){
            if(err){
                console.log(err);
            }
        });
    });
}

这样就在生成的 dist 文件中的每个组件中增加了 SCSS 文件,用户通过“按需加载小节”中的方法在引入组件的时候,会调用对应的 index 文件,在 index.js 文件中就会调用对应的 SCSS 文件,从而也实现了样式文件的按需加载。

但是这样还有一个问题,就是在开发组件库的时候每个组件中的 index.tsx 文件中引入的是 SCSS 文件 import './button.scss'; ,所以 node-sass 编译后的文件需要是 SCSS 后缀的文件(虽然已经是 CSS 格式),如果生成的是 CSS 文件,则用户在使用组件的时候就会因找不到 SCSS 文件而报错,也就是用户在使用组件的时候,也需要安装 node-sass 插件。
不知大家有没有更好的办法,在组件库开发的时候使用的是 SCSS 文件,编译后生成的是 CSS 后缀的文件,在用户使用组件的中调用的也是 CSS 文件呢?欢迎在文末留言讨论~

结语

以上就是整个搭建组件库的过程,从一开始决定使用现有的 create-react-app 脚手架和 docz 来构成核心功能,到文档的网站部署和 npm 资源的发布,最初感觉应该能够快速完成整个组件库的搭建,实际上如果要想改动这些现有的库来实现自己想要的效果,还是经历了一些探索,不过整个摸索过程也是一种收获和乐趣所在,愿走过路过的小伙伴能有所收获~

参考文章

[1] NutUI 组件库: http://nutui.jd.com/#/index

[2] yep-react 组件库: http://yep-react.jd.com

[3] react 官方网站: https://reactjs.org/warnings/...

[4] storybook: https://storybook.js.org/

[5] docz: https://www.docz.site/

[6] docz 官方文档: https://www.docz.site/docs/ga...

[7] Netlify: https://app.netlify.com/teams...

[8]基于 Storybook 5 打造组件库开发与文档站建设小结: http://jelly.jd.com/article/5...

查看原文

赞 21 收藏 18 评论 1

颜海镜 关注了用户 · 4月2日

海秋 @haiqiu

前端新手

关注 14

颜海镜 关注了用户 · 4月2日

yvonne @yvonne

少年(゚∀゚ )有兴趣来鹅厂吗?欢迎投简历至yvonnexchen@tencent.com

关注 57

颜海镜 赞了文章 · 4月2日

【译】让React组件如文档般展示的6大工具

原文 6 Tools for Documenting Your React Components Like a Pro

如果没有人能够理解并找到如何使用我们的组件,那它们有什么用呢?

React鼓励我们使用组件构建模块化程序。模块化给我们带来了非常多的好处,包括提高了可重用性。然而,如果你是为了贡献和复用组件,最好得让你的组件容易被找到、理解和使用。你需要将其文档化。

目前,使用工具可以帮助我们实现自动化文档工作流程,并使我们的组件文档变得丰富、可视化和可交互。有些工具甚至将这些文档组合为共享组件的工作流程的组成部分。

为了轻而易举地将我们的组件文档化,我收集了一些业界流行的工具,如果你有推荐的组件也可以评论留言。

1. Bit

共享组件的平台

clipboard.png

Bit不仅是一个将组件文档化的工具,它还是一个开源工具,支持你使用所有文件和依赖项封装组件,并在不同应用程序中开箱即用地运行它们。
Bit,你可以跨应用地共享和协作组件,你所有共享组件都可以被发现,以便你的团队在项目中查找和使用,并轻松共享他们自己的组件。

clipboard.png
在Bit中,你共享的组件可以在你们团队中的组件共享中心找到,你可以根据上下文、bundle体积、依赖项搜索组件,你可以非常快地找到已经渲染好的组件快照,并且选择使用它们。

浏览bit.dev上的组件

clipboard.png
当你进入组件详情页时,Bit提供了一个可交互的页面实时渲染展示组件,如果该组件包含js或md代码,我们可以对其进行代码修改和相关调试。

找到想要使用的组件后,只需使用NPM或Yarn进行安装即可。你甚至可以使用Bit直接开发和安装组件,这样你的团队就可以协作并一起构建。

clipboard.png

通过Bit共享组件,就不需要再使用存储库或工具,也不需要重构或更改代码,共享、文档化、可视化组件都集中在一起,并且也能实现开箱即用。

快速上手:
Share reusable code components as a team · Bit
teambit/bit

2. StoryBook & Styleguidist

StoryBook和StyleGuidist是非常棒的项目,可以帮助我们开发独立的组件,同时可以直观地呈现和文档化它们。

clipboard.png

StoryBook 提供了一套UI组件的开发环境。它允许你浏览组件库,查看每个组件的不同状态,以及交互式开发和测试组件。在构建库时,StoryBook提供了一种可视化和记录组件的简洁方法,不同的AddOns让你可以更轻松地集成到不同的工具和工作流中。你甚至可以在单元测试中重复使用示例来确认细微差别的功能。

clipboard.png

StyleGuidist是一个独立的React组件开发环境并且具备实时编译指引。它提供了一个热重载的服务器和即时编译指引,列出了组件propTypes并展示基于.md文件的可编辑使用示例。它支持ES6,Flow和TypeScript,并且可以直接使用Create React App。自动生成的使用文档可以帮助Styleguidist作为团队不同组件的文档门户。

类似的工具还有UiZoo

3. Codesandbox, Stackblitz & friends

组件在线编译器是一种非常巧妙的展示组件和理解他们如何运行的工具。当你可以将它们组合为文档的一部分(或作为共享组件的一部分)时,在线编译器可帮助你快速了解代码的工作方式并决定是否要使用该组件。

clipboard.png

Codesandbox是一个在线编辑器,用于快速创建和展示组件等小项目。创建一些有趣的东西后,你可以通过共享网址向他人展示它。CodeSandbox具有实时预览功能,可以在你输入代码时显示运行结果,并且可以集成到你的不同工具和开发工作流程中去。

clipboard.png

Stackblitz是一个由Visual Studio Code提供支持的“Web应用程序在线IDE”。与Codesnadbox非常相似,StackBlitz是一个在线IDE,你可以在其中创建通过URL共享的Angular和React项目。与Codesandbox一样,它会在你编辑时自动安装依赖项,编译,捆绑和热重载。

其他类似工具:
11 React UI Component Playgrounds for 2019

4. Docz

clipboard.png

Docz使你可以更轻松地为你的代码构建Gtabsy支持的文档网站。它基于MDX(Markdown + JSX),即利用markdown进行组件文档化。基本上,你可以在项目的任何位置编写.mdx文件,Docz会将其转换并部署到Netlify,简化你自己设计的文档门户的过程。非常有用不是吗?
pedronauck / docz

5. MDX-docs

clipboard.png

MDX-docs允许你使用MDX和Next.js记录和开发React组件。您可以将markdown与内联JSX混合以展示React组件。像往常一样写下markdown并使用ES导入语法在文档中使用自定义组件。内置组件会将JSX代码块渲染为具有可编辑代码并提供实时预览功能,由react-live提供支持。

jxnblk / MDX-文档

6. React Docgen

clipboard.png

React DocGen是一个用于从React组件文件中提取信息的CLI和工具箱,以便生成文档。它使用ast-types@babel/parser将源解析为AST,并提供处理此AST的方法。输出/返回值是JSON blob/JavaScript对象。它通过React.createClassES2015类定义或功能(无状态组件)为React组件提供了一个默认的定义。功能十分强大。

reactjs/react-docgen
callstack/component-docs

查看原文

赞 33 收藏 19 评论 2

颜海镜 关注了用户 · 3月25日

木子星兮 @funnycoderstar

程序媛一枚,爱折腾新技术,代码洁癖。
最近发起100天前端进阶计划,欢迎关注微信公众号「牧码的星星」,每天学会一个知识点背后的原理。

关注 307

颜海镜 赞了文章 · 3月25日

JavaScript中的Event Loop(事件循环)机制

文章首次发表在 个人博客

前言

最近面试了很多家公司,这道题几乎是必被问到的一道题。之前总觉得自己了解得差不多,但是当第一次被问到的时候,却不知道该从哪里开始说起,涉及到的知识点很多。于是花时间整理了一下。并不仅仅是因为面试遇到了,而是理解JavaScript事件循环机制会让我们平常遇到的疑惑也得到解答。

一般面试官会这么问,出道题,让你说出打印结果。然后会问分别说说浏览器的node的事件循环,区别是什么,什么是宏任务和微任务,为什么要有这两种任务...

本篇文章参考了很多文章,同时加上自己的理解,如果有问题希望大家指出。

事件循环

  1. JavaScript是单线程,非阻塞的
  2. 浏览器的事件循环

    • 执行栈和事件队列
    • 宏任务和微任务
  3. node环境下的事件循环

    • 和浏览器环境有何不同
    • 事件循环模型
    • 宏任务和微任务
  4. 经典题目分析

1. JavaScript是单线程,非阻塞的

单线程:

JavaScript的主要用途是与用户互动,以及操作DOM。如果它是多线程的会有很多复杂的问题要处理,比如有两个线程同时操作DOM,一个线程删除了当前的DOM节点,一个线程是要操作当前的DOM阶段,最后以哪个线程的操作为准?为了避免这种,所以JS是单线程的。即使H5提出了web worker标准,它有很多限制,受主线程控制,是主线程的子线程。

非阻塞:通过 event loop 实现。

2. 浏览器的事件循环

执行栈和事件队列

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲 《Help, I'm stuck in an event-loop》
Help, I'm stuck in an event-loop

执行栈: 同步代码的执行,按照顺序添加到执行栈中

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
}
a();

我们可以通过使用 Loupe(Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响)工具来了解上面代码的执行情况。

调用情况

  1. 执行函数 a()先入栈
  2. a()中先执行函数 b() 函数b() 入栈
  3. 执行函数b(), console.log('b') 入栈
  4. 输出 bconsole.log('b')出栈
  5. 函数b() 执行完成,出栈
  6. console.log('a') 入栈,执行,输出 a, 出栈
  7. 函数a 执行完成,出栈。

事件队列: 异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。

我们再上面代码的基础上添加异步事件,

function a() {
    b();
    console.log('a');
}
function b() {
    console.log('b')
    setTimeout(function() {
        console.log('c');
    }, 2000)
}
a();

此时的执行过程如下
img

我们同时再加上点击事件看一下运行的过程

$.on('button', 'click', function onClick() {
    setTimeout(function timer() {
        console.log('You clicked the button!');    
    }, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
    console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");

img

简单用下面的图进行一下总结

执行栈和事件队列

宏任务和微任务

为什么要引入微任务,只有一种类型的任务不行么?

页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。

不同的异步任务被分为:宏任务和微任务
宏任务:

  • script(整体代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件

微任务:

  • new Promise().then(回调)
  • MutationObserver(html5 新特性)

运行机制

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。

在当前执行栈为空时,主线程会查看微任务队列是否有事件存在

  • 存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。
  • 如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;

当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

简单总结一下执行的顺序:
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

宏任务和微任务

深入理解js事件循环机制(浏览器篇) 这边文章中有个特别形象的动画,大家可以看着理解一下。

console.log('start')

setTimeout(function() {
  console.log('setTimeout')
}, 0)

Promise.resolve().then(function() {
  console.log('promise1')
}).then(function() {
  console.log('promise2')
})

console.log('end')

浏览器事件循环

  1. 全局代码压入执行栈执行,输出 start
  2. setTimeout压入 macrotask队列,promise.then 回调放入 microtask队列,最后执行 console.log('end'),输出 end
  3. 调用栈中的代码执行完成(全局代码属于宏任务),接下来开始执行微任务队列中的代码,执行promise回调,输出 promise1, promise回调函数默认返回 undefined, promise状态变成 fulfilled ,触发接下来的 then回调,继续压入 microtask队列,此时产生了新的微任务,会接着把当前的微任务队列执行完,此时执行第二个 promise.then回调,输出 promise2
  4. 此时,microtask队列 已清空,接下来会会执行 UI渲染工作(如果有的话),然后开始下一轮 event loop, 执行 setTimeout的回调,输出 setTimeout

最后的执行结果如下

  • start
  • end
  • promise1
  • promise2
  • setTimeout

node环境下的事件循环

和浏览器环境有何不同

表现出的状态与浏览器大致相同。不同的是 node 中有一套自己的模型。node 中事件循环的实现依赖 libuv 引擎。Node的事件循环存在几个阶段。

如果是node10及其之前版本,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask队列中的任务。

node版本更新到11之后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,跟浏览器趋于一致。下面例子中的代码是按照最新的去进行分析的。

事件循环模型

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

事件循环各阶段详解

node中事件循环的顺序

外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段...

这些阶段大致的功能如下:

  • 定时器检测阶段(timers): 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
  • I/O事件回调阶段(I/O callbacks): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
  • 闲置阶段(idle, prepare): 这个阶段仅在内部使用,可以不必理会
  • 轮询阶段(poll): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
  • 检查阶段(check): setImmediate()的回调会在这个阶段执行。
  • 关闭事件回调阶段(close callbacks): 例如socket.on('close', ...)这种close事件的回调

poll:
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。
这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
check:
该阶段执行setImmediate()的回调函数。

close:
该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。

timer阶段:
这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

I/O callback阶段:
除了以下的回调函数,其他都在这个阶段执行:

  • setTimeout()和setInterval()的回调函数
  • setImmediate()的回调函数
  • 用于关闭请求的回调函数,比如socket.on('close', ...)

宏任务和微任务

宏任务:

  • setImmediate
  • setTimeout
  • setInterval
  • script(整体代码)
  • I/O 操作等。

微任务:

  • process.nextTick
  • new Promise().then(回调)

Promise.nextTick, setTimeout, setImmediate的使用场景和区别

Promise.nextTick
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。
是所有异步任务中最快执行的。

setTimeout:
setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。

setImmediate:
setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。

经典题目分析

一. 下面代码输出什么

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

  1. 从上往下执行代码,先执行同步代码,输出 script start
  2. 遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
  3. 执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
  4. 接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
  5. 接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
  6. 依次执行微任务中的代码,依次输出 async1 endpromise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

最后的执行结果如下

  • script start
  • async1 start
  • async2
  • promise1
  • script end
  • async1 end
  • promise2
  • setTimeout

二. 下面代码输出什么

console.log('start');
setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(() => {
        console.log('children3');
    })
}, 0);

new Promise(function(resolve, reject) {
    console.log('children4');
    setTimeout(function() {
        console.log('children5');
        resolve('children6')
    }, 0)
}).then((res) => {
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0)
})

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

  1. 从上往下执行代码,先执行同步代码,输出 start
  2. 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列①中
  3. 接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的
  4. 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
  5. 宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
  6. 宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6;

最后的执行结果如下

  • start
  • children4
  • children2
  • children3
  • children5
  • children7
  • children6

三. 下面代码输出什么

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
            resolve(2)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');
  1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
  2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
  3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。

最后的执行结果如下

  • 3
  • end
  • 2
  • 4

你可以将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}


p().then((res) => {
    console.log(res);
})
console.log('end');
  • 3
  • end
  • 4
  • 1

最后强烈推荐几个非常好的讲解 event loop 的视频:

参考

其他

最近发起了一个100天前端进阶计划,主要是深挖每个知识点背后的原理,欢迎关注 微信公众号「牧码的星星」,我们一起学习,打卡100天。

查看原文

赞 78 收藏 52 评论 11

颜海镜 赞了文章 · 2月18日

前端E2E测试框架 cypress了解一下

What is E2E?

所谓的E2E就是end-to-end。
假设我们编写的每个功能程序都是一个黑匣子,最终用户也只会看到这个黑匣子,那么站在用户的角度来看并不需要知道这个黑匣子内部是什么东西也不需要知道怎么实现的,我只管知道最终效果是不是我们想要的。
那么映射到前端这边的话就是:我不管你逻辑使用什么框架什么逻辑写的,我只想知道浏览器上我要的交互效果,ui展示效果是不是正确的,功能使用上是不是正确的,那么这就叫E2E测试。

What is cypress?

打开他Github一目了然. https://github.com/cypress-io...
简单的来说。cypress是一款开箱即用,可以跑在Chrome浏览器上的测试工具。
这种情况下其实很适合开发时模拟各种场景,比如某些接口需要特定操作才可以请求到,并且请求之后又要做很多ui操作,这个时候就可以利用Cypress来模拟用户操作了,一方面可以测试代码是否正确,并且还能看到ui相应变化是否符合预期。同样,测试 fail 了也可以直接调试。

How to use it?

对于新工具的介绍我觉得所有不给出具体例子就直接抛下官方文档的行为都是耍流氓。
所以我这边给出基本使用案例,手把手教你如何做一些基本情况的测试,至于之后的进阶之路,就需要靠你自己啦!
这边为了简单起见我还是用vue-cli构建一个基本应用并在这个应用中教你怎么使用cypress。
系好安全带,老司机即将发车~

从0开始 环境搭建

1.项目初始化,安装依赖
// 首先,进入桌面,使用vue-cli创建vue项目,并安装相应依赖
vue init webpack vue-cypress-demo

// 进入项目目录
cd vue-cypress-demo

// 安装cypress
npm i cypress --save-dev
2.安装好依赖在项目根目录下创建cypress.json文件并编写配置信息
// cypress.json
{
    "baseUrl": "http://localhost:8080", // 测试域名,这里可具体项目更改
    "integrationFolder": "cypress/integration", // 测试文件存放目录
    "testFiles": "**/*.cypress.spec.js", // 根据规则匹配具体测试文件,可根据喜好任意更改
    "videoRecording": false, // 是否使用录制功能 需要的话具体去看官方介绍就好,这边暂时用不上
    "viewportHeight": 800, // 测试浏览器视口高度
    "viewportWidth": 1600 // 测试浏览器视口宽度
}
3.启动

说来你可能不信,我们已经搭建好了测试环境,那么接下来我们要来启动cypress。
由于没有装全局的cypress依赖命令行中无法识别我们的cypress命令。
故我们需要在package.json中添加scripts脚本.

// package.json
{
    "scripts": {
        "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
        "start": "npm run dev",
        "lint": "eslint --ext .js,.vue src",
        "build": "node build/build.js",
        "cypress": "cypress run",
        "cypress-gui": "cypress open"
    }
}

这时候打开命令行输入 npm run cypress-gui
之后cypress会创建一个gui界面。
初次启动会帮你创建以下文件夹,点击ok.
clipboard.png
之后我们会看到这个页面,这是因为还没匹配到对应的测试文件。 .
clipboard.png
我们可以进入cypress/integration目录创建一个test.cypress.spec.js文件,再来看我们的gui界面。

clipboard.png
这个时候我们的测试文件就可以访问了,点击之后发现他需要我们编写测试用例,那么接下来就手把手教你编写基本的测试用例。

举个?

首先我们将App.vue改造成这样.

<template>
  <div id="app">
    <h1>Hello cypress</h1>
    <div class="test">我是:{{user}}</div>
    <button class="btn" @click="test">{{btn}}</button>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'App',
  data () {
    return {
      user: '',
      btn: '测试按钮'
    }
  },
  created () {
    axios.get('http://localhost:8080/api/user.json').then(res => {
      this.user = res.data.user
    })
  },
  methods: {
    test () {
        this.btn = '点击过'
    }
  }
}
</script>
编写测试用例
// cypress/integration/test.cypress.test.js
describe('测试', () => {
    // 测试用例触发前调用的函数钩子
    before(() => {
        // 进入测试页面
        cy.visit('/');
    });

    it('测试是否包含指定文案', () => {
        cy.contains('Hello cypress');
    });
    
    it('获取指定元素', () => {
        cy.get('.test').contains('我是');
    }); 
    
    it('代理本地请求并修改成任意数据', () => {
        cy.server();
        // 拦截/api/user请求并传入自定义数据
        cy.route('/api/user', {user: 'frank'}).as('user');
        cy.visit('/');
    });
    
    it('代理本地请求并使用mock数据', () => {
        cy.server();
        // 请求本地 cypress/fixtrues/user.json文件(需要先创建) then方法可修改成任意数据,若不需要修改可不写
        cy.fixture('/user.json').then(data => data).as('fix_user');
        // 拦截/api/user请求并传入mock数据
        cy.route('/api/user', '@fix_user').as('user');
        cy.visit('/');
    });
    
    it('测试点击事件', () => {
        cy.get('.btn').click();
    });
});

你会在GUI界面中看到:

clipboard.png

以上这4种情况就是最基本的编写范例。
实际使用中肯定还会有很多种情况要判断,这边就做个简单的入门介绍,更多的api使用方式去官方文档查看即可。
官方文档: https://docs.cypress.io/api/i...

查看原文

赞 13 收藏 7 评论 1

颜海镜 关注了用户 · 2月18日

Kyrielin @frank_lin

Coding is amazing!

关注 328

颜海镜 赞了文章 · 2020-12-23

RxJS 6有哪些新变化?

RxJS 6有哪些新变化?

RxJs 6于2018年4月24日正式发布,为开发人员带来了一些令人兴奋的增补和改进。Ben Lesh, rxJS核心开发成员,强调:

  1. RxJS 6在拥有更小API的同时,带来了更整洁的引入方式
  2. 提供一个npm包,该package可以处理RxJS的向后兼容性,使得开发人员可以在不更改代码的情况下进行更新,同时还可以帮助TypeScript代码自动迁移。

RxJs 6这些新的改动为开发人员提供了以下三方面的优化:模块化方面的改进、性能提升、调试更方便。RxJs团队尽力保持新版本的向后兼容性,但是为了减少RxJs的API数量,还是引入了一些重大修改。

下面让我们一起来看一下RxJs团队在新版本中引入了哪些修改。

RxJS 6的向后兼容性

为了便捷地从RxJS 5迁移到RxJS 6,RxJS团队发布了一个名为rxjs-compat的兄弟软件包。该软件包在v6v5的API之间创建了一个兼容层。
RxJs团队建议开发人员通过安装^6.0.0版本的rxjsrxjs-compat包来升级现有应用:

npm install rxjs@6 rxjs-compat@6 --save

此包允许您在升级RxJS 6的同时继续运行现有代码库,而不会出现问题。他支持在RxJs 6中移除掉的功能。
安装rxjs-compat会导致打包后代码包体积的增加,如果你使用的是4.0.0版本以下的Webpack,该影响会被放大。
因此建议升级完成后将rxjs-compat移除。

使用rxjs-compat升级RxJS的限制

只有两个重大修改在rxjs-compat中未覆盖:

TypeScript原型操作符

在极少数情况下,您的代码库定义了它自己的TypeScript原型操作符并修改了Observable命名空间。该情况下,您需要更新你的操作符相关代码才能使TypeScript正常编译。

在版本发布说明中,用户自定义的原型操作符可按如下方式创建:

Observable.prototype.userDefined = () => {
  return new Observable((subscriber) => {
    this.subscribe({
      next(value) { subscriber.next(value); },
      error(err) { subscriber.error(err); },
      complete() { subscriber.complete(); },
   });
  });
});

source$.userDefined().subscribe();

为编译该类型的自定义操作符,需要做如下修改:

const userDefined = <T>() => (source: Observable<T>) => new Observable<T>((subscriber) => {
    this.subscribe({
      next(value) { subscriber.next(value); },
      error(err) { subscriber.error(err); },
      complete() { subscriber.complete(); },
   });
  });
});

source$.pipe(
  userDefined(),
)

同步错误处理

不再支持在try / catch块内调用Observable.subscribe()。使用用Observable.subscribe()方法中的错误回调方法替换原先的try / catch块来完成的异步错误的处理。
示例如下:

// deprecated
try {
  source$.subscribe(nextFn, undefined, completeFn);
} catch (err) {
  handleError(err);
}

// use instead
source$.subscribe(nextFn, handleError, completeFn);

现在在Observable.subscribe()中必须定义一个错误回调方法来异步处理错误。

删除RxJs兼容层前需要做的修改

如上所诉,rxjs-compat提供了V5v6API间的临时兼容层,实质上rxjs-compat为您的代码库提供了所需的v5版本功能,使得您可以逐步将您的代码库升级到v6版本。为了完成升级并移除rxjs-compat依赖,您的代码库需要重构并停止使用v5版本中的如下功能:

修改import路径

建议TypeScript开发人员使用rxjs-tslint来重构import路径。
RxJS团队设计了以下规则来帮助JavaScript开发人员重构import路径:

  • rxjs: 包含创建方法,类型,调度程序和工具库。

    import { Observable, Subject, asapScheduler, pipe, of, from, interval, merge, fromEvent } from 'rxjs';
  • rxjs/operators: 包含所有的管道操作符

    import { map, filter, scan } from 'rxjs/operators';
  • rxjs/webSocket: 包含websocket subject实现.

    import { webSocket } from 'rxjs/webSocket';
  • rxjs/ajax: 包含Rx ajax实现.

    import { ajax } from 'rxjs/ajax';
  • rxjs/testing: 包含RxJS的测试工具库.

    import { TestScheduler } from 'rxjs/testing';

以下是一项小调查:您是否有常识使用rxjs-tslint升级您的应用程序?
clipboard.png

使用管道操作而不是链式操作

使用新的管道操作符语法替换旧有的链式操作。上一个操作符方法的结果会被传递到下一个操作符方法中。
不要移除rxjs-compat包,直到你将所有的链式操作修改为管道操作符。如果您使用TypeScript, ts-lint会在某种程度上自动执行此项重构。
Ben Lesh在ng-conf 2018上解释了为什么我们应该使用管道操作符

请按照如下步骤将您的链式操作替换为管道操作:

  • rxjs-operators中引入您需要的操作符

    注意:由于与Javascript保留字冲突,以下运算符名字做了修改:do -> tap, catch ->
    catchError, switch -> switchAll, finally -> finalize
    import { map, filter, catchError, mergeMap } from 'rxjs/operators';
  • 使用pipe()包裹所有的操作符方法。确保所有操作符间的.被移除,转而使用,连接。记住!!!有些操作符的名称变了!!!
    以下为升级示例:

    // an operator chain
    source
      .map(x => x + x)
      .mergeMap(n => of(n + 1, n + 2)
        .filter(x => x % 1 == 0)
        .scan((acc, x) => acc + x, 0)
      )
      .catch(err => of('error found'))
      .subscribe(printResult);
    
    // must be updated to a pipe flow
    
    source.pipe(
      map(x => x + x),
      mergeMap(n => of(n + 1, n + 2).pipe(
        filter(x => x % 1 == 0),
        scan((acc, x) => acc + x, 0),
      )),
      catchError(err => of('error found')),
    ).subscribe(printResult);

    注意我们在以上代码中嵌套使用了pipe()

使用函数而不是类

使用函数而不是类来操作可观察对象(Observables)。所有的Observable类已被移除。他们的功能被新旧操作符及函数替代。这些替代品的功能与之前的类功能一模一样。
示例如下:

// removed
ArrayObservable.create(myArray)

// use instead

from(myArray)

// you may also use

new operator fromArray().

有关替换v5类为v6函数的完整列表,请查看RxJS文档

特殊情况

  • ConnectableObservable在v6中不能直接使用,要访问它,请使用操作符multicastpublishpublishReplaypublishLast
  • SubscribeOnObservable在v6中不能直接使用,要访问它,请使用操作符subscribeOn

移除resultSelector

Result Selectors是一项没有被广泛使用甚至没有文档说明的RxJs特性,同时Result Selectors严重的增加了RxJs代码库的体积,因此RxJs团队决定弃用或删除他。

对于使用到该功能的开发人员,他们需要将esultSelector参数替换为外部代码。

对于first(), last()这两个函数,这些参数已被移除,在删除rxjs-compat之前务必升级代码。

对于其他拥有resultSelector参数的函数,如mapping操作符,该参数已被弃用,并
以其他方式重写。如果您移除rxjs-compat,这些函数仍可正常工作,但是RxJs团队声明他们必须在v7版本发布之前将其移除。

针对该情况的更多详情,请查阅RxJs文档

其他RxJs6弃用

Observable.if and Observable.throw

Observable.if已被iif()取代,Observable.throw已被throwError()取代。您可使用rxjs-tslint将这些废弃的成员方法修改为函数调用。

代码示例如下:

OBSERVABLE.IF > IIF()

// deprecated
Observable.if(test, a$, b$);

// use instead

iif(test, a$, b$);

OBSERVABLE.ERROR > THROWERROR()

// deprecated
Observable.throw(new Error());

//use instead

throwError(new Error());

已弃用的方法

根据迁移指南,以下方法已被弃用或重构:

merge

import { merge } from 'rxjs/operators';
a$.pipe(merge(b$, c$));

// becomes

import { merge } from 'rxjs';
merge(a$, b$, c$);

concat

import { concat } from 'rxjs/operators';
a$.pipe(concat(b$, c$));

// becomes

import { concat } from 'rxjs';
concat(a$, b$, c$);

combineLatest

import { combineLatest } from 'rxjs/operators';
a$.pipe(combineLatest(b$, c$));

// becomes

import { combineLatest } from 'rxjs';
combineLatest(a$, b$, c$);

race

import { race } from 'rxjs/operators';
a$.pipe(race(b$, c$));

// becomes

import { race } from 'rxjs';
race(a$, b$, c$);

zip

import { zip } from 'rxjs/operators';
a$.pipe(zip(b$, c$));

// becomes

import { zip } from 'rxjs';
zip(a$, b$, c$);

总结

RxJS 6带来了一些重大改变,但是通过添加rxjs-compat软件包可以缓解这一问题,该软件包允许您在保持v5代码运行的同时逐渐迁移。对于Typescript用户,其他中包括大多数Angular开发人员,tslint提供了大量的自动重构功能,使转换变得更加简单。

任何升级与代码修改都会引入一些bug到代码库中。因此请务必测试您的功能以确保您的终端用户最终接受到相同的质量体验。

视频:RxJS 6详细介绍 by Ben Lesh

原文链接

查看原文

赞 17 收藏 15 评论 0

颜海镜 关注了用户 · 2020-12-16

csRyan @csryan

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.

关注 77

认证与成就

  • 获得 1298 次点赞
  • 获得 30 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 25 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • zepto.fullpage

    专注于移动端的fullPage.js

  • template.js

    template.js 一款javascript模板引擎,简单,好用,支持webpack和fis

注册于 2013-11-14
个人主页被 4.8k 人浏览