美文网首页Vue知识点Vue
vue项目ssr试探(一)

vue项目ssr试探(一)

作者: 想做个文人 | 来源:发表于2019-10-16 16:48 被阅读0次

目前做的商城项目处于优化阶段,客户提出SEO优化,让我们给出ssr服务端渲染的方案;此时我的内心是拒绝的;项目做了几年,做完了弄服务端渲染?伊克斯扣斯密?用nuxt吗?重新开发?哦不!怎样增量的嵌入式的修改现有的代码,能做到服务端渲染,是我这段时间来苦心研究的目标;虽然目前客户没有提这个需求了,也是为自己做一个技术储备吧,下面记录我是如何跟着官网给出的文档一步步走下去,实现了一个ssr的小demo的

vue官方ssr指南:https://ssr.vuejs.org/zh/

思路

所谓服务端渲染,个人理解就是数据是从后端获取,然后前端访问url,后端把获取的数据插入到html中然后直接给前端返回HTML页面,这样有利于搜索引擎优化,能够被浏览器收录;同时也加快了页面的打开时间,降低白屏时间,带来更好的用户体验。

第一步:webpack快速搭建vue项目&进行改造

本着从零开始,再一次重新认识webpack的心态,没有用vue-cli搭建项目;毕竟后期如果是真的要改造项目的话,还是要对webpack有比较深入的认识。


image.png

上图是Vue官方ssr原理的介绍图,从这张图我们可以知道,最后webpack打包后会生成两个bundle文件,这两个文件分别作用于不同的渲染。

  • Client Bundle:用于浏览器渲染,这个就是我们正常项目的普通打包
  • Server Bundle:用于服务端渲染

vue项目做ssr,不管是用脚手架搭建还是自己纯手工搭建,都需要用到vue-server-renderer这个库
下面是手工搭建的项目结构图:


image.png

webpack.config.js: webpack打包的基础配置
webpack.client.conf.js: 浏览器渲染打包配置
webpack.server.conf.js: ssr打包配配置
index.ssr.html: 顾名思义这个是用来做ssr的
server.js: 是服务端的配置
entry-client.js: 客户端打包入口
entry-server.js: 服务端打包入口
这里我把index.html拆分成了两个,这样可以区别出ssr,进行单独的配置。
index.ssr.html

<body>
  <div id="app">
    <!--vue-ssr-outlet--> ssr
  </div>
  <script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>

webpack.config.js主要配置:

···
const config = {
  // entry: path.join(__dirname, 'src/app.js'),
  output: {
    filename: '[name].bundle.js',
    publicPath: '/', // history模式刷新报错
    path: path.join(__dirname, '/dist')
  },
  resolve: {
    alias: {
      vue: 'vue/dist/vue.js'
    },
    extensions: ['.vue', '.ts', '.tsx', '.js', '.json']
  },
  plugins: [...plugins],
  module: {
    rules:[...rules],
  }
}
if(isDev){
  config.devtool = '#cheap-module-eval-source-map'
  config.devServer = {
    port: 8005,
    host: '0.0.0.0',
    overlay: {
        errors: true // 将webpack编译的错误显示在网页上面
    },
    open: true // 在启用webpack-dev-server时,自动打开浏览器
  }
  config.plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  )
}
module.exports = config;

webpack.client.conf.js核心配置:

···
module.exports = merge(base, {
  entry: {
    client: './entry-client.js',
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: './index.html',
      files: {
        js: '/client.bundle.js',
      },
      filename: 'index.html',
    }),
  ]
})

webpack.server.conf.js核心配置:

···
module.exports = merge(base, {
  target: 'node',
  entry: {
    server: './entry-server.js',
  },
  output: {
    filename: '[name].js',
    libraryTarget: 'commonjs2',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.ssr.html',
      filename: 'index.ssr.html',
      files: {
        js: '/client.bundle.js',
      },
      excludeChunks: ['server']
    }),
  ]
})

上面这三个是主要的webpack的配置,主要是做了入口的分别打包处理,分别用client.js和server.js作为入口;这两个就是我们正常项目app.js或者main.js中打包的入口文件
entry-server.js和entry-client.js区别:
entry-client.js是在浏览器端执行,需要挂载dom,启动浏览器渲染,所以需要手动的调用$mount()方法。
entry-server.js是在服务端调用,因此需要导入一个函数,返回一个vue的实例。

export default function createApp() {
 const app = new Vue({
   render: h => h(App)
 });
 return app; 
};

关于 webpack.server.conf.js,有两个注意点:
libraryTarget: 'commonjs2' → 因为服务器是 Node,所以必须按照 commonjs 规范打包才能被服务器调用。
target: 'node' → 指定 Node 环境,避免非 Node 环境特定 API 报错,如 document 等。

编写服务端渲染主要逻辑

  • Vue SSR 依赖于包 vue-server-render,它的调用支持两种入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 组件为入口,后者以打包后的 JS 文件为入口,这里采用第二种。
    server.js:
server.use(express.static('dist'));

// server.js 服务端渲染主体逻辑
// dist/server.js 就是以 entry-server.js 为入口打包出来的 JS 
const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
  template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')
});

server.get('*', (req, res) => {
  console.log(req.url)
  const context = { url: req.url, pageTitle: 'default-title' }
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      if (err) {
        console.log(err);
        res.status(500).end('服务器内部错误');
        return;
      }
      res.status = 200
      res.type = 'text/html; charset=utf-8'
      res.body = html
      res.end(html);
      resolve(html);
    })
  });
})

server.listen(8002, () => {
  console.log('后端渲染服务器启动, 端口为:8002');
})
  • 两个打包入口(entry),重构app, store, router, 为每个对象增加工厂方法createXXX

每个用户通过浏览器访问Vue页面时,都是一个全新的上下文,但在服务端,应用启动后就一直运行着,处理每个用户请求的都是在同一个应用上下文中。为了不串数据,需要为每次SSR请求,创建全新的app, store, router。
index.js核心代码

// createApp工厂方法
export default function (ssrContext) {
  const store = createStore(); // 创建全新store实例
  const router = createRouter();
  // 创建Vue应用
  const app = new Vue({
    store,
    router,
    ssrContext,
    render: (h) => h(App)
  })
  return { app, store, router }
}

router.js创建router工厂函数

export function createRouter () {
  return new VueRouter({
    mode: 'history',
    fallback: false,
    routes: [
      {
        path: '/index',
        name: 'index',
        component: Index
      },
    ]
  })
}

store.js 创建store工厂函数

export default function createStore() {
  return new Vuex.Store({
    state: {
      detail: '',
    },
    getters: {
      getDetail(state) {
        return state.detail;
      }
    },
    mutations: {
      detail(state, arg) {
        state.detail = arg;
        console.log(state)
      }
    },
    actions: {
      getDetail({commit}, payload) {
        var p = new Promise((resolve) => {
          setTimeout(() => {
            resolve({data: '我是数据'})
          })
        })
        // action必须返回promise
        return p.then(data => {
          console.log(data);
          commit('detail', data.data);
        })
      }
    }
  })

entry-client.js:

// 创建所需要的app实例
const { app, router, store } = createApp();
// 当路由加载完后挂载dom,渲染
router.onReady(() => {
  // 将Vue实例挂载到dom中,完成浏览器端应用启动
  app.$mount('#app')
})

entry-server.js:

export default function (context) {
  return new Promise((resolve, reject) => {
    const { app, router, store} = createApp(context);
    // 设置路由
    router.push(context.url)

    router.onReady(() => {
        context.state = store.state;
        resolve(app);
  })
}

做到这里基本的ssr的工作已经完成了,使用npm run build打包,然后node server.js启动服务,在浏览器访问localhost:8002,就能看到效果啦~完美!访问首localhost:8002/index在network中可以看到返回html中有提前插入的数据;而普通的是没有任何内容的只有<div id="app"></div>

  • ssr:


    image.png
  • 普通:


    image.png

总结

这篇主要讲了项目的搭建的一些webpack配置,服务端代码,多入口配置以及router,store,app的改造;在这个过程中主要碰到几个问题:

  1. 详情页面:localhost:8002/detail/1,跳转正常,刷新页面报错。查看network,发现刷新后会去加载localhost:8002/detail/1/client.boundle.js;而我们的这个js文件是与index.ssr.html同级的, 改publicPath:'./'为publicPath:'/'。
  2. 启动服务之后,打开url,页面一直在请求
    服务端没有返回html;在服务端,express框架在拦截到所有路由之后,查阅了别人的处理返回一个promise,然后再resolve(html)就可以了; 我也照做了,但是浏览器会出现一直转圈圈,解决办法在resolve(html)之前加上res.end(html)就好了。


    image.png

这篇初步试探ssr到这里就告一段落了,下一篇记录下,如何将异步的数据插入到页面中~

相关文章

网友评论

    本文标题:vue项目ssr试探(一)

    本文链接:https://www.haomeiwen.com/subject/jftsmctx.html