| 7 min read

接着前文 《React-360 源码阅读【1】》,下面主要分析构建这一部分业绩是 /scripts 目录下的内容;

结构

- build-packages.js
- bump-versions.js
- bundle.js
- install-deps.js

install-reps.js

在说这片代码之前,我们需要先去 packages/react-360-cli/src/getPackager.js
了解下如何判断是否支持 yarn

const child_process = require('child_process'); 
let packager = null; 
function getPackager() { 
if (packager !== null) { 
return packager; 
} 
try { 
child_process.execSync( 
/^win/.test(process.platform) ? 'yarn --version 2> NUL' : 'yarn --version 2>/dev/null'
); 
packager = 'yarn'; 
} catch (e) { 
packager = 'npm'; 
} 
return packager; 
} 
module.exports = getPackager; 

实际上是通过 判断 yarn —version 来实现的。

const child_process = require('child_process');
const path = require('path');
const getPackager = require('../packages/react-360-cli/src/getPackager');

/**
 * Script to install development dependencies for users building the codebase
 * from the GitHub repo.
 */
function installDeps() {
  // 大概定义需要构建的包的路径
  const paths = {
    'react-360': path.resolve(__dirname, '..'),
    'react-360-web': path.resolve(__dirname, '..', 'React360'),
  };
  console.log('Determining packager...');
  // 判断使用包的管理器
  const packager = getPackager();
  if (packager !== 'yarn' && packager !== 'npm') {
    throw new Error('Unexpected packager: ' + packager);
  }
  console.log(`\x1b[34;1m${packager}\x1b[0m detected`);
  const args = packager === 'yarn' ? ['--pure-lockfile'] : ['install'];
  let promise = Promise.resolve();
  Object.keys(paths).forEach(pkg => {
    promise = promise.then(() => {
      console.log('Installing dependencies for ' + pkg);
      return new Promise((resolve, reject) => {
        // 调用安装的命令
        const proc = child_process.spawn(packager, args, {
          stdio: 'inherit',
          cwd: paths[pkg],
          shell: true,
        });
        proc.on('close', code => {
          if (code !== 0) {
            reject({pkg, code});
          }
          resolve();
        });
      });
    });
  });
  return promise.then(
    () => {
      console.log('Completed successfully!');
    },
    ({pkg, code}) => {
      console.log(`Dependency installation for ${pkg} failed with code ${code}`);
    }
  );
}

module.exports = installDeps;

if (require.main === module) {
  // Called from the command line directly
  installDeps();
}

运行结果如图:

build-packages.js

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

const installDeps = require('./install-deps');

function buildPackage(name, dir) {
  console.log(`  Packaging \x1b[32m'${name}'\x1b[0m`);
  return new Promise((resolve, reject) => {
    const json = path.resolve(dir, 'package.json');
    fs.stat(json, (err, res) => {
      if (err) {
        console.log(`\x1b[41;1m${json} does not exist\x1b[0m`);
        reject(err);
        return;
      }
      resolve(json);
    });
  }).then(json => {
    // 获取包的 package.json 的最新版本
    const version = require(json).version;
    return new Promise((resolve, reject) => {
      // 使用 pack 命令将文件打包城 tgz 文件
      const npm = child_process.spawn('npm', ['pack'], {stdio: 'inherit', cwd: dir, shell: true});
      npm.on('close', code => {
        if (code !== 0) {
          reject(code);
        }
        resolve(path.resolve(dir, `${name}-${version}.tgz`));
      });
    });
  });
}

// 移动构建后包的位置
function relocatePackage(name, src, dest, rev) {
  const destfile = path.join(dest, `${name}-${rev}.tgz`);
  console.log(`Moving ${src} to ${destfile}`);
  return new Promise((resolve, reject) => {
    fs.rename(src, destfile, () => {
      resolve();
    });
  });
}
	// 确保该目录存在
function ensureDirectory(dir) {
  try {
    const stat = fs.statSync(dir);
    if (stat) {
      return;
    }
  } catch (e) {
    fs.mkdirSync(dir);
  }
}

const PACKAGES = {
  'react-360': path.resolve(__dirname, '..'),
  'react-360-web': path.resolve(__dirname, '..', 'React360'),
};

// 获取最新提交的 commit 字符串
const rev = child_process.execSync('git rev-parse HEAD').toString().trim().substr(0, 8);

const DEST = path.resolve(__dirname, '..', 'package_builds');
installDeps()
  .then(() => {
    console.log(`\x1b[34;1mBuilding packages at git rev ${rev}...\x1b[0m`);
    ensureDirectory(DEST);
    return Promise.all(
      Object.keys(PACKAGES).map(p =>
        // 进行pack然后移动到指定位置
        buildPackage(p, PACKAGES[p]).then(src => relocatePackage(p, src, DEST, rev))
      )
    );
  })
  .then(() => {
    console.log(`\nBuilt all packages! They are located at \x1b[34m${DEST}\x1b[0m`);
  });

bump-versions.js

顾名思义,就是用户版本好的迭代的;

const fs = require('fs');
const path = require('path');

// 版本号的正则表达式
const versionExpr = /^\d+\.\d+\.\d+$/;

// 需要替换掉 package.json 里面的 version
// Can also replace strings of the form `"somedependency": "..."` with newer versions

function updateVersion(pkg, version, deps) {
  const contents = fs.readFileSync(pkg, 'utf8');
  // 替换某个包的的版本号
  let updated = contents.replace(/"version":\s*"[^"]+"/, `"version": "${version}"`);
  if (deps) {
    for (const dep in deps) {
      updated = updated.replace(new RegExp(`"${dep}":\\s*"[^"]+"`), `"${dep}": "${deps[dep]}"`);
    }
  }
  // 将修改的好的  package.json 字符串重新写入
  fs.writeFileSync(pkg, updated);
}

const args = process.argv;
// 获取命令行的版本号
const version = args[2];
if (typeof version !== 'string' || version.length === 0) {
  console.log('\x1b[31;1mMust provide a new version string\x1b[0m');
  console.log('Usage: node bump-versions.js VERSION');
  console.log('    VERSION - a new version string for packages');
  process.exit(1);
}
if (!version.match(versionExpr)) {
  console.log('\x1b[31;1mVersion must be in the form `MAJOR.MINOR.PATCH`\x1b[0m');
}
const packages = {
  'react-360': path.resolve(__dirname, '..', 'package.json'),
  'react-360-web': path.resolve(__dirname, '..', 'React360', 'package.json'),
  'react-360-cli': path.resolve(__dirname, '..', 'react-360-cli', 'package.json'),
};
const cliGenerator = path.resolve(
  __dirname,
  '..',
  'react-360-cli',
  'generators',
  'package.json.generator.js'
);
const currentVersion = require('../package.json').version;
console.log(
  `Current version is \x1b[34;1m${currentVersion}\x1b[0m, updating packages to \x1b[34;1m${version}\x1b[0m`
);
// 这里目录出问题了
const cliVersionPieces = require('../react-360-cli/package.json').version
  .split('.')
  .map(s => parseInt(s, 10));
cliVersionPieces[2]++; // increment patch version of CLI package

console.log('Updating react-360...');
updateVersion(packages['react-360'], version);
console.log('Updating react-360-web...');
updateVersion(packages['react-360-web'], version);
console.log('Updating new project generator...');
updateVersion(cliGenerator, '0.0.1', {
  'react-360': '~' + version,
  'react-360-web': '~' + version,
});
console.log('Updating react-360-cli...');
updateVersion(packages['react-360-cli'], cliVersionPieces.join('.'));

console.log(`All packages updated to \x1b[34;1m${version}\x1b[0m`);

运行结果如图:

bundle.js

主要用于对项目进行 JS 打包的功能;

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

const args = process.argv.slice(2);

let customAssetRoot = null;
for (let i = 0; i < args.length; i++) {
  if (args[i] === '--assets' && args[i + 1]) {
    customAssetRoot = args[i + 1];
    if (customAssetRoot[customAssetRoot.length - 1] !== '/') {
      customAssetRoot += '/';
    }
  }
}

function buildScript(root, buildDir, input, output) {
  // 需要我们找到 react-native cli 的命令行位置
  const cliLocation =
    process.env.RN_CLI_LOCATION ||
    path.resolve('node_modules', 'react-native', 'local-cli', 'cli.js');
  return new Promise((resolve, reject) => {
    const npm = child_process.spawn(
      /^win/.test(process.platform) ? 'node.exe' : 'node',
      [
        cliLocation,
        'bundle',
        '--entry-file',
        input,
        '--platform',
        'vr',
        '--bundle-output',
        output,
        '--dev',
        'false',
        '--assets-dest',
        buildDir,
      ],
      {stdio: 'inherit', cwd: root},
    );
    npm.on('close', code => {
      if (code !== 0) {
        reject(code);
      }
      resolve();
    });
  });
}

// 更新 html 的 js bundle 的地址
function updateHTML(root, output, assetRoot) {
  const indexPath = path.join(root, 'index.html');
  let contents = null;
  try {
    contents = fs.readFileSync(indexPath, 'utf8');
  } catch (e) {
    return [
      'Could not find index.html. You will need to manually update the paths to your bundled JavaScript files',
    ];
  }
  const writeErrors = [];
  if (contents.indexOf('./client.bundle') < 0) {
    writeErrors.push(
      'Could not find path to client in index.html. You will need to manually update it.',
    );
  } else {
    contents = contents.replace(
      'client.bundle?platform=vr',
      'client.bundle.js',
    );
  }
  if (contents.indexOf('index.bundle') < 0) {
    writeErrors.push(
      'Could not find path to index.js in index.html. You will need to manually update it.',
    );
  } else {
    contents = contents.replace(
      /index\.bundle\?platform=vr(&dev=true)?/,
      'index.bundle.js',
    );
  }
  if (assetRoot) {
    if (contents.indexOf("'static_assets/'") < 0) {
      writeErrors.push('Could not find assetRoot');
    } else {
      contents = contents.replace(
        /assetRoot:\s*'static_assets\/'/,
        `assetRoot: '${assetRoot}'`,
      );
    }
  }
  try {
    fs.writeFileSync(path.join(output, 'index.html'), contents, 'utf8');
  } catch (e) {
    return [`Unable to write index.html to ${output}`];
  }
  return writeErrors;
}

根据 react-360 的包的以来判断是否在正确的路径进行构建
function hasPackage(dir) {
  const packagePath = path.join(dir, 'package.json');
  try {
    fs.statSync(packagePath);
  } catch (e) {
    return false;
  }
  const pkg = require(packagePath);
  if (pkg && pkg.dependencies && pkg.dependencies['react-360']) {
    return true;
  }
  return false;
}

// 循环查找正确的包的路径
function findProjectDir(dir) {
  while (!hasPackage(dir)) {
    const next = path.join(dir, '..');
    if (dir === next) {
      console.log('Could not find a React 360 project directory');
      process.exit(1);
    }
    dir = path.join(dir, '..');
  }
  return dir;
}

// Allow overriding the project location with an env variable
const projectDir =
  process.env.PROJECT_LOCATION || findProjectDir(process.cwd());

const buildDir = path.join(projectDir, 'build');

new Promise((resolve, reject) => {
  try {
    const stat = fs.statSync(buildDir);
    if (stat.isDirectory()) {
      return resolve();
    }
  } catch (e) {}
  fs.mkdir(path.join(projectDir, 'build'), err => {
    if (err) {
      console.log('Failed to create `build` directory');
      return reject(1);
    }
    resolve();
  });
})
  .then(() => {
   // 需要打包 index.vr 和 client 的文件
    return Promise.all([
      buildScript(
        projectDir,
        buildDir,
        path.resolve(projectDir, 'index.js'),
        path.resolve(projectDir, 'build', 'index.bundle.js'),
      ),
      buildScript(
        projectDir,
        buildDir,
        path.resolve(projectDir, 'client.js'),
        path.resolve(projectDir, 'build', 'client.bundle.js'),
      ),
    ]);
  })
  .then(() => {
    console.log(
      'Production versions of JS were successfully built.' +
        'They can be found at ' +
        path.resolve(projectDir, 'build'),
    );
    const errors = updateHTML(projectDir, path.resolve(projectDir, 'build'), customAssetRoot);
    if (errors.length) {
      errors.forEach(err => console.log(' ** ', err));
    }
  })
  .catch(err => {
    console.log(
      'An error occurred during the bundling process. Exited with code ' +
        err +
        '.\nLook at the packager output above to see what went wrong.',
    );
    process.exit(1);
  });

下一篇,我们就进入整体,先剖析Libraries 的内容;

You Can Speak "Hi" to Me in Those Ways