一、聊一聊
这个月的发文频率有点低,主要是这个月的工作确实多且难缠,再加上自己比较懒,所以才会这样。
为什么会想到自己实现一个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的基础包引入进来了,但是如何具体用代码来给项目赋能呢,下面的官网有你要的答案。
因为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
。
至此我们的工具到这里也就结束啦~~
五、最后
好啦,本次在工具链上的探索到这里就结束啦,如果各位在阅读过程中发现有问题的地方,还请在评论区里指出,如果我讲的内容对你有启发,也希望你点赞保存一哈,我们下期再见啦~~