react发展介绍

yingwinwin

yingwinwin

前端开发

react18#

note
  • react18 Alpha 已经完成
  • react18 beta 已经完成
  • 进入到RC版本,需要大概1-2个月,具体还需要看反馈 (12月9号左右)
  • RC结束之后,需要大概2~4周就会发布react18了。

自动批处理#

如何理解批处理呢?从一道老生常谈的面试题谈起:setState什么时候是同步的,什么时候是异步的?

  • 来看一下,下面这段代码 (打开控制台点击按钮查看效果)
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((count) => count + 1)
setCount((count) => count + 1)
}
console.log(111);
return <><button onClick={handleClick}>点击</button>{count}</>
}
ReactDOM.render(<App/>, document.getElementById('root'))

效果: image 熟悉react的朋友,应该都知道,react会合并处理在事件函数和生命周期的setState的渲染,所以上面在console中的123,会打印1次。

  • 再来看下面这段代码
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount((count) => count + 1)
setCount((count) => count + 1)
}, 0);
}
console.log(111);
return <><button onClick={handleClick}>点击</button>{count}</>
}
ReactDOM.render(<App/>, document.getElementById('root'))

效果: image 结果可能大家也都已经知道了,console里面的值被打印了2次,也就是说每次点击react组件渲染了2次,在setTimeout中setState的批处理失效了。

那道有名的面试题的答案也就出来了。

  1. react在事件处理和生命周期函数中是异步处理渲染(合并渲染,也就是批处理)
  2. 在异步任务中,如:promise.then,setTimeout等异步任务中,会同步进行渲染(不进行批处理)
  • 这样看其实setState在18版本之前只能算是半自动批处理。

批处理的概念说完了我们来看一下react18做了什么?

  • react18中改掉了这个setState在异步中处理的问题,真正了实现了自动批处理。(狂喜,以后再也不用被setState的面试题支配😎)

如何使用react18的自动批处理呢?需要使用到react提供的新的root Api

tip
  • react提供了一个新的root Api,ReactDOM.createRoot用于并发渲染,可以选择优先级更高的任务进行渲染。
  • 之前我们使用的ReactDOM.render()被称为legacy模式,等待按照顺序渲染。

上面的例子不变,我们来看一下,在react18中的表现。

  1. 安装包更改为react18@beta版本。
# 安装方式
npm install react@rc react-dom@rc
  1. 需要修改react的挂载模式为createRoot Api形式
- ReactDOM.render(<App/>, document.getElementById('root'))
+ ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
  1. 代码
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => { // 异步
setCount((count) => count + 1)
setCount((count) => count + 1)
}, 0);
}
console.log(111);
return <><button onClick={handleClick}>点击</button>{count}</>
}
// 这里换成最新的createRoot API
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);

效果: image 可以看到react18即便是在异步操作中,也不会出现重新渲染两次的情况。达到了真正的自动化批处理。

那么问题来了,我们并不是总是需要批处理呀,那怎么实现之前需要异步处理的情况呢?react官方提供了一个api,帮助我们处理这种不需要批处理的情况。

  • ReactDOM.flushSync
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
setCount((count) => count + 1)
})
setCount((count) => count + 1)
}, 0);
}
console.log(111);
return <><button onClick={handleClick}>点击</button>{count}</>
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);

效果: image 这样效果的表现和之前放到异步的表现是一致的。

  • 原理就是把setState放到两个不同的优先级中处理,如果setState的话,react会自动合并两个在一个环境中的setState,判定为优先级相同,那么就会一起批量更新。

startTransition#

这个是为了解决,大数据下,渲染顺序的问题。很多时候,同步更新如果js更新内容过大,就会造成用户感受卡顿的感觉。react为了解决这个问题,和fiber开始就已经开始铺路了。这次react18,是全面进行并发的异步模式,让translate变成可能。

以下例子均来源于「React18新特性」深入浅出用户体验大师—transition

import React, {memo} from 'react';
import ReactDOM from 'react-dom';
/* 模拟数据 */
const mockDataArray = new Array(10000).fill(1)
/* 高量显示内容 */
function ShowText({ query }){
const text = 'asdfghjk'
let children
if(text.indexOf(query) > 0 ){
/* 找到匹配的关键词 */
const arr = text.split(query)
children = <div>{arr[0]}<span style={{ color:'pink' }} >{query}</span>{arr[1]} </div>
}else{
children = <div>{text}</div>
}
return <div>{children}</div>
}
/* 列表数据 */
function List ({ query }){
console.log('List渲染')
return <div>
{
mockDataArray.map((item,index)=><div key={index} >
<ShowText query={query} />
</div>)
}
</div>
}
/* memo 做优化处理 */
const NewList = memo(List)
export default function App(){
const [ value ,setInputValue ] = React.useState('')
const [ isTransition , setTransion ] = React.useState(false)
const [ query ,setSearchQuery ] = React.useState('')
const handleChange = (e) => {
/* 高优先级任务 —— 改变搜索条件 */
setInputValue(e.target.value)
if(isTransition){ /* transition 模式 */
React.startTransition(()=>{
/* 低优先级任务 —— 改变搜索过滤后列表状态 */
setSearchQuery(e.target.value)
})
}else{ /* 不加优化,传统模式 */
setSearchQuery(e.target.value)
}
}
return <div>
<button onClick={()=>setTransion(!isTransition)} >{isTransition ? 'transition' : 'normal'} </button>
<input onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<NewList query={query} />
</div>
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);

image

通过这个例子可以看出来translate可以最大程度的保护用户体验,在输入内容时,延后list的渲染,不会让用户觉得输入的时候卡顿。

为什么不是setTimeout#

image

  1. setTimeout 是异步的执行任务,不会阻碍浏览器的渲染。但是执行的时候,同样会抢浏览器的线程使用,会让用户产生卡顿的感觉。而translate是react使用并发中断当前list渲染,确保input输入,所以不会让用户觉得卡顿。
  2. setTimeout是异步的,而translate的执行时间会比setTimeout早,是在回调中执行的。可以更好的确保ui的渲染。

为什么不是节流防抖#

image

  1. 节流防抖一定程度上可以解决这个问题。但是在节流防抖的时间把控上,需要人为判断,如果时间长可能会给用户造成延迟展示的感觉。早了的话,和setTimeout一样还是会有卡顿的感觉。translate不用考虑这么多。
  2. 节流防抖本质上还是setTimeout,原理是让list渲染的次数减少。translate是让其延迟展示,而不是减少渲染次数。

useTransition#

react 为了更好的使用translate 提供了一个hook函数

  • 不用hook使用
React.startTransition(()=>{
/* 更新任务 */
setSearchQuery(value)
})
  • hook使用
import { useTransition } from 'react'
/* 使用 */
const [ isPending , startTransition ] = useTransition ()
// 可以精准的判断当前延迟时间,并进行相应的状态展示
{ isPending && <div>loading....</div> }

image 图中可以看到,translate的时间可以精准的被判断出来。

useDeferredValue#

export default function App(){
const [ value ,setInputValue ] = React.useState('')
const query = React.useDeferredValue(value) // 把用translate延迟改为用useDeferredValue
const handleChange = (e) => {
setInputValue(e.target.value)
}
return <div>
<button>useDeferredValue</button>
<input onChange={handleChange}
placeholder="输入搜索内容"
value={value}
/>
<NewList query={query} />
</div>
}

image

图上的效果与useTransition基本一致。实际上,现在react18工作组,对这两个api也没有一个明确的例子来说明两者明显的区别。在新文档出来之前可能还有一定的修改。具体可以看一下帖子中Dan的回复

  1. 目前两者区别不大。可能在api使用上有一些区别。useTransition可以用于获取等待状态。
  2. 在执行时机上useDeferredValue相比较useTransition晚一些。

useId#

如果你需要生成一个唯一Id,可以使用useId

const id = React.useId()

useSyncExternalStore#

在引入外部数据的时候,react引入了一个新的api。一般都是由库作者修改。普通开发者用不到,深入了解可以看下面的文章。

Suspense#

服务端中引入开箱即用的Suspense

  • 请求与suspense一起使用
import useSWR from "swr"; // 支持suspense的取数库
function getUsername() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("name");
}, 3000);
});
}
const Name = () => {
const { data } = useSWR('/getUser', getUsername, {
suspense: true
});
console.log(data);
return <div>{data}</div>;
};
const App = () => {
return (
<Suspense fallback={<div>loading...</div>}>
<Name />
</Suspense>
);
};

参考文档#

react-router v6#

useNavigate#

react-router中会直接依赖history,不需要在pakeage.json中额外安装history依赖,所有导航方法,均从useNavigate()hook中取得。

v5
import { useHistory } from "react-router-dom";
function App() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
v6
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}

useRoutes#

使用useRouteshook,代替react-router-config,使用js对象定义router,更加简洁。

v6
function App() {
let element = useRoutes([
// 每一项想当于一个Route
{ path: "/", element: <Home /> },
{ path: "dashboard", element: <Dashboard /> },
{
path: "invoices",
element: <Invoices />,
// 如果使用嵌套路由需要用children属性。相当于嵌套路由里面的route
children: [
{ path: ":id", element: <Invoice /> },
{ path: "sent", element: <SentInvoices /> }
]
},
// 404的情况
{ path: "*", element: <NotFound /> }
]);
// 可以直接将element对象返回,在根组件挂载
return element;
}
ReactDOM.createRoot(document.getElementById("root")).render(<BrowserRouter>
<App/>
</BrowserRouter>);

<Routes/>#

routes可以完美替换switch,但是功能要强过switch

  1. <Routes/>中的路由都是相对的。在<Link to/> <Route path>的代码更简洁
v5
import {
BrowserRouter,
Switch,
Route,
Link,
useRouteMatch
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/users">
<Users />
</Route>
</Switch>
</BrowserRouter>
);
}
function Users() {
// 在v5中如果要使用 Switch中的route寻找到对应的router就要用match.path 和 match.url
let match = useRouteMatch();
return (
<div>
<nav>
<Link to={`${match.url}/me`}>My Profile</Link>
</nav>
<Switch>
<Route path={`${match.path}/me`}>
<OwnUserProfile />
</Route>
<Route path={`${match.path}/:id`}>
<UserProfile />
</Route>
</Switch>
</div>
);
}
v6
import {
BrowserRouter,
Routes,
Route,
Link
} from "react-router-dom";
function App() {
// 在v6中使用 * 来代表此路由是有子路由的
return (
<BrowserRouter>
<Routes>
{/* 取消了 extra,用有 * 来匹配是否有子代路由 */}
<Route path="/" element={<Home />} />
<Route path="users/*" element={<Users />} />
</Routes>
</BrowserRouter>
);
}
function Users() {
return (
<div>
<nav>
<Link to="me">My Profile</Link>
</nav>
{/* 在子组件中,路由直接写相对路径就可以 */}
<Routes>
<Route path=":id" element={<UserProfile />} />
<Route path="me" element={<OwnUserProfile />} />
</Routes>
</div>
);
}
  1. 匹配规则从上到下,改成了最佳匹配路线

下面这种情况,react-router 会选择渲染 NewTteam 组件,因为他的路由更精准,不用怕因为位置写错,而导致的问题了。

<Routes>
<Route path="teams/:teamId" element={<Team />} />
<Route path="teams/new" element={<NewTeam />} />
</Routes>
  1. 可以将路由都在一个地方嵌套,而不是分散到各处
嵌套路由
import {
BrowserRouter,
Routes,
Route,
Link,
Outlet
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="users" element={<Users />}>
{/* 在这里直接使用嵌套路由,对小应用友好,注意这里不用加*,因为是直接嵌套在这里的 */}
<Route path="me" element={<OwnUserProfile />} />
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Users() {
return (
<div>
<nav>
<Link to="me">My Profile</Link>
</nav>
{/* 可以使用 Outlet 组件进行占位 */}
<Outlet />
</div>
);
}

<Link to/>#

Link to 的表现和我们在使用cd xxx文件时的表现相似

function App() {
return (
<Routes>
<Route path="users" element={<Users />}>
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
);
}
function Users() {
return (
<div>
<h2>
{/* 这个link会跳到 /users - 当前路由 */}
<Link to=".">Users</Link>
</h2>
<ul>
{users.map(user => (
<li>
{/* 这个link会跳到 /users/:id - 孩子路由 */}
<Link to={user.id}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
}
function UserProfile() {
return (
<div>
<h2>
{/* 这个link会跳到 /users - 父路由 */}
<Link to="..">All Users</Link>
</h2>
<h2>
{/* 这个link会跳到 /users/:id - 当前路由 */}
<Link to=".">User Profile</Link>
</h2>
<h2>
{/* 这个link会跳到 /users/mj - 一个兄弟路由 */}
<Link to="../mj">MJ</Link>
</h2>
</div>
);
}

参考文档#

ahooks 3#

其他#

react项目搭建

yingwinwin

yingwinwin

前端开发

webpack搭建react项目#

image

1. 安装依赖包#

mkdir react_app
cd react_app
npm init --yes
npm i -D @babel/preset-env @babel/preset-react autoprefixer babel-loader css-loader html-webpack-plugin less less-loader px2rem-loader style-loader webpack webpack-cli webpack-dev-server postcss-loader
npm i --save react react-dom

2. webpack.config.js#

const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.jsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
devtool: 'source-map',//生成单独的完整的source-map文件,方便开发
devServer: {
port: 8080,//开发服务器的端口号
hot: true,//启动热更新
},
module: {
rules: [
{
test: /\.(t|j)sx?$/,
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env", //解析ES+
"@babel/preset-react", //解析React JSX语法的
],
},
include: path.resolve("src"),
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader"
},
],
},
{
test: /\.less$/,
use: [
"style-loader" ,
{
loader: "css-loader", //处理import和url
options: { importLoaders: 3 }, //import的文件要导入之前需要经过几个loader的处理
},
{
loader: "postcss-loader", //加入厂商的兼容性前缀
options: {
postcssOptions: {
plugins: ["autoprefixer"],
},
},
},
{
loader: "px2rem-loader", //可以把px单位变成rem单位
options: {
remUnit: 37.5, //在标准设计稿下1rem对应的是75px
remPrecesion: 8, //计算后的小数精度是8位
},
},
"less-loader", //把less编译成css
],
},
{
test: /\.(jpg|png|gif|svg|jpeg)/,
type: "asset", //以前url-loader或者file-loader,现在不需要
},
],
},
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
};

3. pageage.json#

"scripts": {
"dev": "webpack server",
"build": "webpack"
},

Vite 建立react项目#

vite官网

# npm 6.x
npm init @vitejs/app my-react-app --template react
# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-react-app -- --template react
  • 理论上这样就可以了,但是根据大佬的教程npm run dev直接报错 image

  • 解决(要自己手动执行一下)

node node_modules/esbuild/install.js
npm run dev
  • 这回我以为可以了,然后浏览器里面又报错 image
  • 解决(关掉安装的浏览器 React Developer Tools 插件,可能是因为我的版本太低了,2.5.2的 4版本的插件是没有问题的)

TODO#

  • 问题只是解决了,但是并不知道是为什么

history API 总结

yingwinwin

yingwinwin

前端开发

History API#

具体参考history MDN官网的文档

  • 属性
    • History.length只读
    • History.scrollRestoration
    • History.state只读
  • 方法
    • History.go(number)
    • History.back()
    • History.forward()
    • History.pushState(state, title, url)
    • History.replaceState(state, title, url)
  • 事件
    • onpopstate

history stack#

  • 浏览器是一个栈结构的存贮器
// 当你依次浏览a, b, c 三个网页的时候,浏览器的操作是
let histroy1 = []; // 放当前浏览器的页面
let histroy2 = []; // 暂存的页面
history1.push(a);
history1.push(b);
history1.push(c);
// 此时浏览器的数据中是 histroy1 = [a, b, c] histroy2 = []
// 这时你点击浏览器的后退按钮
history1.pop(c)
histroy2.push(c)
// 现在浏览器中的数据是 histroy1 = [a, b] histroy2 = [c]
// 现在浏览器的b页面,当你从b页面跳转到新的d页面的时候。
history1.push(d);
history2.pop(c);
// 现在浏览器中的数据是 histroy1 = [a, b, d] histroy2 = []

注意事项#

  • go 参数为数字 history.go(1) 相当于 history.forward(), history.go(-1) 相当于 history.back()
  • state属性可以在除了popstate事件中拿到state的值得另一种方式,注意它是只读
  • pushStatereplaceState 的关系
    • 相同
      • 两者的使用方式基本一直,都是在history.state中添加内容
      • 第一个参数是state,第二个参数是title,第三个参数url,第二个参数没有浏览器实现,所以一般写null或者''就可以。
    • 区别
      • pushState会在浏览器加入一条记录,改变history.length属性。replaceState是重写浏览器当前的记录,不会增加history.length的长度
  • popState事件,在浏览器返回和前进时会触发,也就是说栈结构有pop操作的时候才会触发的事件

具体应用#

1. 清除往返缓存#

  • 往返缓存(Back/Forward cache,下文中简称bfcache)是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略。该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到bfcache中;当用户点击后退按钮的时候,将页面直接从bfcache中加载,节省了网络请求的时间。
window.addEventListener("pageshow", e => e.persisted && location.reload());
// e.persisted 是否从缓存读取,如果true 是从缓存取,如果false相反

2. window.performance.navigation.type 如果没有e.persisted可以通过该属性判断#

  • performance.navigation.type该属性返回一个整数值,表示网页的加载来源,可能有以下4种情况
valuedescribe
0网页通过点击链接、地址栏输入、表单提交、脚本操作等方式加载,相当于常数performance.navigation.TYPE_NAVIGATE
1网页通过“重新加载”按钮或者location.reload()方法加载,相当于常数performance.navigation.TYPE_RELOAD。
2网页通过“前进”或“后退”按钮加载,相当于常数performance.navigation.TYPE_BACK_FORWARD。
255任何其他来源的加载,相当于常数performance.navigation.TYPE_RESERVED
  • performance.navigation.redirectCount表示网页经过重定向的次数。

3. 禁用浏览器后退按钮#

history.pushState(null, null, document.URL);
window.addEventListener('popstate', () => {
history.pushState(null, null, document.URL);
});

参考文档#

echarts桑基图的使用和总结

yingwinwin

yingwinwin

前端开发

因为公司需要使用用流程图,来表示不同客户来源的走向和数据,用到了桑基图来表示。最近echarts刚出了5版本。对比了antV 和 echarts ,处理这种情况echarts要做的好些,但是在应用过程中还是遇到了不少坑,记录一下。

遇到的问题#

1. key不能重复的问题#

  • 每一个node节点都不能重复,但是不知道echarts内部是如何识别中文是否一致的,感觉中文是有bug的。一致在报key相同,其实已经处理过了,是不同的了,最后还是加了英文的唯一key来区别不同的来源。

2. nodes 和 data 节点是不一样#

  • 官网中说是可以一起使用的,其实并不是,echarts源码中,并没有去取nodes的值,而是去取的data上面的配置,所以,echarts的节点,只能用data属性image

3. nodes节点一定不能重复#

  • 这里如果后端返回的节点需要处理,建议数据格式用set,而不是数组

css3打造loading效果

yingwinwin

yingwinwin

前端开发

最近在写公司一个react + hook + typeScript的前端pc项目,组件都是自己一步一步搭起来的,记录一下。loading组件相对简单,下次把form组件,还有自己写的picker组件整理一下。逻辑不多,就直接粘贴代码了。

  • 主要说一下对DOM节点的添加和移除,需要做一下判断,当前是否已经有组件存在了,如果当前用户已经调用了一个loading组件,那么就不可以在调用了。当用户添加移除loading的方法,重置当前的loading数量。

  • 样式上,通过transform对li元素的3d角度和位移角度,来让li元素均匀的分部。

  • 项目结构

import React from 'react';
import ReactDOM from 'react-dom';
import './index.less'
interface IProps {
title?: string; // 传入当前loading的文字,非必传
}
/* 渲染load的组件 */
const Load: React.FC<IProps> = ({
title
}) => {
/* 渲染li元素的数量 */
const renderList = (num: number) => {
let arr = [];
while (num--) {
arr.push(<li key={num} className={`load_item item${num + 1}`}></li>)
}
return arr;
}
return <div className="loading_mask">
<ul className="loading_box">
{renderList(6)}
</ul>
<span className="loading_title">{title}</span>
</div>
}
Load.defaultProps = {
title: '加载中...'
}
interface ILoad {
loadDom: null | HTMLElement; // 创建一个div容器
count: number; // 判断当前有几个load
containter: null | HTMLElement; // 渲染当前reactdom的节点
show: (title?: string) => void; // 展示load的方法
hide: () => void; // 隐藏loading的方法
}
const Loading: ILoad = {
loadDom: null,
count: 0,
containter: null,
show: function (title) {
this.count++; // 记录当前的盒子数量
if (this.loadDom) return; // 只能出现一个
this.loadDom = document.createElement('div') // 创建一个div
this.containter = document.body.appendChild(this.loadDom); // 把div元素放到body上,然后把这个load 挂载到这个已经放到load的容器里面
ReactDOM.render(<Load title={title} />, this.containter);
},
hide: function () {
/* 调用方法后,把count减掉,然后如果count的数量小于等于0,说明loading已经没有了,就把当前的盒子移除掉,count也清零 */
if (--this.count <= 0) {
try {
document.body.removeChild(this.containter as HTMLElement);
} catch {
//todo
} finally {
this.count = 0;
this.loadDom = null;
}
}
}
}
export default Loading;
  • 样式文件
.loop(@counter) when(@counter > 0) {
&.item@{counter} {
transform: rotateZ(60deg * @counter) translateY(20px);
}
.loop(@counter - 1)
}
*{
list-style: none;
padding: 0;
margin: 0;
}
/* loading的遮罩层 */
.loading_mask {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgb(#000 0.5);
width: 100%;
height: 100%;
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
color: #ccc;
font-size: 14px;
flex-direction: column;
/* 当前的ul样式 */
.loading_box {
width: 80px;
height: 80px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
animation: load 2s linear infinite;
/* 每个li的样式 */
.load_item {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ccc;
.loop(6); // 循环设置每一个里元素的位置
}
}
}
@keyframes load {
form {
transform: rotate(0deg);
} to {
transform: rotate(360deg);
}
}
  • 效果 image

docusaurus部署

yingwinwin

yingwinwin

前端开发

因为公司一直用react,看了一下vuepress不太会,然后查了一下react用这个docusaurus很友好,就选择了这个,部署到Github pages遇到了一些问题,记录分享一下。

搭建本地的项目#

  • docusaurus官网脚手架

  • 脚手架的设计真的很良心,像react脚手架一样安装启动,my-website就是项目的名字,classicdocusaurus的默认主题,官网中也有其他的可以下载插件配置主题

npx @docusaurus/init@latest init my-website classic
  • 安装完成之后根据提示
cd my-website
npm run start
  • 这个时候已经可以在localhost:3000,看到项目的主页了

修改配置#

  • 配置文件是docusaurus.config.js,在里面修改一些主页的展示,这里的部分自己改一改都知道是什么意思了。我在这个文件中写了注释可以参考一下。

  • 侧边栏的配置文件sidebars.js,可以参考,也可以看官方文档。

  • 修改好自己的配置之后,就可以尝试部署了。

建立github pages项目 + 自动部署#

1. 准备工作#

  • 创建一个项目,然后在setting里面设置GitHub Pages, 把构建的分支改为gh-pages,这个分支是要自己建立的,目前里面什么都不用有,把当前的项目代码都推到master分支。 image
  • 注意一定要配置好docusaurus.config.js文件中的这四个属性比较重要,如果有什么问题,可能就是这里没有配置好,其他按需就可以
url: 'https://yingwinwin.github.io', // 当前页面的url,setting里面 都可以看到部署后的url
baseUrl: '/', // 这里看自己需要添加,如果添加为/win/ 访问主页就是 https://yingwinwin.github.io/win/
organizationName: 'yingwinwin', // 这里是你github的名字
projectName: 'yingwinwin.github.io', // 这个是你要部署到的github的项目名字

2. 设置自动部署#

  • 设置pushmaster分支之后自动部署,这个时候就要用到刚才建立好的gh-pages分支了,在根目录建立.github/workflows/documentation.yml文件,参考阮一峰大佬的博客
  • 要注意一下ACCESS_TOKEN的配置,这个要提前在github中配置一下。参考github官网
  • 要注意一下CNAME文件,如果你有自己的域名用的不是username.github.io这个域名的话,需要建立CNAME文件,并在里面写上自己的域名。
name: Deploy Github pages
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
with:
persist-credentials: false
- name: Install and Build
run: |
npm install
npm run-script build
- name: Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages
FOLDER: build
BUILD_SCRIPT: npm install && npm run build

3. 推送项目#

  • 把当前的项目文件都推送到master分支上,这个时候如果没有什么问题,githubaction就开始工作自动部署了。在图中这个地方可以查看部署详情。部署完成后就可以打开你的主页看到部署后的内容了。大概就和官网差不多。 image image

  • 如果有其他问题,可以留言一起解决。