之前开发了好些项目,webpack都是用了很多现成的配置,每次使用都只关注到需要调整的地方。借着这次cra的项目,我把webpack的配置文件过了一遍,以一个初学者的角度重新书写一次webpack的作用。附webpack文档网址:webpack.docschina.org/
首先大家需要先知道webpack的作用,通俗来说,我们打开cmd命令窗口,执行脚本的启动命令,便会触发项目的启动以及打包,而webpack就是启动过程中发挥作用的模块打包工具,通过各种配置项,它能让项目的启动和执行拥有很多强大的功能,如压缩文件、代码提高性能,使用转换器转移ES,TS代码,设置路径别名等常用功能
在搭建好CRA之后,默认的webpack设置是隐藏的,可通过npm eject指令引出webpack的配置文件,此操作不可逆,为便于学习,我这次使用了eject,生成了scripts跟config两个文件夹如图。其中比较重要的有几个:
- env.js : 读取env环境变量配置文件并把变量赋值给process.env这全局变量中
- path.js : 提供输出模块的路径名
- start.js : 本地启动项目的脚本 : npm start
- build.js : 把项目打包起来的命令,最终把打包好的文件拿去服务器部署: npm build
- webpackDevServer.config.js : 配置服务器
- webpack.config : 最关键的配置文件,入口出口模块加载器等的配置
"scripts": {
"start": "cross-env REACT_APP_NODE_ENV=development react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
},
我把配置文件中一些注释和不必要的代码去掉,以便于精准读取关键代码
start.js
process.env.BABEL_ENV = 'development'; // 定义环境为开发环境
process.env.NODE_ENV = 'development';
process.on('unhandledRejection', err => { // 打包时查看具体报错抛出异常
throw err; });
require('../config/env'); // 加载环境变量
const chalk = require('react-dev-utils/chalk'); // 在命令窗口打印highlight用的插件
const WebpackDevServer = require('webpack-dev-server'); // 开启服务器
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
// devServer的配置文件
const createDevServerConfig = require('../config/webpackDevServer.config');
// 获取环境变量
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));
// 拿到环境变量文件,优先级是最后一个
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const useYarn = fs.existsSync(paths.yarnLockFile); // 是否使用yarn
// 判断 Node.js 是否运行在一个 TTY 环境中,tty 模块提供终端相关的接口,用来获取终端的行数列数等
const isInteractive = process.stdout.isTTY;
// 检查必要文件,不存在就退出
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1); // node 进程结束
}
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; // 端口
const HOST = process.env.HOST || '0.0.0.0'; // IP
const { checkBrowsers } = require('react-dev-utils/browsersHelper'); // 检查浏览器
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
return choosePort(HOST, DEFAULT_PORT); // 当前port忙碌时 使用其他port
})
.then(port => {
if (port == null) { // 找不到端口直接退出
return;
}
const config = configFactory('development'); // 返回开发环境的配置
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; // 网络协议
const appName = require(paths.appPackageJson).name; // 项目名称
const useTypeScript = fs.existsSync(paths.appTsConfig); // 存在ts-config时为true
const urls = prepareUrls( // 返回一个本地和远程的协议+ip+端口给开发环境的服务器使用
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
const compiler = createCompiler({ // 创建一个webpack编译器,
appName,
config,
urls,
useYarn,
useTypeScript,
webpack,
});
// Load proxy config 代理配置,可在package.json中配置
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
const serverConfig = { // webpack-dev-server的配置
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
host: HOST,
port,
};
const devServer = new WebpackDevServer(serverConfig, compiler);
devServer.startCallback(() => { // 启动devServer服务
if (isInteractive) {
clearConsole(); // 把控制台清除掉
}
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { // 检查react 版本
console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser); // 打开浏览器
});
})
env.js : 引用相应的环境变量文件并注入到process.env中,,并导出一个可以读取环境变量的函数
// Node.js中,require首先会在require.cache中查找,把缓存清除掉这样每次调用就会获取最新的变量值
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.REACT_APP_NODE_ENV;
// node环境,开发环境设置为development,生产环境是production
if (!NODE_ENV) { // 无node环境配置直接抛出异常退出
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
const dotenvFiles = [ // 从几个路径下拿到有效的配置文件,需要自定义env文件需要在根目录创建
`${paths.dotenv}.${NODE_ENV}.local`,
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
`${paths.dotenv}.${NODE_ENV}`,
paths.dotenv,
].filter(Boolean);
// 使用到了 dotenv 和 dotenv-expand 两个专门针对环境变量文件的库 —— 这两个库支持将环境变量文件中的内容读取、解析(支持变量)然后插入 process.env 中
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// 利用正则表达式,只有以REACT_APP为前缀的变量会被保留加入process.env中,跟官方文档的说法吻合
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key)) // 保留REACT_APP_前缀的变量
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
NODE_ENV: process.env.NODE_ENV || 'development',
PUBLIC_URL: publicUrl,
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
}
);
const stringified = { // 将value转为字符型
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;
webpack.config 直接去马吧
// fork一个进程进行检查,将错误信息反馈给webpack
const ForkTsCheckerWebpackPlugin =
process.env.TSC_COMPILE_ON_ERROR === 'true'
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
: require('react-dev-utils/ForkTsCheckerWebpackPlugin'); //
// sourceMap 是否启动,默认true
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// 是否内联runtime文件
const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime');
const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
'@pmmmwh/react-refresh-webpack-plugin'
);
// 是否使用内联runtimeChunk
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
// 是否使用eslint警告提示, 校验代码
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
const imageInlineSizeLimit = parseInt( // 最大转换base64图片的大小 10000
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
const useTypeScript = fs.existsSync(paths.appTsConfig); // 判断是否存在ts配置文件
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc; // service-worker
// style files regexes style文件的正则表达式 用来匹配style相关的文件
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const hasJsxRuntime = (() => { // 检查是否配置jsx-runtime
...
})();
// 生成最终webpack开发或生成环境配置的函数
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// 加载.env文件的环境变量,REACT_APP 开头
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
// 热更新 react 组件,开发环境下开启
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
// 处理style-loader 通过文件后缀名匹配文件并通过loader打包
test: option
use: module
};
return {
target: ['browserslist'], //
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', // 模式
// 在第一个错误出现时抛出失败结果,而不是容忍它.开发环境设置为false,生产打包设置为true
bail: isEnvProduction,
devtool: isEnvProduction // 默认生产环境使用source-map
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
entry: paths.appIndexJs, // 入口文件 src/index
output: {
path: paths.appBuild, // 出口文件 build
pathinfo: isEnvDevelopment, // 是否添加注释到文件中,开发环境为true
filename: isEnvProduction // 文件名,使用contenthash进行命名
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
chunkFilename: isEnvProduction // 代码分割 出来的文件会以 chunkFilename进行命名
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[name].[hash][ext]',
publicPath: paths.publicUrlOrPath, // 默认为 / ,可通过package.json中的homepage进行配置
devtoolModuleFilenameTemplate: isEnvProduction // 在生成SourceMap时的函数的文件名模版字符串。
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
cache: { // 持久化缓存 webpack 5.0新增模块 通过cache缓存跟实际变化的hash对比判断出需要重新编译的文件,极大增强了性能
type: 'filesystem', // 开启持久缓存
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache, // 缓存路径
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
infrastructureLogging: {
level: 'none',
},
optimization: { // 启动压缩, 优化性能
minimize: isEnvProduction, // 生产环境才压缩体积
minimizer: [
new TerserPlugin({ // 压缩js的插件
parallel: true, // 多进程进行提高构建速度
terserOptions: ...
}),
new CssMinimizerPlugin(), // 压缩css
],
},
resolve: { // 定义解析规则
// 告诉 webpack 解析模块是去找哪个目录 默认node_modules
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
extensions: paths.moduleFileExtensions // 省略拓展名,自动在文件名后面加入list中的文件后缀
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: { // 别名 可通过config-overrides.js 加入拓展
},
plugins: [ // 应该使用的额外的解析插件列表
new ModuleScopePlugin(paths.appSrc, [ //为避免混乱,限制查找定定义模块的范围,只能在src内部
paths.appPackageJson,
reactRefreshRuntimeEntry,
reactRefreshWebpackPluginRuntimeEntry,
babelRuntimeEntry,
babelRuntimeEntryHelpers,
babelRuntimeRegenerator,
]),
],
},
module: {
strictExportPresence: true,
rules: [ // 解析模块的规则
shouldUseSourceMap && { // 使用sourceMap
enforce: 'pre',
exclude: /@babel(?:\/|\\{1,2})runtime/,
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader'),
},
{
oneOf: [ // 匹配第一个Loader就结束,提高性能
// ... 各种loader的功能可在官方文档查阅
],
},
].filter(Boolean),
},
plugins: [
// 处理html 插件 : 以template为模板,创建一个html文件,并将 处理好的bundle文件自动引入到html文件中 (css,js,dll单独打包的依赖文件等)
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: { // 生产环境下 配置html的压缩,去除注释
removeComments: true, // 去除注释
...
},
}
: undefined
)
),
// 是否内联runtime文件,作用就是少发一个请求,通过cross-env 将环境变量设置为true
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// HtmlWebpackPlugin的辅助插件,可以在html文件中加入变量
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// 找不到模块,有更好的提示
new ModuleNotFoundPlugin(paths.appPath),
// 等于把环境变量process.env注入全局中,我们可以在模块当中直接使用这些变量。无需作任何声明,
new webpack.DefinePlugin(env.stringified),
// 热更新 react 组件,开发环境下开启,修改js,css等文件会触发更新
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
// 路径的大小写敏感校验模块
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin({ // 抽离css文件插件,生产环境下开启
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// 生成一个文件清单, 内容是打包前文件对应打包后的文件名
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
new webpack.IgnorePlugin({ // wepack内置插件,可以在打包时有选择的忽略一些内容,
// 这里的配置是在打包moment的时候忽略moment的本地化内容
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// ts 配置 开启typescript必备配置
useTypeScript :{
...
}
performance: false, // 在资源的大小超过限制的时候,做出提示。
};
};
这篇文章旨在简单介绍cra的webpack默认配置,还有更多的插件,模块里面并未全部罗列,最重要的是配置自己适合的。必须承认在刚学webpack时碰到了不少壁,要了解工作原理还是自己搭一个简单的webpack工程,直接在现有的开源项目或cra中学习的难度会比较大,内容过多也无法知道哪些是关键配置。可以尝试把一些配置改掉或删除看看打包的效果和性能的变化,会对学习更有帮助哈。