[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算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼