豪翔天下

Change My World by Program

0%

  • 基本上是我现在开发新旧项目的必备工具了

安装配置

基础

配置文件tailwind.config.js

  • 如果项目之前已经有大量的存在的css,为了防止冲突可以使用prefix去防止覆盖,对于负数的属性,prefix仍然需要写在最前面,例如tw--mt-10
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
module.exports = {
important: false, // 是否在所有生成的样式加上!important,不推荐这么做
prefix: 'tw-', // 添加一个前缀,
purge: [ // 指定需要从哪些文件中查找我们需要使用的class(这样可以只编译出我们有使用的class)
'../views/site/*.php'
],
darkMode: false, // or 'media' or 'class',默认选项
theme: {
fontFamily: { // 直接替换默认字体
'sans': 'Roboto, sans-serif',
'serif': 'Roboto, sans-serif',
'mono': 'Roboto, sans-serif',
'display': 'Roboto, sans-serif',
'body': 'Roboto, sans-serif'
},
extend: { // 可以添加一些自定义的样式,或者覆盖之前的样式,在官方文档每一个样式页面下面多有个性化的说明
backgroundImage: {
'my-bg': "url('/')" // 甚至可以这样定义一个背景图片类
main: 'linear-gradient(225deg, #BD7AE3 0%, #8461C9 100%)'
},
boxShadow: {
'md-all': '4px 4px 6px -1px rgba(0, 0, 0, 0.1), -2px 2px 4px -1px rgba(0, 0, 0, 0.06)' // 四周阴影
}
height: {
'full-vw': '100vw'
},
minWidth: {
'36': '9rem'
},
spacing: {
80: '20rem',
'38': '9.5rem',
'120': '30rem',
'128': '32rem',
'144': '36rem',
'160': '40rem',
172: 44rem,
'192': '48rem',
'232': '58rem',
240: '60rem',
272: '68rem',
280: '70rem',
288: '72rem',
'320': '80rem',
},
width: {
232: '58rem'
},
zIndex: {
'-10': '-10',
}
},
},
variants: {
extend: {
borderRadius: ['hover'] // 给rounded添加hover效果
},
},
plugins: [],
corePlugins: {
preflight: false, // 添加这个配置可以让tailwind不覆盖默认的基础元素的样式,例如html、body、h1等https://tailwindcss.com/docs/preflight
}
}
阅读全文 »

安装与配置

常用命令

1
2
3
4
5
yii serve 0.0.0.0 --port=8888	# 指定端口,指定host

# 缓存,缓存的文件在frontend/runtime/cache和backend/runtime/cache下面
yii cache # li
yii cache/flush-schema db # 清除db缓存

常用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# index.php
defined('YII_DEBUG') or define('YII_DEBUG', true); # 打开debug模式

# common/config/main-local.php
'db' => [ # 数据库配置
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=password',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
'enableSchemaCache' => true,
'schemaCacheDuration' => 86400,
'schemaCache' => 'cache',
]

// backend/config/main-local.php
$config = [
'components' => [
'request' => [
'enableCsrfValidation' => false, // 可以全局关闭csrf验证
]
]
]
阅读全文 »

  • 这是一个不怎么好用的工具

Data Sources数据源

  • 可以来自系统中已定义好的第三方服务,也可以直接来自于文件上传、API接口、SQL查询、FTP上传、Email附件

  • 可以自定义时间去刷新(1h - 24h),但是如果是文件上传这种是不能自动刷新的

  • 可以支持参数,但是必须依赖于klip变量,例如,可以写成https://haofly.net/{props.pageName},这里的pageName就是klip的变量,如果是第一次访问一个之前没有请求过的参数,那么可能会比较慢,后续的定时刷新也是可以起作用的,刷新的会把所有请求过的参数都请求一遍

  • 由于数据源的接口请求超时时间是80s,对于数据量大的,我们可以创建email形式的数据源,定时往指定的邮箱发送附件即可

  • 数据源的大小默认最大是10MB,通过付费计划可以升级到15MB,但是更大就不行了,并且modelled data也不能超过这个限制,所以要想join几张大表,可以在klip面板使用ARRAY(表1, 表2),或者LOOKUP来查找需要的mapping数据。明明是我自己研究出来的方法,发现官网有文档的: Managing your data source size

  • 如果要在dashboard上手动请求刷新data sources,可以直接data sources的请求刷新接口:

    1
    2
    3
    4
    // 使用html component做一个刷新按钮,然后手动POST接口
    xmlHttp.open('POST', 'https://app.klipfolio.com/datasources/ajax_refresh_datasource', true);
    xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xmlHttp.send('di=&dsid=' + data source的id); // 这里加上di表示直接等待它完成,如果不加则是把它放入了刷新队列里面去
阅读全文 »

基本请求

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
// 基本请求方式
axios({
method: 'post',
url: '/user/123',
headers: {},
params: {},
data: {}, // POST的data
timeout: 0, // 超时时间,默认为0,表示不超时
responseType: 'json', // 默认接收JSON格式的响应
maxRedirects: 5, // 默认重试次数为5
onUploadProgress: function (progressEvent) {}, // 上传前执行
onDownloadProgress: function (progressEvent) {}, // 下载前执行
validateStatus: function (status) {
return status >= 200 && status < 500 // 定义哪些http状态不会抛错
}
})
.then((rersponse: AxiosResponse) => {})
.catch((error: AxiosError) => {
console.log(error.response.status) // 获取返回状态码
console.log(error.message) // 获取错误信息
console.log(JSON.parse(error.request.response).message) // 另外一种错误相应的格式
console.log(`${error.config.baseURL}${error.config.url}`) // 获取请求的URL
})

// 创建一个可复用的client
const client = axios.create({
baseURL: '',
headers: {}
})

Axios跨域请求

1
2
3
4
axios.get('/user', {
withCredentials: true, // 跨域请求带上认证信息
params: {}
}).then(...).catch(...)
阅读全文 »

  • 样式使用css-in-js风格,得单独学一套
  • 官方中文文档
  • 从5.x开始material-ui更名为mui了,网上搜到不要奇怪
  • 官方没有react-native 相关的UI插件,可以使用react-native-paper来代替,它也是遵循material design的

组件

  • 动态调用组件的方式

    1
    2
    3
    4
    5
    6
    7
    const components = {
    a: AComponent,
    b: BComponent
    }

    const MyComponent = components['a']
    return <MyComponent /> // 调用的时候必须大写

Inputs

Data Display 数据展示

Icons 图标

  • 我们可以用SvgIcon来封装自己的图标,如果有自己的图标并且数量多且用的地方多,最好用这个来封装每一个svg,就能让他们统一起来

  • 还有种借助webpack的svgr进行封装的方式可以让svg仍然以svg的形式存在,但是没有试过,先就不写了

  • 封装只需要这样做即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function HomeIcon(props) {
    return (
    <SvgIcon {...props}
    aria-label="home" <!--语意话-->
    viewBox="0 0 36.997 35.901"<!--通常我们从设计得到的svg不是统一24的尺寸,通常有自己的尺寸,需要将该viewBox写到这里,否则可能会缺少一部分-->
    >
    <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" /> <!--这一块是svg的内容(svg tag的内容部分)-->
    </SvgIcon>
    );
    }
  • 实用起来就方便得多了

    • 注意如果svg的color修改不成功,可能是因为svg中某些样式直接写在了path的fill属性中,可以直接在path元素上面加上fill={props.color || "white"}
    1
    2
    3
    4
    5
    6
    7
    8
    <HomeIcon
    fontSize={"large"} // fontSize=2.1875rem/35px,还可选small
    style={{ fontSize: 40 }}

    color="paimary" // paimary, secondary, action, disabled
    style={{ color: green[500] }}

    />

Tooltip 提示

  • 遇到一个很奇怪的问题,所有的tooltip都只固定在页面的左上角,而不是元素的上方,结果发现是有程序员给所有div添加了width: 100%;height:100%的属性,我去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Tooltip
leaveDelay={200000} // 显示时长,调试的时候可以把这个增大
placement={"top"}
interactive // 交互式,当鼠标移动到弹出框上时不会因为leaveDelay时间到了而关闭,如果没有它,弹出框将不能被点击,鼠标的点击事件都是下层元素的
title={ // 自定义弹出框内容
<React.Fragment>
<Typography color="inherit">Tooltip with HTML</Typography>
<em>{"And here's"}</em> <b>{'some'}</b> <u>{'amazing content'}</u>.{' '}
{"It's very engaging. Right?"}
</React.Fragment>
}
>
<Button>
<Avatar src={avatar} />
</Button>
</Tooltip>

Typography 文字

  • fontWeight/fontSize这些都不能直接设置,只能外面套一层Box
1
2
3
4
5
<Typography 
component="h4" // 使用component能让他直接变成h4元素
color="inherit" align="center" paragraph>
Content
</Typography>

Feedback

Surfaces

Accordion/Expand手风琴

  • 可以伸缩展开的手风琴效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const [expanded, setExpanded] = useState(false)

<Accordion
defaultExpanded={false} // 默认是否展开
onChange={() => setExpanded(!expanded)}
elevation={0}> // evevation参数可以不显示子元素外层的border
<AccordionSummary
expandIcon={expanded ? <FaMinus /> : <FaPlus />} // 可以通过事件来使用不同的icon
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>Accordion 1</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Typography>
</AccordionDetails>
</Accordion>

accordionSummary: {
flexDirection: 'row-reverse' // 添加这个css类可以让icon显示在左侧
}

Bottom Navigation 底部导航栏

Link链接

1
2
3
<Link component="button" color="inherit" underline="always">
This is a button
</Link>

Layout 布局

Box 分组

  • 非常实用的一个组件,类似于bootstrap中的utilities,可以非常方便地通过props来设置样式

  • 这就是material-ui中的System 系统,不过system系统包含一些非常实用的内联样式,不过这玩意儿不是每个元素上都可以直接加的,只有自带的Box组件可以直接加,所以一般是直接在外面包围一层Box,当然如果想要修改子元素的样式,可以用clone方法

    1
    2
    3
    4
    5
    // 这样在实际生成的DOM元素中就不会有一个多的Box层了,而是直接将样式附加到了子元素上
    // 当clone不work的时候,可以尝试调换Box组件和被clone的组件的引入顺序
    <Box color="text.primary" clone>
    <Button />
    </Box>
  • 支持的所有的属性(其中不包含的常用的属性包括background-image/background-position)

    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
    <Box 
    component="span" // box默认是一个div元素,也可以通过这个属性置顶其为特定的元素

    // Borders边框
    border={1}
    borderRadius={"50%"} // border-radius
    borderColor="primary.main" // secondary.main, error.main, grey.500, text.primary

    // Color/Palette 颜色
    bgcolor="primary.main"
    bgcolor="secondary.main"
    bgcolor="text.primary" // 黑色

    // Display 位置
    position={'fixed'}
    bottom={0}

    // Flexbox
    display="flex"
    flexDirection="row"
    flexWrap="nowrap"
    justifyContent="center"
    justifyContent="space-between"
    alignContent="flex-start"
    alignContent="flex-end"

    // Sizing 大小
    width={1/4}
    width={300}
    width="75%"
    width={1} // 100%

    // Spacing 间距
    p={2}
    pt={3}
    px={1}
    py={4}

    // Typography 文字,对于Typography如果改不了内部的样式,那么直接把标签去掉,直接<Bod>文字</Box>
    textAlign="left" // text-align,可选left、center、right
    fontWeight="fontWeightLight" // font-weight,可选fontWeightLight、fontWeightRegular、fontWeightMedium、fontWeightBold或者直接数字{500}
    fontSize="fontSize" // font-size,可选fontSize,其他元素的size:h6.fontSize,或者直接数字{16}
    fontStyle="normal" // font-style,可选normal、italic、poblique
    fontFamily="fontFamily" // font-family
    letterSpacing={6} // letter-space
    lineHeight={10} // line-height

    minWidth="10"
    maxWidth="80"
    ></Box>
阅读全文 »

Cloud Function

  • 不同配置的价格表
  • 最大超时时间只能设置为540s=9min,实在不行可以用Cloud Tasks 队列,或者在时间快完的时候直接再调用一下url参数组织一下
  • 其他限制: 常用的会有未压缩HTTP请求或响应的大小为10 MB
  • 为了减少函数的执行时间,我们需要尽量提升程序的启动时间,默认都是冷启动的,但是如果间隔时间很小,谷歌可能并没有销毁,这个间隔是谷歌自己控制的,并且是不一定的,所以你会发现如果启动时间长的函数,有时候处理得快有时候处理得慢。当然,谷歌也提供付费服务保证至少有几个实例在运行,如果不想用,还可以自己弄个定时任务去定时请求一次,不过当然那也算运行时间呀。
1
req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.headers['fastly-client-ip']	// 可以通过这种方式获取客户端IP地址

Cloud Scheduler

Cloud Tasks

  • 任务队列,官方文档用起来非常简单实用,可用于多消费者,或者减少第三方接口的并发速率限制

  • 速率控制(队列使用令牌桶来控制任务执行速率,每个命令的队列都有一个用于存储令牌的存储分区,应用每执行一个任务,就会从桶中移除一个令牌,会按照max_dispatches_per_second速率不断向令牌桶中补充填充新令牌)

    • Max dispatches: 每秒钟任务分配的速率,每秒将任务分配给多少个worker
    • Max concurrent dispatches: 并发执行的数量,同时运行的任务的最大数量
  • 重试控制:

    • MAX ATTEMPTS:任务可以尝试的最大次数,包括第一次尝试,-1表示不限制?,但是不能设置为0,所以如果说只想执行一次,应该是设置为1
    • MAX INTERVAL:重试尝试之间的最短等待时间
  • 一些限制

    • 任务大小上限:100KB(超过会报错task size too large)
    • 队列执行速率:每个队列每秒500次任务调度
    • 人物的最大倒计时/ETA:30天
    • 可以批量添加的最大任务数:100个
    • 在一项事务中可以添加的最大任务数:5个
    • 默认的最大任务队列数:100个
  • 官方文档给的例子是发送一个字符串,但是如果要发送json格式的payload,可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const task = {
    httpRequest: {
    httpMethod: 'POST',
    url: `${config.baseUrl}?${searchParams.toString()}`,
    body: Buffer.from(JSON.stringify(params)).toString('base64'),
    headers: {
    'Content-Type': 'application/json',
    },
    },
    };

    const request = { parent: this.queue, task };
    await client.createTask(request);

    // 在接口这边不用做其他处理,就像平常的json请求那样即可
    app.post('/endpoint', (req, res) => {
    const {foo} = req.body;
    console.log(foo); // "bar"
    res.send('ok');
    });
  • The queue cannot be created because a queue with this name existed too recently: 队列删除7天后才能创建同名的队列

阅读全文 »

集成

命令行

1
sudo npm install -g firebase-tools

前端集成

Web端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import firebase from "firebase/app";	// 使用时这样引入,这句话必须有

import "firebase/firestore"; // 如果只想使用其中的一个模块,可以import其中某一个。而且要注意这里不要写成firebase/database

// firebase是一个全局的变量,这样可以防止多次初始化出现错误Firebase App named '[DEFAULT]' already exists (app/duplicate-app) [duplicate]
if (!firebase.apps.length) {
firebase.initializeApp({
apiKey: '<your-api-key>',
authDomain: '<your-auth-domain>',
databaseURL: '<your-database-url>',
projectId: '<your-cloud-firestore-project>',
storageBucket: '<your-storage-bucket>',
messagingSenderId: '<your-sender-id>',
appId: '<your-app-id>'
})
} else {
firebase.app()
}

const db = firebase.firestore();

后端集成

  • 如果前端需要读取私有数据,那么后端需要为前端创建自定义令牌createCustomToken

  • 下面的认证文件项目名-firebase-adminsdk-xxxxx.json来自于firebase console -> Project settings -> Service accounts -> Firebase Admin SDK -> Generate new private key,是所有SDK都需要的

  • 后端只需要npm install --save firebase-admin即可

1
2
3
4
5
6
7
8
import * as admin from 'firebase-admin'
import { HttpsProxyAgent } from 'https-proxy-agent' // 只能以这种方式使用代理,直接在命令行使用export不行

const agent = new HttpsProxyAgent('http://127.0.0.1:1080')
admin.initializeApp({
credential: admin.credential.cert(path.join(__dirname, '../../项目名-firebase-adminsdk-xxxxxxxx.json'), agent),
httpAgent: agent
})

PHP集成

  • php firestore 文档

  • 安装方式(需要ext-grpc扩展)

    1
    2
    3
    4
    5
    # 如果仅仅使用firestore可以只安装某个组件
    composer require google/cloud-firestore --with-all-dependencies # 加这个参数防止guzzlehttp/psr7版本错误

    # 一次安装所有
    composer require google/cloud --with-all-dependencies
  • 使用方式

    1
    2
    3
    4
    5
    6
    # export GOOGLE_APPLICATION_CREDENTIALS=认证文件路径
    $firestore = new FirestoreClient();
    $document = $firestore->document('users/123');
    $document->set([
    'name' => 'test'
    ]);

Cloud Messaging(Push Notification/APNs)

fcm api

  • 最简单的测试方式
1
2
3
4
5
6
7
8
9
10
curl --location --request POST 'https://fcm.googleapis.com/fcm/send' \
--header 'Authorization: key=server_key' \ # 这里填写server_key
--header 'Content-Type: application/json' \
--data-raw '{
"notification": {
"title": "test",
"body": "testbody"
},
"to": "用户的fcm token"
}'

firebase-admin-node

  • 强烈建议在开发时先使用这个包进行测试,因为这个包能够返回非常详细的错误信息,特别是证书没配置对这些信息,而且这个包在使用代理的情况下工作很好,不会像移动端那样,有时候连代理不行,有时候不连代理不行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import * as admin from 'firebase-admin'

// 参照上面的后端集成步骤,初始化admin
const payload = {
data: {
foo: 'bar',
notification_foreground: 'true' // 这样能使客户端收到消息的时候app即使在前台也同样能弹出消息,否则前台就需要自己去处理了,这里的true必须是字符串
},
notification: {
title: 'Message title',
body: 'Message body'
}
}

admin.initializeApp({
credential: admin.credential.cert(path.join(__dirname, '../../xxxxxx-firebase-admin-xxxxx-xxxxxx.json'), agent),
httpAgent: agent
})

admin.messaging().sendToDevice('registrationToken', payload, { timeToLive: 120})
.then((response) => {
console.log(JSON.stringify(response))
})

证书配置

阅读全文 »

  • Roadmap: 真的有好多还没开发完成的实用功能

安装与配置

常用命令

1
export PORT=3000 && npm run develop	# 更改启动端口

配置文件

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
// config/server.js, Server相关配置
module.exports = ({ env }) => ({
host: env('HOST', process.env.HOST),
port: env.int('PORT', process.env.PORT),
admin: {
url: 'admin', // 可以修改默认的后台路径,但是不能设置为/ root路径,关注https://github.com/strapi/strapi/issues/9302
auth: {
secret: env('ADMIN_JWT_SECRET', process.env.ADMIN_JWT_SECRET),
},
},
});

// config/api.js, API相关配置
module.exports = ({ env }) => ({
responses: {
privateAttributes: ['_v', 'id', 'created_at'],
},
rest: {
defaultLimit: 100,
maxLimit: 250,
},
});

// config/plugins.js, 插件相关配置
module.exports = {
graphql: { // graphql插件相关配置
endpoint: '/graphql',
shadowCRUD: true,
playgroundAlways: false,
depthLimit: 7,
amountLimit: 100,
apolloServer: {
tracing: false,
}
}
}

// extensions/users-permissions/config/security.json, 修改jwt token的配置
{
"jwt": {
"expiresIn": "3650d"
}
}

// .env, 有几个默认的Strapi相关的配置,例如是否开启更新提示等,默认都是关闭了的

自定义Role及权限

  • 免费版不能在web端直接进行配置,不过可以修改数据库,看一下数据库的表结构就能很好修改了,但问题是每次修改了types结构以后需要重新分配role的权限,否则权限会丢失,可以使用以下代码在重新启动应用时自动更新权限,但还是有个问题,如果应用不完全重启,仍然不会更新,因为应用没有完全重启的话admin的也不会更新的:

    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
    // config/functions/bootstrap.js
    "use strict";

    const lodash = require('lodash');

    // Prevent permissions loss
    const setDefaultRolePermissions = async () => {
    const adminRole = await strapi.query("role", "admin").findOne({id: 1});
    const customRoles = await strapi.query("role", "admin").find({id_gt: 3});

    customRoles.forEach(customRole => {
    console.log(`Check ${customRole.name} permissions`);
    customRole.permissions.forEach(permission => {
    // compare permission fields with adminRole
    const realPermission = adminRole.permissions.find(p => p.action === permission.action && p.subject === permission.subject)
    if (permission.properties.fields && permission.action.includes('plugins::content-manager.explorer') &&(permission.properties.fields.length !== realPermission.properties.fields.length ||
    permission.properties.fields.length !== lodash.uniq(permission.properties.fields.concat( realPermission.properties.fields)).length)) {
    strapi.query("permission", "admin").update({id: permission.id}, {properties: realPermission.properties})
    }
    })
    })
    };


    module.exports = async () => {
    setTimeout(setDefaultRolePermissions, 3000); // 使用timeout是因为刚重启的时候数据库还没根据新的结构更新
    };

升级步骤

升级非常简单,直接在composer.json里面全局替换,例如将3.5.2全部替换成3.6.1,在执行以下命令即可

1
2
npm install
rm -rf .cache && rm -rf build/

修改admin系统文件

  • 只需要参照node_modules/strapi-admin/中的目录结构,在根目录新建admin文件夹与其保持一致,任何想要覆盖的都可以放在这里
  • 替换logo,可以在admin/src/assets/images中放置

其他特性

阅读全文 »

  • 代码风格交给eslint,其他的不要进行强制规定

在实际项目中,最好配合以下几个工具,让整个项目的代码风格统一

  • eslint:代码格式检查工具
  • lint-staged:对git的暂存文件进行lint检查
  • husky:git钩子,能够很方便地在项目中配置git的hook操作,通过它我们能够实现在代码提交时检查并尝试修复一些代码风格问题

安装与初始化

  1. 直接这样一起安装几个工具: npm install --save-dev husky lint-staged eslint
  2. 可以执行./node_modules/.bin/eslint --init对当前目录的项目进行eslint初始化,能够通过交互式的命令进行配置,完成后会在当前目录创建配置文件.eslintrc.js
阅读全文 »

  • ReactSSR框架
  • 渲染有三种方式: BSR(客户端渲染,Browser Side Render),SSG(静态页面生成,Static Site Generation),SSR(服务端渲染,Server Side Render)
  • 需要注意的是如果页面中有用js控制的部分(例如条件渲染),在SSR的时候不会直接渲染成DOM元素,虽然也能导出成静态HTML,但是仍然是前端js来控制的

基础配置

1
2
3
4
5
6
7
8
9
10
11
npx create-next-app@latest	# 初始化项目
npx create-next-app@latest --typescript # 使用typescript初始化

# 如果要使用tailwindcss,不推荐tailwindcss官方的脚手架,-e with-tailwindcss安装完成后typescript和eslint都没有,还是自己集成吧,也挺简单的。
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
# 根据文档https://tailwindcss.com/docs/guides/nextjs,替换tailwindcss中的purge和globals.css

npm install sass # 增加对sass、scss的支持

next -p 3001 # 指定启动端口

next.config.js

  • 每次修改必须重启应用
  • .env中设置的环境变量默认不会在后端渲染中被前端看到,但是如果以NEXT_PUBLIC_开头的环境变量,是能看到也能被前端直接使用的
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
module.exports = {
env: { // 设置环境变量,设置后可以在jsx中直接用{process.env.customKey}获取值,环境变量还能设置在.env中
customKey: 'my-value',
},
images: {
domains: ['example.com'] // 定义哪些image允许展示
loader: 'imgix', // 如果图片存储在第三方,需要加这个设置
path: '', // 如果图片存储在第三方且使用相对路径需要添加域名在这里,但是如果是绝对路径,留空字符串即可
},
async redirects() { // 设置重定向规则
return [{
source: '/home',
destination: '/',
permanent: true // true表示永久重定向302,false表示暂时的,301
}]
},
async rewrites() { // 设置rewrites规则,将原来的路径进行重写以此来屏蔽实际的路径,浏览器url不会变化,但是该规则不适用于导出为纯静态的站点,如果是纯静态站点可能需要nginx等来配合
return [{
source: '/',
source: '/old-blog/:slug', // 也可以匹配参数
source: '/blog/:slug*', // 也可以模糊匹配
source: '/post/:slug(\\d{1,})', // 正则匹配
destination: '/signin',
permanent: true, // 重定向是否是permanent
has: [{
type: 'header', // 可以匹配header中是否有某个key
key: 'x-redirect-me'
}, {
type: 'query', // 可以匹配query参数
key: 'page',
value: 'home'
}, {
type: 'cookie', // 匹配cookie
key: 'authorized',
value: 'true'
}, {
type: 'header',
key: 'x-authorized',
value: '(?<authorized>yes|true)', // 可以提取value作为destination的值destination: '/home?authorized=:authorized',
}]
}]
}
}

路由

路由定义

  • 路由定义默认是根据pages文件夹下的文件名来的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pages/blog/first-post.js → /blog/first-post
pages/dashboard/settings/username.js → /dashboard/settings/username

pages/blog/[slug].js → /blog/:slug (/blog/hello-world)
pages/[username]/settings.js → /:username/settings (/foo/settings)
pages/post/[...all].js → /post/* (/post/2020/id/title)

// 可以通过context获取路由参数
export async function getServerSideProps (context: NextPageContext): Promise<any> {
console.log(context.query.all) // all得到的是一个数组,按每一级来
return {
props: {}
}
}

路由常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useRouter } from 'next/router';

const router = useRouter() // 等同于window.location
router.query.search // 获取参数
router.push('/signin'); // 路由跳转
router.replace('/signin'); // 路由跳转
router.push({pathname: '/post/[pid]', query: {pid: post.id}}) // 指定参数
router.pathname; // 获取当前的pathname,例如/signin
router.back(); // 返回上一页,即window.history.back()
router.reload(); // 刷新页面,即window.location.reload()
router.locale // 当前的locale
router.locales // 所有的locales
router.defaultLocale // 默认的locale

路由事件

包括:routeChangeStart、routeChangeComplete、routeChangeError、beforeHistoryChange、hashChangeStart、hashChangeComplete

页面组件

  • 通过变量渲染html,需要用<div dangerouslySetInnerHTML={{__html = '
    '}}

Layouts

  • 全局定义页面的layout

  • 所有页面都相同的layout可以这样做

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // pages/_app.js
    import Layout from '../components/layout'

    export default function MyApp({ Component, pageProps }) {
    return (
    <Layout>
    <Component {...pageProps} />
    </Layout>
    )
    }
  • 如果不是所有页面的layout都相同,可以参考官方文档

  • 可以在Head里面插入全局的js,例如google analytics代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <Head>
    <script
    dangerouslySetInnerHTML={{
    __html: `[google analytics tracking code here]`
    }}
    />
    <link
    href="https://fonts.googleapis.com/css2?family=Inter&display=optional"
    rel="stylesheet"
    /> // 字体优化,能够在编译阶段就优化字体,这样在打开页面不会因为要获取字体文件而闪一下
    </Head>

Image

  • 需要在next.config.js中配置图片域名images.domains
  • 可以设置width、height、quality、priority、responsive自动修改图片显示大小
  • 但是毕竟是后端js程序在进行转换,不如直接使用cloudinary这样的服务速度快功能多
  • 如果图片存储在第三方需要添加配置images: loader: 'imgix'
  • width和height必填,除非layout=fill
1
2
3
4
5
6
7
8
9
import Image from 'next/image'

<Image
loader={myLoader}
src="me.png" // 要么是本地静态文件,要么用绝对路径,不能用//开头的路径
alt="Picture of the author"
width={500}
height={500}
/>

使用svg作为component

  • 和其他框架一样,都是用npm install @svgr/webpack --save-dev
  • 需要做如下配置:
1
2
3
4
5
6
7
8
9
10
11
// next.config.js 添加如下配置
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"]
});

return config;
}
};
1
2
3
4
5
6
7
<Link href=''>
<a>title</a>
</Link>

<Link href=''>
<a><Image ... /></a> // 如果link下需要包含image,不能直接image,外面得加一层a标签
</Link>

后端渲染SSR

  • 在组件加载前就从接口获取数据,才能实现后端渲染,而不是前端去调用API
  • 需要注意的是通过服务端获取的props,必须直接传递到html中去,不要用useEffect等去传递给另外一个变量,那样就不会直接渲染到HTML中去了,浏览网页源代码发现他们只是在一个变量上,对SEO十分不友好
  • getServerSidePropsgetInitialProps都无法用在404页面上,如果是404页面只能在componentDidMount或者useEffect(() => {}, [])里面去请求获取数据了,官方说明
  • 判断当前是否是后端渲染有一个简单的办法,那就是typeof window === undefined
阅读全文 »