React进阶之react.js、jsx模板语法及babel编译
React
- React介绍
- React官网初识
- React学习
- MVC
- MVVM
- JSX
- 外部的元素props和内部的状态state
- props
- state
- 生命周期
- constructor
- getDerivedStateFromProps
- render
- componentDidMount()
- shouldComponentUpdate
- getSnapshotBeforeUpdate(prevProps, prevState)
- 创建项目
- CRA:create-react-app
- create-react-app源码解析
- packages
- package/create-react-app
- index.js
- createReactApp.js
React介绍
React官网初识
react 官网
react github
团队中选型时候,如果没有历史包的话,就选择最新的react,如果有历史包的话,选择不影响历史包升级的版本
但是最新的也容易踩到坑
在React19中,主要看 新特性和带来的提升即可
新特性:
form类似于 react/field-form
这里的use可以理解为,添加响应式的逻辑
性能上的提升:
ref可以作为props,平常项目中使用的是improve API 去关联,而无法用props来关联
React学习
React 中小厂用的不多,但是大厂用的都是React
React:构建web和原生交互界面的库,React组件
本质上就是一个函数
- 像Vue一样支持
组件
的方式去拓展 - 支持原生元素标签,并且能够在标签中通过js的方法去执行
- 接收props,和state
…
与Vue相比较的话,React有一个典型的特性,React是单向数据流的
我们的视图
是由我们的交互
所决定的
ui视图 = render(data) 由数据所触发ui视图的变化
官网中,把这些内容看完就足够了
MVC
MVC model view controller
是一个架构的设计思路
controller:控制器 操纵数据导致视图变化
modal:数据
view:视图 视图中事件的触发也能够触发controller
// model
var myapp = {}; // 创建这个应用对象
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
/* 观察者模式 /
var self = this,
views = [];
this.register = function(view) {
views.push(view);
};
this.notify = function() {
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};
// view
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
/ 绑定事件 /
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
// controller
myapp.Controller = function() {
var model = null,
view = null;
this.init = function() {
/ 初始化Model和View /
model = new myapp.Model();
view = new myapp.View(this);
/ View向Model注册,当Model更新就会去通知View啦 /
model.register(view);
model.notify();
};
/ 让Model更新数值并通知View更新视图 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};
// init
(function() {
var controller = new myapp.Controller();
controller.init();
})();
MVVM
MVVM: model viewModel view 数据的双向绑定
// model
var data = {
val: 0
};
// view
<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>
// controller
new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v;
}
},
sub(v) {
if(this.val > 0) {
this.val -= v;
}
}
}
});
// Vue是不是MVVM?React呢?
// 严格来讲都不是。
// Vue操作的是虚拟的dom,而不是真实的dom节点,通过ref.current获取的结果,从虚拟上能够直接指向真实dom
// React:ui = render (data) 单向数据流
// Vue: ref 直接操作DOM,跳过了ViewModel
JSX
function VideoList({ videos, emptyHeading }) {
const count = videos.length;
let heading = emptyHeading;
if (count > 0) {
const noun = count > 1 ? 'Videos' : 'Video';
heading = count + ' ' + noun;
}
return (
<section>
<h2>{heading}</h2>
{videos.map(video =>
<Video key={video.id} video={video} />
)}
</section>
);
}
模板化的语言写法,需要注意的是,怎样将模板化的语言写法,转化为对象,最终转化为我们真实的dom
Vue和React都是通过AST去转化的,将我们这样的模板语言转化为js对象的能力:通过 babel
babel 是 js的compiler
compiler是代码执行之前,需要将代码从一种格式转换为另一种格式,方便后续代码的识别和执行
runtime是jsx代码中最终去消费babel编译后的产物,交由给_jsxs去做运行时的逻辑
babel
babel 本质:core-js,定义的js的各种各样的ecma标准都能做到
babel:将jsx模板写法转化为js对象的能力
jsx本质:借助于Babel的 @babel/preset-react 提供的包的能力转换出来的js对象,在compiler代码编译过程中做到的
绑定事件:
这个事件称之为,合成事件,并不是直接绑定到元素上的,通过驼峰的方式,将方法绑定到属性上
写业务开发的市场饱和了,要求是写业务代码基础上,能够将写业务代码的工具能掌握好
外部的元素props和内部的状态state
类似于函数
function bar(a,b){
const d=1
}
a,b就像是props,而d就像是state
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
props和state是怎么影响视图逻辑的?
组件的本质要么是类,要么是函数
props
export default function Profile(){
return (
<Avatar
person={{name:'Lin Lanying',imageId:'1dx5qh6'}}
size={100}
/>
)
}
interface IAvatarProps(){
person?: Record<string,any>;
size?: number|string;
}
function Avatar(props: IAvatarProps){
const {person,size=10}=props;
}
也可以这样作解构:
function Avatar({person,size}){
const {person,size=10}=props;
}
state
useXXX => hooks的标志
import { sculptureList } from './data.js';
import { useState } from 'react';
export default function Gallery() {
const [index,setIndex]=useState(0) //hooks
const [showMore,setShowMore]=useState(false) //hooks
function handleClick() {
setIndex(index+1)
}
function handleMoreClick() {
setShowMore(!showMore)
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<button onClick={handleMoreClick}>
{showMore?'Hide':'Show'} details
</button>
{ showMore && <p>{sculpture.description}</p>}
<img
src={sculpture.url}
alt={sculpture.alt}
/>
</>
);
}
<></> => react 属性中得 Fragment 不表示任何意义,只是提供了一个容器。因为react只能接收一个根节点,因此能使用Fragment将根节点聚合起来
生命周期
- constructor:通过类引入constructor
- getDerivedStateFromProps:通过props的不同来找到state的区别
- render:实际渲染的动作
- Render阶段:react是虚拟dom,这个阶段是在内存中去执行的,并没有真正渲染
- Commit阶段:创建真实dom
- shouldComponentUpdate:判断组件是否需要更新,减少组件没有必要的重复渲染
- getSnapshotBeforeUpdate:上一次更新前的内容,提供了上一次的props和state
- componentDidUpdate:执行更新完之后的生命周期
- componentWillUnmount:组件卸载之前
constructor
class Counter extends Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// ...
}
getDerivedStateFromProps
class Form extends Component {
state = {
email: this.props.defaultEmail,
prevUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// 每当当前用户发生变化时,
// 重置与该用户关联的任何 state 部分。
// 在这个简单的示例中,只是以 email 为例。
if (props.userID !== state.prevUserID) {
return {
prevUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}
render
import { Component } from 'react';
class Greeting extends Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
componentDidMount()
class ChatRoom extends Component {
state = {
serverUrl: 'https://localhost:1234'
};
componentDidMount() {
this.setupConnection();
}
componentDidUpdate(prevProps, prevState) {
if (
this.props.roomId !== prevProps.roomId ||
this.state.serverUrl !== prevState.serverUrl
) {
this.destroyConnection();
this.setupConnection();
}
}
componentWillUnmount() {
this.destroyConnection();
}
// ...
}
shouldComponentUpdate
class Rectangle extends Component {
state = {
isHovered: false
};
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.position.x === this.props.position.x &&
nextProps.position.y === this.props.position.y &&
nextProps.size.width === this.props.size.width &&
nextProps.size.height === this.props.size.height &&
nextState.isHovered === this.state.isHovered
) {
// 没有任何改变,因此不需要重新渲染
return false;
}
return true;
}
// ...
}
getSnapshotBeforeUpdate(prevProps, prevState)
不怎么经常使用的API,应用场景少
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否要向列表中添加新内容?
// 捕获滚动的位置,以便我们稍后可以调整滚动。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们有快照值,那么说明我们刚刚添加了新内容。
// 调整滚动,使得这些新内容不会将旧内容推出视野。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 返回的值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
创建项目
pnpx create-react-app my-app
package.json:
这里的scripts里的指令本质:如何搭建一个自定义的webpack配置
CRA是脚手架,react-scripts是用CRA写的,CRA是用webpack写的
CRA:create-react-app
create-react-app
安装react,react-dom,react-scripts,基于cra-template模板安装的
创建git-commit
提供方法,react-scripts
create-react-app源码解析
create-react-app源码
- docusaurus 静态站点,包含了这些的说明
- website 静态站点
- docs 静态站点的说明
- .github
- PULL_REQUEST_TEMPLATE.md
- workflows 工作流,包含构建,通过github的钩子做的一些事情
- packages 多包结构
- lerna.json 基于lerna的多包结构
{ "lerna": "2.6.0", "npmClient": "yarn", "useWorkspaces": true, //使用多包管理 "version": "independent", //每个包的版本号不需要一致 "changelog": { //更新添加元素 "repo": "facebook/create-react-app", "labels": { "tag: new feature": ":rocket: New Feature", "tag: breaking change": ":boom: Breaking Change", "tag: bug fix": ":bug: Bug Fix", "tag: enhancement": ":nail_care: Enhancement", "tag: documentation": ":memo: Documentation", "tag: internal": ":house: Internal", "tag: underlying tools": ":hammer: Underlying Tools" }, "cacheDir": ".changelog" } }
- tasks 测试用例 通过shell脚本去写的 和test结合去用的,怎样基于测试用例执行事件
- test
- .alexrc rc,runtime config 运行时的配置
…
packages
- cra-template 模板
- create-react-app 由这个执行
pnpx create-react-app 命令
执行的是 create-react-app的package.json中的bin的指令,node作为二进制执行的入口,这个命令等效于在create-react-app目录下执行 node ./index.js
命令
package/create-react-app
index.js
'use strict';
const currentNodeVersion = process.versions.node; //20.12.2
const semver = currentNodeVersion.split('.'); //通过.分割node版本号
const major = semver[0]; //获取第一位数字,即主版本号,如20
if (major < 14) { //主版本号小于14的话就退出
console.error(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 14 or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
//执行CRA的init方法
const { init } = require('./createReactApp');
init();
createReactApp.js
主要内容:
- commander
pnpx create-react-app -h
罗列指令及功能
pnpx create-react-app --info
输出环境信息
- init方法
function init() {
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {
projectName = name; //自己定义的项目名字,这里就是my-app
})
.option('--verbose', 'print additional logs')
.option('--info', 'print environment debug info')
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)
.option(
'--template <path-to-template>',
'specify a template for the created project'
)
.option('--use-pnp')
.allowUnknownOption()
.on('--help', () => {
console.log(
` Only ${chalk.green('<project-directory>')} is required.`
);
console.log();
console.log(
` A custom ${chalk.cyan('--scripts-version')} can be one of:`
);
console.log(` - a specific npm version: ${chalk.green('0.8.2')}`);
console.log(` - a specific npm tag: ${chalk.green('@next')}`);
console.log(
` - a custom fork published on npm: ${chalk.green(
'my-react-scripts'
)}`
);
console.log(
` - a local path relative to the current working directory: ${chalk.green(
'file:../my-react-scripts'
)}`
);
console.log(
` - a .tgz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tgz'
)}`
);
console.log(
` - a .tar.gz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
)}`
);
console.log(
` It is not needed unless you specifically want to use a fork.`
);
console.log();
console.log(` A custom ${chalk.cyan('--template')} can be one of:`);
console.log(
` - a custom template published on npm: ${chalk.green(
'cra-template-typescript'
)}`
);
console.log(
` - a local path relative to the current working directory: ${chalk.green(
'file:../my-custom-template'
)}`
);
console.log(
` - a .tgz archive: ${chalk.green(
'https://mysite.com/my-custom-template-0.8.2.tgz'
)}`
);
console.log(
` - a .tar.gz archive: ${chalk.green(
'https://mysite.com/my-custom-template-0.8.2.tar.gz'
)}`
);
console.log();
console.log(
` If you have any problems, do not hesitate to file an issue:`
);
console.log(
` ${chalk.cyan(
'https://github.com/facebook/create-react-app/issues/new'
)}`
);
console.log();
})
.parse(process.argv);
if (program.info) {
console.log(chalk.bold('\nEnvironment Info:'));
console.log(
`\n current version of ${packageJson.name}: ${packageJson.version}`
);
console.log(` running from ${__dirname}`);
return envinfo //是一个会将当前环境都输出的包
.run(
{
System: ['OS', 'CPU'],
Binaries: ['Node', 'npm', 'Yarn'],
Browsers: [
'Chrome',
'Edge',
'Internet Explorer',
'Firefox',
'Safari',
],
npmPackages: ['react', 'react-dom', 'react-scripts'],
npmGlobalPackages: ['create-react-app'],
},
{
duplicates: true,
showNotFound: true,
}
)
.then(console.log);
}
if (typeof projectName === 'undefined') {
console.error('Please specify the project directory:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
);
console.log();
console.log('For example:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`
);
console.log();
console.log(
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
);
process.exit(1);
}
// We first check the registry directly via the API, and if that fails, we try
// the slower `npm view [package] version` command.
//
// This is important for users in environments where direct access to npm is
// blocked by a firewall, and packages are provided exclusively via a private
// registry.
checkForLatestVersion() //检查最新的版本号
.catch(() => {
try {
return execSync('npm view create-react-app version').toString().trim(); //执行这个命令可以得到最新的版本号
} catch (e) {
return null;
}
})
.then(latest => {
if (latest && semver.lt(packageJson.version, latest)) {
//有最新版本且没有升级到最新的版本,提示要升级到最新的版本了
console.log();
console.error(
chalk.yellow(
`You are running \`create-react-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` +
'We recommend always using the latest version of create-react-app if possible.'
)
);
console.log();
console.log(
'The latest instructions for creating a new app can be found here:\n' +
'https://create-react-app.dev/docs/getting-started/'
);
console.log();
} else {
const useYarn = isUsingYarn(); //判断是否使用yarn
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.template,
useYarn,
program.usePnp
);
}
});
}
- checkForLatestVersion 获取最新的版本号
function checkForLatestVersion() {
return new Promise((resolve, reject) => {
https //通过http发送get请求读取版本号
.get(
'https://registry.npmjs.org/-/package/create-react-app/dist-tags',//版本号
res => {
if (res.statusCode === 200) {
let body = '';
res.on('data', data => (body += data));
res.on('end', () => {
resolve(JSON.parse(body).latest);
});
} else {
reject();
}
}
)
.on('error', () => {
reject();
});
});
}
- isUsingYarn():判断是否使用yarn
function isUsingYarn() {
return (process.env.npm_config_user_agent || '').indexOf('yarn') === 0; //使用环境变量判断是否使用yarn
}
- createApp()
function createApp(name, verbose, version, template, useYarn, usePnp) {
const unsupportedNodeVersion = !semver.satisfies(
// Coerce strings with metadata (i.e. `15.0.0-nightly`).
semver.coerce(process.version),
'>=14'
);
if (unsupportedNodeVersion) {
console.log(
chalk.yellow(
`You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to Node 14 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = 'react-scripts@0.9.x';
}
const root = path.resolve(name);
const appName = path.basename(root);
checkAppName(appName);
fs.ensureDirSync(name);
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1);
}
console.log();
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
// 首先创建一个最基础版
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
// 写入
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
const originalDirectory = process.cwd();
process.chdir(root);
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
if (!useYarn) {
const npmInfo = checkNpmVersion();
if (!npmInfo.hasMinNpm) {
if (npmInfo.npmVersion) {
console.log(
chalk.yellow(
`You are using npm ${npmInfo.npmVersion} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to npm 6 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = 'react-scripts@0.9.x';
}
} else if (usePnp) {
const yarnInfo = checkYarnVersion();
if (yarnInfo.yarnVersion) {
if (!yarnInfo.hasMinYarnPnp) {
console.log(
chalk.yellow(
`You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
`Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
)
);
// 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
usePnp = false;
}
if (!yarnInfo.hasMaxYarnPnp) {
console.log(
chalk.yellow(
'The --use-pnp flag is no longer necessary with yarn 2 and will be deprecated and removed in a future release.\n'
)
);
// 2 supports PnP by default and breaks when trying to use the flag
usePnp = false;
}
}
}
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
);
}
- setCaretRangeForRuntimeDeps()
function setCaretRangeForRuntimeDeps(packageName) {
const packagePath = path.join(process.cwd(), 'package.json');
const packageJson = require(packagePath);
if (typeof packageJson.dependencies === 'undefined') {
console.error(chalk.red('Missing dependencies in package.json'));
process.exit(1);
}
const packageVersion = packageJson.dependencies[packageName];
if (typeof packageVersion === 'undefined') {
console.error(chalk.red(`Unable to find ${packageName} in package.json`));
process.exit(1);
}
// 手动注入react,react-dom
makeCaretRange(packageJson.dependencies, 'react');
makeCaretRange(packageJson.dependencies, 'react-dom');
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + os.EOL);
}
手动注入react,react-dom,然后将 cra-template 模板拼上去,就生成了cra代码
- 后面就是判断依赖,告诉有哪些异常,异常的话就过滤删除
CRA主要做的事:读取用户的输入,根据用户输入创建出对应的模板,写入到指定的目录下