没有原型,没有设计稿,我是如何一步步开发一款微信小程序的

2,218 阅读10分钟

我正在参加「掘金·启航计划」

首先我们来看下百度百科对于前端工程师的一个定义

前端工程师使用 HTML、CSS、JavaScript 等专业技能和工具将产品 UI 设计稿实现成网站产品,涵盖用户 PC 端、移动端网页,处理视觉和交互问题。

顾名思义,作为一名前端工程师,在工作中,我们普遍做的是根据 UI 设计稿来实现用户终端的产品功能,当然也有些没有 UI 稿,直接根据产品原型来实现的。
那么,作为一名前端工程师,当我们接到没有原型或 UI 稿,直接一个 excel 表格文档,就让评估工时,来开发时,这大概是令程序员最头疼的吧。


首先,带大家来还原下真实的场景

开始
你:老板,你看下,这个功能我这样做行不行
老板:嗯,你决定就好,我相信你
你:(内心)老板这么相信我,我一定要做好

一个星期后
老板:小陈,xxx 小程序做完了吗?要不先发个体验版出来看看
你:(内心)还没做完,怎么办,默默不说话

半个小时后
你:老板,这个是体验链接,还有 xxx 没做好,其他的您先看下,剩下的功能大概下班前做完

又过了半个小时
老板:小陈,这个做的挺好好,但是

但是
老板:这个 logo 这里跟这里的位置换下
老板:这个物流我看其他小程序能直接拨打快递员的电话
老板:这个详情页的功能再丰富点吧,我看 xxx 是这样做的
老板:这里要加个 xxx 协议,行业规范
老板:...

十分钟后
你:(内心)有毒吧,刚开始做的时候,问你,说都可以,现在这么多问题,咱不干了
你:好的,老板,我去参考,稍后修改

是不是看完后,顿时会觉得,遇到这种单,作为傲娇的我,肯定直接就拒绝了,不会让自己遭这个罪的。
嗯,不对,很多时候,为了碎银几两,我忍了,还是动手干吧。


下面进入正题 👇

一、我要做的是什么小程序?

朋友说,目前待业在老家,想自己创业,最近有谈到寄快递的二级代理,寄快递比较便宜,也有投资人准备投资,于是想做个寄快递的小程序,顺便还给我发了目前市面上已经有的小程序,嗯,参考这个做就行了。

好家伙,看这意思好像是想简单的做个复制粘贴功能吗?这确定不会被别人嫌弃......

二、开始写代码前我做了哪些事?

1. 确定小程序名称

最初朋友说,想取名为 i云驿站,大概是“我的线上快递驿站”。
驿站,第一印象让我想到菜鸟驿站,给人的感觉,第一功能是存放快递。
然后,我在微信小程序功能里搜索了云驿站,发现出来的有各种类型的驿站,快递,水果、家政等等,啥都有,呃,好像针对性没那么强。
而我们主要的功能是寄快递,于是我建议朋友用 i云快递,这样会更加精准些。
就这样,我们敲定了小程序的名称

2. 分析同类产品,找到差异,确定主题色

小程序上有这么多快递类,我们该如何让自己的小程序有记忆点,跟其他的不一样呢。为此,我看了德邦、顺丰、申通、韵达等小程序,哪些颜色已经被使用了,哪些没被使用,找出差异,来确定好小程序的主题色,以保证开发中各界面的颜色颜色统一。

3. 浅浅做了个LOGO

程序员做设计,这着实不是我的强项,怎奈目前朋友也没有想请人做个 LOGO 的意识,那就只能我自己出马了。

来点小寓意
根据自己以前仅有的一些设计经验,做 LOGO 时,在网上找了一款字体图标修改了下,用字体图标 + 云朵 + 轮子的设计,涵盖了云和快递的概念。

难倒设计师总监的 LOGO
做完后让设计师朋友看看,怎么调整调整,果然,被吐槽了,朋友说这只能重新做了,不知道咋改。好吧,我脆弱的小心脏,能力如此只能将就了。

老板很满意
还好朋友发给投资人,投资人还挺满意的,说云朵加了轮子,跑的快,能赚钱。
嗯,老板的想法果然不一样,只要寓意好就行,设计感不重要,第一关,算是通过了。

4. 找到参照产品,确定模块功能

做完 LOGO 后,就马不停蹄的进入需求整理阶段。

已经有了参照产品,为什么还要整理需求呢?
没错,老板刚开始说的是根据 xxx 程序做就行,按理直接照抄就好了嘛,为什么还要整理需求呢?试用了下参照程序,发现有一下问题:

1.参照程序有些功能过于简单
2.从系统看很多隐藏功能是看不到的,只要操作了,每种情况都试了一下,才能知道实际要做哪些界面。

经过整理从原来看起来的四五个页面,梳理出了这么多功能,当然还没包括一些异常情况及订单不同状态的处理。 image.png 由此可见,作为程序员,不要想当然的来定义一个程序功能的简单跟复杂,一定要提前梳理好,做到“心中有数”。

5. 确定框架,下载开发工具

确定框架
大家都知道,现在微信小程序开发的主流框架有两个,uniapp 和 taro,但是对于用哪个还是没有想清楚,于是做了个框架分析,决定从分析中来得出结论:

uni-apptaro
跨端能力支持微信、支付宝、百度等小程序支持微信、支付宝、百度等小程序
运行性能10000+列表流程运行(都对小程序的 setData 做了优化)10000+列表流程运行(都对小程序的 setData 做了优化
开发体验环境搭建、项目创建、运行编译等比较简单环境搭建、项目创建、运行编译等比较简单
包大小87kb259kb
生态官方有对应的 ui 库,有专门的论坛,qq 群和微信群官方有对应的 ui 库。Issues、微信群
技术栈vuevue/react

从表格中可以看出:

  • uniapp 和 taro 在跨端能力、运行性能、开发体验上都差不多
  • 包大小上, uniapp 更小
  • 生态上,uniapp 相对更完善

同时考虑到我们要做的是一个比较小的程序, 后期小程序可能转给其他人维护,因此 uniapp 更加适合。

工具安装

微信开发者工具

HBuilderX

致此,前期的准备工作算是完成了,接下来就进入正题,开发阶段了。

三、终于进入正题了-开发

1. 搭建项目框架

我这边当时只下载了微信开发者工具,没有下载 HBuilderX,直接用 vue-cli 来搭建的项目

环境安装

全局安装 vue-cli

npm install -g @vue/cli
创建 uni-app 项目
vue create -p dcloudio/uni-preset-vue my-project

更具体的内容可以看 uni-app 的官网 通过 HBuilderX 可视化界面vue-cli 命令创建项目

项目架构

下图左边是用 vue-cli 构建出来的默认架构,右边是在默认架构基础上做的扩展

image.png

vscode 配置
  • extensions.json:用于编写扩展插件
    • 插件如果成员没有的话并不会自己下载, 但可以在扩展中点击筛选器扩展选中推荐就会出来然后一一安装就可以了

    • 添加扩展方法: 打开插件详情, 复制右侧 "详细信息" 下面的标识符即可

    • extensions.json 文件示例

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "donjayamanne.githistory",
    "esbenp.prettier-vscode"
  ]
}
  • settings.json:vscode 编辑器配置项
    • settings.json 文件示例
{
   /*先配置eslint*/
   "editor.codeActionsOnSave": { // 每次保存的时候将代码按eslint格式进行修复
   "source.fixAll.eslint": true
 },
}
eslint 配置

eslint 的配置主要两个文件,.eslintrc.js 文件配置.eslintignore 忽略配置,详尽使用参见官方文档

  1. .eslintrc.js 文件配置
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
  parserOptions: {
    parser: "babel-eslint",
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
  },
  overrides: [
    {
      files: [
        "**/__tests__/*.{j,t}s?(x)",
        "**/tests/unit/**/*.spec.{j,t}s?(x)",
      ],
      env: {
        mocha: true,
      },
    },
  ],
};
  1. .eslintignore 忽略配置
## 通用
.DS_Store
node_modules
*.log
logs
log

# 构建产物
dist

## 脚手架脚本目录
scaffold

# 不必检查的后缀
*.md
*.ejs
*.yml
*ignore

# i18n
i18n/

# 忽略的文件
package-lock.json
src/assets

重点:修改配置文件后,要重启项目才能生效

配置 Prettier

在根目录创建 .prettierrc.js 配置文件及.prettierignore 忽略文件

  1. .prettierrc.js 文件配置
module.exports = {
    printWidth: 80, //单行长度
    tabWidth: 2, //缩进长度
    useTabs: false, //使用空格代替tab缩进
    semi: true, //句末使用分号
    singleQuote: true, //使用单引号
    quoteProps: 'consistent', //仅在必需时为对象的key添加引号
    jsxSingleQuote: true, // jsx中使用单引号
    trailingComma: 'all', //多行时尽可能打印尾随逗号
    bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar }
    jsxBracketSameLine: true, //多属性html标签的‘>’折行放置
    bracketSameLine: true, //多属性html标签的‘>’折行放置
    arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x
    requirePragma: false, //无需顶部注释即可格式化
    insertPragma: false, //在已被preitter格式化的文件顶部加上标注
    proseWrap: 'preserve', //不知道怎么翻译
    htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感
    vueIndentScriptAndStyle: true, //不对vue中的script及style标签缩进
    endOfLine: 'lf', //结束行形式
    embeddedLanguageFormatting: 'auto', //对引用代码进行格式化
    jsxBracketSameLinte: true, //在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
  };
  
  1. .prettierignore 文件配置
## 通用
.DS_Store
node_modules
*.log
logs
log

# 构建产物
dist

## 脚手架脚本目录
scaffold

# 不必检查的后缀
*.md
*.ejs
*.yml
*ignore

# i18n
i18n/

# 忽略的文件
package-lock.json
src/assets

2. 正式开发

配置 AppID

src/manifest.json 文件中配置项目的名称和 appID

image.png

配置 src 目录框架

image.png

路由配置

uni-app 路由的配置由 pages.json 统一管理,类似微信小程序中 app.json 的页面管理部分

{
  "easycom": {
    "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
  },
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "index",
        "enablePullDownRefresh": false
      }
    },
    {
      "path": "pages/wxLogin/wxLogin",
      "style": {
        "navigationBarTitleText": "登录",
        "enablePullDownRefresh": false
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#fff",
    "backgroundColor": "#fff"
  },
  "condition" : { //模式配置,仅开发期间生效
    "current": 0, //当前激活的模式(list 的索引项)
    "list": [
        {
            "name": "pages/index/index", //模式名称
            "path": "dd", //启动页面,必选
            "query": "" //启动参数,在页面的onLoad函数里面得到
        }
    ]
  }
}
vuex状态管理

vuex 的配置可以参考官网的 状态管理 Vuex
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter 的默认情况下,模块内部的 action 和 mutation 仍然是注册在 全局命名空间 的,如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名,在开发中我更喜欢这种方式。

  1. 入口 index 的封装
store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 引入各 module,新增后不用一个个去引入
const modulesFiles = require.context('./modules', true, /\.js$/);

const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1');
  const value = modulesFiles(modulePath);
  modules[moduleName] = value.default;
  return modules;
}, {});

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules,
});
  1. module 的封装
// store/module/user.js
const state = {
  //是否登录
  hasLogin: false,
  // 用户数据
  userInfo: {},
};

const actions = {
  // 获取当前用户信息
  login({ commit }, {}) {
    commit('LOG_IN', {});
  },
  setUserInfo({ commit }, provider) {
    commit('SET_USERINFO', provider);
  },
  logOut({ commit }) {
    commit('LOG_OUT');
  },
};

const mutations = {
  SET_USERINFO(state, provider) {
    state.hasLogin = true;
    state.userInfo = provider;
  },
  //登录
  LOG_IN(state, provider) {
    state.hasLogin = true;
    uni.setStorage({
      key: 'userInfo',
      data: provider,
    });
  },
  //退出登录
  LOG_OUT(state) {
    state.hasLogin = false;
    state.userInfo = {};
    uni.removeStorage({
      key: 'userInfo',
    });
    uni.removeStorageSync('token');
    uni.removeStorageSync('openId');
  },
};

const getters = {
  getUsreInfo(state) {
    return state.userInfo;
  },
};

export default {
  namespaced: true, // 此处一定要设置,才能让模块成为带命名空间的模块
  state,
  mutations,
  actions,
  getters,
};
  1. 使用示例
<template>
  <view></view>
</template>

<script>
import { mapState, mapActions } from 'vuex';
export default {
  name: 'my',
  components: {},
  data() {
    return {};
  },
  created() {},
  mounted() {},
  computed: {
    ...mapState({
      userInfo: (state) => {
        return state.user.userInfo;
      },
    }),
  },
  methods: {
    ...mapActions({
      setUserInfo: 'user/setUserInfo',
      logOut: 'user/logOut',
    }),
    getUser() {
      // 获取用户信息
      // ....
      this.setUserInfo(data);
    },
  },
};
</script>

<style lang="scss"></style>
封装接口请求
  1. 区分环境,配置服务器地址
// uni-app内置有区别环境的不同,可以直接通过 process.env.NODE_ENV 获取
const BASE_URL_CONFIG = {
  "develop": "https://develop.test.com",
  "trial": "https://trial.test.com",
  "release": "https://release.test.com",
};

let baseUrl = '';

// #ifdef MP-WEIXIN 
let envVersion = wx.getAccountInfoSync().miniProgram.envVersion;
baseUrl =  BASE_URL_CONFIG[envVersion] || BASE_URL.release;
// #endif
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
    baseUrl = BASE_URL_CONFIG[develop]
} else {
    baseUrl = BASE_URL_CONFIG[release]
}
// #endif
// #ifdef APP-PLUS
    baseUrl = BASE_URL_CONFIG[release]
// #endif

const config = {
    base_url: baseUrl,
    appId: uni.getAccountInfoSync().miniProgram.appId,
}

export { config }
  1. 封装接口请求
import { config } from '../config.js'
import store from '@/store';
export default class Request {
  http(param) {
    var url = param.url,
      method = param.method,
      header = {},
      data = param.data || {},
      hideLoading = param.hideLoading || false;
    try {
      const token = wx.getStorageSync('token');
      if (token) {
        header.token = token;
      }
    } catch (e) {
      // 获取 token 失败
    }
    //拼接完整请求地址
    var requestUrl = config.base_url + url;
    //请求方式
    if (!method) {
      method = 'GET';
    } else {
      method = method.toUpperCase();
      header['content-type'] = 'application/json';
    }

    //加载圈
    if (!hideLoading) {
      uni.showLoading({
        title: '加载中...',
      });
    }
    //开始请求
    return new Promise((resolve, reject) => {
      uni.request({
        url: requestUrl,
        data: data,
        method: method,
        header: header,
        success: (res) => {
          // 判断 请求api 格式是否正确
          if (res?.statusCode && res.statusCode != 200) {
            uni.showToast({
              title: 'api错误' + res.errMsg,
              icon: 'none',
            });
            return;
          }
          if (res?.data?.code === 1001) {
            // token 失效
            store.dispatch('user/logOut');
            uni.navigateTo({
              url: '/pages/wxLogin/wxLogin',
            });
          }
          // 将结果抛出
          resolve(res.data);
        },
        //请求失败
        fail: (e) => {
          uni.showToast({
            title: '' + e.data.msg,
            icon: 'none',
          });
          resolve(e.data);
        },
        complete() {
          // 隐藏加载
          if (!hideLoading) {
            uni.hideLoading();
          }
          return;
        },
      });
    });
  }
}
  1. 使用示例
import Request from '@/utils/requset.js';
let request = new Request().http;

export default {
  /**
   * 小程序用户登录(调微信登录)
   * @param appid 小程序appId
   * @param code 小程序调用wx.login返回的res.code
   * @param inviteCode 邀请码code,从分享二维码中带入
   */
  userLogin: function (params = { code, appid, inviteCode }) {
    return request({
      url: '/api/user/login', //请求头
      method: 'POST', //请求方式
      data: params, //请求数据
    });
  },
};
在 main 中引用
import Vue from 'vue'
import App from './App'
import store from './store'
import uView from 'uview-ui'
Vue.use(uView)

Vue.config.productionTip = false
Vue.prototype.$store = store

App.mpType = 'app'

const app = new Vue({
    store,
    ...App
})
app.$mount()

致此,一切准备就绪,可以进入功能开发了。

四、功能完成,发布测试上线

功能完成后,测试阶段,可能会出现很多修改的情况,正如我开头讲的场景,此时要做的就是保持好的心态啦。

资料分享

uni-app小程序手把手项目实战 # 如何一人五天开发完复杂小程序(前端必看)