从零搭建SSR及Nxut配置和踩坑记录

学习SSR的原因

学习nuxt的一个根本原因就是为了SEO,要了解SEO,我们要知道,浏览器爬虫的工作流程以及SPA、SSG、SSR这些概念只有这样我们才能了解到为什么学习Nuxt以及为什么学习Nuxt主要解决了什么问题

SPA

单页面程序(SPA)全称是:Single-page-application,SPA应用是在客户端(即浏览器端渲染)术语称CSR

SPA特点
  1. SPA默认只返回一个空白HTML页面,并没有具体的内容
  2. 整个应用程序的内容是通过JS动态加载
  3. 常见框架:React、Vue、Angular
SPA优缺点
  1. 优点
    1. 只需加载一次,页面切换等都不需要重新加载,
    2. 更好的用户体验,相比于传统的Web应用程序,SPA更快,而且更加流畅
  2. 缺点
    1. 由于SPA应用程序只返回一个空白HTML页面,不利于SEO(原因详情看爬虫工作流程)
    2. 首屏加载资源过大
    3. 不利于构建复杂项目
SSG

静态站点生成(SSG)全称是:Static site Generate,即预先生成好的静态网站,一般用于官方文档或者博客网站比较多

SSG特点
  1. SSG应用一般在构建阶段就确定了网站的内容(页面内容是静态的而并非是请求服务器出来的)
  2. 如果网站内容需要更新,就必须重新构建和部署
SSG优缺点
  1. 优点
    1. 访问速度比较快,每个HTML页面都是提前生成好的
    2. 有利于SEO
    3. 保留了SPA应用特性
  2. 缺点
    1. 页面是静态的,不利于展示实时性的内容
    2. 站点更新就必须重新构建部署
SSR

服务器端渲染(SSR)全称是:Server Side Render,即在服务器端渲染页面,将渲染好的HTML返回给浏览器呈现

SSR特点
  1. SSR应用的页面是在服务器端渲染的,用户每次请求都会将渲染好的页面给浏览器进行呈现
SSR优缺点
  1. 优点
    1. 更快的渲染速度,由于服务器返回的是渲染好的页面而并非是JS动态生成的内容,而且不需要加载完整个应用才能访问
    2. 更好的SEO
    3. 保留了SPA应用特性
  2. 缺点
    1. SSR由于是在服务器端渲染的,所以需要更多的服务器资源,成本高
    2. 增加了一定的开发成本
爬虫工作流程

学习SSR的一大目的就是利于SEO,要搞清楚这些就要知道爬虫的基本工作流程

爬虫工作流程

我们在使用浏览器搜索时,浏览器给我们呈现的内容就是爬虫爬取结果后根据一定的算法呈现出来的,简单来说浏览器整个就是爬虫爬取出来的,那么它的工作流程到底是什么样呢

  1. 抓取

爬虫会在网络中发现各种网页,将网页中的爬取的内容存放到临时库中,网页中如果遇到其他网站,就重复该过程

  1. 索引编制

爬取完结果后,爬虫会对爬取的数据进行分析(例如title元素、图片、视频等),将爬取的网页进行归档分类,并且会对临时库中的信息进行筛选不符合规则的会被清理,最后会把爬虫的结果符合规则的存放到索引区供用户搜索时呈现

  1. 呈现搜索结果

用户搜索时,搜索引擎会根据内容的类型,选择一组更加符合规则的呈现给用户

什么是SEO

SEO是搜索引擎优化(Search Engine Optimization)的缩写,是一种通过优化网站的内容、结构和技术等方面,以提高网站在搜索引擎中的排名和曝光度的方法和策略。

为什么SPA不利于SEO

SEO是为了提高网站在搜索引擎中的排名和曝光度,了解了爬虫的工作流程,我们就知道排序中最重要的是索引编制阶段,而该阶段又借助于我们的页面内容,SPA返回的又是空白页面,所以不利于SEO

从零搭建SSR

我们采用Node和webpack来搭建vue的SSR项目,这个只是一个简单的项目,主要目的是为了了解一下搭建SSR的流程,后面我也会使用Nuxt进行搭建Vue SSR项目

前期准备

安装依赖

npm i express
npm i -D nodemon
npm i -D webpack webpack-cli webpack-node-externals
npm i vue
npm i -D vue-loader
npm i -D babel-loader @babel/preset-env
npm i -D webpack-merge webpack-node-externals
npm i vue-router -D
npm i pinia
项目基本文件夹目录
project
|
└───build 打包后的代码
│   │
│   └───client
│   └───server
│   
└───config 打包配置文件
|   └───  base.config.js
|   └───  client.config.js
|   └───  server.config.js
│   
└── src 源代码
|   └───client 客户端
|   |   └─── index.js 入口文件
|   └───router 路由
|   |   └─── index.js 入口文件
|	  └───server 服务器端
|   |   └─── index.js 入口文件
|   └───store pinia
|   |   └─── index.js 入口文件
|   └───views 视图
|   |    └─── about.vue
|   |   └─── home.vue
|   └───app.js
|   └───App.vue
|   └───package-lock.josn
|   └───package.json
package.json文件脚本命令配置
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "nodemon ./src/server/index",
  "start": "nodemon ./build/server/server_bundle.js",
  "build:server": "webpack --config ./config/server.config.js --watch",
  "build:client": "webpack --config ./config/client.config.js --watch"
},

命令详情:

  1. npm run dev 启动服务器端代码
  2. npm run start 启动服务
  3. npm run build:server 打包服务器端代码
  4. npm ruin build:client 打包客户端代码

需要注意的是我们在运行时是要有顺序的,要先把客户端和服务端代码打包后再启动服务

配置文件内容

配置文件是webpack打包配置,我们需要打包服务器端代码和客户端代码,由于两者打包配置有重复点所以新建一个基础的配置文件,并使用webpack-merge去合并配置

  1. base.config.js
let { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
  mode: "development",
  module: {
    rules: [
    {
    test: /\.js$/,
  loader: "babel-loader",
  options: {
    presets: ["@babel/preset-env"],
  },
},
{
  test: /\.vue$/,
  loader: "vue-loader",
},
  ],
},
plugins: [new VueLoaderPlugin()],
resolve: {
  // 添加的后缀,项目中导包就不需要编写文件后缀
  extensions: [".js", ".json", ".wasm", ".jsx", ".vue"],
},
};
  1. client.config.js
let path = require("path");
let { DefinePlugin } = require("webpack");
let { merge } = require("webpack-merge");
let baseConfig = require("./base.config");
module.exports = merge(baseConfig, {
  target: "web", //fs path
  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,
}),
],
});
  1. server.config.js
let path = require("path");
let nodeExternals = require("webpack-node-externals");
let { VueLoaderPlugin } = require("vue-loader/dist/index");
let { merge } = require("webpack-merge");
let baseConfig = require("./base.config");
module.exports = merge(baseConfig, {
  target: "node", //fs path
  entry: "./src/server/index.js",
  output: {
    filename: "server_bundle.js",
    path: path.resolve(__dirname, "../build/server"),
  },
  externals: [nodeExternals()], //排除node_module中的包
});
src 源代码
  1. client/index.js
import { createApp } from "vue";
import App from "../App.vue";

import createRouter from "../router";
import { createWebHashHistory } from "vue-router";
import { createPinia } from "pinia";

let app = createApp(App);

let router = createRouter(createWebHashHistory());
app.use(router);

let pinia = createPinia();
app.use(pinia);

router.isReady().then(() => {
  //等待路由加载完成之后再挂载
  app.mount("#app");
});

  1. router/index.js
import { createRouter } from "vue-router";

const routes = [
  {
    path: "/",
    component: () => import("../views/home.vue"),
  },
  {
    path: "/about",
    component: () => import("../views/about.vue"),
  },
];

export default function (history) {
  return new createRouter({
    history,
    routes,
  });
}
  1. server/index.js
let express = require("express");

let server = express();
import createApp from "../app";
import { renderToString } from "@vue/server-renderer";
// 部署静态资源
server.use(express.static("build"));

import createRouter from "../router";
// 内存路由=>node用
import { createMemoryHistory } from "vue-router";
import { createPinia } from "pinia";

server.get("/*", async (req, res) => {
  let app = createApp();
  let router = createRouter(createMemoryHistory());
  /* 
  服务器端和客户端都注册路由的原因是为了实现路由同步
  用户进入页面时将渲染好的字符串返回(服务器端返回正确的html字符串)
  在页面跳转可以无刷新跳转(客户端可以继续跳转)
  */
  app.use(router);
  // 跳转页面(路由跳转完成之后再渲染)
  await router.push(req.url || "/");
  await router.isReady(); //等待(异步)路由加载完成,再渲染页面
  // 创建pinia
  const pinpa = createPinia();
  app.use(pinpa);
  // 注册路由
  let 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>Document</title>
  </head>
  <body>
  <div id="app">    
    ${appStringHtml}
  </div>
  <script src="/client/client_bundle.js"></script>
  </body>
  </html>
  `);
});

server.listen(3000, () => {
  console.log("服务器启动成功");
});

  1. store/index
import { defineStore } from "pinia";

export const useHomeStore = defineStore("home", {
  state() {
    return {
      count: 1,
    };
  },
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});

  1. views/about.vue
<template>
  <div
    class="app"
    style="border: 1px solid blue"
    >
    <h2>About</h2>
    <div>{{ count }}</div>
    <button @click="addCounter">+1</button>
  </div>
</template>
<script setup>
  import { storeToRefs } from "pinia";
  import { useHomeStore } from "../store/home";
  const store = useHomeStore();
  // storeToRefs使用该方法解构的值是双向绑定
  const { count } = storeToRefs(store);
  const addCounter = () => {
    count.value++;
  };
</script>

<style scoped></style>

  1. views/home.vue
<template>
  <div
    class="app"
    style="border: 1px solid green"
    >
    <h2>Home</h2>
    <div>{{ count }}</div>
    <button @click="addCounter">+1</button>
  </div>
</template>
<script setup>
  import { ref } from "vue";
  import { storeToRefs } from "pinia";
  import { useHomeStore } from "../store/home";
  const store = useHomeStore();
  const { count } = storeToRefs(store);

  const addCounter = () => {
    count.value++;
  };
</script>

<style scoped></style>

  1. app.js
import { createSSRApp } from "vue";

import App from "./App.vue";
// 写函数返回app实例,作用是避免跨请求状态的污染
// 通过函数来返回app实例,可以保证每个请求都会返回一个新的app实例
export default function createApp() {
  return createSSRApp(App);
}

  1. App.vue
<template>
  <div
    class="app"
    style="border: 1px solid red"
    >
    <h2>Vue App</h2>
    <div>{{ count }}</div>
    <button @click="addCounter">+1</button>
    <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(0);
  const addCounter = () => {
    count.value++;
  };
</script>

<style scoped></style>

为了方便大家查看,我这里附上源代码的仓库连接
https://github.com/XY0987/blog_vue3_ssr

Nuxt介绍

Nuxt.js是一个基于Vue.js的通用应用框架,它可以帮助开发者快速构建高性能的单页应用(SPA)和静态网站。Nuxt.js基于Vue.js的生态系统,提供了许多有用的功能和约定,使得开发过程更加简单和高效。

Nuxt项目初始化

创建项目
  1. 方式一
mpx nuxi init 项目名
  1. 方式二
pnpm dix nuxi init 项目名
  1. 方式三
npm i -g nuxi
nuxi init 项目名
项目报错

由于墙的原因,我们再构建项目时大概率会报错,这是我们有两种解决方法

  1. 方法一

配置host,本地dns解析
Mac电脑host配置路径: /stc/hosts
Window电脑host配置路径: c/Windows/System32/drivers/etc/hosts
新增配置

185.199.110.133 raw.githubusercontent.com
  1. 方法二

手动克隆项目(有时候我们配置上述方法也不行,比如我的电脑就不行,挂vpn也不行,只能开加速器手动克隆了)

git clone -b v3 https://github.com/nuxt/starter.git 文件夹名
npm i
命令详解
  1. 打包
npm run build
  1. 运行
npm run dev
  1. 生成静态站点
npm run generate
  1. 预览打包文件
npm run preview
  1. 生成类型文件
npm run postinstall
项目目录结构详解
project
|
└───assets 静态资源
│   
└───components 组件
│   
└── composables 组合API
|   
└── layout 自定义布局
|
└── pages 页面,nuxt会根据页面目录结构和文件名自动注册路由
|
└── plugins 插件
|
└── app.vue 入口文件
|
└── app.config.ts 配置文件
|
└── nuxt.config.ts nuxt配置文件
|
└── package-lock.json
|
└── package.json
|
└── tsconfig.json ts配置

总结

在学习过程中遇到比较难受点就是下载依赖比较卡顿,还有就是有时候即使跟着网上的教程但是还是不行,chargpt给答案也是模棱两可,还是得去看文档或者去github中的issues去找答案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值