从0实现一个简单create-react-app

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
喜欢就支持一下吧
点赞0 分享