Как построить свои собственные леса (часть 2)

Node.js
Как построить свои собственные леса (часть 2)

предисловие

Я уже писал немного об основах.строительные леса, оглядываться@vue/cliИсходный код, должен сказать, действительно очень формален и стандартизирован. Обратитесь к некоторым функциям @vue/cli и измените скаффолдинг.

эффект каштана

Структура проекта

проектchuhc-reactСтруктура разделена на четыре части:

  • @chuhc/cliФормируйте содержимое командной строки, инициализируйте проект с помощью команд и т. д.
  • @chuhc/scriptsПроект компилирует и запускает упакованное содержимое и еще не подключен к процессу развертывания.
  • @chuhc/templateфайл шаблона.
  • @chuhc/plugin-xxxПлагины, упакованные в проект, такие как@chuhc/plugin-typescriptи т.п.

cli

непосредственно черезinquirerНекоторые интерактивные команды , после получения информации, проходятdownload-git-repoсоответствоватьgithubВытащить файл шаблона относительно просто, но если шаблонов несколько, обслуживание будет затруднено.

пока в@vue/cliЗдесь он объединяется в общий шаблон с помощью нескольких плагинов.Во многих корневых каталогах скаффолдинга естьxxx.config.jsнезащищенный. затем работаетnodeПри командовании читать конфигурационный файл и выполнять соответствующие операции в соответствии с содержимым конфигурационного файла, например: использоватьwebpack-chainДинамическая модификацияconfig, и, наконец, вызовtoConfigгенерировать новыеwebpackНастроить контент.

Оценка подключаемого модуля, создание пакета

базовыйpackage.jsonШаблоны, кроме обычных константversion,private,licenseподожди, какname,scripts,dependencies,devDependenciesНам нужно добавить его вручную.

nameПросто используйте скаффолдинг для инициализации входящих параметров иscriptsуспешно внедряет@chuhc/scripts, используйте его для запуска команды.

как некоторые обязательные, например.react,react-dom, мы можем прямо положитьdependenciesв, покаdevDependenciesКак правило, он выбирается пользователем вручную во время инициализации.plugins.

с помощьюinquirerПерейдите к списку плагинов и позвольте пользователю выбрать, какие плагины импортировать.

const CHUHC_PLUGIN_CHECK = [
  {
    name: 'Typescript',
    value: ['tsx', '@chuhc/plugin-typescript'],
  },
  {
    name: 'Less',
    value: ['less', '@chuhc/plugin-less'],
  },
];

const { plugins } = await inquirer.prompt([
  {
    type: 'checkbox',
    name: 'plugins',
    message: 'Do you need these plugins',
    choices: CHUHC_PLUGIN_CHECK, // 一个结构,自己觉得怎么处理方便,怎么来
  },
]);

Тогда в соответствии с вышеизложеннымcodeмогут получить то, что нужно пользователямpluginТак что вы можете поставить этиpluginвставитьjsonизdevDependenciesвнутри.

Получить последнюю версию зависимостей

любой из вышеперечисленныхreactещеplugin, Нам нужен номер версии, я здесь, чтобы использовать командную строку, чтобы получить последнюю версию, а затем как ееvalueстоимость. Если вы перемещаетесь напрямую, чтобы запуститьexecSyncбудет блокировать,oraизloadingТоже застрял, так что выбирайpromiseпробежатьexecперезвонить до концаpromise.

const forEachSetV = (list, obj, key) => {
  const promises = [];
  const manager = hasCnpm() ? 'cnpm' : 'npm'; // 判断选择cnpm还是npm

  list.forEach(item => {
    if (typeof item === 'object') {
      return forEachSetV(item, obj, key);
    }

    const newPromise = new Promise(res => {
      exec(`${manager} view ${item} version`, (err, stdout, stderr) => {
        obj[key][item] = stdout.slice(0, stdout.length - 1);
        res(0);
      });
    });

    promises.push(newPromise);
  });

  return promises;
};

const promise = [
  ...forEachSetV(depe, pkg, 'dependencies'),
  ...forEachSetV(devD, pkg, 'devDependencies'),
];
await Promise.all(promise);

Затем, получив номер версии, заполните остальные данные вместе вjson, используйте его какpackage.jsonvalue в новом каталоге проекта создайте его.

const fs = require('fs-extra'); // fs-extra是系统fs模块的扩展
const path = require('path');

module.exports = (dir, files) => {
  Object.keys(files).forEach(name => {
    const pathName = path.join(dir, name);
    fs.ensureDirSync(path.dirname(pathName)); // 如果没有文件夹则新建文件夹
    fs.writeFileSync(pathName, files[name]); // 新建文件
  });
};

writeFileTree(targetDir, {
  'package.json': JSON.stringify(pkg, null, 2),
});

Выберите инструмент управления пакетами

потому чтоnpmСкорость не идеальна и может рассматриваться как практический результат. Сначала определите, имеет ли текущая средаyarn,cnpmИ так далее, а затем приоритеты выбирают первое, если нет, то повторно используйтеnpmработать.

const PM_CONFIG = {
  npm: {
    install: ['install', '--loglevel', 'error'], // 打印error信息
    remove: ['uninstall', '--loglevel', 'error'],
  },
  yarn: {
    install: [],
    remove: ['remove'],
  },
};
PM_CONFIG.cnpm = PM_CONFIG.npm;

module.exports = class PackageManager {
  constructor({ pkgName }) {
    this.pkgName = pkgName;

    if (hasYarn()) {
      this.bin = 'yarn';
    } else if (hasCnpm()) {
      this.bin = 'cnpm';
    } else {
      this.bin = 'npm';
    }
  }

  // 封装了下运行命令函数
  runCommand(command, args = []) {
    const _commands = [this.bin, ...PM_CONFIG[this.bin][command], ...args];
    execSync(_commands.join(' '), { stdio: [0, 1, 2] });
  }

  install() {
    try {
      this.runCommand('install', ['--offline']); // offline指先去拉取缓存区里的,如果没有则去服务器拉
    } catch (e) {
      this.runCommand('install'); // 报错兜底
    }
  }

  git() {
    try {
      execSync('git init');
      return true;
    } catch (e) {
      return false;
    }
  }
};

судитьyarnа такжеcnpmО том, существует ли он в окружающей среде, можно судить поversionПодождите, пока метод увидит, может ли он быть успешно выполнен, если он успешно выполнен, это означает, что он существует в среде, в противном случае - нет.

const judgeEnv = name => {
  const envKey = `_has${name[0].toUpperCase()}${name.slice(1)}`; // 保存下结果

  if (_env[envKey] !== null) {
    return _env[envKey];
  }

  try {
    execSync(`${name} --version`, { stdio: 'ignore' }); // 不打印信息

    return (_env[envKey] = true);
  } catch (e) {
    return (_env[envKey] = false);
  }
};

const hasYarn = judgeEnv.bind(this, 'yarn');

const hasCnpm = judgeEnv.bind(this, 'cnpm');

затем пройтиinstallСпособ установки зависимостей, а затем пройти несколько параметров@chuhc/templateИдите и скопируйте несколько основных шаблонов.

scripts

Поскольку это многостраничный проект,scriptsВ основном он делает следующие вещи:

  • пройти черезglobчтобы соответствовать записи, затем используйте ее какentryДинамическое входящее и динамическое входящее несколькоhtml-webpack-pluginДатьplugins.
  • Читая корневой каталог проектаchuhc.config.jsфайл для динамического измененияwebpackНастройте контент и вызовите соответствующий плагин.
  • наконец сгенерировать окончательныйwebpackконфигурационный файл, переданныйwebpackПерейдите к компиляции, запуску, упаковке и т. д.

вход в матч

Соответствующая запись в основном используетсяglobЧтобы соответствовать, только если требования соответствия будут выполнены, он будет использоваться в качестве записи. Затем через согласованную информацию, чтобы сгенерировать соответствующийentryсодержание иpluginсодержание, переданноеwebpackконфигурационный файл.

const SRC = './src/**/index.?(js|jsx|ts|tsx)'; 
/**
 * get webpack entry
 */
const getEntries = () => {
  if (entries) return entries;
  entries = {};

  const pages = glob.sync(SRC);
  pages.forEach(page => { // 遍历传entry
    const dirname = path.dirname(page);
    const entry = path.basename(dirname);
    entries[entry] = page;
  });
  return entries;
};

/**
 * get pages info
 * @param {Boolean} isProd
 */
const getPages = isProd => {
  const plugins = [];
  let entries = getEntries();

  Object.keys(entries).map(dirname => { // 遍历传plugin
    plugins.push(
      new HtmlWebpackPlugin({
        chunks: isProd ? ['libs', dirname] : [dirname],
        filename: `./${dirname}/index.html`,
        template: path.join(__dirname, './template/index.html')
      })
    );
  });

  return plugins;
};

цепочка конфигурации конфигурации

Рекомендуется цепная конфигурацияwebpack-chain,@vue/cliтакже использовать его. Поскольку у нас уже есть некоторое базовое содержимое конфигурации, мы можем передатьconfig.mergeОбъедините наш существующий объект конфигурации с экземпляром конфигурации.

Однако прямое преобразование не поддерживается, и нам необходимо вручную преобразовать некоторый контент конфигурации, например:module. а такжеpluginsНе поддерживатьnewизplugin, моя обработка здесь заключается в том, чтобы пропустить вправоplugin, и, наконец, используйтеwebpack-mergeБудуconfig.toConfig()а такжеpluginsА затем объединены в окончательный объект конфигурации.

const Config = require('webpack-chain');
const chuhcConfig = require(`${process.cwd()}/chuhc.config.js`); // 读取根目录的配置文件
const { setBaseConfig } = require('./util/merge'); // 将已有的配置文件对象合并到配置实例
const BASE = require('./config/webpack.base'); // 配置对象base
const DEVE = merge(BASE, require('./config/webpack.deve')); // 配置对象 deve
const PROD = merge(BASE, require('./config/webpack.prod')); // 配置对象 prod

const config = new Config();

// 我这边就只是对plugin做一下处理,可以做其他很多事情,这里只是举个例子
const handleChuhcConfig = ({ plugins } = {}) => {
  // to do sth.
  if (plugins) {
    plugins.forEach(plugin => {
      require(plugin[0])(config, plugin[1]);
    });
  }
};

const getConfig = isDeve => {
  config.clear(); // 清除配置

  setBaseConfig(isDeve ? DEVE : PROD, config);
  handleChuhcConfig(chuhcConfig);

  return merge(config.toConfig(), {
    plugins: isDeve ? DEVE.plugins : PROD.plugins
  }); // 最后再合并
};

скомпилировать и запустить

в полученииwebpack config, то он может быть основан наdevкоманда все ещеbuildкоманду, чтобы вызвать соответствующую функцию, скомпилировать, запустить, упаковать и так далее. (Аналогично, согласноprogram.command)

// dev 运行
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const { getDeveConfig } = require('./config');

module.exports = () => {
  const DEVE_CONFIG = getDeveConfig();
  const { devServer } = DEVE_CONFIG;
  const compiler = webpack(DEVE_CONFIG);
  const server = new WebpackDevServer(compiler, { ...devServer });
  server.listen(devServer.port);
};
// build
const webpack = require('webpack');
const { getProdConfig } = require('./config');
const logSymbols = require('log-symbols');

module.exports = () => {
  const PROD_CONFIG = getProdConfig();
  const compiler = webpack(PROD_CONFIG);

  compiler.run((err, stats) => {
    if (err) {
      // 回调中接收错误信息。
      console.error(err);
    } else {
      console.log(logSymbols.success, '打包成功!');
    }
  });
};

template

templateВ основном через входящие параметры, чтобы определить, следует лиcopyсоответствующий файл, и в то же время согласноoptionsПриходите и измените содержимое и суффикс соответствующего файла. Код слишком скучный, поэтому я не буду его публиковать.

plugin-xx

pluginЕсли это так, есть еще много вещей, которые можно сделать, я здесь только для того, чтобы изменить цепочкуwebpackинформация о конфигурации. Это только одна из функций, их гораздо больше, например: напишите самиwebpack plugin / loaderПроходить, делать что-то другое.

// example
module.exports = config => {
  ['.tsx', '.ts'].forEach(item => config.resolve.extensions.add(item));

  config.module
    .rule('js')
    .test(/\.(js|ts|tsx|jsx)$/)
    .use('babel-loader')
    .tap(options => {
      options.presets.push('@babel/preset-typescript');
      return options;
    });
};

github

полный код

PS: Начиная с сегодняшнего дня, продолжайте учиться. Еще недавно ничего не хотелось делать, каждый день после работы хотелось только лечь и растеряться.