​ 基于React的衍生框架

# create-react-app

# react-app-rewired

安装

npm install react-app-rewired --save-dev
1

在根目录下创建一个config-overrides.js文件

/* config-overrides.js */

module.exports = function override(config, env) {
  //do stuff with the webpack config...
  return config;
}
1
2
3
4
5
6

修改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"
}
1
2
3
4
5
6
7
8
9
10
11

配置文件

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;
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

添加多页面入口

npm install react-app-rewire-multiple-entry --save-dev
1

在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(),
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# CRACO

Create React App Configuration Override修改cra配置的一个包

安装

npm i @craco/craco
1

在根目录下创建一个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"
}
1
2
3
4
5
6
7
8

配置文件可以是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()], [])
        ]
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

配置jest

/* jest.config.js */

const { createJestConfig } = require("@craco/craco");

const cracoConfig = require("./craco.config.js");
const jestConfig = createJestConfig(cracoConfig);

module.exports = jestConfig;
1
2
3
4
5
6
7
8

# superplate

https://github.com/pankod/superplate

# Remix.js

Remix由 React Router 原班团队打造,基于 TypeScript 与 React,内建 React Router V6 特性的全栈 Web 框架 Remix 正式开源。

Remix 开源之后可以说是在 React 全栈框架领域激起千层浪,绝对可以算是 Next.js 的强劲对手。Remix 的特性如下:

  • 追求速度,然后是用户体验(UX),支持任何 SSR/SSG 等
  • 基于 Web 基础技术,如 HTML/CSS 与 HTTP 以及 Web Fecth API,在绝大部分情况可以不依赖于 JavaScript 运行,所以可以运行在任何环境下,如 Web Browser、Cloudflare Workers、Serverless 或者 Node.js 等
  • 客户端与服务端一致的开发体验,客户端代码与服务端代码写在一个文件里,无缝进行数据交互,同时基于 TypeScript,类型定义可以跨客户端与服务端共用
  • 内建文件即路由、动态路由、嵌套路由、资源路由等
  • 干掉 Loading、骨架屏等任何加载状态,页面中所有资源都可以预加载(Prefetch),页面几乎可以立即加载
  • 告别以往瀑布式(Waterfall)的数据获取方式,数据获取在服务端并行(Parallel)获取,生成完整 HTML 文档,类似 React 的并发特性
  • 提供开发网页需要所有状态,开箱即用;提供所有需要使用的组件,包括
  • 内建错误处理,针对非预期错误处理的 和开发者抛出错误处理的

# 路由

Remix 提供基于文件的路由,将读取数据、操作数据和渲染数据的逻辑都写在同一个路由文件里,方便一致性处理,这样可以跨客户端和服务端逻辑共享同一套类型定义。

# Nextjs

Https://juejin.cn/post/6844904017487724557

Next.js是一个基于React的一个服务端渲染简约框架。它使用React语法,可以很好的实现代码的模块化,有利于代码的开发和维护

Next的优点:

  • 默认服务端渲染模式,以文件系统为基础的客户端路由
  • 代码自动分隔使页面加载更快
  • 以页面为基础的简洁的客户端路由
  • webpack的热替换为基础的开发环境
  • 使用ReactJSXES6module,模块化和维护更方便
  • 可以运行在Express和其他Node.jsHTTP 服务器上
  • 可以定制化专属的babelwebpack配置

创建next项目

npm install --save react react-dom next
1

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
1
2
3
4
5
6
7
8
9
10
11
12
13

# 页面(pages)

在 Next.js 中,一个 page(页面) 就是一个从 .jsjsx.ts.tsx 文件导出(export)

这些文件存放在 pages 目录下。每个 page(页面)都使用其文件名作为路由(route)

Next.js 支持具有动态路由的 pages(页面)。例如,如果你创建了一个命名为 pages/posts/[id].js 的文件,那么就可以通过 posts/1posts/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 所提供的以下函数:

  1. 您的页面 内容 取决于外部数据:使用 getStaticProps
  2. 你的页面 paths(路径) 取决于外部数据:使用 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
}
1
2
3
4
5
6
7
8
9
10
11
12

在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!')
}
1
2
3
4
5
6
7

如果在pages下面的子页面创建了_middleware的中间件,那么中间件会按照目录的层级之行

- package.json
- /pages
    index.tsx
    - /about
      _middleware.ts # Will run first
      about.tsx
      - /teams
        _middleware.ts # Will run second
        teams.tsx
1
2
3
4
5
6
7
8
9

# 多页面

# 使用redux

# 路由遮盖

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>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# next-seo

在next项目中添加seo

安装

npm install next-seo
1

添加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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

可以在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>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 自定义babel

在应用顶层创建一个.babelrc或者babel.config.js文件,

{
  "presets": [
    [
      "next/babel",
      {
        "preset-env": {},
        "transform-runtime": {},
        "styled-jsx": {},
        "class-properties": {}
      }
    ]
  ],
  "plugins": []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# SSR

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# @vercel/og

将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,
    },
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

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',
      },
    ],
  },
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# next.config.js

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
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 部署

Next.js 项目的部署,需要一个 Node.js的服务器,可以选择 Express, Koa或其他 Nodejs 的Web服务器。本文中以 Express 为例来部署 Next 项目。

# Code Hike

在next中使用

npm install @next/mdx @mdx-js/loader @code-hike/mdx
1

在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"
  ],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在项目的根组件中引入code-hike的样式

import "@code-hike/mdx/dist/index.css"
1

在项目中mdx中使用

# Hello

Lorem ipsum dolor sit amet.

```python hello.py
print("Rendered with Code Hike")
```

Lorem ipsum dolor sit amet.
1
2
3
4
5
6
7
8
9

https://codehike.org/docs/configuration

# shiki

语法高亮的npm包

安装

npm i shiki
1

使用

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>
1
2
3
4
5
6
7
8
9
10
11
12
13

https://github.com/shikijs/shiki

# T3

T3是一个类型安全的全栈nextjs app

安装

npm create t3-app@latest
1

# Relay

基于react和Graphql的react客户端框架

首先创建一个基于create-react-app的文件夹

# NPM
npx create-react-app your-app-name
# Yarn
yarn create react-app your-app-name
1
2
3
4

然后安装relay包,就可以使用基于graphQL的获取数据方式了

# NPM Users
npm install --save relay-runtime react-relay
npm install --save-dev relay-compiler babel-plugin-relay
1
2
3

修改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"
  }
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

创建一个环境配置文件

// 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()),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在组件中使用

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>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# Dvajs

dva 首先是一个基于 redux 和 redux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router和 fetch,所以也可以理解为一个轻量级的应用框架。

dva把redux的action、reducer、createActions、actionType等不同目录的文件组织在一个modle文件中。

安装

npm install dva-cli@next -g
1

创建项目

dva new myapp
1

进入目录,运行

npm start
1

# blitz.js

安装

npm install -g blitz
1

创建项目

blitz new AppName
cd 
1
2

# Taxonomy

全栈开发app

# Umijs

安装

npm install -g umi
1

Umi 中约定 src/global.css 为全局样式,如果存在此文件,会被自动引入到入口文件最前面

比如用于覆盖样式,

.ant-select-selection {
  max-height: 51px;
  overflow: auto;
}
1
2
3
4

Umi 会自动识别 CSS Modules 的使用,你把他当做 CSS Modules 用时才是 CSS Modules

// CSS Modules
import styles from './foo.css';

// 非 CSS Modules
import './foo.css';
1
2
3
4
5

Umi 内置支持 less,不支持 sass 和 stylus,但如果有需求,可以通过 chainWebpack 配置或者 umi 插件的形式支持

MFSU

mfsu 是一种基于 webpack5 新特性 Module Federation 的打包提速方案。核心原理是将应用的依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译。

因此,开启 mfsu 可以大幅减少热更新所需的时间。在生产模式,也可以通过提前编译依赖,大幅提升部署效率。

# 开发阶段

  1. 初始化一个 umi 应用。
  2. 在 config.ts 中添加 mfsu:{}
  3. umi dev 启动项目。在构建依赖时,会出现 MFSU 的进度条,此时应用可能会被挂起或显示依赖不存在,请稍等。
  4. 多人合作时,可以配置 mfsu.development.output 配置预编译依赖输出目录并添加到 git 中,在其他开发者启动时,就可以免去再次编译依赖的过程。

# 特性

  • 预编译:默认情况下,预编译将会将依赖构建到 ~/.umi/.cache/.mfsu 下。并且使用了 webpack 缓存,减少再次编译依赖的时间。
  • diff:预编译时,会将本次的依赖信息构建到 ~/.mfsu/MFSU_CACHE.json 中,用于依赖的 diff。
  • 持久化缓存:对于预编译依赖的请求,开启了cache-control: max-age=31536000,immutable,减少浏览器刷新拉取依赖的时间。

# 构建阶段

warning: 由于预编译依赖实现了部分的 tree-shaking,不建议在打包大小敏感的项目中启用生产模式。

  1. 配置 config.ts:mfsu.production = {}以开启生产模式。
  2. 执行命令:umi build,默认情况下将会将生产依赖预编译到 ~/.mfsu-production 中。
  3. umi 会将依赖外的产物构建到 ~/dist 中,mfsu 再将生产预编译依赖移动到输出目录中。
  4. 使用 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>
);
1
2
3
4
5

命令式

通过history使用,在事件处理中调用

import { history } from 'umi';

function goToListPage() {
  history.push('/list');
}
1
2
3
4
5

# config

proxy

配置http-proxy-middleware的proxy

proxy: {
  '/api': {
    'target': 'http://jsonplaceholder.typicode.com/',
    'changeOrigin': true,
    'pathRewrite': { '^/api' : '' },
  }
}
1
2
3
4
5
6
7

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/' : '/',
});
1
2
3
4
5

# API

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>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

有时候这个提示会连续出现两次,跳转方法一次push一次replace,可以利用return为true取消第二次提示

# msfu原理

mfsu,是 Module Federation Speed Up 的缩写,含义为:基于 webpack5 的 module federation 特性的提速方案。它有以下的特点:

  • 快!项目启动只需要3s,热更新提速 50%,生产模式部署提升 50 倍 1 !
  • 全!基于 webpack 的研发体系,生态更加完善!未来也会加入 esbuild,让快更快!
  • 稳!蚂蚁内部近千个前端项目,都将开启 mfsu!
  • 狠!云谦老师主持开发,解决 bug 就是快刀斩乱麻!

module federation 是 webpack5 提出的新特性,含义为模块联邦。主要是使用于微前端场景。联邦的含义是:我可以通过一个个分散的联邦,组合成一个强大的帝国。所以在 webpack 的模块联邦里,每一个应用可以对外暴露自己的一些组件,供其他应用使用

既然模块联邦可以让一个应用从另一个应用拉取模块,我们可以不可以构建一个包含了所有依赖的应用呢

在每一次启动项目和热更新的时候,webpack 都需要对依赖和项目文件进行编译,尤其我们项目中的大部分质量都存在于 node_modules。那么如果我们减少了对依赖的重新编译,是不是可以减少项目启动和热更新的时间呢?

基于这样的思考,mfsu 的方案已经呼之欲出:我们可以利用 webpack5 的 module federation 特性,构建一个虚拟的 federation 应用,随后,我们的项目直接仅使用编译好的依赖,这样就可以直接减去热更新和启动时对依赖的重新编译

因此,mfsu 快的原因很简单,就是直接砍掉了对依赖的编译过程!

提前编译了依赖,带来了很多的好处:

  1. 即使项目规模继续增大,依赖的数量继续增多,启动和热更新都可以保持性能!
  2. 可以将预编译产物在团队中进行同步,其他同学可以直接享受到预编译带来的快乐!
  3. 生产模式下,可以持续使用预编译好的依赖,以加快部署速度!

# redwoodjs

基于GraphQL、prisma、fastify和react的全栈开发框架

form

import { MetaTags } from '@redwoodjs/web'
import {
  Form,
  TextField,
  TextAreaField,
  Submit,
  SubmitHandler
} from '@redwoodjs/forms'

interface FormValues {
  name: string
  email: string
  message: string
}

const ContactPage = () => {
  const onSubmit: SubmitHandler<FormValues> = (data) => {
    console.log(data)
  }

  return (
    <>
      <MetaTags title="Contact" description="Contact page" />

      <Form onSubmit={onSubmit}>
        <label htmlFor="name">Name</label>
        <TextField name="name" />

        <label htmlFor="email">Email</label>
        <TextField name="email" />

        <label htmlFor="message">Message</label>
        <TextAreaField name="message" />

        <Submit>Save</Submit>
      </Form>
    </>
  )
}

export default ContactPage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

app配置

使用redwool.toml文件进行配置

# refine

refine 是一个基于 React 的快速框架✨网络应用程序的开发。它消除了 CRUD 操作所需的重复性任务,并为身份验证 **、访问控制路由网络状态管理i18n 等关键部分提供了行业标准解决方案。

refine 在设计上是无限制的,因此提供了无限的样式和定制选项

⚙️使用单个 CLI 命令进行零配置、一分钟设置

🔌用于 15 多种后端服务 的连接器,包括 REST API (opens new window)GraphQL (opens new window)NestJs CRUD (opens new window)Airtable (opens new window)Strapi (opens new window)Strapi v4 (opens new window)Strapi GraphQL (opens new window)Supabase (opens new window)Hasura (opens new window)Nhost (opens new window)Appwrite (opens new window)Firebase (opens new window)Directus (opens new window)Altogic (opens new window)

🌐 Next.js 或 Remix ***支持*** SSR

⚛使用 React Query 完善 状态管理突变

🔀 使用您选择的任何路由器库的高级路由

🔐无缝身份验证访问控制流程的提供商

对实时 / 实时应用程序的开箱即用支持

📄轻松的审核日志文档版本控制

💬支持任何 i18n 框架

💪面向未来的稳健架构

✅完整的测试覆盖

创建/运行项目

npx superplate-cli --preset refine-antd my-project

npm run dev
1
2
3

新建页面

import { Refine, useMany } from "@pankod/refine-core";
import {
    useTable,
    List,
    Table,
    DateField,
    Layout,
    ReadyPage,
    notificationProvider,
    ErrorComponent,
} from "@pankod/refine-antd";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";

import "@pankod/refine-antd/dist/styles.min.css";

const App: React.FC = () => {
    return (
        <Refine
            routerProvider={routerProvider}
            dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
            resources={[{ name: "posts", list: PostList }]}
            Layout={Layout}
            ReadyPage={ReadyPage}
            notificationProvider={notificationProvider}
            catchAll={<ErrorComponent />}
        />
    );
};

export const PostList: React.FC = () => {
    const { tableProps } = useTable<IPost>();

    const categoryIds =
        tableProps?.dataSource?.map((item) => item.category.id) ?? [];

    const { data, isLoading } = useMany<ICategory>({
        resource: "categories",
        ids: categoryIds,
        queryOptions: {
            enabled: categoryIds.length > 0,
        },
    });

    return (
        <List>
            <Table<IPost> {...tableProps} rowKey="id">
                <Table.Column dataIndex="title" title="title" />
                <Table.Column
                    dataIndex={["category", "id"]}
                    title="category"
                    render={(value: number) => {
                        if (isLoading) {
                            return "loading...";
                        }

                        return data?.data.find(
                            (item: ICategory) => item.id === value,
                        )?.title;
                    }}
                />
                <Table.Column
                    dataIndex="createdAt"
                    title="createdAt"
                    render={(value) => <DateField format="LLL" value={value} />}
                />
            </Table>
        </List>
    );
};

export default App;

interface IPost {
  title: string;
  createdAt: string;
  category: { id: number };
}

interface ICategory {
  id: number;
  title: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

# React18

# 并发模式

useTransition是React中用于挂起的hook

const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });

<button disabled={isPending}
  startTransition(()=>{
   	<fetch Calls 
  })>
</button>
{isPending? "Loading": null}
1
2
3
4
5
6
7
8

# 为获取数据的Suspense

Suspense使组件能够在渲染之前等待一段预定的时间

# React XSS

前端一般会面临 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 = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;';
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下

// 一段恶意代码
<img src="empty.png" onerror ="alert('xss')"> 
// 转义后输出到 html 中
&lt;img src=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 
1
2
3
4

# 可能引起漏洞的写法

使用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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

所以平时开发时最好避免使用 dangerouslySetInnerHTML,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤 (opens new window),通过白名单控制允许的 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} /> 
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这段代码将用户提供的数据进行 JSON 转换后直接当做 div 的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。

使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等

const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>
1
2

如果没有对该 URL 进行过滤以防止通过 javascript:data: 来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。 用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。

服务端如何防止XSS攻击

服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:

  • 在接收到用户输入时,需要对输入进行尽可能严格的过滤,过滤或移除特殊的 HTML 标签、JS 事件的关键字等。
  • 在输出时对数据进行转义,根据输出语境 (html/javascript/css/url),进行对应的转义
  • 对关键 Cookie 设置 http-only 属性,JS 脚本就不能访问到 http-only 的 Cookie 了
  • 利用 CSP (opens new window) 来抵御或者削弱 XSS 攻击,一个 CSP 兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和 HTML 的事件处理属性)

出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。

Last Updated: 2/19/2024, 12:16:30 AM