设计模式:Vue 感觉像 React - TypeScript 🔥
封面照片由Ricardo Gomez Angel在Unsplash上拍摄。
当你第一次想要学习一门前端技术时,你会被众多的工具选择所困扰,例如 React、Vue、Angular、Svelte 等。当然,如果我们不尝试其中任何一种,我们就不会知道,当然所有这些技术都有其优点和缺点。
但在本文中,我们不会讨论哪个是最好的,而是讨论 React 开发人员如何以相同的模式轻松掌握这两个框架(React 和 Vue)。
所以,这是一段漫长的旅程!做好准备!😃
设置项目
首先我们要做的是建立项目,让我们先创建一个目录结构。
1. 根目录结构
组件文件夹中包含容器和展示文件夹。区别在于,展示组件专注于 UI 元素,而容器组件则负责规范逻辑/存储数据部分,这些数据部分将显示在组件容器中。
├── src
| ├── assets
| ├── components
| ├── container
| ├── presentational
├── redux
| ├── action
| ├── reducer
├─
你可以自由设置你喜欢的目录结构,这是我创建项目的目录结构
2. 使用 jsx 和 typescript
那么,让我们先安装一些需要的依赖项。我们可以输入以下命令来执行此操作:
npm i --save-dev typescript babel-preset-vca-jsx
npm i --save-dev @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime
npm i --save-dev @babel/preset-typescript @types/webpack-env source-map-loader
npm uninstall babel-plugin-transform-runtime
我们需要卸载这个包babel-plugin-transform-runtime
,因为我们已经安装了最新版本@babel/plugin-transform-runtime
然后,我们必须设置一些额外的配置,因为一些依赖项需要支持的 Babel 版本
注意:我碰巧使用了这个样板:Vue Webpack 模板,您可以选择任何样板。
更新你的 babel core 和 babel loader
npm i --save-dev babel-core@^7.0.0-0 babel-loader@^8.0.6
npm i --save-dev @babel/core@^7.6.4 @babel/preset-env@^7.6.3
安装完所有依赖项后,我们必须在.babelrc
打开文件时设置附加配置,然后添加配置.babelrc我们还需要设置 webpack 加载器webpack 配置
注意:对于 TypeScript 加载器,您可以使用Awesome TypeScript Loader
别忘了,你还需要添加一些配置.eslintrc.js
rules: {
'import/extensions': ['error', 'always', {
jsx: 'never',
ts: 'never',
tsx: 'never'
}],
}
接下来,创建新文件tsconfig.json
并遵循此配置tsconfig.json
注意:如果你想使用
TSLint
,你可以按照此步骤配置 TSLint
添加所有配置后,好了!现在是时候将所有项目文件扩展名从.jsx/.js
替换为.tsx/.ts
3.安装其他依赖项
npm i --save @vue/composition-api vuejs-redux redux @types/redux
主要概念
作为非常流行的前端工具,这两种工具具有相同的功能,例如双向数据绑定、模板、路由、组件、依赖注入等等。
相似但不相同,这两个工具之间存在一些差异,即在编写语法、渲染组件、管理状态和数据方面。因此,在本节中,我们将逐一揭秘如何在 Vue 中实现 React 模式。
组件和道具
组件是特殊类型的指令,例如 JavaScript 函数,它们将显示为单独的部分并可重复使用。
这里我使用Vue.component
和createComponent
(vue Composition API)你可以同时使用
在渲染组件时,两者有很大不同。React 将组件定义为类或函数,而 Vue 将组件定义为对象。
export default createComponent({
name: 'ComponentProps',
props: {
name: String,
authorName: Array as () => string[]
},
setup(props) {
return () => (
<div className="components-props">
<h2>{props.name}</h2>
<p>{props.authorName}</p>
</div>
)
}
})
我们不再需要template
再次使用,只需像 React 一样使用 JSX即可 🙂
render () {
return (
<ComponentProps
name="Your name here"
commentId={['Name1', 'Name2']}
/>
)
}
条件渲染
条件渲染的工作方式与 JavaScript 中的条件工作方式相同,我们可以使用三元或条件运算符。
export default createComponent({
name: 'ConditionalRendering',
props: {
show: Boolean
},
setup(props) {
return () => props.show ? <p>True Condition</p> : <p>False Condition</p>
}
})
render() {
return <ConditionalRendering show={false}/>
}
处理事件
在 Vue JS 中,处理事件时,Vue 会提供指令来 v-on
处理这些事件。由于我们已经使用了 JSX,所以不再需要它,我们可以像在 React 中一样使用 JSX 属性 :)
export default createComponent({
setup(props) {
return () => (
<button onClick={props.handleButtonClick}>
Click Event
</button>
)
},
props: {
handleButtonClick: Function as () => void
}
})
render () {
return (
<HandlingEvent
handleButtonClick={() => alert("Click event. This works!")}
/>
)
}
JSX 中的子项
Children 是一个组件,用于在调用该组件时显示在开始和结束标记之间包含的任何内容。
为了访问该组件,我们可以使用 slots
函数作为内容分发出口。
export default Vue.component('Children', {
render() {
return (
<div className="children">
{this.$slots.default}
</div>
)
}
})
render () {
return (
<div className='container'>
<Children>
{/* what is placed here is passed as children */}
</Children>
</div>
)
}
生命周期和钩子
生命周期是一种规范组件生命周期各个阶段的方法,它们有各自的用途
setup
: 在组件实例创建时,在初始 props 解析之后立即调用。从生命周期角度来看,它在beforeCreate
钩子之前调用。onBeforeMount
在渲染过程运行之前执行的函数。onMounted
首次渲染完成后仅调用一次的函数。通常,此函数用于执行任何可能产生副作用的操作,例如 AJAX 请求。onUnmounted
执行该函数是为了从 DOM 中消除或删除组件。
import {
createComponent,
reactive as useState,
onBeforeMount as componentWillMount,
onMounted as componentDidMount,
onUnmounted as componentWillUnmount
} from '@vue/composition-api';
const LifecycleHooks = createComponent({
setup() {
const state = useState<{ loading: boolean, users: object }>({
loading: false,
users: []
})
componentWillMount(() => {
console.log("Component before mount")
})
componentDidMount(() => {
const API_URL = 'https://jsonplaceholder.typicode.com/users'
fetch(API_URL)
.then(res => res.json() as Promise<any>)
.then(data => {
state.users = data,
state.loading = !state.loading;
})
.catch((err: Error) => {
throw err
})
console.log("Component Mounted")
});
componentWillUnmount(() => {
console.log("Component Will Unmount")
})
return () => (
<div className="lifecycle-hooks">
{state.loading ? JSON.stringify(state.users) : <span>Loading...</span>}
</div>
)
}
})
export default LifecycleHooks
是的,我用它 as ...
来导入模块,这只是命名,所以它看起来和 React 中的方法名一样
reactive
函数相当于 Vue 2 的,Vue.observable()
它将返回一个与 obj 完全相同的新对象,并返回原始对象的反应式代理。watch
function 需要一个函数作为参数。它会跟踪内部的反应变量,就像组件为模板所做的那样。当我们修改传入函数内部使用的反应变量时,给定的函数会再次运行。
import {
createComponent,
reactive as useState,
watch as useEffect
} from '@vue/composition-api';
const LifecycleHooks = createComponent({
setup() {
const state = useState<{ count: number }>({
count: 0
})
/* => Re-run it whenever the dependencies have changed */
useEffect(() => state.count, (nextState, prevState) => {
console.log(nextState, '<= this is nextState')
console.log(prevState, '<= this is prevState');
})
return () => (
<div className="lifecycle-hooks">
<button onClick={() => state.count++}>
Update Value
</button>
</div>
)
}
})
Redux 和 Vue
你肯定已经知道什么是 Redux 了吧?没错!Redux 是一个面向 JavaScript 应用的独立状态管理库框架。与 Vuex 不同,Redux 可以在任何框架中使用。
Redux 有 4 个主要概念:reducers、actions、action creators和store。在 Redux 中,状态是不可变的纯函数。以下是一些关于 Vue 中 Redux 的知识:
行动
操作是简单的 JavaScript 对象,代表将数据从应用程序发送到商店的信息负载。操作具有类型和可选负载。
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
export const RESET = 'RESET'
export const increment = () => {
return {
type: INCREMENT
// your payload here
}
}
export const decrement = () => {
return {
type: DECREMENT
}
}
export const reset = () => {
return {
type: RESET
}
}
Reducers
Reducer 指定应用程序的状态如何响应发送到 store 的操作而变化。Reducer 可以组合成一个根 Reducer,以管理应用程序的所有状态。
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' } | { type: 'RESET' };
const Counter = (state: number = 0, action: Action) => {
switch (action.type) {
case 'INCREMENT': {
return state + 1;
}
case 'DECREMENT': {
return state - 1;
}
case 'RESET': {
return state
}
default: return state
}
}
export default Counter
combineReducers
在一个根 Reducer 函数中调度 Action 时,使用to 调用所有 Reducer。这非常有用:)
import { combineReducers } from 'redux'
import userReducer from './reducer/user.reducer'
export default combineReducers({
user: userReducer
// your another reducer here
})
店铺
Store是存储应用程序状态的地方。Store 保存了应用程序的整个状态树,该状态树包含指向对象的一些方法。Redux 应用程序中只有一个 Store。
import Vue from 'vue'
import { createStore } from 'redux'
import Provider from 'vuejs-redux';
import RootReducer from './rootReducer'
const store = createStore(RootReducer);
export default Vue.component('Provider', {
render() {
return (
<Provider
mapStateToProps={this.mapStateToProps}
mapDispatchToProps={this.mapDispatchToProps}
store={store}>
{this.$scopedSlots.default}
</Provider>
)
},
props: ['mapStateToProps', 'mapDispatchToProps'],
components: {
Provider
}
})
我们还可以创建一个自定义提供程序,接收 mapStateToProps 和 mapDispatchToProps 作为 props,然后导入存储并将其传递给每个Provider
。
import Vue from 'vue';
import ContextConsumer from './redux';
import * as actions from './redux/action/user.action';
import ComponentContainer from './components/container/component-wrap';
export default Vue.component('App', {
render() {
return (
<ContextConsumer
mapStateToProps={this.mapStateToProps}
mapDispatchToProps={this.mapDispatchToProps}>
{({ incrementAction, userData }) => (
<ComponentContainer>
<SingleComponent
value={userData.user}
handleClick={incrementAction}
/>
</ComponentContainer>
)}
</ContextConsumer>
)
},
components: {
ContextConsumer
},
methods: {
mapStateToProps(state) {
return {
userData: state
}
},
mapDispatchToProps(dispatch) {
return {
incrementAction: () => dispatch(actions.increment())
}
}
}
})
高阶组件
高阶组件 (HOC) 是 React 中用于复用组件逻辑的高级技术。HOC 不属于 React API,而是一种源于 React 组合特性的模式。
如果你理解了高阶函数(HOF)的概念,当然做HOC就会非常容易,因为HOC就是HOF的一种实现:)
import Vue from 'vue'
const useDataFetchingHOC = (WrappedComponent: JSX.IntrinsicElements) => (urlParam: string) => {
return Vue.component('HOCFetch', {
data: () => ({
fetchData: null
}),
mounted: function() {
fetch(urlParam)
.then(response => {
if (!response.ok) { throw new Error(response.statusText) }
return response.json() as Promise<any>;
})
.then(data => this.fetchData = data)
.catch((err: Error) => {
throw err
})
},
render(createElement) {
return !this.fetchData ? createElement('span', 'Loading Fetch...') :
createElement(WrappedComponent, {
attrs: this.$attrs,
props: this.$props,
on: this.$listeners
})
}
})
};
export default useDataFetchingHOC
import { createComponent } from '@vue/composition-api'
import useDataFetchingHOC from '../presentational/hoc-component'
const dataSourceUrl = "https://jsonplaceholder.typicode.com/users";
const ContentSite = createComponent({
setup() {
return () => (
<div className="content">
<p>Yes, i'm in HOC</p>
</div>
)
}
})
export default useDataFetchingHOC(ContentSite)(dataSourceUrl)
感谢阅读
感谢阅读,希望你喜欢这篇文章,并希望它能给你的工作带来一些启发。诚然,Vue 和 React 是非常酷的前端工具,并且受到众多用户的青睐。所以,继续尝试和学习新事物吧,别忘了永远相信自己!😎
该项目的完整源代码可在Gitlab上找到。
鏂囩珷鏉ユ簮锛�https://dev.to/natserrat/design-patterns-vue-feels-like-react-typescript-38in