簡(jiǎn)介
繼續(xù)我們的 React 的學(xué)習(xí),上一節(jié)我們介紹了什么是 JSX 語法,并且從 Babel 源碼角度分析了 JSX 語法的轉(zhuǎn)換過程,最后我們還用 CDN 的形式搭建了一個(gè)簡(jiǎn)單的 React 項(xiàng)目,這一節(jié)我們研究一下 React 官方提供的腳手架create-react-app。
知識(shí)點(diǎn)
- React 官方腳手架(create-react-app)
- react-scripts
- react 項(xiàng)目中的 webpack 配置
- start 命令
- build 命令
安裝 React
小伙伴可以先看一下官網(wǎng)的描述:
- React 的安裝:https://zh-hans.reactjs.org/docs/cdn-links.html
- create-react-app 官方文檔:https://create-react-app.dev/docs
- create-react-app 開源地址:https://github.com/facebook/create-react-app
React 的安裝方式有兩種:
- CDN 鏈接。
- 使用React 官方腳手架(create-react-app)。
第一種我們上一節(jié)已經(jīng)使用過了,接下來我們從源碼角度介紹一下 create-react-app。
你可以利用以下方式通過腳手架去創(chuàng)建 React 項(xiàng)目:
npx
npx create-react-app my-app
(npx 在 npm 5.2+ 才能使用,可以看這個(gè) instructions for older npm versions)
npm
npm init react-app my-app
npm init 在 npm 6+ 才能使用
Yarn
yarn create react-app my-appe
yarn create 在 Yarn 0.25+ 才能使用
其實(shí) npm init 和 yarn create 就是 npx 的簡(jiǎn)寫(但是在 npm 和 yarn 中可以省略 create 字符串,直接 npm init react-app 或yarn create react-app 就可以了 ),工作流程大概是這樣的:
- 首先會(huì)判斷你本地有沒有
create-react-app依賴,如果沒有的話就會(huì)去npm官方下載。 - 找到
create-react-app依賴,執(zhí)行create-react-app聲明的bin入口文件。
我們還是來測(cè)試一下吧。
測(cè)試
首先在本地找一個(gè)目錄,然后執(zhí)行以下命令(以 npm 為例),創(chuàng)建一個(gè)叫 react-demo1 的項(xiàng)目:
npm init react-app react-demo1
等執(zhí)行完畢后會(huì)看到一個(gè)新創(chuàng)建好的文件夾 react-demo1:

然后我們?cè)?react-demo1 目錄執(zhí)行 npm start 命令就可以啟動(dòng)項(xiàng)目了:
npm start

可以看到,一個(gè)簡(jiǎn)單的 React 項(xiàng)目就被創(chuàng)建完畢并啟動(dòng)了。
React 官方腳手架(create-react-app)
我們從源碼角度分析一下,當(dāng)我們執(zhí)行:
npm init react-app react-demo1
命令后,create-react-app 腳手架是如何幫我們創(chuàng)建項(xiàng)目的?
我們直接去官網(wǎng)下一份 create-react-app 的源碼:
create-react-app 源碼地址:https://github.com/facebook/create-react-app

可以看到,create-react-app 是一個(gè)用 lerna 管理的項(xiàng)目集合,所以接下來我們先安裝依賴:
lerna bootstrap || yarn
本地沒有安裝 lerna 的話就直接用 yarn 去安裝。
當(dāng)我們執(zhí)行:
npm init react-app react-demo1
命令后,首先執(zhí)行的是 packages/create-react-app/index.js 文件(當(dāng)前版本 4.0.3):
...
const { init } = require('./createReactApp');
init();
可以看到,直接執(zhí)行了 ./createReactApp.js 文件的 init 方法:
function init() {
const program = new commander.Command(packageJson.name)
...
.action(name => {
// 獲取傳遞的項(xiàng)目名 react-demo1
projectName = name;
})
...
// 開始創(chuàng)建項(xiàng)目
createApp(
projectName, // 項(xiàng)目名
program.verbose, // 是否顯示 npm 安裝具體信息
program.scriptsVersion, // react-scripts 版本號(hào)
program.template, // 模版名稱
program.useNpm, // 是否使用 npm
program.usePnp // 是否使用 pnp
);
...
}
init 方法中獲取了一下傳遞進(jìn)來的項(xiàng)目名,然后調(diào)用了 createApp 方法:
function createApp(name, verbose, version, template, useNpm, usePnp) {
// 項(xiàng)目根目錄
const root = path.resolve(name);
// 項(xiàng)目名
const appName = path.basename(root);
// 初始化項(xiàng)目 package.json 文件
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
// 開始創(chuàng)建
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
);
}
可以看到,初始化了我們項(xiàng)目的 package.json 文件,接著又執(zhí)行了 run 方法:
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
) {
Promise.all([
// 獲取 react-scripts 依賴基本信息
getInstallPackage(version, originalDirectory),
// 獲取項(xiàng)目模版依賴基本信息,默認(rèn)是 cra-templagte 模版
getTemplateInstallPackage(template, originalDirectory),
]).then(([packageToInstall, templateToInstall]) => {
...
.then(({ isOnline, packageInfo, templateInfo }) => {
// 在項(xiàng)目根目錄安裝 react、react-dom、cra-tamplte 依賴
return install(
root,
useYarn,
usePnp,
allDependencies,
verbose,
isOnline
).then(() => ({
packageInfo,
supportsTemplates,
templateInfo,
}));
})
.then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
// 執(zhí)行當(dāng)前項(xiàng)目 react-demo1/node_modules/packageName/scripts/init.js 腳本文件
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
});
}
可以看到,run 方法主要是安裝依賴,這些依賴是:
react:react api 基礎(chǔ)庫。
react-dom:react 核心庫。
-
cra-template:react 項(xiàng)目模版。
因?yàn)槲覀冊(cè)趧?chuàng)建項(xiàng)目的時(shí)候沒有指定項(xiàng)目模版,所以默認(rèn)是官方的
cra-template模版,官方中有兩個(gè)模版:- cra-template:默認(rèn)項(xiàng)目模版。
- cra-tamplate-typescript:ts 項(xiàng)目模版。
當(dāng)然,還支持你傳遞自己的模版,可以為
file、npm、gitlab類型,就不具體掩飾了。
接著執(zhí)行了當(dāng)前項(xiàng)目 react-demo1/node_modules/packageName/scripts/init.js 腳本文件:
// 初始化 git
function tryGitInit() {
...
}
module.exports = function (
appPath,
appName,
verbose,
originalDirectory,
templateName
) {
// 找到 react-demo1/nodule_modules/cra-template 目錄,然后按照規(guī)則 copy 文件到當(dāng)前 react-demo1 項(xiàng)目,最后刪除 react-demo1/nodule_modules/cra-template 目錄
console.log();
// 恭喜創(chuàng)建完畢
console.log('Happy hacking!');
};
到這,react-demo1 項(xiàng)目就算是創(chuàng)建完畢了。
start 命令
當(dāng)我們?cè)趧倓?chuàng)建好的 react-demo1 項(xiàng)目中執(zhí)行 npm start 命令的時(shí)候,會(huì)自動(dòng)幫我們開啟一個(gè)開發(fā)環(huán)境,并且打開入口頁面:
npm start
ok,我們看一下當(dāng)我們?cè)陧?xiàng)目根目錄執(zhí)行 npm start 命令到底干了什么?
首先是 react-demo1/package.json 文件中的 start 命令:
...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
...
可以看到,執(zhí)行了 react-scripts start 命令。
我們找到 react-scripts start 命令的源碼 create-react-app/packages/react-scripts/scripts/start.js:
// 設(shè)置當(dāng)前環(huán)境變量為 development
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// 開始項(xiàng)目中配置的環(huán)境變量
require('../config/env');
// 校驗(yàn) typescript 的配置
verifyTypeScriptSetup();
...
// 校驗(yàn)入口文件跟入口 html 模版文件是否存在
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
...
// 創(chuàng)建 webpack 的編譯類
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
// 創(chuàng)建 WebpackDevServer 開啟 webpack 服務(wù)
const devServer = new WebpackDevServer(compiler, serverConfig);
...
});
start 命令其實(shí)就是利用 webpack-dev-server 開啟了一個(gè) webpack 服務(wù)。(對(duì) webpack 不熟的童鞋,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場(chǎng)戀愛吧:https://www.lanqiao.cn/courses/2893)
build 命令
build 命令就不用說了,直接就是 webpack 的打包操作,比如我們?cè)?react-demo1 目錄下執(zhí)行 build 命令:
npm run build

可以看到,在 react-demo1/build 目錄中輸出了 webpack 打包過后的結(jié)果。
start 跟 build 都是利用的 webpack 進(jìn)行編譯打包操作的,只是環(huán)境不同 webpack 的配置也會(huì)不同,下面我們重點(diǎn)看一下在 development 模式與 production 模式中,React 腳手架對(duì) webpack 的配置。
React 項(xiàng)目中的 Webpack 配置
我們直接找到源碼 create-react-app/packages/react-scripts/config/webpack.config.js 文件:
...
// 是否生成 source-map 文件
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// webpack 客戶端熱載入口文件
const webpackDevClientEntry = require.resolve(
'react-dev-utils/webpackHotDevClient'
);
// 是否禁止 eslint 警告提示
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
// 是否禁止 eslint
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
// 媒體文件字節(jié)限制,小于這個(gè)限制會(huì)打包成 base64 字符串,超出這個(gè)限制會(huì)導(dǎo)出文件
// 主要是指對(duì) url-loader 的配置
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// 是否使用 ts
const useTypeScript = fs.existsSync(paths.appTsConfig);
// 根據(jù)環(huán)境返回不同的 webpack 配置,development 或者 production
module.exports = function (webpackEnv) {
// 開發(fā)環(huán)境
const isEnvDevelopment = webpackEnv === 'development';
// 生產(chǎn)環(huán)境
const isEnvProduction = webpackEnv === 'production';
// 生成樣式 loaders 主要是 sass、scss、css
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
// 開發(fā)環(huán)境使用 style-loader(會(huì)生成內(nèi)嵌樣式)
isEnvDevelopment && require.resolve('style-loader'),
// 生產(chǎn)環(huán)境使用 MiniCssExtractPlugin.loader (生成外聯(lián)樣式)
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
// 配置 css-loader
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
// 配置 postcss-loader
{
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: [
// flex 布局兼容插件
require('postcss-flexbugs-fixes'),
[
// postcss env 插件集合
require('postcss-preset-env'),
{
// 自動(dòng)添加樣式兼容前綴
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
postcssNormalize(),
],
},
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
// 添加根路徑解析 loader,默認(rèn)指向項(xiàng)目 src 目錄
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
root: paths.appSrc,
},
},
// 添加 sass loader 等樣式預(yù)加載器
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
return {
// 設(shè)置 webpack 的模式
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// 設(shè)置 source-map 的生成方式
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// 入口文件配置
entry:
isEnvDevelopment && !shouldUseReactRefresh
? [
// 測(cè)試環(huán)境并且允許熱載刷新頁面的時(shí)候
// 加載熱載刷新入口文件
webpackDevClientEntry,
// 加載項(xiàng)目入口文件(默認(rèn) src/index.js)
paths.appIndexJs,
]
: paths.appIndexJs,
// 輸出文件設(shè)置
output: {
// 輸出目錄(默認(rèn)是項(xiàng)目的 build 目錄)
path: isEnvProduction ? paths.appBuild : undefined,
// 開發(fā)環(huán)境打開模塊的 pathinfo 路徑提示
pathinfo: isEnvDevelopment,
// 輸出文 chunk、assets 名稱設(shè)置
filename: isEnvProduction
...
};
太多了,就不一一分析了,小伙伴自己看一下源碼文件哦(對(duì) webpack 不熟的童鞋,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場(chǎng)戀愛吧:https://www.lanqiao.cn/courses/2893)。
那有小伙伴要問了,既然 React 腳手架幫我們內(nèi)置了 webpack 的配置,如果我們需要自己修改 webpack 的一些配置該咋辦呢?
比如我們需要修改以下配置:
修改輸出的目錄
從源碼中我們可以知道,目前項(xiàng)目的輸出文件的目錄為 build,比如我們需要改成 dist,我們需要怎么做呢?
我們先看一下目前的配置文件 packages/react-scripts/config/webpack.config.js:
const paths = require('./paths');
...
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
可以看到,當(dāng)為生產(chǎn)環(huán)境(production)的時(shí)候,path 的值為 paths.appBuild。
我們找到 packages/react-scripts/config/paths.js 文件中的 appBuild 變量:
...
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
...
// 默認(rèn)輸出文件目錄路徑
const buildPath = process.env.BUILD_PATH || 'build';
module.exports = {
...
appBuild: resolveApp(buildPath),
...
可以看到,我們可以通過 process.env.BUILD_PATH 變量去修改輸出文件路徑。
那么 process.env.BUILD_PATH 我們?cè)撛趺炊x呢?
-
利用
cross-env庫,在執(zhí)行命令的時(shí)候聲明process.env.BUILD_PATH變量。我們首先在
react-demo1項(xiàng)目根目錄安裝cross-env:yarn add -D cross-env接著修改一下
package.json中的build命令:"scripts": { "build": "cross-env BUILD_PATH=dist react-scripts build", ... }修改完畢后重新打包測(cè)試:
npm run build

可以看到,打包輸出的目錄變成了 dist。
-
利用腳手架提供的環(huán)境變量文件
.env.[NODE_ENV].[local]來修改,其中NODE_ENV跟local可選,表示根據(jù)環(huán)境來加載。我們?cè)?
react-demo1項(xiàng)目根目錄底下創(chuàng)建一個(gè).env文件,這樣不管是development模式還是production模式,都會(huì)加載.env文件中聲明的變量:touch ./.env然后在
.env文件中聲明BUILD_PATH變量為dist:## 修改項(xiàng)目的輸出路徑 BUILD_PATH=dist修改完畢后重新打包測(cè)試,效果跟上面的一樣,我就不演示了。
總結(jié)
這一節(jié)我們主要介紹了 React 官方提供的腳手架 create-react-app,我們直接從源碼的角度來分析了一個(gè) React 項(xiàng)目創(chuàng)建的過程,其實(shí)無非就是對(duì) Webpack 的一些配置而已,所以對(duì) Webpack 不熟悉的小伙伴一定要加油補(bǔ)上哦,從create-react-app 官方文檔上看,并沒有提供 .env 配置文件的說明、怎么去修改 webpack 配置說明等等,還是需要你自己去看源碼的,所以這就是看源碼的重要性,其實(shí)從源碼中我們可以知道,并不是所有的 webpack 配置都能修改的,那項(xiàng)目中我們又需要修改的話,該怎么辦呢?那就只能拋棄腳手架了,所以這也算是 React 腳手架的一些不足吧,并沒有像 vue-cli 一樣,可以隨意修改 webpack 的配置。
ok,后面我將會(huì)帶大家脫離腳手架,利用 webpack 從 0 開始搭建一個(gè) React 項(xiàng)目,大家敬請(qǐng)期待吧!