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.tsx
、src/index.ts
、src/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 本地开发环境
node: 我们本地需要搭建 node 环境,供项目运行。官网地址:nodejs.cn/
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复制代码
创建 shuang
和 shuang-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