<p><code></p> <p>不久前,vue 升级至了 2.3.0 版本,是一个 minor 的版本。<a href="https://github.com/vuejs/vue/releases/tag/v2.3.0" rel="nofollow noreferrer" target="_blank">该版本</a>除了一些组件功能的优化之外,主要是升级 vue 的 ssr 功能,甚至于为之建立了一个独立的 <a href="https://ssr.vuejs.org/en/" rel="nofollow noreferrer" target="_blank">Git Book</a>。</p> <p>我的博客之前用的就是 ssr,这次升级自然也是要尝试一把。ssr 的优势和实现在这里就不再赘述了,不太了解的可以看<a href="https://discipled.me/posts/ssr" rel="nofollow noreferrer" target="_blank">之前的文章</a>,这里主要还是来看看升级的变化之处。</p> <p>升级的第一件事自然就是先升级依赖,将 vue, vue-server-renderer 等依赖的版本升级至最新 <code>npm up -S</code>(作者 vue 的版本为 v2.3.3)。升级之后,直接启动服务看看,应该是没有问题的,文档也提到可以使用之前的配置,但建议改为新版本的方式。</p> <p>虽然,依赖升级之后同样能运行,但还是来看看有哪些提升或变化的地方?</p> <ul> <li> <p><a>Renderer Create Options</a></p> </li> <li> <p><a>Lifesycle &amp; data prefetch</a></p> </li> <li> <p><a>代码结构与同构</a></p> </li> <li> <p><a>Webpack build plugin</a></p> </li> <li> <p><a>结尾</a></p> </li> </ul> <p><a></a></p> <h2 id="articleHeader0">Renderer Create Options</h2> <p>更新之后,在创建 renderer 时可以为它添加配置,其中的 <code>template</code> 属性可以为我们省去之前的许多繁杂的小工作,比如:</p> <ul> <li> <p>在 html 中使用 <code>&lt;!--vue-ssr-out<a href="http://www.js-code.com/tag/let" title="let" target="_blank">let</a>--&gt;</code>,renderer 会自动将 app 生成的 html 插入此处,而不用自己再进行替换操作</p> </li> <li> <p>将 <code>context.state</code> 插入到 html 中,并自动使用 <a href="https://github.com/yahoo/serialize-javascript" rel="nofollow noreferrer" target="_blank">serialize-javascript</a> 进行转义来防止 XSS 攻击</p> </li> <li> <p>直接通过 <code>cache</code> 属性配置组件缓存</p> </li> </ul> <p>以上这些都是在之前版本中常被使用到的,剩下一些 <code>clientManifest</code>, <code>inject</code>, <code>runInNewContext</code> 等新增的东西后面会再提到。</p> <p><a></a></p> <h2 id="articleHeader1">Lifesycle &amp; data prefetch</h2> <p>由于在 ssr 阶段不会有一系列的变更,所以更新之后 vue 在 ssr 阶段只会执行 <code>beforeCreate</code> 和 <code>created</code> 这个两个生命周期函数。</p> <p>相信你一定会问那如果遇到异步请求该怎么办哪?这里同之前并没有变化,仍旧是通过设置组件的自定义方法来获取数据,最终通过 vuex 将数据传递回客户端。没什么变化就不展开了,不清楚的可以看一下<a href="https://ssr.vuejs.org/en/data.html" rel="nofollow noreferrer" target="_blank">文档</a>,写得已经相当详细了。</p> <div class="google-auto-placed ap_container" style="text-align: center; width: 100%; height: auto; clear: none;"><ins data-ad-format="auto" class="adsbygoogle adsbygoogle-noablate" data-ad-client="ca-pub-6330872677300335" data-adsbygoogle-status="done" style="display: block; margin: auto; background-color: transparent;"><ins id="aswift_4_expand" style="display: inline-table; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 697px; background-color: transparent;"><ins id="aswift_4_anchor" style="display: block; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 697px; background-color: transparent; overflow: hidden; opacity: 0;"><iframe width="697" height="175" frameborder="0" marginwidth="0" marginheight="0" vspace="0" hspace="0" allowtransparency="true" scrolling="no" allowfullscreen="true" onload="var i=this.id,s=window.google_iframe_oncopy,H=s&amp;&amp;s.handlers,h=H&amp;&amp;H[i],w=this.contentWindow,d;try{d=w.document}catch(e){}if(h&amp;&amp;d&amp;&amp;(!d.body||!d.body.firstChild)){if(h.call){setTimeout(h,0)}else if(h.match){try{h=s.upd(h,i)}catch(e){}w.location.replace(h)}}" id="aswift_4" name="aswift_4" style="left:0;position:absolute;top:0;border:0px;width:697px;height:175px;"></iframe></ins></ins></ins></div> <p>不过此处有一点优化,由于数据已经在服务器端已准备完成,客户端就无需再像服务器端发送异步请求,而是可以直接从 store 中获取数据。</p> <p><a></a></p> <h2 id="articleHeader2">代码结构与同构</h2> <p>文档的<a href="https://ssr.vuejs.org/en/structure.html" rel="nofollow noreferrer" target="_blank">这一节</a>在内容上和之前的文档基本没有区别,不过其中提到一点指出了我原有代码的不足之处,也给了我不少启发。</p> <p>通常大家的 app.js 会是这样</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="// 省略其他依赖... import store from './vuex'; import router from './router'; sync(store, router); const app = new Vue({ store, router, render: h => h(/* ... */)<br /> });</p> <p>export {app, router, store};" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="JavaScript"><span class="hljs-comment">// 省略其他依赖...</span> <span class="hljs-keyword">import</span> store <span class="hljs-keyword">from</span> <span class="hljs-string">'./vuex'</span>; <span class="hljs-keyword">import</span> router <span class="hljs-keyword">from</span> <span class="hljs-string">'./router'</span>; sync(store, router); <span class="hljs-keyword"><a href="http://www.js-code.com/tag/const" title="浏览关于“const”的文章" target="_blank" class="tag_link">const</a></span> app = <span class="hljs-keyword">new</span> <a href="http://www.js-code.com/tag/vue" title="浏览关于“Vue”的文章" target="_blank" class="tag_link">Vue</a>({ store, router, <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-params">h</span> =&gt;</span> h(<span class="hljs-comment">/* ... */</span>) }); <span class="hljs-keyword"><a href="http://www.js-code.com/tag/export" title="浏览关于“export”的文章" target="_blank" class="tag_link">export</a></span> {app, router, store};</code></pre> <p>这看上去并没有任何问题。在平时的浏览器环境中,每次刷新页面都会重新加载一次文件,是一个全新的环境(或沙盒)。但当同构了代码之后,服务器端同样运行这段代码时,就可能出现问题。</p> <p>因为 <a href="http://www.js-code.com/tag/node" title="node" target="_blank">node</a> 端服务启动后,vue 的实例就被初始化完成,所有的请求会公用这同一个实例,这就可能造成混乱。所以为每个请求返回一个新的 vue 的实例是一个比较好的处理方法,router 和 store 同样适用这个道理。</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="// 省略其他依赖... import createStore from './vuex'; import createRouter from './router'; const createApp = () => {<br /> const store = createStore();<br /> const router = createRouter();</p> <p> sync(store, router);</p> <p> const app = new Vue({<br /> store,<br /> router,<br /> render: h => h(/* ... */)<br /> });</p> <p> return {app, router, store};<br /> };</p> <p>export default createApp;" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="JavaScript"><span class="hljs-comment">// 省略其他依赖...</span> <span class="hljs-keyword">import</span> createStore <span class="hljs-keyword">from</span> <span class="hljs-string">'./vuex'</span>; <span class="hljs-keyword">import</span> createRouter <span class="hljs-keyword">from</span> <span class="hljs-string">'./router'</span>; <span class="hljs-keyword">const</span> createApp = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> { <span class="hljs-keyword">const</span> store = createStore(); <span class="hljs-keyword">const</span> router = createRouter(); sync(store, router); <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Vue({ store, router, <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-params">h</span> =&gt;</span> h(<span class="hljs-comment">/* ... */</span>) }); <span class="hljs-keyword">return</span> {app, router, store}; }; <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> createApp;</code></pre> <p>虽然,我至今还没有遇到过实例冲突的问题,不过我还是觉得文档说的很有道理,可能会发生这样的情况。多个实例会克服冲突的问题,但它同时也增加服务器的负担。</p> <p>这样处理之后,就可能将之前提到的 <code>runInNewContext</code> 配置设为 <code>false</code>,默认为 <code>true</code> 会为每个 bundle 创建新的上下文。</p> <p><a></a></p> <h2 id="articleHeader3">Webpack build plugin</h2> <p>升级的最大变化在于对 webpack 提供更强大的支持,在 <code>vue-server-renderer</code> 包中新增了两个 webpack plugin: <code>server-plugin</code> 和 <code>client-plugin</code>,分别用于服务器端和客户端。</p> <h3 id="articleHeader4">server-plugin</h3> <p><code>server-plugin</code> 会默认创建一个名为 <code>vue-ssr-server-bundle.json</code> 的文件作为 <code>createBundleRenderer</code> 的第一个参数。</p> <p>这里 webpack 的 <code>output.filename</code> 设置还是要定义的,不然打包的时候会报错。</p> <p>上面这点上一个版本就能做到,使用 <code>server-plugin</code> 的好处是在于,它提供了服务端的 <code>source-map</code>功能,这可是开发利器。另一大好处是,支持 <code>hot-reload</code>,不过我之前使用的是 webpack-middleware 就已经支持该特性了。</p> <p>熟悉 webpack 的都知道,webpack-middleware 是将文件放在内存里的,而这里的 <code>createBundleRenderer</code> 用的是文件访问,所以,直接传路径是有问题的。不过,它也支持传一个对象,所以记得每次服务端代码更新之后要重新创建 renderer,还有读文件之后要将 string 转换为 object 传给 <code>createBundleRenderer</code>。</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="// 省略... const updateRenderer = () => {<br /> try {<br /> const options = {<br /> clientManifest: JSON.parse(expressDevMiddleware.fileSystem.readFileSync(clientManifestFilePath, 'utf-8'))<br /> };<br /> createRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')), options);<br /> } catch(e) {<br /> createRenderer(JSON.parse(mfs.readFileSync(serverBundleFilePath, 'utf-8')));<br /> }<br /> console.log('Renderer is updated.');<br /> };</p> <p>// watch and update server renderer<br /> const serverCompiler = webpack(serverConfig);<br /> serverCompiler.outputFileSystem = mfs;<br /> serverCompiler.watch({}, (err, stats) => {<br /> if (err) throw err;<br /> stats = stats.toJson();<br /> stats.errors.forEach(err => console.error(err));<br /> stats.warnings.forEach(err => console.warn(err));<br /> updateRenderer();<br /> });" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="JavaScript"><span class="hljs-comment">// 省略...</span> <span class="hljs-keyword">const</span> updateRenderer = <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> options = { <span class="hljs-attr">clientManifest</span>: <span class="hljs-built_in">JSON</span>.parse(expressDevMiddleware.fileSystem.readFileSync(clientManifestFilePath, <span class="hljs-string">'utf-8'</span>)) }; createRenderer(<span class="hljs-built_in">JSON</span>.parse(mfs.readFileSync(serverBundleFilePath, <span class="hljs-string">'utf-8'</span>)), options); } <span class="hljs-keyword">catch</span>(e) { createRenderer(<span class="hljs-built_in">JSON</span>.parse(mfs.readFileSync(serverBundleFilePath, <span class="hljs-string">'utf-8'</span>))); } <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Renderer is updated.'</span>); }; <span class="hljs-comment">// watch and update server renderer</span> <span class="hljs-keyword">const</span> serverCompiler = webpack(serverConfig); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) =&gt; { <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err; stats = stats.toJson(); stats.errors.forEach(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> <span class="hljs-built_in">console</span>.error(err)); stats.warnings.forEach(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> <span class="hljs-built_in">console</span>.warn(err)); updateRenderer(); });</code></pre> <h3 id="articleHeader5">client-plugin</h3> <p>如果你使用过升级之前 vue ssr 的功能,那你肯定会对一系列有关 html 的操作有映象,比如替换 html,插入 state 等。现在,有了 <code>client-plugin</code> 它就能代替原有的 <code>html-webpack-plugin</code> 来生成 html,并把之前那些繁杂的事都替你处理了。</p> <p>上面这些对已经实现 ssr 的你可能不是很有吸引力,不过,下面这点可能会让你感兴趣。这个插件还自带为你的 ccs 或 js 添加 <code>preload</code> 和 <code>prefetch</code> 功能,它可以加快你网站的加载速度,如果你还不清楚 <code>prefetch</code> 和 <code>preload</code> 是什么的话,可以先读一下<a href="https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf" rel="nofollow noreferrer" target="_blank">这篇文章</a>。</p> <div class="google-auto-placed ap_container" style="text-align: center; width: 100%; height: auto; clear: none;"><ins data-ad-format="auto" class="adsbygoogle adsbygoogle-noablate" data-ad-client="ca-pub-6330872677300335" data-adsbygoogle-status="done" style="display: block; margin: auto; background-color: transparent;"><ins id="aswift_5_expand" style="display: inline-table; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 697px; background-color: transparent;"><ins id="aswift_5_anchor" style="display: block; border: none; height: 0px; margin: 0px; padding: 0px; position: relative; visibility: visible; width: 697px; background-color: transparent; overflow: hidden; opacity: 0;"><iframe width="697" height="175" frameborder="0" marginwidth="0" marginheight="0" vspace="0" hspace="0" allowtransparency="true" scrolling="no" allowfullscreen="true" onload="var i=this.id,s=window.google_iframe_oncopy,H=s&amp;&amp;s.handlers,h=H&amp;&amp;H[i],w=this.contentWindow,d;try{d=w.document}catch(e){}if(h&amp;&amp;d&amp;&amp;(!d.body||!d.body.firstChild)){if(h.call){setTimeout(h,0)}else if(h.match){try{h=s.upd(h,i)}catch(e){}w.location.replace(h)}}" id="aswift_5" name="aswift_5" style="left:0;position:absolute;top:0;border:0px;width:697px;height:175px;"></iframe></ins></ins></ins></div> <p>如果你使用的是 webpack-server,那么,你按文档上的例子来应该没什么问题。但如果你和我一样使用的是 webpack-middleware,那么,这里还是有些别扭的,需要和之前一样每次 plugin 生成后去重新构建 renderer。</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="// 省略... clientCompiler.plugin('done', updateRenderer);" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="JavaScript"><span class="hljs-comment">// 省略...</span> clientCompiler.plugin(<span class="hljs-string">'done'</span>, updateRenderer);</code></pre> <p>同 <code>server-plugin</code> 一样文件读出来的是 string,你要将它转换为对象。其他基本的配置按文档上的来就行,遇到问题的可以参考下我的<a href="https://github.com/DiscipleD/blog" rel="nofollow noreferrer" target="_blank">代码</a>。</p> <p>吹了这么多,不足之处还是得指出来,<code>client-plugin</code> 还不能像 <code>html-webpack-plugin</code> 监听 html 文件,每次修改 html 都得手动重启服务有点麻烦,可以优化一波...</p> <p>升级所要注意的就差不多就这些了。还有一点,之前 vue 推荐使用 <code>renderToStream</code> 来返回页面,如果组件生命周期中有请求的话,使用 stream 可能导致组件还未构建完成就已经发送。所以,更新之后 vue 推荐使用 <code>renderToString</code>。</p> <p><a></a></p> <h2 id="articleHeader6">结尾</h2> <p>vue 的确是非常紧跟潮流,就像这次加入的 <code>preload</code> 和 <code>prefetch</code> 功能,但因开发团队人员太少(相对于 react 和 angular),导致版本并不是很稳定。</p> <p>如果,你问我 vue 好不好?我会说,好。 <br />如果,你问我要不要学 vue?我会说,学。 <br />如果,你问我 vue 能不能上生产?我的建议是,不如咋们半年后再谈...</p> <p></code></p>

本文固定链接: http://www.js-code.com/vue-js/vue-js_27585.html