предисловие
Несколько лет назад я хотел сымитировать дизайн муравья и написать собственную библиотеку компонентов, чтобы соединить свои инженерные знания и повысить уровень инкапсуляции кода. В конце концов, вырезание картинок не может улучшить уровень нашего кода. процесс строительства, включая проектирование компонентов, оптимизацию рабочего процесса проекта и автоматическое развертывание. Я упомяну, что полный код находится на складе, а конкретные детали зависят от кода склада. Добро пожаловать в звезду ~~
онлайн-адрес
дизайн курицы ткните меня в превью
стек технологий
- Языки и фреймворки для разработки TypeScript + React + Sass
- Инструмент сборки Gulp
- Статический сайт
- Непрерывное развертывание просто использует post-receive in hooks в git.
- Тестовый инструмент Jest
- Ограничения кода проверки ESLINT Husky Обновите код перед отправкой нормативного выпуска
расписание
- Спецификации кода, форматы коммитов и глобальные стили
- Развертывание статического сайта, настройки автоматизации github
- Шуточный тест кода
- выпуск npmchicken-design@1.1.2
- Значок меню кнопки оповещения о ходе выполнения компонента
Справочная документация
Вероятно, это самый подробный обзор React для создания библиотеки компонентов.
Полный процесс (в качестве примера возьмем компонент «Меню»)
Готов к работе
Инициализировать репозиторий компонентов
-
npm init -yИнициализируйте репозиторий кода для хранения собственного кода библиотеки компонентов.
Установка среды
- Установить машинописный текст
npm install typescript
//tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"chicken-design": ["components/index.ts"],
"chicken-design/esm/*": ["components/*"],
"chicken-design/lib/*": ["components/*"]
},
"allowJs": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"outDir": "lib", // 打包输出位置
"module": "esnext", // 设置生成代码的模块标准,可以设置为 CommonJS、AMD 和 UMD 等等。
"target": "es5", // 目标语言的版本
"declaration": true, // 生成声明文件,记得 inde.d.ts
"jsx": "react", // 等效 React.createElement调用
"moduleResolution":"Node", // 模块解析策略,这里提供两种解析策略 node 和 classic,ts 默认使用 node 解析策略。
"allowSyntheticDefaultImports": true, // 允许对不包含默认导出的模块使用默认导入。这个选项不会影响生成的代码,只会影响类型检查。
"skipLibCheck": true // 跳过类库检查
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"include": ["components", "global.d.ts"],
"exclude": ["types", "node_modules", "lib", "esm"]
}
//tsconfig.build,json
{
"extends": "./tsconfig.json",
// "compilerOptions": { "emitDeclarationOnly": true },
"exclude": ["**/__tests__/**", "**/demo/**", "node_modules", "lib", "esm"]
}
- Установить реакцию
npm install react react-dom @types/react @types/react-dom
спецификация кода
- Установить зависимости
"prettier": "^1.19.1","@umijs/fabric": "^1.2.12"
- .eslintrc.js
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
rules: {
'react/require-default-props': 0,
},
};
- .prettierrc.js
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};
- .stylelintrc.js
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/stylelint')],
};
- Установить зависимости
"husky": "^3.1.0""lint-staged": "^9.5.0""@commitlint/cli": "^8.2.0"@commitlint/config-conventional"commitizen": "^4.0.3""cz-conventional-changelog": "^3.0.2"
//工作流程说明
1. git add .将所有改动的文件提交到暂存区
2. git commit -m ""此操作会被husky拦截,之后调用lint-staged对文件进行检查。
3. lint-staged会先进行git stash操作,之后会将与规则相匹配的暂存区的文件进行检查,只有已经提交到暂存区的文件才会被检查。
4. 等到lint-staged执行完成后,只要有一个文件没有通过检查,husky会阻止本次git commit,然后手动修改对应的有问题的文件,重新执行git add和git commit操作,会重复2,3步骤进行检查。
- package.json
"lint-staged": {
"components/**/*.ts?(x)": [
"prettier --write",
"eslint --fix",
"jest --bail --findRelatedTests",
"git add"
],
"components/**/*.scss": [
"stylelint --syntax scss --fix",
"git add"
]
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
- .commitlintrc.js
module.exports = { extends: ['@commitlint/config-conventional'] };
- eg
настройки стиля
В разработке используется язык препроцессинга Sass.После установки соответствующих зависимостей Sass в корневом каталоге создается папка styles.Структура каталогов следующая
styles
├─_animation.scss //存放自定义动画
├─_mixin.scss //存放重复使用的样式
├─_reboot.scss //样式重置 采用了normalize.css解决方案
├─_variables.scss //全局样式变量
└index.scss
Вы можете настроить соответствующий код для этой части кода в соответствии с вашими предпочтениями.Поскольку конфигурации слишком много, я не буду его выкладывать.Код весь на складе.
компоненты разработки
После завершения предыдущей работы наша библиотека компонентов была завершена на уровне конфигурации среды и ограничений спецификации кода, поэтому теперь давайте попробуем разработать компонент меню, чтобы начать наш процесс разработки.
Компонентная структура
components
├─index.ts
├─Menu
| ├─index.mdx
| ├─index.tsx
| ├─menu.tsx
| ├─menuItem.tsx
| ├─subMenu.tsx
| ├─styles
| | ├─index.scss
| | └index.ts
| ├─demo
| | ├─menu-demo-1.tsx
| | └menu-demo-2.tsx
Структурный анализ компонентов
Общая функция компонента меню имеет обычные подменю и раскрывающееся меню, которое можно свернуть, поэтому, естественно, нам нужно написать три компонента.
Menu,MenuItem,subMeun
Вот основной код меню для справки.
- Используйте useContext для обмена данными с дочерними компонентами.
- Используйте React.children.map для работы с реквизитами, переданными дочерним компонентам.
- Динамическое изменение имени класса для изменения стиля
//Menu.tsx
const MenuContext = createContext<IMenuContext>({ index: '0' })
export const Menu: FC<MenuProps> = props => {
const { className, mode, style, children, defaultIndex, onSelect, defaultOpenSubMenus } = props
const [currentActive, setActive] = useState(defaultIndex)
const classes = classNames('chicken-design-menu', className, {
'menu-vertical': mode === 'vertical',
'menu-horizontal': mode !== 'vertical',
})
const handleClick = (index: string) => {
setActive(index)
if (onSelect) {
onSelect(index)
}
}
const passedContext: IMenuContext = {
index: currentActive || '0',
onSelect: handleClick,
mode,
defaultOpenSubMenus,
}
const renderChildren = () => React.Children.map(children, (child, index) => {
const childElement = child as React.FunctionComponentElement<MenuItemProps>
const { displayName } = childElement.type
if (displayName === 'MenuItem' || displayName === 'SubMenu') {
return React.cloneElement(childElement, {
index: index.toString(),
})
}
})
return (
<ul className={classes} style={style} data-testid="test-menu">
<MenuContext.Provider value={passedContext}>
{renderChildren()}
</MenuContext.Provider>
</ul>
)
}
//MenuItem.tsx
const MenuItem: React.FC<MenuItemProps> = props => {
const { index, disabled, className, style, children } = props
const context = useContext(MenuContext)
const classes = classNames('menu-item', className, {
'is-disabled': disabled,
'is-active': context.index === index,
})
const handleClick = () => {
if (context.onSelect && !disabled && (typeof index === 'string')) {
context.onSelect(index)
}
}
return (
<li className={classes} style={style} onClick={handleClick}>
{children}
</li>
)
}
//subMeun.tsx
const SubMenu: React.FC<SubMenuProps> = ({ index, title, children, className }) => {
const context = useContext(MenuContext)
const openedSubMenus = context.defaultOpenSubMenus as Array<string>
const isOpend = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) : false
const [menuOpen, setOpen] = useState(isOpend)
const classes = classNames('menu-item submenu-item', className, {
'is-active': context.index === index,
'is-opened': menuOpen,
'is-vertical': context.mode === 'vertical',
})
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
setOpen(!menuOpen)
}
let timer: any
const handleMouse = (e: React.MouseEvent, toggle: boolean) => {
clearTimeout(timer)
e.preventDefault()
timer = setTimeout(() => {
setOpen(toggle)
}, 300)
}
const clickEvents = context.mode === 'vertical' ? {
onClick: handleClick,
} : {}
const hoverEvents = context.mode !== 'vertical' ? {
onMouseEnter: (e: React.MouseEvent) => { handleMouse(e, true) },
onMouseLeave: (e: React.MouseEvent) => { handleMouse(e, false) },
} : {}
const renderChildren = () => {
const subMenuClasses = classNames('chicken-design-submenu', {
'menu-opened': menuOpen,
})
const childrenComponent = React.Children.map(children, (child, i) => {
const childElement = child as FunctionComponentElement<MenuItemProps>
if (childElement.type.displayName === 'MenuItem') {
return React.cloneElement(childElement, {
index: `${index}-${i}`,
})
}
console.error('Warning: SubMenu has a child which is not a MenuItem component')
})
return (
<Transition
in={menuOpen}
timeout={300}
animation="zoom-in-bottom"
>
<ul className={subMenuClasses}>
{childrenComponent}
</ul>
</Transition>
)
}
return (
<li key={index} className={classes} {...hoverEvents}>
<div className="submenu-title" {...clickEvents}>
{title}
<Icon icon="angle-down" className="arrow-icon"/>
</div>
{renderChildren()}
</li>
)
}
местное развитие
Локальная разработка и демонстрация компонентов, которые я используюdoczДля отладки и демонстрации компонентов, конечно, есть много подобных решений, таких какstorybookа такжеdumi, я попробую, когда у меня будет шанс~
docz основан на MDX (Markdown + JSX), который может вводить компоненты React в Markdown, что позволяет выполнять предварительный просмотр и отладку при написании документов. А благодаря экосистеме компонентов React мы можем писать документацию точно так же, как писать приложения.
Установить документ локально
npm install docz
добавить конфигурацию
Используется нижний слой докаGatsby, поэтому при настройке необходимо добавить конфигурационный файл gatsby.Подробную схему настройки можно найти на официальном сайте
// doczrc.js
export default {
src: 'components',
dist: 'doc-site', // 打包出来的文件目录名
title: 'chicken-desgin', // 站点标题
typescript: true, // 组件源文件是通过typescript开发,需要打开此选项
scripts: {
"docz:dev": "docz dev && gulp",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve"
},
};
//gatsby-config.js 这里添加需要使用的plugin
module.exports = {
plugins: ['gatsby-theme-docz', 'gatsby-plugin-styled-components','gatsby-plugin-sass'],
};
//gatsby-node.js
const path = require('path');
exports.onCreateWebpackConfig = args => {
args.actions.setWebpackConfig({
resolve: {
modules: [path.resolve(__dirname, '../src'), 'node_modules'],
alias: {
'chicken-design/lib': path.resolve(__dirname, '../components/'),
'chicken-design': path.resolve(__dirname, '../lib/'),
},
},
});
};
добавить mdx-файл
---
name: Menu 菜单
route: /docs/menu
menu: 组件
---
import { ShowTools } from '../../doc-comps';
import BasicDemo from './demo/menu-demo-1';
import BasicDemoCode from '!raw-loader!./demo/menu-demo-1.tsx';
import BasicDemo2 from './demo/menu-demo-2';
import BasicDemoCode2 from '!raw-loader!./demo/menu-demo-2.tsx';
## Menu 菜单
菜单 用于分类处理
### 代码演示
#### 基本用法
<ShowTools code={BasicDemoCode} title="默认">
<BasicDemo />
</ShowTools>
<ShowTools code={BasicDemoCode2} title="纵向菜单">
<BasicDemo2 />
</ShowTools>
### API
#### Menu
| 属性 | 说明 | 类型 | 默认值 |
| ---- | -------- | -------------------------------------------- | ------ |
| defaultIndex | 默认active的索引 | number | 0 |
| className | 组件类名 | string | - |
| mode | 菜单类型横项或者纵向 | oneOf "horizontal" | "vertical" | horizonta |
| onSelect | 点击菜单项触发的回掉函数 | function | false |
| defaultOpenSubMenus | 设置子菜单的默认打开 只在纵向模式下生效 | string[默认打开的索引数组] | - |
#### MenuItem
| 属性 | 说明 | 类型 | 默认值 |
| ---- | -------- | -------------------------------------------- | ------ |
| disabled| 选项是否被禁用 | boolean | false |
| className | 组件类名 | string | - |
| style | css | | |
#### SubMenu
| 属性 | 说明 | 类型 | 默认值 |
| ---- | -------- | -------------------------------------------- | ------ |
| title | 名称 | string | - |
| className | 组件类名 | string | - |
| style | css | | |
отладка
npm run startЗапустить локальную отладку
Пакет
ЦельУпакуйте компоненты в новую папку, поддержите как css, так и scss, потому что нельзя требовать от пользователя установить sass, и при этом конвертировать ts в ES5, что удобно пользователям. , наш индексный файл Файл css импортируется по умолчанию, но в коде компонента разработки нет файла .css, поэтому нам нужно преобразовать index.scss в index.css по обычным правилам при упаковке gulp.Конкретный код следующим образом
- Добавьте упакованные полосы в package.json
"build:doc": "npm run clean && npm run build:types && gulp build","build:types": "tsc -p tsconfig.build.json", - Преобразование файлов scss и ts для использования gulp и установка соответствующих зависимостей
//.browserslistrc
>0.2%
not dead
not op_mini all
//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,
},
],
],
},
},
};
//gulpfile.js
const gulp = require('gulp');
const babel = require('gulp-babel');
const scss = require('gulp-sass');
const autoprefixer = require('gulp-autoprefixer');
const cssnano = require('gulp-cssnano');
const through2 = require('through2');
const paths = {
dest: {
lib: 'lib',
esm: 'esm',
dist: 'dist',
},
style: 'components/**/*.scss',
root: 'styles/*.scss',
copyroot: 'components/styles',
scripts: [
'components/**/*.{ts,tsx}',
'!components/**/demo/*.{ts,tsx}',
'!components/**/__tests__/*.{ts,tsx}',
],
};
/**
* @param {string} content
*/
function cssInjection(content) {
return content
.replace(/\/style\/?'/g, "/style/css'")
.replace(/\/style\/?"/g, '/style/css"')
.replace(/\.less/g, '.css')
.replace(/\.scss/g, '.css')
}
/**
* 编译脚本文件
* @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(
through2.obj(function z(file, encoding, next) {
this.push(file.clone());
// 找到目标
if (file.path.match(/(\/|\\)styles(\/|\\)index\.js/)) {
const content = file.contents.toString(encoding);
file.contents = Buffer.from(cssInjection(content)); // 处理文件内容
file.path = file.path.replace(/index\.js/, 'index.js'); // 文件重命名
this.push(file); // 新增该文件
next();
} else {
next();
}
}),
)
.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);
}
const buildScripts = gulp.series(compileCJS, compileESM);
/**
* 拷贝scss文件
*/
function copyScssRoot() {
return gulp
.src(paths.root)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.copyroot))
.pipe(gulp.dest(paths.dest.esm));
}
function copyScss() {
return gulp
.src(paths.style)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
/**
* 生成css文件
*/
function scss2css() {
return gulp
.src(paths.style)
.pipe(scss()) // 处理scss文件
.pipe(autoprefixer()) // 根据browserslistrc增加前缀
.pipe(cssnano({ zindex: false, reduceIdents: false })) // 压缩
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
}
gulp.task("watch-copy-scss",function(){
return gulp
.src(paths.style)
.pipe(gulp.dest(paths.dest.lib))
.pipe(gulp.dest(paths.dest.esm));
})
const watch = gulp.task("watch",function(){
gulp.watch(paths.style,gulp.parallel(copyScss,scss2css))
})
const build = gulp.parallel(buildScripts,copyScssRoot ,copyScss,scss2css);
exports.build = build;
exports.watch = watch;
выпускать
После того, как упаковка будет завершена, мы можем опубликовать
Перед этим нам нужно добавить опубликованный белый список в файл package.json.Конечно, вы также можете использовать файл конфигурации npmignore.Этот метод внесения в черный список не рекомендуется и его легко пропустить.
"files": [ "lib","esm" ]
Процесс публикации
- Зарегистрируйте npm-аккаунт
npm login-
npm publishОбратите внимание, что если вы используете такие источники, как пряжа или табу, вам необходимо вернуться к источнику npm.js, иначе будет сообщено об ошибке.
Затем он успешно публикуется
Затем протестируйте его в своем собственном проекте.
хорошо, нет проблем!
Непрерывное развертывание
Весь процесс, от разработки до публикации библиотеки компонентов, на самом деле довольно удобный, но теперь возникла новая проблема, нам также нужен официальный сайт для отображения, тогда мы обычно делаем новый модуль или модификацию стиля, когда мне нужно его обновить на сервере самая удобная ситуация, что когда я ввожу команду локально, например git push, она автоматически обновится.Сейчас существует множество CI/CD-решений, таких как jekins, travis и т. д. На самом деле, github предоставляет нам с очень удобным решением! , давайте попробуем этот метод, запустив хук в github
- Сначала вам нужен сервер Linux, затем установите сервер nginx и среду node.js.
- Затем нам нужно создать голый репозиторий, мы будем пушить код в этот голый репозиторий.
cd /opt
git init xxxx.git --bare
cd xxxx.git/hook
vim post-receive
Затем выйдите из командной строки vim
chmod +x post-receive 添加文件执行权限
Вернитесь в локальный каталог проекта
git remote add prod ssh://@服务器ip/opt/xxxx.gitgit remote- v-
git add . git commit git pushодна остановка
Ой пардон, по подсказке хаски поменяй и добавь возврат
сделай это снова
В это время ваши статические файлы будут находиться в каталоге /var/www/html вашего сервера.В это время вы должны изменить каталог по умолчанию вашего nginx
vim /etc/nginx/sites-available/default
Конкретную конфигурацию можно посмотреть на официальном сайте nginx или у меня уже была похожая статья.портал
После завершения введите адрес в браузере и вы его увидите
После всего процесса, хотя недоделок еще много, весь процесс разработки все же более комфортный, и вы можете продолжить добавлять его позже.Внизу осталось два модуля, которые будут обновлены, когда у Xiaocaiji будет урожай.Скоро, если прогресс нормальный, обновляется раз в неделю, если вам было полезно, то можете поставить лайк и подписаться ха-ха~, кстати, я не хочу делать статью слишком длинной (лень), поэтому некоторые коды не может быть опубликовано полностью, все На основе моего склада, спасибо~