网页静态化和网页动态化
A static site seems like a good fit for a small and steady project, right? Like one that does not require any advanced features or interaction with users. But how can you leverage the performance benefits and still have your static site be dynamic, personalized and interactive?
静态站点似乎很适合进行小型且稳定的项目,对吗? 就像不需要任何高级功能或与用户交互的那种。 但是,您如何才能利用性能优势,并使静态站点保持动态,个性化和交互性呢?
Whenever I mention a "static site" to devs that haven't yet worked with static site generators, they frown upon them. The buzz word is working against me and doesn't really describe what you are getting if you decide to use a static site generator (like Gatsby or Gridsome).
每当我向尚未使用静态网站生成器的开发人员提及“静态网站”时,他们都会皱眉。 这个流行语对我不利,并且如果您决定使用静态网站生成器(如Gatsby或Gridsome),并不能真正描述您所获得的收益。
So, I explain to them how it all works, including automatic rebuilds when content or implementation changes. They always have the same comment:
因此,我向他们解释了它们是如何工作的,包括内容或实现发生更改时的自动重建。 他们总是有相同的评论:
"It's nice and all, but pre-generating the site won't work for dynamic scenarios like e-commerce or personalization. Thus it's good only for small projects."
“一切都很好,但是预先生成网站不适用于电子商务或个性化等动态场景。因此,它仅对小型项目有用。”
And that just isn't true. I will show you why.
事实并非如此。 我会告诉你为什么。
There are two ways of making a static site dynamic:
有两种方法可以使静态站点动态化:
- during site pre-rendering 在网站预渲染期间
- through user interactions on the site 通过网站上的用户互动
I should explain this using a real website. Lately, I've been facing the task of creating a wedding site. I know, there are thousands of simple templates for that. But being employed in IT, people implicitly expect the site to be state of the art. So I caved. I'll show 'em.
我应该使用一个真实的网站对此进行解释。 最近,我一直面临着创建婚礼网站的任务。 我知道,有成千上万个简单的模板。 但是,由于受雇于IT人员,人们暗中希望该站点成为最新技术。 所以我陷了。 我给他们看。
For the implementation part I decided to use Gridsome static site generator as I prefer Vue.js to React. I'll be using a headless CMS to store the content and two serverless functions to handle the user interaction.
对于实现部分,我决定使用Gridsome静态站点生成器,因为我更喜欢Vue.js而不是React。 我将使用无头CMS来存储内容,并使用两个无服务器功能来处理用户交互。
Prefer video? Watch the Twitch series on YouTube. And make sure to follow me on Twitch to catch all upcoming streams.
喜欢影片吗? 在YouTube上 观看 Twitch系列 。 并确保 在Twitch 上 关注我, 以吸引所有即将到来的视频。
网站预渲染期间的动态内容 (Dynamic content during site pre-rendering)
I put together all the information that I know before I build the website. I know who I want to invite. I know when the event takes place and I know who I am marrying. Just like you know what products you want to sell or what services you want to offer on your site.
在建立网站之前,我会汇总所有我知道的信息。 我知道我想邀请谁。 我知道事件发生的时间,也知道我要嫁给谁。 就像您知道要销售的产品或要在网站上提供的服务一样。
With that in mind, I created a bunch of content models for my site:
考虑到这一点,我为我的网站创建了一堆内容模型:
- Invitee 受邀者
- Accommodation 住所
- Section 部分
- Timeline item 时间轴项目
And this is how they look in the actual site design:
这就是他们在实际网站设计中的外观:
Because I know all the invitees, I used the content from the headless CMS to (automatically) generate a separate page for each invitee (check out the Custom URL label on the picture). As a result, at the build time, the components know the context of the invitee. Imagine the personalization possibilities – I can even return a 404 for some of my least favorite relatives.
因为我了解所有被邀请者,所以我使用了无头CMS的内容来(自动)为每个被邀请者生成一个单独的页面(请查看图片上的“自定义URL”标签)。 结果,在构建时,组件知道被邀请者的上下文。 想象一下个性化的可能性-我什至可以为我最不喜欢的一些亲人返回404。
I actually used it to display personalized salutations and only relevant timeline items.
我实际上用它来显示个性化的称呼和仅相关的时间表项。
If you were building an e-commerce site, you could implement a product page that displays a list of similar products. You would also probably link to those product-relevant services your company offers. You know all the necessary details at build time.
如果您要构建电子商务站点,则可以实施一个产品页面,其中显示了类似产品的列表。 您可能还会链接到公司提供的那些与产品相关的服务。 您在构建时就知道所有必要的细节。
内容建模是预建网站的关键 (Content modeling is the key for pre-build sites)
I identified three content models for my site, but typically it's much more than that. A good way of approaching content modeling is to take a look at the wireframes for your future site. It's not just about how to put the data into the CMS, you need to think about:
我为我的网站确定了三种内容模型,但通常不止于此。 进行内容建模的一种好方法是查看您将来站点的线框。 这不仅涉及如何将数据放入CMS,还需要考虑:
How is the content going to be displayed and consumed?
内容将如何显示和使用?
Take products and categories for example. In most cases, you will find them being in an N:N relationship, but I am aiming at the implementation side of things here. Think about how complicated the queries for data will be. Adjusting the content models to better represent the actual site structure may help a lot with implementation.
以产品和类别为例。 在大多数情况下,您会发现它们之间存在N:N关系,但我的目标是在实现方面。 考虑一下数据查询的复杂程度。 调整内容模型以更好地表示实际的网站结构可能对实施有很大帮助。
In my example, the timeline items are linked from invitees (1:N) which allows for simple implementation while content management is still straightforward. Like reorganizing the order of the items.
在我的示例中,时间轴项是从被邀请者(1:N)链接的,这使得内容管理仍然很简单时仍可以实现简单。 就像重新整理物品的顺序一样。
How is the content related to other content items?
内容与其他内容项有何关系?
What is the relationship between products, packs of products, categories, special offers or discounts? The answers to these questions will help you choose the right tool for connecting content items like Taxonomy or Linked items.
产品,产品包装,类别,特惠或折扣之间是什么关系? 这些问题的答案将帮助您选择正确的工具来连接诸如分类法或链接项目之类的内容项目。
How is the content going to be created?
内容将如何创建?
Will editors understand the structure of the content you have in place? Also, in most cases, they don't get access to whole projects, but only to parts that are relevant for them. Does your structure allow for a sufficient level of permissions granularity? Are your content models restrictive enough to avoid missing content issues on the live site?
编辑者会理解您所拥有内容的结构吗? 而且,在大多数情况下,他们无法访问整个项目,而只能访问与其相关的部分。 您的结构是否允许足够级别的权限粒度? 您的内容模型是否具有足够的限制性,可以避免在实时站点上丢失内容问题?
There is much more to content modeling. If you're interested, take a look at this great series on content modeling written by Michael Kinkaid.
内容建模还有更多内容。 如果您有兴趣,请看Michael Kinkaid撰写的有关内容建模的系列文章 。
动态组件 (Dynamic components)
So with the right content models, we can generate the static site. Well, pre-generated is probably a better label for it. Its content is not old and static - every content change will effectively rebuild the site again.
因此,使用正确的内容模型,我们可以生成静态网站。 好吧,预生成可能是一个更好的标签。 它的内容不是旧的和静态的-每次内容更改都会有效地重新构建站点。
But what if we need to interact with visitors? At times we need to get some input from them or show them different content based on their actions. In those cases, we can use dynamic components. They are pre-initialized with values during the site build, but they can keep interacting with backend systems based on visitors' actions.
但是,如果我们需要与访客互动,该怎么办? 有时我们需要从他们那里得到一些建议,或者根据他们的行动向他们展示不同的内容。 在这些情况下,我们可以使用动态组件。 它们在网站构建过程中已使用值进行了预初始化,但是它们可以根据访问者的行为保持与后端系统的交互。
On my website, I have a form which invitees can use to confirm what type of accommodation they are interested in. Their selection needs to be stored back in the same Invitee content item I created originally in the headless CMS.
在我的网站上,我有一个表格,被邀请人可以用来确认他们感兴趣的住宿类型。他们的选择需要存储在我最初在无头CMS中创建的同一被邀请人内容项中。
I could communicate with the CMS directly from the component on the site. However, we're talking client-side JavaScript here. Exposing the key would be a major security issue even though I don't expect any of my invitees to understand what a security key is or how it can be misused. So, the middle man between the static site and the CMS is a serverless function.
我可以直接从网站上的组件与CMS通信。 但是,我们在这里谈论客户端JavaScript。 公开密钥是一个主要的安全问题,即使我不希望任何受邀者了解什么是安全密钥或如何滥用它。 因此,静态站点和CMS之间的中间人是无服务器功能。
静态站点上的React组件 (Reactive Component on a Static Site)
Let's start with the component. I used Vue.js and Gridsome as the SSG, but the dynamic component concept is the same regardless of the used framework. The headless CMS I've used here is Kontent. It has a generous free tier, but if you like open-source (to quote my operating-systems uni professor "I don't trust it unless I see its code") I heard Strapi is a good choice.
让我们从组件开始。 我使用Vue.js和Gridsome作为SSG,但是无论使用什么框架,动态组件的概念都是相同的。 我在这里使用的无头CMS是Kontent 。 它有一个免费的免费层,但是如果您喜欢开源(引用我的操作系统的单教授“除非看到代码,否则我不信任它”),我听说Strapi是一个不错的选择。
组件实施 (Component Implementation)
At build time, the component will receive initial data - the data we know at that specific point in time. If Michael selected one of the options last week and we are rebuilding the site today, we know his selection.
在构建时,组件将接收初始数据-我们在该特定时间点知道的数据。 如果Michael上周选择了其中一个选项,而我们今天正在重建站点,那么我们知道他的选择。
<RsvpAccommodation inviteeId="{GUID}" optionSelected="sleep_in_a_tent" howManyInvited="2" salutation="Michael" />
On the other hand, if he has yet to interact with the site, the selection would be empty.
另一方面,如果他尚未与该站点进行交互,则选择将为空。
<RsvpAccommodation inviteeId="{GUID}" optionSelected="" howManyInvited="2" salutation="Michael" />
The component looks like this:
该组件如下所示:
<template>
...
<input type="radio" name="option" value="not_interested" id="none" v-model="option" />
<label for="none">Děkuji, nepotřebuji</label>
<input type="radio" name="option" value="interested_in_booking_a_room" id="hotel" v-model="option" />
<label for="hotel">Mám zájem o ubytování v okolí</label>
<input type="radio" name="option" value="sleep_in_a_tent" id="tent" v-model="option" /><label for="tent">Mám zájem o přespání ve vlastním stanu</label>
...
</template>
<script>
export default {
props: {
salutation: String,
inviteeId: String,
howManyInvited: Number,
salutation: String,
optionSelected: String
},
data: function(){
option: this.optionSelected
},
...
</script>
Vue.js is watching over the used data properties. When Michael changes his selection, the data change event is fired. Note that the name of the property in the watch object must match the name of the data property.
Vue.js正在监视使用的数据属性。 当Michael更改选择时,将触发数据更改事件。 请注意,监视对象中的属性名称必须与数据属性的名称匹配。
At that point we need to store his selection - we form the data and make an async request to the serverless function - all using client-side JS.
到那时,我们需要存储他的选择-我们形成数据并向无服务器功能发出异步请求-所有这些都使用客户端JS。
...
<script>
export default {
...
watch: {
option: function(newVal, oldVal) {
let url = `{remote base URL}/action?id=${this.inviteeId}`;
fetch(url, {
method: 'POST',
body: JSON.stringify({
option: this.option,
})
})
.then(response => {
if (response.status !== 200) {
alert("Unable to save, please try again.");
}
});
}
}
}
</script>
无服务器功能实现 (Serverless Function implementation)
I used Netlify to build and deploy the serverless function. If this is your first Netlify function, feel free to take a look at my introduction video where I show how to set up the local Netlify development environment.
我使用Netlify来构建和部署无服务器功能。 如果这是您的第一个Netlify功能,请随时观看我的介绍视频 ,其中演示了如何设置本地Netlify开发环境。
The headless CMS has two APIs. One for data delivery - I used that one to get all data during site build - and another one for data management. In the serverless function, I need to use both APIs so I added the project ID and management API key to .env file in the root of the Netlify functions project:
无头CMS具有两个API。 一个用于数据传递-我用一个用于在站点构建期间获取所有数据-另一个用于数据管理。 在无服务器功能中,我需要使用两个API,因此我将项目ID和管理API密钥添加到了Netlify函数项目根目录中的.env文件中:
KONTENT_PROJECT_ID={project ID}
KONTENT_CM_KEY={management API key}
And it's always nicer to use an SDK than to struggle with bare REST API calls:
而且,使用SDK总是比处理裸露的REST API更好:
npm i @dotenv --save
npm i @kentico/kontent-delivery --save
npm i @kentico/kontent-management --save
The beginning of the function looks like this:
函数的开始看起来像这样:
require("dotenv").config();
const KontentDelivery = require('@kentico/kontent-delivery')
const KontentManagement = require('@kentico/kontent-management')
The function is available more or less publicly - it's URL is stored in the client-side JS code in plain text - so we first need to do some elementary checks. Every request to this function needs to contain an ID parameter in the querystring that holds an identifier of an existing invitee. This is the person who filled the form. If the ID is missing or is invalid, we return 404.
该功能或多或少是公开可用的-它的URL以纯文本格式存储在客户端JS代码中-因此我们首先需要进行一些基本检查。 每个对此功能的请求都需要在查询字符串中包含一个ID参数,该参数包含现有被邀请者的标识符。 这是填写表格的人。 如果ID缺失或无效,则返回404。
exports.handler = async (event, context, callback) => {
const { KONTENT_PROJECT_ID, KONTENT_CM_KEY } = process.env;
const deliveryClient = new KontentDelivery.DeliveryClient({ projectId: KONTENT_PROJECT_ID });
let id = event.queryStringParameters.id;
const invitee = await deliveryClient.items()
.type('invitee')
.elementsParameter(['accommodation'])
.equalsFilter('system.id', id)
.toPromise();
if (invitee.items == null || invitee.items.length == 0)
{
return {
statusCode: 404,
body: `Invitee not found`
};
}
The deliveryClient request is limited only to a single element - accommodation
. That is because the information from the form is not stored within the Invitee model but in a linked item of type Accommodation.
deliveryClient请求仅限于单个元素accommodation
。 这是因为来自表单的信息未存储在Invitee模型内,而是存储在Accommodation类型的链接项中。
The content model Accommodation directly corresponds to the form on the website:
内容模型Accommodation直接对应于网站上的表格:
We want to store the data we got from the client-side JS as a new record. Updating the existing content item is also a possibility, but we would lose all history should the invitees change their selection in the future.
我们要存储从客户端JS获得的数据作为新记录。 更新现有内容项也是可能的,但是如果被邀请者将来更改选择,我们将丢失所有历史记录。
First, we note the ID of the existing Accommodation content item and initialize the Content Management client.
首先,我们记下现有Accommodation内容项的ID并初始化Content Management客户端。
let accommodationId = invitee.items[0].accommodation.value[0].system.id;
const client = new KontentManagement.ManagementClient({ projectId: KONTENT_PROJECT_ID, apiKey: KONTENT_CM_KEY });
Then, we need to create a new language variant of the Accommodation content item. Even if there is just one language in the project, content is stored in a separate bucket labeled with that language codename. This ensures a smooth transition should you decide to add additional languages in the future.
然后,我们需要为Accommodation内容项创建新的语言变体。 即使项目中只有一种语言,内容也会存储在标有该语言代号的单独存储桶中。 如果您将来决定添加其他语言,这可以确保平稳过渡。
await client.createNewVersionOfLanguageVariant()
.byItemId(accommodationId)
.byLanguageCodename('default')
.toPromise();
This code does the same thing as if you click on "Create a new version" in the UI.
此代码的作用与您在用户界面中单击“创建新版本”相同。
Next, we need to fill the variant with data. The data are coming as JSON in the request body.
接下来,我们需要用数据填充变量。 数据以JSON形式出现在请求主体中。
let accommodation = JSON.parse(event.body);
await client.upsertLanguageVariant()
.byItemId(accommodationId)
.byLanguageCodename('default')
.withElements([{
element: { codename: 'option' },
value: [{ codename: accommodation.option }]
}])
.toPromise();
The last step is to publish the new variant:
最后一步是发布新的变体:
await client.publishOrScheduleLanguageVariant()
.byItemId(accommodationId)
.byLanguageCodename('default')
.withData()
.toPromise();
return { statusCode: 200, body: `OK` }
Netlify的CORS问题 (CORS issues with Netlify)
Even if you're running the functions and static site locally, you will stumble upon a CORS issue as both implementations are being served from different ports. On all responses from the serverless function, you need to return the "Access-Control-Allow-Origin" header.
即使您在本地运行功能和静态站点,您也会偶然发现CORS问题,因为这两种实现都是从不同的端口提供服务的。 对于来自无服务器功能的所有响应,您需要返回“ Access-Control-Allow-Origin”标头。
Netlify has a simple way of handling this globally through the netlify.toml
configuration file in the root of the functions project:
通过功能项目根目录中的netlify.toml
配置文件,Netlify有一种简单的全局处理方式:
[build]
Functions = "lambda"
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"
页面刷新后的旧数据 (Old data after page refresh)
Now the component reacts to the actions of the visitors. However, the initial state (which will also be displayed if the visitor refreshes the page) still comes from the static site build. If a visitor changes his or her selection, the change is saved in the CMS, but the site is not rebuilt.
现在,组件会对访问者的行为做出React。 但是,初始状态(如果访问者刷新页面也将显示该初始状态)仍然来自静态网站构建。 如果访问者更改了他或她的选择,则更改将保存在CMS中,但不会重建站点。
It would not be efficient to rebuild the whole site after every user interaction. Even if we did that, it would take a couple of seconds to minutes until the build and deployment are finished.
每次用户交互后重新构建整个站点都没有效率。 即使我们做到了,构建和部署也要花费几秒钟到几分钟的时间。
Instead, we do an async request when the component is first rendered:
相反,当组件首次呈现时,我们会发出一个异步请求:
<script>
...
mounted: function () {
let url = `${baseUrl}/delivery?id=${this.inviteeId}`;
let response = fetch(`{remote base URL}/delivery?id=${this.inviteeId}`, { method: 'GET', mode: 'cors' })
.then(response => response.json())
.then(accommodationObj => {
this.option = accommodationObj.option;
});
},
...
</script>
The component will be pre-initialized with data during the site pre-build. But once the component is created, it will get fresh data from the CMS using another serverless function. That function is very similar to the previous one:
在站点预构建期间,将使用数据对组件进行预初始化。 但是,一旦创建了组件,它将使用另一个无服务器功能从CMS获取新数据。 该功能与上一个功能非常相似:
exports.handler = async (event, context, callback) => {
const { KONTENT_PROJECT_ID } = process.env;
let id = event.queryStringParameters.id;
const deliveryClient = new KontentDelivery.DeliveryClient({ projectId: KONTENT_PROJECT_ID });
const invitee = await deliveryClient.items()
.queryConfig({ waitForLoadingNewContent: true })
.type('invitee')
.elementsParameter(['accommodation', 'option'])
.equalsFilter('system.id', id)
.toPromise();
if (invitee.items == null || invitee.items[0] == null)
{
return {
statusCode: 404,
body: `Invitee not found`
};
}
return {
statusCode: 200,
body: JSON.stringify({
option: invitee.items[0].accommodation.value[0].codename
})
};
};
In this case, we need to add additional configuration to the data query - waitForLoadingNewContent
. The content coming from headless CMS is cached and delivered via CDN so we may be getting outdated content if it was changed in the last few minutes. The configuration option ensures the response will always contain fresh data.
在这种情况下,我们需要向数据查询添加其他配置waitForLoadingNewContent
。 来自无头CMS的内容将通过CDN进行缓存和传递,因此如果在最近几分钟内更改了内容,我们可能会收到过时的内容。 配置选项可确保响应将始终包含新数据。
So, the overall process of a dynamic component on a static site looks like this:
因此,静态站点上动态组件的总体过程如下所示:
快速互动 (It's Fast and Interactive)
You see, the great benefit static sites bring is that all information available at build time can be served as static files, which is fast and easily scalable using a CDN.
您会看到,静态站点带来的巨大好处是可以将构建时可用的所有信息作为静态文件提供,这些文件可以使用CDN轻松快速地进行扩展。
But they can also provide dynamic functionality that can be delivered via serverless functions - also cheap and easily scalable.
但是它们还可以提供可以通过无服务器功能交付的动态功能-价格便宜且易于扩展。
If you take my website as an example - instead of having to deploy the whole application to the cloud, I only needed to host a bunch of small static files and two tiny serverless functions. And I'm also able to scale these functions independently.
如果以我的网站为例-无需将整个应用程序部署到云中,我只需要托管一堆小的静态文件和两个小的无服务器功能。 而且我也能够独立扩展这些功能。
Static sites may not be the ultimate choice for web development, but for many projects bring clarity, performance and security benefits as well as reduced maintenance costs. What is your experience? Let me know.
静态站点可能不是Web开发的最终选择,但是对于许多项目而言,它们带来了清晰度,性能和安全性优势,并降低了维护成本。 你有什么经验? 让我知道。
Make sure to also check out my Twitch streams and YouTube channel about Web Development.
确保还查看我的Twitch流和有关Web开发的YouTube频道 。
翻译自: https://www.freecodecamp.org/news/how-to-make-static-site-dynamic/
网页静态化和网页动态化