<![CDATA[uis.cc - 前端技术博客]]> 2019-02-18T06:31:51.607Z http://www.uis.cc/ Hexo <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2019/02/18/Summary-of-Experience-in-Fast-Application-Development/ 2019-02-18T06:22:37.000Z 2019-02-18T06:31:51.607Z 在过去一年里我所在的前端小组主要负责快应用项目的开发与维护,随着产品不断完善、业务复杂度提升以及对接场景的多样化,前端代码的管理和维护成为我们团队的核心挑战之一。本文总结了汽车之家快应用在开发过程中遇到一些问题以及思考。

先介绍下开发环境

  • macOS 10.14.2
  • node v10.15.0
  • hap-toolkit 0.2.1
  • 编辑器 VSCode
  • 关于接口封装

    在项目中,针对接口的高频调用,需要封装高效且易用的公共方法,进而很大程度上提升代码规范质量及编码效率。封装应该解决的问题:

  • async await 支持
  • 易于配置扩展
  • 易于管理,方便调用
  • 统一错误处理
  • 先看一段接口配置文件

    在配置文件 api.js 中通过调用 reqMethod 方法构造接口函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // api.js
    import req from './reqMethod.js'; // 封装接口方法

    const baseUrl = 'https://www.域名1.com'; // 配置默认域名
    const reqMethod = function () {
    let arg = Array.from(arguments);
    arg.splice(3, 0, baseUrl);
    return req.apply(null, arg);
    };

    export const getBrandmenus = params => reqMethod('GET', `/api/path`, params);
    export const editNickname = params => reqMethod('GET', '/api/path', params);
    export const addFavouriteCar = params => reqMethod('GET', '/api/path', params);
    export const delFavouriteCar = params => reqMethod('GET', '/api/path', params);

    因为 const 特性保证了 API 接口名称的唯一性(多人开发不会出现命名冲突),并保证了接口配置集中在 api.js 文件中方便统一管理维护。

    将接口配置挂载到全局对象上

    1
    2
    3
    4
    // app.ux
    const injectRef = Object.getPrototypeOf(global) || global;
    import * as api from './api.js';
    injectRef.API = api;

    调用示例

    在页面中可以直接使用 asycn/await 方式调用全局 API 方法获取接口数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    export default {
    async getDataList() {
    try {
    const res = await API.getBrandmenus({
    sessionid: deviceInfo.deviceId
    });
    this.list = res.result.map(item => {
    item.date = UTILS.Formate(item.date, 'YYYY-MM-DD');
    return item;
    });
    } catch (error) {
    console.log(error);
    }
    }
    }

    reqMethod 实现:

    reqMethod(method, url, params, baseUrl, stateDetection, showPrompt)

    参数 说明 类型 可选值 默认值 可选性
    method
    请求类型
    String
    GET / POST
    required
    url
    请求地址
    String
    required
    params
    请求参数
    Object
    {}
    optional
    baseUrl
    基础路径配置,最终请求地址为 baseUrl + url
    String
    -
    optional
    stateDetection
    返回状态检测
    Boolean
    true / false
    true
    optional
    showPrompt
    是否弹窗提示错误状态
    Boolean
    true / false
    true
    optional
  • 增加 基础路径 参数,可支持多域名配置。(注:reqMethod 方法入参是完整 URL 时会覆盖默认域名配置)

  • 返回状态检测: 默认:是,业务规定 返回数据中 returncode = 0 为正常请求。

  • 是否弹窗提示: 默认:是,404、500、超时等是否弹窗提示。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    // reqMethod.js
    import fetch from '@system.fetch';
    import prompt from '@system.prompt';

    function reqMethod(method, url, params = {}, baseUrl, stateDetection = true, showPrompt = true) {
    fixXiaomiParamsBug(params);
    url = /^http/.test(url) ? url : baseUrl + url; // 扩展支持多域名配置
    params = sign(params);
    return request(method, url, params, stateDetection, showPrompt);
    }

    function request(method, url, params, stateDetection = true, showPrompt = true) {
    return new Promise((resolve, reject) => {
    fetch.fetch({
    url: url,
    data: params,
    method: method,
    success: (res) => {
    try {
    if (res.code !== 200) {
    prompt.showToast({
    message: '网络错误'
    });
    reject(res);
    return;
    }

    const data = JSON.parse(res.data);
    if (data.returncode === 0) {
    resolve(data);
    return;
    } else {
    if (!stateDetection) {
    resolve(data);
    return;
    }
    if (showPrompt) {
    prompt.showToast({
    message: data.message
    });
    }
    reject(res);
    }
    } catch (error) {
    reject(res);
    }
    },
    fail: function (res, code) {
    prompt.showToast({
    message: '网络错误'
    });
    reject(res);
    }
    });
    });
    }

    // 小米 1030 版本bug 小数点后超过3位的数字会被截取,解决方法转换成字符串传递
    function fixXiaomiParamsBug(params) {
    if (typeof params === 'object') {
    for (let key in params) {
    if (typeof params[key] === 'number') {
    const x = String(params[key]).indexOf('.') + 1; //小数点的位置
    const y = String(params[key]).length - x; //小数的位数
    if (y > 3) params[key] = String(params[key]);
    }
    }
    }
    }

    function sign(obj) {
    // 加密签名方法
    }

    export default reqMethod;

    sign 方法为签名方法,fixXiaomiParamsBug 修复 小米 1030 版本bug,小数点后超过 3 位的数字会被截取,解决方法为转换成字符串传递。

    缩小快应用rpk包的体积

    因为快应用对 rpk 有 1M 尺寸的限制,我们的业务在 3.0 初期版本时一度达到 900K 的尺寸,缩小 rpk 尺寸成为我们的首要任务。

    除了压缩图片,适量地使用网络图片,提取公共组件和方法外,我们还发现:在快应用的模板文件中,如果多个页面通过 import 方式引入相同公共 js 文件,最后这个文件会被多次打包到 rpk 文件中,也就是说构建工具不会提取页面之间的重复引入,在公共模块使用频率较高的情况下会大幅增加包的体积。

    解决方法:

    将公共方法挂载到全局作用域上,模板中直接调用全局方法。最终打包的结果中只包含一份公共 js 的引入。

    入口文件 app.ux

    我们将 utils 文件夹下的方法挂在到全局 UTILS 下,对于高频使用的方法比如 API 方法可提取出来单独挂载,缩短调用路径。

    {2,6}
    1
    2
    3
    4
    5
    6
    7
    // app.ux
    const injectRef = Object.getPrototypeOf(global) || global; // 获取全局对象

    import * as Utils from './utils/index.js'; // 引入公共方法
    const { api } = Utils;
    injectRef.UTILS = Utils; // 挂载到全局对象
    injectRef.API = api; // API 方法是对 fetch 方法的封装含有 sign 和 token

    在业务代码中的调用方式,如:index.ux

    在模板中可直接通过 API.getBrandmenus 获取接口数据, UTILS.Formate 方法对日期做格式化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // index.ux
    export default {
    async getDataList() {
    try {
    const res = await API.getBrandmenus({ // 直接调用全局 API 方法获取接口数据。
    sessionid: deviceInfo.deviceId
    });
    this.list = res.result.map(item => {
    item.date = UTILS.Formate(item.date, 'YYYY-MM-DD'); // 调用全局公共方法 UTILS 下的 Formate 格式化时间
    return item;
    });
    } catch (error) {
    console.log(error);
    }
    }
    }

    callback 转换成 Promise 模式

    在快应用中很多系统能力 API 都是以 callback 形式提供。比如获取地理位置API:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    geolocation.getLocation({
    success: function(data) {
    console.log(
    `handling success: longitude = ${data.longitude}, latitude = ${
    data.latitude
    }`

    )
    },
    fail: function(data, code) {
    console.log(`handling fail, code = ${code}`)
    }
    })

    在业务中如果使用 callback 形式很容易写出回调地狱并且不利于代码整洁,我们可以通过一个简单的方法将 callback 形式的 API 转换成 Promise 模式的,这样业务中就可以使用 promise 或者 async/await 形式调用了。

    在我们的业务中有一个 promiseAPI.js 的公共方法,负责将 callback 转换成 Promise。

    {5}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // promiseAPI.js
    import storage from '@system.storage';
    import device from '@system.device';
    import network from '@system.network';
    import geolocation from '@system.geolocation';

    const promiseFactory = (pointer, params = {}) => {
    return new Promise((resolve, reject) => {
    params = Object.assign({
    success: (data) => { resolve(data); },
    fail: (err, code) => { reject(err, code) }
    }, params);
    pointer(params);
    });
    };

    // 获取设备信息。
    export const getDeviceInfo = () => promiseFactory(device.getInfo);
    // 获取设备Id。
    export const getDeviceId = () => promiseFactory(device.getId, { type: ["device", "mac"] });
    // 读取存储内容。
    export const getStorage = (key) => promiseFactory(storage.get, { key });
    // 修改存储内容。
    export const setStorage = (key, value) => promiseFactory(storage.set, { key, value });
    // 清空存储内容。
    export const clearStorage = (key, value) => promiseFactory(storage.clear);
    // 获取网络类型。
    export const getNetworkType = () => promiseFactory(network.getType);
    // 获取地理位置。
    export const getLocation = () => promiseFactory(geolocation.getLocation, { timeout: 3000 });

    业务代码中调用方式如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // index.ux
    import * as promiseApi from './promiseAPI.js';

    const getDeviceIdMethods = async () => {
    try {
    const data = await promiseApi.getDeviceId();
    systemInfo = Object.assign({}, systemInfo, {
    'deviceId': data.device,
    'IMEI': data.device,
    'macAddress': data.mac
    });
    } catch (error) {
    console.log('获取设备ID失败');
    }
    };

    tabs 优化

    一个内容丰富的选项卡,通常会包含许多页签内容。 tabs 系统组件默认会直接加载所有页签内容,导致 JS 线程持续忙于渲染每个页签,无法响应用户点击事件等,降低用户体验,为此我们在官方给出的 demo 基础上做出了一些优化。官方DEMO

    优化目标

  • 页签内容懒加载
  • 缓存:切换时渲染过的页签不再重复渲染,不再重复请求接口
  • 统计数据:可以分别统计每一个频道的访问次数和停留时长。
  • 效果:

    示例代码如下:

    {52,55,59,62}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    <import name="original-tab" src="./original.ux"></import>
    <import name="recommend-tab" src="./recommend.ux"></import>
    <import name="chejiahao-tab" src="./chejiahao.ux"></import>

    <template>
    <div class="container">
    <tabs onchange="onChangeTabIndex" index="{{currentindex}}" class="tab">
    <tab-bar class="tab-header" mode="scrollable">
    <stack class="tab-header__item" for="{{tabHeadList}}" @click="clickTabBar($idx)">
    <text class="tab-header__text {{currentindex == $idx ? 'tab-header__text--active' : ''}}">{{$item.title}}</text>
    <div class="tab-header__line {{currentindex == $idx ? 'tab-header__line--active' : ''}}"></div>
    </stack>
    </tab-bar>
    <tab-content class="tab-content">
    <div class="tab-content-section">
    <trend-tab page-show="{{tabHeadList[0].isShow}}"></trend-tab>
    </div>
    <div class="tab-content-section">
    <recommend-tab page-show="{{tabHeadList[1].isShow}}"></recommend-tab>
    </div>
    <div class="tab-content-section">
    <original-tab page-show="{{tabHeadList[2].isShow}}"></original-tab>
    </div>
    </tab-content>
    </tabs>
    </div>
    </template>

    <script>
    export default {
    data() {
    return {
    tabHeadList: [{
    title: '热榜',
    pv: 'hotlist',
    isShow: false
    },
    {
    title: '推荐',
    pv: 'recommend',
    isShow: false
    },
    {
    title: '原创',
    pv: 'original',
    isShow: false
    }
    ],
    currentindex: -1
    }
    },
    onShow() {
    this.watchCurrentIndexChange(this.currentindex, null);
    },
    onHide() {
    this.watchCurrentIndexChange(null, this.currentindex);
    },
    async onInit() {
    this.$watch('currentindex', 'watchCurrentIndexChange');
    this.currentindex = 1; // 可以根据业务 设置某一个tab为默认显示tab
    },
    watchCurrentIndexChange(newVal, oldVal) {
    if (oldVal !== null && oldVal >= 0) { // 判断条件:排除首次点击是
    this.tabHeadList.splice(oldVal, 1, Object.assign({}, this.tabHeadList[oldVal], {
    isShow: false
    }));
    PV_TRACK.endTime(this.tabHeadList[oldVal].pv);
    }
    if (newVal !== null && oldVal >= 0) { // 排除离开页面时阻止触发
    this.tabHeadList.splice(newVal, 1, Object.assign({}, this.tabHeadList[newVal], {
    isShow: true
    }));
    PV_TRACK.startTime(this.tabHeadList[newVal].pv);
    }
    },
    clickTabBar(index) {
    this.currentindex = index
    },
    onChangeTabIndex(evt) {
    this.currentindex = evt.index;
    },
    }
    </script>

    trend-tab 组件代码:

    {22,24,25}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <template>
    <div class="flex-coloum">
    <list class="feed-list">
    <list-item for="list" type="feedcard">
    <text>{{$item.title}}</text>
    </list-item>
    </list>
    </div>
    </template>

    <script>
    export default {
    props: [
    'pageShow'
    ],
    data() {
    return {
    list: []
    }
    },
    onInit() {
    this.$watch('pageShow', 'watchPageShowChange');
    },
    watchPageShowChange(newV, oldV) {
    if (newV && !this.list.length) { // 组件监听到 `page-show` 参数为 `true` 时请求数据
    this.getDataList('加载首屏');
    }
    },
    async getDataList(model) {
    // ... 请求数据
    this.list = [1,2,3,4];
    },
    }
    </script>

    我们来分析下上面代码相比官方 demo 的调整。

  • 首先进入 tab 页后所有 tab-content 下的组件都会渲染,但并不会请求接口,因为每一个组件都没有数据相当于空模板,这一改动经过实测,效果与效率均有所提升。

  • 当 tab 组件切换或者被点击时 currentindex 的值发生改变触发监听器 watchCurrentIndexChange 执行,并修改相应 tab-content 下组件(这里用 trend-tab 组件举例)的参数 page-show

  • trend-tab 组件通过监听到 pageShow 参数触发 watchPageShowChange方法。

  • pageShowtrue 时请求数据,渲染列表。

  • 当用户切换到下一个频道时当前 trend-tab 组件的 pageShow 值为 false 还可以做一些业务操作(比如视频频道 inline 播放的 video 可以暂停)。

  • 当用户再次滑回显示 trend-tab 组件时 page-show 参数为 true,但不满足 !this.list.length 条件,所以不会再次请求接口。

  • 1
    2
    3
    4
    5
    6
    onShow() {
    this.watchCurrentIndexChange(this.currentindex, null);
    },
    onHide() {
    this.watchCurrentIndexChange(null, this.currentindex);
    }

    在模板中 onShowonHide 直接调用 watchCurrentIndexChange 方法是出于上报 PV 的考虑,配合 watchCurrentIndexChange 方法内的判断条件过滤一些特殊情况,可以监听用户在任意一个频道的停留时长,以及在任一频道跳出时,通过触发页面的 onHide 事件进行统计上报的操作。

    PV_TRACK 的设计与思考

    PV_TRACK 是我们内部的针对快应用设计的一套统计 SDK,借鉴了轻粒子快应用统计的思路,再结合我们自身的特点。
    统计 PV 时需要发送 2 个请求:进入页面时发送 开始时间,离开时发送 离开时间,用于统计在线时长。

    startTime / endTime

    startTime 用于进入页面时发送开始时间。

    调用示例:

    1
    2
    3
    4
    5
    6
    onShow() {
    PV_TRACK.startTime('页面标识', this.auto_open_from);
    }
    onHide() {
    PV_TRACK.endTime('页面标识', this.auto_open_from);
    }
    参数 说明 类型 可选值 默认值
    page
    页面标识
    string
    argv
    业务数据扩展字段
    String / JSON

    page_show / page_hide

    page_show v2.0 新增 startTime 语法糖,传参可省略 auto_open_from 参数,用于进入页面时发送开始时间,适用于一个页面对应一个 PV 签的场景。

    相对应的,带有 tab 或一个页面因参数对应多个 PV 签的情况使用 startTimeendTime 编程的方式上报。

    调用示例:

    1
    2
    3
    4
    5
    6
    onShow() {
    PV_TRACK.page_show(this, { uid: '业务扩展字段' });
    }
    onHide() {
    PV_TRACK.page_show(this, { uid: '业务扩展字段' });
    }
    参数 说明 类型 可选值 默认值
    this
    当前页面 context
    Object
    argv
    业务数据扩展字段
    JSON

    V2 新增 全局调用方法 PV_TRACK,无需传递 auto_open_from 参数,方法会从 this 中获取。注:上报的 PV name 是当前页面的 router.name

    click

    用于发送点击事件统计。

    调用示例:

    1
    PV_TRACK.click(page, event);
    参数 说明 类型 可选值 默认值
    page
    页面标识
    String
    event
    事件标识
    String

    以上是我们 PV 中暴露的一些基础 API,数据统计对一个项目的长期发展和持续性优化非常重要。

    在我们的业务中有一些页面需要用到设备信息,但这些获取设备信息的方法多数都是异步的需要用户授权后才可获取,举个例子,之家快应用的车系综述页需要获取地理位置信息后,给出用户所在地的车源信息,此时该页面将会自行获取地理位置,但是 PV统计也获取了地理位置信息,导致程序中有两个不同位置的方法同时调用获取设备信息。

    这暴露出 3 个问题:

    1. 获取设备信息这种昂贵的操作没有被缓存和复用。
    2. 在部分手机厂商手机上会提示 2 次需要用户同意的授权提示框,或者仅提示一个且只有提示的回调会被执行(导致丢失逻辑)。
    3. 之家快应用的很多落地页支持网页唤起,这时用户作为首次访问,需要获取的设备信息 和 PV 统计方法中需要获取的设备信息重叠。

    解决方法:抽象获取设备信息公共方法,需要获取设备信息的需求依赖获取设备信息公共方法执行完毕后调用,解决共用数据保证调用顺序。

    实现获取设备信息 从 PV 方法中剥离

  • getDeviceData 方法返回获取设备基础信息的 Promise (包括 device.getInfodevice.getIdnetwork.getTypegeolocation.getLocation)。

  • deviceData 用户存储 getDeviceData 方法执行后返回的数据。

  • PV 方法中建立了发送队列,在 pv.setInfo(res) 方法被调用前不会上报统计,待调用 pv.setInfo(res) 后 pv 方法会连同传入的设备信息按照顺序上报。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    const { PV, getDeviceData } = Utils;
    const pv = new PV();
    injectRef.PV_TRACK = pv;
    let deviceData = null;
    let deviceCompleteArray = [];

    export default {
    onCreate() {
    // 快应用启动统计
    pv.send({
    cate: 'clt',
    event: 'kuai_app_launch_clt'
    });
    getDeviceData().then(res => {
    deviceData = res;
    console.log('################ 获取设备信息 #################');
    pv.setInfo(res);
    while (deviceCompleteArray.length) {
    const resolve = deviceCompleteArray.shift();
    resolve(res);
    }
    }).catch(err => {
    console.log(err);
    });
    },
    getDeviceInfo() {
    return new Promise((resolve, reject) => {
    if (!deviceData) {
    deviceCompleteArray.push(resolve);
    } else {
    resolve(deviceData);
    }
    });
    }
    };

    业务代码中实现等待设备信息逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    async onInit() {
    try {
    await this.$app.$def.getDeviceInfo();
    await this.getList();
    } catch (error) {
    console.log(error);
    }
    }
  • 业务代码中调用 this.$app.$def.getDeviceInfo() 返回一个 Promise,并插入 deviceCompleteArray 数组中。
  • 当获取所有设备信息时,会触发 deviceCompleteArray 数组中每一项的 resolve,实现阻塞代码的后续执行。
  • 1
    2
    3
    4
    while (deviceCompleteArray.length) {
    const resolve = deviceCompleteArray.shift();
    resolve(res);
    }

    至此实现多入口调用集中依赖同一个获取方法的功能。

    总结

    上面总结的一些小方法和思路应用到项目中可以提升开发效率,在项目中我们遵循开发规范可以保证快应用项目的可维护性和扩展性,未来我们将会持续打磨和优化代码,并更多的输出一些我们在项目开发过程中的经验。

    ]]>
    在过去一年里我所在的前端小组主要负责快应用项目的开发与维护,随着产品不断完善、业务复杂度提升以及对接场景的多样化,前端代码的管理和维护成为我们团队的核心挑战之一。本文总结了汽车之家快应用在开发过程中遇到一些问题以及思考。

    先介绍下]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/12/24/Using-Linux-server-to-build-uTorrent-Download-Server/ 2018-12-24T02:31:02.000Z 2018-12-24T02:37:49.697Z 推荐系统选择 Ubuntu。

    uTorrent官方下载:

    https://www.utorrent.com/intl/zh_cn/downloads/linux。

    下载 µTorrent Server for Ubuntu 12.04 解压到指定目录中。

    启动服务

    运行 ./utserver 文件

    ./utserver & 可以启动后台运行。 通过 jobs 查看后台正在运行的进程。

    管理下载任务

    在浏览器输入 http://localhost:8080/gui/ ,帐户为 admin 密码为空,就可以登录 uTorrent Web 控制台了。

    Ubuntu 安装 Nginx

    基于APT源安装

    1
    sudo apt-get install nginx

    Ubuntu安装之后的文件结构大致为:

    所有的配置文件都在/etc/nginx下,并且每个虚拟主机已经安排在了/etc/nginx/sites-available
    程序文件在/usr/sbin/nginx
    日志放在了/var/log/nginx
    并已经在/etc/init.d/下创建了启动脚本nginx
    默认的虚拟主机的目录设置在了/var/www/nginx-default (有的版本 默认的虚拟主机的目录设置在了/var/www, 请参考/etc/nginx/sites-available里的配置)

    启动nginx

    1
    2
    3
    sudo /etc/init.d/nginx start          # 启动

    sudo nginx -s reload # 重启

    搭建简单的文件下载服务器

    编辑 /etc/nginx/sites-available/default

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    server {  
        listen       80;         # 端口  
        server_name  localhost;   # 服务名  
        charset utf-8; # 避免中文乱码
        root    /dev/shm/update;  # 显示的根索引目录,注意这里要改成你自己的,目录要存在 

        location / {
            autoindex on;             # 开启索引功能  
            autoindex_exact_size off; # 关闭计算文件确切大小(单位bytes),只显示大概大小(单位kb、mb、gb)  
            autoindex_localtime on;   # 显示本机时间而非 GMT 时间
    if ($request_filename ~* ^.*?\.(txt|doc|pdf|rar|gz|zip|docx|exe|xlsx|ppt|pptx|zip)$){
    add_header Content-Disposition: 'attachment;';
    }
    }
    }

    Ubuntu下查看实时网络流量

    nethogs 可以查看实时进程网络占用。
    安装:sudo apt install nethogs
    查看网络状态: nethogs eth0
    即 nethogs + 网卡名称,双击table会出现备选网卡名称

    Detail:https://www.cnblogs.com/aaron-agu/p/5743141.html

    Ubuntu 查看磁盘占用情况

    1
    df -h

    zip 压缩和解压缩

    解压:

    1
    unzip FileName.zip

    压缩:

    1
    zip FileName.zip DirName

    zip -r example_service.zip example_service/

    -r 将指定的目录下的所有子目录以及文件一起处理

    Shadowsocks Python版一键安装脚本

    默认配置:

    服务器端口:自己设定(如不设定,默认从 9000-19999 之间随机生成)
    密码:自己设定(如不设定,默认为 teddysun.com)
    加密方式:自己设定(如不设定,默认为 aes-256-gcm)
    备注:脚本默认创建单用户配置文件,如需配置多用户,安装完毕后参照下面的教程示例手动修改配置文件后重启即可。

    Shadowsocks for Windows 客户端下载:

    https://github.com/shadowsocks/shadowsocks-windows/releases

    使用方法:

    使用root用户登录,运行以下命令:

    1
    2
    3
    wget --no-check-certificate -O shadowsocks.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks.sh
    chmod +x shadowsocks.sh
    ./shadowsocks.sh 2>&1 | tee shadowsocks.log

    安装完成后,脚本提示如下:

    1
    2
    3
    4
    5
    6
    7
    8
    Congratulations, Shadowsocks-python server install completed!
    Your Server IP :your_server_ip
    Your Server Port :your_server_port
    Your Password :your_password
    Your Encryption Method:your_encryption_method

    Welcome to visit: https://teddysun.com/342.html
    Enjoy it!

    卸载方法:

    使用root用户登录,运行以下命令:

    1
    ./shadowsocks.sh uninstall

    拓展阅读:
    Shadowsocks Python版一键安装脚本

    ]]>
    推荐系统选择 Ubuntu。

    uTorrent官方下载:

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/12/22/RPC-vs-REST-vs-GraphQL/ 2018-12-22T07:27:39.000Z 2018-12-22T07:48:36.950Z 写在前面

    最近2周的时间由于工作不忙,一直在看有关GraphQL的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC风格的接口。后来转做了前端开发,从实现接口者变成了调用接口者,接触最多的当属REST风格的接口。因此在这段学习GraphQL的过程中,并且也尝试使用它以全栈的角度做了一个小项目,在这个过程中,一直在思考它对比前两者在API设计的整体架构体系中的各个指标上,孰优孰劣。

    其实在使用和学习的过程中,有很多伟德伟诺官网都对比过它们的异同,但是大部分伟德伟诺官网并没有从一个相对客观的角度来对比,更多是为了突显一个的优点而刻意指出另外一个的缺点。这让我想到一句话,脱离业务情景谈bv伟德登录入口就是耍流氓。

    昨天订阅的GraphQL Weekly中推送的一个视频正好是讲关于它们这三者的,于是就点进去看了看,发现质量还是不错的,于是就想整理出来,分享给大家。

    原视频地址(油管地址,自备梯子):这里

    如果没有梯子的话直接看我整理的东西也可以,我觉的应该都覆盖到视频中所讲的重点内容了。

    当然,这些内容如果分开来讲,每一块内容所涉及的东西都够写一本书了,这里仅仅是简单归纳和整理,从宏观的角度来对比它们的异同,从而能够在日后面临bv伟德登录入口选型时,有一个更佳明确的决策方向。

    RPC

    先简单介绍下RPC,它是Remote Procedure Call(远程过程调用)的简称。一般基于RPC协议所设计的接口,是基于网络采用客户端/服务端的模式完成调用接口的。

    优点

  • 简单并且易于理解(面向开发者)
  • 轻量级的数据载体
  • 高性能
  • 缺点

  • 对于系统本身耦合性高
  • 因为RPC本身很简单、轻量,因此很容易造成 function explosion
  • 关于RPC的优点其实很好理解,就是因为它性能高同时又很简单,但是我认为这是对于接口提供者来讲的(因为它的高耦合性)。

    但是如果从接口调用者的角度来看,高耦合性就变成了缺点,因为高耦合意味着调用者必须要足够了解系统本身的实现才能够完成调用,比如:

  • 调用者需要知道所调用接口的函数名、参数格式、参数顺序、参数名称等等
  • 如果接口提供者(server)要对接口做出一些改变,很容易对接口调用者(client)造成breaking change(违背开闭原则)
  • 一般RPC所暴露接口仅仅会暴露函数的名称和参数等信息,对于函数之间的调用关系无法提供,这意味着调用者必须足够了解系统,从能够知道如何正确的调用这些接口,但是对于接口调用者往往不需要了解过多系统内部实现细节
  • 关于上面的第二点,为了减少breaking change,我之前实现接口的时候一般都会引入版本的概念,就是在暴露接口的方法名中加入版本号,一开始效果确实不错,但是随后就不知不觉的形成了function explosion,和视频中主讲人所举例的例子差不多,贴一下视频中的截图感受一波:

    图片描述

    REST

    当前REST风格的API架构方式已经成了主流解决方案了,相比较RPC,它的主要不同之处在于,它是对于资源(Resource)的模型化而非步骤(Procedure)。

    优点

  • 对于系统本身耦合性低,调用者不再需要了解接口内部处理和实现细节
  • 重复使用了一些 http 协议中的已定义好的部分状态动词,增强语义表现力
  • API可以随着时间而不断演进
  • 缺点

  • 缺少约束,缺少简单、统一的规范
  • 有时候 payload 会变的冗余(overload),有时候调用api会比较繁琐(chattiness)
  • 有时候需要发送多条请求已获取数据,在网络带宽较低的场景,往往会造成不好的影响
  • REST的优点基本解决了RPC中存在的问题,就是解耦,从而使得前后端分离成为可能。接口提供者在修改接口时,不容易造成breaking-change,接口调用者在调用接口时,往往面向数据模型编程,而省去了了解接口本身的时间成本。

    但是,我认为REST当前最大的问题在于虽然它利用http的动词约束了接口的暴露方式,同时增强了语义,但是却没有约束接口如何返回数据的最佳实践,总让人感觉只要是返回json格式的接口都可以称作REST。

    我在实际工作中,经常会遇到第二条缺点所指出的问题,就是接口返回的数据冗余度很高,但是却缺少我真正需要的数据,因此不得已只能调用其他接口或者直接和后端商议修改接口,并且这种问题会在web端和移动端共用一套接口中被放大。

    当前比较好的解决方案就是规范化返回数据的格式,比如json-schema或者自己制定的规范。

    GraphQL

    GraphQL是近来比较热门的一个bv伟德登录入口话题,相比REST和RPC,它汲取了两者的优点,即不面向资源,也不面向过程,而是面向数据查询(ask for exactly what you want)。

    同时GraphQL本身需要使用强类型的Schema来对数据模型进行定义,因此相比REST它的约束性更强。

    优点

  • 网络开销低,可以在单一请求中获取REST中使用多条请求获取的资源
  • 强类型Schema(约束意味着可以根据规范形成文档、IDE、错误提示等生态工具)
  • 特别适合状数据结构的业务场景(比如好友、流程、组织架构等系统)
  • 缺点

  • 本身的语法相比较REST和RPC均复杂一些
  • 实现方面需要配套 Caching 以解决性能瓶颈
  • 对于 API 的版本控制当前没有完善解决方案(社区的建议是不要使API版本化)
  • 仍然是新鲜事物,很多bv伟德登录入口细节仍然处于待验证状态
  • 鉴于GraphQL这两个星期我也仅仅是做了一些简单地使用和了解,仅仅说一下感受。

    首先值得肯定的是,在某些程度上确实解决了REST的缺点所带来的问题,同时配套社区建议的各种工具和库,相比使用REST风格,全栈开发体验上升一个台阶。

    但是这个看起来很好的东西为什么没有火起来呢?我觉的最主要的原因是因为GraphQL所带来的好处,大部分是对于接口调用者而言的,但是实现这部分的工作却需要接口提供者来完成。

    同时GraphQL的最佳实践场景应当是类似像Facebook这样的网站,业务逻辑模型是图状数据结构,比如社交。如果在一些业务逻辑模型相对简单的场景,使用GraphQL确实不如使用REST来得简单明了、直截了当。

    另外一方面是GraphQL的使用场景相当灵活,在我自己的调研项目中,我是把它当做一个类似ORM的框架来使用的,在别人的一些伟德伟诺官网中,会把它当做一个中间层来做渐进式开发和系统升级。这应当算是另外一个优点。

    到底用哪个

    下面根据要设计的API类型给予一些bv伟德登录入口选型建议。

    如果是Management API,这类API的特点如下:

  • 关注于对象与资源
  • 会有多种不同的客户端
  • 需要良好的可发现性和文档
  • 这种情景使用REST + JSON API可能会更好。

    如果是Command or Action API,这类API的特点如下:

  • 面向动作或者指令
  • 仅需要简单的交互
  • 这种情况使用RPC就足够了。

    如果是Internal Micro Services API,这类API的特点如下:

  • 消息密集型
  • 对系统性能有较高要求
  • 这种情景仍然建议使用RPC

    如果是Micro Services API,这类API的特点如下:

  • 消息密集型
  • 期望系统开销较低
  • 这种情景使用RPC或者REST均可。

    如果是Data or Mobile API,这类API的特点是:

  • 数据类型是具有图状的特点
  • 希望对于高延迟场景可以有更好的优化
  • 这种场景无疑GraphQL是最好的选择。

    写在最后

    提供一张表格来总览它们之间在不同指标下的表现:

    耦合性 约束性 复杂度 缓存 可发现性 版本控制
    RPC(Function)
    high
    medium
    low
    custom
    bad
    hard
    REST(Resource)
    low
    low
    low
    http
    good
    easy
    GraphQL(Query)
    medium
    high
    medium
    custom
    good
    ???

    最后引用人月神话中的观点no silver bullet,在bv伟德登录入口选型时需要具体情况具体分析,不过鉴于GraphQL的灵活性,把它与RPC和REST配置使用,也是不错的选择。

    本文转自:https://segmentfault.com/a/1190000013961872

    ]]> 写在前面

    最近2周的时间由于工作不忙,一直在看有关GraphQL的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC风格的接口。后来转做了前端开发,从实现接口]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/11/29/YouTube-dl-Download-YouTube-Video/ 2018-11-29T07:30:24.000Z 2019-01-18T18:59:41.066Z 官网地址

    youtube-dl官网:https://yt-dl.org/
    项目地址:https://github.com/rg3/youtube-dl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    Usage: youtube-dl [OPTIONS] URL [URL...]

    Options:
    通用选项:
    -h, --help 打印帮助文档
    --version 打印版本信息
    -U, --update 更新到最新版(需要权限)
    -i, --ignore-errors 遇到下载错误时跳过
    --abort-on-error 遇到下载错误时终止
    --dump-user-agent 显示当前使用的浏览器(User-agent)
    --list-extractors 列出所有的提取器(支持的网站)
    --extractor-descriptions 同上
    --force-generic-extractor 强制使用通用提取器下载
    --default-search PREFIX 使用此前缀补充不完整的URLs,例如:"ytsearch2 yt-dl" 从youtube搜索并下载两个关于yt-dl视频. 使用"auto"youtube-dl就会猜一个,一般效果等价于"ytsearch"("auto_warning"猜测时加入警告).我已知支持的PREFIX:ytsearch (youtube), ytsearchdate (youtube), yvsearch (yahoo videos), gvsearch (google videos)
    --ignore-config 不读取配置文件,当时用了全局配置文件/etc/youtube-dl.conf:不再读取 ~/.config/youtube-dl/config (%APPDATA%/youtube-dl/config.txt on Windows)
    --config-location PATH 使用指定路径下的配置文件
    --flat-playlist 列出列表视频但不下载
    --mark-watched 标记看过此视频 (YouTube only)
    --no-mark-watched 不标记看过此视频 (YouTube only)
    --no-color 打印到屏幕上的代码不带色

    网络选项:
    --proxy URL 使用HTTP/HTTPS/SOCKS协议的代理.如:socks5://127.0.0.1:1080/.
    --socket-timeout SECONDS 放弃连接前等待时间
    --source-address IP 绑定的客户端IP地址
    -4, --force-ipv4 所有连接通过IPv4
    -6, --force-ipv6 所有连接通过IPv6

    地理限制:
    --geo-verification-proxy URL 使用此代理地址测试一些有地理限制的地址
    --geo-bypass 绕过地理限制通过伪装X-Forwarded-For HTTP头部的客户端ip (实验)
    --no-geo-bypass 不 绕过地理限制通过伪装X-Forwarded-For HTTP头部的客户端ip (实验)
    --geo-bypass-country CODE 强制绕过地理限制通过提供准确的ISO 3166-2标准的国别代码(实验) 注:以上三个实验参数实测未成功

    视频选择:
    --playlist-start NUMBER 指定列表中开始下载的视频(默认为1)
    --playlist-end NUMBER 指定列表中结束的视频(默认为last)
    --playlist-items ITEM_SPEC 指定列表中要下载的视频项目编号.如:"--playlist-items 1,2,5,8""--playlist-items 1-3,7,10-13"
    --match-title REGEX 下载标题匹配的视频(正则表达式或区分大小写的字符串)
    --reject-title REGEX 跳过下载标题匹配的视频(正则表达式或区分大小写的字符串)
    --max-downloads NUMBER 下载NUMBER个视频后停止
    --min-filesize SIZE 不下载小于SIZE的视频(e.g. 50k or 44.6m)
    --max-filesize SIZE 不下载大于SIZE的视频(e.g. 50k or 44.6m)
    --date DATE 仅下载上传日期在指定日期的视频
    --datebefore DATE 仅下载上传日期在指定日期或之前的视频 (i.e. inclusive)
    --dateafter DATE 仅下载上传日期在指定日期或之后的视频 (i.e. inclusive)
    --min-views COUNT 不下载观影数小于指定值的视频
    --max-views COUNT 不下载观影数大于指定值的视频
    --match-filter FILTER 通用视频过滤器. Specify any key (see help for -o for a list of available keys) to match if the key is present, !key to check if the key is not present, key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against a number,key = 'LITERAL' (like "uploader = 'Mike Smith'", also works with !=) to match against a string literal and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the operator. For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike functionality is not available at the given service), but who also have a description, use --match-filter "like_count > 100 & dislike_count <? 50 & description" .
    --no-playlist 当视频链接到一个视频和一个播放列表时,仅下载视频
    --yes-playlist 当视频链接到一个视频和一个播放列表时,下载视频和播放列表
    --age-limit YEARS 下载合适上传年限的视频
    --download-archive FILE 仅下载档案文件中未列出的影片,已下载的记录ID
    --include-ads 同时下载广告(实验)

    下载选项:
    -r, --limit-rate RATE 最大bps (e.g. 50K or 4.2M)
    -R, --retries RETRIES 重试次数 (默认10), or "infinite".
    --fragment-retries RETRIES 一个分段的最大重试次数(default is 10), or "infinite" (DASH, hlsnative and ISM)
    --skip-unavailable-fragments 跳过不可用分段(DASH, hlsnative and ISM)
    --abort-on-unavailable-fragment 放弃某个分段当不可获取时
    --keep-fragments 下载完成后,将下载的片段保存在磁盘上; 片段默认被删除
    --buffer-size SIZE 设置缓冲区大小buffer (e.g. 1024 or 16K) (default is 1024)
    --no-resize-buffer 不自动调整缓冲区大小.默认情况下自动调整
    --playlist-reverse 以相反的顺序下载播放列表视频
    --playlist-random 以随机的顺序下载播放列表视频
    --xattr-set-filesize Set file xattribute ytdl.filesize with expected file size (experimental)
    --hls-prefer-native 使用本机默认HLS下载器而不是ffmpeg
    --hls-prefer-ffmpeg 使用ffmpeg而不是本机HLS下载器
    --hls-use-mpegts 使用TS流容器来存放HLS视频,一些高级播放器允许在下载的同时播放视频
    --external-downloader COMMAND 使用指定的第三方下载工具,当前支持:aria2c,avconv,axel,curl,ffmpeg,httpie,wget
    --external-downloader-args ARGS 给第三方下载工具指定参数,如:--external-downloader aria2c --external-downloader-args -j8

    文件系统选项:
    -a, --batch-file FILE 文件中包含需要下载的URL
    --id 仅使用文件名中的视频ID
    -o, --output TEMPLATE Output filename template, see the "OUTPUT TEMPLATE" for all the info
    --autonumber-start NUMBER 指定%(autonumber)s的起始值(默认为1)
    --restrict-filenames 将文件名限制为ASCII字符,并避免文件名中的“&”和空格
    -w, --no-overwrites 不要覆盖文件
    -c, --continue 强制恢复部分下载的文件。 默认情况下,youtube-dl仅在可能时将恢复下载。
    --no-continue 不要恢复部分下载的文件(从头开始重新启动)
    --no-part 不使用.part文件 - 直接写入输出文件
    --no-mtime 不使用Last-modified header来设置文件最后修改时间
    --write-description 将视频描述写入.description文件
    --write-info-json 将视频元数据写入.info.json文件
    --write-annotations 将视频注释写入.annotations.xml文件
    --load-info-json FILE 包含视频信息的JSON文件(使用“--write-info-json”选项创建)
    --cookies FILE 文件从中读取Cookie(经测试,export cookies插件可以使用,但firebug导出的cookies导致错误,chrome下请用cookies.txt)注意:不同平台windows、LinuxOSX之间需要转换CE LF才能使用!
    --cache-dir DIR 文件存储位置。youtube-dl需要永久保存一些下载的信息。默认为$XDG_CACHE_HOME/youtube-dl或/.cache/youtube-dl。目前,只有YouTube播放器文件(对于具有模糊签名的视频)进行缓存,但可能会发生变化。
    --no-cache-dir 不用缓存
    --rm-cache-dir 删除所有缓存文件

    缩略图:
    --write-thumbnail 把缩略图写入硬盘
    --write-all-thumbnails 将所有缩略图写入磁盘
    --list-thumbnails 列出所有可用的缩略图格式

    详细/模拟选项:
    -q, --quiet 激活退出模式
    --no-warnings 忽略警告
    -s, --simulate 不下载不存储任何文件到硬盘,模拟下载模式
    --skip-download 不下载视频
    -g, --get-url 模拟下载获取视频直连
    -e, --get-title 模拟下载获取标题
    --get-id 模拟下载获取id
    --get-thumbnail 模拟下载获取缩略图URL
    --get-description 模拟下载获取视频描述
    --get-duration 模拟下载获取视频长度
    --get-filename 模拟下载获取输出视频文件名
    --get-format 模拟下载获取输出视频格式
    -j, --dump-json 模拟下载获取JSON information.
    -J, --dump-single-json 模拟下载获取每条命令行参数的JSON information.如果是个播放列表,就获取整个播放列表的JSON
    --print-json 下载的同时获取视频信息的JSON
    --newline 进度条在新行输出
    --no-progress 不打印进度条
    --console-title 在控制台标题栏显示进度
    -v, --verbose 打印各种调试信息
    --dump-pages 打印下载下来的使用base64编码的页面来调试问题(非常冗长)
    --write-pages 将下载的中间页以文件的形式写入当前目录中以调试问题
    --print-traffic 显示发送和读取HTTP流量
    -C, --call-home 联系youtube-dl服务器进行调试
    --no-call-home 不联系youtube-dl服务器进行调试

    解决方法:
    --encoding ENCODING 强制指定编码(实验)
    --no-check-certificate 禁止HTTPS证书验证
    --prefer-insecure 使用未加密的连接来检索有关视频的信息(目前仅支持YouTube)
    --user-agent UA 指定user agent
    --referer URL 指定自定义的referer,仅限视频来源于同一网站
    --add-header FIELD:VALUE 指定一个自定义值的HTTP头文件,使用分号分割,可以多次使用此选项
    --bidi-workaround 围绕缺少双向文本支持的终端工作。需要在PATH中有bidiv或fribidi可执行文件
    --sleep-interval SECONDS 在每次下载之前休眠的秒数,或者每次下载之前的随机睡眠的范围的下限(最小可能的睡眠秒数)与-max-sleep-interval一起使用。
    --max-sleep-interval SECONDS 每次下载前随机睡眠范围的上限(最大可能睡眠秒数)。只能与--min-sleep-interval一起使用。

    视频格式选项:
    -f, --format FORMAT 视频格式代码,查看"FORMAT SELECTION"获取所有信息
    --all-formats 获取所有视频格式
    --prefer-free-formats 开源的视频格式优先,除非有特定的请求
    -F, --list-formats 列出请求视频的所有可用格式
    --youtube-skip-dash-manifest 不要下载关于YouTube视频的DASH清单和相关数据
    --merge-output-format FORMAT 如果需要合并(例如bestvideo + bestaudio),则输出到给定的容器格式。mkv,mp4,ogg,webm,flv之一。如果不需要合并,则忽略

    字幕选项:
    --write-sub 下载字幕文件
    --write-auto-sub 下载自动生成的字幕文件 (YouTube only)
    --all-subs 下载所有可用的字幕
    --list-subs 列出所有字幕
    --sub-format FORMAT 字幕格式,接受格式偏好,如:"srt" or "ass/srt/best"
    --sub-lang LANGS 要下载的字幕的语言(可选)用逗号分隔,请使用--list-subs表示可用的语言标签

    验证选项:
    -u, --username USERNAME 使用ID登录
    -p, --password PASSWORD 账户密码,如果此选项未使用,youtube-dl将交互式地询问。
    -2, --twofactor TWOFACTOR 双因素认证码
    -n, --netrc 使用.netrc认证数据
    --video-password PASSWORD 视频密码(vimeo, smotri, youku)

    Adobe Pass Options:
    --ap-mso MSO Adobe Pass多系统运营商(电视提供商)标识符,使用--ap-list-mso列出可用的MSO
    --ap-username USERNAME MSO账号登录
    --ap-password PASSWORD 账户密码,如果此选项未使用,youtube-dl将交互式地询问。
    --ap-list-mso 列出所有支持的MSO

    后处理选项:
    -x, --extract-audio 将视频文件转换为纯音频文件(需要ffmpeg或avconv和ffprobe或avprobe)
    --audio-format FORMAT 指定音频格式: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "best" by default;-x存在时无效
    --audio-quality QUALITY 指定ffmpeg/avconv音频质量,为VBR插入一个0(best)-9(worse)的值(默认5),或者指定比特率
    --recode-video FORMAT 必要时将视频转码为其他格式(当前支持: mp4|flv|ogg|webm|mkv|avi)
    --postprocessor-args ARGS 给后处理器提供这些参数
    -k, --keep-video 视频文件在后处理后保存在磁盘上; 该视频默认被删除
    --no-post-overwrites 不要覆盖后处理文件; 默认情况下,后处理文件将被覆盖
    --embed-subs 在视频中嵌入字幕(仅适用于mp4,webm和mkv视频)
    --embed-thumbnail 将缩略图嵌入音频作为封面艺术
    --add-metadata 将元数据写入视频文件
    --metadata-from-title FORMAT 从视频标题中解析附加元数据,如歌曲标题/艺术家。格式语法和--output相似.也可以使用带有命名捕获组的正则表达式。解析的参数替换现有值。Example: --metadata-from-title "%(artist)s - %(title)s" matches a title like "Coldplay - Paradise". Example (regex): --metadata-from-title "(?P<artist>.+?) - (?P<title>.+)"
    --xattrs 将元数据写入视频文件的xattrs(使用dublin core 和 xdg标准)
    --fixup POLICY 自动更正文件的已知故障。never(不做警告), warn(只发出警告), detect_or_warn (默认;如果可以的话修复文件,否则警告)
    --prefer-avconv 后处理时相较ffmpeg偏向于avconv
    --prefer-ffmpeg 后处理优先使用ffmpeg
    --ffmpeg-location PATH ffmpeg/avconv程序位置;PATH为二进制所在文件夹或者目录.
    --exec CMD 在下载后对文件执行命令,类似于find -exec语法.示例:--exec'adb push {} /sdcard/Music/ && rm {}'
    --convert-subs FORMAT 转换字幕格式(当前支持: srt|ass|vtt)

    2.安装python

    youtube-dl使用Python编写的工具,所以系统里没有Python的话,先安装Python。youtube-dl需要Python 2.6以上的版本。

    Ubuntu:

    1
    sudo apt-get install python2.7

    CentOS:(一般CentOS6以上会自带Python,如果没有Python的话,就按照以下方法安装下)

    1
    yum install -y python python-devel

    3.下载youtube-dl

    下载地址:https://github.com/rg3/youtube-dl

    Linux, macOS:

    1
    2
    sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
    sudo chmod a+rx /usr/local/bin/youtube-dl

    如果没有 curl 可以使用 wget

    1
    2
    sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
    sudo chmod a+rx /usr/local/bin/youtube-dl

    4.安装ffmpeg

    FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec

    1.升级系统 (CentOS)

    1
    2
    3
    sudo yum install epel-release -y
    sudo yum update -y
    sudo shutdown -r now

    2.安装Nux Dextop Yum 源

    由于CentOS没有官方FFmpeg rpm软件包。但是,我们可以使用第三方YUM源(Nux Dextop)完成此工作。

    1) CentOS 7

    1
    2
    sudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
    sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm

    2) CentOS 6

    1
    2
    sudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
    sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm

    3.安装FFmpeg 和 FFmpeg开发包

    1
    sudo yum install ffmpeg ffmpeg-devel -y

    4.测试是否安装成功

    1
    ffmpeg

    5.如果你想了解更多关于FFmpeg使用方面的资料,可以输入:

    1
    ffmpeg -h

    例子:
    使用FFmpeg将mp3转为ogg

    1
    ffmpeg -i MLKDream_64kb.mp3 -c:a libvorbis -q:a 4 MLKDream_64kb.ogg

    使用FFmpeg将flv转为mp4

    1
    ffmpeg -i beeen.flv -y -vcodec copy -acodec copy beeen.mp4

    Ubuntu 安装 ffmpeg:

    1、添加源。

    1
    sudo add-apt-repository ppa:djcj/hybrid

    2、更新源。

    1
    sudo apt-get update

    3、下载安装。

    1
    sudo apt-get install ffmpeg

    5.用法

    使用帮助命令查看其用法:

    1
    youtube-dl -h

    一些常用的参数:

    youtube-dl –list-extractors #查看支持网站列表
    youtube-dl -U #程序升级
    youtube-dl –get-format URL #获取视频格式
    youtube-dl -F URL #获取所有格式(目前仅支持YouTube),例如:

    1
    youtube-dl -F http://www.youtube.com/watch?v=n-BXNXvTvV4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    [youtube] Setting language
    [youtube] n-BXNXvTvV4: Downloading video webpage
    [youtube] n-BXNXvTvV4: Downloading video info webpage
    [youtube] n-BXNXvTvV4: Extracting video information
    Available formats:
    : mp4 [1080x1920]
    : webm [1080x1920]
    : mp4 [720x1280]
    : webm [720x1280]
    : flv [480x854]
    : webm [480x854]
    : flv [360x640]
    : mp4 [360x640]
    : webm [360x640]
    : flv [240x400]
    : 3gp [240x320]
    : 3gp [144x176]
    : mp4 [1080p] (DASH Video)
    : mp4 [720p] (DASH Video)
    : mp4 [480p] (DASH Video)
    : mp4 [360p] (DASH Video)
    : mp4 [240p] (DASH Video)
    : mp4 [192p] (DASH Video)
    : mp4 [256k] (DASH Audio)
    : webm [256k] (DASH Audio)
    : mp4 [128k] (DASH Audio)
    : webm [128k] (DASH Audio)
    : mp4 [48k] (DASH Audio)

    youtube-dl -f format URL #下载指定格式的视频,这里以下载1080p原画质量的视频格式为例:

    1
    youtube-dl -f 137 http://www.youtube.com/watch?v=n-BXNXvTvV4

    6.代理

    推荐使用SS代理。非全局模式,请在命令后面加 --proxy 'socks5://127.0.0.1:1080' (1080是端口号,我的端口是1080,您的端口请按照您自己设置的填写)

    例如: youtube-dl --proxy 'socks5://127.0.0.1:1080' [URL]

    7.下载YouTube视频

    1) 查看视频所有类型,只看不下载
    youtube-dl -F [url]
    或者
    youtube-dl –list-formats [url]

    这是一个列清单参数,执行后并不会下载视频,但能知道这个目标视频都有哪些格式存在,这样就可以有选择的下载啦!

    8.关于音频和视频的合并

    下载指定质量的视频和音频并自动合并
    youtube-dl -f [format code] [url]

    通过上一步获取到了所有视频格式的清单,最左边一列就是编号对应着不同的格式.
    由于YouTube的1080p及以上的分辨率都是音视频分离的,所以我们需要分别下载视频和音频,可以使用137+140这样的组合.
    如果系统中安装了ffmpeg的话, youtube-dl 会自动合并下下好的视频和音频, 然后自动删除单独的音视频文件

    9.下载字幕

    youtubd-dl –write-sub [url] //这样会下载一个vtt格式的英文字幕和mkv格式的1080p视频下来
    youtube-dl –write-sub –skip-download [url] //下载单独的vtt字幕文件,而不会下载视频
    youtube-dl –write-sub –all-subs [url] //下载所有语言的字幕(如果有的话)
    youtube-dl –write-auto-sub [url] //下载自动生成的字幕(YouTube only)

    youtube-dl –list-subs [url] //列出所有可用字幕

    10.关于youtube的字幕接口

    获取所有语言的字幕列表:’http://video.google.com/timedtext?hl=en&v=hRfHcp2GjVI&type=list
    获取字幕,在视频有官方字幕的情况下:’http://www.youtube.com/api/timedtext?lang=%s&v=%s&name=%s

    「上传字幕」和「机器字幕」是不互相「兼容」的,有「上传字幕」的视频是没有「机器字幕」的,当然一个视频也可能「上传字幕」和「机器字幕」都没有

    11.下载视频列表

    youtube-dl -f [format code] [palylist_url] //这种方式可以下载制定清晰度的mp4视频
    youtube-dl [playlist_url] //下载视频列表,这种方式下载的视频可能是mkv格式或者webm格式
    youtube-dl -cit [playlist_url] //下载视频列表,这种方式下载的视频可能是mkv格式或者webm格式
    youtube-dl –yes-playlist [url] //当链接为视频列表,则下载该列表视频,跟上面的一样,可能是mkv或者webm格式

    下载 1080P 视频:

    1
    youtube-dl -f 137+140 https://www.youtube.com/watch?v=Zaem7Ok3PjE

    下载画质最好的mp4命令:(支持频道下载)

    1
    youtube-dl -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio' --merge-output-format mp4 https://www.youtube.com/watch?v=Zaem7Ok3PjE

    跳过已下载文件

    使用选项 --download-archive FILE youtube-dl 读取并添加到不再下载的文件列表。每次成功下载文件时,该视频ID都会添加到FILE。

    您可以按如下方式使用它:

    1
    youtube-dl --download-archive downloaded.txt --no-post-overwrites -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio' --merge-output-format mp4 https://www.youtube.com/watch?v=Zaem7Ok3PjE

    他会记录当前频道中已经下载过的视频,这样方便你随时删除已经下载过的视频防止重新下载。

    获取youtube 频道视频列表:

    1
    JSON.stringify($$('#thumbnail').map(item => item.href))

    本文转自: https://www.cnblogs.com/wpjamer/p/7392592.html

    ]]>
    官网地址

    youtube-dl官网:https://yt-dl.org/
    项目地址: <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/10/19/Use-JWT-to-make-your-RESTful-API-more-secure./ 2018-10-19T09:26:54.000Z 2018-10-19T09:37:54.863Z 传统的 cookie-session 机制可以保证的接口安全,在没有通过认证的情况下会跳转至登入界面或者调用失败。

    在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。

    更多的形式下采用的基于 Token 的验证机制,JWT 本质的也是一种 Token,但是其中又有些许不同。

    什么是 JWT ?

    JWT 及时 JSON Web Token,它是基于 RFC 7519 所定义的一种在各个系统中传递紧凑和自包含的 JSON 数据形式。

  • 紧凑 (Compact) :由于传送的数据小,JWT 可以通过GET、POST 和 放在 HTTP 的 header 中,同时也是因为小也能传送的更快。
  • 自包含 (self-contained) : Payload 中能够包含用户的信息,避免数据库的查询。
  • JSON Web Token 由三部分组成使用 . 分割开:

  • Header
  • Payload
  • Signature
  • 一个 JWT 形式上类似于下面的样子:

    xxxxx.yyyy.zzzz

    Header 一般由两个部分组成:

  • alg
  • typ
  • alg 是是所使用的 hash 算法例如 HMAC SHA256 或 RSA,typ 是 Token 的类型自然就是 JWT。

    1
    2
    3
    4
    {
    "alg": "HS256",
    "typ": "JWT"
    }

    然后使用 Base64Url 编码成第一部分。

    1
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>

    Payload

    这一部分是 JWT 主要的信息存储部分,其中包含了许多种的声明(claims)。

    Claims 的实体一般包含用户和一些元数据,这些 claims 分成三种类型:reserved, public, 和 private claims。

    -(保留声明)reserved claims :预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject), aud(audience) 等。

    这里都使用三个字母的原因是保证 JWT 的紧凑

    -(公有声明)public claims : 这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。

    -(私有声明)private claims : 这个部分是共享被认定信息中自定义部分。

    一个 Pyload 可以是这样子的:

    1
    2
    3
    4
    5
    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }

    这部分同样使用 Base64Url 编码成第二部分。

    1
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>

    Signature

    在创建该部分时候你应该已经有了 编码后的 Header 和 Payload 还需要一个一个秘钥,这个加密的算法应该 Header 中指定。

    一个使用 HMAC SHA256 的例子如下:

    1
    2
    3
    4
    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

    这个 signature 是用来验证发送者的 JWT 的同时也能确保在期间不被篡改。

    所以,做后你的一个完整的 JWT 应该是如下形式:

    1
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

    注意被 . 分割开的三个部分

    JSON Web Token 的工作流程

    在用户使用证书或者账号密码登入的时候一个 JSON Web Token 将会返回,同时可以把这个 JWT 存储在local storage、或者 cookie 中,用来替代传统的在服务器端创建一个 session 返回一个 cookie。

    当用户想要使用受保护的路由时候,应该要在请求得时候带上 JWT ,一般的是在 header 的 Authorization 使用 Bearer 的形式,一个包含的 JWT 的请求头的 Authorization 如下:

    1
    Authorization: Bearer <token>

    这是一中无状态的认证机制,用户的状态从来不会存在服务端,在访问受保护的路由时候回校验 HTTP header 中 Authorization 的 JWT,同时 JWT 是会带上一些必要的信息,不需要多次的查询数据库。

    这种无状态的操作可以充分的使用数据的 APIs,甚至是在下游服务上使用,这些 APIs 和哪服务器没有关系,因此,由于没有 cookie 的存在,所以在不存在跨域(CORS, Cross-Origin Resource Sharing)的问题。

    在 Flask 和 Express 中使用 JSON Web Token

    JWT 在各个 Web 框架中都有 JWT 的包可以直接使用,下面使用 Flask 和 Express 作为例子演示。

    下面会使用 httpie 作为演示工具:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    HTTPie: HTTP client, a user-friendly cURL replacement.

    - Download a URL to a file:
    http -d example.org

    - Send form-encoded data:
    http -f example.org name="bob" profile-picture@"bob.png"

    - Send JSON object:
    http example.org name="bob"

    - Specify an HTTP method:
    http HEAD example.org

    - Include an extra header:
    http example.org X-MyHeader:123

    - Pass a user name and password for server authentication:
    http -a username:password example.org

    - Specify raw request body via stdin:
    cat data.txt | http PUT example.org

    Flask 中使用 JSON Web Token

    这里的演示是 Flask-JWT 的 Quickstart内容。

    安装必要的软件包:

    1
    2
    pip install flask
    pip install Flask-JWT

    一个简单的 DEMO:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    from flask import Flask
    from flask_jwt import JWT, jwt_required, current_identity
    from werkzeug.security import safe_str_cmp

    class User(object):
    def __init__(self, id, username, password):
    self.id = id
    self.username = username
    self.password = password

    def __str__(self):
    return "User(id="%s")" % self.id

    users = [
    User(1, "user1", "abcxyz"),
    User(2, "user2", "abcxyz"),
    ]

    username_table = {u.username: u for u in users}
    userid_table = {u.id: u for u in users}

    def authenticate(username, password):
    user = username_table.get(username, None)
    if user and safe_str_cmp(user.password.encode("utf-8"), password.encode("utf-8")):
    return user

    def identity(payload):
    user_id = payload["identity"]
    return userid_table.get(user_id, None)

    app = Flask(__name__)
    app.debug = True
    app.config["SECRET_KEY"] = "super-secret"

    jwt = JWT(app, authenticate, identity)

    @app.route("/protected")
    @jwt_required()
    def protected():
    return "%s" % current_identity

    if __name__ == "__main__":
    app.run()

    首先需要获取用户的 JWT:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    % http POST http://127.0.0.1:5000/auth username="user1" password="abcxyz"             ~
    HTTP/1.0 200 OK
    Content-Length: 193
    Content-Type: application/json
    Date: Sun, 21 Aug 2016 03:48:41 GMT
    Server: Werkzeug/0.11.10 Python/2.7.10

    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"
    }

    使用 @jwt_required() 装饰器来保护你的 API

    1
    2
    3
    4
    @app.route("/protected")
    @jwt_required()
    def protected():
    return "%s" % current_identity

    这时候你需要在 HTTP 的 header 中使用 Authorization: JWT <token> 才能获取数据

    1
    2
    3
    4
    5
    6
    7
    8
    % http http://127.0.0.1:5000/protected Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"
    HTTP/1.0 200 OK
    Content-Length: 12
    Content-Type: text/html; charset=utf-8
    Date: Sun, 21 Aug 2016 03:51:20 GMT
    Server: Werkzeug/0.11.10 Python/2.7.10

    User(id="1")

    不带 JWT 的时候会返回如下信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    % http http://127.0.0.1:5000/protected                                                ~
    HTTP/1.0 401 UNAUTHORIZED
    Content-Length: 125
    Content-Type: application/json
    Date: Sun, 21 Aug 2016 03:49:51 GMT
    Server: Werkzeug/0.11.10 Python/2.7.10
    WWW-Authenticate: JWT realm="Login Required"

    {
    "description": "Request does not contain an access token",
    "error": "Authorization Required",
    "status_code": 401
    }

    Express 中使用 JSON Web Token

    Auth0 提供了 express-jwt 这个包,在 express 可以很容易的集成。

    1
    2
    3
    4
    5
    npm install express --save
    npm install express-jwt --save
    npm install body-parser --save
    npm install jsonwebtoken --save
    npm install shortid --save

    本例子中只是最简单的使用方法,更多使用方法参看 express-jwt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    const express = require("express");
    const expressJwt = require("express-jwt");
    const bodyParser = require("body-parser");
    const jwt = require("jsonwebtoken");
    const shortid = require("shortid");

    const app = express();

    app.use(bodyParser.json());
    app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));
    app.use(function (err, req, res, next) {
    if (err.name === "UnauthorizedError") {
    res.status(401).send("invalid token");
    }
    });


    app.post("/login", function(req, res) {
    const username = req.body.username;
    const password = req.body.password;

    if (!username) {
    return res.status(400).send("username require");
    }
    if (!password) {
    return res.status(400).send("password require");
    }

    if (username != "admin" && password != "password") {
    return res.status(401).send("invaild password");
    }

    const authToken = jwt.sign({username: username}, "secret");
    res.status(200).json({token: authToken});

    });

    app.post("/user", function(req, res) {
    const username = req.body.username;
    const password = req.body.password;
    const country = req.body.country;
    const age = req.body.age;

    if (!username) {
    return res.status(400).send("username require");
    }
    if (!password) {
    return res.status(400).send("password require");
    }
    if (!country) {
    return res.status(400).send("countryrequire");
    }
    if (!age) {
    return res.status(400).send("age require");
    }

    res.status(200).json({
    id: shortid.generate(),
    username: username,
    country: country,
    age: age
    })
    })

    app.listen(3000);

    express-jwt 作为 express 的一个中间件,需要设置 secret 作为秘钥,unless 可以排除某个接口。

    默认的情况下,解析 JWT 失败会抛出异常,可以通过以下设置来处理该异常。

    1
    2
    3
    4
    5
    6
    app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));
    app.use(function (err, req, res, next) {
    if (err.name === "UnauthorizedError") {
    res.status(401).send("invalid token");
    }
    });

    /login 忽略的 JWT 认证,通过这个接口获取某个用户的 JWT

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    % http POST http://localhost:3000/login username="admin" password="password" country="CN" age=22  
    HTTP/1.1 200 OK
    Connection: keep-alive
    Content-Length: 143
    Content-Type: application/json; charset=utf-8
    Date: Sun, 21 Aug 2016 06:57:42 GMT
    ETag: W/"8f-iMzAS1K5StDQgtNnVSvqtQ"
    X-Powered-By: Express

    {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M"
    }

    不使用 JWT 的时候

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    % http POST http://localhost:3000/user username="hexiangyu" password="password"       ~
    HTTP/1.1 401 Unauthorized
    Connection: keep-alive
    Content-Length: 13
    Content-Type: text/html; charset=utf-8
    Date: Sun, 21 Aug 2016 07:00:02 GMT
    ETag: W/"d-j0viHsPPu6FaNJ6cXoiFeQ"
    X-Powered-By: Express

    invalid token

    使用 JWT 就可以成功调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    % http POST http://localhost:3000/user Authorization:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M" username="hexiangyu" password="password" country="CN" age=22
    HTTP/1.1 200 OK
    Connection: keep-alive
    Content-Length: 66
    Content-Type: application/json; charset=utf-8
    Date: Sun, 21 Aug 2016 07:04:34 GMT
    ETag: W/"42-YnGYuyDLxpVUexEGEcQj1g"
    X-Powered-By: Express

    {
    "age": "22",
    "country": "CN",
    "id": "r1sFMCUc",
    "username": "hexiangyu"
    }

    Reference

    JSON Web Token Introduction
    IANA JSON Web Token
    Flask-JWT
    express-jwt

    本文转自链接:http://blog.zhengxiaowai.cc/post/safe-jwt-restful-api.html

    ]]> 传统的 cookie-session 机制可以保证的接口安全,在没有通过认证的情况下会跳转至登入界面或者调用失败。

    在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。

    更多的形]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/07/19/Traditional-business-fusion-Vue-components/ 2018-07-19T10:14:59.000Z 2018-07-19T10:20:13.030Z 在不同时期不同业务场景下,前端对框架的选择也不近相同,本文将会分享PC伟德伟诺官网页中的评论系统的开发过程,如何在传统业务中使用 vue 重构(开发)一个业务组件,这与直接使用 vue 开发页面有什么不同,同时也会顺便介绍下 vue-cli 3.0 版本的新增的一些功能对开发独立组件/库带来的便利。

    问题分析:

  • vue 组件以何种方式在 jQuery 框架下运行。
  • 组件与页面其他功能之间如何通信。
  • 使用 vue 组件是否会带来加载数据量的提升,如何解决。
  • vue 组件是如何挂载到 jQuery 页面中的

    如下面代码,提供一个id作为挂载点,vue将组件直接渲染到 app 对应的div上,下面我们将介绍下具体是如何操作的。

    1
    2
    3
    4
    5
    6
    7
    8
    <div id="app"></div>

    <script src="https://cdn.bootcss.com/vue/2.5.15/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
    <script src="./commentSystem.umd.min.js"></script>
    <script>
    new Vue({render: h => h(window["commentSystem"])}).$mount('#app');
    </script>

    vue-cli 3.0 提供了构建目标的选项,可以将一个组件打包成一个UMD格式的库对外暴漏。

    运行:

    1
    vue-cli-service build --target lib --name myLib [entry]

    结果:

    1
    2
    3
    4
    5
    6
    File                     Size                     Gzipped

    dist/myLib.umd.min.js 13.28 kb 8.42 kb
    dist/myLib.umd.js 20.95 kb 10.22 kb
    dist/myLib.common.js 20.57 kb 10.09 kb
    dist/myLib.css 0.33 kb 0.23 kb

    也可以使用 vue ui 提供的图形界面完成打包:

    enter image description here

    vue-cli 3.0 提供的构建目标的功能非常实用,在 2.0 版本时我们如果想发布一个组件需要自己手动修改 webpack 配置。对这部分有兴趣的可以移步如何在 npm 上发布你的 vue 组件

    组件内部通信 vuex 状态管理。

    在评论模块中,组件需要和其他组件的交互状态,通过 propsemit 传递状态过于繁琐,引入状态管理必不可少。
    为了便于打包库组件状态管理相对于官方示例来说会有一些小的调整。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // CommentSystem.vue 文件
    import Vue from 'vue';
    import Vuex, { mapState } from 'vuex';
    Vue.use(Vuex);

    export default {
    name: 'CommentSystemComponent',
    store: store
    }

    官方示例把 store 引入在 app.vue 中,因为我们打包的根节点在 CommentSystem.vue 组件上,所以 storeCommentSystem.vue 中引入。

    组件与页面其他功能之间如何通信。

    1. 配置参数传入。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script>
    // http://www.autohome.com.cn/comment/Articlecomment.aspx?articleid=899378&replyid=61166916
    window.ahCSConfig = {
    _appid: 'cms', // 区分来源,例如 pc/m/app
    appid: 1, // 业务线 ID
    contentid: 595293, // 内容 ID
    reply: {
    tips: '汽车之家温馨提示您:留言中请不要恶意攻击国家、其他用户及工作人员,不要发布任何广告性质的留言,我们会在第一时间永久封杀违反以上规定的ID。流畅沟通、观点鲜明、善意提醒是我们推崇的良性留言氛围。',
    },
    comment: {
    tips: '留言中所有与交易和团购相关的信息均为虚假信息,与汽车之家无关,请勿相信。'
    }
    }
    </script>

    <script src="./dist/comment-system.umd.min.js"></script>

    通过全局变量传入配置参数,注意脚本顺序。

    1. 页面向组件发送消息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var rootComponent = new Vue({
    render: h => h(window["comment-system"])
    }).$mount('#app');
    ahCSConfig.$comment = rootComponent.$children[0].$children[0];
    document.querySelector('#btn').addEventListener('click', function() {
    ahCSConfig.$comment.$store.commit('SET_PICTURE_DIALOG_DATA', {
    visible: true,
    info: 'http://www3.autoimg.cn/newsdfs/g24/M02/26/26/80x0_0_autohomecar__ChcCL1p5uC-AFrOcABBbFVoFSGA574.jpg'
    });
    }, false);

    页面向组件通信,通过获取组件对象并commit相关消息来实现。

    1. 组件向页面发送消息。
    1
    2
    3
    4
    // config 配置中配置项
    event: ({type, payload}) => {
    console.log('组件对外暴漏commit方法', type, payload);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // store.js 中对应触发逻辑
    const ahCSConfig = window.ahCSConfig;
    const eventInterceptors = store => {
    // 当 store 初始化后调用
    store.subscribe((mutation, state) => {
    ahCSConfig && ahCSConfig.event && ahCSConfig.event(mutation);
    });
    };

    export default new Vuex.Store({
    plugins: [eventInterceptors]
    })

    通过配置项中添加 event 方法,vue组件内每次 commit 都会调用 event 方法并传入 typepayload

    关于使用外部组件的探讨(element-ui)

    在评论系统中会用大 DialogMessageButtonPagination 等组件。这些组件我们可以选择一个UI库引入,也可以自行开发。

    element-ui 支持按需引入:

    借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

    首先,安装 babel-plugin-component

    1
    npm install babel-plugin-component -D

    然后,将 .babelrc 修改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "presets": [["es2015", { "modules": false }]],
    "plugins": [
    [
    "component",
    {
    "libraryName": "element-ui",
    "styleLibraryName": "theme-chalk"
    }
    ]
    ]
    }

    接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import Vue from 'vue';
    import { Button, Select } from 'element-ui';
    import App from './App.vue';

    Vue.component(Button.name, Button);
    Vue.component(Select.name, Select);
    /* 或写为
    * Vue.use(Button)
    * Vue.use(Select)
    */


    new Vue({
    el: '#app',
    render: h => h(App)
    });

    在评论系统中,借鉴了 element 的源码,自行实现了这几个组件。

    扩展 axios 支持 jsonp

    axios 官方对跨域推荐的解决方式是设置CORS,在我们的业务中很多接口无法设置CORS,所以需要引入jsonp包解决这个问题,对此我们提供了一个接口的请求封装。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /**
    * 这是一个请求方法
    * @param {string} method 请求方式
    * @param {string} url 请求连接
    * @param {object} params 请求参数
    * @return {Promise} 返回 Promise
    */

    function reqMethod(method, url, obj, config = {}) {
    // jsonp
    if (method.toLocaleLowerCase() === 'jsonp') {
    let q = Object.assign({}, obj, { '_': new Date().getTime() });
    return new Promise((resolve, reject) => {
    let path = url + '?' + qs.stringify(q);
    jsonp(path, { timeout: axios.defaults.timeout }, function(err, data) {
    if (err) {
    MsgToast('err');
    reject(err);
    }
    MsgToast({
    data,
    requestURL: path
    });
    if (data.returncode !== undefined && data.returncode !== 0) {
    console.warn('JSONP 响应拦截器拦截 状态码异常:', data);
    return Promise.reject(new Error('状态码异常'));
    }
    resolve(data);
    });
    });
    }

    let modeKey = ['post', 'put'].includes(method.toLowerCase()) ? 'data' : 'params';
    return new Promise((resolve, reject) => {
    axios({
    method: method.toLowerCase(),
    url: url,
    [modeKey]: obj || {},
    ...config
    }).then((response) => {
    resolve(response.data, url);
    }).catch((error) => {
    reject(error);
    });
    });
    }

    总结:

    在 vue-cli 3.0 的构建环境下很容将一个组件输出为一个UMD的库,通过不同的引入方式可以支持在传统业务和 vue项目中引入,这符合vue的一贯思路 The Progressive JavaScript Framework,行动起来吧,是时候选择用 vue 完成你的开发任务了。

    拓展阅读:

    如何在 npm 上发布你的 vue 组件

    Vue-CLI 3.0 构建目标

    评论系统源码

    ]]>
    在不同时期不同业务场景下,前端对框架的选择也不近相同,本文将会分享PC伟德伟诺官网页中的评论系统的开发过程,如何在传统业务中使用 vue 重构(开发)一个业务组件,这与直接使用 vue 开发页面有什么不同,同时也会顺便介绍下 vue-cli 3.0 版本的]]>
    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/07/02/Front-end-development-domain-driven-design/ 2018-07-02T02:34:16.000Z 2018-07-02T02:50:31.138Z

    作者 Kid 蚂蚁金服·数据体验bv伟德登录入口团队

    随着我们解决的场景越来越专业化和复杂化,大型SPA应用的流行,前端承担的职责越来越多。代码的质量和系统的完整性越来越难把握。很容易导致迭代着迭代着发现代码改不动了。最后只能新起炉灶,重新开发。归根到底在于复杂度的失控,本文会尝试分析其中的问题以及从前端如何应用领域模型开发的角度给出一些建议。

    为什么迭代越来越难

    我们的系统架构精心设计过,按照标准的系统分层来管理复杂度。逻辑层展示层数据层。每一层都精心设计。我们抽象出独立的类来放通用逻辑。对着代码不断地重构,将有复用能力的点进行抽象。为什么需求的变动还是能经常摧毁我们的设计呢。

    原因在于:

  • 问题域本身错综复杂
  • bv伟德登录入口模型与领域模型不匹配
  • 知识的丢失
  • 问题域本身错综复杂

    软件本身是为了管理复杂度,我们现在面对的问题域错综复杂。为了创建真正好用的软件,开发者必须有一整套与之相关的知识体系。为此要知道的知识的广度让人生畏。一旦我们不能理解问题域,我们就没法做到控制问题域的复杂性。

    当复杂性失去控制的时候,开发人员就无法理解软件。当领域的复杂度没有得到解决时,基础bv伟德登录入口再好的构思也无济于事。

    bv伟德登录入口模型与领域模型不匹配

    上面我所描述的设计都是bv伟德登录入口层面的设计。我们很容易抽象出一个独立的类来放通用逻辑,可是很难给它业务上的定义!这个通用类只有bv伟德登录入口维度上的通用。

    问题在于bv伟德登录入口维度上的通用很容易被业务摧毁。需求上的变动或者膨胀,bv伟德登录入口维度的通用很容易被摧毁。举个例子,页面变化了,某个视图组件被复用了,他可能就要被提取到上层的common目录。也就是bv伟德登录入口模型立刻需要重新设计,然后就是重构,重构成工程师喜欢的简洁的样子。然后需求再变化,再重构….陷入了怪圈。

    并且这个阶段我们很难保证重构的高效进行,有个理论叫破窗户理论。一幢年老的大楼,一旦第一扇窗户破了,就会立刻给人一种年久失修,腐败的迹象。就像是一辆车,一旦第一个车窗坏了,里面很快就会遭到破坏。

    里面的根本原因就是我们设计的bv伟德登录入口模型领域模型不匹配。于是每次需求的改动,映射到bv伟德登录入口模型的改动可能就是极大的工作量。甚至根本改不动,在业务压力很大的时候,我们只能告诉产品经理,这个可以做,但是我们需要2个月。结局很可能就是需求方的妥协,牺牲用户的利益。导致产品越来越难用。

    知识的丢失

    任何项目都会丢失知识,外包出去的系统可能只交回了代码,知识没有传递回来。离职了,转岗了,一旦出于某种原因人们没有口头传递知识,知识就丢失了。

    丢失的知识也会导致系统越来越难维护,新同学不知道对于通用逻辑的改动会发生什么事情,代码最终变成了“石油坑”,越陷越深,最终无法自拔。

    总结

    以上这三个问题归根到底,就是我们没有在前端代码里把我们业务描述清楚。我们很多情况下是视图驱动,而不是业务驱动。很多时候只关心页面长什么样子,发了什么请求拿了什么数据。于是在业务概念上每个人理解的深度都不同。解这个问题可能采用新的领域驱动设计的开发方式会比较合适。

    领域驱动设计

    领域模型是跨前端-后端-产品-设计的统一的语言。统一的语言既可以形成统一的理解,也可以促进领域模型的不断精化。也能迫使开发人员学习重要的业务原理,而不是机械的功能开发。产品经理也会不断提炼知识,升华自身理解。如果没有一个统一的,有共识的结构化的模型,一定会让项目很快的僵化,最后变成维护代价极高的遗留系统。

    前端应用领域模型

    领域模型很多情况下都是由后端同学建立的,前端同学如何指导开发呢?我对于我们系统的演进过程进行了总结,希望能给大家一些灵感:

  • 理解后端领域模型
  • 建立前端领域模型
  • 分离领域层
  • 主导接口约定
  • 开发中注意业务含义
  • 实时同步
  • 理解后端领域模型

    我们在进行前端设计之前要搞懂我们要开发的业务含义。除了自己理解建立模型之外我们可以寻求后端同学的帮助。拿到他们的领域模型,弄清他们的模块划分。他们其实是业务逻辑的最终实现方,我们可以直接借鉴他们的模型,这样也可以保证前后端对于业务模型的理解一致。

    建立前端领域模型

    我们要绘制出前端的领域模型图,这个图与后端的领域模型图一致程度很高,但绝不是一样的。通常比后端模型简单。比如页面需要进行一项任务的配置,这个配置在后端模型里可能会被解释的相当复杂(会被拿去做一些同环比之类的复杂操作),但是在我们前端模型里,他的业务功能就是简单的任务配置而已~

    分离出领域层

    领域层.png | left

    如图,这一点是必须落到代码上的核心!!一定要根据对应的前端领域模型在代码中分离出单独的领域层。模型必须与实现紧密结合,一定要指导设计,并落到代码上成为最终产品的一部分

    还需要强调的是领域层的建设一定不是两个页面同时发了个请求,于是把这个请求抽出来,给与一个领域的名字。他一定被提前建立好的。在开始进行前端设计之前就被设计出来的一层。

    我们要将所有页面组件与模块内的业务行为都抽离出来,放在合适的领域模块中。只要是业务行为,一定有一个领域模块可以落。如果不行就是领域模型设计的不合理。

    要明白,驱动领域层分离的目的并不是页面被复用,这一点在思想上一定要转化过来。领域层并不是因为被多个地方复用而被抽离。它被抽离的原因是:

  • 领域层是稳定的(页面以及与页面绑定的模块都是不稳定的)
  • 领域层是解耦的(页面是会耦合的,页面的数据会来自多个接口,多个领域)
  • 领域层具有极高复杂度,值得单独管理(view层处理页面渲染以及页面逻辑控制,复杂度已经够高,领域层解耦可以轻view层。view层尽可能轻量是我们架构师cnfi主推的思路)
  • 领域层以层为单位是可以被复用的(你的代码可能会抛弃某个bv伟德登录入口体系,从vue转成react,或者可能会推出一个移动版,在这些情况下,领域层这一层都是可以直接复用)
  • 为了领域模型的持续衍进(模型存在的目的是让人们聚焦,聚焦的好处是加强了前端团队对于业务的理解,思考业务的过程才能让业务前进)
  • 这里想引用下我们leader导演的话说,我们的竞争力绝不仅仅只是前端,我们的竞争力在于我们是数据部门的前端,在于我们对于数据业务的理解。只有对于业务有深层次的理解,才能将系统带到正确的轨道上来。

    主导接口约定阶段

    接口约定尽量由前端主导,毕竟接口是给前端使用,前端来设计接口比较合理。而且在约定的过程中,前端同学又多了一次熟悉后端是如何分模块的机会。必须通过看后端同学的数据库和整体的设计文档来约定接口路径和变量名称,也能够让前后端同学对于系统的各部分的命名一致。

    在开发中注意业务含义

    我们在类,方法,模块命名时要直指业务核心,保持与领域模型的一致。比如一条员工数据记录可能会被翻译成 inputRec或者employeeData, inputRec其实就是一个计算机思维的术语,而employeeData才是直指问题领域。这个错误其实很容易犯,我们开发的程序员思维根深蒂固

    实时同步

    确保团队内部所有同学都要熟悉系统的模型。尤其是对于要熟悉并修改代码的新同学,先向他们分享我们系统的领域模型之后再介绍bv伟德登录入口架构。工作开展的重点的不同会导致编程世界观的不同。这样子会让新同学养成习惯,在进行bv伟德登录入口决断之前先判断是否符合现有的模型。不断的思考模型,才能够帮助我们业务成长。

    总结

    领域驱动设计对于降低项目的复杂度上是明显效果的,而且将前端的代码业务逻辑和视图逻辑解耦。可以做到业务逻辑层的复用。加深了前端同学对于业务的理解和思考,可以促进业务发展。这种分层思想并不局限在某个框架下,建议大家尝试下~

    对我们团队感兴趣的可以关注专栏,关注github或者发送简历至’tao.qit####alibaba-inc.com‘.replace(‘####’, ‘@’),欢迎有志之士加入~

    原文地址:https://github.com/ProtoTeam/blog/blob/master/201806/2.md

    ]]>

    作者 Kid 蚂蚁金服·数据体验bv伟德登录入口团队

    随着我们解决的场景越来越专业化和复杂化,大型SPA应用的流行,前端承担的职责越来越多。代码的质量和系统的完整性越来越难把握。很容易导致迭代着迭代着发现代码改不动了。]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/06/29/What-is-Modern-mode-in-the-Vue-CLI-3-configuration/ 2018-06-29T03:55:03.000Z 2018-06-29T03:58:31.351Z 最近特别关注 vue-cli 3 的更新情况,有很多特别棒的新功能和特性,比如基于 UI 界面的项目管理器(参数配置、数据查看、插件安装一体的界面工具)、可配置的输出构建类型(App、库、组件、异步组件)、构建模式 Modern mode 等等。下面我们重点关注下 Modern mode 是什么,如何实现的。

    目录

  • Modern mode 是什么?
  • Modern mode 实现方式
  • Modern mode 是什么?

    使用 Babel 我们能够利用 ES2015 中最新的语言特性,但这也意味着我们必须通过转换和 添加 polyfille 来支持旧浏览器。这些转换后的代码通常比原生 ES2015+ 代码更冗长,并且解析和运行较慢。鉴于当今大多数现代浏览器对原生 ES2015+ 有着不错的支持,而我们不得不将数据量更大和效率底下的代码发送给浏览器,因为我们必须支持那些旧的浏览器。

    Vue CLI 提供了一个 Modern mode 来帮助您解决这个问题。用以下命令进行生产时:

    1
    vue-cli-service build --modern

    Vue CLI 构建两个版本的 js 包:一个面向支持现代浏览器的原生 ES2015+ 包,以及一个针对其他旧浏览器的包。

    但最酷的部分是没有特殊的部署要求。生成的HTML文件中自动适配。 这个方式采用了Phillip Walton 伟德伟诺官网中讨论的bv伟德登录入口方案

  • 在支持原生 ES2015+ 的浏览器中,js会通过 <script type="module"> 加载,并且可以使用 <link rel="modulepreload"> 预加载。

  • 在不支持的浏览器中使用 <script nomodule> 来加载编译版本,并且这会被支持ES模块的浏览器所忽略。

  • Safari 10 中有一个小问题这里已经解决,可以自动加载。

  • 对比 Hello World 应用(vue 初始化的 Demo)使用这种模式打包出来的文件,通过现代模式输出的包(以后简称现代包)已经小了16%。在生产中,现代包通常会显著的提升 parse 速度和加载性能。

    Modern mode 实现方式

    在浏览器环境语法特性检测还没有一个特别好的解决方案,随着一些新的 JavaScript 语法的出现,单凭特性检测来检查新语法的支持程度很是棘手。尽管如此对于 ES2015+ 的基本语法特性检测我们还是有办法的。解决之道便是 <script type="module">

    大部分开发者认为 <script type="module"> 是用来加载 ES 模块的,但是这里使用是 <script type="module"> 的特性——加载浏览器可以处理的、使用 ES2015+ 语法的 JavaScript 文件。

    换句话说,每个支持 <script type="module"> 的浏览器都支持你所熟知的大部分 ES2015+ 语法,例如:

  • 支持 <script type="module"> 的浏览器也支持 async 和 await 函数。
  • 支持 <script type="module"> 的浏览器也支持 Class 类。
  • 支持 <script type="module"> 的浏览器也支持 arrow functions。
  • 支持 <script type="module"> 的浏览器也支持 fetch 、Promises、Map、Set 等更多 ES2015+ 语法。
  • 因此,唯一需要做的就是为不支持 <script type="module"> 的浏览器提供一个降级方案。对于支持 <script type="module"> 的浏览器会忽略 <script nomodule></script> 方式引入的脚本,如下代码:

    1
    2
    3
    4
    // 支持的浏览器 会加载 app.js, 不支持的浏览器因为 type 值不是 text/javascript 所以脚本并不会被加载。
    <script type="module" src="app.js"></script>
    // 支持的浏览器 会忽略配置 `nomodule` 属性的脚本加载,不支持的浏览器会正常加载。
    <script src="app-legacy.js" nomodule></script>

    下面看一下vue打包出来的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // ...
    <link as="style" href=""/css/app.6166f93b.css" rel="preload">
    <link as="script" href="/js/app.4e3e948a.js" rel="modulepreload">
    <link as="script" href="/js/chunk-vendors.fcf87964.js" rel="modulepreload">
    <link href="/css/app.6166f93b.css" rel="stylesheet">
    // ...

    <script type="module" src="/js/chunk-vendors.fcf87964.js"></script>
    <script type="module" src="/js/app.4e3e948a.js"></script>
    <script>!function () { var e = document, t = e.createElement("script"); if (!("noModule" in t) && "onbeforeload" in t) { var n = !1; e.addEventListener("beforeload", function (e) { if (e.target === t) n = !0; else if (!e.target.hasAttribute("nomodule") || !n) return; e.preventDefault() }, !0), t.type = "module", t.src = ".", e.head.appendChild(t), t.remove() } }();</script>
    <script src="/js/chunk-vendors-legacy.ea74b83d.js" nomodule></script>
    <script src="/js/app-legacy.854b5bc1.js" nomodule></script>

    之前说到现代浏览器中都可以通过 <script type="module"> 来实现 ES2015+ 的特性检测针对性的加载脚本,但是 Safari 10 除外,这里的一段脚本是修复 safari 10 上 nomdoule 的表现不同的:

    1
    <script>!function () { var e = document, t = e.createElement("script"); if (!("noModule" in t) && "onbeforeload" in t) { var n = !1; e.addEventListener("beforeload", function (e) { if (e.target === t) n = !0; else if (!e.target.hasAttribute("nomodule") || !n) return; e.preventDefault() }, !0), t.type = "module", t.src = ".", e.head.appendChild(t), t.remove() } }();</script>

    webpack 相关配置

    如果你对 vue-cli 3 是如何实现这块的感兴趣可以查看源码

    为了生成不同环境的js文件,你需要2个 babel-loader targets配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // resolve targets
    let targets
    if (process.env.VUE_CLI_BABEL_TARGET_NODE) {
    // running tests in Node.js
    targets = { node: 'current' }
    } else if (process.env.VUE_CLI_BUILD_TARGET === 'wc' || process.env.VUE_CLI_BUILD_TARGET === 'wc-async') {
    // targeting browsers that at least support ES2015 classes
    // https://github.com/babel/babel/blob/master/packages/babel-preset-env/data/plugins.json#L52-L61
    targets = {
    browsers: [
    'Chrome >= 49',
    'Firefox >= 45',
    'Safari >= 10',
    'Edge >= 13',
    'iOS >= 10',
    'Electron >= 0.36'
    ]
    }
    } else if (process.env.VUE_CLI_MODERN_BUILD) {
    // targeting browsers that support <script type="module">
    targets = { esmodules: true }
    } else {
    targets = rawTargets
    }

    ES2015+ 浏览器支持目标只需配置 babel-loader 参数 targets = { esmodules: true } 即可。

    示例代码块地址

    preload 作为一个新的web标准,旨在提高性能,为web开发人员提供更细粒度的加载控制。preload 使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。

    preload 还有许多其他好处。使用 as 来指定将要预加载的内容的类型,将使得浏览器能够:

  • 更精确地优化资源加载优先级。
  • 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
  • 为资源应用正确的内容安全策略。
  • 为资源设置正确的 Accept 请求头。
  • modulepreload 诞生前,还没有一种很好的声明式预加载模块的方法。Chrome 从 64 版本后 开始 “实验性的支持这个特征”。<link rel="modulepreload"><link rel="preload"> 的特定模块版本,解决了后者的一些问题。

    总结:

    启用该模式会自动构建两个版本的 js 包,针对支持现代浏览器的原生 ES2015+ 包,和针对其他旧浏览器的包,生成的 HTML 会通过 <script type="module"><script nomodule> 进行自动降级,不需要任何特殊部署配置。原生 ES2015 包几乎不需要任何 polyfill 和编译,代码尺寸更小,现代浏览器 parse 和运行也更快。

    拓展阅读:

    ES6 Modules in Chrome M61+
    ECMAScript modules in browsers
    ES6 Modules in Depth
    Deploying ES2015+ Code in Production Today
    Preloading modules

    ]]>
    最近特别关注 vue-cli 3 的更新情况,有很多特别棒的新功能和特性,比如基于 UI 界面的项目管理器(参数配置、数据查看、插件安装一体的界面工具)、可配置的输出构建类型(App、库、组件、异步组件)、构建模式 Modern mode 等等。下]]>
    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2018/02/27/New-features-of-vue-cli-3-speed/ 2018-02-27T08:48:12.000Z 2018-02-27T08:51:43.852Z vue-cli 是 vue 官方团队推出的一款快速开发 vue 项目的构建工具,具有开箱即用并且提供简洁的自定义配置等功能。 vue-cli 从 2.0 到 3.0 的升级有太多的新东西可以说了,但是不可能在本文中列举所有的内容,这篇伟德伟诺官网作为一个对比 2.0 升级功能的导读,让你快速了解 3.0 更新的内容。

    一.创建项目:

    创建项目命令的变化。

    1
    vue create my-project

    3.0 版本包括默认预设配置 和 用户自定义配置。

    自定义功能配置包括以下功能:

  • TypeScript
  • Progressive Web App (PWA) Support
  • Router
  • Vuex
  • CSS Pre-processors
  • Linter / Formatter
  • Unit Testing
  • E2E Testing
  • 可以注意到 3.0 版本直接新加入了 TypeScript 以及 PWA 的支持。

    在选择 CSS 预处理后会提示选择哪一种预处理器:

  • SCSS/SASS
  • LESS
  • Stylus
  • 以及 eslint 规范的选择:

  • ESLint with error prevention only
  • ESLint + Airbnb config
  • ESLint + Standard config
  • ESLint + Prettier
  • 最后选择 Babel, PostCSS, ESLint 等自定义配置的存放位置:

  • In dedicated config files
  • In package.json
  • 选择好后,可以把以上配置存储为预设值,以后通过 vue-cli 创建的其他项目将都采用刚才的配置。

    二. 项目目录结构变化:

    我们对比发现 vue-cli 3.0 默认项目目录相比 2.0 来说精简了很多。

  • 移除了配置文件目录,configbuild 文件夹。
  • 移除了 static 文件夹,新增 public 文件夹,并且 index.html 移动到 public 中。
  • src 文件夹中新增了 views 文件夹,用于分类 视图组件 和 公共组件。
  • 三.移除了配置文件目录后如何自定义配置。

    从 3.0 版本开始,在项目的根目录放置一个 vue.config.js 文件, 可以配置该项目的很多方面。

    vue.config.js 应该导出一个对象,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    module.exports = {
    baseUrl: '/',
    outputDir: 'dist',
    lintOnSave: true,
    compiler: false,
    // 调整内部的 webpack 配置。
    // 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/webpack.md
    chainWebpack: () => {},
    configureWebpack: () => {},
    // 配置 webpack-dev-server 行为。
    devServer: {
    open: process.platform === 'darwin',
    host: '0.0.0.0',
    port: 8080,
    https: false,
    hotOnly: false,
    // 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/cli-service.md#配置代理
    proxy: null, // string | Object
    before: app => {}
    }
    ....
    }

    调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象,该对象将会被 webpack-merge 合并入最终的 webpack 配置。

    示例代码:配置 webpack 新增一个插件。

    1
    2
    3
    4
    5
    6
    7
    8
    // vue.config.js
    module.exports = {
    configureWebpack: {
    plugins: [
    new MyAwesomeWebpackPlugin()
    ]
    }
    }

    修改插件选项的参数你需要熟悉 webpack-chain 的 API 并阅读一些源码以便了解如何权衡这个选项的全部配置项,但是它给了你比直接修改 webpack 配置中的值更灵活且安全的方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // vue.config.js
    module.exports = {
    chainWebpack: config => {
    config
    .plugin('html')
    .tap(args => {
    return [/* new args to pass to html-webpack-plugin's constructor */]
    })
    }
    }

    注意:当我们更改一个webpack配置时候,可以通过 vue inspect > output.js 输出完整的配置清单,注意它输出的并不是一个有效的 webpack 配置文件,而是一个用于审查的被序列化的格式。

    查看更多细节

    四. ESLint、Babel、browserslist 相关配置:

    Babel 可以通过 .babelrcpackage.json 中的 babel 字段进行配置。
    ESLint 可以通过 .eslintrcpackage.json 文件中的 eslintConfig 字段进行配置。
    你可能注意到了 package.json 中的 browserslist 字段指定了该项目的目标浏览器支持范围。

    五. 关于 public 目录的调整。

    vue 约定 public/index.html 作为入口模板会通过 html-webpack-plugin 插件处理。在构建过程中,资源链接将会自动注入其中。除此之外,vue-cli 也自动注入资源提示(preload/prefetch), 在启用 PWA 插件时注入 manifest/icon 链接, 并且引入(inlines) webpack runtime / chunk manifest 清单已获得最佳性能。

    在 JavaScript 或者 SCSS 中通过相对路径引用的资源会经过 webpack 处理。放置在 public 文件的资源可以通过绝对路径引用,这些资源将会被复制,而不经过 webpack 处理。

    小提示:图片最好使用相对路径经过 webpack 处理,这样可以避免很多因为修改网站根目录导致的图片404问题。

    六. 新增功能:

    1. 对 TypeScript 的支持。

    在 3.0 版本中,选择启用 TypeScript 语法后,vue 组件的书写格式有特定的规范。

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

    const s = Symbol('baz')

    @Component
    export class MyComponent extends Vue {

    @Emit()
    addToCount(n: number){ this.count += n }

    @Emit('reset')
    resetCount(){ this.count = 0 }

    @Inject() foo: string
    @Inject('bar') bar: string
    @Inject(s) baz: string

    @Model('change') checked: boolean

    @Prop()
    propA: number

    @Prop({ default: 'default value' })
    propB: string

    @Prop([String, Boolean])
    propC: string | boolean

    @Provide() foo = 'foo'
    @Provide('bar') baz = 'bar'

    @Watch('child')
    onChildChanged(val: string, oldVal: string) { }

    @Watch('person', { immediate: true, deep: true })
    onPersonChanged(val: Person, oldVal: Person) { }
    }

    以上代码相当于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    const s = Symbol('baz')

    export const MyComponent = Vue.extend({
    name: 'MyComponent',
    inject: {
    foo: 'foo',
    bar: 'bar',
    [s]: s
    },
    model: {
    prop: 'checked',
    event: 'change'
    },
    props: {
    checked: Boolean,
    propA: Number,
    propB: {
    type: String,
    default: 'default value'
    },
    propC: [String, Boolean],
    },
    data () {
    return {
    foo: 'foo',
    baz: 'bar'
    }
    },
    provide () {
    return {
    foo: this.foo,
    bar: this.baz
    }
    },
    methods: {
    addToCount(n){
    this.count += n
    this.$emit("add-to-count", n)
    },
    resetCount(){
    this.count = 0
    this.$emit("reset")
    },
    onChildChanged(val, oldVal) { },
    onPersonChanged(val, oldVal) { }
    },
    watch: {
    'child': {
    handler: 'onChildChanged',
    immediate: false,
    deep: false
    },
    'person': {
    handler: 'onPersonChanged',
    immediate: true,
    deep: true
    }
    }
    })

    更多详细内容请关注这里

    2. 对 PWA 的支持。

    当我们选择启用 PWA 功能时,在打包生成的代码时会默认生成 service-worker.jsmanifest.json 相关文件。如果你不了解 PWA,点击这里查看;

    pwa

    需要注意的是 在 manifest.json 生成的图标信息,可以在 public/img 目录下替换。

    默认情况 service-worker 采用的是 precache,可以通过配置 pwa.workboxPluginMode 自定义缓存策略。详情

    配置示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Inside vue.config.js
    module.exports = {
    // ...其它 vue-cli 插件选项...
    pwa: {
    workboxPluginMode: 'InjectManifest',
    workboxOptions: {
    // swSrc 中 InjectManifest 模式下是必填的。
    swSrc: 'dev/sw.js',
    // ...其它 Workbox 选项...
    },
    },
    };

    总结:

    vue-cli 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在编写你的应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性。

    相关链接:

    https://github.com/vuejs/vue-cli/
    https://github.com/vuejs/vue-cli/blob/dev/docs/README.md

    ]]>
    vue-cli 是 vue 官方团队推出的一款快速开发 vue 项目的构建工具,具有开箱即用并且提供简洁的自定义配置等功能。 vue-cli 从 2.0 到 3.0 的升级有太多的新东西可以说了,]]>
    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2017/10/13/How-to-publish-your-Vue-component-on-NPM/ 2017-10-13T08:23:59.000Z 2017-10-13T09:46:45.003Z 原文地址:https://vuejsdevelopers.com/2017/07/31/vue-component-publish-npm/

    当你开发出一款NB累累的 Vue 组件,并希望其他开发者在的项目中使用它。你将怎么分享组件给他们使用呢?

    在这篇伟德伟诺官网中我将告诉你如何准备你的组件,打包并发布到 NPM。我将使用一个示例项目演示以下内容:

  • 确保包中不包含任何依赖项
  • 使用 Webpack 分别构建应用于浏览器和Node环境中的代码
  • 创建一个应用于浏览器的插件
  • 配置 package.json
  • 发布到 NPM
  • 项目案例: Vue Clock

    我创建了这个简单的时钟组件,我会把它发布到 NPM 上。也许它不是你见过的最酷的组件,但它足以演示。

    enter image description here

    这是组件文件。这里没有什么特别的东西,但是请注意,我正在引入 moment 库用于格式化时间。
    从你的包中排除依赖关系很重要,稍后我们会看到。

    Clock.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div></div>
    </template>
    <script>
    import moment from 'moment';

    export default {
    data() {
    return {
    time: Date.now()
    }
    },
    computed: {
    display() {
    return moment(this.time).format("HH:mm:ss");
    }
    },
    created() {
    setInterval(() => {
    this.time = Date.now();
    }, 1000);
    }
    }
    </script>

    关键工具: Webpack

    我为将组件发布到 NPM 上所做的大部分准备工作都是由 Webpack 完成的。 下面示例代码是本文中将要添加的基本 Webpack 配置。 如果您以前使用过Vue和Webpack,可以忽略没什么特别的地方:

    webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    const webpack = require('webpack');
    const path = require('path');

    module.exports = {
    entry: path.resolve(__dirname + '/src/Clock.vue'),
    output: {
    path: path.resolve(__dirname + '/dist/'),
    filename: 'vue-clock.js'
    },
    module: {
    loaders: [
    {
    test: /\.js$/,
    loader: 'babel',
    include: __dirname,
    exclude: /node_modules/
    },
    {
    test: /\.vue$/,
    loader: 'vue'
    },
    {
    test: /\.css$/,
    loader: 'style!less!css'
    }
    ]
    },
    plugins: [
    new webpack.optimize.UglifyJsPlugin( {
    minimize : true,
    sourceMap : false,
    mangle: true,
    compress: {
    warnings: false
    }
    })
    ]
    };

    Externals

    externals 配置选项可以配置,从 Webpack 打包的代码中排除某个特定的依赖。 我不希望我的组件中包含依赖关系,因为那会使打包出来的代码变得臃肿,并可能导致用户环境中的版本冲突。 用户必须自己安装依赖项。

    在这个例子中,我的包依赖于 moment 库。 为了确保它不会被打包,在 WebPACK 配置中我将指定 moment 为一个 external (外部引用):

    webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    module.exports = {
    ...
    externals: {
    moment: 'moment'
    },
    ...
    }

    环境搭建

    在Vue.js中,用户可能通过两种途经使用组件。 首先是,浏览器引入:

    1
    <script type="text/javascript" src="vue-clock.js"></script>

    其次,基于Node.js的开发环境,例如:

    1
    import VueClock from 'vue-clock';

    理想情况下,我希望用户能够在任意一个环境中使用 Vue Clock。 不幸的是,这些环境需要将代码以不同方式打包,这意味着我必须设置两个不同的构建配置。

    为此,我将创建两个单独的Webpack配置。 这比听起来更容易,因为配置几乎相同。 首先,我将创建一个常见的配置对象,然后使用 webpack-merge 将其包含在两个环境配置中:

    webpack.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const webpack = require('webpack');
    const merge = require('webpack-merge');
    const path = require('path');

    var commonConfig = {
    output: {
    path: path.resolve(__dirname + '/dist/'),
    },
    module: {
    loaders: [ ... ]
    },
    externals: { ... },
    plugins: [ ... ]
    };

    module.exports = [

    // Config 1: For browser environment
    merge(commonConfig, {


    }),

    // Config 2: For Node-based development environments
    merge(commonConfig, {

    })
    ];

    常见的配置与之前完全一样(我简写了大部分内容以节省空间),除了我已经删除的 entryoutput.filename 选项。 我将在单独的构建配置中单独配置它们。

    用于浏览器环境的打包

    浏览器不能像 Node 那样,可以从另一个 JavaScript 模块文件引入。但是它们可以使用像AMD这样的脚本加载器,为了最大限度地简化,我让组件脚本作为一个全局变量,这样可以更简单地添加。

    另外,我不希望用户必须想很多才能弄清楚如何使用组件。我会这样做的,当用户引入脚本时,组件可以很容易地注册为全局组件。Vue的插件系统将有助于实现这个功能。

    我的目标是这个简单的设置:

    index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <body>
    <div id="app">
    <vue-clock></vue-clock>
    </div>
    <script type="text/javascript" src="vue-clock.js"></script>
    <script type="text/javascript">
    Vue.use(VueClock);
    </script>

    </body>

    Plugin

    首先,我将创建一个插件包装器,以方便安装组件:

    plugin.js

    1
    2
    3
    4
    5
    6
    7
    import Clock from './Clock.vue';

    module.exports = {
    install: function (Vue, options) {
    Vue.component('vue-clock', Clock);
    }
    };

    该插件全局注册组件,因此用户可以在其应用程序的任何位置调用 clock 组件。

    webpack配置

    现在,我使用plugin.js路径作为浏览器构建的入口点。我会输出一个名为 vue-clock.min.js 的文件,这对用户来说是最明显的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = [
    merge(config, {
    entry: path.resolve(__dirname + '/src/plugin.js'),
    output: {
    filename: 'vue-clock.min.js',
    }
    }),
    ...
    ];

    以库的形式导出

    Webpack 可以以各种不同的方式导出你的脚本,例如:作为一个对象,作为一个全局变量,遵循AMD或CommonJS规范的模块。您可以使用 libraryTarget 选项指定。

    对于浏览器打包方式,我将使用 window 导出方式。 我也可以使用 UMD 来获得更多的灵活性,但是由于我已经创建了两个打包方式,所以我只是将这个打包方式限制在浏览器中。

    我还将库名称指定为 VueClock。 这意味着当浏览器引入包时,它将挂载在全局上 window.VueClock

    1
    2
    3
    4
    5
    output: {
    filename: 'vue-clock.min.js',
    libraryTarget: 'window',
    library: 'VueClock'
    }

    Node 环境打包

    为了允许用户在 Node 开发环境中使用组件,我将使用 UMD 方式打包输出提供给 Node 环境使用。 UMD是一种灵活的模块类型,可以在各种不同的脚本加载器和环境中使用代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    module.exports = [
    ...
    merge(config, {
    entry: path.resolve(__dirname + '/src/Clock.vue'),
    output: {
    filename: 'vue-clock.js',
    libraryTarget: 'umd',

    // These options are useful if the user wants to load the module with AMD
    library: 'vue-clock',
    umdNamedDefine: true
    }
    })
    ];

    注意,在Node 环境中使用单个文件组件作为入口,不需要使用插件包装器。这样可以更灵活的引入:

    1
    2
    3
    4
    5
    6
    7
    import VueClock from 'vue-clock';

    new Vue({
    components: {
    VueClock
    }
    });

    package.json

    在发布到NPM之前,我将设置我的 package.json 文件。 有关每个选项的详细说明,请访问 npmjs.com。

    package.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "name": "vue-clock-simple",
    "version": "1.0.0",
    "description": "A Vue.js component that displays a clock.",
    "main": "dist/vue-clock.js",
    "scripts": {
    "build": "rimraf ./dist && webpack --config ./webpack.config.js"
    },

    "author": "Anthony Gore",
    "license": "MIT",
    "dependencies": {
    "moment": "^2.18.1"
    },

    "repository": { ... },
    "devDependencies": { ... }
    }

    我省略了这个文件的大部分,但重点要注意的是:

    1. 主脚本文件即 "main": "dist/vue-clock.js"。 这指向 Node bundle 文件,确保模块加载器知道文件的准确路径,即
    1
    import VueClock from 'vue-clock' // this resolves to dist/vue-clock.js
    1. 依赖关系,由于我从包中排除了所有依赖关系,用户必须自行安装依赖才能使用该包。

    发布到 NPM

    现在我的组件设置正确,可以在NPM上发布。 我不会重复的在这里说明,因为它们在npmjs.com上很好地介绍。

    结果如下:

    enter image description here

    https://github.com/anthonygore/vue-clock-simple/blob/master/webpack.config.js
    https://vuejsdevelopers.com/2017/07/31/vue-component-publish-npm/

    ]]>
    原文地址:https://vuejsdevelopers.com/2017/07/31/vue-component-publish-npm/

    当你开发出一款NB累累的 Vue 组件,并希望其他开发者在的项目中使用它。你将怎么分享组件给他们使用呢?

    在这篇伟德伟诺官网中我将告诉你如何准备你的组件,打包并发布到 NPM。我将使用一个示例项目演示以下内容:

  • 确保包中不包含任何依赖项
  • 使用 Webpack 分别构建应用于浏览器和Node环境中的代码
  • 创建一个应用于浏览器的插件
  • 配置 package.json
  • 发布到 NPM
  • 项目案例: Vue Clock

    我创建了这个简单的时钟组件,我会把它发布到 NPM 上。也许它不是你见过的最酷的组件,但它足以演示。

    enter image description here

    这是组件文件。这里没有什么特别的东西,但是请注意,我正在引入 moment 库用于格式化时间。
    从你的包中排除依赖关系很重要,稍后我们会看到。

    ]]>
    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2017/10/13/Develop-a-component-that-loads-Vue-remote-asynchronous-code/ 2017-10-13T08:23:59.000Z 2017-11-20T06:22:02.071Z 在我们的 vue 项目中(特别是后台系统),总会出现一些需要多业务线共同开发同一个项目的场景,如果各业务团队向框架中提供一些私有的展示组件,但是这些组件并不能和框架一起打包,因为框架不能因为某个私有模块的频繁变更而重复构建发布。在这种场景下我们需要一个加载远程异步代码的组件来完成将这些组件加载到框架中。

    vue-cli 作为 Vue 官方推荐的项目构建脚手架,它提供了开发过程中常用的,热重载,构建,调试,单元测试,代码检测等功能。我们本次的异步远端组件将基于 vue-cli 开发。

    需求分析

    1. 如何加载远端的代码?
    2. 如何注册加载后的代码到框架中。
    3. 父组件如何和远端引入的组件通信。
    4. 远端代码如何复用框架中已引入的库。
    5. 避免因远端代码被类似 v-for 多次调用导致的不必要请求。

    加载远端代码

    远端代码应该存储在一个可访问的 URL 上,这样我们通过 Axios 类似的 HTTP client 请求这个链接拿到源码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import Axios from 'axios';

    export default {
    name: 'SyncComponent',
    props: {
    // 父组件提供请求地址
    url: {
    type: String,
    default: ''
    }
    },
    data() {
    return {
    resData: ''
    };
    },
    async mounted() {
    if (!this.url) return;
    const res = await Axios.get(this.url); // 我们在组件挂载完成时,请求远端代码并存储结果。
    this.resData = res.data;
    }
    };

    以上是基础代码 为了方便 一下例子中 我将省略重复的代码部分。

    注册代码到框架中

    这部分有些繁琐,涉及到多个问题:

    1. 浏览器并不支持 .vue 模板 或 ES.next 语法,模块需要编译后才可以使用。

    处理这部分比较简单,我们自己定义一个webpack配置文件来打包这些模板。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    // 在 build 目录下新建 webpack.sync-components.prod.conf.js 文件

    const webpack = require('webpack');
    const path = require('path');
    const utils = require('./utils');
    const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

    function resolve(dir) {
    return path.join(__dirname, '..', dir)
    }

    module.exports = {
    // 此处引入要打包的组件
    entry: {
    componentA: resolve('/src/views/component-a.vue')
    },
    // 输出到静态目录下
    output: {
    path: resolve('/static/'),
    filename: '[name].js',
    },
    resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
    'vue$': 'vue/dist/vue.esm.js',
    '@': resolve('src'),
    }
    },
    module: {
    rules: [
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
    esModule: false, // ****** vue-loader v13 更新 默认值为 true v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
    loaders: utils.cssLoaders({
    sourceMap: true,
    extract: false // css 不做提取
    }),
    transformToRequire: {
    video: 'src',
    source: 'src',
    img: 'src',
    image: 'xlink:href'
    }
    }
    },
    {
    test: /\.js$/,
    loader: 'babel-loader',
    include: [resolve('src'), resolve('test')]
    },
    {
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    loader: 'url-loader',
    options: {
    limit: 10000,
    name: utils.assetsPath('img/[name].[hash:7].[ext]')
    }
    },
    {
    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    loader: 'url-loader',
    options: {
    limit: 10000,
    name: utils.assetsPath('media/[name].[hash:7].[ext]')
    }
    },
    {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    options: {
    limit: 10000,
    name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
    }
    }
    ]
    },
    plugins: [
    new webpack.DefinePlugin({
    'process.env.NODE_ENV': '"production"'
    }),
    // 压缩JS
    new webpack.optimize.UglifyJsPlugin({
    compress: false,
    sourceMap: true
    }),
    // 压缩CSS 注意不做提取
    new OptimizeCSSPlugin({
    cssProcessorOptions: {
    safe: true
    }
    })
    ]
    };

    至此我们的模块已经被编译成框架可以识别的文件。

    1. 如何将字符串转换成js对象。

    new Function

    1
    2
    3
    4
    5
    6
    7

    async mounted() {
    if (!this.url) return;
    const res = await Axios.get(this.url);
    let Fn = Function;
    this.mode = new Fn(`return ${res.data}`)();
    }
    1. 转换后的js对象并不能被vue识别。

    有两种可能会导致这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // vue-loader v13 esModule 更新 默认值为 true, v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
    esModule: false
    ... 以下省略千军万码
    }
    }

    // UglifyJs 需要取消变量名替换配置,此配置并不会极大影响压缩率
    new webpack.optimize.UglifyJsPlugin({
    compress: false,
    sourceMap: true
    })

    至此 远程组件就被引入到框架中了。

    父组件如何和远端引入的组件通信

    这里有一个问题,从 view组件远程异步加载组件 再到 实际业务组件 通信一共三层,中间层 远程异步组件 作为公共组件不可被修改,需要 view组件 直接向 实际业务组件 通信。vuex 和 eventBus 方案都过于繁琐,这里我们采用 $attrs 和 $listeners(vue v2.4+), 来实现 “fallthrough”(vue组件跨层级通信)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 修改 sync-component.vue 组件
    // 新增 v-bind="$attrs" v-on="$listeners"
    <component
    :is="mode"
    v-bind="$attrs"
    v-on="$listeners">
    </component>

    // inheritAttrs: true
    export default {
    name: 'SyncComponent',
    props: {
    // 父组件提供请求地址
    url: {
    type: String,
    default: ''
    }
    },
    inheritAttrs: true
    ... 以下省略千军万码
    }

    远端代码如何复用框架中已引入的库

    我们不希望看到远端组件和框架中存在较大库或插件的重复的引入,这部分内容尚处在实践阶段,主要思路是把公共库挂载到Vue原型链上实现组件公共复用 Vue.prototype.$xxx

    1
    2
    3
    4
    // 全局添加 axios 对象
    import axios from 'axios';

    Vue.prototype.$http = axios;

    引入的远程组件可以访问到框架中的公共包了,这时候还需要配置 webpack 使远程组件打包时不要包含公共包的代码。

    1
    2
    3
    4
    5
    6
    // webpack.sync-components.prod.conf.js 添加
    externals: {
    vue: 'vue',
    'element-ui': 'element-ui',
    axios: 'axios'
    }

    避免因远端代码被类似 v-for 多次调用导致的不必要请求。

    这部分我们直接用一个全局变量做字典,存储 以 请求地址:数据 为子项的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    async mounted() {
    if (!this.url) return;
    // Cache 缓存 根据 url 参数
    if (!window.SyncComponentCache) {
    window.SyncComponentCache = {};
    }
    let res;
    if (!window.SyncComponentCache[this.url]) {
    window.SyncComponentCache[this.url] = Axios.get(this.url);
    res = await window.SyncComponentCache[this.url];
    } else {
    res = await window.SyncComponentCache[this.url];
    }
    let Fn = Function;
    this.mode = new Fn(`return ${res.data}`)();
    console.log(this.mode);
    }

    至此,异步远程组件就可以加载并和框架进行通信了。

    本文中的源码请访问 github 获取,组件已经发布到 NPM 上,可以直接安装。

    拓展阅读:
    如何在 npm 上发布你的 vue 组件

    ]]>
    在我们的 vue 项目中(特别是后台系统),总会出现一些需要多业务线共同开发同一个项目的场景,如果各业务团队向框架中提供一些私有的展示组件,但是这些组件并不能和框架一起打包,因为框架不能因为某个私有模块的频繁变更而重复构建发布。在这种场景下我们需要一个加载远程异步代码的组件]]>
    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2017/10/11/How-to-manage-front-end-single-page-applications-for-100-thousand-lines-of-code/ 2017-10-11T01:54:11.000Z 2018-07-02T02:36:40.630Z

    作者简介 导演 蚂蚁金服数据前端

    蚂蚁金服数据平台前端团队主要负责多个数据相关的PC Web单页面应用程序,业务复杂度类比Excel等桌面应用,业务前端代码量在几万行~几十万行,随着产品不断完善,破百万指日可待。管理好10万行级甚至百万行级代码的前端应用,是我们团队的核心挑战之一。

    接下来的系列伟德伟诺官网,我会尝试从以下几个角度介绍我们团队应对挑战的方法:

  • 前端架构
  • 质量保障
  • 性能优化
  • 团队前端开发流程
  • 人员素养
  • 前端架构

    团队的架构方案是多个产品经历一年的持续迭代,不断摸索出来的一套适合本团队数据产品业务场景的架构方案,架构方案中还存在尚未解决的痛点和有争议的部分需要持续优化,不保证这套架构适合您的产品。

    产品特点

    先介绍下我们团队的产品特点:

  • ToB产品,业务复杂度高、业务理解门槛高;
  • 前端代码量巨大(数据分析产品从零开始经历8个月迭代业务代码8万行,仅实现了产品长期规划需求的20%)
  • 架构方案

    架构的目的是管理复杂度,将复杂问题分而治之、有效管理,我们的具体方法如下:

    1. 首先通过路由切割“页面级”粒度的功能模块

    这里的“页面级”粒度指一个路由映射的组件

    router

    2. 同一“页面”内的模块再划分

    划分原则:

  • 纵向:通过业务功能(可根据视图模块判断)划分
  • 横向:通过Model-View-Controller三种不同职能划分
  • module

    3. 合并同类项

    继续细分粒度,然后将可复用模块或组件抽离到公共区域

    3.1 数据模型

    数据模型根据职责分成两类:

  • Domain Model 领域模型
  • App State Modal 应用状态模型
  • 3.1.1 领域模型

    领域模型是业务数据,往往要持久化到数据库或localStorage中,属于可跨模块复用的公共数据,如:

  • Users 用户信息
  • Datasets 数据集信息
  • Reports 报表信息
  • 领域模型作为公共数据,建议统一存放在一个叫做Domain Model Layer的架构独立分层中(前端业界一般对这层的命名为ORM层)。

    下沉到Domain Model Layer(领域模型层)有诸多利处:

  • 跨模块数据同步问题不复存在,例如:之前Users对象在A和B两个业务模块中单独存储,A模块变更Users对象后,需将Users变更同步到B模块中,如不同步,A、B模块在界面上呈现的User信息不一致,下沉到领域模型层统一管理后,问题不复存在;
  • 除领域模型复用外,还可复用领域模型相关的CRUD Reducer,例如:之前Users对象对应的Create Read Update Delete方法可能在A和B两个业务模块各维护一套,下沉到领域模型层统一管理后,减少了代码重复问题;
  • 自然承担了部分跨模块通信职责,之前数据同步相关的跨模块通信代码没有了存在的必要性;
  • 3.1.2 应用状态模型

    应用状态模型是与视图相关的状态数据,如:

  • 当前页面选中了列表的第n行 currentSelectedRow: someId
  • 窗口是否处于打开状态 isModalShow: false
  • 某种视图元素是否在拖拽中 isDragging: true
  • 这些数据与具体的视图模块或业务功能强相关,建议存放在业务模块的Model中。

    3.2 视图层组件

    组件根据职责划分为两类:

  • Container Component 容器型组件
  • Presentational Component 展示型组件
  • 3.2.1 容器型组件

    容器型组件是与store直连的组件,为展示型组件或其它容器组件提供数据和行为,尽量避免在其中做一些界面渲染相关的事情。

    3.2.2 展示型组件

    展示型组件独立于应用的其它部分内容,不关心数据的加载和变更,保持职责单一,仅做视图呈现和最基本交互行为,通过props接收数据和回调函数输出结果,保证接收的数据为组件数据依赖的最小集。

    一个有成百上千展示型组件的复杂系统,如果展示型组件粒度切分能很好的遵循高内聚低耦合和职责单一原则的话,可以沉淀出很多可复用的通用业务组件

    3.3 公共服务

  • 所有的HTTP请求放在一起统一管理;
  • 日志服务、本地存储服务、错误监控、Mock服务等统一存放在公共服务层;
  • 按照上面三点合并同类项后,业务架构图变更为

    api

    4. 跨模块通信

    模块粒度逐渐细化,会带来更多的跨模块通信诉求,为避免模块间相互耦合、确保架构长期干净可维护,我们规定:

  • 不允许在一个模块内部直接调用其他模块的Dispatch方法(写操作、变更其他模块的state)
  • 不允许在一个模块内部直接读取其他模块的state方法(读操作)
  • 我们建议将跨模块通信的逻辑代码放在父模块中,或者在一个叫做Mediator层中单独维护。

    最终得到我们团队完整的业务逻辑架构图:

    Architecture

    数据流管理

    刚刚从空间维度讲了架构管理的方案,现在从时间维度说说应用的数据流转 — Redux单向数据流。

    Redux架构的设计核心是单向数据流,应用中所有的数据都应该遵循相同的生命周期,确保应用状态的可预测性。

    redux

    1. Action

  • 用户操作行为:click drag input …
  • 服务端返回数据后续的行为
  • 2. Reducer

    每个Action都会对应一个数据处理函数,即Reducer。特别强调,Reducer必须是纯函数(pure function),这个规定带来一个非常大的好处,数据处理层代码变的非常容易写单元测试。

    纯函数的特征是入参相同的情况下,返回值恒等,举个栗子🌰:

    纯函数:

    1
    2
    3
    function add(a, b) {
    return a + b;
    }

    非纯函数:

    1
    2
    3
    4
    function now() {
    let now = new Date();
    return now;
    }

    函数中如果包含 Math.randomnew Date(), 异步请求等内容,且影响到最终结果的返回,即为非纯函数。

    3. Store

    Store 数据存放的地方,store保存从进入页面开始所有Action操作生成的数据状态(state),每次Action引发的数据变更都必须生成一个新的state对象,且确保旧的state对象不被修改。这样做可以保证
    应用的状态的可预测、可追溯,也方便设计Redo/Undo功能。

    我们团队使用轻量级的immutable方案immutability-helper,相比完全拷贝一份(deep clone)性能更优、存储空间利用率更高。

    immutability-helper

    immutability-helper的API不够友好,我们写了一个库immutability-helper-x增强它的易用性。

    immutability-helper API风格:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import update from 'immutability-helper';

    const newData = update(myData, {
    x: {
    y: {
    z: { $set: 7 }
    }
    },
    });

    immutability-helper-x API风格:

    1
    2
    3
    import update from 'immutability-helper-x';

    const newData = update.$set(myData, 'x.y.z', 7);

    4. 统一渲染视图

    React/Redux是一种典型的数据驱动的开发框架(Data-Driven-Development),在开发中,我们可以将更多的精力集中在数据(领域模型+状态模型)的操作和流转上,再也不用被各种繁琐的DOM操作代码困扰,当Store变更时,React/Redux框架会帮助我们自动的统一渲染视图。

    监听Store变更刷新视图的功能是由react-redux完成的:

  • 组件通过context属性向后代组件提供(provide)store对象;
  • 是一个高阶组件,作用是将store与view层组件连接起来(这里重复提一句,redux官方将直接连接的组件定义为container component),向开发者开放了几个回调函数钩子(mapStateToProps, mapDispatchToProps…)用于自定义注入container component的props的姿势;
  • react-redux监听redux store的变更,store改变后通知每一个connect组件刷新自己和后代组件,为了减少不必要的刷新提升性能,connect实现了shouldComponentUpdate方法,如果props不变的话,不刷新connect包裹的container component;
  • 总结

    严格遵循架构规范和单向数据流规范,可以保证我们的前端应用在比较粗的粒度上的可维护性和扩展性,对于更细的粒度的代码,我们组织童鞋学习和分享《设计模式》《重构 - 改善既有代码的设计》,持续打磨和优化自己的代码,未来团队会持续输出这方面的系列伟德伟诺官网。

    本篇先聊前端通用架构,具体模块的业务架构、架构遵循的原则、团队架构组的架构评审流程等内容会在接下来的系列伟德伟诺官网中阐述。感兴趣的同学关注专栏或者发送简历至 tao.qit###alibaba-inc.com,欢迎有志之士加入~

    伟德伟诺官网来源:如何管理好10万行代码的前端单页面应用

    ]]>

    作者简介 导演 蚂蚁金服数据前端

    蚂蚁金服数据平台前端团队主要负责多个数据相关的PC Web单页面应用程序,业务复杂度类比Excel等桌面应用,业务前端代码量在几万行~几十万行,随着产品不断完善,破百万指日]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2017/06/09/Git-Flow---Git-teamwork-best-practices/ 2017-06-09T03:08:01.000Z 2017-06-09T03:21:19.978Z 一、规范的Git使用

    Git是一个很好的版本管理工具,不过相比于传统的版本管理工具,学习成本比较高。
    实际开发中,如果团队成员比较多,开发迭代频繁,对Git的应用比较混乱,会产生很多不必要的冲突或者代码丢失等。
    就像代码需要代码规范一样,使用Git进行代码管理同样需要一个清晰的流程和规范, Git Flow就是一个被广泛认可的Git使用最佳实践。
    Git Flow是Vincent Driessen提出的一个分支管理的策略,http://nvie.com/posts/a-successful-git-branching-model/
    应用这个规范可以使得版本库的演进保持简洁,主干清晰,各个分支有不同的职责,在很大程度上减少冲突的产生。

    二、Git Flow开发流程

    Git Flow通过对分支的管理,实现版本迭代的清晰。
    这个流程图是应用Git Flow的标准流程,可以看到,不同的分支在产品研发和上线的不同阶段有不同的作用,扮演了不同的角色。
    http://cdn.uis.cc/img/2017/06/09/524341-20170112192235150-1905124194.png

    Git Flow不同分支的角色

    结合图片,简单介绍一下不同分支的职责。

    1.Production分支

    这个分支是发布到生产环境的代码,这个分支只能从其他分支合并,不能在这个分支直接修改。

    2.Develop分支

    这个分支是主开发分支,包含所有要发布到下一个Release的代码,这个主要合并自其他分支,比如Feature分支。

    3.Feature分支

    Feature 分支主要用来开发一个新的功能,一旦开发完成,合并回Develop分支,并且进入下一个Release,Feature分支可以选择删除或者保留。

    4.Release分支

    当需要发布一个新Release的时候,基于Develop分支创建一个Release分支,Release分支在测试过程中可能会修改,完成Release后,合并到Master和Develop分支。

    5.Hotfix分支

    当在Production发现新的Bug时候,需要创建一个Hotfix分支, 完成Hotfix后,合并回Master和Develop分支,所以Hotfix的改动会进入下一个Release。

    Git Flow使用原则

  • Master分支是线上稳定分支,Release通常用作测试分支,Develop分支是开发应用的主分支
  • 所有的功能开发都在Feature分支进行,然后合并到Develop分支
  • Release分支发布后出现问题,直接在Release分支修改,避免Develop分支代码污染
  • 三、Git Flow分支协作最佳实践

    我们在应用Git Flow的时候,也遇到了一些问题,比如开发结束后,在develop分支进行merge开发分支操作,出现冲突如果不能很好的解决,容易对develop分支的代码造成污染。

    下面是实际开发中使用的流程,在Feature分支上合并develop代码,然后合并到develop分支上,流程更加清晰,冲突优先在开发分支解决。

    一个开发人员典型的提交流程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //新建分支
    git checkout develop
    git pull origin develop
    git checkout -b myfeature

    //在分支上开发
    git add ***
    git commit -m "*****"

    //在分支开发过程中合并develop分支到本分支(先把自己的工作commit到本地)
    git checkout develop
    git pull origin develop
    git checkout myfeature
    git merge develop

    (如果没有冲突,就继续开发,如果有冲突,执行下面过程)
    首先在本地解决冲突,再把冲突解决commit
    git add ***
    git commit -m "*****"

    //在分支开发结束,需要将本分支合并到develop分支(先把自己的工作commit到本地)
    git checkout develop
    git pull origin develop
    git merge myfeature

    (如果没有冲突,就推送到远程)
    git push origin develop
    (如果有冲突,则解决冲突,再commit,并推送到远程:)
    git add ***
    git commit -m "*****"
    git push origin develop

    应用Git Flow的目的是更好的进行版本管理和持续集成,有些细节并不一定要遵循这个模型,可以根据团队规模进行简单的调整,适合的才是最好的。

    ]]>
    一、规范的Git使用

    Git是一个很好的版本管理工具,不过相比于传统的版本管理工具,学习成本比较高。
    实际开发中,如果团队成员比较多,开发迭代频繁,对Git的应用比较混乱,会产生很多不必要的冲突或者代码丢失等。
    就]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2017/03/30/vuejs-component-style-guide/ 2017-03-30T01:46:29.000Z 2017-03-30T05:19:36.870Z Vue.js 组件编码规范



    目标

    本规范提供了一种统一的编码规范来编写 Vue.js 代码。这使得代码具有如下的特性:

  • 其它开发者或是团队成员更容易阅读和理解。
  • IDEs 更容易理解代码,从而提供高亮、格式化等辅助功能
  • 更容易使用现有的工具
  • 更容易实现缓存以及代码包的分拆
  • 本指南为 De Voorhoede 参考 RiotJS 编码规范 而写。

    目录

    基于模块开发

    始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。

    Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。

    怎么做?

    每一个 Vue 组件(等同于模块)首先)必须专注于解决一个单一的问题独立的可复用的微小的可测试的

    如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。

    ↑ 回到目录

    Vue 组件命名

    组件的命名需遵从以下原则:

  • 有意义的: 不过于具体,也不过于抽象
  • 简短: 2 到 3 个单词
  • 具有可读性: 以便于沟通交流
  • 同时还需要注意:

  • 必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。
  • app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。
  • 为什么?

  • 组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。
  • 如何做?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 推荐 -->
    <app-header></app-header>
    <user-list></user-list>
    <range-slider></range-slider>

    <!-- 避免 -->
    <btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
    <ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
    <slider></slider> <!-- 与自定义元素规范不兼容 -->

    ↑ 回到目录

    组件表达式简单化

    Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化

    为什么?

  • 复杂的行内表达式难以阅读。
  • 行内表达式是不能够通用的,这可能会导致重复编码的问题。
  • IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。
  • 怎么做?

    如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!-- 推荐 -->
    <template>
    <h1>
    {{ `${year}-${month}` }}
    </h1>
    </template>
    <script type="text/javascript">
    export default {
    computed: {
    month() {
    return this.twoDigits((new Date()).getUTCMonth() + 1);
    },
    year() {
    return (new Date()).getUTCFullYear();
    }
    },
    methods: {
    twoDigits(num) {
    return ('0' + num).slice(-2);
    }
    },
    };
    </script>


    <!-- 避免 -->
    <template>
    <h1>
    {{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
    </h1>
    </template>

    ↑ 回到目录

    组件 props 原子化

    虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。

    为什么?

  • 使得组件 API 清晰直观。
  • 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。
  • 其它开发者更好的理解每一个 prop 的含义、作用。
  • 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。
  • 怎么做?

    组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 推荐 -->
    <range-slider
    :values="[10, 20]"
    min="0"
    max="100"
    step="5"
    :on-slide="updateInputs"
    :on-end="updateResults">

    </range-slider>

    <!-- 避免 -->
    <range-slider :config="complexConfigObject"></range-slider>

    ↑ 回到目录

    验证组件的 props

    在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。

    组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value"v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。

    为什么?

    验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。

    怎么做?

  • 提供默认值。
  • 使用 type 属性校验类型
  • 使用 props 之前先检查该 prop 是否存在。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <template>
    <input type="range" v-model="value" :max="max" :min="min">
    </template>
    <script type="text/javascript">
    export default {
    props: {
    max: {
    type: Number, // 这里添加了数字类型的校验
    default() { return 10; },
    },
    min: {
    type: Number,
    default() { return 0; },
    },
    value: {
    type: Number,
    default() { return 4; },
    },
    },
    };
    </script>

    ↑ 回到目录

    this 赋值给 component 变量

    在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。

    换句话说,不要在编写这样的代码 const self = this; ,而是应该直接使用变量 component

    为什么?

  • 将组件 this 赋值给变量 component可用让开发者清楚的知道任何一个被使用的地方,它代表的是组件实例。
  • 怎么做?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <script type="text/javascript">
    export default {
    methods: {
    hello() {
    return 'hello';
    },
    printHello() {
    console.log(this.hello());
    },
    },
    };
    </script>


    <!-- 避免 -->
    <script type="text/javascript">
    export default {
    methods: {
    hello() {
    return 'hello';
    },
    printHello() {
    const self = this; // 没有必要
    console.log(self.hello());
    },
    },
    };
    </script>

    ↑ 回到目录

    组件结构化

    按照一定的结构组织,使得组件便于理解。

    为什么?

  • 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。
  • 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。
  • 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。
  • 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。
  • 合理的 CSS 结构,如 BEMrscss - 详情?
  • 使用单文件 .vue 文件格式来组件代码。
  • 怎么做?

    组件结构化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <template lang="html">
    <div class="Ranger__Wrapper">
    <!-- ... -->
    </div>
    </template>

    <script type="text/javascript">
    export default {
    // 不要忘记了 name 属性
    name: 'RangeSlider',
    // 组合其它组件
    extends: {},
    // 组件属性、变量
    props: {
    bar: {}, // 按字母顺序
    foo: {},
    fooBar: {},
    },
    // 变量
    data() {},
    computed: {},
    // 使用其它组件
    components: {},
    // 方法
    watch: {},
    methods: {},
    // 生命周期函数
    beforeCreate() {},
    mounted() {},
    };
    </script>


    <style scoped>
    .Ranger__Wrapper { /* ... */ }
    </style>

    ↑ 回到目录

    组件事件命名

    Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么

    为什么?

  • 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。
  • 过于宽松的事件命名可能与 DOM 模板不兼容
  • 怎么做?

  • 事件名也使用连字符命名。
  • 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。
  • 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处
  • ↑ 回到目录

    避免 this.$parent

    Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发第一原则。因此你应该尽量避免使用 this.$parent

    为什么?

  • 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。
  • 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。
  • 怎么做?

  • 通过 props 将值传递给子组件。
  • 通过 props 传递回调函数给子组件来达到调用父组件方法的目的。
  • 通过在子组件触发事件来通知父组件。
  • ↑ 回到目录

    谨慎使用 this.$refs

    Vue.js 支持通过 ref 属性来访问其它组件和 HTML 元素。并通过 this.$refs 可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs

    为什么?

  • 组件必须是保持独立的,如果一个组件的 API 不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。
  • 组件的属性和事件必须足够的给大多数的组件使用。
  • 怎么做?

  • 提供良好的组件 API。
  • 总是关注于组件本身的目的。
  • 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的 API 不够通用,或者你可能需要一个新的组件来应对该需求。
  • 检查所有的 props 是否有缺失的,如果有提一个 issue 或是完善这个组件。
  • 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于 props 从忽视了这点。
  • Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。
  • 当遇到 props 和 events 难以实现的功能时,通过 this.$refs来实现。
  • 当需要操作 DOM 无法通过指令来做的时候可使用 this.$ref 而不是 JQuerydocument.getElement*document.queryElement
  • 1
    2
    3
    4
    5
    <!-- 推荐,并未使用 this.$refs -->
    <range :max="max"
    :min="min"
    @current-value="currentValue"
    :step="1"></range>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <!-- 使用 this.$refs 的适用情况-->
    <modal ref="basicModal">
    <h4>Basic Modal</h4>
    <button class="primary" @click="$refs.basicModal.close()">Close</button>
    </modal>
    <button @click="$refs.basicModal.open()">Open modal</button>

    <!-- Modal component -->
    <template>
    <div v-show="active">
    <!-- ... -->
    </div>
    </template>

    <script>
    export default {
    // ...
    data() {
    return {
    active: false,
    };
    },
    methods: {
    open() {
    this.active = true;
    },
    hide() {
    this.active = false;
    },
    },
    // ...
    };
    </script>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
    <template>
    <range :max="max"
    :min="min"
    ref="range"
    :step="1"></range>

    </template>

    <script>
    export default {
    // ...
    methods: {
    getRangeCurrentValue() {
    return this.$refs.range.currentValue;
    },
    },
    // ...
    };
    </script>

    ↑ 回到目录

    使用组件名作为样式作用域空间

    Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。

    为什么?

  • 给样式加上作用域空间可以避免组件样式影响外部的样式。
  • 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。
  • 怎么做?

    使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <style scoped>
    /* 推荐 */
    .MyExample { }
    .MyExample li { }
    .MyExample__item { }

    /* 避免 */
    .My-Example { } /* not scoped to component or module name, not BEM compliant */
    </style>

    ↑ 回到目录

    提供组件 API 文档

    使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。

    为什么?

  • 良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。
  • 组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。
  • 正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况。
  • README.md 是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。
  • 怎么做?

    在模块目录中添加 README.md 文件:

    1
    2
    3
    4
    range-slider/
    ├── range-slider.vue
    ├── range-slider.less
    └── README.md

    在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。

    Range slider

    功能

    range slider 组件可通过拖动的方式来设置一个给定范围内的数值。

    该模块使用 noUiSlider 来实现夸浏览器和 touch 功能的支持。

    如何使用

    <range-slider> 支持如下的自定义属性:

    attribute type description
    min
    Number
    可拖动的最小值.
    max
    Number
    可拖动的最大值.
    values
    Number[] optional
    包含最大值和最小值的数组. 如. values="[10, 20]". Defaults to [opts.min, opts.max].
    step
    Number optional
    增加减小的数值单位,默认为 1.
    on-slide
    Function optional
    用户拖动开始按钮或者结束按钮时的回调函数,函数接受 (values, HANDLE) 格式的参数。 如: on-slide={ updateInputs }, component.updateInputs = (values, HANDLE) => { const value = values[HANDLE]; }.
    on-end
    Function optional
    当用户停止拖动时触发的回调函数,函数接受 (values, HANDLE) 格式的参数。

    如需要自定义 slider 的样式可参考 noUiSlider 文档)

    ↑ 回到目录

    提供组件 demo

    添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。

    为什么?

  • demo 可以说明组件是独立可使用的。
  • demo 可以让开发者预览组件的功能效果。
  • demo 可以展示组件各种配置参数下的功能。
  • ↑ 回到目录

    对组件文件进行代码校验

    代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。

    为什么?

  • 保证所有的开发者使用同样的编码规范。
  • 更早的感知到语法错误。
  • 怎么做?

    为了校验工具能够校验 *.vue文件,你需要将代码编写在 <script>标签中,并使组件表达式简单化,因为校验工具无法理解行内表达式,配置校验工具可以访问全局变量 vue 和组件的 props

    ESLint

    ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。

    通过 .eslintrc 文件来配置 ESlint,这样 IED 可以更好的理解校验配置项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "extends": "eslint:recommended",
    "plugins": ["html"],
    "env": {
    "browser": true
    }
    ,

    "globals": {
    "opts": true,
    "vue": true
    }

    }

    运行 ESLint

    1
    eslint src/**/*.vue

    JSHint

    JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。

    通过 .jshintrc 文件来配置 ESlint,这样 IED 可以更好的理解校验配置项。

    1
    2
    3
    4
    {
    "browser": true,
    "predef": ["opts", "vue"]
    }

    运行 JSHint

    1
    jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/

    注:JSHint 不接受 vue 扩展名的文件,只支持 html


    如何提供帮助?

    Fork 和提 PR 以帮助我们改进或者可以给我们提 Issue

    译者

    本文来自:https://github.com/pablohpsilva/vuejs-component-style-guide/blob/master/README-CN.md#%E5%9F%BA%E4%BA%8E%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91

    ]]>
    Vue.js 组件编码规范


    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/10/13/Redis-installation-and-configuration-under-CentOS6.5/ 2016-10-13T01:16:13.000Z 2016-11-22T03:07:43.360Z 本文详细介绍redis单机单实例安装与配置,服务及开机自启动。

    一.安装redis

    1)下载redis安装包

    可去官网http://redis.io ,也可通过wget命令:

    1
    wget http://download.redis.io/redis-stable.tar.gz

    2)解压

    1
    tar –zxvf redis-stable.tar.gz

    3) 编译、安装

    1
    cd redis-stable

    1
    make

    如果提示 gcc command 不识别,请自行安装 gcc;

    如果提示couldn’t execute tcl : no such file or dicrectory,请自行安装tcl;

    如果提示

    请执行 make distclean,然后再 make

    Make成功之后,会在src目录下多出一些文件,如下

    可手动拷贝redis-server、redis-cli、redis-check-aof、redis-check-dump等至/usr/local/bin目录下,也可执行make install,此处执行make install

    可查看,/usr/local/bin 下已有这些文件。

    注意:若此时执行 redis-server –v (查看版本命令),若提示 redis-server command not found,则需要将 /usr/local/bin 目录加到环境变量,如何添加,此处不做详细介绍,可查看修改 /etc/profile,(查看环境变量命令:echo $PATH)

    正常如下

    至此,redis安装完成,接着配置。

    二.修改配置文件.conf

    1) 创建配置文件目录,dump file 目录,进程pid目录,log目录等

    配置文件一般放在 /etc/ 下,创建redis目录

    1
    2
    3
    cd /etc/

    mkdir redis

    ll 查看创建的redis目录

    ~

    dump file、进程pid、log目录等,一般放在 /var/ 目录下

    1
    2
    3
    4
    5
    6
    7
    cd /var/

    mkdir redis

    cd redis

    mkdir data log run

    至此,目录创建完毕

    2) 修改配置文件,配置参数

    首先拷贝解压包下的 redis.conf 文件至 /etc/redis

    查看 /etc/redis/redis.conf

    1
    2
    3
    cd /etc/redis/

    ll

    打开redis.conf文件

    修改端口(默认6379)

    修改pid目录为新建目录

    修改dump目录为新建目录

    修改log存储目录为新建目录

    3) 持久化

    默认rdb,可选择是否开启aof,若开启,修改配置文件 appendonly

    4) 启动redis,查看各目录下文件

    查看进程

    redis已启动

    查看dump, log, pid等

    发现只有日志,没有dump和pid信息,是因为当前redis服务仍然是console模式运行的,且没有数据存储操作

    停止redis服务,修改配置文件使得redis在 background 运行

    改成yes,保存,重启redis服务

    1
    2
    redis-cli shutdown
    redis-server /etc/redis/redis.conf

    查看pid信息,如下

    查看dump信息

    若配置了aof持久化方式,data目录下还会有aof的相关文件

    5) 客户端连接redis

    默认端口6379

    6) 至此,redis基础配置完毕,若有其他相关配置调整,可查找文档再修改

    三.服务及开机自启动

    1) 创建redis启动脚本

    拷贝解压包下 utils下redis 启动脚本至 /etc/init.d/

    1
    cp redis_init_script /etc/init.d/

    修改脚本名称(也可不修改)为redis

    查看 ll

    修改脚本pid及conf路径为实际路径

    生产环境下,配置时,配置文件、pid等最好加上端口标识,以便区分,如

    保存

    退出

    至此,在 /etc/init.d/ 目录下,已经可以通过 service redis start/stop 命令启动和关闭redis

    若在其他目录下,不能够使用这2个命令,请继续配置2),添加权限

    2) 给启动脚本添加权限

    1
    chmod +x /etc/init.d/redis

    实际命令,根据目录的不同,会不一样

    相应的删除权限是

    1
    chmod –x /etc/init.d/redis

    如果需要在开机的时候,redis服务自动启动,可继续3)

    3) 设置自启动

    1
    chkconfig redis on

    如果运行报错,提示

    是因为没有在启动脚本里加入redis启动优先级信息,可添加如下

    再次执行chkconfig redis on,成功

    至此,自启动配置完毕

    redius 一些小优化

    1) 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能以及最大的内存使用量。

    修改redis配置文件,redis.conf。

    1.注释掉原来的持久化规则

    1
    2
    3
    #save 900 1
    #save 300 10
    #save 60 10000

    2.设置为空

    1
    save ""

    然后重启redis服务即可。

    2) 不要让你的redis所在机器物理内存使用超过实际内存总理的3/5。

    redis.confmaxmemory 选项,该选项是告诉redis当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护redis不会因为使用了过多的物理内存后而导致swap,最终严重影响性能甚至崩溃。

    已本机为例,本机内存为16G,业务需求5G redisu, 设置如下:

    1
    2
    3
    maxmemory 5368709120
    或者
    maxmemory 5gb

    maxmemory 参数为 <bytes>, 计算公式 5G 1024 1024 * 1024 或者直接写 5gb

    设置了maxmemory的选项,redis内存使用达到上限。可以通过设置LRU算法来删除部分key,释放空间。默认是按照过期时间的,如果set时候没有加上过期时间就会导致数据写满maxmemory。

    如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。

    LRU是Least Recently Used 近期最少使用算法。

    如果设置了 maxmemory,一般都要设置过期策略。打开Redis的配置文件有如下描述,Redis有六种过期策略:

  • volatile-lru -> 根据LRU算法生成的过期时间来删除。
  • allkeys-lru -> 根据LRU算法删除任何key。
  • volatile-random -> 根据过期设置来随机删除key。
  • allkeys->random -> 无差别随机删。
  • volatile-ttl -> 根据最近过期时间来删除(辅以TTL)
  • noeviction -> 谁也不删,直接在写操作时返回错误。
  • 那么打开配置文件,添加如下一行,使用volatile-lru的过期策略:

    1
    maxmemory-policy volatile-lru

    保存文件退出,重启redis服务。

    3) info命令查看Redis内存使用情况

    查找列出的信息中 used_memory 字段:

    1
    2
    3
    4
    5
    6
    7
    # Memory
    used_memory:1324304
    used_memory_human:1.26M
    used_memory_rss:9826304
    used_memory_rss_human:9.37M
    used_memory_peak:1958208
    used_memory_peak_human:1.87M

    注意单位依然是 <bytes>,本机 redis 使用量为 1.26M

    4) 开启允许外部机器访问redis

    修改redis的配置文件,将所有bind信息全部屏蔽。

    1
    2
    3
    # bind 192.168.1.100 10.0.0.1
    # bind 192.168.1.8
    # bind 127.0.0.1

    修改完成后,需要重新启动redis服务。

    如果有防火墙相关配置需要开启redis相关端口。

    5) 设置访问密码

    修改redis的配置文件 requirepass 字段。

    1
    requirepass "uis.cc"

    redis相关文件路径信息:(通过上面配置获得)

    程序源路径: /temp/redis-3.2.4/
    程序路径: /usr/local/bin/redis-server
    配置文件路径: /etc/redis/redis.conf
    pidfile路径: /var/redis/run/redis.pid
    dump路径: /var/redis/data
    logfile路径: /var/redis/log/redis.log
    端口: 6379

    客户端快速链接:redis-cli -h 10.10.10.10 -p 6379

    服务方式启动 :service redis start/stop

    路径方式启动:
    启动redis命令: redis-server /etc/redis/redis.conf
    停止redis命令: redis-cli shutdown

    相关伟德伟诺官网:

    http://blog.csdn.net/ludonqin/article/details/47211109
    https://my.oschina.net/dxqr/blog/711578
    http://blog.csdn.net/tanzhang78/article/details/52073440

    ]]> 本文详细介绍redis单机单实例安装与配置,服务及开机自启动。

    一.安装redis

    1)下载redis安装包

    可去官网 <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/09/30/MongoDB-quick-start-finishing/ 2016-09-30T08:42:13.000Z 2016-09-30T07:51:20.741Z 首先启动数据库,控制台输入 mongo 进入数据库命令窗口。

    创建数据库

    MongoDB use DATABASE_NAME 用于创建数据库。该命令如果数据库不存在,将创建一个新的数据库, 否则将返回现有的数据库。

    语法:

    use DATABASE语句的基本语法如下:

    1
    > use DATABASE_NAME

    例子:

    如果想创建一个数据库名称为 <mydb>, 那么 use DATABASE 语句应该如下:

    1
    2
    > use mydb
    switched to db mydb

    要检查当前选择的数据库使用命令 db

    1
    2
    > db
    mydb

    如果想查询数据库列表,那么使用命令 show dbs.

    1
    2
    3
    > show dbs
    local 0.78125GB
    test 0.23012GB

    所创建的数据库(mydb)不存在于列表中。要显示的数据库,需要至少插入一个文档进去。

    1
    2
    3
    4
    5
    > db.movie.insert({"name":"yiibai tutorials"})
    > show dbs
    local 0.78125GB
    mydb 0.23012GB
    test 0.23012GB

    MongoDB的默认数据库是test。 如果没有创建任何数据库,那么集合将被保存在测试数据库。

    删除数据库

    MongoDB db.dropDatabase() 命令用于删除现有的数据库。

    语法

    dropDatabase()指令的基本语法如下:

    1
    db.dropDatabase()

    这将删除选定的数据库。如果没有选择任何数据库,那么它会删除默认的“test”数据库

    例子:

    如果想删除新的数据库 <mydb>, 那么 dropDatabase() 命令将如下所示:

    1
    2
    3
    4
    >use mydb
    switched to db mydb
    >db.dropDatabase()
    >{ "dropped" : "mydb", "ok" : 1 }

    创建集合

    MongoDB 的 db.createCollection(name, options) 用于创建集合。 在命令中, name 是要创建集合的名称。 Options 是一个文档,用于指定集合的配置。

    参数 类型 描述
    Name
    String
    要创建的集合的名称
    Options
    Document(可选)
    指定有关内存大小和索引选项

    选项参数是可选的,所以需要指定集合的唯一名字。

    语法

    createCollection()方法的基本语法如下

    1
    2
    3
    4
    >use test
    switched to db test
    >db.createCollection("mycollection")
    { "ok" : 1 }

    可以通过使用 show collections 命令来检查创建的集合

    1
    2
    3
    >show collections
    mycollection
    system.indexes

    选项列表

    字段 类型 描述
    capped
    Boolean(可选)
    如果为true,它启用上限集合。上限集合是一个固定大小的集合,当它达到其最大尺寸会自动覆盖最老的条目。 如果指定true,则还需要指定参数的大小。
    autoIndexID
    Boolean(可选)
    如果为true,自动创建索引_id字段。默认的值是 false.
    size
    number(可选)
    指定的上限集合字节的最大尺寸。如果capped 是true,那么还需要指定这个字段。
    max
    number(可选)
    指定上限集合允许的最大文件数。尽管插入文档,MongoDB首先检查字段集合的上限大小,那么它会检查最大字段。

    语法 :

    1
    2
    >db.createCollection("mycol", { capped : true, autoIndexID : true, size : 6142800, max : 10000 } )
    { "ok" : 1 }

    在MongoDB中并不需要创建集合。 当插入一些文档 MongoDB 会自动创建集合。

    1
    2
    3
    4
    5
    6
    >db.yiibai.insert({"name" : "yiibai"})
    >show collections
    mycol
    mycollection
    system.indexes
    yiibai

    删除集合

    MongoDB 的 db.collection.drop() 用于从数据库中删除集合。

    语法

    drop() 命令的基本语法如下

    1
    db.COLLECTION_NAME.drop()

    例子:

    下面给出的例子将删除给定名称的集合:mycollection

    1
    2
    3
    4
    >use mydb
    switched to db mydb
    >db.mycollection.drop()
    true

    插入文档

    将数据插入到MongoDB集合,需要使用MongoDB 的 insert() 方法。

    语法

    insert()命令的基本语法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    >db.COLLECTION_NAME.insert(document)
    例子
    >db.mycol.insert({
    _id: ObjectId(7df78ad8902c),
    title: 'MongoDB Overview',
    description: 'MongoDB is no sql database',
    by: 'yiibai tutorials',
    url: 'http://www.yiibai.com',
    tags: ['mongodb', 'database', 'NoSQL'],
    likes: 100
    })

    这里 mycol 是我们的集合名称,它是在之前的教程中创建。如果集合不存在于数据库中,那么MongoDB创建此集合,然后插入文档进去。

    在如果我们不指定_id参数插入的文档,那么 MongoDB 将为文档分配一个唯一的ObjectId

    _id 是12个字节十六进制数在一个集合的每个文档是唯一的。 12个字节被划分如下:

    1
    _id: ObjectId(4 bytes timestamp, 3 bytes machine id, 2 bytes process id, 3 bytes incrementer)

    要以单个查询插入多个文档,可以通过文档 insert() 命令的数组方式。

    例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    >db.post.insert([
    {
    title: 'MongoDB Overview',
    description: 'MongoDB is no sql database',
    by: 'yiibai tutorials',
    url: 'http://www.yiibai.com',
    tags: ['mongodb', 'database', 'NoSQL'],
    likes: 100
    },
    {
    title: 'NoSQL Database',
    description: 'NoSQL database doesn't have tables',
    by: 'yiibai tutorials',
    url: 'http://www.yiibai.com',
    tags: ['mongodb', 'database', 'NoSQL'],
    likes: 20,
    comments: [
    {
    user:'user1',
    message: 'My first comment',
    dateCreated: new Date(2013,11,10,2,35),
    like: 0
    }
    ]
    }
    ])

    查询文档

    要从集合查询MongoDB数据,需要使用MongoDB的 find()方法。

    语法

    find()方法的基本语法如下

    1
    >db.COLLECTION_NAME.find()

    find() 方法将在非结构化的方式显示所有的文件。 如果显示结果是格式化的,那么可以用pretty() 方法。

    语法

    1
    >db.mycol.find().pretty()

    ####例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >db.mycol.find().pretty()
    {
    "_id": ObjectId(7df78ad8902c),
    "title": "MongoDB Overview",
    "description": "MongoDB is no sql database",
    "by": "yiibai tutorials",
    "url": "http://www.yiibai.com",
    "tags": ["mongodb", "database", "NoSQL"],
    "likes": "100"
    }

    除了find()方法还有findOne()方法,仅返回一个文档。

    RDBMS Where子句等效于MongoDB
    查询文档在一些条件的基础上,可以使用下面的操作

    操作 语法 示例 RDBMS等效语句
    Equality
    {<key>:<value>}
    db.mycol.find({“by”:”yiibai tutorials”}).pretty()
    where by = ‘yiibai tutorials’
    Less Than
    {<key>:{$lt:<value>}}
    db.mycol.find({“likes”:{$lt:50}}).pretty()
    where likes < 50
    Less Than Equals
    {<key>:{$lte:<value>}}
    db.mycol.find({“likes”:{$lte:50}}).pretty()
    where likes <= 50
    Greater Than
    {<key>:{$gt:<value>}}
    db.mycol.find({“likes”:{$gt:50}}).pretty()
    where likes > 50
    Greater Than Equals
    {<key>:{$gte:<value>}}
    db.mycol.find({“likes”:{$gte:50}}).pretty()
    where likes >= 50
    Not Equals
    {<key>:{$ne:<value>}}
    db.mycol.find({“likes”:{$ne:50}}).pretty()
    where likes != 50

    AND 在 MongoDB 语法

    find()方法,如果您传递多个键通过”,”将它们分开,那么MongoDB对待它就如 AND 条件一样。基本语法如下所示:

    1
    >db.mycol.find({key1:value1, key2:value2}).pretty()

    例子

    下面给出的例子将显示所有教程含“yiibai tutorials”和其标题是“MongoDB Overview”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >db.mycol.find({"by":"yiibai tutorials","title": "MongoDB Overview"}).pretty()
    {
    "_id": ObjectId(7df78ad8902c),
    "title": "MongoDB Overview",
    "description": "MongoDB is no sql database",
    "by": "yiibai tutorials",
    "url": "http://www.yiibai.com",
    "tags": ["mongodb", "database", "NoSQL"],
    "likes": "100"
    }

    对于上面给出的例子相当于where子句:where by='yiibai tutorials' AND title='MongoDB Overview'。可以传递任何数目的键-值对在find子句。

    OR 在 MongoDB 语法

    要查询基于OR条件的文件,需要使用$or关键字。OR的基本语法如下所示:

    1
    2
    3
    4
    5
    6
    7
    >db.mycol.find(
    {
    $or: [
    {key1: value1}, {key2:value2}
    ]
    }
    ).pretty()

    例子

    下面给出的例子将显示所有撰写含有 ‘yiibai tutorials’ 或是标题为 ‘MongoDB Overview’ 的教程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >db.mycol.find({$or:[{"by":"tutorials point"},{"title": "MongoDB Overview"}]}).pretty()
    {
    "_id": ObjectId(7df78ad8902c),
    "title": "MongoDB Overview",
    "description": "MongoDB is no sql database",
    "by": "yiibai tutorials",
    "url": "http://www.yiibai.com",
    "tags": ["mongodb", "database", "NoSQL"],
    "likes": "100"
    }

    使用 AND 和 OR 在一起 例子

    下面给出的例子显示有喜欢数大于100 的文档,其标题要么是 ‘MongoDB Overview’ 或 ‘yiibai tutorials’. 等效于SQL的where子句:where likes>10 AND (by = 'yiibai tutorials' OR title = 'MongoDB Overview')

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >db.mycol.find("likes": {$gt:10}, $or: [{"by": "yiibai tutorials"}, {"title": "MongoDB Overview"}] }).pretty()
    {
    "_id": ObjectId(7df78ad8902c),
    "title": "MongoDB Overview",
    "description": "MongoDB is no sql database",
    "by": "yiibai tutorials",
    "url": "http://www.yiibai.com",
    "tags": ["mongodb", "database", "NoSQL"],
    "likes": "100"
    }

    更新文档

    MongoDB的update()save()方法用于更新文档到一个集合。 update()方法将现有的文档中的值更新,而save()方法使用传递到save()方法的文档替换现有的文档。

    MongoDB Update() 方法 语法

    update()方法的基本语法如下

    1
    >db.COLLECTION_NAME.update(SELECTIOIN_CRITERIA, UPDATED_DATA)

    例子

    考虑mycol集合有如下数据。

    1
    2
    3
    { "_id" : ObjectId(5983548781331adf45ec5), "title":"MongoDB Overview"}
    { "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
    { "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}

    下面的例子将设置其标题“MongoDB Overview”的文件为新标题为“New MongoDB Tutorial”

    1
    2
    3
    4
    5
    >db.mycol.update({'title':'MongoDB Overview'},{$set:{'title':'New MongoDB Tutorial'}})
    >db.mycol.find()
    { "_id" : ObjectId(5983548781331adf45ec5), "title":"New MongoDB Tutorial"}
    { "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
    { "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Tutorial Overview"}

    默认情况下,MongoDB将只更新单一文件,更新多,需要一个参数 ‘multi’ 设置为 true。

    1
    >db.mycol.update({'title':'MongoDB Overview'},{$set:{'title':'New MongoDB Tutorial'}},{multi:true})

    MongoDB Save() 方法

    save() 取代方法,通过 save()方法取代新文档

    语法

    mongodb 的 save()方法如下所示的基本语法:

    1
    2
    >db.COLLECTION_NAME.save({_id:ObjectId(),NEW_DATA})
    例子

    下面的例子将替换该文件_id '5983548781331adf45ec7'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >db.mycol.save(
    {
    "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai New Topic", "by":"Yiibai Yiibai"
    }
    )
    >db.mycol.find()
    { "_id" : ObjectId(5983548781331adf45ec5), "title":"Yiibai Yiibai New Topic", "by":"Yiibai Yiibai"}
    { "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
    { "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}

    删除文档

    MongoDB 的 remove() 方法用于从集合中删除文档。remove()方法接受两个参数。一个是标准缺失,第二是justOne标志

  • deletion criteria : 根据文件(可选)删除条件将被删除。

  • justOne : (可选)如果设置为true或1,然后取出只有一个文档。

  • 语法

    remove()方法的基本语法如下

    1
    >db.COLLECTION_NAME.remove(DELLETION_CRITTERIA)

    例子

    考虑mycol集合有如下数据。

    1
    2
    3
    { "_id" : ObjectId(5983548781331adf45ec5), "title":"MongoDB Overview"}
    { "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
    { "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}

    下面的例子将删除所有的文件,其标题为 ‘MongoDB Overview’

    1
    2
    3
    4
    >db.mycol.remove({'title':'MongoDB Overview'})
    >db.mycol.find()
    { "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
    { "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Toturials Overview"}

    只删除一个
    如果有多个记录,并要删除仅第一条记录,然后在 remove()方法设置参数 justOne

    1
    >db.COLLECTION_NAME.remove(DELETION_CRITERIA,1)

    删除所有文件

    如果没有指定删除条件,则MongoDB将从集合中删除整个文件。这相当于SQL的 truncate 命令。

    1
    2
    >db.mycol.remove()
    >db.mycol.find()

    ]]> 首先启动数据库,控制台输入 mongo 进入数据库命令窗口。

    创建数据库

    MongoDB use DATABASE_NAME 用于创建数据库。该命令如果数据库不存在,将创]]>

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/09/19/Installation-and-unloading-of-nodejs-on-Linux/ 2016-09-19T08:01:13.000Z 2016-09-19T08:13:14.079Z 安装

    yum 方式安装:

    编译和安装本地插件,你可能需要安装编译工具:

    1
    yum install gcc-c++ make

    在 centos 等 linux 系统中用 root 用户运行 RHEL, 来安装 Node.js v4:

    1
    curl --silent --location https://rpm.nodesource.com/setup_4.x | bash -

    Node.js v6:

    1
    curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -

    Node.js 0.10:

    1
    curl --silent --location https://rpm.nodesource.com/setup | bash -

    然后运行:

    1
    yum -y install nodejs

    检查一下版本号看是否安装成功

    1
    2
    node -v  
    v4.5.0

    编译安装:

    先切换到root用户一些需要的依赖包

    1
    yum install libtool automake autoconf gcc-c++ openssl-devel

    下载源代码自己编译以下代码中的tar.gz包根据node.js官网上的版本来定,比如我现在最新是 0.10.29

    1
    2
    3
    4
    5
    6
    7
    cd /usr/local/src
    wget https://nodejs.org/dist/v4.5.0/node-v4.5.0.tar.gz
    tar zxvf node-v4.5.0.tar.gz
    cd node-v4.5.0
    ./configure
    make
    make install

    检查一下版本号看是否安装成功

    1
    2
    node -v  
    v4.5.0

    卸载node.js

    用自带的包管理先删除一次

    1
    yum remove nodejs npm -y

    依次类推,看你的操作系统用什么包管理,如果你是用 brew 安装的 node 需要用 brew 先删除一次

    手动删除残留

    进入 /usr/local/lib 删除所有 nodenode_modules文件夹

    进入 /usr/local/include 删除所有 nodenode_modules 文件夹

    检查 ~ 文件夹里面的 local lib include文件夹,然后删除里面的所有nodenode_modules文件夹

    可以使用以下命令查找:

    1
    2
    find ~/ -name node  
    find ~/ -name node_modules

    进入 /usr/local/bin 删除 node 的可执行文件

    以下步骤可选:

    删除: /usr/local/bin/npm

    删除: /usr/local/share/man/man1/node.1

    删除: /usr/local/lib/dtrace/node.d

    删除: rm -rf /home/[homedir]/.npm

    删除: rm -rf /home/root/.npm

    ]]>
    安装

    yum 方式安装:

    编译和安装本地插件,你可能需要安装编译工具:

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/08/09/How-to-install-MongoDB-in-OS-X-MAC-system/ 2016-08-09T09:53:30.000Z 2016-09-30T07:48:24.233Z Homebrew 安装MongoDB:


    首先安装 Homebrew:

    1
    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

    如果已经安装过 Homebrew 升级到最新版本:

    1
    brew update

    安装 mongodb:

    1
    brew install mongodb

    安装成功后更具brew提示启动mongodb:

    后台启动 mongodb:

    1
    brew services start mongodb

    后台停止 mongodb:

    1
    brew services start mongodb

    当前控制台运行(关闭控制台窗口后mongodb会退出)

    1
    mongod --config /usr/local/etc/mongod.conf
    ]]> Homebrew 安装MongoDB:


    首先安装 Homebrew:

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/07/26/Express-Chinese-document-API/ 2016-07-26T14:56:31.000Z 2016-07-26T15:29:57.582Z Express 4.x API

    express()

    express()用来创建一个Express的程序。express()方法是express模块导出的顶层方法。

    1
    2
    var express = require('express');
    var app = express();

    Methods

    express.static(root, [options])

    express.static是Express中唯一的内建中间件。它以server-static模块为基础开发,负责托管 Express 应用内的静态资源。
    参数root为静态资源的所在的根目录。
    参数options是可选的,支持以下的属性:

    属性 描述 类型 默认值
    dotfiles
    是否响应点文件。供选择的值有”allow”,”deny”和”ignore”
    String
    “ignore”
    etag
    使能或者关闭etag
    Boolean
    true
    extensions
    设置文件延期回退
    Boolean
    true
    index
    发送目录索引文件。设置false将不发送。
    Mixed
    “index.html”
    lastModified
    设置文件在系统中的最后修改时间到Last-Modified头部。可能的取值有falsetrue
    Boolean
    true
    maxAge
    在Cache-Control头部中设置max-age属性,精度为毫秒(ms)或则一段ms format的字符串
    Number
    0
    redirect
    当请求的pathname是一个目录的时候,重定向到尾随”/“
    Boolean
    true
    setHeaders
    当响应静态文件请求时设置headers的方法
    Funtion

    如果你想获得更多关于使用中间件的细节,你可以查阅Serving static files in Express

    Application()

    app对象一般用来表示Express程序。通过调用Express模块导出的顶层的express()方法来创建它:

    1
    2
    3
    4
    5
    6
    7
    8
    var express = require('express');
    var app = express();

    app.get('/', function(req, res) {
    res.send('hello world!');
    });

    app.listen(3000);

    app对象具有以下的方法:

  • 路由HTTP请求;具体可以看app.METHODapp.param这两个例子。
  • 配置中间件;具体请看app.route
  • 渲染HTML视图;具体请看app.render
  • 注册模板引擎;具体请看app.engine
  • 它还有一些属性设置,这些属性可以改变程序的行为。获得更多的信息,可以查阅Application settings

    Properties

    app.locals

    app.locals对象是一个javascript对象,它的属性就是程序本地的变量。

    1
    2
    3
    4
    5
    app.locals.title
    // => 'My App'

    app.locals.email
    // => 'me@myapp.com'

    一旦设定,app.locals的各属性值将贯穿程序的整个生命周期,与其相反的是res.locals,它只在这次请求的生命周期中有效。

    在程序中,你可以在渲染模板时使用这些本地变量。它们是非常有用的,可以为模板提供一些有用的方法,以及app级别的数据。通过req.app.locals(具体查看req.app),Locals可以在中间件中使用。

    1
    2
    3
    app.locals.title = 'My App';
    app.locals.strftime = require('strftime');
    app.locals.email = 'me@myapp.com';

    app.mountpath

    app.mountpath属性是子程序挂载的路径模式。

    一个子程序是一个express的实例,其可以被用来作为路由句柄来处理请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var express = require('express');
    var app = express(); // the main app
    var admin = express(); // the sub app
    admin.get('/', function(req, res) {
    console.log(admin.mountpath); // /admin
    res.send('Admin Homepage');
    });

    app.use('/admin', admin); // mount the sub app

    它和req对象的baseUrl属性比较相似,除了req.baseUrl是匹配的URL路径,而不是匹配的模式。如果一个子程序被挂载在多条路径模式,app.mountpath就是一个关于挂载路径模式项的列表,如下面例子所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var admin = express();
    admin.get('/', function(req, res) {
    console.log(admin.mountpath); // ['adm*n', '/manager']
    res.send('Admin Homepage');
    });

    var secret = express();
    secret.get('/', function(req, res) {
    console.log(secret.mountpath); // /secr*t
    res.send('Admin secret');
    });

    admin.use('secr*t', secret); // load the 'secret' router on '/secr*t', on the 'admin' sub app
    app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager' , on the parent app

    Events

    app.on(‘mount’, callback(parent))

    当子程序被挂载到父程序时,mount事件被发射。父程序对象作为参数,传递给回调方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var admin = express();
    admin.on('mount', function(parent) {
    console.log('Admin Mounted');
    console.log(parent); // refers to the parent app
    });

    admin.get('/', function(req, res) {
    res.send('Admin Homepage');
    });

    app.use('/admin', admin);

    Methods

    app.all(path, callback[, callback …]

    app.all方法和标准的app.METHOD()方法相似,除了它匹配所有的HTTP动词。
    对于给一个特殊前缀映射一个全局的逻辑处理,或者无条件匹配,它是很有效的。例如,如果你把下面内容放在所有其他的路由定义的前面,它要求所有从这个点开始的路由需要认证和自动加载一个用户。记住这些回调并不是一定是终点:loadUser可以在完成了一个任务后,调用next()方法来继续匹配随后的路由。

    1
    app.all('*', requireAuthentication, loadUser);

    或者这种相等的形式:

    1
    2
    app.all('*', requireAuthentication);
    app.all('*', loadUser);

    另一个例子是全局的白名单方法。这个例子和前面的很像,然而它只是限制以/api开头的路径。

    1
    app.all('/api/*', requireAuthentication);

    app.delete(path, callback[, callback …])

    路由HTTP DELETE请求到有特殊回调方法的特殊的路径。获取更多的信息,可以查阅routing guide
    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果不能满足当前路由的处理条件,那么你可以传递控制到随后的路由。

    1
    2
    3
    app.delete('/', function(req, res) {
    res.send('DELETE request to homepage');
    });

    app.disable(name)

    设置类型为布尔的设置名为name的值为false,此处的nameapp settings table中各属性的一个。调用app.set('foo', false)和调用app.disable('foo')是等价的。
    比如:

    1
    2
    3
    app.disable('trust proxy');
    app.get('trust proxy');
    // => false

    app.disabled(name)

    返回true如果布尔类型的设置值name被禁用为false,此处的nameapp settings table中各属性的一个。

    1
    2
    3
    4
    5
    app.disabled('trust proxy');
    // => true
    app.enable('trust proxy');
    app.disabled('trust proxy');
    // => false

    app.enable(name)

    设置布尔类型的设置值nametrue,此处的nameapp settings table中各属性的一个。调用app.set('foo', true)和调用app.enable('foo')是等价的。

    1
    2
    3
    app.enable('trust proxy');
    app.get('trust proxy');
    // => true

    app.enabled(name)

    返回true如果布尔类型的设置值name被启动为true,此处的nameapp settings table中各属性的一个。

    1
    2
    3
    4
    5
    app.enabled('trust proxy');
    // => false
    app.enable('trust proxy');
    app.enabled('trust proxy');
    // => true

    app.engine(ext, callback)

    注册给定引擎的回调,用来渲染处理ext文件。
    默认情况下,Express需要使用require()来加载基于文件扩展的引擎。例如,如果你尝试渲染一个foo.jade文件,Express在内部调用下面的内容,同时缓存require()结果供随后的调用,来加速性能。

    1
    app.engine('jade', require('jade').__express);

    使用下面的方法对于那些没有提供开箱即用的.__express方法的模板,或者你希望使用不同的模板引擎扩展。
    比如,使用EJS模板引擎来渲染.html文件:

    1
    app.engine('html', require('ejs').renderFile);

    在这个例子中,EJS提供了一个.renderFile方法,这个方法满足了Express规定的签名规则:(path, options, callback),然而记住在内部它只是ejs.__express的一个别名,所以你可以在不做任何事的情况下直接使用.ejs扩展。
    一些模板引擎没有遵循这种规范,consolidate.js库映射模板引擎以下面的使用方式,所以他们可以无缝的和Express工作。

    1
    2
    3
    var engines = require('consolidate');
    app.engine('haml', engines.haml);
    app.engine('html', engines.hogan);

    app.get(name)

    获得设置名为name的app设置的值,此处的nameapp settings table中各属性的一个。
    如下:

    1
    2
    3
    4
    5
    6
    app.get('title');
    // => undefined

    app.set('title', 'My Site');
    app.get('title');
    // => 'My Site'

    app.get(path, callback [, callback …])

    路由HTTP GET请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide
    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。

    1
    2
    3
    app.get('/', function(req, res) {
    res.send('GET request to homepage');
    });

    app.listen(port, [hostname], [backlog], [callback])

    绑定程序监听端口到指定的主机和端口号。这个方法和Node中的http.Server.listen()是一样的。

    1
    2
    3
    var express = require('express');
    var app = express();
    app.listen(3000);

    通过调用express()返回得到的app实际上是一个JavaScript的Function,被设计用来作为一个回调传递给Node HTTP servers来处理请求。这样,其就可以很简便的基于同一份代码提供http和https版本,所以app没有从这些继承(它只是一个简单的回调)。

    1
    2
    3
    4
    5
    6
    var express = require('express');
    var https = require('https');
    var http = require('http');

    http.createServer(app).listen(80);
    https.createServer(options, app).listen(443);

    app.listen()方法是下面所示的一个便利的方法(只针对HTTP协议):

    1
    2
    3
    4
    app.listen = function() {
    var server = http.createServer(this);
    return server.listen.apply(server, arguments);
    };

    app.METHOD(path, callback [, callback …])

    路由一个HTTP请求,METHOD是这个请求的HTTP方法,比如GETPUTPOST等等,注意是小写的。所以,实际的方法是app.get()app.post()app.put()等等。下面有关于方法的完整的表。
    获取更多信息,请看routing guide
    Express支持下面的路由方法,对应与同名的HTTP方法:






  • checkout

  • connect

  • copy

  • delete

  • get

  • head

  • lock

  • merge

  • mkactivity





  • mkcol

  • move

  • m-search

  • notify

  • options

  • patch

  • post

  • propfind

  • proppatch





  • purege

  • put

  • report

  • search

  • subscribe

  • trace

  • unlock

  • unsubscribe





  • 如果使用上述方法时,导致了无效的javascript的变量名,可以使用中括号符号,比如,app['m-search']('/', function ...

    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没有满足当前路由的处理条件,那么传递控制到随后的路由。

    本API文档把使用比较多的HTTP方法app.get()app.postapp.put()app.delete()作为一个个单独的项进行说明。然而,其他上述列出的方法以完全相同的方式工作。

    app.all()是一个特殊的路由方法,它不属于HTTP协议中的规定的方法。它为一个路径加载中间件,其对所有的请求方法都有效。

    1
    2
    3
    4
    app.all('/secret', function (req, res) {
    console.log('Accessing the secret section...');
    next(); // pass control to the next handler
    });

    app.param([name], callback)

    给路由参数添加回调触发器,这里的name是参数名或者参数数组,function是回调方法。回调方法的参数按序是请求对象,响应对象,下个中间件,参数值和参数名。
    如果name是数组,会按照各个参数在数组中被声明的顺序将回调触发器注册下来。还有,对于除了最后一个参数的其他参数,在他们的回调中调用next()来调用下个声明参数的回调。对于最后一个参数,在回调中调用next()将调用位于当前处理路由中的下一个中间件,如果name只是一个string那就和它是一样的(就是说只有一个参数,那么就是最后一个参数,和数组中最后一个参数是一样的)。
    例如,当:user出现在路由路径中,你可以映射用户加载的逻辑处理来自动提供req.user给这个路由,或者对输入的参数进行验证。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    app.param('user', function(req, res, next, id) {
    User.find(id, function(error, user) {
    if (err) {
    next(err);
    }
    else if (user){
    req.user = user;
    } else {
    next(new Error('failed to load user'));
    }
    });
    });

    对于Param的回调定义的路由来说,他们是局部的。它们不会被挂载的app或者路由继承。所以,定义在app上的Param回调只有是在app上的路由具有这个路由参数时才起作用。
    在定义param的路由上,param回调都是第一个被调用的,它们在一个请求-响应循环中都会被调用一次并且只有一次,即使多个路由都匹配,如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    app.param('id', function(req, res, next, id) {
    console.log('CALLED ONLY ONCE');
    next();
    });

    app.get('/user/:id', function(req, res, next) {
    console.log('although this matches');
    next();
    });

    app.get('/user/:id', function(req, res) {
    console.log('and this mathces too');
    res.end();
    });

    GET /user/42,得到下面的结果:

    1
    CALLED ONLY ONCE
    although this matches
    and this matches too
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    app.param(['id', 'page'], function(req, res, next, value) {
    console.log('CALLED ONLY ONCE with', value);
    next();
    });

    app.get('/user/:id/:page', function(req. res, next) {
    console.log('although this matches');
    next();
    });

    app.get('/user/:id/:page', function (req, res, next) {
    console.log('and this matches too');
    res.end();
    });

    当执行GET /user/42/3,结果如下:

    1
    CALLED ONLY ONCE with 42
    CALLED ONLY ONCE with 3
    although this matches
    and this mathes too

    下面章节描述的app.param(callback)在v4.11.0之后被弃用。

    通过只传递一个回调参数给app.param(name, callback)方法,app.param(naem, callback)方法的行为将被完全改变。这个回调参数是关于app.param(name, callback)该具有怎样的行为的一个自定义方法,这个方法必须接受两个参数并且返回一个中间件。
    这个回调的第一个参数就是需要捕获的url的参数名,第二个参数可以是任一的JavaScript对象,其可能在实现返回一个中间件时被使用。
    这个回调方法返回的中间件决定了当URL中包含这个参数时所采取的行为。
    在下面的例子中,app.param(name, callback)参数签名被修改成了app.param(name, accessId)。替换接受一个参数名和回调,app.param()现在接受一个参数名和一个数字。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    var express = require('express');
    var app = express();

    app.param(function(param, option){
    return function(req, res, next, val) {
    if (val == option) {
    next();
    }
    else {
    res.sendStatus(403);
    }
    }
    });

    app.param('id', 1337);

    app.get('/user/:id', function(req, res) {
    res.send('Ok');
    });

    app.listen(3000, function() {
    console.log('Ready');
    });

    在这个例子中,app.param(name, callback)参数签名保持和原来一样,但是替换成了一个中间件,定义了一个自定义的数据类型检测方法来检测user id的类型正确性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    app.param(function(param, validator) {
    return function(req, res, next, val) {
    if (validator(val)) {
    next();
    }
    else {
    res.sendStatus(403);
    }
    }
    });

    app.param('id', function(candidate) {
    return !isNaN(parseFloat(candidate)) && isFinite(candidate);
    });

    在使用正则表达式来,不要使用.。例如,你不能使用/user-.+/来捕获user-gami,用使用[\\s\\S]或者[\\w\\>W]来代替(正如/user-[\\s\\S]+/)。

    1
    2
    3
    4
    5
    6
    7
    8
    //captures '1-a_6' but not '543-azser-sder'
    router.get('/[0-9]+-[[\\w]]*', function);

    //captures '1-a_6' and '543-az(ser"-sder' but not '5-a s'
    router.get('/[0-9]+-[[\\S]]*', function);


    //captures all (equivalent to '.*')
    router.get('[[\\s\\S]]*', function);

    app.path()

    通过这个方法可以得到app典型的路径,其是一个string

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var app = express()
    , blog = express()
    , blogAdmin = express();

    app.use('/blog', blog);
    app.use('/admin', blogAdmin);

    console.log(app.path()); // ''
    console.log(blog.path()); // '/blog'
    console.log(blogAdmin.path()); // '/blog/admin'

    如果app挂载很复杂下,那么这个方法的行为也会很复杂:一种更好用的方式是使用req.baseUrl来获得这个app的典型路径。

    app.post(path, callback, [callback …])

    路由HTTP POST请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide
    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。

    1
    2
    3
    app.post('/', function(req, res) {
    res.send('POST request to homepage')
    });

    app.put(path, callback, [callback …])

    路由HTTP PUT请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide
    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。

    1
    2
    3
    app.put('/', function(req, res) {
    res.send('PUT request to homepage');
    });

    app.render(view, [locals], callback)

    通过callback回调返回一个view渲染之后得到的HTML文本。它可以接受一个可选的参数,可选参数包含了这个view需要用到的本地数据。这个方法类似于res.render(),除了它不能把渲染得到的HTML文本发送给客户端。

    app.render()当作是可以生成渲染视图字符串的工具方法。在res.render()内部,就是使用的app.render()来渲染视图。

    如果使能了视图缓存,那么本地变量缓存就会保留。如果你想在开发的过程中缓存视图,设置它为true。在生产环境中,视图缓存默认是打开的。

    1
    2
    3
    4
    5
    6
    7
    app.render('email', function(err, html) {
    // ...
    });

    app.render('email', {name:'Tobi'}, function(err, html) {
    // ...
    });

    app.route(path)

    返回一个单例模式的路由的实例,之后你可以在其上施加各种HTTP动作的中间件。使用app.route()来避免重复路由名字(因此错字错误)–说的意思应该是使用app.router()这个单例方法来避免同一个路径多个路由实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var app = express();

    app.route('/events')
    .all(function(req, res, next) {
    // runs for all HTTP verbs first
    // think of it as route specific middleware!
    })
    .get(function(req, res, next) {
    res.json(...);
    })
    .post(function(req, res, next) {
    // maybe add a new event...
    })

    app.set(name, value)

    name设置项赋value值,nameapp settings table中属性的一项。
    对于一个类型是布尔型的属性调用app.set('foo', ture)等价于调用app.enable('foo')。同样的,调用app.set('foo', false)等价于调用app.disable('foo')
    可以使用app.get()来取得设置的值:

    1
    2
    app.set('title', 'My Site');
    app.get('title'); // 'My Site'

    Application Settings
    如果name是程序设置之一,它将影响到程序的行为。下边列出了程序中的设置。

    Property Type Value Default
    case sensitive routing
    Boolean
    启用区分大小写。
    不启用。对/Foo/foo处理是一样。
    env
    String
    环境模型。
    process.env.NODE_ENV(NODE_ENV环境变量)或者”development”
    etag
    Varied
    设置ETag响应头。可取的值,可以查阅etag options table。更多关于HTTP ETag header
    weak
    jsonp callback name
    String
    指定默认JSONP回调的名称。
    ?callback=
    json replacer
    String
    JSON替代品回调
    null
    json spaces
    Number
    当设置了这个值后,发送缩进空格美化过的JSON字符串。
    Disabled
    query parser
    Varied
    设置值为false来禁用query parser,或者设置simple,extended,也可以自己实现query string解析函数。simple基于Node原生的query解析,querystring
    “extend”
    strict routing
    Boolean
    启用严格的路由。
    不启用。对/foo/foo/的路由处理是一样。
    subdomain offset
    Number
    用来删除访问子域的主机点分部分的个数
    2
    trust proxy
    Varied
    指示app在一个反向代理的后面,使用x-Forwarded-*来确定连接和客户端的IP地址。注意:X-Forwarded-*头部很容易被欺骗,所有检测客户端的IP地址是靠不住的。trust proxy默认不启用。当启用时,Express尝试通过前端代理或者一系列代理来获取已连接的客户端IP地址。req.ips属性包含了已连接客户端IP地址的一个数组。为了启动它,需要设置在下面trust proxy options table中定义的值。trust proxy的设置实现使用了proxy-addr包。如果想获得更多的信息,可以查阅它的文档
    Disable
    views
    String or Array
    view所在的目录或者目录数组。如果是一个数组,将按在数组中的顺序来查找view
    process.cwd() + ‘/views’
    view cache
    Boolean
    启用视图模板编译缓存。
    在生成环境默认开启。
    view engine
    String
    省略时,默认的引擎被扩展使用。
    x-powered-by
    Boolean
    启用X-Powered-By:ExpressHTTP头部
    true

    Options for trust proxy settings
    查阅Express behind proxies来获取更多信息。


    TypeValue



    Boolean


    如果为true,客户端的IP地址作为X-Forwarded-*头部的最左边的条目。如果为false,可以理解为app直接与英特网直连,客户端的IP地址衍生自req.connection.remoteAddressfalse是默认设置。





    IP addresses


    一个IP地址,子网,或者一组IP地址,和委托子网。下面列出的是一个预先配置的子网名列表。



  • loopback - 127.0.0.1/8, ::1/128

  • linklocal - 169.254.0.0/16, fe80::/10

  • uniquelocal - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7


  • 使用下面方法中的任何一种来设置IP地址:


    app.set(‘trust proxy’, ‘loopback’) // specify a single subnet
    app.set(‘trust proxy’, ‘loopback, 123.123.123.123’) // specify a subnet and an address
    app.set(‘trust proxy’, ‘loopback, linklocal, uniquelocal’) // specify multiple subnets as CSV
    app.set(‘trust proxy’, [‘loopback’, ‘linklocal’, ‘uniquelocal’]) // specify multiple subnets as an array

    当指定IP地址之后, 这个IP地址或子网会被设置了这个IP地址或子网的app排除在外, 最靠近程序服务的没有委托的地址将被看做客户端IP地址。





    Number


    信任从反向代理到app中间小于等于n跳的连接为客户端。





    Function


    客户自定义委托代理信任机制。如果你使用这个,请确保你自己知道你在干什么。


    app.set(‘trust proxy’, function (ip) {
    if (ip === ‘127.0.0.1’ || ip === ‘123.123.123.123’) return true; // trusted IPs
    else return false;
    })




    Options for etag settings
    ETag功能的实现使用了etag包。如果你需要获得更多的信息,你可以查阅它的文档。


    TypeValue



    Boolean


    设置为true,启用weak ETag。这个是默认设置。设置false,禁用所有的ETag。




    String


    如果是strong,使能strong ETag。如果是weak,启用weak ETag。



    Function


    客户自定义ETag方法的实现. 如果你使用这个,请确保你自己知道你在干什么。


    app.set(‘etag’, function (body, encoding) {
    return generateHash(body, encoding); // consider the function is defined
    })




    app.use([path,], function [, function…])

    挂载中间件方法到路径上。如果路径未指定,那么默认为”/“。

    一个路由将匹配任何路径如果这个路径以这个路由设置路径后紧跟着”/“。比如:app.use('/appale', ...)将匹配”/apple”,”/apple/images”,”/apple/images/news”等。

    中间件中的req.originalUrlreq.baseUrlreq.path的组合,如下面的例子所示。

    1
    2
    3
    4
    5
    6
    app.use('/admin', function(req, res, next) {
    // GET 'http://www.example.com/admin/new'
    console.log(req.originalUrl); // '/admin/new'
    console.log(req.baseUrl); // '/admin'
    console.log(req.path);// '/new'
    });

    在一个路径上挂载一个中间件之后,每当请求的路径的前缀部分匹配了这个路由路径,那么这个中间件就会被执行。
    由于默认的路径为/,中间件挂载没有指定路径,那么对于每个请求,这个中间件都会被执行。

    1
    2
    3
    4
    5
    // this middleware will be executed for every request to the app.
    app.use(function(req, res, next) {
    console.log('Time: %d', Date.now());
    next();
    });

    中间件方法是顺序处理的,所以中间件包含的顺序是很重要的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // this middleware will not allow the request to  go beyond it
    app.use(function(req, res, next) {
    res.send('Hello World');
    });

    // this middleware will never reach this route
    app.use('/', function(req, res) {
    res.send('Welcome');
    });

    路径可以是代表路径的一串字符,一个路径模式,一个匹配路径的正则表达式,或者他们的一组集合。

    下面是路径的简单的例子。




    Type
    Example




    Path


    // will match paths starting with /abcd
    app.use(‘/abcd’, function (req, res, next) {
    next();
    })




    Path Pattern


    // will match paths starting with /abcd and /abd
    app.use(‘/abc?d’, function (req, res, next) {
    next();
    })

    // will match paths starting with /abcd, /abbcd, /abbbbbcd and so on
    app.use(‘/ab+cd’, function (req, res, next) {
    next();
    })

    // will match paths starting with /abcd, /abxcd, /abFOOcd, /abbArcd and so on
    app.use(‘/ab*cd’, function (req, res, next) {
    next();
    })

    // will match paths starting with /ad and /abcd
    app.use(‘/a(bc)?d’, function (req, res, next) {
    next();
    })




    Regular Expression


    // will match paths starting with /abc and /xyz
    app.use(/\/abc|\/xyz/, function (req, res, next) {
    next();
    })




    Array


    // will match paths starting with /abcd, /xyza, /lmn, and /pqr
    app.use([‘/abcd’, ‘/xyza’, /\/lmn|\/pqr/], function (req, res, next) {
    next();
    })




    方法可以是一个中间件方法,一系列中间件方法,一组中间件方法或者他们的集合。由于routerapp实现了中间件接口,你可以像使用其他任一中间件方法那样使用它们。




    Usage
    Example




    单个中间件

    你可以局部定义和挂载一个中间件。
    app.use(function (req, res, next) {
    next();
    })

    一个router是有效的中间件。
    var router = express.Router();
    router.get(‘/‘, function (req, res, next) {
    next();
    })
    app.use(router);

    一个Express程序是一个有效的中间件。
    var subApp = express();
    subApp.get(‘/‘, function (req, res, next) {
    next();
    })
    app.use(subApp);




    一系列中间件


    对于一个相同的挂载路径,你可以挂载超过一个的中间件。
    var r1 = express.Router();
    r1.get(‘/‘, function (req, res, next) {
    next();
    })

    var r2 = express.Router();
    r2.get(‘/‘, function (req, res, next) {
    next();
    })

    app.use(r1, r2);




    一组中间件


    在逻辑上使用一个数组来组织一组中间件。如果你传递一组中间件作为第一个或者唯一的参数,接着你需要指定挂载的路径。
    var r1 = express.Router();
    r1.get(‘/‘, function (req, res, next) {
    next();
    })

    var r2 = express.Router();
    r2.get(‘/‘, function (req, res, next) {
    next();
    })

    app.use(‘/‘, [r1, r2]);




    组合


    你可以组合下面的所有方法来挂载中间件。
    function mw1(req, res, next) { next(); }
    function mw2(req, res, next) { next(); }

    var r1 = express.Router();
    r1.get(‘/‘, function (req, res, next) { next(); });

    var r2 = express.Router();
    r2.get(‘/‘, function (req, res, next) { next(); });

    var subApp = express();
    subApp.get(‘/‘, function (req, res, next) { next(); });

    app.use(mw1, [mw2, r1, r2], subApp);




    下面是一些例子,在Express程序中使用express.static中间件。
    为程序托管位于程序目录下的public目录下的静态资源:

    1
    2
    // GET /style.css etc
    app.use(express.static(__dirname + '/public'));

    /static路径下挂载中间件来提供静态资源托管服务,只当请求是以/static为前缀的时候。

    1
    2
    // GET /static/style.css etc.
    app.use('/static', express.static(express.__dirname + '/public'));

    通过在设置静态资源中间件之后加载日志中间件来关闭静态资源请求的日志。

    1
    2
    app.use(express.static(__dirname + '/public'));
    app.use(logger());

    托管静态资源从不同的路径,但./public路径比其他更容易被匹配:

    1
    2
    3
    app.use(express.static(__dirname + '/public'));
    app.use(express.static(__dirname + '/files'));
    app.use(express.static(__dirname + '/uploads'));

    Request

    req对象代表了一个HTTP请求,其具有一些属性来保存请求中的一些数据,比如query stringparametersbodyHTTP headers等等。在本文档中,按照惯例,这个对象总是简称为req(http响应简称为res),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
    如下例子:

    1
    2
    3
    app.get('/user/:id', function(req, res) {
    res.send('user' + req.params.id);
    });

    其实你也可以这样写:

    1
    2
    3
    app.get('/user/:id', function(request, response) {
    response.send('user' + request.params.id);
    });

    Properties

    Express 4中,req.files默认在req对象中不再是可用的。为了通过req.files对象来获得上传的文件,你可以使用一个multipart-handling(多种处理的工具集)中间件,比如busboymulterformidablemultipratyconnect-multiparty或者pez

    req.app

    这个属性持有express程序实例的一个引用,其可以作为中间件使用。
    如果你按照这个模式,你创建一个模块导出一个中间件,这个中间件只在你的主文件中require()它,那么这个中间件可以通过req.app来获取express的实例。
    例如:

    1
    2
    // index.js
    app.get("/viewdirectory", require('./mymiddleware.js'));

    1
    2
    3
    4
    // mymiddleware.js
    module.exports = function(req, res) {
    res.send('The views directory is ' + req.app.get('views'));
    };

    req.baseUrl

    一个路由实例挂载的Url路径。

    1
    2
    3
    4
    5
    6
    var greet = express.Router();
    greet.get('/jp', function(req, res) {
    console.log(req.baseUrl); // greet
    res.send('Konichiwa!');
    });
    app.use('/greet', greet);

    即使你使用的路径模式或者一系列路径模式来加载路由,baseUrl属性返回匹配的字符串,而不是路由模式。下面的例子,greet路由被加载在两个路径模式上。

    1
    app.use(['/gre+t', 'hel{2}o'], greet); // load the on router on '/gre+t' and '/hel{2}o'

    当一个请求路径是/greet/jpbaseUrl/greet,当一个请求路径是/hello/jpreq.baseUrl/hello
    req.baseUrlapp对象的mountpath属性相似,除了app.mountpath返回的是路径匹配模式。

    req.body

    在请求的body中保存的是提交的一对对键值数据。默认情况下,它是undefined,当你使用比如body-parsermulter这类解析body数据的中间件时,它是填充的。
    下面的例子,给你展示了怎么使用body-parser中间件来填充req.body

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var app = require('express');
    var bodyParser = require('body-parser');
    var multer = require('multer');// v1.0.5
    var upload = multer(); // for parsing multipart/form-data
    app.use(bodyParser.json()); // for parsing application/json
    app.use(bodyParser.urlencoded({extended:true})); // for parsing application/x-www-form-urlencoded

    app.post('/profile', upload.array(), function(req, res, next) {
    console.log(req.body);
    res.json(req.body);
    });

    req.cookies

    当使用cookie-parser中间件的时候,这个属性是一个对象,其包含了请求发送过来的cookies。如果请求没有带cookies,那么其值为{}

    1
    2
    3
    // Cookie: name=tj
    req.cookies.name
    // => "tj"

    获取更多信息,问题,或者关注,可以查阅cookie-parser

    req.fresh

    指示这个请求是否是新鲜的。其和req.stale是相反的。
    cache-control请求头没有no-cache指示和下面中的任一一个条件为true,那么其就为true

  • if-modified-since请求头被指定,和last-modified请求头等于或者早于modified响应头。
  • if-none-match请求头是*
  • if-none-match请求头在被解析进它的指令之后,不匹配etag响应头(完全不知道什么鬼)。
  • 1
    2
    req.fresh
    // => true

    req.hostname

    包含了源自HostHTTP头部的hostname
    trust proxy设置项被设置为启用值,X-Forwarded-Host头部被使用来代替Host。这个头部可以被客户端或者代理设置。

    1
    2
    3
    // Host: "example.com"
    req.hostname
    // => "example.com"

    req.ips

    trust proxy设置项被设置为启用值,这个属性包含了一组在X-Forwarded-For请求头中指定的IP地址。不然,其就包含一个空的数组。这个头部可以被客户端或者代理设置。
    例如,如果X-Forwarded-Forclientproxy1proxy2req.ips就是["clinet", "proxy1", "proxy2"],这里proxy2就是最远的下游。

    req.originalUrl

    req.url不是一个原生的Express属性,它继承自Node’s http module

    这个属性很像req.url;然而,其保留了原版的请求链接,允许你自由地重定向req.url到内部路由。比如,app.use()mounting特点可以重定向req.url跳转到挂载点。

    1
    2
    3
    // GET /search?q=something
    req.originalUrl
    // => "/search?q=something"

    req.params

    一个对象,其包含了一系列的属性,这些属性和在路由中命名的参数名是一一对应的。例如,如果你有/user/:name路由,name属性可作为req.params.name。这个对象默认值为{}

    1
    2
    3
    // GET /user/tj
    req.params.name
    // => "tj"

    当你使用正则表达式来定义路由规则,捕获组的组合一般使用req.params[n],这里的n是第几个捕获租。这个规则被施加在无名通配符匹配,比如/file/*的路由:

    1
    2
    3
    // GET /file/javascripts/jquery.js
    req.params[0]
    // => "javascripts/jquery.js"

    req.path

    包含请求URL的部分路径。

    1
    2
    3
    // example.com/users?sort=desc
    req.path
    // => "/users"

    当在一个中间件中被调用,挂载点不包含在req.path中。你可以查阅app.use()获得跟多的信息。

    req.protocol

    请求的协议,一般为http,当启用TLS加密,则为https
    trust proxy设置一个启用的参数,如果存在X-Forwarded-Proto头部的话,其将被信赖和使用。这个头部可以被客户端或者代理设置。

    1
    2
    req.ptotocol
    // => "http"

    req.query

    一个对象,为每一个路由中的query string参数都分配一个属性。如果没有query string,它就是一个空对象,{}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // GET /search?q=tobi+ferret
    req.query.q
    // => "tobi ferret"

    // GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
    req.query.order
    // => "desc"

    req.query.shoe.color
    // => "blue"

    req.query.shoe.type
    // => "converse"

    req.route

    当前匹配的路由,其为一串字符。比如:

    1
    2
    3
    4
    app.get('/user/:id?', function userIdHandler(req, res) {
    console.log(req.route);
    res.send('GET')
    })

    前面片段的输出为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    { path:"/user/:id?"
    stack:
    [
    { handle:[Function:userIdHandler],
    name:"userIdHandler",
    params:undefined,
    path:undefined,
    keys:[],
    regexp:/^\/?$/i,
    method:'get'
    }
    ]
    methods:{get:true}
    }

    req.secure

    一个布尔值,如果建立的是TLS的连接,那么就为true。等价与:

    1
    'https' == req.protocol;

    req.signedCookies

    当使用cookie-parser中间件的时候,这个属性包含的是请求发过来的签名cookies,不签名的并且为使用做好了准备(这句真不知道怎么翻译了…)。签名cookies驻留在不同的对象中来体现开发者的意图;不然,一个恶意攻击可以被施加在req.cookie值上(它是很容易被欺骗的)。记住,签名一个cookie不是把它藏起来或者加密;而是简单的防止篡改(因为签名使用的加密是私人的)。如果没有发送签名的cookie,那么这个属性默认为{}

    1
    2
    3
    // Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
    req.signedCookies.user
    // => "tobi"

    为了获取更多的信息,问题或者关注,可以参阅cookie-parser

    req.stale

    指示这个请求是否是stale(陈旧的),它与req.fresh是相反的。更多信息,可以查看req.fresh

    1
    2
    req.stale
    // => true

    req.subdomains

    请求中域名的子域名数组。

    1
    2
    3
    // Host: "tobi.ferrets.example.com"
    req.subdomains
    // => ["ferrets", "tobi"]

    req.xhr

    一个布尔值,如果X-Requested-With的值为XMLHttpRequest,那么其为true,其指示这个请求是被一个客服端库发送,比如jQuery

    1
    2
    req.xhr
    // => true

    Methods

    req.accepts(types)

    检查这个指定的内容类型是否被接受,基于请求的Accept HTTP头部。这个方法返回最佳匹配,如果没有一个匹配,那么其返回undefined(在这个case下,服务器端应该返回406和”Not Acceptable”)。
    type值可以是一个单的MIME type字符串(比如application/json),一个扩展名比如json,一个逗号分隔的列表,或者一个数组。对于一个列表或者数组,这个方法返回最佳项(如果有的话)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Accept: text/html
    req.accepts('html');
    // => "html"

    // Accept: text/*, application/json
    req.accepts('html');
    // => "html"

    req.accepts('text/html');
    // => "text/html"

    req.accepts(['json', 'text']);
    // => "json"

    req.accepts('application/json');
    // => "application/json"

    // Accept: text/*, application/json
    req.accepts('image/png');
    req.accepts('png');
    // => undefined

    // Accept: text/*;q=.5, application/json
    req.accepts(['html', 'json']);
    // => "json"

    获取更多信息,或者如果你有问题或关注,可以参阅accepts

    req.acceptsCharsets(charset[, …])

    返回指定的字符集集合中第一个的配置的字符集,基于请求的Accept-CharsetHTTP头。如果指定的字符集没有匹配的,那么就返回false。
    获取更多信息,或者如果你有问题或关注,可以参阅accepts

    req.acceptsEncodings(encoding[, …])

    返回指定的编码集合中第一个的配置的编码,基于请求的Accept-EncodingHTTP头。如果指定的编码集没有匹配的,那么就返回false。
    获取更多信息,或者如果你有问题或关注,可以参阅accepts

    req.acceptsLanguages(lang [, …])

    返回指定的语言集合中第一个的配置的语言,基于请求的Accept-LanguageHTTP头。如果指定的语言集没有匹配的,那么就返回false。
    获取更多信息,或者如果你有问题或关注,可以参阅accepts

    req.get(field)

    返回指定的请求HTTP头部的域内容(不区分大小写)。ReferrerReferer的域内容可互换。

    1
    2
    3
    4
    5
    6
    7
    8
    req.get('Content-type');
    // => "text/plain"

    req.get('content-type');
    // => "text/plain"

    req.get('Something')
    // => undefined

    其是req.header(field)的别名。

    req.is(type)

    如果进来的请求的Content-type头部域匹配参数type给定的MIME type,那么其返回true。否则返回false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // With Content-Type: text/html; charset=utf-8
    req.is('html');
    req.is('text/html');
    req.is('text/*');
    // => true

    // When Content-Type is application/json
    req.is('json');
    req.is('application/json');
    req.is('application/*');
    // => true

    req.is('html');
    // => false

    获取更多信息,或者如果你有问题或关注,可以参阅type-is

    req.param(naem, [, defaultValue])

    过时的。可以在适合的情况下,使用req.paramsreq.body或者req.query

    返回当前参数name的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // ?name=tobi
    req.param('name')
    // => "tobi"

    // POST name=tobi
    req.param('name')
    // => "tobi"
    // /user/tobi for /user/:name
    req.param('name')
    // => "tobi"

    按下面给出的顺序查找:

  • req.params
  • req.body
  • req.query
  • 可选的,你可以指定一个defaultValue来设置一个默认值,如果这个参数在任何一个请求的对象中都不能找到。

    直接通过req.paramsreq.bodyreq.query取得应该更加的清晰-除非你确定每一个对象的输入。
    Body-parser中间件必须加载,如果你使用req.param()。详细请看req.body

    Response

    res对象代表了当一个HTTP请求到来时,Express程序返回的HTTP响应。在本文档中,按照惯例,这个对象总是简称为res(http请求简称为req),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
    例如:

    1
    2
    3
    app.get('/user/:id', function(req, res) {
    res.send('user' + req.params.id);
    });

    这样写也是一样的:

    1
    2
    3
    app.get('/user/:id', function(request, response) {
    response.send('user' + request.params.id);
    });

    Properties

    res.app

    这个属性持有express程序实例的一个引用,其可以在中间件中使用。
    res.app和请求对象中的req.app属性是相同的。

    res.headersSent

    布尔类型的属性,指示这个响应是否已经发送HTTP头部。

    1
    2
    3
    4
    5
    app.get('/', function(req, res) {
    console.log(res.headersSent); // false
    res.send('OK'); // send之后就发送了头部
    console.log(res.headersSent); // true
    });

    res.locals

    一个对象,其包含了响应的能够反应出请求的本地参数和因此只提供给视图渲染,在请求响应的周期内(如果有的话)–我要翻译吐了。否则,其和app.locals是一样的。(不知道翻译的什么…)
    这个参数在导出请求级别的信息是很有效的,这些信息比如请求路径,已认证的用户,用户设置等等。

    1
    2
    3
    4
    5
    app.use(function(req, res, next) {
    res.locals.user = req.user;
    res.locals.authenticated = !req.user.anonymous;
    next();
    });

    Methods

    res.append(field [, value])

    res.append()方法在Expresxs4.11.0以上版本才支持。

    在指定的field的HTTP头部追加特殊的值value。如果这个头部没有被设置,那么将用value新建这个头部。value可以是一个字符串或者数组。
    注意:在res.append()之后调用app.set()函数将重置前面设置的值。

    1
    2
    3
    res.append('Lind', ['<http://localhost>', '<http://localhost:3000>']);
    res.append('Set-Cookie', 'foo=bar;Path=/;HttpOnly');
    res.append('Warning', '199 Miscellaneous warning');

    res.attachment([filename])

    设置HTTP响应的Content-Disposition头内容为”attachment”。如果提供了filename,那么将通过res.type()获得扩展名来设置Content-Type,并且设置Content-Disposition内容为”filename=”parameter。

    1
    2
    3
    4
    5
    6
    res.attachment();
    // Content-Disposition: attachment

    res.attachment('path/to/logo.png');
    // Content-Disposition: attachment; filename="logo.png"
    // Content-Type: image/png

    res.cookie(name, value [,options])

    设置namevaluecookievalue参数可以是一串字符或者是转化为json字符串的对象。
    options是一个对象,其可以有下列的属性。

    属性 类型 描述
    domain
    String
    设置cookie的域名。默认是你本app的域名。
    expires
    Date
    cookie的过期时间,GMT格式。如果没有指定或者设置为0,则产生新的cookie。
    httpOnly
    Boolean
    这个cookie只能被web服务器获取的标示。
    maxAge
    String
    是设置过去时间的方便选项,其为过期时间到当前时间的毫秒值。
    path
    String
    cookie的路径。默认值是/
    secure
    Boolean
    标示这个cookie只用被HTTPS协议使用。
    signed
    Boolean
    指示这个cookie应该是签名的。

    res.cookie()所作的都是基于提供的options参数来设置Set-Cookie头部。没有指定任何的options,那么默认值在RFC6265中指定。

    使用实例:

    1
    2
    res.cookie('name', 'tobi', {'domain':'.example.com', 'path':'/admin', 'secure':true});
    res.cookie('remenberme', '1', {'expires':new Date(Date.now() + 90000), 'httpOnly':true});

    maxAge是一个方便设置过期时间的方便的选项,其以当前时间开始的毫秒数来计算。下面的示例和上面的第二条功效一样。

    1
    res.cookie('rememberme', '1', {'maxAge':90000}, "httpOnly":true);

    你可以设置传递一个对象作为value的参数。然后其将被序列化为Json字符串,被bodyParser()中间件解析。

    1
    2
    res.cookie('cart', {'items':[1, 2, 3]});
    res.cookie('cart', {'items':[1, 2, 3]}, {'maxAge':90000});

    当我们使用cookie-parser中间件的时候,这个方法也支持签名的cookie。简单地,在设置options时包含signed选项为true。然后res.cookie()将使用传递给cookieParser(secret)的密钥来签名这个值。

    1
    res.cookie('name', 'tobi', {'signed':true});

    res.clearCookie(name [,options])

    根据指定的name清除对应的cookie。更多关于options对象可以查阅res.cookie()

    1
    2
    res.cookie('name', 'tobi', {'path':'/admin'});
    res.clearCookie('name', {'path':'admin'});

    res.download(path, [,filename], [,fn])

    传输path指定文件作为一个附件。通常,浏览器提示用户下载。默认情况下,Content-Disposition头部”filename=”的参数为path(通常会出现在浏览器的对话框中)。通过指定filename参数来覆盖默认值。
    当一个错误发生时或者传输完成,这个方法将调用fn指定的回调方法。这个方法使用res.sendFile()来传输文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    res.download('/report-12345.pdf');

    res.download('/report-12345.pdf', 'report.pdf');

    res.download('report-12345.pdf', 'report.pdf', function(err) {
    // Handle error, but keep in mind the response may be partially-sent
    // so check res.headersSent
    if (err) {
    } else {
    // decrement a download credit, etc.
    }
    });

    res.end([data] [, encoding])

    结束本响应的过程。这个方法实际上来自Node核心模块,具体的是response.end() method of http.ServerResponse
    用来快速结束请求,没有任何的数据。如果你需要发送数据,可以使用res.send()res.json()这类的方法。

    1
    2
    res.end();
    res.status(404).end();

    res.format(object)

    进行内容协商,根据请求的对象中AcceptHTTP头部指定的接受内容。它使用req.accepts()来选择一个句柄来为请求服务,这些句柄按质量值进行排序。如果这个头部没有指定,那么第一个方法默认被调用。当不匹配时,服务器将返回406“Not Acceptable”,或者调用default回调。
    Content-Type请求头被设置,当一个回调方法被选择。然而你可以改变他,在这个方法中使用这些方法,比如res.set()或者res.type()
    下面的例子,将回复{"message":"hey"},当请求的对象中Accept头部设置成”application/json”或者”/json”(不过如果是`/*`,然后这个回复就是”hey”)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    res.format({
    'text/plain':function() {
    res.send('hey')'
    },
    'text/html':function() {
    res.send('<p>hey</p>');
    },
    'application/json':function() {
    res.send({message:'hey'});
    },
    'default':function() {
    res.status(406).send('Not Acceptable');
    }
    })

    除了规范化的MIME类型之外,你也可以使用拓展名来映射这些类型来避免冗长的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    res.format({
    text:function() {
    res.send('hey');
    },
    html:function() {
    res.send('<p>hey</p>');
    },
    json:function() {
    res.send({message:'hey'});
    }
    })

    res.get(field)

    返回field指定的HTTP响应的头部。匹配是区分大小写。

    1
    2
    res.get('Content-Type');
    // => "text/plain"

    res.json([body])

    发送一个json的响应。这个方法和将一个对象或者一个数组作为参数传递给res.send()方法的效果相同。不过,你可以使用这个方法来转换其他的值到json,例如nullundefined。(虽然这些都是bv伟德登录入口上无效的JSON)。

    1
    2
    3
    res.json(null);
    res.json({user:'tobi'});
    res.status(500).json({error:'message'});

    res.jsonp([body])

    发送一个json的响应,并且支持JSONP。这个方法和res.json()效果相同,除了其在选项中支持JSONP回调。

    1
    2
    3
    4
    5
    6
    7
    8
    res.jsonp(null)
    // => null

    res.jsonp({user:'tobi'})
    // => {"user" : "tobi"}

    res.status(500).jsonp({error:'message'})
    // => {"error" : "message"}

    默认情况下,jsonp的回调方法简单写作callback。可以通过jsonp callback name设置来重写它。
    下面是一些例子使用JSONP响应,使用相同的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ?callback=foo
    res.jsonp({user:'tobo'})
    // => foo({"user":"tobi"})

    app.set('jsonp callback name', 'cb')

    // ?cb=foo
    res.status(500).jsonp({error:'message'})
    // => foo({"error":"message"})

    res.links(links)

    连接这些linkslinks是以传入参数的属性形式提供,连接之后的内容用来填充响应的Link HTTP头部。

    1
    2
    3
    4
    res.links({
    next:'http://api.example.com/users?page=2',
    last:'http://api.example.com/user?page=5'
    });

    效果:

    1
    2
    Link:<http://api.example.com/users?page=2>;rel="next",
    <http://api.example.com/users?page=5>;rel="last"

    res.location(path)

    设置响应的LocationHTTP头部为指定的path参数。

    1
    2
    3
    res.location('/foo/bar');
    res.location('http://example.com');
    res.location('back');

    path参数为back时,其具有特殊的意义,其指定URL为请求对象的Referer头部指定的URL。如果请求中没有指定,那么其即为”/“。

    Express传递指定的URL字符串作为回复给浏览器响应中的Location头部的值,不检测和操作,除了当是back这个case时。浏览器有推导预期URL从当前的URL或者指定的URL,和在Location指定的URL的责任;相应地重定向它。(我也不知道翻译的什么…)

    res.redirect([status,] path)

    重定向来源于指定path的URL,以及指定的HTTP status codestatus。如果你没有指定status,status code默认为”302 Found”。

    1
    2
    3
    4
    res.redirect('/foo/bar');
    res.redirect('http://example.com');
    res.redirect(301, 'http://example.com');
    res.redirect('../login');

    重定向也可以是完整的URL,来重定向到不同的站点。

    1
    res.redirect('http://google.com');

    重定向也可以相对于主机的根路径。比如,如果程序的路径为http://example.com/admin/post/new,那么下面将重定向到http://example.com/admim:

    1
    res.redirect('/admin');

    重定向也可以相对于当前的URL。比如,来之于http://example.com/blog/admin/(注意结尾的/),下面将重定向到http://example.com/blog/admin/post/new

    1
    res.redirect('post/new');

    如果来至于http://example.com/blog/admin(没有尾部/),重定向post/new,将重定向到http://example.com/blog/post/new。如果你觉得上面很混乱,可以把路径段认为目录(有’/‘)或者文件,这样是可以的。相对路径的重定向也是可以的。如果你当前的路径为http://example.com/admin/post/new,下面的操作将重定向到http://example.com/admin/post

    1
    res.redirect('..');

    back将重定向请求到referer,当没有referer的时候,默认为/

    1
    res.redirect('back');

    res.render(view [, locals] [, callback])

    渲染一个视图,然后将渲染得到的HTML文档发送给客户端。可选的参数为:

  • locals,定义了视图本地参数属性的一个对象。
  • callback,一个回调方法。如果提供了这个参数,render方法将返回错误和渲染之后的模板,并且不自动发送响应。当有错误发生时,可以在这个回调内部,调用next(err)方法。
  • 本地变量缓存使能视图缓存。在开发环境中缓存视图,需要手动设置为true;视图缓存在生产环境中默认开启。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    // send the rendered view to the client
    res.render('index');

    // if a callback is specified, the render HTML string has to be sent explicitly

    res.render('index', function(err, html) {
    res.send(html);
    });

    // pass a local variable to the view
    res.render('user', {name:'Tobi'}, function(err, html) {
    // ...
    });

    res.send([body])

    发送HTTP响应。
    body参数可以是一个Buffer对象,一个字符串,一个对象,或者一个数组。比如:

    1
    2
    3
    4
    5
    res.send(new Buffer('whoop'));
    res.send({some:'json'});
    res.send('<p>some html</p>');
    res.status(404).send('Sorry, we cannot find that!');
    res.status(500).send({ error: 'something blew up' });

    对于一般的非流请求,这个方法可以执行许多有用的的任务:比如,它自动给Content-LengthHTTP响应头赋值(除非先前定义),也支持自动的HEAD和HTTP缓存更新。
    当参数是一个Buffer对象,这个方法设置Content-Type响应头为application/octet-stream,除非事先提供,如下所示:

    1
    2
    res.set('Content-Type', 'text/html');
    res.send(new Buffer('<p>some html</p>'));

    当参数是一个字符串,这个方法设置Content-Type响应头为text/html

    1
    res.send('<p>some html</p>');

    当参数是一个对象或者数组,Express使用JSON格式来表示:

    1
    2
    res.send({user:'tobi'});
    res.send([1, 2, 3]);

    res.sendFile(path [, options] [, fn])

    res.sendFile()Express v4.8.0开始支持。

    传输path指定的文件。根据文件的扩展名设置Content-TypeHTTP头部。除非在options中有关于root的设置,path一定是关于文件的绝对路径。
    下面的表提供了options参数的细节:

    属性 描述 默认值 可用版本
    maxAge
    设置Cache-Controlmax-age属性,格式为毫秒数,或者是ms format的一串字符串
    0
    root
    相对文件名的根目录
    lastModified
    设置Last-Modified头部为此文件在系统中的最后一次修改时间。设置false来禁用它
    Enable
    4.9.0+
    headers
    一个对象,包含了文件所在的sever的HTTP头部。(不知道怎么翻译了)
    dotfiles
    是否支持点开头文件名的选项。可选的值”allow”,”deny”,”ignore”
    “ignore”

    当传输完成或者发生了什么错误,这个方法调用fn回调方法。如果这个回调参数指定了和一个错误发生,回调方法必须明确地通过结束请求-响应循环或者传递控制到下个路由来处理响应过程。
    下面是使用了所有参数的使用res.sendFile()的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    app.get('/file/:name', function(req, res, next) {

    var options = {
    root:__dirname + '/public',
    dotfile:'deny',
    headers:{
    'x-timestamp':Date.now(),
    'x-sent':true
    }
    };

    var fileName = req.params.name;
    res.sendFile(fileName, options, function(err) {
    if (err) {
    console.log(err);
    res.status(err.status).end();
    }
    else {
    console.log('sent', fileName);
    }
    });



    });

    res.sendFile提供了文件服务的细粒度支持,如下例子说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    app.get('/user/:uid/photos/:file', function(req, res) {
    var uid = req.params.uid
    , file = req.params.file;


    req.user.mayViewFilesFrom(uid, function(yes) {
    if (yes) {
    res.sendFile('/upload/' + uid + '/' + file);
    }
    else {
    res.status(403).send('Sorry! you cant see that.');
    }
    });

    })

    获取更多信息,或者你有问题或者关注,可以查阅send

    res.sendStatus(statusCode)

    设置响应对象的HTTP status codestatusCode并且发送statusCode的相应的字符串形式作为响应的Body。

    1
    2
    3
    4
    res.sendStatus(200); // equivalent to res.status(200).send('OK');
    res.sendStatus(403); // equivalent to res.status(403).send('Forbidden');
    res.sendStatus(404); // equivalent to res.status(404).send('Not Found');
    res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error')

    如果一个不支持的状态被指定,这个HTTP status依然被设置为statusCode并且用这个code的字符串作为Body。

    1
    res.sendStatus(2000); // equivalent to res.status(2000).send('2000');

    More about HTTP Status Codes

    res.set(field [, value])

    设置响应对象的HTTP头部fieldvalue。为了一次设置多个值,那么可以传递一个对象为参数。

    1
    2
    3
    4
    5
    6
    7
    res.set('Content-Type', 'text/plain');

    res.set({
    'Content-Type':'text/plain',
    'Content-Length':'123',
    'ETag':'123456'
    })

    其和res.header(field [,value])效果一致。

    res.status(code)

    使用这个方法来设置响应对象的HTTP status。其是Node中response.statusCode的一个连贯性的别名。

    1
    2
    3
    res.status(403).end();
    res.status(400).send('Bad Request');
    res.status(404).sendFile('/absolute/path/to/404.png');

    res.type(type)

    设置Content-TypeHTTP头部为MIME type,如果这个指定的type能够被mime.lookup确定。如果type包含/字符,那么设置Content-Typetype(我已经晕了)。

    1
    2
    3
    4
    5
    res.type('.html');              // => 'text/html'
    res.type('html'); // => 'text/html'
    res.type('json'); // => 'application/json'
    res.type('application/json'); // => 'application/json'
    res.type('png'); // => image/png:

    res.vary(field)

    设置Vary响应头为field,如果已经不在那里。(不懂什么意思)

    1
    res.vary('User-Agent').render('docs');

    Router

    一个router对象是一个单独的实例关于中间件和路由。你可以认为其是一个”mini-application”(迷你程序),其具有操作中间件和路由方法的能力。每个Express程序有一个内建的app路由。
    路由自身表现为一个中间件,所以你可以使用它作为app.use()方法的一个参数或者作为另一个路由的use()的参数。
    顶层的express对象有一个Router()方法,你可以使用Router()来创建一个新的router对象。

    Router([options])

    如下,可以创建一个路由:

    1
    var router = express.Router([options]);

    options参数可以指定路由的行为,其有下列选择:

    属性 描述 默认值 可用性
    caseSensitive
    是否区分大小写
    默认不启用。对待/Foo/foo一样。
    mergeParams
    保存父路由的res.params。如果父路由参数和子路由参数冲突,子路由参数优先。
    false
    4.5.0+
    strict
    使能严格路由。
    默认不启用,/foo/foo/被路由一样对待处理

    你可以将router当作一个程序,可以在其上添加中间件和HTTP路由方法(例如getputpost等等)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // invoked for any requests passed to this router
    router.use(function(req, res, next) {
    // .. some logic here .. like any other middleware
    next();
    });

    // will handle any request that ends in /events
    // depends on where the router is "use()'d"
    router.get('/events', function(req, res, next) {
    // ..
    });

    你可以在一个特别的根URL上挂载一个路由,这样你就以将你的各个路由放到不同的文件中或者甚至是mini的程序。

    1
    2
    // only requests to /calendar/* will be sent to our "router"
    app.use('/calendar', router);

    Methods

    router.all(path, [callback, …] callback)

    这个方法和router.METHOD()方法一样,除了这个方法会匹配所有的HTTP动作。
    这个方法对想映射全局的逻辑处理到特殊的路径前缀或者任意匹配是十分有用的。比如,如果你放置下面所示的这个路由在其他路由的前面,那么其将要求从这个点开始的所有的路由进行验证操作和自动加载用户信息。记住,这些全局的逻辑操作,不需要结束请求响应周期:loaduser可以执行一个任务,然后调用next()来将执行流程移交到随后的路由。

    1
    router.all('*', requireAuthentication, loadUser);

    相等的形式:

    1
    2
    router.all('*', requireAuthentication)
    router.all('*', loadUser);

    这是一个白名单全局功能的例子。这个例子很像前面的,不过其仅仅作用于以/api开头的路径:

    1
    router.all('/api/*', requireAuthentication);

    router.METHOD(path, [callback, …] callback)

    router.METHOD()方法提供了路由方法在Express中,这里的METHOD是HTTP方法中的一个,比如GETPUTPOST等等,但router中的METHOD是小写的。所以,实际的方法是router.get()router.put()router.post()等等。
    你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没有满足当前路由的处理条件,那么传递控制到随后的路由。
    下面的片段可能说明了最简单的路由定义。Experss转换path字符串为正则表达式,用于内部匹配传入的请求。在匹配的时候,是不考虑Query strings,例如,”GET /“将匹配下面的路由,”GET /?name=tobi”也是一样的。

    1
    2
    3
    router.get('/', function(req, res) {
    res.send('Hello World');
    });

    如果你对匹配的path有特殊的限制,你可以使用正则表达式,例如,下面的可以匹配”GET /commits/71dbb9c”和”GET /commits/71bb92..4c084f9”。

    1
    2
    3
    4
    5
    router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res) {
    var from = req.params[0];
    var to = req.params[1];
    res.send('commit range ' + from + '..' + to);
    });

    router.param(name, callback)

    给路由参数添加回调触发器,这里的name是参数名,function是回调方法。回调方法的参数按序是请求对象,响应对象,下个中间件,参数值和参数名。虽然name在bv伟德登录入口上是可选的,但是自Express V4.11.0之后版本不推荐使用(见下面)。

    不像app.param()router.param()不接受一个数组作为路由参数。

    例如,当:user出现在路由路径中,你可以映射用户加载的逻辑处理来自动提供req.user给这个路由,或者对输入的参数进行验证。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    router.param('user', function(req, res, next, id) {
    User.find(id, function(error, user) {
    if (err) {
    next(err);
    }
    else if (user){
    req.user = user;
    } else {
    next(new Error('failed to load user'));
    }
    });
    });

    对于Param的回调定义的路由来说,他们是局部的。它们不会被挂载的app或者路由继承。所以,定义在router上的param回调只有是在router上的路由具有这个路由参数时才起作用。
    在定义param的路由上,param回调都是第一个被调用的,它们在一个请求-响应循环中都会被调用一次并且只有一次,即使多个路由都匹配,如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    router.param('id', function(req, res, next, id) {
    console.log('CALLED ONLY ONCE');
    next();
    });

    router.get('/user/:id', function(req, res, next) {
    console.log('although this matches');
    next();
    });

    router.get('/user/:id', function(req, res) {
    console.log('and this mathces too');
    res.end();
    });

    GET /user/42,得到下面的结果:

    1
    CALLED ONLY ONCE
    although this matches
    and this matches too

    下面章节描述的router.param(callback)在v4.11.0之后被弃用。

    通过只传递一个回调参数给router.param(name, callback)方法,router.param(naem, callback)方法的行为将被完全改变。这个回调参数是关于router.param(name, callback)该具有怎样的行为的一个自定义方法,这个方法必须接受两个参数并且返回一个中间件。
    这个回调的第一个参数就是需要捕获的url的参数名,第二个参数可以是任一的JavaScript对象,其可能在实现返回一个中间件时被使用。
    这个回调方法返回的中间件决定了当URL中包含这个参数时所采取的行为。
    在下面的例子中,router.param(name, callback)参数签名被修改成了router.param(name, accessId)。替换接受一个参数名和回调,router.param()现在接受一个参数名和一个数字。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    var express = require('express');
    var app = express();
    var router = express.Router();

    router.param(function(param, option){
    return function(req, res, next, val) {
    if (val == option) {
    next();
    }
    else {
    res.sendStatus(403);
    }
    }
    });

    router.param('id', 1337);

    router.get('/user/:id', function(req, res) {
    res.send('Ok');
    });

    app.use(router);

    app.listen(3000, function() {
    console.log('Ready');
    });

    在这个例子中,router.param(name. callback)参数签名保持和原来一样,但是替换成了一个中间件,定义了一个自定义的数据类型检测方法来检测user id的类型正确性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    router.param(function(param, validator) {
    return function(req, res, next, val) {
    if (validator(val)) {
    next();
    }
    else {
    res.sendStatus(403);
    }
    }
    });

    router.param('id', function(candidate) {
    return !isNaN(parseFloat(candidate)) && isFinite(candidate);
    });

    router.route(path)

    返回一个单例模式的路由的实例,之后你可以在其上施加各种HTTP动作的中间件。使用app.route()来避免重复路由名字(因此错字错误)–后面这句不知道说的什么鬼,大概的意思就是避免同一个路径有两个路由实例。
    构建在上面的router.param()例子之上,下面的代码展示了怎么使用router.route()来指定各种HTTP方法的处理句柄。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    var router = express.Router();

    router.param('user_id', function(req, res, next, id) {
    // sample user, would actually fetch from DB, etc...
    req.user = {
    id:id,
    name:"TJ"
    };
    next();
    });

    router.route('/users/:user_id')
    .all(function(req, res, next) {
    // runs for all HTTP verbs first
    // think of it as route specific middleware!
    next();
    })
    .get(function(req, res, next) {
    res.json(req.user);
    })
    .put(function(req, res, next) {
    // just an example of maybe updating the user
    req.user.name = req.params.name;
    // save user ... etc
    res.json(req.user);
    })
    .post(function(req, res, next) {
    next(new Error('not implemented'));
    })
    .delete(function(req, res, next) {
    next(new Error('not implemented'));
    })

    这种方法重复使用单个/usrs/:user_id路径来添加了各种的HTTP方法。

    router.use([path], [function, …] function)

    给可选的path参数指定的路径挂载给定的中间件方法,未指定path参数,默认值为/
    这个方法类似于app.use()。一个简单的例子和用例在下面描述。查阅app.use()获得更多的信息。
    中间件就像一个水暖管道,请求在你定义的第一个中间件处开始,顺着中间件堆栈一路往下,如果路径匹配则处理这个请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var express = require('express');
    var app = express();
    var router = express.Router();


    // simple logger for this router`s requests
    // all requests to this router will first hit this middleware

    router.use(function(req, res, next) {
    console.log('%s %s %s', req.method, req.url, req.path);
    next();
    })

    // this will only be invoked if the path starts with /bar form the mount ponit
    router.use('/bar', function(req, res, next) {
    // ... maybe some additional /bar logging ...
    next();
    })

    // always be invoked
    router.use(function(req, res, next) {
    res.send('hello world');
    })

    app.use('/foo', router);

    app.listen(3000);

    对于中间件function,挂载的路径是被剥离的和不可见的。关于这个特性主要的影响是对于不同的路径,挂载相同的中间件可能对代码不做改动,尽管其前缀已经改变。
    你使用router.use()定义中间件的顺序很重要。中间们是按序被调用的,所以顺序决定了中间件的优先级。例如,通常日志是你将使用的第一个中间件,以便每一个请求都被记录。

    1
    2
    3
    4
    5
    6
    7
    var logger = require('morgan');

    router.use(logger());
    router.use(express.static(__dirname + '/public'));
    router.use(function(req, res) {
    res.send('Hello');
    });

    现在为了支持你不希望记录静态文件请求,但为了继续记录那些定义在logger()之后的路由和中间件。你可以简单的将static()移动到前面来解决:

    1
    2
    3
    4
    5
    router.use(express.static(__dirname + '/public'));
    router.use(logger());
    router.use(function(req, res){
    res.send('Hello');
    });

    另外一个确凿的例子是从不同的路径托管静态文件,你可以将./public放到前面来获得更高的优先级:

    1
    2
    3
    app.use(express.static(__dirname + '/public'));
    app.use(express.static(__dirname + '/files'));
    app.use(express.static(__dirname + '/uploads'));

    router.use()方法也支持命名参数,以便你的挂载点对于其他的路由而言,可以使用命名参数来进行预加载,这样做是很有益的。

    本文来自:https://github.com/ZhouBox/express_api_4.x_chinese

    ]]>
    Express 4.x API

    express()

    express()用来创建一个Express的程序。express()

    <![CDATA[uis.cc - 前端技术博客]]> http://www.uis.cc/2016/07/06/HTML-5-live-video-live-one-stop-literacy/ 2016-07-06T03:02:22.000Z 2016-07-06T03:22:03.766Z 视频直播这么火,再不学就 out 了。

    为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的bv伟德登录入口点,包括但不限于前端bv伟德登录入口。

    1. H5 到底能不能做视频直播?

    当然可以, H5 火了这么久,涵盖了各个方面的bv伟德登录入口。

    对于视频录制,可以使用强大的 webRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的bv伟德登录入口,缺点是只在 PC 的 chrome 上支持较好,移动端支持不太理想。

    对于视频播放,可以使用 HLS(HTTP Live Streaming)协议播放直播流, ios 和 android 都天然支持这种协议,配置简单,直接使用 video 标签即可。

    webRTC 兼容性:

    video 标签播放 hls 协议视频:

    <video controls autoplay>  
        <source src="http://10.66.69.77:8080/hls/mystream.m3u8" type="application/vnd.apple.mpegurl" />  
        <p class="warning">Your browser does not support HTML5 video.</p>  
    </video>
    

    2. 到底什么是 HLS 协议?

    简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。

    每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。

    .m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件:

    1
    2
    3
    4
    5
    6
    7
    #EXTM3U                 m3u文件头
    #EXT-X-MEDIA-SEQUENCE   第一个TS分片的序列号
    #EXT-X-TARGETDURATION   每个分片TS的最大的时长
    #EXT-X-ALLOW-CACHE      是否允许cache
    #EXT-X-ENDLIST          m3u8文件结束符
    #EXTINF                 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效
    mystream-12.ts

    ts 文件:

    HLS 的请求流程是:

    1 http 请求 m3u8 的 url。

    2 服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。

    3 客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。

    简单流程:

    3. HLS 直播延时

    我们知道 hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟。参考资料:https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html

    4. 视频直播的整个流程是什么?

    当视频直播可大致分为:

    1 视频录制端:一般是电脑上的音视频输入设备或者手机端的摄像头或者麦克风,目前以移动端的手机视频为主。

    2 视频播放端:可以是电脑上的播放器,手机端的 native 播放器,还有就是 h5 的 video 标签等,目前还是已手机端的 native 播放器为主。

    3 视频服务器端:一般是一台 nginx 服务器,用来接受视频录制端提供的视频源,同时提供给视频播放端流服务。

    简单流程:

    5. 怎样进行音视频采集?

    当首先明确几个概念:

    视频编码: 所谓视频编码就是指通过特定的压缩bv伟德登录入口,将某个视频格式的文件转换成另一种视频格式文件的方式,我们使用的 iphone 录制的视频,必须要经过编码,上传,解码,才能真正的在用户端的播放器里播放。

    编解码标准: 视频流传输中最为重要的编解码标准有国际电联的H.261、H.263、H.264,其中 HLS 协议支持 H.264 格式的编码。

    音频编码: 同视频编码类似,将原始的音频流按照一定的标准进行编码,上传,解码,同时在播放器里播放,当然音频也有许多编码标准,例如 PCM 编码,WMA 编码,AAC 编码等等,这里我们 HLS 协议支持的音频编码方式是AAC编码。

    下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤:

    1 音视频的采集,ios 中,利用 AVCaptureSession和AVCaptureDevice 可以采集到原始的音视频数据流。

    2 对视频进行 H264 编码,对音频进行 AAC 编码,在 ios 中分别有已经封装好的编码库来实现对音视频的编码。

    3 对编码后的音、视频数据进行组装封包;

    4 建立 RTMP 连接并上推到服务端。

    ps:由于编码库大多使用 c 语言编写,需要自己使用时编译,对于 ios,可以使用已经编译好的编码库。

    x264编码: https://github.com/kewlbear/x264-ios

    faac编码: https://github.com/fflydev/faac-ios-build

    ffmpeg编码: https://github.com/kewlbear/FFmpeg-iOS-build-script

    关于如果想给视频增加一些特殊效果,例如增加滤镜等,一般在编码前给使用滤镜库,但是这样也会造成一些耗时,导致上传视频数据有一定延时。

    简单流程:

    6. 前面提到的 ffmpeg 是什么?

    和之前的 x264 一样,ffmpeg 其实也是一套编码库,类似的还有 Xvid,Xvid 是基于 MPEG4 协议的编解码器,x264是基于 H.264 协议的编码器, ffmpeg 集合了各种音频,视频编解码协议,通过设置参数可以完成基于 MPEG4,H.264 等协议的编解码,demo 这里使用的是 x264 编码库。

    7. 什么是 RTMP?

    Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议,现在属于 Adobe。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。

    这里列举一下 hls 和 rtmp 对比:

    8. 推流

    简所谓推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的,https://github.com/runner365/LiveVideoCoreSDK

    9. 推流服务器搭建

    简简单的推流服务器搭建,由于我们上传的视频流都是基于 rtmp 协议的,所以服务器也必须要支持 rtmp 才行,大概需要以下几个步骤:

    1 安装一台 nginx 服务器。

    2 安装 nginx 的 rtmp 扩展,目前使用比较多的是https://github.com/arut/nginx-rtmp-module

    3 配置 nginx 的 conf 文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    rtmp {  

       server {  

           listen 1935;  #监听的端口

           chunk_size 4000;  

           application hls {  #rtmp推流请求路径
               live on;  
               hls on;  
               hls_path /usr/local/var/www/hls;  
               hls_fragment 5s;  
           }  
       }  
    }

    4 重启 nginx,将 rtmp 的推流地址写为 rtmp://ip:1935/hls/mystream,其中 hls_path 表示生成的 .m3u8 和 ts 文件所存放的地址,hls_fragment 表示切片时长,mysteam 表示一个实例,即将来要生成的文件名可以先自己随便设置一个。更多配置可以参考:https://github.com/arut/nginx-rtmp-module/wiki/
    根据以上步骤基本上已经实现了一个支持 rtmp 的视频服务器了。

    10. 在 html5 页面进行播放直播视频?

    简单来说,直接使用 video 标签即可播放 hls 协议的直播视频:

    1
    2
    3
    4
    <video autoplay webkit-playsinline>  
       <source src="http://10.66.69.77:8080/hls/mystream.m3u8" type="application/vnd.apple.mpegurl" />  
       <p class="warning">Your browser does not support HTML5 video.</p>  
    </video>

    需要注意的是,给 video 标签增加 webkit-playsinline 属性,这个属性是为了让 video 视频在 ios 的 uiwebview 里面可以不全屏播放,默认 ios 会全屏播放视频,需要给 uiwebview 设置 allowsInlineMediaPlayback=YES。 业界比较成熟的 videojs,可以根据不同平台选择不同的策略,例如 ios 使用 video 标签,pc 使用 flash 等。

    11. 坑点总结

    简根据以上步骤,笔者写了一个 demo,从实现 ios 视频录制,采集,上传,nginx 服务器下发直播流,h5 页面播放直播视频者一整套流程,总结出以下几点比较坑的地方:

    1 在使用 AVCaptureSession 进行采集视频时,需要实现 AVCaptureVideoDataOutputSampleBufferDelegate 协议,同时在- (void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection 捕获到视频流,要注意的是 didOutputSampleBuffer 这个方法不是 didDropSampleBuffer 方法,后者只会触发一次,当时开始写的是 didDropSampleBuffer 方法,差了半天才发现方法调用错了。

    2 在使用 rtmp 推流时,rmtp 地址要以 rtmp:// 开头,ip 地址要写实际 ip 地址,不要写成 localhost,同时要加上端口号,因为手机端上传时是无法识别 localhos t的。

    这里后续会补充上一些坑点,有的需要贴代码,这里先列这么多。

    12. 业界支持

    目前,腾讯云,百度云,阿里云都已经有了基于视频直播的解决方案,从视频录制到视频播放,推流,都有一系列的 sdk 可以使用,缺点就是需要收费,如果可以的话,自己实现一套也并不是难事哈。

    demo地址:https://github.com/lvming6816077/LMVideoTest/

    参考资料:http://www.nihaoshijie.com.cn/index.php/archives/615

    本文来自:http://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653577297&idx=1&sn=a292ff3b499168f4eb589e40b7aa6d13

    ]]>
    视频直播这么火,再不学就 out 了。

    为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的bv伟德登录入口点,包括但不限于前端bv伟德登录入口。

    1. H5 到底能不能做视频直播?

    当然可以, H]]>