Создание 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
.ts
, то же самое для файлов .vue
). Мы фактически получаем файлы TypeScript и Vue, но с содержимым JavaScript. Для этого нам нужно использовать некоторые плагины в Rollup:
rollup-plugin-typescript2
для компиляции TypeScript в JavaScriptrollup-plugin-vue
для компиляции Vue SFC в JavaScriptrenameExtensions
для переименования.ts
и.vue
в.js
.
Файлы конфигурации
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;