查看原文
其他

【第830期】如何搭建一个超完美的服务端渲染开发环境

2017-01-20 ChikaraChan 前端早读课

前言

今日早读文章由@ChikaraChan授权分享。


正文从这开始~


前段时间公司有一个产品需求要求使用Node.js中间层来做服务端渲染,于是翻遍了整个技术社区,没有找到一个特别合适的脚手架,作为一个有追求的前端攻城狮,决定自己去搭建一套最完美的服务端渲染开发环境,期间踩过无数的坑,前前后后差不多折腾了三周时间。


服务端渲染好处

  • SEO,让搜索引擎更容易读取页面内容

  • 首屏渲染速度更快(重点),无需等待js文件下载执行的过程

  • 更易于维护,服务端和客户端可以共享某些代码


思考

  • 如何实现组件同构?

  • 如何保持前后端应用状态一致?

  • 如何解决前后端路由匹配问题?

  • 如何处理服务端对静态资源的依赖?

  • 如何配置两套不同的环境(开发环境和产品环境)?

  • 如何划分更合理的项目目录结构?


由于服务端渲染配置的复杂性,大部分人望而止步,而本文的目的就在于教你如何搭建一套优雅的服务端渲染开发环境,从开发打包部署优化到上线。


原理



一个服务端渲染的同构web应用架构图大致如上图所示,得力于Node.js的发展与流行,Javascript成为了一门同构语言,这意味着我们只需写一套代码,可以同时在客户端与服务端执行。


同构方案

这里我们采用React技术体系做同构,由于React本身的设计特点,它是以Virtual DOM的形式保存在内存中,这是服务端渲染的前提。


对于客户端,通过调用ReactDOM.render方法把Virtual DOM转换成真实DOM最后渲染到界面。



对于服务端,通过调用ReactDOMServer.renderToString方法把Virtual DOM转换成HTML字符串返回给客户端,从而达到服务端渲染的目的。



状态管理方案


我们选择Redux来管理React组件的非私有组件状态,并配合社区中强大的中间件Devtools、Thunk、Promise等等来扩充应用。当进行服务端渲染时,创建store实例后,还必须把初始状态回传给客户端,客户端拿到初始状态后把它作为预加载状态来创建store实例,否则,客户端上生成的markup与服务端生成的markup不匹配,客户端将不得不再次加载数据,造成没必要的性能消耗。


服务端



HTML



客户端



路由方案


客户端路由的好处就不必多说了,客户端可以不依赖服务端,根据hash方式或者调用history API,不同的URL渲染不同的视图,实现无缝的页面切换,用户体验极佳。但服务端渲染不同的地方在于,在渲染之前,必须根据URL正确找到相匹配的组件返回给客户端。


React Router为服务端渲染提供了两个API:

  • match 在渲染之前根据URL匹配路由组件

  • RoutingContext 以同步的方式渲染路由组件


服务端



客户端



静态资源处理方案


在客户端中,我们使用了大量的ES6/7语法,jsx语法,css资源,图片资源,最终通过webpack配合各种loader打包成一个文件最后运行在浏览器环境中。但是在服务端,不支持import、jsx这种语法,并且无法识别对css、image资源后缀的模块引用,那么要怎么处理这些静态资源呢?我们需要借助相关的工具、插件来使得Node.js解析器能够加载并执行这类代码,下面分别为开发环境和产品环境配置两套不同的解决方案。


开发环境


  • 首先引入babel-polyfill这个库来提供regenerator运行时和core-js来模拟全功能ES6环境。

  • 引入babel-register,这是一个require钩子,会自动对require命令所加载的js文件进行实时转码,需要注意的是,这个库只适用于开发环境。

  • 引入css-modules-require-hook,同样是钩子,只针对样式文件,由于我们采用的是CSS Modules方案,并且使用SASS来书写代码,所以需要node-sass这个前置编译器来识别扩展名为.scss的文件,当然你也可以采用LESS的方式,通过这个钩子,自动提取className哈希字符注入到服务端的React组件中。

  • 引入asset-require-hook,来识别图片资源,对小于8K的图片转换成base64字符串,大于8k的图片转换成路径引用。



产品环境


对于产品环境,我们的做法是使用webpack分别对客户端和服务端代码进行打包。客户端代码打包这里不多说,对于服务端代码,需要指定运行环境为node,并且提供polyfill,设置__filename和__dirname为true,由于是采用CSS Modules,服务端只需获取className,而无需加载样式代码,所以要使用css-loader/locals替代css-loader加载样式文件



动态加载方案


对于大型Web应用程序来说,将所有代码打包成一个文件不是一种优雅的做法,特别是对于单页面应用,用户有时候并不想得到其余路由模块的内容,加载全部模块内容,不仅增加用户等待时间,而且会增加服务器负荷。Webpack提供一个功能可以拆分模块,每一个模块称为chunk,这个功能叫做Code Splitting。你可以在你的代码库中定义分割点,调用require.ensure,实现按需加载,而对于服务端渲染,require.ensure是不存在的,因此需要判断运行环境,提供钩子函数。


重构后的路由模块为



优化方案


提取第三方库,命名为vendor



所有js模块以chunkhash方式命名



提取公共模块,manifest文件起过渡作用



提取css文件,以contenthash方式命名



模块排序、去重、压缩



使用babel-plugin-transform-runtime取代babel-polyfill,可节省大量文件体积

需要注意的是,你不能使用最新的内置实例方法,例如数组的includes方法



最终打包结果



部署方案


  • 对于客户端代码,将全部的静态资源上传至CDN服务器

  • 对于服务端代码,则采用pm2部署,这是一个带有负载均衡功能的Node应用的进程管理器,支持监控、日志、0秒重载,并可以根据有效CPU数目以cluster的方式启动最大进程数目





其它


提升开发体验


对于客户端代码,可以使用Hot Module Replacement技术,并配合koa-webpack-dev-middleware,koa-webpack-hot-middleware两个中间件,与传统的BrowserSync不同的是,它可以使我们不用通过刷新浏览器的方式,让js和css改动实时更新反馈至浏览器界面中。



对于服务端代码,则使用nodemon监听代码改动,来自动重启node服务器,相比supervisor,更加灵活轻量,内存占用更少,可配置性更高。



对于React组件状态管理,使用Redux DevTools这个中间件,它可以跟踪每一个状态和action,监控数据流,由于采用纯函数的编程思想,还具备状态回溯的能力。需要注意的是,React组件在服务端生命周期只执行到componentWillMount,因此要把该中间件挂载到componentDidMount方法上,避免在服务端渲染而报错。



代码风格约束


推荐使用时下最为流行的ESLint,相比其它QA工具,拥有更多,更灵活,更容易扩展的配置,无论是对个人还是团队协作,引入代码风格检查工具,百益而无一害,建议你花个一天时间尝试一遍ESLint每一项配置,再决定需要哪些配置,舍弃哪些配置,而不是直接去使用Airbnb规范,Google规范等等。


Tips: 使用fix参数可快速修复一些常见错误,在某种程度上,可以取代编辑器格式化工具



结尾


时至今日,开源社区中并没有一个完美的服务端渲染解决方案,而当初搭建这个脚手架的目的就是从易用性出发,以最清晰的配置,用最流行的栈,组最合理的目录结构,给开发者带来最完美的开发体验,从开发打包部署优化到上线,一气呵成。即使你毫无经验,也可轻松入门服务端渲染开发。


附上源码: https://github.com/chikara-chan/react-isomorphic-boilerplate


关于本文

作者:@ChikaraChan

原文:http://www.jianshu.com/p/0ecd727107bb


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存