简介 最近开始学习 nuxt,先自己搭建一个简易的 SRR,来体验体验,并了解它的运行原理。
搭建 服务端 安装 初始化项目:npm init -y
安装依赖:
npm i vue express
npm i -D nodemon vue-loader babel-loader @babel/preset-env webpack webpack-cli webpack-merge webpack-node-externals
vue: 前端框架
express:服务器
nodemon: 监听文件变化,自动重启服务器
vue-loader: 加载 .vue 文件
babel-loader: 加载 ES6 语法
@babel/preset-env: 转换 ES6 语法
webpack: 打包工具
webpack-cli: 命令行工具
webpack-merge: 合并配置文件
webpack-node-externals: 排除 node 模块
目录结构
config
node_modules
src
demo
server
App.vue
app.js
package-lock.json
package.json
编写代码 App.vue <template > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > </template > <script setup > import { ref } from 'vue' const count = ref (100 )const addHandler = ( ) => { count.value ++ } </script > <style scoped > </style >
app.js 返回函数的原因是避免跨请求状态污染 。
import { createSSRApp } from 'vue' import App from './App.vue' export default function createApp ( ) { const app = createSSRApp (App ) return app }
src/demo/index.html <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > custon-ssr</title > </head > <body > <h1 > 手动搭建ssr</h1 > <div id ="app" > </div > </body > </html >
src/server/index.js const express = require ('express' )const server = express ()import { renderToString } from '@vue/server-renderer' import createApp from '../app.js' server.get ('/' , async (req, res) => { const app = createApp () const appStringHtml = await renderToString (app) res.send (` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>custon-ssr</title> </head> <body> <h1>手动搭建ssr</h1> <div id="app"> ${appStringHtml} </div> </body> </html> ` )}) server.listen (3000 , () => { console .log ('Server is running on http://localhost:3000' ) })
config/server.config.js const path = require ('path' )const { VueLoaderPlugin } = require ('vue-loader' )const nodeExternals = require ('webpack-node-externals' ) module .exports = { entry : './src/server/index.js' , output : { filename : 'server_bundle.js' , path : path.resolve (__dirname, '../build/server' ) }, module : { rules : [ { test : /\.js$/i , loader : 'babel-loader' , options : { presets : ['@babel/preset-env' ] } }, { test : /\.vue$/i , loader : 'vue-loader' } ] }, resolve : { extensions : ['.js' , '.vue' ] }, plugins : [new VueLoaderPlugin ()], externals : [nodeExternals ()], target : 'node' , mode : 'development' }
entry 字段讲解
config 目录和 src 目录是同级的,为什么 entry 是 ./ 而不是 ../?
因为 webpack 的 entry 是相对于配置文件的 webpack.config.js
webpack 配置文件中的 entry 字段是相对于项目根目录的路径,而不是相对于配置文件本身
因此,无论配置文件在哪个目录下,entry 都应该是相对于项目根目录的路径。
所以这里的 ./src/client/index.js 是相对于项目根目录的路径。
package.json package.json 的 script 添加下面字段。
"script" : { "build:server" : "webpack --config ./config/server.config.js --watch" , "start" : "nodemon ./build/server/server_bundle.js" }
查看结果 命令:
打包服务端:npm run build:server
运行:npm run start
ctrl + 鼠标左键单击 终端输出的 http:localhost:3000 可在浏览器中看到静态效果,不过点击按钮是无反应的。下面我们接着写客户端配置。
搭建 客户端 新增目录结构
编写代码 src/client/index.js import { createApp } from 'vue' import App from '../App.vue' const app = createApp (App )app.mount ('#app' )
config/client.config.js const path = require ('path' )const { VueLoaderPlugin } = require ('vue-loader' )module .exports = { entry : './src/client/index.js' , output : { filename : 'client_bundle.js' , path : path.resolve (__dirname, '../build/client' ) }, module : { rules : [ { test : /\.js$/i , loader : 'babel-loader' , options : { presets : ['@babel/preset-env' ] } }, { test : /\.vue$/i , loader : 'vue-loader' } ] }, resolve : { extensions : ['.js' , '.vue' ] }, plugins : [new VueLoaderPlugin ()], target : 'web' , mode : 'development' }
进行 Hydration 水合 服务器端渲染页面 + 客户端激活页面,是页面有交互效果(这个过程称为:Hydration 水合)
Hydration 的具体步骤如下:
开发一个 App 应用,比如 App.vue。
将 App.vue 打包为一个客户端的 client_bundle.js 文件。用来激活应用,使页面有交互效果。
将 App.vue 打包为一个服务器端的 server_bundle.js 文件。用来在服务器端动态生成页面的 HTML。
server_bundle.js 渲染的页面 + client_bundle.js 文件进行 Hydration。
src/server/index.js 添加下面代码
server.use (express.static ('build' )) server.get ('/' , async (req, res) => { res.send (` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>custon-ssr</title> </head> <body> <h1>手动搭建ssr</h1> <div id="app"> ${appStringHtml} </div> <script src="/client/client_bundle.js"></script> </body> </html> ` )})
package.json package.json 的 script 添加下面字段。
"script" : { "build:client" : "webpack --config ./config/client.config.js --watch" }
查看结果 命令:
打包客户端:npm run build:client
打包服务端:npm run build:server
运行:npm run start
ctrl + 鼠标左键单击 终端输出的 http:localhost:3000 可在浏览器中看到效果,此时点击按钮是有反应的。
去除控制台警告 控制台报警告:
index.js:6 Feature flags __VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ are not explicitly defined. You are running the esm-bundler build of Vue, which expects these compile-time feature flags to be globally injected via the bundler config in order to get better tree-shaking in the production bundle. For more details, see https://link.vuejs.org/feature-flags.
去除警告,config/client.config.js 添加下面代码:
const { DefinePlugin } = require ('webpack' )module .exports = { plugins : [ new VueLoaderPlugin (), new DefinePlugin ({ __VUE_OPTIONS_API__ : false , __VUE_PROD_DEVTOOLS__ : false , __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ : false }) ], }
提取重复配置代码 现在的 config/server.config.js 和 config/client.js 有很多的重复代码,我们可以将其重复代码剔除为一个单独的文件,再在两个文件中引入。
base.config.js const { VueLoaderPlugin } = require ('vue-loader' )module .exports = { module : { rules : [ { test : /\.js$/i , loader : 'babel-loader' , options : { presets : ['@babel/preset-env' ] } }, { test : /\.vue$/i , loader : 'vue-loader' } ] }, resolve : { extensions : ['.js' , '.vue' ] }, plugins : [new VueLoaderPlugin ()], mode : 'development' }
server.config.js const path = require ('path' )const nodeExternals = require ('webpack-node-externals' ) const baseConfig = require ('./base.config' )const { merge } = require ('webpack-merge' )module .exports = merge (baseConfig, { entry : './src/server/index.js' , output : { filename : 'server_bundle.js' , path : path.resolve (__dirname, '../build/server' ) }, externals : [nodeExternals ()], target : 'node' })
client.config.js const path = require ('path' )const { DefinePlugin } = require ('webpack' )const baseConfig = require ('./base.config' )const { merge } = require ('webpack-merge' )module .exports = merge (baseConfig, { entry : './src/client/index.js' , output : { filename : 'client_bundle.js' , path : path.resolve (__dirname, '../build/client' ) }, plugins : [ new DefinePlugin ({ __VUE_OPTIONS_API__ : false , __VUE_PROD_DEVTOOLS__ : false , __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ : false }) ], target : 'web' })
搭配 vue-router 安装:npm i vue-router
注意: 为了避免跨请求状态污染,我们需要在每一个请求中都创建一个全新的 Router,也就是生成函数。
目录
代码编写 src/pages/home.vue <template > <div > <h1 > 这里是 Home 页面</h1 > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > </div > </template > <script setup > import { ref } from 'vue' const count = ref (100 )const addHandler = ( ) => { count.value ++ } </script >
src/pages/about.vue <template > <div > <h1 > 这里是 About 页面</h1 > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > </div > </template > <script setup > import { ref } from 'vue' const count = ref (1000 )const addHandler = ( ) => { count.value ++ } </script >
src/App.vue <template > <div > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > <hr /> <div > <router-link to ="/" > 首页</router-link > <router-link to ="/about" > 关于</router-link > </div > <router-view > </router-view > </div > </template > <script setup > import { ref } from 'vue' const count = ref (100 )const addHandler = ( ) => { count.value ++ } </script >
src/router/index.js import { createRouter } from 'vue-router' const routes = [ { path : '/' , component : () => import ('../pages/home.vue' ) }, { path : '/about' , component : () => import ('../pages/about.vue' ) } ] export default function (history ) { return createRouter ({ routes, history }) }
src/server/index.js 注意: 这里的 get('/') 变为了 get('/*'),因为在地址栏中 about 页面也需要通过这里访问。
const express = require ('express' )const server = express ()import { renderToString } from '@vue/server-renderer' import createApp from '../app.js' import createRouter from '../router/index.js' import { createMemoryHistory } from 'vue-router' server.use (express.static ('build' )) server.get ('/*' , async (req, res) => { const app = createApp () const router = createRouter (createMemoryHistory ()) app.use (router) await router.push (req.url || '/' ) await router.isReady () const appStringHtml = await renderToString (app) res.send (` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>custon-ssr</title> </head> <body> <h1>手动搭建ssr</h1> <div id="app"> ${appStringHtml} </div> <script src="/client/client_bundle.js"></script> </body> </html> ` )}) server.listen (3000 , () => { console .log ('Server is running on http://localhost:3000' ) })
src/client/index.js import { createApp } from 'vue' import App from '../App.vue' import createRouter from '../router/index' import { createWebHistory } from 'vue-router' const app = createApp (App )const router = createRouter (createWebHistory ())app.use (router).mount ('#app' )
查看结果 命令:
打包客户端:npm run build:client
打包服务端:npm run build:server
运行:npm run start
ctrl + 鼠标左键单击 终端输出的 http:localhost:3000 可在浏览器中看到效果。
搭配 pinia 安装:npm i pinia
目录
代码编写 src/store/index.js import { defineStore } from 'pinia' export const useCountStore = defineStore ('count' , { state ( ) { return { count : 50 } }, actions : { addHandler ( ) { this .count ++ } }, getters : { countDouble ( ) { return this .count * 2 } } })
src/pages/home.vue <template > <div > <h1 > 这里是 Home 页面</h1 > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > </div > </template > <script setup > import { useCountStore } from '../store' import { storeToRefs } from 'pinia' const countStore = useCountStore ()const { count } = storeToRefs (countStore)const addHandler = ( ) => { count.value ++ } </script >
src/pages/about.vue <template > <div > <h1 > 这里是 About 页面</h1 > <div style ="border: 1px solid #eee" > <h1 > {{ count }}</h1 > <button @click ="addHandler" > 点击增加</button > </div > </div > </template > <script setup > import { useCountStore } from '../store' import { storeToRefs } from 'pinia' const countStore = useCountStore ()const { count } = storeToRefs (countStore)const addHandler = ( ) => { count.value ++ } </script >
src/server/index.js const express = require ('express' )const server = express ()import { renderToString } from '@vue/server-renderer' import createApp from '../app.js' import createRouter from '../router/index.js' import { createMemoryHistory } from 'vue-router' import { createPinia } from 'pinia' server.use (express.static ('build' )) server.get ('/*' , async (req, res) => { const app = createApp () const router = createRouter (createMemoryHistory ()) app.use (router).use (createPinia ()) await router.push (req.url || '/' ) await router.isReady () const appStringHtml = await renderToString (app) res.send (` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>custon-ssr</title> </head> <body> <h1>手动搭建ssr</h1> <div id="app"> ${appStringHtml} </div> <script src="/client/client_bundle.js"></script> </body> </html> ` )}) server.listen (3000 , () => { console .log ('Server is running on http://localhost:3000' ) })
src/client/index.js import { createApp } from 'vue' import App from '../App.vue' import createRouter from '../router/index' import { createWebHistory } from 'vue-router' import { createPinia } from 'pinia' const app = createApp (App )const router = createRouter (createWebHistory ())app.use (router).use (createPinia ()).mount ('#app' )
查看结果 命令:
打包客户端:npm run build:client
打包服务端:npm run build:server
运行:npm run start
ctrl + 鼠标左键单击 终端输出的 http:localhost:3000 可在浏览器中看到效果。
个人简介:脆弱的种子在温室也会死亡,坚强的种子,在沙漠也能发芽。