我也实现了一个create-react-app脚手架

一、聊一聊

这个月的发文频率有点低,主要是这个月的工作确实多且难缠,再加上自己比较懒,所以才会这样。

为什么会想到自己实现一个create-react-app?

is so cool。 这个真的是我的第一想法,我也想要拥有自己的工具,并且别人能实现的,我相信我也可以。带着这种在别人看来可能比较狂妄的想法的我,踏上了工具链的这条路。不过也确实应该这样,什么事情先干了再说。

那么接下来,我将会把自己的心路历程写给大家看,希望对你们有帮助。

二、什么是脚手架?

提供基本能力,支持用户在其上进行定制化开发,从而提升用户的开发效率

想到这里的时候,其实我已经知道了答案。那就是 自己搭建一个懒散的项目架子,让别人下载到本地即可

三、如何从0搭建一个React项目?

搭建前端项目,在这个打包工具蠢蠢欲动的时代,我还是选择了webpack。在使用webpack来搭建项目的时候,我当时的脑子里浮现了2个问题:

  • 问题一、如果存在这样的一个项目,我如何让它跑起来呢?
  • 问题二、如何从0搭建项目?

3.1、npx是如何运行的

使用过webpack的小伙伴应该都知道,webpack项目的启动命令大都是下面的模式:

npx webpack serve --config ...

它是如何跑起来的?这个问题真的值得一看。

为了弄懂npx的运行流程,我还特意实践了很多。这里有人会说,这种原理性的问题,百度上应该一抓一大把呀,为什么还要特意实践一番。

其实不瞒大家说,网上确实存在,而且是出乎意料的一致: 先在项目里找,找不到再去$path里面找。这句话是官网给的,但是官网并没有说path是什么,以及是谁的path。

我画了一张经过验证的简图:

对上图的补充描述如下:

  • ${prefix}指的是node的安装目录,可通过npm config get prefix来访问。

3.2、npx 与 npm install 有什么区别?

这点是最容易被混淆的。它俩的区别在于是否真的会将 package 下载到 prefix/lib/node_modules里。这直接影响到你输入的命令是否能被找到。

这一点从 create-react-app 与 @vue/cli 的使用上 最能体现出来。

React官网描述:想要快速搭好react项目,请运行 npx create-react-app project

而vue官网描述是这样的:想要快速搭好vue项目,请依次运行以下命令:

1、npm install @vue/cli -g;

2、vue create project。

重要的事情我就说一遍,也请屏幕前的你跟着做一下实验:

请依次运行以下命令:

1、npx create-react-app p1

2、create-react-app p2

某人疑惑的说:"第二条命令报错了, commond not found : create-react-app "。

请再依次运行以下命令:

1、npm install @vue/cli -g

2、vue create p3

某人激动的说:"vue命令可以跑通了!!!"

其实啊,在终端输入命令,"它"会去${prefix}/bin目录里找对应的软连接,如果找到就执行,如果找不到,就报错"commond not found"。

${prefix}/bin目录里的东西是怎么来的呢?来源有2类:

  • npm install package -g。前提是package下的package.json文件里的bin字段有值。
  • npm link。

搞懂了上面这些,你就算入门脚手架工具了,如果再有关npm的问题,90%你都可以不用百度。剩下的10%如果你不会搜索的话,官网可以给你答案。下面的命令,相信你心中也一定有了答案:

npx @vue/cli create p3

3.3、npm init有什么用?

生成package.json文件,想项目真正成项目。里面的字段我就不详细说明了。

3.4、开始搭建项目

1、首先初始化项目文件夹。 执行命令:npm init -y

2、由于我们使用webpack来搭建项目,并且知道了如何使用webpack来启动一个项目以及它的工作原理,所以我们需要安装webpack、webpack-cli。执行命令:npm install webpack webpack-cli --save

3、接下来就是当我们运行命令的时候,能够自动打开浏览器并加载页面。

Node 可以通过require('child_process').exec('open')来做到的。这意味着我们要开启一个以node来搭建的服务器。乐观的是,webpack-dev-server这个插件已经帮我们实现了这样的功能,而且比我们预想的还要丰富。

4、安装 webpack-dev-server。执行命令:npm install webpack-dev-server --save

5、根据基础已安装的依赖配置项目。

  • 在项目的根目录下分别创建:index.js、build/index.html、config/webpack.dev.js。
  • 编写 webpack.dev.js 文件。文件内容如下:
javascript 复制代码
let path = require('path');

module.exports = {
    entry: path.resolve(__dirname, '../index.js'),
    output: {
        path: path.resolve(__dirname, '../build'),
        filename: '111.js',
        clean: true
    },
    mode: 'development',
    devtool: 'source-map',
    devServer: {
        open: true,
        port: 8001,
        static: {
            directory: path.resolve(__dirname, '../build')
        }
    }
}

6、接下来我们在终端输入:webpack serve --config config/webpack.dev.js。命令运行成功后,我们实现了自动打开浏览器的功能。

因为我们走的是react技术栈,所以当务之急,我们需要把react引进来,并且将项目跑起来。

执行命令:npm install react react-dom

这里我们需要做出一些定义,react组件使用jsx文件,其余跟js相关的则使用js文件。

我们是把react的基础包引入进来了,但是如何具体用代码来给项目赋能呢,下面的官网有你要的答案。

将React添加到已有项目里

因为webpack只识别 js文件,所以我们需要让webpack来识别 jsx文件,如何识别呢?无非就是将jsx转化为js呗,如何做呢?babel给了你答案。如何用呢?webpack又给了你答案。

执行命令:npm install babel-loader @babel/core @babel/preset-react

webpack新增配置如下:

javascript 复制代码
module: {
    rules: [
        {
            test: /\.jsx$/i,
            use: 'babel-loader',
            options: {
                presets: ["@babel/preset-react"]
            }
        }
    ]
}

此时我们的项目就可以顺理成章的跑react了。

7、继续修补项目,增加css的文件处理。

  • 对于css来说,webpack并不识别它,我们可以使用css-loader将 css 转化为 js字符串,再将处理后的字符串动态的插入到style标签里或者动态的link到html文件里。将字符串插入到style标签里,那你就得选 style-loader;将字符串处理成link引用,那你就得用 Mini-css-extract-plugin。但是使用 mini-css-extract-plugin呢,还得搭配一个 html-webpack-plugin 插件才能正常使用。从性能的角度考虑,我决定使用 mini-css-extract-plugin。

执行命令:npm install mini-css-extract-plugin html-webpack-plugin --save-dev

修改webpack.dev.js配置如下:

javascript 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...其余配置不变
    module: {
        rules: [
            // ...其余配置不变
            {
                test: /\.css$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
        // ...其余配置不变
        new MiniCssExtractPlugin(),
        new HtmlWebpackPlugin()
    ]
}

8、继续修补项目,增加对图片的处理。

这是webpack5与之前版本的区别,webpack5已经为我们预制了图片的处理能力,我们只需要把选项配置出来即可,这里我们简单处理:

javascript 复制代码
module.exports = {
    // ...其余配置不变
    module: {
        // ...其余配置不变
        {
            test: /\.(png|jpg)$/i,
            type: 'asset/resource'
        },
    }
}

9、到这里,我们的项目架子就搭建完毕了。但是我们还需要做的就是优化终端输入的命令。

这一点可以在package.json文件里的scripts字段里做些文章。也就是效仿"npm run xxx"。当我们在终端里输入"npm run dev"的时候,希望能够把项目跑起来。

"npm run xxx"运行原理如下:

1、在项目下的package.json文件里找到对应的 scripts 配置。如果找不到,直接报错。

2、如果找到了,就在 scripts["xxx"] 的值前面加上 node_modules/.bin 。

上面说的有点干,可视化可能会更清晰。

基于上面的分析,我们修改package.json文件内容如下:

javascript 复制代码
"scripts": {
    "dev": "webpack serve --config config/webpack.dev.js"
},

至此,我们的简略模版项目搭建完毕。

3.5、将项目升级为脚手架的灵感

"我要做工具"的这个想法在这段时间里非常强烈,但是从现在的表现形式上看,它就是一个模版项目,不具备终端交互的功能,并不符合预期。于是我把目光瞄向了create-react-app这个脚手架工具。

当然了,做工具看起来也更cool。

四、创建lazy-create-react-app

是的,就这样,我也写了一个创建react项目的脚手架,我把它命名为"lazy-create-react-app"。它比较慵懒,主要体现在以下几个方面:

  • 目前只有创建项目的能力。也就是只有一个模版。
  • 创建的项目能力比较基础,也就是只有公共能力。想要增加什么eslint模块等等,都需要自己去install,然后再配置。

其实这也符合我的预期,因为lazy嘛,只提供公共能力(不要过分设计),对程序员来说比较友好,因为它更简单,不用考虑任何的兼容性,想要什么自己干就完了,也因此更对小白友好。

4.1、初始化脚手架

执行 npm init -y

初始化成功后,修改package.json文件如下:

javascript 复制代码
// package.json文件的内容(其他内容保持不变)
name: 'lazy-create-react-app',
main: './index.js',
"bin": {
    "lazy-create-react-app": "./index.js"
},

这里的name与bin字段的值大家可以根据情况自行修改。

4.2、获取终端里输入的项目名称

在脚手架项目的根目录下新增index.js文件,index文件内容如下:

javascript 复制代码
#!/usr/bin/env node
function createProject(){}

createProject();

#!/usr/bin/env node用来告诉程序,这是一个可被node执行的文件。

此时在脚手架项目的根目录下执行 npm link。根据我们上面学到的知识,此时的${prefix}/bin目录下就会多了一个软连接,这个软连接就是package.json里bin字段的值。下面是流程图,供大家理解。

紧接着,我们借助第三方库"commander"来获取终端命令行里的参数。

先执行:npm install commander --save

修改index文件如下:

javascript 复制代码
const { Command } = require('commander');
const path = require('path');
const os = require('os');

const cliPackageJson = require('./package.json');


function createProject(){
    let projectName = null; 
    const project = new Command(cliPackageJson.name)
    .version(cliPackageJson.version)
    .arguments('<project-directory>')
    .action(
        name => {
            // 1、获取到要创建的项目名称
            projectName = name;
            console.log('即将创建的项目名字:', name);
        }
    )
    .parse(process.argv);
 }

此时我们在终端运行:lazy-create-react-app p1,此时我们就会发现index文件被执行了,并且成功的打印出了项目名称p1。

4.3、限定脚手架支持的node版本

我们都知道node版本是字符串,所以如果我们手动处理版本高低的一些判断会比较麻烦。所以在这里就使用了第三方库semver

在这里我们简单的说一下semver

javascript 复制代码
/**
 * semver: 用于专门分析语义化版本的工具
 * 为什么需要它?
 * 
 * 语义化规范(https://semver.org/lang/zh-CN/)是整个前端都在遵循的规范,它可以快速的帮助我们实现语义化上的判断等功能。
 * 
 * 几个用途说明:
 * 
 * 1、根据node版本制定第三方库的兼容性。(比如某些第三方库仅仅在大于 node@12版本 才会生效 ), 这个时候该怎么做呢?
 *     semver.satisfies( process.version, '>=14' );
 * 
 * 2、比较版本号大小
 *     semver.lt('1.2.3', '5.6.7'); // true
 * 
 * 3、验证版本号是否合法
 *     semver.valid('1.2.3');  // true
 *     semver.valid('a.b.c');  // false
 * 
 * 4、提取主、次、修订版本号
 *     semver.major('1.2.3');  // 主1
 *     semver.minor('1.2.3');  // 次2
 *     semver.patch('1.2.3);   // 修订3
 * 
 * 5、定义版本号
 *     semver.coerce('v18.12.1');
 *     // 输出如下:
 *     {
 *         major: 18,
 *         minor: 12,
 *         patch: 1,
 *         version: '18.12.1',
 *         ...
 *     }
 */

了解了什么是semver,我们现在就将它用到项目里吧。

先执行 npm install semver --save

然后,修改index文件如下:

javascript 复制代码
// 引入semver(其他内容不变。。。。。。)
const semver = require('semver');

// 其他内容不变,主要就是修改 createProject 函数
function createProject (){
    // 其余内容不变.......
    const isSupportCurrentNodeVersion = semver.satisfies(
        semver.coerce(process.version),
        '>=10'
    );
    if (!isSupportCurrentNodeVersion){
        console.error(chalk.red('请将node版本升级到v10以上!'));
        process.exit(1);
        return
    }
}

这样我们就实现了锁定node版本的功能。

4.4、检测项目名称是否符合规范

因为我们的包肯定是放在npm等这一类的包管理平台,所以我们可以根据自身情况决定是否要遵循他们的规范。

这里我们以第三方库validate-npm-package-name为例。

先下载第三方库:npm install validate-npm-package-name --save

修改index文件如下:

javascript 复制代码
// 其余文件内容不变......
const validateProjectName = require('validate-npm-package-name');

function createProject(){
    // 其余内容不变......
    const validationResult = validateProjectName(projectName);
    if (!validationResult.validForNewPackages) {
        console.error(chalk.red('您要创建的项目名称不符合npm规范!'));
        process.exit(1);
        return
    }
}

4.5、创建项目目录

这里我们可以使用node自带的文件系统在当前node正在执行的目录下创建projectName目录。当然我们也可以使用其他优秀的第三方模块来完成这一操作。

我们这里以fs-extra模块为例。

先安装这个库:npm install fs-extra --save

然后修改index文件如下:

javascript 复制代码
const fs = require('fs-extra');

function createProject(){
    // 其余文件内容不变......
    
    // 检测在目标目录下,是否存在project-name目录,如果没有就创建目录
    fs.ensureDirSync(projectName);
}

4.6、生成项目的package.json文件

这个该怎么办呢,其实也很好做,步骤如下:

  • 获取到目标项目的绝对路径。
  • 根据绝对路径,将package.json文件内容写入到项目里。

修改index文件内容:

javascript 复制代码
function createProject(){
    // 其余文件内容不变.......
    
    // 获取目标目录位置。
    let targetAbsolutePath = path.resolve(projectName);
    
    // 预制 package.json 模版内容
    const packageJson = {
        name: projectName,
        version: '0.1.0',
        scripts: {
            "dev": "webpack serve --config config/webpack.dev.js"
        },
        dependencies: {
            "mini-css-extract-plugin": "^2.7.6",
            "webpack": "^5.88.2",
            "webpack-cli": "^5.1.4",
            "@babel/core": "^7.22.10",
            "@babel/preset-react": "^7.22.5",
            "babel-loader": "^9.1.3",
            "css-loader": "^6.8.1",
            "html-webpack-plugin": "^5.5.3",
            "react": "^18.2.0",
            "react-dom": "^18.2.0",
            "react-router": "^6.15.0",
            "style-loader": "^3.3.3",
            "webpack-dev-server": "^4.15.1"
        }
    };
    
    // 将预制到package.json模版内容写到目标目录中。
    fs.writeFileSync(
        path.join(targetAbsolutePath, 'package.json'),
        JSON.stringify(packageJson, null, 2) + os.EOL
    );
}

到目前为止,如果我们在终端输入 lazy-create-react-app p1。我们就会在当前目录下看见,我们已经成功的将 p1 创建到 我们的目录里了。

4.7、根据生成的package.json安装项目依赖

我们如何用代码去实现npm install的功能呢?

cross-spawn给了我们答案。

它是用来干什么的呢?一句话总结:

执行命令行功能,并且消除不同平台之间的差异

还是同样的套路,先安装包:npm install cross-spawn --save

然后修改index文件如下:

javascript 复制代码
// 其余文件内容不变......
const spawn = require('cross-spawn');

function createProject(){
    // 其余内容不变......
    
    // 改变当前node命令执行时的目录, 
    // 也就是去 targetAbsolutePath 下执行 下面的 npm install命令
    process.chdir(targetAbsolutePath);
    
    // 预设置依赖
    const allDependencies = [
        'react@18.2.0', 'react-dom@18.2.0', 'mini-css-extract-plugin@2.7.6',
        'webpack@5.88.2', 'webpack-cli@5.1.4', '@babel/core@7.22.10',
        '@babel/preset-react@7.22.5', 'babel-loader@9.1.3', 'css-loader@6.8.1',
        'html-webpack-plugin@5.5.3', 'react-router@6.15.0', 'style-loader@3.3.3',
        'webpack-dev-server@4.15.1'
    ];
    
    // 执行npm install命令
    const child = spawn('npm', ['install', '--save'].concat(allDependencies), { stdio: 'inherit' });

}

4.8、检测安装依赖是否完毕

修改index文件内容如下:

javascript 复制代码
// 其余文件内容不变.......

function createProject(){
    // 其余内容不变.......
    
    // 检测child进程是否关闭
    child.on('close', code => {
        if (code !== 0) {
            console.log('安装依赖过程中报错');
            process.exit(1);
            return;
        }
        console.log('安装依赖成功!');
    });
}

到这里,我们再运行以下命令: lazy-create-react-app p1,运行成功后,此时我们会发现项目p2目录下已经成功的安装了依赖,但是并不能运行。因为我们没给p2设置配置文件。

其实剩下的这个问题我们已经解决了,因为它与package.json文件一样,都是靠写入就可以完成的。

4.9、生成项目的模版内容

  • 在脚手架工具的根目录下创建contentTemplate目录,在这个目录下分别创建index.js(React首页文件内容)、index.css(初始化样式文件)、webpack.dev.js(项目初始化的配置模版)。
  • 分别将上述3种文件的内容写入到目标目录里。

这里我觉得还是有必要说明一下脚手架(lazy-create-react-app)的目录结构:

javascript 复制代码
| - lazy-create-react-app
    | - node_modules
    | - contentTemplate
        | - index.js
        | - index.css
        | - webpack.dev.js
    | - index.js
    | - package.json
    | - package-lock.json

此时我们修改一下index文件内容,来将最后的资源写入到目标目录中。

javascript 复制代码
// 其余文件内容不变.......
function createProject(){
    // 其余文件内容不变.......
    child.on('close', code => {
        if (code !== 0) {
            console.log(chalk.red('安装依赖过程中报错'));
            process.exit(1);
            return;
        }
        console.log(chalk.green('安装依赖成功!'));
        // 写入js模版
        fs.copySync(
            path.resolve(__dirname, './contentTemplate/index.js'),
            path.join(targetAbsolutePath, 'index.jsx')
        );
        // 写入配置模版
        fs.copySync(
            path.resolve(__dirname, './contentTemplate/webpack.dev.js'),
            path.join(targetAbsolutePath, 'config/webpack.dev.js')
        );
        // 写入css模版
        fs.copySync(
            path.resolve(__dirname, './contentTemplate/index.css'),
            path.join(targetAbsolutePath, 'index.css')
        );
        // 模版使用提示
        console.log(`\r\n 创建项目${projectName}成功`);
        console.log(`\r\n cd ${projectName} `);
        console.log(`\r\n ${`npm run dev`} `);

    });
}    

此时我们再运行lazy-create-react-app p1命令,就会发现在当前文件夹下,我们已经成功的创建了p1项目,并且p1项目可以成功的跑起来。

4.10、发布脚手架

离最终的目标npx lazy-create-react-app p1只差最后1公里了,那就是发布我们的工具。

首先,我们需要更新一下npm的镜像源。

npm config set registry https://registry.npmjs.org

其次,我需要添加用户,请执行下面的命令:

npm adduser

这里输入你在npm官网上注册的用户名、密码、邮箱即可。

最后执行npm publish,完成发布功能。

成功发布后,我们就可以在终端输入命令了:npx lazy-create-react-app p3

至此我们的工具到这里也就结束啦~~

五、最后

好啦,本次在工具链上的探索到这里就结束啦,如果各位在阅读过程中发现有问题的地方,还请在评论区里指出,如果我讲的内容对你有启发,也希望你点赞保存一哈,我们下期再见啦~~

相关推荐
前端百草阁18 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜19 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40419 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish20 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple20 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five21 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序21 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54122 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普23 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省23 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript