Vue 服务端渲染(SSR)、Nuxt.js - 从入门到实践
栏目: JavaScript · 发布时间: 6年前
内容简介:10月初有幸接到公司官网改版需求,要求采用服务端渲染模式对原网站进行seo优化。由于团队一直使用的vue技术栈,所以我第一时间想到的就是采用vue 服务端渲染(SSR)来实现该需求,即能减少团队其他成员后期维护的成本,又能把现有其他项目封装好的内容稍微改改就能直接copy过来使用,大大节省了开发时间(除去ui中途调整,整体改造时间之花了2-3天)。1、原公司网站(改版前),采用
10月初有幸接到公司官网改版需求,要求采用服务端渲染模式对原网站进行seo优化。
由于团队一直使用的vue技术栈,所以我第一时间想到的就是采用vue 服务端渲染(SSR)来实现该需求,即能减少团队其他成员后期维护的成本,又能把现有其他项目封装好的内容稍微改改就能直接copy过来使用,大大节省了开发时间(除去ui中途调整,整体改造时间之花了2-3天)。
改造前后对比
1、原公司网站(改版前),采用 vue(SPA) 模式开发
网址:www2.nicomama.com/
2、新公司网站(改版后),采用 Vue 服务端渲染(SSR) 模式开发
网址:www.nicomama.com/
当然直接从浏览器打开只能看到两个网站只是在风格上和界面上做了升级。
接下来让我们看看两个网站区别在哪里,使用chrome浏览器分别打开两个网站,右击查看源码。
截图如下:
1、改版前
2、改版后
可以明显看出改版后网站源码增加了不止几倍之多,简而言之服务端渲染的模式就是:在请求一个网址的时候,服务端收到请求之后把html的内容先生成好然后再返回给浏览器。这样子搜索引擎就可以通过你返回的a标签抓取到网站的其他页面了,依此类推搜索引擎就可以收录网站的所有(暴露出来的)路径了,后面还会给大家看一下网站改版后的一些搜录数据变化。
在看下面内容之前建议大家先去看下《Vue SSR指南》,这是文档地址 ssr.vuejs.org/zh/#%E4%BB%…
正文
接下来这一块Vue SSR的概念介绍和好处坏处对比的内容是对文档提及的概念摘要,看过文档的可以直接忽略~
Vue SSR
简而言之就是将本来要放在浏览器执行创建的组件,放到服务端先创建好,然后生成对应的html将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
Vue SSR 相比 SPA(单页应用)好处及坏处
1、好处
-
更好的 SEO,由于搜索引擎爬虫抓取 工具 可以直接查看完全渲染的页面。
-
更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
2、坏处
1)开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数(lifecycle hook)中使用;一些外部扩展库(external library)可能需要特殊处理,才能在服务器渲染应用程序中运行。
2)涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序(SPA)不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
3)更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源(CPU-intensive - CPU 密集),因此如果你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。
了解完概念之后,让我们动手实现第一个Vue SSR实例把~
注意:一下内容需要有一定vue基础
相比vue SPA(单页应用),Vue增加了一些扩展工具,首先我们来看一下比较重要的一个工具vue-server-renderer,从名字可以看出它是在服务端渲染的时候用的。
让我们来看一下它的功能和用法
1、创建一个空项目 mkdir vuessr && cd vuessr
2、运行 npm init
进行初始化
3、安装我们需要的依赖 cnpm install vue vue-server-renderer --save
4、创建 index.js
代码如下:
// 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const app = new Vue({ template: `<div>Hello World</div>` }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> }) 复制代码
5、运行 node index.js
可以看到在控制台输出了
<div data-server-rendered="true">Hello World</div> 复制代码
我们再将生成好的html放到指定的html模版里面再返回到浏览器不就实现服务端渲染功能了?
6、安装依赖 cnpm install express --save
7、创建 app.js
代码如下:
const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer() server.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `<div>访问的 URL 是: {{ url }}</div>` }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.end(` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) }) server.listen(8080) 复制代码
8、运行 node app.js
9、打开浏览器输入 http://localhost:8080/
发现我们的内容已经显示出来了,如果有同学发现有中文乱码的问题,可以设置一下编码:
renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } //设置编码 res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(` <!DOCTYPE html> <html lang="zh"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) 复制代码
然后重新运行一下试试。
10、当然直接采用字符串来拼接html内容是非常不优雅的,而且容易出错,我们可以改写成模版形式,创建文件 index.template.html
代码如下:
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html> 复制代码
注意: <!--vue-ssr-outlet-->
注释--这里将是应用程序 HTML 标记注入的地方。
11、修改 app.js
const renderer = require('vue-server-renderer').createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) //*** renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(html) }) 复制代码
12、重新运行一下试试,看看是否能正常运行。
13、它还支持模板插值操作,修改文件 index.template.html
,代码如下:
<html> <head> <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) --> <title>{{ title }}</title> <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) --> {{{ meta }}} </head> <body> <!--vue-ssr-outlet--> </body> </html> 复制代码
修改文件 index.js
,需要调整的代码如下:
const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) const context = { title: 'hello vuessr', meta: ` <meta charset="utf-8"> ` } server.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `<div>访问的 URL 是: {{ url }}</div>` }) renderer.renderToString(app, context, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'}); res.end(html) }) }) server.listen(8080) 复制代码
14、再次重新运行一下试试,看看是否能正常运行。以下是查看浏览器源码的截图,可以看到模板定义的内容已经替换成对应的数据了。
代码地址:
具体详细的api介绍大家可以去看文档,以上只是简单的使用。
到此为止我们已经实现一个最基础的vue 服务端渲染的工程了。
是不是很简单?不过要从头搭建整套Vue SSR还是一个非常繁琐的过程。
后续我也会给大家介绍VueSSR开箱即用的框架Nuxt.js,其实它就是对Vue SSR的一个封装,概念还是一样的。当然再没了解Vue SSR的基本实现过程,直接去使用Nuxt.js还是会一头雾水(大神忽略~)。
在搭建项目之前我们先来看看指南中提供的这张运行原理图吧:
从图中我们可以大致看出vue ssr的运作过程:我们首先通过 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。有了理论基础之后让我们一起实践一下吧。
使用Vue-cli为基础搭建VueSSR
选择vue-cli的webpack模版生成的代码基本上可以复用到VueSSR可以省去繁琐的webpack配置的过程。
1、安装vue-cli,参考文档 cli.vuejs.org/zh/guide/cl… ,这里就不做过多介绍,我采用的版本是2.9.6。
2、运行 vue init webpack vuessr-vuecli
选择配置如下
? Project name vuessr-vuecli ? Project description A Vue.js project ? Author taoxinhua <taoxhsmile@163.com> ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? No ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) npm 复制代码
3、进入文件夹,运行 cnpm run dev
;看下项目是否能正常运行
4、接下来让我们一起对代码进行改造吧~
新增 src/components --about.vue --home.vue 复制代码
about.vue
<template> <div class="hello"> <h1>这是关于我页面</h1> </div> </template> 复制代码
home.vue
<template> <div class="hello"> <h1>这是首页</h1> </div> </template> 复制代码
修改 src/router --index.js 复制代码
index.js
import Vue from 'vue' import Router from 'vue-router' import home from '@/components/home' import about from '@/components/about' Vue.use(Router) export default () => { return new Router({ mode:'history', routes: [ { path: '/', name: 'home', component: home }, { path: '/about', name: 'about', component: about } ] }) } 复制代码
新增 src --app.js --App.vue --entry-client.js 客户端打包入口文件 --entry-server.js 服务端打包入口文件 复制代码
app.js
import Vue from 'vue' import createRouter from './router' import App from './App.vue' // 实例 每次请求都会创建新的实例 export default (context) => { const router = createRouter() const app = new Vue({ router, components: { App }, template: '<App/>' }) return { router, app } } 复制代码
App.vue
<template> <div id="app"> <router-link to="/">首页</router-link> <router-link to="/about">关于我</router-link> <router-view/> </div> </template> <script> export default { name: 'App' } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> 复制代码
entry-client.js
import createApp from './app' let { app, router } = createApp() router.onReady(() => { app.$mount('#app') }) 复制代码
entry-server.js
// 服务端这边,需要把访问的路径给到vue-router import createApp from './app' // 外面的express服务使用 {url: / /about} export default (context) => { return new Promise((resolve, reject) => { let { app, router } = createApp(context); router.push(context.url); router.onReady(() => { // 访问路径,可定匹配到组件 let matchedCompoents = router.getMatchedComponents(); if (!matchedCompoents.length) { return reject({ code: 404 }) } resolve(app) }, reject) }) } 复制代码
新增 build/dev-server.js server.js 复制代码
dev-server.js
const serverConf = require('./webpack.server.conf'); const webpack = require('webpack') const fs =require('fs') const path = require('path'); const Mfs = require('memory-fs') const axios = require('axios') module.exports = (cb) => { const webpackComplier = webpack(serverConf); var mfs = new Mfs(); webpackComplier.outputFileSystem = mfs; webpackComplier.watch({}, async (error, stats) => { if (error) return console.log(error); stats = stats.toJson(); stats.errors.forEach(err => console.log(err)) stats.warnings.forEach(err => console.log(err)) // server Bundle json文件 let serverBundlePath = path.join( serverConf.output.path, 'vue-ssr-server-bundle.json' ) let serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath, "utf-8")) //console.log(serverBundle) // client Bundle json文件 let clientBundle = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json') // 模板 let template = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8'); cb(serverBundle, clientBundle, template) }) } 复制代码
server.js
const devServer = require('./build/dev-server'); const express = require('express'); const app = express(); const vueRenderer = require('vue-server-renderer') const path = require('path'); app.get('*', async (req, res) => { res.status(200); res.setHeader('Content-Type', 'text/html;charset=utf-8;') devServer(function(serverBundle,clientBundle,template){ let renderer = vueRenderer.createBundleRenderer(serverBundle,{ template, clientManifest: clientBundle.data, runInNewContext: false }) renderer.renderToString({ url: req.url }).then((html) => { res.end(html) }).catch(err => console.log(err)) }) }) app.listen(5000, () => { console.log('启动成功') }) 复制代码
修改 build/webpack.dev.conf.js 复制代码
webpack.dev.conf.js
//...忽略 const portfinder = require('portfinder') //新增内容-start const vueSSRClientPlugin = require('vue-server-renderer/client-plugin') //新增内容-end const HOST = process.env.HOST //...忽略 new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]), //新增内容-start new vueSSRClientPlugin() //新增内容-end //...忽略 复制代码
新增文件
build/webpack.server.conf.js 复制代码
webpack.server.conf.js
const webpack = require('webpack'); const merge = require('webpack-merge') const base = require('./webpack.base.conf'); const vueSSRServerPlugin = require('vue-server-renderer/server-plugin') const webpackNodeExternals = require('webpack-node-externals') module.exports = merge(base,{ target: 'node', devtool: 'source-map', entry: './src/entry-server.js', output: { filename: 'server-bundle.js', libraryTarget: 'commonjs2' }, externals: [webpackNodeExternals({ whitelist: /\.css$/ })], plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"devlopment"', 'process.env.VUE_ENV': '"server"' }), new vueSSRServerPlugin() ] }) 复制代码
修改 index.html 复制代码
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>vuessr-vuecli</title> </head> <body> <div id="app"> <!--vue-ssr-outlet--> </div> <!-- built files will be auto injected --> </body> </html> 复制代码
修改 package.json 增加server脚本 复制代码
package.json
"scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "build": "node build/build.js", "server": "node server.js" }, 复制代码
到此为止我们的文件都已经调整完毕了,让我们先来运行一下看看吧。
cnpm run dev cnpm run server 复制代码
可以看到我们的程序已经正常运行了,再来看看网页源代码是否是通过服务端渲染完毕之后再返回的。
没错到这一步我们算是大功高成了,接下来我们回过头来看看整个项目的运行原理吧~ 毕竟这才是重点。
首先回想一下前面的运行原理图,第一步是不是先通过webpack分别打包出给服务端用的bundle和客户端用的bundle。
我们先来找一下客户端用的bundle我们生成在哪里吧
我们运行 cnpm run dev
实际上就是用来生成客户端用的bundle。这一步比较简单,回想一下我们是不是调整了 webpack.dev.conf.js
增加了一个插件 vue-server-renderer/client-plugin
就是用来生成客户端用的bundle, 只不过我们并没有直接把这个bundle生成具体的文件,而是放在了缓存中,我们可以直接通过浏览器访问 http://localhost:8080/vue-ssr-client-manifest.json
查看到这份json文件。
服务端用的bundle
其实它也生成在缓存中, build/dev-server.js
首先我们通过 webpackComplier.outputFileSystem = mfs;
修改了webpack的输出形式(改成输出到缓存中),然后在从缓存中拿到该文件
let serverBundlePath = path.join( serverConf.output.path, 'vue-ssr-server-bundle.json' ) let serverBundle = JSON.parse(mfs.readFileSync(serverBundlePath, "utf-8")) 复制代码
我们再来看一下 server.js
devServer(function(serverBundle,clientBundle,template){ let renderer = vueRenderer.createBundleRenderer(serverBundle,{ template, clientManifest: clientBundle.data, runInNewContext: false }) renderer.renderToString({ url: req.url }).then((html) => { res.end(html) }).catch(err => console.log(err)) }) 复制代码
可以看到我们将拿到的serverBundle(服务端Bundle),clientBundle(客户端Bundle),template(index.html模版) 最终交由 vue-server-renderer
来进行最终处理。
代码地址
大功高成~ 原理大概就是这样,代码的细节还需要大家自行去查看和消化
讲完原理之后,让我们来看看Nuxt.js又是咋回事吧
在进入下一个环节之前,还是建议大家先去看官方文档zh.nuxtjs.org/guide/
Nuxt.js是什么?
简而言之Nuxt.js就是Vue SSR的一个开箱即用的框架。安装好就可以直接写业务代码,而不需要做过多的配置。
既然是一个框架,那我们的就要按照它的规则来配置和写代码,前面提供的官方文档提供了非常详细的介绍。这里就不做过多介绍,我们直接进行实战吧。 首先我们先创建一个新项目。
在改造官网期间刚好Nuxt.js 2.0发布了,看了一下改动还是比较多了,为了确保项目正常上线,决定还是先采用稳定的1.0+版本进行编码(毕竟新版本方发布多少会有一点问题)。以下也是针对1.0+版本开发过程中遇到的一些问题,大家可以借鉴以下。
以下内容主要针对实战过程中遇到的一些问题来进行分享:
第一步让我们先创建一个新项目吧,安装文档地址: zh.nuxtjs.org/guide/insta… 我们采用 create-nuxt-app
命令来安装,运行
npx create-nuxt-app vuessr-nuxt 复制代码
或者
cnpm install -g create-nuxt-app create-nuxt-app vuessr-nuxt 复制代码
选择配置如下
? Project name vuessr-nuxt ? Project description My unreal Nuxt.js project ? Use a custom server framework express ? Use a custom UI framework none ? Choose rendering mode Universal ? Use axios module yes ? Use eslint no ? Use prettier no ? Author name taoxinhua ? Choose a package manager npm 复制代码
然后进入进入项目运行, cnpm run dev
看看项目是否能正常运行,如果不能运行,第一步先检查以下node版本,我的node版本是v8.12.0。第二步如果node升级之后还是不行,运行 cnpm install
看下是否有依赖包少安装了。如果这两步还不能解决问题,大家可以在评论区提问。
打开浏览器进入 http://127.0.0.1:3000
,可以看到我们的项目已经可以运行了。没错就是这么简单
1、使用axios遇到的坑。
注意
在使用1.0+版本开发过程中发现每次修改文件,服务端代码都会重新加载并执行一遍,如果直接把axios的钩子函数放到plugin中去执行,会发现每次修改完毕之后钩子函数都会重复添加一次,导致参数重复被处理,比如我发送请求之前要把传递的data转成字符串的形式。会发现下面代码的config.data会出现重复累加的情况。
axios.interceptors.request.use(function (config) { let data = config.data || {}; let auth = buildHttpHeaders(); config.url = getApiUrl(config.url); config.data = qs.stringify({ data: JSON.stringify(data), auth: JSON.stringify(auth) }) //在请求发出之前进行一些操作 return config; }, function (err) { //Do something with request error return Promise.reject(err); }); 复制代码
解决方法
创建文件
assets/js/config/config-axios.js
import Axios from 'axios' import qs from 'qs' import { getUUID } from '~/assets/js/tools/index' import { getApiUrl } from '~/assets/js/config/config-urls.js' function buildHttpHeaders() { return { "x-user-id": '', "x-access-token": '', "x-platform": 'pc', "x-client-token": getUUID(), "x-system-version": '10.1.1', "x-client-version": '2.0.1', "x-method-version": '1.0', "x-network-type": '3g', } } let axios = Axios.create(); // 添加一个请求拦截器 axios.interceptors.request.use(function (config) { let data = config.data || {}; let auth = buildHttpHeaders(); config.url = getApiUrl(config.url); config.data = qs.stringify({ data: JSON.stringify(data), auth: JSON.stringify(auth) }) //在请求发出之前进行一些操作 return config; }, function (err) { //Do something with request error return Promise.reject(err); }); //添加一个响应拦截器 axios.interceptors.response.use(function (res) { //在这里对返回的数据进行处理 return res.data; }, function (err) { //Do something with response error return Promise.reject(err); }); export default axios 复制代码
每次通过Axios.create返回一个全新的axios实例。
以上是项目里面拷贝出来的代码,无法单独运行,大家可以针对各自的需求进行相应调整。
plugins/axios.js
import Vue from 'vue'; import axios from '~/assets/js/config/config-axios' Vue.prototype.$$axios = axios; export default ({ app }, inject) => { // Set the function directly on the context.app object app.$$axios = axios; } 复制代码
这样每次代码进行热更新就不会出现上面的问题了。
这里还把axios挂到了Vue的原型下面和app对象下面,页面不需要引用axios就进行调用了。
2、asyncData
在nuxt中组件文件申明的asyncData方法会被忽略,所以所有数据的加载都要放到对应page的asyncData中。
3、如何增加额外的全局js文件
我们可以通过定制模版来处理,在根目录下创建 app.html
模版文件,nuxt默认的模版为
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head> {{ HEAD }} </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html> 复制代码
比如我们要增加搜索引擎的收录代码我们可以
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head> {{ HEAD }} <!-- 百度自动收录脚本 --> <script> (function(){ var bp = document.createElement('script'); var curProtocol = window.location.protocol.split(':')[0]; if (curProtocol === 'https') { bp.src = 'https://zz.bdstatic.com/linksubmit/push.js'; } else { bp.src = 'http://push.zhanzhang.baidu.com/push.js'; } var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(bp, s); })(); </script> <!-- 360自动搜录脚本 --> <script>(function(){ var src = (document.location.protocol == "http:") ? "http://js.passport.qihucdn.com/11.0.1.js******":"https://jspassport.ssl.qhimg.com/11.0.1.js?******"; document.write('<script src="' + src + '" id="sozz"><\/script>'); })(); </script> <!-- 百度统计 --> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?******"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html> 复制代码
4、让项目支持ip访问 在 package.json
文件中增加如下配置即可
"config": { "nuxt": { "host": "0.0.0.0", "port": "3000" } }, 复制代码
5、正式环境、测试环境区分配置
package.json
"scripts": { "dev": "cross-env API_ENV=local nuxt", "build_beta": "cross-env API_ENV=beta nuxt build", "build_pro": "cross-env API_ENV=pro nuxt build", "start": "nuxt start", "generate": "nuxt generate", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint" }, 复制代码
添加build_beta、build_pro,然后通过cross-env来设置环境
nuxt.config.js
env: { API_ENV: process.env.API_ENV }, 复制代码
这样就可以在代码中,通过 process.env.API_ENV
来获取到环境变量的值了。
6、通过pm2启动项目
pm2-config.json
{ "apps": [ { "name": "nicomama-site", "script": "npm", "args" : "start", "watch": [".nuxt"] } ] } 复制代码
7、项目部署
先运行 cnpm run build_beta
或者 cnpm run build_pro
进行打包,然后通过运行 pm2 start pm2-config.json 来运行项目
后续有空再给大家理一份项目的结构
那么就先到这里了
以上所述就是小编给大家介绍的《Vue 服务端渲染(SSR)、Nuxt.js - 从入门到实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Octane渲染入门-渲染设置图文版
- Vue 服务端渲染原理及入门
- Nuxt.js服务端渲染入门
- OpenGL ES 入门之旅(15)--分屏滤镜渲染图片
- OpenGL/OpenGL ES入门: 使用OpenGL ES 渲染图片
- OpenGL ES 入门之旅--OpenGL 下的坐标系和着色器渲染流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web容量规划的艺术
阿尔斯帕瓦 / 叶飞、罗江华 / 机械工业出版社 / 2010-1 / 29.00元
《Web容量规划的艺术》由John Allspaw(F订ickr的工程运营经理)撰写,结合了他个人在F1ickr成长过程中的许多经历和很多其他产业中同行的洞察力。在衡量增长、预测趋势、成本效益等方面,他们的经验都会给你一些可靠并有效的指导。 网站的成功是以使用和增长来衡量的,而且网站类公司的成败(生死)是依赖于他们是否有能力来衡量决定他们的基础结构,从而适应不断增长的需求。作者通过自身实践给......一起来看看 《Web容量规划的艺术》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
Markdown 在线编辑器
Markdown 在线编辑器