使用Rollup从零开始搭建Vue脚手架工具

2,041 阅读7分钟

脚手架工具(如 Vue-cli,Vite)可以让我们更高效地进行 Vue 开发,掌握他们的工作原理能有助于提升开发技能。实践最能出真理,自己就从零开始手撸了一个脚手架工具,以加深对脚手架原理的理解。废话不多说,先上代码地址。

究极死胖兽/sps-vue-cli (gitee.com)

初始化

首先来实现脚手架工具最核心的功能------代码打包功能。

创建工程文件夹,在文件夹中生成package.json配置文件并安装rollup包。

npm init
yarn add rollup -D

创建入口文件。

// src/main.js
console.log('hello vue')

对rollup配置入口文件路径与输出文件及格式。

// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
}

配置dev指令,c参数表示执行编译,w参数表示监听文件状态,在文件修改后自动重新编译

// package.json
"scripts": {
  "dev": "rollup -wc"
 }

执行yarn dev指令后,就可以看到在根目录文件夹下多了一个bundle.js文件,其中内容便是main.js打包后的内容

TS插件

TS已成为Vue3的官方标配,使用TS可以提升代码的规范性,减少类型带来的BUG,在大型项目中效果尤为明显。

将入口文件改造为ts文件。

// src/main.ts
interface Test {
  name: string
}

const s: Test = { name: 'sps' }
document.write(s.name)

在根目录下添加tsconfig.json,并进行基础配置。

// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext", // 语法版本
    "strict": false, // 严格模式
    "baseUrl": ".", // 基础目录
    "paths": {
      // alias配置
      "@/*": ["src/*"]
    }
  },
  // ts编译器处理的文件范围
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

安装并引入@rollup/plugin-typescript插件,除此之外还需要安装ts编译器相关的包:tslib typescript

// rollup.config.js
import ts from '@rollup/plugin-typescript'

export default {
  input: 'src/main.ts',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
    ts({
      tsconfig: './tsconfig.json'
    })
  ]
}

再次打开bundle.js文件,ts文件已经被编译为js文件。而且还有一个额外的惊喜,在ts文件中用到的const关键字也被顺便变成了var,这是因为TS是ES的超集,支持ES的最新语法,并能通过配置target属性来控制编译后的js文件版本(默认为ES3)。

HTML

创建一个HTML文件在其中引入bundle.js文件。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Sps-Vue-Cli</title>
    
  </head>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>

接下就可以在浏览器中打开html文件了

Server插件与热更新插件

目前在对代码进行修改后rollup能自动重新编译,但必须要手动刷新浏览器才能看到修改后的页面。

安装并引入rollup-plugin-serve rollup-plugin-livereload来实现代码的热更新。

// rollup.config.js
//...
import server from 'rollup-plugin-serve'
import liverload from 'rollup-plugin-livereload'

export default {
  //...
  plugins: [
    //...
    server({
      open: true,
      openPage: '/index.html',
      port: 5000
    }),
    liverload()
  ]
}

Vue

接下来就该在脚手架中引入Vue了。

简单使用

用render函数创建一个Vue组件。

// src/App.ts
import { defineComponent, h } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    return () => {
      return h('div', null, 'sps')
    }
  }
})

main.ts中将组件挂载到dom节点上。

// src/main.ts
import { createApp } from 'vue'
import App from './App'

const app = createApp(App)
app.mount('#app')

由于这里用到了import语法进行模块引用,所以需在tsconfig.json中进行相应配置。

// tsconfig.json
"moduleResolution": "node",

这里还需要处理三个问题:

  1. rollup本身无法处理vue等外部模块的引入,需要安装并引入@rollup/plugin-node-resolve插件。
  2. vue源码中多处用到环境变量,执行process.env.NODE_ENV操作时会报错并提示processundefined,安装并引入@rollup/plugin-replace插件可以在编译代码时将process.env.NODE_ENV等环境变量替换为具体值。
  3. 环境变量__VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__如果不进行初始化会报警告,可更具自身需要对其进行配置。
// rollup.config.js
//...
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'

const env = process.env.NODE_ENV

export default {
  //...
  plugins: [
    nodeResolve(),
    ts({
      tsconfig: './tsconfig.json'
    }),
    replace({
      preventAssignment: true, // 防止环境变量在代码中被修改
      'process.env.NODE_ENV': JSON.stringify(env),
      '__VUE_OPTIONS_API__': true,
      '__VUE_PROD_DEVTOOLS__': true
    }),
    //...
  ]
}

在真实开发场景中,一般不会直接使用render函数来进行开发,而是采用jsx或者单文件组件的方式。rollup默认是无法编译.js以外格式的文件,所以还需要进行额外的处理。

jsx

首先在tsconfig.json中配置对jsx语法的支持。

// tsconfig.json
"jsx": "preserve"

安装并引入@rollup/plugin-babel插件(注意顺序,babel插件需在ts插件之前,不然ts插件会因无法编译jsx语法而报错)。

// rollup.config.js
//...
import babel from '@rollup/plugin-babel'

const env = process.env.NODE_ENV
const extensions = ['.js', '.ts', 'tsx']

export default {
  //...
  plugins: [
    //...
    babel({
      exclude: 'node_modules/**',
      babelHelpers: 'bundled',
      extensions
    }),
    ts({
      tsconfig: './tsconfig.json'
    }),
    //...
  ]
}

安装babel相关的包:@babel/core @babel/preset-env @babel/preset-typescript @vue/babel-plugin-jsx。并在根目录下新增.babelrc文件来进行配置。

// .babelrc
{
  "presets": [
    "@babel/env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@vue/babel-plugin-jsx"
  ]
}

再将App组件改造为.tsx格式进行测试。

// src/App.tsx
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  setup () {
    return () => {
      return (
        <div class="test">sps</div>
      )
    }
  }
})

SFC

采用单文件组件来进行开发需要安装并引入rollup-plugin-vue插件,需在ts插件之前,原因同上。

// rollup.config.js
//...
import vue from 'rollup-plugin-vue'

const env = process.env.NODE_ENV
const extensions = ['.js', '.ts', 'tsx']

export default {
  //...
  plugins: [
    //...
    vue(),
    ts({
      tsconfig: './tsconfig.json'
    }),
    //...
  ]
}

除此之外需要安装 @vue/compiler-sfc包。这里有个大坑,笔者折腾了小半天才找到原因。直接安装@vue/compiler-sfc包后rollup在进行编译后会报错,点开源码找到报错地点为@vue/compiler-sfc包中有个compileScript函数,在对该函数的第二个参数进行解构时报undefined错误,查看rollup-plugin-vue源码时发现在调用此函数时只传入了一个参数导致报错。两个包都是尤神写的,但是rollup-plugin-vue已经好几个月没更新了,估计尤神专注更新vite去了,而@vue/compiler-sfc一直在更新。版本与最新版vue一致,已经是vue 3.1.x的版本,所以这里需要指定@vue/compiler-sfc的版本为3.0.x来进行安装。

然后就可以使用.vue格式的单文件组件来进行开发。

// src/App.vue
<template>
  <div class="test">sps</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App'
})
</script>

记得在src目录下新增shims-vue-d.ts文件来声明.vue文件的export类型,否则ts会报错。

// src/shims-vue-d.ts
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

Postcss

安装并引入rollup-plugin-postcss插件。

// rollup.config.js
//...
import postcss from 'rollup-plugin-postcss'

const env = process.env.NODE_ENV
const extensions = ['.js', '.ts', 'tsx']

export default {
  //...
  plugins: [
    //...
    postcss({})
    //...
  ]
}

接下来安装postcss和自己需要的css预处理包。

后面就可以编写css样式,并在main.ts中引入。

// src/style/index.scss
.test {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  color: red;
  font-size: 60px;
}
// src/main.ts
import './style/index.scss'

Eslint

安装并引入rollup-plugin-eslint插件。

// rollup.config.js
//...
import { eslint } from 'rollup-plugin-eslint'

const env = process.env.NODE_ENV
const extensions = ['.js', '.ts', 'tsx']

export default {
  //...
  plugins: [
    //...
    eslint({
      include: ['src/**.ts', 'src/**.tsx'],
      throwOnError: true
    }),
    //...
}

对需要使用的eslint规则进行配置,例如这里配置了no-consolewarn后,假如代码中出现console.log这样的代码在编译时rollup就会报出警告。

// .eslintrc.js
module.exports = {
  env: {
    browser: true
  },
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module'
  },
  rules: {
    'no-console': 'warn'
  }
}

配置进行eslint规则检查时忽略的文件夹和文件。

// .eslintignore
/node_nodules/*
/dist/*
rollup.config.js
bundle.js

Build

上面实现了开发模式下的脚手架功能,在真实环境中,还需要实现产品模式的功能。

先将两种模式的公共配置项抽取出来。

// rollupConfig/index.js
import ts from '@rollup/plugin-typescript'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import babel from '@rollup/plugin-babel'
import vue from 'rollup-plugin-vue'
import postcss from 'rollup-plugin-postcss'
import { eslint } from 'rollup-plugin-eslint'

const env = process.env.NODE_ENV
const extensions = ['.js', '.ts', 'tsx']

export default {
  input: 'src/main.ts'
}

export const plugins = [
  nodeResolve(),
  babel({
    exclude: 'node_modules/**',
    babelHelpers: 'bundled',
    extensions
  }),
  vue(),
  eslint({
    include: ['src/**.ts', 'src/**.tsx'],
    throwOnError: true
  }),
  ts({
    tsconfig: './tsconfig.json'
  }),
  postcss({}),
  replace({
    preventAssignment: true,
    'process.env.NODE_ENV': JSON.stringify(env),
    '__VUE_OPTIONS_API__': true,
    '__VUE_PROD_DEVTOOLS__': true
  })
]

服务器与热更新插件只在开发模式下需要用到。

// rollup.config.dev.js
import rollupConfig, { plugins } from './rollupConfig/index'
import server from 'rollup-plugin-serve'
import liverload from 'rollup-plugin-livereload'

export default {
  ...rollupConfig,
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
    ...plugins,
    server({
      openPage: '/index.html',
      port: 5000
    }),
    liverload()
  ]
}

在生产模式中,安装并引入rollup-plugin-terser插件来将编译后的代码最小化。

// rollup.config.prod.js
import rollupConfig, { plugins } from './rollupConfig/index'
import { terser } from 'rollup-plugin-terser'

export default {
  ...rollupConfig,
  output: {
    file: 'dist/index.js',
    format: 'cjs'
  },
  plugins: [
    ...plugins,
    terser()
  ]
}

package.json中分别为两种模式配置对应的指令,生产模式中不需要监控代码改变,所以只需要-c参数。

// package.json
"scripts": {
  "dev": "rollup -wc rollup.config.dev.js",
  "build": "rollup -c rollup.config.prod.js"
}

总结

进行以上操作后便实现了Vue脚手架工具最基本的功能,但是对比Vue-cli与Vite这类成熟工具,还需要进一步完善,例如开发服务器的反向代理功能,编译后js文件的自动分块功能等等。后面的工作就是根据需要对其进行拓展。