简介
上一节我们介绍了 React
官方提供的脚手架(create-react-app),然后用官方脚手架创建了一个 react-demo1
项目,用脚手架创建出来的项目算是 React
官方认为的最佳项目实践了,但是从上一节使用中我们可以看出,官方脚手架可还是有一些弊端,它只提供一些可改的 webpack
配置,如果你想自己添加一个 loader
、plugin
什么的,你可能就需自定义 webpack
配置了,就不能在依赖脚手架了,既然是重新学习 React
,我们就来挑战一下自己,从 0
开始搭建一个企业级的 React
项目,大家跟紧节奏吧。
知识点
- Webpack (打包编译)
- TypeScript(语法规范)
- React(js 框架)
- html-webpack-plugin(webpack 插件)
- Sass(css 预处理框架)
- PostCss(css 预处理框架)
- mini-css-extract-plugin(提取 css 模块插件)
- Babel( es 语法转化)
- Jsx & Tsx(jsx 语法)
- ESLint(代码质量校验)
- Optimization(项目优化)
ok!要搭建一个企业级的项目需要准备的东西还是有点多的,我们分一下类:
- Js 框架(Vue、TypeScript、Tsx、Jsx)
- Css 样式(Sass)
- 工程化工具(Eslint、Babel、webpack、webpack-chain)
我们开始吧。
项目初始化
我们创建一个 cus-react-demo
目录,然后在 cus-react-demo
目录下执行 npx init -y
命令:
mkdir cus-react-demo && cd cus-react-demo && npm init -y
如上图所示,初始化完毕后会看到一个 package.json
文件。
webpack
接下来我们需要安装 webpack 相关的依赖:
安装 webpack
webpack 核心库。
在工程目录 cus-react-demo
执行以下命令安装 webpack:
npm install -D webpack --registry https://registry.npm.taobao.org
目前 webpack
的版本是 5.26.3
。
安装 webpack-cli
webpack 指令库。
在工程目录 cus-react-demo
执行以下命令:
npm install -D webpack-cli --registry https://registry.npm.taobao.org
安装 webpack-dev-server
webpack 开发者服务框架。
在工程目录 cus-react-demo
执行以下命令:
npm install -D webpack-dev-server --registry https://registry.npm.taobao.org
安装 webpack-chain
webpack 配置工具。
在工程目录 cus-react-demo
执行以下命令:
npm install -D webpack-chain --registry https://registry.npm.taobao.org
创建 webpack 配置
在工程目录 cus-react-demo
下创建一个 webpack.config.js
文件:
touch webpack.config.js
然后对 webpack.config.js
进行配置,用 webpack-chain
导入一个 webpack 配置:
const config = new (require('webpack-chain'))();
module.exports = config.toConfig();
安装 cross-env
定义 node
中的 process.env
中的变量。
在工程目录 cus-react-demo
执行以下命令:
npm install -D cross-env --registry https://registry.npm.taobao.org
为了开发方便,我们在 package.json
中声明两个脚本 build
跟 dev
:
{
"name": "cus-react-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rimraf dist && cross-env NODE_ENV=production webpack --mode=production",
"start": "cross-env NODE_ENV=development webpack serve --mode=development --progress"
},
"author": "",
"license": "ISC",
"devDependencies": {
"cross-env": "^7.0.3",
"webpack": "^5.26.3",
"webpack-chain": "^6.5.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
}
}
我们可以试着运行一下 build
跟 start
脚本命令:
npm start
现在运行肯定是报错的,因为我们还没有对项目进行任何配置。
入口与出口
我们首先在工程目录 cus-react-demo
下创建一个 src
目录,然后在 src
目录下创建一个 main.tsx
文件:
mkdir src && cd src && touch main.tsx && cd ..
然后我们找到 webpack.config.js
文件,对 webpack 的入口和出口进行配置:
const path = require('path');
const config = new (require('webpack-chain'))();
const isDev = process.env.NODE_ENV === 'development'; // 判断是否是开发环境
config
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.tsx') // 入口文件为 ./src/main.tsx
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename(isDev ? '[name].[hash:8].js' : '[name].[contenthash:8].js') // 打包出来的 bundle 名称为 "[name].[contenthash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end();
module.exports = config.toConfig();
可以看到,我们配置了一个 app
入口 ./src/main.ts
,然后给所有的输出文件名称改成了 [name].[hash:8].js
。
我们再次执行 build
命令:
npm run build
可以看到,这次没有报错了。
但是当我们给 src/main.tsx
添加点内容试试:
let a: string = 'hello';
我们再次运行 npm run build
命令:
可以看到,又报错了,这次是因为 webpack 没法去解析 main.tsx
中的 ts
语法,所以接下来我们就需要配置一下 webpack
的 loader
了,让我们的项目能够支持 jsx
、ts
、tsx
语法。
Babel & TypeScript
因为我们项目是需要支持 ts 语法的,所以我们需要安装一下 TypeScript 相关依赖:
babel-preset-react-app
很幸运哈,React
官方已经提供了一套 babel
插件集合,它会自动的帮我们添加 babel
相关的配置,让我们能够轻松的使用 jsx
、ts
、tsx
语法,而且不需要考虑 es
语法的兼容性问题,会根据我们的浏览器配置自动做语法兼容等操作。
在工程目录 cus-react-demo
执行以下命令:
npm install -D babel-preset-react-app --registry https://registry.npm.taobao.org
babel-loader
webpack 加载器。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D babel-loader --registry https://registry.npm.taobao.org
ok!安装完 Babel 的一些依赖后,我们开始配置 webpack。
找到 webpack.config.js
文件,然后添加 babel-loader
配置:
const path = require('path');
const config = new (require('webpack-chain'))();
const isDev = process.env.NODE_ENV === 'development'; // 判断是否是开发环境
config
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.tsx') // 入口文件为 ./src/main.tsx
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename('[name].[contenthash:8].js') // 打包出来的 bundle 名称为 "[name].[contenthash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
.resolve
.extensions.add('.js').add('.jsx').add('.ts').add('.tsx').end() // 添加后缀自动解析
.end()
.module
.rule('ts') // 配置 typescript
.test(/\.(js|mjs|jsx|ts|tsx)$/)
.exclude
.add(filepath => {
// Don't transpile node_modules
return /node_modules/.test(filepath)
})
.end()
.use('babel-loader')
.loader('babel-loader')
.end()
.end()
module.exports = config.toConfig();
然后我们需要在工程目录 cus-react-demo
下创建一个文件 babel.config.js
:
touch babel.config.js
然后写入以下代码到 babel.config.js
文件:
module.exports = {
presets: [
[
"babel-preset-react-app", // 添加 react-app 插件集合
],
]
}
TypeScript 配置
因为我们需要进行 ts 语法的我们还需要在工程目录 cus-react-demo
下创建一个 ts
配置文件 tsconfig.json
:
touch tsconfig.json
然后写入以下代码到 tsconfig.json
文件:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"suppressImplicitAnyIndexErrors": true,
"resolveJsonModule": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable"]
},
"include": ["src/**/*.ts", "src/**/*.tsx",],
"exclude": ["node_modules"]
}
ok!我们再次运行 npm run build
命令:
可以看到,这一次 webpack 正常完成了编译打包。
React 配置
我们首先安装一下 React
所需要的依赖:
安装 react & react-dom
React 核心 API。
在工程目录 cus-react-demo
执行以下命令:
npm install react react-dom @types/react @types/react-dom --registry https://registry.npm.taobao.org
然后我们修改一下 src/main.tsx
文件来测试一下:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<div>hello react</div>,
document.getElementById('root')
);
很简单,就是利用 tsx
语法在页面中输出了一句 “hello react” 字符串。
如果需要开启 webpack-dev-server
的话,我们还需要配置一下 devServer
。
我们找到 webpack.config.js
配置文件,然后添加 devServer
配置:
const path = require('path');
const config = new (require('webpack-chain'))();
const isDev = process.env.NODE_ENV === 'development'; // 判断是否是开发环境
config
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.tsx') // 入口文件为 ./src/main.tsx
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename(isDev ? '[name].[hash:8].js' : '[name].[contenthash:8].js') // 打包出来的 bundle 名称为 "[name].[contenthash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
.resolve
.extensions.add('.js').add('.jsx').add('.ts').add('.tsx').end() // 添加后缀自动解析
.end()
.module
.rule('ts') // 配置 typescript
.test(/\.(js|mjs|jsx|ts|tsx)$/)
.exclude
.add(filepath => {
// Don't transpile node_modules
return /node_modules/.test(filepath)
})
.end()
.use('babel-loader')
.loader('babel-loader')
.end()
.end()
.end()
.devServer
.host('0.0.0.0') // 服务器外部可访问
.disableHostCheck(true) // 关闭白名单校验
.contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
.historyApiFallback({
disableDotRule: true, // 禁止在链接中使用 "." 符号
rewrites: [
{ from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
],
})
.port(8080) // 当前端口号
.hot(true) // 打开页面热载功能
.sockPort('location'); // 设置成平台自己的端口
module.exports = config.toConfig();
ok!然后我们在工程目录 cus-react-demo
下执行 npm start
开启 webpack-dev-server
:
npm start
执行完毕后,我们可以直接用浏览器访问一下打包过后的目录(http://127.0.0.1:8080/webpack-dev-server):
可以看到,webpack-dev-server
列出了 webpack 打包出来的所有 js 资源文件。
接下来我们在工程目录 cus-react-demo
底下创建一个 public
目录,然后在 public
目录下创建一个 index.html
文件作为我们 app 的入口页面:
mkdir public && touch public/index.html
然后写入以下代码到 public/index.html
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<noscript>your browser should support javascript!</noscript>
<!-- 挂载点 -->
<div id="root"></div>
<!-- 引入入口文件 -->
<!-- 注意!这里的 64a558f4 每个人是不一样的 -->
<script src="/app.64a558f4.js"></script>
</body>
</html>
(注意!这里的 64a558f4
每个人是不一样的)创建完毕后,我们重启服务:
npm start
然后我们可以直接用浏览器访问 http://127.0.0.1:8080/ 项目入口了:
可以看到,页面渲染了我们在 src/index.js
入口文件中定义的 “hello react” 字符串。
html-webpack-plugin
虽然浏览器中正常显示了我们的结果,但是我们现在还是手动去引入 app.xxx.js
文件的,而 xxx
是经常变化的,所以我们不可能每次都去改 public/index.html
文件,能不能自动去引入 js 资源文件到入口 index.html
呢?
安装 html-webpack-plugin
在工程目录 cus-react-demo
执行以下命令:
npm install -D html-webpack-plugin --registry https://registry.npm.taobao.org
然后修改一下 webpack.config.js
配置文件,引入 html-webpack-plugin
插件:
const path = require('path');
const config = new (require('webpack-chain'))();
const isDev = process.env.NODE_ENV === 'development'; // 判断是否是开发环境
config
.target('web')
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.tsx') // 入口文件为 ./src/main.tsx
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename(isDev ? '[name].[hash:8].js' : '[name].[contenthash:8].js') // 打包出来的 bundle 名称为 "[name].[contenthash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
.resolve
.extensions.add('.js').add('.jsx').add('.ts').add('.tsx').end() // 添加后缀自动解析
.end()
.module
.rule('ts') // 配置 typescript
.test(/\.(js|mjs|jsx|ts|tsx)$/)
.exclude
.add(filepath => {
// Don't transpile node_modules
return /node_modules/.test(filepath)
})
.end()
.use('babel-loader')
.loader('babel-loader')
.end()
.end()
.end()
.plugin('html') // 添加 html-webpack-plugin 插件
.use(require('html-webpack-plugin'), [
{
template: path.resolve(__dirname, './public/index.html'), // 指定模版文件
chunks: ['app'], // 指定需要加载的 chunk
inject: 'body', // 指定 script 脚本注入的位置为 body
},
])
.end()
.devServer
.host('0.0.0.0') // 服务器外部可访问
.disableHostCheck(true) // 关闭白名单校验
.contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
.historyApiFallback({
disableDotRule: true, // 禁止在链接中使用 "." 符号
rewrites: [
{ from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
],
})
.port(8080) // 当前端口号
.hot(true) // 打开页面热载功能
.sockPort('location'); // 设置成平台自己的端口
module.exports = config.toConfig();
ok!配置完毕后,我们就可以去掉 public/index.html
中的入口 js 文件了,让其自动引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<noscript>your browser should support javascript!</noscript>
<!-- 挂载点 -->
<div id="root"></div>
<!-- html-webpack-plugin 将自动引入入口文件 -->
</body>
</html>
我们再次执行 npm start
命令启动项目:
npm start
效果跟之前一致,我就不演示了。
css 样式(Sass)
安装 Sass
Sass & Scss 语法核心 API。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D sass --registry https://registry.npm.taobao.org
安装 sass-loader
Sass & Scss 文件加载器。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D sass-loader --registry https://registry.npm.taobao.org
安装 postcss-loader
css 样式处理工具,自动添加浏览器适配前缀、压缩 css 样式等等。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D postcss-loader --registry https://registry.npm.taobao.org
然后我们在工程目录 sy_webpack-wedding
下创建一个 postcss.config.js
作为 postcss
的配置文件:
touch postcss.config.js
写入以下代码到 postcss.config.js
文件:
module.exports = {
plugins: [
require('autoprefixer')(), // 添加 autoprefixer 插件做自动添加样式前缀
require('cssnano')({
// 添加 cssnano 插件做 css 代码压缩
preset: [
'default',
{
mergeLonghand: false,
cssDeclarationSorter: false,
},
],
}),
],
};
安装 cssnano
压缩 css 代码。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D cssnano --registry https://registry.npm.taobao.org
安装 autoprefixer
自动添加浏览器样式适配前缀。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D autoprefixer@^9.8.5 --registry https://registry.npm.taobao.org
要让 autoprefixer
插件起作用,我们还需要配置一个项目所支持的浏览器列表文件 .browserslistrc
,于是我们在工程目录 sy_webpack-wedding
下创建一个 .browserslistrc
文件:
touch .browserslistrc
然后写入以下内容到 .browserslistrc
文件:
> 0.25%, not dead
意思就是告诉 autoprefixer
插件,我们项目需要兼容的浏览器是 “大于市面上 0.25% 使用率的浏览器,请根据我配置的浏览器列表自动判读是否应该添加 css 前缀做兼容”。
安装 css-loader
css 模块加载器。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D css-loader --registry https://registry.npm.taobao.org
安装 mini-css-extract-plugin
抽离 css 样式到独立的 .css 文件。
在工程目录 sy_webpack-wedding
执行以下命令:
npm install -D mini-css-extract-plugin --registry https://registry.npm.taobao.org
ok!安装完 Sass
相关的依赖后,我们来修改一下 webpack.config.js
文件,让 webpack 支持 Sass
:
const path = require('path');
const config = new (require('webpack-chain'))();
const isDev = process.env.NODE_ENV === 'development'; // 判断是否是开发环境
config
.target('web')
.context(path.resolve(__dirname, '.')) // webpack 上下文目录为项目根目录
.entry('app') // 入口文件名称为 app
.add('./src/main.tsx') // 入口文件为 ./src/main.tsx
.end()
.output
.path(path.join(__dirname, './dist')) // webpack 输出的目录为根目录的 dist 目录
.filename(isDev ? '[name].[hash:8].js' : '[name].[contenthash:8].js') // 打包出来的 bundle 名称为 "[name].[contenthash:8].js"
.publicPath('/') // publicpath 配置为 "/"
.end()
.resolve
.extensions.add('.js').add('.jsx').add('.ts').add('.tsx').end() // 添加后缀自动解析
.end()
.module
.rule('ts') // 配置 typescript
.test(/\.(js|mjs|jsx|ts|tsx)$/)
.exclude
.add(filepath => {
// Don't transpile node_modules
return /node_modules/.test(filepath)
})
.end()
.use('babel-loader')
.loader('babel-loader')
.end()
.end()
.rule('sass') // sass-loader 相关配置
.test(/\.(sass|scss)$/) // Sass 和 Scss 文件
.use('extract-loader') // 提取 css 样式到单独 css 文件
.loader(require('mini-css-extract-plugin').loader)
.end()
.use('css-loader') // 加载 css 模块
.loader('css-loader')
.end()
.use('postcss-loader') // 处理 css 样式
.loader('postcss-loader')
.end()
.use('sass-loader') // Sass 语法转 css 语法
.loader('sass-loader')
.end()
.end()
.end()
.plugin('extract-css') // 提取 css 样式到单独 css 文件
.use(require('mini-css-extract-plugin'), [
{
filename: isDev ? 'css/[name].css': 'css/[name].[contenthash].css',
chunkFilename: isDev ? 'css/[id].css': 'css/[name].[contenthash].css',
},
])
.end()
.plugin('html') // 添加 html-webpack-plugin 插件
.use(require('html-webpack-plugin'), [
{
template: path.resolve(__dirname, './public/index.html'), // 指定模版文件
chunks: ['app'], // 指定需要加载的 chunk
inject: 'body', // 指定 script 脚本注入的位置为 body
},
])
.end()
.devServer
.host('0.0.0.0') // 服务器外部可访问
.disableHostCheck(true) // 关闭白名单校验
.contentBase(path.resolve(__dirname, './public')) // 设置一个 express 静态目录
.historyApiFallback({
disableDotRule: true, // 禁止在链接中使用 "." 符号
rewrites: [
{ from: /^\/$/, to: '/index.html' }, // 将所有的 404 响应重定向到 index.html 页面
],
})
.port(8080) // 当前端口号
.hot(true) // 打开页面热载功能
.sockPort('location') // 设置成平台自己的端口
.open(true)
module.exports = config.toConfig();
测试
我们在 src
目录底下创建一个 main.scss
样式文件:
touch ./src/main.scss
然后写入点样式到 src/main.scss
文件:
.root{
color: red;
font-size: 30px;
}
接着我们修改一下 src/main.tsx
文件,引入 main.scss
样式:
import React from 'react';
import ReactDOM from 'react-dom';
import "./main.scss";
ReactDOM.render(
<div className="root">hello react</div>,
document.getElementById('root')
);
可以看到,我们引入了 main.scss
样式文件,然后给 div
元素上添加了一个 root
类名。
我们重新运行项目看结果:
npm start
可以看到,当我们修改样式跟内容的时候,页面实时刷新了。
这一节有点长,先到这里了。
总结
我们从 0
开始搭建了一个项目,完成了入口与出口的配置、ts 语法支持、react 基本库的安装、css 样式配置等工作,跟上的小伙伴要给你们点个赞哦,所以整个下来基本上是对 webpack 的配置工作(对 webpack 不熟的小伙伴,强烈推荐去看我前面的文章 《来和 webpack 谈场恋爱吧》:https://www.lanqiao.cn/courses/2893),到这我们只完成了一半的配置工作,我们下节继续。
欢迎志同道合的小伙伴一起交流,一起学习,欢迎私信我~