mirror of https://gitee.com/antv-l7/antv-l7
326 lines
13 KiB
Markdown
326 lines
13 KiB
Markdown
# 构建方案
|
||
|
||
考虑到 L7 提供的三种[使用方法](./使用方法.md):CDN、Submodule 和 React 组件,我们需要提供对应的构建方案。
|
||
|
||
由于 React 组件待开发,下面我们将从方案技术细节、优化手段两方面介绍 CDN 和 Submodule 的构建方案。
|
||
|
||
## CDN
|
||
|
||
考虑到后续将引入 WebWorker 特性,目前 Webpack4 暂时还不支持多种 target(web + webworker)混合的输出模式,相关 [ISSUE](https://github.com/webpack/webpack/issues/6525)。
|
||
如果后续支持,配合 SplitChunksPlugin 应该能解决在 Worker 和不同 entry 之间共享代码的问题。
|
||
|
||
因此目前和 Mapbox 做法一样,我们使用 Rollup 构建 CDN Bundler。
|
||
|
||
打包命令如下,会在 `dist/` 下输出产物:
|
||
```bash
|
||
yarn bundle
|
||
```
|
||
|
||
### UMD
|
||
|
||
以 L7 为命名空间,让用户可以通过类似 `L7.Scene` 的方式使用。同时以 UMD 为构建目标:
|
||
```javascript
|
||
{
|
||
input: resolveFile('build/bundle.ts'),
|
||
output: {
|
||
file: resolveFile('dist/bundle.js'),
|
||
format: 'umd',
|
||
name: 'L7',
|
||
},
|
||
}
|
||
```
|
||
|
||
目前只需要暴露场景以及图层相关的 API,因此 Bundler 非常简单:
|
||
```typescript
|
||
// build/bundle.ts
|
||
export * from '@l7/scene';
|
||
export * from '@l7/layers';
|
||
```
|
||
|
||
### Alias
|
||
|
||
为了帮助 resolver 定位 lerna packages,需要重命名类似 `@l7/scene` 这样的依赖路径:
|
||
```javascript
|
||
import alias from '@rollup/plugin-alias';
|
||
|
||
plugins: [
|
||
alias(
|
||
{
|
||
resolve: ['.tsx', '.ts'],
|
||
entries: [
|
||
{
|
||
find: /^@l7\/(.*)/,
|
||
replacement: resolveFile('packages/$1/src'),
|
||
},
|
||
]
|
||
},
|
||
),
|
||
]
|
||
```
|
||
|
||
配合 [`terser`](https://github.com/TrySound/rollup-plugin-terser) 压缩后,我们就能得到可运行的 CDN 版本了,但从减少构建产物大小出发还有很多优化可以做。
|
||
|
||
### 减少包大小
|
||
|
||
除了 Rollup 提供的 TreeShaking,我们主要从三个方面考虑:
|
||
* 减少第三方依赖大小尤其是 Lodash
|
||
* external Mapbox 依赖
|
||
* 压缩 GLSL 代码
|
||
* 去除多余空格、换行符和注释
|
||
* 内联 WebGL 常量
|
||
* 预计算 define 变量
|
||
|
||
#### Lodash 按需引用
|
||
|
||
通过 analysis 插件可以看到第三方依赖大小占比:
|
||
```
|
||
/node_modules/lodash/lodash.js
|
||
███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 23.79 % (540.328 KB)
|
||
/node_modules/regl/dist/regl.js
|
||
██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12.21 % (277.403 KB)
|
||
/node_modules/hammerjs/hammer.js
|
||
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.25 % (73.847 KB)
|
||
/node_modules/uri-js/dist/es5/uri.all.js
|
||
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.28 % (51.721 KB)
|
||
```
|
||
|
||
仔细查看 Lodash 的引用情况:
|
||
```
|
||
███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
||
file: /node_modules/lodash/lodash.js
|
||
bundle space: 23.79 %
|
||
rendered size: 540.328 KB
|
||
original size: 540.51 KB
|
||
code reduction: 0.03 %
|
||
dependents: 13
|
||
- /packages/core/src/services/layer/StyleAttribute.ts
|
||
- /packages/core/src/services/shader/ShaderModuleService.ts
|
||
- /packages/core/src/services/renderer/passes/post-processing/BlurHPass.ts
|
||
```
|
||
|
||
按需引用 Lodash 常见的做法有几种:
|
||
* [loash-es](https://github.com/lodash/lodash/tree/es)
|
||
* babel-plugin-lodash
|
||
* lodash-webpack-plugin
|
||
|
||
由于我们使用 Rollup 以及 `rollup-plugin-babel`,[babel-plugin-lodash](https://github.com/lodash/babel-plugin-lodash) 可以很好地解决这个问题。该插件的[原理](https://github.com/rollup/rollup/issues/610#issuecomment-270801483)其实也是引用 `lodash-es`:
|
||
```javascript
|
||
// this...
|
||
import { template } from 'lodash-es';
|
||
|
||
// ...basically becomes this:
|
||
import template from 'lodash-es/template.js';
|
||
```
|
||
|
||
最终的效果还是很明显的:
|
||
```
|
||
/node_modules/regl/dist/regl.js
|
||
████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 16.55 % (277.403 KB)
|
||
/node_modules/hammerjs/hammer.js
|
||
██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.41 % (73.847 KB)
|
||
/node_modules/uri-js/dist/es5/uri.all.js
|
||
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.09 % (51.721 KB)
|
||
/node_modules/lodash.mergewith/index.js
|
||
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3.06 % (51.256 KB)
|
||
```
|
||
|
||
#### 剔除 Mapbox
|
||
|
||
不同于高德异步加载的方式,Mapbox 用户需要手动引入 Mapbox 的 JS 和 CSS,因此 L7 CDN 版本就需要剔除了。通过 `globals` 假定用户负责引入 Mapbox 的 CDN 版本:
|
||
```javascript
|
||
{
|
||
output: {
|
||
globals: {
|
||
'mapbox-gl': 'mapboxgl',
|
||
},
|
||
},
|
||
external: [
|
||
'mapbox-gl',
|
||
],
|
||
}
|
||
```
|
||
|
||
这样 L7 Bundler 中就不包含 Mapbox 的 Module Bundler(mapbox-gl) 了。
|
||
|
||
#### 内联 WebGL 常量
|
||
|
||
在构建阶段可以将 WebGL 常量替换成对应的值,可以减少字符长度:
|
||
```javascript
|
||
// from
|
||
const max = gl.MAX_VERTEX_ATTRIBS;
|
||
// to
|
||
const max = 34921;
|
||
```
|
||
|
||
luma.gl 和 deck.gl 都使用了 [babel-plugin-inline-webgl-constants](https://www.npmjs.com/package/babel-plugin-inline-webgl-constants)。
|
||
|
||
来看一下实际效果,在压缩前就能减少字符长度:
|
||
```javascript
|
||
// 内联前
|
||
const usageMap = {
|
||
[gl.STATIC_DRAW]: 'static',
|
||
[gl.DYNAMIC_DRAW]: 'dynamic',
|
||
[gl.STREAM_DRAW]: 'stream'
|
||
};
|
||
// 内联后
|
||
const usageMap = {
|
||
[35044]: 'static',
|
||
[35048]: 'dynamic',
|
||
[35040]: 'stream'
|
||
};
|
||
```
|
||
|
||
#### 压缩 GLSL 代码
|
||
|
||
在开发编写 Shader 时,我们是不需要对 GLSL 代码进行压缩的,因为在 Shader 编译失败时能根据错误信息定位到具体行列。
|
||
|
||
但是在生产环境下,我们就需要把 GLSL 源代码中包含的**多余**的换行、空格以及注释去掉,减少最终引入字符串的大小。
|
||
|
||
这里需要注意的是并不是所有换行都可以简单去除,例如 `define` 语句末尾的换行一定要保留。
|
||
|
||
luma.gl 和 deck.gl 使用了 [babel-plugin-remove-glsl-comments](https://github.com/uber/luma.gl/tree/master/dev-modules/babel-plugin-remove-glsl-comments) 简单地移除注释,但很明显,多余的空格和换行符依然存在。
|
||
|
||
因此我们需要写一个简单的 Rollup 插件:
|
||
```javascript
|
||
export default function glsl(include, minify) {
|
||
const filter = createFilter(include);
|
||
return {
|
||
name: 'glsl',
|
||
transform(code, id) {
|
||
if (!filter(id)) return;
|
||
|
||
if (minify) {
|
||
code = code
|
||
.trim() // strip whitespace at the start/end
|
||
.replace(/\n+/g, '\n') // collapse multi line breaks
|
||
// remove comments
|
||
.replace(INLINE_COMMENT_REGEX, '\n')
|
||
.replace(BLOCK_COMMENT_REGEX, '')
|
||
.replace(/\n\s+/g, '\n') // strip identation
|
||
}
|
||
|
||
return {
|
||
code: `export default ${JSON.stringify(code)};`,
|
||
map: { mappings: '' }
|
||
};
|
||
}
|
||
};
|
||
}
|
||
```
|
||
|
||
#### GLSL minifier
|
||
|
||
以上针对 GLSL 的压缩仅限于字符替换,更彻底的优化必然需要生成 GLSL 对应的 AST,从而进行变量重命名、死代码消除等等更高级的优化手段。[glsl-minifier](https://github.com/TimvanScherpenzeel/glsl-minifier) 就是这样一个 CLI 工具。
|
||
|
||
其中的预计算特性有点类似 [Prepack](https://github.com/facebook/prepack),在构建阶段就计算出 `define` 变量的值:
|
||
```glsl
|
||
#define SPREAD 8.00
|
||
#define MAX_DIR_LIGHTS 0
|
||
#define MAX_POINT_LIGHTS 0
|
||
#define MAX_SPOT_LIGHTS 0
|
||
#define MAX_HEMI_LIGHTS 0
|
||
#define MAX_SHADOWS 0
|
||
#define GAMMA_FACTOR 2
|
||
|
||
uniform mat4 viewMatrix;
|
||
uniform vec3 cameraPosition;
|
||
|
||
uniform vec2 resolution;
|
||
uniform float time;
|
||
uniform sampler2D texture;
|
||
|
||
void main() {
|
||
vec2 uv = gl_FragCoord.xy / resolution.xy;
|
||
|
||
float v = texture2D( texture, uv ).x;
|
||
|
||
if (v == 1000.) discard;
|
||
v = sqrt(v);
|
||
|
||
gl_FragColor = vec4( vec3( 1. - v / SPREAD ), 1.0 );
|
||
}
|
||
```
|
||
|
||
上述代码压缩结果如下,`define` 统统不见了,变量名也进行了改写:
|
||
```glsl
|
||
uniform highp vec2 resolution;uniform sampler2D texture;void main(){highp vec2 a;a=(gl_FragCoord.xy/resolution);lowp vec4 b;b=texture2D(texture,a);if((b.x==1000.0)){discard;}lowp vec4 c;c.w=1.0;c.xyz=vec3((1.0-(sqrt(b.x)/8.0)));gl_FragColor=c;}
|
||
```
|
||
|
||
当然 glsl-minifier 做的远不止这些,还会应用变量名改写、死代码消除等等优化手段:
|
||
> Optimisations include function inlining, dead code removal, copy propagation, constant folding, constant propagation, arithmetic optimizations and so on. Minifications includes variable rewriting and whitespace trimming.
|
||
|
||
显然这种手段要求我们的 Shader 代码在构建时是稳定的,然而 L7 使用的 GLSL 模块化方案需要在运行时进行模块拼接,如果在构建时代码片段中包含的变量发生了改写,势必影响运行时的拼接结果。另外 minifier 会校验代码的正确性,不理解我们自定义的模块引入语句 `pragma include 'module'` 是一定会报错的。
|
||
|
||
以这样的 Shader 为例:
|
||
```glsl
|
||
#pragma include "project"
|
||
|
||
void main() {
|
||
// 从 project 模块引入方法
|
||
project(position);
|
||
}
|
||
```
|
||
|
||
执行压缩时会报错:
|
||
```bash
|
||
$ node_modules/.bin/glsl-minifier -i ./build/example.frag -o ./build/example.min.frag
|
||
Error:
|
||
(28,2): error: no function with name 'project'
|
||
|
||
Exiting glsl-minifier!
|
||
```
|
||
|
||
因此要想使用这个终极压缩方案,需要修改 L7 目前的 GLSL 模块化方案,代码拼接不能在运行时而需要在构建时完成。但这样就很难兼顾扩展性,毕竟用户自定义图层的 Shader 代码肯定只有运行时才能拿到。
|
||
|
||
所以一个折中的办法是在构建时先对 L7 内置图层的 Shader 代码进行模块化处理,得到最终的 GLSL 文本,然后再 minify。同时保留运行时模块化拼接的能力,应对用户自定义图层。
|
||
|
||
## Submodule
|
||
|
||
npm 和 yarn 只提供了例如 `npm link` 以及 `yarn link` 这样的功能,而 yarn workspaces 只提供了 monorep 需要的底层 link 功能。相比之下 lerna 提供了更高级的功能例如 publish 和 version。因此 yarn workspaces 和 lerna 完全可以组合使用,这也是例如 Jest 等大型项目的使用方式。
|
||
|
||
![](./screenshots/monorep.png)
|
||
|
||
构建命令如下,会在各个 package 下生成 `/lib` 和 `/es` 两个文件夹分别包含 ES2015 和 ESModule 产物:
|
||
```bash
|
||
yarn build
|
||
```
|
||
|
||
### 编译 TS
|
||
|
||
使用 TS 有两种构建方式:
|
||
* native TypeScript with tsc
|
||
* [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript)
|
||
|
||
由于我们的项目中需要使用到一些 babel plugin(装饰器、引入 GLSL 等),因此后者显然是更好的选择。这里我们使用 babel 7 的项目全局配置 configFile。
|
||
|
||
为了合并 ES2015 与 ESModule,我们参考 [redux](https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages#conflating-javascript-modules-and-es2015):
|
||
```json
|
||
// redux package.json
|
||
{
|
||
"main": "lib/redux.js", // ES5 + Common JS
|
||
"module": "es/redux.js", // ES5 + JS Modules
|
||
}
|
||
```
|
||
|
||
开发模式加上 `--watch` 即可。
|
||
* `--root-mode upward` 使用 root 下的 babel 配置文件
|
||
* `--out-dir dist` 输出到 /dist 文件夹下
|
||
* `--delete-dir-on-start` 每次构建前清空,因此不需要 `rimraf`
|
||
```json
|
||
"scripts": {
|
||
"build": "run-p build:*",
|
||
"build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments",
|
||
"build:esm": "BABEL_ENV=esm babel src --root-mode upward --out-dir es --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments"
|
||
},
|
||
```
|
||
|
||
### [WIP] 生成 TS 声明文件
|
||
|
||
和构建前类型检查不同,此时我们需要 tsc 输出类型声明文件了,当然不需要包含 story 和测试用例。
|
||
|
||
### [WIP] 异步加载 Mapbox
|
||
|
||
以 L7 Bundler 方式使用时,由于需要在运行时根据用户配置项选择地图底图,会导致构建时需要将全部地图依赖引入,无法进行 TreeShaking。
|
||
目前高德地图使用运行时异步加载方式引入,不会导致该问题,但 Mapbox 同样使用 Bundler,对于高德用户就多余了。
|
||
|
||
一个可能的方案是对于 Mapbox 使用 CodeSplitting。在容器首次获取 Mapbox 地图服务时异步加载并缓存。 |