Руководство по созданию библиотеки компонентов React — пакетный вывод

React.js
Руководство по созданию библиотеки компонентов React — пакетный вывод

Вот и наступило главное событие.

Для другого контента, связанного с библиотекой компонентов, вы можете перейти на github, чтобы прочитать

Обзор

Среда хостинга отличается, и исходный код необходимо обработать и опубликовать в npm.

Уточните следующие цели:

  1. файл объявления типа экспорта
  2. экспортumd/Commonjs module/ES moduleи т. д. 3 формы для пользователей, чтобы представить
  3. Поддержка файлов стилейcssПознакомить не толькоless
  4. Поддержка загрузки по требованию

Весь код в этом разделе можно найти в репозиторииchapter-3полученный в отделении.

файл объявления типа экспорта

Поскольку он используетсяtypescriptнаписана библиотека компонентов, пользователи должны пользоваться преимуществами системы типов.

Мы можем сгенерировать файл объявления типа, и вpackage.jsonОпределите запись в следующим образом:

package.json

{
  "typings": "types/index.d.ts", // 定义类型入口文件
  "scripts": {
    "build:types": "tsc --emitDeclarationOnly" // 执行tsc命令 只生成声明文件
  }
}

воплощать в жизньyarn build:types, вы можете обнаружить, что корневой каталог был сгенерированtypesпапка(tsconfig.jsonопределено вoutDirполе), структура каталогов такая же, какcomponentsПапки остаются прежними, а именно:

types

├── alert
│   ├── alert.d.ts
│   ├── index.d.ts
│   ├── interface.d.ts
│   └── style
│       └── index.d.ts
└── index.d.ts

Таким образом, пользователь вводитnpmpackage, вы можете получать автоматические подсказки и повторно использовать определения типов связанных компонентов.

Далее будетts(x)дождитесь обработки файлаjsдокумент.

Обратите внимание, что нам нужно вывестиCommonjs moduleтак же какES moduleДва модульных типа файлов (пока не рассматриваем)umd), следующие виды использованияcjsОтносится кCommonjs module,esmОтносится кES module.
Рекомендуемая литература для студентов, которые сомневаются в этом:Импорт, требование, экспорт, module.exports смешанное подробное объяснение

Экспорт модулей Commonjs

На самом деле его можно использоватьbabelилиtscИнструменты командной строки для компиляции кода (на самом деле, многие библиотеки инструментов делают это), но учитывая, чтоРабота со стилями и их загрузка по запросу, мы используемgulpсвязать воедино этот процесс.

вавилонская конфигурация

Установить первымbabelи связанные с ним зависимости

yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties  @babel/plugin-transform-runtime --dev
yarn 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 dead
not 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-corejs3

yarn 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связанные зависимости

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 types 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

lib/alert/alert.js

экспорт модуля ES

генерироватьES moduleможно сделать лучшеtree shaking, исходя из предыдущего шагаbabelНастройте, обновите следующее:

  1. настроить@babel/preset-envизmodulesВариантыfalse, закройте преобразование модуля;
  2. настроить@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, можно обнаружить, что сгенерированныйtypes/lib/esmтри папки, наблюдайтеesmкаталог с той же структуройlib/typesПоследовательны, файлы jsES moduleИмпорт и экспорт в виде модуля.

esm/alert/alert.js

esm/alert/alert.js

не забудь датьpackage.jsonДобавьте связанные записи.

package.json

{
+ "module": "esm/index.js"
}

Работа с файлами стилей.

копировать меньше файлов

мы будемlessфайл включен вnpmпакет, пользователь может пройтиhappy-ui/lib/alert/style/index.jsПредставлен в форме по запросу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 схемы предварительного отбора:

  1. Уведомить пользователей об увеличенииless-loader;
  2. упаковать полныйcssфайл, продолжитьПолная суммаПредставлять;
  3. Обеспечить отдельныйstyle/css.jsфайл, который импортирует компонентыcssфайловые зависимости, неlessЗависимость, базовая библиотека компонентов сглаживает различия.

Вариант 1 приведет к увеличению затрат на использование.

Сценарий 2 не может импортировать файлы стилей по запросу (впоследствии вumdМы также предоставляем этот файл стиля при упаковке).

Вышеупомянутые два варианта действительно являются последним средством (голос за кадром: если вы используетеcss in jsНе такое уж и дерьмо).

Схема 3 больше соответствует текущей ситуации,antdЭта схема также используется.

В процессе сборки библиотеки компонентов возникает проблема, которая меня давно мучает: зачем мнеalert/style/index.jsпредставлятьlessфайл илиalert/style/css.jsпредставлятьcssдокумент?

ответУправление зависимостями стилей.

Предположим, существует следующий сценарий: введение<Button />,<Button />зависел от<Icon />, пользователю необходимо вручную импортировать стиль вызываемого компонента (<Button />) и зависимые от него стили компонентов (<Icon />), крайне проблематично столкнуться со сложными компонентами, поэтому разработчики библиотек компонентов могут предоставить такую ​​возможность.jsфайл, пользователь вручную импортирует этотjsфайл, вы можете импортировать стили соответствующего компонента и его зависимых компонентов.

Продолжить наше путешествие.

сгенерировать 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 --dev

gulpfile.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.jsonsideEffectsатрибуты, подходит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';

сгенерировать умд

«Упаковка» в прямом смысле, генерирующая полный объемjsфайл иcssФайл импортируется пользователем вне цепочки. выберите здесьrollupупаковать.

Отверстие осталось заполнить.

To be Continued...