最近看了下 xx联盟 后,也想在自己的网站中接入广告,但是我的网站是用 vue 开发的单页面应用,对 SEO
不太友好,自然,接入广告,也会有所损失。而且,对于一个博客系统
来说,SEO 也是一项不可忽视的指标,因此,我把网站用 nuxt
重写了一边,改造成 ssr
(服务端渲染)形式。
github
:https://github.com/love-peach/nuxt-ts-blog
初始化项目
通过 Nuxt.js 官方提供的脚手架工具 create-nuxt-app
,初始化项目
运行
npx create-nuxt-app <项目名>
// 或
yarn create nuxt-app <项目名>
yarn create
参见 yarn create
yarn create nuxt-app <项目名> // 等价于下面
yarn global add create-nuxt-app
create-nuxt-app <项目名>
项目配置
目录结构
.
├── .editorconfig
├── .env
├── .eslintrc.js
├── .git/
├── .gitignore
├── .nuxt/ // Nuxt自动生成,临时的用于编辑的文件
├── .prettierrc
├── README.md
├── assets/ // 用于组织未编译的静态资源如 LESS、SASS 或 JavaScript
├── components/ // 用于组织应用的 Vue.js 组件。不会像页面组件那样有 asyncData 方法的特性
├── jsconfig.json
├── layouts/ // 用于组织应用的布局组件
├── middleware/ // 目录用于存放应用的中间件
├── nuxt.config.js // 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。
├── package.json
├── pages/ // 用于组织应用的路由及视图,并自动生成对应的路由配置
├── plugins/ // 用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
├── server/
├── static/ // 用于存放应用的静态文件,不会被构建编译处理,会映射至应用的根路径 /
├── store/ // 用于组织应用的 Vuex 状态树 文件
├── stylelint.config.js
├── tsconfig.json
└── yarn.lock
运行项目
yarn run dev
在浏览器中,打开 http://localhost:3000
搭建前端页面
项目启动后,我们就可以开发前端页面了。
添加页面
在 nuxt
中,大部分的路由都可以通过 pages/
目录自动生成,例如:
pages/
└── article/
├── index.vue
├── _category/
│ └── index.vue
└── detail/
└── _articleId.vue
将生成如下路由:
router: {
routes: [
{
name: 'article',
path: '/article',
component: 'pages/article/index.vue'
},
{
name: "article-category"
path: "/article/:category",
component: 'pages/article/_category/index.vue',
},
{
name: "article-detail-articleId"
path: "/article/detail/:articleId",
component: 'pages/article/detail/_articleId.vue'
}
]
}
布局layout
因为我的博客有这几个部分,article
、resource
、moive
、ebook
、admin
、user
,会有几种布局,
在 layout/
目录下,添加相应的布局文件。草图如下:
从草图中,可以看到 default
和 user
,又具有相同的结构,header
,footer
,content
, 因此,将其提取成组件 AppLayout
,下面是 default
和 user
布局的内容:
注意 layout/
目录下的文件,name
值不能重复,不然会报错。
使用布局
如果,没有向下面这样注明 layout
字段,将使用 default
布局。
export default Vue.extend({
layout: 'admin',
});
组件,页面组件
公用的组件,我们知道是放在 conponents/
目录下,但是页面中的组件放哪呢?放在 pages 目录下的对应文件夹中肯定是不行的,因为,它会将组件当初页面,会生成路由。
我建议是也放在 componets/
目录下,然后通过目录来区分不同的组件,如下
components/
├── base 基本组件
├── framework 布局相关组件
└── page/ 各个页面下的组件
├── admin
├── ebook
├── home
├── movie
└── user
css js img 放哪?
一般放在 assets/
目录下。
如果,不需要 Webpack
做构建编译处理,应该放在 static/
下,会映射至应用的根路径 /
下。
全局样式
首先,全局样式的文件,可以放在 assets/
目录下,然后在 nuxt.config.js
中引入
module.exports = {
css: ['@/assets/css/grid.less', '@/assets/css/reset.less'],
}
可以将,各个独立的样式,通过一个文件引入,然后,再配置下:
// assets/css/index.less
@import './variables.less';
@import "./clearfix.less";
@import './reset.less';
@import './animate.less';
@import "./grid.less";
@import './common.less';
module.exports = {
css: ['@/assets/css/index.less'],
}
less 全局变量
如果你用 less
进行样式的预处理,可能会用到变量,又不想每个页面中引入 变量文件,怎么做呢?
首先定义好变量文件 variables.less
:
@colorPrimary: #2d8cf0;
@colorPrimaryLight: lighten(@colorPrimary, 10%);
@colorPrimaryDark: darken(@colorPrimary, 10%);
@colorPrimaryFade: fade(@colorPrimary, 10%);
然后在 nuxt.config.js
中如下配置:
module.exports = {
build: {
// extend(config, ctx) {},
loaders: {
less: {
lessOptions: {
modifyVars: getLessVariables(resolve('assets/css/variables.less')),
javascriptEnabled: true,
},
},
},
},
}
getLessVariables
函数实现如下:
const path = require('path');
const fs = require('fs');
function resolve(dir) {
return path.join(__dirname, dir);
}
function getLessVariables(file) {
const themeContent = fs.readFileSync(file, 'utf-8');
const variables = {};
themeContent.split('\n').forEach(function(item) {
if (item.includes('//') || item.includes('/*')) {
return;
}
const _pair = item.split(':');
if (_pair.length < 2) return;
const key = _pair[0].replace('\r', '').replace('@', '');
if (!key) return;
const value = _pair[1]
.replace(';', '')
.replace('\r', '')
.replace(/^\s+|\s+$/g, '');
variables[key] = value;
});
return variables;
}
全局过滤器
在 plugins/
目录下,新建 filters.js
:
import Vue from 'vue';
import dayjs from 'dayjs';
export function dateFormatFilter(date, fmt) {
if (!date) {
return '-';
} else {
return dayjs(date).format(fmt);
}
}
const filters = {
dateFormatFilter,
};
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key]);
});
export default filters;
然后,在 nuxt.config.js
中配置,
module.exports = {
plugins: ['~/plugins/filters.js'],
}
自定义指令
跟定义全局过滤器一样,需要在 plugins
中操作。
在 plugins/directive/focus
目录下,添加 focus.js
import Vue from 'vue';
const focus = Vue.directive('focus', {
inserted(el) {
el.focus();
},
});
export default focus;
然后,在 nuxt.config.js
中配置,
module.exports = {
plugins: [
'~/plugins/filters.js',
{ src: '~/plugins/directive/focus/index.js', ssr: false },
],
}
head 动态设置页面标题
我的博客中的 豆瓣电影
和 ebook
栏目,用到了很多外部的图片,而他们做了 防止盗链
处理,不做配置的情况下,会出现 403
的情况,图片不允许访问。想到的办法是 添加 meta
标签,设置 referrer
。
之前单页面的时候,我是配置在,public/index.html
中,这样每个页面都有这个 标签了,我不想这样。
Nuxt.js
使用 vue-meta 来更新应用的 头部标签(Head) 和 html 属性。
我们可以通过在页面中配置这个属性,来修改页面的 title
,或者动态添加一些 meta
标签,在 head
中,可通过 this
关键字来获取组件的数据。
head() {
return {
title: `${this.blogResult.title} 详情页`,
meta: [{ hid: 'ebook-home referrer', name: 'referrer', content: 'never' }],
};
},
也可以,通过 nuxt.config.js
全局配置 head
信息,所以在配置 meta 标签的时候,最好提供一个 hid
唯一编号。
asyncData
因为采用的是 ssr
,需要在浏览器请求页面的时候,就将页面中的数据渲染好,返回一个完整的 html
内容。
asyncData
方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用asyncData
方法来获取数据并返回给当前组件。
export default {
data () {
return { blogList: [] };
},
async asyncData({ app }: ctxProps) {
const params = { page: 1, limit: 10 };
const res = await app.$myApi.blogs.index(params);
return {
blogList: res.result.list,
pageTotal: res.result.pages,
itemTotal: res.result.total,
};
},
}
fetch
有些时候,我们只是想在页面进来的时候,拉取最新数据而已,不是修改组件中的值,那么,这个时候,可以使用 fetch
方法。
如果页面组件设置了 fetch 方法,它会在组件每次加载前被调用(在服务端或切换至目标路由之前)。
fetch 是在组件初始化之前被调用。
与 asyncData
方法类似,不同的是它不会设置组件的数据。
async fetch({ store }: ctxProps) {
await store.dispatch('common/requestCategoryList');
},
解耦 api
建议接口采用 RESTFUL 风格,这样,一个接口基本就是这样
import request from '@/utils/request';
export default {
GetCategory: (params, options) => request.get('/categories', params, options),
PostCategory: (params, options) => request.post('/categories', params, options),
PutCategory: (params, options) => request.put(`/categories/${params.categoryId}`, params, options),
DeleteCategory: (params, options) => request.delete(`/categories/${params.categoryId}`, params, options),
}
但是这样,每次都需要映入 请求方法
,例如 axios,封装过的 request。
可以参考这篇文章,可以让你的 api 优雅的解耦:组织并解耦你在 NuxtJs 中调用的 api
proxy 代理
请求接口,难免会用到代理,在 nuxt
中配置起来也很方便。
首先,安装依赖(好像不用安装,如果不行,你在安装)
yarn add @nuxtjs/proxy
然后,在 nuxt.config.js
中添加 modules
,并配置,
module.exports = {
modules: ['@nuxtjs/proxy'],
proxy: {
'/api/': {
target: process.env.NODE_ENV === 'production' ? 'http://localhost:3000/' : 'http://localhost:3000/',
// target: process.env.NODE_ENV === 'production' ? 'http://zhangjinpei.cn' : 'http://localhost:3000/',
changeOrigin: true,
},
'/douban/': {
target: 'http://api.douban.com/v2',
changeOrigin: true,
pathRewrite: {
'^/douban': '',
},
},
'/ebookSearch/': {
target: 'http://www.shuquge.com/search.php',
changeOrigin: true,
pathRewrite: {
'^/ebookSearch/': '',
},
},
},
};
之前,在 单页面 的模式下,本地开发,配上类似上面的代理,上线后还需要配置,nginx 代理。但是在 nuxt
中,不需要再做接口相关的代理。
我线上的 ng 代理如下:
upstream server_host {
server localhost:3000;
}
server {
listen 80;
server_name zhangjinpei.cn;
rewrite ^(.*)$ https://$host$1 permanent;
gzip on;
gzip_http_version 1.0;
gzip_proxied any;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/xwoff image/svg+xml;
}
#server {
# listen 80;
# server_name ssr.zhangjinpei.cn;
# location / {
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header Host $http_host;
# proxy_pass http://127.0.0.1:8000;
# }
#}
server {
listen 443 ssl;
server_name zhangjinpei.cn;
root /root/zhangjinpei/nuxt-ts-blog/;
index index.html index.htm;
ssl_certificate /xxx/xx;
ssl_certificate_key /xxx/xx;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
#location / {
# root /root/zhangjinpei/blog-front/dist;
# try_files $uri /index.html;
#}
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Nginx-Proxy true;
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:8000;
}
#location /api/ {
# proxy_pass http://server_host/api/;
# poxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $remote_addr;
# proxy_set_header X-Forwarded-Host $host;
# proxy_set_header X-Forwarded-Server $host;
#}
#location /douban/ {
# proxy_pass http://api.douban.com/v2/;
#}
#location /douban/movie/ {
# proxy_pass http://api.douban.com/v2/movie/;
#}
#location /doubanOld/ {
# proxy_pass https://movie.douban.com/;
#}
}
store
在 nuxt
中使用 vuex
来管理状态,十分方便,只需要在 store/
目录下创建文件即可。
store
目录下的每个 .js
文件会被转换成为状态树指定命名的子模块
(当然,index 是根模块)。
在 nuxt
中,有两种方式使用 store。
一种是,模块模式,直接将 state
、getters
、mutations
、actions
导出,如下:
const state = () => ({
userInfo: null,
});
const getters = {
getUserInfo: state => state.userInfo,
}
const mutations = {
setUserInfo(state, data) {
state.userInfo = data;
},
}
const actions = {
async requestUserInfo({ commit }) {
const res = await axios.get('xx');
commit('setUserInfo', res.data);
},
}
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};
一种是 store/index.js
返回创建Vuex.Store实例的方法(不推荐,官网更推荐前一种)。例如:
export default new Vuex.Store({
state: () => ({
counter: 0
}),,
mutations: {
...mutations,
},
actions: {
...actions,
},
modules: {
common,
},
});
注意
无论使用那种模式,您的state
的值应该始终是function
,为了避免返回引用类型,会导致多个实例相互影响。
我们甚至可以,将模块文件分解成单独的 js 文件,如:state.js
, actions.js
, mutations.js
和 getters.js
注意
在使用拆分文件模块时,必须记住使用箭头函数功能,this
在词法上可用。词法范围this
意味着它总是指向引用箭头函数的所有者。如果未包含箭头函数,那么this
将是未定义的(undefined)
。解决方案是使用"normal"
功能,该功能会将this
指向自己的作用域,因此可以使用。
构建 部署
执行构建命令,然后启动
yarn run build
yarn run start
但是这样启动,就一直得开着后台,我用的 pm2
启动
pm2 启动
在 package.json
中 scripts
配置命令:
{
"scripts": {
"pm2start": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json",
}
}
然后,在 server/
目录下,新建 pm2.config.json
文件
{
"apps": [
{
"name": "blog-front-ssr",
"script": "./server/index.js",
"instances": 0,
"watch": false,
"exec_mode": "cluster_mode"
}
]
}
./server/index.js
这个文件,在创建项目是就有了。
最后,启动:
npm run pm2start
总结
OK,到目前为止,我把自己从完全没用过 nuxt ,到网站正式上线所遇到的问题都记录在这了,希望对对你也有帮助。最后,这是我用 nuxt 做的个人博客 zhangjinpei.cn
参考链接
Vue SSR 指南、
Nuxt 官网、
TypeScript、
Nuxt Typescript、
restfule api
阿里云图片处理