Skip to content

Commit

Permalink
feat: hotupdate
Browse files Browse the repository at this point in the history
  • Loading branch information
mater1996 committed Nov 23, 2021
1 parent 6f9e96d commit 664db99
Show file tree
Hide file tree
Showing 19 changed files with 276 additions and 83 deletions.
10 changes: 9 additions & 1 deletion example/app.js
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
import './app.mpx' // 直接在html里写app.mpx会有问题,导致dev模式失败,看起来dev模式下入口必须是js
import Vue from 'vue'
import App from './app.mpx'

new Vue({
el: '#app',
render: function (h) {
return h(App)
}
})
8 changes: 4 additions & 4 deletions example/pages/index/index.mpx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<view>
<view style="text-align: center">{{index}}</view>
<button bindtap="handleTap">add</button>
<view style="text-align: center">{{index}}123</view>
<button bindtap="handleTap">add123</button>
</view>
</template>

Expand Down Expand Up @@ -30,9 +30,9 @@ createComponent({

<style scoped>
div{
color: blue;
color: red;
}
a{
color: red
color: red;
}
</style>
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/pluginutils": "^4.1.1",
"@types/debug": "^4.1.7",
"debug": "^4.3.2",
"hash-sum": "^2.0.0",
"json5": "^2.2.0",
"rollup-plugin-polyfill-node": "^0.7.0",
Expand Down
2 changes: 2 additions & 0 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import mpxCompiler, {
} from '@mpxjs/webpack-plugin/lib/template-compiler/compiler'
import parseComponent from '@mpxjs/webpack-plugin/lib/parser'
import { JsonConfig } from './utils/resolveJson'
import { ProcessTemplateResult } from './transformer/web/processTemplate'

export * from '@mpxjs/webpack-plugin/lib/template-compiler/compiler'

Expand All @@ -15,6 +16,7 @@ export interface SFCDescriptor extends CompilerResult {
component: boolean
app: boolean
jsonConfig: JsonConfig
builtInComponentsMap: ProcessTemplateResult['builtInComponentsMap']
vue?: string
}

Expand Down
16 changes: 8 additions & 8 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,39 @@ declare module '@mpxjs/webpack-plugin/lib/template-compiler/compiler' {

type Mode = 'wx' | 'web' | 'ali' | 'swan'

export interface Part {
export interface SFCBlock {
tag: 'template' | 'script' | 'style'
src?: string
mode?: Mode
content: string
result?: string
start?: number
attrs?: Record<string, unknown>
attrs: Record<string, unknown>
priority?: number
end?: number
}

export interface Template extends Part {
export interface Template extends SFCBlock {
tag: 'template'
src?: string
lang?: string
}

export interface Script extends Part {
export interface Script extends SFCBlock {
tag: 'script'
src?: string
map?: RawSourceMap
}

export interface JSON extends Part {
export interface JSON extends SFCBlock {
tag: 'script'
attrs: { type: 'application/json' | 'json' }
type: 'application/json' | 'json'
src: string
}

export interface Style extends Part {
export interface Style extends SFCBlock {
tag: 'style'
map?: RawSourceMap
scoped?: boolean
}

export interface CompilerResult {
Expand Down
156 changes: 154 additions & 2 deletions src/handleHotUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,157 @@
import { HmrContext, ModuleNode } from 'vite'
import _debug from 'debug'
import { SFCBlock } from './compiler'
import { ResolvedOptions } from './index'
import processTemplate, {
ProcessTemplateResult
} from './transformer/web/processTemplate'
import {
getDescriptor,
setPrevDescriptor,
createDescriptor
} from './utils/descriptorCache'

export default function ({ modules }: HmrContext): ModuleNode[] {
return [modules[0]]
const debug = _debug('vite:hmr')

/**
* forked from vite-plugin-vue2
* change
* 1. update script when template buildInComponent change
* 2. remove custom block
* 3. remove cssVar
* 4. remove scriptSetup
*/
export default async function handleHotUpdate(
{ modules, file, read }: HmrContext,
options: ResolvedOptions
): Promise<ModuleNode[] | undefined> {
const prevDescriptor = getDescriptor(file)
if (!prevDescriptor) {
return // file hasn't been requested yet (e.g. async component)
}
setPrevDescriptor(file, prevDescriptor)
const content = await read()
const descriptor = createDescriptor(
file,
content,
{
app: prevDescriptor.app,
page: prevDescriptor.page,
component: prevDescriptor.component
},
options
)
descriptor.jsonConfig = prevDescriptor.jsonConfig

// TODO: optimize get builtInComponentsMap way
const templateResult = await processTemplate(descriptor, options)
descriptor.builtInComponentsMap = templateResult.builtInComponentsMap

const updateType = []
const affectedModules = new Set<ModuleNode | undefined>()
const mainModule = modules.find(
(m) => !/type=/.test(m.url) || /type=script/.test(m.url)
)

if (
!isEqualBlock(descriptor.script, prevDescriptor.script) ||
!isEqualBuiltInComponent(
descriptor.builtInComponentsMap,
prevDescriptor.builtInComponentsMap
)
) {
affectedModules.add(mainModule)
updateType.push('script')
}

let needRerender = false
const templateModule = modules.find((m) => /type=template/.test(m.url))

if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
affectedModules.add(templateModule)
needRerender = true
}

let didUpdateStyle = false
const prevStyles = prevDescriptor.styles || []
const nextStyles = descriptor.styles || []

// force reload if scoped status has changed
if (
prevStyles.some((s) => s.attrs.scoped) !==
nextStyles.some((s) => s.attrs.scoped)
) {
// template needs to be invalidated as well
affectedModules.add(templateModule)
affectedModules.add(mainModule)
}

// only need to update styles if not reloading, since reload forces
// style updates as well.
for (let i = 0; i < nextStyles.length; i++) {
const prev = prevStyles[i]
const next = nextStyles[i]
if (!prev || !isEqualBlock(prev, next)) {
didUpdateStyle = true
const mod = modules.find(
(m) =>
m.url.includes(`type=style&index=${i}`) &&
m.url.endsWith(`.${next.attrs.lang || 'css'}`)
)
if (mod) {
affectedModules.add(mod)
if (mod.url.includes('&inline')) {
affectedModules.add(mainModule)
}
} else {
// new style block - force reload
affectedModules.add(mainModule)
}
}
}

if (prevStyles.length > nextStyles.length) {
// style block removed - force reload
affectedModules.add(mainModule)
}

if (needRerender) {
updateType.push(`template`)
}

if (didUpdateStyle) {
updateType.push(`style`)
}

if (updateType.length) {
debug(`[mpx:update(${updateType.join('&')})] ${file}`)
}

return [...affectedModules].filter(Boolean) as ModuleNode[]
}

export function isEqualBuiltInComponent(
a: ProcessTemplateResult['builtInComponentsMap'],
b: ProcessTemplateResult['builtInComponentsMap']
): boolean {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) {
return false
}
return keysA.every((key) => b[key])
}

export function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null): boolean {
if (!a && !b) return true
if (!a || !b) return false
// src imports will trigger their own updates
if (a.src && b.src && a.src === b.src) return true
if (a.content !== b.content) return false
const keysA = Object.keys(a.attrs)
const keysB = Object.keys(b.attrs)
if (keysA.length !== keysB.length) {
return false
}
return keysA.every((key) => a.attrs[key] === b.attrs[key])
}
13 changes: 0 additions & 13 deletions src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import { ResolvedOptions } from './index'
import { SFCDescriptor } from './compiler'
import stringify from './utils/stringify'
import addQuery from './utils/addQuery'

export const ENTRY_HELPER_CODE = 'plugin-mpx:entry-helper'
export const APP_HELPER_CODE = 'plugin-mpx:app-helper'

export const renderEntryCode = (importer: string): string => `
import App from ${stringify(addQuery(importer, { app: true, mpx: true }))}
import Vue from 'vue'
new Vue({
el: '#app',
render: function(h){
return h(App)
}
})
`

export function renderAppHelpCode(
descriptor: SFCDescriptor,
option: ResolvedOptions
Expand Down
52 changes: 25 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from 'path'
import { Plugin, ViteDevServer } from 'vite'
import { createFilter } from '@rollup/pluginutils'
import { createVuePlugin as vue } from 'vite-plugin-vue2'
Expand All @@ -7,16 +6,12 @@ import nodePolyfills from 'rollup-plugin-polyfill-node'
import commonjs from '@rollup/plugin-commonjs'
import mpxGlobal from './mpx'
import transformMpx from './transformer/mpx'
import transfromTemplate from './transformer/template'
import addMode, { esbuildAddModePlugin } from './plugins/addModePlugin'
import {
renderAppHelpCode,
renderEntryCode,
APP_HELPER_CODE,
ENTRY_HELPER_CODE
} from './helper'
import { renderAppHelpCode, APP_HELPER_CODE } from './helper'
import parseRequest from './utils/parseRequest'
import processOptions from './utils/processOptions'
import { getDescriptor } from './utils/descriptorCache'
import { getDescriptor, getPrevDescriptor } from './utils/descriptorCache'
import stringifyObject from './utils/stringifyObject'
import handleHotUpdate from './handleHotUpdate'

Expand Down Expand Up @@ -67,7 +62,8 @@ export interface Options {
export interface ResolvedOptions extends Required<Options> {
sourceMap?: boolean
devServer?: ViteDevServer
isProduction?: boolean
isProduction: boolean
root: string
}

const MpxPluginName = 'vite:mpx'
Expand All @@ -78,7 +74,6 @@ function mpx(options: ResolvedOptions): Plugin {

return {
name: MpxPluginName,
enforce: 'pre',

config() {
return {
Expand All @@ -105,36 +100,39 @@ function mpx(options: ResolvedOptions): Plugin {
},

handleHotUpdate(ctx) {
return handleHotUpdate(ctx)
return handleHotUpdate(ctx, options)
},

async resolveId(id, importer) {
const { filename, query } = parseRequest(id)
if (filter(filename) && !query.mpx && !query.vue) {
// app.mpx => ENTRY_HELPER_CODE => app.mpx?mpx=true&app=true
mpxGlobal.entry = path.resolve(path.dirname(importer || ''), id)
return ENTRY_HELPER_CODE
}
if (id === APP_HELPER_CODE) {
if (id === APP_HELPER_CODE && filter(importer)) {
mpxGlobal.entry = importer
return id
}
},

load(id) {
if (id === ENTRY_HELPER_CODE) {
return renderEntryCode(mpxGlobal.entry)
}
if (id === APP_HELPER_CODE) {
const descriptor = getDescriptor(mpxGlobal.entry)
if (id === APP_HELPER_CODE && mpxGlobal.entry) {
const { filename } = parseRequest(mpxGlobal.entry)
const descriptor = getDescriptor(filename)
return descriptor && renderAppHelpCode(descriptor, options)
}
},

async transform(code, id) {
const { filename, query } = parseRequest(id)
if (filter(filename) && query.mpx && !query.vue) {
// skip vue file transform
return await transformMpx(filename, code, query, options, this)
if (!filter(filename)) return
if (!query.vue) {
// mpx file => vue file
return await transformMpx(code, filename, query, options, this)
} else if (getPrevDescriptor(filename)) {
// hot reload
if (query.type === 'template') {
// mpx template => vue template
const descriptor = getDescriptor(filename)
if (descriptor) {
return await transfromTemplate(descriptor, options, this)
}
}
}
}
}
Expand Down Expand Up @@ -162,7 +160,7 @@ export default function (options: Options = {}): Plugin[] {
})
}),
nodePolyfills({
include: [/@mpxjs/, /\.mpx/, /plugin-mpx:/, /polyfill-node/], // mpx global polyfill
include: [/@mpxjs/, /\.mpx/, /plugin-mpx:/, /polyfill-node/],
exclude: [/polyfill-nodeglobal/] // ignore polyfill self
})
]
Expand Down
Loading

0 comments on commit 664db99

Please sign in to comment.