preface

  • viteIt has been more than half a year since it was launched and the momentum is very strong. Github activity is very high
  • from2.xStart adding precompilation, can be very compatibleCommonJsMode, precompiled with fairly fast cold start speed
  • As the system becomes more and more maintained, the startup speed becomes slower @vue/cliUsed by the project created (vue2.x)[email protected]Version, this problem is getting worse and it’s time to integrate “old age”vue2In the old project

πŸ“’ Note: It is only recommended to run vite in development mode in the production environment as before; Webpack, after all, is more sophisticated in packaging

Project background

  • The transformation project is a very important, iterative and frequent system of the company; It now has over 100 pages
  • Engineering formwork by@vue/cliTo create thevue2.xVersion for internal usewebpack4.xbuild
  • As projects get bigger (50 pages per year or so), the need for a cold start becomes more pressing

Technical analysis

  • While Vite is growing fast, NPM plugins for Vite are also catching on fast; But there was always something out of reach in our old projects

  • Here I mainly with my actual transformation of the problems encountered to do the technical summary, if you have encountered other problems in the process of use, this article to solve the problem ideas also have a certain reference value

  • Here are some of the problems I encountered

    1. You need topublic/index.htmlGenerate a soft link to the root directory —-viteStart the entrance
    2. conversion@import '~normalize.css/normalize.cssIn the~Alias –viteAn error
    3. conversionimport('@/pages/xxxx') —- viteWarning, error reporting
    4. conversionrequire δΈΊ importForm –viteAn error

vite-plugins

  • Our transformation of vite project is based on plug-ins, which can be understood as writing a lot of plug-ins to solve the corresponding problems
  • You might want to know how to write a Vite plugin first
  • This time a couple ofconversionPlugins are used in pluginstransformHooks, relatively simple and easy to understand

public/index.html -> index.html

  • Vite focuses on front-end development, so the entry file is HTML; Webpack uses JS as the entry point

  • Since we still use Webpack during packaging and we only use Vite during development, For compatibility we will create a “soft link” for public/index.html and put it in the root directory of the project so that we can take care of both Vite and Webpack at the same time. SRC =” XXX “can cause more problems, for example, using soft links instead of copy files, in order to modify public/index.html can also affect index.html soft links

  • symlink-index-html.ts

    import fs from 'fs'
    import path from 'path'
    import { Plugin } from 'vite'
    import template from 'lodash.template'
    
    export function symlinkIndexHtml(options: {
      //HTML read path, usually ispublic/index.html
      template: string
      //Compatible with HTML -webpack-plugin compiler injection templateDate? : Record<string, unknown>
      //Js file entry in index.html? :string
    }) :Plugin {
      const rootIndexHtml = path.join(process.cwd(), 'index.html')
    
      try {
        fs.unlinkSync(rootIndexHtml)
      } catch (error) { }
    
      if(! fs.existsSync(rootIndexHtml)) {// Generate index.html soft link
        fs.symlinkSync(options.template, rootIndexHtml)
      }
    
      return {
        name: 'vite-plugin-vue2-compatible:symlinkIndexHtml'.transformIndexHtml(html) {
          let indexHtml = html
          const entry = options.entry || '/src/main.js'
    
          try {
            const compiled = template(indexHtml, { interpolate: /<%=([\s\S]+?) %>/g })
            // Inject the html-webpack-plugin variable
            indexHtml = compiled(options.templateDate)
            // Specify the SRC entry
            indexHtml = indexHtml.split('\n')
              .map(line= > line.includes('</body>')?`    <script type="module" src="${entry}"></script>
          ${line}`
                : line
              )
              .join('\n')}catch (error) {
            indexHtml = `<h2>${error}</h2>`
          }
    
          return indexHtml
        },
      }
    }
    
    Copy the code

Convert @import ~ alias

  • gonzales-pe css ASTtool
  • node-source-walk css ASTTraverse the tool
  • style-import.ts
    import path from 'path'
    import { Plugin } from 'vite'
    import { convertVueFile } from './utils'
    import Walker from 'node-source-walk'
    import gonzales from 'gonzales-pe'
    
    export function styleImport(options? : Record<string, unknown>) :Plugin {
      const walker = new Walker as any
      // check the @import statement
      const isImportStatement = (node) = > {
        if(node.type ! = ='atrule') { return false }
        if(! node.content.length || node.content[0].type ! = ='atkeyword') { return false }
        const atKeyword = node.content[0]
        if(! atKeyword.content.length) {return false }
        const importKeyword = atKeyword.content[0]
        if(importKeyword.type ! = ='ident'|| importKeyword.content ! = ='import') { return false }
        return true
      }
      // Remove the quotes around the string
      const extractDependencies = (importStatementNode) = > {
        return importStatementNode.content
          .filter(function (innerNode) {
            return innerNode.type === 'string' || innerNode.type === 'ident'
          })
          .map(function (identifierNode) {
            return identifierNode.content.replace(/["']/g.' ')})}return {
        enforce: 'pre'.name: 'vite-plugin-vue2-compatible:styleImport'.transform(code, id) {
          if(! id.endsWith('.vue')) return
          let _code = code
    
          try {
            // Add all @import statements
            const imports = convertVueFile(code).styles.reduce((dependencies, cur) = > {
              const ast = (gonzales as any).parse(cur.content, { syntax: cur.lang })
              let deps = dependencies
              walker.walk(ast, (node: any) = > {
                if(! isImportStatement(node))return
                deps = deps.concat(extractDependencies(node))
              })
              return deps
            }, [])
    
            // Convert the ~ alias in the @import statement
            for (const importPath of imports) {
              if (importPath.startsWith('~')) {
                const node_modules = path.join(process.cwd(), 'node_modules')
                const targetPath = path.join(
                  path.relative(path.parse(id).dir, node_modules),
                  importPath.slice(1),// Replace alias '~' to 'node_modules'
                _code = _code.replace(importPath, targetPath)
              }
            }
            return _code
          } catch (error) {
            throw error
          }
        },
      }
    }
    Copy the code

conversionimport('@/pages/xxxx')

  • So this is kind of a tricky one, and there are two things to think about

    1. @This alias replacement —- vite reported an error
    2. xxxxDynamic path analysis —- vite warning
  • Realize the principle of

    1. impot('@/pages/' + path)It essentially enumerates all the files under Pages and generates oneswitchTo provide matching

    If the directory structure is as follows:

    src
      pages
        foo.vue
        bar/index.vue
    Copy the code

    Will generate:

    function __variableDynamicImportRuntime__(path) {
      switch (path) {
        case '.. /pages/foo': return import('.. /pages/foo.vue');
        case '.. /pages/foo.vue': return import('.. /pages/foo.vue');
          break;
        case '.. /pages/bar': return import('.. /pages/bar/index.vue');
        case '.. /pages/bar/index': return import('.. /pages/bar/index.vue');
        case '.. /pages/bar/index.vue': return import('.. /pages/bar/index.vue');
          break; }}Copy the code
    1. Refer to the link

    dynamic-import-vars

  • The dynamic-import code is a bit long, the full code is linked to github

require to import

  • The problem is CommonJs to ESModule, NPM found several packages do not implement my function (either do not convert, or inject environment variables error); Simply wrote a simplified version of their own, but also to broaden their technical line (can not eat ready-made, will do their own is not)

  • Technology selection

    1. acornJs Abstract Syntax tree (AST) tool
    2. acorn-walkSyntax tree traversal tool
  • Realize the principle of

    1. Let’s use Acorn to convert the code toAST
    2. Use acorn-walk to traverseASTParse out the file that require loads and convert it to import format
  • Js-esm code is a bit long, full code github link

  • Write a vite-plugin-commonJS based on CJS-ESM

    If there is code below

    const pkg = require('.. /package.json');
    
    const routers = [{
      path: '/foo'.component: require('@/pages/foo.vue').default;
    }];
    Copy the code

    Will generate:

    import pkg  from ".. /package.json";
    import _MODULE_default___EXPRESSION_object__ from "@/pages/foo.vue";
    
    const routers = [{
      path: '/foo'.component: _MODULE_default___EXPRESSION_object__;
    }];
    Copy the code

Finally, we packaged all the plug-ins into an NPM package

  • Complete code github link
  • Add in the project root directoryvite.config.ts

Note: The following configuration may need to be adjusted to suit your project

import path from 'path'
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import {
  vitePluginCommonjs,
  dynamicImport,
  styleImport,
  symlinkIndexHtml,
} from 'vite-plugin-vue2-compatible'
import pkg from './package.json'

export default defineConfig({
  plugins: [
    / * * *@Repository https://github.com/underfin/vite-plugin-vue2
     */
    createVuePlugin({
      jsx: true.jsxOptions: {
        compositionAPI: true,}}),/** * webpack project require */
    vitePluginCommonjs(),
    /** * compatible with import('@xxxx') for alias */
    dynamicImport(),
    / * * * compatible@import alias
     *   @import '~ the normalize. CSS/normalize. CSS' * left *@import 'node_modules/normalize.css/normalize.css'
     */
    styleImport(),
    / * * *@vue/cli project static file entry is public/index.html * vite project is in the root directory index.html as the entry for everything * symlinkIndexHtml will create a public/index.html file in the root directory Index.html soft link */
    symlinkIndexHtml({
      // Point to the @vue/cli creation project's public/index.html file
      template: path.join(__dirname, 'public/index.html'),
      // Tell vite entry file index.html which js to load; Both entry in the WebPack configuration
      entry: '/src/main.js'.// Compatible with compile injection in htML-webpack-plugin
      templateDate: {
        BASE_URL: '/'.htmlWebpackPlugin: {
          options: {
            title: pkg.name,
          },
        },
      },
    }),
  ],
  resolve: {
    alias: {
      // Same as alias in webpack
      The '@': path.join(__dirname, './src'),},// Same as extensions in Webpack
    extensions: ['.vue'.'.ts'.'.tsx'.'.js'.'.jsx'.'.mjs'],},define: {
    / / with webpack DefinePlugin
    'process.env': process.env,
  }
})
Copy the code

run

  1. npm i -D vite vite-plugin-vue2 vite-plugin-vue2-compatible
  2. Add the packge.json scripts command
{
  "scripts": {
+ "vite": "export NODE_ENV=development; vite"}}Copy the code
  1. npm run vite

πŸŽ‰ Boom shakalaka!!!