手把手教你理论搭建React组件库「超详尽」

18小时前 (16:30:45)阅读1回复0
路亚哦哦哦
路亚哦哦哦
  • 管理员
  • 注册排名7
  • 经验值104670
  • 级别管理员
  • 主题20934
  • 回复0
楼主

做者:海秋

转发链接:

概览本文包罗以下内容:

prepare: 组件库前期开发预备工做。eslint/commit lint/typescript等等;dev: 利用docz停止开发调试以及文档编写;build: umd/cjs/esm、types、polyfill 以及按需加载;test: 组件测试;release: 组件库发布流程;deploy: 利用now摆设文档站点,待填补;other: 利用plop.js快速创建组件模板。假设本文搀扶帮助到了你请给仓库 一颗 ✨✨。

假设有错误烦请在评论区斧正交换,谢谢。

仓库地址:

预备工做初始化项目新建一个happy-ui文件夹,并初始化。

mkdir happy-uicd happy-uinpm init --ymkdir components && cd components && touch index.ts # 新建源码文件夹以及进口文件代码标准此处间接利用@umijs/fabric的设置装备摆设。

yarn add @umijs/fabric --devyarn add prettier --dev # 因为@umijs/fabric没有将prettier做为依靠 所以我们需要手动安拆.eslintrc.js

module.exports = { extends: [require.resolve('@umijs/fabric/dist/eslint')],};.prettierrc.js

const fabric = require('@umijs/fabric');module.exports = { ...fabric.prettier,};.stylelintrc.js

module.exports = { extends: [require.resolve('@umijs/fabric/dist/stylelint')],};想自行设置装备摆设的同窗能够参考以下文章:

Linting Your React+Typescript Project with ESLint and Prettier!利用 ESLint+Prettier 标准 React+Typescript 项目Commit Lint停止pre-commit代码标准检测。

yarn add husky lint-staged --devpackage.json

"lint-staged": { "components/**/*.ts?(x)": [ "prettier --write", "eslint --fix", "git add" ], "components/**/*.less": [ "stylelint --syntax less --fix", "git add" ]},"husky": { "hooks": { "pre-commit": "lint-staged" }}停止 Commit Message 检测。

yarn add @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog --dev新增.commitlintrc.js写进以下内容

module.exports = {extends: ['@commitlint/config-conventional'] };package.json 写进以下内容:

// ..."scripts": { "commit": "git-cz",}// ..."husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "pre-commit": "lint-staged" }},"config": { "commitizen": { "path": "cz-conventional-changelog" }}后续利用 yarn commit 替代 git commit生陈规范的 Commit Message,当然为了效率你能够抉择手写,但是要契合标准。

TypeScriptyarn add typescript --dev新建tsconfig.json并写进以下内容

{ "compilerOptions": { "baseUrl": "./", "target": "esnext", "module": "commonjs", "jsx": "react", "declaration": true, "declarationDir": "lib", "strict": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "resolveJsonModule": true }, "include": ["components", "global.d.ts"], "exclude": ["node_modules"]}测试在components文件夹下新建alert文件夹,目次构造如下:

alert ├── alert.tsx # 源文件 ├── index.ts # 进口文件 ├── interface.ts # 类型声明文件 └── style ├── index.less # 款式文件 └── index.ts # 款式文件里为什么存在一个index.ts - 按需加载款式 治理款式依靠 后面章节会提到安拆React相关依靠:

yarn add react react-dom @types/react @types/react-dom --dev # 开发时依靠,宿主情况必然存在yarn add prop-types # 运行时依靠,宿主情况可能不存在 安拆本组件库时一路安拆此处照旧安拆了prop-types那个库,因为无法包管宿主情况也利用typescript,从而可以停止静态查抄,故利用prop-types包管javascript用户也能得到友好的运行时报错信息。

components/alert/interface.ts

export type Kind = 'info' | 'positive' | 'negative' | 'warning'export type KindMap = RecordKind, string;export interface AlertProps { /** * Set this to change alert kind * @default info */ kind?: 'info' | 'positive' | 'negative' | 'warning'}components/alert/alter.tsx

import React from 'react'import t from 'prop-types'import { AlertProps, KindMap } from './interface'const prefixCls = 'happy-alert'const kinds: KindMap = { info: '#5352ED', positive: '#2ED573', negative: '#FF4757', warning: '#FFA502',};const Alert: React.FCAlertProps = ({ children, kind = 'info', ...rest }) = ( div className={prefixCls} style={{ background: kinds[kind], }} {...rest} {children} /div);Alert.propTypes = { kind: t.oneOf(['info', 'positive', 'negative', 'warning']),};export default Alert;components/alert/index.ts

import Alert from './alert'export default Alert;export * from './interface'components/alert/style/index.less

@popupPrefix: happy-alert;.@{popupPrefix} { padding: 20px; background: white; border-radius: 3px; color: white;}components/alert/style/index.ts

import'./index.less'components/index.ts

export{defaultasAlert }from'./alert'此处组件参考的docz项目typescript以及less示例。

git 一把锁,能够看到掌握台已经停止钩子检测了。

git add .yarn commit # 或 git commit -m'feat: chapter-1 预备工做'git push预备工做完成。代码能够在仓库的chapter-1分收获取,若存在与本文内容不符的处所,以master分收以及文章为准。

开发与调试本节处理开发组件时的预览以及调试问题,顺路处理文档编写。

此处抉择docz来辅助预览调试。

docz基于MDX(Markdown + JSX),能够在 Markdown 中引进 React 组件,使得一边编写文档,一边预览调试成为了可能。并且得益于 React 组件生态,我们能够像编写利用一般编写文档,不单单是枯燥的文字。docz 也内置了一些组件,好比Playground。

安拆 docz 以及自定义设置装备摆设yarn add docz --devyarn add rimraf --dev # 清空目次的一个辅助库增加 npm scripts 至 package.json。

"scripts": { "dev": "docz dev", // 启动当地开发情况 "start": "npm run dev", // dev号令别号 "build:doc": "rimraf doc-site && docz build", // 后续会设置装备摆设打包出来的文件目次名为doc-site,故每次build前删除 "preview:doc": "docz serve" // 预览文档站点},重视:本节所有操做都是针对站点利用。打包指代文档站点打包,而非组件库。

新建doczrc.js设置装备摆设文件,并写进以下内容:

doczrc.js

export default { files: './components/**/*.{md,markdown,mdx}', // 识此外文件后缀 dest: 'doc-site', // 打包出来的文件目次名 title: 'happy-ui', // 站点题目 typescript: true, // 组件源文件是通过typescript开发,需要翻开此选项};因为利用了less做为款式预处置器,故需要安拆 less 插件。

yarn add less gatsby-plugin-less --dev新建gatsby-config.js,并写进以下内容:

gatsby-config.js

module.exports = { plugins: ['gatsby-theme-docz', 'gatsby-plugin-less'],};编写文档新建components/alert/index.mdx,并写进以下内容:

---name: Alert 警告提醒route: /Alertmenu: 组件---import { Playground } from 'docz' import Alert from './alert' // 引进组件 import './style' // 引进组件款式# Alert 警告提醒警告提醒,展示需要存眷的信息。## 代码演示### 根本用法Playground Alert kind="warning"那是一条警告提醒/Alert/Playground## API| 属性 | 阐明 | 类型 | 默认值 || ---- | -------- | -------------------------------------------- | ------ || kind | 警告类型 | 'info'/'positive'/'negative'/'warning'非必填 | 'info' |施行脚本号令:

yarn start# or yarn dev能够在localhost:3000看到如下页面 :

如今能够在index.mdx中愉快地停止文档编写和调试了!

假使本文到了那里就完毕(其实也能够完毕了(_^▽^_)),那我只是官方文档的翻译复读机罢了,有兴致的同窗能够陆续向下看。

优化文档编写假设代码演示部门的demo较多(好比根本用法、高级用法以及各类用法等等),在组件复杂的情状下(事实Alert/实在太简单了),会招致文档很长难以庇护,你到底是在写文档呢仍是在写代码呢?

那就抽离吧。

在components/alert/文件夹下新建demo文件夹,存放我们在编写文档时需要引用的 demo。

components/alert/demo/1-demo-basic.tsx

import React from 'react'import Alert from '../alert'import '../style'export default () = Alert kind="warning"/Alert;components/alert/index.mdx

- import Alert from './alert' // 引进组件- import './style' // 引进组件款式+ import BasicDemo from './demo/1-demo-basic'...Playground- Alert kind="warning"那是一条警告提醒/Alert+ BasicDemo //Playground如许我们就将 demo 与文档停止了分隔。预览如下:

等等,下面展现的是BasicDemo /,而非demo源码。

Playground /组件暂时无法撑持上述形式的展现:自定义下方展现的代码,而非Playground /内部的代码。相关讨论如下:

Allow to hide the LiveError overlay #907Allow to override the playground's editor's code #906其实第一条 PR 已经处理了问题,但是被封闭了,无法。

不外既然都能引进 React 组件了,在MDX的情况下自定义一个Playground组件又有何难呢,无非就是衬着组件(MDX 自带)和展现源码,简单开放的工具各人都是喜闻乐见的,就喊HappyBox吧。

优化代码展现编写 HappyBox /组件安拆依靠:

yarn add react-usereact-tooltip react-feather react-simple-code-editor prismjs react-copy-to-clipboardraw-loader styled-components--devreact-use - 2020 年了,当然要用hooksreact-simple-code-editor - 代码展现区域prismjs - 代码高亮raw-loader - 将源码转成字符串react-copy-to-clipboard - 让用户爸爸们可以 copy demo 代码react-tooltip/react-feather 辅助组件styled-components 便利在文档示例中让用户看到款式,也用做文档组件的款式处置那些依靠都是办事于文档站点利用,和组件库本身毫无联系关系。

最末效果如下:

根目次下新建doc-comps文件夹,存放文档中利用的一些东西组件,好比HappyBox /。

doc-comps

├── happy-box│ ├── style.ts│ └── index.tsx└── index.tscomponents/doc-comps/happy-box/index.tsx

import React from 'react'import Editor from 'react-simple-code-editor'import CopyToClipboard from 'react-copy-to-clipboard'import { useToggle } from 'react-use'import ReactTooltip from 'react-tooltip'import IconCopy from 'react-feather/dist/icons/clipboard'import IconCode from 'react-feather/dist/icons/code'import { highlight, languages } from 'prismjs/components/prism-core'import { StyledContainer, StyledIconWrapper } from './style'import 'prismjs/components/prism-clike'import 'prismjs/components/prism-javascript'import 'prismjs/components/prism-markup'require('prismjs/components/prism-jsx');interface Props { code: string; title?: React.ReactNode; desc?: React.ReactNode;}export const HappyBox: React.FCProps = ({ code, title, desc, children }) = { const [isEditVisible, toggleEditVisible] = useToggle(false); return ( StyledContainer section className="code-box-demo" {children}/section section className="code-box-meta" div className="text-divider" span{title || '示例'}/span /div div className="code-box-description" p{desc || '暂无描述'}/p /div div className="divider" / div className="code-box-action" CopyToClipboard text={code} onCopy={() = alert('复造胜利')} IconCopy data-place="top" data-tip="复造代码" / /CopyToClipboard StyledIconWrapper onClick={toggleEditVisible} IconCode data-place="top" data-tip={isEditVisible ? '收起代码' : '展现代码'} / /StyledIconWrapper /div /section {renderEditor()} ReactTooltip / /StyledContainer ); function renderEditor() { if (!isEditVisible) return null; return ( div className="container_editor_area" Editor readOnly value={code} onValueChange={() = {}} highlight={code = highlight(code, languages.jsx)} padding={10} className="container__editor" style={{ fontFamily: '"Fira code", "Fira Mono", monospace', fontSize: 14, }} / /div ); }};export default HappyBox;相关设置装备摆设变动增加 alias别号,样例源码展现相对途径不敷友好,让用户间接拷贝才够省心新建gatsby-node.js,写进以下内容以开启alias:

const path = require('path');exports.onCreateWebpackConfig = args = { args.actions.setWebpackConfig({ resolve: { modules: [path.resolve(__dirname, '../src'), 'node_modules'], alias: { 'happy-ui/lib': path.resolve(__dirname, '../components/'), 'happy-ui/esm': path.resolve(__dirname, '../components/'), 'happy-ui': path.resolve(__dirname, '../components/'), }, }, });};tsconfig.json 打包时需要漠视demo,制止组件库打包生成types时包罗此中,同时增加paths属性用于 vscode 主动提醒:

tsconfig.json

{ "compilerOptions": { "baseUrl": "./",+ "paths": {+ "happy-ui": ["components/index.ts"],+ "happy-ui/esm/*": ["components/*"],+ "happy-ui/lib/*": ["components/*"]+ }, "target": "esnext", "module": "commonjs", "jsx": "react", "declaration": true, "declarationDir": "lib", "strict": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "resolveJsonModule": true }, "include": ["components", "global.d.ts"],- "exclude": ["node_modules"]+ "exclude": ["node_modules", "**/demo/**"]}新的问题呈现了,vscode 的 alias 提醒依靠 tsconfig.json,漠视 demo 文件夹后,demo 内的文件模块类型找不到声明(paths 失效),所以不克不及将 demo 在 tsconfig.json 中移除:

{- "exclude": ["node_modules", "**/demo/**"]+ "exclude": ["node_modules"]}新建一个 tsconfig.build.json 文件:

tsconfig.build.json

{ "extends": "./tsconfig.json", "exclude": ["**/demo/**", "node_modules"]}后续利用 tsc 生成类型声明文件指定tsconfig.build.json即可。

革新相关文件components/alert/demo/1-demo-basic.tsx

- import Alert from '../alert'+ import Alert from 'happy-ui/lib/alert'- import '../style'+ import 'happy-ui/lib/alert/style'components/alert/index.mdx

- import { Playground } from 'docz'+ import { HappyBox } from '../../doc-comps'+ import BasicDemoCode from '!raw-loader!./demo/1-demo-basic.tsx'...- Playground- BasicDemo /- /Playground+ HappyBox code={BasicDemoCode} title="根本用法" desc="利用kind掌握Alert类型"+ BasicDemo /+ /HappyBoxyarn start卡住时测验考试删除根目次.docz文件夹,然后从头施行号令。

如今能够愉快地开发组件了。代码能够在仓库的chapter-2分收获取,若存在与本文内容不符的处所,以master分收以及文章为准。

组件库打包宿主情况各不不异,需要将源码停止相关处置后发布至 npm。

明白以下目标:

导出类型声明文件导出 umd/Commonjs module/ES module 等 3 种形式供利用者引进撑持款式文件 css 引进,而非只要less撑持按需加载导出类型声明文件既然是利用typescript编写的组件库,那么利用者应当享遭到类型系统的益处。

我们能够生成类型声明文件,并在package.json中定义进口,如下:

package.json

{ "typings": "lib/index.d.ts", // 定义类型进口文件 "scripts": { "build:types": "tsc -p tsconfig.build.json && cpr lib esm" // 施行tsc号令生成类型声明文件 }}值得重视的是:此处利用cpr(需要手动安拆)将lib的声明文件拷贝了一份,并将文件夹重定名为esm,用于后面存放 ES module 形式的组件。如许做的原因是包管用户手动按需引进组件时照旧能够获取主动提醒。

最起头的体例是将声明文件零丁存放在types文件夹,但如许只要通过'happy-ui'引进才能够获取提醒,而'happy-ui/esm/xxx'和'happy-ui/lib/xxx'就无法获取提醒。

tsconfig.build.json

{ "extends": "./tsconfig.json", "compilerOptions": { "emitDeclarationOnly": true }, // 只生成声明文件 "exclude": ["**/__tests__/**", "**/demo/**", "node_modules", "lib", "esm"] // 肃清示例、测试以及打包好的文件夹}施行yarn build:types,能够发现根目次下已经生成了lib文件夹(tsconfig.json中定义的declarationDir字段),目次构造与components文件夹连结一致,如下:

types

├── alert│ ├── alert.d.ts│ ├── index.d.ts│ ├── interface.d.ts│ └── style│ └── index.d.ts└── index.d.ts如许利用者引进npm 包时,便能得到主动提醒,也可以复用相关组件的类型定义。

接下来将ts(x)等文件处置成js文件。

需要重视的是,我们需要输出Commonjs module以及ES module两种模块类型的文件(暂不考虑umd),以下利用cjs指代Commonjs module,esm指代ES module。br/ 对此有疑问的同窗选举阅读:import、require、export、module.exports 混合详解

导出 Commonjs 模块其实完全能够利用babel或tsc号令行东西停止代码编译处置(现实上良多东西库就是如许做的),但考虑到还要处置款式及其按需加载,我们借助 gulp 来串起那个流程。

babel 设置装备摆设起首安拆babel及其相关依靠

yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-transform-runtime --devyarn add @babel/runtime-corejs3新建.babelrc.js文件,写进以下内容:

.babelrc.js

module.exports = { presets: ['@babel/env', '@babel/typescript', '@babel/react'], plugins: [ '@babel/proposal-class-properties', [ '@babel/plugin-transform-runtime', { corejs: 3, helpers: true, }, ], ],};关于@babel/plugin-transform-runtime与@babel/runtime-corejs3:

若helpers选项设置为true,可抽离代码编译过程反复生成的 helper 函数(classCallCheck,extends等),减小生成的代码体积;若corejs设置为3,可引进不污染全局的按需polyfill,常用于类库编写(我更选举:不引进polyfill,转而告知利用者需要引进何种polyfill,制止反复引进或产生抵触,后面会详尽提到)。更多拜见官方文档-@babel/plugin-transform-runtime

设置装备摆设目标情况

为了制止转译阅读器原生撑持的语法,新建.browserslistrc文件,根据适配需求,写进撑持阅读器范畴,感化于@babel/preset-env。

.browserslistrc

0.2%not deadnot op_mini all很遗憾的是,@babel/runtime-corejs3无法在按需引进的根底上根据目标阅读器撑持水平再次削减polyfill的引进,拜见@babel/runtime for target environment 。

那意味着@babel/runtime-corejs3 以至会在针对现代引擎的情状下注进所有可能的 polyfill:没必要要地增加了最末绑缚包的大小。

关于组件库(代码量可能很大),小我定见将polyfill的抉择权交还给利用者,在宿主情况停止polyfill。若利用者具有兼容性要求,天然会利用@babel/preset-env + core-js + .browserslistrc停止全局polyfill,那套组合拳引进了更低目标阅读器不撑持API的全数 polyfill。

营业开发中,将@babel/preset-env的useBuiltIns选项值设置为 usage,同时把node_modules从babel-loader中exclude掉的同窗可能想要那个特征:"useBuiltIns: usage" for node_modules without transpiling #9419,在未撑持该issue提到的内容之前,仍是乖乖地将useBuiltIns设置为entry,或者不要把node_modules从babel-loader中exclude。

所以组件库不消画蛇添足,引进余外的polyfill,写好文档阐明,比什么都重要(就像zent和antd如许)。

如今@babel/runtime-corejs3改换为@babel/runtime,只停止helper函数抽离。

yarn remove @babel/runtime-corejs3yarn add @babel/runtime.babelrc.js

module.exports = { presets: ['@babel/env', '@babel/typescript', '@babel/react'], plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'],};@babel/transform-runtime的helper选项默认为true。

gulp 设置装备摆设再来安拆gulp相关依靠

yarn add gulp gulp-babel --dev新建gulpfile.js,写进以下内容:

gulpfile.js

const gulp = require('gulp');const babel = require('gulp-babel');const paths = { dest: { lib: 'lib', // commonjs 文件存放的目次名 - 本块存眷 esm: 'esm', // ES module 文件存放的目次名 - 暂时不关心 dist: 'dist', // umd文件存放的目次名 - 暂时不关心 }, styles: 'components/**/*.less', // 款式文件途径 - 暂时不关心 scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'], // 脚本文件途径};function compileCJS() { const { dest, scripts } = paths; return gulp .src(scripts) .pipe(babel()) // 利用gulp-babel处置 .pipe(gulp.dest(dest.lib));}// 并行使命 后续加进款式处置 能够并行处置const build = gulp.parallel(compileCJS);exports.build = build;exports.default = build;修改package.json

package.json

{- "main": "index.js",+ "main": "lib/index.js", "scripts": { ...+ "clean": "rimraf lib esm dist",+ "build": "npm run clean && npm run build:types && gulp", ... },}施行yarn build,得到如下内容:

lib

├── alert│ ├── alert.js│ ├── index.js│ ├── interface.js│ └── style│ └── index.js└── index.js看察编译后的源码,能够发现:诸多helper办法已被抽离至@babel/runtime中,模块导进导出形式也是commonjs标准。

lib/alert/alert.js

导出 ES module生成ES module能够更好地停止tree shaking,基于上一步的babel设置装备摆设,更新以下内容:

设置装备摆设@babel/preset-env的modules选项为false,封闭模块转换;设置装备摆设@babel/plugin-transform-runtime的useESModules选项为true,利用ES module形式引进helper函数。.babelrc.js

module.exports = { presets: [ [ '@babel/env', { modules: false, // 封闭模块转换 }, ], '@babel/typescript', '@babel/react', ], plugins: [ '@babel/proposal-class-properties', [ '@babel/plugin-transform-runtime', { useESModules: true, // 利用esm形式的helper }, ], ],};目标达成,我们再利用情况变量区分esm和cjs(施行使命时设置对应的情况变量即可),最末babel设置装备摆设如下:

.babelrc.js

module.exports = { presets: ['@babel/env', '@babel/typescript', '@babel/react'], plugins: ['@babel/plugin-transform-runtime', '@babel/proposal-class-properties'], env: { esm: { presets: [ [ '@babel/env', { modules: false, }, ], ], plugins: [ [ '@babel/plugin-transform-runtime', { useESModules: true, }, ], ], }, },};接下来修改gulp相关设置装备摆设,抽离compileScripts使命,增加compileESM使命。

gulpfile.js

// .../** * 编译脚本文件 * @param {string} babelEnv babel情况变量 * @param {string} destDir 目标目次 */function compileScripts(babelEnv, destDir) { const { scripts } = paths; // 设置情况变量 process.env.BABEL_ENV = babelEnv; return gulp .src(scripts) .pipe(babel()) // 利用gulp-babel处置 .pipe(gulp.dest(destDir));}/** * 编译cjs */function compileCJS() { const { dest } = paths; return compileScripts('cjs', dest.lib);}/** * 编译esm */function compileESM() { const { dest } = paths; return compileScripts('esm', dest.esm);}// 串行施行编译脚本使命(cjs,esm) 制止情况变量影响const buildScripts = gulp.series(compileCJS, compileESM);// 整体并行施行使命const build = gulp.parallel(buildScripts);// ...施行yarn build,能够发现生成了lib/esm三个文件夹,看察esm目次,构造同lib一致,js 文件都是以ES module模块形式导进导出。

esm/alert/alert.js

别忘了给package.json增加相关进口。

package.json

{+ "module": "esm/index.js"}处置款式文件拷贝 less 文件我们会将less文件包罗在npm包中,用户能够通过happy-ui/lib/alert/style/index.js的形式按需引进less文件,此处能够间接将 less 文件拷贝至目标文件夹。

在gulpfile.js中新建copyLess使命。

gulpfile.js

// .../** * 拷贝less文件 */function copyLess() { return gulp .src(paths.styles) .pipe(gulp.dest(paths.dest.lib)) .pipe(gulp.dest(paths.dest.esm));}const build = gulp.parallel(buildScripts, copyLess);// ...看察lib目次,能够发现 less 文件已被拷贝至alert/style目次下。

lib

├── alert│ ├── alert.js│ ├── index.js│ ├── interface.js│ └── style│ ├── index.js│ └── index.less # less文件└── index.js可能有些同窗已经发现问题:若利用者没有利用less预处置器,利用的是sass计划以至原生css计划,那如今计划就搞不定了。经阐发,有以下 3 种预选计划:

告知用户增加less-loader;打包出一份完全的 css 文件,停止全量引进;零丁供给一份style/css.js文件,引进的是组件 css款式文件依靠,而非 less 依靠,组件库底层抹平差别;利用css in js计划。计划 1 会招致营业方利用成本增加。

计划 2 无法停止按需引进。

计划 4 需要详尽聊聊。

css in js除了付与款式编写更多的可能性之外,在编写第三方组件库时更是利器。

假设我们写一个react-use那种hooks东西库,不涉及到款式,只需要在package.json中设置sideEffects为false,营业方利用 webpack 停止打包时,只会打包被利用到的 hooks(优先利用 ES module)。

进口文件index.js中导出的但未被利用的其他 hooks 会被tree shaking,第一次利用那个库的时候我很猎奇,为什么没有按需引进的利用体例,成果打包阐发时我傻了,本来人家生成撑持按需引进。

可能常用的antd以及lodash都要配一配,招致产生了惯性思维。

回到正题。假设将款式利用javascript来编写,在某种维度上讲,组件库和东西库一致了,配好sideEffects,主动按需引进,美滋滋。

并且每个组件都与本身的款式绑定,不需要营业方或组件开发者往庇护款式依靠,什么是款式依靠,后面会讲到。

缺点:

款式无法零丁缓存;styled-components 本身体积较大;复写组件款式需要利用属性抉择器或者利用styled-components,费事了点。需要看取舍了,偷偷说一句styled-components做主题定造也极其便利。

计划 3 是antd利用的那种计划。

在搭建组件库的过程中,有一个问题困扰了我很久:为什么需要alert/style/index.js引进less文件或alert/style/css.js引进css文件?

谜底是治理款式依靠。

因为我们的组件是没有引进款式文件的,需要用户往手动引进。

假设存在以下场景:引进Button /,Button /依靠了Icon /,利用者需要手动往引进挪用的组件的款式(Button /)及其依靠的组件款式(Icon /),碰着复杂组件极其费事,所以组件库开发者能够供给一份如许的js文件,利用者手动引进那个js文件,就能引进对应组件及其依靠组件的款式。

那么问题又来了,为什么组件不克不及本身往import './index.less'呢?

能够,不外营业方要设置装备摆设less-loader,什么,营业方不想配,要你import './index.css'?

能够,营业方爽了,组件开发方不爽。

所以我们要找一个各人都爽的计划:

开发方可以高兴的利用预处置器;营业方不需要额外的利用成本。谜底就是css in js零丁供给一份style/css.js文件,引进的是组件 css款式文件依靠,而非 less 依靠,组件库底层抹平差别。

之前领会到father能够在打包的时候将index.less转成index.css,那却是个好法子,但是一些反复引进的款式模块(好比动画款式),会被反复打包,不晓得有没有好的处理计划。

生成 css 文件安拆相关依靠。

yarn add gulp-less gulp-autoprefixer gulp-cssnano --dev将less文件生成对应的css文件,在gulpfile.js中增加less2css使命。

// .../** * 生成css文件 */function less2css() { return gulp .src(paths.styles) .pipe(less()) // 处置less文件 .pipe(autoprefixer()) // 根据browserslistrc增加前缀 .pipe(cssnano({ zindex: false, reduceIdents: false })) // 压缩 .pipe(gulp.dest(paths.dest.lib)) .pipe(gulp.dest(paths.dest.esm));}const build = gulp.parallel(buildScripts, copyLess, less2css);// ...施行yarn build,组件style目次下已经存在css文件了。

接下来我们需要一个alert/style/css.js来帮用户引进css文件。

生成 css.js此处参考antd-tools的实现体例:在处置scripts使命中,截住style/index.js,生成style/css.js,并通过正则将引进的less文件后缀改成css。

安拆相关依靠。

yarn add through2 --devgulpfile.js

// .../** * 编译脚本文件 * @param {*} babelEnv babel情况变量 * @param {*} destDir 目标目次 */function compileScripts(babelEnv, destDir) { const { scripts } = paths; process.env.BABEL_ENV = babelEnv; return gulp .src(scripts) .pipe(babel()) // 利用gulp-babel处置 .pipe( through2.obj(function z(file, encoding, next) { this.push(file.clone()); // 找到目标 if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) { const content = file.contents.toString(encoding); file.contents = Buffer.from(cssInjection(content)); // 文件内容处置 file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重定名 this.push(file); // 新增该文件 next(); } else { next(); } }), ) .pipe(gulp.dest(destDir));}// ...cssInjection的实现:

gulpfile.js

/** * 当前组件款式 import './index.less' = import './index.css' * 依靠的其他组件款式 import '../test-comp/style' = import '../test-comp/style/css.js' * 依靠的其他组件款式 import '../test-comp/style/index.js' = import '../test-comp/style/css.js' * @param {string} content */function cssInjection(content) { return content .replace(/\/style\/?'/g, "/style/css'") .replace(/\/style\/?"/g, '/style/css"') .replace(/\.less/g, '.css');}再停止打包,能够看见组件style目次下生成了css.js文件,引进的也是上一步less转换而来的css文件。

lib/alert

├── alert.js├── index.js├── interface.js└── style ├── css.js # 引进index.css ├── index.css ├── index.js └── index.less按需加载在 package.json 中增加sideEffects属性,共同ES module到达tree shaking效果(将款式依靠文件标注为side effects,制止被误删除)。

// ..."sideEffects": [ "dist/*", "esm/**/style/*", "lib/**/style/*", "*.less"],// ...利用以下体例引进,能够做到js部门的按需加载,但需要手动引进款式:

import { Alert } from 'happy-ui'import 'happy-ui/esm/alert/style'也能够利用以下体例引进:

import Alert from 'happy-ui/esm/alert' // or import Alert from 'happy-ui/lib/alert'import 'happy-ui/esm/alert/style' // or import Alert from 'happy-ui/lib/alert'以上引进款式文件的体例不太文雅,间接进口处引进全量款式文件又和按需加载的本意相往甚远。

利用者能够借助babel-plugin-import来停止辅助,削减代码编写量(说好的不加进其他利用成本的呢~)。

import{ Alert }from'happy-ui'⬇️

import Alert from 'happy-ui/lib/alert'import 'happy-ui/lib/alert/style'生成 umd没用上,那一块标识表记标帜为 todo 吧。

本节代码能够在仓库的chapter-3分收获取,若存在与本文内容不符的处所,以master分收以及文章为准。

组件测试与软件操做行为越接近的测试,越能赐与你自信心。

本节次要讲述若何在组件库中引进jest以及@testing-library/react,而不会深进单位测试的进修。

假设你对下列问题感兴致:

What-单位测试是什么?Why-为什么要写单位测试?How-编写单位测试的更佳理论?那么能够看看以下文章:

Test React apps with React Testing Library:通过一个Counter /的例子延伸,论述了抉择React Testing Library而非Enzyme的理由,并对其停止了一些进门教学;React Testing Library:@testing-library/react的官方文档,该库供给的 API 在某个水平上就是在指引开发者停止单位测试的更佳理论;React Testing Library-examples:@testing-library/react的一些实例,供给了各类常见场景的测试;React 单位测试战略及落地:如题目所示,值得一看。相关设置装备摆设安拆依靠:

yarn add jest ts-jest @testing-library/react @testing-library/jest-dom identity-obj-proxy @types/jest @types/testing-library__react --devjest: JavaScript 测试框架,专注于简洁明快;ts-jest:为TypeScript编写jest测试用例供给撑持;@testing-library/react:简单而完全的React DOM测试东西,鼓舞优良的测试理论;@testing-library/jest-dom:自定义的jest婚配器(matchers),用于测试DOM的形态(即为jest的except办法返回值增加更多专注于DOM的matchers);identity-obj-proxy:一个东西库,此处用来mock款式文件。新建jest.config.js,并写进相关设置装备摆设,更多设置装备摆设可参考jest 官方文档-设置装备摆设,只看几个常用的就能够。

jest.config.js

module.exports = { verbose: true, roots: ['rootDir/components'], moduleNameMapper: { '\\.(css|less|scss)$': 'identity-obj-proxy', '^components$': 'rootDir/components/index.tsx', '^components(.*)$': 'rootDir/components/$1', }, testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], testPathIgnorePatterns: ['/node_modules/', '/lib/', '/esm/', '/dist/'], preset: 'ts-jest', testEnvironment: 'jsdom',};修改package.json,增加测试相关号令,而且代码提交前,跑测试用例,如下:

package.json

"scripts": { ...+ "test": "jest", # 施行jest+ "test:watch": "jest --watch", # watch形式下施行+ "test:coverage": "jest --coverage", # 生成测试笼盖率陈述+ "test:update": "jest --updateSnapshot" # 更新快照},..."lint-staged": { "components/**/*.ts?(x)": [ "prettier --write", "eslint --fix",+ "jest --bail --findRelatedTests", "git add" ], ...}修改gulpfile.js以及tsconfig.json,制止打包时,把测试文件一并处置了。

gulpfile.js

const paths = { ...- scripts: ['components/**/*.{ts,tsx}', '!components/**/demo/*.{ts,tsx}'],+ scripts: [+ 'components/**/*.{ts,tsx}',+ '!components/**/demo/*.{ts,tsx}',+ '!components/**/__tests__/*.{ts,tsx}',+ ],};tsconfig.json

{- "exclude": ["components/**/demo"]+ "exclude": ["components/**/demo", "components/**/__tests__"]}编写测试用例Alert /比力简单,此处只做示例用,简单停止一下快照测试。

在对应组件的文件夹下新建__tests__文件夹,用于存放测试文件,其内新建index.test.tsx文件,写进以下测试用例:

components/alert/tests/index.test.tsx

import React from 'react'import { render } from '@testing-library/react'import Alert from '../alert'describe('Alert /', () = { test('should render default', () = { const { container } = render(Alertdefault/Alert); expect(container).toMatchSnapshot(); }); test('should render alert with type', () = { const kinds: any[] = ['info', 'warning', 'positive', 'negative']; const { getByText } = render( {kinds.map(k = ( Alert kind={k} key={k} {k} /Alert ))} /, ); kinds.forEach(k = { expect(getByText(k)).toMatchSnapshot(); }); });});更新一下快照:

yarntest:update能够看见同级目次下新增了一个__snapshots__文件夹,里面存放对应测试用例的快照文件。

再施行测试用例:

yarntest

能够发现我们通过了测试用例。。。额,那里当然能通过,次要是后续我们停止迭代重构时,城市从头施行测试用例,与比来的一次快照停止比对,假设与快照纷歧致(构造发作了改动),那么响应的测试用例就无法通过。

关于快照测试,批驳纷歧,那个例子也实在简单得很,以至连扩展的 jest-dom供给的 matchers 都没用上。

若何编写优良的测试用例,我也是一个新手,只能说多看多写多测验考试,前面选举的文章很不错。

本节代码能够在仓库的chapter-4分收获取,若存在与本文内容不符的处所,以master分收以及文章为准。

原则化发布流程本节次要是讲解若何通过一行号令完成以下六点内容:

版本更重生成 CHANGELOG推送至 git 仓库组件库打包发布至 npm打 tag 并推送至 git假设你不想代码,很好,用np(假设我一起头就晓得那个东西,我也不会往写代码,我实傻,实的)。

package.json

"scripts": {+ "release": "ts-node ./scripts/release.ts"},间接甩代码吧,其实不复杂。

/* eslint-disable import/no-extraneous-dependencies,@typescript-eslint/camelcase, no-console */import inquirer from 'inquirer'import fs from 'fs'import path from 'path'import child_process from 'child_process'import util from 'util'import chalk from 'chalk'import semverInc from 'semver/functions/inc'import { ReleaseType } from 'semver'import pkg from '../package.json'const exec = util.promisify(child_process.exec);const run = async (command: string) = { console.log(chalk.green(command)); await exec(command);};const currentVersion = pkg.version;const getNextVersions = (): { [key in ReleaseType]: string | null } = ({ major: semverInc(currentVersion, 'major'), minor: semverInc(currentVersion, 'minor'), patch: semverInc(currentVersion, 'patch'), premajor: semverInc(currentVersion, 'premajor'), preminor: semverInc(currentVersion, 'preminor'), prepatch: semverInc(currentVersion, 'prepatch'), prerelease: semverInc(currentVersion, 'prerelease'),});const timeLog = (logInfo: string, type: 'start' | 'end') = { let info = '' if (type === 'start') { info = `= 起头使命:${logInfo}`; } else { info = `✨ 完毕使命:${logInfo}`; } const nowDate = new Date(); console.log( `[${nowDate.toLocaleString()}.${nowDate .getMilliseconds() .toString() .padStart(3, '0')}] ${info} `, );};/** * 获取下一次版本号 */async function prompt(): Promisestring { const nextVersions = getNextVersions(); const { nextVersion } = await inquirer.prompt([ { type: 'list', name: 'nextVersion', message: `请抉择将要发布的版本 (当前版本 ${currentVersion})`, choices: (Object.keys(nextVersions) as ArrayReleaseType).map(level = ({ name: `${level} = ${nextVersions[level]}`, value: nextVersions[level], })), }, ]); return nextVersion;}/** * 更新版本号 * @param nextVersion 新版本号 */async function updateVersion(nextVersion: string) { pkg.version = nextVersion; timeLog('修改package.json版本号', 'start'); await fs.writeFileSync(path.resolve(__dirname, './../package.json'), JSON.stringify(pkg)); await run('npx prettier package.json --write'); timeLog('修改package.json版本号', 'end');}async function generateChangelog() { timeLog('生成CHANGELOG.md', 'start'); await run(' npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0'); timeLog('生成CHANGELOG.md', 'end');}/** * 将代码提交至git */async function push(nextVersion: string) { timeLog('推送代码至git仓库', 'start'); await run('git add package.json CHANGELOG.md'); await run(`git commit -m "v${nextVersion}" -n`); await run('git push'); timeLog('推送代码至git仓库', 'end');}/** * 组件库打包 */async function build() { timeLog('组件库打包', 'start'); await run('npm run build'); timeLog('组件库打包', 'end');}/** * 发布至npm */async function publish() { timeLog('发布组件库', 'start'); await run('npm publish'); timeLog('发布组件库', 'end');}/** * 打tag提交至git */async function tag(nextVersion: string) { timeLog('打tag并推送至git', 'start'); await run(`git tag v${nextVersion}`); await run(`git push origin tag v${nextVersion}`); timeLog('打tag并推送至git', 'end');}async function main() { try { const nextVersion = await prompt(); const startTime = Date.now(); // =================== 更新版本号 =================== await updateVersion(nextVersion); // =================== 更新changelog =================== await generateChangelog(); // =================== 代码推送git仓库 =================== await push(nextVersion); // =================== 组件库打包 =================== await build(); // =================== 发布至npm =================== await publish(); // =================== 打tag并推送至git =================== await tag(nextVersion); console.log(`✨ 发布流程完毕 共耗时${((Date.now() - startTime) / 1000).toFixed(3)}s`); } catch (error) { console.log(' 发布失败,失败原因:', error); }}main();初始化组件每次初始化一个组件就要新建许多文件以及文件夹,复造粘贴也可,不外还能够利用更高级一点的偷懒体例。

常规构想,新建一个组件模板文件夹,里面包罗一个组件所需要的所有文件,同时写好文件内容。

至于一些动态内容,譬如组件中英文名称,选一个你喜好的模板语言(如 handlebars),用其体例留空{{componentName}}。

package.json

"scripts": {+ "new": "ts-node ./scripts/new.ts"},接下来我们在new.ts中编写相关步调,无非是:

基于inquirer.js询问一些根本组件信息连系信息,衬着模板(填空)至组件文件夹向 components/index.ts 插进导出语句你认为我会写new.ts吗,不,我不会(固然我实写过)。

次要是利用metalsmith停止数据与模板连系,写脚手架的同窗可能比力熟悉。

自从我晓得了plop.js那个库,那么又能够偷懒了(为什么之前没有人告诉我有那么多好用的东西???)

"scripts": {- "new": "ts-node ./scripts/new.ts",+ "new": "plop --plopfile ./scripts/plopfile.ts",},于是上述流程能够大大简化,不需要写代码往询问,不需要手动衬着模板,我们要做的就是写好模板,而且设置装备摆设好问题以及衬着目标地。

详情可见:

设置装备摆设文件:scripts/plopfile.ts模板文件:templates/component结语文章很长,也是我小我进修中的总结,假设本文搀扶帮助到了你请给仓库一颗 ✨✨ 和本文一个赞。

假设有错误烦请在评论区斧正交换,谢谢。

选举React 进修相关文章《在 React 中主动复造文本到剪贴板「理论」》

《「干货满满」从零实现 react-redux》

《深进详解大佬用33行代码实现了React》

《让你的 React 组件性能跑得再快一点「理论」》

《React源码阐发与实现(三):理论 DOM Diff》

《React源码阐发与实现(一):组件的初始化与衬着「理论篇」》

《React源码阐发与实现(二):形态、属性更新-setState「理论篇」》

《细说React 核心设想中的闪光点》

《手把手教你10个案例理解React hooks的衬着逻辑「理论」》

《React-Redux 100行代码简易版探究原理》

《手把手深进教你5个身手编写更好的React代码【理论】》

《React 函数式组件性能优化常识点指南汇总》

《13个精选的React JS框架》

《深进浅出画图讲解React Diff原理【理论】》

《【React深进】React事务机造》

《Vue 3.0 Beta 和React 开发者别离杠上了》

《手把手深进Redux react-redux中间件设想及原理(上)【理论】》

《手把手深进Redux react-redux中间件设想及原理(下)【理论】》

《前端框架用vue仍是react?清晰比照两者差别》

《为了学好 React Hooks, 我解析了 Vue Composition API》

《【React 高级进阶】摸索 store 设想、从零实现 react-redux》

《写React Hooks前必读》

《深进浅出掌握React 与 React Native那两个框架》

《可靠React组件设想的7个原则之SRP》

《React Router v6 新特征及迁徙指南》

《用React Hooks做一个搜刮栏》

《你需要的 React + TypeScript 50 条标准和体味》

《手把手教你绕开React useEffect的陷阱》

《浅析 React / Vue 跨端衬着原理与实现》

《React 开发必需晓得的 34 个身手【近1W字】》

《三张图详尽讲解React组件的生命周期》

《手把手教你深进浅出实现Vue3 & React Hooks新UI Modal弹窗》

《手把手教你搭建一个React TS 项目模板》

《全平台(Vue/React/微信小法式)肆意角度旋图片裁剪组件》

《40行代码把Vue3的响应式集成进React做形态治理》

《手把手教你深进浅出React 迷惘的问题点【完全版】》

做者:海秋

转发链接:

0
回帖

手把手教你理论搭建React组件库「超详尽」 期待您的回复!

取消
载入表情清单……
载入颜色清单……
插入网络图片

取消确定

图片上传中
编辑器信息
提示信息