├── aoife-scripts
├── lib
│ └── aoife-app.d.ts
├── template
│ └── README.md
├── template.md
├── template-typescript
│ └── README.md
├── template-typescript.md
├── README.md
├── config
│ ├── jest
│ │ ├── cssTransform.js
│ │ ├── babelTransform.js
│ │ └── fileTransform.js
│ ├── pnpTs.js
│ ├── getHttpsConfig.js
│ ├── modules.js
│ ├── env.js
│ ├── paths.js
│ └── webpackDevServer.config.js
├── LICENSE
├── bin
│ └── aoife-scripts.js
├── package.json
└── scripts
│ ├── test.js
│ ├── utils
│ ├── createJestConfig.js
│ ├── verifyPackageTree.js
│ └── verifyTypeScriptSetup.js
│ ├── start.js
│ ├── build.js
│ ├── eject.js
│ └── init.js
├── create-aoife-app
├── webpack
│ ├── .prettierrc
│ ├── src
│ │ ├── aoife-app.d.ts
│ │ └── index.tsx
│ ├── .eslintignore
│ ├── public
│ │ ├── robots.txt
│ │ ├── favicon.ico
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── index.html
│ ├── webpack.config.js
│ ├── webpackDevServer.config.js
│ ├── .eslintcache
│ ├── package.json
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── .eslintrc.js
│ └── README.md
├── .gitignore
├── vite
│ ├── src
│ │ ├── index.tsx
│ │ ├── module.d.ts
│ │ └── app.tsx
│ ├── package.json
│ ├── index.html
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── .gitignore
│ └── README.md
├── .npmignore
├── package.json
├── bin.js
├── gitignore
├── yarn.lock
├── package-lock.json
└── README.md
├── document
├── aoife.png
├── Untitled.afdesign
├── aoife_design.afdesign
├── md.json
├── md
│ ├── 常用小方法.md
│ ├── 生命周期.md
│ ├── 结束语.md
│ ├── 异步组件.md
│ ├── 开始一段旅途.md
│ ├── JSX 与组件.md
│ ├── 属性.md
│ ├── 状态管理.md
│ ├── 路由及生态.md
│ └── 动态属性.md
├── index.html
└── README.md
├── aoife
├── .prettierrc.js
├── lib
│ ├── helper.ts
│ ├── interface.d.ts
│ ├── index.ts
│ └── parseChildren.ts
├── .eslintrc.js
├── tsconfig.json
├── LICENSE
├── aoife.d.ts
├── .gitignore
├── package.json
├── .npmignore
├── esm
│ └── index.js
├── cjs
│ └── index.js
└── README.md
├── .gitignore
├── update_version.js
├── LICENSE.md
├── README-zh.md
└── README.md
/aoife-scripts/lib/aoife-app.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "auto"
3 | }
--------------------------------------------------------------------------------
/create-aoife-app/webpack/src/aoife-app.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/document/aoife.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/document/aoife.png
--------------------------------------------------------------------------------
/create-aoife-app/.gitignore:
--------------------------------------------------------------------------------
1 | webpack/node_modules
2 | vite/node_modules
3 | **/node_modules
--------------------------------------------------------------------------------
/create-aoife-app/webpack/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | static
3 | dist
4 | public
5 | cypress/fixtures
--------------------------------------------------------------------------------
/document/Untitled.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/document/Untitled.afdesign
--------------------------------------------------------------------------------
/document/aoife_design.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/document/aoife_design.afdesign
--------------------------------------------------------------------------------
/create-aoife-app/webpack/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/create-aoife-app/vite/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "aoife";
2 | import { App } from "./app";
3 |
4 | document.body.append();
5 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | return Object.assign(config, {});
3 | };
4 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/create-aoife-app/webpack/public/favicon.ico
--------------------------------------------------------------------------------
/create-aoife-app/webpack/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/create-aoife-app/webpack/public/logo192.png
--------------------------------------------------------------------------------
/create-aoife-app/webpack/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ymzuiku/aoife/HEAD/create-aoife-app/webpack/public/logo512.png
--------------------------------------------------------------------------------
/aoife-scripts/template/README.md:
--------------------------------------------------------------------------------
1 | This file has moved [here](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/README.md)
2 |
--------------------------------------------------------------------------------
/aoife-scripts/template.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | This file has moved [here](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/README.md)
4 |
5 |
--------------------------------------------------------------------------------
/create-aoife-app/vite/src/module.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "@babel/standalone" {
4 | const content: any;
5 | export default content;
6 | }
7 |
--------------------------------------------------------------------------------
/aoife-scripts/template-typescript/README.md:
--------------------------------------------------------------------------------
1 | This file has moved [here](https://github.com/facebook/create-react-app/blob/master/packages/cra-template-typescript/template/README.md)
2 |
--------------------------------------------------------------------------------
/aoife-scripts/template-typescript.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | This file has moved [here](https://github.com/facebook/create-react-app/blob/master/packages/cra-template-typescript/template/README.md)
4 |
5 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/webpackDevServer.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | return Object.assign(config, {
3 | proxy: {
4 | "/v1": "http://localhost:4080",
5 | },
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/create-aoife-app/.npmignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | project/node_modules
4 | **/app/uploads/*
5 | # dependencies
6 | **/node_modules
7 | **/.pnp
8 | **/.pnp.js
9 | **/server/tmp
10 |
--------------------------------------------------------------------------------
/aoife/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 2,
4 | singleQuote: false,
5 | semi: true,
6 | trailingComma: 'es5',
7 | bracketSpacing: true,
8 | jsxBracketSameLine: true,
9 | arrowParens: 'always',
10 | parser: 'typescript'
11 | };
--------------------------------------------------------------------------------
/create-aoife-app/vite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aoife-project",
3 | "version": "2.0.13",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build"
7 | },
8 | "dependencies": {
9 | "aoife": "^2.0.13"
10 | },
11 | "devDependencies": {
12 | "vite": "^2.2.4"
13 | }
14 | }
--------------------------------------------------------------------------------
/document/md.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "aoife",
3 | "homepage": "https://github.com/ymzuiku/aoife",
4 | "version": "2.0.7",
5 | "path": "/md/",
6 | "files": [
7 | "开始一段旅途",
8 | "JSX 与组件",
9 | "属性",
10 | "动态属性",
11 | "状态管理",
12 | "异步组件",
13 | "生命周期",
14 | "路由及生态",
15 | "结束语"
16 | ]
17 | }
--------------------------------------------------------------------------------
/create-aoife-app/vite/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {
13 | console.log("每当 aoife.next() 找到到此元素及元素父类都会执行");
14 | }}
15 | onAppend={() => {
16 | console.log("out已插入到页面中");
17 | }}
18 | onEntry={() => {
19 | console.log("out已从屏幕外面进入到屏幕中");
20 | }}
21 | onRemove={() => {
22 | console.log("out已从页面中移除");
23 | }}
24 | />
25 | );
26 |
27 | // 注意以上生命周期都需要在元素在 append 之前声明
28 | document.body.append(out);
29 | ```
30 |
31 | 虽然 aoife 提供了极简的生命周期,但是 aoife 一直强调一个概念,在浏览器中,99%的场景不需要生命周期钩子,因为 DOM 对象已经帮我们管理了最关键的状态。
32 |
33 | 在 aoife 中,可以利用 aoife 自身的事件派发机制,我们不需要取消订阅,组件销毁后订阅也不会继续发生。
34 |
35 | ```jsx
36 | // aoife 例子
37 | function Welcome({ name }) {
38 | return (
39 |
{
41 | console.log("do someting");
42 | }}
43 | >
44 | Hello, {name}
45 |
46 | );
47 | }
48 |
49 | // 派发任务,若组件销毁,组件内部自然不会接收到订阅
50 | aoife.next("h1");
51 | ```
52 |
--------------------------------------------------------------------------------
/aoife/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Pillar.Liang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/aoife-scripts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2013-present, Facebook, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/aoife/aoife.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // declare module "*.css" {
4 | // const content: string;
5 | // export default content;
6 | // }
7 |
8 | // declare module "*.less" {
9 | // const content: string;
10 | // export default content;
11 | // }
12 |
13 | // declare module "*.sass" {
14 | // const content: string;
15 | // export default content;
16 | // }
17 |
18 | // declare module "*.md" {
19 | // const content: string;
20 | // export default content;
21 | // }
22 |
23 | // declare module "*.text" {
24 | // const content: string;
25 | // export default content;
26 | // }
27 |
28 | // declare module "@babel/*" {
29 | // const content: any;
30 | // export default content;
31 | // }
32 |
33 | // declare module "@babel/standalone" {
34 | // const content: any;
35 | // export default content;
36 | // }
37 |
38 | // declare module "@babel/preset-react" {
39 | // const content: any;
40 | // export default content;
41 | // }
42 |
43 | // declare module "babel-preset-minify" {
44 | // const content: any;
45 | // export default content;
46 | // }
47 |
48 | import aoife from "./lib";
49 |
50 | export default aoife;
51 |
--------------------------------------------------------------------------------
/document/md/结束语.md:
--------------------------------------------------------------------------------
1 | # 结束语
2 |
3 | 通过以上几篇简短的教程,我们应该已经完全掌握了 aoife,因为它太简单。
4 |
5 | aoife 的理念是享受 aoife 的简单。这样我们可以更好的把学习重心放在浏览器的 API 和 JS 本身,把工作重心放在业务本身,而不是内卷的学习那些迟早会更新过时的概念上。
6 |
7 | ### 生态扩展
8 |
9 | - [vanilla-route](https://github.com/ymzuiku/vanilla-route.git): 极简 aoife 的路由,由于可以用在非 aoife 项目,所以更名为 vanilla-route
10 | - [vanilla-message](https://github.com/ymzuiku/vanilla-message.git): 极简原生 Message 库,可以和 aoife 很好的配合
11 | - [vanilla-pop](https://github.com/ymzuiku/vanilla-pop.git): tippy.js 的封装,可以和 aoife 很好的配合
12 | - [aoife-doc](https://github.com/ymzuiku/aoife-doc.git): aoife 编写的文档生成器; 本文档是由 aoife-doc 生成
13 | - [aoife-ux](https://github.com/ymzuiku/aoife-ux.git): [开发中]精炼的组件库,自适应移动端和桌面端,希望有社区爱好者的扶持
14 | - [flavorcss](https://github.com/ymzuiku/flavorcss.git): 原生运行时的原子类 css 库,可以和 aoife 很好的配合
15 |
16 | 更多生态,希望有社区爱好者的扶持
17 |
18 | ### 稳定 API
19 |
20 | aoife 以下 API 已经稳定(aoife 公开的 API 会持续保持精简), 请放心使用:
21 |
22 | - JSX 渲染
23 | - aoife.next
24 | - aoife.attributeKeys: 扩展默认 setAttribute 属性
25 | - onAppend
26 | - onEntry
27 | - onRemove
28 | - onUpdate
29 |
30 | aoife 承诺若有关键性的 bug,会立刻响应修复,也更欢迎您提 PR。
31 |
32 | ### 最后
33 |
34 | 轻松的旅途总是短暂的,aoife 仅仅给予了您一个简单的开始。它的初衷是让您放下不必要的概念,焦距本质。
35 |
36 | 最后,欢迎 Star [https://github.com/ymzuiku/aoife.git](https://github.com/ymzuiku/aoife.git)
37 |
38 | 欢迎您的宝贵建议。
39 |
--------------------------------------------------------------------------------
/aoife-scripts/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/document/md/异步组件.md:
--------------------------------------------------------------------------------
1 | ## 异步组件
2 |
3 | aoife 可以异步返回组件,这可以简化远程获取数据渲染的业务。
4 |
5 | ```jsx
6 | import "aoife";
7 |
8 | // 模拟一个阻塞方法,如请求某些东西
9 |
10 | async function SlowPage() {
11 | // 阻塞 1000 ms 获得一个值
12 | const label = await new Promise((res) =>
13 | setTimeout(() => res("world"), 1000)
14 | );
15 |
16 | // 等待阻塞结束之后才返回此元素
17 | return
异步渲染 {label}
;
18 | }
19 |
20 | function App() {
21 | // SlowPage 是一个异步组件
22 | return (
23 |
24 |
hello
25 |
26 |
27 | );
28 | }
29 |
30 | // 注意,根组件不可以是异步组件,因为 document.body.append 不支持 Promise 对象
31 | document.body.append(
);
32 | ```
33 |
34 | ## 异步属性
35 |
36 | 同理异步组件,aoife 可以异步属性取值和异步插入 children。
37 |
38 | 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步的回调
39 |
40 | ```jsx
41 | import "aoife";
42 |
43 | function App() {
44 | return (
45 |
46 |
{
49 | // 异步取值
50 | return new Promise((res) => {
51 | setTimeout(() => res("hello"), 500);
52 | });
53 | }}
54 | />
55 | {() => {
56 | // 异步插入元素
57 | return new Promise((res) => {
58 | setTimeout(() => {
59 | res(
list-a
);
60 | }, 1000);
61 | });
62 | }}
63 | {() => {
64 | // 异步插入元素
65 | return new Promise((res) => {
66 | setTimeout(() => {
67 | res(
list-b
);
68 | }, 300);
69 | });
70 | }}
71 |
72 | );
73 | }
74 |
75 | document.body.append(
);
76 | ```
77 |
--------------------------------------------------------------------------------
/document/md/开始一段旅途.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | > 本文档由 [aoife-doc](https://aoife-doc.writeflowy.com) 生成
4 |
5 | # 开始一段旅途
6 |
7 | 欢迎来到 aoife 之旅,这是短暂且轻松的旅途,我希望您丢掉包袱,感受微风。相信我,aoife 中没有难以理解的概念,一切都很简单。
8 |
9 | ## 安装及开始
10 |
11 | ```bash
12 | $ npm init aoife-app my-project
13 | $ cd my-project
14 | $ yarn install
15 | $ yarn dev
16 | ```
17 |
18 | 创建并启动好工程,我们就可以开始学习 aoife 之旅,若你有 React 基础或明白 JSX 语法,相信你可以在 10 - 15 分钟完成这段旅程。
19 |
20 | ## 减少复杂度
21 |
22 | > 我们已经有了 React/Vue/Angular, 为什么还需要 aoife?
23 |
24 | 现代前端框架 (如 React / Vue) 带来了非常多新概念,但是却隔离了 DOM 的操作。
25 |
26 | 其实现代 DOM 的 API 已经非常优秀,利用原生 DOM 开发的组件、模块生命力极强,可以用在任何高级框架中,并且 API 稳定性极强。
27 |
28 | aoife 的目标是**移除**现代前端开发的复杂度,在此同时**保留**现代前端工程的优秀特性。
29 |
30 | aoife 是一个原生 JS 开发框架,或者叫 Vanilla JS 框架,我们完全抛弃了框架的生命周期的概念,保留了声明式的特性,利用原生 HTMLElement 进行组件封装来确保跨框架的组件生命力。
31 |
32 | 操作 DOM 带来了比使用虚拟 DOM 更强大的能力及性能,而其中的关键是我们如何优雅的创建和操作 DOM,所以 aoife 其实并不是一款框架,内部仅仅是实现了一些 JSX 渲染原生 HTML 的方法、 HTML 更新的方法,这已足够开发任何复杂前端项目了。
33 |
34 | ## 特性
35 |
36 | - 一切基于原生元素,原生元素即组件
37 | - 无虚拟 DOM
38 | - 声明式
39 | - 异步组件
40 | - 可选择的为原生元素添加生命周期
41 | - 普通对象即状态
42 | - 高性能更新:零额外重绘
43 | - 基于您熟悉的 JSX
44 | - 轻量,承诺体积永远小于 10 kb(gzip)
45 |
46 | ## 远离疲倦
47 |
48 | 近年以来,React Hooks 已经普及,Vue 也已发布 Vue 3.0。社区为此需要更新非常多的相关库,行业人员需要学习全新的概念。未来还会有其他新版本,周而复始。而这些都是各类框架提供的概念,我们为此反复奔波学习,前端的本质的 DOM API 却越来越生疏。
49 |
50 | aoife 借助于 JSX 语法和原生 DOM API,它的核心是组织 JSX 和 DOM API,简单意味着生命强、兼容性强。这使得我们得以把核心放在业务、和基础技能的提升,远离疲倦。
51 |
52 | 前端开发最重要解决的两个核心问题:
53 |
54 | - 如何优雅的创建 DOM 树?
55 | - 如何优雅的更新 DOM 树?
56 |
57 | 我们接下来一步步引导大家使用 aoife 解决这两个核心问题。
58 |
59 | ## aoife 非常需要您的关注
60 |
61 | 欢迎 Star [https://github.com/ymzuiku/aoife.git](https://github.com/ymzuiku/aoife.git)
62 |
--------------------------------------------------------------------------------
/aoife/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 |
9 | # testing
10 | /coverage
11 | **/coverage
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # OSX
28 | #
29 | .DS_Store
30 |
31 | **/dll
32 | **/__pycache__
33 | **/.cache
34 | dist
35 | **/dist
36 | **/build
37 | **/ignores
38 | .webpack_cache
39 | **/.webpack_cache
40 | .viminfo
41 | .vimsession
42 | *.sln
43 | .idea
44 |
45 | # Xcode
46 |
47 | build/
48 | *.pbxuser
49 | !default.pbxuser
50 | *.mode1v3
51 | !default.mode1v3
52 | *.mode2v3
53 | !default.mode2v3
54 | *.perspectivev3
55 | !default.perspectivev3
56 | xcuserdata
57 | *.xccheckout
58 | *.moved-aside
59 | DerivedData
60 | *.hmap
61 | *.ipa
62 | *.xcuserstate
63 | project.xcworkspace
64 |
65 | # Android/IntelliJ
66 | #
67 | build/
68 | .idea
69 | .gradle
70 | local.properties
71 | *.iml
72 |
73 | #
74 | node_modules/
75 | **/node_modules/
76 | npm-debug.log
77 | yarn-error.log
78 |
79 | # BUCK
80 | buck-out/
81 | .buckd/
82 | *.keystore
83 |
84 | # fastlane
85 | #
86 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
87 | # screenshots whenever they are needed.
88 | # For more information about the recommended setup visit:
89 | # https://docs.fastlane.tools/best-practices/source-control/
90 |
91 | */fastlane/report.xml
92 | */fastlane/Preview.html
93 | */fastlane/screenshots
94 |
95 | # VSCode Plugins
96 | .project
97 | .classpath
98 | .settings/
99 |
100 | dist/v*/*
101 |
102 | .vscode/
103 | android/app/release/
104 |
105 | .eslintcache
--------------------------------------------------------------------------------
/create-aoife-app/webpack/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
Aoife App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/create-aoife-app/gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | **/node_modules
6 | /.pnp
7 | .pnp.js
8 | .eslintcache
9 |
10 |
11 | # testing
12 | /coverage
13 | **/coverage
14 |
15 | # production
16 | /build
17 | /dist
18 | /dist-ssr
19 | *.local
20 |
21 | # misc
22 | .DS_Store
23 | .env.local
24 | .env.development.local
25 | .env.test.local
26 | .env.production.local
27 |
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # OSX
33 | #
34 | .DS_Store
35 |
36 | **/dll
37 | **/__pycache__
38 | **/.cache
39 | dist
40 | **/dist
41 | **/build
42 | **/ignores
43 | .webpack_cache
44 | **/.webpack_cache
45 | .viminfo
46 | .vimsession
47 | *.sln
48 | .idea
49 |
50 | # Xcode
51 |
52 | build/
53 | *.pbxuser
54 | !default.pbxuser
55 | *.mode1v3
56 | !default.mode1v3
57 | *.mode2v3
58 | !default.mode2v3
59 | *.perspectivev3
60 | !default.perspectivev3
61 | xcuserdata
62 | *.xccheckout
63 | *.moved-aside
64 | DerivedData
65 | *.hmap
66 | *.ipa
67 | *.xcuserstate
68 | project.xcworkspace
69 |
70 | # Android/IntelliJ
71 | #
72 | build/
73 | .idea
74 | .gradle
75 | local.properties
76 | *.iml
77 |
78 | #
79 | node_modules/
80 | **/node_modules/
81 | npm-debug.log
82 | yarn-error.log
83 |
84 | # BUCK
85 | buck-out/
86 | .buckd/
87 | *.keystore
88 |
89 | # fastlane
90 | #
91 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
92 | # screenshots whenever they are needed.
93 | # For more information about the recommended setup visit:
94 | # https://docs.fastlane.tools/best-practices/source-control/
95 |
96 | */fastlane/report.xml
97 | */fastlane/Preview.html
98 | */fastlane/screenshots
99 | .eslintcache
100 | # VSCode Plugins
101 | .project
102 | .classpath
103 | .settings/
104 |
105 | dist/v*/*
106 |
107 | .vscode/
108 | android/app/release/
109 |
110 | .eslintcache
--------------------------------------------------------------------------------
/create-aoife-app/vite/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | **/node_modules
6 | /.pnp
7 | .pnp.js
8 | .eslintcache
9 |
10 |
11 | # testing
12 | /coverage
13 | **/coverage
14 |
15 | # production
16 | /build
17 | /dist
18 | /dist-ssr
19 | *.local
20 |
21 | # misc
22 | .DS_Store
23 | .env.local
24 | .env.development.local
25 | .env.test.local
26 | .env.production.local
27 |
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # OSX
33 | #
34 | .DS_Store
35 |
36 | **/dll
37 | **/__pycache__
38 | **/.cache
39 | dist
40 | **/dist
41 | **/build
42 | **/ignores
43 | .webpack_cache
44 | **/.webpack_cache
45 | .viminfo
46 | .vimsession
47 | *.sln
48 | .idea
49 |
50 | # Xcode
51 |
52 | build/
53 | *.pbxuser
54 | !default.pbxuser
55 | *.mode1v3
56 | !default.mode1v3
57 | *.mode2v3
58 | !default.mode2v3
59 | *.perspectivev3
60 | !default.perspectivev3
61 | xcuserdata
62 | *.xccheckout
63 | *.moved-aside
64 | DerivedData
65 | *.hmap
66 | *.ipa
67 | *.xcuserstate
68 | project.xcworkspace
69 |
70 | # Android/IntelliJ
71 | #
72 | build/
73 | .idea
74 | .gradle
75 | local.properties
76 | *.iml
77 |
78 | #
79 | node_modules/
80 | **/node_modules/
81 | npm-debug.log
82 | yarn-error.log
83 |
84 | # BUCK
85 | buck-out/
86 | .buckd/
87 | *.keystore
88 |
89 | # fastlane
90 | #
91 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
92 | # screenshots whenever they are needed.
93 | # For more information about the recommended setup visit:
94 | # https://docs.fastlane.tools/best-practices/source-control/
95 |
96 | */fastlane/report.xml
97 | */fastlane/Preview.html
98 | */fastlane/screenshots
99 | .eslintcache
100 | # VSCode Plugins
101 | .project
102 | .classpath
103 | .settings/
104 |
105 | dist/v*/*
106 |
107 | .vscode/
108 | android/app/release/
109 |
110 | .eslintcache
--------------------------------------------------------------------------------
/create-aoife-app/webpack/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | **/node_modules
6 | /.pnp
7 | .pnp.js
8 | .eslintcache
9 |
10 |
11 | # testing
12 | /coverage
13 | **/coverage
14 |
15 | # production
16 | /build
17 | /dist
18 | /dist-ssr
19 | *.local
20 |
21 | # misc
22 | .DS_Store
23 | .env.local
24 | .env.development.local
25 | .env.test.local
26 | .env.production.local
27 |
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # OSX
33 | #
34 | .DS_Store
35 |
36 | **/dll
37 | **/__pycache__
38 | **/.cache
39 | dist
40 | **/dist
41 | **/build
42 | **/ignores
43 | .webpack_cache
44 | **/.webpack_cache
45 | .viminfo
46 | .vimsession
47 | *.sln
48 | .idea
49 |
50 | # Xcode
51 |
52 | build/
53 | *.pbxuser
54 | !default.pbxuser
55 | *.mode1v3
56 | !default.mode1v3
57 | *.mode2v3
58 | !default.mode2v3
59 | *.perspectivev3
60 | !default.perspectivev3
61 | xcuserdata
62 | *.xccheckout
63 | *.moved-aside
64 | DerivedData
65 | *.hmap
66 | *.ipa
67 | *.xcuserstate
68 | project.xcworkspace
69 |
70 | # Android/IntelliJ
71 | #
72 | build/
73 | .idea
74 | .gradle
75 | local.properties
76 | *.iml
77 |
78 | #
79 | node_modules/
80 | **/node_modules/
81 | npm-debug.log
82 | yarn-error.log
83 |
84 | # BUCK
85 | buck-out/
86 | .buckd/
87 | *.keystore
88 |
89 | # fastlane
90 | #
91 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
92 | # screenshots whenever they are needed.
93 | # For more information about the recommended setup visit:
94 | # https://docs.fastlane.tools/best-practices/source-control/
95 |
96 | */fastlane/report.xml
97 | */fastlane/Preview.html
98 | */fastlane/screenshots
99 | .eslintcache
100 | # VSCode Plugins
101 | .project
102 | .classpath
103 | .settings/
104 |
105 | dist/v*/*
106 |
107 | .vscode/
108 | android/app/release/
109 |
110 | .eslintcache
--------------------------------------------------------------------------------
/document/md/JSX 与组件.md:
--------------------------------------------------------------------------------
1 | # JSX 与组件
2 |
3 | 经过前端这些年的历程,社区普遍认可 JSX 是轻松高效描述 DOM 的方案,所以 aoife 选择使用 JSX 来描述 DOM。
4 |
5 | `注意 aoife 并不是 React 的轮子`,aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
6 |
7 | 本章我们会阐述一些 JSX 语法,并且说清楚 JSX 在 aoife 的关系。
8 |
9 | ## JSX 表达式
10 |
11 | 首先 JSX 是一个表达式,即 JSX 会得到一个值。
12 |
13 | 在 aoife 中,每个 JSX 表达式的值是一个原生 HTMLElement , 如 `
` 表达式的值是一个 Div 元素,所以可以直接 append 到 document.body 中:
14 |
15 | ```jsx
16 | const hello =
Hello world
;
17 |
18 | document.body.append(hello);
19 | ```
20 |
21 | 以上代码等价于原生 JS 代码:
22 |
23 | ```jsx
24 | const hello = document.createElement("div");
25 | hello.id = "foo";
26 | hello.textContent = "Hello world";
27 |
28 | document.body.append(hello);
29 | ```
30 |
31 | ## 组件约定
32 |
33 | aoife 的组件有两个基本约定:
34 |
35 | 1. 任何返回 HTMLElement 的函数,都可以是 aoife 的组件
36 | 2. 组件只会读取第一个参数,参数类型是一个 object,参数是可选的
37 |
38 | 由于组件不继承任何对象,所以实现 aoife 的组件的第三方库可以不需要引入 aoife。甚至在 aoife 诞生之前,JS 的世界中就已经有许许多多 aoife 组件了。
39 |
40 | ## 一个简单的组件
41 |
42 | 由于 JSX 表达式在 aoife 中就是一个 HTMLElement 声明语法,所以 aoife 的组件只是一个普通的函数,函数的返回值是一个 JSX 表达式。
43 |
44 | ```jsx
45 | const App = () => {
46 | return
Hello world
;
47 | };
48 |
49 | document.body.append(
);
50 | ```
51 |
52 | 同时我们可以放心,在 aoife 中没有 React.hooks 的概念,我们不需要担心这个组件函数最后会变得很复杂。
53 |
54 | ## 原生 HTMLElement 和 JSX 可以混合使用
55 |
56 | 我们使用 createElement 创建一个普通的 div 元素,然后在 JSX 中直接使用它:
57 |
58 | ```jsx
59 | const world = document.createElement("div");
60 | world.id = "foo";
61 | world.textContent = "world";
62 |
63 | function App() {
64 | return
hello {world}
;
65 | }
66 |
67 | document.body.append(
);
68 | ```
69 |
70 | 我们也可以将 HTMLElement 封装成一个 aoife 组件:
71 |
72 | ```jsx
73 | // 纯原生的组件
74 | function World() {
75 | const world = document.createElement("div");
76 | world.id = "foo";
77 | world.textContent = "world";
78 | return world;
79 | }
80 |
81 | function App() {
82 | return (
83 |
84 | hello
85 |
86 | );
87 | }
88 |
89 | document.body.append(
);
90 | ```
91 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true,
5 | node: true,
6 | "jest/globals": true,
7 | },
8 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
9 | parser: "@typescript-eslint/parser",
10 | parserOptions: {
11 | ecmaVersion: 11,
12 | sourceType: "module",
13 | },
14 | plugins: ["prettier", "@typescript-eslint", "jest"],
15 | globals: {
16 | $: true,
17 | test: true,
18 | },
19 | rules: {
20 | "no-async-promise-executor": 0,
21 | "jest/no-disabled-tests": "warn",
22 | "jest/no-focused-tests": "error",
23 | "jest/no-identical-title": "error",
24 | "jest/prefer-to-have-length": "warn",
25 | "jest/valid-expect": "error",
26 | "prettier/prettier": 1,
27 | "no-empty": 0,
28 | "no-constant-condition": 0,
29 | indent: ["error", 2],
30 | "linebreak-style": ["error", "unix"],
31 | // quotes: ["error", "double"],
32 | semi: ["error", "always"],
33 | "no-console": [
34 | "warn",
35 | {
36 | allow: ["warn", "error"],
37 | },
38 | ],
39 | eqeqeq: ["warn", "always"],
40 | "prefer-const": [
41 | "error",
42 | {
43 | destructuring: "all",
44 | ignoreReadBeforeAssign: true,
45 | },
46 | ],
47 | "@typescript-eslint/ban-types": 0,
48 | "@typescript-eslint/no-empty-function": 0,
49 | "@typescript-eslint/no-non-null-assertion": 0,
50 | "@typescript-eslint/no-var-requires": 0,
51 | "@typescript-eslint/no-explicit-any": 0,
52 | "@typescript-eslint/explicit-module-boundary-types": "off",
53 | "@typescript-eslint/indent": 0,
54 | "@typescript-eslint/no-unused-vars": 1,
55 | "@typescript-eslint/interface-name-prefix": 0,
56 | "@typescript-eslint/explicit-member-accessibility": 0,
57 | "@typescript-eslint/no-triple-slash-reference": 0,
58 | "@typescript-eslint/ban-ts-ignore": 0,
59 | "@typescript-eslint/no-this-alias": 0,
60 | "@typescript-eslint/triple-slash-reference": 0,
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/aoife/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aoife",
3 | "version": "2.0.14",
4 | "main": "./lib/index.ts",
5 | "types": "aoife.d.ts",
6 | "exports": {
7 | "import": "./lib/index.ts"
8 | },
9 | "private": false,
10 | "scripts": {
11 | "lint": "eslint --ext .tsx,.ts --fix ./lib",
12 | "cp_readme": "cp -rf ../README.md ./README.md && cp -rf ../README.md ../create-aoife-app/README.md",
13 | "update_version": "node ../update_version.js",
14 | "esm": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" lib/index.ts --outdir=esm --target=es6 --bundle --external:vanilla-ob --external:vanilla-life --external:vanilla-svg-tags --format=esm --minify --splitting",
15 | "cjs": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" lib/index.ts --outdir=cjs --target=es6 --bundle --external:vanilla-ob --external:vanilla-life --external:vanilla-svg-tags --format=cjs --minify",
16 | "lib": "yarn cp_readme && yarn esm && yarn cjs && yarn update_version"
17 | },
18 | "husky": {
19 | "hooks": {
20 | "pre-commit": "lint-staged"
21 | }
22 | },
23 | "lint-staged": {
24 | "./{lib}/**/*.{ts,tsx}": [
25 | "npm run lint",
26 | "prettier --write",
27 | "git add"
28 | ]
29 | },
30 | "devDependencies": {
31 | "@typescript-eslint/eslint-plugin": "^3.7.0",
32 | "@typescript-eslint/parser": "^3.7.0",
33 | "@typescript-eslint/typescript-estree": "^4.22.0",
34 | "babel-eslint": "^10.1.0",
35 | "eslint": "^7.5.0",
36 | "eslint-config-prettier": "^6.11.0",
37 | "eslint-plugin-jsx-control-statements": "^2.2.1",
38 | "eslint-plugin-prettier": "^3.1.4",
39 | "fs-extra": "^9.1.0",
40 | "husky": "^4.2.5",
41 | "lint-staged": "^10.2.11",
42 | "prettier": "^2.0.5",
43 | "tslib": "^2.3.1",
44 | "typescript": "^4.2.4"
45 | },
46 | "repository": {
47 | "type": "git",
48 | "url": "git+https://github.com/ymzuiku/aoife.git"
49 | },
50 | "bugs": {
51 | "url": "git+https://github.com/ymzuiku/aoife.git"
52 | },
53 | "dependencies": {
54 | "vanilla-life": "^0.1.11",
55 | "vanilla-ob": "^0.4.0",
56 | "vanilla-svg-tags": "^0.1.3"
57 | }
58 | }
--------------------------------------------------------------------------------
/aoife-scripts/bin/aoife-scripts.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | "use strict";
10 |
11 | // Makes the script crash on unhandled rejections instead of silently
12 | // ignoring them. In the future, promise rejections that are not handled will
13 | // terminate the Node.js process with a non-zero exit code.
14 | process.on("unhandledRejection", (err) => {
15 | throw err;
16 | });
17 |
18 | const spawn = require("react-dev-utils/crossSpawn");
19 | const args = process.argv.slice(2);
20 |
21 | const scriptIndex = args.findIndex(
22 | (x) => x === "build" || x === "eject" || x === "start" || x === "test"
23 | );
24 | const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
25 | const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
26 |
27 | if (["build", "eject", "start", "test"].includes(script)) {
28 | const result = spawn.sync(
29 | process.execPath,
30 | nodeArgs
31 | .concat(require.resolve("../scripts/" + script))
32 | .concat(args.slice(scriptIndex + 1)),
33 | { stdio: "inherit" }
34 | );
35 | if (result.signal) {
36 | if (result.signal === "SIGKILL") {
37 | console.log(
38 | "The build failed because the process exited too early. " +
39 | "This probably means the system ran out of memory or someone called " +
40 | "`kill -9` on the process."
41 | );
42 | } else if (result.signal === "SIGTERM") {
43 | console.log(
44 | "The build failed because the process exited too early. " +
45 | "Someone might have called `kill` or `killall`, or the system could " +
46 | "be shutting down."
47 | );
48 | }
49 | process.exit(1);
50 | }
51 | process.exit(result.status);
52 | } else {
53 | console.log('Unknown script "' + script + '".');
54 | console.log("Perhaps you need to update react-scripts?");
55 | console.log(
56 | "See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases"
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/aoife-scripts/config/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | // @remove-on-eject-begin
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | // @remove-on-eject-end
9 | 'use strict';
10 |
11 | const fs = require('fs');
12 | const path = require('path');
13 | const crypto = require('crypto');
14 | const chalk = require('react-dev-utils/chalk');
15 | const paths = require('./paths');
16 |
17 | // Ensure the certificate and key provided are valid and if not
18 | // throw an easy to debug error
19 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
20 | let encrypted;
21 | try {
22 | // publicEncrypt will throw an error with an invalid cert
23 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
24 | } catch (err) {
25 | throw new Error(
26 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
27 | );
28 | }
29 |
30 | try {
31 | // privateDecrypt will throw an error with an invalid key
32 | crypto.privateDecrypt(key, encrypted);
33 | } catch (err) {
34 | throw new Error(
35 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
36 | err.message
37 | }`
38 | );
39 | }
40 | }
41 |
42 | // Read file and throw an error if it doesn't exist
43 | function readEnvFile(file, type) {
44 | if (!fs.existsSync(file)) {
45 | throw new Error(
46 | `You specified ${chalk.cyan(
47 | type
48 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
49 | );
50 | }
51 | return fs.readFileSync(file);
52 | }
53 |
54 | // Get the https config
55 | // Return cert files if provided in env, otherwise just true or false
56 | function getHttpsConfig() {
57 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
58 | const isHttps = HTTPS === 'true';
59 |
60 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
61 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
62 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
63 | const config = {
64 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
65 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
66 | };
67 |
68 | validateKeyAndCerts({ ...config, keyFile, crtFile });
69 | return config;
70 | }
71 | return isHttps;
72 | }
73 |
74 | module.exports = getHttpsConfig;
75 |
--------------------------------------------------------------------------------
/document/md/属性.md:
--------------------------------------------------------------------------------
1 | ## 属性
2 |
3 | 在 aoife 一个属性的属性其实仅仅是给 HTMLElement 元素进行赋值
4 |
5 | ```jsx
6 | const ele = (
7 |
13 | );
14 | ```
15 |
16 | 以上代码等同于
17 |
18 | ```jsx
19 | const ele = document.createElement("div");
20 | ele.id = "page";
21 | ele.className = "page";
22 | ele.setAttribute("tab-index", 10);
23 | ele.style.background = "#f33";
24 | ```
25 |
26 | 在 HTMLElement 中,使用 `.` 取属性赋值和使用 `setAttribute`进行赋值的行为不一定是一致的。
27 | 在 aoife 中,若属性中有 `-` 字符, 会使用 `setAttribute` 进行设置属性,否则使用 `.` 取属性赋值.
28 |
29 | 若要扩展 aoife 默认使用 setAttribute 的属性,可以在 aoife.attributeKeys 中增加属性为 true:
30 |
31 | ```js
32 | aoife.attributeKeys.other = true;
33 | console.log(aoife.attributeKeys); // 查看默认使用 setAttribute 的属性
34 | ```
35 |
36 | ## 事件
37 |
38 | 若一个属性为 'on' 开头,我们认为它是一个事件,这和 HTML 原生事件保持一致,我们可以赋予一个函数:
39 |
40 | ```jsx
41 | document.body.append(
42 |
43 | );
44 | ```
45 |
46 | ## 组件 Props
47 |
48 | 组件的 Props 其实是 JSX 的基础特性,JSX 会获取 标签上的属性,并且组合成一个 Object 对象作为参数传递到函数中.
49 |
50 | aoife 的 Props 和 React 的保持一致,只是没有做特殊的约束,仅仅是一个数据传递
51 |
52 | ```jsx
53 | function App({ name }) {
54 | return
hello {name}
;
55 | }
56 |
57 | document.body.append(
);
58 | ```
59 |
60 | ## children 属性
61 |
62 | children 是 Props 中的一个属性,它会获取外部传递的子 JSX 对象,和 React 不同 children,aoife 的 children 永远是一个数组.
63 |
64 | ```jsx
65 | function App({ children }) {
66 | return
hello {children}
;
67 | }
68 |
69 | document.body.append(
70 |
71 | dog
72 | cat
73 |
74 | );
75 | ```
76 |
77 | 我们也可以将 children 分开使用:
78 |
79 | ```jsx
80 | function App({ children }) {
81 | return (
82 |
83 |
{children[0]}
84 |
85 |
86 | );
87 | }
88 |
89 | document.body.append(
90 |
91 | dog
92 | cat
93 |
94 | );
95 | ```
96 |
97 | ## style 属性
98 |
99 | `style` 属性在原生 html 中是一个字符串:
100 |
101 | ```jsx
102 | function App({ children }) {
103 | return
hello
;
104 | }
105 |
106 | document.body.append(
);
107 | ```
108 |
109 | 但是为了照顾 React 的用户,aoife 支持 `style` 属性为一个对象:
110 |
111 | ```jsx
112 | function App({ children }) {
113 | return
hello
;
114 | }
115 |
116 | document.body.append(
);
117 | ```
118 |
119 | > PS: 在 aoife 设计过程中,曾经为 style 扩展了 CSS-IN-JS 特性,但是思量许久,认为保持简单才是正确的
120 |
121 | > 未来 aoife 会设计一个优雅且简单的扩展方式,让社区做扩展
122 |
--------------------------------------------------------------------------------
/create-aoife-app/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/fs-extra@^9.0.5":
6 | version "9.0.5"
7 | resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.5.tgz#2afb76a43a4bef80a363b94b314d0ca1694fc4f8"
8 | integrity sha512-wr3t7wIW1c0A2BIJtdVp4EflriVaVVAsCAIHVzzh8B+GiFv9X1xeJjCs4upRXtzp7kQ6lP5xvskjoD4awJ1ZeA==
9 | dependencies:
10 | "@types/node" "*"
11 |
12 | "@types/node@*":
13 | version "14.14.14"
14 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
15 | integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==
16 |
17 | at-least-node@^1.0.0:
18 | version "1.0.0"
19 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
20 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
21 |
22 | fs-extra@^9.0.1:
23 | version "9.0.1"
24 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
25 | integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
26 | dependencies:
27 | at-least-node "^1.0.0"
28 | graceful-fs "^4.2.0"
29 | jsonfile "^6.0.1"
30 | universalify "^1.0.0"
31 |
32 | graceful-fs@^4.1.6, graceful-fs@^4.2.0:
33 | version "4.2.4"
34 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
35 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
36 |
37 | jsonfile@^6.0.1:
38 | version "6.1.0"
39 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
40 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
41 | dependencies:
42 | universalify "^2.0.0"
43 | optionalDependencies:
44 | graceful-fs "^4.1.6"
45 |
46 | universalify@^1.0.0:
47 | version "1.0.0"
48 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
49 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
50 |
51 | universalify@^2.0.0:
52 | version "2.0.0"
53 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
54 | integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
55 |
--------------------------------------------------------------------------------
/aoife/lib/interface.d.ts:
--------------------------------------------------------------------------------
1 | type AoifeElement = HTMLInputElement & Record
;
2 | type AoifeNode = Element | string | number;
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | type AoifeChildren = any[];
5 |
6 | type AoifeTag =
7 | | string
8 | | AoifeElement
9 | | ((props: AoifeProps & Record) => (string | AoifeElement) | Promise);
10 |
11 | type PartialDetail = {
12 | [P in keyof T]?: Partial | (() => Partial);
13 | };
14 |
15 | type ExtendHTML = {
16 | ref?: RefSelf;
17 | /** 当元素 append 时回调 */
18 | onUpdate?: RefSelf;
19 | /** 当元素 append 时回调 */
20 | onAppend?: RefSelf;
21 | /** 当元素 remove 时回调 */
22 | onRemove?: RefSelf;
23 | /** 当元素 entry 时回调 */
24 | onEntry?: RefSelf;
25 | [key: string]: unknown;
26 | };
27 |
28 | type PartialJSX =
29 | | {
30 | [P in keyof T]?: PartialDetail;
31 | }
32 | | {
33 | [E in keyof ExtendHTML]?: ExtendHTML[E];
34 | };
35 |
36 | type JSXHTML = {
37 | [P in keyof HTMLElementTagNameMap]?: Partial;
38 | };
39 |
40 | type RefSelf = (e: HTMLInputElement) => unknown;
41 |
42 | // type IProps =
43 | // interface IProps extends PartialJSX, ExtendHTML {}
44 |
45 | // HTML标签列表
46 | type AoifeJSX = PartialJSX;
47 | type AoifeEvent = Event & { target: AoifeElement; [key: string]: unknown };
48 |
49 | interface AoifeProps extends PartialDetail, ExtendHTML {
50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
51 | children?: AoifeChildren;
52 | "test-id"?: string;
53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
54 | element?: any;
55 | onclick?: (e: AoifeEvent) => unknown;
56 | onchange?: (e: AoifeEvent) => unknown;
57 | oninput?: (e: AoifeEvent) => unknown;
58 | }
59 |
60 | declare namespace JSX {
61 | interface Element extends AoifeElement {
62 | [key: string]: AoifeProps;
63 | }
64 | interface IntrinsicElements extends AoifeJSX {
65 | // 标准元素之外的元素
66 | modify?: AoifeProps;
67 | [key: string]: AoifeProps;
68 | }
69 | }
70 |
71 | declare const aoife: {
72 | (
73 | tag: K | Element | string,
74 | attrs?: AoifeProps | null,
75 | ...children: AoifeChildren
76 | ): HTMLElementTagNameMap[K] & AoifeElement;
77 | next: (
78 | focusUpdateTargets?: string | HTMLElement | undefined,
79 | ignoreUpdateTargets?: string | HTMLElement | HTMLElement[]
80 | ) => void;
81 | attributeKeys: {
82 | [key: string]: boolean;
83 | };
84 | subscrib(target: T, param: K, props: AoifeProps): unknown;
85 | };
86 |
--------------------------------------------------------------------------------
/create-aoife-app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-aoife-app",
3 | "version": "1.6.23",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/fs-extra": {
8 | "version": "9.0.5",
9 | "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.5.tgz",
10 | "integrity": "sha512-wr3t7wIW1c0A2BIJtdVp4EflriVaVVAsCAIHVzzh8B+GiFv9X1xeJjCs4upRXtzp7kQ6lP5xvskjoD4awJ1ZeA==",
11 | "requires": {
12 | "@types/node": "*"
13 | }
14 | },
15 | "@types/node": {
16 | "version": "14.14.25",
17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz",
18 | "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ=="
19 | },
20 | "at-least-node": {
21 | "version": "1.0.0",
22 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
23 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
24 | },
25 | "fs-extra": {
26 | "version": "9.0.1",
27 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
28 | "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
29 | "requires": {
30 | "at-least-node": "^1.0.0",
31 | "graceful-fs": "^4.2.0",
32 | "jsonfile": "^6.0.1",
33 | "universalify": "^1.0.0"
34 | }
35 | },
36 | "graceful-fs": {
37 | "version": "4.2.5",
38 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",
39 | "integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw=="
40 | },
41 | "jsonfile": {
42 | "version": "6.1.0",
43 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
44 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
45 | "requires": {
46 | "graceful-fs": "^4.1.6",
47 | "universalify": "^2.0.0"
48 | },
49 | "dependencies": {
50 | "universalify": {
51 | "version": "2.0.0",
52 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
53 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
54 | }
55 | }
56 | },
57 | "universalify": {
58 | "version": "1.0.0",
59 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
60 | "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/document/md/状态管理.md:
--------------------------------------------------------------------------------
1 | # 状态管理
2 |
3 | 在现代框架中,我们有许多状态管理工具,大部分是 Redux 或 Redux 的轮子。
4 |
5 | 在 aoife 中,我们不需要额外的引入状态管理工具,因为使用动态属性配合 aoife.next 本质上就是一个状态管理的方案。
6 |
7 | 但是 aoife 中并没有官方管理 State 数据,它仅仅完成了类似**订阅发布**相关的工作,事实上市面上的状态管理库基本都是一个个针对框架特性的发布订阅模式的实现。
8 |
9 | aoife 的理念是尽可能不去实施不必要的功能,仅做关键的功能。这也是 aoife 认为自生生命力可以长期保持的核心价值观。
10 |
11 | State 数据我们交由开发人员自己设计和管理。不过介于大家使用 Redux 的经验,我们有以下建议:
12 |
13 | 1. State 尽量分为局部的和共享的
14 | 2. 共享的 State,尽量以一个单独的文件保存
15 | 3. 修改共享的 State 的方法,要和组件代码分离
16 |
17 | 我们分别实现局部 State 和 共享 State 的案例。
18 |
19 | ## 局部 State
20 |
21 | 在 aoife 中,State 可以是任何自定义对象,我们建议您保持简单,仅用简单的 `const state = {}` 对象即可。
22 |
23 | 局部的 State 非常简单,声明在组件内部即可。
24 |
25 | > 小细节,在 aoife 中,onclick 方法直接写在 JSX 中,并不会有任何性能损失,方法并不会因为组件更新而导致重新创建。
26 |
27 | ```jsx
28 | export const Page = () => {
29 | const state = {
30 | name: "foo",
31 | num: 0,
32 | };
33 |
34 | const ele = (
35 |
36 |
37 |
46 |
47 | );
48 |
49 | return ele;
50 | };
51 | ```
52 |
53 | ## 共享 Store
54 |
55 | 假定若我们需要跨越不同层级、纬度的组件进行状态更新。
56 |
57 | 在共享状态的地方,我们注意,就如 Redux 一样,我们建议要把组件、状态、修改状态的方法分别存放。
58 |
59 | 这样会使得在组件和状态复杂时,项目还能保持相对整洁。
60 |
61 | 我们假定有两个组件,他们任何一个更新了自己的内容,另一个组件的内容就需要修改成 `old` 字符串。
62 |
63 | 我们分为 3 个文件:
64 |
65 | ### store.js:
66 |
67 | 定义一个全局 store,并且定义好更新它的方法
68 |
69 | ```tsx
70 | export const store = {
71 | pageA: "foo",
72 | pageB: "old",
73 | };
74 |
75 | export const updatePageA = (newName) => {
76 | store.pageA = newName;
77 | store.pageB = "old";
78 | // 更新当前页面中所有订阅了的组件
79 | aoife.next();
80 | };
81 |
82 | export const updatePageB = (newName) => {
83 | store.pageA = "old";
84 | store.pageB = newName;
85 | // 更新当前页面中所有订阅了的组件
86 | aoife.next();
87 | };
88 | ```
89 |
90 | ### PageA.js
91 |
92 | 使用了 store 中的数据,并且当点击时,派发一个跨组件的更新
93 |
94 | ```tsx
95 | import { store } from "./store.js";
96 | import { updatePageA } from "./actions.js";
97 |
98 | export const PageA = () => {
99 | return (
100 | updatePageA("next-foo")}>
101 | {() => store.pageA}
102 |
103 | );
104 | };
105 | ```
106 |
107 | ### PageB.js
108 |
109 | 雷同 PageA
110 |
111 | ```tsx
112 | import { store } from "./store.js";
113 | import { updatePageB } from "./actions.js";
114 |
115 | export const PageB = () => {
116 | return (
117 | updatePageB("next-bar")}>
118 | {() => store.pageB}
119 |
120 | );
121 | };
122 | ```
123 |
124 | 通过状态管理的学习,我们可以看到 aoife 的设计是简单且存粹。
125 |
126 | ### 第三方状态管理
127 |
128 | 我们也可以使用任何第三方状态管理,只需要添加组件的生命周期,进行订阅和取消订阅即可。我们更推荐使用 aoife 的 next 派发更新,它并没有使用生命周期钩子,其本质仅仅是 DOM 的查询,更纯粹。
129 |
--------------------------------------------------------------------------------
/aoife/.npmignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | **/app/uploads/*
4 | # dependencies
5 | **/node_modules
6 | **/.pnp
7 | **/.pnp.js
8 | **/server/tmp
9 | **/react-app-env.d.ts
10 | exmaple
11 | create-dom-jsx
12 | dom-jsx-scripts
13 |
14 | # testing
15 | **/coverage
16 | **/self-proxy.js
17 |
18 | # production
19 | **/*.js.map
20 | **/build
21 | **/dist
22 | **/public/dll
23 | **/lib
24 |
25 | # cache
26 | **/.cache
27 | **/.webpack_cache
28 | **/.eslintcache
29 |
30 | # misc
31 | .DS_Store
32 | .env.local
33 | .env.development.local
34 | .env.test.local
35 | .env.production.local
36 |
37 | # npm & yarn debug
38 | npm-debug.log*
39 | yarn-debug.log*
40 | yarn-error.log*
41 |
42 | # git ignore files
43 | **/local-git
44 | **/gitignores
45 |
46 | # idea
47 | **/*.sln
48 | **/.idea
49 | **/.vscode
50 | **/.buckd
51 | **/.project
52 | **/.settings
53 | **/Session.vim
54 |
55 | # native files
56 | **/android/app/release
57 | **/fastlane/report.xml
58 | **/fastlane/Preview.html
59 | **/fastlane/screenshots
60 | **/*.ipa
61 |
62 |
63 | # Miscellaneous
64 | *.class
65 | *.log
66 | *.pyc
67 | *.swp
68 | .DS_Store
69 | .atom/
70 | .buildlog/
71 | .history
72 | .svn/
73 |
74 | # IntelliJ related
75 | *.iml
76 | *.ipr
77 | *.iws
78 | .idea/
79 |
80 | # The .vscode folder contains launch configuration and tasks you configure in
81 | # VS Code which you may wish to be included in version control, so this line
82 | # is commented out by default.
83 | #.vscode/
84 |
85 | # Flutter/Dart/Pub related
86 | **/doc/api/
87 | .dart_tool/
88 | .flutter-plugins
89 | .packages
90 | .pub-cache/
91 | .pub/
92 | /build/
93 |
94 | # Android related
95 | **/android/**/gradle-wrapper.jar
96 | **/android/.gradle
97 | **/android/captures/
98 | **/android/gradlew
99 | **/android/gradlew.bat
100 | **/android/local.properties
101 | **/android/**/GeneratedPluginRegistrant.java
102 |
103 | # iOS/XCode related
104 | **/ios/**/*.mode1v3
105 | **/ios/**/*.mode2v3
106 | **/ios/**/*.moved-aside
107 | **/ios/**/*.pbxuser
108 | **/ios/**/*.perspectivev3
109 | **/ios/**/*sync/
110 | **/ios/**/.sconsign.dblite
111 | **/ios/**/.tags*
112 | **/ios/**/.vagrant/
113 | **/ios/**/DerivedData/
114 | **/ios/**/Icon?
115 | **/ios/**/Pods/
116 | **/ios/**/.symlinks/
117 | **/ios/**/profile
118 | **/ios/**/xcuserdata
119 | **/ios/.generated/
120 | **/ios/Flutter/App.framework
121 | **/ios/Flutter/Flutter.framework
122 | **/ios/Flutter/Generated.xcconfig
123 | **/ios/Flutter/app.flx
124 | **/ios/Flutter/app.zip
125 | **/ios/Flutter/flutter_assets/
126 | **/ios/ServiceDefinitions.json
127 | **/ios/Runner/GeneratedPluginRegistrant.*
128 |
129 | # Exceptions to above rules.
130 | !**/ios/**/default.mode1v3
131 | !**/ios/**/default.mode2v3
132 | !**/ios/**/default.pbxuser
133 | !**/ios/**/default.perspectivev3
134 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
135 |
--------------------------------------------------------------------------------
/document/md/路由及生态.md:
--------------------------------------------------------------------------------
1 | # 路由
2 |
3 | aoife 可以和大部分原生 JS 路由配合使用。
4 |
5 | 为了保持简单,aoife 官方实现了一个极其轻量的路由。
6 |
7 | ## 使用 vanilla-route (原名 aoife-route)
8 |
9 | [vanilla-route](https://github.com/ymzuiku/vanilla-route.git) 是一个极轻量的原生 js 路由,不需要顶层包裹,可以嵌入在局部元素中使用。
10 |
11 | 由于 vanilla-route 内部并无使用 aoife,之所以它能和 aoife 配合使用,是因为它满足了 aoife 对组件的规范:`组件是一个返回 HTMElement 的函数, 组件的参数是一个对象`。
12 |
13 | vanilla-route 极其轻量,体积:gzip < 1kb.
14 |
15 | ### Install
16 |
17 | yarn:
18 |
19 | ```sh
20 | $ yarn add vanilla-route
21 | ```
22 |
23 | ### API
24 |
25 | 实力化一个路由对象,当 url 匹配时,会自动渲染
26 |
27 | ```jsx
28 | import Route from "vanilla-route";
29 |
30 | const ele = hello
} />;
31 | ```
32 |
33 | Route.push: 推进一个新页面
34 |
35 | ```jsx
36 | import Route from "vanilla-route";
37 |
38 | Route.push("/url");
39 | ```
40 |
41 | Route.push 方法推进一个新页面, 并且传递和读取 url 参数
42 |
43 | ```jsx
44 | Route.push("/url", { name: "hello" });
45 |
46 | // url 参数和 Route.state 保持一致
47 | console.log(Route.state);
48 | ```
49 |
50 | ROute.replace 方法更新当前页面, replace 不会增加 history 的长度
51 |
52 | ```jsx
53 | Route.replace("/url");
54 | ```
55 |
56 | Route.back 方法返回上一个页面
57 |
58 | ```jsx
59 | Route.back();
60 | ```
61 |
62 | ### 配合 aoife 使用
63 |
64 | ```js
65 | import "aoife";
66 | import Route from "vanilla-route";
67 |
68 | const Foo = () => {
69 | console.log(Route.state);
70 | return foo
;
71 | };
72 |
73 | const Bar = () => {
74 | return bar
;
75 | };
76 |
77 | const App = () => {
78 | return (
79 |
80 |
81 |
82 |
import("./Cat")} />
83 |
84 |
87 |
90 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | document.body.append();
100 | ```
101 |
102 | ### 脱离 URL 的路由
103 |
104 | url 可以是一个函数,若返回 true 就会渲染
105 |
106 | ```jsx
107 | const ele = user.isVip} render={VipPage} />;
108 | ```
109 |
110 | ## 生态
111 |
112 | 除了路由,我们还需要其他非常多的常用组件,aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
113 |
114 | ### 原生 JS 和 aoife 混用的例子
115 |
116 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
117 |
118 | ```jsx
119 | // npm i --save vanilla-app
120 | import Pop from "vanilla-pop";
121 |
122 | const App = () => {
123 | return (
124 |
125 | label
126 | pop tip
127 |
128 | );
129 | };
130 | ```
131 |
132 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要 JS 组件满足 aoife 组件的约定。
133 |
--------------------------------------------------------------------------------
/aoife/esm/index.js:
--------------------------------------------------------------------------------
1 | var b=(o,n,f)=>new Promise((e,t)=>{var r=s=>{try{a(f.next(s))}catch(y){t(y)}},l=s=>{try{a(f.throw(s))}catch(y){t(y)}},a=s=>s.done?e(s.value):Promise.resolve(s.value).then(r,l);a((f=f.apply(o,n)).next())});import{onAppend as k,onRemove as T,onEntry as N}from"vanilla-life";function m(o){let n=Object.prototype.toString.call(o);if(n==="[object String]"||n==="[object Number]")return!0}function d(o){return Object.prototype.toString.call(o).indexOf("lement")>0}var A=o=>{let n=[];return o.forEach(f=>{Array.isArray(f)?n=n.concat(f):n.push(f)}),n};import{ob as x}from"vanilla-ob";function E(o,n){o.isEqualNode(n)||o.replaceWith(n)}function v(o,n){if(!Array.isArray(o))return;o.filter(e=>e!=null).forEach((e,t)=>{if(m(e)){let r=document.createTextNode(e);r.key=t,n.append(r)}else if(typeof e=="function"){let r=document.createTextNode("");n.append(r);let l=()=>b(this,null,function*(){let a=yield Promise.resolve(e());if(m(a)){let s=document.createTextNode(a);s.key=t;let y=!1;return n.childNodes.forEach(c=>{if(c.key===s.key){if(c.textContent===s.textContent){y=!0;return}E(c,s),y=!0}}),y||n.insertBefore(s,r),t}else if(Array.isArray(a)){let s={},y={};a.forEach((i,u)=>{i.___forList=t,i.key||(i.key=`fn(${t})-list(${u})`),y[i.key]=i});let c=[];return n.childNodes.forEach(i=>{i.___forList===t&&(y[i.key]?s[i.key]=i:c.push(i))}),c.forEach(i=>{i.remove()}),a.forEach(i=>{let u=s[i.key];u?u.isEqualNode(i)||E(u,i):i!==!1&&n.insertBefore(i,r)}),"for-list-"+t}else if(a){a.key||(a.key=t);let s=!1;return n.childNodes.forEach(y=>{y.key===a.key&&(E(y,a),s=!0)}),s||n.insertBefore(a,r),a.key}});l(),x(n,null,l)}else if(d(e))n.append(e);else if(e!==!1)if(Object.prototype.toString.call(e)==="[object Array]"){let r=[];for(let l=0;l{let e={};if(n&&(typeof n=="function"||m(n)||d(n)?f=[n,...f]:Array.isArray(n)?f=[...n,...f]:e=n),f=A(f),(!e.children||!e.children.length)&&(e.children=[...f]),e.class&&(e.className=e.class,delete e.class),Array.isArray(o))return p(...o);let t;if(typeof o=="function"){if(t=o(e),t&&typeof t.then=="function"){let r=document.createElement("span");return r.setAttribute("promise-span",""),t.then(l=>{r.replaceWith(l)}),r}return t}if(typeof o=="string")if(o==="template"&&e.children){let r="";e.children.forEach(l=>{(typeof l=="string"||typeof l=="number")&&(r+=l)}),t=document.createElement("template"),t.innerHTML=r}else j.has(o)?(t=document.createElementNS(L,o),t.__isSvg=!0):t=document.createElement(o);else d(o)&&(t=o);return Object.keys(e).forEach(r=>{H[r]||h(t,r,e[r])}),v(e.children,t),typeof e.onUpdate=="function"&&h(t,null,e.onUpdate),typeof e.onAppend=="function"&&k(t,e.onAppend),typeof e.onRemove=="function"&&T(t,e.onRemove),typeof e.onEntry=="function"&&N(t,e.onEntry),t},w=o=>o&&o.children?p("span",{style:{all:"unset"}},...o.children):"";p.jsxFrag=w;p.next=h.next;p.attributeKeys=g;window.aoife=p;export{p as aoife,w as jsxFrag};
2 |
--------------------------------------------------------------------------------
/aoife/cjs/index.js:
--------------------------------------------------------------------------------
1 | var L=Object.create;var b=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var M=Object.getPrototypeOf,O=Object.prototype.hasOwnProperty;var x=e=>b(e,"__esModule",{value:!0});var S=(e,t)=>{x(e);for(var r in t)b(e,r,{get:t[r],enumerable:!0})},_=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of C(t))!O.call(e,n)&&n!=="default"&&b(e,n,{get:()=>t[n],enumerable:!(r=w(t,n))||r.enumerable});return e},A=e=>_(x(b(e!=null?L(M(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var k=(e,t,r)=>new Promise((n,o)=>{var a=i=>{try{s(r.next(i))}catch(y){o(y)}},l=i=>{try{s(r.throw(i))}catch(y){o(y)}},s=i=>i.done?n(i.value):Promise.resolve(i.value).then(a,l);s((r=r.apply(e,t)).next())});S(exports,{aoife:()=>c,jsxFrag:()=>H});var u=A(require("vanilla-life"));function E(e){let t=Object.prototype.toString.call(e);if(t==="[object String]"||t==="[object Number]")return!0}function h(e){return Object.prototype.toString.call(e).indexOf("lement")>0}var T=e=>{let t=[];return e.forEach(r=>{Array.isArray(r)?t=t.concat(r):t.push(r)}),t};var N=A(require("vanilla-ob"));function v(e,t){e.isEqualNode(t)||e.replaceWith(t)}function g(e,t){if(!Array.isArray(e))return;e.filter(n=>n!=null).forEach((n,o)=>{if(E(n)){let a=document.createTextNode(n);a.key=o,t.append(a)}else if(typeof n=="function"){let a=document.createTextNode("");t.append(a);let l=()=>k(this,null,function*(){let s=yield Promise.resolve(n());if(E(s)){let i=document.createTextNode(s);i.key=o;let y=!1;return t.childNodes.forEach(p=>{if(p.key===i.key){if(p.textContent===i.textContent){y=!0;return}v(p,i),y=!0}}),y||t.insertBefore(i,a),o}else if(Array.isArray(s)){let i={},y={};s.forEach((f,d)=>{f.___forList=o,f.key||(f.key=`fn(${o})-list(${d})`),y[f.key]=f});let p=[];return t.childNodes.forEach(f=>{f.___forList===o&&(y[f.key]?i[f.key]=f:p.push(f))}),p.forEach(f=>{f.remove()}),s.forEach(f=>{let d=i[f.key];d?d.isEqualNode(f)||v(d,f):f!==!1&&t.insertBefore(f,a)}),"for-list-"+o}else if(s){s.key||(s.key=o);let i=!1;return t.childNodes.forEach(y=>{y.key===s.key&&(v(y,s),i=!0)}),i||t.insertBefore(s,a),s.key}});l(),(0,N.ob)(t,null,l)}else if(h(n))t.append(n);else if(n!==!1)if(Object.prototype.toString.call(n)==="[object Array]"){let a=[];for(let l=0;l{let n={};if(t&&(typeof t=="function"||E(t)||h(t)?r=[t,...r]:Array.isArray(t)?r=[...t,...r]:n=t),r=T(r),(!n.children||!n.children.length)&&(n.children=[...r]),n.class&&(n.className=n.class,delete n.class),Array.isArray(e))return c(...e);let o;if(typeof e=="function"){if(o=e(n),o&&typeof o.then=="function"){let a=document.createElement("span");return a.setAttribute("promise-span",""),o.then(l=>{a.replaceWith(l)}),a}return o}if(typeof e=="string")if(e==="template"&&n.children){let a="";n.children.forEach(l=>{(typeof l=="string"||typeof l=="number")&&(a+=l)}),o=document.createElement("template"),o.innerHTML=a}else j.svgTags.has(e)?(o=document.createElementNS(R,e),o.__isSvg=!0):o=document.createElement(e);else h(e)&&(o=e);return Object.keys(n).forEach(a=>{K[a]||(0,m.ob)(o,a,n[a])}),g(n.children,o),typeof n.onUpdate=="function"&&(0,m.ob)(o,null,n.onUpdate),typeof n.onAppend=="function"&&(0,u.onAppend)(o,n.onAppend),typeof n.onRemove=="function"&&(0,u.onRemove)(o,n.onRemove),typeof n.onEntry=="function"&&(0,u.onEntry)(o,n.onEntry),o},H=e=>e&&e.children?c("span",{style:{all:"unset"}},...e.children):"";c.jsxFrag=H;c.next=m.ob.next;c.attributeKeys=m.attributeKeys;window.aoife=c;
2 |
--------------------------------------------------------------------------------
/aoife-scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aoife-scripts",
3 | "version": "4.0.13",
4 | "description": "Configuration and scripts for Create Aoife App, fork react-scripts.",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/ymzuiku/aoife.git"
8 | },
9 | "bugs": {
10 | "url": "git+https://github.com/ymzuiku/aoife.git"
11 | },
12 | "license": "MIT",
13 | "engines": {
14 | "node": "^10.12.0 || >=12.0.0"
15 | },
16 | "files": [
17 | "bin",
18 | "config",
19 | "lib",
20 | "scripts",
21 | "template",
22 | "template-typescript",
23 | "utils"
24 | ],
25 | "bin": {
26 | "aoife-scripts": "./bin/aoife-scripts.js"
27 | },
28 | "types": "./lib/aoife-app.d.ts",
29 | "dependencies": {
30 | "@babel/core": "7.12.3",
31 | "@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
32 | "@svgr/webpack": "5.4.0",
33 | "@typescript-eslint/eslint-plugin": "^4.5.0",
34 | "@typescript-eslint/parser": "^4.5.0",
35 | "babel-eslint": "^10.1.0",
36 | "babel-jest": "^26.6.0",
37 | "babel-loader": "8.1.0",
38 | "babel-plugin-named-asset-import": "^0.3.7",
39 | "babel-preset-custom-jsx": "^10.0.1",
40 | "bfj": "^7.0.2",
41 | "camelcase": "^6.1.0",
42 | "case-sensitive-paths-webpack-plugin": "2.3.0",
43 | "css-loader": "4.3.0",
44 | "dotenv": "8.2.0",
45 | "dotenv-expand": "5.1.0",
46 | "esbuild-loader": "^2.9.2",
47 | "eslint": "^7.11.0",
48 | "eslint-config-react-app": "^6.0.0",
49 | "eslint-plugin-flowtype": "^5.2.0",
50 | "eslint-plugin-import": "^2.22.1",
51 | "eslint-plugin-jest": "^24.1.0",
52 | "eslint-plugin-jsx-a11y": "^6.3.1",
53 | "eslint-plugin-prettier": "^3.3.0",
54 | "eslint-plugin-react": "^7.21.5",
55 | "eslint-plugin-react-hooks": "^4.2.0",
56 | "eslint-plugin-testing-library": "^3.9.2",
57 | "eslint-webpack-plugin": "^2.1.0",
58 | "file-loader": "6.1.1",
59 | "fs-extra": "^9.0.1",
60 | "hard-source-webpack-plugin": "^0.13.1",
61 | "html-webpack-plugin": "4.5.0",
62 | "identity-obj-proxy": "3.0.0",
63 | "jest": "26.6.0",
64 | "jest-circus": "26.6.0",
65 | "jest-resolve": "26.6.0",
66 | "jest-watch-typeahead": "0.6.1",
67 | "mini-css-extract-plugin": "0.11.3",
68 | "optimize-css-assets-webpack-plugin": "5.0.4",
69 | "pnp-webpack-plugin": "1.6.4",
70 | "postcss-flexbugs-fixes": "4.2.1",
71 | "postcss-loader": "3.0.0",
72 | "postcss-normalize": "8.0.1",
73 | "postcss-preset-env": "6.7.0",
74 | "postcss-safe-parser": "5.0.2",
75 | "prettier": "^2.2.1",
76 | "prompts": "2.4.0",
77 | "react-app-polyfill": "^2.0.0",
78 | "react-dev-utils": "^11.0.1",
79 | "react-refresh": "^0.8.3",
80 | "resolve": "1.18.1",
81 | "resolve-url-loader": "^3.1.2",
82 | "sass-loader": "8.0.2",
83 | "semver": "7.3.2",
84 | "style-loader": "1.3.0",
85 | "terser-webpack-plugin": "4.2.3",
86 | "ts-pnp": "1.2.0",
87 | "url-loader": "4.1.1",
88 | "webpack": "4.44.2",
89 | "webpack-dev-server": "3.11.0",
90 | "webpack-manifest-plugin": "2.2.0",
91 | "workbox-webpack-plugin": "5.1.4"
92 | },
93 | "optionalDependencies": {
94 | "fsevents": "^2.1.3"
95 | },
96 | "peerDependencies": {
97 | "typescript": "^3.2.1"
98 | },
99 | "peerDependenciesMeta": {
100 | "typescript": {
101 | "optional": true
102 | }
103 | },
104 | "browserslist": {
105 | "production": [
106 | ">0.2%",
107 | "not dead",
108 | "not op_mini all"
109 | ],
110 | "development": [
111 | "last 1 chrome version",
112 | "last 1 firefox version",
113 | "last 1 safari version"
114 | ]
115 | },
116 | "gitHead": "de8b2b3f2d0a699284420c19d5b4db0be776e0cf"
117 | }
118 |
--------------------------------------------------------------------------------
/document/md/动态属性.md:
--------------------------------------------------------------------------------
1 | ## 动态属性
2 |
3 | > 动态属性的代码已单独拆分到 vanilla-ob 库中,aoife 内部引用 vanilla-ob
4 |
5 | 动态属性类似于 React 的 State / setState,在 aoife 中其实并没有 State 的概念,取而代之的是 动态属性。
6 |
7 | 动态属性是一个函数,如以下代码中的 label 的 class。
8 |
9 | ```jsx
10 | const App = () => {
11 | // 仅仅是一个普通的属性
12 | const state = { name: "foo", css: "page" };
13 | return (
14 |
15 |
16 |
28 |
29 | );
30 | };
31 |
32 | document.body.append();
33 | ```
34 |
35 | 我们仔细解释以上代码,这基本上是 aoife 的私有的概念
36 |
37 | - label 元素的 children 和 class 属性是一个函数,此元素会进行订阅
38 | - 使用 aoife.next(),会在整个 document.body 中找到 所有已订阅元素
39 | - 元素会重新执行函数属性或函数 children,若新的值和当前值不一致,会更新 dom
40 |
41 | ## 减少更新范围
42 |
43 | aoife.next 的参数有两个,分别是需要更新的 document.querySelectorAll 查找器字符串,和需要排出的查找器字符串。
44 |
45 | 在页面元素较多时,减少更新范围可以减少更新时的开销。
46 |
47 | 假定有一个元素:
48 |
49 | ```jsx
50 | let name = "foo";
51 |
52 | const ele = (
53 |
54 |
55 |
56 |
{() => name}
57 |
{() => name}
58 |
59 |
{() => name}
60 |
{() => name}
61 |
62 |
65 |
66 | );
67 | ```
68 |
69 | 我们若需要只更新 dog 相关元素,并且排除 dog 下的 other 元素不更新:
70 |
71 | ```jsx
72 | // 首先更新状态
73 | name = "bar";
74 |
75 | // 第一个参数 .dog 表示找到所有 class="dog" 的元素,更新它及它的子组件
76 | // 第二个参数 .other 表示此次更新排除 class="other" 的元素
77 | aoife.next(".dog", ".other");
78 | ```
79 |
80 | ## 仅更新当前组件
81 |
82 | aoife.next 可以传递一个元素来指定某个元素及子元素的更新
83 |
84 | ```jsx
85 | const Page = () => {
86 | // 仅仅是一个普通的属性
87 | const ele = (
88 |
89 |
90 |
98 |
99 | );
100 |
101 | return ele;
102 | };
103 | ```
104 |
105 | ## 派发更新的最佳实现
106 |
107 | 当我们使用 aoife.next("class") 进行选择范围更新时,我们得到了更好的性能,但是增加了视图曾的耦合到状态管理中
108 |
109 | 当我们使用 aoife.next() 进行全范围更新时,我们减少了复杂度,但是增加了更新的开销
110 |
111 | 如何使用是更合适的方法?其实并没有绝对的答案,但是在 99%的情况下,我们可以参考以下几个规则:
112 |
113 | 1. 组件内部状态使用 aoife.next(ele), 进行局部更新
114 | 2. 非组件内部状态,尽量使用 aoife.next() 进行全范围更新,这可以确保业务没有状态异常的 bug
115 | 3. 个别页面性能有瓶颈时,使用 aoife.next(null, "b") 排除掉一些大规模订阅的组件
116 | 4. 组件的函数属性请误做复杂逻辑,仅做简单判断和值获取
117 |
118 | aoife.next() 性能非常高,只有在当前页面订阅更新的组件达到近万时才会有可体验的差别
119 |
120 | ## 声明式与命令式
121 |
122 | > 这是一个题外话,因为 aoife 给予了 DOM 的所有操作能力,所以略作唠叨
123 |
124 | React 提出了声明式与命令式的概念,编写声明式的代码可以极大减少业务 bug。
125 |
126 | 虽然我们使用了 DOM 查找器,但是这和使用 JQuery 不一样,使用 aoife.next 保留了声明式的开发方式。
127 |
128 | 因为 aoife.next 方法仅仅是选择了更新区域,被更新区域的具体更新结果还是元素内部声明,而不是命令式的传递更新值。
129 |
130 | 我们举两个例子:
131 |
132 | 命令式的 UI 修改, 假定我们需要修改 label 的内容:
133 |
134 | ```jsx
135 | // 组件内部不知道自己会修改成何值
136 | const ele = (
137 |
138 |
139 |
140 |
141 | );
142 |
143 | // update
144 | // 业务某处发起一个指令,直接修改成某个值
145 | document.body.querySelector(".item > label").textContent = "bar";
146 | ```
147 |
148 | 声明式的 UI 修改:
149 |
150 | ```jsx
151 | // 我们知道当状态变更了,组件会呈现哪些值
152 | let isA = false;
153 | const ele = (
154 |
155 |
164 |
165 |
166 | );
167 |
168 | // update
169 | // 业务某处,引入状态并调整,派发修改任务
170 | isA = false;
171 | aoife.next(".item");
172 | ```
173 |
174 | 我们可以看到声明式的方案去编写 UI 更加科学,React(Facebook)、Swift UI(Apple) 、Flutter(Google) 都是使用声明式的 UI 开发方式。
175 |
176 | 在编写业务代码时我们尽量使用 aoife.next, 使用声明式的方式去维护组件的状态。
177 |
178 | 使用声明式的 UI 编写方式,状态管理非常重要;下一节,我们讲讲在 aoife 中如何管理状态。
179 |
--------------------------------------------------------------------------------
/aoife-scripts/scripts/test.js:
--------------------------------------------------------------------------------
1 | // @remove-on-eject-begin
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | // @remove-on-eject-end
9 | 'use strict';
10 |
11 | // Do this as the first thing so that any code reading it knows the right env.
12 | process.env.BABEL_ENV = 'test';
13 | process.env.NODE_ENV = 'test';
14 | process.env.PUBLIC_URL = '';
15 |
16 | // Makes the script crash on unhandled rejections instead of silently
17 | // ignoring them. In the future, promise rejections that are not handled will
18 | // terminate the Node.js process with a non-zero exit code.
19 | process.on('unhandledRejection', err => {
20 | throw err;
21 | });
22 |
23 | // Ensure environment variables are read.
24 | require('../config/env');
25 | // @remove-on-eject-begin
26 | // Do the preflight check (only happens before eject).
27 | const verifyPackageTree = require('./utils/verifyPackageTree');
28 | if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') {
29 | verifyPackageTree();
30 | }
31 | const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup');
32 | verifyTypeScriptSetup();
33 | // @remove-on-eject-end
34 |
35 | const jest = require('jest');
36 | const execSync = require('child_process').execSync;
37 | let argv = process.argv.slice(2);
38 |
39 | function isInGitRepository() {
40 | try {
41 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
42 | return true;
43 | } catch (e) {
44 | return false;
45 | }
46 | }
47 |
48 | function isInMercurialRepository() {
49 | try {
50 | execSync('hg --cwd . root', { stdio: 'ignore' });
51 | return true;
52 | } catch (e) {
53 | return false;
54 | }
55 | }
56 |
57 | // Watch unless on CI or explicitly running all tests
58 | if (
59 | !process.env.CI &&
60 | argv.indexOf('--watchAll') === -1 &&
61 | argv.indexOf('--watchAll=false') === -1
62 | ) {
63 | // https://github.com/facebook/create-react-app/issues/5210
64 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
65 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
66 | }
67 |
68 | // @remove-on-eject-begin
69 | // This is not necessary after eject because we embed config into package.json.
70 | const createJestConfig = require('./utils/createJestConfig');
71 | const path = require('path');
72 | const paths = require('../config/paths');
73 | argv.push(
74 | '--config',
75 | JSON.stringify(
76 | createJestConfig(
77 | relativePath => path.resolve(__dirname, '..', relativePath),
78 | path.resolve(paths.appSrc, '..'),
79 | false
80 | )
81 | )
82 | );
83 |
84 | // This is a very dirty workaround for https://github.com/facebook/jest/issues/5913.
85 | // We're trying to resolve the environment ourselves because Jest does it incorrectly.
86 | // TODO: remove this as soon as it's fixed in Jest.
87 | const resolve = require('resolve');
88 | function resolveJestDefaultEnvironment(name) {
89 | const jestDir = path.dirname(
90 | resolve.sync('jest', {
91 | basedir: __dirname,
92 | })
93 | );
94 | const jestCLIDir = path.dirname(
95 | resolve.sync('jest-cli', {
96 | basedir: jestDir,
97 | })
98 | );
99 | const jestConfigDir = path.dirname(
100 | resolve.sync('jest-config', {
101 | basedir: jestCLIDir,
102 | })
103 | );
104 | return resolve.sync(name, {
105 | basedir: jestConfigDir,
106 | });
107 | }
108 | let cleanArgv = [];
109 | let env = 'jsdom';
110 | let next;
111 | do {
112 | next = argv.shift();
113 | if (next === '--env') {
114 | env = argv.shift();
115 | } else if (next.indexOf('--env=') === 0) {
116 | env = next.substring('--env='.length);
117 | } else {
118 | cleanArgv.push(next);
119 | }
120 | } while (argv.length > 0);
121 | argv = cleanArgv;
122 | let resolvedEnv;
123 | try {
124 | resolvedEnv = resolveJestDefaultEnvironment(`jest-environment-${env}`);
125 | } catch (e) {
126 | // ignore
127 | }
128 | if (!resolvedEnv) {
129 | try {
130 | resolvedEnv = resolveJestDefaultEnvironment(env);
131 | } catch (e) {
132 | // ignore
133 | }
134 | }
135 | const testEnvironment = resolvedEnv || env;
136 | argv.push('--env', testEnvironment);
137 | // @remove-on-eject-end
138 | jest.run(argv);
139 |
--------------------------------------------------------------------------------
/aoife-scripts/config/modules.js:
--------------------------------------------------------------------------------
1 | // @remove-on-eject-begin
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | // @remove-on-eject-end
9 | 'use strict';
10 |
11 | const fs = require('fs');
12 | const path = require('path');
13 | const paths = require('./paths');
14 | const chalk = require('react-dev-utils/chalk');
15 | const resolve = require('resolve');
16 |
17 | /**
18 | * Get additional module paths based on the baseUrl of a compilerOptions object.
19 | *
20 | * @param {Object} options
21 | */
22 | function getAdditionalModulePaths(options = {}) {
23 | const baseUrl = options.baseUrl;
24 |
25 | if (!baseUrl) {
26 | return '';
27 | }
28 |
29 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
30 |
31 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
32 | // the default behavior.
33 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
34 | return null;
35 | }
36 |
37 | // Allow the user set the `baseUrl` to `appSrc`.
38 | if (path.relative(paths.appSrc, baseUrlResolved) === '') {
39 | return [paths.appSrc];
40 | }
41 |
42 | // If the path is equal to the root directory we ignore it here.
43 | // We don't want to allow importing from the root directly as source files are
44 | // not transpiled outside of `src`. We do allow importing them with the
45 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with
46 | // an alias.
47 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
48 | return null;
49 | }
50 |
51 | // Otherwise, throw an error.
52 | throw new Error(
53 | chalk.red.bold(
54 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
55 | ' Create React App does not support other values at this time.'
56 | )
57 | );
58 | }
59 |
60 | /**
61 | * Get webpack aliases based on the baseUrl of a compilerOptions object.
62 | *
63 | * @param {*} options
64 | */
65 | function getWebpackAliases(options = {}) {
66 | const baseUrl = options.baseUrl;
67 |
68 | if (!baseUrl) {
69 | return {};
70 | }
71 |
72 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
73 |
74 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
75 | return {
76 | src: paths.appSrc,
77 | };
78 | }
79 | }
80 |
81 | /**
82 | * Get jest aliases based on the baseUrl of a compilerOptions object.
83 | *
84 | * @param {*} options
85 | */
86 | function getJestAliases(options = {}) {
87 | const baseUrl = options.baseUrl;
88 |
89 | if (!baseUrl) {
90 | return {};
91 | }
92 |
93 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
94 |
95 | if (path.relative(paths.appPath, baseUrlResolved) === '') {
96 | return {
97 | '^src/(.*)$': '/src/$1',
98 | };
99 | }
100 | }
101 |
102 | function getModules() {
103 | // Check if TypeScript is setup
104 | const hasTsConfig = fs.existsSync(paths.appTsConfig);
105 | const hasJsConfig = fs.existsSync(paths.appJsConfig);
106 |
107 | if (hasTsConfig && hasJsConfig) {
108 | throw new Error(
109 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
110 | );
111 | }
112 |
113 | let config;
114 |
115 | // If there's a tsconfig.json we assume it's a
116 | // TypeScript project and set up the config
117 | // based on tsconfig.json
118 | if (hasTsConfig) {
119 | const ts = require(resolve.sync('typescript', {
120 | basedir: paths.appNodeModules,
121 | }));
122 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
123 | // Otherwise we'll check if there is jsconfig.json
124 | // for non TS projects.
125 | } else if (hasJsConfig) {
126 | config = require(paths.appJsConfig);
127 | }
128 |
129 | config = config || {};
130 | const options = config.compilerOptions || {};
131 |
132 | const additionalModulePaths = getAdditionalModulePaths(options);
133 |
134 | return {
135 | additionalModulePaths: additionalModulePaths,
136 | webpackAliases: getWebpackAliases(options),
137 | jestAliases: getJestAliases(options),
138 | hasTsConfig,
139 | };
140 | }
141 |
142 | module.exports = getModules();
143 |
--------------------------------------------------------------------------------
/aoife/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { onAppend, onEntry, onRemove } from "vanilla-life";
2 | import { attributeKeys, ob } from "vanilla-ob";
3 | import { svgTags } from "vanilla-svg-tags";
4 | import { flattenOnce, isElement, isText } from "./helper";
5 | import { parseChildren } from "./parseChildren";
6 |
7 | const ignoreKeys: Record = {
8 | element: 1,
9 | onUpdate: 1,
10 | onAppend: 1,
11 | onRemove: 1,
12 | onEntry: 1,
13 | children: 1,
14 | length: 1,
15 | };
16 |
17 | const svgNS = "http://www.w3.org/2000/svg";
18 |
19 | export const aoife = (
20 | tag: AoifeTag,
21 | attrs?: Record & AoifeProps,
22 | ...children: unknown[]
23 | ): string | AoifeElement => {
24 | let props = {} as AoifeProps;
25 |
26 | if (attrs) {
27 | if (typeof attrs === "function" || isText(attrs) || isElement(attrs)) {
28 | children = [attrs, ...children];
29 | } else if (Array.isArray(attrs)) {
30 | children = [...attrs, ...children];
31 | } else {
32 | props = attrs;
33 | }
34 | }
35 |
36 | // 兼容数组嵌套
37 | children = flattenOnce(children);
38 |
39 | if (!props.children || !props.children.length) {
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | (props as any).children = [...children];
42 | }
43 |
44 | if (Array.isArray(tag)) {
45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
46 | return (aoife as any)(...tag);
47 | }
48 |
49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
50 | let ele: any;
51 |
52 | // 若对象是函数,就是 aoife 组件,直接执行获取返回值
53 | if (typeof tag === "function") {
54 | ele = tag(props) as AoifeElement;
55 | // 适配 promise 类型的组件,异步渲染
56 | if (ele && typeof ((ele as unknown) as Promise).then === "function") {
57 | const temp = (document.createElement("span") as unknown) as AoifeElement;
58 | temp.setAttribute("promise-span", "");
59 | ((ele as unknown) as Promise).then((el) => {
60 | temp.replaceWith(el);
61 | });
62 | return temp;
63 | }
64 | return ele;
65 | } else if (typeof tag === "string") {
66 | if (tag === "modify") {
67 | if (!props.element) {
68 | return aoife("span", { style: { all: "unset" } });
69 | }
70 |
71 | Object.keys(props).forEach((key) => {
72 | if (ignoreKeys[key]) {
73 | return;
74 | }
75 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
76 | ob((props as any).element, key as any, props[key]);
77 | });
78 | return ((props as unknown) as { element: AoifeElement }).element;
79 | }
80 | // 兼容第二个参数,attrs是child
81 | if (tag === "template" && props.children) {
82 | let html = "";
83 | props.children.forEach((v) => {
84 | if (typeof v === "string" || typeof v === "number") {
85 | html += v;
86 | }
87 | });
88 | ele = (document.createElement("template") as unknown) as AoifeElement;
89 | ele.innerHTML = html;
90 | } else {
91 | if (svgTags.has(tag)) {
92 | ele = (document.createElementNS(svgNS, tag as string) as unknown) as AoifeElement;
93 | // 约定:若设置了 isSvg vanilla-ob 可以更高效的检测出此元素为svg,反之得去查找svg关系
94 | ele.__isSvg = true;
95 | } else {
96 | ele = document.createElement(tag);
97 | }
98 | }
99 | } else {
100 | return aoife("span", { style: { all: "unset" } });
101 | }
102 |
103 | Object.keys(props).forEach((key) => {
104 | if (ignoreKeys[key]) {
105 | return;
106 | }
107 | ob(ele, key, props[key]);
108 | });
109 |
110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
111 | parseChildren(props.children as any, ele as any);
112 |
113 | if (typeof props.onUpdate === "function") {
114 | ob(ele, null, props.onUpdate);
115 | }
116 |
117 | if (typeof props.onAppend === "function") {
118 | onAppend(ele, props.onAppend);
119 | }
120 |
121 | if (typeof props.onRemove === "function") {
122 | onRemove(ele, props.onRemove);
123 | }
124 |
125 | if (typeof props.onEntry === "function") {
126 | onEntry(ele, props.onEntry);
127 | }
128 |
129 | return ele;
130 | };
131 |
132 | export const jsxFrag = (props: AoifeProps) => {
133 | if (props && props.children) {
134 | return aoife("span", { style: { all: "unset" } }, ...props.children);
135 | }
136 | return "";
137 | };
138 |
139 | aoife.jsxFrag = jsxFrag;
140 | aoife.next = ob.next;
141 | aoife.attributeKeys = attributeKeys;
142 |
143 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
144 | (window as any).aoife = aoife;
145 |
--------------------------------------------------------------------------------
/aoife/lib/parseChildren.ts:
--------------------------------------------------------------------------------
1 | import { ob } from "vanilla-ob";
2 | import { isElement, isText } from "./helper";
3 |
4 | function replace(old: HTMLElement, ele: HTMLElement) {
5 | if (!old.isEqualNode(ele)) {
6 | old.replaceWith(ele);
7 | }
8 | }
9 |
10 | export function parseChildren(_childs: AoifeElement[], ele: AoifeElement) {
11 | if (!Array.isArray(_childs)) {
12 | return;
13 | }
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | const childs = (_childs as any).filter((v: unknown) => v !== undefined && v !== null);
17 |
18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 | childs.forEach((ch: any, index: number) => {
20 | if (isText(ch)) {
21 | const text = document.createTextNode(ch);
22 | (text as unknown as { key: number }).key = index;
23 | ele.append(text);
24 | } else if (typeof ch === "function") {
25 | const temp = document.createTextNode("");
26 | ele.append(temp);
27 | const fn = async () => {
28 | const child = await Promise.resolve(ch());
29 | if (isText(child)) {
30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
31 | const text = document.createTextNode(child) as any;
32 | text.key = index;
33 | let isHave = false;
34 | ele.childNodes.forEach((e) => {
35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
36 | if ((e as any).key === text.key) {
37 | // 如果内容一致,不更新
38 | if (e.textContent === text.textContent) {
39 | isHave = true;
40 | return;
41 | }
42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
43 | replace(e as any, text);
44 | isHave = true;
45 | }
46 | });
47 | if (!isHave) {
48 | ele.insertBefore(text, temp);
49 | }
50 | return index;
51 | } else if (Array.isArray(child)) {
52 | // 函数返回一个数组
53 | const oldKeys = {} as Record;
54 | const childKeys = {} as Record;
55 | child.forEach((c, i) => {
56 | c.___forList = index;
57 | if (!c.key) {
58 | c.key = `fn(${index})-list(${i})`;
59 | }
60 | childKeys[c.key] = c;
61 | });
62 |
63 | // 找到之前的list元素,并且删除现在key没有的,然后array转map
64 | const needRemove = [] as HTMLElement[];
65 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
66 | ele.childNodes.forEach((el: any) => {
67 | if (el.___forList === index) {
68 | if (!childKeys[el.key]) {
69 | needRemove.push(el);
70 | } else {
71 | oldKeys[el.key] = el;
72 | }
73 | }
74 | });
75 | needRemove.forEach((e) => {
76 | e.remove();
77 | });
78 |
79 | // 遍历数组,替换旧的元素或插入之前没有的
80 | child.forEach((c) => {
81 | const oldEl = oldKeys[c.key] as HTMLElement;
82 | if (oldEl) {
83 | if (!oldEl.isEqualNode(c)) {
84 | replace(oldEl, c);
85 | }
86 | } else if (c !== false) {
87 | ele.insertBefore(c, temp);
88 | }
89 | });
90 | return "for-list-" + index;
91 | } else if (child) {
92 | if (!child.key) {
93 | child.key = index;
94 | }
95 | let isHave = false;
96 | ele.childNodes.forEach((e) => {
97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
98 | if ((e as any).key === child.key) {
99 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
100 | replace(e as any, child);
101 | isHave = true;
102 | }
103 | });
104 | if (!isHave) {
105 | ele.insertBefore(child, temp);
106 | }
107 | return child.key;
108 | }
109 | };
110 | fn();
111 | ob(ele, null, fn);
112 | } else if (isElement(ch)) {
113 | ele.append(ch);
114 | } else if (ch !== false) {
115 | if (Object.prototype.toString.call(ch) === "[object Array]") {
116 | const nextCh = [];
117 | for (let i = 0; i < ch.length; i++) {
118 | const c = ch[i];
119 | if (c !== false) {
120 | nextCh.push(c);
121 | }
122 | }
123 | ele.append(...nextCh);
124 | } else {
125 | ele.appendChild(ch);
126 | }
127 | }
128 | });
129 | }
130 |
--------------------------------------------------------------------------------
/aoife-scripts/config/env.js:
--------------------------------------------------------------------------------
1 | // @remove-on-eject-begin
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | // @remove-on-eject-end
9 | 'use strict';
10 |
11 | const fs = require('fs');
12 | const path = require('path');
13 | const paths = require('./paths');
14 |
15 | // Make sure that including paths.js after env.js will read .env variables.
16 | delete require.cache[require.resolve('./paths')];
17 |
18 | const NODE_ENV = process.env.NODE_ENV;
19 | if (!NODE_ENV) {
20 | throw new Error(
21 | 'The NODE_ENV environment variable is required but was not specified.'
22 | );
23 | }
24 |
25 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
26 | const dotenvFiles = [
27 | `${paths.dotenv}.${NODE_ENV}.local`,
28 | // Don't include `.env.local` for `test` environment
29 | // since normally you expect tests to produce the same
30 | // results for everyone
31 | NODE_ENV !== 'test' && `${paths.dotenv}.local`,
32 | `${paths.dotenv}.${NODE_ENV}`,
33 | paths.dotenv,
34 | ].filter(Boolean);
35 |
36 | // Load environment variables from .env* files. Suppress warnings using silent
37 | // if this file is missing. dotenv will never modify any environment variables
38 | // that have already been set. Variable expansion is supported in .env files.
39 | // https://github.com/motdotla/dotenv
40 | // https://github.com/motdotla/dotenv-expand
41 | dotenvFiles.forEach(dotenvFile => {
42 | if (fs.existsSync(dotenvFile)) {
43 | require('dotenv-expand')(
44 | require('dotenv').config({
45 | path: dotenvFile,
46 | })
47 | );
48 | }
49 | });
50 |
51 | // We support resolving modules according to `NODE_PATH`.
52 | // This lets you use absolute paths in imports inside large monorepos:
53 | // https://github.com/facebook/create-react-app/issues/253.
54 | // It works similar to `NODE_PATH` in Node itself:
55 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
56 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
57 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
58 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
59 | // We also resolve them to make sure all tools using them work consistently.
60 | const appDirectory = fs.realpathSync(process.cwd());
61 | process.env.NODE_PATH = (process.env.NODE_PATH || '')
62 | .split(path.delimiter)
63 | .filter(folder => folder && !path.isAbsolute(folder))
64 | .map(folder => path.resolve(appDirectory, folder))
65 | .join(path.delimiter);
66 |
67 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
68 | // injected into the application via DefinePlugin in webpack configuration.
69 | const REACT_APP = /^REACT_APP_/i;
70 |
71 | function getClientEnvironment(publicUrl) {
72 | const raw = Object.keys(process.env)
73 | .filter(key => REACT_APP.test(key))
74 | .reduce(
75 | (env, key) => {
76 | env[key] = process.env[key];
77 | return env;
78 | },
79 | {
80 | // Useful for determining whether we’re running in production mode.
81 | // Most importantly, it switches React into the correct mode.
82 | NODE_ENV: process.env.NODE_ENV || 'development',
83 | // Useful for resolving the correct path to static assets in `public`.
84 | // For example,
.
85 | // This should only be used as an escape hatch. Normally you would put
86 | // images into the `src` and `import` them in code to get their paths.
87 | PUBLIC_URL: publicUrl,
88 | // We support configuring the sockjs pathname during development.
89 | // These settings let a developer run multiple simultaneous projects.
90 | // They are used as the connection `hostname`, `pathname` and `port`
91 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
92 | // and `sockPort` options in webpack-dev-server.
93 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
94 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
95 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
96 | // Whether or not react-refresh is enabled.
97 | // react-refresh is not 100% stable at this time,
98 | // which is why it's disabled by default.
99 | // It is defined here so it is available in the webpackHotDevClient.
100 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
101 | }
102 | );
103 | // Stringify all values so we can feed into webpack DefinePlugin
104 | const stringified = {
105 | 'process.env': Object.keys(raw).reduce((env, key) => {
106 | env[key] = JSON.stringify(raw[key]);
107 | return env;
108 | }, {}),
109 | };
110 |
111 | return { raw, stringified };
112 | }
113 |
114 | module.exports = getClientEnvironment;
115 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aoife 简介
4 |
5 | ## [完整文档](https://aoife-one.vercel.app/)
6 |
7 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
8 |
9 | > aoife 非常小, gzip 5kb
10 |
11 | 社区已经有了 React/Vue/Ag 为什么还需要 Aoife?
12 |
13 | ## 特性
14 |
15 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
16 |
17 | - 核心 API 只有一个: aoife.next
18 | - 极简的组件声明
19 | - 每次更新只会更新一次,不会有重复渲染
20 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
21 |
22 | ## 安装 / 启动
23 |
24 | 安装
25 |
26 | ```bash
27 | $ npm init aoife-app
28 | $ cd
29 | $ yarn install
30 | ```
31 |
32 | 启动:
33 |
34 | ```bash
35 | $ yarn dev # 开发环境
36 | $ yarn build # 编译
37 | ```
38 |
39 | ## API
40 |
41 | aoife 是一个全局函数, 用于 jsx 解析,其中 aoife.next 用于更新元素
42 |
43 | ```typescript
44 | declare const aoife: {
45 | (
46 | tag: K,
47 | attrs?: PartialDetail,
48 | ...child: any[]
49 | ): HTMLElementTagNameMap[K];
50 | next: (
51 | focusUpdateTargets?: string | HTMLElement | undefined,
52 | ignoreUpdateTargets?: string | HTMLElement | HTMLElement[]
53 | ) => void;
54 | attributeKeys: {
55 | [key: string]: boolean;
56 | };
57 | useMiddleware: (fn: (ele: HTMLElement, props: IProps) => any) => void;
58 | };
59 | ```
60 |
61 | ## 很短且完整的教程
62 |
63 | 如果你会 React,学习 aoife 只需要 5 分钟,`注意 aoife 并不是 React 的轮子`。
64 |
65 | aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
66 |
67 | 但是 aoife 可以完成所有 React 能完成的项目,为了弥补缺少 React 相关的概念,看看我们是怎么做的:
68 |
69 | 前端开发可以抽象为两部分:页面绘制、页面更新;在 aoife 中,页面绘制就是使用 jsx 语法组织原始的 HTMLElement;然后使用 **函数赋值** 来解决元素更新。
70 |
71 | **函数赋值**: 即在声明元素的过程中,给属性绑定一个函数,jsx 解析过程中,若发现属性是一个函数,记录一个发布订阅任务,然后则执行函数,并且赋值;在未来需要更新此属性时,使用 `aoife.next` 函数对文档进行选择,命中的**元素及其子元素**会执行之前订阅的任务,更新属性。
72 |
73 | 我们看一个例子
74 |
75 | ```tsx
76 | import "aoife"; // 在项目入口处引入一次,注册全局 dom 对象
77 |
78 | // 这是一个普通的 jsx 组件
79 | function App() {
80 | return (
81 |
82 |
Hello World
83 |
84 |
85 | );
86 | }
87 |
88 | // 这是一个用于演示 函数赋值/更新 的组件
89 | function StatefulExample({ name }: { name: string }) {
90 | console.log(
91 | "这个日志仅会打印一次,因为 aoife.next 更新仅仅会派发元素的子属性,不会重绘整个组件"
92 | );
93 | let num = 0;
94 | return (
95 |
96 |
104 |
({
107 | fontSize: 20 + num + "px",
108 | })}
109 | >
110 |
{() => num}
111 |
112 |
113 | );
114 | }
115 |
116 | document.body.append();
117 | ```
118 |
119 | ## 异步 JSX
120 |
121 | aoife 可以异步取值和异步插入 children,这可以简化远程获取数据渲染的业务。 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步更新的回调
122 |
123 | ```jsx
124 | import "aoife";
125 |
126 | function App() {
127 | return (
128 |
129 |
{
132 | // 异步取值
133 | return new Promise((res) => {
134 | setTimeout(() => res("hello"), 500);
135 | });
136 | }}
137 | />
138 | {() => {
139 | // 异步插入元素
140 | return new Promise((res) => {
141 | setTimeout(() => {
142 | res(
list-a
);
143 | }, 1000);
144 | });
145 | }}
146 | {() => {
147 | // 异步插入元素
148 | return new Promise((res) => {
149 | setTimeout(() => {
150 | res(
list-b
);
151 | }, 300);
152 | });
153 | }}
154 |
155 | );
156 | }
157 | ```
158 |
159 | ## 设计细节
160 |
161 | 1. 为了延续声明式的开发方式,`aoife.next` 函数并没有传递值,仅仅是派发了更新命令,元素的属性还是由内部状态管理的逻辑来解决状态分支问题
162 | 2. 我们移除了类似 React 中 SCU,purecomponent、memo 等解决重绘问题的概念,因为**一次** aoife.next 执行仅仅更新**一次**局部元素的**属性**,并不会造成大规模重绘
163 | 3. `aoife.next` 已经是全局可选则的更新,所以失去了传统的状态管理库的必要;合理规范好 `aoife.next` 的调用即可。
164 |
165 | ### 编写 css
166 |
167 | ```jsx
168 | const css = (
169 |
174 | );
175 |
176 | document.body.append(css);
177 | ```
178 |
179 | ## 生态
180 |
181 | aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
182 |
183 | ### 原生 JS 和 aoife 混用的例子
184 |
185 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
186 |
187 | ```jsx
188 | // npm i --save vanilla-app
189 | import Pop from "vanilla-pop";
190 |
191 | const App = () => {
192 | return (
193 |
194 | label
195 | pop tip
196 |
197 | );
198 | };
199 | ```
200 |
201 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要此组件满足 3 个规则:
202 |
203 | - 1. 组件是一个函数,返回值是一个 HTMLElement 类型
204 | - 1. 组件的参数是一个对象
205 | - 1. 若 JSX 传递了 children,在组件第一个参数中会包含 children 字段,值是一个 HTMLElement 数组
206 |
207 | ## 完整文档
208 |
209 | [aoife.writeflowy.com](https://aoife.writeflowy.com)
210 |
--------------------------------------------------------------------------------
/aoife/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aoife 简介
4 |
5 | ## [完整文档](https://aoife.writeflowy.com)
6 |
7 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
8 |
9 | > aoife 非常小, gzip 5kb
10 |
11 | 社区已经有了 React/Vue/Ag 为什么还需要 Aoife?
12 |
13 | ## 特性
14 |
15 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
16 |
17 | - 核心 API 只有一个: aoife.next
18 | - 极简的组件声明
19 | - 每次更新只会更新一次,不会有重复渲染
20 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
21 |
22 | > aoife 非常小, gzip 5kb
23 |
24 | ## 安装 / 启动
25 |
26 | ### 特性
27 |
28 | 安装
29 |
30 | - 核心 API 只有一个: aoife.next
31 | - 极简的组件声明
32 | - 每次更新只会更新一次,不会有重复渲染
33 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
34 |
35 | ```bash
36 | $ npm init aoife-app
37 | $ cd
38 | $ yarn install
39 | ```
40 |
41 | 启动:
42 |
43 | ```bash
44 | $ yarn dev # 开发环境
45 | $ yarn build # 编译
46 | ```
47 |
48 | ## API
49 |
50 | aoife 是一个全局函数, 用于 jsx 解析,其中 aoife.next 用于更新元素
51 |
52 | ```typescript
53 | declare const aoife: {
54 | (
55 | tag: K,
56 | attrs?: PartialDetail,
57 | ...child: any[]
58 | ): HTMLElementTagNameMap[K];
59 | next: (
60 | focusUpdateTargets?: string | HTMLElement | undefined,
61 | ignoreUpdateTargets?: string | HTMLElement | HTMLElement[]
62 | ) => void;
63 | attributeKeys: {
64 | [key: string]: boolean;
65 | };
66 | useMiddleware: (fn: (ele: HTMLElement, props: IProps) => any) => void;
67 | };
68 | ```
69 |
70 | ## 很短且完整的教程
71 |
72 | 如果你会 React,学习 aoife 只需要 5 分钟,`注意 aoife 并不是 React 的轮子`。
73 |
74 | aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
75 |
76 | 但是 aoife 可以完成所有 React 能完成的项目,为了弥补缺少 React 相关的概念,看看我们是怎么做的:
77 |
78 | 前端开发可以抽象为两部分:页面绘制、页面更新;在 aoife 中,页面绘制就是使用 jsx 语法组织原始的 HTMLElement;然后使用 **函数赋值** 来解决元素更新。
79 |
80 | **函数赋值**: 即在声明元素的过程中,给属性绑定一个函数,jsx 解析过程中,若发现属性是一个函数,记录一个发布订阅任务,然后则执行函数,并且赋值;在未来需要更新此属性时,使用 `aoife.next` 函数对文档进行选择,命中的**元素及其子元素**会执行之前订阅的任务,更新属性。
81 |
82 | 我们看一个例子
83 |
84 | ```tsx
85 | import "aoife"; // 在项目入口处引入一次,注册全局 dom 对象
86 |
87 | // 这是一个普通的 jsx 组件
88 | function App() {
89 | return (
90 |
91 |
Hello World
92 |
93 |
94 | );
95 | }
96 |
97 | // 这是一个用于演示 函数赋值/更新 的组件
98 | function StatefulExample({ name }: { name: string }) {
99 | console.log(
100 | "这个日志仅会打印一次,因为 aoife.next 更新仅仅会派发元素的子属性,不会重绘整个组件"
101 | );
102 | let num = 0;
103 | return (
104 |
105 |
113 |
({
116 | fontSize: 20 + num + "px",
117 | })}
118 | >
119 |
{() => num}
120 |
121 |
122 | );
123 | }
124 |
125 | document.body.append();
126 | ```
127 |
128 | ## 异步 JSX
129 |
130 | aoife 可以异步取值和异步插入 children,这可以简化远程获取数据渲染的业务。 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步更新的回调
131 |
132 | ```jsx
133 | import "aoife";
134 |
135 | function App() {
136 | return (
137 |
138 |
{
141 | // 异步取值
142 | return new Promise((res) => {
143 | setTimeout(() => res("hello"), 500);
144 | });
145 | }}
146 | />
147 | {() => {
148 | // 异步插入元素
149 | return new Promise((res) => {
150 | setTimeout(() => {
151 | res(
list-a
);
152 | }, 1000);
153 | });
154 | }}
155 | {() => {
156 | // 异步插入元素
157 | return new Promise((res) => {
158 | setTimeout(() => {
159 | res(
list-b
);
160 | }, 300);
161 | });
162 | }}
163 |
164 | );
165 | }
166 | ```
167 |
168 | ## 设计细节
169 |
170 | 1. 为了延续声明式的开发方式,`aoife.next` 函数并没有传递值,仅仅是派发了更新命令,元素的属性还是由内部状态管理的逻辑来解决状态分支问题
171 | 2. 我们移除了类似 React 中 SCU,purecomponent、memo 等解决重绘问题的概念,因为**一次** aoife.next 执行仅仅更新**一次**局部元素的**属性**,并不会造成大规模重绘
172 | 3. `aoife.next` 已经是全局可选则的更新,所以失去了传统的状态管理库的必要;合理规范好 `aoife.next` 的调用即可。
173 |
174 | ### 编写 css
175 |
176 | ```jsx
177 | const css = (
178 |
183 | );
184 |
185 | document.body.append(css);
186 | ```
187 |
188 | ## 生态
189 |
190 | aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
191 |
192 | ### 原生 JS 和 aoife 混用的例子
193 |
194 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
195 |
196 | ```jsx
197 | // npm i --save vanilla-app
198 | import Pop from "vanilla-pop";
199 |
200 | const App = () => {
201 | return (
202 |
203 | label
204 | pop tip
205 |
206 | );
207 | };
208 | ```
209 |
210 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要此组件满足 3 个规则:
211 |
212 | - 1. 组件是一个函数,返回值是一个 HTMLElement 类型
213 | - 1. 组件的参数是一个对象
214 | - 1. 若 JSX 传递了 children,在组件第一个参数中会包含 children 字段,值是一个 HTMLElement 数组
215 |
216 | ## 完整文档
217 |
218 | [aoife.writeflowy.com](https://aoife.writeflowy.com)
219 |
--------------------------------------------------------------------------------
/create-aoife-app/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aoife 简介
4 |
5 | ## [完整文档](https://aoife.writeflowy.com)
6 |
7 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
8 |
9 | > aoife 非常小, gzip 5kb
10 |
11 | 社区已经有了 React/Vue/Ag 为什么还需要 Aoife?
12 |
13 | ## 特性
14 |
15 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
16 |
17 | - 核心 API 只有一个: aoife.next
18 | - 极简的组件声明
19 | - 每次更新只会更新一次,不会有重复渲染
20 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
21 |
22 | > aoife 非常小, gzip 5kb
23 |
24 | ## 安装 / 启动
25 |
26 | ### 特性
27 |
28 | 安装
29 |
30 | - 核心 API 只有一个: aoife.next
31 | - 极简的组件声明
32 | - 每次更新只会更新一次,不会有重复渲染
33 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
34 |
35 | ```bash
36 | $ npm init aoife-app
37 | $ cd
38 | $ yarn install
39 | ```
40 |
41 | 启动:
42 |
43 | ```bash
44 | $ yarn dev # 开发环境
45 | $ yarn build # 编译
46 | ```
47 |
48 | ## API
49 |
50 | aoife 是一个全局函数, 用于 jsx 解析,其中 aoife.next 用于更新元素
51 |
52 | ```typescript
53 | declare const aoife: {
54 | (
55 | tag: K,
56 | attrs?: PartialDetail,
57 | ...child: any[]
58 | ): HTMLElementTagNameMap[K];
59 | next: (
60 | focusUpdateTargets?: string | HTMLElement | undefined,
61 | ignoreUpdateTargets?: string | HTMLElement | HTMLElement[]
62 | ) => void;
63 | attributeKeys: {
64 | [key: string]: boolean;
65 | };
66 | useMiddleware: (fn: (ele: HTMLElement, props: IProps) => any) => void;
67 | };
68 | ```
69 |
70 | ## 很短且完整的教程
71 |
72 | 如果你会 React,学习 aoife 只需要 5 分钟,`注意 aoife 并不是 React 的轮子`。
73 |
74 | aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
75 |
76 | 但是 aoife 可以完成所有 React 能完成的项目,为了弥补缺少 React 相关的概念,看看我们是怎么做的:
77 |
78 | 前端开发可以抽象为两部分:页面绘制、页面更新;在 aoife 中,页面绘制就是使用 jsx 语法组织原始的 HTMLElement;然后使用 **函数赋值** 来解决元素更新。
79 |
80 | **函数赋值**: 即在声明元素的过程中,给属性绑定一个函数,jsx 解析过程中,若发现属性是一个函数,记录一个发布订阅任务,然后则执行函数,并且赋值;在未来需要更新此属性时,使用 `aoife.next` 函数对文档进行选择,命中的**元素及其子元素**会执行之前订阅的任务,更新属性。
81 |
82 | 我们看一个例子
83 |
84 | ```tsx
85 | import "aoife"; // 在项目入口处引入一次,注册全局 dom 对象
86 |
87 | // 这是一个普通的 jsx 组件
88 | function App() {
89 | return (
90 |
91 |
Hello World
92 |
93 |
94 | );
95 | }
96 |
97 | // 这是一个用于演示 函数赋值/更新 的组件
98 | function StatefulExample({ name }: { name: string }) {
99 | console.log(
100 | "这个日志仅会打印一次,因为 aoife.next 更新仅仅会派发元素的子属性,不会重绘整个组件"
101 | );
102 | let num = 0;
103 | return (
104 |
105 |
113 |
({
116 | fontSize: 20 + num + "px",
117 | })}
118 | >
119 |
{() => num}
120 |
121 |
122 | );
123 | }
124 |
125 | document.body.append();
126 | ```
127 |
128 | ## 异步 JSX
129 |
130 | aoife 可以异步取值和异步插入 children,这可以简化远程获取数据渲染的业务。 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步更新的回调
131 |
132 | ```jsx
133 | import "aoife";
134 |
135 | function App() {
136 | return (
137 |
138 |
{
141 | // 异步取值
142 | return new Promise((res) => {
143 | setTimeout(() => res("hello"), 500);
144 | });
145 | }}
146 | />
147 | {() => {
148 | // 异步插入元素
149 | return new Promise((res) => {
150 | setTimeout(() => {
151 | res(
list-a
);
152 | }, 1000);
153 | });
154 | }}
155 | {() => {
156 | // 异步插入元素
157 | return new Promise((res) => {
158 | setTimeout(() => {
159 | res(
list-b
);
160 | }, 300);
161 | });
162 | }}
163 |
164 | );
165 | }
166 | ```
167 |
168 | ## 设计细节
169 |
170 | 1. 为了延续声明式的开发方式,`aoife.next` 函数并没有传递值,仅仅是派发了更新命令,元素的属性还是由内部状态管理的逻辑来解决状态分支问题
171 | 2. 我们移除了类似 React 中 SCU,purecomponent、memo 等解决重绘问题的概念,因为**一次** aoife.next 执行仅仅更新**一次**局部元素的**属性**,并不会造成大规模重绘
172 | 3. `aoife.next` 已经是全局可选则的更新,所以失去了传统的状态管理库的必要;合理规范好 `aoife.next` 的调用即可。
173 |
174 | ### 编写 css
175 |
176 | ```jsx
177 | const css = (
178 |
183 | );
184 |
185 | document.body.append(css);
186 | ```
187 |
188 | ## 生态
189 |
190 | aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
191 |
192 | ### 原生 JS 和 aoife 混用的例子
193 |
194 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
195 |
196 | ```jsx
197 | // npm i --save vanilla-app
198 | import Pop from "vanilla-pop";
199 |
200 | const App = () => {
201 | return (
202 |
203 | label
204 | pop tip
205 |
206 | );
207 | };
208 | ```
209 |
210 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要此组件满足 3 个规则:
211 |
212 | - 1. 组件是一个函数,返回值是一个 HTMLElement 类型
213 | - 1. 组件的参数是一个对象
214 | - 1. 若 JSX 传递了 children,在组件第一个参数中会包含 children 字段,值是一个 HTMLElement 数组
215 |
216 | ## 完整文档
217 |
218 | [aoife.writeflowy.com](https://aoife.writeflowy.com)
219 |
--------------------------------------------------------------------------------
/create-aoife-app/vite/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aoife 简介
4 |
5 | ## [完整文档](https://aoife.writeflowy.com)
6 |
7 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
8 |
9 | > aoife 非常小, gzip 5kb
10 |
11 | 社区已经有了 React/Vue/Ag 为什么还需要 Aoife?
12 |
13 | ## 特性
14 |
15 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
16 |
17 | - 核心 API 只有一个: aoife.next
18 | - 极简的组件声明
19 | - 每次更新只会更新一次,不会有重复渲染
20 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
21 |
22 | > aoife 非常小, gzip 5kb
23 |
24 | ## 安装 / 启动
25 |
26 | ### 特性
27 |
28 | 安装
29 |
30 | - 核心 API 只有一个: aoife.next
31 | - 极简的组件声明
32 | - 每次更新只会更新一次,不会有重复渲染
33 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
34 |
35 | ```bash
36 | $ npm init aoife-app
37 | $ cd
38 | $ yarn install
39 | ```
40 |
41 | 启动:
42 |
43 | ```bash
44 | $ yarn dev # 开发环境
45 | $ yarn build # 编译
46 | ```
47 |
48 | ## API
49 |
50 | aoife 是一个全局函数, 用于 jsx 解析,其中 aoife.next 用于更新元素
51 |
52 | ```typescript
53 | declare const aoife: {
54 | (tag: any, attrs?: ChildOne, ...child: ChildOne[]): HTMLElement;
55 | next: (
56 | focusUpdateTargets?: string | undefined,
57 | ignoreUpdateTargets?: string | any[] | undefined
58 | ) => HTMLElement[];
59 | waitAppend(ele: HTMLElement | string, max?: number): Promise;
60 | subscribe: (fn: any) => () => void;
61 | events: Set;
62 | registerTag(data: { [key: string]: any }): void;
63 | propFn(
64 | target: any,
65 | fn: (val: any) => IStyle | string | boolean | number | any[] | object
66 | ): any;
67 | waitValue(fn: () => T, max?: number): Promise;
68 | memo: (blocker: () => any) => (fn: any) => Promise;
69 | deepEqual: (a: any, b: any) => boolean;
70 | deepMerge: (a: T, b: U) => T & U;
71 | };
72 | ```
73 |
74 | ## 很短且完整的教程
75 |
76 | 如果你会 React,学习 aoife 只需要 5 分钟,`注意 aoife 并不是 React 的轮子`。
77 |
78 | aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
79 |
80 | 但是 aoife 可以完成所有 React 能完成的项目,为了弥补缺少 React 相关的概念,看看我们是怎么做的:
81 |
82 | 前端开发可以抽象为两部分:页面绘制、页面更新;在 aoife 中,页面绘制就是使用 jsx 语法组织原始的 HTMLElement;然后使用 **函数赋值** 来解决元素更新。
83 |
84 | **函数赋值**: 即在声明元素的过程中,给属性绑定一个函数,jsx 解析过程中,若发现属性是一个函数,记录一个发布订阅任务,然后则执行函数,并且赋值;在未来需要更新此属性时,使用 `aoife.next` 函数对文档进行选择,命中的**元素及其子元素**会执行之前订阅的任务,更新属性。
85 |
86 | 我们看一个例子
87 |
88 | ```text
89 | import "aoife"; // 在项目入口处引入一次,注册全局 dom 对象
90 |
91 | // 这是一个普通的 jsx 组件
92 | function App() {
93 | return (
94 |
95 |
Hello World
96 |
97 |
98 | );
99 | }
100 |
101 | // 这是一个用于演示 函数赋值/更新 的组件
102 | function StatefulExample({ name }: { name: string }) {
103 | console.log(
104 | "这个日志仅会打印一次,因为 aoife.next 更新仅仅会派发元素的子属性,不会重绘整个组件"
105 | );
106 | let num = 0;
107 | return (
108 |
109 |
117 |
({
120 | fontSize: 20 + num + "px",
121 | })}
122 | >
123 |
{() => num}
124 |
125 |
126 | );
127 | }
128 |
129 | document.body.append();
130 | ```
131 |
132 | ## 异步 JSX
133 |
134 | aoife 可以异步取值和异步插入 children,这可以简化远程获取数据渲染的业务。 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步更新的回调
135 |
136 | ```jsx
137 | import "aoife";
138 |
139 | function App() {
140 | return (
141 |
142 |
{
145 | // 异步取值
146 | return new Promise((res) => {
147 | setTimeout(() => res("hello"), 500);
148 | });
149 | }}
150 | />
151 | {() => {
152 | // 异步插入元素
153 | return new Promise((res) => {
154 | setTimeout(() => {
155 | res(
list-a
);
156 | }, 1000);
157 | });
158 | }}
159 | {() => {
160 | // 异步插入元素
161 | return new Promise((res) => {
162 | setTimeout(() => {
163 | res(
list-b
);
164 | }, 300);
165 | });
166 | }}
167 |
168 | );
169 | }
170 | ```
171 |
172 | ## 设计细节
173 |
174 | 1. 为了延续声明式的开发方式,`aoife.next` 函数并没有传递值,仅仅是派发了更新命令,元素的属性还是由内部状态管理的逻辑来解决状态分支问题
175 | 2. 我们移除了类似 React 中 SCU,purecomponent、memo 等解决重绘问题的概念,因为**一次** aoife.next 执行仅仅更新**一次**局部元素的**属性**,并不会造成大规模重绘
176 | 3. `aoife.next` 已经是全局可选则的更新,所以失去了传统的状态管理库的必要;合理规范好 `aoife.next` 的调用即可。
177 |
178 | ## 常用额外方法
179 |
180 | ### 去抖动 debounce
181 |
182 | ```jsx
183 |
186 | ```
187 |
188 | ### 节流 throttle
189 |
190 | ```jsx
191 |
194 | ```
195 |
196 | ### 编写 css
197 |
198 | ```jsx
199 | const css = (
200 |
205 | );
206 |
207 | document.body.append(css);
208 | ```
209 |
210 | ## 生态
211 |
212 | aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
213 |
214 | ### 原生 JS 和 aoife 混用的例子
215 |
216 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
217 |
218 | ```jsx
219 | // npm i --save vanilla-app
220 | import Pop from "vanilla-pop";
221 |
222 | const App = () => {
223 | return (
224 |
225 | label
226 | pop tip
227 |
228 | );
229 | };
230 | ```
231 |
232 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要此组件满足 3 个规则:
233 |
234 | - 1. 组件是一个函数,返回值是一个 HTMLElement 类型
235 | - 1. 组件的参数是一个对象
236 | - 1. 若 JSX 传递了 children,在组件第一个参数中会包含 children 字段,值是一个 HTMLElement 数组
237 |
238 | ## [完整文档](https://aoife.writeflowy.com)
239 |
--------------------------------------------------------------------------------
/create-aoife-app/webpack/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aoife 简介
4 |
5 | ## [完整文档](https://aoife.writeflowy.com)
6 |
7 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
8 |
9 | > aoife 非常小, gzip 5kb
10 |
11 | 社区已经有了 React/Vue/Ag 为什么还需要 Aoife?
12 |
13 | ## 特性
14 |
15 | 使用 jsx 开发 native-js 程序, 每个组件都是一个原始的 HTMLElment,可以和所有原生 js 库很好的兼容使用。
16 |
17 | - 核心 API 只有一个: aoife.next
18 | - 极简的组件声明
19 | - 每次更新只会更新一次,不会有重复渲染
20 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
21 |
22 | > aoife 非常小, gzip 5kb
23 |
24 | ## 安装 / 启动
25 |
26 | ### 特性
27 |
28 | 安装
29 |
30 | - 核心 API 只有一个: aoife.next
31 | - 极简的组件声明
32 | - 每次更新只会更新一次,不会有重复渲染
33 | - 拥抱原生 JS 生态,可以和原生 JS 库很好的兼容使用
34 |
35 | ```bash
36 | $ npm init aoife-app
37 | $ cd
38 | $ yarn install
39 | ```
40 |
41 | 启动:
42 |
43 | ```bash
44 | $ yarn dev # 开发环境
45 | $ yarn build # 编译
46 | ```
47 |
48 | ## API
49 |
50 | aoife 是一个全局函数, 用于 jsx 解析,其中 aoife.next 用于更新元素
51 |
52 | ```typescript
53 | declare const aoife: {
54 | (tag: any, attrs?: ChildOne, ...child: ChildOne[]): HTMLElement;
55 | next: (
56 | focusUpdateTargets?: string | undefined,
57 | ignoreUpdateTargets?: string | any[] | undefined
58 | ) => HTMLElement[];
59 | waitAppend(ele: HTMLElement | string, max?: number): Promise;
60 | subscribe: (fn: any) => () => void;
61 | events: Set;
62 | registerTag(data: { [key: string]: any }): void;
63 | propFn(
64 | target: any,
65 | fn: (val: any) => IStyle | string | boolean | number | any[] | object
66 | ): any;
67 | waitValue(fn: () => T, max?: number): Promise;
68 | memo: (blocker: () => any) => (fn: any) => Promise;
69 | deepEqual: (a: any, b: any) => boolean;
70 | deepMerge: (a: T, b: U) => T & U;
71 | };
72 | ```
73 |
74 | ## 很短且完整的教程
75 |
76 | 如果你会 React,学习 aoife 只需要 5 分钟,`注意 aoife 并不是 React 的轮子`。
77 |
78 | aoife 仅仅保留了 JSX 相关的概念,移除了 React 所有非 JSX 相关的概念,所以 aoife 没有生命周期,hooks、diffDOM。
79 |
80 | 但是 aoife 可以完成所有 React 能完成的项目,为了弥补缺少 React 相关的概念,看看我们是怎么做的:
81 |
82 | 前端开发可以抽象为两部分:页面绘制、页面更新;在 aoife 中,页面绘制就是使用 jsx 语法组织原始的 HTMLElement;然后使用 **函数赋值** 来解决元素更新。
83 |
84 | **函数赋值**: 即在声明元素的过程中,给属性绑定一个函数,jsx 解析过程中,若发现属性是一个函数,记录一个发布订阅任务,然后则执行函数,并且赋值;在未来需要更新此属性时,使用 `aoife.next` 函数对文档进行选择,命中的**元素及其子元素**会执行之前订阅的任务,更新属性。
85 |
86 | 我们看一个例子
87 |
88 | ```text
89 | import "aoife"; // 在项目入口处引入一次,注册全局 dom 对象
90 |
91 | // 这是一个普通的 jsx 组件
92 | function App() {
93 | return (
94 |
95 |
Hello World
96 |
97 |
98 | );
99 | }
100 |
101 | // 这是一个用于演示 函数赋值/更新 的组件
102 | function StatefulExample({ name }: { name: string }) {
103 | console.log(
104 | "这个日志仅会打印一次,因为 aoife.next 更新仅仅会派发元素的子属性,不会重绘整个组件"
105 | );
106 | let num = 0;
107 | return (
108 |
109 |
117 |
({
120 | fontSize: 20 + num + "px",
121 | })}
122 | >
123 |
{() => num}
124 |
125 |
126 | );
127 | }
128 |
129 | document.body.append();
130 | ```
131 |
132 | ## 异步 JSX
133 |
134 | aoife 可以异步取值和异步插入 children,这可以简化远程获取数据渲染的业务。 注意,aoife.next 仅仅是一个派发更新,并不会等待所有异步更新的回调
135 |
136 | ```jsx
137 | import "aoife";
138 |
139 | function App() {
140 | return (
141 |
142 |
{
145 | // 异步取值
146 | return new Promise((res) => {
147 | setTimeout(() => res("hello"), 500);
148 | });
149 | }}
150 | />
151 | {() => {
152 | // 异步插入元素
153 | return new Promise((res) => {
154 | setTimeout(() => {
155 | res(
list-a
);
156 | }, 1000);
157 | });
158 | }}
159 | {() => {
160 | // 异步插入元素
161 | return new Promise((res) => {
162 | setTimeout(() => {
163 | res(
list-b
);
164 | }, 300);
165 | });
166 | }}
167 |
168 | );
169 | }
170 | ```
171 |
172 | ## 设计细节
173 |
174 | 1. 为了延续声明式的开发方式,`aoife.next` 函数并没有传递值,仅仅是派发了更新命令,元素的属性还是由内部状态管理的逻辑来解决状态分支问题
175 | 2. 我们移除了类似 React 中 SCU,purecomponent、memo 等解决重绘问题的概念,因为**一次** aoife.next 执行仅仅更新**一次**局部元素的**属性**,并不会造成大规模重绘
176 | 3. `aoife.next` 已经是全局可选则的更新,所以失去了传统的状态管理库的必要;合理规范好 `aoife.next` 的调用即可。
177 |
178 | ## 常用额外方法
179 |
180 | ### 去抖动 debounce
181 |
182 | ```jsx
183 |
186 | ```
187 |
188 | ### 节流 throttle
189 |
190 | ```jsx
191 |
194 | ```
195 |
196 | ### 编写 css
197 |
198 | ```jsx
199 | const css = (
200 |
205 | );
206 |
207 | document.body.append(css);
208 | ```
209 |
210 | ## 生态
211 |
212 | aoife 的核心设计理念就是用原生 JS 解决生态问题,任何一个函数,其返回值是一个 HTMLElement,就可以在 aoife 中作为标签进行使用。
213 |
214 | ### 原生 JS 和 aoife 混用的例子
215 |
216 | vanilla-pop 组件是一个由 tippy.js 封装的函数,内部并无引入 aoife, 使用方法:
217 |
218 | ```jsx
219 | // npm i --save vanilla-app
220 | import Pop from "vanilla-pop";
221 |
222 | const App = () => {
223 | return (
224 |
225 | label
226 | pop tip
227 |
228 | );
229 | };
230 | ```
231 |
232 | 从这个案例可以看到,一个原生 JS 组件,本身可以不需要包含 aoife,也可以被 aoife 使用;只需要此组件满足 3 个规则:
233 |
234 | - 1. 组件是一个函数,返回值是一个 HTMLElement 类型
235 | - 1. 组件的参数是一个对象
236 | - 1. 若 JSX 传递了 children,在组件第一个参数中会包含 children 字段,值是一个 HTMLElement 数组
237 |
238 | ## [完整文档](https://aoife.writeflowy.com)
239 |
--------------------------------------------------------------------------------
/aoife-scripts/scripts/utils/createJestConfig.js:
--------------------------------------------------------------------------------
1 | // @remove-file-on-eject
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | 'use strict';
9 |
10 | const fs = require('fs');
11 | const chalk = require('react-dev-utils/chalk');
12 | const paths = require('../../config/paths');
13 | const modules = require('../../config/modules');
14 |
15 | module.exports = (resolve, rootDir, isEjecting) => {
16 | // Use this instead of `paths.testsSetup` to avoid putting
17 | // an absolute filename into configuration after ejecting.
18 | const setupTestsMatches = paths.testsSetup.match(/src[/\\]setupTests\.(.+)/);
19 | const setupTestsFileExtension =
20 | (setupTestsMatches && setupTestsMatches[1]) || 'js';
21 | const setupTestsFile = fs.existsSync(paths.testsSetup)
22 | ? `/src/setupTests.${setupTestsFileExtension}`
23 | : undefined;
24 |
25 | const config = {
26 | roots: ['/src'],
27 |
28 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
29 |
30 | setupFiles: [
31 | isEjecting
32 | ? 'react-app-polyfill/jsdom'
33 | : require.resolve('react-app-polyfill/jsdom'),
34 | ],
35 |
36 | setupFilesAfterEnv: setupTestsFile ? [setupTestsFile] : [],
37 | testMatch: [
38 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
39 | '/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
40 | ],
41 | testEnvironment: 'jsdom',
42 | testRunner: require.resolve('jest-circus/runner'),
43 | transform: {
44 | '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': isEjecting
45 | ? '/node_modules/babel-jest'
46 | : resolve('config/jest/babelTransform.js'),
47 | '^.+\\.css$': resolve('config/jest/cssTransform.js'),
48 | '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': resolve(
49 | 'config/jest/fileTransform.js'
50 | ),
51 | },
52 | transformIgnorePatterns: [
53 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
54 | '^.+\\.module\\.(css|sass|scss)$',
55 | ],
56 | modulePaths: modules.additionalModulePaths || [],
57 | moduleNameMapper: {
58 | '^react-native$': 'react-native-web',
59 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
60 | ...(modules.jestAliases || {}),
61 | },
62 | moduleFileExtensions: [...paths.moduleFileExtensions, 'node'].filter(
63 | ext => !ext.includes('mjs')
64 | ),
65 | watchPlugins: [
66 | 'jest-watch-typeahead/filename',
67 | 'jest-watch-typeahead/testname',
68 | ],
69 | resetMocks: true,
70 | };
71 | if (rootDir) {
72 | config.rootDir = rootDir;
73 | }
74 | const overrides = Object.assign({}, require(paths.appPackageJson).jest);
75 | const supportedKeys = [
76 | 'clearMocks',
77 | 'collectCoverageFrom',
78 | 'coveragePathIgnorePatterns',
79 | 'coverageReporters',
80 | 'coverageThreshold',
81 | 'displayName',
82 | 'extraGlobals',
83 | 'globalSetup',
84 | 'globalTeardown',
85 | 'moduleNameMapper',
86 | 'resetMocks',
87 | 'resetModules',
88 | 'restoreMocks',
89 | 'snapshotSerializers',
90 | 'testMatch',
91 | 'transform',
92 | 'transformIgnorePatterns',
93 | 'watchPathIgnorePatterns',
94 | ];
95 | if (overrides) {
96 | supportedKeys.forEach(key => {
97 | if (Object.prototype.hasOwnProperty.call(overrides, key)) {
98 | if (Array.isArray(config[key]) || typeof config[key] !== 'object') {
99 | // for arrays or primitive types, directly override the config key
100 | config[key] = overrides[key];
101 | } else {
102 | // for object types, extend gracefully
103 | config[key] = Object.assign({}, config[key], overrides[key]);
104 | }
105 |
106 | delete overrides[key];
107 | }
108 | });
109 | const unsupportedKeys = Object.keys(overrides);
110 | if (unsupportedKeys.length) {
111 | const isOverridingSetupFile =
112 | unsupportedKeys.indexOf('setupFilesAfterEnv') > -1;
113 |
114 | if (isOverridingSetupFile) {
115 | console.error(
116 | chalk.red(
117 | 'We detected ' +
118 | chalk.bold('setupFilesAfterEnv') +
119 | ' in your package.json.\n\n' +
120 | 'Remove it from Jest configuration, and put the initialization code in ' +
121 | chalk.bold('src/setupTests.js') +
122 | '.\nThis file will be loaded automatically.\n'
123 | )
124 | );
125 | } else {
126 | console.error(
127 | chalk.red(
128 | '\nOut of the box, Create React App only supports overriding ' +
129 | 'these Jest options:\n\n' +
130 | supportedKeys
131 | .map(key => chalk.bold(' \u2022 ' + key))
132 | .join('\n') +
133 | '.\n\n' +
134 | 'These options in your package.json Jest configuration ' +
135 | 'are not currently supported by Create React App:\n\n' +
136 | unsupportedKeys
137 | .map(key => chalk.bold(' \u2022 ' + key))
138 | .join('\n') +
139 | '\n\nIf you wish to override other Jest options, you need to ' +
140 | 'eject from the default setup. You can do so by running ' +
141 | chalk.bold('npm run eject') +
142 | ' but remember that this is a one-way operation. ' +
143 | 'You may also file an issue with Create React App to discuss ' +
144 | 'supporting more options out of the box.\n'
145 | )
146 | );
147 | }
148 |
149 | process.exit(1);
150 | }
151 | }
152 | return config;
153 | };
154 |
--------------------------------------------------------------------------------
/aoife-scripts/config/paths.js:
--------------------------------------------------------------------------------
1 | // @remove-on-eject-begin
2 | /**
3 | * Copyright (c) 2015-present, Facebook, Inc.
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 | // @remove-on-eject-end
9 | "use strict";
10 |
11 | const path = require("path");
12 | const fs = require("fs");
13 | const getPublicUrlOrPath = require("react-dev-utils/getPublicUrlOrPath");
14 |
15 | // Make sure any symlinks in the project folder are resolved:
16 | // https://github.com/facebook/create-react-app/issues/637
17 | const appDirectory = fs.realpathSync(process.cwd());
18 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
19 |
20 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
21 | // "public path" at which the app is served.
22 | // webpack needs to know it to put the right