Mình tin rằng nhiều anh em đã từng nghe qua Webpack, nhưng đôi khi vẫn chưa hiểu nó là gì, dùng nó như thế nào. Đừng lo, trong chuỗi bài Webpack siêu tốc này, mình sẽ giải thích và hướng dẫn anh em một cách siêu dễ hiểu luôn, đọc phát làm được ngay. Không nói nhiều nữa, đi vào thực hành nhé.

Webpack là gì?

Để hiểu được webpack thì bạn phải biết lý do mà nó sinh ra.

Ngày xưa thuở mới học lập trình web thì chúng ta thường tạo 1 project với 3 file đơn giản là: index.html, style.css, app.js. Nhưng càng ngày thì css cũng như javascript càng phát triển, chúng ta SASS, LESS để biên dịch sang CSS, rồi các phiên bản Javascript mới hơn như ES6, ES2018, ES2019… Lúc này mới nảy sinh các vấn đề như sau:

  • Nếu anh em dùng cú pháp ES6 hay ES2018 thì các trình duyệt cũ chỉ hỗ trợ đến ES5 không thể hoạt động được
  • Viết code SASS, LESS thì trình duyệt không đọc được. Phải dùng một số công cụ chuyển đổi sang CSS như node-sass, Gulp, Koala,…
  • Viết code xong rồi còn phải nén code lại để giảm dung lượng code nhằm tăng tốc độ load trang
  • Khi thao tác ở môi trường dev, mỗi khi có thay đổi trên code, ta thường tự tay F5 để  reload lại trình duyệt. Nếu muốn tự động thì ta có thể dùng một số tool ngoài kia như Live Server trên VSCode,…

Bên trên là một số trong rất nhiều vấn đề mà anh em sẽ gặp phải và nếu mỗi vấn đề dùng mỗi công cụ thì không phải là lựa chọn tối ưu. Vì thế Webpack sinh ra để giải quyết tất cả vấn đề trên.

Webpack là một công cụ chạy trên môi trường NodeJs, giúp bạn đóng gói js, css, image, font… thành một hay nhiều file một cách dễ dàng.

Oke, Bây giờ chúng ta cùng cấu hình một project đơn giản với Webpack.

Cài đặt Project

Tạo một project mới và khởi tạo file package.json với câu lệnh npm

npm init

Tạo một thư mục public và 1 file index.html trong đó. File này sẽ load một file JS sau khi webpack thực hiện xong.

<!DOCTYPE html>
<html>
  <head>
    <title>Training Webpack</title>
  </head>
  <body>
    <div id="root">
      <h1>Học Webpack siêu tốc</h1>
    </div>
    <noscript> You need to enable JavaScript to run this app. </noscript>
    <script src="../dist/bundle.js"></script>
  </body>
</html>

Tạo file .gitignore để tránh đẩy thư mục node_modulesdist lên git nhé

node_modules
dist

Cài đặt Babel

Babel là trình biên dịch Javascript thành các version thấp cấp hơn để phù hợp cho các trình duyệt khác nhau.

Bạn có thể chạy Babel mà không cần Webpack, nhưng trong bài này mình kết hợp babel với webpack để tạo ra bộ đôi “trai tài gái sắc” :mrgreen:

Nếu các bạn dùng npm thì

npm i @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime --save-dev

Còn mình dùng yarn nên sẽ dùng cú pháp dưới đây. Hơi khác 1 xíu thôi, nhưng yarn cho tốc độ cài nhanh hơn npm 😆

yarn add @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime -D

Giải thích:

  • Ở đây mình dùng yarn thay vì npm, các bạn có thể dùng npm tùy thích nhé. -D tương đương –save-dev, ám chỉ đây là các devDependencies
  • @babel/core là phần lõi của babel, nơi chứa các thuật toán của nó
  • @babel/preset-env là một bộ cài đặt giúp bạn biên dịch code Javascript về cú pháp cũ hơn (nếu không setting gì thì mặc định target là ES5 và modules là auto). Xem thêm tại đây
  • @babel/plugin-transform-runtime: Nếu không dùng cái này thì khi dùng async await bạn sẽ bị lỗi regeneratorRuntime is not defined. Mình cũng chưa rõ lỗi này bắt nguồn từ đâu 😆
  • babel-loader giúp bạn tích hợp babel vào trong webpack

Nếu không dùng babel thì có chạy được webpack không?

Câu trả lời là vẫn oke nhé.

Chúng ta cùng làm rõ vấn đề sau. Nếu không dùng các transpiler như Babel thì khi webpack build 1 project, file build sẽ có 2 thành phần

  1. Phần code của bạn sẽ được giữ nguyên. Nếu bạn dùng ES2016, ES2020 thì nó vẫn sẽ không bị thay đổi
  2. Phần code của webpack thêm vào để nối các file của bạn thành 1 thể thống nhất. Phần code này thì mặc định webpack dùng cú pháp của ES6.

Vậy nên khi bạn dùng các trình biên dịch Javascript thì nó chỉ biên dịch phần 1 (tức code của bạn), còn phần 2 (code webpack thêm vào) thì bạn phải cấu hình bằng output.environment . Nhiều anh em không để ý đoạn này, build xong nhưng trong file bundle vẫn thấy dùng arrow function 😆

Cài đặt Style loader và File Loader

yarn add css-loader file-loader sass sass-loader style-loader -D

Giải thích luôn

  • style-loader, css-loader giúp bạn có thể import được css vào file js
  • sass, sass-loader giúp bạn biên dịch scss sang css
  • file-loader giúp bạn import được các file ví dụ như ảnh, video vào file js

Cài đặt Webpack

Đầu tiên ta phải cài

yarn add webpack webpack-cli webpack-dev-server -D

Giải thích

  • webpack là phần lõi của webpack
  • webpack-cli giúp ta gõ được lệnh của webpack trên terminal
  • webpack-dev-server hỗ trợ tạo một server localhost cho môi trường dev

Tạo 1 file webpack.config.js

const path = require('path')
module.exports = (env, agrv) => {
  const isDev = agrv.mode === 'development'
  return {
    entry: './src/index.js',
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /(node_modules|bower_components)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
              plugins: ['@babel/plugin-transform-runtime']
            }
          }
        },
        {
          test: /\.(s[ac]ss|css)$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: { sourceMap: isDev ? true : false }
            },
            {
              loader: 'sass-loader',
              options: { sourceMap: isDev ? true : false }
            }
          ]
        },
        {
          test: /\.(png|svg|jpg|gif)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[path][name].[ext]'
              }
            }
          ]
        }
      ]
    },
    resolve: { extensions: ['.js', '.jsx'] },
    output: {
      path: path.resolve('dist'),
      publicPath: '../dist/',
      filename: 'bundle.js',
      environment: {
        arrowFunction: false,
        bigIntLiteral: false,
        const: false,
        destructuring: false,
        dynamicImport: false,
        forOf: false,
        module: false
      }
    },
    devtool: isDev ? 'source-map' : false,
    devServer: {
      contentBase: 'public',
      port: 3000,
      hot: true,
      publicPath: '/dist/',
      watchContentBase: true
    }
  }
}

Copy các dòng lệnh này vào mục script trong package.json

"start": "webpack serve --mode development",
"build": "webpack --mode production"

Tạo thư mục src

src/add.js

export const add = (a = 1, b = 2) => a + b
export const treeShaking = () => {
  console.log('Dòng này sẽ không có trong file build')
}

src/subtract.js

export const subtract = (a, b) => a - b

src/loadImage.js

import logo from './logo.jpg'

const component = () => {
  const element = document.createElement('div')
  const webpackLogo = new Image()
  webpackLogo.src = logo
  webpackLogo.width = 200
  element.appendChild(webpackLogo)
  return element
}

document.getElementById('root').appendChild(component())

src/index.scss

$color: #ddd;
#root {
  text-align: center;
  background-color: $color;
  padding: 100px;
}

src/index.js

import { subtract } from './subtract'
import { add } from './add'
import './loadImage'
import './index.scss'

console.log(`1 + 2 = ${add(1, 2)}`)
console.log(`8 - 2 = ${subtract(8, 2)}`)

Copy 1 file ảnh logo.jpg bất kì vào thư mục src

Và giờ ta có một cây thư mục đầy đủ như thế này

Tree Webpack folder

Bây giờ mới giải thích từng cái nha

webpack.config.js

Chúng ta đặt tên là webpack.config.js như vậy thì khi chạy lệnh webpack trong package.json webpack sẽ tự hiểu là dùng config từ file này. Nếu bạn muốn dùng tên file khác thì bạn phải chỉ rõ ra trong package.json. Tìm hiểu thêm tại đây

Trong file config, chúng ta có thể export một object, một function, một promise, hay một array. Trong bài này mình thích export function hơn, vì nó cho phép ta dùng được các biến từ bên ngoài truyền vào. Tìm hiểu thêm tại đây

Chúng ta phải cung cấp mode khi cấu hình webpack, mỗi mode sẽ có một bộ setting được cài sẵn. Công việc của chúng ta là cấu hình lại theo ý muốn của mình. Tìm hiểu thêm tại đây

Nếu các bạn để ý thì ở file package.json mình có dòng --mode development, nghĩa là webpack sẽ chạy ở mode development. Và ở trong file webpack.config.js mình có thể xác định được đang ở mode nào bằng cách dùng agrv.mode

entry: nơi chứa một hay nhiều các file đầu vào. Ở đây mình dùng file index.js. Vì ở trong file này mình import mọi file khác vào mà 🙂 . Tìm hiểu thêm tại đây

module.rules: Nhận vào một array. Đây là nơi chứa các loader

test: Nhận vào một Regex để xác định kiểu file. Nếu là true thì nó sẽ chạy loader. Với babel-loader thì sẽ chạy với các file js, jsx. style-loader sẽ chạy với sass, scss, css. file-loader sẽ chạy với các file png, svg, jpg, gif.

exclude: Nhận vào một regex để loader loại trừ ra những file này

use: Nhận vào một object hoặc một array chứa thông tin loader

Để ý đoạn babel-loader mình dùng presets là @babel/preset-env không cấu hình gì thêm nghĩa là dùng bộ mặc định của @babel/preset-env. Nó sẽ biên dịch sang cú pháp ES5

Để ý đoạn style-loader mình có dùng options sourceMap, nếu là đang dev thì mình sẽ để sourceMap, còn không thì sẽ không để. Cho bạn nào chưa biết thì khi import css vào file js, webpack sẽ chuyển nó lên thẻ <style> trong thẻ head. Vì thế bạn cần phải có source map để thuận tiện cho việc dev

Webpack đưa style lên thẻ <style>

Webpack đưa css vào tag <style> trong <head>

Không có source map

Không có source map thì không biết style này từ đâu ra

Có source map

Có source map, dễ dàng debug css

Để ý đoạn file-loader mình có option là name: '[path][name].[ext]' nghĩa là các file bạn ở thư mục nào thì khi build ra nó cũng sẽ tạo một thư mục tương tự như vậy. Ví dụ bạn có file src/logo.png thì khi build ra bạn sẽ có dist/src/logo.png

resolve: { extensions: [‘.js’, ‘.jsx’] }: Nơi chứa thứ tự ưu tiên khi import các file. Ví dụ các có 2 file name.jsname.jsx trong cùng 1 folder. Ở một file khác bạn import * from 'name' thì nó sẽ ưu tiên chọn file .js

output.path: Nhận vào một string là đường dẫn tuyệt đối đến thư mục sau khi build. Những option mà nhận vào đường dẫn tuyệt đối thì ta hay dùng path.resovle() hoặc path.join() kết hợp với biến global là __dirname. Anh em có thể xem sự khác biệt của 3 thằng đó như thế này. Tùy sở thích và mục đích mà anh em chọn cách dùng cho phù hợp nhé.

__dirname:  D:\Projects\Training-Webpack\Bai1
path.join():  .
path.join("abc"):  abc
path.join(__dirname, "abc"):  D:\Projects\Training-Webpack\Bai1\abc
path.resolve():  D:\Projects\Training-Webpack\Bai1
path.resolve("abc"):  D:\Projects\Training-Webpack\Bai1\abc
path.resolve(__dirname, "abc"):  D:\Projects\Training-Webpack\Bai1\abc

output.publicPath: Chứa đường dẫn tương đối mà từ file index.html trỏ đến các file trong thư mục dist sau khi build. Ví dụ ở file loadImage.js phía trên, ta import logo thì biến logo sẽ bằng output.publicPath + 'src/logo.jpg'. Nếu sau khi build, chúng ta chạy file index.html ở một vị trí khác không phải nằm trong thư mục public thì sẽ vô tình làm biến logo bị sai.

Điều đặc biệt là những nơi cho phép để đường dẫn tương đối thì bạn có thể để đường dẫn tuyệt đối, nhưng ngược lại thì không được, đường dẫn tương đối không thể dùng trong những option yêu cầu đường dẫn tuyệt đối.

publicPath hoạt động vô chỗ này đây

publicPath hoạt động vô chỗ này đây

output.filename: Tên file sau khi build

output.environment: Như mình đã giải thích ở trên thì vì mặc định webpack generate ra code dùng 1 số cú pháp của ES6, nhưng target mình mong muốn là ES5 nên mình cần chỉnh một số thông số như sau.

  • arrowFunction: Hỗ trợ arrow function.
  • bigIntLiteral: Hỗ trợ BigInt
  • const: Hỗ trợ khai báo const và let
  • destructuring: Hỗ trợ destructuring
  • dynamicImport: Hỗ trợ async import
  • forOf: Hỗ trợ vòng lặp forOf cho các array
  • module: Hỗ trợ moudle ES6 (import … from ‘…’)

devtool: chứa cấu hình file sau khi dev hoặc sau khi build. Ở đây thì khi dev mình để source-map cho dễ debug js, còn khi build thì mình xóa source map đi cho nhẹ file build. Tìm hiểu thêm tại đây

Đến dev server nhé. Ở dev server bạn tưởng tượng nó sẽ tạo một server localhost ngay tại thư mục root

devServer.contentBase: Chứa đường dẫn tương đối đến file index.html

devServer.port: port khi chạy localhost

devServer.hot: Chế độ hot reload. Mặc định thì ở dev server thì webpack sẽ refresh lại trang mỗi khi có thay đổi nhỏ trong code. Hot Reload kiểu như nó không cần reload lại trang nhưng vẫn giúp chúng ta nhận biết được sự thay đổi.

devServer.publicPath: Chứa đường dẫn tương đối từ thư mục root trỏ đến thư mục build (ở đây là dist). Chú ý phải thêm / ở trước và sau

devServer.watchContentBase: Nếu bạn có thay đổi gì trong file index.html thì trình duyệt cũng tự động reload.

Oke. Vậy là xong rồi đó. Khi bạn muốn chạy ở môi trường dev thì chỉ cần

yarn start

Mở trình duyệt lên tại địa chỉ http://localhost:3000, bạn sẽ thấy giao diện project

Thành quả xứng đáng :v

Thành quả xứng đáng :v

Muốn build webpack thì

yarn build
Build Webpack

Thư mục dist sau khi build

Sau khi build xong bạn có thể sử dụng file public/index.html để chạy.

Vấn đề về Tree Shaking đối với webpack

Các bạn để ý file add.js có function treeShaking được export nhưng không được dùng ở bất kỳ đâu. Vậy nên webpack sẽ không dùng function này, vì thế nó sẽ không có trong code ở file build. Đây được gọi là tính năng tree shaking của webpack nhằm giúp giảm bớt code dư thừa (unused module export detection).

Tính năng này chỉ hoạt động với code dùng cú pháp ES Module (import export đó :mrgreen: ), còn lại thì sẽ không hoạt động nhé.

Điển hình là thư viện lodash . Nếu bạn import { get } from 'lodash'  thì  nó sẽ không tree-shake, webpack vẫn sẽ import cả 1 thư viện lodash khổng lồ vào file bundle của bạn. Chẳng khác gì so với việc  import _ from 'lodash'

Để fix vấn đề trên thì ta có thể dùng cú pháp import get from 'lodash/get', hoặc đơn giản hơn là dùng thư viện lodash es , vì thư viện này được build bằng es module.

Tóm lại

Bài này khá nhiều kiến thức, nhưng mình đã giải thích rất kĩ rồi. Việc của mọi người là tải project về và tự tay thay đổi các thông số option thử là quen tay ngay.

Hẹn anh em ở bài sau với nhiều cấu hình chuyên sâu hơn nha 😉

Github: link github project, folder bài 1 nhé 😀

Tham khảo:

https://www.andrewsouthpaw.com/basic-babel-webpack-setup-in-2020/