线上浏览地址:http://117.50.94.220:3000/
学习收获:
-
掌握使用Nuxt.js开发同构渲染应用
-
增强vue.js实践能力
-
掌握同构渲染应用中常见的功能处理
- 用户状态管理
- 页面访问权限处理
- SEO优化
-
掌握通过渲染应用的发布与部署
-
DEMO地址 https://demo.realworld.io/#/
- mkidr realworld-nuxt.js
- npm init -y
- npm i nuxt
- 配置启动脚本: script---dev--> nuxt
- 创建pages目录,配置初始页面
- 启动页面: npm run dev 可以看到home内容
-
创建app.html
根目录创建static文件夹:新建index.css并复制内容
<!DOCTYPE html> <html {{ HTML_ATTRS }}> <head {{ HEAD_ATTRS }}> {{ HEAD }} <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="/index.css"> </head> <body {{ BODY_ATTRS }}> {{ APP }} </body> </html>
在pages中创建layout/index.vue
修改layout中a标签为nuxt-link,并添加to 另外添加点击高亮
<template>
<div>
<!-- 顶部导航栏 -->
<nav class="navbar navbar-light">
<div class="container">
<a class="navbar-brand" href="index.html">conduit</a>
<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>
</li>
<li class="nav-item">
<a class="nav-link" href="">
<i class="ion-compose"></i> New Post
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">
<i class="ion-gear-a"></i> Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">Sign up</a>
</li>
</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>
export default {
name: "LayoutIndex",
};
</script>
<style>
</style>
- 根目录创建nuxt.config.js
- 修改导航中
a标签
为nuxt-link
- 路由高亮:添加
linkActiveClass: 'active', // 路由高亮
// nuxt.js的配置
module.exports = {
router: {
linkActiveClass: 'active', // 路由高亮
extendRoutes(routes, resolve) {
routes.splice(0) // 清空默认路由
routes.push(...[
{
path: '/',
component: resolve(__dirname, 'pages/layout/'),
children:[
{
path: '', // 默认子路由
name: 'home',
component: resolve(__dirname,'pages/home/')
}
]
}
])
}
}
}
- 创建home文件夹复制下面home页内容到index.vue
- 分别创建Login/Register,Profile,Settings,Create/Edit Article,Article页面,以及路由添加
// 基于axios的封装
import axios from 'axios'
const request = axios.create({
baseURL: 'https://conduit.productionready.io'
})
// 请求拦截器
// 响应拦截器
export default request
import axios from '@/utils/request'
// 用户登录
export const login = data => {
return {
method: 'POST',
url: '/api/users/login',
data
}
}
// 用户注册
export const register = data => {
return {
method: 'POST',
url: '/api/users',
data
}
}
export default {
name: 'LoginIndex',
computed: {
isLogin(){
return this.$route.name === 'login'
}
}
};
{{isLogin? 'Sign in': 'Sign up'}}
<p class="text-xs-center">
<nuxt-link v-if="isLogin" to='/register'>Have an account?</nuxt-link>
<nuxt-link v-else to='/login'>need an account?</nuxt-link>
</p>
<fieldset v-if="!isLogin" class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Your Name"
/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
{{isLogin? 'Sign in': 'Sign up'}}
</button>
//对接口返回错误信息的处理: 使用try catch 并展示错误信息
try {
// 提交表单请求
const { data }= this.isLogin?await login({
user: {email:this.user.email,password:this.user.password}
}):await register({user: this.user})
this.$store.commit('setUser',data.user)
// 为了防止刷新页面数据丢失 我们需要把数据持久化
Cookie.set('user',data.user)
// 保存用户登录状态 并跳转到首页
this.$router.push('/')
}catch(err){
console.dir(err)
this.errs = err.response.data.errors
}
this.$store.commit('setUser',data.user)
- 安装:
npm i js-cookie
- 使用:
Cookie.set('user',data.user)
如果有登录信息就重定向到'/'路由
middleware: 'notAuthenticaticated', // 也可以是[]
notAuthenticaticated.js
// 验证是否登录的中间件
export default function ({ store, redirect }) {
if (store.state.user) {
return redirect('/')
}
}
- 新建store/index.vue
- 服务端数据持久化
- 安装:npm i cookieparser
- 添加相应代码到store/index.vue
const cookieparser = process.server ? require('cookieparser'):undefined
// 在服务端渲染期间运行都是同一个实例
// 为了防止数据冲突,务必要把state定义成一个函数 返回数据对象
export const state = () => {
return {
// 当前登录用户的登录状态
user: null
}
}
export const mutations = {
setUser(state,data){
// console.log(data)
state.user = data
}
}
export const actions = {
// nuxtServerInit是一个特殊的action方法
// 这个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){
}
// 提交mutation修改state状态
commit('setUser',user)
}
}
}
computed: {
...mapState(['user'])
}
当user存在时,显示home new post setting profile 否则只显示 登录和注册按钮
<template v-if="user"></template>
<template v-else></template>
import {getArticles} from '@/api/api'
export default {
name: 'home',
async asyncData (){
const {data} = await getArticles()
return {
articles: data.articles,
articlesCount: data.articlesCount
}
}
}
<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}}</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right"
:class="{active: article.favorited}"
>
<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>
- 分页数据
- 标签数据
async asyncData ({query}){
const page = Number.parseInt(query.page || 1)
const limit = 2
const {data} = await getArticles({
limit,
offset: (page-1)*limit
})
const {data:tagData} = await getTags()
return {
articles: data.articles,
articlesCount: data.articlesCount,
limit,
page,
tags: tagData.tags
}
},
- 生成分页块
computed: {
totalPage (){
return Math.ceil(this.articlesCount/this.limit)
}
}
- 分页页面激活
:class="{
active: item===page
}"
- 分页点击页面时监听页面刷新数据
watchQuery: ['page'],
async asyncData ({query}){
const page = Number.parseInt(query.page || 1)
const limit = 2
// const {data} = await getArticles({
// limit,
// offset: (page-1)*limit
// })
// const {data:tagData} = await getTags()
// return {
// articles: data.articles,
// articlesCount: data.articlesCount,
// limit,
// page,
// tags: tagData.tags
// }
const [articleRes,tagsRes]=Promise.all([
getArticles({
limit,
offset: (page-1)*limit
}),
getTags()
])
const {articles,articlesCount} = articleRes.data
const {tags} = tagsRes.data
return {
articles,
articlesCount,
page,
limit,
tags
}
},
<ul class="nav nav-pills outline-active">
<li class="nav-item">
<nuxt-link class="nav-link" :to="{
name: 'home',
query: {
tab: 'your_feed'
}
}"
exact
:class="{active: tab==='your_feed'}"
>Your Feed</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link class="nav-link" :to="{
name: 'home',
query: {
tab: 'global_feed'
}
}"
exact
:class="{active: tab==='global_feed'}"
>Global Feed</nuxt-link>
</li>
<li class="nav-item" v-if="tag">
<nuxt-link class="nav-link" :to="{
name: 'home',
query: {
tab: 'tag',
tag: tag
}
}"
exact
:class="{active: tab==='tag'}"
>#{{tag}}</nuxt-link>
</li>
</ul>
<!-- tag -->
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<nuxt-link
v-for="item in tags"
:key="item"
:to="{
name: 'home',
query: {
tab: 'tag',
tag: item,
}
}" class="tag-pill tag-default"
>{{item}}</nuxt-link>
</div>
</div>
</div>
async asyncData ({query}){
const page = Number.parseInt(query.page || 1)
const limit = 2
const {tag} = query
const [articleRes,tagsRes]= await Promise.all([
getArticles({
limit,
offset: (page-1)*limit,
tag
}),
getTags()
])
const {articles,articlesCount} = articleRes.data
const {tags} = tagsRes.data
return {
articles,
articlesCount,
page,
limit,
tags,
tag,
tab: query.tab || 'global_feed'
}
},
watchQuery: ['page','tag','tab'], // 动态检测数据变化 更新数据
- 设置拦截器: 代码地址
// 基于axios的封装
import axios from 'axios'
// 创建请求对象
export const request = axios.create({
baseURL: 'https://conduit.productionready.io'
})
// 通过插件机制获取到上下文对象 query params req res app store
// 插件导出函数必须作为default成员
export default ({store}) => {
// 请求拦截器
// 所有请求都经过这里,可以设置一些公共业务处理 统一设置token
request.interceptors.request.use(function (config) {
// Do something before request is sent
// 请求经过这里
const {user} = store.state
if (user && user.token) {
config.headers.Authorization = `Token ${user.token}`
}
return config;
}, function (error) {
// Do something with request error
// 请求失败进入
return Promise.reject(error);
});
}
- 创建plugins/request.js 放入上面代码
- 在nuxt.config.js中注册插件
plugins: [
'~/plugins/request.js'
]
- 在接口中按需引入
import {request} from '@/plugins/request'
-
安装:
npm install dayjs --save
-
使用:
-
在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(format) })
-
在nuxt.config.js注册插件
// 注册插件 plugins: [ '~/plugins/request.js', '~/plugins/dayjs.js' ]
-
使用的地方
{{ time| date}}
date是格式化的地方<span class="date">{{article.createdAt | date("MMM DD, YYYY")}}</span>
-
-
方法中写入
async onFavorite(article){ article.favoriteDisabled = true //为了防止重复点击 当第一次点击时开始禁用 点击结束 允许下次点击 if (article.favorited){ // 已点赞 取消 await unFavorite(article.slug) // 删除点赞 article.favorited = false // 点赞按钮未点赞显示 article.favoritesCount += -1 // 点赞数据更新 } else { // 添加 await onFavorite(article.slug) // 点赞 article.favorited = true // 点赞按钮点赞显示 article.favoritesCount += 1 // 点赞数据更新 } article.favoriteDisabled = false //为了防止重复点击 }
-
在async asyncData 中 articles中加入favoriteDisabled
articles.forEach(article => { article.favoriteDisabled = false });
-
点赞按钮
<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>
async asyncData ({params}){
const {data} = await getArticle(params.slug)
return {
article: data.article
}
}
-
安装
npm install markdown-it --save
-
在article中引入
import MarkdownIt from 'markdown-it' // data中 const md = new MarkdownIt() article.body = md.render(article.body)
在html中使用
<div class="row article-content"> <div class="col-md-12" v-html="article.body"> </div> </div>
-
创建子组件
-
父组件中引入
<template> <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 :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-sm btn-outline-secondary" :class="{active:article.author.following}" > <i class="ion-plus-round"></i> Follow Eric Simons <span class="counter">(10)</span> </button> <button class="btn btn-sm btn-outline-primary" :class="{active:article.author.favorited}" > <i class="ion-heart"></i> Favorite Post <span class="counter">(29)</span> </button> </div> </template> <script> export default { name: 'ArticleMeta', props: { article: { type: Object, required: true }, }, } </script> <style> </style>
-
head() {
return {
title: `${this.article.title} - RealWorld`,
meta: [
{
hid: 'description',
name: 'description',
content: this.article.description
}
]
}
}
import {getComments,addComments} from '@/api/api'
export default {
name: 'ArticleComment',
props: {
article: {
type: Object,
required: true
}
},
data(){
return{
comments:[],
comment:''
}
},
async mounted(){
const { data } = await getComments(this.article.slug)
this.comments = data.comments
},
methods: {
async addComments(article){
console.log(article)
const slug = article.slug
const res = await addComments(slug,{comment: {body: this.comment}})
console.log(res)
}
}
}
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
npm run build
npm run start
-
配置需要的host+port
在nuxt.config.js中
server: { host: '0.0.0.0', port: 3000 },
-
压缩发布包:需要的内容:.nuxt static nuxt.config.js package.json package-lock.json
-
把包传到服务端:
-
登录服务器:ssh root@ip地址 输入密码 进入服务器
-
再次连接服务器,解压压缩
-
解压
-
zip解压:unzip 包名
-
tar包:
#压缩文件 file1 和目录 dir2 到 test.tar.gz tar -zcvf test.tar.gz file1 dir2 #解压 test.tar.gz(将 c 换成 x 即可) tar -zxvf test.tar.gz #列出压缩文件的内容 tar -ztvf test.tar.gz #-z : 使用 gzip 来压缩和解压文件 #-v : --verbose 详细的列出处理的文件 #-f : --file=ARCHIVE 使用档案文件或设备,这个选项通常是必选的 #-c : --create 创建一个新的归档(压缩包) #-x : 从压缩包中解出文件
-
# 压缩文件 rar a -r test.rar file # 解压文件 unrar x test.rar # a : 添加到压缩文件 # -r : 递归处理 # x : 以绝对路径解压文件
-
ls -a
查看目录下文件
-
-
-
安装依赖:
npm i
-
启动服务
nuxt start
安装: npm install --global pm2
启动: pm2 start 脚本路径 (pm2 start npm --start) ---start是传参的参数
上面部署是传统模式部署,比较繁琐
- jenkins
- gitlab CI
- github actions
- travis CI
- circle CI
-
linux服务器
-
代码可提交的仓库,如:github等远程仓库
-
- 点击头像的setting----找到Developer settings---> Personal access tokens--->Generate new token---> note输入名字---->权限位置选择repo就可以----> 点击生成---->复制令牌
-
配置到项目的secrets中:点开github仓库,找到setting---------->左侧栏Secrets---> new secrets--->输入name value
-
在项目根目录创建.github/workflows目录
-
下载main.yml到workflows目录中:脚本地址
-
修改配置
-
配置PM2配置文件:在项目根目录下创建pm2.config.json
{ "apps": [ "name": "RealWorld", "script": "npm", "args": "start" ] }
-
提交更新
-
查看自动部署状态
-
访问网站
-
提交更新
github中查看actions
TOKEN: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
1、目录拷贝:
scp -r ./util root@192.168.1.0:/home/wwwroot/limesurvey_back/scp
2、文件拷贝
scp ./util root@192.168.1.0:/home/wwwroot/limesurvey_back/scp