“This is the first day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021”

vision

It is hoped that this series of articles will provide readers with an idea for storage/incremental project access to Vite, thereby reducing 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

preface

In the last installment, I outlined the following steps for connecting webPack projects to Vite:

  • HTML Template processing
  • EntryJs processing
  • Webpack configuration is migrated to vite configuration
  • Vite plugin to replace the WebPack plugin

The processing of these contents can be achieved through the Vite plug-in

Webpack – vite – serve is introduced

During this period of time, we are constantly improving the functions of this library. Here is a brief introduction to its use, and then explain the implementation principle of some plug-ins

Goal: To provide webPack projects with one-click access to Vite

Install dependencies

npm install webpack-vite-serve -D
# or
yarn add webpack-vite-serve -D
# or
pnpm add webpack-vite-serve -D
Copy the code

Add startup instructions

# devServer
wvs start [options]
# build
wvs build [options]
Copy the code

Optional parameters

  • -f,--framework <type>: Specifies the service framework (VUE, React) to be used. Basic plug-ins related to the service framework are automatically introduced
  • -s,--spa: The directory is processed according to the single-page application directory structuresrc/${entryJs}
  • -m,--mpa: The directory is processed according to the multi-page application directory structuresrc/pages/${entryName}/${entryJs}
  • -d,--debug [feat]: Displays debugging information
  • -w,--wp2viteUse:wp2viteAutomatically convert Webpack files

Other instructions

The project follows the normal single page/multi-page application project directory structure

Vite configuration can be extended through the official vite. Config.[tj]s configuration file

The effect

Online demo address: Stackblitz has been created

If the demo cannot be accessed due to network problems, clone the repository to experience demo

MPA support

Dev- Page template

The first is page template processing in the devServer environment

Obtain entryName based on the request path

  • use/Split the request path to getpaths
  • Go through and find the first onesrc/pages/${path}The existence ofpath, path isentryName
function getEntryName(reqUrl:string, cfg? :any) {
  const { pathname } = new URL(reqUrl, 'http://localhost');
  const paths = pathname.split('/').filter((v) = >!!!!! v);const entryName = paths.find((p) = > existsSync(path.join(getCWD(), 'src/pages', p)));
  if(! entryName) {console.log(pathname, 'not match any entry');
  }
  return entryName || ' ';
}
Copy the code

Find the template files in the following order

  • src/pages/${entryName}/${entryName}.html
  • src/pages/${entryName}/index.html
  • public/${entryName}.html
  • public/index.html
function loadHtmlContent(reqPath:string) {
  // The bottom page
  const pages = [path.resolve(__dirname, '.. /.. /public/index.html')];

  // Single page/multiple pages default public/index.html
  pages.unshift(resolved('public/index.html'));

  // Multi-page applications can make further judgments based on the requested path
  if (isMPA()) {
    const entryName = getEntryName(reqPath);
    if (entryName) {
      pages.unshift(resolved(`public/${entryName}.html`));
      pages.unshift(resolved(`src/pages/${entryName}/index.html`));
      pages.unshift(resolved(`src/pages/${entryName}/${entryName}.html`)); }}const page = pages.find((v) = > existsSync(v));
  return readFileSync(page, { encoding: 'utf-8' });
}
Copy the code

Dev-entryJs

Multi-page application entryJs is read by the agreement SRC/pages / ${entryName} / ${main | index} file

function getPageEntry(reqUrl) {
  if (isMPA()) {
    const entryName = getEntryName(reqUrl);
    return!!!!! entryName && getEntryFullPath(`src/pages/${entryName}`);
  }
  / / the default SPA
  const SPABase = 'src';
  return getEntryFullPath(SPABase);
}
Copy the code

Build

The entry point to a Vite build is an HTML template, which can be set via the build.rollup.input property

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        index: 'src/pages/index/index.html'.second: 'src/pages/second/second.html',},},},});Copy the code

As configured above, the HTML directory in the build product would look like this

* dist
  * src/pages/index/index.html
  * src/pages/second/second.html
  * assets
Copy the code

Not quite in accordance with the usual convention, the general format is as follows

* dist
  * index.html
  * second.html
  * assets
Copy the code

The plugin handles the build entry file and adjusts the location of the artifacts after the build

The plug-in structure

export default function BuildPlugin() :PluginOption {
  let userConfig:ResolvedConfig = null;
  return {
    name: 'wvs-build'.// Only in the construction phase
    apply: 'build'.// Get the final configuration
    configResolved(cfg) {
      userConfig = cfg;
    },
    // Plug-in configuration processing
    config(){},resolveId(id){},load(id){},// After the build is complete
    closeBundle(){}}; }Copy the code

The configResolved hook gets the final configuration, which is made available to other hooks

Gain entry

First get all the entries under SRC/Pages

const entry = [];
if(isMPA()) { entry.push(... getMpaEntry()); }else {
  // Single page application
  entry.push({
    entryName: 'index'.entryHtml: 'public/index.html'.entryJs: getEntryFullPath('src')}); }Copy the code

Entry is defined as

interface Entry{
  entryHtml:string
  entryName:string
  entryJs:string
}
Copy the code

The acquisition logic is as follows

  • So let’s get all of themEntryName
  • In traversal to obtain the corresponding of each entryentryJswithentryHtml
export function getMpaEntry(baseDir = 'src/pages') {
  const entryNameList = readdirSync(resolved(baseDir), { withFileTypes: true })
    .filter((v) = > v.isDirectory())
    .map((v) = > v.name);

  return entryNameList
    .map((entryName) = > ({ entryName, entryHtml: ' '.entryJs: getEntryFullPath(path.join(baseDir, entryName)) }))
    .filter((v) = >!!!!! v.entryJs) .map((v) = > {
      const { entryName } = v;
      const entryHtml = [
        resolved(`src/pages/${entryName}/${entryName}.html`),
        resolved(`src/pages/${entryName}/index.html`),
        resolved(`public/${entryName}.html`),
        resolved('public/index.html'),
        path.resolve(__dirname, '.. /.. /public/index.html'),
      ].find((html) = > existsSync(html));
      return {
        ...v,
        entryHtml,
      };
    });
}
Copy the code

Build build configuration

Build. Rollup. Input is generated from the resulting entry

  • For eachentryHtmlThe content is then usedmapMake temporary storage
  • Build the entry template pathhtmlEntryPathtakeentryJsThe directory to addindex.html

The htmlEntryPath path doesn’t actually have any files

So you need to further process the content stored in htmlContentMap through other hooks

const htmlContentMap = new Map(a);// omit other extraneous code
{
  config() {
    const input = entry.reduce((pre, v) = > {
      const { entryName, entryHtml, entryJs } = v;
      const html = getEntryHtml(resolved(entryHtml), path.join('/', entryJs));
      const htmlEntryPath = resolved(path.parse(entryJs).dir, tempHtmlName);
      // Store content
      htmlContentMap.set(htmlEntryPath, html);
      pre[entryName] = htmlEntryPath;
      return pre;
    }, {});
    return {
      build: {
        rollupOptions: { input, }, }, }; }}Copy the code

Build entry content generation

The resolveId and the Load hook handle the import file

  • Among themidIs the path of the resource request
  • And then directly fromhtmlContentMapRemove the contents of the template
{
  load(id) {
    if (id.endsWith('.html')) {
      return htmlContentMap.get(id);
    }
    return null;
  },
  resolveId(id) {
    if (id.endsWith('.html')) {
      return id;
    }
    return null; }},Copy the code

Product catalog adjustment

Use the closeBundle hook to adjust the file after the build is complete and before the service is closed

  • traverseentrywilldist/src/pages/entryName/index.htmlMove to thedistUnder the
  • removedist/srcThe content of the
closeBundle() {
  const { outDir } = userConfig.build;
  // Directory adjustment
  entry.forEach((e) = > {
    const { entryName, entryJs } = e;
    const outputHtmlPath = resolved(outDir, path.parse(entryJs).dir, tempHtmlName);
    writeFileSync(resolved(outDir, `${entryName}.html`), readFileSync(outputHtmlPath));
  });
  // Remove temporary resources
  rmdirSync(resolved(outDir, 'src'), { recursive: true });
}
Copy the code

Webpack configures the transformation

There is currently one CLI tool in the community: Wp2vite supports this functionality, so I’m not going to build another from 0-1

Because it is a CLI tool, there is no direct method to call to obtain the configuration before and after the transformation, so the experience of using the access plug-in is not very good. We plan to improve the tool by PR in the future

The plug-in to access Wp2Vite is implemented as follows

import wp2vite from 'wp2vite';
// omit unimportant imports
export default function wp2vitePlugin() :PluginOption {
  return {
    name: 'wvs-wp2vite'.enforce: 'pre'.async config(_, env) {
      const cfgFile = resolved('vite.config.js');
      const tplFile = resolved('index.html');
      const contentMap = new Map([[cfgFile, ' '], [tplFile, ' ']]);
      const files = [cfgFile, tplFile];

      console.time('wp2vite');
      // Check whether vite. Config. js and index.html exist
      Avoid wp2vite overwriting
      files.forEach((f) = > {
        if (existsSync(f)) {
          contentMap.set(f, readFileSync(f, { encoding: 'utf-8'})); }});// Convert the configuration file vite.config.js
      await wp2vite.start(getCWD(), {
        force: false.// Enable the debug function
        debug:!!!!! process.env.DEBUG, });// TODO:To optimize PR
      // Conversion time calculation
      console.timeEnd('wp2vite');

      // Get the configuration converted from wp2vite
      const cfg = await getUserConfig(env, 'js');

      contentMap.forEach((v, k) = > {
        if (v) {
          // If the content is modified, restore the content
          writeFileSync(k, v);
        } else {
          // Remove the created fileunlinkSync(k); }});if (cfg.config) {
        const { config } = cfg || {};
        // Leave the required configuration
        return {
          resolve: config? .resolve,server: config? .server,css: config? .css, }; }return null; }}; }Copy the code

Wp2vite exposes a start method call

After the call, two new files (viet.config. js, index.html) are generated according to the webpack configuration of the project, and package.json is modified to add directives and dependencies

Therefore, if these files exist in the project, they need to be stored before generation

The implementation of getUserConfig to obtain user configuration is as follows

import { loadConfigFromFile, ConfigEnv } from 'vite';

export function getUserConfig(configEnv:ConfigEnv, suffix = ' ') {
  const configName = 'vite.config';
  const _suffix = ['ts'.'js'.'mjs'.'cjs'];
  if (suffix) {
    _suffix.unshift(suffix);
  }
  const configFile = _suffix.map((s) = > `${configName}.${s}`).find((s) = > existsSync(s));
  return loadConfigFromFile(configEnv, configFile);
}
Copy the code

Vite provides the loadConfigFromFile method, which only needs to do a simple layer of encapsulation in this method can be used directly. The method uses esbuild to automatically convert TS and ES syntax

conclusion

So far, the capacity built has been basically sufficient for the development of conventional projects

Users can directly add vite configuration files in the project for their own expansion

Subsequent planning

  1. At presentwp2viteIn the configuration transformation of this piece, is not enough to meet the requirements of use, ready to improve the PR
  2. Separate the internal capabilities into separate Vite plug-ins and provide them externally