你可能不需要 *.vue

barkjs

我只是要写个简单的 html 页面, 里面需要运行一些 js 来操作 DOM/BOM, 如果可能, 我希望利用 vue runtime

注意 本文假设你已具有以下内容的相关知识或者实践经验:

VAR

毫无疑问, 2016 ~ 2017 是 Vue 势头最强劲的两年. 根据笔者的记忆, 2017 年, Vue 的在 Github 上的 Star 数目首次超过 4W, 彼时 React 的 star 数目还在 3W 内, Angular v2/v4 还在 beta 版本. 三大框架互相学习, 尽管粉丝间掐架不少, 利益相关的布道者也上蹿下跳, 但 Vue 2 引入了 vdom, 在实现上, 摆脱源自 Angular 1.x 的模板概念; AngularVue 的作者之间还保持着互通有无的关系; React 升级后放弃了对 IE 8 的支持, 基于 S/P 模式的状态管理方案 mobx 在社区冉冉升起.

时间步入 2018 年, VAR 三大框架在各方面的差异此消彼长, 甚至不约而同地对 Typescript 做了官方的支持. 如果你在同一时间在项目中对三个框架都有实践, 难免会生出"万变不离其宗"的感慨: 说到底, 大家都玩起了预编译渲染纯函数.

进一步地说, *.jsx(React) , angular 的组件 和 *.vue(sfc), 最终都会转化为 runtime 中对 DOM 不同的操作函数. 这一点在 *.jsx(React) 中体现得淋漓尽致 —— 连 *.jsx 这种语法的诞生都是为了让 React 组件的 createElement/cloneElement 函数对开发者更友好; Vue 2 的单文件组件(sfc) 将其包装成了模板中的 v-* 指令、属性和事件绑定, 后来干脆也支持了 *.jsx(Vue). 无论 *.jsx 还是 sfc, 这些框架的 DSL 语法, 都是为了尽可能地减少(乃至彻底清除)自家框架在运行时解析特定指令的耗时, 同时能让开发者有良好的开发体验.

*.vue

对于 vue 新手而言, 直接在浏览器里引入完整版本的 vue.js 然后在 <script type="text/x-template"></script> 中书写 vue 的 template 是不错的开始, 但这样会导致 vue 在运行时,

  1. 先花费一部分时间去解析这段模板, 生成 vue 实例上的渲染纯函数
  2. 运行这些渲染纯函数, 处理 vue 实例中的 data 和 dom.

其中的第 1 步, 通过使用单文件组件(sfc)是完全可以节省的.

vue 的单文件组件(sfc) 是 xml 格式的文件, 经过 Vue 官方工具链的处理(如使用基于 vue-cli 的 webpack 的脚手架进行编译),后, 将变成可以在 Vue runtime 中运行的的一系列纯函数集. 详情可参考这里.

如果你还没安装 vue-cli 并生成一个使用 单文件组件(sfc) 来构建应用的脚手架, 你可以通过 vue-template-explorer 了解 vue template 和编译后的渲染纯函数的联系.

单文件组件(sfc) 的优势很多, 比如:

  • 通过 webpack/browserify 的配置, 可以安心地使用 typescript/es201X 的语法, 同时在编译后得到经过 polyfill 处理的、可以在 es5 环境下运行的代码
  • 使用 css 和 html 预编译器
  • 干净的组件内 scoped 特性.
  • 配合 webpack/browserify, 使用 nodejs 风格的模块管理.

但在此前, 单文件组件(sfc) 有个缺陷, 即需要较为完整的 vue-cli 工具链的支持, 如果你只是想:

  • 写个简单的页面, 里面只有一个 ajax/fetch 请求, 根据返回结果, 在页面上渲染该结果
  • 使用 vue
  • 最好能够支持 jsx(Vue), 这样就不用
  • 能直接书写 es6/typescript 语法, 并且能预编译为 es5 兼容的 js
  • 还希望能够使用 less/stylus/scss/sass 来书写样式, 并预编译为合法的 css

此时最快的途径, 似乎是使用 vue-cli 初始化一个 webpack-simple 的项目, 然后修改 webpack 配置, 使得 src/index.js 在编译后被注入 index.html, 得到一个页面.....稍等, 这类似于有时候你只是想写个简单的窗体应用, 却不得不去下载一个带完整 MFC 的 Visual Studio 一样. 我真的想这样做么? 我很可能配置 webpack 到一半就放弃了, 转而新建一个 index.html 文件, 然后直接在 <script type="text/x-template"></script> 里书写 vue 组件的模板, 然后运行起来 —— 尽管这样会引入完整的 vue.js 的文件(> 100KB), 并且 vue 需要耗时去解析模板, 但是这样的开发效率, 非常高啊!

别的思路

即便是在 vue-component-compiler 发布以后, 社区依然缺乏直接在 index.html 中书写 vue 组件并且能进行预编译的方案.

那么, 如果我们只是想写个简单的页面(就像 jQuery 盛行的时代中我们经常做的那样), 我们非得安装 vue-cli, 再初始化项目, 再安装庞大的 webpack 及其周边依赖么?

也许 parcel 适合做这件事, 它允许你在 index.html 中直接引入相对路径下的 *.js 文件, 并根据 parcel 的配置(默认配置通常已足够你使用)合适地处理其中所有的资源, 包括 *.js 文件自身超前的语法(es201X) 和 css 预编译器.

不过, 截至笔者书写到这里(北京时间 2018-02-11 17:14) 的时候, 根据 腾讯 imweb尝试, parcel 还有以下缺点:

  • 不支持SourceMap:在开发模式下,Parcel也不会输出SourceMap,目前只能去调试可读性极低的代码;
  • 不支持剔除无效代码(TreeShaking):很多时候我们只用到了库中的一个函数,结果Parcel把整个库都打包了进来;
  • 一些依赖会让Parcel出错:当你的项目依赖了一些Npm上的模块时,有些Npm模块会让Parcel运行错误;

注意 实际上, 这些缺点在下文笔者介绍的方案中也存在. 相信以上问题 parcel 会在不久的未来解决.

也许我们可以尝试用 parcel 来写一个简单的页面, 但如果你希望去了解 parcel 内部处理资源的细节, 对自己的项目有更多的了解, 又不必太符合 parcel 的社区形象(开箱即用, 零配置). 此时, 似乎 webpack 又更吸引你(尽管它的配置相对繁冗, 但至少对开发者可见)

有没有什么细节对开发者更为透明, 可以完全自己 DIY、不必造太多轮子的方案呢?如果抛开平时习惯 webpack/browserify 下的思维惯性, 整理一下, 其实真正帮助我们提高了效率的库是:

  • babel/bubble
  • JSX
  • pugjs/ejs/swig
  • stylus/less/scss/sass

回到本文的初衷: "我只是要写个简单的 html 页面, 里面需要运行一些 js 来操作 DOM/BOM, 如果可能, 我希望利用 vue runtime". 为了满足这个需求, 笔者认为, 至少要解决以下两个问题:

  •   支持 js/css/html 预编译器
  •   以 html 为入口, 直接在 html 中书写超前的的预编译器语法

预编译器处理的一种接口标准: jstransformer

jstransformers 是一系列 jstransformer-* 的统称, jstransformer 的目的, 是统一 js(一般指nodejs) 社区中各类预编译器库的 API, 将不同思路、不同风格、不同目的, 但都基于 js、处理 js 或其它资源(比如 less/typescript)的库进行接口标准化. 参看其 github 上的 README:

There are many good template engines and compilers written for Node.js. But there is a problem: all of them have slightly different APIs, requiring slightly different usage. JSTransformer unifies them into one standardized API. Code written for one transformer will work with any other transformer.

(Node.js 社区中)有非常多好的模板引擎和模板编译器. 但是存在这样一个问题: 它们的 API 有些微差异, 用法有细微的不同. JSTransformer 将它们统一为标准的 API. 一个 transformer 应该能与另一个 transformer (以同样的 API)进行协作.

jstransformers 要处理的目标很明确: 预编译器, 比如下表中的这些预编译器

预编译器种类 典型库
html 预编译器
  • pug/jade
  • ejs
  • juice
  • swig
css 预编译器
  • sass/scss
  • stylus
  • less
  • postcss
js 预编译器
  • coffeescript
  • babel
  • typescript

jstransformers 让笔者想到 webpack-loader 概念. 尽管 webpack-loader 集成给 webpack 项目的类库不只是预编译器, 但在只讨论"如何在项目中使用预编译器"的语境下, jstransformers 可以类比于 webpack-loader.

现在我们的第一个问题解决了

  •   支持 js/css/html 预编译器
  •   以 html 为入口, 直接在 html 中书写超前的的 js 语法(es201X)

直接在 HTML 中写预编译器语法

all-in-js ·PK· html-first

在考虑第二个问题之前, 先讨论一下目前社区对 html/css/js 前端三大基石的一种态度: all-in-js

前两年, React 社区有一种 all-in-js 的思路, 即 html 提供一个页面载入的入口, 把所有的资源(包括所有的 css 一些体积可接受的图片)打包到 js 中, 这种思路倾向于:

  • 如果不必要, css 可以完全不在 html 中书写(使用 <style> 标签)或引入(使用 <link> 标签)
  • html 文件除了 head 中的 meta 属性需要做一些定制, 其余部分基本不重要, 反正打包后的 js 会在 html 中注入开发者需要的一切.

这种思路和 React 单页应用(SPA) 的配合可谓天衣无缝, 不止资源, 业务逻辑也 all-in-js , 这似乎没什么问题. 但过去社区对 html 语义化的努力的成果在这种思路下被严重削弱了: 反正对 SEO 也不友好, 我为什么要语义化?并且, 这种思路对一些简单的需求(比如"我就想写个静态页面, 里面有大量的静态元素, 就一个 input 中输入的内容需要被实时显示到另一个元素里")天然不太友好, 可能开发者只希望 vue 或者 React 处理页面中有动态渲染的元素, 其余相对静态的 DOM 元素留用于保持页面结构、语义表达(比如您现在正在阅读的这篇博客, 右边的标签列表就完全只是静态的 ul > li 列表)

现在, 无论你使用 vue-cli 还是 create-react-app, 这类工具都可以给你一个基于 webpack 的脚手架, 并鼓励你: all-in-(js/vue/react), 把 Vue/React 的实例挂载到 body 元素下的一个根元素吧, 让 Vitural DOM 来为你处理一切...

那么问题来了, 如果我现在只是想写一篇博客, 但博客里有一些演示性的片段, 它只占了页面中的一小部分元素, 比如{% post_link fix-parse-date-error-in-chrome-50 "这篇文章" %}的末尾, 提供了一个小工具, 我希望使用 vue 来管理, 我可不希望把我所有的博客内容写在 vue 的组件里, 而是静态的 html, 毕竟, 博客的文字是静态的.

好, 现在我们写一个静态的 html 文件, 它可能是这样的:

{% include_code "样例博客" lang:html /assets/you-might-not-need-vue-sfc/sample-blog.html.raw %}

看起来似乎没什么问题, 但实际上, 我已经无意识地使用了 es6 的语法: 在 Vue 实例的 data() 函数返回值中, 笔者竟然直接使用了 inputstr 变量名作为返回对象的 key 名! 这在 es6 中是没问题的, 但在 es5 环境下, 这样的语法会导致致命错误.

在 webpack 环境下浸淫太久的开发者, 有时候可能忘记了某个特性是 es6 才支持的, 不自觉地在 html 的 <script> 标签中直接使用了. 比如, 笔者经常使用的 Array.from 是 es6 才支持的, 我希望能在 html 中毫无顾虑地使用它.

此时, 可能你开始怀念 webpack 脚手架为你提供的便利: 配好好 .babelrcbabal-loader, 接下来在入口文件(可能命名为 app.js)及其引用文件中就可以放心地使用 es201X 的新特性. 不过等等, 其实我们只是需要 babel 对吧? 将 es6 编译为 es5 甚至 es3 的, 是 babel 而非 webpack 啊!

真的只有 grunt/gulp/browserify/webpack/parcel 这些方案可以让我们毫无顾忌地书写 es6 语法么? 真的必须在 html 中引入一个入口文件再让 babel 处理么?明明我们只是需要 babel 来编译下 es6.

有什么办法可以在 html 中书写的 javascript 直接被 babel 编译么?

基于 pug 的预编译器集成方案

一个在 html 中直接使用 js/css 预编译器的方案是, 通过 pug filters 特性, 在 HTML 中直接书写各种预编译器语法, 参看以下代码:

{% include_code "pug filters 举例" lang:pug /assets/you-might-not-need-vue-sfc/pug-filters.pug.raw %}

jstransformer-babel 是一个可以将 babel 接口的标准化中间件. 根据 pug filters 特性, 我可以在 pug 中书写一段 javascript, 将其置于一个 :babel filter block 中, 并提前安装好 jstransformer-babel, 配置好 .babelrc, jstransformer-babel 将自动帮我们把 es6 语法进行编码.

一个更轻量的方案

综合以上分析, 基于 jstransformerspug, 抛开 webpack 或者 parcel, 我们一样可以完美书写 es201X 的语法(甚至 typescript/coffeescript). 实际上, {% post_link fix-parse-date-error-in-chrome-50 "这篇文章" %} 就使用 pug 书写静态内容的, 文章中所有的交互性区域, 使用 vue 管理; 部分需要在页面中特定书写的 css, 使用 stylus 语法书写, 并由 jstransformer-stylus 进行编译.

至此, 我们解决了第二个问题,

  •   支持 js/css/html 预编译器
  •   以 html 为入口, 直接在 html 中书写超前的的 js 语法(es201X)

在这种方案中, 你可以自由地使用各种 jstransformers 的库来编译你内容, 如果这些库不符合你的需要, 你也可以编写自己的脚本, 使用你自定义的 pug-fitler, 并使用 node.js 来运行.

为了更深入地讲解这种方案, 我写了一个示例项目, 并通过一个小系列来讨论一下这种方案的应用场景和它的局限性, 你可以从项目和系列文章中获得更多的关于这种方案的细节和应用场景. 如果你对这种项目感兴趣, 欢迎到项目 issue 中提出讨论, 或者发起 PR.