├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── doc └── Vue隐藏技能——运行时渲染.md ├── husky.config.js ├── lint-staged.config.js ├── package.json ├── public ├── favicon.ico ├── iframe.html └── index.html ├── scripts └── preCommit.sh ├── src ├── app.vue ├── assets │ └── logo.png ├── components │ ├── banner.vue │ └── helloWorld.vue ├── main.js ├── router │ └── index.js ├── store │ ├── index.js │ └── variable.js ├── utils │ ├── dom.js │ └── vm.js └── views │ ├── about.vue │ ├── customCode │ ├── codeEditor.vue │ ├── index.vue │ ├── mountCrossIframe.vue │ ├── mountSameIframe.vue │ ├── service.js │ ├── withComponent.vue │ └── withMount.vue │ └── home.vue └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const level = process.env.NODE_ENV === 'production' ? 2 : 1 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | node: true, 7 | }, 8 | extends: [ 9 | 'plugin:vue/essential', 10 | '@vue/airbnb', 11 | ], 12 | parserOptions: { 13 | parser: 'babel-eslint', 14 | }, 15 | rules: { 16 | // 'import/no-unresolved': 0, 17 | // 'import/newline-after-import': 0, 18 | // 'import/imports-first': 0, 19 | // 'import/extensions': 0, 20 | // 'import/no-dynamic-require': 0, 21 | // 'import/no-extraneous-dependencies': 0, 22 | // 'import/prefer-default-export': 0, 23 | // 'import/no-named-as-default': 0, 24 | // 'import/no-webpack-loader-syntax': 0, 25 | 26 | // 关闭的规则 27 | 'vue/script-indent': 0, 28 | 'import/extensions': 0, 29 | 'import/prefer-default-export': 0, 30 | 31 | 'arrow-parens': 0, 32 | 'arrow-body-style': 0, 33 | 'consistent-return': 0, 34 | 'function-paren-newline': 0, 35 | 'prefer-destructuring': 0, 36 | 'prefer-promise-reject-errors': 0, 37 | 'no-return-assign': 0, 38 | semi: 0, 39 | 40 | // 开发时关闭的规则 41 | 'vue/html-indent': [level, 2, { baseIndent: 0, closeBracket: 1, alignAttributesVertically: false }], 42 | 'vue/no-unused-components': level, 43 | 44 | 'block-spacing': level, 45 | 'brace-style': level, 46 | camelcase: level, 47 | 'comma-spacing': level, 48 | 'comma-dangle': level, 49 | indent: [level, 2, { SwitchCase: 1 }], 50 | 'key-spacing': level, 51 | 'max-len': level, 52 | 'no-console': [level, { allow: ['warn', 'error', 'info'] }], 53 | 'no-debugger': level, 54 | 'no-empty': [level, { allowEmptyCatch: true }], 55 | 'no-mixed-operators': level, 56 | 'no-multiple-empty-lines': [level, { maxEOF: 2, max: 2, maxBOF: 1 }], 57 | 'no-multi-spaces': [level, { ignoreEOLComments: true }], 58 | 'no-param-reassign': level, 59 | 'no-underscore-dangle': level, 60 | 'no-unreachable': level, 61 | 'no-unused-vars': level, 62 | 'object-curly-newline': [level, { consistent: true }], 63 | 'prefer-const': level, 64 | 'padded-blocks': level, 65 | 'quote-props': level, 66 | quotes: level, 67 | 'spaced-comment': level, 68 | 'space-before-blocks': level, 69 | 'space-before-function-paren': level, 70 | 'space-infix-ops': level, 71 | 72 | // 'arrow-spacing': 1, 73 | // camelcase: 0, 74 | // 'comma-dangle': [1, 'only-multiline'], 75 | // 'comma-spacing': 1, 76 | // eqeqeq: 1, 77 | // 'func-names': [1, 'never'], 78 | // 'guard-for-in': 1, 79 | // 'key-spacing': 1, 80 | // 'keyword-spacing': 1, 81 | // indent: [2, 2, { SwitchCase: 1 }], 82 | // 'max-len': 1, 83 | // 'new-cap': 1, 84 | // 'newline-per-chained-call': 0, 85 | // 'no-console': [ 86 | // level, 87 | // { 88 | // allow: ['warn', 'error', 'info'], 89 | // }, 90 | // ], 91 | // 'no-debugger': level, 92 | // 'no-empty-function': 1, 93 | // 'no-trailing-spaces': [2, { 94 | // skipBlankLines: true 95 | // }], 96 | // 'no-new': 1, 97 | // 'no-mixed-operators': 0, 98 | // 'no-multiple-empty-lines': [1, { max: 2, maxEOF: 1, maxBOF: 1 }], 99 | // 'no-multi-str': 0, 100 | // 'no-multi-spaces': [1, { ignoreEOLComments: true }], 101 | // 'no-unused-vars': [2, { args: 'none' }], 102 | // 'no-unused-expressions': [2, { allowShortCircuit: true }], 103 | // 'no-underscore-dangle': [1, { allowAfterThis: true }], 104 | // 'no-unneeded-ternary': 1, 105 | // 'no-restricted-syntax': [1, 'DebuggerStatement'], 106 | // 'no-plusplus': [1, { allowForLoopAfterthoughts: true }], 107 | // 'no-param-reassign': 0, 108 | // 'no-shadow': 0, 109 | // 'object-shorthand': 0, 110 | // 'object-curly-spacing': 1, 111 | // 'object-curly-newline': [1, { consistent: true }], 112 | // 'operator-linebreak': 0, 113 | // 'one-var': 1, 114 | // 'one-var-declaration-per-line': [1, 'initializations'], 115 | // 'prefer-arrow-callback': 0, 116 | // 'prefer-spread': 1, 117 | // quotes: [1, 'single', { 118 | // avoidEscape: true, 119 | // allowTemplateLiterals: true 120 | // }], 121 | // 'quote-props': 1, 122 | // radix: [1, 'as-needed'], 123 | // 'spaced-comment': 1, 124 | // 'space-infix-ops': 1, 125 | // semi: 0, 126 | // 'space-before-function-paren': [1, 'never'], 127 | // 'space-before-blocks': 1, 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 cof 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-demo 2 | 3 | **vue demo 地址**: [https://merfais.github.io/vue-demo/#/](https://merfais.github.io/vue-demo/#/) 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /doc/Vue隐藏技能——运行时渲染.md: -------------------------------------------------------------------------------- 1 | ### 一语惊人 2 | 3 | 前段时间接了一个需求:能不能让用户自制组件,从而达到定制渲染某个区域的目的。说实话接到这个需求心中一惊,感叹这个想法真是大胆呀,但作为打工人,秉承着只要思想不滑坡,办法总比困难多的打工魂,即使是刀山也得上呀,历经几日的摸索调研,发现其实VUE一早就支持了这么做,只不过时过境迁,渐渐被遗忘了这个隐藏的技能。 4 | 5 | 大致说一下项目的背景:我们做了一个拖拽生成报表的系统,通过拖拽内置的组件供用户定制自己的报表形态,但毕竟内置的组件有限,可定制性不高,那么给用户开放一个code组件,让用户自己通过写`template` + `js` + `css`的方式自由定制岂不是妙哉。 6 | 7 | ### 重提渐进式 8 | 9 | 那么该怎么实现呢?我们先来看一vue官方的介绍 10 | 11 | > [Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的`渐进式框架`。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。](https://cn.vuejs.org/v2/guide/index.html#Vue-js-%E6%98%AF%E4%BB%80%E4%B9%88) 12 | 13 | 很多时候我们貌似已经忽略了`渐进式`这回事,现在基于VUE开发的项目大多都采用vue cli生成,以vue单文件的方式编码,webpack编译打包的形式发布。这与渐进式有什么关系呢,确实没有关系。 14 | 15 | 渐进式其实指的在一个已存在的但并未使用vue的项目上接入vue,使用vue,直到所有的HTML渐渐替换为通过vue渲染完成,渐进开发,渐进迁移,这种方式在vue刚出现那几年比较多,现在或许在一些古老的项目也会出现。 16 | 17 | 为什么要提`渐进式`呢?因为渐进式是不需要本地编译的,有没有get到点!对,就是`不需要本地编译,而是运行时编译`。 18 | 19 | ### 本地编译与运行时编译 20 | 21 | 用户想通过编写`template` + `js` + `css`的方式实现运行时渲染页面,那肯定是不能本地编译的(此处的编译指将vue文件编译为js资源文件),即不能把用户写的代码像编译源码一样打包成静态资源文件。 22 | 23 | 这些代码只能原样持久化到数据库,每次打开页面再恢复回来,实时编译。毕竟不是纯js文件,是不能直接运行的,它需要一个运行时环境,运行时编译,这个环境就是 [vue的运行时 + 编译器](https://cn.vuejs.org/v2/guide/installation.html#%E8%BF%90%E8%A1%8C%E6%97%B6-%E7%BC%96%E8%AF%91%E5%99%A8-vs-%E5%8F%AA%E5%8C%85%E5%90%AB%E8%BF%90%E8%A1%8C%E6%97%B6)。 24 | 25 | 有了思路也只是窥到了天机,神功练成还是要打磨细节。具体怎么做,容我一步步道来。 26 | 27 | ### 技术干货 28 | 29 | #### 第一步:需要一个运行时编译环境 30 | 31 | 按[官方的介绍](https://learning.dcloud.io/#/?vid=2),通过script标签引入vue就可以渐进式开发了,也就具备了运行时+编译器,如下 32 | ```html 33 | 34 | 35 | 36 | Document 37 | 38 | 39 | 40 |
{{message}}
41 | 49 | 50 | 51 | ``` 52 | 53 | 但通过vue单文件+webpack编译的方式,再引入一个vue就多余了,通过CLI也是可以的,只需要在vue.config.js中打开runtimeCompiler开关就行了,[详细看文档](https://cli.vuejs.org/zh/config/#runtimecompiler)。 54 | 55 | 此时我们就有了一个运行时编译环境 56 | 57 | #### 第二步:把用户的代码注册到系统中 58 | 59 | 把代码渲染出来有两个方案 60 | 61 | 62 | 1. 通过 [注册组件](https://cn.vuejs.org/v2/guide/components-registration.html) 的方式,把代码注册为vue实例的组件,注册组件又分 全局注册 和 局部注册 两种方式 63 | 2. 通过挂载点直接挂载vue实例, 即通过`new Vue({ el: '#id' })`的方式 64 | 65 | ##### 第一种方案:动态组件 66 | 67 | 对于这种方式,在官方文档中,组件注册章节,最后给出了一个注意点 68 | > 记住全局注册的行为必须在根 Vue 实例 (通过 new Vue) `创建之前`发生。 69 | 70 | 因此,并不能通过调用`Vue.component('my-component-name', {/* */})`的方式将用户的代码注册到系统中,因为运行时Vue实例已经创建完,用户的代码是在实例完Vue后才进来的,那我们只能通过局部注册的方式了,类似这样 71 | ```jsx 72 | var ComponentB = { 73 | components: { 74 | 'component-a': { 75 | ...customJsLogic, 76 | name: 'custom-component', 77 | template: '
custom template
', 78 | } 79 | }, 80 | // ... 81 | } 82 | ``` 83 | 84 | 但想一下,好像不太对,这还是在写源码,运行时定义了`ComponentB`组件怎么用呢,怎么把`ComponentB`在一个已经编译完页面上渲染出来呢?找不到入口点,把用户代码注入到`components`对象上也无法注册到系统中,无法渲染出来。 85 | 86 | 就止步于此了吗?该怎么办呢? 87 | 88 | 想一下为什么要在`components`中先注册(声明)下组件,然后才能使用?component本质上只不过是一个js object而已。其实主要是为了服务于template模板语法,当你在template中写了 ``,有了这个注册声明才能在编译时找到`compA`。如果不使用template,那么这个注册就可以省了。 89 | 90 | 不使用template怎么渲染呢,使用[render函数](https://cn.vuejs.org/v2/guide/render-function.html)呀! 91 | 92 | 在render函数中如果使用createElement就比较麻烦了,API很复杂,对于渲染一整段用户定义的template也略显吃力,使用jsx就方便多了,都1202年了,想必大家对jsx都应该有所了解。 93 | 94 | 回到项目上,需要使用用户代码的地方不止一处,都用render函数写一遍略显臃肿,那么做一个code的容器,容器负责渲染用户的代码,使用地方把容器挂上就行了。 95 | 96 | + 容器核心代码 97 | 98 | ```javascript 99 | export default { 100 | name: 'customCode', 101 | props: { 102 | template: String, // template模板 103 | js: String, // js逻辑 104 | css: String, // css样式 105 | }, 106 | computed: { 107 | className() { 108 | // 生成唯一class,主要用于做scoped的样式 109 | const uid = Math.random().toString(36).slice(2) 110 | return `custom-code-${uid}` 111 | }, 112 | scopedStyle() { 113 | if (this.css) { 114 | const scope = `.${this.className}` 115 | const regex = /(^|\})\s*([^{]+)/g 116 | // 为class加前缀,做类似scope的效果 117 | return this.css.trim().replace(regex, (m, g1, g2) => { 118 | return g1 ? `${g1} ${scope} ${g2}` : `${scope} ${g2}` 119 | }) 120 | } 121 | return '' 122 | }, 123 | component() { 124 | // 把代码字符串转成js对象 125 | const component = safeStringToObject(this.js) 126 | 127 | // 去掉template的前后标签 128 | const template = (this.template || '') 129 | .replace(/^ *< *template *>|<\/ *template *> *$/g, '') 130 | .trim() 131 | 132 | // 注入template或render,设定template优先级高于render 133 | if (this.template) { 134 | component.template = this.template 135 | component.render = undefined 136 | } else if (!component.render) { 137 | component.render = '
未提供模板或render函数
' 138 | } 139 | 140 | return component 141 | }, 142 | }, 143 | render() { 144 | const { component } = this 145 | return
146 | 147 | 148 |
149 | }, 150 | } 151 | ``` 152 | 153 | + 容器使用 154 | 155 | ```html 156 | 159 | ``` 160 | 以上只是核心的逻辑部分,除了这些,在项目实战中还应考虑容错处理,错误大致可以分两种 161 | 162 | 1. 用户代码语法错误 163 | 164 | 主要是js部分,对于css和template的错误,浏览器有一定的纠错的机制,不至于崩了。 165 | 166 | 这部分的处理主要借助于`safeStringToObject`这个函数,如果有语法错误,则返回Error,处理一下回显给用户,代码大致如下 167 | 168 | ```javascript 169 | // component对象在result.value上取,如果result.error有值,则代表出现了错误 170 | component() { 171 | // 把代码字符串转成js对象 172 | const result = safeStringToObject(this.js) 173 | 174 | const component = result.value 175 | if (result.error) { 176 | console.error('js 脚本错误', result.error) 177 | result.error = { 178 | msg: result.error.toString(), 179 | type: 'js脚本错误', 180 | } 181 | result.value = { hasError: true } 182 | return result 183 | } 184 | 185 | // ... 186 | 187 | retrun result 188 | } 189 | ``` 190 | 191 | 2. 组件运行时错误 192 | 193 | 既然把js逻辑交给了用户控制,那么像类型错误,从undefined中读值,把非函数变量当函数运行,甚至拼写错误等这些运行时错误就很有可能发生。 194 | 195 | 这部分的处理需要通过在容器组件上添加 [`errorCaptured`这个官方钩子](https://cn.vuejs.org/v2/api/index.html#errorCaptured),来捕获子组件的错误,因为并没有一个途径可以获取组件自身运行时错误的钩子。代码大致如下 196 | 197 | ```javascript 198 | errorCaptured(err, vm, info) { 199 | this.subCompErr = { 200 | msg: err && err.toString && err.toString() || err, 201 | type: '自定义组件运行时错误:', 202 | } 203 | console.error('自定义组件运行时错误:', err, vm, info) 204 | }, 205 | ``` 206 | 207 | 结合错误处理,如果希望用户能看到错误信息,则render函数需要把错误展示出来,代码大致如下 208 | 209 | ```Jsx 210 | render() { 211 | const { error: compileErr, value: component } = this.component 212 | const error = compileErr || this.subCompErr 213 | let errorDom 214 | if (error) { 215 | errorDom =
216 |
{error.type}
217 |
{error.msg}
218 |
219 | } 220 | return
221 |
222 | 223 | 224 |
225 | {errorDom} 226 |
227 | }, 228 | ``` 229 | 230 | 这里还有一个点,用户发现组件发生了错误后会修改代码,使其再次渲染,错误的回显需要特别处理下。 231 | 232 | 对于js脚本错误,因component是计算属性,随着computed计算属性再次计算,如果js脚本没有错误,导出的component可重绘出来, 233 | 234 | 但对于运行时错误,使用`this.subCompErr`内部变量保存,props修改了,这个值却不会被修改,因此需要打通props关联,通过添加watch的方式解决,这里为什么没有放在component的计算属性中做,一是违背计算属性设计原则,二是component可能并不仅仅依赖js,css,template这个props的变化,而`this.subCompErr`只需要和这个三个props关联,这么做会有多余的重置逻辑。 235 | 236 | 还有一种场景就是子组件自身可能有定时刷新逻辑,定期或不定期的重绘,一旦发生了错误,也会导致一直显示错误信息,因为用户的代码拿不到`this.subCompErr`的值,因此也无法重置此值,这种情况,可通过注入`beforeUpdate`钩子解决,代码大致如下 237 | 238 | ```javascript 239 | computed: { 240 | component() { 241 | // 把代码字符串转成js对象 242 | const result = safeStringToObject(this.js) 243 | const component = result.value 244 | // ... 245 | // 注入mixins 246 | component.mixins = [{ 247 | // 注入 beforeUpdate 钩子,用于子组件重绘时,清理父组件捕获的异常 248 | beforeUpdate: () => { 249 | this.subCompErr = null 250 | }, 251 | }] 252 | // ... 253 | return result 254 | }, 255 | }, 256 | watch: { 257 | js() { 258 | // 当代码变化时,清空error,重绘 259 | this.subCompErr = null 260 | }, 261 | template() { 262 | // 当代码变化时,清空error,重绘 263 | this.subCompErr = null 264 | }, 265 | css() { 266 | // 当代码变化时,清空error,重绘 267 | this.subCompErr = null 268 | }, 269 | }, 270 | ``` 271 | **`完整的代码见`:[https://github.com/merfais/vue-demo/blob/main/src/views/customCode/withComponent.vue](https://github.com/merfais/vue-demo/blob/main/src/views/customCode/withComponent.vue)** 272 | 273 | **`完整的demo见`:[https://merfais.github.io/vue-demo/#/custom-code](https://merfais.github.io/vue-demo/#/custom-code)** 274 | 275 | ##### 第二种方案:动态实例 276 | 277 | 我们知道在利用vue构建的系统中,页面由组件构成,页面本身其实也是组件,只是在部分参数和挂载方式上有些区别而已。这第二种方式就是将用户的代码视为一个page,通过new一个vm实例,再在DOM挂载点挂载vm(`new Vue(component).$mount('#id')`)的方式渲染。 278 | 279 | 动态实例方案与动态组件方案大致相同,都要通过computed属性,生成`component`对象和`scopedStyle`对象进行渲染,但也有些许的区别,动态实例比动态组件需要多考虑以下几点: 280 | 281 | 1. 需要一个稳定的挂载点 282 | 283 | 从vue2.0开始,vue实例的挂载策略变更为,[所有的挂载元素会被 Vue 生成的 DOM 替换](https://cn.vuejs.org/v2/api/#el),在此策略下,一旦执行挂载,原来的DOM就会消失,不能再次挂载。但我们需要实现代码变更后能够重新渲染,这就要求挂载点要稳定存在,解决方案是对用户的template进行注入,每次渲染前,在template外层包一层带固定id的DOM 284 | 285 | 2. 运行时错误捕获`errorCaptured`需要注入到`component`对象上,不再需要注入`beforeUpdate`钩子 286 | 287 | 因为通过`new Vue()`的方式创建了一个新的vm实例,不再是容器组件的子组件,所以容器组件上的`errorCaptured`无法捕获新vm的运行时错误,`new Vue(component)`中参数component是顶层组件,根据 [Vue错误传播规则](https://cn.vuejs.org/v2/api/#errorCaptured) 可知,在非特殊控制的情况下,顶层的 `errorCaptured` 会捕获到错误 288 | 289 | 3. 首次挂载需要制造一定的延迟才能渲染 290 | 291 | 由于挂载点含在DOM在容器内,与计算属性导出的`component`对象在首次挂载时时序基本是一致的,导致挂载vm(`$mount('#id')`)时,DOM可能还没有渲染到文档流上,因此在首次渲染时需要一定的延迟后再挂载vm。 292 | 293 | 以上的不同点,并未给渲染用户自定义代码带来任何优势,反而增加了限制,尤其 **需要稳定挂载点** 这一条,需要对用户提供的template做二次注入,包裹挂载点,才能实现用户修改组件后的实时渲染更新,因此,也不能支持用户定义render函数,因为无法获取未经运行的render函数的返回值,也就无法注入外层的挂载点。 294 | 295 | 另外一点也需要注意,这种方式也是无法在容器组件中使用template定义渲染模板的,因为如果在template中写style标签会出现以下编译错误,但style标签是必须的,需要为自定义组件提供scoped的样式。(当然,也可以通过提供appendStyle函数实现动态添加style标签,但这样并没有更方便,因此没有必要) 296 | 297 | ```shell 298 | Errors compiling template: 299 | 300 | Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as 305 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 306 | 5 | 307 | | ^^^^^^^ 308 | ``` 309 | 310 | 鉴于以上缺点,就不提供核心代码示范了,直接给源码和demo 311 | 312 | **`完整的代码见`:[https://github.com/merfais/vue-demo/blob/main/src/views/customCode/withMount.vue](https://github.com/merfais/vue-demo/blob/main/src/views/customCode/withMount.vue)** 313 | 314 | **`完整的demo见`:[https://merfais.github.io/vue-demo/#/custom-code](https://merfais.github.io/vue-demo/#/custom-code)** 315 | 316 | 想一下,如果动态实例方案仅仅有以上缺点,那考虑这种方案有什么意义呢?其实,它的意义在于,动态实例方案主要应用于iframe渲染,而使用iframe渲染的目的则是为了隔离。 317 | 318 | iframe会创建独立于主站的一个域,这种隔离可以很好地防止js污染和css污染,隔离方式又分为跨域隔离和非跨域隔离两种,跨域则意味着完全隔离,非跨域则是半隔离,其主要区别在于安全策略的限制,这个我们最后再说。 319 | 320 | iframe是否跨域由iframe的src的值决定,设置同域的src或不设置src均符合同域策略,否则是跨域。对于没有设置src的iframe,页面只能加载一个空的iframe,因此还需要在iframe加载完后再动态加载依赖的资源,如:vuejs,其他运行时的依赖库(示例demo加载了ant-design-vue)等。如果设置了src,则可以将依赖通过script标签和link标签提前写到静态页面文件中,使依赖资源在加载iframe时自动完成加载。 321 | 322 | 先介绍半隔离方式,即通过非跨域iframe渲染,首先需要渲染一个iframe,我们使用不设置src的方式,这样更具备通用性,可以用于任意的站点。核心代码如下 323 | 324 | ```html 325 |