vue的两种服务器端渲染方案
作者:京东零售姜欣
关于服务器端渲染方案,之前只接触了基于react的Next。js,最近业务开发vue用的比较多,所以调研了一下vue的服务器端渲染方案。首先:长文预警,下文包括了两种方案的实践,没有耐心的小伙伴可以直接跳到方案标题下,down代码体验一下。前置知识:1、什么是服务器端渲染(ssr)?
简单来说就是用户第一次请求页面时,页面上的内容是通过服务器端渲染生成的,浏览器直接显示服务端返回的完整html就可以,加快首屏显示速度。
举个栗子:
当我们访问一个商品列表时,如果使用客户端渲染(csr),浏览器会加载空白的页面,然后下载js文件,通过js在客户端请求数据并渲染页面。如果使用服务器端渲染(ssr),在请求商品列表页面时,服务器会获取所需数据并将渲染后的HTML发送给浏览器,浏览器一步到位直接展示,而不用等待数据加载和渲染,提高用户的首屏体验。2、服务器端渲染的优缺点
优点:
(1)更好的seo:抓取工具可以直接查看完全渲染的页面。现在比较常用的交互是页面初始展示loading菊花图,然后通过异步请求获取内容,但是但抓取工具并不会等待异步完成后再行抓取页面内容。
(2)内容到达更快:不用等待所有的js都完成下载并执行,所以用户会更快速地看到完整渲染的页面。
缺点:
(1)服务器渲染应用程序,需要处于Node。jsserver运行环境
(2)开发成本比较高
总结:
总得来说,决定是否使用服务器端渲染,取决于具体的业务场景和需求。对于具有大量静态内容的简单页面,客户端渲染更合适一些,因为它可以更快地加载页面。但是对于需要从服务器动态加载数据的复杂页面,服务器端渲染可能是一个更好的选择,因为他可以提高用户的首屏体验和搜索引擎优化。
下面进入正文方案一:vue插件vueserverrender
git示例demo地址
结论前置:不建议用,配置成本高
官网地址:https:v2。ssr。vuejs。orgzh
首先要吐槽一下官网,按官网教程比较难搞,目录安排的不太合理,一顿操作项目都没起来。。。
并且官网示例的构建配置代码是webpack4的,现在初始化项目后基本安装的都是webpack5,有一些语法不同(1)首先,先初始化一个npm项目,然后安装依赖得到一个基础项目。(此处要注意vueserverrenderer和vue必须匹配版本)npminityyarnaddvuevueserverrendererSyarnaddexpressSyarnaddwebpackwebpackclifriendlyerrorswebpackpluginvueloaderbabelloaderbabelcoreurlloaderfileloadervuestyleloadercssloadersassloadersasswebpackmergewebpacknodeexternalsDyarnaddcleanwebpackpluginbabelpresetenvDyarnaddrimraf模拟linx的删除命令,在build时先删除distyarnaddwebpackdevmiddlewarewebpackhotmiddlewareDyarnaddchokidarD监听变化yarnaddmemoryfsDyarnaddnodemonD。。。实在太多,如有缺失可以在package。json中查找另外:我现在用的vueloader:15。9。0版本,之前用的是vueloader:17。0。1,报了一个styles的错(2)配置app。js,entryclient。js,entryserver。js,将官网参考中的示例代码拷贝至对应文件。
app。jsimportVuefromvueimportAppfrom。App。vueimport{createRouter}from。routerimport{createStore}from。storeimport{sync}fromvuexroutersync导出一个工厂函数,用于创建新的应用程序、router和store实例exportfunctioncreateApp(){创建router和store实例constroutercreateRouter()conststorecreateStore()sync(store,router)constappnewVue({router,store,render:hh(App)})return{app,router,store}}
entryclient。jsimportVuefromvueimport{createApp}from。appVue。mixin({beforeMount(){const{asyncData}this。optionsif(asyncData){this。dataPromiseasyncData({store:this。store,route:this。route})}}})const{app,router,store}createApp()if(window。INITIALSTATE){store。replaceState(window。INITIALSTATE)}router。onReady((){在初始路由resolve后执行,使用router。beforeResolve(),以便确保所有异步组件都resolve。router。beforeResolve((to,from,next){constmatchedrouter。getMatchedComponents(to)constprevMatchedrouter。getMatchedComponents(from)找出两个匹配列表的差异组件letdiffedfalseconstactivatedmatched。filter((c,i){returndiffed(diffed(prevMatched〔i〕!c))})if(!activated。length){returnnext()}Promise。all(activated。map(c{if(c。asyncData){returnc。asyncData({store,route:to})}}))。then((){next()})。catch(next)})app。mount(app)})
entryserver。jsimport{createApp}from。appexportdefaultcontext{返回一个promise,服务器能够等待所有的内容在渲染前,已经准备就绪,returnnewPromise((resolve,reject){const{app,router,store}createApp()router。push(context。url)router。onReady((){constmatchedComponentsrouter。getMatchedComponents()if(!matchedComponents。length){returnreject({code:404})}对所有匹配的路由组件调用asyncData()Promise。all(matchedComponents。map(Component{if(Component。asyncData){returnComponent。asyncData({store,route:router。currentRoute})}}))。then((){context。statestore。stateresolve(app)})。catch(reject)},reject)})}(3)在根目录下创建server。js文件
其中一个非常重要的api:createBundleRenderer,这个api上面有一个方法renderToString将代码转化成html字符串,主要功能就是把用webpack把打包后的服务端代码渲染出来。具体了解可看官网bundlerenderer指引。server。jsconstapprequire(express)()const{createBundleRenderer}require(vueserverrenderer)constfsrequire(fs)constpathrequire(path)constresolvefilepath。resolve(dirname,file)constisProdprocess。env。NODEENEproductionconstcreateRenderer(bundle,options){returncreateBundleRenderer(bundle,Object。assign(options,{basedir:resolve(。dist),runInNewContext:false,}))}letrenderer,readyPromiseconsttemplatePathresolve(。srcindex。template。html)if(isProd){constbundlerequire(。distvuessrserverbundle。json)constclientManifestrequire(。distvuessrclientmanifest。json)consttemplatefs。readFileSync(templatePath,utf8)renderercreateRenderer(bundle,{推荐template,(可选)页面模板clientManifest(可选)客户端构建manifest})}else{开发模式readyPromiserequire(。configsetupdevserver)(app,templatePath,(bundle,options){renderercreateRenderer(bundle,options)})}constrender(req,res){constcontext{title:hellossrwithwebpack,meta:metacharsetUTF8metanameviewportcontentwidthdevicewidth,initialscale1。0metahttpequivXUACompatiblecontentieedge,url:req。url}renderer。renderToString(context,(err,html){if(err){if(err。code404){res。status(404)。end(Pagenotfound)}else{res。status(500)。end(InternalServerError)}}else{res。end(html)}})}在服务器处理函数中app。get(,isProd?render:(req,res){readyPromise。then(()render(req,res))})app。listen(8080)监听的是8080端口(4)接下来是config配置
在根目录新增config文件夹,然后新增四个配置文件:webpack。base。config,webpack。client。config,webpack。server。config,setupdevserver(此方法是一个封装,为了配置个热加载,差点没搞明白,参考了好多)(官网传送门:构建配置)
大部分官网有示例代码,但是要在基础上进行一些更改
webpack。base。configwebpack。base。configconstpathrequire(path)用来处理后缀为。vue的文件const{VueLoaderPlugin}require(vueloader)constFriendlyErrorsWebpackPluginrequire(friendlyerrorswebpackplugin)定位到根目录constresolve(dir)path。join(path。resolve(dirname,。。),dir)打包时会先清除一下const{CleanWebpackPlugin}require(cleanwebpackplugin)constisProdprocess。env。NODEENVproductionmodule。exports{mode:isProd?production:development,output:{path:resolve(dist),publicPath:dist,filename:〔name〕。〔chunkhash〕。js},resolve:{alias:{public:resolve(public)}},module:{noParse:es6promise。js,rules:〔{test:。vue,loader:vueloader,options:{compilerOptions:{preserveWhiteSpace:false}}},{test:。js,loader:babelloader,exclude:nodemodules},{test:。(pngjpggifsvg),loader:urlloader,options:{limit:10000,name:〔name〕。〔ext〕?〔hash〕}},{test:。s(ac)ss?,use:〔vuestyleloader,cssloader,sassloader〕}〕},performance:{hints:false},plugins:〔newVueLoaderPlugin(),编译后的友好提示,比如编译完成或者编译有错误newFriendlyErrorsWebpackPlugin(),打包时会先清除一下newCleanWebpackPlugin()〕}
webpack。client。configwebpack。client。configconst{merge}require(webpackmerge)constbaseConfigrequire(。webpack。base。config。js)constVueSSRClientPluginrequire(vueserverrendererclientplugin)module。exportsmerge(baseConfig,{entry:{app:。srcentryclient。js},optimization:{重要信息:这将webpack运行时分离到一个引导chunk中,以便可以在之后正确注入异步chunk。这也为你的应用程序vendor代码提供了更好的缓存。splitChunks:{name:manifest,minChunks:Infinity}},plugins:〔此插件在输出目录中生成vuessrclientmanifest。json。newVueSSRClientPlugin()〕})
webpack。server。configwebpack。server。configconst{merge}require(webpackmerge)constnodeExternalsrequire(webpacknodeexternals)webpack的基础配置,比如sass,less预编译等constbaseConfigrequire(。webpack。base。config。js)constVueSSRServerPluginrequire(vueserverrendererserverplugin)module。exportsmerge(baseConfig,{将entry指向应用程序的serverentry文件entry:。srcentryserver。js,target:node,对bundlerenderer提供sourcemap支持devtool:sourcemap,此处告知serverbundle使用Node风格导出模块(Nodestyleexports)output:{libraryTarget:commonjs2},https:webpack。js。orgconfigurationexternalsfunctionhttps:github。comliadywebpacknodeexternals外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的bundle文件。externals:nodeExternals({不要外置化webpack需要处理的依赖模块。你可以在这里添加更多的文件类型。例如,未处理。vue原始文件,你还应该将修改global(例如polyfill)的依赖模块列入白名单allowlist:。css}),这是将服务器的整个输出构建为单个JSON文件的插件。默认文件名为vuessrserverbundle。jsonplugins:〔newVueSSRServerPlugin()〕})
setupdevserver:封装createRenderer方法constwebpackrequire(webpack)constfsrequire(fs)constpathrequire(path)constchokidarrequire(chokidar)constmiddlewarerequire(webpackdevmiddleware)constHMRrequire(webpackhotmiddleware)constMFSrequire(memoryfs)constclientConfigrequire(。webpack。client。config)constserverConfigrequire(。webpack。server。config)constreadFile(fs,file){try{returnfs。readFileSync(path。join(clientConfig。output。path,file),utf8)}catch(error){}}constsetupServer(app,templatePath,cb){letbundleletclientManifestlettemplateletreadyconstreadyPromisenewPromise(rreadyr)templatefs。readFileSync(templatePath,utf8)constupdate(){if(bundleclientManifest){通知server进行渲染执行createRendererRenderToStringready()cb(bundle,{template,clientManifest})}}webpackentryserverbundleconstmfsnewMFS();constserverCompilerwebpack(serverConfig);serverCompiler。outputFileSystemmfs;serverCompiler。watch({},(err,stats){if(err)throwerr之后读取输出:statsstats。toJson()stats。errors。forEach(errconsole。error(err))stats。warnings。forEach(errconsole。warn(err))if(stats。errors。length)returnbundleJSON。parse(readFile(mfs,vuessrserverbundle。json))update()});clientConfig。plugins。push(newwebpack。HotModuleReplacementPlugin())clientConfig。entry。app〔webpackhotmiddlewareclient,clientConfig。entry。app〕clientConfig。output。filename〔name〕。jsconstclientCompilerwebpack(clientConfig);constdevMiddlewaremiddleware(clientCompiler,{noInfo:true,publicPath:clientConfig。output。publicPath,logLevel:silent})app。use(devMiddleware);app。use(HMR(clientCompiler));clientCompiler。hooks。done。tap(clientsBuild,stats{statsstats。toJson()stats。errors。forEach(errconsole。error(err))stats。warnings。forEach(errconsole。warn(err))if(stats。errors。length)returnclientManifestJSON。parse(readFile(devMiddleware。fileSystem,vuessrclientmanifest。json))update()})fstemplatePathtemplatechokidar。watch(templatePath)。on(change,(){templatefs。readFileSync(templatePath,utf8)console。log(templateisupdated);update()})returnreadyPromise}module。exportssetupServer(5)配置搞完了接下来是代码渲染
在src目录下,新增index。template。html文件,将官网中的例子(地址:使用一个页面模板)复制,并进行一些更改htmlhead!使用双花括号(doublemustache)进行HTML转义插值(HTMLescapedinterpolation)title{{title}}title!使用三花括号(triplemustache)进行HTML不转义插值(nonHTMLescapedinterpolation){{{meta}}}headbody!这个是告诉我们在哪里插入正文的内容!vuessroutletbodyhtml(6)再搞个store和api模拟一下数据请求
这里介绍一下一个很重要的东西asyncData预取数据,预取数据是在vue挂载前,所以下文这里用了上下文来获取store而不是thisasyncData:({store}){returnstore。dispatch(getDataAction)},
在src下创建api文件夹,并在下面创建data。js文件data。jsconstgetData()newPromise((resolve){setTimeout((){resolve(〔{id:1,item:测试1},{id:2,item:测试2},〕)},1000)})export{getData}
在src下创建store文件夹,并在下面创建index。js文件store。jsimportVuefromvueimportVuexfromvuexVue。use(Vuex)import{getData}from。。apidataexportfunctioncreateStore(){returnnewVuex。Store({state:{lists:〔〕},actions:{getDataAction({commit}){returngetData()。then((res){commit(setData,res)})}},mutations:{setData(state,data){state。listsdata}}})}(7)编写组件,在srccomponents文件夹下写两个组件,在app。vue中引用一下,用上刚写的模拟数据
Hello。vuetemplate这里是测试页面一p{{item}}routerlinktohello1链接到测试页面二routerlinktemplatestylelangscssscopedstyle
Hello1。vuetemplate这里是测试页面二{{item}}templatestylelangscssscopedstyle(8)配置路由并在app。vue使用路由
router。jsimportVuefromvueimportRouterfromvuerouterVue。use(Router)exportfunctioncreateRouter(){returnnewRouter({mode:history,routes:〔{path:hello,component:()import(。componentsHello。vue)},{path:hello1,component:()import(。componentsHello1。vue)},〕})}
app。vuetemplaterouterviewrouterviewtemplatestylelangscssscopedstyle(9)根目录下创建一个。babelrc,进行配置{presets:〔〔babelpresetenv,{modules:false}〕〕}(10)改写package。json执行命令dev:nodemonserver。js,build:rimrafdistnpmrunbuild:clientnpmrunbuild:server,build:client:webpackconfigconfigwebpack。client。config。js,build:server:webpackconfigconfigwebpack。server。config。js
大搞告成,执行一下dev命令,可以通过访问localhost:8080端口看到页面,记得带上路由哦~
执行build命令可看到,最后dist文件下共有三个文件:main。〔chunkhash〕。js,vuessrclientmanifest。json,vuessrserverbundle。json
附上文件整体目录结构
方案二:基于vue的nuxt。js通用应用框架
git示例demo地址
一对比,这个就显得丝滑多了~官网地址:nuxt。js
先对比一下两种方案的差别1。vue初始化虽然有cli,但是nuxt。js的cli更加完备2。nuxt有更合理的工程化目录,vue过于简洁,比如一些component,api文件夹都是要手动创建的3。路由配置:传统应用需要自己来配置,nuxt。js自动生成4。没有统一配置,需手动创建。nuxt。js会生成nuxt。config。js5。传统不易与管理底层框架逻辑(nuxt支持中间件管理,虽然我还没探索过这里)
显而易见这个上手就快多了,也不需要安装一大堆依赖,如果用了sass需要安装sass和sassloader,反正我是用了(1)创建一个项目可选npm,npx,yarn,具体看官方文档npminitnuxtappprojectname(2)pages下面创建几个文件
nuxt是通过pages页面形成动态的路由,不用手动配置路由。比如在pages下面新增了个文件about。vue,那么这个页面对应的路由就是about
其实这个时候运行npmrundev就可以看到简单的页面了(3)模拟接口
这里介绍一个插件,可以快速创建一个服务npmijsonserver
安装完后,在根目录新增db。json文件,模拟几个接口{post:〔{id:1,title:jsonserver,author:jx}〕,comments:〔{id:1,body:somecomment,postId:1}〕,profile:{name:typicode}}
运行命令jsonserverwatchdb。jsonport8000(不加会端口冲突),就可以看到
因为是get请求,可以直接点击访问可以看到mock的数据已经返回了
(4)页面调用
先配置一下axios,推荐使用nuxt。js封装的axios:nuxtjsaxios:5。13。6,然后再在nuxt。config。js文件中modules下面配置一下就可以使用了modules:〔nuxtjsaxios〕,
随便找个接口调用一下template这里是测试页面一接口返回数据:{{posts}}template
刷新下页面就可以看到效果了,这里注意axios有两个get方法,一个axios。get还会返回头部等信息,另一个axios。get只返回结果
总结:
从页面篇幅上应该也能看到哪个容易上手了,nuxt相对于插件来说限定了文件夹的结构,并通过此预定了一些功能,更好上手。预设了利用vue。js开发服务端渲染所需要的各种配置,并且提供了提供了静态站点,异步数据加载,中间件支持,布局支持等