8.Stencil项目结构解析
在上文我们已经按照步骤进行了项目的初始化以及体验了新组件的添加过程,想必大家已经对这个框架有了一个初步的认识。所谓「知己知彼、百战不殆」,接下来我们要全面的来了解一下 stencil 初始化项目的结构,以及它的运作原理,并结合我们的具体需求做一些优化。
也是应了那句谚语「磨刀不误砍柴工」,一个充分的准备还是有必要的。所以接下来,我们会逐步分析 Stencil 的项目结构。
初始项目结构概览
Stencil 作为一个开箱即用的软件,在我们 init 的时候,会给我们初始化好文件项目目录,并提供一个 demo 组件供我们测试。我们来全面的分析一下初始化的结构。
// /sten-design/packages/sten-components
// 最外层结构
├── dist
├── loader
├── package.json
├── readme.md
├── src
├── stencil.config.ts
├── tsconfig.json
└── www
首先我们看下最外层的结构。最外层的结构还是比较清晰:
- dist、loader 文件夹是用来存放编译文件,这块也比较重要我们后说。
- src 文件夹主要工程文件。
- stencil.config.ts 也是比较重要的,它承载了 stencil 编译过程中的配置项。
- tsconfig.json 是用来配置 ts 的一些 config,比较常见。
- www 文件夹是在开发过程中启动调试所用到的资源文件,也就是在我们启动本地 dev 环境后,经过 stencil 编译后的 js 文件和 src 中的 index.html 会被执行 copy 过程,移动到 www 文件夹中去,而启动的热服务器(live server)访问就会被映射到此文件夹,从而进行渲染和调试。
以上就是项目的外层文件结构,比较直观,接下来我们看下一些核心的部分。
src 主要逻辑存放文件夹
src 是用来存放我们项目的主要代码逻辑的文件夹,我们先看下它的初始结构:
// src
├── components
│ ├── my-component
│ │ ├── my-component.css
│ │ ├── my-component.e2e.ts
│ │ ├── my-component.spec.ts
│ │ ├── my-component.tsx
│ │ └── readme.md
│ └── sten-button
│ ├── sten-button.css
│ ├── sten-button.tsx
│ └── test
├── components.d.ts
├── index.html
├── index.ts
└── utils
├── utils.spec.ts
└── utils.ts
我们从上到下依次来看:
- Components 文件夹很显然是用来存放我们的每一个组件的内容。
PS:stencil 的编译还是会按照 components/.来匹配组件的路径,所以说,我们还是需要把组件放到 components 里面才可以让 stencil 的编译脚本识别,正确编译。
还有就是会发现,项目初始化自带的组件 my-component 和我们通过命令行添加的 sten-button 项目结构略微有差异,此处是因为 stencil 默认初始化的结构未更新同步所导致,所以还是以命令行生成的 stencil 组件结构为准。
- components.d.ts 这是所有组件的命名空间,是由编译脚本自动生成,我们可以简单看下:
export class MyComponent {
/**
* The first name
*/
@Prop() first: string;
/**
* The middle name
*/
@Prop() middle: string;
/**
* The last name
*/
@Prop() last: string;
@State() num: number = 0;
private getText(): string {
return format(this.first, this.middle, this.last);
}
render() {
return <div><button onClick={() => this.num += 1}>add</button>Hello, World! I'm {this.getText()}, number is {this.num}</div>;
}
}
// 编译后的命名空间
...
export namespace Components {
interface MyComponent {
/**
* The first name
*/
"first": string;
/**
* The last name
*/
"last": string;
/**
* The middle name
*/
"middle": string;
}
interface StenButton {
}
}
...
以上可以看出,它会根据我们组件的 class 自动生成命名空间,并添加公共属性的定义,像 first、last、middle 这种继承属性就会以直接添加到命名空间,而像 num 和 getText 这种私有属性,就不会被定义到里面。
PS:此文件不建议直接更改,会被脚本编译自动覆盖
- index.html 是用来启动本地 dev 环境时候的一个入口文件,在初始化过程中,它会默认添加需要的 js 产物的路径,如下:
<script type="module" src="/build/sten-components.esm.js"></script>
<script nomodule src="/build/sten-components.js"></script>
在启动后,会被 copy 到 www 文件夹提供给服务器访问,而上面依赖的 js 文件也会被拷贝到相应的build 同级文件夹。这样就相当于进行了 dist 文件的引入。可以使我们的组件正确的注册到浏览器当前的运行环境中,可以在 html 直接以标签的形式引入,并正确渲染。如下:
<body>
<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
<sten-button>你好</sten-button>
</body>
- index.ts 作为一个命名空间的导出入口,其作用为编译脚本编译生成type文件提供入口,最后用来整合所有的 *.d.ts 文件。
- utils 文件夹用来存放一些公共的函数和 class,也是我们构建组件库一个比较重要的地方。
www 本地调式资源文件夹
在我们启动本地测试命令 stencil build --dev --watch --serve
后,试着改动下组件的内容后会发现,www 文件夹内的资源文件也随之更新,然后浏览器相应界面的也会随之更新,整体的逻辑可以梳理为:
所以 www 文件夹承载的是调式过程中 一些资源文件的整合,看下结构可以看出:build 文件夹里是编译的产物;host.config.json 会存放一些本地热更新服务器的配置,比如缓存的配置等;而 index.html 则是经过压缩的 src/index.html 文件。
├── build
│ ├── index.esm.js
│ ├── p-14bff9af.entry.js
│ ├── p-250f505a.js
│ ├── p-ddc47192.js
│ ├── sten-button.entry.js
│ ├── sten-components.esm.js
│ └── sten-components.js
├── host.config.json
└── index.html
stencil.config.ts 配置中心
stencil.config.ts 文件几乎是整个 stencil 工具链的配置中心,你可以在此个性化的配置你所有想要的功能,比如:
-
globalScript 全局脚本的路径,可以用来实现在初始化组件,进行挂载的时候进行一些逻辑的处理。
-
globalStyle 全局的样式路径。
-
plugins 插件集合,可以配置一些编译过程中的额外功能,比如 sass 插件可以用来适配 scss 文件的编译,postcss 可以用来压缩、增加一些兼容性的代码来适配各种浏览器等等,官方还提供了以下插件:
-
outputTargets 也是一个比较重要的配置项,可以根据你的配置 target 进行打包。这块我们后面会详细讲一下。
-
等等,还有其他一些配置项。
dist / loader 打包产物
既然 Stencil 作为一个「编译工具」或者可以称为 「工具链」,编译产物 自然而然是最重要的部分,也是我们必须去了解的一个重点。接下来我们就执行一下 pnpm run build
命令来分析下它的编译产物:
// dist
├── cjs
├── collection
├── components
├── esm
├── index.cjs.js
├── index.js
├── sten-components
└── types
// loader
├── cdn.js
├── index.cjs.js
├── index.d.ts
├── index.es2017.js
├── index.js
└── package.json
通过观察 stencil 默认生成的 package.json 文件中 files 自动的配置:
"files": [
"dist/",
"loader/"
],
我们可以看出 stencil 经过编译后,最终输出的文件资源就是 dist 和 loader,那么这两个文件夹有什么关系呢,我们先看下 loader 文件夹 package.json 的内容和其他文件的内容
loader
{
"name": "sten-components-loader",
"typings": "./index.d.ts",
"module": "./index.js",
"main": "./index.cjs.js",
"jsnext:main": "./index.es2017.js",
"es2015": "./index.es2017.js",
"es2017": "./index.es2017.js",
"unpkg": "./cdn.js"
}
// index.es2017.js
export * from '../dist/esm/polyfills/index.js';
export * from '../dist/esm/loader.js';
// index.cjs.js
module.exports = require('../dist/cjs/loader.cjs.js');
module.exports.applyPolyfills = function() { return Promise.resolve() };
可以看出,loader 文件夹其实相当于一个中转站,作用是根据当前的使用环境,分别引入 dist 的不同的文件夹产物内,比如 cjs 的引入类型会加载 ../dist/cjs/loader.cjs.js
;es6+ 的引入会加载 polyfills 文件和 esm 模式的资源文件../dist/esm/loader.js
。这是一个非常 nice 的设计,这样我们可以在任何情况下都能动态加载一些补丁文件和组件的编译产物。
dist
dist 文件可以说是组件库最终的“归宿”。它是可以根据我们的配置变动,在默认的 stencil.config.ts 配置下,打包结果为:
├── cjs
├── collection
├── components
├── esm
├── index.cjs.js
├── index.js
├── sten-components
└── types
- 其中 cjs 和 esm 的逻辑几乎相同,esm 只是多了一些兼容性的代码,它们都是为了适配不同的环境所构造的不同产物,具体的逻辑可以梳理为:
- types 是所有类型的导出集合。
- 因为 stencil 初始化配置里面默认包含了 dist-custom-elements,意思就是可以单独打包出符合 web components 规范的组件 class, 所以打包的文件夹里会有 components 文件夹,里面包含了自定义组件的所有 class,可以以下面这种形式进行单独的引用:
import { HelloWorld } from 'my-library/dist/components/hello-world';
customElements.define('hello-world', HelloWorld);
- Sten-components 文件夹的内容可以看到与 www/build 文件内容相同,因为 stencil 也默认开启了 www 编译的 outTarget,所以也会编译出一份可以直接用于部署的文件 bundle。
总结
通过分析 stencil 的初始项目架构,我们对 stencil 的运作原理也应该有了一些认识,可以看出它对整个js 生态的兼容性还是做的比较充足,可以根据不同的运行环境进行资源文件的引入,并且提供了 polyfills 补充逻辑。
并且 stencil 还提供了丰富的配置项和插件,提供给开发者自定义编译的结果等。想必大家到现在已经对 stencil 有一个初步了解,接下来我们会一步步进入到实战,开始完善我们的组件库。大家尽请期待。