簡介
繼續(xù)我們的 React
的學(xué)習(xí)次慢,上一節(jié)我們介紹了什么是 JSX
語法,并且從 Babel
源碼角度分析了 JSX
語法的轉(zhuǎn)換過程俱尼,最后我們還用 CDN
的形式搭建了一個簡單的 React
項目薄榛,這一節(jié)我們研究一下 React
官方提供的腳手架create-react-app
。
知識點(diǎn)
- React 官方腳手架(create-react-app)
- react-scripts
- react 項目中的 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
項目:
npx
npx create-react-app my-app
(npx 在 npm 5.2+ 才能使用灶泵,可以看這個 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+ 才能使用
其實 npm init
和 yarn create
就是 npx
的簡寫(但是在 npm
和 yarn
中可以省略 create
字符串育八,直接 npm init react-app
或yarn create react-app
就可以了 ),工作流程大概是這樣的:
- 首先會判斷你本地有沒有
create-react-app
依賴赦邻,如果沒有的話就會去npm
官方下載髓棋。 - 找到
create-react-app
依賴,執(zhí)行create-react-app
聲明的bin
入口文件惶洲。
我們還是來測試一下吧按声。
測試
首先在本地找一個目錄,然后執(zhí)行以下命令(以 npm
為例)恬吕,創(chuàng)建一個叫 react-demo1
的項目:
npm init react-app react-demo1
等執(zhí)行完畢后會看到一個新創(chuàng)建好的文件夾 react-demo1
:
然后我們在 react-demo1
目錄執(zhí)行 npm start
命令就可以啟動項目了:
npm start
可以看到儒喊,一個簡單的 React
項目就被創(chuàng)建完畢并啟動了。
React 官方腳手架(create-react-app)
我們從源碼角度分析一下币呵,當(dāng)我們執(zhí)行:
npm init react-app react-demo1
命令后怀愧,create-react-app
腳手架是如何幫我們創(chuàng)建項目的?
我們直接去官網(wǎng)下一份 create-react-app
的源碼:
create-react-app 源碼地址:https://github.com/facebook/create-react-app
可以看到余赢,create-react-app
是一個用 lerna
管理的項目集合芯义,所以接下來我們先安裝依賴:
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 => {
// 獲取傳遞的項目名 react-demo1
projectName = name;
})
...
// 開始創(chuàng)建項目
createApp(
projectName, // 項目名
program.verbose, // 是否顯示 npm 安裝具體信息
program.scriptsVersion, // react-scripts 版本號
program.template, // 模版名稱
program.useNpm, // 是否使用 npm
program.usePnp // 是否使用 pnp
);
...
}
init
方法中獲取了一下傳遞進(jìn)來的項目名,然后調(diào)用了 createApp
方法:
function createApp(name, verbose, version, template, useNpm, usePnp) {
// 項目根目錄
const root = path.resolve(name);
// 項目名
const appName = path.basename(root);
// 初始化項目 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
);
}
可以看到举塔,初始化了我們項目的 package.json
文件绑警,接著又執(zhí)行了 run
方法:
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
) {
Promise.all([
// 獲取 react-scripts 依賴基本信息
getInstallPackage(version, originalDirectory),
// 獲取項目模版依賴基本信息,默認(rèn)是 cra-templagte 模版
getTemplateInstallPackage(template, originalDirectory),
]).then(([packageToInstall, templateToInstall]) => {
...
.then(({ isOnline, packageInfo, templateInfo }) => {
// 在項目根目錄安裝 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)前項目 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 項目模版。
因為我們在創(chuàng)建項目的時候沒有指定項目模版懈涛,所以默認(rèn)是官方的
cra-template
模版,官方中有兩個模版:- cra-template:默認(rèn)項目模版批钠。
- cra-tamplate-typescript:ts 項目模版泣港。
當(dāng)然,還支持你傳遞自己的模版呛每,可以為
file
、npm
晨横、gitlab
類型,就不具體掩飾了手形。
接著執(zhí)行了當(dā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 項目库糠,最后刪除 react-demo1/nodule_modules/cra-template 目錄
console.log();
// 恭喜創(chuàng)建完畢
console.log('Happy hacking!');
};
到這伙狐,react-demo1
項目就算是創(chuàng)建完畢了。
start 命令
當(dāng)我們在剛創(chuàng)建好的 react-demo1
項目中執(zhí)行 npm start
命令的時候瞬欧,會自動幫我們開啟一個開發(fā)環(huán)境贷屎,并且打開入口頁面:
npm start
ok,我們看一下當(dā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';
// 開始項目中配置的環(huán)境變量
require('../config/env');
// 校驗 typescript 的配置
verifyTypeScriptSetup();
...
// 校驗入口文件跟入口 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
命令其實就是利用 webpack-dev-server
開啟了一個 webpack
服務(wù)野建。(對 webpack
不熟的童鞋属划,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場戀愛吧:https://www.lanqiao.cn/courses/2893)
build 命令
build
命令就不用說了,直接就是 webpack
的打包操作候生,比如我們在 react-demo1
目錄下執(zhí)行 build
命令:
npm run build
可以看到同眯,在 react-demo1/build
目錄中輸出了 webpack
打包過后的結(jié)果。
start
跟 build
都是利用的 webpack
進(jìn)行編譯打包操作的唯鸭,只是環(huán)境不同 webpack
的配置也會不同嗽测,下面我們重點(diǎn)看一下在 development
模式與 production
模式中,React
腳手架對 webpack
的配置肿孵。
React 項目中的 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é)限制唠粥,小于這個限制會打包成 base64 字符串,超出這個限制會導(dǎo)出文件
// 主要是指對 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(會生成內(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'),
{
// 自動添加樣式兼容前綴
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
postcssNormalize(),
],
},
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
// 添加根路徑解析 loader蛉腌,默認(rèn)指向項目 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
? [
// 測試環(huán)境并且允許熱載刷新頁面的時候
// 加載熱載刷新入口文件
webpackDevClientEntry,
// 加載項目入口文件(默認(rèn) src/index.js)
paths.appIndexJs,
]
: paths.appIndexJs,
// 輸出文件設(shè)置
output: {
// 輸出目錄(默認(rèn)是項目的 build 目錄)
path: isEnvProduction ? paths.appBuild : undefined,
// 開發(fā)環(huán)境打開模塊的 pathinfo 路徑提示
pathinfo: isEnvDevelopment,
// 輸出文 chunk官份、assets 名稱設(shè)置
filename: isEnvProduction
...
};
太多了只厘,就不一一分析了,小伙伴自己看一下源碼文件哦(對 webpack
不熟的童鞋舅巷,強(qiáng)烈推薦我之前寫的文章 來和 webpack 談場戀愛吧:https://www.lanqiao.cn/courses/2893)羔味。
那有小伙伴要問了,既然 React
腳手架幫我們內(nèi)置了 webpack
的配置赋元,如果我們需要自己修改 webpack
的一些配置該咋辦呢飒房?
比如我們需要修改以下配置:
修改輸出的目錄
從源碼中我們可以知道狠毯,目前項目的輸出文件的目錄為 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)的時候锨亏,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
我們該怎么定義呢萌丈?
-
利用
cross-env
庫辆雾,在執(zhí)行命令的時候聲明process.env.BUILD_PATH
變量度迂。我們首先在
react-demo1
項目根目錄安裝cross-env
:yarn add -D cross-env
接著修改一下
package.json
中的build
命令:"scripts": { "build": "cross-env BUILD_PATH=dist react-scripts build", ... }
修改完畢后重新打包測試:
npm run build
可以看到,打包輸出的目錄變成了 dist
坛梁。
-
利用腳手架提供的環(huán)境變量文件
.env.[NODE_ENV].[local]
來修改划咐,其中NODE_ENV
跟local
可選,表示根據(jù)環(huán)境來加載丈莺。我們在
react-demo1
項目根目錄底下創(chuàng)建一個.env
文件缔俄,這樣不管是development
模式還是production
模式,都會加載.env
文件中聲明的變量:touch ./.env
然后在
.env
文件中聲明BUILD_PATH
變量為dist
:## 修改項目的輸出路徑 BUILD_PATH=dist
修改完畢后重新打包測試蟹略,效果跟上面的一樣挖炬,我就不演示了意敛。
總結(jié)
這一節(jié)我們主要介紹了 React
官方提供的腳手架 create-react-app
膛虫,我們直接從源碼的角度來分析了一個 React
項目創(chuàng)建的過程稍刀,其實無非就是對 Webpack
的一些配置而已,所以對 Webpack
不熟悉的小伙伴一定要加油補(bǔ)上哦综膀,從create-react-app
官方文檔上看僧须,并沒有提供 .env
配置文件的說明项炼、怎么去修改 webpack
配置說明等等,還是需要你自己去看源碼的暂论,所以這就是看源碼的重要性取胎,其實從源碼中我們可以知道,并不是所有的 webpack 配置都能修改的匪傍,那項目中我們又需要修改的話役衡,該怎么辦呢薪棒?那就只能拋棄腳手架了俐芯,所以這也算是 React
腳手架的一些不足吧,并沒有像 vue-cli
一樣邮辽,可以隨意修改 webpack
的配置逆巍。
ok锐极,后面我將會帶大家脫離腳手架芳肌,利用 webpack
從 0
開始搭建一個 React
項目亿笤,大家敬請期待吧!