随着vue3.0的公布,为vue.js带来了如下变动:

  1. 组合式api
  2. 响应式优化
  3. 编译优化
  4. 源码体积优化

变动

组合式api

在vue3.0之前的版本中应用申明式api,尽管语法上清晰简略,然而带来了问题,即:

  1. 实现某个性能的代码散布在申明的各个变量中,导致实现多个性能的代码糅合在一起,不利于代码复用,尤其是开发大型项目。
  2. 尽管提供了mixin用于代码复用,然而也会带来问题,如命名抵触等。

组合式api就能够解决这些问题,能够通过一系列api将实现某个性能的代码放到一起,从而能够实现代码复用。

如下例子中,页面会捕获用户鼠标地位,能够将捕获鼠标地位的代码提取到一个公共办法中,当须要应用的时候,间接引入代码即可:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    <div id="app">        <div>            X: {{position.x}} <br />            Y: {{position.y}}        </div>    </div>    <script type="module">        import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'                // 共用的监控鼠标地位办法        function useMousePosition() {            const position = reactive({                x: 0,                y: 0            })            const handler = (e) => {                position.x = e.pageX                position.y = e.pageY            }            onMounted(() => {                window.addEventListener('mousemove', handler)            })            onUnmounted(() => {                window.removeEventListener('mousemove', handler)            })            return {                position            }        }        createApp({            setup() {                // 在须要应用的vue组件中引入即可                return {                    ...useMousePosition()                }            }        }).mount('#app')    </script></body></html>

响应式优化

家喻户晓,vue2.0中应用Object.defineProperty增加数据监听,然而其存在如下问题:

  1. 初始化的时候会深度遍历对象属性,为每一个属性增加监听。
  2. 无奈监听动静增加,删除属性。
  3. 无奈监听数组的通过.length和[0]的扭转。

而3.0中,应用Proxy对象重写响应式零碎,解决了上述问题,从而晋升了响应式零碎性能。

编译优化

在vue我的项目编译的时候,会将template编译成render函数,在编译原理的文章中说过,vue2.0会标记动态根节点,在虚构dom的diff过程中会跳过动态根节点的比照,从而优化编译速度。

而在vue3.0中又增加了如下内容用于优化编译过程:

  1. 动态晋升。
  2. Patch Flag。
  3. 缓存事件处理函数。
  4. Fragments(片段)。

在上面具体阐明过程中会应用https://vue-next-template-explorer.netlify.app这个站点用于查看template编译后的render函数。

应用的template案例如下:

<div id="app">    <div>        static root        <div>static node</div>    </div>    <div>static node</div>    <div :id="id">{{count}}</div>    <button @click="handler">button</button></div>

该例子中蕴含动态节点,动态根节点,属性绑定,插值表达式和绑定事件。

动态晋升

当没有启用动态晋升的时候,template被编译成的render函数如下:

其中,红色框标出的是动态节点内容。

当启用动态晋升后,编译后果如下:

能够看出,原来render函数中的动态节点都被提取到render函数的里面,这样就导致,动态节点只有在初始化的时候被编译一次,后续发生变化后,动态节点会复用初始化时编译的后果,从而晋升编译性能。

patch flag

在编译的后果中能够看出,在编译生成的动静节点中,_createVNode办法最初会蕴含一个数字,如9,此数字就是patch flag,9代表的是该节点的text和props中均存在动静绑定的内容:

而如果咱们去掉模版中的:id="id"属性绑定,其编译后果如下:

此时flag变为1,示意该节点只有text被动静绑定,那么在虚构Dom的patch过程中,就能够跳过节点比照,间接比对其蕴含的text内容,从而晋升编译速度。

缓存事件处理函数

在模版中绑定了click事件,其编译后果如下:

此时,默认认为该节点属性中绑定了某个动态数据,所以当事件处理函数发生变化的时候,会登程页面的更新。

如果开启事件缓存,其编译后果如下:

在初始化编译的时候,其外部用一个函数包裹了绑定事件并增加到缓存中,当后续变动的时候,会间接从缓存中读取相应的处理函数,此时放慢了编译速度,同时,因为新生成了一个函数包裹事件处理函数,不论处理函数是否发生变化,包裹得函数是不会变动的,此时也就不会因为处理函数的变动导致页面更新。

fragments

在vue2.0中,template模版只能存在一个根节点,而3.0中通过fragments实现了能够存在多个同级根节点。

在例子中,只存在一个根节点div,其编译后的后果如下:

能够看出,生成的render办法中是应用_createBlock函数来解决根节点,从而将template转换为一个树形数据。

当咱们删除例子中的根节点之后,其编译的后果如下:

此时,_createBlock函数传入_Fragment内置组件作为根节点。

源码体积优化

vue3.0中通过如下两个方面优化了源码体积:

  1. 移除了一些不罕用的api: inline-template, filter等。
  2. 更好的tree-shaking,除了根底代码之外,如各种api都是按需加载的。

响应式原理

vue3.0将响应式局部独自提取成一个包reactivity,其蕴含了所有响应式相干的代码,能够独自应用:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    <script type="module">        import { reactive, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'        // 创立响应式数据        const product = reactive({            name: 'iPhone',            price: 5000,            count: 3        })        let total = 0        // 增加变动监听        effect(() => {            total = product.price * product.count        })        console.log(total)        product.price = 4000        console.log(total)        product.count = 1        console.log(total)    </script></body></html>

波及到两个要害函数:

  1. reactive: 将对象转换为响应式数据。
  2. effect: 注册监听变动函数,当product的price和count属性发生变化的时候会登程该函数执行。

后续会通过模仿reactivity包的实现来阐明vue3.0中响应式实现外围原理。

reactive

在reactive函数中,将为对象增加Proxy代理,并在get办法中增加监听,在set和delete办法中触发变动。

const isObject = val => val !== null && typeof val === 'object'const convert = target => isObject(target) ? reactive(target) : targetconst hasOwnProperty = Object.prototype.hasOwnPropertyconst hasOwn = (target, key) => hasOwnProperty.call(target, key)export function reactive(target) {    // 当target不是对象的时候,间接返回target    if (!isObject(target)) return target    const handler = {        get(target, key, receiver) {            // 收集依赖            const result = Reflect.get(target, key, receiver)            // 对象的某个属性有可能还是对象,此时须要为该属性增加响应式            return convert(result)        },        set(target, key, value, receiver) {            const oldValue = Reflect.get(target, key, receiver)            let result = true            if (oldValue !== value) {                result = Reflect.set(target, key, value, receiver)                // 触发更新            }            return result        },        deleteProperty(target, key) {            const hadKey = hasOwn(target, key)            const result = Reflect.deleteProperty(target, key)            if (hadKey && result) {                // 触发更新                trigger(target, key)            }            return result        }    }    return new Proxy(target, handler)}

effect

effect函数用于为数据变动增加响应操作。整个响应式收集过程可用下图示意:

整个依赖收集过程会波及三个数据:

  1. targetMap: WeakMap,示意一个响应式对象对应一个depsMap。
  2. depsMap: Map,示意响应式对象上的属性key对应一个dep。
  3. dep: Set, 示意一系列响应操作。

实现effect:

let activeEffect = nullexport function effect (callback) {  activeEffect = callback  callback() // 拜访响应式对象属性,去收集依赖  activeEffect = null}

其外部首先会将响应函数存储到全局对象activeEffect中,而后调用响应函数,因为响应函数中存在相似production.price调用,此时会触发get办法,在get办法中就能够将全局的activeEffect保留到相应的dep汇合中。

track

track办法用于收集依赖:

let targetMap = new WeakMap()export function track (target, key) {  if (!activeEffect) return  let depsMap = targetMap.get(target)  if (!depsMap) {    targetMap.set(target, (depsMap = new Map()))  }  let dep = depsMap.get(key)  if (!dep) {    depsMap.set(key, (dep = new Set()))  }  dep.add(activeEffect)}

此办法就是依据target获取depsMap,而后依据key获取相应dep,再而后将全局的activeEffect对象增加到dep汇合中。

定义实现track办法后,须要批改reactive办法收集依赖局部:

// 收集依赖track(target, key)

trigger

trigger函数用于触发更新:

export function trigger (target, key) {  const depsMap = targetMap.get(target)  if (!depsMap) return  const dep = depsMap.get(key)  if (dep) {    dep.forEach(effect => {      effect()    })  }}

此办法就是依据target和key找到相应的dep,而后循环调用dep数组中的响应办法。

定义实现trigger函数后,须要批改reactive函数中get,delete办法中触发更新局部:

// 触发更新trigger(target, key)

通过上述几个函数,就能够实现根本的vue3.0响应过程。

ref

ref函数和reactive函数相似,只不过其是将根本数据类型包装成响应式对象:

export function ref (raw) {  // 判断 raw 是否是ref 创立的对象,如果是的话间接返回  if (isObject(raw) && raw.__v_isRef) {    return  }  let value = convert(raw)  const r = {    __v_isRef: true,    get value () {      track(r, 'value')      return value    },    set value (newValue) {      if (newValue !== value) {        raw = newValue        value = convert(raw)        trigger(r, 'value')      }    }  }  return r}

在ref函数中,次要是依据传入的raw生成一个响应式对象,该对象蕴含一个value属性,在value属性的get和set办法中收集依赖和触发更新。

toRefs

在应用reactive函数将对象转换为响应式对象后,如果间接通过解构的形式获取其外部属性,那么获取到的数据并不是响应式的:

如:

// 创立响应式数据const product = reactive({     name: 'iPhone',     price: 5000,     count: 3})const { price } = product

上例中,通过解构获取到的price并不是响应式的。

此时,能够通过toRefs办法解决响应式对象,将其每个属性都变成可解构的响应式对象。

export function toRefs (proxy) {  const ret = proxy instanceof Array ? new Array(proxy.length) : {}  for (const key in proxy) {    ret[key] = toProxyRef(proxy, key)  }  return ret}function toProxyRef (proxy, key) {  const r = {    __v_isRef: true,    get value () {      return proxy[key]    },    set value (newValue) {      proxy[key] = newValue    }  }  return r}

此办法比较简单,就是遍历响应式数据的属性,而后依据属性创立一个新的对象,对象内所有属性都是响应式对象(和ref相似,蕴含value属性,在get和set办法中调用原来的proxy对象,原来的proxy对象中蕴含依赖收集和触发更新)。

computed

computed函数用于创立计算对象,该对象也是响应式的。

export function computed (getter) {  const result = ref()  effect(() => (result.value = getter()))  return result}

computed函数外部比较简单,就是调用ref办法创立一个新的响应式对象,而后调用effect办法增加响应监听,最初再返回ref对象。

vite原理

目前,vue我的项目应用webpack进行打包,然而webpack打包存在一个问题,就是开发阶段也是须要打包构建而后能力在浏览器上看到页面,这样会导致首次启动等待时间较长。

vite是随着vue3.0一起公布的一个vue构建工具,因为其采纳浏览器原生反对的ES Module个性,在开发阶段跳过编译,能间接在浏览器端运行,所以其存在以下特点:

  1. 疾速冷启动,不必构建后再启动。
  2. 天生反对更好的模块热更新体验。
  3. 按需编译,如vue单文件组件,只有在加载的时候才会在服务端进行构建。
  4. 开箱即用,其依赖的js包非常少。

Vite创立我的项目

  • 增加全局依赖
npm install -g create-vite-app
  • 创立我的项目
create-vite-app project-name
  • 启动我的项目
npm run dev

通过简略的几步,我的项目就被创立好并且能够应用了。

模仿Vite实现

模仿vite须要首先明确vite的实现原理,即在浏览器端通过module形式加载我的项目下的main.js文件,但遇到vue单文件、css、图片等ES Module不辨认的资源时,再在服务器端进行编译转换为浏览器能够辨认的js资源。

创立空的js包

  • 创立我的项目文件夹
  • 增加package.json
npm init --y
  • 增加bin配置,指向我的项目目录下的index.js文件

  • 在index.js文件中增加node程序启动头

增加动态服务器

在vite中是应用koa来创立服务器的,咱们在这个我的项目中也用它,装置依赖:

npm i -D koa koa-send

批改index.js

const Koa = require('koa')const send = require('koa-send')const app = new Koa()// 动态文件服务器app.use(async (ctx, next) => {    await send(ctx, ctx.path, {root: process.cwd(), index: 'index.html'})    // 继续执行后续的中间件    await next()})app.listen(3000)console.log('Server running @ http://localhost:3000')

批改实现后执行

npm link

关上vite创立的我的项目,在我的项目中执行:

my-vite

我的项目名称是my-vite,能够依据本人的我的项目名去执行命令。

此时命令行中会提醒,动态服务器启动胜利,关上浏览器,也能够看到可能失常获取资源:

只不过此时页面没有失常显示,这个是因为控制台报错了:

这个报错的起因是因为在我的项目的main.js中引入了其余包:

import { createApp } from 'vue'

这个援用在浏览器端无奈辨认,所以下一步须要解决第三方模块援用。

第三方模块援用

浏览器只能辨认'/'或者'./'援用的js文件,然而咱们的我的项目中存在如:

import { createApp } from 'vue'

这样的大量援用,这些援用的第三方模块ES Module是无奈辨认的,因而须要在服务端对这样的门路进行解决:

Vite的解决逻辑是将第三方的援用后面加上'/@modules',如:

import { createApp } from 'vue'

批改为

import { createApp } from '/@modules/vue'

批改咱们的代码:

#!/usr/bin/env nodeconst Koa = require('koa')const send = require('koa-send')const app = new Koa()// 用于将读取的文件转换为字符串const streamToString = stream => new Promise((resolve, reject) => {    const chunks = []    stream.on('data', chunk => chunks.push(chunk))    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))    stream.on('error', reject)})// 动态文件服务器app.use(async (ctx, next) => {    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })    // 继续执行后续的中间件    await next()})// 批改第三方文件门路app.use(async (ctx, next) => {    // 如果是js文件    if (ctx.type === 'application/javascript') {        const contents = await streamToString(ctx.body)        // 为第三方援用后面加上/@module        ctx.body = contents            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')    }    next()})app.listen(3000)console.log('Server running @ http://localhost:3000')

此时再次启动之后,会发现:

浏览器会去服务端申请:http://localhost:3000/@modules/vue

这个地址在服务端是不存在的,因而须要手动解决这个申请,解决的逻辑就是当门路中蕴含@/modules时,服务端截取第三方包的名称,而后去node_modules文件夹下获取第三方包,读取package.json中module属性指向的文件,而后将文件返回给浏览器:

#!/usr/bin/env nodeconst Koa = require('koa')const send = require('koa-send')const path = require('path')const app = new Koa()// 用于将读取的文件转换为字符串const streamToString = stream => new Promise((resolve, reject) => {    const chunks = []    stream.on('data', chunk => chunks.push(chunk))    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))    stream.on('error', reject)})app.use(async (ctx, next) => {    // 如果申请的门路蕴含'/@modules/',那么就将path批改成node_modules文件夹下指定包的门路    // 而后动态文件处理器会读取文件并返回    if (ctx.path.startsWith('/@modules/')) {        const moduleName = ctx.path.substr(10)        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')        const pkg = require(pkgPath)        ctx.path = path.join('/node_modules', moduleName, pkg.module)    }    await next()})// 动态文件服务器app.use(async (ctx, next) => {    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })    // 继续执行后续的中间件    await next()})// 批改第三方文件门路app.use(async (ctx, next) => {    // 如果是js文件    if (ctx.type === 'application/javascript') {        const contents = await streamToString(ctx.body)        // 为第三方援用后面加上/@module        ctx.body = contents            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')    }    await next()})app.listen(3000)console.log('Server running @ http://localhost:3000')

批改实现后,重新启动,此时能失常获取第三方包, 而且第三方外部援用的其余第三方包也能被失常解决:


此时,第三方包的解决曾经实现,然而咱们还是不能看到页面失常显示,是因为浏览器无奈辨认获取到的单文件组件和css文件。

解决css文件

在main.js中引入了css文件:

这个文件尽管可能被浏览器正确下载,然而,ES Module无奈辨认import的资源,此时须要对css进行解决。

Vite解决css文件和解决第三方资源的逻辑类似,就是将css的援用改为:

而后在服务端解决,返回可能被辨认的js文件,js文件中就是将读取到的css内容插入到以后html中。

#!/usr/bin/env nodeconst Koa = require('koa')const send = require('koa-send')const path = require('path')const { Readable } = require('stream')const app = new Koa()// 用于将读取的文件转换为字符串const streamToString = stream => new Promise((resolve, reject) => {    const chunks = []    stream.on('data', chunk => chunks.push(chunk))    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))    stream.on('error', reject)})const stringToStream = text => {    const stream = new Readable()    stream.push(text)    stream.push(null)    return stream}// 解决@modulesapp.use(async (ctx, next) => {    // 如果申请的门路蕴含'/@modules/',那么就将path批改成node_modules文件夹下指定包的门路    // 而后动态文件处理器会读取文件并返回    if (ctx.path.startsWith('/@modules/')) {        const moduleName = ctx.path.substr(10)        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')        const pkg = require(pkgPath)        ctx.path = path.join('/node_modules', moduleName, pkg.module)    }    await next()})// 动态文件服务器app.use(async (ctx, next) => {    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })    // 继续执行后续的中间件    await next()})// 解决?importapp.use(async (ctx, next) => {    if (ctx.query.import === '') {        const contents = await streamToString(ctx.body)        let code = 'const content = '        code += '`' + contents + '`\n'        code += `        const style = document.createElement('style');        style.setAttribute('type', 'text/css');        style.innerHTML = content;        document.head.appendChild(style);`        ctx.type = 'application/javascript'        ctx.body = stringToStream(code)    }    await next()})// 批改第三方文件门路app.use(async (ctx, next) => {    // 如果是js文件    if (ctx.type === 'application/javascript') {        const contents = await streamToString(ctx.body)        // 为第三方援用后面加上/@module        ctx.body = contents            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')            // 解决css门路            .replace(/(import\s+['"].*\.css)/g, '$1?import')    }    await next()})app.listen(3000)console.log('Server running @ http://localhost:3000')

重新启动站点

此时css资源可能被正确处理。

解决单文件组件

ES Module并不能辨认.vue结尾的资源,此时须要在服务端进行解决,解决逻辑就是解析单文件组件的js代码和template模板,将template模板转换为render函数,而后返回给浏览器解决后的js文件。

解析单文件组件须要应用@vue/compiler-sfc,因而增加依赖:

npm i -D @vue/compiler-sfc
  • 解析单文件组件,获取js局部

判断以后申请是否是单文件组件,如果是,那么应用compiler-sfc进行解析,获取其中的js局部,而后返回一个js文件,js内容格局如下:

const __script = '解析到的js内容'import { render as __render } from "单文件门路?type=template"__script.render = __renderexport default __script

此时js内容中蕴含一个模板地址援用,这个地址在后续进行解决。

解决逻辑如下:

#!/usr/bin/env nodeconst Koa = require('koa')const send = require('koa-send')const path = require('path')const { Readable } = require('stream')const compilerSFC = require('@vue/compiler-sfc')const app = new Koa()// 用于将读取的文件转换为字符串const streamToString = stream => new Promise((resolve, reject) => {    const chunks = []    stream.on('data', chunk => chunks.push(chunk))    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))    stream.on('error', reject)})const stringToStream = text => {    const stream = new Readable()    stream.push(text)    stream.push(null)    return stream}// 解决@modulesapp.use(async (ctx, next) => {    // 如果申请的门路蕴含'/@modules/',那么就将path批改成node_modules文件夹下指定包的门路    // 而后动态文件处理器会读取文件并返回    if (ctx.path.startsWith('/@modules/')) {        const moduleName = ctx.path.substr(10)        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')        const pkg = require(pkgPath)        ctx.path = path.join('/node_modules', moduleName, pkg.module)    }    await next()})// 动态文件服务器app.use(async (ctx, next) => {    await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })    // 继续执行后续的中间件    await next()})// 解决?importapp.use(async (ctx, next) => {    if (ctx.query.import === '') {        const contents = await streamToString(ctx.body)        let code = 'const content = '        code += '`' + contents + '`\n'        code += `        const style = document.createElement('style');        style.setAttribute('type', 'text/css');        style.innerHTML = content;        document.head.appendChild(style);`        ctx.type = 'application/javascript'        ctx.body = stringToStream(code)    }    await next()})// 解决单文件组件app.use(async (ctx, next) => {    if (ctx.path.endsWith('.vue')) {        const contents = await streamToString(ctx.body)        const { descriptor } = compilerSFC.parse(contents)        let code        if (!ctx.query.type) {            code = descriptor.script.content            // console.log(code)            code = code.replace(/export\s+default\s+/g, 'const __script = ')            // 将template模板转换为另一个import申请,"/src/App.vue?type=template"            code += `        import { render as __render } from "${ctx.path}?type=template"        __script.render = __render        export default __script        `        }        ctx.type = 'application/javascript'        ctx.body = stringToStream(code)    }    await next()})// 批改第三方文件门路app.use(async (ctx, next) => {    // 如果是js文件    if (ctx.type === 'application/javascript') {        const contents = await streamToString(ctx.body)        // 为第三方援用后面加上/@module        ctx.body = contents            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')            // 解决css门路            .replace(/(import\s+['"].*\.css)/g, '$1?import')    }    await next()})app.listen(3000)console.log('Server running @ http://localhost:3000')

重新启动站点:

  • 解决模板

因为在解析js的时候,将模板独自作为一个门路引入,此门路返回的是一个蕴含导出render函数的js文件,所以须要增加逻辑对模板门路进行解决:

在解决单文件组件js逻辑代码的中间件中持续批改,当申请单文件组件的query参数中的type=template时,就应用compiler-sfc解析单文件组件,获取render函数,而后返回给浏览器:

app.use(async (ctx, next) => {    if (ctx.path.endsWith('.vue')) {        const contents = await streamToString(ctx.body)        const { descriptor } = compilerSFC.parse(contents)        let code        if (!ctx.query.type) {            code = descriptor.script.content            // console.log(code)            code = code.replace(/export\s+default\s+/g, 'const __script = ')            // 将template模板转换为另一个import申请,"/src/App.vue?type=template"            code += `        import { render as __render } from "${ctx.path}?type=template"        __script.render = __render        export default __script        `        } else if (ctx.query.type === 'template') {            const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })            code = templateRender.code        }        ctx.type = 'application/javascript'        ctx.body = stringToStream(code)    }    await next()})

重新启动:

此时模板也可能失常解决。

解决图片资源

自定义vite写到这里,咱们解决了第三方资源,单文件组件和css资源,然而页面还是不能失常显示,这是因为在单文件组件中援用了图片资源

通过import形式引入的图片,浏览器是不能辨认的,此时须要将图片资源的import援用去掉,间接通过src援用图片门路。

大抵逻辑如下:

  1. 首先须要通过正则匹配:

import _imports_0 from './assets/logo.png'

  1. 获取_imports_0和图片门路。
  2. 将_createNode参数中的src值替换为图片门路
  3. 删除图片的import援用。
// 解决单文件组件app.use(async (ctx, next) => {    if (ctx.path.endsWith('.vue')) {        const contents = await streamToString(ctx.body)        const { descriptor } = compilerSFC.parse(contents)        let code        if (!ctx.query.type) {            code = descriptor.script.content            // console.log(code)            code = code.replace(/export\s+default\s+/g, 'const __script = ')            // 将template模板转换为另一个import申请,"/src/App.vue?type=template"            code += `        import { render as __render } from "${ctx.path}?type=template"        __script.render = __render        export default __script        `        } else if (ctx.query.type === 'template') {            const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })            code = templateRender.code            // 解决图片            const imgs = code.match(/import.*from\s+['"].*\.png'/g)            if (imgs) {                imgs.forEach(item => {                    code = code.replace(item, '')                    const strs = item.split('from')                    const key = strs[0].replace(/import\s+/g, '')                    code = code.replace(key.trim(), strs[1].replace('./', '/src/'))                })            }        }        ctx.type = 'application/javascript'        ctx.body = stringToStream(code)    }    await next()})

此时图片资源可能被失常加载,

然而页面还是报错:

这个谬误是因为在代码中应用了环境变量:

所以还须要解决环境变量。

解决环境变量

环境变量的解决很简略,只须要将js内容中的环境变量进行替换即可:

// 批改第三方文件门路app.use(async (ctx, next) => {    // 如果是js文件    if (ctx.type === 'application/javascript') {        const contents = await streamToString(ctx.body)        // 为第三方援用后面加上/@module        ctx.body = contents            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')            // 解决css门路            .replace(/(import\s+['"].*\.css)/g, '$1?import')            // 替换环境变量            .replace(/process\.env\.NODE_ENV/g, '"development"')    }    await next()})

解决实现后,从新关上站点,能够发现我的项目可能失常显示并失常工作: