we* 目录
一、SSR与CSR对比
二、各环境准备与插件安装
三、express服务
四、SSR服务渲染实现
五、webpack与解析loader配置(本文重点)
六、问题记录
- 开始正文
一、SSR与CSR对比
渲染模式 | 原理 | 优点 | 缺点 |
---|---|---|---|
SSR-服务端渲染 | 客户端发送请求到服务端,服务端返回整个页面的HTML字符串给浏览器 | SEO优化,首屏渲染,性能优化 | 性能全都依赖于服务器,前端界面开发可操作性不高 |
CSR-客户端渲染 | 接口请求数据,前端动态处理和生成页面需要的结构和页面展示 | 用户交互多场景,vue的生命周期全 | 整体加载完速度慢,HTTP请求损耗严重等 |
选型适用场景
SSR适用于首页或者静态网页, CSR适用于交互页面
二、各环境准备与插件安装
我们使用的是nodejs,所以需要准备node 于 npm 一般开发都会有,本文使用
node -v // v14.5.0
npm -v // 6.14.5
-
初始化项目并安装所需要插件
npm init // 初始化项目
创建package.json文件
-
安装插件
express - 服务端框架
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服务
-
在根目录创建一个server.js
// 后台服务serve const express = require("express"); const app = express(); app.get('*',(request,response) => { response.end("start server ok"); }) // 3000 端口号 192.168.18.83 本机IP const server = app.listen(3000, "192.168.18.83", () => { const host = server.address().address; const port = server.address().port; console.log("服务已启动,访问地址为 http://%s:%s", host, port) })
-
在package.json中配置启动服务脚本
"scripts": { "serve": "node server.js" },
-
运行脚本
npm run serve
结果 :浏览器输入http://192.168.18.83:3000/ 地址可以看到 “start server ok”
说明我们的后台服务启动OK啦
注意:过程中出现服务连接不上,请切换端口号,又可以你的端口被暂用
四、SSR服务渲染实现
SSR服务渲染分为简单版与加强版
简单版: 修改serve.js 文件 简单实现渲染
// 后台服务serve
const express = require("express");
const app = express();
// 0. 导入vue 与 vue SSR渲染插件
const Vue = require("vue");
const vueServerRender = require("vue-server-renderer").createRenderer();
app.get('*',(request,response) => {
// 1. 创建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.转化为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("服务已启动,访问地址为 http://%s:%s", host, port)
})
结果:浏览器输入http://192.168.18.83:3000/ 地址可以看到 页面渲染成功
加强版-最终实现(直接看代码)
代码目录
serve.js 服务文件
// 编译服务
const compilerServer = require("./build/compiler-server.js");
// 后台服务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;");
// 运行webpack 编译
compilerServer((serverBundle,template) => {
// console.log('serve ----',serverBundle);
let render = vueRender.createBundleRenderer(serverBundle,{
template,
// 每次创建一个独立的上下文
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("服务已启动,访问地址为 http://%s:%s", host, port)
})
compiler-server.js 运行webpack 文件
const webpackServeConfig = require("./webpack.server.conf.js");
const webpack = require("webpack");
const fs = require("fs");
const path = require("path");
// 读取内存中的.json文件
const MFS = require("memory-fs");
module.exports = (cb) =>{
const webpackCompiler = webpack(webpackServeConfig);
const mfs = new MFS();
// 把文件保存到内存中
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 服务端入口文件
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回调函数
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(()=> {
const matchedComponents = router.getMatchedComponents();
if(!matchedComponents.length){
return reject({
code:404,
});
}
// // Promise 应该 resolve 应用程序实例,以便它可以渲染
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 服务配置
const webpack = require("webpack");
const merge = require("webpack-merge").merge;
const base = require("./webpack.base.conf.js");
const utils = require('./utils');
// 在服务端渲染中,所需要的文件都是使用require引入,不需要把node_modules文件打包
const webpackNodeExternals = require("webpack-node-externals");
const vueSSRServerPlugin = require("vue-server-renderer/server-plugin");
module.exports = merge(base,{
// 告知webpack,需要在node端运行
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 基础配置
'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']
}
}
},
{
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 块
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
esModule: false, // 必须配置 不让出现 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};
// 它会应用到普通的 `.css` 文件
// // 以及 `.vue` 文件中的 `<style>` 块
const cssLoader = {
loader: 'css-loader', // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
options: {
sourceMap: options.sourceMap,
// 开启 CSS Modules 设置之后会被生成唯一id
modules: false,
}
};
const postcssLoader = {
loader: 'postcss-loader', // 使用 PostCSS 加载和转译 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">` 块都应用它
sass: generalLoader('sass', {
indentedSyntax: true,
sassOptions: {
indentedSyntax: true
}
}),
// 普通的 `.scss` 文件和 `*.vue` 文件中的 `<style lang="scss">` 块都应用它
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 实例
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">关于</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>
基本所有的核心代码都已经贴出来,原理可有通过官网的一张图搞定,我没有复制
五、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是一个包含语法转换等诸多功能的工具链,通过这个工具链的使用可以使低版本的浏览器兼容最新的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调用
@babel/plugin-transform-runtime: "^7.10.5", // 减少包的体积
@babel/runtime: "^7.10.5",
使用plugin-transform-runtime. transform-runtime的转换是非侵入性的,也就是它不会污染你的原有的方法。遇到需要转换的方法它会另起一个名字,否则会直接影响使用库的业务代码@babel/preset-env这是一个预设的插件集合,包含了一组相关的插件,Bable中是通过各种插件来指导如何进行代码转换。该插件包含所有es6转化为es5的翻译规则, 比如箭头函数转换插件
@babel/polyfill: "^7.10.4", @babel/preset-env只是提供了语法转换的规则,但是它并不能弥补浏览器缺失的一些新的功能,如一些内置的方法和对象,如Promise,Array.from等,此时就需要polyfill来做js得垫片,弥补低版本浏览器缺失的这些新功能
babel-loader : "^8.1.0", // babel-loader了,它作为一个中间桥梁
参考: https://www.tangshuang.net/7427.html
- loader 相关
"css-loader": "^3.6.0", // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
"file-loader": "^6.0.0", // ,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名。
"less": "^3.12.2",
"less-loader": "^6.2.0", // 加载和转译 LESS 文件 与less 一起出现
"node-sass": "^4.14.1", // python 环境
"postcss-loader": "^3.0.0", // 适配浏览器 增加前缀
"postcss-scss": "^2.1.1",
"autoprefixer": "^9.8.5", // 与postcss-loader 一起出现 增加前缀
"sass-loader": "^9.0.2", // 加载和转译 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", // 加载和转译 Vue 组件
"vue-style-loader": "^4.1.2", // 加载和转译 Vue 组件 style 与style-loader 使用一个就可以
- webpack 相关
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12", // 脚手架
"webpack-dev-server": "^3.11.0", // 开发环境
"webpack-merge": "^5.0.9", // 合并webpack配置
"webpack-node-externals": "^2.5.0" // 所需要的文件都是使用require引入,不需要把node_modules文件打包
- 其他
"memory-fs": "^0.5.0", // 内存操作
"mini-css-extract-plugin": "^0.9.0", // 分离css
六、问题记录
问题一: style 没有效果 class 变为唯一识别id
原因:css-loader 的 modules = true // 开启 CSS Modules 设置之后会被生成唯一id
解决版本: 修改为false
const cssLoader = {
loader: 'css-loader', // 解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
options: {
sourceMap: options.sourceMap,
// 开启 CSS Modules 设置之后会被生成唯一id
modules: true,
}
};