通过脚手架创建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)
这里会读取所有的devDependencies
和dependencies
,取出其中的插件。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
。可以看到这里导出了vueConfig
、webpackConfig
、chainWebpack
。然后再通过api.configureWebpack
和api.chainWebpack
运用到webpack中。(chainWebpack与configureWebpack、vue-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'
}
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`来生成虚拟节点,我们可以把它类比成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 技术揭秘