requirejs和react的动态模块加载

requirejs是当前比较好的AMD规范的实现,所有的JS文件会被打包成一个js文件,减少请求数量,同时比较方便的实现了SPA的模式。但是随着项目的推进,SPA的首屏加载问题就会越来越明显。针对这个问题,webpack在v1版本中通过require.ensure,在v2版本中通过dynamic-imports来解决这个问题,不过实现最重要的就是在代码运行过程中动态加载其他的js文件(模块)。

requirejs官方文档中,我们可以看到在模块中,也是可以随时使用require来加载新的模块,如下

1
2
3
4
5
6
7
8
9
require(["require", "alpha", "beta"], function(require, alpha, beta) {
setTimeout(function() {
require(["omega"],
function(omega) {
console.log("load omega finish");
}
);
}, 100);
});

不过我们在上面的代码中看到了很常见的回调地狱的问题,同时也会有一些异常处理的问题,所以我们首先先使用promise封装一下require的文件加载

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
/**
* [asyncModule description]
* @param {[type]} paths [传入一个模块路径的数组]
* @return {[type]} [Promise]
*/
function asyncModule(paths) {
return new Promise((resolve, reject) => {
if (!(paths instanceof Array)) {
reject('paths should be a array');
return false;
}
/**
* [paths description]
* @type {[type]}
*/
paths = paths.map((path) => {
path = manifest[path] || path;
let url = require.toUrl(path).slice(1);
return url.slice(0, url.indexOf('?'));
});
require(paths, function callback(Module) {
resolve.apply(this, arguments);
});
});
}

上面有一段代码是path的处理,主要是搭配require来处理模块的路径:

1
require.config({ baseUrl: 'js/', waitSeconds: 60});

我们再通过react来封装一个”模块加载器”:

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
define([], () => class DynamicModuleLoader extends React.Component {
constructor(props) {
super(props);
this.state = {
log: 'module loading',
module: undefined
};
}
componentDidMount() {
asyncModule([this.props.module]).then((module) => {
this.setState({
module
});
}).catch((e) => {
this.setState({
log: 'module load error'
});
});
}
render() {
if (this.state.module) {
return <this.state.module {...this.props} />;
}
return (
<div data-ilog={this.state.log} />
);
}
});

接下来我们在代码中以如下方式使用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
define(['dynamic-module-loader'], (DynamicModuleLoader) => class PageHome extends React.Component {
constructor(props) {
super(props);
this.state = {
...
};
}
componentDidMount() {
}
render() {
return (
<div>
<h1>Home</h1>
<DynamicModuleLoader module="module1/index.dynamic.js" renderData={this.props} />
</div>
);
}
});

除了代码层面的修改,还需要针对构建工具做修改, 首先我们将需要动态加载的js文件加上.dynamic.js的后缀,主要的问题在于,我们需要使用module1/index.dynamic.js来加载,但是实际上,我们还需要考虑版本的问题,所以真实的加载文件的路径可能是module1/index-f37e86f666.dynamic.js,所以需要考虑开发时的便利和生成环境的需求,这里我们使用了gulp,:

1
2
3
4
5
6
7
8
9
10
11
const gulp = require('gulp');
const babel = require('gulp-babel');
const rev = require('gulp-rev');

gulp.src(`${PROJECT_PATH}/js/**/*.async.js`)
.pipe(babel())
.pipe(uglifyJS())
.pipe(rev())
.pipe(gulp.dest(`${BUILD_PATH}js/`))
.pipe(rev.manifest())
.pipe(gulp.dest(`${BUILD_PATH}js/`));

这里我们通过gulp-rev来重命名模块,并且生成了manifest.json。那我们只需要在上面function asyncModule()中拿到manifest.json,获取就可以查询得到真正的模块路径。

这里你可以通过ajax.get来获取manifest.json然后保存在全局变量。不过也可以通过gulp-repalce,在构建的时候直接将整个json插入到代码中,就不需要单独加载一个json了。