==>wp rest api:1.安装phpStudy和wordpress2.phpStudy站点域名设置和hosts内容添加3.wordpress安装wp rest api插件给浏览器安装json-handler插件 ,这样获取的json格式能在浏览器当中自动换行显示==>redux:三大块: action、reducer、store,它们都是相应的函数处理模块,三者的顺序为先action,然后reducer,最后store。action: 返回一个对象给 reducer ,触发动作reducer: 处理接受 action 传递过来的东西store: 接收 reducer 传递过来的东西,其相当于一个仓库,把网站的所有数据都储存在里面==>各部分组件机构:==>moment的用法(import moment form 'moment' ):
vue比较好。
VUE是iOS和Android平台上的一款Vlog社区与编辑工具,允许用户通过简单的操作实现Vlog的拍摄、剪辑、细调、和发布,记录与分享生活。还可以在社区直接浏览他人发布的Vlog,与Vloggers互动。
随着手机摄像头的发展,越来越多的人开始使用手机拍照和摄像。摄像一般来说要比拍照门槛高,但是视频传播的信息量又远大于照片。VUE就诞生在这样的背景下,希望用拍照一样简单的操作,帮助用户在手机上拍摄精美的短视频。
主要功能:
分镜头:通过点按改变视频的分镜数实现简易的剪辑效果,而剪辑能够让视频传达更多的信息。
实时滤镜:由电影调色专家调制的12款滤镜供选择,切换至前置摄像头会出现自然的自拍美颜功能。
贴纸:支持40款手绘贴纸,还可以编辑贴纸的出现时间。
自由画幅设置:支持1:1、16:9、2.39:1三种画幅的视频拍摄。
1、模板不能修改,选好了模板,用户无法对其进行独立、随意地编辑,无法自定义模板,共享类模板设计出来的页面,往往是一个呆板而雷同的页面,让人感觉死色沉沉。
2、功能简单且没有个性,在页面、风格、功能、维护管理等方面的功能得不到保障。
3、网站自身服务器的约束性,由于管理和维护工作必须在服务商的服务器上完成,因此用户的一些网站也被牢牢捆绑在该公司的服务器上,用户不可以自由地移植到别的服务器上,用户就失去了自由选择的权利。
模板建站和智能建站有啥区别?反正不是用技术原生建站就是了 这样的产品可定制性太低 原生建站就是任你行
上一篇,我们使用gatsby创建了一个非常简单的初始化的例子,并本地运行了develop模式看到了starter的基本页面。
这章,我们将使用蚂蚁金服的antd框架,替换掉首页模板,做一个公司首页简介页面。
首先我们去蚂蚁金服的首页模板网站页面,选择一个现有的模板页面,,我们这里选择seeconf模板页面。
首页模板
点击编辑,然后下载模板zip包,里面会包含生成的jsx文件。
模板目录
拷贝这些文件到gatsby的src/pages目录下面,除了index.jsx文件。
然后我们需要安装依赖包。
Gatsby-plugin-less, less包,主要是因为这里包含了less文件,所以我们需要支持less的webpack。antd是我们基于的ui包,下面的是首页模板用到的一些库,主要是跟动画和全屏展示相关的包。
然后,修改gatsby-browser.js文件,全局导入antd的样式文件。
接着修改Gatsby-config.js文件,enable less plugin。
plugins列表,添加了gatsby-plugin-less。
修改index.jsx文件,把依赖dva2.0的代码部分删掉。
在渲染返回函数中,直接在div中填写{children}去掉show的判断this.state.show。
改完之后,我们就可以再次启动develop,查看首页。
目前为止,我们就成功了使用antd的首页模板替换了默认的gatsby的首页模板。下一篇,我们将会使用gatsby的数据源功能,把首页的数据内容填写到不同的数据源,然后通过graphql查询数据,然后渲染页面生成。谢谢大家。
作者:Jiawei Pan
转发链接:
前言我的朋友取消了我们的周末聚会计划之后,我就打算做点别的事情打发时间,然后从“to-do-list”中选择了“创建个人作品集网站”这一项。
我花了几个小时搜索技术和模板,然后确定用 React.js 创建这个网页(),并把它部署到 GitHub pages上。你可以在这里()找到网页的代码。
本文将介绍什么React.js 基础知识使用 create-react-app使用 GitHub pages 部署你的个人作品集网页预备知识提示 1:如果你对 React.js 和 React 组件的基础概念有一定了解可以跳过这部分。
提示 2:这些知识点能让你对 React 的世界有个基础的了解。我非常建议你通过React 官方文档 和 freeCodeCamp 学习更多内容。
什么是 React.js基本的,你只需要知道 React.js 是一个用来构建 UI 组件的 JavaScript 库,它是由 Facebook 的工程师创建的项目,它正影响着 JavaScript 的世界。
什么是 React 组件你可以通过类或者函数的方式来定义一个 React 组件,可以向组件传入 props 参数。
页面的 UI 可以通过组件的形式拆分成独立的部分,比如可以分成页头 header、主体 body、页尾 footer。每个组件都是独立运行的,因此每个组件都分别渲染到 ReactDOM()而不会影响整个页面。
通过 React 组件提供的生命周期方法,可以将想要执行的代码放到组件的 mounting(挂载)、rendering(渲染)、updating(更新)和 un-mounting(卸载)等各个阶段。
使用 React 组件时需要权衡利弊。比如,我们可以通过将组件导出到别的组件中来达到复用的效果,但有时候多个组件间的通信和触发渲染的问题会让人比较头疼。
这是 React 组件的样子。
import React, { Component } from 'react'export default class Component-name extends Component { render() { return ( <div> {these code will be rendered into the DOM} </div> ) }}
什么是 GitHub Pages
通过 GitHub Pages,你可以轻松地使用 GitHub 免费部署你的网页,无需担心配置问题。他们提供了各种模块,帮你处理很多事情。如果你坚持到最后,你会发现这就像魔法一样神奇。
预备工作确定要在网站上放哪些内容看一下你最新的简历(如果没有就立马创建一份),这会帮助你你理清哪些信息需要被放到作品集网站上。
寻找设计灵感你可以在网上搜索到大量免费的作品集网站模版,看一下哪些内容适合自己的网站—— 拿出纸和笔,把你对网站的想法通过草图展现出来。我会用这个模板(#jackson)来画草图。
搜集一些你的美照你肯定不想把自己邋遢的形象展示在作品集网站上,那就找一张你最满意的个人照吧。
打开你的最喜欢的歌单俗话说得好:好的音乐可以帮助我们创建好的作品。不妨给你的网站增加点音乐!
我的个人作品集网站()
开始创建项目接下来我会一步步展示如何创建个人作品集网站。你不需要跟着我写同样的代码,只需要专注于学习概念,然后发挥你的创造力!我会分三部分进行说明。
设置 React-app将 HTML 页面分解成 React 组件在 GitHub pages 上部署应用创建 React-app我们会使用 create-react-app —— Facebook 提供的一个组件 —— 它可以帮助我们轻松创建 React 应用而不需要担心构建工具。
切换到控制台,执行 npm install create-react-app,安装这个模块(确保在此之前安装了 npm —— 在 查看更多信息)接着运行 npm create-react-app ${project-name} 构建代码,创建出来的文件目录结构如下:my-portfolio-app├── README.md (GitHub 的项目描述文件)├── node_modules (存储项目所需的模块)├── package.json (存储项目源信息,如依赖包,版本号等等)├── .gitignore (这里声明的文件和目录在提交到 GitHub 时会被忽略,如 node_modules)├── public (存储图片,js,css文件)│ ├── favicon.ico│ ├── index.html│ └── manifest.json └── src (应用的主要代码) ├── {在这里创建 Components 组件文件} ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js
在 src 目录下创建一个 components 目录,稍后我们会在这里存放组件。
从HTML template 中拷贝所有的图片、字体、HTML 和 CSS 到 public 目录现在你的目录结构看起来应该像这样。
运行 npm install ,安装所有的模块到 node_module目录中如果你已经到了这一步了, 那么运行 npm start,React 应用会被加载到 localhost 的 3000 端口,访问 :3000,现在你应该能够看到 React-app 的开始页面了拆分 HTML 页面到 React 组件中请回忆我们前面在 src 目录下创建的 component 文件夹,现在我们将要把 HTML 模板页面拆分成一个个组件,然后把这些组件拼接起来组成 React 应用。
首先,你需要确定可以把单个 HTML 文件拆分成哪些组件 —— 就像 header、footer 和 contact me。你需要在这里发挥点创造力!!
找到没有在嵌套别的 section/div 标签中的 section/div 标签及其他类似标签,其中应包含有关页面特定部分的信息,并且独立于其他部分。我的 GitHub Repo()有详细介绍这一点。
提示:使用 “inspect element” 工具来演示代码,并注意浏览器中对应的变化。
这些 HTML 会被应用到组件的 render() 方法中。无论组件是否渲染到 ReactDOM,render() 方法都会返回这些 HTML。
<section id="colorlib-hero" class="js-fullheight" data-section="home"> <div class="flexslider js-fullheight"> <ul class="slides"> <li style="background-image: url(images/img_bg_1.jpg);"> <div class="overlay"></div> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3 col-md-pull-3 col-sm-12 col-xs-12 js-fullheight slider-text"> <div class="slider-text-inner js-fullheight"> <div class="desc"> <h1>Hi! <br>I'm Jackson</h1> <h2>100% html5 bootstrap templates Made by <a href="" target="_blank">colorlib.com</a></h2> <p><a class="btn btn-primary btn-learn">Download CV <em class="icon-download4"></em></a></p> </div> </div> </div> </div> </div> </li> <li style="background-image: url(images/img_bg_2.jpg);"> <div class="overlay"></div> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3 col-md-pull-3 col-sm-12 col-xs-12 js-fullheight slider-text"> <div class="slider-text-inner"> <div class="desc"> <h1>I am <br>a Designer</h1> <h2>100% html5 bootstrap templates Made by <a href="" target="_blank">colorlib.com</a></h2> <p><a class="btn btn-primary btn-learn">View Portfolio <em class="icon-briefcase3"></em></a></p> </div> </div> </div> </div> </div> </li> </ul> </div></section>
HTML 文件中的 home 部分
import React, { Component } from 'react'export default class Home extends Component { render() { return ( <div> <section id="colorlib-hero" className="js-fullheight" data-section="home"> <div className="flexslider js-fullheight"> <ul className="slides"> <li style={{backgroundImage: 'url(images/img_bg_1.jpg)'}}> <div className="overlay" /> <div className="container-fluid"> <div className="row"> <div className="col-md-6 col-md-offset-3 col-md-pull-3 col-sm-12 col-xs-12 js-fullheight slider-text"> <div className="slider-text-inner js-fullheight"> <div className="desc"> <h1>Hi! <br />I'm Jackson</h1> <h2>100% html5 bootstrap templates Made by <a href="" target="_blank">colorlib.com</a></h2> <p><a className="btn btn-primary btn-learn">Download CV <em className="icon-download4" /></a></p> </div> </div> </div> </div> </div> </li> <li style={{backgroundImage: 'url(images/img_bg_2.jpg)'}}> <div className="overlay" /> <div className="container-fluid"> <div className="row"> <div className="col-md-6 col-md-offset-3 col-md-pull-3 col-sm-12 col-xs-12 js-fullheight slider-text"> <div className="slider-text-inner"> <div className="desc"> <h1>I am <br />a Designer</h1> <h2>100% html5 bootstrap templates Made by <a href="" target="_blank">colorlib.com</a></h2> <p><a className="btn btn-primary btn-learn">View Portfolio <em className="icon-briefcase3" /></a></p> </div> </div> </div> </div> </div> </li> </ul> </div> </section> </div> ) }}
将 HTML 的 home 部分创建为 React 组件
提示:如果你暂时不知道怎么把它们变成 React 组件,试着重点关注“如何从 HTML 中辨别需要成为组件的部分”。当你渐渐地适应了 React 的使用,实现功能将会是小菜一碟。
你发现了 HTML 有些变化吗?class 变成了 className。这些变化是因为 React 不支持 HTML 吗?实际上这是 JavaScript 的语法扩展,叫作 JSX,能让我们在 JS 中写 HTML。所以,我们需要在 HTML 基础上做些改变,把它们变成 JSX。
在这个项目中,我使用了 HTML to JSX 转换器(),一个可以将 HTML 转换为 JSX 代码的工具。我非常建议你使用这些工具而不是手动转换代码。
稍后你应该有了几个不同的组件,马上就要到精彩环节了!在 App.js 组件中将这些不同类型的组件结合在一起(没错,你可以从一个组件中渲染另一个组件!),你的个人作品集应用马上就要好了。
import React, { Component } from 'react';import './App.css';import Sidebar from './components/sidebar'import Introduction from './components/introduction'import About from './components/about'import Projects from './components/projects'import Blog from './components/blog'import Timeline from './components/timeline'class App extends Component { render() { return ( <div id="colorlib-page"> <div id="container-wrap"> <Sidebar></Sidebar> <div id="colorlib-main"> <Introduction></Introduction> <About></About> <Projects></Projects> <Blog></Blog> <Timeline></Timeline> </div> </div> </div> ); }}export default App;
在 app.js 中结合所有组件
注意前面的代码,为了能够在 render() 中使用代码,首先我们需要 import 组件。我们可以将 <component-name></component-name> 或 <component-name/> 将标签添加到方法里。
在终端运行 npm start,然后你应该能在网页上看到变化。当你对代码做出修改时,你不需要再次运行这条命令,只要保存更新,React 会自动响应。多亏了hot reload(热加载) ,我们进行快速轻量级的部署。
根据你简历的内容,使用 HTML 和 CSS 去美化页面,使你的作品集看起来更加炫酷。可以尝试使用使用不同的字体、颜色和图片。
将 React-app 部署到 GitHub pages 上好了,恭喜你坚持到了这里。奖励一下努力工作的自己,休息一下,然后开始部署吧。
首先,你需要安装 GitHub pages 的 npm 包,在终端运行 _npm install gh-pages_。
现在,你需要修改一下_manifest.json_文件:
添加 _homepage_ 属性,它的值会以这样的格式呈现——https://{github_id}.github.io/{github_repo}在 _scripts_ 添加 _predeploy_ 和 _deploy_ 属性现在你的 manifest.json 应该是这样:
{ "name": "portfolio-app", "version": "0.1.0", "private": true, "homepage": "", "dependencies": { "gh-pages": "^2.0.1", "react": "^16.8.3", "react-dom": "^16.8.3", "react-scripts": "2.1.5", "yarn": "^1.13.0"}, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "predeploy": "yarn run build", "deploy": "gh-pages -d build", "test": "react-scripts test", "eject": "react-scripts eject"}, "eslintConfig": { "extends": "react-app"}, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ]}
添加 gh-pages 链接后的 manifest.json
现在转到终端界面,运行 npm run deploy 命令,然后等待神奇事情发生。你的应用会在脚本成功执行后部署。访问你在 homepage 中提供的地址,检查应用是否正确部署。
提醒: 将任何东西部署到网上前请务必认真仔细。进行安全检查,移除内部链接、密码或者任何你不想被别人看到的东西。
留给你的作业好棒,你成功创建并部署了个人作品集应用!如果你有兴趣,可以将这些功能添加到你的网站中:
博客功能: 通过 Node.js 好像 MongoDB 这样的非关系型数据库创建你的个人博客并整合到你的网站中。图册展示: 在页面中添加一个区域,展示你在社交媒体网站最近发布的照片来自 Twitter 的反馈: 在页面中添加一个展示你最近的推文的区域随机的名人名言: 在页面中添加一个随机展示名人名言的区域如果你实现了任何一个功能,请和我分享你的成果。我非常乐意帮助别人,如果我帮得上的话^_^
推荐React 学习相关文章《细聊 React 是如何创建 vdom 和 fiber tree「实践」》
《深入浅出 React V16.4 生命周期的来龙去脉》
《在 React 中进行事件驱动的状态管理「实践」》
《一文带你搞懂React-native源码解析》
《常见的8个问题带你进阶 React》
《分析 React 组件的渲染性能「实践」》
《实践React Router v5:完整指南》
《前端必备的20种基本React工具「干货」》
《8个顶级React.js免费模板》
《推荐36种免费React模板和主题「干货」》
《「笔记」React Hooks 深入细品系列》
《这就是你日思夜想的 React 原生动态加载「值得收藏」》
《「干货满满」React Hooks 最佳实践》
《手把手教你如何实现一个React水印组件「实践」》
《「实践」React 中必会的 10 个概念》
《「干货」深入浅出React组件逻辑复用的那些事儿》
《手把手教你从Mixin深入到HOC再到Hook【React】》
《深入Facebook 官方React 状态管理器Recoil讲解》
《手把手教你实践搭建React组件库「超详细」》
《在 React 中自动复制文本到剪贴板「实践」》
《「干货满满」从零实现 react-redux》
《深入详解大佬用33行代码实现了React》
《让你的 React 组件性能跑得再快一点「实践」》
《React源码分析与实现(三):实践 DOM Diff》
《React源码分析与实现(一):组件的初始化与渲染「实践篇」》
《React源码分析与实现(二):状态、属性更新->setState「实践篇」》
《细说React 核心设计中的闪光点》
《手把手教你10个案例理解React hooks的渲染逻辑「实践」》
《React-Redux 100行代码简易版探究原理》
《手把手深入教你5个技巧编写更好的React代码【实践】》
《React 函数式组件性能优化知识点指南汇总》
《13个精选的React JS框架》
《深入浅出画图讲解React Diff原理【实践】》
《【React深入】React事件机制》
《Vue 3.0 Beta 和React 开发者分别杠上了》
《手把手深入Redux react-redux中间件设计及原理(上)【实践】》
《手把手深入Redux react-redux中间件设计及原理(下)【实践】》
《前端框架用vue还是react?清晰对比两者差异》
《为了学好 React Hooks, 我解析了 Vue Composition API》
《【React 高级进阶】探索 store 设计、从零实现 react-redux》
《写React Hooks前必读》
《深入浅出掌握React 与 React Native这两个框架》
《可靠React组件设计的7个准则之SRP》
《React Router v6 新特性及迁移指南》
《用React Hooks做一个搜索栏》
《你需要的 React + TypeScript 50 条规范和经验》
《手把手教你绕开React useEffect的陷阱》
《浅析 React / Vue 跨端渲染原理与实现》
《React 开发必须知道的 34 个技巧【近1W字】》
《三张图详细解说React组件的生命周期》
《手把手教你深入浅出实现Vue3 & React Hooks新UI Modal弹窗》
《手把手教你搭建一个React TS 项目模板》
《全平台(Vue/React/微信小程序)任意角度旋图片裁剪组件》
《40行代码把Vue3的响应式集成进React做状态管理》
《手把手教你深入浅出React 迷惑的问题点【完整版】》
1. 前言在前端圈子有这样一种说法,Vue 入门最简单,React 学习曲线太陡,Angular...我还是选择狗带吧。
在 React 诞生之初,Facebook 宣传这是一个用于前端开发的界面库,仅仅是一个 View 层,在大型应用中,处理好 React 组件通信和状态管理就显得非常重要。
为了解决这一问题,Facebook 最先提出了单向数据流的 Flux 架构,弥补了使用 React 开发大型网站的不足。
Flux:
随后,Dan Abramov 受到 Flux 和函数式编程语言 Elm 启发,开发了 Redux 这个状态管理库。
Redux 源码非常精简,实现也很巧妙,这篇文章将带你从零手写一个 Redux 和 react-redux 库,以及告诉你该如何设计 Redux 中的 store。
在开始前,我已经将这篇文章的完整代码都整理到 GitHub 上,大家可以参考一下。
React-redux:
2. 状态管理2.1 理解数据驱动在开始讲解状态管理前,我们先来了解一下现代前端框架都做了些什么。
以 Vue 为例子,在刚开始的时候,Vue 官网首页写的卖点是数据驱动、组件化、MVVM 等等(现在首页已经改版了)。

那么数据驱动的意思是什么呢?不管是原生 JS 还是 jQuery,他们都是通过直接修改 DOM 的形式来实现页面刷新的。
而 Vue/React 之类的框架不是粗暴地直接修改 DOM,而是通过修改 data/state 中的数据,实现了组件的重新渲染。也就是说,他们封装了从数据变化到组件渲染这一个过程。
原本我们用 jQuery 开发应用,除了要实现业务逻辑,还要操作 DOM 来手动实现页面的更新。尤其是涉及到渲染列表的时候,更新起来非常麻烦。
var ul = document.getElementById("todo-list");$.each(todos, function(index, todo) {var li = document.createElement('li');li.innerHTML = todo.content;li.dataset.id = todo.id;li.className = "todo-item";ul.appendChild(li);})复制代码
所以后来出现了 jQuery.tpl 和 Underscore.template 之类的模板,这些让操作 DOM 变得容易起来,有了数据驱动和组件化的雏形,可惜我们还是要手动去渲染一遍。
<script type="text/template" id="tpl"><ul id="todo-list"><% _.each(todos, function(todo){ %><li data-id="<%=todo.id%>" class="todo-item"><%= todo.content %></li><% }); %></ul></script>复制代码
如果说用纯原生 JS 或者 jQuery 开发页面是原始农耕时代,那么 React/Vue 等现代化框架则是自动化的时代。
有了前端框架之后,我们不需要再去关注怎么生成和修改 DOM,只需要关心页面上的这些数据以及流动。 所以如何管理好这些数据流动就成了重中之重,这也是我们常说的“状态管理”。
2.2 什么状态需要管理?前面讲了很多例子,可状态管理到底要管理什么呢?在我看来,状态管理的一般就是这两种数据。
Domain StateDomain State 就是服务端的状态,这个一般是指通过网络请求来从服务端获取到的数据,比如列表数据,通常是为了和服务端数据保持一致。
{"data": {"hotels": [{"id": "31231231","name": "希尔顿","price": "1300"}]}}复制代码UI State
UI State 常常和交互相关。例如模态框的开关状态、页面的 loading 状态、单(多)选项的选中状态等等,这些状态常常分散在不同的组件里面。
{"isLoading": true,"isShowModal": false,"isSelected": false}复制代码2.3 全局状态管理
我们用 React 写组件的时候,如果需要涉及到兄弟组件通信,经常需要将状态提升到两者父组件里面。一旦这种组件通信多了起来,数据管理就是个问题。
结合上面的例子,如果想要对应用的数据流进行管理,那是不是可以将所有的状态放到顶层组件中呢? 将数据按照功能或者组件来划分,将多个组件共享的数据单独放置,这样就形成了一个大的树形 store。这里更建议按照功能来划分。
这个大的 store 可以放到顶层组件中维护,也可以放到顶层组件之外来维护,这个顶层组件我们一般称之为“容器组件”。
容器组件可以将组件依赖的数据以及修改数据的方法一层层传给子组件。
我们可以将容器组件的 state 按照组件来划分,现在这个 state 就是整个应用的 store。将修改 state 的方法放到 actions 里面,按照和 state 一样的结构来组织,最后将其传入各自对应的子组件中。
class App extends Component {constructor(props) {this.state = {common: {},headerProps: {},bodyProps: {sidebarProps: {},cardProps: {},tableProps: {},modalProps: {}},footerProps: {}}this.actions = {header: {changeHeaderProps: this.changeHeaderProps},footer: {changeFooterProps: this.changeFooterProps},body: {sidebar: {changeSiderbarProps: this.changeSiderbarProps}}}}changeHeaderProps(props) {this.setState({headerProps: props})}changeFooterProps() {}changeSiderbarProps() {}...render() {const {headerProps,bodyProps,footerProps} = this.state;const {header,body,footer} = this.actions;return (<div className="main"><Header {...headerProps} {...header} /><Body {...bodyProps} {...body} /><Footer {...footerProps} {...footer} /></div>)}}复制代码
我们可以看到,这种方式可以很完美地解决子组件之间的通信问题。只需要修改对应的 state 就行了,App 组件会在 state 变化后重新渲染,子组件接收新的 props 后也跟着渲染。
这种模式还可以继续做一些优化,比如结合 Context 来实现向深层子组件传递数据。
const Context = createContext(null);class App extends Component {...render() {return (<div className="main"><Context.Provider value={...this.state, ...this.events}><Header /><Body /><Footer /></Context.Provider></div>)}}const Header = () => {// 获取到 Context 数据const context = useContext(Context);}复制代码
如果你已经接触过 Redux 这个状态管理库,你会惊奇地发现,如果我们把 App 组件中的 state 移到外面,这不就是 Redux 了吗?
没错,Redux 的核心原理也是这样,在组件外部维护一个 store,在 store 修改的时候会通知所有被 connect 包裹的组件进行更新。这个例子可以看做 Redux 的一个雏形。
3. 实现一个 Redux根据前面的介绍我们已经知道了,Redux 是一个状态管理库,它并非绑定于 React 使用,你还可以将其和其他框架甚至原生 JS 一起使用,比如这篇文章:如何在非 React 项目中使用 Redux
Redux 工作原理:
在学习 Redux 之前需要先理解其工作原理,一般来说流程是这样的:
用户触发页面上的某种操作,通过 dispatch 发送一个 action。Redux 接收到这个 action 后通过 reducer 函数获取到下一个状态。将新状态更新进 store,store 更新后通知页面重新渲染。从这个流程中不难看出,Redux 的核心就是一个 发布-订阅 模式。一旦 store 发生了变化就会通知所有的订阅者,view 接收到通知之后会进行重新渲染。
Redux 有三大原则:
单一数据源 前面的那个例子,最终将所有的状态放到了顶层组件的 state 中,这个 state 形成了一棵状态树。在 Redux 中,这个 state 则是 store,一个应用中一般只有一个 store。State 是只读的 在 Redux 中,唯一改变 state 的方法是触发 action,action 描述了这次修改行为的相关信息。只允许通过 action 修改可以使应用中的每个状态修改都很清晰,便于后期的调试和回放。通过纯函数来修改 为了描述 action 使状态如何修改,需要你编写 reducer 函数来修改状态。reducer 函数接收前一次的 state 和 action,返回新的 state。无论被调用多少次,只要传入相同的 state 和 action,那么就一定返回同样的结果。关于 Redux 的用法,这里不做详细讲解,建议参考阮一峰老师的《Redux 入门》系列的教程:Redux 入门教程
3.1 实现 store在 Redux 中,store 一般通过 createStore 来创建。
import { createStore } from 'redux'; const store = createStore(rootReducer, initalStore, middleware);复制代码
先看一下 Redux 中暴露出来的几个方法。
其中 createStore 返回的方法主要有 subscribe、dispatch、replaceReducer、getState。
createStore 接收三个参数,分别是 reducers 函数、初始值 initalStore、中间件 middleware。
store 上挂载了 getState、dispatch、subscribe 三个方法。
getState 是获取到 store 的方法,可以通过 store.getState() 获取到 store。
dispatch 是发送 action 的方法,它接收一个 action 对象,通知 store 去执行 reducer 函数。
subscribe 则是一个监听方法,它可以监听到 store 的变化,所以可以通过 subscribe 将 Redux 和其他框架结合起来。
replaceReducer 用来异步注入 reducer 的方法,可以传入新的 reducer 来代替当前的 reducer。
3.2 实现 getStatestore 的实现原理比较简单,就是根据传入的初始值来创建一个对象。利用闭包的特性来保留这个 store,允许通过 getState 来获取到 store。
之所以通过 getState 来获取 store 是为了获取到当前 store 的快照,这样便于打印日志以对比前后两次 store 变化,方便调试。
const createStore = (reducers, initialState, enhancer) => {let store = initialState;const getState = () => store;return {getState}}复制代码
当然,现在这个 store 实现的比较简单,毕竟 createStore 还有两个参数没用到呢。 先别急,这俩参数后面会用到的。
3.3 实现 subscribe && unsubscribe既然 Redux 本质上是一个 发布-订阅 模式,那么就一定会有一个监听方法,类似 jQuery 中的 $.on,在 Redux 中提供了监听和解除监听的两个方法。 实现方式也比较简单,使用一个数组来保存所有监听的方法。
const createStore = (...) => {...let listeners = [];const subscribe = (listener) => {listeners.push(listener);}const unsubscribe = (listener) => {const index = listeners.indexOf(listener)listeners.splice(index, 1)}}复制代码3.4 实现 dispatch
dispatch 和 action 是息息相关的,只有通过 dispatch 才能发送 action。而发送 action 之后才会执行 subscribe 监听到的那些方法。
所以 dispatch 做的事情就是将 action 传给 reducer 函数,将执行后的结果设置为新的 store,然后执行 listeners 中的方法。
const createStore = (reducers, initialState) => {...let store = initialState;const dispatch = (action) => {store = reducers(store, action);listeners.forEach(listener => listener())}}复制代码
这样就行了吗?当然还不够。如果有多个 action 同时发送,这样很难说清楚最后的 store 到底是什么样的,所以需要加锁。在 Redux 中 dispatch 执行后的返回值也是当前的 action。
const createStore = (reducers, initialState) => {...let store = initialState;let isDispatch = false;const dispatch = (action) => {if (isDispatch) return action// dispatch必须一个个来isDispatch = truestore = reducers(store, action);isDispatch = falselisteners.forEach(listener => listener())return action;}}复制代码
至此为止,Redux 工作流程的原理就已经实现了。但你可能还会有很多疑问,如果没有传 initialState,那么 store 的默认值是什么呢?如果传入了中间件,那么又是什么工作原理呢?
3.5 实现 combineReducers在刚开始接触 Redux 的 store 的时候,我们都会有一种疑问,store 的结构究竟是怎么定的?combineReducers 会揭开这个谜底。
现在来分析 createStore 接收的第一个参数,这个参数有两种形式,一种直接是一个 reducer 函数,另一个是用 combineReducers 把多个 reducer 函数合并到一起。
可以猜测 combineReducers 是一个高阶函数,接收一个对象作为参数,返回了一个新的函数。这个新的函数应当和普通的 reducer 函数传参保持一致。
const combineReducers = (reducers) => {return function combination(state = {}, action) {}}复制代码
那么 combineReducers 做了什么工作呢?主要是下面几步:
收集所有传入的 reducer 函数在 dispatch 中执行 combination 函数时,遍历执行所有 reducer 函数。如果某个 reducer 函数返回了新的 state,那么 combination 就返回这个 state,否则就返回传入的 state。const combineReducers = reducers => {const finalReducers = {},nativeKeys = Object.keys// 收集所有的 reducer 函数nativeKeys(reducers).forEach(reducerKey => {if(typeof reducers[reducerKey] === "function") {finalReducers[reducerKey] = reducers[reducerKey]}})return function combination(state, action) {let hasChanged = false;const store = {};// 遍历执行 reducer 函数nativeKeys(finalReducers).forEach(key => {const reducer = finalReducers[key];// 很明显,store 的 key 来源于 reducers 的 key 值const nextState = reducer(state[key], action)store[key] = nextStatehasChanged = hasChanged || nextState !== state[key];})return hasChanged ? nextState : state;}}复制代码
细心的童鞋一定会发现,每次调用 dispatch 都会执行这个 combination 的话,那岂不是不管我发送什么类型的 action,所有的 reducer 函数都会被执行一遍?
如果 reducer 函数很多,那这个执行效率不会很低吗?但不执行貌似又无法完全匹配到 switch...case 中的 action.type。
如果能通过键值对的形式来匹配 action.type 和 reducer 是不是效率更高一些?类似这样:
// reduxconst count = (state = 0, action) => {switch(action.type) {case 'increment':return state + action.payload;case 'decrement':return state - action.payload;default:return state;}}// 改进后的const count = {state: 0, // 初始 statereducers: {increment: (state, payload) => state + payload,decrement: (state, payload) => state - payload}}复制代码
这样每次发送新的 action 的时候,可以直接用 reducers 下面的 key 值来匹配了,无需进行暴力的遍历。
天啊,你实在太聪明了。小声告诉你,社区中一些类 Redux 的方案就是这样做的。
以 rematch 和 relite 为例: rematch:
import { init, dispatch } from "@rematch/core";import delay from "./makeMeWait";const count = { state: 0, reducers: {increment: (state, payload) => state + payload,decrement: (state, payload) => state - payload }, effects: {async incrementAsync(payload) {await delay();this.increment(payload);} }};const store = init({ models: { count }});dispatch.count.incrementAsync(1);复制代码
relite:
const increment = (state, payload) => {state.count = state.count + payload;return state;}const decrement = (state, payload) => {state.count = state.count - payload;return state;}复制代码3.6 中间件 和 Store Enhancer
考虑到这样的情况,我想要打印每次 action 的相关信息以及 store 前后的变化,那我只能到每个 dispatch 处手动打印信息,这样繁琐且重复。
createStore 中提供的第三个参数,可以实现对 dispatch 函数的增强,我们称之为 Store Enhancer。
Store Enhancer 是一个高阶函数,它的结构一般是这样的:
const enhancer = () => {return (createStore) => (reducer, initState, enhancer) => {...}}复制代码
enhancer 接收 createStore 作为参数,最后返回的是一个加强版的 store,本质上是对 dispatch 函数进行了扩展。 logger:
const logger = () => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer);const dispatch = (action) => {console.log(`action=${JSON.stringify(action)}`);const result = store.dispatch(action);const state = store.getState();console.log(`state=${JSON.stringify(state)}`);return result;}return {...state,dispatch}}}复制代码
createStore 中如何使用呢?一般在参数的时候,会直接返回。
const createStore = (reducer, initialState, enhancer) => {if (enhancer && typeof enhancer === "function") {return enhancer(createStore)(reducer, initialState)}}复制代码
如果你有看过 applyMiddleware 的源码,会发现这两者实现方式很相似。applyMiddleware 本质上就是一个 Store Enhancer。
3.7 实现 applyMiddleware在创建 store 的时候,经常会使用很多中间件,通过 applyMiddleware 将多个中间件注入到 store 之中。
const store = createStore(reducers, initialStore, applyMiddleware(thunk, logger, reselect));复制代码
applyMiddleware 的实现类似上面的 Store Enhancer。由于多个中间件可以串行使用,因此最终会像洋葱模型一样,action 传递需要经过一个个中间件处理,所以中间件做的事情就是增强 dispatch 的能力,将 action 传递给下一个中间件。
那么关键就是将新的 store 和 dispatch 函数传给下一个中间件。
来看一下 applyMiddleware 的源码实现:
const applyMiddleware = (...middlewares) => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer)const middlewareAPI = {getState: store.getState,dispatch: (action) => dispatch(action)}let chain = middlewares.map(middleware => middleware(middlewareAPI))store.dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}}复制代码
这里用到了一个 compose 函数,compose 函数类似管道,可以将多个函数组合起来。compose(m1, m2)(dispatch) 等价于 m1(m2(dispatch))。 使用 reduce 函数可以实现函数组合。
const compose = (...funcs) => {if (!funcs) {return args => args}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}复制代码
再来看一下 redux-logger 中间件的精简实现,会发现两者恰好能匹配到一起。
function logger(middlewareAPI) { return function (next) { // next 即 dispatchreturn function (action) {console.log('dispatch 前:', middlewareAPI.getState());var returnValue = next(action);console.log('dispatch 后:', middlewareAPI.getState(), ' ');return returnValue;}; };}复制代码
至此为止,Redux 的基本原理就很清晰了,最后整理一个精简版的 Redux 源码实现。
// 这里需要对参数为0或1的情况进行判断const compose = (...funcs) => {if (!funcs) {return args => args}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((f1, f2) => (...args) => f1(f2(...args)))}const bindActionCreator = (action, dispatch) => {return (...args) => dispatch(action(...args))}const createStore = (reducer, initState, enhancer) => {if (!enhancer && typeof initState === "function") {enhancer = initStateinitState = null}if (enhancer && typeof enhancer === "function") {return enhancer(createStore)(reducer, initState)}let store = initState,listeners = [],isDispatch = false;const getState = () => storeconst dispatch = (action) => {if (isDispatch) return action// dispatch必须一个个来isDispatch = truestore = reducer(store, action)isDispatch = falselisteners.forEach(listener => listener())return action}const subscribe = (listener) => {if (typeof listener === "function") {listeners.push(listener)}return () => unsubscribe(listener)}const unsubscribe = (listener) => {const index = listeners.indexOf(listener)listeners.splice(index, 1)}return {getState,dispatch,subscribe,unsubscribe}}const applyMiddleware = (...middlewares) => {return (createStore) => (reducer, initState, enhancer) => {const store = createStore(reducer, initState, enhancer);const middlewareAPI = {getState: store.getState,dispatch: (action) => dispatch(action)}let chain = middlewares.map(middleware => middleware(middlewareAPI))store.dispatch = compose(...chain)(store.dispatch)return {...store}}}const combineReducers = reducers => {const finalReducers = {},nativeKeys = Object.keysnativeKeys(reducers).forEach(reducerKey => {if(typeof reducers[reducerKey] === "function") {finalReducers[reducerKey] = reducers[reducerKey]}})return (state, action) => {const store = {}nativeKeys(finalReducers).forEach(key => {const reducer = finalReducers[key]const nextState = reducer(state[key], action)store[key] = nextState})return store}}复制代码4. 实现一个 react-redux
如果想要将 Redux 结合 React 使用的话,通常可以使用 react-redux 这个库。 看过前面 Redux 的原理后,相信你也知道 react-redux 是如何实现的了吧。
react-redux 一共提供了两个 API,分别是 connect 和 Provider,前者是一个 React 高阶组件,后者是一个普通的 React 组件。react-redux 实现了一个简单的***发布-订阅***库,来监听当前 store 的变化。
两者的作用如下:
Provider:将 store 通过 Context 传给后代组件,注册对 store 的监听。connect:一旦 store 变化就会执行 mapStateToProps 和 mapDispatchToProps 获取最新的 props 后,将其传给子组件。使用方式:
// ProviderReactDOM.render({<Provider store={store}></Provider>,document.getElementById('app')})// connect@connect(mapStateToProps, mapDispatchToProps)class App extends Component {}复制代码4.1 实现 Provider
先来实现简单的 Provider,已知 Provider 会使用 Context 来传递 store,所以 Provider 直接通过 Context.Provider 将 store 给子组件。
// Context.jsconst ReactReduxContext = createContext(null);// Provider.jsconst Provider = ({ store, children }) => {return (<ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>)}复制代码
Provider 里面还需要一个***发布-订阅器***。
class Subscription {constructor(store) {this.store = store;this.listeners = [this.handleChangeWrapper];}notify = () => {this.listeners.forEach(listener => {listener()});}addListener(listener) {this.listeners.push(listener);}// 监听 storetrySubscribe() {this.unsubscribe = this.store.subscribe(this.notify);}// onStateChange 需要在组件中设置handleChangeWrapper = () => {if (this.onStateChange) {this.onStateChange()}}unsubscribe() {this.listeners = null;this.unsubscribe();}}复制代码
将 Provider 和 Subscription 结合到一起,在 useEffect 里面注册监听。
// Provider.jsconst Provider = ({ store, children }) => {const contextValue = useMemo(() => {const subscription = new Subscription(store);return {store,subscription}}, [store]);// 监听 store 变化useEffect(() => {const { subscription } = contextValue;subscription.trySubscribe();return () => {subscription.unsubscribe();}}, [contextValue]);return (<ReactReduxContext.Provider value={contextValue}>{children}</ReactReduxContext.Provider>)}复制代码4.2 实现 connect
再来看 connect 的实现,这里主要有三步:
使用 useContext 获取到传入的 store 和 subscription。对 subscription 添加一个 listener,这个 listener 的作用就是一旦 store 变化就重新渲染组件。store 变化之后,执行 mapStateToProps 和 mapDispatchToProps 两个函数,将其和传入的 props 进行合并,最终传给 WrappedComponent。先来实现简单的获取 Context。
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {return function Connect(props) {const { store, subscription } = useContext(ReactReduxContext);return <WrappedComponent {...props} />}}复制代码
接下来就要来实现如何在 store 变化的时候更新这个组件。
我们都知道在 React 中想实现更新组件只有手动设置 state 和调用 forceUpdate 两种方法,这里使用 useState 每次设置一个 count 来触发更新。
const connect = (mapStateToProps, mapDispatchToProps) => {return (WrappedComponent) => {return (props) => {const { store, subscription } = useContext(ReactReduxContext);const [count, setCount] = useState(0)useEffect(() => {subscription.onStateChange = () => setCount(count + 1)}, [count])const newProps = useMemo(() => {const stateProps = mapStateToProps(store.getState()),dispatchProps = mapDispatchToProps(store.dispatch);return {...stateProps,...dispatchProps,...props}}, [props, store, count])return <WrappedComponent {...newProps} />}}}复制代码
react-redux 的原理和上面比较类似,这里只作为学习原理的一个例子,不建议用到生产环境中。
5. 如何设计 store在开发中,如果想要查看当前页面的 store 结构,可以使用 Redux-DevTools 或者 React Developer Tools 这两个 chrome 插件来查看。
前者一般用于开发环境中,可以将 store 及其变化可视化展示出来。后者主要用于 React,也可以查看 store。
关于 Redux 中 store 如何设计对初学者来说一直都是难题,在我看来这不仅是 Redux 的问题,在任何前端 store 设计中应该都是一样的。
5.1 store 设计误区这里以知乎的问题页 store 设计为例。在开始之前,先安装 React Developer Tools,在 RDT 的 Tab 选中根节点。
然后在 Console 里面输入 $r.state.store.getState(),将 store 打印出来。
可以看到 store 中有一个 entities 属性,这个属性中分别有 users、questions、answer 等等。
这是一个问题页,自然包括问题、回答、回答下面的评论 等等。
一般情况下,这里应该是当进入页面的时候,根据 question_id 来分批从后端获取到所有的回答。点开评论的时候,会根据 answer_id 来分批从后端获取到所有的评论。
所以你可能会想到 store 结构应当这样设计,就像俄罗斯套娃一样,一层套着一套。
{questions: [{content: 'LOL中哪个英雄最能表达出你对刺客的想象?',question_id: '1',answers: [{answer_id: '1-1',content: '我就是来提名一个已经式微的英雄的。没错,就是提莫队长...'comments: [{comment_id: '1-1-1',content: '言语精炼,每一句话都是一幅画面,一组镜头'}]}]}]}复制代码
看图可以更直观感受数据结构:
这是初学者经常进入的一个误区,按照 API 来设计 store 结构,这种方法是错误的。

以评论区回复为例子,如何将评论和回复的评论关联起来呢?也许你会想,把回复的评论当做评论的子评论不就行了吗?
{comments: [{comment_id: '1-1-1',content: '言语精炼,每一句话都是一幅画面,一组镜头',children: [{comment_id: '1-1-2',content: '我感觉是好多画面,一部电影。。。'}]},{comment_id: '1-1-2',content: '我感觉是好多画面,一部电影。。。'}]}复制代码
这样挺好的,满足了我们的需求,但 children 中的评论和 comments 中的评论数据亢余了。
5.2 扁平化 store聪明的你一定会想到,如果 children 中只保存 comment_id 不就好了吗?展示的时候只要根据 comment_id 从 comments 中查询就行了。
这就是设计 store 的精髓所在了。我们可以将 store 当做一个数据库,store 中的状态按照领域(domain)来划分成一张张数据表。不同的数据表之间以主键来关联。
因此上面的 store 可以设计成三张表,分别是 questions、answers、comments,以它们的 id 作为 key,增加一个新的字段来关联子级。
{questions: {'1': {id: '1',content: 'LOL中哪个英雄最能表达出你对刺客的想象?',answers: ['1-1']}},answers: {'1-1': {id: '1-1',content: '我就是来提名一个已经式微的英雄的。没错,就是提莫队长...',comments: ['1-1-1', '1-1-2']}},comments: {'1-1-1': {id: '1-1-1',content: '言语精炼,每一句话都是一幅画面,一组镜头',children: ['1-1-2']},'1-1-2': {id: '1-1-2',content: '我感觉是好多画面,一部电影。。。'}}}复制代码
你会发现数据结构变得非常扁平化,避免了数据亢余以及嵌套过深的问题。在查找的时候也可以直接通过 id 来查找,避免了通过索引来查找某一具体项。
6. 最后我花了大半年时间写了一本前端进阶教程,从基础知识到框架原理,从编程技巧到设计模式等等。面向初中级前端进阶,教你如何搞定业务代码,助力升职加薪,大家有兴趣的可以来支持一下。
Web前端开发修炼指南 —— 前端进阶必备,未来少走弯路
Web前端开发修炼指南 原链接: