阅读建议:建议通过左侧导航栏进行阅读
RealWord
是一个开源学习项目,目的是帮助开发者快速学习新技能,开发者可以使用任何技术去实现它。RealWord
是一个类似一个小论坛的项目,业务流程很简单,包括的功能有用户登录/注册/退出/个人中心、文章信息发布/修改/详情查看/分页展示/作者信息查看/点赞/取消点赞/评论。
Nuxt.js
开发同构渲染应用Vue.js
实践能力SEO
优化新建项目,进入项目目录
yarn init //生成 package.json 文件
yarn add nuxt //安装 nuxt 依赖
在package.json
中添加启动脚本:
"scripts": {
"dev": "nuxt"
},
新建pages
目录,创建子目录,并将提供的对应页面模板放入相应子目录下的index.vue
中,处理完所有页面组件后,pages
目录结构如下图:
在项目根目录下,新建一个app.html
文件,并使用Nuxt.js
默认的应用模板,导入项目中需要使用的CSS
样式及字体文件。
ionicons.min.css
文件中引用了其他字体文件,因此使用jsDelivr
进行地址转换;
index.css
是项目自身的样式文件,直接下载文件,放入static/index.css即可
<html {
{
HTML_ATTRS }}>
<head {
{
HEAD_ATTRS }}>
{
{ HEAD }}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/ionicons.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/index.css">
head>
<body {
{
BODY_ATTRS }}>
{
{ APP }}
body>
html>
到此,正式开发的前期准备工作完成。
在项目根目录下新建nuxt.config.js
,通过Nuxt.js
路由的extendRoutes
选项自定义路由规则。
/**
* 配置自定义路由表规则
*/
module.exports = {
router: {
extendRoutes(routes, resolve) {
routes.splice(0);//清除nuxtjs根据Pages目录生成的默认路由
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'), //父组件
children: [
{
path: '', //默认子路由
name: 'home',
component: resolve(__dirname, 'pages/home/')
},
{
path: '/login',
name: 'login',
component: resolve(__dirname, 'pages/login/')
},
{
path: '/register',
name: 'register',
component: resolve(__dirname, 'pages/login/')
},
{
path: '/profile/:username',
name: 'profile',
component: resolve(__dirname, 'pages/profile/')
},
{
path: '/settings',
name: 'settings',
component: resolve(__dirname, 'pages/settings/')
},
{
path: '/editor',
name: 'editor',
component: resolve(__dirname, 'pages/editor/')
},
{
path: '/article/:slug',
name: 'article',
component: resolve(__dirname, 'pages/article/')
}
]
}
]);
}
}
}
pages/login/index.vue
登录/注册功能需要共用一个页面组件,使用计算属性判断当前页面是登录还是注册,根据该计算属性,进行登录注册页面差异性处理。
computed: {
isLogin() {
return this.$route.name === 'login';
}
},
pages/layout/index.vue
将页面模板顶部导航栏组件中所有的a
标签换成nuxt-link
标签,并为其to
属性添加正确的页面链接,达到点击以后跳转到正确页面的效果。
<nav class="navbar navbar-light">
<div class="container">
<nuxt-link class="navbar-brand" to="/">conduitnuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<nuxt-link exact class="nav-link" to="/">Homenuxt-link>
li>
<template v-if="user">
<li class="nav-item">
<nuxt-link class="nav-link" to="/editor">
<i class="ion-compose">i> New Post
nuxt-link>
li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/settings">
<i class="ion-gear-a">i> Settings
nuxt-link>
li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/profile/lpz">
<img class="user-pic"
:src="user.image">
{
{user.username}}
nuxt-link>
li>
template>
<template v-else>
<li class="nav-item">
<nuxt-link class="nav-link" to="/login">Sign innuxt-link>
li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/register">Sign upnuxt-link>
li>
template>
ul>
div>
nav>
为了使当前激活的顶部导航栏高亮,需要在nuxt.config.js
中配置linkActiveClass
module.exports = {
router: {
//全局配置 组件默认的激活类名,使当前激活的顶部导航栏高亮
linkActiveClass: 'active',
.....
}
}
安装axios
:
yarn add axios
plugins/request.js
封装基于 axios
的请求模块
import axios from 'axios';
export const request = axios.create({
baseURL: 'https://conduit.productionready.io/', //设置基准路径
});
api/user.js
封装登录/注册网络请求模块
import {
request } from '@/plugins/request';
//用户登录接口
const login = data => {
return request ({
method: 'Post',
url: '/api/users/login',
data
});
}
//用户注册接口
const register = data => {
return request ({
method: 'Post',
url: '/api/users',
data
});
}
export {
login, register }
pages/login/index.vue
获取用户在页面输入的用户信息,进行表单验证,完成基本的登录/注册功能,并将登录失败之后的错误信息在页面展示。
<template>
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">{
{
isLogin ? 'Sign in' : 'Sign up' }}</h1>
<p class="text-xs-center">
<nuxt-link v-if="isLogin" to="/register">Need an account?</nuxt-link>
<nuxt-link v-else to="/login">Have an account?</nuxt-link>
</p>
<!--显示请求失败之后的信息-->
<ul class="error-messages">
<template v-for="(messages, filed) in errors">
<li v-for="(message, index ) in messages"
:key="index">{
{
filed }} {
{
message}}</li>
</template>
</ul>
<form @submit.prevent="onSubmit">
<fieldset v-if="!isLogin" class="form-group">
<!--required标识字段是必填项-->
<input required
v-model="user.username"
class="form-control form-control-lg"
type="text"
placeholder="Your Name"
/>
</fieldset>
<fieldset class="form-group">
<input required
v-model="user.email"
class="form-control form-control-lg"
type="email"
placeholder="email"
/>
</fieldset>
<fieldset class="form-group">
<input required
v-model="user.password"
class="form-control form-control-lg"
type="password"
placeholder="Password"
minlength="8"
/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
{
{
isLogin ? 'Sign in' : 'Sign up' }}
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import {
login, register } from '@/api/user';
export default {
name: 'Login',
data() {
return {
user: {
username: '',
email: '',
password: ''
},
errors: {
}
}
},
computed: {
//判断页面是否为登陆页面还是注册页面,方便进行差异化处理
isLogin() {
return this.$route.name === 'login';
}
},
methods: {
async onSubmit() {
// 通过 try catch 捕获错误信息
try {
// 调用登录/注册接口
const {
data } = this.isLogin ?
await login({
user: this.user }) : await register({
user: this.user });
this.$router.push('/'); //跳转到首页
} catch (error) {
//记录登录/注册失败的信息,用于页面显示
this.errors = error.response.data.errors;
}
}
}
};
</script>
存储用户登录态
api
接口也必须携带登录用户的身份令牌Token
基于以上原因,必须在用户成功登录后将用户信息存储起来,以方便后续数据共享
注意:不同于纯客户端渲染,这里存储起来的用户登录态需要前后端共享,要同时能被客户端和服务端获取
store/index.vue
创建数据容器
/**
* 在服务端运行期间都是同一个实例
* 为了防止数据命名冲突,要把state定义成一个函数,返回数据对象
*/
const state = () => {
return {
user: null //当前登录用户的登录态
}
}
const mutations = {
setUser(state, data) {
state.user = data;
}
}
注意:
1、Nuxt.js
中已经集成了vuex
,它会自动加载store目录下的数据容器模块,承载数据模块的目录名必须是store
2、服务端运行期间都是同一个实例,为了防止数据命名冲突,要把state定义成一个函数,然后返回数据对象
pages/login/index.vue
将登录用户的登录态保存到数据容器vuex中
this.$store.commit('setUser', data.user); //保存用户登录状态
持久化用户登录态
使用vuex
进行用户登录态的存储,只是在程序运行期间,将数据存储到了容器中,属于内存中的数据,刷新之后,页面进行初始化,同时内存中的数据也被初始化了,要想一直保持用户登录态需要对数据容器中的数据进行持久化。由于是服务端渲染,前后端需要共享数据,用户登录态需要在客户端和服务端同时访问,这里选择cookie
进行数据持久化。
store/index.vue
const cookieparser = process.server ? require('cookieparser') : undefined;
/**
* 在服务端运行期间都是同一个实例
* 为了防止数据命名冲突,要把state定义成一个函数,返回数据对象
*/
const state = () => {
return {
user: null //当前登录用户的登录态
}
}
const mutations = {
setUser(state, data) {
state.user = data;
}
}
const actions = {
/**
* nuxtServerInit是nuxt特有的action方法,只会在服务端渲染期间自动调用
* 作用:初始化容器数据,传递给客户端使用
*/
nuxtServerInit ({
commit }, {
req }) {
let user = null
//如果请求头中有cookie
if (req.headers.cookie) {
//使用cookieparser将cookie字符串转换为js对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
commit('setUser', user)
}
}
export {
state, mutations, actions }
pages/login/index.vue
//如果是浏览器端就加载js-cookie
const Cookie = process.client ? require('js-cookie') : undefined
...
this.$store.commit('setUser', data.user); //保存用户登录状态
Cookie.set('user', data.user); //数据持久化
考虑到同构渲染的页面拦截,从服务端的角度出发,进入页面之前就处理页面的访问,需要使用中间件来处理页面访问权限。Nuxt.js
提供了自己的方案——路由中间件,它既能处理服务端渲染的路由拦截,也能处理客户端渲染的路由拦截
middleware/authenticated.js
/**
* 验证用户是否登录的中间件
*/
export default function ({
store, redirect }) {
//用户没有登录,重定向到登录页面
if (!store.state.user) {
return redirect('/login')
}
}
middleware/notAuthenticated.js
export default function ({
store, redirect }) {
//用户已经登录过了,直接跳转到首页
if (store.state.user) {
return redirect('/')
}
}
哪个页面需要进行权限校验,就给对应的页面加上中间件
export default {
//在路由匹配组件之前会先执行中间件处理
middleware: 'authenticated',
name: 'Editor'
}
注意:给页面加入中间件以后,
Nuxt.js
会自动根据页面配置,找到middleware
目录下对应的中间件,进行页面权限控制
展示公共文章列表
api/article.js
封装首页公共文章列表请求接口
import {
request } from '@/plugins/request';
//获取公共文章接口
export const getArticles = params => {
return request ({
method: 'Get',
url: '/api/articles',
params
});
}
pages/home/index.vue
获取首页公共文章列表数据
async asyncData () {
const {
data } = await getArticles()
return {
articles: data.articles }
}
pages/home/index.vue
首页公共文章列表模板-数据进行绑定
<div class="article-preview"
v-for="article in articles" :key="article.slug">
<div class="article-meta">
<nuxt-link :to="{
name: 'profile',
params: {
username: article.author.username
}
}">
<img :src="article.author.image"/>
</nuxt-link>
<div class="info">
<nuxt-link class="author" :to="{
name: 'profile',
params: {
username: article.author.username
}
}">
{
{
article.author.username }}
</nuxt-link>
<span class="date">{
{
article.createdAt | date('MMM DD,YYYY') }}</span>
</div>
<button
class="btn btn-outline-primary btn-sm pull-xs-right"
:disabled="article.favoriteDisabled"
:class="{active: article.favorited}"
@click="onFavorite(article)">
<i class="ion-heart"></i>
{
{
article.favoritesCount}}
</button>
</div>
<nuxt-link class="preview-link" :to="{
name: 'article',
params: {
slug: article.slug
}
}">
<h1>{
{
article.title}}</h1>
<p>{
{
article.description}}</p>
<span>Read more...</span>
</nuxt-link>
</div>
首页数据分页处理
pages/home/index.vue
处理页面参数
async asyncData ({
query }) {
const page = Number.parseInt(query.page || 1)
const limit = 20
const {
data } = await getArticles({
limit, // 每页大小
offset: (page-1) *limit
})
return {
limit,
page,
articlesCount: data.articlesCount,
articles: data.articles
}
},
pages/home/index.vue
使用计算属性计算总页码
computed: {
totalPage() {
return Math.ceil(this.articlesCount / this.limit);
}
},
pages/home/index.vue
遍历生成页码列表
<nav>
<ul class="pagination">
<li class="page-item"
v-for="item in totalPage" :key="item"
:class="{ active: item === page }">
<nuxt-link class="page-link" :to="{
name: 'home',
query: {
page: item,
tag: $route.query.tag
}
}">{
{
item}}</nuxt-link>
</li>
</ul>
</nav>
pages/home/index.vue
设置导航链接,实现页面展示当前点击页码的数据,使用nuxt-link
实现页面的局部刷新
<nuxt-link class="page-link" :to="{
name: 'home',
query: {
page: item,
tag: $route.query.tag
}
}">{
{
item}}
</nuxt-link>
由于在vue当中,query参数的变化不会导致页面或路由组件重新渲染,页面也不会刷新——Nuxt.js提供了监听query参数变化机制。
pages/home/index.vue
//使用watchQuery属性可以监听参数字符串的更改。 如果定义的字符串发生变化,将调用所有组件方法
watchQuery: ['page', 'tag', 'tab']
优化并行异步请求
pages/home/index.vue
import {
getArticles, getFeedArticles } from '@/api/article';
const loadArticles = store.state.user && tab === 'your_feed' ? getFeedArticles: getArticles;
const [ articleRes, tagRes ] = await Promise.all([
loadArticles({
limit,
tag: tag,
offset: ( page - 1 ) * limit
}),
getTags()
]);
plugins/request.js
通过插件获取当前app实例
/**
* 网络请求相关的插件
*/
import axios from 'axios';
export const request = axios.create({
baseURL: 'https://conduit.productionready.io/', //设置基准路径
});
//通过插件机制获取上下文对象(query、params、req、res、app、store)
export default ({
store }) => {
//请求拦截器
request.interceptors.request.use(function (config) {
const {
user } = store.state;
if(user && user.token) {
config.headers.Authorization = `Token ${
user.token}`;
}
return config;
}, function (error) {
//如果请求失败,对错误信息处理
return Promise.reject(error);
});
}
plugins/request.js
注册插件
//注册插件
plugins: [
'~/plugins/request.js',
'~/plugins/dayjs.js'
]
plugins/dayjs.js
使用dayjs插件结合过滤器统一处理日期格式
import vue from 'vue';
import dayjs from 'dayjs';
vue.filter('date', (value, format="YYYY-MM-DD HH:mm:ss") => {
return dayjs(value).format(format);
})
pages/home/index.vue
显示统一处理后的时间
<span class="date">{
{
article.createdAt | date('MMM DD,YYYY') }}</span>
pages/article/index.vue
由于用户提交的文章内容有可能是Markdown格式的,在页面显示的时候,需要把Markdown转化为Html,这里使用markdown-it
插件
import MarkdownIt from "markdown-it";
async asyncData({
params }) {
const {
data } = await getArticleDetails(params.slug);
const {
article } = data;
const md = new MarkdownIt(); //调用构造函数
article.body = md.render(article.body);//将Markdown转化为Html
return {
article };
}
<div class="col-md-12" v-html="article.body">div>
pages/article/index.vue
设置页面组件的head方法
head() {
return {
title: `${this.article.title} -realword`,
meta: [
{
hid: "description",
name: "description",
content: this.article.description,
},
],
};
},
注意:为了避免子组件中的 meta 标签不能正确覆盖父组件中相同的标签而产生重复的现象,建议利用 hid 键为 meta 标签配一个唯一的标识编号。
Nuxt.js
进行同构应用的开发时,必须严格遵守Nuxt
目录结构规则
pages
页面目录用于组织应用的路由及视图components
组件目录用于组织应用的 Vue.js 组件middleware
目录用于存放应用的中间件assets
资源目录 用于组织未编译的静态资源如 LESS、SASS 或 JavaScriptplugins
插件目录用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件static
静态文件目录用于存放应用的静态文件store
目录用于组织应用的 Vuex 状态树 文件Nuxt.js
开发组件时,与使用Vue.js
开发组件、父子组件的传值一致Nuxt.js
提供了自己的方案——路由中间件,它既能处理服务端渲染的路由拦截,也能处理客户端渲染的路由拦截服务器端渲染基础
服务器端渲染-Nuxt.js基础
服务器端渲染-Nuxt.js综合案例
服务器端渲染-Nuxt.js综合案例发布部署
服务器端渲染-Vue SSR搭建