本文写作目的
本文作为学习React.dev的笔记。通过阅读React.dev源码,记录其使用的技术栈,最根本目的是为了学习Next.js。
1. 网络上收集到的有用知识
检测data改变后hot reloads开发工具(nextJS本身存在热加载功能,这里对md文件等进行热加载的) :next-remote-watch
衡量用户与网站互动情况:ga-lite、10分钟学会看懂谷歌分析(Google Analytics)
一起来学 next.js - getStaticProps、getStaticPaths 篇
2. 从Markdown到React的虚拟DOM
2.1 markdown转JSON
在getStaticProps中,需要将markdown转成JSON。
注意:该JSON和虚拟DOM是有区别的,主要体现在type属性上,该JSON中每项的type属性=组件名是字符串,在虚拟DOM中type属性=函数组件是一个函数。目的是,将getStaticProps获取的值传给要渲染的组件,在组件中进行解析、type属性的替换得到最终需要的虚拟DOM。
虚拟DOM的格式,可从packages/react/src/jsx/ReactJSXElement.js知晓。
本文以src/content/learn/index.md文件为例,讲解React.dev项目是如何将它转成虚拟DOM的,在这个过程中有很多可以学习或熟悉的知识点。
(后续会将这个过程画成图,应该会非常有意思)
2.1.1 markdown转js
markdown转成"react/jsx-runtime"写的js代码。
使用fs.readFileSync(rootDir + path + '.md', 'utf8')读取到文件内容后,使用如下代码添加自定义的jsx组件:
// If we don't add these fake imports, the MDX compiler
// will insert a bunch of opaque components we can't introspect.
// This will break the prepareMDX() call below.
let mdxWithFakeImports =
mdx +
'\n\n' +
mdxComponentNames
.map((key) => 'import ' + key + ' from "' + key + '";\n')
.join('\n');
引入自定义组件后的markdown文件(本文意在说明转换过程,该markdown文件有适当删减):
"---
title: Installation
---
<Intro>
React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started.
</Intro>
<YouWillLearn isChapter={true}>
* [How to start a new React project](/learn/start-a-new-react-project)
* [How to add React to an existing project](/learn/add-react-to-an-existing-project)
* [How to set up your editor](/learn/editor-setup)
* [How to install React Developer Tools](/learn/react-developer-tools)
</YouWillLearn>
## Try React {/*try-react*/}
You don't need to install anything to play with React. Try editing this sandbox!
<Sandpack>
```js
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
export default function App() {
return <Greeting name="world" />
}
```
</Sandpack>
You can edit it directly or open it in a new tab by pressing the "Fork" button in the upper right corner.
Most pages in the React documentation contain sandboxes like this. Outside of the React documentation, there are many online sandboxes that support React: for example, [CodeSandbox](https://codesandbox.io/s/new), [StackBlitz](https://stackblitz.com/fork/react), or [CodePen.](https://codepen.io/pen?&editors=0010&layout=left&prefill_data_id=3f4569d1-1b11-4bce-bd46-89090eed5ddb)
### Try React locally {/*try-react-locally*/}
To try React locally on your computer, [download this HTML page.](https://gist.githubusercontent.com/gaearon/0275b1e1518599bbeafcde4722e79ed1/raw/db72dcbf3384ee1708c4a07d3be79860db04bff0/example.html) Open it in your editor and in your browser!
## Start a new React project {/*start-a-new-react-project*/}
If you want to build an app or a website fully with React, [start a new React project.](/learn/start-a-new-react-project)
## Add React to an existing project {/*add-react-to-an-existing-project*/}
If want to try using React in your existing app or a website, [add React to an existing project.](/learn/add-react-to-an-existing-project)
## Next steps {/*next-steps*/}
Head to the [Quick Start](/learn) guide for a tour of the most important React concepts you will encounter every day.
import p from "p";
...
import CodeDiagram from "CodeDiagram";
...
"
(1)unified生态
(2)markdown转js
Extending MDX: 介绍remark plugins(markdown),rehype plugins(html)
转换使用的代码
// Turn the MDX we just read into some JS we can execute.
const {remarkPlugins} = require('../../plugins/markdownToHtml');
const {compile: compileMdx} = await import('@mdx-js/mdx');
const visit = (await import('unist-util-visit')).default;
const jsxCode = await compileMdx(mdxWithFakeImports, {
remarkPlugins: [
...remarkPlugins,
(await import('remark-gfm')).default,
(await import('remark-frontmatter')).default,
],
rehypePlugins: [
// Support stuff like ```js App.js {1-5} active by passing it through.
function rehypeMetaAsAttributes() {
return (tree) => {
visit(tree, 'element', (node) => {
if (node.tagName === 'code' && node.data && node.data.meta) {
node.properties.meta = node.data.meta;
}
});
};
},
],
});
转换后的代码(本文意在说明转换过程,该markdown文件有适当删减):
/*@jsxRuntime automatic @jsxImportSource react*/
/*try-react*/
/*try-react-locally*/
/*start-a-new-react-project*/
/*add-react-to-an-existing-project*/
/*next-steps*/
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import p from "p";
import strong from "strong";
...
import CodeStep from "CodeStep";
import YouTubeIframe from "YouTubeIframe";
function _createMdxContent(props) {
const _components = Object.assign({
p: "p",
ul: "ul",
li: "li",
a: "a",
h2: "h2",
pre: "pre",
code: "code",
h3: "h3"
}, props.components);
return _jsxs(_Fragment, {
children: [_jsx(Intro, {
children: _jsx(_components.p, {
children: "React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started."
})
}), "\n", _jsx(YouWillLearn, {
isChapter: true,
children: _jsxs(_components.ul, {
children: ["\n", _jsx(_components.li, {
children: _jsx(_components.a, {
href: "/learn/start-a-new-react-project",
children: "How to start a new React project"
})
}), "\n", _jsx(_components.li, {
children: _jsx(_components.a, {
href: "/learn/add-react-to-an-existing-project",
children: "How to add React to an existing project"
})
}), "\n", _jsx(_components.li, {
children: _jsx(_components.a, {
href: "/learn/editor-setup",
children: "How to set up your editor"
})
}), "\n", _jsx(_components.li, {
children: _jsx(_components.a, {
href: "/learn/react-developer-tools",
children: "How to install React Developer Tools"
})
}), "\n"]
})
}), "\n", _jsx(_components.h2, {
id: "try-react",
children: "Try React "
}), "\n", _jsx(_components.p, {
children: "You don’t need to install anything to play with React. Try editing this sandbox!"
}), "\n", _jsx(Sandpack, {
children: _jsx(_components.pre, {
children: _jsx(_components.code, {
className: "language-js",
children: "function Greeting({ name }) {\n return <h1>Hello, {name}</h1>;\n}\n\nexport default function App() {\n return <Greeting name=\"world\" />\n}\n"
})
})
}),
...
)]
});
}
function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {
children: _jsx(_createMdxContent, props)
})) : _createMdxContent(props);
}
export default MDXContent;
2.1.2 babel转换
个人认为这里进行babel转换,主要目的是为了能在node端运行markdown转成的js代码。(如使用@babel/plugin-transform-modules-commonjs将ES6 module转成Commonjs)
「前端基建」带你在Babel的世界中畅游(非常值得一看)
前端模块的默认导出、导入的兼容性问题(非常值得一看,可以关注下babel是如何编译导入导出的)
@babel/plugin-transform-react-jsx
@babel/plugin-transform-modules-commonjs
项目中配置的babel转换。
const {transform} = require('@babel/core');
const jsCode = await transform(jsxCode, {
plugins: ['@babel/plugin-transform-modules-commonjs'],
presets: ['@babel/preset-react'],
}).code;
转换后的js代码,即jsCode字符串(本文意在说明转换过程,该markdown文件有适当删减): 对于src/content/learn/index.md文件,我尝试不用babel中的@babel/preset-react,发现得到和下面的一样。
特殊语法(0, function)(param)讲解见: javascript_冷知识之_(0, function)(param)
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _jsxRuntime = require("react/jsx-runtime");
var _p = _interopRequireDefault(require("p"));
...
var _CodeStep = _interopRequireDefault(require("CodeStep"));
var _YouTubeIframe = _interopRequireDefault(require("YouTubeIframe"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*@jsxRuntime automatic @jsxImportSource react*/
/*try-react*/
/*try-react-locally*/
/*start-a-new-react-project*/
/*add-react-to-an-existing-project*/
/*next-steps*/
function _createMdxContent(props) {
const _components = Object.assign({
p: "p",
ul: "ul",
li: "li",
a: "a",
h2: "h2",
pre: "pre",
code: "code",
h3: "h3"
}, props.components);
return (0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [(0, _jsxRuntime.jsx)(_Intro.default, {
children: (0, _jsxRuntime.jsx)(_components.p, {
children: "React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started."
})
}), "\n", (0, _jsxRuntime.jsx)(_YouWillLearn.default, {
isChapter: true,
children: (0, _jsxRuntime.jsxs)(_components.ul, {
children: ["\n", (0, _jsxRuntime.jsx)(_components.li, {
children: (0, _jsxRuntime.jsx)(_components.a, {
href: "/learn/start-a-new-react-project",
children: "How to start a new React project"
})
})
...
});
}
function MDXContent(props = {}) {
const {
wrapper: MDXLayout
} = props.components || {};
return MDXLayout ? (0, _jsxRuntime.jsx)(MDXLayout, Object.assign({}, props, {
children: (0, _jsxRuntime.jsx)(_createMdxContent, props)
})) : _createMdxContent(props);
}
var _default = MDXContent;
exports.default = _default;
2.1.3 js转虚拟DOM
执行babel编译后的jsCode,如下代码,得到虚拟DOM即JSON。
// Prepare environment for MDX.
let fakeExports = {};
const fakeRequire = (name) => {
if (name === 'react/jsx-runtime') {
return require('react/jsx-runtime');
} else {
// For each fake MDX import, give back the string component name.
// It will get serialized later.
return name;
}
};
const evalJSCode = new Function('require', 'exports', jsCode);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// THIS IS A BUILD-TIME EVAL. NEVER DO THIS WITH UNTRUSTED MDX (LIKE FROM CMS)!!!
// In this case it's okay because anyone who can edit our MDX can also edit this file.
evalJSCode(fakeRequire, fakeExports);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const reactTree = fakeExports.default({});
对上面这段代码可以在阅读前端模块的默认导出&导入的兼容性问题、new Function 了解多少后进行理解。
需要着重关注注释For each fake MDX import, give back the string component name. It will get serialized later.,在执行evalJSCode时非react/jsx-runtime的组件require到的是组件名字对应的字符串,原因如下:
new Function('require', 'exports', jsCode)中的require,对应实参fakeExports,由于babel中编译后有:
var _jsxRuntime = require("react/jsx-runtime");
var _Intro = _interopRequireDefault(require("Intro"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
因此这里执行evalJSCode时有:
var _jsxRuntime = react/jsx-runtime对应代码
var _Intro = {default: '_Intro'}
最终得到reactTree如下,即虚拟DOM,即JSON对象(本文意在说明转换过程,该markdown文件有适当删减)
{
"key": null,
"ref": null,
"props": {
"children": [{
"type": "Intro",
"key": null,
"ref": null,
"props": {
"children": {
"type": "p",
"key": null,
"ref": null,
"props": {
"children": "React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started."
},
"_owner": null,
"_store": {}
}
},
"_owner": null,
"_store": {}
},
"\n",
{
"type": "YouWillLearn",
"key": null,
"ref": null,
"props": {
"isChapter": true,
"children": {
"type": "ul",
"key": null,
"ref": null,
"props": {
"children": [
"\n",
{
"type": "li",
"key": null,
"ref": null,
"props": {
"children": {
"type": "a",
"key": null,
"ref": null,
"props": {
"href": "/learn/start-a-new-react-project",
"children": "How to start a new React project"
},
"_owner": null,
"_store": {}
}
},
...
}
serialized later:
string component name对应reactTree中type属性,后续会从type属性读取出组件名,根据组件名从src/components/MDX/MDXComponents.tsx中取出对应组件,组成最终需要的Layout,详情见本文的Layout反序列化数据。
2.1.4 后续转换
(1)Pre-process MDX output
在React.dev项目中,获取到reactTree之后还会对数据进行预处理,得到最终想要的数据。
let {toc, children} = prepareMDX(reactTree.props.children);
if (path === 'index') {
toc = [];
}
// Parse Frontmatter headers from MDX.
const fm = require('gray-matter');
const meta = fm(mdx).data;
const output = {
props: {
content: JSON.stringify(children, stringifyNodeOnServer),
toc: JSON.stringify(toc, stringifyNodeOnServer),
meta,
},
};
可以从 src/utils/prepareMDX.js中看到prepareMDX函数的具体功能。
children
(1)对reactTree.props.children组件数组进行遍历,用child.type是Sandpack、FullWidth、Illustration、IllustrationBlock、Challenges、Recipes的组件对reactTree.props.children数组进行分割得到多段组件数组(数组中不包含这些特殊的child.type),每个组件数组作为MaxWidth组件的子组件,即用MaxWidth组件对这些组件组进行包裹。
(2)在这个过程中值得注意的是,在创建MaxWidth组件时,直接使用JSX语句,finalChildren.push(<Wrapper key={key}>{wrapQueue}</Wrapper>),那么这段JSX语句要怎么转换呢?答案在介绍全新的 JSX 转换,React17之后,React与Babel合作,在用Babel编译时,@babel/preset-react实际是@babel/plugin-transform-react-jsx会自动引入react/jsx-runtime并对jsx进行编译,如下例:
//原始代码:
function App() {
return <h1>Hello World</h1>;
}
// babel转换后
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
得到的children如下所示:
[{
"type": "MaxWidth",
"key": "8",
"ref": null,
"props": {
"children": [{
"type": "Intro",
"key": null,
"ref": null,
"props": {
...
},
"_owner": null,
"_store": {}
},
"\n",
...
]
},
"_owner": null,
"_store": {}
},
{
"type": "Sandpack",
"key": null,
...
},
{
"type": "MaxWidth",
"key": "last",
...
}
]
toc
在prepareMDX中,直接从reactTree.props.children组件数组中获取type是h1,h2,h3,Challenges,Recap,TeamMember相关的,得到的结果如下例:
[{
"url": "#", // 对应原数组中每项的child.props.id
"text": "Overview", // 对应原数组中每项的child.props.children
"depth": 2
},
{
"url": "#try-react",
"depth": 2,
"text": "Try React "
},
...
]
meta
React.dev中会在开头部分写一些front-matter,关于这部分相关的可见:
即原始Markdown文件中,---与---之间的内容会被提取出为JSON,赋值给meta,如下:
(1)原始Markdown文件如下:
---
title: Installation
---
...
<Intro>
(2)转换后
meta = {title: "Installation"}
2.1.5 Serialize
{
"props": {
"content": "[[\"$r\",\"MaxWidth\",\"8\",{\"children\":[[\"$r\",\"Intro\",null,{\"children\":[\"$r\",\"p\",null,{\"children\":\"React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started.\"}]}],\"\\n\",[\"$r\",\"YouWillLearn\",null,{\"isChapter\":true,\"children\":[\"$r\",\"ul\",null,{\"children\":[\"\\n\",[\"$r\",\"li\",null,{\"children\":[\"$r\",\"a\",null,{\"href\":\"/learn/start-a-new-react-project\",\"children\":\"How to start a new React project\"}]}],\"\\n\",[\"$r\",\"li\",null,{\"children\":[\"$r\",\"a\",null,{\"href\":\"/learn/add-react-to-an-existing-project\",\"children\":\"How to add React to an existing project\"}]}],\"\\n\",[\"$r\",\"li\",null,{\"children\":[\"$r\",\"a\",null,{\"href\":\"/learn/editor-setup\",\"children\":\"How to set up your editor\"}]}],\"\\n\",[\"$r\",\"li\",null,{\"children\":[\"$r\",\"a\",null,{\"href\":\"/learn/react-developer-tools\",\"children\":\"How to install React Developer Tools\"}]}],\"\\n\"]}]}],\"\\n\",[\"$r\",\"h2\",null,{\"id\":\"try-react\",\"children\":\"Try React \"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":\"You don’t need to install anything to play with React. Try editing this sandbox!\"}],\"\\n\"]}],[\"$r\",\"Sandpack\",null,{\"children\":[\"$r\",\"pre\",null,{\"children\":[\"$r\",\"code\",null,{\"className\":\"language-js\",\"children\":\"function Greeting({ name }) {\\n return <h1>Hello, {name}</h1>;\\n}\\n\\nexport default function App() {\\n return <Greeting name=\\\"world\\\" />\\n}\\n\"}]}]}],[\"$r\",\"MaxWidth\",\"last\",{\"children\":[\"\\n\",[\"$r\",\"p\",null,{\"children\":\"You can edit it directly or open it in a new tab by pressing the “Fork” button in the upper right corner.\"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":[\"Most pages in the React documentation contain sandboxes like this. Outside of the React documentation, there are many online sandboxes that support React: for example, \",[\"$r\",\"a\",null,{\"href\":\"https://codesandbox.io/s/new\",\"target\":\"_blank\",\"rel\":\"nofollow noopener noreferrer\",\"children\":\"CodeSandbox\"}],\", \",[\"$r\",\"a\",null,{\"href\":\"https://stackblitz.com/fork/react\",\"target\":\"_blank\",\"rel\":\"nofollow noopener noreferrer\",\"children\":\"StackBlitz\"}],\", or \",[\"$r\",\"a\",null,{\"href\":\"https://codepen.io/pen?&editors=0010&layout=left&prefill_data_id=3f4569d1-1b11-4bce-bd46-89090eed5ddb\",\"target\":\"_blank\",\"rel\":\"nofollow noopener noreferrer\",\"children\":\"CodePen.\"}]]}],\"\\n\",[\"$r\",\"h3\",null,{\"id\":\"try-react-locally\",\"children\":\"Try React locally \"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":[\"To try React locally on your computer, \",[\"$r\",\"a\",null,{\"href\":\"https://gist.githubusercontent.com/gaearon/0275b1e1518599bbeafcde4722e79ed1/raw/db72dcbf3384ee1708c4a07d3be79860db04bff0/example.html\",\"target\":\"_blank\",\"rel\":\"nofollow noopener noreferrer\",\"children\":\"download this HTML page.\"}],\" Open it in your editor and in your browser!\"]}],\"\\n\",[\"$r\",\"h2\",null,{\"id\":\"start-a-new-react-project\",\"children\":\"Start a new React project \"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":[\"If you want to build an app or a website fully with React, \",[\"$r\",\"a\",null,{\"href\":\"/learn/start-a-new-react-project\",\"children\":\"start a new React project.\"}]]}],\"\\n\",[\"$r\",\"h2\",null,{\"id\":\"add-react-to-an-existing-project\",\"children\":\"Add React to an existing project \"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":[\"If want to try using React in your existing app or a website, \",[\"$r\",\"a\",null,{\"href\":\"/learn/add-react-to-an-existing-project\",\"children\":\"add React to an existing project.\"}]]}],\"\\n\",[\"$r\",\"h2\",null,{\"id\":\"next-steps\",\"children\":\"Next steps \"}],\"\\n\",[\"$r\",\"p\",null,{\"children\":[\"Head to the \",[\"$r\",\"a\",null,{\"href\":\"/learn\",\"children\":\"Quick Start\"}],\" guide for a tour of the most important React concepts you will encounter every day.\"]}]]}]]",
"toc": "[{\"url\":\"#\",\"text\":\"Overview\",\"depth\":2},{\"url\":\"#try-react\",\"depth\":2,\"text\":\"Try React \"},{\"url\":\"#try-react-locally\",\"depth\":3,\"text\":\"Try React locally \"},{\"url\":\"#start-a-new-react-project\",\"depth\":2,\"text\":\"Start a new React project \"},{\"url\":\"#add-react-to-an-existing-project\",\"depth\":2,\"text\":\"Add React to an existing project \"},{\"url\":\"#next-steps\",\"depth\":2,\"text\":\"Next steps \"}]",
"meta": {
"title": "Installation"
}
}
}
2.2 Layout
(1)Layout反序列化数据
在next.js中,getStaticProps返回的output会被结构成content、toc、meta,之后会对content、toc字符串进行反序列化,得到react组件最终需要的数据,代码如下,在看这段代码前建议看下如下两个链接:
React 是如何防止 XSS 攻击的,论$$typeof 的作用(这个知识点挺有意思的,值得一看)
JSON.parse 和 JSON.stringify 详解
export default function Layout({content, toc, meta}) {
const parsedContent = useMemo(
() => JSON.parse(content, reviveNodeOnClient),
[content]
);
const parsedToc = useMemo(() => JSON.parse(toc, reviveNodeOnClient), [toc]);
...
return (
<Page toc={parsedToc} routeTree={routeTree} meta={meta} section={section}>
{parsedContent}
</Page>
);
}
// Deserialize a client React tree from JSON.
function reviveNodeOnClient(key, val) {
if (Array.isArray(val) && val[0] == '$r') {
// Assume it's a React element.
let type = val[1];
let key = val[2];
let props = val[3];
if (type === 'wrapper') {
type = Fragment;
props = {children: props.children};
}
if (MDXComponents[type]) {
type = MDXComponents[type];
}
if (!type) {
console.error('Unknown type: ' + type);
type = Fragment;
}
return {
$$typeof: Symbol.for('react.element'),
type: type,
key: key,
ref: null,
props: props,
_owner: null,
};
} else {
return val;
}
}
最终得到的parsedContent如下,就是我们想要的虚拟DOM,可以直接放在JSX中使用,如上面所示<Page ...>{parsedContent}</Page>。在该虚拟DOM中,使用type属性指向其对应的函数组件(定义在src/components/MDX/MDXComponents.tsx中),$$typeof取值Symbol用来防止XSS攻击。
// 数组中每项的原始数据,包含$$typeof和type属性,type属性对应该项的组件,在src/components/MDX/MDXComponents.tsx中。
// 一项一项的补充$$typeof和type属性实在是太费时间了,也没有必要,这里只补充了部分数据项的这两个属性,读者知道其意思即可。
[{
"key": "8",
"ref": null,
$$typeof: Symbol(react.element),
type: "MaxWidth组件",// 实际对应src/components/MDX/MDXComponents.tsx中MaxWidth组件,这里暂时用字符串名称代替,让读者了解其作用
"props": {
"children": [{
"key": null,
"ref": null,
$$typeof: Symbol(react.element),
type: "Intro组件",// 实际对应src/components/MDX/MDXComponents.tsx中Intro组件,这里暂时用字符串名称代替,让读者了解其作用
"props": {
"children": {
"key": null,
"ref": null,
$$typeof: Symbol(react.element),
type: "P组件",// 实际对应src/components/MDX/MDXComponents.tsx中P组件,这里暂时用字符串名称代替,让读者了解其作用
"props": {
"children": "React has been designed from the start for gradual adoption. You can use as little or as much React as you need. Whether you want to get a taste of React, add some interactivity to an HTML page, or start a complex React-powered app, this section will help you get started."
},
"_owner": null
}
},
"_owner": null
},
"\n",
{
"key": null,
"ref": null,
"props": {
"isChapter": true,
"children": {
"key": null,
"ref": null,
"props": {
"children": [
"\n",
{
"key": null,
"ref": null,
"props": {
"children": {
"key": null,
"ref": null,
"props": {
"href": "/learn/start-a-new-react-project",
"children": "How to start a new React project"
},
"_owner": null
}
},
"_owner": null
},
"\n",
{
"key": null,
"ref": null,
"props": {
"children": {
"key": null,
"ref": null,
"props": {
"href": "/learn/add-react-to-an-existing-project",
"children": "How to add React to an existing project"
},
2.3 缓存
React.dev使用metro-cache包将getStaticProps的返回值缓存到本地,在下次使用时可从本地缓存中读取。
// See if we have a cached output first.
const {FileStore, stableHash} = require('metro-cache');
const store = new FileStore({
root: process.cwd() + '/node_modules/.cache/react-docs-mdx/',
});
const hash = Buffer.from(
stableHash({
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~ IMPORTANT: Everything that the code below may rely on.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mdx,
mdxComponentNames,
DISK_CACHE_BREAKER,
PREPARE_MDX_CACHE_BREAKER,
lockfile: fs.readFileSync(process.cwd() + '/yarn.lock', 'utf8'),
})
);
const cached = await store.get(hash);
if (cached) {
console.log(
'Reading compiled MDX for /' + path + ' from ./node_modules/.cache/'
);
return cached;
}
...
const output = {
props: {
content: JSON.stringify(children, stringifyNodeOnServer),
toc: JSON.stringify(toc, stringifyNodeOnServer),
meta,
},
};
// Cache it on the disk.
await store.set(hash, output);
...
metro-cache包提供生成hash值、保存、读取本地文件的API,React.dev选择将文件缓存到/node_modules/.cache/react-docs-mdx/目录下。重要的是生成的hash值(metro-cache包使用md5算法),它是缓存内容标识,hash值是一个Buffer(是Unit8Array的子类),如本文示例src/content/learn/index.md对应的hash值是{ "type": "Buffer", "data": [ 36, 144, 220, 34, 182, 120, 94, 83, 102, 36, 9, 143, 86, 203, 132, 194 ] },metro-cache使用hash[0]作为文件夹名(转成16进制后取字符串,这里36转换成'24'),使用hash[1:]作为最终文件名(转成16进制后取字符串),缓存文件最终被保存在node_modules/.cache/react-docs-mdx/24/90dc22b6785e536624098f56cb84c2。
读者需要注意hash值由哪几部分生成部分,文件内容+文件名+yarn.lock文件内容+DISK_CACHE_BREAKER常量+PREPARE_MDX_CACHE_BREAKER常量,这两个常量的定义如下:
src/pages/[[...markdownPath]].js中
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~
const DISK_CACHE_BREAKER = 7;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/utils/prepareMDX.js中
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export const PREPARE_MDX_CACHE_BREAKER = 2;
// !!! IMPORTANT !!! Bump this if you change any logic.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 开启debug模式
V8 Inspector Protocol + Chrome DevTools
参考自:Next.js高级特性:调试
或者点击这里
4. 预加载
Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.
Next.js 默认有做 route pre-fetching ,当 Link 出现在视图中时,会去提前请求相关的资源,这样跳转路由界面切换会很快。
体现在代码中,就是在生成的HTML中有很多的rel="preconnect"、rel="preload",如下图。关于预加载,可参考:一些预加载/预处理资源的方式、通过切割代码和预加载来提高页面加载速度 #48
其他
React 官网为什么这么快 #47(挺有意思,有时间详细看)