react工程结构浅析

浅析

这里以 15-stable 这个 tag,也就是 15.6.2 为基准分析
这里还是得从 package.json 开始,npm scrip 命令如下

1
2
3
4
5
6
7
8
9
"scripts": {
"build": "grunt build",
"linc": "git diff --name-only --diff-filter=ACMRTUB `git merge-base HEAD master` | grep '\\.js$' | xargs eslint --",
"lint": "grunt lint",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
"test": "jest",
"flow": "flow",
"prettier": "node ./scripts/prettier/index.js write"
},

可见 build 是使用的 grunt 打包

gruntfile任务总览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
grunt.registerTask("build", [
"delete-build-modules",
"build-modules",
"version-check",
"browserify:basic",
"browserify:addons",
"browserify:min",
"browserify:addonsMin",
"browserify:dom",
"browserify:domMin",
"browserify:domServer",
"browserify:domServerMin",
"browserify:domFiber",
"browserify:domFiberMin",
"npm-react:release",
"npm-react:pack",
"npm-react-dom:release",
"npm-react-dom:pack",
"npm-react-test:release",
"npm-react-test:pack",
"compare_size"
]);

如果熟悉 grunt,会很清楚这个 build 实际上是下面数组里面所有任务的集合,这里挨个分解一下每一行的任务

delete-build-modules

1
2
3
4
grunt.registerTask("delete-build-modules", function() {
// Use gulp here.
spawnGulp(["react:clean"], null, this.async());
});

这里调用了一个函数 spawnGulp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function spawnGulp(args, opts, done) {
grunt.util.spawn(
{
// This could be more flexible (require.resolve & lookup bin in package)
// but if it breaks we'll fix it then.
cmd: path.join("node_modules", ".bin", GULP_EXE),
args: args,
opts: Object.assign({ stdio: "inherit" }, opts)
},
function(err, result, code) {
if (err) {
grunt.fail.fatal("Something went wrong running gulp: ", result);
}
done(code === 0);
}
);
}

grunt.util.spawn 是 grunt 一个 api,它可以创建一个子进程,它的 API 比较简单,grunt.util.spawn(opts, callback),执行一个命令(opts 里面配置),当这个命令执行完毕退出时候会执行 callback。
this.async()则是 grunt 内部设定异步的一个方式,通过这种方式 grunt 才会知道这是一个异步任务。
综上,spawnGulp([‘react:clean’], null, this.async())实际上是在对 grunt 说,『调用一个子进程,执行’gulp react:clean’,并等待它执行完毕,如果抛错就中断整个 grunt 任务并抛出错误。』
gulp 任务的配置文件是 gulpfile,里面定义的任务如下,很容易明白,这是一个删除各个库类 lib 文件的任务,不需要说太多。

1
2
3
4
5
6
7
8
9
gulp.task("react:clean", function() {
return del([
paths.react.lib,
paths.reactDOM.lib,
paths.reactNative.lib,
paths.reactShallowRenderer.lib,
paths.reactTestRenderer.lib
]);
});

build-modules

1
2
3
grunt.registerTask("build-modules", function() {
spawnGulp(["react:modules"], null, this.async());
});

这个和 1 同理,只不过任务换成了 gulp react:modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
gulp.task("react:modules", function() {
return merge(
gulp
.src(paths.react.src)
.pipe(babel(babelOptsReact))
.pipe(stripProvidesModule())
.pipe(flatten())
.pipe(gulp.dest(paths.react.lib)),
gulp
.src(paths.reactDOM.src)
.pipe(babel(babelOptsReactDOM))
.pipe(stripProvidesModule())
.pipe(flatten())
.pipe(gulp.dest(paths.reactDOM.lib)),
gulp
.src(paths.reactNative.src)
.pipe(babel(babelOptsReactNative))
.pipe(stripProvidesModule())
.pipe(flatten())
.pipe(gulp.dest(paths.reactNative.lib)),
gulp
.src(paths.reactShallowRenderer.src)
.pipe(stripProvidesModule())
.pipe(babel(babelOptsReactShallowRenderer))
.pipe(flatten())
.pipe(gulp.dest(paths.reactShallowRenderer.lib)),
gulp
.src(paths.reactTestRenderer.src)
.pipe(stripProvidesModule())
.pipe(babel(babelOptsReactTestRenderer))
.pipe(flatten())
.pipe(gulp.dest(paths.reactTestRenderer.lib))
);
});

这个任务是 gulpfile 的核心部分,gulpfile291 行代码里面 90%都是它的配置、和相关预定义。这几个任务主要的就是 babel(options)、stripProvidesModule、flatten
这里先找其中第一个 options 看看,这五个 options 整体结构都雷同

1
2
3
4
5
6
var babelOptsReact = {
plugins: [
devExpressionWithCodes, // this pass has to run before `rewrite-modules`
[babelPluginModules, { map: moduleMapReact }]
]
};

可以很容易理解这个代码的意思,实质上就是运行 babel,只不过给每个 bable 传入的 plugins 都不一样而已。这里看看这个 devExpressionWithCodes 文件,它指向了 scripts/error-codes/dev-expression-with-codes.js,这里看看这个文件的内容,这里核心一点的地方就是需要对 babel 的 plugins 有所了解.
官方的简介里面提及,babel 是一个编译器,在高层级设计上,有三个运行阶段:parsing, transforming, and generation,plugins 这个东西会影响 transforming 这个环节。
根据官方页面的#plugin-development 提及的,来到babel-handbook 的中文版 plugins 引导页面
scripts/error-codes/dev-expression-with-codes.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
var evalToString = require("./evalToString");
var existingErrorMap = require("./codes.json");
var invertObject = require("./invertObject");
var errorMap = invertObject(existingErrorMap);
module.exports = function(babel) {
var t = babel.types;
var SEEN_SYMBOL = Symbol("dev-expression-with-codes.seen");
// Generate a hygienic identifier
function getProdInvariantIdentifier(path, localState) {
if (!localState.prodInvariantIdentifier) {
localState.prodInvariantIdentifier = path.scope.generateUidIdentifier(
"prodInvariant"
);
path.scope.getProgramParent().push({
id: localState.prodInvariantIdentifier,
init: t.callExpression(t.identifier("require"), [
t.stringLiteral("reactProdInvariant")
])
});
}
return localState.prodInvariantIdentifier;
}
var DEV_EXPRESSION = t.binaryExpression(
"!==",
t.memberExpression(
t.memberExpression(t.identifier("process"), t.identifier("env"), false),
t.identifier("NODE_ENV"),
false
),
t.stringLiteral("production")
);
return {
pre: function() {
this.prodInvariantIdentifier = null;
},
visitor: {
Identifier: {
enter: function(path) {
// Do nothing when testing
if (process.env.NODE_ENV === "test") {
return;
}
// Replace __DEV__ with process.env.NODE_ENV !== 'production'
if (path.isIdentifier({ name: "__DEV__" })) {
path.replaceWith(DEV_EXPRESSION);
}
}
},
CallExpression: {
exit: function(path) {
var node = path.node;
// Ignore if it's already been processed
if (node[SEEN_SYMBOL]) {
return;
}
// Insert `var PROD_INVARIANT = require('reactProdInvariant');`
// before all `require('invariant')`s.
// NOTE it doesn't support ES6 imports yet.
if (
path.get("callee").isIdentifier({ name: "require" }) &&
path.get("arguments")[0] &&
path.get("arguments")[0].isStringLiteral({ value: "invariant" })
) {
node[SEEN_SYMBOL] = true;
getProdInvariantIdentifier(path, this);
} else if (path.get("callee").isIdentifier({ name: "invariant" })) {
// Turns this code:
//
// invariant(condition, argument, 'foo', 'bar');
//
// into this:
//
// if (!condition) {
// if ("production" !== process.env.NODE_ENV) {
// invariant(false, argument, 'foo', 'bar');
// } else {
// PROD_INVARIANT('XYZ', 'foo', 'bar');
// }
// }
//
// where
// - `XYZ` is an error code: a unique identifier (a number string)
// that references a verbose error message.
// The mapping is stored in `scripts/error-codes/codes.json`.
// - `PROD_INVARIANT` is the `reactProdInvariant` function that always throws with an error URL like
// [http://facebook.github.io/react/docs/error-decoder.html?invariant=XYZ&args[\]=foo&args[]=bar](http://facebook.github.io/react/docs/error-decoder.html?invariant=XYZ&args%5B%5D=foo&args%5B%5D=bar)
//
// Specifically this does 3 things:
// 1. Checks the condition first, preventing an extra function call.
// 2. Adds an environment check so that verbose error messages aren't
// shipped to production.
// 3. Rewrites the call to `invariant` in production to `reactProdInvariant`
// - `reactProdInvariant` is always renamed to avoid shadowing
// The generated code is longer than the original code but will dead
// code removal in a minifier will strip that out.
var condition = node.arguments[0];
var errorMsgLiteral = evalToString(node.arguments[1]);
var prodErrorId = errorMap[errorMsgLiteral];
if (prodErrorId === undefined) {
// The error cannot be found in the map.
node[SEEN_SYMBOL] = true;
if (process.env.NODE_ENV !== "test") {
console.warn(
'Error message "' +
errorMsgLiteral +
'" cannot be found. The current React version ' +
"and the error map are probably out of sync. " +
"Please run `gulp react:extract-errors` before building React."
);
}
return;
}
var devInvariant = t.callExpression(
node.callee,
[
t.booleanLiteral(false),
t.stringLiteral(errorMsgLiteral)
].concat(node.arguments.slice(2))
);
devInvariant[SEEN_SYMBOL] = true;
var localInvariantId = getProdInvariantIdentifier(path, this);
var prodInvariant = t.callExpression(
localInvariantId,
[t.stringLiteral(prodErrorId)].concat(node.arguments.slice(2))
);
prodInvariant[SEEN_SYMBOL] = true;
path.replaceWith(
t.ifStatement(
t.unaryExpression("!", condition),
t.blockStatement([
t.ifStatement(
DEV_EXPRESSION,
t.blockStatement([t.expressionStatement(devInvariant)]),
t.blockStatement([t.expressionStatement(prodInvariant)])
)
])
)
);
} else if (path.get("callee").isIdentifier({ name: "warning" })) {
// Turns this code:
//
// warning(condition, argument, argument);
//
// into this:
//
// if ("production" !== process.env.NODE_ENV) {
// warning(condition, argument, argument);
// }
//
// The goal is to strip out warning calls entirely in production. We
// don't need the same optimizations for conditions that we use for
// invariant because we don't care about an extra call in __DEV__
node[SEEN_SYMBOL] = true;
path.replaceWith(
t.ifStatement(
DEV_EXPRESSION,
t.blockStatement([t.expressionStatement(node)])
)
);
}
}
}
}
};
};

其中 DEV_EXPRESSION 实质上是指:process.evn.NODE_ENV !== ‘product’
visitor.Identifier.enter 主要逻辑则是遇到 name 为DEV的 Identifier 时时替换为 process.evn.NODE_ENV !== ‘product’
可能这样说比较绕,通俗点说就是全局替换遇到的DEV 为 process.evn.NODE_ENV !== ‘product’
类似的, visitor.CallExpression.exit 则是函数执行节点退出时候被执行的函数,常规的,举个例子,test(-8)解析成 AST 后,时这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "test"
},
arguments: [{
type: "UnaryExpression",
operator: "-",
prefix: true,
arguments: {
type: "NumericLiteral",
value: 8
}
}]
}
}

有了换个例子,继续下面的分析也就会顺利很多。不过考虑到代码里面已经有非常详尽的功能注释,所以这里暂时对 API 关键词做记录,但是暂时不再深究如何做到。

保留关键词:
prodInvariantIdentifier

getProgramParent

generateUidIdentifier

然后 visitor.CallExpression.exit 这块,主要功能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 功能一
// 插入 `var PROD_INVARIANT = require('reactProdInvariant');`
// 到所有的 `require('invariant')`前面.
# 功能二
// 转换类似这种调用 invariant(condition, argument, 'foo', 'bar');
//
// 为下面这种形式:
//
if (!condition) {
if ("production" !== process.env.NODE_ENV) {
invariant(false, argument, 'foo', 'bar');
} else {
PROD_INVARIANT('XYZ', 'foo', 'bar');
}
}
# 功能三
// 转换 warning(condition, argument, argument);
//
// 为下方代码:
//
if ("production" !== process.env.NODE_ENV) {
warning(condition, argument, argument);
}

总体归纳一下,这个其实就是对两个函数进行转译过程.
功能一确保后面调用 invariant 时候(被功能二转译所以需要用到 PROD_INVARIANT)功能二可以顺利执行
功能二在生产模式执行 PROD_INVARIANT 在非生产执行 invariant,两者的区别是一个传递 errorId 到线上看报错,一个本地看
功能三类似,在非生产显示 warning,在生产模式去掉
接下来是[babelPluginModules, {map: moduleMapReact}],,这个 babelPluginModules 指向/node_modules/fbjs-scripts/babel-6/rewrite-modules.js, map 则时传入的参数。

moduleMapReact 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var moduleMapBase = Object.assign(
{
"object-assign": "object-assign",
"create-react-class": "create-react-class",
"create-react-class/factory": "create-react-class/factory",
"prop-types": "prop-types",
"prop-types/factory": "prop-types/factory"
},
require("fbjs/module-map")
);
var moduleMapReact = Object.assign(
{
// Addons needs to reach into DOM internals
ReactDOM: "react-dom/lib/ReactDOM",
ReactInstanceMap: "react-dom/lib/ReactInstanceMap",
ReactTestUtils: "react-dom/lib/ReactTestUtils",
ReactPerf: "react-dom/lib/ReactPerf",
getVendorPrefixedEventName: "react-dom/lib/getVendorPrefixedEventName"
},
moduleMapBase
);

rewrite-modules.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* Rewrites module string literals according to the `map` and `prefix` options.
* This allows other npm packages to be published and used directly without
* being a part of the same build.
*/
function mapModule(state, module) {
var moduleMap = state.opts.map || {};
if (moduleMap.hasOwnProperty(module)) {
return moduleMap[module];
}
// Jest understands the haste module system, so leave modules intact.
if (process.env.NODE_ENV !== 'test') {
var modulePrefix = state.opts.prefix;
if (modulePrefix == null) {
modulePrefix = './';
}
return modulePrefix + module;
}
}
function getArguments(path) {
var args = path.get('arguments');
if (args && args.length) {
return args[0].node;
}
}
var jestMethods = [
'dontMock',
'genMockFromModule',
'mock',
'setMock',
'unmock',
];
function isJestProperty(t, property) {
return t.isIdentifier(property) && jestMethods.indexOf(property.name) !== -1;
}
module.exports = function(babel) {
var t = babel.types;
/**
* Transforms `require('Foo')` and `require.requireActual('Foo')`.
*/
function transformRequireCall(path, state) {
var calleePath = path.get('callee');
if (
!t.isIdentifier(calleePath.node, {name: 'require'}) &&
!(
t.isMemberExpression(calleePath.node) &&
t.isIdentifier(calleePath.node.object, {name: 'require'}) &&
t.isIdentifier(calleePath.node.property, {name: 'requireActual'})
)
) {
return;
}
var args = path.get('arguments');
if (!args.length) {
return;
}
var moduleArg = args[0];
if (moduleArg.node.type === 'StringLiteral') {
var module = mapModule(state, moduleArg.node.value);
if (module) {
moduleArg.replaceWith(t.stringLiteral(module));
}
}
}
/**
* Transforms either individual or chained calls to `jest.dontMock('Foo')`,
* `jest.mock('Foo')`, and `jest.genMockFromModule('Foo')`.
*/
function transformJestHelper(path, state) {
var calleePath = path.get('callee');
var args = path.get('arguments');
if (!args.length) {
return;
}
var moduleArg = args[0];
if (
moduleArg.node.type === 'StringLiteral' &&
calleePath.node &&
isJestProperty(t, calleePath.node.property)
) {
var module = mapModule(state, moduleArg.node.value);
if (module) {
moduleArg.replaceWith(t.stringLiteral(module))
}
}
}
const jestIdentifier = {
Identifier(path) {
if (path.[node.name](http://node.name) === 'jest') {
this.isJest = true;
}
},
};
function transformJestCall(path, state) {
let params = {isJest: false};
path.traverse(jestIdentifier, params);
if (params.isJest) {
transformJestHelper(path, state);
}
}
return {
visitor: {
CallExpression: {
exit(path, state) {
if (path.node.seen) {
return;
}
transformRequireCall(path, state);
transformJestCall(path, state);
path.node.seen = true;
},
},
},
};
};

transformRequireCall 很容易理解,根据 map 重写模块的引用,比如将 require(‘Promise’) 转译成 require(‘fbjs/lib/Promise’)等。而且注释也有标明Transforms require('Foo') and require.requireActual('Foo')
transformJestHelper 类似 transformRequireCall,只不过时对 jest 的调用做一些替换,如果 map 里面有那么和 transformRequireCall 一样替换掉
到这里 bable(options)就到此结束了,不过最后还是总结这个环节做的事情

  • 处理DEV、invariant、warning 这三个变量、函数调用等的转译

  • 替换 require、require.requireActual、jest 环节包名的重写(rewrite)
    接下来是 stripProvidesModule(node_modules/fbjs-scripts/gulp/strip-provides-module.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var gutil = require("gulp-util");
var through = require("through2");
var PM_REGEXP = require("./shared/provides-module").regexp;
module.exports = function(opts) {
function transform(file, enc, cb) {
if (file.isNull()) {
cb(null, file);
return;
}
if (file.isStream()) {
cb(new gutil.PluginError("module-map", "Streaming not supported"));
return;
}
// Get the @providesModule piece out of the file and save that.
var contents = file.contents.toString().replace(PM_REGEXP, "");
file.contents = new Buffer(contents);
this.push(file);
cb();
}
return through.obj(transform);
};

函数本身很简单,用途时将代码注释中包含的@providesModule 这一行删除掉。
flatten()这个的话可能是最好理解的。直接看 gulp-flatten 官方文档基本可以秒懂,就是去除文件中间路径,将指定目录遍历所有下级文件都归集到一起方便放到一个指定目录用的。
到这里 gulp 的 build 环节就分析完毕了,但是还是做一个最后的总结。
当代码执行下面代码时候

1
2
3
4
5
6
gulp
.src(paths.react.src)
.pipe(babel(babelOptsReact))
.pipe(stripProvidesModule())
.pipe(flatten())
.pipe(gulp.dest(paths.react.lib)),

一共发生了这些事情:

  • babel 环节
    • 替换DEV,替换 invariant、warning
    • 根据 Alias 文件产生的 Map 文件(主要是 Alias 映射关系维护),更新 require 环节的引用路径 2.删除注释中的@providesModule(这个主要时用来生成之前提到的 Map 文件,是一套维护 Map 系统) 3.扁平化,将目标 src 文件处理好后全部放到目的文件夹
      至此我们的 gruntFile 里面的 build-modules 任务已经分析完毕。
      接下来是 version-check,这个也是一个 gulp 任务的引用,根据 getTask 定义
1
2
3
function getTask(name) {
return require(`./gulp/tasks/${name}`)(gulp, plugins);
}

该任务位于 gulp/tasks/version-check.js
核心代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var reactVersion = require("../../package.json").version;
var versions = {
"packages/react/package.json": require("../../packages/react/package.json")
.version,
"packages/react-dom/package.json": require("../../packages/react-dom/package.json")
.version,
"packages/react-test-renderer/package.json": require("../../packages/react-test-renderer/package.json")
.version,
"src/ReactVersion.js": require("../../src/ReactVersion")
};
var allVersionsMatch = true;
Object.keys(versions).forEach(function(name) {
var version = versions[name];
if (version !== reactVersion) {
allVersionsMatch = false;
gutil.log(
gutil.colors.red(
"%s version does not match package.json. Expected %s, saw %s."
),
name,
reactVersion,
version
);
}
});
if (!allVersionsMatch) {
process.exit(1);
}

作用很明显,确保 package 目录下的 react、react-dom、react-test-renderer 和 src/ReactVersion 以及根目录下 package.json 版本号完全一致。

browserify

接下来是系列 browserify 的任务

1
2
3
4
5
6
7
8
9
10
'browserify:basic',
'browserify:addons',
'browserify:min',
'browserify:addonsMin',
'browserify:dom',
'browserify:domMin',
'browserify:domServer',
'browserify:domServerMin',
'browserify:domFiber',
'browserify:domFiberMin',

这些配置在 grunt/config/browserify.js,这些 basic,addons 任务都是一个套路,代码相似就不重复记录了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var basic = {
entries: [
'./build/node_modules/react/lib/ReactUMDEntry.js',
],
outfile: './build/react.js',
debug: false,
standalone: 'React’, // 全局导出时候的库名称
// Apply as global transform so that we also envify fbjs and any other deps
globalTransforms: [envifyDev],
plugins: [collapser],
after: [derequire, simpleBannerify],
// 其他还有一些 after
// [wrapperify, minify, bannerify, simpleBannerify]
};

这里大概有几个包需要简单说一下
loose-envify 这是一个替换用的包,类似 envify,效果如下,不解释

1
2
3
4
5
6
7
8
// before
if (process.env.NODE_ENV === "development") {
console.log("development only");
}
//after
if ("production" === "development") {
console.log("development only");
}

bundle-collapser(collapser)也是一个替换用的包,可以将 require(‘helloword’)转换成类似 require(1)这种极短的引用
wrapperify 将库包裹成 UMD 规范
minify 压缩代码
simpleBannerify、bannerify 给代码文件 Header 包括开源协议和一系列作者信息等等
总体大致的流程就是这个对象设置了入口文件、出口文件,然后打包过程中进行一系列替换、封装 UMD、添加文件头、压缩等优化操作,弄清楚每个引用代表的功能,整个过程很明晰。
接下来就是 npm 这块的任务了

release && pack

‘npm-react:release’,
‘npm-react:pack’,
‘npm-react-dom:release’,
‘npm-react-dom:pack’,
‘npm-react-test:release’,
‘npm-react-test:pack’,
这些任务的流程基本一样,所以只已 npm-react 为记录,其中 npm-react:release 的任务主要代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function buildRelease() {
// delete build/react-core for fresh start
if (grunt.file.exists(dest)) {
grunt.file.delete(dest);
}
// mkdir -p build/react-core/lib
grunt.file.mkdir(lib);
// Copy npm-react/*_/_ to build/npm-react
// and build/modules/*_/_ to build/react-core/lib
var mappings = [].concat(
grunt.file.expandMapping("**/*", dest, { cwd: src }),
grunt.file.expandMapping("**/*", lib, { cwd: modSrc }),
grunt.file.expandMapping("LICENSE", dest)
);
mappings.forEach(function(mapping) {
var mappingSrc = mapping.src[0];
var mappingDest = mapping.dest;
if (grunt.file.isDir(mappingSrc)) {
grunt.file.mkdir(mappingDest);
} else {
grunt.file.copy(mappingSrc, mappingDest);
}
});
// Make built source available inside npm package
grunt.file.mkdir(dist);
distFiles.forEach(function(file) {
grunt.file.copy("build/" + file, dist + file);
});
// modify build/react-core/package.json to set version ##
var pkg = grunt.file.readJSON(dest + "package.json");
pkg.version = grunt.config.data.pkg.version;
grunt.file.write(dest + "package.json", JSON.stringify(pkg, null, 2));
}

总体来讲这个任务虽然挂着 buildRelease 的名头实际上他就是一个做复制粘贴的苦力活儿,主要 build 任务在 build:modules 已经做完了,这里仅仅说一下它做了什么,然后就是怎么做的。

  • 复制 packages/react/里面所有文件到 build/packages/react/目录
  • 复制 build/node_modules/react/lib/里面所有文件到 build/packages/react/lib/目录
  • 复制 build:modules 打包好的 react.js、react-addons.js 和它们的 min 文件到 build/packages/react/dist/目录
  • 修改 package.json 里面的版本号

然后是 npm-react:pack 相关的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function packRelease() {
var done = this.async();
var spawnCmd = {
cmd: "npm",
args: ["pack", "packages/react"],
opts: {
cwd: "build/"
}
};
grunt.util.spawn(spawnCmd, function() {
var buildSrc = "build/react-" + grunt.config.data.pkg.version + ".tgz";
var buildDest = "build/packages/react.tgz";
fs.rename(buildSrc, buildDest, done);
});
}

spawnCmd 这个相当于时执行一个命令: cd build/ && npm pack package/react
pack 是 npm 工具内建的一个命令 打包 npm 包生成-.tgz 格式压缩包
下面代码则是执行完毕生成了-.tgz 格式压缩包后重命名为 react.tgz