aamis520

aamis520 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

喜欢动手实践,喜欢篮球,喜欢技术,喜欢前端的即时呈现

个人动态

aamis520 关注了专栏 · 2020-11-16

createjs由浅入深

由于业务的需求,开始做H5动画项目,第一次接触createjs,附上我学习的笔记以及心得,一起学习一起交流

关注 7

aamis520 赞了文章 · 2019-07-24

解决vue2.0下IE浏览器白屏问题

公司新开发的项目需要兼容到IE9+

就在index.html页面加入

<meta http-equiv="X-UA-Compatible" content="IE=edge">

不起作用

总结方法:

  1. 兼容IE9/IE10可能会遇到语法或者 Promise错误,安装 babel-polyfill

    npm install babel-polyfill

  2. 在页面入口配置main.js中引入 babel-polyfill

    import("babel-polyfill")

  3. 解决编译错误----重要

    重新安装一下webpack-dev-server
    npm install webpack-dev-server@2.6.1 如果有淘宝镜像也可使用 cnpm
  4. 修改 build/webpack.base.conf.js

    clipboard.png

    app:['babel-polyfill','./src/main.js']
  5. 安装babel-preset-es2015-ie
  6. 根目录下新建.babelrc

    {
      "presets": [
        
        ["env", {
          "modules": false,
          "targets": {
            "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
          }
        }],
        "stage-2"
      ],
      "plugins": ["transform-runtime"],
      "env": {
        "test": {
          "presets": ["env", "stage-2"],
          "plugins": ["istanbul"]
        }
      }
    }
    
  7. 以上操作完成后 运行

    npm run dev

查看原文

赞 4 收藏 3 评论 1

aamis520 关注了专栏 · 2019-07-03

从入门到放弃

XXX,从入门到放弃

关注 11

aamis520 回答了问题 · 2019-03-26

解决如果我需要在这个方法里传入参数怎么办

匿名函数接收组件传递的参数,然后在匿名函数中把组件的参数传递给你的业务函数,在你的业务函数中可以传递你自定义的参数

<i-switch size="large" @on-change="(val => {
    change(val, youParams)
})" >   
                            
</i-switch> 

关注 6 回答 6

aamis520 发布了文章 · 2019-03-26

iview组件事件额外传递参数

<template>
    <Table border :columns="columns6" :data="data5" @on-row-click="(row, index) => { youFunc(row, index , 'params')}"></Table>
</template>
<script>
    export default {
        methods:{
          youFunc(row, index, params) {
            console.log(row, index, params)
          }
        },
        data () {
            return {
                columns6: [
                    {
                        title: 'Date',
                        key: 'date'
                    },
                    {
                        title: 'Name',
                        key: 'name'
                    },
                    {
                        title: 'Age',
                        key: 'age',
                        filters: [
                            {
                                label: 'Greater than 25',
                                value: 1
                            },
                            {
                                label: 'Less than 25',
                                value: 2
                            }
                        ],
                        filterMultiple: false,
                        filterMethod (value, row) {
                            if (value === 1) {
                                return row.age > 25;
                            } else if (value === 2) {
                                return row.age < 25;
                            }
                        }
                    },
                    {
                        title: 'Address',
                        key: 'address',
                        filters: [
                            {
                                label: 'New York',
                                value: 'New York'
                            },
                            {
                                label: 'London',
                                value: 'London'
                            },
                            {
                                label: 'Sydney',
                                value: 'Sydney'
                            }
                        ],
                        filterMethod (value, row) {
                            return row.address.indexOf(value) > -1;
                        }
                    }
                ],
                data5: [
                    {
                        name: 'John Brown',
                        age: 18,
                        address: 'New York No. 1 Lake Park',
                        date: '2016-10-03'
                    },
                    {
                        name: 'Jim Green',
                        age: 24,
                        address: 'London No. 1 Lake Park',
                        date: '2016-10-01'
                    },
                    {
                        name: 'Joe Black',
                        age: 30,
                        address: 'Sydney No. 1 Lake Park',
                        date: '2016-10-02'
                    },
                    {
                        name: 'Jon Snow',
                        age: 26,
                        address: 'Ottawa No. 2 Lake Park',
                        date: '2016-10-04'
                    }
                ],
            }
        }
    }
</script>
查看原文

赞 3 收藏 1 评论 0

aamis520 回答了问题 · 2019-03-18

解决根据数据不同的值,渲染出不同的div,类型太多该怎么写呢?

建议搭配 switch 使用, 建议这种type 太多的 让后台直接拼好template 然后render,参考https://cn.vuejs.org/v2/api/#...

关注 2 回答 2

aamis520 赞了文章 · 2019-03-12

Vue & Bootstrap 结合学习笔记(一)

本文是不才在学习Vue和Bootstrap过程中遇到问题解决的一些思路,主要描述了项目搭建,组件封装、获取、编辑、更新的一步步实现,一些解决方案也没找到正确的官方API,还请大拿们多多提点。

项目介绍

旨在通过项目的形式同时学习Vue和Bootstrap,实现一个在线配置页面的功能。通过Bootstrap封装好的组件样式提供界面需要的组件,通过Vue实现组件状态更改及页面渲染。

项目地址

https://github.com/shixia226/bootstrap-vue-designer

项目设计

  • 组件模块区
    提供可用于拖拽到编辑区的所有组件,分类别展示

    该功能与本学习目的关联不强,且其主要拖拽功能比较花时间,暂且搁置
  • 页面编辑区
    提供所有已添加到页面的组件的编辑预览,并提供组件增,删,排版,选中功能

    增,删,排版功能可以与模板区的拖拽功能结合,同样暂时搁置
  • 组件配置区
    提供具体组件内部状态查看及更改功能

项目搭建

  1. 基本的项目搭建,创建index.html, index.js配置好webpack

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Vue Demo</title>
    </head>
    <body>
        <script data-original="../index.js"></script>
    </body>
    </html>
    module.exports = {
        entry: './index.js',
        output: {
            filename: 'index.js'
        },
        module: {
            rules: [{
                test: /^[^.]+\.scss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }, {
                test: /(\.js|\.vue)$/,
                exclude: /(node_modules|bower_components)(?!.*webpack-dev-server)/,
                loader: 'babel-loader',
                query: {
                    "presets": ["env"]
                }
            }]
        }
    };
  2. Bootstrap样式引入

    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  3. Vue框架引入

    <script data-original="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  4. 运行

    //node
    webpack-dev-server --port=9926
    //Browser
    http://localhost:9926/

第一个组件Badage

Bootstrap官网例子:

<span class="badge badge-light badge-pill">9</span>

组件分析

  • badge-light 样式可以替换成badge-primary等,可以设置成属性变量用于选择哪个颜色;
  • badge-pill 样式有和无表现是不一样的,可以设置属性变量用于控制要不该样式;
  • 9 文本内容作为最终的展示内容,可以设置成属性变量;
  • 组件名取 widget-badge.

Vue组件封装

Vue.component('widget-badge', {
    template: `<span :class="['badge', theme ? 'badge-' + theme : '', pill ? 'badge-pill' : '']">{{text}}</span>`,
    props: ['theme', 'pill', 'text']
});

组件展示

html

<div class="app">
    <widget-badge></widget-badge>
</div>

js

new Vue({
    el: '.app'
})

组件配置

以上步骤后刷新浏览器应该是可以看到组件效果了,但该组件的所有属性都是在标签内写死的,无法在编辑页面动态设置

动态属性

  • vue 中 props 属性是不允许动态更改的,一般都只能更改 data 中的属性值,所以需要把 props 中的所有可变属性拷贝一份到 data 中,且命名上不能相同,所以在此先规定 data 中的所有属性都以字母'v'开头;
  • 每个可变属性加一个编辑项,对应属性名name="vpropA", 取值为当前属性值:value="vpropsA",所有的编辑项全部定义属性 editor 上。

    没找到对应获取editor属性值的API,但通过分析vue对象发现可以通过vue实例vm.$options.editor获取到该定义值,暂且先就这么用着。

组件封装更改如下:

Vue.component('widget-badge', {
    template: `<span :class="['badge', 'badge-' + vtheme, vpill ? 'badge-pill' : '']">{{vtext}}</span>`,
    props: ['theme', 'pill', 'text'],
    editor: `
        <input name="vtheme" :value="vtheme" />
        <input name="vpill" :value="vpill" />
        <input name="vtext" :value="vtext" />
    `,
    data() {
        return {
            vtheme: this.theme || 'secondary',
            vpill: this.pill,
            vtext: this.text || 'Badge'
        }
    }
});

属性配置面板

  • 点击不同的组件要展示对应的(不同的)配置面板

根据点击元素获取所属vue组件

vue本来就是通过状态更新的方式更改dom的,所以很少有dom相关的api,又只得分析vue实例里的数据,发现$children好像就是直接下级组件的一个集合,且$children每一项里都又一个$el的属性对应到实际DOM元素
function getVueCmp(vm, elem) {
    let pelems = [],
        $root = vm.$el;
    while (elem !== $root) {
        pelems.push(elem);
        elem = elem.parentNode;
    }
    return getVueCmpByPelem(vm, pelems);
}
function getVueCmpByPelem(vm, pelems) {
    let $children = vm.$children;
    if ($children) {
        for (let i = 0, len = $children.length; i < len; i++) {
            let vcmp = $children[i],
                $el = vcmp.$el,
                idx = pelems.indexOf($el);
            if (idx !== -1) {
                pelems.length = idx;
                return getVueCmpByPelem(vcmp, pelems);
            }
        }
    }
    return vm;
}

增加点击事件

<div class="app" @click="showPpt">
    <widget-badge></widget-badge>
</div>

获取组件实时数据

根据前面的数据命名规则直接遍历$data中所有以字母'v'开头的属性
function getVueCmpData(vcmp) {
    if (!vcmp) return {};
    let $data = vcmp.$data,
        data = {};
    let names = Object.getOwnPropertyNames($data);
    for (let i = 0, len = names.length; i < len; i++) {
        let name = names[i];
        if (name.charAt(0) === 'v') {
            data[name.substr(1)] = $data[name];
        }
    }
    return data;
}

数据更新

在vue根节点上设置全局监听事件,然后在属性值中定义$emit方法触发该监听事件
  • 根节点设置监听事件,并将监听结果反馈到当前选中的组件上
created() {
    this.$on('changeppt', function(name, value) {
        if (vcmp) {
            let names = name.split('.'),
                data = vcmp,
                len = names.length - 1;
            for (let i = 0; i < len; i++) {
                data = data[names[i]];
            }
            data[names[len]] = value;
        }
    })
}
  • 封装编辑器的输入框为组件如下:
Vue.component('editor-text', {
    template: `<input v-model="vvalue" @change="$root.$emit('changeppt', name, vvalue)">`,
    props: ['name', 'value'],
    data() {
        return {
            vvalue: this.value
        }
    }      
})
  • 更改编辑器配置如下
{
    ...
    /*
    editor: `
        <input name="vtheme" :value="vtheme" />
        <input name="vpill" :value="vpill" />
        <input name="vtext" :value="vtext" />
    `,
    */
    editor: `
        <editor-text name="vtheme" :value="theme" ></editor-text>
        <input name="vpill" :value="pill" ></editor-text>
        <input name="vtext" :value="text" ></editor-text>
    `,
    ...
}

vue最终初始化更改如下

new Vue({
    el: '.app',
    data: {
        pptCmp: undefined
    },
    watch: {
        pptCmp(vcmp) {
            new Vue({
                el: '.ppt',
                template: '<div class="ppt">' + (vcmp ? vcmp.$options.editor || '' : '') + '</div>',
                data() {
                    return getVueCmpData(vcmp, true);
                },
                created() {
                    this.$on('changeppt', function(name, value) {
                        if (vcmp) {
                            let names = name.split('.'),
                                data = vcmp,
                                len = names.length - 1;
                            for (let i = 0; i < len; i++) {
                                data = data[names[i]];
                            }
                            data[names[len]] = value;
                        }
                    })
                }
            })
        }
    },
    methods: {
        showPpt: function(evt) {
            let elem = evt.target;
            if (!document.querySelector('.ppt').contains(elem)) {
                let vcmp = getVueCmp(this, elem);
                if (vcmp === this.$root) {
                    vcmp = null;
                }
                this.pptCmp = vcmp;
            }
        }
    }
}
查看原文

赞 1 收藏 0 评论 0

aamis520 收藏了文章 · 2018-10-17

浅谈使用 Vue 构建前端 10w+ 代码量的单页面应用开发底层

开始之前

随着业务的不断累积,目前我们 ToC 端主要项目,除去 node_modulesbuild 配置文件dist 静态资源文件的代码量为 137521 行,后台管理系统下各个子应用代码,除去依赖等文件的总行数也达到 100万 多一点。

代码量意味不了什么,只能证明模块很多,但相同两个项目,在运行时性能相同情况下,你的 10 万行代码能容纳并维护 150 个模块,并且开发顺畅,我的项目中 10 万行代码却只能容纳 100 个模块,添加功能也好,维护起来也较为繁琐,这就很值得思考

本文会在主要描述以 Vue 技术栈技术主体ToC 端项目业务主体,在构建过程中,遇到或者总结的点(也会提及一些 ToB 项目的场景),可能并不适合你的业务场景(仅供参考),我会尽可能多的描述问题与其中的思考,最大可能的帮助到需要的同学,也辛苦开发者发现问题或者不合理/不正确的地方及时向我反馈,会尽快修改,欢迎有更好的实现方式来 pr

Git 地址
React 项目

可以参考蚂蚁金服数据体验技术团队编写的文章:

本文并不是基于上面文章写的,不过当时在看到他们文章之后觉得有相似的地方,相较于这篇文章,本文可能会枯燥些,会有大量代码,同学可以直接用上仓库看。

① 单页面,多页面

首先要思考我们的项目最终的构建主体单页面,还是多页面,还是单页 + 多页,通过他们的优缺点来分析:

  • 单页面(SPA)

    • 优点:体验好,路由之间跳转流程,可定制转场动画,使用了懒加载可有效减少首页白屏时间,相较于多页面减少了用户访问静态资源服务器的次数等。
    • 缺点:初始会加载较大的静态资源,并且随着业务增长会越来越大,懒加载也有他的弊端,不做特殊处理不利于 SEO 等。
  • 多页面(MPA)

    • 优点:对搜索引擎友好,开发难度较低。
    • 缺点:资源请求较多,整页刷新体验较差,页面间传递数据只能依赖 URLcookiestorage 等方式,较为局限。
  • SPA + MPA

    • 这种方式常见于较老 MPA 项目迁移至 SPA 的情况,缺点结合两者,两种主体通信方式也只能以兼容MPA 为准
    • 不过这种方式也有他的好处,假如你的 SPA 中,有类似文章分享这样(没有后端直出,后端返 HTML 串的情况下),想保证用户体验在 SPA 中开发一个页面,在 MPA 中也开发一个页面,去掉没用的依赖,或者直接用原生 JS 来开发,分享出去是 MPA 的文章页面,这样可以加快分享出去的打开速度,同时也能减少静态资源服务器的压力,因为如果分享出去的是 SPA 的文章页面,那 SPA 所需的静态资源至少都需要去进行协商请求,当然如果服务配置了强缓存就忽略以上所说。

我们首先根据业务所需,来最终确定构建主体,而我们选择了体验至上的 SPA,并选用 Vue 技术栈。

② 目录结构

其实我们看开源的绝大部分项目中,目录结构都会差不太多,我们可以综合一下来个通用的 src 目录:

src
├── assets          // 资源目录 图片,样式,iconfont
├── components      // 全局通用组件目录
├── config          // 项目配置,拦截器,开关
├── plugins         // 插件相关,生成路由、请求、store 等实例,并挂载 Vue 实例
├── directives      // 拓展指令集合
├── routes          // 路由配置
├── service         // 服务层
├── utils           // 工具类
└── views           // 视图层

③ 通用组件

components 中我们会存放 UI 组件库中的那些常见通用组件了,在项目中直接通过设置别名来使用,如果其他项目需要使用,就发到 npm 上。

结构

// components 简易结构
components
├── dist
├── build
├── src      
    ├── modal
    ├── toast
    └── ...
├── index.js             
└── package.json        

项目中使用

如果想最终编译成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置简单的打包逻辑,搭配着 package.json 构建 UI组件 的自动化打包发布,最终部署 dist 下的内容,并发布到 npm 上即可。

而我们也可直接使用 es6 的代码:

import 'Components/src/modal'

其他项目使用

假设我们发布的 npm 包bm-ui,并且下载到了本地 npm i bm-ui -S:

修改项目的最外层打包配置,在 rules 里 babel-loaderhappypack 中添加 includenode_modules/bm-ui

// webpack.base.conf
...
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
    },
    {
        test: /\.js$/,
        loader: 'babel-loader',
        // 这里添加
        include: [resolve('src'), resolve('test'), resolve('node_modules/bm-ui')]
    },{
    ...
    }]
...

然后搭配着 babel-plugin-import 直接在项目中使用即可:

import { modal } from 'bm-ui'

多个组件库

同时有多个组件库的话,又或者有同学专门进行组件开发的话,把 `components
内部细分`一下,多一个文件分层。

components
├── bm-ui-1 
├── bm-ui-2
└── ...

你的打包配置文件可以放在 components 下,进行统一打包,当然如果要开源出去还是放在对应库下。

④ 全局配置,插件与拦截器

这个点其实会是项目中经常被忽略的,或者说很少聚合到一起,但同时我认为是整个项目中的重要之一,后续会有例子说道。

全局配置,拦截器目录结构

config
├── index.js             // 全局配置/开关
├── interceptors        // 拦截器
    ├── index.js        // 入口文件
    ├── axios.js        // 请求/响应拦截
    ├── router.js       // 路由拦截
    └── ...
└── ...

全局配置

我们在 config/index.js 可能会有如下配置:

// config/index.js

// 当前宿主平台 兼容多平台应该通过一些特定函数来取得
export const HOST_PLATFORM = 'WEB'
// 这个就不多说了
export const NODE_ENV = process.env.NODE_ENV || 'prod'

// 是否强制所有请求访问本地 MOCK,看到这里同学不难猜到,每个请求也可以单独控制是否请求 MOCK
export const AJAX_LOCALLY_ENABLE = false
// 是否开启监控
export const MONITOR_ENABLE = true
// 路由默认配置,路由表并不从此注入
export const ROUTER_DEFAULT_CONFIG = {
    waitForData: true,
    transitionOnLoad: true
}

// axios 默认配置
export const AXIOS_DEFAULT_CONFIG = {
    timeout: 20000,
    maxContentLength: 2000,
    headers: {}
}

// vuex 默认配置
export const VUEX_DEFAULT_CONFIG = {
    strict: process.env.NODE_ENV !== 'production'
}

// API 默认配置
export const API_DEFAULT_CONFIG = {
    mockBaseURL: '',
    mock: true,
    debug: false,
    sep: '/'
}

// CONST 默认配置
export const CONST_DEFAULT_CONFIG = {
    sep: '/'
}

// 还有一些业务相关的配置
// ...


// 还有一些方便开发的配置
export const CONSOLE_REQUEST_ENABLE = true      // 开启请求参数打印
export const CONSOLE_RESPONSE_ENABLE = true     // 开启响应参数打印
export const CONSOLE_MONITOR_ENABLE = true      // 监控记录打印

可以看出这里汇集了项目中所有用到的配置,下面我们在 plugins 中实例化插件,注入对应配置,目录如下:

插件目录结构

plugins
├── api.js              // 服务层 api 插件
├── axios.js            // 请求实例插件
├── const.js            // 服务层 const 插件
├── store.js            // vuex 实例插件
├── inject.js           // 注入 Vue 原型插件
└── router.js           // 路由实例插件

实例化插件并注入配置

这里先举出两个例子,看我们是如何注入配置,拦截器并实例化的

实例化 router

import Vue from 'vue'
import Router from 'vue-router'
import ROUTES from 'Routes'
import {ROUTER_DEFAULT_CONFIG} from 'Config/index'
import {routerBeforeEachFunc} from 'Config/interceptors/router'

Vue.use(Router)

// 注入默认配置和路由表
let routerInstance = new Router({
    ...ROUTER_DEFAULT_CONFIG,
    routes: ROUTES
})
// 注入拦截器
routerInstance.beforeEach(routerBeforeEachFunc)

export default routerInstance

实例化 axios

import axios from 'axios'
import {AXIOS_DEFAULT_CONFIG} from 'Config/index'
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from 'Config/interceptors/axios'

let axiosInstance = {}

axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)

// 注入请求拦截
axiosInstance
    .interceptors.request.use(requestSuccessFunc, requestFailFunc)
// 注入响应拦截
axiosInstance
    .interceptors.response.use(responseSuccessFunc, responseFailFunc)

export default axiosInstance

我们在 main.js注入插件

// main.js
import Vue from 'vue'

GLOBAL.vbus = new Vue()

// import 'Components'// 全局组件注册
import 'Directives' // 指令

// 引入插件
import router from 'Plugins/router'
import inject from 'Plugins/inject'
import store from 'Plugins/store'
// 引入组件库及其组件库样式 
// 不需要配置的库就在这里引入 
// 如果需要配置都放入 plugin 即可
import VueOnsen from 'vue-onsenui'
import 'onsenui/css/onsenui.css'
import 'onsenui/css/onsen-css-components.css'
// 引入根组件
import App from './App'

Vue.use(inject)
Vue.use(VueOnsen)

// render
new Vue({
    el: '#app',
    router,
    store,
    template: '<App/>',
    components: { App }
})

axios 实例我们并没有直接引用,相信你也猜到他是通过 inject 插件引用的,我们看下 inject

import axios from './axios'
import api from './api'
import consts from './const'
GLOBAL.ajax = axios
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        Vue.prototype.$ajax = axios
        Vue.prototype.$const = consts
        // 需要挂载的都放在这里
    }
}

这里可以挂载你想在业务中( vue 实例中)便捷访问的 api,除了 $ajax 之外,apiconst 两个插件是我们服务层中主要的功能,后续会介绍,这样我们插件流程大致运转起来,下面写对应拦截器的方法。

请求,路由拦截器

ajax 拦截器中(config/interceptors/axios.js):

// config/interceptors/axios.js

import {CONSOLE_REQUEST_ENABLE, CONSOLE_RESPONSE_ENABLE} from '../index.js'

export function requestSuccessFunc (requestObj) {
    CONSOLE_REQUEST_ENABLE && console.info('requestInterceptorFunc', `url: ${requestObj.url}`, requestObj)
    // 自定义请求拦截逻辑,可以处理权限,请求发送监控等
    // ...
    
    return requestObj
}

export function requestFailFunc (requestError) {
    // 自定义发送请求失败逻辑,断网,请求发送监控等
    // ...
    
    return Promise.reject(requestError);
}

export function responseSuccessFunc (responseObj) {
    // 自定义响应成功逻辑,全局拦截接口,根据不同业务做不同处理,响应成功监控等
    // ...
    // 假设我们请求体为
    // {
    //     code: 1010,
    //     msg: 'this is a msg',
    //     data: null
    // }
    
    let resData =  responseObj.data
    let {code} = resData
    
    switch(code) {
        case 0: // 如果业务成功,直接进成功回调  
            return resData.data;
        case 1111: 
            // 如果业务失败,根据不同 code 做不同处理
            // 比如最常见的授权过期跳登录
            // 特定弹窗
            // 跳转特定页面等
            
            location.href = xxx // 这里的路径也可以放到全局配置里
            return;
        default:
            // 业务中还会有一些特殊 code 逻辑,我们可以在这里做统一处理,也可以下方它们到业务层
            !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit('global.$dialog.show', resData.msg);
            return Promise.reject(resData);
    }
}

export function responseFailFunc (responseError) {
    // 响应失败,可根据 responseError.message 和 responseError.response.status 来做监控处理
    // ...
    return Promise.reject(responseError);
}

定义路由拦截器(config/interceptors/router.js):

// config/interceptors/router.js

export function routerBeforeFunc (to, from, next) {
    // 这里可以做页面拦截,很多后台系统中也非常喜欢在这里面做权限处理
    
    // next(...)
}

最后在入口文件(config/interceptors/index.js)中引入并暴露出来即可:

import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from './ajax'
import {routerBeforeEachFunc} from './router'

let interceptors = {
    requestSuccessFunc,
    requestFailFunc,
    responseSuccessFunc,
    responseFailFunc,
    routerBeforeEachFunc
}

export default interceptors

请求拦截这里代码都很简单,对于 responseSuccessFunc 中 switch default 逻辑做下简单说明:

  1. responseObj.config.noShowDefaultError 这里可能不太好理解

我们在请求的时候,可以传入一个 axios 中并没有意义的 noShowDefaultError 参数为我们业务所用,当值为 false 或者不存在时,我们会触发全局事件 global.dialog.showglobal.dialog.show我们会注册在 app.vue 中:

// app.vue

export default {
    ...
    created() {
        this.bindEvents
    },
    methods: {
        bindEvents() {
            GLOBAL.vbus.$on('global.dialog.show', (msg) => {
                if(msg) return
                // 我们会在这里注册全局需要操控试图层的事件,方便在非业务代码中通过发布订阅调用
                this.$dialog.popup({
                    content: msg 
                });
            })
        }
        ...
    }
}
这里也可以把弹窗状态放入 Store 中,按团队喜好,我们习惯把公共的涉及视图逻辑的公共状态在这里注册,和业务区分开来
  1. GLOBAL 是我们挂载 window 上的全局对象,我们把需要挂载的东西都放在 window.GLOBAL 里,减少命名空间冲突的可能。
  2. vbus 其实就是我们开始 new Vue() 挂载上去的
GLOBAL.vbus = new Vue()
  1. 我们在这里 Promise.reject 出去,我们就可以在 error 回调里面只处理我们的业务逻辑,而其他如断网超时服务器出错等均通过拦截器进行统一处理。

拦截器处理前后对比

对比下处理前后在业务中的发送请求的代码

拦截器处理前

this.$axios.get('test_url').then(({code, data}) => {
    if( code === 0 ) {
        // 业务成功
    } else if () {}
        // em... 各种业务不成功处理,如果遇到通用的处理,还需要抽离出来
    
    
}, error => {
   // 需要根据 error 做各种抽离好的处理逻辑,断网,超时等等...
})

拦截器处理后

// 业务失败走默认弹窗逻辑的情况
this.$axios.get('test_url').then(({data}) => {
    // 业务成功,直接操作 data 即可
})

// 业务失败自定义
this.$axios.get('test_url', {
    noShowDefaultError: true // 可选
}).then(({data}) => {
    // 业务成功,直接操作 data 即可
    
}, (code, msg) => {
    // 当有特定 code 需要特殊处理,传入 noShowDefaultError:true,在这个回调处理就行
})

为什么要如此配置与拦截器?

在应对项目开发过程中需求的不可预见性时,让我们能处理的更快更好

到这里很多同学会觉得,就这么简单的引入判断,可有可无,
就如我们最近做的一个需求来说,我们 ToC 端项目之前一直是在微信公众号中打开的,而我们需要在小程序中通过 webview 打开大部分流程,而我们也没有时间,没有空间在小程序中重写近 100 + 的页面流程,这是我们开发之初并没有想到的。这时候必须把项目兼容到小程序端,在兼容过程中可能需要解决以下问题:

  1. 请求路径完全不同。
  2. 需要兼容两套不同的权限系统。
  3. 有些流程在小程序端需要做改动,跳转到特定页面。
  4. 有些公众号的 api ,在小程序中无用,需要调用小程序的逻辑,需要做兼容。
  5. 很多也页面上的元素,小程序端不做展示等。
可以看出,稍微不慎,会影响公众号现有逻辑。
  • 添加请求拦截 interceptors/minaAjax.jsinterceptors/minaRouter.js,原有的换更为 interceptors/officalAjax.jsinterceptors/officalRouter.js,在入口文件interceptors/index.js根据当前宿主平台,也就是全局配置 HOST_PLATFORM,通过代理模式策略模式,注入对应平台的拦截器minaAjax.js中重写请求路径和权限处理,在 minaRouter.js 中添加页面拦截配置,跳转到特定页面,这样一并解决了上面的问题 1,2,3
  • 问题 4 其实也比较好处理了,拷贝需要兼容 api 的页面,重写里面的逻辑,通过路由拦截器一并做跳转处理
  • 问题 5 也很简单,拓展两个自定义指令 v-mina-show 和 v-mina-hide ,在展示不同步的地方可以直接使用指令。

最终用最少的代码,最快的时间完美上线,丝毫没影响到现有 toC 端业务,而且这样把所有兼容逻辑绝大部分聚合到了一起,方便二次拓展和修改。

虽然这只是根据自身业务结合来说明,可能没什么说服力,不过不难看出全局配置/拦截器 虽然代码不多,但却是整个项目的核心之一,我们可以在里面做更多 awesome 的事情。

⑤ 路由配置与懒加载

directives 里面没什么可说的,不过很多难题都可以通过他来解决,要时刻记住,我们可以再指令里面操作虚拟 DOM。

路由配置

而我们根据自己的业务性质,最终根据业务流程来拆分配置:

routes
├── index.js            // 入口文件
├── common.js           // 公共路由,登录,提示页等
├── account.js          // 账户流程
├── register.js         // 挂号流程
└── ...

最终通过 index.js 暴露出去给 plugins/router 实例使用,这里的拆分配置有两个注意的地方:

  • 需要根据自己业务性质来决定,有的项目可能适合业务线划分,有的项目更适合以 功能 划分。
  • 在多人协作过程中,尽可能避免冲突,或者减少冲突。

懒加载

文章开头说到单页面静态资源过大,首次打开/每次版本升级后都会较慢,可以用懒加载来拆分静态资源,减少白屏时间,但开头也说到懒加载也有待商榷的地方:

  • 如果异步加载较多的组件,会给静态资源服务器/ CDN 带来更大的访问压力的同时,如果当多个异步组件都被修改,造成版本号的变动,发布的时候会大大增加 CDN 被击穿的风险。
  • 懒加载首次加载未被缓存的异步组件白屏的问题,造成用户体验不好。
  • 异步加载通用组件,会在页面可能会在网络延时的情况下参差不齐的展示出来等。

这就需要我们根据项目情况在空间和时间上做一些权衡。

以下几点可以作为简单的参考:

  • 对于访问量可控的项目,如公司后台管理系统中,可以以操作 view 为单位进行异步加载,通用组件全部同步加载的方式。
  • 对于一些复杂度较高,实时度较高的应用类型,可采用按功能模块拆分进行异步组件加载。
  • 如果项目想保证比较高的完整性和体验,迭代频率可控,不太关心首次加载时间的话,可按需使用异步加载或者直接不使用。
打包出来的 main.js 的大小,绝大部分都是在路由中引入的并注册的视图组件。

⑥ Service 服务层

服务层作为项目中的另一个核心之一,“自古以来”都是大家比较关心的地方。

不知道你是否看到过如下组织代码方式:

views/
    pay/
        index.vue
        service.js
        components/
            a.vue
            b.vue

service.js 中写入编写数据来源

export const CONFIAG = {
    apple: '苹果',
    banana: '香蕉'
}
// ...

// ① 处理业务逻辑,还弹窗
export function getBInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    }).then({age} => {
        this.$modal.show({
            content: age
        })
    })
}

// ② 不处理业务,仅仅写请求方法
export function getAInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    })
}

...

简单分析:

  • ① 就不多说了,拆分的不够单纯,当做二次开发的时候,你还得去找这弹窗到底哪里出来的。
  • ② 看起来很美好,不掺杂业务逻辑,但不知道你与没遇到过这样情况,经常会有其他业务需要用到一样的枚举,请求一样的接口,而开发其他业务的同学并不知道你在这里有一份数据源,最终造成的结果就是数据源的代码到处冗余

我相信②在绝大多数项目中都能看到。

那么我们的目的就很明显了,解决冗余,方便使用,我们把枚举和请求接口的方法,通过插件,挂载到一个大对象上,注入 Vue 原型,方面业务使用即可。

目录层级(仅供参考)

service
├── api
    ├── index.js             // 入口文件
    ├── order.js             // 订单相关接口配置
    └── ...
├── const                   
    ├── index.js             // 入口文件
    ├── order.js             // 订单常量接口配置
    └── ...
├── store                    // vuex 状态管理
├── expands                  // 拓展
    ├── monitor.js           // 监控
    ├── beacon.js            // 打点
    ├── localstorage.js      // 本地存储
    └── ...                  // 按需拓展
└── ...

抽离模型

首先抽离请求接口模型,可按照领域模型抽离 (service/api/index.js):

{
    user: [{
        name: 'info',
        method: 'GET',
        desc: '测试接口1',
        path: '/api/info',
        mockPath: '/api/info',
        params: {
            a: 1,
            b: 2
        }
    }, {
        name: 'info2',
        method: 'GET',
        desc: '测试接口2',
        path: '/api/info2',
        mockPath: '/api/info2',
        params: {
            a: 1,
            b: 2,
            b: 3
        }
    }],
    order: [{
        name: 'change',
        method: 'POST',
        desc: '订单变更',
        path: '/api/order/change',
        mockPath: '/api/order/change',
        params: {
            type: 'SUCCESS'
        }
    }]
    ...
}

定制下需要的几个功能:

  • 请求参数自动截取。
  • 请求参数不传,则发送默认配置参数。
  • 得需要命名空间。
  • 通过全局配置开启调试模式。
  • 通过全局配置来控制走本地 mock 还是线上接口等。

插件编写

定制好功能,开始编写简单的 plugins/api.js 插件:

import axios from './axios'
import _pick from 'lodash/pick'
import _assign from 'lodash/assign'
import _isEmpty from 'lodash/isEmpty'

import { assert } from 'Utils/tools'
import { API_DEFAULT_CONFIG } from 'Config'
import API_CONFIG from 'Service/api'


class MakeApi {
    constructor(options) {
        this.api = {}
        this.apiBuilder(options)
    }


    apiBuilder({
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        Object.keys(config).map(namespace => {
            this._apiSingleBuilder({
                namespace, 
                mock, 
                mockBaseURL, 
                sep, 
                debug, 
                config: config[namespace]
            })
        })
    }
    _apiSingleBuilder({
        namespace, 
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        config.forEach( api => {
            const {name, desc, params, method, path, mockPath } = api
            let apiname = `${namespace}${sep}${name}`,// 命名空间
                url = mock ? mockPath : path,//控制走 mock 还是线上
                baseURL = mock && mockBaseURL
            
            // 通过全局配置开启调试模式。
            debug && console.info(`调用服务层接口${apiname},接口描述为${desc}`)
            debug && assert(name, `${apiUrl} :接口name属性不能为空`)
            debug && assert(apiUrl.indexOf('/') === 0, `${apiUrl} :接口路径path,首字符应为/`)

            Object.defineProperty(this.api, `${namespace}${sep}${name}`, {
                value(outerParams, outerOptions) {
                
                    // 请求参数自动截取。
                    // 请求参数不穿则发送默认配置参数。
                    let _data = _isEmpty(outerParams) ? params : _pick(_assign({}, params, outerParams), Object.keys(params))
                    return axios(_normoalize(_assign({
                        url,
                        desc,
                        baseURL,
                        method
                    }, outerOptions), _data))
                }
            })      
        })
    }       
}

function _normoalize(options, data) {
    // 这里可以做大小写转换,也可以做其他类型 RESTFUl 的兼容
    if (options.method === 'POST') {
        options.data = data
    } else if (options.method === 'GET') {
        options.params = data
    }
    return options
} 
// 注入模型和全局配置,并暴露出去
export default new MakeApi({
    config: API_CONFIG,
    ...API_DEFAULT_CONFIG
})['api']

挂载到 Vue 原型上,上文有说到,通过 plugins/inject.js

import api from './api'
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        // 需要挂载的都放在这里
    }
}

使用

这样我们可以在业务中愉快的使用业务层代码:

// .vue 中
export default {
    methods: {
        test() {
            this.$api['order/info']({
                a: 1,
                b: 2
            })
        }
    }
}

即使在业务之外也可以使用:

import api from 'Plugins/api'

api['order/info']({
    a: 1,
    b: 2
})

当然对于运行效率要求高的项目中,避免内存使用率过大,我们需要改造 API,用解构的方式引入使用,最终利用 webpacktree-shaking 减少打包体积。几个简单的思路

一般来说,多人协作时候大家都可以先看 api 是否有对应接口,当业务量上来的时候,也肯定会有人出现找不到,或者找起来比较费劲,这时候我们完全可以在 请求拦截器中,把当前请求的 urlapi 中的请求做下判断,如果有重复接口请求路径,则提醒开发者已经配置相关请求,根据情况是否进行二次配置即可。

最终我们可以拓展 Service 层的各个功能:

基础

  • api异步与后端交互
  • const常量枚举
  • storeVuex 状态管理

拓展

  • localStorage:本地数据,稍微封装下,支持存取对象即可
  • monitor监控功能,自定义搜集策略,调用 api 中的接口发送
  • beacon打点功能,自定义搜集策略,调用 api 中的接口发送
  • ...

constlocalStoragemonitorbeacon 根据业务自行拓展暴露给业务使用即可,思想也是一样的,下面着重说下 store(Vuex)

插一句:如果看到这里没感觉不妥的话,想想上面 plugins/api.js 有没有用单例模式?该不该用?

⑦ 状态管理与视图拆分

Vuex 源码分析可以看我之前写的文章

我们是不是真的需要状态管理?

答案是否定的,就算你的项目达到 10 万行代码,那也并不意味着你必须使用 Vuex,应该由业务场景决定。

业务场景

  1. 第一类项目:业务/视图复杂度不高,不建议使用 Vuex,会带来开发与维护的成本,使用简单的 vbus 做好命名空间,来解耦即可。
let vbus = new Vue()
vbus.$on('print.hello', () => {
    console.log('hello')
})

vbus.$emit('print.hello')
  1. 第二类项目:类似多人协作项目管理有道云笔记网易云音乐微信网页版/桌面版应用,功能集中,空间利用率高,实时交互的项目,无疑 Vuex 是较好的选择。这类应用中我们可以直接抽离业务领域模型
store
├── index.js          
├── actions.js        // 根级别 action
├── mutations.js      // 根级别 mutation
└── modules
    ├── user.js       // 用户模块
    ├── products.js   // 产品模块
    ├── order.js      // 订单模块
    └── ...

当然对于这类项目,vuex 或许不是最好的选择,有兴趣的同学可以学习下 rxjs

  1. 第三类项目:后台系统或者页面之间业务耦合不高的项目,这类项目是占比应该是很大的,我们思考下这类项目:

全局共享状态不多,但是难免在某个模块中会有复杂度较高的功能(客服系统,实时聊天,多人协作功能等),这时候如果为了项目的可管理性,我们也在 store 中进行管理,随着项目的迭代我们不难遇到这样的情况:

store/
    ...
    modules/
        b.js
        ...
views/
    ...
    a/
        b.js
        ...
        
  • 试想下有几十个 module,对应这边上百个业务模块,开发者在两个平级目录之间调试与开发的成本是巨大的。
  • 这些 module 可以在项目中任一一个地方被访问,但往往他们都是冗余的,除了引用的功能模块之外,基本不会再有其他模块引用他。
  • 项目的可维护程度会随着项目增大而增大。

如何解决第三类项目的 store 使用问题?

先梳理我们的目标:

  • 项目中模块可以自定决定是否使用 Vuex。(渐进增强)
  • 从有状态管理的模块,跳转没有的模块,我们不想把之前的状态挂载到 store 上,想提高运行效率。(冗余)
  • 让这类项目的状态管理变的更加可维护。(开发成本/沟通成本)

实现

我们借助 Vuex 提供的 registerModuleunregisterModule 一并解决这些问题,我们在 service/store 中放入全局共享的状态:

service/
    store/
        index.js
        actions.js
        mutations.js
        getters.js
        state.js
一般这类项目全局状态不多,如果多了拆分 module 即可。

编写插件生成 store 实例

import Vue from 'vue'
import Vuex from 'vuex'
import {VUEX_DEFAULT_CONFIG} from 'Config'
import commonStore from 'Service/store'

Vue.use(Vuex)

export default new Vuex.Store({
    ...commonStore,
    ...VUEX_DEFAULT_CONFIG
})

对一个需要状态管理页面或者模块进行分层:

views/
    pageA/
        index.vue
        components/
            a.vue
            b.vue
            ...
        children/
            childrenA.vue
            childrenB.vue
            ...
        store/
            index.js
            actions.js
            moduleA.js  
            moduleB.js

module 中直接包含了 gettersmutationsstate,我们在 store/index.js 中做文章:

import Store from 'Plugins/store'
import actions from './actions.js'
import moduleA from './moduleA.js'
import moduleB from './moduleB.js'

export default {
    install() {
        Store.registerModule(['pageA'], {
            actions,
            modules: {
                moduleA,
                moduleB
            },
            namespaced: true
        })
    },
    uninstall() {
        Store.unregisterModule(['pageA'])
    }
}

最终在 index.vue 中引入使用, 在页面跳转之前注册这些状态和管理状态的规则,在路由离开之前,先卸载这些状态和管理状态的规则

import store from './store'
import {mapGetters} from 'vuex'
export default {
    computed: {
        ...mapGetters('pageA', ['aaa', 'bbb', 'ccc'])
    },
    beforeRouterEnter(to, from, next) {
        store.install()
        next()
    },
    beforeRouterLeave(to, from, next) {
        store.uninstall()
        next()
    }
}

当然如果你的状态要共享到全局,就不执行 uninstall

这样就解决了开头的三个问题,不同开发者在开发页面的时候,可以根据页面特性,渐进增强的选择某种开发形式。

其他

这里简单列举下其他方面,需要自行根据项目深入和使用。

打包,构建

这里网上已经有很多优化方法:dllhappypack多线程打包等,但随着项目的代码量级,每次 dev 保存的时候编译的速度也是会愈来愈慢的,而一过慢的时候我们就不得不进行拆分,这是肯定的,而在拆分之前尽可能容纳更多的可维护的代码,有几个可以尝试和规避的点:

  1. 优化项目流程:这个点看起来好像没什么用,但改变却是最直观的,页面/业务上的化简为繁会直接体现到代码上,同时也会增大项目的可维护,可拓展性等。
  2. 减少项目文件层级纵向深度。
  3. 减少无用业务代码,避免使用无用或者过大依赖(类似 moment.js 这样的库)等。

样式

  • 尽可能抽离各个模块,让整个样式底层更加灵活,同时也应该尽可能的减少冗余。
  • 如果使用的 sass 的话,善用 %placeholder 减少无用代码打包进来。
MPA 应用中样式冗余过大,%placeholder 也会给你带来帮助。

Mock

很多大公司都有自己的 mock 平台,当前后端定好接口格式,放入生成对应 mock api,如果没有 mock 平台,那就找相对好用的工具如 json-server 等。

代码规范

请强制使用 eslint,挂在 git 的钩子上。定期 diff 代码,定期培训等。

TypeScript

非常建议用 TS 编写项目,可能写 .vue 有些别扭,这样前端的大部分错误在编译时解决,同时也能提高浏览器运行时效率,可能减少 re-optimize 阶段时间等。

测试

这也是项目非常重要的一点,如果你的项目还未使用一些测试工具,请尽快接入,这里不过多赘述。

拆分系统

当项目到达到一定业务量级时,由于项目中的模块过多,新同学维护成本,开发成本都会直线上升,不得不拆分项目,后续会分享出来我们 ToB 项目在拆分系统中的简单实践。

最后

时下有各种成熟的方案,这里只是一个简单的构建分享,里面依赖的版本都是我们稳定下来的版本,需要根据自己实际情况进行升级。

项目底层构建往往会成为前端忽略的地方,我们既要从一个大局观来看待一个项目或者整条业务线,又要对每一行代码精益求精,对开发体验不断优化,慢慢累积后才能更好的应对未知的变化。

最后请允许我打一波小小的广告

EROS

如果前端同学想尝试使用 Vue 开发 App,或者熟悉 weex 开发的同学,可以来尝试使用我们的开源解决方案 eros,虽然没做过什么广告,但不完全统计,50 个在线 APP 还是有的,期待你的加入。

最后附上部分产品截图~

(逃~)

查看原文

aamis520 评论了文章 · 2018-09-29

vuescroll-一款基于vuejs2.x的虚拟滚动条

介绍

Vuescroll 一个功能强大,有多种模式的基于Vue.js的滚动条插件,它的原理是创建 div 用于包裹要滚动的内容,操后操作容器的样式或者scrollTop或scrollLeft完成内容的滚动。

设计它的目的是用来美化和增强你的滚动条。

你可以通过更改配置来选择不同的模式:

  • native 模式: 类似于原生的滚动条,但是可以自定义滚动条样式,使用于 PC 端用户。
  • slide 模式: 允许你用手指或鼠标滑动内容, 可以滑动超出边界范围,适用于移动端端用户。

你也可以通过更改配置滚动条的样式,包括:

  • 透明度
  • 高度/宽度
  • 位置
  • 背景色
  • 是否保持显示
想了解更多请访问官方网站指南页面

如果你不满足上述特性,想要扩展特性的话,请考虑贡献代码

总的来说,Vuescroll 不仅仅只一个滚动条, 你可以用它制作一个轮播图、时间选择器、能够自动侦测内容发生变化的一个插件等等。下面是部分预览效果。

预览



在线 Demo & 文档

  • 你可以浏览这个 repo 的根目录下的 Demo 文件夹。
  • 详细的 Demo, 文档: 请访问 官方地址.

特点

基本特点

  • 支持自定义滚动条,包括可以设置滚动条/轨道的颜色透明度。可以设置滚动条是否保持显示
  • 支持平滑滚动,可以通过设置easing来获得不同的滚动动画。
  • 支持自动检测内容是否发生变化,可以查看这个demo

只在 slide 模式下有效的特点

其他特点

快速开始

引入

在你的入口文件处:

import Vue from 'vue';
import vuescroll from 'vuescroll';
import 'vuescroll/dist/vuescroll.css';

Vue.use(vuescroll);

为了去掉不会用到的部分,可以分开地引入 vuescroll

只引入 slide 模式的特性

import Vue from 'vue';
import vuescroll from 'vuescroll/dist/vuescroll-slide';
import 'vuescroll/dist/vuescroll.css';

Vue.use(vuescroll);

只引入 native 模式的特性:

import Vue from 'vue';
import vuescroll from 'vuescroll/dist/vuescroll-native';
import 'vuescroll/dist/vuescroll.css';

Vue.use(vuescroll);

用法

把你需要滚动的内容用vuescroll包裹起来即可

  <template>
    <div class='your-container'>
        <!-- bind your configurations -->
        <vue-scroll :ops="ops">
            <div class='your-content'>
            </div>
        </vue-scroll>
    </div>
  </template>
  <script>
    export default {
      data() {
        return {
          ops: {
            // some configs....
          }
        }
      }
    }
  </script>

指南列表

最后

附上项目的地址 希望朋友们多多star 哈哈

查看原文

aamis520 收藏了文章 · 2018-09-25

WEEX-EROS | 或许不用 RN ,我们也能开发一个 APP

与其一堆原理,倒不如先直接介绍 eros 到底能干什么?

eros 是基于 weex 的,他可以让前端同学通过 vue 的语法和 api 来写出 iOS/Android 原生应用

学习 weex 或者 eros 之前,请您一定要熟练使用 vue2 进行开发。

本文会通过 eros 由来,优缺点评估,开发现状,环境搭建,调试等方面来介绍 eros,并会在文章后半部分与开发者一起写一个 Hello World,来更好的评估 eros。

eros 现状

目前 eros 已有数十个 app 在开发中和上线的状态,其中有正在开发中的国外应用 starLife(100+页面),也有国内正在开发的蜂觅(60+ 页面),还有已上线的应用都在 0-50+ 页面不等,行业分布于资讯,医疗,招商,购物,政府,办公等。

而本木医疗(京医通)技术团队本身也基于 weex 开发了三个已上线的 app,均可在苹果商店和应用宝下载:

  • 健康首都(京医通 app版,100+页面,很多功能还未开放)
  • 本木医疗助手(30+页面)
  • 本木医生助理(20+页面)

所以开发者大可放心,这不是一个 KPI 项目(公司没有 KPI ),已基于 MIT 协议开源。

eros 是怎么来的

我们需要先从 weex 说起 ,我们直接看原理图:
weex原理

weex 原理图中分为了 Server 和 Client 两部分。

Server

weex file: 就是我们的 .vue 文件(或官方的 .we 文件),和我们平时进行前端开发的一样。

transformer: 以 vue 开发为例,在浏览器中我们通常都会写 .vue 文件和 es6 等浏览器目前并不支持的语法,通过前端资源打包工具 webpack 等通过 vue-loaderbabel-loader等最终转化成为 es5 代码,让浏览器识别,而在 weex 最终编译出来资源文件能在浏览器跑,自然也是这个道理。

而浏览器端运行的 es5 代码是无法直接运行在客户端的(如浏览器有 BOM,DOM,客户端是没有的),所以在通过 weex 来开发客户端有很多限制的。

在编译客户端静态资源文件的时候,是通过 weex-loader 来加载这些经过限制语法编写的 weex file,最终编译成为能让客户端读懂的 JS 文件,也就是 JS Bundle

有兴趣的同学可以深入了解这部分内容。深入Weex中的transformer实现原理

所以这块之所以叫 server,也就是这些静态资源文件可以在远端服务器打包生成,被客户端访问到并下载解析。

Client

引用 weex 官网上的话。

Weex 的 iOS 和 Android 客户端中都会运行一个 JavaScript 引擎,来执行 JS bundle,同时向各端的渲染层发送规范化的指令,调度客户端的渲染和其它各种能力。我们在 iOS 下选择了 JavaScriptCore 内核,而在 Android 下选择了 UC 提供的 v8 内核。无论是从性能还是稳定性方面都提供了强有力的保障。

为了让整个移动应用的资源利用得更好,我们在客户端提供的 JavaScript 引擎是单例的,即所有 JS bundle 公用一个 JavaScript 内核实例,同时对每个 JS bundle 在运行时进行了上下文的隔离,使得每个 JS bundle 都能够高效安全的工作。我们还把 Vue 2.0 这样的 JS Framework 做了预置,开发者不必把 JS Framework 打包在每个 JS bundle 里,从而大大减少了 JS bundle 的体积,也就进一步保障了页面打开的速度。

client 对于前端来说肯定是越了解会更好,不了解也没关系,但 weex 有个功能是很重要的,那就是 weex 搭建起了一条 JS Bridge,通过客户端自定义 modulecomponent,让前端与客户端有了交互能力。

而自定义 modulecomponent 需要一定的客户端开发知识,让很多前端开发的同学,望而却步,又因为官方的环境搭建,脚手架打包等目前还存在一些问题,把很多想学习 weex 的同学拦在了外面,所以 eros 因应而生。

weex 官方文档

eros 介绍

eros 是基于 weex 封装面向前端的 vue 写法的解决方案,由于 app 开发的特殊性,eros 则更偏重关心于整个 app 项目,当熟练使用了 eros 之后,您能快速通过 vue 暴露出来的方法快速构建原生 app 应用。

eros 流程图如下:
eros原理

eros 能解决什么

  • 详细的文档来解决环境搭建过程中的坑。
  • 一套代码编译成 ios,android 两端原生应用。
  • 封装了大量 module,让前端开发方便进行原生的操作。
  • 提供 appboard 机制来减少包体积,并可以对其实时修改。
  • 内置了一套完整的 widget ,可根据业务自行修改。
  • 中介者模式来集中管理业务。
  • 提供了服务器端增量发布更新逻辑。
  • 脚手架可直接生成开发最新模板。
  • 脚手架启动服务进行实时开发效果查看和 debug 调试。
  • 脚手架更新开发平台所需 eros 依赖。
  • 脚手架支持打对应平台内置包。
  • 脚手架支持生成全量包,增量包,并内置与更新服务器交互逻辑。
  • 脚手架支持同步更新模板内容。
  • 脚手架支持 weex 的 vue 入口和 js 入口两种开发方式。
  • 支持 weex debug
  • 支持 weex-ui,bui-weex 等组件库

eros 不能做什么

eros 开发中也有很多限制,需要开发者自行斟酌。
  • weex 代码在浏览器端还有很多兼容性问题,所有 eros 目前不支持浏览器端
  • eros 不能使用 weex 市场,如果您有原生开发经验可以自行接入
  • 由于 eros 对 JS Bundle 运行机制采用了 appboard 机制来减少了 js bundle 的大小,导致 weex debug 需要特定的处理。
  • eros 打出来的包体积稍大,为解决这个问题,eros 客户端动态加载依赖正在开发中。
  • 如果遇到复杂的页面,如 IM 之类的,eros 建议用原生实现,weex 应付此类需求还是比较吃力。

支持性

  • Android 4.1 (API 16)
  • iOS 8.0+
  • WebKit 534.30+

开始 eros

脚手架安装:

$ npm i eros-cli -g

如果你在中国地区,我们还是推荐您使用 cnpm 安装脚手架

$ cnpm i eros-cli -g 

如果安装过程中报错,是因为 eros-cli 依赖了 node-sass,解决的方式有很多,可以自行搜索解决一下。

darwin 开发 iOS:

  • Xcode
  • CocoaPods

    • 升级 Ruby 环境:$ sudo gem update --system
    • 移除现有 Ruby 镜像:$ gem sources --remove https://rubygems.org/
    • 添加ruby-china镜像:$ gem source -a https://gems.ruby-china.org/
    • 安装 CocoaPods:$ sudo gem install cocoapods
    • 如果以上命令报错则执行:$ sudo gem install -n /usr/local/bin cocoapods --pre
    • 最后执行:$ pod setup 过程比较漫长,请耐心等待执行完成

darwin/windows/linux 开发 Android:

JDK 是 JAVA 开发包,AndroidStudio 是 Android开发IDE,这两项不再做过多介绍。

如果您使用虚拟机进行跨平台开发,也需要配置好对应平台的所需环境。

模拟器安装

  • ios 开发中 xcode 已经自带了模拟器。
  • android 开发者可以下载 Genymotion

模板生成

  1. 首先通过脚手架自动生成开发模板(我们不推荐用sudo来执行)。
$ eros init

按提示选择模板,填写 app/项目名称和版本后在当前路径下会生成对应模板,然后 cd 到项目中。
eros-init

  1. 下载所需依赖
$ npm install
$ eros install

eros install 会让你选择下载依赖:

  • ios: eros ios 开发所需依赖
  • android: eros android 开发所需依赖
  • components: eros 内置了 weex-ui 和 bui 2套组件库,一般来说不用执行,当我们更新 weex-ui 和 bui 的时候可以执行。

每次 eros 解决了 bug 或者开发/更改了 module 和 component 时,只需要 eros install ,ios 更新前请关闭 xcode, android 会在编辑器 android studio 中弹出同步,点击即可。

安卓同步

eros install 是执行 iOS/Android 目录中的 install.sh,所以 windows 系统下最好用 Git Bash 等工具,不然无法下载。
  1. 安装完依赖之后:
  • iOS: 会自动打开 Xcode ,然后选择一个模拟器,点击左上角的播放(运行)按钮,即可看到内置包中已经内置好的 eros demo.
  • Android:首次生成项目开发者需要多几个步骤,之后每次 eros install 都会有同步提示:
  1. 点击AndroidStudio上方的 File--->New--->Import Project。

  1. 找到eros在你本地的地址,选择 platforms/android/WeexFrameworkWrapper ,点击OK。

  1. 待项目构建完成,点击 AndroidStudio 上方工具栏的 Run ,即可运行项目。

注意:

第一次打开 AndroidStuido 时,由于本地环境未配置好,AndroidStuido 会提示错误,按照 IDE 提示,大部分环境问题都可以解决。

于是 eros 的 demo 便能在模拟器中跑起来了。

eros-demo

在 eros demo 中我们可以看到:

  • tab1 中内置了 weex-ui 并有 demo
  • tab2 中内置了 bui 并有 demo
  • tab3 中有大部分 widget 使用的实例
  • tab4 中跑了官方的瀑布流例子和我们编写一个支持手势滑动的多个 List 的类似新闻的实例。
eros 的 demo 很重要,建议在开发中,首先跟随 demo 编写几个页面,并保留其代码作为使用参考。

开发前

我们先来介绍 eros 开发中需要知道的点:

Server JS Bundle

本地开发的时候(运行脚手架 eros dev 指令),脚手架 eros-cli 会通过读取配置文件来在特定端口跑一个服务,让你在本地访问到项目中 dist 下通过 webpack 打包生成的 JS Bundle。

假如你配置的端口号是8899,在浏览器中输入localhost:8899/dist 便可以看到打包生成的 JS Bundle。

而在不同调试载体通过 localhost 访问这些 JS Bundle 之前,都需要确保在同一局域网内,而在访问的时候,情况是不同的:

载体hosts 文件是否需要手动修改网络代理
iOS 模拟器共享电脑 hosts 文件不需要
Android 模拟器需要
iOS 真机需要
Android 真机需要

iOS 模拟器比较特殊,是因为 iOS 模拟器和 Mac 共用一套网络设置。

由此也能看出来,如果开发者需要进行两个端的快速开发,直接用 iOS 模拟器开发即可,开发完成之后,在适配真机和 Android 端是最快的。

所以直接修改网络代理,指向本机的 IP 地址即可访问到 JS Bundle,而我们一般为了代理软件抓包时候看这更方便,会给个新的 host :

127.0.0.1   app.weex-eros.com

设置完了之后,在不同载体的浏览器中都能访问 http://app.weex-eros.com/dist 下的 JS Bundle 了。

Client JS Bundle

上面介绍了通过服务来访问 JS Bundle,那我们拔了真机拔了数据线,断了网,没了有 JS Bundle 来源,用户打开是一片空白怎么办?答案就是 app 内置中 JS Bundle,我们也叫这部分 JS Bundle 为内置包,这个过程叫打内置包

Interceptor 拦截器

那么又有问题来了,我们如何告诉 app 是访问服务包还是内置包呢?答案是 Interceptor 开关。

Interceptor

  • Interceptor 选中的时候,我们会拦截请求,让 app 读取内置包;
  • Interceptor 未选中的时候,不拦截请求,让 app 去配置的服务上去取服务包;

第一次跑起来 demo 的开发者可以看到,拦截器是开启的,访问的是内置包,app 上线,交付测试的时候,都是走内置包。

项目结构

下面列出了对于开发而言关心的项目结构:

.
├── config
│   ├── eros.dev.js                     // 脚手架配置文件
│   └── eros.native.js                  // 客户端配置文件
├── platforms                           
│   ├── android                         // Android 平台主项目和依赖
│   └── ios                             // iOS 平台主项目和依赖
└── src
    ├── assets                          // 本地静态资源存放,一般可存放图片
    ├── iconfont                        // 本地 iconfont 存放
    ├── js
    │   ├── components                  // 组件,存放了经过修改的 weex-ui 和 bui
    │   ├── config                      // 项目开发配置
    │   │   ├── apis.js                 // 接口别名配置
    │   │   ├── index.js
    │   │   ├── pages.js                // 路由别名配置
    │   │   └── push.js                 // 个推事件处理
    │   ├── css                         // 可抽离公共 css 逻辑
    │   ├── mediator                    // 中介者
    │   ├── pages                       // 页面开发,所有页面都放置在这里
    │   └── widget                      // widget 源码
    └── mock
        └── test                        // mock 服务,在 eros.dev.js 可进行配置

有几个需要注意的地方

  • eros.dev.js 中如果改变,这是如果你在跑着 eros dev 服务,需要断开,让脚手架重新读取配置文件。(开发中会经常添加新的打包入口)
  • eros.native.js客户端读取的配置文件,目前是客户端在开启 app 的时候统一从内置包中读取,所以当此文件变动的时候,需要重新打内置包 eros pack,重新运行下 app,即可生效。

Hello Eros

我们来简单开发一个 Hello World:

1.首先关闭调试中的拦截器,让 app 访问服务包,这时候刷新页面肯定是空白的,因为都没有服务。

2.项目根目录下运行开发服务 eros dev,运行成功之后刷新出现内置的 demo 页面,这是其实你已经可以任意修改 pages/eros-demo 中代码,刷新后看效果了,有兴趣可以到处试一试。

tips: 双击调试按钮即可刷新。

3.在 pages 目录下新建一个 Hello.vue 文件。

Hello.vue
文件中写一些很简单的语法:

<template>
    <div style="margin-top: 50px;">
        <text class="title">Hello,</text>
        <text class="title">developer</text>
        <bui-button class="btn-250" type="primary" value="show eros">show eros</bui-button>
    </div>
</template>
<script>
    import buiButton from 'Eros/bui/components/bui-button'
    export default {
        components: { buiButton }
    }
</script>
<style>
.title{
    font-size: 60;
}
.btn-250{
    width: 250;
}
</style>

4.修改 eros.dev.js 中的 exports,如果不需要,可以把 eros-demo 中的路径都删掉,只填入新的文件入口 :

"exports": [
    // appBoard 
    "js/config/index.js",
    // mediator
    "js/mediator/index.vue",
    // home
    "js/pages/Hello.vue"
],

这里注意上面两个是和 eros.native.js 中的 appBoard,mediator 一一对应的,如果这里两边修改没有对应上会导致报错,建议平时不用变动。

5.告诉 app 我要重新改变首页,修改 eros.native.js 中的 page.homePage 路径:

"page": {
    "homePage": "/pages/Hello.js",
}

6.断开 eros dev 服务,因为要告诉脚手架配置文件的变动。

7.eros pack 打内置包,因为要告诉 app 配置文件中的变动。

8.重新运行(run)app。

这时首页就已经开发好了:
首页
下面我们修改做一个页面间的跳转,试一试 Widget:

9.再在 pages 目录下新建一个页面 Eros.vue

<template>
    <div style="margin-top: 50px;">
        <text class="title">Hi!</text>
        <text class="title">Enjoy it!</text>
    </div>
</template>
<script>
    export default {
    }
</script>
<style>
.title{
    font-size: 60;
}
</style>

10.修改 eros.dev.js 告诉脚手架添加页面了:

"exports": [
    // appBoard 
    "js/config/index.js",
    // mediator
    "js/mediator/index.vue",
    // home
    "js/pages/Hello.vue",
    // eros
    "js/pages/Eros.vue"
],

11.注册路由,修改 js/config/pages.js,清空 demo 中现有的配置:

export default {
    'Eros': {
        title: 'Eros',
        url: '/pages/Eros.js',
    },
}

这里的 url 是填写 dist 目录中打包出来 JS Bundle 的相对路径(现在并没有这个 JS Bundle,需要重启开发服务读取配置才会有),注意因为是 JS Bundle 所以以 .js 为结尾。

12.重启 eros dev,刷新一下,并无任何变化,这时候还无法跳转到新建的页面,因为只是配置了路由,并未触发跳转方法,我们需要修改下 Hello.vue:

<template>
    <div style="margin-top: 50px;">
        <text class="title">Hello,</text>
        <text class="title">developer</text>
        <bui-button class="btn-250" type="primary" value="show eros" @click="showEros">show eros</bui-button>
    </div>
</template>
<script>
    import buiButton from 'Eros/bui/components/bui-button'
    export default {
        components: { buiButton },
        methods: {
            // 这里给按钮添加 showEros 事件来跳转
            showEros() {
                this.$router.open({
                    name: 'Eros'
                })
            }
        }
    }
</script>
<style>
.title{
    font-size: 60;
}
.btn-250{
    width: 250;
}
</style>

13.双击调试按钮刷新,跳转逻辑已经完成了!

router

至此 Hello world 已经编写完成,可以便根据文档来编写你的业务了。

eros 还有 demo 是根据网易严选 demo 进行改编的(感谢 zwwill 的开源和指导),开发者也可以进行参考:

最后开发者需要自行修改原生项目中的一些信息,就可以发 app 正式版本,对外使用了,发布的方法网上有很多介绍,就不过多赘述。

增量发布

具体更新逻辑可以点击这里,这里写下简单的说明。

app 发布有两种情况:

  • 当 platforms ios/android 目录下的代码发生变动(包括 eros install 平台的依赖)的时候,我们是需要重新发布到市场上重新走审核逻辑的,用户需要重新去市场上面下载。
  • 而当项目中的业务逻辑发生变动,如新增页面,修改当前页面逻辑等,最终导致 JS Bundle 发生变化,便可以使用增量发布,每次 app 启动会自动检测更新,下载 JS Bundle 中发生变动的部分,用户重启即生效。

同时 eros-cli 也支持生成全量包和生成增量包:

生成全量包:

$ eros build

生成增量包:

$ eros build -d

目前增量发布的搭建还是比较麻烦,当开发者开发完 app 之后可以参考 eros-publish 来搭建增量发布的服务,有经验的同学也可以在发布机上部署 eros-cli,来自行编写发布系统。

就如已有 eros 开发者基于 eros-publish 编写了的自己增量发布系统 lygtq-eros-publish,并开源,非常感谢 hodgevk 的贡献。

总结

就如官方文档中所说,在熟悉了 eros 之后可以快速开发中小型 app 应用,但 eros 还有很多的不足,需要更多的人加入进来完善他,最终是想给 vue 开发者们另一个舞台。

查看原文

认证与成就

  • 获得 49 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-12
个人主页被 789 人浏览