├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── main │ ├── assets │ │ ├── js │ │ │ ├── api │ │ │ │ ├── config.js │ │ │ │ ├── index.js │ │ │ │ ├── roseApi │ │ │ │ │ ├── instance.js │ │ │ │ │ └── index.js │ │ │ │ └── util.js │ │ │ ├── template.js │ │ │ └── helper.js │ │ ├── font │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.scss │ │ │ └── iconfont.svg │ │ ├── images │ │ │ ├── arrows.png │ │ │ └── iphone.png │ │ └── style │ │ │ ├── var.scss │ │ │ └── index.scss │ ├── store │ │ ├── helper.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── state.js │ │ ├── actions.js │ │ └── mutations.js │ ├── index.html │ ├── index.js │ ├── App.vue │ └── components │ │ ├── right-menu.vue │ │ ├── left-menu.vue │ │ ├── main-container.vue │ │ └── editor-jsx.vue └── preview │ ├── index.html │ ├── index.js │ └── App.vue ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── package.json └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/main/assets/js/api/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // env: 'production' 3 | env: 'dev' 4 | }; 5 | -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.eot -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /src/main/assets/images/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/images/arrows.png -------------------------------------------------------------------------------- /src/main/assets/images/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/images/iphone.png -------------------------------------------------------------------------------- /src/main/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muwoo/rose/HEAD/src/main/assets/font/iconfont.woff -------------------------------------------------------------------------------- /src/main/assets/js/api/index.js: -------------------------------------------------------------------------------- 1 | import * as roseApi from './roseApi/index.js' 2 | 3 | export { 4 | roseApi, 5 | }; 6 | -------------------------------------------------------------------------------- /src/main/assets/style/var.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #21374B; 2 | $success-color: rgb(0, 168, 84); 3 | $error-color: #e96900; 4 | $white: #fff; 5 | $info: #2973b7; 6 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /src/main/assets/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "../font/iconfont.scss"; 2 | 3 | body,html { 4 | margin: 0; 5 | padding: 0; 6 | height: 100%; 7 | } 8 | #app { 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
24 |
25 | ## 开撸
26 | #### 组件的实现
27 | 首先我们来实现组件这一部分,因为组件关联着后台编辑的预览和最后发布的使用。组件设计我们应该尽量保持组件的对外一致性,这样在进行渲染的时候,我们可以提供一个统一的对外数据接口。这里我们的技术选型是基于 Vue 的,所以下面的代码部分也主要是基于 Vue 的,但是万变不离其宗,其他语言也类似。
28 |
29 | 根据上图,我们的组件是会被一个个拆分单独发布到 `npm`仓库的,为什么这么设计呢?其实之前也考虑过设计成一个组件库,所有组件都包含在一个组件库内,这样只需要发布一个组件库包,用的时候按需加载就好了。后来在实践的过程中发现这样并不合适协同开发,其他前端如果想贡献组件,接入的改造成本也很大。举个🌰:小明在业务中写了个`Button`组件,这个组件经常会被其他项目复用,他想把这个组件贡献到我们的系统中,被模板使用,如果是一个组件库的话,他首先得拉取我们组件库的代码,然后按照组件库的规范格式进行提交。这样一来,偷懒的小明可能就不太愿意这么干,最爽的方法当然是在本地构建一个npm库,开发选用的是用`TypeScript`还是其他的我们不关心,选用的 Css 预处理器我们也不关心,甚至编码规范的`ESLint`我们也不关心。最后只需通过编译后的文件即可。这样就避免了一个组件库的约束。依托于NPM完善的发布/拉取,以及版本控制机制,可以让我们少做一些额外的工作,也可以快速的把平台搭建起来。
30 |
31 | 说了这么多,代码呢?,我们以一个`Button`为例,我们对外提供这样的形式组件:
32 | ```html
33 |
34 |
37 |
38 |
50 | ```
51 | 可以看到我们只对外暴露了一个`props`,这样做法的好处是可以统一组件对外暴露的数据,组件内部爱怎么玩怎么玩。注意,这里我们也可以引入一些第三方组件库,比如`mint-ui`之类的。
52 |
53 | #### 后台编辑的实现
54 | 在写代码前,我们先考虑一下需要实现哪些功能:
55 | 1. 一个属性编辑区,提供给使用者编辑组件内部`props`的功能
56 | 2. 一个组件选择区,提供使用者选择需要的组件
57 | 3. 一个组件预览区,提供使用者拖拽排序页面预览的功能
58 |
59 | ###### 编辑区的实现
60 | 按照顺序,我们先来实现组件的属性编辑功能。我们要考虑,一个组件暴露出哪些可配置的信息。这些可配置的信息如何同步到后台编辑区,让使用者进行编辑,一个按钮的可配置信息可能是这样:
61 |
62 | 
63 |
64 | 如果把这些配置全部写在后台库里面,根据当前选择的组件加载不同的配置,维护起来会相当麻烦,而且随着组件数量的增加,也会变得臃肿,所以我们可以将这些配置存储在服务端,后台只需要根据存储的规则进行解析便可,举个例子,我们其实可以存储这样的编辑配置:
65 | ```json
66 | [
67 | {
68 | "blockName": "按钮布局设置",
69 | "settings": {
70 | "src": {
71 | "type": "input",
72 | "require": true,
73 | "label": "按钮文案"
74 | }
75 | }
76 | }
77 | ]
78 | ```
79 | 我们在编辑后台,通过接口请求到这些配置,便可以进行规则渲染:
80 | ```js
81 | /**
82 | * 根据类型,选择创建对应的组件
83 | * @param {VNode} vm
84 | * @returns {any}
85 | */
86 | createEditorElement (vm: VNode) {
87 | let dom = null
88 | switch (vm.config.type) {
89 | case 'align':
90 | dom = this.createAlignElement(vm)
91 | break;
92 | case 'select':
93 | dom = this.createSelectElement(vm)
94 | break;
95 | case 'actions':
96 | dom = this.createActionElement(vm)
97 | break;
98 | case 'vue-editor':
99 | dom = this.createVueEditor(vm)
100 | break;
101 | default:
102 | dom = this.createBasicElement(vm)
103 | }
104 | return dom
105 | }
106 | ```
107 |
108 | #### 组件选择功能
109 | 首先我们需要考虑的是,组件怎么进行注册?因为组件被用户选用的时候,我们是需要渲染该组件的,所以我们可以提供一段 node 脚本来遍历所需组件,进行组件的安装注册:
110 | ```js
111 | // 定义渲染模板和路径
112 | var OUTPUT_PATH = path.join(__dirname, '../packages/index.js');
113 | console.log(chalk.yellow('正在生成包引用文件...'))
114 | var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
115 | var IMPORT_TEMPLATE = 'import {{componentName}} from \'{{name}}\'';
116 | var MAIN_TEMPLATE = `/* Automatic generated by './compiler/build-entry.js' */
117 |
118 | {{include}}
119 |
120 | const components = [
121 | {{install}}
122 | ]
123 |
124 | const install = function(Vue) {
125 | components.map((component) => {
126 | Vue.component(component.name, component)
127 | })
128 | }
129 |
130 | /* istanbul ignore if */
131 | if (typeof window !== 'undefined' && window.Vue) {
132 | install(window.Vue)
133 | }
134 |
135 | export {
136 | install,
137 | {{list}}
138 | }
139 | `;
140 | // 渲染引用文件
141 | var template = render(MAIN_TEMPLATE, {
142 | include: includeComponentTemplate.join(endOfLine),
143 | install: installTemplate.join(`,${endOfLine}`),
144 | version: process.env.VERSION || require('../package.json').version,
145 | list: listTemplate.join(`,${endOfLine}`)
146 | });
147 |
148 | // 写入引用
149 | fs.writeFileSync(OUTPUT_PATH, template);
150 | ```
151 |
152 | 最后渲染出来的文件大概是这样:
153 | ```js
154 | import WButton from 'w-button'
155 | const components = [
156 | WButton
157 | ]
158 | const install = function(Vue) {
159 | components.map((component) => {
160 | Vue.component(component.name, component)
161 | })
162 | }
163 | /* istanbul ignore if */
164 | if (typeof window !== 'undefined' && window.Vue) {
165 | install(window.Vue)
166 | }
167 | export {
168 | install,
169 | WButton
170 | }
171 | ```
172 | 这个也是组件库的通用写法,所以这里的思想就是把发布到`npm`上的组件,进行聚合,聚合成一个组件包引用,我们在后台编辑的时候,是需要全量引入的:
173 | ```js
174 | import * as W_UI from '../../packages'
175 |
176 | Vue.use(W_UI)
177 | ```
178 | 这样,我们组件便注册完了,组件选择区,主要是提供组件的可选项,我们可以遍历组件,提供一个个 List 让用户选择,当然如果我们每个组件如果只提供一个组件名,用户可能并不知道组件长什么样,所以我们最好可以提供一下组件长什么样的缩略图。这里我们可以在组件发布的时候,也通过 node 脚本进行。这里要实现的代码比较多,我就大致说一下过程,因为也不是核心逻辑,可有可无,只能说有了体验上会好一点:
179 | 1. 用户启用 dev-server 进行代码编写测试
180 | 2. server 脚本使用 Chrome 工具 `puppeteer`,调整页面到手机端模式, 进行当前 dev-server 截图。
181 | 3. 生成截图文件,上传到node服务,关联组件
182 |
183 | 这样,就可以在加载组件选择区的时候,为组件附上缩略图。
184 |
185 | #### 组件预览区
186 | 当用户在选择区选择了组件,我们需要展示在预览区域,那么我们怎么知道用户选择了哪些组件呢?总不能提前全部把组件写入渲染区域,通过 `v-if`来判断选择吧?当然没有这么蠢,Vue 已经提供了动态组件的功能了:
187 | ```html
188 |
201 |
202 | 利用 `vuex` 做数据存储池,所有的变化,通过 `postMessgae`进行同步,这样我们只用确保数据池中的数据变化,便可以映射到渲染层的变化。比如,我们在预览区进行了组件选择和拖拽排序,那么我们只需通过`vuex`出发同步信息便可:
203 | ```ts
204 | // action.ts
205 | const action = {
206 | setCurrentPage ({commit, state}, page: number) {
207 | // 更新当前store
208 | commit('setCurrentPage',page)
209 | // 对应postMessage
210 | helper.postMsgToChild({type: 'syncState', value: state})
211 | },
212 | // ...
213 | }
214 | ````
215 |
216 | ## Template 模板的实现
217 | 模板的设计实现,我参考了 `Vue-cli 2.x` 版本的思想,把这里的模板,存在了对应的 `git` 仓库中。当用户需要进行页面构建的时候,直接从 git 仓库中拉取对应的模板即可。当然拉取完,也会缓存一份在本地,以后渲染,直接从本地缓存中读取即可。我们现在把中心放在模板的格式和规范上。模板我们采用什么样的语法无所谓,这里我才用了和 `Vue-cli`一样的`Handlerbars`引擎。这里直接上我们模板的设计:
218 | ```html
219 |
220 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
--------------------------------------------------------------------------------
/src/main/assets/font/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
69 |
--------------------------------------------------------------------------------