简介
上一节我们从 0
开始搭建了一个项目,完成了入口与出口的配置、ts 语法支持、react 基本库的安装、css 样式配置等工作,我们继续上一节的内容。
知识点
- Eslint(代码质量校验)
- eslint-webpack-plugin(Eslint webpack 插件)
- eslint-config-react-app(React 官方 eslint 配置)
- fork-ts-checker-webpack-plugin(ts 语法校验插件)
- Optimization(分包优化等)
准备
上一节所有的内容在 dev 分支:https://gitee.com/vv_bug/cus-react-demo/tree/dev,我们重新切一个新分支 dev-v1.0.0
。
我们安装好依赖,并且用 npm start
命令启动项目:
npm install --registry https://registry.npm.taobao.org && npm start
可以看到,浏览器自动打开了我们项目入口,并且在页面正常显示了 ”hello react111“ ,到这,我们的准备工作算是完成了。
Eslint
ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。
接下来我们来安装一下 ESLint
相关依赖:
安装 ESLint
Eslint 核心 API。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint --registry https://registry.npm.taobao.org
安装 eslint-webpack-plugin
Eslint 在 Webpack
中的插件。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint-webpack-plugin --registry https://registry.npm.taobao.org
安装 eslint-config-react-app
React
官方提供的 Eslint
配置。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint-config-react-app --registry https://registry.npm.taobao.org
安装 @typescript-eslint/eslint-plugin
ESLint 中的 ts 插件。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D @typescript-eslint/eslint-plugin --registry https://registry.npm.taobao.org
安装 @typescript-eslint/parser
ESLint 中的 ts 语法解析器。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D @typescript-eslint/parser --registry https://registry.npm.taobao.org
安装 typescript
ts 语法核心库。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D typescript --registry https://registry.npm.taobao.org
安装 eslint-plugin-import
Eslint
中对 EsModule
导入导出的规范插件。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint-plugin-import --registry https://registry.npm.taobao.org
安装 eslint-plugin-flowtype
Eslint
中对 flow
语法规范插件。
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint-plugin-flowtype --registry https://registry.npm.taobao.org
安装 eslint-plugin-jsx-a11y
安装 eslint-plugin-react-hooks
安装 eslint-plugin-react
安装 babel-eslint
在工程目录 cus-react-demo
下执行以下命令安装:
npm install -D eslint-plugin-jsx-a11y eslint-plugin-react-hooks eslint-plugin-react babel-eslint --registry https://registry.npm.taobao.org
总算是安装完毕了,其实这些都是 React
官方要求的一些 Eslint
插件,都是在 eslint-config-react-app
中定义要求的,每一个具体什么功能我就不一一分析了,小伙伴自己去官网查看哦,童鞋们平时也不需要刻意去记这些东西,直接 copy 然后安装即可,ESLint 依赖我们是安装完毕了,下面我们来给项目配置一下 ESLint。
首先找到 webpack 配置文件 webpack.config.js
,然后添加 eslint-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()
.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()
.plugin('eslint') // 添加 eslint-webpack-plugin 插件
.use(require('eslint-webpack-plugin'), [
{
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
eslintPath: require.resolve('eslint'),
failOnError: !isDev,
context: path.resolve(__dirname, "./src"),
// ESLint class options
cwd: __dirname,
resolvePluginsRelativeTo: __dirname,
}
])
.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();
如果要让 eslint 起作用的话,我们还需要给 eslint 添加一个配置文件 .eslintrc.js
。
我们在工程目录 cus-react-demo
下创建一个 .eslintrc.js
文件:
touch .eslintrc.js
然后写入以下代码到 .eslintrc.js
文件:
module.exports = {
env: {
node: true, // 添加 node 环境
},
extends: [
"react-app", // 继承 react-app 配置
"react-app/jest" // 继承 react-app/jest 配置
],
rules: {
// 自定义规则
semi: [
// 代码结尾必须使用 “;“ 符号
'error',
'always',
],
quotes: [
// 代码中字符串必须使用 ”” 符号
'error',
'double',
],
'no-console': 'error', // 代码中不允许出现 console
},
};
其实我们需要的只是对 src
目录底下的所有文件做代码质量校验,其它的文件是不需要的。所以我们在工程目录 cus-react-demo
下再创建一个 .eslintignore
文件,列出那些不需要校验的文件列表:
touch .eslintignore
然后写入以下内容到 .eslintignore
文件:
node_modules/*
public/*
dist/*
webpack.config.js
.eslintrc.js
ok!万事都已俱备。
然后我们重新运行项目:
npm start
可以看到,终端中报了一些警告,我们尝试修复一下这些警告。
为了方便,我们在 package.json
文件中声明一个 lint
脚本:
{
"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",
"lint": "eslint --ext .js,.mjs,.jsx,.ts,.tsx --fix ./src"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@webpack-cli/serve": "^1.3.0",
"autoprefixer": "^9.8.6",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-preset-react-app": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^5.1.3",
"cssnano": "^4.1.10",
"eslint": "^7.22.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.4.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-webpack-plugin": "^2.5.2",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^1.3.9",
"postcss-loader": "^5.2.0",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"typescript": "^4.2.3",
"webpack": "^5.26.3",
"webpack-chain": "^6.5.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}
然后我们在工程目录 cus-react-demo
执行 npm run lint
脚本去修复当前项目代码:
npm run lint
运行过后,ESLint 会自动帮我们修复掉了一些格式方面的问题。
从这里也可以看出,ESLint 不但可以发现我们代码中的一些问题,还能自动帮我们解决大部分语法、格式等问题。但也很多小伙伴是很反感 ESLint 的,因为他们觉得有了 ESLint 后,代码变得难写多了,但我想说的是 “真的代码难写了?还是你写出来的代码本来就问题呢?”。
ok!警告解决完毕后,我们再次执行 npm start
的时候就会发现,没有错误和警告信息了:
npm start
一般我们在平时开发中,为了更好的显示代码规范错误信息,我们直接利用 webpack.devServer
的 overlay
选项将 webpack 的报错跟警告显示到页面中去,所以我们修改一下 webpack.config.js
文件:
...
.devServer
.host("0.0.0.0") // 为了让外部服务访问
.port(8090) // 当前端口号
.hot(true) // 热载
.open(true) // 开启页面
.overlay({
warnings: true,
errors: true
}) // webpack 错误和警告信息显示到页面
...
比如我们现在项目中有代码不符合我们的代码规范:
可以看到,页面中提示了 ”编译失败“,IDE
提示了 “语句结尾需要添加分号”,终端中照样提示了 “语句结尾需要添加分号”,这样就很好的保持了项目代码风格的一致性,在团队合作中还是很有必要的。
fork-ts-checker-webpack-plugin
校验 ts 语法,检测 ts 语法中的一些报错并且通过 webpack 在终端中打印出来。
我们首先在工程目录 cus-react-demo
下执行以下命令进行安装:
npm install -D fork-ts-checker-webpack-plugin --registry https://registry.npm.taobao.org
安装完毕后,我们在 webpack.config.js
中将其引入:
.plugin('fork-ts-checker') // 配置 fork-ts-checker
.use(require('fork-ts-checker-webpack-plugin'), [{
eslint: {
files: './src/**/*.{ts,tsx,js,jsx}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
},
typescript: {
extensions: {
vue: {
enabled: true,
compiler: "vue-template-compiler"
},
}
}
}])
webpack.config.js
全部配置:
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()
.plugin('eslint') // 添加 eslint-webpack-plugin 插件
.use(require('eslint-webpack-plugin'), [
{
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
eslintPath: require.resolve('eslint'),
failOnError: !isDev,
context: path.resolve(__dirname, "./src"),
// ESLint class options
cwd: __dirname,
resolvePluginsRelativeTo: __dirname,
}
])
.end()
.plugin('fork-ts-checker') // 配置 fork-ts-checker
.use(require('fork-ts-checker-webpack-plugin'), [{
eslint: {
files: './src/**/*.{ts,tsx,js,jsx}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
},
typescript: {
extensions: {
vue: {
enabled: true,
compiler: "vue-template-compiler"
},
}
}
}])
.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) // 开启页面
.overlay({
warnings: true,
errors: true
}) // webpack 错误和警告信息显示到页面
module.exports = config.toConfig();
我们来测试一下,我们在 src/main.tsx
文件中添加一些错误的 ts
语法:
可以看到,我们对一个 string
类型的 a
赋值为 number
。
首先 IDE
直接报错了,说我们不能对一个string
类型的变量赋值 number
类型。
接下来我们运行一下项目,看 webpack
会不会编译通过:
npm start
可以看到,还是三个地方提示我们报错了,所以很好的避免了 ts
语法的报错。
Optimization
minimizer
将生产环境的代码进行压缩,并且去掉代码中的注释:
//----- optimization start-----
config.when(
!isDev,
() => {
// 生成环境的时候
config.optimization
.minimize(true) // 打开压缩代码开关
.minimizer('terser')
.use(require('terser-webpack-plugin'), [
{
extractComments: false, // 去除注释
terserOptions: {
output: {
comments: false, // 去除注释
},
},
},
]);
},
() => {
// 开发环境的时候
}
);
//----- optimization end-------
Devtool
开发环境的时候将 devtool
改成 cheap-module-source-map
加快速度:
//----- optimization start-----
config.when(
!isDev,
() => {
// 生成环境的时候
config.optimization
.minimize(true) // 打开压缩代码开关
.minimizer('terser')
.use(require('terser-webpack-plugin'), [
{
extractComments: false, // 去除注释
terserOptions: {
output: {
comments: false, // 去除注释
},
},
},
]);
},
() => {
// 开发环境的时候
config.devtool('cheap-module-source-map');
}
);
//----- optimization end-------
分包机制
- 将
node_modules
目录下所有的依赖都打包到chunk-vendor.js
文件,优先级最高。 - 将重复次数 >=2 的依赖打包到
chunk-common.js
文件。 - 把
webpack runtime
代码从每个chunk
中抽离,单独打包到runtime.js
文件。
config.optimization
.splitChunks({
cacheGroups: {
vendors: {
// 分离入口文件引用 node_modules 的 module(vue、@babel/xxx)
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial',
},
common: {
// 分离入口文件引用次数 >=2 的 module
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true,
},
},
})
.runtimeChunk('single'); // 分离 webpack 的一些帮助函数,比如 webpackJSONP 等等
webpack.config.js
所有配置内容:
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()
.plugin('eslint') // 添加 eslint-webpack-plugin 插件
.use(require('eslint-webpack-plugin'), [
{
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
eslintPath: require.resolve('eslint'),
failOnError: !isDev,
context: path.resolve(__dirname, "./src"),
// ESLint class options
cwd: __dirname,
resolvePluginsRelativeTo: __dirname,
}
])
.end()
.plugin('fork-ts-checker') // 配置 fork-ts-checker
.use(require('fork-ts-checker-webpack-plugin'), [{
eslint: {
files: './src/**/*.{ts,tsx,js,jsx}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
},
typescript: {
extensions: {
vue: {
enabled: true,
compiler: "vue-template-compiler"
},
}
}
}])
.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) // 开启页面
.overlay({
warnings: true,
errors: true
}) // webpack 错误和警告信息显示到页面
//----- optimization start-----
config.when(
!isDev,
() => {
// 生成环境的时候
config.optimization
.minimize(true) // 打开压缩代码开关
.minimizer('terser')
.use(require('terser-webpack-plugin'), [
{
extractComments: false, // 去除注释
terserOptions: {
output: {
comments: false, // 去除注释
},
},
},
]);
},
() => {
// 开发环境的时候
config.devtool('cheap-module-source-map');
}
);
config.optimization
.splitChunks({
cacheGroups: {
vendors: {
// 分离入口文件引用 node_modules 的 module(vue、@babel/xxx)
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial',
},
common: {
// 分离入口文件引用次数 >=2 的 module
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true,
},
},
})
.runtimeChunk('single'); // 分离 webpack 的一些帮助函数,比如 webpackJSONP 等等
//----- optimization end-------
module.exports = config.toConfig();
到这,我们的项目就算是搭建完毕了,可能还需要一些缝缝补补,不过已经可以满足一个企业级的项目标准了,小伙伴自己直接拖到项目中去使用。
总结
我们花了两节课完成了从 0
开始搭建一个企业级 React
的项目,怎么样?是不是感觉还是有点难度的? 正因为 Webpack
的配置太多了,所以 React
、Vue
官方都会直接提供一个脚手架,然后通过一些简单的命令把一个配置好的项目直接给你,你不需要进行配置就可以轻松的把一个项目跑起来了,官方提供的项目可以满足大多数公司的一个业务需求,但是如果不能满足业务需求的时候,我们就需要自己具备从 0
搭建一个项目的功底了,这也是大公司必备的一个技能(对 webpack 不熟的小伙伴,强烈推荐去看我课程 《来和 webpack 谈场恋爱吧》:https://www.lanqiao.cn/courses/2893)。