Skip to content

开发和架构生态

Overview

y

core-js 及 Polyfill 理念

core-js 及垫片理念:设计一个“最完美”的Pollyfill方案

core-js 概念

  • core-js 是一个 javascript 标准库,包含了 ECMAScript 2020 在内的多项特性的 polyfills,以及 ECMAScript 在 proposals 阶段的特性、WHATWG/W3C 新特性等,它是一个现代化前端项目的 “标准套件”。
  • 通过 core-js 可以窥见前端工程化的方方面面;
  • core-js 与 babel 深度绑定,帮助更好理解 babel 生态
  • 通过解析 core-js,梳理前端一个特色概念 —— Pollyfill(垫片/补丁)

core-js 工程一览

  • core-js

    • 引入全局 polyfill
    • import 'core-js';
    • import 'core-js/features/array/from';
  • core-js-pure

    • 提供不污染全局变量的垫片能力
    • import _from from 'core-js-pure/features/array/from';
  • core-js-compact

    • 维护了按照 browserlist 规范的垫片需求数据
    • require('core-js-compat')({ target: '>2.5%' })
    • core-js-compact 被 babel 生态使用,由 Babel 分析出根据环境按需加载的垫片
  • core-js-builder

    • core-js-builder 被 Node.js 服务使用,构建出不同场景的垫片包
    • core-js-builder 结合 core-js-compat 以及 core-js,并利用 webpack 能力,根据需求打包出 core-js 代码
  • core-js-bundle

寻找最佳 Polyfill 方案

  • 目标:侵入性最小,工程化、自动化程度最高,业务影响最低
  • 方案一:“手动打补丁”

    • es5-shim 和 es6-shim

      • 笨重
    • babel-polyfill 结合 @babel/preset-env + useBuiltins(entry) + preset-env targets 方案

      • 避免项目size过大、污染全局变量
      • 直接引入 import '@babel/pollyfill' 被编译为:
      • import 'core-js/xxx/xxx'; import 'core-js/xxx/yyy';
    • @babel/preset-env + useBuiltins(usage) + preset-env targets

      • usage:根据代码AST分析,更细粒度的 按需引入
      • 直接使用 core-js
  • 方案二:“在线动态打补丁”

Babel 生态

梳理混乱的Babel,不再被编译报错困扰

babel 是什么

  • Babel is a JavaScript compiler, for next generation JavaScript.
  • 职责范围

    • 语法转换,一般是高级语言降级
    • Polyfill(垫片/补丁) 特性的实现和接入
    • 源码转换,比如 jsx 等
  • 工程化的设计理念

    • 可插拔(Pluggable):灵活的插件机制,利用第三方插件
    • 可调试(Debuggable):编译时提供 sourcemap,建立源码与编译结果之间的映射关系
    • 基于协议(Compact)

Babel 工程的 Monorepo

  • @babel/core

    • @babel/core 是 Babel 实现转换的核心,提供了基础的编译能力。
    • require("@babel.core").transform(code, options, (err, result) => { result; // {code, map, ast}})
    • 组成元素

      • @babel/parser
      • @babel/code-frame
      • @babel/generator
      • @babel/traverse
      • @babel/types
  • @babel/standalone

    • @babel/standalone 在浏览器中直接执行;
    • 基于 @babel/core,对于浏览器环境,动态插入高级语言特性的脚本、在线自动解析编译。
    • 未来应用场景:Web IDE和智能化方向
  • @babel/parser

    • 1) 解析:输入源码,获取相应的编译器,使用编译器将源码转换为 AST,输出。
    • require("@babel/parser").parse("code", { sourseType: "", pugins: []})
  • @babel/traverse

    • 2) 遍历:遍历AST,对源码的AST进行修改,才能产出编译后的代码
    • traverse(ast, { enter(path){} })
  • @babel/types

    • 3) 修改:提供了对AST节点的修改能力;
    • 指导如何对遍历的ast内容,进行具体的修改。
  • @babel/generator

    • 4) 聚合:使用 @babel/generator 对新的 AST 进行聚合,并生成 Javascript代码。
    • const output = generate(ast, {}, code)
  • @babel/helper-*

  • @babel/preset-*

    • @babel/preset-env 是直接暴露给开发者在业务中运用的包能力。
    • @babel/preset-env 如何按需引入polyfills:通过targets参数,按照 browserslist 规范,结合core-js-compat,筛选中适合环境的polyfills。
  • @babel/cli

    • @babel/cli 是 Babel 提供的命令行。
    • 负责获取配置内容,并依赖 @babel/core 完成编译。
  • @babel/preset

  • @babel/plugin-*

    • @babel/plugin-transform-runtime

      • @babel/plugin-transform-runtime: 重复使用 Babel 注入的helpers函数,减少代码体积。
      • @babel/plugin-transform-runtime 用于编译时,作为 devDependencies 使用;
      • @babel/plugin-transform-runtime 需要和 @babel/runtime 配合使用,引用 @babel/runtime 提供的helpers,对代码瘦身,同时避免了全局变量污染。
    • @babel/plugin 是Babel插件集合

    • @babel/plugin-syntax-* 是Babel语法插件,作用是扩展 @babel/parser的能力,比如 @babel/plugin-syntax-top-level-await 插件等
    • @babel/plugin-proposal-* 用于编译转换在提议阶段的语言特性
    • @babel/plugin-transform-* 是Babel的转换插件,
  • @babel/polyfill

    • @babel/polyfillcore-jsregenerator-runtime 两个包的结合
    • 在新的babel生态中已废弃,鼓励开发者直接在工程中使用 core-js 和 regenerator-runtime。
  • @babel/runtime

  • @babel/runtime-corejs2
  • @babel/runtime-corejs3
  • @babel/node

    • 类似 Node.js Cli,@babel/node 提供在命令行执行高级语法的环境。
  • @babel/register

    • 为 require 增加了一个 hook,所有被 Node.js 引用的文件都先被 Babel 转码。
  • @babel/template

    • 封装了基于 AST 的模块能力,将字符串代码转换为AST

生态架构与分层

  • 图示

x

  • 应用层:终端命令行、webpack loader、浏览器端编译

    • babel-loader:帮助 Babel 结合Webpack,融入到整个基建环节。
    • @babel/eslint-parser:与eslint合作
  • 胶水层

  • 基础层:提供了基础的编译能力
  • 辅助层:基础层的抽象能力下沉

公共库与 babel-preset

探索前端工具生态,制定一个统一标准化 babel-preset

企业级公共库设计原则

  • 应用项目构建 vs 公共库构建

    • 应用项目:“只要能在需要兼容的环境中跑起来”就达到了基本目的
    • 公共库:要兼顾性能和易用性,注重质量和广泛度
  • 对于开发者,公共库要最大化确保开发体验

    • 最快地搭建调试和开发环境
    • 安全地发版维护
  • 对于使用者,公共库要最大化确保使用体验

    • 公共库文档建设完善
    • 公共库质量有保障
    • 接入和使用负担最小

设计方案推荐

  • UI 组件类文档

    • 考虑部署静态组件展示站点,进行组件展示以及用法说明
  • 智能、工程化

    • 考虑使用类似 JSDoc 来实现 JavaScript API 文档生成
  • 组件类公共库

    • 考虑 Storybook 或者 Styleguides 作为标准接入方案

引入 babel-preset

  • 公共库适配环境是什么?可能需要兼容:浏览器/Node.js/同构群等不同环境对应了不同的编译和打包标准。
  • babel-preset 保证编译产出的统一
  • {loader: 'babel-loader, options: {presets: ['@lego/babel-preset/dependencies']}'}

制定统一标准化 babel-preset

  • 架构图

x

  • @lego/babel-preset/app:负责编译除node_modules外的业务代码
  • @lego/babel-preset/dependencies:编译node_modules第三方代码
  • @lego/babel-preset/library:按照当前Node环境编译输出代码
  • @lego/babel-preset/compact:编译降级为 ES5
  • 要点

    • 1)对于企业级公共库,建议使用标准ES5特性发布;对tree-shaking有需求的库,应同时发布ES module 格式代码
    • 2)对于企业级公共库,发布代码不包含 polyfills,由使用方统一处理
    • 3)对于应用编译,使用@babel/preset-env同时编译应用代码与第三方库代码
    • 4)对于应用编译,需要对 node_modules进行编译;并为 node_modules 配置 sourceType: 'unambigous',以确保三方依赖包中的 CommonJS 模块能够被正确处理
    • 5)对于应用编译,启用 plugin-tranform-runtime,避免同样的helper被重复注入多个文件;同时自动注入 regenerator-runtime,避免全局变量污染

标准公共库实战

从实战出发,从 0 到 1 构建一个符合标准的公共库

目标

  • 产出一个在 浏览器端Node.js 端 复用的npm包,编译构建使用 Webpack 和 Babel。

支持script标签引入

  • 将已有公共库脚本编译为 UMD 方式
  • 1)降级:将降级代码输出到 output,被浏览器直接引用;使用 @babel/plugin-transform-modules-umd 处理:
  • npm install -D @babel/plugin-transform-modules-umd @babel/core @babel/cli
  • 2)打包依赖:在源码中,没有使用引入并编译 index.js 所需的依赖的;使用webpack处理:
  • npm install -D webpack webpack-cli

支持 Node.js 环境

  • 经过 @babel/plugin-transform-modules-umd 处理后的代码,可以直接使用

代码拆分与按需加载

代码拆分和按需加载:缩减 bundle size,把性能做到极致

按需加载 vs 按需打包

  • 按需加载:代码模块在交互需要时动态引入。
  • 按需打包:针对第三方依赖库及业务模块,只打包真正在运行时可能需要的代码。
  • 使用 ES Module 的 tree shaking 方案,实现 按需打包
  • 使用 babel-plugin-import 为主的 Babel 插件,学习编写babel插件,实现 按需打包

    • Babel 插件通过观察者+访问者模式,遍历 AST
    • 在 Babel 对AST 语法树进行转换时介入,通过改写 import 处理逻辑(重写 buildExpressionHandler方法),改变生成结果

重新认识 dynamic import

  • 静态导入的性能优劣

    • 静态导入:标准 import 方法属于 静态导入,使所有被import 的模块在 加载时就被编译
    • 静态导入降低代码加载速度,可用性低;
    • 静态导入运行时时占用大量内存,可用性低。
  • 深入理解 dynamic import

    • 按需加载一个模块,按运行时间选定一个模块
    • 被导入的模块加载时并不存在,需要异步获取;导入模块的说明符需要动态构建
    • Dynamic Import vs Function

      • Dynamic Import 并非继承自 Function.prototype,不能使用Function原型方法
      • Dynamic Import 并非继承自Object.prototype
  • 实现一个 dynamic import

    • 1) 返回一个 new Promise
    • 2) 创建一个 script 元素,且 script.type = "module"; script.textContent= "import * as m from ${url}";
    • 3) script.onload 触发后 resolve 该 Promise

webpack 赋能拆分和按需

  • Webpack 对 dynamic import 的支持

    • require.ensure():能将对应文件拆分到一个单独的bundle中,该bundle异步加载
    • import( /* webpackChunkName: "chunk-name" */ /* webpackMode: "lazy" */ 'module');

      • webpackChunkName:自定义 chunk 名称
      • webpackMode:每个import导入的模块,生成一个可延迟加载的(lazy-loadable)chunk
    • 流程图

    x

  • Webpack splitChunk 插件和代码分割

    • 代码分割,一种代码拆包技术,与代码合并相逆
    • 代码分割的意义: 避免重复打包以及提升缓存利用率,进而提升访问速度。
    • splitChunk 插件,触发自动分割条件

      • splitChunk
      • 压缩体积大于 30KB 的模块
      • 按需加载时,并行加载模块不得超过 5 个
      • 页面初始化加载时,并行加载模块不得超过3 个

Tree Shaking

Tree Shaking:移除Javascript上下文中的未引用代码

Tree Shaking 必备理论

  • ESM 规范?

    • import 模块名只能是字符串常量
    • import 一般只能在模块最顶层出现
    • import binding 是 immutable 的
  • 问题一:Tree Shaking 为什么要依赖 ESM 规范?

    • 因为 ESM 规范属于JS 解释器(JS Core)层的规范;对代码静态分析时,删除无依赖模块。
    • 而 CommonJS 是运行环境层面的规范;只有在执行代码后,才能动态确定依赖模块。
  • 问题二:什么是副作用模块,如何对副作用模块进行 Tree Shaking?

    • 问题:Webpack 会将无法确定会否触发副作用(比如维护全局的CacheMap)的模块,打包bundle中;
    • 即使开发者知道该模块无副作用,依旧无法优化。
    • 解决:设置 sideEffects: "false": 声明该项目无副作用,告知webpack可以放心大胆优化。

Tree Shaking 与导出模式

  • 一个 Tree Shaking 友好的导出模式
  • 使用 export default 导出对象或class,不利于 Tree Shaking;
  • export 原子化或颗粒化导出,有利于 Tree Shaking。

Webpack 与 Tree Shaking

  • minimizer: [new TerserPlugin({...})]
  • Webpack 负责对模块进行分析和标记:

    • harmony export:使用过的export标记;
    • used harmony export:未使用过的export标记;
    • harmony import:所有import。
    • Webpack 在编译分析阶段,将每一个模块放入 ModuleGraph中维护;依靠 HarmonyExportSpecifierDependency 识别 used export 和 unused export
  • 压缩插件 TerserPlugin 负责根据标记结果,进行代码删除

Tree Shaking 与公共库

  • 目标:设计一个兼顾Tree Shaking 和易用性的公共库
  • 问题:Node.js 中,如果遵循ESM规范,会报错;如果以 CommonJS规范 对外暴露代码,又不利于 Tree Shaking。
  • 解决:根据协议约定来暴露
// package.json
{
    "name": "Library",
    "main": "dist/index.cjs.js",
    "module": "dist/index.esm.js"
}

CSS 与 Tree Shaking

  • 遍历所有CSS文件选择器;在 javascript 代码中进行选择器匹配;如果未匹配到,则删除对应样式代码
  • 处理工具:PostCSS

x

AST 编译原理

如何理解AST实现和编译原理

AST Explorer 网站

acorn 解析

  • acorn 是一个完全使用 Javascript 实现的、小型且快速的 Javascript 解析器。
  • require('acorn').parse(code)

    • 功能:完成AST的封装以及错误抛出
    • Program(整个程序)
    • Statement(语句)
    • Expression(表达式)
    • Program 由多段Statement构成;Expression 组成 Statement。
  • 解析流程

    • 1) source code
    • 2) 词法分析(tokenizer)
    • 3) 分词结果([token, token, token,...])
    • 4) 语法分析
    • 5)AST

AST 实战

AST 实战:实现一个简易的Tree Shaking脚本

PostCSS 与主题切换

工程化思维处理方案:如何实现应用主题切换?

主题切换工程架构设计

  • 架构思路

    • 如何维护不同主题色值?谁来维护不同主题色值?
    • 研发和设计之间,如何保持不同主题色值的同步沟通?
    • 如何最小化前端工程师的开发量,不需要 hard coding 多份颜色数值?
    • 如何做到一键切换时的性能最优?
    • 如何配合JavaScript状态管理,同步主题切换?
  • PostCSS 原理和插件能力

    • PostCSS 是一款编译CSS的工具,基于AST技术实现。
    • PostCSS 插件机制,提供给开发者分析、修改CSS的规则。

主题切换架构实现

  • 目标

    • 源码:a { color: themed(GBK05A) }
    • 编译结果:a { color: #646464; } html[data-theme='dark'] a { color: #808080; }
  • PostCSS 插件体系

  • 手动实现 postcss-theme-colors

  • 架构平台化——色组&色值平台设计

x

解析 Webpack

解析Webpack源码,实现自己的构建工具

Webpack:静态模块打包器

  • Webpack is a static module bundler for modern JavaScript applications.
  • 将 JavaScript 模块(各种模块化规范)打包为一个或多个脚本文件。

Webpack 源码流程

  • Dependency Resoluting 阶段

    • 1)分析入口脚本
    • 2)递归解析AST,获取依赖
    • 3)产出 Dependency Graph
  • Bundling 阶段

    • 4) 为每个模块包裹 factory function,moduleFunc(require, module, exports) {...}
    • 5) 以入口脚本为起点,递归执行模块
    • 6)拼接IIFE
    • 7)产出 bundle

Webpack vs Rollup

  • Rollup 不会维护一个 module map,而是将所有模块拍平(flatten)放到 bundle 中
  • Rollup 为避免命名冲突,将函数和变量名进行改写

实现自己的构建工具

小程序多端方案

从编译到运行,跨端解析小程序多端方案

小程序多端方案

  • DSL “Write once, run everywhere.”
  • 关键技术

    • 编译时方案

      • 工作主要集中在编译转化环节,
      • 基于 AST 技术,实现各平台适配
    • 运行时方案

    • 运行时结合编译时方案
  • 框架风格

    • 类 Vue 风格框架
    • 类 React 风格框架
    • 自定义 DSL 框架

编译时方案

  • Vue DSL 静态编译
  • 开发者代码 (类Vue)-> AST -> 小程序代码

运行时方案

  • 通过响应式理念,监听数据变化,调用 setData() 方法,触发小程序渲染层变化,实现视图修改
  • 不需要处理渲染层,不需要 vue 中的 patch 操作
  • 只需要调用 setData() 方法,更新数据

编译时和运行时结合方案

  • 流派

    • 强行静态编译型:京东 taro ½,去哪儿 Nanachi(缺陷明显)
    • 运行时处理型:即类 React 的编译时和运行时结合方案,如 taro next,蚂蚁 Remax
  • React 设计理念主力小程序起飞

    • React Core

      • 处理核心API,与终端平台和渲染解耦
      • React.createElement()
      • React.createClass()
      • React.Component
      • React.Children
      • React.PropTypes
    • React Renderer

      • 渲染器,定义React Tree 如何接轨不容平台
      • React Dom:渲染组件树为 DOM Elements
      • React Native:渲染组件树为不同原生平台视图
    • Reconciler

      • 负责 diff 算法,patch 行为
      • Stack reconciler:React 15 及更早版本
      • Fiber reconciler:新一代架构
    • 其它

      • React Components
      • React Instances
      • React Elements
    • 不同平台,可以依赖 hostConfig 配置与react-reconciler互动,使用 Reconciler 能力;

    • 不同平台的 Renderers 在 hostConfig 中内置基本方法,即可构造自己的渲染逻辑。
    • 小程序方案:React Core + React reconciler + 宿主配置 hostConfig + 自定义渲染器 renderers
  • 网红框架——Taro Next

    • React 实现流程图

    x

    • Remax 基本思路一致

多端优化方向

  • 性能优化

    • 编译时做的事情越多,意味着运行时越轻量,负担越小,性能也更好。

      • mpvue:编译时完成静态模板编译工作
      • remax:动态构建视图层表达,在运行时完成
    • 框架包 size

      • 初始加载性能直接依赖资源包大小
    • 数据更新粒度

      • 利用框架完成 setData() 方法调优
  • 未来发展方向

    • 工程化方案

      • 小程序多端需要有一体化的工程解决方案,设计师可与Webpack等深度绑定,同时兼顾可插拔性
    • 框架方案

      • 主流:vue 和 React
      • 小众:Flutter 和 Angular 也应重视
    • 跟进 Web 发展

      • 通过适配层,实现自定义渲染器,对技术能力和水平提出较高要求
    • 渐进增强型能力

      • 语法或DSL层面上的渐进增强能力
      • 如腾讯的 Omix,有自己的一套DSL,但整体保留小程序语法

原生跨平台技术

原生跨平台技术:移动端跨平台到Flutter的技术变革

跨端技术时间线

  • Hybrid 时期

    • JSBridge技术:H5( Webview 容器)和原声平台交互
    • HTML + Javascript + CSS
    • Cordova
    • Ionic
  • OEM 时期

    • React Native
    • Weex
  • 自渲染时期

    • Flutter

基于 Webview/JSBridge 的 Hybrid 方案

  • JSBridge技术

    • H5( Webview 容器)和原生平台的双向交互
    • 1)Javascript 调用原生

      • 注入 APIs:原生平台通过 WebView APIs, 向 Javascript Context 中注入数据
      • 拦截 URL Scheme:通过发送定义好的 URL Scheme请求,将相关数据放在请求体,该请求被原生平台拦截后做出响应
    • 2)Native 调用 Javascript

      • 原生平台直接通过 WebView APIs,直接执行 Javascript 代码
    • 存在问题

      • JavaScript Context和原生通信频繁,导致性能体验较差
      • 页面逻辑由前端负责,组件也是前端渲染,也造成了性能短板
      • 运行JavaScript的WebView内核在各平台上不统一
      • 国内厂商对于系统的深度定制,导致内核碎片化
  • React Native

    • 使用 Web 语言,原生平台解析虚拟节点,渲染原生组件
    • 分层架构图

    x

    • C++ 适配层与 MessageQueue.js 通信格式:类 JSON-PRC
    • 优点:通过前端能力,实现了原生应用的跨平台,快速编译、快速发布
    • 缺点:数据通信过程是异步的,通信成本很高,在一定程度上需要开发者了解原生开发细节

React Native 技术重构

  • 旧的架构图

    • 图示

    x

    • Asynchronous Bridge:通过数据通信,架起了Web和原生的桥梁;
    • 问题:Javascript 能力与原生能力永远不在一个时空,无法共享内存空间。
  • 新的架构图

    • 图示

    x

    • 改变线程模型

      • JS 线程:在这个线程中,Metro负责生成 JS bundle,JavascriptCore 负责运行时解析执行Javascript 代码。
      • 原生UI线程:负责用户UI界面,每当需要更新UI时,该线程与JS线程通信。分为原生UI和原生模块。
      • Shadow线程:负责计算布局,React Native 通过 Yoga 布局引擎来解析并计算 FlexBox 布局,然后将结果返回原生UI线程。
    • 引入异步渲染能力

      • 实现不同优先级的渲染
      • 简化渲染数据信息
    • 简化 Bridge 实现

      • 新的线程模型能够使手势触发交互和UI渲染效率更高,减少异步通信更新UI成本。
  • 新Bridge方案:Javascript Interface(JSI) 的

    • 新Bridge

      • Fabric:新的 UIManager。
      • TurboModules:新的原生模块。
    • 直接用C++生成 Shadow Tree,降低通信成本,提升交互性能

    • 依赖 JSI,获取C++ Host Objects,并调用 Host Objects 上的方法;这样能够完成 Javascript 和原生平台的直接感知,线程之间直接调用。
    • 允许Javascript代码仅在真正需要时,按需加载Native 模块。

Flutter 新贵背后技术变革

  • 采用Dart编程语言,不同于React Native:Flutter 不使用原生平台组件进行渲染。
  • Skia 渲染引擎,原生平台只需提供 Canvas。