分类 精读 下的文章

从0开始理解Vite的主要新特性(一)

vite这个工具确实尤大在微博上造势很猛,又以各种骚操作着实火了一把,那我们今天就一起了解一下vite吧~



我们可以从vite仓库的readme可以看到安装vite非常的方便

// https://github.com/vitejs/vite
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

我们在本地环境运行之后可以看到这样的页面,就说明我们可以开始使用了。




我们首先要理解vite的工作原理,它为什么这么快?



当我们打开工程的index.html的时候,我们可以发现script的type是module

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>


如果你不了解script标签中的module是什么意思,那么MDN解释说如果标示了module的话会把代码当作js模块来执行,一篇关于es6的文章也很好的介绍了:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script

如果你熟悉es6的模块概念,模块仅仅就是普通的含有js代码的文件而已,我们可以用importexport关键字对变量,对象的导出和导入,而这个机制在高级浏览器已经完全实现了。



不得不说,尤大不仅是技术上的大神,而且富有创造和想象力。让vite变成了一个0捆绑的开发服务器,利用浏览器高级特性让开发体验变得更好

当浏览器解析保留了模块关键字的代码,从而会导致HTTP请求,vite通过koa拦截了这些请求:

.vue => 拦截请求 => 编译 => 返回给客户端

那么没有打包的vite和老东家的基于打包的vue-cli就有了一些明显的优势了:

1. vite利用了客户端能力不用打包其他服务,原生的ES Import直接输出提高了冷启动速度。

2. vite按需编译当前页面需要的组件,而不需要打包整个APP的组件,这样的提升对比cli无疑是项目越大速度差距越大。

3. HMR更新速度不会和模块数量牵扯,vite会让HMR一如既往的保持快速。

使用vite开发应用可能在前期除了启动速度,其他功能是要等到应用慢慢变大才能真实的感受vite的强大。


TS支持

vite内置TS的支持,开箱即用:

<script lang="ts">
import { ref } from "vue";

export default {
  name: 'App',
  setup(){
    let hello = ref<string>("1");
    console.log(hello.value);
  }
}
</script>

值得一提的是,内置的TS不是TS官方出的tsc cli,而是之前就听说过的ESBuild,现在vite的TS支持是ESBuild也不奇怪,毕竟是要一快到底么。

——ESBuild的ReadMe说了一句振奋人心的话

实际上,我们目前的网站构建工具比实际速度慢10-100倍



为什么ESbuild这么快?

  • 因为Esbuild是用GO语言直接编译成原生代码
  • 由于GO语言特点,它的解析和映射并发非常快
  • 避免了不必要的配置
  • 数据转换很简单很快速

虽然ESbuild是一个非常快速的打包器,但是不支持热模块更新和没有开箱即用的工具,而且要像webpack一样想做一款基于ESbuild的插件,我认为目前是非常难的。所以Vite将它的长处用在了处理ts编译上,大型项目中编译TS文件,Vite几乎是一瞬间的事情。


热模块更新

我们要知道热模块更新和我们传统的刷新页面的区别,以webpack的dev-server服务器举例,通过启动开发服务器,页面与服务器建立了websocket,我们修改了代码之后给页面发送消息,页面才会执行刷新命令,本质上这种live-reload机制已经对开发非常友好了,但是在带有状态的页面上,reload不会有更好的开发体验:

当页面存在弹窗或者编辑框等,代码修改之后,liveReload会重载页面,如果刷新代码的同时不会重载页面而是重新加载修改过的文件就完美,所以这个机制就是webpack提出的热替换技术,也就是我们说的热更新

webpack.config.js

module.exports = {
  mode: "development", // 开发环境
  devtool: "cheap-module-eval-source-map",
  devServer: {
    contentBase: "./bundle",
    open: true,
    hot: true, // 开启热模块更新
    hotOnly: true  // 更新失败不会刷新页面配置
  }
  module: {
    rules: [{
      test: /\.css$/,
      use: ["style-loader", "css-loader"]
    }]
  }
}

css的loader中的实现已经做了热更新的处理,通过HMR这个插件中的API

// main.jsif (module.hot) {
  module.hot.accept(function() {
    // 监听变化,则修改
  });
  module.hot.dispose(function() {
    // 移除  });
}

我们了解了HMR基本操作之后就可以看看Vite是如何做HMR的:

vite和webpack的HMR实现机制是一样的,都是通过客户端和服务端建立socket连接,服务端有变化则通知客户端做出改变:

 // server/serverPlugin.ts
 watcher.on('change', (file) => {
    if (!(file.endsWith('.vue') || isCSSRequest(file))) {
      handleJSReload(file);
    }
  })

在handleJSReload函数中递归调用了walkImportChain这个函数,这个函数的作用就是查看当前变化的文件是谁引入了它(JS/Vue),那么在递归中没有找到谁引入它,就Full-reload

 send({
     type: 'full-reload',
     path: publicPath
  })

如果找到了引用这个JS的文件了就热更新:

send({
    type: 'multi',
    updates: boundaries.map((boundary) => {
      return {
        type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
        path: boundary,
        changeSrcPath: publicPath,
        timestamp
      }
    })
  })

在客户端中,vite则在核心处理函数handleMessage中定义了消息的类型:

  • connected: websocket连接
  • vue-reload: vue文件的script更新
  • vue-rerender: vue文件的template更新
  • style-update:css样式表变更
  • style-remove: css样式表移除
  • js-update: js文件更新
  • full-reload:刷新页面

客户端接受了不同的消息类型去做不同处理,根据timestamp时间戳去请求新文件,而vue文件则通过HMRRuntime更新。


裸模块导入

vite同样也支持其他家打包器的日常功能,浏览器不允许我们直接引入裸模块,例如:

import { add } from "lodash"

vite在裸模块处理上有着对vue得天独厚的优势,vite不仅仅的可以改写普通模块的路径然后正确的解析,还对vue这个依赖有特殊的处理:

如果项目本地没有安装vue依赖,那么引入vue模块会按照vite依赖的vue版本去执行,这就说明了如果你全局安装了vue,那么在vite项目中能更方便的找到它

vite重写了模块加载路径:

 // src/node/server/serverPluginModuleRewrite.ts
 ctx.body = rewriteImports(
    root,
    content!,
    importer,
    resolver,
    ctx.query.t
  )

引入的模块上下文在经过rewriteImports方法处理body以后,就会造成这样的效果:

import vue from "vue" => import vue from "@/modules/vue.js"

rewriteImports这个方法中使用到了Esbuild中的es-module-lexer进行词法分析,对esbuild本来就不熟悉的我去看了这个插件发现这个词法分析器又小,又可以快速对JS进行分析,这里就简单看一下官网的demo吧。

// 伪代码
// 和vite源码中一样
import {   
  init as initLexer,   
  parse as parseImports,   
  ImportSpecifier   
} from 'es-module-lexer' 

// 需要初始化
await initLexer;
const [import, export] = parseImports(`
  import {a} from "a"
  export const add = 1;
`
console.log(import[0].s); 
// 解析结果的返回有这样几种
// "s" is shorthand for "start"
// "e" is shorthand for "end"
// "ss" is shorthand for "statement start"
// "se" is shorthand for "statement end"

经过这样的词法处理,我们的模块引入路径就被这样替换了,尽管这个插件这么强,对于词法分析这种东西我们开发者平时也用不到,所以大家只需要知道vite的模块路径替换是借助es-module-lexer进行词法分析的就可以啦~


总结

vite和webpack对比,我认为webpack是一个纯正的打包工具,它的生态非常丰富,可以基于插件做各种事情,但是像尤大说的一样,很少有基于webpack上层封装的工具出现,也就是具有很大学习和配置成本,而vite提供了更丝滑的开发体验,以及内置的强大的HMR,TS支持,WebAssembly支持等等,所以使用哪款产品要看业务需要。


由于vite目前还在不断的更新中,但是主要特性的原理应该是不会变的,因为vite有非常多优秀的其他特性,还有我们这篇文章提到的3个特性还有很多值得细细研究的地方,所以这个vite系列会继续做,谢谢大家支持哦

好家伙,他闭着眼睛写Vue3

一个风和日丽的下午,我用了3个小时写了一个Vue3的小应用,这个应用小到不足为奇,但是Vue3的开发体验最值得我吹一波,这个小应用在登录我用了Vuex4,路由管理用到了最新的VueRouter4,而UI框架选择的是Vant3,有赞团队的Vant3最新版也是目前为止Vue3支持度最高的移动UI库,放一个项目预览GIF图

整个的开发体验,我总结了一下:有近似ReactHook的开发体验,又保留了Vue2的原汁原味,同时因为Vue3的本身设计原因,也提升了在react中没有的hook开发体验,所以如同标题,如果开发者拥有reactHook开发经验和vue2开发经验,可谓是“闭着眼睛”都能撸Vue3。

Github地址:https://github.com/1018715564/PlanListForVue3


main.js

新版的vue一改以前的构造函数去挂载,而是把函数风格贯彻到底了,在入口文件中我们使用链式调用,一直use一直爽。

import { createApp } from "vue";
import App from "./App.vue";
import Router from "./router";
import Vuex from "./store";
import Vant from "vant";
import "vant/lib/index.css";

createApp(App).use(Router).use(Vant).use(Vuex).mount("#app");

Router

import { createRouter, createWebHistory } from "vue-router";
import Login from "../pages/Login/Index.vue";
import Index from "../pages/Index/Index.vue";
const routerHistory = createWebHistory();
const Router = createRouter({
  history: routerHistory,
  routes: [
    {
      name: "Index",
      path: "/",
      component: Index
    },
    {
      name: "Login",
      path: "/Login",
      component: Login
    }
  ]
});

export default Router;

可以看到Router还是保留了vueRouter3的路由配置,但是创建路由的方式改为了createRouter,原先的路由模式也从原来的mode: "history"也改为了通过函数引入:

"history":createWebHistory()
"hash":createWebHashHistory()
"abstract":createMemoryHistory()

Vuex

import Vuex from "vuex";
export default Vuex.createStore({
  state: {
    user: null,
    planList: []
  },
  mutations: {
    // 删除一个计划
    deletePlan(state, index) {
      state.planList.splice(index, 1);
    },
    // 新增一个计划
    addPlan(state, plan) {
      state.planList.push(plan);
    },
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {},
  modules: {}
});

我们通过vuex4中的createStore创建了实例,其中我们定义了一些方法和state,这些我们在登录和添加删除计划时会用到,下来我们再看看如何在页面中使用Vue3的组合API以及路由和状态管理。

登录页面

import { reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { Toast } from "vant";
export default {
  name: "App",
  setup() {
    const store = useStore();
    const router = useRouter();
    // 用户名和密码
    const Form = reactive({
      username: "",
      password: "",
    });
    // 登录
    function handelLogin() {
      store.commit("setUser", {
        username: Form.username,
        password: Form.password,
      });
      Toast(`登录成功,你好: ${Form.username}, 请添加你的计划吧~`);
      // 跳转到首页
      router.push({
        path: "/",
      });
    }
    return {
      Form,
      handelLogin,
      handelNavBack,
    };
  }

我们使用路由以及状态管理需要从2个包中引入对应的hook去调用

调用vuex中的mutation:

store.commit(fnName, args);

路由也沿用了VueRouter3中一些老API,replace, go, back等方法都基本不变

 const router = useRouter();
 router.push({
   path: "/",
 });

如果从来没有使用过组合API的开发者,应该需要了解一下前置知识,例如在老版本中,我们定义变量和方法是这样:

data(){
  return {
    count: 0
  }
},
methods: {
  back(){
    return "It is back"
  }
}
{{count}} //在template中这样渲染

我们在vue3中需要把这些东西写到setup这个函数中,包括变量,函数,监听,计算属性,生命周期等等,然后把变量return出去供模板使用,很显然我们在登录时候用了reactive这个API,它可以传入一个普通对象,返回一个响应式的代理,我们用这个对象中的用户名和密码与视图绑定最后调用vuex的方法即可让全局状态管理知道此时 “计划APP” 是有身份进入的。

const Form = reactive({
  username: "",
  password: "",
});

// 调用vuex登录方法
store.commit("setUser", {
  username: Form.username,
  password: Form.password,
});

首页

import { ref, reactive, computed, watchEffect } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { Toast, Dialog } from "vant";

export default {
  name: "Index",
  setup() {
    const store = useStore();
    const router = useRouter();
    const planList = store.state.planList;
    const addPlanForm = reactive({
      title: "",
      remark: "",
    });
    // 添加的弹出层
    const addPopup = ref(false);
    // 计算计划的个数
    const planCount = computed(() => store.state.planList.length);
    // 如果没登录重定向到登录
    if (store.state.user === null) {
      Toast("未登录,请先登录");
      router.replace("Login");
    }
    const handelNavBack = () => {
      router.go(-1);
    };
    const handelAddPlan = (e) => {
      store.commit("addPlan", {
        ...e,
        id: Math.random().toString(36).substr(2),
        date: new Date().toDateString(),
      });
      Toast("添加计划成功");
      addPopup.value = false;
      addPlanForm.title = "";
      addPlanForm.remark = "";
    };
    const handelDeletePlan = (index) => {
      Dialog.confirm({
        title: "提示",
        message: "您缺点要删除此计划吗?",
      }).then(() => {
        store.commit("deletePlan", index);
        Toast("删除计划成功");
      });
    };
    return {
      handelNavBack,
      planList,
      addPopup,
      addPlanForm,
      handelAddPlan,
      handelDeletePlan,
      planCount,
    };
  },
};

引入了vuex和router的hook之后,我们导出了vuex和router的实例,从这个实例中我们可以获得到vuex中的state,则可以判断APP是否有登录用户,如果没有登录就重定向到登录页面。

const store = useStore();
if (store.state.user === null) {
  Toast("未登录,请先登录");
  router.replace("Login");
}

添加计划额外传递id和时间

store.commit("addPlan", {
    ...e, // Form表单的回调,是计划标题和计划备注
    id: Math.random().toString(36).substr(2),
    date: new Date().toDateString(),
  });

使用refAPI将传入的参数返回其响应式代理

// 控制弹窗显示/隐藏的变量
const addPopup = ref(false);

如果传入的是一个对象,那么Vue内部自动会调用reactive,值得注意的是如果需要更改此响应变量,需要对响应式对象中的value属性进行更改。

响应式变量在template渲染时我们不需要写.value

计划APP的全部代码都已经梳理完毕,我们可以在这个APP中学到,状态管理,路由,组合常用的API的简单应用,在组合API中还有一些我们以前经常用到的API,比如计算属性,生命周期,watch等等在组合API拓展阅读中有简单的总结。


组合API拓展阅读

  • computed

计算属性在vue3中非常简单和vue2中如出一辙,在新版本中计算属性需要从vue引入:

import { computed } from "vue"

setup(){
  const planList = computed(() => state.list);
}

计算属性默认传入一个get函数,当然可以像以前一样传入一个set函数

 const planList = computed({
  get(){
    return state.list
  },
  set(list){
    state.list = list
  }
})

planList.value = []; // 重新set值
  • watchEffect

这个Hook非常简单,如果使用过reactHook的useEffect应该非常好理解,如果这个函数所依赖的内容变化,它会重新执行这个函数。

const countEffect = watchEffect(() => {
  console.log(count); // 定义的响应式变量,如果该变量有变更,将会重新打印count
});

当组件卸载或者调用返回中的stop方法即可停止监听,这个机制和vue2中的watch是一样滴。

watchEffect我们称之为副作用函数,我们的业务场景中如果有好友列表,鼠标移动上去能异步获取好友详情,那么如果在鼠标在移动新好友之后,上一次好友详情的请求还没结束,这可能会造成数据混淆的Bug,所以我们副作用函数支持我们这样清除副作用:

watchEffect((onInvalidate) => {
  const detail = getFriendDetail(id.value)
  onInvalidate(() => {
    // id 改变时 或 停止侦听时
    detail.cancel() // 取消请求
  })
})

第一遍看文档时感觉这样清除副作用很麻烦,React是这样清除的:

useEffect(() => {
  // do something
  return () => {
    // 清除副作用
  }
})

Vue设计副作用返回是通过传入一个回调注册清除函数,是因为使用effect钩子往往是异步请求,而异步请求返回的是Promise,所以清除函数一定要在被resolve之前注册,当然文档还说这样的好处还有可以自动帮助我们处理Promise潜在的错误(这个就是后话了嘻嘻)。

下面就是重头戏了,大胆预测一波,未来关于Vue3面试题必将有它的一席之地:watchEffect的刷新时机是什么?

第一次看文档就有猜测过这样傻白甜的问题,副作用监听了依赖它的变量,是如何很好的控制多个变量触发的机制呢,是每个变量触发都会快速的执行一次吗?

用户自定义的副作用函数会在全局缓存一遍会异步地刷新它们,Vue组件的更新函数也是一个副作用函数,刷新机制是在更新函数之后去一遍一遍走自定义的副作用函数
  • watch

watch和Vue2一样,只不过和watchEffect的区别就是:

1. 仅在依赖数据源变化才会回调副作用函数

2. 可以访问到变化前和变化后的值

3. 可以自由设置哪些值是需要监听的

共同点就是:

1. 清除副作用和停止监听

2. 副作用刷新机制也是一样的哟

const star = ref(0);
watch(star, (star, prevStar) => {
  // do something
});

// 监听多个
watch([star, rose, flower], ([star, rose, flower], [prevstar, prevrose, prevflower]) => {
  // do something
});


结语

这段时间拖得太久,Vue系列还有一个Vite的浅析还没发,也基本差亿点就结束了,因为是国庆节写的,也希望大家多多支持呀,冲鸭!

1_-Ijet6kVJqGgul6adezDLQ.png

在以前的react 开发中我们习惯使用class类的写法写react组件,但是现在随着函数式编程的流行,函数作为js的第一公民,我们在react使用函数编写业务组件,在原来只能写无状态的组件,没有自身的state,hook的出现可以让函数组件钩入一些方法,可以让我们在函数组件中使用state和一些class类的组件写法,那么我们这篇笔记将以react-hook文档为基础来做一些总结。

Hook 简介 - React

State Hook

import React, { useState } from "react";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );  
}

export default Main;

我们可以使用useState这个hook来声明state,返回是一个数组,第一个是我们的变量,第二个是设置这个变量的函数,我们使用数组解构的方式结构出来,那么在这个函数的作用域中直接使用count来展示这个state,我们调用函数直接调用setCount这个函数,这样我们就简单的使用statehook这个钩子来实现class组件this.state这个功能了,注意这个set函数和class中的setState不同,在class组件中的setState中,是要传入当前示例的全部state对象,我们往往要使用对象合并API来操作,但是hook中的set函数只需要传递当前的要设置的值即可。

setCount(100);

我们可以设置更多的state

const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

我们没有必要使用这样的hook来声明多个变量,在之后的Q&A中,文档会建议我们更好的方法。

Effect Hook

在class写法中的各种生命周期的钩子,在函数组件中也有对应的hook来替代它们,那么这些钩子我们叫做effect hook,在组件渲染成功后通常会进行一些基于业务的操作,所以我们也称之为“副作用hook”,下面我们对比一下class写法和hook写法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

在componentDidMount以及componentDidUpdate这两个生命周期中,当组件被挂载或者已有prop或者state变更都会触发对应的document.title的业务逻辑,那么在class这样的写法,同样的逻辑在不同的生命写,这会让代码又臭又长,不好维护,我们使用hook来写,那么就非常简单。

import React, { useState, useEffect } from "react";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );
}

export default Main;

useEffect可以传递一个函数作为入参,我们写一个useEffect就相当于写了组件挂载和更新2个钩子,那么当组件销毁(清除副作用)我们可以这么写

useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
        return () => {
            API.off(); // 销毁时再取消订阅API
        }
});

useEffect会在每次挂载和更新都会执行我们传入的逻辑,如何控制它将在后面我们会有具体的例子。

那么我们使用effectHook和传统的class中的挂载和更新生命周期还是有区别的。effectHook不会阻塞浏览器的屏幕更新,它会让应用看起来更快。

我们肯定写过下面的逻辑代码:

componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
        // 调用A API
        A();
        // 调用B API
        B();
        // 调用C API
        C();
  }

不得不说,不同的业务逻辑混在一个生命去写,尽管我们有时会把不同的逻辑封装到不同函数中进行组合调用,但是代码还是难以直接去表达业务逻辑,这让维护代码的人叫苦连连,那么我们使用hook,将可以使用像stateHook一样,可以调用多个hook:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
    return () => {
      API.off(); // 销毁时再取消订阅API
    };
  });
  // ...
}

react会顺序执行多个effectHook

那么在官方文档中,提到了一个我也很苦恼的问题,在我进行第一次学习effectHook的时候,为什么组件更新还需要调用effect,直到文档做了一个很简单的查询好友信息的demo:

通过好友ID查询好友详情,好友详情组件需要接收一个prop,那么如果当prop进行更新,组件内的effect回调不会触发还是展示上一个好友的信息,而在清除副作用的时候,因为ID错误导致程序异常,那么显然effect设计如此是为了规避程序BUG,那么在class类中,我们应该使用如下方式来规避这样的更新问题:

componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
      API.off(prevProps.id); // 销毁时再取消订阅API
    // 订阅新的 friend.id
    API.on(prevProps.id);
 }

而使用hook则不需要关心更新组件带来的异常, 因为使用effect时,在调用一个新effect时就会清除上一个effect(触发清除回调effect),也就不会产生因为忘记写update逻辑造成的BUG

我们既然知道了effectHook更新设计的原因,那么当部分业务逻辑因为更新而造成的多余性能浪费,class和hook解决方案是怎么样的呢?

// class的写法,比较ID是否变更,变更再进行逻辑
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

// effectHook提供了第二个可选参数,我们可以传入一个数组,比如
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

当我们传入一个空数组时

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []);

那么此effect将在挂载销毁执行,任何值的update都和此effect无关,所以这样的写法一定要注意应用场景。

总结Effect Hook:

  1. 自带的清除机制,可以避免在class中因为update而造成的问题
  2. 使用effect可以让业务逻辑分离解耦,不会像class中的生命周期分离逻辑变的很难

Hook规则概要

hook本质就是JS的函数,但是使用它也需要规则,虽然有专门的语法检查工具:

eslint-plugin-react-hooks

  1. hook需要写在函数中最高的层级,不要在循环,嵌套中去使用它们
  2. 只能在React中使用hook,不要在普通JS函数中去使用hook

关于插件的使用,文档也有非常清楚的配置方法

Hook 规则 - React

至于为什么hook只能在函数中最高层级去使用,官方也在文档中说明了:

demo1:

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

useState和useEffect如果都在最高层(函数作用域的最高层)中,那么React就可以把对应的Maryname 进行关联Poppinssurname进行关联,react通过声明的顺序去查找对应的变量,每次渲染的顺序和声明的顺序都是相同的,所以React才可以找到,那么如果我们写了如下的代码:

if(name !== null){
    useEffect(function persistForm() {
    localStorage.setItem('name ', name);
  });
}

这样触犯了hook的第一条规定,导致了程序有可能不会声明useEffect这个钩子,如果没有声明,那么声明的顺序和渲染的顺序就会不一致,就导致这个钩子之后的所有声明的钩子都将会被提前执行,导致BUG产生

自定义Hook的实现

我们自己写一个求差的小demo,参数传入减数和被减数简单体验一下自定义hook:

这个demo展示了hook和组件的传参,当组件的count改变时会触发到hook中的effect,effect会重新计算差并返回。

import { useState, useEffect } from "react";

export function useSeekingDifference(
  subtraction: number,
  minuend: number
): number {
  const [difference, setDifference] = useState(0);
  useEffect(() => {
    setDifference(subtraction - minuend);
  }, [subtraction, minuend]);
  return difference;
}

使用hook

import React, { useState, useEffect } from "react";
// 引入自定义hook
import { useSeekingDifference } from "./../hook/computed";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
 console.log(useSeekingDifference(count * 2, count)); // 使用自定义hook
}

export default Main;

我们写自定义hook的目标就是:

  1. 抽离业务逻辑

但是对于业务性非常强,以及对状态管理有频繁操作的建议使用redux(尽管我没用过,但是官方建议这样)

我们书写hook时需要注意:

  1. hook不是react特有,只是遵循了hook的规定的函数
  2. 必须使用use开头,因为不仅仅代表了它是一个hook,也可以让插件去做校验规则
  3. 相同hook不会共享state,每一个hook都是有独立的state和effect的,是重用状态逻辑机制

前排提示:非推广软文

微信公众号: 因卓诶;此文已同步到因卓诶blog

Uniapp这两年是Vue开发者很喜欢的跨平台开发框架,作为一个国产开发框架,其实文档和周边工具都对国人非常友好,但是由于框架本身的跨多端所以从诞生以来都被很多开发者诟病“坑太多”,那么这篇文章将结合本人2年Uniapp开发经验,给新手小白一个从0到1的教程&踩坑说明。


目录

  • 历史
  1. uniapp编译器发展历程
  • 踩坑经验(框架部分)
  1. 条件编译
  2. onLoad和onShow 用合适的生命周期
  3. 触底加载和下拉加载的实例
  4. 如何操作DOM
  5. 上传图片/文件
  6. websocket
  7. Nvue介绍
  8. SubNvue介绍
  9. 全局通信
  10. h5+API介绍
  11. 应用内更新和wgt的介绍和使用
  12. APP自定义tabbar
  13. 如何协同原生插件来工作
  14. manifest.json简介
  15. uniapp如何请求数据
  16. uniapp应该用什么方式创建项目
  • 社区
  • 插件市场
  • HbuilderX
  1. 简介和快速开发的准备工作
  2. 打包APP前的准备
  3. uniapp的debug
  4. APP打包
  5. 关于IOS
  • 拓展阅读

历史
Uniapp从诞生到目前,经历了3次重大变革,首先是最初借鉴了Mpvue的模板语法(非自定义组件模式)这个模式是性能最差且支持Vue语法有很多欠缺的版本,在当时我认为Uniapp开发APP是满足不了企业需要的,倒是做一款小程序开发框架很是不错。

在2018-2019这个时间段,也就是uniapp是模板编译模式的时代,网上对于uniapp也是褒贬不一,社区的完善度还有官方的UI支持都是非常落后的,唯一吹一吹的就是QQ群当时很火,官方的解答和处理BUG的速度还很快。我个人认为web前端跨平台框架受到了很多原生安卓&IOS开发者的排挤,拿生成的代码质量和性能说事,说不定自己也没有使用过。

在我接触uniapp不久之后(大概几个月)就出了自定义组件模式,这个组件模式相当于革命性的更新,支持用户编写的Vue组件转换成微信小程序的自定义组件,基于这个uniapp在安卓平台上放置了JSCore,从这个时候开始uniapp开始慢慢的被更多的原生开发者接受,因为跨平台开发的好处是在是太大了。

那么接下来的一次更新让我瞠目结舌,因为由于19年的年底这一段时间我没有从事uniapp开发,直到今年才继续开发uniapp,发现了uniapp的一次重大更新:“v3引擎”。

V3引擎同样也是Dcloud自研,我认为主要的更新在于安卓和IOS端,开始注重了启动速度和包体积大小,我们可以在V3更新说明中看到。

到此为止,我认为uniapp到此刻应该是推广的时候,让前端开发人员坐上跨平台开发的小车车。

踩坑经验(框架部分)

首先,uniapp绝大部分官网描述的API,比如设备信息,内存,蓝牙等等都是原生开发比较常用的,uniapp的优点就体现出来了,不同平台的各种API都会略微有差异,那么我们应该仔细看文档。

如果文档已经说明此API存在【平台差异】,那么我们应该注意API下方可能会有这样的【tips】

这也是文档的一个小坑,很多新手如果不仔细看文档,在调API的时候不考虑兼容问题,导致在小程序/APP中,比如出现安卓和ios功能不一致的问题,如果看仔细文档,那么百分之99以上的问题都会在tips中说明。

注释魔法:条件编译

其次uniapp还有一个非常强大的功能【条件编译】,这也是开发中非常常用的功能,如下,我们可以使用像C语言中核心注释,注释中的代码片段将在指定的平台出现,反之亦然;

<!-- #ifdef APP-PLUS -->代码片段,APP-PLUS代表着APP端,ifdef包含,ifndef不包含,多个平台可以用空格隔开<!--#endif-->
<!--#ifndefAPP-PLUS-NVUEAPP-NVUEMP-->编译多个平台<!--#endif-->

条件编译可以存在于任何地方,template css js;同样uniapp支持更强大的条件编译:比如支持在page.json中进行判断,达到不同端不同的分包功能;静态资源也可以进行条件编译,通过static下构建platform目录即可把不同的静态资源编译到不同的平台上去;那么同理也可以把页面进行条件编译,即不同平台不同页面;

onLoad&onShow

我们已经了解了uniapp极具特色的条件编译之后,我们可以了解一下uniapp必不可少的生命周期,uniapp在目前版本支持vue的所有生命周期以及绝大多数API;比较重要的nextTickcompile不支持,组件选项中比较重要的是render函数不支持,具体更多的支持特性表请移步官网查看(https://uniapp.dcloud.io/use?id=vue%e7%89%b9%e6%80%a7%e6%94%af%e6%8c%81%e8%a1%a8),下面我们将推荐开发中常用的生命周期以及应用场景:

onLoad(e){// 组件渲染未完成但是已创建的钩子,与Vue的create同理,这里推荐使用onLoad代替create// 行参e可以获取当前路径的参数,比如当前页面是b,如果a跳转到b页面是如下url:///pages/index/a?type=1 console.log(e.type); // "1"}

onShow(){//用于监听:页面在屏幕上显示时触发,从APP应用后台到APP前台也会触发//通常我们可以使用这个钩子做一些非列表页面的数据刷新(比如从上一个页面返回,然后此页面刷新)}

触底加载&下拉加载的实践

APP中有一个非常常见的场景叫做触底加载,那么针对APP页面级别的滚动触底回调就是onReachBottom

onReachBottom(){//在这里进行当前页加1this.listConfig.nowPage ++;// 调用加载列表的方法this.initList();}methods: {async initList(){consttoList=awaitgetALLTodoList({limit:this.listConfig.pageSize,offset:(this.listConfig.nowPage-1)*this.listConfig.pageSize),})}}

那么下拉加载这个功能在APP中非常常见,我们需要在page.json中对应的节点style下开启下拉加载

"enablePullDownRefresh": true

那么对应页面的监听钩子将会触发

onPullDownRefresh(){//用户下拉了页面}

【注意事项】不管是触底加载和下拉刷新此实例都是针对页面级别,如果你不知道何为页面级,那么请耐心看完uniapp的组件相关内容,在uniapp组件中有一个,它提供一个滚动视图,在这个滚动视图中有着自己的下拉加载和触底加载方法,因此如果页面中存在此组件,开发者应该妥善处理组件级别的滚动和页面级别的滚动,这是非常重要的。

在开发中,我们经常会出现一些列表的场景,那么在uniapp中处理长列表,我们不应该使用scroll-view组件级别的滚动,而应该使用页面级别的滚动,这样性能会更好

如何操作DOM?

我们通常在开发Vue应用时,很少直接操作DOM,在uniapp中没有暴漏DOM这个概念,但是我们可以通过指定的API去操作DOM,尽管这是非常不建议的,但是在某些业务场景不得不说

它是很方便的;

lettabInfo=uni.createSelectorQuery().select("#tab");tabInfo.boundingClientRect((data)=>{//目标区域的信息}).exec();

文件的选择

uniapp在文件的选择上非常丰富,举一个例子比如说图片,我们可以在文档中很清楚的查询到对应的API,通过封装的API我们可以通过配置参数来chooseImage,然后拿到临时路径再继续上传;

uni.chooseImage({count: 6, //默认9    sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有    sourceType: ['album'], //从相册选择    success: function (res) {//临时路径 res.tempFilePathsuni.uploadFile({url: '', //仅为示例,非真实的接口地址            filePath: tempFilePaths[0],name: 'file',formData: {'user': 'test'            },success: (uploadFileRes) => {console.log(uploadFileRes.data);            }        });    }});

重点来了,我们上传图片固然简单,但是上传文件是非常难的,我们如果在APP端想做一个上传文件的功能,就要尽量使用html的帮助,由html的input type=“file”来做上传,那么首先得简单了解一下利用html如何上传文件。

  1. 首先我们需要使用h5+API中的webview相关的API

    let wv = plus.webview.create("", "/hybrid/html/index.html", {'uni-app': 'none', //不加载uni-app渲染层框架,避免样式冲突 top: 0,height: '100%',background: 'transparent' }, { url, header,key: name, ...formData, });wv.loadURL("/hybrid/html/index.html"); // 加载本地的html文件currentWebview.append(wv);//把html追加到当前页面中
    把本地的HTML加载到APP之上之后,我们在本地编写的HTML文件中引入对应uni.webview文件以及需要的业务JS文件,我们在HTML是这样做的(核心代码)
    <scripttype="text/javascript"src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"> <scriptsrc="js/h5-uploader.js"type="text/javascript"charset="utf-8">

我们在本地编写的业务JS

xhr.onreadystatechange=(ev)=>{if(xhr.readyState == 4) {if (xhr.status == 200) {progress.innerText='成功';//从这里可以看出,上传成功之后,我们通过改变html的title把信息返回        title.innerText = `${file.name}|^$%^|${src}|^$%^|${suffix}`;      }else {        progress.innerText = '上传失败了';}      setTimeout(()=>{        tis.style.display = 'none';// 上传成功,关闭了上传webview的窗口        plus.webview.currentWebview().close();},1000);    }  };  xhr.send(formData);

当index.html的title变化之后,在vue组件创建webview的代码之后,我们可以在当前webview去监听标题的变化。

//wv就是plus.webview.create的返回,上述代码有举例子wv.addEventListener('titleUpdate', ({  title}) => {  console.log(title)}

那vue组件拿到回调信息之后,就可以正常拿到图片信息去在页面中渲染。

顺便说一句,在文档的后面,会给大家介绍uniapp相关生态,一些业务组件插件都会在相关生态中查询到。

websocket

oh,又到了这个该死的东西了,今年的一段时间内,在uniapp上使用websocket可是真的太难了,先说一下事情的经过,我司开发的APP中本来使用了聊天相关功能,之后在部分业务中加入了websocket,前端需要根据websocket实时获取一些信息,在测试APP的时候发现,当2个websocket并行连接的时候,发现只有一个连接能有效的发送和接受消息,无奈只好去文档中寻找答案。

App平台自定义组件模式下,以及支付宝小程序下,所有 vue 页面只能使用一个 websocket 连接。App下可以使用 plus-websocket
插件替代实现多链接。App平台,2.2.6+起支持多个socket链接,数量没有限制。
uniapp-websocket文档
import socket from'plus-websocket'// #ifdef APP-PLUSObject.assign(uni, socket)// #endif

通过把依赖中的socket合并到uni中,把uni中原本封装的socket进行替换;

Nvue

uniapp支持用户编写.nvue的组件,那么此组件将会使用改进后的weex引擎进行渲染,提供了原生渲染的能力,那么如果你是weex用户如果用nvue写,那么全程和weex的写法无异,如果是普通的vue开发者使用nvue开发,那么将注意,nvue对于css和js的限制有非常多,尤其是css很多常用的简写都将不能支持,所以我们需要额外查询对应的weex框架的文档。

那么我们在APP中有必要全部使用nvue原生渲染么,使用普通vue组件进行webview渲染有什么缺点么?那么我建议APP中核心/用户访问次数多/需要更强劲的性能的页面可以使用nvue,其余普通页面我们仍然可以使用vue页面。

那么如果是weex用户转uniapp,可以全项目使用nvue,uniapp支持纯原生渲染模式,可以减少APP包体积,也可以减少内存占用,因为此时webview相关依赖是不存在的。使用nvue开发,即保留了原weex开发者的习惯又可以提供强大的API能力,这是非常让人兴奋的开发体验。

SubNvue

subNvue本质上是一个nvue组件,支持在普通vue页面之上运行,subNvue能覆盖map,video等原生组件,uniapp有很多办法支持覆盖原生组件,subNvue是我认为最好的方法,因为相比组件,不能嵌套,而且写起来和原页面耦合;又和webview相比,需要原生h5+api做技术支撑,nvue相对来说更多vue开发者友好。

实例:由于首页有swiper那么悬浮的头部需要遮挡住swiper,在pages.json中注册即可

"subNVues":[{"id":"indexTitle",//唯一标识"path":"pages/index/subNVue/indexTitle",//页面路径"style":{"position":"fixed","dock":"top","background":"transparent"}}]

uni全局通信

在uniapp中,vue,nvue组件中进行信息传递是很难的,尤其是vue和nvue中的信息传递我们可以通过全局通信的方式来做业务逻辑。

uni.$emit('update',{msg:'我是首页,用户下滑了'})

uni.$on('update',function(data){console.log("我是subnvue头部,你下滑了,那我就改变自身的透明度动画")})

适用场景:

vue与nvue,nvue与vue间的通讯
tabbar页面之间的通讯
父页面与多级子页面间的通讯
uniapp-emit文档

注意事项:

  1. 只有在页面打开时候才能注册或者监听

h5+API

其实像uniapp这样的框架(或者其他的跨平台框架)我认为h5+api带动了整个相关技术,我们在没有这样框架的时候,都是使用h5+api完成一个又一个优秀的APP,这都离不开这个非常强大的API

在前面的demo中,我们或多或少地看到了h5API的影子,下面我们再复述一遍在uniapp中使用h5+API时的一些注意事项,这非常重要。

首先uniapp内置了HTML5+引擎,我们可以直接调用h5+相关规范,但是在小程序,H5端并没有对应的规范拓展,所以在这些平台不会识别“plus”这个变量,所以我们需要写条件编译

// #ifdef APP-PLUSconsole.log(plus)//#endif

在普通的H5项目中,我们如果要使用H5+API,我们需要进行ready,但是在uniapp不需要ready,直接上去就是一套军体拳就是干。

还有我们使用一些监听事件的时候,由于uniapp没有document这个对象,所以需要使用

plus.globalEvent.addEventListener("这里写h5+拓展的事件")

应用内更新&WGT的使用

应用内更新和WGT这一块是每一个APP不可缺少的部分,所以我这里会比较详细的做一些介绍和实践。

首先我们来了解一下WGT是干嘛的。wgt是APP资源更新包,通常来讲这个包体积很小,只有1m2m左右,APP在没有拓展原生模块下或者没有增加修改一些原生的插件情况(如果不懂这个概念,后面会讲到)APP是可以使用wgt资源包升级的,整个升级过程用户可以是“无感知的”,我们通常在APP中可以看到所谓的升级提示,这个是有感知的;而有的时候你并没有升级这个APP缺发现ui变化,功能变化,那么这个时候就是无感知升级,技术手端也有很多,比如云端更新代码,在uniapp中我们可以用wgt来实现这样的功能。

我们先来APP普通的有感知更新,uniapp的实例。首先请求接口去请求服务器,拿到最新版本的下载地址,在这之前我们需要判断当前APP的版本号,那么这里就有一个小坑,我们千万不要使用下面这个API获取版本号。

plus.runtime.version

我们如果使用wgt作为资源升级包的话,那么此API获取的版本号不是准确的,它会获取APP内核的应用版本号,我们必须要使用

plus.runtime.getProperty(plus.runtime.appid,function(inf){console.log(inf.version)});

我们用此API获取到的版本号去数据库比对(前方伪代码):

// 比对版本方法, 此方法网友提供,侵权删除const compare = (curV, reqV) => {if (curV && reqV) {//将两个版本号拆成数字var arr1 = curV.split('.'),        arr2 = reqV.split('.');var minLength = Math.min(arr1.length, arr2.length),        position = 0,        diff = 0;//依次比较版本号每一位大小,当对比得出结果后跳出循环(后文有简单介绍)while (position < minLength && ((diff = parseInt(arr1[position]) - parseInt(arr2[position])) == 0)) {        position++;      }      diff = (diff != 0) ? diff : (arr1.length - arr2.length);//若curV大于reqV,则返回truereturn diff >= 0;    } else {//输入为空returnfalse;    }}
const downLoadFail=()=>{    uni.hideToast();    uni.showToast({title: "下载新版本失败,请在设置页面检查更新再试",duration: 2000,icon: "none",position: "bottom"    });}


// 获取更新列表,取最新的更新包constupdatePackageList=awaitgetUpdatePackageList();if(compare(updatePackageList[0].version,"根据上面的方法获取的版本号")){// 如果存在更新// 整包更新  uni.downloadFile({url: "整包APK的下载地址(非wgt包地址)",success: res => {if (res.statusCode === 200) {//下载之后打开临时路径的文件        plus.runtime.openFile(res.tempFilePath);      }else {        downLoadFail(); // 调用更新失败的方法      }    },fail: error => {      downLoadFail(); // 调用更新失败的方法    }  });}


以上是普通的整包升级的伪代码,如果遇到强制更新,非强制更新,即开发者需要自己控制对应的button,这里就不阐述了。

对于wgt的更新,相比整包更新有一定区别,因为整包更新非常简单,无非就是下载apk文件,然后下完之后打开,让用户自己安装。wgt的生成需要在hbuilderx中操作(后续hbuilderx篇会讲到),我们需要把wgt包上传在服务器上,前方伪代码:

uni.downloadFile({url:"wgt下载地址",success: res => {if (res.statusCode === 200) {// 安装wgt        plus.runtime.install(res.tempFilePath, {  force: false        }, function() {  // wgt安装成功if(silence){            uni.showToast({title: "已更新最新的资源,重启应用获取更佳的用户体验",duration: 4000,icon: "none",position: "bottom"            });          }else {            plus.runtime.restart();// 不是静默升级,就立即重启应用          }        }, function(e) {if(!silence){            downLoadFail();          }        });       }else {        downLoadFail();      }    },fail: error => {      downLoadFail();    }  });

细心的同学已经发现了,wgt安装需要用户重启,否则不会生效。所以我们在开发中会有一个silence的选项,如果是true那么会选择静默更新(用户无感知)这个时候我们只需要install安装即可,如果不是无感知,那么需要自动重启。

APP自定义tabbar

我们在企业级开发中,总会有一些沙雕产品想这种“凸起”或者“奇奇怪怪”的tabbar(希望我司产品不会看到)事实上,这种自定义tabbar对于uniapp开发者来说,是有一定的难度,首先如果是完全定制,那么需要有较强的webview功底或者subnvue功底。

由于自定义tabbar代码较多,我们可以借助一些插件来实现,但是值得注意的事情是,如果自定义tabbar在每一个页面都引用,会出现抖动闪烁的问题,所以我们应该在main.js中去draw这个tabbar

Vue.prototype.$tabbarView=newTabbarView();

然后在每一个tabbar页面去watch对应的路径变化,然后改变当前选中是第几项item

onShow(){//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},onHide() {//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},

这个watch等方法,是目前我司使用的组件的API,所以具体的定制tabbar思路知道了,我们使用一些第三方插件,可以尽量少踩一些坑。

引入原生安卓/IOS插件

ios/安卓原生的插件我们可以在【插件市场】(之后会讲到)中找到,我们在manifest.json配置本地插件即可,那么具体的插件的添加办法,如果是在插件市场直接点击添加到APP中。

如果我们是云打包(之后会说到这个概念)那么建议大家去社区直接购买然后添加,在配置文件中选中即可。

constPluginName=uni.requireNativePlugin(PluginName);

此API只存在于APP端,所以需要条件编译,传入插件名称,就会在对应的APP中找已添加的插件是否存在(如果不懂这块,可以看下面一篇说明:《manifest.json:uniapp的半壁江山》),如果插件已被添加则正常使用。

我们这边实例选用的是uniapp官方文档中的demo例子,插件是官方提供的原生增强提示框:

constdcRichAlert=uni.requireNativePlugin('DCloud-RichAlert')dcRichAlert.show({    position: 'bottom',    title: "提示信息",    titleColor: '#FF0000',    content: "<a href='https://uniapp.dcloud.io/' value='Hello uni-app'>uni-app</a> 是一个使用 Vue.js 开发跨平台应用的前端框架!\n免费的\n免费的\n免费的\n重要的事情说三遍",    contentAlign: 'left',    checkBox: {        title: '不再提示',        isSelected: true    },    buttons: [{        title: '取消'    }, {        title: '否'    }, {        title: '确认',        titleColor: '#3F51B5'    }]}, result => {console.log(result)});

说明一下引入原生插件的API,不管是vue还是nvue页面都可以使用。

如果要debug,那么就必须重新打一个自定义基座包(下面会讲到这个概念)否则不会生效。

manifest.json:uniapp的半壁江山

由于这个地方是uniapp配置项重中之重的地方,虽然用hbx直接预览它会帮助我们自动格式化,但是可能我的解释能让这块变得更简单。

app的ID是非常重要的,一般建立成功之后就不需要再更改,我们在注册微信开放平台,支付宝或者高德等等,都需要用到APP的ID和包名(packageName),包名设置会在之后介绍到,基本设置中包含了几个重要的信息,一个是应用版本名称一个是应用版本号,我们在做更新的时候需要+1,包括生成wgt包的时候。

我们可以在此处可视化的配置APP的模块权限,那么uniapp封装的模块权限真的是傻瓜式的,配置一些平台的key和secret然后再使用uniapp对应的api即可,那么肯定的事是这是增加包体积的。

引入原生插件,app如果有对应特殊的业务需求也可以编译原生的插件,具体如何引入本地原生插件,我们之前也提到过。

剩余的配置都是针对于小程序/H5的配置,所以根据我们自己的业务可以自己去查询文档。

uniapp应该使用哪种请求库呢?axios支持么?

uniapp是跨端框架,为了迎合跨端,我们不可以使用axios,因为axios不支持APP原生端,它仅仅支持网页端,所以我们可以使用uniapp提供的API(uni.request)一般这种API就够用,但是如果你想有像axios一样的配置体验的话,强烈建议你使用flyio.js这个库我们在新建项目时候会帮助我们安装好的。

uniapp项目是cli创建好还是hbx创建好?

创建uniapp项目,可以分为2种模式:

  1. cli命令行创建
  2. hbuilderX创建

结论:优先使用cli命令行创建,因为对于开发vue
web的人来说,cli命令行是最熟悉的,那么uniapp的cli方式创建的项目整体的目录结构类似于普通vue
web项目,而且我们直接使用npm快速安装依赖,像普通方式去引入;uniapp在版本支持上也是在cli项目上最先上线,可以使用cli体验到最新的功能,对于初学者来说,标签的学习是一个必须考虑进去的成本,cli方式创建的工程允许使用普通html标签,比如

它们在编译的时候会转换成uniapp的标签组件(尽管这样写并不推荐

hbx创建的uniapp项目,是随着hbx软件的版本升级而升级,cli创建的版本必须执行

npm update

才能更新到最新版本,所以我建议大家搭建项目使用cli项目,在开发中使用uniapp的标签不要使用html标签。

社区

uniapp有着非常活跃的社区,这是真的切身体验过,使得整个开发遇到的问题我们都可以在社区找到(https://ask.dcloud.net.cn/

我们遇到了所有的bug,首选需要查询是否是自身开发问题,如果自测确认没问题那么就可以在社区发布bug贴,如图

发布成功之后我们在文章底部邀请,输入dcloud就可以邀请官方人员解答,如果出现bug,也可以上传代码压缩包(指定官方人员查看)这样可以更快地让官方找到问题帮助你解决问题。

插件市场

打一个广告先,我的UI库的alpha版本在插件市场发布,尽管现在不开放下载,大概在年底左右会重构给大家带来高质量的组件(名子叫i-uniapp)希望大家关注一下

https://ext.dcloud.net.cn/ 插件市场地址

插件市场有很多vue组件/原生sdk等等,具体的安装方法我们已经在前面提到了,使用hbx开发uniapp项目可以直接点击导入插件到项目中

HbuilderX:开发uniapp神器(本文章重点)

介绍Hbx和快速开发指南

我可以很负责任的说,虽然内核是eclipse,但是也算是国产的编辑器IDE,它在Vue的支持程度上足以让我震惊,说实话从Vue开发角度来说,HbuilderX比vscode好。但是hbx被吐槽地点也有很多,比如占用内存高,UI不如vscode/sublime等等,但是这是IDE之争不在我们文章的讨论重点之内,所以我们如果要开发uniapp项目,使用hbx开发是上上策

我们如果是使用vscode或者sublime,可以在hbx调整快捷键语法

插件安装,我目前安装的是这些,为了教程的顺利和以后开发者的开发顺利,请务必装这几个基础插件。

打包APP前的准备

我们首先来看一下打包APP需要什么

我们需要一个安卓证书,我们来生成一下这个安卓证书

  1. 首先需要安装jdk,如果没有jdk,就去安装,然后在其bin文件夹下运行cmd
    keytool-genkey-alias这里写证书别名-keyalgRSA-validity36500-keystore这里写证书名称.keystore
其中参数-validity为证书有效天数,我们可以写的大写。-alias后面是证书别名
输入密码的时候没有显示,就输入就行了。退格,tab等都属于密码内容,这个密码在给.apk文件签名的时候需要。输入这个命令之后会提示您输入秘钥库的口令
接着是会提示你输入:姓氏,组织单位名称,组织名称,城市或区域名称,省市,国家、地区代码,密钥口令。确认正确输入y,回车
作者:草字头乌君
链接:https://www.jianshu.com/p/14add4a02ed6 来源:简书
keystore的密码一定要记住,如果忘记需要执行另外一个命令去查询
生成keystore成功之后我们可以在其bin目录下发现一个以你设置为名字后缀为keystore文件
  1. 填写包名

这个包名一般标准是以com开始的(java工程中的包名标准),ios不清楚,具体可以询问具体的ios开发,命名包名是一个非常重要的事情,因为包名关系着一个APP的主要信息,在上架应用商店的时候,包名则代表着APP的唯一性,所以一定要设置一个尽量贴合企业/个人的信息相关的名字,比如公司叫“将进酒”:

com.qiangjinjiu.andriod

或者公司有具体的规定,则按照自身公司进行设置,这里就不阐述了

  1. 渠道包选项

对应的渠道包,可以在对应的平台做一个标识,可以在后台看到每个平台的使用指数,这个根据业务需求可以打开,一般不会选择

  1. 原生混淆

可以对js和nvue文件进行原生混淆,提高安全性

  1. 广告投放选择

uniapp自带的广告插件,根据业务需要自行选择

debug:自定义基座/小程序模拟器

我们在上一段讲解了如何打包APP,打包APP之前的准备工作,那么细心的同学会发现,打包APP中有这样的一个选项

什么是自定义基座,自定义基座是我们开发日常debug的包,这个包没有压缩,所以会比正式包大,正式包记得选择正式包不要选择基座包。自定义基座APP包安装到手机上,可以连接HbuilderX上,进行调试,下面介绍一下如何打自定义基座然后debug调试

  1. 云打包:发布之前的编译阶段

  1. 打包成功会返回成功信息,自定义基座打包

打包成功之后,我们拿起我们的诺基亚手机,打开usb调试模式(作为开发,如果连这个都不知道咋打开,那就去google吧),调试模式打开之后,一定要选中hbx左边的项目,然后点击运行,运行手机模拟器,如图(重点):

基座选择一定要选择自定义基座(如果你没打包自定义基座这里就没有这个选项),然后等待手机,会出现安装基座(app)的提示,点击安装,然后手机会自动打开APP,然后这个时候我们就可以更改代码,ui,然后手机及时更新变化。

那么debug自定义基座有什么注意事项呢?

1.当我们要调试登录,分享,地图等具有原生权限功能的话,需要配置对应平台的key和secret。那么我们增加,修改了原生插件配置/原生插件的修改,这个时候我们需要重新打包自定义基座查看最新的代码。

2.切记不要把自定义基座当成正式包发布

3.不要打包打正式包debug,因为云打包正式包一天只有免费20次,我们debug用自定义基座就行了。

那么我们觉得,日常的开发除了调试第三方登录,地图等需要用手机去实际测试,那么平时我们更改UI布局,难道也用自定义基座这么麻烦么?

答案是否定的,我们要知道,虽然布局UI在不同机型可能会产生偏差,但是我们日常可以使用小程序模拟器进行开发,然后开发完毕,在实体机型上进行初步测试,看看是否Ui或者功能有偏差,再具体更改,因为小程序模拟器是最接近APP的,大部分UI布局功能都是最贴近用户手机的。

那么使用小程序模拟器调试,这里也有一些注意事项,我们如果运行项目到微信小程序,如果是第一次,我们需要提前在小程序模拟器中的安全打开RFC调用

设置->安全->端口开启

然后此时我们可以使用hbx来启动微信小程序进行开发啦。

APP打包

我们项目模块迭代开发之后,需要打包成APP,那么我们有2种途径打包

  1. 云打包
  2. 离线打包

以及特殊的wgt资源包(对应之前提到的wgt更新)

首先我们来说一下云打包,云打包是dcloud提供的云打包服务器,我们在客户端HBX编译之后就会进入云打包的队列,由dcloud打包服务器打包成功之后返回给hbx控制台临时的下载链接。

那么在之前,我们已经了解了自定义基座,那么这个自定义基座的打包和云打包(正式包)只是一个选项的问题,云打包每天有20次的免费机会,所以切记我们debug千万不要生成线上包

离线打包指的是我们可以把uniapp工程生成出原生工程,由我们开发者自己去打包,那么我相信如果有原生基础的开发者会很乐意这样做,但是务必提醒一点就是社区很多的问题都是离线打包造成的,那么为了更稳定的APP开发,我建议使用云打包。

我们在之前提到的wgt资源包,我们应该这样生成,生成成功的wgt资源包上传到服务器,然后按照前面的demo进行更新

关于IOS

其实关于IOS,本人并不是了解很多,我可以简单讲一下,可能对于新手很重要。

1.uniapp的应用是可以被审核成功的,因为19年年底苹果发了一则通知,大概是性能差劲的套壳APP不会被上架,但是2020年,必须知道uniapp不是套壳APP,它也有原生宣传,webview渲染性能也是非常强劲的,没有网上说的那么不堪。
2.如果是windows用户那么我建议你使用mac开发uniapp,因为windows开发打包出的ios包不太方便去上架等等,而mac系统没有这个顾虑。
3.windows新手如果没有上架过,可以在windows下载ApplicationLoader, 如果是mac用户那么强烈推荐你下载Transporter App ,老司机就用xcode内置的上传吧~

拓展阅读
1.uniapp统计功能,免费,安全,统计数据非常详细,支持自定义埋点。

https://tongji.dcloud.net.cn/

2.uniapp云开发,现在云开发,云数据库这么火,早在几年前小程序就有这样的东西,云开发也是今年uniapp非常大的更新,现在支持腾讯云和阿里云,据说还比较便宜,感兴趣的小伙伴可以去看看。

https://unicloud.dcloud.net.cn/login

3.uniapp广告平台,非常适用于个人,个人做的APP可以通过这个广告平台变现。

https://uniad.dcloud.net.cn/login

基于NPM的包或者库,项目中的package.json是对项目的描述,这个json对象中的script标签就是npm运行脚本,vue.js在这里配置了如下的内容

"build": "node build/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", (**注意这一块是用逗号分割的**)
"build:weex": "npm run build -- weex"

vue当然有很多script命令,但是仅仅只有这几种是build的,build:ssr和build:weex其实和build一样,只不过提供了不同的运行参数

查找build的入口文件,vue是如何做build源码的?

打开对应的build/build.js

let builds = require('./config').getAllBuilds()
// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',') (**分割出来的数组是["web-runtime-cjs","web-server-renderer"]** )
 // 通过getAllBuild函数返回的config对象对其打包模式进行了rollup的配置,包括output等设置
 // 而下面这一段代码的b则是返回的config对象,filters这个参数数组使用some来判断builds中output中的file(被resolve函数定义了,这个resolve函数下面会具体讨论)
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

这一段代码其实非常简单,作者也在源码中写了注释解释了这段代码的作用

通过命令行arg构建过滤器

其中引入的config 文件调用了getAllBuilds这个方法,在config.js可以看出

 exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

这一句代码是导出config的关键代码,builds是预定义的一些对象

 // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
entry: 入口文件
dest: 输出目标文件
format: 构建的格式 umd为umd格式 cjs遵循commonJS规范 es遵循esmodule规范
env:开发模式/生产模式
banner指的是每个js的页头,比如作者,信息,开源协议等信息
还有其他的一些关于rollup(很像webpack的一些设计)别名诸如此类的配置,这边就不阐述了,因为我也忘记的差不多了,文档都很好查,想知道具体的意思也很容易

resolve函数的定义

这里的resolve函数比较简短,很容易理清

//假定一个config中使用resolve这个函数,它传递的字符串是这样的
'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js')
  }

const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

首先base则是 “web”这个字符串,
这个base并不是真实的路径,而这个web则指向了aliases的配置

module.exports = {
  web: resolve('src/platforms/web'),
}

这里的web指向的路径就是src/platforms/web
那么resolve函数返return的就是path.resolve,其中第一个参数就是web,第二个参数则是entry-runtime
所以由此得知,通过这样的一个过程找到了build的入口文件然后经过rollup的打包就会在dist目录下生成web/entry-runtime.js

拓展阅读

Runtime Only VS Runtime + Compiler 推荐阅读