webpack 多应用处理方案

方案说明

前端工程多数是使用webpack来开发,那么问题来了,是一个前端工程就得搭建一个webpack项目吗?我是觉得没这必要,因为很多是固定不变的,如编译… 只是前端工程对应的业务不相同而已!

先展示一下打包完毕的内容

kejian
由上图可见,页面工程按模块区分,并放入pages下,最终编译出来对应的目录处于build下,且相关资源也在该目录中,参照下图,看一下other里面包括了什么?
在这里插入图片描述
可以看到有入口文件,切割后(按需加载)的资源js等,这就是一个前端工程该用的资源了!

生产环境

看一下生产环境相关的js,

// 命令
npm run build

script/build.js
config/webpack.config.prod.js

可以进入 webpack.config.prod.js 中看,发现目录相关的配置,如入口文件,打包之后的目录都在里面配置好了!针对的是一对一!
那么可以考虑把以下代码作出相应的调整

// module.exports = { ... } 调整为
module.exports = function (pack) {
	return {
		... //这里就是上面的之前导出的对象
		// 添加 entry, 入口文件
		entry: {
			...
			main: paths.resolveApp(`src/pages/${pack}/index.js`)
		},
		// 修改 output.path
		output: {
			path: paths.resolveApp(`build/${pack}`)
		}
	}
}
// 目的是为了多次获取配置,并根据包名决定编译路径,以完成多个模块的分离打包

可以看见上面的 paths.resolveApp 不存在,进入 config/paths.js 加入下面代码

module.exports = {
	...,
	resolveApp
}

接下来要调整 build.js

'use strict';
/*
	1.先读取命令参数,识别当前要打包哪一模块
	2.模块参数不存在,那么就扫描pages下的目录,并查看该目录结构是否拥有相关文件
	命令: npm run build 打包pages下的所有目录且存在index.js入口文件的模块
	      npm run build name=other 先检查该模块是否合理,只编译该模块
*/

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
  throw err;
});

// Ensure environment variables are read.
require('../config/env');

const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const webpack = require('webpack');
const bfj = require('bfj');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const getConfig = require('../config/webpack.config.prod');
const paths = require('../config/paths');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');

const measureFileSizesBeforeBuild =
  FileSizeReporter.measureFileSizesBeforeBuild;
/* const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile); */

// These sizes are pretty large. We'll warn for bundles exceeding them.
/* const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; */

const isInteractive = process.stdout.isTTY;

// Process CLI arguments
const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;

// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');

packProgress();

// 打包流程
async function packProgress() {
  console.log('Getting package list information...\n');
  // 打包列表
  const packList = [];
  // 一、读取打包模块参数,name={对应src/pages/包名},如果没有name参数,则获取src/pages下拥有index.js入口文件的目录
  //    例如 npm run build name=newB2B
  const packName = process.argv.filter(argv => {
    if(/^name/.test(argv)) return argv;
  })[0];
  if(packName) {
    // 检查包是否正常,1、存在该目录;2、目录下必须有入口文件 - index.js
    const name = packName.replace(/^name=/, '');
    if(!checkRequiredFiles([paths.appHtml, paths.resolveApp(`src/pages/${name}/index.js`)])) {
      console.log(
        chalk.red(
          `\n${packName} does not exist or has no index.js, please check!\n`
        )
      );
      process.exit(1);
    }
    packList.push(name);
  } else {
    // 读取src/pages下的一级目录,且拥有index.js入口文件
    const filePath = paths.resolveApp('src/pages');
    const files = fs.readdirSync(filePath);
    files.forEach((filename) => {
      try {
        const stats = fs.statSync(path.join(filePath, `${filename}/index.js`));
        if(stats.isFile()) packList.push(filename);
      } catch (error) {
        //目录不存在index.js入口,不作打包处理
      }
    });
  }
  // 获取打包列表完毕,开始编译
  for(let i = 0; i < packList.length; i++) {
    await handle(packList[i]);
  }
}

/**
 * 处理包
 * @param {String} name 
 */
function handle(pack) {
  return new Promise((resolve) => {
    const buildPath = paths.resolveApp(`build/${pack}`);
    checkBrowsers(paths.appPath, isInteractive)
    .then(() => {
      // First, read the current file sizes in build directory.
      // This lets us display how much they changed later.
      return measureFileSizesBeforeBuild(buildPath);
    })
    .then(previousFileSizes => {
      // Remove all content but keep the directory so that
      // if you're in it, you don't end up in Trash
      fs.emptyDirSync(buildPath);
      // Merge with the public folder
      fs.copySync(paths.appPublic, buildPath, {
        dereference: true,
        filter: file => file !== paths.appHtml,
      });
      // Start the webpack build
      return build(pack, previousFileSizes);
    })
    .then(
      ({ stats, previousFileSizes, warnings }) => {
        if (warnings.length) {
          console.log(chalk.yellow('Compiled with warnings.\n'));
          console.log(warnings.join('\n\n'));
          console.log(
            '\nSearch for the ' +
              chalk.underline(chalk.yellow('keywords')) +
              ' to learn more about each warning.'
          );
          console.log(
            'To ignore, add ' +
              chalk.cyan('// eslint-disable-next-line') +
              ' to the line before.\n'
          );
        } else {
          console.log(chalk.green('Compiled successfully.\n'));
        }

       /* console.log('File sizes after gzip:\n');
        printFileSizesAfterBuild(
          stats,
          previousFileSizes,
          buildPath,
          WARN_AFTER_BUNDLE_GZIP_SIZE,
          WARN_AFTER_CHUNK_GZIP_SIZE
        );
        console.log(); */

        /* console.log('File after build:\n');
        fileDisplay(buildPath, previousFileSizes.root.substr(0, previousFileSizes.root.lastIndexOf('/') + 1));
        console.log('\n'); */
        console.log('File after build:\n');
        readFile(buildPath, previousFileSizes.root.substr(0, previousFileSizes.root.lastIndexOf('/') + 1));
        console.log('\n');

        /* const appPackage = require(paths.appPackageJson);
        const publicUrl = paths.publicUrl;
        const publicPath = config.output.publicPath;
        const buildFolder = path.relative(process.cwd(), paths.appBuild);
        printHostingInstructions(
          appPackage,
          publicUrl,
          publicPath,
          buildFolder,
          useYarn
        ); */
        resolve();
      },
      err => {
        console.log(chalk.red(`Failed to compile ${pack}.\n`));
        printBuildError(err);
        //process.exit(1);
        resolve();
      }
    )
    .catch(err => {
      if (err && err.message) {
        console.log(err.message);
      }
      resolve();
      //process.exit(1);
    });
  });
}

/**
 * 打包
 * @param {String} pack 
 * @param {String} previousFileSizes 
 */
function build(pack, previousFileSizes) {
  console.log(`Compiling package - ${pack} ...`);

  let compiler = webpack(getConfig(pack));
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      let messages;
      if (err) {
        if (!err.message) {
          return reject(err);
        }
        messages = formatWebpackMessages({
          errors: [err.message],
          warnings: [],
        });
      } else {
        messages = formatWebpackMessages(
          stats.toJson({ all: false, warnings: true, errors: true })
        );
      }
      if (messages.errors.length) {
        // Only keep the first error. Others are often indicative
        // of the same problem, but confuse the reader with noise.
        if (messages.errors.length > 1) {
          messages.errors.length = 1;
        }
        return reject(new Error(messages.errors.join('\n\n')));
      }
      if (
        process.env.CI &&
        (typeof process.env.CI !== 'string' ||
          process.env.CI.toLowerCase() !== 'false') &&
        messages.warnings.length
      ) {
        console.log(
          chalk.yellow(
            '\nTreating warnings as errors because process.env.CI = true.\n' +
              'Most CI servers set it automatically.\n'
          )
        );
        return reject(new Error(messages.warnings.join('\n\n')));
      }

      const resolveArgs = {
        stats,
        previousFileSizes,
        warnings: messages.warnings,
      };
      if (writeStatsJson) {
        return bfj
          .write(paths.resolveApp(`build/${pack}`) + '/bundle-stats.json', stats.toJson())
          .then(() => resolve(resolveArgs))
          .catch(error => reject(new Error(error)));
      }

      return resolve(resolveArgs);
    });
  });
}

/**
 * 输出路径下相关文件大小
 * @param {String} filePath 
 * @param {String} root 
 */
function readFile(filePath, root) {
  const files = fs.readdirSync(filePath);
  files.forEach(file => {
    const states = fs.statSync(filePath + '/' + file);
    if(states.isDirectory()) {
      readFile(filePath + '/' + file, root);
    } else {
      let size = Math.floor(states.size / 1024 * 100) / 100 + ' KB';
      size += ' '.repeat(12 - size.length);
      console.log(
        '  ' +
        size +
        '  ' +
        chalk.dim(filePath.replace(root, '') + path.sep) +
        chalk.cyan(file)
      );
    }
  });
}

开发环境

开发环境区分比较简单,一个服务就是针对一个页面模块!

同样,先调整 webpack.config.dev.js

// 类似上面生产环境的配置
// module.exports = { ... } 调整为
module.exports = function (pack) {
	return {
		... //这里就是上面的之前导出的对象
		// 添加 entry, 入口文件
		entry: {
			...
			main: paths.resolveApp(`src/pages/${pack}/index.js`)
		}
	}
}

由于 webpack.config.dev.js 有被 config/webpackDevServer.config.js 引用,所以需要调整下里面内容

// 注释掉这里
// const config = require('./webpack.config.dev');

// 修改 publicPath
publicPath: '/',

修改 start.js

//const config = require('../config/webpack.config.dev'); 改为
const getConfig = require('../config/webpack.config.dev');

// 加入判断包
// 读取打包模块参数,name={对应src/pages/包名},如果没有name参数,则获取src/pages下首个拥有index.js入口文件的目录
//    例如 npm run start name=other
let packName = process.argv.filter(argv => {
  if(/^name/.test(argv)) return argv;
})[0];
if(packName) {
  // 检查包是否正常,1、存在该目录;2、目录下必须有入口文件 - index.js
  packName = packName.replace(/^name=/, '');
  if(!checkRequiredFiles([paths.appHtml, paths.resolveApp(`src/pages/${packName}/index.js`)])) {
    console.log(
      chalk.red(
        `\n${packName} does not exist or has no index.js, please check!\n`
      )
    );
    process.exit(1);
  }
} else {
  // 读取src/pages下的一级目录,且拥有index.js入口文件
  const filePath = paths.resolveApp('src/pages');
  const files = fs.readdirSync(filePath);
  packName = files.find((filename) => {
    try {
      const stats = fs.statSync(path.join(filePath, `${filename}/index.js`));
      if(stats.isFile()) return true;
    } catch (error) {
      //目录不存在index.js入口,不作打包处理
      return false;
    }
  });
}

// 上面代码放置于 const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 之前

执行命令

// 开发环境
npm run start // 读取pages下的所有包,并且取第一个符合规范的模块启动
npm run start name=other // 验证模块,启动other

// 生产环境
npm run build // 读取pages下面所有符合规范的模块,并逐个编译打包到对应目录
npm run build name=other // 验证模块,编译other 到 build/other

验证规则

  1. 模块目录必须存在
  2. 目录下存在index.js入口文件

方案说明

前端页面工程以模块的方式放置于pages下,编译完毕后到各自对应的目录,便于区分管理,且不会存在引入不用的内容,提取的公用资源各不相同!使用node.js执行编译任务!

强调:这只是一种处理方式,多多自行研究与分享!


版权声明:本文为u013224660原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。