学一点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
模式具有development
,production
和 none
三个值,其默认值是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
配置了打包文件输出的目的地和方式。你可能也发现了,在配置文件中还有module
、plugins
和devServer
没有添加任何东西。不需要太急,后面会一步一步带着大家把这里的内容补全的,而且随着配置的东西越来越多,整个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.json
的scripts
中添加相关的命令:
// 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.js
、webpack.dev.js
和webpack.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: [
],
})
上面的配置信息只是将
Step02
中webpack.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的环境需要先安装react
和react-dom
。所以先在命令终端中执行下面的命令:
⇒ npm i react react-dom --save
执行完上面的命令之后,在package.json
文件中的dependencies
增加了ract
和react-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
文件在dependencies
和devDependencies
添加了新的配置信息:
// 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的两个插件:
- 生成
index.html
文件,并且自动插入到/dist
目录下:html-webpack-plugin
- 能够将元素
id
附加到mount
:html-webpack-template
在命令终端上执行下面的命令,安装这两个插件:
⇒ npm i html-webpack-plugin html-webpack-template -D
安装完成之后,在package.json
的devDependencies
会添加相应的信息:
// 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的时候,组件Button
和App
中就添加了对应的CSS文件button.css
和app.css
,只不过注释掉了引用的CSS文件,没让它们生效。我们可以先开启已被注释的CSS,重新运行项目,会发现有报错信息:
这是因为在Webpack中没有CSS相关的Loader配置。那么接下来,来解决CSS Loader在Webpack中的配置:
css-loader
使你能够使用类似@import
和url()
的方法实现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-env
、Autoprefixer
等。另外自己还可以根据自己需要扩展自己想要的PostCSS插件。而且在一些移动端的布局中的方案都会需要处理CSS单位之间的转换,比如Flexible(px2rem
),vw-layout
(px2vw
)等。
可以说,现代Web开发中PostCSS相关的配置是工程体系中必不可少的。接下来,我们就看看如何在该工程体系中添加PostCSS相关的配置。要配置PostCSS相关的事项,需要:
- 安装
postcss
、postcss-loader
- 在Webpack配置中添加PostCSS相关的配置
- 安装自己需要的PostCSS插件
- 在
postcss.config.js
或.postcssrc.js
添加有关于PostCSS的插件配置
先来执行第一步,安装postcss
和postcss-loader
:
⇒ npm i postcss postcss-loader -D
执行完上面命令之后,package.json
的devDependencies
会添加postcss
和postcss-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.js
中module.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-env
和postcss-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-sass
和sass-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"
}
]
]
}
特别提出一点,.babelrc
中babel-plugin-react-css-modules
的generateScopedName
必须要和webpack.common.js
中css-loader
的modules.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-modules
和babel-plugin-react-css-modules
解决的。
个人建议:如果的工程中使用了CSS Modules的话,那么就没有必要再使用CSS处理器(如Sass,LESS)。
说真的,在Typescript、React和Webpack中实现CSS Modules的功能,还真费了不少的劲,也查也很多的资料。在这里,希望这份配置对你构建相应的开发环境有所帮助。
Step11:图片加载
请把分支切换到
step11
查看代码。
在HTML和CSS中我们都会有用到图片的时候。接下来看看Webpack中怎么配置图片加载。在Webpack配置图片资源的加载需要依赖file-loader
和url-loader
两个包:
file-loader
:解决CSS等文件中引入图片路径问题。当遇到图片文件时会将其打包移动到/dist
目录下,接下来会获得图片模块的地址,并将地址返回到引入模块到变量之中url-loader
:当图片较小的时候会把图片转换成base64
编码,大于limit
参数的时候还是使用file-loader
进行拷贝。这样做是好处是可以直接将图片打包到bundle.js
里,不用额外请求图片(省去HTTP
请求);其坏处是遇到文件较大时,加载会很耗时,影响用户体验
在命令终端执行下面的命令,安装file-loader
和url-loader
:
⇒ npm i file-loader url-loader -D
为了更好的测试接下来的配置,先在/src
目录下创建一个新的目录/assets
,并在该目录下创建一个放置图片的文件夹/images
。同时把一张大图(security.svg
)和一张小图(copy-solid.svg
)放到images
中:
然后在webpack.common.js
的module.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.json
在compilerOptions
增加"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的img
的src
中引入图片资源:
<img src="../../../../assets/images/security.svg" alt=""/>
也会报404的错误信息。
网上找了一通,并未找到较好的解决方案,这也算是一个预留的坑吧。
上面看到的是开发环境下的效果。接下来验证一下打包后的结果。在命令终端执行npm run build
,可以看到在/dist
目录下将会新增/assets/images
目录,并且未能编译成base64
的图片都将放置在该目录下:
我们来验证一下打包出来的文件,在图片资源的引用上是否正确:
和开发环境下运行的结果是一样的。这样表示我们图片的加载已配置成功(注意,在.tsx
中src
直接引入相对路径存在图片资源加载404
错误,并未得到配置化方案的解决!)
Step12:字体文件加载
请把分支切换到
step12
查看代码。
有些项目中会使用字体图标,如果字体图标使用的不是第三方字库或CDN
的地址,就需要将字体文件放到本地项目中。比如放在/assets
目录下的/fonts
目录中。这样就需要面临字体文件加载相关的配置。
在Web技巧系列的第12期聊了一些关于字体加载相关的话题,感兴趣的同学可以移步看看。
在Webpack构建的工程体系中,字体文件加载的策略和图片资源加载的策略是相似的,需要依赖file-loader
和url-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
查看代码。
每个人都有着自己的编码习惯,这种习惯并没有什么好坏之分,但是在一个团队协作中,如果每个人都有着自己与众不同的编码习惯,那这将可能是一个致命的问题。如果你是在一个协同作战的团队或项目中,个人还是建议你按照一定的编码风格去编码,哪怕是你个人平时编码,也建议你按照一些语言规范去编码。不管是哪种语言,都有着其自己较好的编码风格。或者通过一些工具、配置来约束代码风格,当然,这可能在最开始的时候会令你很烦感,但慢慢地你会喜欢上的,因为这样一来,你的代码质量会更高,也有一个相应的保障。
在项目工程中,我们就可以借助 ESLint、Prettier、StyleLint、Husky、lint-staged 或 pre-commit等来帮助我们约束自己的代码。
这些配置都挺令人感到头疼的,但一步一步往下撸,会让你的恐惧和惧怕变得越来越小。(^_^)
ESLint的配置
ESLint是什么就不做过多的阐述,只要知道它能够帮助我们发现代码中常见的问题,并能与项目的生态系统其他部分集成。我们接下来会花一些时间来看看怎么在Webpack中配置ESLint的相关能力。
先在命令终端执行:
⇒ npm i eslint -D
针对于我们教程中使用的环境React、Typscript等,ESLint和后续要聊到的相关规范代码的工具,还需要安装一些其他的依赖关系包,比如
eslint-config-prettier
、eslint-plugin-prettier
、eslint-plugin-react
、eslint-plugin-jsx-control-statements
、babel-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-loader
和babel-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.js
中esling-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
的配置项都可以根据自己的喜好进行增减和修改,每个配置项的可选值一般有三个:off
、warn
和error
。下面的列表是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,文本编辑器,也可以被挂载在git
的pre-commit
钩子上(或者husky
和lint-staged
)。
接下来看看怎么在配置中添加Prettier的功能。为了和ESLint配合使用,需要安装prettier
、eslint-plugin-prettier
和eslint-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.js
中rules
的几个配置:
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。
首先我们需要安装几个依赖包:stylelint
、stylelint-config-standard
、stylelint-prettier
和stylelint-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.json
的scripts
中加上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 dev
,stylelint
就会检测项目中的.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
中的规则是非常重要的。
强制较验和格式化
另外我们还可以做一些强制较验的事情。前面也提到过了,可以使用husky
、lint-staged
或者pre-commit
之类的。这里我们主要来看如何借助husky
和lint-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开发环境、生产环境方面的优化。如果你感兴趣的,欢迎持续关注后续的相关更新。