案例名称:RealWorld
一个开源的学习项目,目的是帮助开发者开始学习到新技能
nuxt.js 官网: https://zh.nuxtjs.org/
GitHub仓库:https://github.com/gothinkster/realworld
在线示例:https://demo.realworld.io/#/
接口文档:https://github.com/gothinkster/realworld/tree/master/api
页面模板:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INS
TRUCTIONS.md
创建项目目录 mkdir realworld-nuxtjs
进入项目目录 cd realworld-nuxtjs
生成 package.json 文件 npm init -y
安装 nuxt 依赖 npm install nuxt
在 package.json 中添加启动脚本:
创建 pages/index.vue :
启动服务:
创建项目目录 mkdir realworld-nuxtjs
进入项目目录 cd realworld-nuxtjs
生成 package.json 文件 npm init -y
安装 nuxt 依赖 npm install nuxt
“scripts”: { “dev”: “nuxt” }
<template> <div> <h1>Home Page</h1> </div> </template>
<script> export default { name: 'HomePage' }</script>
在浏览器中访问 http://localhost:3000/ 测试。
增加app.html,并倒入样式资源
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/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">
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<!-- <link rel="stylesheet" href="//demo.productionready.io/main.css"> -->
<link rel="stylesheet" href="/index.css">
</head>
<body {{ BODY_ATTRS }}>
{{ APP }}
</body>
</html>
配置布局组件
增加pages/layout/index.vue
<template>
<div>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-light">
<div class="container">
<!-- <a class="navbar-brand" href="index.html">conduit</a> -->
<nuxt-link
class="navbar-brand"
to="/"
>Home</nuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<!-- <a class="nav-link active" href="">Home</a> -->
<nuxt-link
class="nav-link"
to="/"
exact
>Home</nuxt-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/123">
<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 in
</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link
class="nav-link"
to="/register"
>
Sign up
</nuxt-link>
</li>
</template>
</ul>
</div>
</nav>
<!-- /顶部导航栏 -->
<!-- 子路由 -->
<nuxt-child/>
<!-- /子路由 -->
<!-- 底部 -->
<footer>
<div class="container">
<a href="/" class="logo-font">conduit</a>
<span class="attribution">
An interactive learning project from <a href="https://thinkster.io">Thinkster</a>. Code & design licensed under MIT.
</span>
</div>
</footer>
<!-- /底部 -->
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'LayoutIndex',
computed: {
...mapState(['user'])
}
}
</script>
<style>
</style>
新增nuxt.config.js 配置
/**
* Nuxt.js 配置文件
*/
/**
* Nuxt.js 配置文件
*/
module.exports = {
router: {
linkActiveClass: 'active',
// 自定义路由表规则
extendRoutes (routes, resolve) {
// 清除 Nuxt.js 基于 pages 目录默认生成的路由表规则
routes.splice(0)
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/')
}
]
}
])
}
},
server: {
host: '0.0.0.0',
port: 3000
},
// 注册插件
plugins: [
'~/plugins/request.js',
'~/plugins/dayjs.js'
]
}
新建首页 pages/home/index.vue,作为layout的子路由
<template>
<div class="home-page">
<div class="banner">
<div class="container">
<h1 class="logo-font">nuxt练习</h1>
<p>A place to share your knowledge.</p>
</div>
</div>
<div class="container page">
<div class="row">
<div class="col-md-9">
<div class="feed-toggle">
<ul class="nav nav-pills outline-active">
<li v-if="user" class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'your_feed'
}"
exact
:to="{
name: 'home',
query: {
tab: 'your_feed'
}
}"
>Your Feed</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'global_feed'
}"
exact
:to="{
name: 'home'
}"
>Global Feed</nuxt-link>
</li>
<li v-if="tag" class="nav-item">
<nuxt-link
class="nav-link"
:class="{
active: tab === 'tag'
}"
exact
:to="{
name: 'home',
query: {
tab: 'tag',
tag: tag
}
}"
># {{ tag }}</nuxt-link>
</li>
</ul>
</div>
<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"
:class="{
active: article.favorited
}"
@click="onFavorite(article)"
:disabled="article.favoriteDisabled"
>
<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>
<!-- 分页列表 -->
<nav>
<ul class="pagination">
<li
class="page-item"
:class="{
active: item === page
}"
v-for="item in totalPage"
:key="item"
>
<nuxt-link
class="page-link"
:to="{
name: 'home',
query: {
page: item,
tag: $route.query.tag,
tab: tab
}
}"
>{{ item }}</nuxt-link>
</li>
</ul>
</nav>
<!-- /分页列表 -->
</div>
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<nuxt-link
:to="{
name: 'home',
query: {
tab: 'tag',
tag: item
}
}"
class="tag-pill tag-default"
v-for="item in tags"
:key="item"
>{{ item }}</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name:"HomeIndex"
}
</script>
可以删除pages下index.vue 未用到
static下 存放index.css
创建登录页,注册页,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">
<!-- <a href="">Have an account?</a> -->
<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, field) in errors"
>
<li
v-for="(message, index) in messages"
:key="index"
>{{ field }} {{ message }}</li>
</template>
</ul>
<form @submit.prevent="onSubmit">
<fieldset v-if="!isLogin" class="form-group">
<input v-model="user.username" class="form-control form-control-lg" type="text" placeholder="Your Name" required>
</fieldset>
<fieldset class="form-group">
<input v-model="user.email" class="form-control form-control-lg" type="email" placeholder="Email" required>
</fieldset>
<fieldset class="form-group">
<input v-model="user.password" class="form-control form-control-lg" type="password" placeholder="Password" required 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'
// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined
export default {
middleware: 'notAuthenticated',
name: 'LoginIndex',
computed: {
isLogin () {
return this.$route.name === 'login'
}
},
data () {
return {
user: {
username: '',
email: 'qqqqq@163.com',
password: '12345678'
},
errors: {} // 错误信息
}
},
methods: {
async onSubmit () {
try {
// 提交表单请求登录
const { data } = this.isLogin
? await login({
user: this.user
})
: await register({
user: this.user
})
// console.log(data)
// TODO: 保存用户的登录状态
this.$store.commit('setUser', data.user)
// 为了防止刷新页面数据丢失,我们需要把数据持久化
Cookie.set('user', data.user)
// 跳转到首页
this.$router.push('/')
} catch (err) {
// console.log('请求失败', err)
this.errors = err.response.data.errors
}
}
}
}
</script>
<style>
</style>
新建用户资料页 pages/profile/index.vue
<template>
<div class="profile-page">
<div class="user-info">
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-10 offset-md-1">
<img src="http://i.imgur.com/Qr71crq.jpg" class="user-img" />
<h4>Eric Simons</h4>
<p>
Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from the Hunger Games
</p>
<button class="btn btn-sm btn-outline-secondary action-btn">
<i class="ion-plus-round"></i>
Follow Eric Simons
</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-10 offset-md-1">
<div class="articles-toggle">
<ul class="nav nav-pills outline-active">
<li class="nav-item">
<a class="nav-link active" href="">My Articles</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">Favorited Articles</a>
</li>
</ul>
</div>
<div class="article-preview">
<div class="article-meta">
<a href=""><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 29
</button>
</div>
<a href="" class="preview-link">
<h1>How to build webapps that scale</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
</a>
</div>
<div class="article-preview">
<div class="article-meta">
<a href=""><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
<div class="info">
<a href="" class="author">Albert Pai</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 32
</button>
</div>
<a href="" class="preview-link">
<h1>The song you won't ever stop singing. No matter how hard you try.</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">Music</li>
<li class="tag-default tag-pill tag-outline">Song</li>
</ul>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
middleware: 'authenticated',
name: 'UserProfile'
}
</script>
<style>
</style>
新建page/settings/index.vue
<template>
<div class="settings-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Your Settings</h1>
<form>
<fieldset>
<fieldset class="form-group">
<input class="form-control" type="text" placeholder="URL of profile picture">
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name">
</fieldset>
<fieldset class="form-group">
<textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you"></textarea>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email">
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password">
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
Update Settings
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
middleware: 'authenticated',
name: 'SettingsIndex'
}
</script>
<style>
</style>
新增创建文章的pages/pages/editor/index.vue
<template>
<div class="editor-page">
<div class="container page">
<div class="row">
<div class="col-md-10 offset-md-1 col-xs-12">
<form>
<fieldset>
<fieldset class="form-group">
<input type="text" class="form-control form-control-lg" placeholder="Article Title">
</fieldset>
<fieldset class="form-group">
<input type="text" class="form-control" placeholder="What's this article about?">
</fieldset>
<fieldset class="form-group">
<textarea class="form-control" rows="8" placeholder="Write your article (in markdown)"></textarea>
</fieldset>
<fieldset class="form-group">
<input type="text" class="form-control" placeholder="Enter tags"><div class="tag-list"></div>
</fieldset>
<button class="btn btn-lg pull-xs-right btn-primary" type="button">
Publish Article
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
// 在路由匹配组件渲染之前会先执行中间件处理
middleware: 'authenticated',
name: 'EditorIndex'
}
</script>
<style>
</style>
设置文章组件 pages/pages/article/index.vue
<template>
<div class="article-page">
<div class="banner">
<div class="container">
<h1>{{ article.title }}</h1>
<article-meta :article="article" />
</div>
</div>
<div class="container page">
<div class="row article-content">
<div class="col-md-12" v-html="article.body"></div>
</div>
<hr />
<div class="article-actions">
<article-meta :article="article" />
</div>
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<article-comments :article="article" />
</div>
</div>
</div>
</div>
</template>
<script>
import { getArticle } from '@/api/article'
import MarkdownIt from 'markdown-it'
import ArticleMeta from './components/article-meta'
import ArticleComments from './components/article-comments'
export default {
name: 'ArticleIndex',
async asyncData ({ params }) {
const { data } = await getArticle(params.slug)
const { article } = data
const md = new MarkdownIt()
article.body = md.render(article.body)
return {
article
}
},
components: {
ArticleMeta,
ArticleComments
},
head () {
return {
title: `${this.article.title} - RealWorld`,
meta: [
{ hid: 'description', name: 'description', content: this.article.description }
]
}
}
}
</script>
<style>
</style>
安装 axios npm i axios
根目录下 新建 utils/request.js
// 基于axios封装的请求模块
import axios from 'axios'
const request = axios.create({
baseURL: "https://conduit.productionready.io"
})
// 请求拦截器
// 响应拦截器
export default request
api/user.js
import { request } from '@/plugins/request'
// 用户登录
export const login = data => {
return request({
method: 'POST',
url: '/api/users/login',
data
})
}
// 用户注册
export const register = data => {
return request({
method: 'POST',
url: '/api/users',
data
})
}
如果报错可以修改package.json 重新安装依赖
{
"name": "realworld-nuxtjs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nuxt",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"cookieparser": "^0.1.0",
"dayjs": "^1.8.28",
"js-cookie": "^2.2.1",
"markdown-it": "^11.0.0",
"nuxt": "^2.14.12"
}
}
nuxt中以及自动集成了 vuex
直接在项目中创建 store 文件夹即可
必须叫store,nuxt发现之后会自动加载
直接定义state,mutation等即可使用
提交时候可以this.$store.commit(‘setUser’,data.user)
// 在服务端渲染期间运行都是听一个实例
// 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
export const state = () => {
return {
// 当前用户登录的登录状态
foo: 'bar'
}
}
export const mutation = {
setUser (state,data) {
state.user = data
}
}
export const actions = () =>{
}
vuex是为了解决状态共享,
需要解决页面刷新数据丢失问题
可以存入cookie中,前后端通用存储到服务端
前端可以使用 js-cookie,是一个专门客户端浏览器操作的cookie
通过cookie 使得 vuex初始化
// 仅在客户端加载 js-cookie 包
// process.client 是nuxt中提供的数据,如果是true,运行在客户端,false运行在服务端
const Cookie = process.client ? require('js-cookie') : undefined
// 为了防止刷新页面数据丢失,我们需要把数据持久化
Cookie.set('user', data.user)
const cookieparser = process.server ? require('cookieparser') : undefined
// 在服务端渲染期间运行都是同一个实例
// 为了防止数据冲突,务必要把 state 定义成一个函数,返回数据对象
export const state = () => {
return {
// 当前登录用户的登录状态
user: null
}
}
export const mutations = {
setUser (state, data) {
state.user = data
}
}
export const actions = {
// nuxtServerInit 是一个特殊的 action 方法
// 这个 action 会在服务端渲染期间自动调用
// 作用:初始化容器数据,传递数据给客户端使用
nuxtServerInit ({ commit }, { req }) {
let user = null
// 如果请求头中有 Cookie
if (req.headers.cookie) {
// 使用 cookieparser 把 cookie 字符串转为 JavaScript 对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
// 提交 mutation 修改 state 状态
commit('setUser', user)
}
}
不能使用vue的拦截器
可以使用nuxt提供的路由中间件
中间件允许自定义函数运行在一个页面或一组页面渲染之前
中间件放在 middleware 目录下,文件名称将成为中间件名称
中间件接收context作为参数
中间件执行流程
使用,增加middleware文件夹
/**
* 验证是否登录的中间件
*/
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.user) {
return redirect('/login')
}
}
在页面中的使用
一个中间件是字符串形式,多个是数组形式
// 在路由匹配组件渲染之前会先执行中间件处理
middleware: 'authenticated',
Nuxt.js 使用了 vue-meta 更新应用 的头部标签 和html 属性
可以在nuxt.config.js 文件在设置head
head () {
return {
title: this.message,
meta: [
{
name: 'viewport',
content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0'
}
]
}
}
插件:nuxt允许在运行vue之前执行插件,可运行自己的库或者第三方插件
第三方库:例如axios等,使用npm安装
使用vue插件:例如vue-notifications,显示应用的通知信息
需要增加plugins/vue-notifications.js
在nuxt.config.js中配置plugins;
export default (context) =>{
console.log(context) //context是上下文对象
}