【JavaScript】一个后端兼职Vue.js前端的开发回顾、总结
前言: 即将不碰前端了, 仅以此文献给将来的自己, 为那时省些力气,总的而言是挺有意思的学习之旅,不算一无所获。
一. 开始之前
1.历史: 如同Java 发展至今日, 拥有了自己成熟的包管理系统, IDE, 调试开发环境, 开源库 等等完备的生态系统一样, js历经几十年的发展, 也不是那个脆弱的, 依靠浏览器活着的语言.
尤其是新的 fs 模块也引入了对本地文件IO的处理,使其更加地“本地化”
2.JavaScript 与 Java
JavaScript 真的一无可取吗? 不是的. 作为一门天生异步的语言, 现如今各大js引擎的优化已经让它做到了不慢, 综合速度不落下风 :
想象你要写个小工具, 没工夫去画流程图, 做详尽的设计, 需要快速的调试, 尽快实现想法.
well, js 的优势体现的淋漓尽致, 你不需要一个 IDE, 也不需要配环境变量, 调试很多东西, 还要找一台足够快的电脑避免影响效率. 统统不需要 , 你只需要找个浏览器, F12, 或者cmd -> node 把用nodepad++ 写的代码直接贴进去即可。
又或是, 你在写正则, 想试试某个规则能否匹配到自己想要的东西;
又或是, 你想知道算法A和算法B哪个更快, 等等.
可能jshell靠着“抄袭”Scala shell 追上来了一点点, 但体验仍旧输了一截。
试图理解二者的区别,以及Js为何在效率上更胜一筹,还是从几个日常的例子入手。
这里是一段Java想要使用异步-回调模式需要写的代码:
FutureTask<Object> t=new FutureTask<>(new Callable() { @Override public Object call() throws Exception { // can throw exceptions System.out.println("executing ..."); return null; } }); // service.submit(t);// runs in executor // t.run();// runs in current thread. new Thread(t).start(); t.get();// if not running, block
然后你还要注意: FutureTask的默认run()是在本线程, 自己一时犯蠢忘掉又是麻烦.
而对比之下, 这是js的
function executeTask0(id){ new Promise(()=>{ exeFor2sec(); }).then(console.log('task ' + id + ' complete')) }
另外一点是, 它对函数式的支持允许了更灵活的实现, 因而不需要复杂的设计去实现想法.
或是你想要判断回调在不在 ?
if(callback) 即可
function somefuc(callback){ if(callback){ callback() } }
假如是Java7 , 你还需要使用匿名对象去实现回调. Java8 ? 语法糖罢了, 不但需要思维转换, 还要记住被匿名的对象有几个参数要传
同样以异步回调为例:
Future<?> submit = service.submit(()->{
...
});
看上去足够简单了,实际上你仍要记住,submit传入的是Runnable,然后runnable 的run()不需要任何入参。
或是你想要个对象 var a ={} 即可, 也不需要声明Class,Nothing.
一些常用函数Js也给出了更便捷的处理方式
如:判断是不是数值 ?
if(Number(x)){ console.log('x is a number') }
而Java则需要
boolean isNum = true ; try{ Integer.valueOf(x); Double.valueOf(x); }catch(NumberFormatException e){ // 还要考虑不能捕获全体, 否则会漏掉一些情况 isNum= false; } System.out.println(" x is a number")
同时,JS还支持许多好玩的特性
如:柯里化
柯里化来源应该是函数式编程的学术定义,甚至不需要怎么理解,只要记住一个大前提,js函数是可以作为返回值的
于是我们有:
(x=>x+2)(2) == 4 // true xqcl
那么上述 x+2 作为新函数返回后:
(y=>(x=>x+y)(2))(2) == 4
其中,内层 x + y 在经过第一次运算后,成为 2+y,整体作为函数返回给外部函数去运算,于是有 2 + 2 = 4
如:解构赋值
结构赋值允许我们在给对象赋值时无需依赖诸如反射等手段,直接将值赋值给目标对象。如:
var a, b, rest; [a, b] = [10, 20]; console.log(a); // 10 console.log(b); // 20 [a, b, ...rest] = [10, 20, 30, 40, 50]; console.log(a); // 10 console.log(b); // 20 console.log(rest); // [30, 40, 50] ({ a, b } = { a: 10, b: 20 }); console.log(a); // 10 console.log(b); // 20
如:融入语言的异步模型,
Javascript 提出了一个模型,也即事件循环,这种设计常见于游戏引擎,当然也存在于操作系统中,只不过不会这么简单。通过事件循环,js 得以把大部分操作交由一个队列处理,而主线程会持续不断地询问这个队列有没有任务,从而屏蔽掉线程地概念,没有了线程,就没有了并发问题。
对于这个队列,Nodejs如是说到:
当调用 setTimeout() 时,浏览器或 Node.js 会启动定时器。 当定时器到期时(在此示例中会立即到期,因为将超时值设为 0),则回调函数会被放入“消息队列”中。在消息队列中,用户触发的事件(如单击或键盘事件、或获取响应)也会在此排队,然后代码才有机会对其作出反应。 类似 onLoad 这样的 DOM 事件也如此。事件循环会赋予调用堆栈优先级,它首先处理在调用堆栈中找到的所有东西,一旦其中没有任何东西,便开始处理消息队列中的东西。
然后随着Es6的提出,工作队列被引入,另一种特殊API被引入(Promise),通过使用 promise 并为其准备回调函数,可以将任务异步提交,并立即执行。
new Promise(()=>{ exec(); }).then(console.log('task ' + id + ' complete'))
提到这点不得不提一句 await 关键字,上文提到,2处异步事件不在同一处队列中运行,自然也不对应着同一条线程,但如果,如果,真的要等待 ,就只能通过 await 关键字,通过声明一个函数是 async 的,你可以等待特定的await 函数执行结束。
而且神奇的是,promise中的setTimeout() 放入的函数,也将被等待。
async function executeTask(id){ // await can only be used inside a async task // it will await till set Timeout Complete await new Promise(resolve => { setTimeout(() => { resolve('resolved'); }, 2000); }) console.log('task ' + id + ' complete') }
具体见这几篇文章,详细地说明了上述的问题,并且阐述了一些技巧
二. 开发环境搭建
-
package.json
以配置文件引入, 现代js离不开包管理, 彼时的js仍是html的附属脚本, 严重依赖全局变量, 每一个外部自定义函数, 都需要不厌其烦地引入, 比如vue官网的快速开始,
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
但以这种方式, 难免有部分引入的函数没有被使用到。
在网络上, 它吃掉了不必要的带宽 (尽管有 xx-min.js的方式去尽量减少带宽使用, chrome 等现代浏览器为了优化这一部分也做出了本地缓存的努力;
而对于网页本身, 则耗费了不必要的加载时间, 于是, 类似webpack等打包技术应运而生.
随着js的逐渐发展, 谷歌将v8 引擎单独独立出来, js也从浏览器中“打破了第四面墙”, 来到外面的世界, 成了独立的顶级项目 Node.js , nodejs 可能早生几年, 就没python什么事了.
随着node 一同而来的, 还有npm包管理系统.
node.js 的安装包自带npm包管理工具,只要不刻意勾选掉,按照默认安装就可以了。
npm 依靠package.json来查找需要的依赖。
默认情况下,
如果当前文件夹存在 package.json ,npm install 将安装依赖到当前文件夹下的node_modules文件夹中(没有的话会自动创建)。
如果当前文件夹下不存在package.json,则将安装到当前用户文件夹下 (linux: ~/)
大多数时候我们并不需要手动写下面这个文件, 只需要运行npm install package_name 即可, 着实有 apt/yum 的感觉了.
{ "name": "test", "version": "4.5.0", "private": true, "scripts": { "dev-online": "vue-cli-service serve --open --host 0.0.0.0 --port 9000", "dev": "vue-cli-service serve --open", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "eslint": "eslint --fix --ext .js,.vue src" }, "dependencies": { "axios": "^0.19.0", "core-js": "^2.6.5", "cropperjs": "^1.5.4", "echarts": "^4.7.0", "element-ui": "^2.13.1", "jsencrypt": "^3.0.0-rc.1", "marked": "^1.2.7", "view-design": "^4.0.2", "vue": "^2.6.10", "vue-router": "^3.1.3", "vuex": "^3.0.1" }, "devDependencies": { } }
如, 为你的前端项目引入vue的简单方式: $ npm install vue # 最新稳定版
-
lanuch.json
来到第二步, vscode, 作为后现代IDE, vscode吸取了之前各家的教训, 奠定了以插件为本\ 统一入口, 简化UI 的总体逻辑
任何时候, 忘了功能在哪, Ctrl + Shift + P
缺了功能, 去插件商店看看即可。
它需要注意的地方不多, 总体而言只需要如下面的运行配置即可.
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "<node_internals>/**" ], "program": "${workspaceFolder}\\AsyncFunctions.js" } ] }
-
babel
前端代码“不需要编译”,这是常识,然而也带来了一些缺点,比如编译期检查,如Java等,现代IDE如 eclipse或idea通过即时编译提供报错信息,辅助开发者发现代码中的错误,
而Js IDE则基本无法提供这点,即使是vscode也是如此。Babel的出现缓解了这个问题,
实际上,vue-cli 的build 可以通过babel提供兼顾所有最新的 ES2015+ 语言特性,通过下列命令提供
vue-cli-service build --modern
-
eslint
它有自作聪明的点,比如它极其不待见 ==,同时不允许无空格 ,但大部分是可取的,常用来检查 js 中不符合规范的写法。同时 elint 也提供了 --fix等一键式命令辅助代码规范
三. 主角: Vue
Vue, 将前端重复性劳动简化为组件, 同时保证了足够简单.
提到Vue,就不得不说到 Thymeleaf\jsp\freemarker 等后端模板化语言,freemarker严格意义上是xml 模板语言,并不限于html。
相比 Thymeleaf 等模板语言靠服务器端生成html, 也可以弄成组件化, 但仅针对html而言, 不能(不建议)额外绑定js 函数\ 事件 等, 从用户角度还是不如vue 这种前端组件化.
JSP 过于容易将业务和代码及页面效果强耦合的“特点”, 也是如今不被推荐的最大原因。
1.组件\内置组件\slot
Vue由组件组成,每个组件都由2部分组成。
一是HTML
二是vue对象
var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
1.1 引用组件$refs
在我们引入组建后,$refs 使得父组件可以直接访问子组件的方法、字段等(尽管不建议这么干)
this.$refs.select0.setQuery(null)
-
组件绑定:
双向绑定:v-model
常用于表单输入,因为此时较有可能出现这样的需求:输入某字段时显示预览效果。
单向绑定:v-bind 常以语法糖的形式出现 :value="some"
引入:事件 $emit()
-
生命周期
同安卓每个activity的生命周期一样,想介入vue的渲染过程,可以使用 vue 提供的生命周期回调。
如created(最常用)
created: function () { // `this` 指向 vm 实例 console.log('a is: ' + this.a) }
具体生命周期如下,回调逻辑同上。
-
监听函数:用来针对某些值更新需要触发额外操作
watch: { id: function (new_id, old_id) { updateOptions(new_id); } }
-
生命周期函数无法解决的问题:页面初始化时,单个子组件渲染完成后自动加载
1.2 内置组件
常见于各大UI框架,如elemet-UI, iview等。
1.3 slot
slot可以看作是简易便捷的渲染函数,通过 <template/> 元素定义的slot可以在随后使用,
<template #header> <h1>Here might be a page title</h1> </template>
vue将自动替换下文中的 <slot></slot>
<header> <slot name="header"></slot> </header>
1.4 组件化带来的思路变化:
由于vue组件为王, 组件是最小单元,这样避免了一部分问题,如全局变量污染问题,但是从现在开始,组件间的互操作就不那么容易了。
比如两个下拉框,前一个选中后,后一个根据前一个值筛选下拉框内容。
如果是原生js,或者jQuery,思路会是这样子:
第一个下拉框值选中后,更新第二个下拉框的内容(<option>)
但如果切换成vue则要转变思维,
页面本身是一个组件,而两个下拉框是2个子组件,于是涉及到2个问题,子组件间通信,和父子组件通信。
子组件A需要把值更新到父组件的某个参数,
function onChange(id){ this.$emit('update:id', id) }
同时另一个子组件B绑定内部值到那个参数,并在内部监听该参数。
watch: { id: function (new_id, old_id) { updateOptions(new_id); } }
于是我们有:
tricks: 两个控件绑定同一个值,触发彼此刷新
<choose-a :value.sync="formData.id" /> <!-- tricks 两个控件绑定同一个值,触发彼此刷新,此处为id--> <choose-b :innerId="formData.id" />
2.”响应式“
vue框架负责对dom元素建立watcher监听,所以对其内部实现的推测是:解析模板,然后根据v-if 等标签,解析完成后,根据解析结果(抽象语法树?)建立创建对各dom元素的监听。
至于虚拟dom其实不需要深入了解,只需要知道是异步更新就够了。
内容详见:
3.路由
意义: 减少跳转\iframe嵌套等复杂化业务的东西.
常见用法
this.$router.push('/weekText')
4.Vuex
如前文所说,vue在引入了模块化之后,解决了全局变量的问题,但是此时,如果仍需要“old fashion”,你就需要vuex 了。通过将vuex 注册为一个组件,再在别处引用,vuex可以作为前端全局变量的所在地,具体实现细节不需要太关心。
new Vuex.Store({ modules: { app, user, data } })
5.Es6模块
使用Babel后可以放心的使用es6 语法作为vue的组件注册语法。
6.Vue-cli
目前使用的不多,主要是vue-cli-sevice 开启node.js 服务器,从而前端可以单独启动。