从零开始建站(三) - 前端项目搭建

简介:

前端选用VUE是因为它比较简单,容易上手,对于不熟悉前端的小伙伴很适合。对于软件发展来说,后端技术已趋于成熟和稳定,在功能已稳定的情况下,客户会把要求更多的放在页面的美观和合理排版布局上,学习一下前端,特别是自己设计一个页面,有助于对前端的了解和对美观设计的培养。

一、搭建VUE项目

1.搭建VUE基础框架

1.1 安装node.js

安装过程中记得勾选Add to path,安装完成后再cmd命令行输入:node -v 和 npm -v 如果分别显示版本号则安装成功。

1.2 安装vue脚手架vue-cli

输入以下命令:npm install -g vue-cli (其中-g表示全局安装)

1.3 初始化一个项目

cmd命令行进入要安装项目的文件夹,输入以下命令:vue init webpack projectName (其中projectName填写你的项目名称)比如下图,进入Project文件夹,按着问号?后的提示操作,没有用红字写备注的都是默认或者选NO的,最后提示 Project initialization finished 代表成功。

然后我们可以看到在d:project下生成的项目文件夹:

1.4 安装依赖组件

通常我们安装组件方法是先进入项目目录下(比如这里是命令行进入yytf文件夹):输入命令: npm install xxx (比如安装jqueryxxx就填jquery),但我们这里尽量不要通过这种方式安装,还是那个问题,为了减小webpack打包后vendor.js的大小,我们通过cdn方式引入,比如index.html中引入:<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>

1.5 启动服务

通过命令: npm run dev 如果没有报错,就可以通过提示的链接在浏览器登录,看到Welcome to Your Vue.js App”表示登录成功

2.路由模块

2.1 index.html

引入:<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>

2.2 webpack.base.conf.js

module.exports = {}中最后加上

externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'axios': 'axios',
  'vue-resource': 'VueResource'
}

2.3 routes.js

两种引入方式:

//>普通路由引入方式(所有的vue模块的js文件都打包进vendor.js和app.js中)
//import Articles from './components/Articles'
//import Topics from './components/Topics'
//import AboutMe from './components/AboutMe'
//import TimeLine from './components/TimeLine'
//import Pictures from './components/Pictures'

//>按需加载路由引入方式(各个vue模块的js文件分别打包进0.xxx.js、1.xxx.js、2.xxx.....)
const Articles = r => require.ensure([], () => r(require('./components/Articles')));
const Topics = r => require.ensure([], () => r(require('./components/Topics')));
const AboutMe = r => require.ensure([], () => r(require('./components/AboutMe')));
const TimeLine = r => require.ensure([], () => r(require('./components/TimeLine')));
const Pictures = r => require.ensure([], () => r(require('./components/Pictures')));

//构建vue-router实例(这里的VueRouter要和2.2中的名字对应):
export default new VueRouter({
  mode:"history",
  routes: [
    {path: '/',name: 'Articles',component: Articles},
    {path: '/topics',name: 'Topics',component: Topics},
    {path: '/aboutMe',name: 'AboutMe',component: AboutMe},
    {path: '/timeLine',name: 'TimeLine',component: TimeLine},
    {path: '/pictures',name: 'Pictures',component: Pictures}
  ]
})

这里有个坑,如果我们不加mode:"history",那么浏览器的路径会出现#不美观,如果我们加上mode:"history"后,在本地环境下一切都是正常的,但部署到服务器的nginx上跳转后如果刷新页面就会出现404了,这是因为那是因为在history模式下,只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求,但是当我直接在浏览器里输入这个地址的时候,就一定要对服务器发起http请求,但是这个目标在服务器上又不存在,所以会返回404而本地开发时用的服务器为nodeDev环境中自然已配置好了。所以要在nginx.conf里面做一些配置:

location / {
  root   html;
  index  index.html;
    if (!-e $request_filename){
        rewrite ^/(.*) /index.html last;
        break;
  }
}

2.4 使用路由

2.4.1main.js中引入之前的routes.js./routes这个相对路径视情况而定):import router from './routes'

2.4.2main.js中把路由挂载到vue实例上(注意vue对象中左边的router不能随便更换名称):

new Vue({
  el: '#app',
  axios,
  router:router,
  components: { App },
  template: '<App/>'
})

2.4.3app,vue中使用router-view标签:

<template>
    <page-header></page-header>
    <router-view></router-view>
</template>

2.4.4PageHeader.vue中使用导航标签做跳转

<div class="nav">
    <ul class="wow pulse navul">
    <li>
          <router-link to="/" exact>技术文章</router-link>
          <router-link to="/topics" exact>随笔杂谈</router-link>
          <router-link to="/timeLine" exact>时光轴</router-link>
          <router-link to="/aboutMe" exact>关于我</router-link>
          <router-link to="/pictures" exact>图集</router-link>
       </li>
    </ul>
</div>

2.5 路由跳转前的权限校验(需要在routes.js中加meta:{requireAuth: true}

router.beforeEach((to, from, next) => {
  if (to.meta.requireAuth) { //该路由需要登录权限
      if (window.localStorage.getItem('token')) {//进入后台
        next();
      }else {//返回前台
          next({
              path: '/',
              query: {redirect: to.fullPath}  //将跳转的路由path作为参数,登录成功后跳转到该路由
          })
      }
  }
  else {//该路由不需要登录权限
      next();
  }
})

3.axios模块

3.1 简介:

Axios是一个基于promiseHTTP库,可以用在浏览器和node.js中:

>从浏览器中创建 XMLHttpRequests

>支持Promise API

>node.js 创建 http 请求

>拦截请求和响应

>转换请求数据和响应数据

3.2 配置

3.2.1 index.html引入<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>

3.2.2 webpack.base.conf.js module.exports = {}中最后加上

externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'axios': 'axios',
    'vue-resource': 'VueResource'
}

3.2.3 main.js

设置为Vue的内置对象: Vue.prototype.$axios = axios;

设置请求默认前缀: axios.defaults.baseURL = 'http://localhost:8080/blog/';

这里也可以利用上面方法设置请求的header信息,具体可以百度

3.3 使用

getArticles: function() {
var _this = this;
this.$axios.post("article/list", {
      title: _this.xxx
    })
    .then(function(result) {
      var response = result.data;
      if (response.statusCode == "200") {} else {}
})
});

3.4 前端后台管理对请求拦截:

main.js同级目录新建http.js

import router from './routes'
// axios 配置
axios.defaults.timeout = 5000
axios.defaults.baseURL = process.env.BASE_API

// http request 拦截器
axios.interceptors.request.use(
    config => {
      if (window.localStorage.getItem('token')) {
        config.headers.Authorization = window.localStorage.getItem('token');
      }
      return config
    },
    err => {
      return Promise.reject(err)
    }
)
// http response 拦截器
axios.interceptors.response.use(
    response => {
        return response;
    },
    error => {
        if (error.response) {
            switch (error.response.status) {
                case 401:
                    // 返回 401 清除token信息并跳转到登录页面
                    window.localStorage.setItem('token', null)
                    router.replace({
                        path: '/',
                        query: {redirect: router.currentRoute.fullPath}
                    })
            }
        }
        return Promise.reject(error.response.data)   //返回接口返回的错误信息
    }
);
export default axios

main.js中:

import Axios from './http'
Vue.prototype.$axios = Axios;
axios.defaults.baseURL = 'http://localhost:8080/blog/';
new Vue({
  el: '#app',
  axios,
  router,
  components: { App },
  template: '<App/>'
})

到这里我们已经在前端做了路由跳转拦截和后台请求拦截,因为不懂前端,所以感觉上安全性应该能得到保障,但到底真的安不安全还真不知道,以后会慢慢多了解一下。

4.富文本模块

4.1 wangeditor因为这个比较简单,功能一般,所以我放在评论和留言里用):

<div id="editorMneuElem"></div>
<div id="editorElem" style="text-align:left;height: 150px;"></div>
mounted: function() {
  //wangEditor配置,根据自己情况配置,图片也可以使用相对路径或cdn方式
  this.editor = new E('#editorMneuElem','#editorElem')
  this.editor.customConfig.menus = ['head','foreColor','emoticon','code'];
  this.editor.customConfig.colors = ['#FF0000','#0000FF','#00FF00','#FF6EB4','#FFA500','#A020F0','#00FF7F'];
  this.editor.customConfig.onchange = (html) => {this.editorContent = html}
  var icon = new Array();
  var def = new Array();
  for(var i=1;i<=20;i++){
    icon[i-1] = {src:'http://xxx.com/images/emoji/'+(i-1)+'.gif'};
  }
  for(var i=1;i<=10;i++){
    def[i-1] = {src:'http://xxx.com/images/huaji/'+(i-1)+'.png'};
  }
  this.editor.customConfig.emotions = [
    {title: "默认",type: "image",content: icon},
    {title: "滑稽",type: "image",content: def}
  ];
  this.editor.create();
}

4.2 tinymce这个功能比较强大,可以直接从word拖拽图片或有格式的文档,所以我放在添加文章里使用,界面也比较漂亮,可惜官方文档是英文):

这里可以参考网上的大神的配置,贴一下我填完坑的代码(红色部分是上传图片后回显在编辑器里的图片路径,这个使用相对路径时很容易错,推荐使用绝对路径的cdn,曾经搞这个tinymce的图片上传堵了我好久):

<template>
  <div><textarea :id= "id"></textarea></div>
</template>
<script>
import tinymce from "../../static/tinymce/tinymce.min.js";
import "../../static/tinymce/themes/modern/theme.min.js";
import "../../static/tinymce/plugins/autosave/plugin.min.js";
import "../../static/tinymce/plugins/colorpicker/plugin.min.js";
import "../../static/tinymce/plugins/codesample/plugin.min.js";
import "../../static/tinymce/plugins/contextmenu/plugin.min.js";
import "../../static/tinymce/plugins/emoticons/plugin.js";
import "../../static/tinymce/plugins/insertdatetime/plugin.min.js";
import "../../static/tinymce/plugins/image/plugin.min.js";
import "../../static/tinymce/plugins/imagetools/plugin.min.js";
import "../../static/tinymce/plugins/lists/plugin.min.js";
import "../../static/tinymce/plugins/link/plugin.min.js";
import "../../static/tinymce/plugins/paste/plugin.min.js";
import "../../static/tinymce/plugins/fullpage/plugin.min.js";
import "../../static/tinymce/plugins/fullscreen/plugin.min.js";
import "../../static/tinymce/plugins/preview/plugin.min.js";
import "../../static/tinymce/plugins/media/plugin.min.js";
import "../../static/tinymce/plugins/table/plugin.min.js";
import "../../static/tinymce/plugins/textcolor/plugin.min.js";
import "../../static/tinymce/plugins/textpattern/plugin.min.js";
import "../../static/tinymce/plugins/wordcount/plugin.min.js";
import "../../static/tinymce/plugins/toc/plugin.min.js";

import "../../static/tinymce/langs/zh_CN.js";

const INIT = 0;
const CHANGED = 2;
var EDITOR = null;

export default {
  data() {
    return {status: INIT,  id: "editor-" + new Date().getMilliseconds()};
  },
  props: {
    value: {default: "",type: String},
    url: {default: "http:",type: String},
    accept: {default: "image/jpg, image/jpeg, image/png, image/gif",type: String},
    maxSize: {default: 2097152,type: Number}
  },
  watch: {
    value: function(val) {
      if (this.status === INIT || tinymce.activeEditor.getContent() !== val) {tinymce.activeEditor.setContent(val);}
      this.status = CHANGED;
    }
  },
  mounted: function() {
    console.log("editor");
    window.tinymce.baseURL = '/static/tinymce';
    const self = this;
    const setting = {
      selector: "#" + self.id,
      language: "zh_CN",
      language_url: "../../static/tinymce/langs/zh_CN.js",
      init_instance_callback: function(editor) {
        EDITOR = editor;
        console.log("Editor: " + editor.id + " is now initialized.");
        editor.on("input change undo redo", () => {var content = editor.getContent();self.$emit("input", content);});
      },
      plugins: [],
      images_upload_handler: function(blobInfo, success, failure) {
        if (blobInfo.blob().size > self.maxSize) {failure("文件体积过大");}
        if (self.accept.indexOf(blobInfo.blob().type) > 0) {uploadPic();} else {failure("图片格式错误");}
        function uploadPic() {
          const xhr = new XMLHttpRequest();
          const formData = new FormData();
          xhr.open("POST", self.url, true);
          xhr.withCredentials = true;//允许带认证信息的配置.解决跨域问题前端需要的配置
          formData.append("file", blobInfo.blob());
          xhr.send(formData);
          xhr.onload = function() {
            if (xhr.status !== 200) {self.$emit("on-upload-fail"); failure("上传失败: " + xhr.status); return;}// 抛出 'on-upload-fail' 钩子
            const json = JSON.parse(xhr.responseText);
            self.$emit("on-upload-complete", [json, success, failure]);// 抛出 'on-upload-complete' 钩子
            success(json.data.file.filePath);
          };

        }
      },
      setup: (editor) => {
        // 抛出 'on-ready' 事件钩子
        editor.on('init', () => {self.loading = false; self.$emit('on-ready'); editor.setContent(self.value);})
        // 抛出 'input' 事件钩子,同步value数据
        editor.on('input change undo redo', () => {self.$emit('input', editor.getContent())})
      },
      height: 500,
        theme: 'modern',
        menubar: true,
        toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough hr | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | media preview removeformat code  link fullscreen | undo redo | image emoticons codesample`,
        plugins: `paste importcss image code codesample table advlist fullscreen link media lists textcolor colorpicker hr preview emoticons`,
        codesample_languages: [
          {text: 'HTML/XML', value: 'markup'},{text: 'JavaScript', value: 'javascript'},{text: 'CSS', value: 'css'},{text: 'PHP', value: 'php'},{text: 'Python', value: 'python'},{text: 'Java', value: 'java'},{text: 'C', value: 'c'},{text: 'C++', value: 'cpp'}
        ],
        // codesample_content_css:"../../static/common/css/prism.css",
        // CONFIG
        forced_root_block: 'p',
        force_p_newlines: true,
        importcss_append: true,
        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
        content_style: `
          *                         { padding:0; margin:0; }
          html, body                { height:100%; }
          img                       { max-width:100%; display:block;height:auto; }
          a                         { text-decoration: none; }
          iframe                    { width: 100%; }
          p                         { line-height:1.6; margin: 0px; }
          table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
          .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
          ul,ol                     { list-style-position:inside; }
        `,
        insert_button_items: 'image link | inserttable',
        // CONFIG: Paste
        paste_retain_style_properties: 'all',
        paste_word_valid_elements: '*[*]',        // word需要它
        paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
        paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
        paste_webkit_styles: 'all',
        paste_merge_formats: true,
        nonbreaking_force_tab: false,
        paste_auto_cleanup_on_paste: false,
        // CONFIG: Font
        fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px 26px 28px 32px',
        // CONFIG: StyleSelect
        style_formats: [
          {title: '首行缩进',block: 'p',styles: { 'text-indent': '2em' }},
          {title: '行高',items: [{title: '1', styles: { 'line-height': '1' }, inline: 'span'},{title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},{title: '2', styles: { 'line-height': '2' }, inline: 'span'},{title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},{title: '3', styles: { 'line-height': '3' }, inline: 'span'}]}
        ],
        // FontSelect
        font_formats: `微软雅黑=微软雅黑;宋体=宋体;黑体=黑体;仿宋=仿宋;楷体=楷体;隶书=隶书;幼圆=幼圆;Andale Mono=andale mono,times;Arial=arial, helvetica,sans-serif;
          Arial Black=arial black, avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;
          Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;
          Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats`,
        // Tab
        tabfocus_elements: ':prev,:next',
        object_resizing: true,
        // Image
        imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
    };
    tinymce.init(setting);
  },
  beforeDestroy: function() {
    tinymce.get(this.id).destroy();
  }
};
</script>

5.点击特效

这个也是搬运的,JS博大精深,研究了一下,其实也就是使用了一下animate.css的消退特效,main.js中:

/* 鼠标点击特效 -start*/
var getColor = function() {
    var randomColor=['#0000FF','#FFA500','#FF0000','#A020F0','#00F5FF','#008B00','#FF6A6A','#FF00FF','#00FF00','#FFB90F'];var colorIndex = parseInt(Math.random()*10);return randomColor[colorIndex];};
    var fnTextPopup = function(arr, options) {if (!arr || !arr.length) {return;}var index = 0;document.documentElement.addEventListener("click", function(event) {var x = event.pageX,y = event.pageY;
    var eleText = document.createElement("span");eleText.className = "text-popup";this.appendChild(eleText);if (arr[index]) {eleText.innerHTML = arr[index];} else {index = 0;eleText.innerHTML = arr[0];}
    eleText.addEventListener("animationend", function() {eleText.parentNode.removeChild(eleText);});eleText.style.left = x - eleText.clientWidth / 2 + "px";eleText.style.top = y - eleText.clientHeight + "px";
    index++;var textPopupElement = document.getElementsByClassName("text-popup")[0];textPopupElement.style.color = getColor();});
};
fnTextPopup(["富强","民主","文明","和谐","自由","平等","公正","法治","爱国","敬业","诚信","友善"]);

/* css */
.text-popup {
    animation: textPopup 800ms;
    user-select: none;
    white-space: nowrap;
    position: absolute;
    z-index: 999;
    font-size: 24px;
}
@keyframes textPopup {
    0%,
    100% {
        opacity: 0;
    }
    5% {
        opacity: 1;
    }
    100% {
        transform: translateY(-50px);
    }
}
/* 鼠标点击特效 -end*/

二、项目优化

由于刚做完项目,然后上线后,第一次打开首页用了18s+,吓到我了,然后花了很长时间在寻找页面优化的方法,最后效果也是刚刚的:

1.静态资源尽量使用CDN

比如index.html中引入 <script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.min.js"></script> 替换掉使用 npm install vue 这种方式

2.VUE懒加载

在上面介绍路由中已详细讲解了,贴张使用全部加载和按需加载的张图,可以看到上面的这种方式举个栗子:打开首页只会加载0.xxx.js,当打开第二个页面再加载1.xxx.js,当代码量和引用资源比较多时,可以极大减轻首页的加载压力

3.开启GZIP打包

config/index.js的官方注释里说了开启gzip前要先安装 compression-webpack-plugin

所以先运行:npm install --save-dev compression-webpack-plugin

再在index.js中设置 productionGzip: true

4.JSCSS压缩成min

一般vue打包的js自带压缩功能,如果你像我一样把css都提取到common.css中,你可以使用网上的css在线压缩工具,放到common.min.css中做生产环境的css

5.抽取出VUE中的重复代码作公共模块

这个自己视情况而定。

6.还有很多优化的方法可以百度一下

貌似其它都是些无关紧要的东西,也没什么说的了,就这样吧!下篇开始搭建后台项目,最近心情很差,也不想说什么话,只想一个人安静的听着歌,做着自己的事。

猜你喜欢

转载自www.cnblogs.com/songzhen/p/blogs-diary-3.html