一款使用Vue JS,Webpack&Material Design开发的渐进式web应用(PWA)

原文地址:https://blog.sicara.com/a-progressive-web-application-with-vue-js-webpack-material-design-part-1-c243e2e6e402
PWA相关的中文文章比较少,这篇手把手教程非常适合当做pwa入门教程,因此翻译一哈。作者写了一系列手把手教程,这是part 1.

渐进式web应用(PWA)是web未来的趋势,越来越多的大公司们开始使用这项技术(如推特...)。

想象一下:当你在乘坐地铁时浏览一款web应用,这款web应用通过推送通知、实时更新数据和提供原生app一般的导航等功能让你沉醉其中。而这些功能就是PWA所包含的一些特性。

PWA就是一款能为用户提供类似于原生app般体验的web应用。得益于现代web技术的不断创新(如Service Worker,Native APIS,JS frameworks等),PWA可以提升web应用的质量标准。
Google PWA
如果想了解更多关于PWA的消息,你可以访问Google developer page
看一哈下面的PWA,它看起来很像一款原生的app吧!

Twitter progressive web application

从开发者的角度来看,PWA相比于原生的应用有巨大的优点。它本质上是一个网页,所以:

  • 你可以使用任何你喜欢的框架进行开发
  • 编写一次代码就可以让它跨平台、跨设备运行:因为它是由用户的浏览器来运行的
  • 易于传播:它不需要通过应用商店来下载

然而在2017年早些时候,PWA仍然面临着一些限制:

  • Safari并不支持PWA的一些基本特性,如Service Workers,但看起来Apple已经开始着手去实现它了。
  • 一些原生的方法仍然不被支持,你可以在What web can do看到更多信息。

教程目标

本教程旨在使用VueJS和Webpack从头编写一款基本但功能完善的PWA。这款应用将会实现所有在介绍中提到的特性:渐进式的,响应式的,独立连接的等等。我想让你对PWA能实现的功能有了大体的了解:流畅的类原生应用体验,离线行为,设备原生功能的接口,通知推送等。

为了让这件事充满挑战性,我们将会完成一款猫咪图片交流的app:CropChat!CropChat的用户可以浏览各用户上传的猫咪图片,点击图片阅读更多细节,并且可以发表新的猫咪图片(图片来源可以是互联网,也可以从相册中选取或拍照)。

本教程将分为如下几部分,各部分会陆续发布:

  • [Part 1] 使用VueJS、Webpack和Material DesignLite创建一个单页应用
  • [Part 2]使用Vue-Resource和VueFire连接App和远程API
  • [Part 3] 使用Service Workers实现离线模式
  • [Part 4] 访问设备相机进行拍照
  • [Part 5] 访问设备存储实现图片上传
  • [Part 6] 实现消息推送
  • [Part 7] 获取设备位置

这款PWA的用到的基本组件(工具)

我们的PWA基于现代的开发工具,你将会爱上他们!

  • VueJS 2 视图层:使用Material Design作为UI框架
  • Vue-Router:单页应用的前端路由工具
  • Vue-Resource&Vuefire:处理和数据库Firebase之间的通信
  • Service Workers:实现离线模式和数据更新
  • Webpack&Vue-loader:构建这款应用的工具,提供诸如热重载、ES2016和预处理器等支持。

接下来就让我们开始part 1吧!

[PART 1] 使用VueJS、Webpack和Material DesignLite创建一个单页应用

如果你对VueJS 2还不是很熟悉的话,我(作者)强烈建议你看一哈官方教程

构建一个最基本的VueJS APP

我们将会使用Vue-cli作为脚手架工具构建这个应用:

npm install -g vue-cli

Vue-cli自带了几个模板,我们将会选择pwa template。Vue-cli会使用Webpack,vue-loader(热重载),一个manifest文件和基于service workers实现基本的离线功能创建一个虚拟的VueJS应用。

然后介绍了Webpack,就是网上很多文章都说的那种,大家可以点击了解一哈。敲下面的命令初始化Cropchat这个应用:

vue init pwa cropchat

初始化的过程中终端会问你几个问题,我(作者)通常使用如下的配置:

? Project name cropchat
? Project short name: fewer than 12 characters to not be truncated on homescreens (default: same as name) cropchat
? Project description A cat pictures messaging application
? Author Charles BOCHET <charlesb@theodo.fr>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? No
vue-cli · Generated "cropchat".

这波骚操作就把项目的文件夹创建好了,各级目录结构如下说明:

  • build:包含webpack和vue-loader的配置文件
  • config:包含该app的配置(environments,parameters...)
  • src:应用的源码所在
  • static:像图片啊css文件还有别的静态文件都丢这里面
  • test:通过Karma & Mocha创建的单元测试文件

说了这么多再就跑一跑下面的命令先把初始化完的项目跑起来再说:

cd cropchat
npm install
npm run dev

然后访问地址localhost:8080就能看到了:

Manifest.json: 让你的应用可安装

PWA最大的优点之一便是应用可以很方便地安装和分享。所有提供了合法的manifest.json文件并在index.html中引入的web应用都可以安装。
Vue pwa模板提供了一个默认的manifest.json文件。
编辑默认的static/manifest.json文件使之能应用于本次的项目:

{
  "name": "cropchat",
  "short_name": "cropchat",
  "icons": [
    {
      "src": "/static/img/icons/cropchat-icon-64x64.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons/cropchat-icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons/cropchat-icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/static/img/icons/cropchat-icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "fullscreen",
  "orientation": "portrait",
  "background_color": "#2196f3",
  "theme_color": "#2196f3"
}

在manifest.json文件中你可以修改定制一些属性:
· app names
· app icons,设置该图标使之作为桌面和启动页图标(你可以找到Cropchat官方图标在这个仓库);
· start url
· displayorientation,display主要控制页面的显示方式,是否要隐藏浏览器UI或全屏或像正常网页一般;orientation主要控制页面的初始方向;
· backgroundtheme colors
Mozilla Developer website中有对清单中各个属性的详细说明。

然后看一哈index.html,发现manifest.json文件已经被引入了:

<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath %>static/manifest.json">

同时确认一哈在index.html中有对viewport的声明:

<meta name="viewport" content="width=device-width, initial-scale=1">

这样搞完我们就要试试把Cropchat安装到手机上了。由许多方法可以访问到本地的localhost:8080,作者最喜欢的是ngrok,这里就不翻译了大家各显神通,跟着原文敲一哈命令也可以。
然后当我们用手机访问这个页面的时候,奇迹出现了:



Cropchat的源码在GitHub可以看到。
要想了解更多关于ngrok的信息,你可以看看Matthieu Auger的文章: Expose your local environment to the world with ngrok

处理视图页面和路由

既然我们已经成功地将项目搭建并运行起来,那接下来就是去完成Cropchat的更多功能了。Cropchat有三个视图页面:
· Home View:以列表的形式显示猫咪图片
· Detail View:显示特定猫咪图片的具体信息(通过首页点击图片进入)
· Post View:用户可以在该页面发表新的图片

那我们首先就先搞一把HomeView页面。创建src/components/HomeView.vue文件,整体结构如下:

<template>
  <ul class="list">
  </ul>
</template>
<script>
export default {
}
</script>
<style scoped>
  .list {
    width: 100%;
    padding: 0;
  }
</style>

同理可得src/components/DetailView.vue页面:

<template>
  <div class="card-image">
  </div>
</template>
<script>
  export default {
  }
</script>
<style scoped>
</style>

同理可得src/components/PostView.vue页面:

<template>
  <div class="waiting">
    Not yet available
  </div>
</template>
<script>
export default {
}
</script>
<style scoped>
  .waiting {
    padding: 10px;
    color: #555;
  }
</style>

最后更新路由文件src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import HomeView from '@/components/HomeView'
import DetailView from '@/components/DetailView'
import PostView from '@/components/PostView'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/detail/:id',
      name: 'detail',
      component: DetailView
    },
    {
      path: '/post',
      name: 'post',
      component: PostView
    }
  ]
})

移除没使用的Hello.vue文件。你现在已经可以看到页面发生改变了(热重载很棒吧日常商业吹捧)

安装Material Design Lite

废话不多说,文档看这里:Get MDL.io,总之它很棒是了~~,当然Material Design思想也是超棒的,配合MDL像我们这些屌丝开发者也能快速搭建大体符合materail design规范的网站~~
更新Cropchat依赖:

npm install material-design-lite --save

更新src/App.vue组件,导入MDL样式并加载MDL模块:

<script>
  require('material-design-lite')
  ...
</script>
<style>
  @import url('https://fonts.googleapis.com/icon?family=Material+Icons');
  @import url('https://code.getmdl.io/1.2.1/material.blue-red.min.css');
</style>

为单页应用增加一个导航

更新src/App.vue文件模板部分:


<template>
  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
    <header class="mdl-layout__header">
      <div class="mdl-layout__header-row">
        <span class="mdl-layout-title">CropChat</span>
      </div>
    </header>
    <div class="mdl-layout__drawer">
      <span class="mdl-layout-title">CropChat</span>
      <nav class="mdl-navigation">
        <router-link class="mdl-navigation__link" to="/" @click.native="hideMenu">Home</router-link>
        <router-link class="mdl-navigation__link" to="/post" @click.native="hideMenu">Post a picture</router-link>
      </nav>
    </div>
    <main class="mdl-layout__content">
      <div class="page-content">
        <router-view></router-view>
      </div>
    </main>
  </div>
</template>

由于MDL不是特别为单页应用设计的,所以当用户点击菜单按钮时需要隐藏burger menu:

<script>
...
export default {
  name: 'app',
  methods: {
    hideMenu: function () {
      document.getElementsByClassName('mdl-layout__drawer')[0].classList.remove('is-visible')
      document.getElementsByClassName('mdl-layout__obfuscator')[0].classList.remove('is-visible')
    }
  }
}
</script>

效果如图所示

丰富视图并让应用跑起来

到现在为止我们并没有使用后台服务器,而是创建一个data.js文件做一些模拟的数据。
创建src/data.js文件:

export default {
  pictures: [
    {
      'id': 0,
      'url': 'https://25.media.tumblr.com/tumblr_m40h4ksiUa1qbyxr0o1_400.gif',
      'comment': 'A cat game',
      'info': 'Posted by Kevin on Friday'
    },
    {
      'id': 1,
      'url': 'https://25.media.tumblr.com/tumblr_lhd7n9Qec01qgnva2o1_500.jpg',
      'comment': 'Tatoo & cat',
      'info': 'Posted by Charles on Tuesday'
    },
    {
      'id': 2,
      'url': 'https://24.media.tumblr.com/tumblr_m4j2atctRm1qejbiro1_1280.jpg',
      'comment': 'Santa cat',
      'info': 'Posted by Richard on Monday'
    },
    {
      'id': 3,
      'url': 'https://25.media.tumblr.com/tumblr_m3rmbwhVB51qhwmnpo1_1280.jpg',
      'comment': 'Mexico cat',
      'info': 'Posted by Richard on Monday'
    },
    {
      'id': 4,
      'url': 'https://24.media.tumblr.com/tumblr_mceknxs4Lo1qd477zo1_500.jpg',
      'comment': 'Curious cat',
      'info': 'Posted by Richard on Monday'
    }
  ]
}

HomeView.vue的script标签中导入数据并将图片连接关联到响应的DetailView页面:

<script>
  import data from '../data'
  export default {
    methods: {
      displayDetails (id) {
        this.$router.push({name: 'detail', params: { id: id }})
      }
    },
    data () {
      return {
        'pictures': data.pictures
      }
    }
  }
</script>

更新HomeView.vue模板和样式:

<template>
  <div>
    <div class="mdl-grid">
      <div class="mdl-cell mdl-cell--3-col mdl-cell mdl-cell--1-col-tablet mdl-cell--hide-phone"></div>
      <div class="mdl-cell mdl-cell--6-col mdl-cell--4-col-phone">
        <div v-for="picture in this.pictures" class="image-card" @click="displayDetails(picture.id)">
          <div class="image-card__picture">
            <img :src="picture.url" />
          </div>
          <div class="image-card__comment mdl-card__actions">
            <span>{{ picture.comment }}</span>
          </div>
        </div>
      </div>
    </div>
    <router-link class="add-picture-button mdl-button mdl-js-button mdl-button--fab mdl-button--colored" to="/post">
      <i class="material-icons">add</i>
    </router-link>
  </div>
</template>
...
<style scoped>
  .add-picture-button {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 998;
  }
  .image-card {
    position: relative;
    margin-bottom: 8px;
  }
  .image-card__picture > img {
    width:100%;
  }
  .image-card__comment {
    position: absolute;
    bottom: 0;
    height: 52px;
    padding: 16px;
    text-align: right;
    background: rgba(0, 0, 0, 0.5);
  }
  .image-card__comment > span {
    color: #fff;
    font-size: 14px;
    font-weight: bold;
  }
</style>

同理可得DetailView.vue页面:


<template>
  <div class="mdl-grid">
    <div class="mdl-cell mdl-cell--8-col">
      <div class="picture">
        <img :src="this.pictures[$route.params.id].url" />
      </div>
      <div class="info">
        <span>{{ this.pictures[$route.params.id].info }}</span>
      </div>
    </div>
    <div class="mdl-cell mdl-cell--4-col mdl-cell--8-col-tablet">
      <div class="comment">
        <span>{{ this.pictures[$route.params.id].comment }}</span>
      </div>
      <div class="actions">
        <router-link class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored" to="/post">
          ANSWER
        </router-link>
      </div>
    </div>
  </div>
</template>
<script>
import data from '../data'
export default {
  data () {
    return {
      'pictures': data.pictures
    }
  }
}
</script>
<style scoped>
  .picture > img {
    color: #fff;
    width:100%;
  }
  .info {
    text-align: right;
    padding: 5px;
    color: #555;
    font-size: 10px;
  }
  .comment {
    padding: 10px;
    color: #555;
  }
  .actions {
    text-align: center;
  }
</style>

终于搞定了!

搞定了,看一哈是怎么个效果:


在这个仓库你可以找到源码噢:
https://github.com/charlesBochet/vueJSPwa

但是Cropchat还不完全是个PWA,我们来看看PWA需要哪些特性:

在之后的教程中我们会不断完善这些特性,我也会继续半吊子翻译教程。谢谢大家,祝各位和这位作者新年快乐!