使用Webpack等搭建一个适用于React项目的脚手架(5 - 脚手架)

939 阅读5分钟

《使用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在浏览器中查看效果。

源码地址

源码地址1-和博客内容对应

源码地址2-创建的项目模版代码

源码地址3-脚手架代码

更新

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'.