Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

vision

It is hoped that this article will provide readers with an idea for storage/incremental project access to Vite, which will serve as a catalyst to reduce the cost of building capacity in this area

In the process of elaboration, we will gradually improve the tool webpack-viet-serve

Readers can fork the tool repository and customize the secondary development for individual/corporate project scenarios

background

Webpack is everywhere in today’s business development, and is the building tool used by most business projects.

As time goes on, existing projects get bigger and bigger, and development starts/builds take longer and longer. Optimization tools for Webpack are increasingly limited.

As a result, some scenarios have tools written in other languages to help build/develop improvements. Such as SWC (Rust), ESbuild (Go)

Of course, the above tool is not a complete build tool, can not replace webpack directly used, only through plugin, webpack work to improve the efficiency

Another popular solution is Bundleless, which makes use of the browser’s native ES Module support feature to let the browser take over the “packaging” work. The tool is only responsible for the corresponding conversion of resources requested by the browser, thus greatly reducing the startup time of the service and improving the development experience and happiness

Two of the best known products are Snowpack and Vite

The star of this article is Vite: the next generation front-end development and build tool

Since Vite is still under construction, it will take some time to completely replace WebPack. In order to ensure the stability of online projects, It is recommended that Vite be used as an optional capability during development.

# webpack devServer
npm run dev

# Vite devServer
npm run vite
Copy the code

The target

Provide the simplest Vite access solution for webPack project development environment

With minimal changes, in-app projects can enjoy the development benefits of Vite

plan

  1. Make a CLI tool that encapsulates Vite’s ability to start projects
  2. The webPack configuration is automatically converted to the Vite configuration by converging all the Vite related configurations within the plug-in
  3. Some optional parameters are provided to manually specify the location of the configuration file

Effect of the demo

Vue SPA

React SPA

In the simplest Demo projects, Vite’s startup /HMR speed is significantly faster than WebPack’s

Demos of other common project types will also gradually be refined into the source repository

implementation

1. Initialize the project

Complete engineering structure moved to warehouse

Register a start method

src/bin.ts

#! /usr/bin/env node
import { Command } from 'commander';
import { startCommand } from './command';
program.command('start')
  .alias('s')
  .action(startCommand);

program.parse(process.argv);
Copy the code
export default function startCommand() {
  console.log('hello vite');
}
Copy the code

Add directives to package.json

  • Among themwvsIs a custom instruction
  • npm run dev:typescriptDepending on the instructions provided, listen for file changes and automatically convert themjsfile
{
  "bin": {
    "wvs": "./dist/bin.js"
  },
  "scripts": {
    "dev": "tsc -w -p ."."build": "rimraf dist && tsc -p ."}},Copy the code

The project root executes NPM link, registering instructions

npm link
Copy the code

test

wvs start
Copy the code

Then we used vuE-CLI and Create React App to Create two WEBpack SPA applications respectively for the following experiment

vue create vue-spa
Copy the code

npx create-react-app react-spa
Copy the code

2. The Vite is started

Vite startup is relatively simple, only need to execute the Vite command s

Start Vite using spawn in our CLI tool

  • Among themcwdUse to specify the working directory of the child process
  • Stdio: Standard INPUT/output configuration for child processes
import { spawn } from 'child_process';

export default function startCommand() {
  const viteService = spawn('vite'['--host'.'0.0.0.0'] and {cwd: process.cwd(),
    stdio: 'inherit'}); viteService.on('close'.(code) = > {
    process.exit(code);
  });
}
Copy the code

Here for easy debugging, let’s install the global Vite

npm i -g vite
Copy the code

Add a

Hello Vite

to the startup template public/index.html

Run WVS Start in the demo project

Open corresponding address

# vue
http://localhost:3000/
# react
http://localhost:3001/
Copy the code

You get the following result, indicating that the page could not be found (unsurprisingly)

According to the documentation,Vite by default looks for index.html as the entry file for the project

This brings us to our first problem, which is the possibility of multiple template files in a multi-page application

How to dynamically specify the entry to this X. HTML based on the access route?

Before solving the problem, let’s simply refine the startup command by specifying a vite.config.js configuration file for it

With vite –help, you can see that the configuration file location is specified with the –config parameter

export default function startCommand() {
  const configPath = require.resolve('. /.. /config/vite.js');
  const viteService = spawn('vite'['--host'.'0.0.0.0'.'--config', configPath], {
    cwd: process.cwd(),
    stdio: 'inherit'}); }Copy the code

This is the absolute path to the configuration file

config/vite.ts

import { defineConfig } from 'vite';

module.exports = defineConfig({
  plugins: [].optimizeDeps: {},});Copy the code

3. HTML template processing

The ability to extend Vite is to customize various plug-ins, according to the plug-in documentation

Write a simple plugin that uses the configServer hook to read resource requests made by the browser

import type { PluginOption } from 'vite';

export default function HtmlTemplatePlugin() :PluginOption {
  return {
    name: 'wvs-html-tpl'.apply: 'serve'.configureServer(server) {
      const { middlewares: app } = server;
      app.use(async (req, res, next) => {
        const { url } = req;
        console.log(url); next(); }); }}; }Copy the code

Introduced in the configuration file above

import { htmlTemplatePlugin } from '.. /plugins/index';
module.exports = defineConfig({
  plugins: [
    htmlTemplatePlugin(),
  ]
});
Copy the code

Start service observation again

  • accesshttp://localhost:3000, output in the terminal/
  • accesshttp://localhost:3000/path1/path2, output in the terminal/path1/path2
  • accesshttp://localhost:3000/path1/path2?param1=123, output in the terminal/path1/path2? param1=123

In devTools panel content as you can see, the first in the Accept field with a resource request head of text/HTML, such as application/XHTML + XML content, we will request in this field show that the HTML document

Modify the code that handles resource requests again

import { readFileSync } from 'fs';
import path from 'path';
import { URL } from 'url';

function loadHtmlContent(reqPath) {
  // Single page defaults to public/index.html
  const tplPath = 'public/index.html';
  Path: reqPath
  return readFileSync(path.resolve(process.cwd(), tplPath));
}

// Omit the previous code
app.use(async (req, res, next) => {
  const { pathname } = new URL(req.url, `http://${req.headers.host}`);
  const htmlAccepts = ['text/html'.'application/xhtml+xml'];
  constisHtml = !! htmlAccepts.find((a) = > req.headers.accept.includes(a));
  if (isHtml) {
    const html = loadHtmlContent(pathname);
    res.end(html);
    return;
  }
  next();
});
Copy the code

Start the service again in Demo, and you’ll see Hello Vite correctly

An error will be found in the terminal

UnhandledPromiseRejectionWarning: URIError: URI malformed
Copy the code

Open the template and you’ll see that there is something else in there that contains variables that are handled by the HTmL-webpack-plugin in webpack

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
  <%= htmlWebpackPlugin.options.title %>
</title>
Copy the code

Here’s a simple method to do some simple things with the template (this method only deals with the current situation)

/** * Initializes the template content (replace <%= varName %> something) */
function initTpl(tplStr:string, data = {}, ops? :{ backup? :stringmatches? :RegExp[]}) {
  const { backup = ' ', matches = [] } = ops || {};
  // match %Name% <%Name%>
  return [/ 
       (. *)? /g].concat(matches).reduce((tpl, r) = > tpl.replace(r, (_, $1) = > {
    const keys = $1.trim().split('. ');
    const v = keys.reduce((pre, k) = > (pre instanceof Object ? pre[k] : pre), data);
    return (v === null || v === undefined)? backup : v; }), tplStr); }Copy the code

If the template still has complex EJS syntax, you can use the EJS library for further processing

import ejs from 'ejs';

/** * ejs render */
function transformEjsTpl(html:string, data = {}) {
  return ejs.render(html, data);
}
Copy the code

Of course, if there are other cases that are not considered, the template can be further processed according to the specific situation

Now integrate the methods you wrote above into your plug-in

export default function HtmlTemplatePlugin() :PluginOption {
  return {
    configureServer(server) {
      const { middlewares: app } = server;
      app.use(async (req, res, next) => {
        // omit the code
        if (isHtml) {
          const originHtml = loadHtmlContent(pathname);
          // Call the transformIndexHtml hook in the plug-in for further processing of the template
          const html = await server.transformIndexHtml(req.url, originHtml, req.originalUrl);
          res.end(html);
          return;
        }
        next();
      });
    },
    transformIndexHtml(html) {
      // Data can be passed in some of the variables contained in the template
      // You can get the WebPack configuration here to do the automatic conversion
      return initTpl(html, {
        PUBLIC_URL: '. '.BASE_URL: '/'.htmlWebpackPlugin: {
          options: {
            title: 'App',}}}); }}; }Copy the code

To this run again in the demo, the page ran, the terminal also no error, the page template to this is processed

Having the initial template means that we have provided entry to the page for Vite, but there are no js/ TS dependencies that are processed yet

Insert entry into the template

4. Specify an entry

Entrance to the filename (entryName) is usually (main | index), js | | ts JSX | TSX

  • EntryBase in a single page application (SPA) is usually:src
  • EntryBase in multi-page applications (MPA) is usually:src/pages/${pageName}

export default function pageEntryPlugin() :PluginOption {
  return {
    name: 'wvs-page-entry'.apply: 'serve'.transformIndexHtml(html, ctx) {
      return html.replace('</body>'.`<script type="module" src="${getPageEntry(ctx.originalUrl)}"></script>
        </body>
        `); }}; }Copy the code

Take SPA as an example

function getPageEntry(reqUrl) {
  // SPA
  const SPABase = 'src';
  return getEntryFullPath(SPABase);
}
Copy the code

GetEntryFullPath is implemented as follows

  • Check whether the directory exists
  • Read directories and traverse files using re/(index|main)\.[jt]sx? $/Determines whether a file is a target file
const resolved = (. p) = >path.resolve(getCWD(), ... p);const getEntryFullPath = (dirPath) = > {
  if(! existsSync(resolved(dirPath))) {return false;
  }
  // main|index.js|ts|jsx|tsx
  const entryName = /(index|main)\.[jt]sx? $/;
  const entryNames = readdirSync(resolved(dirPath), { withFileTypes: true })
    .filter((v) = > {
      entryName.lastIndex = 0;
      return v.isFile() && entryName.test(v.name);
    });
  return entryNames.length > 0 ? path.join(dirPath, entryNames[0].name) : false;
};
Copy the code

Add this plug-in to the configuration

import { pageEntryPlugin } from '.. /plugins/index';
module.exports = defineConfig({
  plugins: [
    pageEntryPlugin(),
  ]
});
Copy the code

Launched demo to see the effect, threw a bunch of errors

wvs start
Copy the code

The following is the framework-specific processing

React

  1. React: the content contains invalid JS syntax

React: JSX: JSX: JSX: JSX: JSX: JSX

  1. Uncaught ReferenceError: React is not defined

Install react at the top of the React component, or install @vitejs/plugin-react

import React from 'react';
Copy the code
  1. HMR support

Introduce the @vitejs/plugin-react plugin

import react from '@vitejs/plugin-react'

module.exports = defineConfig({
  plugins: [
    react(),
  ]
});
Copy the code

Vue

Need to add plug-in to handle.vue files

Introduce the @vitejs/plugin-vue plugin

import vue from '@vitejs/plugin-vue'

module.exports = defineConfig({
  plugins: [
    vue(),
  ]
});
Copy the code

While @vitejs/plugin-vue needs vue (>=3.2.13)

Because the previous use is NPM link to create soft connection for debugging, configuration files will be in the development directory to find Vue dependencies, not in the command run directory to find, will continue to throw the above problems

Here we install our dependencies locally in the Demo project and add the relevant directives in package.json

yarn add file:webpack-vite-service-workspace-path
Copy the code
{
  "scripts": {
    "vite": "wvs start -f vue"}},Copy the code

There are no React dependencies in the Vue project, so the @vitejs/plugin-react plugin cannot be introduced in the Vue project

You can add parameters related to the framework in the command entry to judge the processing, and only introduce plug-ins corresponding to the framework

// src/bin.ts
program.command('start')
  .option('-f, --framework <type>'.'set project type [vue/react]')
  .action(startCommand);

// src/command/start.ts
export default function startCommand(options:{[key:string] :string}) {
  const { framework = ' ' } = options;
  process.env.framework = framework.toUpperCase();
}

// src/config/vite.ts
import react from '@vitejs/plugin-react';
import vue from '@vitejs/plugin-vue';

const extraPlugins: any[] = [
  process.env.framework === 'REACT' ? [react()] : [],
  process.env.framework === 'VUE' ? [vue()] : [],
];
module.exports = defineConfig({
  plugins: [
    htmlTemplatePlugin(),
    pageEntryPlugin(),
    ...extraPlugins,
  ],
});
Copy the code

That completes the two most important steps

5. Other engineering skills

There are already a number of plug-ins and solutions available in the community for common webPack capabilities, so this is a brief introduction

Of course, some of these plugins may not be able to handle the situation, or expect developers, dare to experiment, and then submit PR/issues to the plugin author

  • Sass/Less: Installed in a dependencySass/LessCan be
  • Component libraries are imported on demand: viet-plugin-style-import
  • Process. Env: vite – plugin – env – compatible
  • Xx /xx undefined: usedtransformIndexHtmlHook development plugins that introduce this method ahead of time in the templatepolyfillOr the bottom line
  • .

conclusion

Enterprises: Most of them have their own research and development framework, and only need to add a CLI command for Vite startup to the research and development framework, so as to minimize the impact on the access party and the use cost

Personal: if you want to change the original code, you can follow the above process and develop your new project directly using the official Vite template

Bottom line: It’s nice to use Vite in development

Due to the limited space and time, part of the article only introduces the implementation ideas, and did not paste the complete code, the complete code can be viewed in the source repository, can also be forked directly for secondary development

The transformation of WebPack to Vite configuration will be covered in the next installment