Тремтячі дерева модулі ES6 у веб-пакеті 2

Минулого тижня Webpack 2 щойно вийшов з бета-версії. Це приносить із собою різноманітні очікувані функції, включаючи вбудовану підтримку модулів ES6.

Замість використання var module = require('module')синтаксису, webpack 2 підтримує ES6 importsта exports. Це відкриває двері для оптимізації коду, як струшування дерев .

Що таке струшування дерев?

Популяризований пакувальником модуля Rollup.js Річа Гарріса, тремтіння дерев - це можливість включати лише код, який використовується.

Коли я вперше погрався з Rollup, я був вражений тим, як добре він працював з модулями ES6. Досвід розробки просто відчувався ... правильно. Я міг би створити окремі модулі, написані на "майбутньому JavaScript", а потім включити їх де завгодно в свій код. Будь-який код, який залишається невикористаним, не потрапляє до мого набору. Геній!

Яку проблему вона вирішує?

Якщо ви пишете JavaScript у 2017 році і розумієте (див.: Втома від JavaScript) різні інструменти навколо, ваш досвід розробки, ймовірно, відчувається досить мінливо. Це важливо, але важливим є також досвід користувачів . Багато з цих сучасних інструментів закінчує здуття веб-додатків масивними файлами JavaScript, що призводить до зниження продуктивності.

Що мені подобається у Rollup, так це те, що вона вражає цю проблему і виводить рішення на перший план спільноти JavaScript. Зараз такі імена, як webpack, намагаються повторити це.

Струшування дерев може бути не «рішенням для закінчення всіх рішень», але воно є важливою частиною великого пирога.

Простий приклад

Перш ніж почати, я хочу надати вам тривіальний приклад похитування дерев. Ваша заявка складається з 2 файлів index.jsта module.js.

Усередині module.jsвас експортується 2 іменовані функції зі стрілками:

// module.js export const sayHello = name => `Hello ${name}!`; export const sayBye = name => `Bye ${name}!`

Імпортується лише sayHelloу index.jsфайл:

// index.js import { sayHello } from './module'; sayHello('World');

sayByeекспортується, але ніколи не імпортується. Де завгодно. Тому через похитування дерев він не буде включений у ваш комплект:

// bundle.js const sayHello = name => `Hello ${name}!`; sayHello('World');

Залежно від використовуваного пакета, вихідний файл вище може виглядати по-різному. Це просто спрощена версія, але ви розумієте.

Нещодавно я прочитав статтю, написану Романом Лютіковим, і він зробив велику аналогію для того, щоб наочно уявити концепцію струшування дерев:

«Якщо ви задаєтеся питанням, чому це називається« тремтінням дерев »: подумайте про свою програму як про графік залежностей, це дерево, а кожен експорт - гілка. Тож якщо ти струсиш дерево, мертві гілки впадуть ». - Роман Лютіков

Тремтіння дерев у веб-пакеті 2

На жаль, для тих з нас, хто використовує веб-пакет, тремтіння дерев, якщо хочете, є «за перемикачем». На відміну від Rollup, перед тим, як отримати функціональність, яку ви шукаєте, потрібно виконати певну конфігурацію. Частина «за перемикачем» може заплутати деяких людей. Поясню.

Крок 1: Налаштування проекту

Я збираюся припустити, що ви розумієте основи webpack і можете знайти шлях до основного конфігураційного файлу webpack.

Почнемо зі створення нового каталогу:

mkdir webpack-tree-shaking && cd webpack-tree-shaking

Опинившись всередині, давайте ініціалізуємо новий npmпроект:

npm init -y

Ця -yопція package.jsonшвидко генерується, не вимагаючи відповіді на купу питань.

Далі, давайте встановимо кілька залежностей проекту:

npm i --save-dev [email protected] html-webpack-plugin

Команда вище встановить останню бета-версію webpack 2 локально у вашому проекті, а також корисний плагін з іменем html-webpack-plugin. Останнє не є необхідним для цілей цього покрокового керівництва, але зробить це дещо швидшим.

Примітка : Команда npm i --save-dev [email protected]все ще рекомендується командою webpack на момент написання. [email protected]врешті-решт буде скасовано на користь webpackостанньої команди. Перевірте, як завантажити?розділ останньої публікації веб-пакета, щоб дізнатися більше.

Відкрийте package.jsonта переконайтесь, що вони встановлені як devDependencies.

Крок 2: Створіть файли JS

Для того, щоб побачити похитування дерев у дії, вам потрібно мати трохи JavaScript, щоб пограти. У кореневій srcпапці проекту створіть папку з 2 файлами всередині:

mkdir src && cd src touch index.js touch module.js

Примітка:touch команда створює новий файл через термінал.

Скопіюйте наведений нижче код у правильні файли:

// module.js export const sayHello = name => `Hello ${name}!`; export const sayBye = name => `Bye ${name}!`;
// index.js import { sayHello } from './module'; const element = document.createElement('h1'); element.innerHTML = sayHello('World'); document.body.appendChild(element);

Якщо ви зайшли так далеко, структура папок повинна виглядати так:

/ | - node_modules/ | - src/ | | - index.js | | - module.js | - package.json

Крок 3: Webpack від CLI

Since you have no configuration file created for your project, the only way to get webpack to do any work at the moment is through the webpack CLI. Let’s perform a quick test.

In your terminal, run the following command in your project’s root:

node_modules/.bin/webpack

After running this command, you should see output like this:

No configuration file found and no output filename configured via CLI option. A configuration file could be named 'webpack.config.js' in the current directory. Use --help to display the CLI options.

The command doesn’t do anything, and the webpack CLI confirms this. You haven’t given webpack any information about what files you want to bundle up. You could provide this information via the command line or a configuration file. Let’s choose the former just to test that everything is working:

node_modules/.bin/webpack src/index.js dist/bundle.js

What you’ve done now is pass webpack an entry file and an output file via the CLI. This information tells webpack, "go to src/index.js and bundle up all the necessary code into dist/bundle.js". And it does just that. You'll notice that you now have a dist directory containing bundle.js.

Open it up and check it out. There’s some extra javascript in the bundle necessary for webpack to do its thing, but at the bottom of the file you should see your own code as well.

Step 4: Create a webpack configuration file

Webpack can handle a lot of things. I’ve spent a good chunk of my free time diving into this bundler and I still have barely scratched the surface. Once you’ve move passed trivial examples its best to leave the CLI behind and create a configuration file to handle the heavy lifting.

In your project’s root, create a webpack.config.js file:

touch webpack.config.js

This file can be as complicated as you make it. We’ll keep it light for the sake of this post:

// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: 'dist' }, plugins: [ new HtmlWebpackPlugin({ title: 'Tree-shaking' }) ] }

This file provides webpack with the same information you gave to the CLI earlier. You’ve defined index.js as your entry file and bundle.js as your output file. You've also added your html-webpack-plugin which will generate an html file in your dist directory. Convenient.

Go ahead and test this to make sure it’s still working. Remove your dist directory, and in the command line type:

webpack

If everything went smoothly, you can open up dist/index.html and see "Hello World!".

Note: The use of a configuration file gives us the convenience of typing webpack instead of node_modules/.bin/webpack. Small wins.

Step 5: Babel

I mentioned earlier that webpack 2 brings native support for ES6 modules. This is all true, but it doesn’t change the fact that ES6 is not fully supported across all browsers. Because of this, you’re required to transform your ES6 code into readily acceptable JavaScript using a tool like Babel. In conjunction with webpack, Babel gives us the ability to write your “future JavaScript” without worrying about the implications of unsupported browsers.

Let’s go ahead and install Babel in your project:

npm i --save-dev babel-core babel-loader babel-preset-es2015

Take note of the babel-preset-es2015 package. This little guy is the reason I sat down to write all of this up.

Step 6: babel-loader

Webpack can be configured to transform specific files into modules via loaders. Once they are transformed, they are added to a dependency graph. Webpack uses the graph to resolve dependencies and includes only what is needed into the final bundle. This is the basis for how webpack works.

You can now configure webpack to use babel-loader to transform all of your .js files:

// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: 'dist' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: [ 'es2015' ] } } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'Tree-shaking' }) ] };

The module property provides a set of instructions for webpack. It says, "take any files ending in .js and transform them using babel-loader, but don't transform any files inside of node_modules!"

We’re also passing the babel-preset-es2015 package as an option to babel-loader. This just tells babel-loaderhow to transform the JavaScript.

Run webpack again to make sure everything is good. Yes? Great! What we've done is bundled up your JavaScript files while compiling them down to JavaScript thats readily supported across browsers.

The underlying problem

The package babel-preset-es2015 contains another package named babel-plugin-transform-es2015-modules-commonjs that turns all of your ES6 modules into CommonJS modules. This isn't ideal, and here's why.

Javascript bundlers such as webpack and Rollup can only perform tree-shaking on modules that have a static structure. If a module is static, then the bundler can determine its structure at build time, safely removing code that isn’t being imported anywhere.

CommonJS modules do not have a static structure. Because of this, webpack won’t be able to tree-shake unused code from the final bundle. Luckily, Babel has alleviated this issue by providing developers with an option that you can pass to your presets array along with babel-preset-es2015:

options: { presets: [ [ 'es2015', { modules: false } ] ] }

According to Babel’s documentation:

“modules - Enable transformation of ES6 module syntax to another module type (Enabled by default to "commonjs"). Can be false to not transform modules, or one of ["amd", "umd", "systemjs", "commonjs"]".

Slide that extra bit of code into your configuration and you’ll be cooking with peanut oil.

The final state of webpack.config.js looks like this:

// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: 'dist' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: [ [ 'es2015', { modules: false } ] ] } } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'Tree-shaking' }) ] };

The Grand Finale

Run webpack again and pop open your bundle.js file. You won't notice any difference. Before you go crazy, know this! It's ok. We've been running webpack in development mode this whole time. Webpack knows that you have unused exports in your code. Even though it's included in the final bundle, sayBye will never make it to production.

If you still don’t believe me, run webpack -p in your terminal. The -p option stands for production. Webpack will perform a few extra performance optimizations, including minification, removing any unused code along the way.

Open up bundle.js. Since it's minified, go ahead and search for Hello. It should be there. Search for Bye. It shouldn't.

Voila! You now have a working implementation of tree-shaking in webpack 2!

For the curious, I’ve been slowly iterating over my own lightweight webpack configuration in a GitHub Repo:

jake-wies/webpack-hotplate

webpack-hotplate - A webpack boilerplate for personal projects

github.com

It’s not meant to be overly verbose and bloated. It’s focused on being an approachable boilerplate with walkthroughs at every turn. If you’re interested, check it out!

If you have any questions, feel free to reach out on Twitter!