1.0.2 • Published 2 years ago

@husandao/multi-cli v1.0.2

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years ago

从零单排:基于vite+vue3实现多入口打包插件

前言

本文为从零单排系列的第三篇,通过本文我们能够基于vite实现一个多页面(多入口)的打包插件,推荐先阅读本系列文章的先导片从零单排:前端进阶之路、第一篇文章从零单排:使用pnpm创建monorepo和第二篇文章从零单排:基于vite+vue3搭建一个多入口的移动端项目(支持单入口、多入口和全部入口的打包)以获得更好的体验

这个项目我们在之前创建好的build项目中实现

初始化

初始化package.json

在build文件夹下,执行pnpm init命令

pnpm init

初始化ts

本项目我们使用typescript进行开发,先全局安装ts,在build文件夹下再执行tsc --init命令初始化

tsc --init

修改package.json

  • type - 声明遵循的模块化规范,我们这里设置为module,即采用ESModule规范
  • bin - 将可执行文件加载到全局环境中,那么我们的包发布后,项目安装这个包就会自动链接到项目的node_module/.bin目录中,就可以使用别名(我们这里设置的别名是adv-build)执行相应的命令
  • script - 脚本,我们这里声明两个脚本,dev实时编译ts,build清除编译后的文件重新编译,清除我们使用的rimraf,所以需要提前安装
pnpm add rimraf
// package.json
{
  "name": "@advance/build",
  "version": "1.0.0",
  "private": false,
  "description": "",
  "type": "module",
  "main": "./bin.js",
  "bin": {
    "adv-build": "./bin.js"
  },
  "scripts": {
    "dev": "tsc --watch",
    "build": "rimraf ./lib && tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/node": "^18.7.18",
    "@vitejs/plugin-vue": "^3.1.0",
    "commander": "^9.4.0",
    "del": "^7.0.0",
    "rimraf": "^3.0.2",
    "vite": "^3.1.0",
    "vite-plugin-html": "^3.2.0"
  }
}

修改tsconfig.json

  • include - 指定哪些文件需要编译,我们设置为编译src文件夹下的所有ts文件
  • outDir - 编译后的文件存放位置
  • module - 指定要使用模块化的规范
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "ESNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./lib",
    "declaration": true,
    "moduleResolution": "Node"
  },
  "include": [
    "src/**/*"
  ]
}

实现

在build文件夹下新建src文件夹和bin.js入口文件,不难发现我们上面的package.json中的bin设置的执行文件就是bin.js,相当于我们执行adv-build时就会去执行bin.js

我们主要的代码都在src下面编写,在src下新建commands文件夹和common文件夹,commands文件夹用于存放命令相关代码,common文件夹存放公共代码。在commands文件夹下新加dev.ts文件和build.ts文件,对应的dev命令和build命令相关的处理代码。在common文件夹下新建constant.ts文件用于存放公共变量,新建utils.ts用于存放工具方法,目录如下

.
├── src # 主目录
│   ├── common # 公共代码逻辑
|   |   |── constant.ts # 公共变量
|   |   |── utils.ts # 公共方法
│   ├── commands # 命令相关代码
|   |   |── dev.ts # dev命令相关
|   |   |── build.ts # build命令相关

bin.js编码

代码的第一行 #!/usr/bin/env node 是告诉系统使用node执行此文件,入口文件我们没有做过多的操作只是引入了编译后的cli.js

#!/usr/bin/env node
import './lib/cli.js'

cli.ts编码

这个文件相当于指挥官,根据不同的命令调用对应的处理方法,我们使用commander这个库来帮助我们去解析命令和参数,commander.js是node.js命令行界面的完整解决方案,大家可以先自行去阅读一下官方文档,这里主要说下我们会使用到的:

  • version - 设置版本号
  • command - 添加命令名称
  • description - 对该命令的描述
  • option - 定义选项
  • action - 命令的回调函数
// src/cli.ts

import {Command} from 'commander'

const program = new Command()
program.version('@advance/build 1.0.0')

program
  .command('dev') // 注册dev命令
  .description('Run dev server') // 对dev命令的描述
  .option('--open <open>', 'auto open page of url', false) // 定义open选项,默认是false,用于指定启动dev-server时自动打开哪个页面
  .action(async ({open}) => {   // 当用户输入dev命令时的回调,它会把上面定义的option注入到这个回调函数中
    const {dev} = await import('./commands/dev.js') // 我们在这里异步引入dev函数,执行
    dev(open)
  });

program
  .command('build') // 注册build命令
  .description('Compile pages in production mode') // 对build命令的描述
  .option('--all', 'build all page', false) // 定义all选项,默认是false,用于指定是否打包全部的页面
  .option('--pages <pages...>', 'build page list') // 定义pages选项,它是可变长参数,最终会将我们输入的参数解析成数组,用于指定需要打包的页面
  .action(async (options) => { // 当用户输入build命令时的回调,它会把上面定义的option注入到这个回调函数中
    const {build} = await import('./commands/build.js') // 我们在这里异步引入build函数,执行
    build(options)
  })

program.parse()

utils.ts编码

这个文件存放公共方法

import fs from 'fs'

// 判断文件是否存在
const isExist = (path: string) => {
  return fs.existsSync(path)
}

export {
  isExist
}

constant.ts编码

这个文件存放变量

// src/common/constant.ts

import {resolve} from 'path'
import {isExist} from './utils.js'
// 当前Node.js进程执行时的文件夹地址
const CWD = process.cwd()
// 存放多页面的路径
const PAGES_PATH = resolve(CWD, 'src/pages')
// 需要注入的js代码,解决在ios12以下机型在dev server白屏问题
const INJECTSCRIPT = `
                      <script >if (globalThis === undefined) { var globalThis = window; }</script>
                      `

// 在根目录下查找是否有vite的配置文件
const configFileOfTs = resolve(CWD, 'vite.config.ts')
const configFileOfJs = resolve(CWD, 'vite.config.js')
let configFile: string | false = false
if(isExist(configFileOfTs)) {
  configFile= configFileOfTs
} else if(isExist(configFileOfJs)) {
  configFile= configFileOfJs
}

export {
  CWD,
  PAGES_PATH,
  INJECTSCRIPT,
  configFile // vite配置文件的路径
}

dev.ts编码

这个文件通过调用vite的createServer API启动dev-server,dev函数接受一个open参数,用于指定自动打开的页面,同时我们会去寻找业务项目(我们的是第二篇文章中的H5项目)的vite的配置文件,并传递给createServer函数,这样就可以自己根据业务的需要设置相应的vite配置了

import {createServer} from 'vite'
import vue from '@vitejs/plugin-vue'
import {PAGES_PATH, INJECTSCRIPT, configFile} from '../common/constant.js'
import {resolve} from 'path'
import fs from 'fs'
import {createHtmlPlugin} from 'vite-plugin-html'
import { isExist } from '../common/utils.js'

export async function dev(open: string | false) {
  try {
    let openPath: string | false = false
    if(open && isExist(resolve(PAGES_PATH, `./${open}/index.html`))) {
      openPath = `/${open}/index.html`
    }
    const server = await createServer({
      configFile,
      root: PAGES_PATH,
      plugins: [
        vue(),
        createHtmlPlugin({
          pages: fs.readdirSync(PAGES_PATH).map(page => {
            return {
              entry: `/${page}/main.ts`,
              filename: `${page}.html`,
              template: `src/pages/${page}/index.html`,
              injectOptions: {
                data: {
                  injectScript: INJECTSCRIPT
                }
              }
            }
          })
        })
      ],
      server: {
        open: openPath
      }
    })
    await server.listen();
    server.printUrls();
  } catch(err) {
    console.log(err);
  }
}

build.ts编码

这个文件通过调用vite的build方法进行打包,为防止和我们的build函数冲突,给vite的build方法设置别名为viteBuild,我们的build函数根据参数来进行全部页面打包或部分页面打包。跟上面的dev.ts一样,我们会去找项目的vite配置文件并传递给vite提供的build函数中,利用递归实现按顺序打包

import { build as viteBuild } from 'vite'
import path from 'path'
import { CWD, INJECTSCRIPT, PAGES_PATH, configFile } from '../common/constant.js'
import fs from 'fs'
import vue from '@vitejs/plugin-vue'
import { createHtmlPlugin } from 'vite-plugin-html'
import { deleteSync } from 'del'
import { isExist } from '../common/utils.js'

const compile = (page: string) => {
  return new Promise(async (resolve, reject) => {
    try {
      // 不是文件夹的直接跳过
      if (!fs.statSync(path.resolve(PAGES_PATH, `./${page}`)).isDirectory()) {
        reject()
        return
      }
      console.log(`开始打包${page}`);
      const entry = path.resolve(PAGES_PATH, `./${page}/index.html`)
      // 判断入口文件是否存在
      if (!isExist(entry)) {
        console.log(`${page}的入口文件不存在`);
        reject()
        return
      }
      const outDir = path.resolve(CWD, `./dist/${page}`)
      // 删除旧的打包资源
      deleteSync(outDir)
      await viteBuild({
        configFile,
        root: path.resolve(PAGES_PATH, page),
        base: './',
        plugins: [
          vue(),
          createHtmlPlugin({
            entry: '/main.ts',
            template: 'index.html',
            inject: {
              data: {
                injectScript: INJECTSCRIPT
              }
            }
          })
        ],
        build: {
          outDir
        }
      })
      console.log(`${page}打包成功`);
      resolve(`${page}打包成功`)
    } catch (err) {
      console.log('err====>', err);

      reject(err)
    }
  })
}

export async function build({ all, pages }: { all?: boolean, pages?: string[] }) {
  const buildPages = all ? fs.readdirSync(PAGES_PATH) : pages
  if (!Array.isArray(buildPages)) {
    console.log('请输入要打包的页面');
    return
  }
  // 递归实现按顺序打包
  const runner = async () => {
    if (!buildPages || !buildPages.length) return
    const page = buildPages.shift() as string
    try {
      await compile(page)
    } catch (error) {
    }
    runner()
  }
  runner()
}

注意

通过上面的代码,我们会发现,在引入本地自己写的ts文件时,我们加上了.js的后缀,你可能会疑惑为什么这样做,因为tsc在将ts文件编译时,不会给引用的文件加上后缀,而我们又在package.json设置了type为module,这样导致,node执行编译后js文件会提示找不到对应的引入文件会报错,所以我们在ts编码时需要引入自己写的ts文件就加上.js为后缀

本文章的代码是将上一篇文章中的打包部分代码给抽离出来,使用ts编码,增加了一些异常错误的处理和优化部分代码,但是核心逻辑并没有改变,对优化部分代码都做了解释,对没有修改的部分未解释的很清楚,如果你有看着模糊的地方可以阅读上一篇文章的实现按需打包章节里面有详细的解释

测试

准备工作

在build文件夹下运行dev命令编译代码

pnpm dev

修改multi-page-app项目的package.json的scripts

"scripts": {
  "dev": "adv-build dev",
  "build": "adv-build build",
},

测试本地服务

在multi-page-app文件夹下运行dev命令

  • 无open参数
pnpm dev

可以通过http://127.0.0.1:5173/page-1/index.html#/访问到页面

  • 有open参数
pnpm dev --open page-1

自动在浏览器打开page-1的页面

测试打包

在multi-page-app文件夹下运行build命令

  • 无参数
pnpm build

提示输入打包页面,退出打包

  • 有all参数
pnpm build --all

打包pages下面的所有子项目,并输出到dist目录

  • 有pages参数
pnpm build --page page-1

打包page-1,并输出到dist目录

  • 有all参数和pages参数
pnpm build --all --page page-1

打包pages下面的所有子项目,并输出到dist目录

发布

打包项目

在build文件夹下执行build命令

pnpm build

注册npm账号

npm官网注册账号

登录

npm login

发布

注意源需要设置成官方的,如果是淘宝源需要切换到官方源,否则会发布失败,一般包名不能一样(发布前先去官网搜索这个包名是否存在),我这里在package.json修改name为adv-build

npm publish

总结

我们的目标是:搞事,搞事,还是TM的搞事

本系列的代码都已上传到github,如有需要可自行下载

如果你觉得文章不错,不妨:

  • 点赞-让更多人也能够看到这篇文章
  • 关注-防止找不到我了。。。

文档

从零单排:前端进阶之路系列全部文章 1. 从零单排:使用pnpm创建monorepo 2. 从零单排:基于vite+vue3搭建一个多入口的移动端项目(支持单入口、多入口和全部入口的打包) 3. 从零单排:基于vite+vue3实现多入口打包插件 4. 从零单排:搭建一个属于自己的脚手架---敬请期待

打个广告