前情回顾

上一章 的结尾也提到了匆匆结束了一章(不要打我,过了这么久发第二张还不是难产了。。。倒不是难度高而是太懒了写不动),那么上一章具体做了什么?

对啥也没做,就简单的完成了一个拖拽功能和一个中心 event center(来自未来icepro鄙视的眼神)。

所以本章到底要做什么呢?

主要的目标是把上一章中没有实现的打包下载功能实现掉。

1. 添加结构

这里需要补全另外一种结构也就是 text 这个,具体的非常简单直接(我觉得有点粗暴来着…

import {DragableElement} from './../../utils/dragableElement.class';
/**
 * text
 * @desc 画布上使用的 text
 */
export default class Text extends DragableElement {
    _text = null;
    _content = 'default';
    /**
     * 初始化
     */
    constructor() {
        let _text = document.createElement('div');
        super(_text);
        this._text = _text;
    }
    /**
     * 文本内容
     * @return {string} 返回具体的内容
     */
    get content() {
        return this._content;
    }
 
    /**
     * render
     * @override
     * @param {Arguments} arg
     */
    render(...arg) {
        this._text.innerHTML = this.content;
        super.render(...arg);
    }
}

对你没有看错,就继承了一下,重写了一下render方法就完事儿了,就是这么的简单。。。

2. 自动化工厂

对于创建元素我们现在放置在 canvas 里面,当然这里的做法是完全错误的,对完全错误的,至于为什么错误,主要原因是作为一个画布,不应该提供过多的功能,实际上,画布应该只是region的一个实现,而没有其他任何的功能才对。当然这里作为前期,就暂且先将内容放置画布在这里了。

那么如何实现一个自动化工厂呢?

这样我们需要变更一下目录结构,使得结构更加合理(对对对对,还不是你学艺不精,正则写不出来嘛!)

目录结构

那么接下来就是写目录中的那个 dragableElementFactory.js 了,这里直接用 webpack 提供的 require.context 去自动化注入对象,具体这样写:

const r = require.context('./dragableElement', true, /\.js$/);
// 模块列表
const modules = new Map();
/**
 * 获取模块名称
 * @type {RegExp}
 */
const nameRegexp = /\/([^\/]+)\.js$/;
r.keys().forEach((key) => {
    let name = key.match(nameRegexp)[1];
    modules.set(name, r(key).default);
})
 
/**
 * dom工厂
 * @param {string} key
 * @return {class} 具体的class
 */
export default function factory(key) {
    if (!modules.has(key)) {
        throw new Error('unkown key of `' + key + '` of content');
    }
    let ClassOfObject = modules.get(key);
    return new ClassOfObject();
}

这样 factory 就可以自动化的生产每一个实例化的对象了,那么然后怎么使用呢?

这样使用:

// ./src/components/canvas.js
/**
 * 创建一个元素
 * @param {string} type
 * @param {{x: number, y: number}} position
 */
function createElement(type, position) {
    // console.log(type, position)
    let element = factory(type);
    let item = canvas.canvasContainer[0].getBoundingClientRect();
    element.setPosition({
        x: position.x - item.x,
        y: position.y - item.y,
    });
    element.render();
}

好了这样就创建了一个 element 了然后这里其实还缺少一个地方储存这些数据,由于现在没有所谓的 scene 概念,所以这里直接使用传说中的map 去储存数据即可

然后我们在 canvas.js 开头储存一个 map

/**
 * @desc create element map
 * @type {Map<string, DragableElement>}
 */
const elementsMap = new Map();

然后在上面那个地方 render 后面加一个储存的选项

    element.render();
    elementsMap.set(element.key, element);

然后我们把这个map丢到window 作用域下方便我们测试,当然不放置也没啥关系。

3. 提供下载前的准备工作

首先在提供下载前,我们需要提供一些准备工作,比如预打包的 js 文件,比如 swiper 或者 jquery 这样的东西,好了,当然现在只是提供最简单的下载操作。

首先假定我们的数据结构是这样的

{
    images: [{
        name: 'c.png',
        data: 'asdqwer',
    }],
    jss: [{
        name: 'test.js',
        data: 'console.log(1)',
    }],
    csss: [{
        name: 'let.css',
        data: 'text.css { border: 1px solid #111;}',
    }],
    htmls: [{
        name: 'index.html',
        data: '<html><body><div>111</div></body></html>',
    }],
}

然后我们可以写这么一个类去下载这种数据格式的内容:

import JSZip from 'jszip';
 
/**
 * @param {Blob} blob
 * @param {string} name
 */
function downloadBlob(blob, name) {
    let alink = window.document.createElement('a');
    let evt = window.document.createEvent('HTMLEvents');
    evt.initEvent('click', false, false);
    alink.download = name;
    alink.href = window.URL.createObjectURL(blob);
    alink.dispatchEvent(evt);
    alink.click();
}
/**
 * @param {Array} images
 * @param {Array} jss
 * @param {Array} csss
 * @param {Array} htmls
 * @return {Promise}
 */
function buildZip({images, jss, csss, htmls} = datas) {
    let zip = new JSZip();
    let img = zip.folder('images');
    let js = zip.folder('js');
    let css = zip.folder('css');
    images.forEach((image) =>img.file(image.name, image.data));
    jss.forEach((jsFile) =>js.file(jsFile.name, jsFile.data));
    csss.forEach((cssFile) =>css.file(cssFile.name, cssFile.data));
    htmls.forEach((html) => zip.file(html.name, html.data));
    return zip.generateAsync({type: 'blob'})
        .then(function(content) {
            return Promise.resolve(content);
        });
}
 
/**
 * @param {Object} data
 */
export async function build(data) {
    let content = await buildZip(data);
    downloadBlob(content, 'test');
}
 

最后下载下来效果差不多是这样

显示效果

看起来效果还不错,下面就是构建其余的内容了。

4. element的自动化导出

现在我们碰到一个难题了,之前我们只搞了一个 类叫做 dragable element,但是我们忽然发现了一个问题,这个 element 需要继承多个类了,比如下面就要实现的一个类叫做 auto export element(明明可以用的接口的方式非要用类,是不是该扇自己一巴掌 = =,还有这都什么鬼名字!)

没关系这不我们还可以用 mixin 来实现问题不大!

经过一番内心的挣扎以后,我最终将魔爪伸向了 decorator ,拿 decorator 实现 mixins 貌似是比较好的方案了,大致可以这么写:

/**
 * @desc 基础类,提供 mixins 方式的 decorator
 * @param {Array} list
 * @return {Function}
 */
export default function mixins(...list) {
    return function(target) {
        Object.assign(target.prototype, ...list);
    };
}

假装用一下

import mixins from 'common/mixins.decorator';
let AutoExportElement = {
    foo() {
        console.log('a');
    },
};
 
 
@mixins(AutoExportElement)
/**
 * 画布上使用的 Img
 */
export default class _Image extends DragableElement {
    _img = null;
    /**
     * 初始化
     */
    constructor() {
        let _image = new Image();
        _image.width = 100;
        _image.height = 100;
        super(_image);
        this._img = _image;
    }
}

看起来效果很 nice 啊,所以就用这个吧,然后把乱七八糟的 class 都用别名规整一下

这些内容有点多简单的讲就是把 所有引用 util/common 目录下内容的全部使用 common/xxx代替

目录结构修正为:

目录结构

将部分 common 的 class 全部挪到了 utils 内部

随后调整 webpack.dev.js:

plugins: [
    ...
],
// 上面是plugin啥的配置,这里加一行
resolve: {
    extensions: ['.js'],
    alias: {
        'common': path.resolve(__dirname, './../src/utils/common/'),
    },
},

以后对于不同的方法使用不同后缀标明

模式使用方法后缀
decorator@xxxx.decorator.js
es classclass xxx extends yyy.class.js
mixinimport mixin from ‘common/mixin.decorator’; @minixs(xxx).mixin.js
辅助方法(主要为函数)import xxx from ‘common/xxx.helper’;xxx().helper.js

其实在此前就已经有对这些类型的内容作出了一定程度上的修正和显示。

下面让我们开始写一下那个 AutoExportElement (摔,什么鬼名字)

4.1 类的功能

我们知道对于 h5 页面 都有一下组成 :

  • html
  • css
  • js
  • image

对于每一个 element 都会包含以上内容:

假设对于一个没有 场景 概念的 h5 而言(指的是只有一页的那种)大概需要这样做(讲不清楚了还是画图吧)

图

大致要编译成上面那几种东西。

简单的讲就是需要将之前的 那个 element 提供以下输出接口,使用管道或者类似于管道的方法处理:

  • exportCssClass
  • exportJsEvent
  • exportJsAnimation
  • exportHtmlElement

输出的内容再填充对应模板后,增加对应lib库后通过,前文所述的 download 在下载。

/**
 * @mixin AutoExportElement
 */
const AutoExportElement = {
    _getClassName() {
        return 'i' + this.key;
    },
    exportCssClass() {
        let className = this._getClassName();
        let left = this.x ? `left: ${this.x}px;` : '';
        let top = this.y ? `top: ${this.y}px;`: '';
        let template = `
            .${className} {
                ${left}
                ${top}
            }
        `;
        return template;
    },
    exportJsEvent() {
        return '';
    },
    exportJsAnimation() {
        return '';
    },
    exportHtmlElement() {
        let className = this._getClassName();
        let innerHTML = this._dom.html();
        return `
            <div class="${className} item">${innerHTML}</div>
        `;
    },
};
export default AutoExportElement;

大致就这样吧,随便拖拽几个元素,试着下载一下,效果还可以

目录图片

html

css

使用效果如下:

5 一些零散的补充

5.1 decorator

首先是 decorator ,这一个功能并不是原生支持的,同时也在babel原生支持中被移除,目前即使是最新的草案(截止到 es8),这一部分都没有纳入提案中,但是在 es6 发型之初这一内容以及被提及,而且许多其他的后端语言也都有此支持,我觉得使用 decorator 是一个很方便的选择(当然大量的前端书籍也都会提及修饰器模式,或是类似的思想)

那么在使用前是需要作出一定的配置的:

首先是在 babelrc 中需要加入 transform-decorators-legacy

"plugins": ["transform-decorators-legacy"]

此外需要安装对应的 babel-plugin (babel-plugin-transform-decorators-legacy)

具体的 decorator 如何使用大可前往 阮一峰 的es6入门教程 观看,这是目前为止 es6 教程中最好的一份了,同时下文提到 async 和 await 也在其中。

5.2 async & await

从实现角度来讲,实现一个 async 和 await 在基于promise & generator 存在的基础下,并不困难,所以不论任何我都会加入这一个库,同样的这也不是 babel 原生提供转换的。

我们需要在 babelrc 中加入 transform-async-to-generator

"plugins": ["transform-async-to-generator"]

除了安装对应的 babel-plugin 外还需要对 eslintrc 作出修改以支持 上述特性

    "parserOptions": {
        "experimental": true
    },

5.3 路径补全方面

很多人问过我,在使用 webpack alias 后就没法自动补全了,对于其他编辑器暂不知道如何处理,但是对于webstorm只需要简单的将alias的文件的父文件夹设置为 resource 即可:

图片

之后自动补全就能自动的将比如前文设置的 common 自动识别了

5.4 关于注释方面

有同学问我说我使用的注释是基于什么规范的,这里基本参考的是 jsdoc 3 的规范

具体的可以查阅 jsdoc 3 的官网规范,其实常用的也就那么几种,使用注释的好处最关键的是提供了显式的类型声明,配合 eslint 在代码编写阶段就可以显示出错误的方法使用或者类型错误。(说这么多为啥不用typescript 摔!)

同样的在写代码的时候将会非常“智能” 的提示你相关的函数,如下图所示

例如对于 factory 生成的对象提供了 dragable element 所有的方法:

声明 declare

使用 factory

所有被 jsdoc 声明的 类,函数,方法,变量 等都会相对智能的在编辑过程有所提示,并且多数情况下能被 eslint 识别是否具有改方法。

5.5 关于代码风格方面

这里其实更换了代码风格使用了 google 的标准而不是此前 Airbnb + 少量自定义规则,主要是出于觉得 google 的风格更喜欢,所以就转换了过去,并没有什么最佳实践的原因。

此外有人追问我说这边 eslint 的 规则太严苛,和 vue-cli 那边的一样(QAQ vue 那边给的不也就是 standard,google 和 airbnb 么),根本无法遵守,其实对于代码风格我其实不太 care 因为大部分情况下我的代码是编辑器自动整理的,很少人肉编码,也就不存在所谓的 规则严苛 无法遵守的情况。

而事实上,eslint 中 google 和 airbnb 指出的绝大部分错误(如 不允许使用 eval 这些),都是不应该出现的,确实应该改正的,假定你确实认为你清楚的知道在做什么(如果 this 相关的一些配置),大可以屏蔽掉这几行的 eslint。

这里上一下配置:

配置

然后一般我会在 工具栏 摆一个:

eslint

然后设置一个快捷键,这样就不用老人肉处理了,或者设置一个 task 在保存的时候自动格式化。

至于其他编辑器,但凡提供 eslint 功能的编辑器都应当集成了 eslint-fix 的功能,即使没有大部分编辑器都提供了 hook 的手段,利用 hook 大可以编写一个脚本触发就行了。

至于用文本编辑器的大佬。。。emmmm 你都用文本编辑器,命令行输一下不就完事儿了,还要啥轮子啊╮(╯_╰)╭

5.6 关于 lience 问题

凡是有明确作者和来源的,在模块的开头都会标注 @author 以及 @see ,如果有遗漏的,请联系我以改正,至于 lience 这个东西我也是一知半解,凡是有任何设置到 协议 存在问题的,可以直接联系我解决。。。(生平首次收到提出 lience 使用不正确,之前都没注意过  ̄へ ̄ 后面一定会注意的)

6 结语

本章的话基本主要实现了h5的下载功能,那么实际上并没有完成一个标准化的模板搭建,这里的话暂且还是匆匆结束一下,准备吧那部分内容放置到下一章,那么下一章主要会包含下面几个功能:

  1. h5页面的模板
  2. 扩展属性
  3. 视图层单项数据流

如果有时间的话可能会酌情增加一些内容。

本文标题:[h5] 写个h5编辑器有多难?[2]

永久链接:https://iceprosurface.com/2018/05/01/2018/h5/h5-2/

作者授权:本文由 icepro 原创编译并授权刊载发布。

版权声明:本文使用「署名-非商业性使用-相同方式共享 4.0 国际」创作共享协议,转载或使用请遵守署名协议。

查看源码: