1. 前言
随着前端项目的不断扩大, 一个原本简单的网页应用所引用的 js 文件可能变得越来越庞大尤其在近期流行的单页面应用中, 越来越依赖一些打包工具(例如 webpack), 通过这些打包工具将需要处理相互依赖的模块直接打包成一个单独的 bundle 文件, 在页面第一次载入时, 就会将所有的 js 全部载入但是, 往往有许多的场景, 我们并不需要在一次性将单页应用的全部依赖都载下来例如: 我们现在有一个带有权限的 "订单后台管理" 单页应用, 普通管理员只能进入 "订单管理" 部分, 而超级用户则可以进行 "系统管理"; 或者, 我们有一个庞大的单页应用, 用户在第一次打开页面时, 需要等待较长时间加载无关资源这些时候, 我们就可以考虑进行一定的代码拆分(code splitting)
2. 实现方式
2.1 简单的按需加载
代码拆分的核心目的, 就是实现资源的按需加载考虑这么一个场景, 在我们的网站中, 右下角有一个类似聊天框的组件, 当我们点击圆形按钮时, 页面展示聊天组件
btn.addEventListener('click', function(e) {
// 在这里加载 chat 组件相关资源 chat.js
});
从这个例子中我们可以看出, 通过将加载 chat.js 的操作绑定在 btn 点击事件上, 可以实现点击聊天按钮后聊天组件的按需加载而要动态加载 js 资源的方式也非常简单 (方式类似熟悉的 jsonp) 通过动态在页面中添加 < scrpt > 标签, 并将 src 属性指向该资源即可
btn.addEventListener('click', function(e) {
// 在这里加载 chat 组件相关资源 chat.js
var ele = document.createElement('script');
ele.setAttribute('src','/static/chat.js');
document.getElementsByTagName('head')[0].appendChild(ele);
});
代码拆分就是为了要实现按需加载所做的工作想象一下, 我们使用打包工具, 将所有的 js 全部打包到了 bundle.js 这个文件, 这种情况下是没有办法做到上面所述的按需加载的, 因此, 我们需要讲按需加载的代码在打包的过程中拆分出来, 这就是代码拆分那么, 对于这些资源, 我们需要手动拆分么? 当然不是, 还是要借助打包工具下面就来介绍 webpack 中的代码拆分
3. 代码拆分
这里回到应用场景, 介绍如何在 webpack 中进行代码拆分在 webpack 有多种方式来实现构建是的代码拆分
3.1 import()
这里的 import 不同于模块引入时的 import, 可以理解为一个动态加载的模块的函数 (function-like), 传入其中的参数就是相应的模块例如对于原有的模块引入 import react from 'react'可以写为 import('react') 但是需要注意的是, import()会返回一个 Promise 对象因此, 可以通过如下方式使用:
btn.addEventListener('click', e = >{
// 在这里加载 chat 组件相关资源 chat.js
import('/components/chart').then(mod = >{
someOperate(mod);
});
});
可以看到, 使用方式非常简单, 和平时我们使用的 Promise 并没有区别当然, 也可以再加入一些异常处理:
btn.addEventListener('click', e = >{
import('/components/chart').then(mod = >{
someOperate(mod);
}).catch(err => {
console.log('failed');
});
});
当然, 由于 import()会返回一个 Promise 对象, 因此要注意一些兼容性问题解决这个问题也不困难, 可以使用一些 Promise 的 polyfill 来实现兼容可以看到, 动态 import()的方式不论在语意上还是语法使用上都是比较清晰简洁的
3.2 require.ensure()
在 webpack 2 的官网上写了这么一句话:
require.ensure() is specific to webpack and superseded by import().
所以, 在 webpack 2 里面应该是不建议使用 require.ensure()这个方法的但是目前该方法仍然有效, 所以可以简单介绍一下包括在 webpack 1 中也是可以使用下面是 require.ensure()的语法:
require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
require.ensure()接受三个参数:
第一个参数 dependencies 是一个数组, 代表了当前 require 进来的模块的一些依赖;
第二个参数 callback 就是一个回调函数其中需要注意的是, 这个回调函数有一个参数 require, 通过这个 require 就可以在回调函数内动态引入其他模块值得注意的是, 虽然这个 require 是回调函数的参数, 理论上可以换其他名称, 但是实际上是不能换的, 否则 webpack 就无法静态分析的时候处理它;
第三个参数 errorCallback 比较好理解, 就是处理 error 的回调;
第四个参数 chunkName 则是指定打包的 chunk 名称
因此, require.ensure()具体的用法如下:
btn.addEventListener('click', e => {
require.ensure([], require => {
let chat = require('/components/chart');
someOperate(chat);
}, error => {
console.log('failed');
}, 'mychat');
});
3.3 Bundle Loader
除了使用上述两种方法, 还可以使用 webpack 的一些组件例如使用 Bundle Loader:
npm i --save bundle-loader
使用 require("bundle-loader!./file.js")来进行相应 chunk 的加载该方法会返回一个 function, 这个 function 接受一个回调函数作为参数
let chatChunk = require("bundle-loader?lazy!./components/chat");
chatChunk(function(file) {
someOperate(file);
});
和其他 loader 类似, Bundle Loader 也需要在 webpack 的配置文件中进行相应配置 Bundle-Loader 的代码也很简短, 如果阅读一下可以发现, 其实际上也是使用 require.ensure()来实现的, 通过给 Bundle-Loader 返回的函数中传入相应的模块处理回调函数即可在 require.ensure()的中处理, 代码最后也列出了相应的输出格式:
/*
Output format:
var cbs = [],
data;
module.exports = function(cb) {
if(cbs) cbs.push(cb);
else cb(data);
}
require.ensure([], function(require) {
data = require("xxx");
var callbacks = cbs;
cbs = null;
for(var i = 0, l = callbacks.length; i < l; i++) {
callbacks[i](data);
}
});
*/
4. react-router v4 中的代码拆分
最后, 回到实际的工作中, 基于 webpack, 在 react-router4 中实现代码拆分 react-router 4 相较于 react-router 3 有了较大的变动其中, 在代码拆分方面, react-router 4 的使用方式也与 react-router 3 有了较大的差别
在 react-router 3 中, 可以使用 Route 组件中 getComponent 这个 API 来进行代码拆分 getComponent 是异步的, 只有在路由匹配时才会调用但是, 在 react-router 4 中并没有找到这个 API, 那么如何来进行代码拆分呢?
在
react-router 4
官网上有一个代码拆分的例子其中, 应用了 Bundle Loader 来进行按需加载与动态引入
import loadSomething from 'bundle-loader?lazy!./Something'
然而, 在项目中使用类似的方式后, 出现了这样的警告:
Unexpected '!' in 'bundle-loader?lazy!./component/chat'.Do not use import syntax to configure webpack loaders import / no - webpack - loader - syntax Search
for the keywords to learn more about each error.
在 webpack 2 中已经不能使用 import 这样的方式来引入 loader 了(no-webpack-loader-syntax)
Webpack allows specifying the loaders to use in the import source string using a special syntax like this: var moduleWithOneLoader = require("my-loader!./my-awesome-module");
This syntax is non - standard,
so it couples the code to Webpack.The recommended way to specify Webpack loader configuration is in a Webpack configuration file.
我的应用使用了 create-react-app 作为脚手架, 屏蔽了 webpack 的一些配置当然, 也可以通过运行 npm run eject 使其暴露 webpack 等配置文件然而, 是否可以用其他方法呢? 当然
这里就可以使用之前说到的两种方式来处理: import()或 require.ensure()
和官方实例类似, 我们首先需要一个异步加载的包装组件 BundleBundle 的主要功能就是接收一个组件异步加载的方法, 并返回相应的 react 组件:
export default class Bundle extends Component {
constructor(props) {
super(props);
this.state = {
mod: null
};
}
componentWillMount() {
this.load(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load(props) {
this.setState({
mod: null
});
props.load((mod) => {
this.setState({
mod: mod.default ? mod.default : mod
});
});
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
在原有的例子中, 通过 Bundle Loader 来引入模块:
import loadArticleDetail from 'bundle-loader?lazy!./functions/ArticleDetial'const ArticleDetail = (props) = >( < Bundle load = {
loadArticleDetail
} > { (ArticleDetail) = ><About {...props
}
/>}
</Bundle > )
注意: webpack 2 还是可以用 Bundle Loader 的
由于不再使用 Bundle Loader, 我们可以使用 import()对该段代码进行改写:
const ArticleDetail = (props) = >( < Bundle load = { () = >import('./functions/ArticleDetail')
} > { (ArticleDetail) = ><ArticleDetail {...props
}
/> }
</Bundle > )
需要注意的是, 由于 import()会返回一个 Promise 对象, 因此 Bundle 组件中的代码也需要相应进行调整
export default class Bundle extends Component {
constructor(props) {
super(props);
this.state = {
mod: null
};
}
componentWillMount() {
this.load(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps)
}
}
load(props) {
this.setState({
mod: null
});
// 注意这里, 使用 Promise 对象; mod.default 导出默认
props.load().then((mod) => {
this.setState({
mod: mod.default ? mod.default : mod
});
});
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
路由部分没有变化
<Route exact path="/post/:id" component={ArticleDetail}/>
这时候, 执行 npm run start, 可以看到在载入最初的页面时加载的资源如下
image.png
而当点击触发到 / post 路径时, 可以看到
image.png
动态加载了 2.chunk.js 这个 js 文件, 如果打开这个文件查看, 就可以发现这个就是我们刚才动态 import()进来的模块
当然, 除了使用 import()仍然可以使用 require.ensure()来进行模块的异步加载相关示例代码如下:
const ArticleDetail = (props) = >( < Bundle load = { (cb) = >{
require.ensure([], require = >{
cb(require('./function/ArticleDetail'))
});
}
} > { (ArticleDetail) = ><ArticleDetail {...props
}
/> }
</Bundle > ); export
default class Bundle extends Component {
constructor(props) {
super(props);
this.state = {
mod: null
};
}
load = props = >{
this.setState({
mod: null
});
props.load(mod = >{
this.setState({
mod: mod ? mod: null
});
});
}
componentWillMount() {
this.load(this.props);
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null
}
}
此外, 如果是直接使用 webpack config 的话, 也可以进行如下配置
output: {
// The build folder.
path: paths.appBuild,
// There will be one main bundle, and one file per asynchronous chunk.
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
},
5. 结束
代码拆分在单页应用中非常常见, 对于提高单页应用的性能与体验具有一定的帮助我们通过将第一次访问应用时, 并不需要的模块拆分出来, 通过 scipt 标签动态加载的原理, 可以实现有效的代码拆分在实际项目中, 使用 webpack 中的 import()require.ensure()或者一些 loader(例如 Bundle Loader)来做代码拆分与组件按需加载
后续打算弄一个脚手架出来
本项目地址: geekjc-antd-mobile
来源: http://www.jianshu.com/p/d3cd57f352fb