2分钟搞定React服务端渲染

什么是服务端渲染这里就再解释了,网上已经有很多详细的介绍了。

为什么需要服务端渲染

这里再说下为什么需要服务端渲染,最主要解决的问题就是解决 SEO 问题了,因为 React Vue 基于这些框架写出来的项目数据都是浏览器端动态调用后端接口获取的,包括页面的元素结构什么的都是放在 Js 文件里的。当爬虫来访问时只拿到了 <div id="app"></div> 这样一个空的 div 里边什么内容也没,十分不利于 SEO 。

有了上述的 SEO 问题,那么解决问题思路就很简单了,就是如何能让搜索引擎的爬虫爬到页面是包含了完整内容的。

如何实现

实现的方法也很多,大都是要启动个单独的服务来处理这些爬虫的请求。这里使用的是一个开源的解决方案 prerender 使用非常简单,需要有个 nodejs 运行环境,以下上官方的使用文档。

  1. 安装 pretender

    1
    npm install prerender
  2. 创建文件 server.js

    1
    2
    3
    const prerender = require('prerender');
    const server = prerender();
    server.start();
  3. 启动并测试

    1
    2
    3
    node server.js

    curl http://localhost:3000/render?url=https://www.example.com/

其原理就是启动一个 headlesschrome 浏览器,在渲染完成后把结果在返回给客户端。

如何部署

到服务器上部署的时候会有个麻烦的问题,服务器一般都是 Liunx 系统安装 chrome 比较麻烦,那么就可以通过 Docker 来完美解决这个问题了,我已经构建了这样一个镜像,如果要使用可以通过以下步骤使用。

  1. 获取镜像

    1
    docker pull registry.aliyuncs.com/leiem/prerender
  2. 启动容器

    1
    docker run -d --restart=always -p 3000:3000 registry.aliyuncs.com/leiem/prerender
  3. 配置Nginx规则

    1
    2
    3
    4
    5
    6
    7
    8
    location / {
    try_files $uri /index.html;

    if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider") {
    rewrite .* /render?url=$scheme://$host$request_uri break;
    proxy_pass http://127.0.0.1:3000;
    }
    }

    上述规则会匹配常见的搜索引擎的 User-Agent,让这些请求通过我们搭建的服务去处理。

自己构建镜像

可以直接使用我制作好的镜像 registry.aliyuncs.com/leiem/prerender ,如果要自己构建镜像可参考以下文档。

  • Dockerfile

    1
    2
    3
    4
    5
    6
    7
    8
    FROM browserless/chrome
    USER root
    WORKDIR /usr/src/prerender
    COPY server.js package.json ./
    RUN npm install
    USER blessuser
    EXPOSE 3000
    CMD [ "npm", "run", "start" ]
  • server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const prerender = require('prerender')

    const server = prerender({
    followRedirects: true,
    chromeLocation: '/usr/bin/google-chrome',
    chromeFlags: [ '--no-sandbox', '--headless', '--disable-gpu', '--remote-debugging-port=9222', '--hide-scrollbars' ],
    })

    server.use(prerender.blockResources())
    server.use(prerender.removeScriptTags())
    // server.use(require('prerender-memory-cache'))
    server.start()
  • package.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "name": "leiem.cn",
    "version": "1.0.0",
    "license": "MIT",
    "dependencies": {
    "prerender": "5.19.0",
    "prerender-memory-cache": "1.0.2"
    },
    "scripts": {
    "start": "node server.js"
    }
    }

Antd动态控制表单项

再来赞一波 antd 简直不要太好用 ♥️

Antd 的 Form 组件出场率还是非常高的,除了常规的表单各项填写并提交外,有些时候需要根据用户的操作对表单项做一些控制,废话不多说直接拿个例子说

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
import React from 'react';
import { Form, Select, Input } from 'antd';


export default function HomeIndex() {
const [form] = Form.useForm()
return (
<Form form={form}>
<Form.Item name="is_love" label="是否喜欢">
<Select placeholder="请选择">
<Select.Option value="1"></Select.Option>
<Select.Option value="0"></Select.Option>
</Select>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) =>
getFieldValue('is_love') === '0' ? (
<Form.Item name="reason" label="原因">
<Input placeholder="请输入"/>
</Form.Item>
) : null
}
</Form.Item>
</Form>
)
}

以上例子会根据用户选择 是否喜欢 项,如果选择 则会出现新的表单项。可以看到这里主要使用了 shouldUpdate 属性,默认为 false 。当其值为 true 时则整个表单任何改动都会触发改项的重新渲染,其值也可以为一个函数,例如 shouldUpdate={(p, c) => p.is_love != c.is_love} 则意味着只有 is_love 的值发生变化时才会触发重新渲染。

去除antd蓝色边框

先来赞一波 antd 简直不要太好用 ♥️

某些特殊情况下我们会隐藏例如 InputSelect 等组件的边框,边框很好隐藏,找到对应的 class 覆盖样式即可,但隐藏了边框会发现在获取焦点的时候还是会有一个淡蓝色的边框就像这样。

image-20220207213220054

下边分别介绍几个组件的边框隐藏方法。

Input 组件

通过观察可以发现有个 focus 的伪类中的 box-shadow 属性的效果

image-20220207213737163

知道了原因解决方法也很简单

1
2
3
:global(.ant-input):focus {
box-shadow: none;
}

这里我使用了 CSS Modules 所以外层加了 :global ,大家可根据情况处理。

Select / Cascader / AutoComplete 组件

这几个组件表现形式相似,同样也是通过 box-shadow 属性实现的

image-20220207214702679

直接覆盖即可

1
2
3
:global(.ant-select-selector) {
box-shadow: none !important;
}

InputNumber 组件

1
2
3
:global(.ant-input-number-focused) {
box-shadow: none !important;
}

DatePicker 组件

1
2
3
:global(.ant-picker-focused) {
box-shadow: none !important;
}

Django ORM ManyToManyField

Django 官网对 ManyToManyField 操作的例子写的比较少,正好用到了就来总结下,先来定义下 Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.db import model

class Product(models.Model, ModelMixin): # 产品表
name = models.CharField(max_length=64)

class Meta:
db_table = 'products'

class Company(models.Model, ModelMixin): # 公司表
products = models.ManyToManyField(Product)
name = models.CharField(max_length=64)

class Meta:
db_table = 'companies'

如果执行 manage.py migrate 后会自动多生成一个 companies_products 的关系表。

关系维护

  • 创建关联关系

    1
    2
    3
    4
    5
    6
    7
    company = Company.objects.get(pk=1)
    company.products.add(1)
    company.products.add(2, 3)
    # 反向操作
    product = Product.objects.get(pk=1)
    product.company_set.add(1)
    product.company_set.add(2, 3)
  • 设置关联关系

    1
    2
    company.products.set([2, 3])
    product.company_set.set([1, 3])
  • 关联创建

    1
    2
    company = Company.objects.get(pk=1)
    company.products.create(name='Model 3')
  • 移除关联关系

    1
    2
    company.products.remove(1, 3)
    product.company_set.remove(2)
  • 清除关联关系

    1
    2
    company.products.clear()
    product.company_set.clear()

关联查询

  • 查询某公司关联的所有产品

    1
    2
    company = Company.objects.get(pk=1)
    company.products.all()

    等价于

    1
    Product.objects.filter(company__id=1)
  • 查询拥有某产品的所有公司

    1
    2
    product = Product.objects.get(pk=1)
    product.company_set.all()

    等价于

    1
    Company.objects.filter(products__id=1)

获取关系表对象

如上例子可以通过 Company.products.through 获取关系表对象,例如

1
2
3
4
5
6
rel = Company.products.through.objects.first()
rel.product_id
rel.company_id
# out
2
1

Typora图片自动上传阿里云OSS

Typora 是非常好用的所见即所得的 Markdown 编辑器,关于图片上传支持自定义写脚本来上传,这就给予我们很大的灵活性,我们就自己动手写个脚本来把图片自动上传到阿里云的OSS里。

创建上传脚本

以下既是脚本内容,要根据自己的 OSS 配置稍作更改,这里使用了阿里云官方的 OSS命令行工具

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
#!/bin/bash
# upload blog image to oss

# 指定ossutil的命令路径
ossutil='/Users/aka/Software/ossutil/ossutilmac64'
# 要上传的buket名称
buket='fizee'
# OSS对应的endpoint
endpoint='https://oss-cn-zhangjiakou.aliyuncs.com'
# 用于访问域名,例如cdn加速域名
access_endpoint='https://cdn.qbangmang.com'
# access key
access_key_id='LTAIDOvVPccSEKRi'
# access secret
access_key_secret='awU0S8XdJPyHMZfKF3wCTGq86eJkQh'
# 上传到OSS的文件名,这里使用上传提供的文件名
file=$(basename "$1")
# 上传到OSS的路径,这里使用年月分割,例如: blog/2022/01
path=blog/$(date +%Y/%m)

cd $(dirname $ossutil)
$ossutil -f -i $access_key_id -k $access_key_secret -e $endpoint cp "$1" oss://$buket/$path/$file > /dev/null


echo $access_endpoint/blog/$path/$file

配置Typora

直接上截图了,插入图片时将会自动上传到阿里云OSS

image-20220125133209253

禁用mac输入首字母大写

Mac 系统自带的输入法默认会在你输入英文时自动提示首字母大写,不管里这时候敲回车还是空格,都会自动转成建议的首字母大写,就像这样

image-20220121145328931

正常编辑文章估计感觉这个特性还蛮好的,但像我们这写代码文档什么的就感觉非常不方便太耽误事儿了,关闭方法也很简单直接上截图了

image-20220121145439004

小程序引用网络字体在安卓无效

在微信小程序中引用网络字体有两种写法。

在css文件中引用

1
2
3
4
@font-face {
font-family: 'Pacifico';
src: url('https://sungd.github.io/Pacifico.ttf');
}

使用 wx.loadFontFace 加载字体

1
2
3
4
wx.loadFontFace({
family: 'Pacifico',
source: 'url("https://sungd.github.io/Pacifico.ttf")',
})

安卓中加载失败的问题

iOS 里两种方式都可以,但在安卓手机上都无法正常加载字体,先看下官网的相关文档是这样描述的:

动态加载网络字体,文件地址需为下载类型。‘2.10.0’起支持全局生效,需在 app.js 中调用。

注意:

  1. 字体文件返回的 contet-type 参考 font,格式不正确时会解析失败。
  2. 字体链接必须是https(ios不支持http)
  3. 字体链接必须是同源下的,或开启了cors支持,小程序的域名是servicewechat.com
  4. 工具里提示 Faild to load font可以忽略
  5. ‘2.10.0’ 以前仅在调用页面生效。

其他几项都可以忽略了,应该都可以满足条件,最主要的问题在第3条,安卓下必须开启跨域支持,以下例子是我使用阿里云的 OSS 的跨域配置(权限管理 / 跨域设置):

image-20220121143840096

经测试完美解决😄

defaultdict嵌套使用

Python标准库collections 里的 defaultdict 在某写情况下非常好用,看如下例子:

1
2
3
4
5
6
from collections import defaultdict
data = defaultdict(int)
data['a'] += 2

print(data)
# defaultdict(int, {'a': 2})

如果 data 是常规的字典类型,可就没法直接用 += 2,那如果我们需要两层嵌套时该怎么用呢?

1
2
3
4
5
6
7
8
data = defaultdict(defaultdict(int))

Traceback (most recent call last):
File "/Users/aka/WorkSpace/venvs/django1.11/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3331, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-25-25b6aac66099>", line 1, in <module>
data = defaultdict(defaultdict(int))
TypeError: first argument must be callable or None

会看到直接就引发了一个异常,提示参数必须时 callableNone,根据提示我们可以做如下改动:

1
2
data = defaultdict(lambda: defaultdict(list))
data['a']['b'].append('aa')

这样就可以正常使用了,这里使用 lambda 构造了一个匿名函数。