在此前我使用的前端框架是 Angular,使用过 TypeScript 后你就会讨厌 JS 了,我学习 Vue 时的最新版本是 2.5,相信大部分同学都不会认为 Vue 那样又细又长的代码很美观吧,简单看了一些网络博客后,我毅然决然引入了 TypeScript 进行开发,本文仅整理记录我自己遇到的一些坑。
使用 Cli
脚手架是一个比较方便的工具,这里需要注意的是@vue/cli和vue-cli是不一样的,推荐使用npm i -g @vue/cli安装。
安装完成后,可以直接使用vue create your-app创建项目,你可以选择使用默认配置亦或是自己手动选择配置,按提示一步一步向下走即可,它会根据你的选择自己创建比如tsconfig.json等等配置文件。这里推荐使用less开发样式,sass老是在安装的过程中出问题。
当然你也可以使vue ui命令启动一个本地服务,它是一个 Vue 项目管理器,提供了一个可视化的页面供你管理自己的项目,它的样子如下图所示,还是比较清新的。
使用 vue-property-decorator
Vue 官方维护了 vue-class-component 装饰器,vue-property-decorator 则是在vue-class-component基础上增强了更多结合Vue特性的装饰器,它可以让 Vue 组件语法在结合了 TypeScript 语法后变得更加扁平化。
截止本文时间,vue-property-decorator共提供了 11 个装饰器和 1 个Mixins方法,下面用@Prop举个例子,是不是看起来引起极度舒适。
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
// 上面的内容将会被解析成如下格式
export default {
props: {
propA: {
type: Number
},
propB: {
default: 'default value'
},
propC: {
type: [String, Boolean]
}
}
}
使用 Vuex
关于怎么使用Vuex此处就不再做过多说明了,需要注意的一点是,如果你需要访问$store属性的话,那么你必须得继承Vue类,坑的地方是在某些情况下即使你没有继承Vue,它也能通过编译,只有在程序运行起来的时候才报错。
class ExampleApi extends Vue {
public async getExampleData() {
if (!this.$store.state.exampleData) {
const res = await http.get('url/exampleData');
if (res.result) {
this.$store.commit('setExampleData', res.data);
return res.data;
} else {
promptUtil.showMessage('get exampleData failed', 'warning');
}
} else {
return this.$store.state.exampleData;
}
}
}
使用自己的配置(含代理)
vue.config.js是一个可选的配置文件,如果项目的根目录中存在这个文件,那么它会被@vue/cli-service自动加载,它的配置项说明可以查看配置参考。
我们再开发过程中都会使用代理来转发请求,代理的配置也是在这个文件中,它的官方说明在devserver-proxy中,下面是一个简单的vue.config.js文件例子。
module.exports = {
filenameHashing: true,
outputDir: 'dist',
assetsDir: 'asserts',
indexPath: 'index.html',
productionSourceMap: false,
transpileDependencies: [
'vue-echarts',
'resize-detector'
],
devServer: {
hotOnly: true,
https: false,
proxy: {
"/statistics": {
target: "http://10.7.213.186:3889",
secure: false,
pathRewrite: {
"^/statistics": "",
},
changeOrigin: true
},
"/mail": {
target: "http://10.7.213.186:8888",
secure: false,
changeOrigin: true
}
}
}
}
让 Vue 识别全局方法和变量
我们在项目中都会使用一些第三方 UI 组件,比如我自己就使用了 Element,但是在使用它的$message、$notify等方法时就直接报错了,究其原因就是$message等属性并没有在 Vue 实例中声明。
官方对此给出了很明确的解决方案,使用的是 TypeScript 的 模块补充特性,可以查看增强类型以配合插件使用。既然知道是因为没有声明导致的错误,那我们就给它声明一下好了,在src/shims-vue.d.ts文件中添加如下代码即可,如果没有该文件请自行创建。
看到网上也有一部分人说的是src/vue-shim.d.ts,反正不管是怎么命名这个文件的,它们的作用是一样的。
declare module 'vue/types/vue' {
interface Vue {
$message: any,
$confirm: any,
$prompt: any,
$notify: any
}
}
这里顺道提一下,src/shims-vue.d.ts文件中的如下代码是为了让你的 IDE 明白以.vue结尾的文件是什么玩意儿。
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
路由懒加载
Vue Router 官方有关于路由懒加载的说明,但不知道为什么官方给的这个说明在我的项目里面都没有生效,但使用require.ensure()按需加载组件可以生效。
// base-view 是模块名,写了相同的模块名则代码会被组织到同一个文件中
const Home = (r: any) => require.ensure([], () => r(require('@/views/home.vue')), layzImportError, 'base-view');
// 路由加载错误时的提示函数
function layzImportError() {
alert('路由懒加载错误');
}
上面的方式会在编译的时候把文件自动分成多个小文件,编译后的文件会以你自己命名的模块名来命名,如果代码之间有相互依赖,依赖部分代码编译后的文件会以两个模块名相连后进行命名。
但是需要注意的是,这样拆分小文件之后引入了另外一个新的问题,因为客户端会缓存这些编译后的 js 文件,如果功能 A 同时依赖了a.js和b.js两个文件,但用户在使用其它功能时已经把a.js缓存到本地了,使用功能 A 时需要请求b.js文件,这时程序就很容易报错,因为此时在客户端这两个文件不是同一个版本,所以可能导致a.js调用b.js中的方法已经被删了,进而导致客户端页面异常。
关于引入第三方包
项目在引入第三方包的时候经常会报出各种奇奇怪怪的错误,这里仅提供我目前找到的一些解决办法。
/*
引入 jquery 等库可以尝试下面这种方式
只需要把相应的 js 文件放到指定文件夹即可
**/
const $ = require('@/common/js/jquery.min.js');
const md5 = require('@/common/js/md5.js');
引入一些第三方样式文件、UI 组件等,如果引入不成功可以尝试建一个 js 文件,将导入语句都写在 js 文件中,然后再在main.ts文件中导入这个 js 文件,这个方法能解决大部分的问题。例如我先建了一个lib.js,然后在main.ts中引入lib.js就没有报错。
// src/plugins/lib.js
import Vue from 'vue';
// 树形组件
import 'vue-tree-halower/dist/halower-tree.min.css';
import {VTree} from 'vue-tree-halower';
// 饿了么组件
import Element from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// font-awesome 图标
import '../../node_modules/font-awesome/css/font-awesome.css';
import VueCookies from 'vue-cookies';
import VueJWT from 'vuejs-jwt';
Vue.use(VueJWT);
Vue.use(VueCookies);
Vue.use(VTree);
Vue.use(Element);
// src/main.ts
import App from '@/app.vue';
import Vue from 'vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
import './plugins/lib';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
因为第三方包写的各有特点,在引入不成功的时候基本也只能是见招拆招,当然如果你的功底比较深厚,你也可以自己写一个index.d.ts文件,实在不行的话,那个特殊的组件不使用 TypeScript 来写也能解决,我目前还没有找一个可以完全解决第三方包引入错误的方法,如果您已经有相关的方法了,希望能与你一起探讨交流。
Read More ~
标签:#
TypeScript
Bootstrap-table 如何合并相同单元格
Bootstrap-table 官方提供了合并单元格方法 mergeCells,它根据四个参数可以合并任意个单元格,我们要做的只是告诉它怎么合并。
要合并同一列相同的单元格,无非两种办法,一种是一边遍历一边合并,遍历完了再合并。这里采用第二种办法,这里不需要遍历所有数据,因为用户只能看到当前页的数据,所以只遍历当前页的数据更省时间。
下面是我实现的获取合并信息算法,最终返回的是一个哈希表,比如下面的这个表格,如果要对「性别」这一列进行合并,很明显前面两个“男”需要合并成一个单元格,再去看下 Bootstrap-table 提供的 API,它需要的是从哪个单元格开始,合并多少个单元格,也就是它需要的是两个数值类型的参数。
姓名
性别
年龄
张三
男
23
李四
男
19
王二
女
20
麻子
男
21
所以我把哈希表设置为,键存的是索引,值存的是从这个索引开始后面连续有多少个和它一样的单元格,那么上述表格性别这一列所得到的合并信息哈希表就为:
{
0: 2,
2: 1,
3: 1
}
下面算法很简单,使用两个指针遍历指定的列,如果两个指针所指向的数据相同,那么就将键所对应的值进行加一操作,整个方法只会对该列数据遍历一边,所以时间复杂度为 O(n)。
let getMergeMap = function (data, index: number) {
let preMergeMap = {};
// 第 0 项为表头,索引从 2 开始为了防止数组越界
for (let i = 2; i < data.length; i++) {
let preText = $(data[i-1]).find('td')[index].innerText;
let curText = $(data[i]).find('td')[index].innerText;
let key = i - 2;
preMergeMap[key] = 1;
while ((preText == curText) && (i < data.length-1)) {
preMergeMap[key] = parseInt(preMergeMap[key]) + 1;
i++;
preText = $(data[i - 1]).find('td')[index].innerText;
curText = $(data[i]).find('td')[index].innerText;
}
// while循环跳出后,数组最后一项没有判断
if (preText == curText) {
preMergeMap[key] = parseInt(preMergeMap[key]) + 1;
}
}
return preMergeMap;
}
上述算法得到了单列数据的合并信息,下一步就是按照这个信息进行相同单元格的合并了,因此封装了下面的方法按照指定哈希表进行合并。
let mergeCells = function (preMergeMap: Object, target, fieldName: string) {
for (let prop in preMergeMap) {
let count = preMergeMap[prop];
target.bootstrapTable('mergeCells', { index: parseInt(prop), field: fieldName, rowspan: count });
}
}
到目前为止,我们实现的都只是对单列数据进行合并,要实现对多列数据进行合并,那么只需要对所有列都进行相同的操作即可。
export let mergeCellsByFields = function (data: Object[], target, fields) {
for (let i = 0; i < fields.length; i++) {
let field = fields[i];
// 保证 field 与 i 是相对应的
let preMergeMap = getMergeMap(data, i);
let table = target.bootstrapTable();
mergeCells(preMergeMap, table, field);
}
}
因为我在程序中做了一点处理,保证了fields中每个值得索引与对应表头的索引是一样的,因此不需要额外传入索引信息。简单来说就是我所实现的表格会根据fields的顺序,实现列之间的动态排序。你需要注意的是这一点很可能和你不一样。
到现在已经能够合并所有的列了,查看 Bootstrap-table 的配置信息发现,它有个属性是 onPostBody 它会在 table body 加载完成是触发,所以把这个属性配置成我们的合并单元格方法即可。
// groups 为要合并的哪些列
onPostBody: function () {
mergeCellsByFields($('#table' + ' tr'), $('#table'), groups);
}
再说一点不太相关的,我实现的是让用户可以自己选可以合并多少列,即用了一个可多选的下拉列表框供用户选择,根据用户选择的数量去合并,所以传入了一个groups参数。
最后推荐一个排序插件 thenBy,你可以用它进行多字段排序,比如用在合并相同单元格的场景,在绘制表格前先对数据进行排序,那么最后合并的结果就是把所有相同的数据聚合到一起了,并且还将它们合并到一起了,起到了一个隐形的过滤查询功能。
Read More ~
学习 Angulr 容易忽略的知识点
参考内容:
《Angulr5 高级编程(第二版)》
函数声明式和表达式
// 第一种:函数声明式
myFunc();
function myFunc(){
...
}
// 第二种:函数表达式
myFunc();
let myFunc = function(){
...
}
虽然上面两种函数声明方式在大部分情况下是一样的,第一种可执行,第二种却不可以执行,这是因为浏览器在解析 js 时找到函数声明,并在执行剩余语句之前设置好函数,此过程称为函数提升,但是函数表达式却不会受到提升,因此无法正常工作。
js 不具备多态性
js 重不能创建名称相同但参数不同的两个函数,它不具备这个多态性,比如你定义的函数中有两个形参,调用函数时只传一个参数,第二形参的值就是 undefined ,如果传的参数大于 3 个,那么会自动忽略多余的参数。可以使用下列方法来处理函数定义参数数量和用于调用函数实际参数数量之间不匹配的问题。
// 使用默认参数
let func = function(age, sex='男'){
...
}
func(23);
// 使用可变长参数
let func = function(age, sex, ...extraArgs){
...
}
func(23, '女', '张三', '深圳');
// 最后一个参数是一个数组,任何额外的实参都会被赋给这个数组
let 和 war 的区别
使用 let 和 var 声明变量的区别,使用 let 声明变量会把变量的作用范围限定在它所在的代码区域内。而使用 var 所创建的变量的作用域是它所在的函数。
function func(){
if(false){
var age = 23;
}
}
// 上面的代码会被解析成下面的形式,使用 let 则不会出现这样的结果
function func(){
var age;
if(false){
age = 23;
}
}
相等 == 和恒等 === 以及 连接操作符 +
相等操作符尝试将操作数强制转换为相同的类型,再评估是否相等,实质上相等操作符==是测试二者的值是否相等,而与二者的类型无关;如果要测试值和类型是否都相等则应该用恒等操作符===。
5 == '5' // 结果为 true
5 === '5' // 结果为 false
在 js 中,连接操作符的优先级高于加法操作,也就是说5 + '5'的结果是55。
不同的模块指定方式
import { Name } from "./modules/NameUtil";// 第一种
import { Compont } from "@angular/core";// 第二种
上面两种导入模块的方式有所不同,第一种是相对模块,第二种是非相对导入。第一种告诉的 TypeScript 编译器,该模块所在的位置是相对于包含 import 语句的文件而言;第二种非相对导入,编译器会用 node_modules 文件夹中的 npm 包来解析它。
如果在导入模块时,出现需要导入两个不同模块但是名字却相同的情况,可以使用as关键字给导入的模块取一个别名。
import { Name as otherName } from "./modules/Name";//取别名
还有一种方法是将模块作为对象导入,如下 import 所示,导入 Name 模块的内容,并创建一个名为 otherName 的对象,然后就可以使用该对象的属性了。
import * as otherName from "./modules/NameUtil";
let name = new otherName.Name("Admin", "China");// Name 是 NameUtil 中的类
多类型和类型断言
在 ts 中允许指定多个类型,使用字符|进行分隔。看下面的的方法,其功能是把华氏温度转换为摄氏温度。
// 使用多类型,该函数可以传入 number 和 string 类型的参数
static convertFtoC(temp: number | string): string {
/*
尝试使用 <> 声明一个类型断言,将一个对象转换为指定类型,也可以使用 as 关键字实现下列相同的效果
let value: number = (temp as number).toPrecision ? temp as number : parseFloat(temp as string);
*/
let value: number = (<number>temp).toPrecision ? <number>temp : parseFloat(<string>temp);
return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
元组是固定长度的数组,数组的每一项都是指定的类型;可索引类型可以将键与值关联起来,创建类似于 map 的集合。
// 元组
let tuple: [string, string, string];
tuple = ["a", "b", "c"];
// 可索引类型
let cities: {[index: string] : [string, string]} = {};
cities["Beijing"] = ["raining", "2摄氏度"];
数据绑定
[target]="expr"// 方括号表示单向绑定,数据从表达式流向目标;
(target)="expr"// 圆括号表示单向绑定,数据从目标流向表达式,用于处理事件的绑定;
[(target)]="expr"// 圆方括号组合表示双向绑定,数据在表达式与目标之间双向流动;
{{ expression }}// 字符串插入绑定。
[] 绑定有很多不同的形式,下面介绍不同表现形式的效果。
<!--
标准属性绑定(dom对象有的属性),将 input 的 value 属性绑定到一个表达式的结果
因为 model.getProduct(1) 可能返回 null ,所以使用模板空条件操作符 ? 浏览返回结果
如果返回不为空,那么将读取 name 属性,否则由 null 合并操作符 || 将结果设置为 None
字符串插入绑定也可以使用这种表达式
-->
<input [value]="model.getProduct(1)?.name || 'None'">
<!--
元素属性绑定,有时候我们需要绑定的属性在 DOMAPI 上面没有
可以使用通过在属性名称前加上 attr 前缀的方式来定义目标
-->
<td [attr.colspan]="model.getProducts().length">
{{ model.getProduct(1)?.name || 'None' }}
</td>
<!-- 还有其他的 ngClass,ngStyle 等绑定,理解大体上和上面差不多 -->
内置指令
<!--
ngIf指令,如果表达式求值结果为 true ,那么 ngIf 将宿主元素机器内容包含在 html 文件中
指令前面的星号表示这是一条微模板指令
组要注意的是,ngIf 会向 html 中添加元素,也会从中删除元素,并非只是显示和隐藏
如果只是控制可见性,可以使用属性绑定挥着样式绑定
-->
<div *ngIf="expr"></div>
<!--
ngSwitch指令,
-->
<div [ngSwitch]="expr">
<span *ngSwitchCase="expr"></span>
<span *ngSwitchDefault></span>
</div>
<!--
ngFor指令,见名知意,为数组中的每个对象生成同一组元素
ngFor 指令还支持其他的一系列可赋给变量的值,有如下局部模板变量
index:当前对象的位置
odd:如果当前对象的位置为奇数,那么这个布尔值为 true
even:同上相反
first:如果为第一条记录,那么为 true
last:同上相反
-->
<div *ngFor="let item of expr; let i = index">
{{ i }}
</div>
<!--
ngTemplateOutlet指令,用于重复模板中的内容块
其用法如下所示,需要给源元素指定一个 id 值
<ng-template #titleTemplate>
<h1>我是重复的元素哦</h1>
</ng-template>
<ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
...省略若万行 html 代码
<ng-template [ngTemplateOutlet]="titleTemplate"></ng-template>
-->
<ng-template [ngTemplateOutlet]="myTempl"></ng-template>
<!--
下面两个指令就是见名知意了,不解释
-->
<div ngClass="expr"></div>
<div ngStyle="expr"></div>
事件绑定
事件绑定使用 (target)="expr",是单向绑定,数据从目标流向表达式,用于响应宿主元素发送的事件。
当浏览器触发一个时间时,它将提供一个对象来描述该事件,对于不同类型的事件有不同类型的事件对象,事件对象被赋给一个名为$event的模板变量,但是所有事件对象都有下面三个属性:
type:返回一个 string 值,用于标识已触发事件类型;
target:返回触发事件的对象,一般是 html元素对象。
timeStamp:返回事件触发事件的 number 值,用 1970.1.1 毫秒数表示。
下面举几个例子,作为理解帮助使用。
<!-- 当数鼠标在上面移动时,就会触发 mouseover 事件 -->
<td *ngFor="let item of getProducts()" (mouseover)="selectedProduct = item.name"></td>
<!-- 当用户编辑 input 元素的内容时就会触发 input 事件 -->
<input (input)="selectedProduct=$event.target.value" />
<input (keyup)="selectedProduct=product.value" />
<!-- 使用事件过滤,上面的写法按下任何一个键都会触发事件,而下面的写法只有回车事件才会触发事件 -->
<input (keyup.enter="selectedProduct=product.value") />
表单验证
Angular 提供了一套可扩展的系统来验证表单元素的内容,总共可以向 input表元素中添加 4 个属性,每个属性定义一条验证规则,如下所示:
required:用于指定必须填写值;
minlength:用于指定最小字符数;
maxlength:用于指定最大字符数,(不能在表单元素直接使用,因为它与同名的 H5 属性冲突);
pattern:该属性用于指定用户填写的值必须匹配正则表达式
<!--
Angular 要求验证的元素必须定义 name 属性
由于 Angular 使用的验证属性和 H5 规范使用的验证属性相同,
所以向表单元素中添加 novalidate 属性,告诉浏览器不要使用原生验证功能
ngSubmit 绑定表单元素的 submit 事件
-->
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<button type="submit">提交</button>
</form>
Angular 提供了 3 对验证 CSS 类,这些类可以用于样式化表单元素,向用户提供验证反馈,具体说明如下所示。
ng-untouched ng-touched:如果一个元素未被用户访问,就将其加入到 nguntouched 类中;一旦访问就加入到 ngtouched 类中。
ng-prisstine ng-dirty:元素内容没有被改变被加入到 ng-prisstine 类中,否则将其加入到 ng-dirty 类中。
ng-valid ng-invalid:如果满足验证规则定义的条件,就加入到 ng-valid 类中,否则加入到 ng-invalid 类中。
在实际使用过程中,直接定义对应的样式即可,如下所示:
<style>
input.ng-dirty.ng-invalid{
border: 2px solid red;
}
input.ng-dirty.ng-valid{
border: 2px solid green;
}
</style>
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<button type="submit">提交</button>
</form>
上面的验证方式无法给用户提供更加具体的信息,用户不知道应该做什么,可以使用 ngModel 指令来访问宿主元素的验证状态,当存在验证错误的时候,使用该指令向用户提供指导性信息。
<form novalidate (ngSubmit)="addProduct(newProduct)">
<input class="form-control"
#nameRef="ngModel"
name="name"
[(ngModel)]="newProduct.name"
required
minlength="5"
pattern="^[A-Za-z]+$" />
<ul class="text-danger list-unstyled"
*ngIf="name.dirty && name.invalid">
<li *ngIf="name.errors?required">
you must enter a product name
</li>
<li *ngIf="name.errors?.pattern">
product name can only contain letters and spases
</li>
<li *ngIf="name.errors?minlength">
<!--
Angular 表单验证错误描述属性
required:如果属性已被应用于 input 元素,此属性返回 true
minlength.requiredLength:返回满足 minlength 属性所需的字符数
minlength.actualLength:返回用户输入的字符数
pattern.requiredPattern:返回使用 pattern 属性指定的正则表达式
pattern.actualValue:返回元素的内容
-->
product name must be at least {{ name.errors.minlength.requiredLenth }} characters
</li>
</ul>
<button type="submit">提交</button>
</form>
如果在用户尝试提交表单时就显示大量的错误信息,给人的体验感就会很差,所以可以让用户提交表单时再验证整个表单,示例代码如下所示。
export class ProductionCompont {
// ...省略若万行代码
formSubmited: boolean = false;
submitForm(form: ngForm) {
this.formSubmited = true;
if(form.valid) {
this.addProduct(this.newProduct);
this.newProduct = new Product();
form.reset();
this.formSubmited = true;
}
}
}
<form novalidate #formRef="ngForm" (ngSubmit)="submitForm(formRef)">
<div *ngIf="formsubmited && formRef.invalid">
there are problems with the form
</div>
<!-- 禁用提交按钮,验证成功提交按钮才可用 -->
<button [disabled]="formSubmited && formRef.valid">提交</button>
</form>
fromSubmited 属性用于指示表单是否已经提交,并将用于在用户提交整个表单之前阻止表单验证。当用户提交表单时,调用 submitForm 方法,并将 ngForm 对象作为实参传入,ngForm 提供了 reset 方法,该方法可以重置表单的验证状态,使其返回到最初的未访问状态。
更高级的还有使用基于模型的表单验证,可以自行查阅相关资料。
使用 json-server 模拟 web 服务
因为json-server会经常用到,建议使用全局安装命令npm install -g json-server。因为开发后端的同学太慢了,而我们如果要等他们把接口都提供给我们的时候再开发程序的话,那效率就太低了,所以使用 json-server 来模拟后端服务。只需要建好一个 json 文件,比如下面的格式:
{
"user" : [
{
"name" : "张三",
"number" : "1234",
},
{
"name" : "王二",
"number" : "5678",
}
],
"praise": [
{"info":"我是一只小老虎呀!"},
{"info":"我才是大老虎"}
]
}
启动服务使用命令json-server [你的 json 文件路径],然后就可以根据提示访问了,你甚至可以使用http://localhost:3000/user?number=5678去过滤数据。这样就能模拟 web 服务,而不必等后端同学的进度了。
解决跨域请求问题
Angular 跨域请求问题可以通过 Angular 自身的代理转发功能解决,在项目文件夹下新建一个 proxy.conf.json 并在其中添加如下内容。
// 可以通过下列配置解决
"/api": {
"target": "http://10.9.176.120:8888",
}
在启动时使用npm start,或者使用ng serve --proxy-config proxy.conf.json,Anular 中的/api请求就会被转发到 http://10.9.176.120:8888/api,从而解决跨域请求问题。
使用第三方 js 插件
共有三种方式引入第三方插件,第一种很简单,直接在 html 中引入插件就可以了;第二种在angular.json中进行配置;第三种在 ts 文件中使用 import 导入库即可。
// 第一种(需要重启服务)
"scripts": ["src/assets/jquery-3.2.1.js","src/assets/jquery.nicescroll.js","src/assets/ion.rangeSlider.js"]
// 第二种
<script type="text/javascript" src="assets/jquery-3.2.1.js"></script>
<script type="text/javascript" src="assets/jquery.nicescroll.js"></script>
// 第三种
import "assets/jquery-3.2.1.js";
import "assets/jquery.nicescroll.js";
import "assets/ion.rangeSlider.js";
深拷贝与浅拷贝
深拷贝与浅拷贝是围绕引用类型变量说的,其本质区别是不可变性,基本类型是不可变得,而引用类型是可变的。
直接使用赋值操作符,就是浅拷贝,如果对拷贝源进行操作,会直接影响在拷贝目标上,因为这个赋值行为本质是内存地址的赋值,为了获得与拷贝源完全相同但又不会影响彼此的对象就要使用深拷贝。
let objA = {
x: 1,
y: -1
}
let objB = objA;
objA.x++;
console.log("objA.x:"+objA.x, "objB.x:"+objB.x);
//打印结果如下:
objA.x : 2
objB.x : 2
Typescript 提供了一种方法来实现引用类型的深拷贝,即Object.assign(target, ...source),此方法接受多个参数,第一个参数为拷贝目标,剩余参数为拷贝源,同名属性会进行覆盖。
let objA = {
x: 1,
y: -1,
c: {
d: 1,
}
}
let objB = {};
Object.assign(objB, objA);
objA.x++;
console.log("objA.x:"+objA["x"], "objB.x:"+objB["x"]);
//打印结果如下:
objA.x : 2
objB.x : 1
需要注意的是,Typescript 提供的深拷贝方法不能实现嵌套对象的深拷贝,会出现下面的情况。
let objA = {
x: 1,
y: -1,
c: {
d: 1,
}
}
let objB = {};
Object.assign(objB, objA);
objA.c.d++;
console.log("objA.c.d:"+objA["c"].d, "objB.c.d:"+objB["c"].d);
//打印结果如下:
objA.c.d : 2
objB.c.d : 2
要实现嵌套对象的深拷贝,可以使用 JSON 对象提供的方法,JSON 对象提供了两个方法,分别为:stringify()和parse(),前者将对象 JSON 化,后者将 JSON 对象化,使用这种方式可以实现嵌套深拷贝,但是也有缺点:破坏原型链,不能拷贝属性值为 function 的属性。
let objA = {
a: 1,
b: {
c: 1
}
}
let objB = JSON.parse(JSON.stringify(objA));
objA.b.c++;
console.log("objA.b.c:"+objA.b.c, "objB.b.c:"+objB.b.c);
//打印结果如下:
objA.b.c:2
objB.b.c:1
Read More ~