Webpack 轉譯 Typescript 現有方案

秋天不落葉發表於2020-01-21

現有方案

1. awesome-typescript-loader

  • 這個 npm 包好久不更新了,而且型別檢查的時候會有遺漏,所以不推薦使用

2. ts-loader + babel-loader + fork-ts-checker-webpack-plugin

  • 這種方案,當 webpack 編譯的時候,ts-loader 會呼叫 typescript(所以本地專案需要安裝 typescript),然後 typescript 執行的時候會去讀取本地的 tsconfig.json 檔案。
  • 預設情況下,ts-loader 會進行 轉譯型別檢查,每當檔案改動時,都會重新去 轉譯型別檢查,當檔案很多的時候,就會特別慢,影響開發速度。所以需要使用 fork-ts-checker-webpack-plugin ,開闢一個單獨的執行緒去執行型別檢查的任務,這樣就不會影響 webpack 重新編譯的速度。
  • fork-ts-checker-webpack-plugin 這個外掛要求最低 Node.js 6.11.5,webpack 4,TypeScript 2.1 和可選的 ESLint 6(其本身要求最低 Node.js 8.10.0)。

webpack 配置方法一

  • 使用並行化構建提升速度(thread-loader),對於 webpack 4+ 來說,速度提升好像不是很明顯

webpack.config.js

const cpus = require('os').cpus().length;
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
    module: {
        rules: [
                // 單程式
                //{
                    // test: /\.tsx?$/, 
                    // 預設情況下,ts-loader 會進行轉譯和型別檢查
                    // 因為是單程式,所以 webpack 可以收集到錯誤資訊,並通過 dev-server 反饋到瀏覽器
                    // 但這也導致了 webpack 構建速度極慢
                    // use:['ts-loader']
                //},

                // 多程式
                {
                    test/\.tsx?$/,
                    exclude/node_modules/,
                    use: [
                        {loader'cache-loader'},
                        {
                            loader'thread-loader',
                            options: {
                                workers: cpus - 1,
                            },
                        },
                        'babel-loader',
                        {
                            loader'ts-loader',
                            options: {
                                // 關閉型別檢查,即只進行轉譯
                                // 型別檢查交給 fork-ts-checker-webpack-plugin 在別的的執行緒中做
                                // transpileOnly: true,
                                // 如果設定了 happyPackMode 為 true
                                // 會隱式的設定 transpileOnly: true
                                happyPackMode: true
                                // 如果是 vue 應用,需要配置下這個
                                // appendTsSuffixTo: [/\.vue$/]
                            }
                        }
                    ]
                },
                {
                    test/\.(js|jsx)$/,
                    use: ['happypack/loader?id=js'],
                    exclude: [/node_modules/, /(.|_)min\.js$/],
                    // include: [
                    //     path.resolve(__dirname, "src")
                    // ],
                }
        ],
 },
 plugins: [
        // fork 一個程式進行檢查
        new ForkTsCheckerWebpackPlugin({
           // async 為 false,同步的將錯誤資訊反饋給 webpack,如果報錯了,webpack 就會編譯失敗
           // async 預設為 true,非同步的將錯誤資訊反饋給 webpack,如果報錯了,不影響 webpack 的編譯
           // async: false,
               // eslint: false,
           checkSyntacticErrors: true
        })
  ]
};
複製程式碼

tsconfig.json

{
  "compilerOptions": {
    //"module""commonjs",
    "target""es5",
    /* 'react' 模式下,ts 會將 tsx 編譯成 jsx 後再將 jsx 編譯成 js*/
    /* 'preserve' 模式下:TS 會將 tsx 編譯成 jsx 後,不再將 jsx 編譯成 js,保留 jsx */
    /* 保留 jsx 時,就需要在 ts-loader 前面配置 babel-loader 去處理 jsx */
    /* 換句話說:只有想要用 babel-laoder 的時候,才需要設定這個值 */
    "jsx""preserve",
  },
}
複製程式碼

webpack 配置方法二

  • 編譯 ts/tsx 檔案時,不使用並行化構建(thread-loader)
const cpus = require('os').cpus().length;
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
    module: {
        rules: [
                {
                    test/\.tsx?$/,
                    exclude/node_modules/,
                    use: [
                       'babel-loader',
                        {
                            loader'ts-loader',
                            options: {
                                // 關閉型別檢查,即只進行轉譯
                                // 型別檢查交給 fork-ts-checker-webpack-plugin 在別的的執行緒中做
                                transpileOnly: true,
                            }
                        }
                    ]
                },
                {
                    test/\.(js|jsx)$/,
                    use: ['happypack/loader?id=js'],
                    exclude: [/node_modules/, /(.|_)min\.js$/],
                    // include: [
                    //     path.resolve(__dirname, "src")
                    // ],
                }
        ],
 },
 plugins: [
        // fork 一個程式進行檢查
        new ForkTsCheckerWebpackPlugin({
           // async 為 false,同步的將錯誤資訊反饋給 webpack,如果報錯了,webpack 就會編譯失敗
           // async 預設為 true,非同步的將錯誤資訊反饋給 webpack,如果報錯了,不影響 webpack 的編譯
           // async: false,
               // eslint: false,
           checkSyntacticErrors: true
        })
  ]
};
複製程式碼

tsconfig.json

{
  "compilerOptions": {
    //"module""commonjs",
    "target""es5",
    /* 'react' 模式下,ts 會將 tsx 編譯成 jsx 後再將 jsx 編譯成 js*/
    /* 'preserve' 模式下:TS 會將 tsx 編譯成 jsx 後,不再將 jsx 編譯成 js,保留 jsx */
    /* 保留 jsx 時,就需要在 ts-loader 前面配置 babel-loader 去處理 jsx */
    /* 換句話說:只有想要用 babel-laoder 的時候,才需要設定這個值 */
    "jsx""preserve",
  },
}
複製程式碼

3. babel-loader + @babel/preset-typescript

  • 這種方案,當 webpack 編譯的時候,babel-loader 會讀取 .babelrc 裡的配置,不會呼叫 typescript(所以本地專案無需安裝 typescript),不會去檢查型別
  • 但是 tsconfig.json 是需要配置的,因為需要在開發程式碼時,讓 idea 提示錯誤資訊

webpack.config,js

rules: [
        {
          test:/\.(tsx?|jsx?)$/,
          // 預設會呼叫 @babel/core 
          use:'babel-loader'
        }
]
複製程式碼

.babelrc

{
    "presets": [
         "@babel/preset-env"
            "@babel/preset-react",
            "@babel/preset-typescript"
    ]
}
複製程式碼

常見問題

awesome-typescript-loader 與 ts-loader 的主要區別

  • awesome-typescript-loader 不需要安裝額外的外掛,可以通過內建的 CheckerPlugin 外掛,把型別檢查放在獨立的程式中執行
  • 編譯時間對比:
  • 如果都是用預設配置的話,awesome-typescript-loader 的速度相對快一些
  • 如果都設定了禁止型別檢查的選項,ts-loader 的速度相對快一些
  • 如果都設定了禁止型別檢查的選項並且將型別檢查放到獨立的程式中執行,awesome-typescript-loader 相對快一些

並行構建不再適合新版本的 webpack 了

It's possible to parallelise your builds. Historically this was useful from a performance perspective with webpack 2 / 3. With webpack 4+ there appears to be significantly less benefit and perhaps even cost.
But if that's what you want to do, there's two ways to achieve this: happypack and thread-loader. Both should be used in combination with fork-ts-checker-webpack-plugin for typechecking.)

  • 並行化構建有兩種方式: happypackthread-loader
  • 並行化構建對於 webpack 2/3 的效能有明顯的提升,使用 webpack 4+時,速度提升的收益似乎要少得多。

使用了 TypeScript,為什麼還需要 Babel

  • 大部分已存專案依賴了 babel
  • 有些需求/功能需要 babel 的外掛去實現(如:按需載入)
  • babel 有非常豐富的外掛,它的生態發展得很好
  • babel 7 之前:需要前面兩種方案來轉譯 TS
  • babel 7 之後:babel 直接移除 TS,轉為 JS,這使得它的編譯速度飛快

為什麼用了 ts-loader 後,還要使用 babel-loader

  • ts-loader 是不會讀取 .babelrc 裡的配置,即無法使用 babel 系列的外掛,所以直接使用 ts-loader 將 ts/tsx 轉成 js ,就會出現墊片無法按需載入、antd 無法按需引入的問題。所以需要用 ts-loader 把 ts/tsx 轉成 js/jsx,然後再用 babel-loader 去呼叫 babel 系列外掛,編譯成最終的 js。

如何選擇轉譯方案

如果在使用 babel-loader + @babel/preset-typescript 這種方案時,也想要型別檢查,該怎麼做

package.json

{
  "scripts": {
     // 再開一個 npm 指令碼自動檢查型別
    "type-check""tsc --watch",
  },
    "devDependencies": {
      "@babel/cli""^7.4.4",
      "@babel/core""^7.4.5",
      "@babel/plugin-proposal-class-properties""^7.4.4",
      "@babel/plugin-proposal-object-rest-spread""^7.4.4",
      "@babel/preset-env""^7.4.5",
      "@babel/preset-typescript""^7.3.3",
      "typescript""^3.5.2"
    }
}
複製程式碼

tsconfig.json

{
  "compilerOptions": {
    // 不生成檔案,只做型別檢查
         "noEmit"true,                      
  },
}
複製程式碼

使用 @babel/preset-typescript 需要注意的地方

有四種語法在 babel 中是無法編譯的

  • namespace:不要再用了,已經過時了。改用標準的 ES6 module(import/export),在推薦的 tslint 規則中也建議不要使用 namesapce。
namespace Person{
    const name = 'abc';
}
複製程式碼
  • 型別斷言:改用 as 來斷言型別(但是在 demo 中試了下,好像沒報錯,不知道是不是可以正常編譯了)。
interface Person {
    name: string;
    age: number
}

let p1 = {age: 18} as Person;
console.log(p2.name);

let p2 = <Person>{age: 18};
console.log(p3.name);
複製程式碼
  • 常量列舉
const enum Sex {
    man,
    woman
}
複製程式碼
  • 歷史遺留風格的 import/export 語法:import xxx= require(…)export = xxx

Typescript 官方轉向 ESLint 的原因

  • TSLint 執行規則的方式存在一些架構問題,從而影響了效能,而修復這些問題會破壞現有規則;
  • ESLint 的效能更好並且使用者較多

使用了 TypeScript,為什麼還需要 ESLint

  • TS 主要是用來做型別檢查和語言轉換的,順帶一小部分的語法檢查
  • ESLint 主要是用來檢查程式碼風格和語法錯誤的

使用 `npx create-react-app xxx --typescript` 可以快速建立 TS 專案

三種方案的配置 Demo

參考

[譯] TypeScript 和 Babel:一場美麗的婚姻

使用@babel/preset-typescript取代awesome-typescript-loader和ts-loader

build-performance

https://segmentfault.com/q/1010000019545436

推薦閱讀

你真的瞭解 React 生命週期嗎

React Hooks 詳解 【近 1W 字】+ 專案實戰

React SSR 詳解【近 1W 字】+ 2個專案實戰

從 0 到 1 實現一款簡易版 Webpack

傻傻分不清之 Cookie、Session、Token、JWT

相關文章