最近看了下 xx联盟 后,也想在自己的网站中接入广告,但是我的网站是用 vue 开发的单页面应用,对
初始化项目
通过 Nuxt.js 官方提供的脚手架工具
运行
1 2 3 | npx create-nuxt-app <项目名> // 或 yarn create nuxt-app <项目名> |
1 2 3 4 | yarn create nuxt-app <项目名> // 等价于下面 yarn global add create-nuxt-app create-nuxt-app <项目名> |
项目配置
目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | . ├── .editorconfig ├── .env ├── .eslintrc.js ├── .git/ ├── .gitignore ├── .nuxt/ // Nuxt自动生成,临时的用于编辑的文件 ├── .prettierrc ├── README.md ├── assets/ // 用于组织未编译的静态资源如 LESS、SASS 或 JavaScript ├── components/ // 用于组织应用的 Vue.js 组件。不会像页面组件那样有 asyncData 方法的特性 ├── jsconfig.json ├── layouts/ // 用于组织应用的布局组件 ├── middleware/ // 目录用于存放应用的中间件 ├── nuxt.config.js // 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。 ├── package.json ├── pages/ // 用于组织应用的路由及视图,并自动生成对应的路由配置 ├── plugins/ // 用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。 ├── server/ ├── static/ // 用于存放应用的静态文件,不会被构建编译处理,会映射至应用的根路径 / ├── store/ // 用于组织应用的 Vuex 状态树 文件 ├── stylelint.config.js ├── tsconfig.json └── yarn.lock |
运行项目
1 | yarn run dev |
在浏览器中,打开 http://localhost:3000
搭建前端页面
项目启动后,我们就可以开发前端页面了。
添加页面
在
1 2 3 4 5 6 7 | pages/ └── article/ ├── index.vue ├── _category/ │ └── index.vue └── detail/ └── _articleId.vue |
将生成如下路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | router: { routes: [ { name: 'article', path: '/article', component: 'pages/article/index.vue' }, { name: "article-category" path: "/article/:category", component: 'pages/article/_category/index.vue', }, { name: "article-detail-articleId" path: "/article/detail/:articleId", component: 'pages/article/detail/_articleId.vue' } ] } |
布局layout
因为我的博客有这几个部分,
在
从草图中,可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <template> <AppLayout> <nuxt /> </AppLayout> </template> <script> import AppLayout from '@/components/framework/app-layout/index.js'; export default { name: 'AppLayoutDefault', components: { AppLayout, }, }; </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <template> <AppLayout> <div class="z-container"> <div class="user-page-wrap"> <div class="user-page-side"> <div class="hidden-xs"> <UserBrief /> </div> <Card :padding="0" style="margin-bottom: 0;"> <UserMenu></UserMenu> </Card> </div> <div class="user-page-main"> <nuxt /> </div> </div> </div> </AppLayout> </template> <script lang="ts"> import AppLayout from '@/components/framework/app-layout/index.js'; import Card from '@/components/base/card/'; import UserMenu from '@/components/framework/user-menu.vue'; import UserBrief from '@/components/framework/user-brief.vue'; export default { name: 'AppLayoutUser', components: { AppLayout, Card, UserMenu, UserBrief, }, head() { return { title: '用户中心', }; }, }; </script> |
注意
使用布局
如果,没有向下面这样注明
1 2 3 | export default Vue.extend({ layout: 'admin', }); |
组件,页面组件
公用的组件,我们知道是放在
我建议是也放在
1 2 3 4 5 6 7 8 9 | components/ ├── base 基本组件 ├── framework 布局相关组件 └── page/ 各个页面下的组件 ├── admin ├── ebook ├── home ├── movie └── user |
css js img 放哪?
一般放在
如果,不需要
全局样式
首先,全局样式的文件,可以放在
1 2 3 | module.exports = { css: ['@/assets/css/grid.less', '@/assets/css/reset.less'], } |
可以将,各个独立的样式,通过一个文件引入,然后,再配置下:
1 2 3 4 5 6 7 | // assets/css/index.less @import './variables.less'; @import "./clearfix.less"; @import './reset.less'; @import './animate.less'; @import "./grid.less"; @import './common.less'; |
1 2 3 | module.exports = { css: ['@/assets/css/index.less'], } |
less 全局变量
如果你用
首先定义好变量文件
1 2 3 4 | @colorPrimary: #2d8cf0; @colorPrimaryLight: lighten(@colorPrimary, 10%); @colorPrimaryDark: darken(@colorPrimary, 10%); @colorPrimaryFade: fade(@colorPrimary, 10%); |
然后在
1 2 3 4 5 6 7 8 9 10 11 12 13 | module.exports = { build: { // extend(config, ctx) {}, loaders: { less: { lessOptions: { modifyVars: getLessVariables(resolve('assets/css/variables.less')), javascriptEnabled: true, }, }, }, }, } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | const path = require('path'); const fs = require('fs'); function resolve(dir) { return path.join(__dirname, dir); } function getLessVariables(file) { const themeContent = fs.readFileSync(file, 'utf-8'); const variables = {}; themeContent.split('\n').forEach(function(item) { if (item.includes('//') || item.includes('/*')) { return; } const _pair = item.split(':'); if (_pair.length < 2) return; const key = _pair[0].replace('\r', '').replace('@', ''); if (!key) return; const value = _pair[1] .replace(';', '') .replace('\r', '') .replace(/^\s+|\s+$/g, ''); variables[key] = value; }); return variables; } |
全局过滤器
在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import Vue from 'vue'; import dayjs from 'dayjs'; export function dateFormatFilter(date, fmt) { if (!date) { return '-'; } else { return dayjs(date).format(fmt); } } const filters = { dateFormatFilter, }; Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]); }); export default filters; |
然后,在
1 2 3 | module.exports = { plugins: ['~/plugins/filters.js'], } |
自定义指令
跟定义全局过滤器一样,需要在
在
1 2 3 4 5 6 7 | import Vue from 'vue'; const focus = Vue.directive('focus', { inserted(el) { el.focus(); }, }); export default focus; |
然后,在
1 2 3 4 5 6 | module.exports = { plugins: [ '~/plugins/filters.js', { src: '~/plugins/directive/focus/index.js', ssr: false }, ], } |
head 动态设置页面标题
我的博客中的
之前单页面的时候,我是配置在,
我们可以通过在页面中配置这个属性,来修改页面的
1 2 3 4 5 6 | head() { return { title: `${this.blogResult.title} 详情页`, meta: [{ hid: 'ebook-home referrer', name: 'referrer', content: 'never' }], }; }, |
也可以,通过
asyncData
因为采用的是
asyncData 方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用asyncData 方法来获取数据并返回给当前组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | export default { data () { return { blogList: [] }; }, async asyncData({ app }: ctxProps) { const params = { page: 1, limit: 10 }; const res = await app.$myApi.blogs.index(params); return { blogList: res.result.list, pageTotal: res.result.pages, itemTotal: res.result.total, }; }, } |
fetch
有些时候,我们只是想在页面进来的时候,拉取最新数据而已,不是修改组件中的值,那么,这个时候,可以使用
如果页面组件设置了 fetch 方法,它会在组件每次加载前被调用(在服务端或切换至目标路由之前)。
fetch 是在组件初始化之前被调用。
与
1 2 3 | async fetch({ store }: ctxProps) { await store.dispatch('common/requestCategoryList'); }, |
解耦 api
建议接口采用 RESTFUL 风格,这样,一个接口基本就是这样
1 2 3 4 5 6 7 8 | import request from '@/utils/request'; export default { GetCategory: (params, options) => request.get('/categories', params, options), PostCategory: (params, options) => request.post('/categories', params, options), PutCategory: (params, options) => request.put(`/categories/${params.categoryId}`, params, options), DeleteCategory: (params, options) => request.delete(`/categories/${params.categoryId}`, params, options), } |
但是这样,每次都需要映入
可以参考这篇文章,可以让你的 api 优雅的解耦:组织并解耦你在 NuxtJs 中调用的 api
proxy 代理
请求接口,难免会用到代理,在
首先,安装依赖(好像不用安装,如果不行,你在安装)
1 | yarn add @nuxtjs/proxy |
然后,在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | module.exports = { modules: ['@nuxtjs/proxy'], proxy: { '/api/': { target: process.env.NODE_ENV === 'production' ? 'http://localhost:3000/' : 'http://localhost:3000/', // target: process.env.NODE_ENV === 'production' ? 'http://zhangjinpei.cn' : 'http://localhost:3000/', changeOrigin: true, }, '/douban/': { target: 'http://api.douban.com/v2', changeOrigin: true, pathRewrite: { '^/douban': '', }, }, '/ebookSearch/': { target: 'http://www.shuquge.com/search.php', changeOrigin: true, pathRewrite: { '^/ebookSearch/': '', }, }, }, }; |
之前,在 单页面 的模式下,本地开发,配上类似上面的代理,上线后还需要配置,nginx 代理。但是在
我线上的 ng 代理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | upstream server_host { server localhost:3000; } server { listen 80; server_name zhangjinpei.cn; rewrite ^(.*)$ https://$host$1 permanent; gzip on; gzip_http_version 1.0; gzip_proxied any; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/xwoff image/svg+xml; } #server { # listen 80; # server_name ssr.zhangjinpei.cn; # location / { # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header Host $http_host; # proxy_pass http://127.0.0.1:8000; # } #} server { listen 443 ssl; server_name zhangjinpei.cn; root /root/zhangjinpei/nuxt-ts-blog/; index index.html index.htm; ssl_certificate /xxx/xx; ssl_certificate_key /xxx/xx; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; #location / { # root /root/zhangjinpei/blog-front/dist; # try_files $uri /index.html; #} location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Nginx-Proxy true; proxy_cache_bypass $http_upgrade; proxy_pass http://127.0.0.1:8000; } #location /api/ { # proxy_pass http://server_host/api/; # poxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $remote_addr; # proxy_set_header X-Forwarded-Host $host; # proxy_set_header X-Forwarded-Server $host; #} #location /douban/ { # proxy_pass http://api.douban.com/v2/; #} #location /douban/movie/ { # proxy_pass http://api.douban.com/v2/movie/; #} #location /doubanOld/ { # proxy_pass https://movie.douban.com/; #} } |
store
在
在
一种是,模块模式,直接将
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | const state = () => ({ userInfo: null, }); const getters = { getUserInfo: state => state.userInfo, } const mutations = { setUserInfo(state, data) { state.userInfo = data; }, } const actions = { async requestUserInfo({ commit }) { const res = await axios.get('xx'); commit('setUserInfo', res.data); }, } export default { namespaced: true, state, getters, mutations, actions, }; |
一种是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | export default new Vuex.Store({ state: () => ({ counter: 0 }),, mutations: { ...mutations, }, actions: { ...actions, }, modules: { common, }, }); |
注意
无论使用那种模式,您的
state 的值应该始终是function ,为了避免返回引用类型,会导致多个实例相互影响。
我们甚至可以,将模块文件分解成单独的 js 文件,如:
注意
在使用拆分文件模块时,必须记住使用箭头函数功能,
this 在词法上可用。词法范围this 意味着它总是指向引用箭头函数的所有者。如果未包含箭头函数,那么this 将是未定义的(undefined) 。解决方案是使用"normal" 功能,该功能会将this 指向自己的作用域,因此可以使用。
构建 部署
执行构建命令,然后启动
1 2 | yarn run build yarn run start |
但是这样启动,就一直得开着后台,我用的
pm2 启动
在
1 2 3 4 5 | { "scripts": { "pm2start": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json", } } |
然后,在
1 2 3 4 5 6 7 8 9 10 11 | { "apps": [ { "name": "blog-front-ssr", "script": "./server/index.js", "instances": 0, "watch": false, "exec_mode": "cluster_mode" } ] } |
最后,启动:
1 | npm run pm2start |
总结
OK,到目前为止,我把自己从完全没用过 nuxt ,到网站正式上线所遇到的问题都记录在这了,希望对对你也有帮助。最后,这是我用 nuxt 做的个人博客 zhangjinpei.cn
参考链接
Vue SSR 指南、
Nuxt 官网、
TypeScript、
Nuxt Typescript、
restfule api
阿里云图片处理