案例介绍
案例名称: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-linkclass="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-linkclass="nav-link"to="/"exact>Home</nuxt-link></li><template v-if="user"><li class="nav-item"><nuxt-linkclass="nav-link"to="/editor"><i class="ion-compose"></i> New Post</nuxt-link></li><li class="nav-item"><nuxt-linkclass="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"><imgclass="user-pic":src="user.image">{
{
user.username }}</nuxt-link></li></template><template v-else><li class="nav-item"><nuxt-linkclass="nav-link"to="/login">Sign in</nuxt-link></li><li class="nav-item"><nuxt-linkclass="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-linkclass="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-linkclass="nav-link":class="{
active: tab === 'global_feed'}"exact:to="{
name: 'home'}">Global Feed</nuxt-link></li><li v-if="tag" class="nav-item"><nuxt-linkclass="nav-link":class="{
active: tab === 'tag'}"exact:to="{
name: 'home',query: {
tab: 'tag',tag: tag}}"># {
{
tag }}</nuxt-link></li></ul></div><divclass="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><buttonclass="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-linkclass="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"><liclass="page-item":class="{
active: item === page}"v-for="item in totalPage":key="item"><nuxt-linkclass="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"><templatev-for="(messages, field) in errors"><liv-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') : undefinedexport 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 } = dataconst 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
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中的数据持久化
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// 如果请求头中有 Cookieif (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作为参数
中间件执行流程
- nuxt.config.js
- 匹配布局
- 匹配页面
- 中间件异步执行,只需要返回一个promise,或使用第二个callback作为第一个参数
使用,增加middleware文件夹
/*** 验证是否登录的中间件*/
export default function ({
store, redirect }) {
// If the user is not authenticatedif (!store.state.user) {
return redirect('/login')}
}
在页面中的使用
一个中间件是字符串形式,多个是数组形式
// 在路由匹配组件渲染之前会先执行中间件处理middleware: 'authenticated',
设置页面meta优化 SEO
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 插件
插件:nuxt允许在运行vue之前执行插件,可运行自己的库或者第三方插件
第三方库:例如axios等,使用npm安装
使用vue插件:例如vue-notifications,显示应用的通知信息
需要增加plugins/vue-notifications.js
在nuxt.config.js中配置plugins;
export default (context) =>{
console.log(context) //context是上下文对象
}