基于React的衍生框架
安装
npm install react-app-rewired --save-dev
在根目录下创建一个config-overrides.js文件
/* config-overrides.js */
module.exports = function override(config, env) {
//do stuff with the webpack config...
return config;
}
修改package.json脚本
/* package.json */
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
"eject": "react-scripts eject"
}
配置文件
module.exports = {
// The Webpack config to use when compiling your react app for development or production.
webpack: function(config, env) {
// ...add your webpack config
return config;
},
// The Jest config to use when running your jest tests - note that the normal rewires do not
// work here.
jest: function(config) {
// ...add your jest config customisation...
// Example: enable/disable some tests based on environment variables in the .env file.
if (!config.testPathIgnorePatterns) {
config.testPathIgnorePatterns = [];
}
if (!process.env.RUN_COMPONENT_TESTS) {
config.testPathIgnorePatterns.push('<rootDir>/src/components/**/*.test.js');
}
if (!process.env.RUN_REDUCER_TESTS) {
config.testPathIgnorePatterns.push('<rootDir>/src/reducers/**/*.test.js');
}
return config;
},
// The function to use to create a webpack dev server configuration when running the development
// server with 'npm run start' or 'yarn start'.
// Example: set the dev server to use a specific certificate in https.
devServer: function(configFunction) {
// Return the replacement function for create-react-app to use to generate the Webpack
// Development Server config. "configFunction" is the function that would normally have
// been used to generate the Webpack Development server config - you can use it to create
// a starting configuration to then modify instead of having to create a config from scratch.
return function(proxy, allowedHost) {
// Create the default config by calling configFunction with the proxy/allowedHost parameters
const config = configFunction(proxy, allowedHost);
// Change the https certificate options to match your certificate, using the .env file to
// set the file paths & passphrase.
const fs = require('fs');
config.https = {
key: fs.readFileSync(process.env.REACT_HTTPS_KEY, 'utf8'),
cert: fs.readFileSync(process.env.REACT_HTTPS_CERT, 'utf8'),
ca: fs.readFileSync(process.env.REACT_HTTPS_CA, 'utf8'),
passphrase: process.env.REACT_HTTPS_PASS
};
// Return your customised Webpack Development Server config.
return config;
};
},
// The paths config to use when compiling your react app for development or production.
paths: function(paths, env) {
// ...add your paths config
return paths;
},
}
添加多页面入口
npm install react-app-rewire-multiple-entry --save-dev
在config-overrides.js配置
const { override, overrideDevServer } = require('customize-cra');
const multipleEntry = require('react-app-rewire-multiple-entry')([{
entry: 'src/pages/options.tsx',
template: 'public/options.html',
outPath: '/options.html',
}]);
const addEntry = () => config => {
multipleEntry.addMultiEntry(config);
return config;
};
const addEntryProxy = () => (configFunction) => {
multipleEntry.addEntryProxy(configFunction);
return configFunction;
}
module.exports = {
webpack: override(
addEntry(),
),
devServer: overrideDevServer(
addEntryProxy(),
)
}
Create React App Configuration Override修改cra配置的一个包
安装
npm i @craco/craco
在根目录下创建一个craco.config.js并且修改package.json
"scripts": {
- "start": "react-scripts start",
+ "start": "craco start",
- "build": "react-scripts build",
+ "build": "craco build"
- "test": "react-scripts test",
+ "test": "craco test"
}
配置文件可以是craco.config.ts
, craco.config.js
, .cracorc.ts
, .cracorc.js
或者 .cracorc
const { when, whenDev } = require("@craco/craco");
module.exports = {
eslint: {
mode: ESLINT_MODES.file,
configure: {
formatter: when(process.env.NODE_ENV === "CI", require("eslint-formatter-vso"))
}
},
webpack: {
plugins: [
new ConfigWebpackPlugin(),
...whenDev(() => [new CircularDependencyPlugin()], [])
]
}
};
配置jest
/* jest.config.js */
const { createJestConfig } = require("@craco/craco");
const cracoConfig = require("./craco.config.js");
const jestConfig = createJestConfig(cracoConfig);
module.exports = jestConfig;
Remix由 React Router 原班团队打造,基于 TypeScript 与 React,内建 React Router V6 特性的全栈 Web 框架 Remix 正式开源。
Remix 开源之后可以说是在 React 全栈框架领域激起千层浪,绝对可以算是 Next.js 的强劲对手。Remix 的特性如下:
Remix 提供基于文件的路由,将读取数据、操作数据和渲染数据的逻辑都写在同一个路由文件里,方便一致性处理,这样可以跨客户端和服务端逻辑共享同一套类型定义。
Https://juejin.cn/post/6844904017487724557
Next.js
是一个基于React
的一个服务端渲染简约框架。它使用React
语法,可以很好的实现代码的模块化,有利于代码的开发和维护
Next的优点:
webpack
的热替换为基础的开发环境React
的JSX
和ES6
的module
,模块化和维护更方便Express
和其他Node.js
的HTTP
服务器上babel
和webpack
配置创建next项目
npm install --save react react-dom next
Next.js
是从服务器生成页面,再返回给前端展示。Next.js
默认从 pages
目录下取页面进行渲染返回给前端展示,并默认取 pages/index.js
作为系统的首页进行展示。注意,pages
是默认存放页面的目录,路由的根路径也是pages
目录
在pages目录下创建indexjs
// next-Link用于引入文件
import Link from 'next/link'
const Index = () => (
<div>
<Link href="/about">
<a>About Page</a>
</Link>
<p>Hello Next.js</p>
</div>
)
export default Index
在 Next.js 中,一个 page(页面) 就是一个从 .js
、jsx
、.ts
或 .tsx
文件导出(export)
这些文件存放在 pages
目录下。每个 page(页面)都使用其文件名作为路由(route)
Next.js 支持具有动态路由的 pages(页面)。例如,如果你创建了一个命名为 pages/posts/[id].js
的文件,那么就可以通过 posts/1
、posts/2
等类似的路径进行访问
预渲染
默认情况下,Next.js 将 预渲染 每个 page(页面)。这意味着 Next.js 会预先为每个页面生成 HTML 文件,而不是由客户端 JavaScript 来完成。预渲染可以带来更好的性能和 SEO 效果
每个生成的 HTML 文件都与该页面所需的最少 JavaScript 代码相关联。当浏览器加载一个 page(页面)时,其 JavaScript 代码将运行并使页面完全具有交互性。(此过程称为 水合(hydration)。)
Next.js 具有两种形式的预渲染: 静态生成(Static Generation) 和 服务器端渲染(Server-side Rendering)。这两种方式的不同之处在于为 page(页面)生成 HTML 页面的 时机 。
重要的是,Next.js 允许你为每个页面 选择 预渲染的方式。你可以创建一个 “混合渲染” 的 Next.js 应用程序:对大多数页面使用“静态生成”,同时对其它页面使用“服务器端渲染”。
出于性能考虑,相对服务器端渲染,我们更 推荐 使用 静态生成 。 CDN 可以在没有额外配置的情况下缓存静态生成的页面以提高性能。但是,在某些情况下,服务器端渲染可能是唯一的选择。
你还可以将 客户端渲染 与静态生成或服务器端渲染一起使用。这意味着页面的某些部分可以完全由客户端 JavaScript 呈现。
如果一个页面使用了 静态生成,在 构建时(build time) 将生成此页面对应的 HTML 文件 。这意味着在生产环境中,运行 next build
时将生成该页面对应的 HTML 文件。然后,此 HTML 文件将在每个页面请求时被重用,还可以被 CDN 缓存。
在 Next.js 中,你可以静态生成 带有或不带有数据 的页面。
不需要获取数据的静态页面
默认情况下,Next.js 使用 “静态生成” 来预渲染页面但不涉及获取数据。
此页面在预渲染时不需要获取任何外部数据。在这种情况下,Next.js 只需在构建时为每个页面生成一个 HTML 文件即可。
需要获取数据的静态生成
某些页面需要获取外部数据以进行预渲染。有两种情况,一种或两种都可能适用。在每种情况下,你都可以使用 Next.js 所提供的以下函数:
getStaticProps
。getStaticPaths
(通常还要同时使用 getStaticProps
)。在根目录下创建一个middleware.ts文件
import { NextResponse } from 'next/server'
export function middleware() {
// Store the response so we can modify its headers
const response = NextResponse.next()
// Set custom header
response.headers.set('x-modified-edge', 'true')
// Return response
return response
}
在pages目录下创建一个_middleware.ts,那么所有的路由都会执行这个中间件
// pages/_middleware.ts
import type { NextFetchEvent, NextRequest } from 'next/server'
export function middleware(req: NextRequest, ev: NextFetchEvent) {
return new Response('Hello, world!')
}
如果在pages下面的子页面创建了_middleware的中间件,那么中间件会按照目录的层级之行
- package.json
- /pages
index.tsx
- /about
_middleware.ts # Will run first
about.tsx
- /teams
_middleware.ts # Will run second
teams.tsx
Next.js
上提供了一个独特的特性:路由遮盖(Route Masking)。它可以使得在浏览器上显示的是路由A
,而App
内部真正的路由是B
。这个特性可以让我们来设置一些比较简洁的路由显示在页面,而系统背后是使用一个带参数的路由。比如上面的例子中,地址栏中显示的是 http://localhost:3000/post?title=Hello%20Next.js
,这个地址含有一个title
参数,看着很不整洁。下面我们就用Next.js
来改造路由,使用路由遮盖来创建一个更加简洁的路由地址。比如我们将该地址改造成 `http://localhost:3000/p/hello-nextjs
在page下面创建_app.js,能覆盖默认的appjs
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
return (
<>
<Navbar />
<main>{children}</main>
<Footer />
</>
)
}
// pages/_app.js
import Layout from '../components/layout'
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
在next项目中添加seo
安装
npm install next-seo
添加seo
import { NextSeo } from 'next-seo';
const Page = () => (
<>
<NextSeo
title="Using More of Config"
description="This example uses more of the available config options."
canonical="https://www.canonical.ie/"
openGraph={{
url: 'https://www.url.ie/a',
title: 'Open Graph Title',
description: 'Open Graph Description',
images: [
{
url: 'https://www.example.ie/og-image-01.jpg',
width: 800,
height: 600,
alt: 'Og Image Alt',
type: 'image/jpeg',
},
{
url: 'https://www.example.ie/og-image-02.jpg',
width: 900,
height: 800,
alt: 'Og Image Alt Second',
type: 'image/jpeg',
},
{ url: 'https://www.example.ie/og-image-03.jpg' },
{ url: 'https://www.example.ie/og-image-04.jpg' },
],
site_name: 'SiteName',
}}
twitter={{
handle: '@handle',
site: '@site',
cardType: 'summary_large_image',
}}
/>
<p>SEO Added to Page</p>
</>
);
export default Page;
可以在app.js中添加默认的seo属性,没有设置seo的page使用默认的seo
import App, { Container } from 'next/app';
import { DefaultSeo } from 'next-seo';
// import your default seo configuration
import SEO from '../next-seo.config';
export default class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<DefaultSeo
openGraph={{
type: 'website',
locale: 'en_IE',
url: 'https://www.url.ie/',
site_name: 'SiteName',
}}
twitter={{
handle: '@handle',
site: '@site',
cardType: 'summary_large_image',
}}
/>
<Component {...pageProps} />
</Container>
);
}
}
在应用顶层创建一个.babelrc或者babel.config.js文件,
{
"presets": [
[
"next/babel",
{
"preset-env": {},
"transform-runtime": {},
"styled-jsx": {},
"class-properties": {}
}
]
],
"plugins": []
}
getInitialProps是在渲染页面之前就会运行的API。 如果该路径下包含该请求,则执行该请求,并将所需的数据作为props传递给页面。 (实际上有时会有发送日志等不影响HTML的副作用。 )
getInitialProps只能在pages文件夹内的文件中使用。直接访问后,getInitialProps将在服务器端运行。 另一方面,使用next/link进行客户端路由时,在客户端执行。 因此,建议使用isomorphic-unfetch等fetch库
getStaticProps是用于在构建时预先执行getInitialProps进行的处理并预先生成静态文件的API。 不会在客户端上运行。 始终在服务器端运行。
getStaticPaths用于在使用动态路由时生成静态文件。
import fetch from 'node-fetch'
function Zeit({ name, stars }) {
return <div>{name} stars: {stars}</div>
}
// 首先执行。 返回路径以使用数组进行预构建。
export async function getStaticPaths() {
// zeit获取30个由API管理的存储库
const res = await fetch('https://api.github.com/orgs/zeit/repos')
const repos = await res.json()
// 存储库名称的路径
const paths = repos.map(repo => `/zeit/${repo.name}`)
return { paths, fallback: false }
}
// 接收带有路由信息的参数
export async function getStaticProps({ params }) {
// 对应于文件名zeit/[name].js
const name = params.name
const res = await fetch(`https://api.github.com/repos/zeit/${name}`)
const json = await res.json()
const stars = json.stargazers_count
return { props: { name, stars } }
}
export default Zeit
将html、css转换为图片
// /pages/api/og.tsx
import { ImageResponse } from '@vercel/og';
export const config = {
runtime: 'experimental-edge',
};
export default function () {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
}}
>
Hello world!
</div>
),
{
width: 1200,
height: 600,
},
);
}
node使用satori生成图片
// api.jsx
import satori from 'satori'
const svg = await satori(
<div style={{ color: 'black' }}>hello, world</div>,
{
width: 600,
height: 400,
fonts: [
{
name: 'Roboto',
data: robotoArrayBuffer,
weight: 400,
style: 'normal',
},
],
},
)
module.exports = {
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// 自定义输出目录
distDir: 'build',
// 重定向
async redirects() {
return [
{
source: '/about',
destination: '/',
permanent: true,
},
]
},
// 重写路径
async rewrites() {
return [
{
source: '/about',
destination: '/',
},
]
},
// 环境变量
env: {
customKey: 'my-value',
},
// 自定义webpack配置
config.module.rules.push({
test: /\.mdx/,
use: [
options.defaultLoaders.babel,
{
loader: '@mdx-js/loader',
options: pluginOptions.options,
},
],
})
// Important: return the modified config
return config
},
}
Next.js
项目的部署,需要一个 Node.js
的服务器,可以选择 Express
, Koa
或其他 Nodejs
的Web服务器。本文中以 Express
为例来部署 Next
项目。
在next中使用
npm install @next/mdx @mdx-js/loader @code-hike/mdx
在next.config.js中使用
const theme = require("shiki/themes/nord.json")
const {
remarkCodeHike,
} = require("@code-hike/mdx")
const withMDX = require("@next/mdx")({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
[remarkCodeHike, { theme }]
],
},
})
module.exports = withMDX({
pageExtensions: [
"ts", "tsx", "js",
"jsx", "md", "mdx"
],
})
在项目的根组件中引入code-hike的样式
import "@code-hike/mdx/dist/index.css"
在项目中mdx中使用
# Hello
Lorem ipsum dolor sit amet.
```python hello.py
print("Rendered with Code Hike")
```
Lorem ipsum dolor sit amet.
https://codehike.org/docs/configuration
语法高亮的npm包
安装
npm i shiki
使用
const shiki = require('shiki')
shiki
.getHighlighter({
theme: 'nord'
})
.then(highlighter => {
console.log(highlighter.codeToHtml(`console.log('shiki');`, { lang: 'js' }))
})
// <pre class="shiki nord" style="background-color: #2e3440"><code>
// <!-- Highlighted Code -->
// </code></pre>
https://github.com/shikijs/shiki
T3是一个类型安全的全栈nextjs app
安装
npm create t3-app@latest
基于react和Graphql的react客户端框架
首先创建一个基于create-react-app的文件夹
# NPM
npx create-react-app your-app-name
# Yarn
yarn create react-app your-app-name
然后安装relay包,就可以使用基于graphQL的获取数据方式了
# NPM Users
npm install --save relay-runtime react-relay
npm install --save-dev relay-compiler babel-plugin-relay
修改package.json
// your-app-name/package.json
{
...
"scripts": {
...
"start": "yarn run relay && react-scripts start",
"build": "yarn run relay && react-scripts build",
"relay": "yarn run relay-compiler"
...
},
"relay": {
"src": "./src/",
"schema": "./schema.graphql",
"language": "javascript"
}
...
}
创建一个环境配置文件
// your-app-name/src/RelayEnvironment.js
import {Environment, Network, RecordSource, Store} from 'relay-runtime';
import fetchGraphQL from './fetchGraphQL';
// Relay passes a "params" object with the query name and text. So we define a helper function
// to call our fetchGraphQL utility with params.text.
async function fetchRelay(params, variables) {
console.log(`fetching query ${params.name} with ${JSON.stringify(variables)}`);
return fetchGraphQL(params.text, variables);
}
// Export a singleton instance of Relay Environment configured with our network function:
export default new Environment({
network: Network.create(fetchRelay),
store: new Store(new RecordSource()),
});
在组件中使用
import React from 'react';
import './App.css';
import graphql from 'babel-plugin-relay/macro';
import {
RelayEnvironmentProvider,
loadQuery,
usePreloadedQuery,
} from 'react-relay/hooks';
import RelayEnvironment from './RelayEnvironment';
const { Suspense } = React;
// Define a query
const RepositoryNameQuery = graphql`
query AppRepositoryNameQuery {
repository(owner: "facebook", name: "relay") {
name
}
}
`;
const preloadedQuery = loadQuery(RelayEnvironment, RepositoryNameQuery, {
/* query variables */
});
function App(props) {
const data = usePreloadedQuery(RepositoryNameQuery, props.preloadedQuery);
return (
<div className="App">
<header className="App-header">
<p>{data.repository.name}</p>
</header>
</div>
);
}
dva 首先是一个基于 redux 和 redux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router和 fetch,所以也可以理解为一个轻量级的应用框架。
dva把redux的action、reducer、createActions、actionType等不同目录的文件组织在一个modle文件中。
安装
npm install dva-cli@next -g
创建项目
dva new myapp
进入目录,运行
npm start
安装
npm install -g blitz
创建项目
blitz new AppName
cd
安装
npm install -g umi
Umi 中约定 src/global.css
为全局样式,如果存在此文件,会被自动引入到入口文件最前面
比如用于覆盖样式,
.ant-select-selection {
max-height: 51px;
overflow: auto;
}
Umi 会自动识别 CSS Modules 的使用,你把他当做 CSS Modules 用时才是 CSS Modules
// CSS Modules
import styles from './foo.css';
// 非 CSS Modules
import './foo.css';
Umi 内置支持 less,不支持 sass 和 stylus,但如果有需求,可以通过 chainWebpack 配置或者 umi 插件的形式支持
MFSU
mfsu 是一种基于 webpack5 新特性 Module Federation 的打包提速方案。核心原理是将应用的依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译。
因此,开启 mfsu 可以大幅减少热更新所需的时间。在生产模式,也可以通过提前编译依赖,大幅提升部署效率。
mfsu:{}
。umi dev
启动项目。在构建依赖时,会出现 MFSU 的进度条,此时应用可能会被挂起或显示依赖不存在,请稍等。mfsu.development.output
配置预编译依赖输出目录并添加到 git 中,在其他开发者启动时,就可以免去再次编译依赖的过程。~/.umi/.cache/.mfsu
下。并且使用了 webpack 缓存,减少再次编译依赖的时间。~/.mfsu/MFSU_CACHE.json
中,用于依赖的 diff。cache-control: max-age=31536000,immutable
,减少浏览器刷新拉取依赖的时间。warning: 由于预编译依赖实现了部分的 tree-shaking,不建议在打包大小敏感的项目中启用生产模式。
mfsu.production = {}
以开启生产模式。umi build
,默认情况下将会将生产依赖预编译到 ~/.mfsu-production
中。~/dist
中,mfsu 再将生产预编译依赖移动到输出目录中。~/.mfsu-production
添加到 git 中。在部署时,仅编译应用文件,速度快到飞起。和creat-react-app的不同
create-react-app 是基于 webpack 的打包层方案,包含 build、dev、lint 等,他在打包层把体验做到了极致,但是不包含路由,不是框架,也不支持配置。所以,如果大家想基于他修改部分配置,或者希望在打包层之外也做技术收敛时,就会遇到困难。
和nextjs的不同
next.js 是个很好的选择,Umi 很多功能是参考 next.js 做的。要说有哪些地方不如 Umi,我觉得可能是不够贴近业务,不够接地气。比如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面等等一线开发者才会遇到的问题。
除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
如果没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages
目录拿到路由配置。
动态路由
约定 []
包裹的文件或文件夹为动态路由。
嵌套路由
Umi 里约定目录下有 _layout.tsx
时会生成嵌套路由,以 _layout.tsx
为该目录的 layout。layout 文件需要返回一个 React 组件,并通过 props.children
渲染子组件。
404路由
约定 src/pages/404.tsx
为 404 页面,需返回 React 组件。
权限路由
通过指定高阶组件 wrappers
达成效果。
在 umi 里,页面之间跳转有两种方式:声明式和命令式。
声明式
通过Link使用,通常作为react 组件使用
import { Link } from 'umi';
export default () => (
<Link to="/list">Go to list page</Link>
);
命令式
通过history使用,在事件处理中调用
import { history } from 'umi';
function goToListPage() {
history.push('/list');
}
proxy
配置http-proxy-middleware的proxy
proxy: {
'/api': {
'target': 'http://jsonplaceholder.typicode.com/',
'changeOrigin': true,
'pathRewrite': { '^/api' : '' },
}
}
publicPath
配置 webpack 的 publicPath。当打包的时候,webpack 会在静态文件路径前面添加 publicPath
的值,当你需要修改静态文件地址时,比如使用 CDN 部署,把 publicPath
的值设为 CDN 的值就可以。如果使用一些特殊的文件系统,比如混合开发或者 cordova 等技术,可以尝试将 publicPath
设置成 ./
相对路径
相对路径 ./
有一些限制,例如不支持多层路由 /foo/bar
,只支持单层路径 /foo
如果你的应用部署在域名的子路径上,例如 https://www.your-app.com/foo/
,你需要设置 publicPath
为 /foo/
,如果同时要兼顾开发环境正常调试,你可以这样配置
import { defineConfig } from 'umi';
export default defineConfig({
publicPath: process.env.NODE_ENV === 'production' ? '/foo/' : '/',
});
useIntl
umi的useIntl是基于react-intl的。使用formatMessage api
useRequest
Prompt
提供一个用户离开页面时的提示选择
import { Prompt } from 'umi';
export default () => {
return (
<div>
{/* 用户离开页面时提示一个选择 */}
<Prompt message="你确定要离开么?" />
{/* 用户要跳转到首页时,提示一个选择 */}
<Prompt
message={(location) => {
return location.pathname !== '/' ? true : `您确定要跳转到首页么?`;
}}
/>
{/* 根据一个状态来确定用户离开页面时是否给一个提示选择 */}
<Prompt when={formIsHalfFilledOut} message="您确定半途而废么?" />
</div>
);
};
有时候这个提示会连续出现两次,跳转方法一次push一次replace,可以利用return为true取消第二次提示
mfsu,是 Module Federation Speed Up 的缩写,含义为:基于 webpack5 的 module federation 特性的提速方案。它有以下的特点:
module federation 是 webpack5 提出的新特性,含义为模块联邦。主要是使用于微前端场景。联邦的含义是:我可以通过一个个分散的联邦,组合成一个强大的帝国。所以在 webpack 的模块联邦里,每一个应用可以对外暴露自己的一些组件,供其他应用使用
既然模块联邦可以让一个应用从另一个应用拉取模块,我们可以不可以构建一个包含了所有依赖的应用呢
在每一次启动项目和热更新的时候,webpack 都需要对依赖和项目文件进行编译,尤其我们项目中的大部分质量都存在于 node_modules。那么如果我们减少了对依赖的重新编译,是不是可以减少项目启动和热更新的时间呢?
基于这样的思考,mfsu 的方案已经呼之欲出:我们可以利用 webpack5 的 module federation 特性,构建一个虚拟的 federation 应用,随后,我们的项目直接仅使用编译好的依赖,这样就可以直接减去热更新和启动时对依赖的重新编译
因此,mfsu 快的原因很简单,就是直接砍掉了对依赖的编译过程!
提前编译了依赖,带来了很多的好处:
useTransition是React中用于挂起的hook
const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });
<button disabled={isPending}
startTransition(()=>{
<fetch Calls
})>
</button>
{isPending? "Loading": null}
Suspense使组件能够在渲染之前等待一段预定的时间
前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框架的流行,使我们在平时开发时不用太关注安全问题。以 React 为例,React 从设计层面上就具备了很好的防御 XSS 的能力。
XSS无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。React 官方中提到了 React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了 XSS 攻击。
自动转义
React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<>
这几个字符进行转义,转义部分源码如下
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '"';
break;
case 38: // &
escape = '&';
break;
case 39: // '
escape = ''';
break;
case 60: // <
escape = '<';
break;
case 62: // >
escape = '>';
break;
default:
continue;
}
}
这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下
// 一段恶意代码
<img src="empty.png" onerror ="alert('xss')">
// 转义后输出到 html 中
<img src="empty.png" onerror ="alert('xss')">
使用dangerouslySetInnerHTML
dangerouslySetInnerHTML
是 React 为浏览器 DOM 提供 innerHTML
的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易使用户暴露在 XSS 攻击下,因为当使用 dangerouslySetInnerHTML
时,React 将不会对输入进行任何处理并直接渲染到 HTML 中,如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码,那么浏览器将会运行恶意代码。
function getNonChildrenInnerMarkup(props) {
const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性,会不经转义就渲染__html的内容
if (innerHTML != null) {
if (innerHTML.__html != null) {
return innerHTML.__html;
}
} else {
const content = props.children;
if (typeof content === 'string' || typeof content === 'number') {
return escapeTextForBrowser(content);
}
}
return null;
}
所以平时开发时最好避免使用 dangerouslySetInnerHTML
,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤,通过白名单控制允许的 HTML 标签及各标签的属性
通过用户提供的对象来创建react组件
// 用户的输入
const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' src='empty.png' />"}}"`;
// 经过 JSON 转换
const userProvideProps = JSON.parse(userProvidePropsString);
// userProvideProps = {
// dangerouslySetInnerHTML: {
// "__html": `<img onerror='alert("xss");' src='empty.png' />`
// }
// };
render() {
// 出于某种原因解析用户提供的 JSON 并将对象作为 props 传递
return <div {...userProvideProps} />
}
这段代码将用户提供的数据进行 JSON 转换后直接当做 div
的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。
使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等
const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>
如果没有对该 URL 进行过滤以防止通过 javascript:
或 data:
来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。 用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。
服务端如何防止XSS攻击
服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:
出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。