脚手架工具(如 Vue-cli,Vite)可以让我们更高效地进行 Vue 开发,掌握他们的工作原理能有助于提升开发技能。实践最能出真理,自己就从零开始手撸了一个脚手架工具,以加深对脚手架原理的理解。废话不多说,先上代码地址。
初始化
首先来实现脚手架工具最核心的功能------代码打包功能。
创建工程文件夹,在文件夹中生成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",
这里还需要处理三个问题:
- rollup本身无法处理
vue等外部模块的引入,需要安装并引入@rollup/plugin-node-resolve插件。 vue源码中多处用到环境变量,执行process.env.NODE_ENV操作时会报错并提示process为undefined,安装并引入@rollup/plugin-replace插件可以在编译代码时将process.env.NODE_ENV等环境变量替换为具体值。- 环境变量
__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-console为warn后,假如代码中出现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文件的自动分块功能等等。后面的工作就是根据需要对其进行拓展。