1. 准备工作
1.源码调试
// 1. 下载源码
// 2. 执行yarn 按照依赖 创建链接
// 3. .vscode\launch.json中配置
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "create"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
}
// 4. 在package.json中新增脚本
"create": "node ./packages/create-react-app/index.js hello-world",
// 5. 查看源码目录结构 可以发现cra也是使用lerna的monorepo管理的项目
复制代码
2.创建项目
// 使用npx create-react-app my-app创建项目 控制台输出结果如下
// 1. 创建项目 mkdir my-app
Creating a new React app in /Users/xueshuai.liu/my-app.
// 2.初始化npm按照依赖
// cd my-app npm init -y
// yarn add react react-dom react-scripts cra-template
Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
yarn add v1.22.10
[1/4] ? Resolving packages...
[2/4] ? Fetching packages...
[3/4] ? Linking dependencies...
[4/4] ? Building fresh packages...
success Saved lockfile.
├─ cra-template@1.1.2
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
└─ react@17.0.2
info All dependencies
├─ cra-template@1.1.2
├─ immer@8.0.1
├─ react-dev-utils@11.0.4
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
├─ react@17.0.2
└─ scheduler@0.20.2
✨ Done in 20.65s.
// 3.初始化git仓库 git init
Initialized a git repository.
// 4.使用yarn安装模板 cra-template
Installing template dependencies using yarnpkg...
yarn add v1.22.10
[1/4] ? Resolving packages...
[2/4] ? Fetching packages...
[3/4] ? Linking dependencies...
[4/4] ? Building fresh packages...
success Saved lockfile.
success Saved 17 new dependencies.
// 5.安装模版之后将cra-template remove掉
Removing template package using yarnpkg...
yarn remove v1.22.10
[1/2] ? Removing module cra-template...
[2/2] ? Regenerating lockfile and installing missing dependencies...
success Uninstalled packages.
✨ Done in 7.42s.
// 6.创建git的commit
Created git commit.
// 成功
Success! Created my-app at /Users/xueshuai.liu/my-app
Inside that directory, you can run several commands:
// 7. 可以运行的命令 build start
yarn start
Starts the development server.
yarn build
Bundles the app into static files for production.
We suggest that you begin by typing:
cd my-app
yarn start
Happy hacking!
复制代码
3.模拟流程
// 创建目录
mkdir my-app
// 初始化npm
cd my-app && npm init -y
// 安装依赖
yarn add react react-dom react-scripts cra-template
// 初始化仓库
git init
// 安装cra-template 然后remove
复制代码
2.实现自己的简单cra
1. 项目初始化
// 根据源码目录我们主要实现三个包 create-react-app cra-template react-scripts
// 使用lerna初始化 执行yarn安装lerna
lerna init
// 创建项目
lerna create @ishopee/create-react-app
lerna create @ishopee/cra-template
lerna create @ishopee/react-scripts
// 在package.json增加scripts命令用来之后的调试
"create": "node ./packages/create-react-app/index.js"
// 指明workspaces
"workspaces": [
"packages/*"
],
复制代码
2.实现create-react-app包
// 1.在package.json中新增bin命令
"bin": { "cra": "./index.js" }
// 2.在index.js中使用node执行命令 引入createReactApp中的init方法执行
#!/usr/bin/env node
const { init } = require("./createReactApp");
init();
// 3.createReactApp.js中需要实现init方法
async function init() {}
module.exports = { init }
// 4. 实现init方法 1.注册命令 2.执行createApp创建项目
// 为子项目安装依赖
// yarn workspace @ishopee/create-react-app add chalk commander fs-extra cross-spawn
let appName;
async function init() {
// 1.注册命令
new Command("cra") // 注册cra
.arguments("<projectName>") // 参数名projectName是必须的
.action((projectName) => (appName = projectName))
.parse(process.argv)
await createApp(appName);
}
// 2.创建项目
async function createApp(appName) {
const root = path.resolve(appName);
fs.ensureDirSync(appName); // mkdir my-app
console.log(`Creating a new React app in ${chalk.green(root)}.`);
const packageJson = {
name: appName,
version: "0.1.0",
private: true,
};
// 新增package.json文件
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify(packageJson, null, 2)
);
const originalDirectory = process.cwd();
process.chdir(root);
// 执行run方法 主要就是安装四个包
await run(root, appName, originalDirectory);
}
// 3. 执行run方法
async function run(root, appName, originalDirectory) {
// @ishopee/react-scripts因为我们还没有发布包 先使用官方的包
const scriptName = "react-scripts";
const templateName = "cra-template"; // 模版其实是可以配置的
const allDependencies = ["react", "react-dom", scriptName, templateName];
console.log("Installing packages. This might take a couple of minutes.");
console.log(
`Installing ${chalk.cyan("react")}, ${chalk.cyan(
"react-dom"
)}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
);
await install(root, allDependencies);
// 安装完之后 create-react-app包的功能基本就完成了
// 接下来要去react-scripts包去执行相关逻辑
let data = [root, appName, true, originalDirectory, templateName];
let source = `
var init = require('react-scripts/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`;
// 执行node命令 react-script包中scripts目录下的init.js
await executeNodeScript({ cwd: process.cwd() }, data, source);
process.exit(0);
}
// 安装react react-dom react-scripts cra-template
function install(root, allDependencies) {
return new Promise((resolve) => {
const command = "yarnpkg";
// 拼接参数
const args = ["add", "--exact", ...allDependencies, "--cwd", root];
// 执行yarn add spawn跨平台的开启子进程的包
const child = spawn(command, args, { stdio: "inherit" });
child.on("close", resolve);
});
}
async function executeNodeScript({ cwd }, data, source) {
return new Promise((resolve) => {
const child = spawn(
process.execPath,
["-e", source, "--", JSON.stringify(data)],
{ cwd, stdio: "inherit" }
);
child.on("close", resolve);
});
}
module.exports = { init }
// 进行调试
复制代码
3.实现cra-template包
// cra-template中就是一个模板文件夹 将我们平时开发的一个模板搞过来就行了
mkdir template && cd template
// 就是我们平时开发的一个目录结构
touch README.md
// .会被忽略所以这里没有加上.
touch gitignore
// 开发的目录
mkdir src
// 存在index.html文件
mkdir public
// 我们需要在package.json中指定files
"files": [
"template",
"template.json"
],
复制代码
4. 实现react-script包
// react-script是一个比较核心的包 webpack的最佳实践就在这里
// vue-cli中的核心插件包 vue-cli-service有点类似 react-script + cra-template的功能
// react-script分为两部分来实现
// 1.继续create-react-app run方法中install之后的逻辑 执行 scripts中的init.js
// 2.作为命令来使用 react-scripts start (yarn start)
// 1.一些准备工作
// 先安装一些依赖
yarn workspace @ishopee/react-scripts add react react-dom
yarn workspace @ishopee/react-scripts add cross-spawn fs-extra chalk webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin open
// 在package.json文件中配置bin和scripts脚本
"bin": {
"rs": "./bin/react-scripts"
},
"scripts": {
"build": "node ./bin/react-scripts build",
"start": "node ./bin/react-scripts start"
},
复制代码
4.1先实现create-react-app中的后面的逻辑
// 在create-react-app中install安装了依赖之后我们会执行scripts中的init.js方法
// 我们先回顾下上面的流程 在安装完依赖之后 主要做了两件事
// 1.初始化仓库 2.安装模板 cra-template 成功之后remove
// 我们发现这个步骤其实只是修改了package.json的内容
// 增加了cra-template下template.json的一些依赖 增加了一些配置 eslint 等
// 初始化了仓库 做了git commit 删除了cra-template
module.exports = function (
appPath, // path.resolve(appName)
appName, // my-app
verbose,
originalDirectory, // process.cwd()
templateName // cra-template
) {
// 拿到项目中的package.json文件
const appPackage = require(path.join(appPath, "package.json"));
// 先拿到模板中的package.json文件 将package.json的内容做一个合并 merge
const templatePath = path.dirname(
require.resolve(`${templateName}/package.json`, { paths: [appPath] })
);
// 在package.json文件中新增几个命令 start build test eject
appPackage.scripts = Object.assign({
start: "react-scripts start",
build: "react-scripts build",
});
// 其他一系列的设置 eslint config browsers list appPackage.xxx = xx
// 写入package.json文件内容
fs.writeFileSync(
path.join(appPath, "package.json"),
JSON.stringify(appPackage, null, 2) + os.EOL
);
// 拷贝文件 拿到cra-template下的template中的内容
const templateDir = path.join(templatePath, "template");
fs.copySync(templateDir, appPath); // 将template中的内容拷贝过来
// template中的gitignore文件 写入到.gitignore中
const data = fs.readFileSync(path.join(appPath, "gitignore"));
fs.appendFileSync(path.join(appPath, ".gitignore"), data);
fs.unlinkSync(path.join(appPath, "gitignore"));
// 初始化仓库 require("child_process").execSync
execSync("git init", { stdio: "ignore" });
console.log("Initialized a git repository.");
// 安装cra-template中的依赖 template.json
let command = "yarnpkg",
remove = "remove",
args = ["add"];
// args = args.concat(["react", "react-dom"]); // old CRA cli
console.log(`Installing template dependencies using ${command}...`);
// 执行安装命令 主要是处理template.json中的文件
const proc = spawn.sync(command, args, { stdio: "inherit" });
// 移除cra-template
console.log(`Removing template package using ${command}...`);
const proc1 = spawn.sync(command, [remove, templateName], {
stdio: "inherit",
});
// 提交git commit
execSync("git add -A", { stdio: "ignore" });
execSync('git commit -m "Initialize project using Create React App"', {
stdio: "ignore",
});
// 完成
console.log(`Success! Created ${appName} at ${appPath}`);
};
复制代码
4.2发布npm包
// 经过上面的init过程分析 其实没做什么特殊的处理 我们可以先发布测试下
// 1.使用lerna publish 发布
// 我们需要增加license
// Packages @ishopee/cra-template, @ishopee/create-react-app, and @ishopee/react-scripts are missing a license.
// You must sign up for private packages PUT 402 https://registry.npmjs.org/@ishopee%2fcreate-react-app 368ms
// 对于带有私有的(我们可以在npm中增加组织) @ishopee/react-scripts lerna publish
// 每次只能发布一个(我们可以进入到npm包里面先发布在使用lerna publish) npm publish --access public
// 2.我们将create-react-app中的包改为我们自己的测试
// 可以正确的创建项目 但是我们的命令无法执行
复制代码
4.3 实现react-scripts的命令
// 之前我们在package.json文件中增加了 bin的配置 "rs": "./bin/react-scripts"
// 我们先将init.js中增加命令的部分 react-scripts改成我们自己的rs执行bin/react-scripts.js
// react-scripts
#!/usr/bin/env node
const spawn = require("cross-spawn");
const args = process.argv.slice(2); // ['build']
// 执行scripts目录下对应的文件 build.js start.js eject.js test.js
spawn.sync(process.execPath, [require.resolve("../scripts/" + args[0])], {
stdio: "inherit",
});
复制代码
4.3.1 scripts/build.js
// build和start都是webpack相关的 这里cra实现了webpack配置的最佳实践 我们只实现最简单的功能
// build的核心就是执行webpack()
// start的核心就是开一个WebpackDevServer
process.env.NODE_ENV = "production"; // 环境变量设置为production
fs.emptyDirSync(paths.appBuild); // 打包的目录build webpack默认是dist
copyPublicFolder(); // 拷贝public目录
build();
// 打包
function build() {
// config是webpack的配置 执行webpack得到编译对象compiler
const compiler = webpack(config);
compiler.run((err, stats) => {
console.log(err);
console.log(chalk.green("Compiled successfully.\n"));
});
}
// 拷贝文件
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
filter: (file) => file !== paths.appHtml,
});
}
// 定义一些常量配置paths
module.exports = {
appHtml: resolveApp("public/index.html"),
appIndexJs: resolveApp("src/index.js"),
appBuild: resolveApp("build"),
appPublic: resolveApp("public"),
};
复制代码
4.3.2 script/start.js
process.env.NODE_ENV = "development"; // 开发环境
const webpack = require("webpack");
const compiler = webpack(config);
// devServer{}配置
const serverConfig = createDevServerConfig();
// 创建一个webpack-dev-server
const devServer = new WebpackDevServer(compiler, serverConfig);
devServer.listen(DEFAULT_PORT, HOST, (err) => {
if (err) {
return console.log(err);
}
console.log(chalk.cyan("Starting the development server...\n"));
});
复制代码
4.3.3 webpack的简单配置
const paths = require('../config/paths');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
output: {
path: paths.appBuild
},
module:{
rules:[
{
test: /\.(js|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
presets: [
[
require.resolve('babel-preset-react-app')
]
]
}
},
]
},
plugins:[
new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml
})
]
}
}
复制代码
4.3.4本地测试script脚本命令
新建src/index.js和public/index.html
执行命令 npm run build npm run start
重新发布下npm包就可以执行了
复制代码
4.5.5pnp
给大家安利个好东西 pnp
我们知道yarn install之前的流程大概是这样的
1.将依赖包的版本区间解析为某个具体的版本号
2.下载对应版本依赖的 tar 包到本地离线镜像
3.将依赖从离线镜像解压到本地缓存
4.将依赖从缓存拷贝到当前目录的 node_modules 目录
PnP工作原理是作为把依赖从缓存拷贝到 node_modules 的替代方案
使用yarn --pnp开启
复制代码
结语
到此 我们就实现了一个简单的cra 但是功能还是非常的弱
基于学习的考虑 建议大家down下源码进行调试
cra中有webpack的最佳实践
例如
devtool: isEnvProduction
? shouldUseSourceMap ? 'source-map' : false
: isEnvDevelopment && 'cheap-module-source-map'
还有一些其他的知识
react-refresh React开发的模块热替换(HMR)方案 react-hot-loader
babel macro
webpackDevClientEntry react重写webpack-dev-server客户端 热更新
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END