什么是React
React是起源于Facebook的一个前端框架,用于构建用户界面的JavaScript库,Facebook用来探索一种更加高效优雅的Javascript MVC框架来架设Instagram网站用的,后来觉得还不错,于是开源出来。
- 官方开源地址:https://github.com/facebook/react
- 官方案例地址:https://reactjs.org
- FaceBook开源官网:https://opensource.facebook.com/projects/
React特性
- 声明式
React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。
以声明式编写 UI,可以让你的代码更加可靠,且方便调试。
- 组件化
创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。
组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。
- 一次学习,随处编写
无论你现在正在使用什么技术栈,你都可以随时引入 React 来开发新特性,而不需要重写现有代码。
React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。
安装最基础的环境NPM
React依赖NPM(Node.js Package Manager)来安装,所以我们可以先安装Node.Js环境。
Node.Js会自动带NPM组件和自动安装配套的可选组件,非常简便。
v14.15.1 LTS长期支持版:https://nodejs.org/dist/v14.15.1/node-v14.15.1-x64.msi
安装推荐的集成编辑器Visual Studio Code
最新Stable版:https://aka.ms/win32-x64-user-stable
官网:https://code.visualstudio.com/
创建项目目录并进行NPM初始化
创建一个名为zeroreact
的文件夹,用Visual Studio Code来进行打开,在终端界面,执行如下命令,进行NPM初始化。
npm init
一路回车就行了,创建后还能继续编辑的。
初始化完成之后,会看到当前项目根目录会新建一个叫package.json
的文件,其内容如下:
{
"name": "zeroreact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC"
}
其实中main
是一个Node项目指向的开始文件,但是没有也可以。
创建Visual Studio Code的调式配置Launch.json
切换到Visual Studio Code左侧的运行菜单,点击创建Launch.json文件
,选择你中意的可选浏览器平台即可。
当前项目根目录会自动生成一个Launch.json配置文件,这个配置就是调式配置,它指向了调式项目时启动哪个平台。
其内容如下(以Edge:Launch为例):
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-msedge",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
配置好了之后,启动调式即可打开对应的浏览器,并且自动打开其中url
配置所对应的地址值。
关于React实际必选项JSX
虽然React官方说,React可以不依赖JSX去运行,但是实际上JSX可以说是必选项。
而JSX是什么呢?
JSX是一种JavaScript的语法扩展,运用于React架构中,其格式比较像是模版语言,但事实上完全是在JavaScript内部实现的。元素是构成React应用的最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面。
注意的是,JSX的特性更接近JavaScript而不是HTML,所以React DOM使用camelCase(小驼峰)命名来定义属性的名称,而不是使用HTML的属性名称。例如:class变成了className,而tableindex则对应着tableIndex
JSX基本格式:
- 简单闭合
const element = <img src={user.avatarUrl} />;
- 嵌套闭合
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
这些有趣的标签语法既不是字符串也不是 HTML。
它被称为JSX,是一个JavaScript的语法扩展。建议在React中配合使用JSX,JSX可以很好地描述UI应该呈现出它应有交互的本质形式。JSX可能会使人联想到模版语言,但它具有JavaScript的全部功能。
JSX可以生成React“元素”。
安装JSX所需的WebPack和Babel组件
React中的JSX实际上是通过Babel组件,把JSX的代码最终翻译成普通的Javascript代码的。
Babel转译器会把JSX转换成一个名为React.createElement()的方法调用
安装WebPack
什么是WebPack呢?
WebPack其核心在于让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。不仅仅是JavaScript文件,我们的CSS、图片、json文件等等在webpack中都可以被当做模块来使用,webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作,接着我们只要处理最后那个js文件即可。
为什么要用WebPack呢?
WebPack其实它是一个Javascript的打包工具,它的输入和产出在正常情况下都是Javascript的文件,它最大的作用就是帮助我们把Javascript里面的import和require,把多文件打包成一个单个的Js文件。所以说呢,webpack往往是由一个文件作为入口,这个文件可能会import一些东西,可能会require一些东西,不管你用哪种写法都是可以的,然后它最终把它变成一个单个的大的文件,这样呢,比较符合我们在Web上的性能还有发布各方面的一些需求,当然WebPack它还同时承载了很多工具,其中就包括接下来会说到的前端重要的工具Babel。
npm install webpack webpack-cli --save-dev
通过以上命令,同时安装webpack
和webpack-cli
两个组件,并且--save-dev
表示这两个组件会被加到package.json
中的devDependencies
节点中去。
注意:这时候,如果你想直接使用webpack
指令,是不可行的,因为它没有被全局安装,如果你需要可以通过npm install xxx -g
这种形式去安装,但是目前已经不被推荐了。
我们可以采用被推荐的如下命令来调用WebPack
:
npx webpack
然后你会看到有个报错。
其实,webpack命令是执行了,但是因为我们没有给webpack做对应的配置及入口文件,所以它最终执行失败。
这时候,我们可以在根目录新建一个叫webpack.config.js
的文件,根据Node的标准,我们可以用module.exports
的写法,内如如下:
module.exports={
entry:{
main: './main.js'
}
}
其中entry
就是入口的意思,然后main
是默认入口,main
的指向我们可以暂时先给一个main.js
文件,同时我们需要在根目录新建一个空的main.js
文件。
完成以上配置之后,我们可以再次执行
npx webpack
即可看到打包成功的信息了,同时你会发现会新建一个dist
目录来输出WebPack
最终打包好的Js文件。
打包后的main.js是一段被压缩的JS代码,可阅读性呢不是很好,如果我们想看到更加可阅读性的JS代码,可以在webpack.config.js
增加开发阶段的配置:
module.exports={
entry:{
main: './main.js'
},
mode: 'development',
optimization:{
minimize: false
}
}
新增配置项mode
=development
,optimization
.minimize
被设置成false
之后,再次执行npx webpack
会看到main.js
中会得到更加可阅读性的JS代码。
安装Babel
什么是Babel呢?
Babel是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
为什么使用Babel呢?
Babel这个工具是一个把新版本JS文件翻译成老版本JS文件的这样一种工具。
Babel在WebPack
里面是以Loader
的形式去使用的,WebPack
允许我们使用Loader
去定制各种各样的文件,比如说,原则上WebPack
只能打包普通的JS文件,但是我们如果想把CSS文件以某种形式打包成JS文件的话,那么我们就可以写一个CSS-Loader
,如果我们想把HTML当作一个JS文件去打包进来,那我们就可以写一个HTML-Loader
,而这个Loader
也可以是独立的包,我们只要在WebPack
的配置里面配一下就可以了。
接下来我们,安装Babel组件,执行如下命令:
npm install --save-dev babel-loader @babel/core @babel/preset-env
这里安装三个组件babel-loader
、@babel/core
、@babel/preset-env
,以空格隔开即可。
配置并使用Babel
我们可以通过WebPack
的Module
来配置Babel组件,其中Module
中重要的概念就是Rules
,它是一个数组,里面可以是一个对象,如下配置新增一个关于babel-loader
的rule规则配置。
module:{
rules:[
{
test: /.js$/,
use: {
loader: 'babel-loader',
options:{
presets: ['@babel/preset-env']
}
}
}
]
}
如配置所示,test
中是一个针对所有JS文件的正则表达式,会匹配所有的JS文件,那么遇到JS文件会执行use
中的loader
应用,这里指定了loader
为Babel-Loader
,这样所有的JS文件都会走Babel-Loader
来完成一次翻译。同时,我们还需要给Babel-Loader
配置一个presets
(注意presets也是一个数组值),其值是前面我们安装的@babel/preset-env
,这里的presets
可以理解为它是一系列Babel的config的一种快捷方式。
完成上诉Babel-Loader
配置之后,我们可以来实现下真正的翻译。
在main.js
中,我们可以加入一段JS代码,如下:
for(let i of [1,2,3])
{
console.log(i);
}
再次执行npx webpack
之后,我们将看到打包之后的main.js
翻译后的JS代码变成了一个eval
的for方法,这将是最终被执行的JS代码。
为了看到这段JS最终执行的效果,我们在dist目录里面新建一个main.html,来引用最终生成main.js
,并且F12看下输入效果。
可以看到,如预期结果,依次输出了1,2,3
配置并启用用于翻译JSX的Babel插件
默认Babel组件是没有能力来处理JSX的。
如果我们此时在main.js
里面写一个JSX,那么它会报错。
let a = <div />
但是有一个Babel插件是可以的,叫babel/plugin-transform-react-jsx
,接下来我们安装并配置它。
npm install @babel/plugin-transform-react-jsx --save-dev
然后我们还需要在Babel-Loader
中配置这个Plugin
,在Options
配置节点中,新建一个名为plugins
的节点,这也是一个数字,里面填入我们刚刚安装的Babel插件@babel/plugin-transform-react-jsx
。
module:{
rules:[
{
test: /.js$/,
use: {
loader: 'babel-loader',
options:{
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-react-jsx']
}
}
}
]
}
再次执行npx webpack
,就会发现,带JSX语法的JS已经可以正常翻译了,得到后的JS如图:
这里我们看到<div/>
被翻译为一个React.createElement("div", null);
的方法了。
备注:
可以看到,默认它会被一个叫React
的函数来调用createElement
方法。
这里我们也可以通过配置来修改调React.createElement
这个名字,这里@babel/plugin-transform-react-jsx
支持一个叫pragma
的参数,这里可以指定你喜欢的名字,比如我这里的zero_react_create
plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'zero_react_create'}]]
这样翻译后会变成你自定义的名字:
探索JSX语法糖机制
带属性的JSX
let a = <div id="a" class="c"/>
翻译后:
var a = zero_react_create("div", {
id: "a",
"class": "c"
});
带子节点的JSX
let a = <div id="a" class="c">
<div/>
<div/>
<div/>
</div>
翻译后:
var a = zero_react_create("div", {
id: "a",
"class": "c"
}, zero_react_create("div", null), zero_react_create("div", null), zero_react_create("div", null));
发现子节点都会追加到后面了。
构造zero_react_create
函数,并且输出它
function zero_react_create(tagName, attributes, ...children){
return document.createElement(tagName);
}
window.a = <div id="a" class="c">
<div/>
<div/>
<div/>
</div>
翻译执行后,可以在浏览器console里面输入a,回车看到a的输入值:
应用子节点,并且输出它
function zero_react_create(tagName, attributes, ...children){
let e = document.createElement(tagName);
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
for(let child of children){
e.appendChild(child);
}
return e;
}
window.a = <div id="a" class="c">
<div/>
<div/>
<div/>
</div>
翻译后执行:
子节点中包含值,并且输出它
function zero_react_create(tagName, attributes, ...children){
let e = document.createElement(tagName);
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
for(let child of children){
if(typeof child === 'string'){
child = document.createTextNode(child);
}
e.appendChild(child);
}
return e;
}
window.a = <div id="a" class="c">
<div>abc</div>
<div/>
<div/>
</div>
这里的变化是,如果子节点类型是个字符串,那我们就把Child变成一个文本节点。
翻译后:
将输出挂载到Body里面
为了挂载到Body,我们现在main.html
新建好Body标签:
<body>
<script src="main.js"></script>
</body>
然后修改JSX代码如下:
document.body.appendChild(<div id="a" class="c">
<div>abc</div>
<div/>
<div/>
</div>);
翻译后执行,就可以把我们JSX描述的HTML及数据,成功的以DOM形式挂载到Boby里面了
这样我们就实现了一种完全基于实DOM的zero_react_create
方法。
探索JSX自定义组件机制
初探自定义标签
在JSX中有个规定,如果你的Tag是小写,比如div
,它就认为这是一种原生的标签,如果是大写开头的,那就认为是自定义组件,比如我们将div
改成MyComponent
document.body.appendChild(<MyComponent id="a" class="c">
<div>abc</div>
<div/>
<div/>
</MyComponent>);
翻译后运行,会得到一个报错。
在这里,MyComponent
变成了我们自定义的一个对象,或者Class或者函数
这里我们先做Class
处理。
class MyComponent{
}
翻译后执行,会得到一个报错
这是因为zero_react_create
方法中TagName
这时候已经不是一个原始的标签字符串了,在执行let e = document.createElement(tagName);
会报错,因为这时候TagName
已经变成了一个对象。
这里,我们修改下zero_react_create
方法的判断。
function zero_react_create(tagName, attributes, ...children){
let e;
if(typeof tagName === 'string'){
e = document.createElement(tagName);
}
else
{
e = new tagName;
}
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
for(let child of children){
if(typeof child === 'string'){
child = document.createTextNode(child);
}
e.appendChild(child);
}
return e;
}
翻译运行后发现报错如下:
这里是因为,e这时候已经不是一个原生对象了,那我们自然有很多原始对象可以支持的就运行不了了,所以这里e.setAttribute(p, attributes[p]);
自然会报错。
这时候,我们可以给所有原生的DOM对象,都加一个Wrapper,让它可以正确的执行下去。
开启自定义组件之路
我们新建一个名为zero-react.js
的文件,把前面main.js
那个zero_react_create
函数搬过来,并且Export
出来。
export function zero_react_create(tagName, attributes, ...children){
let e;
if(typeof tagName === 'string'){
e = document.createElement(tagName);
}
else
{
e = new tagName;
}
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
for(let child of children){
if(typeof child === 'string'){
child = document.createTextNode(child);
}
e.appendChild(child);
}
return e;
}
并且在main.js
中import
文件zero-react.js
。
import { zero_react_create } from './zero-react.js'
重新编写zero-react.js
对外公开一个Component
组件,所有自定义组件继承它
export class Component{
constructor(){
this.props = Object.create(null);
this.children = [];
this._root = null;
}
setAttribute(name, value){
this.props[name] = value;
}
appendChild(component){
this.children.push(component);
}
get root(){
if(!this._root){
this._root = this.render().root;
}
return this._root;
}
}
新建一个TextWrapper
来处理纯文本子节点。
class TextWrapper{
constructor(content){
this.root = document.createTextNode(content);
}
}
新建一个ElementWrapper
来处理原生标签节点。
class ElementWrapper{
constructor(tagName){
this.root = document.createElement(tagName);
}
setAttribute(name, value){
this.root.setAttribute(name, value);
}
appendChild(component){
this.root.appendChild(component.root);
}
}
替换原来zero_react_create
函数中的两个实现
其中把e = document.createElement(tagName);
替换成e = new ElementWrapper(tagName);
,把child = document.createTextNode(child);
替换成child = new TextWrapper(child);
,得到如下新的zero_react_create
函数
export function zero_react_create(tagName, attributes, ...children){
let e;
if(typeof tagName === 'string'){
e = new ElementWrapper(tagName);
}
else
{
e = new tagName;
}
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
for(let child of children){
if(typeof child === 'string'){
child = new TextWrapper(child);
}
e.appendChild(child);
}
return e;
}
最后对外公开一个render
函数
export function render(component, parentElement){
parentElement.appendChild(component.root);
}
修改main.js
中MyComponent
的实现。
import { render, Component, zero_react_create } from './zero-react.js'
class MyComponent extends Component{
render(){
return <div>zero component</div>
}
}
render(<MyComponent id="a" class="c">
<div>abc</div>
<div/>
<div/>
</MyComponent>, document.body);
翻译后,运行可得:
但是我们还看不到子节点元素,这时候如果把子节点元素传进来:
class MyComponent extends Component{
render(){
return <div><h1>zero component</h1>
{this.children}
</div>
}
}
运行会报错:
因为这时候子节点会把当作一个数组传进去,但是原来child = new TextWrapper(child);
处是没有能力处理这种情况的。
这时候,我们需要改造下这里。
export function zero_react_create(tagName, attributes, ...children){
let e;
if(typeof tagName === 'string'){
e = new ElementWrapper(tagName);
}
else
{
e = new tagName;
}
for(let p in attributes){
e.setAttribute(p, attributes[p]);
}
let insertChilder = (children) => {
for(let child of children){
if(typeof child === 'string'){
child = new TextWrapper(child);
}
if((typeof child === 'object') && (child instanceof Array)){
insertChilder(child);
}
else{
e.appendChild(child);
}
}
}
insertChilder(children);
return e;
}
翻译执行后,就可以看到成功的处理了带多个子节点的情况了。
于是我们就打造了一个属于自己的自定义组件处理引擎了。