hyangteng

hyangteng 查看完整档案

成都编辑西南科技大学  |  园艺 编辑东信北邮技术有限公司  |  前端开发 编辑 hyangteng.com 编辑
编辑

stay foolish,stay hungry

个人动态

hyangteng 关注了用户 · 1月3日

Arcthur @arcthur

现任职于阿里巴巴集团,花名流形
现任阿里巴巴大数据对外产品前端团队负责人
专注在React、Node等领域

关注 90

hyangteng 赞了文章 · 2019-12-29

在Vue项目中使用Eslint+Prettier+Stylelint

准备工作

首先搭建vue项目,lint选择ESLint + Prettier,配置方式选择In dedicated config files。具体搭建过程这里就不赘述了,如果不熟悉的同学可以点击这里

配置Eslint

项目搭建完成后,根目录下会自动生成一个.eslintrc.js文件,我们直接来看默认的配置:

module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: ["plugin:vue/essential", "@vue/prettier"],
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
  },
  parserOptions: {
    parser: "@typescript-eslint/parser"
  }
};

这里extends是一个数组,数组第一个成员"plugin:vue/essential"表示的是:引入eslint-plugin-vue插件,并开启essential类别中的一系列规则。

eslint-plugin-vue把所有规则分为四个类别,依次为:base, essential, strongly-recommended, recommended,后面的每个类别都是对前一个类别的拓展。除了这四个类别外,还有两个未归类的规则,所有的类别及规则都可以在这里查看

这里默认启用的是essential类别里面的规则,我们也可以使用"plugin:vue/strongly-recommended""plugin:vue/recommend" 启用更多的规则,如果仅仅想启用strongly-recommendedrecommend里面的部分规则,可以在.eslintrc.js文件的rules选项中配置。

eslint 补充知识

extends的属性值可以是:

  • 指定配置的字符串(配置文件的路径、可共享配置的名称、eslint:recommendedeslint:all
  • 字符串数组:每个配置继承它前面的配置

可选的配置项如下:

  • eslint:recommended 启用一些列核心规则
  • 可共享的配置(比如,eslint-config-standard),这是一个npm包,属性值可以省略包名的前缀eslint-config-
  • 插件,是一个 npm 包,属性值可以省略包名的前缀eslint-plugin-,属性值为,plugin:包名/配置名称
  • 指向一个配置文件的相对路径或绝对路径
  • eslint:all 启用当前安装的eslint版本中所有核心规则,不推荐使用

plugins的属性值是一个字符串列表:

  • 在使用插件之前,你必须安装它
  • 插件名称可以省略eslint-plugin-前缀

eslint规则文档中,带扳手图标的规则就是eslint能够自动修复的规则。而不带该图标的规则,eslint则只能给出错误或警告,需要开发者手动修复。

配置Prettier

我们搭建项目时已经选择了Prettier,所以这里可以不用再做额外的配置。如果想改变Prettier的默认配置,只需要在根目录下新建一个.prettierrc.js文件,在里面修改配置就可以了。

如果搭建项目时没有选用Prettier,需要我们自己执行以下操作:

1,安装 prettier

yarn add --dev prettier eslint-config-prettier eslint-plugin-prettier

2,修改.eslintrc.js

  extends: [
    // ...other extends,
    "prettier"
  ], 
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "error"
  }

  extends: [
    // ...other extends,
    "plugin:prettier/recommended"
  ], 
  rules: {
    "prettier/prettier": "error"
  }

如果用eslint-config-prettier启用Prettier,建议不要使用"plugin:vue/strongly-recommended""plugin:vue/recommend",因为这两个类别中有部分规则与Prettier冲突。

所以更推荐的做法是安装 @vue/eslint-config-prettier eslint-plugin-prettier,然后修改.eslintrc.js

extends: [
  // ...other extends,
  "@vue/prettier"
],

prettier 补充知识

  • eslint-config-prettier 关闭 Eslint 中与 Prettier 冲突的选项,只会关闭冲突的选项,不会启用Prettier的规则
  • eslint-plugin-prettier 启用 Prettier 的规则

配置 Stylelint

使用vue-cli搭建项目时,目前还没有stylelint选项,需要我们自己安装相关的 npm

1,安装

yarn add --dev stylelint stylelint-scss stylelint-config-standard-scss stylelint-config-prettier

2,根目录下新增 .stylelintrc.js文件 这里列出我自己的stylelint配置

module.exports = {
  extends: ["stylelint-config-standard-scss", "stylelint-config-prettier"],
  rules: {
    "declaration-colon-space-after": "always-single-line",
    "declaration-colon-space-before": "never",
    "declaration-block-trailing-semicolon": "always",
    "rule-empty-line-before": [
      "always",
      {
        ignore: ["after-comment", "first-nested"]
      }
    ]
  }
}

stylelint把所有规则分为三个类别:

  • Possible errors: 可以使用stylelint-config-recommended启用这些规则
  • Stylistic issuesstylelint-config-standard拓展了Possible errors,并启用此类的规则
  • Limit language features: 其他规则,如果有需要,可以在rules里面配置

stylelint的规则分类可以在这个页面查看

VSCode 保存时自动修复

1,打开VSCode, 安装 ESLint, Vertur, Prettier - Code formatter, stylelint-stzhang 这几个插件

2,settings.json 添加如下配置

  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "html",
    {
      "language": "vue",
      "autoFix": true
    },
    {
        "language": "typescript",
        "autoFix": true
    },
    {
        "language": "typescriptreact",
        "autoFix": true
    }
  ],
  "vetur.format.defaultFormatter.scss": "prettier",
  "vetur.format.defaultFormatter.css": "prettier",
  "vetur.format.defaultFormatter.js": "prettier-eslint",
  "vetur.format.defaultFormatter.html": "js-beautify-html",
  "stylelint.autoFix": true

效果演示

图片描述

查看原文

赞 10 收藏 4 评论 0

hyangteng 赞了文章 · 2019-06-21

【小技巧】CSS如何实现文字两端对齐效果?

需求如下,红框所在的文字有四个字的、三个字的、两个字的,如果不两端对齐可以选择居中对齐,或者右对齐。但是如果要像下面这样两端对齐呢?

我相信以前很多人都这么干过:两个字中间使用 来隔开达到四个字的宽度,三个字也可以,但是,像上图中“122账号”“122密码”这样的,就不好计算该用几个空格了。

假如我们有如下HTML:

<div>这世间唯有梦想和好姑娘不可辜负!</div>

给它加点样式

div{
  width:500px;
  border:1px solid red;
  text-align: justify;
}

初始效果是这样的

text-align: justify这是什么东西?CSS2中text-align有一个属性值为justify,为对齐之意。其实现的效果就是可以让一行文字两端对齐显示(文字内容要超过一行)。

但是光使用它依然没什么卵用…..

要使文字两端对齐,我们还得使用一个行内空标签来助阵,比如<span><i>等等,这里是我用<i>标签

<div>这世间唯有梦想和好姑娘不可辜负!<i></i></div>

给这个i标签设置如下样式

div i{
  display:inline-block;
  /*padding-left: 100%;*/
  width:100%;
}

padding-left: 100%width:100%都可以达到效果,选用其一即可。效果如下

但是加入HTML元素又违反了结构表现分离的原则,我们可以改用after、before伪元素:

div:after {
    content: " ";
    display: inline-block;
    width: 100%;
}

感谢 @依韵_宵音 的提醒

全文完。


关注公众号,第一时间接收最新文章。如果对你有一点点帮助,可以点喜欢点赞点收藏,还可以小额打赏作者,以鼓励作者写出更多更好的文章。
关注公众号

查看原文

赞 40 收藏 168 评论 28

hyangteng 发布了文章 · 2019-03-22

vue开发项目完全指南

这篇文章总结了vue项目的所遇到的问题,包括跨域、用户认证、接口统一管理、路由配置、兼容性处理,性能优化等内容。

项目github地址 :

image-20190318090248419

一、环境依赖安装

1. node环境

1.1 node和npm环境的安装

根据以下教程安装,然后设置好环境变量

http://www.runoob.com/nodejs/...

视频教程 http://101.110.118.22/github....

centos如果装不上看这里:https://www.rosehosting.com/b...

1.2 为npm更改源

npm默认使用的源的服务器在国外下载速度慢,所以需要更换源

以下两种方法任选一种

1.2.1使用cnpm代替npm

参考链接:https://npm.taobao.org/
# 安装
npm install -g cnpm --registry=https://registry.npm.taobao.org

#安装完cnpm,之后再按照依赖就要使用cnpm
cnpm install [包名]

1.2.2为npm更换源

参考链接 https://segmentfault.com/a/11...

修改源为淘宝的源

npm config set registry http://registry.npm.taobao.org/

我们在发布自己包的时候需要将官方的源改回来

npm config set registry https://registry.npmjs.org/

1.3 管理(更新)nodejs的版本

切换nodejs版本有两种方式,分别是nvmn,n更简单推荐使用

使用n管理nodejs版本

参考链接 https://www.jianshu.com/p/c64...

官网 https://github.com/tj/n

#安装
npm install -g n

#使用n下载所需node版本
n 版本号
#下载最新版本
n latest
# 切换版本
输入 n,
然后选中所需版本
#以指定的版本来执行版本
n use 7.4.0 index.js

linux使用n安装新版本nodejs之后,如果node -v还是原来的版本,那么就需要改变一下环境变量

vim .bash_profile

export NODE_HOME=/usr/local     #NODE_HOME改成新版本nodejs安装的目录,如果找不到,find / -name node
export PATH=$NODE_HOME/bin:$PATH
export NODE_PATH=$NODE_HOME/lib/node_modules:$PATH

修改环境变量参考:https://blog.csdn.net/yi412/a...

1.4 package.json文件详解

参考文档 http://javascript.ruanyifeng....

2. vue脚手架

vue-cli目前已经更新到3版本,vue-cli3把webpack相关的配置隐藏起来了,所有的配置都在vue.config.js文件夹中,所以使用vue-cli3需要的webpack水平较高,建议使用vue-cli2

3.1 vue-cli2.x安装

参考链接:https://github.com/vuejs/vue-...

安装:

npm install -g vue-cli

用法:

$ vue init < template-name >  < project-name >

例:

$ vue init webpack my-project

目前可用的模块包括:

  • webpack - 一个功能齐全的Webpack + vue-loader设置,具有热重载,linting,测试和css提取功能。
  • webpack-simple - 一个简单的Webpack + vue-loader设置,用于快速原型设计。
  • browserify -全功能Browserify + vueify设置用热重装载,linting&单元测试。
  • browserify -simple - 一个简单的Browserify + vueify设置,用于快速原型设计。
  • pwa - 基于webpack模板的vue-cli的PWA模板
  • simple - 单个HTML文件中最简单的Vue设置

3.2 vue-cli3.x安装及配置(仅供参考)

vue-cli3x的官方文档:https://cli.vuejs.org/

Vue-cli3 中vue.config.js文件配置参考文档:https://cli.vuejs.org/zh/conf...

Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli(1.x 或 2.x),你需要先通过 npm uninstall vue-cli -gyarn global remove vue-cli 卸载它。

安装

npm install -g @vue/cli

安装了vue-cli3如果还想使用vue-cli2的init功能,需要安装一个桥接功能

npm install -g @vue/cli-init
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档
module.exports = {
  // 部署生产环境和开发环境下的URL。
  // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
  //例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置 baseUrl 为 /my-app/。
  baseUrl: process.env.NODE_ENV === "production" ? "./" : "/",
 
  // outputDir: 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)
  outputDir: "dist",
  //用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
  assetsDir: "assets",
  //指定生成的 index.html 的输出路径  (打包之后,改变系统默认的index.html的文件名)
  // indexPath: "myIndex.html",
  //默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存。你可以通过将这个选项设为 false 来关闭文件名哈希。(false的时候就是让原来的文件名不改变)
  filenameHashing: false,
 
  //   lintOnSave:{ type:Boolean default:true } 问你是否使用eslint
  `lintOnSave`: true,
  //如果你想要在生产构建时禁用 eslint-loader,你可以用如下配置
  // lintOnSave: process.env.NODE_ENV !== 'production',
 
  //是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后你就可以在 Vue 组件中使用 template 选项了,但是这会让你的应用额外增加 10kb 左右。(默认false)
  // runtimeCompiler: false,
 
  /**
   * 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
   *  打包之后发现map文件过大,项目文件体积很大,设置为false就可以不输出map文件
   *  map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。
   *  有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
   * */
  productionSourceMap: false,
 
  // 它支持webPack-dev-server的所有选项
  devServer: {
    host: "localhost",
    port: 1111, // 端口号
    https: false, // https:{type:Boolean}
    open: true, //配置自动启动浏览器
    // proxy: 'http://localhost:4000' // 配置跨域处理,只有一个代理
 
    // 配置多个代理
    proxy: {
      "/api": {
        target: "<url>",
        ws: true,
        changeOrigin: true
      },
      "/foo": {
        target: "<other_url>"
      }
    }
  }
};

二、开发

以下内容依赖环境为 : vue-cli 版本2.9.x

项目github地址 :

安装完以上依赖后,就可以开始一个项目了,我们先看下后端api的定义

前后端交互报文定义以及数据api接口

前后端交互报文定义

请求

http request header{ //除登录注册以外的请求,发起请求时要在请求头中加入token
    authorization:jwt
}
http request body{
    
}

返回

http response header{
    
}
http response body{
    code:业务处理状态码
    msg:业务处理描述
    token:jwt token
    data:业务数据
}

项目中使用的后台api定义如下

注:服务器端的host为118.24.85.97,端口为22222

1.测试api是否可用

  1. uri: http://118.24.85.97:22222/api
  2. 描述:测试接口是否能用,能用的话返回 'API WORDS'字符串
  3. 请求类型 GET
  4. 请求参数 无
  5. 返回值 {'Api Works'}

2.注册

  1. uri: http://118.24.85.97:22222/api/users/reg
  2. 描述:注册
  3. 请求类型 POST
  4. 请求参数
序号参数名是否必填描述
1namey用户名
2passy密码
  1. 返回参数 不重要

3.登录

  1. uri: http://118.24.85.97:22222/api/users/login
  2. 描述:登录
  3. 请求类型 POST
  4. 请求参数
序号参数名是否必填描述
1namey用户名
2passy密码
  1. 返回参数
序号参数名描述
1msgok
2token用于验证用户身份的token

4.获取当前用户信息

  1. uri: http://118.24.85.97:22222/api/users/current
  2. 描述:获取用户信息
  3. 请求类型 GET
  4. 请求参数 无
  5. 返回参数
序号参数名描述
1id用户id
2token用于验证用户身份的token

0.初始化项目

在终端中输入

vue init webpack vue2_template

然后会有一些选项让你选,按照项目需求选择,例如我不需要eslint,unit test,就可以选No,现在选no将来如果需要的话也可以自己安装

image-20190301151747514

安装完成之后,按照提示切换到相应目录,执行相应指令,然后在浏览器打开网址,这样一个简单的vue项目就启动起来了

image-20190301152115255

1. 项目文件介绍

整个文件介绍:

image-20190301153205422

注意:

  1. 开发主要使用src文件夹
  2. webpack的配置文件配置文件详解看这里:https://segmentfault.com/a/11...
  3. package.json配置详解 http://javascript.ruanyifeng....

src目录介绍

首先在src目录下新建一个文件夹views,用来放我们的主要页面,然后在assets文件夹中建立fonts styles imgs,用来存放相应的资源,建完之后,文件夹如下

image-20190301155249665

2. 跨域、axios配置与api管理

在这个项目中,我们使用axios进行数据请求

axios中文文档: https://www.kancloud.cn/yunye...
# 安装axios
npm/cnpm i axios -S      # -S 指安装到package.json中的dependencies中

安装完成后,我们要在main.js中引入,然后测试一下是否成功引入

//main.js文件
import axios from 'axios'

axios.get('https://api.github.com/users?since=10')   //使用github接口做一下测试
  .then(res=>console.log(res))
  .catch(err=>console.log(err))

浏览器显示以下信息,说明引入成功image-20190301160510216

github提供的接口配置了cors,所以我们能够能够在浏览器正常访问到,但cors兼容性最低到ie10,而且后台不一定会配置cors,所以在开发时我们需要配置一下跨域

参考链接:

  1. cors详解 http://www.ruanyifeng.com/blo...

2.1配置跨域

参考文档:https://segmentfault.com/a/11...

先找个没有设置cors的api使用axios访问一下

axios.get('http://118.24.85.97:22222/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

浏览器会因为同源策略报错

image-20190307094529285

下面进行跨域的配置

配置目录 config/index.js 13行
proxyTable: {
  '/apis':{
    target:'http://118.24.85.97:22222',//后台地址 proxyTable  把/apis映射成target 即 /apis=http://118.24.85.97:22222
    changeOrigin:true,//是否跨域
    pathRewrite:{
      '^/apis':''
    }
  }

}

再进行访问数据时就要在接口前面加上/apis(/apis就相当于http://118.24.85.97:22222)

axios.get('/apis/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

然后就发现浏览器访问成功了

image-20190307095002857

proxyTable原理:跨域是浏览器禁止的,服务端并不禁止跨域 ,所以浏览器可以发给自己的服务端然后,由自己的服务端再转发给要跨域的服务端,做一层代理。proxyTable使用的是http-proxy-middleware中间件,内部用的是http-proxy

以上配置的跨域是开发环境下的,在生产环境就自动失效了,而且这样配置我们开发时访问接口时,都要写成/apis/xxx/xxx格式,在部署到服务器中时,我们要把/apis拿掉,才能访问到正确的url。有两种方法,一种是在开发环境中设置(通过axios的baseURL),另一种是在服务器上修改nginx的配置设置。

2.2生产环境去除/apis前缀

在这里详细说下第一种方式,原理是这样的:

通过检测是开发环境和生产环境,设置不同的baseURL,使生产环境和开发环境都能正确访问url

在src目录下新建一个apis目录,然后在apis目录下新建一个api.config.js文件

//判断是否是生产环境
//webpack在开发环境和生产环境分别执行不同的js文件,process.env.NODE_ENV设置了不同的值,process.env.NODE_ENV在生产环境中值为'production'(这个值是在build/build.js中第4行设置的)
var isPro = process.env.NODE_ENV=== 'production'
// 如果是生产环境 我们就使用服务器的uri,如果是开发环境,我们就添加/apis前缀
module.exports = {
    baseUrl: isPro ? 'http://118.24.85.97:22222' : '/apis'
}

在main.js中引入这个文件,然后设置axios的baseURL

//引入api.config.js文件,然后设置axios的baseURL
import apiConfig from './apis/api.config'
axios.defaults.baseURL=apiConfig.baseUrl

再来测试一下不加/apis的接口

axios.get('/api')
.then(res=>console.log(res))
.catch(err=>console.log(err))

浏览器显示是ok的。这样我们以后使用axios访问接口就可以不加/apis了,打包后访问也不用手动去除/apis

2.3 api统一管理

在vue项目开发过程中,会涉及到很多接口的处理,当项目足够大时,就需要统一管理接口。

具体方法应该挺多的,这里只介绍一种:使用axios+async/await进行接口的统一管理

一般来说,后台的接口是分模块的,例如我们后台的测试接口

  • 身份认证 /api/login /api/reg
  • 用户信息 /v1/api/user

我们首先在src目录下新建一个apis文件夹,后台提供的所有接口都在这里定义

第二步,按照后台提供的模块新建js文件,我们新建user.jsauth.js

第三步,引入axios,做相应的配置

在apis目录下新建一个http.js,在里面做axios相应的配置

  1. 我们上文中是在main.js文件引入的axios,设置的baseURL,以上代码可以去除,改为在http.js中引入
  2. 我们做的主要是:引入axios,创建一个axios的实例(实例的功能和axios一样)
import axios from 'axios'
import apiConfig from './api.config'
//创建axios的一个实例
var instance = axios.create({
    baseURL:apiConfig.baseUrl,
    timeout: 6000
})


//------------------- 一、请求拦截器 后面介绍
instance.interceptors.request.use(function (config) {

    return config;
}, function (error) {
    // 对请求错误做些什么
    
    return Promise.reject(error);
});

//----------------- 二、响应拦截器 后面介绍
instance.interceptors.response.use(function (response) {
    
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

/**
 * 使用es6的export default导出了一个函数,导出的函数代替axios去帮我们请求数据,
 * 函数的参数及返回值如下:
 * @param {String} method  请求的方法:get、post、delete、put
 * @param {String} url     请求的url:
 * @param {Object} data    请求的参数
 * @returns {Promise}     返回一个promise对象,其实就相当于axios请求数据的返回值
 */
export default function (method, url, data = null) {
    method = method.toLowerCase();
    if (method == 'post') {
        return instance.post(url, data)
    } else if (method == 'get') {
        return instance.get(url, { params: data })
    } else if (method == 'delete') {
        return instance.delete(url, { params: data })
    }else if(method == 'put'){
        return instance.put(url,data)
    }else{
        console.error('未知的method'+method)
        return false
    }
}

第四步,在apis/xxx.js文件中引入http.js导出的函数,拿其中一个文件auth.js说明

//auth.js 用于定义用户的登录、注册、注销等

import req from './http.js'

//定义接口

//在这里定义了一个登陆的接口,把登陆的接口暴露出去给组件使用
export const LOGIN =params=>req('post','/api/users/login',params)
//这里使用了箭头函数,转换一下写法:
// export const LOGIN=function(params){
//   return req('post','/api/login',params)
// }

//定义注册接口
export const REG =params=>req('post','/api/users/reg',params)


最后一步,在需要用的该api的组件中引入并调用,我们在App.vue文件中测试下

<template>
  <div>
    <h2>登录</h2>
    用户名<input type="text" v-model="user">
    密码<input type="password" v-model="pass">
    <input type="button" @click="reg" value="注册">
    <input type="button" @click="login" value="登录">
  </div>
</template>
<script>
import {LOGIN,REG} from '../../apis/auth.js'
export default {
  data() {
    return {
      user:'',
      pass:'',
      err:[]
    }
  },
  methods: {
    async reg(){
      try {
        const data = await REG({ name: this.user,pass: this.pass })
        console.log(data)
        alert(JSON.stringify(data))
        this.cleanForm()


      } catch (error) {
        console.log(error)
      }

    },
    async login(){
      try {
        const data = await LOGIN({ name: this.user,pass: this.pass })
        alert(JSON.stringify(data))
        this.cleanForm()
      } catch (error) {
        console.log(error)
      }
    },
    cleanForm(){
      this.user=''
      this.pass=''
    }
  },

}
</script>

注:如果要打开Login.vue,需要配置对应的路由

上面的代码引入了auth.js定义的api,并在对应的方法中使用。代码中用到了async/await,其实很简单,可以假设async是个标识,说明这个函数中有异步请求,await翻译为'等',后面接一个异步请求,等后面的异步请求执行完成之后,会把结果赋给=左边的值

参考链接 http://www.runoob.com/w3cnote...

总结一下,像上面那样定义接口虽然麻烦点,但有两个好处:

  1. 代码看起来规范,所有的接口都在一个文件夹定义,不用分散的各个组件,维护起来简单,例如后台的一些url变了,改起来也方便
  2. 可以做到接口一次定义,到处使用

3. 路由配置

Vue Router官方文档 https://router.vuejs.org/zh/

前端路由原理:https://segmentfault.com/a/11...

3.1 最简配置

路由的配置文件在router/index.js文件中

先引入文件,再进行配置

首先在views目录中新建以下页面,主页(Home/Home.vue),登录页(Login/Login.vue),测试页(Test/Test.vue)

然后配置下路由

import Vue from 'vue'
import Router from 'vue-router'
//@表示 src目录 webpack的配置在webpack.base.conf.js第29行 alias{'@':resolve('src')}
import Home from '@/views/Home/Home.vue'
import Login from '@/views/Login/Login.vue'
import Test from '@/views/Test/Test.vue'

Vue.use(Router)

export default new Router({
  routes: [//路由规则
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path:'/login',
      name:'Login',
      component:Login
    },
    {
      path:'/test',
      name:'Test',
      component:Test
    }
  ]
})

路由规则在routes中进行配置,routes是一个数组,接受一系列路由规则,每个路由规则是一个对象,包括路径、路由名字,和路径匹配的组件,建议给每个路由加个名字,在后面可能会用到。

打开浏览器,输入相应的url查看配置的路由是否正确,不正确的话检查下自己的配置

3.2配置路由懒加载

参考文档:

路由懒加载官方文档:https://router.vuejs.org/zh/g...

webpack之mainfest解读:https://github.com/younth/blo...

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。所以,懒加载的含义是当路由被访问时再去加载对应的js代码。

首先,不做路由懒加载的情况下,我们打包一下(切换到项目目录,执行npm run build),然后会发现项目下生产了3个js文件
image-20190304110812440
简单介绍一下作用:

  1. vendor.js 第三方库,一般是 node_modules里面的依赖进行打包 体积最大
  2. app.js 入口js打包的结果,即我们编写的所有代码都会打包进去
  3. manifest.js 主要是一些异步加载的实现方法(通过建立script方式动态引入js),内容上包含异步js的文件名和路径。

然后我们实现一下路由懒加载 @/router/router.js

import Vue from 'vue'
import Router from 'vue-router'
// import Home from '@/views/Home/Home.vue'
// import Login from '@/views/Login/Login.vue'
// import Test from '@/views/Test/Test.vue'
// 懒加载方式
const Home=()=>import('@/views/Home/Home.vue')
const Login=()=>import('@/views/Login/Login.vue')
const Test=()=>import('@/views/Test/Test.vue')
Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path:'/login',
      name:'Login',
      component:Login
    },
    {
      path:'/test',
      name:'Test',
      component:Test
    }
  ]
})

懒加载只是改变了一下组件的引用方式,由原来的直接引入变成异步引入,当我们访问对应的路由path时,才会加载相应的路由组件。

配置完成后再执行一次打包,结果如下:
image-20190304112607087

我们会发现目录中多出来3个js文件,并且app.js文件变小了。这说明配置了懒加载之后,app.js中其他组件的内容被抽离出来,分配到各自的js文件中。配置懒加载之后,刚开始打开页面只会加载app.js文件,只有在用户点击相应路由时,才会加载对应的js代码。当我们的业务代码非常多时,懒加载是个很好的选择。

3.3 配置history模式

官方文档:https://router.vuejs.org/zh/g...

配置history模式有两个原因,一是因为hash模式看很丑,二是因为预加载要用到History模式,配置非常简单,只需要配置属性mode的值为'history'

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

不过这种方式需要后台的支持,当匹配不到url时,返回url/index.html页面

nginx配置如下

location / {
  try_files $uri /index.html;
}

4. 权限管理

参考链接:

json web token入门教程 http://www.ruanyifeng.com/blo...

jwt官网 https://jwt.io/

4.1 token验证

我们通过jwt进行用户认证,jwt的原理是:服务器认证以后,生成一个json对象,发回给用户.

{
    "id":"001",
    "姓名":"小明",
    "角色":"管理员",
    "到期时间":"2019年3月3日12时30分"
}

以后用户与服务端通信的时候,都要发回这个json对象。服务器完全靠这个对象认定用户身份(一般是通过这个对象的中id去数据库请求数据)。为了防止用户篡改数据,服务器会在生成这个对象的时候,加上签名。就像这种形式:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

关于JWT保存更新的业务流程如下:

  1. 保存:登录后保存token
  2. 添加:每次发送请求之前检查token是否存在,存在,添加到请求头中,发送请求
  3. 更新:每次发送请求服务器返回数据之后更新token

主要逻辑包括:

  1. 登录之后,在localStorage中保存token
  2. 每次发送请求之前,使用axios请求拦截器将token放到请求头中
  3. 每次发送请求服务器返回数据之后在axios的响应拦截器中更新token
//1.登录之后保存token login.vue
async login(){
    const data = await LOGIN({ name: this.user,pass: this.pass })
    //保存token
    localStorage.setItem('token',data.token)
    //查看是否保存成功
    console.log(localStorage.getItem('token'))
}
//每次发送请求之前,讲token放到请求头中 api/http.js
//---使用axios的请求拦截器,每次发送请求之前拦截一下
instance.interceptors.request.use(function (config) {
    // 给头添加token
    if (localStorage.getItem('token')){//存在token,加入头
        config.headers.authorization=localStorage.getItem('token')
    }
    return config;
}, function (error) {
    // 对请求错误做些什么

    return Promise.reject(error);
});
//完成之后,记得发送一个请求,看看是否正确添加token

//---响应拦截器,服务器响应后先到达这里
instance.interceptors.response.use(function (response) {
    if(response.data.code=='2000'){//成功响应,更新token
      if(response.data.token){
        localStorage.setItem('token',response.data.token)
      }
    }else{
        //错误处理 根据不同的状态码,进行错误处理  
    }
    return response.data;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

4.2 对页面的访问权限

除了对token的操作,我们还要判断用户有没有权限访问这个页面(有些页面是用户必须登录才能访问的),具体配置要使用Vue Router的导航守卫

参考链接:https://router.vuejs.org/zh/g...

在全局前置守卫中进行验证


//在router/index.js进行配置
//在每次进行路由跳转之前进行
router.beforeEach((to,from,next)=>{//增加登录验证
  const isLogin=localStorage.getItem('token')?true:false;
  if(to.path=='/login'){//如果是登录页面,不需要token
    next();
  }else{//如果不是登录页面就要判断是否登录
    isLogin?next():next('/login');
  }

})

5. 将界面交给第三方UI库

iview官网:https://www.iviewui.com/

为节省开发时间,我们往往会使用一些第三方ui库,比如iview elementui等

我们在这里只介绍iview,其他ui库大同小异

iview的安装与引入

安装

cnpm i iview --save

按需引入组件

官网说,需要下载插件才能按需引入,官网说明,但是不下好像也可以正常引入

//在main.js文件中引入项目需要的组件
import {Button,Table,Message} from 'iview'
//然后注册组件
Vue.component('Button',Button)
Vue.component('Table',Table)
Vue.component('Message',Message)

这样注册的话太繁琐,所以需要优化一下

//main.js
import {Button,Table,Message} from 'iview'
const iviewComs={Button,Table,Message}
Object.keys(iviewComs).forEach(key=>{Vue.component(key,component[key])})

代码都写在main.js中显得太拥挤,我们可以把代码拿出去,写成一个插件

我们在components文件夹中新建一个文件iview-coms,用来放iview中引入的组件

//components/iview-coms.js  

import {Button,Table,Message} from 'iview'
const components={Button,Table,Message}
const install = function(Vue, opts = {}){
  Object.keys(components).forEach(key=>{
    Vue.component(key,components[key])
  })
}

export default install

然后在main.js中引入,use这个插件

import iviewComs from './components/iview-coms'
Vue.use(iviewComs)

ok了,接下来看自定义主题

自定义主题

官网链接:https://www.iviewui.com/docs/...

原理很简单,就是把ivew的less文件引入,并且覆盖掉,然后在main.js文件中引入自己的less文件

首先,我们需要下载解析less文件的loader ,lessless-loader,这里有个坑,下载less的时候要下载3版本以下的,不然会报一堆错误

cnpm i less@2.7.2 less-loader -D

下载完就ok了,不需要在webpack中进行配置,因为已经配置好了

然后,在assets/styles/base.less(没有需要自己新建)中,引入iview的样式文件,并且覆盖掉

默认变量列表:https://github.com/iview/ivie...

//assets/styles/base.less
//------ 引入iview样式
@import '~iview/src/styles/index.less';
//------ 覆盖iview的样式
@primary-color: #E91E63;
@error-color : #FF3300;

最后在main.js引入该less文件

//main.js
import './assets/styles/base.less'

此时,引入的组件就可以在.vue文件中使用了,看一下效果:

image-20190307150805499

ok了。最后还要补充一下,在项目开发过程中,不可避免的要覆盖iview默认的样式,我们分为两种情况,一种是全局覆盖,一种是局部覆盖。

全局覆盖的话我们要新建一个less文件,比如叫cover-iview.less所有覆盖iview样式的代码都放在这里,然后在base.less中引入这个文件。

局部覆盖的话要注意不要影响到别的样式,所以要充分利用less的作用域,例如我们只需要改home页面下的iview按钮样式,我们可以这样:

.home{
    .ivu-btn{
        
    }
}

6.开发中注意问题

6.1编写自己的工具库插件

参考文档:

vue插件说明:https://cn.vuejs.org/v2/guide...

项目中往往会使用一些通用的函数,比如获取当前时间、时间格式转化,防抖,节流等,我们可以把这个公用的部分封装成插件,在main.js中引入。

首先,在src目录下新建utils文件夹,在里面新建index.js,utils.js文件

我们在utils.js中编写自己的工具库,然后导出

class Utils{
    constructor(){
        this.d=new Date();//date对象
        this.instance=null;
    }
    static getInstance(){//单例模式
        if(!this.instance){
            this.instance = new Utils();
        }
        return this.instance;
    }

    pick(obj,arr){//pick({ a: 1, b: '2', 'c': 3 }, ['a', 'c'])  =>{a:1,c:3}
       return arr.reduce((acc,curr)=>{
            return (curr in obj && (acc[curr] = obj[curr]), acc)
        },{})
    }

    dateFormat(datetime,pattern=""){
        let vWeek = ["星期天","星期一","星期二","星期三","星期四","星期五","星期六"];
        let dt=new Date(datetime);
        let y=dt.getFullYear();
        let m=(dt.getMonth()+1).toString().padStart(2,'0');
        let d=dt.getDate().toString().padStart(2,'0');
        let hh=dt.getHours().toString().padStart(2,'0');
        let mm=dt.getMinutes().toString().padStart(2,'0');
        let ss=dt.getSeconds().toString().padStart(2,'0');
        let vWeek_s = dt.getDay();//星期
        if(pattern.toLowerCase() === 'yyyy-mm-dd'){
            return `${y}-${m}-${d}`
        }else if(pattern.toLowerCase() === 'mm-dd'){
            return `${m}-${d}`
        }else if(pattern.toLowerCase() === 'yyyymmddhhmmss'){
            return `${y}${m}${d}${hh}${mm}${ss}`
        }else {
            return `${y}-${m}-${d} ${hh}:${mm}:${ss} ${vWeek[vWeek_s]}`
        }

    }

}

const UTIL = Utils.getInstance();

// console.log(UTIL.dateFormat(new Date(),'yyyymmddhhmmss')) //=>20190312110722
// console.log(UTIL.dateFormat(new Date()))//=>2019-03-12 11:07:22 星期二
// console.log(UTIL.pick({ a: 1, b: '2', 'c': 3 }, ['a', 'c']))//=>{a:1,c:3}

export default UTIL;

然后在index.js中编写插件,导出

//utils/index.js

import UTIL from './utils.js'

const UtilPlugin={}

UtilPlugin.install=function(Vue,options){//插件必须有install方法,接受两个参数,一个是Vue构造器,一个是参数
  Vue.prototype.$utils=UTIL//在vue prototype上添加实例方法
}
export default UtilPlugin

最后在main.js中引入并use插件

// utils
import Util from './utils/index'
Vue.use(Util)
console.log(Vue.prototype.$util)//打印下是否引入成功

之后就可以在组件中通过使用this.$utils调用方法了

7. 兼容性处理

我们的目标是兼容到ie9,对ie8及以下的浏览器做相应的跳转处理(跳转到浏览器下载界面)

兼容性对一个程序来说是非常重要的,兼容性测试越早越好

image-20190307151841810

7.1 对ie8及以下浏览器的跳转处理

在项目根目录下中的html中head中加入下面代码

<!--[if lte IE 8]><script>window.location.href="https://support.dmeng.net/upgrade-your-browser.html?referrer="+encodeURIComponent(window.location.href);</script><![endif]-->

目的是检测ie浏览器的版本,如果低于<=ie8,就跳转到下面这个页面

image-20190307153138889

7.2 兼容ie9

参考链接:https://juejin.im/post/5b2868...

7.2.1 ES6兼容

我们把浏览器调到ie9,然后看控制台报错信息

image-20190307154807782

报这个错的原因是es6的新对象,新表达式,ie9不支持,为解决这个问题,我们需要引入babel-polyfill

cnpm i babel-polyfill -D

安装完成之后,在main.js文件中引入

import 'babel-polyfill'

在项目使用 vue-cli 生成的代码中,根目录有一个 .babelrc 文件,这是项目使用 babel 的配置文件。在默认生成的模板内容中,增加 "useBuiltIns": "entry" 的设置内容,这是一个指定哪些内容需要被 polyfill(兼容) 的设置

useBuiltIns 有三个设置选项

  • false - 不做任何操作
  • entry - 根据浏览器版本的支持,将 polyfill 需求拆分引入,仅引入有浏览器不支持的polyfill
  • usage - 检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的 polyfill

7.2.2建立自己的polyfill

加入这些代码后,工程中大部分代码已可以兼容到ie9版本,但还是会有少部分不兼容的特性,例如requestAnimationFrameclassList等。对于这些内容,我们需要自己定义polyfill来解决,在src目录下新建一个文件夹polyfill,然后在polyfill文件夹下面建一个polyfill.js,我们在polyfill.js中加入我们的兼容代码

然后在main.js中引入这个文件

import './polyfill/polyfill'

解决兼容方式的正确姿势是:拿到ie9浏览器下的报错信息,去goole或者baidu搜索,得到polyfill,然后加入到自己的polyfill.js文件中

三、优化

1. webpack3.x优化打包速度

我们执行一下npm run build,结果如下:

image-20190307161705933

整个打包过程花了32s左右,现在我们的项目只是引入了相关的依赖,一些业务逻辑还没有写,打包速度就那么慢了,等到我们写完整个项目,打包速度还会继续变长,所以我们需要优化一下。

优化打包速度,我们修改的主要是webpack.prod.conf.js文件

替换代码压缩工具

Webpack 默认提供的 UglifyJS 插件,由于采用单线程压缩,速度慢 ;

webpack-parallel-uglify-plugin 插件可以并行运行 UglifyJS 插件,更加充分而合理的使用 CPU 资源,这可以大大减少的构建时间;

//安装
cnpm i webpack-parallel-uglify-plugin -D
//配置 webpack.prod.conf.js

//首先删除项目中的 UglifyJsPlugin插件及配置,第二次打包时提高速度,要把.cache文件加入到gitignore中
// new webpack.optimize.UglifyJsPlugin({
//   compress: {
//     warnings: false,
//     drop_console: true
//   },
//   sourceMap: true
// }),

//然后引入并使用我们刚才装的插件

==注意:版本控制工具提交时,要忽略.cache文件==

配置完后我们执行npm run build,发现打包速度降到了23s

image-20190307162957635

再执行一次npm run build,发现打包速度降到了12s

image-20190307164513348

时间降低那么多是因为文件没有改动,直接利用了缓存中的js文件

happypack开启多核构建项目

一般node.js是单线程执行编译,而happypack则是启动node的多线程进行构建,大大提高了构建速度。

首先安装,

修改webpack.base.conf.js

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
...
...
// 增加plugins
 plugins: [
  new HappyPack({
    id: 'happy-babel-js',
    loaders: ['babel-loader?cacheDirectory=true'],
    threadPool: happyThreadPool,
  })
]
...
...
// 修改对应loader
{
  test: /\.js$/,
  loader: 'happypack/loader?id=happy-babel-js',
  include: [resolve('src'), resolve('test')],
}

配置完成,执行npm run build

image-20190307165549102

what??并没有提高速度 不要用这个鬼东西了

hardSourceWebpackPlugin节省70%的时间

https://github.com/mzgoddard/...
#安装
cnpm install --save-dev hard-source-webpack-plugin

使用,在webpack.prod.conf.js中引入并使用

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  context: // ...
  entry: // ...
  output: // ...
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}

结果:

image-20190307171310562

注:要第二次打包才生效

总结下,使用了三个插件,我们的打包速度从30s降低到4s,awesome!

2. webpack3.x优化首屏加载速度

首先要说明一下,首屏加载速度优化针对的是打包后dist文件。我们如果要在本地进行测试的话,需要本地有个服务器,我们在这里使用nginx。

2.1本地安装nginx

下载地址: http://nginx.org/en/download....

在官网上找到自己系统适合的nginx版本,下载到本地

2.1.1window安装

  1. 解压文件
  2. 双击运行nginx.exe,在任务管理器中出现nginx的进程,则表示安装成功

2.1.2 mac/linux安装

#1.解压文件
tar -xzf nginx-1.14.0.tar.gz  #mac可以使用解压缩工具解压,不必用命令行

#2. 配置安装路径   --prefix指定安装路径  假设我要装到/usr/local/nginx文件夹中
./configure --prefix=/Users/best9/local/nginx

#编译
make

##安装
make install

安装完成后进入到—prefix指定的文件夹中,执行ll,会发现文件夹下有以下目录

image-20190308144717721

我们要关心就是我上面标出来的三个目录

进到sbin目录中,启动nginx程序

image-20190308145219037

cd sbin
#需要使用root权限,否则会报错  报错信息可以在日志中查看到,错误日志目录 /logs/error.log
sudo ./nginx     

正常的话,nginx会默认在localhost:80端口启动,在浏览器访问localhost,就会显示默认界面

image-20190308145304691

如果电脑的80端口被占用的话,在conf/nginx.conf文件中修改端口

2.2 nginx常用命令

nginx使用-s发送信号操作运行中的进程,常用命令如下:

注意:使用命令需要在sbin目录下

#启动nginx
./nginx
#立即停止服务 -s stop
./nginx -s stop
#优雅地停止服务 -s quit
./nginx -s quit
#重启服务 -s reload
./nginx -s reload

2.3 nginx配置静态文件服务器

我们在这里使用nginx配置一个最简单的静态文件服务器,更复杂的配置稍后再讲

nginx的配置文件地址:conf/nginx.conf

使用vim或者其他编辑器打开该文件,修改配置文件第43-45行:

vim conf/nginx.conf

image-20190308150902046

location / {
  alias /Users/best9/github/vue2_template/dist;  #访问/相当于访问alias配置的目录    
}

配置完成后保存,然后重启服务

sudo ./sbin/nginx -s reload 要使用root权限重启

打开浏览器访问localhost

image-20190308151058193

因为没有登录,会自动跳转到登录界面

到这里静态文件服务器就配置好了,但我们刷新下页面,会报错404

image-20190308151213416

这是因为我们使用了vue router的history模式,我们需要在nginx中加入以下配置

image-20190308151523068

location / {
  try_files $uri $uri/  /index.html;
}

然后重启nginx,再刷新页面就没问题了

2.4 优化首屏加载速度

以上步骤就绪后,我们就可以来优化加载速度了

打开chrome的devTools面板,切换到Network,禁用浏览器缓存,刷新测试下加载速度,发现整个应用加载大约需要1.97s,如下图:

image-20190308152853851

把网络环境切换到Fast 3G,再测试一次,发现加载用了7.56s,白屏时间6.89s

image-20190308165101522

我们使用预渲染插件进行优化

2.4.1 预渲染

使用插件:prerender-spa-plugin

参考链接:https://juejin.im/post/59d49d...

首先,安装 prerender-spa-plugin,安装时件略长,因为其依赖了 phantomjs

cnpm install prerender-spa-plugin --save-dev

我们只在生产环境中进行预渲染,修改 build/webpack.prod.conf.js,在配置插件的地方加入如下代码。

//引入 预渲染插件
const PrerenderSpaP=require('prerender-spa-plugin')

//在plugins中配置
new PrerenderSpaP(
  // 输出目录的绝对路径
  path.join(__dirname,'../dist'),
  //预渲染路由
  ['/home','/login']
)

再次执行打包,然后再进行测试:

image-20190308165347855

发现白屏时间为4.10s,在弱网环境下,使用预渲染,大约能缩减2.5秒的白屏时间

预渲染注意事项
  • 预渲染的路由不能是动态加载的,否则会报webpackJsonp is not define的错误,要想解决这个错误,可以看这里 https://juejin.im/entry/5911a...
  • 预渲染的路由不能是需要权限才能访问的页面。预渲染的机制是在本地跑一个chromium浏览器,然后去爬取你预渲染页面的Html,如果你的页面需要权限(登录)才能进入,就爬不到,也不会报错,最终只会渲染不需要权限的页面

举个例子:

插件配置如下:

new PrerenderPlugin({
    staticDir:path.join(__dirname,'../dist')
    routes:['/','/about','/login']
})

路由配置如下:

image-20190314164834830

2.4.2 配置gzip压缩

gzip官方文档 http://nginx.org/en/docs/http...

nginx默认是关闭gzip的,我们需要自己打开,并进行一些配置:

image-20190311103518953

gzip:on;  #打开gzip,关闭为off
gzip_min_length 1;  #小于gzip_min_length,不进行压缩(默认单位为byte)
gzip_comp_level 2;  #压缩级别
gzip_types text/plain text/css application/javascript text/javascript image/jpeg image/gif image/png;#指定类型进行gzip压缩

配置完成后,我们再测试一下加载速度:

image-20190311103446777

发现白屏时间为1.95s,加载文件的体积也变小了

四、部署

1. nginx配置反向代理

我们要在本地部署测试,所以后台的地址是127.0.0.1:22222

项目开发完成后需要部署到服务器,因为是前后端分离,所以前端的应用部署到nginx,后端的应用部署到自己对应的服务器,所以我们需要配置一下,把后端的服务器变成上游服务,nginx做反向代理服务器

反向代理:服务器根据客户端的请求,从其关系的一组或多组后端服务器上获取资源,然后将这些资源返回给客户端。

由于上游服务器(后台服务器)要处理非常复杂的逻辑,所以性能不怎么样,我们使用nginx作为反向代理服务器后,可以将请求按照负载均衡算法代理给多台上游服务器。配置如下:

image-20190311112112788

以上配置是将所有的请求转发给上游服务器,但如果我们只想将动态请求转发给上游服务器,静态资源由nginx自己处理,就可以这样做:

判断是否是后台api(根据location的匹配规则),如果是的话,就进行转发

匹配规则看这里:https://stackoverflow.com/que...

upstream local{
    server 127.0.0.1:22222;  #假设在本地部署
}
server{
    listen:80;
    server_name localhost;
    location ~ /api/ {  #以`/api/`开头的uri就行转发,否则不转发 ~代表正则表达式匹配
        proxy_set_header: Host $host;
        proxy_set_header: X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://local;
    }    
    location / {
        #... alias index等配置 
          
    }
}

这里需要注意一个问题:proxy_pass是转发请求的模块,当你访问localhost:80/api/users/login时,会被转发到local的地址,即127.0.0.1:22222/api/users/login,所以开发环境下访问后台接口的URI要写你部署到nginx的URI,而不是真正的后台地址(因为被转发了)

前端配置

//apis/api.config.js
//判断是否是生产环境
var isPro = process.env.NODE_ENV=== 'production'
module.exports = {
    baseUrl: isPro ? 'http://localhost:80' : '/apis'//生产环境下的baseURl是nginx的hoost:port
}

2. 持续部署

项目做完需要发布到服务器,但每次手动打包,然后ftp传上去的话就太麻烦了,所以我们的需求是:git或者svn提交后,自动打包发布到服务器。使用的工具是jenkins.

参考文档:https://juejin.im/post/5ad198...

官网:https://jenkins.io/

jenkins安装与启动

jenkins一般情况下会装在服务器,但如果是同一个局域网的话,装在本机也可以

linux:

  1. https://blog.csdn.net/fenglai...
  2. https://www.jianshu.com/p/8a7... (centos)
  3. 配置文件地址 /etc/sysconfig/jenkins
  4. 工作空间 /var/lib/jenkins

windows下:

  1. 从Jenkins官网下载最新war文件。
  2. 运行java -jar jenkins.war即可。

mac:

  1. 从官网下载pkg文件
  2. 双击安装,安装之后自己就会启动

jenkins初始化

  1. jenkins的默认端口是8080,启动成功后在浏览器打开。
  2. 进入后会让我们输管理员密码,打开网页上提示路径下的文件,复制密码粘贴输入即可。
  3. 然后会让安装需要的插件,此处选默认即可,等待安装完成。
  4. 创建一个管理员账户。
  5. 上面都完成后会看到这个界面。

image-20190314171915326

创建任务

在主页上点击创建

image-20190314172049214

直接点保存,然后去安装插件

image-20190314172224487

安装插件

首先返回主页,然后点击左侧菜单 系统管理->插件管理

image-20190315091230194

需要安装的插件有:

  • Generic Webhook Trigger 实现git提交触发更新功能
  • Publish Over SSH 实现服务器部署功能
  • nvm wrapper 引入node

安装插件的方式:

image-20190315091837845

安装完插件之后重启一下jenkins(安装完插件后,有个重启的选项,勾选即可)

实现git钩子功能

当我们向github/码云等远程仓库push我们的代码时,jenkins能知道我们提交了代码,这是自动构建自动部署的前提,钩子的实现原理是在远端仓库上配置一个Jenkins服务器的接口地址,当本地向远端仓库发起push时,远端仓库会向配置的Jenkins服务器的接口地址发起一个带参数的请求,jenkins收到后开始工作

打开创建的项目(进入工程->点击配置)

image-20190315092840660

构建触发器

勾选 Generic Webhook Trigger

image-20190315094851281

github仓库配置钩子:

进入github项目中该项目页面,点击setting->webhooks,添加payload URL,

URL格式为 http://<User ID>:<API Token>@<Jenkins IP地址>:端口/generic-webhook-trigger/invoke userid和api token在jenkins的系统管理-管理用户-选择你的用户点进去-左侧设置

image-20190315095453871

实现自动化构建

自动化构建:jenkins实现安装依赖,打包(npm install && npm run build),此外还可以执行一些测试行为

点击构建环境,勾选nvm,输入node版本

image-20190315103055020

点击构建,选择执行shell,输入执行命令,多个命令使用&&分开

npm config set registry http://registry.npm.taobao.org/ &&
npm install && 
npm run build
查看原文

赞 320 收藏 238 评论 7

hyangteng 收藏了文章 · 2019-03-20

Javascript自动化文档工具:YUI Doc, JSDoc 3, JSDuck等比较

最近随着写Node以及独立的CommonJS模块越来越多,我发现有一份好的文档不仅可以帮助自己在应用这些接口的时候不至于迷糊,而且对于共同开发的情况下,能够省去大量团队的交流和Debug的时间。

本文比较了5种较为主流的Javascript注释文档生成工具。需要指出的一点是,Javascript是一个极为灵活的语言,文档生成并不像Java那样具有绝对统一的规范,还要根据使用场景确定哪个工具更加适合。


文中涉及的工具


比较标准

  • 生成文档的易读性
  • 工具的可扩展性
  • 注释语法标准
  • 注释语义的丰富程度

长话短说

对没有兴趣了解细节比较的你,可以快速浏览下面的总结来了解这些工具。

YUIDoc是YUI的附属项目。YUI团队希望这个文档工具不仅仅可以支持Javascript,而是更多的脚本语言,因此它并不考虑实际的代码实施细节,而只是对注释部分进行了扫描。从好的一面来说,如果你同时使用了Ruby/PHP/Python等等,YUI或许是一个比较适合的工具。从反面来说,因为它没有考虑实施细节,你需要额外的变量声明、同时生成的文档有可能会和实际的实施细节不吻合。

JSDoc 3的前身是JSDoc Toolkit。它会对代码的具体实施进行扫描,因此你如果不按照符合JSDoc的注释语法来进行注释的话,可能生成的文档一个字也没有。虽然它的学习曲线很高,但是一旦掌握了之后,由于它提供了完整的模版开发,事件触发等等接口,其灵活性也是不容小觑的。

Dox是一个非常轻量级和可以定制化的工具,它使用和JSDoc 3兼容的注释语法标准,并将其转化成JSON格式。因此在呈现方式上具有比JSDoc 3更强的可定制性,当然也意味着你初期的麻烦会比较多。

Docco是一种行间注释的方式。比起API注释,它更适合于注释代码的实施逻辑,以便于后期程序员能够快速捕捉到原作者在此处的实施意图。应该说是非常适合开源项目、多个作者共同维护的一个文档工具。比如最经典的Backbone Annotated Source就是使用Docco进行注释的。

JSDuck由Sencha开发,因此对于自家extJS具有非常强大的支持功能,包括令人咋舌的实时代码修改。但即使你不是使用extJS进行开发,JSDuck仍然是一个非常强大的工具,一方面它的语法非常灵活,描述支持Markdown语法,上手难度远远低于JSDoc 3;另一方面它生成的文档可读性堪比YUIDoc,同时支持源码的对照,学习起来也非常的容易。要说JSDuck有什么不好的地方,估计就是它把一切都Handle太完美了以致于没有给你提供什么可以自己定制的余地。

最后我选择了JSDuck作为文档生成的工具。从目的上说,我需要生成的是提供给Q&A和其他团队成员参考的API文档,考虑到毕竟写代码才是我们的主要工作,注释和文档越简单能够表达意思越好用,而JSDuck恰好十分符合我的需求。但是你选择使用哪种工具还需要根据使用场景来具体考虑,还请参考下面的细节比较


细节比较(JSDoc 3, YUI, JSDuck)

生成文档的易读性(YUIDoc、JSDuck)

  • YUIDoc
    YUIDoc提供了非常清晰的文档格式。不仅对象内部的内容非常清晰,而且相互引用的link也工作的非常好。同时YUIDoc提供了全局搜索的功能,你可以容易根据关键词找到对应的内容。
    图片描述

  • JSDuck
    JSDuck生成的文档绝对不输给YUIDoc。构造方法参数、属性、方法都非常清晰的列在文档之中。它的link也非常的好用,能够准确的定位到不同模块中的内容。JSDuck同样提供了全局搜索的方法,而且还在你敲下关键词的同时给你相关的提示。除了这些以外,JSDuck还提供了对于依赖(dependency)、以及查看源码(View source)的方法。
    图片描述

  • JSDoc 3
    JSDoc生成的文档使用了Bootstrap样式。默认的样式非常非常的糟糕,不过JSDoc 3在自己的介绍页面里推荐了一个第三方开发的模版系统“Docstrap”。这个模版虽然比JSDoc 3的默认模版好上很多,但是与YUIDoc和JSDuck生成的内容相比就差强人意了。其中的link的锚点也会偶尔不能正常工作。此外,它并不支持全局对于变量的搜索,你可以在Docstrap Github的issue中找到他们相关的开发计划。
    除了Docstrap以外,它还有一些推荐的模版系统,例如Jaguar,你也可以在这个基础上开发自己的模版。
    图片描述

工具的可扩展性(JSDoc 3)

  • YUIDoc
    由于YUIDoc是Yahoo下属YUI项目的一个部分,它并不像JSDoc 3提供了那么多可定制的功能。能够修改的大概就只有Logo,基本的CSS样式而已。

  • JSDuck
    JSDuck和YUIDoc的情况非常相似,属于Sencha下属的项目。样式上能够修改的也只有Logo而已。不过JSDuck灵活的地方在于,你可以定制文档顶部的Tag:你可以除了文档以外进一步包含视频、教程等等多种内容。

  • JSDoc 3
    JSDoc 3属于完全开源的项目,因此如果你等不及社区的更新,你完全可以自己对JSDoc进行深度的开发。JSDoc对外提供的开发接口有3个:

    • 模版Template
    • 事件Event
      其中最有意思的我认为是事件功能。JSDoc几乎对载入文件、分析注释和生成文档的每一步都提供了事件的hook:parseBegin, fileBegin, beforeParse, jsdocCommentFound……通过这些事件,你甚至可以进一步定义自己需要的comment tag和解析规则。
    • 插件Plugins

    更加详细的内容则可以在JSDoc的使用说明上找到详细的讲解。

注释语法标准(JSDuck)

最早这一票是投给JSDoc的,但是经过实际的测试之后发现JSDoc的语法标准非常严格,稍微不符合它的解析规则JSDoc便不能够正确的生成文档,其中最让我不能接受的事情是它对于立刻执行函数IIFE的支持实在是%……&*&*……%(里面定义的内容大多被认为是不暴露在全局中的,非常不方便)
下面的说明中我给出了注释一个函数的详细示例。

    function exampleName(config, extra){
        extra = !!extra;   //set the default value of extra to false
        this.callback = config.callback;

        return this;
    }

从这个简单的例子上几乎不太能看出来三者实现的差别,但是当你要注释命名空间、AMD或者CommonJS模块的时候,三者的差别就会凸显出来了。此外要声明的一点是,三者的语法几乎是不能通用的。我曾经试着将用YUIDoc注释的文件使用JSDoc解析,结果非常悲剧,反之亦然。

  • YUIDoc
    YUIDoc为了支持多种语言,它仅对注释块内部的内容进行解析。这意味着你需要对于函数、类、命名空间等的名称和更多的内容进行显性的声明。此外,就像前文提到的,它可能会造成文档和代码实现的不一致,甚至对于接口的使用造成不好的影响。
    YUIDoc对于注释内容要求严格的两层结构:Primary Tag和Secondary Tag
    详细的YUIDoc支持的语法标签可以参考这里

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    * @method exampleName  此处必须显性声明method名称
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    * @example
    *     new exampleName(function(){console.log("Hello World")})
    * @return {Object} Returns the constructed target object
    */
    
  • JSDuck
    JSDuck在这点上恰好处在YUIDoc和JSDoc 3之间。它既对代码的实现进行了最基本的解析,从中获取对应的名称、变量,有效的减少了Tag的使用。同时又不像JSDoc 3那样严格,我尝试了CommonJS、AMD和IIFE都能够非常自然的生成对应的文档。
    详细的JSDuck支持的语法标签可以参考这里

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    *     new exampleName(function(){console.log("Hello World")})  JSDuck支持Markdown格式,插入代码示例非常简单
    *
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    *     
    * @return {Object} The constructed target object
    * @return {Function} return.callback the callback function of the object  JSDuck可以支持返回对象的详细结构注释
    */
    
  • JSDoc 3
    JSDoc 3很大程度上参考了Javadoc的实现。它有非常详细的语法规则,并且你只有当和代码实现完全一致的时候,它才能正确的生成文档。但是对于Javascript这样一门灵活的语言而言,这似乎并不是最适合的方式。虽然代码有推荐的最佳实践,但是为了让文档生成工具能够正确识别而要对原本的代码进行修改就有点僭越本份的意味了。

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    * @example
    * new exampleName(function(){console.log("Hello World")}) 
    * @returns {Object} The constructed target object
    */
    

注释语义的丰富程度(JSDoc 3、JSDuck)

YUIDoc并非针对Javascript量身定制,因此它有一些用法会让你使用的时候感到比较别扭。比如最明显的一个例子就是,YUIDoc并没有一个专门的方法用来注释namespace,而是必须使用@class来对namespace进行注释。
相比之下JSDoc 3和JSDuck都是针对Javascript而设计的,虽然支持的标签略有差别,但是两者都能够很好的支持常见的设计模式。另外,JSDoc 3由于是从JSDoc Toolkit发展而来的,悠久的历史让它在Stackoverflow上有很多不错的例子可以参考。

查看原文

hyangteng 赞了回答 · 2019-03-07

node,npm运行的项目怎么让别的电脑访问?

监听 0.0.0.0就可以了

关注 3 回答 2

hyangteng 赞了回答 · 2019-03-05

解决json web token过期后怎么搞

楼上的都在说什么!不要乱误导人!你们到底看过 RFC7519 文档吗,你们有实现过JWT吗?!

第一名说过期时间5秒的也是够了,居然还有赞!!!!

segmentfualt现在的回答都是这种水准吗?

JWT实现的时候,一般会有两个过期时间

  • 第一个是Token本身的过期时间,这个时间一般1到2个小时,不能太长,也可以在短一点,不过5s的简直纯属扯淡。

  • 第二个是Token过期后,再次刷新的有效期,也就是Token过期后,你还有一段时间可以重新刷新,把过期的Token发给服务端,如果没有过刷新截止期,则服务端返回一个新的Token,不再需要通过用户名密码重新登录获取Token了。

所以为了减少过期后重新获取Token所带来的麻烦,我们一般在每次Http请求成功后,将目前的Token刷新,然后可以在Http响应中返回新的Token。

JWT由于过期数据(exp claim)是封装在Payload中的,所以必须返回一个新Token,而不是在旧Token的基础上刷新。

但是在并发的时候也会出现问题,如果前一个请求刷新了Token(为了安全,刷新后一般会把旧Token加入黑名单),后面的请求使用了一个旧的Token像服务请求数据,这个时候请求会被拒绝。

可以说这真的是JWT的一个缺陷,目前没有特别好的办法来解决并发刷新的问题。

不过可以通过设置一个宽限时间,在Token刷新后,如果旧Token仍处于刷新宽限时间内,就放行。

我最近在写一个JWT的扩展包,给Lumen用的,如果想了解一些JWT的原理,构成,可以关注我的专栏,和这个系列的文章。

Code杂货铺-lsxiao的知乎专栏

从零实现Lumen-JWT扩展包

关注 32 回答 10

hyangteng 收藏了问题 · 2019-03-05

json web token过期后怎么搞

现在不是流行restful么,认证的时候用jwt,token有过期时间,有人说时间越短越好,
那过期后怎么认证,要在登录吗,过期时间多久比较好

hyangteng 赞了问题 · 2019-03-05

解决json web token过期后怎么搞

现在不是流行restful么,认证的时候用jwt,token有过期时间,有人说时间越短越好,
那过期后怎么认证,要在登录吗,过期时间多久比较好

关注 32 回答 10

hyangteng 发布了文章 · 2019-02-20

你知道前端单页面路由是怎么实现的吗?

首先要学习一下history对象,history对象保存着用户的上网记录,从浏览器窗口打开的那一刻算起。出于安全的考虑,开发人员无法得知用户浏览过的URL。不过,借由用户访问过的页面列表,同样可以在不知道实际URL的情况下实现后退与前进

一、history对象的方法

go(Stirng|number)

使用go方法可以在用户的历史记录中任意跳转,可以向后也可以向前。这个方法接受一个参数,表示向后或向前跳转的页面数的一个整数值。负数表示向后跳转(类似浏览器的后退按钮),正数表示向前跳转(类似浏览器的前进按钮)。来看下例子

//后退一页
history.go(-1)

//前进一页
history.go(1)

//前进两页
history.go(2)

也可以给go()方法船体一个字符串参数,此时浏览器会跳转到历史记录中包含改字符串的第一个位置,可能后退也可能前进,具体要看哪一个位置最近。如果历史记录中不包含该字符串,则什么都不做。例如:

//跳转到最近的wrox.com页面
history.go("wrox.com")

//跳转到最近的douban.cn页面
history.go("douban.cn")

back()forward

这两个方法可以来代替go(),模仿浏览器的后退和前进功能

back()相当于 go(-1) 后退一个页面

forward相当于go(1) 前进一个页面

注:接下来几个方法是html5新增的方法

二、html5中history新增的方法

pushState(state,title,url)

该方法的作用是 在历史记录中新增一条记录,改变浏览器地址栏的url,但是,不刷新页面

pushState对象接受三个参数,

  • state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null
  • title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

举个例子,假设当前网址是hello.com/1.html,使用puchState()方法在浏览记录中添加一个新纪录

var stateObj={foo:'bar'}
history.pushState(starteObj,'','2.html')

添加新纪录后,浏览器的地址栏立刻显示`hello.com/2.html,但不会跳转到2.html,也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。

总之,pushState()方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应,使用该方法后,就可以使用history.state属性读出状态对象

var stateObj={foo:'bar'}
history.pushState(starteObj,'','2.html')
history.state //=> {foo:"bar"}

注意:如果pushState的URL参数设置了一个新的hash值,并不会触发hashchange事件。

replaceState(state,title,url)

replaceState方法的作用是替换当前的历史记录,其他的都与pushState()方法一模一样。

假定当前网页是example.com/example.html

history.pushState({page: 1}, 'title 1', '?page=1')
// URL 显示为 http://example.com/example.html?page=1

history.pushState({page: 2}, 'title 2', '?page=2');
// URL 显示为 http://example.com/example.html?page=2

history.replaceState({page: 3}, 'title 3', '?page=3');
// URL 显示为 http://example.com/example.html?page=3

history.back()
// URL 显示为 http://example.com/example.html?page=1

history.back()
// URL 显示为 http://example.com/example.html

history.go(2)
// URL 显示为 http://example.com/example.html?page=3

三、popstate事件

popstate事件是window对象上的事件,配合pushState()和replaceState()方法使用。当同一个文档(可以理解为同一个网页,不能跳转,跳转了就不是同一个网页了)的浏览历史出现变化时,就会触发popstate事件。

上面我们说过,调用pushState()或者replaceState()方法都会改变当前的历史记录,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,另外一个条件是用户必须点击浏览器的倒退按钮或者前进按钮,或者使用js调用history.back()或者history.forward()等方法。

所以,记住popstate事件触发的条件

1. 处在同一个文档(同一个html页面)
2. 文档的浏览历史(即history对象)发生改变

只要符合这两个条件,popstate事件就会触发

具体例子

//index.html
<head>
    <script>
        window.onpopstate=function(){
            alert('location '+document.location+',state '+JSON.stringify(event.state))
        }
    </script>
</head>
<body>
    <!--第二步 -->
    <button onclick="window.history.back()">后退</button>
    <button onclick="window.history.forward()">前进</button>
    <!--第一步 -->
    <button onclick="window.history.pushState(null,'','1.html')">pushState</button>    
</body>

先点击pushState按钮,在点击后退按钮,就会触发popstate事件

再来一个例子

//index.html
<head>
    <script>
        window.onpopstate=function(){
            alert('location '+document.location+',state '+JSON.stringify(event.state))
        }
    </script>
</head>
<body>
    <a href="#one">#one</a>   
</body>

直接点击a标签,也可以触发popstate事件

四、浏览器兼容性

图片来自mdn传送门

image-20190219150537866

五、单页面路由原理

前端路由的本质是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新。

目前单页面使用的路由就只有两种实现方式

  • hash模式
  • history模式

hash模式

www.test.com/##/就是Hash URL,当##后面的哈希值发生变化时,不会向服务器请求数据,可以通过hashchange事件来监听到URL的变化,从而进行跳转页面

网上偷来的一张图:

hash

history模式

history模式相比hash模式更美观,需要用到Html5新增的几个api实现,原理如下:

继续偷图:

history

五、实例,使用history api实现简单的单页面路由

在介绍实例前先介绍下location对象,location对象提供了与当前窗口中加载的文档有关的信息。它包含以下属性:

属性名例子说明
hostwww.hello.com:8080返回服务器名称和端口号(如果有的话)
hostnamewww.hello.com返回服务器名称,不带端口号
hrefhttp://www.hello.com返回当前加载页面的完整url
pathname/user/ming返回url中的目录
hash#content返回url中的hash,如果没有返回空字符串
search?q=javascript返回Url的查询字符串,这个字符串以问号开头

我们在下方的示例中需要用到pathname属性拿到访问的路径

一个简单的history模式单页面路由实现如下:

//1. 路由规则
const routes={
    '/user':user, //user是引入的视图   import user from './view/user' 
    '/about':about
}
//2. 路由控制类
class Router {
  start() {
    // 点击浏览器后退/前进按钮时会触发window.onpopstate事件, 我们在这时切换到相应页面
    // https://developer.mozilla.org/en-US/docs/Web/Events/popstate
    window.addEventListener('popstate', () => {
      this.load(location.pathname)
    })

    // 打开页面时加载当前页面 在单页面入口文件中要调用start方法
    this.load(location.pathname)
  }

  // 前往path, 变更地址栏URL, 并加载相应页面
  go(path) {
    // 变更地址栏URL
    history.pushState({}, '', path)
    // 加载页面
    this.load(path)
  }

  // 加载path路径的页面
  load(path) {
    // 首页
    if (path === '/') path = '/foo'
    // 创建页面实例
    const view = new routes[path]()
    // 调用页面方法, 把页面加载到document.body中
    view.mount(document.body)
  }
}

Router类的作用是控制页面根据当前Url切换

  • start()

    • 作用1: 监听onpopstate事件,在浏览器前进或后退时加载相应的页面
    • 作用2: 打开页面时加载当前页面,需要在单页面的入口文件引入,并执行
  • go(path)

    • 跳转到path对应的页面
  • load(path)

    • 加载path路径的页面

参考链接

查看原文

赞 25 收藏 19 评论 1

认证与成就

  • 获得 1082 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-08-10
个人主页被 2.7k 人浏览