
终于来到了第五篇,到了东家发盒饭的日子了。
打工人的快乐,就是如此简单!不过让我最开心的就是上篇文章的一个诚恳的评论!

这个世道
势利眼、冷脸子、闲言碎语、指桑骂槐, 好了遭人嫉妒,差了让人瞧不起。 忠厚,人家说你傻; 精明,人家说你奸; 冷淡了,大伙说你傲; 热情了,群众说你浪;(——————编辑部的故事)
导致很多人,开始商业互吹!嘴里从来没有真话,特别是在现在的互联网时代!
敢直言不讳的人确实不多了,
听到真话,我很开心,思索良久,确实略表歉意!
理工男虽然不善言谈,小作文也不是我的强项,但基本的行文措辞,语句逻辑,也应该仔细考究!
不然对不起辛勤耕耘在一线的语文老师们!所以这一期,请jym监督一下,如有再有错别字,请评论区指出,我改正!
我也是在发表之前仔细阅读,防止出现纰漏!
好,我们言归正传,上回书我们说到了 双向通信,说大白话就是设计一个结构让宿主环境和沙箱环境能够畅通无阻的通信。
而这一期,我们就要进入到重头戏了--沙箱编译
聊起编译,对于很多jym来说就是盲盒,因为在平常的业务中,谁会用得上这个呢?
我就是个臭写业务的,我只会crud
但是,其实编译这一块的内容了解之后,你甚至都不需要了解的很深入,你就会发现他非常有用,我们就能了解整个项目大概的运行流程!
能快速的预防,定位并解决我们在业务中的问题,也就是老话说的预判到你的预判
举个例子:
我们还拿vue举例!
我们知道vue的template 模板最后的编译结果是一个render函数
如下一段代码:
<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>他最后的编译结果就会这样
const __sfc__ = {}
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, vModelText as _vModelText, withDirectives as _withDirectives, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((_ctx.msg) = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _ctx.msg]
])
], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "App.vue"
export default __sfc__他最终就会编译成一个函数去做执行,那么有了这个前提之后,我们就可以不用那么教条,做一些巧妙的骚操作,来达到我们的目的!
你比如说,我们知道vue的模板的调试是非常困难的,通常情况下如果是模板报错之后,很多人排查问题是相当费力的!
但当你知道了编译之后是一个函数,我们就可以去猜想,函数中也可以去执行函数啊,于是你就不会教条的认为在双花括号中只能放变量,然后我们就可以这样
<script setup>
import { ref } from 'vue'
const log=(val)=>{
console.log(val)
}
</script>
<template>
<h1>{{ log(msg)}}</h1>
</template>编译之后:
/* Analyzed bindings: {
"ref": "setup-const",
"log": "setup-const"
} */
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import { ref } from 'vue'
const __sfc__ = {
__name: 'App',
setup(__props) {
const log=(val)=>{
console.log(val)
}
//在这个函数中,我们就能清楚的知道打印的值的内容以便来做一些判断
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(log(_ctx.msg)), 1 /* TEXT */))
}
}
}
__sfc__.__file = "App.vue"
export default __sfc__如果你想断点调试,我们还可以突发奇想的这样:
<script setup>
import { ref } from 'vue'
</script>
<template>
<h1>{{ (()=>{debugger})() }}</h1>
</template>对应编译之后的内容如下:
/* Analyzed bindings: {
"ref": "setup-const"
} */
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import { ref } from 'vue'
const __sfc__ = {
__name: 'App',
setup(__props) {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock("h1", null, _toDisplayString((()=>{debugger})()), 1 /* TEXT */))
}
}
}
__sfc__.__file = "App.vue"
export default __sfc__
当你启动之后,你就会发现,他在render 模板中走了断点了,那你不就能看到模板中每个函数执行的值了。
但是,各位jym,我在这里提醒大家,不要因为学会了点小技巧,就到处嘲笑别人指点别人,人家不会这个技巧, 人家只是听话的按照文档的要求一步步的循规蹈矩的操作而已!
他有什么错,他只是教条了一点!
对别人来说,写vue可能就是个工作,人家没必要那么上心,也许人家别的方向,天赋异禀呢?
老话说得好,三人行,必有我师,我们要心怀敬畏!
我相信以上例子足以说明我们懂编译的重要性,接下来,我们就来简单的了解一下,沙箱中的编译
要简单的了解沙箱编译,我们就要从丘处机路过牛家村,额不,从什么是编译器开始!
编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。
一个现代编译器的主要工作流程如下:
源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables),最后打包好的文件就可以给电脑去判读执行了。
这是在业界给编译器下的一个通用的定义,然而在面对实际问题的时候,还得区别对待,就像我泱泱中华走的虽然是社会主义,但确是中国特色的社会主义!
放到前端领域也是一样,在前端领域,所谓的编译器,最后为了方便使用都封装成了个包供我们使用!
比如大名鼎鼎的babel、比如vue的@vue/compiler-sfc 等等,都被封装成了一个开箱即用的包,来供我们使用
那有人问,我们想要实现沙箱的编译,只需要使用这两个包就足够了吗?
额!当然不是了。
由于我们编译后的代码要在浏览器中跑,那么必然我还要对代码做二次编译,来带达到目。
别急我们一个个来。
我们先说vue的编译,因为我们吃的就是vue的这碗饭!
说起vue 我们知道他是模板写法 ,这种语法更直观,更好用,也是我们这种老前端,非常喜欢的写法! 但是他却有一个致命的软肋,它属于民间个人开发者维护。
个人开发者,自然而然的就不会那么财大气粗,平亿近人! 也不会引领和改变市场的走向。
所以他不像react这种财大气粗的团队作案,背靠facebook。
所谓靠山吃山,导致babel 给他们开了后门,将jsx语法直接纳入编译中来。我们在使用的时候,只需要在babel中做配置即可
而反观vue 就没有这样好的待遇!他必须自己解决问题,他唯一的优势就是灵活,能快速迎合市场需求!
在激烈的竞争中脱脱颖而出!
这样导致的结果,就是vue每个版本就会单独出一个编译器。使用方法也不尽相同,接下来我们一个个来简单的列举一下
vue2.6的模板就会使用vue-template-compiler来执行编译
该模块可用于将 Vue 2.0 模板预编译为render函数 ,以避免运行时编译开销。大都数场情况下,他都是 vue-loader一起使用
Vue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFC)的格式撰写 Vue 组件
我们熟悉的书写格式如下:
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
}
}
}
</script>通过 Vue Loader中执行vue-template-compiler 在webpack的打包中将以上代码编译为一个包含render函数的配置对象
代码如下:
export default {
data () {
return {
msg: 'Hello world!'
}
},
render() {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c('div', {
staticClass: "example"
}, [_vm._v(_vm._s(_vm.msg))])
}
}其实他在沙箱中的的使用方式非常简单,根据提供的api 将整个单文件组件分开即可代码如下,然后再对应的处理,最后拼接为一个文本字符串即可!
// 提供的方法
import { parseComponent, compile } from 'vue-template-compiler'
// polyfill解决with 等等
import transpile from 'vue-template-es2015-compiler'
//scss编译
import { scssCompile } from '../sass/index'
// css 编译添加scoped
import {
compileStyleAsync,
} from '@vue/compiler-sfc'
// log方法
import * as logger from '../../../utils/logger';
// 插入css
import { insertCss } from '../style/insert-css';
// 编译需要配置
import assetUrlsModule from './assetUrl'
// @ts-ignore
// 生成唯一id
import hashId from 'hash-sum'
// @ts-ignore
// 默认对象名
export const COMP_IDENTIFIER = `__sfc__`
// 编译方法主体
export async function compileFile(code: string, filename: string): Promise<any> {
const id = hashId(filename)
let clientCode = ''
const appendSharedCode = (code: string) => {
clientCode += code
}
// 拆分模板
const { template, script, styles } = parseComponent(code, { pad: 'line' })
//处理js 内容
const clientScriptResult = await doCompileScript(script)
// 拼接js内容
appendSharedCode(clientScriptResult)
// 处理编译模板
const clientTemplateResult = await doCompileTemplate(template)
// 拼接处理后的模板内容
appendSharedCode(clientTemplateResult)
// 处理css
const hasScoped = styles.some((s) => s.scoped)
let css = ''
for (const style of styles) {
let source: any = style.content
// 处理scss 内容
if (style.lang && ['scss', 'sass'].includes(style.lang)) {
source = await scssCompile(source)
}
// 处理scoped内容
const styleResult = await compileStyleAsync({
source,
filename,
id,
scoped: style.scoped,
})
if (styleResult.errors.length) {
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
logger.error(styleResult.errors)
}
} else {
css += styleResult.code + '\n'
}
}
if (hasScoped) {
//处理Scopedde的情况
appendSharedCode(
`\n${COMP_IDENTIFIER}._scopeId = ${JSON.stringify(`data-v-${id}`)}`
)
}
// 添加css
const cssObj = insertCss(id, css, true);
appendSharedCode(cssObj.code)
appendSharedCode(genHotReloadCode(id))
//添加当前单文件组件的一些内容
appendSharedCode(
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
`\nexport default ${COMP_IDENTIFIER}`
)
// 返回最后的结果
return { js: clientCode, setMap: cssObj.setMap }
}
// 生成js
async function doCompileScript(script) {
let code = `${script.content.replace('export default', 'const ' + COMP_IDENTIFIER + '=')}`
return code
}
async function doCompileTemplate(template) {
const templateResult = compile(template.content, {
outputSourceRange: true,
modules: [assetUrlsModule(undefined)]
})
const fnName = `render`
let code = `\n${COMP_IDENTIFIER}.${fnName} = function(){${templateResult.render}}`
return transpile(code)
}
function genHotReloadCode(
id: string,
): string {
// 生成代码
return `
/* 热更新相关 */
if (module.hot) {
const api = require('vue-hot-reload-api')
const Vue = require('vue')
api.install(Vue)
${COMP_IDENTIFIER}.__hmrId = "${id}"
if (!api.createRecord('${id}', ${COMP_IDENTIFIER})) {
api.reload('${id}', ${COMP_IDENTIFIER})
}
api.rerender('${id}', ${COMP_IDENTIFIER})
}
`;
}他的具体编译原理,我们就不再赘述了, 首先他不是我们这次的重点,其次,由于vue的热门,他的原理解析都烂大街了
我们只是带jym简单的了解下大概的编译以便为了后续的vue3的编译做铺垫。
毕竟vue3才是热门,才是流量,
额,我又暴露本性了!
好我们言归正传!!!
我之前说过vue的各个版本都有各自的编译器,所以在升级了vue2.7之后,同样的,他还有一套单独的编译器!
因为,vue2.7版本,号称是vue2的绝唱
他继承了vue2 的所有能力,并且有了vue3的所有特性!
那么自然而然的它的编译器就会重写。
需要注意的是vue2.7版本虽然重写了编译器,其实他也是站在巨人的肩膀上,他也是在vue2.6那个编译器的基础上拓展了功能
我们从代码上就能看出来
import { generateCodeFrame } from 'compiler/codeframe'
import { camelize, capitalize, isBuiltInTag, makeMap } from 'shared/util'
// 引用了之前包的很多方法
import { parseHTML } from 'compiler/parser/html-parser'
import { baseOptions as webCompilerOptions } from 'web/compiler/options'我们发现他其实还是引用了之前编译器中的很多方法!而不是完全自己重写一套!
然而,需要大家注意的是,vue2.7的编译器,目前官方打包后没有浏览器版本,如果我们要想在浏览器环境执行编译,就需要费一番周折了!
我目前的做法,是将编译器的代拿出来,打包一份浏览器版本代码!
由于vue2.7的编译器,和vue3的@vue/compiler-sfc 使用方式很相似 ,他这里的使用方法,我们就不再赘述,我们直接上vue3的编译!
说起@vue/compiler-sfc的编译器,他确实是重写,并且是从内到外的重写,他的源码想当年我也是扎扎实实的撸了一遍。
虽然没怎么看懂,但我大有收获!
这种感觉我给大家形容一下就是,虽然我不明白,但大受震撼
并且vue3的@vue/compiler-sfc还惊喜的提供了一个浏览器版本

大家可以去看一下,2.7目前是没有的,目前我也没有猜到官方是个什么目的!
难道是尤大准备开个qq群?付费进群发代码?
额,不可能不可能,尤大是大佬,怎么能是这种人!
是我无耻的yy了。
什么?我在含沙射影谁?

那不敢,我们这等小人物,哪敢质疑行业的某些位大佬呢?
毕竟人家也是凭本事挣钱,不寒碜!
不过,在这里,我要给大家提个醒,如今互联网行当,也是粉丝经济盛行 ,鱼龙混杂,大家一定要擦亮双眼!
不要被蒙蔽,那些位告诉你买了他的课就能怎么怎样怎样的人。
千万要留个心眼! 他可能不是在教育你,可能是在收割你。
学习技术这个东西,还是要脚踏实地!而且技术也不是工作的全部。还有很多东西值得我们思考。
比如说,行业趋势,选择方向,协作能力,沟通本领,项目经验,行业壁垒,运气成分,其实都是我们升职加薪,人生巅峰的因素!

看到这,各位jym是不是以为我要提醒大家 花钱卖课要谨慎?互联网寒冬要理智?
不不不,其实我也是在用正确的废话收割各位,吸引眼球,没有各位的捧,东家怎么能给我结账呢?
总之,对我自己来说,吾日三省吾身吧,做个有良心的镰刀(其实我也想做个没良心的镰刀,可惜没这能耐,毕竟人丑嘴不甜长得磕颤还没钱,哈哈哈)
我们言归正传,接着说vue3的编译器,vue3的编译器想要在沙箱中跑,其实就相当简单了,因为他有贴心的浏览器版本,我们只需要在代码中引入即可。并且他提供了丰富的api来供我们使用。并且,他跟vue2.6的编译很相似
具体代码如下:
// 引入@vue/compiler-sfc
import {
SFCDescriptor,
BindingMetadata,
compileStyleAsync,
compileTemplate,
CompilerOptions,
parse,
compileScript,
rewriteDefault
} from '@vue/compiler-sfc'
// 处理sass
import { scssCompile } from '../sass/index'
// log内容
import * as logger from '../../../utils/logger';
// 处理style
import { insertCss } from '../style/insert-css';
// 转义ts 语法
import { transform } from 'sucrase'
// @ts-ignore
// 唯一id
import hashId from 'hash-sum'
// 编译ts 语法
async function transformTS(src: string) {
return transform(src, {
transforms: ['typescript'],
disableESTransforms: true,
}).code
}
// 组件默认名称
export const COMP_IDENTIFIER = `__sfc__`
// 编译入口
export async function compileFile(code: string, filename: string): Promise<any> {
const id = hashId(filename)
// 解析vue文件
const { errors, descriptor } = parse(code, {
filename,
sourceMap: true
})
if (errors.length) {
errors.forEach(err => {
logger.error(err)
});
}
// 根据处理后的信息,编译script
const scriptLang =
(descriptor.script && descriptor.script.lang) ||
(descriptor.scriptSetup && descriptor.scriptSetup.lang)
// 处理模板中Lang瞎写等情况
const isTS = scriptLang === 'ts'
if (scriptLang && !isTS) {
logger.error(`Only lang="ts" is supported for <script> blocks.`)
return ""
}
//编译后字符串初始值
let clientCode = ''
const appendSharedCode = (code: string) => {
clientCode += code
}
//scoped 的情况
const hasScoped = descriptor.styles.some((s) => s.scoped)
// css字符串初始值
let css = ''
for (const style of descriptor.styles) {
let source: any = style.content
// 处理scss 内容
if (style.lang && ['scss', 'sass'].includes(style.lang)) {
source = await scssCompile(source)
}
// 处理scoped内容
const styleResult = await compileStyleAsync({
source,
filename,
id,
scoped: style.scoped,
modules: !!style.module
})
// 错误处理
if (styleResult.errors.length) {
// postcss uses pathToFileURL which isn't polyfilled in the browser
// ignore these errors for now
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
logger.error(styleResult.errors)
}
// proceed even if css compile errors
} else {
// 生成css 结果
css += styleResult.code + '\n'
}
}
// 处理js 内容
const clientScriptResult = await doCompileScript(
descriptor,
id,
isTS
)
if (!clientScriptResult) {
return {}
}
const [clientScript, bindings] = clientScriptResult
clientCode += clientScript
if (
descriptor.template &&
(!descriptor.scriptSetup)
) {
// 处理模板内容
const clientTemplateResult = await doCompileTemplate(
descriptor,
id,
bindings,
false,
)
if (!clientTemplateResult) {
return {}
}
clientCode += clientTemplateResult
}
// 处理Scoped 内容
if (hasScoped) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
)
}
// 添加css
const cssObj = insertCss(id, css, true);
appendSharedCode(cssObj.code)
appendSharedCode(genHotReloadCode(id))
// 添加模板信息
appendSharedCode(
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
`\nexport default ${COMP_IDENTIFIER}`
)
// 返回结果
return { js: clientCode, setMap: cssObj.setMap }
}
async function doCompileScript(
descriptor: SFCDescriptor,
id: string,
isTS: boolean
): Promise<[string, BindingMetadata | undefined] | undefined> {
if (descriptor.script || descriptor.scriptSetup) {
try {
const expressionPlugins: CompilerOptions['expressionPlugins'] = isTS
? ['typescript']
: undefined
// 利用sfc 编译script
const compiledScript = compileScript(descriptor, {
inlineTemplate: true,
id,
templateOptions: {
compilerOptions: {
}
}
})
let code = ''
// 拼接内容
if (compiledScript.bindings) {
code += `\n/* Analyzed bindings: ${JSON.stringify(
compiledScript.bindings,
null,
2
)} */`
}
code +=
`\n` +
rewriteDefault(
compiledScript.content,
COMP_IDENTIFIER,
expressionPlugins
)
// 处理tss 语法
if ((descriptor.script || descriptor.scriptSetup)!.lang === 'ts') {
code = await transformTS(code)
}
return [code, compiledScript.bindings]
} catch (e: any) {
logger.error(e.stack.split('\n').slice(0, 12).join('\n'))
return
}
} else {
return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
}
}
async function doCompileTemplate(
descriptor: SFCDescriptor,
id: string,
bindingMetadata: BindingMetadata | undefined,
isTS: boolean
) {
// 处理编译模板
const templateResult = compileTemplate({
source: descriptor.template!.content,
filename: descriptor.filename,
id,
scoped: descriptor.styles.some((s) => s.scoped),
slotted: descriptor.slotted,
ssrCssVars: descriptor.cssVars,
isProd: false,
compilerOptions: {
bindingMetadata,
expressionPlugins: isTS ? ['typescript'] : undefined
}
})
if (templateResult.errors.length) {
logger.error(templateResult.errors)
return
}
const fnName = `render`
// 拼接内容
let code =
`\n${templateResult.code.replace(
/\nexport (function|const) (render|ssrRender)/,
`$1 ${fnName}`
)}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
return code
}
function genHotReloadCode(
id: string,
): string {
return `
/* hot reload */
if (module.hot) {
${COMP_IDENTIFIER}.__hmrId = "${id}"
const api = __VUE_HMR_RUNTIME__
if (!api.createRecord('${id}', ${COMP_IDENTIFIER})) {
api.reload('${id}', ${COMP_IDENTIFIER})
}
api.rerender('${id}', ${COMP_IDENTIFIER}.render)
}
`;
}我们最后生成的代码如下:
/* Analyzed bindings: {} */
import { ref, reactive, watch, onMounted } from 'vue'
const __sfc__ = {
setup(props, { emit }) {
},
}
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { class: "tpl-editor" }
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", _hoisted_1))
}
__sfc__.render = render
const getUurl=(url)=>{
let requireUrl;
let requireDefault;
if(typeof require!="undefined"){
requireUrl = require;
}else{
requireUrl = function(){};
}
if(typeof _interopRequireDefault != "undefined"){
requireDefault = _interopRequireDefault;
}else if(typeof _interopRequireDefault2 != "undefined"){
requireDefault = _interopRequireDefault2;
}else{
requireDefault = function (obj) { return obj && obj.__esModule ? obj : { default: obj }; };
}
return requireDefault(requireUrl(url));
};
function doCss(css) {
const reg = /url\(["|'](.*?)["|']\)/g
return css.replace(reg, function (match, $1) {
return "url('" + getUurl($1).default + "')"
})
};
function createStyleNode(id, content) {
var styleNode =
document.getElementById(id) || document.createElement('style');
styleNode.setAttribute('id', id);
styleNode.type = 'text/css';
if (styleNode.styleSheet) {
styleNode.styleSheet.cssText = doCss(content);
} else {
styleNode.innerHTML = '';
styleNode.appendChild(document.createTextNode(doCss(content)));
}
document.head.appendChild(styleNode);
}
createStyleNode(
"241901a6",
""
);
module.hot.accept()
/* hot reload */
if (module.hot) {
__sfc__.__hmrId = "241901a6"
const api = __VUE_HMR_RUNTIME__
if (!api.createRecord('241901a6', __sfc__)) {
api.reload('241901a6', __sfc__)
}
api.rerender('241901a6', __sfc__.render)
}
__sfc__.__file = "/src/index.vue"
export default __sfc__如此一来在沙箱中中即可使用。
沙箱编译我们讲完了vue的编译,但是,react ,css,babel 等我们还没有讲完, 如果还有后续的话。
将继续为大家讲解!