作一個基於react-scripts的腳手架

前言

create-react-app做爲facebook官方的react腳手架是至關好用的。主要設計原理是將配置好的如Webpack,Babel,ESLint,合併到react-scripts這npm包中,用戶就能夠開箱即用。不少開發者都在這基礎上進行改造開發。注意react-scripts就是create-react-app腳手架的核心配置代碼。css

目前若是要本身定製配置,有兩種方案可選。一個是eject,他的原理是將react-scripts拆除而後將配置暴露到應用頂層,用戶就能夠自行進行配置。另外一個是使用react-app-rewired,用戶經過config-overrides.js增長修改配置。二者各有好處。eject直接暴露能夠自行配置,可是壞處就是react-scripts被解散了,就不能隨官方配置進行升級。react-scripts包攬了那些最基礎配置的髒活累活,而且一直再維護,好比修復BUG和打包優化,運行速度優化。前端發展的迅速,這些基礎配置隨着基礎設施的升級,可能隨時都會變化。我以爲eject後要就須要承擔維護成本的風險。個人理念是將專業的事情交給專業的人去作就行了,咱們應該享受金字塔底層帶來的基礎設施便利去創造價值,不必重複造輪子,更不必在輪子上耗費過多的維護成本。前端

個人理念是推薦使用config-overrides.js來定製配置,下降維護成本。也就是在react-scripts的配置上進行增刪改查,不影響底層配置代碼,在將來須要的時候還能夠進行無縫升級react-scripts,來提高速度或者解決你未關注到的BUG等等。可是create-react-app只是提供最最基礎的設施建設,咱們最經常使用的框架配置都須要本身去定製,每次建立項目的時候都須要再寫一次定製代碼,至關煩人。因此纔有了今天的主題基於create-react-app的腳手架,確切說應該是基於react-scripts的腳手架。node

因此這篇文章主題應該有兩個react

  • 怎麼製做CLI工具
  • 怎麼根據react-scripts來寫腳手架

項目核心代碼在github上:(github.com/LinYouYuan/…),這個連接上面也有使用幫助說明,能夠先點擊進去看,能夠更好的理解使用和需求。webpack

項目核心需求

咱們需求是:ios

  1. 保證基礎依賴和官方同步;
  2. 建立時增長經常使用框架選擇;
  3. 建立項目後配置項可定製;

第一點,咱們須要引入react-scriptsreact-app-rewired,來保持官方同步和可定製型。git

第二點,我整理出咱們經常使用的框架可選項:github

類型 可選框架名稱
語言 JavaScript / TypeScript
狀態管理庫 Redux / Mobx
css預處理器 SCSS / LESS / styled-components
UI組件 Antd / Ant-mobile
代碼規範 Airbnb
HTTP庫 Axios
路由 react-router

第三點,創項目後咱們能夠經過config-overrides.js文件來預先配置,而後用戶能夠再此文件進行繼續配置和改造。web

製做CLI工具

引入經常使用工具包

首先建立nodejs項目。製做經常使用的Cli工具,咱們通常都須要安裝下面5個工具包:(執行npm install或者其餘工具安裝)npm

  • commander: 用來接收輸入命令參數,而後處理事件;
  • execa: 用來執行操做命令,一個更好的child_process
  • inquirer: 這是建立cli最主要的工具,能夠生成很是美觀的命令行界面;
  • chalk: 能夠修改字體顏色;
  • fs-extra: 比原生fs更好用的fs;

建立全局使用

咱們首先要建立一個像creact-react-app同樣直接在全局就能夠執行使用的命令。

  1. 咱們在根目錄下建立文件夾和文件lib/index.js,這個其實就是入口執行文件。其中#!/usr/bin/env node必定要填寫。

lib/index.js

#!/usr/bin/env node
console.log('hello world')
複製代碼
  1. 而後在package.json中添加代碼,以下,其中react-cli就是全局要使用的命令名稱,lib/index.js就是上面要執行的文件地址。

package.json

"bin": {
    "react-cli": "lib/index.js"
}
複製代碼
  1. 執行npm link。執行完成後,咱們就能夠把命令掛載到全局,效果和npm install -g後同樣,能夠全局輸入命令。link的主要目的是給我開發調試用的。如今能夠直接在控制檯輸入react-cli執行,你就能夠看到打印的hello world了。

  2. 等開發完成,你能夠試試發佈到npm包上,可是我推薦等開發完成後再發布,固然不妨礙你好奇心想試試。發佈前須要執行npm login,登陸npm帳號密碼,注意你若是是淘寶源你須要經過npm config set registry http://registry.npm.tongdun.cn暫時切回官方源。而後執行npm publish發佈,這個時候也要注意,你的package.json中的name也就是項目名稱不要和別人重名了。發佈好你就能夠經過npm i <you project name> -g來全局安裝你的包。

命令管理

lib/index.js中,咱們輸入以下

const program = require('commander');
const chalk = require("chalk");

program
  .version(require('../package').version)
  .usage('<command> [options]');

program
  .command('create <app-name>')
  .description('create a new project powered by react-cli')
  .action(name => {
    // 這裏處理邏輯
    console.log(chalk.blue(`React CLI v${require('../package').version}`));
    // const create = require('./cli/create');
    // create(name);
  });
複製代碼

這裏主要經過commander來配置接受不一樣命令處理。這裏主要就是要接受create <app-name>參數,而後處理輸入命令後的邏輯。其中chalk就是顏色處理。

而後繼續處理未輸入和輸入錯時候彈出幫助以下

program
  .arguments('<command>')
  .action((cmd) => {
    program.outputHelp()
    console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
    console.log()
  })

program.parse(process.argv);

if (!program.args.length) {
  program.outputHelp();
}
複製代碼

交互界面

接收到用戶輸入的命令後,咱們就要呈現交互界面,這個時候咱們就用到了很是好用的工具inquirer。具體能夠實現多少種交互形式能夠點inquirer的npm網站的介紹看。我這裏主要用了listconfirm的功能,也就是列表選擇和尋問功能。好比讓用戶選擇使用什麼框架:

function selectManually(appName) {
  inquirer
    .prompt([
      {
        type: 'list',
        name: 'language',
        message: 'pick a language:',
        choices: [
          'JavaScript',
          'TypeScript',
        ]
      },
      {
        type: 'list',
        name: 'stateManagement',
        message: 'Pick a state management:',
        choices: [
          'Mobx',
          'Redux',
        ]
      },
      {
        type: 'list',
        name: 'cssPre',
        message: 'Pick a CSS pre-processor:',
        choices: [
          'LESS',
          'SCSS/SASS',
          'styled-components',
        ]
      },
      {
        type: 'list',
        name: 'design',
        message: 'Pick a UI Design:',
        choices: [
          'Ant Design',
          'Ant Design Mobile',
        ]
      },
    ])
    .then(answers => {
      const creator = new Creator(appName, answers);
      creator.create();
    })
}
複製代碼

建立項目

新建一個Creator類,主要用來建立項目用的,初始化接受兩個參數,一個是項目名稱,一個是用戶選擇的框架。我項目中的模板存放在lib/packages/common-default中。這裏我主要針對各類不一樣的配置,來修改packages.jsonbabelrcconfig-overrides.js文件的內容就行了,而後執行復制操做。

const chalk = require("chalk");

const fs = require("fs-extra");

const path = require("path");

const inquirer = module.require('inquirer');

const {
  getPackageJson,
  writeJsonToApp,
  copyFiles,
  setNewPackageVersion,
  installPackge,
  setUserConfig,
} = require('../packages/common');

class Creator {
  constructor(appName, answers) {
    this.appName = appName;
    this.answers = answers;
    this.appDir = path.resolve(process.cwd(), this.appName);
    this.package = getPackageJson('cli-switch');
    this.babelrc = {
      plugins: [
        [
          "import",
          {
            libraryName: "antd",
            style: true,
          }
        ]
      ]
    }
  }

  async testExistDir() {
    if (fs.existsSync(this.appDir)) {
      const { override } = await inquirer.prompt([
        {
          type: "confirm",
          name: "override",
          message: chalk.red(`directory ${this.appName} exist,override it?`)
        }
      ]);
      if (override) {
        console.log(chalk.green("removing..."));
        fs.removeSync(this.appDir);
        return true;
      } else {
        process.exit(1);
        return false;
      }
    }
    return true;
  }

  async create() {
    const { stateManagement, cssPre, design } = this.answers;

    console.log();

    console.log(`you pick: ${chalk.yellow(`${stateManagement}, ${cssPre}, ${design}, Router, ESLint`)}`);

    console.log();

    const isOk = await this.testExistDir(this.appDir, this.appName);

    if (!isOk) {
      return;
    }

    console.log(`🚀 Invoking generators...`);

    console.log();

    let { dependencies, devDependencies } = this.package;

    switch (stateManagement) {
      case 'Mobx':
        dependencies['mobx'] = '';
        dependencies['mobx-react'] = '';
        break;
      case 'Redux':
        devDependencies['redux-devtools'] = '';
        dependencies['redux'] = '';
        dependencies['react-redux'] = '';
        break;
    }

    switch (design) {
      case 'Ant Design':
        let myTd = this.babelrc.plugins[0][1];
        myTd.libraryDirectory = 'es';
        dependencies['antd'] = '';
        break;
      case 'Ant Design Mobile':
        let myTdw = this.babelrc.plugins[0][1];
        myTdw.libraryName = 'antd-mobile';
        myTdw.style = 'css';
        dependencies['antd-mobile'] = '';
        break;
    }

    switch (cssPre) {
      case 'LESS':
        dependencies['less-loader'] = '';
        devDependencies['react-app-rewire-less-modules'] = '';
        break;
      case 'SCSS/SASS':
        dependencies['node-sass'] = '';
        break;
      case 'styled-components':
        dependencies['styled-components'] = '';
        devDependencies['babel-plugin-styled-components'] = '';
        this.babelrc.plugins.push("babel-plugin-styled-components");
        break;
    }

    fs.mkdirSync(this.appDir);

    this.beginCopy(cssPre === 'LESS');

    writeJsonToApp(this.appDir, '.babelrc', this.babelrc);

    console.log(`📦 Installing additional dependencies...`);

    installPackge(this.appDir);

    setUserConfig({ hasConfig: true, config: this.answers });

    console.log(`🎉 Successfully created project ${chalk.yellow(this.appName)}.`)

    process.exit(1);
  }

  async beginCopy(isLess = false) {
    setNewPackageVersion(this.package.dependencies);
    setNewPackageVersion(this.package.devDependencies);

    this.package.name = this.appName;

    copyFiles(path.join(__filename, '../../packages/common-default'), this.appDir);

    writeJsonToApp(this.appDir, 'package.json', this.package);

    if (!isLess) {
      fs.copySync(path.join(__filename, '../../packages/cli-switch/config-overrides.js'), this.appDir + '/config-overrides.js');
    }

  }
}

module.exports = Creator;
複製代碼

建立好項目後配置

建立好項目只要在config-overrides.js裏配置Webpack devServer jest。能夠在這裏添加自定義的config配置來增長修改loader, plugin, optimization進行配置。webpackMerge使用混入的方式去添加config。

config-overrides.js

const path = require('path');
const webpackMerge = require('@/webpack-merge');

const appSrc = path.join(__dirname, 'src');

SKIP_PREFLIGHT_CHECK = true

const {
  override, addLessLoader, addWebpackAlias, useBabelRc, addDecoratorsLegacy,
} = require('@/customize-cra');

//打包分析
const BundleAnalyzerPlugin = require('@/webpack-bundle-analyzer').BundleAnalyzerPlugin;

// 這裏能夠直接修改 Host 或者 Port
// process.env.HOST = 'localhost.xxxx.com';
// process.env.PORT = 3006;

// 生產環境是否打包 Source Map
process.env.GENERATE_SOURCEMAP = false;

module.exports = {
  // 配置devServer
  devServer: configFunction => (proxy, allowedHost) => {
    proxy = {
      '/mock': {
        // 這裏配置代理服務地址
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: { '^/mock': '' },
      },
    }
    // allowedHost: 添加額外的地址
    const config = configFunction(proxy, allowedHost);
    return config;
  },

  // 配置webpack 
  webpack: (config, env) => {
    // 開發環境
    const isEnvDevelopment = env === 'development';
    // 生產環境
    const isEnvProduction = env === 'production';

    // 經過customize-cra插件覆蓋
    config = override(
      // 配置路徑別名
      addWebpackAlias({ '@': appSrc }),
      // 對Decorators支持
      addDecoratorsLegacy(),
      useBabelRc(),
    )(config, env);

    return webpackMerge(config, {
      // 用戶能夠在這裏添加自定義的config配置 來增長修改loader, plugin, optimization
      plugins: [
        // new BundleAnalyzerPlugin(),
      ],
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendors: { // 基本框架
              chunks: 'all',
              test: /(react|react-dom|react-dom-router|babel-polyfill|mobx|antd)/,
              priority: 100,
              name: 'vendors',
            },
            asyncCommons: { // 其他異步加載包
              chunks: 'async',
              minChunks: 2,
              name: 'async-commons',
              priority: 90,
            },
            commons: { // 其他同步加載包
              chunks: 'all',
              minChunks: 2,
              name: 'commons',
              priority: 80,
            },
            // echartsVendor: { // 異步加載echarts包
            // test: /echarts/,
            // priority: 100, // 高於async-commons優先級
            // name: 'echartsVendor',
            // chunks: 'async'
            // },
          }
        },
      }
    })
  },

  // 配置測試
  jest: config => {
    config.moduleNameMapper = {
      // 同webpack同樣配置別名
      '@/(.*)$': '<rootDir>/src/$1',
    }
    return config;
  },
}
複製代碼

使用簡單演示

用戶第一次建立有兩個選項

  • default (JavaScript, Redux, Antd, Less, Router, ESLint) 默認配置
  • Manually select features 選擇配置

第二次建立的時候會多一個用戶上次選擇過的選項配置config,就像以下進行選擇配置。

演示1

演示2

演示3
相關文章
相關標籤/搜索