写点什么

深入浅出 React(二):React 开发神器 Webpack

  • 2015 年 6 月 09 日
  • 本文字数:6331 字

    阅读完需:约 21 分钟

编者按:自 2013 年 Facebook 发布以来,React 吸引了越来越多的开发者,基于它的衍生技术,如 React Native、React Canvas 等也层出不穷。InfoQ 精心策划“深入浅出React ”系列文章,为读者剖析React 开发的技术细节。

上一篇我们对React 有了一个总体的认识,在介绍其中的技术细节之前,我们首先来了解一下用于React 开发和模块管理的主流工具Webpack。称之为React 开发神器有点标题党了,不过Webpack 确实是笔者见过的功能最为强大的前端模块管理和打包工具。虽然Webpack 是一个通用的工具,并不只适合于React,但是很多React 的文章或者项目都使用了Webpack,尤其是 react-hot-loader 这样的神器存在,让 Webpack 成为最主流的 React 开发工具。

CommonJS 和 AMD 是用于 JavaScript 模块管理的两大规范,前者定义的是模块的同步加载,主要用于 NodeJS;而后者则是异步加载,通过 requirejs 等工具适用于前端。随着 npm 成为主流的 JavaScript 组件发布平台,越来越多的前端项目也依赖于 npm 上的项目,或者自身就会发布到 npm 平台。因此,让前端项目更方便的使用 npm 上的资源成为一大需求。于是诞生了类似 browserify 这样的工具,代码中可以使用 require 函数直接以同步语法形式引入 npm 模块,打包后再由浏览器执行。

Webpack 其实有点类似 browserify,出自 Facebook 的 Instagram 团队,但功能比 browserify 更为强大。其主要特性如下:

  1. 同时支持 CommonJS AMD 模块(对于新项目,推荐直接使用 CommonJS);
  2. 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6 的支持;
  3. 可以基于配置或者智能分析打包成多个文件,实现公共模块或者按需加载;
  4. 支持对 CSS,图片等资源进行打包,从而无需借助 Grunt 或 Gulp;
  5. 开发时在内存中完成打包,性能更快,完全可以支持开发过程的实时打包需求;
  6. 对 sourcemap 有很好的支持,易于调试。

Webpack 将项目中用到的一切静态资源都视之为模块,模块之间可以互相依赖。Webpack 对它们进行统一的管理以及打包发布,其官方主页用下面这张图来说明 Webpack 的作用:

可以看到 Webpack 的目标就是对项目中的静态资源进行统一管理,为产品的最终发布提供最优的打包部署方案。本文就将围绕 React 对其相关用法做一个总体介绍,从而能让你将其应用在自己的实际项目之中。

安装 Webpack,并加载一个简单的 React 组件

Webpack 一般作为全局的 npm 模块安装:

复制代码
npm install -g webpack

之后便有了全局的 webpack 命令,直接执行此命令会默认使用当前目录的 webpack.config.js 作为配置文件。如果要指定另外的配置文件,可以执行:

复制代码
webpackconfig webpack.custom.config.js

尽管 Webpack 可以通过命令行来指定参数,但我们通常会将所有相关参数定义在配置文件中。一般我们会定义两个配置文件,一个用于开发时,另外一个用于产品发布。生产环境下的打包文件不需要包含 sourcemap 等用于开发时的代码。配置文件通常放在项目根目录之下,其本身也是一个标准的 CommonJS 模块。

一个最简单的 Webpack 配置文件 webpack.config.js 如下所示:

复制代码
module.exports = {
entry:[
'./app/main.js'
],
output: {
path: __dirname + '/assets/',
publicPath: "/assets/",
filename: 'bundle.js'
}
};

其中 entry 参数定义了打包后的入口文件,数组中的所有文件会按顺序打包。每个文件进行依赖的递归查找,直到所有相关模块都被打包。output 参数定义了输出文件的位置,其中常用的参数包括:

  • path: 打包文件存放的绝对路径
  • publicPath: 网站运行时的访问路径
  • filename: 打包后的文件名

现在来看如何打包一个 React 组件。假设有如下项目文件夹结构:

复制代码
- react-sample
+ assets/
- js/
Hello.js
entry.js
index.html
webpack.config.js

其中 Hello.js 定义了一个简单的 React 组件,使用 ES6 语法:

复制代码
var React = require('react');
class Hello extends React.Component {
render() {
return (
<h1>Hello {this.props.name}!</h1>
);
}
}

entry.js 是入口文件,将一个 Hello 组件输出到界面:

复制代码
var React = require('react');
var Hello = require('./Hello');
React.render(<Hello name="Nate" />, document.body);

index.html 的内容如下:

复制代码
<html>
<head></head>
<body>
<script src="/assets/bundle.js"></script>
</body>
</html>

在这里 Hello.js 和 entry.js 都是 JSX 组件语法,需要对它们进行预处理,这就要引入 webpack 的 JSX 加载器。因此在配置文件中加入如下配置:

复制代码
module: {
loaders: [
{ test: /\.jsx?$/, loaders: ['jsx?harmony']}
]
}

加载器的概念稍后还会详细介绍,这里只需要知道它能将 JSX 编译成 JavaScript 并加载为 Webpack 模块。这样在当前目录执行 webpack 命令之后,在 assets 目录将生成 bundle.js,打包了 entry.js 的内容。当浏览器打开当前服务器上的 index.html,将显示“Hello Nate!”。这是一个非常简单的例子,演示了如何使用 Webpack 来进行最简单的 React 组件打包。

加载 AMD 或 CommonJS 模块

在实际项目中,代码以模块进行组织,AMD 是在 CommonJS 的基础上考虑了浏览器的异步加载特性而产生的,可以让模块异步加载并保证执行顺序。而 CommonJS 的require函数则是同步加载。在 Webpack 中笔者更加推荐 CommonJS 方式去加载模块,这种方式语法更加简洁直观。即使在开发时,我们也是加载 Webpack 打包后的文件,通过 sourcemap 去进行调试。

除了项目本身的模块,我们也需要依赖第三方的模块,现在比较常用的第三方模块基本都通过 npm 进行发布,使用它们已经无需单独下载管理,需要时执行npm install即可。例如,我们需要依赖 jQuery,只需执行:

复制代码
npm install jquery —save-dev

更多情况下我们是在项目的 package.json 中进行依赖管理,然后通过直接执行 npm install 来安装所有依赖。这样在项目的代码仓库中并不需要存储实际的第三方依赖库的代码。

安装之后,在需要使用 jquery 的模块中需要在头部进行引入:

复制代码
var $ = require('jquery');
$('body').html('Hello Webpack!');

可以看到,这种以 CommonJS 的同步形式去引入其它模块的方式代码更加简洁。浏览器并不会实际的去同步加载这个模块,require 的处理是由 Webpack 进行解析和打包的,浏览器只需要执行打包后的代码。Webpack 自身已经可以完全处理 JavaScript 模块的加载,但是对于 React 中的 JSX 语法,这就需要使用 Webpack 的扩展加载器来处理了。

Webpack 开发服务器

除了提供模块打包功能,Webpack 还提供了一个基于 Node.js Express 框架的开发服务器,它是一个静态资源 Web 服务器,对于简单静态页面或者仅依赖于独立服务的前端页面,都可以直接使用这个开发服务器进行开发。在开发过程中,开发服务器会监听每一个文件的变化,进行实时打包,并且可以推送通知前端页面代码发生了变化,从而可以实现页面的自动刷新。

Webpack 开发服务器需要单独安装,同样是通过 npm 进行:

复制代码
npm install -g webpack-dev-server

之后便可以运行 webpack-dev-server 命令来启动开发服务器,然后通过 localhost:8080/webpack-dev-server/ 访问到页面了。默认情况下服务器以当前目录作为服务器目录。在 React 开发中,我们通常会结合 react-hot-loader 来使用开发服务器,因此这里不做太多介绍,只需要知道有这样一个开发服务器可以用于开发时的内容实时打包和推送。详细配置和用法可以参考官方文档

Webpack 模块加载器(Loaders)

Webpack 将所有静态资源都认为是模块,比如 JavaScript,CSS,LESS,TypeScript,JSX,CoffeeScript,图片等等,从而可以对其进行统一管理。为此 Webpack 引入了加载器的概念,除了纯 JavaScript 之外,每一种资源都可以通过对应的加载器处理成模块。和大多数包管理器不一样的是,Webpack 的加载器之间可以进行串联,一个加载器的输出可以成为另一个加载器的输入。比如 LESS 文件先通过 less-load 处理成 css,然后再通过 css-loader 加载成 css 模块,最后由 style-loader 加载器对其做最后的处理,从而运行时可以通过 style 标签将其应用到最终的浏览器环境。

对于 React 的 JSX 也是如此,它通过 jsx-loader 来载入。jsx-loader 专门用于载入 React 的 JSX 文件,Webpack 的加载器支持参数,jsx-loader 就可以添加?harmony 参数使其支持 ES6 语法。为了让 Webpack 识别什么样的资源应该用什么加载器去载入,需要在配置文件进行配置:通过正则表达式对文件名进行匹配。例如:

复制代码
module: {
preLoaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'jsxhint'
}],
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'react-hot!jsx-loader?harmony'
}, {
test: /\.less/,
loader: 'style-loader!css-loader!less-loader'
}, {
test: /\.(css)$/,
loader: 'style-loader!css-loader'
}, {
test: /\.(png|jpg)$/,
loader: 'url-loader?limit=8192'
}]
}

可以看到,该使用什么加载器完全取决于这里的配置,即使对于 JSX 文件,我们也可以用 js 作为后缀,从而所有的 JavaScript 都可以通过 jsx-loader 载入,因为 jsx 本身就是完全兼容 JavaScript 的,所以即使没有 JSX 语法,普通 JavaScript 模块也可以使用 jsx-loader 来载入。

加载器之间的级联是通过感叹号来连接,例如对于 LESS 资源,写法为 style-loader!css-loader!less-loader。对于小型的图片资源,也可以将其进行统一打包,由 url-loader 实现,代码中url-loader?limit=8192含义就是对于所有小于 8192 字节的图片资源也进行打包。这在一定程度上可以替代 Css Sprites 方案,用于减少对于小图片资源的 HTTP 请求数量。

除了已有加载器,你也可以自己实现自己的加载器,从而可以让Webpack 统一管理项目特定的静态资源。现在也已经有很多第三方的加载器实现常见静态资源的打包管理,可以参考Webpack 主页上的加载器列表

React 开发神器:react-hot-loader

Webpack 本身具有运行时模块替换功能,称之为 Hot Module Replacement (HMR)。当某个模块代码发生变化时,Webpack 实时打包将其推送到页面并进行替换,从而无需刷新页面就实现代码替换。这个过程相对比较复杂,需要进行多方面考虑和配置。而现在针对 React 出现了一个第三方 react-hot-loader 加载器,使用这个加载器就可以轻松实现 React 组件的热替换,非常方便。其实正是因为 React 的每一次更新都是全局刷新的虚拟 DOM 机制,让 React 组件的热替换可以成为通用的加载器,从而极大提高开发效率。

要使用 react-hot-loader,首先通过 npm 进行安装:

复制代码
npm install —save-dev react-hot-loader

之后,Webpack 开发服务器需要开启 HMR 参数 hot,为了方便,我们创建一个名为 server.js 的文件用以启动 Webpack 开发服务器:

复制代码
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('../webpack.config');
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
noInfo: false,
historyApiFallback: true
}).listen(3000, '127.0.0.1', function (err, result) {
if (err) {
console.log(err);
}
console.log('Listening at localhost:3000');
});

为了热加载 React 组件,我们需要在前端页面中加入相应的代码,用以接收 Webpack 推送过来的代码模块,进而可以通知所有相关 React 组件进行重新 Render。加入这个代码很简单:

复制代码
entry: [
'webpack-dev-server/client?http://127.0.0.1:3000', // WebpackDevServer host and port
'webpack/hot/only-dev-server',
'./scripts/entry' // Your appʼs entry point
]

需要注意的是,这里的 client? http://127.0.0.1:3000 需要和在 server.js 中启动 Webpack 开发服务器的地址匹配。这样,打包生成的文件就知道该从哪里去获取动态的代码更新。下一步,我们需要让 Webpack 用 react-hot-loader 去加载 React 组件,如上一节所介绍,这通过加载器配置完成:

复制代码
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'react-hot!jsx-loader?harmony'
},
]

做完这些配置之后,使用 Node.js 运行 server.js:

复制代码
node server.js

即可启动开发服务器并实现 React 组件的热加载。为了方便,我们也可以在 package.json 中加入一节配置:

复制代码
"scripts": {
"start": "node ./js/server.js"
}

从而通过 npm start 命令即可启动开发服务器。示例代码也上传在 Github 上,大家可以参考。

这样,React 的热加载开发环境即配置完成,任何修改只要以保存,就会在页面上立刻体现出来。无论是对样式修改,还是对界面渲染的修改,甚至事件绑定处理函数的修改,都可以立刻生效,不得不说是提高开发效率的神器。

将 Webpack 开发服务器集成到已有服务器

尽管 Webpack 开发服务器可以直接用于开发,但实际项目中我们可能必须使用自己的 Web 服务器。这就需要我们能将 Webpack 的服务集成到已有服务器,来使用 Webpack 提供的模块打包和加载功能。要实现这一点其实非常容易,只需要在载入打包文件时指定完整的 URL 地址,例如:

<script src="http://127.0.0.1:3000/assets/bundle.js"></script>这就告诉当前页面应该去另外一个服务器获得脚本资源文件,在之前我们已经在配置文件中指定了开发服务器的地址,因此打包后的文件也知道应该通过哪个地址去建立 Socket IO 来动态加载模块。整个资源架构如下图所示:

打包成多个资源文件

将项目中的模块打包成多个资源文件有两个目的:

  1. 将多个页面的公用模块独立打包,从而可以利用浏览器缓存机制来提高页面加载效率;
  2. 减少页面初次加载时间,只有当某功能被用到时,才去动态的加载。

Webpack 提供了非常强大的功能让你能够灵活的对打包方案进行配置。首先来看如何创建多个入口文件:

复制代码
{
entry: { a: "./a", b: "./b" },
output: { filename: "[name].js" },
plugins: [ new webpack.CommonsChunkPlugin("init.js") ]
}

可以看到,配置文件中定义了两个打包资源“a”和“b”,在输出文件中使用方括号来获得输出文件名。而在插件设置中使用了 CommonsChunkPlugin,Webpack 中将打包后的文件都称之为“Chunk”。这个插件可以将多个打包后的资源中的公共部分打包成单独的文件,这里指定公共文件输出为“init.js”。这样我们就获得了三个打包后的文件,在 html 页面中可以这样引用:

复制代码
<script src="init.js"></script>
<script src="a.js"></script>
<script src="b.js"></script>

除了在配置文件中对打包文件进行配置,还可以在代码中进行定义:require.ensure,例如:

复制代码
require.ensure(["module-a", "module-b"], function(require) {
var a = require("module-a");
// ...
});

Webpack 在编译时会扫描到这样的代码,并对依赖模块进行自动打包,运行过程中执行到这段代码时会自动找到打包后的文件进行按需加载。

小结

本文结合 React 介绍了 Webpack 的基本功能和用法,希望能让大家对这个新兴而强大的模块管理工具有一个总体的认识,并能将其应用在实际的项目开发中。笔者也将其应用在之前提供的 React 示例组件项目中,大家可以参考。除了这里介绍的功能,Webpack 还有许多强大的特性,例如插件机制、支持动态表达式的 require、打包文件的智能重组、性能优化、代码混淆等等。限于篇幅不再一一介绍,其官方文档也非常完善,需要时可以参考。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 6 月 09 日 00:47119573

评论

发布
暂无评论
发现更多内容

uni-app技术分享| 用uni-app实现拖动的诀窍

anyRTC开发者

uni-app 音视频 WebRTC 移动开发 视频通话

融云 x 微脉:让互联网医疗服务更长远、更连续

融云 RongCloud

通信云 医疗信息化

深入剖析 Spring WebFlux

vivo互联网技术

spring WebFlux java

如何基于Jupyter notebook搭建Spark集群开发环境

华为云开发者社区

spark Jupyter Notebook 集群 Spark集群 Sparkmagic

政企融合商城,运营商打开B端市场利器

鲸品堂

运营商

北冥多样性计算融合架构系列解读之 一文读懂华为昇思科学计算

Geek_32c4d0

信息流推荐系统智能交付解决方案探索

百度Geek说

后端

北冥多样性计算融合架构系列解读之 一文读懂华为MindStudio统一工具链 多样性计算系统下的开发挑战

Geek_32c4d0

算力 多样性计算 北冥

1688 商家基于 HarmonyOS 的多屏协同直播技术方案

阿里巴巴移动技术

ios android 客户端开发 HarmonyOS 直播技术

和12岁小同志搞创客开发:如何驱动LED数码管?

不脱发的程序猿

少儿编程 DIY 创客开发 LED数码管

一个神器,让写东西快得飞起

锋享前端

小工具

肝不爆我不停!这套阿里10月最新面试手册(题+视频)爆砍55K+16薪Offer!

Java架构追梦

Java 阿里巴巴 后端 java面试 offer

怒肝半月!Python 学习路线+资源大汇总

程序员鱼皮

Python 人工智能 大数据 算法 数据分析

阿里大牛把算法面试必问的排序、递归、链表、栈、队列、二叉树、动态规划撸完了

编程 程序员 架构 面试 算法

在Github找的一份面试资料,看了感觉直接啥也不是

程序员小呆

程序员 面试 架构师 java

新人融入团队的必备python技巧,python 编码规范,滚雪球学Python第4季12篇

梦想橡皮擦

10月日更

StreamNative 宣布 2300 万美元 A 轮融资,Prosperity7 Ventures 与华泰创新联合领投

Apache Pulsar

融资 Apache Pulsar StreamNative

从一盏路灯,看亿万级物联网联接的智能之路

华为云开发者社区

物联网 IoT 华为云 LiteOS NB- IoT

区块链底层平台如何实现国密改造?

旺链科技

区块链 国密改造

北冥多样性计算融合架构系列解读之 一文读懂北冥基础使能:毕昇C++编译器及北冥融合加速库

Geek_32c4d0

北冥多样性计算融合架构系列解读之 一文读懂华为多瑙统一调度器

Geek_32c4d0

5G NR 网络类型移动开发小记

阿里巴巴移动技术

ios android 5G 移动开发 移动网络

当物联网遇上云原生:K8s向边缘计算渗透中

华为云开发者社区

Kubernetes 云原生 物联网 边缘计算 kubeedge

回顾|鉴释梁宇宁在嵌入式技术大会发表WASM安全性演讲

鉴释

操作系统 嵌入式 Wasm

宇宙条一面:十道经典面试题解析

编程 架构 面试 后端 计算机

WICC · 广州开启报名!包揽最「in」社交、泛娱乐、出海话题

融云 RongCloud

开发者 游戏 通信云 社交 泛娱乐

猛攻一线大厂,Java架构面试点+技术点标准手册完整版来了!

Java 程序员 架构 面试 后端

👊 【Spring技术实战】@Async机制的使用技巧以及异步注解源码解析

浩宇天尚

Java spring API 10月日更

解决外卖配送最后一公里:外卖柜存在哪些问题

石头IT视角

「ANR」Android SIGQUIT(3) 信号拦截与处理

阿里巴巴移动技术

android 信号量 anr

把Github“炸”翻了!的阿里面试总结,惨遭多家大厂威胁下架!

程序员小呆

Java 程序员 面试 架构师 java面试

深入浅出React(二):React开发神器Webpack_移动_王沛_InfoQ精选文章