魅族官网基于-nextjs-重构实践总结与分享

项目背景俗话说,脱离业务谈代码的都是耍流氓。在此我先简单介绍下重构项目的背景。 截图镇楼:魅族官网首页 在 2015 年,公司前端大佬猫哥基于 FIS3 深度定制开发了一套前端工程体系 mz-fis,该框架经历3年来的网站改版升级需求,都很好的完成了需求任务。 但随着项目越来越大,以及前端技术快速迭代。老项目的痛点越发明显。 此次重构解决了那些痛点1.随着项目越来越大,前端编译打包流程巨慢。(算上图片视频等资源,仓库有3.9G大小)2.运营需要经常改动网站内容,由于需要SEO,哪怕改几个字也需要前端打包发布。3.旧框架的核心还是Jquery,虽然结果3年开发积累了很多组件,但在数据维护、模块化以及开发体验上已经落后了。 以上痛点想必手上有老项目的,都感同身受。改起来伤筋动骨,但不改吧工作效率太低了。 此次重构需要满足哪些要求再说说重构的基本要求,咱得渐进增强而不是优雅降级。:D 1.支持SEO,也就是说需要服务端渲染。2.解放前端、测试劳动力,让运营在网站内容管理平台编辑数据后发布,官网及时生效。(不同于传统AJAX,这里数据需要SEO)。3.支持多国语言。4.需要新旧框架同存,同域名下无缝对接,要求两套工作流都可以正常工作。(一些不频繁改动的页面,可以不改,减少重构成本)。5.更快的页面性能、更畅快的开发体验和更好可维护性。 此次重构技术选型首先,服务端渲染 SSR 是没跑了,它可以更快渲染首屏,同时对 SEO 更友好。 于是我在带着鸭梨与小兴奋寻遍各大SSR方案后,最终选择了 Next.jsNext.js 是一个轻量级的 React 服务端渲染应用框架。目前在 github 已获得 4W+ 的 star。 之所以火爆,是因为它有以下优点:1.默认服务端渲染模式,以文件系统为基础的客户端路由2.代码自动分隔使页面加载更快3.简洁的客户端路由(以页面为基础的)4.以webpack的热替换为基础的开发环境5.使用React的JSX和ES6的module,模块化和维护更方便6.可以运行在其他Node.js的HTTP 服务器上7.可以定制化专属的babel和webpack配置 这里不做过多讲解了,大家可以访问 next.js中文网、github地址了解更多。 重构过程中遇到的问题以及解决方案问题一:网站采用 next.js 的 start 模式服务,还是 export 出静态化文件让 ngxin 做web服务两种方案都可行,但各有优缺点。 考虑到运营并不在乎那点等待时间,相比之下项目稳定性更重要。于是选择方案二:「export 出静态化文件让 ngxin 做web服务」。 ok~ 选定后要做的就是静态化了。 问题二:如何静态化如何做呢? 恩... 最简单的就是 cd 到项目目录下 npm run build && npm run export 下,打包出文件到./out文件夹,然后打个zip包扔服务器上。当然,为了运营数据及时更新,你得24小时不停重复以上步奏,还不能手抖出错。 为了不被同事打死,我设计了一套开发流程,在项目中写一个shell脚本: #!/bin/bashecho node版本:$(node -v)BASEDIR=$(dirname $0)cd ${BASEDIR}/../sudo npm run buildwhile true;do whoami && pwd sudo npm run export >/dev/null 2>&1 || continue sudo chown -R {服务器用户名} ./out || echo 'chown Err' sudo cp -ar ./out/* ./www || echo 'cp Err' sudo chown -R {服务器用户名} ./www || echo 'chown Err' echo '静态化并复制完毕' sleep 15done好了,只要执行这段 shell,你的服务器就会cd到项目目录,先build构建项目,然后每间隔15秒构建一次。并输出当前环境和相关信息。 ...

October 14, 2019 · 5 min · jiezi

基于-Webpack-4-多入口生成模板用于服务端渲染的方案及实战

原作者:@LinuxerPHL原链接:基于 Webpack 4 多入口生成模板用于服务端渲染的方案及实战 法律声明警告:本作品遵循 署名-非商业性使用-禁止演绎3.0 未本地化版本(CC BY-NC-ND 3.0) 协议发布。你应该明白与本文有关的一切行为都应该遵循此协议。 这是什么?背景现代化的前端项目中很多都使用了客户端渲染(Client-side Rendering, CSR)的单页面应用(Single Page Application, SPA)。在大多数情况下,它们都应该通过加载 JavaScript 脚本在浏览器中执行以将页面的大部分视图渲染出来,以及获取页面所需要的数据。单页面应用有着许多非常显著的优势,如它们(单页面应用)依赖的公共资源通常仅需加载一次。数据都是通过异步的 JavaScript 与 XML 技术(Asynchoronous JavaScript and XML, Ajax))加载的,异步性能往往非常高。在路由切换时,仅需刷新和(或)更改页面的一部分,而不需要重新加载整个页面以达到切换路由的目的,因此路由的切换在单页面应用中显得比较流畅自然。然而,单页面应用也存在着很多缺陷,它们包括但不限于: 搜索引擎无法收录我们的网页,因为绝大部分的视图和数据都是通过 JavaScript 在浏览器中异步渲染或加载出来的。即使现在有一些搜索引擎爬虫(如 Google)已经具备了爬取单页面应用的能力(即爬虫具备了解析这些 JavaScript 代码的能力),但等到所有搜索引擎爬虫都支持爬取单页面应用显然不是一个好想法;对于业务逻辑复杂一些的单页面应用,它们用于渲染页面的 JavaScript 脚本(可以称为 Bundle)通常体积巨大。试想加载一个上百 KB,甚至几 MB 的 JavaScript 脚本,特别是在没有内容分发网络(Content Delivery Network, CDN)的情况下,首页渲染的时延是非常大的。同构与服务端渲染对于单页面应用上述的缺点,我们可以考虑利用 Webpack 的多入口配置,将原有的单页面应用同构成与原先的前端路由相似甚至相同的目录结构,指定打包后输出的 HTML 模板。在 Webpack 对整个应用打包之后,将根据入口配置从指定的 HTML 模板生成对应的 HTML 文件,交给位于前端页面与后端之间的中间层(通常使用 Node.js 编写,作为服务端渲染(Server-side Rendering, SSR)的服务器)。注意,此时 Webpack 生成的这些 HTML 文件并不能完全被浏览器解析,因为这些文件里还有提供给中间层渲染使用的一些插值,在用户访问中间层路由时,这些 HTML 文件将被用作服务端渲染的模板,将中间层从后端 API 获取的数据按照插值的格式填充(也可以称为“渲染”),最后发送给用户。 进一步理解到目前为止,这个描述还是十分令人困惑。不过我们没有必要一直纠结这些问题,因为下面的图片也许可以帮助我们进一步了解整个流程: ...

August 7, 2019 · 6 min · jiezi

记录使用Nuxt开发服务端渲染项目时遇到的问题难点整理不定时更新

因为公司官网SEO优化问题,必须要用服务端渲染,prerender-spa-plugin预渲染插件不能满足需求。在此记录整理,Nuxt开发服务端渲染项目时遇到的问题难点(不定时更新)1、掘金上有一些很不错的Nuxt教程文章,推荐几篇:https://juejin.im/post/58ff96...https://juejin.im/post/5bd3fb...https://juejin.im/post/5cc81e...https://www.jianshu.com/p/840... 2、在使用window对象时,页面有时会报错window is not defined,解决方法: //用process.client判断是否客户端,包着window对象的代码就行if (process.client) {}3、vue-cli创建的项目,即客户端渲染,在详情页类型的文章页面中,如果右侧列表有同类型的文章,那么在点击跳转路由时,需要用到beforeRouteUpdate,判断from来源是不是本路由,是的话则把详情id更新为to路由对象里的id即可,参考Vue Router官方文档:https://router.vuejs.org/zh/g... //我的代码是这样beforeRouteUpdate(to, from, next){ next(); // console.log(to, from, next) if(from.path.indexOf('NewsDetails') != -1){ this.newsId = to.params.newsId; //更新新闻资讯详情数据 this.getNewsDetailsData(); }},而Nuxt服务端渲染的项目,我做到这个功能点时,发现不需要写beforeRouteUpdate路由钩子,处理判断,页面也能正常跳转展示正确的数据。

June 26, 2019 · 1 min · jiezi

Vue服务端渲染框架Nuxt的那些事

Vue服务端渲染框架Nuxt的那些事最近公司在重构项目,为了有利于SEO,需要使用到服务端渲染,在查阅了一番资料后选择了Nuxt.js,Nuxt.js 是一个基于 Vue.js 的通用应用框架,详情可以看官网,这里主要记录下在使用过程中遇到的问题及解决方案。 技术栈:Vue + Vue-Router + Vuex + Element-Ui + Nuxt + Axios项目构建 Nuxt.js 官方提供了脚手架「 确保安装了npx(npx在NPM版本5.2.0默认安装了)」 npx create-nuxt-app <项目名> 按照提示选择适合自己项目的配置即可 然后 npm run dev。遇到的问题 一、router自定义 发现现在很多的框架都有一套自己的路由生成规则(基于vue-router)然后在对应的目录下创建目录,即会自己生成对应的路由,如果对url的路径没有要求的这样也是可以的,如果想要自定义路由的话,就需要添加些配置。具体如下: extendRoutes (routes, resolve) { routes.push({ name: ‘father’, /** 自定义路由的name / path: ‘/father’, / 自定义路由的path / component: resolve(__dirname, ‘pages/father/index.vue’), / 组件路径 / children: [{ / 子路由配置 (其它相同) / name: ‘son’, path: ‘/son’, component: resolve(__dirname, ‘pages/son/index.vue’) }, { name: ‘daughter’, path: ‘/daughter’, component: resolve(__dirname, ‘pages/daughter/index.vue’) }] }) }对应的参考官方Nuxt.js自定义路由自定义请求头(基于axios请求的base_url修改)需求描述:公司的有正式环境和特使环境对应不同的服务器,所以需要在请求的时候添加对应的请求头,具体配置可以参考如下代码:package.json配置: “scripts”: { “dev”: “cross-env NODE_ENV=development PORT=3333 nuxt”, / 本地环境:这里给环境变量NODE_ENV指定了对应的development的值和指定了运行端口 / “build”: “cross-env NODE_ENV=online nuxt build”, / 打包:指定了环境变量的值为online / “start”: “HOST=0.0.0.0 PORT=3333 nuxt start”, / 打包:指定了环境变量的值为online 端口为3333 HOST为0.0.0.0 百度了一下, 0.0.0.0代表本机的所有ip地址,即同网段其他机器也可以访问的, 默认的127.0.0.1由于和本地ip绑定了,所以只有绑定到本机地址的服务能被同网段其他机器访问**/ “generate”: “nuxt generate”, “lint”: “eslint –ext .js,.vue –ignore-path .gitignore .”, “precommit”: “npm run lint” },axios.js配置: /** 自定义请求base_url /if (process.env.NODE_ENV === ’test’) { axios.defaults.baseURL = ‘http://test’} else if(process.env.NODE_ENV === ‘online’) { axios.defaults.baseURL = ‘http://online’} else { axios.defaults.baseURL = ‘http://127.0.0.1’}这里使用的NODE_ENV由于在nuxt.js默认就存在,所以不需要定义这个变量,如果需要声明一个不存在的环境变量,需要在nuxt.config.js里面添加如下配置/ 下面声明了一个PATH_TYPE变量,其余的不需要改变,只需要将对应的NODE_ENV改成PATH_TYPE即可 /env: { PATH_TYPE: process.env.PATH_TYPE}一定要看备注:要运行上面的示例,你需要运行npm install –save-dev cross-env 安装 cross-env。如果你在非Windows环境下开发,你可以不用安装cross-env,这时需要把 start 脚本中的cross-env去掉。 官方文档:1.主机和端口配置 2.dev属性修改打包webpack配置 nuxt.js框架默认使用过了一套配置,但是看了编译出来的源码后发现css文件全部在源码里,感觉不是很利于收缩引擎的SEO,所以自定义了打包配置,代码如下: optimization: { runtimeChunk: { name: ‘manifest’ }, splitChunks: { chunks: ‘all’, cacheGroups: { libs: { name: ‘chunk-libs’, chunks: ‘initial’, priority: -10, reuseExistingChunk: false, test: /node_modules/(.*).js/ }, styles: { name: ‘chunk-styles’, test: /.(scss|css)$/, chunks: ‘all’, minChunks: 1, reuseExistingChunk: true, enforce: true } } } }, extractCSS: true, / 将css单独打包成一个文件,默认的是全部加载到有事业 **/参考文档: 1.Nuxt.js将CSS提取到一个单独的CSS文件 2.构建配置End:杭州前端一枚:如有疑惑欢迎留言咨询或者474268433@qq.com ???????? ...

December 21, 2018 · 1 min · jiezi

解析Nuxt.js Vue服务端渲染摸索

本篇文章主要介绍了详解Nuxt.js Vue服务端渲染摸索,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。Nuxt.js 十分简单易用。一个简单的项目只需将 nuxt 添加为依赖组件即可。Vue因其简单易懂的API、高效的数据绑定和灵活的组件系统,受到很多前端开发人员的青睐。国内很多公司都在使用vue进行项目开发,我们正在使用的简书,便是基于Vue来构建的。我们知道,SPA前端渲染存在两大痛点:(1)SEO。搜索引擎爬虫难以抓取客户端渲染的页面meta信息和其他SEO相关信息,使网站无法在搜索引擎中被用户搜索到。(2)用户体验。大型webApp打包之后的js会很庞大,于是就有了按模块加载,像require.js一样,异步请求。webpack盛行,就变成了代码分割。即便如此,受制于用户设备,页面初次渲染还是有可能很慢,白屏等待时间太长,对日益挑剔的用户群体来说,无法接受。因此,对于那些展示宣传型页面,如官网,必须进行服务端渲染(SSR)。安装 nuxt.js$ vue init nuxt-community/starter-template <你项目的名字>// 后面 安装依赖你懂的// 安装koa版本$ vue init nuxt/koa <你的项目名字>运行npm run dev应用现在运行在 http://localhost:3000注意:Nuxt.js 会监听 pages 目录中的文件变更并自动重启, 当添加新页面时没有必要手工重启应用。路由nuxt 是根据pages 目录结构生成路由配置异步数据asyncData注意必须要页面组件才能调用asyncData(就是components下是不能调用,必须路由的页面才行)异步数据beforeCreate,created注意:在任何vue组件的生命周期内,只有beforeCreate和created这两个钩子会在浏览器端和服务端均被调用;其他的钩子都只会在浏览器端调用。使用插件mint-ui首先我们需要在plugins文件夹中添加插件文件 mint-ui.jsimport Vue from “vue”;import Mint from “mint-ui”;//欢迎加入前端全栈开发交流圈一起学习交流:864305860 Vue.use(Mint);在nuxt.config.js中配置plugins字段/** * 配置第三方插件 */ plugins: [{ src: “~plugins/mint-ui”, ssr: true }], //欢迎加入前端全栈开发交流圈一起学习交流:864305860//同时nuxt还支持区分只在浏览器中运行和只在服务端运行的插件 //只在浏览器运行:配置nuxt.config.js中plugins字段,将引入的插件属性设置为ssr: false//只在服务端运行:直接在webpack打包server.bundle.js文件中,将process.SERVER_BUILD设置为true即可layout布局1.nuxt.js实现了一个新的概念,layout布局,我们可以通过layout布 局方便的实现页面的多个布局之间方便的切换。本项目中实现了三种常用的布局,即:1)两栏布局,左栏固定,右栏动态宽度;2、错误页提示,页面中间一个提示框的布局方案;3、纯白页面布局。具体开发的页面中,如果使用默认布局,则不需指定页面的布局,nuxt框架会自动对没有指定布局的页面和default布局进行关联。如果需要指定布局,则在layout字段中对布局进行指定。如图在login页面中对full布局进行了指定。结语感谢您的观看,如有不足之处,欢迎批评指正。

December 14, 2018 · 1 min · jiezi

React 服务端渲染方案完美的解决方案

最近在开发一个服务端渲染工具,通过一篇小文大致介绍下服务端渲染,和服务端渲染的方式方法。在此文后面有两中服务端渲染方式的构思,根据你对服务端渲染的利弊权衡,你会选择哪一种服务端渲染方式呢?什么是服务器端渲染使用 React 构建客户端应用程序,默认情况下,可以在浏览器中输出 React 组件,进行生成 DOM 和操作 DOM。React 也可以在服务端通过 Node.js 转换成 HTML,直接在浏览器端“呈现”处理好的 HTML 字符串,这个过程可以被认为 “同构”,因为应用程序的大部分代码都可以在服务器和客户端上运行。为什么使用服务器端渲染与传统 SPA(Single Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。更好的用户体验,对于缓慢的网络情况或运行缓慢的设备,加载完资源浏览器直接呈现,无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的HTML。服务端渲染的弊端由于服务端与浏览器客户端环境区别,选择一些开源库需要注意,部分库是无法在服务端执行,比如你有 document、window 等对象获取操作,都会在服务端就会报错,所以在选择的开源库要做甄别。使用服务端渲染,比如要起一个专门在服务端渲染的服务,与之前,只管客户端所需静态资源不同,你还需要 Node.js 服务端的和运维部署的知识,对你所需要掌握的知识点要求更多服务器需要更多的负载,在 Node.js 中完成渲染,由于 Node.js 的原因大量的CPU资源会被占用。下文介绍一种服务端渲染的“操作”,这个新的操作拥有新的问题,比如API请求两次,各种服务端问题,你就无能为力了,因为这个新的工具用Golang写的,你的团队或者是你,需要了解一下Golang,你说气不气人又要多学东西。服务端渲染两种方式根据上文介绍对服务端渲染利弊有所了解,我们可以根据利弊权衡取舍,最近在做服务端渲染的项目,找到多种服务端渲染解决方案,大致分为两类。第一种方式传统方式服务端渲染,解决用户体验和更好的 SEO,有诸多工具使用这种方式如React的(Next.js)、Vue的(Nuxt.js)等。有些工具将 webpack 运行在服务端生产环境,实时编译,将编译结果缓存起来,这都还是传统的方式,只不过将 webpack 运行在服务端实时编译,还是开发环境编译预编译好的问题。我选择了将 webpack 放在开发环境,只做开发打包的功能,打包 客户端 bundle ,服务端 bundle,资源映射文件 assets.json,CSS 等资源进行部署。服务器 bundle 用于服务器端渲染(SSR)客户端 bundle 给浏览器加载,浏览器通过 bundle 加载更多其它模块(chunk)js资源映射文件 assets.json 则是,服务器 bundle 在准备所需 HTML,需要预插入那些模块(chunk)js,和CSS,这只是提高用户体验。具体使用方法,可以看我最近造的个轮子 kkt-ssr,这个轮子将工具的部分封装起来,你只需要写业务代码,和少量的服务端渲染代码即可,还附赠十几个示例,加上一个相对比较完善的示例react-router+rematch,类似于 next.js,但是有相当大的区别。第二种方式这是一种创新的方法,前端单页面应用,以前怎么玩儿,现在还怎么玩儿,多的一步是,你得先访问一个Rendora的服务,在前面拦截是否需要服务端渲染。下图为官方图:这种方式原本只是个想法,想法是前端不用管服务端渲染的事儿了,不就是解决SEO?,这些爬虫过来的时候,可以通过头信息判断,写个服务,然后将需要的内容给爬虫就可以了,昨天恰巧在GitHub的趋势榜上,恰巧看到 Rendora 个工具,也就那么巧,刚好思路一致,这个工具主要为网络爬虫提供零配置服务器端渲染,以便毫不费力地改进在现代Javascript框架(如React.js,Vue.js,Angular.js等)中开发的网站的SEO问题。这种方式非常好,之前写好的项目一句不用改,只需新起 Rendora 服务。对于来自前端服务器或外部的每个请求(百度谷歌爬虫),Rendora会根据配置文件,根据头,路径来检测或过滤,以确定 Rendora 是否应该只传递从后端服务器返回的初始HTML或使用Chrome提供的无头服务器端呈现的HTML。更具体地说,对于每个请求,有2条路径:请求被列入白名单作为SSR的候选者(即过滤后的Get请求),Rendora 会指示无头Chrome实例请求相应的页面,呈现它,并返回包含最终服务器端的响应呈现出HTML。通常只需要将百度、谷歌、必应爬虫等网络抓取工具列入白名单即可。未列入白名单(即请求不是GET请求或未通过任何过滤器),Rendora将只是充当反向HTTP代理,只是按原样传送请求和响应。Rendora可以看作是位于后端服务器(例如Node.js / Express.js,Python / Django等等)之间的反向HTTP代理服务器,也可能是你的前端代理服务器(例如nginx,traefik,apache等),Rendora 是我见过的接近于完美的动态渲染器,提供零配置服务器端渲染我们到底选择哪一种服务端渲染呢?Rendora,新的方式非常厉害,有很多优势:方便迁移老的项目,前端和后端代码不需要更改。可能更快的性能,资源(CPU)消耗可能更少,Golang编写的二进制文件多种缓存策略已经拥有 docker 容器方案此工具,服务端渲染的页面需要缓存,缓存引发的小问题就是通过缓存解决,性能问题和调用API两次的问题,服务端渲染,客户端展示渲染,平常调用一次API,现在调用了两次。被缓存的页面,不能及时清理,比如网站发现用户发了不良信息,需要清理,就需要清理缓存页面了。如果想提高用户体验,浏览器端一些页面需要服务端渲染,这个时候服务端需要请求API,就会有权限问题,或者直接从缓存里面读取的HTML,到浏览器客户端,可能会有服务端和浏览器端渲染不一致的错误。如果上面两种方式不在你的考虑范畴之内,那Rendora将是你完美的服务端渲染解决方案总结感觉我的轮子 kkt-ssr 好像白写了一样,经过分析发现目前还有一点作用吧,至少解决了不多调用一次API,和API调用权限问题导致渲染不一致的问题。但是我更推荐Rendora的方式,这将是未来。 ...

December 13, 2018 · 1 min · jiezi

React 中同构(SSR)原理脉络梳理

随着越来越多新型前端框架的推出,SSR 这个概念在前端开发领域的流行度越来越高,也有越来越多的项目采用这种技术方案进行了实现。SSR 产生的背景是什么?适用的场景是什么?实现的原理又是什么?希望大家在这篇文章中能够找到你想要的答案。说到 SSR,很多人的第一反应是“服务器端渲染”,但我更倾向于称之为“同构”,所以首先我们来对“客户端渲染”,“服务器端渲染”,“同构”这三个概念简单的做一个分析:客户端渲染:客户端渲染,页面初始加载的 HTML 页面中无网页展示内容,需要加载执行JavaScript 文件中的 React 代码,通过 JavaScript 渲染生成页面,同时,JavaScript 代码会完成页面交互事件的绑定,详细流程可参考下图(图片取材自 fullstackacademy.com):服务器端渲染:用户请求服务器,服务器上直接生成 HTML 内容并返回给浏览器。服务器端渲染来,页面的内容是由 Server 端生成的。一般来说,服务器端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入 JavaScript 文件来辅助实现。服务器端渲染这个概念,适用于任何后端语言。同构:同构这个概念存在于 Vue,React 这些新型的前端框架中,同构实际上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示内容和交互写在一起,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互,详细流程可参考下图(图片取材自 fullstackacademy.com):一般情况下,当我们使用 React 编写代码时,页面都是由客户端执行 JavaScript 逻辑动态挂 DOM 生成的,也就是说这种普通的单页面应用实际上采用的是客户端渲染模式。在大多数情况下,客户端渲染完全能够满足我们的业务需求,那为什么我们还需要 SSR 这种同构技术呢?使用 SSR 技术的主要因素:CSR 项目的 TTFP(Time To First Page)时间比较长,参考之前的图例,在 CSR 的页面渲染流程中,首先要加载 HTML 文件,之后要下载页面所需的 JavaScript 文件,然后 JavaScript 文件渲染生成页面。在这个渲染过程中至少涉及到两个 HTTP 请求周期,所以会有一定的耗时,这也是为什么大家在低网速下访问普通的 React 或者 Vue 应用时,初始页面会有出现白屏的原因。CSR 项目的 SEO 能力极弱,在搜索引擎中基本上不可能有好的排名。因为目前大多数搜索引擎主要识别的内容还是 HTML,对 JavaScript 文件内容的识别都还比较弱。如果一个项目的流量入口来自于搜索引擎,这个时候你使用 CSR 进行开发,就非常不合适了。SSR 的产生,主要就是为了解决上面所说的两个问题。在 React 中使用 SSR 技术,我们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了所有的页面展示内容,这样,页面展示的过程只需要经历一个 HTTP 请求周期,TTFP 时间得到一倍以上的缩减。同时,由于 HTML 中已经包含了网页的所有内容,所以网页的 SEO 效果也会变的非常好。之后,我们让 React 代码在客户端再次执行,为 HTML 网页中的内容添加数据及事件的绑定,页面就具备了 React 的各种交互能力。但是,SSR 这种理念的实现,并非易事。我们来看一下在 React 中实现 SSR 技术的架构图:使用 SSR 这种技术,将使原本简单的 React 项目变得非常复杂,项目的可维护性会降低,代码问题的追溯也会变得困难。所以,使用 SSR 在解决问题的同时,也会带来非常多的副作用,有的时候,这些副作用的伤害比起 SSR 技术带来的优势要大的多。从个人经验上来说,我一般建议大家,除非你的项目特别依赖搜索引擎流量,或者对首屏时间有特殊的要求,否则不建议使用 SSR。好,如果你确实遇到了 React 项目中要使用 SSR 的场景并决定使用 SSR,那么接下来我们就结合上面这张 SSR 架构图,开启 SSR 技术点的难点剖析。在开始之前,我们先来分析下虚拟 DOM 和 SSR 的关系。SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在上面我们说过,SSR 的工程中,React 代码会在客户端和服务器端各执行一次。你可能会想,这没什么问题,都是 JavaScript 代码,既可以在浏览器上运行,又可以在 Node 环境下运行。但事实并非如此,如果你的 React 代码里,存在直接操作 DOM 的代码,那么就无法实现 SSR 这种技术了,因为在 Node 环境下,是没有 DOM 这个概念存在的,所以这些代码在 Node 环境下是会报错的。好在 React 框架中引入了一个概念叫做虚拟 DOM,虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务器,我可以操作 JavaScript 对象,判断环境是服务器环境,我们把虚拟 DOM 映射成字符串输出;在客户端,我也可以操作 JavaScript 对象,判断环境是客户端环境,我就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载。其他的一些框架,比如 Vue,它能够实现 SSR 也是因为引入了和 React 中一样的虚拟 DOM 技术。好,接下来我们回过头看流程图,前两步不说了,服务器端渲染肯定要先向 Node 服务器发送请求。重点是第 3 步,大家可以看到,服务器端要根据请求的地址,判断要展示什么样的页面了,这一步叫做服务器端路由。我们再看第 10 步,当客户端接收到 JavaScript 文件后,要根据当前的路径,在浏览器上再判断当前要展示的组件,重新进行一次客户端渲染,这个时候,还要经历一次客户端路由(前端路由)。那么,我们下面要说的就是服务器端路由和客户端路由的区别。SSR 中客户端渲染与服务器端渲染路由代码的差异实现 React 的 SSR 架构,我们需要让相同的 React 代码在客户端和服务器端各执行一次。大家注意,这里说的相同的 React 代码,指的是我们写的各种组件代码,所以在同构中,只有组件的代码是可以公用的,而路由这样的代码是没有办法公用的,大家思考下这是为什么呢?其实原因很简单,在服务器端需要通过请求路径,找到路由组件,而在客户端需通过浏览器中的网址,找到路由组件,是完全不同的两套机制,所以这部分代码是肯定无法公用。我们来看看在 SSR 中,前后端路由的实现代码:客户端路由:const App = () => { return ( <Provider store={store}> <BrowserRouter> <div> <Route path=’/’ component={Home}> </div> </BrowserRouter> </Provider> )}ReactDom.render(<App/>, document.querySelector(’#root’))客户端路由代码非常简单,大家一定很熟悉,BrowserRouter 会自动从浏览器地址中,匹配对应的路由组件显示出来。服务器端路由代码:const App = () => { return <Provider store={store}> <StaticRouter location={req.path} context={context}> <div> <Route path=’/’ component={Home}> </div> </StaticRouter> </Provider>}Return ReactDom.renderToString(<App/>)服务器端路由代码相对要复杂一点,需要你把 location(当前请求路径)传递给 StaticRouter 组件,这样 StaticRouter 才能根据路径分析出当前所需要的组件是谁。(PS:StaticRouter 是 React-Router 针对服务器端渲染专门提供的一个路由组件。)通过 BrowserRouter 我们能够匹配到浏览器即将显示的路由组件,对浏览器来说,我们需要把组件转化成 DOM,所以需要我们使用 ReactDom.render 方法来进行 DOM 的挂载。而 StaticRouter 能够在服务器端匹配到将要显示的组件,对服务器端来说,我们要把组件转化成字符串,这时我们只需要调用 ReactDom 提供的 renderToString 方法,就可以得到 App 组件对应的 HTML 字符串。对于一个 React 应用来说,路由一般是整个程序的执行入口。在 SSR 中,服务器端的路由和客户端的路由不一样,也就意味着服务器端的入口代码和客户端的入口代码是不同的。我们知道, React 代码是要通过 Webpack 打包之后才能运行的,也就是第 3 步和第10 步运行的代码,实际上是源代码打包过后生成的代码。上面也说到,服务器端和客户端渲染中的代码,只有一部分一致,其余是有区别的。所以,针对代码运行环境的不同,要进行有区别的 Webpack 打包。服务器端代码和客户端代码的打包差异简单写两个 Webpack 配置文件作为 DEMO:客户端 Webpack 配置:{ entry: ‘./src/client/index.js’, output: { filename: ‘index.js’, path: path.resolve(__dirname, ‘public’) }, module: { rules: [{ test: /.js?$/, loader: ‘babel-loader’ },{ test: /.css?$/, use: [‘style-loader’, { loader: ‘css-loader’, options: {modules: true} }] },{ test: /.(png|jpeg|jpg|gif|svg)?$/, loader: ‘url-loader’, options: { limit: 8000, publicPath: ‘/’ } }] }}服务器端 Webpack 配置:{ target: ’node’, entry: ‘./src/server/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘build’) }, externals: [nodeExternals()], module: { rules: [{ test: /.js?$/, loader: ‘babel-loader’ },{ test: /.css?$/, use: [‘isomorphic-style-loader’, { loader: ‘css-loader’, options: {modules: true} }] },{ test: /.(png|jpeg|jpg|gif|svg)?$/, loader: ‘url-loader’, options: { limit: 8000, outputPath: ‘../public/’, publicPath: ‘/’ } }] }};上面我们说了,在 SSR 中,服务器端渲染的代码和客户端的代码的入口路由代码是有差异的,所以在 Webpack 中,Entry 的配置首先肯定是不同的。在服务器端运行的代码,有时我们需要引入 Node 中的一些核心模块,我们需要 Webpack 做打包的时候能够识别出类似的核心模块,一旦发现是核心模块,不必把模块的代码合并到最终生成的代码中,解决这个问题的方法非常简单,在服务器端的 Webpack配置中,你只要加入 target: node 这个配置即可。服务器端渲染的代码,如果加载第三方模块,这些第三方模块也是不需要被打包到最终的源码中的,因为 Node 环境下通过 NPM 已经安装了这些包,直接引用就可以,不需要额外再打包到代码里。为了解决这个问题,我们可以使用 webpack-node-externals 这个插件,代码中的 nodeExternals 指的就是这个插件,通过这个插件,我们就能解决这个问题。关于 Node 这里的打包问题,可能看起来有些抽象,不是很明白的同学可以仔细读一下 webpack-node-externals 相关的文章或文档,你就能很好的明白这里存在的问题了。接下来我们继续分析,当我们的 React 代码中引入了一些 CSS 样式代码时,服务器端打包的过程会处理一遍 CSS,而客户端又会处理一遍。查看配置,我们可以看到,服务器端打包时我们用了 isomorphic-style-loader,它处理 CSS 的时候,只在对应的 DOM 元素上生成 class 类名,然后返回生成的 CSS 样式代码。而在客户端代码打包配置中,我们使用了 css-loader 和 style-loader,css-loader 不但会在 DOM 上生成 class 类名,解析好的 CSS 代码,还会通过 style-loader 把代码挂载到页面上。不过这么做,由于页面上的样式实际上最终是由客户端渲染时添加上的,所以页面可能会存在一开始没有样式的情况,为了解决这个问题, 我们可以在服务器端渲染时,拿到 isomorphic-style-loader 返回的样式代码,然后以字符串的形式添加到服务器端渲染的 HTML 之中。而对于图片等类型的文件引入,url-loader 也会在服务器端代码和客户端代码打包的过程中分别进行打包,这里,我偷了一个懒,无论服务器端打包还是客户端打包,我都让打包生成的文件存储在 public 目录下,这样,虽然文件会打包出来两遍,但是后打包出来的文件会覆盖之前的文件,所以看起来还是只有一份文件。当然,这样做的性能和优雅性并不高,只是给大家提供一个小的思路,如果想进行优化,你可以让图片的打包只进行一次,借助一些 Webpack 的插件,实现这个也并非难事,你甚至可以自己也写一个 loader,来解决这样的问题。如果你的 React 应用中没有异步数据的获取,单纯的做一些静态内容展示,经过上面的配置,你会发现一个简单的 SSR 应用很快的就可以被实现出来了。但是,真正的一个 React 项目中,我们肯定要有异步数据的获取,绝大多数情况下,我们还要使用 Redux 管理数据。而如果想在 SSR 应用中实现,就不是这么简单了。SSR 中异步数据的获取 + Redux 的使用客户端渲染中,异步数据结合 Redux 的使用方式遵循下面的流程(对应图中第 12 步):创建 Store根据路由显示组件派发 Action 获取数据更新 Store 中的数据组件 Rerender而在服务器端,页面一旦确定内容,就没有办法 Rerender 了,这就要求组件显示的时候,就要把 Store 的数据都准备好,所以服务器端异步数据结合 Redux 的使用方式,流程是下面的样子(对应图中第 4 步):创建 Store根据路由分析 Store 中需要的数据派发 Action 获取数据更新Store 中的数据结合数据和组件生成 HTML,一次性返回下面,我们分析下服务器端渲染这部分的流程:创建 Store:这一部分有坑,要注意避免,大家知道,客户端渲染中,用户的浏览器中永远只存在一个 Store,所以代码上你可以这么写:const store = createStore(reducer, defaultState)export default store;然而在服务器端,这么写就有问题了,因为服务器端的 Store 是所有用户都要用的,如果像上面这样构建 Store,Store 变成了一个单例,所有用户共享 Store,显然就有问题了。所以在服务器端渲染中,Store 的创建应该像下面这样,返回一个函数,每个用户访问的时候,这个函数重新执行,为每个用户提供一个独立的 Store:const getStore = (req) => { return createStore(reducer, defaultState);}export default getStore;根据路由分析 Store 中需要的数据: 要想实现这个步骤,在服务器端,首先我们要分析当前出路由要加载的所有组件,这个时候我们可以借助一些第三方的包,比如说 react-router-config, 具体这个包怎么使用,不做过多说明,大家可以查看文档,使用这个包,传入服务器请求路径,它就会帮助你分析出这个路径下要展示的所有组件。派发 Action 获取数据: 接下来,我们在每个组件上增加一个获取数据的方法:Home.loadData = (store) => { return store.dispatch(getHomeList())}这个方法需要你把服务器端渲染的 Store 传递进来,它的作用就是帮助服务器端的 Store 获取到这个组件所需的数据。 所以,组件上有了这样的方法,同时我们也有当前路由所需要的所有组件,依次调用各个组件上的 loadData 方法,就能够获取到路由所需的所有数据内容了。更新 Store 中的数据: 其实,当我们执行第三步的时候,已经在更新 Store 中的数据了,但是,我们要在生成 HTML 之前,保证所有的数据都获取完毕,这怎么处理呢?// matchedRoutes 是当前路由对应的所有需要显示的组件集合matchedRoutes.forEach(item => { if (item.route.loadData) { const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }})Promise.all(promises).then(() => { // 生成 HTML 逻辑})这里,我们使用 Promise 来解决这个问题,我们构建一个 Promise 队列,等待所有的 Promise 都执行结束后,也就是所有 store.dispatch 都执行完毕后,再去生成 HTML。这样的话,我们就实现了结合 Redux 的 SSR 流程。在上面,我们说到,服务器端渲染时,页面的数据是通过 loadData 函数来获取的。而在客户端,数据获取依然要做,因为如果这个页面是你访问的第一个页面,那么你看到的内容是服务器端渲染出来的,但是如果经过 react-router 路由跳转道第二个页面,那么这个页面就完全是客户端渲染出来的了,所以客户端也要去拿数据。在客户端获取数据,使用的是我们最习惯的方式,通过 componentDidMount 进行数据的获取。这里要注意的是,componentDidMount 只在客户端才会执行,在服务器端这个生命周期函数是不会执行的。所以我们不必担心 componentDidMount 和 loadData 会有冲突,放心使用即可。这也是为什么数据的获取应该放到 componentDidMount 这个生命周期函数中而不是 componentWillMount 中的原因,可以避免服务器端获取数据和客户端获取数据的冲突。Node 只是一个中间层上一部分我们说到了获取数据的问题,在 SSR 架构中,一般 Node 只是一个中间层,用来做 React 代码的服务器端渲染,而 Node 需要的数据通常由 API 服务器单独提供。这样做一是为了工程解耦,二也是为了规避 Node 服务器的一些计算性能问题。请大家关注图中的第 4 步和第 12,13 步,我们接下来分析这几个步骤。服务器端渲染时,直接请求 API 服务器的接口获取数据没有任何问题。但是在客户端,就有可能存在跨域的问题了,所以,这个时候,我们需要在服务器端搭建 Proxy 代理功能,客户端不直接请求 API 服务器,而是请求 Node 服务器,经过代理转发,拿到 API 服务器的数据。这里你可以通过 express-http-proxy 这样的工具帮助你快速搭建 Proxy 代理功能,但是记得配置的时候,要让代理服务器不仅仅帮你转发请求,还要把 cookie 携带上,这样才不会有权限校验上的一些问题。// Node 代理功能实现代码app.use(’/api’, proxy(‘http://apiServer.com’, { proxyReqPathResolver: function (req) { return ‘/ssr’ + req.url; }}));总结:到这里,整个 SSR 的流程体系中关键知识点的原理就串联起来了,如果你之前适用过 SSR 框架,那么这些知识点的整理我相信可以从原理层面很好的帮助到你。当然,我也考虑到阅读本篇文章的同学可能有很大一部分对 SSR 的基础知识非常有限,看了文章可能会云里雾里,这里为了帮助这些同学,我编写了一个非常简单的 SSR 框架,代码放在这里:https://files.alicdn.com/tpss…初学者结合上面的流程图,一步步梳理流程图中的逻辑,梳理结束后,回来再看一遍这篇文章,相信大家就豁然开朗了。当然在真正实现 SSR 架构的过程中,难点有时不是实现的思路,而是细节的处理。比如说如何针对不同页面设置不同的 title 和 description 来提升 SEO 效果,这时候,我们其实可以用 react-helmet 这样的工具帮我们达成目标,这个工具对客户端和服务器端渲染的效果都很棒,值得推荐。还有一些诸如工程目录的设计,404,301 重定向情况的处理等等,不过这些问题,我们只需要在实践中遇到的时候逐个攻破就可以了。好了,关于 SSR 的全部分享就到这里,希望这篇文章能够或多或少帮助到你。参考文档Webpack 官方网站What is React Server Side Rendering and should I use it?StaticRouterThe Pain and the Joy of creating isomorphic apps in ReactJS文章可随意转载,但请保留此 原文链接。非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。 ...

October 18, 2018 · 4 min · jiezi