• 个人博客网站(三)


    简介:

    前端选用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-100%; display:block;height:auto; }
              a                         { text-decoration: none; }
              iframe                    {  100%; }
              p                         { line-height:1.6; margin: 0px; }
              table                     { word-wrap:break-word; word-break:break-all; max-100%; border:none; border-color:#999; }
              .mce-object-iframe        { 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.还有很多优化的方法可以百度一下

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

  • 相关阅读:
    [状压DP][二分]JZOJ 3521 道路覆盖
    字符串操作
    练习: 判断一个数是否为小数
    Python 深浅拷贝
    编码
    python中的 == 和 is 的区别
    Python3 字典的增删改查
    Python3 列表的基本操作
    初识 Python
    方法的入门
  • 原文地址:https://www.cnblogs.com/songzhen/p/my-blog-wesite-record-3.html
Copyright © 2020-2023  润新知