手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」正在着手写 THE LAST TIME 系列的 Typescript 篇 而 Decorator 一直是我个人看来一个非常不错的切面方案 Decorator 是 ES7 添加的新特性 当然 在 Typescript 很早就有了

大家好,欢迎来到IT知识分享网。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

作者:Nealyang

转发连接:
https://mp.weixin..com/s/PFgc8xD7gT40-9qXNTpk7A

前言

正在着手写 THE LAST TIME 系列的 Typescript 篇,而Decorator 一直是我个人看来一个非常不错的切面方案。所谓的切面方案就是我们常说的切面编程 AOP。一种编程思想,简单直白的解释就是,一种在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是 AOP。AOP 和我们熟悉的 OOP 一样,只是一个编程范式,AOP 没有说什么规定要使用什么代码协议,必须要用什么方式去实现,这只是一个范式。而 Decorator 也就是AOP 的一种形式。

而本文重点不在于讨论编程范式,主要介绍 Typescript+Decorator 下图的一些知识讲解,其中包括最近笔者在写项目的一些应用。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

介绍

什么是 Decorator

貌似在去年的时候在公众号:【全栈前端精选】中,有分享过关于 Decorator 的基本介绍:Decorator 从原理到实战,里面有对Decorator 非常详细的介绍。

本质上,它也就是个函数的语法糖。

Decorator 是 ES7 添加的新特性,当然,在 Typescript 很早就有了。早在此之前,就有提出与 Decorator 思想非常相近的设计模式:装饰者模式。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

上图的WeaponAccessory就是一个Decorator,他们添加额外的功能到基类上。让其能够满足你的需求。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

简单的理解 Decorator,可以认为它是一种包装,对 对象,方法,属性的包装。就像 Decorator 侠,一身盔甲,只是装饰,以满足需求,未改变是人类的本质。

为什么要使用 Decorator

为什么要使用 Decorator,其实就是介绍到 AOP 范式的最大特点了:非侵入式增强。

比如笔者正在写的一个页面容器,叫 PageContainer.tsx,基本功能包括滚动、autoCell、事件注入与解绑、placeHolder Container 的添加等基本功能。

class PageContainer extends Components{ xxx }

这时候我正使用这个容器,想接入微信分享功能。或者错误兜底功能。但是使用这个容器的人非常多。分享不一定都是微信分享、错误兜底不一定都是张着我想要的样子。所以我必定要对容器进行改造和增强

从功能点划分,这些的确属于容器的能力。所以在无侵入式的增强方案中,装饰者模式是一个非常好的选择。也就是话落到我们所说的 Decorator。(对于 React 或者 Rax,HOC 也是一种很好的方案,当然,其思想是一致的。)

+ @withError + @withWxShare class PageContainer extends Components{ xxx }

我们添加 Decorator,这样的做法,对原有代码毫无入侵性,这就是AOP的好处了,把和主业务无关的事情,放到代码外面去做

关于 Typescript

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

JavaScript 毋庸置疑是一门非常好的语言,但是其也有很多的弊端,其中不乏是作者设计之处留下的一些 “bug”。当然,瑕不掩瑜~

话说回来,JavaScript 毕竟是一门弱类型语言,与强类型语言相比,其最大的编程陋习就是可能会造成我们类型思维的缺失(高级词汇,我从极客时间学到的)。而思维方式决定了编程习惯,编程习惯奠定了编程质量,工程质量划定了能力边界,而学习 Typescript,最重要的就是我们类型思维的重塑

那么其实,Typescript 在我个人理解,并不能算是一个编程语言,它只是 JavaScript 的一层壳。当然,我们完全可以将它作为一门语言去学习。网上有很多推荐 or 不推荐 Typescript 之类的文章这里我们不做任何讨论,学与不学,用或不用,利与弊。各自拿捏~

再说说 typescript,其实对于 ts 相比大家已经不陌生了。更多关于 ts 入门文章和文档也是已经烂大街了。此文不去翻译或者搬运各种 api或者教程章节。只是总结罗列和解惑,笔者在学习 ts 过程中曾疑惑的地方。道不到的地方,欢迎大家评论区积极讨论。

首先推荐下各自 ts 的编译环境:typescriptlang.org

再推荐笔者收藏的两个网站:

  • Typescript 中文网
  • 深入理解 Typescript
  • TypeScript Handbook
  • TypeScript 精通指南

Typescript 中的 Decorator 签名

interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; } declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

如上是 ClassDecorator、PropertyDecorator以及 MethodDecorator 的三个类型签名。

基本配置

由于 Decorator 在 Typescript 中还是一项实验性的给予支持,所以在 ts 的配置配置文件中,我们指明编译器对 Decorator 的支持。

在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

  • 命令行:
tsc --target ES5 --experimentalDecorators
  • tsconfig.json
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }

类型

在 Typescript 中,Decorator 可以修饰五种语句:类、属性、方法、访问器方法参数

class definitions

类装饰器应用于构造函数之上,会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

注意,在 Typescript 中的class 关键字只是 JavaScript 构造函数的一个语法糖。由于类装饰器的参数是一个构造函数,其也应该返回一个构造函数。

我们先看一下官网的例子:

 function classDecorator<T extends { new (...args: any[]): {} }>( constructor: T ) { return class extends constructor { newProperty = "new property"; hello = "override"; }; } @classDecorator class Greeter { property = "property"; hello: string; constructor(m: string) { this.hello = m; } } const greeter: Greeter = new Greeter("world"); console.log({ greeter }, greeter.hello);
手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

{ new (…args: any[]): {} }表示一个构造函数,为了看起来清晰一些,我们也可以将其声明到外面:

/ *构造函数类型 * * @export * @interface Constructable */ export interface IConstructable { new (...args:any[]):any }

properties

属性装饰器有两个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的key。

descriptor不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

 function setDefaultValue(target: Object, propertyName: string) { target[propertyName] = "Nealayng"; } class Person { @setDefaultValue name: string; } console.log(new Person().name); // 输出: Nealayng

将上面的代码修改一下,我们给静态成员添加一个 Decorator

 function setDefaultValue(target: Object, propertyName: string) { console.log(target === Person); target[propertyName] = "Nealayng"; } class Person { @setDefaultValue static displayName = 'PersonClass' name: string; constructor(name:string){ this.name = name; } } console.log(Person.prototype); console.log(new Person('全栈前端精选').name); // 输出: 全栈前端精选 console.log(Person.displayName); // 输出: Nealayng
手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

以此可以验证,上面我们说的:Decorator 的第一个参数,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

methods

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符 descriptor。

注意: 如果代码输出目标版本小于ES5,descriptor将会是undefined。

 function log( target: Object, propertyName: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => any> ) { const method = descriptor.value; descriptor.value = function(...args: any[]) { // 将参数转为字符串 const params: string = args.map(a => JSON.stringify(a)).join(); const result = method!.apply(this, args); // 将结果转为字符串 const resultString: string = JSON.stringify(result); console.log(`Call:${propertyName}(${params}) => ${resultString}`); return result; }; } class Author { constructor(private firstName: string, private lastName: string) {} @log say(message: string): string { return `${message} by: ${this.lastName}${this.firstName}`; } } const author:Author = new Author('Yang','Neal'); author.say('《全站前端精选》');//Call:say("全站前端精选") => "全站前端精选 by: NealYang"

上述的代码比较简单,也就不做过多解释了。其中需要注意的是属性描述符 descriptor 的类型和许多文章写的类型有些不同:propertyDescriptor: PropertyDescriptor。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

从官方的声明文件可以看出,descriptor 设置为TypedPropertyDescriptor加上泛型约束感觉更加的严谨一些。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

当然,官网也是直接声明为类型PropertyDescriptor的。这个,仁者见仁。

accessors

访问器,不过是类声明中属性的读取访问器和写入访问器。访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符。

如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。同时 TypeScript 不允许同时装饰一个成员的get和set访问器

 function Enumerable( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { //make the method enumerable descriptor.enumerable = true; } class Person { _name: string; constructor(name: string) { this._name = name; } @Enumerable get name() { return this._name; } } console.log("-- creating instance --"); let person = new Person("Diana"); console.log("-- looping --"); for (let key in person) { console.log(key + " = " + person[key]); }
手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

如果上面 get 不添加Enumerable的话,那么 for in 只能出来_name _name = Diana

parameters

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 参数在函数参数列表中的索引。

参数装饰器只能用来监视一个方法的参数是否被传入。

在下面的示例中,我们将使用参数装饰器@notNull来注册目标参数以进行非空验证,但是由于仅在加载期间调用此装饰器(而不是在调用方法时),因此我们还需要方法装饰器@validate,它将拦截方法调用并执行所需的验证。

function notNull(target: any, propertyKey: string, parameterIndex: number) { console.log("param decorator notNull function invoked "); Validator.registerNotNull(target, propertyKey, parameterIndex); } function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) { console.log("method decorator validate function invoked "); let originalMethod = descriptor.value; //wrapping the original method descriptor.value = function (...args: any[]) {//wrapper function if (!Validator.performValidation(target, propertyKey, args)) { console.log("validation failed, method call aborted: " + propertyKey); return; } let result = originalMethod.apply(this, args); return result; } } class Validator { private static notNullValidatorMap: Map<any, Map<string, number[]>> = new Map(); //todo add more validator maps static registerNotNull(target: any, methodName: string, paramIndex: number): void { let paramMap: Map<string, number[]> = this.notNullValidatorMap.get(target); if (!paramMap) { paramMap = new Map(); this.notNullValidatorMap.set(target, paramMap); } let paramIndexes: number[] = paramMap.get(methodName); if (!paramIndexes) { paramIndexes = []; paramMap.set(methodName, paramIndexes); } paramIndexes.push(paramIndex); } static performValidation(target: any, methodName: string, paramValues: any[]): boolean { let notNullMethodMap: Map<string, number[]> = this.notNullValidatorMap.get(target); if (!notNullMethodMap) { return true; } let paramIndexes: number[] = notNullMethodMap.get(methodName); if (!paramIndexes) { return true; } let hasErrors: boolean = false; for (const [index, paramValue] of paramValues.entries()) { if (paramIndexes.indexOf(index) != -1) { if (!paramValue) { console.error("method param at index " + index + " cannot be null"); hasErrors = true; } } } return !hasErrors; } } class Task { @validate run(@notNull name: string): void { console.log("running task, name: " + name); } } console.log("-- creating instance --"); let task: Task = new Task(); console.log("-- calling Task#run(null) --"); task.run(null); console.log("----------------"); console.log("-- calling Task#run('test') --"); task.run("test"); 

对应的输出位:

param decorator notNull function invoked method decorator validate function invoked -- creating instance -- -- calling Task#run(null) -- method param at index 0 cannot be null validation failed, method call aborted: run ---------------- -- calling Task#run('test') -- running task, name: test

@validate装饰器把run方法包裹在一个函数里在调用原先的函数前验证函数参数.

装饰器工厂

装饰器工厂真的也就是一个噱头(造名词)而已,其实也是工厂的概念哈,毕竟官方也是这么号称的。在实际项目开发中,我们使用的也还是挺多的

装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。其实说白了,就是一个函数 return 一个 Decorator。非常像JavaScript 函数柯里化,个人称之为“函数式Decorator”~

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

import { logClass } from './class-decorator'; import { logMethod } from './method-decorator'; import { logProperty } from './property-decorator'; import { logParameter } from './parameter-decorator'; // 装饰器工厂,根据传入的参数调用相应的装饰器 export function log(...args) { switch (args.length) { case 3: // 可能是方法装饰器或参数装饰器 // 如果第三个参数是数字,那么它是索引,所以这是参数装饰器 if typeof args[2] === "number") { return logParameter.apply(this, args); } return logMethod.apply(this, args); case 2: // 属性装饰器 return logProperty.apply(this, args); case 1: // 类装饰器 return logClass.apply(this, args); default: // 参数数目不合法 throw new Error('Not a valid decorator'); } } @log class Employee { @log private name: string; constructor(name: string) { this.name = name; } @log greet(@log message: string): string { return `${this.name} says: ${message}`; } }

加载顺序

一个类中,不同位置声明的装饰器,按照以下规定的顺序应用:

  • 有多个参数装饰器(parameterDecorator)时,从最后一个参数依次向前执行
  • 方法(methodDecorator)和方法参数装饰器(parameterDecorator)中,参数装饰器先执行
  • 类装饰器(classDecorator)总是最后执行。
  • 方法(methodDecorator)和属性装饰器(propertyDecorator),谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。
function ClassDecorator() { return function (target) { console.log("I am class decorator"); } } function MethodDecorator() { return function (target, methodName: string, descriptor: PropertyDescriptor) { console.log("I am method decorator"); } } function Param1Decorator() { return function (target, methodName: string, paramIndex: number) { console.log("I am parameter1 decorator"); } } function Param2Decorator() { return function (target, methodName: string, paramIndex: number) { console.log("I am parameter2 decorator"); } } function PropertyDecorator() { return function (target, propertyName: string) { console.log("I am property decorator"); } } @ClassDecorator() class Hello { @PropertyDecorator() greeting: string; @MethodDecorator() greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { } }

输出为:

I am parameter2 decorator I am parameter1 decorator I am method decorator I am property decorator I am class decorator

实战

由于是业务代码,与技术无关琐碎,只截取部分代码示意,非 Decorator 代码,以截图形式

这应该也是整理这篇文章最开始的原因了。直接说说项目(rax1.0+Decorator)吧。

需求很简单,就是是编写一个页面的容器。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

部分项目结构:

pm-detail ├─ constants │ └─ index.ts //常量 ├─ index.css ├─ index.tsx // 入口文件 └─ modules // 模块 └─ page-container // 容器组件 ├─ base //容器基础组件 ├─ decorator // 装饰器 ├─ index.tsx ├─ lib // 工具 └─ style.ts

重点看下如下几个文件

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

  • base.tsx
手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

其实是基础功能的封装

在此基础上,我们需要个能滚动的容器

  • scrollbase.tsx
手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

也是基于 Base.tsx 基础上,封装一些滚动容器具有的功能

  • style decorator
import is from './util/is'; import map from './util/map'; const isObject = is(Object); const isFunction = is(Function); class Style { static factory = (...args) => new Style(...args); analyze(styles, props, state) { return map(v => { if (isFunction(v)) { const r = v.call(this.component, props, state); return isObject(r) ? this.analyze(r, props, state) : r; } if (isObject(v)) return this.analyze(v, props, state); return v; })(styles); } generateStyles(props, state) { const { styles: customStyles } = props; const mergedStyles = this.analyze(this.defaultStyles, props, state); if (customStyles) { Object.keys(customStyles).forEach(key => { if (mergedStyles[key]) { if (isObject(mergedStyles[key])) { Object.assign(mergedStyles[key], customStyles[key]); } else { mergedStyles[key] = customStyles[key]; } } else { mergedStyles[key] = customStyles[key]; } }); } return { styles: mergedStyles, }; } constructor(defaultStyles = {}, { vary = true } = {}) { const manager = this; this.defaultStyles = defaultStyles; return BaseComponent => { const componentWillMount = BaseComponent.prototype.componentWillMount; const componentWillUpdate = BaseComponent.prototype.componentWillUpdate; BaseComponent.prototype.componentWillMount = function() { manager.component = this; Object.assign(this, manager.generateStyles(this.props, this.state)); return componentWillMount && componentWillMount.apply(this, arguments); }; if (vary) { BaseComponent.prototype.componentWillUpdate = function(nextProps, nextState) { Object.assign(this, manager.generateStyles(nextProps, nextState)); return componentWillUpdate && componentWillUpdate.apply(this, arguments); }; } return BaseComponent; }; } } export default Style.factory;

然后我们需要一个错误的兜底功能,但是这个本身应该不属于容器的功能。所以我们封装一个 errorDecorator

  • withError.txs
function withError<T extends IConstructable>(Wrapped: T) { const willReceiveProps = Wrapped.prototype.componentWillReceiveProps; const didMount = Wrapped.prototype.componentDidMount; const willUnmount = Wrapped.prototype.componentWillUnmount; return class extends Wrapped { static displayName: string = `WithError${getDisplayName(Wrapped)}·`; static defaultProps: IProps = { isOffline: false, isError: false, errorRefresh: () => { window.location.reload(true); } }; private state: StateType; private eventNamespace: string = ""; constructor(...args: any[]) { super(...args); const { isOffline, isError, errorRefresh, tabPanelIndex } = this.props; this.state = { isOffline, isError, errorRefresh }; if (tabPanelIndex > -1) { this.eventNamespace = `.${tabPanelIndex}`; } } triggerErrorHandler = e => {...}; componentWillReceiveProps(...args) { if (willReceiveProps) { willReceiveProps.apply(this, args); } const [nextProps] = args; const { isOffline, isError, errorRefresh } = nextProps; this.setState({ isOffline, isError, errorRefresh }); } componentDidMount(...args) { if (didMount) { didMount.apply(this, args); } const { eventNamespace } = this; emitter.on( EVENTS.TRIGGER_ERROR + eventNamespace, this.triggerErrorHandler ); } componentWillUnmount(...args) { if (willUnmount) { willUnmount.apply(this, args); } const { eventNamespace } = this; emitter.off( EVENTS.TRIGGER_ERROR + eventNamespace, this.triggerErrorHandler ); } render() { const { isOffline, isError, errorRefresh } = this.state; if (isOffline || isError) { let errorType = "system"; if (isOffline) { errorType = "offline"; } return <Error errorType={errorType} refresh={errorRefresh} />; } return super.render(); } }; }

然后我们进行整合导出

import { createElement, PureComponent, RaxNode } from 'rax'; import ScrollBase from "./base/scrollBase"; import withError from "./decorator/withError"; interface IScrollContainerProps { spmA:string; spmB:string; renderHeader?:()=>RaxNode; renderFooter?:()=>RaxNode; [key:string]:any; } @withError class ScrollContainer extends PureComponent<IScrollContainerProps,{}> { render() { return <ScrollBase {...this.props} />; } } export default ScrollContainer;

使用如下:

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

思维导图

最后附一张,本文思维导图。

手把手教你如何用 Decorator 装饰你的 Typescript?「实践」

作者:Nealyang

转发连接:
https://mp.weixin..com/s/PFgc8xD7gT40-9qXNTpk7A

推荐Vue学习资料文章:

《「干货」Deno TCP Echo Server 是怎么运行的?》

《Vue仿蘑菇街商城项目(vue+koa+mongodb)》

《基于 electron-vue 开发的音乐播放器「实践」》

《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》

《Vue插件总结【前端开发必备】》

《一篇文章,教你学会Vue-CLI 插件开发【Vue进阶篇】》

《消息队列助你成为高薪 Node.js 工程师》

《Node.js 中的 stream 模块详解》

《「干货」了不起的 Deno 实战教程》

《「干货」通俗易懂的Deno 入门教程》

《Deno 正式发布,彻底弄明白和 node 的区别》

《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》

《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》

《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》

《深入Vue 必学高阶组件 HOC「进阶篇」》

《深入学习Vue的data、computed、watch来实现最精简响应式系统》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》

《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》

《2020前端就业Vue框架篇「实践」》

《详解Vue3中 router 带来了哪些变化?》

《Vue项目部署及性能优化指导篇「实践」》

《Vue高性能渲染大数据Tree组件「实践」》

《尤大大细品VuePress搭建技术网站与个人博客「实践」》

《10个Vue开发技巧「实践」》

《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》

《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》

《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》

《实践Vue 3.0做JSX(TSX)风格的组件开发》

《一篇文章教你并列比较React.js和Vue.js的语法【实践】》

《手拉手带你开启Vue3世界的鬼斧神工【实践】》

《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》

《怎样为你的 Vue.js 单页应用提速》

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开发者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《使用vue实现HTML页面生成图片》

《实现全栈收银系统(Node+Vue)(上)》

《实现全栈收银系统(Node+Vue)(下)》

《vue引入原生高德地图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战经验汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开发必备】》

《Vue 开发必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《使用vue+node搭建前端异常监控系统》

《推荐 8 个漂亮的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰对比两者差异》

《Vue组件间通信几种方式,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染原理与实现》

《10个Vue开发技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文+多图,带你了解vue的双向数据绑定源码实现》

《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue应用》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》

《Vue3.0权限管理实现流程【实践】》

《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》

作者:Nealyang

转发连接:
https://mp.weixin..com/s/PFgc8xD7gT40-9qXNTpk7A

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/176758.html

(0)
上一篇 2025-04-21 12:45
下一篇 2025-04-21 13:10

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信