这边文章逻辑较乱,如果你有看过eject的create-react-app相关webpack源码,阅读下面的文章说不定会有意外收获(或者直接看10. 如何修改create-react-app的默认配置)
1. dotenv
Dotenv是一个零依赖模块,可以将.env
文件中的环境变量加载到process.env
。
2. 修改配置项,如端口号
//Windows (cmd.exe) set PORT=true&&npm start //Windows (Powershell) ($env:PORT = "true") -and (npm start) //Linux, macOS (Bash) PORT=true npm start
3. react-dev-utils
此程序包包含Create React App使用的一些实用程序。主要用于webpack;
//可以在GitHub参照源代码 clearConsole(); //清空控制台信息 openBrowser(url); //在控制台打开网址
4. path模块相关介绍
// 返回绝对路径(fs.realpathSync) const appDirectory = fs.realpathSync(process.cwd());path.isAbsolute()
方法检测path
是否为绝对路径
path.delimiter 系统分隔符
5. config/paths.js
'use strict'; const path = require('path'); const fs = require('fs'); const url = require('url'); // Make sure any symlinks in the project folder are resolved: // https://github.com/facebook/create-react-app/issues/637 const appDirectory = fs.realpathSync(process.cwd()); const resolveApp = relativePath => path.resolve(appDirectory, relativePath); const envPublicUrl = process.env.PUBLIC_URL;
// 路径是否添加'/'后缀 function ensureSlash(inputPath, needsSlash) { const hasSlash = inputPath.endsWith('/'); if (hasSlash && !needsSlash) { return inputPath.substr(0, inputPath.length - 1); } else if (!hasSlash && needsSlash) { return `${inputPath}/`; } else { return inputPath; } } const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; //通过require加载json文件,然后读取里面的配置 // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. // Webpack needs to know it to put the right <script> hrefs into HTML even in // single-page apps that may serve index.html for nested URLs like /todos/42. // We can't use a relative path in HTML because we don't want to load something // like /todos/42/static/js/bundle.7289d.js. We have to know the root. function getServedPath(appPackageJson) { const publicUrl = getPublicUrl(appPackageJson); const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); return ensureSlash(servedUrl, true); } const moduleFileExtensions = [ 'web.mjs', 'mjs', 'web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', ]; // Resolve file paths in the same order as webpack const resolveModule = (resolveFn, filePath) => { const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`)) ); if (extension) { return resolveFn(`${filePath}.${extension}`); } return resolveFn(`${filePath}.js`); }; // config after eject: we're in ./config/ module.exports = { dotenv: resolveApp('.env'), appPath: resolveApp('.'), appBuild: resolveApp('build'), appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveModule(resolveApp, 'src/index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), appJsConfig: resolveApp('jsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), testsSetup: resolveModule(resolveApp, 'src/setupTests'),
//我们可以在package.json的proxy配置开发环境的后端地址,如果需要更多自定义,
//我们可以安装'http-proxy-middleware',然后在src目录下创建setupProxy.js
// 没有调用resolveModule,所以文件的后缀必须为.js proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), publicUrl: getPublicUrl(resolveApp('package.json')), servedPath: getServedPath(resolveApp('package.json')), }; module.exports.moduleFileExtensions = moduleFileExtensions;
6. config/env.js
function getClientEnvironment(publicUrl) { const raw = Object.keys(process.env) .filter(key => REACT_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; return env; }, { // Useful for determining whether we’re running in production mode. // Most importantly, it switches React into the correct mode. NODE_ENV: process.env.NODE_ENV || 'development', // Useful for resolving the correct path to static assets in `public`. // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />. // This should only be used as an escape hatch. Normally you would put // images into the `src` and `import` them in code to get their paths. PUBLIC_URL: publicUrl, } ); // Stringify all values so we can feed into Webpack DefinePlugin const stringified = { 'process.env': Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); // key=value; value是被JSON.stringify() return env; }, {}), }; return { raw, stringified }; }
7. webpack-dev-server
https://webpack.docschina.org/configuration/dev-server
//compiler 类似const compiler = Webpack(webpackConfig); //devServerOptions为上述链接的配置项 // 这是参数的类型 // https://github.com/webpack/webpack-dev-server/blob/84cb4817a3fb9d8d98ac84390964cd56d533a3f5/lib/options.json new WebpackDevServer(compiler, devServerOptions);
8. fs模块
fs.accessSync(path[, mode]) //同步地测试用户对 path 指定的文件或目录的权限 F_OK,表明文件对调用进程可见。 这对于判断文件是否存在很有用,但对 rwx 权限没有任何说明。 如果未指定模式,则默认值为该值。 R_OK,表明调用进程可以读取文件。 W_OK,表明调用进程可以写入文件。 X_OK,表明调用进程可以执行文件。 在 Windows 上无效(表现得像 fs.constants.F_OK) var fs = require('fs'); var path = require('path'); var chalk = require('chalk'); function checkRequiredFiles(files) { var currentFilePath; try { files.forEach(filePath => { currentFilePath = filePath; fs.accessSync(filePath, fs.F_OK); }); return true; } catch (err) { var dirName = path.dirname(currentFilePath); var fileName = path.basename(currentFilePath); console.log(chalk.red('Could not find a required file.')); console.log(chalk.red(' Name: ') + chalk.cyan(fileName)); console.log(chalk.red(' Searched in: ') + chalk.cyan(dirName)); return false; } } module.exports = checkRequiredFiles;
fs.existsSync(path) //判断文件是否存在
9. config/webpack.config.js
#html-webpack-plugin new HtmlWebpackPlugin( Object.assign( {}, { inject: true, template: paths.appHtml, }, isEnvProduction ? { minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, } : undefined ) ), // 其中minify的参数配置可借鉴 https://github.com/kangax/html-minifier#options-quick-reference
#pnp-webpack-plugin 但是 Yarn 作为一个包管理器, 它知道你的项目的依赖树. 那能不能让 Yarn 告诉 Node? 让它直接到某个目录去加载模块. 这样即可以提高 Node 模块的查找效率, 也可以减少 node_modules 文件的拷贝. 这就是Plug'n'Play的基本原理. const PnpWebpackPlugin = require(`pnp-webpack-plugin`); module.exports = { resolve: { plugins: [ PnpWebpackPlugin, ], }, resolveLoader: { plugins: [ PnpWebpackPlugin.moduleLoader(module), ], }, }; //需要注意的是 如果您的部分配置来自使用自己的加载器的第三方软件包,请确保它们使用require.resolve- 这将确保解决过程在整个环境中都是可移植的 module.exports = { module: { rules: [{ test: /.js$/, loader: require.resolve('babel-loader'), // 原先是 loader: 'babel-loader' }] }, };
#case-sensitive-paths-webpack-plugin 此Webpack插件强制所有必需模块的完整路径与磁盘上实际路径的确切大小写相匹配 var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); var webpackConfig = { plugins: [ new CaseSensitivePathsPlugin() // other plugins ... ] // other webpack config ... }
#terser-webpack-plugin new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, drop_console: false, //默认为false inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, // Turned on because emoji and regex is not minified properly using default // https://github.com/facebook/create-react-app/issues/2488 ascii_only: true, }, }, parallel: !isWsl, // Enable file caching cache: true, sourceMap: shouldUseSourceMap, })
// Makes some environment variables available to the JS code new webpack.DefinePlugin(env.stringified), // Makes some environment variables available in index.html const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)
10. 如何修改create-react-app的默认配置
npm install react-app-rewired -S npm install customize-cra -S //package.json "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", "eject": "react-scripts eject" }, 一般在根目录下创建config-overrides.js
customize-cra提供了一些简遍的api(customize-cra/api.md),通常就可以满足大部分的开发需求
源码(customize-cra/src/customizes/webpack.js)(比如当前版本https://github.com/arackaf/customize-cra/blob/0b50907a724b04fa347164a5e8b6cd1f0c2c067b/src/customizers/webpack.js)
看了customize-cra提供的API,有简单的提供了addWebpackPlugin这个API,但是,如果我想修改CRA默认的配置项,比如HtmlWebpackPlugin
并没有提供对应的API;联想到customize-cra1.0x是开发者自己修改config,然后看了下某个API的源码,eg
// This will remove the CRA plugin that prevents to import modules from // outside the `src` directory, useful if you use a different directory export const removeModuleScopePlugin = () => config => { config.resolve.plugins = config.resolve.plugins.filter( p => p.constructor.name !== "ModuleScopePlugin" ); return config; };
可以看的这些API其实也是返回一个函数,然后通过override()包装传入config。
const { override, addWebpackPlugin } = require("customize-cra") const HtmlWebpackPlugin = require('html-webpack-plugin'); const isEnvProduction = process.env.NODE_ENV === 'production' const paths = require('react-scripts/config/paths') // 第五点有其源码
// 自己定义一个函数,过滤config.plugins数组 const removeHtmlWebpackPlugin = () => { return (config) => { config.plugins = config.plugins.filter( p => p.constructor.name !== "HtmlWebpackPlugin" ); // throw new Error() return config; } }
//生产环境去除console.* functions
// console.log(paths, 'paths') module.exports = override( removeHtmlWebpackPlugin(), addWebpackPlugin(new HtmlWebpackPlugin( Object.assign( {}, { inject: true, template: paths.appHtml, }, isEnvProduction ? { minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, // minifyJS: true, // 我们去掉默认的压缩js配置项,具体配置项参数第9点有提及 minifyCSS: true, minifyURLs: true, }, } : undefined ) )),
dropConsole() )
如果你只想删除console.log,可以用pure_funcs代替 const dropConsole = () => { return (config) => { if(config.optimization.minimizer){ config.optimization.minimizer.forEach( (minimizer) => { if( minimizer.constructor.name === 'TerserPlugin'){ // minimizer.options.terserOptions.compress.drop_console = true minimizer.options.terserOptions.compress.pure_funcs = ['console.log'] } }) } return config; } }
override()函数详解
import flow from "lodash.flow";
//flow;创建一个函数。 返回的结果是调用提供函数的结果,this
会绑定到创建函数。 每一个连续调用,传入的参数都是前一个函数返回的结果。
// 这里的plugins是个数组,里面是每个函数,返回值都是config,然后传给下一个函数 export const override = (...plugins) => flow(...plugins.filter(f => f)); // Use this helper to override the webpack dev server settings // it works just like the `override` utility export const overrideDevServer = (...plugins) => configFunction => ( proxy, allowedHost ) => { const config = configFunction(proxy, allowedHost); const updatedConfig = override(...plugins)(config); return updatedConfig; };
11. webpack-stats-plugin 通过这个插件,可以默认生成stats.json的构建信息(在线分析: http://webpack.github.io/analyse)
查找不到如何获取create-react-app的打包时间;这里只是简单用于大概获取打包所需时间,具体的打包文件分析可以使用webpack-bundle-analyzer 插件
//config-overrides.js const { override, addWebpackPlugin } = require("customize-cra"); const { StatsWriterPlugin } = require("webpack-stats-plugin") let startTime = Date.now() module.exports = override( addWebpackPlugin(new StatsWriterPlugin({ fields: null, stats: { timings: true,// 不生效 }, transform: (data) => { let endTime = Date.now() data = { Time: (endTime - startTime)/1000 + 's' } return JSON.stringify(data, null, 2); } })) )
12. progress-bar-webpack-plugin 用于显示构建的进度条
其他npm包
//Check if the process is running inside Windows Subsystem for Linux (Bash on Windows) const isWsl = require('is-wsl'); // When running inside Windows Subsystem for Linux console.log(isWsl); //=> true