Try   HackMD

umi as a Compiler (Full Version)

大纲

  • 《Compilers are the new frameworks》 文章为切入点,解释 umi 的理论基础
  • Demo 演示,给大家更直观的印象
  • 约定优于配置
  • umi 从源码到产物,解释相比传统的 webpack/roadhog 有啥区别
  • 关于路由,umi 为什么要接管路由,以及接管了能做啥
  • 不能满足?用插件爆改 umi !
  • 关于部署,umi 如何和流程对接
  • 还有时间的话,可以讲讲 duck dictory 的 dva 项目、PWA、umi-test、内置的性能优化、各种配置、未来规划、其他开发体验等等

内容

Compilers are the new frameworks

Pasted Image 2.png

不知大家有没有看过这篇文章,《Compilers are the new frameworks》,作者认为,“Web 框架正在从运行库转变为优化编译器”,我深表认同,最近新出的框架,比如 next.js、umijs、after.js、pri 等,都是这个思路的贯彻者。

之前,工具是编译时的,框架是运行时的,两者互不强依赖,相互独立。但是,我们发现,把两者结合起来会让框架更强大,对使用者也更友好。比如,我在 pages 目录下建立 404.js 的文件,然后他就变成了整个项目的 fallback 路由,这在工具和框架分离的情况下是很难做到的。

为了让大家有更直观的印象,我们来看一个 demo 演示。

Demo

copy & paste

  • pages
  • antd
  • 404
  • layouts
  • mock
  • test
  • 按需加载 & 按需编译

不知大家有没有发现,Demo 里有大量基于约定的编码方式,这是如何实现的呢?

约定优于配置

设计不好的框架通常需要多个配置文件,每一个都有许多设置。 不知道是谁说的

尽管 umi 仍然有不少配置,但在文件组织上,我们尽量选择约定的方式。"约定优于配置"是 umi 遵循的另一设计规则,我最早是在 Ruby on Rails 里感受到的,他能让框架在被使用时少做决定,但仍不失扩展性。

比如:

  • layouts/index.js,全局路由
  • global.js,写 polyfill 的地方
  • global.css,写全局样式,覆盖 antd 等
  • .global.css(计划)
  • hd.js,自定义的高清方案
  • document.ejs,定制 HTML 默认
  • 404.js,404 路由
  • mock/,基于 express 的 mock
  • .test.js,测试用例
  • loading.js(计划)
  • _routes.json,配置路由
  • _layout.js,嵌套路由
  • page.js,目录路由

从源码到产物

Pasted Image 1.png

这是使用 roadhog/webpack 的流程,没啥好讲的,大家都很熟悉了。

Pasted Image 2.png

这是 umi 的流程,

  • 首先,umi 是基于路由的,所以需要有一份路由配置,路由配置哪里来?可以是基于约定的,也可以是基于配置的,看个人喜好,推荐约定式的
  • 然后,路由配置会用于生成路由文件,在这里 umi 做了很多复杂的事情,比如开发时按需编译、运行时按需加载、各种性能优化等等,这个路由的部分后面会展开讲下
  • 然后,把入口文件交给 webpack 做打包
  • 同时,会处理 HTML 的生成(可选)
  • 最后是部署,和各种流程对接

举个具体的例子。

文件目录:

+ src
  + layouts/index.js
  + pages
    - a.js
    - b.js
    - 404.js

这会生成路由配置,

{
  component: 'layouts/index.js',
  routes: [
    { path: '/a', exact: true, component: 'pages/a.js' },
    { path: '/b', exact: true, component: 'pages/b.js' },
    { component: 'pages/404.js' },
  ],
}

然后生成路由文件,

const routes = {
  component: require('layouts/index.js'),
  routes: [
    { path: '/a', exact: true, component: require('pages/a.js') },
    { path: '/b', exact: true, component: require('pages/b.js') },
    { component: require('pages/404.js') },
  ],
};

export default () =>
  <Router history={window.g_history}>
    { renderRoutes(routes) }
  </Router>

从例子中可以看到,umi 完全接管了路由的部分工作,那么为什么要接管路由,以及接管了能做啥呢?

为什么要接管路由?

不管是单页应用还是多页应用,都是以路由为维度组织的。单页应用里是路由,多页应用里的页面其实也可以理解为路由。路由决定渲染时显示什么,跳转时用什么方式,切换时用什么动画等等。

所以,接管路由之后,框架就可以:

  • 开发时按需编译
  • 运行时按需加载,且可一键关闭
  • 根据路由数进行智能的 common 提取 (阈值:pages.length/2)
  • 根据路由生成静态页面
  • preload
  • 接管页面跳转方式,比如在容器里用 bridgex
  • 定制路由切换动画
  • 定制路由切换的 scroll 行为
  • 定制 layout
  • 定制权限路由
  • SPA 应用的埋点
  • 路由代码可选
  • SSR 时在服务器处理数据请求(未实现)

这个列表我每列一次都会增加不少,说明这里其实还有很大的想象空间。

路由约定

  • 基于配置 vs. 基于约定
  • layout
  • 嵌套路由
  • 目录路由
  • umi-plugin-routes

之前一直不敢推,是因为不支持嵌套路由,在 umi@1.1 支持了之后,大家在 PC 等路由复杂的场景下也可以放心用了。

插件机制

不能满足?用插件爆改 umi !

Pasted Image 2.png

再看回这张图,umi 整体梳理过运行流程的每个环节,并且都预留了埋点,甚至 umi 内部的实现也是由插件组成,这点有些类似 webpack,api 接口设计参考了 vue-cli@3,这点之前 @皓默 也有分享过。

比如,每个环节都提供修改的接口:

  • modifyRoutes
  • modifyRouteComponent
  • modifyHTML
  • modifyWebpackConfig
  • modifyEntryFile
  • modifyRouterFile

更多参考 umijs/umi#87

内置的插件

  • hd
  • service-worker
  • layout
  • 404
  • mock

umi-plugin-ali 包含的插件:

  • basement
  • xmas-fastclick
  • spm-bacon

还有些其他的:

  • umi-plugin-routes
  • umi-plugin-dva
  • umi-plugin-dll
  • umi-plugin-yunfengdie

关于部署

umi 如何和流程对接?

灵活运用 publicPath 和 router base 就可以支持各种应用了,详见 https://umijs.org/docs/zh-Hans/deploy.html

目前支持这些应用类型:

  • 云凤蝶
  • site + 离线包
  • egg

计划支持:

  • assets
  • 凤蝶活动

换一个维度,支持:

  • SPA
  • 静态化
  • 静态化 + htmlSuffix
  • HTML 输出 publicPath
  • 纯 JS + CSS(无 HTML)
  • 本地 file 协议

注,部署 PWA 时需要对 service-worker.js 做特殊处理。

Thanks