美文网首页前端
uni-app源码分析

uni-app源码分析

作者: HoooChan | 来源:发表于2020-07-06 19:06 被阅读0次

    通过脚手架创建uni-app并编译

    vue create -p dcloudio/uni-preset-vue test-uni-app
    

    使用脚手架创建的项目可以更清晰的看到它的架构,也可以直接阅读打包编译的源码。

    我们可以看看uni-app的模板代码,App.vue并没有<template>代码,那它是怎么把元素挂载上去的呢?其实可以在它编译的过程中找到答案。我们后面遇到的问题,也都是在这个过程找到解决方案的。

    创建出来的项目,package.json自带了一些编译脚本,来看其中一条:

    cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve
    

    cross-env 是一个用来设置环境变量的库,一般来说可以这样设置环境变量:

    NODE_ENV_TEST=development node env-test.js
    

    但windows不支持NODE_ENV=development的设置方式,使用cross-env就不必考虑平台差异。

    再继续看这行命令,一般我们用vue的脚手架来编译,直接执行

    vue-cli-service serve
    

    这里的vue-cli-service就是一个自定义的node命令(自定义node命令)。

    serve是vue-cli-service自带的插件,而uni-serve是uni-app自定义的一个插件。

    这个插件是怎么被vue-cli-service识别出来并调用的呢?

    App.vue

    先看看vue-cli-service命令的执行

    可以在node_modules的.bin文件夹下找到vue-cli-service命令,也是执行这段命令会运行的源码

    const Service = require('../lib/Service')
    const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
    

    可以看到每次运行vue-cli-service都会创建一个service, 我们来看看service的构造函数,源码在node_modules/@vue/cli-service/lib/Service.js

    可以看到一个很关键的代码:

        this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    

    这里就是解析我们在项目里引用的插件的。

    resolvePlugins的源码:

          const projectPlugins = Object.keys(this.pkg.devDependencies || {})
            .concat(Object.keys(this.pkg.dependencies || {}))
            .filter(isPlugin)
            .map(id => {
              if (
                this.pkg.optionalDependencies &&
                id in this.pkg.optionalDependencies
              ) {
                let apply = () => {}
                try {
                  apply = require(id)
                } catch (e) {
                  warn(`Optional dependency ${id} is not installed.`)
                }
    
                return { id, apply }
              } else {
                return idToPlugin(id)
              }
            })
          plugins = builtInPlugins.concat(projectPlugins)
    

    这里会读取所有的devDependenciesdependencies,取出其中的插件。isPlugin用来筛选插件。isPlugin的源码在node_modules/@vue/cli-shared-utils/lib/pluginResolution.js

    const pluginRE = /^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/
    
    exports.isPlugin = id => pluginRE.test(id)
    

    可以看到只要符合特定格式的就会被识别为插件。

    再看idToPlugin的源码

        const idToPlugin = id => ({
          id: id.replace(/^.\//, 'built-in:'),
          apply: require(id)
        })
    

    这里主要是把插件封装起来待后面调用。

    再回到vue-cli-service命令的源码,可以看到最底下调用了service的run方法

    service.run(command, args, rawArgv).catch(err => {
      error(err)
      process.exit(1)
    })
    

    在看看run的源码:

      async run (name, args = {}, rawArgv = []) {
        // resolve mode
        // prioritize inline --mode
        // fallback to resolved default modes from plugins or development if --watch is defined
        const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    
        // --skip-plugins arg may have plugins that should be skipped during init()
        this.setPluginsToSkip(args)
    
        // load env variables, load user config, apply plugins
        this.init(mode)
        ...
    

    this.init(mode)是关键,从注释里面也可以看到,这里会运行插件。

    init的源码:

        // apply plugins.
        this.plugins.forEach(({ id, apply }) => {
          if (this.pluginsToSkip.has(id)) return
          apply(new PluginAPI(id, this), this.projectOptions)
        })
    

    这块代码会是插件正在执行的地方。这里利用解构直接取出插件的id和apply。

    可以看看我们在项目里引用的uni-app插件,src/pages.json

        "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-26920200409002",
        "@dcloudio/vue-cli-plugin-uni": "^2.0.0-26920200409002",
        "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-26920200409002",
    

    vue-cli-plugin-uni-optimize

    这个插件定义了一些别名,在分析源码的时候我们需要用到:

      api.configureWebpack(webpackConfig => {
        return {
          watch: true,
          resolve: {
            alias: {
              ['uni-' + process.env.UNI_PLATFORM]: path.join(lib, `${process.env.UNI_PLATFORM}/main.js`),
              'uni-core': path.join(src, 'core'),
              'uni-view': path.join(src, 'core/view'),
              'uni-service': path.join(src, 'core/service'),
              'uni-shared': path.join(src, 'shared'),
              'uni-mixins': path.join(src, 'core/view/mixins'),
              'uni-helpers': path.join(src, 'core/helpers'),
              'uni-platform': path.join(src, 'platforms/' + process.env.UNI_PLATFORM),
    
              // tree shaking
              'uni-components': uniComponentsPath,
              'uni-invoke-api': uniInvokeApiPath,
              'uni-service-api': uniServiceApiPath,
              'uni-api-protocol': uniApiProtocolPath,
              'uni-api-subscribe': uniApiSubscribePath,
              // h5 components
              'uni-h5-app-components': uniH5AppComponentsPath,
              'uni-h5-app-mixins': uniH5AppMixinsPath,
              'uni-h5-system-routes': uniH5SystemRoutes
            }
          },
    

    vue-cli-plugin-uni

    来看看vue-cli-plugin-uni的源码,在/node_modules/@dcloudio/vue-cli-plugin-uni/index.js

    前面插件的运行代码是:apply(new PluginAPI(id, this), this.projectOptions), apply就是apply: require(id)

    再看看vue-cli-plugin-uni的源码

    module.exports = (api, options) => {
      initServeCommand(api, options)
    
      initBuildCommand(api, options)
    ...
    

    这样就和apply的调用对应起来了。api就是new PluginAPI(id, this)

    先看下面的代码:

    const type = ['app-plus', 'h5'].includes(process.env.UNI_PLATFORM)
        ? process.env.UNI_PLATFORM
        : 'mp'
    
      const platformOptions = require('./lib/' + type)
    
      let vueConfig = platformOptions.vueConfig
    
      if (typeof vueConfig === 'function') {
        vueConfig = vueConfig(options, api)
      }
    
      Object.assign(options, { // TODO 考虑非 HBuilderX 运行时,可以支持自定义输出目录
        outputDir: process.env.UNI_OUTPUT_TMP_DIR || process.env.UNI_OUTPUT_DIR,
        assetsDir
      }, vueConfig) // 注意,此处目前是覆盖关系,后续考虑改为webpack merge逻辑
    
      require('./lib/options')(options)
    
      api.configureWebpack(require('./lib/configure-webpack')(platformOptions, manifestPlatformOptions, options, api))
      api.chainWebpack(require('./lib/chain-webpack')(platformOptions, options, api))
    

    这里先获取当前的编译类型,我们的是h5,取到的平台配置就是在./lib/h5中。源码在node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js。可以看到这里导出了vueConfigwebpackConfigchainWebpack。然后再通过api.configureWebpackapi.chainWebpack运用到webpack中。(chainWebpack与configureWebpackvue-cli中chainWebpack的使用)

    chainWebpack与configureWebpack用来修改webpack的配置 chainWebpack的粒度更细

    h5配置的webpackConfig实际上定义了一个规则,在加载App.vue文件时插入了<template>代码块。

    {
              test: /App\.vue$/,
              use: {
                loader: path.resolve(__dirname, '../../packages/wrap-loader'),
                options: {
                  before: ['<template><App :keepAliveInclude="keepAliveInclude"/></template>']
                }
              }
            }
    

    wrap-loader的用途:

    Add custom content before and after the loaded source.

    到这里我们就知道App.vue实际是挂载了一个App的自定义组件。那这个组件是什么时候注册到Vue当中的呢? 同样是这块代码:

    const statCode = process.env.UNI_USING_STAT ? 'import \'@dcloudio/uni-stat\';' : ''
    ...
    const beforeCode = (useBuiltIns === 'entry' ? 'import \'@babel/polyfill\';' : '') +
          `import 'uni-pages';import 'uni-${process.env.UNI_PLATFORM}';`
    ...
    {
              test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
              use: [{
                loader: path.resolve(__dirname, '../../packages/wrap-loader'),
                options: {
                  before: [
                    beforeCode + statCode + getGlobalUsingComponentsCode()
                  ]
                }
              }]
            }
    

    getMainEntry:

    function getMainEntry () {
      if (!mainEntry) {
        mainEntry = fs.existsSync(path.resolve(process.env.UNI_INPUT_DIR, 'main.ts')) ? 'main.ts' : 'main.js'
      }
      return mainEntry
    }
    

    这里主要就是给main.js插入引用。我们这里是h5,所以这里引用了uni-h5。(node_modules文件夹查找规则)

    引用uni-h5的源码路径在:node_modules/@dcloudio/uni-h5/dist/index.umd.min.js。这里面是经过编译压缩的。lib里面有源代码。
    node_modules/@dcloudio/uni-h5/src/platforms/h5/components/index.js中就是注册App组件的源码:

    Vue.component(App.name, App)
    Vue.component(Page.name, Page)
    Vue.component(AsyncError.name, AsyncError)
    Vue.component(AsyncLoading.name, AsyncLoading)
    

    到这里App组件的来源就清楚了。

    App.vue的源码:node_modules/@dcloudio/uni-h5/src/platforms/h5/components/app/index.vue

    Pages.json

    webpack-uni-pages-loader

    上面的流程中我们可以发现在调用api.configureWebpack之前还调用了另一个方法,把平台特有的webpack配置和公共的配置合并起来再返回,源码在node_modules/@dcloudio/vue-cli-plugin-uni/lib/configure-webpack.js

    这里面的公共配置有个很重要的东西,涉及到怎么解析page.json的:

    {
          test: path.resolve(process.env.UNI_INPUT_DIR, 'pages.json'),
          use: [{
            loader: 'babel-loader'
          }, {
            loader: '@dcloudio/webpack-uni-pages-loader'
          }],
          type: 'javascript/auto'
        }
    

    Rule.type

    webpack-uni-pages-loader的源码node_modules/@dcloudio/webpack-uni-pages-loader/lib/index.js

      if (
        process.env.UNI_USING_COMPONENTS ||
        process.env.UNI_PLATFORM === 'h5' ||
        process.env.UNI_PLATFORM === 'quickapp'
      ) {
        return require('./index-new').call(this, content)
      }
    

    node_modules/@dcloudio/webpack-uni-pages-loader/lib/index-new.js

      if (process.env.UNI_PLATFORM === 'h5') {
        return require('./platforms/h5')(pagesJson, manifestJson)
      }
    

    根据平台类型判断,最终来到node_modules/@dcloudio/webpack-uni-pages-loader/lib/platforms/h5.js,经过这层处理,pages.json变成了以下代码:

    import Vue from 'vue'
    global['________'] = true;
    delete global['________'];
    global.__uniConfig = {"globalStyle":{"navigationBarTextStyle":"black","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#F8F8F8","backgroundColor":"#F8F8F8"}};
    global.__uniConfig.router = {"mode":"hash","base":"/"};
    global.__uniConfig['async'] = {"loading":"AsyncLoading","error":"AsyncError","delay":200,"timeout":60000};
    global.__uniConfig.debug = false;
    global.__uniConfig.networkTimeout = {"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000};
    global.__uniConfig.sdkConfigs = {};
    global.__uniConfig.qqMapKey = "XVXBZ-NDMC4-JOGUS-XGIEE-QVHDZ-AMFV2";
    global.__uniConfig.nvue = {"flex-direction":"column"}
    
    // 注册Page
    Vue.component('pages-index-index', resolve=>{
    const component = {
      component:require.ensure([], () => resolve(require('/Users/chenzhendong/Documents/WorkSpace/H5/test-uni-app/src/pages/index/index.vue')), 'pages-index-index'),
      delay:__uniConfig['async'].delay,
      timeout: __uniConfig['async'].timeout
    }
    if(__uniConfig['async']['loading']){
      component.loading={
        name:'SystemAsyncLoading',
        render(createElement){
          return createElement(__uniConfig['async']['loading'])
        }
      }
    }
    if(__uniConfig['async']['error']){
      component.error={
        name:'SystemAsyncError',
        render(createElement){
          return createElement(__uniConfig['async']['error'])
        }
      }
    }
    return component
    })
    
    // 定义路由
    global.__uniRoutes=[
    {
    path: '/',
    alias:'/pages/index/index',
    component: {
      render (createElement) {
        return createElement(
          // 创建Page组件
          'Page',
          {
            props: Object.assign({
              isQuit:true,
    
              isEntry:true,
    
              
              
            },__uniConfig.globalStyle,{"navigationBarTitleText":"uni-app"})
          },
          [
            // 创建我们的页面,作为子组件插入到Page的slot中
            createElement('pages-index-index', {
              slot: 'page'
            })
          ]
        )
      }
    },
    meta:{
    id:1,
      name:'pages-index-index',
      isNVue:false,
      pagePath:'pages/index/index',
    isQuit:true,
    isEntry:true,
      windowTop:44
    }
    },
    {
    path: '/preview-image',
    component: {
      render (createElement) {
        return createElement(
          'Page',
          {
            props:{
              navigationStyle:'custom'
            }
          },
          [
            createElement('system-preview-image', {
              slot: 'page'
            })
          ]
        )
      }
    },
    meta:{
      name:'preview-image',
      pagePath:'/preview-image'
    }
    }
        ,
    {
    path: '/choose-location',
    component: {
      render (createElement) {
        return createElement(
          'Page',
          {
            props:{
              navigationStyle:'custom'
            }
          },
          [
            createElement('system-choose-location', {
              slot: 'page'
            })
          ]
        )
      }
    },
    meta:{
      name:'choose-location',
      pagePath:'/choose-location'
    }
    }
        ,
    {
    path: '/open-location',
    component: {
      render (createElement) {
        return createElement(
          'Page',
          {
            props:{
              navigationStyle:'custom'
            }
          },
          [
            createElement('system-open-location', {
              slot: 'page'
            })
          ]
        )
      }
    },
    meta:{
      name:'open-location',
      pagePath:'/open-location'
    }
    }
        ]
    

    这里做的工作:
    1、注册我们的页面;
    2、定义路由,实际上是创建Page组件然后再把我们的控件插到Page的slot中;

    修改路径

    生成路由的时候,path是pageComponents的route,通过getPageComponents方法生成。这个值的路径如下:
    page.json => pageJson => pageJson.pages => page.path => route

    所有我们只需要修改getPageComponents的返回值即可

        return {
          name,
          route: page.routePath || page.path,
          path: pagePath,
          props,
          isNVue,
          isEntry,
          isTabBar,
          tabBarIndex,
          isQuit: isEntry || isTabBar,
          windowTop
        }
    

    直接修改源码在重新npm install后会丢失修改,所以我们用到了一个工具:patch-package,它可以把我们的修改记录下来,在下一次npm install时再还原我们的修改。

    page.json的引入node_modules/@dcloudio/vue-cli-plugin-uni/lib/configure-webpack.js:

        return merge({
          resolve: {
            alias: {
              '@': path.resolve(process.env.UNI_INPUT_DIR),
              './@': path.resolve(process.env.UNI_INPUT_DIR), // css中的'@/static/logo.png'会被转换成'./@/static/logo.png'加载
              'vue$': getPlatformVue(vueOptions),
              'uni-pages': path.resolve(process.env.UNI_INPUT_DIR, 'pages.json'),
              '@dcloudio/uni-stat': require.resolve('@dcloudio/uni-stat'),
              'uni-stat-config': path.resolve(process.env.UNI_INPUT_DIR, 'pages.json') +
                '?' +
                JSON.stringify({
                  type: 'stat'
                })
            }
    

    node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js

        const beforeCode = (useBuiltIns === 'entry' ? `import '@babel/polyfill';` : '') +
          `import 'uni-pages';import 'uni-${process.env.UNI_PLATFORM}';`
    

    路由

    路由的基本使用:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import App from './App'
    
    Vue.use(VueRouter)
    
    // 1. 定义(路由)组件。
    // 可以从其他文件 import 进来
    const Foo = { template: '<div>foo</div>' }
    
    // 2. 定义路由
    // 每个路由应该映射一个组件。 其中"component" 可以是
    // 通过 Vue.extend() 创建的组件构造器,
    // 或者,只是一个组件配置对象。
    const routes = [
      { path: '/foo', component: Foo }
    ]
    
    // 3. 创建 router 实例,然后传 `routes` 配置
    const router = new VueRouter({
      routes // (缩写)相当于 routes: routes
    })
    
    // 4. 创建和挂载根实例。
    // 记得要通过 router 配置参数注入路由,
    // 从而让整个应用都有路由功能
    const app = new Vue({
      el: '#app',
      render(h) {
        return h(App)
      },
      router
    })
    

    Vue的构造函数最终来到如下代码src/core/instance/index.js

    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    
    initMixin(Vue)
    ...
    

    _init函数是在initMixin(Vue)里面定义的。传进来的options最终会保存在Vue实例的$options中。因此在根组件中可以通过this.$options.router拿到路由实例。那子组件是怎么拿到这个router的呢?

    Vue.use(VueRouter)会来到VueRouter定义的install函数src/install.js

      Vue.mixin({
        beforeCreate () {
          if (isDef(this.$options.router)) {
            this._routerRoot = this
            this._router = this.$options.router
            this._router.init(this)
            Vue.util.defineReactive(this, '_route', this._router.history.current)
          } else {
            this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
          }
          registerInstance(this, this)
        },
        destroyed () {
          registerInstance(this)
        }
      })
    

    这里利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中。根组件上的this.$options.router就是我们创建Vue实例的时候传进来的router,然后设置_routerRoot,初始化router。此外还定义了一个被监听的_route变量。

    初始化函数src/index.js

      init (app: any /* Vue component instance */) {
        process.env.NODE_ENV !== 'production' && assert(
          install.installed,
          `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
        )
    
        this.apps.push(app)
    
        // set up app destroyed handler
        // https://github.com/vuejs/vue-router/issues/2639
        app.$once('hook:destroyed', () => {
          // clean out app from this.apps array once destroyed
          const index = this.apps.indexOf(app)
          if (index > -1) this.apps.splice(index, 1)
          // ensure we still have a main app or null if no apps
          // we do not release the router so it can be reused
          if (this.app === app) this.app = this.apps[0] || null
        })
    
        // main app previously initialized
        // return as we don't need to set up new history listener
        if (this.app) {
          return
        }
    
        this.app = app
    
        const history = this.history
    
        if (history instanceof HTML5History) {
          history.transitionTo(history.getCurrentLocation())
        } else if (history instanceof HashHistory) {
          const setupHashListener = () => {
            history.setupListeners()
          }
          history.transitionTo(
            history.getCurrentLocation(),
            setupHashListener,
            setupHashListener
          )
        }
    
        history.listen(route => {
          this.apps.forEach((app) => {
            app._route = route
          })
        })
      }
    

    history.listen其实只是把这个函数保存起来,当history监听到路径变化时就会调用这个函数,把最新的路径返回。这个app会更新自己的_route,就会引起router-view更新。

    再看router的install函数:

      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
      })
    

    给Vue的原型定义了$router$route,它们都是从_routerRoot拿到的,从前面可以知道在钩子函数beforeCreate里面已经给每个组件都设置了_routerRoot。组件调用route时其实是拿根组件的route。而router-view的render函数中调用了`route`来生成虚拟节点,我们可以把它类比成computed函数,当app._route变了就自然引起router-view重新计算。

    uni-app是怎么注入router的呢?我们并没有在uni-app调用Vue.use(VueRouter),也没有创建router在创建Vue的时候传进去,这些uni-app帮我们实现了。

    node_modules/@dcloudio/uni-h5/lib/h5/main.js

    Vue.use(require('uni-service/plugins').default, {
      routes: __uniRoutes
    })
    
    Vue.use(require('uni-view/plugins').default, {
      routes: __uniRoutes
    })
    

    node_modules/@dcloudio/uni-h5/src/core/service/plugins/index.js

        Vue.mixin({
          beforeCreate () {
            const options = this.$options
            if (options.mpType === 'app') {
              options.data = function () {
                return {
                  keepAliveInclude
                }
              }
              const appMixin = createAppMixin(routes, entryRoute)
              // mixin app hooks
              Object.keys(appMixin).forEach(hook => {
                options[hook] = options[hook] ? [].concat(appMixin[hook], options[hook]) : [
                  appMixin[hook]
                ]
              })
    
              // router
              options.router = router
    
              // onError
              if (!Array.isArray(options.onError) || options.onError.length === 0) {
                options.onError = [function (err) {
                  console.error(err)
                }]
              }
            }
            ...
    

    uni-serve

    再回到自定义命令的流程,可以看到 vue-cli-plugin-uni首先初始化了两个命令 build 和 serve。插件开发指南

    先看看initServeCommand 的源码:

    api.registerCommand('uni-serve', {
    ...
    

    从前面的调用可以知道api就是 PluginAPI PluginAPI的registerCommand:

      /**
       * Register a command that will become available as `vue-cli-service [name]`.
       *
       * @param {string} name
       * @param {object} [opts]
       *   {
       *     description: string,
       *     usage: string,
       *     options: { [string]: string }
       *   }
       * @param {function} fn
       *   (args: { [string]: string }, rawArgs: string[]) => ?Promise
       */
      registerCommand (name, opts, fn) {
        if (typeof opts === 'function') {
          fn = opts
          opts = null
        }
        this.service.commands[name] = { fn, opts: opts || {}}
      }
    

    到这里把命令注册完成了,保存在service的commands里面。

    再回到service的run方法

        args._ = args._ || []
        let command = this.commands[name]
        if (!command && name) {
          error(`command "${name}" does not exist.`)
          process.exit(1)
        }
        if (!command || args.help || args.h) {
          command = this.commands.help
        } else {
          args._.shift() // remove command itself
          rawArgv.shift()
        }
        const { fn } = command
        return fn(args, rawArgv)
    

    这里取出命令的执行函数来执行
    也就是/node_modules/@dcloudio/vue-cli-plugin-uni/commands/serve.js中注册uni-serve命令时传进去的函数。

    至此打包流程结束。

    css

    node_modules/@dcloudio/vue-cli-plugin-uni/index.js

      require('./lib/options')(options)
    

    uni.scss

    node_modules/@dcloudio/vue-cli-plugin-uni/lib/options.js

    [Vue CLI 3] 插件开发之 registerCommand 到底做了什么
    vue-cli 3学习之vue-cli-service插件开发(注册自定义命令)
    vue-router工作原理概述和问题分析
    Vue.js 技术揭秘

    相关文章

      网友评论

        本文标题:uni-app源码分析

        本文链接:https://www.haomeiwen.com/subject/dppgqktx.html