关于ssr:解决基于-Webpack-构建的-Vue-服务端渲染项目首屏渲染样式闪烁的问题

前言当咱们应用 Webpack 搭建一个基于 Vue 的服务端渲染我的项目时,通常会遇到一个很麻烦的问题,即咱们无奈提前获取到以后页面所需的资源,从而不能提前加载以后页面所需的 CSS,导致客户端在获取到服务端渲染的 HTML 时,失去的只有 HTML 文本而没有 CSS 款式,之后须要期待一会儿能力将 CSS 加载进去,也就是会遇到『款式闪动』这样的问题。 问题剖析这是因为 webpack 利用的代码加载机制导致的。 在大型利用中,webpack 不可能将我的项目只打包为独自的一个 js、css 文件,而是会利用 webpack 的 代码宰割 机制,将宏大的代码依照肯定的规定(比方超过肯定的大小、或者被屡次援用)进行拆分,这样代码的产出就会成为如下的样子: 注:xxx 指的是每次打包生成的文件哈希,用于更新浏览器的本地缓存,更多详情参考 官网文档// 入口文件main.xxx.jsmain.xxx.css// runtime 文件,后续重点介绍runtimechunk~main.xxx.js// 应用了异步加载形式引入而被拆分的包,如 vue-router 的路由懒加载layout.xxx.jslayout.xxx.csshome-page.xxx.jshome-page.xxx.cssuser-page.xxx.jsuser-page.xxx.css// 被拆分的子包(如果被拆分的子包中没有 css 文件的引入,那么就不会生成 css 子包)73e8df2.xxx.js73e8df2.xxx.css980e123.xxx.js如上,如果没有进行非凡的 webpack 分包配置,个别就会生成如上四种类型的包,并且如果应用了 css-minimizer-webpack-plugin 的话(PS:这个包是必须的),还会为每个援用了 css 的子包再独自生成一个对应的 css 文件。这四种类型的包在整体上还能够被具体划分为两类: 具名子包(namedChunk)随机命名子包main.xxx.js 这种入口文件,以及 home-page.xxx.js 这样异步引入同时并应用 Comments 进行命名的包,被称为『具名子包』;而相似 73e8df2.xxx.js 这种文件名是由一串随机哈希组成的文件,咱们将其称为『随机命名子包』。 通常这两种包是存在依赖关系的,随机命名子包其实就是从命名子包中拆分进去的代码,或者是多个命名子包共用的某一部分代码,依赖关系示例如下: 当咱们打包好一个 Vue 利用之后,假如 chunk 之间的依赖关系如上图所示,打包好的 HTML 会按程序内联入如下几个 js 和 css: runtimechunk~main.js73e9df.js29fe22.jsmian.jsmain.cssmian.js 被内联入 HTML 的起因是因为其是以后 Vue 利用的入口文件,不管用户拜访哪个页面都会加载,因而必须被内联到 HTML 中;73e9df.js、29fe22.js 这两个文件被内联入 HTML 的起因是因为他们属于 main.js 的依赖 chunk,vue 相干的代码就很可能被打包到这两个子包中,main.js 如果想要失常运行就必须要先加载这两个包;main.css 被内联到 HTML 的起因是因为 main.js 中援用了一些 css,这些 css 也会被视作利用加载的必要加载项。 ...

June 7, 2023 · 4 min · jiezi

关于ssr:Hydration-failed-because-the-initial-UI-does-not-match

ErrorHydration failed because the initial UI does not match what was rendered on the server. 起因在出现您的应用程序时,预出现的 React 树 (SSR/SSG) 与在浏览器中首次出现期间出现的 React 树之间存在差别。第一个渲染称为 Hydration,这是 React 的一个个性。这可能会导致 React 树与 DOM 不同步,并导致出现意外的内容/属性。注:还有一种水合谬误起因是,Nextjs中并不倡议应用p标签中包裹div标签,如果呈现如此构造,那么也将导致呈现谬误。此外,antd的Typography 组件默认是p标签,当对此应用时,那么也可能会导致呈现此类问题。 水合作用是什么?为什么应用它?服务器端渲染(SSR)被 nextjs 等框架用来进步性能(LCP 和 FCP)和用户体验(SEO),它首先在服务器上渲染应用程序。它向用户返回一个残缺格局的 HTML 文档,但应用程序是“动静的”,并不是所有事件都能够通过 HTML 和 CSS 实现,所以咱们通知 React 将事件处理程序附加到 HTML 以使应用程序具备交互性。这个渲染咱们的组件和附加事件处理程序的过程被称为“水化”。这就像用交互性(JS)的“水”浇灌“干燥”的 HTML。水合作用后,咱们的应用程序变得交互式或“动静”。hydration 和 rehydration 通常能够调换应用,然而在 rehydration 期间,它在用户的设施上运行,并构建了一幅世界应该是什么样子的图画。而后将其与文档中内置的 HTML 进行比拟。这是一个称为再水合的过程。在再水合中,React 假如 DOM 不会扭转。它只是试图适应现有的 DOM。当 React 应用程序从新水合时,它假设 DOM 构造将匹配。如果不是那么你晓得将会呈现问题。用大白话来说就是,服务端渲染了一幅dom构造,而后浏览器端又依据某个状态从新渲染了一副dom构造,这时候就会产生,前后两幅构造进行交融(再水合),ssr默认构造是不变的,然而如果构造产生了扭转,那么问题就呈现了!解决一export default function Nav() { const [hasMounted, setHasMounted] = React.useState(false); React.useEffect(() => { setHasMounted(true); }, []); if (!hasMounted) { return null; } const user = getUser();{user ? <AuthenticatedNav /> : <UnauthenticatedNav />}解决二export function UserState() { const { user } = useUser(); const [isUserLoggedIn, setIsUserLoggedIn] = useState(false); useEffect(() => { setIsUserLoggedIn(!!user); }, [user]); return isUserLoggedIn;}import { UserState } from './userProvider'export default function Nav() { UserState() ? <AuthenticatedNav /> : <UnauthenticatedNav />}

March 23, 2023 · 1 min · jiezi

关于ssr:手摸手搭建-VueTSExpress-全栈-SSR-博客系统项目架构和技术选型篇

前言之前接触到了 NuxtJs ,做了一些案例,发现自己还是对整个 SSR 构建流程了解的没有很透彻,决定本人入手试试从0开始搭建 Vue SSR,并且因为苦于想重构本人的博客我的项目,也借此机会将 最新的SSR技术 使用到理论我的项目中。 前端技术选型前端技术选型的核心在于 对 SSR 的反对水平,如果一个第三方库编写时没有思考到通用性,那么要将它集成到一个 SSR 利用中可能会很辣手,很有可能会呈现在node中调用浏览器API的状况。 开发框架(Vue3) 选用 Vue3 作为次要开发框架,能够应用 hooks 写法抽离局部逻辑,使代码构造更加清晰。 预处理器(Stylus) 和平时应用的 SCSS 预处理器比照, Stylus 在 CSS 代码书写上比前者简洁了不少,而且在遵循书写标准的根底上容纳度也很高,甚至能够省去不必要的冒号和分号等。 开发语言(TypeScript) 应用 TypeScript 进行类型束缚,缩小未知谬误产生概率,大胆批改逻辑内容。 网络(Axios) 选用 Axios 的起因是其成熟的双端运行能力,这为我的项目中的 SSR 带来了劣势。 UI框架(Element-plus) 选用适配 Vue3 的成熟 UI 库 Element-plus,其对 SSR 也有高度反对。 路由库(Vue-Router) 搭配 Vue3 的 Vue-Router4 ,同样也反对 SSR。 状态存储库(Pinia) 选用 Pinia ,第1点是其与 Vue3 适配,其给到了 TypeScript 智能补全的性能,且比 Vuex 更轻量更简洁(去除了 mutation ),反对 hook 写法,第2点是反对 SSR ,并且官网文档有很好的服务端反对。 ...

January 9, 2023 · 1 min · jiezi

关于ssr:SSR制作网站你需要知道的知识点

前言在开发SSR网站的时候,我置信大家或多或少会遇到好多问题,然而理解SSR网站的实质之后,这些都不是问题,上面就分享一下我的总结,心愿可能帮忙到大家! 总结以next.js为例1、页面第一次加载或者跳转到某个页面、刷新页面的时候,都会先getServerSideProps办法中拿数据再渲染到客户端浏览器 2、getServerSideProps办法中的数据会全副缓存到客户端浏览器__NEXT_DATA__中,所以其它页面须要这个数据不须要再申请,能够间接拿了。 3、getServerSideProps接口申请传递cookie要从context中拿,申请完之后如果后盾接口有set-cookie又要从ctx.res.setHeader.set-cookit带回客户端浏览器 4、getServerSideProps数据能够注入到客户端浏览器中,然而客户端的数据不能注入到服务端,举个例子:你点击登录之后申请完接口返回用户的信息是客户端获取的数据,这个时候你刷新页面登录信息会失落,为什么?因为客户端数据服务端(刷新页面进入getServerSideProps办法时)获取不到,这也就是为什么开发者要把登录信息如token存储到cookie中,刷新页面也能拿!

August 18, 2022 · 1 min · jiezi

关于ssr:运行在-SSR-模式下的-Angular-应用的内存泄漏问题分析

运行在 SSR 模式下的 Angular 利用,为了防止服务器端和客户端两次调用同样的 API 引起屏幕的 Flickering 问题,通过都会应用 Angular TransferState 服务将信息从服务器发送到客户端,其工作原理如下图所示: 首先在应用程序 app.module.ts 中导入 BrowserTransferStateModule: import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';imports: [ BrowserModule.withServerTransition({appId: 'my-app'}), BrowserTransferStateModule, ...]而后在服务器端模块 app.server.module.ts 中导入 ServerTransferStateModule: import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';imports: [ AppModule, ServerModule, ServerTransferStateModule, ...]咱们能够应用 makeStateKey 函数来创立一个键,以存储状态中的数据(将传递给浏览器)。 应用 this.state.get 从状态中获取数据,并应用 this.state.set 设置状态中的数据。 进行 API 调用时,应用之前调用 makeStateKey 创立的密钥将返回的数据存储在Angular state 中。 采纳了 TransferState 服务的 Spartacus SSR 利用,在呈现内存透露时通常有下列体现: 用户申请响应工夫减少jsapps pods 频繁重启运行时呈现如下日志:(1) SSR rendering exceeded timeout 3000, fallbacking to CSR for /xyz(2) PM2 Process 0 restarted because it exceeds --max-memory-restart value (current_memory=4009730048 max_memory_limit=3865051136 [octets])(3) Rendering of /xyz was not able to complete. This might cause memory leaks! ...

April 29, 2022 · 1 min · jiezi

关于ssr:SSR-和前端编译在这点上是一样的

当初咱们都是通过组件的形式来开发前端页面,在浏览器外面,组件渲染时会通过 dom api 对 dom 做增删改来显示相应的内容。但在服务端并没有 dom api,咱们能够把组件渲染成 html 字符串,而后下发到浏览器渲染,因为曾经有了 html 了,就能够间接渲染成 dom,不再须要执行 JS,所以很快。 第一种浏览器渲染的形式叫做 CSR (client side render),第二种服务端渲染的形式叫做 SSR(server side render)。 很显著,SSR 渲染出画面的速度会很快,因为不须要执行 JS ,而是间接解析 html。因而,app 里嵌的页面根本都用 SSR,这样体验会更好。而且低端机执行 JS 是可能很慢的,要是 CSR,那页面可能会有很长一段白屏工夫。 此外,SSR 是间接返回了 html,这样搜索引擎的爬虫就能从中抓取到具体的内容,就会给更高的搜寻权重,也就是更有利于 SEO (search engine optimize)。 在 app 里嵌的页面、搜索引擎排名优化这两种场景下,咱们都要做 SSR。 晓得了 SSR 是什么和为什么要做 SSR,那如何实现 SSR 呢? SSR 实现原理咱们晓得 vue 是通过 template 形容页面构造,而 react 是通过 jsx,但不论是 template 还是 jsx,编译后都会产生 render function,而后执行产生 vdom。 vdom 在浏览器里会通过 dom api 增删改 dom 来实现 CSR,在服务端会通过拼接字符串来实现 SSR。 ...

February 26, 2022 · 2 min · jiezi

关于ssr:CSR和SSR更新中

前言当初的web网站都是十分考究用户体验,个别都会采纳服务端渲染加客户端渲染一起实现性能。服务端渲染有利于搜索引擎优化(SEO),利于被网页爬虫抓取数据,多见于电商网站商品信息获取等。客户端渲染不利于搜索引擎优化,网页数据异步获取,首页加载工夫长,用户体验绝对较好,罕用于不须要对SEO敌对的中央 内容1.服务端渲染(SSR)简略了解就是浏览器发送申请后,服务器把客户端网页和数据在后盾渲染解析,之后把渲染后的后果返回客户端。服务器端渲染的页面交互能力无限,如果要实现简单交互,还是要通过引入 JavaScript 文件来辅助实现。 客户端拿到的是渲染后的后果,能够间接展现。服务器端渲染的页面在网络中传输的时候,传输的是一个实在的页面。因而,爬虫客户端当爬到咱们的页面后,会分系咱们给他提供的这个页面,此时,咱们页面中的要害数据就会被爬虫给收录了。服务端渲染能够解决首页白屏工夫过久,然而也容易导致服务器压力大,因而,能够应用服务器端的页面缓存技术,加重服务器的渲染压力。 流程让 React 代码在服务器端先执行一次,使得用户下载的 HTML 曾经蕴含了所有的页面展现内容,这样,页面展现的过程只须要经验一个 HTTP 申请周期,TTFP 工夫失去一倍以上的缩减。同时,因为 HTML 中曾经蕴含了网页的所有内容,所以网页的 SEO 成果也会变的十分好。之后,咱们让 React 代码在客户端再次执行,为 HTML 网页中的内容增加数据及事件的绑定,页面就具备了 React 的各种交互能力。 实现原理下面咱们说过,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,实现页面挂载。 SSR框架图剖析 ...

September 15, 2021 · 2 min · jiezi

关于ssr:SSR-技术概述

前言服务端渲染的概念这几年能够说是炒得炽热,它不是一种新型的技术,而是互联网最开始时所应用的加载技术。 那么到底是什么起因,使得人们违心拭去历史的尘埃,让服务端渲染这一古老的概念从新绽开光辉呢? 什么是服务端渲染?服务端渲染简称 SSR,全称是 Server Side Render,是指一种传统的渲染形式,就是在浏览器申请页面URL的时候,服务端将咱们须要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不须要通过 JavaScript 脚本的执行,即可间接构建出心愿的 DOM 树并展现到页面中。 SSR 有两种模式,单页面和非单页面模式,第一种是后端首次渲染的单页面利用,第二种是齐全应用后端路由的后端模版渲染模式。他们区别在于应用后端路由的水平。 与之绝对的是 CSR(Client Side Render),是一种目前风行的渲染形式,它依赖的是运行在客户端的JS,用户首次发送申请只能失去小局部的指引性HTML代码。第二次申请将会申请更多蕴含HTML字符串的JS文件。 为什么须要 SSR ?目前前端风行的框架大都是实用于构建 SPA(单页面应用程序),在SPA这个模型中,是通过动静地重写页面的局部与用户交互,而防止了过多的数据交换,响应速度天然绝对更高。 然而,SPA利用的首屏关上速度个别都很慢,因为用户首次加载须要先下载SPA框架及应用程序的代码,而后再渲染页面,并且 SPA 利用不利于 SEO 优化。 这时候,人们想着是不是能够将利用首页先加载进去,而后让首页用不到的其余 JS 文件再缓缓加载。然而因为 JS 引擎是单线程的,数据的组装过程会受到阻塞,单靠浏览器端的话不容易实现。 SSR 从新焕发生机的契机就在于此,如果将组装数据、渲染 HTML 页面的过程放在服务端,而浏览器端只负责显示接管到的 HTML 文件,那首屏的关上速度无疑会快很多。 SSR 的优缺点那么,SSR 技术到底有哪些长处呢?咱们来列举一下: 更快的响应工夫,绝对于客户端渲染,服务端渲染在浏览器申请URL之后曾经失去了一个带有数据的HTML文本,浏览器只须要解析HTML,间接构建DOM树就能够。有利于 SEO ,能够将 SEO 的要害信息间接在后盾就渲染成 HTML,而保障搜索引擎的爬虫都能爬取到要害数据,而后在他人应用搜索引擎搜寻相干的内容时,你的网页排行能靠得更前,这样你的流量就有越高。以上是 SSR 技术最次要的两大长处,虽有劣势,但毛病也不容忽视: 绝对于仅仅须要提供动态文件的服务器,SSR中应用的渲染程序天然会占用更多的CPU和内存资源。一些罕用的浏览器API可能无奈失常应用,比方window、docment和alert等,如果应用的话须要对运行的环境加以判断。开发调试会有一些麻烦,因为波及了浏览器及服务器,对于SPA的一些组件的生命周期的治理会变得复杂。可能会因为某些因素导致服务器端渲染的后果与浏览器端的后果不统一。总结以上就是对 SSR 技术的一些简要介绍,总结一下就是: SSR 进步 SPA 利用的首屏响应速度,有利于 SEO 优化。SSR 最实用于动态展现页面,如果页面动态数据较多时须要审慎应用。是否应用 SSR、应用到什么水平都须要开发者认真衡量。~ ~本文完,感激浏览! ~ 学习乏味的常识,结识乏味的敌人,塑造乏味的灵魂! 大家好,我是〖编程三昧〗的作者 隐逸王,我的公众号是『编程三昧』,欢送关注,心愿大家多多指教! 你来,怀揣冀望,我有墨香相迎! 你归,无论得失,唯以余韵相赠! ...

August 30, 2021 · 1 min · jiezi

关于ssr:vuessr-手写服务端渲染集成路由vuex

1. 介绍构建过程如上图所示,应用webpack利用咱们配置不同的入口生成服务端和客户端的bundle,服务端的bundle是用来生成html字符串,客户端bundle是用来注入到服务端生成的html字符串中的,因为服务端返回的是字符串,一系列的事件须要依赖客户端打包的js代码(客户端的js + 服务端渲染的字符串)由浏览器渲染这样就实现了一个ssr的构建 长处1.利于seo优化在浏览器渲染的时候当咱们查看源代码只能看到一个<div id='app'></div> 内容是由js生成,这样不利于爬虫所爬取到,服务端渲染是将解析过程放到了服务端来做,服务端将解析好的字符串传给前端,当查看源代码时就会显示解析后的元素,爬虫更容易被检索 2.解决首页白屏的成果如果数据量比拟大那么浏览器会卡顿处于白屏状态,应用服务端渲染间接将解析好的HTML字符串传递给浏览器,大大放慢了首屏加载工夫 毛病1.占用内存所有的渲染逻辑都在服务端进行的,那么会占用更多的CPU和内存资源,当申请过多时不停的解析页面返回给客户端,会导致卡顿成果 2.浏览器Api不能应用因为页面在服务端渲染那么服务端是不能调用浏览器的api的 3.生命周期因为服务器端不晓得什么时候挂载实现,在vue中只反对beforeCreated和created两个生命周期 2. 开发前配置1.装置依赖包cnpm i webpack webpack-cli webpack-dev-server koa koa-router koa-static vue vue-router vuex vue-server-renderer vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge url-loader2.意识目录 3.根底代码App.vue<template> <!-- id="app" 客户端激活,服务端解析成字符串返回给客户端,使其变为由 Vue 治理的动静 DOM 的过程 --> <div id="app"> <Bar></Bar> <Foo></Foo> </div></template><script>import Bar from "./components/Bar";import Foo from "./components/Foo";export default { components: { Bar, Foo, },};</script>Bar.vue<template> <div id="bar"> Bar </div></template><style scoped>#bar { background: red;}</style>Foo.vue<template> <div> Foo <button @click="clickMe">点击</button> </div></template><script>export default { methods: { clickMe() { alert("点我"); }, },};</script>public/server.html<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <!--vue-ssr-outlet--> </body></html>main.jsNode.js 服务器是一个长期运行的过程。当咱们的代码进入该过程时,它将进行一次取值并留存在内存中。这意味着如果创立一个单例对象,它将在每个传入的申请之间共享。所以咱们须要保障每次拜访都会产生一个新的Vue实例,裸露一个函数每次调用都保障是新的根实例 ...

August 26, 2021 · 4 min · jiezi

关于ssr:使用Chrome开发者工具调试-Server–Side-Rendered-SAP-Spartacus-Storefront

In SAP Spartacus document there is a page for "How to Debug a Server–Side Rendered Storefront" using Visual Studio Code. https://sap.github.io/spartac... This document just introduces another way to debug, using Chrome Dev Tools instead of Visual Studio Code.The steps are written based on Spartacus library with version 3.4.1. (1) create a Storefront using Spartacus library and enable it with SSR support using Schematics,by following this document: https://sap.github.io/spartac... (2) By default a script build:ssr is generated to build Storefront and launch it in SSR mode. ...

August 7, 2021 · 2 min · jiezi

关于ssr:SSR笔记

搭建本人的SSR、动态站点生成(SSG)及封装 Vue.js 组件库搭建本人的SSR一、渲染一个Vue实例 mkdir vue-ssrcd vue-ssrnpm init -ynpm i vue vue-server-renderderserver.jsconst Vue = require('vue')const renderer = require('vue-server-renderer').createRenderer()const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: '拉钩教育' }})renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html)})node server.js,运行后果:<div id="app" data-server-rendered="true"><h1>拉钩教育</h1></div>data-server-rendered="true"这个属性是为了未来客户端渲染激活接管的接口二、联合到Web服务器中 server.jsconst Vue = require('vue')const express = require('express')const renderer = require('vue-server-renderer').createRenderer()const server = express()server.get('/', (req, res) => { const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: '拉钩教育' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码 res.end(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> ${html} </body> </html> `) })})server.listen(3000, () => { console.log('server running at port 3000...')})三、应用HTML模板 ...

July 13, 2021 · 10 min · jiezi

关于ssr:新一代Web技术栈的演进SSRSSGISRDPR都在做什么

在之前的一篇文章里,初步介绍了 Jamstack 这套建站技术栈的背景以及各方面优劣势。 所以这次这篇文章会更加深刻,聊聊这套技术栈的演进以及业界的一些最佳实际。 在开始浏览之前,先解释一下文章里用到的英文缩写: CSR:Client Side Rendering,客户端(通常是浏览器)渲染;SSR:Server Side Rendering,服务端渲染;SSG:Static Site Generation,动态网站生成;ISR:Incremental Site Rendering,增量式的网站渲染;DPR:Distributed Persistent Rendering,分布式的继续渲染。从 SSR 到 SSGSSR 这套技术栈置信很多人应该都十分相熟了(如果你不相熟的话能够先浏览相干文章),React/Vue/Angular 等等都从框架层面间接提供了反对,例如在 React 中你能够这样应用: import React from 'react'import ReactDOMServer from 'react-dom/server'const html = ReactDOMServer.render(<h1>Hello, world!</h1>)SSR 最早是为了解决单页利用(SPA)产生的 SEO、首屏渲染工夫等问题而诞生的,在服务端间接实时同构渲染用户看到的页面,能最大水平上进步用户的体验,流程相似上面: 但 SSR 引入了另一个问题,既然要做服务端渲染,就必然须要一个实时在线的后盾服务(通常是基于 Node.js 的服务)用来承载页面申请,那么: 须要服务器的计算资源和公网流量来部署这套服务,并且耗费的资源与页面的访问量成正相干,当页面的访问量突增时,渲染服务也须要进行扩容;服务端只能部署在无限的几个地区,对于间隔服务端较远的用户而言,加载速度跟动态资源的 CDN 相比,慢了一个数量级(通常是 1-5ms VS 50-100+ms);日常也存在传统服务端同样的运维、监控告警等方面的累赘,团队须要额定的人力来开发和保护。有没有方法解决这些问题呢? 咱们从新对 SSR 进行扫视,服务端渲染出的页面,逻辑上讲能够分成上面两大块: 变动不频繁,甚至不会变动的内容:例如文章、排行榜、商品信息、举荐列表等等,这些数据非常适合缓存;变动比拟频繁,或者千人千面的内容:例如用户头像、Timeline、登录状态、实时评论等。例如,在一篇文章的页面中,文章的主题内容是偏差于动态的,很少有改变,那么每次用户的页面申请,都通过服务端来渲染就变得十分不值得,因为每次服务端渲染进去大部分内容都是一样的! 咱们齐全能够将文章的页面渲染为动态页面,至于页面内那些动静的内容(用户头像、评论框等),就通过 HTTP API 的模式进行浏览器端渲染(CSR): 这样做有很多益处: 因为文章内容曾经被动态化了,所以它是 SEO 敌对的,能被搜索引擎轻松爬取;大大加重了服务端渲染的资源累赘,不须要额定做一套 Node.js 服务;用户始终通过 CDN 加载页面核心内容,CDN 的边缘节点有缓存,速度极快;通过 HTTP API + CSR,页面内主要的动静内容也能够被很好地渲染;数据有变动时,从新触发一次网站的异步渲染,而后推送新的内容到 CDN 即可。因为每次都是全站渲染,所以网站的版本能够很好的与 Git 的版本对应上,甚至能够做到原子化公布和回滚。这便是 Gatsby.js、Next.js 这样的网站生成器解决的问题,他们属于 React/Vue 更上一层的框架(Meta Framework),通过 SSR 把动态化的 Web 利用渲染为多个动态页面,并且对高度动静的内容也保留了 CSR 的能力。 ...

April 30, 2021 · 2 min · jiezi

关于ssr:React-ssr框架Nextjs-的生产实践

首先感叹一句,next 真是一个版本狂人啊,一周就一个下版本1. 依照官网的形式初始化一个Next.js我的项目遇到的一些问题默认初始化的框架款式反对sass,组件级别的[name].module.css,但默认不反对less,官网提供了配置less的办法 `yarn add @zeit/next-less less`// next.config.jsconst withLess = require('@zeit/next-less')module.exports = withLess({ webpack(config, options) { config.resolve = { alias: { ...config.resolve.alias, // 配置alias同时要在tsconfig.json 配置 baseUrl,paths "@": path.resolve(__dirname, ".") }, extensions: [".ts", ".tsx", ".js", ".jsx", ".less", ".css"] }; return config }})重新启动后会报 Error: Cannot find module 'webpack' 于是就装置webpack但默认装置的是webpack@5.x,依照这个 issue,要替换成 webpack@4.x 才能够 Next.js 打包后动态资源门路中的 _next 目录哪来的 ?参考此文 打包配置 配置assetPrefix,相当于webpack外面的publicPath(将动态资源放在cdn) 但打包后html引入的动态资源地址会多一层_next目录,解决方案: 本人代码里创立 _next 目录太过繁琐,且耗费性能,故让运维在拷贝动态资源时,在oss目录上多加一层 _next 目录ssr框架 ajax 申请库的抉择node-fetch、window, document的应用注意事项getInitialProps getStaticProps getStaticPaths getServerSideProps 的区别clint Link 跳转时子页面款式文件获取不到(如同只有开发阶段有次问题,临时未解决)当进行重构我的项目时,如果不想扭转老我的项目的url(可能因为url曾经被百度seo收录),能够利用自定义node服务器,将老url转发到新的url,参考文档,然而有个问题这种被转发的url为什么浏览器申请会是 404,但页面还是会失常显示,不晓得是为什么???2. 生产部署接入公司cicd标准,部署没有应用传统的pm2,而是把服务放在一个容器内,服务重启靠node提供一个健康检查接口,容器来保障服务稳固,当该接口没有失常返回docker容器就会重启利用因为公司标准启动node服务都是在容器内用 node app.js,但next默认启动是靠next start启动生产服务的。解决方案是:next提供了自定义服务器,我这里采纳了koa,将next作为一个中间件来解决参考 ...

March 21, 2021 · 1 min · jiezi

关于ssr:vivo-商城架构升级SSR-实战篇

一、前言在后面几篇文章中,置信大家对vivo官网商城的前端架构演变有了肯定的理解,从稳步推动前后端拆散到小程序多端摸索实际,团队不断创新尝试。 在本文中,咱们来分享一下vivo官网商城在Node 服务端渲染(Server Side Rendering, SSR)方面的实战经验。本文次要围绕以下几个方面进行论述: CSR与SSR的比照性能优化自动化部署容灾、降级日志、监控二、背景vivo官网商城目前前后端拆散采纳的是SPA单页模式,SPA会把所有 JS 整体打包,无奈漠视的问题就是文件太大,导致渲染前期待很长时间。特地是网速差的时候,让用户期待白屏完结并非一个很好的体验。因而 vivo 官网商城前端团队尝试引入了SSR技术,以此来放慢页面首屏的访问速度,从而晋升用户体验。 三、SSR简介3.1 什么是SSR?页面渲染次要分为客户端渲染(Client Side Render)和服务端渲染(Server Side Rendering): 客户端渲染(CSR)服务端只返回一个根本的html模板,浏览器依据html内容去加载js,获取数据,渲染出页面内容; 服务端渲染(SSR)页面的内容是在服务端渲染实现,返回到浏览器间接展现。 3.2 为什么要应用SSR?与传统 SPA (单页应用程序 (Single-Page Application)) 相比,SSR的劣势次要在于: 更好的搜索引擎优化(SEO),SPA应用程序初始展现loading菊花图,而后通过Ajax获取内容,搜索引擎并不会期待异步实现后再行抓取页面内容;更快的内容达到工夫 (time-to-content),特地是对于迟缓的网络状况或运行迟缓的设施,无需期待所有的JavaScript都实现下载并执行,才显示服务器渲染的标记,用户可能更疾速地看到残缺渲染的页面,晋升用户体验。下图可能更直观的反馈加载时成果。CSR和SSR页面渲染比照: 四、SSR 实际vivo官网商城我的项目的技术栈是Vue, 思考到从头搭建一套服务端渲染的利用比较复杂,所以抉择了Vue官网举荐的Nuxt.js框架,这是基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 利用提供了极其便当的开发体验。 这里不做根底应用的分享,有趣味的同学能够到Nuxt.js官网学习根底用法;咱们次要聚焦于在整个实际过程中,次要遇到的一些挑战: 性能:如何进行性能优化,晋升QPS,节约服务器资源?容灾:如何做好容灾解决,实现主动降级?日志:如何接入日志,不便问题定位?监控:如何对Node服务进行监控?部署:如何买通公司CI/CD流程,实现自动化部署?4.1 性能优化尽管Vue SSR渲染速度曾经很快,然而因为创立组件实例和虚构DOM节点的开销,与基于字符串拼接的模板引擎的性能相差很大,在高并发状况下,服务器响应会变慢,极大的影响用户体验,因而必须进行性能优化。 4.1.1 计划1 启用缓存a、页面缓存: 在创立render实例时利用LRU-Cache来缓存渲染好的html,当再有申请拜访该页面时,间接将缓存中的html字符串返回。 nuxt.config.js减少配置: serverMiddleware: ["~/serverMiddleware/pageCache.js"]根目录创立serverMiddleware/pageCache.js b、组件缓存: 将渲染后的组件DOM存入缓存,定时刷新,有效期内取缓存中DOM。次要实用于重复使用的组件,多用于列表,例如商品列表。 配置文件nuxt.config.js: const LRU = require('lru-cache')module.exports = { render: { bundleRenderer: { cache: LRU({ max: 1000, // 最大的缓存个数 maxAge: 1000 * 60 * 5 // 缓存5分钟 }) } }}缓存组件减少name及serverCacheKey作为惟一键值: ...

December 22, 2020 · 2 min · jiezi

关于ssr:4图看懂React-SSR中的hydrate

React CSR:水车模型当初在了解 React CSR 时做过一个比喻,把单向数据流比作瀑布模型: 瀑布模型:由props(水管)和state(水源)把组件组织起来,组件间数据流向相似于瀑布。数据流向总是从先人到子孙(从根到叶子),不会顺流(摘自深刻 React) 单组件的宏观视角下,咱们把props了解为水管(数据通道),接管内部传递进来的数据(水),每一份state都是一处水源(设想泉眼冒水,即产生数据的中央),将这棵通过props管道连贯而成的组件建立起来,就造成了自上而下的水流(瀑布): 设想上图整面瀑布墙上有有数的泉眼,state值顺着props管道流淌 从更巨大的视角来看,组件树就像是一系列竹管连接起来的水车,数据是水源(state、props、context以及内部数据源),水自上而下地流经整个组件树达到叶子组件,渲染出丑陋的视图 先通过一张图来感触竹管输水: 再感触水源以及水车整体的运行: 左侧的小桶就是内部数据源,随时舀起一瓢灌到某个组件(竹管)中,让其外部的state(储水)发生变化,变动的水流通过整个子树达到叶子组件,渲染出变动后的视图,这就是交互操作导致数据变动时的组件更新过程 React SSR:三体人模型CSR 模式下,咱们把水了解为数据,同样实用于 SSR,只是过程稍简单些: 服务端渲染:在服务端注入数据,构建出组件树序列化成 HTML:脱水成人干客户端渲染:达到客户端后泡水,激活水流,变回活人类比三体人的生存模式,乱纪元来长期先脱水成人干(SSR 中的服务端渲染局部),恒纪元到来后再泡水复活(SSR 中的客户端 hydrate 局部) 喝水(render)首先要有水可脱,所以先要拉取数据(水),在服务端实现组件首次渲染(mount)的过程: 也就是依据内部数据构建出初始组件树,过程中仅执行render及之前的几个生命周期,是为了尽可能缩短保命招数的前摇,尽快脱水 脱水(dehydrate)接着对组件树进行脱水,使其在顽劣的环境同样可能以一种更简略的状态“生存”下来,比方禁用了 JavaScript 的客户端环境 比组件树更简略的状态是 HTML 片段,脱去生命的水气(动态数据),成为风干标本一样的动态快照: 内存里的组件树被序列化成了动态的 HTML 片段,还能看进去人样(初始视图),不过曾经无奈与之交互了,但这种便携的状态尤其适宜运输,可能通过网络传输到地球上的某个客户端 注水(hydrate)到达客户端后,如果环境合适(没有禁用 JavaScript),就立刻开始“浸泡”(hydrate),组件随之复苏 客户端“浸泡”的过程实际上是从新创立了组件树,将新生的水(state、props、context等)注入其中,并将鲜活的组件树塞进服务端渲染的干瘪躯壳里,使之复活: 注水复活其实比三体人浸泡复苏更弱小一些,可能修复肢体性的伤害(缺失的 HTML 构造会从新创立),但并不纠正口歪眼斜之类的小毛病(疏忽属性多了少了、属性值对不上之类的问题,具体见React SSR 之原理篇) P.S.浸泡也须要肯定工夫,所以在 SSR 模式下,客户端有一段时间是无奈失常交互的,注水实现之后能力彻底复活(单向数据流和交互行为都恢复正常) 参考资料三体 I:地球往事三体 II:光明森林有所得、有所惑,真好关注「前端向后」微信公众号,你将播种一系列「用心原创」的高质量技术文章,主题包含但不限于前端、Node.js以及服务端技术 本文首发于 ayqy.net ,原文链接:http://www.ayqy.net/blog/ssr-...

December 1, 2020 · 1 min · jiezi

关于ssr:双十一SSR优化实践秒开率提升新高度

前言会场是每年双十一的配角之一,会场的用户体验天然也是每年最关注的点。在日趋简单的业务需要下,如何保障咱们的用户体验不劣化甚至能更优化是永恒的命题。 往年(2020)咱们在不扭转现有架构,不扭转业务的前提下,在会场上应用了 SSR 技术,将秒开率进步到了新的高度(82.6%);也察看到在用户体验失去优化的同时,业务指标如 UV 点击率等也有小幅的增长(视不同业务场景有不同的晋升,最大可达 5%),带来了不错的业务价值。 本文将从服务端、前端两个角度介绍咱们在 SSR 上的计划与教训 前端在解决工程化、业务成果评估上的具体实际与方法论服务端在解决前端模块代码于服务端执行、隔离和性能优化上的具体实际与方法论(更多干货欢送关注【淘系技术】公众号) 页面体验性能的外围指标在注释开始前咱们先介绍一下掂量的相干指标,从多年前雅虎 yslow 定义出了绝对残缺的体验性能评估指标,到起初的谷歌的 Lighthouse 等新工具的呈现,体验性能的评估规范逐步的对立且更加被大家认同。 会场的评估体系基于 Web.Dev 以及其余的一些参考,咱们定义了本人的简化评估体系 TTFB(Time to First Byte): 第一个字节的工夫 - 从点击链接到收到第一个字节内容的工夫 FP(First Paint): 第一次绘制 - 用户第一次看到任何像素内容的工夫 FCP(First Contentful Paint): 第一次内容绘制 - 用户看到第一次无效内容的工夫 FSP(First Screen Paint,首屏可视工夫): 第一屏内容绘制 - 用户看到第一屏内容的工夫 LCP(Largest Contentful Paint): 第一次最大内容绘制 - 用户看到最大内容的工夫 TTI(Time To Interactive): 可交互工夫 - 页面变为可交互的工夫(比方可响应事件等) 大体上来说 FSP 约等于 FCP 或 LCP 会场的现状咱们的会场页面是应用基于低代码计划的页面搭建平台产出的,一个由搭建平台产出的会场页面简略而言由两局部组成:页面框架(layout)和楼层模块。 页面框架有一份独自的构建产物(即页面的 layout html 以及根底公共的 js、css 等 assets 资源)。每个楼层模块也有独自的一份构建产物(模块的 js、css 等 assets 资源,根底公共 js 的依赖版本信息等)。 ...

November 19, 2020 · 3 min · jiezi

关于ssr:精通-React-SSR-之-API-篇

写在后面React 提供的 SSR API 分为两局部,一部分面向服务端(react-dom/server),另一部分仍在客户端执行(react-dom) <img src="http://cdn.ayqy.net/data/home/qxu1001840309/htdocs/cms/wordpress/wp-content/uploads/2020/11/1_VG33xLBOqcpfctgiyh0jtA.png" alt="react ssr" width="625" height="446" class="size-large wp-image-2317" /> 一.ReactDOMServerReactDOMServer相干 API 可能在服务端将 React 组件渲染成动态的(HTML)标签: The ReactDOMServer object enables you to render components to static markup.把组件树渲染成对应 HTML 标签的工作在浏览器环境也能实现,因而,面向服务端的 React DOM API 也分为两类: 能跨 Node.js、浏览器环境运行的 String API:renderToString()、renderToStaticMarkup()只能在 Node.js 环境运行的 Stream API:renderToNodeStream()、renderToStaticNodeStream()renderToStringReactDOMServer.renderToString(element)最根底的 SSR API,输出 React 组件(精确来说是ReactElement),输入 HTML 字符串。之后由客户端 hydrate API 对服务端返回的视图构造附加上交互行为,实现页面渲染: If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers.renderToStaticMarkupReactDOMServer.renderToStaticMarkup(element)与renderToString相似,区别在于 API 设计上,renderToStaticMarkup只用于纯展现(没有事件交互,不须要 hydrate)的场景: ...

November 19, 2020 · 3 min · jiezi

关于ssr:SSR-与当年的-JSPPHP-有什么区别

写在后面SSR(Server-Side Rendering)并不是什么离奇的概念,前后端分层之前很长的一段时间里都是以服务端渲染为主(JSP、PHP),在服务端生成残缺的 HTML 页面(摘自前端渲染模式的摸索) 也就是说,历经 SSR 到 CSR 的大改革之后,现在又从 CSR 登程去摸索 SSR 的可能性……仿佛兜兜转转又回到了终点,在这之间产生了什么?现在的 SSR 与当年的 JSP、PHP 又有什么区别? 一.SSR 大行其道回到论坛、博客、聊天室仍旧炽热的年代,行业最佳实际是基于 JSP、PHP、ASP/ASP.NET 的动静网站 以 PHP 为例: <?php if ( count( $_POST ) ): ?><?php include WTG_INCPATH . '/wechat_item_template.php' ?><div style="..."> <div id="wechat-post" class="wechat-post" style="..."> <div class="item" id="item-list"> <?php $order = 1; foreach ( $_POST['posts'] as $wechat_item_id ) { echo generate_item_list( $wechat_item_id, $order ); $order++; } ?> </div> <?php $order = 1; foreach ( $_POST['posts'] as $wechat_item_id ) { echo generate_item_html( $wechat_item_id, $order ); $order++; } ?> <fieldset style="..."> <section style="..."> <p style="...">如果心中仍有疑难,请查看原文并留下评论噢。(<span style="font-size:0.8em; font-weight:600">特地要紧的问题,能够间接微信分割 ayqywx</span> )</p> </section> </fieldset></div><script> function refineStyle () { var post = document.getElementById('wechat-post'); // ul ol li var uls = post.getElementsByTagName('ul'); for (var i = uls.length - 1; i >= 0; i--) { uls[i].style.cssText = 'padding: 0; margin-left: 1.8em; margin-bottom: 1em; margin-top: -1em; list-style-type: disc;'; uls[i].removeAttribute('class'); }; } document.addEventListener('DOMContentLoaded', function() { refineStyle(); }); </script></div><?php endif ?>(摘自ayqy/wechat_subscribers,一款用来主动生成微信公众平台图文音讯的 WordPress 插件) ...

November 11, 2020 · 2 min · jiezi

关于ssr:2020-SSR落地开花的三大机遇

写在后面上篇SSR 的利与弊列举了 SSR 渲染模式的 6 大难题: 难题 1:如何利用存量 CSR 代码实现同构难题 2:服务的稳定性和性能要求难题 3:配套设施的建设难题 4:钱的问题难题 5:hydration 的性能损耗难题 6:数据申请这些问题是 SSR 始终以来远不如 CSR 利用宽泛的次要起因,但时至今日,Serverless、low-code、4G/5G 网络环境三大时机让 SSR 呈现了新的转折,落地开花正过后 第一大时机:Serverless无服务器计算(serverless computing)将服务器相干的配置管理工作通通交给云供应商去做,以加重用户治理云资源的累赘对云计算用户而言,Serverless 服务可能(主动)弹性伸缩而无需显式预配资源,不仅免去了云资源的管理负担,还可能按应用状况计费,这一特点在很大水平上解决了“难题 4:钱的问题”: 引入 SSR 渲染服务,实际上是在网络结构上加了一层节点,而大流量所过之处,每一层都是钱将组件渲染逻辑从客户端改到服务器执行,势必会减少老本,但无望通过 Serverless 将个中老本降到最低 另一方面,Serverless Computing的要害是 FaaS(Function as a Service),由云函数提供惯例计算能力: 间接运行后端代码,而无需思考服务器等计算资源以及服务的扩展性、稳定性等问题,甚至连日志、监控、报警等配套设施也都开箱即用也就是说,喂给 FaaS 一个 JavaScript 函数,就能上线一个高可用的服务,无需操心如何承载大流量(几万 QPS)、如何保障服务稳固牢靠……听起来有些跨时代是么,实际上,AWS Lambda、阿里云 FC、腾讯云 SCF 都曾经是成熟的商业产品了,甚至可能收费试用 无状态的模板渲染工作尤其适宜用云函数(输出 React/Vue 组件,输入 HTML)来实现,“难题 2:服务的稳定性和性能要求”最要害的后端专业性问题迎刃而解,SSR 面临的技术难题从一个高可用的组件渲染服务放大到了一个 JavaScript 函数中: 与客户端程序相比,服务端程序对稳定性和性能的要求严苛得多,例如: 稳定性:异样解体、死循环(由前端人员自行解决)性能:内存/CPU 资源占用(由 FaaS 基础设施解决)、响应速度(网络传输间隔等都要思考在内)如何应答大流量/高并发,如何辨认故障,如何降级/疾速复原(由 FaaS 基础设施解决),哪些环节须要加缓存,缓存如何更新…… FaaS 基础设施解决了大部分的性能问题和可用性问题,函数内的稳定性问题可通过纯前端伎俩解决,至于剩下的响应速度、缓存/缓存更新问题,则须要引入另一个云计算概念——边缘计算 边缘计算所谓的边缘计算,就是将计算和数据存储散布到离用户更近的(CDN)节点(或者叫边缘服务器,Edge server)上,节俭带宽的同时更快响应用户申请: ...

November 4, 2020 · 2 min · jiezi

Egg-React-SSR-服务端渲染-Webpack-构建流程

1. 本地Egg项目启动 首先执行node index.js 或者 npm run dev 启动 Egg应用在 Egg Agent 里面启动koa服务, 同时在koa服务里面启动Webpack编译服务挂载Webpack内存文件读取方法覆盖本地文件读取的逻辑app.react.render = (name, locals, options) => { const filePath = path.isAbsolute(name) ? name : path.join(app.config.view.root[0], name); const promise = app.webpack.fileSystem.readWebpackMemoryFile(filePath, name); return co(function* () { const code = yield promise; if (!code) { throw new Error(`read webpack memory file[${filePath}] content is empty, please check if the file exists`); } // dynamic execute javascript const wrapper = NativeModule.wrap(code); vm.runInThisContext(wrapper)(exports, require, module, __filename, __dirname); const reactClass = module.exports; if (options && options.markup) { return Promise.resolve(app.react.renderToStaticMarkup(reactClass, locals)); } return Promise.resolve(app.react.renderToString(reactClass, locals)); });};Worker 监听Webpack编译状态, 检测Webpack 编译是否完成, 如果未完成, 显示Webpack 编译Loading, 如果编译完成, 自动打开浏览器Webpack编译完成, Agent 发送消息给Worker,  Worker检测到编译完成, 自动打开浏览器, Egg服务正式可用 ...

November 3, 2019 · 1 min · jiezi

Nuxtjs服务端渲染实践搭建一个blog

关于SSR的简介SSR,即服务端渲染,这其实是旧事重提的一个概念,我们常见的服务端渲染,一般见于后端语言生成的一段前端脚本,如:php后端生成html+jsscript内容传递给浏览器展现,nodejs在后端生成页面模板供浏览器呈现,java生成jsp等等。 Vuejs、Reactjs、AngularJs这些js框架,原本都是开发web单页应用(SPA)的,单页应用的好处就是只需要初次加载完所有静态资源便可在本地运行,此后页面渲染都只在本地发生,只有获取后端数据才需要发起新的请求到后端服务器;且因为单页应用是纯js编写,运行较为流畅,体验也稍好,故而和本地原生应用结合很紧密,有些对页面响应流畅度要求不是特别苛刻的页面,用js写便可,大大降低了app开发成本。 然而单页应用并不支持良好的SEO,因为对于搜索引擎的爬虫而言,抓取的单页应用页面源码基本上没有什么变化,所以会认为这个应用只有一个页面,试想一下,一个博客网站,如果所有文章被搜索引擎认为只有一个页面,那么你辛辛苦苦写的大多数文章都不会被收录在里面的。 SSR首先解决的就是这个问题,让人既能使用Vuejs、Reactjs来进行开发,又能保证有良好的SEO,且技术路线基本都是属于前端开发栈序列,语言语法没有多大变化,而搭载在Nodejs服务器上的服务端渲染又可以有效提高并发性能,一举多得,何乐而不为? ps:当然,目前某些比较先进的搜索引擎爬虫已经支持抓取单页应用页面了,比如谷歌。但并不意味着SSR就没用了,针对于资源安全性要求比较高的场景,搭载在服务器上的SSR有着天然的优势。关于Nuxtjs这里是官方介绍,Nuxtjs是诞生于社区的一套SSR解决方案,是一个比较完备的Vuejs服务端渲染框架,包含了异步数据加载、中间件支持、布局支持等功能。 关于nuxtjs,你必须要掌握以下几点知识: vuejs、vue-router、vuex等nodejs编程webpack构建前端工程babel-loader如果想使用进程管理工具,推荐使用pm2管理nodejs进程,安装方式为:npm install -g pm2搭建一个blog准备好工具推荐下载 这里iview将作为一个插件在nuxtjs项目中使用。 注意几个配置:nux.config.js module.exports = { /* ** Headers of the page */ head: { title: '{{ name }}', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '{{escape description }}' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] }, plugins: [ {src: '~plugins/iview', ssr: true} ], /* ** Customize the progress bar color */ loading: { color: '#3B8070' }, /* ** Build configuration */ build: { /* ** Run ESLint on save */ extend (config, { isDev, isClient }) { if (isDev && isClient) { config.module.rules.push({ enforce: 'pre', test: /\.(js|vue)$/, loader: 'eslint-loader', exclude: /(node_modules)/ }) } } }}plugins文件夹下,加入iview.js ...

July 11, 2019 · 6 min · jiezi

Vuejs-SSR-内容总结

本文只是对Vue.js官方SSR文档和对官方hackernews demo的个人学习总结,说得不够完整的请见谅本文主要对以下几方面内容对Vue.js SSR的内容进行分析总结 SSR出现的原因Vue.js SSR的总体原理SSR当中的数据预取SSR在编写代码时候的限制SSR的webpack构建原理SSR出现的原因单页应用有一个很大的缺点就是SEO问题,搜索引擎目前只能对同步的javascript进行索引,但对于需要异步获取数据的单页应用来说,搜索引擎并不会抓取到它们的内容更快的首屏内容展示速度,单页应用需要等待JS文件加载完成,然后再进行页面渲染,而SSR是将渲染完毕的html传输给客户端Vue.js SSR的总体原理如果用一句话概括Vue.js SSR的运作过程,那就是在服务端将Vue.js实例转换成html字符串传输到客户端,然后进行客户端激活,使网页内容能在Vue实例的控制之下 这一句话包含两步内容 Vue.js实例转换成html字符串客户端激活先来看第一步 Vue.js应用转换成html字符串一个最简单的Vue.js单页应用是这样的: new Vue({ render: h => h('div', '123')}).$mount('#app')这里也包含两步 新建Vue实例挂在到DOM上面在服务端当中我们不进行上面第二步操作,取而代之的是将这个实例直接渲染成字符串,做这个工作的就是我们的vue-server-renderer const renderer = require('vue-server-renderer').createRenderer()const Vue = require('vue')renderer.renderToString(new Vue({ render: h => h('div', 123)})).then(html => { console.log(html)}).catch(err => { console.error(err)})// 输出<div data-server-rendered="true">123</div>到现在一个最简单的vue ssr应用在服务端的工作已经完成了,下面我们转向下一步客户端激活 客户端激活客户端激活跟我们单页应用所做的工作相比,最大的不同点就是它并不会构建DOM元素,只会对现有的DOM元素进行激活,使它们能被Vue实例进行控制,而判断激活的关键就是上面的data-server-rendered属性 至此,最简单的一个SSR应用已经构建完成了,下面是对这个应用的功能进行进一步的补充 SSR当中的数据预取数据预取包含着两个方面,客户端的数据预取和服务端的数据预取 服务端的数据预取我们渲染一个内容完整页面的时候往往需要向服务器请求数据,所以现在服务端的逻辑变成等待数据获取完毕,然后将页面转换成html字符串 其中数据获取有以下几个问题: 获取哪些数据?如何得到获取数据的方法?应在何时预取数据?预取的数据应保存在哪里?预取的数据应该怎么样跟客户端进行同步?问题1:我们的数据用来渲染页面,那么我们就需要组成当前页面的所有组件各自所需要的数据 问题2:每个需要进行服务端数据预取的组件定义一个asyncData方法,此方法用于数据预取 问题3:我们需要先得到当前页面所有需要渲染的组件,然后再进行数据预取 问题4:由于还需要进行数据同步,所以很难将数据保存在组件的私有data上面,放在vuex上面是个普遍的选择 问题5:服务端在返回html字符串的时候,store数据将被序列化以后以window.__INITIAL_STATE__=/* store state */的形式插入到脚本当中被客户端获取,客户端的store使用store.replaceState方法同步state 简单复述一下上面的流程就是:在渲染当前页面的所有组件加载完毕以后,执行这些组件的asyncData方法,这些方法将获取到的数据将由vuex托管,获取数据完毕以后即可将应用渲染成html字符串,vuex store的state将会被序列化以后一并传输到客户端,被客户端进行同步 下面是实现的一些细节: 判断组件加载完毕的方法是vue-router的onReady方法获取当前页面的所有组件为vue-router的getMatchedComponents方法由于源码太长所以没贴出来,具体可以到官网浏览 服务端数据预取的关键点算是总结的差不多了,下面简单说一下客户端的数据预取 客户端数据预取客户端的数据预取方法可分为两种: 等待数据获取完毕后再进行视图切换先进行视图切换然后在进行数据获取两种方法区别在于让用户在什么时候产生等待的感觉,第一种是在页面切换时,而第二种是在页面切换完毕等待内容的出现时 第一种方法的实现使用了vue-router实例的beforeResolve方法,这个方法执行在异步组件加载完毕后,导航被确认之前,当完成数据预取以后router才会进行DOM更新等步骤 第二种方法的实现跟我们一般进行数据获取一致,在beforeMount钩子当中执行 SSR在编写代码时候的限制由于浏览器特定的API将会在服务端报错,如'document'、'window'等,尽量避免使用此类API或者在非服务端运行的声明周期函数中调用如'mounted'等等指令由于能直接操作DOM会受到很大的限制SSR的webpack构建原理以官方的hackernews demo为例,webpack有两个入口entry-client和entry-server分别负责构建客户端和服务端的文件 服务端方面webpack会输出一个名叫vue-ssr-server-bundle的json文件,此文件由官方提供的VueSSRServerPlugin插件所构建而成,是服务端的构建清单,传入createBundleRenderer生成服务端渲染所需要的renderer 客户端方面webpack输出的是由代码分割而成的chunk和公用bundle,与一般单页应用的构建相似,不同的是会生成一个vue-ssr-client-manifest,此文件是客户端方面的构建清单,包含所有chunk的信息,将其传入上面的renderer当中能自动将chunk嵌入到html当中,当然用户也能够取消,自行选择手动嵌入的内容 ...

July 1, 2019 · 1 min · jiezi

安卓手机分享加密网络给其他设备使用包括linux

之前的ss被墙了,舍不得60rmb换IP,于是找了一个其他的加速器,只能用他们的客户端连。毫无意外,他们不出linux的客户端,还好他们出安卓客户端,我手机也是安卓,就寻思用手机分享加密网络出来给电脑用。 用到软件: 安卓端: netShare pro链接: https://pan.baidu.com/s/1QHU6... 提取码: pmpp 复制这段内容后打开百度网盘手机App,操作更方便哦 linux端,任意支持使用http代理的软件,chrome插件的话推荐 SwitchyOmega,其他软件很多也提供代理功能的,如 Android-studio开始 1 . 手机先连上加密网络 2 . 打开 netShare , 点“开始共享” (有一些网上的汉化版本在我手机点这个按钮没反应,我好不容易找到一个能用的),成功后应该如下 3 . 手机可以用数据线连电脑,然后手机选择分享网络,也可以电脑连 netShare 中显示的WiFi,然后打开想用的软件的代理设置,把 netShare 中显示的 IP和端口写进去 下面截图是我SwitchyOmega的配置 4 . 完成了。 真是麻烦的网络啊。

June 27, 2019 · 1 min · jiezi

手把教你搭建SSRvuevuecli-express

最近简单的研究了一下SSR,对SSR已经有了一个简单的认知,主要应用于单页面应用,Nuxt是SSR很不错的框架。也有过调研,简单的用了一下,感觉还是很不错。但是还是想知道若不依赖于框架又应该如果处理SSR,研究一下做个笔记。 什么是SSR把Vue组件渲染为服务器端的HTML字符串,将他们直接发送到浏览器,最后将静态标记混合为客户端上完全交互的应用程序。 为什么要使用SSR更好的SEO,搜索引擎爬虫爬取工具可以直接查看完全渲染的页面更宽的内容达到时间(time-to-content),当权请求页面的时候,服务端渲染完数据之后,把渲染好的页面直接发送给浏览器,并进行渲染。浏览器只需要解析html不需要去解析js。SSR弊端开发条件受限,Vue组件的某些生命周期钩子函数不能使用开发环境基于Node.js会造成服务端更多的负载。在Node.js中渲染完整的应用程序,显然会比仅仅提供静态文件server更加占用CPU资源,因此如果你在预料在高流量下使用,请准备响应的服务负载,并明智的采用缓存策略。准备工作在正式开始之前,在vue官网找到了一张这个图片,图中详细的讲述了vue中对ssr的实现思路。如下图简单的说一下。 下图中很重要的一点就是webpack,在项目过程中会用到webpack的配置,从最左边开始就是我们所写入的源码文件,所有的文件都有一个公共的入口文件app.js,然后就进入了server-entry(服务端入口)和client-entry(客户端入口),两个入口文件都要经过webpack,当访问node端的时候,使用的是服务端渲染,在服务端渲染的时候,会生成一个server-Bender,最后通过server-Bundle可以渲染出HTML页面,若在客户端访问的时候则是使用客户端渲染,通过client-Bundle在以后渲染出HTML页面。so~通过这个图可以很清晰的看出来,接下来会用到两个文件,一个server入口,一个client入口,最后由webpack生成server-Bundle和client-Bundle,最终当去请求页面的时候,node中的server-Bundle会生成HTML界面通过client-Bundle混合到html页面中即可。 对于vue中使用ssr做了一些简单的了解之后,那么就开始我们要做的第一步吧,首先要创建一个项目,创建一个文件夹,名字不重要,但是最好不要使用中文。 mkdir domecd domenpm initnpm init命令用来初始化package.json文件: { "name": "dome", // 项目名称 "version": "1.0.0", // 版本号 "description": "", // 描述 "main": "index.js", // 入口文件 "scripts": { // 命令行执行命令 如:npm run test "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Aaron", // 作者 "license": "ISC" // 许可证}初始化完成之后接下来需要安装,项目所需要依赖的包,所有依赖项如下: npm install express --save-devnpm install vue --save-devnpm install vue-server-render --save-devnpm install vue-router --save-dev如上所有依赖项一一安装即可,安装完成之后就可以进行下一步了。前面说过SSR是服务端预渲染,所以当然要创建一个Node服务来支撑。在dome文件夹下面创建一个index.js文件,并使用express创建一个服务。 ...

June 13, 2019 · 4 min · jiezi

HTML-转-PDF-图文报表实践

导出 PDF 图文报表实践 方法一: jsPDF使用 jsPDF 时,需要注意的是其默认单位为 mm,需要在 new jsPDF() 时传入配置 const doc = new jsPDF({ unit: 'px', format: 'a4'})这个方法废了。这个鬼东西多行文本和多个图片,简直要人命! 方法二: wkhtmltopdf Vue SSR使用 Vue.js ,需要 SSR 支持,否则页面为空白 使用 Nuxt.js 作为服务端渲染框架,这里记录一下遇到的问题和解决方案 1. 使用 Element UI,启动就报错 HTMLElement is not definedElement UI 版本问题,使用最新的 2.8.x 会出现问题,则需要降版本并在 package.json 中配置版本策略,仅更新小版本范围 "element-ui": "~2.4.11",参考资料:https://github.com/ElemeFE/element/issues/15261 2. JS的兼容问题在使用 wkhtmltopdf 时,提示报错:Warning: http://localhost:3000/_nuxt/vendors.app.js:1941 SyntaxError: Parse error,估计是 ES6 的语法在wkhtmltopdf 的运行环境当中不支持,导致出现了这些错误提示。 官方Nuxt.js 2.6.X 版本其实给了 babel 的配置,默认会自动根据浏览器的运行环境做代码兼容,并不需要以下的这些设置(下面只是自己的实践过程,供参考),请直接使用第3点的解决方法。 ...

May 22, 2019 · 2 min · jiezi

Nextjs源码简析服务端渲染过程以及documentapppages这三者调用关系

首先分析一下整体加载逻辑在自定义服务端中通过const app = next()创建实例并使用app.render(req, res)方法进行渲染 所以可以从app.render这个渲染入口开始着手 了解框架逻辑唯一的方式就是看源码,由于源码过于细节,下面我会简化涉及到的代码,仅保留主要逻辑,附带具体地址,有兴趣深入的同学可以看看首先是app.render next-server/server/next-server.ts import { renderToHTML } from './render.tsx'// app.render入口函数this.render(req, res){ const html = await this.renderToHTML(req, res) return this.sendHTML(req, res, html)}this.renderToHTML(req, res){ const html = await this.renderToHTMLWithComponents(req, res) return html}this.renderToHTMLWithComponents(req, res) { // render内的renderToHTML return renderToHTML(req, res)}可以看到上面都是简单的调用关系,虽然删除了大部分代码,但我们只需要知道,最终它调用了render.tsx内的renderToHTML 这是一个相当长的函数,也就是本篇文章的主要内容,通过renderToHTML能够了解到大部分内容,和上面相同,删除了大部分逻辑,仅保留核心代码 // next-server/server/render.tsxfunction renderToHTML(req, res) {// 参考下文#补充 loadGetInitialProps,非常简单的函数,就是调用了_app.getInitialProps// _app.getInitialProps函数内部会先调用pages.Component的getInitialProps// 也就是在这里,我们编写的组件内的getInitialProps同样会被调用,获取部分初始数据 let props = await loadGetInitialProps(App, { Component, router, ctx }); // 定义渲染函数,返回html和head const renderPage = () => { // 参考下文#补充 render return render( renderToStaticMarkup, //渲染_app,以及其内部的pages.Component也就是我们编写的代码,详情参考next/pages/_app.tsx <App Component={EnhancedComponent} router={router} {...props} /> ); }; // _document.getInitialProps会调用renderPage,渲染_app也就是我们的正常开发时编写的组件代码,详情参考next/pages/_app.tsx const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }); // 参考下文#补充 renderDocument let html = renderDocument(Document, { props, docProps, }); return html;}小结req=>render(req, res) renderToHTML(req, res) renderToHTMLWithComponents(req, res) renderToHTML(req,res) _app.initialProps = loadGetInitialProps(App, { Component, router, ctx }) _document.initialProps = loadGetInitialProps(Document, { ...ctx, renderPage }) renderDocument(Document, _app.initialProps, _document.initialProps)<=res对应 ...

May 12, 2019 · 3 min · jiezi

服务端预渲染之Nuxt - (爬坑篇)

Nuxt是解决SEO的比较常用的解决方案,随着Nuxt也有很多坑,每当突破一个小技术点的时候,都有很大的成就感,在这段时间里着实让我痛并快乐着。在这里根据个人学习情况,所踩过的坑做了一个汇总和总结。Nuxt开发跨域项目可以使用Nginx来反向代理,将外来的请求(这里也注意下将Linux的防火墙放行相应端口)转发的内部Nuxt默认的3000端口上,最简单的配置文件如下:nuxtjs.config.js{ modules: [ ‘@nuxtjs/axios’, ‘@nuxtjs/proxy’ ], proxy: [ [ ‘/api’, { target: ‘http://localhost:3001’, // api主机 pathRewrite: { ‘^/api’ : ‘/’ } } ] ]}@nuxtjs/proxy需要手动单独安装。Nuxt Store 使用在Nuxt中使用Vuex跟传统在Vue中使用Vuex还不太一样,首先Nuxt已经集成了Vuex,不需要我们进行二次安装,直接引用就好,在默认Nuxt的框架模板下有一个Store的文件夹,就是我们用来存放Vuex的地方。Nuxt官方也提供了相关文档,可以简单的过一下,但是官方文档我看来比较潦草。根据官方文档在store文件下面创建两个.js文件,分别是index.js和todo.js。并在pages文件夹下面创建index.vue。store - index.jsexport const state = () => ({ counter: 0})export const mutations = { increment (state) { state.counter++ }}store - todo.jsexport const state = () => ({ list: []})export const mutations = { add (state, text) { state.list.push({ text: text, done: false }) }, remove (state, { todo }) { state.list.splice(state.list.indexOf(todo), 1) }, toggle (state, todo) { todo.done = !todo.done }}pages - index.vue<template> <section class=“container”> <div> <h2 @click="$store.commit(‘increment’)">{{counter}}</h2> <ul> <li v-for="(item,index) of list" :key=“index”>{{item.text}}</li> </ul> </div> </section></template><script>import Logo from ‘/components/Logo.vue’import {mapState} from “vuex”;export default { components: { Logo }, computed:{ …mapState([“counter”]), …mapState(“todos”,{ list:state => state.list }) }, created(){ for(let i =0;i<10;i++){ this.$store.commit(“todos/add”,i); } console.log(this.list) }}</script>在Nuxt中可以直接使用this.$store,并且是默认启用命名空间的。再看一下computed中的代码,在使用mapState的时候,counter属性是直接获取出来的,然而todos属性则是通过命名空间才获取到的。这又是怎么回事?Nuxt把store中的index.js文件中所有的state、mutations、actions、getters都作为其公共属性挂载到了,store实例上,然而其他的文件则是使用的是命名空间,其对应的命名空间的名字就是其文件名。运行项目的时候可以在.nuxt文件夹内找到store.js看下是怎么完成的。简单的解释一下代码作用,以及做什么用的。.nuxt - store.js// 引入vueimport Vue from ‘vue’// 引入vueximport Vuex from ‘vuex’// 作为中间件Vue.use(Vuex)// 保存console 函数const log = console// vuex的属性const VUEX_PROPERTIES = [‘state’, ‘getters’, ‘actions’, ‘mutations’]// store属性容器let store = {}// 没有返回值的自执行函数void (function updateModules() { // 初始化根数据,也就是上面所说的index文件做为共有数据 store = normalizeRoot(require(’@/store/index.js’), ‘store/index.js’) // 如果store是函数,提示异常,停止执行 if (typeof store === ‘function’) { // 警告:经典模式的商店是不赞成的,并将删除在Nuxt 3。 return log.warn(‘Classic mode for store is deprecated and will be removed in Nuxt 3.’) } // 执行存储模块 // store - 模块化 store.modules = store.modules || {} // 解决存储模块方法 // 引入todos.js 文件,即数据 // ’todos.js’ 文件名 resolveStoreModules(require(’@/store/todos.js’), ’todos.js’) // 如果环境支持热重载 if (process.client && module.hot) { // 无论何时更新Vuex模块 module.hot.accept([ ‘@/store/index.js’, ‘@/store/todos.js’, ], () => { // 更新的根。模块的最新定义。 updateModules() // 在store中触发热更新。 window.$nuxt.$store.hotUpdate(store) }) }})()// 创建store实例// - 如果 store 是 function 则使用 store// - 否则创建一个新的实例export const createStore = store instanceof Function ? store : () => { // 返回实例 return new Vuex.Store(Object.assign({ strict: (process.env.NODE_ENV !== ‘production’) }, store))}// 解决存储模块方法// moduleData - 导出数据// filename - 文件名function resolveStoreModules(moduleData, filename) { // 获取导出数据,为了解决es6 (export default)导出 moduleData = moduleData.default || moduleData // 远程store src +扩展(./foo/index.js -> foo/index) const namespace = filename.replace(/.(js|mjs|ts)$/, ‘’) // 空间名称 const namespaces = namespace.split(’/’) // 模块名称(state,getters等) let moduleName = namespaces[namespaces.length - 1] // 文件路径 const filePath = store/${filename} // 如果 moduleName === ‘state’ // - 执行 normalizeState - 正常状态 // - 执行 normalizeModule - 标准化模块 moduleData = moduleName === ‘state’ ? normalizeState(moduleData, filePath) : normalizeModule(moduleData, filePath) // 如果是 (state,getters等)执行 if (VUEX_PROPERTIES.includes(moduleName)) { // module名称 const property = moduleName // 存储模块 // 获取存储模块 const storeModule = getStoreModule(store, namespaces, { isProperty: true }) // 合并属性 mergeProperty(storeModule, moduleData, property) // 取消后续代码执行 return } // 特殊处理index.js // 模块名称等于index const isIndexModule = (moduleName === ‘index’) // 如果等于 if (isIndexModule) { // 名称空间弹出最后一个 namespaces.pop() // 获取模块名称 moduleName = namespaces[namespaces.length - 1] } // 获取存储模块 const storeModule = getStoreModule(store, namespaces) // 遍历 VUEX_PROPERTIES for (const property of VUEX_PROPERTIES) { // 合并属性 // storeModule - 存储模块 // moduleData[property] - 存储模块中的某个属性数据 // property - 模块名称 mergeProperty(storeModule, moduleData[property], property) } // 如果moduleData.namespaced === false if (moduleData.namespaced === false) { // 删除命名空间 delete storeModule.namespaced }}// 初始化根数据// moduleData - 导出数据// filePath - 文件路径function normalizeRoot(moduleData, filePath) { // 获取导出数据,为了解决es6 (export default)导出 moduleData = moduleData.default || moduleData // 如果导入的数据中存在commit方法,则抛出异常 // - 应该导出一个返回Vuex实例的方法。 if (moduleData.commit) { throw new Error([nuxt] ${filePath} should export a method that returns a Vuex instance.) } // 如果 moduleData 不是函数,则使用空队形进行合并处理 if (typeof moduleData !== ‘function’) { // 避免键入错误:设置在覆盖顶级键时只有getter的属性 moduleData = Object.assign({}, moduleData) } // 对模块化进行处理后返回 return normalizeModule(moduleData, filePath)}// 正常状态// - 模块数据// - 文件路径function normalizeState(moduleData, filePath) { // 如果 moduleData 不是function if (typeof moduleData !== ‘function’) { // 警告提示 // ${filePath}应该导出一个返回对象的方法 log.warn(${filePath} should export a method that returns an object) // 合并 state const state = Object.assign({}, moduleData) // 以函数形式导出state return () => state } // 对模块化进行处理 return normalizeModule(moduleData, filePath)}// 对模块化进行处理// moduleData - 导出数据// filePath - 文件路径function normalizeModule(moduleData, filePath) { // 如果module数据的state存在并且不是function警告提示 if (moduleData.state && typeof moduleData.state !== ‘function’) { // “state”应该是返回${filePath}中的对象的方法 log.warn('state' should be a method that returns an object in ${filePath}) // 合并state const state = Object.assign({}, moduleData.state) // 覆盖原有state使用函数返回 moduleData = Object.assign({}, moduleData, { state: () => state }) } // 返回初始化数据 return moduleData}// 获取store的Model// - storeModule store数据模型// - namespaces 命名空间名称数组// - 是否使用命名空间 默认值 为falsefunction getStoreModule(storeModule, namespaces, { isProperty = false } = {}) { // 如果 namespaces 不存在,启动命名空间,命名空间名称长度1 if (!namespaces.length || (isProperty && namespaces.length === 1)) { // 返回model return storeModule } // 获取命名空间名称 const namespace = namespaces.shift() // 保存命名空间中的数据 storeModule.modules[namespace] = storeModule.modules[namespace] || {} // 启用命名空间 storeModule.modules[namespace].namespaced = true // 添加命名数据 storeModule.modules[namespace].modules = storeModule.modules[namespace].modules || {} // 递归 return getStoreModule(storeModule.modules[namespace], namespaces, { isProperty })}// 合并属性// storeModule - 存储模块// moduleData - 存储模属性数据// property - 模块名称function mergeProperty(storeModule, moduleData, property) { // 如果 moduleData 不存在推出程序 if (!moduleData) return // 如果 模块名称 是 state if (property === ‘state’) { // 把state数据分到模块空间内 storeModule.state = moduleData || storeModule.state } else { // 其他模块 // 合并到对应的模块空间内 storeModule[property] = Object.assign({}, storeModule[property], moduleData) }}以上就是编译后的store文件,大致的意思就是对store文件进行遍历处理,根据不同的文件使用不同的解决方案,使用命名空间挂载model。页面loadingNuxt有提供加载Loading组件,一下是配置。nuxtjs.config.jsmodule.exports = { loading: { color: ‘#3B8070’ }}Nuxt提供的loading不能满足项目需求,可能有的项目不需要这样加载动画,so,就需要自己手动配置一个。添加一个loading组件 (官方示例如下,详情可看官方文档)引用该组件。nuxtjs.config.jsmodule.exports = { loading: ‘~components/loading.vue’}一个小插曲在Nuxt中,~与@都指向的是根目录。components/loading.vue<template lang=“html”> <div class=“loading-page” v-if=“loading”> <p>Loading…</p> </div></template><script>export default { data: () => ({ loading: false }), methods: { start () { this.loading = true }, finish () { this.loading = false } }}</script>第三方组件库项目开发过程中,难免会用到组件库,与在Vue中使用的时候是太一样的,需要添加一些依赖才能正常使用。plugins - element-ui.jsimport Vue from ‘vue’;import Element from ’element-ui’;import locale from ’element-ui/lib/locale/lang/en’;export default () => { Vue.use(Element, { locale })};nuxtjs.config.jsmodule.exports = { css: [ ’element-ui/lib/theme-chalk/index.css’ ], plugins: [ ‘@/plugins/element-ui’, ‘@/plugins/router’ ]};使用中间件中间件Nuxt没有给出具体的使用文档,而是放入了一个编辑器。这一点我感觉到了一丝丝的 差异。为什么要这样。。。简单的研究了一下,弄明白了大概。在middleware中创建想要的中间件。这里借用一下官网的例子。middleware - visits.jsexport default function ({ store, route, redirect }) { store.commit(‘ADD_VISIT’, route.path)}向上面这样就创建好了一个中间件,但是应该怎么使用呢?在使用的时候有两种方式,一种是全局使用,另一种是在页面中单独使用,文件名会作为其中间件的名称。++全局使用++nuxtjs.config.jsexport default { router: { middleware: [‘visits’] }}页面中单独使用export default { middleware: ‘auth’}官网中在页面中的asyncData中有一段这样的代码。export default { asyncData({ store, route, userAgent }) { return { userAgent } }}持续更新。。。总结Nuxt的学习曲线非常小,就像Vue框架一样,已经是一个开箱即用的状态,我们可以直接跨过配置直接开发。对配置有兴趣的可以在Vue官方文档找到SSR渲染文档。 ...

April 20, 2019 · 5 min · jiezi

Vue SSR 踩坑之旅

前言本文并不是Vue SSR的入门指南,没有一步步介绍Vue SSR入门,如果你想要Vue SSR入门教程,建议阅读Vue官网的《Vue SSR指南》,那应该是最详细的Vue SSR入门教程了。这篇文章的意义是,主要介绍如何在SSR服务端渲染中使用最受欢迎的vue ui 库element-ui组件库和echarts插件,以及本文中介绍的实例克服尤大大给的 HackerNews Demo 需要翻墙才能运行起来的问题,新手在阅读SSR官方文档时,如果遇到疑惑点,可以直接在本文实例的基础上进行相关实验验证,从而解决疑惑。本文实例的 github地址为:https://github.com/fengshi123… (欢迎 star)一、什么是服务端渲染(SSR)?官网给出的解释:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作DOM。然而,也可以将同一个组件渲染为服务端的 HTML字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。即:SSR大致的意思就是vue在客户端将标签渲染成的整个html片段的工作在服务端完成,服务端形成的html片段直接返回给客户端这个过程就叫做服务端渲染。二、服务端渲染的优缺点2.1、服务端渲染的优点:(1)更好的SEO: 因为SPA页面的内容是通过Ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再抓取页面内容,所以在SPA中是抓取不到页面通过Ajax获取到的内容的;而SSR是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;(2)更快的内容到达时间(首屏加载更快): SPA会等待所有vue编译后的js文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR直接由服务端渲染好页面直接返回显示,无需等待下载js文件及再去渲染等,所以SSR有更快的内容到达时间;2.2、服务端渲染的缺点:(1)更多的开发条件限制: 例如服务端渲染只支持beforCreate和created两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序(SPA)不同,服务端渲染应用程序,需要处于Node.js server运行环境;(2)更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。三、vue-ssr demo 介绍本文示例基于尤大大给的 HackerNews Demo 进行改造,去除需要翻墙访问 https://hacker-news.firebasei…的相关api , 然后项目中使用了最受欢迎的vue ui 库element-ui ,并且调研了echarts.js 插件在服务端渲染的可行性;实例的目录结构以及实例的效果图分别如下所示:具体每个文件的相关代码的逻辑在代码中都有进行详细的注释,所以这里就不详细再介绍一遍,可以在github上面 clone demo 进行查看,这里主要看下 Vue官网上的服务端渲染的示意图从图上可以看出,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。四、本实例踩坑汇总4.1、引用 vue 文件会报文件找不到问题:如果引用 vue文件没有加 .vue 后缀,会报文件找不到,即:import adminContent from ‘./views/adminContent’;会报以下错误:解决:引用 vue文件时添加 .vue后缀名,即:import adminContent from ‘./views/adminContent.vue’;4.2、引入 element-ui 的样式时,报错问题: 在项目中引入 element-ui 的样式时,即:import ’element-ui/lib/theme-chalk/index.css’;会报以下的错误,不引入样式文件则不会报错:ReferenceError: window is not defined解决: 需要进行样式文件的解析配置:(1)安装样式解析插件: npm install css-loader –save(2)在webpack.base.config.js 中进行配置css-loader:{ test: /.css$/, loader: [“css-loader”]}4.3、引入 element-ui 的样式时,报错 问题: 在项目中引入 element-ui 的样式时,即:import ’element-ui/lib/theme-chalk/index.css’;会报以下的错误,不引入样式文件则不会报错:Module parse failed: Unexpected character ‘@’ (1:0) You may need an appropriate loader to handle this file type.解决: 需要进行样式文件的解析配置:(1)安装样式解析插件:npm install style-loader –save(2)在webpack.base.config.js 中进行相关配置:{ test: /.css$/, loader: [“vue-style-loader”, “css-loader”]},{ test: /.(eot|svg|ttf|woff|woff2)(?\S*)?$/, loader: ‘file-loader’},4.4、element-ui的组件 el-table 不支持服务端渲染问题: 如果服务端渲染中的页面包含 el-table组件,从服务端返回的页面中的 el-table 组件中数据为空的,原因是 el-table 组件在mount钩子函数中初始化table数据,而服务端渲染时,不支持mount钩子函数。解决:github 上面的 elment-ui 有分支修复了这个问题,https://github.com/ElemeFE/el… ;将该分支的源码进行编译,然后替换在node_modules中替换 element-ui 的 lib编译包即可。4.5、el-table 服务端渲染后,表格宽度不是 100%问题:el-table 服务端渲染后,表格的宽度不是代码中设置的 100%,表格宽度比较小。解决:进行样式额外设置:.el-table__header{ width: 100%;}.el-table__body{ width: 100%;}.el-table-column–selection{ width: 48px;}4.6、echarts 插件怎么支持服务端渲染?解决:使用 node-canvas 插件,具体使用可以查看本实例的写法,也可以查看 node-canvas 在github上面的介绍:https://github.com/Automattic…存在问题:实例中是利用node-canvas 生成对应的图片,然后页面中引用该图片,存在问题:生成的图片没有动效的效果。(这个问题没有继续研究,因为:图片没有文字内容,seo 是不需要的;然后图片在服务端生成,在下载图片在页面中渲染,会直接在客户端渲染更节省资源吗?)五、总结SSR有更好的SEO和更快的内容到达时间的优点,但也存在开发条件限制、服务器资源消耗多、开发上手难等缺点,所以你的项目是否需要服务端渲染,需要你结合你的项目具体进行相关指标的评估,切勿跟风,为 SSR而 SSR。本文实例主要基于尤大大给的 HackerNews Demo 进行改造,去除需要翻墙访问https://hacker-news.firebasei… 的相关api , 然后项目中使用了最受欢迎的vue ui 库element-ui ,并且调研了echarts.js 插件在服务端渲染的可行性,帮助新手更好更快地入门 ssr,如果在阅读官方SSR文档的过程中,有些疑问点,可以自己在本文实例中进行相关的试验验证,然后帮助解决疑惑。如果觉得本文以及github的实例帮助到你,请帮忙给个 star ,本文实例的github地址为:https://github.com/fengshi123… ...

April 17, 2019 · 1 min · jiezi

vue ssr 从认识到构建一个工程项目(一)

vue ssr入门前言近期需要接手一个vue ssr项目,由于本人之前没有写过ssr,只是稍微了解了点。所以跟着官网学了下,并整理出了这篇学习笔记。方便自己以后对vue ssr知识的回顾。好记性不如烂笔头。介绍相信大家在看到这篇文章之前,都知道ssr是什么了。SSR,英文全称叫 Server(服务) side(端) rendering (渲染)哈哈☺那么究竟什么是服务器端渲染?Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。如果你问我为什么使用ssr呢?(具体可参考官网)有利于seo。更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。大体可以理解为渲染出页面时间,csr比ssr多了个js下载时间。因为ssr一开始加载下来就渲染出来了,然后在下载激活html的js。csr是下载完在渲染。正文基本用法ssr主要依靠两个包vue-server-renderer和 vue(两个版本必须匹配)安装: npm install vue vue-server-renderer –save入门配置ssr最简易配置// server.jsconst server = require(’express’)()const Vue = require(‘vue’);const renderer = require(‘vue-server-renderer’).createRenderer();server.get(’’, (req, res) => { const context = { url: req.url } const app = new Vue({ template: &lt;div&gt;${context.url}&lt;/div&gt; }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end(‘Internal Server Error’) return } res.end( &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt;&lt;title&gt;Hello&lt;/title&gt;&lt;/head&gt; &lt;body&gt;${html}&lt;/body&gt; &lt;/html&gt; ) })})server.listen(8080)node server.js 浏览器输入localhost:8080访问该ssr页面这时候你可以看到,无论你输入什么路径,页面文本都会显示出你的路径ssr使用模板当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。纯客户端渲染的时候,会有一个模板,会插入你打包后的一些文件等。那么ssr会不会也有这种模板呢?当然会有。首先在根目录下新建一个index.template.html文件<!DOCTYPE html><html lang=“en”> <head><title>Hello</title></head> <body> <!–vue-ssr-outlet–> </body></html>注意了 –跟vue或者outlet跟–之间不能用空格。注释 – 这里将是应用程序 HTML 标记注入的地方。接下来,修改下刚才的server.js文件后如下const server = require(’express’)()const Vue = require(‘vue’);const renderer = require(‘vue-server-renderer’).createRenderer({ template: require(‘fs’).readFileSync(’./index.template.html’, ‘utf-8’)});server.get(’’, (req, res) => { const context = { url: req.url } const app = new Vue({ template: &lt;div&gt;${context.url}&lt;/div&gt; }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end(‘Internal Server Error’) return } res.end(html) })})server.listen(8080)就是在createRenderer中多加一个参数 template(读取模板文件),并传递给createRenderer方法模板还支持插值<html> <head> <!– 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) –> <title>{{ title }}</title> <!– 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) –> {{{ meta }}} </head> <body> <!–vue-ssr-outlet–> </body></html>我们可以通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供插值数据:const context = { title: ‘hello’, meta: &lt;meta ...&gt; &lt;meta ...&gt; }renderer.renderToString(app, context, (err, html) => { // 页面 title 将会是 “Hello” // meta 标签也会注入})编写通用代码我们以往的纯浏览器渲染都是把js下载到本地执行的。上述代码你会发现都是用的同一个Vue构造函数,但是想对该构造函数做特殊处理时,就会对其他用户造成污染。因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:// 修改原先代码如下-const Vue = require(‘vue’);+const createApp = require(’./app.js’)- const app = new Vue({- template: &lt;div&gt;${context.url}&lt;/div&gt;- })+ const { app } = createApp(context)// 新增app.jsconst Vue = require(‘vue’);module.exports = function createApp(context) { const app = new Vue({ template: &lt;div&gt;${context.url}&lt;/div&gt; }) return { app }}这样,每次访问该服务器的时候,都会生成一个新的vue实例。同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 createApp 中创建一个新的实例,并从根 Vue 实例注入。参考Vue SSR 指南vue 服务端渲染ssr带你五步学会Vue SSR ...

April 11, 2019 · 2 min · jiezi

服务端预渲染之Nuxt(介绍篇)

现在前端开发一般都是前后端分离,mvvm和mvc的开发框架,如Angular、React和Vue等,虽然写框架能够使我们快速的完成开发,但是由于前后台分离,给项目SEO带来很大的不便,搜索引擎在检索的时候是在网页中爬取数据,由于单页面应用读取到的页面是几乎空白的,无法爬取到任何数据信息。<!DOCTYPE html><html> <head> <meta charset=utf-8> <meta name=viewport content=“width=device-width,initial-scale=1”> <title>authorization_web</title> </head><body> <div id=app></div></body></html>如上代码,单页面应用查看源代码的时候如上所示,所以搜索引擎无法爬取到任何信息,搜索引擎会认为当前页面为一个空页面。为了解决SEO问题,推出了SSR服务端预渲染,以便提高对SEO优化。什么是SSR在认识SSR之前,首先对CSR与SSR之间做个对比。首先看一下传统的web开发,传统的web开发是,客户端向服务端发送请求,服务端查询数据库,拼接HTML字符串(模板),通过一系列的数据处理之后,把整理好的HTML返回给客户端,浏览器相当于打开了一个页面。这种比如我们经常听说过的jsp,PHP,aspx也就是传统的MVC的开发。SPA应用,到了Vue、React,单页面应用优秀的用户体验,逐渐成为了主流,页面整体式javaScript渲染出来的,称之为客户端渲染CSR。SPA渲染过程。由客户端访问URL发送请求到服务端,返回HTML结构(但是SPA的返回的HTML结构是非常的小的,只有一个基本的结构,如第一段代码所示)。客户端接收到返回结果之后,在客户端开始渲染HTML,渲染时执行对应javaScript,最后渲染template,渲染完成之后,再次向服务端发送数据请求,注意这里时数据请求,服务端返回json格式数据。客户端接收数据,然后完成最终渲染。SPA虽然给服务器减轻了压力,但是也是有缺点的:首屏渲染时间比较长:必须等待JavaScript加载完毕,并且执行完毕,才能渲染出首屏。SEO不友好:爬虫只能拿到一个div元素,认为页面是空的,不利于SEO。为了解决如上两个问题,出现了SSR解决方案,后端渲染出首屏的DOM结构返回,前端拿到内容带上首屏,后续的页面操作,再用单页面路由和渲染,称之为服务端渲染(SSR)。SSR渲染流程是这样的,客户端发送URL请求到服务端,服务端读取对应的url的模板信息,在服务端做出html和数据的渲染,渲染完成之后返回html结构,客户端这时拿到的之后首屏页面的html结构。所以用户在浏览首屏的时候速度会很快,因为客户端不需要再次发送ajax请求。并不是做了SSR我们的页面就不属于SPA应用了,它仍然是一个独立的spa应用。SSR是处于CSR与SPA应用之间的一个折中的方案,在渲染首屏的时候在服务端做出了渲染,注意仅仅是首屏,其他页面还是需要在客户端渲染的,在服务端接收到请求之后并且渲染出首屏页面,会携带着剩余的路由信息预留给客户端去渲染其他路由的页面。Nuxt.js 介绍在Nuxt官方网站有一句这样的话:Nuxt.js预设了使您开发Vue.js应用程序所需的所有配置。Nuxt是一个基于Vue.js的通用应用框架。通过对客户端/服务端基础框架的抽象组织,Nuxt主要关注的是应用的ui渲染。通过上面的这些介绍可以简单的得出:Nuxt不仅仅用于服务端渲染也可以用于SPA应用的开发利用Nuxt提供的项目结构、异步数据加载,中间件的支持,布局等特性可大幅提升开发效率Nuxt可用于网站静态化,可以使用命令将整个网页打包成静态页面,使SEO更加友好Nuxt.js 特性基于Vue自动代码分层服务端渲染强大的路由功能,支持异步数据静态文件服务EcmaScript6和EcmaScript7的语法支持打包和压缩JavaScript和CssHTML头部标签管理本地开发支持热加载集成ESLint支持各种样式预编译器SASS、LESS等等支持HTTP/2推送Nuxt 渲染流程一个完整的服务器请求到渲染的流程通过上面的流程图可以看出,当一个客户端请求进入的时候,服务端有通过nuxtServerInit这个命令执行在Store的action中,在这里接收到客户端请求的时候,可以将一些客户端信息存储到Store中,也就是说可以把在服务端存储的一些客户端的一些登录信息存储到Store中。之后使用了中间件机制,中间件其实就是一个函数,会在每个路由执行之前去执行,在这里可以做很多事情,或者说可以理解为是路由器的拦截器的作用。然后再validate执行的时候对客户端携带的参数进行校验,在asyncData与fetch进入正式的渲染周期,asyncData向服务端获取数据,把请求到的数据合并到Vue中的data中,Nuxt说明Nuxt安装:确保安装了npx(npx在NPM版本5.2.0默认安装了):npx create-nuxt-app <项目名>安装向导:Project name // 项目名称Project description // 项目描述Use a custom server framework // 选择服务器框架Choose features to install // 选择安装的特性Use a custom UI framework // 选择UI框架Use a custom test framework // 测试框架Choose rendering mode // 渲染模式 Universal // 渲染所有连接页面 Single Page App // 只渲染当前页面这些都是比较重要的其他的配置内容就不做介绍了,一路回车即可。目录结构介绍assets // 存放素材(需要执行webpack预处理操作)components // 组件layouts // 布局文件static // 静态文件(不需要webpack预处理操作)middleware // 中间件pages // 所有页面plugins // 插件server // 服务端代码store // vuex配置文件const pkg = require(’./package’)module.exports = { mode: ‘universal’, // 当前渲染使用模式 head: { // 页面head配置信息 title: pkg.name, // title meta: [ // meat { charset: ‘utf-8’ }, { name: ‘viewport’, content: ‘width=device-width, initial-scale=1’ }, { hid: ‘description’, name: ‘description’, content: pkg.description } ], link: [ // favicon,若引用css不会进行打包处理 { rel: ‘icon’, type: ‘image/x-icon’, href: ‘/favicon.ico’ } ] }, loading: { color: ‘#fff’ }, // 页面进度条 css: [ // 全局css(会进行webpack打包处理) ’element-ui/lib/theme-chalk/index.css’ ], plugins: [ // 插件 ‘@/plugins/element-ui’ ], modules: [ // 模块 ‘@nuxtjs/axios’, ], axios: {}, build: { // 打包 transpile: [/^element-ui/], extend(config, ctx) { // webpack自定义配置 } }}Nuxt运行命令{ “scripts”: { // 开发环境 “dev”: “cross-env NODE_ENV=development nodemon server/index.js –watch server”, // 打包 “build”: “nuxt build”, // 在服务端运行 “start”: “cross-env NODE_ENV=production node server/index.js”, // 生成静态页面 “generate”: “nuxt generate” }}结语这里简单的对Nuxt做了一些介绍,会持续更新对Nuxt的跟进,希望会对大家有所帮助,如果有什么问题,可以在下面留言。 ...

April 6, 2019 · 1 min · jiezi

6.eslint和editorconfig配置

本章节内容主要时要时参照官方文档配置即可。eslint配置在根项目目录项新建.eslintrc文件// 这里要安装 eslint-config-standard包,安装完后按照提示,安装相关的依赖。// 这里主要时对项目中所有内容生效,要求比较低{ “extends”: “standard”}然后在client目录下新建同样的文件,来规范client端的代码// babel-eslint , eslint-config-airbnb及其相关依赖包{ “parser”: “babel-eslint”, “env”: { “browser”: true, “es6”: true, “node”: true }, “parserOptions”: { “ecmaVersion”: 6, “sourceType”: “module” }, “extends”: “airbnb”, “rules”: { “semi”: [0] }}在webpack客户端和服务端的配置文件中,在rules下新增一个rule。 { enforce: ‘pre’, // 在babel编译之前进行检查 test: /.(js|jsx)$/, loader: ’eslint-loader’, // 使用eslint-loader,需安装 exclude: [ resolvePath(’../node_modules’) ] },配置完这些后,我们启动我们的服务。会发现出现很多错误,window环境下可以会见到很多"LF"的错误,这是因为不同的操作系统,行末的符号时不一致的。所以我们需要配置editorconfig文件。现在主流的ide,如webstorm,vs code都带有edit的插件,在项目根目录下新建.editorconfig文件,按照如下配置即可。root = true // 是否为根节点,说明在子目录下也可配置该文件[*] // 用于所有文件charset = utf-8 //编码格式indent_style = space //缩进样式indent_size = 2 // 缩进大小end_of_line = lf // 以lf结尾insert_final_newline = true // 自动在文件末尾插入新行trim_trailing_whitespace = true // 去除行末的空格git hook在提交代码之前进行lint检查,如果不合格,不能提交代码。以前一直用的是husky -哈士奇,后来在vue-cli中看到了yorkie,看说明应该是husky的改进版本。下面来说说两者的配置方式。 // package.json的scripts增加lint命令,检查client目录下的代码 “lint”: “eslint –ext .js –ext .jsx client/” // husky:在scripts下配置 “precommit”: “npm run lint” // yorkie, 与scripts平级 “gitHooks”: { “pre-commit”: “npm run lint” }这样,在你commit代码前就会进行检查,不符合要求的代码不能提交。本节的配置位于仓库的2-9分支 ...

April 4, 2019 · 1 min · jiezi

5.开发时服务端渲染

由于配置了webpack-dev-server,客户端启动时,就不必再本地生成dist目录。但是服务器端的编译还是需要本地的dist目录,所以本节我们将会配置服务端的内容,使得服务端也不用依赖本地的dist目录。相关依赖npm i axios // http依赖,估计大家都知道npm i memery-fs -D // 相关接口和node的fs一样,只不过是在内存中生成文件npm i http-proxy-middleware -D // 服务器端一个代理的中间件本章节内容比较难,对于没有接触过node和webpack的同学,理解起来不是那么容易。我也是不知道看了多少遍,才大概知道其流程。开发时的服务端配置以前server.js中要依赖本地dist中的文件,所以首先要对其进行更改。const static = require(’./util/dev-static’)const isDev = process.env.NODE_ENV === ‘development’ // 增加环境的判断const app =express()if(!isDev) { // 生产环境,和以前的处理方式一样 const serverEntry = require(’../dist/server-entry’).default // 配置静态文件目录 app.use(’/public’, express.static(path.join(__dirname, ‘../dist’))) const template = fs.readFileSync(path.join(__dirname, ‘../dist/index.html’), ‘utf-8’) // https://blog.csdn.net/qq_41648452/article/details/80630598 app.get(’’, function(req, res) { const appString = ReactSSR.renderToString(serverEntry) res.send(template.replace(’<!– <app /> –>’, appString)) })} else { // 开发环境,进行单独的处理 static(app)}开发环境中的static方法,位于server/util/dev-static.js中,接受一个app参数。按照生产模式的处理逻辑,开发模式下的配置也分为如下几点:获取打包好的入口文件,即server-entry.js文件获取模板文件将模板文件中的内容替换为server-entry.js中的内容,返回给客户端对静态文件的请求进行处理。获取模板文件获取模板文件最简单,所以最先解决这个问题。配置客户端的devServer时,再http://localhost:8888下面就可以访问到index.html文件,调用下面getTemplate方法就可以拿到模板文件。const axios = require(‘axios’)const getTemplate = () => { return new Promise((resolve, reject) => { axios.get(‘http://localhost:8888/public/index.html’) .then(res => { resolve(res.data) }) .catch(reject) })}获取server-entry.js文件获取服务端的文件,我们需要用到memory-fs包,直接再内存中生成打包好的文件,读取速度更快,那要怎么配置呢?const path = require(‘path’)const webpack = require(‘webpack’)const MemoryFs = require(‘memory-fs’)const serverConfig = require(’../../build/webpack.config.server’) // 读取配置文件// webpack(serverConfig)和我们的build:server命令类似const serverCompile = webpack(serverConfig) // webpack处理const mfs = new MemoryFs()serverCompile.outputFileSystem = mfs // 将文件的输出交给mfs;默认应该是node的fs模块// 监听文件的变化serverCompile.watch({}, (err, stats) => { if(err) throw err // stats对象有一些状态信息,如我们编译过程中的一些错误或警告,我们直接将这些信息打印出来 stats = stats.toJson() stats.errors.forEach(err => console.err(err)) stats.warnings.forEach(warn => console.warn(warn)) // 通过配置文件获取文件的路径和名称 const bundlePath = path.join( serverConfig.output.path, serverConfig.output.filename ) // 读取文件的内容 const bundle = mfs.readFileSync(bundlePath, ‘utf-8’)})所以服务端文件也获取到了?其实还是有问题的,我们获取的仅仅是字符串,并不是node中的一个模块(如果听不懂,先去补补node中模块的概念),所以还需要做进一步的处理。const Module = module.constructor// node中,每个文件中都有一个Module变量,不懂的就要多学习了const m = new Module() m._compile(bundle, serverConfig.output.filename) // 将字符串编译为一个模块serverBundle = m.exports.default // 这才是我们需要的内容替换内容app.get(’’, function (req, res) { getTemplate().then(template => { const content = ReactDomSSR.renderToString(serverBundle) res.send(template.replace(’<!– <app /> –>’, content)) }) })静态资源处理和模板文件一样,静态资源我们将会代理到localhost:8888里面去获取 const proxy = require(‘http-proxy-middleware’) app.use(’/public’, proxy({ target: ‘http://localhost:8888’ }))到这里,开发时服务端渲染就完成了。本小节完整代码位于仓库的2-8分支,觉得有用的可以去start一下。 ...

April 3, 2019 · 1 min · jiezi

4.hot-load-replacement配置(react-hot-loaderV4)

什么是热更新按照前面的配置,更改App.jsx中的内容,保存后,页面上的内容也会实时的变化,这难道不是热更行吗?我刚开始也有这样的疑问。但是,你要注意,目前更改内容保存后,浏览器执行的是刷新操作,相当于F5刷新页面。而热更新就像ajax一样,只会更改修改的那部分,不会引起浏览器的刷新。如何配置热更新热更新主要用到的包时react-hot-loader,课程中用的包版本较低,配置比较麻烦(相对于新版本),我在网上搜了一下react-hot-loader的配置,画风基本时这样的。目前react-hot-loader版本为4.8.2版本,根据官网的介绍,现在react-hot-loader可以直接当作正常的依赖,可以不用当作开发依赖。打包时,会自动去掉这个包。根据官网的说明,配置起来也很简单。配置.babelrc文件{ “plugins”: [“react-hot-loader/babel”]}将App.jsx导出的App用hot包裹import { hot } from ‘react-hot-loader’class App extends React.Component { render() { return ( <div>This is app</div> ) }}export default hot(App)同时,client的webpack配置中devServer的hot属性设置为true即可每次启动时,在浏览器的console里面就会出现 [WDS] Hot Module Replacement enabled. 的提示,代表热更新成功。另外,还有一个注意事项。配置热更新成功后,更改文件内容,在network窗口会发现有新的请求文件,本人浏览器窗口会去请求热更新的文件,请求地址如下http://localhost:8888/publica31180d047a509a4bdc0.hot-update.jsonpublic后面缺少’/’,奇怪的是并没有报404的错误,还是可以请求得到。但是最好还是处理一下,将webpack配置中的publicPath属性值改为’/public/’,以免以后出现问题。相关代码位于仓库的2-7分支

April 2, 2019 · 1 min · jiezi

3.webpack-dev-server配置

修复前面版本的一些问题在前面2-5分支中,运行后控制台总会出现一些错误。原因就是client目录下app.js和App.jsx的文件名相似引起的。因此我们将app.js重新命名为main.js,然后修改客户端webpack的入口文件为main.js即可。webpack-dev-server的作用前面都是buil命令,直接在硬盘上生成打包好的文件。而我们在开发过程中,往往会在本地启动一个服务器,webpack-dev-server就是帮助我们启动一个本地的服务器。本届主要时配置webpack的devServer属性,感兴趣的可以先去看看官方文档。本节内容需要安装两个开发环境的依赖。webpack-dev-server 启动本地服务器cross-env 判断不同系统下的开发或生产环境由于开发时的配置,所以主要是修改client端的配置文件。而且需要判断是否为开发环境。const isDev = process.env.NODE_ENV === ‘development’ //判断是否为开发环境// 以前是直接 module.exports = config {}// 现在需要在开发时增加一些配置config = {….} // 还是以前的配置,省略// 如果时开发环境,增加如下配置if (isDev) { config.devServer = { host: ‘0.0.0.0’, // 可以通过localhost或127.0.0.1方式访问 port: ‘8888’, // 端口号 contentBase: path.join(__dirname, ‘../dist’), // 访问的文件目录 // hot: true, // 热更替,后面配置react后会开启 overlay: { errors: true // 在浏览器窗口出口错误的提示层 }, publicPath: ‘/public’, // 与前面的功能一致 historyApiFallback: { index: ‘/public/index.html’ // 404页面默认回到首页 } }}module.exports = config前面我们在webpack中配置了mode:‘development’,就已经设置为开发模式了。关于mode这个属性,可以去看看官方文档。接下来,我们在package.json中配置scripts。// cross-env判断不同系统环境下的NODE_ENV的值 “dev:client”: “cross-env NODE_ENV=development webpack-dev-server –config build/webpack.config.client.js"注意,运行dev:client命令时,记得先删除本地编译的dist目录。本节代码位于仓库的2-6分支 ...

April 2, 2019 · 1 min · jiezi

2.React服务端渲染基础配置

服务端渲染服务端渲染(SSR)主要是为了SEO,加快首屏的加载速度等作用。利用react-dom/server提供的工具,我们很容易进行服务端渲染。基本原理服务端渲染的基本原理就是读取我们的模板文件,然后将其中的内容替换成我们自己的代码,然后生成一个完整的html文件返回给前端页面。webpack配置在第一篇文章中,已经进行了基础的配置,本文是在前面的基础上来配置的。本次配置需要安装以下两个依赖express, 涉及到服务端代码,用到express包rimraf, 看着名字就知道是删库跑路的包。每次我们运行build命令时,都会生成新的文件。我们可以用这个包先删除dist目录,然后在重新生成新的dist目录。首先在client目录下新增template.html和server-entry.js两个文件。前面的html时模板文件,后面的js作为服务端的入口文件。// template文件很简单,只有一个id为app的div,后面我们将会把<!– <app /> –>替换为我们自己的内容。<!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> <div id=“app”><!– <app /> –></div></body></html>// 入口文件目前也很简单,只是导入App组件import React from ‘react’import App from ‘./App.jsx’export default <App />在build目录下新增webpack.config.server.js文件,作为服务端打包的配置文件。同时为了区分客户端,将客户端的配置文件改为webpack.config.client.js。服务端与客户端的配置基本一样,主要时入口文件和出口文件的配置不同。 entry: { app: resolvePath(’../client/server-entry.js’) // 服务端入口文件 }, output: { filename: ‘server-entry.js’, // 输出文件名 path: resolvePath(’../dist’), // 输出路劲 publicPath: ‘’, // libraryTarget: ‘commonjs2’ // 模块化的方式 },在项目目录下新建server文件夹,新建一个server.js文件,该文件主要为服务端逻辑。const express = require(’express’)const ReactSSR = require(‘react-dom/server’)const fs = require(‘fs’)const path = require(‘path’)const serverEntry = require(’../dist/server-entry’).default // 打包好的服务端文件const app =express()const template = fs.readFileSync(path.join(__dirname, ‘../dist/index.html’), ‘utf-8’) // 读取模板文件app.get(’*’, function(req, res) { const appString = ReactSSR.renderToString(serverEntry) res.send(template.replace(’<!– <app /> –>’, appString)) // 将模板文件中的注释替换为我们自己的内容,然后返回到客户端})app.listen(3333, function() { console.log(‘server is listen on 3333’)})服务端渲染的基本逻辑就已经完成。接下来我们在package.json文件中新增一些命令。 “scripts”: { “build:client”: “webpack –config build/webpack.config.client.js”, // 编译客户端代码 “build:server”: “webpack –config build/webpack.config.server.js”, // 编译服务端代码 “clear”: “rimraf dist”, // 每次build前,先自动删除dist目录 “build”: “npm run clear && npm run build:client && npm run build:server”,// build客户端和服务端的代码 “start”: “node server/server.js” // 启动服务器 },先运行build命令,然后运行start命令,访问localhost:3333,就可以看到内容了。而且在network窗口中可以看到返回的时完整的html页面,而不是一个空页面。但时目前请求js文件,返回的也是html文件。因为在server.js中,任意的请求都是返回html文件。可以通过express来配置静态文件目录。// 以/public开头的请求都会去dist目录中找。app.use(’/public’, express.static(path.join(__dirname, ‘../dist’)))同时需要修改客户端和服务端的webpack配置。 // 会在路径前加上/public前缀 output: { publicPath: ‘/public’, },重新运行build和start命令,访问3333端口,就会返现请求都是正常的。从返回的html文件中,script标签的src属性中的路径会带有/public前缀,这就是publicPath属性的作用。至此,服务端渲染的基础配置就已经完成。本次的代码位于仓库的2-5分支。在使用rimraf时,window可能会遇到一些权限相关的问题,可能的解决方法点这里 ...

April 2, 2019 · 1 min · jiezi

1.webpack基础配置与loader的基础应用

写在开头该系列文章主要是本人在学习慕课网React全栈课程中的一些记录和分享。该课程主要是利用React构建cnode网站,接口由cnode官方提供。由于课程中的webpack,babel版本较老,本次分享均是用的webpack4和Babel7。本系列文章重点不是React,主要是分享前端工程化的构建和服务端渲染(SSR)。本次分享的代码将会放到我的github上面。工程初始化和webpack基础配置新建项目文件夹,在cmd窗口中运行npm init,输入一些配置项后即可生成一个npm项目。运行git init,对该项目进行git版本管理。在项目中新建build和client文件夹,build文件夹存放webpack配置文件,client文件夹存放客户端的开发文件。首先安装基础的依赖React和Webpack。npm i react -S npm i webpack -D // -D为开发依赖npm i webpack-cli -D // webpack4,需要安装cli依赖新建一些文件build/webpack.config.js // webpack配置文件client/app.js // 项目的入口文件client/App.jsx // react入口文件webpack基础配置,详细配置可参照官网const path = require(‘path’) // path包解决不同操作系统中路径不一致问题function resolvePath(filePath) { return path.join(__dirname, filePath);}module.exports = { mode: ‘development’, // 开发模式或生产模式 // 入口文件,webpack编译的入口 entry: { app: resolvePath(’../client/app.js’) }, // 打包后文件的输出地址 output: { filename: ‘[name].[hash].js’, //name和hash是其中的两个变量 path: resolvePath(’../dist’), // 打包后文件的位置 publicPath: ’’ // }}webpack基本配置完成了,在package.json中的scripts中增加一个build命令 “scripts”: { “build”: “webpack –config build/webpack.config.js” },在app.js中随便写一点内容,在cmd中运行npm run build,在当前文件夹下会生成一个dist目录,该目录下即为经webpack编译后的文件。该部分代码位于仓库的2-3分支下如果初始化git后,在项目下添加 .gitignore 文件,用来配置不需要版本管理的文件夹或文件,如node_modules等babel-loader及babel的配置由于在项目中用到ES6和jsx语法,所有需要用babel先编译。babel-loader也是react官方的编译器。我们现在app和App文件中写一些简单的内容。App.jsx import React from ‘react’ // 一个简单的react组件 export default class App extends React.Component { render() { return ( <div>This is app</div> ) } }app.js //将App.jsx中的组件挂载到body上(仅作演示,不建议挂载到body上) import React from ‘react’ import ReactDOM from ‘react-dom’ import App from ‘./App’ ReactDOM.render(<App />, document.body) 配置webpack中的loader,loader主要是转换代码的作用,如将jsx代码转为js代码。我们需要安装babel-loader,@babel/core和@babel/preset-react三个依赖,均用-D安装。 resolve: { extensions: [’.js’,’.jsx’] // 默认文件后缀。在app.js中,直接引入App,而不是App.jsx。所有的js和jsx文件在引入时均可省略后缀 }, // 配置loader module: { rules: [ { test: /.jsx$/, // 正则,处理以.jsx结尾的文件 loader: ‘babel-loader’ // 使用的loader }, { test: /js$/, // 主要是将ES6或更高级别的无法转为ES5版本 loader: ‘babel-loader’, exclude: [ resolvePath(’../node_modules’) // 忽略node_modules中的文件 ] } ] },配置完webpack后,babel还没有生效。需要在项目中新建一个.babelrc文件,配置项如下。// babel7的配置比较简洁,直接使用官方的preset-react即可。{ “presets”: ["@babel/preset-react"]}运行npm run build命令,新生成的js文件就会包含react相关的代码了。目前生成的文件均为js文件,并没有html文件的生成。我们之需要安装html-webpack-plugin,然后再webpack中配置即可const HTMLPlugin = require(‘html-webpack-plugin’)modeule.exports = { //……… plugins: [ new HTMLPlugin() ]}再次运行build命令后,会再dist目录下生成一个index.html文件,打开即可看见我们再App.jsx中的内容。该部分代码位于仓库的2-4分支下 ...

March 31, 2019 · 1 min · jiezi

React SSR 技术摘要

单页面应用(SPA)模式被越来越多的站点所采用,这种模式势必面临着首次有效绘制(FMP)耗时较长和不利于搜索引擎优化(SEO)的问题。“同构应用” 就像是精灵,可以游刃有余的穿梭在服务端与客户端之间各尽其能。但是想驾驭 “同构应用” 往往会面临一系列的问题,下面针对一个示例进行一些细节介绍。示例代码:https://github.com/xyyjk/reac…“同构” 是指一套代码可以在服务端和客户端两种环境下运行,通过用这种灵活性,可以在服务端渲染初始内容输出到页面,后续工作交给客户端来完成,最终来解决SEO的问题并提升性能。构建配置选择一个灵活的脚手架为项目后续的自定义功能及配置是十分有利的,Neutrino 提供了一些常用的 webpack 预设配置,使初始化和构建项目的过程更加简单。这里基于 @neutrinojs/react 预设做一些定义用于开发.neutrinorc.jsconst isDev = process.env.NODE_ENV !== ‘production’;const isSSR = process.argv.includes(’–ssr’);module.exports = { use: [ [’@neutrinojs/react’, { devServer: { port: isSSR ? 3000 : 5000, host: ‘0.0.0.0’, disableHostCheck: true, contentBase: ${__dirname}/src, before(app) { if(isSSR) { require(’./src/server’)(app); } }, }, manifest: true, html: isSSR ? false: {}, clean: { paths: [’./node_modules/.cache’]}, }], ({ config }) => { if (isDev) { return; } config .output .filename(‘assets/[name].[chunkhash].js’) .chunkFilename(‘assets/chunk.[chunkhash].js’) .end() .optimization .minimize(false) .end(); }, ],};为了达到开发环境下可以选择 SSR(服务端渲染)、CSR(客户端渲染) 任意一种渲染模式,在开始先定义一个变量 isSSR 用以做差异配置:devServer.before 方法可以在服务内部的所有其他中间件之前,提供执行自定义中间件的功能。在 SSR 模式 下加入一个中间件,稍后用于进行处理服务端内容渲染。启用 manifest 插件,打包后生成资源映射文件用于服务端渲染时模板中引入。构建用于服务端运行的配置项稍有不同由于 SSR 模式 最终代码要运行在 node 环境,这里需要对配置再做一些调整:target 调整为 node,编译为类 Node 环境可用libraryTarget 调整为 commonjs2,使用 Node 风格导出模块preset-env 运行环境调整为 node排除组件中 css/sass 资源的引用在打包的时候通过 webpack-node-externals 排除 node_modules 依赖模块,可以使服务器构建速度更快,并生成较小的 bundle 文件。webpack.server.config.jsconst Neutrino = require(’neutrino/Neutrino’);const nodeExternals = require(‘webpack-node-externals’);const NormalPlugin = require(‘webpack/lib/NormalModuleReplacementPlugin’);const babelMerge = require(‘babel-merge’);const config = require(’./.neutrinorc’);const neutrino = new Neutrino();neutrino.use(config);neutrino.config .target(’node’) .entryPoints .delete(‘index’) .end() .entry(‘server’) .add(${__dirname}/src/server) .end() .output .path(${__dirname}/build) .filename(‘server.js’) .libraryTarget(‘commonjs2’) .end() .externals([nodeExternals()]) .plugins .delete(‘clean’) .delete(‘manifest’) .end() .plugin(’normal’) .use(NormalPlugin, [/.css$/, ’lodash/noop’]) .end() .optimization .minimize(false) .runtimeChunk(false) .end() .module .rule(‘compile’) .use(‘babel’) .tap(options => babelMerge(options, { presets: [ [’@babel/preset-env’, { targets: { node: true }, }], ], }));module.exports = neutrino.config.toConfig();环境差异由于运行环境和平台 API 的差异,当运行在不同环境中时,我们的代码将不会完全相同。Webpack 全局对象中定义了 process.browser,可以在开发环境中来判断当前是客户端还是服务端。自定义中间件开发环境 SSR 模式 下,如果我们在组件中引入了图片或样式资源,不经过 webpack-loader 进行编译,Node 环境下是无法直接运行的。在 Node 环境下,通过 ignore-styles 可以把这些资源进行忽略。此外,为了让 Node 环境下能够运行 ES6 模块的组件,需要引入 @babel/register 来做一些转换:src/server/register.jsrequire(‘ignore-styles’);require(’@babel/register’)({ presets: [ [’@babel/preset-env’, { targets: { node: true }, }], ‘@babel/preset-react’, ], plugins: [ ‘@babel/plugin-proposal-class-properties’, ],});如果 webpack 中配置了 resolve.alias,与之对应的还需要增加 babel-plugin-module-resolver 插件来做解析。由于 require() 引入方式模块将会被缓存, 为了使组件内的修改实时生效,通过 decache 模块从 require() 缓存中删除模块:src/server/dev.jsrequire(’./register’);const decache = require(‘decache’);const routes = require(’./routes’);let render = require(’./render’);const handler = async (req, res, next) => { decache(’./render’); render = require(’./render’); res.send(await render({ req, res })); next();};module.exports = (app) => { app.get(routes, handler);};服务端渲染在服务端通过 ReactDOMServer.renderToString() 方法将组件渲染为初始 HTML 字符串。获取数据往往需要从 query、cookie 中取一些内容作为接口参数,Node 环境下没有 window、document 这样的浏览器对象,可以借助 Express 的 req 对象来拿到一些信息:href: ${req.protocol}://${req.headers.host}${req.url}cookie: req.headers.cookieuserAgent: req.headers[‘user-agent’]src/server/render.jsconst React = require(‘react’);const { renderToString } = require(‘react-dom/server’);…module.exports = async ({ req, res }) => { const locals = { data: await fetchData({ req, res }), href: ${req.protocol}://${req.headers.host}${req.url}, url: req.url, }; const markup = renderToString(<App locals={locals} />); const helmet = Helmet.renderStatic(); return template({ markup, helmet, assets, locals });};入口文件前端调用 ReactDOM.hydrate() 方法与服务端返回的静态标记相绑定事件。src/index.jsximport React from ‘react’;import ReactDOM from ‘react-dom’;import App from ‘./App’;const renderMethod = ReactDOM[module.hot ? ‘render’ : ‘hydrate’];renderMethod(<App />, document.getElementById(‘root’));根组件在服务端使用 StaticRouter 组件,通过 location 属性设置服务器收到的URL,并在 context 属性中存入渲染期间所需要的数据。src/App.jsximport React from ‘react’;import { BrowserRouter, StaticRouter, Route } from ‘react-router-dom’;import { hot } from ‘react-hot-loader/root’;…const Router = process.browser ? BrowserRouter : StaticRouter;const App = ({ locals = {} }) => ( <Router location={locals.url} context={locals}> <Layout> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> <Route path="/contact" component={Contact}/> <Route path="/character/:key" component={Character}/> </Layout> </Router>);export default hot(App);内容数据通过 constructor 接收 StaticRouter 组件传入的数据,客户端 URL 与服务端请求地址相一致时直接使用传入的数据,否则再进行客户端数据请求。src/comps/Content.jsximport React from ‘react’;import { withRouter } from ‘react-router-dom’;import fetchData from ‘../utils/fetchData’;function isCurUrl() { if (!window.INITIAL_DATA) { return false; } return document.location.href === window.INITIAL_DATA.href;}class Content extends React.Component { constructor(props) { super(props); const { staticContext = {} } = props; let { data = {} } = staticContext; if (process.browser && isCurUrl()) { data = window.INITIAL_DATA.data; } this.state = { data }; } async componentDidMount() { if (isCurUrl()) { return; } const { match } = this.props; const data = await fetchData({ match }); this.setState({ data }); } render() { return this.props.render(this.state); }}export default withRouter(Content);自定义标记通常在不同页面中需要输出不同的页面标题、页面描述,HTML 属性等,可以借助 react-helmet 来处理此类问题:模板设置const markup = ReactDOMServer.renderToString(<Handler />);const helmet = Helmet.renderStatic();const template = &lt;!DOCTYPE html&gt;&lt;html ${helmet.htmlAttributes.toString()}&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; ${helmet.title.toString()} ${helmet.meta.toString()} ${helmet.link.toString()} &lt;/head&gt; &lt;body ${helmet.bodyAttributes.toString()}&gt; &lt;div id="root"&gt;${markup}&lt;/div&gt; &lt;/body&gt;&lt;/html&gt;;组件中的使用import React from ‘react’;import Helmet from ‘react-helmet’;const Contact = () => ( <> <h2>This is the contact page</h2> <Helmet> <title>Contact Page</title> <meta name=“description” content=“This is a proof of concept for React SSR” /> </Helmet> </>);总结想要做好 “同构应用” 并不简单,需要了解非常多的概念。好消息是目前 React 社区有一些比较著名的同构方案 Next.js、Razzle 等,如果你想快速入手 React SSR 这些或许是不错的选择。如果面对复杂应用,自定义完整的体系将会更加灵活。 ...

March 20, 2019 · 3 min · jiezi

使用 Nuxt.js 快速搭建服务端渲染(SSR) 应用

安装 nuxt.jsNuxt.js 官方提功了两种方法来进行项目的初始化,一种是使用Nuxt.js团队的脚手架工具 create-nuxt-app ,一种是根据自己的需求自由配置 使用脚手架适合新手,对 nodejs 后台框架有所了解;按照自己需求自由配置,需要对如何配置 webpack 以及 nodejs 后台框架有所了解。 两种方式比较下就是原生和插件的区别。使用脚手架安装需要有 nodejs 或者 yarn 环境,推荐使用 vscode 自带的控制台输入命令行命令进行操作在有了环境之后直接输入以下命令就可以直接创建一个项目(npx 在npm 5.2.0默认安装,使用最新稳定nodejs环境不用考虑有没有)npx create-nuxt-app <项目名>#或者用yarnyarn create nuxt-app <项目名>之后他会提示你进行一些选择 1.项目名 在这里可以设置项目名,亦可以之后在 package.js 中设置 name 属性,一般是在输入上面命令时的项目名,不需要修改直接回车就好2.项目描述这里是关于项目的描述,比如是做什么的,也可以之后在 package.js 中设置 description 属性3.选择服务器端框架 看自己习惯使用什么了,一般 Express Koa 居多4.扩展插件选择 axios EsLint Prettieraxios 发送HTTP请求EsLint 在保存时代码规范和错误检查自己的代码。Prettier 在保存时格式化/美化自己的代码。5.选择 UI 框架 UI 框架方便快速开发,提供了很多现成的样式,近几年听到最多的就是 Element UI 6.选择测试框架测试框架是用来检测程序有没有到达预期的目的,有没有出错,这里暂时用不到,所以选择 none 就好7.选择渲染模式这里分单页应用(spa)以及普遍的方式(Universal),单页应用有很多路由但是页面只有一个,所有能看到的页面都是 js 即时生成的 dom,第二种是在服务器生成 html ,有多少路由就有多少页面。 使用 nuxt 就是为了解决 SEO 的问题,使其实现所有网站路径完全被收录8.作者这个也可以之后在 package.js 中设置 author 属性 9.选择包管理工具这里选择那个都可以,看自己习惯用哪个10.选择完成开始安装11.安装完成提示信息项目目录关于如何根据自己的需求自由配置,这里不讲,有需要自由配置的一般都不是新手了,推荐看看官方文档添加其他常用功能除了 nuxt 脚手架自带的,我们还需要其他配置,ES6的编译 ,CSS的预处理,其他的用到了再添加安装 babelyarn add babel-cli babel-preset-env配置文件.babelrc{ “presets”: [“env”]}安装 scssyarn add node-sass yarn add sass-loader之后只需要在 vue 文件的 style 标签加一条属性声明下就好<style lang=“sass”></style># or<style lang=“scss”></style> ...

March 13, 2019 · 1 min · jiezi

vueSSR: 从0到1构建vueSSR项目 --- vuex的配置(数据预取)

vuex的相关配置上一章做了node以及vue-cli3的配置,今天就来做vuex的部分。先打开官方文档-数据预取和状态看完之后,发现大致的逻辑就是利用mixin,拦截页面渲染完成之前,查看当前实例是否含有’asyncData’函数(由你创建以及任意名称),如果含有就进行调用,并且传入你需要的对象比如(store,route)例子// pages/vuex/index.jsexport default { name: “vuex”, asyncData({ store, route }) { // 触发 action 后,会返回 Promise return store.dispatch(‘fetchItem’, route.path) }, computed: { // 从 store 的 state 对象中的获取 item。 item() { return this.$store.state.items[this.$route.path] } }}//mixinimport Vue from ‘vue’;Vue.mixin({ beforeMount() { const { asyncData } = this.$options if (asyncData) { this.dataPromise = asyncData({ store: this.$store, route: this.$route }) } }})上面只是个大概的示例,下面开始正式来做吧。先创建一些文件 src/store.config.js 跟router.config.js 一样,在服务端运行避免状态单例 src/pages/store/all.js 全局公共模块// src/pages/store/all.js const all = { //开启命名空间 namespaced: true, //ssr注意事项 state 必须为函数 state: () => ({ count:0 }), mutations: { inc: state => state.count++ }, actions: { inc: ({ commit }) => commit(‘inc’) } } export default all;vuex 模块all 单独一个全局模块如果home有自己的数据,那么就在home下 惰性注册模块但是记得页面销毁时,也一定要销毁模块!!!因为当多次访问路由时,可以避免在客户端重复注册模块。如果想只有个别几个路由共用一个模块,可以在all里面进行模块嵌套,或者将这个几个页面归纳到一个父级路由下 ,在父级实例进行模块惰性注册。// home/index.jsimport all from ‘../store/all.js’;export default { name: ‘home’, computed:{ count(){ return this.$store.state.all.count } }, asyncData({ store}){ store.registerModule(‘all’,all); return store.dispatch(‘all/inc’) }, data() { return { activeIndex2: ‘1’, show:false, nav:[ { path:’/home/vue’, name:‘vue’ }, { path:’/home/vuex’, name:‘vue-vuex’ }, { path:’/home/vueCli3’, name:‘vue-cli3’ }, { path:’/home/vueSSR’, name:‘vue ssr’ } ] }; }, watch:{ $route:function(){ if(this.show){ this.show = false; } //切换路由时,进行自增 this.$store.dispatch(‘all/inc’); } }, mounted() { //做额外请求时,在mounted下进行 }, methods: { user_info(){ this.http.post(’/cms/i1/user_info’).then(res=> { console.log(res.data); }).catch( error => { console.log(error) }) } }, destroyed(){ this.$store.unregisterModule(‘all’) }}数据预取// store.config.js //store总配置 import Vue from ‘vue’; import Vuex from ‘vuex’; Vue.use(Vuex); //预请求数据 function fetchApi(id){ //该函数是运行在node环境 所以需要加上域名 return axios.post(‘http://localhost:8080/cms/i1/user_info’); } //返回 Vuex实例 避免在服务端运行时的单利状态 export function createStore() { return new Vuex.Store({ state:{ items:{} }, actions: { fetchItem ({commit}, id) { return fetchApi(id).then(item => { commit(‘setItem’,{id, item}) }) } }, mutations: { setItem(state, {id, item}){ Vue.set(state.items, id, item.data) } } }) }mixin相关在src下新建个methods 文件夹,这里存放写vue的全局代码以及配置 获取当前实例// src/methods/index.jsimport ‘./mixin’;import Vue from ‘vue’;import axios from ‘axios’;Vue.prototype.http = axios;// src/methods/mixin/index.js import Vue from ‘vue’; Vue.mixin({ beforeMount() { const { asyncData } = this.$options;//这里 自己打印下就知道了。就不过多解释了 //当前实例是否有该函数 if (asyncData) { // 有就执行,并传入相应的参数。 asyncData({ store: this.$store, route: this.$route }) } } })main.js 新增代码 import Vue from ‘vue’; Vue.config.productionTip = false; import VueRouter from ‘vue-router’; import App from ‘./App.vue’;+ import ‘./methods’; //同步路由状态+ import { sync } from ‘vuex-router-sync’; import { createRouter } from ‘./router.config.js’;+ import { createStore } from ‘./store.config.js’; export function createApp() { const router = createRouter() const store = createStore() //同步路由状态(route state)到 store sync(store, router) const app = new Vue({ router,+ store, render: h => h(App) }) return { app, router,+ store }; }下面更新,开发环境部署相关 ...

March 6, 2019 · 2 min · jiezi

【翻译】Web渲染概述

本文简单介绍了web应用各种渲染方案,其中包括客户端渲染、服务器端渲染等各种渲染方案。文章翻译自:https://developers.google.com…。由我所在的团队共同翻译完成,并发布在前端技术公众号:方凳雅集上,转载于此。方凳雅集是阿里CBU前端技术专业号,有兴趣的小伙伴可以关注一发。1. 起航作为开发人员,我们经常面临影响应用程序整个架构的决策。我们必须做出的核心决策之一是在什么地方实现业务逻辑和渲染逻辑。这可能很困难,因为有很多不同的方法来构建网站。对这个领域的理解来自于过去几年我们在一些大型网站的工作。从广义上说,我们鼓励开发人员选用带有rehydration(下一小节有解释)的服务器端渲染或静态化渲染。为了更好理解我们在做决定时所选择的架构,需要对每种方法和术语有充分的理解,通过不同渲染方式的页面性能可以帮助我们理解它们之间的差异。2. 术语渲染:SSR:服务器端渲染。CSR:客户端渲染。Rehydration:在服务器端渲染的dom树和数据的基础上,浏览器端利用JavaScript再次渲染。Prerendering:在构建时生成静态HTML和页面的初始状态。性能:TTFB:Time to First Byte —— 浏览器发出资源请求到接受到资源第一个字节的时间。FP:First Paint —— 页面打开到可视内容第一个像素渲染出来的时间。FCP:First Contentful Paint —— 页面打开到页面主要内容可见的时间。TTI:Time To Interactive —— 页面打开到变得可以交互的时间。这里只是简单的介绍了这些性能术语,想要详细的了解它们,可以看一下我们之前写的两篇文章性能优化篇——以用户为中心的指标(一)和性能优化篇——以用户为中心的指标(二)。3. 服务器端渲染服务器端渲染:打开页面时服务器将完整的HTML生成好了并返回。这避免了在浏览器端取数然后渲染所产生的消耗,因为这些事情已经在服务器端响应用户之前就已经做好了。服务器端渲染会带来快速的FP和FCP,在服务器端处理业务逻辑和渲染逻辑,可以避免向浏览器发送大量JavaScript,这有助于实现快速的TTI,这种方法适用于各种设备和网络环境,如果你开启了一些流浏览器优化,比如文档采用流的方式解析(streaming document parsing)。对于服务器端渲染,用户不太可能会去等浏览器执行其他的耗CPU的JavaScript代码执行完毕再去操作,因为页面内容已经显示出来了。在第三方js无法避免时(广告),虽然服务器端渲染可以减少FP和FCP渲染的JavaScript消耗,但是可能会给接下来要执行的js带来一定的“负担”。服务器端渲染的主要缺点在于,渲染需要消耗时间,所以可能TTFB会比较大。服务器渲染是否可以满足应用程序,很大程度上取决于您正在构建的体验类型。关于服务器渲染与客户端渲染的哪个更好的争论从未停过。但是我们需要记住的时,我们可以有选择让一些页面进行服务器端渲染,其他页面使用客户端渲染。一些网站使用这种混合渲染模式就取得了不错的效果,比如Netflix对他的登录页面采用了服务器端渲染,同时为重交互的页面预取js,为那些重交互并且客户端渲染的页面加快页面加载的机会。许多现代框架、库和架构使得同一个应用同时在服务器端和浏览器端都能渲染成为可能。这些技术虽然可以用于服务器端渲染,但需要注意的是,在服务器和客户端上进行渲染的体系结构具有非常不同的性能特征和权衡。React用户可以用它的renderToString方法或者其他基于该方法的框架进行服务器端渲染,比如Next.js; Vue用户可以去看它的服务器端渲染指南或者Nuxt; Angular用户可以去看Universal。4. 静态化渲染静态化渲染:在构建(build)时将页面中不会变化的内容直接渲染成出来,然后打到HTML中去。在浏览器端需要执行的js有限的假设下,该方法能够提供快速的FP、FCP和TTI。与服务器端渲染不同,它还能提供快速的TTFB,因为服务器端不需要生成HTML。一般而言,静态化渲染需要为每个URL生成一个单独的HTML,当用户访问的时候,直接将预先渲染好的HTML返回就好。另外渲染出的HTML也可以部署到CDN上,通过edge caching(边缘缓存)缓存做一些优化。对于不了解edge caching的同学,可以去看一下我们之前写的React缓存小记,里面有关于它的介绍。静态化渲染也有不同的方案,比如像Gatsby这样的工具旨在让开发人员感觉他们的应用程序是动态渲染的,而不是在构建过程中产生静态的HTML;像Jekyl和Metalsmith这样的工具拥抱的静态特性,提供了很多模板驱动的方法。静态化渲染需要为每个URL生成单独的HTML,这是它的一个缺点。如果您无法提前预测这些URL的内容,或者或一个网站存在大量的URL,静态化渲染可能是不合适的。对于静态化渲染,React用户可能对Gatsby、Next.js的static export或者Navi比较熟悉。这里我们需要重点理解一下静态化渲染和预渲染之间的差异:静态化渲染的页面在浏览器端变得能够交互之前只需要执行很少的js代码,甚至不需要;而预渲染虽然能够实现快速的FP、FCP,但应用必须在浏览器端执行js代码才能变得可交互。如果您不能确定到底是使用静态化渲染还是预渲染,那就测试一下呗,在应用加载的时候,禁止JavaScript的加载和执行。禁止JavaScript后,对于静态化渲染,页面的大多数功能还是能够正常运行;而对于预渲染,页面可能只有一些基础的功能能够工作,比如连接跳转。另一个有用的测试方式是通过Chrome的开发者工具DevTools减慢网络速度,观察在页面变为交互之前下载了多少JavaScript。预渲染通常需要更多的JavaScript,并且复杂度往往也比使用渐进增强的静态化渲染要高。5. 服务器端渲染VS静态化渲染服务器端渲染不是一颗银弹,它的动态特性会带来巨大的计算开销。服务器端渲染会加大TTFB或发送双倍的数据(比如将客户端的State数据打到HTML中去),在React中,renderToString会很慢,因为它是同步并且单线程的。为了让服务器端渲染能够”正确“运行,我们需要关注组件缓存、内存消耗、memoization技术应用和其他问题。通常情况你可能需要多次渲染同一个应用——一次在服务器端,一次在客户端。服务器端渲染只能让需要展示的内容显示的的更快,并没有减少工作量。服务器端渲染可以根据不同的URL生成不同的内容,相较于静态化渲染,它的速度可能会慢一些。其实我们可以做一些工作来缓解这个问题,服务器渲染 + HTML缓存可以大大减少渲染时间。服务器端渲染的优势在于它能够获取实时数据,并且所能处理的请求集比静态化渲染要大。因为静态化渲染只能处理那些提前能够预测内容的页面,对于需要个性化的页面(千人千面),静态化渲染就无法处理了。在构建PWA时,服务器端渲染也有用武之地,服务器端对页面片段进行渲染,前端使用Service Worker进行缓存。6. 客户端渲染客户端渲染:是指在浏览器中直接使用JavaScript来渲染页面,所有的逻辑、数据的获取、模板和路由都是在客户端而不是服务器上处理的。在移动端,客户端渲染很难获得并保持一个较快的渲染速度。有时我们只需要做很少的工作,就能让客户端渲染的性能与服务器端渲染的性能相差无几,比如尽可能的保持小的JS体积和少的RTT(https://en.wikipedia.org/wiki…)。甚至关键的脚本和数据如果使用HTTP/2的服务器端推送,或者使用<link rel=preload>,我们还可以让解析工作更早开始。另外,像PRPL这样的技术也可以帮助我们加快页面的初始化及其后续导航。但事情并不是这么简单。客户端渲染的主要问题是,所需的JavaScript会随着应用程序的增长而增长。随着新的JavaScript库、兼容组件和第三方代码的添加,控制脚本的规模会变得格外困难——尤其是这些代码和库都经常都需要在页面内容渲染之前被加载。所以对于那些脚本规模很大的应用,应该优先考虑使用代码拆分的方案。特别的,对于懒加载的JavaScript,要确保只在有需要时才加载必要的代码。而对于只有很少或者没有什么交互的应用,服务器渲染是一个可扩展性更好的方案。如果你想构建SPA(单页)应用,使用app shell缓存页面中交互的核心组件会给你很大的帮助。如果结合PWA的Service Work技术,还可以有效提高页面重复访问时的性能。7. 通过rehydration将服务器渲染和客户端渲染相结合通常称为同构渲染或者直接简单地称为"SSR",这种方式尝试在客户端渲染和服务端渲染之间寻找平衡,希望能够减少两者的弊端。页面导航导致跳转或刷新时,服务器会输出页面的HTML文档,并把该页面所需要的javascript和(用于渲染的)数据内联到文档一起输出。如果实现得当,这种方式确实可以像服务端渲染那样实现较快的首次内容绘制(First Contentful Paint),之后客户端会通过一种叫rehydration的技术继续(在客户端)渲染。这是个新颖的技术,但它会引起比较大的性能问题。使用reydration技术进行服务端渲染的主要问题在于它会对可交互时间(Time To Interactive)有明显的负面影响,尽管它缩短了首次绘制时间(First Paint)。服务端渲染的页面往往让人感觉已经加载完毕并可以开始交互了,但实际上只有等到客户端的js脚本执行并完成DOM事件绑定才能响应用户的交互(例如用户的输入行为)。在一些手机终端,这个过程会耗费几秒甚至几分钟的时间。也许你自己也经历过这样的场景:一个页面看起来已经加载完成了,但是在页面执行点击或者轻触的动作,结果却什么也没发生。这很快变得令人沮丧……“为什么(页面)没有反应?为什么我不能滚动?”Rehydration问题:重复Rehydration的问题不止于此,通常比因js导致的交互延迟更糟糕。为了让客户端js能够准确地渲染,而不用重新向服务器请求渲染所需的数据,目前服务端渲染通常会把UI所需的数据序列化并内联到HTML文档的script标签里。最终的HTML文档包含了更高层面的重复:可以看到,对于页面导航请求,服务器会返回了相应的UI描述(HTML),但它同样返回了渲染UI所需的数据。同时,客户端脚本同样包括了UI的描述(译者注:前端渲染需要包含对UI的描述,例如JSX),以便在客户端继续渲染。只有当bundle.js完成下载和执行后,UI才进入可交互状态。从使用rehydration方案的一些真实网站搜集到的性能数据来看,该方案是极度不推荐的。究其原因,还是回到用户体验上:这种方式很容易让用户停留在“神秘的峡谷”之中。(译者注:即界面可见但不可交互的状态)尽管如此,外界对rehydration方案还是有些许期待的。简单来说,使用服务端渲染时,只对需要高度缓存的内容才会降低首字节时间(TTFB),得到和预渲染(prerendering)类似的结果。在未来,rehydtration方案可能会逐渐地、或者部分地被应用,并成为服务端渲染的关键。8. 流式服务端渲染和渐进式Rehydration服务端渲染在过去几年中有了长足的进展。流式服务端渲染允许你以块的形式发送HTML,同时浏览器端接收并逐一渲染,这种方案可以实现快速的FP和FCP。React提供了一个异步的、以流的方式传输的方式 renderToNodeStream,与同步的renderToString相比,它能够更好处理服务器压力。渐进式Rehydration也值得关注,React团队正在做一些有趣的探索(https://github.com/facebook/r…)。使用这种方法,服务器渲染随着时间的推移被“启动”去渲染应用的各个片段,而不是当前一次性渲染整个应用。这可以帮助减少使页面交互所需的JavaScript,因为这样可以延迟页面中低优先级展示内容的渲染(比如非首屏的内容),同时可以防止这些低优先级的渲染阻塞主线程。而且,这种方案也能避免带Rehydration的服务端渲染的一个很大的陷阱:服务端渲染生成的DOM树在浏览器端被销毁然后被重建,这个问题大多数是因为同步的客户端渲染生成DOM树所需要的初始数据还没准备好。局部Rehydration局部rehydration被证明难以实现。这个方案是渐进式Rehydration的一个扩展,需要分析出相互独立的片段(组件/视图/树)中哪些具有极少交互或者完全没有交互的部分进行渐进式rehydrated。对于这些近乎静态的部分,相应JavaScript代码会被改造成惰性的引用,从而将它们对客户端的影响降低到近乎为0。局部hydration为缓存带来了一定挑战,客户端导航意味着我们不能假设应用程序的惰性部分即服务器渲染生成的HTML是可用的,除非页面完全加载完。Trisomorphic渲染如果Service Worker是你的一个选项,“trisomorphic”渲染也是一个有趣的点子。利用这项技术,你可以利用流式服务端渲染生成初始的或不依赖JS的部分,然后在Service Worker install后利用它渲染生成html。这种方案能使缓存的组件和模板保持实时而且还支持SPA类型的应用,在同一会话中根据不同的导航渲染不同的视图。这种方法在服务器端、客户端和Service Worker中可以复用模板和路由代码。9. SEO在选择在渲染方案,通常会考虑SEO。通常我们会选择服务器渲染来应对爬虫。爬虫可能能理解JavaScript,但是它们有很多局限性,我们需要重点关注一下他们是如何渲染的。虽然客户端渲染可以工作,但是一般都没有做测试等工作,如果您的应用依赖于客户端渲染,动态渲染是您值得考虑的一个选项,具体可以参考https://developers.google.com…。如果有疑问,Mobile Friendly Test可以测试您选择的方法是否符合预期,它可以显示页面在爬虫中的显示方式、序列化的HTML内容(JavaScript执行之后)以及渲染过程中出现的任何错误。Mobile Friendly Test地址如下:https://search.google.com/tes…10. 总结当决定用什么方式渲染的时候,要知道我们即将遇到的瓶颈是什么。采用客户端渲染还是服务端渲染将决定你90%的架构设计。一个完美的解决方案通常服务端发送html跟最小的js来完成交互。以下是服务端-客户端渲染的一个总结图:

March 4, 2019 · 1 min · jiezi

超简单的react服务器渲染(ssr)入坑指南

前言本文是基于react ssr的入门教程,在实际项目中使用还需要做更多的配置和优化,比较适合第一次尝试react ssr的小伙伴们。技术涉及到 koa2 + react,案例使用create-react-app创建SSR 介绍Server Slide Rendering,缩写为 ssr 即服务器端渲染,这个要从SEO说起,目前react单页应用HTML代码是下面这样的<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8” /> <link rel=“shortcut icon” href=“favicon.ico” /> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”/> <meta name=“theme-color” content="#000000" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id=“root”></div> <script src="/js/main.js"></script> </body></html>如果main.js 加载比较慢,会出现白屏一闪的现象。传统的搜索引擎爬虫因为不能抓取JS生成后的内容,遇到单页web项目,抓取到的内容啥也没有。在SEO上会吃很多亏,很难排搜索引擎到前面去。React SSR(react服务器渲染)正好解决了这2个问题。React SSR介绍这里通过一个例子来带大家入坑!先使用create-react-app创建一个react项目。因为要修改webpack,这里我们使用react-app-rewired启动项目。根目录创建一个server目录存放服务端代码,服务端代码我们这里使用koa2。目录结构如下:这里先来看看react ssr是怎么工作的。这个业务流程图比较清晰了,服务端只生成HTML代码,实际上前端会生成一份main.js提供给服务端的HTML使用。这就是react ssr的工作流程。有了这个图会更好的理解,如果这个业务没理解清楚,后面的估计很难理解。react提供的SSR方法有两个renderToString 和 renderToStaticMarkup,区别如下:renderToString 方法渲染的时候带有 data-reactid 属性. 在浏览器访问页面的时候,main.js能识别到HTML的内容,不会执行React.createElement二次创建DOM。renderToStaticMarkup 则没有 data-reactid 属性,页面看上去干净点。在浏览器访问页面的时候,main.js不能识别到HTML内容,会执行main.js里面的React.createElement方法重新创建DOM。实现流程好了,我们都知道原理了,可以开始coding了,目录结构如下:create-react-app 的demo我没动过,直接用这个做案例了,前端项目基本上就没改了,等会儿我们服务器端要使用这个模块。代码如下:import “./App.css”;import React, { Component } from “react”;import logo from “./logo.svg”;class App extends Component { componentDidMount() { console.log(‘哈哈哈~ 服务器渲染成功了!’); } render() { return ( <div className=“App”> <header className=“App-header”> <img src={logo} className=“App-logo” alt=“logo” /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className=“App-link” href=“https://reactjs.org” target="_blank" rel=“noopener noreferrer” > Learn React </a> </header> </div> ); }}export default App;在项目中新建server目录,用于存放服务端代码。为了简化,我这里只有2个文件,项目中我们用的ES6,所以还要配置下.babelrc.babelrc 配置,因为要使用到ES6{ “presets”: [ “env”, “react” ], “plugins”: [ “transform-decorators-legacy”, “transform-runtime”, “react-hot-loader/babel”, “add-module-exports”, “transform-object-rest-spread”, “transform-class-properties”, [ “import”, { “libraryName”: “antd”, “style”: true } ] ]}index.js 项目入口做一些预处理,使用asset-require-hook过滤掉一些类似 import logo from “./logo.svg”; 这样的资源代码。因为我们服务端只需要纯的HTML代码,不过滤掉会报错。这里的name,我们是去掉了hash值的require(“asset-require-hook”)({ extensions: [“svg”, “css”, “less”, “jpg”, “png”, “gif”], name: ‘/static/media/[name].[ext]’});require(“babel-core/register”)();require(“babel-polyfill”);require("./app");public/index.html html模版代码要做个调整,{{root}} 这个可以是任何可以替换的字符串,等下服务端会替换这段字符串。<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8” /> <link rel=“shortcut icon” href="%PUBLIC_URL%/favicon.ico" /> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”/> <meta name=“theme-color” content="#000000" /> <link rel=“manifest” href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id=“root”>{{root}}</div> </body></html>app.js 服务端渲染的主要代码,加载App.js,使用renderToString 生成html代码,去替换掉 index.html 中的 {{root}} 部分import App from ‘../src/App’;import Koa from ‘koa’;import React from ‘react’;import Router from ‘koa-router’;import fs from ‘fs’;import koaStatic from ‘koa-static’;import path from ‘path’;import { renderToString } from ‘react-dom/server’;// 配置文件const config = { port: 3030};// 实例化 koaconst app = new Koa();// 静态资源app.use( koaStatic(path.join(__dirname, ‘../build’), { maxage: 365 * 24 * 60 * 1000, index: ‘root’ // 这里配置不要写成’index’就可以了,因为在访问localhost:3030时,不能让服务默认去加载index.html文件,这里很容易掉进坑。 }));// 设置路由app.use( new Router() .get(’*’, async (ctx, next) => { ctx.response.type = ‘html’; //指定content type let shtml = ‘’; await new Promise((resolve, reject) => { fs.readFile(path.join(__dirname, ‘../build/index.html’), ‘utfa8’, function(err, data) { if (err) { reject(); return console.log(err); } shtml = data; resolve(); }); }); // 替换掉 {{root}} 为我们生成后的HTML ctx.response.body = shtml.replace(’{{root}}’, renderToString(<App />)); }) .routes());app.listen(config.port, function() { console.log(‘服务器启动,监听 port: ’ + config.port + ’ running~’);});config-overrides.js 因为我们用的是create-react-app,这里使用react-app-rewired去改下webpack的配置。因为执行npm run build的时候会自动给资源加了hash值,而这个hash值,我们在asset-require-hook的时候去掉了hash值,配置里面需要改下,不然会出现图片不显示的问题,这里也是一个坑,要注意下。module.exports = { webpack: function(config, env) { // …add your webpack config // console.log(JSON.stringify(config)); // 去掉hash值,解决asset-require-hook资源问题 config.module.rules.forEach(d => { d.oneOf && d.oneOf.forEach(e => { if (e && e.options && e.options.name) { e.options.name = e.options.name.replace(’[hash:8].’, ‘’); } }); }); return config; }};好了,所有的代码就这些了,是不是很简单了?我们koa2读取的静态资源是 build目录下面的。先执行npm run build打包项目,再执行node ./server 启动服务端项目。看下http://localhost:3030页面的HTML代码检查下:没有{{root}}了,服务器渲染成功!总结相信这篇文章是最简单的react服务器渲染案例了,这里交出github地址,如果学会了,记得给个starhttps://github.com/mtsee/reac… ...

February 27, 2019 · 2 min · jiezi

全新版本仿网易云音乐来啦

前言在前端技术领域中,我们可以切身感受得到技术的更新、变革的速度是非常快的,所以工程师们都会需要时常关注和学习一些新技术、新标准。因为在工作中负责项目的技术栈相比于业界来说,算比较落后了,所以自己动手来开发一个音乐类 web app,可以尝试一些新技术栈,或者往一些特定方向深挖学习。项目开发时间从年末至今,利用工作之余的时间断断续续地开发,主体功能已经大致完成了,接下来也会陆续添加一些新功能上去,也会持续优化代码,在此也做一下记录和总结。项目信息在线体验使用 chrome 移动端调试体验项目地址技术栈vue:vue 2.6, vue-router, vuex, vue-server-rendererwebpack:webpack 4, webpack-dev-middleware, webpack-hot-middlewarenode:express 4test:karma 4, mocha, sinon-chai, vue/test-utils, eslint整体架构后端 api 是使用 NeteaseCloudMusicApi,提供了非常多接口,并且支持 CORS 跨域。项目分为两个部分,分别是前端,比如 javascript、css、img、components 等;还有服务端,负责请求响应和服务端渲染,所以项目整体架构如图:技术实现项目刚开始使用 vue-cli 初始化,开箱即用的使用体验为我省去了不少繁琐的流程,可以直接上手进行开发。登录态用户登录是首先需要解决的问题,因为许多接口都依赖用户登录态。最终是将 api 服务和项目分成两个子域名:163api.domain.cn // api163music.domain.cn // 项目但是后来发现,请求登录接口成功后,用户 cookie 无法写入到浏览器内,发现原来是 cookie 内的 domain 设置的是 api 子域名,所以导致 163music.domain.cn 下是无法读取到 cookie 的,但是经过调试发现,接口在 set cookie 的时候是并没有设置 domain,解决方案是在 nginx 内加上 proxy_cookie_path 的配置,为 cookie 添加 domain 为 .domain.cn,那么在其他子域名下就能正常读取到 cookie(刚开始设置的是替换 domain,然而不会生效):// nginx.confserver { listen 80; server_name 163api.domain.cn; location / { proxy_pass http://127.0.0.1:3000/; proxy_cookie_path ^(.+)$ “$1; domain=domain.cn”; }}webpack在项目开始初期,一切都是那么的和谐,可以欢腾畅快的开发。开发到中期功能都完成的差不多时候决定添加 ssr 了。vue-cli 3 是可以通过配置文件 vue.config.js 来实现自定义的 webpack 配置,在加入了 ssr 相关配置之后,就可以成功构建打包了,但我希望代码能够实时重载和模块热替换,不然开发效率会比较低下。然后,在尝试了一些改造方案(一番挣扎)之后,还是觉得不能够很灵活地实现,我决定重新搭建环境 Orz主要的 webpack 配置是参考 vue-cli,node 代码主要参考官方 demo,当代码编写好后就尝试运行了,结果当然是…满屏红色报错。因为官方 demo 使用的 webpack 3,所以有些配置需要更新,还有一些依赖随版本升级也需要更新调用方法等等。但值得高兴的是,错误提示都基本是准确的,比如:// 需要提供 mode 选项The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.// 配置迁移,需要使用新配置选项Error: webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead.还有可能会缺少各种 loader,需要安装各种依赖。确保构建流程正常后,就可以打开浏览器内看效果了,但又会发现这样的报错:window is not defined.原因是因为一个轮播插件内包含 window,而在 node 环境内是没有这个全局变量的,所以导致了这个报错。亦或者访问其他浏览器内置对象时,也会出现这样类似的报错,所以需要确保代码和插件都可以在 node 环境下正常运行。因为轮播插件本身是支持 ssr 模式的,所以修改完代码后即可正常运行。最后,项目中共有四份 webpack 配置和两个构建脚本。在开发环境下,搭配 webpack-dev-middleware 和 webpack-hot-middleware 来实现了代码的热加载。在生产环境构建时,因为希望能清晰看到错误和警告,也想对构建耗时进行统计,所以将构建脚本拎出来。build├── build-production.js // 生产环境构建脚本├── setup-dev-server.js // 开发环境构建脚本├── webpack.base.config.js // 基本配置├── webpack.client.config.js // 客户端配置├── webpack.server.config.js // 服务端配置└── webpack.test.config.js // 测试配置播放器音乐播放是最主要的核心功能,底层就是使用 audio 标签,并且监听了标签元素的 canplay、timeupdate、ended 事件来分别实现时长计算、更新当前播放进度、下一首播放。因为播放器是可以支持“后台”播放,所以将播放器放到根组件中并且设置隐藏,所以 dom 结构如下:<div id=“app”> <!– audio –> <player></player> <transition :name=“transitionName”> <router-view class=“component”></router-view> </transition> <footBox></footBox></div>组件数据同步是使用 vuex,比如播放的状态、歌曲总时长、当前的播放进度等,当歌曲播放完毕时候需要播放下一首,这里使用的是 eventBus 来做事件触发,它会比较适合这种类似的场景。当用户打开播放页面时,我希望音乐是能够自动播放的,无论用户是从其他入口进来亦或者是直接刷新的时候。自动播放是通过 play() 方法去触发的,前者没有问题,但是后者在调用时就会提示错误,错误意思是需要用户进行手势操作之后才能够播放,然后尝试了模拟点击、静音播放的方案之后发现在 chrome 内依然无效,后来感觉 chrome 这样做是正确的,应该把网站的控制权交给用户,让用户清楚页面到底发生了什么,而不是让用户在一堆标签页里寻找是哪个页面发出了奇怪的声音。更新从播放列表进入播放页后才会自动播放,感谢小伙伴提供解决方案单元测试单元测试也是早期规划的功能之一,开始是参考一些开源项目来搭建,最终选型是 karma + mocha + sinon-chai (官方 demo)。搭建的过程就是摸着石头过河了,其中也经历了一些报错,比如:安装依赖失败、配置文件出错、缺少依赖插件等等,然后接近搭建完成后才发现还有官方文档。不得不说是, cli 的确帮开发者节省了非常多配置、搭建的工作,搭建完成之后就可以根据官方文档来编写用例了,根据官方文档内例子已经可以覆盖到绝大部分场景,比如模拟浏览器渲染、用户点击等等。但同时也发现一个问题,如果项目代码经常发生变更的话,那么之前的测试用例也可能需要重新编写了,想知道大家在项目中是怎么处理或者怎么看待呢?以上是在开发过程中遇到一小部分问题,还有过程当中大部分问题描述和解决方案就不在这里一一展开去讲了,大家如果有问题的地方,欢迎大家私信或者邮件与我交流。总结在项目的开发过程中也参考和使用了很多优秀的开源项目,帮助我快速消化一些功能实现,还有提供了后端 api,不然也没有开发这个项目的灵感;Vue 生态下有丰富、详细的官方文档和活跃的社区,基本上遇到的问题都能够解决,超赞;项目在立项之初可能只是大脑一闪而过的简单想法,再回顾这几个月开发经历,其实过得是比较充实和富有激情的,就是有点费头发 ????;最后,自知项目中还有很多不足的地方,如果您发现有什么问题或者有更好的想法,欢迎 issue 或者 pr。如果您觉得项目有参考和学习的价值,可以在 github 上点个 star,谢谢参考资料NeteaseCloudMusicApivue-awesome-swiperuse nginx to add Domain to a Set-CookieCookies on localhost with explicit domainmini-css-extract-plugin with SSRAutoplay Policy Changes ...

February 26, 2019 · 2 min · jiezi

ssr 客户端下载及使用方法!

GIthub 地址windows 版 直达下载解压windows 8 及以上系统打开 4.0exe 启动客户端win7系统则打开 2.0exe 启动设置方法差不多,服务器 IP 端口 密码 加密方式 都要和搭建时候设置一致.安卓版 直达也是要和服务端搭建过程中自己设置的信息一致…转自博客:MR96.ME —— 玖六先生的自留地

January 29, 2019 · 1 min · jiezi

vueSSR: 从0到1构建vueSSR项目 --- node以及vue-cli3的配置

前言上一次做了路由的相关配置,原本计划今天要做vuex部分,但是想了想,发现vuex单独的客户端部分穿插解释起来很麻烦,所以今天改做服务端部分。服务端部分做完,再去做vuex的部分,这样就会很清晰。vue ssr是分两个端,一个是客户端,一个是服务端。所以要做两个cli3的配置。那么下面就直接开始做吧。修改package.json的命令//package.json :client代表客户端 :server代表服务端//使用VUE_NODE来作为运行环境是node的标识//cli3内置 命令 –no-clean 不会清除dist文件 “scripts”: { “serve:client”: " vue-cli-service serve", “build”:“npm run build:server – –silent && npm run build:client – –no-clean –silent”, “build:client”: “vue-cli-service build”, “build:server”: “cross-env VUE_NODE=node vue-cli-service build”, “start:server”: “cross-env NODE_ENV=production nodemon nodeScript/index” }修改vue.config.js配置添加完相关脚本命令之后,我们开始改造cli3配置。首先要require(‘vue-server-renderer’)然后再根据VUE_NODE环境变量来决定编译的走向以及生成不同的环境清单先做cli3服务端的入口文件// src/entry/server.jsimport { createApp} from ‘../main.js’export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp(context.data) //根据node传过来的路由 来调用router路由的指向 router.push(context.url) router.onReady(() => { //获取当前路由匹配的组件数组。 const matchedComponents = router.getMatchedComponents() //长度为0就是没找到该路由所匹配的组件 //可以路由设置重定向或者传回node node来操作也可以 if (!matchedComponents.length) { return reject({ code: 404 }) } resolve(app) }, reject) })}这里是cli3的配置//vue.config.jsconst ServerPlugin = require(‘vue-server-renderer/server-plugin’),//生成服务端清单 ClientPlugin = require(‘vue-server-renderer/client-plugin’),//生成客户端清单 nodeExternals = require(‘webpack-node-externals’),//忽略node_modules文件夹中的所有模块 VUE_NODE = process.env.VUE_NODE === ’node’, entry = VUE_NODE ? ‘server’ : ‘client’;//根据环境变量来指向入口module.exports = { css: { extract: false//关闭提取css,不关闭 node渲染会报错 }, configureWebpack: () => ({ entry: ./src/entry/${entry}, output: { filename: ‘js/[name].js’, chunkFilename: ‘js/[name].js’, libraryTarget: VUE_NODE ? ‘commonjs2’ : undefined }, target: VUE_NODE ? ’node’ : ‘web’, externals: VUE_NODE ? nodeExternals({ //设置白名单 whitelist: /.css$/ }) : undefined, plugins: [//根据环境来生成不同的清单。 VUE_NODE ? new ServerPlugin() : new ClientPlugin() ] }), chainWebpack: config => { config.resolve .alias .set(‘vue$’, ‘vue/dist/vue.esm.js’) config.module .rule(‘vue’) .use(‘vue-loader’) .tap(options => { options.optimizeSSR = false; return options; }); config.module .rule(‘images’) .use(‘url-loader’) .tap(options => { options = { limit: 1024, fallback:‘file-loader?name=img/[path][name].[ext]’ } return options; }); }}node相关配置用于node渲染 必然要拦截get请求的。然后根据get请求地址来进行要渲染的页面。官方提供了vue-server-renderer插件大概的方式就是 node拦截所有的get请求,然后将获取到的路由地址,传给前台,然后使用router实例进行push再往下面看之前 先看一下官方文档创建BundleRenderercreateBundleRenderer将 Vue 实例渲染为字符串。renderToString渲染应用程序的模板template生成所需要的客户端或服务端清单clientManifest先创建 服务端所需要的模板//public/index.nodeTempalte.html<!DOCTYPE html><html> <head> <meta http-equiv=“X-UA-Compatible” content=“IE=edge”> <meta name=“viewport” content=“width=device-width,initial-scale=1.0”> <link rel=“icon” href="/favicon.ico"> <meta charset=“utf-8”> <title>vuessr</title> </head> <body> <!–vue-ssr-outlet–> </body></html>node部分先创建三个文件index.js //入口proxy.js //代理server.js //主要配置//server.jsconst fs = require(‘fs’);const { resolve } = require(‘path’);const express = require(’express’);const app = express();const proxy = require(’./proxy’);const { createBundleRenderer } = require(‘vue-server-renderer’)//模板地址const templatePath = resolve(__dirname, ‘../public/index.nodeTempalte.html’)//客户端渲染清单const clientManifest = require(’../dist/vue-ssr-client-manifest.json’)//服务端渲染清单const bundle = require(’../dist/vue-ssr-server-bundle.json’)//读取模板const template = fs.readFileSync(templatePath, ‘utf-8’)const renderer = createBundleRenderer(bundle,{ template, clientManifest, runInNewContext: false})//代理相关proxy(app);//请求静态资源相关配置app.use(’/js’, express.static(resolve(__dirname, ‘../dist/js’)))app.use(’/css’, express.static(resolve(__dirname, ‘../dist/css’)))app.use(’/font’, express.static(resolve(__dirname, ‘../dist/font’)))app.use(’/img’, express.static(resolve(__dirname, ‘../dist/img’)))app.use(’.ico’, express.static(resolve(__dirname, ‘../dist’)))//路由请求app.get(’’, (req, res) => { res.setHeader(“Content-Type”, “text/html”) //传入路由 src/entry/server.js会接收到 使用vueRouter实例进行push const context = { url: req.url } renderer.renderToString(context, (err, html) => { if (err) { if (err.url) { res.redirect(err.url) } else { res.status(500).end(‘500 | 服务器错误’); console.error(${req.url}: 渲染错误 ); console.error(err.stack) } } res.status(context.HTTPStatus || 200) res.send(html) })})module.exports = app;//proxy.jsconst proxy = require(‘http-proxy-middleware’);function proxyConfig(obj){ return { target:’localhost:8081’, changeOrigin:true, …obj }}module.exports = (app) => { //代理开发环境 if (process.env.NODE_ENV !== ‘production’) { app.use(’/js/main*’, proxy(proxyConfig())); app.use(’/hot-update’,proxy(proxyConfig())); app.use(’/sockjs-node’,proxy(proxyConfig({ws:true}))); }}//index.jsconst app = require(’./server’)app.listen(8080, () => { console.log(’\033[42;37m DONE \033[40;33m localhost:8080 服务已启动\033[0m’)})做完这一步之后,就可以预览基本的服务渲染了。后面就只差开发环境的配置,以及到node数据的传递(vuex) npm run build npm run start:server 打开localhost:8080 F12 - Network - Doc 就可以看到内容最终目录结构|– vuessr |– .gitignore |– babel.config.js |– package-lock.json |– package.json |– README.md |– vue.config.js |– nodeScript //node 渲染配置 | |– index.js | |– proxy.js | |– server.js |– public//模板文件 | |– favicon.ico | |– index.html | |– index.nodeTempalte.html |– src |– App.vue |– main.js |– router.config.js//路由集合 |– store.config.js//vuex 集合 |– assets//全局静态资源源码 | |– 备注.txt | |– img | |– logo.png |– components//全局组件 | |– Head | |– index.js | |– index.scss | |– index.vue | |– img | |– logo.png |– entry//cli3入口 | |– client.js | |– server.js | |– 备注.txt |– methods//公共方法 | |– 备注.txt | |– mixin | |– index.js |– pages//源码目录 | |– home | | |– index.js | | |– index.scss | | |– index.vue | | |– img | | | |– flow.png | | | |– head_portrait.jpg | | | |– logo.png | | | |– vuessr.png | | |– vue | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vueCli3 | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vueSSR | | | |– index.js | | | |– index.scss | | | |– index.vue | | |– vuex | | |– index.js | | |– index.scss | | |– index.vue | |– router//路由配置 | | |– index.js | |– store//vuex配置 | |– all.js | |– gather.js | |– index.js |– static//cdn资源 |– 备注.txtgithub欢迎watch ...

January 28, 2019 · 3 min · jiezi

一个免费送50美元的活动!!!

新用户注册vultr,现在可获得50元美元,限时活动,心动不如行动!!活动地址:https://www.vultr.com/?ref=78…

January 27, 2019 · 1 min · jiezi

vueSSR: 从0到1构建vueSSR项目 --- 路由的构建

vue开发依赖的相关配置Vue SSR 指南今天先做客户端方面的配置,明天再做服务端的部分。那么马上开始吧~修改部分代码脚手架生成的代码肯定是不适合我们所用的 所以要修改一部分代码//App.vue<template> <div id=“app”> <router-view></router-view> </div></template><script>export default { name: ‘app’}</script><style> html,body,#app,#app>*{ width: 100%; height: 100%; } body{ font-family: ‘Avenir’, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; font-size: 16px; margin: 0; overflow-x: hidden; } img{ width: 200px; }</style>修改main.js为什么要这么做?为什么要避免状态单例main.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。main.js 简单地使用 export 导出一个 createApp 函数:import Vue from ‘vue’;Vue.config.productionTip = false;import App from ‘./App.vue’;//router store 实例//这么做是避免状态单例export function createApp() { const app = new Vue({ //挂载router,store render: h => h(App) }) //暴露app实例 return { app };}添加Vue.config.js配置webpack的入口文件有两个,一个是客户端使用,一个是服务端使用。为何这么做?今天只做客户端部分。 src/vue.config.js module.exports = { css: { extract: false//关闭提取css,不关闭 node渲染会报错 }, configureWebpack: () => ({ entry: ‘./src/entry/client’ }) } app.$mount(’#app’)根目录创建 entry 文件夹,以及webpack入口代码 mdkir entry cd entry 创建 入口文件 client.js 作为客户端入口文件。 server,js 作为服务端端入口文件。 //先创建不做任何配置 entry/client.js import { createApp } from ‘../main.js’; const { app } = createApp(); app.$mount(’#app’);路由和代码分割官方说明的已经很清楚了,我就不做过多介绍了,下面直接展示代码添加新路由,这里将存放pages的相关路由src/pages/router/index.js/** * * @method componentPath 路由模块入口 * @param {string} name 要引入的文件地址 * @return {Object} /function componentPath (name = ‘home’){ return { component:() => import(../${name}/index.vue) }}export default [ { path: ‘/home’, …componentPath(), children: [ { path: “vue”, name: “vue”, …componentPath(‘home/vue’) }, { path: “vuex”, name: “vuex”, …componentPath(‘home/vuex’) }, { path: “vueCli3”, name: “vueCli3”, …componentPath(‘home/vueCli3’) }, { path: “vueSSR”, name: “vueSSR”, …componentPath(‘home/vueSSR’) } ] }]src/router.config.js作为路由的总配置 易于管理//路由总配置 import Vue from ‘vue’; import VueRouter from ‘vue-router’; Vue.use(VueRouter); //为什么采用这种做法。 //如果以后有了别的大模块可以单独开个文件夹与pages平级 //再这里导入即可。这样易于管理 // pages import pages from ‘./pages/router’; export function createRouter() { return new VueRouter({ mode: ‘history’, routes: [ { path: “”, redirect: ‘/home/vue’ }, …pages ] }) }更新main.jsimport Vue from ‘vue’;Vue.config.productionTip = false;import App from ‘./App.vue’;+ import { createRouter } from ‘./router.config.js’//router store 实例//这么做是避免状态单例export function createApp() {+ const router = createRouter() const app = new Vue({+ router, render: h => h(App) }) //暴露app,router实例 return { app, router };}更新 client.js由于使用的路由懒加载,所以必须要等路由提前解析完异步组件,才能正确地调用组件中可能存在的路由钩子。// client.jsimport { createApp } from ‘../main.js’;const { app, router } = createApp();router.onReady( () => { app.$mount(’#app’);})最近有点事情 vuex 部分后续再做 ...

January 25, 2019 · 2 min · jiezi

vps 一键安装谷歌BBR加速

Google 开源了其 TCP BBR 拥塞控制算法,并提交到了 Linux 内核,从 4.9 开始,Linux 内核已经用上了该算法。部署了最新版内核,开启TCP BBR 加速的 VPS,网速可以提升几个数量级。使用root用户登录服务器,执行以下命令:wget –no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh && chmod +x bbr.sh && ./bbr.sh安装大概需要两分钟,安装完成后会提示是否重启服务器输入 y 重启服务器重启需要一般只要几秒钟重新连接服务器,验证是否成功安装最新内核并开启 BBR 加速,执行命令:uname -r内核版本显示为最新版就表示 成功了5 $ 服务器优化后 Youtube 可以达到 4K 画质..博客:Mr96.me-玖六先生的自留地

January 25, 2019 · 1 min · jiezi

vueSSR: 从0到1构建vueSSR项目 --- 初始化

开始初始化 npm install -g @vue/cli nodemon nodemon 检测目录文件更改时,来重启基于node开发的程序 vue create vuessr 我选的附带 babel,eslint cd vuessr 创建文件以及文件夹 type null > vue.config.js //node相关配置文件 mkdir nodeScript cd nodeScript type null > index.js type null > proxy.js type null > server.js 进入src目录 //目录初始化 cd ../src type null > router.config.js //路由配置 mkdir pages //项目展示页面主要目录 cd pages mkdir router mkdir entry //vue-cli3 entry的相关配置入口 vueSSR需要。 mkdir static/js //gulp提取框架,插件等几年不动的源码整合后存放于cdn服务器 mkdir static/css //gulp提取整合初始化的样式表,存放的位置 mkdie methods //vue等全局代码的存放比如拦截器 use mixin 兼容函数 //安装依赖 npm install –save-dev sass-loader npm-run-all npm运行多个命令 -s 是顺序 -p是并行 cross-env 可以修改node环境变量 webpack-node-externals 忽略node_modules文件夹中的所有模块 vue-server-renderer 不解释修改eslint配置package.json eslintConfig rules 这个对象下面添加,cli的eslint附带以下的配置 所以手动关闭下。 “no-console”: 0, “no-unused-vars”: 0, “no-undef”: 0如果你觉得eslint警告很烦,那么可以 vue.config.js module.exports = { …, lintOnSave:false, … }明天开始配置 vue-router vuex entry 相关github欢迎watch ...

January 24, 2019 · 1 min · jiezi

vue ssr 实现方式学习笔记

为什么要写本文呢,话说现在 vue-ssr 官网上对 vue 服务端渲染的介绍已经很全面了,包括各种服务端渲染框架比如 Nuxt.js 、 集成 Koa 和vue-server-renderer 的 node.js 框架 egg.js,都有自己的官网和团队在维护,文档真是面面俱到功能强大,但是,我个人在刚开始看这些资料的时候,总是忍不住发起灵魂三问:“我是谁?我在哪?我在干什么?”,提前没有相关知识的人开始学这些,肯定是要走一些弯路或者卡在某个点一段时间的,所以我想把我的学习经验做下总结,一方面方便自己以后查阅,一方面也会在文中加一些针对官网上没有细说的点的理解,希望能帮助你减少些学习成本,毕竟这是一个知识共享的时代嘛。本文不涉及到源码解析,主要讲解如何实现 vue 的服务端渲染,比较适合 vue-ssr 小白阅读,下面我们进入正文:先说下基本概念:ssr 的全称是 server side render,服务端渲染,vue ssr 的意思就是在服务端进行 vue 的渲染,直接对前端返回带有数据,并且是渲染好的HTML页面;而不是返回一个空的HTML页面,再由vue 通过异步请求来获取数据,再重新补充到页面中。这么做的最主要原因,就是搜索引擎优化,也就是SEO,这更利于网络爬虫去爬取和收集数据。为什么这样就有利于网络爬虫爬取呢?这里简单说一下爬虫的爬取方式,爬虫通过访问 URL 获取一个页面后,会获取当前HTML中已存在的数据,也可以理解为把拿到的 HTML 页面转为了字符串内容,然后解析、存储这些内容,但是如果页面中有些数据是通过异步请求获得的,那么爬虫是不会等待异步请求返回之后才结束对页面数据的解析的,这样就会没有爬取到这部分数据,很不利于其他搜索引擎的收录。这也就是为什么单页面网站是不具备良好的SEO效果的,因为单页面返回的就是一个基本为空的 HTML 文件,里面就一个带有ID的元素等待挂载而已,页面的内容都是通过 js 后续生成的,比如这样:<!DOCTYPE html><html lang=“en”> <head><title>Hello</title></head> <body><div id=“app”></div></body> <script src=“bundle.js”></script></html>但对于很多公司来说,公司的产品是希望能被百度、谷歌等搜索引擎收录之后,进行排名,进一步的被用户搜索到,能更利于品牌的推广、流量变现等操作,要实现这些,就必须保证产品的网页是能够被网络爬虫爬取到的,显然一个完整的带有全部数据的页面更利于爬虫的爬取,当然现在也有很多方法可以去实现针对页面异步数据的爬取,github 上也开源了很多的爬虫代码,但是这显然对于爬虫来说更加的不友好、成本更高。SSR 当然也是有着其他的好处的,比如首屏页面加载速度更快,用户等待时间更短等,其他更多概念可以查看官网 https://ssr.vuejs.org/zh/ ,这些官网上都有介绍。代码实现下面我们结合官网上的代码,做一下代码实操,来加深下理解:在官网中,提供了一个使用模块 vue-server-renderer 简单实现 vue 服务端渲染的示例: 新建一个文件夹vue-ssr-demo,进入其中执行如下命令:// 安装模块 npm install vue vue-server-renderer –save创建文件 server.js// vue-ssr-demo/server.js 示例代码//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ template: “<div>hello world</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer();//第三步,将vue渲染为HTMLrenderer.renderToString(app, (err, html)=>{ if(err){ throw err; } console.log(html);});保存以上代码后,在 vue-ssr-demo 文件夹下打开命令行工具,执行 node server.js 命令,可得到如下 HTML 内容:➜ vue-ssr-demo node server.js <div data-server-rendered=“true”>hello world</div>好,上面的例子中我们已经让 vue 在服务端,也就是 node 环境下运行起来了,到这里其实已经实现了 vue 的服务端渲染了。可是,实际项目中使用哪有这么简单,起码数据还没渲染啊,那接下来我们看看如何渲染数据:vue-ssr 渲染数据的方式有两种,我们先看下第一种:// server.jsconst data_vue = { word: ‘Hello World!’};//第一步,创建vue实例const Vue = require(‘vue’);//vue 实例化过程中插入数据const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer();//第三步,将vue渲染为HTMLrenderer.renderToString(app, (err, html)=>{ if(err){ throw err; } console.log(html);}); 第一种方式,在创建 vue 实例时,将需要的数据传入 vue 的模板,使用方法与客户端 vue 一样;运行 server.js 结果如下,数据 data_vue 已经插入到 vue 模板里面了:➜ vue-ssr-demo node server.js<div data-server-rendered=“true”>Hello World!</div> 第二种,模板插值,这里我们也直接先放代码:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个 renderer 实例const renderer = require(‘vue-server-renderer’).createRenderer({ template: “<!–vue-ssr-outlet–><div>{{people}}</div>”});//第三步,将vue渲染为HTMLrenderer.renderToString(app, data_tpl, (err, html)=>{ if(err){ throw err; } console.log(html);});这里我们增加了数据 data_tpl,你会发现,在 renderToString 方法中传入了这个参数,那么这个参数作用在哪里呢?这就要看下官网中关于 createRenderer 和 renderToString 方法的介绍了,createRenderer: 使用(可选的)选项创建一个 Renderer 实例。 const { createRenderer } = require(‘vue-server-renderer’) const renderer = createRenderer({ / 选项 / })在选项中,就有一个参数叫 template,看官网怎么说的:template: 为整个页面的 HTML 提供一个模板。此模板应包含注释 <!–vue-ssr-outlet–>,作为渲染应用程序内容的占位符。为整个页面的 HTML 提供一个模板。此模板应包含注释 <!–vue-ssr-outlet–>,作为渲染应用程序内容的占位符。模板还支持使用渲染上下文 (render context) 进行基本插值:使用双花括号 (double-mustache) 进行 HTML 转义插值 (HTML-escaped interpolation);使用三花括号 (triple-mustache) 进行 HTML 不转义插值 (non-HTML-escaped interpolation)。根据介绍,在创建 renderer 实例时,可以通过 template 参数声明一个模板,这个模板用来干嘛呢?就用来挂载 vue 模板渲染完成之后生成的 HTML。这里要注意一下,当创建 renderer 实例时没有声明 template 参数,那么默认渲染完就是 vue 模板生成的 HTML;当创建 renderer 实例时声明了 template 参数,一定要在模板中增加一句注释 “<!–vue-ssr-outlet–>” 作为 vue 模板插入的占位符,否则会报找不到插入模板位置的错误。再次运行 server.js ,结果如下,vue 模板已成功插入,且 template 模板中的 {{people}} 变量也因在 renderToString 方法中第二位参数的传入,显示了数据:➜ vue-ssr-demo node server.js<div data-server-rendered=“true”>Hello World!</div><div>Hello People!</div>如果我们把 template 换成一个 HTML 页面的基本架构,来包裹 vue 模板,是不是就能得到一个完整页面了呢?我们来试一下:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};//第一步,创建vue实例const Vue = require(‘vue’);const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>”});//第二步,创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer({ template: &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt;&lt;title&gt;Hello&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;!--vue-ssr-outlet--&gt;&lt;div&gt;{{people}}&lt;/div&gt; &lt;/body&gt; &lt;/html&gt;});//第三步,将vue渲染为HTMLrenderer.renderToString(app, data_tpl, (err, html)=>{ if(err){ throw err; } console.log(html);});运行 server.js ,结果如下,我们得到了一个完整的 HTML 页面,且成功插入了数据:➜ vue-ssr-demo node server.js<!DOCTYPE html><html lang=“en”> <head><title>Hello</title></head> <body> <div data-server-rendered=“true”>Hello World!</div><div>Hello People!</div> </body></html>好,现在页面生成了,该怎么显示呢?这里我们借助下框架 Koa 实现,先来安装:npm install koa -S然后修改 server.js ,如下:const data_vue = { word: ‘Hello World!’};const data_tpl = { people: ‘Hello People!’};const Koa = require(‘koa’);//创建 koa 实例const koa = new Koa();const Vue = require(‘vue’);//创建一个rendererconst renderer = require(‘vue-server-renderer’).createRenderer({ template: &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt;&lt;title&gt;Hello&lt;/title&gt;&lt;/head&gt; &lt;body&gt; &lt;!--vue-ssr-outlet--&gt;&lt;div&gt;{{people}}&lt;/div&gt; &lt;/body&gt; &lt;/html&gt;});// 对于任何请求,app将调用该异步函数处理请求:koa.use(async (ctx, next) => { // await next(); //创建vue实例 const app = new Vue({ data: data_vue, template: “<div>{{word}}</div>” }); //将vue渲染为HTML const body = await renderer.renderToString(app, data_tpl); ctx.body = body;});// 在端口3001监听:koa.listen(3001);console.log(‘app started at port 3001…’);运行 server.js :➜ vue-ssr-demo node server.jsapp started at port 3001…然后打开浏览器,输入网址 http://localhost:3001/ ,即可看到运行后的效果。这样就实现了一个简单的服务端渲染项目,但是我们在平常开发的时候,肯定不会这么简单的去构建一个项目,必然会用到一些,比如打包、压缩的工具,这篇就写到这里,下一篇我们尝试使用 webpack 来构建一个 vue 的服务端渲染项目。如有问题,欢迎指正!谢谢! ...

January 18, 2019 · 3 min · jiezi

vue + koa2 + webpack4 构建ssr项目

什么是服务器端渲染 (SSR)?为什么使用服务器端渲染 (SSR)?看这 Vue SSR 指南技术栈vue、vue-router、vuexkoa2webpack4axiosbabel、eslintcss、stylus、postcsspm2目录层次webpack4-ssr-config├── client # 项目代码目录│ ├── assets # css、images等静态资源目录│ ├── components # 项目自定义组件目录│ ├── plugins # 第三方插件(只能在客户端运行)目录,比如 编辑器│ ├── store # vuex数据存储目录│ ├── utils # 通用Mixins目录│ ├── views # 业务视图.vue和route路由目录│ ├── app.vue # │ ├── config.js # vue组件、mixins注册,http拦截器配置等等│ ├── entry-client.js # 仅运行于浏览器│ ├── entry-server.js # 仅运行于服务器│ ├── index.js # 通用 entry│ ├── router.js # 路由配置和相关钩子配置│ └── routes.js # 汇聚业务模块所有路由route配置├── config # 配置文件目录│ ├── http # axios封装的http请求│ ├── logger # .vue里this.[log,warn,info,error]和koa2里 logger日志输出│ ├── middle # koa2中间件目录│ │ ├── errorMiddleWare.js # 错误处理中间件│ │ ├── proxyMiddleWare.js # 接口代理中间件│ │ └── staticMiddleWare.js # 静态资源中间件│ ├── eslintrc.conf.js # eslint详细配置│ ├── index.js # server入口│ ├── koa.server.js # koa2服务详细配置│ ├── setup.dev.server.js # koa2开发模式实现hot热更新│ ├── vue.koa.ssr.js # vue ssr的koa2中间件。匹配路由、请求接口生成dom,实现SSR│ ├── webpack.base.config.js # 基本配置 (base config) │ ├── webpack.client.config.js # 客户端配置 (client config)│ └── webpack.server.config.js # 服务器配置 (server config)├── dist # 代码打包目录├── log # pm2日志输出目录├── node_modules # node包├── .babelrc # babel配置├── .eslintrc.js # eslint配置├── .gitignore # git配置├── app.config.js # 端口、代理配置、webpack配置等等├── constants.js # 存放常量├── favicon.ico # ico图标├── index.template.ejs # index模板├── package.json # ├── package-lock.json # ├── pm2.config.js # 项目pm2配置├── pm2.md # pm2的api文档├── postcss.config.js # postcss配置文件└── README.md # 文档源码结构构建使用 webpack 来打包我们的 Vue 应用程序,参考官方分成3个配置,这里使用的webpack4和官方的略有区别。├── webpack.base.config.js # 基本配置 (base config) ├── webpack.client.config.js # 客户端配置 (client config)├── webpack.server.config.js # 服务器配置 (server config)具体webpack配置代码这里省略…对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。基本流程如下图:项目代码├── entry-client.js # 仅运行于浏览器├── entry-server.js # 仅运行于服务器├── index.js # 通用 entry├── router.js # 路由配置├── routes.js # 汇聚业务模块所有路由route配置index.jsindex.js 是我们应用程序的「通用 entry」,对外导出一个 createApp 函数。这里使用工厂模式为为每个请求创建一个新的根 Vue 实例,从而避免server端单例模式,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染。entry-client.js:客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:import Vue from ‘vue’import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’……const { app, router, store } = createApp()if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) // 客户端和服务端保持一致 store.state.$http = http}router.onReady(() => { …… Promise.all(asyncDataHooks.map(hook => hook({ store, router, route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) // 挂载 app.$mount(’#app’)})entry-server.js:服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,还在此执行服务器端路由匹配和数据预取逻辑。import { createApp } from ‘./index’// 引入http请求import http from ‘./../config/http/http’// 处理ssr期间cookies穿透import { setCookies } from ‘./../config/http/http’// 客户端特定引导逻辑……const { app } = createApp()// 这里假定 App.vue 模板中根元素具有 id="app"app.$mount(’#app’)export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() const { url } = context …… // 设置服务器端 router 的位置,路由配置里如果设置过base,url需要把url.replace(base,’’)掉,不然会404 router.push(url) // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { …… // SSR期间同步cookies setCookies(context.cookies || {}) // http注入到rootState上,方便store里调用 store.state.$http = http // 使用Promise.all执行匹配到的Component的asyncData方法,即预取数据 Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, router, route: router.currentRoute, }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 template 选项用于 renderer 时, // 状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) })}router.js、routes.js、store.jsrouter和store也都是工厂模式,routes是业务模块路由配置的集合。routerimport Vue from ‘vue’import Router from ‘vue-router’import routes from ‘./routes’Vue.use(Router)export function createRouter() { const router = new Router({ mode: ‘history’, fallback: false, // base: ‘/ssr’, routes }) router.beforeEach((to, from, next) => { /todo * 做权限验证的时候,服务端和客户端状态同步的时候会执行一次 * 建议vuex里用一个状态值控制,默认false,同步时直接next,因为服务端已经执行过。 * / next() }) router.afterEach((route) => { /todo/ }) return router}routeimport testRoutes from ‘./views/test/routes’import entry from ‘./app.vue’const home = () => import(’./views/home.vue’)const routes = [ { path: ‘/’, component: home }, { path: ‘/test’, component: entry, children: testRoutes },]export default routesstoreimport Vue from ‘vue’import Vuex from ‘vuex’import test from ‘./modules/test’Vue.use(Vuex)export function createStore() { return new Vuex.Store({ modules: { test } })}Http请求http使用Axios库封装/ * Created by zdliuccit on 2019/1/14. * @file axios封装 * export default http 接口请求 * export addRequestInterceptor 请求前拦截器 * export addResponseInterceptor 请求后拦截器 * export setCookies 同步cookie */import axios from ‘axios’const currentIP = require(‘ip’).address()const appConfig = require(’./../../app.config’)const defaultHeaders = { Accept: ‘application/json, text/plain, /; charset=utf-8’, ‘Content-Type’: ‘application/json; charset=utf-8’, Pragma: ’no-cache’, ‘Cache-Control’: ’no-cache’,}Object.assign(axios.defaults.headers.common, defaultHeaders)if (!process.browser) { axios.defaults.baseURL = http://${currentIP}:${appConfig.appPort}}const methods = [‘get’, ‘post’, ‘put’, ‘delete’, ‘patch’, ‘options’, ‘request’, ‘head’]const http = {}methods.forEach(method => { http[method] = axios[method].bind(axios)})export const addRequestInterceptor = (resolve, reject) => { if (axios.interceptors.request.handlers.length === 0) axios.interceptors.request.use(resolve, reject)}export const addResponseInterceptor = (resolve, reject) => { if (axios.interceptors.response.handlers.length === 0) axios.interceptors.response.use(resolve, reject)}export const setCookies = Cookies => axios.defaults.headers.cookie = Cookiesexport default httpstore中已经注入到rootState,使用如下:loading({ commit, rootState: { $http } }) { return $http.get(‘path’).then(res => { … }) }在config.js中,把http注册到vue的原型链和配置request、response的拦截器import Vue from ‘vue’// 引入http请求插件import http from ‘./../config/http’// 引入log日志插件import { addRequestInterceptor, addResponseInterceptor } from ‘./../config/http/http’import titleMixin from ‘./utils/title’// 引入log日志插件import vueLogger from ‘./../config/logger/vue-logger’// 注册插件Vue.use(http)Vue.use(vueLogger)Vue.mixin(titleMixin)// request前自动添加api配置addRequestInterceptor( (config) => { /统一加/api前缀/ config.url = /api${config.url} return config }, (error) => { return Promise.reject(error) })// http 返回response前处理addResponseInterceptor( (response) => { /*todo 在这里统一前置处理请求响应 / return Promise.resolve(response.data) }, (error) => { / * todo 统一处理500、400等错误状态 * 这里reject下,交给entry-server.js的处理 */ const { response, request } = error return Promise.reject({ code: response.status, data: response.data, method: request.method, path: request.path }) })这样,.vue中间中直接调用this.$http.get()、this.$http.post()…cookies穿透在ssr期间我们需要截取客户端的cookie,保持用户会话唯一性。在entry-server.js中使用setCookies方法,传入的参数是从context上获取。…… // SSR期间同步cookies setCookies(context.cookies || {})……在vue.koa.ssr.js代码中往context注入cookie…… const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie }……其他title处理参考官方用到全局变量的第三方插件、组件如何处理等等流式渲染预渲染……还有很多优化、深坑,看看官方文档、踩踩就知道了Koa官方使用express框架。express虽然现在也支持async、await,不过独爱koa。koa主文件// 引入相关包和中间件等等const Koa = require(‘koa’)…const appConfig = require(’./../app.config’)const uri = http://${currentIP}:${appConfig.appPort}// koa serverconst app = new Koa()// 定义中间件,const middleWares = [ ……]middleWares.forEach((middleware) => { if (!middleware) { return } app.use(middleware)})// vue ssr处理vueKoaSSR(app, uri)// http代理中间件app.use(proxyMiddleWare())console.log(\n&gt; Starting server... ${uri} \n)// 错误处理app.on(’error’, (err) => { // console.error(‘Server error: \n%s\n%s ‘, err.stack || ‘’)})app.listen(appConfig.appPort)vue.koa.ssr.jsvue koa2 ssr中间件开发模式直接使用setup.dev.server.jswebpack hot热更新生产模块直接读取dist目录的文件路由匹配匹配proxy代理配置,接口请求进入proxyMiddleWare.js接口代理中间件非接口进入render(),返回htmlconst fs = require(‘fs’)const path = require(‘path’)const LRU = require(’lru-cache’)const { createBundleRenderer } = require(‘vue-server-renderer’)const isProd = process.env.NODE_ENV === ‘production’const proxyConfig = require(’./../app.config’).proxyconst setUpDevServer = require(’./setup.dev.server’)module.exports = function (app, uri) { const renderData = (ctx, renderer) => { const context = { url: ctx.url, title: ‘Vue Koa2 SSR’, cookies: ctx.request.headers.cookie } return new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => { if (err) { return reject(err) } resolve(html) }) }) } function createRenderer(bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { cache: LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), runInNewContext: false })) } function resolve(dir) { return path.resolve(process.cwd(), dir) } let renderer if (isProd) { // prod mode const template = fs.readFileSync(resolve(‘dist/index.html’), ‘utf-8’) const bundle = require(resolve(‘dist/vue-ssr-server-bundle.json’)) const clientManifest = require(resolve(‘dist/vue-ssr-client-manifest.json’)) renderer = createRenderer(bundle, { template, clientManifest }) } else { // dev mode setUpDevServer(app, uri, (bundle, options) => { try { renderer = createRenderer(bundle, options) } catch (e) { console.log(’\nbundle error’, e) } } ) } app.use(async (ctx, next) => { if (!renderer) { ctx.type = ‘html’ return ctx.body = ‘waiting for compilation… refresh in a moment.’; } if (Object.keys(proxyConfig).findIndex(vl => ctx.url.startsWith(vl)) > -1) { return next() } let html, status try { status = 200 html = await renderData(ctx, renderer) } catch (e) { console.log(’\ne’, e) if (e.code === 404) { status = 404 html = ‘404 | Not Found’ } else { status = 500 html = ‘500 | Internal Server Error’ } } ctx.type = ‘html’ ctx.status = status ? status : ctx.status ctx.body = html })}setup.dev.server.jskoa2的webpack热更新配置和相关中间件的代码,这里就不贴出来了,和express略有区别。部署Pm2简介PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。pm2.config.js配置如下module.exports = { apps: [{ name: ‘ml-app’, // app名称 script: ‘config/index.js’, // 要运行的脚本的路径。 args: ‘’, // 由传递给脚本的参数组成的字符串或字符串数组。 output: ‘./log/out.log’, error: ‘./log/error.log’, log: ‘./log/combined.outerr.log’, merge_logs: true, // 集群的所有实例的日志文件合并 log_date_format: “DD-MM-YYYY”, instances: 4, // 进程数 1、数字 2、‘max’根据cpu内核数 max_memory_restart: ‘1G’, // 当内存超过1024M时自动重启 watching: true, env_test: { NODE_ENV: ‘production’ }, env_production: { NODE_ENV: ‘production’ } }],}构建生产代码npm run build 构建生产代码pm2启动服务初次启动pm2 start pm2.config.js –env production # production 对应 env_productionorpm2 start ml-apppm2的用法和参数说明可以参考pm2.md,也可参考PM2实用入门指南Nginx在pm2基础上,Nginx配置upstream实现负载均衡在http节点下,加入upstream节点。upstream server_name { server 172.16.119.198:8018 max_fails=2 fail_timeout=30s; server 172.16.119.198:8019 max_fails=2 fail_timeout=30s; server 172.16.119.198:8020 max_fails=2 fail_timeout=30s; …..}将server节点下的location节点中的proxy_pass配置为:http:// + server_name,即“ http://server_name”.location / { proxy_pass http://server_name; proxy_set_header Host localhost; proxy_set_header X-Forwarded-For $remote_addr}详细配置参考文档如果应用服务是域名子路径ssr的话,需要注意如下location除了需要设置匹配/ssr规则之外,还需设置接口、资源的前缀比如(/api,/dist) location ~ /(ssr|api|dist) {…}vue的路由也该设置base:’/ssr’entry-server.js里router.push(url)这里,url应该把/ssr去掉,即router.push(url.replace(’/ssr’,’’’))参考文档vue官方文档koanginxpm2Demo地址 服务器带宽垃圾,将就看看。 git仓库地址还有很多不足,后续慢慢折腾….结束语:生命的价值在于瞎折腾 ...

January 17, 2019 · 6 min · jiezi

vue服务端渲染——Nuxt.js全面配置

Nuxt.js 全面配置nuxt-config: 持续更新中其他系列★ vue-cli3 全面配置<span id=“top”>目录</span>√ 初始化项目√ 环境变量配置√ 打包分析√ 提供全局 scss 变量√ 按需引入 element-ui√ 配置 hard-source-webpack-plugin√ 去除多余 css√ Brotli 压缩<span id=“init”>☞ 初始化项目</span>npx create-nuxt-app <项目名>或npx create-nuxt-app <项目名>▲ 回顶部<span id=“env”>☞ 环境变量配置</span> 可以配置在客户端和服务端共享的环境变量module.exports = { env: { baseUrl: process.env.BASE_URL || ‘http://localhost:3000’ }} 通过以下两种方式来使用 baseUrl 变量通过 process.env.baseUrl通过 context.baseUrl,请参考context api▲ 回顶部<span id=“analyze”>☞ 打包分析</span> package.json 中添加 analyze 命令"analyze": “nuxt build –analyze” 修改 nuxt.config.jsexport default { build: { analyza: { analyzeMode: ‘static’ } }}▲ 回顶部<span id=“scss”>☞ 提供全局 scss 变量</span>方法一:npm i -S @nuxtjs/style-resourcesnpm i -D sass-loader node-sass 修改 nuxt.config.jsexport default { modules: [ ‘@nuxtjs/style-resources’, ], styleResources: { scss: ‘/assets/scss/variable.scss’ }}方法二:npm i -D nuxt-sass-resources-loader sass-loader node-sass 修改 nuxt.config.jsexport default { modules: [ [’nuxt-sass-resources-loader’, [’/assets/scss/variable.scss’]] ], styleResources: { scss: ‘~/assets/scss/variable.scss’ }}▲ 回顶部<span id=“elementui”>☞ 按需引入 element-ui</span>npm i -D babel-plugin-component// oryarn add -D babel-plugin-component 修改 nuxt.config.jsmodule.exports = { plugins: [’@/plugins/element-ui’], build: { babel: { plugins: [ [ ‘component’, { libraryName: ’element-ui’, styleLibraryName: ’theme-chalk’ } ] ] } },} 修改 plugins/element-ui.jsimport Vue from ‘vue’import { Button, Loading, Notification, Message, MessageBox} from ’element-ui’import lang from ’element-ui/lib/locale/lang/zh-CN’import locale from ’element-ui/lib/locale’// configure languagelocale.use(lang)// setVue.use(Loading.directive)Vue.prototype.$loading = Loading.serviceVue.prototype.$msgbox = MessageBoxVue.prototype.$alert = MessageBox.alertVue.prototype.$confirm = MessageBox.confirmVue.prototype.$prompt = MessageBox.promptVue.prototype.$notify = NotificationVue.prototype.$message = Message// import componentsVue.use(Button);// or// Vue.component(Button.name, Button)▲ 回顶部<span id=“hard”>☞ 配置 hard-source-webpack-plugin</span>npm i -D hard-source-webpack-plugin 修改 nuxt.config.jsmodule.exports = { build: { extractCSS: true, extend(config, ctx) { if (ctx.isDev) { config.plugins.push( new HardSourceWebpackPlugin({ cacheDirectory: ‘.cache/hard-source/[confighash]’ }) ) } } }}▲ 回顶部<span id=“removecss”>☞ 去除多余 css</span>npm i –D glob-all purgecss-webpack-plugin 若安装失败,请先用管理员身份安装以下全局依赖npm install –global windows-build-tools或yarn global add windows-build-tools 修改 nuxt.config.jsconst PurgecssPlugin = require(‘purgecss-webpack-plugin’)const glob = require(‘glob-all’)const path = require(‘path’)const resolve = dir => path.resolve(__dirname, dir);module.exports = { build: { extractCSS: true, extend(config, ctx) { if (!ctx.isDev) { config.plugins.push( new PurgecssPlugin({ paths: glob.sync([ resolve(’./pages//*.vue’), resolve(’./layouts//.vue’), resolve(’./components/**/.vue’) ]), extractors: [ { extractor: class Extractor { static extract(content) { const validSection = content.replace( /<style([\s\S]*?)</style>+/gim, "" ); return validSection.match(/[A-Za-z0-9-_:/]+/g) || []; } }, extensions: [‘vue’] } ], whitelist: [‘html’, ‘body’, ’nuxt-progress’] }) ) } } }}▲ 回顶部<span id=“brotli”>☞ Brotli 压缩</span>npm i shrink-ray-current 若安装失败,请先用管理员身份安装以下全局依赖npm install –global windows-build-tools或yarn global add windows-build-tools 修改 nuxt.config.jsexport default { render: { http2: { push: true }, compressor: shrinkRay() }}▲ 回顶部 ...

January 15, 2019 · 2 min · jiezi

Vue SSR学习

最近学习了下vue ssr方面知识,学习过程为先大致过了一遍官方VUE SSR文档,一路看下来还是有些懵逼的,跟着官方步骤下来我是没办法搭一个简单的vue-ssr的demo,所以网上找了下tutorial强烈推荐从0开始,搭建Vue2.0的SSR服务端渲染,跟着作者一步步搭建,可以循序渐进的理解vue ssr是怎么实现的然后就是自己回到官网,不用脚手架一步步用webpack搭建vue基础环境,这里主要参考vue-loader和webapck文档,ssr方面内的配置主要参考了vue ssr官方例子hackernews的配置与实现。先说明下采用的各个主要package的版本如下:vue 2.5.21vue-server-renderer 2.5.21webpack 4.27.1vue-loader 15.4.2然后分享下学习中碰到的问题,具体搭建和学习参见demo和上面步骤2文章链接,作者写的很好。终端代理设置在我把官方hackerners的例子下下来,装好依赖跑起来,控制台显示成功了,但是代开浏览器,却一直不显示页面,后来在issue中查到,你运行的Terminal需要翻墙,具体可以见issue。这里是解决问题,但是后面自己的demo用axios请求出了问题,自己mock的一些数据在浏览器中输入http://localhost:3000/api/user是可以出来数据的,但是起的服务却老是出错,后来查到因为我设置了终端的proxy,axios会自动检测到并使用他,所以他请求的url一直是,所以遇到一个连环问题,解决的话参考axios这个issue去掉终端的proxydocument is not defined服务器环境是没有document的,webpack如果用了MiniCssExtractPlugin会提取css会用到document,所以在服务器端的webpack配置中需要去掉,这一部分参考了这篇文章,也可以参考我的webpack配置看一看css 打包不到一个文件webpack4中使用splitchunks这个配置来优化和做缓存策略,然而我怎么配置都css文件不会被打包成一个,搜索了很久终于看到MiniCssExtractPlugin的issue中解决,这样就打包到一个css文件,后来持续关注到知道这是webpack的一个bug。开发环境热重载这里我用的是官方的方法,照抄过来的[逃:)],基本原来我觉得应该就是利用webpack-hot-middleware和webpack-dev-middleware来实现的,因为webpack-dev-middleware是一个express中间件,它实现fs基于内存,提高了编译读取速度;然后通过watch文件的变化,动态编译结束欢迎讨论和大佬的指导,放上github地址,还有线上地址

December 28, 2018 · 1 min · jiezi

【vuejs开源】vuejs ssr 完美解决方案「vuex,axios,stylus,proxy 」

vuejs ssr 完美解决方案「vuex,axios,stylus,proxy 」博客地址:https://shudong.wang/10227.html???????? vue ssr 完美解决方案,特性: 「vuex 」 「router 」 「 proxy 」 「 vuex 」 「 axios 」 「 ssr 」「 stylus 」「 bootstarp」 blog; ???????????? 都来到这里了,点个star呗!????????????????项目地址:https://github.com/wsdo/vue-s…博客:博客特性vuexrouterproxyvuexaxiosssrstylusbootstarpdemo blog已经完善的blog安装npm i开发模式npm run dev打包项目npm run build打包后运行npm start项目部署npm run pm2Nginx 反向代理跨域代理const proxyTable = { ‘/v1’: { target: config.baseUrl, changeOrigin: true // pathRewrite: { // ‘^/api’: ‘/api’ // } }}界面:

December 26, 2018 · 1 min · jiezi

Google Cloud 释放外网IP

Google Cloud 释放外网IP 来自Google Cloud 一年免费版的受益者近几日突然发现上推的时候,访问情况不稳,时断时续,而且出现了访问速度急速下降的问题,就比如说在推特看小视频,以前缓冲几乎是秒级别,现在就会卡顿顿的,看的不爽,自然心情也就不好。 Google Cloud 比 vultr 好一点的地方是服务器的IP可以单独配置,这样就可以既保留我服务器上的资源也可以同时避免被墙的尴尬。 第一步 在自己的 Compute Engine 中修改服务器的外网IP为 “空” 或者 “临时” ,目的是为了释放外网IP做准备。⬇点击修改按钮,进入修改模式⬇修改外部IP为“空”或“临时”⬇记得在底部点击保存按钮,这时外部IP是无占用状态第二步 释放外网IP⬇进入VPC网络 ⬇点击选中需要释放的IP,点击右上角“释放” 第三步 重新创建IP,回到第一步骤修改的状态,点击外部IP之后有一项是创建外部IP,创建即可。

December 24, 2018 · 1 min · jiezi

Shadowsocks 1分钟 配置

Shadowsocks 1分钟部署服务端安装shadowsocks软件pip install –upgrade pippip install shadowsocks创建 /etc/shadowsocks.json{ “server_port”:443, “local_address”:“127.0.0.1”, “local_port”:1080, “password”:“zhaojunlike”, “timeout”:60, “method”:“rc4-md5”, “fast_open”:false}启动ssserver -c /etc/shadowsocks.json -d start创建开机自启动服务

December 22, 2018 · 1 min · jiezi

VueJS SSR 后端绘制内存泄漏的相关解决经验

引言Memory Leak 是最难排查调试的 Bug 种类之一,因为内存泄漏是个 undecidable problem,只有开发者才能明确一块内存是不是需要被回收。再加上内存泄漏也没有特定的报错信息,只能通过一定时间段的日志来判断是否存在内存泄漏。大家熟悉的常用调试工具对排查内存泄漏也没有用武之地。当然了,除了专门用于排查内存泄漏的工具(抓取Heap之类的工具)之外。对于不同的语言,各种排查内存泄漏的方式方法也不尽相同。对于 JavaScript 来说,针对不同的平台,调试工具也是不一样的,最常用的恐怕还是 Chrome 自带的各种利器(针对 browser 也好,nodeJS 也好)都有不错的使用体验,网上也有很多使用教程。这次我想给大家介绍的内存泄漏的定位方法,并非工具的使用。而是一些经验的总结,也就是我所知道的 VueJS SSR 中最容易出现内存泄漏的地方,如果大家知道更多 VueJS SSR 内存泄漏点,可以在评论处留言告诉更多的人。难点遇到过 VueJS SSR 内存泄漏的朋友可能知道,针对 VueJS SSR 内存泄漏的排查,与普通 NodeJS 和 Browser 平台相比是要麻烦很多的。如果你使用了 webpack-dev-server 在本地调试,你会发现常用的内存泄漏工具毫无用武之地,因为抓取到的信息不仅包括 VueJS SSR 进程信息,还包含了 Webpack 的进程信息,甚至还有 webpack-dev-server 的各种堆信息。当然了,你也可以通过各种手段来过滤掉无关的信息,从而只剩下 VueJS SSR 的堆信息。我在排查我们组项目内存泄漏的时候,动用了各种常规工具,但最终发现 VueJS SSR 的内存泄漏有很大可能性出现在以下地方,也就说如果,你碰巧也有 VueJS SSR 内存泄漏的问题,先不要使用内存泄漏排查工具,首先从下面几个地方着手,看看是否有内存泄漏的逻辑。可能直击要害,节约时间。可能造成泄漏的位置生命周期处的 beforeCreate/created以下是 VueJS 开发者看过无数次的说明图,我还请大家再多看一遍在官方文档里,有这么一句话:Since there are no dynamic updates, of all the lifecycle hooks, only beforeCreate and created will be called during SSR. This means any code inside other lifecycle hooks such as beforeMount or mounted will only be executed on the client.也就是说 SSR 跟前端绘制一样,也有生命周期,只不过 SSR 的生命周期里只有 beforeCreate 和 created 。所以你需要首先排查你的组件的 beforeCreate 和 created 里面是否有内存泄漏的代码,或者他们是否调用了会内存泄漏的代码。路由守卫(Route Guards)处路由也是会引起 SSR 内存泄漏的地方之一跟生命周期不同,所有的 route guard 都会在 SSR 运行。他们分别都是beforeEachbeforeRouteUpdatebeforeEnterbeforeRouteEnterbeforeResolveafterEachbeforeRouteEnterData-Prefetch 处还需要特别注意的地方就是 Date-prefetch 的地方,里面很容易出现内存泄漏的代码。 所谓 Date-prefetch 就是自定义实现的,在SSR处提前获取第三方数据,用于绘制的过程。Global Mixin 处这个内存泄漏的点想必大家都已经熟知,作者也在github上详细阐述过:GitHub issue简单来说,就是 global mixin 会给每个 Vue 实例一个拷贝,而不是引用。内存泄漏的例子以上列举了一些可能出现内存泄漏的地方,那么具体怎么样的代码才会引起内存泄漏呢?引起代码泄漏的例子网上有很多,我在这里想给大家介绍几种常见的泄漏例子。不小心造成的全局变量function foo(arg) { bar = “this is a hidden global variable”;}以上的代码会顺利运行,但是因为不小心声明了一个 bar 的变量。相当于:function foo(arg) { window.bar = “this is an explicit global variable”;}生成了一个全局变量 window.bar如果不手动回收,这个全局变量会一直存在于内存中,不会被CG回收。积少成多,最后造成内存泄漏。现在大家都是在各种模块化(CommonJS/AMD/CMD/etc..)之后的环境下进行开发,这种全局变量的内存泄漏的问题基本上是被消除了。但是要提醒大家,由于JavaScript的各种特性,会有很多意想不到的状况发生。当摸不清头脑的时候,可以尝试从这些特性出发找到问题。被遗忘了的 Timer 或者 callback请大家先看以下的例子var someResource = getData();setInterval(function() { var node = document.getElementById(‘Node’); if(node) { // Do stuff with node and someResource. node.innerHTML = JSON.stringify(someResource)); }}, 1000);乍一看没啥问题,之后如果 Node 节点从DOM上被移除,因为上面的 callback 对 Node 节点有引用,所以 Node 节点会一直常驻内存,不会被CG回收。要避免以上问题,就要养成 removeEventListener 和 clearInterval 的习惯。var someResource = getData();var interval = setInterval(function() { var node = document.getElementById(‘Node’); if(node) { // Do stuff with node and someResource. node.innerHTML = JSON.stringify(someResource)); } else { // Remove Timer clearInterval(interval); }}, 1000);还比如:var element = document.getElementById(‘button’);function onClick(event) { element.innerHtml = ’text’;}element.addEventListener(‘click’, onClick);// Do stuffelement.removeEventListener(‘click’, onClick);element.parentNode.removeChild(element);// Now when element goes out of scope,// both element and onClick will be collected even in old browsers that don’t// handle cycles well.在 addEventListener 之后已经要记得 removeEventListener闭包闭包造成内存泄漏的情况比较复杂,而且较难查找。限于本文主旨,不做原理说明。但是,在这里我给大家推荐一篇非常不错的文章,详细地介绍了闭包是如何造成内存泄漏的过程:An interesting kind of JavaScript memory leak总结个人认为 VueJS SSR 后端绘制内存泄漏造成影响要比普通的 VueJS 前端内存泄漏造成的影响要更大。前端内存泄漏的影响,都是发生在客户机器上,而且基本上现代浏览器也会做好保护机制,一般自行刷新之后都会解决。但是,一旦后端绘制内存泄漏造成宕机之后,整个服务器都会受影响,危险性更大,搞不好年终奖就没了。前端工程师一般都是关注于浏览器端表现,在开发过程中的内存泄漏问题不太在意也不太容易被发现。一般都是在项目上线一段时间之后,才发现内存泄漏的情况。那个时候再去着手,可能会有些无从下手或者手忙脚乱。那么,就让我们在开发的时候开始关注内存泄漏问题,将 VueJS SSR 后端绘制内存泄漏问题扼杀于襁褓之中。 ...

December 20, 2018 · 2 min · jiezi

服务端渲染Next.js下配置SEO文件

服务端渲染Next.js下配置SEO文件使用服务端渲染Next.js提供SEO静态文件(例如sitemap.xml,robots.txt和favicon.ico),只需将这些静态文件放在static文件夹中,然后将以下代码添加到服务器(server.js)配置中即可完成:const robotsOptions = { root: __dirname + ‘/static/’, headers: { ‘Content-Type’: ’text/plain;charset=UTF-8’, }};server.get(’/robots.txt’, (req, res) => ( res.status(200).sendFile(‘robots.txt’, robotsOptions)));const sitemapOptions = { root: __dirname + ‘/static/’, headers: { ‘Content-Type’: ’text/xml;charset=UTF-8’, }};server.get(’/sitemap.xml’, (req, res) => ( res.status(200).sendFile(‘sitemap.xml’, sitemapOptions)));const faviconOptions = { root: __dirname + ‘/static/’};server.get(’/favicon.ico’, (req, res) => ( res.status(200).sendFile(‘favicon.ico’, faviconOptions)));

December 18, 2018 · 1 min · jiezi

React 服务器端渲染和客户端渲染效果对比

React 服务器端渲染和客户端渲染对比最近在学习 React 的服务端渲染,于是使用 Express+React 写了一个 Demo,用于对比和客户端渲染的差异。github 地址先看一下效果吧:1、访问 服务器端渲染 Online Demo2、我们可以看到,首屏数据很快的就显示出来了,可是页面的进度条却还在加载中(因为客户端 js 很大)。3、当进度条加载完成后,页面才能进行交互操作(切换路由,登录等)。4、查看网页源代码,页面内容都在页面中。效果不明显的话,可以打开控制台,在 Network 栏 Disable cache,然后刷新。通过这次简单的访问,我们就能看出服务器端渲染的 2 大特点,首屏直出,SEO 友好。为什么要做服务器端渲染?1、访问 客户端渲染 Online Demo2、我们可以看到,首屏至少等待了 6 秒才渲染出来,这对于一般的用户,是难以容忍的。3、不过一旦渲染完成,页面就立即可交互了(切换路由,登录等)。4、查看网页源代码,页面只有一个空 div 容器,而没有实际内容。通过这次访问,我们就能看出客户端渲染的特点,首屏加载时间长,SEO 不友好,但可见即可操作。其实我们在访问客户端渲染的页面时,请求到的只是一个 html 空壳,里面引入了一个 js 文件,所有的内容都是通过 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>ssr</title> </head> <body> <div id=“root”></div> <script src=“bundle.js”></script> </body></html>正是因为页面是由 js 渲染出来的,所以会带来如下几个问题:1、页面要等待 js 加载,并执行完成了才能展示,在这期间页面展现的是白屏。2、爬虫不能识别 js 内容,所以抓取不到任何数据,不利于 SEO 优化。为了解决这 2 个问题,我们可以使用服务器端渲染。React 服务器端渲染流程之前说道,客户端渲染的页面,请求到的是一个 html 空壳,然后通过 js 去渲染页面。那如果请求到的直接是一个渲染好的页面,是不是就可以解决这 2 个问题了呢?没错,服务器端渲染就是这个原理。简化流程1、服务器端使用 renderToString 直接渲染出包含页面信息的静态 html。2、客户端根据渲染出的静态 html 进行二次渲染,做一些绑定事件等操作。服务器端没有 DOM,Window 等概念,所以只能渲染出字符串,不能进行事件绑定,样式渲染等。只有第一次访问页面时才使用服务器端渲染,之后会被客户端渲染接管。开始写代码吧接下来我们一起来写一个 React 服务器端渲染 Demo。编写路由这里使用 react-router 对前后端代码进行同构。1、客户端使用 react-router-dom 下的 BrowserRouter 进行前端路由控制。2、服务器端使用 react-router-dom 下的 StaticRouter 进行静态路由控制,具体操作如下:使用 react-router-config 下的 matchRoutes 匹配后端路由,使用 renderRoutes 渲染匹配到的路由。使用 react-router-dom/server 下的 renderToString 方法,渲染出 html 字符串,并返回给前端。使用 StaticRouter 中通过 context 可以和前端页面通信,传参。状态管理在 React 中,我们常常使用 redux 来存储数据,管理状态。1、客户端使用 redux 进行状态管理,使用 react-redux 提供的 Provider 为组件注入 store。2、服务器端和客户端一样,但每一次接收到请求需产生一个新的 store,避免多个用户操作同一个 store。数据请求1、客户端使用 axios 在 componentDidMount 中请求数据。2、服务器端同样使用 axios 去请求数据,但是服务器端不会触发 componentDidMount 生命周期。我们可以在后端匹配到路由的时候,进行数据请求,并把数据存入 redux 中的 store,然后渲染出包含数据的 html 页面,为了避免客户端二次请求,服务器端向 window 中注入 REDUX_STORE 数据,客户端直接使用此数据作为客户端 redux 的初始数据,以免发生数据抖动。具体操作如下:在 routes 对象上挂载一个自定义方法 loadData。在服务器端 matchRoutes 后,如果有 loadData,则进行请求数据,并把请求到的数据写入 store 中。服务器端等待请求完成后,再进行 renderToString 渲染。样式处理1、客户端使用 css-loader,style-loader 打包编写好的 css 代码并插入到页面中。2、服务器端由于 style-loader 会插入到页面,而服务器端并没有 document 等概念,所以这里使用 isomorphic-style-loader 打包 css 代码。引入 isomorphic-style-loader 后,客户端就可以通过 styles._getCss 方法获取到 css 代码。通过 staticRouter 中的 context 把 css 代码传入到后端。后端拼接好 css 代码,然后插入到 html 中,最后返回给前端。SEO 优化SEO 主要是针对搜索引擎进行优化,为了提高网站在搜索引擎中的自然排名,但搜索引擎只能爬取落地页内容(查看源代码时能够看到的内容),而不能爬取 js 内容,我们可以在服务器端做优化。常规的 SEO 主要是优化:文字,链接,多媒体内部链接尽量保持相关性外部链接尽可能多多媒体尽量丰富由于网页上的文字,链接,图片等信息都是产品设计好的,技术层面不能实现优化。我们需要做的就是优化页面的 title,description 等,让爬虫爬到页面后能够展示的更加友好。这里借助于 react-helmet 库,在服务期端进行 title,meta 等信息注入。你可能不需要服务器端渲染?现在,我们成功地通过服务器端渲染解决了首次加载白屏时间和 SEO 优化。但也带来了一些问题:服务器端压力增大。引入了 node 中间层,可维护性增大。以上两个问题归根结底还是钱的问题。服务器压力大,可以通过买更多的服务器来解决。可维护性增大,可以招募更多人来维护。但是对于小型团队来说,增加服务器,招募更多维护人员,都会额外增加的支出,所以在选择服务器端渲染时,要权衡好利弊。解决 SEO 的另一种方法如果只是想优化 SEO,不妨使用预渲染来实现,推荐使用 prerender 库来实现。prerender 库的原理:先请求客户端渲染的页面,把客户端渲染完成之后的结果,拿给爬虫看,这样爬虫获取到的页面就是已经渲染好的页面。prerender 库在使用时会开启一个服务,通过传递 url 来解析客户端渲染页面,这就需要我们对服务器端架构进行调整。1、 nginx 判断访问类型2.1、 用户访问 :直接走客户端渲染2.2、 爬虫访问 :走预渲染总结通过这个 Demo,让我加深了对服务器端的理解,如有错误,麻烦多多指正,谢谢大家!如果觉得有用得话给个 ⭐ 吧。react-ssr-demo ...

December 14, 2018 · 2 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

带你五步学会Vue SSR

前言SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档。通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助。我会通过五个步骤,一步步带你完成SSR的配置:纯浏览器渲染服务端渲染,不包含Ajax初始化数据服务端渲染,包含Ajax初始化数据服务端渲染,使用serverBundle和clientManifest进行优化一个完整的基于Vue + VueRouter + Vuex的SSR工程如果你现在对于我上面说的还不太了解,没有关系,跟着我一步步向下走,最终你也可以独立配置一个SSR开发项目,所有源码我会放到github上,大家可以作为参考。正文1. 纯浏览器渲染这个配置相信大家都会,就是基于weback + vue的一个常规开发配置,这里我会放一些关键代码,完整代码可以去github查看。目录结构- node_modules- components - Bar.vue - Foo.vue- App.vue- app.js- index.html- webpack.config.js- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignoreapp.jsimport Vue from ‘vue’;import App from ‘./App.vue’;let app = new Vue({ el: ‘#app’, render: h => h(App)});App.vue<template> <div> <Foo></Foo> <Bar></Bar> </div></template><script>import Foo from ‘./components/Foo.vue’;import Bar from ‘./components/Bar.vue’;export default { components: { Foo, Bar }}</script>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>纯浏览器渲染</title></head><body> <div id=“app”></div></body></html>components/Foo.vue<template> <div class=“foo”> <h1>Foo Component</h1> </div></template><style>.foo { background: yellowgreen;}</style>components/Bar.vue<template> <div class=“bar”> <h1>Bar Component</h1> </div></template><style>.bar { background: bisque;}</style>webpack.config.jsconst path = require(‘path’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const ExtractTextPlugin = require(’extract-text-webpack-plugin’);module.exports = { mode: ‘development’, entry: ‘./app.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘bundle.js’ }, module: { rules: [ { test: /.js$/, use: ‘babel-loader’ }, { test: /.css$/, use: [‘vue-style-loader’, ‘css-loader’, ‘postcss-loader’] // 如果需要单独抽出CSS文件,用下面这个配置 // use: ExtractTextPlugin.extract({ // fallback: ‘vue-style-loader’, // use: [ // ‘css-loader’, // ‘postcss-loader’ // ] // }) }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: { loader: ‘url-loader’, options: { limit: 10000 // 10Kb } } }, { test: /.vue$/, use: ‘vue-loader’ } ] }, plugins: [ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template: ‘./index.html’ }), // 如果需要单独抽出CSS文件,用下面这个配置 // new ExtractTextPlugin(“styles.css”) ]};postcss.config.jsmodule.exports = { plugins: [ require(‘autoprefixer’) ]};.babelrc{ “presets”: [ “@babel/preset-env” ], “plugins”: [ // 让其支持动态路由的写法 const Foo = () => import(’../components/Foo.vue’) “dynamic-import-webpack” ]}package.json{ “name”: “01”, “version”: “1.0.0”, “main”: “index.js”, “license”: “MIT”, “scripts”: { “start”: “yarn run dev”, “dev”: “webpack-dev-server”, “build”: “webpack” }, “dependencies”: { “vue”: “^2.5.17” }, “devDependencies”: { “@babel/core”: “^7.1.2”, “@babel/preset-env”: “^7.1.0”, “babel-plugin-dynamic-import-webpack”: “^1.1.0”, “autoprefixer”: “^9.1.5”, “babel-loader”: “^8.0.4”, “css-loader”: “^1.0.0”, “extract-text-webpack-plugin”: “^4.0.0-beta.0”, “file-loader”: “^2.0.0”, “html-webpack-plugin”: “^3.2.0”, “postcss”: “^7.0.5”, “postcss-loader”: “^3.0.0”, “url-loader”: “^1.1.1”, “vue-loader”: “^15.4.2”, “vue-style-loader”: “^4.1.2”, “vue-template-compiler”: “^2.5.17”, “webpack”: “^4.20.2”, “webpack-cli”: “^3.1.2”, “webpack-dev-server”: “^3.1.9” }}命令启动开发环境yarn start构建生产环境yarn run build最终效果截图:完整代码查看github2. 服务端渲染,不包含Ajax初始化数据服务端渲染SSR,类似于同构,最终要让一份代码既可以在服务端运行,也可以在客户端运行。如果说在SSR的过程中出现问题,还可以回滚到纯浏览器渲染,保证用户正常看到页面。那么,顺着这个思路,肯定就会有两个webpack的入口文件,一个用于浏览器端渲染weboack.client.config.js,一个用于服务端渲染webpack.server.config.js,将它们的公有部分抽出来作为webpack.base.cofig.js,后续通过webpack-merge进行合并。同时,也要有一个server来提供http服务,我这里用的是koa。我们来看一下新的目录结构:- node_modules- config // 新增 - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - App.vue - app.js - entry-client.js // 新增 - entry-server.js // 新增 - index.html - index.ssr.html // 新增- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore在纯客户端应用程序(client-only app)中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染(cross-request state pollution)。所以,我们要对app.js做修改,将其包装为一个工厂函数,每次调用都会生成一个全新的根组件。app.jsimport Vue from ‘vue’;import App from ‘./App.vue’;export function createApp() { const app = new Vue({ render: h => h(App) }); return { app };}在浏览器端,我们直接新建一个根组件,然后将其挂载就可以了。entry-client.jsimport { createApp } from ‘./app.js’;const { app } = createApp();app.$mount(’#app’);在服务器端,我们就要返回一个函数,该函数的作用是接收一个context参数,同时每次都返回一个新的根组件。这个context在这里我们还不会用到,后续的步骤会用到它。entry-server.jsimport { createApp } from ‘./app.js’;export default context => { const { app } = createApp(); return app;}然后再来看一下index.ssr.htmlindex.ssr.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>服务端渲染</title></head><body> <!–vue-ssr-outlet–> <script type=“text/javascript” src="<%= htmlWebpackPlugin.options.files.js %>"></script></body></html><!–vue-ssr-outlet–>的作用是作为一个占位符,后续通过vue-server-renderer插件,将服务器解析出的组件html字符串插入到这里。<script type=“text/javascript” src="<%= htmlWebpackPlugin.options.files.js %>"></script>是为了将webpack通过webpack.client.config.js打包出的文件放到这里(这里是为了简单演示,后续会有别的办法来做这个事情)。因为服务端吐出来的就是一个html字符串,后续的Vue相关的响应式、事件响应等等,都需要浏览器端来接管,所以就需要将为浏览器端渲染打包的文件在这里引入。用官方的词来说,叫客户端激活(client-side hydration)。所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:// 这里假定 App.vue template 根元素的 id="app"app.$mount(’#app’)由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:<div id=“app” data-server-rendered=“true”>Vue在浏览器端就依靠这个属性将服务器吐出来的html进行激活,我们一会自己构建一下就可以看到了。接下来我们看一下webpack相关的配置:webpack.base.config.jsconst path = require(‘path’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);module.exports = { mode: ‘development’, resolve: { extensions: [’.js’, ‘.vue’] }, output: { path: path.resolve(__dirname, ‘../dist’), filename: ‘[name].bundle.js’ }, module: { rules: [ { test: /.vue$/, use: ‘vue-loader’ }, { test: /.js$/, use: ‘babel-loader’ }, { test: /.css$/, use: [‘vue-style-loader’, ‘css-loader’, ‘postcss-loader’] }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: { loader: ‘url-loader’, options: { limit: 10000 // 10Kb } } } ] }, plugins: [ new VueLoaderPlugin() ]};webpack.client.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { entry: { client: path.resolve(__dirname, ‘../src/entry-client.js’) }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.html’), filename: ‘index.html’ }) ]});注意,这里的入口文件变成了entry-client.js,将其打包出的client.bundle.js插入到index.html中。webpack.server.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { target: ’node’, entry: { server: path.resolve(__dirname, ‘../src/entry-server.js’) }, output: { libraryTarget: ‘commonjs2’ }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.bundle.js’ }, excludeChunks: [‘server’] }) ]});这里有几个点需要注意一下:入口文件是 entry-server.js因为是打包服务器端依赖的代码,所以target要设为node,同时,output的libraryTarget要设为commonjs2这里关于HtmlWebpackPlugin配置的意思是,不要在index.ssr.html中引入打包出的server.bundle.js,要引为浏览器打包的client.bundle.js,原因前面说过了,是为了让Vue可以将服务器吐出来的html进行激活,从而接管后续响应。那么打包出的server.bundle.js在哪用呢?接着往下看就知道啦package.json{ “name”: “01”, “version”: “1.0.0”, “main”: “index.js”, “license”: “MIT”, “scripts”: { “start”: “yarn run dev”, “dev”: “webpack-dev-server”, “build:client”: “webpack –config config/webpack.client.config.js”, “build:server”: “webpack –config config/webpack.server.config.js” }, “dependencies”: { “koa”: “^2.5.3”, “koa-router”: “^7.4.0”, “koa-static”: “^5.0.0”, “vue”: “^2.5.17”, “vue-server-renderer”: “^2.5.17” }, “devDependencies”: { “@babel/core”: “^7.1.2”, “@babel/preset-env”: “^7.1.0”, “autoprefixer”: “^9.1.5”, “babel-loader”: “^8.0.4”, “css-loader”: “^1.0.0”, “extract-text-webpack-plugin”: “^4.0.0-beta.0”, “file-loader”: “^2.0.0”, “html-webpack-plugin”: “^3.2.0”, “postcss”: “^7.0.5”, “postcss-loader”: “^3.0.0”, “style-loader”: “^0.23.0”, “url-loader”: “^1.1.1”, “vue-loader”: “^15.4.2”, “vue-style-loader”: “^4.1.2”, “vue-template-compiler”: “^2.5.17”, “webpack”: “^4.20.2”, “webpack-cli”: “^3.1.2”, “webpack-dev-server”: “^3.1.9”, “webpack-merge”: “^4.1.4” }}接下来我们看server端关于http服务的代码:server/server.jsconst Koa = require(‘koa’);const Router = require(‘koa-router’);const serve = require(‘koa-static’);const path = require(‘path’);const fs = require(‘fs’);const backendApp = new Koa();const frontendApp = new Koa();const backendRouter = new Router();const frontendRouter = new Router();const bundle = fs.readFileSync(path.resolve(__dirname, ‘../dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’)});// 后端ServerbackendRouter.get(’/index’, (ctx, next) => { // 这里用 renderToString 的 promise 返回的 html 有问题,没有样式 renderer.renderToString((err, html) => { if (err) { console.error(err); ctx.status = 500; ctx.body = ‘服务器内部错误’; } else { console.log(html); ctx.status = 200; ctx.body = html; } });});backendApp.use(serve(path.resolve(__dirname, ‘../dist’)));backendApp .use(backendRouter.routes()) .use(backendRouter.allowedMethods());backendApp.listen(3000, () => { console.log(‘服务器端渲染地址: http://localhost:3000’);});// 前端ServerfrontendRouter.get(’/index’, (ctx, next) => { let html = fs.readFileSync(path.resolve(__dirname, ‘../dist/index.html’), ‘utf-8’); ctx.type = ‘html’; ctx.status = 200; ctx.body = html;});frontendApp.use(serve(path.resolve(__dirname, ‘../dist’)));frontendApp .use(frontendRouter.routes()) .use(frontendRouter.allowedMethods());frontendApp.listen(3001, () => { console.log(‘浏览器端渲染地址: http://localhost:3001’);});这里对两个端口进行监听,3000端口是服务端渲染,3001端口是直接输出index.html,然后会在浏览器端走Vue的那一套,主要是为了和服务端渲染做对比使用。这里的关键代码是如何在服务端去输出html字符串。const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8');const renderer = require('vue-server-renderer').createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')});可以看到,server.bundle.js在这里被使用啦,因为它的入口是一个函数,接收context作为参数(非必传),输出一个根组件app。这里我们用到了vue-server-renderer插件,它有两个方法可以做渲染,一个是createRenderer,另一个是createBundleRenderer。const { createRenderer } = require('vue-server-renderer')const renderer = createRenderer({ /* 选项 */ })const { createBundleRenderer } = require('vue-server-renderer')const renderer = createBundleRenderer(serverBundle, { /* 选项 */ })createRenderer无法接收为服务端打包出的server.bundle.js文件,所以这里只能用createBundleRenderer。serverBundle 参数可以是以下之一:绝对路径,指向一个已经构建好的 bundle 文件(.js 或 .json)。必须以 / 开头才会被识别为文件路径。由 webpack + vue-server-renderer/server-plugin 生成的 bundle 对象。JavaScript 代码字符串(不推荐)。这里我们引入的是.js文件,后续会介绍如何使用.json文件以及有什么好处。renderer.renderToString((err, html) =&gt; { if (err) { console.error(err); ctx.status = 500; ctx.body = '服务器内部错误'; } else { console.log(html); ctx.status = 200; ctx.body = html; }});使用createRenderer和createBundleRenderer返回的renderer函数包含两个方法renderToString和renderToStream,我们这里用的是renderToString成功后直接返回一个完整的字符串,renderToStream返回的是一个Node流。renderToString支持Promise,但是我在使用Prmoise形式的时候样式会渲染不出来,暂时还不知道原因,如果大家知道的话可以给我留言啊。配置基本就完成了,来看一下如何运行。yarn run build:client // 打包浏览器端需要bundleyarn run build:server // 打包SSR需要bundleyarn start // 其实就是 node server/server.js,提供http服务最终效果展示:访问http://localhost:3000/index我们看到了前面提过的data-server-rendered="true"属性,同时会加载client.bundle.js文件,为了让Vue在浏览器端做后续接管。访问http://localhost:3001/index还和第一步实现的效果一样,纯浏览器渲染,这里就不放截图了。完整代码查看github3. 服务端渲染,包含Ajax初始化数据如果SSR需要初始化一些异步数据,那么流程就会变得复杂一些。我们先提出几个问题:服务端拿异步数据的步骤在哪做?如何确定哪些组件需要获取异步数据?获取到异步数据之后要如何塞回到组件内?带着问题我们向下走,希望看完这篇文章的时候上面的问题你都找到了答案。服务器端渲染和浏览器端渲染组件经过的生命周期是有区别的,在服务器端,只会经历beforeCreate和created两个生命周期。因为SSR服务器直接吐出html字符串就好了,不会渲染DOM结构,所以不存在beforeMount和mounted的,也不会对其进行更新,所以也就不存在beforeUpdate和updated等。我们先来想一下,在纯浏览器渲染的Vue项目中,我们是怎么获取异步数据并渲染到组件中的?一般是在created或者mounted生命周期里发起异步请求,然后在成功回调里执行this.data = xxx,Vue监听到数据发生改变,走后面的Dom Diff,打patch,做DOM更新。那么服务端渲染可不可以也这么做呢?答案是不行的。在mounted里肯定不行,因为SSR都没有mounted生命周期,所以在这里肯定不行。在beforeCreate里发起异步请求是否可以呢,也是不行的。因为请求是异步的,可能还没有等接口返回,服务端就已经把html字符串拼接出来了。所以,参考一下官方文档,我们可以得到以下思路:在渲染前,要预先获取所有需要的异步数据,然后存到Vuex的store中。在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。把store中的数据设置到window.__INITIAL_STATE__属性中。在浏览器环境中,通过Vuex将window.__INITIAL_STATE__里面的数据注入到相应组件中。正常情况下,通过这几个步骤,服务端吐出来的html字符串相应组件的数据都是最新的,所以第4步并不会引起DOM更新,但如果出了某些问题,吐出来的html字符串没有相应数据,Vue也可以在浏览器端通过Vuex注入数据,进行DOM更新。更新后的目录结构:- node_modules- config - webpack.base.config.js - webpack.client.config.js - webpack.server.config.js- src - components - Bar.vue - Foo.vue - store // 新增 store.js - App.vue - app.js - entry-client.js - entry-server.js - index.html - index.ssr.html- package.json- yarn.lock- postcss.config.js- .babelrc- .gitignore先来看一下store.js:store/store.jsimport Vue from ‘vue’;import Vuex from ‘vuex’;Vue.use(Vuex);const fetchBar = function() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(‘bar 组件返回 ajax 数据’); }, 1000); });};function createStore() { const store = new Vuex.Store({ state: { bar: ’’ }, mutations: { ‘SET_BAR’(state, data) { state.bar = data; } }, actions: { fetchBar({ commit }) { return fetchBar().then((data) => { commit(‘SET_BAR’, data); }).catch((err) => { console.error(err); }) } } }); if (typeof window !== ‘undefined’ && window.INITIAL_STATE) { console.log(‘window.INITIAL_STATE’, window.INITIAL_STATE); store.replaceState(window.INITIAL_STATE); } return store;}export default createStore;typeof window如果不太了解Vuex,可以去Vuex官网先看一些基本概念。这里fetchBar可以看成是一个异步请求,这里用setTimeout模拟。在成功回调中commit相应的mutation进行状态修改。这里有一段关键代码:if (typeof window !== ‘undefined’ && window.INITIAL_STATE) { console.log(‘window.INITIAL_STATE’, window.INITIAL_STATE); store.replaceState(window.INITIAL_STATE);}因为store.js同样也会被打包到服务器运行的server.bundle.js中,所以运行环境不一定是浏览器,这里需要对window做判断,防止报错,同时如果有window.__INITIAL_STATE__属性,说明服务器已经把所有初始化需要的异步数据都获取完成了,要对store中的状态做一个替换,保证统一。components/Bar.vue<template> <div class=“bar”> <h1 @click=“onHandleClick”>Bar Component</h1> <h2>异步Ajax数据:</h2> <span>{{ msg }}</span> </div></template><script> const fetchInitialData = ({ store }) => { store.dispatch(‘fetchBar’); }; export default { asyncData: fetchInitialData, methods: { onHandleClick() { alert(‘bar’); } }, mounted() { // 因为服务端渲染只有 beforeCreate 和 created 两个生命周期,不会走这里 // 所以把调用 Ajax 初始化数据也写在这里,是为了供单独浏览器渲染使用 let store = this.$store; fetchInitialData({ store }); }, computed: { msg() { return this.$store.state.bar; } } }</script><style>.bar { background: bisque;}</style>这里在Bar组件的默认导出对象中增加了一个方法asyncData,在该方法中会dispatch相应的action,进行异步数据获取。需要注意的是,我在mounted中也写了获取数据的代码,这是为什么呢? 因为想要做到同构,代码单独在浏览器端运行,也应该是没有问题的,又由于服务器没有mounted生命周期,所以我写在这里就可以解决单独在浏览器环境使用也可以发起同样的异步请求去初始化数据。components/Foo.vue<template> <div class=“foo”> <h1 @click=“onHandleClick”>Foo Component</h1> </div></template><script>export default { methods: { onHandleClick() { alert(‘foo’); } },}</script><style>.foo { background: yellowgreen;}</style>这里我对两个组件都添加了一个点击事件,为的是证明在服务器吐出首页html后,后续的步骤都会被浏览器端的Vue接管,可以正常执行后面的操作。app.jsimport Vue from ‘vue’;import createStore from ‘./store/store.js’;import App from ‘./App.vue’;export function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return { app, store, App };}在建立根组件的时候,要把Vuex的store传进去,同时要返回,后续会用到。最后来看一下entry-server.js,关键步骤在这里:entry-server.jsimport { createApp } from ‘./app.js’;export default context => { return new Promise((resolve, reject) => { const { app, store, App } = createApp(); let components = App.components; let asyncDataPromiseFns = []; Object.values(components).forEach(component => { if (component.asyncData) { asyncDataPromiseFns.push(component.asyncData({ store })); } }); Promise.all(asyncDataPromiseFns).then((result) => { // 当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中 context.state = store.state; console.log(222); console.log(store.state); console.log(context.state); console.log(context); resolve(app); }, reject); });}我们通过导出的App拿到了所有它下面的components,然后遍历,找出哪些component有asyncData方法,有的话调用并传入store,该方法会返回一个Promise,我们使用Promise.all等所有的异步方法都成功返回,才resolve(app)。context.state = store.state作用是,当使用createBundleRenderer时,如果设置了template选项,那么会把context.state的值作为window.__INITIAL_STATE__自动插入到模板html中。这里需要大家多思考一下,弄清楚整个服务端渲染的逻辑。如何运行:yarn run build:clientyarn run build:serveryarn start最终效果截图:服务端渲染:打开http://localhost:3000/index可以看到window.__INITIAL_STATE__被自动插入了。我们来对比一下SSR到底对加载性能有什么影响吧。服务端渲染时performance截图:纯浏览器端渲染时performance截图:同样都是在fast 3G网络模式下,纯浏览器端渲染首屏加载花费时间2.9s,因为client.js加载就花费了2.27s,因为没有client.js就没有Vue,也就没有后面的东西了。服务端渲染首屏时间花费0.8s,虽然client.js加载扔花费2.27s,但是首屏已经不需要它了,它是为了让Vue在浏览器端进行后续接管。从这我们可以真正的看到,服务端渲染对于提升首屏的响应速度是很有作用的。当然有的同学可能会问,在服务端渲染获取初始ajax数据时,我们还延时了1s,在这个时间用户也是看不到页面的。没错,接口的时间我们无法避免,就算是纯浏览器渲染,首页该调接口还是得调,如果接口响应慢,那么纯浏览器渲染看到完整页面的时间会更慢。完整代码查看github4. 使用serverBundle和clientManifest进行优化前面我们创建服务端renderer的方法是:const bundle = fs.readFileSync(path.resolve(__dirname, ‘../dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’)});serverBundle我们用的是打包出的server.bundle.js文件。这样做的话,在每次编辑过应用程序源代码之后,都必须停止并重启服务。这在开发过程中会影响开发效率。此外,Node.js 本身不支持 source map。vue-server-renderer 提供一个名为 createBundleRenderer 的 API,用于处理此问题,通过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。所创建的 bundle renderer,用法和普通 renderer 相同,但是 bundle renderer 提供以下优点:内置的 source map 支持(在 webpack 配置中使用 devtool: ‘source-map’)在开发环境甚至部署过程中热重载(通过读取更新后的 bundle,然后重新创建 renderer 实例)关键 CSS(critical CSS) 注入(在使用 *.vue 文件时):自动内联在渲染过程中用到的组件所需的CSS。更多细节请查看 CSS 章节。使用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。preload和prefetch有不了解的话可以自行查一下它们的作用哈。那么我们来修改webpack配置:webpack.client.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const VueSSRClientPlugin = require(‘vue-server-renderer/client-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { entry: { client: path.resolve(__dirname, ‘../src/entry-client.js’) }, plugins: [ new VueSSRClientPlugin(), // 新增 new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.html’), filename: ‘index.html’ }) ]});webpack.server.config.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const nodeExternals = require(‘webpack-node-externals’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const VueSSRServerPlugin = require(‘vue-server-renderer/server-plugin’);const base = require(’./webpack.base.config’);module.exports = merge(base, { target: ’node’, // 对 bundle renderer 提供 source map 支持 devtool: ‘#source-map’, entry: { server: path.resolve(__dirname, ‘../src/entry-server.js’) }, externals: [nodeExternals()], // 新增 output: { libraryTarget: ‘commonjs2’ }, plugins: [ new VueSSRServerPlugin(), // 这个要放到第一个写,否则 CopyWebpackPlugin 不起作用,原因还没查清楚 new HtmlWebpackPlugin({ template: path.resolve(__dirname, ‘../src/index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.bundle.js’ }, excludeChunks: [‘server’] }) ]});因为是服务端引用模块,所以不需要打包node_modules中的依赖,直接在代码中require引用就好,所以配置externals: [nodeExternals()]。两个配置文件会分别生成vue-ssr-client-manifest.json和vue-ssr-server-bundle.json。作为createBundleRenderer的参数。来看server.jsserver.jsconst serverBundle = require(path.resolve(__dirname, ‘../dist/vue-ssr-server-bundle.json’));const clientManifest = require(path.resolve(__dirname, ‘../dist/vue-ssr-client-manifest.json’));const template = fs.readFileSync(path.resolve(__dirname, ‘../dist/index.ssr.html’), ‘utf-8’);const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template: template, clientManifest: clientManifest});效果和第三步就是一样的啦,就不截图了,完整代码查看github。5. 配置一个完整的基于Vue + VueRouter + Vuex的SSR这里和第四步不一样的是引入了vue-router,更接近于实际开发项目。在src下新增router目录。router/index.jsimport Vue from ‘vue’;import Router from ‘vue-router’;import Bar from ‘../components/Bar.vue’;Vue.use(Router);function createRouter() { const routes = [ { path: ‘/bar’, component: Bar }, { path: ‘/foo’, component: () => import(’../components/Foo.vue’) // 异步路由 } ]; const router = new Router({ mode: ‘history’, routes }); return router;}export default createRouter;这里我们把Foo组件作为一个异步组件引入,做成按需加载。在app.js中引入router,并导出:app.jsimport Vue from ‘vue’;import createStore from ‘./store/store.js’;import createRouter from ‘./router’;import App from ‘./App.vue’;export function createApp() { const store = createStore(); const router = createRouter(); const app = new Vue({ router, store, render: h => h(App) }); return { app, store, router, App };}修改App.vue引入路由组件:App.vue<template> <div id=“app”> <router-link to="/bar">Goto Bar</router-link> <router-link to="/foo">Goto Foo</router-link> <router-view></router-view> </div></template><script>export default { beforeCreate() { console.log(‘App.vue beforeCreate’); }, created() { console.log(‘App.vue created’); }, beforeMount() { console.log(‘App.vue beforeMount’); }, mounted() { console.log(‘App.vue mounted’); }}</script>最重要的修改在entry-server.js中,entry-server.jsimport { createApp } from ‘./app.js’;export default context => { return new Promise((resolve, reject) => { const { app, store, router, App } = createApp(); router.push(context.url); router.onReady(() => { const matchedComponents = router.getMatchedComponents(); console.log(context.url) console.log(matchedComponents) if (!matchedComponents.length) { return reject({ code: 404 }); } Promise.all(matchedComponents.map(component => { if (component.asyncData) { return component.asyncData({ store }); } })).then(() => { // 当使用 template 时,context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中 context.state = store.state; // 返回根组件 resolve(app); }); }, reject); });}这里前面提到的context就起了大作用,它将用户访问的url地址传进来,供vue-router使用。因为有异步组件,所以在router.onReady的成功回调中,去找该url路由所匹配到的组件,获取异步数据那一套还和前面的一样。于是,我们就完成了一个基本完整的基于Vue + VueRouter + VuexSSR配置,完成代码查看github。最终效果演示:访问http://localhost:3000/bar:完整代码查看github后续上面我们通过五个步骤,完成了从纯浏览器渲染到完整服务端渲染的同构,代码既可以运行在浏览器端,也可以运行在服务器端。那么,回过头来我们在看一下是否有优化的空间,又或者有哪些扩展的思考。1. 优化我们目前是使用renderToString方法,完全生成html后,才会向客户端返回,如果使用renderToStream,应用bigpipe技术可以向浏览器持续不断的返回一个流,那么文件的加载浏览器可以尽早的显示一些东西出来。const stream = renderer.renderToStream(context)返回的值是 Node.js stream:let html = ‘‘stream.on(‘data’, data => { html += data.toString()})stream.on(’end’, () => { console.log(html) // 渲染完成})stream.on(’error’, err => { // handle error…})在流式渲染模式下,当 renderer 遍历虚拟 DOM 树(virtual DOM tree)时,会尽快发送数据。这意味着我们可以尽快获得"第一个 chunk",并开始更快地将其发送给客户端。然而,当第一个数据 chunk 被发出时,子组件甚至可能不被实例化,它们的生命周期钩子也不会被调用。这意味着,如果子组件需要在其生命周期钩子函数中,将数据附加到渲染上下文(render context),当流(stream)启动时,这些数据将不可用。这是因为,大量上下文信息(context information)(如头信息(head information)或内联关键 CSS(inline critical CSS))需要在应用程序标记(markup)之前出现,我们基本上必须等待流(stream)完成后,才能开始使用这些上下文数据。因此,如果你依赖由组件生命周期钩子函数填充的上下文数据,则不建议使用流式传输模式。webpack优化webpack优化又是一个大的话题了,这里不展开讨论,感兴趣的同学可以自行查找一些资料,后续我也可能会专门写一篇文章来讲webpack优化。2. 思考是否必须使用vuex?答案是不用。Vuex只是为了帮助你实现一套数据存储、更新、获取的机制,入股你不用Vuex,那么你就必须自己想一套方案可以将异步获取到的数据存起来,并且在适当的时机将它注入到组件内,有一些文章提出了一些方案,我会放到参考文章里,大家可以阅读一下。是否使用SSR就一定好?这个也是不一定的,任何技术都有使用场景。SSR可以帮助你提升首页加载速度,优化搜索引擎SEO,但同时由于它需要在node中渲染整套Vue的模板,会占用服务器负载,同时只会执行beforeCreate和created两个生命周期,对于一些外部扩展库需要做一定处理才可以在SSR中运行等等。结语本文通过五个步骤,从纯浏览器端渲染开始,到配置一个完整的基于Vue + vue-router + Vuex的SSR环境,介绍了很多新的概念,也许你看完一遍不太理解,那么结合着源码,去自己手敲几遍,然后再来看几遍文章,相信你一定可以掌握SSR。最后,本文所有源代码都放在我的github上,如果对你有帮助的话,就来点一个赞吧参考链接https://ssr.vuejs.org/zh/https://zhuanlan.zhihu.com/p/…http://www.cnblogs.com/qingmi...https://juejin.im/entry/590ca...https://github.com/youngwind/… ...

October 11, 2018 · 8 min · jiezi