0%

由零开始使用 Webpack 来搭建 React 项目

今天动动手回顾下 Webpack 基本配置,搭了个 react 项目脚手架,做下笔记。

本文适合有了解过 webpack 基础配置的人阅读,因为有些基础配置和流程我就不做详细解释了。

初始化

新建文件夹react-cli,初始化

1
npm init -y
  • 安装 webpack
1
yarn add webpack webpack-cli -D

我们知道 webpack 主要是帮我们打包构建,我们新建个文件src/index.js

1
console.log('hello webpack')
  • 新建 webpack.config.js
1
2
3
4
5
6
7
8
9
const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js',
},
}

再到package.json设置打包命令

1
2
3
"scripts": {
"build": "npx webpack --config webpack.config.js"
},

因为我没有全局安装 webpack,这里使用 npx(npm5.2 开始自带 npx 命令)
关于 npx 的使用可以看阮一峰大神这篇博客:npx 使用教程

  • 执行打包命令
1
npm run build

就会看到生成了dist文件夹和打包生成的文件app.js

引入 Html 模板

html-webpack-plugin简化了 HTML 文件的创建,该插件将为你生成一个 HTML5 文件, 其中包括使用 script 标签的 body 中的所有 webpack 包。

1
yarn add html-webpack-plugin -D

新建模板 HTML 文件public/index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root">Hello</div>
</body>
</html>

webpack.config.js配置

1
2
3
4
5
6
7
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
}

再执行npm run build,会看到生成了一个index.html文件,打开它会发现app.js也被引入到文件且能正常运行

编译 JS 文件

ES6 或以上版本的语法有些浏览器还是不识别语法的,所以我们需要用 babel 对它进行编译,编译成 ES5 代码
src/index.js写一个 ES6 的箭头函数

1
2
3
const func = () => {
return 100
}

安装 babel 插件

1
yarn add babel-loader @babel/core @babel/preset-env -D
  • babel-loader:使用 Babel 和 webpack 来转译 JavaScript 文件。
  • @babel/preset-env:将语法翻译成 es5 语法
  • @babel/core:babel 的核心模块

在根目录下新建.babelrc

1
2
3
{
"presets": ["@babel/preset-env"]
}

webpack.config.js中配置解析 js 的规则

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
}

执行npm run build,查看app.js已经把箭头函数编译成 ES5 语法了

1
2
3
4
5
6
7
/***/ ;(function (module, exports) {
eval(
"console.log('hello webpack');\n\nvar func = function func() {\n return 100;\n};\n\n//# sourceURL=webpack:///./src/index.js?"
)

/***/
})

处理浏览器没有的新特性

有些浏览器没有 Promise、Symbol、Map 等一些新特性,就需要使用 polyfill 来实现;

首先来理解下 babel 和 babel polyfill 的关系:

  1. babel 解决语法层面的问题,比如将 ES6+语法转为 ES5 语法。
  2. babel polyfill 解决API层面的问题,需要使用垫片,比如下面三种:
  • @babel/polyfill:通过向全局对象和内置对象的 prototype 上添加方法来实现的,从而解决了低版本浏览器无法实现的一些 es6+语法;但污染了全局空间和内置对象也是个缺点。

  • @babel/runtime:不会污染全局空间和内置对象原型,但如果使用的特性非常多,就会导致这种按需引入非常繁琐。由于它是 polyfill 的包,适合在组件,类库中使用,所以需要添加到生产环境中。

  • @babel/plugin-transform-runtime:将 helper 和 polyfill 都改为从一个统一的地方引入,解决了全局空间和内置对象的问题;需要用到两个依赖包 @babel/runtime@babel/runtime-corejs3——加载 ES6+ 新的 API(corejs 根据你的版本)

  • babel-runtime 适合在组件,类库项目中使用,而 babel-polyfill 适合在业务项目中使用。

polyfill 用到的 core-js 是可以指定版本的,比如使用 core-js@3 core-js@2

1
2
3
yarn add @babel/polyfill core-js@3
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime @babel/runtime-corejs3

安装完毕后,在 .babelrc进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage", // 按需引入,它能自动给每个文件添加其需要的polyfill
"corejs": 3 // 根据你的版本来写
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"helpers": true, // 开启内联的babel helpers(即babel或者环境本来的存在的垫片或者某些对象方法函数)
"regenerator": true, // 开启generator函数转换成使用regenerator runtime来避免污染全局域
"useESModules": false
}
]
]
}

编译 React

安装 react 有关包和编译 React 的 babel 插件

1
2
yarn add react react-dom
yarn add @babel/preset-react -D

重写src/index.js

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'

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

新建文件src/App.jsx

1
2
3
4
5
import React from 'react'

const App = () => <div>App</div>

export default App

修改.babelrc文件(presets 的顺序从右向左)

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage", // 按需引入,它能自动给每个文件添加其需要的polyfill
"corejs": 3 // 根据你的版本来写
}
],
"@babel/preset-react"
],
}

执行打包命令,成功编译

devServer 配置

安装 devServer,在开发中实时检测文件的变化

1
yarn add webpack-dev-server -D

在``package.json`中设置启动命令

1
"dev": "webpack-dev-server",

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
devServer: {
publicPath: '/',
host: '0.0.0.0',
compress: true, // 使用gzip压缩
port: 8080, // 监听的端口号
historyApiFallback: true,
overlay: true, // 代码出错弹出浮动层
hot: true, // 开启热更新
},
}

开启服务器yarn dev,打开http://localhost:8080,就可以 React 组件正常编译运行了。

开发环境热更新

1
yarn add -D react-hot-loader

webpack.config.js配置,devServer 上面已经开启hot:true

1
2
3
plugins: [
new webpack.HotModuleReplacementPlugin(),
],

src/index.js中添加

1
2
3
4
5
6
7
8
9
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

if (module && module.hot) {
module.hot.accept()
}

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

重新运行yarn dev,热更新生效

省略文件后缀和设置别名

上面引入 App 组件我们是import App from './App.jsx'加了后缀,如果省略的话会报错;我们可以在 webpack 配置文件的resolve设置可以省略后缀;

还有一个问题,如果项目较大,组件层级关系多的时候,我们可以给路径设置别名

新建src/components/Header/index.js

1
2
3
4
5
import React from 'react'

const Header = () => <div>Header</div>

export default Header

在 webpack.config.js 进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js',
},
resolve: {
extensions: ['.jsx', '.js', '.json'],
alias: {
'@components': path.resolve(__dirname, 'src/components'),
},
},

修改App.jsx

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import Header from '@components/Header'

const App = () => (
<div>
<Header />
</div>
)

export default App

加载 Css

1
yarn add style-loader css-loader -D

给 Header 组件添加样式,新建文件src/components/Header/index.css,再到 Header 组件中引入import './index.css'

1
2
3
div {
background-color: lightgreen;
}
  • css-loader 的作用是处理 css 文件中 @import,url 之类的语句(将 CSS 转化成 CommonJS 模块)
  • style-loader 则是将 css 文件内容放在 style 标签内并插入 head 中

webpack.config.js配置(loader 执行顺序从右到左)

1
2
3
4
5
6
7
8
9
 module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
}

支持 Sass

1
yarn add sass sass-loader -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module: {
rules: [
{
test: /\.(sass|scss)$/,
use: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
implementation: require('sass'), // 使用dart-sass代替node-sass
},
},
],
},
],
},

src/components/Header/index.css改为.scss文件,Header 组件index.js的引入也修改一下引入 css 文件名,然后执行打包命令,OK。

less 就不写了,毕竟一个项目选一种就行了。

集成 Postcss

postcss-loader 作用:

  • 把 css 解析为一个抽象语法树
  • 调用插件处理抽象语法树并添加功能,如处理 CSS 兼容添加一些新特性的厂商前缀
1
yarn add postcss-loader autoprefixer -D

在根目录下新建postcss.config.js

1
2
3
4
5
6
7
8
module.exports = {
plugins: [
require('autoprefixer')({
// 自动在样式中添加浏览器厂商前缀
overrideBrowserslist: ['last 2 versions', '>1%'],
}),
],
}

webpack.config.js中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.(sass|scss)$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
implementation: require('sass'), // 使用dart-sass代替node-sass
},
},
],
},
]
}

在样式文件添加 CSS3 一些新特性,就能看到被自动添加了兼容处理了。

加载图片

1
yarn add file-loader url-loader -D

加入一张图片src/components/Header/img/fe-house-qrcode.jpg,在src/components/Header/index.js中引入

1
import './img/fe-house-qrcode.jpg'

webpack.config.js中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/', // 打包图片到dist目录下的images文件夹
limit: 102400, // 图片超过100KB就使用file-loader打包,否则图片以base64格式存在
},
},
},
]
}

解析字体图标

这里就不演示了,直接上配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module: {
rules: [
{
test: /\.(eot|ttf|svg|woff|woff2)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'fonts/',
},
},
},
]
}

区分生产环境和开发环境

到这里,最基本配置差不多可以用了,接下来需要做一些细节或优化的配置。

安装webpack-merge整合

1
yarn add webpack-merge -D

提炼出三个文件:

  • webpack.common.js 公共配置
  • webpack.dev.js 开发环境配置
  • webpack.prod.js 生产环境配置

webpack.config.js的东西提炼出来分三个文件

  • webpack.common.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js',
},
resolve: {
extensions: ['.jsx', '.js', '.json'],
alias: {
'@components': path.resolve(__dirname, 'src/components'),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.(sass|scss)$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'sass-loader',
options: {
implementation: require('sass'), // 使用dart-sass代替node-sass
},
},
],
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/', // 打包图片到dist目录下的images文件夹
limit: 102400, // 图片超过100KB就使用file-loader打包,否则图片以base64格式存在
},
},
},
{
test: /\.(eot|ttf|svg|woff|woff2)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'fonts/',
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
devServer: {
publicPath: '/',
host: '0.0.0.0',
compress: true, // 使用gzip压缩
port: 8080, // 监听的端口号
historyApiFallback: true,
overlay: true, // 代码出错弹出浮动层
hot: true, // 开启热更新
},
}
  • webpack.dev.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map', // 帮助我们定位到错误信息位置的源代码文件
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
publicPath: '/',
host: '0.0.0.0',
compress: true, // 使用gzip压缩
port: 8080, // 监听的端口号
historyApiFallback: true,
overlay: true, // 代码出错弹出浮动层
hot: true, // 开启热更新
},
}

module.exports = merge(commonConfig, devConfig)
  • webpack.prod.js
1
2
3
4
5
6
7
8
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const devConfig = {
mode: 'production',
devtool: 'none',
}

module.exports = merge(commonConfig, devConfig)

修改package.json

1
2
3
4
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "npx webpack --config webpack.prod.js"
},

大功告成。

打包前清理 dist 目录

在你调试打包的过程可能会多出一些遗留文件,我们想要最新打包编译的文件,就需要先清除 dist 目录

1
yarn add clean-webpack-plugin -D

加到webpack.common.js文件

1
2
3
4
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [new CleanWebpackPlugin()],
}

项目规范

作为项目代码风格的统一规范,我们需要借助第三方工具来强制,让团队成员的代码风格更趋于一致。

EditorConfig

.editorconfig 是跨编辑器维护一致编码风格的配置文件,在 VSCode 中需要安装相应插件EditorConfig for VS Code,安装完毕之后,
使用快捷键ctrl+shift+p打开命令台,输入 Generate .editorcofig 即可快速生成 .editorconfig 文件。

.editorcofig文件,就可以大家根据不同来设置文件了,比如我的是这样:

1
2
3
4
5
6
7
8
9
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

Prettier

1
yarn add prettier -D

同样也需要安装 VSCode 插件Prettier - Code formatter

安装完成后在根目录下新建文件夹.vscode,在该文件夹下新建文件settings.json,该文件的配置优先于你自己 VSCode 全局的配置,不会因为团队不同成员的 VSCode 全局配置不同而导致格式化不同。

settings.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
// 指定哪些文件不参与搜索
"search.exclude": {
"**/node_modules": true,
"dist": true,
"yarn.lock": true
},
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "javascriptreact"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

安装完后新建文件.prettierrc

1
2
3
4
5
6
7
8
9
10
{
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"endOfLine": "lf",
"printWidth": 120,
"bracketSpacing": true,
"arrowParens": "avoid"
}

ESLint

1
yarn add eslint -D

安装完后运行npx eslint --init,运行后有选项,选择如下(自行根据需要):

  • To check syntax, find problems, and enforce code style
  • JavaScript modules (import/export)
  • React
  • No(这里我们没使用 TS 就不必了)
  • Browser Node
  • Use a popular style guide
  • Airbnb: https://github.com/airbnb/javascript
  • JavaScript
  • Would you like to install them now with npm(Yes)直接安装

完毕后会发现已经自动安装了 ESLint 相关的包,也自动生成了.eslintrc.js文件,里面的rules默认是空,这个就需要团队自己的额外配置了。注:VSCode 需要安装ESLint插件

新建文件.eslintignore,忽略一些文件的检查

1
2
3
/node_modules
/public
/dist

StyleLint

上面是针对规范 JS 代码的,接下来规范 CSS 代码,同样需要安装 VSCode 插件stylelint

1
yarn add stylelint stylelint-config-standard -D

新建文件.stylelintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
extends: ['stylelint-config-standard'],
rules: {
'comment-empty-line-before': null,
'declaration-empty-line-before': null,
'function-name-case': 'lower',
'no-descending-specificity': null,
'no-invalid-double-slash-comments': null,
'rule-empty-line-before': 'always',
},
ignoreFiles: ['node_modules/**/*', 'dist/**/*'],
}

Prettier、ESLint、Stylelint 冲突解决

上面一顿操作后,可能发现 ESLint 给文件飘红,但保存后格式变了又好像恢复原样,这个就是冲突问题,一般解决就是利用插件禁用与 Prettier 起冲突的规则。

1
yarn add eslint-config-prettier -D

修改.eslintrc.js

1
2
3
4
5
6
7
module.exports = {
extends: [
//...
'prettier',
'prettier/react',
],
}

如果代码还因为 ESLint 飘红,可以看情况添加忽略的规则。

1
yarn add stylelint-config-prettier -D

修改.stylelintrc.js文件中增加一个 extend

1
extends: ['stylelint-config-prettier'],

结语

写到这里就停了,当然要继续往下做的话还有很多,比如用 husky 代码提交,还有很多像 webpack 压缩分离 JS 和 CSS 等一些,这些留到以后写一篇 webpack 性能优化再来一起总结。

react-cli Github 地址


-------------本文结束感谢您的阅读-------------