為什麼要寫這篇文章呢?因為給團隊的同學提了要求,要有思考,設計和產出。很多時候技術實現都是一個idea,想到就搞了,搞了之後又覺得如此簡單,如此簡單還回顧它做什麼呢?其實不然,每一個方案的落地,可能都是N個想法相互拼殺的結果。
團隊維護了一個Landing Page系統,準備將這個系統平臺化,平臺化就需要具備組件的多元化的能力,這樣才能夠適應更多的場景,滿足更多的用戶。目前的所有組件都是標準組件,包含在整個Landing Page平臺中;所以頁面的首屏、SEO和性能完全可以基於同構策略去實現。
基於目前的同構策略,csr和ssr都用的是同一份代碼,那如果要引入自定義組件,就會涉及到重新編譯。那麼將面臨如下兩個問題1、自定義組件將會越來越多,如果每次都重新編譯,代碼會越來越大。2、自定義組件發布了不能立即使用,需要重新發布Landing Page的服務。
首先最容易想到的解決方案,是動態組件,但是動態組件不能很好的支持seo和首屏,所以這個idea在腦袋中過了三秒就pass了。
然後想想SSR和CSR的能夠保持一致的本質是虛擬dom計算的結果是一致的,那麼要保證結果一致,就一定要保證服務端代碼和客戶端代碼是一致的嗎?答案是是否定的。
下面我們來看一下,通過build以後的client和server的代碼結構(基於nuxt腳手架)
顯而易見client和server的js代碼完全是不一樣的,client由於要考慮按需和文件大小的問題將項目的js文件按照一定規則拆成了若干份,但是server端則將所有內容都打包進了server.js。
所以可以得到一個簡單的結論,我們可以將客戶端的代碼按照自己的想法,拆成一份份的,那如果我們把所有的自定義組件當成一份文件或者幾分文件不就ok了嗎?
具體實現:
1、將組件庫通過webpack打包成若干個文件,並根據commonjs2和iife原則分別打包成兩份,配置如下
commonjs配置
const path = require('path');const ProgressBarPlugin = require('progress-bar-webpack-plugin');const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpackConfig = { mode: 'production', devtool: false, entry: { 'comp1': './comp1.js', 'comp2': './comp2.js', 'comp3': './comp3.js', }, output: { path: path.resolve(process.cwd(), './lib'), publicPath: '/dist/', filename: '[name].js', libraryTarget: 'commonjs2' }, resolve: { extensions: ['.js', '.vue', '.json'], modules: ['node_modules'] }, performance: { hints: false }, stats: 'none', optimization: { minimize: false }, module: { rules: [ { test: /\.(jsx?|babel|es6)$/, include: process.cwd(), loader: 'babel-loader' }, { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/, loader: 'url-loader' } ] }, plugins: [ new ProgressBarPlugin(), new VueLoaderPlugin() ]};
module.exports = webpackConfig;iife配置
const path = require('path');const ProgressBarPlugin = require('progress-bar-webpack-plugin');const VueLoaderPlugin = require('vue-loader/lib/plugin');
const webpackConfig = { mode: 'production', devtool: false, entry: { 'comp1': './comp1.js', 'comp2': './comp2.js', 'comp3': './comp3.js' }, output: { path: path.resolve(process.cwd(), './lib'), publicPath: '/dist/', filename: '[name].iife.js', iife: true }, resolve: { extensions: ['.js', '.vue', '.json'], modules: ['node_modules'] }, performance: { hints: false }, stats: 'none', optimization: { minimize: false }, module: { rules: [ { test: /\.(jsx?)$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: ['@vue/babel-plugin-transform-vue-jsx'] } } }, { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/, loader: 'url-loader' } ] }, plugins: [ new ProgressBarPlugin(), new VueLoaderPlugin() ]};
module.exports = webpackConfig;import comp from './comp1.vue';
function regiseterComp(vue) { vue.component('comp1', comp);}
if (typeof window !== 'undefined') { window.dynamicCompFuncList = window.dynamicCompFuncList || []; window.dynamicCompFuncList.push(regiseterComp);}
export default regiseterComp;1、由於在客戶端中,我們需要擇機觸發組件的註冊;
所以我們需要將組件組冊緩存起來,找一個合適的時機觸發。
2、將自定義組件引入代碼邏輯中,保證客戶端和服務端正確的渲染邏輯 (得益於webpack的打包邏輯,打包的client的時候,執行不到的引用不會被打包)
module.exports = function (vue) { if (process.server) { const comp1 = require('../../dynamic-comp/lib/comp1.js').default; const comp2 = require('../../dynamic-comp/lib/comp2.js').default; const comp3 = require('../../dynamic-comp/lib/comp3.js').default; comp1(vue); comp2(vue); comp3(vue); } else { if (window.dynamicCompFuncList) { window.dynamicCompFuncList.forEach((fn) => { fn(vue); }); } }};3、保證客戶端的正確渲染結果,需要將動態組件庫優先加載,才能通過 window.dynamicCompFuncList獲取到,並在客戶端渲染前註冊需要的組件。
最簡單的辦法就是將script內置到head裡,nuxt的中間件可以很容易地做到這件事情
項目跑起來,想要的結果就實現了,這樣的實踐也產生了一些問題,需要再做一些優化:
1、內嵌script容易造成頁面html內容很多,不利於網絡傳輸;
2、動態計算需要的自定義組件依賴(這個需要別的平臺提供能力來配合實現);
3、實時獲取自定義組件,或者通過mq獲取新增組件,動態改變服務端渲染的自定義入口文件(服務端公用一個vue實例,需要重新啟動);