点击查看第二章
React+Redux前端开发实战
徐顺发 编著
第1章 React入门
React开源于2013年5月,一发布就引起了开发者广泛的关注和认可。截至笔者写作本章内容,React在GitHub上面的star数量已经达到129680。这是一个非常庞大的star数量,在主流JavaScript(简称JS)库中排名第二。其后来衍生的React Native在开源的第一天在GitHub上面的Start数量就达到了5000个,由此可见其受欢迎的程度非同凡响。本章将带领读者正式踏入React的世界。通过本章,读者将学会如何在现代前端工程项目中使用React开发简单的组件,同时也会了解React的基本设计思想。
假如读者之前接触过jQuery之类直接操作DOM(Document Object Model,文档对象模型)的JS写法或其他JS库,现在起,请跳出以往的思维,拥抱React的理念和思想。
1.1 开始学习React之前
工欲善其事,必先利其器。现在,开发生态系统需要读者基于Node.js、Webpack、ES 6、ES 7等进行开发,其中,Node.js是前端工程化的重要支柱。所以在学习React之前,读者需要对Node.js、NPM以及ES 6(ECMAScript 6.0)有一定的认识。本节将带领读者熟悉这些基本概念,如果读者对本节内容已有一定了解,可以直接跳过。
1.1.1 下载与使用Node.js和NPM
Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它使JavaScript能够脱离浏览器直接运行。通过Node.js的开源命令行工具,可以让JavaScript在本地运行。
Node.js通过官网https://nodejs.org/en/ 下载。下载后可以直接安装,这里安装过程不再详述,相信读者已经有安装软件的经验。
安装后在终端输入命令:
node -v
可以验证Node.js在本地是否安装成功。如果输入后显示一个类似于v8.9.3的版本号,就说明安装成功。
NPM(Node Package Manager)是Node.js的包管理工具(我们常说的Node包),是全球最大的开源库生态系统。它允许开发人员使用创建的JavaScript脚本运行本地项目(比如开启一个本地服务器)、下载应用程序所依赖的JavaScript包等。这些在本书后面的前端项目搭建或引用第三方插件时都会用到,用法如下:
npm install <package_name>
由于网络环境问题,有的Node包会出现无法下载或下载速度很慢的情况,此时可以使用淘宝NPM镜像来代替NPM,安装方式如下:
npm install -g cnpm --registry=https://registry.npm.taobao.org
淘宝NPM镜像其实是一个对NPM的复制,用法与NPM一样,命令如下:
cnpm install <package_name>
1.1.2 模块打包工具之Browserify
使用React进行开发,势必会包含对各种语法的解析。这需要将所有模块打包成一个或多个文件,以方便在浏览器中执行使用。拥有一个好的代码构建工具,无疑能提升开发者的开发效率。
由于近几年前端开发对于模块化思想的追崇,出现了如Require.js和sea.js等优秀的模块化开发工具。模块化实现了项目的高内聚低耦合,降低了模块与模块之间的依赖关系。特别是对于大型复杂的单页应用来说,模块化更能提升应用的性能。
此外,还有一些浏览器不支持的功能(如ES 6中提出的export和import),需要借助一些工具实现。
基于以上种种原因,就引出了接下来要介绍的两款较为流行的模块打包工具Browserify和Webpack。本节先介绍Browserify。
Browserify是一个浏览器端代码模块化的JavaScript打包工具,其图标如图1.1所示。可以使用类似于Node的require()方法加载NPM模块,在HTML文件中使用script标签引用Browserify编译后的代码。
使用NPM全局安装Browserify:
npm install -g browserify
命令行参数及说明如下:
- –outfile,-o:将Browserify日志打印到文件;
- –require,-r:绑定模块名或文件,用逗号分隔;
- –entry,-e:应用程序的入口;
- –ignore,-i:省略输出;
- –external,-x:从其他绑定引入文件;
- –transform,-t:对上层文件进行转换;
- –command,-c:对上层文件使用转换命令;
- –standalone
-s:生成一个UMD(Universal Module Definition)类型的接口,提供给其他模块使用;
- –debug -d:激活source maps调试文件;
- –help,-h:显示帮助信息;
- –insert-globals,–ig,–fast:跳过检查,定义全局变量;
- –detect-globals,–dg:检查全局变量是否存在;
- –ignore-missing,–im:忽略require()方法;
- –noparse=FILE:不解析文件,直接build;
- –deps:打印完整输出日志;
- –list:打印每个文件的依赖关系;
- –extension=EXTENSION:指定扩展名的文件作为模块加载,允许多次设置。
使用Browserify时必须在终端,例如:
borwserify index.js > bun dle.js
上述代码将index.js编译为bundle.js,生成的文件在浏览器端能直接运行。
总结一下使用Browserify的步骤:写Node程序或代码模块→用Browserify预编译成bundle.js→在HTML中引用bundle.js。
1.1.3 模块打包工具之Webpack
本节重点介绍Webpack,这是本书案例代码最常用的打包工具。当然市面上类似的打包工具还有Rollup、Parceljs等,感兴趣的读者可自行研究。
Webpack是一个“模块打包器”,如图1.2所示。它能根据模块的依赖关系递归地构建一个依赖关系图(Dependency Graph),当中包含了应用程序需要的所有模块,最后打包成一个或多个bundle。
Webpack的全局安装命令如下:
npm install -g webpack webpack-cli
同Browserify一样,Webpack最后打包输出的静态资源在HTML中被引用。但Webpack相比Browserify又多了更多的特色,Webpack能将CSS和图片等打包到同一个包;打包前还能对文件进行预编译(比如Less、TypeScript和JSX等);还能配置多入口,将包拆分;还能进行“热”替换等。
在Webpack中有4个核心概念:
- entry(入口);
- output(输出);
- loader(转换器);
- plugins(插件)。
1.entry(入口)
entry(入口)用于指引Webpack应该从哪个模块开始,它是构建的入口。之后Webpack会自动找出应用内其他相互依赖的内容进行打包。通过在Webpack配置文件中配置entry属性来指定入口。虽然一般项目中只指定一个入口,但实际上是可以指定多个入口的。
entry配置示例:
module.exports = {
entry: './src/file.js'
}
2.output(出口)
output(出口)告诉Webpack所构建的bundles在哪里输出,默认输出路径是./dist。
output配置示例:
const path = require('path');
module.exports = {
entry: './src/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
上面的配置通过output.path和output.filename属性来自定义Webpack打包后bundle的路径和名称。
3.loader(转换器)
loader用于配置Webpack处理一些非JS文件,因为Webpack本身只能理解JavaScript。通过loader可以实现import导入的任何类型模块(如.jsx,.css,.less等)。
loader配置示例:
const path = require('path');
module.exports ={
entry: './src/file.js', // 打包入口
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{ test: /\.less$/, use: 'less-loader' }
]
}
};
上面的配置中,loader的test属性告诉Webpack需要处理的“对象”;use属性告诉Webpack用什么去处理。当Webpack打包的时候会识别所有后缀为.less的文件,并用less-loader去转换。
4.plugins(插件)
plugins(插件)的作用主要是打包优化、压缩等,它的功能同loader一样极其强大,使用任何插件时只需要require()进来即可。
plugins配置示例:
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过NPM安装
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [
{ test: /\.less$/, use: 'less-loader' }
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
在真实项目中会区分开发环境(production)和生产环境(development),两种环境可以通过mode参数来配置,如下:
module.exports = {
mode: 'production'
};
接下来通过新建一个项目来深入了解Webpack的使用。
1.1.4 第一个Webpack构建实战
本节将构建一个很简单的项目,重点是让读者先了解Webpack。新建项目app(一个空目录),并以此为根目录。
使用Webpack构建项目示例:
(1)在根目录下新建文件index.html,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>document</title>
</head>
<body>
<script type="text/javascript" src="bundle.js" charset="utf-8"></script>
</body>
</html>
(2)新建文件a.js,内容如下:
document.write("It works from a.js");
此时目录结构为:
├── a.js
└── index.html
(3)在根目录下执行如下命令(Webpack 4以上版本):
webpack a.js -o bundle.js
Webpack会根据模块的依赖关系进行静态分析,模块a.js被打包为bundle.js。终端效果如图1.3所示,同时根目录下会生成一个新的文件./bundle.js。
1.1.5 Webpack loader实战
使用Webpack loader构建项目示例:
在前面示例基础上对文本样式进行修改,在根目录下安装loader,命令如下:
npm install css-loader style-loader –D
(1)新建文件./webpack.config.js,代码如下,其中的rules用来配置loader。
module.exports = {
entry: "./a.js",
output: {
path: __dirname,
filename: "bundle.js"
},
mode: "production",
module: {
rules: [
{ test: /\.css$/, loader: "style-loader!css-loader" }
]
}
};
(2)新建./style.css样式文件,代码如下:
body {
color: red;
}
(3)在a.js中引入样式文件:
import "./style.css";
此时项目结构如下:
├── a.js
├── index.html
├── package.json
├── style.css
└── webpack.config.js
(4)在终端执行Webpack命令:
webpack
Webpack会默认找到webpack.config.js配置文件,直接执行里面的命令,显示结果如图1.4所示。
(5)根目录下会再次生成bundle.js文件。打开浏览器后,黑色英文Hello World!的颜色将变为红色。
实际项目中,一般不会直接执行Webpack命令,而是在package.json中的scripts内配置:
"scripts": {
"a" :"webpack --config ./webpack.config.js"
},
其中,参数--config用于指定读取哪个配置文件。如果没有指定--config,Webpack会默认读取webpack.config.js或webpackfile.js文件。项目中通常会配置两三个Webpack配置文件,命名时一般会带上环境,如webpack.config.base.js,webpack.config.dev.js和webpack.config.prod.js。
然后在终端执行:
npm run a
1.1.6 Webpack配置详解
前面已经使用过webpack.config.js中的一些配置,本节将详细介绍Webpack的主要配置项。
(1)模式mode:
mode: "production", // 生产模式
mode: "development", // 开发模式
(2)入口entry:
entry: "./app/entry", // 入口可以是字符串、对象或数组
entry: ["./app/entry1", "./app/entry2"],
entry: {
a: "./app/entry-a",
b: ["./app/entry-b1", "./app/entry-b2"]
},
(3)出口output:
output: { // Webpack如何输出结果的相关选项
path: path.resolve(__dirname, "dist"), // 字符串
// 所有输出文件的目标路径
// 必须是绝对路径(使用Node.js的path模块)
filename: "bundle.js", // 字符串
filename: "[name].js", // 用于多个入口点(entry point)
filename: "[chunkhash].js", // 用于长效缓存
// 「入口分块(entry chunk)」的文件名模板
publicPath: "/assets/", // 字符串
publicPath: "",
publicPath: "https://cdn.example.com/",
// 输出解析文件的目录,URL相当于HTML页面
library: "MyLibrary", // string,
// 导出库(exported library)的名称
libraryTarget: "umd", // 通用模块定义
libraryTarget: "commonjs2", // 作为exports的属性导出
libraryTarget: "commonjs", // 作为exports的属性导出
libraryTarget: "amd", // 使用amd定义方法来定义
libraryTarget: "this", // 在this上设置属性
libraryTarget: "var", // 变量定义于根作用域下
libraryTarget: "assign", // 盲分配(blind assignment)
libraryTarget: "window", // 在window对象上设置属性
libraryTarget: "global", // 设置global 对象
libraryTarget: "jsonp", // jsonp wrapper
// 导出库(exported library)的类型
/- 高级输出配置(点击显示) */ pathinfo: true, // boolean
// 以文件形式异步加载模块配置项
chunkFilename: "[id].js",
chunkFilename: "[chunkhash].js", // 长效缓存(/guides/caching)
// 「附加分块(additional chunk)」的文件名模板
jsonpFunction: "myWebpackJsonp", // string
// 用于加载分块的 JSONP 函数名
sourceMapFilename: "[file].map", // string
sourceMapFilename: "sourcemaps/[file].map", // string
// 「source map 位置」的文件名模板
devtoolModuleFilenameTemplate: "webpack:///[resource-path]", // string
// 「devtool 中模块」的文件名模板 devtoolFallbackModuleFilenameTemplate: "webpack:///[resource-path]?
[hash]", // string
// 「devtool 中模块」的文件名模板(用于冲突)
umdNamedDefine: true, // boolean
// 在 UMD 库中使用命名的 AMD 模块
crossOriginLoading: "use-credentials", // 枚举
crossOriginLoading: "anonymous",
crossOriginLoading: false,
// 指定运行时如何发出跨域请求问题
/- 专家级输出配置(自行承担风险) */
}
(4)module模块处理:
module: {
// 关于模块配置
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.jsx?$/,
include: [
path.resolve(__dirname, "app")
],
exclude: [
path.resolve(__dirname, "app/demo-files")
],
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test和include具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于test和include)
// 最佳实践:
// - 只在test和文件名匹配 中使用正则表达式
// - 在include和exclude中使用绝对路径数组
// - 尽量避免使用exclude,更倾向于使用include
issuer: { test, include, exclude },
// issuer 条件(导入源)
enforce: "pre",
enforce: "post",
// 标识应用这些规则,即使规则覆盖(高级选项)
loader: "babel-loader",
// 应该应用的 loader,它相对上下文解析
// 为了更清晰,-loader 后缀在Webpack 2中不再是可选的
options: {
presets: ["es2015"]
},
// loader的可选项
},
{
test: /\.html$/,
use: [
// 应用多个loader和选项
"htmllint-loader",
{
loader: "html-loader",
options: {
/- ... */
}
}
]
},
{ oneOf: [ /- rules */ ] },
// 只使用这些嵌套规则之一
{ rules: [ /- rules */ ] },
// 使用所有嵌套规则(合并可用条件)
{ resource: { and: [ /- 条件 */ ] } },
// 仅当所有条件都匹配时才匹配
{ resource: { or: [ /- 条件 */ ] } },
{ resource: [ /- 条件 */ ] },
// 任意条件匹配时匹配(默认为数组)
{ resource: { not: /- 条件 */ } }
// 条件不匹配时匹配
],
/- 高级模块配置(点击展示) */
noParse: [
/special-library\.js$/
],
// 不解析这里的模块
unknownContextRequest: ".",
unknownContextRecursive: true,
unknownContextRegExp: /^\.\/.*$/,
unknownContextCritical: true,
exprContextRequest: ".",
exprContextRegExp: /^\.\/.*$/,
exprContextRecursive: true,
exprContextCritical: true,
wrappedContextRegExp: /.*/,
wrappedContextRecursive: true,
wrappedContextCritical: false,
// specifies default behavior for dynamic requests
},
(5)resolve解析:
resolve: {
// 解析模块请求的选项(不适用于对 loader 解析)
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
// 用于查找模块的目录
extensions: [".js", ".json", ".jsx", ".css"],
// 使用的扩展名
alias: {
// 模块别名列表
"module": "new-module",
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-
module/path/file"
"only-module$": "new-module"
// 起别名 "only-module" -> "new-module",但不匹配 "only-module/path/
file" -> "new-module/path/file"
"module": path.resolve(__dirname, "app/third/module.js"),
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
},
/- 可供选择的别名语法(点击展示) */
alias: [
{
name: "module",
// 旧的请求
alias: "new-module",
// 新的请求
onlyModule: true
// 如果为true,那么只有module是别名
// 如果为false,则"module/inner/path" 也是别名
}
],
/- 高级解析选项(点击展示) */ symlinks: true,
// 遵循符号链接(symlinks)到新位置
descriptionFiles: ["package.json"],
// 从 package 描述中读取的文件
mainFields: ["main"],
// 从描述文件中读取的属性
// 当请求文件夹时
aliasFields: ["browser"],
// 从描述文件中读取的属性
// 以对此 package 的请求起别名
enforceExtension: false,
// 如果为 true,请求必不包括扩展名
// 如果为 false,请求可以包括扩展名
moduleExtensions: ["-module"],
enforceModuleExtension: false,
// 类似 extensions/enforceExtension,但是用模块名替换文件
unsafeCache: true,
unsafeCache: {},
// 为解析的请求启用缓存
// 这是不安全的,因为文件夹结构可能会改动
// 但是性能改善是很大的
cachePredicate: (path, request) => true,
// predicate function which selects requests for caching
plugins: [
...
]
// 应用于解析器的附加插件
},
(6)performance打包后命令行如何展示性能提示,如果超过某个大小时是警告还是报错:
performance: {
hints: "warning", // 枚举 hints: "error", // 性能提示中抛出错误
hints: false, // 关闭性能提示
maxAssetSize: 200000, // 整数类型(以字节为单位)
maxEntrypointSize: 400000, // 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
}
},
(7)devtool用于配置调试代码的方式,打包后的代码和原始代码存在较大的差异,此选项控制是否生成,以及如何生成sourcemap:
devtool: "source-map", // enum devtool: "inline-source-map",
// 嵌入到源文件中
devtool: "eval-source-map", // 将sourcemap嵌入到每个模块中
devtool: "hidden-source-map", // sourcemap 不在源文件中引用
devtool: "cheap-source-map", // 没有模块映射(module mappings)的
sourcemap低级变体(cheap-variant)
devtool: "cheap-module-source-map", // 有模块映射(module mappings)的
sourcemap低级变体
devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的sourcemap是最详细的
(8)context基础目录,绝对路径,用于从配置中解析入口起点(entry point)和loader:
context: __dirname, // string(绝对路径!)
(9)target构建目标:
target: "web", // 枚举 target: "webworker", // WebWorker
target: "webworker" //webworker
target: "node", // Node.js 通过require加载模块
target: "async-node", // Node.js 通过fs和vm加载分块
target: "node-webkit", // 编译为webkit可用,并且用jsonp去加载分块
target: "electron-main", // electron,主进程(main process)
target: "electron-renderer", // electron,渲染进程(renderer process)
target: (compiler) => { /- ... */ }, // 自定义
(10)externals外部拓展:
externals: ["react", /^@angular\//], externals: "react", // string(精
确匹配)
externals: /^[a-z\-]+($|\/)/, // 正则
externals: { // 对象
angular: "this angular", // this["angular"]
react: { // 使用UMD规范
commonjs: "react",
commonjs2: "react",
amd: "react",
root: "React"
}
},
externals: (request) => { /- ... */ return "commonjs " + request }
(11)stats统计信息:
stats: "errors-only",
stats: { //object
assets: true,
colors: true,
errors: true,
errorDetails: true,
hash: true,
...
},
(12)devServer配置本地运行环境:
devServer: {
proxy: { // 服务器代理
'/api': 'http://localhost:3000'
},
contentBase: path.join(__dirname, 'public'), // boolean、string或array,
服务器资源根目录
compress: true, // 启用gzip压缩
historyApiFallback: true, // 返回404页面时定向到特定页面
...
},
(13)plugins插件:
plugins: [
...
],
(14)其他插件:
// 附加插件列表
/- 高级配置(点击展示) */
resolveLoader: { /- 等同于 resolve */ }
// 独立解析选项的 loader
parallelism: 1, // number
// 限制并行处理模块的数量
profile: true, // boolean数据类型
// 捕获时机信息
bail: true, //boolean
// 在第一个错误出错时抛出,而不是无视错误
cache: false, // boolean
// 禁用/启用缓存
watch: true, // boolean
// 启用观察
watchOptions: {
aggregateTimeout: 1000, // in ms毫秒单位
// 将多个更改聚合到单个重构建(rebuild)
poll: true,
poll: 500, // 间隔单位ms
// 启用轮询观察模式
// 必须用在不通知更改的文件系统中
// 即 nfs shares(Network FileSystem)
},
node: {
// Polyfills和mocks可以在非Node环境中运行Node.js环境代码
// environment code in non-Node environments.
console: false, // boolean | "mock"布尔值或"mock"
global: true, // boolean | "mock"布尔值或"mock"
process: true, // boolean布尔值
__filename: "mock", // boolean | "mock"布尔值或"mock"
__dirname: "mock", // boolean | "mock"布尔值或"mock"
Buffer: true, // boolean | "mock"布尔值或"mock"
setImmediate: true // boolean | "mock" | "empty"布尔值或"mock"或"empty"
},
recordsPath: path.resolve(__dirname, "build/records.json"),
recordsInputPath: path.resolve(__dirname, "build/records.json"),
recordsOutputPath: path.resolve(__dirname, "build/records.json"),
// TODO
}
更多详情可请访问Webpack官方网站https://webpack.js.org/ 。当然读者也可以直接使用Facebook官方提供的create-react-app进行搭建(https://github.com/facebook/create- react-app )。后面章节会继续介绍Webpack真实项目的搭建与实践开发。
1.1.7 ES 6语法
ES 6(ECMAScript 6.0)是一个历史名词,也是一个泛指,指代ECMAScript 5.1版本之后JavaScript的下一代标准。其中包含ES 2015、ES 2016和ES 2017等,而这些年份表示在当年发布的正式版本的语言标准。
最早的ECMAScript 1.0于1997年发布,ECMAScript 2.0于1998年发布,ECMAScript 3.0于1999年发布。有意思的是,2000年ECMAScript 4.0的草案由于太过于激进没有被发布。到了2007年,ECMAScript 4.0草案发布。当时以Microsoft和Google为首的互联网“巨头”反对ES的大幅升级,希望能小幅改动;而Brendan Eich(JavaScript创造者)为首的Mozilla公司坚持当时的草案。由于分歧太大,2018年而中止了对ECMAScript 4.0的开发,将其中激进的部分放到以后的版本,将其中改动小的部分发布为ECMAScript 3.1,之后又将其改名为ECMAScript 5,并于2009年12月发布。在2015年6月,ECMAScript 6正式通过。但很多人不知道,时至今日,JavaScript初学者学习的其实就是ES 3.0版本。目前为止,各大浏览器厂商对ES 6语法特性的支持已经超过90%。
以上是对ECMAScript语言规范的简单历史介绍。
由于本书使用的示例代码会涉及ES 6相关语法,因此下面对项目中经常使用到的几点特性进行简单介绍。
1.变量声明let和const
ES 6之前,通常用var关键字来声明变量。无论在何处声明,都会被视为在所在函数作用域最顶部(变量提升)。那么为什么需要用let和const关键词来创建变量或常量呢?理由是:
- 可以解决ES 5使用var初始化变量时会出现的变量提升问题;
- 可以解决使用闭包时出错的问题;
- ES 5只有全局作用域和函数作用域,却没有块级作用域;
- 可以解决使用计数的for循环变量时会导致泄露为全局变量的问题。
let命令表示被声明的变量值在作用域内生效。比如:
{
let a = 1;
var b = 2;
}
a // 报错ReferenceError
b // 2
从上述代码可以看出,let声明的代码只能在其所在的代码块内有效,出了代码块,就会出错。
另外,对于let来说,不存在变量提升。在一般代码逻辑中,变量应该是定义后才可以使用,但var的变量提升却可以先使用再定义,多少有些“奇怪”,而let就更改了这种语法行为。要使用一个变量必须先声明,不然就报错,显然这样更合理。
var和let的对比示例:
Console.log(a); // undefined
var a = 1;
Console.log(a) // 报错 ReferenceError
let a = 1;
此外,let不允许重复声明,比如:
// 报错
function func(){
let a = 1;
var a = 2;
}
// 报错
function func(){
let a = 1;
let a = 2;
}
在代码块内,使用let声明变量之前,该变量都是不可用的(不可访问、不可赋值等)。在语法上,这被称为“暂时性死区”(Temporal Dead Zone,TDZ)。
if (true) {
// TDZ开始,不可访问,不可赋值
temp = "hello"; // ReferenceError
console.log(temp); // ReferenceError
let temp; // TDZ结束
console.log(temp); // 输出undefined,可访问
temp = 1; // 可赋值
console.log(temp); // 输出1,访问
}
在ES 5中,变量提升可能还会导致内层变量覆盖外层变量,比如:
var i = 1;
function func() {
console.log(i);
if (true) {
var i = 1;
}
}
func(); // undefined
let还引入了块级作用域的概念,传统ES 5中不存在块级作用域。假如没有块级作用域,可能会碰到这种问题:
var arr = [1, 2, 3, 4]
for (var i = 0; i < arr.length; i++){
console.log(i);
}
console.log(i); // 4
上述代码希望达到的结果是,在for循环之后变量i被垃圾回收机制回收。但用来计数的循环变量在循环结束后并没有被回收,内存泄露为了全局变量。这种场景非常适合使用块级作用域let:
var arr = [1, 2, 3, 4]
for (let i = 0; i < arr.length; i++){
console.log(i);
}
console.log(i); // Uncaught ReferenceError: i is not defined
从上面的示例代码可以看出,当循环结束后,就可以将不需要的用于计数的变量回收,让它消失。虽然一个简单的变量泄漏并不会造成很大危害,但这种写法是错误的。
块级作用域的出现无疑带来了很多好处,它允许作用域的任意嵌套,例如:
{{{{
{let i = 1;}
console.log(i); // 报错
}}}}
内层作用域可以使用跟外层同名的变量名,比如:
{{{{
let i =1;
console.log(i); // 1
{
let i = 2;
console.log(i); // 2
}
}}}}
块级作用域还使立即执行函数表达式(IIFE)不再成为必要项,比如:
// 立即执行函数
(function () {
var a = ...;
...
}());
// 块级作用域写法
{
let a = ...;
...
}
再来看看const。const用于声明只读的常量,一旦声明就不能改变。和let一样,const只在块级作用域内有效,不存在变量提升,存在暂时性死区和不可重复声明。
2.解构赋值
按照一定模式从数组或对象中提取值,对变量进行赋值,叫做解构赋值(Destructuring)。
用于对象的解构赋值示例:
const cat = {
name: 'Tom',
sex: 'male',
age: 3
};
let { name, sex, age } = cat;
console.log(name, sex, age); // Tom male 3
上述代码将cat中的属性解构出来并赋值给name、sex和age。同样的示例,传统写法如下:
const cat = {
name: 'Tom',
sex: 'male',
age: 3
};
let name = cat.name;
let sex = cat.sex;
let age = cat.age;
对象解构也可以指定默认值:
var {a =1} = {};
a // 1
var {a, b = 2} = {a: 1}
a // 1
b // 2
当解构不成功时,变量的值为undefined:
let {a} = {b: 1};
a // undefined
ES 6的解构赋值给开发者带来了很大的便捷,这就是解构赋值的魅力。同样,解构赋值也能在数组中使用。
数组的解构赋值示例:
let [a, b , c] = [1, 2, 3];
a // 1
b // 2
c // 3
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [e, f, …g] = ["hello"];
e // "hello"
f // undefined
g // [ ]
以上代码表明可以从数组中提取值,按照对应位置赋值给对应变量。如果解构失败就会赋值为undfined 。如果等号右边是不可遍历的结构,也会报错。
// 报错
let [a] = 1;
let [a] = false;
let [a] = {};
let [a] = NaN;
let [a] = undefined;
以上都会报错。
在解构赋值中也允许有默认值,例如:
let {a = [1, 2, 3]} = { };
a // [1, 2, 3]
let [x, y = 'hi'] = ["a"];
x // x='a', y='b'
3.拓展运算符(spread)…
拓展运算符(spread)是3个点(…)。可以将它比作rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。下面来看看它有哪些作用。
(1)合并数组。
在ES 5中要合并两个数组,写法是这样的:
var a = [1, 2];
var b = [3, 4];
a.concat(b); // [1, 2, 3, 4]
但在ES 6中拓展运算符提供了合并数组的新写法:
let a = [1, 2];
let b = [3, 4];
[...a, …b]; // [1, 2, 3, 4]
如果想让一个数组添加到另一个数组后面,在ES 5中是这样写的:
var x = ["a", "b"];
var y = ["c", "d"];
Array.prototype.push.apply(arr1, arr2);
arr1 // ["a", "b", "c", "d"]
上述代码中由于push()方法不能为数组,所以通过apply()方法变相使用。但现在有了ES 6的拓展运算符后就可以直接使用push()方法了:
let x = ["a", "b"];
let y = ["c", "d"];
x.push(…y); // ["a", "b", "c", "d"]
(2)数组复制。
拓展运算符还可以用于数组复制,但要注意的是,复制的是指向底层数据结构的指针,并非复制一个全新的数组。
数组复制示例:
const x = ['a', 'b'];
const x1 = x;
x1[0]; // 'a'
x1[0] = 'c';
x // ['c', 'b']
(3)与解构赋值结合。
拓展运算符可以和解构赋值相结合用于生成新数组:
const [arr1, …arr2] = [1, 2, 3, 4];
arr1 // 1
arr2 // [2, 3, 4]
注意,使用拓展运算符给数组赋值时,必须放在参数最后的位置,不然会报错。例如:
const […arr1, arr2] = [1, 2, 3, 4]; // 报错
const [1, …arr1, arr2] = [1, 2, 3, 4]; // 报错
(4)函数调用(替代apply()方法)。
在ES 5中要合并两个数组,写法是这样的:
function add(a, b) {
return a + b;
}
const num = [1, 10];
add.apply(null, num); // 11
在ES 6中可以这样写:
function add(a, b) {
return a + b;
}
const num = [1, 10];
add(…num); // 11
上述代码使用拓展运算符将一个数组变为参数序列。当然,拓展运算符也可以和普通函数参数相结合使用,非常灵活。比如:
function add(a, b, c, d) {
return a + b + c + d;
}
const num = [1, 10];
add(2, …num, -2); // 11
拓展运算符中的表达式如下:
[…(true ? [1, 2] : [3]), 'a']; // [1, 2, 'a']
4.箭头函数
ES 6对于函数的拓展中增加了箭头函数=>,用于对函数的定义。
箭头函数语法很简单,先定义自变量,然后是箭头和函数主体。箭头函数相当于匿名函数并简化了函数定义。
不引入参数的箭头函数示例:
var sum = () => 1+2; // 圆括号代表参数部分
// 等同于
var sum = function() {
return 1 + 2;
}
引入参数的箭头函数示例:
// 单个参数
var sum = value => value; // 可以不给参数value加小括号
// 等同于
var sum = function(value) {
return value;
};
// 多个参数
var sum = (a, b) => a + b;
// 等同于
var sum = function(a, b) {
return a + b;
};
花括号{}内的函数主体部分写法基本等同于传统函数写法。
var getInfo = id =>({
id: id,
title: 'Awesome React'
});
// 等同于
var getInfo = function(id) {
return {
id: id,
title: 'Awesome React'
}
}
箭头函数与传统的JavaScript函数主要区别如下:
- 箭头函数内置this不可改变;
- 箭头函数不能使用new关键字来实例化对象;
- 箭头函数没有arguments对象,无法通过arguments对象访问传入的参数。
这些差异的存在是有理可循的。首先,对this的绑定是JavaScript错误的常见来源之一,容易丢失函数内置数值,或得出意外结果;其次,将箭头函数限制为使用固定this引用,有利于JavaScript引擎优化处理。
箭头函数看似匿名函数的简写,但与匿名函数有明显区别,箭头函数内部的this是词法作用域,由上下文确定。如果使用了箭头函数,就不能对this进行修改,所以用call()或apply()调用箭头函数时都无法对this进行绑定,传入的第1个参数会被忽略。
更多详情,可参考阮一峰的《ECMAScript 6入门》一书。
1.2 React简介
A JavaScript library for building user interfaces,这是React官网给React的一句话概括。
简单来说,React就是一个使用JavaScript构建可组合的用户界面引擎,主要作用在于构建UI。虽然有人说React属于MVC的V(视图)层,但在React官方博客中阐明React不是一个MVC框架,而是一个用于构建组件化UI的库,是一个前端界面开发工具,他们并不认可MVC这种设计模式。
React源于Facebook内部PHP框架XHP的一个分支,在每次有请求进入时会渲染整个页面。而React的出现就是为了把这种重新渲染整个页面的PHP式工作流,带入客户端应用,在使用React构建用户界面时,只需定义一次,就能将其复用在其他多个地方。当状态改变时,无须做出任何操作,它会自动、高效地更新界面。从此开发人员只需要关心维护应用内的状态,而不需要再关注DOM节点。这样开发人员就能从复杂的DOM操作中解脱出来,让工作重心回归状态本身。
由于React是一个专注于UI组件的类库,简单的理念和少量的API能和其他各种技术相融合,加之是由互联网“巨头”Facebook维护,使React的生态圈在全球社区得以不断地良性发展。同时,基于React还诞生了React Native,这无疑给当今移动互联网的蓬勃发展投下了的一枚重型“炸弹”。
得益于虚拟DOM的实现,React可以实现跨平台开发:
- Web应用;
- 原生iOS和Android应用;
- Canvas应用和原生桌面应用;
- TV应用。
可以说是“Learn once,Write Anywhere”。
1.3 React的特征
本节将介绍有关React的三大突出特点:组件化、虚拟DOM和单向数据流,这有助于读者更好地认识和理解React的设计思想。
1.3.1 组件化
React书写的一切用户界面都是基于组件的。这么做的好处是什么呢?
最显而易见的就是组件具备良好的封装性,可以重复使用。想象一下,在一个应用中,假如每个页面顶部都有一个类似功能的搜索框,需要写多次重复的代码,如果把它单独抽象封装成一个单独的组件,在每个使用到的地方去引用,这样可以减少大量重复、多余的代码,并且方便迭代维护。
在React中,对于组件的理解可以比喻成古代的“封邦建国”。天子把自己直接管辖(父组件)以外的土地分封给诸侯,让他们各自管辖属于自己的领地(子组件)。只要天子(父组件)有什么需要,就吩咐(调用)诸侯(子组件)去做就行了。有什么旨意,就派信使传达(props 属性,2.12节将详细讲解)。这样一来,天子一个人要管辖这么多的领土也不会觉得累了,同时又让自己的国家繁荣富强,实现自治,但又不脱离自己的掌控。
简单的组件示例:
import React, { Component } from 'react';
import { render } from 'react-dom';
export default Class MyComponent extends React.Component {
render() {
return (
<div>
Hello, I am {this.props.name}.
</div>
)
}
}
自定义组件后,在其他需要使用这个组件的地方就可以像使用HTML标签一样去引用了,例如:
import React, { Component } from 'react';
import { render } from 'react-dom';
import MyComponent from './myComponent'
export default class extends React.Component {
render() {
return (
<MyComponent name="Jack" /> // name 是自定义组件的属性
)
}
}
运行程序,输出结果如图1.5所示。
1.3.2 虚拟DOM
先来了解一下什么是DOM,什么又是虚拟DOM。
Document Object Model(DOM,文档对象模型)是W3C(World Wide Web Consortium,万维网联盟)的标准,定义了访问HTML和XML文档的标准方法:W3C文档对象模型(DOM)是中立于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。简单来说,就是用于连接document和JavaScript的桥梁。
每个页面都有一个DOM树,用于对页面的表达。真实场景中用户的很多操作会导致浏览器的重排(引起DOM树重新计算的行为)。一般的写法都是手动操作DOM,获取页面真实的DOM节点,然后修改数据。在复杂场景或需要频繁操作DOM的应用中,这样的写法非常消耗性能。当然也有许多方式可以避免页面重排,比如把某部分节点position设置为absolute/fixed定位,让其脱离文档流;或者在内存中处理DOM节点,完成之后推进文档等。这些方法维护成本昂贵,代码难以维护。
React为了摆脱操作真实DOM的噩梦,开创性地把DOM树转换为JavaScript对象树,这就是虚拟DOM(Virtual DOM)。
简单理解,虚拟DOM就是利用JS去构建真实DOM树,用于在浏览器中展示。每当有数据更新时,将重新计算整个虚拟DOM树,并和旧DOM树进行一次对比。对发生变化的部分进行最小程度的更新,从而避免了大范围的页面重排导致的性能问题。虚拟DOM树是内存中的数据,所以本身操作性能高很多。
可以说,React赢就赢在了利用虚拟DOM超高性能地渲染页面,另辟蹊径地处理了这个对于开发者来说真实存在的痛点。除此之外,由于操作的对象是虚拟DOM,与真实浏览器无关,与是否是浏览器环境都没有关系。只需要存在能将虚拟DOM转换为真实DOM的转换器,就能将其转为真实DOM在界面中展现,从而就达到了利用React实现跨平台的目的,比如React Native的实现。
1.3.3 单向数据流
在React中,数据流是单向的。
数据的流向是从父组件流向子组件,至上而下,如图1.6所示。这样能让组件之间的关系变得简单且可预测。
props和state是React组件中两个非常重要的概念。props是外来的数据,state是组件内部的数据。一个组件内,可以接受父组件传递给它的数据,如果props改变了,React会递归地向下遍历整棵组件树,在使用到这个属性的组件中重新渲染;同时组件本身还有属于自己的内部数据,只能在组件内部修改。可以将其与面向对象编程进行类比:this.props就是传递给构造函数的参数,this.state就是私有属性。
单向数据流的好处就是,所有状态变化都是可以被记录和跟踪的,源头容易追溯,没有“暗箱操作”,只有唯一入口和出口,使程序更直观易理解,利于维护。
1.4 JSX语法
前面一起了解了React的3个主要特征。本节将带领读者来学习React书写的“最佳姿势”——JSX语法。它是由React官方推出的一种基于JavaScript的拓展语法。虽然不是使用React编写代码的必要条件,不过相信当读者了解到JSX的好处之后,便不会使用原生JavaScript开发React了。
1.4.1 JSX简介
JSX(JavaScript XML),是JavaScript的一种拓展语法。可以在JavaScript代码中编写更像XML写法的代码。React官方推荐使用JSX替代常规JavaScript写法,从而让代码更直观,达到更高的可读性。但需要注意的一点是,它不能直接在浏览器中执行,需要经过转换器将JSX语法转为JS之后才可以。从本质上来讲,JSX也只是React.createElement(component, props, ...children)函数的语法糖,所以在JSX当中,依旧可以正常使用JavaScript表达式。
当然,使用JSX并不是React开发应用的必要条件,JSX是独立于React的,也可以不使用。
下面通过两个示例来对比一下使用原生JS和使用JSX的区别,它们都是用来在页面中展示一个HelloReact的文案。
Before:使用原生JS实现Hello React。
class HelloMessage extends React.Component {
render() {
return React.createElement(
"div",
null,
"Hello React"
);
}
}
ReactDOM.render(React.createElement(HelloMessage, null), mountNode);
After:使用JSX实现HelloReact。
class HelloMessage extends React.Component {
render() {
return (
<div>Hello React</div>
);
}
}
ReactDOM.render(
<HelloMessage />,
mountNode
);
再来看一个略微“复杂”的示例,三层div嵌套,还是输出HelloReact。
Before:使用原生JSX实现。
class HelloMessage extends React.Component {
render() {
return React.createElement(
"div",
null,
React.createElement(
"div",
null,
React.createElement(
"div",
null,
"Hello React"
)
)
);
}
}
ReactDOM.render(React.createElement(HelloMessage, null), mountNode);
After:使用JSX实现。
class HelloMessage extends React.Component {
render() {
return (
<div>
<div>
<div>Hello React</div>
</div>
</div>
);
}
}
ReactDOM.render(
<HelloMessage />,
mountNode
);
从上面两个案例的对比中可以明显看出,JSX语法更接近开发者平时的书写方式。
1.4.2 JSX的转译
JSX代码是不能被浏览器直接运行的,需要将其转译为JavaScript之后才能运行。转译之后的代码功能相同。由于前端发展速度较快,在很多老项目中依旧可以见到这类写法。这也是本节对JSX编译工具发展作一个简单介绍的初衷,初次学习React的读者暂时可以当成小故事去阅读。下面来看一看对JSX转译的一段小“历史”。
早期Facebook提供了一个简单的工具JSXTransformer,这是一个浏览器端具有转译功能的脚本,将这个JSXTransformer.js文件直接引入HTML文档就能使用。例如:
<script src="./jsxtransformer.js"></script>
// type为text/jsx
<script type="text/jsx">
// JSX代码
</script>
这样写就需要在浏览器端进行转译工作,所以对性能有损耗,影响效率。当然Facebook也考虑到了这点,于是对应的也就有了服务端去渲染的工具,那就是react-tools。这里暂不介绍,读者先大致了解下即可。
随后在React v 0.14之后官方发布:
Deprecation of react-tools
The react-tools package and JSXTransformer.js browser file have been deprecated. You can continue using version 0.13.3 of both, but we no longer support them and recommend migrating to Babel, which has built-in support for React and JSX.
也就是说,在React v0.14版本后将JSXTransformer.js弃用了。接下去可以使用Babel,如图1.7所示,这也是当下主流使用的转译工具。
Babel原名是6to5,是一个开源的转译工具。它的作用就是把当前项目中使用的ES 6、ES 7和JSX等语法,转译为当下可以执行的JavaScript版本,让浏览器能够识别。简单来说,它是一个转码工具集,包含各种各样的插件。
在Babel 6.0版本以前,使用了browser.js,也就是最早替代JSXTransform.js的转化器脚本。在HTML中引用如下:
<script src="./babel-core/browser.js"></script>
// type为text/babel
<script type="text/babel">
// JSX代码
</script>
Babel还提供了在线编译的功能(http://babeljs.io/repl/ ),如图1.8所示,可以在上面进行测试或学习。
以上就是JSX转译的大致历程。
本书之后的项目,将使用Webpack等构建工具配置Babel,以实现对JSX语法的支持。
1.4.3 JSX的用法
在React中,可以使用花括号{}把JavaScript表达式放入其中运行,放入{}中的代码会被当作JavaScript代码进行处理。
1.JSX嵌套
定义标签时,最外层只允许有一个标签,例如:
const MessageList = () =>(
<div>
<div>Hello React!</div>
<ul>
<li>List1</li>
<li>List2</li>
<li>List2</li>
</ul>
</div>
)
假如写成以下这样:
const MessageList = () =>(
<div>
<div>Hello React!</div>
<ul>
<li>List1</li>
<li>List2</li>
<li>List2</li>
</ul>
</div>
<div>
...
</div>
)
进行程序后会报错,无法通过编译,控制台报错如图1.9所示。
2.属性表达式
在传统写法中,属性这样写:
<div id="test-id" class="test-class">…</div>
JSX中也实现了同样的写法,比如:
const element = <div id="test-id" className="test-class"></div>
// 注意class写为className
同时还支持JavaScript动态设置属性值,比如:
// 注意class写为className
const element = <div id={this.state.testId} className={this.state.testClass}> </div>;
const element = <img src={user.avatarUrl}></img>;
对于复杂情景,还可以直接在属性内运行一个JavaScript函数,把函数返回值赋值给属性,比如:
const element = <img src={this.getImgUrl()}></img>;
这样,每当状态改变时,返回值会被渲染到真实元素的属性中。
3.注释
JSX语法中的注释沿用JavaScript的注释方法,唯一需要注意的是,在一个组件的子元素位置使用注释需要用{}括起来。例如:
class App extends React.Component {
render() {
return (
<div>
{/- 注释内容 */}
<p>Hi, man</p>
</div>
)
}
}
4.Boolean属性
Boolean的属性值,JSX语法中默认为true。要识别为false就需要使用{},这类标签常出现在表单元素中,如disable、checked和readOnly等。例如:
<checkbox checked />等价于<checkbox checked={true} />,但要让checked为false,必须这么写:<checkbox checked={false} />。
5.条件判断
在JavaScript中可以使用判断条件的真假来判断对DOM节点进行动态显示或选择性显示。通常,在HTML中处理这样的问题比较麻烦,但对于JavaScript而言则轻而易举,这给开发者带来了极大的方便。本书主要介绍3种条件判断,分别是三目运算符、逻辑与(&&)运算符和函数表达式。
三目运算符示例:
class App extends React.Component {
constructor(){
super();
this.state={
visible: false
}
}
render() {
return (
<div>
{
this.state.visible ? <span>visible为真</span> : <span>visible为真</span>
}
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
代码运行结果如图1.10所示。
逻辑与(&&)运算符示例:
class App extends React.Component {
constructor(){
super();
this.state={
visible: false
}
}
render() {
return (
<div>
{
!this.state.visible && <span>visible为真</span>
}
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
代码运行结果,如图1.11所示。
函数表达式示例:
style样式:
.red{
color: red;
}
.blue{
color: blue;
}
JSX:用JSX语法定义一个名为App的组件,用于在页面中渲染一个div节点。
class App extends React.Component {
constructor(){
super();
this.state={
isRed: true
}
}
getClassName(){
return this.state.isRed?"red":"blue"
}
render() {
return (
<div className={this.getClassName()}>
Hello React!
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
运行代码,显示效果如图1.12所示。
6.非DOM属性
在React中还有几个特殊的属性是HTML所没有的:
- key(键);
- refs(引用);
- dangerouslySetInnerHTML。
下面简单介绍它们的作用。
(1)key(键):是一个可选的唯一标识符。比如有一组列表,当对它们进行增加或删除操作时,会导致列表重新渲染。当这组列表属性中拥有一个独一无二的key属性值之后就可以高性能地渲染页面。后面6.1节的diff算法中会详细分析该属性。示例用法如下:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
(2)refs(引用ref):任何组件都可以附加这个属性,该属性可以是字符串或回调函数。当refs是一个回调函数时,函数接收底层DOM元素或实例作为参数。这样就可以直接访问这个DOM或组件的节点了。但此时获取到的不是真实的DOM,而是React用来创建真实DOM的描述对象。写法如下:
<input ref="myInput" />
然后就可以通过this.refs.myInput去访问DOM或组件的节点了。
refs适用的场景有处理焦点、文本选择、媒体控制、触发强制动画和集成第三方DOM库等。需要注意的是,官方并不推荐使用这个属性,除非“迫不得已”。
?注意:无状态组件不支持ref。在React调用无状态组件之前没有实例化的过程,因此就没有所谓的ref。
(3)dangerouslySetInnerHTML:这算是React中的一个冷知识,它接收一个对象,可以通过字符串形式的HTML来正常显示。
<div dangerouslySetInnerHTML={{__html: '<span>First · Second </span>'}} />
上面这段代码将在页面中显示First · Second。
通过这种方式还可以避免cross-site scripting(XSS)攻击。这里不展开对XSS的介绍,读者可根据兴趣自行了解。
7.样式属性
样式(style)属性接收一个JavaScript对象,用于改变DOM的样式。
JSX中的样式示例:
let styles = {
fontSize: "14px",
color: "#red"
}
function AppComponent() {
return <div style={styles}>Hello World!</div>
}
1.5 Hello World实战训练
遵循传统,在学习React前先带领读者构建一个基于Webpack的Hello World应用。
1.5.1 不涉及项目构建的Hello World
本节实现一个不涉及项目构建的Hello World。
React的第一个Hello World网页示例(源码地址是https://jsfiddle.net/allan91/2h1sf0ky/8/ ):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello World</title>
<script src="https://cdn.bootcss.com/react/15.4.2/react.min.js"></script>
<script src="https://cdn.bootcss.com/react/15.4.2/react-dom.min.js"></script>
<script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.
min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello World</h1>, //JSX格式
document.getElementById("root")
);
</script>
</body>
</html>
上面的代码很简单,直接引用CDN(Content-Delivery-Network)上的react.min.js、react-dom.min.js和babel.min.js这3个脚本即可直接使用。唯一需要注意的是,script的type属性需要写为text/babel。在浏览器中打开这个HTML文件即可展示Hello World文案。
说明:CDN(Content Delivery Network)是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
react.main.js是React的核心代码包;react-dom.min.js是与DOM相关的包,主要用于把虚拟DOM渲染的文档变为真实DOM,当然还有其他一些方法;babel.min.js是用来编译还不被浏览器支持的代码的编译工具,其中min表示这是被压缩过的JS库。
也可以将JavaScript代码写在外面,比如在根目录下新建main.js:
ReactDOM.render(
<h1>Hello World</h1>, //JSX格式
document.getElementById("root")
);
然后在HTML文件内引入:
<script type="text/babel" src="./main.js"></script>
1.5.2 基于Webpack的Hello World
真实项目中一般不会像1.5.1节介绍的这样来搭建项目,有了前面小节中的基础知识,接下来开始动手搭建一个基于Webpack的Hello World应用。这次搭建分为两部分:一部分是前期必要配置,另一部分是开发React代码。
基于Webpack的React Hello World项目示例:
1.前期必要配置
(1)首先要确保读者的开发设备上已经安装过Node.js,新建一个项目:
mkdir react-hello-world
cd react-hello-world
npm init -y
(2)项目中使用的是Webpack 4.x,在项目根目录下执行如下命令:
npm i webpack webpack-cli -D
注意:上面命令代码中npm install module_name -D 即 npm intsll module_name —save-dev。表示写入package.json的devDependencies。devDependencies里面的插件用于开发环境,不用于生产环境。npm install module_name —S即npm intsll module_name —save。dependencies是需要发布到生产环境的。
(3)安装完Webpack,需要有一个配置文件让Webpack知道要做什么事,这个文件取名为webpack.config.js。
touch webpack.config.js
然后配置内容如下:
var webpack = require('webpack');
var path = require('path');
var APP_DIR = path.resolve(__dirname, 'src');
var BUILD_DIR = path.resolve(__dirname, 'build');
var config = {
entry: APP_DIR + '/index.jsx', // 入口
output: {
path: BUILD_DIR, // 出口路径
filename: 'bundle.js' // 出口文件名
}
};
module.exports = config;
这是Webpack使用中最简单的配置,只包含了打包的入口和出口。APP_DIR表示当前项目的入口路径,BUILD_DIR表示当前项目打包后的输出路径。
(4)上面配置的入口需要新建一个应用的入口文件./src/index.jsx,我们让其打印Hello World:
console.log('Hello World');
(5)用终端在根目录下执行:
./node_modules/.bin/webpack -d
上面的命令在开发环境运行之后,会在根目录下生成一个新的build文件夹,里面包含了Webpack打包的bundle.js文件。
(6)接下来创建index.html,用于在浏览器中执行bundle.js :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello World</title>
</head>
<body>
<div id="app"></div>
<!--bundle.js是Webpack打包后生成的文件-->
<script src="build/bundle.js" type="text/javascript"></script>
</body>
</html>
在浏览器中打开index.html文件,在控制台就能看到./src/index.jsx打印的内容:Hello World 。
(7)为了提高效率和使用最新的ES语法,通常使用JSX和ES 6进行开发。但JSX和ES 6语法在浏览器中还没有被完全支持,所以需要在Webpack中配置相应的loader模块来编译它们。只有这样,打包出来的bundle.js文件才能被浏览器识别和运行。
接下来安装Babel:
npm i -D babel-core babel-loader@7 babel-preset-env babel-preset-react
注意:babel-core是调用Babel的API进行转码的包;babel-loader是执行转义的核心包;babel-preset-env是一个新的preset,可以根据配置的目标运行环境自动启用需要的Babel插件;babel-preset-react用于转译React的JSX语法。
(8)在webpack.config.js中配置loader:
var webpack = require("webpack");
var path = require("path");
var BUILD_DIR = path.resolve(__dirname, "build"); // 构建路径
var APP_DIR = path.resolve(__dirname, "src"); // 项目路径
var config = {
entry: APP_DIR + "/index.jsx", // 项目入口
output: {
path: BUILD_DIR, // 输出路由
filename: "bundle.js" // 输出文件命名
},
module: {
rules: [
{
test: /\.(js|jsx)$/, // 编译后缀为js和jsx格式文件
exclude: /node_modules/,
use: {
loader: "babel-loader" // 使用babel-loader这个loader库
}
}
]
}
};
module.exports = config;
(9)创建.babelrc文件:
touch .babelrc
配置相应内容来告诉babel-loader使用ES 6和JSX插件:
{
"presets" : ["env", "react"]
}
至此为止,已经完成开发该项目的基础配置工作。
2.使用React编码
下面正式开始使用React来编写前端代码。
(1)用NPM命令安装react和react-dom:
npm install react react-dom -S
(2)用下面代码替换./src/index.jsx中的console:
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
render () {
return <p> Hello React</p>;
}
}
render(<App/>, document.getElementById('app'));
(3)在根目录下执行:
./node_modules/.bin/webpack -d
在浏览器中打开index.html,将会在页面展示Hello World。当然真实开发中不能每一次修改前端代码就执行一次Webpack编译打包,可以执行如下命令来监听文件变化:
./node_modules/.bin/webpack -d —-watch
终端将会显示:
myfirstapp Jack$ ./node_modules/.bin/webpack -d --watch
webpack is watching the files…
Hash: 6dbf97954b511aa86515
Version: webpack 4.22.0
Time: 839ms
Built at: 2018-10-23 19:05:01
Asset Size Chunks Chunk Names
bundle.js 1.87 MiB main [emitted] main
Entrypoint main = bundle.js
[./src/index.jsx] 2.22 KiB {main} [built]
+ 11 hidden modules
这就是Webpack的监听模式,一旦项目中的文件有改动,就会自动执行Webpack编译命令。不过浏览器上展示的HTML文件不会主动刷新,需要手动刷新浏览器。如果想实现浏览器自动刷新,可以使用react-hot-loader(源码地址https://github.com/gaearon/react- hot-loader )。
(4)在真实的项目开发中,一般使用NPM执行./node_modules/.bin/webpack -d --watch命令来开发。这需要在package.json中进行如下配置:
{
...
"scripts": {
"dev": "webpack -d --watch",
"build": "webpack -p",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
}
(5)现在只需要在根目录下执行如下命令就能开发与构建:
npm run dev
npm run build
以上为真实项目中一个较为完整的项目结构,读者可以在此基础上根据项目需要自行拓展其他功能。本例源码地址为https://github.com/khno/react-hello-world ,分支为master。项目完整的结构如下:
.
├── build
│ └── bundle.js
├── index.html
├── package-lock.json
├── package.json
├── src
│ └── index.jsx
├── .gitignore
├── .babelrc
└── webpack.config.js
1.5.3 Hello World进阶
前面使用Webpack实现了最简单的项目构建,每次写完代码都需要手动刷新浏览器来查看实时效果。接下来继续完善上面的构建项目,实现如下功能:
- 项目启动,开启HTTP服务,自动打开浏览器;
- 实时监听代码变化,浏览器实时更新;
- 生产环境代码构建。
模块热替换(Hot Module Replacement,HMR)是Webpack中最令人兴奋的特性之一。当项目中的代码有修改并保存后,Webpack能实时将代码重新打包,并将新的包同步到浏览器,让浏览器立即自动刷新。
在开始配置前,读者可以先思考一个问题,如何实现浏览器实时更新呢?也许会想到让浏览器每隔一段时间向本地服务器发送请求,采用轮询(Polling)方式来实现;或者让服务器端来通知客户端,服务器端接收到客户端有资源需要更新,就主动通知客户端;或者直接使用WebSocket?
说明:WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC6455,并由RFC7936补充规范。
WebSocket API也被W3C定为标准。WebSocket使客户端和服务器端之间的数据交换变得更加简单,允许服务器端主动向客户端推送数据。以上信息参考来源于*。
其实HMR原理就是上面说的那样,早期HMR的原理是借助于EventSource,后来使用了WebSocket。可以在本地开发环境开启一个服务,将其作为本地项目的服务端,然后与本地浏览器之间进行通信即可。欲了解详情,请读者自行学习。
注意:WebSocket是基于TCP的全双工通信协议。
大致了解了HMR的原理之后,开始动手实践吧。
实时更新的Hello World示例:
(1)首先,采用webpack-dev-server作为HMR的工具。在之前的项目基础上安装:
npm install webpack-dev-server –D
(2)修改webpack.config.js文件:
+ devServer: {
+ port: 3000,
+ contentBase: "./dist"
+ },
devServer.port是服务启动后的端口号,devServer.contentBase是配置当前服务读取文件的目录。启动后可以通过localhost:3000端口访问当前项目。
(3)修改package.json文件:
"scripts": {
+ "start": "webpack-dev-server --open --mode development",
"test": "echo \"Error: no test specified\" && exit 1"
},
按照上面配置start命令后,执行npm start会直接找到webpack.config.js来启动项目。
(4)最后安装html-webpack-plugin和clean-webpack-plugin。
npm install html-webpack-plugin clean-webpack-plugin -D
html-webpack-plugin插件用于简化创建HTML文件,它会在body中用script标签来包含我们生成的所有bundles文件。配置如下:
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
// favicon: 'favicon.ico',
inject: true,
sourceMap: true,
chunksSortMode: "dependency"
})
]
clean-webpack-plugin插件在生产环境中编译文件时,会先删除build或dist目录文件,然后生成新的文件。
(5)随着文件的增、删,打包的dist文件内可能会产生一些不再需要的静态资源,我们并不希望将这些静态资源部署到服务器上占用空间,所以最好在每次打包前清理dist目录。在Webpack中配置如下:
plugins: [
new CleanWebpackPlugin(["dist"])
]
此时,Webpack代码清单如下:
var webpack = require("webpack");
var path = require("path");
const CleanWebpackPlugin = require("clean-webpack-plugin");
var BUILD_DIR = path.resolve(__dirname, "dist"); // 输出路径
var APP_DIR = path.resolve(__dirname, "src"); // 项目路径
const HtmlWebpackPlugin = require("html-webpack-plugin");
var config = {
entry: APP_DIR + "/index.jsx", // 项目入口
output: {
path: BUILD_DIR, // 输出路由
filename: "bundle.js" // 输出文件命名
},
module: {
rules: [
{
test: /\.(js|jsx)$/, // 编译后缀为js和jsx的格式文件
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
devServer: { // 在开发模式下,提供虚拟服务器用于项目开发和调试
port: 3000, // 端口号
contentBase: "./dist"
},
plugins: [ // 拓展Webpack功能
new HtmlWebpackPlugin({ // 生成HTML文件
template: "index.html",
// favicon: 'theme/img/favicon.ico',
inject: true,
sourceMap: true, // 是否生成sourceMap
chunksSortMode: "dependency"
}),
new CleanWebpackPlugin(["dist"]) // 清除文件
]
};
module.exports = config;
(6)在根目录终端执行npm run start:
AllandeMacBook-Pro:react-hello-world jack$ npm start
> myfirstapp@1.0.0 start /Users/allan/react-hello-world
> webpack-dev-server --open --mode development
// 删除dis文件
clean-webpack-plugin: /Users/alan/react-hello-world/dist has been removed.
?wds?: Project is running at http://localhost:3000/
?wds?: webpack output is served from /
?wds?: Content not from webpack is served from ./dist
?wdm?: wait until bundle finished: /
?wdm?: Hash: 4bfc0734100357258e1f
Version: webpack 4.23.1
Time: 1358ms
Built at: 2018-11-06 20:11:12
?? ? Asset ? ? ? Size? Chunks ? ? ? ? ? ? Chunk Names
?bundle.js ? 1.12 MiB? ? main? [emitted]? main
index.html? 342 bytes? ? ? ? ? [emitted] ?
Entrypoint main = bundle.js
[0] multi ./node_modules/_webpack-dev-server@3.1.10@webpack-dev-server/ client?http://localhost:3000 ./src/index.jsx 40 bytes {main} [built]
[./node_modules/_ansi-html@0.0.7@ansi-html/index.js] 4.16 KiB {main} [built]
[./node_modules/_ansi-regex@2.1.1@ansi-regex/index.js] 135 bytes {main} [built]
[./node_modules/_events@1.1.1@events/events.js] 8.13 KiB {main} [built]
[./node_modules/_loglevel@1.6.1@loglevel/lib/loglevel.js] 7.68 KiB {main} [built]
[./node_modules/_react-dom@16.6.0@react-dom/index.js] 1.33 KiB {main} [built]
[./node_modules/_react@16.6.0@react/index.js] 190 bytes {main} [built]
[./node_modules/_strip-ansi@3.0.1@strip-ansi/index.js] 161 bytes {main} [built]
[./node_modules/_url@0.11.0@url/url.js] 22.8 KiB {main} [built]
[./node_modules/_webpack-dev-server@3.1.10@webpack-dev-server/client/index.js?http://localhost:3000] ./node_modules/_webpack-dev-server@3.1.10@webpack-dev-server/client?http://localhost:3000 7.78 KiB {main} [built]
[./node_modules/_webpack-dev-server@3.1.10@webpack-dev-server/client/overlay.js] 3.58 KiB {main} [built]
[./node_modules/_webpack-dev-server@3.1.10@webpack-dev-server/client/socket.js] 1.05 KiB {main} [built]
[./node_modules/_webpack@4.23.1@webpack/hot/emitter.js](webpack)/hot/emitter.js 75 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] ./node_modules/webpack/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./src/index.jsx] 2.22 KiB {main} [built]
? ? + 22 hidden modules
Child html-webpack-plugin for "index.html":
?? ? 1 asset
? ? Entrypoint undefined = index.html
? ? [./node_modules/_html-webpack-plugin@3.2.0@html-webpack-plugin/lib/loader.js!./index.html] 506 bytes {0} [built]
? ? [./node_modules/_lodash@4.17.11@lodash/lodash.js] 527 KiB {0} [built]
? ? [./node_modules/_webpack@4.23.1@webpack/buildin/global.js](webpack)/ buildin/global.js 489 bytes {0} [built]
? ? [./node_modules/_webpack@4.23.1@webpack/buildin/module.js](webpack)/ buildin/module.js 497 bytes {0} [built]
?wdm?: Compiled successfully.
此时,一旦项目内的文件有改动,就会自动执行编译,并通知浏览器自动刷新。
(7)至此开发配置已经完成,接下来配置生产命令,只需执行webpack-p即可。webpack -p表示压缩打包代码。在package.json内配置:
"scripts": {
"start": "webpack-dev-server --open --mode development",
+ "build": "webpack -p",
"test": "echo \"Error: no test specified\" && exit 1"
},
(8)打包的时候只需执行npm run build即可,打包成功会出现:
AllandeMacBook-Pro:react-hello-world allan$ npm run build
> myfirstapp@1.0.0 build /Users/allan/react-hello-world
> webpack -p
clean-webpack-plugin: /Users/allan/react-hello-world/dist has been removed.
Hash: 97bb34a13d3fc8cbfccc
Version: webpack 4.23.1
Time: 2647ms
Built at: 2018-11-06 20:22:45
?? ? Asset ? ? ? Size? Chunks ? ? ? ? ? ? Chunk Names
?bundle.js? ? 110 KiB ? ? ? 0? [emitted]? main
index.html? 342 bytes? ? ? ? ? [emitted] ?
Entrypoint main = bundle.js
[2] ./src/index.jsx 2.22 KiB {0} [built]
? ? + 7 hidden modules
Child html-webpack-plugin for "index.html":
?? ? 1 asset
? ? Entrypoint undefined = index.html
? ? [0] ./node_modules/_html-webpack-plugin@3.2.0@html-webpack-plugin/lib/loader.js!./index.html 506 bytes {0} [built]
? ? [2](webpack)/buildin/global.js 489 bytes {0} [built]
? ? [3](webpack)/buildin/module.js 497 bytes {0} [built]
? ? ? ? + 1 hidden module
上面的打包结果展示了此次打包的哈希值、耗时,以及打包后每个包的大小等信息。此时项目的结构为:
.
├── README.md
├── dist
│ ├── bundle.js
│ └── index.html
├── index.html
├── package-lock.json
├── package.json
├── src
│ └── index.jsx
└── webpack.config.js
以上项目的完整代码可以在https://github.com/khno/react-hello-world 中下载,分支为dev。本书中后面章节的代码案例默认将以此为基础运行,读者可以自行下载。