最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue2 手写一个简易的服务端渲染 - vue ssr(含vuex+vue-router)

    正文概述 掘金(小桂summer)   2021-01-17   636

    前言

    • 为什么服务端渲染
    • 客户端不利于SEO搜索引擎优化
    • 服务端渲染是可以被爬虫抓到的, 客户端很难被抓取到
    • SSR直接将HTML字符串传递给浏览器 大大加快了首屏加载时间
    • 但同时SSR占用更多的CPU和内存资源
    • 一些常用的浏览器API可能无法使用
    • 只支持vue的beforeCreate和created两个生命周期
    • 本文服务用的是node
    • Vue SSR 指南
    vue2 手写一个简易的服务端渲染 - vue ssr(含vuex+vue-router)

    示例

    src/app.vue

    <template>
      <div id="app">
        <router-link to="/">foo</router-link>
        <router-link to="/bar">bar</router-link>
        <router-view></router-view>
      </div>
    </template>
    
    

    src/Bar.vue

    <template>
      <div>
        <!-- lisi -->
        {{ $store.state.name }}  
      </div>
    </template>
    
    <script>
      export default {
          /**
           * @description 在服务端执行的方法 这个方法在后端执行
           */
          asyncData(store){
              return store.dispatch('changeName')
          },
    
          /**
           * @description 服务端也会执行 beforeCreate 和 created
           */
          beforeCreate() {
            console.log('服务端会调用beforeCreate')
          },
          created() {
            console.log('服务端会调用created')
          },
    
          /**
           * @description 浏览器执行 后端忽略
           */
          mounted() {
            console.log('服务端不会调用mounted')
          }
    
      }
    </script>
    
    <style scoped="true">
        div {
          width: 100%;
          height: 50px;
          line-height: 50px;
          background: goldenrod;
        }
    </style>
    
    
    

    src/components/Foo.vue

    <template>
        <div @click="show">foo</div>
    </template>
    <script>
    export default {
        methods:{
            show(){
                alert('前端逻辑操作, 与服务端渲染无关')
            }
        }
    }
    </script>
    

    效果

    服务端掉用vue的钩子 只支持beforeCreate 和 create, 在bar.vue文件中

    vue2 手写一个简易的服务端渲染 - vue ssr(含vuex+vue-router)

    vue2 手写一个简易的服务端渲染 - vue ssr(含vuex+vue-router)

    基本流程

    • 将导出的vue实例分成两份 一份是客户端要打包的 一份是服务端要打包的 (都是webpack打包)
    • 客户端打包的是针对客端使用的(比如一些事件 视图操作 路由跳转等)
    • 服务端打包的是node server要执行的函数
    • 在node server中通过VueServerRenderer中的createBundleRenderer方法 去调用函数(server-entry.js导出的函数), 获取实例
    • 将要执行的函数结合createBundleRenderer选项中添加的html模板(模板要加入<!--vue-ssr-outlet-->)
    • 通过.renderToString根据实例生成一个字符串 传给浏览器

    有vue-router流程

    • 这里用到模式是history(问题刷新时返回404)
    • 用户输入url, 服务端会将路径传给render函数
    • 在函数中向让路由跳转完毕router.push(url)
    • 跳转完毕后通过router.onReady方法(这可以有效确保服务端渲染时服务端和客户端输出的一致)
    • 接着router.getMatchedComponents()获取当前路由的所有组件(返回数组)
    • 如果length等于0 没有匹配路由 服务端返回not found(前端路由也可做处理, 具体看router.js文件)

    有vuex流程

    • 服务端渲染的数据 只针对路由可以访问的组件
    • 组件有一个函数asyncData 专为服务端调用
    • 当用户输入完路径 组件中有asyncData 并调用
    • context.state = store.state 将其挂载到window.__INITIAL_STATE__
    • 接着浏览器 开始渲染 将服务端加载好的数据替换掉

    目录结构

    ├── config
    │   ├── webpack.base.js
    │   ├── webpack.client.js
    │   └── webpack.server.js
    ├── dist
    │   ├── client.bundle.js
    │   ├── index.html
    │   ├── index.ssr.html
    │   └── server.bundle.js
    ├── public
    │   ├── index.html
    │   └── index.ssr.html
    ├── src
    │   ├── App.vue
    │   ├── components
    │   │   ├── Bar.vue
    │   │   └── Foo.vue
    │   ├── entry-client.js
    │   ├── entry-server.js
    │   ├── router.js
    │   ├── store.js
    │   └── app.js
    ├── server.js
    └── package.json
    
    

    有哪些包

    * vue vuex vue-router
    
    # node 服务端
    * koa
    * koa-router                监听路由
    * koa-static                前端静态文件、图片等静态资源处理模块。配置静态资源目录后,将不会出现静态资源not found错误
    * vue-server-renderer       node服务渲染vue
    
    # webpack 打包部分
    * webpack
    * webpack-cli       
    * webpack-merge             合并
    * babel-loader              webpack和babel的一个桥梁
    * @babel/core               babel的核心模块(默认直接调用)
    * @babel/preset-env         把es6+转换成低级语法
    * vue-loader                解析.vue
    * vue-template-compiler     编译模板
    * css-loader                解析css样式
    * vue-style-loader          解析的css文件插入到style标签中(vue-style-loader支持服务端渲染)
    
    * concurrently              可以同时执行两个脚本命令
    * nodemon                   修改node服务端直接更新, 无需重新启动
    

    package.json

    "scripts": {
        "client:dev": "webpack serve --config scripts/webpack.client.js",
        "client:build": "webpack --config scripts/webpack.client.js --watch",
        "server:build": "webpack --config scripts/webpack.server.js --watch",
        "run:all": "concurrently \"npm  run client:build\" \"npm run server:build\""
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "concurrently": "^5.3.0",
        "koa": "^2.13.1",
        "koa-router": "^10.0.0",
        "koa-static": "^5.0.0",
        "vue": "^2.6.12",
        "vue-router": "^3.4.9",
        "vue-server-renderer": "^2.6.12",
        "vuex": "^3.6.0",
        "webpack-merge": "^5.7.3"
      },
      "devDependencies": {
        "@babel/core": "^7.12.10",
        "@babel/preset-env": "^7.12.11",
        "babel-loader": "^8.2.2",
        "css-loader": "^5.0.1",
        "html-webpack-plugin": "^4.5.1",
        "vue-loader": "^15.9.6",
        "vue-style-loader": "^4.1.2",
        "vue-template-compiler": "^2.6.12",
        "webpack": "^5.13.0",
        "webpack-cli": "^4.3.1"
      }
    

    正题

    src/app.js

    import Vue from 'vue'
    import App from './App.vue'
    
    /**
     * @description 入口改装成了函数 目的是服务端渲染时 每次访问的适合都可以通过这个工厂函数返回一个全新的实例
     * @description 保证每个人访问都可以拿到一个自己的实例
     */
    export default () => {
        const app = new Vue({
            render: h => h(App)
        })
        return { app }
    }
    
    

    src/client-entry.js

    /** 客户端 */
    import createApp from './app.js'
    let {app} = createApp()
    app.$mount('#app')
    
    

    src/server-entry.js

    /** 服务端入口 */
    import createApp from './app.js'
    
    /**
     * @description 服务端渲染可以返回一个函数
     * @description 每次都能产生一个新的应用
     * @param {context} 调用方法时 服务端会传入url
     */
    export default (context)=>{
        const { url } = context
        
        /** 路由是异步组件 promise等待路由加载完毕 */
        return new Promise((resolve, reject) => {
            let { app, router, store } = createApp()
    
            // 要跳转的url路径
            router.push(url)
    
            // 路由跳转完毕后 组件触发
            router.onReady(() => {
                const matchComponents = router.getMatchedComponents()
    
                // 没有匹配到
                if (matchComponents.length == 0) {
                    return reject({ code: 404 })
    
                } else {
    
                    // matchComponents 指的是当前路径下的所有组件
                    // 服务端在渲染的时候 默认会找当前路径下的所有组间中的asyncData
                    // 并且在服务端也会创建一个vuex 传递给asyncData(store)
                    Promise.all(matchComponents.map(component => {
                        if (component.asyncData) {
                            return component.asyncData(store)
                        }
                    })).then(() => {
                        // 会默认在window下生成一个变量 内部默认就这样做的
                        // window.__INITIAL_STATE__ = {"name":"jiangwen"}
                        // 服务器执行完毕后 最新的状态保存在store.state上
                        context.state = store.state
    
                        // app 是已经获取到数据的实例
                        resolve(app)
                    })
    
                }
    
            })
    
        })
    
    }
    
    
    

    config/webpack.base.js

    /** 打包的公告配置文件 */
    
    const path = require('path')
    const VueLoaderPlugin = require('vue-loader/lib/plugin')
    
    module.exports = {
        mode: 'development', // 开发模式
        output: {
            filename: '[name].bundle.js',
            path: path.resolve(__dirname,'../dist')
        },
        module: {
            rules: [
                {
                    test: /\.vue$/,
                    use: 'vue-loader'
                },
                {
                    test: /\.js$/,
                    use: {
                        loader: 'babel-loader',     // babel-loader自动调用 @babel/core -> preset-env
                        options: {
                            presets: ['@babel/preset-env'],
                        }
                    },
                    exclude: /node_modules/ // node_modules文件排除
                },
                {
                    test: /\.css$/,
                    /** 执行顺序 从右向左执行 从下向上执行 */
                    use: [
                        'vue-style-loader',
                        {
                            loader: 'css-loader',
                            options: {
                                esModule: false, // 注意为了配套使用vue-style-loader 置为false 不然样式出不来
                            }
                        }
                    ]
                }
            ]
        },
    
        plugins: [
            /** 固定的 */
            new VueLoaderPlugin()
        ]
    }
    
    

    config/webpack.client.js

    const { merge } = require('webpack-merge')
    const base = require('./webpack.base')
    const path = require('path')
    const  HtmlWebpackPlugin = require('html-webpack-plugin')
    
    /**
     * @description 客户端打包入口
     */
    module.exports = merge(base,{
        entry: {
            client: path.resolve(__dirname, '../src/client-entry.js')
        },
    
        plugins:[
            new HtmlWebpackPlugin({ // html模板
                template: path.resolve(__dirname, '../public/index.html'),
                filename:'client.html' // 修改导出后的默认名(index.html)
            }),
        ]
    
    })
    
    

    config/webpack.server.js

    const base = require('./webpack.base')
    const { merge } = require('webpack-merge')
    const  HtmlWebpackPlugin = require('html-webpack-plugin')
    const path = require('path')
    
    /** 服务端打包入口 */
    module.exports = merge(base,{
        target: 'node', // node 使用
        entry: {
            server: path.resolve(__dirname, '../src/server-entry.js')
        },
        output:{
            libraryTarget: "commonjs2" // export.modules
        },
        plugins:[
            new HtmlWebpackPlugin({ /** html 模板 */
                template: path.resolve(__dirname, '../public/index.ssr.html'),
                filename: 'server.html',
                excludeChunks: ['server'], // 不让server文件注入
                minify: false, // 不压缩
                client:'/client.bundle.js' // 加个参数 方便注入 index.ssr.html模板
            }),
        ]
    })
    
    

    public/index.ssr.html 模板

    <!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>
        <!--vue-ssr-outlet-->
        <script src="/client.bundle.js"></script>
    
        <!-- ejs模板 -->
        <script src="<%=htmlWebpackPlugin.options.client%>"></script>
    </body>
    </html>
    
    

    server.js 服务端文件

    const Koa = require('koa')
    const app = new Koa()
    const Router = require('koa-router')
    const router = new Router()
    const VueServerRenderer = require('vue-server-renderer')
    const static = require('koa-static')
    
    // 读取文件
    const fs = require('fs')
    const path = require('path')
    const serverBundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.bundle.js'), 'utf8')
    const template = fs.readFileSync(path.resolve(__dirname, 'dist/server.html'), 'utf8')
    
    // 根据实例
    const render = VueServerRenderer.createBundleRenderer(serverBundle, {
        template
    })
    
    router.get('/', async (ctx) => {
        ctx.body = await new Promise((resolve, reject) => {
            render.renderToString({url: ctx.url}, (err, html) => { // 如果想让css生效 只能使用回调的方式
                if (err) reject(err);
                resolve(html)
            })
        })
    })
    
    // 只要用户刷新就会像服务器发请求
    router.get('/(.*)',async (ctx)=>{
        ctx.body = await new Promise((resolve, reject) => {
            // 通过服务端渲染 渲染后返回
            render.renderToString({url: ctx.url}, (err, html) => {
                if (err && err.code == 404) resolve(`not found`)
                
                console.log("? ~ file: server.js ~ line 36 ~ render.renderToString ~ html", html)
                resolve(html)
    
            })
        })
    })
    
    app.use(static(path.resolve(__dirname, 'dist')))
    app.use(router.routes())
    
    app.listen(3000)
    

    起源地下载网 » vue2 手写一个简易的服务端渲染 - vue ssr(含vuex+vue-router)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元