we* 目錄
一、SSR與CSR對比
二、各環(huán)境準(zhǔn)備與插件安裝
三、express服務(wù)
四、SSR服務(wù)渲染實(shí)現(xiàn)
五、webpack與解析loader配置(本文重點(diǎn))
六、問題記錄
- 開始正文
一、SSR與CSR對比
| 渲染模式 | 原理 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|---|
| SSR-服務(wù)端渲染 | 客戶端發(fā)送請求到服務(wù)端,服務(wù)端返回整個頁面的HTML字符串給瀏覽器 | SEO優(yōu)化,首屏渲染,性能優(yōu)化 | 性能全都依賴于服務(wù)器,前端界面開發(fā)可操作性不高 |
| CSR-客戶端渲染 | 接口請求數(shù)據(jù),前端動態(tài)處理和生成頁面需要的結(jié)構(gòu)和頁面展示 | 用戶交互多場景,vue的生命周期全 | 整體加載完速度慢,HTTP請求損耗嚴(yán)重等 |
選型適用場景
SSR適用于首頁或者靜態(tài)網(wǎng)頁, CSR適用于交互頁面
二、各環(huán)境準(zhǔn)備與插件安裝
我們使用的是nodejs,所以需要準(zhǔn)備node 于 npm 一般開發(fā)都會有,本文使用
node -v // v14.5.0
npm -v // 6.14.5
-
初始化項(xiàng)目并安裝所需要插件
npm init // 初始化項(xiàng)目創(chuàng)建package.json文件
-
安裝插件
express - 服務(wù)端框架
vue
vue-router - vue 框架
vue-server-renderer - vue SSR渲染的核心框架"dependencies": { "express": "^4.17.1", "vue": "^2.6.11", "vue-router": "^3.3.4", "vue-server-renderer": "^2.6.11" }
注意:
- 推薦使用 Node.js 版本 6+。
- vue-server-renderer 和 vue 必須匹配版本。
- vue-server-renderer 依賴一些 Node.js 原生模塊,因此只能在 Node.js 中使用
三、express服務(wù)
-
在根目錄創(chuàng)建一個server.js
// 后臺服務(wù)serve const express = require("express"); const app = express(); app.get('*',(request,response) => { response.end("start server ok"); }) // 3000 端口號 192.168.18.83 本機(jī)IP const server = app.listen(3000, "192.168.18.83", () => { const host = server.address().address; const port = server.address().port; console.log("服務(wù)已啟動,訪問地址為 http://%s:%s", host, port) }) -
在package.json中配置啟動服務(wù)腳本
"scripts": { "serve": "node server.js" }, -
運(yùn)行腳本
npm run serve 結(jié)果 :瀏覽器輸入http://192.168.18.83:3000/ 地址可以看到 “start server ok”
說明我們的后臺服務(wù)啟動OK啦
注意:過程中出現(xiàn)服務(wù)連接不上,請切換端口號,又可以你的端口被暫用
四、SSR服務(wù)渲染實(shí)現(xiàn)
SSR服務(wù)渲染分為簡單版與加強(qiáng)版
簡單版: 修改serve.js 文件 簡單實(shí)現(xiàn)渲染
// 后臺服務(wù)serve
const express = require("express");
const app = express();
// 0. 導(dǎo)入vue 與 vue SSR渲染插件
const Vue = require("vue");
const vueServerRender = require("vue-server-renderer").createRenderer();
app.get('*',(request,response) => {
// 1. 創(chuàng)建vue
const vueApp = new Vue({
data:{
message:"hello world,一切從hello world 開始"
},
template:`<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>
這是SSR頁面
<h2>{{message}}</h2>
</body>
</html>`
});
// 2.轉(zhuǎn)化為html
vueServerRender.renderToString(vueApp).then((html) => {
response.end(html);
}).catch(error => console.log(error));
})
const server = app.listen(3000, "192.168.18.83", () => {
const host = server.address().address;
const port = server.address().port;
console.log("服務(wù)已啟動,訪問地址為 http://%s:%s", host, port)
})
結(jié)果:瀏覽器輸入http://192.168.18.83:3000/ 地址可以看到 頁面渲染成功

加強(qiáng)版-最終實(shí)現(xiàn)(直接看代碼)
代碼目錄

serve.js 服務(wù)文件
// 編譯服務(wù)
const compilerServer = require("./build/compiler-server.js");
// 后臺服務(wù)serve
const express = require("express");
const app = express();
// // vue --> html
const vueRender = require("vue-server-renderer");
app.get('*',(request,response) => {
const {url} = request;
response.status(200);
response.header("Content-Type","text/html;charset-utf-8;");
// 運(yùn)行webpack 編譯
compilerServer((serverBundle,template) => {
// console.log('serve ----',serverBundle);
let render = vueRender.createBundleRenderer(serverBundle,{
template,
// 每次創(chuàng)建一個獨(dú)立的上下文
renInNewContext:false
});
render.renderToString({
url:request.url
}).then((html) => {
response.end(html);
}).catch(error => {
if (error) {
if (error.code === 404) {
response.status(404).end('Page not found')
} else {
response.status(500).end('Internal Server Error')
}
} else {
// response.end(html)
response.end(JSON.stringify(error));
}
});
});
});
// 端口號
const config = require("./config/config.js");
const server = app.listen(config.server.port, config.server.host, function () {
const host = server.address().address;
const port = server.address().port;
console.log("服務(wù)已啟動,訪問地址為 http://%s:%s", host, port)
})
compiler-server.js 運(yùn)行webpack 文件
const webpackServeConfig = require("./webpack.server.conf.js");
const webpack = require("webpack");
const fs = require("fs");
const path = require("path");
// 讀取內(nèi)存中的.json文件
const MFS = require("memory-fs");
module.exports = (cb) =>{
const webpackCompiler = webpack(webpackServeConfig);
const mfs = new MFS();
// 把文件保存到內(nèi)存中
webpackCompiler.outputFileSystem = mfs;
webpackCompiler.watch( {}, async (error, stats ) => {
if(error) return console.log(error);
stats = stats.toJson();
stats.errors.forEach(error => console.log(error));
stats.warnings.forEach(warning => console.log(warning));
// // 獲取server bundle的json文件 - 為什么要從從這里取文件,為什么是這個文件 原因參考https://ssr.vuejs.org/zh/guide/bundle-renderer.html
const serverBundlePath = path.join(webpackServeConfig.output.path,'vue-ssr-server-bundle.json');
const serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath,"utf-8"));
// 獲取html模板路徑讀取文件
const templateIndexPath = path.join(__dirname,"../src/template/index.template.html");
const template = fs.readFileSync(templateIndexPath,"utf-8");
if(cb){
cb(serverBundle,template);
}
});
};
entry-server.js 服務(wù)端入口文件
import {createApp} from "./app.js";
export default (context) => {
return new Promise( (resolve, reject) => {
const { url } = context;
let { app, router } = createApp(context);
router.push(url);
// router回調(diào)函數(shù)
// 等到 router 將可能的異步組件和鉤子函數(shù)解析完
router.onReady(()=> {
const matchedComponents = router.getMatchedComponents();
if(!matchedComponents.length){
return reject({
code:404,
});
}
// // Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗员闼梢凿秩? resolve(app);
},reject);
});
}
index.template.html 模板文件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>模板</title>
</head>
<body>
<div id="app">
<!--vue-ssr-outlet-->
</div>
</body>
</html>
webpack.serve.conf.js 服務(wù)配置
const webpack = require("webpack");
const merge = require("webpack-merge").merge;
const base = require("./webpack.base.conf.js");
const utils = require('./utils');
// 在服務(wù)端渲染中,所需要的文件都是使用require引入,不需要把node_modules文件打包
const webpackNodeExternals = require("webpack-node-externals");
const vueSSRServerPlugin = require("vue-server-renderer/server-plugin");
module.exports = merge(base,{
// 告知webpack,需要在node端運(yùn)行
target:"node",
entry:"./src/entry-server.js",
devtool:"source-map",
output:{
filename:'server-bundle.js',
libraryTarget: "commonjs2"
},
module: {
rules: utils.styleLoader({ sourceMap:true, usePostCSS: true })
},
externals:[
webpackNodeExternals()
],
plugins:[
new vueSSRServerPlugin()
]
});
webpack.base.conf.js 基礎(chǔ)配置
'use strict'
const path = require("path");
const config = require("../config/config.js");
const VueLoaderPlugin = require('vue-loader/lib/plugin');
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
mode: 'development',
entry: {
app:"./src/entry-server.js",
},
output: {
path: config.build.assetsRoot,
publicPath: config.build.assetsPublicPath,
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
},
// 配置哪些引入路徑按照模塊方式查找
transformAssetUrls: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
}
},
{
// 它會應(yīng)用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 塊
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
esModule: false, // 必須配置 不讓出現(xiàn) src加載圖片為object
}
},
]
},
plugins:[
new VueLoaderPlugin(),
],
}
utils.js loader 加載工具類
const path = require("path");
const config = require("../config/config.js");
// 加載路徑
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
const resultPath = path.posix.join(assetsSubDirectory, _path);
console.log('------',resultPath);
return resultPath;
};
// 加載所有的cssloader
exports.cssLoaders= function (options) {
options = options || { sourceMap : false};
// 它會應(yīng)用到普通的 `.css` 文件
// // 以及 `.vue` 文件中的 `<style>` 塊
const cssLoader = {
loader: 'css-loader', // 解析 CSS 文件后,使用 import 加載,并且返回 CSS 代碼
options: {
sourceMap: options.sourceMap,
// 開啟 CSS Modules 設(shè)置之后會被生成唯一id
modules: false,
}
};
const postcssLoader = {
loader: 'postcss-loader', // 使用 PostCSS 加載和轉(zhuǎn)譯 CSS/SSS 文件
options: {
sourceMap: options.sourceMap,
config: {
path: 'postcss.config.js'
}
}
};
function generalLoader(loaderName, generalOptions) {
const loaderList = [cssLoader];
if(loaderName){
const item = {
loader: loaderName + '-loader',
options:Object.assign({}, generalOptions, {
//sourceMap: options.sourceMap
})
};
loaderList.push(item);
}
if(options.usePostCSS){
loaderList.push(postcssLoader);
}
// 是否分離css
if(options.extract){
}else {
const item = {
loader: "vue-style-loader"
};
return [item].concat(loaderList)
}
}
const resultLoader = {
css: generalLoader(),
// 適配瀏覽器 增加前綴
postcss: generalLoader('postcss',{
}),
less: generalLoader('less'),
// 普通的 `.scss` 文件和 `*.vue` 文件中的 `<style lang="scss">` 塊都應(yīng)用它
sass: generalLoader('sass', {
indentedSyntax: true,
sassOptions: {
indentedSyntax: true
}
}),
// 普通的 `.scss` 文件和 `*.vue` 文件中的 `<style lang="scss">` 塊都應(yīng)用它
scss: generalLoader('sass'),
// stylus: generalLoader('stylus'),
// styl: generalLoader('stylus')
};
return resultLoader;
};
exports.styleLoader = function (options) {
const outRules = [];
const loaders = exports.cssLoaders(options);
for (const extension in loaders) {
const loader = loaders[extension];
const item = {
test: new RegExp('\\.' + extension + '$'),
use: loader
};
outRules.push(item);
console.log('---------\n',item.test, item.use);
}
return outRules;
}
其次還有app.js App.vue 比較簡單
app.js
import Vue from "vue";
import createRouter from "./router/router.js";
import App from "./App.vue";
export function createApp(context) {
const router = createRouter();
const app = new Vue({
router,
// 注入 router 到根 Vue 實(shí)例
render: h => h(App),
});
return {
app,
router
};
}
App.vue
<template>
<div id="app">
<div class="app-title">我就是一個頁面</div>
<div style="color: yellow">這個是測試style</div>
<router-link to="/">首頁</router-link>-->
<router-link to="/about">關(guān)于</router-link>
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="scss" >
#app{
text-align: center;
color: red;
.app-title{
color: purple;
}
}
</style>
基本所有的核心代碼都已經(jīng)貼出來,原理可有通過官網(wǎng)的一張圖搞定,我沒有復(fù)制
五、webpack與解析loader配置(本文的核心)
package.json文件說明:
{
"name": "ssr",
"version": "1.0.0",
"description": "ssr",
"scripts": {
"build-server": "webpack --config build/webpack.server.conf.js",
"http": "node server.js"
},
"author": "ddd",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"vue": "^2.6.11",
"vue-router": "^3.3.4",
"vue-server-renderer": "^2.6.11"
},
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/runtime": "^7.10.5",
"autoprefixer": "^9.8.5",
"babel-loader": "^8.1.0",
"css-loader": "^3.6.0",
"file-loader": "^6.0.0",
"less": "^3.12.2",
"less-loader": "^6.2.0",
"memory-fs": "^0.5.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.14.1",
"postcss-loader": "^3.0.0",
"postcss-scss": "^2.1.1",
"sass-loader": "^9.0.2",
"style-loader": "^1.2.1",
"url-loader": "^4.1.0",
"vue-loader": "^15.9.3",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.0.9",
"webpack-node-externals": "^2.5.0"
},
}
使用了這么多插件,這么多的loader 具體的作用是是什么,今天就來好好整整
里面存在4類
- babel 編譯
babel是一個包含語法轉(zhuǎn)換等諸多功能的工具鏈,通過這個工具鏈的使用可以使低版本的瀏覽器兼容最新的javascript語法
- babel 編譯
"@babel/core": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/runtime": "^7.10.5",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"babel-loader": "^8.1.0",
@babel/core是babel的核心庫,所有的核心Api都在這個庫里,這些Api供babel-loader調(diào)用
@babel/plugin-transform-runtime: "^7.10.5", // 減少包的體積
@babel/runtime: "^7.10.5",
使用plugin-transform-runtime. transform-runtime的轉(zhuǎn)換是非侵入性的,也就是它不會污染你的原有的方法。遇到需要轉(zhuǎn)換的方法它會另起一個名字,否則會直接影響使用庫的業(yè)務(wù)代碼@babel/preset-env這是一個預(yù)設(shè)的插件集合,包含了一組相關(guān)的插件,Bable中是通過各種插件來指導(dǎo)如何進(jìn)行代碼轉(zhuǎn)換。該插件包含所有es6轉(zhuǎn)化為es5的翻譯規(guī)則, 比如箭頭函數(shù)轉(zhuǎn)換插件
@babel/polyfill: "^7.10.4", @babel/preset-env只是提供了語法轉(zhuǎn)換的規(guī)則,但是它并不能彌補(bǔ)瀏覽器缺失的一些新的功能,如一些內(nèi)置的方法和對象,如Promise,Array.from等,此時就需要polyfill來做js得墊片,彌補(bǔ)低版本瀏覽器缺失的這些新功能
babel-loader : "^8.1.0", // babel-loader了,它作為一個中間橋梁
參考: https://www.tangshuang.net/7427.html
- loader 相關(guān)
"css-loader": "^3.6.0", // 解析 CSS 文件后,使用 import 加載,并且返回 CSS 代碼
"file-loader": "^6.0.0", // ,生成的文件的文件名就是文件內(nèi)容的 MD5 哈希值并會保留所引用資源的原始擴(kuò)展名。
"less": "^3.12.2",
"less-loader": "^6.2.0", // 加載和轉(zhuǎn)譯 LESS 文件 與less 一起出現(xiàn)
"node-sass": "^4.14.1", // python 環(huán)境
"postcss-loader": "^3.0.0", // 適配瀏覽器 增加前綴
"postcss-scss": "^2.1.1",
"autoprefixer": "^9.8.5", // 與postcss-loader 一起出現(xiàn) 增加前綴
"sass-loader": "^9.0.2", // 加載和轉(zhuǎn)譯 SASS/SCSS 文件 與node-sass 一起
"style-loader": "^1.2.1",
"url-loader": "^4.1.0", //url-loader功能類似于file-loader,但是在文件大?。▎挝?byte)低于指定的限制時,可以返回一個 DataURL。
比如加載圖片
"vue-loader": "^15.9.3", // 加載和轉(zhuǎn)譯 Vue 組件
"vue-style-loader": "^4.1.2", // 加載和轉(zhuǎn)譯 Vue 組件 style 與style-loader 使用一個就可以
- webpack 相關(guān)
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12", // 腳手架
"webpack-dev-server": "^3.11.0", // 開發(fā)環(huán)境
"webpack-merge": "^5.0.9", // 合并webpack配置
"webpack-node-externals": "^2.5.0" // 所需要的文件都是使用require引入,不需要把node_modules文件打包
- 其他
"memory-fs": "^0.5.0", // 內(nèi)存操作
"mini-css-extract-plugin": "^0.9.0", // 分離css
六、問題記錄
問題一: style 沒有效果 class 變?yōu)槲ㄒ蛔R別id

原因:css-loader 的 modules = true // 開啟 CSS Modules 設(shè)置之后會被生成唯一id
解決版本: 修改為false
const cssLoader = {
loader: 'css-loader', // 解析 CSS 文件后,使用 import 加載,并且返回 CSS 代碼
options: {
sourceMap: options.sourceMap,
// 開啟 CSS Modules 設(shè)置之后會被生成唯一id
modules: true,
}
};