学一点Webpack配置:基本配置

发布于 大漠

这两天朋友圈流行这么一张图:

多么形象的展示了前端学习的曲线图。真可谓是一言难尽呀,现在的前端真不好学,乱而杂。如果你要是再看看@Kamran Ahmed整理的2017年2018年2019年现代Web开发者要掌握的Roadmap,估计更会泪崩:

点击这里可以查看大图

现状是如此,未来可能会更混乱,但我们不应该去抱怨,应该更应该保持一颗爱学习的心,继续往前行走。

学点Webpack配置方面的知识

Webpack是构建工具中必不可少的一部分:

作为现代Web开发者就需要对Webpack有所了解,哪怕掌握的不够深入,略知皮毛也对我们自己的工作或学习都是有所帮助的。比如说吧,前段时间折腾React环境下的CSS Modules,就是因为自己对Webpack不了解,有些坑踩了无法立刻解决,就算借助互联网,解的也是知半解(而且现在技术更新太快,网上有些教程根本走不通,不踩不知道,一踩只有泪)。正因为这个原因,促使自己去了解Webpack更多的知识。接下来的内容是一些基础,主要会介绍怎么用Webpack来构建自己的开发环境,感兴趣的请继续往下阅读。

Webpack是什么

一直以来,在我自己的印象和理解中,都认为Webpack是一个构建工具。主要用来构建开发的工程体系。但从其官网来看,告诉我Webpack是一个模块Bundler(捆绑器):

那么,Webpack到底是一个构建工具(或者说一个构建系统)还是一个模块Bundler(捆绑器)呢?答案是:

Webpack既是一个构建系统,也是一个捆绑器

Webpack不是先构建你的资源(Assets),然后再bundle你的模块,它把你的资源本身就当做是一个模块。这些模块可以被导入修改操作等,最后才被打包到你最后的bundle。

简单地说,Webpack其最核心的功能就是 解决模板之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并成一个JS文件(比如bundle.js。这个整个过程也常常被称为是模块打包。换句话说,Webpack是一个指令集合的配置文档,然后通过配置好的这些指令去驱动程序做一些指令要求要做的事情。而这些动作都是通过自己写的规则去做编译,而且通过JavaScript的引入(import)语法让Webpack知道需要它帮忙编译什么东西(比如Pug、Sass等等)。所以我们始终会有一个入口文件(比如index.js)注入那些Preprocess,让那些Preprocess可以通过这些入口文件的JavaScript让Webpack去根据相关的配置指令编译它,然后打包到一个出口文件中,比如bundles.js

为什么要用Webpack

一直以来,在开发Web页面或Web应用程序的时候,都习惯性的将不同资源放置在不同的文件目录之中,比如图片放置在images(或img)下,样式文件放置在styles(或css)中,脚本文件放在js和模板文件放置在pages中。一直以来,发布的时候都会一次性的将所有资源打包发布,不管这些资源用到了还是没用到(事实上很多时候自己都分不清楚哪资源被使用)。用一句话来描述就是:依赖太复杂,太混乱,无法维护和有效跟踪。比如哪个样式文件引用了a.img,哪个样式文件引用了b.img;另外页面到底是引用了a.css呢还是b.css呢?

而Webpack这样的工具却能很多好的解决它们之间的依赖关系,使其打包后的结果能运行在浏览器上。其目前的工作方式主要被分为两种:

  • 将存在依赖关系的模块按照特定规则合并成为单个.js文件,一次性全部加载进页面
  • 在页面初始时加载一个入口模块,其他模块异步加载

相比于Parcel、Rollup具有同等功能的工具而言,Webpack还具有其他的优势:

  • Webpack支持多种模块标准:这对于一些同时使用多种模块标准的工程非常有用,Webpack会帮我们处理好不同类型模块之间的依赖关系
  • Webpack有完备的代码分割解决方案:它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载
  • Webpack可以处理各种类型的资源:除了JavaScript之外,Webpack还可以处理样式、模板、图片等资源。开发者要做的只是将之些资源导入,而无需关注其他

另外,Webpack还拥有一个强大的社区。这也是其受开发者青眯的原因之一。接下来,我们还是实际一点,动手来撸码。

从零开始构建你自己的开发环境

为了更好的理解Webpack能帮我们做什么,我打算从零开始构建一个属于自己的开发环境。可能在后面的内容中会涉及到很多关键词,比如Webpack、loaders、Babel、sourcemaps、React、TypeScript,CSS Modules等等。接下来一步一步的学习中会了解到这些单词和相关技术。

后面会一步一步的带大家如何使用Webpack配置适合自己的开发环境,会涉及到一些相关技术,但不会深入到具体技术细节中。

在写这篇文章所具备的环境是:Node是v10.9.0,NPM是v6.9.2,Webpack是v4.34.0,React是v16.8.6,TypeScript是v3.4.4,Sass是v.6.9.0,PostCSS是v.6.9.0等。接下来的内容会以配置React + TypeScript + CSS Modules + PostCSS为主线,从零开始一个项目。另外,接下来的内容会以不同分支的形式将代码放置在Github上。感兴趣的可以直接将仓库克隆下来,切换到对应步骤的分支,查看代码。

Step01:初始化项目

请将Git分支切换到step1分支查看代码

首先在你的本地创建一个项目,比如我这里创建了一个webpack-sample项目:

⇒ mkdir webpack-sample && cd webpack-sample

进入到新创建的项目目录下,执行npm init或者npm init -y命令来初始化项目,执行完该命令之后,在你的命令终端会看到类似下图这样的命令询问,你可以根据你自己的需要去输入你想要的内容,或者一路Enter键执行下去:

此时你的项目根目录下会增加一些文件和文件夹:

|--webpack-sample/
|----node_modules/
|----package.json
|----package-lock.json

其中package.json文件里将包含一些项目信息:

注意,这个文件随着后面的步骤完成,会增加更多的内容。

package-lock.json文件是当 node_modules/package.json 发生变化时自动生成的文件,它的主要功能是 确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新

在Step01中,我们对package.json文件只做一个修改,删除"main": "index.js"入口,并添加"private":true选项,以便确保安装包是私有的,这样可以防止意外发布你的代码。

Step02:安装Webpack和初始配置Webpack

请将分支切换到step2查看代码

在这一步,先来安装Webpack。执行下面的命令安装Webpack配置所需要的包:

⇒ npm i webpack webpack-cli webpack-dev-server -D

此时打开package.json文件,你会发现在文件中有一个新增项 devDependencies

{
    // 其他项信息在这省略,详细请查看该文件

    "devDependencies": {
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2"
    }
}

注意,在命令终端使用npm i安装依赖关系时,如果带后缀 -D(或--save-dev 安装的包会记录在"devDependencies"下;如果使用 **--save**后缀(我们后面会用到)安装的包会记录在"dependencies"下。两者的区别是:

  • "devDependencies"dev开发时的依赖包
  • dependencies:程序运行时的依赖包

为了验证Webpack是否能正常工作,这个时候我们需要创建一些新的文件。在webpack-sample根目录下创建/src目录,并且在该目录下创建一个index.js文件:

⇒ mkdir src && cd src && touch index.js

执行完上面的命令,你会发现你的项目目录结构变成下图这样:

我们在新创建的/src/index.js文件下添加一行最简单的JavaScript代码:

console.log("Hello, Webpack!(^_^)~~")

保存之后回到命令终端,第一次执行有关于Webpack相关的命令:

⇒ npx webpack src/index.js --output dist/bundle.js

执行完上面的命令后,如果看到下图这样的结果,那么要恭喜你,Webpack的安装已经成功,你可以在你的命令终端执行有关于Webpack相关的命令:

回到项目中,会发现项目根目下自动创建了一个/dist目录,而且该目录下包含了一个bundle.js文件:

执行完上面的命令之后,可以看到有相关的警告信息。那是因为Webpack4增加了mode属性,用来表示不同的环境。mode模式具有developmentproductionnone三个值,其默认值是production 。也就是说,在执行上面的命令的时候,我们可以带上相应的mode属性的值,比如说,设置none来禁用任何默认行为:

⇒  npx webpack src/index.js --output dist/bundle.js --mode none

执行到这里,只知道我们可以运行Webpack相关命令。并不知道/src/index.js的代码是否打包到/dist/bundle.js中。为此,我们可以在/dist目录下创建一个index.html

⇒  cd dist && touch index.html

并且将生成出来的bundle.js引入到新创建的index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Hello Webpack (^_^) ~~~</title>
    </head>
    <body>
        <script src="./bundle.js"></script>
    </body>
</html>

在浏览器中打开/dist/index.html,或者在命令行中执行:

⇒  npm i -g http-server
⇒  http-server dist

http-server是一个启动服务器的npm包,执行上面的命令之后,就可以在浏览器中访问http://127.0.0.1:8080/(访问的/dist/index.html),在浏览器的console.log控制台中,可以看到src/index.js的脚本输出的值:

上面的过程足以验证,你的Webpack能正常的工作了。

不过,当你需要构建的东西越复杂,需要的标志就会越多。在某种程度上说,就会变得难以控制。这个时候我们就需要一个文件来管理这些配置。接下来我们需要创建一个webpack.config.js这样的一个文件,用来配置Webpack要做的事情。注意,这个文件是一个node.js文件,所以你可以在任何节点文件中执行任何你能够执行的操作。你也可以写成json文件,但是node文件更强大一些。

首先们先创建Webpack的配置文件,在webpack-sample根目录下创建一个/build目录,然后在该目录下添加一个名为webpack.config.js文件:

⇒  mkdir build && cd build && touch webpack.config.js

执行完上面的命令之后,你会发现你的项目文件目录结构变成下面这样了:

这个时候,新创建的webpack.config.js文件里面是一片空白,它就是Webpack的配置文件,将会导出一个对象的JavaScript文件。我们需要在这个文件中添加一些配置:

var webpack = require('webpack');
var path = require('path');
var DIST_PATH = path.resolve(__dirname, '../dist');  // 声明/dist的路径

module.exports = {
    // 入口JS路径
    // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始
    entry: path.resolve(__dirname,'../src/index.js'),


    // 编译输出的JS入路径 
    // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
    output: {
        path: DIST_PATH,        // 创建的bundle生成到哪里
        filename: 'bundle.js',    // 创建的bundle的名称
    },

    // 模块解析
    module: {

    },

    // 插件
    plugins: [

    ],

    // 开发服务器
    devServer: {

    }
}

Webpack配置是标准的 Node.js CommonJS模块,它通过require来引入其他模块,通过module.exports导出模块,由Webpack根据对象定义属性进行解析。

上面很简单,到目前为止只通过entry设置了入口起点,然后通过output配置了打包文件输出的目的地和方式。你可能也发现了,在配置文件中还有modulepluginsdevServer没有添加任何东西。不需要太急,后面会一步一步带着大家把这里的内容补全的,而且随着配置的东西越来越多,整个webpack.config.js也会更变越复杂。

完成webpack.config.js的基础配置之后,回到package.json文件,并在"scripts"下添加"build": "webpack --config ./build/webpack.config.js"

// package.json

{
    // ...

    "scripts": {
        "build": "webpack --config ./build/webpack.config.js",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
}

这样做的,为让我们直接在命令终端执行相关的命令就可以实现相应的功能。比如上面配置的build,在命令终端执行:

⇒  npm run build

上面的命令执行的效果前面提到的npx webpack src/index.js --output dist/bundle.js --mode none等同。同样有警告信息,主要是mode的配置没有添加。在上面的配置中添加:

{
    "scripts": {
        "build": "webpack --config ./build/webpack.config.js --mode production",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
}

再次执行npm run build,不会再有警告信息。你可以试着修改/src/index.js的代码:

alert(`Hello, Webpack! Let's Go`);

重新编译之后,打开/dist/index.html你会发现浏览器会弹出alert()框:

为了开发方便,不可能通过http-server来启用服务。我们可以把这部分事件放到开发服务器中来做,对应的就是devServer,所以我们接着在webpack.config.js中添加devServer相关的配置:

// webpack.config.js

// 开发服务器
devServer: {
    hot: true,                  // 热更新,无需手动刷新
    contentBase: DIST_PATH,     // 
    host: '0.0.0.0',            // host地址
    port: 8080,                 // 服务器端口
    historyApiFallback: true,   // 该选项的作用所用404都连接到index.html
    proxy: {
        "/api": "http://localhost:3000" // 代理到后端的服务地址,会拦截所有以api开头的请求地址
    }
}

有关于devServer更详细的配置参数描述,可以查阅读Webpack官网相关文档

build类似,需要在package.jsonscripts中添加相关的命令:

// package.json

"scripts": {
    "build": "webpack --config ./build/webpack.config.js --mode production",
    "dev": "webpack-dev-server --config ./build/webpack.config.js --mode development --open",
    "test": "echo \"Error: no test specified\" && exit 1"
},

保存所有文件,在命令行中执行npm run dev就可以启动服务器:

你可以验证一下,修改/src/index.js

document.addEventListener('DOMContentLoaded', () => {
    const h1Ele = document.createElement('h1')

    document.body.append(h1Ele);

    h1Ele.innerText = 'Hello Webpack (^_^)'

    h1Ele.style.color = '#f46';
})

保存该文件之后,浏览器会立刻刷新,你将看到修改之后的变化:

Step03: 优化Webpack配置

请将分支切换到step3查看代码

Step02中,开发和生产环境相关的配置都集成在webpack.config.js一个文件中。为了更好的维护代码,在Step03中做一些优化。把webpack.config.js拆分成三个部分:

  • 公共配置:把开发和生产环境需要的配置都集中到公共配置文件中,即webpack.common.js
  • 开发环境配置:把开发环境需要的相关配置放置到webpack.dev.js
  • 生产环境配置:把生产环境需要的相关配置放置到webpack.prod.js

先在/build目录下创建上面提到的三个配置文件。在命令终端执行下面的命令即可:

⇒  cd build && touch webpack.common.js webpack.dev.js webpack.prod.js

这个时候,整个项目目录结构变成下图这样:

Step02中遗留下来的webpack.config.js文件将会从/build目录中移除。

为了更好的管理和维护这三个文件,需要安装一个webpack-merge插件:

⇒  npm i webpack-merge -D

执行完上面的命令之后,package.json文件中的devDependencies会增加webpack-merge相关的配置:

// package.json

{
    //... 省略的信息请查看原文件
    "devDependencies": {
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2",
        "webpack-merge": "^4.2.1"
    }
}

接下来分别给webpack.common.jswebpack.dev.jswebpack.prod.js文件添加相关的配置:

Webpack公共配置

在公共配置文件webpack.common.js文件中添加相应的配置:

const webpack = require('webpack');
const path =  require('path');
const DIST_PATH = path.resolve(__dirname, '../dist/');  // 声明/dist的路径

module.exports = {
    // 入口JS路径
    // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始
    entry: path.resolve(__dirname,'../src/index.js'),


    // 编译输出的JS入路径 
    // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
    output: {
        path: DIST_PATH,        // 创建的bundle生成到哪里
        filename: 'bundle.js',    // 创建的bundle的名称
    },

    // 模块解析
    module: {

    },

    // 插件
    plugins: [

    ]
}

Webpack开发环境配置

接着给Webpack开发环境配置文件webpack.dev.js添加下面的相关配置:

const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');

const commonConfig = require('./webpack.common.js');

const DIST_PATH = path.resolve(__dirname, '../dist/');  // 声明/dist的路径

module.exports = merge(commonConfig, {
    mode: 'development', // 设置webpack mode的模式

    // 开发环境下需要的相关插件配置
    plugins: [

    ],

    // 开发服务器
    devServer: {
        hot: true,                  // 热更新,无需手动刷新
        contentBase: DIST_PATH,     // 
        host: '0.0.0.0',            // host地址
        port: 8080,                 // 服务器端口
        historyApiFallback: true,   // 该选项的作用所用404都连接到index.html
        proxy: {
            "/api": "http://localhost:3000" // 代理到后端的服务地址,会拦截所有以api开头的请求地址
        }
    }
})

Webpack生产环境配置

继续给Webpack生产环境配置文件webpack.prod.js添加相关配置:

const webpack = require('webpack');
const path = require('path');
const merge = require('webpack-merge');

const commonConfig = require('./webpack.common.js');

module.exports = merge(commonConfig, {
    mode: 'production', // 设置Webpack的mode模式

    // 生产环境下需要的相关插件配置
    plugins: [

    ],
})

上面的配置信息只是将Step02webpack.config.js分成三个文件来配置,随着后续添加相应的配置信息,那么这三个文件中的配置信息会越来越多,也会越来越复杂。

修改完Webpack的配置之后,对应的package.json中的scripts中的信息也要做相应的调整:

// package.json
{
    // ... 其他配置信息请查看原文件
    "scripts": {
        "build": "webpack --config ./build/webpack.prod.js --mode production",
        "dev": "webpack-dev-server --config ./build/webpack.dev.js --mode development --open",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
}

这个时候重新在命令终端执行

// 执行build命令,重新打包
⇒  npm run build

// 执行dev命令
⇒  npm run dev

这仅仅是最基础部分的优化,因为我们的配置还是最简单的,后续我们添加了别的配置之后,也会在相应的步骤做相应的优化。

Step04: 配置React开发环境

请将分支切换到step4查看代码

经过前面三步,我们完成了Webpack的基本配置,知道文件入口,出口,打包以及开发,生产等环境。接下来,我们来给工程配置React相关的环境。

React的环境需要先安装reactreact-dom。所以先在命令终端中执行下面的命令:

⇒  npm i react react-dom --save

执行完上面的命令之后,在package.json文件中的dependencies增加了ractreact-dom相应的信息:

// package.json
{
    "dependencies": {
        "react": "^16.8.6",
        "react-dom": "^16.8.6"
    },
}

为了验证React相关的环境是否能正常工作,将/src/index.js中的内容做一些修改:

// /src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

import App from './components/App'

ReactDOM.render(<App />, document.getElementById('root'))

在这个index.js中引用了App组件。所以我们在/src目录下新增components/目录,并在该目录下新增App.js

// src/components/App.js

import React from 'react';

export default class App extends React.Component {
    render() {
        return (
            <h1>Hello Webpack and React! (^_^)</h1>
        )
    }
}

另外在/src/新增一个模板文件index.html

<!-- /src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello Webpack (^_^) ~~~</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

这个时候你在命令终端不管是执行npm run build还是npm run dev都无法正常运行,会报错:

首先我要告诉你的是Step04这一步的操作并没有任何问题,主要是在编译的过程中缺少必要的东西。那就是Babel相关的配置。接下来的Step05将会添加Babel相关的配置。

Step05:添加Babel相关的配置

请将分支切换到step5查看代码

Step04中会失败主要是因为Webpack只识别JavaScript文件,而且只能编译ES5。实际上ES6(甚至后面要说的JSX),Webpack它根本不认识。那么要解决这个问题,就需要借助Babel来处理。先安装需要的插件:

⇒  npm i babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-transform-modules-commonjs @babel/preset-react -D

⇒  npm i @babel/runtime --save

执行完上面的命令之后,package.json文件在dependenciesdevDependencies添加了新的配置信息:

// package.json

{
    // ...省略的信息可以查看原文件
    
    "dependencies": {
        "@babel/runtime": "^7.4.5",
        "react": "^16.8.6",
        "react-dom": "^16.8.6"
    },
    "devDependencies": {
        "@babel/core": "^7.4.5",
        "@babel/plugin-transform-modules-commonjs": "^7.4.4",
        "@babel/plugin-transform-runtime": "^7.4.4",
        "@babel/preset-env": "^7.4.5",
        "@babel/preset-react": "^7.0.0",
        "babel-loader": "^8.0.6",
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2",
        "webpack-merge": "^4.2.1"
    }
}

接着在webpack-sample根目录下创建.babelrc文件来配置Babel:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "browsers": [
                        "> 1%",
                        "last 5 versions",
                        "ie >= 8"
                    ]
                }
            }
        ],
        "@babel/preset-react"
    ],
    "plugins": [
        "@babel/plugin-transform-runtime",
        "@babel/plugin-transform-modules-commonjs"
    ]
}

有关于Babel更详细的配置可以点击这里阅读,这里不再做过多的阐述。

最后在webpack.common.js配置文件中的module中添加rules来处理.js.jsx文件,这也是我们添加的第一个有关于Webpack的Loader相关的东西:

// webpack.common.js

module.exports = {
    // ... 省略的信息查看原文件代码
    
    // 模块解析
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            }
        ]
    },
}

这个时候执行npm run build可以正常的执行,但是执行npm run dev还是会报错:

执行npm run dev不成功是因为我们的模板文件没有自动插入到/dist目录下。为了让/src/目录下的模板文件index.html能自动编译到/dist目录下,并且所有的.js引用能自动插入到index.html中。我们需要使用Webpack的两个插件:

在命令终端上执行下面的命令,安装这两个插件:

⇒  npm i html-webpack-plugin html-webpack-template -D

安装完成之后,在package.jsondevDependencies会添加相应的信息:

// package.json
{
    // ... 省略的信息可以查看原文件
    "devDependencies": {
        "@babel/core": "^7.4.5",
        "@babel/plugin-transform-modules-commonjs": "^7.4.4",
        "@babel/plugin-transform-runtime": "^7.4.4",
        "@babel/preset-env": "^7.4.5",
        "@babel/preset-react": "^7.0.0",
        "babel-loader": "^8.0.6",
        "html-webpack-plugin": "^3.2.0",
        "html-webpack-template": "^6.2.0",
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2",
        "webpack-merge": "^4.2.1"
    }
}

接着修改webpack.common.js文件,在plugins中添加刚安装好的插件配置:

// webpack.common.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackTemplate = require('html-webpack-template');

module.exports = {
    // 省略的信息可以查看原文件
    // 插件
    plugins: [
        new HtmlWebpackPlugin({
            inject: false,
            template: HtmlWebpackTemplate,
            appMountId: 'root', // 模板文件中挂载元素的id名称
            filename: 'index.html'
        }),
    ]
}

保存完上成的文件,在命令终端执行npm run dev命令之后,就会看到你想要的效果:

这个时候已经成功了。上面验证的是.js文件,再来验证一个.jsx的文件。在/src/components/目录下新创建另一个组件文件,比如Button.jsx文件,并添加相应的React创建组件的代码:

// /src/components/Button.jsx

import React from 'react';

export default class Button extends React.Component {
    render() {
        return (
            <button className="button">我就是一个按钮</button>
        )
    }
}

如果你在App组件中引用这个Button组件:

// /src/components/App.js

import React from 'react';

import Button from './Button.jsx';

export default class App extends React.Component {
    render() {
        return (
            <div>
                <h1>Hello Webpack and React! (^_^)</h1>
                <Button />
            </div>
        )
    }
}

这个时候你在浏览器中能看到新添加的按钮:

Step06:Typescript的配置

请将分支切换到step6查看代码

在开启Typescript的相关配置之前,先把在Step05的基础上对项目的目录结构做一个调整:

注意,文件结构做了调整之后,我们的每个文件引入的相互关系也会发生变化。这里就不做详细的阐述。另外,入口文件也发生了变化,所以在webpack.common.js中的entry也要做相应的调整:

// webpack.common.js

module.exports = {
    // 入口JS路径
    // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始
    entry: {
        index: path.resolve(__dirname,'../src/pages/index/index.js'),
    },


    // 编译输出的JS入路径 
    // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
    output: {
        path: DIST_PATH,        // 创建的bundle生成到哪里
        filename: '[name].bundle.js',    // 创建的bundle的名称
    },

    // ...省略的内容请查看文件源码
}

在命令终端执行npm run dev之后,又可以看到Step5结束的效果:

接下来开始开启Typescript的相关配置。和前面的配置一样,首先需要安装相关的依赖关系:

⇒  npm i typescript awesome-typescript-loader @babel/preset-typescript -D

再安装有Typescript和React相关的依赖关系:

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

package.json依旧会发生相应的变化:

// package.json

{
    // ... 省略的信息请查看原文件

    "dependencies": {
        "@babel/runtime": "^7.4.5",
        "@types/react": "^16.8.22",
        "@types/react-dom": "^16.8.4",
        "node-sass": "^4.12.0",
        "react": "^16.8.6",
        "react-dom": "^16.8.6"
    },
    "devDependencies": {
        "@babel/core": "^7.4.5",
        "@babel/plugin-transform-modules-commonjs": "^7.4.4",
        "@babel/plugin-transform-runtime": "^7.4.4",
        "@babel/preset-env": "^7.4.5",
        "@babel/preset-react": "^7.0.0",
        "@babel/preset-typescript": "^7.3.3",
        "awesome-typescript-loader": "^5.2.1",
        "babel-loader": "^8.0.6",
        "html-webpack-plugin": "^3.2.0",
        "html-webpack-template": "^6.2.0",
        "typescript": "^3.5.2",
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2",
        "webpack-merge": "^4.2.1"
    }
}

我们需要为Typescript添加编译所需的译置,在项目根目录下创建一个tsconfig.json文件,并在文件中添加下面的内容:

{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitAny": false,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "module": "commonjs",
        "target": "es6",
        "lib": [
            "es2015",
            "es2017",
            "dom"
        ],
        "removeComments": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "moduleResolution": "node",
        "pretty": true,
        "jsx": "react",
        "allowJs": true,
        "baseUrl": "./src",
        "rootDir": "./src",
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules"]
}

有关于tsconfig.json更详细的配置可以点击这里查阅

修改完tsconfig.json文件之后还得给.babelrc文件添加Typescript所需的相关配置:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "browsers": [
                        "> 1%",
                        "last 5 versions",
                        "ie >= 8"
                    ]
                }
            }
        ],
        "@babel/preset-react",
        "@babel/typescript"
    ],
    "plugins": [
        "@babel/plugin-transform-runtime",
        "@babel/plugin-transform-modules-commonjs"
    ]
}

这离我们越来越近了。现在需要将项目中带有.js.jsx的文件更换成Typescript的文件格式tsx,方便我们后续修改Webpack的配置得到相应的验证。修改完之后就剩下调整Webpack的配置文件了webpack.common.js

const webpack = require('webpack');
const path =  require('path');
const DIST_PATH = path.resolve(__dirname, '../dist/');  // 声明/dist的路径

const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackTemplate = require('html-webpack-template');

module.exports = {
    // 入口JS路径
    // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始
    entry: {
        index: path.resolve(__dirname,'../src/pages/index/index.tsx'),
    },


    // 编译输出的JS入路径 
    // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
    output: {
        path: DIST_PATH,        // 创建的bundle生成到哪里
        filename: '[name].bundle.js',    // 创建的bundle的名称
    },

    resolve: {
        // 配置之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配
        extensions: ['.ts', '.tsx', '.js', '.jsx','.json'],
        modules: [
            path.resolve(__dirname, '../src'),
            path.resolve(__dirname, '../node_modules'),
        ],
    },

    // 模块解析
    module: {
        rules: [
            {
                test: /\.(j|t)sx?$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "babel-loader"
                    },
                    {
                        loader: "awesome-typescript-loader"
                    }
                ]
            }
        ]
    },

    // 插件
    plugins: [
        new HtmlWebpackPlugin({
            inject: false,
            template: HtmlWebpackTemplate,
            appMountId: 'root',
            filename: 'index.html'
        }),
    ]
}

保存文件,重新在命令终端执行npm run dev命令。你可以看到浏览器如下图的效果,表示React、Typescript和Webpack的环境已经成功了:

Step07:CSS Loader配置

请将分支切换到step7查看代码

在Step06的时候,组件ButtonApp中就添加了对应的CSS文件button.cssapp.css,只不过注释掉了引用的CSS文件,没让它们生效。我们可以先开启已被注释的CSS,重新运行项目,会发现有报错信息:

这是因为在Webpack中没有CSS相关的Loader配置。那么接下来,来解决CSS Loader在Webpack中的配置:

  • css-loader使你能够使用类似@importurl()的方法实现require()的功能
  • style-loader将所有的计算后的样式加入页面中

两者结合在一起能够把样式嵌入Webpack打包后的JavaScript文件中。

⇒  npm i style-loader css-loader -D 

在命令终端执行完上面的命令之后,package.json文件中会增加相应的配置信息:

// package.json

{
    // ... 省略的信息可以查看文件源码

    "devDependencies": {
        "@babel/core": "^7.4.5",
        "@babel/plugin-transform-modules-commonjs": "^7.4.4",
        "@babel/plugin-transform-runtime": "^7.4.4",
        "@babel/preset-env": "^7.4.5",
        "@babel/preset-react": "^7.0.0",
        "@babel/preset-typescript": "^7.3.3",
        "awesome-typescript-loader": "^5.2.1",
        "babel-loader": "^8.0.6",
        "css-loader": "^3.0.0",
        "html-webpack-plugin": "^3.2.0",
        "html-webpack-template": "^6.2.0",
        "style-loader": "^0.23.1",
        "typescript": "^3.5.2",
        "webpack": "^4.35.0",
        "webpack-cli": "^3.3.5",
        "webpack-dev-server": "^3.7.2",
        "webpack-merge": "^4.2.1"
    }
}

接下来在webpack.common.js中的module.rules添加有关于CSS Loader相关的配置:

// webpack.common.js

module.exports = {

    // ...省略的信息可以查看文件源码
    
    // 模块解析
    module: {
        rules: [
            // ...

            // CSS Loader
            {
                test: /\.css$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "style-loader"
                    },
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1
                        }
                    }
                ]
            }
        ]
    },
}

保存完之后,重新执行npm run dev,你可以看到引入的button.css文件生效了:

Step08:添加PostCSS相关配置

请把分支切换到step8查看代码

PostCSS是一个很优秀的东西,他有很多优秀的插件,比如postcss-preset-envAutoprefixer等。另外自己还可以根据自己需要扩展自己想要的PostCSS插件。而且在一些移动端的布局中的方案都会需要处理CSS单位之间的转换,比如Flexible(px2remvw-layoutpx2vw等。

可以说,现代Web开发中PostCSS相关的配置是工程体系中必不可少的。接下来,我们就看看如何在该工程体系中添加PostCSS相关的配置。要配置PostCSS相关的事项,需要:

  • 安装postcsspostcss-loader
  • 在Webpack配置中添加PostCSS相关的配置
  • 安装自己需要的PostCSS插件
  • postcss.config.js.postcssrc.js添加有关于PostCSS的插件配置

先来执行第一步,安装postcsspostcss-loader

⇒  npm i postcss postcss-loader -D

执行完上面命令之后,package.jsondevDependencies会添加postcsspostcss-loader信息:

// package.json

"devDependencies": {
    "@babel/core": "^7.4.5",
    "@babel/plugin-transform-modules-commonjs": "^7.4.4",
    "@babel/plugin-transform-runtime": "^7.4.4",
    "@babel/preset-env": "^7.4.5",
    "@babel/preset-react": "^7.0.0",
    "@babel/preset-typescript": "^7.3.3",
    "awesome-typescript-loader": "^5.2.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.0.0",
    "html-webpack-plugin": "^3.2.0",
    "html-webpack-template": "^6.2.0",
    "postcss": "^7.0.17",
    "postcss-loader": "^3.0.0",
    "style-loader": "^0.23.1",
    "typescript": "^3.5.2",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.5",
    "webpack-dev-server": "^3.7.2",
    "webpack-merge": "^4.2.1"
}

接着在webpack.common.jsmodule.rules有关于CSS规则部分添加PostCSS相关的配置:

// webpack.common.js

// ...
// 模块解析
module: {
    rules: [
        // ...

        // CSS Loader
        {
            test: /\.css$/,
            exclude: /node_modules/,
            use: [
                {
                    loader: "style-loader",
                    options: {
                        "sourceMap": true
                    }
                },
                {
                    loader: "css-loader",
                    options: {
                        importLoaders: 1,
                        "sourceMap": true
                    }
                },
                {
                    loader: 'postcss-loader',
                    options: {
                        "sourceMap": true
                    }
                }
            ]
        }
    ]
},

接下来需要给项目添加PostCSS配置相关的信息。先在项目的根目录下创建postcss.config.js文件,并添加相关的配置:

// postcss.config.js
module.exports = {
    plugins: {

    },
}

上面的配置还没有添加任何PostCSS相关的插件。为了验证我们的配置是否成功,来安装两个PostCSS的插件,比如postcss-preset-envpostcss-px-to-viewport。前者是为了让我们能更好的使用CSS未来的特性,后一个插件是为了让我们的工程中具备vw-layout的主要能力之一(px转换成视窗单位)。在命令行中执行:

⇒  npm i postcss-preset-env postcss-px-to-viewport -D

并把这两插件的相关配置中添加到postcss.config.js中:

// https://github.com/michael-ciniawsky/postcss-load-config
// https://preset-env.cssdb.org/
// https://cssdb.org/

module.exports = {
    plugins: {
        "postcss-preset-env": { 
            autoprefixer: {
                flexbox: 'no-2009',
            },
            stage: 3,
        },
        "postcss-px-to-viewport": {
            viewportWidth: 750,                             // ⇒ (Number) The width of the viewport.
            viewportHeight: 1334,                           // ⇒ (Number) The height of the viewport.
            unitPrecision: 3,                               // ⇒ (Number) The decimal numbers to allow the REM units to grow to.
            viewportUnit: 'vw',                             // ⇒ (String) Expected units.
            selectorBlackList: ['.ignore', '.hairlines'],   // ⇒ (Array) The selectors to ignore and leave as px.
            minPixelValue: 1,                               // ⇒ (Number) Set the minimum pixel value to replace.
            mediaQuery: false                               // ⇒ (Boolean) Allow px to be converted in media queries.
        }
    },
}

button.css样式文件中添加一些样式代码来验证是否生效了:

// button.css

.button {
    --primary: #f36;
    --color: #fff;

    padding: 5px 10px;
    border: 1px solid currentColor;
    border-radius: 5px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--primary);
    color: var(--color);
}

如果就这样执行npm run dev,会有autoprefixer相关的警告信息:

主要是因为postcss-preset-env插件涵盖了autoprefixer插件的能力,要解决这个警告信息,需要在package.json中添加browserslist相关的配置信息:

// package.json
{
    // ... 省略的信息可以查看原文件
    "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
        ]
    }
}

重新执行 npm run dev之后,通过浏览器开发者工具可以验证PostCSS的配置已成功:

Step09:添加Sass配置

请把分支切换到step9查看代码

有些同学可能使用CSS处理器来处理CSS,比如LESS,Sass和Stylus之类的。我个人以前较喜欢Sass,但现在一般都直接使用PostCSS来处辅助自己写CSS。但在React环境下,更倾向于使用CSS Modules(后面也会介绍怎么在这个工程中添加CSS Modules)。不过我们先来给工程添加CSS处理器的能力,这里以Sass为主,如果你对LESS或Stylus感兴趣的话,可以尝试着换成你喜欢的CSS处理器,或者跳开这一步。

要让Sass能正常在工程中跑起来,需要安装node-sasssass-loader两个依赖包:

⇒  npm i node-sass sass-loader -D

同样要修改Webpack的配置文件webpack.common.js

// webpack.common.js

module.exports = {
    // ...

    // 模块解析
    module: {
        rules: [
            // ...

            // CSS Loader
            {
                test: /\.(sc|sa|c)ss$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "style-loader",
                        options: {
                            "sourceMap": true
                        }
                    },
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                            "sourceMap": true
                        }
                    },
                    {
                        loader: "postcss-loader",
                        options: {
                            "sourceMap": true
                        }
                    },
                    {
                        loader: "sass-loader",
                        options: {
                            "sourceMap": true
                        }
                    }
                ]
            }
        ]
    },

    // ...
}

来验证一下。把App组件下的app.css更换成app.scss文件,然后在该文件中添加SCSS方面的代码:

// app.scss
$color: #19a;
$bgColor: #fc9;

.title {
    color: $color;
    background-color: $bgColor;
    text-align: center;
    padding: 5px;
}

并在App.tsx中引入该SCSS文件:

import * as React from 'react';
import './app.scss';

import Button from '../../../../components/Button/Button';

export default class App extends React.Component<any> {
    render() {
        return (
            <div>
                <h1 className="title"> Hello, Webpack + React + Typescript!(^_^)</h1>
                <Button />
            </div>
        )
    }
}

命令终端执行npm run dev。要是能看到下图这样的效果,表示你的Sass环境已经配置成功了:

Step10:添加CSS Modules配置

请把分支切换到step10查看代码

有关于CSS Modules相关的阐述这里就不做过多的介绍了。前两天和大家一起探讨了React环境下怎么使用CSS Modules更能接受我们的编写CSS的习惯。在这篇文章中我们主要以Create React APP为主线一路探讨了CSS Modules的使用以及怎么能更好的结合React。最终了解到:

使用 babel-plugin-react-css-modules 实现CSS Modules。该插件是预先发生,可以免去运行时,处理styleName对应问题,提升生产环境下的性能。也更接近我们平时编写CSS的习惯。

在该方的结尾提到,想在Webpack下来配置CSS Modules(不借助Create React APP的环境)和React的环境。接下来我们就来看看怎么配置,而且环境更复杂一些:Typescript、React、CSS Modules和Webpack。

css-loader自身就带有modules这个参数,在Webpack中,如果把该参数设置为true就表示开启了CSS Modules的能力,再配上localIdentName参数就可以编译出带有hash值的类名。但在React、TypeScript环境下还是有所不同的。我们需要安装一些需要的依赖关系:

⇒  npm i css-modules-typescript-loader @babel/plugin-transform-react-jsx-source @types/react-css-modules -D
⇒  npm i babel-plugin-react-css-modules --save

安装完之后package.json会做相应的调整。这里不一一列出。接下来需要做的是调整tsconfig.json的相关配置:

{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitAny": false,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "module": "es2015",
        "target": "es6",
        "lib": [
        "es2015",
        "es2017",
        "dom"
        ],
        "removeComments": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "moduleResolution": "node",
        "pretty": true,
        "jsx": "preserve",
        "allowJs": true,
        "baseUrl": "./src",
        "rootDir": "./src",
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules"]
}

调整后的差异性请和Step09中的文件相对比。除此之外,还需要修改webpack.common.js的配置:

// webpack.common.js

module.exports = {
    // ...

    resolve: {
        // 配置之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配
        extensions: ['.ts', '.tsx', '.js', '.jsx','.json', '.css', '.scss'],
        modules: [
            path.resolve(__dirname, '../src'),
            path.resolve(__dirname, '../node_modules'),
        ],
    },

    // 模块解析
    module: {
        rules: [
            // ...

            // CSS Loader
            {
                test: /\.(sc|sa|c)ss$/,
                exclude: /node_modules/,
                include: path.resolve(__dirname, '../src'),
                use: [
                    {
                        loader: "style-loader",
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader:  "css-modules-typescript-loader",
                        options: {
                            namedExport: true,
                            camelCase: true,
                            sass: true,
                            modules: true
                        }
                    },
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                            sourceMap: true,
                            modules: {
                                localIdentName: "[name]__[local]___[hash:base64:5]"
                            }
                        }
                    },
                    {
                        loader: "postcss-loader",
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader: "sass-loader",
                        options: {
                            sourceMap: true
                        }
                    }
                ]
            }
        ]
    },

    // ...
}

最后再调整.babelrc的相关配置:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "browsers": [
                        "> 1%",
                        "last 5 versions",
                        "ie >= 8"
                    ],
                    "node": "current"
                },
                "modules": false
            }
        ],
        "@babel/preset-react",
        "@babel/typescript"
    ],
    "plugins": [
        "@babel/plugin-transform-runtime",
        "@babel/plugin-transform-modules-commonjs",
        "@babel/plugin-transform-react-jsx-source",
        [
            "babel-plugin-react-css-modules",
            {
                "context": "./",
                "generateScopedName": "[name]__[local]___[hash:base64:5]",
                "autoResolveMultipleImports": true,
                "webpackHotModuleReloading": true,
                "exclude": "node_modules",
                "handleMissingStyleName": "warn"
            }
        ]
    ]
}

特别提出一点,.babelrcbabel-plugin-react-css-modulesgenerateScopedName必须要和webpack.common.jscss-loadermodules.localIdentName保持一致。

由于我们在前面配置了Sass环境,如果要让Typescript下对.scss有较好的支持和识别度,需要做一些相应的处理,首先是需要css-modules-typescript-loader的支持,并且设置:

// webpack.common.js

{
    loader:  "css-modules-typescript-loader",
    options: {
        namedExport: true,  // 类名导出
        camelCase: true,    // 支持驼峰
        sass: true,         // 是否使用Sass
        modules: true       // 是否开启CSS Modules
    }
},

另外还存在一个问题,那就是.scss文件中并没有类似export这样的关键词用于导出一个模块,所以会导致报错找不到模块。如果碰到这样的报错,需要通通过ts的模块声明(declare module)来解决。解决这样的问题需要先在项目的根目录下创建一个typings目录(用于存放.scss的模块声明,以及后续需要用到的全局校对模口)。然后在该目录下创建一个typed-css-modules.d.ts文件,用来放置.scss文件相关的模块声明:

// typed-css-modules.d.ts
declare module '*.scss' {
    const content: any
    export = content
}

这样配置完之后,在终端中执行npm run dev命令之后,你会发现在对应的.css.scss文件名下面会新增*.scss.d.ts*.css.d.ts文件:

*.scss.d.ts文件下会有对应的类名字符串的添加,比如:

到这一步,有关于CSS Modules相关的配置就算是完成了。我们来看一下是否CSS Modules生效了。分别来看.css.scss的使用。

// Button.tsx

import './button.css';

<button styleName="primary btn-info">我就是一个按钮</button>

// button.css
.button {
    background: #f23;
    padding: 5px 10px;
    border: 1px solid currentColor;
    color: #fff;
    border-radius: 5px;
}

.primary {
    composes: button;
    background: #f09;
}

.btn-info {
    color: #23f;
    box-shadow: 1px 1px 2px rgba(40,90,123, .5);
}

最后编译出来的结果:

<!-- HTML -->
<button class="button__primary___2Bj3i button__button___34EB0 button__btn-info___lycHE">我就是一个按钮</button>

<!-- style -->
.button__btn-info___lycHE {
    color: #23f;
    box-shadow: 1px 1px 0.267vw rgba(40, 90, 123, 0.5);
}
.button__primary___2Bj3i {
    background: #f09;
}
.button__button___34EB0 {
    background: #f23;
    padding: 0.667vw 1.333vw;
    border: 1px solid currentColor;
    color: #fff;
    border-radius: 0.667vw;
}

这里借助babel-plugin-react-css-modules的能力,实现styleName替代className。至于使用styleName的优势,这里就不做过多的阐述,详细的可以阅读《React环境下怎么使用CSS Modules》一文。

需要注意的是,如果你想在.tsx中使用className来引用类名,那么在引用.css文件的时候,按下面的方式来处理:

// button.tsx

import * as styles from './button.css';

<button styleName="primary btn-info" className={styles.buttonSuccess}>我就是一个按钮</button>

// button.css

.button {
    background: #f23;
    padding: 5px 10px;
    border: 1px solid currentColor;
    color: #fff;
    border-radius: 5px;
}

.primary {
    composes: button;
    background: #f09;
}

.btn-info {
    color: #23f;
    box-shadow: 1px 1px 2px rgba(40,90,123, .5);
}

.buttonSuccess {
    color: #90f;
}

编译出来的结果如下:

<!-- HTML -->
<button class="button__buttonSuccess___3o8xw button__primary___2Bj3i button__button___34EB0 button__btn-info___lycHE">我就是一个按钮</button>

<!-- Style -->
.button__buttonSuccess___3o8xw {
    color: #90f;
}
.button__btn-info___lycHE {
    color: #23f;
    box-shadow: 1px 1px 0.267vw rgba(40, 90, 123, 0.5);
}
.button__primary___2Bj3i {
    background: #f09;
}
.button__button___34EB0 {
    background: #f23;
    padding: 0.667vw 1.333vw;
    border: 1px solid currentColor;
    color: #fff;
    border-radius: 0.667vw;
}

接下来再来看看.scss的使用:

// App.tsx

import * as styles from './app.scss';

<h1  className={`${styles.title} ${styles['App-logo']}`}> Hello, Webpack + React + Typescript!(^_^)</h1>

// app.scss
$color: #19a;
$bgColor: #fc9;

.title {
    color: $color;
    background-color: $bgColor;
    text-align: center;
    padding: 5px;
}

.App-logo {
    font-size: 2rem;
    color: orange;
}

编译出来的结果如下:

<!-- HTML -->
<h1 class="app__title___1yAlR app__App-logo___2JZ_6"> Hello, Webpack + React + Typescript!(^_^)</h1>

<!-- Style -->
.app__App-logo___2JZ_6 {
    font-size: 2rem;
    color: orange;
}
.app__title___1yAlR {
    color: #19a;
    background-color: #fc9;
    text-align: center;
    padding: 0.667vw;
}

我们再来看看styleName的运用:

// App.tsx
import * as styles from './app.scss';

<h1  className={`${styles.title} ${styles['App-logo']}`} styleName="app-title"> Hello, Webpack + React + Typescript!(^_^)</h1>
<Button styleName="app-button"/>

结果并没达到我们想要的效果,编译时报错:

尝试着另外一种改变,在引入.scss的方式上做相应的调整:

// import * as styles from './app.scss';
// import './app.scss';
const styles = require('./app.scss');

都未能正常的编译。具体原因我也没有找到,相应的解决方案我也没找到。希望有同学能知道这里面的坑。也就是说,如果你是基于Sass的基础上做开发,那么要使用styleName来引入类名,实现CSS Modules的功能的话,只能通过className的方式,而且引入.scss需要以对象的方式引入。

其实在最开始的时候,.css中也碰到一些麻烦,后来借助@types/react-css-modulesbabel-plugin-react-css-modules解决的。

个人建议:如果的工程中使用了CSS Modules的话,那么就没有必要再使用CSS处理器(如Sass,LESS)

说真的,在Typescript、React和Webpack中实现CSS Modules的功能,还真费了不少的劲,也查也很多的资料。在这里,希望这份配置对你构建相应的开发环境有所帮助。

Step11:图片加载

请把分支切换到step11查看代码

在HTML和CSS中我们都会有用到图片的时候。接下来看看Webpack中怎么配置图片加载。在Webpack配置图片资源的加载需要依赖file-loaderurl-loader两个包:

  • file-loader:解决CSS等文件中引入图片路径问题。当遇到图片文件时会将其打包移动到/dist目录下,接下来会获得图片模块的地址,并将地址返回到引入模块到变量之中
  • url-loader:当图片较小的时候会把图片转换成base64编码,大于limit参数的时候还是使用file-loader进行拷贝。这样做是好处是可以直接将图片打包到bundle.js里,不用额外请求图片(省去HTTP请求);其坏处是遇到文件较大时,加载会很耗时,影响用户体验

在命令终端执行下面的命令,安装file-loaderurl-loader

⇒  npm i file-loader url-loader -D

为了更好的测试接下来的配置,先在/src目录下创建一个新的目录/assets,并在该目录下创建一个放置图片的文件夹/images。同时把一张大图(security.svg)和一张小图(copy-solid.svg)放到images中:

然后在webpack.common.jsmodule.rules添加有关于图片资源的相关配置:

module.exports = {
    // ...

    // 模块解析
    module: {
        rules: [
            //...

            // images loader
            {
                test: /\.(png|jp(e*g)|gif|svg|webp)$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            limit: 1024,                        // 小于10kb的图片编译成base64编码,大于的单独打包成图片 
                            name: "images/[hash]-[name].[ext]", // Placeholder占位符
                            publicPath: "assets",               // 最终生成的CSS代码中,图片URL前缀
                            outputPath: "assets",               // 图片输出的实际路径(相对于/dist目录)
                        }
                    }
                ]
            },

        ]
    },

    // ...
}

另外在tsconfig.jsoncompilerOptions增加"outDir": "./dist/"配置项。完成上面的配置,执行npm run dev命令之后,可以看到页面已成功加载所需要图片:

我们分别在CSS和HTML中做了测试。在CSS中可以按照我们正常的方式来引用src/assets/images目录下的图片资源。但在.tsx文件中引用图片的时候,有所不同。

import Security from '../../../../assets/images/security.svg';
import * as Security from '../../../../assets/images/security.svg';

使用上面方式引入图片资源会报图片找不到的错误:

如果我们换成下面的方式,使用可以正常找到图片:

const Security = require('../../../../assets/images/security.svg');

<img src={String(Security)} />

另外如果直接在HTML的imgsrc中引入图片资源:

<img src="../../../../assets/images/security.svg" alt=""/>

也会报404的错误信息。

网上找了一通,并未找到较好的解决方案,这也算是一个预留的坑吧。

上面看到的是开发环境下的效果。接下来验证一下打包后的结果。在命令终端执行npm run build,可以看到在/dist目录下将会新增/assets/images目录,并且未能编译成base64的图片都将放置在该目录下:

我们来验证一下打包出来的文件,在图片资源的引用上是否正确:

和开发环境下运行的结果是一样的。这样表示我们图片的加载已配置成功(注意,在.tsxsrc直接引入相对路径存在图片资源加载404错误,并未得到配置化方案的解决!

Step12:字体文件加载

请把分支切换到step12查看代码

有些项目中会使用字体图标,如果字体图标使用的不是第三方字库或CDN的地址,就需要将字体文件放到本地项目中。比如放在/assets目录下的/fonts目录中。这样就需要面临字体文件加载相关的配置。

Web技巧系列第12期聊了一些关于字体加载相关的话题,感兴趣的同学可以移步看看

在Webpack构建的工程体系中,字体文件加载的策略和图片资源加载的策略是相似的,需要依赖file-loaderurl-loader。基于Step11基础上,在webpack.common.js中添加有关于字体加载相关的配置:

module.exports = {
    // ...

    // 模块解析
    module: {
        rules: [
            // ...

            // font loader
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            limit: 1024,                        // 小于10kb的字体编译成base64编码
                            name: "fonts/[hash]-[name].[ext]", // Placeholder占位符
                            publicPath: "assets",               // 最终生成的CSS代码中,字体URL前缀
                            outputPath: "assets",               // 字体输出的实际路径(相对于/dist目录)
                        }
                    }
                ]
            }

        ]
    },

    // ...
}

我们从IconFont网站上下载字体文件来做测试。把下载下来的字体文件放置到/assets/fonts目录下:

使用@font-face在测试用例中引用所需要的字休息图标文件等:

// app.css
@font-face {
    font-family: "iconfont";
    src: url('../../../../assets/fonts/iconfont.eot');
    src: url('../../../../assets/fonts/iconfont.eot#iefix') format('embedded-opentype'), 
    url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAANwAAsAAAAAB3QAAAMkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDBgqCWII1ATYCJAMMCwgABCAFhG0HPBuBBhHVm4vIfhymW4xbM2f46dSfYt9BPOV91Ut3MMmA2JNEnMjj/gDYb9vTrKSFhEXqt679ZKfAeGN8MeBy/Et8nr29Y2sPXRhgHFCge2AUZQ9cIAF5w9gFLfE+BHBJJB9SsXLNhjhozGUCSM9uXdrhZFzohm7BQbAq9uqKMhcTx5hm3ATmBN+Xz+QXBwxMhflQ3c6VOlL2Xd93Q9RY39iWaiYzpgsA7BAoIB+gQVpXhpqiolI+FK5q0s3NFhrBmCrr8xGoNIu10T+eAWJhdvoDkA3LBrzr60gQeDfkAAYACgALVDoDMyATfrZgZrpuYHBgl5uPa59/XOHCk1pN9j958ggr6uHD6g1XJTdaFHHjUblzD0uefFj79uMCI9rqdseepNnrH/c11j2q5JnuxYfD3Olbx27zvBk7xm13pWnjxZs3Ll22LaLRpiNRK5ZHtm6yZE1sXiHhDReujFm0aYJVbZxO/OGMnbxxdNS6+4skqXXToh29qjX8OhRv0iYh9Pny/2PvL85fYMfQhA6VnBF3vg73OlROHrKC3Aq9Kp5ua5/zoWxyeMfwspn32zaPrFa1/cS2E9v7M6q9PbYndGr61NC95A3EHl/YeOmGlZfjwi8saGTnrKm75sXKiwsbfRrbdtS1vwsaf5rbduY12gLgG6FWq0lBZZC+pWb+k9/QrtW/wDZBJf86ngbgWbnaM6GmWwScMw4L/IEefRN8Pn2La0230oqRRpVRVZTFvD8nrhbFhxkL5fVwS0TnThAcIhIY2CRA4ZBOaCYfTDwKwsKhDFzyqHLYI4z+KET7AblMAhCC2QODQM5AEcwtQjOvYBLJV1gEi4JLYwk70yNT1NicGJWgBf2HaGh1tleasPuO/pgVp7UB+0beYxaGts9Xb7giT7Fh//hRxIFjWuAKHsN5JtiYEhppg8g2dZ2relFraCkaToxK0IL+QzS0Om+8NIXP39Efs+KGoar8jbzH6WFo+w7EzbR2GrqVZ/aPH0UcOKYFriCAs1Ui2KpnJTTShgGFberQKNfV1C5vWd7vBOBiXqTEECWaqjOuv6hWX/rDS2oqCg==') format('woff2'),
    url('../../../../assets/fonts/iconfont.woff') format('woff'),
    url('../../../../assets/fonts/iconfont.ttf') format('truetype'),
    url('../../../../assets/fonts/iconfont.svg#iconfont') format('svg'); 
}

.icon {
    color: #fff;
    text-shadow: 1px 1px 1px rgba(0,0,0,.7);
}

.iconfont {
    composes: icon;
    font-family: "iconfont" !important;
    font-style: normal;
    font-size: 40px;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.icon-fenxiang:before {
    content: "\e670";
}

.icon-guangbo:before {
    content: "\e677";
}

// App.tsx
<span styleName="iconfont icon-fenxiang"></span>
<span styleName="iconfont icon-guangbo"></span>

运行npm run dev的效果,你可以看到页面上已经有了字体实现的图标:

同样的,可以执行npm run build来验证,打包后字体有没有生效:

如果你看到上图的效果,说明你的字体加载配置已成功。恭喜你。如果你感兴趣请继续往下阅读。

Step13:SourceMap的配置

请把分支切换到step13查看代码

使用Webpack打包的项目,编译到/dist目录中的[name].bundle.js代码已被重新混淆。如果出现错误,无法将错误正确定位到原始代码对应位置,这样不便于我们调试代码。如果需要精确调试生产环境代码,我们就需要在Webpack中配置SourceMap相关的配置。

在Webpack中可以通过devtool选项来控制SourceMap是否生效,以及如何生成。换句话说,正确的配置SourceMap有助于我们提高开发效率,更快的定位到错误位置。Webpack给devtool的配置提供了七种不同的模式,而且官方文档对这方面也有详细的描述。如果你对详细的配置感兴趣的话,可以查看相关的文档,如果你只是想在Webpack相关的配置中开启相应的功能的话,可以按下面的方式来开启。

简单地说,为了更高的性能,或者说devtool开启后SourceMap起到最大化的功能作用,我们应该在不同的环境下开启不同的配置。

  • 开发环境devtool的设置取值cheap-module-eval-source-map更佳
  • 线上环境devtool的设置取值cheap-module-source-map更佳

在Step03中我们把Webpack的环境做了一定的优化(虽然不够彻底,但对共用,生产和开发不同的环境做出了相应的分割),我们需要在不同的位置来设置SourceMap。首先在webpack.common.js中的module.exports.output指定sourceMapFilename的值:

// ...

module.exports = {
    // 入口JS路径
    // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始
    entry: {
        index: path.resolve(__dirname,'../src/pages/index/index.tsx'),
    },


    // 编译输出的JS入路径 
    // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
    output: {
        path: DIST_PATH,        // 创建的bundle生成到哪里
        filename: '[name].bundle.js',    // 创建的bundle的名称
        sourceMapFilename: '[name].js.map' // 创建的SourceMap的文件名
    },

    resolve: {
        // ...
    },

    // 模块解析
    module: {
        rules: [
            // ...
        ]
    },

    // ...
}

然后在webpack.dev.js(开发环境)和webpack.prod.js(线上环境)分别设置devtool

// webpack.dev.js

// ...
module.exports = merge(commonConfig, {
    mode: 'development', // 设置webpack mode的模式
    devtool: 'cheap-module-eval-source-map', // 设置SoureMap的模式

    // 开发环境下需要的相关插件配置
    plugins: [

    ],

    // 开发服务器
    devServer: {
        // ...
    }
})

// webpack.prod.js

// ...
module.exports = merge(commonConfig, {
    mode: 'production', // 设置Webpack的mode模式
    devtool: 'cheap-module-source-map',

    // 生产环境下需要的相关插件配置
    plugins: [

    ],
})

Step14:规范代码配置

请把分支切换到step14查看代码

每个人都有着自己的编码习惯,这种习惯并没有什么好坏之分,但是在一个团队协作中,如果每个人都有着自己与众不同的编码习惯,那这将可能是一个致命的问题。如果你是在一个协同作战的团队或项目中,个人还是建议你按照一定的编码风格去编码,哪怕是你个人平时编码,也建议你按照一些语言规范去编码。不管是哪种语言,都有着其自己较好的编码风格。或者通过一些工具、配置来约束代码风格,当然,这可能在最开始的时候会令你很烦感,但慢慢地你会喜欢上的,因为这样一来,你的代码质量会更高,也有一个相应的保障。

在项目工程中,我们就可以借助 ESLintPrettierStyleLintHuskylint-stagedpre-commit等来帮助我们约束自己的代码。

这些配置都挺令人感到头疼的,但一步一步往下撸,会让你的恐惧和惧怕变得越来越小。(^_^)

ESLint的配置

ESLint是什么就不做过多的阐述,只要知道它能够帮助我们发现代码中常见的问题,并能与项目的生态系统其他部分集成。我们接下来会花一些时间来看看怎么在Webpack中配置ESLint的相关能力。

先在命令终端执行:

⇒  npm i eslint -D

针对于我们教程中使用的环境React、Typscript等,ESLint和后续要聊到的相关规范代码的工具,还需要安装一些其他的依赖关系包,比如eslint-config-prettiereslint-plugin-prettiereslint-plugin-reacteslint-plugin-jsx-control-statementsbabel-eslint@typescript-eslint/eslint-plugin@typescript-eslint/parser等。后续用到之后再来安装,或者你可以一次性就直接安装完成。

在安装好ESLint之后,需要在项目的根目录下创建.eslintrc.js文件,然后在文件中添加一些ESLint所需要的配置。创建该文件可以有两种方式,一种是直接创建,另外一种是可以执行下面的命令,以问答的形式来完成.eslintrc.js文件的创建以及一些基本信息的配置:

⇒  ./node_modules/.bin/eslint --init

回答完一系列的问题之后,会安装所需要的依赖包,比如我这里,系统会提示我要不要安装eslint-plugin-react@latest

这样,你的项目根目录底下有了一个带有基本配置的信息的.eslintrc.js

目前为止,虽然该配置是有效的,但它什么都还不会做。那是因为我们还没有将它和Webpack或者文本编辑器结合在一起。接下来在Webpack中配置ESLint。在配置之前需要安装eslint-loaderbabel-eslint

⇒  npm i eslint-loader babel-eslint -D

手动的修改一下.eslintrc.js配置:

module.exports = {
    "parser": "babel-eslint",
    // 指定脚本运行环境
    "env": {
        "browser": true,
        "es6": true,
        "node": true,
        "commonjs": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
    // 脚本在执行期间访问的额外的全局变量
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    // 指定解析器选项
    "parserOptions": {
        "ecmaFeatures": {
            "expperimentalObjectRestSpread": true,
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    // 指定需要的插件
    "plugins": [
        "react"
    ],
    // 启用的规则及其各自的错误级别
    "rules": {
    }
};

只修改.eslintrc.js文件还不够,需要再修改webpack.common.js的配置:

// ...
module.exports = {
    // ...

    // 模块解析
    module: {
        rules: [
            {
                test: /\.(j|t)sx?$/,
                exclude: /node_modules/,
                include: path.resolve(__dirname, '../src'),
                use: [
                    {
                        loader: "babel-loader"
                    },
                    {
                        loader: "awesome-typescript-loader"
                    },
                    {
                        loader: "eslint-loader"
                    },
                ],
            },

            // ...

        ]
    },

    // ...
}

保存之后,在命令终端执行npm run dev之后,会报错:

在全网查了一下,不少同学碰到同样的问题

eslint 5.x: eslint/lib/formatters/stylish
eslint 6.x: eslint/lib/cli-engine/formatters/stylish

在这个教程中eslint对应的版本是"eslint": "^6.0.1"。根据@davalapar提供的相应解决方案,在webpack.common.jsesling-loader中添加相应的stylish路径:

{
    loader: "eslint-loader",
    options: {
        formatter: require('eslint/lib/cli-engine/formatters/stylish')
    },
},

再执行npm run dev,前面碰到的问题已经解决了。接下来,来验证一下ESLint是否生效。假设在App.tsx文件中添加下面两行代码:

// App.tsx

import * as React from 'react';
import './app.css';
const Security = require('../../../../assets/images/security.svg');

let b = "Hello"
alert('Hellow ESLint')

import Button from '../../../../components/Button/Button';

export default class App extends React.Component<any> {
    render() {
        return (
            <div>
                <div>
                    <img src={String(Security)} />
                    {/* 下面这样引用,图片会报404错误 */}
                    {/* <img src="../../../../assets/images/security.svg" alt=""/> */}
                </div>
                <h1  styleName="title"> Hello, Webpack + React + Typescript!(^_^)</h1>
                <div>
                    <span styleName="iconfont icon-fenxiang"></span>
                    <Button />
                    <span styleName="iconfont icon-guangbo"></span>
                </div>
                
            </div>
        )
    }
}

保存该文件时,ESLint会检测出不规范的代码,并且会提供相应的报错信息:

接着再调整package.json文件中的scripts中的配置,在执行npm run dev的时候可以执行ESLint:

"scripts": {
    "eslint": "eslint --ext .js,.jsx,.tsx,.ts ./src",
    "build": "webpack --config ./build/webpack.prod.js --mode production",
    "dev": "npm run eslint && webpack-dev-server --config ./build/webpack.dev.js --mode development --open",
    "test": "echo \"Error: no test specified\" && exit 1"
},

这个时候我们执行npm run dev时会报错:

看到了button.css.d.ts等文件报错,我尝试着添加Typescript相关的配置。先执行下面的相关命令,安装所需要的依赖关系:

>> npm i eslint-plugin-jsx-control-statements @typescript-eslint/eslint-plugin @typescript-eslint/parser -D

同时修改.eslintrc.js的配置:

module.exports = {
    "parser": "@typescript-eslint/parser",
    // 指定脚本运行环境
    "env": {
        "browser": true,
        "es6": true,
        "node": true,
        "commonjs": true,
        "jsx-control-statements/jsx-control-statements": true
    },
    "extends": [
        "plugin:@typescript-eslint/recommended",
        "plugin:react/recommended",
        'plugin:jsx-control-statements/recommended',
    ],
    // 脚本在执行期间访问的额外的全局变量
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    // 指定解析器选项
    "parserOptions": {
        "ecmaFeatures": {
            "expperimentalObjectRestSpread": true,
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    // 指定需要的插件
    "plugins": [
        "@typescript-eslint", 
        "jsx-control-statements",
        "react"
    ],
    // 启用的规则及其各自的错误级别
    "rules": {
    }
};

再次尝试着执行npm run dev,还是有几个报错信息:

知道报错的相关信息就好办了。接着寻找相应的解决方案呗。

这个时候你需要知道怎么用Google来帮你寻找答案。我一般尝试着在Google中搜索相应的报错信息,比如输入关键词warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any

最简单的处理方案是在.eslintrc.js中的rules{}添加相应的配置:

"rules": {
    "@typescript-eslint/no-explicit-any": "off",
    "@typescript-eslint/explicit-member-accessibility": "off",
    "@typescript-eslint/no-var-requires": "off",
    "@typescript-eslint/explicit-function-return-type": "off",
}

添加完配置,保存之后继续执行npm run dev又出来新的问题:

根据相关提示,在package.json文件中的eslint配置命令中添加--fix参数:

"eslint": "eslint --ext .js,.jsx,.tsx,.ts --fix ./src",

这样就越来越接近我们想要的了,报错信息越来越少了:

从上面的示例上来看,仅剩下一条报错信息了。如果把App.tsx文件中代码修复之后,就OK了。

.eslintrc.js配置文件中的rules的配置项都可以根据自己的喜好进行增减和修改,每个配置项的可选值一般有三个:offwarnerror。下面的列表是ESLint所有规则配置的相关解读(值可以修改):

"no-alert": 0,//禁止使用alert confirm prompt
"no-array-constructor": 2,//禁止使用数组构造器
"no-bitwise": 0,//禁止使用按位运算符
"no-caller": 1,//禁止使用arguments.caller或arguments.callee
"no-catch-shadow": 2,//禁止catch子句参数与外部作用域变量同名
"no-class-assign": 2,//禁止给类赋值
"no-cond-assign": 2,//禁止在条件表达式中使用赋值语句
"no-console": 2,//禁止使用console
"no-const-assign": 2,//禁止修改const声明的变量
"no-constant-condition": 2,//禁止在条件中使用常量表达式 if(true) if(1)
"no-continue": 0,//禁止使用continue
"no-control-regex": 2,//禁止在正则表达式中使用控制字符
"no-debugger": 2,//禁止使用debugger
"no-delete-var": 2,//不能对var声明的变量使用delete操作符
"no-div-regex": 1,//不能使用看起来像除法的正则表达式/=foo/
"no-dupe-keys": 2,//在创建对象字面量时不允许键重复 {a:1,a:1}
"no-dupe-args": 2,//函数参数不能重复
"no-duplicate-case": 2,//switch中的case标签不能重复
"no-else-return": 2,//如果if语句里面有return,后面不能跟else语句
"no-empty": 2,//块语句中的内容不能为空
"no-empty-character-class": 2,//正则表达式中的[]内容不能为空
"no-empty-label": 2,//禁止使用空label
"no-eq-null": 2,//禁止对null使用==或!=运算符
"no-eval": 1,//禁止使用eval
"no-ex-assign": 2,//禁止给catch语句中的异常参数赋值
"no-extend-native": 2,//禁止扩展native对象
"no-extra-bind": 2,//禁止不必要的函数绑定
"no-extra-boolean-cast": 2,//禁止不必要的bool转换
"no-extra-parens": 2,//禁止非必要的括号
"no-extra-semi": 2,//禁止多余的冒号
"no-fallthrough": 1,//禁止switch穿透
"no-floating-decimal": 2,//禁止省略浮点数中的0 .5 3.
"no-func-assign": 2,//禁止重复的函数声明
"no-implicit-coercion": 1,//禁止隐式转换
"no-implied-eval": 2,//禁止使用隐式eval
"no-inline-comments": 0,//禁止行内备注
"no-inner-declarations": [2, "functions"],//禁止在块语句中使用声明(变量或函数)
"no-invalid-regexp": 2,//禁止无效的正则表达式
"no-invalid-this": 2,//禁止无效的this,只能用在构造器,类,对象字面量
"no-irregular-whitespace": 2,//不能有不规则的空格
"no-iterator": 2,//禁止使用__iterator__ 属性
"no-label-var": 2,//label名不能与var声明的变量名相同
"no-labels": 2,//禁止标签声明
"no-lone-blocks": 2,//禁止不必要的嵌套块
"no-lonely-if": 2,//禁止else语句内只有if语句
"no-loop-func": 1,//禁止在循环中使用函数(如果没有引用外部变量不形成闭包就可以)
"no-mixed-requires": [0, false],//声明时不能混用声明类型
"no-mixed-spaces-and-tabs": [2, false],//禁止混用tab和空格
"linebreak-style": [0, "windows"],//换行风格
"no-multi-spaces": 1,//不能用多余的空格
"no-multi-str": 2,//字符串不能用\换行
"no-multiple-empty-lines": [1, {"max": 2}],//空行最多不能超过2行
"no-native-reassign": 2,//不能重写native对象
"no-negated-in-lhs": 2,//in 操作符的左边不能有!
"no-nested-ternary": 0,//禁止使用嵌套的三目运算
"no-new": 1,//禁止在使用new构造一个实例后不赋值
"no-new-func": 1,//禁止使用new Function
"no-new-object": 2,//禁止使用new Object()
"no-new-require": 2,//禁止使用new require
"no-new-wrappers": 2,//禁止使用new创建包装实例,new String new Boolean new Number
"no-obj-calls": 2,//不能调用内置的全局对象,比如Math() JSON()
"no-octal": 2,//禁止使用八进制数字
"no-octal-escape": 2,//禁止使用八进制转义序列
"no-param-reassign": 2,//禁止给参数重新赋值
"no-path-concat": 0,//node中不能使用__dirname或__filename做路径拼接
"no-plusplus": 0,//禁止使用++,--
"no-process-env": 0,//禁止使用process.env
"no-process-exit": 0,//禁止使用process.exit()
"no-proto": 2,//禁止使用__proto__属性
"no-redeclare": 2,//禁止重复声明变量
"no-regex-spaces": 2,//禁止在正则表达式字面量中使用多个空格 /foo bar/
"no-restricted-modules": 0,//如果禁用了指定模块,使用就会报错
"no-return-assign": 1,//return 语句中不能有赋值表达式
"no-script-url": 0,//禁止使用javascript:void(0)
"no-self-compare": 2,//不能比较自身
"no-sequences": 0,//禁止使用逗号运算符
"no-shadow": 2,//外部作用域中的变量不能与它所包含的作用域中的变量或参数同名
"no-shadow-restricted-names": 2,//严格模式中规定的限制标识符不能作为声明时的变量名使用
"no-spaced-func": 2,//函数调用时 函数名与()之间不能有空格
"no-sparse-arrays": 2,//禁止稀疏数组, [1,,2]
"no-sync": 0,//nodejs 禁止同步方法
"no-ternary": 0,//禁止使用三目运算符
"no-trailing-spaces": 1,//一行结束后面不要有空格
"no-this-before-super": 0,//在调用super()之前不能使用this或super
"no-throw-literal": 2,//禁止抛出字面量错误 throw "error";
"no-undef": 1,//不能有未定义的变量
"no-undef-init": 2,//变量初始化时不能直接给它赋值为undefined
"no-undefined": 2,//不能使用undefined
"no-unexpected-multiline": 2,//避免多行表达式
"no-underscore-dangle": 1,//标识符不能以_开头或结尾
"no-unneeded-ternary": 2,//禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
"no-unreachable": 2,//不能有无法执行的代码
"no-unused-expressions": 2,//禁止无用的表达式
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],//不能有声明后未被使用的变量或参数
"no-use-before-define": 2,//未定义前不能使用
"no-useless-call": 2,//禁止不必要的call和apply
"no-void": 2,//禁用void操作符
"no-var": 0,//禁用var,用let和const代替
"no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],//不能有警告备注
"no-with": 2,//禁用with

"array-bracket-spacing": [2, "never"],//是否允许非空数组里面有多余的空格
"arrow-parens": 0,//箭头函数用小括号括起来
"arrow-spacing": 0,//=>的前/后括号
"accessor-pairs": 0,//在对象中使用getter/setter
"block-scoped-var": 0,//块语句中使用var
"brace-style": [1, "1tbs"],//大括号风格
"callback-return": 1,//避免多次调用回调什么的
"camelcase": 2,//强制驼峰法命名
"comma-dangle": [2, "never"],//对象字面量项尾不能有逗号
"comma-spacing": 0,//逗号前后的空格
"comma-style": [2, "last"],//逗号风格,换行时在行首还是行尾
"complexity": [0, 11],//循环复杂度
"computed-property-spacing": [0, "never"],//是否允许计算后的键名什么的
"consistent-return": 0,//return 后面是否允许省略
"consistent-this": [2, "that"],//this别名
"constructor-super": 0,//非派生类不能调用super,派生类必须调用super
"curly": [2, "all"],//必须使用 if(){} 中的{}
"default-case": 2,//switch语句最后必须有default
"dot-location": 0,//对象访问符的位置,换行的时候在行首还是行尾
"dot-notation": [0, { "allowKeywords": true }],//避免不必要的方括号
"eol-last": 0,//文件以单一的换行符结束
"eqeqeq": 2,//必须使用全等
"func-names": 0,//函数表达式必须有名字
"func-style": [0, "declaration"],//函数风格,规定只能使用函数声明/函数表达式
"generator-star-spacing": 0,//生成器函数*的前后空格
"guard-for-in": 0,//for in循环要用if语句过滤
"handle-callback-err": 0,//nodejs 处理错误
"id-length": 0,//变量名长度
"indent": [2, 4],//缩进风格
"init-declarations": 0,//声明时必须赋初值
"key-spacing": [0, { "beforeColon": false, "afterColon": true }],//对象字面量中冒号的前后空格
"lines-around-comment": 0,//行前/行后备注
"max-depth": [0, 4],//嵌套块深度
"max-len": [0, 80, 4],//字符串最大长度
"max-nested-callbacks": [0, 2],//回调嵌套深度
"max-params": [0, 3],//函数最多只能有3个参数
"max-statements": [0, 10],//函数内最多有几个声明
"new-cap": 2,//函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用
"new-parens": 2,//new时必须加小括号
"newline-after-var": 2,//变量声明后是否需要空一行
"object-curly-spacing": [0, "never"],//大括号内是否允许不必要的空格
"object-shorthand": 0,//强制对象字面量缩写语法
"one-var": 1,//连续声明
"operator-assignment": [0, "always"],//赋值运算符 += -=什么的
"operator-linebreak": [2, "after"],//换行时运算符在行尾还是行首
"padded-blocks": 0,//块语句内行首行尾是否要空行
"prefer-const": 0,//首选const
"prefer-spread": 0,//首选展开运算
"prefer-reflect": 0,//首选Reflect的方法
"quotes": [1, "single"],//引号类型 `` "" ''
"quote-props":[2, "always"],//对象字面量中的属性名是否强制双引号
"radix": 2,//parseInt必须指定第二个参数
"id-match": 0,//命名检测
"require-yield": 0,//生成器函数必须有yield
"semi": [2, "always"],//语句强制分号结尾
"semi-spacing": [0, {"before": false, "after": true}],//分号前后空格
"sort-vars": 0,//变量声明时排序
"space-after-keywords": [0, "always"],//关键字后面是否要空一格
"space-before-blocks": [0, "always"],//不以新行开始的块{前面要不要有空格
"space-before-function-paren": [0, "always"],//函数定义时括号前面要不要有空格
"space-in-parens": [0, "never"],//小括号里面要不要有空格
"space-infix-ops": 0,//中缀操作符周围要不要有空格
"space-return-throw-case": 2,//return throw case后面要不要加空格
"space-unary-ops": [0, { "words": true, "nonwords": false }],//一元运算符的前/后要不要加空格
"spaced-comment": 0,//注释风格要不要有空格什么的
"strict": 2,//使用严格模式
"use-isnan": 2,//禁止比较时使用NaN,只能用isNaN()
"valid-jsdoc": 0,//jsdoc规则
"valid-typeof": 2,//必须使用合法的typeof的值
"vars-on-top": 2,//var必须放在作用域顶部
"wrap-iife": [2, "inside"],//立即执行函数表达式的小括号风格
"wrap-regex": 0,//正则表达式字面量用小括号包起来
"yoda": [2, "never"]//禁止尤达条件

可以说每个团队都有属于自己的一份ESLint配置清单,除ESLint官方提供的一份标准配置之外,还有Airbnb阿里腾讯AlloyTeam等团队提供的相关配置清单。

不管是哪份配置只是建设性的意见,更多的应该要大家自己在使用中去体会。比如下面这份示例代码:

module.exports = {
    // 解析语法
    "parser": "@typescript-eslint/parser",
    // 指定脚本运行环境
    "env": {
        "browser": true,
        "es6": true,
        "node": true,
        "commonjs": true,
        "jsx-control-statements/jsx-control-statements": true
    },
    "extends": [
        "plugin:@typescript-eslint/recommended",
        "plugin:react/recommended",
        "plugin:jsx-control-statements/recommended",
    ],
    "root": true,
    // 脚本在执行期间访问的额外的全局变量
    // 当访问当前源文件内未定义的变量时,no-undef规则将发出警告
    // 所以需要定义这些额外的全局变量
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly",
        "OnlySVG": true,
        "monitor": true,
        "CanvasRender": true,
    },
    // 指定解析器选项
    "parserOptions": {
        // 使用额外的语言特性
        "ecmaFeatures": {
            "expperimentalObjectRestSpread": true,
            "jsx": true,
            "modules": true
        },
        // 启用ES2018语法支持
        "ecmaVersion": 2018,
        // module表示ECMAScript模块
        "sourceType": "module"
    },
    // 指定需要的插件
    "plugins": [
        "@typescript-eslint", 
        "jsx-control-statements",
        "react"
    ],
    // 启用的规则及其各自的错误级别
    "rules": {
        "@typescript-eslint/no-explicit-any": "off",
        "@typescript-eslint/explicit-member-accessibility": "off",
        "@typescript-eslint/no-var-requires": "off",
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/indent": [
            "error", 
            4, 
            { VariableDeclarator: 4, SwitchCase: 1 }
        ],
        "@typescript-eslint/no-unused-vars": 0,
        "@typescript-eslint/interface-name-prefix": 0,
        

        // React相关校验规则
        "react/jsx-indent": [2, 4],
        "react/jsx-no-undef": [2, { allowGlobals: true }],
        "jsx-control-statements/jsx-use-if-tag": 0,

        // 设置了 setter ,必须相应设置 getter ,反之不必须
        "accessor-pairs": 2,

        // 数组方括号前后的换行符使用规则
        // @off 不关心
        "array-bracket-newline": 0,

        // 数组方括号前后的空格使用规则
        // @off 不关心
        "array-bracket-spacing": 0,

        // 数组的 map、filter、sort 等方法,回调函数必须有返回值
        "array-callback-return": 2,

        // 每个数组项是否独占一行
        // @off 不关心
        "array-element-newline": 0,

        // 箭头函数的书写规则
        // @off 不限制
        "arrow-body-style": 0,

        // 箭头函数的圆括号使用规则
        // @off 不限制
        "arrow-parens": 0,

        // 箭头函数的空格使用规则
        // @off 不限制
        "arrow-spacing": 0,

        // 不能在块外使用块作用域内 var 定义的变量
        "block-scoped-var": 2,

        // 代码块花括号前后的空格规则
        // @off 不关心
        "block-spacing": 0,

        // if else 的花括号换行规则
        // @off 不关心
        "brace-style": 0,

        // callback 之后必须立即 return
        // @off 没必要
        "callback-return": 0,

        // 变量名必须使用驼峰式
        // @off 暂不限制
        "camelcase": 0,

        // 注释的首字母应该大写
        // @off 没必要
        "capitalized-comments": 0,

        // 对象的最后一项后面是否写逗号
        // @off 此项目不关心
        // @fixable 对于 PC 项目考虑兼容性时需要设置
        "comma-dangle": 0,

        // 逗号前后是否有空格
        // @off 不关心
        "comma-spacing": 0,

        // 逗号写在行首还是行尾
        // @off 不关心
        "comma-style": 0,

        // 禁止函数 if ... else if ... else 的复杂度超过 20
        "complexity": 2,

        // 使用方括号访问对象属性时,方括号前后的空格规则
        // @off 不关心
        "computed-property-spacing": 0,

        // 禁止函数在不同条件下返回不同类型的值
        // @off 有时候会希望通过参数获取不同类型的返回值
        "consistent-return": 0,

        // this 的别名规则,只允许 self 或 that
        "consistent-this": [2, "self", "that"],

        // 构造函数中必须调用 super
        // @off 没必要
        "constructor-super": 0,

        // if 后必须包含 { ,单行 if 除外
        "curly": [2, "multi-line", "consistent"],

        // switch 语句必须包含 default
        "default-case": 2,

        // 链式操作时,点的位置,是在上一行结尾还是下一行开头
        // @off 不关心
        "dot-location": 0,

        // 文件最后必须有空行
        // @off 不限制
        "eol-last": 0,

        // 必须使用 === 和 !== ,和 null 对比时除外
        "eqeqeq": [2, "always", { "null": "ignore" }],

        // for 循环不得因方向错误造成死循环
        "for-direction": 2,

        // 执行函数的圆括号前后的空格规则
        // @off 不关心
        "func-call-spacing": 0,

        // 把函数赋给变量或对象属性时,函数名和变量名或对象属性名必须一致
        // @off 不限制
        "func-name-matching": 0,

        // 不允许匿名函数
        // @off 不限制
        "func-names": 0,

        // 必须只使用函数申明或只使用函数表达式
        // @off 不限制
        "func-style": 0,

        // generator 的 * 前后空格使用规则
        // @off 不限制
        "generator-star-spacing": 0,

        // getter 必须有返回值,允许返回 undefined
        "getter-return": [2, { allowImplicit: true }],

        // require 必须在全局作用域下
        // @off 条件加载很常见
        "global-require": 0,

        // for in 时需检测 hasOwnProperty
        "guard-for-in": 2,

        // callback 中的 err、error 参数和变量必须被处理
        "handle-callback-err": 2,

        // id 黑名单
        // @off 暂时没有
        "id-blacklist": 0,

        // 变量名长度限制
        // @off 长度不是重点,清晰易读才是关键
        "id-length": 0,

        // 限制变量名必须匹配指定的正则表达式
        // @off 没必要限制变量名
        "id-match": 0,

        // 缩进使用 tab 还是空格
        // @off 不关心
        "indent": 0,

        // 变量必须在定义的时候赋值
        // @off 先定义后赋值很常见
        "init-declarations": 0,

        // jsx 语法中,属性的值必须使用双引号
        "jsx-quotes": [1, "prefer-single"],

        // 对象字面量冒号前后的空格使用规则
        // @off 不关心
        "key-spacing": 0,

        // 关键字前后必须有空格
        "keyword-spacing": 2,

        // 换行符使用规则
        // @off 不关心
        "linebreak-style": 0,

        // 单行注释必须写在前一行还是行尾
        // @off 不限制
        "line-comment-position": 0,

        // 注释前后是否要空一行
        // @off 不限制
        "lines-around-comment": 0,

        // 最大块嵌套深度为 5 层
        "max-depth": [2, 5],

        // 限制单行代码的长度
        // @off 不限制
        "max-len": 0,

        // 限制单个文件最大行数
        // @off 不限制
        "max-lines": 0,

        // 最大回调深度为 3 层
        "max-nested-callbacks": [2, 3],

        // 函数的形参不能多于8个
        "max-params": [2, 8],

        // 限制一行中的语句数量
        // @off 没必要限制
        "max-statements-per-line": 0,

        // 限制函数块中的语句数量
        // @off 没必要限制
        "max-statements": 0,

        // 三元表达式的换行规则
        // @off 不限制
        "multiline-ternary": 0,

        // new关键字后类名应首字母大写
        "new-cap": [2, {
            "capIsNew": false, // 允许大写开头的函数直接执行
        }],

        // new 关键字后类应包含圆括号
        "new-parens": 2,

        // 链式调用是否要换行
        // @off 不限制
        "newline-per-chained-call": 0,

        // 禁止 alert,提醒开发者,上线时要去掉
        "no-alert": 1,

        // 禁止使用 Array 构造函数,使用 Array(num) 直接创建长度为 num 的数组时可以
        "no-array-constructor": 2,

        // 禁止将 await 写在循环里
        "no-await-in-loop": 2,

        // 禁止位运算
        // @off 不限制
        "no-bitwise": 0,

        // 禁止在 Node.js 中直接调用 Buffer 构造函数
        "no-buffer-constructor": 2,

        // 禁止使用 arguments.caller 和 arguments.callee
        "no-caller": 2,

        // switch的条件中出现 var、let、const、function、class 等关键字,必须使用花括号把内容括起来
        "no-case-declarations": 2,

        // catch中不得使用已定义的变量名
        "no-catch-shadow": 2,

        // class定义的类名不得与其它变量重名
        "no-class-assign": 2,

        // 禁止与 -0 做比较
        "no-compare-neg-zero": 2,

        // 禁止在 if、for、while 中出现赋值语句,除非用圆括号括起来
        "no-cond-assign": [2, "except-parens"],

        // 禁止出现难以理解的箭头函数,除非用圆括号括起来
        "no-confusing-arrow": [2, { "allowParens": true }],

        // 禁止使用 console,提醒开发者,上线时要去掉
        "no-console": 1,

        // 禁止使用常量作为判断条件
        "no-constant-condition": [2, { "checkLoops": false }],

        // 禁止对 const 定义重新赋值
        "no-const-assign": 2,

        // 禁止 continue
        // @off 很常用
        "no-continue": 0,

        // 禁止正则表达式中出现 Ctrl 键的 ASCII 表示,即/\x1f/
        "no-control-regex": 2,

        // 禁止 debugger 语句,提醒开发者,上线时要去掉
        "no-debugger": 1,

        // 禁止对变量使用 delete 关键字,删除对象的属性不受限制
        "no-delete-var": 2,

        // 禁止在正则表达式中出现形似除法操作符的开头,如 let a = /=foo/
        // @off 有代码高亮的话,在阅读这种代码时,也完全不会产生歧义或理解上的困难
        "no-div-regex": 0,

        // 函数参数禁止重名
        "no-dupe-args": 2,

        // 禁止对象出现重名键值
        "no-dupe-keys": 2,

        // 类方法禁止重名
        "no-dupe-class-members": 2,

        // 禁止 switch 中出现相同的 case
        "no-duplicate-case": 2,

        // 禁止重复 import
        "no-duplicate-imports": 2,

        // 禁止出现 if (cond) { return a } else { return b },应该写为 if (cond) { return a } return b
        // @off 有时前一种写法更清晰易懂
        "no-else-return": 0,

        // 正则表达式中禁止出现空的字符集[]
        "no-empty-character-class": 2,

        // 禁止空的 function
        // 包含注释的情况下允许
        "no-empty-function": 2,

        // 禁止解构中出现空 {} 或 []
        "no-empty-pattern": 2,

        // 禁止出现空代码块
        "no-empty": [2, { "allowEmptyCatch": true }],

        // 禁止 == 和 != 与 null 做比较,必须用 === 或 !==
        // @off 非严格相等可以同时判断 null 和 undefined
        "no-eq-null": 0,

        // 禁止使用 eval
        "no-eval": 2,

        // catch 定义的参数禁止赋值
        "no-ex-assign": 2,

        // 禁止扩展原生对象
        "no-extend-native": [2, { "exceptions": ["Array", "Object"] }],

        // 禁止额外的 bind
        "no-extra-bind": 2,

        // 禁止额外的布尔值转换
        "no-extra-boolean-cast": 2,

        // 禁止额外的 label
        "no-extra-label": 2,

        // 禁止额外的括号,仅针对函数体
        "no-extra-parens": [2, "functions"],

        // 禁止额外的分号
        "no-extra-semi": 2,

        // 每一个 switch 的 case 都需要有 break, return 或 throw
        // 包含注释的情况下允许
        "no-fallthrough": [2, { "commentPattern": "." }],

        // 不允许使用 2. 或 .5 来表示数字,需要用 2、2.0、0.5 的格式
        "no-floating-decimal": 2,

        // 禁止对函数声明重新赋值
        "no-func-assign": 2,

        // 禁止对全局变量赋值
        "no-global-assign": 2,

        // 禁止使用隐式类型转换
        "no-implicit-coercion": [2, {
            "allow": ["+", "!!"] // 允许 + 转数值 "" + 转字符串和 !! 转布尔值
        }],

        // 禁止在 setTimeout 和 setInterval 中传入字符串,因会触发隐式 eval
        "no-implied-eval": 2,

        // 禁止隐式定义全局变量
        "no-implicit-globals": 2,

        // 禁止行内注释
        // @off 很常用
        "no-inline-comments": 0,

        // 禁止在块作用域内使用 var 或函数声明
        "no-inner-declarations": [2, "both"],

        // 禁止使用非法的正则表达式
        "no-invalid-regexp": 2,

        // 禁止在类之外的地方使用 this
        // @off this 的使用很灵活,事件回调中可以表示当前元素,函数也可以先用 this,等以后被调用的时候再 call
        "no-invalid-this": 0,

        // 禁止使用不规范空格
        "no-irregular-whitespace": [2, {
            "skipStrings": true, // 允许在字符串中使用
            "skipComments": true, // 允许在注释中使用
            "skipRegExps": true, // 允许在正则表达式中使用
            "skipTemplates": true, // 允许在模板字符串中使用
        }],

        // 禁止使用 __iterator__
        "no-iterator": 2,

        // label 不得与已定义的变量重名
        "no-label-var": 2,

        // 禁止使用 label
        // @off 禁止了将很难 break 多重循环和多重 switch
        "no-labels": 0,

        // 禁止使用无效的块作用域
        "no-lone-blocks": 2,

        // 禁止 else 中只有一个单独的 if
        // @off 单独的 if 可以把逻辑表达的更清楚
        "no-lonely-if": 0,

        // 禁止 for (var i) { function() { use i } },使用 let 则可以
        "no-loop-func": 2,

        // 禁止魔法数字
        "no-magic-numbers": 0,

        // 禁止使用混合的逻辑判断,必须把不同的逻辑用圆括号括起来
        "no-mixed-operators": [2, {
            "groups": [
                ["&&", "||"]
            ]
        }],

        // 相同类型的 require 必须放在一起
        // @off 不限制
        "no-mixed-requires": 0,

        // 禁止混用空格和 tab 来做缩进,必须统一
        "no-mixed-spaces-and-tabs": 2,

        // 禁止连等赋值
        "no-multi-assign": 2,

        // 禁止使用连续的空格
        "no-multi-spaces": 2,

        // 禁止使用 \ 来定义多行字符串,统一使用模板字符串来做
        "no-multi-str": 2,

        // 连续空行的数量限制
        "no-multiple-empty-lines": [2, {
            max: 3, // 文件内最多连续 3 个
            maxEOF: 1, // 文件末尾最多连续 1 个
            maxBOF: 1 // 文件头最多连续 1 个
        }],

        // 禁止 if 中出现否定表达式 !==
        // @off 否定的表达式可以把逻辑表达的更清楚
        "no-negated-condition": 0,

        // 禁止嵌套的三元表达式
        // @off 没有必要限制
        "no-nested-ternary": 0,

        // 禁止 new Function
        // @off 有时会用它来解析非标准格式的 JSON 数据
        "no-new-func": 0,

        // 禁止使用 new Object
        "no-new-object": 2,

        // 禁止使用 new require
        "no-new-require": 2,

        // 禁止使用 new Symbol
        "no-new-symbol": 2,

        // 禁止 new Boolean、Number 或 String
        "no-new-wrappers": 2,

        // 禁止 new 一个类而不存储该实例
        "no-new": 2,

        // 禁止把原生对象 Math、JSON、Reflect 当函数使用
        "no-obj-calls": 2,

        // 禁止使用八进制转义符
        "no-octal-escape": 2,

        // 禁止使用0开头的数字表示八进制
        "no-octal": 2,

        // 禁止使用 __dirname + "file" 的形式拼接路径,应该使用 path.join 或 path.resolve 来代替
        "no-path-concat": 2,

        // 禁止对函数的参数重新赋值
        "no-param-reassign": 2,

        // 禁止 ++ 和 --
        // @off 很常用
        "no-plusplus": 0,

        // 禁止使用 process.env.NODE_ENV
        // @off 使用很常见
        "no-process-env": 0,

        // 禁止使用 process.exit(0)
        // @off 使用很常见
        "no-process-exit": 0,

        // 禁止使用 hasOwnProperty, isPrototypeOf 或 propertyIsEnumerable
        // @off 与 guard-for-in 规则冲突,且没有必要
        "no-prototype-builtins": 0,

        // 禁止使用 __proto__
        "no-proto": 2,

        // 禁止重复声明
        "no-redeclare": 2,

        // 禁止在正则表达式中出现连续空格
        "no-regex-spaces": 2,

        // 禁止特定的全局变量
        // @off 暂时没有
        "no-restricted-globals": 0,

        // 禁止 import 特定的模块
        // @off 暂时没有
        "no-restricted-imports": 0,

        // 禁止使用特定的模块
        // @off 暂时没有
        "no-restricted-modules": "off",

        // 禁止特定的对象属性
        // @off 暂时没有
        "no-restricted-properties": 0,

        // 禁止使用特定的语法
        // @off 暂时没有
        "no-restricted-syntax": 0,

        // 禁止在return中赋值
        "no-return-assign": 2,

        // 禁止在 return 中使用 await
        "no-return-await": 2,

        // 禁止 location.href = "javascript:void"
        "no-script-url": 2,

        // 禁止将自己赋值给自己
        "no-self-assign": 2,

        // 禁止自己与自己作比较
        "no-self-compare": 2,

        // 禁止逗号操作符
        "no-sequences": 2,

        // 禁止使用保留字作为变量名
        "no-shadow-restricted-names": 2,

        // 禁止在嵌套作用域中出现重名的定义,如 let a; function b() { let a }
        "no-shadow": 2,

        // 禁止数组中出现连续逗号
        "no-sparse-arrays": 2,

        // 禁止使用 node 中的同步的方法,比如 fs.readFileSync
        // @off 使用很常见
        "no-sync": 0,

        // 禁止使用 tabs
        // @off 不限制
        "no-tabs": 0,

        // 禁止普通字符串中出现模板字符串语法
        "no-template-curly-in-string": 2,

        // 禁止三元表达式
        // @off 很常用
        "no-ternary": 0,

        // 禁止在构造函数的 super 之前使用 this
        "no-this-before-super": 2,

        // 禁止 throw 字面量,必须 throw 一个 Error 对象
        "no-throw-literal": 2,

        // 禁止行尾空格
        "no-trailing-spaces": [2, {
            "skipBlankLines": true, // 不检查空行
            "ignoreComments": true // 不检查注释
        }],

        // 禁止将 undefined 赋值给变量
        "no-undef-init": 2,

        // 禁止访问未定义的变量或方法
        "no-undef": 2,

        // 禁止使用 undefined,如需判断一个变量是否为 undefined,请使用 typeof a === "undefined"
        "no-undefined": 2,

        // 禁止变量名中使用下划线
        // @off 暂不限制
        "no-underscore-dangle": 0,

        // 禁止出现难以理解的多行代码
        "no-unexpected-multiline": 2,

        // 循环体内必须对循环条件进行修改
        "no-unmodified-loop-condition": 2,

        // 禁止不必要的三元表达式
        "no-unneeded-ternary": [2, { "defaultAssignment": false }],

        // 禁止出现不可到达的代码,如在 return、throw 之后的代码
        "no-unreachable": 2,

        // 禁止在finally块中出现 return、throw、break、continue
        "no-unsafe-finally": 2,

        // 禁止出现不安全的否定,如 for (!key in obj} {},应该写为 for (!(key in obj)} {}
        "no-unsafe-negation": 2,

        // 禁止出现无用的表达式
        "no-unused-expressions": [2,
            {
                "allowShortCircuit": true, // 允许使用 a() || b 或 a && b()
                "allowTernary": true, // 允许在表达式中使用三元运算符
                "allowTaggedTemplates": true, // 允许标记模板字符串
            }
        ],

        // 禁止定义不使用的 label
        "no-unused-labels": 2,

        // 禁止定义不使用的变量
        "no-unused-vars": [2,
            {
                "vars": "all", // 变量定义必须被使用
                "args": "none", // 对于函数形参不检测
                "ignoreRestSiblings": true, // 忽略剩余子项 fn(...args),{a, b, ...coords}
                "caughtErrors": "none", // 忽略 catch 语句的参数使用
            }
        ],

        // 禁止在变量被定义之前使用它
        "no-use-before-define": [2,
            {
                "functions": false, // 允许函数在定义之前被调用
                "classes": false, // 允许类在定义之前被引用
            }
        ],

        // 禁止不必要的 call 和 apply
        "no-useless-call": 2,

        // 禁止使用不必要计算的key,如 var a = { ["0"]: 0 }
        "no-useless-computed-key": 2,

        // 禁止不必要的字符串拼接
        "no-useless-concat": 2,

        // 禁止无用的构造函数
        "no-useless-constructor": 2,

        // 禁止无用的转义
        "no-useless-escape": 2,

        // 禁止无效的重命名,如 import {a as a} from xxx
        "no-useless-rename": 2,

        // 禁止没有必要的 return
        // @off 没有必要限制
        "no-useless-return": 0,

        // 禁止使用 var,必须用 let 或 const
        "no-var": 2,

        // 禁止使用void
        "no-void": 2,

        // 禁止注释中出现 TODO 或 FIXME,用这个来提醒开发者,写了 TODO 就一定要做完
        "no-warning-comments": 1,

        // 禁止属性前出现空格,如 foo. bar()
        "no-whitespace-before-property": 2,

        // 禁止 with
        "no-with": 2,

        // 禁止 if 语句在没有花括号的情况下换行
        "nonblock-statement-body-position": 2,

        // 定义对象的花括号前后是否要加空行
        // @off 不关心
        "object-curly-newline": 0,

        // 定义对象的花括号前后是否要加空格
        // @off 不关心
        "object-curly-spacing": 0,

        // 对象每个属性必须独占一行
        // @off 不限制
        "object-property-newline": 0,

        // obj = { a: a } 必须转换成 obj = { a }
        // @off 没必要
        "object-shorthand": 0,

        // 每个变量声明必须独占一行
        // @off 有 one-var 就不需要此规则了
        "one-var-declaration-per-line": 0,

        // 是否允许使用逗号一次声明多个变量
        "one-var": [2, {
            "const": "never" // 所有 const 声明必须独占一行,不允许用逗号定义多个
        }],

        // 必须使用 x = x + y 而不是 x += y
        // @off 没必要限制
        "operator-assignment": 0,

        // 断行时操作符位于行首还是行尾
        // @off 不关心
        "operator-linebreak": 0,

        // 代码块首尾必须要空行
        // @off 没必要限制
        "padded-blocks": 0,

        // 限制语句之间的空行规则,比如变量定义完之后必须要空行
        // @off 没必要限制
        "padding-line-between-statements": 0,

        // 必须使用箭头函数作为回调
        // @off 没必要
        "prefer-arrow-callback": 0,

        // 声明后不再修改的变量必须使用 const
        // @off 没必要
        "prefer-const": 0,

        // 必须使用解构
        // @off 没必要
        "prefer-destructuring": 0,

        // 必须使用 0b11111011 而不是 parseInt("111110111", 2)
        // @off 没必要
        "prefer-numeric-literals": 0,

        // promise 的 reject 中必须传入 Error 对象,而不允许使用字面量
        "prefer-promise-reject-errors": 2,

        // 必须使用解构 ...args 来代替 arguments
        "prefer-rest-params": 2,

        // 必须使用 func(...args) 来代替 func.apply(args)
        // @off 没必要
        "prefer-spread": 0,

        // 必须使用模板字符串来代替字符串拼接
        // @off 不限制
        "prefer-template": 0,

        // 字符串必须使用单引号
        "quotes": [2, "single", {
            "avoidEscape": true, // 允许包含单引号的字符串使用双引号
            "allowTemplateLiterals": true, // 允许使用模板字符串
        }],

        // 对象字面量的键名禁止用引号括起来
        // @off 没必要限制
        "quote-props": 0,

        // parseInt方法必须传进制参数
        "radix": 2,

        // async 函数中必须存在 await 语句
        // @off async function 中没有 await 的写法很常见,比如 koa 的示例中就有这种用法
        "require-await": 0,

        // 必须使用 jsdoc 风格的注释
        // @off 暂不考虑开启
        "require-jsdoc": 0,

        // generator 函数内必须有 yield
        "require-yield": 2,

        // ...后面不允许有空格
        "rest-spread-spacing": [2, "never"],

        // 分号前后的空格规则
        // @off 不限制
        "semi-spacing": 0,

        // 禁止行首出现分号
        "semi-style": [2, "last"],

        // 行尾必须使用分号结束
        "semi": 2,

        // imports 必须排好序
        // @off 没必要限制
        "sort-imports": 0,

        // 对象字面量的键名必须排好序
        // @off 没必要限制
        "sort-keys": 0,

        // 变量声明必须排好序
        // @off 没必要限制
        "sort-vars": 0,

        // function 等的花括号之前是否使用空格
        // @off 不关心
        "space-before-blocks": 0,

        // function 的圆括号之前是否使用空格
        // @off 不关心
        "space-before-function-paren": 0,

        // 圆括号内的空格使用规则
        // @off 不关心
        "space-in-parens": 0,

        // 操作符前后要加空格
        "space-infix-ops": 2,

        // new, delete, typeof, void, yield 等表达式前后必须有空格,-, +, --, ++, !, !! 等表达式前后不许有空格
        "space-unary-ops": [2, {
            "words": true,
            "nonwords": false,
        }],

        // 注释的斜线和星号后要加空格
        "spaced-comment": [2, "always", {
            "block": {
                exceptions: ["*"],
                balanced: true
            }
        }],

        // 禁用严格模式,禁止在任何地方出现 "use strict"
        "strict": [2, "never"],

        // switch 中冒号前后的空格规则
        // @off 不关心
        "switch-colon-spacing": 0,

        // 创建 Symbol 的时候必须传入描述
        "symbol-description": 2,

        // 模板字符串 ${} 前后的空格规则
        // @off 不限制
        "template-curly-spacing": 0,

        // 模板字符串前后的空格规则
        // @off 不限制
        "template-tag-spacing": 0,

        // 所有文件头禁止出现 BOM
        "unicode-bom": 2,

        // 禁止直接对 NaN 进行判断,必须使用 isNaN
        "use-isnan": 2,

        // 注释必须符合 jsdoc 的规范
        // @off 暂不考虑开启
        "valid-jsdoc": 0,

        // typeof 判断条件只能是 "undefined", "object", "boolean", "number", "string", "function" 或 "symbol"
        "valid-typeof": 2,

        // var 必须在作用域的最前面
        // @off var 不在最前面也是很常见的用法
        "vars-on-top": 0,

        // 自执行函数必须使用圆括号括起来,如 (function(){do something...})()
        "wrap-iife": [2, "inside"],

        // 正则表达式必须用圆括号括起来
        // @off 不限制
        "wrap-regex": 0,

        // yield 的 * 前后空格规则
        // @off 不限制
        "yield-star-spacing": 0,

        // 禁止Yoda格式的判断条件,如 if (true === a),应使用 if (a === true)
        "yoda": 2,
    }
};

Prettier的配置

Prettier是当今最流行的格式化程序之一,受到编码社区的广泛使用。它可以添加到ESLint,文本编辑器,也可以被挂载在gitpre-commit钩子上(或者huskylint-staged)。

接下来看看怎么在配置中添加Prettier的功能。为了和ESLint配合使用,需要安装prettiereslint-plugin-prettiereslint-config-prettier依赖关系。其中eslint-config-prettier是用来处理eslint的规则和prettier的规则发生冲突的时候(主要是不必要的冲突),如果eslint驱动prettier来做代码检查的话,就会提示两种报错,虽然他们都指向同一种代码错误,这个时候就会由这个插件来关闭掉额外的报错。

别的先不说,先在命令终端上执行下面的命令,安装Prettier所需要依赖包:

⇒  npm i prettier eslint-plugin-prettier eslint-config-prettier -D

要让Prettier生效,还需要创建相关的配置文件。对于Prettier有三种不同的配置方式:

  • 在项目根目录下创建.prettierrc文件,其支持.yaml.yml.json.js后缀名格式,一般使用.prettierrc.js
  • 在项目根目录下创建prettierrc.config.js文件,并对外export一个对象
  • package.json中添加一个prettier属性

下面我们使用.prettierrc.js的方式来配置Prettier:

module.exports = {
    "printWidth": 100,
    "tabWidth": 2,
    "parser": "typescript",
    "trailingComma": "es5",
    "jsxBracketSameLine": true,
    "semi": true,
    "singleQuote": true,
    "bracketSpacing": true,
    "arrowParens": 'avoid',
    "requirePragma": false,
    "proseWrap": 'preserve'
}

更详细的配置可以查阅官方文档

接着还需要在.eslintrc.js中添加Prettier相关的配置:

module.exports = {
    // 解析语法
    "parser": "@typescript-eslint/parser",
    // 指定脚本运行环境
    "env": {
        // ...
    },
    "extends": [
        "plugin:@typescript-eslint/recommended",
        "plugin:react/recommended",
        "plugin:jsx-control-statements/recommended",
        "prettier",
        "prettier/@typescript-eslint", 
        "plugin:prettier/recommended", 
        "prettier/react"
    ],
    "settings": {
        "react": {
            "version": "detect",
        }
    },
    "root": true,
    // 脚本在执行期间访问的额外的全局变量
    // 当访问当前源文件内未定义的变量时,no-undef规则将发出警告
    // 所以需要定义这些额外的全局变量
    "globals": {
        // ...
    },
    // 指定解析器选项
    "parserOptions": {
        // ...
    },
    // 指定需要的插件
    "plugins": [
        "@typescript-eslint", 
        "jsx-control-statements",
        "react",
        "prettier"
    ],
    // 启用的规则及其各自的错误级别
    "rules": {
        // ...
        
        "prettier/prettier": "error",

        // ...
    }
};

保存之后,在命令终端执行npm run dev之后,你会发现报错信息:

有的错信息不用太担心,我们可以一步一步来解决。就像ESLint最初的配置一样。

首先来解决配置问题:

{ parser: "babylon" } is deprecated; we now treat it as { parser: "babel" }.

针对上面这个问题,只需要将.prettierrc.js中的parser的值改成babel即可。这样剩下的就是代码规范约束上的问题了。继续往下,继续修改。

调整.eslintrc.jsrules的几个配置:

rules: {
    '@typescript-eslint/indent': ['error', 2, { VariableDeclarator: 2, SwitchCase: 1 }],
    'react/jsx-indent': [2, 2],
    'jsx-quotes': [2, 'prefer-double'],
}

再次执行的时候,不再报错。说明我们撸的代码和代码规范相关的约束匹配度没有差异。Prettier还可以在文本编辑器上添加相应的配置。比如VS Code中,开启Prettier插件,然后手动调整其配置文件:

然后在配置文件中settings.json中添加下面的配置信息:

{
    "workbench.startupEditor": "newUntitledFile",
    "editor.fontSize": 14,
    "editor.renderIndentGuides": false,
    "editor.formatOnPaste": false,
    "editor.formatOnType": false,
    "editor.fontLigatures": true,
    "workbench.fontAliasing": "antialiased",

    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
        "language": "vue",
        "autoFix": true
        },
        { "language": "typescript", "autoFix": true },
        { "language": "typescriptreact", "autoFix": true }
    ],
    "eslint.autoFixOnSave": true,
    "eslint.run": "onSave",
    "editor.formatOnSave": false,
    "window.zoomLevel": 1,
    "prettier.eslintIntegration": true,
    "vetur.format.defaultFormatter.js": "prettier-eslint",
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    // "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[jsonc]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "git.enableSmartCommit": true,
    "vetur.useWorkspaceDependencies": true,
    "typescript.updateImportsOnFileMove.enabled": "always"
}

这样一来,保存代码的时候,会根据ESLint中的配置,修改你的代码。比如我们在App.tsx中的代码做一些调整:

仔细看上面录屏效果。你也可以自己在自己的项目中体验一吧。是不是觉得突然清爽了。这样的配置估计也有有些同学不太喜欢,因为一不容易保存文件,你的代码就会有一些调整。虽然是按照你的ESLint的配置,但总觉得怪怪的。

StyleLint的配置

StyleLint是用来检测CSS代码的(包括Sass、SCSS、LESS和Stylus等处理器写的样式代码)。它和ESLint有点类似,可以结合Prettier让你的CSS代码更为规范统一。接下来,我们来看看怎么在工程中配置StyleLint。

首先我们需要安装几个依赖包:stylelintstylelint-config-standardstylelint-prettierstylelint-config-prettier。如果你不想结合Prettier一起使用的话,也只需要安装前面两个依赖关系即可。这里我将StyleLint与Prettier结合在一起使用。

在你的命令终端执行下面的命令,安装所需要的依赖包:

⇒  npm i stylelint stylelint-config-standard stylelint-prettier stylelint-config-prettier -D

其实还需要安装prettier包,只不过我们在配置ESLint的时候,已经安装了该依赖包,所以这里没有列入。

安装完上面的依赖关系之后,需要在项目根目录下创建一个放置StyleLint配置的文件,即**.stylelintrc.json**(也可以是stylelint.config.js.stylelintrc)。我这里创建了一个.stylelintrc文件:

{
    "plugins": ["stylelint-prettier"],
    "extends": [
        "stylelint-config-standard", 
        "stylelint-config-prettier"
    ],
    "rules": {
        "prettier/prettier": true
    },
}

然后在package.jsonscripts中加上stylelint的命令:

"scripts": {
    "eslint": "eslint --ext .js,.jsx,.tsx,.ts --fix ./src",
    "stylelint": "stylelint './src/**/*.{css,scss}'",
    "build": "webpack --config ./build/webpack.prod.js --mode production",
    "dev": "npm run eslint && npm run stylelint && webpack-dev-server --config ./build/webpack.dev.js --mode development --open",
    "test": "echo \"Error: no test specified\" && exit 1"
},

这个时候执行npm run devstylelint就会检测项目中的.css.scss文件。如果发现和配置不相匹配的话,就会报错,比如我们这个示例,执行完上面的命令有报错信息,如下所示:

和ESLint操作类似,针对不同的报错,找到相应的原因,一一解决:

先来解较为容易的:

Unexpected unknown property "composes"            property-no-unknown

因为我们的项目中使用了CSS Modules功能,在使用CSS Modules的时候会使用取composes等关键词。为了避免使用CSS Modules不报错,我们可以安装CSS Modules相关的插件:

⇒  npm i stylelint-config-css-modules -D

并在.stylelintrc配置文件中"extends"添加"stylelint-config-css-modules"。再次执行时剩下一个”Parsing error: Declaration or statement expected prettier/prettier“错。针对这个错误找了好久,并没有找到相关的解决方案。后来群里同学给了一个思路,是不是把CSS当JS来做了。后来发现.prettierrc.js的配置中,指定了parser的语言为typescript。在该配置文件中添加overrides选项对CSS文件做一个处理:

module.exports = {
    "printWidth": 100,
    "tabWidth": 2,
    "parser": "typescript",
    "trailingComma": "es5",
    "jsxBracketSameLine": true,
    "semi": true,
    "singleQuote": true,
    "bracketSpacing": true,
    "arrowParens": 'avoid',
    "requirePragma": false,
    "proseWrap": 'preserve',
    "overrides": [
        {
            "files": "*.{css,sass,scss,less}",
            "options": {
            "parser": "css",
            "tabWidth": 4
            }
        },
    ]
}

再次执行的时候。不会报错了。对于StyleLint的规则,大家还可以根据自己的习惯或者团队的习惯,添加一些约定规则进去,比如我这里添加了:

"rules": {
    "prettier/prettier": true,
    "color-no-invalid-hex": true, 
    "font-family-name-quotes": "always-where-recommended", 
    "function-url-quotes": "always", 
    "number-leading-zero": "never", 
    "number-no-trailing-zeros": true,
    "string-quotes": "double",
    "length-zero-no-unit": true,
    "value-keyword-case": "lower",
    "value-list-comma-newline-after": "always-multi-line",
    "shorthand-property-no-redundant-values": true,
    "property-case": "lower",
    "keyframe-declaration-no-important": true,
    "block-opening-brace-newline-after": "always-multi-line",
    "no-empty-source": null,
    "at-rule-no-unknown": null,
    "max-nesting-depth": 3,
    "no-duplicate-selectors": true, 
    "no-eol-whitespace": true, 
    "order/order": [
        ["custom-properties", "declarations"], {
            "disableFix": true
        }
    ],
}

有关于StyleLint中规则的说明可以点击这里查看

这个时候你可以来验证一下。比如在app.css写错一个或者写个不规范的,比如:

body {
    background: url("../../../../assets/images/body-background.jpg") no-repeat center,
        linear-gradient(to bottom, #f560a9, #09aefa, #2390af) no-repeat center;
    background-size: cover;
    background-blend-mode: overlay, screen;
    width: 100vw;
    min-height: 100vh;
    color: #fA9;
    fontsize: 26ppx;
    opacity: 0.9;
    margin: 1px 1px 1px;
}

在编辑器保存文件,Prettier就会根据StyleLint的配置做一些修改:

body {
    opacity: .9;
    margin: 1px;
    background: url("../../../../assets/images/body-background.jpg") no-repeat center,
        linear-gradient(to bottom, #f560a9, #09aefa, #2390af) no-repeat center;
    background-size: cover;
    background-blend-mode: overlay, screen;
    width: 100vw;
    min-height: 100vh;
    color: #fa9;
    fontsize: 26ppx;
}

命令终端同时会显示错误信息:

src/pages/index/components/App/app.css
16:5   ✖  Unexpected unknown property "fontsize"   property-no-unknown
16:15  ✖  Unexpected unknown unit "ppx"            unit-no-unknown

根据对应的信息,就可以找到地方去修正你的代码。

特别声明,保存文件会调整你的代码,这里会存有一定的风险在,所以在配置.stylelintrc中的规则是非常重要的

强制较验和格式化

另外我们还可以做一些强制较验的事情。前面也提到过了,可以使用huskylint-staged或者pre-commit之类的。这里我们主要来看如何借助huskylint-staged插件如何和Git的钩子绑定在一起。在提交代码的时候做一些强较验。

先安装这两个依赖关系:

⇒  npm i husky lint-staged -D

安装完之后,需要在pageage.json中配置相关信息:

{
    "name": "webpack-sample",
    "version": "1.0.0",
    "description": "Learning webpack step by step.",
    "private": true,
    "scripts": {
        "precommit": "lint-staged",
        "eslint": "eslint --ext .js,.jsx,.tsx,.ts --fix ./src",
        "stylelint": "stylelint './src/**/*.{css,scss}' --fix",
        "build": "webpack --config ./build/webpack.prod.js --mode production",
        "dev": "npm run eslint && npm run stylelint && webpack-dev-server --config ./build/webpack.dev.js --mode development --open",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    // ...
    "husky": {
        "hooks": {
            "pre-commit": "lint-staged"
        }
    },
    "lint-staged": {
        "./src/**/*.{js,jsx,ts,tsx}": [
            "eslint --fix",
            "git add"
        ],
        "./src/**/*.{css,scss,less}": [
            "stylelint --fix",
            "git add"
        ]
    }
}

不管是你的JavaScript代码还是CSS代码,如果代码不符合ESLint和StyleLint的约束,在使用Git提交代码的时候就会通不过。我们来验证一下,假设你在app.css中有一行样式代码出问题了:

body {
    
    fontsize: 26px;
}

如果你没有通过别的途径来修复这有错误的代码的话,在你使用Git来提交代码的时候就会出现类似下图的报错信息提供给你:

根据相应的提供去修复有问题的代码,然后再来提交代码,检测无误:

husky > pre-commit (node v10.9.0)
↓ Stashing changes... [skipped]
    → No partially staged files found...
✔ Running linters...

就会自动将代码提交到Github上。

到这一步,你的工程就具备了代码检测相关的能力。他可以帮助你更好的约束你的代码,让你的代码更规范。特别是在团队协作的时候,该配置更佳有效。

小结

上面分了15个步骤,介绍了Webpack中的一些基本配置。比如说项目初始化、Webpack初始配置、优化Webpack配置、配置React、TypeScript环境、Webpack的基本Loader、CSS Modules、代码检测等。这仅仅是Webpack配置中的一部分,在接下来的部分中,将会一起讨论Webpack开发环境、生产环境方面的优化。如果你感兴趣的,欢迎持续关注后续的相关更新。