vue项目ssr试探(一)

目前做的商城项目处于优化阶段,客户提出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到这里就告一段落了,下一篇记录下,如何将异步的数据插入到页面中~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容