vue項(xiàng)目ssr試探(一)

目前做的商城項(xiàng)目處于優(yōu)化階段展懈,客戶提出SEO優(yōu)化销睁,讓我們給出ssr服務(wù)端渲染的方案;此時(shí)我的內(nèi)心是拒絕的存崖;項(xiàng)目做了幾年冻记,做完了弄服務(wù)端渲染?伊克斯扣斯密来惧?用nuxt嗎冗栗?重新開(kāi)發(fā)?哦不!怎樣增量的嵌入式的修改現(xiàn)有的代碼贞瞒,能做到服務(wù)端渲染偶房,是我這段時(shí)間來(lái)苦心研究的目標(biāo);雖然目前客戶沒(méi)有提這個(gè)需求了军浆,也是為自己做一個(gè)技術(shù)儲(chǔ)備吧棕洋,下面記錄我是如何跟著官網(wǎng)給出的文檔一步步走下去,實(shí)現(xiàn)了一個(gè)ssr的小demo的

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

思路

所謂服務(wù)端渲染乒融,個(gè)人理解就是數(shù)據(jù)是從后端獲取掰盘,然后前端訪問(wèn)url,后端把獲取的數(shù)據(jù)插入到html中然后直接給前端返回HTML頁(yè)面赞季,這樣有利于搜索引擎優(yōu)化愧捕,能夠被瀏覽器收錄;同時(shí)也加快了頁(yè)面的打開(kāi)時(shí)間申钩,降低白屏?xí)r間次绘,帶來(lái)更好的用戶體驗(yàn)。

第一步:webpack快速搭建vue項(xiàng)目&進(jìn)行改造

本著從零開(kāi)始撒遣,再一次重新認(rèn)識(shí)webpack的心態(tài)邮偎,沒(méi)有用vue-cli搭建項(xiàng)目;畢竟后期如果是真的要改造項(xiàng)目的話义黎,還是要對(duì)webpack有比較深入的認(rèn)識(shí)禾进。


image.png

上圖是Vue官方ssr原理的介紹圖,從這張圖我們可以知道廉涕,最后webpack打包后會(huì)生成兩個(gè)bundle文件泻云,這兩個(gè)文件分別作用于不同的渲染。

  • Client Bundle:用于瀏覽器渲染狐蜕,這個(gè)就是我們正常項(xiàng)目的普通打包
  • Server Bundle:用于服務(wù)端渲染

vue項(xiàng)目做ssr宠纯,不管是用腳手架搭建還是自己純手工搭建,都需要用到vue-server-renderer這個(gè)庫(kù)
下面是手工搭建的項(xiàng)目結(jié)構(gòu)圖:


image.png

webpack.config.js: webpack打包的基礎(chǔ)配置
webpack.client.conf.js: 瀏覽器渲染打包配置
webpack.server.conf.js: ssr打包配配置
index.ssr.html: 顧名思義這個(gè)是用來(lái)做ssr的
server.js: 是服務(wù)端的配置
entry-client.js: 客戶端打包入口
entry-server.js: 服務(wù)端打包入口
這里我把index.html拆分成了兩個(gè)层释,這樣可以區(qū)別出ssr征椒,進(jìn)行單獨(dú)的配置。
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模式刷新報(bào)錯(cuò)
    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編譯的錯(cuò)誤顯示在網(wǎng)頁(yè)上面
    },
    open: true // 在啟用webpack-dev-server時(shí)湃累,自動(dòng)打開(kāi)瀏覽器
  }
  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']
    }),
  ]
})

上面這三個(gè)是主要的webpack的配置勃救,主要是做了入口的分別打包處理,分別用client.js和server.js作為入口治力;這兩個(gè)就是我們正常項(xiàng)目app.js或者main.js中打包的入口文件
entry-server.js和entry-client.js區(qū)別:
entry-client.js是在瀏覽器端執(zhí)行蒙秒,需要掛載dom,啟動(dòng)瀏覽器渲染宵统,所以需要手動(dòng)的調(diào)用$mount()方法晕讲。
entry-server.js是在服務(wù)端調(diào)用覆获,因此需要導(dǎo)入一個(gè)函數(shù),返回一個(gè)vue的實(shí)例瓢省。

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

關(guān)于 webpack.server.conf.js弄息,有兩個(gè)注意點(diǎn):
libraryTarget: 'commonjs2' → 因?yàn)榉?wù)器是 Node,所以必須按照 commonjs 規(guī)范打包才能被服務(wù)器調(diào)用勤婚。
target: 'node' → 指定 Node 環(huán)境摹量,避免非 Node 環(huán)境特定 API 報(bào)錯(cuò),如 document 等馒胆。

編寫(xiě)服務(wù)端渲染主要邏輯

  • Vue SSR 依賴于包 vue-server-render缨称,它的調(diào)用支持兩種入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 組件為入口祝迂,后者以打包后的 JS 文件為入口睦尽,這里采用第二種。
    server.js:
server.use(express.static('dist'));

// server.js 服務(wù)端渲染主體邏輯
// dist/server.js 就是以 entry-server.js 為入口打包出來(lái)的 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('服務(wù)器內(nèi)部錯(cuò)誤');
        return;
      }
      res.status = 200
      res.type = 'text/html; charset=utf-8'
      res.body = html
      res.end(html);
      resolve(html);
    })
  });
})

server.listen(8002, () => {
  console.log('后端渲染服務(wù)器啟動(dòng)型雳, 端口為:8002');
})
  • 兩個(gè)打包入口(entry)当凡,重構(gòu)app, store, router, 為每個(gè)對(duì)象增加工廠方法createXXX

每個(gè)用戶通過(guò)瀏覽器訪問(wèn)Vue頁(yè)面時(shí),都是一個(gè)全新的上下文纠俭,但在服務(wù)端沿量,應(yīng)用啟動(dòng)后就一直運(yùn)行著,處理每個(gè)用戶請(qǐng)求的都是在同一個(gè)應(yīng)用上下文中柑晒。為了不串?dāng)?shù)據(jù)欧瘪,需要為每次SSR請(qǐng)求眷射,創(chuàng)建全新的app, store, router匙赞。
index.js核心代碼

// createApp工廠方法
export default function (ssrContext) {
  const store = createStore(); // 創(chuàng)建全新store實(shí)例
  const router = createRouter();
  // 創(chuàng)建Vue應(yīng)用
  const app = new Vue({
    store,
    router,
    ssrContext,
    render: (h) => h(App)
  })
  return { app, store, router }
}

router.js創(chuàng)建router工廠函數(shù)

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

store.js 創(chuàng)建store工廠函數(shù)

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: '我是數(shù)據(jù)'})
          })
        })
        // action必須返回promise
        return p.then(data => {
          console.log(data);
          commit('detail', data.data);
        })
      }
    }
  })

entry-client.js:

// 創(chuàng)建所需要的app實(shí)例
const { app, router, store } = createApp();
// 當(dāng)路由加載完后掛載dom,渲染
router.onReady(() => {
  // 將Vue實(shí)例掛載到dom中妖碉,完成瀏覽器端應(yīng)用啟動(dòng)
  app.$mount('#app')
})

entry-server.js:

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

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

做到這里基本的ssr的工作已經(jīng)完成了涌庭,使用npm run build打包,然后node server.js啟動(dòng)服務(wù)欧宜,在瀏覽器訪問(wèn)localhost:8002坐榆,就能看到效果啦~完美!訪問(wèn)首localhost:8002/index在network中可以看到返回html中有提前插入的數(shù)據(jù)冗茸;而普通的是沒(méi)有任何內(nèi)容的只有<div id="app"></div>

  • ssr:


    image.png
  • 普通:


    image.png

總結(jié)

這篇主要講了項(xiàng)目的搭建的一些webpack配置席镀,服務(wù)端代碼,多入口配置以及router夏漱,store豪诲,app的改造;在這個(gè)過(guò)程中主要碰到幾個(gè)問(wèn)題:

  1. 詳情頁(yè)面:localhost:8002/detail/1,跳轉(zhuǎn)正常挂绰,刷新頁(yè)面報(bào)錯(cuò)屎篱。查看network,發(fā)現(xiàn)刷新后會(huì)去加載localhost:8002/detail/1/client.boundle.js;而我們的這個(gè)js文件是與index.ssr.html同級(jí)的, 改publicPath:'./'為publicPath:'/'交播。
  2. 啟動(dòng)服務(wù)之后重虑,打開(kāi)url,頁(yè)面一直在請(qǐng)求
    服務(wù)端沒(méi)有返回html秦士;在服務(wù)端缺厉,express框架在攔截到所有路由之后,查閱了別人的處理返回一個(gè)promise伍宦,然后再resolve(html)就可以了; 我也照做了芽死,但是瀏覽器會(huì)出現(xiàn)一直轉(zhuǎn)圈圈,解決辦法在resolve(html)之前加上res.end(html)就好了次洼。


    image.png

這篇初步試探ssr到這里就告一段落了关贵,下一篇記錄下,如何將異步的數(shù)據(jù)插入到頁(yè)面中~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卖毁,一起剝皮案震驚了整個(gè)濱河市揖曾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亥啦,老刑警劉巖炭剪,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異翔脱,居然都是意外死亡奴拦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)届吁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)错妖,“玉大人,你說(shuō)我怎么就攤上這事疚沐≡萋龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵亮蛔,是天一觀的道長(zhǎng)痴施。 經(jīng)常有香客問(wèn)我,道長(zhǎng)究流,這世上最難降的妖魔是什么辣吃? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮芬探,結(jié)果婚禮上神得,老公的妹妹穿的比我還像新娘。我一直安慰自己灯节,他們只是感情好循头,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布绵估。 她就那樣靜靜地躺著,像睡著了一般卡骂。 火紅的嫁衣襯著肌膚如雪国裳。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天全跨,我揣著相機(jī)與錄音缝左,去河邊找鬼。 笑死浓若,一個(gè)胖子當(dāng)著我的面吹牛渺杉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挪钓,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼是越,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了碌上?” 一聲冷哼從身側(cè)響起倚评,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎馏予,沒(méi)想到半個(gè)月后天梧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霞丧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年呢岗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛹尝。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡后豫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出箩言,到底是詐尸還是另有隱情硬贯,我是刑警寧澤焕襟,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布陨收,位于F島的核電站,受9級(jí)特大地震影響鸵赖,放射性物質(zhì)發(fā)生泄漏务漩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一它褪、第九天 我趴在偏房一處隱蔽的房頂上張望饵骨。 院中可真熱鬧,春花似錦茫打、人聲如沸居触。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)轮洋。三九已至制市,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弊予,已是汗流浹背祥楣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汉柒,地道東北人误褪。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像碾褂,于是被迫代替她去往敵國(guó)和親兽间。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容