捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:webpack 5 與 TypeScript

第 4 天個人曾經推薦在 tsconfig.json 中透過 paths 設定短路徑、省去在檔案間互相 import 時計算路徑的麻煩。而今天就會來詳細解釋如何為 webpack 加上 alias 設定,以及要安裝哪些 loader 來讓 webpack 能夠將 TypeScript 轉為瀏覽器能夠理解的 JavaScript 內容。

流程說明

先來順一下今天預計要做的事情:

  1. tsconfig.json 讀取 paths 資料,並把這些內容轉為 webpack alias 能夠使用的格式
  2. 將 alias 灌進 webpack 開發環境設定中。注意:沒有提供 alias 的話,webpack-dev-server 拉起來之後,是無法正確解析我們在 tsconfig.json 中自訂的 @Asset/* 這類路徑的
  3. 為 webpack 加上 esbuild-loader 來將 .ts/.tsx/.js 處理成瀏覽器環境能夠讀懂的格式。選擇 esbuild-loader 是因為此套件是用 Go 寫的,編譯速度較快
  4. ./public 資料夾中新增一個給 React app 掛載的 index.html 檔案,然後設定 html-webpack-plugin 讓 webpack 知道去哪裡撈 html 內容

完成以上步驟後,你的本機伺服器就能正常顯示專案中的 TypeScript 內容了 (・∀・)

處理 alias

首先在專案根目錄的 ./tool 資料夾中新增 resolvePath.ts

/* Packages */
import fs from 'fs';
import path from 'path';

/* Data */
const APP_ROOT = fs.realpathSync(process.cwd());

/* Functions */
export function resolvePath(file: string) {
  return path.resolve(APP_ROOT, file);
}

這個工具會負責回傳「傳入的參數相對於專案根目錄的絕對路徑」。比如當我呼叫 resolvePath('src/index') 時,我會得到(以 mac 環境為例)/Users/<user name>/ithome-2023/src/index 這樣的路徑資訊。

接著新增 ./config/data/alias.ts 並撰寫以下內容:

/* Tools */
import { resolvePath } from '@/tool/resolvePath';

/* Data */
import tsConfig from '@/tsconfig.json';

type PathPair = [string, string];

/* Main */
const paths: PathPair[] = Object.entries(tsConfig.compilerOptions.paths).map(
  (pathPair) => {
    const [pathKey, pathValue] = pathPair;
    return [pathKey.replace('/*', ''), pathValue.join().replace('/*', '')];
  }
);
const alias = paths.reduce((reducedValue, currentValue) => {
  const [key, pathToResolve] = currentValue;
  return {
    ...reducedValue,
    [key]: resolvePath(pathToResolve),
  };
}, {});

export default alias;

首先透過 Object.entries(tsConfig.compilerOptions.paths) 將 tsconfig.json 中的 paths 物件轉為 [key, value] 陣列,再將 key 部分的 @Asset/* 轉為 @Asset、將 value 部分的 ["src/asset/*"] 轉為 src/asset。印出來看會長這樣:

[
  [ '@', '.' ],
  [ '@Api', './src/api' ],
  [ '@Asset', './src/asset' ],
  [ '@Component', './src/component' ],
  [ '@Hook', './src/hook' ],
  [ '@Model', './src/model' ],
  [ '@Reducer', './src/reducer' ],
  [ '@Style', './src/style' ],
  [ '@Tool', './src/tool' ]
]

然後使用 Array..prototype.reduce() 並搭配一開始寫好的 resolvePath 工具,將 paths 加工為以下格式:

{
  '@': '/Users/<user name>/ithome-2023/',
  '@Api': '/Users/<user name>/ithome-2023/src/api',
  '@Asset': '/Users/<user name>/ithome-2023/src/asset',
  '@Component': '/Users/<user name>/ithome-2023/src/component',
  '@Hook': '/Users/<user name>/ithome-2023/src/hook',
  '@Model': '/Users/<user name>/ithome-2023/src/model',
  '@Reducer': '/Users/<user name>/ithome-2023/src/reducer',
  '@Style': '/Users/<user name>/ithome-2023/src/style',
  '@Tool': '/Users/<user name>/ithome-2023/src/tool'
}

接下來把這個 alias 變數餵進 resolve.alias 中,webpack 就能認得我們在 tsconfig.json 中自訂的路徑了:

const webpackDevelopmentConfig: WebpackConfiguration = {
  resolve: {
    alias,
  },
};

處理 TypeScript

以下是個人慣用的最小啟動+處理 TypeScript 設定:

const webpackDevelopmentConfig: WebpackConfiguration = {
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        loader: 'esbuild-loader',
        options: {
          loader: 'tsx',
          target: 'es2015',
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.ts', '.tsx'],
    alias,
  },
};

較詳細的說明如下:

現在你可以放心撰寫 TypeScript 檔案了 (*゚ー゚)

目前為止的 webpack 設定

只差一點點就能啟動了!現在加上 html-webpack-plugin 來讓 webpack 知道要去哪裡抓 index.html 模板(記得模板內要留一個 html 元件綁 id="root" 讓 React app 有地方住)。另外個人也習慣設定 devtool: source-map 來確保除錯時能夠取得最完整的檔案名、程式行數資訊:

/* Packages */
import HtmlWebpackPlugin from 'html-webpack-plugin';
import Webpack from 'webpack';

/* Tools */
import { resolvePath } from '@/tool/resolvePath';

/* Data */
import alias from './data/alias';
import env from './data/env';
import type {
  WebPackDevServerConfiguration,
  WebpackConfiguration,
  EnvForStartApp,
} from './data/types';

const envForStartApp = env['process.env'] as EnvForStartApp;
const port = +JSON.parse(envForStartApp.APP_PORT);

/* Main */
const webpackDevelopmentConfig: WebpackConfiguration = {
  mode: 'development',
  entry: resolvePath('src/index'),
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js|ts|tsx)$/,
        loader: 'esbuild-loader',
        options: {
          loader: 'tsx',
          target: 'es2015',
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: resolvePath('public/index.html'),
      publicPath: '/',
    }),
    new Webpack.DefinePlugin({ ...env }),
  ],
  resolve: {
    extensions: ['.js', '.ts', '.tsx'],
    alias,
  },
};

export const webpackDevServerConfig: WebPackDevServerConfiguration = {
  port,
  open: true,
  historyApiFallback: true,
};

export default webpackDevelopmentConfig;

./public/index.html 先簡單寫就好,後續開始部署或是有 SEO 優化需求時再更新即可:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ithome 2023 demo</title>
  </head>
  <body>
    <main id="root"></main>
  </body>
</html>

現在 webpack 已經知道如何編譯 TypeScript、也知道去哪裡尋找 index.html 內容了。在終端輸入 make start (參考第 6 天)來看看到目前為止的成果吧 (≖ᴗ≖๑)