浏览 React.js 生态系统
已发表: 2022-03-11JavaScript Land 的创新速度如此之快,以至于有些人甚至认为它适得其反。 图书馆可以在几个月的时间内从早期采用的玩具变成最先进的玩具,再到过时。 能够识别一种至少能再流行一年的工具本身就是一门艺术。
两年前发布 React.js 时,我刚刚学习 Angular,我很快就将 React 视为一些晦涩难懂的另一个模板库。 在那两年里,Angular 真正在 JavaScript 开发者中站稳了脚跟,几乎成了现代 JS 开发的代名词。 我什至开始在非常保守的企业环境中看到 Angular,我认为它的光明未来是理所当然的。
但是突然,奇怪的事情发生了。 Angular 似乎成为了奥斯本效应的受害者,或者说是“预先宣布死亡”。 该团队宣布,首先,Angular 2 将完全不同,没有从 Angular 1 明确的迁移路径,其次,Angular 2 将在一年左右的时间内无法使用。 这告诉想要开始一个新的网络项目的人是什么? 你想在一个将被新版本发布过时的框架中编写你的新项目吗?
开发人员之间的这种焦虑在 React 的手中发挥了作用,它正在寻求在社区中建立自己的地位。 但 React 总是将自己标榜为“MVC”中的“V”。 这在习惯于使用完整框架的 Web 开发人员中造成了一定程度的挫败感。 如何填写缺失的部分? 我应该自己写吗? 我应该只使用现有的库吗? 如果有,是哪一个?
果然,Facebook(React.js 的创造者)又多了一张王牌:Flux 工作流,它承诺填补缺失的“M”和“C”函数。 为了让事情变得更有趣,Facebook 表示 Flux 是一种“模式”,而不是框架,他们对 Flux 的实现只是该模式的一个示例。 正如他们所说,他们的实现非常简单,并且涉及编写大量冗长、重复的样板来让事情顺利进行。
开源社区出手相救,一年后,我们有了几十个 Flux 库,甚至还有一些旨在比较它们的元项目。 这是一件好事; Facebook 没有发布一些现成的企业框架,而是设法激发了社区的兴趣,并鼓励人们提出自己的解决方案。
这种方法有一个有趣的副作用:当您必须组合许多不同的库来获得完整的框架时,您实际上是在摆脱供应商锁定,并且在您自己的框架构建过程中出现的创新可以很容易地重用别处。
这就是为什么围绕 React 的新东西如此有趣的原因; 其中大部分可以很容易地在其他 JavaScript 环境中重用。 即使你不打算使用 React,看看它的生态系统也是鼓舞人心的。 您可能希望使用功能强大但相对容易配置的模块捆绑器 Webpack 来简化构建系统,或者现在开始使用 Babel 编译器编写 ECMAScript 6 甚至 ECMAScript 7。
在本文中,我将介绍一些可用的有趣功能和库。 那么,让我们探索一下 React 生态系统吧!
构建系统
在创建新的 Web 应用程序时,构建系统可以说是您应该关心的第一件事。 构建系统不仅是运行脚本的工具,而且在 JavaScript 世界中,它通常会塑造应用程序的一般结构。 构建系统必须涵盖的最重要任务如下:
- 管理外部和内部依赖项
- 运行编译器和预处理器
- 优化生产资产
- 运行开发 Web 服务器、文件观察器和浏览器重新加载器
近年来,Bower 和 Grunt 的 Yeoman 工作流被认为是现代前端开发的三位一体,分别解决了样板生成、包管理和常见任务运行的问题,最近更进步的人从 Grunt 切换到 Gulp。
在 React 环境中,您可以放心地忘记这些。 并不是说你不能使用它们,而是你很有可能使用 Webpack 和旧的 NPM 就可以侥幸逃脱。 这怎么可能? Webpack 是一个模块捆绑器,它实现了 CommonJS 模块语法,在 Node.js 世界中很常见,在浏览器中也是如此。 它实际上使事情变得更简单,因为您不需要学习另一个前端包管理器; 您只需使用 NPM 并在服务器和前端之间共享依赖项。 您也不需要处理以正确顺序加载 JS 文件的问题,因为它是从每个文件中指定的依赖项导入推断出来的,并且整个 swarm 正确连接到一个可加载脚本。
为了让事情更具吸引力,Webpack 与它的老兄弟 Browserify 不同,它也可以处理其他资产类型。 例如,使用加载器,您可以将任何资产文件转换为内联或加载引用文件的 JavaScript 函数。 因此,忘记手动预处理和引用 HTML 中的资产。 只require
JavaScript 中的 CSS/SASS/LESS 文件,Webpack 通过一个简单的配置文件来处理其余的事情。 Webpack 还包括一个开发 Web 服务器和一个文件观察器。 另外,您可以使用package.json
中的"scripts"
键来定义 shell 单行:
{ "name": "react-example-filmdb", "version": "0.0.1", "description": "Isomorphic React + Flux film database example", "main": "server/index.js", "scripts": { "build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", "dev": "node --harmony ./webpack/dev-server.js", "prod": "NODE_ENV=production node server/index.js", "test": "./node_modules/.bin/karma start --single-run", "postinstall": "npm run build" } ... }
这就是替换 Gulp 和 Bower 所需的全部内容。 当然,您仍然可以使用 Yeoman 来生成应用程序样板。 当你想要的东西没有 Yeoman 生成器时不要气馁(最前沿的库通常没有)。 你仍然可以从 GitHub 克隆一些样板,然后破解。
明天,今天的 ECMAScript
近年来,JavaScript 语言开发的步伐大幅加快,经过一段时间的消除怪癖和稳定语言后,我们现在看到强大的新功能出现。ECMAScript 6 (ES6) 规范草案已经完成,尽管它尚未正式发布,它已经被广泛采用。 ECMAScript 7 (ES7) 的工作正在进行中,但它的许多功能已经被更前沿的库所采用。
这怎么可能? 也许您认为在 Internet Explorer 支持之前,您无法利用这些闪亮的新 JavaScript 功能,但请再想一想。 ES 转译器已经变得如此普遍,我们甚至可以在没有适当的浏览器支持的情况下完成。 目前可用的最好的 ES 转译器是 Babel:它将获取您最新的 ES6+ 代码,并将其转换为原始 ES5,因此,您可以使用任何新的 ES 功能,一旦它被发明(并在 Babel 中实现,这通常发生迅速地)。
最新的 JavaScript 功能在所有前端框架中都很有用,并且最近更新了 React 以很好地与 ES6 和 ES7 规范配合使用。 在使用 React 进行开发时,这些新功能应该会消除很多令人头疼的问题。 让我们来看看一些最有用的添加,以及它们如何使 React 项目受益。 稍后,我们将看到如何在 React 中使用一些有用的工具和库,同时利用这种改进的语法。
ES6 类
面向对象编程是一种强大且被广泛采用的范例,但 JavaScript 对它的理解有点奇怪。 大多数前端框架,无论是 Backbone、Ember、Angular 还是 React,都采用了自己专有的方式来定义类和创建对象。 但是在 ES6 中,我们现在在 JavaScript 中有传统的类,使用它们而不是编写我们自己的实现是很有意义的。 所以,而不是:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
我们可以写:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
对于更详细的示例,请考虑使用旧语法的代码:
React.createClass({ displayName: 'Counter', getDefaultProps: function(){ return {initialCount: 0}; }, getInitialState: function() { return {count: this.props.initialCount} }, propTypes: {initialCount: React.PropTypes.number}, tick() { this.setState({count: this.state.count + 1}); }, render() { return ( <div onClick={this.tick}> Clicks: {this.state.count} </div> ); } });
并与 ES6 版本进行比较:
class Counter extends React.Component { static propTypes = {initialCount: React.PropTypes.number}; static defaultProps = {initialCount: 0}; constructor(props) { super(props); this.state = {count: props.initialCount}; } state = {count: this.props.initialCount}; tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> ); } }
在这里,不再需要 React 生命周期方法getDefaultProps
和getInitialState
。 getDefaultProps
成为静态类变量defaultProps
,初始状态只是在构造函数中定义。 唯一的缺点是,方法不再是自动绑定的,因此在从 JSX 调用处理程序时必须使用bind
。
装饰器
装饰器是 ES7 的一个有用特性。 它们允许您通过将函数或类包装在另一个函数中来增强其行为。 例如,假设您希望在某些组件上使用相同的更改处理程序,但您不想提交继承反模式。 您可以改用类装饰器。 让我们定义装饰器如下:
addChangeHandler: function(target) { target.prototype.changeHandler = function(key, attr, event) { var state = {}; state[key] = this.state[key] || {}; state[key][attr] = event.currentTarget.value; this.setState(state); }; return target; }
这里重要的是函数addChangeHandler
将changeHandler
函数添加到目标类的原型中。
要应用装饰器,我们可以这样写:
MyClass = changeHandler(MyClass)
或者更优雅,使用 ES7 语法:
@addChangeHandler class MyClass { ... }
至于changeHandler
函数本身的内容,由于 React 没有双向数据绑定,在 React 中处理输入可能很乏味。 changeHandler
函数试图使它更容易。 第一个参数指定状态对象上的一个key
,它将作为输入的数据对象。 第二个参数是属性,输入字段的值将被保存到该属性。 这两个参数是从 JSX 使用bind
关键字设置的。
@addChangeHandler class LoginInput extends React.Component { constructor(props) { super(props); this.state = { login: {} }; } render() { return ( <input type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'login', 'username')} /> <input type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'login', 'password')} /> ) } }
当用户更改用户名字段时,其值将保存到this.state.login.username
,无需定义更多自定义处理程序。
箭头函数
JavaScript 的动态this
上下文一直是开发人员的痛点,因为有点不直观,嵌套函数的this
上下文被重置为全局,即使在类内部也是如此。 为了解决这个问题,通常需要将this
保存到一些外部范围变量(通常是_this
)并在内部函数中使用它:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
使用新的 ES6 语法, function(x){
可以重写为(x) => {
。 这个“箭头”方法定义不仅正确地将this
绑定到外部范围,而且相当短,这在编写大量异步代码时肯定很重要。
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
解构赋值
ES6 中引入的解构赋值允许你在赋值的左侧有一个复合对象:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
这很好,但它实际上如何帮助我们在 React 中? 考虑以下示例:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
通过解构,您可以节省一些击键。 键字面量{url, method, params}
会自动从作用域中分配与键同名的值。 这个习语经常使用,消除重复使代码不易出错。
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
解构还可以帮助您仅加载模块的子集:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
参数:默认、休息和传播
函数参数在 ES6 中更强大。 最后,您可以设置默认参数:
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
厌倦了摆弄笨拙的arguments
对象? 使用新规范,您可以将其余参数作为数组获取:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
如果你不喜欢调用apply()
,你可以将一个数组传播到函数参数中:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
生成器和异步函数
ES6 引入了 JavaScript 生成器。 生成器基本上是一个 JavaScript 函数,它的执行可以暂停,然后在稍后恢复,记住它的状态。 每次遇到yield
关键字时,都会暂停执行,并将yield
参数传回调用对象:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
下面是这个生成器的一个例子:
> var cursor = sequence(1,3) Ready! > cursor.next() { value: 1, done: false } > cursor.next() { value: 2, done: false } > cursor.next() { value: 3, done: false } > cursor.next() { value: undefined, done: true }
当我们调用生成器函数时,它会执行到第一个yield
,然后停止。 在我们调用next()
之后,它返回第一个值,然后继续执行。 每个yield
返回另一个值,但是在第三次调用之后,生成器函数终止,并且对next()
的每个后续调用都将返回{ value: undefined, done: true }
。
当然,生成器的目的不是创建复杂的数字序列。 令人兴奋的部分是它们能够停止和恢复函数执行,可以用来控制异步程序流,最终摆脱那些讨厌的回调函数。
为了演示这个想法,我们首先需要一个异步函数。 通常,我们会有一些 I/O 操作,但为了简单起见,我们只使用setTimeout
并返回一个 Promise。 (请注意,ES6 还向 JavaScript 引入了原生 Promise。)
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
接下来,我们需要一个消费者函数:
function consumer(generator){ var cursor = generator(); var value; function loop() { var data = cursor.next(value); if (data.done) { return; } else { data.value.then(x => { value = x; loop(); }) } } loop(); }
该函数将任何生成器作为参数,只要有要yield
的值,它就会不断调用next()
。 在这种情况下,yield 的值是 Promise,因此需要等待 Promise 解析,并使用递归与loop()
来实现嵌套函数之间的循环。
返回值在then()
处理程序中解析,并传递给在外部范围中定义的value
,并将传递给next(value)
调用。 此调用使该值成为相应 yield 表达式的结果。 这意味着我们现在可以在没有任何回调的情况下异步编写:
function* myGenerator(){ const data1 = yield asyncDouble(1); console.log(`Double 1 = ${data1}`); const data2 = yield asyncDouble(2); console.log(`Double 2 = ${data2}`); const data3 = yield asyncDouble(3); console.log(`Double 3 = ${data3}`); } consumer(myGenerator);
生成器myGenerator
将在每次yield
时暂停,等待消费者交付已解决的承诺。 事实上,我们会看到计算出的数字以一秒的间隔出现在控制台中。
Double 1 = 2 Double 2 = 4 Double 3 = 6
这演示了基本概念,但是,我不建议您在生产中使用此代码。 相反,请选择经过良好测试的库,例如 co。 这将允许您轻松编写带有 yield 的异步代码,包括错误处理:
co(function *(){ var a = yield Promise.resolve(1); console.log(a); var b = yield Promise.resolve(2); console.log(b); var c = yield Promise.resolve(3); console.log(c); }).catch(function(err){ console.error(err.stack); });
所以,这个例子展示了如何使用 ES6 生成器编写没有回调的异步代码。 ES7 通过引入async
和await
关键字将这种方法更进一步,并且完全不需要生成器库。 使用此功能,前面的示例将如下所示:
async function (){ try { var a = await Promise.resolve(1); console.log(a); var b = await Promise.resolve(2); console.log(b); var c = await Promise.resolve(3); console.log(c); } catch (err) { console.error(err.stack); } };
在我看来,这消除了在 JavaScript 中使用异步代码的痛苦。 不仅在 React 中,在其他任何地方也是如此。
生成器不仅更加简洁明了,而且还允许我们使用在回调中很难实现的技术。 生成器优秀的一个突出例子是 Node.js 的 koa 中间件库。 它旨在取代 Express,并且为了实现这一目标,它具有一个杀手级功能:中间件链不仅流向下游(带有客户端请求),而且流向上游,允许进一步修改服务器的响应。 考虑以下 koa 服务器示例:
// Response time logger middleware app.use(function *(next){ // Downstream var start = new Date; yield next; // Upstream this.body += ' World'; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // Response handler app.use(function *(){ this.body = 'Hello'; }); app.listen(3000);
响应中间件yield
下游进入响应处理程序,设置响应主体,在上游流程中(在yield
表达式之后),允许进一步修改this.body
,以及其他功能,如时间记录,这是可能的因为上游和下游共享相同的函数上下文。 这比 Express 功能强大得多,在 Express 中,完成相同事情的尝试会像这样结束:
var start; // Downstream middleware app.use(function(req, res, next) { start = new Date; next(); // Already returned, cannot continue here }); // Response app.use(function (req, res, next){ res.send('Hello World') next(); }); // Upstream middleware app.use(function(req, res, next) { // res already sent, cannot modify var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); next(); }); app.listen(3000);
您可能已经发现这里出了什么问题; 使用“全局” start
变量将导致竞争条件,并发请求返回废话。 解决方案是一些不明显的解决方法,您可以忘记修改上游流中的响应。
此外,使用 koa 时,您将免费获得生成器异步工作流:
app.use(function *(){ try { const part1 = yield fs.readFile(this.request.query.file1, 'utf8'); const part2 = yield fs.readFile(this.request.query.file2, 'utf8'); this.body = part1 + part2; } catch (err) { this.status = 404 this.body = err; } }); app.listen(3000);
您可以想象在 Express 中重新创建这个小示例所涉及的承诺和回调。

所有这些 Node.js 讨论与 React 有什么关系? 好吧,在为 React 考虑合适的后端时,Node 是首选。 由于 Node 也是用 JavaScript 编写的,它支持后端和前端之间的代码共享,允许我们构建同构的 React Web 应用程序。 但是,稍后再谈。
通量库
React 擅长创建可组合的视图组件,但我们需要一些方法来管理整个应用程序的数据和状态。 几乎普遍认为 React 是 Flux 应用程序架构的最佳补充。 如果您对 Flux 完全陌生,我建议您快速刷新一下。
在众多 Flux 实现中选择哪一个尚未得到广泛认同。 Facebook Flux 将是显而易见的选择,但对于大多数人来说,它过于冗长。 替代实现主要侧重于使用约定优于配置的方法减少所需的样板数量,以及一些用于高阶组件、服务器端渲染等的便利功能。 可以在这里看到一些具有各种受欢迎程度指标的顶级竞争者。 我研究过 Alt、Reflux、Flummox、Fluxxor 和 Marty.js。
我选择正确库的方式绝不是客观的,但无论如何它可能会有所帮助。 Fluxxor 是我检查的第一个库之一,但现在它看起来有点陈旧。 Marty.js 很有趣,有很多功能,但仍然涉及很多样板,有些功能似乎是多余的。 Reflux 看起来很棒并且有一定的吸引力,但对于初学者来说感觉有点困难,并且也缺乏适当的文档。 Flummox 和 Alt 非常相似,但 Alt 的样板似乎更少,开发非常活跃,文档最新,Slack 社区很有帮助。因此,我选择了 Alt。
替代通量
使用 Alt,Flux 工作流程变得更加简单,而不会失去任何功能。 Facebook 的 Flux 文档对调度程序进行了很多说明,但我们可以忽略这一点,因为在 Alt 中,调度程序按照惯例隐式连接到操作,并且通常不需要任何自定义代码。 这让我们只剩下存储、操作和组件。 这三层可以很好地映射到MVC思想模型中:商店是模型,动作是控制器,组件是视图。 主要区别在于 Flux 模式的中心单向数据流,这意味着控制器(动作)不能直接修改视图(组件),而是只能触发模型(存储)修改,视图被动绑定到模型(存储)。 对于一些开明的 Angular 开发人员来说,这已经是一个最佳实践。
工作流程如下:
- 组件启动动作。
- 商店监听动作并更新数据。
- 组件绑定到存储,并在数据更新时重新呈现。
行动
使用 Alt Flux 库时,动作通常有两种形式:自动和手动。 自动操作是使用generateActions
函数创建的,它们直接发送给调度程序。 手动方法被定义为您的操作类的方法,它们可以通过额外的有效负载进入调度程序。 自动操作最常见的用例是通知商店有关应用程序中的某些事件。 除其他外,手动操作是处理服务器交互的首选方式。
所以 REST API 调用属于操作。 完整的工作流程如下:
- 组件触发一个动作。
- 动作创建者运行一个异步服务器请求,结果作为有效负载发送到调度程序。
- 存储监听动作,相应的动作处理程序接收结果作为参数,存储相应地更新其状态。
对于 AJAX 请求,我们可以使用 axios 库,其中包括无缝处理 JSON 数据和标头。 我们可以使用 ES7 async
/ await
模式,而不是 promises 或 callbacks。 如果POST
响应状态不是 2XX,则抛出错误,我们分派返回的数据或接收到的错误。
让我们看一个登录页面,了解 Alt 工作流程的一个简单示例。 注销动作不需要做任何特别的事情,只通知商店,这样我们就可以自动生成了。 登录动作是手动的,并且期望登录数据作为动作创建者的参数。 在我们得到服务器的响应后,我们要么发送成功数据,要么,如果抛出错误,我们发送接收到的错误。
class LoginActions { constructor() { // Automatic action this.generateActions('logout'); } // Manual action async login(data) { try { const response = await axios.post('/auth/login', data); this.dispatch({ok: true, user: response.data}); } catch (err) { console.error(err); this.dispatch({ok: false, error: err.data}); } } } module.exports = (alt.createActions(LoginActions));
专卖店
Flux 存储有两个目的:它有动作处理程序,并携带状态。 让我们继续我们的登录页面示例,看看它是如何工作的。
让我们创建具有两个状态属性的LoginStore
: user
,用于当前登录的用户,以及error
,用于当前与登录相关的错误。 本着减少样板的精神,Alt 允许我们使用单个函数bindActions
绑定到一个类中的所有操作。
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
处理程序名称是按约定定义的, on
相应的操作名称之前。 因此login
操作由onLogin
处理,依此类推。 请注意,动作名称的第一个字母将以驼峰形式大写。 在我们的LoginStore
中,我们有以下处理程序,由相应的操作调用:
... onLogin(data) { if (data.ok) { this.user = data.user; this.error = null; router.transitionTo('home'); } else { this.user = null; this.error = data.error } } onLogout() { this.user = null; this.error = null; } }
成分
将 store 绑定到组件的常用方法是使用某种 React mixin。 但是由于 mixins 已经过时了,所以需要一些其他的方式。 其中一种新方法是使用高阶组件。 我们将我们的组件放入一个包装组件中,该组件将负责监听存储并调用重新渲染。 我们的组件将在props
中接收商店的状态。 这种方法也有助于将我们的代码组织成智能和愚蠢的组件,这些组件最近变得很流行。 对于 Alt,组件包装器由AltContainer
实现:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
我们的LoginPage
组件也使用了前面介绍的changeHandler
装饰器。 LoginStore
中的数据用于在登录失败时显示错误,并由AltContainer
重新渲染。 单击登录按钮执行login
操作,完成 Alt 通量工作流程:
@changeHandler export default class LoginPage extends React.Component { constructor(props) { super(props); this.state = { loginForm: {} }; } login() { LoginActions.login(this.state.loginForm) } render() { return ( <Alert>{{this.props.LoginStore.error}}</Alert> <Input label='Username' type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'loginForm', 'username')} /> <Input label='Password' type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'loginForm', 'password')} /> <Button onClick={this.login.bind(this)}>Login</Button> )} }
同构渲染
同构 Web 应用程序现在是一个热门话题,因为它们解决了传统单页应用程序的一些最大难题。 在这些应用程序中,标记是由浏览器中的 JavaScript 动态创建的。 结果是内容对关闭 JavaScript 的客户端不可用,尤其是搜索引擎网络爬虫。 这意味着您的网页未编入索引,也不会出现在搜索结果中。 有一些方法可以解决这个问题,但它们远非最佳。 同构方法试图通过在服务器上预渲染请求的单页应用程序的 URL 来解决这个问题。 使用 Node.js,您在服务器上拥有 JavaScript,这意味着 React 也可以在服务器端运行。 这应该不会太难吧?
一个障碍是某些 Flux 库,尤其是那些使用单例的库,在服务器端渲染方面存在困难。 当您有单例 Flux 存储和多个并发请求到您的服务器时,数据将会混淆。 一些库通过使用 Flux 实例解决了这个问题,但这也带来了其他缺点,特别是需要在代码中传递这些实例。 Alt 也提供了 Flux 实例,但它也解决了服务端渲染单例的问题; 它在每个请求之后刷新存储,以便每个并发请求都以干净的状态开始。
服务器端渲染功能的核心由React.renderToString
提供。 整个 React 前端应用程序也在服务器上运行。 这样,我们不需要等待客户端 JavaScript 来创建标记; 它在服务器上为访问的 URL 预先构建,并以 HTML 的形式发送到浏览器。 当客户端 JavaScript 运行时,它会从服务器停止的地方继续。 为了支持这一点,我们可以使用 Iso 库,该库旨在与 Alt 一起使用。
首先,我们使用alt.bootstrap
在服务器上初始化 Flux。 可以使用数据预填充 Flux 存储以进行渲染。 还需要决定为哪个 URL 渲染哪个组件,这是客户端Router
的功能。 我们正在使用alt
的单例版本,因此在每次渲染之后,我们需要alt.flush()
来清理存储以处理另一个请求。 使用iso
插件,将 Flux 的状态序列化为 HTML 标记,以便客户端知道从哪里获取:
// We use react-router to run the URL that is provided in routes.jsx var getHandler = function(routes, url) { var deferred = Promise.defer(); Router.run(routes, url, function (Handler) { deferred.resolve(Handler); }); return deferred.promise; }; app.use(function *(next) { yield next; // We seed our stores with data alt.bootstrap(JSON.stringify(this.locals.data || {})); var iso = new Iso(); const handler = yield getHandler(reactRoutes, this.request.url); const node = React.renderToString(React.createElement(handler)); iso.add(node, alt.flush()); this.render('layout', {html: iso.render()}); });
在客户端,我们获取服务器状态,并使用数据引导alt
。 然后我们在目标容器上运行Router
和React.render
,这将根据需要更新服务器生成的标记。
Iso.bootstrap(function (state, _, container) { // Bootstrap the state from the server alt.bootstrap(state) Router.run(routes, Router.HistoryLocation, function (Handler, req) { let node = React.createElement(Handler) React.render(node, container) }) })
迷人的!
有用的前端库
如果不提及一些与 React 配合得特别好的前端库,React 生态系统的指南将是不完整的。 这些库处理几乎所有 Web 应用程序中最常见的任务:CSS 布局和容器、样式化的表单和按钮、验证、日期选择等等。 当这些问题已经解决时,重新发明轮子是没有意义的。
反应引导
Twitter 的 Bootstrap 框架已经变得司空见惯,因为它对每个不想花大量时间在 CSS 上工作的 Web 开发人员都有巨大的帮助。 特别是在原型设计阶段,Bootstrap 是必不可少的。 To leverage the power of bootstrap in a React application, it's good to use it with React-Bootstrap, which comes with nice React syntax and re-implements Bootstrap's jQuery plugins using native React components. The resulting code is terse and easy to understand:
<Navbar brand='React-Bootstrap'> <Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar>
Personally, I cannot escape the feeling that this is what HTML should always have been like.
If you want to use Bootstrap source with Webpack, consider using less-loader or bootstrap-sass-loader, depending on the preprocessor you prefer. It will allow you to easily customize which Bootstrap components to include, and also allow the usage of LESS or SASS global variables in your CSS code.
反应路由器
React Router has become the de-facto standard for routing in React. It allows nested routing, support for redirections, plays nicely with isomorphic rendering, and has a simple JSX-based syntax:
<Router history={new BrowserHistory}> <Route path="/" component={App}> <Route path="about" name="about" component={About}/> <Route path="users" name="users" component={Users} indexComponent={RecentUsers}> <Route path="/user/:userId" name="user" component={User}/> </Route> <Route path="*" component={NoMatch}/> </Route> </Router>
React Router also provides a Link
component that you can use for navigation in your application, specifying only the route name:
<nav> <Link to="about">About</Link> <Link to="users">Users</Link> </nav>
There is even a library for integration with React-Bootstrap, so if you are using Bootstrap's components and don't feel like setting the active
class on them manually all the time, you can use react-router-bootstrap and write code like this:
<Nav> <NavItemLink to="about">About</NavItemLink> <NavItemLink to="users">Users</NavItemLink> </Nav>
No additional setup is necessary. Active links will take care of themselves.
Formsy-React
Working with forms can be tedious, so let's get some help from the formsy-react library, which will help us manage validations and data models. The Formsy-React library, strangely enough, does not include the actual form inputs because users are encouraged to write their own (which is great). But if you are content with the common ones, just use formsy-react-components. Bootstrap classes are included:
import Formsy from 'formsy-react'; import {Input} from 'formsy-react-components'; export default class FormsyForm extends React.Component { enableButton() { this.setState({canSubmit: true}); } disableButton() { this.setState({canSubmit: true}); } submit(model) { FormActions.saveEmail(model.email); } render() { return ( <Formsy.Form onValidSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}> <Input name="email" validations="isEmail" validationError="This is not a valid email" required/> <button type="submit" disabled={!this.state.canSubmit}>Submit</button> </Formsy.Form> )} }
Calendar and Typeahead
Calendar and typeahead are icing on the cake of every UI toolkit. Sadly, these two components were removed from Bootstrap 3, probably because they are too specialized for a general purpose CSS framework. Luckily, I've been able to find worthy replacements in react-pikaday and react-select. I've tested more than 10 libraries, and these two came out as the best. They are dead easy to use, as well:
import Pikaday from 'react-pikaday'; import Select from 'react-select'; export default class CalendarAndTypeahead extends React.Component { constructor(props){ super(props); this.options = [ { value: 'one', label: 'One' }, { value: 'two', label: 'Two' } ]; } dateChange(date) { this.setState({date: date}); }, selectChange(selected) { this.setState({selected: selected}); }, render() { return ( <Pikaday value={this.state.date} onChange={this.dateChange} /> <Select name="form-field-name" value={this.state.selected} options={this.options} onChange={selectChange} /> )} }
Conclusion - React.JS
In this article I've presented libraries and techniques that I consider some of the most productive in current web development. Some of them are React-specific, but due to React's open nature, many of them are usable in other environments as well. Technological progress is sometimes hindered by fear of the newest stuff, so I hope this article will help to dissipate doubts concerning React, Flux and the newest features in ECMAScript.
If you are interested, you can take a look at my example application built with these technologies. The source code is available on GitHub.
谢谢阅读!