写这篇文章的目的,更多是让自己更熟悉vue-cli脚手架创建项目的依据和项目结构,其次是希望我的学习过程可以帮到有疑惑的同学,有什么错误还希望可以得到指教
为什么要分上、下,因为最近学习react.js,发现项目框架除了使用的js库不同(vue.js、react.js),配置基本上是大同小异的
这也是我占坑(脸大)的理由
徒手撸个vue项目框架(上)
徒手撸个vue项目框架(下)
徒手撸个react项目框架(上)
徒手撸个react项目框架(下)
这次的目的是接着上篇在框架中添加vue-router,在第三方工具包的基础上针对业务进行封装
一、路由
路由是vue组件能够灵活切换的关键所在,vue-router是vue的官方路由。
1.引入vue-router
a.下载
cnpm i vue-router --save-dev
b.实例化
我们在src目录下新建router文件夹,并在router新建index.js,同时在components下新建login.vue
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: "hash",
routes: [
{
path: "/",
name: "index",
components: require("../components/hello.vue")
},
{
path: "/login",
name: "login",
components: require("../components/login.vue")
}
]
})
export default router
c.挂载
到这里还没结束,我们还需要将router对象挂载到根实例上,在index.js中,如下操作
import router from "./router/index";
new Vue({
el: "#app",
router,
render: h => h(App)
});
在你需要渲染组件的地方添加router-view标签
<template>
<div>
<router-view />
</div>
</template>
现在你可以在浏览器中访问了
2.利用导航守卫实现登录控制
但是有时候我们的项目要求没有登陆是不能进入首页的,这个功能可以利用router的前置导航守卫实现
beforeEach就是前置导航守卫的钩子函数,它接收三个参数,to、from、next
- to:即将进入的路由对象
- from:离开的路由对象
- next:一定要调用该方法来resolve这个钩子
我使用了h5的localStorage模拟cookie保存用户信息,这里只是个测试,如果你喜欢cookie,可以使用自己喜欢的cookie包,个人喜欢js-cookie
思路是这样的,登录的时候验证完对的用户名和密码,就设置一个字符串作为token存储在本地,token的作用就是下次可以免登录(这种做法的安全性是个问题)
在router/index.js中
router.beforeEach((to, from, next) => {
// 如果登录的时候设置过
if(localStorage.getItem("token") != null){
if(to.name == 'login'){// 如果还访问登录页就导向首页
next({path: '/'})
}else{// 给所有其它页面放行
next()
}
}else{// 如果没有设置这个值,为空,说明没有登录,导向登录页
if (to.name == "login") {
next();
} else {
if (to.name == "login") {
next(); // 这里要记得给登录页放行,不然会进入死循环
} else {
next({ path: "/login" });
}
}
}
})
虽然这里已经做得很不错了,但依然没有发挥出vue-router灵活和强大的一面。换句话说,这里能做的事还有很多很多,比如项目是一个管理系统的话,可能会因角色不同,进入首页的侧边栏目是不一样的,虽然看上去是个很复杂的过程,但是仔细分析下也就几步,感兴趣的同学可以看这里
3.利用命名视图实现特殊布局
a.同级路由
同级路由就是两个router-view标签是并列的,分别给两个标签用name属性命名,所以命名视图就是给视图命名了后的视图router-view
// app.vue
<router-view name="navbar"></router-view>
<router-view name="main"></router-view>
// 对应的路由写法就是
import { NavbarComponent, MainComponent } from "@/components"
const router = new VueRouter({
routes: [
{
path: '/',
components: {
navbar: NavbarComponent,
main: MainComponent
}
}
]
})
// 在app.vue中
<template>
<div>
<router-view name="navbar" />
<router-view name="main" />
</div>
</template>
这时候会看到
b.嵌套路由
嵌套路由在管理系统更为常见,我们经常使用的layout布局就是嵌套路由实现的
// 这次我这样定义路由
{
path: "/index",
name: "index",
components: require("@/components/hello"),
children: [
{
path: "login",
name: "login",
components: require("@/components/login")
}
]
},
// 并且这样写hello组件
<template>
<div class="test">index page
<router-view></router-view>
</div>
</template>
我在浏览器中访问http://localhost:8080/#/index/login,可以访问到下面的页面
二、与服务端交互
官方推荐与服务端交互使用axios,它是基于ajax封装的,用起来十分简洁
1.下载
cnpm i axios --save-dev
2.介绍
axios有两种使用方法一种是使用axios对象调用get或者post请求方法,
另一种是使用axios api,使用axios()函数,参数是个配置对象options
axios.get('/getUser')
.then(data=>{
// 请求成功处理函数
console.log(data)
})
.catch(err=>{
// 请求失败处理函数
console.log(err)
})
// 或者
axios({
url: '/getUser',
mothod: 'get'
})
.then(data=>{
// 请求成功处理函数
console.log(data)
})
.catch(err=>{
// 请求失败处理函数
console.log(err)
})
3.封装
在实际的项目中,经常需要对发送的请求或者服务端响应的结果进行处理,请求很多时,挨个处理就很繁琐,也很不切合实际,希望在全局就已经处理好了,我们只负责发送请求和接收服务端响应。
好在axios提供了这种方法。
在src下新建http文件夹,下面新建request.js文件
import axios from "axios";
// 重新实例化一个axios实例,这个实例会覆盖所有默认属性
const server = axios.create({
baseURL: "/api",
timeout: 5000,
heads: { 'content-type': 'application/x-www-form-urlencoded' },
});
// 或者通过修改实例的defaults属性,这两种方法是等价的
server.defaults.baseURL = "/api";
server.defaults.timeout = 5000;
export default server
不仅如此,我们还希望在每次发送请求的时候带上登录是设置的token值,在收到服务器错误时可以做出相应的反馈,比如返回的状态码为404,就导航到404页面
这里可以使用axios提供的拦截器对象,具体做法如下
// 设置拦截器
// 请求拦截器
server.interceptors.request.use(
config => {
config.headers.token = localStorage.get("token");
return config;
},
err => {
return Promise.reject(err);
}
);
// 响应拦截器
server.interceptors.response.use(
response => {
return response;
},
err => {
switch (err.response.status) {
case 404:
router.push({
path: "/404"
});
break;
case 504:
router.push({
path: "/504"
});
break;
}
return Promise.reject(err);
}
);
++记得添加错误对应的页面和路由++
// router/index.js
{
path: "/404",
name: "404",
components: require("../components/404.vue")
},
{
path: "/504",
name: "504",
components: require("../components/504.vue")
}
简单的封装已经完成了,只需要在使用的地方引入它
4.跨域代理
这里我们就得说说跨域了,这是前后端分离项目无法避开的。对于前后端分离的项目,之所以跨域是因为浏览器有同源策略的安全限制,来防止跨站请求伪造和跨站脚本攻击
跨域就是违背了同源策略,webpack-dev-server提供了解决跨域的方案
// webpack-dev-server方案
// 在webpack.config.js中devServer
devServer: {
port: 8080,
proxy: {
"/api": {
target: "http://127.0.0.1:10000", // 代理的目标地址
changeOrigin: true // 是否开启跨域
}
}
}
当然axios也提供了跨域方案,只是比较复杂,还需要后端同学的设置配合才行,这里就不说了
5.使用
这里使用一个例子来说明使用方法。当然,我没有写登录功能,所以记得把之前写的路由前置守卫的钩子函数注释掉,不然无法跳转
逻辑是这样的,在接入首页时自动发送请求,获取用户列表,请求方式为get
// hello.vue
import axios from '../http/request.js'
export default {
created() {
axios({
url: '/getUsers',
method: 'get'
})
.then(data =>{console.log(data)})
.catch(err =>{console.log(err)})
}
};
如果你已经有后端服务器的支持,但是没有写对应的路由,那它会在控制台报错超时,即状态码为504,页面会自动跳转到504页面
假如你和我一样没有后端服务程序,那控制台报错为404,即状态码为404,页面会自动跳转到404页面
三、细节的完善
到这里项目框架功能已经算是撸完了,但是它有很多地方不够正规,和cli创建的项目相比,生产环境和开发环境还没有分离;其次一个合格的大众框架,应该有eslint语法检测;虽然主流的浏览器已经开始支持es6测试语法,但是最好可以加入babel-loader,将es6语法转换es5语法。
1. babel-loader
babel-loader主要是将es6语法转换成浏览器兼容的es5语法,但是项目中node_moudles下也有很多js文件,全都转换会使得速度变慢,并且使项目无法运行,所以需要配置转换文件的路径或者屏蔽掉无需转换的路径
a.下载
babel-loader只是个加载器,转换代码的工作是babel-core
babel在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,
例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。
所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小
// babel-preset-env这是babel预设的语法规则,babel-preset-是前缀,env是包名
// @babel/plugin-transform-runtime是插件,babel-plugin-是前缀,transform-runtime是插件名
cnpm i babel-core babel-loader babel-preset-env @babel/plugin-transform-runtime --save-dev
这里要注意下载的版本,起初我下载的是babel-plugin-transform-runtime,结果一直报错
TypeError: this.setDynamic is not a function
后来我下载了@babel/plugin-transform-runtime才解决了这个问题
b.配置
// rules中
// include 表示哪些目录中的 .js 文件需要进行 babel-loader
// exclude 表示哪些目录中的 .js 文件不要进行 babel-loader
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
// include: [path.resolve("src")],
exclude: /node_modules/,
options: {
presets: ['env'], // env提供语法转化的规则,这里是babel预设的
plugins: ['transform-runtime'] // 这里放我们自己想要使用的插件
}
}
]
}
babel-loader默认是什么都不会做的,需要在预设presets选项中指定插件为你工作的语法规则,babel已经为我们提供了几套预设方案,babel-preset-env就是最新的语法转换规则,其中babel-preset-只是前缀,
另外babel-loader还可以提供丰富的插件做更多的事,只需要在options下的plugins选项中指明
++注意:要用的插件一定要记得下载++
c. 一个例子说明.babelrc文件的用法
下面除了想自动语法转化外还希望将组件懒加载,使加载变快,这时候需要使用插件babel-plugin-syntax-dynamic-import,所以先下载这个包,然后将它放入plugins中
// 下载
cnpm i babel-plugin-syntax-dynamic-import --save-dev
// 在module选项下
rules: [
{
test: /\.js$/,
loader: "babel-loader",
include: [path.resolve("src")],
exclude: [path.resolve("node_modules")],
options: {
presets: ['env'], // env提供语法转化的规则,这里是babel预设的
plugins: ['@babel/transform-runtime',"syntax-dynamic-import"]
}
}
]
假如你需要的插件很多,还需要单独的配置,你可以在项目根目录下创建一个.babelrc的文件替换掉options,配置语法完全同json,下面给个示例,完全等同上面的options
// 在.babelrc文件中,必须是要有两个数组类型的选项:presets、plugins
{
"presets": ["env"],
"plugins": ["@babel/transform-runtime", "syntax-dynamic-import"]
}
presets更多选项看这里
2.eslint语法检测
eslint对初学者来说是极其不友好的,因为它有严格的要求,使得很多人都想着关闭它,但是不得不说eslint在多人协作团队来说是至关重要的,它可以保证代码风格的一致性
a.下载
cnpm i eslint-plugin-vue eslint-friendly-formatter eslint-loader eslint --save-dev
// 此外记得全局安装eslint支持vue的插件,否则会报错Cannot find module 'eslint-plugin-vue',但是局部仍然要安装
cnpm i eslint-plugin-vue -g
b.使用
在webpack配置文件中,添加新的rules
{
test: /\.(js|vue)$/,
loader: "eslint-loader",
include: [path.resolve(__dirname, 'src')], // 指定检查的目录
options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine
formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范
}
}
c.配置规则
eslint官网提到了三种使用方法(这也是查找规则所在位置的顺序)
- 使用注释的形式嵌入到代码
- 在package.json中添加eslintConfig字段,这里指定配置
- 使用.eslintrc.*文件(任何后缀名)
我选择效仿别人使用.eslintrc.js形式,既然是js文件我们就需要跟写js模块文件一样暴露出一个对象
// 像这样
module.exports = {}
这里我只说几个常用的或者重要的配置选项,官网支持中文版,比babel中文版友好很多
// 像这样
module.exports = {
// 默认情况下,ESLint 会在所有父级目录里寻找配置文件,一直到根目录。如果你想要你所有项目都遵循一个特定的约定时,这将会很有用,但有时候会导致意想不到的结果。为了将 ESLint 限制到一个特定的项目,在你项目根目录下的 package.json 文件或者 .eslintrc.* 文件里的 eslintConfig 字段下设置 "root": true。ESLint 一旦发现配置文件中有 "root": true,它就会停止在父级目录中寻找。
"root": true,
// extends: 继承属性,值可以是 "eslint:recommended" "eslint:all" 或者是个插件 或者是个文件
extends:[
"eslint:recommended",
"eslint:all"
// "plugin:react/recommended"
],
// env: 指定运行的环境,可选值有browser、node、es6等等,值为布尔类型,true为开启,默认为false
"env": {
"browser": true
},
// plugins: 指定插件,使用之前需下载,vue项目中使用的话vue就是属于插件
"plugins": [
"vue"
// "react"
],
// rules: 指定语法规则,分为0,1,2三个等级对应off(关闭检测),warn(只警告),error(直接报错)
"rules": {
"eqeqeq": "off",
"curly": "error", // if结构中必须使用{}
"quotes": ["error", "double"]
// 更多rules看这里 https://www.jianshu.com/p/80267604c775
// 这里要说下rules的extends特性
// 假如你的设置是这样的
// "eqeqeq": ["error", "allow-null"]
// "eqeqeq": "warn"
// 那么它最后生成的配置是 "eqeqeq": ["warn", "allow-null"]
},
}
4.设置是否开启eslint
优秀的框架肯定会根据配置文件查看是否开启eslint,我在配置文件中定义了一个可配置的变量esLint,当它的值为true时开启eslint语法检测,但默认值为false
const esLint = false
// 替换掉配置好的eslint-loader
....
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
esLint?{
test: /\.(js|vue)$/,
loader: "eslint-loader",
include: [path.resolve(__dirname, "src")], // 指定检查的目录
options: {
// 这里的配置项参数将会被传递到 eslint 的 CLIEngine
formatter: require("eslint-friendly-formatter") // 指定错误报告的格式规范
}
}:{},
...
3.使用绝对路径
项目里我一直使用的是相对路径,虽然有友好的代码提示,但是一旦你改变了文件位置,就会报错不止,直到你把所有路径修改正确,最好的做法就是使用绝对路径,并且给路径添加别名,可以方便我们的书写
resolve: {
alias: {
···
"@": resolve("src")
}
}
这样我们在import中书写路径时可以用@表示根目录到src,后面继续跟剩下的路径
import hello from "@/components/hello.vue";
4.省略后缀名
resolve: {
extensions: [".js", ".vue", ".json"], // 当然,这里还可以添加.css、.less、.sass,这都是允许的
···
}
现在你在import时有.js或者.vue文件时不用再写后缀名了
import hello from "@/components/hello";
结语
花了两天时间终于把框架撸完了,说实话,以前没有在意的细节现在都很通透,当然这对我来说只是一小步,我还打算选择一套UI框架来封装常见的业务,使框架更加的模块化和完善
另外值得一提的是,一段时间前已经发布了@vue/cli3,也就是vue脚手架第三个版本,看了文档发现它只是将插件配置用vue.config.js代替了,它会自动根据vue.config.js的配置生成一份webpack.config.js的文件,我们只需要提供插件,和设置是否开启它
如果你看到这里了,那你的毅力告诉我,你以后技术肯定更加厉害