开发和架构生态¶
Overview¶
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)
- 4) 聚合:使用
-
@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/polyfill
是core-js
和regenerator-runtime
两个包的结合- 在新的babel生态中已废弃,鼓励开发者直接在工程中使用 core-js 和 regenerator-runtime。
-
@babel/runtime
@babel/runtime-corejs2
@babel/runtime-corejs3
-
@babel/node
- 类似 Node.js Cli,
@babel/node
提供在命令行执行高级语法的环境。
- 类似 Node.js Cli,
-
@babel/register
- 为 require 增加了一个 hook,所有被 Node.js 引用的文件都先被 Babel 转码。
-
@babel/template
- 封装了基于 AST 的模块能力,将字符串代码转换为AST
生态架构与分层¶
- 图示
-
应用层:终端命令行、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¶
- 架构图
@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
-
流程图
-
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
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 插件体系
-
架构平台化——色组&色值平台设计
解析 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
- 4) 为每个模块包裹 factory function,
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 实现流程图
- 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 语言,原生平台解析虚拟节点,渲染原生组件
- 分层架构图
- C++ 适配层与 MessageQueue.js 通信格式:类 JSON-PRC
- 优点:通过前端能力,实现了原生应用的跨平台,快速编译、快速发布
- 缺点:数据通信过程是异步的,通信成本很高,在一定程度上需要开发者了解原生开发细节
React Native 技术重构¶
-
旧的架构图
- 图示
- Asynchronous Bridge:通过数据通信,架起了Web和原生的桥梁;
- 问题:Javascript 能力与原生能力永远不在一个时空,无法共享内存空间。
-
新的架构图
- 图示
-
改变线程模型
- 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。