在《使用Webpack等搭建一个适用于React项目的脚手架(4 - 优化)》中记录Webpack打包的一点优化,到上篇文章为止,模板就已经搭建好了,这篇文章主要记录实现一个简单的脚手架。
准备实现一个特别简单的功能:在执行create-app myProject
时,会将前几篇文章中搭建好的模板代码下载到本地,并执行npm i
安装依赖,安装好依赖后,给用户展示提示内容。
参考写一个自用的前端脚手架选用npm包:
commander是用于命令行界面的完整的解决方案。
chalk 用于美化终端输出的字符串。
ora用于展示加载效果。
download-git-repo用于下载一个git仓库。
fs-extra添加了fs没有的文件系统的方法,并且支持Promise。
1.初始化项目
npm init -y
初始化项目,以上代码会生成一个package.json文件,根据需要调整文件内容如下:
{
"name": "@lana-rm/create-app",
"version": "0.0.1",
"private": true,
"description": "a simple scaffold for creating react app.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["react"],
"author": "任沫",
"license": "ISC"
}
2.创建忽略文件.gitignore
.DS_Store
.vscode
node_modules
3.设置package.json中的bin
"bin": {
"create-app": "bin/main.js"
},
当在命令行中执行create-app时,会执行bin/main.js文件。
4.安装依赖
npm i --save commander download-git-repo fs-extra chalk ora
5.根据文章开头的计划编写main.js文件:
#!/usr/bin/env node
const commander = require('commander');
const download = require('download-git-repo');
const fs = require('fs-extra');
const chalk = require('chalk');
const ora = require('ora');
const { exec } = require('child_process');
const currentWorkingDirectory = process.cwd();
commander.parse(process.argv);
const processArgvOptions = process.argv.slice(2);
if (!processArgvOptions.length) {
commander.outputHelp();
return;
}
const projectName = processArgvOptions[0];
const spinner = ora('downloading code and installing dependencies...').start();
main();
async function main () {
try {
const repository = 'direct:https://github.com/renmo/simple-scaffold.git#master';
const destination = `${currentWorkingDirectory}/${projectName}`;
const downloadOptions = { clone: true };
await fs.emptyDir(destination);
await downloadCode(repository, destination, downloadOptions);
changePackageName(projectName);
const command = 'npm i';
await execFunc(command);
spinner.succeed('succeed');
const promptText = `
${chalk.blue(`cd ${projectName}`)}
start development from ${chalk.blue('npm start')}
`;
console.log(promptText);
} catch (error) {
spinner.fail('failed');
}
}
async function changePackageName (projectName) {
try {
const packageJson = await fs.readJson(`${currentWorkingDirectory}/package.json`);
packageJson.name = projectName;
await fs.writeJson('./package.json', packageJson);
} catch (err) {
console.log(chalk.red(err));
}
}
function downloadCode(repository, destination, options) {
return new Promise((resolve, reject) => {
download(repository, destination, options, function (err) {
if (err) {
reject(err);
console.log(chalk.red(err));
return;
}
resolve();
})
});
}
function execFunc (command) {
return new Promise((resolve, reject) => {
exec(command, (err) => {
if (err) {
reject(err);
console.log(chalk.red(err));
return;
}
resolve();
});
});
}
#!/usr/bin/env node
表示该文件运行在node环境。
6.本地测试的时候执行npm link
建立链接。
在项目根目录下执行npm link,npm link 会在全局的文件夹({prefix}/lib/node_modules/
)和npm link命令执行的包(本文中的包指的是@lana-rm/create-app项目)之间建立链接。它也会链接执行的包中的任何bins到目录({prefix}/bin/{name}
)。这就相当于我们全局安装了我们的包,并且我们可以使用包里面的bins,也就是我们在package.json中定义的bin。(如果想要断开链接,使用npm unlink
。)
7.发布包
npm login
登录, npm publish --access public
发布。
8.测试
全局安装包:
npm i @lana-rm/create-app@0.0.4 -g --force
因为之前使用了npm link,所以直接使用npm i @lana-rm/create-app@0.0.4 -g
会报错文件已存在,根据报错提示使用--force
强制覆盖一下。
执行命令:
create-app a
创建一个名为a的项目,cd a && npm start
在浏览器中查看效果。
源码地址
更新
2020-10-25
1.在tsconfig.json
中加上:
{
"compilerOptions": {
"target": "ES2020",
...
},
...
}
使TypeScript能够识别ES2020语法。
2.今天更新了下npm包,在npm start
运行项目的时候报错:Uncaught ReferenceError: regeneratorRuntime is not defined
。在使用async
的地方需要一个全局的regeneratorRuntime
,但是没有定义这个regeneratorRuntime
。
安装@babel/plugin-transform-runtime解决这个问题。
npm install --save-dev @babel/plugin-transform-runtime
在.babelrc.js
中加上插件:
module.exports = {
...
plugins: ["@babel/plugin-transform-runtime"],
};
重新执行npm start
项目就能正常跑起来了。
3.安装@babel/plugin-proposal-class-properties插件,支持像下面这样属性初始化、私有属性、静态属性等仅在ECMAScript提案阶段的属性语法:
class A {
m1 = "a";
#m2 = 1;
static m3 = "b";
m4 = () => {
return this.#m2 + 1;
};
}
安装插件:
npm install --save-dev @babel/plugin-proposal-class-properties
在.babelrc.js
加上:
module.exports = {
...
plugins: [
...
["@babel/plugin-proposal-class-properties", { "loose": true }],
],
};
4.安装@babel/plugin-proposal-decorators支持装饰器语法。
@decoratorB
class B { }
function decoratorB(target) {
target.m1 = 1;
}
安装插件:
npm install --save-dev @babel/plugin-proposal-decorators
在.babelrc.js
加上:
["@babel/plugin-proposal-decorators", { "legacy": true }],
要注意@babel/plugin-proposal-decorators
要放在@babel/plugin-proposal-class-properties
前面,并且在当使用legacy: true
模式的时候,@babel/plugin-proposal-class-properties
一定要使用loose
模式来支持@babel/plugin-proposal-decorators
。
所以现在.babelrc.js
是这样的:
module.exports = {
presets: [
...
],
plugins: [
"@babel/plugin-transform-runtime",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
],
};
legacy
表示使用旧版(stage 1,第一阶段的)装饰器语法。这里简单说一下,根据ECMAScript规范对JavaScript语言进行规划的机构——TC39(Technical Committee number 39,技术委员会编号39)在确定标准的过程中会有以下几个阶段,stage0 ~ stage4,直到s tage4才会准备好下个ECMAScript版本要添加的特性。
5.在tsconfig.json
中使用"experimentalDecorators": true
识别装饰器语法:
"compilerOptions": {
...
"experimentalDecorators": true
}
6.注释Header组件
因为放在示例代码中获取用户信息的请求地址是不存在的,所以在执行npm start
将项目跑起来的时候会报错。直接在App.tsx中把用到该请求的组件Header注释掉。
// import Header from '@/components/Header/Header';
...
{/* <Header /> */}
如果想要观察请求再把这个注释放开。
7.不知道从哪步起出了问题,发现使用import axios from 'axios'
的时候报错:
Cannot find module 'axios' or its corresponding type declarations.
根据这个回答在tsconfig.json
中加上"moduleResolution": "node"
解决了问题。
8.默认引用index
文件的方式不起作用了
import api from '@/request/api';
要换成:
import api from '@/request/api/index';
9.tsconfig.json
中加上 "module": "ES2020",
否则src/router/index.ts
中动态引入模块的时候会报错:
Dynamic imports are only supported when the '--module' flag is set to 'es2020', 'esnext', 'commonjs', 'amd', 'system', or 'umd'.