从零开始,配置一套现代前端工具链

现代前端应用框架(如 Next.js、Nuxt.js 等)都直接集成了完整的工具链,按照官方文档做,一行命令就可以配置完毕。这整套工具在我们调试和构建项目时,在背后做了大量工作。虽然这有助于快速上手,但是非常不利于我们了解其中的原理。

然而,各种工具纷繁复杂,文档浩如烟海。由于工具之多,即使文档再友好、工具本身再易用,也很难快速入门。

本文将带你踏上一段旅程,从一个空文件夹开始,一步一步添加工具,最终配置一套完整的前端工具链。在其中,我们可以对各个工具的概念、用途和原理有一个比较系统的认识。每个部分都列出了相关文档的链接,方便查阅。

我们使用 React.js 前端框架,使用 Tailwind 编写 CSS,使用 TypeScript 编写脚本,并使用 ESLint 进行代码检查。最终,希望达到和使用 create-react-app 工具创建的项目类似的开发体验。

TL;DR

配置完毕后,整套工具链的示意图如下:

配置完毕后的工具链示意图

从创建一个 npm 项目开始

创建一个空目录(一般目录名就是项目名),进入其中执行 npm init,这个命令会交互式地让你填写该项目的元信息。

我们将这个项目命名为 study-chain(意为 study frontend toolchain):

mkdir study-chain
cd study-chain
npm init

确认信息之后,目录下会生成 package.json 文件,记录了项目的元信息:

// package.json
{
  "name": "study-chain",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "SkyWT",
  "license": "ISC"
}

接下来,我们以 moment 模块为例,这是一个用于转换日期格式的模块(这个模块其实已经废弃,不推荐新项目使用。我们只是将其作为示例,参见文档)。

在项目根目录下,使用 npm i 安装模块:

npm i moment

我们只使用这个模块的一个功能为例:将模块引入为 moment 之后,moment().format() 返回当前日期时间字符串。

Webpack

让我们先忘掉 React.js,从编写纯 HTML 和 JavaScript 开始。如何在这个项目里使用之前安装的 moment 模块呢?

考虑编写一个简单的 HTML 文件 index.html

<html>
  <body>
    <div id="app"></div>
    <script src="./index.js"></script>
  </body>
</html>

这个 HTML 引用了 index.js。这个 js 文件引入了 moment 模块,将 div 内的内容设置为当前时间:

// index.js
const moment = require("moment");

const app = document.getElementById("app");
app.innerText = moment().format();

当然,如果此时在浏览器中打开 HTML,这段 js 是无法运行的。因为 require 是 Node.js 的语法,浏览器并不支持。

但是我们知道这个模块就在本地,它的源文件就在 node_modules/moment 路径下。我们需要一个工具获取这个模块,整合进这段 js 里。这种工具就叫做 bundler。有了 bundler,即使在用于前端的 js 中,我们也能引入模块了。

Webpack 就是其中之一。

类似的工具有:Rollup、Parcel。

安装与使用

首先安装 webpack 和 webpack-cli。后者是配套的命令行工具。这两个工具都只是在开发阶段使用,所以使用 --save-dev 安装为开发环境依赖:

npm i webpack webpack-cli --save-dev

安装后,可以直接使用 npx webpack 命令:

npx webpack ./index.js --mode=development

这个命令处理 index.js 文件,解析其中引用的模块,将对应的 js 代码注入该文件。参数 --mode=development 指示生成开发环境下易于调试的文件版本。如果在生产环境,应使用 --mode=production

运行之后,会生成 dist/main.js(这是默认的输出文件,可配置),这就相当于浏览器版的源文件。于是,修改 HTML 中引用的 script 路径:

<html>
  <body>
    <div id="app"></div>
    <script src="./dist/main.js"></script>
  </body>
</html>

用浏览器打开,可以发现成功地调用了该模块,div 中显示了当前的日期时间。

使用 --watch 参数可以使 webpack 保持运行,持续监听源文件的修改:

npx webpack ./index.js --mode=development --watch

运行时,每当编辑 index.js 并保存,都会自动重新生成 dist/main.js 文件。可以在终端看到对应的输出。

除了 require 语法,webpack 也支持更常用的 import 语法。刚才的引入模块语句可以改成:

// index.js
import moment from "moment";
// ...

配置文件 webpack.config.js

使用 webpack 的配置文件,可以替代运行命令时传递的参数,让命令行的使用更简洁和灵活。(相关文档)

在项目根目录创建名为 webpack.config.js 的文件,内容如下:

// webpack.config.js
const path = require("path");
module.exports = {
  mode: 'development',
  entry: './index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, "dist")
  }
};

创建了配置文件之后,使用命令行时,只需要使用如下命令:

npx webpack
npx webpack --watch

设置 npm scripts

为了方便起见,可以将以上 webpack 命令设置为 npm scripts。

编辑 package.json 文件,添加 scripts:

// package.json
{
  // ...
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
  },
  // ...
}

保存之后,只需使用如下命令,就等同于运行设置的 webpack 命令:

npm run build
npm run watch

为了表述方便,下文将运行 npm run build 命令的这一操作简称为 build。

生成 HTML

现在,构建完成后,访问 index.html 就能看到我们的网站。然而可以发现,这个 HTML 中 main.js 需要我们手动引用。能否让 webpack 帮我们完成这件事情呢?

这就需要让 webpack 为我们在 dist 目录中生成 HTML 文件。这可以通过 html-webpack-plugin 这个插件实现。没错,webpack 不仅是一个打包工具,其还拥有着丰富的插件生态。

运行以下命令安装 html-webpack-plugin(文档):

npm i html-webpack-plugin --save-dev

index.html 重命名为 template.html,内容如下:

<html>
  <body>
    <div id="app"></div>
  </body>
</html>

接下来修改 webpack.config.js,添加 html-webpack-plugin 插件的配置:

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
// ...
module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: "template.html"
    })
  ]
};

再次使用 build 构建,会发现 dist 目录下生成了 index.html,这个 HTML 引用了生成的 main.js 脚本。打开就能看见其实现了我们要的应用逻辑。

使代码可以 import CSS 文件

现在,有了 webpack 的加持,我们的 js 代码已经可以导入 module 了。但是如果需要引入其他静态资源,比如 CSS 文件,还是无法直接完成。为了使代码能直接 import 其他类型的文件,webpack 中可以安装配置一种称为 loader 的模块。

💡 Webpack 中的 loader 与 plugin:二者都是可以集成到 webpack 的模块,但是两个不同的概念。loader 一般用于处理特定类型的文件,而 plugin 可以提供更加广泛的功能。

比如,为了引入 CSS 文件,可以安装 style-loader 和 css-loader 两个模块(相关文档):

npm i style-loader css-loader --save-dev

接下来,修改 webpack 配置文件,添加一条规则:对于文件名以 .css 结尾的文件,使用这两个模块:

// webpack.config.js
// ...
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
 };

Webpack 会按照配置的顺序调用 loader。在该配置文件下,先调用 style-loader,再调用 css-loader。这两个 loader 分别的作用是:

style-loader:将一个 CSS 文件注入 DOM,放在 <style> 元素中。(文档)css-loader:解析 CSS 中的 @importurl() 等语句,将对应引用的文件配置好。(文档)

现在,可以使用 import 语句导入 CSS 文件了。首先还是在根目录下编写 style.css 文件:

// style.css
.bg-gray {
  background-color: #aaa;
}

保存后,修改 index.js 文件:

// index.js
import moment from "moment";
import "./style.css";

const app = document.getElementById("app");
app.innerText = moment().format();

app.classList.add("bg-gray");

重新 build 后,打开 HTML 即可发现样式的变化。

PostCSS

顾名思义。PostCSS 能够对 CSS 文件进行「后处理」(post-processing)。

和之前提到的 style-loader 和 css-loader 一样,PostCSS 也可以作为 loader 集成到 webpack。

集成到 webpack

首先还是安装 postcss-loader,同时安装 PostCSS 的一个插件 autoprefixer:

npm i postcss-loader autoprefixer --save-dev

webpack.config.js 中添加配置:

// webpack.config.js
// ...
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          "postcss-loader",
        ],
      },
    ],
  },
  // ...
}

⚠️ 注意:此处调用 css-loader 处添加了 options,将 importLoaders 设置为 1。这是考虑到 PostCSS 可能引入新的 @import 等语句,css-loader 要在其运行之后重新进行解析(相关文档)。如果确定 PostCSS 不会添加新的 @import 等语句,则此参数可不加。(可参考 GitHub 上的相关讨论)

配置文件 postcss.config.js

接下来创建 PostCSS 的配置文件,项目根目录下的 postcss.config.js 文件(文档):

// postcss.config.js
/** @type {import('postcss-load-config').Config} */
module.exports = {
  plugins: [require("autoprefixer")],
};

在以上的配置文件中,我们加载了 PostCSS 的 autoprefixer 插件(文档)。由于浏览器支持的差异,部分浏览器中使用某些样式需要加上特定的前缀,比如 webkit 或者 moz,这叫做 vendor prefix。这个插件会自动添加这种前缀,确保样式的兼容性。这里使用此插件只是为了演示 PostCSS 插件的使用,因为接下来我们将配置使用 Tailwind 插件。

PostCSS 是 webpack 的插件,autoprefixer 又是 PostCSS 的插件,也就是 webpack 的插件的插件。接下来我们还可以安装 Tailwind 的插件,即 webpack 的插件的插件的插件。前端工具链就是如此。

Tailwind CSS

使用过 Tailwind 之后,在开发任何前端项目时,我的心理状态:

没有它我不能活!😭😭😭

是的,之后在开发任何前端项目的时候,我没有一次离开过 Tailwind。即使是写纯 HTML 也要从 CDN 引入静态文件。因为它彻底改变了我们编写样式的方式。

作为现代前端项目,Tailwind 当然是必备的工具。

集成到 PostCSS

首先还是安装 Tailwind:

npm install tailwindcss --save-dev

接下来,在 PostCSS 中添加 Tailwind 插件(官方指南):

// postcss.config.js
/** @type {import('postcss-load-config').Config} */
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
};

配置文件 tailwind.config.js

下一步,使用以下命令创建 Tailwind 的配置文件:

npx tailwindcss init

Tailwind 会生成自己的配置文件 tailwind.config.js(文档):

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

配置文件中的 content 的值,是一个字符串数组,其中存放着需要处理的文件路径。Tailwind 会尝试检测所有匹配的文件中出现的 class 值,并添加对应的 CSS 定义。

为了匹配我们根目录下的模板 HTML 和 js 文件,删除路径中 src 部分。(或者也可以将所有源文件放到 src 子目录里——大多数项目都是这样做的。下一步在整理文件环节,我们也会这样做)。

接下来,在我们引用的主样式表(即 style.css)的开头,加上 @tailwind 指令:

// style.css
@tailwind base;
@tailwind components;
@tailwind utilities;

.bg-gray {
    background-color: #aaaaaa;
}

大功告成。接下来可以尝试修改 HTML 模板并重新 build,就可以发现能使用 Tailwind 了!

<html>
  <body>
    <div class="text-4xl" id="app"></div>
  </body>
</html>

(当然,集成到 PostCSS 并不是使用 Tailwind 的唯一方式。官方的Get started 中提供了大量框架、工具的集成指南)

安装 Tailwind 插件

没错,Tailwind 也有插件生态,比如 tailwindcss-animated(文档)和 typography(文档),这两个插件我都比较常用。

Tailwind 插件配置起来并不难,这里不再展开了,可以查阅相关文档。

中场休息:整理目录结构

至此,CSS 相关的工具配置完了。在进行下一步之前,是时候整理一下我们项目的目录结构了。

如前文所述,为了让项目目录更简洁,我们将所有源文件移动到新建的 src 文件夹内。移动之后,项目目录结构如下:

node_modules/
  ...
dist/
  ...
src/
  template.html
  index.js
  style.css
package-lock.json
package.json
postcss.config.js
tailwind.config.js
webpack.config.js

为了使所有工具只处理 src 目录下的文件,需要修改部分配置文件。

修改 tailwind.config.js

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{html,js}"],
  // ...
};

修改 webpack.config.js

// webpack.config.js
module.exports = {
  // ...
  entry: "./src/index.js",
  // ...
};

这下,我们的项目目录就干净了很多。是时候进行下一步了!

Babel

JavaScript 是浏览器原生支持的唯一语言,但:1)不同浏览器对该语言的新特性支持有所不同;2)许多人不喜欢 JavaScript 弱类型的特性,TypeScript 应运而生。但浏览器本身不支持 TypeScript。

所以,需要这样一种工具:1)将 JavaScript 的新特性相关代码转换为使用旧特性的实现;2)将 TypeScript 翻译为 JavaScript。这个过程和 C++ 这类语言「编译」的过程有些相似,只是目标是 JavaScript 而非二进制。

这种工具就叫做 transpiler(可以翻译成「转译器」)。它的作用是将一段代码「翻译」成另一段代码,但目标代码仍然是高级语言(一般是 JavaScript)。这个「翻译」和传统编程语言中的「Compile」概念不同,称为「Transpile」。

Babel 就是其中之一。

(吐槽:既然都要 transpile 才能运行代码,不如直接 compile 成更低级的字节码,执行效率还会更高。Web Assembly 就这样诞生了。不过这里不介绍了)

集成到 webpack

Babel 可以和 PostCSS 一样作为 loader 集成在 webpack 里。安装 Babel:

npm i babel-loader @babel/core --save-dev

安装后,修改 webpack 配置文件,对 .js 文件使用 babel-loader(排除 node_modules 目录中的文件):

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

(同样,集成到 webpack 也非安装 Babel 的唯一方式。官方指南提供了很多种配置方式)

目前,重新 build 时,虽然会调用 babel-loader,但是 Babel 还什么事情都没做。这是因为我们没有为其指定任何规则。一般可以通过 preset 指定规则。

presets

Babel 中的 preset 这一概念,官方的定义是「可分享的一组插件和配置的集合」(文档)。

官方提供了四种 preset:

env:用于将较新的 ECMAScript 特性转译为兼容较旧环境的实现。react:用于转译 React.js 的 JSX 语法。typescript:用于转译 TypeScript。flow:用于 flow 工具,这是一个静态类型检查器。

配置文件 babel.config.json

在项目根目录下创建配置文件 babel.config.json,其中可以添加 preset 指定规则。我们先添加一个 preser-env:

// babel.config.json
{
  "presets": ["@babel/preset-env"]
}

别忘了安装这个 preset:

npm i @babel/preset-env --save-dev

安装和配置完毕后,重新 build,就会使用 preset-env 指定的 transpile 规则。这套规则有什么用呢?

preset-env

ECMAScript 标准每一两年都推出新的版本,引入新的特性。而不同浏览器对其的实现难免会有所滞后。为了:1)能及时使用 ECMAScript 的新特性;2)确保我们的代码在所有浏览器环境中的表现一致,Babel 提供的 preset-env 可以将使用新特性的代码 transpile 为使用旧特性的实现。(文档)(在 Babel 出现之前,许多应用引入一个静态的 js 脚本完成这一功能,这种脚本叫做「polyfill」)

比如,ES6 引入了箭头函数和 const 关键字:

const a = [1,2,3];
a.forEach((x) => console.log(x));

如果要兼容不支持 ES6 的环境(虽然所有现代浏览器都已经支持了 ES6),Babel 就要将箭头函数转换成普通函数,const 换成 var:

var a = [1, 2, 3];
a.forEach(function (x) {
  return console.log(x);
});

可以在官网的 Try it out 中尝试。

TypeScript

TypeScript 也是开发现代 Web 应用的必备。如果 standalone 地安装,可以使用 tsc 命令将一个 .ts 文件 transpile 成一个 .js 文件。然而,为了使这一过程在 build 时自动完成,还是要将其集成到 Babel。

集成到 Babel

如前所述,Babel 已经提供了 TypeScript 的 preset(文档),其中包含了转译 TypeScript 的插件。只需直接安装:

npm i @babel/preset-typescript --save-dev

webpack.config.js 中,要修改两个地方:1)将 entry 改为 index.ts;2)将 babel-loader 的 test 规则改为匹配 .ts 结尾的文件:

// webpack.config.js
module.exports = {
  // ...
  entry: "./src/index.ts",
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

babel.config.json 里,加入 preset-typescript:

// babel.config.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ]
}

现在,可以将 src 中的 index.js 改写为 index.ts 了。由于这段代码很短,只需要改一个地方,即判断 app 是否为 null

import moment from "moment";
import "./style.css";

const app = document.getElementById("app");
if (app !== null) {
  app.innerText = moment().format();
  app.classList.add("bg-gray");
}

配置文件 tsconfig.json

TypeScript 也有配置文件。在项目根目录下创建 tsconfig.json 即可。

具体规则可参考官方文档。当我们配置好 React 后会再来修改 TypeScript 的规则配置。

React.js

使用 React 时我们会编写 JSX(或 TSX)语法的代码。JSX(或 TSX)全称 JavaScript(TypeScript)Extension,这是一种糅合了 HTML 和 JavaScript(TypeScript)语法的代码。当然,无论是浏览器还是 Node 都不支持这种代码,所以需要 Babel 为我们转译。其实,这样的代码中,类似 HTML 的那部分会被转译成 JavaScript 递归的函数调用的形式。

集成到 Babel

Babel 也提供了 React 的 preset(文档),包含了对应插件。只要安装:

npm i @babel/preset-react --save-dev

webpack.config.js 中设置匹配 .ts 或 .tsx 结尾的文件:

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

修改 Babel 配置文件 babel.config.json,添加 preset-react:

// babel.config.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

接下来,为了让 TypeScript 解释 TSX 语法,要在 tsconfig.json 中加入如下配置:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "jsx": "react",
    "esModuleInterop": true,
    "noEmit": true,
    "allowImportingTsExtensions": true
  }
}

这段配置文件中:

strict 设为 true 表示开启严格类型检查,包括不允许隐式 any 类型等等。jsx 设为 react,表示启用 JSX 支持。esModuleInterop 设为 true 允许用 import 语法直接导入 CommonJS 模块(否则,必须使用 require 的语法)。noEmit 表示不输出编译后的结果文件。由于在该配置中 TypeScript 是作为 Babel 的一个插件,转译后结果文件由 Babel 输出。allowImportingTsExtensions 表示允许导入 .tsx 类型的文件。

以及,现在我们的脚本文件可以是 js、jsx、ts、tsx 格式了,要在 Tailwind 的配置文件中修改其检测的文件格式:

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

最后,别忘了安装 React 本体,以及其对应的类型定义:

npm i react react-dom --save
npm i @types/react @types/react-dom --save-dev

编写 React 组件

现在,在 src 下创建 App.tsx 文件,我们可以在其中用 TSX 语法编写一个 React 组件了。

将之前写的显示时间的组件写进这里面:

// App.tsx
import React, { Component } from "react";
import "./style.css";
import moment from "moment";

class App extends Component {
  render (): React.JSX.Element {
    return (
      <div className="App">
        <h1 className="text-4xl">Hello, World!</h1>
        <p>{moment().format()}</p>
      </div>
    );
  }
}

export default App;

为了引用该组件,入口文件 index.ts 也需要用到 TSX 语法。因此,将其重命名为 index.tsx,修改为如下内容:

// index.tsx
import React from "react";
import { createRoot } from "react-dom/client";

import App from "./App.tsx";

const container = document.getElementById("app");
if (container !== null) {
  const root = createRoot(container);
  root.render(<App />);
}

同时,要在 webpack 配置中修改 entry:

// webpack.config.js
module.exports = {
  // ...
  entry: "./src/index.tsx",
  // ...
}

现在,build 之后,打开生成的 HTML,可以看到我们用 React 写的组件了。

ESLint

ESLint 是一个代码检查工具。对于团队项目,统一代码风格十分重要,而 ESLint 可以方便地做到这一点:如果没有满足指定的代码风格,则显示警告或错误(如果在 IDE 中集成的话),或者拒绝提交或部署(如果在提交部署流程中集成的话)。

为了使流程更加清晰,我们还是选择将 ESLint 作为一个插件集成到 webpack。

集成到 webpack

安装 eslint-webpack-plugin(相关文档):

npm i eslint-webpack-plugin --save-dev

修改 webpack 的配置,添加该插件:

// webpack.config.js
// ...
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
  // ...
  plugins: [
    // ...
    new ESLintPlugin({
      extensions: ["js", "jsx", "ts", "tsx"],
    }),
  ],
  // ...
};

配置文件中 new ESLintPlugin({}) 可以传入一个 options 对象,用于指定 ESLint 插件选项(文档)。这里我们指定了要 lint 的文件拓展名。

配置文件 .eslintrc.js

可以使用 @eslint/config 创建配置文件(文档),这是一个友好的交互式命令:

npm init @eslint/config

在其中可以选择「项目使用了 React.js、TypeScript」,该命令会自动为我们安装配置对应的 ESLint 插件。

运行完成后,除了安装了一堆插件,项目根目录会产生配置文件 .eslintrc.js(或者其他文件格式,取决于你的选择)。

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ["standard-with-typescript", "plugin:react/recommended"],
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  plugins: ["react"],
};

现在,再次 build,ESLint 会按照我们设定的规则进行代码检查。

可以在配置文件中添加一些自己习惯的规则,比如使用双引号、行末加分号。并且,需要设置对于 *.config.js 这类配置文件的特殊检测规则。我的 .eslintrc.js 文件设置如下:

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: ["standard-with-typescript", "plugin:react/recommended"],
  overrides: [
    {
      env: {
        node: true
      },
      files: [".eslintrc.js", "*.config.js"],
      parserOptions: {
        sourceType: "script"
      },
      extends: ["plugin:@typescript-eslint/disable-type-checked"],
      rules: {
        "@typescript-eslint/no-var-requires": "off"
      }
    }
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module"
  },
  rules: {
    "@typescript-eslint/semi": ["error", "always"],
    "@typescript-eslint/quotes": ["error", "double"]
  },
  plugins: ["react"]
};

⚠️ 一个坑点:使用 TypeScript 时,由于使用 typescript-eslint 的解析器而非默认解析器(文档),添加规则要写 @typescript-eslint/quotes 而非 quotes,否则不会生效。例如:

// eslintrc.js
module.exports = {
  // ...
  rules: {
    "@typescript-eslint/quotes": ["error", "double"],
    "@typescript-eslint/semi": ["error", "always"],
  },
  // ...
};

💡 或许你同时在 IDE 中使用 Prettier 一类的代码格式化工具。其文档中 Prettier vs. Linters 介绍了这两种工具的区别;Integrating with Linters 介绍了其与 linter 的集成指南。简而言之,在 ESLint 配置中应用 eslint-config-prettier 规则集即可自动关闭所有与 Prettier 冲突的规则。不过我更推荐的是在 IDE 中安装 ESLint 插件,对于 js 类文件直接使用 ESLint 作为代码格式化工具,这样能够确保遵循 eslintrc 中的规则。

(ESLint 这部分配置起来还是挺麻烦的,尤其要集成到 VSCode,同时兼容 TypeScript,并考虑到其和 Prettier 的冲突。改天配置好了一个比较 fancy 的方案再单独写一篇)

Recap:站在巨人的肩膀上

至此,一套比较完整的前端项目 starter 终于配置完毕了。完整的项目可以在这个仓库查看。

回顾一下,我们首先使用了 webpack 作为打包工具;其 PostCSS 插件能够对 CSS 进行处理;Tailwind 则可以作为 PostCSS 插件集成。我们使用 Babel 这个 transpiler 处理各种脚本文件,其中 env preset 将 ECMAScript 较先进的特性转译为旧特性的实现,确保兼容性;TypeScript 和 React JSX 两个 preset 则分别将它们各自的语法转译成 JavaScript。最后,我们使用 ESLint 作为代码质量检查工具,并配置其针对 TypeScript 和 JSX 的规则。

使用的工具链关系示意图如下:

配置完毕后的工具链示意图

相比从前,各种工具让开发的过程变得越来越优雅和美妙。然而每个工具背后,都有无数前人的辛勤付出,没有他们的这些努力,我们无法得到这样现代化的前端开发体验。

现代前端开发,就是站在巨人的肩膀上。

一点思考 🤔

最后,还有一个我的疑问:相比其他领域,为什么 Web 前端开发的工具链会呈现如此复杂的形态呢?我体验过 iOS 开发,也了解过基于 Qt 等框架的客户端开发,我个人的感觉是没有一个领域的客户端开发像 Web 前端这样有如此庞大复杂的工具链:某个工具可以配置插件,插件又有插件,插件的插件又有插件……那么归根结底,Web 前端工具链这种复杂的形式,是历史发展的必然,是某种设计缺陷的后果,还是某种设计思想的体现?🤔

欢迎分享你的思考。

值得一读

Web development used to be a great entry point for people new to programming precisely because it was so easy to get up and running; nowadays it can be quite daunting, especially because the various tools tend to change rapidly.

—— Modern JavaScript Explained For Dinosaurs

值得一读的相关文章;

Modern JavaScript Explained For Dinosaurs介绍完整的工具链 - 学习 Web 开发 | MDNBuild your own React

文章来源:

Author:SkyWT
link:https://blog.skywt.cn/posts/configure-a-modern-frontend-toolchain-from-scratch