Phát hành, gửi và cài đặt JavaScript hiện đại cho các ứng dụng nhanh hơn

Cải thiện hiệu suất bằng cách bật các phần phụ thuộc và đầu ra JavaScript hiện đại.

Hơn 90% trình duyệt có khả năng chạy JavaScript hiện đại, nhưng sự phổ biến của JavaScript cũ vẫn là một nguồn lớn gây ra các vấn đề về hiệu suất trên web ngay hôm nay.

JavaScript hiện đại

JavaScript hiện đại không có đặc điểm là mã được viết bằng một ECMAScript cụ thể phiên bản chỉ dẫn kỹ thuật mà là trong cú pháp được hỗ trợ bởi trình duyệt. Các trình duyệt web hiện đại như Chrome, Edge, Firefox và Safari tạo thành hơn 90% thị trường trình duyệt và các trình duyệt khác nhau dựa vào cùng một công cụ kết xuất cơ bản tạo nên 5%. Điều này có nghĩa là 95% lưu lượng truy cập web trên toàn cầu đến từ các trình duyệt hỗ trợ các tính năng ngôn ngữ JavaScript được sử dụng rộng rãi nhất trong 10 năm, bao gồm:

  • Lớp (ES2015)
  • Các hàm mũi tên (ES2015)
  • Máy phát điện (ES2015)
  • Phạm vi khối (ES2015)
  • Phá vỡ cấu trúc (ES2015)
  • Tham số nghỉ và trải rộng (ES2015)
  • Viết tắt đối tượng (ES2015)
  • Không đồng bộ/đang chờ (ES2017)

Các tính năng trong phiên bản mới hơn của thông số kỹ thuật ngôn ngữ thường có ít hỗ trợ nhất quán trên các trình duyệt hiện đại. Ví dụ: nhiều ES2020 và ES2021 các tính năng mới chỉ được hỗ trợ tại 70% thị trường trình duyệt—vẫn còn phần lớn trình duyệt, nhưng không đủ an toàn để dựa trực tiếp vào các tính năng đó. Chiến dịch này có nghĩa là mặc dù quảng cáo "hiện đại" JavaScript là một mục tiêu luôn thay đổi, ES2017 có khả năng tương thích rộng nhất với trình duyệt trong khi bao gồm hầu hết tính năng cú pháp hiện đại thường dùng. Nói cách khác, ES2017 là cú pháp gần giống nhất với cú pháp hiện đại hiện nay.

JavaScript cũ

JavaScript cũ là mã giúp tránh sử dụng tất cả ngôn ngữ trên các tính năng AI mới. Hầu hết các nhà phát triển đều viết mã nguồn bằng cú pháp hiện đại, nhưng biên dịch mọi thứ thành cú pháp cũ để tăng cường hỗ trợ trình duyệt. Biên dịch cú pháp cũ giúp tăng khả năng hỗ trợ của trình duyệt, tuy nhiên hiệu quả này thường nhỏ hơn chúng ta có thể nghĩ. Trong nhiều trường hợp, mức hỗ trợ tăng từ khoảng 95% lên 98%, đồng thời phải chịu một khoản chi phí đáng kể:

  • JavaScript cũ thường lớn hơn và chậm hơn khoảng 20% so với mã hiện đại tương đương. Lỗi công cụ và cấu hình sai thường xuyên giúp mở rộng khoảng cách này hơn nữa.

  • Các thư viện đã cài đặt chiếm đến 90% bản phát hành thông thường Mã JavaScript. Mã thư viện phát sinh JavaScript cũ thậm chí cao hơn mức hao tổn do có thể tránh được polyfill và trùng lặp trình trợ giúp bằng cách xuất bản mã hiện đại.

JavaScript hiện đại trên npm

Gần đây, Node.js đã chuẩn hoá một trường "exports" để xác định điểm truy cập cho gói:

{
  "exports": "./index.js"
}

Các mô-đun được tham chiếu bởi trường "exports" ngụ ý một phiên bản Nút ít nhất là 12.8, hỗ trợ ES2019. Điều này có nghĩa là bất kỳ mô-đun nào được tham chiếu bằng cách sử dụng Trường "exports" có thể được viết bằng JavaScript hiện đại. Người tiêu dùng gói phải giả định các mô-đun có trường "exports" chứa mã hiện đại và mã hoá nếu nếu cần.

Chỉ hiện đại

Nếu bạn muốn xuất bản một gói có mã hiện đại và để tuỳ chọn này để xử lý việc chuyển đổi mã khi họ sử dụng nó như một phần phụ thuộc—chỉ sử dụng Trường "exports".

{
  "name": "foo",
  "exports": "./modern.js"
}

Hiện đại với tính năng dự phòng cũ

Sử dụng trường "exports" cùng với "main" để xuất bản gói bằng cách sử dụng mã hiện đại nhưng cũng bao gồm tính năng dự phòng ES5 + CommonJS cho phiên bản cũ trình duyệt.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Hiện đại với tính năng tối ưu hoá bộ gói ESM và tính năng dự phòng cũ

Ngoài việc xác định điểm truy cập CommonJS dự phòng, trường "module" có thể được dùng để trỏ đến một gói dự phòng cũ tương tự, nhưng là gói sử dụng Cú pháp mô-đun JavaScript (importexport).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Nhiều trình gói dịch vụ, chẳng hạn như webpack và Rollup, dựa vào trường này để tận dụng các tính năng của mô-đun và cho phép cây rung lắc. Đây vẫn là một gói cũ không chứa mã hiện đại nào ngoài import/export, vì vậy, hãy sử dụng phương pháp này để gửi mã hiện đại bằng một dự phòng cũ mà vẫn được tối ưu hoá để nhóm.

JavaScript hiện đại trong các ứng dụng

Các phần phụ thuộc của bên thứ ba chiếm phần lớn trong quy trình sản xuất thông thường Mã JavaScript trong ứng dụng web. Mặc dù các phần phụ thuộc npm trước đây đã được xuất bản dưới dạng cú pháp ES5 cũ, đây không còn là một giả định an toàn và dẫn đến rủi ro việc cập nhật phần phụ thuộc làm hỏng khả năng hỗ trợ trình duyệt trong ứng dụng của bạn.

Với số lượng gói npm ngày càng tăng được chuyển sang JavaScript hiện đại, bạn cần đảm bảo rằng công cụ xây dựng được thiết lập để xử lý các lỗi đó. Có một khả năng một số gói npm mà bạn phụ thuộc vào đã sử dụng các tính năng ngôn ngữ khác. Có một số cách để sử dụng mã hiện đại từ npm mà không phá vỡ ứng dụng của bạn trong các trình duyệt cũ hơn, nhưng việc xem chung ý tưởng là đảm bảo hệ thống xây dựng chuyển đổi các phần phụ thuộc thành cùng một cú pháp làm mã nguồn của mình.

gói web

Kể từ webpack 5, giờ đây bạn có thể định cấu hình cú pháp webpack sẽ sử dụng khi tạo mã cho gói và mô-đun. Điều này không chuyển mã của mã hoặc phần phụ thuộc, thì điều này chỉ ảnh hưởng đến "glue" mã do webpack tạo. Để chỉ định mục tiêu hỗ trợ trình duyệt, hãy thêm cấu hình danh sách trình duyệt vào dự án của bạn hoặc thực hiện trực tiếp trong cấu hình gói web:

module.exports = {
  target: ['web', 'es2017'],
};

Bạn cũng có thể định cấu hình gói web để tạo các gói được tối ưu hoá bỏ qua các hàm trình bao bọc không cần thiết khi nhắm đến các Mô-đun ES hiện đại môi trường. Thao tác này cũng định cấu hình webpack để tải các gói phân tách mã bằng cách sử dụng <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Hiện có một số trình bổ trợ webpack giúp bạn biên dịch và gửi JavaScript hiện đại trong khi vẫn hỗ trợ các trình duyệt cũ, chẳng hạn như Optimize Plugin và v0EsmPlugin.

Trình bổ trợ Optimize

Trình bổ trợ Optimize là một gói web trình bổ trợ biến đổi mã đi kèm cuối cùng từ JavaScript hiện đại sang JavaScript cũ thay vì từng tệp nguồn riêng lẻ. Đây là một thiết lập độc lập cho phép cấu hình gói web của bạn để giả định mọi thứ đều là JavaScript hiện đại, không có phân nhánh đặc biệt cho nhiều đầu ra hoặc cú pháp.

Vì Trình bổ trợ Optimize hoạt động trên các gói thay vì từng mô-đun riêng lẻ, nên trình bổ trợ này xử lý mã của ứng dụng và các phần phụ thuộc như nhau. Điều này giúp các phần phụ thuộc JavaScript hiện đại từ npm, vì mã của các phần phụ thuộc này sẽ được nhóm và chuyển đổi sang cú pháp chính xác. Cũng có thể nhanh hơn các giải pháp truyền thống bao gồm 2 bước biên dịch, mà vẫn tạo ra các gói riêng cho trình duyệt hiện đại và cũ. Hai tập hợp gói là được thiết kế để tải bằng cách sử dụng mô-đun/không mô-đun mẫu.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin có thể nhanh và hiệu quả hơn gói web tuỳ chỉnh , thường là gói mã hiện đại và mã cũ một cách riêng biệt. Nó cũng xử lý việc chạy Babel cho bạn và giảm kích thước các gói bằng Terser với chế độ cài đặt tối ưu riêng cho kết quả hiện đại và cũ. Cuối cùng, polyfill cần thiết cho các gói cũ được trích xuất vào một tập lệnh chuyên dụng để chúng không bao giờ trùng lặp hoặc được tải một cách không cần thiết trong các trình duyệt mới.

So sánh: dịch chuyển mô-đun nguồn hai lần so với dịch chuyển gói được tạo.

BabelEsmPlugin

BabelEsmPlugin là một gói web trình bổ trợ hoạt động cùng với @babel/preset-env để tạo phiên bản hiện đại của các gói hiện có nhằm gửi mã ít được dịch hơn đến các trình duyệt hiện đại. Đây là giải pháp phổ biến nhất có sẵn để module/nomodule, được Next.js sử dụng và Dự đoán CLI.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin hỗ trợ nhiều cấu hình gói web vì đây là chạy hai bản dựng ứng dụng lớn riêng biệt. Việc biên dịch hai lần có thể mất thêm một chút thời gian cho các ứng dụng lớn, tuy nhiên kỹ thuật này cho phép BabelEsmPlugin để tích hợp liền mạch vào các cấu hình webpack hiện có và làm cho nó trở thành một trong những tuỳ chọn thuận tiện nhất hiện có.

Định cấu hình trình tải babel để chuyển đổi nút_modules

Nếu bạn đang sử dụng babel-loader mà không có một trong hai trình bổ trợ trước, bạn cần thực hiện một bước quan trọng để sử dụng npm JavaScript hiện đại các mô-đun. Việc xác định hai cấu hình babel-loader riêng biệt giúp bạn có thể để tự động biên dịch các tính năng ngôn ngữ hiện đại có trong node_modules thành ES2017, trong khi vẫn chuyển đổi mã bên thứ nhất của riêng bạn bằng adb các trình bổ trợ và giá trị đặt trước được xác định trong cấu hình của dự án. Điều này không tạo các gói hiện đại và cũ cho việc thiết lập mô-đun/không mô-đun, nhưng có giúp bạn có thể cài đặt và sử dụng các gói npm chứa JavaScript hiện đại mà không làm hỏng các trình duyệt cũ.

webpack-plugin-modern-npm sử dụng kỹ thuật này để biên dịch các phần phụ thuộc npm có trường "exports" trong package.json, vì các mã này có thể chứa cú pháp hiện đại:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Ngoài ra, bạn có thể triển khai kỹ thuật này theo cách thủ công trong gói web của mình bằng cách kiểm tra trường "exports" trong package.json các mô-đun khi chúng được giải quyết. Bỏ qua việc lưu vào bộ nhớ đệm để ngắn gọn, một tuỳ chỉnh phương thức triển khai có thể giống như sau:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

Khi dùng phương pháp này, bạn cần đảm bảo cú pháp hiện đại được hỗ trợ bởi công cụ khai thác của bạn. Cả hai Terseruglify-es có thể chỉ định {ecma: 2017} để duy trì và trong một số trường hợp tạo cú pháp ES2017 trong quá trình nén và định dạng.

Kết quả tổng hợp

Công cụ hợp nhất có sẵn tính năng hỗ trợ tạo nhiều nhóm gói trong một bản dựng duy nhất và tạo mã hiện đại theo mặc định. Do đó, Công cụ hợp nhất có thể được định cấu hình để tạo các gói hiện đại và cũ có trình bổ trợ chính thức mà bạn có thể đang sử dụng.

@rollup/plugin-babel

Nếu bạn sử dụng hợp nhất, Phương thức getBabelOutputPlugin() (được cung cấp bởi trình bổ trợ JDK chính thức) biến đổi mã trong các gói được tạo thay vì từng mô-đun nguồn. Công cụ hợp nhất có sẵn tính năng hỗ trợ tạo nhiều nhóm gói trong một bản dựng, mỗi bản dựng có trình bổ trợ riêng. Bạn có thể dùng công cụ này để tạo các gói khác nhau cho phiên bản hiện đại và cũ bằng cách truyền tải mỗi gói thông qua một Cấu hình trình bổ trợ đầu ra adb:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Công cụ xây dựng bổ sung

Cuộn và webpack có cấu hình cao, thường nghĩa là mỗi dự án phải cập nhật cấu hình để bật cú pháp JavaScript hiện đại trong các phần phụ thuộc. Ngoài ra còn có các công cụ xây dựng cấp cao hơn ưu tiên quy ước và chế độ mặc định hơn như Parcel, Snowpack, Vitekiện. Hầu hết các công cụ này giả định các phần phụ thuộc npm có thể chứa cú pháp hiện đại và sẽ chuyển đổi các phần đó thành (các) cấp cú pháp thích hợp khi tạo phiên bản phát hành công khai.

Ngoài các trình bổ trợ chuyên dụng cho webpack và Rollup, JavaScript hiện đại các gói có tính năng dự phòng cũ có thể được thêm vào bất kỳ dự án nào sử dụng phát triển. Quá trình phát triển là một công cụ độc lập biến đổi đầu ra từ một hệ thống xây dựng để tạo ra các sản phẩm cũ Các biến thể JavaScript, cho phép gói và chuyển đổi giả định mục tiêu đầu ra.