1 项目结构
为了方便讲解,这里我们可以直接先使用
npx create-react-app my-app快速搭建一个简单的项目,创建出来的项目的的src就是上图的src
添加一些配置文件
在项目根目录下加一些配置文件
.gitignore
git提交忽略的文件配置
.eslintignore
eslint忽略目录文件
.babelrc.js
babel配置,下面是社区总结出来的最佳实践,嗯官方也承认了,只不过官方觉得太多了,目前还在讨论中
/**
* babel 配置
*/
function resolvePlugin(plugins) {
return plugins.filter(Boolean).map((plugin) => {
if (Array.isArray(plugin)) {
const [pluginName, ...args] = plugin
return [require.resolve(pluginName), ...args]
}
return require.resolve(plugin)
})
}
/**
* 基本plugins
* @doc https://babeljs.io/blog/2018/07/27/removing-babels-stage-presets
*/
const basePlugins = [
// Stage 0
'@babel/plugin-proposal-function-bind',
// Stage 1
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-logical-assignment-operators',
['@babel/plugin-proposal-optional-chaining', { loose: false }],
['@babel/plugin-proposal-pipeline-operator', { proposal: 'minimal' }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose: false }],
'@babel/plugin-proposal-do-expressions',
// Stage 2
['@babel/plugin-proposal-decorators', { legacy: true }],
'@babel/plugin-proposal-function-sent',
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-proposal-numeric-separator',
'@babel/plugin-proposal-throw-expressions',
// Stage 3
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-import-meta',
['@babel/plugin-proposal-class-properties', { loose: false }],
'@babel/plugin-proposal-json-strings',
// '@babel/plugin-proposal-private-methods',
]
// 项目自定义plugins
const customPlugins = [
'@babel/plugin-transform-runtime',
[
'ramda',
{
useES: true,
},
],
'lodash',
[
'import',
{
libraryName: 'antd',
style: true,
},
],
]
module.exports = {
presets: resolvePlugin([
[
'@babel/preset-env',
{
// will add direct references to core-js modules as bare imports (or requires).
useBuiltIns: 'usage',
// Set the corejs version we are using to avoid warnings in console
// This will need to change once we upgrade to corejs@3
// https://github.com/babel/babel/blob/master/packages/babel-preset-env/src/polyfills/corejs3/built-in-definitions.js
corejs: 3,
// Do not transform modules to CJS
modules: false,
// Exclude transforms that make all code slower
exclude: ['transform-typeof-symbol'],
},
],
'@babel/preset-react',
'@babel/preset-typescript',
]),
plugins: [...resolvePlugin(basePlugins), ...customPlugins],
// sourceType: 'unambiguous',
}
.prettierrc
prettier插件格式化配置,因团队所好
2. 包管理package.json介绍
{
"name": "my-app",
"version": "0.1.0",
"author": "ywen",
"license": "ISC",
"description": "react template",
"dependencies": {
"axios": "0.18.0"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.5",
"@babel/plugin-proposal-decorators": "^7.16.5",
"@babel/plugin-proposal-do-expressions": "^7.16.5",
"@babel/plugin-proposal-export-default-from": "^7.16.5",
"@babel/plugin-proposal-export-namespace-from": "^7.16.5",
"@babel/plugin-proposal-function-bind": "^7.16.5",
"@babel/plugin-proposal-function-sent": "^7.16.5",
"@babel/plugin-proposal-json-strings": "^7.16.5",
"@babel/plugin-proposal-logical-assignment-operators": "^7.16.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.5",
"@babel/plugin-proposal-numeric-separator": "^7.16.5",
"@babel/plugin-proposal-optional-chaining": "^7.16.5",
"@babel/plugin-proposal-pipeline-operator": "^7.16.5",
"@babel/plugin-proposal-private-methods": "^7.16.5",
"@babel/plugin-proposal-throw-expressions": "^7.16.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"antd": "^4.17.1",
"babel-loader": "^8.2.3",
"babel-plugin-import": "^1.13.3",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-ramda": "^2.0.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"connected-react-router": "^6.9.1",
"copy-webpack-plugin": "^10.0.0",
"core-js": "^3.19.1",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^3.3.1",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"json-loader": "^0.5.7",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.5",
"numeral": "^2.0.6",
"postcss": "^8.3.11",
"postcss-loader": "^6.2.0",
"postcss-preset-env": "^7.1.0",
"react": "^17.0.2",
"react-dev-inspector": "^1.7.1",
"react-dev-utils": "^11.0.4",
"react-document-title": "^2.0.3",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-refresh": "^0.11.0",
"react-router-dom": "^5.3.0",
"redux": "^4.1.2",
"redux-thunk": "^2.4.0",
"shelljs": "^0.8.4",
"style-loader": "^3.3.1",
"url-loader": "^4.1.1",
"web-vitals": "^2.1.2",
"webpack": "^5.64.2",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.5.0",
"webpack-merge": "^5.8.0"
},
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development node scripts/webpack/build.dev.js",
"build": "cross-env NODE_ENV=production node scripts/webpack/build.prod.js"
}
}
嗯,看起来很多包,像create-react-app,就把这些包的作用分别拆分不同的包里,接下里我们说说这些包的作用
处理js
包中带babel名字的都是和babel处理相关,其中截图中的包都是.babelrc.js文件中用到的
处理css
- css-loader 处理css文件
- less-loader 处理less文件
- postcss-loader 处理css前缀比如 -ms-
- style-loader 开发环境处理,把样式全打入html根下style标签里,这样能更快的响应热更新
- mini-css-extract-plugin 生产环境用于模块分析,异步加载、css分割,样式冲突检测等
html处理
html-webpack-plugin可以把一些变量打入html模版中
其他图片,json文件等资源处理
webpack5之前会用到url-loader、file-loader;webpack5可以使用内置的资源处理模块(asset module type)来处理,当然你也可以继续使用原来的方式
其他的包
- lodash 工具库,用过了都说好
- antd UI组件库
- case-sensitive-paths-webpack-plugin 严格区分大小写文件名,以解决git识别不了大小写文件名导致的问题
- axios 借口请求库 0.18以后的版本需要注意下请求头权限的配置...,我一般写死0.18
- @pmmmwh/react-refresh-webpack-plugin 开发环境热更新
- react-dev-inspector 用于点击浏览器位置直接触发vscode代码位置神器
- shelljs 自定义脚本处理,你懂的
- web-vitals create-react-app自带的性能统计,我留下了
构建webpack配置讲解
我们为项目根目录下创建一个scripts的目录用于存放各种处理脚本,比如包检查,webassembly编译等。在scripts目录下创建一个叫webpack目录,用于放webpack处理的脚本和配置。
嗯,看到了熟悉的webpack配置文件
- webpack.base.conf.js 基本配置
- webpack.dev.conf.js 开发环境配置
- webpack.prod.conf.js 生产环境配置
- paths.js 文件路径集合
- build.dev.js 开发环境启动文件
- build.prod.js 生产环境启动文件 可能大家会疑惑为啥要多出两个build.*.js文件下面我会一一介绍各个文件
基本配置base.conf
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin')
const { resolve } = require('path')
const paths = require('./paths')
const fs = require('fs')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const appDirectory = fs.realpathSync(process.cwd())
const resolveApp = (relativePath) => resolve(appDirectory, relativePath)
// Exclude and Include
const defaultIncludePath = [paths.src]
const isDev = process.env.WEBPACK_SERVE === 'true'
const entriesMap = {
index: 'src/index.js',
}
const entriesNames = Object.keys(entriesMap) || []
const baseCssLoader = [
{
loader: isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
},
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [['postcss-preset-env']],
},
},
},
]
module.exports = {
entry: entriesMap,
target: 'web',
resolve: {
extensions: [
'.web.js',
'.jsx',
'.js',
'.css',
'.vue',
'.html',
'.less',
'.postcss',
'.json',
],
alias: { // 快捷路径别名设置比如可以直接 import utils from 'utils'
'@': paths.src,
public: paths.public,
src: paths.src,
components: resolve('src/components'),
utils: resolve('src/utils')
},
},
/**
* 核心编译模块可以对照上图所示
* webpack 中所有的loader 都可以拥有include和exclude属性。
* exclude:排除不满足条件的文件夹(这样可以排除webpack查找不必要的文件)
* include:需要被loader 处理的文件或文件夹
*/
module: {
rules: [
{ // 处理js
test: /\.js|\.jsx?$/,
use: [
{
loader: 'babel-loader',
},
// 调试代码神器,点击页面快速定位到代码位置,注意这个 loader babel 编译之前执行
// {
// loader: 'react-dev-inspector/plugins/webpack/inspector-loader',
// options: { exclude: [resolve(__dirname, 'assets')] },
// },
],
include: defaultIncludePath,
exclude: /node_modules/,
},
{ // 处理.css后缀文件
test: /\.css$/,
use: [...baseCssLoader],
},
{ // 处理.less后缀文件和注入全局less变量
test: /\.less$/,
use: [
...baseCssLoader,
{
loader: 'less-loader',
options: {
lessOptions: {
modifyVars: {
'primary-color': '#1890ff', // 全局主色
'link-color': '#1890ff', // 链接色
'success-color': '#52c41a', // 成功色
'warning-color': '#faad14', // 警告色
'error-color': '#f5222d', // 错误色
},
javascriptEnabled: true,
},
},
},
],
},
// assets资源处理
// {
// test: /\.(png|svg|jpg|jpeg|gif)$/i,
// include: [paths.src],
// type: 'asset/resource',
// },
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
// esModule: false,
limit: 10000,
name: 'img/[name].[hash:base64:7].[ext]',
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name].[hash:base64:7].[ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:base64:7].[ext]',
},
},
],
},
plugins: [
// 忽略moment国际化文件
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
// html模版处理,这里可以注入 变量然后在html模版中直接使用变量
...entriesNames.map((entryName) => {
// const excludeChunks = entriesNames.filter((n) => n !== entryName)
return new HtmlWebpackPlugin({
// excludeChunks: excludeChunks.concat(
// excludeChunks.map((entryName) => `runtime~${entryName}`),
// ),
filename: `${entryName}.html`,
// inject: true,
templateParameters: {
NODE_ENV: process.env.NODE_ENV,
},
favicon: resolve('public/favicon.ico'),
template: resolve('public/index.html'),
minify: false,
})
}),
// 严格大小写文件区分
new CaseSensitivePathsPlugin(),
// 拷贝
new CopyWebpackPlugin({
patterns: [
{
from: resolve('public/manifest.json'),
to: resolve('dist'),
},
{
from: resolve('public/logo192.png'),
to: resolve('dist'),
},
],
}),
new ModuleNotFoundPlugin(resolveApp('.')),
],
}
开发环境dev.conf
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
// const ESLintPlugin = require('eslint-webpack-plugin') // webpack5专用的eslint
// const CircularDependencyPlugin = require('circular-dependency-plugin') // 找出循环依赖
const { resolve } = require('path')
const defaultIncludePath = [resolve('src')]
const eslintExclude = [/node_modules/]
const devWebpackConfig = merge(baseWebpackConfig, {
output: {
path: resolve('dist'),
filename: 'js/[name].[hash:6].js',
publicPath: '/',
devtoolModuleFilenameTemplate: (info) =>
resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
},
mode: 'development',
cache: {
type: 'filesystem', // 使用文件缓存
},
module: {
rules: [
// 热更新
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
plugins: [require.resolve('react-refresh/babel')].filter(Boolean),
},
},
],
},
],
},
infrastructureLogging: {
level: 'none',
},
devServer: {
historyApiFallback: true,
hot: true,
compress: true,
port: 5000,
open: false,
},
devtool: 'cheap-module-source-map',
plugins: [
new webpack.DefinePlugin({
process: {
env: {
NODE_ENV: '"development"',
PWD: JSON.stringify(process.env.PWD),
},
},
}),
// 热更新
new ReactRefreshWebpackPlugin({
overlay: false,
}),
// new CircularDependencyPlugin({
// exclude: /node_modules/,
// include: /src/,
// failOnError: true,
// allowAsyncCycles: false,
// cwd: process.cwd(),
// }),
],
optimization: {
providedExports: true,
usedExports: true,
},
})
module.exports = devWebpackConfig
这里特别要注意的是webpack.DefinePlugin,和之前使用的不一样,参数要变成对象方式
生产环境
const path = require('path')
const { merge } = require('webpack-merge')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const webpack = require('webpack')
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const baseWebpackConfig = require('./webpack.base.conf')
const { resolve } = require('path')
const wbpackConfig = merge(baseWebpackConfig, {
mode: 'production',
output: {
publicPath: './',
path: resolve('dist'),
filename: path.posix.join('static', 'js/[name].[contenthash].js'), // 占位符如[id]、[chunkhash]、[name]等分别代表编译后的模块id、chunk的hashnum值、chunk名等
},
optimization: {
minimize: true,
moduleIds: 'deterministic',
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
drop_debugger: true,
// drop_console 会删去 console.*
// drop_console: true,
// 仅丢弃 console.log
pure_funcs: ['console.log'],
},
output: {
comments: false,
},
},
}),
new CssMinimizerPlugin({
parallel: 4,
}),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial',
},
dll: {
name: `chunk-dll`,
test: /[\\/]bizcharts|[\\/]\@antv[\\/]data-set/,
priority: 15,
reuseExistingChunk: true,
},
common: {
name: `chunk-common`,
minChunks: 3,
priority: -20,
chunks: 'all',
reuseExistingChunk: true,
},
},
},
runtimeChunk: true,
},
plugins: [
new webpack.DefinePlugin({
process: {
env: {
NODE_ENV: '"production"',
PWD: JSON.stringify(process.env.PWD),
},
},
}),
// css处理
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[name].[contenthash].css',
// 如果出现css 覆盖冲突,可以加上这个
// ignoreOrder: true,
}),
// 包大小分析
// new BundleAnalyzerPlugin({ analyzerPort: '9900' })
],
})
module.exports = wbpackConfig
开发环境启动build.dev
为什么不直接用webpackConfig.devServer的熟悉open就行了呢,那是因为如果那么用的话,会导致每次启动都会新开一个窗口,而不是看浏览器是否已经有了打开的窗口直接复用
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const clearConsole = require('react-dev-utils/clearConsole')
const openBrowser = require('react-dev-utils/openBrowser')
const webpackConfig = require('./webpack.dev.conf')
const portfinder = require('portfinder')
const HOST = webpackConfig.devServer.host || '0.0.0.0'
const PORT = webpackConfig.devServer.port || 5000
const protocol = webpackConfig.devServer.https ? 'https' : 'http'
const url = `${protocol}://${HOST}:${PORT}`
let isFirstCompile = true
const compiler = webpack(webpackConfig)
// 防止重复打开窗口
const devServer = new WebpackDevServer(webpackConfig.devServer, compiler)
compiler.hooks.done.tap('done', stats => {
clearConsole()
if (isFirstCompile) {
isFirstCompile = false
console.log('oppo the Browser to::', url)
openBrowser(url)
}
})
// ip冲突时候重新获取
portfinder.getPort(
{
port: PORT,
stopPort: 7000,
},
(err, port) => {
if (err) {
return
}
devServer.start(port, 'localhost', (error, result) => {
if (error) {
console.log(error)
}
})
}
)
生产环境启动build.prod
这里我没有用clean-webpack插件,而是直接运行rm删除dist,单独把构建文件拎出来还有个好处就是定制输出日志就这里的stats,你可以用网上的一些好看的插件去输出
const rm = require('rimraf')
const chalk = require('chalk')
const webpack = require('webpack')
const webpackConfig = require('./webpack.prod.conf')
rm('dist', err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
最后
命令配置
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development node scripts/webpack/build.dev.js",
"build": "cross-env NODE_ENV=production node scripts/webpack/build.prod.js"
}
这样就可以去构建一个项目了