备战秋招:如何介绍一个前端的后台管理系统项目 | 您所在的位置:网站首页 › 项目有啥特点 › 备战秋招:如何介绍一个前端的后台管理系统项目 |
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情 本文介绍如何在面试中介绍一个前端项目,以后台管理系统为例,预计阅读时间12分钟 介绍项目:主要分四块来介绍 场景:介绍项目的背景 任务:在项目中承担的角色 行动:做了什么,使用了什么技术,做出了什么功能 结果:项目有什么成效 项目提问后台管理系统会问到的高频问题总结如下: token有效期 为了保证安全性后台所有token设置有效期(Expires/Max-Age),都放在cookies里面,就是当浏览器关闭了就丢失了。重新打开浏览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。 登录的过程 1.登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态) 2.前端会根据token再去获取用户的详细信息(如用户权限,用户名等等信息)。 3.用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,判断是否已获得token,在获得token之后我们就要去获取用户的基本信息了 权限管理(异步路由) 1.权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。 上述所有的数据和操作都是通过vuex全局管理控制的。(刷新页面后 vuex的内容也会丢失,所以需要重复上述的那些操作) 但这些控制都只是页面级的,前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的,前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。路由权限控制 1、权限路由全部存到数据库 思路:设计一个路由数据库表,将需要进行权限控制的路由全部存到数据库中,然后当成一个资源表,分配给对应的角色。用户登录时,根据角色,查询对应的路由信息,返回前端,存到vuex的state中和存档本地的localStorage中(存到本地缓存是防止频繁向后台请求路由。因为每次页面刷新,state的中的数据会消失,就需要向后台请求。存到本地缓存可以解决这个问题)。然后进行解析,router.beforeEach函数中,通过router.addRoutes(menu)新增路由。 2、后台只返回角色思路:前端的每个路由添加对应的角色(在meta中的roles配置)。后端只需要返回用户的角色,然后根据角色来筛选路由,每次路由跳转前做校验 实现: 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。 meta: { title: 'permission', icon: 'lock', roles: ['admin', 'editor'] // you can set roles in root nav },缺点: 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。 全局路由守卫里,每次路由跳转都要做权限判断。 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识 3、后端返回路由名称思路:设计一个简单的路由表,只需要填写路由的name即可,然后将数据库表中的路由分配给对应的角色。用户登录时,返回路由的name数组,前端只需要根据返回的数据,遍历路由,就可以实现路由的权限控制。这种方式和第一种方式的思路有点像,但是比较简单,不需要重新在前端重新解析和构建路由树。相对于第二种方式来说,安全性更高一点,因为权限控制最安全的做法还是后端来进行控制。 按钮权限控制 通过自定义指令进行按钮权限的判断 1. 路由中配置:meta: {btnPermissions: ['admin', 'supper']} //按钮需要的权限 2. 自定义指令: Vue.directive("has", {不符合要求就 el.parentNode.removeChild(el);} 权限检查方法:从浏览器缓存中获取权限数组(该数组在登入成功后拉取用户的权限信息时保存在浏览器的缓存中),若在按钮中定义的权限字段能在后端返回的权限数组中能找到,则该按钮可显示接口权限控制 登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token 项目亮点: 一、项目优化: 减少请求大小和次数 打包体积(通过externals加载外部CDN资源,图片svg图片或者字体图标) 加载速度(路由懒加载,webpack懒加载,图片的懒加载,虚拟列表) 请求大小(开启gzip压缩,减少对cookie的使用) 请求数量(善用本地存储,合并图片,应用http2,网越差越快) 骨架屏,进度条(vue的骨架屏插件vue-skeleton-webpack-plugin) 二、代码优化 事件代理 事件的节流和防抖 页面的回流和重绘 EventLoop事件循环机制 少使用闭包 CSS 放在文件头部,JavaScript文件放在底部,async和defer vue的代码:computed 和 watch,v-if 和 v-show,v-for ,合理组件化优化也可以总结为: 代码优化(事件代理,节流和防抖,回流和重绘,少使用闭包) 打包前优化:添加进度条,图片懒加载,可视化面板清理警告,移除console.log,提高加载速度 打包优化:生成打包报告,缩小体积(启用CDN,路由懒加载,会使得打包的过程变的特别慢,webpack懒加载),不同打包环境首页不同 性能优化知识可以参考这两个帖子:性能优化 性能优化总结 三、部分优化技术详解图片懒加载: document.body.scrollTop控制页面滚动 document.body.scrollTop = 0;回到顶部 优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。一张图片就是一个 开启gzip压缩,在vue.config.js里面进行 webpack配置(nginx也要开启gzip压缩,才能生效) 通过npogress添加进度条效果:(请求都比较快,就没添加了) 导入包的js和css 在requess拦截器中展示进度条 NProgress.start() 在response拦截器中隐藏进度条 NProgress.done()在vue可视化面板控制台中,找到警告 eslint报的错误:定义了插槽没使用 换行问题:eslint和代码格式化插件冲突,.prettierrc文件中修改发布阶段 移除console.log 安装babel-plugin-transfrom-remove-console插件 在babel配置文件 babel.config.js里增加 (Babel 是一个 JavaScript 编译器;主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript ) 通过process.env.NOOE_ENV === 'production'判断是什么阶段生成打包报告 js体积太大 通过vue.config.js修改webpack配置 - 导出:module.exports = {} - 为开发模式与发布模式指定不同打包入口(src/main.js) - chainWebpack和configuerWebpack,相同,一个通过链式编程修改,一个通过操作对象修改通过externals加载外部CDN资源 通过import导入的第三方依赖包,会被打包合并到一个文件,导致单文件体积过大 通过externals加载外部CDN资源,在index.html引入,就不会被打包到同一个文件夹,而在使用到这些依赖的时候,我们会从window全局找到这个依赖包。不同打包环境,首页不同,通过插件定制 在vue.confige.js定制参数路由懒加载:打包项目时,js文件很大,影响页面加载速率,把不同路由对应组件分割成不同代码块,当路由被访问时才加载组件,更快 1.vue异步组件技术 ==== 异步加载 路由配置中按需引入,component中写一个函数,在调用函数的时候再引入 component: () => import('./components/Home.vue') component:resolve=>(require(['./components/Home.vue'],resolve)) 2.通过webpack进行懒加载(另外的作用:三方库和我们的代码需要分开打包,我们可以给第三方库设置一个较长的强缓存时间) 在项目进行webpack打包的时候,对不同模块进行代码分割,加载时,用到哪个模块再加载哪个模块,实现懒加载 const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home') const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index') const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about') 四、防抖和节流详解 1. 防抖下拉控制条下拉到底部,显示一个返回顶部按钮 //非立即执行 function debounce(fn) { let t = null return function () { // e是形参 // console.log(arguments[0]);//arguments对象,实参列表,第0项是事件对象 if (t) clearTimeout(t) t = setTimeout(() => { // fn(...arguments) //使用箭头函数,拿到外层的arguments fn.apply(this, arguments)//通过this拿到 return函数的this }, 1000) } } //立即执行 function debounce(func, wait) { let timeout = null; return function () { if (timeout) clearTimeout(timeout) ////停止定时器,一直有timeout,没被清空 //let callNow = !timeout//引入一个callNow是因为如果用timeout==null判断,可能会被undefined干扰 if (timeout===null) func.apply(this, arguments) timeout = setTimeout(function () { timeout = null }, wait) } } 停止点击后执行 通过一个flag控制是立即执行还是非立即执行 2. 节流:搜索框input事件,支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。 //时间戳方法 function throttle(callback, wait=100){ let start = 0 return function(){ let now = Date.now() if(now - start >= wait) { //now随着不停的点击改变 callback(...arguments) start = now } } } //经过一定时间再运行通过判断结果 //定时器方法 function throttle(callback, wait=100){ let flag = true return function(){ //不用return 返回,相当于直接执行了 if(flag){ flag = false setTimeout(()=>{ flag = true callback(...arguments) }, wait) } } } 封装axios 安装 npm install axios; // 安装axios 引入一般我会在项目的src目录中,新建一个request文件夹,然后在里面新建一个http.js和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。 // 在http.js中引入axios import axios from 'axios'; // 引入axios import QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到 // vant的toast提示框组件,大家可根据自己的ui组件更改。 import { Toast } from 'vant'; 环境的切换我们的项目环境可能有开发环境、测试环境和生产环境。我们通过node的环境变量来匹配我们的默认的接口url前缀。axios.defaults.baseURL可以设置axios的默认请求地址 // 环境的切换 if (process.env.NODE_ENV == 'development') { axios.defaults.baseURL = 'https://www.baidu.com';} else if (process.env.NODE_ENV == 'debug') { axios.defaults.baseURL = 'https://www.ceshi.com'; } else if (process.env.NODE_ENV == 'production') { axios.defaults.baseURL = 'https://www.production.com'; }复制代码 设置请求超时通过axios.defaults.timeout设置默认的请求超时时间。例如超过了10s,就会告知用户当前请求超时,请刷新等。 axios.defaults.timeout = 10000; post请求头的设置post请求的时候,我们需要加上一个请求头,所以可以在这里进行一个默认的设置,即设置post的请求头为application/x-www-form-urlencoded; axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; 请求拦截我们在发送请求前可以进行一个请求的拦截,有些请求是需要用户登录之后才能访问的,或者post请求的时候,我们需要序列化我们提交的数据。这时候,我们可以在请求被发送之前进行一个拦截,从而进行我们想要的操作。 请求拦截 // 先导入vuex,因为我们要使用到里面的状态对象 // vuex的路径根据自己的路径去写 import store from '@/store/index'; // 请求拦截器axios.interceptors.request.use( config => { // 每次发送请求之前判断vuex中是否存在token // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况 // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断 const token = store.state.token; token && (config.headers.Authorization = token); return config; }, error => { return Promise.error(error); })这里说一下token,一般是在登录完成之后,将用户的token通过localStorage或者cookie存在本地,然后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,如果token存在说明用户已经登陆过,则更新vuex中的token状态。然后,在每次请求接口的时候,都会在请求的header中携带token,后台人员就可以根据你携带的token来判断你的登录是否过期,如果没有携带,则说明没有登录过。这时候或许有些小伙伴会有疑问了,就是每个请求都携带token,那么要是一个页面不需要用户登录就可以访问的怎么办呢?其实,你前端的请求可以携带token,但是后台可以选择不接收啊! 响应的拦截 // 响应拦截器 axios.interceptors.response.use( response => { // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 // 否则的话抛出错误 if (response.status === 200) { return Promise.resolve(response); } else { return Promise.reject(response); } }, // 服务器状态码不是2开头的的情况 // 这里可以跟你们的后台开发人员协商好统一的错误状态码 // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等 // 下面列举几个常见的操作,其他需求可自行扩展 error => { if (error.response.status) { switch (error.response.status) { // 401: 未登录 // 未登录则跳转登录页面,并携带当前页面的路径 // 在登录成功后返回当前页面,这一步需要在登录页操作。 case 401: router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }); break; // 403 token过期 // 登录过期对用户进行提示 // 清除本地token和清空vuex中token对象 // 跳转登录页面 case 403: Toast({ message: '登录过期,请重新登录', duration: 1000, forbidClick: true }); // 清除token localStorage.removeItem('token'); store.commit('loginSuccess', null); // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面 setTimeout(() => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }); }, 1000); break; // 404请求不存在 case 404: Toast({ message: '网络请求不存在', duration: 1500, forbidClick: true }); break; // 其他错误,直接抛出错误提示 default: Toast({ message: error.response.data.message, duration: 1500, forbidClick: true }); } return Promise.reject(error.response); } } });响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。例如上面的思想:如果后台返回的状态码是200,则正常返回数据,否则的根据错误的状态码类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后调整登录页的一个操作。 要注意的是,上面的Toast()方法,是我引入的vant库中的toast轻提示组件,你根据你的ui库,对应使用你的一个提示组件。 封装get方法和post方法下面我们主要封装两个方法:get和post。 get方法:我们通过定义一个get函数,get函数有两个参数,第一个参数表示我们要请求的url地址,第二个参数是我们要携带的请求参数。get函数返回一个promise对象,当axios其请求成功时resolve服务器返回值,请求失败时reject错误值。最后通过export抛出get函数。 /** * get方法,对应get请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function get(url, params){ return new Promise((resolve, reject) =>{ axios.get(url, { params: params }).then(res => { resolve(res.data); }).catch(err =>{ reject(err.data) }) });}post方法: 原理同get基本一样,但是要注意的是,post方法必须要使用对提交从参数对象进行序列化的操作,所以这里我们通过node的qs模块来序列化我们的参数。这个很重要,如果没有序列化操作,后台是拿不到你提交的数据的。这就是文章开头我们import QS from 'qs';的原因。 将params序列化: QS.stringify(params) var last=JSON.stringify(obj); //将JSON对象转化为JSON字符 var obj = JSON.parse(data); //由JSON字符串转换为JSON对象 /** * post方法,对应post请求 * @param {String} url [请求的url地址] * @param {Object} params [请求时携带的参数] */ export function post(url, params) { return new Promise((resolve, reject) => { axios.post(url, QS.stringify(params)) .then(res => { resolve(res.data); }) .catch(err =>{ reject(err.data) }) }); }复制代码这里有个小细节说下,axios.get()方法和axios.post()在提交数据时参数的书写方式还是有区别的。区别就是,get的第二个参数是一个{},然后这个对象的params属性值是一个参数对象的。而post的第二个参数就是一个参数对象。两者略微的区别要留意哦! 整齐的api就像电路板一样,即使再复杂也能很清晰整个线路。上面说了,我们会新建一个api.js,然后在这个文件中存放我们所有的api接口。 首先我们在api.js中引入我们封装的get和post方法 /** * api接口统一管理 */ import { get, post } from './http'复制代码现在,例如我们有这样一个接口,是一个post请求: http://www.baiodu.com/api/v1/users/my_address/address_edit_before复制代码我们可以在api.js中这样封装: export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p);复制代码我们定义了一个apiAddress方法,这个方法有一个参数p,p是我们请求接口时携带的参数对象。而后调用了我们封装的post方法,post方法的第一个参数是我们的接口地址,第二个参数是apiAddress的p参数,即请求接口时携带的参数对象。最后通过export导出apiAddress。 然后在我们的页面中可以这样调用我们的api接口: import { apiAddress } from '@/request/api';// 导入我们的api接口 export default { name: 'Address', created () { this.onLoad(); }, methods: { // 获取数据 onLoad() { // 调用api接口,并且提供了两个参数 apiAddress({ type: 0, sort: 1 }).then(res => { // 获取数据成功后的其他操作 ……………… }) } } }其他的api接口,就在pai.js中继续往下面扩展就可以了。友情提示,为每个接口写好注释哦!!! api接口管理的一个好处就是,我们把api统一集中起来,如果后期需要修改接口,我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口然后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是如果直接在我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦。 响应式原理 数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢? 想完成这个过程,我们需要: 侦测数据的变化 收集视图依赖了哪些数据 数据变化时,自动“通知”需要更新的视图部分,并进行更新 对应专业俗语分别是: 数据劫持 / 数据代理 依赖收集 发布订阅模式 - 在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,Data通过Object.defineProperty方法转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。 - 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知 - 在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。导出:module.exports = {} 为开发模式与发布模式指定不同打包入口(src/main.js) chainWebpack和configuerWebpack,相同,一个通过链式编程修改,一个通过操作对象修改 通过webpack进行懒加载 在项目进行webpack打包的时候,对不同模块进行代码分割,加载时,用到哪个模块再加载哪个模块,实现懒加载 const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home') const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index') const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about') git Git是一个分布式的版本控制工具SVN 是集中版本控制工具客户端可以在其本地系统上克隆整个存储库版本历史记录存储在服务器端存储库中git add # 添加当前目录的所有文件到暂存区$ git add . git commit -a .git 子目录,其中包含你的仓库所有相关的 Git 修订历史记录 git revert //git 取消commit或已经push的,再重新提交一次 git config //命令可用来更改你的 git 配置,包括你的用户名。 git branch –merged //它列出了已合并到当前分支的分支。 git branch –no-merged //它列出了尚未合并的分支。 git init [project-name] git clone [url] $ git branch//本地所有分支 git branch dev //新建一个分支 git checkout dev//切换分支 git merge dev//hgit merge origin/dev 将分支dev与当前分支进行合并 git branch -d 如果需要删除的分支不是当前正在打开的分支,使用branch -d直接删除 git branch git reflog 想要恢复的分支的散列值简单对比git pull和git pull --rebase的使用 echart //首先,创建一个div盒子来盛放我们即将绘制的柱状图。 //然后,我们导入Echarts类库。 //最后,开始绘图。(重点是option里的设置,设置坐标轴、设置数据。) // 基于准备好的dom,初始化echarts实例 function a1() { var echar=echarts.init(document.getElementById("first")); // 指定图表的配置项和数据 var option={ //设置标题 title:{text:"柱状图",subtext:'柱状图哟柱状图' }, //设置提示 tooltip:{show:true}, //设置图例 legend:{data:["销量"]},// //设置坐标轴 xAxis:{ data:["一月","二月","三月","四月"] }, yAxis:{type:'value'}, //设置数据 series:{ name:"销量", type:"bar", data:[200,400,600,300] } }; // 使用刚指定的配置项和数据显示图表。 echar.setOption(option); } 封装组件封装了一个echart组件 lesscss预处理语言 嵌套特性让代码选择器变简单 通过npm安装 变量: @width 可以直接引入嵌套: 直接进行嵌套伪类: &:hover{ } &::befor { }媒体查询: @media screen and (max-width:500px) {background:#88888}//实现缩小到500改变颜色命名空间 当我们拥有了大量选择器的时候,采用命名空间来对名字进行分组,来避免重名问题。 #mynamespace { .home {...} .user {...} } 定义了一个名为 mynamespace 的命名空间,如果我们要复用 user 这个选择器的时候,在需要混入这个选择器的地方,只需使用 #mynamespace > .user eslint 是一种编码规范 npm 只是开发阶段用到这个包 在.eslintrc.js里面修改 单引号双引号,有没有console.log等 Nginx 负载均衡如果一个网站只有一台服务器的话,如果这台服务器宕机了,那么整个网站将无法正常访问。当访问网站人数过多,并发量达到一定规模,超过服务器性能的极限,整个网站也将无法访问。为了缓解这类问题,使用负载均衡;负载均衡是通过后端引入一个负载均衡器和至少一个额外的web服务器(增加的web服务器和原本的web服务器提供相同的内容)。用户访问的时候,先访问到负载均衡器,再通过负载均衡器将请求转发给后台服务器。通过这种方法,当有一台服务器宕机时,负载均衡器就分配其他的服务器给用户,极大的增加的网站的稳定性。 对vue-cli以及webpack可以进行一些配置 publicPath: '/by', alias: {"assets": '@/assets', "views": '@/views', "api": '@/api', } //开启js、css压缩,生成gz压缩文件 上线流程将代码提交到git后就自动打包部署,jenkins自动化部署 常见问题处理 页面卡顿 先会检查是否是网络请求太多,导致数据返回较慢,可以适当做一些缓存 也有可能是某块资源的bundle太大,可以考虑拆分一下 然后排查一下js代码,是不是某处有过多循环导致占用主线程时间过长 浏览器某帧渲染的东西太多,导致的卡顿 在页面渲染过程中,可能有很多重复的重排重绘 内存泄漏引起的 排查内存泄漏 排查内存泄漏,谷歌浏览器开发者工具,Performance性能这一栏,点击录制,看到整个的渲染过程的摘要,看谁耗时多,以及内存这一栏,记录页面内存的具体情况内存泄漏的可能情况: 闭包使用不当引起内存泄漏 全局变量 分离的DOM节点 控制台的打印 遗忘的定时器 history和hash hash模式和history模式有什么区别? 首先,都是前端路由方式,路由跳转时都会阻止浏览器(a)默认行为,不向服务器请求数据。 直观区别: hash模式url带#, history模式不带#。 深层区别: hash模式通过改变location属性的hash值,实现url的改变;基于浏览器的hashchange事件、实现前端路由页面的更新。 url#后面内容不会被包括在HTTP请求中, 对后端完全没有影响,改变hash不会重新加载页面。 history模式基于location属性的pathname 和HTML5新增的pushState()和replaceState()两个api,实现url的改变,和路由页面更新。以及浏览器的popstate事件,实现前进后退等功能。url地址就是向服务器实际请求的地址,页面刷新会基于当前url向服务器请求,服务器返回当前url对应数据。如何在项目组织CSS 项目中如何引入静态文件,如图片、图表等(assets文件夹下。。。) 如何实现前后端交互的 原生的ajax如何发送请求 如何实现打包的 项目部署是咋部署的 前端工程化:前端工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化、标准化,其主要目的为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间 ,而前端工程本质上是软件工程的一种,因此我们应该从软件工程的角度来研究前端工程。 模块化,组件化,规范化,自动化 pinia待完善... vite待完善... |
CopyRight 2018-2019 实验室设备网 版权所有 |