惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
博客园_首页
T
Tailwind CSS Blog
美团技术团队
博客园 - 叶小钗
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Y
Y Combinator Blog

BAIYUN'S BLOG

AI Coding 从小工到专家 - BAI YUN 电信 IPTV 内网融合教程:在局域网任意设备观看 IPTV - BAI YUN 美国住宅 IP 提供商体验对比 - BAI YUN Node.js 程序以 Keyless 模式从 1Password 读取密钥 - BAI YUN 高效优雅的观影指南 - BAI YUN 基于 Surge 的 DNS 优化指南 - BAI YUN RIME 鼠须管输入法简明使用教程 - BAI YUN 自动给不同 Git 项目设置不同的用户名邮箱 - BAI YUN Web3 新手如何选择加密货币钱包 - BAI YUN Web3 到底是什么? - BAI YUN 谨慎开启 Safari 的阻止跨站跟踪功能 - BAI YUN iOS Surge 开启后无法使用部分国产 APP 或网络出错的解决办法 - BAI YUN 屏蔽 Telegram 垃圾私信的有效办法 - BAI YUN 每个人都应该读一遍《1984》和《动物庄园》 - BAI YUN iOS 给常见机构号码增加联系人头像 - BAI YUN 在国内如何购买比特币和投资加密货币 - BAI YUN WEB 性能优化:如何让你的网站页面秒开(方法论) - BAI YUN 加密货币投资这件事 - BAI YUN 国内手机号注册推特教程 - BAI YUN 国内注册币安账号并通过 KYC 教程 - BAI YUN 使用一套助记词创建多个以太坊钱包账户 - BAI YUN Apple Watch 的几个用途 - BAI YUN 钱包安全指南:如何保护你的 NFT 和加密资产不被盗 - BAI YUN 我在 macOS 上高频使用的应用 - BAI YUN Web3 时代:如何使用 MetaMask 钱包实现一键登录 - BAI YUN 2022 年了 Safari 无法渲染渐变文字的 BUG 依旧存在 - BAI YUN 境外手机卡:澳门电信大湾区预付卡使用体验 - BAI YUN 解决 iCloud Drive 上传文件卡住的问题 - BAI YUN Electron 应用如何获取系统代理配置 - BAI YUN 解决 iOS 上 Google 账号登录会卡在加载状态的问题 - BAI YUN
从传统 MVC 到同构:Web 发展历程 - BAI YUN
baiyun · 2017-12-22 · via BAIYUN'S BLOG

Web 技术一直在不断发展和演进,从最早的静态页面到如今的复杂单页应用,这其中经历了许多阶段和技术的迭代。本文将从最传统的 MVC 模式一直介绍到目前常用的同构模式,探讨各种技术的优缺点,帮助读者更好地理解 Web 技术的发展历程。无论是 Web 开发初学者,还是有一定经验的开发者,都能从中获得一些启示和思考。

最传统的 MVC 模式

浏览器发出请求,服务器收到后渲染出完整的 HTML 页面返回给浏览器,在这个阶段里大部分业务逻辑都包含在服务端端代码里,如果想要获取新的页面内容,需要重新发送请求,服务器再渲染出完整的页面返回给浏览器,即使实际改变的内容只有一个字也需要服务器重新渲染完整的页面。

这种模式最大的缺点在于客户端无法在不刷新页面的情况下动态更改页面内容,可以很明显看出性能的巨大浪费,以及用户体验的差劲

AJAX

在这个阶段,借助浏览器提供的 XMLHttpRequest 对象,可以实现在不刷新页面的情况下向服务器请求新的数据,然后使用 JS 动态渲染到页面中,但是点击链接的时候还是需要刷新整个页面,无法做到像原生 APP 一样的体验。

SPA (Single page application)

这个阶段和之前的大不相同,浏览器请求页面只会获得一个静态的 HTML 入口文件,例如:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="/assets/js/app.js"></script>
</body>
</html>

页面中的实际内容完全由浏览器里的 JS 渲染出来,客户端会维护自己的路由、控制器和模型,与服务器之间完全通过 API 来交流数据,此时服务端代码不再处理视图层相关逻辑,可以专注于数据。

整个流程通常是这样的:

  1. 浏览器发出请求
  2. 服务端返回 HTML 入口文件
  3. 浏览器下载并执行 HTML 入口文件里包含的 JS 文件
  4. JS 通过 API 向服务器请求初始数据
  5. 服务器返回数据后 JS 会结合数据渲染出实际的内容填充到页面中

这种模式可以在切换页面时避免刷新整个页面,做到类似原生 APP 的用户体验,但是也有两个缺点:

  1. 爬虫只能爬到 HTML 入口文件,SEO 不友好
  2. 首屏呈现时间比较慢,也就是第一次打开时,用户会看到比较久的白屏或者加载动画

对于 SEO 不友好的问题,有一种解决方法是对爬虫返回使用 Headless 浏览器预渲染页面,这种方法需要额外维护预渲染相关的逻辑,投入回报是个问题

对于首屏呈现时间慢的问题,通常是在 HTML 入口页面中返回首屏需要的初始数据,例如:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script>
    window.__INITIAL_STATE__ = { data: '...' }
  </script>
  <script src="/assets/js/app.js"></script>
</body>
</html>

JS 可以直接读取 window.__INITIAL_STATE__ 中的初始数据,这样可以省下通过 Ajax 获取首屏数据的时间,效果比较明显,但是在 JS 代码下载、parsing,执行之前用户还是会看到白屏或者加载动画,而 JS 代码的下载和 parsing 又是比较耗时的,所以在移动网络下这个问题会尤其明显。

同构

所谓同构就是指,JS 代码可以同时运行在服务端和浏览器,凭借虚拟 DOM,现在服务器也可以渲染出 HTML 字符串了,例如 React 或者 Vue 这些使用了虚拟 DOM 的库和框架,它们可以运行的服务端和浏览器,同样的代码在浏览器中渲染出真实 DOM,在服务器中渲染出 HTML 字符串。

如果一个 SPA 使用 React 或者 Vue 构建而成,那么它可以很容易转换为同构代码,在同构代码中通常有以下几个注意点:

隔离环境特定代码

例如在 Node 环境中没有windowdocument 之类的仅存在于浏览器环境的对象,所以在我们写同构代码时要注意尽量避免副作用,例如:

// moduleA.js
const list = document.querySelectorAll('ul li')
export default list

上面的代码就不能称为同构代码,它只能在浏览器环境运行,如果想让它变为同构代码通常有两种办法:

  1. 将副作用封装到函数里
// moduleA.js
const getList = () => document.querySelectorAll('ul li')
export default getList
  1. 只在浏览器运行,例如在 React 中可以在 componentDidMount 中执行
class App extends React.Component {
  state = { list: [] }
  componentDidMount() {
    this.setState({ list: document.querySelectorAll('ul li') })
  }
}

共享状态

在浏览器环境很多状态天然就是单独的,但在服务端它们就是共享状态了,如果我们有一个 API 可以获取用户收藏帖子,如下:

import axios from 'axios'

const getFavoritePosts = () => axios('/api/posts').then((res) => res.data)

export default getPosts

上面的代码在浏览器中使用是没有问题的,浏览器会自动发送用户的 cookie 给服务器,但是在服务器环境中需要将用户的 cookie 手动传给 API 服务器,我们修改代码为如下:

import axios from 'axios'

const getFavoritePosts = (axiosInstance) => {
  const ins = axiosInstance || axios
  return ins('/api/posts').then((res) => res.data)
}

export default getPosts

此时,我们在服务端调用 API 时需要传入 axios 实例

import getFavoritePosts from './api'

class IndexPage extends React.Component {
  static getInitialProps({ axios }) {
    return getFavoritePosts(axios)
  }
  render() {
    return this.props.posts.map((post) => (
      <h3 key={post.id}>
        <a href={post.url}>{post.title}</a>
      </h3>
    ))
  }
}

这种做法固然解决了问题,但是每次使用比较繁琐,更好的办法是创建一个通用的 http 请求函数,由它负责动态适配当前运行环境,同时服务端的每个请求都需要携带当前请求的 cookie 和必要的 header 信息。

function request(requestConfig, req) {
  if (isServer) {
    // 从 req 读取 cookie 和 header
  } else {
    // 浏览器环境
  }
}

class IndexPage extends React.Component {
  static getInitialProps({ req }) {
    return request({ url: '/api/xxx' }, req)
  }
  render() {
    return this.props.posts.map((post) => (
      <h3 key={post.id}>
        <a href={post.url}>{post.title}</a>
      </h3>
    ))
  }
}

本文原载于:baiyun.me

原文链接:https://baiyun.me/web-development-from-mvc-to-isomorphic