Создание tree-shaking библиотеки с Rollup и Vue

месяц назад·3 мин. на чтение

В статье описан процесс оптимизации библиотеки с использованием Rollup и Vue, с акцентом на tree-shaking для удаления неиспользуемого кода и повышения производительности приложений.

Почему не Webpack для создания библиотеки?

Webpack можно использовать для основного приложения (проекта), которое будет использовать нашу UI библиотеку (для которой выбрали Rollup). Мы используем Rollup для компиляции библиотеки, потому что Webpack не поддерживает формат вывода ES6 (пока) Webpack хорошо подойдет для сборки основного проекта, поскольку он поддерживает встроенное встряхивание дерева и может объединять код, отличный от JS. Единственное предостережение относительно алгоритма встряхивания дерева Webpack заключается в том, что он вряд ли может определить, вызывает ли код побочные эффекты при импорте, поэтому в package.json библиотеки следует добавить ключ sideEffects, для которого можно установить значение false, если библиотека не запускает побочные эффекты.
// В библиотеке нет сайд эффектов
{
  …
  "sideEffects": false
}

// Все *.scss, которые при импорте будут вызывать сайд эффекты
{
  …
  "sideEffects": [ "src/**/*.scss" ]
}

Некоторые понятия

Давайте определим некоторые понятия, чтобы пост был понятнее:
  • Tree shaking (встряхивание дерева) - это термин, широко используемый в контексте JavaScript, чтобы описать удаление неиспользуемого кода. Он основан на операторах import и export в ES2015 для определения, были ли кодовые модули экспортированы и импортированы для использования в JavaScript-файлах.
  • Однофайловые Vue компоненты (SFC) - это способ определения компонентов в VueJS с помощью файлов .vue, которые включают шаблон, скрипт и стиль в одном файле. Другие способы определения компонентов VueJS - это обычный JavaScript (с помощью Vue.extent(...)) и JSX.

Почему нужна поддержка tree shaking?

Один из факторов, которые следует учитывать при разработке веб-приложения, - это размер пакета, доставляемого в браузер. Для небольших проектов это может быть незначительно, но для больших проектов это может стать проблемой из-за количества зависимостей и устаревшего кода. При создании внешней библиотеки следует поддерживать удаление неиспользуемого кода, потому что:
  • В крупных проектах часто есть много зависимостей, и по мере роста проекта и изменения требований некоторые из этих зависимостей становятся неиспользуемым кодом, который тем не менее упаковывается, увеличивая размер приложения.
  • При создании больших библиотек (например, библиотек UI компонентов) многие потребители будут использовать только подмножество предоставляемых функций, и если библиотека не поддерживает удаление неиспользуемого кода, то им придется импортировать все функции.

Требования к библиотеке для поддержки tree shaking

Если мы хотим, чтобы наша библиотека была статически анализируема, чтобы сборщики могли удалять неиспользуемый код, мы должны выполнить следующие требования:
  • Она должна быть экспортирована в формате ES6, конкретно с использованием синтаксиса import/export (а не синтаксиса require CommonJS). Таким образом, код статически анализируем, и поэтому можно определить, используется ли код или нет.
  • Он не должен быть упакован (bundled). Это облегчает работу компилятора, изолируя код по модулям и не объединяя все в один файл.
  • Модули, которые мы хотим встряхнуть, не должны вызывать побочных эффектов. Это означает, что они не должны изменять глобальные переменные или вызывать любые другие виды действий с побочными эффектами при импортировании.

Конфигурация Rollup

Обычно настройка Rollup для tree shaking довольно прямолинейна, но в этом случае есть некоторые ограничения:
  • TypeScript нужно скомпилировать в ES6 JavaScript
  • VueJS SFC нужно скомпилировать в ES6 JavaScript
Rollup может сохранить модули в отдельных файлах, но сохраняет оригинальное название (так что файлы TypeScript останутся .ts, то же самое для файлов .vue). Мы фактически получаем файлы TypeScript и Vue, но с содержимым JavaScript. Для этого нам нужно использовать некоторые плагины в Rollup:

Файлы конфигурации

package.json

{
  "name": "your-library-name",
  "version": "0.1.0",
  "module": "dist/index.js",
  "sideEffects": false,
  "scripts": {
    "build": "rollup --config ./config/rollup.config.js",
    "serve": "rollup --config ./config/rollup.config.js --watch",
    "test": "jest --config ./config/jest.config.js --rootDir ."
  },
  "devDependencies": {
    "@betit/rollup-plugin-rename-extensions": "^0.0.4",
    "@types/jest": "^24.0.15",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-preset-vue": "^2.0.2",
    "jest": "^24.8.0",
    "jest-serializer-vue": "^2.0.2",
    "postcss": "^7.0.17",
    "rollup": "^1.16.7",
    "rollup-plugin-cleaner": "^1.0.0",
    "rollup-plugin-commonjs": "^10.0.1",
    "rollup-plugin-typescript2": "^0.22.0",
    "rollup-plugin-vue": "^5.0.1",
    "standard-changelog": "^2.0.18",
    "ts-jest": "^24.0.2",
    "ts-loader": "^6.0.4",
    "typescript": "^3.5.3",
    "vue": "^2.6.10",
    "vue-jest": "^3.0.4",
    "vue-loader": "^15.7.0",
    "vue-property-decorator": "^8.2.1",
    "vue-template-compiler": "^2.6.10"
  },
  "peerDependencies": {
    "vue": "^2.6.10"
  }
}

rollup.config.js

import vue from 'rollup-plugin-vue'
import typescript from 'rollup-plugin-typescript2'
import renameExtensions from '@betit/rollup-plugin-rename-extensions'
import cleaner from 'rollup-plugin-cleaner'
import commonjs from 'rollup-plugin-commonjs'
export default {
  input: 'index.js',
  output: {
    format: 'esm', // Это то, что говорит rollup использовать ES6 модули
    dir: 'dist'
  },
  external: [ 'vue', 'vue-class-component' ],
  plugins: [
    cleaner({ targets: [ 'dist' ] }),
    commonjs(),
    typescript({ rollupCommonJSResolveHack: true, clean: true }),
    // Это расширение переименовывает .vue и .ts в .js и обновляет импорты
    renameExtensions({
      include: ['**/*.ts', '**/*.vue'],
      mappings: { '.vue': '.vue.js', '.ts': '.js' }
    }),
    vue()
  ],
  // Предотвращает бандлинг, но не переименовывает файлы
  preserveModules: true
}

Как вызвать метод дочернего компонента из родительского компонента с помощью useImperativeHandle

2 года назад·3 мин. на чтение

Быстрый старт с useImperativeHandle

В этой статье будет показано, как вызвать метод дочернего компонента с помощью ссылки. Чтобы решить эту проблему, мы будем использовать хуки useRef и useImperativeHandle.

Дочерний компонент

Начнем с простого дочернего компонента, в котором содержится кнопка. Нажатие на кнопку вызывает внутренний метод doSomething.
// Child.jsx

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };
  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default Child;

Родительский компонент

Далее рассмотрим родительский компонент. В нем используется дочерний компонент, описанный выше. Обратите внимание, что в родительском компоненте есть собственная кнопка сохранения.
// App.jsx

import Child from "./Child";

function App() {
  const save = () => {};
  return (
    <div>
      <Child />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;

Хук useImperativeHandle

Теперь давайте сосредоточимся на нашей задаче. Мы хотим вызвать метод (doSomething) дочернего компонента при нажатии кнопки (Save) из родительского компонента. Чтобы вызвать метод из дочернего компонента, нам нужно сначала выставить его наружу. useImperativeHandle определяет значение объекта, которое предоставляется родительскому компоненту при использовании ref. Добавляя наш метод к этому объекту, мы делаем его доступным в родительских компонентах.
// Child.jsx

import { useImperativeHandle } from "react";

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };

  useImperativeHandle(ref, () => ({ doSomething }));

  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}
export default Child;
useImperativeHandle следует использовать с forwardRef.
forwardRef позволяет родительскому компоненту передавать ссылки своим дочерним элементам. Чтобы прикрепить функции или поля к этой ссылке (к рефу), используется хук useImperativeHandle.
// Child.jsx

import { forwardRef, useImperativeHandle } from "react";

function Child(props, ref) {
  const doSomething = () => {
    console.log("do something");
  };

  useImperativeHandle(ref, () => ({ doSomething }));

  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default forwardRef(Child); // Child обернут в forwardRef
На этом этапе мы можем создать ссылку в родительском компоненте с помощью хука useRef и передать ее дочернему компоненту. Получив эту ссылку, мы можем вызвать метод doSomething дочернего компонента.
// App.jsx

import { useRef } from "react";
import Child from "./Child";

function App() {
  const childRef = useRef(null);

  const save = () => {
    if (childRef.current) {
      childRef.current.doSomething();
    }
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;

Добавим TypeScript

Далее посмотрим, какие изменения нужно сделать, чтобы вызвать тот же дочерний метод из родительского компонента при использовании TypeScript. Во-первых, нам нужно определить новый интерфейс, содержащий метод, который будет представлен.
export interface RefType {
  doSomething: () => void;
}
Затем новый тип (RefType) используется при получении ссылки в дочернем компоненте.
function Child(props: PropsType, ref: Ref<RefType>)
Ниже приведен полный код дочернего компонента.
// Child.jsx

import { forwardRef, useImperativeHandle, Ref } from "react";

export interface PropsType {}
export interface RefType {
  doSomething: () => void;
}

function Child(props: PropsType, ref: Ref<RefType>) {
  const doSomething = () => {
    console.log("do something");
  };
  useImperativeHandle(ref, () => ({ doSomething }));
  return (
    <div>
      <h1>Child Component</h1>
      <button onClick={doSomething}>Run</button>
    </div>
  );
}

export default forwardRef(Child);
В родительский компонент нам нужно импортировать этот RefType, содержащий все публичные дочерние методы, и использовать его при создании ref.
// App.jsx

import Child, { RefType } from "./Child";
//...
const childRef = useRef<RefType>(null);
Полный код родительского компонента.
import { useRef } from "react";
import Child, { RefType } from "./Child";

function App() {
  const childRef = useRef<RefType>(null);

  const save = () => {
    if (childRef.current) {
      childRef.current.doSomething();
    }
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={save}>Save</button>
    </div>
  );
}

export default App;