├── .eslintrc.json ├── .gitignore ├── README.md ├── doc ├── lone-api │ ├── class_style.md │ ├── component.md │ ├── conditional.md │ ├── introduction.md │ ├── lifecycle.md │ ├── list.md │ ├── router.md │ └── template.md └── lone-guide │ ├── principle.md │ ├── structure.md │ └── structure │ └── messenger-model.gif ├── example ├── basic │ ├── app.config.js │ ├── app.main.js │ ├── app.page.js │ ├── index.html │ └── logic.html └── multi-miniapp │ ├── app.config.js │ ├── app.main.js │ ├── app.page.js │ └── index.html ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── lone-compiler-core │ ├── codegen │ │ ├── events.js │ │ └── index.js │ ├── create-compiler.js │ ├── directives │ │ ├── bind.js │ │ ├── index.js │ │ ├── model.js │ │ └── on.js │ ├── error-detector.js │ ├── helpers.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── parser │ │ ├── entity-decoder.js │ │ ├── filter-parser.js │ │ ├── html-parser.js │ │ ├── index.js │ │ └── text-parser.js │ └── to-function.js ├── lone-compiler-dom │ ├── directives │ │ ├── html.js │ │ ├── index.js │ │ ├── model.js │ │ └── text.js │ ├── index.js │ ├── modules │ │ ├── class.js │ │ ├── index.js │ │ ├── model.js │ │ └── style.js │ ├── options.js │ └── package.json ├── lone-logic-master │ ├── index.js │ └── package.json ├── lone-logic-worker │ ├── index.js │ └── package.json ├── lone-logic │ ├── component │ │ ├── events.js │ │ ├── helper.js │ │ ├── index.js │ │ ├── observer.js │ │ ├── router.js │ │ └── state.js │ ├── index.js │ ├── package.json │ └── schedule.js ├── lone-messenger │ ├── README.md │ ├── index.js │ ├── master │ │ ├── index.js │ │ ├── native-messenger.js │ │ ├── post-messenger.js │ │ └── worker-messenger.js │ ├── package.json │ └── slave │ │ ├── base.js │ │ ├── index.js │ │ ├── native-messenger.js │ │ ├── post-messenger.js │ │ └── worker-messenger.js ├── lone-page │ ├── component │ │ ├── eventListener.js │ │ ├── index.js │ │ ├── init.js │ │ └── slot.js │ ├── index.js │ └── package.json ├── lone-ui │ ├── index.js │ ├── package.json │ ├── page.js │ ├── router.js │ └── schedule.js ├── lone-util │ ├── constants.js │ ├── env.js │ ├── error.js │ ├── index.js │ ├── package.json │ ├── url.js │ └── web │ │ ├── attrs.js │ │ ├── class.js │ │ ├── compat.js │ │ ├── element.js │ │ ├── index.js │ │ └── style.js └── lone-virtualdom │ ├── create-component.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── render-helpers │ ├── h.js │ ├── index.js │ ├── render-list.js │ └── render-slot.js └── scripts ├── replace-loader.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 双线程-多WebView架构下的小程序(实验中) 2 | 3 | 在提交您的贡献之前,请确保花一点事件通读以下准则: 4 | 5 | * [开发环境设置](#开发环境设置) 6 | * [项目架构](#项目架构) 7 | * [工作流程指南](工作流程指南) 8 | 9 | ## 开发环境设置 10 | 11 | 安装依赖: 12 | 13 | ``` 14 | npm install && npm run bootstrap 15 | ``` 16 | 17 | ### 脚本 18 | 19 | #### `npm run dev` 20 | 21 | 持续观察文件系统的变化,当文件修改时,可以重新执行构建流程。 22 | 23 | 该命令会在`/dist`目录创建三个文件: 24 | 25 | * `Lone.logic.js` - Logic线程下的基础库,负责处理与执行用户的JS逻辑。 26 | * `Lone.page.js` - 每个WebView需要携带的JS文件,负责渲染UI。 27 | * `Lone.ui.js` - 负责调度 Logic 与 Page 之间的消息通信,负责路由功能。 28 | * `Lone.[name].js.map` - 用于 Debug 的 SourceMap 文件。 29 | 30 | #### `npm run build` 31 | 32 | 与`dev`类似,但不会持续观察文件系统的变化,并且构建出的文件是经过压缩的。 33 | 34 | ### Demo演示 35 | 36 | 使用 `http-server` 或 Chrome 扩展 `Web Server for Chrome` 浏览Demo。 37 | 38 | ### `http-server` 39 | 40 | 在项目根目录执行以下命令: 41 | 42 | ``` 43 | http-server 44 | ``` 45 | 46 | 然后访问:[http://127.0.0.1:8080/example/basic/](http://127.0.0.1:8080/example/basic/) 47 | 48 | 端口号可以自行设置。 49 | 50 | ### Chrome 浏览器插件 `Web Server for Chrome` 51 | 52 | 插件安装完毕后,可以将目录设置为项目根目录。 53 | 54 | 然后访问:[http://127.0.0.1:8887/example/basic/](http://127.0.0.1:8887/example/basic/) 55 | 56 | 端口号可以自行设置。 57 | 58 | ## 项目架构 59 | 60 | * `lone-compiler-core` - 与平台无关的编译器核心逻辑 61 | * `lone-compiler-dom` - 针对浏览器环境的编译器插件 62 | * `lone-virtualdom` - 虚拟DOM渲染逻辑 63 | * `lone-logic` - 用于执行组件逻辑的核心模块(触发生命周期,执行组件方法等功能),跨环境(同时服务于在沙箱环境下执行的用户组件逻辑和在Page层下执行的官方组件逻辑),且负责与UI线程通信,提供创建组件实例等功能。 64 | * `lone-logic-master` - Page线程下执行逻辑组件(官方逻辑组件)的基础代码,基于`lone-logic`,提供在Page层下执行逻辑组件的基础库代码。 65 | * `lone-logic-worker` - Logic线程下执行逻辑组件的基础代码,基于`lone-logic`,提供在沙箱环境下执行用户组件逻辑的基础库代码。 66 | * `lone-ui` - UI线程下的基础库代码,信号中转站,Master层,负责调度Page与Logic线程之间的通信,负责创建webview,负责路由等全局功能。 67 | * `lone-page` - webview 内部的基础库代码,用于执行组件渲染的核心模块(模板编译,virtualDOM渲染,事件监听等各种UI操作),在页面内执行。 68 | * `lone-messenger` 跨进程通信模块 69 | * `lone-util` 公共工具函数库 70 | 71 | ## 工作流程指南 72 | 73 | 为了确保本项目的代码健康程度不会随着时间的推移慢慢下降,请严格遵循Google工程指南实施代码评审制度(Code Review),在为本项目贡献代码前,请花一部分时间阅读Google工程实践文档。 74 | 75 | 文档地址:[https://github.com/google/eng-practices](https://github.com/google/eng-practices) 76 | 77 | 中文参考资料:[https://github.com/berwin/Blog/issues/44](https://github.com/berwin/Blog/issues/44) 78 | 79 | ### 代码提交流程 80 | 81 | 本项目严格施行PR制度,所有功能都应该通过提交PR合并到分支中。 82 | 83 | 所有功能均需要新建 feature 分支,例如:`feature/v-model`。当功能开发完毕时,将代码提交到远程`feature/v-model`分支,然后在Git系统的`Merge Requests`中点击`New Merge Request`,并详细编写描述。等待Code Review,期间会进行比较频繁的沟通,修改代码再提交Commit,直到PR完全没问题后成功合并代码并删除`feature/v-model`分支,代表该功能开发完毕。 84 | 85 | 但请注意,要保持PR的体积越小越好,否则会导致Code Review非常难以进行,评审者有权让开发者将一个大PR拆分成多个小的PR进行Code Review。 86 | 87 | 提交PR的准则是:PR的数量可以多,但是PR的改动要小。如果是一个改动特别大的PR,则最好拆分成多个小PR分批提交并进行Code Review。 88 | 89 | 格式化代码的PR和功能修改的PR不要同时提交,需要单独提交,以免对Code Review进行干扰。 90 | 91 | #### PR描述准则 92 | 93 | 提交PR时,需要非常清晰的编写描述。 94 | 95 | **第一行**:PR描述的第一行应该是对PR所做的具体工作的简短摘要,然后是空白行。 96 | 97 | **主体**:主体应该尽可能地详细,充分包含代码改动的上下文,以便于Code Review。需要包括 98 | 99 | 1. 背景:该代码解决了什么问题 100 | 2. 解决方案:怎么解决的 101 | 3. 横向对比:为什么这是最好的方法。 102 | 4. 如果该解决方案有任何缺点,则应予以提及。 103 | 104 | 例如: 105 | 106 | > 新增v-model指令,实现双向绑定功能 107 | > 108 | > v-model指令提供了双线程模式下的表单与逻辑的双向绑定功能。 109 | > 110 | > 实现方式借鉴了vue中v-model的实现。在vue中,v-model="value"指令经过编译处理为动态绑定了value, 然后给标签添加一个事件,并且在事件触发的时候动态修改value的值,以达到双向绑定的模式。 111 | > 112 | > 在双线程模式下,直接通过value=XXX是无法完成渲染层和逻辑层的同步更新的,需要分别在渲染层和逻辑层进行数据的更新。而组件逻辑层的setData方法刚好实现了同时更新渲染层和逻辑层的功能。 113 | > 114 | > 因此,当事件触发时,从Page层组件发送通知到逻辑层组件,在逻辑层组件中执行setData方法进行数据更新。步骤如下: 115 | > 116 | > 1. 渲染层触发事件,调用slave模块的send方法将要更新的数据和当前的组件id传递给master主线程。 117 | > 118 | > 2. master主线程监听到对应事件,通过send方法将数据和组件id传递到逻辑层。 119 | > 120 | > 3. 逻辑层监听到对应事件,根据组件id获取要更新的组件,然后调用该组件的setData方法进行逻辑层和渲染层的数据与视图更新。 121 | > 122 | > 缺点:现在的实现方式在性能上不是最优的解决方案。因为在page线程下监听到的change事件,需要先传到UI,然后再传到Logic,然后再传回UI然后再传回page更新DOM。相比之下,在page线程下监听到事件后, 直接更新当前DOM,然后发一个信号到Logic更新Logic中的数据 性能上会提升很多。 123 | > 124 | > 优点:实现方式简单,而且在肉眼上无法观察到明显的性能差别。 125 | 126 | ### 文档 127 | 128 | 新增或修改某个功能时,如果对抛出的API或使用方式产生了变化,则需要同步修改或新增文档。 129 | 130 | > 文档在`/doc/lone-api/`目录下。 131 | 132 | 文档中最好包含该功能的实现原理,以便日后其他开发者在修改这部分功能时,可以知道之前是如何实现的,以及代码整体的设计思路。 133 | 134 | ### Git 流程 135 | 136 | Git流程遵从广为流传的 GitFlow 流程,两个长期分支:master与develop、以及若干个短期分支:feature、hotfix。 137 | 138 | > 由于项目现在并没有线上版本,所以只有一个长期分支:Master。所有feature分支可以从master建立,开发完毕后通过Merge Request合并回Master。 139 | 140 | **长期分支:** 141 | 142 | * `master`:最终产品的代码,始终与线上保持一致,不允许直接在master上开发 143 | * `develop`:汇总所有已完成的新功能的基础分支,长期存在(之后会将代码合并到master)。这里需要注意的是,develop分支的代码始终是已完成的,并且是经过测试无bug的代码。不要将未完成的功能合并到develop。可以将develop视作第二Master。 144 | 145 | **短期分支:** 146 | 147 | * `feature`:feature分支从develop分支生成,当功能开发完毕经过测试没问题后,将代码合并到develop,并且将分支删除。分支仅存在于开发过程中。 148 | * `hotfix`:仅仅存在于对线上功能修复时,临时基于master生成的分支,修复完毕后将代码合并到master。(注意:将代码合并到master后,再合并到develop一份,以避免develop代码落后于master) 149 | 150 | 分支名以 `/` 分割,例如:`feature/a`、`hotfix/b`。 151 | 152 | 参考链接:[https://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/git-flow](https://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/git-flow) -------------------------------------------------------------------------------- /doc/lone-api/class_style.md: -------------------------------------------------------------------------------- 1 | # Class 与 Style 绑定 2 | 3 | 操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是属性,所以我们可以用 `v-bind` 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 `v-bind` 用于 `class` 和 `style` 时做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。 4 | 5 | ## 绑定 HTML Class 6 | 7 | ### 对象语法 8 | 9 | 我们可以传给 `v-bind:class` 一个对象,以动态地切换 `class`: 10 | 11 | ```html 12 |
13 | ``` 14 | 15 | 上面的语法表示 `active` 这个 `class` 存在与否将取决于数据属性 `isActive` 的值。 16 | 17 | 可以在对象中传入更多属性来动态切换多个 `class`。此外,`v-bind:class` 指令也可以与普通的 `class` 属性共存。当有如下模板: 18 | 19 | ```html 20 |
24 | ``` 25 | 26 | 和如下 data: 27 | 28 | ```javascript 29 | data: { 30 | isActive: true, 31 | hasError: false 32 | } 33 | ``` 34 | 35 | 结果渲染为: 36 | 37 | ```html 38 |
39 | ``` 40 | 41 | 当 `isActive` 或者 `hasError` 变化时,class 列表将相应地更新。例如,如果 `hasError` 的值为 `true`,class 列表将变为 `"static active text-danger"`。 42 | 43 | ### 数组语法 44 | 45 | 我们可以把一个数组传给 `v-bind:class`,以应用一个 class 列表: 46 | 47 | ```html 48 |
49 | ``` 50 | 51 | ```javascript 52 | data: { 53 | activeClass: 'active', 54 | errorClass: 'text-danger' 55 | } 56 | ``` 57 | 58 | 渲染为: 59 | 60 | ```html 61 |
62 | ``` 63 | 64 | 如果你也想根据条件切换列表中的 class,可以用三元表达式: 65 | 66 | ```html 67 |
68 | ``` 69 | 70 | 这样写将始终添加 `errorClass`,但是只有在 `isActive` 为真时才添加 `activeClass`。 71 | 72 | ## 绑定内联样式 73 | 74 | ### 对象语法 75 | 76 | `v-bind:style` 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名: 77 | 78 | ```html 79 |
80 | ``` 81 | 82 | ```javascript 83 | data: { 84 | activeColor: 'red', 85 | fontSize: 30 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /doc/lone-api/component.md: -------------------------------------------------------------------------------- 1 | # Component 2 | 3 | ### props 4 | 5 | * 类型:Array | Object 6 | * 详情: 7 | 8 | props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。 9 | 10 | 你可以基于对象的语法使用以下选项: 11 | 12 | * `type`: 可以是下列原生构造函数中的一种:`String`、`Number`、`Boolean`、`Array`、`Object`、`Date`、`Function`、`Symbol`、或上述内容组成的数组。。检查一个 `prop` 是否是给定的类型,否则抛出警告。 13 | * `default`: `any` 为该 `prop` 指定一个默认值。如果该 `prop` 没有被传入,则换做用这个值。 14 | * `required`: `Boolean` 定义该 `prop` 是否是必填项。在非生产环境中,如果这个值为 `true` 且该 `prop` 没有被传入的,则一个控制台警告将会被抛出。 15 | * `validator`: `Function` 自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。你可以在这里查阅更多 prop 验证的相关信息。 16 | * `observer`: `Function` 属性值变化时的回调函数。 17 | 18 | * **代码示例:** 19 | 20 | ``` 21 | // 简单语法 22 | { 23 | props: ['size', 'myMessage'] 24 | } 25 | 26 | // 对象语法 27 | { 28 | props: { 29 | // 检测类型 30 | height: Number, 31 | // 检测类型 + 其他验证 32 | age: { 33 | type: Number, 34 | default: 0, 35 | required: true, 36 | validator: function (value) { 37 | return value >= 0 38 | } 39 | }, 40 | observer: function(newVal, oldVal) { 41 | // 属性值变化时执行 42 | } 43 | } 44 | } 45 | 46 | // 读取数据 47 | this.data.height 48 | ``` 49 | 50 | Props与Data均通过 `this.data` 访问数据。 51 | 52 | ### Slot 53 | 54 | 1. **基本功能** 55 | 56 | 组件实现: 57 | 58 | ```javascript 59 | { 60 | template: ` 61 |
62 | Error! 63 | 64 |
65 | ` 66 | } 67 | ``` 68 | 69 | 组件调用: 70 | 71 | ```html 72 | 73 | Something bad happened. 74 | 75 | ``` 76 | 77 | 渲染的结果为: 78 | 79 | ```html 80 |
81 | Error! 82 | Something bad happened. 83 |
84 | ``` 85 | 86 | 2. **组件调用时,可以设置属性,而这个属性与值,可以在组件内通过`this.data[name]`拿到。** 87 | 88 | ```html 89 | 90 | Your Profile 91 | 92 | ``` 93 | 94 | 然后在 组件内可以访问`url`变量: 95 | 96 | ```html 97 | 98 | 99 | 100 | ``` 101 | 102 | 最终渲染结果为: 103 | 104 | ```html 105 | 106 | Your Profile 107 | 108 | ``` 109 | 110 | 3. **备用内容(打底内容)** 111 | 112 | 备用内容也就是默认的内容,当组件被调用时,没有提供slot数据,那么这时候使用备用内容渲染。(等价于业务中常用的打底数据) 113 | 114 | 例如在一个 `` 组件中: 115 | 116 | ```html 117 | 120 | ``` 121 | 122 | 我们希望用户不提供任何内容时,按钮的内容为文本:Submit,所以为了将“Submit”作为后备内容,我们可以将它放在 标签内: 123 | 124 | ```html 125 | 128 | ``` 129 | 130 | 这样当用户不提供任何内容时: 131 | 132 | ```html 133 | 134 | ``` 135 | 136 | 最终实际上渲染的结果为: 137 | 138 | ```html 139 | 142 | ``` 143 | 144 | 4. **在插槽中使用数据时,应该访问当前组件的数据,而不是子组件的数据。** 145 | 146 | 例如: 147 | 148 | ```html 149 | 150 | Logged in as {{ user.name }} 151 | 152 | ``` 153 | 154 | 插槽内容中的`user.name`应该是当前组件中的数据,而不是`navigation-link`组件中的数据。 155 | 156 | 关于模板访问数据,应始终遵循一个准则: **当前组件的模板里,所有内容都访问当前组件的数据**,例如: 157 | 158 | ```html 159 | 160 | 161 | 162 | {{ foo }} {{ bar }} {{ baz }} 163 | 164 | 165 | 166 | ``` 167 | 168 | 在模板中访问数据,无论嵌套多少层组件的Slot,都只访问本组件的数据。 169 | 170 | 5. **具名插槽** 171 | 172 | 有时我们需要多个插槽。例如对于一个带有如下模板的 组件: 173 | 174 | ```html 175 |
176 |
177 | 178 |
179 |
180 | 181 |
182 |
183 | 184 |
185 |
186 | ``` 187 | 188 | 对于这样的情况,`` 元素有一个特殊的特性:`name`。这个特性可以用来定义额外的插槽: 189 | 190 | ```html 191 |
192 |
193 | 194 |
195 |
196 | 197 |
198 |
199 | 200 |
201 |
202 | ``` 203 | 204 | 不带 name 的 出口会带有隐含的名字“default”。 205 | 206 | 在向具名插槽提供内容的时候,我们可以在元素上使用 `slot` 属性的形式提供其名称: 207 | 208 | ```html 209 | 210 |

Slot Header

211 |

Slot main content.

212 |

Slot Footer

213 |
214 | ``` 215 | 216 | 现在元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带`slot` 属性中的内容都会被视为默认插槽内容。 217 | 218 | 支持更明确的写法,可以使用`slot="default"`包裹默认插槽的内容: 219 | 220 | ```html 221 | 222 |

Slot Header

223 |

Slot main content.

224 |

Slot Footer

225 |
226 | ``` 227 | 228 | 任何一种写法都会渲染出: 229 | 230 | ```html 231 |
232 |
233 |

Slot Header

234 |
235 |
236 |

Slot main content.

237 |
238 |
239 |

Slot Footer

240 |
241 |
242 | ``` 243 | -------------------------------------------------------------------------------- /doc/lone-api/conditional.md: -------------------------------------------------------------------------------- 1 | # 条件渲染 2 | 3 | ## v-if 4 | 5 | v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 `truthy` 值的时候被渲染。 6 | 7 | ```html 8 |

Vue is awesome!

9 | ``` 10 | 11 | 也可以用 `v-else` 添加一个“else 块”: 12 | 13 | ```html 14 |

Vue is awesome!

15 |

Oh no 😢

16 | ``` 17 | 18 | ## v-else-if 19 | 20 | `v-else-if`,顾名思义,充当 `v-if` 的 “else-if 块”,可以连续使用: 21 | 22 | ```html 23 |
24 | A 25 |
26 |
27 | B 28 |
29 |
30 | C 31 |
32 |
33 | Not A/B/C 34 |
35 | ``` 36 | 37 | ## v-show 38 | 39 | 另一个用于根据条件展示元素的选项是 `v-show` 指令。用法大致一样: 40 | 41 | ```html 42 |

Hello!

43 | ``` 44 | 45 | 不同的是带有 `v-show` 的元素始终会被渲染并保留在 DOM 中。`v-show` 只是简单地切换元素的 CSS 属性 `display`。 46 | 47 | ## v-if vs v-show 48 | 49 | `v-if` 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。 50 | 51 | `v-if` 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。 52 | 53 | 相比之下,`v-show` 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。 54 | 55 | 一般来说,`v-if` 有更高的切换开销,而 `v-show` 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 `v-show` 较好;如果在运行时条件很少改变,则使用 `v-if` 较好。 56 | -------------------------------------------------------------------------------- /doc/lone-api/introduction.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 该框架完全基于Web技术提供了小程序运行引擎。 4 | 5 | 框架运行架构为:双线程 + 多WebView 架构。双线程指的是主线程(主页面)与逻辑线程(`web-worker`)。多WebView指的是小程序切换路由时是多页应用,目前技术方案为多个iframe叠加在一起实现路由跳转功能,每个iframe为一个小程序页面。 6 | 7 | 该框架提供三个JS文件,分别为:`Lone.ui.js`、`Lone.logic.js`与`Lone.page.js`。 8 | 9 | * `Lone.ui.js`:该文件需在主页面中引入,负责收集小程序的全局配置,控制小程序的路由,调度进程间的信号传递等功能。 10 | * `Lone.logic.js`:该文件在`web-worker`或其他沙箱环境下引入,负责注册用户的逻辑层组件,执行用户的生命周期,提供一些API等。 11 | * `Lone.page.js`:该文件在Page中引入,`Lone.ui.js`在创建页面时,会自动将该文件引入到iframe中。该文件负责注册用户的渲染组件,控制模板渲染,模板编译等功能。 12 | 13 | 由于组件的逻辑和渲染在不同的环境下执行,所以组件的逻辑部分与渲染部分是拆开注册的。因此,逻辑组件指的是组件的逻辑执行部分,渲染组件指组件的模板渲染部分。 14 | 15 | 简单来说,组件的模板是渲染组件,组件的其他(数据,逻辑,生命周期等)为逻辑组件。 16 | 17 | ## Lone.ui(options) 18 | 19 | 该方法需要在主页面执行,每执行一次便实例化一个小程序。多次执行将在当前页面实例化多个小程序。 20 | 21 | **基础用法:** 22 | 23 | ```javascript 24 | Lone.ui({ 25 | entry: { 26 | logic: './app.main.js', 27 | page: './app.page.js' 28 | }, 29 | routes: [ 30 | { path: '/', component: 'test' }, 31 | { path: '/test', component: 'test2' }, 32 | { path: '/official', component: 'official' }, 33 | { path: '/lifecycle', component: 'lifecycle' } 34 | ] 35 | }) 36 | ``` 37 | 38 | ### options - 必填项 39 | 40 | #### `entry` [Object] - 必填项 41 | 42 | 用于设置用户代码文件地址,对象下有两个Key,均为必填项,分别是:`logic`与`page`。 43 | 44 | * `logic`:该参数配置的URL所指向的文件会在沙箱环境下(例如:`web-worker`)引入并执行,该文件包含用户的逻辑组件注册代码。 45 | * `page`:该参数配置的URL所指向的文件会在Page下引入并执行,该文件包含选渲染组件的注册逻辑。 46 | 47 | #### `routes` [Array] - 必填项 48 | 49 | 配置小程序路由,数组中的每个 `Object` 为一个路由配置,配置中可以配置的参数为: 50 | 51 | * `path` [String] - 路由地址 52 | * `component` [String] - 路由所对应的根组件名 53 | 54 | #### `container` [String] - 可选项 55 | 56 | 用于配置小程序的容器元素,默认值 `document.body`。**当一个页面内需要实例化多个小程序时非常有用。** 57 | 58 | 该参数的值为选择器字符串,内部使用`document.querySelector`实现,所以支持该API的所有语法。例如:`#cube-one`。 59 | 60 | #### `mid` [String] - 可选项 61 | 62 | 用于设置小程序ID,`mid` 为 Miniapp ID 的缩写形式,默认值 `[timestamp_count]`,每个小程序都有自己独立的`mid`。**当一个页面内需要实例化多个小程序时非常有用。** 63 | 64 | ## Lone.logic(componentName, options) 65 | 66 | 逻辑组件注册器,该API用于将用户的逻辑代码注册到沙箱中执行。除了组件的模板,其余的一切(数据,生命周期,方法等)都通过该API注册。 67 | 68 | 例如: 69 | 70 | ```javascript 71 | Lone.logic('test', { 72 | data () { 73 | return { 74 | n: 0, 75 | msg: 'default text' 76 | } 77 | }, 78 | methods: { 79 | showModel () { 80 | console.log(this.data) 81 | } 82 | }, 83 | mounted () { 84 | setTimeout(_ => { 85 | this.setData({ 86 | n: 2 87 | }) 88 | }, 1000) 89 | } 90 | }) 91 | ``` 92 | 93 | ### componentName - 必选项 94 | 95 | 组件名,该组件名一定要和渲染组件相同。在框架内部,将组件的逻辑与组件的模板联系在一起的唯一凭证是组件名,所以在注册组件时,同一个组件的模板和逻辑,使用不同的api注册时,组件名一定要相同。 96 | 97 | ### options - 可选项 98 | 99 | 逻辑组件的 生命周期,数据,方法等都在该 `options` 中设置。更多详细用法请参考详细的API文档。 100 | 101 | > 当不设置任何配置时,表示该组件无任何逻辑与数据,只是单纯的模板渲染。 102 | 103 | ## Lone.page(options) 104 | 105 | 渲染组件注册器,该API用于在Page内部执行,注册组件的模板。当组件的模板被注册后,会根据路由的配置实例化根组件。 106 | 107 | 例子: 108 | 109 | ```javascript 110 | Lone.page({ 111 | components: [ 112 | { 113 | name: 'test', 114 | template: '
Message: {{ msg }}
' 115 | }, 116 | { 117 | name: 'ad', 118 | template: '
我是广告
' 119 | } 120 | ] 121 | }) 122 | ``` 123 | 124 | ### options - 必填项 125 | 126 | #### `components: Array` 127 | 128 | 数组中的每个对象为一个独立的组件配置。 129 | 130 | **name: String** - 必填项 131 | 132 | 组件名,该组件名必须与其对应的逻辑组件保持一致。 133 | 134 | **template: String** - 必填项 135 | 136 | 模板字符串,支持Vue的所有模板语法,例如:指令,slot等。 137 | 138 | **official: Boolean** - 可选项 139 | 140 | 标识该组件是否为 **官方组件**。 141 | 142 | 官方组件代码逻辑不应该放在web-worker里,应该直接放在Page层,已获得更高的渲染权限,所以官方组件需要使用`official`标识自己为官方组件。 143 | 144 | 如果组件为官方组件,那么组件逻辑直接在 `Lone.page` 中注册,无需使用 `Lone.logic` 注册。 145 | 146 | 例如: 147 | 148 | ```javascript 149 | Lone.page({ 150 | official: true, 151 | name: 'test', 152 | template: ` 153 |
154 |

我是官方组件,我可以直接用JS访问DOM与BOM

155 |

因为我的JS在页面内的UI主线程中运行

156 |
157 | `, 158 | onReady () { 159 | console.log(document, window) 160 | } 161 | }) 162 | ``` 163 | -------------------------------------------------------------------------------- /doc/lone-api/lifecycle.md: -------------------------------------------------------------------------------- 1 | # 生命周期 2 | 3 | * beforeCreate 4 | * created && onLoad 5 | * onShow 6 | * beforeMount 7 | * onReady && mounted 8 | * beforeUpdate 9 | * updated 10 | * onHide 11 | 12 | #### beforeCreate 13 | 14 | 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。 15 | 16 | #### created && onLoad 17 | 18 | 在实例创建完成后被立即调用。在这一步,除了UI还没有渲染,其他都已经完成。 19 | 20 | #### onShow 21 | 22 | 页面展示的时候被调用 23 | 24 | #### beforeMount 25 | 26 | 在挂载开始之前被调用:相关的 render 函数首次被调用。 27 | 28 | #### onReady && mounted 29 | 30 | 首次渲染完成后触发。 31 | 32 | #### beforeUpdate 33 | 34 | 数据更新后,发生在虚拟 DOM 打补丁之前调用。 35 | 36 | 换句话说,渲染前调用。 37 | 38 | #### updated 39 | 40 | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 41 | 42 | 换句话说,渲染完成后调用。 43 | 44 | #### onHide 45 | 46 | 页面隐藏的时候被调用 47 | -------------------------------------------------------------------------------- /doc/lone-api/list.md: -------------------------------------------------------------------------------- 1 | # 列表渲染 2 | 3 | ## 用 v-for 把一个数组对应为一组元素 4 | 5 | 我们可以用 `v-for` 指令基于一个数组来渲染一个列表。`v-for` 指令需要使用 `item in items` 形式的特殊语法,其中 `items` 是源数据数组,而 `item` 则是被迭代的数组元素的别名。 6 | 7 | ```html 8 |
    9 |
  • 10 | {{ item.message }} 11 |
  • 12 |
13 | ``` 14 | 15 | ```javascript 16 | var example1 = new Vue({ 17 | el: '#example-1', 18 | data: { 19 | items: [ 20 | { message: 'Foo' }, 21 | { message: 'Bar' } 22 | ] 23 | } 24 | }) 25 | ``` 26 | 结果: 27 | 28 | ```html 29 |
    30 |
  • Foo
  • 31 |
  • Bar
  • 32 |
33 | ``` 34 | 35 | `v-for` 还支持一个可选的第二个参数,即当前项的索引。 36 | 37 | ```html 38 |
    39 |
  • 40 | {{ index }} - {{ item.message }} 41 |
  • 42 |
43 | ``` 44 | 45 | 你也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法: 46 | 47 | ```html 48 |
49 | ``` 50 | 51 | ## 在 v-for 里使用对象 52 | 53 | 你也可以用 v-for 来遍历一个对象的属性。 54 | 55 | ```html 56 |
    57 |
  • 58 | {{ value }} 59 |
  • 60 |
61 | ``` 62 | 63 | ```javascript 64 | new Vue({ 65 | el: '#v-for-object', 66 | data: { 67 | object: { 68 | title: 'How to do lists in Vue', 69 | author: 'Jane Doe', 70 | publishedAt: '2016-04-10' 71 | } 72 | } 73 | }) 74 | ``` 75 | 76 | 结果: 77 | 78 | ```html 79 |
    80 |
  • How to do lists in Vue
  • 81 |
  • Jane Doe
  • 82 |
  • 2016-04-10
  • 83 |
84 | ``` 85 | 也可以提供第二个的参数为 property 名称 (也就是键名): 86 | 87 | ```html 88 |
89 | {{ name }}: {{ value }} 90 |
91 | ``` 92 | 93 | 还可以用第三个参数作为索引: 94 | 95 | ```html 96 |
97 | {{ index }}. {{ name }}: {{ value }} 98 |
99 | ``` -------------------------------------------------------------------------------- /doc/lone-api/router.md: -------------------------------------------------------------------------------- 1 | # 路由 2 | 3 | #### `this.navigateTo(options)` 4 | 5 | 跳转到新页面。 6 | 7 | ```javascript 8 | this.navigateTo({ 9 | url: '/test', 10 | success () {}, 11 | fail () {}, 12 | complete () {} 13 | }) 14 | ``` 15 | 16 | **参数具体描述:** 17 | 18 | | 属性 | 类型 | 必填 | 说明 | 19 | | ---- | ---- | ---- | ---- | 20 | | url | String | 是 | 需要跳转的页面的路由地址,路径后可以带参数。参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 'path?key=value&key2=value2' | 21 | | success | Function | 否 | 接口调用成功的回调函数 | 22 | | fail | Function | 否 | 接口调用失败的回调函数 | 23 | | complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) | 24 | 25 | #### `this.redirectTo(options)` 26 | 27 | 参数与 `this.navigateTo` 相同,只是先移除当前页面,然后在跳转到新页面。 28 | 29 | #### `this.navigateBack(options)` 30 | 31 | 关闭当前页面,返回上一页面或多级页面。 32 | 33 | **参数具体描述:** 34 | 35 | | 属性 | 类型 | 必填 | 说明 | 36 | | ---- | ---- | ---- | ---- | 37 | | delta | Number | 否 | 返回的页面数,如果 delta 大于现有页面数,则返回到首页。默认值:1 | 38 | | success | Function | 否 | 接口调用成功的回调函数 | 39 | | fail | Function | 否 | 接口调用失败的回调函数 | 40 | | complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) | 41 | 42 | -------------------------------------------------------------------------------- /doc/lone-api/template.md: -------------------------------------------------------------------------------- 1 | # 模板 2 | 3 | 模板支持绝大多数Vue常用的语法。 4 | 5 | ## 文本 6 | 7 | 使用“Mustache”语法 (双大括号) 向文本中插值。 8 | 9 | ```html 10 | Message: {{ msg }} 11 | ``` 12 | 13 | ## 原始 HTML 14 | 15 | 双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 `v-html` 指令: 16 | 17 | 18 | ```html 19 |

Using mustaches: {{ rawHtml }}

20 |

Using v-html directive:

21 | ``` 22 | 23 | ## Attribute 24 | 25 | Mustache 语法不能作用在 HTML attribute 上,遇到这种情况应该使用 `v-bind` 指令: 26 | 27 | ```html 28 |
29 | ``` 30 | 31 | 或使用简写形式: 32 | 33 | ```html 34 |
35 | ``` 36 | 37 | 对于布尔 attribute (它们只要存在就意味着值为 true),`v-bind` 工作起来略有不同,在这个例子中: 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | 如果 `isButtonDisabled` 的值是 `null`、`undefined` 或 `false`,则 disabled attribute 甚至不会被包含在渲染出来的 ` 10 |
  • 11 |
  • 12 |
  • 13 | 14 |
      15 |
    • item: {{item}}, index: {{index}}
    • 16 |
    17 |
      18 |
    • 19 | {{ index }}. {{ name }}: {{ value }} 20 |
    • 21 |
    22 |

    N:{{ n }}

    23 | 24 | I'm a Slot content. 25 | 26 | 27 |

    Slot Header

    28 |

    29 | 30 |

    Nest slot Header

    31 |

    Nest slot main content.{{list}}

    32 |

    Nest slot2 Footer

    33 |
    34 |

    35 |

    Slot Footer

    36 | 37 | 38 | 39 | 40 | ` 41 | }, 42 | { 43 | name: 'ad', 44 | template: '
    我是广告{{n}} {{title}}, {{list}}
    ' 45 | }, 46 | { 47 | name: 'alert', 48 | template: '
    I\'m a Slot default content.
    ' 49 | }, 50 | { 51 | name: 'base-layout', 52 | template: ` 53 |
    54 |
    55 |
    56 | 57 |
    58 |
    59 | 60 |
    61 |
    62 | 63 |
    64 |
    65 |
    66 | ` 67 | }, 68 | { 69 | name: 'v-model', 70 | template: `
    71 | 72 |

    you input is: {{n}}

    73 |

    click me show the input model on console.

    74 |

    textarea:

    75 | 76 |
    77 | Multiline message is:{{ message }} 78 | 79 |

    单个复选框,绑定到布尔值:

    80 |
    81 | 82 | 83 |
    84 |

    多个复选框,绑定到同一个数组:

    85 |
    86 | 87 | 88 | 89 | 90 | 91 | 92 |
    93 | Checked names: {{ checkedNames }} 94 |
    95 |

    单选按钮

    96 |
    97 | 98 | 99 |
    100 | 101 | 102 |
    103 | Picked: {{ picked }} 104 |
    105 |
    106 | ` 107 | }, 108 | { 109 | official: true, 110 | name: 'official', 111 | template: ` 112 |
    113 | 114 |

    我是官方组件,我可以直接用JS访问DOM与BOM: {{n}}

    115 |

    因为我的JS在页面内的UI主线程中运行

    116 |
    117 | `, 118 | data () { 119 | return { 120 | n: 0 121 | } 122 | }, 123 | onReady () { 124 | setTimeout(() => { 125 | this.setData({ n: 1 }) 126 | }, 1000) 127 | console.log(document, window) 128 | }, 129 | back () { 130 | this.navigateBack() 131 | } 132 | }, 133 | { 134 | name: 'handle-error', 135 | template: ` 136 |
    137 |

    Handle Error Section

    138 |
    139 |
    140 | ` 141 | }, 142 | { 143 | name: 'lifecycle', 144 | template: ` 145 |
    146 |

    lifecycle

    147 |
    148 |
    149 |
    150 |
    151 |
    {{item}}
    152 |
    153 | 154 |
    155 | ` 156 | }, 157 | { 158 | name: 'destory', 159 | template: ` 160 |
    161 | 162 |

    销毁组件Demo

    163 |
    164 | ` 165 | }, 166 | { 167 | name: 'query', 168 | template: ` 169 |
    170 |
    171 |
    172 |

    Query:{{query}}

    173 |
    174 | ` 175 | } 176 | ] 177 | }) 178 | -------------------------------------------------------------------------------- /example/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/basic/logic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lone.js • Basic 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/multi-miniapp/app.config.js: -------------------------------------------------------------------------------- 1 | Lone.ui({ 2 | container: '#cube-one', 3 | entry: { 4 | logic: './app.main.js', 5 | page: './app.page.js' 6 | }, 7 | routes: [ 8 | { path: '/', component: 'test' }, 9 | { path: '/test2', component: 'test2' } 10 | ] 11 | }) 12 | 13 | Lone.ui({ 14 | container: '#cube-two', 15 | entry: { 16 | logic: './app.main.js', 17 | page: './app.page.js' 18 | }, 19 | routes: [ 20 | { path: '/', component: 'test' }, 21 | { path: '/test2', component: 'test2' } 22 | ] 23 | }) 24 | -------------------------------------------------------------------------------- /example/multi-miniapp/app.main.js: -------------------------------------------------------------------------------- 1 | importScripts('../../dist/lone.logic.js') 2 | 3 | Lone.logic('test', { 4 | data: () => ({ list: [1, 2] }), 5 | methods: { 6 | to (url) { 7 | this.navigateTo({ url }) 8 | } 9 | }, 10 | mounted () { 11 | const vm = this 12 | setTimeout(() => { 13 | vm.setData({ 14 | list: [...vm.data.list, 3] 15 | }) 16 | }, 1000) 17 | } 18 | }) 19 | 20 | Lone.logic('test2', { 21 | data () { 22 | return { 23 | n: 0 24 | } 25 | }, 26 | onReady () { 27 | setTimeout(() => { 28 | this.setData({ n: 1 }) 29 | }, 1000) 30 | }, 31 | back () { 32 | this.navigateBack() 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /example/multi-miniapp/app.page.js: -------------------------------------------------------------------------------- 1 | Lone.page({ 2 | components: [ 3 | { 4 | name: 'test', 5 | template: ` 6 |
    7 |

    Page 1

    8 |
      9 |
    • {{item}}
    • 10 |
    11 |
      12 |
    • 13 |
    14 |
    15 | ` 16 | }, 17 | { 18 | name: 'test2', 19 | template: ` 20 |
    21 | 22 |

    我是第二个页面: {{n}}

    23 | 24 |
    25 | ` 26 | } 27 | ] 28 | }) 29 | -------------------------------------------------------------------------------- /example/multi-miniapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 多小程序并存 8 | 15 | 16 | 17 |
    18 |
    19 |
    20 |
    21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "dist/lone.js", 6 | "scripts": { 7 | "dev": "webpack -w --config scripts/webpack.dev.conf.js", 8 | "build": "webpack --config scripts/webpack.prod.conf.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "bootstrap": "lerna bootstrap" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.6.4", 14 | "@babel/plugin-proposal-decorators": "^7.6.0", 15 | "@babel/plugin-transform-modules-commonjs": "^7.7.5", 16 | "@babel/preset-env": "^7.6.3", 17 | "babel-eslint": "^10.0.3", 18 | "babel-loader": "^8.0.6", 19 | "eslint": "^6.5.1", 20 | "eslint-config-standard": "^14.1.0", 21 | "eslint-friendly-formatter": "^4.0.1", 22 | "eslint-loader": "^3.0.2", 23 | "eslint-plugin-import": "^2.18.2", 24 | "eslint-plugin-node": "^10.0.0", 25 | "eslint-plugin-promise": "^4.2.1", 26 | "eslint-plugin-standard": "^4.0.1", 27 | "lerna": "^3.15.0", 28 | "webpack": "^4.41.1", 29 | "webpack-cli": "^3.3.9", 30 | "webpack-merge": "^4.2.2" 31 | } 32 | } -------------------------------------------------------------------------------- /packages/lone-compiler-core/codegen/events.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const fnExpRE = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/ 4 | const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/ 5 | 6 | // keyCode aliases 7 | const keyCodes = { 8 | esc: 27, 9 | tab: 9, 10 | enter: 13, 11 | space: 32, 12 | up: 38, 13 | left: 37, 14 | right: 39, 15 | down: 40, 16 | delete: [8, 46] 17 | } 18 | 19 | // #4868: modifiers that prevent the execution of the listener 20 | // need to explicitly return null so that we can determine whether to remove 21 | // the listener for .once 22 | const genGuard = condition => `if(${condition})return null;` 23 | 24 | const modifierCode = { 25 | stop: '$event.stopPropagation();', 26 | prevent: '$event.preventDefault();', 27 | self: genGuard('$event.target !== $event.currentTarget'), 28 | ctrl: genGuard('!$event.ctrlKey'), 29 | shift: genGuard('!$event.shiftKey'), 30 | alt: genGuard('!$event.altKey'), 31 | meta: genGuard('!$event.metaKey'), 32 | left: genGuard('\'button\' in $event && $event.button !== 0'), 33 | middle: genGuard('\'button\' in $event && $event.button !== 1'), 34 | right: genGuard('\'button\' in $event && $event.button !== 2') 35 | } 36 | 37 | export function genHandlers (events, isNative, warn) { 38 | let res = isNative ? 'nativeOn:{' : 'on:{' 39 | for (const name in events) { 40 | const handler = events[name] 41 | // #5330: warn click.right, since right clicks do not actually fire click events. 42 | if (process.env.NODE_ENV !== 'production' && 43 | name === 'click' && 44 | handler && handler.modifiers && handler.modifiers.right 45 | ) { 46 | warn( 47 | 'Use "contextmenu" instead of "click.right" since right clicks ' + 48 | 'do not actually fire "click" events.' 49 | ) 50 | } 51 | res += `"${name}":${genHandler(name, handler)},` 52 | } 53 | return res.slice(0, -1) + '}' 54 | } 55 | 56 | function genHandler (name, handler) { 57 | if (!handler) { 58 | return 'function(){}' 59 | } 60 | 61 | if (Array.isArray(handler)) { 62 | return `[${handler.map(handler => genHandler(name, handler)).join(',')}]` 63 | } 64 | 65 | const isMethodPath = simplePathRE.test(handler.value) 66 | const isFunctionExpression = fnExpRE.test(handler.value) 67 | 68 | if (!handler.modifiers) { 69 | return isMethodPath || isFunctionExpression 70 | ? handler.value 71 | : `function($event){${handler.value}}` // inline statement 72 | } else { 73 | let code = '' 74 | let genModifierCode = '' 75 | const keys = [] 76 | for (const key in handler.modifiers) { 77 | if (modifierCode[key]) { 78 | genModifierCode += modifierCode[key] 79 | // left/right 80 | if (keyCodes[key]) { 81 | keys.push(key) 82 | } 83 | } else if (key === 'exact') { 84 | const modifiers = (handler.modifiers) 85 | genModifierCode += genGuard( 86 | ['ctrl', 'shift', 'alt', 'meta'] 87 | .filter(keyModifier => !modifiers[keyModifier]) 88 | .map(keyModifier => `$event.${keyModifier}Key`) 89 | .join('||') 90 | ) 91 | } else { 92 | keys.push(key) 93 | } 94 | } 95 | if (keys.length) { 96 | code += genKeyFilter(keys) 97 | } 98 | // Make sure modifiers like prevent and stop get executed after key filtering 99 | if (genModifierCode) { 100 | code += genModifierCode 101 | } 102 | const handlerCode = isMethodPath 103 | ? handler.value + '($event)' 104 | : isFunctionExpression 105 | ? `(${handler.value})($event)` 106 | : handler.value 107 | return `function($event){${code}${handlerCode}}` 108 | } 109 | } 110 | 111 | function genKeyFilter (keys) { 112 | return `if(!('button' in $event)&&${keys.map(genFilterCode).join('&&')})return null;` 113 | } 114 | 115 | function genFilterCode (key) { 116 | const keyVal = parseInt(key, 10) 117 | if (keyVal) { 118 | return `$event.keyCode!==${keyVal}` 119 | } 120 | const code = keyCodes[key] 121 | return ( 122 | '_k($event.keyCode,' + 123 | `${JSON.stringify(key)},` + 124 | `${JSON.stringify(code)},` + 125 | '$event.key)' 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/codegen/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { genHandlers } from './events' 4 | import baseDirectives from '../directives/index' 5 | import { camelize, no, extend } from 'lone-util' 6 | import { baseWarn, pluckModuleFunction } from '../helpers' 7 | 8 | export class CodegenState { 9 | constructor (options) { 10 | this.options = options 11 | this.warn = options.warn || baseWarn 12 | this.transforms = pluckModuleFunction(options.modules, 'transformCode') 13 | this.dataGenFns = pluckModuleFunction(options.modules, 'genData') 14 | this.directives = extend(extend({}, baseDirectives), options.directives) 15 | const isReservedTag = options.isReservedTag || no 16 | this.maybeComponent = el => !isReservedTag(el.tag) 17 | this.onceId = 0 18 | this.staticRenderFns = [] 19 | } 20 | } 21 | 22 | export function generate (ast, options) { 23 | const state = new CodegenState(options) 24 | // 如果ast为空,则创建一个空div 25 | const code = ast ? genElement(ast, state) : '_c("div")' 26 | return { 27 | render: `with(this){return ${code}}`, 28 | staticRenderFns: state.staticRenderFns 29 | } 30 | } 31 | 32 | export function genElement (el, state) { 33 | if (el.staticRoot && !el.staticProcessed) { 34 | return genStatic(el, state) 35 | } else if (el.once && !el.onceProcessed) { 36 | return genOnce(el, state) 37 | } else if (el.for && !el.forProcessed) { 38 | return genFor(el, state) 39 | } else if (el.if && !el.ifProcessed) { 40 | return genIf(el, state) 41 | } else if (el.tag === 'template' && !el.slotTarget) { 42 | return genChildren(el, state) || 'void 0' 43 | } else if (el.tag === 'slot') { 44 | return genSlot(el, state) 45 | } else { 46 | // component or element 47 | let code 48 | if (el.component) { 49 | code = genComponent(el.component, el, state) 50 | } else { 51 | // 如果el.plain是true,说明该结点没有 attr, attrsList.length === 0 52 | const data = el.plain ? undefined : genData(el, state) 53 | 54 | const children = el.inlineTemplate ? null : genChildren(el, state, true) 55 | code = `_c('${el.tag}'${ 56 | data ? `,${data}` : '' // data 57 | }${ 58 | children ? `,${children}` : '' // children 59 | })` 60 | } 61 | // module transforms 62 | for (let i = 0; i < state.transforms.length; i++) { 63 | code = state.transforms[i](el, code) 64 | } 65 | return code 66 | } 67 | } 68 | 69 | // hoist static sub-trees out 70 | function genStatic (el, state) { 71 | el.staticProcessed = true 72 | state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) 73 | return `_m(${state.staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})` 74 | } 75 | 76 | // v-once 77 | // once 其实就当做静态节点处理 78 | function genOnce (el, state) { 79 | el.onceProcessed = true 80 | if (el.if && !el.ifProcessed) { 81 | return genIf(el, state) 82 | } else if (el.staticInFor) { 83 | let key = '' 84 | let parent = el.parent 85 | while (parent) { 86 | if (parent.for) { 87 | key = parent.key 88 | break 89 | } 90 | parent = parent.parent 91 | } 92 | if (!key) { 93 | process.env.NODE_ENV !== 'production' && state.warn( 94 | 'v-once can only be used inside v-for that is keyed. ' 95 | ) 96 | return genElement(el, state) 97 | } 98 | return `_o(${genElement(el, state)},${state.onceId++},${key})` 99 | } else { 100 | return genStatic(el, state) 101 | } 102 | } 103 | 104 | export function genIf (el, state, altGen, altEmpty) { 105 | el.ifProcessed = true // avoid recursion 106 | return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) 107 | } 108 | 109 | function genIfConditions (conditions, state, altGen, altEmpty) { 110 | if (!conditions.length) { 111 | return altEmpty 112 | } 113 | 114 | const condition = conditions.shift() 115 | if (condition.exp) { 116 | return `(${condition.exp})?${ 117 | genTernaryExp(condition.block) 118 | }:${ 119 | genIfConditions(conditions, state, altGen, altEmpty) 120 | }` 121 | } else { 122 | return `${genTernaryExp(condition.block)}` 123 | } 124 | 125 | // v-if with v-once should generate code like (a)?_m(0):_m(1) 126 | function genTernaryExp (el) { 127 | return altGen 128 | ? altGen(el, state) 129 | : el.once 130 | ? genOnce(el, state) 131 | : genElement(el, state) 132 | } 133 | } 134 | 135 | export function genFor (el, state, altGen, altHelper) { 136 | const exp = el.for 137 | const alias = el.alias 138 | const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' 139 | const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' 140 | 141 | if (process.env.NODE_ENV !== 'production' && 142 | state.maybeComponent(el) && 143 | el.tag !== 'slot' && 144 | el.tag !== 'template' && 145 | !el.key 146 | ) { 147 | state.warn( 148 | `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + 149 | 'v-for should have explicit keys. ' + 150 | 'See https://vuejs.org/guide/list.html#key for more info.', 151 | true /* tip */ 152 | ) 153 | } 154 | 155 | el.forProcessed = true // avoid recursion 156 | return `${altHelper || '_l'}((${exp}),` + 157 | `function(${alias}${iterator1}${iterator2}){` + 158 | `return ${(altGen || genElement)(el, state)}` + 159 | '})' 160 | } 161 | 162 | export function genData (el, state) { 163 | let data = '{' 164 | 165 | // directives first. 166 | // directives may mutate the el's other properties before they are generated. 167 | const dirs = genDirectives(el, state) 168 | if (dirs) data += dirs + ',' 169 | 170 | // key 171 | if (el.key) { 172 | data += `key:${el.key},` 173 | } 174 | // ref 175 | if (el.ref) { 176 | data += `ref:${el.ref},` 177 | } 178 | if (el.refInFor) { 179 | data += 'refInFor:true,' 180 | } 181 | // pre 182 | if (el.pre) { 183 | data += 'pre:true,' 184 | } 185 | // record original tag name for components using "is" attribute 186 | if (el.component) { 187 | data += `tag:"${el.tag}",` 188 | } 189 | // module data generation functions 190 | for (let i = 0; i < state.dataGenFns.length; i++) { 191 | data += state.dataGenFns[i](el) 192 | } 193 | // attributes 194 | if (el.attrs) { 195 | data += `attrs:{${genProps(el.attrs)}},` 196 | } 197 | // DOM props 198 | if (el.props) { 199 | data += `props:{${genProps(el.props)}},` 200 | } 201 | // event handlers 202 | if (el.events) { 203 | data += `${genHandlers(el.events, false, state.warn)},` 204 | } 205 | if (el.nativeEvents) { 206 | data += `${genHandlers(el.nativeEvents, true, state.warn)},` 207 | } 208 | // slot target 209 | // only for non-scoped slots 210 | if (el.slotTarget && !el.slotScope) { 211 | data += `slot:${el.slotTarget},` 212 | } 213 | // scoped slots 214 | if (el.scopedSlots) { 215 | data += `${genScopedSlots(el.scopedSlots, state)},` 216 | } 217 | // component v-model 218 | if (el.model) { 219 | data += `model:{value:${ 220 | el.model.value 221 | },callback:${ 222 | el.model.callback 223 | },expression:${ 224 | el.model.expression 225 | }},` 226 | } 227 | // inline-template 228 | if (el.inlineTemplate) { 229 | const inlineTemplate = genInlineTemplate(el, state) 230 | if (inlineTemplate) { 231 | data += `${inlineTemplate},` 232 | } 233 | } 234 | data = data.replace(/,$/, '') + '}' 235 | // v-bind data wrap 236 | if (el.wrapData) { 237 | data = el.wrapData(data) 238 | } 239 | // v-on data wrap 240 | if (el.wrapListeners) { 241 | data = el.wrapListeners(data) 242 | } 243 | return data 244 | } 245 | 246 | function genDirectives (el, state) { 247 | const dirs = el.directives 248 | if (!dirs) return 249 | let res = 'directives:[' 250 | let hasRuntime = false 251 | let i, l, dir, needRuntime 252 | for (i = 0, l = dirs.length; i < l; i++) { 253 | dir = dirs[i] 254 | needRuntime = true 255 | const gen = state.directives[dir.name] 256 | if (gen) { 257 | // compile-time directive that manipulates AST. 258 | // returns true if it also needs a runtime counterpart. 259 | needRuntime = !!gen(el, dir, state.warn) 260 | } 261 | if (needRuntime) { 262 | hasRuntime = true 263 | res += `{name:"${dir.name}",rawName:"${dir.rawName}"${ 264 | dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : '' 265 | }${ 266 | dir.arg ? `,arg:"${dir.arg}"` : '' 267 | }${ 268 | dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : '' 269 | }},` 270 | } 271 | } 272 | if (hasRuntime) { 273 | return res.slice(0, -1) + ']' 274 | } 275 | } 276 | 277 | function genInlineTemplate (el, state) { 278 | const ast = el.children[0] 279 | if (process.env.NODE_ENV !== 'production' && ( 280 | el.children.length !== 1 || ast.type !== 1 281 | )) { 282 | state.warn('Inline-template components must have exactly one child element.') 283 | } 284 | if (ast.type === 1) { 285 | const inlineRenderFns = generate(ast, state.options) 286 | return `inlineTemplate:{render:function(){${ 287 | inlineRenderFns.render 288 | }},staticRenderFns:[${ 289 | inlineRenderFns.staticRenderFns.map(code => `function(){${code}}`).join(',') 290 | }]}` 291 | } 292 | } 293 | 294 | function genScopedSlots (slots, state) { 295 | return `scopedSlots:_u([${ 296 | Object.keys(slots).map(key => { 297 | return genScopedSlot(key, slots[key], state) 298 | }).join(',') 299 | }])` 300 | } 301 | 302 | function genScopedSlot (key, el, state) { 303 | if (el.for && !el.forProcessed) { 304 | return genForScopedSlot(key, el, state) 305 | } 306 | const fn = `function(${String(el.slotScope)}){` + 307 | `return ${el.tag === 'template' 308 | ? el.if 309 | ? `${el.if}?${genChildren(el, state) || 'undefined'}:undefined` 310 | : genChildren(el, state) || 'undefined' 311 | : genElement(el, state) 312 | }}` 313 | return `{key:${key},fn:${fn}}` 314 | } 315 | 316 | function genForScopedSlot (key, el, state) { 317 | const exp = el.for 318 | const alias = el.alias 319 | const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' 320 | const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' 321 | el.forProcessed = true // avoid recursion 322 | return `_l((${exp}),` + 323 | `function(${alias}${iterator1}${iterator2}){` + 324 | `return ${genScopedSlot(key, el, state)}` + 325 | '})' 326 | } 327 | 328 | export function genChildren (el, state, checkSkip, altGenElement, altGenNode) { 329 | const children = el.children 330 | if (children.length) { 331 | const el = children[0] 332 | // optimize single v-for 333 | if (children.length === 1 && 334 | el.for && 335 | el.tag !== 'template' && 336 | el.tag !== 'slot' 337 | ) { 338 | return (altGenElement || genElement)(el, state) 339 | } 340 | const gen = altGenNode || genNode 341 | return `[${children.map(c => gen(c, state)).join(',')}]` 342 | } 343 | } 344 | 345 | function genNode (node, state) { 346 | if (node.type === 1) { 347 | return genElement(node, state) 348 | } if (node.type === 3 && node.isComment) { 349 | return genComment(node) 350 | } else { 351 | return genText(node) 352 | } 353 | } 354 | 355 | export function genText (text) { 356 | return `_v(${text.type === 2 357 | ? text.expression // no need for () because already wrapped in _s() 358 | : transformSpecialNewlines(JSON.stringify(text.text)) 359 | })` 360 | } 361 | 362 | export function genComment (comment) { 363 | return `_e(${JSON.stringify(comment.text)})` 364 | } 365 | 366 | function genSlot (el, state) { 367 | const slotName = el.slotName || '"default"' 368 | const children = genChildren(el, state) 369 | let res = `_t(${slotName}${children ? `,${children}` : ''}` 370 | const attrs = el.attrs && `{${el.attrs.map(a => `${camelize(a.name)}:${a.value}`).join(',')}}` 371 | const bind = el.attrsMap['v-bind'] 372 | if ((attrs || bind) && !children) { 373 | res += ',null' 374 | } 375 | if (attrs) { 376 | res += `,${attrs}` 377 | } 378 | if (bind) { 379 | res += `${attrs ? '' : ',null'},${bind}` 380 | } 381 | return res + ')' 382 | } 383 | 384 | // componentName is el.component, take it as argument to shun flow's pessimistic refinement 385 | function genComponent (componentName, el, state) { 386 | const children = el.inlineTemplate ? null : genChildren(el, state, true) 387 | return `_c(${componentName},${genData(el, state)}${ 388 | children ? `,${children}` : '' 389 | })` 390 | } 391 | 392 | function genProps (props) { 393 | let res = '' 394 | for (let i = 0; i < props.length; i++) { 395 | const prop = props[i] 396 | res += `"${prop.name}":${transformSpecialNewlines(prop.value)},` 397 | } 398 | return res.slice(0, -1) 399 | } 400 | 401 | // #3895, #4268 402 | function transformSpecialNewlines (text) { 403 | return text 404 | .replace(/\u2028/g, '\\u2028') 405 | .replace(/\u2029/g, '\\u2029') 406 | } 407 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/create-compiler.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { extend } from 'lone-util' 4 | import { detectErrors } from './error-detector' 5 | import { createCompileToFunctionFn } from './to-function' 6 | 7 | export function createCompilerCreator (baseCompile) { 8 | return function createCompiler (baseOptions) { 9 | function compile (template, options) { 10 | const finalOptions = Object.create(baseOptions) 11 | const errors = [] 12 | const tips = [] 13 | finalOptions.warn = (msg, tip) => { 14 | (tip ? tips : errors).push(msg) 15 | } 16 | 17 | if (options) { 18 | // merge custom modules 19 | if (options.modules) { 20 | finalOptions.modules = 21 | (baseOptions.modules || []).concat(options.modules) 22 | } 23 | // merge custom directives 24 | if (options.directives) { 25 | finalOptions.directives = extend( 26 | Object.create(baseOptions.directives), 27 | options.directives 28 | ) 29 | } 30 | // copy other options 31 | for (const key in options) { 32 | if (key !== 'modules' && key !== 'directives') { 33 | finalOptions[key] = options[key] 34 | } 35 | } 36 | } 37 | 38 | const compiled = baseCompile(template, finalOptions) 39 | if (process.env.NODE_ENV !== 'production') { 40 | errors.push.apply(errors, detectErrors(compiled.ast)) 41 | } 42 | compiled.errors = errors 43 | compiled.tips = tips 44 | return compiled 45 | } 46 | 47 | return { 48 | compile, 49 | compileToFunctions: createCompileToFunctionFn(compile) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/directives/bind.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export default function bind (el, dir) { 4 | el.wrapData = (code) => { 5 | return `_b(${code},'${el.tag}',${dir.value},${ 6 | dir.modifiers && dir.modifiers.prop ? 'true' : 'false' 7 | }${ 8 | dir.modifiers && dir.modifiers.sync ? ',true' : '' 9 | })` 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/directives/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import on from './on' 4 | import bind from './bind' 5 | import { noop } from 'lone-util' 6 | 7 | export default { 8 | on, 9 | bind, 10 | cloak: noop 11 | } 12 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/directives/model.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /** 4 | * Cross-platform code generation for component v-model 5 | */ 6 | export function genComponentModel (el, value, modifiers) { 7 | const { number, trim } = modifiers || {} 8 | 9 | const baseValueExpression = '$$v' 10 | let valueExpression = baseValueExpression 11 | if (trim) { 12 | valueExpression = 13 | `(typeof ${baseValueExpression} === 'string'` + 14 | `? ${baseValueExpression}.trim()` + 15 | `: ${baseValueExpression})` 16 | } 17 | if (number) { 18 | valueExpression = `_n(${valueExpression})` 19 | } 20 | const assignment = genAssignmentCode(value, valueExpression) 21 | 22 | el.model = { 23 | value: `(${value})`, 24 | expression: `"${value}"`, 25 | callback: `function (${baseValueExpression}) {${assignment}}` 26 | } 27 | } 28 | 29 | /** 30 | * Cross-platform codegen helper for generating v-model value assignment code. 31 | */ 32 | export function genAssignmentCode (value, assignment) { 33 | const res = parseModel(value) 34 | if (res.key === null) { 35 | return `slave.send('page:data', getLogicChannel(), {id: id, data:{${value}: ${assignment}}})` 36 | } else { 37 | return `$set(${res.exp}, ${res.key}, ${assignment})` 38 | } 39 | } 40 | 41 | /** 42 | * Parse a v-model expression into a base path and a final key segment. 43 | * Handles both dot-path and possible square brackets. 44 | * 45 | * Possible cases: 46 | * 47 | * - test 48 | * - test[key] 49 | * - test[test1[key]] 50 | * - test["a"][key] 51 | * - xxx.test[a[a].test1[key]] 52 | * - test.xxx.a["asa"][test1[key]] 53 | * 54 | */ 55 | 56 | let len, str, chr, index, expressionPos, expressionEndPos 57 | 58 | export function parseModel (val) { 59 | len = val.length 60 | 61 | if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) { 62 | index = val.lastIndexOf('.') 63 | if (index > -1) { 64 | return { 65 | exp: val.slice(0, index), 66 | key: '"' + val.slice(index + 1) + '"' 67 | } 68 | } else { 69 | return { 70 | exp: val, 71 | key: null 72 | } 73 | } 74 | } 75 | 76 | str = val 77 | index = expressionPos = expressionEndPos = 0 78 | 79 | while (!eof()) { 80 | chr = next() 81 | /* istanbul ignore if */ 82 | if (isStringStart(chr)) { 83 | parseString(chr) 84 | } else if (chr === 0x5B) { 85 | parseBracket(chr) 86 | } 87 | } 88 | 89 | return { 90 | exp: val.slice(0, expressionPos), 91 | key: val.slice(expressionPos + 1, expressionEndPos) 92 | } 93 | } 94 | 95 | function next () { 96 | return str.charCodeAt(++index) 97 | } 98 | 99 | function eof () { 100 | return index >= len 101 | } 102 | 103 | function isStringStart (chr) { 104 | return chr === 0x22 || chr === 0x27 105 | } 106 | 107 | function parseBracket (chr) { 108 | let inBracket = 1 109 | expressionPos = index 110 | while (!eof()) { 111 | chr = next() 112 | if (isStringStart(chr)) { 113 | parseString(chr) 114 | continue 115 | } 116 | if (chr === 0x5B) inBracket++ 117 | if (chr === 0x5D) inBracket-- 118 | if (inBracket === 0) { 119 | expressionEndPos = index 120 | break 121 | } 122 | } 123 | } 124 | 125 | function parseString (chr) { 126 | const stringQuote = chr 127 | while (!eof()) { 128 | chr = next() 129 | if (chr === stringQuote) { 130 | break 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/directives/on.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { warn } from 'lone-util' 4 | 5 | export default function on (el, dir) { 6 | if (process.env.NODE_ENV !== 'production' && dir.modifiers) { 7 | warn('v-on without argument does not support modifiers.') 8 | } 9 | el.wrapListeners = (code) => `_g(${code},${dir.value})` 10 | } 11 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/error-detector.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { dirRE, onRE } from './parser/index' 4 | 5 | // these keywords should not appear inside expressions, but operators like 6 | // typeof, instanceof and in are allowed 7 | const prohibitedKeywordRE = new RegExp('\\b' + ( 8 | 'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' + 9 | 'super,throw,while,yield,delete,export,import,return,switch,default,' + 10 | 'extends,finally,continue,debugger,function,arguments' 11 | ).split(',').join('\\b|\\b') + '\\b') 12 | 13 | // these unary operators should not be used as property/method names 14 | const unaryOperatorsRE = new RegExp('\\b' + ( 15 | 'delete,typeof,void' 16 | ).split(',').join('\\s*\\([^\\)]*\\)|\\b') + '\\s*\\([^\\)]*\\)') 17 | 18 | // check valid identifier for v-for 19 | const identRE = /[A-Za-z_$][\w$]*/ 20 | 21 | // strip strings in expressions 22 | const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g 23 | 24 | // detect problematic expressions in a template 25 | export function detectErrors (ast) { 26 | const errors = [] 27 | if (ast) { 28 | checkNode(ast, errors) 29 | } 30 | return errors 31 | } 32 | 33 | function checkNode (node, errors) { 34 | if (node.type === 1) { 35 | for (const name in node.attrsMap) { 36 | if (dirRE.test(name)) { 37 | const value = node.attrsMap[name] 38 | if (value) { 39 | if (name === 'v-for') { 40 | checkFor(node, `v-for="${value}"`, errors) 41 | } else if (onRE.test(name)) { 42 | checkEvent(value, `${name}="${value}"`, errors) 43 | } else { 44 | checkExpression(value, `${name}="${value}"`, errors) 45 | } 46 | } 47 | } 48 | } 49 | if (node.children) { 50 | for (let i = 0; i < node.children.length; i++) { 51 | checkNode(node.children[i], errors) 52 | } 53 | } 54 | } else if (node.type === 2) { 55 | checkExpression(node.expression, node.text, errors) 56 | } 57 | } 58 | 59 | function checkEvent (exp, text, errors) { 60 | const stipped = exp.replace(stripStringRE, '') 61 | const keywordMatch = stipped.match(unaryOperatorsRE) 62 | if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') { 63 | errors.push( 64 | 'avoid using JavaScript unary operator as property name: ' + 65 | `"${keywordMatch[0]}" in expression ${text.trim()}` 66 | ) 67 | } 68 | checkExpression(exp, text, errors) 69 | } 70 | 71 | function checkFor (node, text, errors) { 72 | checkExpression(node.for || '', text, errors) 73 | checkIdentifier(node.alias, 'v-for alias', text, errors) 74 | checkIdentifier(node.iterator1, 'v-for iterator', text, errors) 75 | checkIdentifier(node.iterator2, 'v-for iterator', text, errors) 76 | } 77 | 78 | function checkIdentifier (ident, type, text, errors) { 79 | if (typeof ident === 'string' && !identRE.test(ident)) { 80 | errors.push(`invalid ${type} "${ident}" in expression: ${text.trim()}`) 81 | } 82 | } 83 | 84 | function checkExpression (exp, text, errors) { 85 | try { 86 | // eslint-disable-next-line 87 | new Function(`return ${exp}`) 88 | } catch (e) { 89 | const keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE) 90 | if (keywordMatch) { 91 | errors.push( 92 | 'avoid using JavaScript keyword as property name: ' + 93 | `"${keywordMatch[0]}"\n Raw expression: ${text.trim()}` 94 | ) 95 | } else { 96 | errors.push( 97 | `invalid expression: ${e.message} in\n\n` + 98 | ` ${exp}\n\n` + 99 | ` Raw expression: ${text.trim()}\n` 100 | ) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/helpers.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { parseFilters } from './parser/filter-parser' 4 | 5 | export function baseWarn (msg) { 6 | console.error(`[Lone compiler]: ${msg}`) 7 | } 8 | 9 | export function pluckModuleFunction (modules, key) { 10 | return modules 11 | ? modules.map(m => m[key]).filter(_ => _) 12 | : [] 13 | } 14 | 15 | export function addProp (el, name, value) { 16 | (el.props || (el.props = [])).push({ name, value }) 17 | } 18 | 19 | export function addAttr (el, name, value) { 20 | (el.attrs || (el.attrs = [])).push({ name, value }) 21 | } 22 | 23 | export function addDirective (el, name, rawName, value, arg, modifiers) { 24 | (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers }) 25 | } 26 | 27 | export function addHandler (el, name, value, modifiers, important, warn) { 28 | // warn prevent and passive modifier 29 | /* istanbul ignore if */ 30 | if ( 31 | process.env.NODE_ENV !== 'production' && warn && 32 | modifiers && modifiers.prevent && modifiers.passive 33 | ) { 34 | warn( 35 | 'passive and prevent can\'t be used together. ' + 36 | 'Passive handler can\'t prevent default event.' 37 | ) 38 | } 39 | // check capture modifier 40 | if (modifiers && modifiers.capture) { 41 | delete modifiers.capture 42 | name = '!' + name // mark the event as captured 43 | } 44 | if (modifiers && modifiers.once) { 45 | delete modifiers.once 46 | name = '~' + name // mark the event as once 47 | } 48 | /* istanbul ignore if */ 49 | if (modifiers && modifiers.passive) { 50 | delete modifiers.passive 51 | name = '&' + name // mark the event as passive 52 | } 53 | let events 54 | if (modifiers && modifiers.native) { 55 | delete modifiers.native 56 | events = el.nativeEvents || (el.nativeEvents = {}) 57 | } else { 58 | events = el.events || (el.events = {}) 59 | } 60 | const newHandler = { value, modifiers } 61 | const handlers = events[name] 62 | /* istanbul ignore if */ 63 | if (Array.isArray(handlers)) { 64 | important ? handlers.unshift(newHandler) : handlers.push(newHandler) 65 | } else if (handlers) { 66 | events[name] = important ? [newHandler, handlers] : [handlers, newHandler] 67 | } else { 68 | events[name] = newHandler 69 | } 70 | } 71 | 72 | export function getBindingAttr (el, name, getStatic) { 73 | const dynamicValue = 74 | getAndRemoveAttr(el, ':' + name) || 75 | getAndRemoveAttr(el, 'v-bind:' + name) 76 | if (dynamicValue != null) { 77 | return parseFilters(dynamicValue) 78 | } else if (getStatic !== false) { 79 | const staticValue = getAndRemoveAttr(el, name) 80 | if (staticValue != null) { 81 | return JSON.stringify(staticValue) 82 | } 83 | } 84 | } 85 | 86 | // note: this only removes the attr from the Array (attrsList) so that it 87 | // doesn't get processed by processAttrs. 88 | // By default it does NOT remove it from the map (attrsMap) because the map is 89 | // needed during codegen. 90 | export function getAndRemoveAttr (el, name, removeFromMap) { 91 | let val 92 | if ((val = el.attrsMap[name]) != null) { 93 | const list = el.attrsList 94 | for (let i = 0, l = list.length; i < l; i++) { 95 | if (list[i].name === name) { 96 | list.splice(i, 1) 97 | break 98 | } 99 | } 100 | } 101 | if (removeFromMap) { 102 | delete el.attrsMap[name] 103 | } 104 | return val 105 | } 106 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parser/index' 2 | import { generate } from './codegen/index' 3 | import { createCompilerCreator } from './create-compiler' 4 | 5 | // `createCompilerCreator` allows creating compilers that use alternative 6 | // parser/optimizer/codegen, e.g the SSR optimizing compiler. 7 | // Here we just export a default compiler using the default parts. 8 | export const createCompiler = createCompilerCreator(function baseCompile (template, options) { 9 | const ast = parse(template.trim(), options) 10 | const code = generate(ast, options) 11 | return { 12 | ast, 13 | render: code.render, 14 | staticRenderFns: code.staticRenderFns 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lone-compiler-core", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "he": { 8 | "version": "1.2.0", 9 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 10 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lone-compiler-core", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "license": "ISC", 9 | "dependencies": { 10 | "he": "^1.2.0", 11 | "lone-util": "^0.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/parser/entity-decoder.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | let decoder 4 | 5 | export default { 6 | decode (html) { 7 | decoder = decoder || document.createElement('div') 8 | decoder.innerHTML = html 9 | return decoder.textContent 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/parser/filter-parser.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const validDivisionCharRE = /[\w).+\-_$\]]/ 4 | 5 | export function parseFilters (exp) { 6 | let inSingle = false 7 | let inDouble = false 8 | let inTemplateString = false 9 | let inRegex = false 10 | let curly = 0 11 | let square = 0 12 | let paren = 0 13 | let lastFilterIndex = 0 14 | let c, prev, i, expression, filters 15 | 16 | // 循环将filter push到 filters 中 17 | // Example: a|b 则 filters = [a,b] 18 | for (i = 0; i < exp.length; i++) { 19 | prev = c 20 | c = exp.charCodeAt(i) 21 | if (inSingle) { 22 | if (c === 0x27 && prev !== 0x5C) inSingle = false 23 | } else if (inDouble) { 24 | if (c === 0x22 && prev !== 0x5C) inDouble = false 25 | } else if (inTemplateString) { 26 | if (c === 0x60 && prev !== 0x5C) inTemplateString = false 27 | } else if (inRegex) { 28 | if (c === 0x2f && prev !== 0x5C) inRegex = false 29 | } else if ( 30 | c === 0x7C && // pipe 31 | exp.charCodeAt(i + 1) !== 0x7C && 32 | exp.charCodeAt(i - 1) !== 0x7C && 33 | !curly && !square && !paren 34 | ) { 35 | if (expression === undefined) { 36 | // first filter, end of expression 37 | lastFilterIndex = i + 1 38 | expression = exp.slice(0, i).trim() 39 | } else { 40 | pushFilter() 41 | } 42 | } else { 43 | switch (c) { 44 | case 0x22: inDouble = true; break // " 45 | case 0x27: inSingle = true; break // ' 46 | case 0x60: inTemplateString = true; break // ` 47 | case 0x28: paren++; break // ( 48 | case 0x29: paren--; break // ) 49 | case 0x5B: square++; break // [ 50 | case 0x5D: square--; break // ] 51 | case 0x7B: curly++; break // { 52 | case 0x7D: curly--; break // } 53 | } 54 | if (c === 0x2f) { // / 55 | let j = i - 1 56 | let p 57 | // find first non-whitespace prev char 58 | for (; j >= 0; j--) { 59 | p = exp.charAt(j) 60 | if (p !== ' ') break 61 | } 62 | if (!p || !validDivisionCharRE.test(p)) { 63 | inRegex = true 64 | } 65 | } 66 | } 67 | } 68 | 69 | if (expression === undefined) { 70 | expression = exp.slice(0, i).trim() 71 | } else if (lastFilterIndex !== 0) { 72 | pushFilter() 73 | } 74 | 75 | function pushFilter () { 76 | (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()) 77 | lastFilterIndex = i + 1 78 | } 79 | 80 | if (filters) { 81 | for (i = 0; i < filters.length; i++) { 82 | expression = wrapFilter(expression, filters[i]) 83 | } 84 | } 85 | 86 | return expression 87 | } 88 | 89 | function wrapFilter (exp, filter) { 90 | const i = filter.indexOf('(') 91 | if (i < 0) { 92 | // _f: resolveFilter 93 | return `_f("${filter}")(${exp})` 94 | } else { 95 | const name = filter.slice(0, i) 96 | const args = filter.slice(i + 1) 97 | return `_f("${name}")(${exp},${args}` 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/parser/html-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Not type-checking this file because it's mostly vendor code. 3 | */ 4 | 5 | /*! 6 | * HTML Parser By John Resig (ejohn.org) 7 | * Modified by Juriy "kangax" Zaytsev 8 | * Original code by Erik Arvidsson, Mozilla Public License 9 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 10 | */ 11 | 12 | import { makeMap, no, isNonPhrasingTag } from 'lone-util' 13 | 14 | // Regular Expressions for parsing tags and attributes 15 | // eslint-disable-next-line no-useless-escape 16 | const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ 17 | // could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName 18 | // but for Vue templates we can enforce a simple charset 19 | const ncname = '[a-zA-Z_][\\w\\-\\.]*' 20 | const qnameCapture = `((?:${ncname}\\:)?${ncname})` 21 | const startTagOpen = new RegExp(`^<${qnameCapture}`) 22 | const startTagClose = /^\s*(\/?)>/ 23 | const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) 24 | const doctype = /^]+>/i 25 | const comment = /^') 72 | 73 | if (commentEnd >= 0) { 74 | if (options.shouldKeepComment) { 75 | options.comment(html.substring(4, commentEnd)) 76 | } 77 | advance(commentEnd + 3) 78 | continue 79 | } 80 | } 81 | 82 | // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment 83 | if (conditionalComment.test(html)) { 84 | const conditionalEnd = html.indexOf(']>') 85 | 86 | if (conditionalEnd >= 0) { 87 | advance(conditionalEnd + 2) 88 | continue 89 | } 90 | } 91 | 92 | // Doctype: 93 | const doctypeMatch = html.match(doctype) 94 | if (doctypeMatch) { 95 | advance(doctypeMatch[0].length) 96 | continue 97 | } 98 | 99 | // End tag: 100 | const endTagMatch = html.match(endTag) 101 | if (endTagMatch) { 102 | const curIndex = index 103 | advance(endTagMatch[0].length) 104 | parseEndTag(endTagMatch[1], curIndex, index) 105 | continue 106 | } 107 | 108 | // Start tag: 109 | const startTagMatch = parseStartTag() 110 | if (startTagMatch) { 111 | handleStartTag(startTagMatch) 112 | if (shouldIgnoreFirstNewline(lastTag, html)) { 113 | advance(1) 114 | } 115 | continue 116 | } 117 | } 118 | 119 | // 把纯文本剪切掉 120 | // 需要处理一个特俗情况如果 < 是文本的情况,aaa<:fdfd
    = 0) { 123 | rest = html.slice(textEnd) 124 | while ( 125 | !endTag.test(rest) && 126 | !startTagOpen.test(rest) && 127 | !comment.test(rest) && 128 | !conditionalComment.test(rest) 129 | ) { 130 | // < in plain text, be forgiving and treat it as text 131 | next = rest.indexOf('<', 1) 132 | if (next < 0) break 133 | textEnd += next 134 | rest = html.slice(textEnd) 135 | } 136 | text = html.substring(0, textEnd) 137 | advance(textEnd) 138 | } 139 | 140 | // html 中没有标签的情况 141 | if (textEnd < 0) { 142 | text = html 143 | html = '' 144 | } 145 | 146 | if (options.chars && text) { 147 | options.chars(text) 148 | } 149 | } else { 150 | // lastTag 为 script,style,textarea的情况 151 | let endTagLength = 0 152 | const stackedTag = lastTag.toLowerCase() 153 | const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(]*>)', 'i')) 154 | const rest = html.replace(reStackedTag, function (all, text, endTag) { 155 | endTagLength = endTag.length 156 | if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { 157 | text = text 158 | .replace(//g, '$1') 159 | .replace(//g, '$1') 160 | } 161 | if (shouldIgnoreFirstNewline(stackedTag, text)) { 162 | text = text.slice(1) 163 | } 164 | if (options.chars) { 165 | options.chars(text) 166 | } 167 | return '' 168 | }) 169 | index += html.length - rest.length 170 | html = rest 171 | parseEndTag(stackedTag, index - endTagLength, index) 172 | } 173 | 174 | if (html === last) { 175 | options.chars && options.chars(html) 176 | if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) { 177 | options.warn(`Mal-formatted tag at end of template: "${html}"`) 178 | } 179 | break 180 | } 181 | } 182 | 183 | // Clean up any remaining tags 184 | parseEndTag() 185 | 186 | function advance (n) { 187 | index += n 188 | html = html.substring(n) 189 | } 190 | 191 | function parseStartTag () { 192 | const start = html.match(startTagOpen) 193 | if (start) { 194 | const match = { 195 | tagName: start[1], 196 | attrs: [], 197 | start: index 198 | } 199 | advance(start[0].length) 200 | let end, attr 201 | while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { 202 | advance(attr[0].length) 203 | match.attrs.push(attr) 204 | } 205 | if (end) { 206 | match.unarySlash = end[1] 207 | advance(end[0].length) 208 | match.end = index 209 | return match 210 | } 211 | } 212 | } 213 | 214 | function handleStartTag (match) { 215 | const tagName = match.tagName 216 | const unarySlash = match.unarySlash 217 | 218 | if (expectHTML) { 219 | if (lastTag === 'p' && isNonPhrasingTag(tagName)) { 220 | parseEndTag(lastTag) 221 | } 222 | if (canBeLeftOpenTag(tagName) && lastTag === tagName) { 223 | parseEndTag(tagName) 224 | } 225 | } 226 | 227 | const unary = isUnaryTag(tagName) || !!unarySlash 228 | 229 | const l = match.attrs.length 230 | const attrs = new Array(l) 231 | for (let i = 0; i < l; i++) { 232 | const args = match.attrs[i] 233 | // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 234 | if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) { 235 | if (args[3] === '') { delete args[3] } 236 | if (args[4] === '') { delete args[4] } 237 | if (args[5] === '') { delete args[5] } 238 | } 239 | const value = args[3] || args[4] || args[5] || '' 240 | attrs[i] = { 241 | name: args[1], 242 | value: decodeAttr( 243 | value, 244 | options.shouldDecodeNewlines 245 | ) 246 | } 247 | } 248 | 249 | // 如果当前标签的开始是没有 “/” 的,那表示当前标签有子集 250 | // 当进入子标签时将当前标签数据推入到 stack 中 251 | // lastTag 是 parentDOM 252 | if (!unary) { 253 | stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs }) 254 | lastTag = tagName 255 | } 256 | 257 | if (options.start) { 258 | options.start(tagName, attrs, unary, match.start, match.end) 259 | } 260 | } 261 | 262 | function parseEndTag (tagName, start, end) { 263 | let pos, lowerCasedTagName 264 | if (start == null) start = index 265 | if (end == null) end = index 266 | 267 | if (tagName) { 268 | lowerCasedTagName = tagName.toLowerCase() 269 | } 270 | 271 | // Find the closest opened tag of the same type 272 | if (tagName) { 273 | for (pos = stack.length - 1; pos >= 0; pos--) { 274 | if (stack[pos].lowerCasedTag === lowerCasedTagName) { 275 | break 276 | } 277 | } 278 | } else { 279 | // If no tag name is provided, clean shop 280 | pos = 0 281 | } 282 | 283 | if (pos >= 0) { 284 | // Close all the open elements, up the stack 285 | for (let i = stack.length - 1; i >= pos; i--) { 286 | if (process.env.NODE_ENV !== 'production' && 287 | (i > pos || !tagName) && 288 | options.warn 289 | ) { 290 | options.warn( 291 | `tag <${stack[i].tag}> has no matching end tag.` 292 | ) 293 | } 294 | if (options.end) { 295 | options.end(stack[i].tag, start, end) 296 | } 297 | } 298 | 299 | // Remove the open elements from the stack 300 | stack.length = pos 301 | lastTag = pos && stack[pos - 1].tag 302 | } else if (lowerCasedTagName === 'br') { 303 | if (options.start) { 304 | options.start(tagName, [], true, start, end) 305 | } 306 | } else if (lowerCasedTagName === 'p') { 307 | if (options.start) { 308 | options.start(tagName, [], false, start, end) 309 | } 310 | if (options.end) { 311 | options.end(tagName, start, end) 312 | } 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /packages/lone-compiler-core/parser/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import he from 'he' 4 | import { parseHTML } from './html-parser' 5 | import { parseText } from './text-parser' 6 | import { parseFilters } from './filter-parser' 7 | import { cached, no, camelize } from 'lone-util' 8 | import { genAssignmentCode } from '../directives/model' 9 | import { isIE, isEdge } from 'lone-util/env' 10 | 11 | import { 12 | addProp, 13 | addAttr, 14 | baseWarn, 15 | addHandler, 16 | addDirective, 17 | getBindingAttr, 18 | getAndRemoveAttr, 19 | pluckModuleFunction 20 | } from '../helpers' 21 | 22 | export const onRE = /^@|^v-on:/ 23 | export const dirRE = /^v-|^@|^:/ 24 | export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ 25 | export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ 26 | 27 | const argRE = /:(.*)$/ 28 | const bindRE = /^:|^v-bind:/ 29 | const modifierRE = /\.[^.]+/g 30 | 31 | const decodeHTMLCached = cached(he.decode) 32 | 33 | // configurable state 34 | export let warn 35 | let delimiters 36 | let transforms 37 | let preTransforms 38 | let postTransforms 39 | let platformIsPreTag 40 | let platformMustUseProp 41 | let platformGetTagNamespace 42 | 43 | export function createASTElement (tag, attrs, parent) { 44 | return { 45 | type: 1, 46 | tag, 47 | attrsList: attrs, 48 | attrsMap: makeAttrsMap(attrs), 49 | parent, 50 | children: [] 51 | } 52 | } 53 | 54 | /** 55 | * Convert HTML string to AST. 56 | */ 57 | export function parse (template, options) { 58 | warn = options.warn || baseWarn 59 | 60 | platformIsPreTag = options.isPreTag || no 61 | platformMustUseProp = options.mustUseProp || no 62 | platformGetTagNamespace = options.getTagNamespace || no 63 | 64 | transforms = pluckModuleFunction(options.modules, 'transformNode') 65 | preTransforms = pluckModuleFunction(options.modules, 'preTransformNode') 66 | postTransforms = pluckModuleFunction(options.modules, 'postTransformNode') 67 | 68 | delimiters = options.delimiters 69 | 70 | const stack = [] 71 | const preserveWhitespace = options.preserveWhitespace !== false 72 | let root 73 | let currentParent 74 | let inVPre = false 75 | let inPre = false 76 | let warned = false 77 | 78 | function warnOnce (msg) { 79 | if (!warned) { 80 | warned = true 81 | warn(msg) 82 | } 83 | } 84 | 85 | function endPre (element) { 86 | // check pre state 87 | if (element.pre) { 88 | inVPre = false 89 | } 90 | if (platformIsPreTag(element.tag)) { 91 | inPre = false 92 | } 93 | } 94 | 95 | parseHTML(template, { 96 | warn, 97 | expectHTML: options.expectHTML, 98 | isUnaryTag: options.isUnaryTag, 99 | canBeLeftOpenTag: options.canBeLeftOpenTag, 100 | shouldDecodeNewlines: options.shouldDecodeNewlines, 101 | shouldKeepComment: options.comments, 102 | start (tag, attrs, unary) { 103 | // check namespace. 104 | // inherit parent ns if there is one 105 | const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag) 106 | 107 | // handle IE svg bug 108 | /* istanbul ignore if */ 109 | if (isIE && ns === 'svg') { 110 | attrs = guardIESVGBug(attrs) 111 | } 112 | 113 | let element = createASTElement(tag, attrs, currentParent) 114 | if (ns) { 115 | element.ns = ns 116 | } 117 | 118 | // isForbiddenTag:判断element是不是 style 或者 script 标签 119 | if (isForbiddenTag(element)) { 120 | element.forbidden = true 121 | process.env.NODE_ENV !== 'production' && warn( 122 | 'Templates should only be responsible for mapping the state to the ' + 123 | 'UI. Avoid placing tags with side-effects in your templates, such as ' + 124 | `<${tag}>` + ', as they will not be parsed.' 125 | ) 126 | } 127 | 128 | // apply pre-transforms 129 | for (let i = 0; i < preTransforms.length; i++) { 130 | element = preTransforms[i](element, options) || element 131 | } 132 | 133 | if (!inVPre) { 134 | processPre(element) 135 | if (element.pre) { 136 | inVPre = true 137 | } 138 | } 139 | 140 | if (platformIsPreTag(element.tag)) { 141 | inPre = true 142 | } 143 | if (inVPre) { 144 | processRawAttrs(element) 145 | } else if (!element.processed) { 146 | // structural directives 147 | processFor(element) 148 | processIf(element) 149 | processOnce(element) 150 | // element-scope stuff 151 | processElement(element, options) 152 | } 153 | 154 | function checkRootConstraints (el) { 155 | if (process.env.NODE_ENV !== 'production') { 156 | if (el.tag === 'slot' || el.tag === 'template') { 157 | warnOnce( 158 | `Cannot use <${el.tag}> as component root element because it may ` + 159 | 'contain multiple nodes.' 160 | ) 161 | } 162 | // eslint-disable-next-line no-prototype-builtins 163 | if (el.attrsMap.hasOwnProperty('v-for')) { 164 | warnOnce( 165 | 'Cannot use v-for on stateful component root element because ' + 166 | 'it renders multiple elements.' 167 | ) 168 | } 169 | } 170 | } 171 | 172 | // tree management 173 | if (!root) { // 首次解析到标签开始 174 | root = element // 将第一个解析到的标签设置为root 175 | checkRootConstraints(root) 176 | } else if (!stack.length) { 177 | // 能进到这里面,说明root元素不唯一,也就是有两个或多个root元素 178 | // allow root elements with v-if, v-else-if and v-else 179 | if (root.if && (element.elseif || element.else)) { 180 | checkRootConstraints(element) 181 | addIfCondition(root, { 182 | exp: element.elseif, 183 | block: element 184 | }) 185 | } else if (process.env.NODE_ENV !== 'production') { 186 | warnOnce( 187 | 'Component template should contain exactly one root element. ' + 188 | 'If you are using v-if on multiple elements, ' + 189 | 'use v-else-if to chain them instead.' 190 | ) 191 | } 192 | } 193 | // 如果当前 标签不是 style 和 script 194 | // 将当前标签 push 到 父标签的 children 中 195 | // 将当前标签的 parent 设置为父标签(stack 中的最后一个) 196 | if (currentParent && !element.forbidden) { 197 | if (element.elseif || element.else) { 198 | processIfConditions(element, currentParent) 199 | } else if (element.slotScope) { // scoped slot 200 | currentParent.plain = false 201 | const name = element.slotTarget || '"default"' 202 | ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element 203 | } else { 204 | currentParent.children.push(element) 205 | element.parent = currentParent 206 | } 207 | } 208 | 209 | // 如果不是自闭和标签将当前标签推入stack 210 | // stack 记录DOM深度用 211 | if (!unary) { 212 | currentParent = element 213 | stack.push(element) 214 | } else { 215 | endPre(element) 216 | } 217 | 218 | // apply post-transforms 219 | for (let i = 0; i < postTransforms.length; i++) { 220 | postTransforms[i](element, options) 221 | } 222 | }, 223 | 224 | end () { 225 | // remove trailing whitespace 226 | const element = stack[stack.length - 1] 227 | const lastNode = element.children[element.children.length - 1] 228 | if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { 229 | element.children.pop() 230 | } 231 | // pop stack 232 | stack.length -= 1 233 | currentParent = stack[stack.length - 1] 234 | endPre(element) 235 | }, 236 | 237 | chars (text) { 238 | if (!currentParent) { 239 | if (process.env.NODE_ENV !== 'production') { 240 | if (text === template) { 241 | warnOnce( 242 | 'Component template requires a root element, rather than just text.' 243 | ) 244 | } else if ((text = text.trim())) { 245 | warnOnce( 246 | `text "${text}" outside root element will be ignored.` 247 | ) 248 | } 249 | } 250 | return 251 | } 252 | // IE textarea placeholder bug 253 | /* istanbul ignore if */ 254 | if (isIE && 255 | currentParent.tag === 'textarea' && 256 | currentParent.attrsMap.placeholder === text 257 | ) { 258 | return 259 | } 260 | const children = currentParent.children 261 | text = inPre || text.trim() 262 | ? isTextTag(currentParent) ? text : decodeHTMLCached(text) 263 | // only preserve whitespace if its not right after a starting tag 264 | // children.length 可以判断出当前是否解析到开始标签后面,开始标签后面children肯定是0,因为还没解析到children呢 265 | // 所以只有开始标签后面的没有空格例如

    ,这中间的空格就没有了 266 | : preserveWhitespace && children.length ? ' ' : '' 267 | if (text) { 268 | let expression 269 | if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) { 270 | children.push({ 271 | type: 2, 272 | expression, 273 | text 274 | }) 275 | } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { 276 | children.push({ 277 | type: 3, 278 | text 279 | }) 280 | } 281 | } 282 | }, 283 | comment (text) { 284 | currentParent.children.push({ 285 | type: 3, 286 | text, 287 | isComment: true 288 | }) 289 | } 290 | }) 291 | return root 292 | } 293 | 294 | function processPre (el) { 295 | if (getAndRemoveAttr(el, 'v-pre') != null) { 296 | el.pre = true 297 | } 298 | } 299 | 300 | function processRawAttrs (el) { 301 | const l = el.attrsList.length 302 | if (l) { 303 | const attrs = el.attrs = new Array(l) 304 | for (let i = 0; i < l; i++) { 305 | attrs[i] = { 306 | name: el.attrsList[i].name, 307 | value: JSON.stringify(el.attrsList[i].value) 308 | } 309 | } 310 | } else if (!el.pre) { 311 | // non root node in pre blocks with no attributes 312 | el.plain = true 313 | } 314 | } 315 | 316 | export function processElement (element, options) { 317 | processKey(element) 318 | 319 | // determine whether this is a plain element after 320 | // removing structural attributes 321 | element.plain = !element.key && !element.attrsList.length 322 | 323 | processRef(element) 324 | processSlot(element) 325 | processComponent(element) 326 | for (let i = 0; i < transforms.length; i++) { 327 | element = transforms[i](element, options) || element 328 | } 329 | processAttrs(element) 330 | } 331 | 332 | function processKey (el) { 333 | const exp = getBindingAttr(el, 'key') 334 | if (exp) { 335 | if (process.env.NODE_ENV !== 'production' && el.tag === 'template') { 336 | warn('