阅读 379

father 源码解析(father further区别)

father 是 umi 团队用于构建项目使用的一个工具包,属于一个工程类的项目。现在开始我们从零到一的学习如何从一步一步的搭建一个新的 father build 工具。

1. 分析 father 代码

当我们在使用 father 的过程中,我们发现是通过 father build xxx 去使用的。father build 是一个指令,这个指令是由于什么去透传出来的呢?
这时候我们需要研究一下了,我们将 father 的代码下载到本地 kang yi kang~
father 源码下载地址:github.com/umijs/fathe…

源码下载下来之后我们进入项目:packages/* 下面存在两个项目的包

  • father

  • father-build

通过指令,我们可以了解 father 是由于 father 这个项目透传出来的呢?本质上,father 是一个脚手架。脚手架的指令集合入口文件可以从当前项目的 package.json 可以看出来。我们现在打开 packages/father/package.json 文件。 package.json 文件内容如下:

{
  "name": "father",
  "version": "2.30.10",
  "description": "Library toolkit based on rollup, docz, storybook, jest and eslint.",
  "homepage": "http://github.com/umijs/father",
  "bugs": "http://github.com/umijs/father/issues",
  "repository": {
    "type": "git",
    "url": "http://github.com/umijs/father"
  },
  "license": "MIT",
  "main": "lib/index.js",
  "typings": "./index.d.ts",
  "bin": {
    "father": "./bin/father.js"
  },
  "dependencies": {
    "@babel/plugin-proposal-decorators": "7.4.4",
    "@storybook/addon-a11y": "^5.2.6",
    "@storybook/addon-actions": "^5.2.6",
    "@storybook/addon-console": "^1.2.1",
    "@storybook/addon-knobs": "^5.2.6",
    "@storybook/addon-links": "^5.2.6",
    "@storybook/addon-notes": "^5.2.6",
    "@storybook/addon-options": "^5.2.6",
    "@storybook/addon-storysource": "^5.2.6",
    "@storybook/addons": "^5.2.6",
    "@storybook/cli": "^5.2.6",
    "@storybook/react": "^5.2.6",
    "@storybook/theming": "^5.2.6W",
    "@umijs/fabric": "^2.5.6",
    "babel-loader": "^8.0.6",
    "docz": "1.2.0",
    "docz-core": "1.2.0",
    "docz-plugin-umi-css": "^0.14.1",
    "docz-theme-umi": "^2.0.0",
    "father-sudo cnpm run build": "1.20.1",
    "fs-extra": "^8.0.1",
    "gh-pages": "2.0.1",
    "lodash": "^4.17.20",
    "prettier": "^2.2.1",
    "rc-source-loader": "^1.0.2",
    "react-markdown": "^4.0.8",
    "sass-loader": "^8.0.0",
    "signale": "1.4.0",
    "slash2": "^2.0.0",
    "staged-git-files": "^1.2.0",
    "storybook-addon-source": "^2.0.9",
    "umi-test": "^1.5.10",
    "update-notifier": "3.0.0",
    "yargs-parser": "13.1.2"
  },
  "authors": [
    "clock157 <clock157@163.com> (https://github.com/clock157)",
    "chencheng <sorrycc@gmail.com> (https://github.com/sorrycc)"
  ]
}复制代码

father 指令就是由:bin 中属性透露出来的。你可以认为 bin 属性一条数据,这条数据指向的地址是 ./bin/father.js。然后,我们就打开这个文件。

function printHelp() {
  console.log(`
  Usage: father <command> [options]

  Commands:

    ${chalk.green('build')}       build library
  `);
}复制代码

我们本地执行 father --help 指令后的结果就是这一个方法暴露出来的。 而 father build 执行调用的方法是下面的代码

function build() {
  // Parse buildArgs from cli
  const buildArgs = stripEmptyKeys({
    esm: args.esm && { type: args.esm === true ? 'rollup' : args.esm },
    cjs: args.cjs && { type: args.cjs === true ? 'rollup' : args.cjs },
    umd: args.umd && { name: args.umd === true ? undefined : args.umd },
    file: args.file,
    target: args.target,
    entry: args._.slice(1),
  });

  if (buildArgs.file && buildArgs.entry && buildArgs.entry.length > 1) {
    signale.error(new Error(
      `Cannot specify file when have multiple entries (${buildArgs.entry.join(', ')})`
    ));
    process.exit(1);
  }

  require('father-build').default({
    cwd,
    watch: args.w || args.watch,
    buildArgs,
  }).catch(e => {
    signale.error(e);
    process.exit(1);
  });
}复制代码

看完这段代码之后,我们就从中筛选出主要的逻辑代码应该是在 father-build 这个包之中,father-build 就是 packages/father-build 这个项目了呀! 所以现在就来看 father-build 这个项目了。 按照之前的思路,我们先看 package.json 文件

{
  "name": "father-build",
  "version": "1.20.1",
  "description": "Library build tool based on rollup.",
  "main": "lib/index.js",
  "bin": {
    "father-build": "./bin/father-build.js"
  },
  "typings": "./index.d.ts",
  "dependencies": {
    "@babel/core": "7.12.3",
    "@babel/plugin-proposal-class-properties": "7.12.1",
    "@babel/plugin-proposal-decorators": "7.12.1",
    "@babel/plugin-proposal-do-expressions": "7.12.1",
    "@babel/plugin-proposal-export-default-from": "7.12.1",
    "@babel/plugin-proposal-export-namespace-from": "7.12.1",
    "@babel/plugin-proposal-nullish-coalescing-operator": "7.12.1",
    "@babel/plugin-proposal-optional-chaining": "7.12.1",
    "@babel/plugin-syntax-dynamic-import": "7.8.3",
    "@babel/plugin-transform-modules-commonjs": "7.12.1",
    "@babel/plugin-transform-runtime": "7.12.1",
    "@babel/preset-env": "7.12.1",
    "@babel/preset-react": "7.12.1",
    "@babel/preset-typescript": "7.12.1",
    "@babel/register": "7.12.1",
    "@lerna/filter-packages": "4.0.0",
    "@lerna/project": "4.0.0",
    "@lerna/query-graph": "4.0.0",
    "@rollup/plugin-babel": "5.2.1",
    "@rollup/plugin-commonjs": "16.0.0",
    "@rollup/plugin-inject": "4.0.2",
    "@rollup/plugin-json": "4.1.0",
    "@rollup/plugin-node-resolve": "10.0.0",
    "@rollup/plugin-replace": "2.3.4",
    "@rollup/plugin-url": "5.0.1",
    "@svgr/rollup": "5.5.0",
    "ajv": "6.12.6",
    "autoprefixer": "9.6.0",
    "babel-plugin-istanbul": "^5.2.0",
    "babel-plugin-react-require": "3.1.1",
    "chalk": "2.4.2",
    "chokidar": "^3.0.2",
    "glob": "^7.1.4",
    "gulp-if": "2.0.2",
    "gulp-less": "^4.0.1",
    "gulp-plumber": "^1.2.1",
    "gulp-typescript": "5.0.1",
    "less": "3.9.0",
    "less-plugin-npm-import": "2.1.0",
    "lodash": "4.17.21",
    "rimraf": "2.6.3",
    "rollup": "2.33.3",
    "rollup-plugin-postcss": "3.1.8",
    "rollup-plugin-terser": "7.0.2",
    "rollup-plugin-typescript2": "0.29.0",
    "signale": "1.4.0",
    "slash2": "2.0.0",
    "temp-dir": "2.0.0",
    "through2": "3.0.1",
    "ts-loader": "^8.0.7",
    "typescript": "^4.0.5",
    "update-notifier": "3.0.0",
    "vinyl-fs": "3.0.3",
    "yargs-parser": "13.1.2"
  },
  "repository": {
    "type": "git",
    "url": "http://github.com/umijs/father"
  },
  "homepage": "http://github.com/umijs/father",
  "bugs": "http://github.com/umijs/father/issues",
  "authors": [
    "chencheng <sorrycc@gmail.com> (https://github.com/sorrycc)"
  ],
  "license": "MIT",
  "devDependencies": {
    "@types/gulp-plumber": "^0.0.32"
  }
}复制代码

看到 bin 属性,找到它指向的文件 ./bin/father-build.js ,打开 ./bin/father-build.js 文件。当被访问的时候,当前文件自己执行了下面这边代码。

function build() {
  // Parse buildArgs from cli
  const buildArgs = stripEmptyKeys({
    esm: args.esm && { type: args.esm === true ? 'rollup' : args.esm },
    cjs: args.cjs && { type: args.cjs === true ? 'rollup' : args.cjs },
    umd: args.umd && { name: args.umd === true ? undefined : args.umd },
    file: args.file,
    target: args.target,
    entry: args._,
  });

  if (buildArgs.file && buildArgs.entry && buildArgs.entry.length > 1) {
    signale.error(new Error(
      `Cannot specify file when have multiple entries (${buildArgs.entry.join(', ')})`
    ));
    process.exit(1);
  }

  require('../lib/build').default({
    cwd: args.root || process.cwd(),
    watch: args.w || args.watch,
    buildArgs,
  }).catch(e => {
    signale.error(e);
    process.exit(1);
  });
}

build();复制代码

看上面情况需要携带参数进入 lib/build 文件中,但是我们看原来的源码文件是没有 lib 文件夹的,所以这个文件夹是哪里的呢? 找来找去,我们打开根目录下面的 package.json。看 scripts 的指令

"scripts": {
  "bootstrap": "lerna bootstrap",
  "build": "umi-tools build",
  "changelog": "lerna-changelog",
  "clean": "lerna clean -y",
  "test": "umi-test --coverage",
  "debug": "umi-test",
  "publish": "npm run build && lerna publish"
 } 
复制代码

看到 build,是不是想起什么呢?接下来我丝毫没有犹豫,赶紧执行 npm run build ,得得得~果然是这个原因。接下来大体沿着这个思路研究差不多就行了吧!所以为了看到效果。在 packages/father 目录下执行 npm link 。将本地项目link到全局。

执行!

完美 !

接下来,应该是代码最逻辑的部分了,我们慢慢看! 其实我本不应该查看到 lib/build 文件,因为这是打包之后的代码,跟原来的代码观阅度上还是有点差距的。我们这边应该查看 src/build 文件,这是才属于一手代码。

export default async function(opts: IOpts) {
  const useLerna = existsSync(join(opts.cwd, 'lerna.json'));
  const isLerna = useLerna && process.env.LERNA !== 'none';

  const dispose = isLerna ? await buildForLerna(opts) : await build(opts);
  return () => dispose.forEach(e => e());
}复制代码

这边先判断当前项目是否是 lerna 项目。如果是 lerna 项目,则走一下 lerna 项目的配置,否则走常见配置生成构建任务。

for (const bundleOpts of bundleOptsArray) {
   ...
   // 这边是使用 rollup 进行相应文件打包
   await rollup({ ... });
   ...
}
...
() => dispose.forEach(e => e())复制代码

最后通过遍历的方式去执行每一步任务步骤。那现在我们看看这个构建方法里面具体有哪些东西呢! 首先看 father 是如何针对 lerna 项目生成构建任务的。

export async function buildForLerna(opts: IOpts) {
  const { cwd, rootConfig = {}, buildArgs = {} } = opts;

  // register babel for config files
  registerBabel({
    cwd,
    only: CONFIG_FILES,
  });

  const userConfig = merge(getUserConfig({ cwd }), rootConfig, buildArgs);

  let pkgs = await getLernaPackages(cwd, userConfig.pkgFilter);

  // support define pkgs in lerna
  // TODO: 使用lerna包解决依赖编译问题
  if (userConfig.pkgs) {
    pkgs = userConfig.pkgs
      .map((item) => {
        return pkgs.find(pkg => basename(pkg.contents) === item);
      })
      .filter(Boolean);
  }

  const dispose: Dispose[] = [];
  for (const pkg of pkgs) {
    if (process.env.PACKAGE && basename(pkg.contents) !== process.env.PACKAGE) continue;
    // build error when .DS_Store includes in packages root
    const pkgPath = pkg.contents;
    assert.ok(
      existsSync(join(pkgPath, 'package.json')),
      `package.json not found in packages/${pkg}`,
    );
    process.chdir(pkgPath);
    dispose.push(...await build(
      {
        // eslint-disable-line
        ...opts,
        buildArgs: opts.buildArgs,
        rootConfig: userConfig,
        cwd: pkgPath,
        rootPath: cwd,
      },
      {
        pkg,
      },
    ));
  }
  return dispose;
}复制代码

看上面代码我们可以看出来,father 这边写将配置文件注册了一下。

registerBabel({
  cwd,
  only: CONFIG_FILES,
});复制代码

然后这个 CONFIG_FILES 指向的是我们本地配置的 father 配置文件信息。这个配置文件的命名有下面几种:**

export const CONFIG_FILES = [
  '.fatherrc.js',
  '.fatherrc.jsx',
  '.fatherrc.ts',
  '.fatherrc.tsx',
  '.umirc.library.js',
  '.umirc.library.jsx',
  '.umirc.library.ts',
  '.umirc.library.tsx',
];复制代码

当 father 这边扫描到存在的哪几种配置文件,它会根据配置文件生成一些基本的 babel 的配置信息,然后通过 @babel/register 进行注册。

export default function(opts: IRegisterBabelOpts) {
  const { cwd, only } = opts;
  const { opts: babelConfig } = getBabelConfig({
    target: 'node',
    typescript: true,
  });
  require('@babel/register')({
    ...babelConfig,
    extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'],
    only: only.map(file => slash(join(cwd, file))),
    babelrc: false,
    cache: false,
  });
}复制代码

在注册完相关的 Babel 配置之后,father 这边在获取用户这边的捆绑配置。这边呢会先定死几个入口文件比如:src/index.tsxsrc/index.tssrc/index.js 以及 src/index.js。当项目上存在着几个文件的时候,会认定这定义好的文件是入口文件。

export function getBundleOpts(opts: IOpts): IBundleOptions[] {
  const { cwd, buildArgs = {}, rootConfig = {} } = opts;
  const entry = getExistFile({
    cwd,
    files: ['src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js'],
    returnRelative: true,
  });
  const userConfig = getUserConfig({ cwd });
  const userConfigs = Array.isArray(userConfig) ? userConfig : [userConfig];
  return (userConfigs as any).map(userConfig => {
    const bundleOpts = merge(
      {
        entry,
      },
      rootConfig,
      userConfig,
      buildArgs,
    );

    // Support config esm: 'rollup' and cjs: 'rollup'
    if (typeof bundleOpts.esm === 'string') {
      bundleOpts.esm = { type: bundleOpts.esm };
    }
    if (typeof bundleOpts.cjs === 'string') {
      bundleOpts.cjs = { type: bundleOpts.cjs };
    }

    return bundleOpts;
  });
}复制代码

当然一个项目这边只能有一个入口文件,所以 father 团队这边是认定按照书写顺序,由前往后,一旦匹配上就会 return 出去。

export function getExistFile({ cwd, files, returnRelative }) {
  for (const file of files) {
    const absFilePath = join(cwd, file);
    if (existsSync(absFilePath)) {
      return returnRelative ? file : absFilePath;
    }
  }
}复制代码

获取到配置文件之后,进行遍历执行任务了。先清空 dist 文件,然后打包,打包使用的是 rollup 。 在这个项目中是如何使用 rollup 的呢?

async function build(entry: string, opts: IRollupOpts) {
  const { cwd, rootPath, type, log, bundleOpts, importLibToEs, dispose } = opts;
  // 根据之前的配置生成打包的 rollup 配置脚本
  const rollupConfigs = getRollupConfig({
    cwd,
    rootPath:rootPath || cwd,
    type,
    entry,
    importLibToEs,
    bundleOpts: normalizeBundleOpts(entry, bundleOpts),
  });

  for (const rollupConfig of rollupConfigs) {
    if (opts.watch) {
     // ...
    } else {
      const { output, ...input } = rollupConfig;
      // rollup 根据输入进行打包
      const bundle = await rollup(input); // eslint-disable-line
      // 写入到 dist 文件下面
      await bundle.write(output); // eslint-disable-line
    }
  }
}复制代码

大体先看到这边,之后边搭建,边补充更多细节的部分。

2. 本地开发 father 工具

2.1 本地开发环境

  1. node: 我们本地需要搭建 node 环境,供项目运行。官网地址:nodejs.cn/

  2. lerna: lerna 出现的目的是为了方便团队对项目进行更好的管理,将多个相互有相关联的项目集合管理在一起。官网地址:github.com/lerna/lerna…

我们需要了解在项目开发中使用的相关的技术点

2.2 实际项目搭建

2.2.1 lerna 安装 全局安装 lerna 脚手架

npm i -g lerna复制代码

安装成功之后使用 lerna --help 查看 lerna 提供哪些功能。

Usage: lerna <command> [options]

命令:
  lerna add <pkg> [globs..]  Add a single dependency to matched packages
  lerna bootstrap            Link local packages together and install remaining package
                             dependencies
  lerna changed              List local packages that have changed since the last tagged
                             release                                  [aliases: updated]
  lerna clean                Remove the node_modules directory from all packages
  lerna create <name> [loc]  Create a new lerna-managed package
  lerna diff [pkgName]       Diff all packages or a single package since the last
                             release
  lerna exec [cmd] [args..]  Execute an arbitrary command in each package
  lerna import <dir>         Import a package into the monorepo with commit history
  lerna info                 Prints debugging information about the local environment
  lerna init                 Create a new Lerna repo or upgrade an existing repo to the
                             current version of Lerna.
  lerna link                 Symlink together all packages that are dependencies of each
                             other
  lerna list                 List local packages                   [aliases: ls, la, ll]
  lerna publish [bump]       Publish packages in the current project.
  lerna run <script>         Run an npm script in each package that contains that script
  lerna version [bump]       Bump version of packages changed since the last release.

Global Options:
      --loglevel       What level of logs to report.             [字符串] [默认值: info]
      --concurrency    How many processes to use when lerna parallelizes tasks.
                                                                      [数字] [默认值: 8]
      --reject-cycles  Fail if a cycle is detected among dependencies.            [布尔]
      --no-progress    Disable progress bars. (Always off in CNPM I)                  [布尔]
      --no-sort        Do not sort packages topologically (dependencies before
                       dependents).                                               [布尔]
      --max-buffer     Set max-buffer (in bytes) for subcommand execution         [数字]
  -h, --help           显示帮助信息                                               [布尔]
  -v, --version        显示版本号                                                 [布尔]复制代码

2.2.2 使用 lerna 初始化项目

lerna init复制代码

创建 shuangshuang-build 项目包

lerna create shuang复制代码
lerna create shuang-build复制代码

2.2.3 集成 typescript
项目中安装 typescript 依赖包:

npm i -D typescript复制代码

在项目根目录下面创建 tsconfig.json 文件。内容填充为

{
  "compilerOptions": {
    "target": "esnext",
    "moduleResolution": "node",
    "jsx": "preserve",
    "esModuleInterop": true
  }
}复制代码

--- 待续 ---


作者:shuangyue
链接:https://juejin.cn/post/7029235887154135077


文章分类
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐