本文首发于我的GitHub博客,其它平台同步更新。
本文旨在一步步搭建起完整的NuxtJS项目,方便学习
项目结构
realworld-nuxtjs
├── .git
├── .gitignore
├── .nuxt
├── .vscode
├── api
├── app.html
├── LICENSE
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── pm2.config.js
├── README.md
├── realworld-nuxtjs.zip
├── static
└── store
创建项目
- 生成 package.json 文件 / 安装 nuxt 依赖
# 创建项目目录
mkdir realworld-nuxtjs
# 进入项目目录
cd realworld-nuxtjs
# 生成 package.json 文件
npm init -y
# 安装 nuxt 依赖
npm install nuxt
- 在
package.json中添加启动脚本
"scripts": {
"dev": "nuxt"
}
- 创建
pages/index.vue - 启动服务
导入样式资源
格式:[页面模板名称] === [Nuxtjs视图]
Header === 模板
app.html
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
<link href="//code.ionicframework.com/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">
</head>
<body {{ BODY_ATTRS }}>
<h1>app.html</h1>
{{ APP }}
</body>
</html>
Header === 视图布局
pages/layout/index.vue
- 需要区分登录与未登录状态
<template>
<!-- 顶部导航栏 -->
<div>
<nav class="navbar navbar-light">
<div class="container">
<nuxt-link class="navbar-brand" to="/">Home</nuxt-link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<nuxt-link class="nav-link" to="/" exact>Home</nuxt-link>
</li>
<!-- 登录了 -->
<template v-if="user">
<!--粘贴模板-->
</template>
<!-- 未登录 -->
<template v-else>
</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>
登录注册
pages/login.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="/login">Have an account?</nuxt-link>
<nuxt-link v-else to="/register">Need 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"
minlength="8"
required
/>
</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 {
middleware: 'notAuthenticated',
name: "LoginIndex",
computed: {
isLogin() {
return this.$route.name === "login";
},
},
data() {
return {
user: {
username: "",
email: "2676697536@qq.com",
password: "",
},
errors: [], //错误信息
};
},
methods: {
async onSubmit() {
//...
},
},
};
</script>
<style>
</style>
剩余页面(略)
nuxt.config.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',
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/')
},
]
}
])
}
},
}
封装请求模块
安装axios:
npm i axios
plugins\request.js
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
})
}
//修改
export const updateUser = data => {
return request({
method: 'PUT',
url: "/api/user",
data
})
}
pages\login\index.vue引入:
import { login, register } from "@/api/user";
api\tag.jsapi\profile.jsapi\article.js
登录注册
- 封装请求
- 表单验证
- 错误处理
- 用户注册
跨域身份验证
登录态持久化
// 仅在客户端加载 js-cookie 包
const Cookie = process.client ? require('js-cookie') : undefined
async onSubmit() {
try {
//TODO: 保存用户的登录状态
//程序在运行期间存储到内存中,方便共享,刷新就没了
this.$store.commit("setUser", data.user);
//为了防止刷新特面数据丢失,我们要把数据持久化
Cookie.set('user',data.user)
} catch (error) {
...
}
},
存储用户登录状态 === Vuex
store
index.js 在此目录中创建 文件将启用存储
- 导入Vuex
- 将
store选项添加到根Vue实例
- 使用Vuex
- 初始化容器数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default () => {
return new Vuex.Store({
state: {
user: null
},
mutations: {
setUser (state, user) {
state.user = user
}
},
actions: {}
})
}
- 登录成功,将用户信息存入容器
this.$store.commit('setUser', data.user)
- 将登录状态持久化到 Cookie 中
安装js-cookie:
npm i js-cookie
const Cookie = process.client ? require('js-cookie') : undefined
Cookie.set('user', data.user)
- 从 Cookie 中获取并初始化用户登录状态
安装cookieparser:
npm i cookieparser
store/index.js
const cookieparser = process.server ? require('cookieparser') : undefined
actions: {
nuxtServerInit ({ commit }, { req }) {
let user = null
if (req.headers.cookie) {
// 将请求头中的 Cookie 字符串解析为一个对象
const parsed = cookieparser.parse(req.headers.cookie)
try {
// 将 user 还原为 JavaScript 对象
user = JSON.parse(parsed.user)
} catch (err) {
// No valid cookie found
}
}
commit('setUser', user)
}
}
处理页面访问权限 === 路由中间件
middleware\authenticated.js:没登录转跳登录页
/**
* 验证是否登录的中间件
*/
export default function ({ store, redirect }) {
// If the user is not authenticated
console.log('store',store.state)
if (!store.state.user) {
return redirect('/login')
}
}
middleware\notAuthenticated.js:用于拦截已登录状态在地址栏输入登录页面地址,转跳到首页
export default function ({ store, redirect }) {
// If the user is authenticated redirect to home page
if (store.state.user) {
return redirect('/')
}
}
在需要判断登录权限的页面中配置使用中间件:
middleware: 'notAuthenticated',
middleware: "authenticated",
首页模块
pages\home\index.vue
获取数据
async asyncData () {
const { data } = await getArticles()
return data
}
分页处理
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
}
},
页码处理
- 使用计算属性计算总页码
totalPage () {
return Math.ceil(this.articlesCount / this.limit)
}
- 遍历生成页码列表
<nav v-if="$route.query.tab !== 'your_feed'">
<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>
- 设置导航链接
- 响应 query 参数的变化
watchQuery: ['page'],
视图处理
- 没有登录不展示 my-feed
- 处理 tab 切换(
query)以及高亮(exact)问题
<li class="nav-item">
<nuxt-link
class="nav-link"
:class="{ active: tab === 'global_feed' }"
exact
:to="{
name: 'home',
query: {
tab: 'global_feed',
},
}"
>Global Feed</nuxt-link
>
</li>
统一添加数据TOKEN
import axios from 'axios'
//通过插件机制获取到上下文对象(query、params、req、res、app、store...)
//插件导出函数必须作为default成员
export default ({ store }) => {
//请求拦截器:任何请求都要经过请求拦截器
//可以在请求拦截器中做一些公共的业务处理,例如统一设置token
request.interceptors.request.use(function (config) {
//请求经过这里
const { user } = store.state
if (user && user.token) {
config.headers.Authorization = `Token ${user.token}`
}
//返回config请求配置对象
return config;
}, function (error) {
// 如果请求失败(此时请求还没有发出去)就会进入这里
return Promise.reject(error);
});
}//导出一个函数
nuxtServerInit
展示文章标签列表
-
封装接口请求方法
-
数据过滤
- 标签链接
<li v-if="tag" class="nav-item"> <nuxt-link class="nav-link active" :class="{ active: tab === 'tag' }" :to="{ name: 'home', query: { tab: 'tag', tag: tag, }, }" >#{{ tag }}</nuxt-link > </li>- 分页页码链接
<nav v-if="$route.query.tab !== 'your_feed'"> <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>
日期格式处理
npm install dayjs --save
引入nuxtjs的插件机制:nuxt.config.js
//注册插件
plugins: [
'~/plugins/dayjs.js'
]
plugins\dayjs.js
import Vue from 'vue'
import dayjs from 'dayjs'
//{{表达式 | 过滤器}}
Vue.filter('date', (value,format = 'YYYY-MM-DD HH:mm:ss') => {
return dayjs(value).format('MMM DD, YYYY')
})
<span class="date">{{
article.createdAt | date("MMM DD, YYYY")
}}</span>
部署
-
配置Host + Port
nuxt.config.hs
module.exports = {
server: {
host: '0.0.0.0',//监听所有网卡地址
port: 3000
}
}
- 压缩发布包
- 把发布包传到服务端
#连接服务器
ssh root@182.92.21
#上传压缩包
scp ./realworld-nuxtjs.zip root@182.92.210.40:/root/realworld-nuxtjs
- 解压
unzip ./realworld-nuxtjs.zip
- 安装依赖
npm i
- 启动服务器
npm run start
使用PM2启动服务
pm2.config.js
{
"apps": "RealWorld",
"script": "npm",
"args": "start"
}
服务器上运行:
#全局安装PM2
npm install --global pm2
#建立软连接
ln -s /root/node/node-v14.15.3-linux-x64/bin/pm2 /usr/local/bin/pm2
#运行脚本文件
pm2 start npm -- start