分类 精读 下的文章

在这篇文章之后,我会经常发布一些关于框架设计/架构的一些文章,因为这将作为我的读书笔记,我最近在看一些书比如《vue.js设计与实现》和《前端架构入门和微前端》;我简单介绍一下这两本书,希望对你们有所帮助,首先前端架构这本书一直是我的床头书但是目前对我的工作帮助并不大,因为它比较偏理论个人认为,如果你有耐心并且非常愿意入门前端架构,这本书是一个非常不错的入门书籍;其次就是vue.js设计这本书是最近前端圈的网红书,如果你已经使用过了vue3一段时间了,想精通/深入了解vue3,那么这本书将会带你从设计到实现理清楚vue的所有脉络!

前言

我最近在写我人生中的第一款框架,尽管没有任何含金量,而且这种低级的作品居然是出自一个有着3年开发经验的程序员之手,我还蛮不好意思的;在写这款框架我犯了很多错误和技术债,由于前期没有很好的规划功能以及模块,导致走了不少弯路,而且没有设计框架的经验,我经常会把一个功能放到编译时还是运行时而苦恼,同样我会时常考虑用户的习惯,去联想其他后端框架,导致在框架API设计上有点四不像的感觉。无论如何这款框架再丑也是自己生的,相信不久之后就会和大家见面了,所以我这篇文章将结合我设计的sword.js和vue.js给大家好好聊一聊框架中如何权衡某些事情。

什么是权衡

我们在讲比如vue.js这一类框架时,其中的每一个模块并非独立的,而是互相依赖和制约,框架作者需要有着全局的把控才能更好的扣细节做优化,拆分...那么想象一下当我们要设计一款前端视图层框架的时候,我们需要首先考虑范式,它是声明式的还是命令式的呢,再比如说如果在框架中做hmr底层实现,甚至是构建工具,webpack/rollup/esbuild?可见我们要遇到的选择都太多太多了,那么这就是“权衡”的艺术,框架中的每一个地方,或者说我们在平时写业务的时候,我们都需要去考虑更多东西,这就是权衡。

声明式和命令式

我们从原生js开始说起,如果我想要给一个dom绑定一个点击事件(我全部用伪代码写):

const e = document.querySelector("#app");
e.innerText = "foo";
e.addEventListenner('click', () => {
    alert("hello foo")
})

这就是典型的命令式代码,代码的执行方式是可预期的,因为都是由开发人员自己编写的每一步操作,但是这就遇到一个很难的问题了,当程序越来越大,我们有多个dom需要绑定点击事件,就要获取n次dom并且一一绑定,这无疑是一种痛苦。那么声明式呢,它可以解决命令式的一些什么问题呢?

<div @click="() => {}" id="app">
</div>

如果你使用过vue.js,那么你肯定写过n个这样的代码,我们只给click提供了一个函数,我们并不关心vue是如何获取dom并且绑定点击事件的,我们只需要关注结果就可以了,但是不可否定的是,在vue内部的实现中一定是命令式的,而暴露给用户却是声明式的。那么关乎性能它们谁更好,答案当然是可以预想到的,命令式的代码有着不可替代的性能:

e.innerText = 'update text';

在命令式代码中只需要写这一句就可以了,但是如果是声明式代码,我们需要找出新dom和旧dom之间的差异,然后再动态修改text(调用上面这个代码),所以由此得知,尽管声明式代码的性能不如命令式,但是为了更好的维护,我们需要做的就是权衡(既然性能有差距,我们就往可维护性上靠,并且尽可能的优化diff算法,让性能无限接近命令式代码)。

虚拟dom的性能

刚刚我们讨论了声明式和命令式的区别,那么虚拟dom如果你使用了vue.js就一定不陌生,而且它是每个面试官都喜欢问的(我也不知道为什么喜欢问,感觉没啥技术含量)。那么虚拟dom就是为了能够更好的给vue进行diff而出现的,我们要比对如下2行代码:

<div @click="() => {}" id="app">hello foo</div> // old
<div @click="() => {}" id="app">hello bar</div> // new

如何用最小的性能消耗找出它们的差异呢?就是虚拟dom,我们在之前说过声明式和命令式代码天然的差距(虚拟dom更新不会比js dom api性能更好),但是事实上99%场景都很难写出绝对优化的命令式代码,但是声明式代码我们可以很轻松的写出来相对还不错的代码。我们为了了解虚拟dom,需要知道我们上述提到的js dom api是什么,要么是createElemnt或者innerHTML,所以我们就用虚拟dom对比一下这两个api的差异。

innerHTML vs 虚拟dom

innerHTML是我写jquery/jsp时的噩梦,因为在新手时期为了构建一个html字符串,我每天半夜调试屎山项目的html字符串,这个过程非常痛苦:

const html = `
<div>
    <span>innerHTML</span>
</div>
`
dom.innerHTML = html;

js新手小白都知道,dom操作的效率和js层面的计算是不能比较的,差距非常大,为了页面的展示,需要把html字符串转成dom树,然后再执行innerHTML; 反观虚拟dom创建页面需要2步:

  1. 把我们的模板代码转换成js对象
  2. 无限递归对象创建真实dom

这么一看,好像innerHTML更直接,而且html字符串转成dom树是dom层一次性且“高效”的运算,所以说虚拟dom创建页面的性能是不如innerHTML的,但是更新页面,虚拟dom的优势就显示出来了,首先innerHTML不仅会对html字符串进行运算,还会把之前的旧dom销毁,然后创建一个新的dom(恨人啊);虚拟dom只需要创建一个新的js对象再与旧的虚拟dom进行比对,哪里有变化就变更哪里!虽然说虚拟dom多了一个diff的操作,但是终究是js层面的运算是很快速的;当页面越来越大,而innerhtml必定都是全量更新,性能也会随着内容变多,和虚拟dom差距越来越大。

粗略比较三个方式的创建&更新技术

  • 性能:原生JS > 虚拟dom > innerhtml
  • 综合可维护性和性能以及心智负担权衡之下,虚拟dom是一个不错的选择。

运行时和编译时

我们作为框架的作者,希望程序是如何运行的,我们还是用vue.js举例子,刚刚我们讲了虚拟dom,但是却不知道虚拟dom这个js对象是什么样子,我们可以通过这个部分把虚拟dom重新梳理一下:

const obj = {
    tag: "div",
    children: [{
        tag: "p",
        children: "hello bar"
    }]
}

这就是一个虚拟dom对象,描述了每个node的信息以及每个子node的信息,我们如果要实现render方法,就需要对虚拟dom对象进行递归,我们简单实现一下:

const obj = {
  tag: "div",
  children: [
    {
      tag: "p",
      children: "hello bar"
    }
  ]
};

const render = (obj, root) => {
  // 创建一个父节点
  const element = document.createElement(obj.tag);
  if (typeof obj.children === "string") {
    // text节点
    element.appendChild(document.createTextNode(obj.children));
  } else if (obj.children) {
    obj.children.forEach((e) => {
      // 如果有多个子节点,就递归创建
      render(e, element);
    });
  }
  root.appendChild(element);
};

render(obj, document.body);

这样我们就完成了一个在运行时环境可以完美运行的render,用户可以使用render对页面进行创建元素,但是没有用户愿意每天写这种破数据结构的,所以就肯定要用到编译的东西帮助我们把模板语法转换成数据结构,这个时候就是编译时+运行时,所以vue大多数情况也是这样做的,通过vite/vue-cli对单组件文件进行编译。那么同理既然可以有纯运行时,那么就有纯编译时的东西,可以把我们的模板语法编译成命令式的代码,比如这样:

<div @click="() => {}" id="app">hello foo</div> // old

转换成

const e = document.querySelector("#app");
e.innerText = "foo";
e.addEventListenner('click', () => {
    alert("hello foo")
})

没有虚拟dom,没有diff,only compile!! 这也是svelte.js在做的很酷的事情。所以作为框架设计者关于运行时和编译时我们需要有自己的权衡,虽然vue.js是运行时+编译时,但是在编译时会提取内容,看看哪些内容是永远不可变哪些又是可变的,然后这部分会在运行时再次做优化。所以关于运行时和编译时,没有绝对的好也没有绝对的坏,还是看框架定位和作者自己的权衡了(佛系不引战)。

关于sword.js所做的权衡

如果还不清楚sword.js是做什么的,你可以看看以前的文章,简单的就是说一个nodejs框架,框架中自然就是拥有运行时和编译时,一个framework-core,一个cli。在sword.js中有一个蛮好玩的功能就是,ts运行时检测,这个技术的大概的原理就是,分析ts的类型生成一份schema,然后会有一个函数去比对对象和schema是否吻合,如果匹配成功,那么就算校验通过,这个技术用到参数校验特别好,比如这样:

export interface ReqParams{
    title: string;
    name: "小红" | "小蓝"
}

const obj = {
    title: "test',
    name: "小红"
}
validate(obj, schema); // 这里的schema就是interface转的json对象

那么我在实现这个功能的时候,分2步走,第一个就是生成schema,第二个就是校验;我把生成这部分放到了cli的编译层这里,程序会自动读取每一个API下的类型,然后转成一个proto.json,在这个json中,运行时可以去校验这部分的对象是否符合要求。权衡好了运行时该做什么,编译时该做什么,就可以把2个工具的大小大大压缩。

再比如说日志模块,在开发nodejs应用的时候,我们需要core的日志,也需要cli的日志,那么如何在终端表现也是需要权衡的。

结语

今年实在是很少时间写文章,就趁着看书和写框架做一个随心记录,希望你们能看得懂(内容偏水,应该都有看得懂)

往期回顾

2020年终总结

前言

每年都会写年终总结,目的就是为了3 5年后从博客中找出每一年的年终总结,可以一目了然看到成长,这种感觉是非常幸福的。今年真的收获巨大,因为完成了我职业生涯中很多第一次;或许前几年初入圈子有些许迷茫吧,虽然目前我对我今后几年的发展抱有很大期望,但是如果要达到我的最终目标,付出的时间和精力将会成倍上增。这个目标是什么,后面会有聊到。往年我写年终总结的时候主要是3个核心概念:疫情,心境成长,技术成长,今年我打算多增加几个板块,而且还会传一些图片上来,也算是一份宝贵经历。

先听首歌吧,边听边看

疫情

今年已经临近尾声,没想到西安疫情爆发,仅次于当年的武汉,不知道过年还能不能回家。我现在在家办公中,在家办公很舒服,但是有一点非常不好,我的生物钟全部被打乱了,每天11点睡,自然醒已经早上8点了;如果按照往常工作日,我应该是6点或者不到6点就起床了,但是这样也问题不大,省去了大量通勤时间,尽管我睡眠时间长,但是仍然有学习时间。西安疫情的事件上了很多次热搜,ZF的种种蜜汁操作,还有一码通小程序平均每周崩一次,应急预案也没有这次丢人真的丢大发了。

封城之后,时隔一周看到了政府的救济菜:白萝卜,大白菜,土豆,洋葱,但是不知道下一次送菜是什么时候...

疫情中不得不提的就是,我和几个小伙伴搞了一个核酸地图的小程序,可以清楚的看到自己身边有多少个核酸检测点,上线之后流量暴增,上了纸媒,也上了热搜,接受了采访,这一段经历真的是非常难忘。

IMG_0349.PNG

IMG_0350.PNG

小组开会的随手截图

WechatIMG3527.jpeg

随后当小区居家隔离,西安的临时检测点也就没了,这个程序在一段时间帮助了很多人,虽然现在它没有用处,但是它也曾出现过...

在家办公很爽,尤其是和姐姐们一起住,自己在屋里写代码,饭不用担心,只洗碗就够了哈哈,总之疫情居家生活还是蛮舒服的,舒服是相对的,相信大家也听说过西安有些小伙伴都饿晕了,也有出门买馒头被抓的,也有医院门口因为核酸流产的,也有老父亲因为送医不及时心脏病故,这样对比起来,我真的算幸运的了。


回顾老剧

跑男排面:

IMG_0090.JPG

薛仁贵传奇,真老剧了:

i45iqmbggfuik29.jpeg

怪侠一枝梅:

8c0f65f348ff435f91150b2a760608ee.png

庆余年:

v2-e3c1be7492a0a5b9b2eafc7504467820_1440w.jpeg

越狱1-5:

143892174951732900_a700x398.jpeg


推荐一些最近看的资源

不光有b站视频,还有最近一年写的比较好的文章,还有看的一些书

书籍

视频

文章


未来几年的打算

熟悉我的朋友们都知道,我毕业之后的工资很低很低,曾经只有1.2k只够养活自己,2年过去了,我的薪资差不多是翻了10倍多,听起来感觉很不错,但其实远远达不到我心中的高度,2022年将是我的第三年,在写这篇文章的前一天晚上,和朋友小马哥聊天,改变了一些原先计划:

WX20220105-152052.png

我决定还是2022年安稳过一年吧,因为2022年太多计划了,比如无止境的考试还有驾照,我希望安稳度过,而不是逞一时之快去拿所谓的15 16k的高薪offer。我希望2022年产出极高,水平提升极快,能够奠定相对扎实的基础去实现后面的事情。

我个人非常希望纸贵这个公司是我在中国近几年最后一家公司,这里的小伙伴很nice,等待疫情结束(几年后),如果我的本科以及雅思学习旅途顺利,我大概率将会申请国外的在职研究生,这也算圆了我的学习梦,也算是变相脱离内卷,也希望以后的职业发展将会在其他国家。

2022年非技术目标:

  • 一切考试顺利,英语学习顺利,驾照考试顺利

关于技术

今年说实话没什么开源的作品,大部分都是下半年开始做的,vite的流行意味着前端构建已经变天了,webpack纵然还有用武之地,但是中小新项目基本都会选择vite作为构建工具。vite的出现进一步的带动了esbuild等新兴工具的热度,使用go,rust等语言,借助语言特性可以使编译,检查等工作变得更快更靠谱。

在下半年,来到新团队,就开始搞模板搞新技术栈,开源了一套模板和项目管理工具

WX20220105-180205.png

也写了相关文章介绍了几个模板 基于vite的模板

同样的,写了一个较为完整的命令行工具

1641377080467.jpg

在来新团队时候,也做了一个试验性的东西就是低代码雏形,解决了模板代码生成的问题

WX20220105-180602.png

尽管这个项目不维护了哈哈: https://github.com/seho-code-life/code-template-generation-web

同样的,今年还给antdv以及esbuild-node-tsc(基于esbuild的tsc编译工具)贡献了一部分代码,而且还出了一款教程关于tsrpc(typescript的rpc框架)。

临近年底,也写了一个关于状态管理持久化的库,它是一个支持多平台的持久化插件,马上就发布了

WX20220105-181006.png

https://github.com/1018715564/store-persistedstate-killer

而且在公司内部做了2次分享,第一次是关于前端基建,第二次是关于全栈开发,第二次的经历录制成视频了换一个方式构建全栈应用

顺便说一句,slidev是真好用,用熟悉的语法构建ppt,真的省心不少,建议开发者以后使用slidev写ppt

今年技术这块没有取的突破进展,只是对rust和ts有了新的领悟,以后会在文章体现。明年团队有flutter的工作,到时候我也会分享一些flutter的小文。

2022年技术相关目标:

  • 学习rust和wasm
  • 学习flutter
  • vue3全家桶源码分析
  • 算法
  • ts水平提高
  • js基础和http基础

拜拜咯,2022年终总结见~

原文链接:因卓诶-简单聊聊前端渲染模式以及Nuxt3.js

前言

最近的工作有涉及到ssr,所以这篇文章算是一个总结,并且对还在beta阶段的nuxt3做一个浅析。前段时间有一个蛮火的视频,关于rollup作者rich的一段演讲,在演讲里面rich梳理了ssr和csr,并且讲述了痛点,和提出新的概念“transition app”,如果你有兴趣可以看看这个视频

在文章开始前,我来简单介绍一下"spa", "mpa", "ssr", "csr"......这些个名词的意义。如果你是做web前端开发的,这几个词可能伴随着你的工作生涯很久很久了,相关文章互联网上多如牛毛,如果你对这些概念比较模糊甚至压根不知道,那么别关闭网页,我希望这篇文章能够拯救你。

SPA与MPA

MPA称之为“多页应用”, 那么什么是多页应用呢?字面意思其实就是有多个页面的应用就是多页应用。从技术手段上来讲,你可以这么粗略地理解。SPA,MPA不同点太多了,而且各有利弊。

MPA应用你需要单独维护多个html页面,而且我们每加载/切换一次页面,都需要加载一整个页面。但是它对于seo特别友好,因为我们可以给每一个html页面设置不同的meta等信息,从而达到更好的收录效果;所以MPA多出现在大型的电商/新闻网站等。

不同于MPA,SPA可以使得我们通过ajax或者其他技术动态的更改某一个区域的内容而不需要重新加载页面,包括切换页面也不会重新加载整个html,它对状态的留存做的很好,而且在移动端表现特别优异(因为在以前流量是很珍贵的,可以以最小的损失切换页面,无论是用户体验还是成本相较于MPA都是极大的改善)

SSR

在我们web较早的时候,开发者喜欢使用jsp或者其他模板渲染引擎来构造一个应用。我们一般称之为SSR(服务端渲染) 它的大致架构是如下这个样子

untitled.png

用户发起一个请求抵达后端服务器后:

  1. 后端会将用户所需要的内容通过数据层进行查询
  2. 处理业务
  3. 通过模板来拼接页面
  4. 返回一个html字符串给客户端
  5. 前端渲染然后加载js脚本完成剩余交互

你可能也发现了,在SSR服务端渲染中,前端负责的东西太过单薄,说得好听叫交互,难听点就是“点击事件工程师”。所以老一辈的后端基本人人都会前端,js的水平高的一抓一大把。随着使用SSR渲染页面的应用越来越多,弊端也出现了:

  1. 后端做了太多事情了,再牛逼的人也吃不消
  2. 前后端耦合,维护难度升级
  3. 内容更新/跳转,都需要重新加载一次页面
  4. 服务端渲染成本很高
  5. ...

CSR

CSR(客户端渲染)大致是以下的架构:

csr.png

CSR架构更贴近我们的现代前端开发,我们一般使用VUE, REACT这一类的前端视图框架时,都是默认CSR体系的。大致的流程是下面这样子的:

  1. 浏览器向前端服务器请求html和js,html页面是空html,并且同时执行js
  2. js渲染页面
  3. 通过后端暴露的api进行交互

SSR和CSR的区别

可以发现,使用CSR进行开发,会有几个明显的缺点

SEO

因为从前端服务器获取的html最开始是空html,这非常不利于seo,很多搜索引擎的老版本蜘蛛会直接爬页面,不会等待js加载完,所以会直接爬出来一个空页面。尽管现在的百度,谷歌等搜索引擎的爬虫能力很强,能够部分支持CSR SPA页面,SEO效果虽然可以其他方式弥补 (比如加入meta标签等等); 但是我们使用SSR完全不用担心,因为获得的html页面是一个完整的,可以直接渲染的。

用户体验(白屏)

关于白屏,由于CSR从HTML构建完成到JS渲染页面完成(但还没呈现页面)这一段过程中,是处于一个白屏的时间,用户体验很不好,反之使用SSR获得HTML之后只需要直接构建DOM就可以了。

同样的,我们使用SSR还有不一样的缺点:

  1. 成本问题(相比CSR多了构建HTML以及获取数据,需要更多的服务器负载均衡)
  2. 部署问题(与CSR部署环境不同,不是仅仅需要一个静态文件托管服务器那么简单了)
  3. 代码难度问题
  4. ...

使用Vite快速构建一个SSR(实践SSR)

Vite SSR虽然现在是一个实验性质,不能用于生产环境。但是我们可以使用Vite做一个ssr的demo,帮助我们理解SSR的构建,理解之后我们再来引入"Nuxt", "同构"等概念。Vite里面为SSR提供了很多支持,所以我们要开发一个demo,会非常非常简单,你也可以参考这篇官网文档

我们首先需要更改index.html的内容

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
    <script type="module" src="/src/entry-client"></script>
  </body>
</html>

可以看到我们在app的div里写了一段注释,到时候我们渲染完之后的html将会replace这个注释。

然后需要在根目录新建一个server.mjs,作为我们的服务入口,用express作为一个例子:

// server.mjs
import { readFileSync } from 'fs'
import { resolve } from 'path'
import express from 'express'
import { createServer as createViteServer } from 'vite'

const createServer = async () => {
  const app = express()
  const vite = await createViteServer({
    server: { middlewareMode: 'ssr' }
  })
  // 使用vite这个中间件
  app.use(vite.middlewares)
  app.use('*', async (req, res) => {
    try {
      // 服务 index.html - 下面我们来处理这个问题
      const url = req.originalUrl
      // 读取根目录的模板
      let template = readFileSync(resolve('index.html'), 'utf-8')
      // 转换index.html 使其hmr有效
      template = await vite.transformIndexHtml(url, template)
      // 加载entry-server这个文件中的render方法
      const { render } = await vite.ssrLoadModule('./src/entry-server.js')
      // 根据url进行渲染
      const appHtml = await render(url)
      // 替换注释为准备好的html
      const html = template.replace(`<!--ssr-outlet-->`, appHtml)
      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (error) {
      vite.ssrFixStacktrace(e)
      console.error(e)
      res.status(500).end(e.message)
    }
  })

  app.listen(3000)
}

createServer()

我们的main.js也需要更改

// src/main.js

import App from './App.vue'
import Router from './router'
import { createSSRApp } from 'vue'

export function createApp() {
  const app = createSSRApp(App)
  app.use(Router)
  return { app, router: Router }
}

我们在main.js中,从vue导出createSSRApp函数,并且使用router,并且返回一个对象,这个对象之后将会被entry-server引用。

那么router也和我们传统的csr应用不太一样,我们根据env判断,传入了不同的路由类型:

// src/router/router.js

import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'

const Router = createRouter({
  history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
  routes: [
    {
      name: 'index',
      path: '/index',
      component: () => import('../pages/index.vue')
    }
  ]
})

export default Router

然后我们需要在src中新建 entry-client.js(会被index.html引入) 以及 entry-server.js

// src/entry-client.js

import { createApp } from './main'

const { app, router } = createApp()

router.isReady().then(() => {
  app.mount('#app')
})
// src/entry-server.js

import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'

export const render = async (url) => {
  try {
    const { app, router } = createApp()
    // url跳转一下路径
    router.push(url)
    // 路由准备好
    await router.isReady()
    const ctx = {}
    // 返回一个html
    const html = await renderToString(app, ctx)
    return html
  } catch (error) {
    // console.log(error)
  }
}

到此为止我们可以在本地启动一个服务器,并且可以将我们的页面以ssr的形式渲染到浏览器中了,由于我们的demo代码都是esm,所以我们使用node执行,必须要写成mjs的后缀。

node server.mjs

启动服务器之后,访问/index这个路由,你就能看到我们的页面了

如果你的node版本不支持mjs,请先升级...

ssr示例项目:

  1. 本篇文章的demo
  2. 官方的demo

喝水,脱水,注水(SSR)

读到这里,你或许已经对ssr的流程有一个粗略的了解了;那么这一part的三个例子会加深你对ssr的理解,就是ssr常常说的喝水,脱水,注水

我们ssr在服务端构造页面时,数据是从数据源流下,使得我们页面数据得到填充,这个过程就叫做喝水(render & beforeRender)
喝水的过程就是在服务端渲染页面做的事情,就好比下面这个图:

1.png

饱满的水气球代表了一个健壮的网页

我们实现ssr需要直出html,所以需要把结构以及数据进行脱水 (如图)

fpic6008.jpeg

然后到了客户端,我们需要ssr应用重新焕活,就要让原本脱水了的state,prop等等数据恢复到原来的生机,并且重新render组件,这个过程就叫做注水

SSG

SSG这种渲染模式采取了CSR和SSR的共同优点,它不需要开发者介入服务器操作,开发者只需要准备cdn或者其他静态网页托管服务器,prerender出静态资源这一步将在构建时就已经做了,呈现在用户眼前的虽然不是实时变更的,但是也保留了CSR和SSR的精髓,一定程度上有了平衡。但是因为prerender的缘故,它和SSR的大致工作方式会相似一点。

也是有缺点的

  1. 随着业务的复杂,需要生成的页面可能不单单只有1,2个,所以这对于构建的要求很高
  2. 时效性问题,用户可能看到的页面是上一次生成的,所以这一部分仍需要其他模式来补充...

同构SSR和CSR(共享data)

同构说白了,就是将我们的前端代码,既能在客户端运行,也能在服务端运行,而且还能保持上下文的状态,我们在上面的改造例子已经实现了同一份代码在2个端的运行,但是并没有实现状态的同步,比如我们在nuxt中,使用asyncData这类钩子一样,能在服务端运行而且返回的data可以和客户端共享。

// 在nuxt2中我们可以这样
async asyncData({ store, $axios, $oss }) {
    return {
        hello: "world"
    }
}
<div>{{hello}}</div>

我们现在需要改造我们的demo:

// index.vue
// 新增一个option
asyncData() {
   return {
     hello: 'message'
   }
 }

其次在server端将asyncData返回的对象和其他页面html一起进行脱水:

// entry-
import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'

export const render = async (url) => {
  try {
    const { app, router } = createApp()
    router.push(url)
    await router.isReady()
    let data = {}
    // 命中路由组件,且执行asyncData这个函数
    if (router.currentRoute.value.matched[0].components.default.asyncData) {
      const asyncFunc = router.currentRoute.value.matched[0].components.default.asyncData
      data = asyncFunc.call()
    }
    const html = await renderToString(app)
    return { html, data }
  } catch (error) {
    // console.log(error)
  }
}

// 我们的server.mjs也需要变更一下

// server.mjs

app.use('*', async (req, res) => {
    try {
      // 服务 index.html - 下面我们来处理这个问题
      const url = req.originalUrl
      let template = readFileSync(resolve('index.html'), 'utf-8')
      template = await vite.transformIndexHtml(url, template)
      const { render } = await vite.ssrLoadModule('./src/entry-server.js')
      const { html: appHtml, data } = await render(url)
      // 拼接标签,把data序列化插入到文档中
      const html = template.replace(`<!--ssr-outlet-->`, `${appHtml}<script>window.__data__=${JSON.stringify(data)}</script>`)
      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (error) {
      vite.ssrFixStacktrace(e)
      console.error(e)
      res.status(500).end(e.message)
    }
  })

可以看到我们将data序列化到了window对象中了,接下来我们需要在client端注水的时候,把新data进行替换

// entry-client.js

router.isReady().then(() => {
  const component = router.currentRoute.value.matched[0].components.default
  let _data = {}
  // 判断是否是函数
  if (typeof component.data === 'function') {
    _data = component.data.call()
  }
  // 判断是否有脱水的data
  if (window.__data__) {
    _data = {
      ..._data,
      ...window.__data__
    }
  }
  component.data = () => _data
  app.mount('#app')
})

这个时候我们已经成功的看到index.vue中能够正确的在template中打印hello这个字段了

到这里,你就可以举一反三,使用vuex也可以进行同步数据,都是把data序列化到window中保存,然后在client挂载前重新commit到store里面就可以了。

Nuxt3

是时候引入nuxt了,我们如果使用nuxt将会更容易的完成ssr需求,这一部分不会教大家怎么写nuxt,毕竟都是框架,都很简单。我会和大家梳理一下nuxt2和nuxt3的变化,如果你用过nuxt2,那么这一部分内容你可能会非常感兴趣。写这篇文章的时候,nuxt3并没有release,所以到时候release后会考虑再出一篇总结。

值得关注的更新内容

  1. 更好的性能
  2. esm的支持
  3. vue3更好的集成,说明我们可以使用composition api了
  4. vite开发服务器加持
  5. webpack5 支持(尽管我不用)

Nitro Engine

简单翻阅了一下文档,和大家分享一下,在nuxt3中的新服务端引擎 Nitro Engine, nuxt2中服务端核心使用的是connect.js,而nuxt3使用的是nuxt团队自研的h3框架,特点就是具有很强的可移植性,而且非常轻量级,并且还支持connect编写的中间件。也就是说nuxt3基于h3编写的server端,可以无缝地移植到支持js运行环境的地方,比如说woker,serverless...

我们先试试,开发一个在nuxt3中使用的api

// server/api/hello.ts
export default (req, res) => {
  return 'Hello World'
}

同样,支持异步,也支持nodejs风格的调用

export default async (req, res) => {
  res.statusCode = 200
  res.end('hello world')
}

nuxt3也支持在同一个server文件夹中编写middleware,而且是自动导入的。nuxt3这次的更新,属于是把文件系统玩出花了,不光plugins不需要重复声明了(nuxt2要在config重复声明),而且components,composables(nuxt3新增的文件夹,可以存放公共hook)... 都可以支持自动导入。

试想一下,如今写nuxt3应用,搭配vue3 composition api,将会使开发体验上升好几个台阶。

文末,我们可以试试打包一个nuxt应用到cloudflare 作为woker运行是什么效果?我们在build之后会发现output文件夹很简洁(不像nuxt2迁移部署都很令人头疼)

我们不仅可以在最后的demo中看到页面,也可以访问 api/hello 这个路由查看刚刚我们在nuxt中定义的api

点击访问
部署到cloudflare-文档
demo地址

结语

又是水文一篇,希望以后可以出一些高质量的总结文章,希望这篇文章所讲述的前端常见的渲染模式,你能够知道,并且知道原理,这也就是本文最终的目标。框架会不会都没关系,我们要洞悉一切技术背后的真相,再去研究框架不是手到擒来么?

前言

不知不觉剑指题解即将陪伴大家走过了一年时间,事实上从项目开始萌生想法和方向是2020年的10月中旬,在今年的三月份才开始大量产出代码。我利用自己每天的空余时间去设计,去运营,去开发自己的东西,我把所学所感都会输出在这个项目。结果很不错我在今年的6月份拿到了Dcloud三等奖,并且坚守诺言,开源了每一句代码以及每一个设计素材,这里要非常感谢@谢敏和@马瑞朝对项目的大力支持。没有谢敏同学就没有APP丰富好看的ui界面,她在很多地方都加入了代码色彩非常贴近用户,所以她设计出来的成品是非常好的,开源之后很多人都借鉴或者复用了谢敏同学的设计,这也是对谢敏大大的认可。在题解项目前期主要是马瑞朝同学给我疏通逻辑,去参与系统设计,让我一个以前端为主的假全栈能够得心应手的去玩转serverless以及文档数据库。在10月份会迎来一次全新的 "release版本: v1.1.0",这篇文章将会简单说一下v1.1.0我们新增了哪些东西~

版本说明v1.1.0

【npm包】新增typescript-type核心包,重构了unicloud/explain/sword-core等所有类型提示
【后端】unicloud后端架构大幅调整,从explain1->explain2,所有核心云函数都将采用http云函数URL化
【后端】后端架构调整&代码重构&美化代码
【后端】新增TS运行时校验功能
【后端】后端核心函数新增中间件体系
【后端】更改explain2核心部分代码
【后端】核心主要函数将cjs替换为es
【生态】QQ机器人系统,推送海量题目
【生态】微信小程序结束了审核,此后微信小程序&QQ小程序将会正常迭代
【前端】重构&美化了API层的所有代码(*下一个版本会重点改进这里)
【官网】更改官网的部分内容
【依赖】app核心工程升级了所有的依赖包到最新版本(*后续会对windows电脑部署项目出现的bug进行修复)
【app】整个app工程引入了husky和lint-staged,eslint

架构调整

请输入图片描述

代码重构&美化

控制器改造之前:

WX20211006-144636.png

控制器改造之后:

WX20211006-144603.png

TS运行时校验

1633503145303.jpg

QQ机器人

WX20211006-145431.png

新增全局类型npm包

github: sword-typescript-type-core
npm: sword-typescript-type-core

WX20211006-145610.png

补丁包&v1.1.0 next版本预告

【前端】API类型提示
【前端】部分页面代码进行美化&重构
【前端】全方位优化用户体验
【生态】服务号推送&通知(微信端)
【生态】QQ群合作
【生态】题库完善,增加java&php的题库
【后端】完善其他云函数(*openapi)