├── 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 | 5 | 6 | Aoife App 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /create-aoife-app/webpack/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/pillar/Documents/work/github/aoife-group/create-aoife-app/project/src/index.tsx":"1"},{"size":723,"mtime":1609764951690,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"28u1by","/Users/pillar/Documents/work/github/aoife-group/create-aoife-app/project/src/index.tsx",[]] -------------------------------------------------------------------------------- /create-aoife-app/vite/src/app.tsx: -------------------------------------------------------------------------------- 1 | export function App() { 2 | let num = 0; 3 | const ele = ( 4 |
5 |

Hello Vite + Aoife!

6 |

num: {() => num}

7 | 15 |
16 | ); 17 | 18 | return ele; 19 | } 20 | -------------------------------------------------------------------------------- /aoife-scripts/README.md: -------------------------------------------------------------------------------- 1 | # react-scripts 2 | 3 | This package includes scripts and configuration used by [Create React App](https://github.com/facebook/create-react-app). 4 | Please refer to its documentation: 5 | 6 | * [Getting Started](https://facebook.github.io/create-react-app/docs/getting-started) – How to create a new app. 7 | * [User Guide](https://facebook.github.io/create-react-app/) – How to develop apps bootstrapped with Create React App. 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | example 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 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 | -------------------------------------------------------------------------------- /create-aoife-app/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | optimizeDeps: { 5 | exclude: ["monaco-editor", "vscode", "aoife"], 6 | }, 7 | esbuild: { 8 | jsxFactory: "aoife", 9 | jsxFragment: "aoife.Frag", 10 | }, 11 | server: { 12 | proxy: { 13 | "/test-proxy": { 14 | target: "http://127.0.0.1:4080", 15 | changeOrigin: true, 16 | }, 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /create-aoife-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-aoife-app", 3 | "version": "2.0.13", 4 | "bin": { 5 | "create-aoife-app": "bin.js" 6 | }, 7 | "dependencies": { 8 | "fs-extra": "^9.0.1" 9 | }, 10 | "devDependencies": { 11 | "@types/fs-extra": "^9.0.5" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/ymzuiku/aoife.git" 16 | }, 17 | "bugs": { 18 | "url": "git+https://github.com/ymzuiku/aoife.git" 19 | } 20 | } -------------------------------------------------------------------------------- /document/md/常用小方法.md: -------------------------------------------------------------------------------- 1 | ## 常用小方法 2 | 3 | aoife 提供了一些常用方法 4 | 5 | ### 去抖动 debounce 6 | 7 | ```jsx 8 | 11 | ``` 12 | 13 | ### 节流 throttle 14 | 15 | ```jsx 16 | 19 | ``` 20 | 21 | ### 编写 css 22 | 23 | ```jsx 24 | const css = ( 25 | 30 | ); 31 | 32 | document.body.append(css); 33 | ``` 34 | -------------------------------------------------------------------------------- /create-aoife-app/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs-extra"); 4 | const resolve = require("path").resolve; 5 | const pwd = (...args) => resolve(process.cwd(), ...args); 6 | const argv = process.argv.splice(2); 7 | 8 | const build = argv[1] === "--webpack" ? "webpack" : "vite"; 9 | 10 | fs.copySync(resolve(__dirname, build), pwd(argv[0])); 11 | fs.copyFile(resolve(__dirname, "gitignore"), pwd(argv[0], ".gitignore")); 12 | 13 | console.log("create aoife done!"); 14 | console.log("Please run:", `cd ${argv[0]} && yarn install`); 15 | -------------------------------------------------------------------------------- /aoife/lib/helper.ts: -------------------------------------------------------------------------------- 1 | export function isText(obj: unknown) { 2 | const t = Object.prototype.toString.call(obj); 3 | if (t === "[object String]" || t === "[object Number]") { 4 | return true; 5 | } 6 | } 7 | 8 | export function isElement(obj: unknown) { 9 | return Object.prototype.toString.call(obj).indexOf("lement") > 0; 10 | } 11 | 12 | export const flattenOnce = (arr: unknown[]): unknown[] => { 13 | let res = [] as unknown[]; 14 | arr.forEach((item) => { 15 | if (Array.isArray(item)) { 16 | res = res.concat(item); 17 | } else { 18 | res.push(item); 19 | } 20 | }); 21 | return res; 22 | }; 23 | -------------------------------------------------------------------------------- /create-aoife-app/webpack/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Aoife App", 3 | "name": "Create Aoife App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /aoife/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 7 | parser: "@typescript-eslint/parser", 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | ecmaVersion: 12, 13 | sourceType: "module", 14 | }, 15 | plugins: ["@typescript-eslint"], 16 | rules: { 17 | "@typescript-eslint/explicit-module-boundary-types": 0, 18 | "@typescript-eslint/no-non-null-assertion": 0, 19 | "@typescript-eslint/no-unused-vars": 2, 20 | "@typescript-eslint/no-empty-function": 0, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /aoife-scripts/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2014-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 | // This is a custom Jest transformer turning style imports into empty objects. 12 | // http://facebook.github.io/jest/docs/en/webpack.html 13 | 14 | module.exports = { 15 | process() { 16 | return 'module.exports = {};'; 17 | }, 18 | getCacheKey() { 19 | // The output is always the same. 20 | return 'cssTransform'; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /create-aoife-app/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "types": [], 7 | "allowJs": false, 8 | "skipLibCheck": false, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react", 19 | "jsxFactory": "aoife", 20 | "jsxFragmentFactory": "aoife.Frag" 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /update_version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path') 3 | 4 | 5 | 6 | const version = require(path.resolve(__dirname, './aoife/package.json')).version; 7 | 8 | console.log('Update all to:', version); 9 | const changeVersion = (url)=>{ 10 | const realURL = path.resolve(__dirname, url); 11 | const pkg = require(realURL); 12 | pkg.version = version; 13 | if (pkg.dependencies && pkg.dependencies.aoife) { 14 | pkg.dependencies.aoife = "^"+version; 15 | } 16 | fs.writeFileSync(realURL, JSON.stringify(pkg, '', 2)) 17 | } 18 | changeVersion('create-aoife-app/package.json') 19 | changeVersion('create-aoife-app/vite/package.json') 20 | changeVersion('create-aoife-app/webpack/package.json') 21 | -------------------------------------------------------------------------------- /create-aoife-app/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aoife-project", 3 | "version": "2.0.13", 4 | "private": true, 5 | "scripts": { 6 | "start": "aoife-scripts start", 7 | "build": "aoife-scripts build", 8 | "test": "aoife-scripts test", 9 | "eject": "aoife-scripts eject" 10 | }, 11 | "dependencies": { 12 | "aoife": "^2.0.13" 13 | }, 14 | "devDependencies": { 15 | "aoife-scripts": "^4.0.12", 16 | "typescript": "^4.1.3" 17 | }, 18 | "browserslist": { 19 | "production": [ 20 | ">0.2%", 21 | "not dead", 22 | "not op_mini all" 23 | ], 24 | "development": [ 25 | "last 1 chrome version", 26 | "last 1 firefox version", 27 | "last 1 safari version" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /document/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Aoife 官方文档 8 | 14 | 15 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /aoife-scripts/config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | // @remove-file-on-eject 2 | /** 3 | * Copyright (c) 2014-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 babelJest = require('babel-jest'); 11 | 12 | const hasJsxRuntime = (() => { 13 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 14 | return false; 15 | } 16 | 17 | try { 18 | require.resolve('react/jsx-runtime'); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | })(); 24 | 25 | module.exports = babelJest.createTransformer({ 26 | presets: [ 27 | [ 28 | require.resolve('babel-preset-react-app'), 29 | { 30 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 31 | }, 32 | ], 33 | ], 34 | babelrc: false, 35 | configFile: false, 36 | }); 37 | -------------------------------------------------------------------------------- /create-aoife-app/webpack/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "aoife"; 2 | 3 | function Label() { 4 | return new Promise((res) => { 5 | setTimeout(() => res(
Label
), 1000); 6 | }); 7 | } 8 | 9 | function App({ name }: { name: string }) { 10 | let num = 0; 11 | let age = 0; 12 | return ( 13 |
14 |

Hello {name}

15 |
38 | ); 39 | } 40 | 41 | document.body.append(); 42 | -------------------------------------------------------------------------------- /aoife/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "webworker" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": false, 20 | "removeComments": false, 21 | "noEmitHelpers": false, 22 | "importHelpers": true, 23 | "noResolve": false, 24 | "noEmit": true, 25 | "inlineSourceMap": false, 26 | "declaration": false, 27 | "outDir": "./umd", 28 | "jsxFactory": "h", 29 | "jsx": "preserve" 30 | }, 31 | "exclude": [ 32 | "node_modules", 33 | "example/node_modules", 34 | "umd", 35 | "dev" 36 | ], 37 | "include": [ 38 | "lib" 39 | ] 40 | } -------------------------------------------------------------------------------- /create-aoife-app/webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typedocOptions": { 3 | "mode": "modules", 4 | "out": "docs" 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": "./src", 8 | "target": "es5", 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext", 13 | "webworker" 14 | ], 15 | "allowJs": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "module": "esnext", 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "noEmitHelpers": false, 26 | "importHelpers": true, 27 | "noResolve": false, 28 | "noEmit": true, 29 | "inlineSourceMap": false, 30 | "declaration": false, 31 | "outDir": "./umd", 32 | "jsx": "preserve", 33 | "noFallthroughCasesInSwitch": true 34 | }, 35 | "exclude": [ 36 | "node_modules" 37 | ], 38 | "include": [ 39 | "src" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /aoife-scripts/config/pnpTs.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 { resolveModuleName } = require('ts-pnp'); 12 | 13 | exports.resolveModuleName = ( 14 | typescript, 15 | moduleName, 16 | containingFile, 17 | compilerOptions, 18 | resolutionHost 19 | ) => { 20 | return resolveModuleName( 21 | moduleName, 22 | containingFile, 23 | compilerOptions, 24 | resolutionHost, 25 | typescript.resolveModuleName 26 | ); 27 | }; 28 | 29 | exports.resolveTypeReferenceDirective = ( 30 | typescript, 31 | moduleName, 32 | containingFile, 33 | compilerOptions, 34 | resolutionHost 35 | ) => { 36 | return resolveModuleName( 37 | moduleName, 38 | containingFile, 39 | compilerOptions, 40 | resolutionHost, 41 | typescript.resolveTypeReferenceDirective 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /document/README.md: -------------------------------------------------------------------------------- 1 | # Aoife Doc 2 | 3 | > Aoife Doc 是一个极简的 Markdown 运行时 markdown 文档工具 4 | 5 | 不需要配置编译的工程,只需编写 html 和 JSON 配置,引入 JS,即可完成 markdown 文档配置。 6 | 7 | [查看 Demo](https://aoife-doc.writeflowy.com) 8 | 9 | # 使用 10 | 11 | 我们使用命令行创建一个工程 12 | 13 | ```bash 14 | $ npx aoife-doc my-document 15 | ``` 16 | 17 | 在工程内已创建以下 md.json, index.html 2 个文件, 和一个包含若干个 .md 文件的文件夹: 18 | 19 | ``` 20 | - md.json 21 | - index.html 22 | - md/ 23 | - hello.md 24 | - world.md 25 | ``` 26 | 27 | ## 编写 JSON 配置文件 28 | 29 | 其中 md.json 的内容如下: 30 | 31 | ```json 32 | { 33 | "title": "The Aoife Example", 34 | "version": "1.0.0", 35 | "path": "/markdown/", 36 | "files": ["hello", "world"] 37 | } 38 | ``` 39 | 40 | 我们一个个字段解释: 41 | 42 | - title 指文档的标题 43 | - version 指文档的版本,在后续读取 .md 文件时,会根据版本进行缓存 44 | - path 指需要读取的 markdown 文档的文件夹路径 45 | - files 指需要读取的 markdown 文档的文件路径,可以解析多个 .md 文件,根据自行的需要排序 46 | 47 | ## 预览 48 | 49 | 我们借助 serve 启动一个本地静态服务, 然后打开 `http://localhost:5000/` 即可访问文档 50 | 51 | ```bash 52 | $ npx serve -s ./ 53 | ``` 54 | 55 | ## 部署 56 | 57 | 将工程文件拷贝到服务端或其 github.io 即可 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /document/md/生命周期.md: -------------------------------------------------------------------------------- 1 | # 生命周期 2 | 3 | > 生命周期的代码已单独拆分到 vanilla-life 库中,aoife 内部引用 vanilla-life 4 | 5 | aoife 99% 的场景不需要生命周期,但是极个别情况可能会用到,如获取一个元素插入之后的实际宽高等等。 6 | 7 | 为 Element 添加一次性的生命周期(触发即移除监听) 8 | 9 | ```tsx 10 | const out = ( 11 |
{ 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 |
63 |
no update...
64 |
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