[TOC]
# 什么是vue-ssr
SSR是Server-Side Rendering的简写,即由服务端负责渲染页面直出,亦即同构应用。程序的大部分代码都可以在服务端和客户端运行。在服务端vue组件渲染为html字符串,在客户端生成dom和操作dom。
<br>
能在服务端渲染为html字符串得益于vue组件结构是基于vnode的。vnode是dom的抽象表达,它不是真实的dom,它是由js对象组成的树,每个节点代表了一个dom。因为vnode所以在服务端vue可以把js对象解析为html字符串。同样在客户端vnode因为是存在内存之中的,操作内存总比操作dom快的多,每次数据变化需要更新dom时,新旧vnode树经过diff算法,计算出最小变化集,大大提高了性能。
<br>

<br>
<br>
# 实现
## 返回html文本
~~~JavaScript
import Koa2 from 'koa';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
const renderer = createRenderer();
const app = new Koa2();
/**
* 应用接管路由
*/
app.use(async function(ctx) {
const vm = new Vue({
template:"<div>hello world</div>"
});
ctx.set('Content-Type', 'text/html;charset=utf-8');
const htmlString = await renderer.renderToString(vm);
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
</body>
</html>`;
});
app.listen(3000);
~~~
我们现在在服务器端创建了一个`vue`实例`vm`。vm是一个对象,对象是不能直接发送给浏览器的,发送前必须转换为字符串。
`vue-server-renderer` 把一个`vue`实例转化成字符串,通过`renderer.renderToString`这个方法,将`vm`作为参数传递进去运行,便很轻松的返回了`vm`转化后的字符串,如下。
~~~text
<div data-server-rendered="true">hello world</div>
~~~
从上面的案例,可以从宏观上把握服务器端渲染的整个脉络.
* 首先是要获取到当前这个请求路径是想请求哪个`vue`组件
* 将组件数据内容填充好转化成字符串
* 最后把字符串拼接成`html`发送给前端.
<br>
## 打包
这里客户端和服务端的入口不一样,webpack配置也不一样
客户端
~~~
{
mode: 'development',
entry: './src/client/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public'),
},
...
}
~~~
服务端
~~~
{
return {
target: 'node',
mode: 'development',
entry: './src/index.js',
devtool: 'eval-source-map',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'build'),
libraryTarget: 'commonjs2',
},
...
}
~~~
<br>
<br>
## 路由集成
在实现`srr`的任务里,主要工作是为了在客户端发送请求后能找出当前的请求路径是匹配哪个`vue`组件。
* 使用`createRouter()`方法创建一个路由实例对象`router`,把它注入到`Vue`实例中.
* router.onready 方法把一个回调排队,在路由完成初始导航时调用,这意味着它可以解析所有的异步进入钩子和路由初始化相关联的异步组件。 这可以有效确保服务端渲染时服务端和客户端输出的一致。
route.js
~~~
import Vue from 'vue';
import Router from 'vue-router';
import List from './pages/List';
import Search from './pages/Search';
Vue.use(Router);
export const createRouter = () => {
return new Router({
mode: 'history',
routes: [
{
path: '/list',
component: List,
},
{
path: '/search',
component: Search,
},
],
});
};
export const routerReady = async (router) => {
return new Promise((resolve) => {
router.onReady(() => {
resolve(null);
});
});
};
~~~
<br>
<br>
index.js
* 执行`router.push(req.url)`,这一步非常关键.相当于告诉`Vue`实例,当前的请求路径已经传给你了,你快点根据路径寻找要渲染的页面组件.
* `await routerReady(router);`执行完毕后,就已经可以得到当前请求路径匹配的页面组件了.
* `matchedComponents.length`如果等于`0`,说明当前的请求路径和我们定义的路由没有一个匹配上,那么这里应该要定制一个精美的`404`页面返回给浏览器.
* `matchedComponents.length`不等于`0`,说明当前的`vm`已经根据请求路径让匹配的页面组件占据了视口.接下来只需要将`vm`转化成字符串发送给浏览器就可以了.
~~~
import Koa2 from 'koa';
// 静态文件处理
import staticFiles from 'koa-static';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
// Vue部分
import App from './App.vue';
import { createRouter, routerReady } from './route.js';
const renderer = createRenderer();
const app = new Koa2();
app.use(staticFiles('public'));
app.use(async function (ctx) {
const req = ctx.request;
// 创建路由
const router = createRouter();
const vm = new Vue({
// 添加路由
router,
render: (h) => h(App),
});
// 告诉vue 渲染 当前所需组件
router.push(req.url);
// 等到 router 钩子函数解析完
await routerReady(router);
//获取匹配的页面组件
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) {
ctx.body = '没有找到该网页,404';
return;
}
ctx.set('Content-Type', 'text/html;charset=utf-8');
let htmlString
try {
htmlString = await renderer.renderToString(vm);
} catch (error) {
ctx.status = 500;
ctx.body = 'Internal Server Error';
}
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
</body>
// 引入页面js
<script src="./index.js"></script>
</html>`;
});
app.listen(3000);
~~~
<br>
client/index.js
~~~
import Vue from 'vue';
import VueMeta from 'vue-meta';
import App from '../App.vue';
import { createRouter } from '../route';
Vue.config.productionTip = false;
Vue.use(VueMeta);
//创建路由
const router = createRouter();
new Vue({
router,
render: (h) => h(App),
}).$mount('#root', true);
~~~
<br>
<br>
## Vuex集成
路由集成后虽然能够根据路径渲染指定的页面组件,但是服务器渲染也存在局限性。
<br>
比如你在页面组件模板上加一个`v-click`事件,结果会发现页面在浏览器上渲染完毕后事件无法响应,这样肯定会违背我们的初衷。事件绑定,
<br>
点击链接跳转这些都是浏览器赋予的能力。因此可以借助客户端渲染来帮助我们走出困境。
<br>
整个流程可以设计如下.
* 浏览器输入链接请求服务器,服务器端将包含页面内容的`html`返回,但是在`html`文件下要加上客户端渲染的`js`脚本.
* `html`开始在浏览器上加载,页面上已经呈现出静态内容了.当线程走到`html`文件下的`script`标签,开始请求客户端渲染的脚本并执行.
* 此时客户端脚本里面的`vue`实例开始接管了整个应用,它开始赋予原本后端返回的静态`html`各种能力,比如让标签上的事件绑定开始生效.
<br>
store/index.js
~~~
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
list: [],
name: 'kay',
},
actions: {
getList({ commit }, params) {
return new Promise((resolve)=>{
commit("setList",[{
name:"广州"
},{
name:"深圳"
}]);
resolve();
},2000)
},
},
mutations: {
setList(state, data) {
state.list = data || [];
},
},
});
}
~~~
page/list/index.vue
~~~
<template>
<div class="list">
<p>当前页:列表页</p>
<a @click="jumpSearch()">go搜索页</a>
<ul>
<li v-for="item in list" :key="item.name">
<p>城市: {{ item.name }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
// 服务端获取异步数据公共方法
asyncData({ store, route }) {
return store.dispatch("getList");
},
};
</script>
~~~
index.js
~~~
import Koa2 from 'koa';
import staticFiles from 'koa-static';
import { createRenderer } from 'vue-server-renderer';
import Vue from 'vue';
import App from './App.vue';
import { createRouter, routerReady } from './route.js';
import { createStore } from './vuex/store';
const renderer = createRenderer();
const app = new Koa2();
app.use(staticFiles('public'));
app.use(async function (ctx) {
const req = ctx.request;
const router = createRouter();
// 创建Store
const store = createStore();
const vm = new Vue({
router,
store,
render: (h) => h(App),
});
router.push(req.url);
await routerReady(router);
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) {
ctx.body = '没有找到该网页,404';
return;
}
ctx.set('Content-Type', 'text/html;charset=utf-8');
let htmlString
try {
// 匹配到的组件执行 asyncData方法,调用dispatch来更新store
await Promise.all(
matchedComponents.map((Component) => {
if (Component.asyncData) {
Component.asyncData({
store,
route: router.currentRoute,
});
}
})
);
htmlString = await renderer.renderToString(vm);
} catch (error) {
ctx.status = 500;
ctx.body = 'Internal Server Error';
}
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
</body>
<script src="./index.js"></script>
</html>`;
});
app.listen(3000);
~~~
<br>
### 脱水
现在ssr和客户端都配置了vuex,但区别是服务端的store里面放着List.vue需要的远程请求的数据,而客户端的store是空的.
<br>
srr返回的静态html是带着城市列表的,一旦客户端的vue接管了整个应用就会展开各种各样的初始化操作.客户端也要配置vuex,由于它的数据仓库是空的所以重新引发了页面渲染.致使原本来含有城市列表的页面部分消失了.
<br>
为了解决这个问题,就要想办法让ssr远程请求来的数据也给客户端的store发一份.这样客户端即使接管了应用,但发现此时store存储的城市列表数据和页面保持一致也不会造成闪烁问题.
~~~
ctx.body = `<html>
<head>
</head>
<body>
${htmlString}
// 注入服务端strore的数据
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="/index.js"></script>
</body>
</html>`;
~~~
### 注水
服务器端将数据放入了js脚本里,客户端此时就可以轻松拿到这份数据.
<Br>
在客户端入口文件里加上 store.replaceState(window.context.state); 如果发现window.context.state存在,就把这部分数据作为vuex的初始数据,这个过程称之为注水.
client/index.js
~~~
import Vue from 'vue';
import App from '../App.vue';
import { createRouter } from '../route';
import VueMeta from 'vue-meta';
import { createStore } from '../vuex/store';
Vue.config.productionTip = false;
Vue.use(VueMeta);
const router = createRouter();
// 创建Store
const store = createStore();
// 若有 window.context.state,更新客户端store
if (window.context && window.context.state) {
store.replaceState(window.context.state);
}
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#root', true);
~~~
<br>
<br>
## 装载真实数据
上面在`vuex`里是使用定时器模拟的请求数据,接下来利用网上的一些开放`API`接入真实的数据.
对`vuex`里的`action`方法做如下修改.
~~~text
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
}
}
~~~
<br>
`asyncData`一运行就会走到上面`actions`里面的`getList`,它就会对上面那个`url`地址发起请求.但仔细观察发现这个`url`是没有写域名的,这样访问肯定会报错.
那把远程域名给它加上去行不行呢?如果这样硬加是会出现问题的.有一种场景就是客户端接管应用它也可以调用`getList`方法,我们写的这部分`vuex`代码可是服务端和客户端共用的.那如果客户端直接访问带有远程域名的路径就会引起跨域.
那如何解决这一问题呢?这里的`url`最好不要加域名,以`/`开头.那样客户端访问这个路径就会引向`node`服务器.此时只要加一个接口代理转发就搞定了.
~~~text
import proxy from 'koa-server-http-proxy';
export const proxyHanlder = (app)=>{
app.use(proxy('/api', {
target: 'https://geoapi.qweather.com', //网上寻找的开放API接口,支持返回地理数据.
pathRewrite: { '^/api': '' },
changeOrigin: true
}));
}
~~~
定义一个中间件函数,在执行服务器端渲染前添加到`koa2`上.
这样`node`服务器只要看到以`/api`开头的请求路径就会转发到远程地址上获取数据,不会再走后面服务器端渲染的逻辑.
### 服务器端路径请求的问题
使用上面的代理转发之后又会带来新的问题,设想一种场景.如果浏览器输入`localhost:3000/list`后,`node`解析请求发现要加载`List.vue`这个页面组件,而这个组件又有一个`asyncData`异步方法,因此就运行异步方法获取数据.
~~~text
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
}
}
~~~
这个异步方法就是`getList`,注意此时执行这段脚本的是`node`服务器,不是客户端的浏览器.
浏览器如果请求以`/`开头的`url`,请求会发给`node`服务器.`node`服务器现在需要自己请求自己,只要请求了自己设置的代理就能把请求转发给远程服务器,而如今`node`服务器请求以`/`开头的路径是绝对无法请求到自己的,这个时候只能用绝对路径.
我们上面提到这部分的`vuex`代码是客户端和服务端共用的,最好不用绝对路径写死.还有一个更优雅的方法,就是对`axios`的`baseURL`进行配置生成带有域名的`axios`实例来请求.那这部分代码就可以改成如下.
~~~text
export function createStore(_axios) {
return new Vuex.Store({
state: {
list: [],
name: 'kay',
},
actions: {
getList({ commit }, params) {
const url = '/api/v2/city/lookup?location=guangzhou&key=9423bb18dff042d4b1716d084b7e2fe0';
return _axios.get(url).then((res)=>{
commit("setList",res.data.location);
})
},
},
mutations: {
setList(state, data) {
state.list = data || [];
},
},
});
}
~~~
`_axios`是配置基础域名后的实例对象,客户端会生成一个`_axios`,服务端也会生成一个,只不过客户端是不用配置`baseURL`的.
~~~text
import axios from "axios";
//util/getAxios.js
/**
* 获取客户端axios实例
*/
export const getClientAxios = ()=>{
const instance = axios.create({
timeout: 3000,
});
return instance;
}
/**
* 获取服务器端axios实例
*/
export const getServerAxios = (ctx)=>{
const instance = axios.create({
timeout: 3000,
baseURL: 'http://localhost:3000'
});
return instance;
}
~~~
<br>
index.js
~~~
import { getServerAxios } from "./util/getAxios";
import { proxyHanlder } from "./middleware/proxy";
proxyHanlder(app);
app.use(async function (ctx) {
// ...
const store = createStore(getServerAxios(ctx));
})
~~~
通过生成两份`axios`实例既保持了`vuex`代码的统一性,另外还解决了`node`服务器自己访问不了自己的问题.
<br>
### cookie如何处理
使用了接口代理之后,怎么确保每次接口转发都能把`cookie`也一并传给远程的服务器.可以按如下配置.
在`ssr`的入口文件里.
~~~text
***省略
**
* 应用接管路由,服务器端渲染代码
*/
app.use(async function(ctx) {
const req = ctx.request;
//图标直接返回
if (req.path === '/favicon.ico') {
ctx.body = '';
return false;
}
const router = createRouter(); //创建路由
const store = createStore(getServerAxios(ctx)); //创建数据仓库
***省略
})
~~~
在创建`ctx`和`axios`实例的时候将`ctx`传递进去.
~~~text
/**
* 获取服务器端axios实例
*/
export const getServerAxios = (ctx)=>{
const instance = axios.create({
timeout: 3000,
headers:{
cookie:ctx.req.headers.cookie || ""
},
baseURL: 'http://localhost:3000'
});
return instance;
}
~~~
将`ctx`中的`cookie`取出来赋值给`axios`的`headers`,这样就确保`cookie`被携带上了.
<br>
<br>
## 样式处理
`.vue`页面的文件通常把代码分成三个标签`<template>`,`<script>`和`<style>`.
`<style scoped lang="scss"></style>`上还可以添加一些属性.
和客户端渲染相比,实现`ssr`的过程要多处理一步.即将`<style>`里面的样式内容提取出来,再渲染到`html`的`<head>`里面.
在`ssr`入口文件`index.js`添加如下代码.
~~~text
...省略
const context = {}; //创建一个上下文对象
htmlString = await renderer.renderToString(vm, context);
ctx.body = `<html>
<head>
${context.styles ? context.styles : ''}
</head>
<body>
${htmlString}
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="./bundle.js"></script>
</body>
</html>`;
~~~
服务端提取样式的过程非常简单,定义一个上下文对象`context`.
`renderer.renderToString`函数的第二个参数里传入`context`,该函数执行完毕后,`context`对象的`styles`属性就会拥有页面组件的样式.最后将这份样式拼接到`html`的`head`头部里即可.
<br>
## **Head信息处理**
常规的`html`文件的`head`里面不仅包含样式,它可能还需要设置`<title>`和`<meta />`.如何针对每个页面设置个性化的头部信息,可以利用`vue-meta`插件.
现在需要给`List.vue`页面组件添加一些头信息,可以按如下设置.
~~~text
<script>
export default {
metaInfo: {
title: "列表页",
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
],
},
asyncData({ store, route }) {
return store.dispatch("getList");
}
...省略
}
~~~
在导出的对象上添加一个属性`metaInfo`,在其中分别设置`title`和`meta`;
在`ssr`的入口文件处加入如下代码.
~~~text
import Koa2 from 'koa';
import Vue from 'vue';
import App from './App.vue';
import VueMeta from 'vue-meta';
Vue.use(VueMeta);
/**
* 应用接管路由
*/
app.use(async function(ctx) {
...省略
const vm = new Vue({
router,
store,
render: (h) => h(App),
});
const meta_obj = vm.$meta(); // 生成的头信息
router.push(req.url);
...省略
htmlString = await renderer.renderToString(vm, context);
const result = meta_obj.inject();
const { title, meta } = result;
ctx.body = `<html>
<head>
${title ? title.text() : ''}
${meta ? meta.text() : ''}
${context.styles ? context.styles : ''}
</head>
<body>
${htmlString}
<script>
var context = {
state: ${JSON.stringify(store.state)}
}
</script>
<script src="./index.js"></script>
</body>
</html>`;
});
app.listen(3000);
~~~
通过`vm.$meta()`生成头信息`meta_obj`,待到`vue`实例加载完毕后,执行`meta_obj.inject()`获取被渲染页面组件的`meta`和`title`数据,再将它们填充到`html`字符串即可.
# 参考资料
[从原理上实现Vue的ssr渲染](https://zhuanlan.zhihu.com/p/346674458)
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码